feat: tune deeplink fallback page

This commit is contained in:
3252a8
2026-05-23 23:26:10 +03:00
parent 5918a6cc71
commit 46391b10e2
12 changed files with 635 additions and 213 deletions
+15 -41
View File
@@ -14,6 +14,7 @@
import AuthScreen from "./webapp/auth/AuthScreen.svelte";
import PaymentDialogs from "./webapp/PaymentDialogs.svelte";
import TariffDialogs from "./webapp/TariffDialogs.svelte";
import AppLaunchScreen from "./webapp/screens/AppLaunchScreen.svelte";
import DevicesScreen from "./webapp/screens/DevicesScreen.svelte";
import HomeScreen from "./webapp/screens/HomeScreen.svelte";
import InstallGuideScreen from "./webapp/screens/InstallGuideScreen.svelte";
@@ -508,18 +509,17 @@
return appLaunchTarget;
}
function openAppLaunchTarget() {
const target = refreshAppLaunchTarget();
if (!target) return;
function openAppLaunchTarget(nextTarget = "") {
const target = String(nextTarget || refreshAppLaunchTarget() || "").trim();
if (!target) return false;
appLaunchTarget = target;
openUrlWithHiddenAnchor(target);
return true;
}
onMount(() => {
if (isPreviewBoard) return;
if (isAppLaunchRoute) {
window.setTimeout(openAppLaunchTarget, 80);
return;
}
if (isAppLaunchRoute) return;
const onAnyPointerDown = () => {
if (mode === "login") loginEmailTooltipOpen = false;
};
@@ -1094,7 +1094,7 @@
const isTelegramMiniApp = hasTelegramLaunchParams();
const currentTg = tg || telegramSdk.refresh();
const gatewayUrl = isTelegramMiniApp ? buildExternalAppLaunchUrl(raw) : "";
const gatewayUrl = isTelegramMiniApp ? buildExternalAppLaunchUrl(raw, null, currentLang) : "";
if (gatewayUrl) {
if (currentTg?.openLink) {
try {
@@ -1427,39 +1427,13 @@
<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>
<AppLaunchScreen
{brand}
{appLaunchTarget}
{refreshAppLaunchTarget}
{openAppLaunchTarget}
{t}
/>
{:else if mode === "publicInstall"}
<div class="public-install-shell">
<a class="public-install-brand" href="/" aria-label={brandTitle}>
+7 -1
View File
@@ -33,7 +33,7 @@ export function readExternalAppLaunchTarget(locationRef = null) {
return target;
}
export function buildExternalAppLaunchUrl(value, locationRef = null) {
export function buildExternalAppLaunchUrl(value, locationRef = null, language = "") {
const target = String(value || "").trim();
if (isUnsafeAppUrl(target) || isHttpUrl(target)) return "";
@@ -41,6 +41,12 @@ export function buildExternalAppLaunchUrl(value, locationRef = null) {
if (!ref?.href) return "";
const url = new URL("/open-app", ref.href);
const lang = String(language || "")
.trim()
.toLowerCase();
if (/^[a-z]{2}(?:-[a-z0-9]{2,8})?$/.test(lang)) {
url.searchParams.set("lang", lang);
}
url.hash = new URLSearchParams({ url: target }).toString();
return url.href;
}
+1 -3
View File
@@ -2,7 +2,6 @@ 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;
@@ -29,10 +28,9 @@ async function loadBootstrap() {
}
const target = document.getElementById("app");
const skipBootstrap = isExternalAppLaunchPath(window.location.pathname);
if (target) {
(skipBootstrap ? Promise.resolve() : loadBootstrap()).finally(() => {
loadBootstrap().finally(() => {
target.replaceChildren();
mount(App, { target });
});
-51
View File
@@ -53,57 +53,6 @@ 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;
@@ -0,0 +1,241 @@
<script>
import { onMount } from "svelte";
import BrandMark from "$lib/webapp/BrandMark.svelte";
const AUTO_OPEN_DELAY_MS = 80;
const MANUAL_STATE_DELAY_MS = 1600;
const DONE_STATE_DELAY_MS = 900;
const CLOSE_ATTEMPT_DELAY_MS = 120;
export let brand = {};
export let appLaunchTarget = "";
export let refreshAppLaunchTarget = () => appLaunchTarget;
export let openAppLaunchTarget = () => false;
export let t = (_key, _params = {}, fallback = "") => fallback;
let activeTarget = appLaunchTarget;
let state = activeTarget ? "opening" : "unavailable";
let attempted = false;
let pageLeft = false;
let autoOpenTimer = null;
let manualStateTimer = null;
let doneStateTimer = null;
let closeAttemptTimer = null;
$: if (!attempted && appLaunchTarget !== activeTarget) {
activeTarget = appLaunchTarget;
state = activeTarget ? "opening" : "unavailable";
}
$: isDone = state === "done";
$: isUnavailable = state === "unavailable";
$: title = isUnavailable
? t("wa_app_launch_unavailable_title", {}, "App link unavailable")
: isDone
? t("wa_app_launch_done_title", {}, "Settings added")
: t("wa_app_launch_title", {}, "Opening app");
$: hint = isUnavailable
? t("wa_app_launch_unavailable_hint", {}, "Return to Telegram and try again.")
: isDone
? t("wa_app_launch_done_hint", {}, "If the app opened, you can close this window.")
: state === "manual"
? t(
"wa_app_launch_hint",
{},
"If the app did not open automatically, tap the button below."
)
: t("wa_app_launch_opening_hint", {}, "Opening the app on this device...");
$: openLabel = isDone
? t("wa_app_launch_retry_button", {}, "Open again")
: t("wa_app_launch_button", {}, "Open app");
onMount(() => {
autoOpenTimer = window.setTimeout(openTarget, AUTO_OPEN_DELAY_MS);
window.addEventListener("pagehide", notePageLeft);
window.addEventListener("blur", notePageLeft);
document.addEventListener("visibilitychange", handleVisibilityChange);
return () => {
clearTimer(autoOpenTimer);
clearTimer(manualStateTimer);
clearTimer(doneStateTimer);
clearTimer(closeAttemptTimer);
window.removeEventListener("pagehide", notePageLeft);
window.removeEventListener("blur", notePageLeft);
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
});
function clearTimer(timer) {
if (timer) window.clearTimeout(timer);
}
function refreshTarget() {
const target = String(refreshAppLaunchTarget?.() || appLaunchTarget || "").trim();
activeTarget = target;
if (!activeTarget) state = "unavailable";
return activeTarget;
}
function tryCloseWindow() {
try {
window.close();
} catch (_error) {
void _error;
}
}
function markDone() {
if (!attempted || state === "done" || !activeTarget) return;
state = "done";
clearTimer(closeAttemptTimer);
closeAttemptTimer = window.setTimeout(tryCloseWindow, CLOSE_ATTEMPT_DELAY_MS);
}
function notePageLeft() {
if (!attempted) return;
pageLeft = true;
clearTimer(doneStateTimer);
doneStateTimer = window.setTimeout(markDone, DONE_STATE_DELAY_MS);
}
function handleVisibilityChange() {
if (!attempted) return;
if (document.hidden) {
pageLeft = true;
return;
}
if (pageLeft) markDone();
}
function openTarget() {
const target = refreshTarget();
if (!target) {
state = "unavailable";
return;
}
attempted = true;
pageLeft = false;
state = "opening";
openAppLaunchTarget(target);
clearTimer(manualStateTimer);
manualStateTimer = window.setTimeout(() => {
if (state === "opening" && !pageLeft) state = "manual";
}, MANUAL_STATE_DELAY_MS);
}
function closeWindow() {
tryCloseWindow();
state = "done";
}
</script>
<div class="app-launch-shell">
<main class="app-launch-panel">
<div class="app-launch-brand" aria-hidden="true">
<BrandMark {brand} size="md" />
</div>
<h1>{title}</h1>
<p>{hint}</p>
<div class="app-launch-actions">
<a
class="app-launch-button"
class:disabled={isUnavailable}
href={activeTarget || "#"}
aria-disabled={isUnavailable ? "true" : undefined}
rel="noreferrer"
onclick={(event) => {
event.preventDefault();
if (!isUnavailable) openTarget();
}}
>
{openLabel}
</a>
{#if isDone}
<button class="app-launch-button secondary" type="button" onclick={closeWindow}>
{t("wa_app_launch_close_button", {}, "Close window")}
</button>
{/if}
</div>
</main>
</div>
<style>
.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-actions {
display: grid;
width: 100%;
gap: 10px;
margin-top: 4px;
}
.app-launch-button {
display: inline-flex;
width: 100%;
min-height: 48px;
align-items: center;
justify-content: center;
border: 1px solid transparent;
border-radius: 8px;
background: var(--accent);
color: var(--accent-contrast);
padding: 0 18px;
box-sizing: border-box;
font: inherit;
font-size: 15px;
font-weight: 850;
text-decoration: none;
cursor: pointer;
}
.app-launch-button.secondary {
border-color: var(--border-strong);
background: transparent;
color: var(--text);
}
.app-launch-button.disabled {
pointer-events: none;
border-color: transparent;
background: var(--panel-3);
color: var(--muted);
}
</style>