/** * Native app lifecycle polish for the bundled Capacitor app (Phase 1): * - Hardware BACK button: navigate back within the mobile flow; at the mobile home require a * second press within 2s to exit, so a stray gesture can't drop someone out of a half-finished * receipt/return. (Default Capacitor behaviour exits the app on every back press at the root.) * - Status bar: paint it to match the mobile theme (dark canvas by default) and re-apply on resume. * - On resume: nudge the existing token keep-alive (the API plugin also listens to visibilitychange). * * App-only + fail-soft: early-returns on the web build (__CAP_BUILD__ undefined); also dropped from the * web bundle by the nuxt.config app:resolve hook (name 'native-lifecycle'). @capacitor/app and * @capacitor/status-bar are DYNAMICALLY imported so the web build never loads them. Any failure is * swallowed — the app just keeps the default native behaviour. */ // Injected by Vite only in the app build (see nuxt.config.ts). declare const __CAP_BUILD__: boolean | undefined export default defineNuxtPlugin(() => { if (typeof __CAP_BUILD__ === 'undefined') return // web build → no-op if (!process.client) return const router = useRouter() // ---- hardware back button + double-press-to-exit guard ---- const HOME = '/mobile' let lastBackAt = 0 const flashExitHint = () => { try { const el = document.createElement('div') el.textContent = 'Zum Beenden erneut „Zurück“ drücken' Object.assign(el.style, { position: 'fixed', left: '50%', bottom: 'calc(24px + env(safe-area-inset-bottom))', transform: 'translateX(-50%)', background: 'rgba(0,0,0,0.85)', color: '#fff', padding: '10px 18px', borderRadius: '999px', fontSize: '14px', zIndex: '99999', pointerEvents: 'none', maxWidth: '90vw', textAlign: 'center', }) document.body.appendChild(el) setTimeout(() => { try { el.remove() } catch { /* gone */ } }, 1900) } catch { /* DOM unavailable */ } } const setupBackButton = async () => { try { const { App } = await import('@capacitor/app') App.addListener('backButton', ({ canGoBack }: any) => { const path = router.currentRoute.value.path const atHome = path === HOME || path === HOME + '/' if (!atHome && (canGoBack || window.history.length > 1)) { router.back() return } if (!atHome) { navigateTo(HOME) return } // At the mobile home: confirm exit via a quick double-press. const now = Date.now() if (now - lastBackAt < 2000) { App.exitApp() } else { lastBackAt = now flashExitHint() try { useFeedback().warning() } catch { /* ignore */ } } }) // Keep the session warm when the app returns to the foreground. App.addListener('appStateChange', ({ isActive }: any) => { if (isActive) window.dispatchEvent(new Event('visibilitychange')) }) } catch (e: any) { console.warn('[native-lifecycle] back-button setup failed:', e?.message) } } // ---- status bar theming ---- const isDark = () => { try { return document.documentElement.classList.contains('dark') || document.documentElement.getAttribute('data-mode') === 'dark' || (window.localStorage?.getItem('logship_theme') || '').includes('dark') } catch { return true } // mobile canvas is dark by default } const applyStatusBar = async () => { try { const { StatusBar, Style } = await import('@capacitor/status-bar') const dark = isDark() await StatusBar.setOverlaysWebView({ overlay: false }) // Style.Dark = light icons (for a dark bar); Style.Light = dark icons (for a light bar). await StatusBar.setStyle({ style: dark ? Style.Dark : Style.Light }) await StatusBar.setBackgroundColor({ color: dark ? '#1a1a1a' : '#ffffff' }) } catch { /* iOS / unsupported / not a native platform */ } } setupBackButton() applyStatusBar() // Re-apply after the app resumes (theme may have changed in another surface). document.addEventListener('visibilitychange', () => { if (!document.hidden) applyStatusBar() }) })