Skip to content

feat(self-custodial): UI/UX launch polish across settings and flows#3769

Open
esaugomez31 wants to merge 65 commits intofeat--spark-rollout-and-hardeningfrom
feat--self-custodial-launch-polish
Open

feat(self-custodial): UI/UX launch polish across settings and flows#3769
esaugomez31 wants to merge 65 commits intofeat--spark-rollout-and-hardeningfrom
feat--self-custodial-launch-polish

Conversation

@esaugomez31
Copy link
Copy Markdown
Collaborator

@esaugomez31 esaugomez31 commented Apr 29, 2026

Summary

UI/UX launch polish for the self-custodial (Spark) experience: stable-balance gating, settings reorganization, account redesign, delete flow, default-account preference, receive flow QR by default, single-screen backup phrase, dynamic success messaging, stablecoins-aware Dollar modal, removal of the legacy "You Are in Control" trust-model modal, country-aware account-type gating, a comprehensive multi-account stack with auto-convert UX polish, and SDK amount-adjust block on the convert-details screen.

Primary ticket: blinkbitcoin/blink-wip#692 — feat(spark/ui): Review and fixes. Also addresses blinkbitcoin/blink-wip#664 (Stablesats/Dollar popup modal — non-custodial variant).

Ticket blinkbitcoin/blink-wip#692 — Tasks

  • 1. Hide Stable Balance Setting (Feature Flag)stableBalanceEnabled remote flag (default false) gates isStableBalanceActive. Stable-balance settings hidden when off.
  • 2. Change Stable Balance Default Flag to False — new wallets no longer auto-activate stable balance on creation.
  • 3. Move "Backup Phrase" to "Security & Privacy" — relocated from Advanced.
  • 4. Hide "Advanced" Section for Non-Custodial Users — Advanced group hidden when active wallet is SC; empty SettingsGroup no longer renders.
  • 5. Remove Modal with Alert in Transfer Flow (First Time Only) — first-time convert modal dropped; unused i18n removed.
  • 6. Remove orange text from Review Transfer screen — the silent SDK amount-adjust path is gone; the convert-details screen now blocks the Review-transfer button when the SDK would auto-adjust the requested amount and surfaces an inline error explaining the user must use the full balance. The confirm screen no longer shows orange adjustment text.
  • 7. Hide "Bitcoin Deposit Address" from "Ways to Get Paid" — row hidden for all users; unused i18n removed.
  • 8. Add "Delete Account and Data" Button in Your Account Section — Account screen bifurcated for SC. SC layout matches the Figma: avatar + LN address + "Non-custodial" subtitle, then Wallet identifier / Lightning address / Backup status boxed read-only fields (shared with the AccountInformation sub-screen via SelfCustodialAccountFields), then the danger zone with the delete flow (balance check + warning screen + SDK storage wipe + activeAccountId clear). Custodial layout untouched.
  • 9. Remove "You Are in Control" Modal + Add Test CoverageTrustModelModal, useTrustModelSeen hook, trust-model-screen re-export, and the four BackupNudge.trustModel* i18n keys deleted across 28 locales. Home-screen integration removed; regression test asserts the modal no longer renders even for SC users with positive balance (the previous trigger condition).
  • 10. Backup Phrase — Show All Mnemonics on One Screen — new SparkViewBackupPhraseScreen renders all 12 words in a 2-column grid. Success screen accepts a dynamic message route param ("Your backup phrase is correct" for the test-backup flow). Onboarding 2-step flow untouched.
  • 11. IP-Based Country MappinguseAccountTypeOptions hides the Custodial card when the detected country isn't in CUSTODIAL_ALLOWED_COUNTRIES, and the SC card when nonCustodialEnabled is off. Loading skeleton while detection runs; banner explains the state when SC is temporarily disabled.
  • 12. Add LN Address QR by Default (Custodial Parity) — Receive defaults to PayCode/LN-address QR when on Bitcoin mode and an LN address exists, mirroring custodial behavior.

Ticket blinkbitcoin/blink-wip#664 — Stablesats/Dollar popup modal

  • Non-custodial variantStableSatsModal accepts a variant prop. SC variant renders close-X, Dollar pill, centered title, centered body explaining stablecoins, primary "Back home", secondary "Learn more" → https://www.blink.sv/en/dollar-account. Custodial variant unchanged.

Additional work (beyond ticket scope)

Account & default preferences

  • New persistent state selfCustodialDefaultWalletCurrency (BTC | USD), scoped per account; settings picker; honored in send and receive flows; currency list always loaded so SC users can pick.
  • Multi-account profile row shows the user's Lightning address as the title for SC accounts.
  • Account Information and Transaction Limits rows removed from non-custodial settings (data already lives on the new SC Account screen; Transaction Limits is custodial-only).
  • "Dollar (Stablecoin)" label on the SC Default account picker via a dedicated dollarStablecoin i18n key. Custodial keeps "Dollar (Stablesats)".

Multi-account preferences (display currency + language)

  • When the user has multiple self-custodial accounts, display currency and language are now persisted per account. Switching between SC accounts reflects both preferences instantly across Home, Settings, and the send/transfer flows — previously the change either lagged up to 5 minutes (waiting on the price poll) or required a full app reload.
  • Persistent state schema bumped from v10 to v11 with two additive optional fields (selfCustodialDisplayCurrencyByAccountId, selfCustodialLanguageByAccountId); migration is identity.
  • New useEffective* adapter hooks (useEffectiveDisplayCurrency, useEffectiveLanguage) unify custodial (GraphQL-backed) and self-custodial (persistent-state-backed) sources behind a single interface. Screens, LanguageSync, and usePriceConversion consume the adapter and never bifurcate by account type.
  • usePriceConversion is fixed at the root: displayCurrency now flows from the adapter rather than from Apollo's cached denominatorCurrency (which kept showing the previous account's currency on switch). A guard discards a cached price whose denominator disagrees with the active preference, and fetchPolicy: "cache-and-network" on both the authed and unauthed price queries forces an immediate refresh on account switches (custodial→custodial included).
  • Conversion fee no longer breaks for non-USD display currencies: ConvertQuote.feeAmount is typed as UsdMoneyAmount (the SDK returns the USDB swap fee in cents, pegged 1:1 to USD) and the conversion-flow hook converts it via convertMoneyAmount before formatting. Previously the fee row rendered "Currency issue. Refresh needed" for EUR, JPY, and any other non-USD locale.

Self-custodial contact dedup

  • bridge/addContact is now an upsert: when a contact with the same paymentIdentifier already exists (matched case-insensitive and trimmed via the existing normalizeString helper), it calls sdk.updateContact with the existing record's data — preserving any user-edited name (so a contact renamed "Mom" isn't clobbered back to its LN address) and bumping updatedAt for future "recent contacts" sorting. When no match exists, it falls through to sdk.addContact. Fixes the bug where every payment to the same LN address created another duplicate contact.

Account-type labels i18n alignment

  • The Custodial / Non-custodial labels shown under each account's title in the multi-account switcher were inconsistent across locales: the English source said Custodial / Non-custodial, but all 28 translation files said Blink / Spark (a brand-name swap, not a translation). All 28 locales were aligned to the English technical concept, with proper grammatical agreement per language (es: Custodiada / No custodiada; pt: Custodiada / Não custodiada; it: Custodito / Non custodito; de: Verwahrt / Selbstverwahrt; fr: Sous garde / Sans garde; and so on).

Convert flow

  • The convert-details screen runs a pre-flight SDK quote and blocks the Review-transfer button when the SDK would auto-adjust the requested amount (FlooredToMin or IncreasedToAvoidDust). For dust adjustments the block is suppressed when the user already requested the full balance, since asymmetric conversion math can flag it spuriously at 100%.
  • Inline error message ("Full balance has to be transferred.") shown inside the input area; the legacy orange "Amount increased…" text is removed from the Review Transfer screen.
  • Quote params passed as primitives so the memo stays referentially stable across renders, avoiding a setState/re-render loop in useConversionQuote.

Auto-convert UX

  • Inline converting state on the receive screen: spinner + "Please wait until the conversion is done" copy while BTC→USDB auto-convert runs. Per-invoice status tracked through a context provider so the screen reflects progress without polling.
  • Parallel pre-convert and post-send wallet syncs (~19s → ~9s end-to-end wait).

Multi-account hardening

  • Account registry made KeyStore-aware; stale-closure fix in setActiveAccountId.
  • Custodial token dropped from GraphQL requests when the active account is SC.
  • BackendFeatureGate returns blocked when the active account is SC, so feature gating respects the active account.
  • Backup nudge dismissal scoped per active account.
  • Restored wallets are marked as backed up using account-aware state.
  • Banner-triggered backup routes to the success screen instead of the test flow.
  • Dedup guard prevents duplicate wallet creation from concurrent useEffect runs.
  • create-wallet no longer clobbers the previously-active account's backup state.
  • Removing a self-custodial profile from the multi-account list now probes the account's balance via a short-lived SDK using its stored mnemonic (no active-account switch). When funds are present, the existing "has funds" warning modal opens instead of the confirm-removal modal. The X icon is replaced by a spinner while the probe runs.
  • Cloud backups now support multiple self-custodial wallets per Google Drive account. Each backup file is named blink-spark-backup-{network}-{walletIdentifier}.json so different wallets no longer overwrite each other. The payload now embeds walletIdentifier and (when set) lightningAddress as plaintext metadata; only the mnemonic stays encrypted. On restore, the app lists all backups by prefix, parses metadata without decrypting, and shows a picker when more than one is present (LN address as the row title with the identifier as subtitle when set, or the identifier alone). A single backup auto-restores; an encrypted backup proceeds straight to the password step.

Settings & UI polish for SC

  • POS and Printable static QR rows hidden in SC mode.
  • Login methods section hidden in SC.
  • Blink User fallback and empty Ways-to-get-paid groups hidden in SC.
  • Loading skeleton kept until the initial wallet sync completes.
  • Non-custodial subtitle restored in the multi-account profile row.
  • Pull-to-refresh on the Account screen no longer logs out SC users.
  • Confirm-account-removal modal title centered to match the warning modal layout (replaces the previous left-aligned wrap that broke the title visually).

Send flow

  • Self-custodial contacts surfaced in destination search.
  • Multi-account-aware delete-account confirm modal reused across flows; balance check warns before deleting an account that still has funds.

Performance

  • Transaction history rendering moved off the UI thread for SC wallets.

Out of scope

  • 13. Add custom LN Address (bonus) — listed as a bonus in blinkbitcoin/blink-wip#692, not part of this PR's scope.

Test plan

  • Enable / disable stableBalanceEnabled remotely; confirm stable-balance settings appear / disappear and that toggling off mid-session re-aligns Receive to Dollar.
  • Create a new SC wallet; confirm stable balance is not auto-activated.
  • Verify Backup Phrase row is in Security & Privacy and that Advanced is hidden for SC.
  • Open the Account screen as a SC user: confirm avatar + LN address + "Non-custodial" header, the three boxed fields (Wallet identifier, Lightning address, Backup status), copy buttons, and the danger zone with delete flow. With a custodial active account the layout stays as before.
  • Trigger Delete Account: with balance (blocked), without balance (warning → SDK storage wipe + activeAccountId cleared).
  • Set Default account = USD/BTC; confirm Send and Receive default to that currency.
  • On the SC Default account / Receive currency screen, the Dollar option label reads "Dollar (Stablecoin)"; on custodial it still reads "Dollar (Stablesats)".
  • In Receive (BTC mode, SC), the LN address QR is shown by default; toggling currency switches behavior.
  • Tap Settings → Backup phrase → see all 12 words on one screen → tap "Test your backup" → confirm the dynamic success message renders.
  • Tap the ? icon next to the Dollar pill on Home: in custodial → Stablesats modal; in SC → stablecoins modal with "Learn more" linking to blink.sv/en/dollar-account.
  • Open Home as a self-custodial user with positive balance; confirm the legacy "You Are in Control" trust-model modal never appears.
  • On the account-type selection screen, confirm the Custodial card hides when the detected country is restricted, and the SC card hides when nonCustodialEnabled is off (with the "temporarily disabled" banner).
  • Generate a USDB Lightning invoice and have it paid; confirm the spinner + "Please wait until the conversion is done" copy shows during the convert, then transitions to the paid state.
  • Switch between multiple accounts; confirm backup-nudge dismissals, default-currency preferences, and backed-up state stay scoped per account.
  • On the convert-details screen, type an amount that the SDK would auto-adjust (below minimum, or one that leaves dust); confirm the Review-transfer button is disabled and the inline error appears. Tap the 100% selector and confirm the block clears for the dust case.
  • On the multi-account list with a self-custodial profile that has a balance, tap the X button; confirm a spinner appears on the row while the balance is fetched, the warning modal opens with the correct balance, and no funds-less profile triggers the warning.
  • Back up wallet A, switch to wallet B and back it up; confirm Drive holds both files (blink-spark-backup-{network}-{pubkeyA}.json + {pubkeyB}.json) and neither overwrites the other.
  • Restore on a fresh device with multiple Drive backups; confirm the picker shows each entry with its LN address (or wallet identifier when no LN address) and that selecting one proceeds to either auto-restore or the password step depending on whether it was encrypted.
  • Restore with only one backup in Drive; confirm the picker is skipped and the flow goes straight to restore (or password if encrypted).
  • Restore with zero backups; confirm the existing "no backup found" UI still appears.
  • Open the confirm-removal modal (delete account from danger zone or from multi-account list with zero balance); confirm the title "Confirm account removal" is centered.
  • With two SC accounts holding different display currencies (e.g. EUR and USD), switch between them; Home, the Settings → Display currency row, and amount inputs must show the correct currency instantly.
  • With two SC accounts holding different languages, switch between them; the app locale must change immediately.
  • With two custodial accounts holding different display currencies, switch between them; the price must refresh instantly (previously it lagged the 5-minute poll).
  • On the "Confirm transfer" screen of the Convert flow with display currency ≠ USD (e.g. EUR), confirm the "Conversion fee" row shows the value converted to the active currency, not "Currency issue. Refresh needed".
  • Pay the same LN address three times in a row from a self-custodial account; open People → All Contacts and confirm a single entry appears (not three duplicates).
  • Rename a self-custodial contact to a custom alias, then make another payment to it; confirm the custom alias is preserved.
  • Open the multi-account switcher in Spanish, French, Italian, Portuguese, and German; confirm the label below each account name shows the correct translated equivalent of "Custodial" / "Non-custodial" (not "Blink" / "Spark").
  • Verify all 28 locales render the new copy without missing keys.

@esaugomez31 esaugomez31 changed the title feat(self-custodial): stop auto-activating stable balance on new wallet creation feat(self-custodial): launch polish Apr 29, 2026
@esaugomez31 esaugomez31 self-assigned this Apr 29, 2026
@esaugomez31 esaugomez31 changed the title feat(self-custodial): launch polish feat(self-custodial): UI/UX launch polish across settings and flows Apr 29, 2026
@designsats
Copy link
Copy Markdown
Contributor

designsats commented Apr 29, 2026

Additional small changes:

@esaugomez31

  1. Change "Stablesats" for "Stablecoin" on this screen
image
  1. Remove "Transaction limits" and "Account information" from non-custodial mode.
image
  1. Delete account button has wrong text,. Should be "Delete account and data"

  2. Im still able to delete a wallet with balance. Shouldnt be possible.

  3. "Add LN Address QR by Default (Custodial Parity)" - I dont see this done. I cant see LN address in "Ways to get paid" or under "Account"

@designsats
Copy link
Copy Markdown
Contributor

When restoring, there is no loading state for balance, instead I see $0.00 which is quite bad UX since I expect to see the full balance.

@esaugomez31

zero-balance-error.webm

Copy link
Copy Markdown
Contributor

@designsats designsats left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@esaugomez31 esaugomez31 force-pushed the feat--self-custodial-launch-polish branch from 18d14a7 to 17e5ae9 Compare April 30, 2026 07:05
@esaugomez31 esaugomez31 marked this pull request as ready for review April 30, 2026 08:02
@designsats
Copy link
Copy Markdown
Contributor

designsats commented Apr 30, 2026

@esaugomez31

Reviewing on android emulator shows these still dont work:

  • Transfer alert "Full balance has to be transferred"
  • everything related to LN address
  • display currency
  • language
  • wait with checkbox on receive screen until conversion successfuly happens and only update balance once it does complete
  • bulletin briefly flickers when creating new wallet
  • bulletin shows on other accounts if a new trial account was created, these bulletins have to be separated by account
  • lets drop the symbols requirement in cloud backup additional password, for the future: reconsider making password mandatory or using passkey
  • no new transaction notification (bonus)
  • lets write "Anon" instead of "Anonymous user"

@esaugomez31 esaugomez31 force-pushed the feat--self-custodial-launch-polish branch from 9c9bf69 to f8fc5f1 Compare May 1, 2026 01:59
@esaugomez31 esaugomez31 requested a review from grimen May 4, 2026 16:56
@designsats
Copy link
Copy Markdown
Contributor

designsats commented May 5, 2026

@esaugomez31
Found a bug. When user wants to login into custodial account, they cant unless they are in permited country.
Login (not Signup) has to be allowed.

Screenshot_1777989003

@esaugomez31 esaugomez31 force-pushed the feat--self-custodial-launch-polish branch from b958e5f to d93e65a Compare May 6, 2026 02:05
@esaugomez31
Copy link
Copy Markdown
Collaborator Author

@esaugomez31 Found a bug. When user wants to login into custodial account, they cant unless they are in permited country. Login (not Signup) has to be allowed.
Screenshot_1777989003

Already fixed!

esaugomez31 added 24 commits May 7, 2026 08:15
@esaugomez31 esaugomez31 force-pushed the feat--spark-rollout-and-hardening branch from 7bb7129 to d464d96 Compare May 7, 2026 15:54
@esaugomez31 esaugomez31 force-pushed the feat--self-custodial-launch-polish branch from 16e3919 to 5c7a7ea Compare May 7, 2026 15:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants