Skip to content

fix(app): surface cloud restore failures to the user and telemetry (SELF-3932) - #2273

Open
seshanthS wants to merge 2 commits into
fix/self-3931-recovery-protocol-datafrom
self-3932-restore-failure-visibility
Open

fix(app): surface cloud restore failures to the user and telemetry (SELF-3932)#2273
seshanthS wants to merge 2 commits into
fix/self-3931-recovery-protocol-datafrom
self-3932-restore-failure-visibility

Conversation

@seshanthS

Copy link
Copy Markdown
Collaborator

Closes SELF-3932.

Stacked on #2272 (SELF-3931). Base is fix/self-3931-recovery-protocol-data; retarget to dev once that merges.

Problem

AccountRecoveryChoiceScreen caught every cloud restore failure, logged to console, fired an event and cleared the spinner — and rendered nothing. The user saw a spinner, then an idle button, with nothing to report to support. 1787 of 1938 Cloud Restore Failed: Unknown Error events in the last 30 days carried no properties, so the failing branch was unknowable, and there was no restore-attempted event to serve as a denominator.

Two adjacent gating bugs on the same screen: biometricsAvailable was a device capability stored in persisted settings, so a stale false survived across launches and kept the cloud button permanently disabled; and it was only ever written by a fire-and-forget check at splash.

Changes

  • Typed cloud-backup errors — new app/src/services/cloud-backup/errors.ts with CloudBackupError carrying a reason (sign_in_cancelled, cloud_unavailable, no_backup_found, backup_corrupt, backup_read_failed). Every throw site in index.ts/ios.ts is classified. Messages are byte-identical to before, so the existing prose assertions act as the regression guard.
  • iOS signed-out is now distinguishableios.ts download() calls CloudStorage.isCloudAvailable() before exists(). A signed-out device resolves exists as false, so without this guard it reported "no backup found" instead of "sign in to iCloud".
  • Choice-screen error state — local error union mirroring RecoverWithPhraseScreen, rendered beneath the description. Built on CloudBackupErrorReason, so any new download failure is renderable by construction.
  • Events — new CLOUD_RESTORE_STARTED, fired imperatively once per attempt; the CLOUD_BACKUP_STARTED prop is removed from the recover button. Untyped download failures keep reason: 'backup_download_failed' so they stay separable from unexpected_error.
  • Copy — shared recoveryCopy.errors superset; the phrase screen's inline ERROR_MESSAGES moved there verbatim. The Android↔iOS backup incompatibility is stated in no_backup_found, the one branch a platform-switcher actually hits.
  • De-persisted biometricsAvailable — moved to NonPersistedSettingsState, excluded from partialize, SETTING_STORE_VERSION bumped to 2 with a migration that deletes the stale key. partialize alone only stops future writes; the already-persisted value would still rehydrate once and clobber the fresh check.
  • useBiometricsAvailability — re-reads capability on focus and on AppState active. Both are needed: the OS-settings round trip never unfocuses the screen, and foreground alone misses in-app navigation onto it. isSensorAvailable() is a capability query, so no biometric prompt is shown.
  • No inert button — the cloud button stays visible but disabled, with the explanation moved out of the shared description to directly beneath it, naming the OS-settings remedy. Phrase path untouched.

Notes for reviewers

  • CloudBackupScreen also reads biometricsAvailable. It now reads false until the splash check resolves, where it previously read a sticky persisted value. This widens an existing SplashScreen ordering race rather than creating one; the recovery screen is immunised by the new hook. Extending the hook to CloudBackupScreen is a follow-up.
  • google.ts googleSignIn() swallows every authorize() failure, so a genuine Android auth or network failure reports as sign_in_cancelled. react-native-app-auth exposes no cancel sentinel in its JS surface, so splitting them means string-matching native errors. Follow-up, not scope.
  • cloud_unavailable is iOS-only in practice — Google Drive always reports available — so there is no Android repro for testing instruction 2.
  • The focus/foreground re-checks increase biometric_sensor_checked log volume.
  • CloudStorageError.code is deliberately not mapped: withRetries replaces the original error, so every classifiable branch throws outside it and anything escaping the retry wrapper is unclassifiable by construction.

Validation

pnpm --filter @selfxyz/mobile-sdk-alpha test      # 528 passed
pnpm --filter @selfxyz/mobile-sdk-alpha types     # clean
madge src/browser.ts | grep react-native          # prints nothing
pnpm --filter @selfxyz/mobile-sdk-alpha build     # required before app types
pnpm --filter @selfxyz/mobile-app run test        # 1257 passed, 111 suites
pnpm --filter @selfxyz/mobile-app run types       # clean
pnpm --filter @selfxyz/mobile-app run lint        # 0 errors

New coverage: per-reason download classification (incl. exists never called when iCloud is unavailable), one test per rendered failure branch with its analytics reason, CLOUD_RESTORE_STARTED exactly once and CLOUD_BACKUP_STARTED never, error clearing on retry, the v2 migration, and the focus/foreground re-check with listener cleanup.

Device QA still owed — these cannot be unit-tested: iOS signed out of iCloud (instruction 2), cancelling the real Google sign-in sheet (instruction 3), and toggling biometrics in OS settings and returning (instruction 5).

🤖 Generated with Claude Code

@vercel

vercel Bot commented Aug 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
self-webview-app Ignored Ignored Preview Aug 24, 2026 2:19pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (3)
  • main
  • dev
  • staging

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ee3d2fea-b7c1-443a-bb80-ec31d076bb49

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@seshanthS

Copy link
Copy Markdown
Collaborator Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aaa331e764

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

* error with a fresh one, so every classifiable branch must throw outside it —
* whatever escapes the retry wrapper is unclassifiable by construction.
*/
| 'backup_read_failed';

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 Badge Wrap provider read failures in the declared error type

When Google Drive list/getText or iCloud isCloudAvailable/exists/readFile rejects, such as during a transient network or provider failure, the plain error bypasses every CloudBackupError throw site. The screen consequently records backup_download_failed and displays unexpected_error, while the newly declared backup_read_failed reason is never produced by production code, defeating the branch-specific copy and telemetry for storage failures. Catch these provider errors and rethrow CloudBackupError('backup_read_failed', ...).

Useful? React with 👍 / 👎.

Comment on lines +36 to +40
checkBiometricsAvailable()
.then(available => {
if (active) {
setBiometricsAvailable(available);
}

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 Badge Preserve availability when the real capability check fails

When biometrics.isSensorAvailable() throws during a focus or foreground refresh, the production checkBiometricsAvailable implementation catches that exception and resolves false, so this .then overwrites a previously valid true value and the .catch intended to preserve it is never reached. A transient native error after returning to the app therefore disables cloud recovery until another successful refresh; make the provider expose an indeterminate/failure result and update the store only for an actual capability result. The added test currently mocks a rejection that the real provider never produces.

AGENTS.md reference: AGENTS.md:L24-L24

Useful? React with 👍 / 👎.

Comment on lines +105 to +108
trackEvent(BackupEvents.CLOUD_RESTORE_FAILED_UNKNOWN, {
reason: 'restore_failed',
});
setError('restore_failed');

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 Badge Report keychain failures instead of blaming the cloud phrase

In the cloud path, download() has already parsed and validated the BIP39 mnemonic before this call, while restoreFromMnemonic catches failures from Keychain.setGenericPassword and returns false. This newly surfaced branch consequently shows copy telling the user to double-check a phrase they did not enter and that has already been validated, while telemetry records only restore_failed; on a keychain write or security-configuration failure the suggested remedy cannot help. Propagate and classify the secure-storage failure so the screen can present an actionable message.

Useful? React with 👍 / 👎.

seshanthS and others added 2 commits August 24, 2026 19:49
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@seshanthS
seshanthS force-pushed the self-3932-restore-failure-visibility branch from d3543b5 to f762794 Compare August 24, 2026 14:19
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