diff --git a/native/account/registration/registration-server-call.js b/native/account/registration/registration-server-call.js index f4ada4c5c..2d23cc2cb 100644 --- a/native/account/registration/registration-server-call.js +++ b/native/account/registration/registration-server-call.js @@ -1,338 +1,351 @@ // @flow import * as React from 'react'; import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js'; import { setSyncedMetadataEntryActionType } from 'lib/actions/synced-metadata-actions.js'; import { keyserverRegisterActionTypes, keyserverRegister, useIdentityPasswordRegister, identityRegisterActionTypes, } from 'lib/actions/user-actions.js'; import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js'; import { isLoggedInToKeyserver } from 'lib/selectors/user-selectors.js'; import type { LogInStartingPayload } from 'lib/types/account-types.js'; import { syncedMetadataNames } from 'lib/types/synced-metadata-types.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 { setURLPrefix } from 'lib/utils/url-utils.js'; import type { RegistrationServerCallInput, UsernameAccountSelection, AvatarData, } from './registration-types.js'; import { authoritativeKeyserverID } from '../../authoritative-keyserver.js'; import { useNativeSetUserAvatar, useUploadSelectedMedia, } from '../../avatars/avatar-hooks.js'; +import { commCoreModule } from '../../native-modules.js'; import { useSelector } from '../../redux/redux-utils.js'; import { nativeLogInExtraInfoSelector } from '../../selectors/account-selectors.js'; import { AppOutOfDateAlertDetails, UsernameReservedAlertDetails, UsernameTakenAlertDetails, UnknownErrorAlertDetails, } from '../../utils/alert-messages.js'; import Alert from '../../utils/alert.js'; import { setNativeCredentials } from '../native-credentials.js'; import { useLegacySIWEServerCall, useIdentityWalletRegisterCall, } from '../siwe-hooks.js'; // We can't just do everything in one async callback, since the server calls // would get bound to Redux state from before the registration. The registration // flow has multiple steps where critical Redux state is changed, where // subsequent steps depend on accessing the updated Redux state. // To address this, we break the registration process up into multiple steps. // When each step completes we update the currentStep state, and we have Redux // selectors that trigger useEffects for subsequent steps when relevant data // starts to appear in Redux. type CurrentStep = | { +step: 'inactive' } | { +step: 'waiting_for_registration_call', + +clearCachedSelections: () => void, +avatarData: ?AvatarData, +resolve: () => void, +reject: Error => void, }; const inactiveStep = { step: 'inactive' }; function useRegistrationServerCall(): RegistrationServerCallInput => Promise { const [currentStep, setCurrentStep] = React.useState(inactiveStep); // STEP 1: ACCOUNT REGISTRATION const logInExtraInfo = useSelector(nativeLogInExtraInfoSelector); const dispatchActionPromise = useDispatchActionPromise(); const callKeyserverRegister = useLegacyAshoatKeyserverCall(keyserverRegister); const callIdentityPasswordRegister = useIdentityPasswordRegister(); const identityRegisterUsernameAccount = React.useCallback( async ( accountSelection: UsernameAccountSelection, farcasterID: ?string, ) => { const identityRegisterPromise = (async () => { try { const result = await callIdentityPasswordRegister( accountSelection.username, accountSelection.password, farcasterID, ); await setNativeCredentials({ username: accountSelection.username, password: accountSelection.password, }); return result; } catch (e) { if (e.message === 'username reserved') { Alert.alert( UsernameReservedAlertDetails.title, UsernameReservedAlertDetails.message, ); } else if (e.message === 'username already exists') { Alert.alert( UsernameTakenAlertDetails.title, UsernameTakenAlertDetails.message, ); } else if (e.message === 'Unsupported version') { Alert.alert( AppOutOfDateAlertDetails.title, AppOutOfDateAlertDetails.message, ); } else { Alert.alert( UnknownErrorAlertDetails.title, UnknownErrorAlertDetails.message, ); } throw e; } })(); void dispatchActionPromise( identityRegisterActionTypes, identityRegisterPromise, ); await identityRegisterPromise; }, [callIdentityPasswordRegister, dispatchActionPromise], ); const keyserverRegisterUsernameAccount = React.useCallback( async ( accountSelection: UsernameAccountSelection, keyserverURL: string, ) => { const extraInfo = await logInExtraInfo(); const keyserverRegisterPromise = (async () => { try { const result = await callKeyserverRegister( { ...extraInfo, username: accountSelection.username, password: accountSelection.password, }, { urlPrefixOverride: keyserverURL, }, ); await setNativeCredentials({ username: result.currentUserInfo.username, password: accountSelection.password, }); return result; } catch (e) { if (e.message === 'username_reserved') { Alert.alert( UsernameReservedAlertDetails.title, UsernameReservedAlertDetails.message, ); } else if (e.message === 'username_taken') { Alert.alert( UsernameTakenAlertDetails.title, UsernameTakenAlertDetails.message, ); } else if (e.message === 'client_version_unsupported') { Alert.alert( AppOutOfDateAlertDetails.title, AppOutOfDateAlertDetails.message, ); } else { Alert.alert( UnknownErrorAlertDetails.title, UnknownErrorAlertDetails.message, ); } throw e; } })(); void dispatchActionPromise( keyserverRegisterActionTypes, keyserverRegisterPromise, undefined, ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), ); await keyserverRegisterPromise; }, [logInExtraInfo, callKeyserverRegister, dispatchActionPromise], ); const legacySiweServerCall = useLegacySIWEServerCall(); const identityWalletRegisterCall = useIdentityWalletRegisterCall(); const dispatch = useDispatch(); const returnedFunc = React.useCallback( (input: RegistrationServerCallInput) => new Promise( // eslint-disable-next-line no-async-promise-executor async (resolve, reject) => { try { if (currentStep.step !== 'inactive') { return; } - const { accountSelection, avatarData, keyserverURL, farcasterID } = - input; + const { + accountSelection, + avatarData, + keyserverURL, + farcasterID, + siweBackupSecrets, + clearCachedSelections, + } = input; if ( accountSelection.accountType === 'username' && !usingCommServicesAccessToken ) { await keyserverRegisterUsernameAccount( accountSelection, keyserverURL, ); } else if (accountSelection.accountType === 'username') { await identityRegisterUsernameAccount( accountSelection, farcasterID, ); } else if (!usingCommServicesAccessToken) { try { await legacySiweServerCall(accountSelection, { urlPrefixOverride: keyserverURL, }); } catch (e) { Alert.alert( UnknownErrorAlertDetails.title, UnknownErrorAlertDetails.message, ); throw e; } } else { try { await identityWalletRegisterCall({ address: accountSelection.address, message: accountSelection.message, signature: accountSelection.signature, fid: farcasterID, }); } catch (e) { Alert.alert( UnknownErrorAlertDetails.title, UnknownErrorAlertDetails.message, ); throw e; } } dispatch({ type: setURLPrefix, payload: keyserverURL, }); if (farcasterID) { dispatch({ type: setSyncedMetadataEntryActionType, payload: { name: syncedMetadataNames.CURRENT_USER_FID, data: farcasterID, }, }); } + if (siweBackupSecrets) { + await commCoreModule.setSIWEBackupSecrets(siweBackupSecrets); + } setCurrentStep({ step: 'waiting_for_registration_call', avatarData, + clearCachedSelections, resolve, reject, }); } catch (e) { reject(e); } }, ), [ currentStep, keyserverRegisterUsernameAccount, identityRegisterUsernameAccount, legacySiweServerCall, dispatch, identityWalletRegisterCall, ], ); // STEP 2: SETTING AVATAR const uploadSelectedMedia = useUploadSelectedMedia(); const nativeSetUserAvatar = useNativeSetUserAvatar(); const isLoggedInToAuthoritativeKeyserver = useSelector( isLoggedInToKeyserver(authoritativeKeyserverID), ); const avatarBeingSetRef = React.useRef(false); React.useEffect(() => { if ( !isLoggedInToAuthoritativeKeyserver || currentStep.step !== 'waiting_for_registration_call' || avatarBeingSetRef.current ) { return; } avatarBeingSetRef.current = true; - const { avatarData, resolve } = currentStep; + const { avatarData, resolve, clearCachedSelections } = currentStep; void (async () => { try { if (!avatarData) { return; } let updateUserAvatarRequest; if (!avatarData.needsUpload) { ({ updateUserAvatarRequest } = avatarData); } else { const { mediaSelection } = avatarData; updateUserAvatarRequest = await uploadSelectedMedia(mediaSelection); if (!updateUserAvatarRequest) { return; } } await nativeSetUserAvatar(updateUserAvatarRequest); } finally { dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); + clearCachedSelections(); setCurrentStep(inactiveStep); avatarBeingSetRef.current = false; resolve(); } })(); }, [ currentStep, isLoggedInToAuthoritativeKeyserver, uploadSelectedMedia, nativeSetUserAvatar, dispatch, ]); return returnedFunc; } export { useRegistrationServerCall }; diff --git a/native/account/registration/registration-terms.react.js b/native/account/registration/registration-terms.react.js index 2823d3ebc..bfbc24bb5 100644 --- a/native/account/registration/registration-terms.react.js +++ b/native/account/registration/registration-terms.react.js @@ -1,130 +1,138 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Text, View, Image, Linking } from 'react-native'; +import type { SIWEBackupSecrets } from 'lib/types/siwe-types.js'; + import RegistrationButtonContainer from './registration-button-container.react.js'; import RegistrationButton from './registration-button.react.js'; import RegistrationContainer from './registration-container.react.js'; import RegistrationContentContainer from './registration-content-container.react.js'; import { RegistrationContext } from './registration-context.js'; import type { RegistrationNavigationProp } from './registration-navigator.react.js'; import type { CoolOrNerdMode, AccountSelection, AvatarData, } from './registration-types.js'; import commSwooshSource from '../../img/comm-swoosh.png'; import type { NavigationRoute } from '../../navigation/route-names.js'; import { useStyles } from '../../themes/colors.js'; export type RegistrationTermsParams = { +userSelections: { +coolOrNerdMode: CoolOrNerdMode, +keyserverURL: string, +farcasterID: ?string, +accountSelection: AccountSelection, +avatarData: ?AvatarData, + +siweBackupSecrets?: ?SIWEBackupSecrets, }, }; const onTermsOfUsePressed = () => { void Linking.openURL('https://comm.app/terms'); }; const onPrivacyPolicyPressed = () => { void Linking.openURL('https://comm.app/privacy'); }; type Props = { +navigation: RegistrationNavigationProp<'RegistrationTerms'>, +route: NavigationRoute<'RegistrationTerms'>, }; function RegistrationTerms(props: Props): React.Node { const registrationContext = React.useContext(RegistrationContext); invariant(registrationContext, 'registrationContext should be set'); - const { register } = registrationContext; + const { register, setCachedSelections } = registrationContext; const [registrationInProgress, setRegistrationInProgress] = React.useState(false); const { userSelections } = props.route.params; + + const clearCachedSelections = React.useCallback(() => { + setCachedSelections({}); + }, [setCachedSelections]); + const onProceed = React.useCallback(async () => { setRegistrationInProgress(true); try { - await register(userSelections); + await register({ ...userSelections, clearCachedSelections }); } finally { setRegistrationInProgress(false); } - }, [register, userSelections]); + }, [register, userSelections, clearCachedSelections]); const styles = useStyles(unboundStyles); const termsNotice = ( By registering, you are agreeing to our{' '} Terms of Use {' and '} Privacy Policy . ); return ( Finish registration {termsNotice} ); } const unboundStyles = { scrollViewContentContainer: { flexGrow: 1, }, header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, }, body: { fontFamily: 'Arial', fontSize: 15, lineHeight: 20, color: 'panelForegroundSecondaryLabel', paddingBottom: 16, }, commSwooshContainer: { flexGrow: 1, flexShrink: 1, alignItems: 'center', justifyContent: 'center', }, commSwoosh: { resizeMode: 'center', width: '100%', height: '100%', }, hyperlinkText: { color: 'purpleLink', }, }; export default RegistrationTerms; diff --git a/native/account/registration/registration-types.js b/native/account/registration/registration-types.js index 22681afa6..8db7816ef 100644 --- a/native/account/registration/registration-types.js +++ b/native/account/registration/registration-types.js @@ -1,67 +1,69 @@ // @flow import type { UpdateUserAvatarRequest, ClientAvatar, } from 'lib/types/avatar-types.js'; import type { NativeMediaSelection } from 'lib/types/media-types.js'; import type { SIWEResult, SIWEBackupSecrets } from 'lib/types/siwe-types.js'; export type CoolOrNerdMode = 'cool' | 'nerd'; export type EthereumAccountSelection = { +accountType: 'ethereum', ...SIWEResult, +avatarURI: ?string, }; export type UsernameAccountSelection = { +accountType: 'username', +username: string, +password: string, }; export type AccountSelection = | EthereumAccountSelection | UsernameAccountSelection; export type AvatarData = | { +needsUpload: true, +mediaSelection: NativeMediaSelection, +clientAvatar: ClientAvatar, } | { +needsUpload: false, +updateUserAvatarRequest: UpdateUserAvatarRequest, +clientAvatar: ClientAvatar, }; export type RegistrationServerCallInput = { +coolOrNerdMode: CoolOrNerdMode, +keyserverURL: string, +farcasterID: ?string, +accountSelection: AccountSelection, +avatarData: ?AvatarData, + +siweBackupSecrets?: ?SIWEBackupSecrets, + +clearCachedSelections: () => void, }; export type CachedUserSelections = { +coolOrNerdMode?: CoolOrNerdMode, +keyserverURL?: string, +username?: string, +password?: string, +avatarData?: ?AvatarData, +ethereumAccount?: EthereumAccountSelection, +farcasterID?: string, +siweBackupSecrets?: ?SIWEBackupSecrets, }; export const ensAvatarSelection: AvatarData = { needsUpload: false, updateUserAvatarRequest: { type: 'ens' }, clientAvatar: { type: 'ens' }, }; export const enableNewRegistrationMode = __DEV__; export const enableSIWEBackupCreation = __DEV__; diff --git a/native/account/registration/siwe-backup-message-creation.react.js b/native/account/registration/siwe-backup-message-creation.react.js index 0d1abbc70..4ecb4b806 100644 --- a/native/account/registration/siwe-backup-message-creation.react.js +++ b/native/account/registration/siwe-backup-message-creation.react.js @@ -1,187 +1,198 @@ // @flow import Icon from '@expo/vector-icons/MaterialIcons.js'; import invariant from 'invariant'; import * as React from 'react'; import { View, Text } from 'react-native'; import { type SIWEResult, SIWEMessageTypes } from 'lib/types/siwe-types.js'; import RegistrationButtonContainer from './registration-button-container.react.js'; import RegistrationButton from './registration-button.react.js'; import RegistrationContainer from './registration-container.react.js'; import RegistrationContentContainer from './registration-content-container.react.js'; import { RegistrationContext } from './registration-context.js'; import { type RegistrationNavigationProp } from './registration-navigator.react.js'; import type { CoolOrNerdMode, AccountSelection, AvatarData, } from './registration-types.js'; import { type NavigationRoute, RegistrationTermsRouteName, } from '../../navigation/route-names.js'; import { useStyles } from '../../themes/colors.js'; import SIWEPanel from '../siwe-panel.react.js'; export type CreateSIWEBackupMessageParams = { +userSelections: { +coolOrNerdMode: CoolOrNerdMode, +keyserverURL: string, +farcasterID: ?string, +accountSelection: AccountSelection, +avatarData: ?AvatarData, }, }; type PanelState = 'closed' | 'opening' | 'open' | 'closing'; type Props = { +navigation: RegistrationNavigationProp<'CreateSIWEBackupMessage'>, +route: NavigationRoute<'CreateSIWEBackupMessage'>, }; function CreateSIWEBackupMessage(props: Props): React.Node { const { navigate } = props.navigation; const { params } = props.route; + const { userSelections } = params; const styles = useStyles(unboundStyles); const [panelState, setPanelState] = React.useState('closed'); const openPanel = React.useCallback(() => { setPanelState('opening'); }, []); const onPanelClosed = React.useCallback(() => { setPanelState('closed'); }, []); const onPanelClosing = React.useCallback(() => { setPanelState('closing'); }, []); const siwePanelSetLoading = React.useCallback( (loading: boolean) => { if (panelState === 'closing' || panelState === 'closed') { return; } setPanelState(loading ? 'opening' : 'open'); }, [panelState], ); const registrationContext = React.useContext(RegistrationContext); invariant(registrationContext, 'registrationContext should be set'); const { cachedSelections, setCachedSelections } = registrationContext; const onSuccessfulWalletSignature = React.useCallback( (result: SIWEResult) => { const { message, signature } = result; + const newUserSelections = { + ...userSelections, + siweBackupSecrets: { message, signature }, + }; setCachedSelections(oldUserSelections => ({ ...oldUserSelections, siweBackupSecrets: { message, signature }, })); navigate<'RegistrationTerms'>({ name: RegistrationTermsRouteName, - params, + params: { userSelections: newUserSelections }, }); }, - [navigate, params, setCachedSelections], + [navigate, setCachedSelections, userSelections], ); + const { siweBackupSecrets } = cachedSelections; const onExistingWalletSignature = React.useCallback(() => { + const registrationTermsParams = { + userSelections: { + ...userSelections, + siweBackupSecrets, + }, + }; + navigate<'RegistrationTerms'>({ name: RegistrationTermsRouteName, - params, + params: registrationTermsParams, }); - }, [params, navigate]); + }, [navigate, siweBackupSecrets, userSelections]); let siwePanel; if (panelState !== 'closed') { siwePanel = ( ); } - const { siweBackupSecrets } = cachedSelections; - const newSignatureButtonText = siweBackupSecrets ? 'Encrypt with new signature' : 'Encrypt with Ethereum signature'; const newSignatureButtonVariant = siweBackupSecrets ? 'outline' : 'enabled'; let useExistingSignatureButton; if (siweBackupSecrets) { useExistingSignatureButton = ( ); } const body = ( Comm encrypts user backups so that our backend is not able to see user data. ); return ( <> Encrypting your Comm Backup {body} {useExistingSignatureButton} {siwePanel} ); } const unboundStyles = { scrollViewContentContainer: { flexGrow: 1, }, header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, }, body: { fontFamily: 'Arial', fontSize: 15, lineHeight: 20, color: 'panelForegroundSecondaryLabel', paddingBottom: 16, }, siweBackupIcon: { color: 'panelForegroundIcon', }, siweBackupIconContainer: { flexGrow: 1, alignItems: 'center', justifyContent: 'center', }, }; export default CreateSIWEBackupMessage;