Skip to content

fix(receive): Default account modal re-appears on every "Receive" tap — choice not persisted (regression since 2.4.46) #3811

@pretyflaco

Description

@pretyflaco

Description

On release 2.4.50, the "Select default account" modal (Bitcoin vs Dollar/Stablesats) pops up every time the user taps "Receive", even after the user has already made a selection. The user's choice is not being persisted across sessions.

This is a regression — the modal should only appear once (after the user has >= 1 transaction) and remember the choice permanently.

Steps to Reproduce

  1. Open Blink on v2.4.50 (custodial account, Mainnet, with at least 1 transaction)
  2. Tap Receive
  3. The "Select default account" modal appears — choose Bitcoin or Dollar
  4. Go back to home screen
  5. Tap Receive again
  6. The modal appears again, despite having already chosen

Expected Behavior

The modal should appear once. After the user selects a default account, the choice is persisted and the modal does not reappear.

Actual Behavior

The modal reappears on every "Receive" tap. The persisted flag hasPromptedSetDefaultAccount is not being restored from the Apollo cache on app startup.

Affected Versions

  • Regression introduced in: 2.4.46
  • Still present in: 2.4.50 (latest)

Root Cause Analysis

The default account choice is persisted via a client-only Apollo cache field hasPromptedSetDefaultAccount, which is written to disk by apollo3-cache-persist. The trigger condition is in app/screens/home-screen/home-screen.tsx:411:

if (
  !isSelfCustodial && target === "receiveBitcoin" && !hasPromptedSetDefaultAccount && // <-- reads false when cache not restored
  numberOfTxs >= 1 && galoyInstanceId === "Main"
) {
  toggleSetDefaultAccountModal()
  return
}

Regressing commit

Commit c276cf1ee8 — PR #3769 ("feat(spark): UI/UX launch polish", 2026-05-20) changed cache restore in app/graphql/client.tsx:296-300 from unconditional to token-gated:

- await persistor.restore()
+ // Skip restore in self-custodial mode so the persisted custodial cache cannot leak.
+ if (token) {
+   await persistor.restore()
+ }

The token now comes from useEffectiveAuthToken() which resolves asynchronously. On cold start it transiently returns "" while the async account-type check (listSelfCustodialAccounts()) is in flight. If the Apollo client effect runs during this window, token is falsy and persistor.restore() is skipped — so hasPromptedSetDefaultAccount is never loaded back into the in-memory cache and defaults to false.

Key files | File | Relevance |

|------|-----------|
| app/graphql/client.tsx:296-312 | Token-gated persistor.restore() — the regression |
| app/graphql/hooks/use-effective-auth-token.ts | Async token resolution (transiently returns "") |
| app/screens/home-screen/home-screen.tsx:402-420 | Modal trigger condition |
| app/components/set-default-account-modal/set-default-account-modal.tsx | Modal component + persist write |
| app/graphql/client-only-query.ts:196-208 | setHasPromptedSetDefaultAccount() writer |
| app/graphql/cache.ts:107-109 | Cache read policy (defaults to false) |

Suggested Fix Direction

Ensure persistor.restore() runs for custodial sessions regardless of token-resolution timing. Options:

  1. Distinguish "still resolving" from "self-custodial (no token)" in useEffectiveAuthToken() — e.g., return a tri-state (loading | token | null) — and only skip restore when explicitly in self-custodial mode, not during the loading phase.
  2. Restore unconditionally, and instead purge/namespace the cache by account type when entering self-custodial mode.
  3. Await token resolution before building the client, ensuring token is accurate by the time the restore decision is made.

/cc @grimen @esaugomez31 @ihaveadifferentname

Metadata

Metadata

Assignees

Labels

mobileMobile app related

Type

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions