Why is my code causing my component to show up for a split second? - react-native

I am developing an application for a religious group, and I just implemented AsyncStorage to store the JWT received from my back end. My back end is working perfectly, and I'm able to store and delete the token on login/register and logout. But when the app is refreshed, it shows the login screen for a split second.
Here is my action:
export const fetchUser = () => {
return async (dispatch) => {
const token = await getData();
const reqObj = {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
};
if (token) {
dispatch({ type: "IS_LOADING_JWT" });
fetch(`http://${IP_ADDRESS}:4000/api/v1/auto_login`, reqObj)
.then((resp) => resp.json())
.then((current_user) => {
dispatch({ type: "LOGIN_SUCCESS", user: current_user });
dispatch({ type: "NOT_LOADING_JWT" });
});
}
};
};
Here is my helper function to retrieve the stored value-key pair:
const getData = async () => {
try {
const token = await AsyncStorage.getItem("jwt");
return token;
} catch (error) {
console.log(error);
}
};
This is my user reducer:
const user = (state = null, action) => {
switch (action.type) {
case "LOGIN_SUCCESS":
return action.user;
case "LOGOUT_USER":
return null;
default:
return state;
}
};
export default user;
Here is my 'authentication flow' with my stack navigator:
class Navigator extends Component {
componentDidMount() {
this.props.fetchUser();
}
render() {
return (
<NavigationContainer>
{this.props.jwtLoader ? (
<Stack.Navigator
screenOptions={{
title: "",
headerShown: false,
animationEnabled: false,
}}
>
<Stack.Screen name="loader" component={LoaderScreen} />
</Stack.Navigator>
) : !this.props.user ? (
<Stack.Navigator
screenOptions={{
title: "",
headerShown: false,
animationEnabled: false,
}}
>
<Stack.Screen name="Login" component={LoginScreen} />
</Stack.Navigator>
) : (
<Stack.Navigator
screenOptions={{
title: "",
headerShown: false,
animationEnabled: false,
}}
>
<Stack.Screen name="Main" component={MainScreen} />
</Stack.Navigator>
)}
</NavigationContainer>
);
}
}
My jwtLoader:
const jwtLoader = (state = false, action) => {
switch (action.type) {
case "IS_LOADING_JWT":
return true;
case "NOT_LOADING_JWT":
return false;
default:
return state;
}
};
export default jwtLoader;
My theory is that there is a delay on dispatching the action. That is why I believe my Login component shows for a split second.
I have already tried getting and setting my user during my SplashScreen load, but that doesn't work either.
Here is a demo video: Text

Can you try fetching user from the constructor instead of componentDidMount?

Related

Move code for React-Context to a dedicated file and using it

I followed the tutorial from react-navigation for the authentication context:
https://reactnavigation.org/docs/auth-flow/
But now i want to move my context out of the App.js in something like AuthContext.js
but i dont succeed in using it in App.js, but there is no tutorial for i tried multiple times but it always break something, here's my code:
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
import { NavigationContainer } from "#react-navigation/native";
import { createBottomTabNavigator } from '#react-navigation/bottom-tabs';
import * as SecureStore from 'expo-secure-store';
import * as React from 'react';
import SignInScreen from "./screens/SignInScreen";
import SignUpScreen from "./screens/SignUpScreen";
import HomeScreen from "./screens/HomeScreen";
import SplashScreen from "./screens/SplashScreen";
import { getTokens } from "./services/AuthServices";
import "react-native-gesture-handler";
const Stack = createBottomTabNavigator();
export const AuthContext = React.createContext();
export default function App({ navigation }) {
const [state, dispatch] = React.useReducer(
(prevState, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
accessToken: action.accesToken,
isLoading: false,
};
case 'SIGN_IN':
return {
...prevState,
isSignout: false,
isSignedIn: true,
accessToken: action.token,
};
case 'SIGN_OUT':
const deleteTokens = async () => {
try {
await SecureStore.deleteItemAsync("access_token");
await SecureStore.deleteItemAsync("refresh_token");
} catch (error) {
console.log(error);
}
};
deleteTokens();
return {
...prevState,
isSignout: true,
accessToken: null,
};
}
},
{
isLoading: true,
isSignout: false,
isSignedIn: false,
accessToken: null,
refreshToken: null
}
);
React.useEffect(() => {
// Fetch the token from storage then navigate to our appropriate place
const bootstrapAsync = async () => {
let accessToken;
try {
accessToken = await SecureStore.getItemAsync('access_token');
} catch (e) {
console.log(e)
}
// After restoring token, we may need to validate it in production apps
// This will switch to the App screen or Auth screen and this loading
// screen will be unmounted and thrown away.
dispatch({ type: 'RESTORE_TOKEN', accessToken: accessToken });
};
bootstrapAsync();
}, []);
const authContext = React.useMemo(
() => ({
signIn: async (data) => {
let tokens = await getTokens(data);
if (tokens.access_token && tokens.refresh_token) {
try {
await SecureStore.setItemAsync("access_token", tokens.access_token);
await SecureStore.setItemAsync("refresh_token", tokens.refresh_token);
} catch (error) {
console.log(error);
}
dispatch({ type: 'SIGN_IN', token: tokens.access_token });
}
console.log(tokens);
},
signOut: () => dispatch({ type: 'SIGN_OUT' }),
signUp: async (data) => {
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
}),
[]
);
return (
<AuthContext.Provider value={authContext}>
<NavigationContainer>
<Stack.Navigator>
{state.isLoading ? (
<Stack.Screen
name="Splash"
component={SplashScreen}
options={{ title: "Splash" }}
/>
) : state.accessToken != null ? (
<>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: "Home" }}
/>
</>
) : (
<>
<Stack.Screen
name="SignIn"
component={SignInScreen}
options={{ title: "SignIn" }}
/>
<Stack.Screen
name="SignUp"
component={SignUpScreen}
options={{ title: "SignUp" }}
/>
</>
)}
</Stack.Navigator>
</NavigationContainer>
</AuthContext.Provider>
);
}

Stripe payment integration React Native. calling presentPaymentSheet function leads to app crash

I am building an e-commerce app using React Native and Django as backend. The payment integration is built with Stripe. The following lines of code are from Django views, whose aim is to create a customer, ephemeral key, and payment intent and return their specific values which will be needed to complete the transactions...
#api_view(['POST'])
#authentication_classes([TokenAuthentication])
#permission_classes([IsAuthenticated])
def payment_sheet(request, *args, **kwargs):
data = request.data
subtotal = data['subtotal']
customer = stripe.Customer.create()
ephemeralKey = stripe.EphemeralKey.create(
customer=customer['id'],
stripe_version='2020-08-27'
)
paymentIntent = stripe.PaymentIntent.create(
amount=int(subtotal),
currency='eur',
customer=customer['id'],
)
return JsonResponse({
'paymentIntent': paymentIntent.client_secret,
'ephemeralKey': ephemeralKey.secret,
'customer': customer.id
})
My screen is within the StripeProvider as Docs guides.
const MainFlow = ({ }) => {
return (
<ShoppingProvider>
<StripeProvider
publishableKey="pk_test_Dt4ZBItXSZT1EzmOd8yCxonL"
>
<OrderItemProvider>
<MainFlowStack.Navigator>
<MainFlowStack.Screen name="Home" component={HomeScreen} options={{ headerShown: false }} />
<MainFlowStack.Screen name="Cart" component={CartScreen} options={{ headerShown: false }} />
<MainFlowStack.Screen name="Favourite" component={FavouriteScreen} options={{ headerShown: false }} />
<MainFlowStack.Screen name="Search" component={SearchScreen} options={{ headerShown: false }} />
<MainFlowStack.Screen name="Detail" component={DetailScreen} options={{ headerShown: false }} />
<MainFlowStack.Screen name="Checkout" component={CheckoutScreen} options={{ headerShown: false }} />
</MainFlowStack.Navigator>
</OrderItemProvider>
</StripeProvider>
</ShoppingProvider>
)
On the other hand, here is the code from my Checkout Screen from my React Native project.
const CheckoutScreen = ({ route }) => {
const { token } = useUserInfo()
const { initPaymentSheet, presentPaymentSheet } = useStripe();
const [clientSecret, setClientSecret] = useState('')
const [loading, setLoading] = useState(false);
const subtotal = route.params.subtotal
const fetchPaymentSheetData = (token, subtotal) => {
const total = (subtotal * 100).toString()
const data = { "subtotal": total }
return fetch(`${API}/api/payment/payment-sheet/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Token ${token}`
},
body: JSON.stringify(data)
})
.then((res) => {
return res.json()
})
.catch((err) => console.log(err))
}
const initializePaymentSheet = (token, subtotal) => {
fetchPaymentSheetData(token, subtotal)
.then((res) => {
setClientSecret(res.paymentIntent)
initPaymentSheet({
customerId: res.customer,
customerEphemeralKeySecret: res.ephemeralKey,
paymentIntentClientSecret: res.paymentIntent,
})
.then((res) => {
console.log(res)
setLoading(true)
})
.catch((err) => console.log(err))
})
.catch((err) => console.log(err))
// const { error } = await initPaymentSheet({
// customerId: paymentInfo.customer,
// customerEphemeralKeySecret: paymentInfo.ephemeralKey,
// paymentIntentClientSecret: paymentInfo.paymentIntent,
// })
// if (!error) {
// setLoading(true)
// }
}
const openPaymentSheet = async () => {
const { error } = await presentPaymentSheet({ clientSecret });
if (error) {
Alert.alert(`Error code: ${error.code}`, error.message);
} else {
Alert.alert('Success', 'Your order is confirmed!');
}
};
useEffect(() => {
initializePaymentSheet(token, subtotal)
}, [])
return (
<View style={style.root}>
<Navbar />
<Text>{subtotal}</Text>
<Button
variant="primary"
disabled={!loading}
title="Checkout"
onPress={openPaymentSheet}
/>
</View>
);
}
The initPaymentSheet promise is not rejected and the 'Checkout' Button is now available. The problem comes when the button is pressed and hence within the 'openPaymentSheet' function, presentPaymentSheet is called... this leads to a crashing in the app.
Can anyone point out a bug or tell why this error is happening?
Thanks in advance
I fell into the same problem, and for me the problem was solved by providing some additional information while initPaymentSheet.
const {error} = await initPaymentSheet({
customerEphemeralKeySecret: ephemeralKey,
setupIntentClientSecret: setupIntent,
customerId: customer,
customFlow: false,
merchantDisplayName: 'Test Inc.',
style: 'alwaysDark',
});
packages versions are:
"#stripe/stripe-react-native": "^0.13.1",
"react-native": "^0.68.2",
I was following the bellow document to implement the stripe
https://stripe.com/docs/payments/save-and-reuse

How to import AuthContext created in App.js in other components

I am following the procedures mentioned in React navigation Docs for implementing Authentication using Expo secure Store. https://reactnavigation.org/docs/auth-flow/
I tried to minimise their code snippet as short as possible if this is not clear please refer this snippet
//some imports
const AuthContext = React.createContext();
function SplashScreen() {
//some jsx
}
function HomeScreen() {
const { signOut } = React.useContext(AuthContext);
return (
//somejsx
);
}
function SignInScreen() {
const { signIn } = React.useContext(AuthContext);
return (//somejsx);
}
const Stack = createStackNavigator();
export default function App({ navigation }) {
const [state, dispatch] = React.useReducer(
(prevState, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
userToken: action.token,
isLoading: false,
};
case 'SIGN_IN':
return {
...prevState,
isSignout: false,
userToken: action.token,
};
case 'SIGN_OUT':
return {
...prevState,
isSignout: true,
userToken: null,
};
}
},
{
isLoading: true,
isSignout: false,
userToken: null,
}
);
const authContext = React.useMemo(
() => ({
signIn: () => dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' }),
signOut: () => dispatch({ type: 'SIGN_OUT' }),
signUp: () => dispatch({ type: 'SIGN_IN', token: 'dummy' });
}),
[]
);
return (
<AuthContext.Provider value={authContext}>
<NavigationContainer>
<Stack.Navigator>
{state.isLoading ? (
<Stack.Screen name="Splash" component={SplashScreen} />
) : state.userToken == null ? (
<Stack.Screen
name="SignIn"
component={SignInScreen}
/>
) : (
<Stack.Screen name="Home" component={HomeScreen} />
)}
</Stack.Navigator>
</NavigationContainer>
</AuthContext.Provider>
);
}
Now Coming to my Issue, In the given snippet they used the Authcontext they created in the login and register screens directly
const { signOut } = React.useContext(AuthContext);
But in my use case scenario i have to use the signOut function outside the App component, in my signOut screen.
When i tried to use the AuthContext in a separate file, react native is throwing up an error "can't find variable AuthContext". I encoded My App component with the and also provided the value with correct syntax.
I am not sure if i have to import the AuthContext in the login and register components.
Any help would be highly appreciated, Thanks in Advance.
You need to wrap the compenent where you need to use the context, like this as example
<AuthenticationProvider>
<TheComponent/>
</AuthenticationProvider>
You can first create a component called Provider
const Providers: FC = (props) => (
<AuthenticationProvider>
{props.children}
</AuthenticationProvider>
);
export default Providers;
and then pass your component as children
In App.js:
export const AuthContext = React.createContext();
In SignInScreen.js:
import { AuthContext } from 'App.js'

Logging out of react app results in nullpointer in homescreen

I'm working on a react application with a login page. When the user logs in successfully, the backend returns a user-object (JSON), containing some user properties (id, username, ...) aswel as JWT token. The user-object gets stored in AsyncStorage (so the user only has to log in once).
try {
await AsyncStorage.setItem('authUser', JSON.stringify(data));
} catch (e) {
console.log(e.message);
}
dispatch({ type: 'SIGN_IN', authUser:data});
The state :
const [state, dispatch] = React.useReducer(
(prevState, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
authUser: action.authUser,
isLoading: false,
};
case 'SIGN_IN':
return {
...prevState,
isSignout: false,
authUser: action.authUser,
};
case 'SIGN_OUT':
return {
...prevState,
isSignout: true,
authUser: null,
};
}
},
{
isLoading: true,
isSignout: false,
authUser: "",
}
);
The second time i retrieve the user-object from the AsyncStorage and pass it to the state.
try {
authUser = await AsyncStorage.getItem('authUser');
} catch (e) {
console.log(e.message);
}
dispatch({ type: 'RESTORE_TOKEN', authUser: JSON.parse(authUser) });
I use a UserContext to pass the authUser to my welcome screen :
<AuthContext.Provider value={authContext}>
<UserContext.Provider value={{authUser: state.authUser }}>
<NavigationContainer>
<RootStack.Navigator mode="modal" headerMode="none">
{state.isLoading ? (
<RootStack.Screen name="Splash" component={SplashScreen} />
) : state.authUser == null ? (
<RootStack.Screen name="Login" component={AuthenticationStackScreen} />
) : (
<RootStack.Screen name="Home" component={HomeScreen} />
)}
</RootStack.Navigator>
</NavigationContainer>
</UserContext.Provider>
</AuthContext.Provider>
In my homescreen I print a welcome message containing the username and a signOut button :
function HomeScreen(props) {
const { signOut } = React.useContext(AuthContext);
const { authUser } = React.useContext(UserContext);
return (
<View style={{ flex:1, alignItems: 'center', justifyContent: 'center', backgroundColor:'cyan' }}>
<Text>Hallo {authUser.username}</Text>
<Button title="Log out" onPress={signOut} />
<Text>HomeScreen </Text>
</View>
);
}
When a user logs in or was already logged in, the Homescreen is shown correctly with the correct username. The problem appears when the logout button is clicked. The signOut method in the authContext gets called, which logs out the user and removes the object from the AsyncStorage :
signOut: async () => {
try {
await AsyncStorage.removeItem('authUser');
} catch (e) {
console.log(e.message);
}
dispatch({ type: 'SIGN_OUT' })
}
The problem :
The state changes to SIGN_OUT which brings me back to the login page. But since i've cleared the user-object, the welcome notification throws an error because {authUser.username} is null:
TypeError: null is not an object (evaluating 'authUser.username')
I didn't expect the WelcomeScreen to be re-rendered when clicking the sign-out button. Did i mess up the structure of my application, or why is this? Any help on how to fix this would be greatly appreciated. Thanks in advance!
According to React documentation useContext will always re-render when the context value changes, so that's why HomeScreen is been called again.
Another thing you need to fix is the authUser initial state to be equal to authUser signOut state:
const [state, dispatch] = React.useReducer(
(prevState, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
authUser: action.authUser,
isLoading: false,
};
case 'SIGN_IN':
return {
...prevState,
isSignout: false,
authUser: action.authUser,
};
case 'SIGN_OUT':
return {
...prevState,
isSignout: true,
authUser: null,
};
}
},
{
isLoading: true,
isSignout: false,
authUser: "", //MAKE THIS THE SAME AS SIGN_OUT (null)
}
);

what is cause infinite rerenders?

So i am trying to follow the react navigation tutorial for auth flow https://reactnavigation.org/docs/auth-flow/
But for some reason its causing infinite rerenders, the screen flickering like crazy, and i have no idea why other than the dispatch in useEffect is just updating the state every render, but as far as i understand it that shouldn't be happening.
import React from "react"
import { NavigationContainer, NavigationContainerRef } from "#react-navigation/native"
import { createNativeStackNavigator } from "react-native-screens/native-stack"
import { PrimaryNavigator, AuthNavigator, DrawerNavigator } from "./primary-navigator"
import { SplashScreen } from "../screens/splash-screen"
import AsyncStorage from "#react-native-community/async-storage"
export type RootParamList = {
primaryStack: undefined,
authStack: undefined,
splashScreen: undefined,
}
const Stack = createNativeStackNavigator<RootParamList>()
const RootStack = () => {
const AuthContext = React.createContext()
const [state, dispatch] = React.useReducer(
(prevState, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
userToken: action.token,
isLoading: false,
}
case 'SIGN_IN':
return {
...prevState,
isSignout: false,
userToken: action.token,
}
case 'SIGN_OUT':
return {
...prevState,
isSignout: true,
userToken: null,
}
}
},
{
isLoading: true,
isSignout: false,
userToken: null,
}
)
React.useEffect(() => {
const bootstrapAsync = async () => {
let userToken
try {
userToken = await AsyncStorage.getItem('userToken')
console.log(userToken)
} catch (e) {
console.log(e)
}
// After restoring token, we may need to validate it in production apps
// This will switch to the App screen or Auth screen and this loading
// screen will be unmounted and thrown away.
dispatch({ type: 'RESTORE_TOKEN', token: userToken })
}
bootstrapAsync()
})
const authContext = React.useMemo(
() => ({
signIn: async data => {
// In a production app, we need to send some data (usually username, password) to server and get a token
// We will also need to handle errors if sign in failed
// After getting token, we need to persist the token using `AsyncStorage`
// In the example, we'll use a dummy token
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' })
},
signOut: () => dispatch({ type: 'SIGN_OUT' }),
signUp: async data => {
// In a production app, we need to send user data to server and get a token
// We will also need to handle errors if sign up failed
// After getting token, we need to persist the token using `AsyncStorage`
// In the example, we'll use a dummy token
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' })
},
}), [])
return (
<AuthContext.Provider value={authContext}>
<Stack.Navigator
screenOptions={{
headerShown: false,
gestureEnabled: true,
stackPresentation: "modal",
}}
>
{ state.userToken == null
? <Stack.Screen
name="authStack"
component={AuthNavigator}
options={{
headerShown: false
}}
/>
: <Stack.Screen
name="primaryStack"
component={DrawerNavigator}
options={{
headerShown: false,
}}
/>
}
</Stack.Navigator>
</AuthContext.Provider>
)
}
export const RootNavigator = React.forwardRef<
NavigationContainerRef,
Partial<React.ComponentProps<typeof NavigationContainer>>
>((props, ref) => {
return (
<NavigationContainer {...props} ref={ref}>
<RootStack />
</NavigationContainer>
)
})
RootNavigator.displayName = "RootNavigator"
You missed the dependency '[]' which is provided in the tutorial. When you provide a dependency to the useEffect it will run only when a dependency changes. This is provided in the tutorial seems like you missed it.
This is from the tutorial
React.useEffect(() => {
// Fetch the token from storage then navigate to our appropriate place
const bootstrapAsync = async () => {
let userToken;
try {
userToken = await AsyncStorage.getItem('userToken');
} catch (e) {
// Restoring token failed
}
// After restoring token, we may need to validate it in production apps
// This will switch to the App screen or Auth screen and this loading
// screen will be unmounted and thrown away.
dispatch({ type: 'RESTORE_TOKEN', token: userToken });
};
bootstrapAsync();
}, []);