fix: open install guide deeplinks via external app gateway
This commit is contained in:
+83
-23
@@ -39,6 +39,14 @@
|
||||
readJsonScript,
|
||||
structuredCloneSafe,
|
||||
} from "./lib/webapp/browser.js";
|
||||
import {
|
||||
buildExternalAppLaunchUrl,
|
||||
hasControlChars,
|
||||
isExternalAppLaunchPath,
|
||||
isHttpUrl,
|
||||
openUrlWithHiddenAnchor,
|
||||
readExternalAppLaunchTarget,
|
||||
} from "./lib/webapp/appLinks.js";
|
||||
import { createApiClient } from "./lib/webapp/publicApi.js";
|
||||
import { createI18n } from "./lib/webapp/i18n.js";
|
||||
import { normalizedEmail, telegramName } from "./lib/webapp/formatters.js";
|
||||
@@ -80,6 +88,7 @@
|
||||
} from "./lib/webapp/routes.js";
|
||||
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
const isAppLaunchRoute = isExternalAppLaunchPath(window.location.pathname);
|
||||
applyPreviewMock(query.get("mock"));
|
||||
const isPreviewBoard = query.get("preview") === "all";
|
||||
const injectedConfig = readJsonScript("webapp-config");
|
||||
@@ -98,10 +107,11 @@
|
||||
let telegramSdkStatus = "idle";
|
||||
let telegramMiniAppInitData = "";
|
||||
|
||||
let mode = isPreviewBoard ? "preview" : "loading";
|
||||
let mode = isAppLaunchRoute ? "appLaunch" : isPreviewBoard ? "preview" : "loading";
|
||||
let activeTab = "home";
|
||||
let screen = "home";
|
||||
let data = isPreviewBoard ? structuredCloneSafe(DEV_MOCK.data) : null;
|
||||
let appLaunchTarget = isAppLaunchRoute ? readExternalAppLaunchTarget() : "";
|
||||
let publicInstallSubscription = null;
|
||||
let publicInstallToken = "";
|
||||
let trialBusy = false;
|
||||
@@ -493,8 +503,23 @@
|
||||
return Boolean(enabled && sub?.active);
|
||||
}
|
||||
|
||||
function refreshAppLaunchTarget() {
|
||||
appLaunchTarget = readExternalAppLaunchTarget();
|
||||
return appLaunchTarget;
|
||||
}
|
||||
|
||||
function openAppLaunchTarget() {
|
||||
const target = refreshAppLaunchTarget();
|
||||
if (!target) return;
|
||||
openUrlWithHiddenAnchor(target);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (isPreviewBoard) return;
|
||||
if (isAppLaunchRoute) {
|
||||
window.setTimeout(openAppLaunchTarget, 80);
|
||||
return;
|
||||
}
|
||||
const onAnyPointerDown = () => {
|
||||
if (mode === "login") loginEmailTooltipOpen = false;
|
||||
};
|
||||
@@ -1057,42 +1082,43 @@
|
||||
window.location.assign(url);
|
||||
}
|
||||
|
||||
function hasControlChars(value) {
|
||||
return Array.from(String(value || "")).some((char) => {
|
||||
const code = char.charCodeAt(0);
|
||||
return code <= 31 || code === 127;
|
||||
});
|
||||
}
|
||||
|
||||
function openAppLink(url) {
|
||||
const raw = String(url || "").trim();
|
||||
if (!raw || hasControlChars(raw) || /^(javascript|data|vbscript):/i.test(raw)) {
|
||||
return;
|
||||
}
|
||||
if (/^https?:\/\//i.test(raw)) {
|
||||
if (isHttpUrl(raw)) {
|
||||
openExternalLink(raw);
|
||||
return;
|
||||
}
|
||||
if (/^tg:\/\//i.test(raw) && tg?.openTelegramLink) {
|
||||
|
||||
const isTelegramMiniApp = hasTelegramLaunchParams();
|
||||
const currentTg = tg || telegramSdk.refresh();
|
||||
const gatewayUrl = isTelegramMiniApp ? buildExternalAppLaunchUrl(raw) : "";
|
||||
if (gatewayUrl) {
|
||||
if (currentTg?.openLink) {
|
||||
try {
|
||||
tg = currentTg;
|
||||
currentTg.openLink(gatewayUrl);
|
||||
return;
|
||||
} catch {
|
||||
// Fall back to regular browser navigation below.
|
||||
}
|
||||
}
|
||||
window.location.assign(gatewayUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
if (/^tg:\/\//i.test(raw) && currentTg?.openTelegramLink) {
|
||||
try {
|
||||
tg.openTelegramLink(raw);
|
||||
tg = currentTg;
|
||||
currentTg.openTelegramLink(raw);
|
||||
return;
|
||||
} catch {
|
||||
// Fall back to the generic deeplink path below.
|
||||
}
|
||||
}
|
||||
try {
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = raw;
|
||||
anchor.target = "_self";
|
||||
anchor.rel = "noreferrer";
|
||||
anchor.style.display = "none";
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
} catch {
|
||||
window.location.assign(raw);
|
||||
}
|
||||
openUrlWithHiddenAnchor(raw);
|
||||
}
|
||||
|
||||
function openConnectLink() {
|
||||
@@ -1400,6 +1426,40 @@
|
||||
<BrandMark {brand} size="md" />
|
||||
<div>{t("wa_loading")}</div>
|
||||
</div>
|
||||
{:else if mode === "appLaunch"}
|
||||
<div class="app-launch-shell">
|
||||
<main class="app-launch-panel">
|
||||
<div class="app-launch-brand" aria-hidden="true">
|
||||
<BrandMark {brand} size="md" />
|
||||
</div>
|
||||
{#if appLaunchTarget}
|
||||
<h1>{t("wa_app_launch_title", {}, "Opening app")}</h1>
|
||||
<p>
|
||||
{t(
|
||||
"wa_app_launch_hint",
|
||||
{},
|
||||
"If the app did not open automatically, tap the button below."
|
||||
)}
|
||||
</p>
|
||||
<a
|
||||
class="app-launch-button"
|
||||
href={appLaunchTarget}
|
||||
rel="noreferrer"
|
||||
onclick={(event) => {
|
||||
event.preventDefault();
|
||||
openAppLaunchTarget();
|
||||
}}
|
||||
>
|
||||
{t("wa_app_launch_button", {}, "Open app")}
|
||||
</a>
|
||||
{:else}
|
||||
<h1>{t("wa_app_launch_unavailable_title", {}, "App link unavailable")}</h1>
|
||||
<p>
|
||||
{t("wa_app_launch_unavailable_hint", {}, "Return to Telegram and try again.")}
|
||||
</p>
|
||||
{/if}
|
||||
</main>
|
||||
</div>
|
||||
{:else if mode === "publicInstall"}
|
||||
<div class="public-install-shell">
|
||||
<a class="public-install-brand" href="/" aria-label={brandTitle}>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
export function hasControlChars(value) {
|
||||
return Array.from(String(value || "")).some((char) => {
|
||||
const code = char.charCodeAt(0);
|
||||
return code <= 31 || code === 127;
|
||||
});
|
||||
}
|
||||
|
||||
export function isUnsafeAppUrl(value) {
|
||||
return (
|
||||
!String(value || "").trim() ||
|
||||
hasControlChars(value) ||
|
||||
/^(?:javascript|data|vbscript):/i.test(String(value || "").trim())
|
||||
);
|
||||
}
|
||||
|
||||
export function isHttpUrl(value) {
|
||||
return /^https?:\/\//i.test(String(value || "").trim());
|
||||
}
|
||||
|
||||
export function isExternalAppLaunchPath(pathname) {
|
||||
const normalized = String(pathname || "")
|
||||
.trim()
|
||||
.replace(/\/+$/, "");
|
||||
return normalized === "/open-app";
|
||||
}
|
||||
|
||||
export function readExternalAppLaunchTarget(locationRef = null) {
|
||||
const ref = locationRef || (typeof window === "undefined" ? null : window.location);
|
||||
if (!ref?.hash) return "";
|
||||
|
||||
const target = String(new URLSearchParams(ref.hash.replace(/^#/, "")).get("url") || "").trim();
|
||||
if (isUnsafeAppUrl(target) || isHttpUrl(target)) return "";
|
||||
return target;
|
||||
}
|
||||
|
||||
export function buildExternalAppLaunchUrl(value, locationRef = null) {
|
||||
const target = String(value || "").trim();
|
||||
if (isUnsafeAppUrl(target) || isHttpUrl(target)) return "";
|
||||
|
||||
const ref = locationRef || (typeof window === "undefined" ? null : window.location);
|
||||
if (!ref?.href) return "";
|
||||
|
||||
const url = new URL("/open-app", ref.href);
|
||||
url.hash = new URLSearchParams({ url: target }).toString();
|
||||
return url.href;
|
||||
}
|
||||
|
||||
export function openUrlWithHiddenAnchor(url) {
|
||||
try {
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.target = "_self";
|
||||
anchor.rel = "noreferrer";
|
||||
anchor.style.display = "none";
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
} catch {
|
||||
window.location.assign(url);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { mount } from "svelte";
|
||||
|
||||
import App from "./App.svelte";
|
||||
import "./styles.css";
|
||||
import { isExternalAppLaunchPath } from "./lib/webapp/appLinks.js";
|
||||
|
||||
async function loadBootstrap() {
|
||||
if (document.getElementById("webapp-config")) return;
|
||||
@@ -28,9 +29,10 @@ async function loadBootstrap() {
|
||||
}
|
||||
|
||||
const target = document.getElementById("app");
|
||||
const skipBootstrap = isExternalAppLaunchPath(window.location.pathname);
|
||||
|
||||
if (target) {
|
||||
loadBootstrap().finally(() => {
|
||||
(skipBootstrap ? Promise.resolve() : loadBootstrap()).finally(() => {
|
||||
target.replaceChildren();
|
||||
mount(App, { target });
|
||||
});
|
||||
|
||||
@@ -53,6 +53,57 @@ a {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.app-launch-shell {
|
||||
display: grid;
|
||||
min-height: 100dvh;
|
||||
place-items: center;
|
||||
padding: max(24px, env(safe-area-inset-top)) max(18px, env(safe-area-inset-right))
|
||||
max(24px, env(safe-area-inset-bottom)) max(18px, env(safe-area-inset-left));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.app-launch-panel {
|
||||
display: grid;
|
||||
width: min(100%, 420px);
|
||||
gap: 14px;
|
||||
justify-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.app-launch-brand {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.app-launch-panel h1 {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-size: 24px;
|
||||
line-height: 1.18;
|
||||
}
|
||||
|
||||
.app-launch-panel p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 15px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.app-launch-button {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background: var(--accent);
|
||||
color: var(--accent-contrast);
|
||||
padding: 0 18px;
|
||||
box-sizing: border-box;
|
||||
font-size: 15px;
|
||||
font-weight: 850;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.loader {
|
||||
display: grid;
|
||||
min-height: 100dvh;
|
||||
|
||||
Reference in New Issue
Block a user