2026-03-22 03:30:21 +00:00
< template >
2026-03-31 07:03:57 +01:00
< div class = "relative flex-1 min-h-0 bg-black/40 overflow-hidden app-session-frame-safe" >
2026-03-22 03:30:21 +00:00
< Transition name = "content-fade" >
< div v-if = "loading" class="absolute inset-0 z-10 flex items-center justify-center bg-black/40" >
< svg class = "animate-spin h-8 w-8 text-blue-400" viewBox = "0 0 24 24" fill = "none" >
< circle class = "opacity-25" cx = "12" cy = "12" r = "10" stroke = "currentColor" stroke -width =" 4 " />
< path class = "opacity-75" fill = "currentColor" d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</ svg >
</ div >
</ Transition >
2026-06-13 04:49:32 -04:00
<!-- ElectrumX sync screen — shown before the real UI while the on - chain
index is still being built ( the Electrum server can 't serve clients
until then). Mirrors the Fedimint Guardian "wait page" design. -->
<Transition name="content-fade">
<div v-if="electrsSync" class="absolute inset-0 z-10 flex flex-col items-center justify-center">
<div class="text-center px-8 w-full max-w-md">
<div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center">
<svg class="w-8 h-8 text-orange-300 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 7v10a2 2 0 002 2h12a2 2 0 002-2V7M4 7l8 5 8-5M4 7l8-4 8 4" />
</svg>
</div>
<h3 class="text-lg font-semibold text-white mb-2">{{ appTitle }} is syncing</h3>
<p class="text-white/50 text-sm mb-5">
ElectrumX is building its index from the blockchain. The UI opens
automatically once it' s ready — you can keep using the rest of Archipelago .
</ p >
< div class = "w-full h-2 rounded-full bg-white/10 overflow-hidden mb-2" >
< div
class = "h-full bg-orange-400/80 transition-all duration-700"
: style = "{ width: `${Math.min(100, Math.max(2, electrsSync.progress_pct)).toFixed(1)}%` }"
></ div >
</ div >
< p class = "text-white/70 text-sm font-medium mb-1" >{{ electrsSync . progress_pct . toFixed ( 1 ) }} % </ p >
< p class = "text-white/40 text-xs" >
Block {{ electrsSync . indexed_height . toLocaleString () }} of {{ electrsSync . network_height . toLocaleString () }}
< template v-if = "electrsSync.index_size" > · {{ electrsSync.index_size }} indexed </ template >
</ p >
< p v-if = "electrsSync.stale" class="text-yellow-400/70 text-xs mt-2" > Reconnecting to ElectrumX … </ p >
</ div >
</ div >
</ Transition >
2026-05-19 19:21:43 -04:00
< div
2026-06-13 04:49:32 -04:00
v-if = "appUrl && !iframeBlocked && !electrsSync"
2026-05-19 19:21:43 -04:00
class = "absolute inset-0 app-session-frame-scroll-host"
tabindex = "-1"
@pointerdown ="focusIframe"
@focusin ="focusIframe"
>
2026-05-19 18:29:04 -04:00
< iframe
ref = "iframeRef"
:key = "refreshKey"
:src = "appUrl"
class = "w-full h-full border-0 iframe-scrollbar-hide"
title = "App content"
tabindex = "0"
2026-05-19 19:21:43 -04:00
@load ="handleIframeLoad"
2026-05-19 18:29:04 -04:00
@error ="$emit('iframeError')"
/>
</ div >
2026-03-22 03:30:21 +00:00
2026-06-14 07:58:24 -04:00
<!-- Iframe blocked fallback . Suppressed while the ElectrumX sync screen
( the "pre UI" ) is showing : a still - syncing Electrum server isn 't
reachable yet, so the "App not reachable / retry" overlay would just
paint over the sync progress and read as a hard error. -->
2026-03-22 03:30:21 +00:00
<Transition name="content-fade">
2026-06-14 07:58:24 -04:00
<div v-if="iframeBlocked && !electrsSync" class="absolute inset-0 z-10 flex flex-col items-center justify-center">
2026-03-22 03:30:21 +00:00
<div class="text-center px-8">
<div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center">
<svg class="w-8 h-8 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
2026-06-11 00:24:40 -04:00
<h3 class="text-lg font-semibold text-white mb-2">{{ blockedReason ? blockedTitle : (mustOpenNewTab ? ' This app opens in a new tab ' : ' App not reachable ') }}</h3>
2026-03-22 03:30:21 +00:00
<p class="text-white/50 text-sm mb-6">
<template v-if="mustOpenNewTab">{{ appTitle }} sets security headers that prevent iframe embedding.<br>Open it in a new browser tab instead.</template>
2026-06-11 00:24:40 -04:00
<template v-else-if="blockedReason">{{ blockedReason }}<br><span v-if="autoRetryCount > 0" class="text-yellow-400/70">Checking again automatically ({{ autoRetryCount }})...</span></template>
2026-03-22 03:30:21 +00:00
<template v-else>{{ appTitle }} may still be starting up or the container is stopped.<br><span v-if="autoRetryCount > 0" class="text-yellow-400/70">Retrying automatically ({{ autoRetryCount }})...</span></template>
</p>
2026-05-06 09:23:57 -04:00
<div class="flex flex-wrap items-center justify-center gap-3">
2026-03-22 03:30:21 +00:00
<button
v-if="!mustOpenNewTab"
@click="$emit(' refresh ')"
class="glass-button px-6 py-3 rounded-lg text-sm font-semibold inline-flex items-center gap-2"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Retry now
</button>
<button
@click="$emit(' openNewTabAndBack ')"
class="glass-button px-6 py-3 rounded-lg text-sm font-semibold inline-flex items-center gap-2"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
Open in new tab
</button>
</div>
</div>
</div>
</Transition>
<div v-if="!appUrl" class="absolute inset-0 flex items-center justify-center">
<div class="text-center px-8">
<h3 class="text-lg font-semibold text-white mb-2">App not configured</h3>
<p class="text-white/50 text-sm">No URL found for {{ appId }}</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
2026-05-19 18:29:04 -04:00
import { nextTick, ref, watch } from ' vue '
2026-06-13 04:49:32 -04:00
import type { ElectrsSyncStatus } from ' @ / composables / useElectrsSync '
2026-03-22 03:30:21 +00:00
2026-05-19 18:29:04 -04:00
const props = defineProps<{
2026-03-22 03:30:21 +00:00
appUrl: string
appId: string
appTitle: string
loading: boolean
iframeBlocked: boolean
mustOpenNewTab: boolean
autoRetryCount: number
refreshKey: number
2026-06-11 00:24:40 -04:00
blockedReason?: string
blockedTitle?: string
2026-06-13 04:49:32 -04:00
// Non-null only for ElectrumX while its index is still building — shows the
// sync screen and gates the iframe until status flips to "synced".
electrsSync?: ElectrsSyncStatus | null
2026-03-22 03:30:21 +00:00
}>()
2026-05-19 19:21:43 -04:00
const emit = defineEmits<{
2026-03-22 03:30:21 +00:00
iframeLoad: []
iframeError: []
refresh: []
openNewTabAndBack: []
}>()
const iframeRef = ref<HTMLIFrameElement | null>(null)
2026-05-19 19:21:43 -04:00
function focusIframe() {
iframeRef.value?.focus({ preventScroll: true })
}
async function handleIframeLoad() {
emit(' iframeLoad ' )
await nextTick ()
requestAnimationFrame ( focusIframe )
}
2026-05-19 18:29:04 -04:00
watch (() => [ props . appUrl , props . refreshKey , props . iframeBlocked ], async () => {
if ( ! props . appUrl || props . iframeBlocked ) return
await nextTick ()
2026-05-19 19:21:43 -04:00
requestAnimationFrame ( focusIframe )
2026-05-19 18:29:04 -04:00
}, { immediate : true })
2026-03-22 03:30:21 +00:00
defineExpose ({ iframeRef })
</ script >