feat: enhance iframe communication and caching mechanisms
Some checks failed
lotteryfront CI / build (push) Has been cancelled
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:
20
.github/workflows/ci.yml
vendored
Normal file
20
.github/workflows/ci.yml
vendored
Normal 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
|
||||||
@@ -12,3 +12,4 @@ This version has breaking changes — APIs, conventions, and file structure may
|
|||||||
|
|
||||||
- 信用盘流水对外类型:`win_credit`(中奖释额)、`bill_settlement`(账期收付),与钱包 `prize`/派彩文案分离。
|
- 信用盘流水对外类型:`win_credit`(中奖释额)、`bill_settlement`(账期收付),与钱包 `prize`/派彩文案分离。
|
||||||
- 信用流水结余:`game_settlement_win`/`settlement_confirm`/`bet_hold_release` 会减 `used_credit` 并倒推结余;`settlement_payout`(账期收付)仅记账,`affects_available_credit: false`。
|
- 信用流水结余:`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')`。
|
||||||
|
|||||||
@@ -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[]> {
|
async function loadRuntimeOrigins(): Promise<string[]> {
|
||||||
|
if (cachedRuntimeOrigins !== null && Date.now() - cachedAt < RUNTIME_ORIGINS_TTL_MS) {
|
||||||
|
return cachedRuntimeOrigins;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = `${lotteryApiOrigin()}/api/v1/integration/runtime-origins`;
|
const url = `${lotteryApiOrigin()}/api/v1/integration/runtime-origins`;
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
@@ -25,9 +33,12 @@ async function loadRuntimeOrigins(): Promise<string[]> {
|
|||||||
|
|
||||||
if (!Array.isArray(origins)) return [];
|
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 {
|
} catch {
|
||||||
return [];
|
return cachedRuntimeOrigins ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/dompurify": "^3.2.0",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ export function IframeBridge({ children }: { children: ReactNode }): ReactNode {
|
|||||||
(type: string, payload?: Record<string, unknown>): void => {
|
(type: string, payload?: Record<string, unknown>): void => {
|
||||||
if (typeof window === "undefined" || window.parent === window) return;
|
if (typeof window === "undefined" || window.parent === window) return;
|
||||||
|
|
||||||
|
const targetOrigin = resolvePostMessageTargetOrigin();
|
||||||
|
if (!targetOrigin) return;
|
||||||
|
|
||||||
window.parent.postMessage(
|
window.parent.postMessage(
|
||||||
{
|
{
|
||||||
type: `LOTTERY_${type}`,
|
type: `LOTTERY_${type}`,
|
||||||
@@ -35,7 +38,7 @@ export function IframeBridge({ children }: { children: ReactNode }): ReactNode {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
source: "lottery-iframe",
|
source: "lottery-iframe",
|
||||||
},
|
},
|
||||||
resolvePostMessageTargetOrigin(),
|
targetOrigin,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
|
|||||||
@@ -87,12 +87,15 @@ export function useTokenRefresh(): {
|
|||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
// 向主站请求新 Token
|
// 向主站请求新 Token
|
||||||
|
const targetOrigin = resolvePostMessageTargetOrigin();
|
||||||
|
if (!targetOrigin) return;
|
||||||
|
|
||||||
window.parent.postMessage(
|
window.parent.postMessage(
|
||||||
{
|
{
|
||||||
type: "LOTTERY_TOKEN_REFRESH_REQUEST",
|
type: "LOTTERY_TOKEN_REFRESH_REQUEST",
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
},
|
},
|
||||||
resolvePostMessageTargetOrigin(),
|
targetOrigin,
|
||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -336,6 +336,7 @@
|
|||||||
"2008": "Odds or play configuration has changed. Close the preview and try again.",
|
"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.",
|
"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.",
|
"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."
|
"fallback": "Bet failed. Please try again later."
|
||||||
},
|
},
|
||||||
"amountHint": {
|
"amountHint": {
|
||||||
|
|||||||
@@ -4,18 +4,19 @@
|
|||||||
* 支持 iframe 嵌入场景,允许主站加载彩票系统
|
* 支持 iframe 嵌入场景,允许主站加载彩票系统
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 允许的主站来源
|
// 允许的主站来源(生产须通过环境变量配置)
|
||||||
const ALLOWED_PARENT_ORIGINS: string[] = [
|
const ALLOWED_PARENT_ORIGINS: string[] = [
|
||||||
process.env.NEXT_PUBLIC_MAIN_SITE_URL,
|
process.env.NEXT_PUBLIC_MAIN_SITE_URL,
|
||||||
process.env.NEXT_PUBLIC_PARENT_ORIGIN,
|
process.env.NEXT_PUBLIC_PARENT_ORIGIN,
|
||||||
// 开发环境
|
// 开发环境
|
||||||
"http://localhost:5173",
|
...(process.env.NODE_ENV === "development"
|
||||||
"http://127.0.0.1:5173",
|
? [
|
||||||
"http://192.168.0.101:5173",
|
"http://localhost:5173",
|
||||||
"http://localhost:3801",
|
"http://127.0.0.1:5173",
|
||||||
"http://127.0.0.1:3801",
|
"http://localhost:3801",
|
||||||
"http://192.168.0.101:3801",
|
"http://127.0.0.1:3801",
|
||||||
// 生产环境应从环境变量读取
|
]
|
||||||
|
: []),
|
||||||
].filter((o): o is string => Boolean(o));
|
].filter((o): o is string => Boolean(o));
|
||||||
|
|
||||||
function normalizeOrigin(value: string): string | null {
|
function normalizeOrigin(value: string): string | null {
|
||||||
@@ -104,10 +105,9 @@ export function generateCSP(extraParentOrigins: string[] = []): string {
|
|||||||
* @param parentOrigin 父窗口来源
|
* @param parentOrigin 父窗口来源
|
||||||
*/
|
*/
|
||||||
export function isAllowedParent(parentOrigin: string): boolean {
|
export function isAllowedParent(parentOrigin: string): boolean {
|
||||||
if (ALLOWED_PARENT_ORIGINS.length === 0) return true; // 未配置时允许所有
|
const origins = staticAllowedParentOrigins();
|
||||||
return ALLOWED_PARENT_ORIGINS.some(
|
if (origins.length === 0) return false;
|
||||||
(origin) => origin && parentOrigin.startsWith(origin),
|
return origins.some((origin) => parentOrigin.startsWith(origin));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -111,11 +111,11 @@ export function isIframeOriginAllowed(origin: string): boolean {
|
|||||||
return allowedOrigins.includes(normalized);
|
return allowedOrigins.includes(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** postMessage 目标:优先 referrer 对应白名单,否则取首个白名单 origin */
|
/** postMessage 目标:优先 referrer 对应白名单,否则取首个白名单 origin;无白名单时拒绝发送 */
|
||||||
export function resolvePostMessageTargetOrigin(): string {
|
export function resolvePostMessageTargetOrigin(): string | null {
|
||||||
const allowedOrigins = getKnownIframeAllowedOrigins();
|
const allowedOrigins = getKnownIframeAllowedOrigins();
|
||||||
if (allowedOrigins.length === 0) {
|
if (allowedOrigins.length === 0) {
|
||||||
return "*";
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof document !== "undefined" && document.referrer) {
|
if (typeof document !== "undefined" && document.referrer) {
|
||||||
|
|||||||
Reference in New Issue
Block a user