react navigation in react native, with conditional stack, and authentication - react-native

import React, { Component } from 'react'
import { Container, Content, Form, Item, Input, Label, Button,Text, Icon} from 'native-base'
import AsyncStorage from '#react-native-community/async-storage';
import authStore from '../store/authStore';
export default class Login extends Component {
constructor(props){
super(props);
this.state={
email:'',
password:''
}
}
handleLogin = async () =>{
let requestObject = {
email: this.state.email,
password: this.state.password
}
authStore.userLogin(requestObject, response => {
this.storeUserData(response.data.data);
this.props.navigation.navigate('Home');
})
}
storeUserData = async (value) => {
try {
const jsonValue = JSON.stringify(value)
await AsyncStorage.setItem('#userData', jsonValue)
} catch (e) {
console.log(e);
}
}
render() {
return (
<Container>
<Content contentContainerStyle={{flex: 1, justifyContent:'center'}}>
<Form style={{margin:10}}>
<Item rounded last style={{margin:10}}>
<Icon active type="FontAwesome" name='user' />
<Input placeholder='User Name'
onChangeText={(email)=>this.setState({email})}
value={this.state.email}/>
</Item>
<Item rounded last style={{margin:10}}>
<Icon active type="FontAwesome" name='key' />
<Input placeholder='Password'
secureTextEntry
onChangeText={(password)=>this.setState({password})}
value={this.state.password}/>
</Item>
<Button rounded block style={{margin:10}} onPress={() => this.handleLogin()}>
<Text>Sign-In</Text>
</Button>
</Form>
</Content>
</Container>
)
}
}
const AuthStack = createStackNavigator();
AuthStackScreen = () =>
<AuthStack.Navigator>
<AuthStack.Screen name="Login" component={Login} />
</AuthStack.Navigator>
HomeStackScreen = () =>
<HomeStackDrawer.Navigator>
<HomeStackDrawer.Screen name="Home" component={HomeScreen}/>
<HomeStackDrawer.Screen name="Form" component={FormScreen}/>
<HomeStackDrawer.Screen name="Logout" component={Logout}/>
</HomeStackDrawer.Navigator>
export default class App extends Component{
constructor(props){
super(props);
this.state={
isloggedIn:false
}
this.loginStatusCheck();
}
loginStatusCheck = async () =>{
const userToken = await AsyncStorage.getItem('#accessToken');
if (userToken) {
this.setState({isloggedIn:true})
} else {
this.setState({isloggedIn:false})
}
}
render(){
return(
<NavigationContainer>
{this.state.isloggedIn ? <HomeStackScreen/> : <AuthStackScreen/>}
</NavigationContainer>
)
}
}
This is my App.js, I am checking if the user is logged in or not, then loading the Navigation stack accordingly. I know the problem, If I Logout, I want to navigate to the sign-in component, but this.props.navigation.navigate('Login') gives error. because I am not returning the Login route. How to solve this issue? Also, when I Log in same issue, as the Login is not present in the stack.
Thank you in advance
Included the login component

You will have to do some changes to fix this issue. Your problem is you are trying to access a screen in a navigation stack which is not there.
And the biggest problem is using a state variable in App.js to handle the switch of navigation stacks. You can resolve this by maintaining the login status in a context in your application. You can update it from other screens as well. Once you update the login status you dont have to worry about the navigation and your condition in the App.js will manage that for you.
The code should be something like below. I have given a sample Login component which will update the context. You will have to switch to functional component. From your code i dont see any problem of doing that.
const AppContext = createContext({
isloggedIn: {},
setLoggedIn: () => {},
});
const Login = () => {
const { setLoggedIn } = useContext(AppContext);
return (
<View>
<Button onPress={() => setLoggedIn(true)} />
</View>
);
};
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
isloggedIn: false,
loading: true,
};
this.loginStatusCheck();
}
setLoggedIn = value => {
this.setState({ isloggedIn: value });
};
loginStatusCheck = async () => {
const userToken = await AsyncStorage.getItem('#accessToken');
if (userToken) {
this.setState({ isloggedIn: true, loading: false });
} else {
this.setState({ isloggedIn: false, loading: false });
}
};
render() {
if (this.state.loading) return <ActivityIndicator />;
return (
<AppContext.Provider
value={{
isloggedIn: this.state.isloggedIn,
setLoggedIn: this.setLoggedIn,
}}>
<NavigationContainer>
{this.state.isloggedIn ? <HomeStackScreen /> : <AuthStackScreen />}
</NavigationContainer>
</AppContext.Provider>
);
}
}
Hope this helps.

Related

React Native WebView App not exit on pressing back button

React Native WebView App not exit on pressing back button after setting Go back functionality on back button pressed. I want go back functionality on pressing back button when webview is not on home page and when webview is on home page then exit the app.
export default class WebView extends Component {
constructor (props) {
super(props);
this.WEBVIEW_REF = React.createRef();
}
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.handleBackButton);
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackButton);
}
handleBackButton = ()=>{
this.WEBVIEW_REF.current.goBack();
return true;
}
onNavigationStateChange(navState) {
this.setState({
canGoBack: navState.canGoBack
});
}
render(){
return (
<WebView
source={{ uri: 'https://stackoverflow.com' }}
ref={this.WEBVIEW_REF}
onNavigationStateChange={this.onNavigationStateChange.bind(this)}
/>
);
}
}
Since you are managing the state of canGoBack inside onNavigationStateChange function, Change your handleBackButton function as below,
handleBackButton = () => {
if (this.state.canGoBack) {
this.WEBVIEW_REF.current.goBack();
return true;
}
}
Check below complete example
import React, { Component } from "react";
import { BackHandler } from "react-native";
import { WebView } from "react-native-webview";
export default class App extends Component {
WEBVIEW_REF = React.createRef();
state = {
canGoBack: false,
};
componentDidMount() {
BackHandler.addEventListener("hardwareBackPress", this.handleBackButton);
}
componentWillUnmount() {
BackHandler.removeEventListener("hardwareBackPress", this.handleBackButton);
}
handleBackButton = () => {
if (this.state.canGoBack) {
this.WEBVIEW_REF.current.goBack();
return true;
}
};
onNavigationStateChange = (navState) => {
this.setState({
canGoBack: navState.canGoBack,
});
};
render() {
return (
<WebView
source={{ uri: "https://stackoverflow.com" }}
ref={this.WEBVIEW_REF}
onNavigationStateChange={this.onNavigationStateChange}
/>
);
}
}
Hope this helps you. Feel free for doubts.
I had this problem for quite a while but i have managed to resolve it. Problem that I experienced was that goBack (which is used as back event) function was triggered before onNavigationStateChange but somehow state was change although goBack function was called first.
const HomeScreen = () => {
const {web} = config;
const ref = useRef();
const [canGoBack, setCanGoBack] = useState(false);
const setupState = event => {
setCanGoBack(event?.canGoBack);
};
useEffect(() => {
const goBack = () => {
if (canGoBack === false) {
Alert.alert(
'Exit App',
'Do you want to exit app?',
[
{text: 'No', onPress: () => console.log('No'), style: 'cancel'},
{text: 'Yes', onPress: () => BackHandler?.exitApp()},
],
{cancelable: false},
);
}
ref?.current?.goBack();
return true;
};
BackHandler?.addEventListener('hardwareBackPress', () => goBack());
return () =>
BackHandler?.removeEventListener('hardwareBackPress', () => goBack());
}, [canGoBack]);
return (
<View style={styles.mainContainer}>
{/* last version 11.21.1 */}
<WebView
ref={ref}
source={{uri: web?.url}}
style={{flex: 1}}
cacheEnabled={web.cacheEnabled}
automaticallyAdjustContentInsets={false}
domStorageEnabled={true}
startInLoadingState={true}
allowsInlineMediaPlayback={true}
allowsBackForwardNavigationGestures
onNavigationStateChange={e => setupState(e)}
/>
</View>
);
};
export default HomeScreen;

TypeError : props.navigation.getParam is not a function. In(props.navigation.getParam('name')

I am facing issue on TypeError : props.navigation.getParam is not a function. In (props.navigation.getParam('name'). I am using reactNavigation version 5.x. this code is working in reactNavigation 3. What am I doing wrong?
Here is my code
export default class ChatScreen extends Component {
static navigationOption = ({ navigation }) => {
return {
title: navigation.getParam('name', null)
}
}
constructor(props) {
super(props);
this.state = {
person:{
name:props.navigation.getParam('name'),
phone:props.navigation.getParam('phone'),
// name:'Raushan',
// phone:9931428888
},
textMessage: ''
};
}
Error in state section value.
Stack navigator
`
const Stack = createStackNavigator();
function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Auth">
<Stack.Screen name="AuthLoading" component={AuthLoadingScreen} />
<Stack.Screen name="App" component={HomeScreen} options={{ title: 'Chats' }}/>
<Stack.Screen name="Chat" component={ChatScreen} options={({ route }) => ({ title: route.params.name })}/>
<Stack.Screen name="Auth" component={LoginScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
export default App;
`
and Navigate screen
onPress={()=>this.props.navigation.navigate('Chat',item)}
use props.route.params.name this is working
this.state = {
person:{
name: props.route.params.name,
phone: props.route.params.phone,
},
textMessage: ''
};
Mostly you are using the latest version of React-Navigation with the same old setup.
So, You gonna pass your custom param as the second argument in the navigation function that I pointed HERE:
// Index SCREEN:=>
const IndexScreen = ({ navigation: { navigate } }) => {
return (
<View style={Styles.container}>
<FlatList
data={state}
keyExtractor={(item) => `${item.id}`}
renderItem={({ item }) => (
<TouchableOpacity
style={{ ...Styles.singleBlog, ...Styles.rowing }}
onPress={() => navigate('Previewing', { id: item.id })} // HERE
>
<Text style={Styles.blogTitle}>{`${item.title}`}</Text>
</TouchableOpacity>
)}
/>
</View>
);
};
Then you will be able to extract the id wirth a new function called params
like so:
// PREVIEWING SCREEN:=>
const PreviewScreen = ({ route: { params } }) => {
const { state } = useContext(Context);
const { id } = params;
return (
<View style={Styles.container}>
<Text>preview post with id {id}</Text>
</View>
);
};
As of version 6.x you need to use route.
function Index({ navigation }) {
return (
<View>
<Button
title="Details"
onPress={() => {
navigation.navigate('Details', {
itemId: abcdef
});
}}
/>
</View>
);
}
function DetailsScreen({ route, navigation }) {
const { itemId } = route.params;
// console.log(itemId);
// abcdef
return (
// Content...
);
}
Source: https://reactnavigation.org/docs/params
Yes I found that in version2, 3, 4 of navigation using getParam.
Link is as follows: https://reactnavigation.org/docs/2.x/params/
when using version5 of navigation using props.navigation.route.param as per documentation.
https://reactnavigation.org/docs/params/ - version5.
In class component props can be access using this keyword. So try this :
export default class ChatScreen extends Component {
static navigationOption = ({ navigation }) => {
return {
title: navigation.getParam('name', null)
}
}
constructor(props) {
super(props);
this.state = {
person: {
name: this.props.navigation.getParam('name'), // access with this.
phone: this.props.navigation.getParam('phone'), //access with this.
// name:'Raushan',
// phone:9931428888
},
textMessage: ''
};
}
}

How to set other first screen when I am logged in

How can I choose according value in AsyncStorage which screen should be displayed? I don't know why setting screen value 'Home' to InitialScreen variable doesn't work?
Once I log in login.js screen and I close app, after launching the app again I am navigated to login.js. But now I want to go to home.js screen.
Parent's file routes.js:
let InitialScreen
const RoutesNavigation = StackNavigator({
Login: { screen: Login },
Home: { screen: Home }
}, {
initialRouteName: InitialScreen,
navigationOptions: {
header: false,
}
});
export default class App extends Component {
constructor(props) {
super(props);
value = AsyncStorage.getItem('name');
if (value !== null) {
InitialScreen = 'Home'; //This doesn't change Initial screen!!!
console.log("JJJJJJJJJJJJJJJJJJ routes.js value !== null ");
}
}
render() {
return (
<RoutesNavigation />
);
}
}
This is login.js, where I store value from received json:
export default class Login extends Component {
constructor(props) {
super(props);
this.state = {
username: '',
password: '',
}
}
render() {
return (
<View style={styles.container}>
<TextInput
style={styles.textInput} placeholder='Username'
onChangeText={(username) => this.setState({ username })}
underlineColorAndroid='transparent'
/>
<TextInput
style={styles.textInput} placeholder='Password'
onChangeText={(password) => this.setState({ password })}
secureTextEntry={true}
underlineColorAndroid='transparent'
/>
<TouchableOpacity
style={styles.btn}
onPress={this.login}>
<Text>Log in</Text>
</TouchableOpacity>
</View>
);
}
login = () => {
var formData = new FormData();
formData.append('userName', this.state.username);
formData.append('password', this.state.password);
fetch('http://....', {
method: 'POST',
body: formData
})
.then((response) => response.json())
.then((responseJson) => {
console.log("JJJJJJJJJJJJJJJJJJJJJJJJJ name: " + responseJson.name);
AsyncStorage.setItem('name', responseJson.name);
this.props.navigation.navigate('Home');
})
.catch(() => {
console.log("JJJJJJJJJJJJJJJJJJ Wrong connection");
alert('Wrong connection');
})
}
}
This is home.js:
export default class Home extends Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.text}> Member area. You are logged in. </Text>
<TouchableOpacity
style={styles.btn}
onPress={this.logout}>
<Text>Log out</Text>
</TouchableOpacity>
</View>
);
}
logout = () => {
AsyncStorage.removeItem('name');
this.props.navigation.navigate('Login');
console.log("JJJJJJJJJJJJJJJJJJ Logged out");
}
}
Create your navigator in here:
value = AsyncStorage.getItem('name');
if (value !== null) {
InitialScreen = 'Home';
const RoutesNavigation = StackNavigator({
Login: { screen: Login },
Home: { screen: Home }
},{
initialRouteName: InitialScreen,
navigationOptions: {
header: false,
}
});
}
Because you are creating your navigator at the top with empty initial route but you are changing value in here so you must create here.
Hope it will work.
AsyncStorage is async.Because of the js nature thread won't wait result of this
AsyncStorage.getItem('name');
use callback with getItem
AsyncStorage.getItem('name',(error,result) => {
if (result!== null) {
//do something
}
});

Shoutem fetch data not displaying

First I want to start by saying I am a total noob at React Native and Shoutem. I am not sure if I should write Shoutem or React Native in the subject of my questions, but here is my problem.
I have two screens:
Screen1.js
Screen2.js
Screen1 displays a list of items returned from a fetch. Once I click on the item, it will open the second screen which is the details screen.
I am passing data from screen1 to screen2. In screen2 I need to make another fetch call for different data, but it does not work. I am doing exactly the same thing on both screens.
Here is my code for Screen1:
import React, {
Component
} from 'react';
import {
ActivityIndicator,
TouchableOpacity
} from 'react-native';
import {
View,
ListView,
Text,
Image,
Tile,
Title,
Subtitle,
Overlay,
Screen
} from '#shoutem/ui';
import {
NavigationBar
} from '#shoutem/ui/navigation';
import {
navigateTo
} from '#shoutem/core/navigation';
import {
ext
} from '../extension';
import {
connect
} from 'react-redux';
export class List extends Component {
constructor(props) {
super(props);
this.renderRow = this.renderRow.bind(this);
this.state = {
isLoading: true,
content: null,
}
}
componentDidMount() {
return fetch('https://www.cannabisreports.com/api/v1.0/strains').then((response) => response.json()).then((responseData) => {
this.setState({
isLoading: false,
content: responseData
});
}).done();
}
renderRow(rowData) {
const { navigateTo } = this.props;
return (
//<Text>{rowData.name}, {rowData.createdAt.datetime}</Text>
<TouchableOpacity onPress={() => navigateTo({
screen: ext('Strain'),
props: { rowData }
})}>
<Image styleName="large-banner" source={{ uri: rowData.image &&
rowData.image ? rowData.image : undefined }}>
<Tile>
<Title>{rowData.name}</Title>
<Subtitle>none</Subtitle>
</Tile>
</Image>
</TouchableOpacity>
);
}
render() {
if (this.state.isLoading) {
return (
<View style={{flex: 1, paddingTop: 20}}>
<ActivityIndicator />
</View>
);
}
return (
<View style={{flex: 1, paddingTop: 20}}>
<ListView
data={this.state.content.data}
renderRow={rowData => this.renderRow(rowData)}
/>
</View>
);
}
}
// connect screen to redux store
export default connect(
undefined,
{ navigateTo }
)(List);
I am passing rowData to Screen2. I then need to make another fetch calling using data from rowData as it is a path parameter needed for the API call in Screen2.
So basically I need to make a fetch call in Screen2 like this:
fetch('https://mywebsite.com/'+rowData.something+'/myotherdata')
.then((response) => response.json())
.then((responseJson) => {
this.setState({
content: responseJson.data
})
})
.catch((error) => {
console.error(error);
});
Here is my code for screen2:
export default class Strain extends Component {
constructor(props) {
super(props);
this.state = {
content: null,
}
}
componentDidMount() {
return fetch('https://mywebsite.com/'+rowData.something+'/myotherdata')
.then((response) => response.json())
.then((responseJson) => {
this.setState({
content: responseJson.data
})
})
.catch((error) => {
console.error(error);
});
}
renderRow(dataContent) {
return (
<Text>{dataContent.name}</Text>
// This does not work either -> <Text>{dataContent}</Text>
);
}
render() {
const { rowData } = this.props; //This is coming from screen1.js
return (
<ScrollView style = {{marginTop:-70}}>
<Image styleName="large-portrait" source={{ uri: rowData.image &&
rowData.image ? rowData.image : undefined }}>
<Tile>
<Title>{rowData.name}</Title>
<Subtitle>{rowData.createdAt.datetime}</Subtitle>
</Tile>
</Image>
<Row>
<Text>Seed Company: {rowData.seedCompany.name}</Text>
</Row>
<Divider styleName="line" />
<Row>
<Icon name="laptop" />
<View styleName="vertical">
<Subtitle>Visit webpage</Subtitle>
<Text>{rowData.url}</Text>
</View>
<Icon name="right-arrow" />
</Row>
<Divider styleName="line" />
<View style={{flex: 1, paddingTop: 20}}>
<ListView
data={content}
renderRow={dataContent => this.renderRow(dataContent)}
/>
</View>
<Divider styleName="line" />
</ScrollView>
);
}
}
My fetch URL returns data like this:
{
data: {
name: "7.5833",
another_name: "8.6000",
different_name: "5.7500",
}
}
This only returns one data object like what you see above.
When I run the code I get this error:
null is not an object (evaluating 'Object.keys(e[t])')
Please let me know if you need me to provide more info.
I have tried so many different things and nothing seems to work so I am in need of some help. What am I doing wrong?
Not sure why this works but it does.
I used a function to fetch my data and then call the function in componentDidMount like this:
getData() {
return fetch('https://mywebsite.com/myotherdata')
.then((response) => response.json())
.then((responseJson) => {
this.setState({
data: responseJson.data
});
})
.catch((error) => {
console.error(error);
});
}
componentDidMount() {
this.getData();
}
Then to get the values from the JSON response I am doing this:
this.state.data.name
I am having another issue, but I will create another question.

React Native Navigation - Action using Component's State

I've made a full-screen TextInput and would like to have an action performed when the Post button in the NavigationBar is pressed. However, because I have to make the method that the Button is calling in the onPress prop a static method, I don't have access to the state.
Here is my current code, and the state comes up undefined in the console.log.
import React, { Component } from 'react';
import { Button, ScrollView, TextInput, View } from 'react-native';
import styles from './styles';
export default class AddComment extends Component {
static navigationOptions = ({ navigation }) => {
return {
title: 'Add Comment',
headerRight: (
<Button
title='Post'
onPress={() => AddComment.postComment() }
/>
),
};
};
constructor(props) {
super(props);
this.state = {
post: 'Default Text',
}
}
static postComment() {
console.log('Here is the state: ', this.state);
}
render() {
return (
<View onLayout={(ev) => {
var fullHeight = ev.nativeEvent.layout.height - 80;
this.setState({ height: fullHeight, fullHeight: fullHeight });
}}>
<ScrollView keyboardDismissMode='interactive'>
<TextInput
multiline={true}
style={styles.input}
onChangeText={(text) => {
this.state.post = text;
}}
defaultValue={this.state.post}
autoFocus={true}
/>
</ScrollView>
</View>
);
}
}
Any ideas how to accomplish what I'm looking for?
I see you've found the solution. For future readers:
Nonameolsson posted how to achieve this on Github:
In componentDidMount set the method as a param.
componentDidMount () {
this.props.navigation.setParams({ postComment: this.postComment })
}
And use it in your navigationOptions:
static navigationOptions = ({ navigation }) => {
const { params } = navigation.state
return {
title: 'Add Comment',
headerRight: (
<Button
title='Post'
onPress={() => params.postComment()}
/>
),
};
};
Kinda like a hack but i use the global variable method where we assign this to a variable call foo. Works for me.
let foo;
class App extends Component {
static navigationOptions = ({ navigation }) => {
return {
title: 'Add Comment',
headerRight: (
<Button
title='Post'
onPress={() => foo.postComment() } <- Use foo instead of this
/>
),
};
};
componentWillMount() {
foo = this;
}
render() {
return (<div>Don't be a foo</div>)
}
}