feat: enhance iframe communication and caching mechanisms
Some checks failed
lotteryfront CI / build (push) Has been cancelled

- Improved the entry lifecycle management to prevent duplicate token requests and ensure smooth transitions.
- Updated the `resolvePostMessageTargetOrigin` function to return null when no allowed origins are present, enhancing security.
- Cached runtime origins to optimize performance and reduce unnecessary API calls.
- Added a new error message for frozen lottery wallets in the player notifications.
This commit is contained in:
2026-06-16 17:08:34 +08:00
parent 9a42f47ac7
commit 9711909893
9 changed files with 59 additions and 19 deletions

20
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: lotteryfront CI
on:
push:
branches: [main, master, develop]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: package-lock.json
- run: npm ci
- run: npm run lint
- run: npm run build

View File

@@ -12,3 +12,4 @@ This version has breaking changes — APIs, conventions, and file structure may
- 信用盘流水对外类型:`win_credit`(中奖释额)、`bill_settlement`(账期收付),与钱包 `prize`/派彩文案分离。
- 信用流水结余:`game_settlement_win`/`settlement_confirm`/`bet_hold_release` 会减 `used_credit` 并倒推结余;`settlement_payout`(账期收付)仅记账,`affects_available_credit: false`
- Token 进场(`EntryGate``entryLifecycleRef`idle→running→done防 URL token 剥离或 store 写入后重复触发 `doEntry` 导致成功/失败页闪现;成功后直接 `router.replace('/hall')`

View File

@@ -10,7 +10,15 @@ type RuntimeOriginsEnvelope = {
};
};
const RUNTIME_ORIGINS_TTL_MS = 60_000;
let cachedRuntimeOrigins: string[] | null = null;
let cachedAt = 0;
async function loadRuntimeOrigins(): Promise<string[]> {
if (cachedRuntimeOrigins !== null && Date.now() - cachedAt < RUNTIME_ORIGINS_TTL_MS) {
return cachedRuntimeOrigins;
}
try {
const url = `${lotteryApiOrigin()}/api/v1/integration/runtime-origins`;
const response = await fetch(url, {
@@ -25,9 +33,12 @@ async function loadRuntimeOrigins(): Promise<string[]> {
if (!Array.isArray(origins)) return [];
return origins.filter((origin): origin is string => typeof origin === "string");
cachedRuntimeOrigins = origins.filter((origin): origin is string => typeof origin === "string");
cachedAt = Date.now();
return cachedRuntimeOrigins;
} catch {
return [];
return cachedRuntimeOrigins ?? [];
}
}

View File

@@ -35,6 +35,7 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/dompurify": "^3.2.0",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",

View File

@@ -28,6 +28,9 @@ export function IframeBridge({ children }: { children: ReactNode }): ReactNode {
(type: string, payload?: Record<string, unknown>): void => {
if (typeof window === "undefined" || window.parent === window) return;
const targetOrigin = resolvePostMessageTargetOrigin();
if (!targetOrigin) return;
window.parent.postMessage(
{
type: `LOTTERY_${type}`,
@@ -35,7 +38,7 @@ export function IframeBridge({ children }: { children: ReactNode }): ReactNode {
timestamp: Date.now(),
source: "lottery-iframe",
},
resolvePostMessageTargetOrigin(),
targetOrigin,
);
},
[],

View File

@@ -87,12 +87,15 @@ export function useTokenRefresh(): {
if (typeof window === "undefined") return;
// 向主站请求新 Token
const targetOrigin = resolvePostMessageTargetOrigin();
if (!targetOrigin) return;
window.parent.postMessage(
{
type: "LOTTERY_TOKEN_REFRESH_REQUEST",
timestamp: Date.now(),
},
resolvePostMessageTargetOrigin(),
targetOrigin,
);
}, []);

View File

@@ -336,6 +336,7 @@
"2008": "Odds or play configuration has changed. Close the preview and try again.",
"2009": "This order was refunded or cannot be resubmitted. Close the preview and place a new bet.",
"1003": "Stake amount is outside the allowed range for this play type.",
"1007": "The lottery wallet is frozen. Betting is temporarily unavailable.",
"fallback": "Bet failed. Please try again later."
},
"amountHint": {

View File

@@ -4,18 +4,19 @@
* 支持 iframe 嵌入场景,允许主站加载彩票系统
*/
// 允许的主站来源
// 允许的主站来源(生产须通过环境变量配置)
const ALLOWED_PARENT_ORIGINS: string[] = [
process.env.NEXT_PUBLIC_MAIN_SITE_URL,
process.env.NEXT_PUBLIC_PARENT_ORIGIN,
// 开发环境
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://192.168.0.101:5173",
"http://localhost:3801",
"http://127.0.0.1:3801",
"http://192.168.0.101:3801",
// 生产环境应从环境变量读取
...(process.env.NODE_ENV === "development"
? [
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://localhost:3801",
"http://127.0.0.1:3801",
]
: []),
].filter((o): o is string => Boolean(o));
function normalizeOrigin(value: string): string | null {
@@ -104,10 +105,9 @@ export function generateCSP(extraParentOrigins: string[] = []): string {
* @param parentOrigin 父窗口来源
*/
export function isAllowedParent(parentOrigin: string): boolean {
if (ALLOWED_PARENT_ORIGINS.length === 0) return true; // 未配置时允许所有
return ALLOWED_PARENT_ORIGINS.some(
(origin) => origin && parentOrigin.startsWith(origin),
);
const origins = staticAllowedParentOrigins();
if (origins.length === 0) return false;
return origins.some((origin) => parentOrigin.startsWith(origin));
}
/**

View File

@@ -111,11 +111,11 @@ export function isIframeOriginAllowed(origin: string): boolean {
return allowedOrigins.includes(normalized);
}
/** postMessage 目标:优先 referrer 对应白名单,否则取首个白名单 origin */
export function resolvePostMessageTargetOrigin(): string {
/** postMessage 目标:优先 referrer 对应白名单,否则取首个白名单 origin;无白名单时拒绝发送 */
export function resolvePostMessageTargetOrigin(): string | null {
const allowedOrigins = getKnownIframeAllowedOrigins();
if (allowedOrigins.length === 0) {
return "*";
return null;
}
if (typeof document !== "undefined" && document.referrer) {