diff --git a/apps/edr-passenger-web/portal/src/app/go/page.tsx b/apps/edr-passenger-web/portal/src/app/go/page.tsx new file mode 100644 index 000000000..255b3980a --- /dev/null +++ b/apps/edr-passenger-web/portal/src/app/go/page.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { Suspense, useEffect, useMemo } from "react"; +import { useSearchParams } from "next/navigation"; +import { Loader2, ShieldAlert, ExternalLink } from "lucide-react"; + + + +const ALLOWED_HOSTS = ( + process.env.NEXT_PUBLIC_DMONEY_ALLOWED_HOSTS ?? "d-money.dj" +) + .split(",") + .map((h) => h.trim().toLowerCase()) + .filter(Boolean); + +/** True only for https URLs whose host is (or is a subdomain of) an allowlisted host. */ +function isTrustedDMoneyUrl(raw: string | null): raw is string { + if (!raw) return false; + let parsed: URL; + try { + parsed = new URL(raw); + } catch { + return false; + } + if (parsed.protocol !== "https:") return false; + const host = parsed.hostname.toLowerCase(); + return ALLOWED_HOSTS.some( + (allowed) => host === allowed || host.endsWith(`.${allowed}`), + ); +} + +const REDIRECT_DELAY_MS = 1000; + +function RedirectView() { + const searchParams = useSearchParams(); + const raw = searchParams.get("url"); + const target = useMemo(() => (isTrustedDMoneyUrl(raw) ? raw : null), [raw]); + + useEffect(() => { + if (!target) return; + const timer = setTimeout(() => { + + window.location.replace(target); + }, REDIRECT_DELAY_MS); + return () => clearTimeout(timer); + }, [target]); + + if (!target) { + return ( +
+ This link is missing a valid D-Money checkout address or points to an + untrusted destination. Please start the payment again from the app. +
++ Taking you to the secure D-Money checkout to complete your payment… +
+ + + Continue to D-Money +