feat(navigation): add external entry routing
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import {
|
||||
persistExternalEntryPayload,
|
||||
resolveExternalEntryTarget,
|
||||
} from "@/lib/navigation/external_entry";
|
||||
import { Logger } from "@/utils";
|
||||
|
||||
const log = new Logger("ExternalEntryPersist");
|
||||
|
||||
interface ExternalEntryPersistProps {
|
||||
deviceId: string | null;
|
||||
facebookId: string | null;
|
||||
facebookAppId: string | null;
|
||||
avatarUrl: string | null;
|
||||
target: string | null;
|
||||
redirect: string | null;
|
||||
next: string | null;
|
||||
}
|
||||
|
||||
export default function ExternalEntryPersist({
|
||||
deviceId,
|
||||
facebookId,
|
||||
facebookAppId,
|
||||
avatarUrl,
|
||||
target,
|
||||
redirect,
|
||||
next,
|
||||
}: ExternalEntryPersistProps) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const destination = resolveExternalEntryTarget({
|
||||
target,
|
||||
redirect,
|
||||
next,
|
||||
});
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
await persistExternalEntryPayload({
|
||||
deviceId,
|
||||
facebookId,
|
||||
facebookAppId,
|
||||
avatarUrl,
|
||||
});
|
||||
} catch (error) {
|
||||
log.warn("[ExternalEntryPersist] failed to persist payload", error);
|
||||
} finally {
|
||||
router.replace(destination);
|
||||
}
|
||||
})();
|
||||
}, [
|
||||
avatarUrl,
|
||||
deviceId,
|
||||
facebookAppId,
|
||||
facebookId,
|
||||
next,
|
||||
redirect,
|
||||
router,
|
||||
target,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-1 items-center justify-center"
|
||||
style={{ minHeight: "100dvh" }}
|
||||
>
|
||||
<div
|
||||
aria-label="Redirecting"
|
||||
className="size-10 animate-spin rounded-full border-4 border-t-transparent"
|
||||
style={{ borderColor: "var(--color-accent)" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 外部入口落地页(Server Component)
|
||||
*
|
||||
* 外部应用可以通过 query 传入 Facebook 相关信息和最终目标页,例如:
|
||||
* `/external-entry?target=chat&fbid=xxx&fb_app_id=yyy`
|
||||
*
|
||||
* 页面不直接 `redirect()`,而是把数据交给 Client 组件先写入本地存储,
|
||||
* 再通过 `router.replace()` 清理 URL 并跳转到最终页面。
|
||||
*/
|
||||
|
||||
import ExternalEntryPersist from "./external-entry-persist";
|
||||
|
||||
type SearchParams = Promise<Record<string, string | string[] | undefined>>;
|
||||
|
||||
interface ExternalEntryPageProps {
|
||||
searchParams: SearchParams;
|
||||
}
|
||||
|
||||
export default async function ExternalEntryPage({
|
||||
searchParams,
|
||||
}: ExternalEntryPageProps) {
|
||||
const params = await searchParams;
|
||||
|
||||
return (
|
||||
<ExternalEntryPersist
|
||||
deviceId={pickFirst(params.deviceId, params.device_id)}
|
||||
facebookId={pickFirst(
|
||||
params.fbid,
|
||||
params.fb_id,
|
||||
params.facebookId,
|
||||
params.facebook_id,
|
||||
)}
|
||||
facebookAppId={pickFirst(
|
||||
params.fbAppId,
|
||||
params.fb_app_id,
|
||||
params.facebookAppId,
|
||||
params.facebook_app_id,
|
||||
params.appId,
|
||||
params.app_id,
|
||||
)}
|
||||
avatarUrl={pickFirst(params.avatarUrl, params.avatar_url)}
|
||||
target={pickFirst(params.target)}
|
||||
redirect={pickFirst(params.redirect)}
|
||||
next={pickFirst(params.next)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function pickFirst(
|
||||
...values: Array<string | string[] | undefined>
|
||||
): string | null {
|
||||
for (const value of values) {
|
||||
if (Array.isArray(value)) {
|
||||
const first = value.find((item) => item.trim().length > 0);
|
||||
if (first) return first;
|
||||
continue;
|
||||
}
|
||||
if (value && value.trim().length > 0) return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user