React Native Navigation: screen flickers when function interacts with SecureStorage - react-native

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>
)
)

Related

stack from NativeStackNavigator (nested in BottomTabNavigator) resets everytime the BottomTabNavigator changes tabs

Situation:
The react native app has a BottomTabNavigator (react-navigation/material-bottom-tabs) and one of the tabs has a NativeStackNavigator (react-navigation/native-stack).
BottomTabNavigator:
tab1
tab2
tab3:
NativeStackNavigator:
screen1
screen2
Problem:
When I press tab3 (from the BottomTabNavigator) I see screen 1.
If I press a button on screen 1 then it will navigate from screen 1 to screen 2.
When I press tab1 or tab2 and then I press tab3 again then I see screen 1.
I want to see screen 2 (because that's what should be at the top of the stack from the NativeStackNavigator right?).
Did the stack from NativeStackNavigator reset?
Did the whole NativeStackNavigator render again?
What causes this behavior?
Code BottomTabNavigator:
export type MainNavigationBarParam = {
tab1: undefined;
tab2: undefined;
tab3: undefined;
};
const Tab = getMainNavigationBarTabNavigator();
export const tab3Stack = createNativeStackNavigator();
export function MainNavigationBar() {
const sizeToUse = 25;
return (
<Tab.Navigator
screenOptions={defaultScreenOptions()}
barStyle={{backgroundColor: theme.colors.primary}}>
<Tab.Screen
name="tab1"
component={Component1}
/>
<Tab.Screen
name="tab2"
component={Component2}
/>
<Tab.Screen
name="tab3"
component={Component3}
/>
</Tab.Navigator>
);
}
function defaultScreenOptions() {
const screenOptions: any = {
headerShown: false,
tabBarHideOnKeyboard: true,
};
if (Platform.OS === 'web') {
screenOptions.swipeEnabled = false;
}
return screenOptions;
}
Code Component3:
export type Tab3Stack ParamList = {
Screen1: undefined;
Screen2: {id: string; name: string};
};
export default function Component3() {
const [topComponentHeight, setTopComponentHeight] = useState(0);
function onLayout(event: LayoutChangeEvent) {
if ('top' in event.nativeEvent.layout) {
const withTop = event.nativeEvent.layout as unknown as {top: number};
setTopComponentHeight(withTop.top);
}
event;
}
return (
<View style={navContainerStyle(topComponentHeight).navContainer} onLayout={onLayout}>
<tab3Stack.Navigator screenOptions={{headerShown: false}}>
<tab3Stack.Screen
name={'Screen1'}
component={Screen1}
/>
<tab3Stack.Screen
name={'Screen2'}
component={Screen2}
/>
</tab3Stack.Navigator>
</View>
);
}
Update 1
Reproduced the code above for 2 tabs: https://snack.expo.dev/#jacobdel/182747
The problem does not occur on snack, only in my app.
Console in chrome:
No errors or warnings.
Sometimes this is shown, but after disabling the quill editor it doesn't appear anymore
Update 2
Cause is found: getMainNavigationBarTabNavigator(); from BottomTabNavigator
File MainNavigationBarTabNavigator.ts looks like this:
import {createMaterialBottomTabNavigator} from '#react-navigation/material-bottom-tabs';
import {MainNavigationBarParam} from './MainNavigationTabs';
export function getMainNavigationBarTabNavigator() {
return createMaterialBottomTabNavigator<MainNavigationBarParam>();
}
While MainNavigationBarTabNavigator.web.ts looks like this:
import {createMaterialTopTabNavigator} from '#react-navigation/material-top-tabs';
import {MainNavigationBarParam} from './MainNavigationTabs';
export function getMainNavigationBarTabNavigator() {
return createMaterialTopTabNavigator<MainNavigationBarParam>();
}
Snack only works as intended when using MainNavigationBarTabNavigator.ts
Update 3
Stuck..
Replacing the code from MainNavigationBarTabNavigator.web.ts with the code from MainNavigationBarTabNavigator.ts does not show the intended behavior as shown in the Snack example.
There seems to be an issue with the navigation event handling of #react-navigation/material-top-tabs.
If we handle this on our own, then the nested stack is not reset. We can prevent the default navigation action by using a tabPress event listener and calling e.preventDefault.
In your case, this is done as follows for screen2 (which is the nested stack).
<Tab.Screen
name="screen2"
component={Screen2}
listeners={({ navigation, route }) => ({
tabPress: (e) => {
e.preventDefault();
navigation.navigate({ name: route.name, merge: true });
},
})}
/>
Notice that this works fine on the phone, but has some issues on the web. If we navigate fast between screen2 and screen1 multiple times, then the navigation does not work at all. I haven't found the root cause for this issue.
However, we can make this work on the web as well as on the phone by providing a custom top navigation bar using the tabBar prop of the tab navigator. We override the default behavior of the tabPress function.
The original component is exported and is named MaterialTopTabBar. Sadly, it does not provide the possibility to provide a custom onTabPress function via props.
I have forked the component and 'fixed' (without knowing what exactly is going wrong here) the onTabPress function.
import {
useTheme,
} from '#react-navigation/native';
import Color from 'color';
import * as React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { TabBar, TabBarIndicator } from 'react-native-tab-view';
export default function FixedTabBarTop({
state,
navigation,
descriptors,
...rest
}) {
const { colors } = useTheme();
const focusedOptions = descriptors[state.routes[state.index].key].options;
const activeColor = focusedOptions.tabBarActiveTintColor ?? colors.text;
const inactiveColor =
focusedOptions.tabBarInactiveTintColor ??
new Color(activeColor).alpha(0.5).rgb().string();
return (
<TabBar
{...rest}
navigationState={state}
scrollEnabled={focusedOptions.tabBarScrollEnabled}
bounces={focusedOptions.tabBarBounces}
activeColor={activeColor}
inactiveColor={inactiveColor}
pressColor={focusedOptions.tabBarPressColor}
pressOpacity={focusedOptions.tabBarPressOpacity}
tabStyle={focusedOptions.tabBarItemStyle}
indicatorStyle={[
{ backgroundColor: colors.primary },
focusedOptions.tabBarIndicatorStyle,
]}
indicatorContainerStyle={focusedOptions.tabBarIndicatorContainerStyle}
contentContainerStyle={focusedOptions.tabBarContentContainerStyle}
style={[{ backgroundColor: colors.card }, focusedOptions.tabBarStyle]}
getAccessibilityLabel={({ route }) =>
descriptors[route.key].options.tabBarAccessibilityLabel
}
getTestID={({ route }) => descriptors[route.key].options.tabBarTestID}
onTabPress={({ route }) => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (!event.defaultPrevented) {
navigation.navigate({ name: route.name, merge: true });
}
}}
onTabLongPress={({ route }) =>
navigation.emit({
type: 'tabLongPress',
target: route.key,
})
}
renderIcon={({ route, focused, color }) => {
const { options } = descriptors[route.key];
if (options.tabBarShowIcon === false) {
return null;
}
if (options.tabBarIcon !== undefined) {
const icon = options.tabBarIcon({ focused, color });
return (
<View style={[styles.icon, options.tabBarIconStyle]}>{icon}</View>
);
}
return null;
}}
renderLabel={({ route, focused, color }) => {
const { options } = descriptors[route.key];
if (options.tabBarShowLabel === false) {
return null;
}
const label =
options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: route.name;
if (typeof label === 'string') {
return (
<Text
style={[styles.label, { color }, options.tabBarLabelStyle]}
allowFontScaling={options.tabBarAllowFontScaling}
>
{label}
</Text>
);
}
return label({ focused, color });
}}
renderBadge={({ route }) => {
const { tabBarBadge } = descriptors[route.key].options;
return tabBarBadge?.() ?? null;
}}
renderIndicator={({ navigationState: state, ...rest }) => {
return focusedOptions.tabBarIndicator ? (
focusedOptions.tabBarIndicator({
state: state,
...rest,
})
) : (
<TabBarIndicator navigationState={state} {...rest} />
);
}}
/>
);
}
const styles = StyleSheet.create({
icon: {
height: 24,
width: 24,
},
label: {
textAlign: 'center',
textTransform: 'uppercase',
fontSize: 13,
margin: 4,
backgroundColor: 'transparent',
},
});
I have used it as follows.
export function MainNavigationBar() {
return (
<Tab.Navigator
tabBar={props => <FixedTabBarTop {...props} />}
screenOptions={defaultScreenOptions()}>
<Tab.Screen
name="screen1"
component={Screen1}
/>
<Tab.Screen
name="screen2"
component={Screen2}
/>
</Tab.Navigator>
);
}
I have updated your snack. You might want to submit an issue on GitHub as well. It feels like a bug.

Making react native TopNavigation bar absolute

So what i want to achieve is a navigation bar that has a background color of blue and have a 60% opacity so that it can show the image behind it. I can only achieve this by making the header's position an absolute but it wont let me click the back button.
here is my code stacknavigator
const UnregisteredNavigator = () => {
return (
<Stack.Navigator
screenOptions={({ navigation,route }) => ({
headerStyle: {
height: 60
},
header: ({}) => {
return (
<ThemedTopNavigation navigation={navigation} route={route} />
);
},
})}
>
<Stack.Screen name="LandingScreen" component={LandingScreen} options={{headerShown:false}} />
<Stack.Screen name="Login" component={SignInScreen} />
</Stack.Navigator>
);
};
and here is my ThemedTopNavigation code
const TopNavigationBar = ({ navigation: { goBack },route,eva }) => {
const themeContext = React.useContext(ThemeContext);
console.log("styled", eva);
const [menuVisible, setMenuVisible] = React.useState(false);
const [checked, setChecked] = React.useState(false);
const onCheckedChange = (isChecked) => {
themeContext.toggleTheme()
setChecked(isChecked);
};
const backFunction = () => {
alert("back");
// goBack()
};
const renderBackAction = () => (
<TopNavigationAction style={{backgroundColor:"red",zIndex: 1, position:'relative'}} icon={BackIcon} onPress={backFunction}/>
);
return (
<Layout style={eva.style.navLayout}>
<TopNavigation
style={[eva.style.navContainer, styles]}
alignment='center'
title={()=>(<Button>CLICK</Button>)}
accessoryLeft={renderBackAction}
/>
</Layout>
);
};
I am using UI kitten. anyone can help me?

Passing a props component through a Stack Navigator (React Navigation v5)

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 !

Multiple prompts AzureAD promptAuthAsync() Expo

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!!

Why after login my react-navigation routing does not work properly?

I have navigation container(created in react-navigation)
const AppStack = createStackNavigator();
const AppStackScreen = () => (
<AppStack.Navigator>
<AppStack.Screen name="Tabbed" component={TabsScreenNavigationScreen} />
</AppStack.Navigator>
);
class AppNavigationContainer extends Component {
constructor(props) {
super(props);
this.state = {
appLoading : true,
}
}
user = {};
componentDidMount() {
let _this = this;
this.getUser()
.then(() => {
this.setState({appLoading: !_this.state.appLoading})
})
}
getUser = async () => {
return await AsyncStoreService.getUserFromStore()
.then((user) => {
this.user = user;
});
}
render() {
const user = this.user;
const {
appLoading
} = this.state;
return (
<NavigationContainer>
{appLoading ?
<SplashScreen/>
:
<>
{user ?
<AppStackScreen/>
:
<AuthStackNavigationScreen/>
}
</>
}
</NavigationContainer>
)
}
}
export default AppNavigationContainer;
How can you see I have separated modules for app and login. login router:
const AuthStack = createStackNavigator();
const AuthStackNavigationScreen = () => (
<AuthStack.Navigator>
<AuthStack.Screen
name="ChooseRole"
component={SelectRoleScreen}
options={{title: false}}
/>
<AuthStack.Screen
name="AuthStart"
component={MainScreen}
options={{title: false}}
/>
<AuthStack.Screen
name="SignIn"
component={LoginScreen }
options={{title: 'Sign In'}}
/>
<AuthStack.Screen
name="CreateAccount"
component={RegisterScreen}
options={{title: 'Create Account'}}
/>
</AuthStack.Navigator>
);
export default AuthStackNavigationScreen;
Tabbed router for app:
const GalleryStack = createStackNavigator();
const SearchStack = createStackNavigator();
const MessagesStack = createStackNavigator();
const MenuStack = createStackNavigator();
const Tabs = createBottomTabNavigator();
const GalleryStackScreen = () => (
<GalleryStack.Navigator>
<GalleryStack.Screen name="Gallery" component={GalleryScreen} />
<GalleryStack.Screen name="GalleryItem" component={GalleryItemScreen} />
</GalleryStack.Navigator>
);
const SearchStackScreen = () => (
<SearchStack.Navigator>
<SearchStack.Screen name="Search" component={SearchScreen} />
<SearchStack.Screen name="SearchResults" component={SearchResultsScreen} />
</SearchStack.Navigator>
);
const MessagesStackScreen = () => (
<MessagesStack.Navigator>
<MessagesStack.Screen name="ConversationList" component={ConversationListScreen} />
<MessagesStack.Screen name="Conversation" component={ConversationScreen} />
</MessagesStack.Navigator>
);
const MenuStackScreen = () => (
<MenuStack.Navigator>
<MenuStack.Screen name="Menu" component={MenuScreen} />
<MenuStack.Screen name="About" component={AboutScreen} />
</MenuStack.Navigator>
);
const TabsScreenNavigationScreen = () => (
<Tabs.Navigator>
<Tabs.Screen name="Gallery" component={GalleryStackScreen} />
<Tabs.Screen name="Search" component={SearchStackScreen} />
<Tabs.Screen name="Messages" component={MessagesStackScreen} />
<Tabs.Screen name="Menu" component={MenuStackScreen} />
</Tabs.Navigator>
);
export default TabsScreenNavigationScreen;
So on login screen name="SignIn" I login, perform navigation.navigate('Tabbed') after succesfully login and get message:
The action 'NAVIGATE' with payload {"name":"Tabbed"} was not handled by any navigator.
Do you have a screen named 'Tabbed'?
He doesnt 'see' my tab navigation. Why it happens so(I have such screen name and put it to render), and how can I fix this?
According to the stack you have you will either have the appstack or the authstack at a given moment
<>
{user ?
<AppStackScreen/>
:
<AuthStackNavigationScreen/>
}
</>
So you cant navigate to tabbed from signin screen which does not exist.
The way you can handle this is update the user object maybe using a callback function or use context instead of state which will trigger a render of the AppNavigationContainer and the tabbed stack will automatically rendered instead of the auth stack. You wont need a navigate you should do the same for logout to where you will set the user to null.
You can refer more on Auth flows