Skip to content

feat(backup): recurring backup health reminders and Backup Settings redesign - #2453

Open
BullishNode wants to merge 13 commits into
SatoshiPortal:developfrom
BullishNode:feat/backup-health-reminders
Open

feat(backup): recurring backup health reminders and Backup Settings redesign#2453
BullishNode wants to merge 13 commits into
SatoshiPortal:developfrom
BullishNode:feat/backup-health-reminders

Conversation

@BullishNode

@BullishNode BullishNode commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Recurring backup-health reminders, plus the Backup Settings screen redesign they belong to. Rebased onto current develop.

The whole feature answers one question — "if this phone vanished right now, could you get your money back?" — and gives each state exactly one verb: nothing backed up ⇒ back up; encrypted vault only ⇒ add a physical backup; physical backup done ⇒ occasionally confirm it still works.

Reminder engine

State per wallet is three fields: last_tested, last_acknowledged, crossed_ten_million_sats.

Posture Reminder
Nothing backed up No popup — owned by the existing every-launch warning and the screen hero
Encrypted vault only Every 90 days: add a physical backup
Physical backup done (± vault) Every 365 days since last test: test your backup
Any posture, first time balance ≥ 10,000,000 sats One popup, once per wallet lifetime
  • Fires on app open, wallet home only; silent during onboarding.
  • Dismiss snoozes a full cycle and never writes a tested timestamp — the screen keeps telling the truth ("Last tested 14 months ago") while the popup is quiet.
  • last_tested resets only on completing a verification; creating a backup counts as day-zero tested (creation requires a test).
  • One popup = one question = one primary action.
  • Balance = total of wallets whose keys are on the device (mainnet, locally signing, not hardware) + Ark. Read lazily at evaluation time; never watched.

Simplified relative to the first version of this PR

Deleted: balance tiers and their persisted state, tier-driven escalation, the pending-action bookkeeping, and the both-verified "review your backups" nag (it had no real action). There is deliberately no PIN / vault-freshness reminder: a vault-only user's real exposure is having a single recovery path that depends on someone else, and the fix for that is a physical backup — not testing a PIN.

Behaviour fix

The schedule anchor was latest(vault, physical), so a user with a fresh vault and a two-year-old physical test was never reminded. The anchor now follows the thing being urged. Regression-tested.

Backup Settings screen

  • Status rows — Physical Backup and Encrypted Vault, tested/not tested, with "Last tested …" underneath.
  • Hero — one card naming the single most useful action, or nothing at all when the rows already say everything: urgent Back up your wallet when nothing is backed up, Add a physical backup for vault-only, Test your backup when a physical test is stale, and no hero when it is fresh.
  • Start backup is a button, not a menu row — creating a backup is an action, not a setting — filled in primary red, present in every state except the one where the hero already offers it.
  • Menu — "Recoverbull" is now "Encrypted vault settings", matching the status row's vocabulary.
  • The encrypted vault option card now also carries a "Uses Tor" tag, since reaching the key server over Tor is worth knowing before choosing that option.

Encrypted vault flow

  • The permission gate is gone (−763 lines). It was not an OS permission and gated nothing: tapping Continue wrote a local boolean whose only effect was whether the screen showed again. A user who has chosen the encrypted vault does not need a consent screen for the thing they just chose.
  • The custom-server warning moved to the moment of choice. Instead of disclosing the server to every user on every vault flow, saving a custom key server URL now requires confirming a warning. Reverting to the default needs no confirmation; URL validation runs first.
  • User-facing "Recoverbull" is retired as a feature name throughout, while recoverbull.com (a real domain) and "RecoverBull Server" (the server's actual product name, which a superuser configuring a URL needs) are kept.

Validation

  • flutter analyze --fatal-warnings --fatal-infos: no issues
  • repo-wide dart format --set-exit-if-changed: 0 changed
  • dart fix --dry-run: nothing to fix
  • ARB validation: 27/27 locales
  • Full suite: 860 passed, plus bull_ui 17 and bull_ui_catalogue 1

Notes for the reviewer

  • Please squash on merge. The first commit briefly adds ~50 lines of fork-specific feature graph to FEATURES.md which the second removes; the final tree is clean, but squashing keeps that out of upstream history entirely.
  • isPermissionGranted is now dead weight: a NOT NULL column baked into drift schemas v10–v14, written but never read. Dropping it needs a schema bump and migration on live data, so it is deliberately left for a future schema change rather than done here.
  • Retired strings were deleted from all locales and re-added English-only, so a stale translation cannot resurface the old wording; strings whose wording did not change kept their existing translations.

@BullishNode

Copy link
Copy Markdown
Contributor Author

@thibistaken experimental idea, but thouroughly reviewed by AI

The engine asked one question in several voices and tracked state it did
not need. It now answers "could you get your money back?" with one verb
per posture.

Delete the balance-tier machinery (BackupBalanceTier, the 1,000,000-sat
tier, highestHandledBalanceTier) and the pending-action bookkeeping. The
record is now the acknowledgement time plus a one-time milestone flag;
keys written by the earlier shape are ignored rather than rejected, so no
migration is needed for an unreleased feature.

Make the cadence posture-dependent: 90 days for a vault-only wallet,
365 for one that already holds words. Anchor the schedule on the clock of
the thing being urged rather than on the most recent backup of any kind —
a both-backups wallet with a fresh vault and a two-year-old physical test
was never reminded, which was the whole point of the reminder.

Fire the 10,000,000-sat notice at most once per wallet, inclusive of the
threshold, retired by either dismissing it or acting on it. Merge the
both-backups posture into "test your backup" and drop the "review your
backups" nag, which named no action. There is deliberately no vault or
PIN freshness reminder: the fix for a single server-dependent recovery
path is a physical backup, not a PIN rehearsal.

The popup now asks one question and offers one action, with a
low-emphasis dismissal that snoozes a full cycle and never records a test
the app did not observe.
The screen listed every backup action at equal weight, so the one that
mattered was the reader's problem to find. It now answers the same
question the reminder does, in three parts: the status rows state the
facts, one hero names the single most useful action, and the menu holds
everything else.

The hero is urgent when nothing is backed up (and replaces the Start
Backup row), asks a vault-only wallet for a physical backup, asks for a
test once the physical one is over a year old, and is absent when the
backup is fresh — no empty card, no heading over nothing. It derives its
posture from the same domain code the reminder uses, so the two surfaces
cannot disagree, but it reads the real tested dates rather than the
snooze-aware verdict: the screen keeps saying "last tested 14 months ago"
while the popup is quiet. It stays silent until the first load resolves,
because every wallet looks unprotected before then.

Rename the Recoverbull menu row to "Encrypted vault settings" to match
the status row's vocabulary. The key is deleted and re-added rather than
renamed: all 26 non-EN values still said "Recoverbull", and falling back
to English reads better than a translation of retired vocabulary.

Compose the three sections as separate builders so the fork can add its
metadata backup status row and menu row without touching hero logic.
The Backup Settings menu row now reads "Encrypted vault settings", but the
page it opened was still titled "Recoverbull Settings". Bring the six
strings that use Recoverbull as a feature name in line with the vocabulary
the user already sees.

Each key is deleted and re-added English-only. The identifiers keep their
recoverbull* names — renaming them is churn with no user benefit — but the
26 non-EN entries are dropped so those locales fall back to English rather
than rendering a translation of the retired wording.

Left alone deliberately: the three "Visit recoverbull.com" strings and the
four that name the RecoverBull server. That is the server's real name, and
a superuser typing its URL needs to recognise it.

recoverbullLearnMore reads "Learn more at recoverbull.com" rather than
naming the vault: both of its usages open recoverbull.com, and one of them
sits on the server-confirmation page where the subject is the server.
@BullishNode
BullishNode force-pushed the feat/backup-health-reminders branch from 855593d to b18dae2 Compare July 27, 2026 19:43
@BullishNode
BullishNode marked this pull request as draft July 27, 2026 19:43
The page asked the user to permit contacting the key server for the vault
they had just chosen to create. It gated nothing: allowPermission wrote a
local boolean whose only effect was whether the page appeared again.

So the vault flows now go straight to their first real step. The navigator
builds its page directly — settings to SettingsPage, everything else to
ConnectingPage — with no permission fetch and no loading state while it
resolves. Tor initialisation and the server check, previously dispatched
only once permission had been granted, now always run.

Deleting the page frees six strings that described the server rather than
asking for anything. recoverbullContinue stays; password_input_page still
uses it.

The isPermissionGranted column stays. It is part of drift schemas v10
through v14 with a migration step behind it, so dropping it would mean a
new schema version rather than abandoning a value; unread and still
written as false on insert, it is inert. Users who already granted
permission notice nothing: the flag is simply never read again.
Moving the Start Backup CTA into the hero made it disappear exactly when
the hero does — so a user whose physical backup was fresh had no way to
deliberately add an encrypted vault from this screen.

Availability is not encouragement. We still never nag for a second backup
once a physical one is tested, but the row is back in the menu, in every
state, with its original label, icon and destination. In the zero-backup
state it sits below the urgent hero, which offers the same action more
prominently; that repetition is cheaper than a rule about when the row is
allowed to exist.

The label keeps its 26 existing translations rather than coming back
English-only.
Removing the permission page took the custom-server warning with it. Put
it back where it is actually useful: at the moment the user chooses a
server, rather than on a screen they walk past afterwards.

Saving a URL that is neither the default nor the server already in use now
opens the shared warning sheet, and the URL is stored only if the user
confirms. Dismissing stores nothing and drops back to showing the server
in use, so nobody can end up pointed at someone else's key server without
having read the warning. Returning to the default is the safe direction
and passes straight through.

Validation still runs first: a malformed URL is rejected before any
warning, so the user is never asked to confirm something the app is about
to refuse.
With nothing backed up, the hero already renders START BACKUP as its
primary action, so the identical menu row a few pixels below it was pure
noise. The row is suppressed in that one state and stays in every other
one, where the hero offers a different action or none at all.
- start-backup becomes an outlined button between the hero and the
  settings list instead of a menu row: creating a backup is an action,
  not a setting, and it should read as one even when a backup exists.
  It keeps the translated label (27 locales) and is still suppressed in
  the zero-backup state, where the hero already offers it.
- BackupOptionCard takes a list of tags so a card can carry more than
  one; the encrypted vault card now also shows 'Uses Tor', since
  reaching the key server over Tor is a property worth knowing before
  choosing that option.
It is the screen's main action whenever no hero is claiming that role,
so it wears the app's primary fill rather than an outline.
settingsRecoverbullTitle, recoverbullRecoveryTitle and
recoverbullVaultRecoveryTitle have no reader in lib, test or
integration_test. Two of them had also come out of the vocabulary rename
holding the same string, which is the duplicate-literal smell the repo
warns about — resolved here by removing both rather than picking one.
@BullishNode

Copy link
Copy Markdown
Contributor Author

@ethicnology I have removed forcing the user to "accept" the default recoverbull server but I've added a "uses Tor" label on the encrypted backup description card. Also, if user adds custom recoverbull server, there is still a little warning.

@BullishNode
BullishNode force-pushed the feat/backup-health-reminders branch from 3b54451 to 9393bef Compare July 27, 2026 23:22
BackupOptionCard grew from one optional tag to a list so the encrypted
vault card could say both how easy it is and that it uses Tor, but nothing
verified either the list or the Wrap that carries it.

Covers all three cardinalities and both layouts: two tags sit side by side
when the card is wide, and the second moves to its own row when it is not.
That last case is the whole reason for the Wrap — with a Row it would
overflow instead, and the test would fail.
@BullishNode
BullishNode marked this pull request as ready for review July 28, 2026 06:32
@BullishNode BullishNode changed the title feat(backup): add recurring backup health reminders feat(backup): recurring backup health reminders and Backup Settings redesign Jul 28, 2026
@BullishNode

Copy link
Copy Markdown
Contributor Author
Screenshot_1785195196 Screenshot_1785195156 Screenshot_1785195138 Screenshot_1785195122 Screenshot_1785193839

some screenshots

BullishNode pushed a commit to BullishNode/bullbitcoin-mobile that referenced this pull request Jul 28, 2026
Brings SatoshiPortal#2453 (13 commits) into the Get Paid
stack. The screen is a merge, not a copy: the fork already wraps a second
cubit and renders the inline "Bull backup" metadata card, so the upstream
skeleton is rebuilt around both.

The reminder engine is new to the fork. It answers one question — if this
phone vanished right now, could you get your money back? — and each posture
gets exactly one verb:

  - vault only, no physical: every 90 days, "add a physical backup"
  - physical done (± vault): every 365 days since the last test
  - a one-time popup the first time the device-key balance is observed at or
    above 10,000,000 sats
  - zero backup makes NO decision here; that state stays owned by the
    existing every-launch backup warning overlay

Each cadence is anchored on the clock of the thing being urged, so a fresh
vault write cannot silence a stale physical backup. Dismissing snoozes the
popup for a full cycle and never touches last_tested, so the screen keeps
telling the truth while the popup is quiet. There is deliberately no PIN or
vault-freshness reminder and no both-verified "review" nag: neither has an
action behind it.

The screen becomes status rows -> posture hero -> Start Backup button ->
menu rows, each section built by its own function so the fork's metadata
insertions land without disturbing the others. Start Backup is a button
rather than a settings row, and it is suppressed only in the zero-backup
state where the hero already offers it.

Recoverbull: the vault permission gate is gone (nothing else in the fork
consumed FetchPermission/AllowPermission), a custom key server URL now warns
before it is saved, and user-facing "Recoverbull" vocabulary is retired in
favour of "encrypted vault" — while keeping the recoverbull.com domain
strings and the four server-product strings.

Localization follows the fork rule: retired wording is deleted from every
locale and re-added EN-only so stale translations cannot resurface;
unchanged wording keeps its translations. 9 keys deleted, 4 reworded,
21 added.
@i5hi

i5hi commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

@claude please add your review

@claude

claude Bot commented Jul 28, 2026

Copy link
Copy Markdown

Claude finished @i5hi's task in 5m 53s —— View job


Review: recurring backup health reminders + Backup Settings redesign

  • Gather context (diff vs origin/develop; no CLAUDE.md in repo)
  • Review reminder engine (evaluate/acknowledge usecases, repository, model)
  • Review presentation layer (cubit, overlay, backup settings screen)
  • Review flow refactors (onboarding, test_wallet_backup, permission-gate removal)
  • Review tests, l10n, and docs
  • Post review feedback

Overall: this is a well-built PR and I'd be comfortable merging it after considering the findings below (none are blockers, though #1 and #4 are worth fixing before merge). The domain layer is genuinely nice: injected clocks everywhere, isBackupReminderDue treating a missing anchor and a backwards clock as "due" (fail toward reminding), a versioned SharedPreferences record that degrades to an empty record instead of wedging, and no secrets in reminder state. The test suite covers the exact boundaries that matter (90/365-day edges, the anchor regression, inclusive milestone threshold, once-per-lifetime guarantee). CI ("Analyze and Test") is green.

One change deserves explicit praise: moving the encrypted-backup timestamp out of CreateEncryptedVaultUsecase (where it was written before the vault was ever uploaded or the key stored) into CompleteEncryptedVaultBackupUsecase at the end of the flow. That was a real lie in the old data — a failed upload still stamped "backed up".

Findings

1. RecoverBullFlowNavigator dispatches bloc events inside build()lib/features/recoverbull/flow.dart:24-25

context.read<RecoverBullBloc>().add(const OnTorInitialization());
context.read<RecoverBullBloc>().add(const OnServerCheck());

Every rebuild of this widget (theme/locale change, MediaQuery change, parent rebuild) re-fires Tor init and the server check. The old FutureBuilder version had the same smell but this refactor makes it unconditional. Move both into initState (post-frame callback since they need context.read). Fix this →

2. BackupHealthReminderCubit: a background evaluate can stomp an in-flight savelib/features/backup_settings/presentation/cubit/backup_health_reminder_cubit.dart:53-57

evaluate() is triggered by didUpdateWidget whenever WalletBloc emits (a balance sync, for example). If the user has just tapped "Not now" (isSaving: true, buttons disabled), a concurrent evaluate emits a fresh BackupHealthReminderVisible with isSaving: false — re-enabling the buttons while the acknowledge is still in flight, and clearing any shown failure. Low severity (a double acknowledge is harmless), but a one-line guard closes it: skip the emit when state is BackupHealthReminderVisible && state.isSaving. Fix this →

3. Vault creation reports failure after the backup has actually succeededlib/features/recoverbull/presentation/bloc.dart:327-333

In _onSaveVault, if CompleteEncryptedVaultBackupUsecase fails after the vault file is saved and the key is stored on the server, the bloc emits VaultStatusPersistenceFailure and returns without emitting vault:. The user sees a failed flow and will likely retry — creating a second vault — even though their backup fully succeeded. Since the only thing that failed is a local timestamp write, consider treating it as non-fatal (log it, still emit success), or wording the failure so it's clear the backup itself is safe.

4. Verify-mnemonic screen dead-ends after a persistence failurelib/features/test_wallet_backup/ui/screens/verify_mnemonic_screen.dart:41-63

Verification auto-fires when the last word is placed. If CompletePhysicalBackupVerificationUsecase returns Err (DB write fails), the snackbar shows and ClearError clears the failure — but the words stay fully placed, and none of the listenWhen transitions can re-dispatch VerifyPhysicalBackup. The user has no retry path except leaving the screen. Consider resetting the word order on persistence failure (like the wrong-order path does) or adding an explicit retry. Fix this →

5. Custom-server warning comparison is string-fragilelib/features/recoverbull/ui/pages/settings_page.dart:65-67

entered != SettingsConstants.recoverbullUrl && entered != _originalUrl compares raw text against a Uri.toString(). A trailing slash or case difference (http://x.onion vs http://x.onion/) makes the warning fire or not fire inconsistently. Normalize both sides through Uri.parse(...).toString() before comparing. (The failure direction is at least safe — ambiguity produces an extra warning, not a missing one.)

Nits / notes

  • timeago output isn't localized — "Last tested {when}" wraps an English-only timeago.format() for all 27 locales (backup_settings_screen.dart:213, backup_health_reminder_overlay.dart:161). Existing repo usage has the same gap, so this just extends it, but here the English fragment sits inside a translated sentence, which makes the mix visible.
  • Docs drifted from the final screendocs/backup-health-reminders.md:92-96 describes "status rows → at most one hero → menu" and says the zero-backup hero is the only way into Start Backup; the final screen (commits 97f2af0dd/e620825ac) also renders the standalone Start Backup button in every non-zero-backup state.
  • The reminder blocker is fully modalPopScope(canPop: false) swallows Android back entirely and the overlay covers the whole shell including bottom nav. "Not now" is one tap away so this is fine, but it's a deliberate interruption on app open — confirming that's the intended strength.
  • _hero in backup_settings_screen.dart:145 uses DateTime.now() directly where the engine uses an injected clock — fine for a widget, just noting the asymmetry.

Design decisions I checked and agree with

  • The anchor-follows-the-urged-thing fix is correct and regression-tested (a fresh vault does not excuse a stale physical backup).
  • Dismiss-never-writes-tested keeps the settings screen honest — good separation of "quiet" from "healthy".
  • Removing the permission gate: verified it gated nothing but its own re-display; the isPermissionGranted column left in drift schemas is acknowledged in the PR body.
  • The milestone firing before the schedule check, and being retired on either dismiss or act, matches the docs and tests.

@i5hi

i5hi commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Second review pass (Claude, multi-angle deep run — posted by @i5hi)

This complements the automated review above rather than repeating it: everything below is new. Agreements with the earlier review are listed at the end. Same overall verdict — well-built PR, strong domain tests, honest PR description (every claim in it verified against the code) — but the popup lifecycle around the evaluate/acknowledge loop has real gaps.

Findings (most severe first)

1. "Not now" cannot silence a future-dated anchor — the blocking popup refires after every wallet sync.
lib/features/backup_settings/domain/usecases/evaluate_backup_health_reminder_usecase.dart:89-97 + domain/backup_health_reminder.dart:50-52
isBackupReminderDue returns due when anchor.isAfter(now) (the fail-toward-reminding rule the earlier review praised), and the anchor is latest(urgedTestedAt, lastAcknowledgedAt). A latestPhysicalBackup stamped while the device clock was fast (then corrected) outvotes any acknowledge forever: ack writes now, _latest still picks the future tested date, still due. Every WalletBloc emit re-evaluates via didUpdateWidget, so the canPop:false modal reappears seconds after each "Not now" until real time passes the stored timestamp. Fail-toward-reminding is right; ack-can't-snooze is not — consider anchor = max(anchor, ack) clamped to now, or treating a future anchor as now at evaluation time.

2. The 10M-sats milestone popup flaps around the threshold.
lib/features/backup_settings/presentation/cubit/backup_health_reminder_cubit.dart:51-57
The latch persists only on user interaction, and a re-evaluation that lands below threshold emits BackupHealthReminderHidden unconditionally — yanking a visible popup mid-read. Concrete: 9,999,000 confirmed + 2,000 unconfirmed → popup; the unconfirmed tx is RBF-replaced → next sync hides it; re-broadcast → it's back. Once shown, either latch on display or don't withdraw a visible one-time notice on a balance dip.

3. Vault-only users get two contradictory blocking popups in one launch.
lib/features/recoverbull/domain/complete_encrypted_vault_backup_usecase.dart (called with only defaultWallet.id) + wallet_state.hasNoBackup()
Vault creation flags only the default bitcoin wallet; the liquid default keeps both flags false, so hasNoBackup() stays true and the every-launch "no backup" modal still shows (pre-existing quirk — only the vault test flow flags both defaults). Dismissing it then lets the new 90-day vault-only reminder fire immediately after: "you have no backup" followed by "add a physical backup to your vault", breaking this PR's own one-popup-one-question rule.

4. Upgrade path: a custom key server saved before this PR never re-confirms.
lib/features/recoverbull/ui/pages/settings_page.dart:64-72
The old gate re-fired on the next vault flow after every URL change (store() reset isPermissionGranted on save — it still writes it, nothing reads it). The new warning fires only for entered != default && entered != _originalUrl, i.e. new saves. A device upgrading with a pre-existing custom .onion sends its next vault key there with zero disclosure anywhere in the flow. New saves are airtight (single caller, validation first, dismiss stores nothing) — this is purely the migration case, and it's privacy-sensitive enough to deserve an explicit "intended" or a one-time re-confirmation for already-custom URLs.

5. fetch() converts IO failures to Ok(blank record) — and acknowledge writes the blank back.
lib/features/backup_settings/data/shared_preferences_backup_health_reminder_repository.dart:35-61
Corrupt JSON → default record is a fine read-time tradeoff. But acknowledge is fetch → copyWith → save, so one transient SharedPreferences read failure during an ack durably persists crossedTenMillionSats: false over a latched true — breaking the once-per-lifetime invariant the tests pin. Distinguish decode failure (default) from IO failure (Err); acknowledge's Err branch is currently dead code because fetch never errors.

6. Usecase calls usecase in onboarding.
lib/features/onboarding/domain/usecases/create_onboarding_wallets_usecase.dart:16-27
CreateOnboardingWalletsUsecase wraps core's CreateDefaultWalletsUsecase — orchestration belongs in the bloc, and the sanctioned wrap target is a repo/facade (the sibling CompletePhysicalBackupVerificationUsecase wrapping TestWalletBackupFacade in this same PR does it right).

7. The due-ness rule executes inside a widget build().
lib/features/backup_settings/ui/screens/backup_settings_screen.dart:126-175
_hero derives the posture and calls isBackupReminderDue(now: DateTime.now(), ...) in UI, bypassing cubit and usecase — the 90/365 policy now runs in two layers that can drift, and the wall-clock read is why the widget tests have to keep wide margins from the boundaries. Belongs on the state via the cubit. (The earlier review noted the DateTime.now() asymmetry as fine; the layering duplication is the sharper cost.)

8. The blocking-overlay shell is now copy-pasted three times (nit).
backup_health_reminder_overlay.dart:100-125 duplicates the Positioned.fill > PopScope > Material > ... > rounded-top sheet chrome of backup_warning_overlay.dart:50-77 and the legacy-storage overlay byte-for-byte. One extracted shell in core/widgets stops them drifting.

Agreements with the earlier review

The events-dispatched-in-build() finding (its #1), the evaluate-stomps-in-flight-save race (#2, same mechanism as this list's popup lifecycle theme), the backup-succeeded-but-reported-failed path (#3), the verify-mnemonic retry dead-end (#4), and the URL-normalization fragility (#5) all verified as real here too — no need to restate them.

Verified clean (so nobody re-checks)

Schedule tests would genuinely fail on the pre-fix anchor code (checked against the PR's first commit); no drift schema change needed and none made; all 27 locale files parse, zero references to the 9 deleted keys anywhere, all 26 new keys have descriptions; isPermissionGranted has zero readers in lib/ exactly as the body says; all router references to moved/deleted screens resolve.

BullishNode pushed a commit to BullishNode/bullbitcoin-mobile that referenced this pull request Jul 28, 2026
Brings SatoshiPortal#2453 (13 commits) into the Get Paid
stack. The screen is a merge, not a copy: the fork already wraps a second
cubit and renders the inline "Bull backup" metadata card, so the upstream
skeleton is rebuilt around both.

The reminder engine is new to the fork. It answers one question — if this
phone vanished right now, could you get your money back? — and each posture
gets exactly one verb:

  - vault only, no physical: every 90 days, "add a physical backup"
  - physical done (± vault): every 365 days since the last test
  - a one-time popup the first time the device-key balance is observed at or
    above 10,000,000 sats
  - zero backup makes NO decision here; that state stays owned by the
    existing every-launch backup warning overlay

Each cadence is anchored on the clock of the thing being urged, so a fresh
vault write cannot silence a stale physical backup. Dismissing snoozes the
popup for a full cycle and never touches last_tested, so the screen keeps
telling the truth while the popup is quiet. There is deliberately no PIN or
vault-freshness reminder and no both-verified "review" nag: neither has an
action behind it.

The screen becomes status rows -> posture hero -> Start Backup button ->
menu rows, each section built by its own function so the fork's metadata
insertions land without disturbing the others. Start Backup is a button
rather than a settings row, and it is suppressed only in the zero-backup
state where the hero already offers it.

Recoverbull: the vault permission gate is gone (nothing else in the fork
consumed FetchPermission/AllowPermission), a custom key server URL now warns
before it is saved, and user-facing "Recoverbull" vocabulary is retired in
favour of "encrypted vault" — while keeping the recoverbull.com domain
strings and the four server-product strings.

Localization follows the fork rule: retired wording is deleted from every
locale and re-added EN-only so stale translations cannot resurface;
unchanged wording keeps its translations. 9 keys deleted, 4 reworded,
21 added.
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.

3 participants