diff --git a/lib/facts/links.js b/lib/facts/links.js --- a/lib/facts/links.js +++ b/lib/facts/links.js @@ -5,18 +5,6 @@ return `https://comm.app/invite/${secret}`; } -function parseSecretFromInviteLinkURL(url: string): ?string { - const urlRegex = /invite\/(\S+)$/; - const match = urlRegex.exec(url); - return match?.[1]; -} - -function parseInstallReferrerFromInviteLinkURL(referrer: string): ?string { - const referrerRegex = /utm_source=(invite\/(\S+))$/; - const match = referrerRegex.exec(referrer); - return match?.[1]; -} - /* QR Code */ function qrCodeLinkUrl(aes256Param: string, ed25519Param: string): string { const keys = { @@ -27,16 +15,49 @@ return `comm://qr-code/${keysString}`; } -function parseKeysFromQRCodeURL(url: string): ?string { - const urlRegex = /qr-code\/(\S+)$/; - const match = urlRegex.exec(url); +/* Deep Link Utils */ +function parseInstallReferrerFromInviteLinkURL(referrer: string): ?string { + const referrerRegex = /utm_source=(invite\/(\S+))$/; + const match = referrerRegex.exec(referrer); return match?.[1]; } +type ParsedInviteLinkData = { + +type: 'invite-link', + +data: { +secret: string }, +}; +type ParsedQRCodeData = { + +type: 'qr-code', + +data: { +keys: string }, +}; +export type ParsedDeepLinkData = ParsedInviteLinkData | ParsedQRCodeData | null; + +function parseDataFromDeepLink(url: string): ParsedDeepLinkData { + const inviteLinkSecretRegex = /invite\/(\S+)$/; + const qrCodeKeysRegex = /qr-code\/(\S+)$/; + + const inviteLinkSecretMatch = inviteLinkSecretRegex.exec(url); + if (inviteLinkSecretMatch) { + return { + type: 'invite-link', + data: { secret: inviteLinkSecretMatch[1] }, + }; + } + + const qrCodeKeysMatch = qrCodeKeysRegex.exec(url); + if (qrCodeKeysMatch) { + return { + type: 'qr-code', + data: { keys: qrCodeKeysMatch[1] }, + }; + } + + return null; +} + export { inviteLinkUrl, - parseSecretFromInviteLinkURL, - parseInstallReferrerFromInviteLinkURL, qrCodeLinkUrl, - parseKeysFromQRCodeURL, + parseInstallReferrerFromInviteLinkURL, + parseDataFromDeepLink, }; diff --git a/native/navigation/deep-links-context-provider.react.js b/native/navigation/deep-links-context-provider.react.js --- a/native/navigation/deep-links-context-provider.react.js +++ b/native/navigation/deep-links-context-provider.react.js @@ -10,8 +10,9 @@ verifyInviteLinkActionTypes, } from 'lib/actions/link-actions.js'; import { - parseSecretFromInviteLinkURL, parseInstallReferrerFromInviteLinkURL, + parseDataFromDeepLink, + type ParsedDeepLinkData, } from 'lib/facts/links.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import type { SetState } from 'lib/types/hook-types.js'; @@ -20,7 +21,10 @@ useServerCall, } from 'lib/utils/action-utils.js'; -import { InviteLinkModalRouteName } from './route-names.js'; +import { + InviteLinkModalRouteName, + SecondaryDeviceQRCodeScannerRouteName, +} from './route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { useOnFirstLaunchEffect } from '../utils/hooks.js'; @@ -48,7 +52,7 @@ const subscription = Linking.addEventListener('url', ({ url }) => setCurrentLink(url), ); - // We're also checking if the app was opened by using an invite link. + // We're also checking if the app was opened by using a link. // In that case the listener won't be called and we're instead checking // if the initial URL is set. (async () => { @@ -91,25 +95,30 @@ // results in at most one validation and navigation. setCurrentLink(null); - const secret = parseSecretFromInviteLinkURL(currentLink); - if (!secret) { + const parsedData: ParsedDeepLinkData = parseDataFromDeepLink(currentLink); + if (!parsedData) { return; } - const validateLinkPromise = validateLink({ secret }); - dispatchActionPromise(verifyInviteLinkActionTypes, validateLinkPromise); - const result = await validateLinkPromise; - if (result.status === 'already_joined') { - return; + if (parsedData.type === 'invite-link') { + const { secret } = parsedData.data; + 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, + }, + }); + } else if (parsedData.type === 'qr-code') { + navigation.navigate(SecondaryDeviceQRCodeScannerRouteName); } - - navigation.navigate<'InviteLinkModal'>({ - name: InviteLinkModalRouteName, - params: { - invitationDetails: result, - secret, - }, - }); })(); }, [currentLink, dispatchActionPromise, loggedIn, navigation, validateLink]); 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 @@ -11,7 +11,6 @@ import { useIsAppLoggedIn } from './nav-selectors.js'; import { NavContext, type NavAction } from './navigation-context.js'; import PolicyAcknowledgmentHandler from './policy-acknowledgment-handler.react.js'; -import QRCodeLinkHandler from './qr-code-link-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'; @@ -44,7 +43,6 @@ - {devTools} ); diff --git a/native/navigation/qr-code-link-handler.react.js b/native/navigation/qr-code-link-handler.react.js deleted file mode 100644 --- a/native/navigation/qr-code-link-handler.react.js +++ /dev/null @@ -1,51 +0,0 @@ -// @flow - -import { useNavigation } from '@react-navigation/native'; -import * as React from 'react'; -import { Linking } from 'react-native'; - -import { parseKeysFromQRCodeURL } from 'lib/facts/links.js'; -import { isLoggedIn } from 'lib/selectors/user-selectors.js'; - -import { SecondaryDeviceQRCodeScannerRouteName } from './route-names.js'; -import { useSelector } from '../redux/redux-utils.js'; - -function QRCodeLinkHandler(): null { - const [currentLink, setCurrentLink] = React.useState(null); - - React.useEffect(() => { - const subscription = Linking.addEventListener('url', ({ url }) => - setCurrentLink(url), - ); - (async () => { - const initialURL = await Linking.getInitialURL(); - if (initialURL) { - setCurrentLink(initialURL); - } - })(); - - return () => subscription.remove(); - }, []); - - const loggedIn = useSelector(isLoggedIn); - const { navigate } = useNavigation(); - - React.useEffect(() => { - if (!loggedIn || !currentLink) { - return; - } - - setCurrentLink(null); - - const keys = parseKeysFromQRCodeURL(currentLink); - if (!keys) { - return; - } - - navigate(SecondaryDeviceQRCodeScannerRouteName); - }, [currentLink, loggedIn, navigate]); - - return null; -} - -export default QRCodeLinkHandler; diff --git a/native/profile/secondary-device-qr-code-scanner.react.js b/native/profile/secondary-device-qr-code-scanner.react.js --- a/native/profile/secondary-device-qr-code-scanner.react.js +++ b/native/profile/secondary-device-qr-code-scanner.react.js @@ -5,7 +5,7 @@ import * as React from 'react'; import { View } from 'react-native'; -import { parseKeysFromQRCodeURL } from 'lib/facts/links.js'; +import { parseDataFromDeepLink } from 'lib/facts/links.js'; import { useStyles } from '../themes/colors.js'; import Alert from '../utils/alert.js'; @@ -39,9 +39,10 @@ const onConnect = React.useCallback((barCodeEvent: BarCodeEvent) => { const { data } = barCodeEvent; - const keysMatch = parseKeysFromQRCodeURL(data); + const parsedData = parseDataFromDeepLink(data); + const keysMatch = parsedData?.data?.keys; - if (!keysMatch) { + if (!parsedData || !keysMatch) { Alert.alert( 'Scan failed', 'QR code does not contain a valid pair of keys.',