feat(ui): app sessions overlay the current page in every display mode
Launching an app (or opening a tx in mempool from Home/wallet/channels) used to router.push the app-session page in overlay/fullscreen modes, swapping the page underneath — confusing and lossy. All display modes are now store-driven: panel renders beside the page, overlay/fullscreen render above it (teleported to body), and the route never changes. Deep-link paths (/tx/<hash>) ride along via the launcher's panelPath instead of a route query; closing always returns exactly where the user was. The app-session route stays for direct links. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, watch } from 'vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import router from '@/router'
|
||||
import { recordAppLaunch } from '@/utils/appUsage'
|
||||
import { requestExternalOpen } from '@/api/remote-relay'
|
||||
import { openInAppOrNewTab } from '@/utils/openExternal'
|
||||
import { IS_DEMO, isDemoExternal, demoAppUrl } from '@/composables/useDemoIntro'
|
||||
import { IS_DEMO, isDemoApp, isDemoExternal, demoAppUrl } from '@/composables/useDemoIntro'
|
||||
|
||||
/**
|
||||
* Open a URL in a new browser tab — but if a companion (phone) is currently
|
||||
@@ -209,25 +208,17 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
const showConsent = ref(false)
|
||||
let previousActiveElement: HTMLElement | null = null
|
||||
|
||||
/** Active app in panel mode (store-based, no route change) */
|
||||
/** Active app in the store-driven session (no route change) */
|
||||
const panelAppId = ref<string | null>(null)
|
||||
/** Optional deep-link path inside the active app (e.g. /tx/<hash> for mempool) */
|
||||
const panelPath = ref<string | null>(null)
|
||||
|
||||
/** Open app in session view — panel mode uses store, overlay/fullscreen uses route */
|
||||
function dashboardReturnPath(): string {
|
||||
const current = router.currentRoute?.value
|
||||
if (!current) return '/dashboard/apps'
|
||||
const fullPath = current.fullPath || '/dashboard/apps'
|
||||
if (!fullPath.startsWith('/dashboard') || current.name === 'app-session') return '/dashboard/apps'
|
||||
return fullPath
|
||||
}
|
||||
|
||||
function openSession(appId: string) {
|
||||
function openSession(appId: string, opts: { path?: string } = {}) {
|
||||
recordAppLaunch(appId)
|
||||
const mobile = isMobileViewport()
|
||||
|
||||
// Demo: apps backed by a real external site that blocks iframing
|
||||
// (mempool.space) open externally; everything else demoable renders in the
|
||||
// in-app session.
|
||||
// Demo: apps backed by a real external site that blocks iframing open
|
||||
// externally; everything else demoable renders in the in-app session.
|
||||
if (IS_DEMO && isDemoExternal(appId)) {
|
||||
const ext = demoAppUrl(appId)
|
||||
if (ext) {
|
||||
@@ -239,7 +230,9 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
// Tab-only apps (set X-Frame-Options, can't be iframed). No interstitial:
|
||||
// desktop opens a new browser tab; mobile opens the in-app WebView (Android
|
||||
// companion) or a new browser tab (PWA) — see openInAppOrNewTab.
|
||||
if (NEW_TAB_APP_IDS.has(appId)) {
|
||||
// In the demo, demoable apps are served same-origin by the mock backend
|
||||
// (no framing headers), so they always render in the in-app session.
|
||||
if (NEW_TAB_APP_IDS.has(appId) && !(IS_DEMO && isDemoApp(appId))) {
|
||||
const launchUrl = directAppUrl(appId)
|
||||
if (launchUrl) {
|
||||
if (mobile) openInAppOrNewTab(launchUrl)
|
||||
@@ -248,21 +241,17 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Iframeable apps. Mobile and desktop-panel mode both use the store-driven
|
||||
// panel so the underlying page/tab never changes (no background swap) and
|
||||
// closing returns the user to wherever they launched from. Only desktop
|
||||
// overlay/fullscreen modes use a routed session.
|
||||
const mode = localStorage.getItem(DISPLAY_MODE_KEY) || 'panel'
|
||||
if (mobile || mode === 'panel') {
|
||||
panelAppId.value = appId
|
||||
} else {
|
||||
panelAppId.value = null
|
||||
router.push({ name: 'app-session', params: { appId }, query: { returnTo: dashboardReturnPath() } })
|
||||
}
|
||||
// Iframeable apps always use the store-driven session so the underlying
|
||||
// page never changes: panel mode renders beside the page, overlay and
|
||||
// fullscreen modes render above it (AppSession styles per display mode).
|
||||
// Closing always returns the user exactly where they launched from.
|
||||
panelPath.value = opts.path ?? null
|
||||
panelAppId.value = appId
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
panelAppId.value = null
|
||||
panelPath.value = null
|
||||
}
|
||||
|
||||
/** Legacy: open app in iframe overlay (kept for backward compat) */
|
||||
@@ -299,9 +288,20 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
return
|
||||
}
|
||||
|
||||
// Route to full-page session if we can resolve an app ID from the URL
|
||||
// Open the in-app session if we can resolve an app ID from the URL,
|
||||
// carrying through any deep-link path (e.g. /tx/<hash>).
|
||||
if (resolvedId) {
|
||||
openSession(resolvedId)
|
||||
let deepPath: string | undefined
|
||||
try {
|
||||
const u = new URL(launchUrl, window.location.origin)
|
||||
let p = `${u.pathname}${u.search}${u.hash}`
|
||||
// /app/<id>/-style URLs carry the mount prefix — strip it so only the
|
||||
// app-internal path is treated as the deep link.
|
||||
const prefix = `/app/${resolvedId}`
|
||||
if (p.startsWith(prefix)) p = p.slice(prefix.length) || '/'
|
||||
if (p && p !== '/') deepPath = p
|
||||
} catch { /* no deep link */ }
|
||||
openSession(resolvedId, { path: deepPath })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -517,6 +517,7 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
close,
|
||||
closePanel,
|
||||
panelAppId,
|
||||
panelPath,
|
||||
showConsent,
|
||||
consentRequest,
|
||||
approveConsent,
|
||||
|
||||
Reference in New Issue
Block a user