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