back button in react native - react-native

Is there is a way to hide back button for android devices and make it visible for iOS devices?
Following code displays Back button for both devices.
const Stack = createStackNavigator({
Login: {
screen : LoginTabs,
navigationOptions : {
header: null
}
},
Home : {
screen : Home,
navigationOptions : {
title : 'Dashboard',
headerStyle : {
backgroundColor : '#1565C0'
}
}
}
})

this is what you can do
static navigationOptions = ({ navigation }) => {
const { state } = navigation
if(Platform.OS === 'ios'){
return {
title: 'title',
headerLeft: (
<Button />
),
}
}else{
return {
title: 'title',
headerLeft: (
null
),
}
}
}

You can import Platform from react-native like:
import { Platform } from 'react-native'
And check in your component like:
if(Platform.OS === 'ios') {
//Render Back button
}

Related

React Navigation linking initialRouteName not working when opening app from quit state

I have followed the documentation of React Navigation and implemented an authentication flow as well the Linking mechanism for notifications via the linking prop of NavigationContainer.
When I open a notification when the app is running, e.g. the link https://domain/transactions/history, I am redirected to the screen TransactionHistory, and when I go back I am redirected to ChargingStations (as I want)
But, when I do the same thing from a quit state, I am being redirected as well but I can't go back to ChargingStations and I see the warning (The action GO_BACK was not handled by any navigator)..
My code is a follow:
App.tsx
export default class App extends React.Component<Props, State> {
public state: State;
public props: Props;
public centralServerProvider: CentralServerProvider;
public deepLinkingManager: DeepLinkingManager;
private appVersion: CheckVersionResponse;
private readonly navigationRef: React.RefObject<NavigationContainerRef<ReactNavigation.RootParamList>>;
private readonly appContext;
private initialUrl: string;
public constructor(props: Props) {
super(props);
this.navigationRef = React.createRef();
this.appContext = {
handleSignIn: () => this.setState({isSignedIn: true}),
handleSignOut: () => this.setState({isSignedIn: false})
};
this.state = {
switchTheme: false,
navigationState: null,
showAppUpdateDialog: false,
isSignedIn: undefined
};
}
public setState = (
state: State | ((prevState: Readonly<State>, props: Readonly<Props>) => State | Pick<State, never>) | Pick<State, never>,
callback?: () => void
) => {
super.setState(state, callback);
};
public async componentDidMount() {
// Get the central server
this.centralServerProvider = await ProviderFactory.getProvider();
// Setup notifications
await Notifications.initialize();
// Check for app updates
this.appVersion = await Utils.checkForUpdate();
// Set
this.setState({
showAppUpdateDialog: !!this.appVersion?.needsUpdate,
isSignedIn: true
});
}
public render() {
const { switchTheme, showAppUpdateDialog, isSignedIn } = this.state;
return switchTheme ? (
<NativeBaseProvider>
<GestureHandlerRootView style={{ flex: 1 }}>
{showAppUpdateDialog && (
<AppUpdateDialog appVersion={this.appVersion} close={() => this.setState({ showAppUpdateDialog: false })} />
)}
{isSignedIn == null ?
<Loading />
:
this.createRootNavigator()
}
</GestureHandlerRootView>
</NativeBaseProvider>
) : (
<NativeBaseProvider>
<View />
</NativeBaseProvider>
);
}
private buildLinking(): LinkingOptions<ReactNavigation.RootParamList> {
return (
{
prefixes: DeepLinkingManager.getAuthorizedURLs(),
getInitialURL: () => this.initialUrl,
subscribe: (listener) => {
// Listen for background notifications when the app is running,
const removeBackgroundNotificationListener = messaging().onNotificationOpenedApp(async (remoteMessage: Notification) => {
const canHandleNotification = await Notifications.canHandleNotificationOpenedApp(remoteMessage);
if (canHandleNotification) {
this.setState({isSignedIn: true}, () => listener(remoteMessage.data.deepLink));
}
});
return () => {
removeBackgroundNotificationListener();
};
},
config: {
screens: {
AuthNavigator: {
screens: {
Login: 'login'
}
},
AppDrawerNavigator: {
initialRouteName: 'ChargingStationsNavigator', // <-- Initial screen I would like to always be present as first screen when navigating
screens: {
ChargingStationsNavigator: {
initialRouteName: 'ChargingStations',
screens: {
ChargingStations: 'charging-stations/all'
}
},
InvoicesNavigator: 'invoices',
TransactionInProgressNavigator: {
screens: {
TransactionsInProgress: 'transactions/inprogress'
}
},
TransactionHistoryNavigator: {
screens: {
TransactionsHistory: 'transactions/history'
}
}
}
}
}
}
}
);
}
private createRootNavigator() {
const { isSignedIn } = this.state;
return (
<AuthContext.Provider value={this.appContext}>
<SafeAreaProvider>
<NavigationContainer
onReady={() => this.onReady()}
linking={this.buildLinking()}
ref={this.navigationRef}
onStateChange={(newState) => this.setState({navigationState: newState})}
initialState={this.state.navigationState}
>
<rootStack.Navigator initialRouteName="AuthNavigator" screenOptions={{ headerShown: false }}>
{isSignedIn ?
<rootStack.Screen name="AppDrawerNavigator" children={createAppDrawerNavigator} />
:
<rootStack.Screen options={{animationTypeForReplace: 'pop'}} name="AuthNavigator" children={createAuthNavigator} />
}
</rootStack.Navigator>
</NavigationContainer>
</SafeAreaProvider>
</AuthContext.Provider>
);
}
}
Expected behavior
I expect the ChargingStations screen to always be present as first screen, even from a quit state
Reproduction
https://github.com/sap-labs-france/ev-mobile/tree/upgrade_react_native
Platform
[X] Android
[X] iOS
Environment
[x] I've removed the packages that I don't use
package
version
#react-navigation/native
6.0.14
#react-navigation/drawer
6.5.1
#react-navigation/material-bottom-tabs
6.2.5
#react-navigation/stack
6.3.5
react-native-safe-area-context
4.4.1
react-native-screens
3.18.2
react-native-gesture-handler
2.8.0
react-native-reanimated
2.13.0
react-native
0.70.6
node
16.13.0
npm or yarn
9.1.2
You should define a nested linking route: Docs
const config = {
screens: {
Home: {
screens: {
Profile: 'users/:id',
},
},
},
};
TransactionHistoryNavigator: {
screens: {
ChargingStationsNavigator: {
screens: {
TransactionsHistory: 'transactions/history'
}
}
}
}
Maybe you can try to redeclare initial route (unfortunately I have currently no access to verify my suggestion):
TransactionHistoryNavigator: {
screens: {
TransactionsHistory: {
initialRouteName: 'ChargingStationsNavigator', //Define initial route here
}
}
}
I have managed to obtain the desired behavior by using following code in the drawer navigator props.
backBehavior={'initialRoute'}

How do I create an embedded Stack navigator inside a React Native formSheet modal?

Like so:
I'm running react-navigation v4.
First you have to follow the tutorial on how to set up a react-navigation modal, the one that has a jump animation and doesn't look like the native formSheet. You have to set up a stack navigator with your root navigator as one child and the modal as another:
And it scales, because you can have more than one of these modals as children.
The code for this is the following:
const RootNavigator = createStackNavigator(
{
index: { screen: AppNavigator },
[NC.SCREEN_ROOM_INFO]: { screen: RoomInfoNavigator },
[NC.SCREEN_CHAT_CREATE]: { screen: RoomCreateNavigator },
[NC.SCREEN_CHAT_SEARCH]: { screen: ChatSearchNavigator },
[NC.SCREEN_CHAT_GIF_PICKER]: { screen: GifPickerNavigator }
},
{
mode: 'modal',
headerMode: 'none',
transitionConfig: () => ({
transitionSpec: {
duration: 0
}
}),
transparentCard: true
}
)
Then you need to implement these, from my example, 4 navigators that will be displayed as modals each like so:
// Here you'll specify the screens you'll navigate in this modal, starting from index.
const RoomInfoStack = createStackNavigator({
index: { screen: NavigableRoomInfo },
[NC.SCREEN_ROOM_ROSTER]: { screen: NavigableRoomRoster },
[NC.SCREEN_ROOM_NOTIFICATION_PREFERENCES]: { screen: NavigableRoomNotificationPreferences },
[NC.SCREEN_ROOM_EDIT]: { screen: NavigableRoomEdit }
})
type NavigationComponent<T = any> = {
navigation?: NavigationScreenProp<NavigationState, T>
}
type Props = NavigationComponent
// THIS code is from the react-navigation tutorial on how to make a react-navigation modal:
// https://reactnavigation.org/docs/4.x/custom-navigators/#extending-navigators
class RoomInfoNavigator extends React.Component<Props> {
static router = {
...RoomInfoStack.router,
getStateForAction: (action, lastState) => {
// check for custom actions and return a different navigation state.
return RoomInfoStack.router.getStateForAction(action, lastState)
}
}
constructor(props) {
super(props)
this.onClose = this.onClose.bind(this)
}
onClose() {
this.props.navigation?.goBack()
}
// And here is the trick, you'll render an always open RN formSheet
// and link its dismiss callbacks to the goBack action in react-navigation
// and render your stack as its children, redirecting the navigator var to it in props.
render() {
return (
<Modal
visible={true}
animationType={'slide'}
supportedOrientations={['portrait', 'landscape']}
presentationStyle={'formSheet'}
onRequestClose={() => this.onClose()}
onDismiss={() => this.onClose()}
>
<RoomInfoStack {...this.props} />
</Modal>
)
}
}
export { RoomInfoNavigator }
This export is what our root stack imported before. Then you just need to render the screens, I have a pattern that I do to extract the navigation params to props in case this screen is ever displayed without navigation:
const NavigableRoomInfo = (props: NavigationComponent<RoomInfoProps>) => {
const roomInfo = props.navigation!.state!.params!.roomInfo
const roomInfoFromStore = useRoomFromStore(roomInfo.id)
// Here I update the navigation params so the navigation bar also updates.
useEffect(() => {
props.navigation?.setParams({
roomInfo: roomInfoFromStore
})
}, [roomInfoFromStore])
// You can even specify a different Status bar color in case it's seen through modal view:
return (
<>
<StatusBar barStyle="default" />
<RoomInfo roomInfo={roomInfoFromStore} navigation={props.navigation} />
</>
)
}
Sources:
https://reactnavigation.org/docs/4.x/modal
https://reactnavigation.org/docs/4.x/custom-navigators/#extending-navigators

Confirm/warn dialog on back

Like in the web browser, we have onBeforeUnload (vs onUnload) to show a alert or some warning "there is unsaved data - are you sure you want to go back".
I am trying to do the same. I couldn't find anything in the docs of react-navigation.
I thought of doing something real hacky like this, but I don't know if its the right way:
import React, { Component } from 'react'
import { StackNavigator } from 'react-navigation'
export default function ConfirmBackStackNavigator(routes, options) {
const StackNav = StackNavigator(routes, options);
return class ConfirmBackStackNavigatorComponent extends Component {
static router = StackNav.router;
render() {
const { state, goBack } = this.props.navigation;
const nav = {
...this.props.navigation,
goBack: () => {
showConfirmDialog()
.then(didConfirm => didConfirm && goBack(state.key))
}
};
return ( <StackNav navigation = {nav} /> );
}
}
}
React navigation 5.7 has added support for it:
function EditText({ navigation }) {
const [text, setText] = React.useState('');
const hasUnsavedChanges = Boolean(text);
React.useEffect(
() =>
navigation.addListener('beforeRemove', (e) => {
if (!hasUnsavedChanges) {
// If we don't have unsaved changes, then we don't need to do anything
return;
}
// Prevent default behavior of leaving the screen
e.preventDefault();
// Prompt the user before leaving the screen
Alert.alert(
'Discard changes?',
'You have unsaved changes. Are you sure to discard them and leave the screen?',
[
{ text: "Don't leave", style: 'cancel', onPress: () => {} },
{
text: 'Discard',
style: 'destructive',
// If the user confirmed, then we dispatch the action we blocked earlier
// This will continue the action that had triggered the removal of the screen
onPress: () => navigation.dispatch(e.data.action),
},
]
);
}),
[navigation, hasUnsavedChanges]
);
return (
<TextInput
value={text}
placeholder="Type something…"
onChangeText={setText}
/>
);
}
Doc: https://reactnavigation.org/docs/preventing-going-back
On current screen set
this.props.navigation.setParams({
needUserConfirmation: true,
});
In your Stack
const defaultGetStateForAction = Stack.router.getStateForAction;
Stack.router.getStateForAction = (action, state) => {
if (state) {
const { routes, index } = state;
const route = get(routes, index);
const needUserConfirmation = get(route.params, 'needUserConfirmation');
if (
needUserConfirmation &&
['Navigation/BACK', 'Navigation/NAVIGATE'].includes(action.type)
) {
Alert.alert('', "there is unsaved data - are you sure you want to go back", [
{
text: 'Close',
onPress: () => {},
},
{
text: 'Confirm',
onPress: () => {
delete route.params.needUserConfirmation;
state.routes.splice(index, 1, route);
NavigationService.dispatch(action);
},
},
]);
// Returning null from getStateForAction means that the action
// has been handled/blocked, but there is not a new state
return null;
}
}
return defaultGetStateForAction(action, state);
};
Notes,
Navigating without the navigation prop
https://reactnavigation.org/docs/en/navigating-without-navigation-prop.html
NavigationService.js
function dispatch(...args) {
_navigator.dispatch(...args);
}
This can be accomplished by displaying a custom back button in the header, and capturing the hardware back-event before it bubbles up to the navigator.
We'll first configure our page to show a custom back button by overriding the navigation options:
import React, { Component } from 'react'
import { Button } from 'react-native'
function showConfirmDialog (onConfirmed) { /* ... */ }
class MyPage extends Component {
static navigationOptions ({ navigation }) {
const back = <Button title='Back' onPress={() => showConfirmDialog(() => navigation.goBack())} />
return { headerLeft: back }
}
// ...
}
The next step is to override the hardware back button. For that we'll use the package react-navigation-backhandler:
// ...
import { AndroidBackHandler } from 'react-navigation-backhandler'
class MyPage extends Component {
// ...
onHardwareBackButton = () => {
showConfirmDialog(() => this.props.navigation.goBack())
return true
}
render () {
return (
<AndroidBackHandler onBackPress={this.onHardwareBackButton}>
{/* ... */}
</AndroidBackHandler>
)
}
}

React Navigation changing tab icons on tab navigator dynamically

So I am new to react native and redux. The app is already configured (by someone else) to have react-navigation and redux. Now we're using a TabNavigator (bottom) for our menu and this TabNavigator also contains the Login button. Now what I want to do is when the user logs in, the Login button (with text and icon) becomes Logout.
Is there a way to do that? Also my TabNavigator is in a separate file.
What I want is something like this:
TabNavigator(
{
...other screens,
//show this only if not logged in
Login: {
screen: LoginScreen
},
//show this only if logged in
Logout: {
screen: //There should be no screen here just the logout functionality
}
},
{...options here}
)
Thanks in advance.
You can do it using Redux:
AuthIcon.js:
const LOGGED_IN_IMAGE = require(...)
const LOGGED_OUT_IMAGE = require(...)
class AuthIcon extends React.Component {
render() {
const { loggedIn, focused, tintColor } = this.props
// loggedIn is what tells your app when the user is logged in, you can call it something else, it comes from redux
return (
<View>
<Image source={loggedIn ? LOGGED_IN_IMAGE : LOGGED_OUT_IMAGE} resizeMode='stretch' style={{ tintColor: focused ? tintColor : null, width: 21, height: 21 }} />
</View>
)
}
}
const ConnectedAuthIcon = connect(state => {
const { loggedIn } = state.auth
return { loggedIn }
})(AuthIcon)
export default ConnectedAuthIcon;
then inside your TabNavigator file:
import ConnectedAuthIcon from './AuthIcon.js'
export default TabNavigator({
Auth: {
screen: Auth,
navigationOptions: ({ navigation }) => ({
tabBarLabel: null,
tabBarIcon: ({ tintColor, focused }) => <ConnectedAuthIcon tintColor={tintColor} focused={focused} />,
title: "Auth"
})
}
})
Edit:
In your Auth.js:
class Auth extends React.Component {
render() {
const { loggedIn } = this.props
if (loggedIn) {
return <Profile />
} else {
return <Login />
}
}
}
export default connect(state => {
const { loggedIn } = state.auth
return { loggedIn }
})(Auth)

how to show/hide icons in a react native toolbar

I need to hide a hamburger-menu/location icon on the toolbar while the login screen is active. One option I thought would work is to have the icons set to a empty string by default. And use the EventEmitter in the success callback function in my Login.js & Logout.js, and then listen for it in my toolbar component. Sending a bool to determine show/hide. I am not sure if there is a better way of doing this so I'm up for suggestions. The Emit/Listen events work as expected. The issue is how I use a variable to apply the empty string or named icon.
here is the Toolbar Component.
export default class Toolbar extends Component {
//noinspection JSUnusedGlobalSymbols
static contextTypes = {
navigator: PropTypes.object
};
//noinspection JSUnusedGlobalSymbols
static propTypes = {
onIconPress: PropTypes.func.isRequired
};
//noinspection JSUnusedGlobalSymbols
constructor(props) {
super(props);
this.state = {
title: AppStore.getState().routeName,
theme: AppStore.getState().theme,
menuIcon: '',
locationIcon: ''
};
}
emitChangeMarket() {
AppEventEmitter.emit('onClickEnableNavigation');
}
//noinspection JSUnusedGlobalSymbols
componentDidMount = () => {
AppStore.listen(this.handleAppStore);
AppEventEmitter.addListener('showIcons', this.showIcons.bind(this));
};
//noinspection JSUnusedGlobalSymbols
componentWillUnmount() {
AppStore.unlisten(this.handleAppStore);
}
handleAppStore = (store) => {
this.setState({
title: store.routeName,
theme: store.theme
});
};
showIcons(val) {
if (val === true) {
this.setState({
menuIcon: 'menu',
locationIcon: 'location-on'
});
} else {
this.setState({
menuIcon: '',
locationIcon: ''
});
}
}
render() {
let menuIcon = this.state.menuIcon;
let locationIcon = this.state.locationIcon;
const {navigator} = this.context;
const {theme} = this.state;
const {onIconPress} = this.props;
return (
<MaterialToolbar
title={navigator && navigator.currentRoute ? navigator.currentRoute.title : 'Metro Tracker Login'}
primary={theme}
icon={navigator && navigator.isChild ? 'keyboard-backspace' : {menuIcon}}
onIconPress={() => navigator && navigator.isChild ? navigator.back() : onIconPress()}
actions={[{
icon: {locationIcon},
onPress: this.emitChangeMarket.bind(this)
}]}
rightIconStyle={{
margin: 10
}}
/>
);
}
}
The warning message I get is the:
Invalid prop icon of type object supplied to toolbar, expected a string.
how can I pass a string while wrapped in variable brackets?
Or if easier how can I hide/show the entire toolbar? either way works.
Try removing the brackets around menuIcon and locationIcon:
...
icon={navigator && navigator.isChild ? 'keyboard-backspace' : menuIcon}
...
icon: locationIcon,
...