From 6083a469e31be6e3417d5cea8c83c42f09d83ab8 Mon Sep 17 00:00:00 2001 From: Wei Yang Date: Sat, 15 Nov 2025 18:03:08 -0600 Subject: [PATCH] Fix: Prevent extension breakage when exiting wallet setup halfway MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes bug where exiting wallet setup partway through would cause: - Black screen flickering - Infinite loading spinner - Unusable popup Root cause: Incomplete wallet setup data remained in localStorage with no cleanup, causing RequireUserUnlocked to return null (black screen) and race conditions when opening/closing popups. Changes: - Add cleanupIncompleteOnboarding() to SecureStore for auto-cleanup of partial state - Integrate cleanup in KeyringStore.state() before returning NeedsOnboarding - Fix RequireUserUnlocked to show loading spinner instead of null/black screen - Open onboarding in full tab (not popup) matching first install behavior The extension now automatically detects and cleans up incomplete setup state, then opens onboarding in a full browser tab exactly like first install. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/store/KeyringStore/KeyringStore.ts | 4 ++ .../src/store/SecureStore.ts | 41 +++++++++++++++++++ .../RequireUserUnlocked.tsx | 27 ++++++++---- yarn.lock | 31 +++++--------- 4 files changed, 75 insertions(+), 28 deletions(-) diff --git a/packages/secure-background/src/store/KeyringStore/KeyringStore.ts b/packages/secure-background/src/store/KeyringStore/KeyringStore.ts index d5d4701..00d0bd5 100644 --- a/packages/secure-background/src/store/KeyringStore/KeyringStore.ts +++ b/packages/secure-background/src/store/KeyringStore/KeyringStore.ts @@ -126,6 +126,10 @@ export class KeyringStore { if (await this.isLocked()) { return KeyringStoreState.Locked; } + + // Before returning NeedsOnboarding, check for and cleanup incomplete onboarding state + await this.store.cleanupIncompleteOnboarding(); + return KeyringStoreState.NeedsOnboarding; } diff --git a/packages/secure-background/src/store/SecureStore.ts b/packages/secure-background/src/store/SecureStore.ts index fc8777a..1d4bca7 100644 --- a/packages/secure-background/src/store/SecureStore.ts +++ b/packages/secure-background/src/store/SecureStore.ts @@ -663,6 +663,47 @@ export class SecureStore { return ciphertext !== undefined && ciphertext !== null; } + /** + * Detects and cleans up incomplete onboarding state. + * Returns true if incomplete state was found and cleaned up. + */ + async cleanupIncompleteOnboarding(): Promise { + try { + // Check if user data exists + const userData = await this.persistentDB.get( + PersistentStorageKeys.STORE_KEY_USER_DATA + ); + + if (!userData || !userData.activeUser) { + return false; // No user data, nothing to cleanup + } + + // Check if ciphertext (encrypted keyring) exists + const hasCiphertext = await this.doesCiphertextExist(); + + // Check if user has any public keys + const publicKeys = await this.getUserPublicKeys(userData.activeUser.uuid); + const hasPublicKeys = + publicKeys && Object.keys(publicKeys.platforms || {}).length > 0; + + // If user data exists but no ciphertext or no public keys, it's incomplete + if (!hasCiphertext || !hasPublicKeys) { + console.log("Detected incomplete onboarding state, cleaning up..."); + + // Clean up all partial data + await this.reset(); + + return true; + } + + return false; + } catch (error) { + // If any error occurs during detection, don't cleanup + console.error("Error detecting incomplete onboarding:", error); + return false; + } + } + async setKeyringStore( json: KeyringStoreJson, password: string diff --git a/packages/secure-ui/src/RequireUserUnlocked/RequireUserUnlocked.tsx b/packages/secure-ui/src/RequireUserUnlocked/RequireUserUnlocked.tsx index 609c0fc..1c84611 100644 --- a/packages/secure-ui/src/RequireUserUnlocked/RequireUserUnlocked.tsx +++ b/packages/secure-ui/src/RequireUserUnlocked/RequireUserUnlocked.tsx @@ -1,6 +1,3 @@ -import type { ReactNode } from "react"; -import { memo, useEffect, useMemo, useState } from "react"; - import { openOnboarding } from "@coral-xyz/common"; import { userClientAtom, @@ -9,10 +6,13 @@ import { } from "@coral-xyz/recoil"; import { KeyringStoreState } from "@coral-xyz/secure-clients/types"; import { BanIcon } from "@coral-xyz/tamagui"; +import { memo, useEffect, useMemo, useState } from "react"; +import type { ReactNode } from "react"; import { useRecoilState, useRecoilValue, useRecoilValueLoadable } from "recoil"; import { LoginRequest } from "./LoginRequest"; import { ErrorMessage } from "../_sharedComponents/ErrorMessage"; +import { Loading } from "../_sharedComponents/Loading"; import { WithMotion } from "../_sharedComponents/WithMotion"; export function RequireUserUnlocked({ @@ -55,16 +55,29 @@ export function RequireUserUnlocked({ useEffect(() => { if (!disabled && keyringState === KeyringStoreState.NeedsOnboarding) { - openOnboarding(); - onReset?.(); + // Check if we're not already on the onboarding page + const isOnOnboardingPage = + window.location.search.includes("onboarding=true"); // eslint-disable-line no-restricted-properties + if (!isOnOnboardingPage) { + // Open onboarding in full tab (like first install) instead of popup + const url = globalThis.chrome?.runtime?.getURL( + "options.html?onboarding=true" + ); + if (url) { + globalThis.chrome?.tabs?.create({ url }); + // Close the popup after opening the tab + window.close(); + } + } } - }, [disabled, onReset, keyringState]); + }, [disabled, keyringState]); if (disabled) { return <>{children}; } if (keyringState === KeyringStoreState.NeedsOnboarding) { - return null; + // Show loading spinner instead of black screen while opening onboarding + return ; } if (showLogin) { diff --git a/yarn.lock b/yarn.lock index 6141c92..369554e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30484,10 +30484,10 @@ __metadata: languageName: node linkType: hard -"isexe@npm:^2.0.0": - version: 2.0.0 - resolution: "isexe@npm:2.0.0" - checksum: 7c9f715c03aff08f35e98b1fadae1b9267b38f0615d501824f9743f3aab99ef10e303ce7db3f186763a0b70a19de5791ebfc854ff884d5a8c4d92211f642ec92 +"isexe@npm:^3.1.1": + version: 3.1.1 + resolution: "isexe@npm:3.1.1" + checksum: 7fe1931ee4e88eb5aa524cd3ceb8c882537bc3a81b02e438b240e47012eef49c86904d0f0e593ea7c3a9996d18d0f1f3be8d3eaa92333977b0c3a9d353d5563e languageName: node linkType: hard @@ -46174,25 +46174,14 @@ __metadata: languageName: node linkType: hard -"which@npm:^1.2.12, which@npm:^1.2.14, which@npm:^1.2.9": - version: 1.3.1 - resolution: "which@npm:1.3.1" - dependencies: - isexe: "npm:^2.0.0" - bin: - which: ./bin/which - checksum: 549dcf1752f3ee7fbb64f5af2eead4b9a2f482108b7de3e85c781d6c26d8cf6a52d37cfbe0642a155fa6470483fe892661a859c03157f24c669cf115f3bbab5e - languageName: node - linkType: hard - -"which@npm:^2.0.1, which@npm:^2.0.2": - version: 2.0.2 - resolution: "which@npm:2.0.2" +"which@npm:6.0.0": + version: 6.0.0 + resolution: "which@npm:6.0.0" dependencies: - isexe: "npm:^2.0.0" + isexe: "npm:^3.1.1" bin: - node-which: ./bin/node-which - checksum: 4782f8a1d6b8fc12c65e968fea49f59752bf6302dc43036c3bf87da718a80710f61a062516e9764c70008b487929a73546125570acea95c5b5dcc8ac3052c70f + node-which: bin/which.js + checksum: df19b2cd8aac94b333fa29b42e8e371a21e634a742a3b156716f7752a5afe1d73fb5d8bce9b89326f453d96879e8fe626eb421e0117eb1a3ce9fd8c97f6b7db9 languageName: node linkType: hard