diff --git a/native/account/registration/account-does-not-exist.react.js b/native/account/registration/account-does-not-exist.react.js index 7088887bc..dc6f54530 100644 --- a/native/account/registration/account-does-not-exist.react.js +++ b/native/account/registration/account-does-not-exist.react.js @@ -1,79 +1,79 @@ // @flow import * as React from 'react'; import { Text, View, Image } from 'react-native'; 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 type { RegistrationNavigationProp } from './registration-navigator.react.js'; +import PrimaryButton from '../../components/primary-button.react.js'; import commSwooshSource from '../../img/comm-swoosh.png'; import { type NavigationRoute, ConnectFarcasterRouteName, } from '../../navigation/route-names.js'; import { useStyles } from '../../themes/colors.js'; type Props = { +navigation: RegistrationNavigationProp<'AccountDoesNotExist'>, +route: NavigationRoute<'AccountDoesNotExist'>, }; function AccountDoesNotExist(props: Props): React.Node { const { navigate } = props.navigation; const onSubmit = React.useCallback(() => { navigate(ConnectFarcasterRouteName); }, [navigate]); const styles = useStyles(unboundStyles); return ( New Comm account It looks like this is your first time logging into Comm. Let’s get started with registering your Ethereum account! - + ); } 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%', }, }; export default AccountDoesNotExist; diff --git a/native/account/registration/avatar-selection.react.js b/native/account/registration/avatar-selection.react.js index 982be85c6..a71d92430 100644 --- a/native/account/registration/avatar-selection.react.js +++ b/native/account/registration/avatar-selection.react.js @@ -1,217 +1,217 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Text, View } from 'react-native'; import { EditUserAvatarContext, type UserAvatarSelection, } from 'lib/components/edit-user-avatar-provider.react.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, type AccountSelection, type AvatarData, ensAvatarSelection, } from './registration-types.js'; import { enableSIWEBackupCreation } from './registration-types.js'; import EditUserAvatar from '../../avatars/edit-user-avatar.react.js'; +import PrimaryButton from '../../components/primary-button.react.js'; import { useCurrentLeafRouteName } from '../../navigation/nav-selectors.js'; import { type NavigationRoute, RegistrationTermsRouteName, CreateSIWEBackupMessageRouteName, AvatarSelectionRouteName, EmojiAvatarSelectionRouteName, RegistrationUserAvatarCameraModalRouteName, } from '../../navigation/route-names.js'; import { useStyles } from '../../themes/colors.js'; export type AvatarSelectionParams = { +userSelections: { +coolOrNerdMode?: ?CoolOrNerdMode, +keyserverURL?: ?string, +accountSelection: AccountSelection, +farcasterID: ?string, }, }; type Props = { +navigation: RegistrationNavigationProp<'AvatarSelection'>, +route: NavigationRoute<'AvatarSelection'>, }; function AvatarSelection(props: Props): React.Node { const { userSelections } = props.route.params; const { accountSelection } = userSelections; const usernameOrETHAddress = accountSelection.accountType === 'username' ? accountSelection.username : accountSelection.address; const registrationContext = React.useContext(RegistrationContext); invariant(registrationContext, 'registrationContext should be set'); const { cachedSelections, setCachedSelections } = registrationContext; const editUserAvatarContext = React.useContext(EditUserAvatarContext); invariant(editUserAvatarContext, 'editUserAvatarContext should be set'); const { setRegistrationMode } = editUserAvatarContext; const prefetchedAvatarURI = accountSelection.accountType === 'ethereum' ? accountSelection.avatarURI : undefined; let initialAvatarData = cachedSelections.avatarData; if (!initialAvatarData && prefetchedAvatarURI) { initialAvatarData = ensAvatarSelection; } const [avatarData, setAvatarData] = React.useState(initialAvatarData); const setClientAvatarFromSelection = React.useCallback( (selection: UserAvatarSelection) => { if (selection.needsUpload) { const newAvatarData = { ...selection, clientAvatar: { type: 'image', uri: selection.mediaSelection.uri, }, }; setAvatarData(newAvatarData); setCachedSelections(oldUserSelections => ({ ...oldUserSelections, avatarData: newAvatarData, })); } else if (selection.updateUserAvatarRequest.type !== 'remove') { const clientRequest = selection.updateUserAvatarRequest; invariant( clientRequest.type !== 'image' && clientRequest.type !== 'encrypted_image', 'image avatars need to be uploaded', ); const newAvatarData = { ...selection, clientAvatar: clientRequest, }; setAvatarData(newAvatarData); setCachedSelections(oldUserSelections => ({ ...oldUserSelections, avatarData: newAvatarData, })); } else { setAvatarData(undefined); setCachedSelections(oldUserSelections => ({ ...oldUserSelections, avatarData: undefined, })); } }, [setCachedSelections], ); const currentRouteName = useCurrentLeafRouteName(); const avatarSelectionHappening = currentRouteName === AvatarSelectionRouteName || currentRouteName === EmojiAvatarSelectionRouteName || currentRouteName === RegistrationUserAvatarCameraModalRouteName; React.useEffect(() => { if (!avatarSelectionHappening) { return undefined; } setRegistrationMode({ registrationMode: 'on', successCallback: setClientAvatarFromSelection, }); return () => { setRegistrationMode({ registrationMode: 'off' }); }; }, [ avatarSelectionHappening, setRegistrationMode, setClientAvatarFromSelection, ]); const { navigate } = props.navigation; const onProceed = React.useCallback(async () => { const newUserSelections = { ...userSelections, avatarData, }; if ( userSelections.accountSelection.accountType === 'ethereum' && enableSIWEBackupCreation ) { navigate<'CreateSIWEBackupMessage'>({ name: CreateSIWEBackupMessageRouteName, params: { userSelections: newUserSelections }, }); return; } navigate<'RegistrationTerms'>({ name: RegistrationTermsRouteName, params: { userSelections: newUserSelections }, }); }, [userSelections, avatarData, navigate]); const clientAvatar = avatarData?.clientAvatar; const userInfoOverride = React.useMemo( () => ({ username: usernameOrETHAddress, avatar: clientAvatar, }), [usernameOrETHAddress, clientAvatar], ); const styles = useStyles(unboundStyles); return ( Pick an avatar - + ); } const unboundStyles = { scrollViewContentContainer: { paddingHorizontal: 0, }, header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, paddingHorizontal: 16, }, stagedAvatarSection: { marginTop: 16, backgroundColor: 'panelForeground', paddingVertical: 24, alignItems: 'center', }, editUserAvatar: { alignItems: 'center', justifyContent: 'center', }, }; export default AvatarSelection; diff --git a/native/account/registration/connect-ethereum.react.js b/native/account/registration/connect-ethereum.react.js index d3e433a22..d05b28fb4 100644 --- a/native/account/registration/connect-ethereum.react.js +++ b/native/account/registration/connect-ethereum.react.js @@ -1,345 +1,345 @@ // @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 PrimaryButton from '../../components/primary-button.react.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, ); const siweSignatureRequestData = { messageType: SIWEMessageTypes.MSG_AUTH, }; export type ConnectEthereumParams = { +userSelections: { +coolOrNerdMode?: ?CoolOrNerdMode, +keyserverURL?: ?string, +farcasterID: ?string, }, }; 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 { 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; invariant( !ethereumAccount || ethereumAccount.nonceTimestamp, 'nonceTimestamp must be set after connecting to Ethereum account', ); const nonceExpired = ethereumAccount && ethereumAccount.nonceTimestamp && 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/connect-farcaster.react.js b/native/account/registration/connect-farcaster.react.js index 34c4341d9..f5221c6ac 100644 --- a/native/account/registration/connect-farcaster.react.js +++ b/native/account/registration/connect-farcaster.react.js @@ -1,259 +1,259 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import { useIsAppForegrounded } from 'lib/shared/lifecycle-utils.js'; import { 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 FarcasterPrompt from '../../components/farcaster-prompt.react.js'; import FarcasterWebView from '../../components/farcaster-web-view.react.js'; import type { FarcasterWebViewState } from '../../components/farcaster-web-view.react.js'; +import PrimaryButton from '../../components/primary-button.react.js'; import { type NavigationRoute, ConnectEthereumRouteName, AvatarSelectionRouteName, } from '../../navigation/route-names.js'; import { getFarcasterAccountAlreadyLinkedAlertDetails, type AlertDetails, } from '../../utils/alert-messages.js'; import Alert from '../../utils/alert.js'; import { useStaffCanSee } from '../../utils/staff-utils.js'; export type ConnectFarcasterParams = ?{ +userSelections?: { +coolOrNerdMode?: CoolOrNerdMode, +keyserverURL?: string, }, }; type Props = { +navigation: RegistrationNavigationProp<'ConnectFarcaster'>, +route: NavigationRoute<'ConnectFarcaster'>, }; function ConnectFarcaster(prop: Props): React.Node { const { navigation, route } = prop; const { navigate } = navigation; const userSelections = route.params?.userSelections; const registrationContext = React.useContext(RegistrationContext); invariant(registrationContext, 'registrationContext should be set'); const { cachedSelections, setCachedSelections, skipEthereumLoginOnce, setSkipEthereumLoginOnce, } = registrationContext; const [webViewState, setWebViewState] = React.useState('closed'); const { ethereumAccount } = cachedSelections; const goToNextStep = React.useCallback( (fid?: ?string) => { setWebViewState('closed'); invariant( !ethereumAccount || ethereumAccount.nonceTimestamp, 'nonceTimestamp must be set after connecting to Ethereum account', ); const nonceExpired = ethereumAccount && ethereumAccount.nonceTimestamp && siweNonceExpired(ethereumAccount.nonceTimestamp); if (nonceExpired) { setCachedSelections(oldUserSelections => ({ ...oldUserSelections, ethereumAccount: undefined, })); } if (!skipEthereumLoginOnce || !ethereumAccount || nonceExpired) { navigate<'ConnectEthereum'>({ name: ConnectEthereumRouteName, params: { userSelections: { ...userSelections, farcasterID: fid, }, }, }); return; } const newUserSelections = { ...userSelections, farcasterID: fid, accountSelection: ethereumAccount, }; setSkipEthereumLoginOnce(false); navigate<'AvatarSelection'>({ name: AvatarSelectionRouteName, params: { userSelections: newUserSelections }, }); }, [ navigate, skipEthereumLoginOnce, setSkipEthereumLoginOnce, ethereumAccount, userSelections, setCachedSelections, ], ); const onSkip = React.useCallback(() => goToNextStep(), [goToNextStep]); const identityServiceClient = React.useContext(IdentityClientContext); const getFarcasterUsers = identityServiceClient?.identityClient.getFarcasterUsers; invariant(getFarcasterUsers, 'Could not get getFarcasterUsers'); const [queuedAlert, setQueuedAlert] = React.useState(); const onSuccess = React.useCallback( async (fid: string) => { try { const commFCUsers = await getFarcasterUsers([fid]); if (commFCUsers.length > 0 && commFCUsers[0].farcasterID === fid) { const commUsername = commFCUsers[0].username; const alert = getFarcasterAccountAlreadyLinkedAlertDetails(commUsername); setQueuedAlert(alert); setWebViewState('closed'); } else { goToNextStep(fid); setCachedSelections(oldUserSelections => ({ ...oldUserSelections, farcasterID: fid, })); } } catch (e) { setQueuedAlert({ title: 'Failed to query Comm', message: 'We failed to query Comm to see if that Farcaster account is ' + 'already linked', }); setWebViewState('closed'); } }, [goToNextStep, setCachedSelections, getFarcasterUsers], ); const isAppForegrounded = useIsAppForegrounded(); React.useEffect(() => { if (!queuedAlert || !isAppForegrounded) { return; } Alert.alert(queuedAlert.title, queuedAlert.message); setQueuedAlert(null); }, [queuedAlert, isAppForegrounded]); const { farcasterID } = cachedSelections; const alreadyHasConnected = !!farcasterID; const onPressConnectFarcaster = React.useCallback(() => { setWebViewState('opening'); }, []); const defaultConnectButtonVariant = alreadyHasConnected ? 'outline' : 'enabled'; const connectButtonVariant = webViewState === 'opening' ? 'loading' : defaultConnectButtonVariant; const connectButtonText = alreadyHasConnected ? 'Connect new Farcaster account' : 'Connect Farcaster account'; const onUseAlreadyConnectedAccount = React.useCallback(() => { invariant( farcasterID, 'farcasterID should be set in onUseAlreadyConnectedAccount', ); goToNextStep(farcasterID); }, [farcasterID, goToNextStep]); const alreadyConnectedButton = React.useMemo(() => { if (!alreadyHasConnected) { return null; } return ( - ); }, [alreadyHasConnected, onUseAlreadyConnectedAccount]); const staffCanSee = useStaffCanSee(); const skipButton = React.useMemo(() => { if (!staffCanSee) { return undefined; } return ( - ); }, [staffCanSee, onSkip]); const farcasterPromptTextType = staffCanSee ? 'optional' : 'required'; const connectFarcaster = React.useMemo( () => ( {alreadyConnectedButton} - {skipButton} ), [ alreadyConnectedButton, connectButtonText, connectButtonVariant, onPressConnectFarcaster, onSuccess, webViewState, farcasterPromptTextType, skipButton, ], ); return connectFarcaster; } const styles = { scrollViewContentContainer: { flexGrow: 1, }, }; export default ConnectFarcaster; diff --git a/native/account/registration/cool-or-nerd-mode-selection.react.js b/native/account/registration/cool-or-nerd-mode-selection.react.js index 688ded4fe..ca7c559df 100644 --- a/native/account/registration/cool-or-nerd-mode-selection.react.js +++ b/native/account/registration/cool-or-nerd-mode-selection.react.js @@ -1,145 +1,141 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Text } from 'react-native'; 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 { RegistrationTile, RegistrationTileHeader, } from './registration-tile.react.js'; import type { CoolOrNerdMode } from './registration-types.js'; +import PrimaryButton from '../../components/primary-button.react.js'; import { type NavigationRoute, KeyserverSelectionRouteName, } from '../../navigation/route-names.js'; import { useStyles } from '../../themes/colors.js'; type Props = { +navigation: RegistrationNavigationProp<'CoolOrNerdModeSelection'>, +route: NavigationRoute<'CoolOrNerdModeSelection'>, }; function CoolOrNerdModeSelection(props: Props): React.Node { const registrationContext = React.useContext(RegistrationContext); invariant(registrationContext, 'registrationContext should be set'); const { cachedSelections, setCachedSelections } = registrationContext; const [currentSelection, setCurrentSelection] = React.useState(cachedSelections.coolOrNerdMode); const selectCool = React.useCallback(() => { setCurrentSelection('cool'); setCachedSelections(oldUserSelections => ({ ...oldUserSelections, coolOrNerdMode: 'cool', })); }, [setCachedSelections]); const selectNerd = React.useCallback(() => { setCurrentSelection('nerd'); setCachedSelections(oldUserSelections => ({ ...oldUserSelections, coolOrNerdMode: 'nerd', })); }, [setCachedSelections]); const { navigate } = props.navigation; const onSubmit = React.useCallback(() => { invariant( currentSelection, 'Button should be disabled if currentSelection is not set', ); navigate<'KeyserverSelection'>({ name: KeyserverSelectionRouteName, params: { userSelections: { coolOrNerdMode: currentSelection } }, }); }, [navigate, currentSelection]); const buttonState = currentSelection ? 'enabled' : 'disabled'; const styles = useStyles(unboundStyles); return ( To begin, choose your fighter Do you want Comm to choose reasonable defaults for you, or do you want to see all the options and make the decisions yourself? This setting will affect behavior throughout the app, but you can change it later in your settings. 🤓 Nerd mode We present more options and talk through their security and privacy implications in detail. 😎 Cool mode We select reasonable defaults for you. - + ); } const unboundStyles = { header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, }, body: { fontFamily: 'Arial', fontSize: 15, lineHeight: 20, color: 'panelForegroundSecondaryLabel', paddingBottom: 16, }, tileTitleText: { flex: 1, fontSize: 18, color: 'panelForegroundLabel', }, tileBody: { fontFamily: 'Arial', fontSize: 13, color: 'panelForegroundSecondaryLabel', }, emojiIcon: { fontSize: 32, bottom: 1, marginRight: 5, color: 'black', }, }; export default CoolOrNerdModeSelection; diff --git a/native/account/registration/existing-ethereum-account.react.js b/native/account/registration/existing-ethereum-account.react.js index 466920f25..4279edaf9 100644 --- a/native/account/registration/existing-ethereum-account.react.js +++ b/native/account/registration/existing-ethereum-account.react.js @@ -1,212 +1,212 @@ // @flow import type { StackNavigationEventMap, StackNavigationState, StackOptions, } from '@react-navigation/core'; import invariant from 'invariant'; import * as React from 'react'; import { Text, View } from 'react-native'; import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js'; import { useENSName } from 'lib/hooks/ens-cache.js'; import { useWalletLogIn } from 'lib/hooks/login-hooks.js'; import type { SIWEResult } from 'lib/types/siwe-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-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 PrimaryButton from '../../components/primary-button.react.js'; import type { RootNavigationProp } from '../../navigation/root-navigator.react.js'; import type { NavigationRoute, ScreenParamList, } from '../../navigation/route-names.js'; import { useStyles } from '../../themes/colors.js'; import { unknownErrorAlertDetails, appOutOfDateAlertDetails, } from '../../utils/alert-messages.js'; import Alert from '../../utils/alert.js'; import { useLegacySIWEServerCall } from '../siwe-hooks.js'; export type ExistingEthereumAccountParams = SIWEResult; type Props = { +navigation: RegistrationNavigationProp<'ExistingEthereumAccount'>, +route: NavigationRoute<'ExistingEthereumAccount'>, }; function ExistingEthereumAccount(props: Props): React.Node { const legacySiweServerCall = useLegacySIWEServerCall(); const walletLogIn = useWalletLogIn(); const [logInPending, setLogInPending] = React.useState(false); const registrationContext = React.useContext(RegistrationContext); invariant(registrationContext, 'registrationContext should be set'); const { setCachedSelections } = registrationContext; const { params } = props.route; const dispatch = useDispatch(); const { navigation } = props; const goBackToHome = navigation.getParent< ScreenParamList, 'Registration', StackNavigationState, StackOptions, StackNavigationEventMap, RootNavigationProp<'Registration'>, >()?.goBack; const onProceedToLogIn = React.useCallback(async () => { if (logInPending) { return; } setLogInPending(true); try { if (usingCommServicesAccessToken) { await walletLogIn(params.address, params.message, params.signature); } else { await legacySiweServerCall({ ...params, doNotRegister: true }); dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); } } catch (e) { const messageForException = getMessageForException(e); if (messageForException === 'nonce_expired') { setCachedSelections(oldUserSelections => ({ ...oldUserSelections, ethereumAccount: undefined, })); Alert.alert( 'Login attempt timed out', 'Try logging in from the main SIWE button on the home screen', [{ text: 'OK', onPress: goBackToHome }], { cancelable: false, }, ); } else if ( messageForException === 'unsupported_version' || messageForException === 'client_version_unsupported' ) { Alert.alert( appOutOfDateAlertDetails.title, appOutOfDateAlertDetails.message, [{ text: 'OK', onPress: goBackToHome }], { cancelable: false }, ); } else { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK' }], { cancelable: false, }, ); } throw e; } finally { setLogInPending(false); } }, [ logInPending, legacySiweServerCall, walletLogIn, params, dispatch, goBackToHome, setCachedSelections, ]); const { address } = params; const walletIdentifier = useENSName(address); const walletIdentifierTitle = walletIdentifier === address ? 'Ethereum wallet' : 'ENS name'; const { goBack } = navigation; const styles = useStyles(unboundStyles); return ( Account already exists for wallet You can proceed to log in with this wallet, or go back and use a different wallet. {walletIdentifierTitle} {walletIdentifier} - - ); } const unboundStyles = { header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, }, body: { fontFamily: 'Arial', fontSize: 15, lineHeight: 20, color: 'panelForegroundSecondaryLabel', paddingBottom: 40, }, walletTile: { backgroundColor: 'panelForeground', borderRadius: 8, padding: 24, alignItems: 'center', }, walletIdentifierTitleText: { fontSize: 17, color: 'panelForegroundLabel', textAlign: 'center', }, walletIdentifier: { backgroundColor: 'panelSecondaryForeground', paddingVertical: 8, paddingHorizontal: 24, borderRadius: 56, marginTop: 8, alignItems: 'center', }, walletIdentifierText: { fontSize: 15, color: 'panelForegroundLabel', }, }; export default ExistingEthereumAccount; diff --git a/native/account/registration/keyserver-selection.react.js b/native/account/registration/keyserver-selection.react.js index 3ce7990af..34feb5b3d 100644 --- a/native/account/registration/keyserver-selection.react.js +++ b/native/account/registration/keyserver-selection.react.js @@ -1,251 +1,247 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Text, View, TextInput } from 'react-native'; import { getVersionActionTypes } from 'lib/actions/device-actions.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { useIsKeyserverURLValid } from 'lib/shared/keyserver-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 RegistrationTextInput from './registration-text-input.react.js'; import { RegistrationTile, RegistrationTileHeader, } from './registration-tile.react.js'; import type { CoolOrNerdMode } from './registration-types.js'; import CommIcon from '../../components/comm-icon.react.js'; +import PrimaryButton from '../../components/primary-button.react.js'; import { type NavigationRoute, ConnectFarcasterRouteName, } from '../../navigation/route-names.js'; import { useSelector } from '../../redux/redux-utils.js'; import { useStyles, useColors } from '../../themes/colors.js'; import { defaultURLPrefix } from '../../utils/url-utils.js'; type Selection = 'ashoat' | 'custom'; export type KeyserverSelectionParams = { +userSelections: { +coolOrNerdMode: CoolOrNerdMode, }, }; const getVersionLoadingStatusSelector = createLoadingStatusSelector( getVersionActionTypes, ); type KeyserverSelectionError = 'cant_reach_keyserver'; type Props = { +navigation: RegistrationNavigationProp<'KeyserverSelection'>, +route: NavigationRoute<'KeyserverSelection'>, }; function KeyserverSelection(props: Props): React.Node { const registrationContext = React.useContext(RegistrationContext); invariant(registrationContext, 'registrationContext should be set'); const { cachedSelections, setCachedSelections } = registrationContext; const initialKeyserverURL = cachedSelections.keyserverURL; const [customKeyserver, setCustomKeyserver] = React.useState( initialKeyserverURL === defaultURLPrefix ? '' : initialKeyserverURL, ); const customKeyserverTextInputRef = React.useRef>(); let initialSelection; if (initialKeyserverURL === defaultURLPrefix) { initialSelection = 'ashoat'; } else if (initialKeyserverURL) { initialSelection = 'custom'; } const [error, setError] = React.useState(); const [currentSelection, setCurrentSelection] = React.useState(initialSelection); const selectAshoat = React.useCallback(() => { setCurrentSelection('ashoat'); customKeyserverTextInputRef.current?.blur(); if (currentSelection !== 'ashoat') { setError(undefined); } }, [currentSelection]); const customKeyserverEmpty = !customKeyserver; const selectCustom = React.useCallback(() => { setCurrentSelection('custom'); if (customKeyserverEmpty) { customKeyserverTextInputRef.current?.focus(); } if (currentSelection !== 'custom') { setError(undefined); } }, [customKeyserverEmpty, currentSelection]); const onCustomKeyserverFocus = React.useCallback(() => { setCurrentSelection('custom'); setError(undefined); }, []); let keyserverURL; if (currentSelection === 'ashoat') { keyserverURL = defaultURLPrefix; } else if (currentSelection === 'custom' && customKeyserver) { keyserverURL = customKeyserver; } const versionLoadingStatus = useSelector(getVersionLoadingStatusSelector); let buttonState = keyserverURL ? 'enabled' : 'disabled'; if (versionLoadingStatus === 'loading') { buttonState = 'loading'; } const isKeyserverURLValidPromiseCallback = useIsKeyserverURLValid(keyserverURL); const { navigate } = props.navigation; const { coolOrNerdMode } = props.route.params.userSelections; const onSubmit = React.useCallback(async () => { setError(undefined); const isKeyserverURLValid = await isKeyserverURLValidPromiseCallback(); if (!isKeyserverURLValid) { setError('cant_reach_keyserver'); return; } setCachedSelections(oldUserSelections => ({ ...oldUserSelections, keyserverURL, })); const userSelections = { coolOrNerdMode, keyserverURL }; navigate<'ConnectFarcaster'>({ name: ConnectFarcasterRouteName, params: { userSelections }, }); }, [ keyserverURL, isKeyserverURLValidPromiseCallback, setCachedSelections, navigate, coolOrNerdMode, ]); const styles = useStyles(unboundStyles); let errorText; if (error === 'cant_reach_keyserver') { errorText = ( Can’t reach that keyserver :( ); } const colors = useColors(); return ( Select a keyserver to join Chat communities on Comm are hosted on keyservers, which are user-operated backends. Keyservers allow Comm to offer strong privacy guarantees without sacrificing functionality. ashoat Ashoat is Comm’s founder, and his keyserver currently hosts most of the communities on Comm. Enter a keyserver {errorText} - + ); } const unboundStyles = { header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, }, body: { fontFamily: 'Arial', fontSize: 15, lineHeight: 20, color: 'panelForegroundSecondaryLabel', paddingBottom: 16, }, tileTitleText: { flex: 1, fontSize: 18, color: 'panelForegroundLabel', }, tileBody: { fontFamily: 'Arial', fontSize: 13, color: 'panelForegroundSecondaryLabel', }, cloud: { marginRight: 8, }, error: { marginTop: 16, }, errorText: { fontFamily: 'Arial', fontSize: 15, lineHeight: 20, color: 'redText', }, }; export default KeyserverSelection; diff --git a/native/account/registration/password-selection.react.js b/native/account/registration/password-selection.react.js index 79351bb6c..4ceff3f9a 100644 --- a/native/account/registration/password-selection.react.js +++ b/native/account/registration/password-selection.react.js @@ -1,251 +1,251 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, Text, Platform, TextInput } from 'react-native'; import sleep from 'lib/utils/sleep.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 RegistrationTextInput from './registration-text-input.react.js'; import type { CoolOrNerdMode } from './registration-types.js'; +import PrimaryButton from '../../components/primary-button.react.js'; import { type NavigationRoute, AvatarSelectionRouteName, } from '../../navigation/route-names.js'; import { useStyles } from '../../themes/colors.js'; import type { KeyPressEvent } from '../../types/react-native.js'; export type PasswordSelectionParams = { +userSelections: { +coolOrNerdMode?: ?CoolOrNerdMode, +keyserverURL?: ?string, +farcasterID: ?string, +username: string, }, }; type PasswordError = 'passwords_dont_match' | 'empty_password'; type Props = { +navigation: RegistrationNavigationProp<'PasswordSelection'>, +route: NavigationRoute<'PasswordSelection'>, }; function PasswordSelection(props: Props): React.Node { const registrationContext = React.useContext(RegistrationContext); invariant(registrationContext, 'registrationContext should be set'); const { cachedSelections, setCachedSelections } = registrationContext; const [password, setPassword] = React.useState( cachedSelections.password ?? '', ); const [confirmPassword, setConfirmPassword] = React.useState( cachedSelections.password ?? '', ); const passwordsMatch = password === confirmPassword; const passwordIsEmpty = password === ''; const [passwordError, setPasswordError] = React.useState(); const potentiallyClearErrors = React.useCallback(() => { if (!passwordsMatch || passwordIsEmpty) { return false; } setPasswordError(null); return true; }, [passwordsMatch, passwordIsEmpty]); const checkPasswordValidity = React.useCallback(() => { if (!passwordsMatch) { setPasswordError('passwords_dont_match'); return false; } else if (passwordIsEmpty) { setPasswordError('empty_password'); return false; } return potentiallyClearErrors(); }, [passwordsMatch, passwordIsEmpty, potentiallyClearErrors]); const { userSelections } = props.route.params; const { navigate } = props.navigation; const onProceed = React.useCallback(() => { if (!checkPasswordValidity()) { return; } const { username, ...rest } = userSelections; const newUserSelections = { ...rest, accountSelection: { accountType: 'username', username, password, }, }; setCachedSelections(oldUserSelections => ({ ...oldUserSelections, password, })); navigate<'AvatarSelection'>({ name: AvatarSelectionRouteName, params: { userSelections: newUserSelections }, }); }, [ checkPasswordValidity, userSelections, password, setCachedSelections, navigate, ]); const styles = useStyles(unboundStyles); let errorText; if (passwordError === 'passwords_dont_match') { errorText = ( Passwords don’t match ); } else if (passwordError === 'empty_password') { errorText = Password cannot be empty; } const confirmPasswordInputRef = React.useRef>(); const focusConfirmPasswordInput = React.useCallback(() => { confirmPasswordInputRef.current?.focus(); }, []); const iosPasswordBeingAutoFilled = React.useRef(false); const confirmPasswordEmpty = confirmPassword.length === 0; const onPasswordKeyPress = React.useCallback( (event: KeyPressEvent) => { const { key } = event.nativeEvent; // On iOS, paste doesn't trigger onKeyPress, but password autofill does // Password autofill calls onKeyPress with `key` set to the whole password if ( key.length > 1 && key !== 'Backspace' && key !== 'Enter' && confirmPasswordEmpty ) { iosPasswordBeingAutoFilled.current = true; } }, [confirmPasswordEmpty], ); const passwordInputRef = React.useRef>(); const passwordLength = password.length; const onChangePasswordInput = React.useCallback( (input: string) => { setPassword(input); if (iosPasswordBeingAutoFilled.current) { // On iOS, paste doesn't trigger onKeyPress, but password autofill does iosPasswordBeingAutoFilled.current = false; setConfirmPassword(input); passwordInputRef.current?.blur(); } else if ( Platform.OS === 'android' && input.length - passwordLength > 1 && confirmPasswordEmpty ) { // On Android, password autofill doesn't trigger onKeyPress. Instead we // rely on observing when the password field changes by more than one // character at a time. This means we treat paste the same way as // password autofill setConfirmPassword(input); passwordInputRef.current?.blur(); } }, [passwordLength, confirmPasswordEmpty], ); const shouldAutoFocus = React.useRef(!cachedSelections.password); /* eslint-disable react-hooks/rules-of-hooks */ if (Platform.OS === 'android') { // It's okay to call this hook conditionally because // the condition is guaranteed to never change React.useEffect(() => { void (async () => { await sleep(250); if (shouldAutoFocus.current) { passwordInputRef.current?.focus(); } })(); }, []); } /* eslint-enable react-hooks/rules-of-hooks */ const autoFocus = Platform.OS !== 'android' && shouldAutoFocus.current; return ( Pick a password {errorText} - ); } const unboundStyles = { header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, }, error: { marginTop: 16, }, errorText: { fontFamily: 'Arial', fontSize: 15, lineHeight: 20, color: 'redText', }, confirmPassword: { marginTop: 16, }, }; export default PasswordSelection; diff --git a/native/account/registration/registration-terms.react.js b/native/account/registration/registration-terms.react.js index bbd8da161..86f1b88c2 100644 --- a/native/account/registration/registration-terms.react.js +++ b/native/account/registration/registration-terms.react.js @@ -1,193 +1,193 @@ // @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 PrimaryButton from '../../components/primary-button.react.js'; import commSwooshSource from '../../img/comm-swoosh.png'; import { logInActionType } from '../../navigation/action-types.js'; import type { NavigationRoute } from '../../navigation/route-names.js'; import { useStyles } from '../../themes/colors.js'; import Alert from '../../utils/alert.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, setCachedSelections } = registrationContext; const [registrationInProgress, setRegistrationInProgress] = React.useState(false); const { userSelections } = props.route.params; const clearCachedSelections = React.useCallback(() => { setCachedSelections({}); }, [setCachedSelections]); const { navigation } = props; const { reconnectEthereum } = navigation; const { coolOrNerdMode, keyserverURL, farcasterID } = userSelections; const navigateToConnectEthereum = React.useCallback(() => { reconnectEthereum({ userSelections: { coolOrNerdMode, keyserverURL, farcasterID, }, }); }, [reconnectEthereum, coolOrNerdMode, keyserverURL, farcasterID]); const onNonceExpired = React.useCallback(() => { setCachedSelections(oldUserSelections => ({ ...oldUserSelections, ethereumAccount: undefined, })); Alert.alert( 'Registration attempt timed out', 'Please try to connect your Ethereum wallet again', [{ text: 'OK', onPress: navigateToConnectEthereum }], { cancelable: false, }, ); }, [setCachedSelections, navigateToConnectEthereum]); const onProceed = React.useCallback(async () => { setRegistrationInProgress(true); try { await register({ ...userSelections, clearCachedSelections, onNonceExpired, }); } finally { setRegistrationInProgress(false); } }, [register, userSelections, clearCachedSelections, onNonceExpired]); React.useEffect(() => { if (!registrationInProgress) { return undefined; } navigation.setOptions({ gestureEnabled: false, headerLeft: null, }); const removeListener = navigation.addListener('beforeRemove', e => { if (e.data.action.type !== logInActionType) { e.preventDefault(); } }); return () => { navigation.setOptions({ gestureEnabled: true, headerLeft: undefined, }); removeListener(); }; }, [navigation, registrationInProgress]); 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/siwe-backup-message-creation.react.js b/native/account/registration/siwe-backup-message-creation.react.js index 6234abc3d..918975c6f 100644 --- a/native/account/registration/siwe-backup-message-creation.react.js +++ b/native/account/registration/siwe-backup-message-creation.react.js @@ -1,326 +1,326 @@ // @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 PrimaryButton from '../../components/primary-button.react.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'; const siweBackupSignatureRequestData = { messageType: SIWEMessageTypes.MSG_BACKUP, }; type CreateSIWEBackupMessageBaseProps = { +onSuccessfulWalletSignature: (result: SIWEResult) => void, +onExistingWalletSignature?: () => void, +onSkip?: () => void, }; const CreateSIWEBackupMessageBase: React.ComponentType = React.memo( function CreateSIWEBackupMessageBase( props: CreateSIWEBackupMessageBaseProps, ): React.Node { const styles = useStyles(unboundStyles); const { onSuccessfulWalletSignature, onExistingWalletSignature, onSkip } = props; 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 ( ); } type SignSIWEBackupMessageForRestoreBaseProps = { +siweNonce: string, +siweIssuedAt: string, +siweStatement: string, +onSuccessfulWalletSignature: (result: SIWEResult) => void, +onSkip: () => void, }; function SignSIWEBackupMessageForRestore( props: SignSIWEBackupMessageForRestoreBaseProps, ): React.Node { const styles = useStyles(unboundStyles); const { panelState, openPanel, onPanelClosed, onPanelClosing, siwePanelSetLoading, } = useSIWEPanelState(); const { siweIssuedAt, siweNonce, siweStatement, onSuccessfulWalletSignature, onSkip, } = props; const siweSignatureRequestData = React.useMemo( () => ({ messageType: SIWEMessageTypes.MSG_BACKUP_RESTORE, siweNonce, siweStatement, siweIssuedAt, }), [siweStatement, siweIssuedAt, siweNonce], ); let siwePanel; if (panelState !== 'closed') { siwePanel = ( ); } return ( <> Decrypting your Comm backup To make sure we can’t see your data, Comm encrypts your backup using a signature from your wallet. - - + {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 { CreateSIWEBackupMessageBase, CreateSIWEBackupMessage, SignSIWEBackupMessageForRestore, }; diff --git a/native/account/registration/username-selection.react.js b/native/account/registration/username-selection.react.js index 3e435cd75..cdbefeef4 100644 --- a/native/account/registration/username-selection.react.js +++ b/native/account/registration/username-selection.react.js @@ -1,240 +1,240 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, Text } 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 { validUsernameRegex } from 'lib/shared/account-utils.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import { isValidEthereumAddress } from 'lib/utils/siwe-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 RegistrationTextInput from './registration-text-input.react.js'; import type { CoolOrNerdMode } from './registration-types.js'; +import PrimaryButton from '../../components/primary-button.react.js'; import { commRustModule } from '../../native-modules.js'; import { type NavigationRoute, PasswordSelectionRouteName, } from '../../navigation/route-names.js'; import { useSelector } from '../../redux/redux-utils.js'; import { useStyles } from '../../themes/colors.js'; const exactSearchUserLoadingStatusSelector = createLoadingStatusSelector( exactSearchUserActionTypes, ); export type UsernameSelectionParams = { +userSelections: { +coolOrNerdMode?: ?CoolOrNerdMode, +keyserverURL?: ?string, +farcasterID: ?string, }, }; type UsernameError = 'username_invalid' | 'username_taken'; type Props = { +navigation: RegistrationNavigationProp<'UsernameSelection'>, +route: NavigationRoute<'UsernameSelection'>, }; function UsernameSelection(props: Props): React.Node { const registrationContext = React.useContext(RegistrationContext); invariant(registrationContext, 'registrationContext should be set'); const { cachedSelections, setCachedSelections } = registrationContext; const [username, setUsername] = React.useState( cachedSelections.username ?? '', ); const validUsername = username.search(validUsernameRegex) > -1 && !isValidEthereumAddress(username.toLowerCase()); const [usernameError, setUsernameError] = React.useState(); const checkUsernameValidity = React.useCallback(() => { if (!validUsername) { setUsernameError('username_invalid'); return false; } setUsernameError(null); return true; }, [validUsername]); const { userSelections } = props.route.params; const { keyserverURL } = userSelections; const serverCallParamOverride = React.useMemo(() => { if (keyserverURL) { return { urlPrefix: keyserverURL }; } return undefined; }, [keyserverURL]); const exactSearchUserCall = useLegacyAshoatKeyserverCall( exactSearchUser, serverCallParamOverride, ); const dispatchActionPromise = useDispatchActionPromise(); const { navigate } = props.navigation; const onProceed = React.useCallback(async () => { if (!checkUsernameValidity()) { return; } let userAlreadyExists; if (usingCommServicesAccessToken) { const findUserIDResponseString = await commRustModule.findUserIDForUsername(username); const findUserIDResponse = JSON.parse(findUserIDResponseString); userAlreadyExists = !!findUserIDResponse.userID || findUserIDResponse.isReserved; } else { const searchPromise = exactSearchUserCall(username); void dispatchActionPromise(exactSearchUserActionTypes, searchPromise); const { userInfo } = await searchPromise; userAlreadyExists = !!userInfo; } if (userAlreadyExists) { setUsernameError('username_taken'); return; } setUsernameError(undefined); setCachedSelections(oldUserSelections => ({ ...oldUserSelections, username, })); navigate<'PasswordSelection'>({ name: PasswordSelectionRouteName, params: { userSelections: { ...userSelections, username, }, }, }); }, [ checkUsernameValidity, username, exactSearchUserCall, dispatchActionPromise, setCachedSelections, navigate, userSelections, ]); const exactSearchUserCallLoading = useSelector( state => exactSearchUserLoadingStatusSelector(state) === 'loading', ); let buttonVariant = 'disabled'; if (exactSearchUserCallLoading) { buttonVariant = 'loading'; } else if (validUsername) { buttonVariant = 'enabled'; } const styles = useStyles(unboundStyles); let errorText; if (usernameError === 'username_invalid') { errorText = ( <> Usernames must: {'1. '} Be at least one character long. {'2. '} Start with either a letter or a number. {'3. '} Contain only letters, numbers, or the characters “-” and “_”. ); } else if (usernameError === 'username_taken') { errorText = ( Username taken. Please try another one ); } const shouldAutoFocus = React.useRef(!cachedSelections.username); return ( Pick a username {errorText} - ); } const unboundStyles = { header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, }, error: { marginTop: 16, }, errorText: { fontFamily: 'Arial', fontSize: 15, lineHeight: 20, color: 'redText', }, listItem: { flexDirection: 'row', }, listItemNumber: { fontWeight: 'bold', }, listItemContent: { flexShrink: 1, }, }; export default UsernameSelection; diff --git a/native/community-creation/community-configuration.react.js b/native/community-creation/community-configuration.react.js index 0a0c2c2f0..489f73b17 100644 --- a/native/community-creation/community-configuration.react.js +++ b/native/community-creation/community-configuration.react.js @@ -1,222 +1,222 @@ // @flow import * as React from 'react'; import { Text, View } from 'react-native'; import { useNewThinThread, newThreadActionTypes, } from 'lib/actions/thread-actions.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import type { NewThreadResult } from 'lib/types/thread-types.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import CommunityCreationKeyserverLabel from './community-creation-keyserver-label.react.js'; import type { CommunityCreationNavigationProp } from './community-creation-navigator.react.js'; import RegistrationButtonContainer from '../account/registration/registration-button-container.react.js'; -import RegistrationButton from '../account/registration/registration-button.react.js'; import RegistrationContainer from '../account/registration/registration-container.react.js'; import RegistrationContentContainer from '../account/registration/registration-content-container.react.js'; import { useNavigateToThread } from '../chat/message-list-types.js'; import { ThreadSettingsCategoryFooter, ThreadSettingsCategoryHeader, } from '../chat/settings/thread-settings-category.react.js'; import EnumSettingsOption from '../components/enum-settings-option.react.js'; +import PrimaryButton from '../components/primary-button.react.js'; import TextInput from '../components/text-input.react.js'; import { useCalendarQuery } from '../navigation/nav-selectors.js'; import { type NavigationRoute } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { useColors, useStyles } from '../themes/colors.js'; type Props = { +navigation: CommunityCreationNavigationProp<'CommunityConfiguration'>, +route: NavigationRoute<'CommunityConfiguration'>, }; const createNewCommunityLoadingStatusSelector = createLoadingStatusSelector(newThreadActionTypes); // eslint-disable-next-line no-unused-vars function CommunityConfiguration(props: Props): React.Node { const styles = useStyles(unboundStyles); const colors = useColors(); const dispatchActionPromise = useDispatchActionPromise(); const callNewThinThread = useNewThinThread(); const calendarQueryFunc = useCalendarQuery(); const createNewCommunityLoadingStatus: LoadingStatus = useSelector( createNewCommunityLoadingStatusSelector, ); const [pendingCommunityName, setPendingCommunityName] = React.useState(''); const [announcementSetting, setAnnouncementSetting] = React.useState(false); const [errorMessage, setErrorMessage] = React.useState(); const onChangePendingCommunityName = React.useCallback((newValue: string) => { setErrorMessage(); setPendingCommunityName(newValue); }, []); const callCreateNewCommunity = React.useCallback(async () => { const calendarQuery = calendarQueryFunc(); try { const newThreadResult: NewThreadResult = await callNewThinThread({ name: pendingCommunityName, type: announcementSetting ? threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT : threadTypes.COMMUNITY_ROOT, calendarQuery, }); return newThreadResult; } catch (e) { setErrorMessage('Community creation failed. Please try again.'); throw e; } }, [ announcementSetting, calendarQueryFunc, callNewThinThread, pendingCommunityName, ]); const [newCommunityID, setNewCommunityID] = React.useState(null); const createNewCommunity = React.useCallback(async () => { setErrorMessage(); const newThreadResultPromise = callCreateNewCommunity(); void dispatchActionPromise(newThreadActionTypes, newThreadResultPromise); const newThreadResult = await newThreadResultPromise; setNewCommunityID(newThreadResult.newThreadID); }, [callCreateNewCommunity, dispatchActionPromise]); const navigateToThread = useNavigateToThread(); const threadInfos = useSelector(threadInfoSelector); React.useEffect(() => { if (!newCommunityID) { return; } const communityThreadInfo = threadInfos[newCommunityID]; if (communityThreadInfo) { navigateToThread({ threadInfo: communityThreadInfo }); } }, [navigateToThread, newCommunityID, threadInfos]); const onCheckBoxPress = React.useCallback(() => { setErrorMessage(); setAnnouncementSetting(!announcementSetting); }, [announcementSetting]); const enumSettingsOptionDescription = 'Make it so only admins can post to ' + 'the root channel of the community.'; return ( Name You may edit your community’s image and name later. - {errorMessage} ); } const unboundStyles = { containerPaddingOverride: { padding: 0, }, communityNameRow: { backgroundColor: 'panelForeground', flexDirection: 'row', paddingHorizontal: 24, paddingVertical: 8, }, communityNameLabel: { color: 'panelForegroundTertiaryLabel', fontSize: 16, width: 96, }, communityNamePendingValue: { color: 'panelForegroundSecondaryLabel', flex: 1, fontFamily: 'Arial', fontSize: 16, margin: 0, paddingLeft: 4, paddingRight: 0, paddingVertical: 0, borderBottomColor: 'transparent', }, communityNameNoticeContainer: { display: 'flex', flexDirection: 'row', justifyContent: 'center', }, communityNameNoticeText: { color: 'panelForegroundTertiaryLabel', }, optionalSettingsContainer: { backgroundColor: 'panelForeground', paddingVertical: 4, }, errorMessageContainer: { alignItems: 'center', }, errorMessageText: { color: 'redText', }, }; export default CommunityConfiguration; diff --git a/native/community-settings/tag-farcaster-channel/tag-farcaster-channel-by-name.react.js b/native/community-settings/tag-farcaster-channel/tag-farcaster-channel-by-name.react.js index ac0f3c615..0d6fc0c5d 100644 --- a/native/community-settings/tag-farcaster-channel/tag-farcaster-channel-by-name.react.js +++ b/native/community-settings/tag-farcaster-channel/tag-farcaster-channel-by-name.react.js @@ -1,154 +1,154 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, Text } from 'react-native'; import { NeynarClientContext } from 'lib/components/neynar-client-provider.react.js'; import { tagFarcasterChannelErrorMessages, useCreateFarcasterChannelTag, } from 'lib/shared/community-utils.js'; import type { TagFarcasterChannelNavigationProp } from './tag-farcaster-channel-navigator.react.js'; -import RegistrationButton from '../../account/registration/registration-button.react.js'; +import PrimaryButton from '../../components/primary-button.react.js'; import TextInput from '../../components/text-input.react.js'; import type { NavigationRoute } from '../../navigation/route-names.js'; import { useStyles, useColors } from '../../themes/colors.js'; export type TagFarcasterChannelByNameParams = { +communityID: string, }; type Props = { +navigation: TagFarcasterChannelNavigationProp<'TagFarcasterChannelByName'>, +route: NavigationRoute<'TagFarcasterChannelByName'>, }; function TagFarcasterChannelByName(prop: Props): React.Node { const { navigation, route } = prop; const { goBack } = navigation; const { communityID } = route.params; const styles = useStyles(unboundStyles); const colors = useColors(); const [channelSelectionText, setChannelSelectionText] = React.useState(''); const [error, setError] = React.useState(null); const neynarClientContext = React.useContext(NeynarClientContext); invariant(neynarClientContext, 'NeynarClientContext is missing'); const { createTag, isLoading } = useCreateFarcasterChannelTag( communityID, setError, goBack, ); const onPressTagChannel = React.useCallback(async () => { const channelInfo = await neynarClientContext.client.fetchFarcasterChannelByName( channelSelectionText, ); if (!channelInfo) { setError('channel_not_found'); return; } createTag(channelInfo.id); }, [channelSelectionText, createTag, neynarClientContext.client]); const errorMessage = React.useMemo(() => { if (!error) { return ; } return ( {tagFarcasterChannelErrorMessages[error] ?? 'Unknown error.'} ); }, [error, styles.error, styles.errorPlaceholder]); let submitButtonVariant = 'disabled'; if (isLoading) { submitButtonVariant = 'loading'; } else if (channelSelectionText.length > 0) { submitButtonVariant = 'enabled'; } return ( CHANNEL NAME {errorMessage} - ); } const unboundStyles = { container: { paddingTop: 24, }, header: { color: 'panelBackgroundLabel', fontSize: 12, fontWeight: '400', paddingBottom: 4, paddingHorizontal: 16, }, panelSectionContainer: { backgroundColor: 'panelForeground', padding: 16, borderBottomColor: 'panelSeparator', borderBottomWidth: 1, borderTopColor: 'panelSeparator', borderTopWidth: 1, }, inputContainer: { flexDirection: 'row', justifyContent: 'space-between', paddingHorizontal: 16, paddingVertical: 12, borderWidth: 1, borderColor: 'panelSecondaryForegroundBorder', borderRadius: 8, marginBottom: 8, }, input: { color: 'panelForegroundLabel', fontSize: 16, }, error: { fontSize: 12, fontWeight: '400', lineHeight: 18, textAlign: 'center', color: 'redText', }, errorPlaceholder: { height: 18, }, }; export default TagFarcasterChannelByName; diff --git a/native/components/connect-farcaster-bottom-sheet.react.js b/native/components/connect-farcaster-bottom-sheet.react.js index cad814f4f..2e24f3b32 100644 --- a/native/components/connect-farcaster-bottom-sheet.react.js +++ b/native/components/connect-farcaster-bottom-sheet.react.js @@ -1,124 +1,124 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, StyleSheet } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useIsAppForegrounded } from 'lib/shared/lifecycle-utils.js'; import { useCurrentUserFID } from 'lib/utils/farcaster-utils.js'; import FarcasterPrompt from './farcaster-prompt.react.js'; import FarcasterWebView, { type FarcasterWebViewState, } from './farcaster-web-view.react.js'; -import RegistrationButton from '../account/registration/registration-button.react.js'; +import PrimaryButton from './primary-button.react.js'; import { BottomSheetContext } from '../bottom-sheet/bottom-sheet-provider.react.js'; import BottomSheet from '../bottom-sheet/bottom-sheet.react.js'; import type { RootNavigationProp } from '../navigation/root-navigator.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useTryLinkFID } from '../utils/farcaster-utils.js'; const bottomSheetPaddingTop = 32; const farcasterPromptHeight = 350; const marginBottom = 40; const buttonHeight = 48; type Props = { +navigation: RootNavigationProp<'ConnectFarcasterBottomSheet'>, +route: NavigationRoute<'ConnectFarcasterBottomSheet'>, }; function ConnectFarcasterBottomSheet(props: Props): React.Node { const { navigation } = props; const { goBack } = navigation; const [webViewState, setWebViewState] = React.useState('closed'); const [isLoadingLinkFID, setIsLoadingLinkFID] = React.useState(false); const fid = useCurrentUserFID(); const tryLinkFID = useTryLinkFID(); const onSuccess = React.useCallback( async (newFID: string) => { setWebViewState('closed'); try { await tryLinkFID(newFID); } finally { setIsLoadingLinkFID(false); } }, [tryLinkFID], ); const bottomSheetRef = React.useRef(null); const bottomSheetContext = React.useContext(BottomSheetContext); invariant(bottomSheetContext, 'bottomSheetContext should be set'); const { setContentHeight } = bottomSheetContext; const insets = useSafeAreaInsets(); React.useLayoutEffect(() => { setContentHeight( bottomSheetPaddingTop + farcasterPromptHeight + marginBottom + buttonHeight + insets.bottom, ); }, [insets.bottom, setContentHeight]); const isAppForegrounded = useIsAppForegrounded(); React.useEffect(() => { if (fid && isAppForegrounded) { bottomSheetRef.current?.close(); } }, [fid, isAppForegrounded]); const onPressConnect = React.useCallback(() => { setIsLoadingLinkFID(true); setWebViewState('opening'); }, []); const connectButtonVariant = isLoadingLinkFID ? 'loading' : 'enabled'; const connectFarcasterBottomSheet = React.useMemo( () => ( - ), [connectButtonVariant, goBack, onPressConnect, onSuccess, webViewState], ); return connectFarcasterBottomSheet; } const styles = StyleSheet.create({ container: { flex: 1, paddingHorizontal: 16, }, promptContainer: { marginBottom: 40, }, }); export default ConnectFarcasterBottomSheet; diff --git a/native/account/registration/registration-button.react.js b/native/components/primary-button.react.js similarity index 91% rename from native/account/registration/registration-button.react.js rename to native/components/primary-button.react.js index 1207b6d2e..75af2dd76 100644 --- a/native/account/registration/registration-button.react.js +++ b/native/components/primary-button.react.js @@ -1,101 +1,101 @@ // @flow import * as React from 'react'; import { Text, View, ActivityIndicator } from 'react-native'; -import Button from '../../components/button.react.js'; -import { useColors, useStyles } from '../../themes/colors.js'; +import Button from './button.react.js'; +import { useColors, useStyles } from '../themes/colors.js'; type Props = { +onPress: () => mixed, +label: string, +variant?: 'enabled' | 'disabled' | 'loading' | 'outline', }; -function RegistrationButton(props: Props): React.Node { +function PrimaryButton(props: Props): React.Node { const { onPress, label, variant } = props; const styles = useStyles(unboundStyles); const buttonStyle = React.useMemo(() => { if (variant === 'disabled' || variant === 'loading') { return [styles.button, styles.disabledButton]; } else if (variant === 'outline') { return [styles.button, styles.outlineButton]; } else { return styles.button; } }, [variant, styles.button, styles.disabledButton, styles.outlineButton]); const buttonTextStyle = React.useMemo(() => { if (variant === 'disabled') { return [styles.buttonText, styles.disabledButtonText]; } else if (variant === 'loading') { return [styles.buttonText, styles.invisibleLoadingText]; } return styles.buttonText; }, [ variant, styles.buttonText, styles.disabledButtonText, styles.invisibleLoadingText, ]); const colors = useColors(); const spinner = React.useMemo(() => { if (variant !== 'loading') { return undefined; } return ( ); }, [variant, styles.spinner, colors.panelForegroundLabel]); return ( ); } const unboundStyles = { button: { backgroundColor: 'purpleButton', borderRadius: 8, marginVertical: 8, }, buttonText: { fontSize: 18, color: 'panelForegroundLabel', textAlign: 'center', padding: 12, }, disabledButton: { backgroundColor: 'disabledButton', }, outlineButton: { backgroundColor: 'panelBackground', borderColor: 'panelForegroundLabel', borderWidth: 1, }, disabledButtonText: { color: 'disabledButtonText', }, invisibleLoadingText: { color: 'transparent', }, spinner: { position: 'absolute', width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center', }, }; -export default RegistrationButton; +export default PrimaryButton; diff --git a/native/profile/farcaster-account-settings.react.js b/native/profile/farcaster-account-settings.react.js index 03cce29bc..71144d4c9 100644 --- a/native/profile/farcaster-account-settings.react.js +++ b/native/profile/farcaster-account-settings.react.js @@ -1,145 +1,145 @@ // @flow import * as React from 'react'; import { View } from 'react-native'; import { useCurrentUserFID, useUnlinkFID } from 'lib/utils/farcaster-utils.js'; import type { ProfileNavigationProp } from './profile.react.js'; -import RegistrationButton from '../account/registration/registration-button.react.js'; import FarcasterPrompt from '../components/farcaster-prompt.react.js'; import FarcasterWebView from '../components/farcaster-web-view.react.js'; import type { FarcasterWebViewState } from '../components/farcaster-web-view.react.js'; +import PrimaryButton from '../components/primary-button.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useStyles } from '../themes/colors.js'; import { unknownErrorAlertDetails } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; import { useTryLinkFID } from '../utils/farcaster-utils.js'; type Props = { +navigation: ProfileNavigationProp<'FarcasterAccountSettings'>, +route: NavigationRoute<'FarcasterAccountSettings'>, }; // eslint-disable-next-line no-unused-vars function FarcasterAccountSettings(props: Props): React.Node { const fid = useCurrentUserFID(); const styles = useStyles(unboundStyles); const [isLoadingUnlinkFID, setIsLoadingUnlinkFID] = React.useState(false); const unlinkFID = useUnlinkFID(); const onPressDisconnect = React.useCallback(async () => { setIsLoadingUnlinkFID(true); try { await unlinkFID(); } catch { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, ); } finally { setIsLoadingUnlinkFID(false); } }, [unlinkFID]); const [webViewState, setWebViewState] = React.useState('closed'); const [isLoadingLinkFID, setIsLoadingLinkFID] = React.useState(false); const tryLinkFID = useTryLinkFID(); const onSuccess = React.useCallback( async (newFID: string) => { setWebViewState('closed'); try { await tryLinkFID(newFID); } finally { setIsLoadingLinkFID(false); } }, [tryLinkFID], ); const onPressConnectFarcaster = React.useCallback(() => { setIsLoadingLinkFID(true); setWebViewState('opening'); }, []); const disconnectButtonVariant = isLoadingUnlinkFID ? 'loading' : 'outline'; const connectButtonVariant = isLoadingLinkFID ? 'loading' : 'enabled'; const button = React.useMemo(() => { if (fid) { return ( - ); } return ( - ); }, [ connectButtonVariant, disconnectButtonVariant, fid, onPressConnectFarcaster, onPressDisconnect, ]); const farcasterPromptTextType = fid ? 'disconnect' : 'optional'; const farcasterAccountSettings = React.useMemo( () => ( {button} ), [ button, farcasterPromptTextType, onSuccess, styles.buttonContainer, styles.connectContainer, styles.promptContainer, webViewState, ], ); return farcasterAccountSettings; } const unboundStyles = { connectContainer: { flex: 1, backgroundColor: 'panelBackground', paddingBottom: 16, }, promptContainer: { flex: 1, padding: 16, justifyContent: 'space-between', }, buttonContainer: { marginVertical: 8, marginHorizontal: 16, }, }; export default FarcasterAccountSettings;