Skip to content

Refactor and navigation fix#720

Merged
praveenperera merged 19 commits intomasterfrom
refactor-and-navigation-fix
Apr 29, 2026
Merged

Refactor and navigation fix#720
praveenperera merged 19 commits intomasterfrom
refactor-and-navigation-fix

Conversation

@praveenperera
Copy link
Copy Markdown
Contributor

@praveenperera praveenperera commented Apr 28, 2026

Summary by CodeRabbit

  • New Features

    • Sidebar-aware navigation with cancellable delayed navigation to avoid race conditions.
  • Bug Fixes

    • Prevents stale/delayed navigations from executing.
    • Wallet selection failures now surface explicit alerts instead of failing silently.
    • Cloud backup now reports unsupported backup versions and improves passkey/authorization handling.
  • Refactor

    • Wallet selection and navigation flows centralized behind higher-level app APIs for consistency.
  • Documentation

    • Updated style guidance for blank-line spacing and comment placement.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Navigation is now generation-aware: AppManager coordinates navigation with a monotonic generation token to gate/cancel delayed sidebar and load-and-reset navigations. Many call sites were changed to use AppManager wallet-selection helpers that throw on failure. Rust cloud-backup and passkey handling received substantial refactors and safer error mapping.

Changes

Cohort / File(s) Summary
Core Navigation / AppManager (Android/iOS)
android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt, ios/Cove/AppManager.swift
Adds navigationGeneration and begin/capture APIs, pendingSidebarNavigationTask, and new methods: selectWalletOrThrow, selectLatestOrNewWallet, captureLoadAndResetGeneration, resetAfterLoadingIfCurrent, closeSidebarAndNavigate. Generation-aware gating/cancellation for delayed navigation and resets.
Load & Reset UI (Android/iOS)
android/.../RouteView.kt, android/.../navigation/CoveNavDisplay.kt, ios/Cove/LoadAndResetContainer.swift, ios/Cove/RouteView.swift
Load-and-reset composables now accept full Route.LoadAndReset, derive nextRoutes/loadingTimeMs from route, capture a generation token, and call resetAfterLoadingIfCurrent after the delay instead of unconditionally invoking rust reset.
Android Wallet Selection Call Sites
android/app/src/main/.../AuthManager.kt, MainActivity.kt, ScanManager.kt, flows/.../NewWalletSelectScreen.kt, cold_wallet/.../QrCodeImportScreen.kt, hot_wallet/.../HotWalletImportScreen.kt, flows/SelectedWalletFlow/.../SelectedWalletContainer.kt, flows/SettingsFlow/..., sidebar/SidebarView.kt
Replaces direct app.rust.selectWallet* and app.rust.selectLatestOrNewWallet() calls with app.selectWalletOrThrow / app.selectLatestOrNewWallet; updates error/alert handling and sidebar-close sequencing accordingly.
iOS Wallet Selection Call Sites
ios/Cove/AuthManager.swift, CoveMainView.swift, Flows/NewWalletFlow/*, Flows/SelectedWalletFlow/*, Flows/SendFlow/*, Flows/SettingsFlow/*, HomeScreens/ListWalletsScreen.swift, ScanManager.swift, Views/SidebarView.swift
Mirrors Android changes: call sites now use app.selectWalletOrThrow / app.selectLatestOrNewWallet; sidebar close/navigation flows moved to closeSidebarAndNavigate and are generation-aware.
Transactions UI Nullability & Formatting (Android)
android/app/src/main/java/org/bitcoinppl/cove/flows/SelectedWalletFlow/TransactionsCardView.kt, .../TransactionItem.kt, .../ConfirmedTransactionWidget.kt, .../UnconfirmedTransactionWidget.kt, .../UnsignedTransactionWidget.kt
Makes WalletManager/AppManager parameters non-null, removes nullable guards, centralizes amount formatting into formatAmountFor(...), updates click/navigation handlers to use non-null APIs, and introduces TransactionsPreviewShell for previews.
TopAppBar Color Helper (Android)
android/.../SelectedWalletScreen.kt, .../TransactionDetailsScreen.kt, .../SendFlowHardwareScreen.kt
Replaces centerAlignedTopAppBarColors with topAppBarColors while preserving explicit color values.
Sidebar/Navigation UX (iOS)
ios/Cove/Views/SidebarView.swift
Delegates sidebar-close + navigation to app.closeSidebarAndNavigate, removes ad-hoc animation/delay code, and simplifies navigation call paths.
UniFFI / Kotlin wrapper
android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt
Adds throwing dispatch (dispatchThrowing) and new AppAction variants for wallet selection; updates generated FFI variant ordinals and checksum expectations.
Rust: Cloud Backup, Passkey, and Error Handling
multiple rust/src/manager/cloud_backup_manager/*, rust/crates/cove-cspp/*, rust/crates/cove-device/src/keychain.rs, rust/src/keys.rs, rust/src/manager/pending_wallet_manager.rs, rust/src/qr_scanner.rs
Introduces typed master-key backup version, PasskeyAuthenticator/PasskeyMaterialAcquirer/NamespacePasskeyMatcher, threads blocked-on-authorization outcomes, replaces many unreachable!() panics with explicit error mapping, centralizes xprv extraction, adds keychain.load_cspp_credential_id, restructures verification and passkey auth flows, and adjusts re-exports/imports.
Cloud Backup Upload Behavior
rust/src/manager/cloud_backup_manager/wallets/upload.rs
Treats CloudBackupError::Compatibility(_) as non-retryable in upload-prep retry classification.
Docs / Style
AGENTS.md
Adds style rules about blank-line spacing between logical steps and positioning of explanatory comments.

Sequence Diagram

sequenceDiagram
    participant UI as UI Component
    participant AppMgr as AppManager
    participant Nav as Router
    participant Rust as Rust Backend

    UI->>AppMgr: selectWallet(id) / resetRoute(...)
    activate AppMgr
    AppMgr->>AppMgr: beginNavigationIntent() (inc generation)
    AppMgr->>Nav: record navigation intent / update router
    AppMgr->>Rust: call FFI select/reset
    deactivate AppMgr

    UI->>AppMgr: closeSidebarAndNavigate(route)
    activate AppMgr
    AppMgr->>AppMgr: cancel pendingSidebarNavigationTask
    AppMgr->>AppMgr: capture generation g0
    AppMgr->>AppMgr: sleep 200ms / loadingTimeMs
    AppMgr->>AppMgr: if current generation == g0 then
    alt generation matches
        AppMgr->>Rust: resetAfterLoading(nextRoutes)
        AppMgr->>Nav: navigate to nextRoutes
    else generation advanced
        AppMgr->>AppMgr: skip (stale)
    end
    deactivate AppMgr
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Normalize passkey blocking calls #680: large passkey/passkey-auth refactor touching verify/passkey_auth and wallets/passkey modules — closely related to the Rust passkey changes in this PR.
  • More iOS 26 fixes #463: touches navigation/load-and-reset code paths (AppManager / LoadAndResetContainer) and may overlap with generation-aware navigation APIs introduced here.
  • Simplify Onboarding #629: modifies UniFFI/FFI bindings and Kotlin wrapper generation that overlap with the cove.kt binding changes in this PR.

Suggested labels

android

Poem

🐰 I counted generations with a twitchy nose,
closed sleepy sidebars so no stale route goes,
I turned panics into polite error notes,
hopped through Rust, Swift, and Kotlin on my toes—
now routes behave and the app happily flows.

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is entirely empty, lacking any explanation of changes, testing information, or completion of the required template sections. Add a comprehensive description following the template: include a Summary section explaining the refactoring and navigation fix, document testing performed across platforms, and complete the provided checklist.
Docstring Coverage ⚠️ Warning Docstring coverage is 43.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is vague and generic, using non-descriptive terms like 'Refactor' that don't clearly convey the specific changes made in the changeset. Provide a more specific title that highlights the primary change, such as 'Coordinate navigation via generation counter for wallet selection' or 'Refactor wallet selection to use app-level APIs'.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor-and-navigation-fix

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 and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 28, 2026

Greptile Summary

This PR introduces a navigation-generation counter on both iOS and Android to prevent stale delayed sidebar navigations from executing after a newer navigation event has already been dispatched. On the Rust side, wallet selection is unified behind AppAction::SelectWallet / AppAction::SelectLatestOrNewWallet, dispatch now returns Result so callers see errors instead of silent panics, and the cloud backup restore flow is extended to recover wallets from all passkey-matching namespaces rather than just the first one.

Confidence Score: 5/5

Safe to merge — all findings are P2 dead-code clarity nits with no runtime impact.

No P0 or P1 issues found. The navigation generation pattern is correctly implemented on both platforms, error propagation from dispatch is handled at every call site, and the multi-namespace restore path has corresponding tests. Only two unreachable code branches were identified, both harmless.

rust/src/manager/cloud_backup_manager/ops.rs (unreachable else branch in enable flow)

Important Files Changed

Filename Overview
ios/Cove/AppManager.swift Adds navigation generation counter and sidebar-aware navigation helpers to prevent stale delayed navigations; popRoute guarded against empty routes; rust = FfiApp() dropped from reset() (safe, FfiApp is a stateless handle)
android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt Mirrors the iOS navigation generation pattern; adds high-level closeSidebarAnd* helpers that call *WithoutNavigationGeneration variants inside the delayed action to avoid double-generation-increment
ios/Cove/Views/SidebarView.swift Removed local goTo/navigateRoute helpers; all sidebar navigation now delegates to the new AppManager.closeSidebarAnd* API, removing the @Environment(.navigate) dependency
android/app/src/main/java/org/bitcoinppl/cove/sidebar/SidebarView.kt Replaced closeSidebarAndNavigate lambda calls with the new typed AppManager helpers; removed Route/RouteFactory/SettingsRoute imports from the view
ios/Cove/LoadAndResetContainer.swift Added route field and generation capture so the post-delay resetAfterLoading is skipped if a newer navigation has already occurred, preventing phantom navigations
rust/src/app.rs dispatch now returns Result<(), AppError>; SelectWallet/SelectLatestOrNewWallet actions added; select_wallet/select_latest_or_new_wallet moved to pub(crate) with proper error-discriminating return types
rust/src/manager/cloud_backup_manager/wallets/passkey.rs Refactored free functions into PasskeyMaterialAcquirer and NamespacePasskeyMatcher structs; NamespaceMatchOutcome::Matched now returns Vec (all matches) instead of the first match only
rust/src/manager/cloud_backup_manager/verify/passkey_auth.rs Refactored authenticate_with_policy into PasskeyAuthenticator struct; PrfUnsupportedProvider now surfaces as UnsupportedPasskeyProvider in stored-only and stored-then-discover paths; new test for map_discovery_error
rust/src/manager/cloud_backup_manager/ops.rs Restore flow now iterates all matching namespaces (multi-namespace restore); enable flow errors on >1 match; master-key/passkey persisted only after first successful wallet restore; contains one unreachable else branch
rust/src/manager/cloud_backup_manager/verify/session.rs repair_wrapper_from_local_key now returns RepairedMasterKeyResolution (removes the unreachable NeedsWrapperRepair arm); backup_version() checked via typed enum instead of raw integer comparison

Sequence Diagram

sequenceDiagram
    participant UI as SidebarView
    participant AM as AppManager
    participant Rust as FfiApp (Rust)

    UI->>AM: closeSidebarAndSelectWallet(id)
    AM->>AM: cancel pendingSidebarNavigationJob
    AM->>AM: advanceNavigationGeneration() → gen=N
    AM->>AM: isSidebarVisible = false
    AM->>AM: launch coroutine (delay 250ms)

    alt Another navigation fires before delay expires
        UI->>AM: pushRoute / popRoute / etc.
        AM->>AM: advanceNavigationGeneration() → gen=N+1
        Note over AM: delayed job wakes up
        AM->>AM: isNavigationGenerationCurrent(N)? → false → return
    else No intervening navigation
        Note over AM: delayed job wakes up
        AM->>AM: isNavigationGenerationCurrent(N)? → true
        AM->>Rust: dispatch(AppAction.SelectWallet(id))
        Rust-->>AM: Ok(()) or Err → logged
    end
Loading

Reviews (2): Last reviewed commit: "Handle unexpected wallet creation errors" | Re-trigger Greptile

Comment thread rust/src/qr_scanner.rs Outdated
Comment thread android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt
Comment thread rust/src/manager/pending_wallet_manager.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
ios/Cove/Flows/NewWalletFlow/NewWalletSelectScreen.swift (1)

169-177: ⚠️ Potential issue | 🟡 Minor

Set the success alert only after selection succeeds.

Line 174 queues .importedSuccessfully before the throwing call on Line 175. If selection fails, the local error alert is shown but the app-wide success alert is still pending, so users can get a contradictory success dialog afterward.

💡 Suggested change
-            app.alertState = TaggedItem(.importedSuccessfully)
             try app.selectWalletOrThrow(id)
+            app.alertState = TaggedItem(.importedSuccessfully)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/Cove/Flows/NewWalletFlow/NewWalletSelectScreen.swift` around lines 169 -
177, In newWalletFromXpub, the code sets app.alertState =
TaggedItem(.importedSuccessfully) before calling try
app.selectWalletOrThrow(id), which can leave a success alert queued even if
selection fails; move the app.alertState assignment so it runs only after try
app.selectWalletOrThrow(id) completes without throwing (i.e., call
Wallet.newFromXpub(xpub:), get id, call try app.selectWalletOrThrow(id), then
set app.alertState = TaggedItem(.importedSuccessfully)); keep the catch block
unchanged to present the local error AlertItem on failure.
android/app/src/main/java/org/bitcoinppl/cove/MainActivity.kt (1)

821-828: ⚠️ Potential issue | 🟡 Minor

Surface TapSigner wallet-selection failures to the user.

This branch dismisses the dialog before selectWalletOrThrow(...) and only logs on failure, so the user gets no feedback if selection fails. The duplicate-wallet path already raises UnableToSelectWallet; this one should stay consistent.

💡 Suggested change
                         } catch (e: Exception) {
                             Log.e("GlobalAlert", "Failed to select wallet", e)
+                            app.alertState = TaggedItem(AppAlertState.UnableToSelectWallet)
                         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/org/bitcoinppl/cove/MainActivity.kt` around lines
821 - 828, Move the onDismiss() call to after a successful wallet selection and
propagate failures to the UI instead of only logging: call
app.selectWalletOrThrow(state.walletId) inside the try block before calling
onDismiss() and app.resetRoute(Route.SelectedWallet(state.walletId)), and in the
catch block rethrow or surface the same UnableToSelectWallet error (or invoke
the same user-visible error path used by the duplicate-wallet branch) instead of
only Log.e in the TextButton onClick handler so the user sees the selection
failure.
ios/Cove/Flows/NewWalletFlow/ColdWallet/QrCodeImportScreen.swift (1)

154-169: ⚠️ Potential issue | 🟡 Minor

Don't queue .importedSuccessfully before the throwing select.

Line 159 sets the app-wide success alert before Line 160 can fail. If selection throws, the local error alert is shown, but the global success alert is still queued and can appear afterward on a later screen.

💡 Suggested change
-                    app.alertState = TaggedItem(.importedSuccessfully)
                     try app.selectWalletOrThrow(id)
+                    app.alertState = TaggedItem(.importedSuccessfully)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/Cove/Flows/NewWalletFlow/ColdWallet/QrCodeImportScreen.swift` around
lines 154 - 169, The code sets app.alertState =
TaggedItem(.importedSuccessfully) before calling app.selectWalletOrThrow(id),
which can throw and leave the success alert queued; change the flow in the
import completion block so you first compute let id = wallet.id(), then if let
onImported { onImported(id) } else try app.selectWalletOrThrow(id) and only
after a successful select set app.alertState =
TaggedItem(.importedSuccessfully); keep existing catch handling
(WalletError.MultiFormat, WalletError.WalletAlreadyExists ->
handleWalletAlreadyExists(id), and the generic catch that sets alert) unchanged
so the success alert is never queued if selectWalletOrThrow throws.
🧹 Nitpick comments (3)
rust/src/manager/cloud_backup_manager/verify/session.rs (1)

113-115: Add a regression test for this new retry branch.

This new fallback path is important; a focused test would prevent future regressions back to panic-style handling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/manager/cloud_backup_manager/verify/session.rs` around lines 113 -
115, Add a focused regression test that exercises the new
MasterKeyResolution::NeedsWrapperRepair branch to ensure it returns the
retry_result instead of panicking; specifically, create a test in the same
module that sets up a VerifySession (or the function that calls the match on
MasterKeyResolution in session.rs) and injects/mocks the master key resolution
to return MasterKeyResolution::NeedsWrapperRepair, then call the verify path and
assert the returned value equals the result of self.retry_result("cloud master
key wrapper still needs repair") (or that it is a Retry outcome) and that no
panic occurs; refer to the MasterKeyResolution enum, the retry_result method,
and the verify/session entrypoint to locate where to hook the mock.
android/app/src/main/java/org/bitcoinppl/cove/flows/SelectedWalletFlow/TransactionsCardView.kt (1)

301-325: Extract shared BTC/FIAT amount formatting to one helper.

Both branches now carry near-identical formatting code; centralizing it will reduce drift and make future display changes safer.

Refactor sketch
+private fun WalletManager.formatTxnAmounts(
+    fiatOrBtc: FiatOrBtc,
+    sentAndReceived: SentAndReceived,
+    fiatAmount: FiatAmount?,
+): Pair<String, String> {
+    val direction = sentAndReceived.direction()
+    val btc = rust.displaySentAndReceivedAmount(sentAndReceived)
+    val fiat = fiatAmount?.let { rust.displayFiatAmountWithDirection(it.amount, direction) } ?: "---"
+    return if (fiatOrBtc == FiatOrBtc.BTC) btc to fiat else fiat to btc
+}

Also applies to: 363-387

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/org/bitcoinppl/cove/flows/SelectedWalletFlow/TransactionsCardView.kt`
around lines 301 - 325, The BTC/FIAT formatting logic duplicated between
formattedAmount and secondaryAmount should be moved into a single helper to
avoid drift; create a private function (e.g., formatAmountFor(fiatOrBtc:
FiatOrBtc, txn: TransactionWrapper, direction: Direction, manager: Manager))
that encapsulates the when-switch and the null-check for txn.v1.fiatAmount(),
calling manager.rust.displaySentAndReceivedAmount(txn.v1.sentAndReceived()) for
BTC and manager.rust.displayFiatAmountWithDirection(fiatAmount.amount,
direction) or "---" when fiatAmount is null for FIAT, then replace both computed
properties formattedAmount and secondaryAmount to call this helper (also apply
the same refactor to the similar block referenced at lines 363-387).
android/app/src/main/java/org/bitcoinppl/cove/RouteView.kt (1)

77-98: Derive the reset payload from route inside the container.

Now that LoadAndResetContainer already takes the full Route.LoadAndReset, passing nextRoutes and loadingTimeMs separately gives this code two sources of truth for the same state. Computing them inside the container will make future edits harder to desync.

♻️ Suggested simplification
             is Route.LoadAndReset -> {
                 LoadAndResetContainer(
                     app = app,
-                    route = route,
-                    nextRoutes = route.resetTo.map { it.route() },
-                    loadingTimeMs = route.afterMillis.toLong(),
+                    route = route,
                 )
             }
@@
 private fun LoadAndResetContainer(
     app: AppManager,
     route: Route.LoadAndReset,
-    nextRoutes: List<Route>,
-    loadingTimeMs: Long,
 ) {
+    val nextRoutes = route.resetTo.map { it.route() }
+    val loadingTimeMs = route.afterMillis.toLong()
+
     // show loading indicator
     Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
         CircularProgressIndicator()
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/org/bitcoinppl/cove/RouteView.kt` around lines 77 -
98, The container currently accepts nextRoutes and loadingTimeMs separately
which duplicates state from the Route.LoadAndReset; change LoadAndResetContainer
to derive those values from the passed-in route instead: inside
LoadAndResetContainer compute nextRoutes = route.resetTo.map { it.route() } and
loadingTimeMs = route.afterMillis.toLong(), remove the nextRoutes and
loadingTimeMs parameters from the LoadAndResetContainer signature, and update
the caller in the Route.LoadAndReset branch to only pass app and route (remove
nextRoutes and loadingTimeMs there as well).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@android/app/src/main/java/org/bitcoinppl/cove/flows/NewWalletFlow/hot_wallet/HotWalletImportScreen.kt`:
- Around line 277-282: The code currently calls
app.selectWalletOrThrow(walletMetadata.id) before checking onImported and then
again in the fallback branch, causing duplicated side effects; change the flow
so selection happens exactly once: remove the pre-branch app.selectWalletOrThrow
call, invoke onImported?.invoke(walletMetadata.id) if present, otherwise call
app.resetRoute(Route.SelectedWallet(walletMetadata.id)), and then call
app.selectWalletOrThrow(walletMetadata.id) once after the branching (or place
the single select call into both branches identically) to match the
duplicate-wallet handling and avoid double selection; update references around
manager.importWallet, onImported, app.resetRoute, app.clearWalletManager
accordingly.

In `@ios/Cove/AppManager.swift`:
- Around line 261-279: closeSidebarAndNavigate spawns a delayed Task stored in
pendingSidebarNavigationTask which can run after reset(); ensure reset()
cancels/invalidates this delayed navigation by calling
pendingSidebarNavigationTask?.cancel() and setting pendingSidebarNavigationTask
= nil (or otherwise invalidating the navigation generation) inside the reset()
implementation. Update the AppManager.reset() method to perform that
cancellation and/or bump the navigation generation
(beginNavigationIntent/isNavigationGenerationCurrent) so any queued sidebar
navigation cannot resurrect stale routing after a full reset.

In `@ios/Cove/Flows/NewWalletFlow/HotWallet/HotWalletImportScreen.swift`:
- Around line 516-517: The code is swallowing selection errors by using `try?`
before calling `app.resetRoute(to: .selectedWallet(walletId))`; replace this
with proper error handling so the route is only reset when
`app.selectWalletOrThrow(walletId)` succeeds — for example, wrap the call in a
do-catch (or propagate the error) and in the do-block call `try
app.selectWalletOrThrow(walletId)` followed by `app.resetRoute(to:
.selectedWallet(walletId))`, and in the catch block handle or surface the error
instead of proceeding with navigation.

In `@ios/Cove/LoadAndResetContainer.swift`:
- Around line 17-25: Change the unkeyed SwiftUI task in LoadAndResetContainer to
a keyed task so it cancels when the route changes: replace the current .task
usage (which calls app.captureLoadAndResetGeneration() and later await
app.resetAfterLoadingIfCurrent(generation:route:nextRoute:)) with .task(id:
route) so the task is tied to the route value and will be cancelled/restarted on
route changes, preventing concurrent task instances for different routes.

In `@rust/src/qr_scanner.rs`:
- Line 349: The early return in the Self::Complete arm loses the cached state
because std::mem::replace(self, Self::Uninitialized) was already performed; fix
by restoring the scanner state before returning: after extracting result from
the match arm (the Self::Complete(result) branch), assign *self =
Self::Complete(result) (or avoid replacing until after matching) so the cached
completion is preserved instead of leaving self as Self::Uninitialized.

---

Outside diff comments:
In `@android/app/src/main/java/org/bitcoinppl/cove/MainActivity.kt`:
- Around line 821-828: Move the onDismiss() call to after a successful wallet
selection and propagate failures to the UI instead of only logging: call
app.selectWalletOrThrow(state.walletId) inside the try block before calling
onDismiss() and app.resetRoute(Route.SelectedWallet(state.walletId)), and in the
catch block rethrow or surface the same UnableToSelectWallet error (or invoke
the same user-visible error path used by the duplicate-wallet branch) instead of
only Log.e in the TextButton onClick handler so the user sees the selection
failure.

In `@ios/Cove/Flows/NewWalletFlow/ColdWallet/QrCodeImportScreen.swift`:
- Around line 154-169: The code sets app.alertState =
TaggedItem(.importedSuccessfully) before calling app.selectWalletOrThrow(id),
which can throw and leave the success alert queued; change the flow in the
import completion block so you first compute let id = wallet.id(), then if let
onImported { onImported(id) } else try app.selectWalletOrThrow(id) and only
after a successful select set app.alertState =
TaggedItem(.importedSuccessfully); keep existing catch handling
(WalletError.MultiFormat, WalletError.WalletAlreadyExists ->
handleWalletAlreadyExists(id), and the generic catch that sets alert) unchanged
so the success alert is never queued if selectWalletOrThrow throws.

In `@ios/Cove/Flows/NewWalletFlow/NewWalletSelectScreen.swift`:
- Around line 169-177: In newWalletFromXpub, the code sets app.alertState =
TaggedItem(.importedSuccessfully) before calling try
app.selectWalletOrThrow(id), which can leave a success alert queued even if
selection fails; move the app.alertState assignment so it runs only after try
app.selectWalletOrThrow(id) completes without throwing (i.e., call
Wallet.newFromXpub(xpub:), get id, call try app.selectWalletOrThrow(id), then
set app.alertState = TaggedItem(.importedSuccessfully)); keep the catch block
unchanged to present the local error AlertItem on failure.

---

Nitpick comments:
In
`@android/app/src/main/java/org/bitcoinppl/cove/flows/SelectedWalletFlow/TransactionsCardView.kt`:
- Around line 301-325: The BTC/FIAT formatting logic duplicated between
formattedAmount and secondaryAmount should be moved into a single helper to
avoid drift; create a private function (e.g., formatAmountFor(fiatOrBtc:
FiatOrBtc, txn: TransactionWrapper, direction: Direction, manager: Manager))
that encapsulates the when-switch and the null-check for txn.v1.fiatAmount(),
calling manager.rust.displaySentAndReceivedAmount(txn.v1.sentAndReceived()) for
BTC and manager.rust.displayFiatAmountWithDirection(fiatAmount.amount,
direction) or "---" when fiatAmount is null for FIAT, then replace both computed
properties formattedAmount and secondaryAmount to call this helper (also apply
the same refactor to the similar block referenced at lines 363-387).

In `@android/app/src/main/java/org/bitcoinppl/cove/RouteView.kt`:
- Around line 77-98: The container currently accepts nextRoutes and
loadingTimeMs separately which duplicates state from the Route.LoadAndReset;
change LoadAndResetContainer to derive those values from the passed-in route
instead: inside LoadAndResetContainer compute nextRoutes = route.resetTo.map {
it.route() } and loadingTimeMs = route.afterMillis.toLong(), remove the
nextRoutes and loadingTimeMs parameters from the LoadAndResetContainer
signature, and update the caller in the Route.LoadAndReset branch to only pass
app and route (remove nextRoutes and loadingTimeMs there as well).

In `@rust/src/manager/cloud_backup_manager/verify/session.rs`:
- Around line 113-115: Add a focused regression test that exercises the new
MasterKeyResolution::NeedsWrapperRepair branch to ensure it returns the
retry_result instead of panicking; specifically, create a test in the same
module that sets up a VerifySession (or the function that calls the match on
MasterKeyResolution in session.rs) and injects/mocks the master key resolution
to return MasterKeyResolution::NeedsWrapperRepair, then call the verify path and
assert the returned value equals the result of self.retry_result("cloud master
key wrapper still needs repair") (or that it is a Retry outcome) and that no
panic occurs; refer to the MasterKeyResolution enum, the retry_result method,
and the verify/session entrypoint to locate where to hook the mock.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 04010720-2b12-43e2-82ac-a726708a764e

📥 Commits

Reviewing files that changed from the base of the PR and between aab22f3 and e370b60.

📒 Files selected for processing (40)
  • android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt
  • android/app/src/main/java/org/bitcoinppl/cove/AuthManager.kt
  • android/app/src/main/java/org/bitcoinppl/cove/MainActivity.kt
  • android/app/src/main/java/org/bitcoinppl/cove/RouteView.kt
  • android/app/src/main/java/org/bitcoinppl/cove/ScanManager.kt
  • android/app/src/main/java/org/bitcoinppl/cove/flows/NewWalletFlow/NewWalletSelectScreen.kt
  • android/app/src/main/java/org/bitcoinppl/cove/flows/NewWalletFlow/cold_wallet/ColdWalletQrScanScreen.kt
  • android/app/src/main/java/org/bitcoinppl/cove/flows/NewWalletFlow/cold_wallet/QrCodeImportScreen.kt
  • android/app/src/main/java/org/bitcoinppl/cove/flows/NewWalletFlow/hot_wallet/HotWalletImportScreen.kt
  • android/app/src/main/java/org/bitcoinppl/cove/flows/SelectedWalletFlow/SelectedWalletContainer.kt
  • android/app/src/main/java/org/bitcoinppl/cove/flows/SelectedWalletFlow/SelectedWalletScreen.kt
  • android/app/src/main/java/org/bitcoinppl/cove/flows/SelectedWalletFlow/TransactionDetails/TransactionDetailsScreen.kt
  • android/app/src/main/java/org/bitcoinppl/cove/flows/SelectedWalletFlow/TransactionsCardView.kt
  • android/app/src/main/java/org/bitcoinppl/cove/flows/SendFlow/SendFlowHardwareScreen.kt
  • android/app/src/main/java/org/bitcoinppl/cove/flows/SettingsFlow/MainSettingsScreen.kt
  • android/app/src/main/java/org/bitcoinppl/cove/flows/SettingsFlow/NetworkSettingsScreen.kt
  • android/app/src/main/java/org/bitcoinppl/cove/navigation/CoveNavDisplay.kt
  • android/app/src/main/java/org/bitcoinppl/cove/sidebar/SidebarView.kt
  • ios/Cove/AppManager.swift
  • ios/Cove/AuthManager.swift
  • ios/Cove/CoveMainView.swift
  • ios/Cove/Flows/NewWalletFlow/ColdWallet/QrCodeImportScreen.swift
  • ios/Cove/Flows/NewWalletFlow/HotWallet/HotWalletImportScreen.swift
  • ios/Cove/Flows/NewWalletFlow/NewWalletSelectScreen.swift
  • ios/Cove/Flows/SelectedWalletFlow/SelectedWalletContainer.swift
  • ios/Cove/Flows/SelectedWalletFlow/TransactionDetails/TransactionsDetailScreen.swift
  • ios/Cove/Flows/SendFlow/SendFlowContainer.swift
  • ios/Cove/Flows/SettingsFlow/MainSettingsScreen.swift
  • ios/Cove/Flows/SettingsFlow/SettingsContainer.swift
  • ios/Cove/Flows/SettingsFlow/WalletSettings/WalletSettingsContainer.swift
  • ios/Cove/HomeScreens/ListWalletsScreen.swift
  • ios/Cove/LoadAndResetContainer.swift
  • ios/Cove/RouteView.swift
  • ios/Cove/ScanManager.swift
  • ios/Cove/Views/SidebarView.swift
  • rust/src/keys.rs
  • rust/src/manager/cloud_backup_manager/pending/queue_processor.rs
  • rust/src/manager/cloud_backup_manager/verify/session.rs
  • rust/src/manager/pending_wallet_manager.rs
  • rust/src/qr_scanner.rs

Comment thread ios/Cove/AppManager.swift Outdated
Comment thread ios/Cove/Flows/NewWalletFlow/HotWallet/HotWalletImportScreen.swift Outdated
Comment thread ios/Cove/LoadAndResetContainer.swift Outdated
Comment thread rust/src/qr_scanner.rs Outdated
Avoid selecting the imported wallet when onboarding supplies its own import callback. The callback owns the next onboarding route, while the no-callback path still selects and opens the imported wallet.
Record additional repository guidance for readability and comment placement in Rust, Swift, and Kotlin edits.
Consolidate passkey material handling and authentication behind explicit helper types, and add explicit master key version parsing so callers share clearer, stable error behavior.
Refine verification prompts and pending cloud upload state transitions for clearer behavior around authorization failures, retries, and startup integrity refresh conditions.
Align wallet import/reset side effects and route reset timing in Android and iOS so selection, navigation, and alerts update deterministically.
Improve pending-wallet import diagnostics and preserve completed QR scan state for reliable scanner consumers.
Move cloud backup recovery, verification, and cloud-only action
handling into a single cloud_backup_manager detail module.

This removes a separate module boundary that was only delegating
manager control flow, reducing indirection and making all related
state transitions co-located.

Imports and enum references were updated to preserve behavior while
keeping the public API surface unchanged.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
ios/Cove/AppManager.swift (1)

158-164: ⚠️ Potential issue | 🟠 Major

Re-register reconcile listener after rebuilding FfiApp in reset().

Line 158 creates a new FfiApp, but unlike initialization (Line 97), reset() does not call rust.listenForUpdates(updater: self). After reset, app-state reconcile messages can stop flowing.

🔧 Proposed fix
 public func reset() {
     pendingSidebarNavigationTask?.cancel()
     pendingSidebarNavigationTask = nil
     beginNavigationIntent()

     rust = FfiApp()
+    rust.listenForUpdates(updater: self)
     database = Database()
     walletManager = nil

     let state = rust.state()
     router = state.router
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/Cove/AppManager.swift` around lines 158 - 164, The reset() implementation
creates a new FfiApp instance (rust = FfiApp()) but does not re-register the
reconcile listener, so state updates stop; after rebuilding FfiApp in reset(),
call rust.listenForUpdates(updater: self) (the same call used during
initialization where rust.listenForUpdates(updater: self) is invoked) to
reattach the app as the update listener so reconcile messages continue flowing.
rust/src/manager/cloud_backup_manager/verify/pending_completion.rs (1)

87-104: ⚠️ Potential issue | 🔴 Critical

Confirm failed uploads have a guaranteed terminal-failure path or revise is_pending_upload_confirmed to prevent pending loops.

Line 87 treats PersistedCloudBlobState::Failed(_) as confirmed without verifying remote blob presence. However, in verify_pending_wallet_backup (line 175-189), if reader.summary() returns NotFound, the function returns Pending rather than marking the upload as failed. Since Pending does not clear pending_verification_completion (line 48 only clears on success or error), finalize_pending_verification_if_ready() can loop indefinitely:

  1. Upload fails before or during cloud write → Failed state stored
  2. is_pending_upload_confirmed() returns true (line 87, no remote check)
  3. verify_pending_wallet_backup() finds no remote blob and returns Pending
  4. Completion not cleared, retry loops on next call

Failed uploads with revision_hash: None or failed uploads where the cloud write never completed can trigger this path. Either ensure failed uploads always have remote blobs, add a retry limit to Pending states, or verify remote presence before confirming failed uploads in is_pending_upload_confirmed().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/manager/cloud_backup_manager/verify/pending_completion.rs` around
lines 87 - 104, The code treats PersistedCloudBlobState::Failed(_) as
immediately confirmed in is_pending_upload_confirmed, which can cause an
infinite pending loop when the remote blob doesn't exist; modify
is_pending_upload_confirmed to check remote presence for the Failed(_) variant
(same pattern used for UploadedPendingConfirmation(_)) by calling
CloudStorage::global_silent_client().download_wallet_backup with
completion.namespace_id() and upload.record_id(), mapping
CloudStorageError::NotFound(_) to Ok(false) and other errors to Err, and
returning false on unwrap_or(false); ensure you update the branch handling
PersistedCloudBlobState::Failed(_) instead of returning true unconditionally so
verify_pending_wallet_backup can transition out of Pending when remote is
absent.
🧹 Nitpick comments (4)
rust/src/manager/cloud_backup_manager/pending/detail.rs (1)

91-96: Add a blank line between these match arms.

Please separate the multi-line UploadedPendingConfirmation arm from the fallback arm with a blank line for consistency.

♻️ Proposed formatting-only diff
             let pending_state = match &state.state {
                 PersistedCloudBlobState::UploadedPendingConfirmation(pending_state) => {
                     pending_state
                 }
+
                 _ => continue,
             };

As per coding guidelines, **/*.{rs,swift,kt,kts} requires blank lines between multi-line match/switch arms.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/manager/cloud_backup_manager/pending/detail.rs` around lines 91 -
96, The match expression extracting pending_state from state.state should have a
blank line between the multi-line arm for
PersistedCloudBlobState::UploadedPendingConfirmation and the fallback arm;
update the match in the function where pending_state is assigned (matching
PersistedCloudBlobState::UploadedPendingConfirmation(pending_state) => {
pending_state } _ => continue) to insert a single empty line between those two
arms to satisfy the Rust formatting guideline.
rust/crates/cove-device/src/keychain.rs (1)

221-228: Add unit coverage for malformed credential hex behavior.

This method intentionally treats decode failures as None; please add a test that stores invalid hex and asserts None (and valid hex still round-trips) to keep this contract stable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/crates/cove-device/src/keychain.rs` around lines 221 - 228, Add a unit
test for load_cspp_credential_id that verifies decode failures return None and
valid hex round-trips: write a test which uses the same key name constant
CSPP_CREDENTIAL_ID_KEY to insert an invalid hex string via the keychain backing
(exercise Keychain::get/put or the test helper used elsewhere), assert
load_cspp_credential_id() returns None, then store a valid hex (e.g. hex::encode
of a byte vec) and assert load_cspp_credential_id() returns the original bytes;
reference the load_cspp_credential_id method and ensure the test covers the
hex::decode error path and successful path so the contract remains stable.
rust/src/manager/cloud_backup_manager/verify/wrapper_repair.rs (1)

209-216: Avoid redundant credential_id clones in credential construction.

You can move credential_id directly into WrapperRepairCredentials in both branches and skip the extra allocations.

Proposed clone removal
                 Ok(WrapperRepairCredentials {
                     prf_key: new_prf.prf_key,
                     prf_salt: new_prf.prf_salt,
-                    credential_id: new_prf.credential_id.clone(),
+                    credential_id: new_prf.credential_id,
                 })
@@
                 Ok(WrapperRepairCredentials {
                     prf_key: passkey.prf_key,
                     prf_salt: passkey.prf_salt,
-                    credential_id: passkey.credential_id.clone(),
+                    credential_id: passkey.credential_id,
                 })

Also applies to: 220-229

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/manager/cloud_backup_manager/verify/wrapper_repair.rs` around lines
209 - 216, The code is cloning credential_id unnecessarily when building
WrapperRepairCredentials; instead take ownership of the credential_id from the
created struct to avoid the extra allocation. In the branches that call
PasskeyMaterialAcquirer::new(&self.passkey).create_for_wrapper_repair().await
(and the other branch around lines 220-229), move new_prf.credential_id into
WrapperRepairCredentials (e.g., use the owned value rather than
new_prf.credential_id.clone()) and adjust any pattern/matching to consume
new_prf so the credential_id can be moved.
rust/src/manager/cloud_backup_manager/wallets/passkey.rs (1)

192-195: Use the typed backup-version parser here too.

This is the one remaining raw version != 1 gate in the new flow. Calling EncryptedMasterKeyBackup::backup_version() keeps namespace matching aligned with the version handling you just centralized elsewhere.

♻️ Suggested cleanup
-            if encrypted.version != 1 {
+            if encrypted.backup_version().is_err() {
                 had_unsupported_versions = true;
                 continue;
             }

As per coding guidelines: rust/**/*.rs: Verify correct functionality and logic consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/manager/cloud_backup_manager/wallets/passkey.rs` around lines 192 -
195, Replace the raw numeric version check on the EncryptedMasterKeyBackup
struct (the `if encrypted.version != 1 { ... }` gate) with the typed parser call
EncryptedMasterKeyBackup::backup_version() so namespace/version handling is
centralized; i.e., call EncryptedMasterKeyBackup::backup_version() on
`encrypted`, branch when it returns an unsupported value to set
had_unsupported_versions and continue, keeping the existing behavior but using
the typed API for consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@rust/src/manager/cloud_backup_manager/verify.rs`:
- Around line 261-268: Change the error classification in the
encrypted.backup_version() match: instead of returning
CloudBackupError::Internal when the Err(unsupported) case occurs, return the
compatibility-specific variant (e.g., CloudBackupError::Compatibility or the
project’s forward‑compatibility error type) with the same descriptive message
about the unsupported master key backup version; update the Err(unsupported)
branch that currently references MasterKeyBackupVersion::V1 and
CloudBackupError::Internal to use the compatibility error variant so unsupported
backup versions are treated as forward‑compatibility cases.

In `@rust/src/manager/cloud_backup_manager/verify/passkey_auth.rs`:
- Around line 39-52: The PasskeyAuthenticator type and its associated methods
use pub(super) which violates the repo visibility rule; update the visibility to
pub(crate) for the struct declaration and all impl items (e.g., change
`pub(super) struct PasskeyAuthenticator`, `pub(super) fn new`, and `pub(super)
async fn authenticate_with_policy` to use `pub(crate)`) so the type and its API
follow the crate-level visibility guideline.

In `@rust/src/manager/cloud_backup_manager/wallets/passkey.rs`:
- Around line 221-225: The namespace-matching match that binds `discovered` maps
all `PasskeyError` variants except `UserCancelled` and `NoCredentialFound` into
a generic `CloudBackupError::Passkey(...)`, losing the special-case
`PrfUnsupportedProvider`; update that match (the block binding `discovered` and
the similar block later around the other namespace-checking match) to handle
`Err(PasskeyError::PrfUnsupportedProvider)` explicitly by returning
`Err(CloudBackupError::UnsupportedPasskeyProvider)` (the same mapping used in
the acquirer path), leaving the existing `UserCancelled` ->
`NamespaceMatchOutcome::UserDeclined` and `NoCredentialFound` ->
`NamespaceMatchOutcome::NoMatch` arms unchanged.

---

Outside diff comments:
In `@ios/Cove/AppManager.swift`:
- Around line 158-164: The reset() implementation creates a new FfiApp instance
(rust = FfiApp()) but does not re-register the reconcile listener, so state
updates stop; after rebuilding FfiApp in reset(), call
rust.listenForUpdates(updater: self) (the same call used during initialization
where rust.listenForUpdates(updater: self) is invoked) to reattach the app as
the update listener so reconcile messages continue flowing.

In `@rust/src/manager/cloud_backup_manager/verify/pending_completion.rs`:
- Around line 87-104: The code treats PersistedCloudBlobState::Failed(_) as
immediately confirmed in is_pending_upload_confirmed, which can cause an
infinite pending loop when the remote blob doesn't exist; modify
is_pending_upload_confirmed to check remote presence for the Failed(_) variant
(same pattern used for UploadedPendingConfirmation(_)) by calling
CloudStorage::global_silent_client().download_wallet_backup with
completion.namespace_id() and upload.record_id(), mapping
CloudStorageError::NotFound(_) to Ok(false) and other errors to Err, and
returning false on unwrap_or(false); ensure you update the branch handling
PersistedCloudBlobState::Failed(_) instead of returning true unconditionally so
verify_pending_wallet_backup can transition out of Pending when remote is
absent.

---

Nitpick comments:
In `@rust/crates/cove-device/src/keychain.rs`:
- Around line 221-228: Add a unit test for load_cspp_credential_id that verifies
decode failures return None and valid hex round-trips: write a test which uses
the same key name constant CSPP_CREDENTIAL_ID_KEY to insert an invalid hex
string via the keychain backing (exercise Keychain::get/put or the test helper
used elsewhere), assert load_cspp_credential_id() returns None, then store a
valid hex (e.g. hex::encode of a byte vec) and assert load_cspp_credential_id()
returns the original bytes; reference the load_cspp_credential_id method and
ensure the test covers the hex::decode error path and successful path so the
contract remains stable.

In `@rust/src/manager/cloud_backup_manager/pending/detail.rs`:
- Around line 91-96: The match expression extracting pending_state from
state.state should have a blank line between the multi-line arm for
PersistedCloudBlobState::UploadedPendingConfirmation and the fallback arm;
update the match in the function where pending_state is assigned (matching
PersistedCloudBlobState::UploadedPendingConfirmation(pending_state) => {
pending_state } _ => continue) to insert a single empty line between those two
arms to satisfy the Rust formatting guideline.

In `@rust/src/manager/cloud_backup_manager/verify/wrapper_repair.rs`:
- Around line 209-216: The code is cloning credential_id unnecessarily when
building WrapperRepairCredentials; instead take ownership of the credential_id
from the created struct to avoid the extra allocation. In the branches that call
PasskeyMaterialAcquirer::new(&self.passkey).create_for_wrapper_repair().await
(and the other branch around lines 220-229), move new_prf.credential_id into
WrapperRepairCredentials (e.g., use the owned value rather than
new_prf.credential_id.clone()) and adjust any pattern/matching to consume
new_prf so the credential_id can be moved.

In `@rust/src/manager/cloud_backup_manager/wallets/passkey.rs`:
- Around line 192-195: Replace the raw numeric version check on the
EncryptedMasterKeyBackup struct (the `if encrypted.version != 1 { ... }` gate)
with the typed parser call EncryptedMasterKeyBackup::backup_version() so
namespace/version handling is centralized; i.e., call
EncryptedMasterKeyBackup::backup_version() on `encrypted`, branch when it
returns an unsupported value to set had_unsupported_versions and continue,
keeping the existing behavior but using the typed API for consistency.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c7b12918-075c-4a2f-96ce-3c48fdc5e6be

📥 Commits

Reviewing files that changed from the base of the PR and between e370b60 and fc3b2fb.

📒 Files selected for processing (33)
  • AGENTS.md
  • android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt
  • android/app/src/main/java/org/bitcoinppl/cove/MainActivity.kt
  • android/app/src/main/java/org/bitcoinppl/cove/RouteView.kt
  • android/app/src/main/java/org/bitcoinppl/cove/flows/NewWalletFlow/hot_wallet/HotWalletImportScreen.kt
  • android/app/src/main/java/org/bitcoinppl/cove/flows/SelectedWalletFlow/TransactionsCardView.kt
  • android/app/src/main/java/org/bitcoinppl/cove/navigation/CoveNavDisplay.kt
  • ios/Cove/AppManager.swift
  • ios/Cove/Flows/NewWalletFlow/ColdWallet/QrCodeImportScreen.swift
  • ios/Cove/Flows/NewWalletFlow/HotWallet/HotWalletImportScreen.swift
  • ios/Cove/Flows/NewWalletFlow/NewWalletSelectScreen.swift
  • ios/Cove/LoadAndResetContainer.swift
  • rust/crates/cove-cspp/src/backup_data.rs
  • rust/crates/cove-cspp/src/master_key_crypto.rs
  • rust/crates/cove-device/src/keychain.rs
  • rust/src/manager.rs
  • rust/src/manager/cloud_backup_manager.rs
  • rust/src/manager/cloud_backup_manager/detail.rs
  • rust/src/manager/cloud_backup_manager/ops.rs
  • rust/src/manager/cloud_backup_manager/pending/detail.rs
  • rust/src/manager/cloud_backup_manager/pending/queue_processor.rs
  • rust/src/manager/cloud_backup_manager/prompt.rs
  • rust/src/manager/cloud_backup_manager/runtime_actor.rs
  • rust/src/manager/cloud_backup_manager/verify.rs
  • rust/src/manager/cloud_backup_manager/verify/integrity.rs
  • rust/src/manager/cloud_backup_manager/verify/passkey_auth.rs
  • rust/src/manager/cloud_backup_manager/verify/pending_completion.rs
  • rust/src/manager/cloud_backup_manager/verify/session.rs
  • rust/src/manager/cloud_backup_manager/verify/wrapper_repair.rs
  • rust/src/manager/cloud_backup_manager/wallets.rs
  • rust/src/manager/cloud_backup_manager/wallets/passkey.rs
  • rust/src/manager/pending_wallet_manager.rs
  • rust/src/qr_scanner.rs
💤 Files with no reviewable changes (1)
  • rust/src/manager.rs
✅ Files skipped from review due to trivial changes (3)
  • AGENTS.md
  • rust/src/manager/cloud_backup_manager/runtime_actor.rs
  • rust/src/manager/cloud_backup_manager/wallets.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • ios/Cove/Flows/NewWalletFlow/NewWalletSelectScreen.swift
  • rust/src/manager/pending_wallet_manager.rs
  • rust/src/manager/cloud_backup_manager/verify/session.rs
  • rust/src/manager/cloud_backup_manager/pending/queue_processor.rs

Comment thread rust/src/manager/cloud_backup_manager/verify.rs
Comment thread rust/src/manager/cloud_backup_manager/verify/passkey_auth.rs Outdated
Comment thread rust/src/manager/cloud_backup_manager/wallets/passkey.rs
@praveenperera praveenperera force-pushed the refactor-and-navigation-fix branch from 35ae586 to e5df590 Compare April 28, 2026 23:18
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
ios/Cove/AppManager.swift (2)

260-264: Add a blank line between mutation and return in beginNavigationIntent().

This function currently mutates state and returns immediately without the required visual separation.

🎨 Minimal formatting fix
 `@discardableResult`
 private func beginNavigationIntent() -> UInt64 {
     navigationGeneration &+= 1
+
     return navigationGeneration
 }
As per coding guidelines "`**/*.{rs,swift,kt}`: Add blank lines between logical steps in function bodies ... between state mutations and final returns".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/Cove/AppManager.swift` around lines 260 - 264, In
beginNavigationIntent(), add a blank line after the state mutation (the
navigationGeneration &+= 1 statement) and before the return of
navigationGeneration so the mutation and the return are visually separated;
update the body of the beginNavigationIntent() method to include that blank line
while leaving the existing logic and `@discardableResult` annotation unchanged.

194-203: Consider routing these user intents through dispatch(action:) for manager consistency.

selectWalletOrThrow and selectLatestOrNewWallet are user-intent entry points; aligning them with dispatch(action:) (where equivalent actions exist) would keep AppManager intent handling consistent and easier to audit.

Based on learnings: "For long-lived UI-facing managers, prefer dispatch(action:) for user intents and keep named methods for reads, bootstrap/lifecycle hooks, and special service-style operations; use state() for the initial snapshot only; send typed delta reconcile messages for ongoing UI updates instead of re-sending the whole state after every mutation".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/Cove/AppManager.swift` around lines 194 - 203, Replace direct calls from
UI intents with dispatched actions: stop calling beginNavigationIntent();
rust.selectWallet(id:); rust.selectLatestOrNewWallet() directly in
selectWalletOrThrow and selectLatestOrNewWallet. Instead define/ reuse typed
actions (e.g., SelectWallet(id: WalletId) and SelectLatestOrNewWallet) that the
AppManager reducer/handler will run (calling rust.selectWallet /
rust.selectLatestOrNewWallet, handle throwing errors for SelectWallet, and set
isSidebarVisible = false there). Then have selectWalletOrThrow and
selectLatestOrNewWallet call beginNavigationIntent() and dispatch(action: /*
corresponding action */) so all intent handling flows through dispatch(action:).
rust/src/manager/cloud_backup_manager/verify/pending_completion.rs (1)

87-93: Log remote-check failures before falling back to false.

Current unwrap_or(false) keeps verification pending (good) but silently drops non-NotFound cloud errors, which makes stalls hard to diagnose.

Suggested refactor
-            Some(PersistedCloudBlobState::Failed(_)) => {
-                Self::remote_pending_upload_exists(completion, upload).await.unwrap_or(false)
-            }
+            Some(PersistedCloudBlobState::Failed(_)) => {
+                Self::remote_pending_upload_exists(completion, upload).await
+            }

             Some(PersistedCloudBlobState::UploadedPendingConfirmation(_)) => {
-                Self::remote_pending_upload_exists(completion, upload).await.unwrap_or(false)
+                Self::remote_pending_upload_exists(completion, upload).await
             }

@@
-    ) -> Result<bool, CloudStorageError> {
-        CloudStorage::global_silent_client()
+    ) -> bool {
+        match CloudStorage::global_silent_client()
             .download_wallet_backup(
                 completion.namespace_id().to_string(),
                 upload.record_id().to_string(),
             )
             .await
-            .map(|_| true)
-            .or_else(|error| match error {
-                CloudStorageError::NotFound(_) => Ok(false),
-                other => Err(other),
-            })
+        {
+            Ok(_) => true,
+            Err(CloudStorageError::NotFound(_)) => false,
+            Err(error) => {
+                warn!(
+                    "Pending verification: remote upload existence check failed for record_id={}: {error}",
+                    upload.record_id(),
+                );
+                false
+            }
+        }
     }

Also applies to: 99-114

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/manager/cloud_backup_manager/verify/pending_completion.rs` around
lines 87 - 93, The code currently calls
Self::remote_pending_upload_exists(...).await.unwrap_or(false) in the
PersistedCloudBlobState::Failed and ::UploadedPendingConfirmation arms (and
similar places) which hides cloud errors; change those unwrap_or(false) calls to
explicitly match the Result, log the Err with context (include the completion
and upload identifiers or other unique context) and then return false on error.
Specifically, replace the unwrap_or(false) uses around
remote_pending_upload_exists in pending_completion.rs with a Result match (or
.await.map_or_else) that on Ok(v) returns v and on Err(e) calls the logger
(e.g., error!("remote_pending_upload_exists failed for completion={} upload={} :
{:?}", completion.id, upload.id, e)) then returns false.
rust/src/manager/cloud_backup_manager/ops.rs (1)

559-568: Use an enable-specific discover/create entrypoint here.

do_enable_cloud_backup_create_new now goes through discover_or_create_for_wrapper_repair(), so normal enable cancellations and logs inherit wrapper-repair wording/context. The no-discovery enable path already uses the enable-specific create_new(...) API, so these two enable flows now diverge operationally. I’d extract a dedicated discover_or_create_for_enable wrapper (or parameterize the context) to keep enable telemetry and error paths consistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/manager/cloud_backup_manager/ops.rs` around lines 559 - 568, The
current enable flow calls
PasskeyMaterialAcquirer::discover_or_create_for_wrapper_repair (via
do_enable_cloud_backup_create_new), which causes enable-specific
cancellations/logs to inherit "wrapper-repair" wording; change the enable path
to use an enable-specific discovery/create entrypoint: add a new wrapper (e.g.,
PasskeyMaterialAcquirer::discover_or_create_for_enable or a helper
discover_or_create_for_enable) that forwards to the appropriate internal logic
but uses enable-specific context/messages, then update the
acquire_enable_passkey call site to invoke this new
discover_or_create_for_enable closure instead of
discover_or_create_for_wrapper_repair so do_enable_cloud_backup_create_new and
the no-discovery create_new path share consistent enable telemetry and error
wording.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ios/Cove/AppManager.swift`:
- Around line 225-228: popRoute() currently calls router.routes.removeLast()
which can crash if router.routes is empty; update popRoute() to check the routes
array before mutating it (e.g., guard !router.routes.isEmpty else { return } or
log and return) so removeLast() is only called when safe; keep the call to
beginNavigationIntent() only if appropriate (either before or after the
emptiness check depending on intended behavior) and reference popRoute(),
router.routes, removeLast(), beginNavigationIntent(), and currentRoute when
making the change so callers relying on empty-route behavior remain supported.

In `@rust/src/manager/cloud_backup_manager/verify/passkey_auth.rs`:
- Around line 83-86: The StoredPasskeyAuthOutcome::Failed arm in
verify/passkey_auth.rs currently converts all failures (e.g., in the match that
returns Ok(PasskeyAuthOutcome::NoCredentialFound)) which hides
PasskeyError::PrfUnsupportedProvider; change the failure handling to detect and
propagate PasskeyError::PrfUnsupportedProvider up as
CloudBackupError::UnsupportedPasskeyProvider (instead of mapping to
NoCredentialFound), and update the map_discovery_error usage to preserve /
convert PrfUnsupportedProvider into the specific UnsupportedPasskeyProvider
variant rather than a generic Passkey(...) error; apply the same change to the
other identical handlers referenced (the ones around
StoredPasskeyAuthOutcome::Failed at the other two locations) so matcher/acquirer
and verification paths behave consistently.

---

Nitpick comments:
In `@ios/Cove/AppManager.swift`:
- Around line 260-264: In beginNavigationIntent(), add a blank line after the
state mutation (the navigationGeneration &+= 1 statement) and before the return
of navigationGeneration so the mutation and the return are visually separated;
update the body of the beginNavigationIntent() method to include that blank line
while leaving the existing logic and `@discardableResult` annotation unchanged.
- Around line 194-203: Replace direct calls from UI intents with dispatched
actions: stop calling beginNavigationIntent(); rust.selectWallet(id:);
rust.selectLatestOrNewWallet() directly in selectWalletOrThrow and
selectLatestOrNewWallet. Instead define/ reuse typed actions (e.g.,
SelectWallet(id: WalletId) and SelectLatestOrNewWallet) that the AppManager
reducer/handler will run (calling rust.selectWallet /
rust.selectLatestOrNewWallet, handle throwing errors for SelectWallet, and set
isSidebarVisible = false there). Then have selectWalletOrThrow and
selectLatestOrNewWallet call beginNavigationIntent() and dispatch(action: /*
corresponding action */) so all intent handling flows through dispatch(action:).

In `@rust/src/manager/cloud_backup_manager/ops.rs`:
- Around line 559-568: The current enable flow calls
PasskeyMaterialAcquirer::discover_or_create_for_wrapper_repair (via
do_enable_cloud_backup_create_new), which causes enable-specific
cancellations/logs to inherit "wrapper-repair" wording; change the enable path
to use an enable-specific discovery/create entrypoint: add a new wrapper (e.g.,
PasskeyMaterialAcquirer::discover_or_create_for_enable or a helper
discover_or_create_for_enable) that forwards to the appropriate internal logic
but uses enable-specific context/messages, then update the
acquire_enable_passkey call site to invoke this new
discover_or_create_for_enable closure instead of
discover_or_create_for_wrapper_repair so do_enable_cloud_backup_create_new and
the no-discovery create_new path share consistent enable telemetry and error
wording.

In `@rust/src/manager/cloud_backup_manager/verify/pending_completion.rs`:
- Around line 87-93: The code currently calls
Self::remote_pending_upload_exists(...).await.unwrap_or(false) in the
PersistedCloudBlobState::Failed and ::UploadedPendingConfirmation arms (and
similar places) which hides cloud errors; change those unwrap_or(false) calls to
explicitly match the Result, log the Err with context (include the completion
and upload identifiers or other unique context) and then return false on error.
Specifically, replace the unwrap_or(false) uses around
remote_pending_upload_exists in pending_completion.rs with a Result match (or
.await.map_or_else) that on Ok(v) returns v and on Err(e) calls the logger
(e.g., error!("remote_pending_upload_exists failed for completion={} upload={} :
{:?}", completion.id, upload.id, e)) then returns false.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b767bd08-830f-46ce-a908-e836b1ffa677

📥 Commits

Reviewing files that changed from the base of the PR and between 61617df and e5df590.

⛔ Files ignored due to path filters (1)
  • ios/CoveCore/Sources/CoveCore/generated/cove.swift is excluded by !**/generated/**
📒 Files selected for processing (13)
  • android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt
  • ios/Cove/AppManager.swift
  • rust/crates/cove-device/src/keychain.rs
  • rust/src/manager/cloud_backup_manager.rs
  • rust/src/manager/cloud_backup_manager/ops.rs
  • rust/src/manager/cloud_backup_manager/pending/detail.rs
  • rust/src/manager/cloud_backup_manager/verify.rs
  • rust/src/manager/cloud_backup_manager/verify/passkey_auth.rs
  • rust/src/manager/cloud_backup_manager/verify/pending_completion.rs
  • rust/src/manager/cloud_backup_manager/verify/wrapper_repair.rs
  • rust/src/manager/cloud_backup_manager/wallets.rs
  • rust/src/manager/cloud_backup_manager/wallets/passkey.rs
  • rust/src/manager/cloud_backup_manager/wallets/upload.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • rust/src/manager/cloud_backup_manager/wallets.rs
  • rust/src/manager/cloud_backup_manager/pending/detail.rs
  • rust/src/manager/cloud_backup_manager/verify/wrapper_repair.rs

Comment thread ios/Cove/AppManager.swift
Comment thread rust/src/manager/cloud_backup_manager/verify/passkey_auth.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
rust/src/app.rs (1)

242-245: Use map_err_str instead of manual stringification.

Replace .map_err(|error| AppError::WalletSelection(error.to_string())) with .map_err_str(AppError::WalletSelection) to align with the established pattern across the codebase per coding guidelines.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/app.rs` around lines 242 - 245, In the AppAction::SelectWallet match
arm, change the manual map_err closure on FfiApp::global().select_wallet(id,
None) to use the codebase helper by replacing `.map_err(|error|
AppError::WalletSelection(error.to_string()))` with
`.map_err_str(AppError::WalletSelection)` so the select_wallet call uses
map_err_str with AppError::WalletSelection.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ios/Cove/AppManager.swift`:
- Around line 225-229: popRoute currently returns early when router.routes is
empty, skipping beginNavigationIntent and leaving a previously queued deferred
sidebar navigation valid; change popRoute so beginNavigationIntent() is always
invoked before the guard/early-return (or move the guard to only protect
router.routes.removeLast), e.g., ensure beginNavigationIntent() is called
unconditionally inside popRoute and only conditionally call
router.routes.removeLast() when router.routes.isEmpty is false.

In `@rust/src/app.rs`:
- Around line 248-250: The handler for AppAction::SelectLatestOrNewWallet
blindly treats any failure inside FfiApp::global().select_latest_or_new_wallet()
as "no wallets" because select_latest_or_new_wallet() currently falls back to
Route::NewWallet for every select_latest_wallet() error; change
select_latest_or_new_wallet() so it distinguishes the "no wallets found" case
from data-access/query errors and returns a Result<Route, AppError> (or
otherwise propagates AppError::WalletSelection on DB/query failures), then
update the call site in AppAction::SelectLatestOrNewWallet to handle the
Err(AppError::WalletSelection) path (propagate/log/return the error) rather than
treating it as Route::NewWallet; keep Route::NewWallet only for the explicit
empty-wallet case from select_latest_wallet().

In `@rust/src/manager/cloud_backup_manager/wallets/passkey.rs`:
- Around line 83-98: The current match_namespaces() implementation returns on
the first successful decrypt, allowing a single credential_id to be reused
across namespaces and causing nondeterministic/incorrect restores; change
match_namespaces() to continue scanning all candidate namespaces after the first
successful decrypt, collect all successful matches, and then either (a) surface
an explicit ambiguity error (e.g., AmbiguousCredentialForNamespaces) to callers
(used by discover_or_create_for_enable() and the restore flow in ops.rs) or (b)
apply a deterministic tie-breaker based on real metadata before returning a
single namespace; update discover_or_create_for_enable() and its callers in
rust/src/manager/cloud_backup_manager/ops.rs to handle the new ambiguity result,
and add a regression test that creates two namespaces encrypted with the same
credential to ensure ambiguity is detected and handled.

---

Nitpick comments:
In `@rust/src/app.rs`:
- Around line 242-245: In the AppAction::SelectWallet match arm, change the
manual map_err closure on FfiApp::global().select_wallet(id, None) to use the
codebase helper by replacing `.map_err(|error|
AppError::WalletSelection(error.to_string()))` with
`.map_err_str(AppError::WalletSelection)` so the select_wallet call uses
map_err_str with AppError::WalletSelection.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1222bd45-1843-4048-88fb-695e3ed59060

📥 Commits

Reviewing files that changed from the base of the PR and between e5df590 and 6ce379d.

⛔ Files ignored due to path filters (1)
  • ios/CoveCore/Sources/CoveCore/generated/cove.swift is excluded by !**/generated/**
📒 Files selected for processing (7)
  • android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt
  • ios/Cove/AppManager.swift
  • rust/src/app.rs
  • rust/src/manager/cloud_backup_manager/ops.rs
  • rust/src/manager/cloud_backup_manager/verify/passkey_auth.rs
  • rust/src/manager/cloud_backup_manager/verify/pending_completion.rs
  • rust/src/manager/cloud_backup_manager/wallets/passkey.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • rust/src/manager/cloud_backup_manager/verify/pending_completion.rs

Comment thread ios/Cove/AppManager.swift Outdated
Comment thread rust/src/app.rs
Comment thread rust/src/manager/cloud_backup_manager/wallets/passkey.rs
Selection now returns an error when no wallets exist, and callers
handle fallback behavior instead of assuming success.

Native Kotlin and Swift bindings were regenerated for the updated error-
returning API and mobile startup/navigation paths were adjusted to avoid
unnecessary crashes during wallet bootstrap.,
@praveenperera praveenperera force-pushed the refactor-and-navigation-fix branch from 205c5a4 to 874da81 Compare April 29, 2026 01:59
Restore now keeps all namespaces matched by one passkey and
restores wallets from every match in order.

This includes aggregate progress reporting for the full download set,
namespace-scoped error reporting, and updated matching behavior to return
multiple candidates instead of a single namespace result.,
@praveenperera praveenperera force-pushed the refactor-and-navigation-fix branch from 874da81 to 363094c Compare April 29, 2026 02:06
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
rust/src/manager/cloud_backup_manager/ops.rs (1)

629-631: 🛠️ Refactor suggestion | 🟠 Major

Use pub(crate) here instead of pub(super).

This new entrypoint violates the repo's Rust visibility rule.

Suggested fix
-    pub(super) async fn do_enable_cloud_backup_no_discovery(&self) -> Result<(), CloudBackupError> {
+    pub(crate) async fn do_enable_cloud_backup_no_discovery(&self) -> Result<(), CloudBackupError> {

As per coding guidelines "**/*.rs: Never use pub(in ...) or pub(super) visibility modifiers; use pub(crate) or pub if non-private visibility is needed".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/manager/cloud_backup_manager/ops.rs` around lines 629 - 631, The
function visibility for do_enable_cloud_backup_no_discovery is declared as
pub(super) which violates the repo rule; change its visibility to pub(crate) so
the signature becomes pub(crate) async fn
do_enable_cloud_backup_no_discovery(&self) -> Result<(), CloudBackupError>
(locate this method in cloud_backup_manager::ops.rs and update the visibility
modifier only).
♻️ Duplicate comments (1)
rust/src/manager/cloud_backup_manager/ops.rs (1)

469-485: ⚠️ Potential issue | 🟠 Major

Don't silently bind recovery to the first matched namespace.

When one passkey matches multiple namespaces, these branches still persist only matches[0]. That choice is effectively arbitrary, but the rest of this file uses current_namespace_id() for future sync/delete/detail flows, so wallets restored from the other matched namespaces can later be treated as out-of-sync and re-uploaded into the selected namespace. Please surface this as an explicit ambiguity/user-choice instead of auto-picking the first match.

Also applies to: 731-759

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/manager/cloud_backup_manager/ops.rs` around lines 469 - 485, The
code currently auto-picks the first namespace from
NamespaceMatchOutcome::Matched by calling matches.into_iter().next() and then
calls complete_recovery with that arbitrary choice; change this to detect when
matched_count > 1 and surface an explicit ambiguity flow (e.g., prompt user to
choose which namespace to restore or fail with a clear ambiguity error) instead
of silently binding to matches[0]; after the user selects a namespace (or
aborts), pass that selected namespace to complete_recovery(keychain, &cloud,
&cspp, selected_namespace) and ensure the same ambiguity-handling change is
applied to the duplicate block around the other occurrence (the similar logic at
lines 731-759) so future current_namespace_id() uses reflect the user’s
selection rather than an implicit first element.
🧹 Nitpick comments (2)
rust/src/app.rs (1)

678-689: Use map_err_prefix for static-context error wrapping.

Line 678 and Line 685 build static-prefix error strings manually. This is better expressed with map_err_prefix for consistency with the repository standard.

Suggested refactor
-        let wallets = database.wallets().all_sorted_active().map_err_str(|error| {
-            SelectLatestWalletError::WalletSelection(AppError::WalletSelection(format!(
-                "unable to get sorted wallets: {error}"
-            )))
-        })?;
+        let wallets = database
+            .wallets()
+            .all_sorted_active()
+            .map_err_prefix("unable to get sorted wallets: ")
+            .map_err(AppError::WalletSelection)
+            .map_err(SelectLatestWalletError::WalletSelection)?;
         let latest_wallet = wallets.first().ok_or(SelectLatestWalletError::NoWalletsFound)?;
 
-        self.select_wallet(latest_wallet.id.clone(), None).map_err_str(|error| {
-            SelectLatestWalletError::WalletSelection(AppError::WalletSelection(format!(
-                "unable to select latest wallet: {error}"
-            )))
-        })?;
+        self.select_wallet(latest_wallet.id.clone(), None)
+            .map_err_prefix("unable to select latest wallet: ")
+            .map_err(AppError::WalletSelection)
+            .map_err(SelectLatestWalletError::WalletSelection)?;

As per coding guidelines **/*.rs: Use cove_util::ResultExt::map_err_prefix instead of .map_err(|e| Error::Variant(format!("context: {e}"))) when the prefix is a static string.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/app.rs` around lines 678 - 689, Replace the manual error wrapping
that uses map_err_str when fetching and selecting the latest wallet with the
repository standard map_err_prefix: change the wallets retrieval call that
currently uses .map_err_str(|error|
SelectLatestWalletError::WalletSelection(AppError::WalletSelection(format!("unable
to get sorted wallets: {error}")))) to use .map_err_prefix("unable to get sorted
wallets", SelectLatestWalletError::WalletSelection) (or equivalent
map_err_prefix signature), and likewise change the self.select_wallet(...)
.map_err_str(...) wrapping that produces
SelectLatestWalletError::WalletSelection(AppError::WalletSelection(...)) to use
map_err_prefix("unable to select latest wallet",
SelectLatestWalletError::WalletSelection) so the static-context prefix is used
consistently instead of formatting the error string manually; keep the same
error variants (SelectLatestWalletError::WalletSelection and
AppError::WalletSelection) as the target wrappers.
android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt (1)

9975-9978: Avoid exposing wallet selection through multiple public APIs with different failure semantics.

SelectWallet / SelectLatestOrNewWallet are now part of AppAction, but wallet selection is also exposed through dedicated methods and a separate throwing dispatch path. That makes it easy for future callers to pick non-throwing dispatch() and accidentally lose AppException propagation for the same user intent. I’d pick one canonical source-level API for wallet selection, then regenerate this binding.

Based on learnings, For long-lived UI-facing managers, prefer dispatch(action:) for user intents and keep named methods for reads, bootstrap/lifecycle hooks, and special service-style operations, use state() for the initial snapshot only, and send typed delta reconcile messages for ongoing UI updates instead of re-sending the whole state after every mutation, and Never manually edit generated files.

Also applies to: 10597-10605, 30334-30343

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt` around lines 9975
- 9978, The code exposes wallet selection both via AppAction
(SelectWallet/SelectLatestOrNewWallet) and via dedicated public methods plus a
throwing dispatch path (`dispatchThrowing`), which risks callers using
non-throwing `dispatch` and dropping AppException propagation; consolidate by
choosing one canonical API for wallet selection (prefer dispatch(action:
AppAction) for UI intents), remove or make private the dedicated
wallet-selection methods and/or `dispatchThrowing`, update callsites to use
`dispatch(SelectWallet...)`/`dispatch(SelectLatestOrNewWallet...)`, then
regenerate the binding so the generated file reflects the single API; retain
named public methods only for reads/bootstrap/service operations and keep
`state()` as initial snapshot access.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt`:
- Around line 9975-9978: The interface method dispatchThrowing(action:
AppAction) is missing a Kotlin checked-exception annotation: add
`@Throws`(AppException::class) to the method declaration in the UniFFI/source
definition for FfiAppInterface (not by editing the generated cove.kt), then
regenerate the bindings so FfiAppInterface.dispatchThrowing and the generated
override both carry `@Throws`(AppException::class) and Java callers will see the
checked exception in the interface signature.

In `@ios/Cove/AppManager.swift`:
- Around line 198-201: selectLatestOrNewWallet currently calls the non-throwing
rust.dispatch which swallows actionable failures; change selectLatestOrNewWallet
to propagate dispatch errors by making it a throwing function and call the
throwing variant (e.g., use try rust.dispatch(action:
.selectLatestOrNewWallet)); keep beginNavigationIntent() before dispatch, and
let the error propagate (or rethrow) so callers can react (retry/alert/fallback
UI) when AppAction.selectLatestOrNewWallet fails.

---

Outside diff comments:
In `@rust/src/manager/cloud_backup_manager/ops.rs`:
- Around line 629-631: The function visibility for
do_enable_cloud_backup_no_discovery is declared as pub(super) which violates the
repo rule; change its visibility to pub(crate) so the signature becomes
pub(crate) async fn do_enable_cloud_backup_no_discovery(&self) -> Result<(),
CloudBackupError> (locate this method in cloud_backup_manager::ops.rs and update
the visibility modifier only).

---

Duplicate comments:
In `@rust/src/manager/cloud_backup_manager/ops.rs`:
- Around line 469-485: The code currently auto-picks the first namespace from
NamespaceMatchOutcome::Matched by calling matches.into_iter().next() and then
calls complete_recovery with that arbitrary choice; change this to detect when
matched_count > 1 and surface an explicit ambiguity flow (e.g., prompt user to
choose which namespace to restore or fail with a clear ambiguity error) instead
of silently binding to matches[0]; after the user selects a namespace (or
aborts), pass that selected namespace to complete_recovery(keychain, &cloud,
&cspp, selected_namespace) and ensure the same ambiguity-handling change is
applied to the duplicate block around the other occurrence (the similar logic at
lines 731-759) so future current_namespace_id() uses reflect the user’s
selection rather than an implicit first element.

---

Nitpick comments:
In `@android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt`:
- Around line 9975-9978: The code exposes wallet selection both via AppAction
(SelectWallet/SelectLatestOrNewWallet) and via dedicated public methods plus a
throwing dispatch path (`dispatchThrowing`), which risks callers using
non-throwing `dispatch` and dropping AppException propagation; consolidate by
choosing one canonical API for wallet selection (prefer dispatch(action:
AppAction) for UI intents), remove or make private the dedicated
wallet-selection methods and/or `dispatchThrowing`, update callsites to use
`dispatch(SelectWallet...)`/`dispatch(SelectLatestOrNewWallet...)`, then
regenerate the binding so the generated file reflects the single API; retain
named public methods only for reads/bootstrap/service operations and keep
`state()` as initial snapshot access.

In `@rust/src/app.rs`:
- Around line 678-689: Replace the manual error wrapping that uses map_err_str
when fetching and selecting the latest wallet with the repository standard
map_err_prefix: change the wallets retrieval call that currently uses
.map_err_str(|error|
SelectLatestWalletError::WalletSelection(AppError::WalletSelection(format!("unable
to get sorted wallets: {error}")))) to use .map_err_prefix("unable to get sorted
wallets", SelectLatestWalletError::WalletSelection) (or equivalent
map_err_prefix signature), and likewise change the self.select_wallet(...)
.map_err_str(...) wrapping that produces
SelectLatestWalletError::WalletSelection(AppError::WalletSelection(...)) to use
map_err_prefix("unable to select latest wallet",
SelectLatestWalletError::WalletSelection) so the static-context prefix is used
consistently instead of formatting the error string manually; keep the same
error variants (SelectLatestWalletError::WalletSelection and
AppError::WalletSelection) as the target wrappers.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3b47bd6f-e6ff-4c66-b050-c0212e1bad8d

📥 Commits

Reviewing files that changed from the base of the PR and between e5df590 and 363094c.

⛔ Files ignored due to path filters (1)
  • ios/CoveCore/Sources/CoveCore/generated/cove.swift is excluded by !**/generated/**
📒 Files selected for processing (9)
  • android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt
  • android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt
  • ios/Cove/AppManager.swift
  • rust/src/app.rs
  • rust/src/manager/cloud_backup_manager/ops.rs
  • rust/src/manager/cloud_backup_manager/verify/passkey_auth.rs
  • rust/src/manager/cloud_backup_manager/verify/pending_completion.rs
  • rust/src/manager/cloud_backup_manager/wallets/passkey.rs
  • rust/src/manager/onboarding_manager.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt

Comment thread android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt Outdated
Comment thread ios/Cove/AppManager.swift Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
ios/Cove/AppManager.swift (1)

198-205: ⚠️ Potential issue | 🟠 Major

selectLatestOrNewWallet still swallows actionable dispatch errors.

Line 201–204 logs failure but does not let callers react (retry/alert/fallback). This weakens the new error-propagating wallet-selection path.

🛠️ Suggested fix
+    func selectLatestOrNewWalletOrThrow() throws {
+        beginNavigationIntent()
+        try rust.dispatch(action: .selectLatestOrNewWallet)
+        isSidebarVisible = false
+    }
+
     func selectLatestOrNewWallet() {
-        beginNavigationIntent()
         do {
-            try rust.dispatch(action: .selectLatestOrNewWallet)
+            try selectLatestOrNewWalletOrThrow()
         } catch {
             Log.error("Unable to select latest wallet, error: \(error)")
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/Cove/AppManager.swift` around lines 198 - 205, The
selectLatestOrNewWallet function currently swallows dispatch errors by catching
and only logging them; change it to propagate failures so callers can
react—either make selectLatestOrNewWallet a throwing method that calls try
rust.dispatch(action: .selectLatestOrNewWallet) without catching (or rethrow the
caught error), or add a completion/Result callback parameter to return
success/failure; ensure beginNavigationIntent() remains invoked and preserve the
rust.dispatch(action: .selectLatestOrNewWallet) reference while removing the
silent catch so callers can retry/alert/fallback.
🧹 Nitpick comments (1)
android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt (1)

30399-30418: Optional: document the AppAction ordinal contract in AGENTS.md.

Given the tight Rust/Kotlin ordinal coupling, adding a short “do not reorder without regen + checksum update” note would reduce future integration mistakes.

Based on learnings: Document common mistakes and confusion points in the AGENTS.md file to alert future developers and agents of surprises encountered in the project.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt` around lines
30399 - 30418, Add a short note to AGENTS.md documenting the tight ordinal
contract between Kotlin and Rust for AppAction (the enum/variant ordering used
in the switch/when that maps ordinals like 4 -> AppAction.SelectWallet, 5 ->
AppAction.SelectLatestOrNewWallet, 6 -> AppAction.ChangeNetwork, etc.), warning
developers not to reorder, insert, or remove variants without regenerating
bindings and updating the checksum; briefly list the common pitfalls
(reordering, missing regen, checksum update) and where to update (reference
AppAction and the FfiConverterType* readers) so future maintainers know the
exact steps to keep Kotlin/Rust serialization in sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt`:
- Around line 244-251: The method selectLatestOrNewWallet currently catches
exceptions from rust.dispatch(AppAction.SelectLatestOrNewWallet) and only logs
them, preventing callers from reacting to failures; remove the
swallow-or-log-only behavior by letting the exception propagate (either remove
the try/catch or rethrow the caught Exception after logging) so callers can
handle selection failures, keeping references to beginNavigationIntent() and
rust.dispatch(AppAction.SelectLatestOrNewWallet) when locating the code.

In `@rust/src/manager/cloud_backup_manager/ops.rs`:
- Around line 889-908: The code currently persists namespace_wallets.first()
which may be lexicographically first rather than the namespace that actually
restored wallets; change the logic to pick the namespace associated with the
first successful restore (or first namespace with downloadable/restored wallets)
returned by restore_via_passkey_matching() instead of namespace_wallets.first().
Locate where restore results are collected and add a variable like
first_success_namespace (or find the first element in the restore results with
non-empty downloads/success), then use that namespace when calling
cspp.save_master_key(&...master_key) and
keychain.save_cspp_passkey_and_namespace(... &active.namespace_id) (also apply
the same change to the analogous block referenced around the 1062-1069 region)
so persisted master key/passkey/namespace matches the namespace that actually
restored data.

---

Duplicate comments:
In `@ios/Cove/AppManager.swift`:
- Around line 198-205: The selectLatestOrNewWallet function currently swallows
dispatch errors by catching and only logging them; change it to propagate
failures so callers can react—either make selectLatestOrNewWallet a throwing
method that calls try rust.dispatch(action: .selectLatestOrNewWallet) without
catching (or rethrow the caught error), or add a completion/Result callback
parameter to return success/failure; ensure beginNavigationIntent() remains
invoked and preserve the rust.dispatch(action: .selectLatestOrNewWallet)
reference while removing the silent catch so callers can retry/alert/fallback.

---

Nitpick comments:
In `@android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt`:
- Around line 30399-30418: Add a short note to AGENTS.md documenting the tight
ordinal contract between Kotlin and Rust for AppAction (the enum/variant
ordering used in the switch/when that maps ordinals like 4 ->
AppAction.SelectWallet, 5 -> AppAction.SelectLatestOrNewWallet, 6 ->
AppAction.ChangeNetwork, etc.), warning developers not to reorder, insert, or
remove variants without regenerating bindings and updating the checksum; briefly
list the common pitfalls (reordering, missing regen, checksum update) and where
to update (reference AppAction and the FfiConverterType* readers) so future
maintainers know the exact steps to keep Kotlin/Rust serialization in sync.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 39affd77-239e-4fff-a918-69e4c6c16242

📥 Commits

Reviewing files that changed from the base of the PR and between e5df590 and 8a88ad5.

⛔ Files ignored due to path filters (1)
  • ios/CoveCore/Sources/CoveCore/generated/cove.swift is excluded by !**/generated/**
📒 Files selected for processing (9)
  • android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt
  • android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt
  • ios/Cove/AppManager.swift
  • rust/src/app.rs
  • rust/src/manager/cloud_backup_manager/ops.rs
  • rust/src/manager/cloud_backup_manager/verify/passkey_auth.rs
  • rust/src/manager/cloud_backup_manager/verify/pending_completion.rs
  • rust/src/manager/cloud_backup_manager/wallets/passkey.rs
  • rust/src/manager/onboarding_manager.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • rust/src/manager/cloud_backup_manager/verify/pending_completion.rs

Comment thread android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt Outdated
Comment thread rust/src/manager/cloud_backup_manager/ops.rs Outdated
Split sidebar navigation triggers from low-level route updates and add safe wallet-selection entrypoints across Android and iOS.
Add a dedicated unexpected wallet creation error and propagate it to generated Kotlin and Swift UniFFI bindings. Update restore flow ordering so the active cloud namespace tracks the first successful restore wallet.
@praveenperera praveenperera merged commit 75e2b72 into master Apr 29, 2026
10 checks passed
@praveenperera praveenperera deleted the refactor-and-navigation-fix branch April 29, 2026 15:03
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.

1 participant