From 73d181abea216a62c548d7a34e1f80d4f487cba7 Mon Sep 17 00:00:00 2001 From: archipelago Date: Thu, 16 Jul 2026 22:16:58 -0400 Subject: [PATCH] =?UTF-8?q?fix(setup):=20wizard=20navigation=20round-trips?= =?UTF-8?q?=20=E2=80=94=20back=20to=20Setup=20tab,=20goal-aware=20channels?= =?UTF-8?q?=20back=20button,=20configure=20launches=20the=20app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "Back to Goals" now returns to Home's Setup tab (?tab=setup) instead of landing on the dashboard tab. - The channels screen remembers when a setup wizard sent you there (?from=goal) — its back button reads "Back to Setup" and returns to the wizard; it also now uses the shared BackButton pill instead of a bare link. - "Open & Configure" steps launch the actual app via the app launcher — iframe apps overlay on top of the wizard, tab-only apps (BTCPay, Nextcloud) open a tab, mobile uses the in-app browser — instead of routing to the app-details page. Co-Authored-By: Claude Fable 5 --- neode-ui/src/views/GoalDetail.vue | 18 +++++++---- neode-ui/src/views/Home.vue | 10 +++++-- neode-ui/src/views/apps/LightningChannels.vue | 27 ++++++++++++----- .../goals/__tests__/goalStepActions.test.ts | 30 ++++++++++++------- neode-ui/src/views/goals/goalStepActions.ts | 13 ++++---- 5 files changed, 67 insertions(+), 31 deletions(-) diff --git a/neode-ui/src/views/GoalDetail.vue b/neode-ui/src/views/GoalDetail.vue index f824f067..17de96a7 100644 --- a/neode-ui/src/views/GoalDetail.vue +++ b/neode-ui/src/views/GoalDetail.vue @@ -231,7 +231,7 @@ import { useGoalStore } from '@/stores/goals' import { useAppLauncherStore } from '@/stores/appLauncher' import { getGoalById, ZEUS_CHANNEL_MIN_SATS } from '@/data/goals' import type { GoalStep } from '@/types/goals' -import { goalStepTargetPath } from './goals/goalStepActions' +import { goalStepRouteOverride } from './goals/goalStepActions' import BackButton from '@/components/BackButton.vue' import ReceiveBitcoinModal from '@/components/ReceiveBitcoinModal.vue' import { rpcClient } from '@/api/rpc-client' @@ -398,9 +398,16 @@ async function installApp(step: GoalStep) { function openConfigureStep(step: GoalStep) { ensureGoalStarted() goalStore.completeStep(goalId.value, step.id) - const targetPath = goalStepTargetPath(step) - if (targetPath) { - router.push(targetPath) + const override = goalStepRouteOverride(step) + if (override) { + // Internal screens (channels, web5, settings) — tag where we came from so + // their back button returns to this wizard. + router.push({ path: override, query: { from: 'goal', goal: goalId.value } }) + } else if (step.appId) { + // Launch the app itself: iframe apps overlay on top of the wizard, + // tab-only apps open a tab (mobile: the in-app browser) — the app + // launcher handles every case. + useAppLauncherStore().openSession(step.appId) } } @@ -421,7 +428,8 @@ function ensureGoalStarted() { } function goBack() { - router.push('/dashboard') + // The goal cards live on Home's Setup tab — return there, not the dashboard. + router.push({ path: '/dashboard', query: { tab: 'setup' } }) } // ── Fund-wallet step: live sync status + on-chain balance ─────────────────── diff --git a/neode-ui/src/views/Home.vue b/neode-ui/src/views/Home.vue index e06072f6..d7855cdd 100644 --- a/neode-ui/src/views/Home.vue +++ b/neode-ui/src/views/Home.vue @@ -287,7 +287,7 @@ diff --git a/neode-ui/src/views/goals/__tests__/goalStepActions.test.ts b/neode-ui/src/views/goals/__tests__/goalStepActions.test.ts index c948ef72..45e22ad5 100644 --- a/neode-ui/src/views/goals/__tests__/goalStepActions.test.ts +++ b/neode-ui/src/views/goals/__tests__/goalStepActions.test.ts @@ -1,29 +1,37 @@ import { describe, expect, it } from 'vitest' import { GOALS } from '@/data/goals' -import { goalStepTargetPath } from '../goalStepActions' +import { goalStepRouteOverride } from '../goalStepActions' import type { GoalStep } from '@/types/goals' describe('goalStepActions', () => { - it('routes app-backed steps to their app details page', () => { - expect(goalStepTargetPath(step({ id: 'configure-filebrowser', appId: 'filebrowser' }))).toBe('/dashboard/apps/filebrowser') + it('app-backed configure steps have no route override — they launch the app itself', () => { + expect(goalStepRouteOverride(step({ id: 'configure-filebrowser', appId: 'filebrowser' }))).toBeNull() + expect(goalStepRouteOverride(step({ id: 'configure-store', appId: 'btcpay-server' }))).toBeNull() }) it('routes built-in identity and backup steps to their owning screens', () => { - expect(goalStepTargetPath(step({ id: 'setup-nostr' }))).toBe('/dashboard/web5') - expect(goalStepTargetPath(step({ id: 'export-identity' }))).toBe('/dashboard/web5/credentials') - expect(goalStepTargetPath(step({ id: 'create-passphrase' }))).toBe('/dashboard/settings') + expect(goalStepRouteOverride(step({ id: 'setup-nostr' }))).toBe('/dashboard/web5') + expect(goalStepRouteOverride(step({ id: 'export-identity' }))).toBe('/dashboard/web5/credentials') + expect(goalStepRouteOverride(step({ id: 'create-passphrase' }))).toBe('/dashboard/settings') + }) + + it('routes channel steps to the Lightning channels screen', () => { + expect(goalStepRouteOverride(step({ id: 'open-channel' }))).toBe('/dashboard/apps/lnd/channels') + expect(goalStepRouteOverride(step({ id: 'open-channels' }))).toBe('/dashboard/apps/lnd/channels') + expect(goalStepRouteOverride(step({ id: 'open-zeus-channel' }))).toBe('/dashboard/apps/lnd/channels') }) it('keeps passive info steps without a target route', () => { - expect(goalStepTargetPath(step({ id: 'sync-setup' }))).toBeNull() + expect(goalStepRouteOverride(step({ id: 'sync-setup' }))).toBeNull() }) - it('gives every configure step in the shipped goals a destination', () => { + it('gives every shipped configure step a destination — a route override or an app to launch', () => { const configureSteps = GOALS.flatMap((goal) => goal.steps.filter((candidate) => candidate.action === 'configure')) - expect(configureSteps.map((candidate) => [candidate.id, goalStepTargetPath(candidate)])).toEqual( - configureSteps.map((candidate) => [candidate.id, expect.any(String)]), - ) + for (const candidate of configureSteps) { + const destination = goalStepRouteOverride(candidate) ?? candidate.appId ?? null + expect(destination, `configure step ${candidate.id} has no destination`).not.toBeNull() + } }) }) diff --git a/neode-ui/src/views/goals/goalStepActions.ts b/neode-ui/src/views/goals/goalStepActions.ts index 136e6bb4..647c765e 100644 --- a/neode-ui/src/views/goals/goalStepActions.ts +++ b/neode-ui/src/views/goals/goalStepActions.ts @@ -1,5 +1,6 @@ import type { GoalStep } from '@/types/goals' +// Steps that land on an internal screen rather than launching an app UI. const STEP_ROUTE_OVERRIDES: Record = { 'setup-nostr': '/dashboard/web5', 'export-identity': '/dashboard/web5/credentials', @@ -13,9 +14,11 @@ const STEP_ROUTE_OVERRIDES: Record = { 'open-zeus-channel': '/dashboard/apps/lnd/channels', } -export function goalStepTargetPath(step: GoalStep): string | null { - const override = STEP_ROUTE_OVERRIDES[step.id] - if (override) return override - if (step.appId) return `/dashboard/apps/${step.appId}` - return null +/** + * Internal route a step navigates to, or null when the step should launch its + * app instead (via the app launcher — iframe apps overlay on top, tab-only + * apps open a tab / the mobile in-app browser). + */ +export function goalStepRouteOverride(step: GoalStep): string | null { + return STEP_ROUTE_OVERRIDES[step.id] ?? null }