Files
store_ai_front/src/app/onboarding/extension/pair-handoff.tsx
2026-05-13 16:59:46 +08:00

145 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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) 换取扩展 tokenWeb 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>
);
}