I am new here and to Expo/RN. I’m trying to implement some basic authentication in my app with Azure ad to then make Graph API Calls. Right now I have a log in button that is displayed displayed on the WelcomeScreen, however when a user presses it and logs in (via AzureAd prompt), it takes them back to the Welcomescreen until they login again. Its like state isn’t being update or re-evaluated after they login. Am I missing something obvious with promptAsync?
App.js
export default function App() {
const [user, setUser] = useState(null);
WebBrowser.maybeCompleteAuthSession();
const discovery = useAutoDiscovery(
"placeholder"
);
// Request
const [request, response, promptAsync] = useAuthRequest(
{
clientId: "placeholder",
scopes: ["openid", "profile", "email", "offline_access"],
// For usage in managed apps using the proxy
redirectUri: makeRedirectUri({
// For usage in bare and standalone
native: "placeholder",
}),
responseType: ResponseType.Token,
},
discovery
);
const handleLogin = async () => {
await promptAsync();
if (response && response.type === "success") {
var userDecoded = jwtDecode(response.params.access_token);
setUser({
accessToken: response.params.access_token,
userDetails: userDecoded,
});
}
};
return (
<AuthContext.Provider value={{ user, handleLogin }}>
<NavigationContainer theme={navigationTheme}>
{user ? <AppNavigator /> : <AuthNavigator />}
</NavigationContainer>
</AuthContext.Provider>
);
}
(Where login button is)
WelcomeScreen.js
function WelcomeScreen({ navigation }) {
const authContext = useContext(AuthContext);
return (
<ImageBackground
style={styles.background}
source={require("../assets/background.jpg")}
blurRadius={4}
>
<View style={styles.logoContainer}>
<Image source={require("../assets/lo.png")} />
<Text style={styles.titleText}>ITS Inventory</Text>
</View>
<View style={styles.buttonsContainer}>
<AppButton
title={"Login Via Azure"}
style={styles.loginButton}
onPress={() => authContext.handleLogin()}
></AppButton>
</View>
</ImageBackground>
);
}
Thanks!!
Related
The left gif shows the flickering of the screen when the Switch is pressed and the right image shows the screen at the moment when the flickering happens. That means that my Stack.Screen from #react-navigation/native-stack, that displays the Settings page gets removed for a small moment when the Switch is pressed, which results in the flicker effect.
The function that gets called when the Switch is pressed is my toggleLocalAuth function. This is an asynchronous function, so my guess would be that the screen flickers when the toggleLocalAuth is running.
const toggleLocalAuth = async () => {
await SecureStorage.save('local-auth', JSON.stringify(!usesLocalAuth))
setUsesLocalAuth((usesLocalAuth) => !usesLocalAuth)
}
This is my Security component which contains the Switch and the "Security" subheader.
const Security: React.FC = () => {
const theme = useTheme()
const { usesLocalAuth, toggleLocalAuth } = useAuth()
return (
<Box>
<Text variant="subheader" paddingHorizontal="m" paddingBottom="xs">
{i18n.t('settings.security')}
</Text>
<Paper>
<Box
flexDirection="row"
alignItems="center"
justifyContent="space-between"
>
<Box flexDirection="row">
<Ionicons
name="finger-print"
size={24}
color={theme.colors.icon}
style={{ paddingRight: theme.spacing.m }}
/>
<Text variant="subtitle">
{Platform.OS === 'ios' || Platform.OS === 'macos'
? i18n.t('settings.touchId')
: i18n.t('settings.fingerprint')}
</Text>
</Box>
<Switch value={usesLocalAuth} onValueChange={toggleLocalAuth} />
</Box>
</Paper>
</Box>
)
}
This is my whole screen that is shown in the image, which is a component of a StackNavigator Screen from #react-navigation/native-stack.
const Settings = ({
navigation,
}: {
navigation: NativeStackNavigationProp<SessionParamList, 'Settings'>
}) => {
const theme = useTheme()
return (
<ScrollView style={{ paddingVertical: theme.spacing.m }}>
<Profile
onPersonalPress={() => navigation.push('PersonalData')}
onResidencePress={() => navigation.push('Residence')}
onContactPress={() => navigation.push('ContactInformation')}
/>
<Preferences />
<Security />
</ScrollView>
)
}
export default Settings
This is the StackNavigator Screen which holds the Settings page.
const Stack = createNativeStackNavigator<SessionParamList>()
const Tab = createBottomTabNavigator()
function SettingsStack() {
return (
<Stack.Navigator>
<Stack.Screen
options={{
...stackOptions,
headerTitle: i18n.t('settings.settings'),
}}
name="Settings"
component={Settings}
/>
</Stack.Navigator>
)
}
My guess is that the Settings Stack.Screen gets removed for a small moment when the async toggleLocalAuth function is called. The background and Tabbar do not flicker because they are defined as screenOptions on my Tab.Navigator which sits above the Settings page in the component tree.
Edit:
I use useContext and createContext to gloablly manage the authentication state.
export type ContextType = {
isAuthenticated: boolean
usesLocalAuth: boolean
jwt: string
logout: () => void
login: (jwt: string) => void
toggleLocalAuth: () => void
}
const AuthContext = createContext<ContextType>({
isAuthenticated: false,
jwt: '',
usesLocalAuth: false,
logout: () => console.warn('Not Auth provider above component'),
login: () => console.warn('Not Auth provider above component'),
toggleLocalAuth: () => console.warn('Not Auth provider above component'),
})
export const useAuth = () => useContext(AuthContext)
These are the relevant excerpts of my App.tsx file:
export default function App() {
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false)
const [usesLocalAuth, setUsesLocalAuth] = useState<boolean>(false)
const [jwt, setJwt] = useState('')
function logout() {
setIsAuthenticated(false)
setDid(null)
}
function login(jwt: string) {
setIsAuthenticated(true)
setJwt(jwt)
}
const authProvider = useMemo(
() => ({
isAuthenticated: isAuthenticated,
jwt: jwt,
usesLocalAuth: usesLocalAuth,
logout: logout,
login: login,
toggleLocalAuth: toggleLocalAuth,
}),
[isAuthenticated, usesLocalAuth, did, jwt, logout, login, toggleLocalAuth],
...
return (
<AuthContext.Provider value={authProvider}>
...
</AuthContext.Provider>
)
)
I'm developing an app in React Native in which I'm trying to display some data provided by a fake API I set up using json server. I'm using the useContext hook to handle the general state of the app and since I'm fairly new to React Native and React in general I need some help handling the response I'm manipulating through the context API.
This is the State file I set up in the context folder
import React, { useReducer } from 'react'
import MenusReducer from './MenusReducer'
import MenusContext from './MenusContext'
import { baseUrl } from '../../shared/baseURL'
const MenusState = (props) => {
const initialState = {
menus: [],
selectedMenu: null
}
const [state, dispatch] = useReducer(MenusReducer, initialState)
const getMenus = async () => {
const response = await fetch(baseUrl + 'RESTAURANTES')
const data = await response.json()
console.log('This is the reducer working'); // This is a test log to see if it works
dispatch({
type: 'GET_MENUS',
payload: data
})
}
const getDetails = async (id) => {
const response = await fetch(`${baseUrl}RESTAURANTES/${id}`)
const data = await response.json()
dispatch({
type: 'GET_DETAILS',
payload: data
})
}
return (
<MenusContext.Provider value={{
menus: state.menus,
selectedMenu: state.selectedMenu,
getMenus,
getDetails
}}>
{props.children}
</MenusContext.Provider>
)
}
export default MenusState;
So here I set up a getMenus() function by which I get all the items I'd like to display in my components. As you can see, I put a test log inside the function to see if it works, which it does.
The problem comes when I try to get those items inside my app components. Here's one of the instances in which I try to get the items to display.
const Home = ({ navigation }) => {
const { menus, getMenus } = useContext(MenusContext)
const [search, setSearch] = useState('')
const [response, setResponse] = useState([])
const [categories, setCategories] = useState(allCategories)
const [loading, setLoading] = useState(true)
useEffect(() => {
const data = async () => await getMenus();
console.log('This is the app executing');
setLoading(false);
setResponse(data)
console.log(response);
}, [])
// ... some code later
return (
<ScrollView style={styles.yScroll}>
<View>
<Text style={styles.sectionTitle}>Destacados</Text>
</View>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View style={styles.sectionContainer}>
<Text>{response[0]}</Text> // Here's where I'm trying to print something about the response but it's not working
</View>
</ScrollView>
<View>
<Text style={styles.sectionTitle}>Categorias</Text>
</View>
<View style={styles.sectionContainer}>
{categories.map((item, index) => {
return (
<View key={index} style={styles.category}>
<Text>{item}</Text>
</View>
)
})}
</View>
</ScrollView>
)
}
So inside one of the ScrollViews I'm setting up a test to see if the response can be displayed, which it is not. However, inside the useEffect, I'm setting up a test log with the message 'This is the app executing' which is working, BUT, the response being logged is an empty array.
I'm sure the problem I'm facing has something to do with the asynchronous response between app and server, but I have no clear idea as to how I can address this.
Can someone please point me in the right direction? Thanks in advance!!
Based on your code, I think you can do this
const Home = ({ navigation }) => {
const { menus, getMenus } = useContext(MenusContext)
const [search, setSearch] = useState('')
const [categories, setCategories] = useState(allCategories)
const [loading, setLoading] = useState(true)
useEffect(() => {
const data = async () => await getMenus();
console.log('This is the app executing');
data();
setLoading(false);
}, [])
// ... some code later
return (
<ScrollView style={styles.yScroll}>
<View>
<Text style={styles.sectionTitle}>Destacados</Text>
</View>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View style={styles.sectionContainer}>
<Text>{menus[0]}</Text> // Here's where I'm trying to print something about the response but it's not working
</View>
</ScrollView>
<View>
<Text style={styles.sectionTitle}>Categorias</Text>
</View>
<View style={styles.sectionContainer}>
{categories.map((item, index) => {
return (
<View key={index} style={styles.category}>
<Text>{item}</Text>
</View>
)
})}
</View>
</ScrollView>
)
}
I am working on an expo react native application with react-navigation v5.
I have created 2 navigators : one StackNavigator if the user is not signed in, and one bottomTabNavigator if he is. In the first stackNavigator, I have a screen Login which takes one prop onAuthSucces used in a TouchableOpacity like this :
<TouchableOpacity style={styles.button} onPress={this.signIn}>
<Text style={{ color: "dimgray", fontWeight: "bold" }}>
Se connecter
</Text>
</TouchableOpacity>
The signIn function is written like this :
signIn = () => {
const user = authenticationService.authenticate(
this.state.login,
this.state.password
);
if (user !== null) {
this.props.onAuthSuccess(user);
} else
Alert.alert(
"Erreur de connexion",
"Votre identifiant et/ou votre mot de passe sont incorrects. Réessayez."
);
};
with a props onAuthSuccess of type : onAuthSuccess: (loggedUser: User) => void;
I tried to create a StackNavigator which can take parameters, and will affect these to the Login props, with :
const LoginPageStack = createStackNavigator<RootStackParamList>();
export const LoginStackScreen = (onAuthSuccess: any) => {
return (
<NavigationContainer>
<LoginPageStack.Navigator screenOptions={{ headerShown: false }}>
<LoginPageStack.Screen name="Login">
{(props) => <Login {...props} onAuthSuccess={onAuthSuccess} />}
</LoginPageStack.Screen>
<LoginPageStack.Screen name="Register" component={Register} />
</LoginPageStack.Navigator>
</NavigationContainer>
);
};
Finally, in App.tsx I use these different pages this like :
interface AppState {
currentUser: User | null;
isConnected: boolean;
}
export default class App extends Component<AppState> {
state: AppState = {
currentUser: null,
isConnected: false,
};
updateCurrentUser = (loggedUser: User) => {
this.setState({ currentUser: loggedUser });
this.setState({ isConnected: true });
};
render() {
if (this.state.isConnected) return <MainTabNavigator />;
else {
return (
<LoginStackScreen onAuthSuccess={this.updateCurrentUser} />
);
}
}
But when I try to connect me with a login and a password that are stocked in the database, Expo Go tells me : "TypeError: _this.props.onAuthSuccess is not a function. (In '_this.props.onAuthSuccess(user)', '_this.props.onAuthSuccess' is an instance of Object)"
Do you have any idea from where the problem can come ? I don't understand.
Thank you !
I am new to React Native and using Expo and trying to login using AuthContext and AsyncStorage, after login it takes me to HomeScreen . But when I make changes in components, it refreshes the app and LogOut me from the app. And everytime I make some changes, I need to login again.
Can anyone help me where I am going wrong. I am using AsyncStorage for now for offline support and reducers.
App.js
<AuthContext.Provider value={authContext}>
<NavigationContainer>
{ loginState.userToken != null ? (
<Drawer.Navigator>
<Drawer.Screen name="Home" component={PreNav}/>
<Drawer.Screen name="Support" component={SupportScreen}/>
<Drawer.Screen name="Settings" component={SettingsScreen}></Drawer.Screen>
</Drawer.Navigator>
): <RootStackScreen />
}
</NavigationContainer>
</AuthContext.Provider>
const initialLoginState = {
isLoading: true,
userName: null,
userToken: null,
}
const loginReducer = (prevState, action) => {
switch(action.type) {
case 'RETRIEVE_TOKEN':
return {
...prevState,
userToken: action.token,
isLoading: false
};
case 'LOGIN':
return {
...prevState,
userName: action.id,
userToken: action.token,
isLoading: false
};
case 'LOGOUT':
return {
...prevState,
userName: null,
userToken: null,
isLoading: false
};
case 'REGISTER':
return {
...prevState,
userName: action.id,
userToken: action.token,
isLoading: false
};
}
}
const [loginState, dispatch] = useReducer(loginReducer, initialLoginState)
const authContext = useMemo(() => ({
userLogin: async (userName, password) => {
// setUserToken("abc");
// setLoading(false);
let userToken;
userToken = 'abc';
if (userName == 'user' && password == 'pass') {
try {
await AsyncStorage.setItem('userToken', userToken)
}
catch(e) {
console.log(e);
}
}
dispatch({type: 'LOGIN', id: userName, token: userToken});
},
userSignup: () => {
},
}));
useEffect(() => {
setTimeout(async () => {
// setLoading(false);
let userToken;
try {
await AsyncStorage.getItem('userToken')
}
catch(e) {
console.log(e);
}
dispatch({type: 'RETRIEVE_TOKEN', token: userToken});
}, 1000);
}, []);
LoginScreen.js
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [pwd, resetPwd] = useState(false);
const { userLogin } = useContext(AuthContext);
return(
<KeyboardAvoidingView behavior='position'>
<View style={styles.logoView}>
<Image style={styles.loginImage} source={require('../assets/login.png')}/>
<View>
<Text style={styles.logoText}> Welcome </Text>
<Text style={styles.logoSubText}> Please Login to continue, If account already created. </Text>
</View>
</View>
<View style={styles.loginInput}>
<Input placeholder='Enter your email address' value={email} onChangeText={(email) => setEmail(email)}
leftIcon={<IconI name='email' size={24} color='black' />}/>
<Input placeholder='Enter your password' secureTextEntry={true} value={password} onChangeText={(password) => {setPassword(password)}}
leftIcon={<Icon name='key' size={24} color='black' />}/>
</View>
<View style={styles.loginButtonBox}>
<TouchableOpacity onPress={() => userLogin(email, password)}>
<Text style={styles.loginButton}> LOGIN </Text>
</TouchableOpacity>
</View>
<View style={styles.noAccount}>
<Text>{"\n"} Don't have an account yet ? </Text>
<TouchableOpacity onPress={() => navigation.navigate('SignupScreen')}>
<Text style={styles.linkText}>{"\n"} Create New One </Text>
</TouchableOpacity>
</View>
<View style={styles.forgotPassword}>
<Text>{"\n"} Don't remember the Password ? </Text>
<TouchableOpacity style={styles.linkText} onPress={() => {
resetPwd(true);
}}>
<Text style={styles.linkText}>{"\n"} Forgot Password </Text>
</TouchableOpacity>
</View>
context.js
export const AuthContext = createContext();
As per the observation of the code provided by you,
here is the issue, occurring due to the saving of userToken using RETRIVE_TOKEN action not working properly as the code behavior hasn't initialized the variable of userToken to the value It getting from async storage so, you must have to initialize the userToken variable with the value getting from asyncStorage.
you can try with the below code changes in your App.js useEffect hook
useEffect(() => {
setTimeout(async () => {
let userToken;
try {
userToken = await AsyncStorage.getItem('userToken')
}
catch(e) {
console.log(e);
}
dispatch({type: 'RETRIEVE_TOKEN', token: userToken});
}, 1000);
}, []);
I'm setting up token authentication in apollo-client with react-native, but my app shows NetworkError. It works fine if I remove all authentication settings.
Error screenshot
This is for a local demo app with react-navigation, apollo-client, formik and yup.
App.js
const httpLink = new createHttpLink({
uri: 'http://192.168.0.2/api/graphql'
});
const authLink = setContext((_, { headers }) => {
const authToken = localStorage.getItem('authToken');
return {
headers: {
...headers,
authorization: authToken ? `JWT ${authToken}` : '',
}
}
});
const APIClient = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache()
});
class App extends Component {
render(){
return (
<ApolloProvider client={APIClient}>
<SafeAreaView style={styles.container}>
<AppContainer />
</SafeAreaView>
</ApolloProvider>
);
}
};
SignInScreen.js
const TOKENAUTH_MUTATION = gql`
mutation SignIn($username: String!, $password: String!){
tokenAuth(username: $username, password: $password){
token
}
}
`;
class SignInForm extends Component{
constructor(props){
super(props);
}
render(){
return(
<View style={styles.container}>
<Text style={styles.label}>Username:</Text>
<TextInput
autoCapitalize='none'
autoFocus={true}
onBlur={this.props.handleBlur('username')}
onChangeText={this.props.handleChange('username')}
style={styles.textInput}
value={this.props.values.username}
/>
<Text style={styles.helpTextInput}>{this.props.errors.username}</Text>
<Text style={styles.label}>Password:</Text>
<TextInput
onBlur={this.props.handleBlur('password')}
onChangeText={this.props.handleChange('password')}
secureTextEntry={true}
style={styles.textInput}
value={this.props.values.password}
/>
<Text style={styles.helpTextInput}>{this.props.errors.password}</Text>
{this.props.isSubmitting ? (
<ActivityIndicator />
) : (
<Button title="Sign In" onPress={this.props.handleSubmit} />
)}
</View>
);
}
}
class SignInScreen extends Component{
static navigationOptions = {
title: "Sign In"
};
_submit(values, actions, mutateFunc){
mutateFunc({
variables: values
});
}
async _onSuccess(dataResult){
await AsyncStorage.setItem('authToken', dataResult.tokenAuth.token);
this.props.navigation.navigate('Questions');
}
_onError(error){
alert("ERROR: " + JSON.stringify(error));
}
render(){
const formValidationSchema = yup.object().shape({
username: yup.string().required().label("Username"),
password: yup.string().required().label("Password")
});
return(
<Mutation
mutation={TOKENAUTH_MUTATION}
onCompleted={dataResult => this._onSuccess(dataResult)}
onError={error => this._onError(error)}>
{(signInMutationFunc, {loading, error, data}) => {
return (
<Formik
initialValues={{
username: '',
password: ''
}}
onSubmit={(values, actions) => this._submit(values, actions, signInMutationFunc)}
validationSchema={formValidationSchema}>
{formProps => {
return(<SignInForm {...formProps} />);
}}
</Formik>
);
}}
</Mutation>
);
}
}
I expected the token was stored and used in header every request (like Apollo docs says).
If I use a basic ApolloClient it works but doesn't use the header
const APIClient = new ApolloClient({
uri: 'http://192.168.0.2/api/graphql'
});
I'm getting same error in Android and IOS devices :'(
Ok
In my example I was working with Apollo Boost. When I use Apollo Client it works.