diff --git a/desktop/src/app/App.tsx b/desktop/src/app/App.tsx index 6b799636f3..d8a499a0eb 100644 --- a/desktop/src/app/App.tsx +++ b/desktop/src/app/App.tsx @@ -463,6 +463,7 @@ function MachineBootstrap({ sharedIdentity }: { sharedIdentity: boolean }) { <> ( null, ); + const continuingPubkeyRef = React.useRef(null); const startupPubkeyRef = React.useRef(null); const [bootedLost, setBootedLost] = React.useState(false); const [bootedLocked, setBootedLocked] = React.useState(false); @@ -157,6 +158,10 @@ export function useMachineOnboardingState({ [currentPubkey], ); + const continueWithIdentity = React.useCallback((pubkey: string) => { + continuingPubkeyRef.current = pubkey; + }, []); + const reopen = React.useCallback(() => { clearMachineOnboardingCompletion(currentPubkey); setCompletedPubkey((pubkey) => (pubkey === currentPubkey ? null : pubkey)); @@ -188,7 +193,12 @@ export function useMachineOnboardingState({ identityQuery.fetchStatus === "fetching", ) || !currentPubkey || - (!hasCompletedCurrentPubkey && evaluatedPubkey !== currentPubkey) + // Imported identities are published before the flow can advance to setup. + // Keep that explicitly requested identity switch in onboarding; only the + // startup identity needs the one-render evaluation gate above. + (!hasCompletedCurrentPubkey && + evaluatedPubkey !== currentPubkey && + continuingPubkeyRef.current !== currentPubkey) ) { stage = "blocking"; } else if (identityLost || !hasCompletedCurrentPubkey) { @@ -199,6 +209,7 @@ export function useMachineOnboardingState({ return { complete, + continueWithIdentity, currentPubkey, identityLost, queryClient, diff --git a/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx b/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx index 4d6bf54c76..d7e5da4e93 100644 --- a/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx +++ b/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx @@ -33,11 +33,13 @@ export type MachineOnboardingPage = export function MachineOnboardingFlow({ complete, + continueWithIdentity, identityLost, initialPage, queryClient, }: { complete: (pubkey?: string) => void; + continueWithIdentity: (pubkey: string) => void; identityLost: boolean; initialPage?: MachineOnboardingPage; queryClient: QueryClient; @@ -141,12 +143,13 @@ export function MachineOnboardingFlow({ const importExistingIdentity = React.useCallback( async (nsec: string) => { const identity = await importIdentity(nsec); + continueWithIdentity(identity.pubkey); queryClient.setQueryData(["identity"], identity); setIdentityWasImported(true); setSelectedPubkey(identity.pubkey); setPage("setup"); }, - [queryClient], + [continueWithIdentity, queryClient], ); return ( diff --git a/desktop/tests/e2e/onboarding.spec.ts b/desktop/tests/e2e/onboarding.spec.ts index 08b5b82123..9a82b2feca 100644 --- a/desktop/tests/e2e/onboarding.spec.ts +++ b/desktop/tests/e2e/onboarding.spec.ts @@ -565,6 +565,23 @@ test("completed users skip the loading gate while profile is still settling", as await expectHomeView(page); }); +test("first-launch key import continues to machine setup", async ({ page }) => { + await installMockBridge(page, undefined, { + skipCommunitySeed: true, + skipOnboardingSeed: true, + }); + await page.goto("/"); + + await page.getByRole("button", { name: "Enter a key" }).click(); + const importedNsec = nsecEncode(hexToBytes(TEST_IDENTITIES.alice.privateKey)); + await page.getByTestId("nostr-import-nsec-input").fill(importedNsec); + await page.getByTestId("nostr-import-submit").click(); + + await expect(page.getByTestId("onboarding-page-2")).toBeVisible(); + await expect(page.getByTestId("machine-onboarding-gate")).toBeVisible(); + await expect(page.getByTestId("app-loading-gate")).toHaveCount(0); +}); + test("first-community choices expose npub and invite input", async ({ page, }) => {