From 2077a1595eca4088b4a42c1c932ecbe61bd4b2ea Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 25 Mar 2026 11:22:56 -0400 Subject: [PATCH] signup cleanup --- shared/settings/account/add-modals.tsx | 1 - shared/signup/common.tsx | 208 ++++++++++++--------- shared/signup/device-name.tsx | 46 +++-- shared/signup/email.tsx | 54 +++--- shared/signup/feedback.tsx | 29 ++- shared/signup/navigation.ts | 46 +++++ shared/signup/phone-number/index.tsx | 49 +++-- shared/signup/phone-number/verify-body.tsx | 1 - shared/signup/phone-number/verify.tsx | 65 +++---- shared/signup/routes.tsx | 31 +-- shared/signup/username.tsx | 67 +++---- 11 files changed, 332 insertions(+), 265 deletions(-) create mode 100644 shared/signup/navigation.ts diff --git a/shared/settings/account/add-modals.tsx b/shared/settings/account/add-modals.tsx index db284fefe26e..c8c544a70fdc 100644 --- a/shared/settings/account/add-modals.tsx +++ b/shared/settings/account/add-modals.tsx @@ -274,7 +274,6 @@ export const VerifyPhone = () => { diff --git a/shared/signup/common.tsx b/shared/signup/common.tsx index 0bfd0b91b3e5..2c3fb5a1a84a 100644 --- a/shared/signup/common.tsx +++ b/shared/signup/common.tsx @@ -62,6 +62,43 @@ type HeaderProps = { onRightAction?: () => void } +const HeaderInfoIcon = (props: Pick) => { + if (!props.showInfoIcon && !props.showInfoIconRow) { + return null + } + + return ( + + + + ) +} + +const HeaderRightAction = (props: Pick) => { + if (props.rightActionComponent) { + return ( + + {props.rightActionComponent} + + ) + } + + if (!props.onRightAction || !props.rightActionLabel) { + return null + } + + return ( + + ) +} + // Only used on desktop const Header = (props: HeaderProps) => ( ( fullWidth={true} style={Kb.Styles.collapseStyles([styles.headerContainer, props.style])} > - {(props.showInfoIcon || props.showInfoIconRow) && ( - - - - )} + {props.onBack && ( @@ -95,21 +132,11 @@ const Header = (props: HeaderProps) => ( )} {props.titleComponent || {props.title}} - {props.onRightAction && !!props.rightActionLabel && ( - - )} - {props.rightActionComponent && ( - - {props.rightActionComponent} - - )} + ) @@ -140,89 +167,96 @@ type SignupScreenProps = { rightActionComponent?: React.ReactNode rightActionLabel?: string onRightAction?: () => void + showHeaderInfoIcon?: boolean + showHeaderInfoIconRow?: boolean showHeaderInfoicon?: boolean showHeaderInfoiconRow?: boolean } +const SignupButtons = (props: {buttons?: Array}) => + !props.buttons ? null : ( + + {props.buttons.map(button => + button.waitingKey !== undefined ? ( + + ) : ( + + ) + )} + + ) + // Screens with header + body bg color (i.e. all but join-or-login) -export const SignupScreen = (props: SignupScreenProps) => ( - - {!Kb.Styles.isMobile && ( -
- )} - {Kb.Styles.isMobile && props.header} +export const SignupScreen = (props: SignupScreenProps) => { + const showHeaderInfoIcon = props.showHeaderInfoIcon ?? props.showHeaderInfoicon + const showHeaderInfoIconRow = props.showHeaderInfoIconRow ?? props.showHeaderInfoiconRow + + return ( + {!Kb.Styles.isMobile && ( +
+ )} + {Kb.Styles.isMobile && props.header} - {props.children} - - {!!props.footer && ( - - {props.footer} - - )} - {/* Banners after children so they go on top */} - {!!props.banners && } - {!!props.buttons && ( - - {props.buttons.map(b => - b.waitingKey !== undefined ? ( - - ) : ( - - ) - )} - - )} + {props.children} + + {!!props.footer && ( + + {props.footer} + + )} + {!!props.banners && } + + - -) + ) +} export const errorBanner = (error: string) => error.trim() ? ( diff --git a/shared/signup/device-name.tsx b/shared/signup/device-name.tsx index fc55f9791f42..20a5b1183574 100644 --- a/shared/signup/device-name.tsx +++ b/shared/signup/device-name.tsx @@ -6,21 +6,25 @@ import * as Provision from '@/stores/provision' import {useSignupState} from '@/stores/signup' const ConnectedEnterDevicename = () => { - const error = useSignupState(s => s.devicenameError) - const initialDevicename = useSignupState(s => s.devicename) + const {checkDeviceName, error, goBackAndClearErrors, initialDevicename} = useSignupState( + C.useShallow(s => ({ + checkDeviceName: s.dispatch.checkDeviceName, + error: s.devicenameError, + goBackAndClearErrors: s.dispatch.goBackAndClearErrors, + initialDevicename: s.devicename, + })) + ) const waiting = C.Waiting.useAnyWaiting(C.waitingKeyProvision) - const goBackAndClearErrors = useSignupState(s => s.dispatch.goBackAndClearErrors) - const checkDeviceName = useSignupState(s => s.dispatch.checkDeviceName) - const onBack = goBackAndClearErrors - const onContinue = checkDeviceName - const props = { - error, - initialDevicename, - onBack, - onContinue, - waiting, - } - return + + return ( + + ) } export default ConnectedEnterDevicename @@ -53,12 +57,18 @@ const EnterDevicename = (props: Props) => { !Provision.goodDeviceRE.test(cleanDeviceName) || Provision.badDeviceRE.test(cleanDeviceName) const showDisabled = disabled && !!cleanDeviceName && readyToShowError - const _setDeviceName = (deviceName: string) => { - setDeviceName(deviceName) + const onChangeDeviceName = (nextDeviceName: string) => { + setDeviceName(nextDeviceName) setReadyToShowError(false) _setReadyToShowError(true) } - const onContinue = () => (disabled ? {} : props.onContinue(cleanDeviceName)) + const onContinue = () => { + if (disabled) { + return + } + + props.onContinue(cleanDeviceName) + } React.useEffect(() => { if (cleanDeviceName !== deviceName) { @@ -97,7 +107,7 @@ const EnterDevicename = (props: Props) => { error={showDisabled} maxLength={64} placeholder="Name" - onChangeText={_setDeviceName} + onChangeText={onChangeDeviceName} onEnterKeyDown={onContinue} value={deviceName} /> diff --git a/shared/signup/email.tsx b/shared/signup/email.tsx index 80f741f1f361..4e5c6b39e2ec 100644 --- a/shared/signup/email.tsx +++ b/shared/signup/email.tsx @@ -4,34 +4,27 @@ import * as Kb from '@/common-adapters' import {SignupScreen, errorBanner} from './common' import {useSettingsEmailState} from '@/stores/settings-email' import {useSignupState} from '@/stores/signup' -import {usePushState} from '@/stores/push' +import {useCompleteSignupWithEmail, useSkipSignupEmail} from './navigation' const ConnectedEnterEmail = () => { - const _showPushPrompt = usePushState(s => C.isMobile && !s.hasPermissions && s.showPushPrompt) - const addedEmail = useSettingsEmailState(s => s.addedEmail) - const error = useSettingsEmailState(s => s.error) + const {addEmail, addedEmail, error} = useSettingsEmailState( + C.useShallow(s => ({ + addEmail: s.dispatch.addEmail, + addedEmail: s.addedEmail, + error: s.error, + })) + ) const initialEmail = useSignupState(s => s.email) const waiting = C.Waiting.useAnyWaiting(C.addEmailWaitingKey) - const clearModals = C.useRouterState(s => s.dispatch.clearModals) - const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend) - const setJustSignedUpEmail = useSignupState(s => s.dispatch.setJustSignedUpEmail) - const _onSkip = () => { - setJustSignedUpEmail(C.noEmail) - } - const _onSuccess = setJustSignedUpEmail + const onSkip = useSkipSignupEmail() + const onCompleteSignupWithEmail = useCompleteSignupWithEmail() - const addEmail = useSettingsEmailState(s => s.dispatch.addEmail) - const onSkip = () => { - _onSkip() - _showPushPrompt ? navigateAppend('settingsPushPrompt', true) : clearModals() - } const [addEmailInProgress, setAddEmailInProgress] = React.useState('') React.useEffect(() => { - if (addedEmail === addEmailInProgress) { - _onSuccess(addEmailInProgress) - _showPushPrompt ? navigateAppend('settingsPushPrompt', true) : clearModals() + if (addEmailInProgress && addedEmail === addEmailInProgress) { + onCompleteSignupWithEmail(addedEmail) } - }, [addedEmail, addEmailInProgress, _onSuccess, _showPushPrompt, navigateAppend, clearModals]) + }, [addedEmail, addEmailInProgress, onCompleteSignupWithEmail]) const onCreate = (email: string, searchable: boolean) => { addEmail(email, searchable) @@ -40,8 +33,15 @@ const ConnectedEnterEmail = () => { const [email, onChangeEmail] = React.useState(initialEmail || '') const [searchable, onChangeSearchable] = React.useState(true) - const disabled = !email.trim() - const onContinue = () => (disabled ? {} : onCreate(email.trim(), searchable)) + const emailTrimmed = email.trim() + const disabled = !emailTrimmed + const onContinue = () => { + if (disabled) { + return + } + + onCreate(emailTrimmed, searchable) + } return ( { rightActionLabel="Skip" onRightAction={onSkip} title="Your email address" - showHeaderInfoicon={true} + showHeaderInfoIcon={true} > { ) } -export type Props = { - error: string - initialEmail: string - onCreate: (email: string, searchable: boolean) => void - onSkip?: () => void - waiting: boolean -} - type BodyProps = { onChangeEmail: (email: string) => void onContinue: () => void diff --git a/shared/signup/feedback.tsx b/shared/signup/feedback.tsx index 1bc404e8c6a3..8e0fdc3a24fe 100644 --- a/shared/signup/feedback.tsx +++ b/shared/signup/feedback.tsx @@ -11,27 +11,26 @@ const SignupFeedback = () => { const loggedOut = useConfigState(s => !s.loggedIn) const sending = C.Waiting.useAnyWaiting(C.waitingKeySettingsSendFeedback) const navigateUp = C.useRouterState(s => s.dispatch.navigateUp) - const onBack = () => { - navigateUp() - } + const onBack = () => navigateUp() const [feedbackSent, setFeedbackSent] = React.useState(false) + const banners = ( + <> + {feedbackSent ? ( + + + + ) : null} + {sendError ? errorBanner(sendError) : null} + + ) return ( - {feedbackSent ? ( - - - - ) : null} - {sendError ? errorBanner(sendError) : null} - - } + banners={banners} title="Send feedback" onBack={onBack} - showHeaderInfoicon={false} - showHeaderInfoiconRow={!loggedOut} + showHeaderInfoIcon={false} + showHeaderInfoIconRow={!loggedOut} > { + const showPushPrompt = usePushState(s => C.isMobile && !s.hasPermissions && s.showPushPrompt) + const clearModals = C.useRouterState(s => s.dispatch.clearModals) + const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend) + + return React.useCallback(() => { + showPushPrompt ? navigateAppend('settingsPushPrompt', true) : clearModals() + }, [showPushPrompt, navigateAppend, clearModals]) +} + +export const useCompleteSignupWithEmail = () => { + const finishSignup = useFinishSignup() + const setJustSignedUpEmail = useSignupState(s => s.dispatch.setJustSignedUpEmail) + + return React.useCallback( + (email: string) => { + setJustSignedUpEmail(email) + finishSignup() + }, + [setJustSignedUpEmail, finishSignup] + ) +} + +export const useSkipSignupEmail = () => { + const completeSignupWithEmail = useCompleteSignupWithEmail() + + return React.useCallback(() => { + completeSignupWithEmail(C.noEmail) + }, [completeSignupWithEmail]) +} + +export const useNavigateToSignupEmail = () => { + const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend) + const clearPhoneNumberAdd = useSettingsPhoneState(s => s.dispatch.clearPhoneNumberAdd) + + return React.useCallback(() => { + clearPhoneNumberAdd() + navigateAppend('signupEnterEmail', true) + }, [clearPhoneNumberAdd, navigateAppend]) +} diff --git a/shared/signup/phone-number/index.tsx b/shared/signup/phone-number/index.tsx index d0c948ea3601..f08d1a40a56f 100644 --- a/shared/signup/phone-number/index.tsx +++ b/shared/signup/phone-number/index.tsx @@ -2,6 +2,7 @@ import * as C from '@/constants' import * as React from 'react' import * as Kb from '@/common-adapters' import {SignupScreen, errorBanner} from '../common' +import {useNavigateToSignupEmail} from '../navigation' import {useSettingsPhoneState} from '@/stores/settings-phone' type BodyProps = { @@ -71,25 +72,32 @@ const styles = Kb.Styles.styleSheetCreate(() => ({ })) const ConnectedEnterPhoneNumber = () => { - const defaultCountry = useSettingsPhoneState(s => s.defaultCountry) - const error = useSettingsPhoneState(s => s.error) - const pendingVerification = useSettingsPhoneState(s => s.pendingVerification) + const { + addPhoneNumber, + clearPhoneNumberErrors, + defaultCountry, + error, + loadDefaultPhoneCountry, + pendingVerification, + } = useSettingsPhoneState( + C.useShallow(s => ({ + addPhoneNumber: s.dispatch.addPhoneNumber, + clearPhoneNumberErrors: s.dispatch.clearPhoneNumberErrors, + defaultCountry: s.defaultCountry, + error: s.error, + loadDefaultPhoneCountry: s.dispatch.loadDefaultPhoneCountry, + pendingVerification: s.pendingVerification, + })) + ) const waiting = C.Waiting.useAnyWaiting(C.waitingKeySettingsPhoneAddPhoneNumber) - const clearPhoneNumberErrors = useSettingsPhoneState(s => s.dispatch.clearPhoneNumberErrors) - const clearPhoneNumberAdd = useSettingsPhoneState(s => s.dispatch.clearPhoneNumberAdd) - const onClear = clearPhoneNumberErrors - const addPhoneNumber = useSettingsPhoneState(s => s.dispatch.addPhoneNumber) const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend) - const onSkip = () => { - clearPhoneNumberAdd() - navigateAppend('signupEnterEmail', true) - } + const onSkip = useNavigateToSignupEmail() React.useEffect(() => { return () => { - onClear() + clearPhoneNumberErrors() } - }, [onClear]) + }, [clearPhoneNumberErrors]) const lastPendingVerificationRef = React.useRef(pendingVerification) React.useEffect(() => { @@ -100,15 +108,22 @@ const ConnectedEnterPhoneNumber = () => { }, [pendingVerification, error, navigateAppend]) // trigger a default phone number country rpc if it's not already loaded - const loadDefaultPhoneCountry = useSettingsPhoneState(s => s.dispatch.loadDefaultPhoneCountry) React.useEffect(() => { - !defaultCountry && loadDefaultPhoneCountry() + if (!defaultCountry) { + loadDefaultPhoneCountry() + } }, [defaultCountry, loadDefaultPhoneCountry]) const [phoneNumber, onChangePhoneNumber] = React.useState('') const [valid, onChangeValidity] = React.useState(false) const disabled = !valid - const onContinue = () => (disabled || waiting ? {} : addPhoneNumber(phoneNumber, true /* searchable */)) + const onContinue = () => { + if (disabled || waiting) { + return + } + + addPhoneNumber(phoneNumber, true) + } const onChangeNumberCb = (phoneNumber: string, validity: boolean) => { onChangePhoneNumber(phoneNumber) onChangeValidity(validity) @@ -128,7 +143,7 @@ const ConnectedEnterPhoneNumber = () => { rightActionLabel="Skip" onRightAction={onSkip} title="Your phone number" - showHeaderInfoicon={true} + showHeaderInfoIcon={true} > void - code: string onResend: () => void resendWaiting: boolean } diff --git a/shared/signup/phone-number/verify.tsx b/shared/signup/phone-number/verify.tsx index 83fc1ef27533..d7a51d86ff1a 100644 --- a/shared/signup/phone-number/verify.tsx +++ b/shared/signup/phone-number/verify.tsx @@ -1,40 +1,31 @@ import * as C from '@/constants' import * as React from 'react' import * as Kb from '@/common-adapters' -import {SignupScreen} from '../common' +import {SignupScreen, errorBanner} from '../common' import {e164ToDisplay} from '@/util/phone-numbers' import VerifyBody from './verify-body' import {useSettingsPhoneState} from '@/stores/settings-phone' const Container = () => { - const error = useSettingsPhoneState(s => (s.verificationState === 'error' ? s.error : '')) - const phoneNumber = useSettingsPhoneState(s => s.pendingVerification) + const {clearPhoneNumberAdd, error, phoneNumber, resendVerificationForPhone, verificationStatus, verifyPhoneNumber} = + useSettingsPhoneState( + C.useShallow(s => ({ + clearPhoneNumberAdd: s.dispatch.clearPhoneNumberAdd, + error: s.verificationState === 'error' ? s.error : '', + phoneNumber: s.pendingVerification, + resendVerificationForPhone: s.dispatch.resendVerificationForPhone, + verificationStatus: s.verificationState, + verifyPhoneNumber: s.dispatch.verifyPhoneNumber, + })) + ) const resendWaiting = C.Waiting.useAnyWaiting([ C.waitingKeySettingsPhoneResendVerification, C.waitingKeySettingsPhoneAddPhoneNumber, ]) - const verificationStatus = useSettingsPhoneState(s => s.verificationState) const verifyWaiting = C.Waiting.useAnyWaiting(C.waitingKeySettingsPhoneVerifyPhoneNumber) - - const verifyPhoneNumber = useSettingsPhoneState(s => s.dispatch.verifyPhoneNumber) - const resendVerificationForPhone = useSettingsPhoneState(s => s.dispatch.resendVerificationForPhone) - - const clearPhoneNumberAdd = useSettingsPhoneState(s => s.dispatch.clearPhoneNumberAdd) - - const _onContinue = (phoneNumber: string, code: string) => { - verifyPhoneNumber(phoneNumber, code) - } - const _onResend = (phoneNumber: string) => { - resendVerificationForPhone(phoneNumber) - } const navigateUp = C.useRouterState(s => s.dispatch.navigateUp) - const onBack = () => { - navigateUp() - } - const onCleanup = clearPhoneNumberAdd const onSuccess = C.useRouterState(s => s.dispatch.clearModals) - const ponContinue = (code: string) => _onContinue(phoneNumber, code) - const onResend = () => _onResend(phoneNumber) + const onBack = () => navigateUp() React.useEffect(() => { if (verificationStatus === 'success') { @@ -44,29 +35,25 @@ const Container = () => { React.useEffect(() => { return () => { - onCleanup() + clearPhoneNumberAdd() } - }, [onCleanup]) + }, [clearPhoneNumberAdd]) const [code, onChangeCode] = React.useState('') - const disabled = !code - const onContinue = disabled - ? () => {} - : () => { - ponContinue(code) - } + const onContinue = () => { + if (!code) { + return + } + + verifyPhoneNumber(phoneNumber, code) + } + const onResend = () => resendVerificationForPhone(phoneNumber) const displayPhone = e164ToDisplay(phoneNumber) return ( - - - ) : null - } + banners={errorBanner(error)} buttons={[{label: 'Continue', onClick: onContinue, type: 'Success', waiting: verifyWaiting}]} titleComponent={ @@ -87,9 +74,9 @@ const Container = () => { } negativeHeader={true} - showHeaderInfoicon={true} + showHeaderInfoIcon={true} > - + ) } diff --git a/shared/signup/routes.tsx b/shared/signup/routes.tsx index 4cbcd42e2136..0cd710fbac8e 100644 --- a/shared/signup/routes.tsx +++ b/shared/signup/routes.tsx @@ -1,40 +1,23 @@ import * as React from 'react' -import * as C from '@/constants' import * as Kb from '@/common-adapters' import {InfoIcon} from './common' -import {useSignupState} from '@/stores/signup' -import {useSettingsPhoneState} from '@/stores/settings-phone' -import {usePushState} from '@/stores/push' +import {useNavigateToSignupEmail, useSkipSignupEmail} from './navigation' const EmailSkipButton = () => { - const showPushPrompt = usePushState(s => C.isMobile && !s.hasPermissions && s.showPushPrompt) - const clearModals = C.useRouterState(s => s.dispatch.clearModals) - const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend) - const setJustSignedUpEmail = useSignupState(s => s.dispatch.setJustSignedUpEmail) + const onSkip = useSkipSignupEmail() + return ( - { - setJustSignedUpEmail(C.noEmail) - showPushPrompt ? navigateAppend('settingsPushPrompt', true) : clearModals() - }} - > + Skip ) } const PhoneSkipButton = () => { - const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend) - const clearPhoneNumberAdd = useSettingsPhoneState(s => s.dispatch.clearPhoneNumberAdd) + const onSkip = useNavigateToSignupEmail() + return ( - { - clearPhoneNumberAdd() - navigateAppend('signupEnterEmail', true) - }} - > + Skip ) diff --git a/shared/signup/username.tsx b/shared/signup/username.tsx index fca50d729add..878f7799164d 100644 --- a/shared/signup/username.tsx +++ b/shared/signup/username.tsx @@ -6,33 +6,35 @@ import {useSignupState} from '@/stores/signup' import {useProvisionState} from '@/stores/provision' const ConnectedEnterUsername = () => { - const error = useSignupState(s => s.usernameError) - const initialUsername = useSignupState(s => s.username) - const usernameTaken = useSignupState(s => s.usernameTaken) - const checkUsername = useSignupState(s => s.dispatch.checkUsername) + const {checkUsername, error, initialUsername, resetState, usernameTaken} = useSignupState( + C.useShallow(s => ({ + checkUsername: s.dispatch.checkUsername, + error: s.usernameError, + initialUsername: s.username, + resetState: s.dispatch.resetState, + usernameTaken: s.usernameTaken, + })) + ) const waiting = C.Waiting.useAnyWaiting(C.waitingKeySignup) const navigateUp = C.useRouterState(s => s.dispatch.navigateUp) - const resetState = useSignupState(s => s.dispatch.resetState) + const onBack = () => { resetState() navigateUp() } - const onContinue = checkUsername - const startProvision = useProvisionState(s => s.dispatch.startProvision) - const onLogin = (initUsername: string) => { - startProvision(initUsername) - } - const props = { - error, - initialUsername, - onBack, - onContinue, - onLogin, - usernameTaken, - waiting, - } - return + + return ( + + ) } type Props = { @@ -55,23 +57,24 @@ const EnterUsername = (props: Props) => { if (disabled) { return } + onChangeUsername(usernameTrimmed) // maybe trim the input props.onContinue(usernameTrimmed) } - const eulaLabel = ( - - I accept the{' '} - - Keybase Acceptable Use Policy - - - ) + const eulaTextType = Kb.Styles.isMobile ? 'BodySmall' : 'Body' + const eulaLinkType = Kb.Styles.isMobile ? 'BodySmallPrimaryLink' : 'BodyPrimaryLink' const eulaBlock = ( - setAcceptedEULA(s => !s)} /> + + I accept the Keybase Acceptable Use Policy + + } + checked={acceptedEULA} + onCheck={() => setAcceptedEULA(s => !s)} + /> ) + return (