diff --git a/shared/constants/init/shared.tsx b/shared/constants/init/shared.tsx index d6d812b37e6d..b17c5fea13ce 100644 --- a/shared/constants/init/shared.tsx +++ b/shared/constants/init/shared.tsx @@ -28,7 +28,6 @@ import type * as UseNotificationsStateType from '@/stores/notifications' import type * as UsePeopleStateType from '@/stores/people' import type * as UsePinentryStateType from '@/stores/pinentry' import type * as UseSettingsPasswordStateType from '@/stores/settings-password' -import type * as UseSignupStateType from '@/stores/signup' import type * as UseTeamsStateType from '@/stores/teams' import type * as UseTracker2StateType from '@/stores/tracker' import type * as UnlockFoldersType from '@/stores/unlock-folders' @@ -54,8 +53,6 @@ import {useNotifState} from '@/stores/notifications' import {useProvisionState} from '@/stores/provision' import {usePushState} from '@/stores/push' import {useSettingsContactsState} from '@/stores/settings-contacts' -import {useSettingsEmailState} from '@/stores/settings-email' -import {useSignupState} from '@/stores/signup' import {useState as useRecoverPasswordState} from '@/stores/recover-password' import {useTeamsState} from '@/stores/teams' import {useTrackerState} from '@/stores/tracker' @@ -63,6 +60,8 @@ import {useUsersState} from '@/stores/users' import {useRouterState} from '@/stores/router' import * as Util from '@/constants/router' import {setConvoDefer} from '@/stores/convostate' +import {clearSignupEmail} from '@/people/signup-email' +import {clearSignupDeviceNameDraft} from '@/signup/device-name-draft' let _emitStartupOnLoadDaemonConnectedOnce: boolean = __DEV__ ? (globalThis.__hmr_startupOnce ?? false) : false @@ -325,24 +324,6 @@ export const initRecoverPasswordCallbacks = () => { }) } -export const initSignupCallbacks = () => { - const currentState = useSignupState.getState() - useSignupState.setState({ - dispatch: { - ...currentState.dispatch, - defer: { - ...currentState.dispatch.defer, - onEditEmail: (p: {email: string; makeSearchable: boolean}) => { - useSettingsEmailState.getState().dispatch.editEmail(p) - }, - onShowPermissionsPrompt: (p: {justSignedUp?: boolean}) => { - usePushState.getState().dispatch.showPermissionsPrompt(p) - }, - }, - }, - }) -} - export const initTracker2Callbacks = () => { const currentState = useTrackerState.getState() useTrackerState.setState({ @@ -458,6 +439,9 @@ export const initSharedSubscriptions = () => { if (s.loggedIn) { ignorePromise(storeRegistry.getState('daemon').dispatch.loadDaemonBootstrapStatus()) storeRegistry.getState('fs').dispatch.checkKbfsDaemonRpcStatus() + } else { + clearSignupEmail() + clearSignupDeviceNameDraft() } storeRegistry .getState('daemon') @@ -620,10 +604,9 @@ export const initSharedSubscriptions = () => { prev && Util.getTab(prev) === Tabs.peopleTab && next && - Util.getTab(next) !== Tabs.peopleTab && - storeRegistry.getState('signup').justSignedUpEmail + Util.getTab(next) !== Tabs.peopleTab ) { - storeRegistry.getState('signup').dispatch.clearJustSignedUpEmail() + clearSignupEmail() } if (prev && Util.getTab(prev) === Tabs.peopleTab && next && Util.getTab(next) !== Tabs.peopleTab) { @@ -657,7 +640,6 @@ export const initSharedSubscriptions = () => { initNotificationsCallbacks() initPushCallbacks() initRecoverPasswordCallbacks() - initSignupCallbacks() initTracker2Callbacks() } @@ -736,8 +718,7 @@ export const _onEngineIncoming = (action: EngineGen.Actions) => { if (emailAddress) { storeRegistry.getState('settings-email').dispatch.notifyEmailVerified(emailAddress) } - const {useSignupState} = require('@/stores/signup') as typeof UseSignupStateType - useSignupState.getState().dispatch.onEngineIncomingImpl(action) + clearSignupEmail() } break case 'keybase.1.secretUi.getPassphrase': diff --git a/shared/people/container.tsx b/shared/people/container.tsx index e56f23293068..1b3f14f71612 100644 --- a/shared/people/container.tsx +++ b/shared/people/container.tsx @@ -11,11 +11,11 @@ import isEqual from 'lodash/isEqual' import People from '.' import * as T from '@/constants/types' import {useFollowerState} from '@/stores/followers' -import {useSignupState} from '@/stores/signup' import {usePeopleState} from '@/stores/people' import {useCurrentUserState} from '@/stores/current-user' import type {e164ToDisplay as e164ToDisplayType} from '@/util/phone-numbers' import {navToProfile} from '@/constants/router' +import {useSignupEmail} from './signup-email' const getPeopleDataWaitingKey = 'getPeopleData' const waitToRefresh = 1000 * 60 * 5 @@ -398,7 +398,7 @@ const PeopleReloadable = () => { } = usePeoplePageState() const refreshCount = usePeopleState(s => s.refreshCount) const username = useCurrentUserState(s => s.username) - const signupEmail = useSignupState(s => s.justSignedUpEmail) + const signupEmail = useSignupEmail() const waiting = C.Waiting.useAnyWaiting(getPeopleDataWaitingKey) const lastRefreshRef = React.useRef(0) const lastSeenRefreshRef = React.useRef(refreshCount) diff --git a/shared/people/index.shared.tsx b/shared/people/index.shared.tsx index 3f249fc57dd2..eb9c6f4e1523 100644 --- a/shared/people/index.shared.tsx +++ b/shared/people/index.shared.tsx @@ -7,7 +7,7 @@ import FollowNotification from './follow-notification' import FollowSuggestions from './follow-suggestions' import type {Props} from '.' import Todo from './todo' -import {useSignupState} from '@/stores/signup' +import {clearSignupEmail} from './signup-email' // import WotTask from './wot-task' const itemToComponent: (item: T.Immutable, props: Props) => React.ReactNode = ( @@ -63,16 +63,15 @@ const itemToComponent: (item: T.Immutable, props: Pro } } -function EmailVerificationBanner() { - const clearJustSignedUpEmail = useSignupState(s => s.dispatch.clearJustSignedUpEmail) - const signupEmail = useSignupState(s => s.justSignedUpEmail) +function EmailVerificationBanner(props: {signupEmail: string}) { + const {signupEmail} = props React.useEffect( () => // Only have a cleanup function () => { - signupEmail && clearJustSignedUpEmail() + signupEmail && clearSignupEmail() }, - [clearJustSignedUpEmail, signupEmail] + [signupEmail] ) if (!signupEmail) { @@ -117,7 +116,7 @@ function ResentEmailVerificationBanner(props: {resentEmail: string; setResentEma export function PeoplePageList(props: Props) { return ( - + {props.newItems .filter(item => item.type !== 'todo' || item.todoType !== 'verifyAllEmail' || !props.signupEmail) diff --git a/shared/people/signup-email.tsx b/shared/people/signup-email.tsx new file mode 100644 index 000000000000..37cf0e063eae --- /dev/null +++ b/shared/people/signup-email.tsx @@ -0,0 +1,35 @@ +import * as React from 'react' + +let signupEmail = '' +const listeners = new Set<() => void>() + +const notify = () => { + listeners.forEach(listener => listener()) +} + +const subscribe = (listener: () => void) => { + listeners.add(listener) + return () => { + listeners.delete(listener) + } +} + +export const useSignupEmail = () => React.useSyncExternalStore(subscribe, () => signupEmail) + +export const getSignupEmail = () => signupEmail + +export const setSignupEmail = (email: string) => { + if (signupEmail === email) { + return + } + signupEmail = email + notify() +} + +export const clearSignupEmail = () => { + if (!signupEmail) { + return + } + signupEmail = '' + notify() +} diff --git a/shared/signup/device-name-draft.tsx b/shared/signup/device-name-draft.tsx new file mode 100644 index 000000000000..f37c1e8974a4 --- /dev/null +++ b/shared/signup/device-name-draft.tsx @@ -0,0 +1,13 @@ +import * as S from '@/constants/strings' + +let devicename = S.defaultDevicename + +export const getSignupDeviceNameDraft = () => devicename + +export const setSignupDeviceNameDraft = (nextDevicename: string) => { + devicename = nextDevicename +} + +export const clearSignupDeviceNameDraft = () => { + devicename = S.defaultDevicename +} diff --git a/shared/signup/device-name.tsx b/shared/signup/device-name.tsx index c08002cc58c5..14a15a1c9977 100644 --- a/shared/signup/device-name.tsx +++ b/shared/signup/device-name.tsx @@ -3,28 +3,27 @@ import * as Kb from '@/common-adapters' import * as React from 'react' import {SignupScreen, errorBanner} from './common' import * as Provision from '@/stores/provision' -import {useSignupState} from '@/stores/signup' +import {usePushState} from '@/stores/push' import * as T from '@/constants/types' import {RPCError} from '@/util/errors' import {ignorePromise} from '@/constants/utils' import * as Platforms from '@/constants/platform' import logger from '@/logger' import type {StaticScreenProps} from '@react-navigation/core' +import { + clearSignupDeviceNameDraft, + getSignupDeviceNameDraft, + setSignupDeviceNameDraft, +} from './device-name-draft' type Props = StaticScreenProps<{inviteCode?: string; username?: string}> const ConnectedEnterDevicename = (p: Props) => { - const initialDevicename = useSignupState(s => s.devicename) + const showPermissionsPrompt = usePushState(s => s.dispatch.showPermissionsPrompt) + const initialDevicename = getSignupDeviceNameDraft() const inviteCode = p.route.params.inviteCode ?? '' const username = p.route.params.username ?? '' const waiting = C.Waiting.useAnyWaiting(C.waitingKeySignup) - const {resetState, setDevicename, showPermissionsPrompt} = useSignupState( - C.useShallow(s => ({ - resetState: s.dispatch.resetState, - setDevicename: s.dispatch.setDevicename, - showPermissionsPrompt: s.dispatch.defer.onShowPermissionsPrompt, - })) - ) const {navigateAppend, navigateUp} = C.useRouterState( C.useShallow(s => ({ navigateAppend: s.dispatch.navigateAppend, @@ -34,7 +33,7 @@ const ConnectedEnterDevicename = (p: Props) => { const [error, setError] = React.useState('') const onContinue = (devicename: string) => { setError('') - setDevicename(devicename) + setSignupDeviceNameDraft(devicename) const f = async () => { try { await T.RPCGen.deviceCheckDeviceNameFormatRpcPromise({name: devicename}, C.waitingKeySignup) @@ -51,7 +50,7 @@ const ConnectedEnterDevicename = (p: Props) => { } try { - showPermissionsPrompt?.({justSignedUp: true}) + showPermissionsPrompt({justSignedUp: true}) await T.RPCGen.signupSignupRpcListener({ customResponseIncomingCallMap: { 'keybase.1.gpgUi.wantToAddGPGKey': (_, response) => { @@ -79,10 +78,10 @@ const ConnectedEnterDevicename = (p: Props) => { }, waitingKey: C.waitingKeySignup, }) - resetState() + clearSignupDeviceNameDraft() } catch (error_) { if (error_ instanceof RPCError) { - showPermissionsPrompt?.({justSignedUp: false}) + showPermissionsPrompt({justSignedUp: false}) navigateAppend({ name: 'signupError', params: {errorCode: error_.code, errorMessage: error_.desc}, diff --git a/shared/signup/email.tsx b/shared/signup/email.tsx index 1f8ac176b00c..0fb71351e168 100644 --- a/shared/signup/email.tsx +++ b/shared/signup/email.tsx @@ -3,17 +3,16 @@ import * as React from 'react' import * as Kb from '@/common-adapters' import {SignupScreen, errorBanner} from './common' import {useAddEmail} from '@/settings/account/use-add-email' -import {useSignupState} from '@/stores/signup' import {usePushState} from '@/stores/push' +import {setSignupEmail} from '@/people/signup-email' const ConnectedEnterEmail = () => { const _showPushPrompt = usePushState(s => C.isMobile && !s.hasPermissions && s.showPushPrompt) const {error, submitEmail, waiting} = useAddEmail() 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) + setSignupEmail(C.noEmail) } const onSkip = () => { @@ -23,7 +22,7 @@ const ConnectedEnterEmail = () => { const onCreate = (email: string, searchable: boolean) => { submitEmail(email, searchable, addedEmail => { - setJustSignedUpEmail(addedEmail) + setSignupEmail(addedEmail) _showPushPrompt ? navigateAppend('settingsPushPrompt', true) : clearModals() }) } diff --git a/shared/signup/routes.tsx b/shared/signup/routes.tsx index 07c920a218ef..ada9aea14a17 100644 --- a/shared/signup/routes.tsx +++ b/shared/signup/routes.tsx @@ -2,19 +2,18 @@ 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 {usePushState} from '@/stores/push' +import {setSignupEmail} from '@/people/signup-email' 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) return ( { - setJustSignedUpEmail(C.noEmail) + setSignupEmail(C.noEmail) showPushPrompt ? navigateAppend('settingsPushPrompt', true) : clearModals() }} > diff --git a/shared/signup/username.tsx b/shared/signup/username.tsx index ab007f881616..1dd27ee62bdc 100644 --- a/shared/signup/username.tsx +++ b/shared/signup/username.tsx @@ -2,7 +2,6 @@ import * as C from '@/constants' import * as Kb from '@/common-adapters' import * as React from 'react' import {SignupScreen, errorBanner} from './common' -import {useSignupState} from '@/stores/signup' import {useProvisionState} from '@/stores/provision' import * as T from '@/constants/types' import {RPCError} from '@/util/errors' @@ -10,13 +9,13 @@ import {ignorePromise} from '@/constants/utils' import logger from '@/logger' import {isValidUsername} from '@/util/simple-validators' import type {StaticScreenProps} from '@react-navigation/core' +import {clearSignupDeviceNameDraft} from './device-name-draft' type Props = StaticScreenProps<{inviteCode?: string; username?: string}> const ConnectedEnterUsername = (p: Props) => { const initialUsername = p.route.params.username ?? '' const inviteCode = p.route.params.inviteCode ?? '' - const resetState = useSignupState(s => s.dispatch.resetState) const waiting = C.Waiting.useAnyWaiting(C.waitingKeySignup) const {navigateAppend, navigateUp} = C.useRouterState( C.useShallow(s => ({ @@ -25,7 +24,7 @@ const ConnectedEnterUsername = (p: Props) => { })) ) const onBack = () => { - resetState() + clearSignupDeviceNameDraft() navigateUp() } const [error, setError] = React.useState('') diff --git a/shared/stores/signup.tsx b/shared/stores/signup.tsx deleted file mode 100644 index 8672e578e352..000000000000 --- a/shared/stores/signup.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as S from '@/constants/strings' -import type * as EngineGen from '@/constants/rpc' -import type * as T from '@/constants/types' -import * as Z from '@/util/zustand' - -type Store = T.Immutable<{ - devicename: string - justSignedUpEmail: string -}> - -const initialStore: Store = { - devicename: S.defaultDevicename, - justSignedUpEmail: '', -} - -export type State = Store & { - dispatch: { - defer: { - onEditEmail?: (p: {email: string; makeSearchable: boolean}) => void - onShowPermissionsPrompt?: (p: {justSignedUp?: boolean}) => void - } - clearJustSignedUpEmail: () => void - onEngineIncomingImpl: (action: EngineGen.Actions) => void - resetState: () => void - setDevicename: (devicename: string) => void - setJustSignedUpEmail: (email: string) => void - } -} - -export const useSignupState = Z.createZustand('signup', (set, get) => { - const dispatch: State['dispatch'] = { - clearJustSignedUpEmail: () => { - set(s => { - s.justSignedUpEmail = '' - }) - }, - defer: { - onEditEmail: () => { - throw new Error('onEditEmail not implemented') - }, - onShowPermissionsPrompt: () => { - throw new Error('onShowPermissionsPrompt not implemented') - }, - }, - onEngineIncomingImpl: action => { - switch (action.type) { - case 'keybase.1.NotifyEmailAddress.emailAddressVerified': - get().dispatch.clearJustSignedUpEmail() - break - default: - } - }, - resetState: () => { - set(s => ({ - ...s, - ...initialStore, - })) - }, - setDevicename: (devicename: string) => { - set(s => { - s.devicename = devicename - }) - }, - setJustSignedUpEmail: (email: string) => { - set(s => { - s.justSignedUpEmail = email - }) - }, - } - return { - ...initialStore, - dispatch, - } -}) diff --git a/shared/stores/store-registry.tsx b/shared/stores/store-registry.tsx index fef27d3ecefc..1636bdd86945 100644 --- a/shared/stores/store-registry.tsx +++ b/shared/stores/store-registry.tsx @@ -15,7 +15,6 @@ import type { } from '@/stores/recover-password' import type {State as SettingsEmailState, useSettingsEmailState} from '@/stores/settings-email' import type {State as SettingsPhoneState, useSettingsPhoneState} from '@/stores/settings-phone' -import type {State as SignupState, useSignupState} from '@/stores/signup' import type {State as TeamsState, useTeamsState} from '@/stores/teams' import type {State as TrackerState, useTrackerState} from '@/stores/tracker' import type {State as UsersState, useUsersState} from '@/stores/users' @@ -30,7 +29,6 @@ type StoreName = | 'recover-password' | 'settings-email' | 'settings-phone' - | 'signup' | 'teams' | 'tracker' | 'users' @@ -45,7 +43,6 @@ type StoreStates = { 'recover-password': RecoverPasswordState 'settings-email': SettingsEmailState 'settings-phone': SettingsPhoneState - signup: SignupState teams: TeamsState tracker: TrackerState users: UsersState @@ -61,7 +58,6 @@ type StoreHooks = { 'recover-password': typeof useRecoverPasswordState 'settings-email': typeof useSettingsEmailState 'settings-phone': typeof useSettingsPhoneState - signup: typeof useSignupState teams: typeof useTeamsState tracker: typeof useTrackerState users: typeof useUsersState @@ -107,10 +103,6 @@ class StoreRegistry { const {useSettingsPhoneState} = require('@/stores/settings-phone') return useSettingsPhoneState } - case 'signup': { - const {useSignupState} = require('@/stores/signup') - return useSignupState - } case 'teams': { const {useTeamsState} = require('@/stores/teams') return useTeamsState diff --git a/shared/stores/tests/signup.test.ts b/shared/stores/tests/signup.test.ts index 1c5b73f575ca..48770703774e 100644 --- a/shared/stores/tests/signup.test.ts +++ b/shared/stores/tests/signup.test.ts @@ -1,41 +1,38 @@ /// import * as S from '@/constants/strings' -import {resetAllStores} from '@/util/zustand' - -import {useSignupState} from '../signup' +import {clearSignupEmail, getSignupEmail, setSignupEmail} from '@/people/signup-email' +import {clearSignupDeviceNameDraft, getSignupDeviceNameDraft, setSignupDeviceNameDraft} from '@/signup/device-name-draft' afterEach(() => { jest.restoreAllMocks() - resetAllStores() + clearSignupEmail() + clearSignupDeviceNameDraft() }) -test('setDevicename stages the selected signup device name', () => { - useSignupState.getState().dispatch.setDevicename('Phone 2') +test('device name draft stages the selected signup device name', () => { + setSignupDeviceNameDraft('Phone 2') - expect(useSignupState.getState().devicename).toBe('Phone 2') + expect(getSignupDeviceNameDraft()).toBe('Phone 2') }) -test('email verification notifications clear the staged signup email', () => { - useSignupState.getState().dispatch.setJustSignedUpEmail('alice@example.com') - expect(useSignupState.getState().justSignedUpEmail).toBe('alice@example.com') - - useSignupState - .getState() - .dispatch.onEngineIncomingImpl({type: 'keybase.1.NotifyEmailAddress.emailAddressVerified'} as any) +test('device name draft clears back to the default', () => { + setSignupDeviceNameDraft('Phone 2') + clearSignupDeviceNameDraft() - expect(useSignupState.getState().justSignedUpEmail).toBe('') + expect(getSignupDeviceNameDraft()).toBe(S.defaultDevicename) }) -test('resetState clears staged signup values back to defaults', () => { - useSignupState.setState(s => ({ - ...s, - devicename: 'Phone 2', - justSignedUpEmail: 'alice@example.com', - })) +test('signup email helper stores and clears the pending welcome email', () => { + setSignupEmail('alice@example.com') + expect(getSignupEmail()).toBe('alice@example.com') + + clearSignupEmail() + + expect(getSignupEmail()).toBe('') +}) - useSignupState.getState().dispatch.resetState() +test('signup email helper can stage the no-email sentinel', () => { + setSignupEmail(S.noEmail) - const state = useSignupState.getState() - expect(state.devicename).toBe(S.defaultDevicename) - expect(state.justSignedUpEmail).toBe('') + expect(getSignupEmail()).toBe(S.noEmail) }) diff --git a/skill/zustand-store-pruning/references/store-checklist.md b/skill/zustand-store-pruning/references/store-checklist.md index ba1946709611..6c133d0d5da5 100644 --- a/skill/zustand-store-pruning/references/store-checklist.md +++ b/skill/zustand-store-pruning/references/store-checklist.md @@ -34,7 +34,7 @@ Status: - [x] `settings-notifications` removed the store; notifications screens and chat settings now own refresh/toggle RPC state in a feature-local hook while preserving the existing settings load path - [x] `settings-password` kept only `randomPW` in store; moved submit/load flows into settings screens - [x] `settings-phone` kept notification-backed `phones` and `addedPhone`; moved add/verify/default-country flow into local hooks and route params -- [ ] `signup` +- [x] `signup` removed the store; People now owns the one-shot welcome banner helper and signup keeps device-name draft state in feature-local helpers - [ ] `team-building` - [ ] `tracker` - [x] `unlock-folders` removed dead phase/device state; kept only engine callback forwarding into `config`