diff --git a/lib/shared/invite-links.js b/lib/shared/invite-links.js index 3697c915d..3b12c4c96 100644 --- a/lib/shared/invite-links.js +++ b/lib/shared/invite-links.js @@ -1,37 +1,44 @@ // @flow +import type { AuthMetadata } from './identity-client-context.js'; import blobService from '../facts/blob-service.js'; import { getBlobFetchableURL } from '../utils/blob-service.js'; +import { createDefaultHTTPRequestHeaders } from '../utils/services-utils.js'; function inviteLinkBlobHash(secret: string): string { return `invite_${secret}`; } export type KeyserverOverride = { +keyserverID: string, +keyserverURL: string, }; async function getKeyserverOverrideForAnInviteLink( secret: string, + authMetadata?: AuthMetadata, ): Promise { const blobURL = getBlobFetchableURL(inviteLinkBlobHash(secret)); + const headers = authMetadata + ? createDefaultHTTPRequestHeaders(authMetadata) + : {}; const result = await fetch(blobURL, { method: blobService.httpEndpoints.GET_BLOB.method, + headers, }); 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 { inviteLinkBlobHash, getKeyserverOverrideForAnInviteLink }; diff --git a/native/navigation/deep-links-context-provider.react.js b/native/navigation/deep-links-context-provider.react.js index b94dea152..58d72fbca 100644 --- a/native/navigation/deep-links-context-provider.react.js +++ b/native/navigation/deep-links-context-provider.react.js @@ -1,181 +1,193 @@ // @flow import { useNavigation } from '@react-navigation/native'; import * as Application from 'expo-application'; +import invariant from 'invariant'; import * as React from 'react'; import { Linking, Platform } from 'react-native'; import { useVerifyInviteLink, verifyInviteLinkActionTypes, } from 'lib/actions/link-actions.js'; import { parseInstallReferrerFromInviteLinkURL, parseDataFromDeepLink, type ParsedDeepLinkData, } from 'lib/facts/links.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; +import { IdentityClientContext } from 'lib/shared/identity-client-context.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'; +import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import { InviteLinkModalRouteName, SecondaryDeviceQRCodeScannerRouteName, } from './route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { useOnFirstLaunchEffect } from '../utils/hooks.js'; type DeepLinksContextType = { +setCurrentLinkUrl: SetState, }; const defaultContext = { setCurrentLinkUrl: () => {}, }; const DeepLinksContext: React.Context = React.createContext(defaultContext); type Props = { +children: React.Node, }; function DeepLinksContextProvider(props: Props): React.Node { const { children } = props; 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 const subscription = Linking.addEventListener('url', ({ url }) => setCurrentLink(url), ); // 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. void (async () => { const initialURL = await Linking.getInitialURL(); if (initialURL) { setCurrentLink(initialURL); } })(); return () => { subscription.remove(); }; }, []); const checkInstallReferrer = React.useCallback(async () => { if (Platform.OS !== 'android') { return; } const installReferrer = await Application.getInstallReferrerAsync(); if (!installReferrer) { return; } const linkSecret = parseInstallReferrerFromInviteLinkURL(installReferrer); if (linkSecret) { setCurrentLink(linkSecret); } }, []); useOnFirstLaunchEffect('ANDROID_REFERRER', checkInstallReferrer); const [keyserverOverride, setKeyserverOverride] = React.useState(undefined); const inviteLinkSecret = React.useRef(null); + const identityContext = React.useContext(IdentityClientContext); + invariant(identityContext, 'Identity context should be set'); + const { getAuthMetadata } = identityContext; + const loggedIn = useSelector(isLoggedIn); const dispatchActionPromise = useDispatchActionPromise(); const validateLink = useVerifyInviteLink(keyserverOverride); const navigation = useNavigation(); React.useEffect(() => { void (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); setKeyserverOverride(undefined); inviteLinkSecret.current = null; const parsedData: ParsedDeepLinkData = parseDataFromDeepLink(currentLink); if (!parsedData) { return; } if (parsedData.type === 'invite-link') { + let authMetadata; + if (usingCommServicesAccessToken) { + authMetadata = await getAuthMetadata(); + } + const { secret } = parsedData.data; inviteLinkSecret.current = secret; try { const newKeyserverOverride = - await getKeyserverOverrideForAnInviteLink(secret); + await getKeyserverOverrideForAnInviteLink(secret, authMetadata); 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]); + }, [currentLink, getAuthMetadata, 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, ); result = await validateLinkPromise; } catch (e) { console.log(e); result = { status: 'invalid', }; } navigation.navigate<'InviteLinkModal'>({ name: InviteLinkModalRouteName, params: { invitationDetails: result, secret, keyserverOverride, }, }); })(); }, [dispatchActionPromise, keyserverOverride, navigation, validateLink]); const contextValue = React.useMemo( () => ({ setCurrentLinkUrl: setCurrentLink, }), [], ); return ( {children} ); } export { DeepLinksContext, DeepLinksContextProvider }; diff --git a/web/invite-links/invite-link-handler.react.js b/web/invite-links/invite-link-handler.react.js index de7bb0ceb..21cd0d049 100644 --- a/web/invite-links/invite-link-handler.react.js +++ b/web/invite-links/invite-link-handler.react.js @@ -1,125 +1,139 @@ // @flow +import invariant from 'invariant'; import * as React from 'react'; import { useVerifyInviteLink, verifyInviteLinkActionTypes, } from 'lib/actions/link-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; +import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import { getKeyserverOverrideForAnInviteLink, 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'; +import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import AcceptInviteModal from './accept-invite-modal.react.js'; import { updateNavInfoActionType } from '../redux/action-types.js'; import { useSelector } from '../redux/redux-utils.js'; 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 identityContext = React.useContext(IdentityClientContext); + invariant(identityContext, 'Identity context should be set'); + const { getAuthMetadata } = identityContext; + const dispatchActionPromise = useDispatchActionPromise(); const dispatch = useDispatch(); const { pushModal } = useModalContext(); React.useEffect(() => { void (async () => { if (!inviteSecret || !loggedIn) { return; } dispatch({ type: updateNavInfoActionType, payload: { inviteSecret: null }, }); setKeyserverOverride(undefined); inviteLinkSecret.current = inviteSecret; + let authMetadata; + if (usingCommServicesAccessToken) { + authMetadata = await getAuthMetadata(); + } + try { - const newKeyserverOverride = - await getKeyserverOverrideForAnInviteLink(inviteSecret); + const newKeyserverOverride = await getKeyserverOverrideForAnInviteLink( + inviteSecret, + authMetadata, + ); setKeyserverOverride(newKeyserverOverride); } catch (e) { console.error('Error while downloading an invite link blob', e); pushModal( , ); } })(); - }, [dispatch, inviteSecret, loggedIn, pushModal]); + }, [dispatch, getAuthMetadata, inviteSecret, loggedIn, pushModal]); const validateLink = useVerifyInviteLink(keyserverOverride); const threadInfos = useSelector(threadInfoSelector); 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, ); result = await validateLinkPromise; } catch (e) { console.error('Error while verifying an invite link', e); result = { status: 'invalid', }; } const threadID = result.thread?.id ?? result.community?.id; if ( threadID && result.status === 'already_joined' && threadInfos[threadID] ) { dispatch({ type: updateNavInfoActionType, payload: { chatMode: 'view', activeChatThreadID: threadID, tab: 'chat', }, }); } pushModal( , ); })(); }, [ dispatch, dispatchActionPromise, keyserverOverride, pushModal, threadInfos, validateLink, ]); return null; } export default InviteLinkHandler;