diff --git a/lib/actions/link-actions.js b/lib/actions/link-actions.js --- a/lib/actions/link-actions.js +++ b/lib/actions/link-actions.js @@ -50,10 +50,12 @@ }; }; -function useVerifyInviteLink(keyserverOverride?: { - +keyserverID: string, - +keyserverURL: string, -}): ( +function useVerifyInviteLink( + keyserverOverride?: ?{ + +keyserverID: string, + +keyserverURL: string, + }, +): ( request: InviteLinkVerificationRequest, ) => Promise { const keyserverID = keyserverOverride?.keyserverID ?? ashoatKeyserverID; diff --git a/lib/shared/invite-links.js b/lib/shared/invite-links.js --- a/lib/shared/invite-links.js +++ b/lib/shared/invite-links.js @@ -1,5 +1,8 @@ // @flow +import blobService from '../facts/blob-service.js'; +import { getBlobFetchableURL } from '../utils/blob-service.js'; + const inviteLinkErrorMessages: { +[string]: string } = { invalid_characters: 'Link cannot contain any spaces or special characters.', offensive_words: 'No offensive or abusive words allowed.', @@ -15,4 +18,36 @@ return `invite_${secret}`; } -export { inviteLinkErrorMessages, defaultErrorMessage, inviteLinkBlobHash }; +export type KeyserverOverride = { + +keyserverID: string, + +keyserverURL: string, +}; + +async function getKeyserverOverrideForAnInviteLink( + secret: string, +): Promise { + const blobURL = getBlobFetchableURL(inviteLinkBlobHash(secret)); + const result = await fetch(blobURL, { + method: blobService.httpEndpoints.GET_BLOB.method, + }); + if (result.status !== 200) { + return null; + } + const resultText = await result.text(); + const resultObject = JSON.parse(resultText); + if (resultObject.keyserverID && resultObject.keyserverURL) { + const keyserverURL: string = resultObject.keyserverURL; + return { + keyserverID: resultObject.keyserverID, + keyserverURL: keyserverURL.replace(/\/$/, ''), + }; + } + return null; +} + +export { + inviteLinkErrorMessages, + defaultErrorMessage, + inviteLinkBlobHash, + getKeyserverOverrideForAnInviteLink, +}; 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 @@ -15,6 +15,8 @@ type ParsedDeepLinkData, } from 'lib/facts/links.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; +import { getKeyserverOverrideForAnInviteLink } from 'lib/shared/invite-links.js'; +import type { KeyserverOverride } from 'lib/shared/invite-links.js'; import type { SetState } from 'lib/types/hook-types.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; @@ -79,9 +81,13 @@ }, []); useOnFirstLaunchEffect('ANDROID_REFERRER', checkInstallReferrer); + const [keyserverOverride, setKeyserverOverride] = + React.useState(undefined); + const inviteLinkSecret = React.useRef(null); + const loggedIn = useSelector(isLoggedIn); const dispatchActionPromise = useDispatchActionPromise(); - const validateLink = useVerifyInviteLink(); + const validateLink = useVerifyInviteLink(keyserverOverride); const navigation = useNavigation(); React.useEffect(() => { void (async () => { @@ -91,6 +97,8 @@ // We're setting this to null so that we ensure that each link click // results in at most one validation and navigation. setCurrentLink(null); + setKeyserverOverride(undefined); + inviteLinkSecret.current = null; const parsedData: ParsedDeepLinkData = parseDataFromDeepLink(currentLink); if (!parsedData) { @@ -99,28 +107,64 @@ if (parsedData.type === 'invite-link') { const { secret } = parsedData.data; + inviteLinkSecret.current = secret; + try { + const newKeyserverOverride = + await getKeyserverOverrideForAnInviteLink(secret); + setKeyserverOverride(newKeyserverOverride); + } catch (e) { + console.log('Error while downloading an invite link blob', e); + navigation.navigate<'InviteLinkModal'>({ + name: InviteLinkModalRouteName, + params: { + invitationDetails: { + status: 'invalid', + }, + secret, + }, + }); + } + } else if (parsedData.type === 'qr-code') { + navigation.navigate(SecondaryDeviceQRCodeScannerRouteName); + } + })(); + }, [currentLink, loggedIn, navigation]); + + React.useEffect(() => { + const secret = inviteLinkSecret.current; + if (keyserverOverride === undefined || !secret) { + return; + } + setKeyserverOverride(undefined); + + void (async () => { + let result; + try { const validateLinkPromise = validateLink({ secret }); void dispatchActionPromise( verifyInviteLinkActionTypes, validateLinkPromise, ); - const result = await validateLinkPromise; + 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); + } catch (e) { + console.log(e); + result = { + status: 'invalid', + }; } + + navigation.navigate<'InviteLinkModal'>({ + name: InviteLinkModalRouteName, + params: { + invitationDetails: result, + secret, + }, + }); })(); - }, [currentLink, dispatchActionPromise, loggedIn, navigation, validateLink]); + }, [dispatchActionPromise, keyserverOverride, navigation, validateLink]); const contextValue = React.useMemo( () => ({ diff --git a/web/invite-links/invite-link-handler.react.js b/web/invite-links/invite-link-handler.react.js --- a/web/invite-links/invite-link-handler.react.js +++ b/web/invite-links/invite-link-handler.react.js @@ -8,6 +8,8 @@ } from 'lib/actions/link-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; +import { getKeyserverOverrideForAnInviteLink } from 'lib/shared/invite-links.js'; +import type { KeyserverOverride } from 'lib/shared/invite-links.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; @@ -17,45 +19,80 @@ function InviteLinkHandler(): null { const inviteSecret = useSelector(state => state.navInfo.inviteSecret); + const inviteLinkSecret = React.useRef(null); + const [keyserverOverride, setKeyserverOverride] = + React.useState(undefined); const loggedIn = useSelector(isLoggedIn); const dispatchActionPromise = useDispatchActionPromise(); const dispatch = useDispatch(); - const validateLink = useVerifyInviteLink(); const { pushModal } = useModalContext(); React.useEffect(() => { - if (!inviteSecret || !loggedIn) { + void (async () => { + if (!inviteSecret || !loggedIn) { + return; + } + dispatch({ + type: updateNavInfoActionType, + payload: { inviteSecret: null }, + }); + setKeyserverOverride(undefined); + inviteLinkSecret.current = inviteSecret; + + try { + const newKeyserverOverride = + await getKeyserverOverrideForAnInviteLink(inviteSecret); + setKeyserverOverride(newKeyserverOverride); + } catch (e) { + console.error('Error while downloading an invite link blob', e); + pushModal( + , + ); + } + })(); + }, [dispatch, inviteSecret, loggedIn, pushModal]); + + const validateLink = useVerifyInviteLink(keyserverOverride); + React.useEffect(() => { + const secret = inviteLinkSecret.current; + if (keyserverOverride === undefined || !secret) { return; } - dispatch({ - type: updateNavInfoActionType, - payload: { inviteSecret: null }, - }); - const validateLinkPromise = validateLink({ secret: inviteSecret }); - void dispatchActionPromise( - verifyInviteLinkActionTypes, - validateLinkPromise, - ); + setKeyserverOverride(undefined); + void (async () => { - const result = await validateLinkPromise; - if (result.status === 'already_joined') { - return; + let result; + try { + const validateLinkPromise = validateLink({ secret }); + void dispatchActionPromise( + verifyInviteLinkActionTypes, + validateLinkPromise, + ); + + result = await validateLinkPromise; + if (result.status === 'already_joined') { + return; + } + } catch (e) { + console.error('Error while verifying an invite link', e); + result = { + status: 'invalid', + }; } + pushModal( , ); })(); - }, [ - dispatch, - dispatchActionPromise, - inviteSecret, - loggedIn, - pushModal, - validateLink, - ]); + }, [dispatchActionPromise, keyserverOverride, pushModal, validateLink]); return null; }