From fb52c9f166139985f0d26e09e4fdfcd3dc0bfe37 Mon Sep 17 00:00:00 2001 From: Ross Hartmann <1750494+RossHartmann@users.noreply.github.com> Date: Sat, 22 Aug 2026 00:38:56 -0700 Subject: [PATCH 1/2] fix(desktop): preserve existing identity onboarding context Signed-off-by: Ross Hartmann <1750494+RossHartmann@users.noreply.github.com> --- desktop/src/app/App.tsx | 1 + .../onboarding/machineOnboarding.test.mjs | 12 ++++++++++ .../features/onboarding/machineOnboarding.ts | 22 +++++++++++++++++++ .../onboarding/ui/MachineOnboardingFlow.tsx | 4 +++- 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/desktop/src/app/App.tsx b/desktop/src/app/App.tsx index bfaf2ba2008..f82c47afd99 100644 --- a/desktop/src/app/App.tsx +++ b/desktop/src/app/App.tsx @@ -758,6 +758,7 @@ function MachineBootstrap({ sharedIdentity }: { sharedIdentity: boolean }) { complete={completeMachineOnboarding} continueWithIdentity={machine.continueWithIdentity} continueWithRecoveredIdentity={machine.continueWithRecoveredIdentity} + existingIdentityPubkey={machine.existingIdentityPubkey} identityLost={machine.identityLost} initialPage={machineInitialPage} navigateAfterComplete={navigateAfterOnboarding} diff --git a/desktop/src/features/onboarding/machineOnboarding.test.mjs b/desktop/src/features/onboarding/machineOnboarding.test.mjs index ec564d83d1c..e6d712f08c1 100644 --- a/desktop/src/features/onboarding/machineOnboarding.test.mjs +++ b/desktop/src/features/onboarding/machineOnboarding.test.mjs @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { + existingMachineIdentityPubkey, migrateMachineOnboardingCompletion, readMachineOnboardingCompletion, } from "./machineOnboarding.ts"; @@ -46,6 +47,17 @@ const PUBKEY_B = const LEGACY_KEY = `buzz-onboarding-complete.v1:${PUBKEY_A}`; const V2_KEY = `buzz-machine-onboarding-complete.v2:${PUBKEY_A}`; +test("existing community state presents the recovered identity as a continuation", () => { + assert.equal(existingMachineIdentityPubkey(PUBKEY_A, null), PUBKEY_A); + assert.equal(existingMachineIdentityPubkey(PUBKEY_A, PUBKEY_A), PUBKEY_A); + assert.equal(existingMachineIdentityPubkey(PUBKEY_A, PUBKEY_B), PUBKEY_A); +}); + +test("blank first launch does not present the generated identity as recovered", () => { + assert.equal(existingMachineIdentityPubkey(PUBKEY_A, undefined), null); + assert.equal(existingMachineIdentityPubkey(null, null), null); +}); + // ── Fix A regression case ──────────────────────────────────────────────────── test("migrate_mismatched_community_pubkey_does_not_vouch_for_current_key", () => { diff --git a/desktop/src/features/onboarding/machineOnboarding.ts b/desktop/src/features/onboarding/machineOnboarding.ts index eacc2c35628..1b7af4e4cbe 100644 --- a/desktop/src/features/onboarding/machineOnboarding.ts +++ b/desktop/src/features/onboarding/machineOnboarding.ts @@ -28,6 +28,24 @@ export function readMachineOnboardingCompletion(pubkey: string | null) { ); } +/** + * Return the already-loaded identity when persisted community state proves this + * is an upgrade/recovery onboarding pass rather than a blank first launch. + * + * This is deliberately presentation-only: an unstamped or mismatched community + * still cannot vouch for onboarding completion in + * `migrateMachineOnboardingCompletion`. It only prevents the landing action + * from claiming that it will create a key when the native layer has already + * recovered the user's current key. + */ +export function existingMachineIdentityPubkey( + currentPubkey: string | null, + activeCommunityPubkey: string | null | undefined, +) { + if (!currentPubkey || activeCommunityPubkey === undefined) return null; + return currentPubkey; +} + function clearMachineOnboardingCompletion(pubkey: string | null) { if (typeof window === "undefined" || !pubkey) return; window.localStorage.removeItem( @@ -250,6 +268,10 @@ export function useMachineOnboardingState({ continueWithIdentity, continueWithRecoveredIdentity, currentPubkey, + existingIdentityPubkey: existingMachineIdentityPubkey( + currentPubkey, + activeCommunityPubkey, + ), identityLost, queryClient, reopen, diff --git a/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx b/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx index 27d6b8ce447..cf2d4a1b8be 100644 --- a/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx +++ b/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx @@ -64,6 +64,7 @@ export function MachineOnboardingFlow({ complete, continueWithIdentity, continueWithRecoveredIdentity, + existingIdentityPubkey, identityLost, initialPage, queryClient, @@ -72,6 +73,7 @@ export function MachineOnboardingFlow({ complete: (pubkey?: string) => void; continueWithIdentity: (pubkey: string) => void; continueWithRecoveredIdentity: (pubkey: string) => void; + existingIdentityPubkey?: string | null; identityLost: boolean; initialPage?: MachineOnboardingPage; queryClient: QueryClient; @@ -100,7 +102,7 @@ export function MachineOnboardingFlow({ >(null); const [phoneRecoveryStep, setPhoneRecoveryStep] = React.useState("loading"); const [selectedPubkey, setSelectedPubkey] = React.useState( - null, + existingIdentityPubkey ?? null, ); const [identityStorage, setIdentityStorage] = React.useState< IdentityStorage | undefined From 93eac3db2f33485afae305f3f2162d1bc870cd27 Mon Sep 17 00:00:00 2001 From: Ross Hartmann <1750494+RossHartmann@users.noreply.github.com> Date: Sat, 22 Aug 2026 01:34:21 -0700 Subject: [PATCH 2/2] fix(desktop): continue with recovered machine identity Signed-off-by: Ross Hartmann <1750494+RossHartmann@users.noreply.github.com> --- .../onboarding/ui/MachineOnboardingFlow.tsx | 44 ++++++++++++++++++- desktop/tests/e2e/harness-management.spec.ts | 9 ++-- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx b/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx index cf2d4a1b8be..2286079fa2d 100644 --- a/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx +++ b/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx @@ -174,6 +174,27 @@ export function MachineOnboardingFlow({ } }, [continueWithRecoveredIdentity, queryClient]); + const continueWithExistingIdentity = React.useCallback(async () => { + setIsPending(true); + setError(null); + try { + const identity = await getIdentity(); + continueWithIdentity(identity.pubkey); + queryClient.setQueryData(["identity"], identity); + setIdentityWasImported(false); + setSelectedPubkey(identity.pubkey); + setIdentityStorage(identity.storage); + setTransitionDirection("forward"); + setPage("setup"); + } catch (cause) { + setError( + cause instanceof Error ? cause.message : "Failed to load identity", + ); + } finally { + setIsPending(false); + } + }, [continueWithIdentity, queryClient]); + const replaceLostIdentity = React.useCallback(async () => { const confirmed = window.confirm( "This will create a new identity and abandon your previous key. This cannot be undone. Continue?", @@ -238,6 +259,15 @@ export function MachineOnboardingFlow({ }, [backupSession]); const backFromSetup = React.useCallback(() => { + if ( + existingIdentityPubkey && + selectedPubkey === existingIdentityPubkey && + !identityWasImported + ) { + setTransitionDirection("backward"); + setPage("identity"); + return; + } if (identityWasImported) { setKeyImportFormKey((current) => current + 1); setKeyImportStage("key-entry"); @@ -252,7 +282,13 @@ export function MachineOnboardingFlow({ setTransitionDirection("backward"); setReturningFromSecurity(false); setPage("backup"); - }, [backupSession, backupSubview, identityWasImported]); + }, [ + backupSession, + backupSubview, + existingIdentityPubkey, + identityWasImported, + selectedPubkey, + ]); const chromeBackAction = page === "key-import" && @@ -329,7 +365,11 @@ export function MachineOnboardingFlow({