React Native : IOS app stuck at splash screen - ios

My IOS app stuck at splash screen, I can't debug because no log when app stuck at splash, it looks like app can't loading or something else which I don't know. This is my App.js :
import AppLoading from 'expo-app-loading';
import { Asset } from 'expo-asset';
import { createDrawerNavigator } from '#react-navigation/drawer';
const MyDrawer = createDrawerNavigator();
export default class App extends Component<Props> {
state = {
isReady: false,
};
async _cacheResourcesAsync() {
const images = [require('./src/img/logo_0.png')];
const cacheImages = images.map(image => {
return Asset.fromModule(image).downloadAsync();
});
return Promise.all(cacheImages);
}
render() {
console.log(this.state.isReady);
if (!this.state.isReady) {
return(
<AppLoading
startAsync={this._cacheResourcesAsync}
onFinish={() => this.setState({ isReady: true })}
onError={console.warn}
autoHideSplash={true}
/>
);
}
return (
<MyDrawer.Navigator initialRouteName="Main" drawerContentOptions={{
activeTintColor: '#FFF',
itemStyle: { marginVertical: 5 },
}}
drawerStyle={{
backgroundColor: '#ea2d49',
width: 240,
marginTop:'23%',
color: '#fff'
}}
>
<MyDrawer.Screen name="Home" component={StackScreen}/>
<MyDrawer.Screen name="Contact US" component={ContactStackScreen}/>
<MyDrawer.Screen name="Privacy Policy" component={PrivacyStackScreen}/>
</MyDrawer.Navigator>
);
}
}
I build with expo.
Thank you everyone!

Related

On Application Resume From Background To Foreground, App Restarts From First Navigation Screen

I have following navigation stack
const AppNavigator = createStackNavigator({
AppSplashScreen: AppSplashScreen,
LanguageScreen: LanguageScreen,
WalkthroughScreen: WalkthroughScreen,
LoginScreen: LoginScreen,
ForgotPasswordScreen: ForgotPasswordScreen,
ResetPasswordScreen: ResetPasswordScreen,
RegistrationTypeScreen: RegistrationTypeScreen,
RegistrationFormScreen: RegistrationFormScreen,
OTPConfirmationScreen: OTPConfirmationScreen,
BottomTabNavigator: BottomTabNavigator
}, {
headerMode: 'none',
cardStyle: { backgroundColor: '#000000' },
});
const AppContainer = createAppContainer(AppNavigator);
export default App;
I am displaying splash screen video when the first app opens.
Here is what my AppSplashScreen looks like
import React, { Component } from 'react';
import { View } from 'react-native';
import SplashScreen from 'react-native-splash-screen';
import Video from 'react-native-video';
import { VIDEO_SPLASH_2 } from '../assets/videos/index';
export default class AppSplashScreen extends Component {
state = {
displayVideoPlayer: true,
firstLaunch: false
}
componentDidMount() {
SplashScreen.hide();
}
componentWillUnmount() {
this.setState({
displayVideoPlayer: false
});
}
isFirstLaunch() {
let firstLaunch = true;
if (true === storage.get('APP_ALREADY_LAUNCHED')) {
firstLaunch = false;
} else {
storage.set('APP_ALREADY_LAUNCHED', true);
firstLaunch = true;
}
return firstLaunch;
}
didCompleteVideoPlayback() {
if (true === this.state.displayVideoPlayer) {
this.setState({
displayVideoPlayer: false
});
}
const currentRouteName = this.props.navigation.state.routeName;
if ('AppSplashScreen' !== currentRouteName) {
return false;
}
if (true === global.SKIP_SPLASH_SCREEN_REDIRECT) {
return false;
}
if (this.isFirstLaunch()) {
this.props.navigation.navigate('LanguageScreen');
return false;
}
this.props.navigation.navigate('HomeScreen');
}
render() {
return (
<View style={{flex: 1, backgroundColor: '#000000', alignItems: 'center', justifyContent: 'center'}}>
{true === this.state.displayVideoPlayer &&
<Video
source={VIDEO_SPLASH_2}
muted={true}
repeat={false}
playInBackground={false}
resizeMode="contain"
onEnd={() => this.didCompleteVideoPlayback()}
style={{height: '100%', width: '100%', backgroundColor: '#000000'}}
/>
}
</View>
);
}
}
My issue is, whenever I put the application in Background, and resume after 30 seconds, it always starts with AppSplashScreen whereas I expect it to resume from the last screen. It works correctly if I open it before 30 seconds. I assume somewhere it is killing the memory and starting the app from start when I resume after 30 second.
What could be the issue here. Or is there another workaround to resume the app in the same screen where the user left off.
I solved it by using State Persistence of react-navigation
Here is the documentation https://reactnavigation.org/docs/4.x/state-persistence/
Here is what my App.js look like now
import AsyncStorage from '#react-native-community/async-storage';
const App: () => React$Node = () => {
const persistenceKey = "navigationStatePersistenceKey"
const persistNavigationState = async (navState) => {
try {
await AsyncStorage.setItem(persistenceKey, JSON.stringify(navState));
} catch(err) {
// handle error
}
}
const loadNavigationState = async () => {
const jsonString = await AsyncStorage.getItem(persistenceKey);
return JSON.parse(jsonString);
}
return(
<View style={{flex: 1, backgroundColor: '#000000'}}>
<AppContainer
persistNavigationState={persistNavigationState}
loadNavigationState={loadNavigationState}
/>
</View>
);
};
It now takes user to the same screen where it was left off, no more restart from first screen.

Google SignIn for IOS with React-native (expo)

I'm setting up a page where I can Id with a Google account. This is my code:
import React from "react"
import { StyleSheet, Text, View, Image, Button } from "react-native"
import * as Google from "expo-google-app-auth";
export default class App extends React.Component {
constructor(props) {
super(props)
this.state = {
signedIn: false,
name: "",
photoUrl: ""
}
}
signIn = async () => {
try {
const result = await Google.logInAsync({
androidClientId:
"1051966217196.apps.googleusercontent.com",
iosClientId:
"1051966217196.apps.googleusercontent.com",
scopes: ["profile", "email"]
})
if (result.type === "success") {
this.setState({
signedIn: true,
name: result.user.name,
photoUrl: result.user.photoUrl
})
} else {
console.log("cancelled")
}
} catch (e) {
console.log("error", e)
}
}
render() {
return (
<View style={styles.container}>
{this.state.signedIn ? (
<LoggedInPage name={this.state.name} photoUrl={this.state.photoUrl} />
) : (
<LoginPage signIn={this.signIn} />
)}
</View>
)
}
}
const LoginPage = props => {
return (
<View>
<Text style={styles.header}>Sign In With Google</Text>
<Button title="Sign in with Google" onPress={() => props.signIn()} />
</View>
)
}
const LoggedInPage = props => {
return (
<View style={styles.container}>
<Text style={styles.header}>Welcome:{props.name}</Text>
<Image style={styles.image} source={{ uri: props.photoUrl }} />
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center"
},
header: {
fontSize: 25
},
image: {
marginTop: 15,
width: 150,
height: 150,
borderColor: "rgba(0,0,0,0.2)",
borderWidth: 3,
borderRadius: 150
}
})
On Android it's ok, everything is fine but in IOS I have that mistake :
Google 400 - That's an error - Error : redirect_uri_mismatch
Where is it goes wrong? I'm new to react native so I need detailed explanations.
I changed the bundle identifier to host.exp.exponent in Google credentials and it works. It's weird because I didn't change it in app.json. But it works.

React-Native navigation.addListener is not a functio

Is there a way I can fix this without using redux?
This is only happening on iOS, on android the AddListener works perfectly fine without it.
I have a component and I call the props.navigation.addListener on the componentDidMount functon.
Some code to help understand exactly where it breaks:
componentDidMount(){
var _this = this;
this.willBlurListener = this.props.navigation.addListener('willBlur', () => {
_this.timer.clearTimeout();
});
this.willFocusListener = this.props.navigation.addListener('willFocus', () => {
_this._action();
});
AppState.addEventListener('change', this._handleAppStateChange);
}
And then I use the component like this:
<Inactivity name='SomeNameView' navigation={ this.props.navigation }>
{this.renderDetails()}
</Inactivity>
Can you please try to use withNavigation function, it returns a HOC that has navigation in it props so you don't have to pass from the parent component to the child:
I created a simple app that uses this concept that probably can help you:
import React from 'react';
import {
View,
Text,
Button,
} from 'react-native';
import {
createStackNavigator,
withNavigation,
} from 'react-navigation';
class SomeComponent extends React.Component {
componentDidMount() {
this.willBlurListener = this.props.navigation.addListener('willBlur', () => {
this.someAction();
})
}
someAction() {
console.log('Some action is called!');
}
componentWillUnmount() {
this.willBlurListener.remove();
}
render() {
return (
<View>
<Text>Some Component</Text>
<Button
title={'Open settings'}
onPress={() => this.props.navigation.navigate('Settings')}
/>
</View>
)
}
}
const SomeComponentWithNavigation = withNavigation(SomeComponent);
class HomeScreen extends React.Component {
static navigationOptions = {
title: 'Home'
}
render() {
return (
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<SomeComponentWithNavigation/>
<Text>Welcome to home screen!</Text>
</View>
)
}
}
class SettingsScreen extends React.Component {
static navigationOptions = {
title: 'Settings'
}
render() {
return (
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<Text>Welcome to settings screen!</Text>
</View>
)
}
}
export default createStackNavigator(
{
Home: HomeScreen,
Settings: SettingsScreen,
},
);
I have used import { useNavigation } from '#react-navigation/native'; to achieve this. This could work for you as well.
Sample code example
import { useNavigation } from '#react-navigation/native';
class CurrentOrderClass extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.onFocusSubscribe = this.props.navigation.addListener('focus', () => {
// Your code
});
}
componentWillUnmount() {
this.onFocusSubscribe();
}
.
.
.
.
function CurrentOrder(props) {
const navigation = useNavigation(props)
return <CurrentOrderClass {...props} navigation={navigation} />
}
}
export default CurrentOrder;
You can also check to React Native docs https://reactnavigation.org/docs/navigation-events/
I found this a bit tricky and after looking into it for a bit, I come up with the following solution. Note that is tested on React Navigation 5.x.
import { useIsDrawerOpen } from "#react-navigation/drawer";
let lastDrawerStateIsOpened = false;
const DrawerComponent = (props) => {
const isOpened = useIsDrawerOpen();
if (lastDrawerStateIsOpened != isOpened) {
lastDrawerStateIsOpened = isOpened;
if (isOpened) {
// Do what needs to be done when drawer is opened.
}
}
};
Also, note that I'm using a functional component.

E2E: Select an image from a UIImagePickerController with Wix Detox

Description
I need to write an e2e test that in some point it has to select an image in UIImagePickerController, I tried to use element(by.type('UIImagePickerController')). tapAtPoint() with no use. I need a way to select an image. I have found a way to do it with native tests.
Also mocking isn't an option for me since I use a higher version that the one react-native-repackeger needs.
Steps to Reproduce
Use with any application that uses image picker
Try to use element(by.type('UIImagePickerController')).tapAtPoint({ x: 50, y: 200 })
Detox, Node, Device, Xcode and macOS Versions
Detox: 6.0.2
Node: 8.9.0
Device: iOS Simulator 6s
Xcode: 9.2
macOS: 10.13.1
React-Native: 0.46.4
Device and verbose Detox logs
There's no logs, the device taps on the right location but the tap doesn't make an effect.
Noticed the original question stated that mocks were not an option in the case presented, but I came across this Stack Overflow question a few times in my searches for a solution and thought to share what I ultimately came up with for my situation.
I was able to get around the limitations for the e2e test by wrapping react-native-image-picker in my own export:
ImagePicker.js
import ImagePicker from 'react-native-image-picker';
export default ImagePicker;
And then creating a mock with a custom extension (i.e. e2e.js):
ImagePicker.e2e.js
const mockImageData = '/9j/4AAQSkZ...MORE BASE64 DATA OF CUTE KITTENS HERE.../9k=';
export default {
showImagePicker: function showImagePicker(options, callback) {
if (typeof options === 'function') {
callback = options;
}
callback({
data: mockImageData,
});
},
};
Finally, configure the metro bundler to prioritize your custom extension:
[project root]/rn-cli.config.js
const defaultSourceExts = require('metro-config/src/defaults/defaults')
.sourceExts;
module.exports = {
resolver: {
sourceExts: process.env.RN_SRC_EXT
? process.env.RN_SRC_EXT.split(',').concat(defaultSourceExts)
: defaultSourceExts,
},
};
Then run with the RN_SRC_EXT environment variable set to the custom extension:
RN_SRC_EXT=e2e.js react-native start
See the Detox Mocking Guide for more information.
Not sure if this is related, but for iOS 11 I can't even see those native view types in the Debug View Hierarchy.
For iOS 9 and 10 however, I would solve the problem like this:
it('select first image from camera roll', async () => {
// select a photo
await element(by.id('select_photo')).tap();
// Choose from Library...
await element(by.traits(['button']).and(by.type('_UIAlertControllerActionView'))).atIndex(1).tap();
// select Cemara Roll, use index 0 for Moments
await element(by.type('UITableViewCellContentView')).atIndex(1).tap();
// select first image
await element(by.type('PUPhotoView')).atIndex(0).tap();
});
There are probably many other possibilities to solve this problem with different native view types and accessibility traits.
I just used the example provided from react-native-image-picker to test with above code:
import React from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View,
PixelRatio,
TouchableOpacity,
Image,
} from 'react-native';
import ImagePicker from 'react-native-image-picker';
export default class App extends React.Component {
state = {
avatarSource: null,
videoSource: null
};
selectPhotoTapped() {
const options = {
quality: 1.0,
maxWidth: 500,
maxHeight: 500,
storageOptions: {
skipBackup: true
}
};
ImagePicker.showImagePicker(options, (response) => {
console.log('Response = ', response);
if (response.didCancel) {
console.log('User cancelled photo picker');
}
else if (response.error) {
console.log('ImagePicker Error: ', response.error);
}
else if (response.customButton) {
console.log('User tapped custom button: ', response.customButton);
}
else {
let source = { uri: response.uri };
// You can also display the image using data:
// let source = { uri: 'data:image/jpeg;base64,' + response.data };
this.setState({
avatarSource: source
});
}
});
}
selectVideoTapped() {
const options = {
title: 'Video Picker',
takePhotoButtonTitle: 'Take Video...',
mediaType: 'video',
videoQuality: 'medium'
};
ImagePicker.showImagePicker(options, (response) => {
console.log('Response = ', response);
if (response.didCancel) {
console.log('User cancelled video picker');
}
else if (response.error) {
console.log('ImagePicker Error: ', response.error);
}
else if (response.customButton) {
console.log('User tapped custom button: ', response.customButton);
}
else {
this.setState({
videoSource: response.uri
});
}
});
}
render() {
return (
<View style={styles.container}>
<TouchableOpacity testID="select_photo" onPress={this.selectPhotoTapped.bind(this)}>
<View style={[styles.avatar, styles.avatarContainer, {marginBottom: 20}]}>
{ this.state.avatarSource === null ? <Text>Select a Photo</Text> :
<Image style={styles.avatar} source={this.state.avatarSource} />
}
</View>
</TouchableOpacity>
<TouchableOpacity onPress={this.selectVideoTapped.bind(this)}>
<View style={[styles.avatar, styles.avatarContainer]}>
<Text>Select a Video</Text>
</View>
</TouchableOpacity>
{ this.state.videoSource &&
<Text style={{margin: 8, textAlign: 'center'}}>{this.state.videoSource}</Text>
}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF'
},
avatarContainer: {
borderColor: '#9B9B9B',
borderWidth: 1 / PixelRatio.get(),
justifyContent: 'center',
alignItems: 'center'
},
avatar: {
borderRadius: 75,
width: 150,
height: 150
}
});
AppRegistry.registerComponent('example', () => App);

React Native: Maximum Call Stack Size Exceeded Error After Adding Conditional

I am receiving there error "Maximum Call Stack Size Exceeded". After adding a conditional to the navigation file in my code. The stack I am using is React-Native with Expo, Redux, GraphQL and my navigation library is react-navigation. I am working on iOS simulator. This is the code that breaks the
app:
//navigations.js
if (!this.props.user.isAuthenticated) {
return <AuthenticationScreen />;
}
and this is the entirety of the navigations.js
import React, { Component } from "react";
import {
addNavigationHelpers,
StackNavigator,
TabNavigator
} from "react-navigation";
import { connect } from "react-redux";
import { FontAwesome } from "#expo/vector-icons";
import HomeScreen from "./screens/HomeScreen";
import ExploreScreen from "./screens/ExploreScreen";
import FavoritesScreen from "./screens/FavoritesScreen";
import ProfileScreen from "./screens/ProfileScreen";
import AuthenticationScreen from "./screens/AuthenticationScreen";
import { colors } from "./utils/constants";
const TAB_ICON_SIZE = 20;
const Tabs = TabNavigator(
{
Home: {
screen: HomeScreen,
navigationOptions: () => ({
headerTitle: "Saga",
tabBarIcon: ({ tintColor }) => (
<FontAwesome size={TAB_ICON_SIZE} color={tintColor} name="home" />
)
})
},
Explore: {
screen: ExploreScreen,
navigationOptions: () => ({
headerTitle: "Explore",
tabBarIcon: ({ tintColor }) => (
<FontAwesome size={TAB_ICON_SIZE} color={tintColor} name="search" />
)
})
},
Favorites: {
screen: FavoritesScreen,
navigationOptions: () => ({
headerTitle: "Favorites",
tabBarIcon: ({ tintColor }) => (
<FontAwesome
size={TAB_ICON_SIZE}
color={tintColor}
name="file-video-o"
/>
)
})
},
Profile: {
screen: ProfileScreen,
navigationOptions: () => ({
headerTitle: "Profile",
tabBarIcon: ({ tintColor }) => (
<FontAwesome size={TAB_ICON_SIZE} color={tintColor} name="user" />
)
})
}
},
{
lazy: true,
tabBarPosition: "bottom",
swipeEnabled: false,
tabBarOptions: {
showIcon: true,
showLabel: false,
activeTintColor: colors.PRIMARY,
inactiveTintColor: colors.LIGHT_GRAY,
style: {
backgroundColor: colors.BASE_GRAY,
height: 50,
paddingVertical: 5
}
}
}
);
const AppMainNav = StackNavigator(
{
Home: {
screen: Tabs
}
},
{
cardStyle: {
backgroundColor: "#F1F6FA"
},
navigationOptions: () => ({
headerStyle: {
backgroundColor: colors.BASE_GRAY
},
headerTitleStyle: {
fontWeight: "bold",
fontSize: 24,
color: colors.BLUE
}
})
}
);
class AppNavigator extends Component {
render() {
const nav = addNavigationHelpers({
dispatch: this.props.dispatch,
state: this.props.nav
});
if (!this.props.user.isAuthenticated) {
return <AuthenticationScreen />;
}
return <AppMainNav navigation={nav} />;
}
}
export default connect(state => ({
nav: state.nav,
user: state.user
}))(AppNavigator);
export const router = AppMainNav.router;
For good measure this is the user.js reducer:
const initialState = {
token: null,
isAuthenticated: false,
info: null
};
export default (state = initialState, action) => {
switch (action.type) {
default:
return state;
}
};
I can't see what would be causing an infinite loop.
Update #1
AuthenticationScreen.js (Minus Imports)
const Root = styled.View``;
const T = styled.Text``;
class AuthenticationScreen extends Component {
state = {};
render() {
return (
<Root>
<T>AuthenticationScreen</T>
</Root>
);
}
}
export default AuthenticationScreen;
As I understand it, you've isolated the problem to this code...
if (!this.props.user.isAuthenticated) {
return <AuthenticationScreen />;
}
return <AppMainNav navigation={nav} />;
And if you remove...
if (!this.props.user.isAuthenticated) {
return <AuthenticationScreen />;
}
it works fine, which means <AppMainNav> must be fine. So the infinite loop must be in your <AuthenticationScreen /> code.

Resources