diff --git a/neode-ui/mock-backend.js b/neode-ui/mock-backend.js
index 26c91c69..ff5099dc 100755
--- a/neode-ui/mock-backend.js
+++ b/neode-ui/mock-backend.js
@@ -182,10 +182,11 @@ const SEED_BTCRELAY = {
}
// User state (simulated file-based storage). Returns a fresh object per session.
-// In DEMO mode the effective dev mode is "onboarding" so the intro/onboarding can
-// play for each new visitor (the per-day replay gate lives in the frontend).
+// In DEMO the visitor is always treated as fully set up ("existing") so the
+// onboarding WIZARD (seed/identity/backup) is never forced by the route guard.
+// The welcome INTRO still shows via the frontend's per-day replay gate.
function seedUserState() {
- const mode = DEMO ? 'onboarding' : DEV_MODE
+ const mode = DEMO ? 'existing' : DEV_MODE
switch (mode) {
case 'setup':
// Setup mode: user needs to set a password (simple setup, not onboarding).
@@ -982,6 +983,117 @@ app.get('/app/bitcoin-ui/', async (_req, res) => {
}
})
+// ── Mock app UIs served in the in-app iframe (DEMO) ─────────────────────────
+function demoAppShell(title, accent, bodyHtml) {
+ return `
This app isn't interactive in the demo, but it runs fully on a real Archipelago node.
+
Demo preview
+
`)
+})
+
// Health check
app.get('/health', (req, res) => {
res.status(200).send('healthy')
diff --git a/neode-ui/src/composables/useDemoIntro.ts b/neode-ui/src/composables/useDemoIntro.ts
index a05b15c9..c3942726 100644
--- a/neode-ui/src/composables/useDemoIntro.ts
+++ b/neode-ui/src/composables/useDemoIntro.ts
@@ -47,3 +47,43 @@ export function clearDemoIntroSeen(): void {
/* ignore */
}
}
+
+// ── Demoable apps ───────────────────────────────────────────────────────────
+// Only these apps actually do something in the demo (a mock UI or a real
+// external site). Everything else shows "No demo" on a disabled install button
+// and is not launchable.
+const DEMO_EXTERNAL_URLS: Record = {
+ // Real, public sites that we open instead of mocking.
+ indeedhub: 'https://indee.tx1138.com/',
+ mempool: 'https://mempool.space/testnet',
+ 'mempool-web': 'https://mempool.space/testnet',
+}
+
+// Apps with a same-origin mock UI served by the demo backend.
+const DEMO_MOCK_UI: Record = {
+ 'bitcoin-knots': '/app/bitcoin-ui/',
+ 'bitcoin-core': '/app/bitcoin-ui/',
+ bitcoin: '/app/bitcoin-ui/',
+ 'bitcoin-ui': '/app/bitcoin-ui/',
+ electrs: '/app/electrumx/',
+ electrumx: '/app/electrumx/',
+ 'archy-electrs-ui': '/app/electrumx/',
+ fedimint: '/app/fedimint/',
+ fedimintd: '/app/fedimint/',
+ filebrowser: '/app/filebrowser/',
+}
+
+/** Apps that open in a new tab (external real sites) rather than an iframe. */
+export function isDemoExternal(appId: string): boolean {
+ return appId === 'mempool' || appId === 'mempool-web'
+}
+
+/** Can this app be launched/installed in the demo? */
+export function isDemoApp(appId: string): boolean {
+ return appId in DEMO_EXTERNAL_URLS || appId in DEMO_MOCK_UI
+}
+
+/** Resolve the demo launch URL for an app, or null if it isn't demoable. */
+export function demoAppUrl(appId: string): string | null {
+ return DEMO_EXTERNAL_URLS[appId] ?? DEMO_MOCK_UI[appId] ?? null
+}
diff --git a/neode-ui/src/stores/appLauncher.ts b/neode-ui/src/stores/appLauncher.ts
index 68bc66f1..4c7206ef 100644
--- a/neode-ui/src/stores/appLauncher.ts
+++ b/neode-ui/src/stores/appLauncher.ts
@@ -4,6 +4,7 @@ import { rpcClient } from '@/api/rpc-client'
import router from '@/router'
import { recordAppLaunch } from '@/utils/appUsage'
import { requestExternalOpen } from '@/api/remote-relay'
+import { IS_DEMO, isDemoExternal, demoAppUrl } from '@/composables/useDemoIntro'
/**
* Open a URL in a new browser tab — but if a companion (phone) is currently
@@ -222,6 +223,12 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
function openSession(appId: string) {
recordAppLaunch(appId)
const mobile = isMobileViewport()
+ // Demo: apps backed by a real external site that blocks iframing (mempool.space)
+ // open in a new tab; everything else demoable renders in the in-app session.
+ if (IS_DEMO && isDemoExternal(appId)) {
+ const ext = demoAppUrl(appId)
+ if (ext) { openExternal(ext); return }
+ }
const launchUrl = NEW_TAB_APP_IDS.has(appId) ? directAppUrl(appId) : null
if (launchUrl && !mobile) {
openExternal(launchUrl)
diff --git a/neode-ui/src/views/Login.vue b/neode-ui/src/views/Login.vue
index bbc477e4..63aff776 100644
--- a/neode-ui/src/views/Login.vue
+++ b/neode-ui/src/views/Login.vue
@@ -208,14 +208,16 @@
>
{{ t('login.replayIntro') }}
- |
-
+
+ |
+
+
diff --git a/neode-ui/src/views/MarketplaceAppDetails.vue b/neode-ui/src/views/MarketplaceAppDetails.vue
index b6d13954..f4ae28ed 100644
--- a/neode-ui/src/views/MarketplaceAppDetails.vue
+++ b/neode-ui/src/views/MarketplaceAppDetails.vue
@@ -63,8 +63,8 @@
@@ -129,8 +129,8 @@
@@ -351,6 +351,7 @@