eCourt-note/components/wordaddin/WordAddinGate.tsx
Suphonchai Phoonsawat c0833c00f7
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 4s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 1m23s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 5s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 0s
Refactor authentication flow and ticket handling in add-in
- Removed client-side HMAC verification; server now handles all HMAC checks.
- Updated ticket processing logic to improve UX and security.
- Adjusted API request handling to return discriminated results based on HTTP status.
- Enhanced error messages for better clarity on authentication failures.
- Added unit tests for the new token endpoint to ensure compliance with updated contract.
- Cleaned up unused code and comments related to previous HMAC handling.
2026-11-04 21:18:19 +07:00

158 lines
7.5 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { withBasePath } from "@/utils/base-path";
import { signInForDemo } from "@/wordaddin/services/authService";
import { MessageBar } from "./MessageBar";
import { Spinner } from "./Spinner";
import TaskpaneApp from "./TaskpaneApp";
/**
* Gate ของ taskpane ใน App Router — ทำหน้าที่แทน Office.onReady ก่อน render ของ webpack
* ต้นฉบับ: Office.js โหลดใน <head> ของ taskpane.html แล้ว Office.onReady ก่อน createRoot
* (บริบท Next: ฉีด <script> self-host หลัง hydration แทน — ไฟล์ services ทั้งหมดไม่สัมผัส
* Office ที่ module top-level จึงปลอดภัย)
*
* โหมด ?addindemo=untracked|tracking (เฉพาะ dev) — ข้าม office.js ทั้งหมด + sign in mock
* ทันที เพื่อดู UI ทุก phase ใน browser ธรรมดา (mock versioningService ทำงานได้เอง
* แต่ flow ที่แตะ Word จริงจะจบที่ error ตาม expected — ดู docs/WORDADDIN-DEV.md)
*/
/** office.js self-host จาก postinstall scripts/fetch-office-js.mjs (intranet ไม่ใช้ CDN) */
const OFFICE_JS_SRC = withBasePath("/wordaddin/office-js/office.js");
/**
* ช่วง poll ของ fallback — ทุก 5 วิเช็คว่า global `Office` โผล่แล้วหรือยัง:
* โผล่แล้วแต่ onReady เงียบ (webview แปลก ๆ) = proceed ตามเจตนาเดิม · ยังไม่โผล่
* (เครื่อง/เน็ตช้า — script ยังโหลดไม่เสร็จ) = รอต่อรอบหน้า ไม่ proceed บอด ๆ
*/
const ONREADY_FALLBACK_MS = 5_000;
/** เพดานรวมหลัง inject script — ครบแล้วยังไม่มี Office = ไปหน้า error (มีปุ่มลองใหม่) */
const ONREADY_HARD_CAP_MS = 60_000;
/**
* โหลด office.js ครั้งเดียวต่อ page เป็น singleton promise ระดับ module —
* ห้ามฉีด <script> ซ้ำ: React StrictMode รัน effect สองรอบใน dev และ guard
* `typeof Office !== "undefined"` ไม่ครอบช่วงที่ตัวแรกยังโหลดไม่เสร็จ
* (โหลดซ้ำ = evaluate ซ้ำ = "Cannot redefine property: context" เพราะ office.js
* define `Microsoft.Office.WebExtension.context` แบบ configurable:false)
* error → คืน promise ใหม่ครั้งหน้า (ลบ script เก่าทิ้งให้ inject ใหม่ได้)
*/
let officeReadyPromise: Promise<boolean> | null = null;
function ensureOfficeJs(): Promise<boolean> {
if (!officeReadyPromise) {
officeReadyPromise = new Promise<boolean>((resolve) => {
if (typeof Office !== "undefined") {
void Office.onReady(() => resolve(true));
return;
}
const script = document.createElement("script");
const startedAt = Date.now();
let fallbackTimer: ReturnType<typeof setTimeout>;
const armFallback = () => {
fallbackTimer = setTimeout(() => {
// Office โผล่แล้ว (script evaluate แล้ว) = proceed ได้เลย ไม่รอ onReady —
// กัน webview ที่โหลดสำเร็จแต่ callback เงียบ (เจตนาเดิมของ fallback)
if (typeof Office !== "undefined") {
resolve(true);
return;
}
// ยังไม่มี Office global = เครื่องช้า script ยังไม่โหลดเสร็จ — รอต่อ (re-arm)
// จนครบเพดานค่อยยอมแพ้ไปหน้า error กัน proceed แล้ว Word.run พังกลางคัน
if (Date.now() - startedAt >= ONREADY_HARD_CAP_MS) {
officeReadyPromise = null;
script.remove();
resolve(false);
return;
}
armFallback();
}, ONREADY_FALLBACK_MS);
};
armFallback(); // ตั้งก่อนเสียบ src เหมือนเดิม — ครอบทั้ง network fetch + evaluate + รอ onReady
const onReady = () => {
// เคลียร์ timer เฉพาะเมื่อ callback ของ Office.onReady ยิงจริง — ถ้าเคลียร์ตั้งแต่ onload
// แล้ว onReady เงียบ fallback จะตายไปก่อนทำหน้าที่ (ค้าง loading ตลอดไป)
void Office.onReady(() => {
clearTimeout(fallbackTimer);
resolve(true);
});
};
script.onload = onReady;
script.onerror = () => {
clearTimeout(fallbackTimer);
officeReadyPromise = null;
script.remove();
resolve(false);
};
script.src = OFFICE_JS_SRC;
script.async = false; // ให้ script นี้ได้ทำงานตามลำดับก่อนโค้ดที่แตะ Office
script.id = "office-js";
document.head.appendChild(script);
});
}
return officeReadyPromise;
}
type GateStatus = "loading" | "ready" | "error";
export function WordAddinGate() {
const [status, setStatus] = useState<GateStatus>("loading");
const [demo, setDemo] = useState<"untracked" | "tracking" | null>(null);
useEffect(() => {
const demoParam = new URLSearchParams(window.location.search).get("addindemo");
if (process.env.NODE_ENV !== "production" && (demoParam === "untracked" || demoParam === "tracking")) {
signInForDemo();
setDemo(demoParam);
setStatus("ready");
return;
}
let cancelled = false;
void ensureOfficeJs()
.then((ok) => {
if (!cancelled) setStatus(ok ? "ready" : "error");
})
.catch(() => {
// Office.onReady throw sync ใน executor กลายเป็น rejection — ไป error screen แทนค้าง loading
if (!cancelled) setStatus("error");
});
return () => {
cancelled = true;
};
}, []);
if (status === "loading") {
return (
<div className="flex min-h-screen items-center justify-center">
<Spinner label="กำลังเริ่มต้น…" className="text-ink-soft" />
</div>
);
}
if (status === "error") {
return (
<div className="flex min-h-screen items-center justify-center p-4">
<MessageBar
intent="error"
action={
<button
type="button"
className="focus-ring rounded-md border border-error/40 bg-white px-2 py-1.5 text-xs text-error-ink"
onClick={() => window.location.reload()}
>
ลองใหม่
</button>
}
>
โหลด Office.js ไม่สำเร็จ — เช็คว่า dev server รันอยู่และไฟล์ public/wordaddin/office-js/office.js
ถูกสร้างแล้ว (npm install รัน postinstall scripts/fetch-office-js.mjs)
</MessageBar>
</div>
);
}
return <TaskpaneApp demo={demo ?? undefined} />;
}
export default WordAddinGate;