From fb540813ff245307aec5f3b30f3eb5e12a677fa2 Mon Sep 17 00:00:00 2001 From: Kroonk Date: Thu, 21 May 2026 23:05:09 +0200 Subject: [PATCH] fix: avoid web fallback after app handoff --- app.js | 103 ++++++++++++++++++++++++++++++++++++++++++----------- index.html | 2 +- sw.js | 4 +-- 3 files changed, 85 insertions(+), 24 deletions(-) diff --git a/app.js b/app.js index c0607d3..97719bb 100644 --- a/app.js +++ b/app.js @@ -60,11 +60,87 @@ function isMobileDevice() { return /Android|iPhone|iPad|iPod/i.test(navigator.userAgent); } -function getMobileUrl(service) { +function getMobilePlatform() { + const userAgent = navigator.userAgent || ''; + if (/Android/i.test(userAgent)) return 'android'; + if (/iPhone|iPad|iPod/i.test(userAgent)) return 'ios'; + return 'other'; +} + +function getMobileLaunch(service) { if (service.id === 'drive') { - return 'nextcloud://openFiles'; + const fallbackUrl = service.url; + if (getMobilePlatform() === 'android') { + return { + url: `intent://openFiles#Intent;scheme=nextcloud;package=com.nextcloud.client;S.browser_fallback_url=${encodeURIComponent(fallbackUrl)};end`, + fallbackUrl + }; + } + return { + url: 'nextcloud://openFiles', + fallbackUrl + }; } - return service.mobileUrl; + if (!service.mobileUrl) return null; + return { + url: service.mobileUrl, + fallbackUrl: service.url + }; +} + +function openMobileLaunch(url, fallbackUrl) { + let didLeavePage = false; + let fallbackTimer = null; + let cleanupTimer = null; + let launcherFrame = null; + + const cleanup = () => { + window.removeEventListener('pagehide', markPageLeft); + document.removeEventListener('visibilitychange', handleVisibilityChange); + if (fallbackTimer) window.clearTimeout(fallbackTimer); + if (cleanupTimer) window.clearTimeout(cleanupTimer); + if (launcherFrame) launcherFrame.remove(); + }; + + const markPageLeft = () => { + didLeavePage = true; + cleanup(); + }; + + const handleVisibilityChange = () => { + if (document.visibilityState === 'hidden') { + markPageLeft(); + } + }; + + window.addEventListener('pagehide', markPageLeft, { once: true }); + document.addEventListener('visibilitychange', handleVisibilityChange); + + fallbackTimer = window.setTimeout(() => { + cleanup(); + if (!didLeavePage && document.visibilityState === 'visible') { + window.location.href = fallbackUrl; + } + }, 2200); + + if (/^https?:\/\//i.test(url) || /^intent:/i.test(url)) { + window.location.href = url; + return; + } + + launcherFrame = document.createElement('iframe'); + launcherFrame.hidden = true; + launcherFrame.style.display = 'none'; + launcherFrame.setAttribute('aria-hidden', 'true'); + document.body.appendChild(launcherFrame); + launcherFrame.src = url; + + cleanupTimer = window.setTimeout(() => { + if (launcherFrame && launcherFrame.parentNode) { + launcherFrame.remove(); + launcherFrame = null; + } + }, 2600); } function createServiceCard(service) { @@ -92,28 +168,13 @@ function createServiceCard(service) { ${service.domain} `; - const mobileUrl = getMobileUrl(service); - if (mobileUrl) { + const mobileLaunch = getMobileLaunch(service); + if (mobileLaunch) { card.addEventListener('click', (e) => { if (!isMobileDevice()) return; e.preventDefault(); - - if (/^https?:\/\//i.test(mobileUrl)) { - window.location.href = mobileUrl; - return; - } - - const start = Date.now(); - // Try to open the native mobile app through its registered URL scheme. - window.location.href = mobileUrl; - - // Web fallback if the app is not installed or the scheme is not handled. - setTimeout(() => { - if (Date.now() - start < 1500) { - window.open(service.url, '_blank'); - } - }, 800); + openMobileLaunch(mobileLaunch.url, mobileLaunch.fallbackUrl); }); } diff --git a/index.html b/index.html index cee9185..1fdd833 100644 --- a/index.html +++ b/index.html @@ -119,6 +119,6 @@ - + diff --git a/sw.js b/sw.js index 38546b5..04619eb 100644 --- a/sw.js +++ b/sw.js @@ -1,10 +1,10 @@ -const CACHE_NAME = 'mischlabs-pwa-v13'; +const CACHE_NAME = 'mischlabs-pwa-v14'; const APP_SHELL = [ '/', '/index.html', '/offline.html', '/style.css?v=27', - '/app.js?v=30', + '/app.js?v=31', '/services.json?v=1', '/manifest.webmanifest?v=4', '/favicon.ico?v=4',