2026-06-05 19:06:53 +08:00
|
|
|
import { Copy, X } from "lucide-react";
|
|
|
|
|
import { useEffect, useState, type ReactNode } from "react";
|
|
|
|
|
import { createPortal } from "react-dom";
|
|
|
|
|
import { useI18n } from "../i18n";
|
|
|
|
|
import { useToast } from "./Toast";
|
|
|
|
|
import {
|
|
|
|
|
IN_APP_DOWNLOAD_GUIDE_EVENT,
|
|
|
|
|
type InAppDownloadGuideDetail,
|
|
|
|
|
} from "./messageStream/utils/downloadFile";
|
|
|
|
|
import { inAppBrowserName } from "../utils/inAppBrowser";
|
|
|
|
|
|
|
|
|
|
async function copyTextToClipboard(text: string): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
if (
|
|
|
|
|
typeof navigator !== "undefined" &&
|
|
|
|
|
navigator.clipboard &&
|
|
|
|
|
typeof navigator.clipboard.writeText === "function"
|
|
|
|
|
) {
|
|
|
|
|
await navigator.clipboard.writeText(text);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// fall through to legacy path
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const ta = document.createElement("textarea");
|
|
|
|
|
ta.value = text;
|
|
|
|
|
ta.setAttribute("readonly", "");
|
|
|
|
|
ta.style.position = "fixed";
|
|
|
|
|
ta.style.top = "0";
|
|
|
|
|
ta.style.left = "0";
|
|
|
|
|
ta.style.opacity = "0";
|
|
|
|
|
document.body.append(ta);
|
|
|
|
|
ta.select();
|
|
|
|
|
const ok = document.execCommand("copy");
|
|
|
|
|
ta.remove();
|
|
|
|
|
return ok;
|
|
|
|
|
} catch {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function InAppDownloadGuideProvider({
|
|
|
|
|
children,
|
|
|
|
|
}: {
|
|
|
|
|
children: ReactNode;
|
|
|
|
|
}) {
|
|
|
|
|
const { t } = useI18n();
|
|
|
|
|
const { showToast } = useToast();
|
2026-06-05 19:15:10 +08:00
|
|
|
const [open, setOpen] = useState(false);
|
2026-06-05 19:06:53 +08:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const onShow = (event: Event) => {
|
|
|
|
|
const ce = event as CustomEvent<InAppDownloadGuideDetail>;
|
|
|
|
|
if (!ce.detail) return;
|
2026-06-05 19:15:10 +08:00
|
|
|
setOpen(true);
|
2026-06-05 19:06:53 +08:00
|
|
|
};
|
|
|
|
|
window.addEventListener(IN_APP_DOWNLOAD_GUIDE_EVENT, onShow);
|
|
|
|
|
return () =>
|
|
|
|
|
window.removeEventListener(IN_APP_DOWNLOAD_GUIDE_EVENT, onShow);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-06-05 19:15:10 +08:00
|
|
|
if (!open) return;
|
2026-06-05 19:06:53 +08:00
|
|
|
const onKey = (event: KeyboardEvent) => {
|
2026-06-05 19:15:10 +08:00
|
|
|
if (event.key === "Escape") setOpen(false);
|
2026-06-05 19:06:53 +08:00
|
|
|
};
|
|
|
|
|
window.addEventListener("keydown", onKey);
|
|
|
|
|
return () => window.removeEventListener("keydown", onKey);
|
2026-06-05 19:15:10 +08:00
|
|
|
}, [open]);
|
2026-06-05 19:06:53 +08:00
|
|
|
|
2026-06-05 19:15:10 +08:00
|
|
|
const close = () => setOpen(false);
|
2026-06-05 19:06:53 +08:00
|
|
|
|
2026-06-05 19:15:10 +08:00
|
|
|
const handleCopyPageLink = async () => {
|
|
|
|
|
if (typeof window === "undefined") return;
|
|
|
|
|
const ok = await copyTextToClipboard(window.location.href);
|
2026-06-05 19:06:53 +08:00
|
|
|
if (ok) {
|
|
|
|
|
showToast(t("inAppDownloadCopied"));
|
|
|
|
|
} else {
|
|
|
|
|
showToast(t("inAppDownloadCopyFail"), "error");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const browser = inAppBrowserName();
|
|
|
|
|
const intro = browser
|
|
|
|
|
? t("inAppDownloadIntroNamed").replace("{browser}", browser)
|
|
|
|
|
: t("inAppDownloadIntro");
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{children}
|
2026-06-05 19:15:10 +08:00
|
|
|
{open
|
2026-06-05 19:06:53 +08:00
|
|
|
? createPortal(
|
|
|
|
|
<div
|
|
|
|
|
role="dialog"
|
|
|
|
|
aria-modal="true"
|
|
|
|
|
aria-labelledby="in-app-download-guide-title"
|
|
|
|
|
className="fixed inset-0 z-[140] flex items-center justify-center bg-black/70 px-4 backdrop-blur-sm"
|
|
|
|
|
onClick={close}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className="w-full max-w-md overflow-hidden rounded-3xl border border-white/10 bg-[#1c1c21] text-neutral-100 shadow-2xl shadow-black/70"
|
|
|
|
|
onClick={(event) => event.stopPropagation()}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-start justify-between gap-4 border-b border-white/10 px-5 py-4">
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-ark-gold/80">
|
|
|
|
|
{t("download")}
|
|
|
|
|
</p>
|
|
|
|
|
<h2
|
|
|
|
|
id="in-app-download-guide-title"
|
|
|
|
|
className="mt-1 text-lg font-semibold text-white"
|
|
|
|
|
>
|
|
|
|
|
{t("inAppDownloadTitle")}
|
|
|
|
|
</h2>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={close}
|
|
|
|
|
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white/10 text-white transition hover:bg-white/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80"
|
|
|
|
|
aria-label={t("cancel")}
|
|
|
|
|
>
|
|
|
|
|
<X className="h-5 w-5" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-4 px-5 py-5">
|
|
|
|
|
<p className="text-sm leading-6 text-neutral-300">{intro}</p>
|
|
|
|
|
|
|
|
|
|
<ol className="space-y-3 text-sm leading-6 text-neutral-100">
|
|
|
|
|
<li className="flex gap-3">
|
|
|
|
|
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-ark-gold text-sm font-bold text-black">
|
|
|
|
|
1
|
|
|
|
|
</span>
|
|
|
|
|
<span className="pt-0.5">
|
|
|
|
|
{t("inAppDownloadStepCopy")}
|
|
|
|
|
</span>
|
|
|
|
|
</li>
|
|
|
|
|
<li className="flex gap-3">
|
|
|
|
|
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-ark-gold text-sm font-bold text-black">
|
|
|
|
|
2
|
|
|
|
|
</span>
|
|
|
|
|
<span className="pt-0.5">
|
|
|
|
|
{t("inAppDownloadStepOpen")}
|
|
|
|
|
</span>
|
|
|
|
|
</li>
|
|
|
|
|
<li className="flex gap-3">
|
|
|
|
|
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-ark-gold text-sm font-bold text-black">
|
|
|
|
|
3
|
|
|
|
|
</span>
|
|
|
|
|
<span className="pt-0.5">
|
|
|
|
|
{t("inAppDownloadStepDownload")}
|
|
|
|
|
</span>
|
|
|
|
|
</li>
|
|
|
|
|
</ol>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
2026-06-05 19:15:10 +08:00
|
|
|
onClick={handleCopyPageLink}
|
2026-06-05 19:06:53 +08:00
|
|
|
className="flex h-11 w-full items-center justify-center gap-2 rounded-full bg-ark-gold px-4 text-sm font-semibold text-black transition hover:bg-ark-gold2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-[#1c1c21]"
|
|
|
|
|
>
|
|
|
|
|
<Copy className="h-4 w-4" />
|
2026-06-05 19:15:10 +08:00
|
|
|
{t("inAppDownloadCopyPageLink")}
|
2026-06-05 19:06:53 +08:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>,
|
|
|
|
|
document.body,
|
|
|
|
|
)
|
|
|
|
|
: null}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|