Skip to content

Commit 9b20190

Browse files
committed
feat: extend interstitial page to all mobile requests and preserve URL fragments
Serve the interstitial (try app scheme, fall back to store) for all mobile requests when app_scheme and a store fallback URL are configured, not just iOS in-app browsers. Fixes broken 302-to-URI-scheme behavior when the app is not installed. Interstitial JavaScript now reads window.location.hash and appends it to the scheme URL, preserving E2E encryption keys in URL fragments.
1 parent 7c52bfc commit 9b20190

File tree

1 file changed

+26
-14
lines changed

1 file changed

+26
-14
lines changed

src/routes/redirect.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,12 @@ function isIOSInAppBrowser(userAgent: string): boolean {
2525
}
2626

2727
/**
28-
* Generate an interstitial HTML page for iOS in-app browsers.
29-
* Tries to open the app via custom scheme URL, falls back to the App Store.
28+
* Generate an interstitial HTML page that tries to open the app via custom scheme,
29+
* then falls back to the App Store / Play Store.
30+
*
31+
* The JavaScript reads the URL fragment (window.location.hash) and appends it
32+
* to the scheme URL. This preserves the E2E encryption key, which lives only
33+
* in the fragment and is never sent to the server.
3034
*/
3135
function generateInterstitialHTML(schemeUrl: string, fallbackUrl: string, title?: string): string {
3236
const safeSchemeUrl = schemeUrl.replace(/"/g, '&quot;').replace(/</g, '&lt;');
@@ -54,11 +58,15 @@ function generateInterstitialHTML(schemeUrl: string, fallbackUrl: string, title?
5458
<div class="spinner"></div>
5559
<h1>Opening ${safeTitle}...</h1>
5660
<p>If the app doesn't open automatically:</p>
57-
<a class="btn btn-primary" href="${safeSchemeUrl}">Open App</a>
58-
<a class="btn btn-secondary" href="${safeFallbackUrl}">Download App</a>
61+
<a class="btn btn-primary" id="open-btn" href="${safeSchemeUrl}">Open App</a>
62+
<a class="btn btn-secondary" id="store-btn" href="${safeFallbackUrl}">Download App</a>
5963
</div>
6064
<script>
61-
window.location = "${safeSchemeUrl}";
65+
// Preserve URL fragment (E2E encryption key) through the scheme redirect
66+
var hash = window.location.hash || '';
67+
var schemeUrl = "${safeSchemeUrl}" + hash;
68+
document.getElementById('open-btn').href = schemeUrl;
69+
window.location = schemeUrl;
6270
setTimeout(function() { window.location.replace("${safeFallbackUrl}"); }, 1500);
6371
</script>
6472
</body></html>`;
@@ -460,16 +468,21 @@ export async function redirectRoutes(fastify: FastifyInstance) {
460468
}
461469
}
462470

463-
// For iOS in-app browsers (Gmail, Facebook, etc.), serve an interstitial page
464-
// that tries to open the app via custom scheme, then falls back to the App Store.
465-
// Universal Links don't fire from WKWebView, so a direct 302 would skip the app entirely.
466-
if (device === 'ios' && isIOSInAppBrowser(userAgent)) {
471+
// Serve an interstitial page for mobile requests when a custom scheme is available.
472+
// The interstitial tries to open the app via URI scheme, then falls back to the store.
473+
// This works for both in-app browsers (where Universal Links don't fire) and regular
474+
// browsers (where a 302 to a custom scheme fails silently if the app isn't installed).
475+
// The interstitial JavaScript preserves the URL fragment (E2E encryption key).
476+
if ((device === 'ios' || device === 'android') && link.app_scheme) {
477+
const deepPath = link.deep_link_path ? link.deep_link_path.replace(/^\//, '') : '';
467478
const schemeUrl = link.custom_scheme_url
468-
|| (link.app_scheme && link.deep_link_path
469-
? `${link.app_scheme}://${link.deep_link_path.replace(/^\//, '')}`
470-
: null);
479+
|| `${link.app_scheme}://${deepPath}`;
471480

472-
if (schemeUrl) {
481+
const storeFallback = device === 'ios'
482+
? (iosUrl || webFallbackUrl || link.original_url)
483+
: (androidUrl || webFallbackUrl || link.original_url);
484+
485+
if (storeFallback) {
473486
let fullSchemeUrl = schemeUrl;
474487
if (link.deep_link_parameters && Object.keys(link.deep_link_parameters).length > 0) {
475488
const params = new URLSearchParams(
@@ -478,7 +491,6 @@ export async function redirectRoutes(fastify: FastifyInstance) {
478491
fullSchemeUrl += (fullSchemeUrl.includes('?') ? '&' : '?') + params.toString();
479492
}
480493

481-
const storeFallback = iosUrl || webFallbackUrl || link.original_url;
482494
return reply
483495
.header('Content-Type', 'text/html; charset=utf-8')
484496
.send(generateInterstitialHTML(fullSchemeUrl, storeFallback, link.title || link.og_title));

0 commit comments

Comments
 (0)