Skip to content

Add wallet recovery word status tracking and error handling#665

Open
kartikangiras wants to merge 1 commit intobitcoinppl:masterfrom
kartikangiras:seedwords
Open

Add wallet recovery word status tracking and error handling#665
kartikangiras wants to merge 1 commit intobitcoinppl:masterfrom
kartikangiras:seedwords

Conversation

@kartikangiras
Copy link
Copy Markdown
Contributor

@kartikangiras kartikangiras commented Apr 16, 2026

Summary

Display a clear warning in My Wallets when a hot wallet's recovery words are unavailable on device. This addresses the issue where users restore to a new iPhone and don't realize their seed words are missing from the Secure Enclave which doesn't transfer between devices.

Fixes #316

Testing

  • just build-ios
  • just ci
  • just fmt

Platform Coverage

  • Tested on iOS device
  • Tested on Android device
  • Tested on iOS simulator
  • Tested on Android simulator
  • Not tested
    Recovery words warning only appears when keychain entry is missing which happens in device restore. Could not simulate this in simulator without manual keychain deletion.

Checklist

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced error messaging for recovery words with improved visual styling, including color-coded alerts for better clarity.
    • Fixed wallet item layout in sidebar to prevent text overflow and added border styling.
    • QR code button now displays only when recovery words are available.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 16, 2026

Warning

Rate limit exceeded

@kartikangiras has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 25 minutes and 8 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 25 minutes and 8 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ffc67e57-acd1-41d4-9494-2514a0af5e61

📥 Commits

Reviewing files that changed from the base of the PR and between 8f54680 and 40dae3c.

📒 Files selected for processing (3)
  • android/app/src/main/java/org/bitcoinppl/cove/secret_words/SecretWordsScreen.kt
  • ios/Cove/Flows/SelectedWalletFlow/SecretWordsScreen.swift
  • ios/Cove/Views/SidebarView.swift
📝 Walkthrough

Walkthrough

Enhanced error handling in SecretWordsScreen by replacing generic errorMessage state with typed MnemonicError state and derived user-facing messaging. Added explicit error styling with red-tinted background, border, and callout font for improved visibility. Updated toolbar QR button visibility and refined SidebarView wallet item layout with spacing adjustments.

Changes

Cohort / File(s) Summary
Error Handling & Styling
ios/Cove/Flows/SelectedWalletFlow/SecretWordsScreen.swift
Replaced single errorMessage state with loadError: MnemonicError? (private) and fallbackErrorMessage: String?. Added recoveryWordsErrorMessage computed property that maps specific error cases (.NotAvailable, .GetWalletKeychain, .UnknownWord) to user-facing messages. Enhanced UI rendering with red-tinted background, red border, callout font, and padding. Updated onAppear to catch MnemonicError and separate mnemonic-specific errors from fallback errors. Changed QR toolbar button visibility to only display when words are loaded.
Layout Refinements
ios/Cove/Views/SidebarView.swift
Added Spacer(minLength: 0) after wallet names to prevent text from consuming remaining horizontal space. Applied .overlay with RoundedRectangle stroke styling to wallet items for consistent border appearance.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

Poem

🐰 With whiskers twitched and errors caught in sight,
Missing words now glow in warning's light!
Red borders frame the messages clear,
No more confusion, just help drawing near! 🔴✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The SidebarView.swift changes (Spacer insertion and overlay styling) appear tangential to the recovery words error handling objective from issue #316; they seem unrelated to the main scope of tracking missing seed words. Clarify or remove the SidebarView styling changes, or document their connection to the missing seed words warning feature in the PR description.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: adding wallet recovery word status tracking and error handling, which aligns with the PR's objective of detecting missing seed words.
Description check ✅ Passed The description includes a clear summary of the change, testing details with platform coverage, and completed checklist items, meeting the repository's template requirements.
Linked Issues check ✅ Passed The code changes implement the requirements from issue #316: detecting missing mnemonics via MnemonicError, displaying error messages in the recovery words screen, and surfacing warnings when keychain entries are unavailable.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 16, 2026

Greptile Summary

This PR tracks whether hot wallet recovery words are accessible on the device and surfaces a warning in the sidebar and on the SecretWordsScreen when they are not — a clear UX improvement for users who restore to a new iPhone and lose their keychain seed data. The implementation cleanly hooks into the existing event-reconciliation pattern in AppManager.

  • "Loading…" rendered inside the red error box: Before onAppear runs and populates words or loadError, the else branch in SecretWordsScreen renders the error-styled red box with the text "Loading…", creating a brief visual artifact that looks like an error state.
  • Synchronous keychain I/O on the main thread: refreshWalletRecoveryWordStatus() calls Mnemonic(id:) — a blocking Rust FFI / keychain read — for every hot wallet synchronously inside DispatchQueue.main.async handlers and during init(). With a large number of wallets or slow keychain access (common post-device-restore), this can cause main-thread jank.

Confidence Score: 5/5

Safe to merge; all findings are P2 style/polish suggestions with no blocking correctness or security issues.

All three comments are P2 (style, UX polish, and a minor visibility concern). No data loss, broken logic, or security issues were found. The core feature — detecting missing recovery words via MnemonicError.NotAvailable and surfacing the warning in the sidebar — is implemented correctly and follows the existing AppManager reconcile pattern.

ios/Cove/AppManager.swift (synchronous keychain reads on main thread) and ios/Cove/Flows/SelectedWalletFlow/SecretWordsScreen.swift (loading state rendered with error styling).

Important Files Changed

Filename Overview
ios/Cove/AppManager.swift Adds walletsMissingRecoveryWords set and refreshWalletRecoveryWordStatus() helper; called from init and all reconcile events that touch wallet data — synchronous keychain reads on the main thread are the main concern.
ios/Cove/Flows/SelectedWalletFlow/SecretWordsScreen.swift Replaces generic errorMessage with typed MnemonicError state and improves error messaging; "Loading…" initial state is incorrectly styled with the red error box before onAppear populates the actual state.
ios/Cove/Views/SidebarView.swift Adds per-wallet missing-recovery-words indicator (red background, orange triangle label) cleanly using the new walletMissingRecoveryWords helper.

Sequence Diagram

sequenceDiagram
    participant App as App Startup / Event
    participant AM as AppManager
    participant KC as Keychain (Mnemonic FFI)
    participant SB as SidebarView
    participant SW as SecretWordsScreen

    App->>AM: init() / reconcile(.walletsChanged)
    AM->>AM: refreshWalletRecoveryWordStatus()
    loop each hot wallet
        AM->>KC: Mnemonic(id:) [blocking, main thread]
        alt NotAvailable
            KC-->>AM: throws MnemonicError.NotAvailable
            AM->>AM: missingWalletIds.insert(wallet.id)
        else success
            KC-->>AM: Mnemonic object (discarded)
        end
    end
    AM->>AM: walletsMissingRecoveryWords = missingWalletIds

    SB->>AM: walletMissingRecoveryWords(wallet)
    AM-->>SB: Bool
    alt missing
        SB->>SB: show red bg + orange warning label
    end

    SW->>SW: onAppear → auth.lock()
    SW->>KC: Mnemonic(id:) [blocking, main thread]
    alt success
        KC-->>SW: words set → show word grid + QR button
    else MnemonicError.NotAvailable
        KC-->>SW: loadError = .NotAvailable → show red error box
    else MnemonicError.GetWalletKeychain
        KC-->>SW: loadError = .GetWalletKeychain → show red error box
    else other
        KC-->>SW: fallbackErrorMessage → show red error box
    end
Loading

Reviews (1): Last reviewed commit: "Add wallet recovery word status tracking..." | Re-trigger Greptile

Comment on lines 76 to 89
} else {
Text(errorMessage ?? "Loading...")
Text(recoveryWordsErrorMessage)
.multilineTextAlignment(.leading)
.font(.callout)
.foregroundStyle(.white.opacity(0.95))
.padding(16)
.frame(maxWidth: screenWidth * 0.9, alignment: .leading)
.background(Color.red.opacity(0.16))
.overlay {
RoundedRectangle(cornerRadius: 10)
.stroke(Color.red.opacity(0.30), lineWidth: 1)
}
.cornerRadius(10)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 "Loading…" rendered with red error styling

The else branch is entered whenever words == nil, which includes the initial render frame before onAppear has had a chance to populate either words or loadError. At that point recoveryWordsErrorMessage returns "Loading…", so users briefly see the red-border error box with "Loading…" — indistinguishable from an actual error. On slower devices or if keychain access is delayed, this persists visibly.

Consider separating the loading state from the error state:

} else if loadError != nil || fallbackErrorMessage != nil {
    Text(recoveryWordsErrorMessage)
        .multilineTextAlignment(.leading)
        .font(.callout)
        .foregroundStyle(.white.opacity(0.95))
        .padding(16)
        .frame(maxWidth: screenWidth * 0.9, alignment: .leading)
        .background(Color.red.opacity(0.16))
        .overlay {
            RoundedRectangle(cornerRadius: 10)
                .stroke(Color.red.opacity(0.30), lineWidth: 1)
        }
        .cornerRadius(10)
} else {
    ProgressView()
        .tint(.white)
}

Comment thread ios/Cove/AppManager.swift Outdated
Comment thread ios/Cove/AppManager.swift Outdated
var router: Router
var database: Database
var wallets: [WalletMetadata] = []
var walletsMissingRecoveryWords: Set<WalletId> = []
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 walletsMissingRecoveryWords should be private

Since walletMissingRecoveryWords(_ wallet:) is already the intended public read interface, the backing Set can be made private to prevent external callers from observing or mutating it directly.

Suggested change
var walletsMissingRecoveryWords: Set<WalletId> = []
private var walletsMissingRecoveryWords: Set<WalletId> = []

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

}

var recoveryWordsErrorMessage: String {
if let loadError {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

combine if let with switch just match directly in switch

Comment thread ios/Cove/AppManager.swift Outdated
var router: Router
var database: Database
var wallets: [WalletMetadata] = []
var walletsMissingRecoveryWords: Set<WalletId> = []
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why do we need this and refreshing recovery words status on each change to wallets in AppManager? Just check if theres an error in the SecretWordsScreen

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 (1)
ios/Cove/Views/SidebarView.swift (1)

79-87: Avoid competing flexible width in the wallet row.

Text(wallet.name) already expands with .frame(maxWidth: .infinity, alignment: .leading), so the added Spacer(minLength: 0) is redundant and can make long wallet names truncate/scale earlier. Keep one flexible layout mechanism.

♻️ Proposed cleanup
 Text(wallet.name)
     .font(.footnote)
     .fontWeight(.medium)
     .foregroundStyle(.white)
     .lineLimit(1)
     .minimumScaleFactor(0.80)
     .frame(maxWidth: .infinity, alignment: .leading)
-
- Spacer(minLength: 0)

As per coding guidelines, ios/Cove/**/*.swift: “Review SwiftUI view code for proper layout, best practices”.

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

In `@ios/Cove/Views/SidebarView.swift` around lines 79 - 87, The wallet row uses
two competing flexible width mechanisms—Text(wallet.name).frame(maxWidth:
.infinity, alignment: .leading) and Spacer(minLength: 0)—which causes premature
truncation/scaling; remove the redundant Spacer(minLength: 0) (or alternatively
remove the .frame(maxWidth: .infinity) and keep the Spacer) so only one flexible
layout mechanism remains, updating the wallet row around Text(wallet.name) to
rely on a single approach.
🤖 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/Flows/SelectedWalletFlow/SecretWordsScreen.swift`:
- Around line 30-40: The computed property recoveryWordsErrorMessage treats the
loading state as an error ("Loading...") so the UI callout always shows; change
it to return an optional String (or return nil) for the loading case and only
provide actual error strings for real loadError cases (.NotAvailable,
.GetWalletKeychain, .UnknownWord) or fallbackErrorMessage when present; then
update the view that renders the red error callout (where
recoveryWordsErrorMessage is consumed) to only show the callout when
recoveryWordsErrorMessage is non-nil/non-empty (i.e., when a real error exists)
so normal loading remains neutral.

In `@ios/Cove/Views/SidebarView.swift`:
- Around line 93-96: The overlay using RoundedRectangle(cornerRadius:
10).stroke(Color.clear, lineWidth: 1) inside SidebarView is a no-op and should
either be removed or replaced with a visible warning that is conditionally shown
based on the wallet’s recovery-word availability; update the SidebarView (or the
specific view that contains the .overlay call) to check the wallet/recoveryWords
state and, when recovery words are missing, replace the clear stroke with a
visible badge/overlay (e.g., .stroke(Color.red, lineWidth: 2) or a small
Text/Image warning inside a ZStack) and ensure the UI binds to the wallet model
so the warning appears and disappears correctly.

---

Nitpick comments:
In `@ios/Cove/Views/SidebarView.swift`:
- Around line 79-87: The wallet row uses two competing flexible width
mechanisms—Text(wallet.name).frame(maxWidth: .infinity, alignment: .leading) and
Spacer(minLength: 0)—which causes premature truncation/scaling; remove the
redundant Spacer(minLength: 0) (or alternatively remove the .frame(maxWidth:
.infinity) and keep the Spacer) so only one flexible layout mechanism remains,
updating the wallet row around Text(wallet.name) to rely on a single approach.
🪄 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: 5cc76d53-8aa9-4f92-9160-5ecf8e35b7a2

📥 Commits

Reviewing files that changed from the base of the PR and between ba61cd2 and 8f54680.

📒 Files selected for processing (2)
  • ios/Cove/Flows/SelectedWalletFlow/SecretWordsScreen.swift
  • ios/Cove/Views/SidebarView.swift

Comment on lines +30 to +40
var recoveryWordsErrorMessage: String {
switch loadError {
case .some(.NotAvailable):
"Recovery words are unavailable on this device for this wallet. This can happen after restoring to a new iPhone because Secure Enclave data does not transfer. Restore this wallet from your saved recovery words before sending more BTC to it."
case .some(.GetWalletKeychain):
"Cove could not access this wallet's recovery words in the keychain right now."
case .some(.UnknownWord):
"Cove could not decode this wallet's recovery words."
case .none:
fallbackErrorMessage ?? "Loading..."
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Don’t render the loading state as an error.

Line 39 returns "Loading...", but lines 75-86 always display it in the red error callout. This can flash a false warning during normal load; keep loading neutral and reserve the callout for real errors.

Proposed adjustment
-    var recoveryWordsErrorMessage: String {
+    var recoveryWordsErrorMessage: String? {
         switch loadError {
         case .some(.NotAvailable):
             "Recovery words are unavailable on this device for this wallet. This can happen after restoring to a new iPhone because Secure Enclave data does not transfer. Restore this wallet from your saved recovery words before sending more BTC to it."
         case .some(.GetWalletKeychain):
             "Cove could not access this wallet's recovery words in the keychain right now."
         case .some(.UnknownWord):
             "Cove could not decode this wallet's recovery words."
         case .none:
-            fallbackErrorMessage ?? "Loading..."
+            fallbackErrorMessage
         }
     }
                 } else {
-                    Text(recoveryWordsErrorMessage)
-                        .multilineTextAlignment(.leading)
-                        .font(.callout)
-                        .foregroundStyle(.white.opacity(0.95))
-                        .padding(16)
-                        .frame(maxWidth: screenWidth * 0.9, alignment: .leading)
-                        .background(Color.red.opacity(0.16))
-                        .overlay {
-                            RoundedRectangle(cornerRadius: 10)
-                                .stroke(Color.red.opacity(0.30), lineWidth: 1)
-                        }
-                        .cornerRadius(10)
+                    if let recoveryWordsErrorMessage {
+                        Text(recoveryWordsErrorMessage)
+                            .multilineTextAlignment(.leading)
+                            .font(.callout)
+                            .foregroundStyle(.white.opacity(0.95))
+                            .padding(16)
+                            .frame(maxWidth: screenWidth * 0.9, alignment: .leading)
+                            .background(Color.red.opacity(0.16))
+                            .overlay {
+                                RoundedRectangle(cornerRadius: 10)
+                                    .stroke(Color.red.opacity(0.30), lineWidth: 1)
+                            }
+                            .cornerRadius(10)
+                    } else {
+                        Text("Loading...")
+                            .font(.callout)
+                            .foregroundStyle(.white.opacity(0.75))
+                    }
                 }

Also applies to: 75-86

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

In `@ios/Cove/Flows/SelectedWalletFlow/SecretWordsScreen.swift` around lines 30 -
40, The computed property recoveryWordsErrorMessage treats the loading state as
an error ("Loading...") so the UI callout always shows; change it to return an
optional String (or return nil) for the loading case and only provide actual
error strings for real loadError cases (.NotAvailable, .GetWalletKeychain,
.UnknownWord) or fallbackErrorMessage when present; then update the view that
renders the red error callout (where recoveryWordsErrorMessage is consumed) to
only show the callout when recoveryWordsErrorMessage is non-nil/non-empty (i.e.,
when a real error exists) so normal loading remains neutral.

Comment on lines +93 to +96
.overlay {
RoundedRectangle(cornerRadius: 10)
.stroke(Color.clear, lineWidth: 1)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make the recovery-words warning visible, or remove this no-op overlay.

This Color.clear stroke is invisible and does not affect layout, so it won’t provide the “My Wallets” warning required by the PR objective. If this is the intended warning hook, wire it to the wallet’s recovery-word availability and use visible styling/content.

🛠️ Example direction
 .background(Color.coveLightGray.opacity(0.06))
-.overlay {
-    RoundedRectangle(cornerRadius: 10)
-        .stroke(Color.clear, lineWidth: 1)
-}
+.overlay {
+    if wallet.recoveryWordsUnavailable {
+        RoundedRectangle(cornerRadius: 10)
+            .stroke(Color.red, lineWidth: 1)
+    }
+}

As per coding guidelines, ios/Cove/**/*.swift: “Review SwiftUI view code for proper layout, best practices”.

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

In `@ios/Cove/Views/SidebarView.swift` around lines 93 - 96, The overlay using
RoundedRectangle(cornerRadius: 10).stroke(Color.clear, lineWidth: 1) inside
SidebarView is a no-op and should either be removed or replaced with a visible
warning that is conditionally shown based on the wallet’s recovery-word
availability; update the SidebarView (or the specific view that contains the
.overlay call) to check the wallet/recoveryWords state and, when recovery words
are missing, replace the clear stroke with a visible badge/overlay (e.g.,
.stroke(Color.red, lineWidth: 2) or a small Text/Image warning inside a ZStack)
and ensure the UI binds to the wallet model so the warning appears and
disappears correctly.

Comment on lines +18 to +19
@State private var loadError: MnemonicError?
@State private var fallbackErrorMessage: String?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can you explain why we have 2 error messages here? can we use an enum? and we need to bring the change to android too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

my initial thought was to have one err check for rust and other the normal try catch, but it was a redundant approach so i shifted to using enum and also applied similar changes to android as well.

@praveenperera
Copy link
Copy Markdown
Contributor

@kartikangiras can you respond to my comments inline, and resolve or respond to the bots too thanks

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.

Need better "missing seed words" warning

2 participants