diff --git a/native/account/registration/connect-ethereum.react.js b/native/account/registration/connect-ethereum.react.js index e4eec14ea..0e0a645b2 100644 --- a/native/account/registration/connect-ethereum.react.js +++ b/native/account/registration/connect-ethereum.react.js @@ -1,349 +1,335 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Text, View } from 'react-native'; import { exactSearchUser, exactSearchUserActionTypes, } from 'lib/actions/user-actions.js'; import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { type SIWEResult, SIWEMessageTypes } from 'lib/types/siwe-types.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import { useGetEthereumAccountFromSIWEResult, siweNonceExpired, } from './ethereum-utils.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 } from './registration-types.js'; import { commRustModule } from '../../native-modules.js'; import { type NavigationRoute, ExistingEthereumAccountRouteName, UsernameSelectionRouteName, AvatarSelectionRouteName, } from '../../navigation/route-names.js'; import { useSelector } from '../../redux/redux-utils.js'; import { useStyles } from '../../themes/colors.js'; import { defaultURLPrefix } from '../../utils/url-utils.js'; import EthereumLogoDark from '../../vectors/ethereum-logo-dark.react.js'; +import { useSIWEPanelState } from '../siwe-hooks.js'; import SIWEPanel from '../siwe-panel.react.js'; const exactSearchUserLoadingStatusSelector = createLoadingStatusSelector( exactSearchUserActionTypes, ); export type ConnectEthereumParams = { +userSelections: { +coolOrNerdMode?: ?CoolOrNerdMode, +keyserverURL?: ?string, +farcasterID: ?string, }, }; -type PanelState = 'closed' | 'opening' | 'open' | 'closing'; - type Props = { +navigation: RegistrationNavigationProp<'ConnectEthereum'>, +route: NavigationRoute<'ConnectEthereum'>, }; function ConnectEthereum(props: Props): React.Node { const { params } = props.route; const registrationContext = React.useContext(RegistrationContext); invariant(registrationContext, 'registrationContext should be set'); const { cachedSelections, setCachedSelections } = registrationContext; const userSelections = params?.userSelections; const isNerdMode = userSelections?.coolOrNerdMode === 'nerd'; const styles = useStyles(unboundStyles); let body; if (!isNerdMode) { body = ( Connecting your Ethereum wallet allows you to use your ENS name and avatar in the app. You’ll also be able to log in with your wallet instead of a password. ); } else { body = ( <> Connecting your Ethereum wallet has three benefits: {'1. '} Your peers will be able to cryptographically verify that your Comm account is associated with your Ethereum wallet. {'2. '} You’ll be able to use your ENS name and avatar in the app. {'3. '} You can choose to skip setting a password, and to log in with your Ethereum wallet instead. ); } - 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 { navigate } = props.navigation; const onSkip = React.useCallback(() => { navigate<'UsernameSelection'>({ name: UsernameSelectionRouteName, params: { userSelections, }, }); }, [navigate, userSelections]); const keyserverURL = userSelections?.keyserverURL ?? defaultURLPrefix; const serverCallParamOverride = React.useMemo( () => ({ urlPrefix: keyserverURL, }), [keyserverURL], ); const exactSearchUserCall = useLegacyAshoatKeyserverCall( exactSearchUser, serverCallParamOverride, ); const dispatchActionPromise = useDispatchActionPromise(); const getEthereumAccountFromSIWEResult = useGetEthereumAccountFromSIWEResult(); const onSuccessfulWalletSignature = React.useCallback( async (result: SIWEResult) => { let userAlreadyExists; if (usingCommServicesAccessToken) { const findUserIDResponseString = await commRustModule.findUserIDForWalletAddress(result.address); const findUserIDResponse = JSON.parse(findUserIDResponseString); userAlreadyExists = !!findUserIDResponse.userID || findUserIDResponse.isReserved; } else { const searchPromise = exactSearchUserCall(result.address); void dispatchActionPromise(exactSearchUserActionTypes, searchPromise); const { userInfo } = await searchPromise; userAlreadyExists = !!userInfo; } if (userAlreadyExists) { navigate<'ExistingEthereumAccount'>({ name: ExistingEthereumAccountRouteName, params: result, }); return; } const ethereumAccount = await getEthereumAccountFromSIWEResult(result); const newUserSelections = { ...userSelections, accountSelection: ethereumAccount, }; navigate<'AvatarSelection'>({ name: AvatarSelectionRouteName, params: { userSelections: newUserSelections, }, }); }, [ userSelections, exactSearchUserCall, dispatchActionPromise, navigate, getEthereumAccountFromSIWEResult, ], ); + const { + panelState, + onPanelClosed, + onPanelClosing, + siwePanelSetLoading, + openPanel, + } = useSIWEPanelState(); + let siwePanel; if (panelState !== 'closed') { siwePanel = ( ); } const { ethereumAccount } = cachedSelections; const nonceExpired = ethereumAccount && siweNonceExpired(ethereumAccount.nonceTimestamp); const alreadyHasConnected = !!ethereumAccount && !nonceExpired; React.useEffect(() => { if (nonceExpired) { setCachedSelections(oldUserSelections => ({ ...oldUserSelections, ethereumAccount: undefined, })); } }, [nonceExpired, setCachedSelections]); const exactSearchUserCallLoading = useSelector( state => exactSearchUserLoadingStatusSelector(state) === 'loading', ); const defaultConnectButtonVariant = alreadyHasConnected ? 'outline' : 'enabled'; const connectButtonVariant = exactSearchUserCallLoading || panelState === 'opening' ? 'loading' : defaultConnectButtonVariant; const connectButtonText = alreadyHasConnected ? 'Connect new Ethereum wallet' : 'Connect Ethereum wallet'; const onUseAlreadyConnectedWallet = React.useCallback(() => { invariant( ethereumAccount, 'ethereumAccount should be set in onUseAlreadyConnectedWallet', ); const newUserSelections = { ...userSelections, accountSelection: ethereumAccount, }; navigate<'AvatarSelection'>({ name: AvatarSelectionRouteName, params: { userSelections: newUserSelections, }, }); }, [ethereumAccount, userSelections, navigate]); let alreadyConnectedButton; if (alreadyHasConnected) { alreadyConnectedButton = ( ); } return ( <> Do you want to connect an Ethereum wallet? {body} {alreadyConnectedButton} {siwePanel} ); } const unboundStyles = { scrollViewContentContainer: { flexGrow: 1, }, header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, }, body: { fontFamily: 'Arial', fontSize: 15, lineHeight: 20, color: 'panelForegroundSecondaryLabel', paddingBottom: 16, }, ethereumLogoContainer: { flexGrow: 1, alignItems: 'center', justifyContent: 'center', }, list: { paddingBottom: 16, }, listItem: { flexDirection: 'row', }, listItemNumber: { fontFamily: 'Arial', fontWeight: 'bold', fontSize: 15, lineHeight: 20, color: 'panelForegroundSecondaryLabel', }, listItemContent: { fontFamily: 'Arial', flexShrink: 1, fontSize: 15, lineHeight: 20, color: 'panelForegroundSecondaryLabel', }, }; export default ConnectEthereum; diff --git a/native/account/registration/siwe-backup-message-creation.react.js b/native/account/registration/siwe-backup-message-creation.react.js index 2320b30b6..8c563e19d 100644 --- a/native/account/registration/siwe-backup-message-creation.react.js +++ b/native/account/registration/siwe-backup-message-creation.react.js @@ -1,255 +1,240 @@ // @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 Alert from '../../utils/alert.js'; +import { useSIWEPanelState } from '../siwe-hooks.js'; import SIWEPanel from '../siwe-panel.react.js'; -type PanelState = 'closed' | 'opening' | 'open' | 'closing'; - type CreateSIWEBackupMessageBaseProps = { +onSuccessfulWalletSignature: (result: SIWEResult) => void, +onExistingWalletSignature?: () => void, +onSkip?: () => void, }; const CreateSIWEBackupMessageBase: React.ComponentType = React.memo( function CreateSIWEBackupMessageBase( props: CreateSIWEBackupMessageBaseProps, ): React.Node { const { onSuccessfulWalletSignature, onExistingWalletSignature, onSkip } = props; 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 { + panelState, + onPanelClosed, + onPanelClosing, + openPanel, + siwePanelSetLoading, + } = useSIWEPanelState(); let siwePanel; if (panelState !== 'closed') { siwePanel = ( ); } const newSignatureButtonText = onExistingWalletSignature ? 'Encrypt with new signature' : 'Encrypt with Ethereum signature'; const newSignatureButtonVariant = onExistingWalletSignature ? 'outline' : 'enabled'; let useExistingSignatureButton; if (onExistingWalletSignature) { useExistingSignatureButton = ( ); } let onSkipButton; if (onSkip) { onSkipButton = ( ); } return ( <> Encrypting your Comm backup To make sure we can’t see your data, Comm encrypts your backup using a signature from your wallet. This signature is private and never leaves your device, unlike the prior signature, which is public. This signature ensures that you can always recover your data as long as you still control your wallet. {useExistingSignatureButton} {onSkipButton} {siwePanel} ); }, ); export type CreateSIWEBackupMessageParams = { +userSelections: { +coolOrNerdMode?: ?CoolOrNerdMode, +keyserverURL?: ?string, +farcasterID: ?string, +accountSelection: AccountSelection, +avatarData: ?AvatarData, }, }; 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 registrationContext = React.useContext(RegistrationContext); invariant(registrationContext, 'registrationContext should be set'); const { cachedSelections, setCachedSelections } = registrationContext; const onSuccessfulWalletSignature = React.useCallback( (result: SIWEResult) => { const selectedEthereumAddress = userSelections.accountSelection.address; const { message, signature, address } = result; if (address !== selectedEthereumAddress) { Alert.alert( 'Mismatched Ethereum address', 'You picked a different wallet than the one you use to sign in.', ); return; } const newUserSelections = { ...userSelections, siweBackupSecrets: { message, signature }, }; setCachedSelections(oldUserSelections => ({ ...oldUserSelections, siweBackupSecrets: { message, signature }, })); navigate<'RegistrationTerms'>({ name: RegistrationTermsRouteName, params: { userSelections: newUserSelections }, }); }, [navigate, setCachedSelections, userSelections], ); const { siweBackupSecrets } = cachedSelections; const onExistingWalletSignature = React.useCallback(() => { const registrationTermsParams = { userSelections: { ...userSelections, siweBackupSecrets, }, }; navigate<'RegistrationTerms'>({ name: RegistrationTermsRouteName, params: registrationTermsParams, }); }, [navigate, siweBackupSecrets, userSelections]); if (siweBackupSecrets) { return ( ); } return ( ); } 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 { CreateSIWEBackupMessageBase, CreateSIWEBackupMessage }; diff --git a/native/account/siwe-hooks.js b/native/account/siwe-hooks.js index a42d4746c..4cb27f813 100644 --- a/native/account/siwe-hooks.js +++ b/native/account/siwe-hooks.js @@ -1,127 +1,175 @@ // @flow import * as React from 'react'; import { legacySiweAuth, legacySiweAuthActionTypes, } from 'lib/actions/siwe-actions.js'; import { identityRegisterActionTypes, useIdentityWalletRegister, } from 'lib/actions/user-actions.js'; import type { CallSingleKeyserverEndpointOptions } from 'lib/keyserver-conn/call-single-keyserver-endpoint.js'; import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js'; import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import type { LegacyLogInStartingPayload, LegacyLogInExtraInfo, } from 'lib/types/account-types.js'; import type { IdentityWalletRegisterInput } from 'lib/types/siwe-types.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import { useSelector } from '../redux/redux-utils.js'; import { nativeLegacyLogInExtraInfoSelector } from '../selectors/account-selectors.js'; type SIWEServerCallParams = { +message: string, +signature: string, +doNotRegister?: boolean, ... }; function useLegacySIWEServerCall(): ( SIWEServerCallParams, ?CallSingleKeyserverEndpointOptions, ) => Promise { const legacySiweAuthCall = useLegacyAshoatKeyserverCall(legacySiweAuth); const callSIWE = React.useCallback( ( message: string, signature: string, extraInfo: $ReadOnly<{ ...LegacyLogInExtraInfo, +doNotRegister?: boolean, }>, callSingleKeyserverEndpointOptions: ?CallSingleKeyserverEndpointOptions, ) => legacySiweAuthCall( { message, signature, ...extraInfo, }, callSingleKeyserverEndpointOptions, ), [legacySiweAuthCall], ); const legacyLogInExtraInfo = useSelector(nativeLegacyLogInExtraInfoSelector); const getInitialNotificationsEncryptedMessage = useInitialNotificationsEncryptedMessage(authoritativeKeyserverID); const dispatchActionPromise = useDispatchActionPromise(); return React.useCallback( async ( { message, signature, doNotRegister }, callSingleKeyserverEndpointOptions, ) => { const extraInfo = await legacyLogInExtraInfo(); const initialNotificationsEncryptedMessage = await getInitialNotificationsEncryptedMessage({ callSingleKeyserverEndpointOptions, }); const siwePromise = callSIWE( message, signature, { ...extraInfo, initialNotificationsEncryptedMessage, doNotRegister, }, callSingleKeyserverEndpointOptions, ); void dispatchActionPromise( legacySiweAuthActionTypes, siwePromise, undefined, ({ calendarQuery: extraInfo.calendarQuery, }: LegacyLogInStartingPayload), ); await siwePromise; }, [ legacyLogInExtraInfo, dispatchActionPromise, callSIWE, getInitialNotificationsEncryptedMessage, ], ); } function useIdentityWalletRegisterCall(): IdentityWalletRegisterInput => Promise { const identityWalletRegister = useIdentityWalletRegister(); const dispatchActionPromise = useDispatchActionPromise(); return React.useCallback( async ({ address, message, signature, fid }) => { const siwePromise = identityWalletRegister( address, message, signature, fid, ); void dispatchActionPromise(identityRegisterActionTypes, siwePromise); await siwePromise; }, [dispatchActionPromise, identityWalletRegister], ); } -export { useLegacySIWEServerCall, useIdentityWalletRegisterCall }; +type PanelState = 'closed' | 'opening' | 'open' | 'closing'; + +function useSIWEPanelState(): { + +panelState: PanelState, + +openPanel: () => void, + +onPanelClosed: () => void, + +onPanelClosing: () => void, + +siwePanelSetLoading: (loading: boolean) => void, +} { + 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], + ); + + return React.useMemo( + () => ({ + panelState, + openPanel, + onPanelClosed, + onPanelClosing, + siwePanelSetLoading, + }), + [panelState, openPanel, onPanelClosed, onPanelClosing, siwePanelSetLoading], + ); +} + +export { + useLegacySIWEServerCall, + useIdentityWalletRegisterCall, + useSIWEPanelState, +};