diff --git a/native/navigation/invite-link-handler.react.js b/native/navigation/invite-link-handler.react.js
new file mode 100644
--- /dev/null
+++ b/native/navigation/invite-link-handler.react.js
@@ -0,0 +1,82 @@
+// @flow
+
+import { useNavigation } from '@react-navigation/native';
+import * as React from 'react';
+import { Linking } from 'react-native';
+
+import {
+ verifyInviteLink,
+ verifyInviteLinkActionTypes,
+} from 'lib/actions/link-actions.js';
+import { isLoggedIn } from 'lib/selectors/user-selectors.js';
+import {
+ useDispatchActionPromise,
+ useServerCall,
+} from 'lib/utils/action-utils.js';
+
+import { InviteLinkModalRouteName } from './route-names.js';
+import { useSelector } from '../redux/redux-utils.js';
+
+function InviteLinkHandler(): null {
+ const [currentLink, setCurrentLink] = React.useState(null);
+
+ React.useEffect(() => {
+ // This listener listens for an event where a user clicked a link when the
+ // app was running
+ Linking.addEventListener('url', ({ url }) => setCurrentLink(url));
+ // We're also checking if the app was opened by using an invite link.
+ // In that case the listener won't be called and we're instead checking
+ // if the initial URL is set.
+ (async () => {
+ const initialURL = await Linking.getInitialURL();
+ if (initialURL) {
+ setCurrentLink(initialURL);
+ }
+ })();
+ }, []);
+
+ const loggedIn = useSelector(isLoggedIn);
+ const dispatchActionPromise = useDispatchActionPromise();
+ const validateLink = useServerCall(verifyInviteLink);
+ const navigation = useNavigation();
+ React.useEffect(() => {
+ (async () => {
+ if (!loggedIn || !currentLink) {
+ return;
+ }
+ // We're setting this to null so that we ensure that each link click
+ // results in at most one validation and navigation.
+ setCurrentLink(null);
+
+ const secret = parseSecret(currentLink);
+ if (!secret) {
+ return;
+ }
+
+ const validateLinkPromise = validateLink({ secret });
+ dispatchActionPromise(verifyInviteLinkActionTypes, validateLinkPromise);
+ const result = await validateLinkPromise;
+ if (result.status === 'already_joined') {
+ return;
+ }
+
+ navigation.navigate<'InviteLinkModal'>({
+ name: InviteLinkModalRouteName,
+ params: {
+ invitationDetails: result,
+ secret,
+ },
+ });
+ })();
+ }, [currentLink, dispatchActionPromise, loggedIn, navigation, validateLink]);
+
+ return null;
+}
+
+const urlRegex = /invite\/(\S+)$/;
+function parseSecret(url: string) {
+ const match = urlRegex.exec(url);
+ return match?.[1];
+}
+
+export default InviteLinkHandler;
diff --git a/native/navigation/navigation-handler.react.js b/native/navigation/navigation-handler.react.js
--- a/native/navigation/navigation-handler.react.js
+++ b/native/navigation/navigation-handler.react.js
@@ -5,6 +5,7 @@
import { isLoggedIn } from 'lib/selectors/user-selectors.js';
import { logInActionType, logOutActionType } from './action-types.js';
+import InviteLinkHandler from './invite-link-handler.react.js';
import ModalPruner from './modal-pruner.react.js';
import NavFromReduxHandler from './nav-from-redux-handler.react.js';
import { useIsAppLoggedIn } from './nav-selectors.js';
@@ -43,6 +44,7 @@
+
{devTools}
>
);