fix: persist default-account modal flag across sessions #3818
Conversation
…nd account switches
There was a problem hiding this comment.
Review — bugs + test coverage
Critical
None.
Important
-
I1 — Add the upgrade tradeoff to the PR description.
app/store/persistent-state/state-migrations.ts:106-107
Users who already dismissed the modal under the Apollo mechanism will see it one more time after updating — the old flag lived in Apollo cache and is structurally unavailable tomigrate12ToCurrent. One-time, then it sticks. QA should expect it. -
I2 — Backdrop/close-icon dismiss does not mark the flag — pre-existing, unchanged.
app/components/set-default-account-modal/set-default-account-modal.tsx:150-159
Only the BTC/USD selection handlers mark it; dismissing via backdrop re-prompts later. Identical onmain, so not a regression — recorded so the semantics are a decision, not an accident.
Design
SOLID violations
- None.
Incomplete adapter pattern
- D1 —
resolveAccountKeycopy-pasted; the per-account pattern's 5th use has no shared key primitive.
app/store/persistent-state/theme-preference.ts:7-8+app/store/persistent-state/default-account-modal-shown.ts:5-6
Character-for-character identical (activeAccountId ?? DefaultAccountId.Custodial), and it encodes the write-key-must-equal-read-key invariant this PR depends on — one definition means it can't silently diverge between features. Extract to a sharedapp/store/persistent-state/account-key.tsbefore the 6th copy. Keepself-custodial-language.ts:5-9's resolver separate — different semantics (excludes custodial).
High cyclomatic complexity
- None.
Code smells
- D2 — Duplicated
markDefaultAccountModalShown(); toggleModal()sequence in both handlers.
set-default-account-modal.tsx:80,103
ExtractcloseAsAcknowledged()— dropping the mark from one branch silently reintroduces the bug for that choice path.
Naming smells
- D3 —
markDefaultAccountModalShownnames two different things.
app/store/persistent-state/default-account-modal-shown.ts:13+app/hooks/use-default-account-modal-shown.ts:23
A pure transformer(state) => statein the store and a void imperative callback returned by the hook — the hook imports one and exports the other under the identical name, so auto-import will pick the wrong one. Also breaks the siblingwith*convention for pure transformers (withThemePreference,withSelfCustodialLanguage). Rename the store fn towithDefaultAccountModalShown— fixes both. UseDefaultAccountModalShownReturnvs siblings'EffectiveThemeReturn/EffectiveLanguageReturn(noUseprefix). Cosmetic.- Stale spec title at
__tests__/store/persistent-state/state-migrations.spec.ts:113— says "preserves schema 11 …" over aschemaVersion: 12input. Rename.
Test coverage
Tier 1 — production code with no test file
app/hooks/use-default-account-modal-shown.ts(25 LOC, new) — contains the only non-trivial wiring in the feature: the functional updaterupdateState((prev) => prev && mark(prev)). Exact copy-able precedent exists:__tests__/hooks/use-effective-language.spec.ts:158-166(mock context → capture updater frommock.calls[0][0]→ apply to fakeprev→ assert).
Tier 2 — tests that pass while a regression could ship invisibly
__tests__/store/persistent-state/state-migrations.spec.ts— all 18 changed lines are mechanical12→13find-replace;defaultAccountModalShownByAccountIdandthemeByAccountIdappear nowhere in the spec. No behavioral spec for the new v12→v13 step or the v13 identity — a future migration step that forgets to spread a field ships invisibly.__tests__/store/persistent-state/default-account-modal-shown.spec.ts— optional gaps: explicitactiveAccountId === DefaultAccountId.Custodialcase; empty-string accountId ("" ?? xkeeps""). Low priority.
Tier 3 — recommended regression tests per finding
- Hook updater semantics (→ Tier 1) —
__tests__/hooks/use-default-account-modal-shown.spec.ts: assertmarkDefaultAccountModalShowncallsupdateStatewith a functional updater (regressing to a direct-value update would clobber concurrent writes); apply the captured updater to a fakeprevand assert the resulting map; assert the updater returns falsy (no write) whenprevis undefined. - Migration field preservation (→ Tier 2) —
"migrates a v12 state to v13 preserving themeByAccountId and self-custodial maps";"v13 identity migration preserves defaultAccountModalShownByAccountId untouched". - Both modal handlers mark the flag (→ D2) — mock the hook in a
set-default-account-modalcomponent spec and assertmarkDefaultAccountModalShownfires on both the keep-current and set-new paths — or skip the spec and rely on thecloseAsAcknowledged()extraction (one of the two suffices).
…re fn to withDefaultAccountModalShown
…very close path marks the flag
8eac43b to
0db18c8
Compare
ImportantI1 — add the upgrade tradeoff to the PR description I2 — backdrop/close-icon dismiss does not mark the flag (pre-existing) DesignD1 — D2 — duplicated D3 —
Test coverageTier 1 — Tier 2 — migration field preservation Tier 3 — per-finding regression |

Summary
Fixes #3811 — the "Select default account" modal re-appearing on every Receive tap.
The flag tracking whether the modal has already been shown lived in the Apollo client-only cache (
hasPromptedSetDefaultAccount). Its restore was token-gated in PR #3769 anduseEffectiveAuthToken()resolves asynchronously, so on cold start the restore is often skipped and the flag defaults tofalse— the modal re-appears.This PR moves the flag out of Apollo cache into
persistentState, keyed byaccountId. Same persistence pattern used by PR #3795 (theme preference).Why persistentState
PersistentStateProviderdefaultAccountModalShownByAccountId[id]), which also fixes the secondary issue where the choice was lost on account switchesChanges
New
app/store/persistent-state/default-account-modal-shown.ts—getDefaultAccountModalShown/markDefaultAccountModalShownapp/hooks/use-default-account-modal-shown.ts—useDefaultAccountModalShown__tests__/store/persistent-state/default-account-modal-shown.spec.ts— 9 specs covering per-account isolation, fallback, immutabilitySchema migration
PersistentState_13addsdefaultAccountModalShownByAccountId?: Record<string, boolean>migrate12ToCurrent→ bumps to 13,migrate13ToCurrentis identityRemoved (Apollo client-only field)
hasPromptedSetDefaultAccountfromlocal-schema.gqlcache.tssetHasPromptedSetDefaultAccountwriter + query fromclient-only-query.tsgenerated.tsConsumers migrated
home-screen.tsx— readsdefaultAccountModalShownfrom the new hookset-default-account-modal.tsx— callsmarkDefaultAccountModalShown()instead of writing the Apollo cache