145 lines
5.2 KiB
TypeScript
145 lines
5.2 KiB
TypeScript
"use client";
|
||
|
||
import {useEffect, useState} from "react";
|
||
import {useSearchParams} from "next/navigation";
|
||
|
||
import useUserStore from "@/store/user";
|
||
import {pairExtensionTokenApi} from "@/api/scan";
|
||
import {getSettingApi} from "@/api/set";
|
||
import {EXTENSION_ID} from "@/utils/extension/detect_extension";
|
||
|
||
declare const chrome: any;
|
||
|
||
/**
|
||
* 网页侧“配对回传”:
|
||
* - 由扩展 popup 的“登录”按钮打开本页(带 from=extension)。
|
||
* - 若网页已登录:调用 `/api/auth/extension-pair` 拿到 extension token,然后回传给扩展保存。
|
||
* 中文备注:这里不搞复杂类型,按需求全用 any。
|
||
*/
|
||
export default function PairHandoff() {
|
||
const params = useSearchParams();
|
||
const token = useUserStore((s) => s.token);
|
||
const [status, setStatus] = useState<"idle" | "pairing" | "ok" | "need_login" | "error">("idle");
|
||
const [errorText, setErrorText] = useState("");
|
||
|
||
useEffect(() => {
|
||
const from = params.get("from");
|
||
if (from !== "extension") return;
|
||
|
||
// 中文备注:必须在浏览器环境 + 可调用 chrome.runtime 的情况下才能回传。
|
||
if (typeof chrome === "undefined" || !chrome.runtime?.sendMessage) {
|
||
return;
|
||
}
|
||
|
||
if (!token) {
|
||
setStatus("need_login");
|
||
return;
|
||
}
|
||
|
||
void (async () => {
|
||
setStatus("pairing");
|
||
setErrorText("");
|
||
|
||
try {
|
||
const extId = params.get("extId") || EXTENSION_ID;
|
||
|
||
// 1) 先问扩展是否已经配对(避免重复 pair)
|
||
const authCheck: any = await new Promise((resolve, reject) => {
|
||
chrome.runtime.sendMessage(
|
||
extId,
|
||
{type: "DIANSHAN_AUTH_CHECK"},
|
||
(resp: any) => {
|
||
if (chrome.runtime.lastError) {
|
||
reject(new Error(chrome.runtime.lastError.message));
|
||
return;
|
||
}
|
||
resolve(resp);
|
||
},
|
||
);
|
||
});
|
||
|
||
if (authCheck?.data?.authed) {
|
||
setStatus("ok");
|
||
return;
|
||
}
|
||
|
||
// 2) 换取扩展 token(Web token -> extension token)
|
||
const pair = await pairExtensionTokenApi();
|
||
|
||
// 3) 读取品牌/店铺/定时配置(给扩展做 ingest 与 scheduled scan 使用)
|
||
const settings: any = await getSettingApi();
|
||
const brand = settings?.brand || null;
|
||
const storeId = brand?.stores?.[0]?.id || brand?.storeId || brand?.store_id || null;
|
||
|
||
const authState: any = {
|
||
token: pair?.token,
|
||
apiBaseUrl: pair?.apiBaseUrl,
|
||
userEmail: pair?.userEmail ?? null,
|
||
brandId: brand?.id ?? null,
|
||
storeId,
|
||
timezone: brand?.timezone ?? null,
|
||
morningBriefHour: brand?.morning_brief_hour ?? null,
|
||
eveningRecapHour: brand?.evening_recap_hour ?? null,
|
||
};
|
||
|
||
// 4) 回传给扩展保存
|
||
await new Promise<void>((resolve, reject) => {
|
||
chrome.runtime.sendMessage(
|
||
extId,
|
||
{type: "DIANSHAN_SSO_HANDOFF", payload: {authState}},
|
||
(resp: any) => {
|
||
if (chrome.runtime.lastError) {
|
||
reject(new Error(chrome.runtime.lastError.message));
|
||
return;
|
||
}
|
||
if (!resp?.ok) {
|
||
reject(new Error(resp?.error || "handoff_failed"));
|
||
return;
|
||
}
|
||
resolve();
|
||
},
|
||
);
|
||
});
|
||
|
||
setStatus("ok");
|
||
} catch (e: any) {
|
||
setStatus("error");
|
||
setErrorText(e?.message || "Pairing failed.");
|
||
}
|
||
})();
|
||
}, [params, token]);
|
||
|
||
if (status === "idle") return null;
|
||
|
||
if (status === "need_login") {
|
||
return (
|
||
<div className="rounded-lg border border-border/60 bg-muted/20 p-4 text-xs text-muted-foreground">
|
||
请先在网页完成登录,然后保持本页面打开,系统会自动把登录态同步给扩展。
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (status === "pairing") {
|
||
return (
|
||
<div className="rounded-lg border border-border/60 bg-muted/20 p-4 text-xs text-muted-foreground">
|
||
正在与扩展配对中…
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (status === "ok") {
|
||
return (
|
||
<div className="rounded-lg border border-emerald-200 bg-emerald-50/60 p-4 text-xs text-emerald-700">
|
||
扩展已配对成功,你可以回到扩展继续操作。
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="rounded-lg border border-rose-200 bg-rose-50/60 p-4 text-xs text-rose-700">
|
||
配对失败:{errorText || "unknown error"}
|
||
</div>
|
||
);
|
||
}
|
||
|