feat: tune deeplink fallback page
This commit is contained in:
+15
-41
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user