feat(ui): dual-ecash wallet settings, buy-peer-files, seed backup, assorted fixes
- Tabbed Wallet Settings modal (Cashu + Fedimint) and dual-balance wallet card - Buy a peer's paid file (ecash / node Lightning / on-chain / external QR) - Recovery-phrase reveal + backup section; onboarding seed retry resilience - NetBird HTTPS launch, remote-control two-finger scroll + external-open - Shared BackButton, single-v version label, mesh Bitcoin header toggles Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
bd567cd165
commit
87769cbfbf
@@ -3,6 +3,26 @@ 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'
|
||||
|
||||
/**
|
||||
* Open a URL in a new browser tab — but if a companion (phone) is currently
|
||||
* driving this kiosk, hand the URL to the phone instead so it opens in the
|
||||
* phone's browser rather than the (often headless / unattended) kiosk display.
|
||||
* Falls back to a local `window.open` when no companion is active.
|
||||
*/
|
||||
function openExternal(launchUrl: string) {
|
||||
// Resolve to an absolute URL so the phone can open it (window.open also
|
||||
// handles absolute URLs fine).
|
||||
let absolute = launchUrl
|
||||
try {
|
||||
absolute = new URL(launchUrl, window.location.origin).href
|
||||
} catch {
|
||||
/* keep as-is */
|
||||
}
|
||||
if (requestExternalOpen(absolute)) return
|
||||
window.open(launchUrl, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
/** Ports of apps that set X-Frame-Options (can't iframe, must open in new tab) */
|
||||
const NEW_TAB_PORTS = new Set([
|
||||
@@ -29,8 +49,16 @@ const NEW_TAB_APP_IDS = new Set([
|
||||
'nginx-proxy-manager',
|
||||
'uptime-kuma',
|
||||
'gitea',
|
||||
// netbird's dashboard needs a secure context (window.crypto.subtle for OIDC
|
||||
// PKCE), so it's served over HTTPS and must open in a real tab — a
|
||||
// self-signed-HTTPS iframe is blocked by the browser (you can't accept the
|
||||
// cert warning inside an iframe).
|
||||
'netbird',
|
||||
])
|
||||
|
||||
// Apps served over HTTPS (self-signed) rather than plain HTTP.
|
||||
const HTTPS_APP_IDS = new Set(['netbird'])
|
||||
|
||||
function mustOpenInNewTab(url: string): boolean {
|
||||
try {
|
||||
const u = new URL(url)
|
||||
@@ -127,12 +155,16 @@ const APP_ID_TO_PORT: Record<string, string> = {
|
||||
'nginx-proxy-manager': '8081',
|
||||
'uptime-kuma': '3002',
|
||||
gitea: '3001',
|
||||
// Without this, directAppUrl('netbird') returns null and netbird falls
|
||||
// through to the iframe (and never gets its https URL) — issue #15.
|
||||
netbird: '8087',
|
||||
}
|
||||
|
||||
function directAppUrl(appId: string): string | null {
|
||||
const port = APP_ID_TO_PORT[appId]
|
||||
if (!port || typeof window === 'undefined') return null
|
||||
return `http://${window.location.hostname}:${port}`
|
||||
const scheme = HTTPS_APP_IDS.has(appId) ? 'https' : 'http'
|
||||
return `${scheme}://${window.location.hostname}:${port}`
|
||||
}
|
||||
|
||||
|
||||
@@ -192,7 +224,7 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
const mobile = isMobileViewport()
|
||||
const launchUrl = NEW_TAB_APP_IDS.has(appId) ? directAppUrl(appId) : null
|
||||
if (launchUrl && !mobile) {
|
||||
window.open(launchUrl, '_blank', 'noopener,noreferrer')
|
||||
openExternal(launchUrl)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -212,12 +244,25 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
/** Legacy: open app in iframe overlay (kept for backward compat) */
|
||||
function open(payload: { url: string; title: string; openInNewTab?: boolean }) {
|
||||
const titleHintId = inferAppIdFromTitle(payload.title)
|
||||
const launchUrl = normalizeLaunchUrl(payload.url, titleHintId)
|
||||
let launchUrl = normalizeLaunchUrl(payload.url, titleHintId)
|
||||
const resolvedId = resolveAppIdFromUrl(launchUrl) || titleHintId
|
||||
|
||||
// Apps served over HTTPS (e.g. netbird, which needs a secure context for
|
||||
// its OIDC dashboard) must be launched over https — a stale http URL hits
|
||||
// the TLS port and 400s. Upgrade the scheme defensively in every path.
|
||||
if (resolvedId && HTTPS_APP_IDS.has(resolvedId)) {
|
||||
try {
|
||||
const u = new URL(launchUrl, window.location.origin)
|
||||
if (u.protocol === 'http:') {
|
||||
u.protocol = 'https:'
|
||||
launchUrl = u.href
|
||||
}
|
||||
} catch { /* leave as-is */ }
|
||||
}
|
||||
|
||||
if (!isMobileViewport() && payload.openInNewTab) {
|
||||
if (resolvedId) recordAppLaunch(resolvedId)
|
||||
window.open(launchUrl, '_blank', 'noopener,noreferrer')
|
||||
openExternal(launchUrl)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -226,7 +271,7 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
// native launchers and keep the user inside Archipelago.
|
||||
if (!isMobileViewport() && resolvedId && NEW_TAB_APP_IDS.has(resolvedId)) {
|
||||
recordAppLaunch(resolvedId)
|
||||
window.open(launchUrl, '_blank', 'noopener,noreferrer')
|
||||
openExternal(launchUrl)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -238,7 +283,7 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
|
||||
// Unknown apps that block iframes — open directly in new tab
|
||||
if (!isMobileViewport() && mustOpenInNewTab(launchUrl)) {
|
||||
window.open(launchUrl, '_blank', 'noopener,noreferrer')
|
||||
openExternal(launchUrl)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ export interface MeshStatus {
|
||||
messages_sent: number
|
||||
messages_received: number
|
||||
detected_devices?: string[]
|
||||
/** Bitcoin block-header send/receive prefs (issue #28). */
|
||||
announce_block_headers?: boolean
|
||||
receive_block_headers?: boolean
|
||||
}
|
||||
|
||||
export interface MeshPeer {
|
||||
|
||||
Reference in New Issue
Block a user