fix(setup): wizard navigation round-trips — back to Setup tab, goal-aware channels back button, configure launches the app
Some checks failed
Demo images / Build & push demo images (push) Failing after 1m56s
Some checks failed
Demo images / Build & push demo images (push) Failing after 1m56s
- "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 <noreply@anthropic.com>
This commit is contained in:
parent
55d7f19545
commit
73d181abea
@ -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 ───────────────────
|
||||
|
||||
@ -287,7 +287,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, onBeforeUnmount, onMounted } from 'vue'
|
||||
import { RouterLink, useRouter } from 'vue-router'
|
||||
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import SendBitcoinModal from '@/components/SendBitcoinModal.vue'
|
||||
import ReceiveBitcoinModal from '@/components/ReceiveBitcoinModal.vue'
|
||||
@ -315,9 +315,15 @@ import type { WalletTransaction } from './home/HomeWalletCard.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const uiMode = useUIModeStore()
|
||||
const isDev = import.meta.env.DEV
|
||||
const homeTab = ref<'dashboard' | 'setup'>('dashboard')
|
||||
// ?tab=setup lands on the Setup tab (e.g. "Back to Goals" from a goal wizard)
|
||||
const homeTab = ref<'dashboard' | 'setup'>(route.query.tab === 'setup' ? 'setup' : 'dashboard')
|
||||
watch(() => route.query.tab, (tab) => {
|
||||
if (tab === 'setup') homeTab.value = 'setup'
|
||||
else if (tab === 'dashboard') homeTab.value = 'dashboard'
|
||||
})
|
||||
const topGoals = GOALS.slice(0, 3)
|
||||
|
||||
const QUICK_START_APPS = [...new Set(topGoals.flatMap((g) => g.requiredApps))]
|
||||
|
||||
@ -1,12 +1,6 @@
|
||||
<template>
|
||||
<div class="pb-16 md:pb-4">
|
||||
<!-- Back Button -->
|
||||
<button @click="router.replace('/dashboard/apps/lnd')" class="mb-6 flex items-center gap-2 text-white/70 hover:text-white transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Back to LND
|
||||
</button>
|
||||
<BackButton :label="backLabel" desktop-margin="mb-6" @click="goBack" />
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-6">Lightning Channels</h1>
|
||||
|
||||
@ -15,8 +9,25 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import BackButton from '@/components/BackButton.vue'
|
||||
import LightningChannelsPanel from '@/components/LightningChannelsPanel.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// When a setup wizard sent us here (?from=goal&goal=<id>), back returns to it.
|
||||
const fromGoalId = computed(() =>
|
||||
route.query.from === 'goal' && typeof route.query.goal === 'string' ? route.query.goal : null,
|
||||
)
|
||||
const backLabel = computed(() => (fromGoalId.value ? 'Back to Setup' : 'Back to LND'))
|
||||
|
||||
function goBack() {
|
||||
if (fromGoalId.value) {
|
||||
router.push(`/dashboard/goals/${fromGoalId.value}`)
|
||||
} else {
|
||||
router.replace('/dashboard/apps/lnd')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -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()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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<string, string> = {
|
||||
'setup-nostr': '/dashboard/web5',
|
||||
'export-identity': '/dashboard/web5/credentials',
|
||||
@ -13,9 +14,11 @@ const STEP_ROUTE_OVERRIDES: Record<string, string> = {
|
||||
'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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user