I'm trying to enable deeplink in my React Native project, and have it working for all use-cases where the deeplink opens the app from the background, not from the first load. I believe the issue is that my path that I am deep-linking, is not available until after the "loading screen" has gone away.
For example I have this setup as my RootStack.Navigator
<RootStack.Navigator
detachInactiveScreens={false}
headerMode="none"
initialRouteName={"AppTabsScreen"}
screenOptions={{ animationEnabled: false }}
mode="modal"
>
{isLoading ? (
<RootStack.Screen
name="LoadingScreen"
component={LoadingScreen}
options={{ animationEnabled: true }}
/>
) : user ? (
<RootStack.Screen name="AppTabsScreen" component={AppTabsScreen} />
) : (
<RootStack.Screen name="AuthStackScreen" component={AuthStackScreen} />
)}
...
And my linking config as:
const deepLinksConf = {
screens: {
AppTabsScreen: {
screens: {
initialRouteName: "Activity",
Activity: {
screens: {
Activity: "activity",
Details: "workout/:userId/:id",
},
},
Goals: {
screens: {
Goals: "goal",
GoalDetail: "goal/:id",
},
},
Settings: "settings",
Profile: "profile",
},
},
}
};
In my getInitialURL() function, I get the URL correctly, but I believe the path is already on the LoadingScreen, and not the AppTabsScreen, which causes it not to fire. Is there anyway around this situation? How can I delay the deeplink to wait until the AppTabScreen is present to process the link? This works fine when the app is the fore or background, but never at first start.
....
async getInitialURL() {
console.log("fired");
// Check if app was opened from a deep link
const url = await Linking.getInitialURL();
console.log("deeplinking");
console.log(url);
if (url != null) {
console.log("just returning");
return url;
}
// Check if there is an initial firebase notification
const message = await messaging().getInitialNotification();
// Get deep link from data
// if this is undefined, the app will open the default/home page
return messsage.data?.link;
},
I am using react-navigation, and my main navigation controller is as follows:
<NavigationContainer
linking={linking}
theme={scheme === "dark" ? DarkTheme : DefaultTheme}
ref={navigationRef}
onReady={() => {
if (navigationRef.current) {
routeNameRef.current = navigationRef?.current.getCurrentRoute().name;
}
}}
onStateChange={async () => {
const previousRouteName = routeNameRef.current;
const currentRouteName = navigationRef?.current.getCurrentRoute().name;
if (previousRouteName !== currentRouteName) {
await analytics().logScreenView({
screen_name: currentRouteName,
screen_class: currentRouteName,
});
}
// Save the current route name for later comparision
routeNameRef.current = currentRouteName;
}}
>
<FlashMessage position="top" />
<RootStackScreen />
</NavigationContainer>
Related
const [cred, setcred] = React.useState(false);
const [state, setState] = React.useState(false)
React.useEffect(() => {
isLogg();
}, []);
const isLogg = async () => {
const credentials = await Keychain.getGenericPassword();
if (credentials) {
setcred(true);
//SplashScreen.hide();
} else {
// await Keychain.resetGenericPassword();
setcred(false);
SplashScreen.hide();
}
};
const Stack = () => {
return (
<StackNav.Navigator>
{!cred ? (
<StackNav.Screen
name={SCREEN.AuthNavigation}
component={AuthNavigation}
options={{ headerShown: false }}
/>
) : (
<StackNav.Screen
name={SCREEN.PageNavigation}
component={PageNavigation}
options={{ headerShown: false }}
/>
)}
</StackNav.Navigator>
);
};
Here I am checking with keychain.
If the keychain is coming, it should go to the dashboard without showing the login screen, otherwise it should redirect to the login screen, but it doesn't work.
The application freezes.
how do i solve this
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'}
I have a typescript react-native application. I have used navigation with some sucess but in this case, no matter what I do, the id, filename, and file are all undefined.
Here is the code with the issue. I know according to react-native navigation doing what I'm doing with the file isn't necessary great coding practice, but this is just displaying a file, so it's not a huge deal. (I am storing the filename and id in a sqlite database). I added the useState hoping that the file gets passed or change that it can change the state.
export type Props = {
navigation: PropTypes.func.isRequired;
id:PropTypes.int.isRequired;
filename:Protypes.string.isRequired;
file:{
name: PropTypes.string.isRequired;
uri: PropTypes.path.isRequired;
type: PropTypes.mime.isRequired};
};
const FileViewScreen: React.FC<Props> = ({navigation,id,filename,file}) => {
console.log("File View Screen?")
console.log("currentFile");
console.log(id)
console.log(currentFile)
console.log(filename)
console.log(file)
const [currentFile,setCurrentFile] = useState(file);
Here is where the user gets routed to the FileScreen. Here I was testing to see if any id is passed, I'm aware that the id needs changed to the id and not 1 but this was testing.
const HomeScreen: React.FC<Props> = ({navigation}) => {
const [loading, setLoading] = useState(false);
const [file, setFile] = useState({});
const [files, setFiles] = useState([]);
const downloadFile = async () => {
try {
...
const newEntry = {
name: 'ImageFileName' + Math.random().toString(),
uri: result.path,
type: result.mime,
};
const res = await addFile(result.path);
console.log(res)
navigation.navigate('FileView', { id:1,filename:res,file:newEntry });
} catch (error) {
console.log('downloadFile error', error);
}
};
return (
<View style={styles}>
<Text>Welcome Home</Text>
{loading && <ActivityIndicator size={'large'} color="#000" />}
{!loading && (
<>
<Button
title="Start Recording"
onPress={downloadFile}
/>
Here is the addFile function. I don't think this matters but I've been wrong before. Here
export const addFile = file_path => {
db.transaction(txn => {
console.log("db transaction")
console.log(file_path)
const response = txn.executeSql(
'INSERT INTO files(file_path,uploaded) VALUES (' +
file_path +
',' +
false +
')',
(sqlTxn, res) => {
console.log("adding")
console.log(`${file_path} video added successfully`);
return file_path;
},
error => {
console.log('error on adding file ' + error.message);
return 0;
},
);
});
console.log(resopnse)
};
In my app.js (i do have a working register and, login, home screen. Right now this is the only time I have an issue.
<NavigationContainer>
<Stack.Navigator initialRouteName={initalRoute}>
<Stack.Screen name="Login">
{props => (
<LoginScreen {...props} setToken={setUserToken} setUser={setUser} />
)}
</Stack.Screen>
<Stack.Screen name="Home">
{props => (
<HomeScreen {...props}/>
)}
</Stack.Screen>
<Stack.Screen name="Register" component={RegisterScreen} />
<Stack.Screen name="FileView">
{props =>(
<FileViewScreen {...props} />
)}
</Stack.Screen>
</NavigationContainer>
Things that I've tried.
I tried to change the RecordingView in app.js to make sure it's specifically passing props
I've changed props to be only an id, only a filename, or only the newentry.
I've tried to set the state as the file in case it gets passed later.
Things that I haven't tried
I haven't put this in a button. That's the main thing I haven't been able to find if navigation.navigate only works on a push event. I don't see any documentation stating that.
If your FileViewScreen is a child component of some parent view then id,filename,file will be available from component props object. If instead you navigate to FileViewScreen from another screen then id,filename,file will be part of route prop.
To account for both use cases you could so something like this
const FileViewScreen: React.FC<Props> = (props) {
// try extracting props from root prop object
let { id,filename,file } = props;
// if navigation route params are available,
// then extract props from route.params instead
// you could also check if id, filename, file etc are null
// before extracting from route.params
const { route } = props;
if (route && route.params) {
({ id,filename,file } = route.params);
}
...
}
Given the following setup:
const config = {
screens: {
Discover: 'discover',
Theater: {
screens: {
Watch: 'watch/:name?',
},
},
},
}
export const linking: LinkingOptions<any> = {
prefixes: ['test://'],
config,
}
const Tab = createBottomTabNavigator()
export function TheaterStackScreen({route}: Props) {
return (
<Tab.Navigator screenOptions={{headerShown: false}}>
<Tab.Screen name="Watch" component={WatchScreen} initialParams={{name: route.params.name}} />
</Tab.Navigator>
)
}
const Stack = createNativeStackNavigator()
export function StackScreen() {
return (
<Stack.Navigator screenOptions={{headerShown: false}}>
<Stack.Screen name="Discover" component={DiscoverScreen} />
<Stack.Screen name="Theater" component={TheaterStackScreen} />
</Stack.Navigator>
)
}
...and running:
npx uri-scheme open "test://watch/test" --android
I recieve:
Android: Opening URI "test://watch/test" in emulator
...with no errors, however, there is no navigation within the app...
and this warning:
WARN The navigation state parsed from the URL contains routes not present in the root navigator. This usually means that the linking configuration doesn't match the navigation structure. See https://reactnavigation.org/docs/configuring-links for more details on how to specify a linking configuration.
Does something look inherently wrong with this setup?
current setup:
"#react-navigation/bottom-tabs": "6.3.1",
"#react-navigation/native": "6.0.10",
"#react-navigation/native-stack": "6.6.2",
"react-native": "0.68.2",
This is being run on device, but I don't get any different behavior on sim either. Ive attempted with debugger on and off with no difference.
As always any and all direction is appreciated so thanks in advance!
Edit with more details:
const {createNavigationContainer} = Bugsnag.getPlugin('reactNavigation') as BugsnagPluginReactNavigationResult
const BugsnagNavigationContainer = createNavigationContainer(NavigationContainer)
const Stack = createNativeStackNavigator()
...
<BugsnagNavigationContainer linking {linking} onStateChange={(state: any) => {
const newRouteNam = Analytics.getActiveRouteName(state)
if (routeName !== newRouteName) {
analyticsClient.screen(newRouteName)
setRouteName(newRouteName)
}
}}>
<Stack.Navigator screenOptions={{headerShown: false}}>
<Stack.Screen name="Main" component={StackScreen} />
</Stack.Navigator>
</BugsnagNavigationContainer>
Edit 2:
Unfortunately, updating the config had no effect, I still see the tracking but no navigation or opening of the specified path:
const config = {
screens: {
Main: {
screens: {
Discover: 'discover',
Theater: {
screens: {
Watch: 'watch/:name',
},
},
},
},
},
}
results:
TRACK (Deep Link Opened) event saved {"event": "Deep Link Opened", "properties": {"referring_application": "android-app://com.android.shell", "url": "test://watch"}, "type": "track"}
When I receive the push notification, the app should navigate to specific screen on react-navigation 5.
How can I do it?
Currently, push notification listener is set on Routing as following:
const Routing = () => {
useEffect(() => {
OneSignal.init(...);
OneSignal.inFocusDisplaying(2);
OneSignal.promptForPushNotificationsWithUserResponse();
OneSignal.addEventListener('received', onReceived);
OneSignal.addEventListener('opened', onOpened);
}, []);
const onReceived = (notification) => {
console.log("Notification received: ", notification);
};
const onOpened = (openResult) => {
if (_.get(openResult, 'notification.payload.additionalData.profile_id', null)) {
// Here, the app should navigate to the screen of MainStack.
}
};
return (
<RootStack.Navigator>
<RootStack.Screen name="Auth" component={AuthStack} />
<RootStack.Screen name="Main" component={MainStack} />
</RootStack.Navigator>
);
};