diff --git a/native/navigation/invite-link-handler.react.js b/native/navigation/invite-link-handler.react.js new file mode 100644 index 000000000..98f0b0e41 --- /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 index a90e3c9f3..33d576248 100644 --- a/native/navigation/navigation-handler.react.js +++ b/native/navigation/navigation-handler.react.js @@ -1,86 +1,88 @@ // @flow import * as React from 'react'; 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'; import { NavContext, type NavAction } from './navigation-context.js'; import PolicyAcknowledgmentHandler from './policy-acknowledgment-handler.react.js'; import ThreadScreenTracker from './thread-screen-tracker.react.js'; import DevTools from '../redux/dev-tools.react.js'; import { useSelector } from '../redux/redux-utils.js'; import type { AppState } from '../redux/state-types.js'; import { usePersistedStateLoaded } from '../selectors/app-state-selectors.js'; const NavigationHandler: React.ComponentType<{}> = React.memo<{}>( function NavigationHandler() { const navContext = React.useContext(NavContext); const persistedStateLoaded = usePersistedStateLoaded(); const devTools = __DEV__ ? : null; if (!navContext || !persistedStateLoaded) { if (__DEV__) { return ( <> {devTools} ); } else { return null; } } const { dispatch } = navContext; return ( <> + {devTools} ); }, ); NavigationHandler.displayName = 'NavigationHandler'; type LogInHandlerProps = { +dispatch: (action: NavAction) => void, }; const LogInHandler = React.memo(function LogInHandler( props: LogInHandlerProps, ) { const { dispatch } = props; const hasCurrentUserInfo = useSelector(isLoggedIn); const hasUserCookie = useSelector( (state: AppState) => !!(state.cookie && state.cookie.startsWith('user=')), ); const loggedIn = hasCurrentUserInfo && hasUserCookie; const navLoggedIn = useIsAppLoggedIn(); const prevLoggedInRef = React.useRef(); React.useEffect(() => { if (loggedIn === prevLoggedInRef.current) { return; } prevLoggedInRef.current = loggedIn; if (loggedIn && !navLoggedIn) { dispatch({ type: (logInActionType: 'LOG_IN') }); } else if (!loggedIn && navLoggedIn) { dispatch({ type: (logOutActionType: 'LOG_OUT') }); } }, [navLoggedIn, loggedIn, dispatch]); return null; }); LogInHandler.displayName = 'LogInHandler'; export default NavigationHandler;