Skip to content

feat(#141): migrate backup_confirmed from SharedPreferences to the Rust identity record - #266

Open
codaMW wants to merge 3 commits into
MostroP2P:mainfrom
codaMW:feat/141-backup-confirmed-identity
Open

feat(#141): migrate backup_confirmed from SharedPreferences to the Rust identity record#266
codaMW wants to merge 3 commits into
MostroP2P:mainfrom
codaMW:feat/141-backup-confirmed-identity

Conversation

@codaMW

@codaMW codaMW commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Closes #141. Moves the backup-confirmed flag out of Dart SharedPreferences into the Rust identity record, per Principle I (Rust core, Flutter shell). grunch verified the issue is still valid on 2026-07-16.

Scope

Migrates only the security-relevant backup_confirmed flag (the backupCompleted state). The reminder-scheduling state (active / dismissed / snoozed) stays in Dart it's UI concern, not identity state.

Rust

  • backup_confirmed added to IdentityInfo, serialized into the existing identity JSON blob. #[serde(default)] so identities persisted before this field deserialize as false (unconfirmed -> reminder stays armed). No schema migration the identity is a JSON blob, not columns.
  • get_backup_confirmed / set_backup_confirmed / reset_backup_confirmation in identity.rs. set persists via save_identity, mirroring the trade_key_index durability pattern from Restore: resync trade_key_index to the max recovered index (prevents trade-key reuse) #217 but best-effort, not required: a lost flag only re-arms the reminder (safe), unlike a lost key index (which causes CantDo). On the web IndexedDB backend (save_identity is a stub) the flag simply doesn't persist, which fails safe.
  • create_identity / import_from_nsec construct with backup_confirmed: false a fresh mnemonic is by definition not backed up, which re-arms the reminder for a new identity (grunch's stated concern). load_identity_from_mnemonic restores the flag from the persisted blob via a pure restore_backup_confirmed helper, guarded on the public key so a leftover blob from another mnemonic can't leak its state.

Semantic choice worth a look

Importing a mnemonic does not auto-confirm the backup typing recovery words isn't the in-app verification ritual so an imported identity with no persisted flag stays unconfirmed. Happy to change if you'd prefer import-implies-backed-up.

Dart

  • BackupCompletedNotifier reads/writes through the bridge, with a one-time copy of the legacy SharedPreferences value into Rust (guarded by a migration marker) and a fallback to false when the bridge is unavailable.
  • The three bridge calls are injectable (constructor params defaulting to the real identity_api functions) so the notifier is testable without a live Rust runtime the seam pattern feat(bond): recognize pay-bond-invoice and add taker bond payment flow #213 established.

Tests

  • Rust: restore_backup_confirmed unit tests (same-identity read, default-false, cross-identity guard), a serde-default deserialization test, and a SQLite save/load round-trip asserting the flag persists.
  • Dart: migration, read, markCompleted, and reset exercised through fake bridge functions.
  • cargo test (239) / clippy -D warnings / cargo check --target wasm32 clean; flutter analyze clean; account tests pass.

Note: pre-existing escrow_mode_dev_card_test failures reproduce on clean main (unrelated to this change).

Summary by CodeRabbit

  • New Features

    • Backup completion status is now securely stored with identity data.
    • Backup status is restored when the matching identity is loaded.
    • Added support for marking backup completion and resetting the status.
  • Bug Fixes

    • Existing backup completion settings are migrated automatically.
    • Legacy or mismatched identity records now default to unconfirmed status.
    • Storage failures no longer incorrectly report backup completion.

…to the Rust identity record

The backup-confirmed flag lived in Dart SharedPreferences, violating
Principle I (Rust core, Flutter shell). Move the security-relevant state into
the Rust identity record, keeping the reminder-scheduling state (active /
dismissed / snoozed) in Dart since that is UI concern.

Rust:
- Add backup_confirmed to IdentityInfo, serialized into the existing identity
  JSON blob. #[serde(default)] so identities persisted before this field load
  as false (unconfirmed → reminder stays armed); no schema migration.
- get/set/reset_backup_confirmation in identity.rs. set persists via
  save_identity, mirroring the trade_key_index durability pattern (MostroP2P#217) — but
  best-effort, not required: a lost flag only re-arms the reminder (safe),
  unlike a lost key index. On the web IndexedDB backend (save_identity is a
  stub) the flag simply does not persist, which fails safe.
- create_identity / import_from_nsec construct with backup_confirmed: false; a
  fresh mnemonic is by definition not backed up (this re-arms the reminder for
  a new identity). load_identity_from_mnemonic restores the flag from the
  persisted blob via a pure restore_backup_confirmed helper — guarded on the
  public key so a leftover blof from another mnemonic cannot leak its state.
- Semantic choice worth review: importing a mnemonic does NOT auto-confirm the
  backup — typing recovery words is not the in-app verification ritual — so an
  imported identity with no persisted flag stays unconfirmed.

Dart:
- BackupCompletedNotifier reads/writes through the bridge, with a one-time copy
  of the legacy SharedPreferences value into Rust (guarded by a migration
  marker) and a fallback to false when the bridge is unavailable.
- The three bridge calls are injectable (constructor params defaulting to the
  real identity_api functions) so the notifier is testable without a live Rust
  runtime — the seam pattern MostroP2P#213 established.

Tests: restore_backup_confirmed unit tests (same-identity read, default-false,
cross-identity guard), a serde-default deserialization test, and a SQLite
save/load round-trip asserting the flag persists. Dart tests exercise the
migration, read, markCompleted and reset through fake bridge functions.

Verification: cargo test (239) / clippy / wasm check clean; flutter analyze
clean; account tests pass. (Pre-existing, unrelated escrow_mode_dev_card test
failures reproduce on clean main.)
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@codaMW, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 47 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f0aa5fa7-e906-4c2a-9ad7-902d552edd08

📥 Commits

Reviewing files that changed from the base of the PR and between 6cb67f7 and aa4b0b3.

📒 Files selected for processing (1)
  • test/features/account/backup_reminder_provider_test.dart

Walkthrough

Backup confirmation now persists in Rust identity state. The Dart notifier performs a one-time legacy migration, then uses Rust bridge APIs for loading, completion, and reset. Identity loading validates the stored public key before restoring the flag.

Changes

Backup confirmation persistence

Layer / File(s) Summary
Identity state and restoration
rust/src/api/types.rs, rust/src/api/identity.rs, rust/src/db/sqlite.rs
IdentityInfo stores backup_confirmed with a false default. Identity creation, loading, nsec imports, restoration, and persistence tests handle the field.
Rust APIs and bridge wiring
rust/src/api/identity.rs, rust/src/frb_generated.rs
Rust exposes get, set, and reset operations. Generated bridge handlers dispatch these APIs and serialize backup_confirmed.
Dart notifier migration and validation
lib/features/account/providers/backup_reminder_provider.dart, test/features/account/*
The notifier performs one-time SharedPreferences migration, then uses Rust callbacks for state changes. Tests cover migration, loading, completion, reset, and screen overrides.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BackupCompletedNotifier
  participant SharedPreferences
  participant RustIdentityAPI
  participant IdentityStorage
  BackupCompletedNotifier->>SharedPreferences: Read legacy completion value
  BackupCompletedNotifier->>RustIdentityAPI: Migrate or get confirmation
  RustIdentityAPI->>IdentityStorage: Read or persist identity flag
  IdentityStorage-->>RustIdentityAPI: Return confirmation state
  RustIdentityAPI-->>BackupCompletedNotifier: Return confirmation state
  BackupCompletedNotifier->>RustIdentityAPI: Set or reset confirmation
  RustIdentityAPI->>IdentityStorage: Persist updated flag
Loading

Possibly related PRs

  • MostroP2P/app#224: Modifies the shared backup ritual flow and its tests, but handles verification failure behavior and localization.

Poem

A rabbit marks the backup true,
Rust keeps the record safe and new.
Old preferences move once through,
The bridge handles what actions do.
Confirm or reset, the state stays right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements the APIs and notifier migration, but it uses serialized identity JSON instead of the issue-required SQLite and IndexedDB backup_confirmed column. Add backup_confirmed persistence to the SQLite and IndexedDB identity storage backends, or update issue #141 to approve serialized identity JSON storage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes migrating backup_confirmed from SharedPreferences to Rust identity storage.
Out of Scope Changes check ✅ Passed The changes support the backup_confirmed migration, including Rust APIs, bridge bindings, notifier updates, and related tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/src/api/identity.rs`:
- Around line 314-325: Update set_backup_confirmed so it clones the identity
record, applies the new backup_confirmed value to the clone, and persists the
clone before assigning it to state.identity_info; only commit the in-memory
change after save_identity succeeds. Preserve the direct assignment path when no
database exists, and add a test that forces save_identity to fail and verifies
the flag remains unconfirmed.

In `@test/features/account/backup_reminder_provider_test.dart`:
- Around line 173-190: Update the markCompleted() and reset() tests to retain
access to the fake bridge backing value and assert its effect directly:
markCompleted() must set it to true, while reset() must set it to false. Keep
the existing notifier state assertions and use the test’s existing fake bridge
setup rather than introducing unrelated changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3e727952-ff62-4aaa-9c83-5ab2d8f62142

📥 Commits

Reviewing files that changed from the base of the PR and between a149b8f and ab84bf4.

📒 Files selected for processing (7)
  • lib/features/account/providers/backup_reminder_provider.dart
  • rust/src/api/identity.rs
  • rust/src/api/types.rs
  • rust/src/db/sqlite.rs
  • rust/src/frb_generated.rs
  • test/features/account/backup_reminder_provider_test.dart
  • test/features/account/backup_ritual_screen_test.dart

Comment thread rust/src/api/identity.rs
Comment thread test/features/account/backup_reminder_provider_test.dart
codaMW added 2 commits August 2, 2026 21:32
…memory (CodeRabbit)

set_backup_confirmed mutated state.identity_info before save_identity could
fail. On a persistence failure the session would report a confirmed backup that
never reached disk, and the no-op short-circuit would stop a retry from
re-saving — so the flag silently vanished on restart. Build the updated record,
persist it, and assign to state only after the save succeeds (the persist-then-
commit discipline from MostroP2P#217).

Also strengthen the Dart markCompleted/reset tests to assert the fake bridge's
backing value changed, not only that notifier.state flipped.
…fake bridge (CodeRabbit)

The markCompleted/reset tests asserted only notifier.state, which would pass
even if the bridge write regressed. Hold the fake's backing value and assert it
flips to true/false.
@codaMW

codaMW commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

Two clarifications on the linked-issue check:

Backend persistence (SQLite + IndexedDB): identity is stored as a single JSON blob (identity (id, data)), not columns, so backup_confirmed serializes into that blob for both backends automatically no per-backend column needed. The SQLite round-trip test asserts it persists; IndexedDB uses the same serialized struct.
reset_backup_confirmation() on new identity: there's no generate_new_user() the function is create_identity(), which constructs the identity with backup_confirmed: false inline (that is the reset a fresh mnemonic is unconfirmed). The standalone reset_backup_confirmation() exists for other callers.

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.

Migrate backup_confirmed from SharedPreferences (Dart) to identity table (Rust)

1 participant