diff --git a/lib/actions/link-actions.js b/lib/actions/link-actions.js index 764ee6c7a..be63aa254 100644 --- a/lib/actions/link-actions.js +++ b/lib/actions/link-actions.js @@ -1,150 +1,186 @@ // @flow +import * as React from 'react'; import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; import type { CallKeyserverEndpoint } from '../keyserver-conn/keyserver-conn-types.js'; import type { FetchInviteLinksResponse, InviteLinkVerificationRequest, InviteLinkVerificationResponse, CreateOrUpdatePublicLinkRequest, InviteLink, DisableInviteLinkRequest, DisableInviteLinkPayload, } from '../types/link-types.js'; import type { CallSingleKeyserverEndpoint } from '../utils/call-single-keyserver-endpoint.js'; import { useKeyserverCall } from '../utils/keyserver-call.js'; +import { ashoatKeyserverID } from '../utils/validation-utils.js'; const verifyInviteLinkActionTypes = Object.freeze({ started: 'VERIFY_INVITE_LINK_STARTED', success: 'VERIFY_INVITE_LINK_SUCCESS', failed: 'VERIFY_INVITE_LINK_FAILED', }); const verifyInviteLink = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, - ): (( - request: InviteLinkVerificationRequest, - ) => Promise) => - async request => { - const response = await callSingleKeyserverEndpoint( + ): ((input: { + +request: InviteLinkVerificationRequest, + +keyserverID: string, + }) => Promise) => + async input => { + const { request, keyserverID } = input; + const requests = { + [keyserverID]: request, + }; + const responses = await callSingleKeyserverEndpoint( 'verify_invite_link', - request, + requests, ); + const response = responses[keyserverID]; if (response.status === 'valid' || response.status === 'already_joined') { return { status: response.status, community: response.community, }; } return { status: response.status, }; }; +function useVerifyInviteLink(keyserverOverride?: { + +keyserverID: string, + +keyserverURL: string, +}): ( + request: InviteLinkVerificationRequest, +) => Promise { + const keyserverID = keyserverOverride?.keyserverID ?? ashoatKeyserverID; + let paramOverride = null; + if (keyserverOverride) { + paramOverride = { + keyserverInfos: { + [keyserverOverride.keyserverID]: { + urlPrefix: keyserverOverride.keyserverURL, + }, + }, + }; + } + const callVerifyInviteLink = useKeyserverCall( + verifyInviteLink, + paramOverride, + ); + return React.useCallback( + (request: InviteLinkVerificationRequest) => + callVerifyInviteLink({ request, keyserverID }), + [callVerifyInviteLink, keyserverID], + ); +} + const fetchPrimaryInviteLinkActionTypes = Object.freeze({ started: 'FETCH_PRIMARY_INVITE_LINKS_STARTED', success: 'FETCH_PRIMARY_INVITE_LINKS_SUCCESS', failed: 'FETCH_PRIMARY_INVITE_LINKS_FAILED', }); const fetchPrimaryInviteLinks = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): (() => Promise) => async () => { const requests: { [string]: void } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = undefined; } const responses = await callKeyserverEndpoint( 'fetch_primary_invite_links', requests, ); let links: $ReadOnlyArray = []; for (const keyserverID in responses) { links = links.concat(responses[keyserverID].links); } return { links, }; }; function useFetchPrimaryInviteLinks(): () => Promise { return useKeyserverCall(fetchPrimaryInviteLinks); } const createOrUpdatePublicLinkActionTypes = Object.freeze({ started: 'CREATE_OR_UPDATE_PUBLIC_LINK_STARTED', success: 'CREATE_OR_UPDATE_PUBLIC_LINK_SUCCESS', failed: 'CREATE_OR_UPDATE_PUBLIC_LINK_FAILED', }); const createOrUpdatePublicLink = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: CreateOrUpdatePublicLinkRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.communityID); const requests = { [keyserverID]: { name: input.name, communityID: input.communityID, }, }; const responses = await callKeyserverEndpoint( 'create_or_update_public_link', requests, ); const response = responses[keyserverID]; return { name: response.name, primary: response.primary, role: response.role, communityID: response.communityID, expirationTime: response.expirationTime, limitOfUses: response.limitOfUses, numberOfUses: response.numberOfUses, }; }; function useCreateOrUpdatePublicLink(): ( input: CreateOrUpdatePublicLinkRequest, ) => Promise { return useKeyserverCall(createOrUpdatePublicLink); } const disableInviteLinkLinkActionTypes = Object.freeze({ started: 'DISABLE_INVITE_LINK_STARTED', success: 'DISABLE_INVITE_LINK_SUCCESS', failed: 'DISABLE_INVITE_LINK_FAILED', }); const disableInviteLink = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: DisableInviteLinkRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.communityID); const requests = { [keyserverID]: input }; await callKeyserverEndpoint('disable_invite_link', requests); return input; }; function useDisableInviteLink(): ( input: DisableInviteLinkRequest, ) => Promise { return useKeyserverCall(disableInviteLink); } export { verifyInviteLinkActionTypes, - verifyInviteLink, + useVerifyInviteLink, fetchPrimaryInviteLinkActionTypes, useFetchPrimaryInviteLinks, createOrUpdatePublicLinkActionTypes, useCreateOrUpdatePublicLink, disableInviteLinkLinkActionTypes, useDisableInviteLink, }; diff --git a/native/navigation/deep-links-context-provider.react.js b/native/navigation/deep-links-context-provider.react.js index d28216fa3..36f392070 100644 --- a/native/navigation/deep-links-context-provider.react.js +++ b/native/navigation/deep-links-context-provider.react.js @@ -1,140 +1,139 @@ // @flow import { useNavigation } from '@react-navigation/native'; import * as Application from 'expo-application'; import * as React from 'react'; import { Linking, Platform } from 'react-native'; import { - verifyInviteLink, + 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 type { SetState } from 'lib/types/hook-types.js'; -import { useLegacyAshoatKeyserverCall } from 'lib/utils/action-utils.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-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 loggedIn = useSelector(isLoggedIn); const dispatchActionPromise = useDispatchActionPromise(); - const validateLink = useLegacyAshoatKeyserverCall(verifyInviteLink); + const validateLink = useVerifyInviteLink(); 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); const parsedData: ParsedDeepLinkData = parseDataFromDeepLink(currentLink); if (!parsedData) { return; } if (parsedData.type === 'invite-link') { const { secret } = parsedData.data; const validateLinkPromise = validateLink({ secret }); void 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); } })(); }, [currentLink, dispatchActionPromise, loggedIn, 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 931e048cb..d75f564c2 100644 --- a/web/invite-links/invite-link-handler.react.js +++ b/web/invite-links/invite-link-handler.react.js @@ -1,64 +1,63 @@ // @flow import * as React from 'react'; import { - verifyInviteLink, + useVerifyInviteLink, verifyInviteLinkActionTypes, } from 'lib/actions/link-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; -import { useLegacyAshoatKeyserverCall } from 'lib/utils/action-utils.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-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 loggedIn = useSelector(isLoggedIn); const dispatchActionPromise = useDispatchActionPromise(); const dispatch = useDispatch(); - const validateLink = useLegacyAshoatKeyserverCall(verifyInviteLink); + const validateLink = useVerifyInviteLink(); const { pushModal } = useModalContext(); React.useEffect(() => { if (!inviteSecret || !loggedIn) { return; } dispatch({ type: updateNavInfoActionType, payload: { inviteSecret: null }, }); const validateLinkPromise = validateLink({ secret: inviteSecret }); void dispatchActionPromise( verifyInviteLinkActionTypes, validateLinkPromise, ); void (async () => { const result = await validateLinkPromise; if (result.status === 'already_joined') { return; } pushModal( , ); })(); }, [ dispatch, dispatchActionPromise, inviteSecret, loggedIn, pushModal, validateLink, ]); return null; } export default InviteLinkHandler;