feat(docs, integration): update integration documentation and redirect legacy paths

Introduced a new public documentation site for client integration at `/docs` and `/docs/integration`, removing the need for admin login. Updated the integration guide to redirect from the old admin path to the new documentation site. Added localization support for the integration documentation in English, Nepali, and Chinese. Enhanced the layout structure and improved the handling of currency display in settlement bills.
This commit is contained in:
2026-06-15 11:08:19 +08:00
parent e7b72cfdca
commit 17335cb47a
35 changed files with 2668 additions and 436 deletions

View File

@@ -0,0 +1,154 @@
/** 代码示例(语言无关,三语共用) */
export const SSO_JWT_PAYLOAD_EXAMPLE = `{
"site_code": "demo",
"site_player_id": "100001",
"iat": 1718000000,
"exp": 1718000300
}`;
export const SSO_JWT_SIGN_EXAMPLE = `const header = base64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
const payload = base64url(JSON.stringify({
site_code: "demo",
site_player_id: "100001",
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 300,
}));
const sig = hmacSha256Base64url(\`\${header}.\${payload}\`, SSO_JWT_SECRET);
const token = \`\${header}.\${payload}.\${sig}\`;`;
export const SSO_ENTRY_URL = `https://{lottery_host}/?token={JWT}`;
export const SSO_POSTMESSAGE = `iframe.contentWindow.postMessage(
{ type: "MAIN_INIT_TOKEN", token: jwt, source: "main-site" },
"https://lottery.example.com"
);`;
export const PLAYER_ME_REQUEST = `GET /api/v1/player/me
Authorization: Bearer {JWT}
Accept-Language: zh`;
export const PLAYER_ME_SUCCESS = `{
"code": 0,
"msg": "success",
"data": {
"id": 42,
"site_code": "demo",
"site_player_id": "100001",
"auth_source": "main_site_sso",
"funding_mode": "wallet",
"username": null,
"nickname": null,
"default_currency": "NPR",
"status": 0,
"locale": "zh",
"last_login_at": "2026-06-14T10:00:00+00:00",
"created_at": "2026-06-14T10:00:00+00:00"
}
}`;
export const IFRAME_CHILD_READY = `{
"type": "LOTTERY_READY",
"payload": { "url": "https://lottery.example.com/", "userAgent": "..." },
"timestamp": 1718000000000,
"source": "lottery-iframe"
}`;
export const IFRAME_PARENT_INIT = `{
"type": "MAIN_INIT_TOKEN",
"token": "eyJhbGciOiJIUzI1NiIs...",
"timestamp": 1718000000000,
"source": "main-site"
}`;
export const ACCEPTANCE_PLAYER_ME = `curl -sS "https://{lottery_api}/api/v1/player/me" \\
-H "Authorization: Bearer {JWT}" \\
-H "Accept: application/json"`;
export const ACCEPTANCE_WALLET_DEBIT = `curl -sS -X POST "https://{wallet_host}/wallet/debit-for-lottery" \\
-H "Authorization: Bearer {wallet_api_key}" \\
-H "Content-Type: application/json" \\
-d '{
"site_code": "demo",
"site_player_id": "100001",
"player_id": 42,
"currency_code": "NPR",
"amount_minor": 100,
"idempotent_key": "accept-debit-001"
}'`;
export const PLAYER_AUTH_ERROR = `HTTP/1.1 401 Unauthorized
{
"code": 8002,
"msg": "Token 无效或已过期",
"data": null
}`;
export const WALLET_BALANCE_RESPONSE = `{
"success": true,
"data": { "main_balance": 500000, "currency_code": "NPR" }
}`;
export const WALLET_DEBIT_REQUEST = `POST /wallet/debit-for-lottery
Authorization: Bearer {wallet_api_key}
{
"site_code": "demo",
"site_player_id": "100001",
"player_id": 42,
"currency_code": "NPR",
"amount_minor": 2000,
"idempotent_key": "TI-20260610-abc"
}`;
export const WALLET_SUCCESS = `{
"success": true,
"external_ref_no": "MW-001",
"data": { "main_balance": 498000, "currency_code": "NPR" }
}`;
export const WALLET_FAIL = `{
"success": false,
"message": "main balance insufficient",
"data": { "main_balance": 500, "currency_code": "NPR" }
}`;
export const WALLET_CREDIT_REQUEST = `POST /wallet/credit-from-lottery
Authorization: Bearer {wallet_api_key}
{
"site_code": "demo",
"site_player_id": "100001",
"player_id": 42,
"currency_code": "NPR",
"amount_minor": 2000,
"idempotent_key": "TO-20260610-abc"
}`;
export const TRANSFER_IN = `POST /api/v1/wallet/transfer-in
Authorization: Bearer {player_jwt}
{ "amount": 2000, "currency": "NPR", "idempotent_key": "ti-001" }`;
export const TRANSFER_OUT = `POST /api/v1/wallet/transfer-out
Authorization: Bearer {player_jwt}
{ "amount": 1000, "currency": "NPR", "idempotent_key": "to-001" }`;
export const TRANSFER_SUCCESS = `{
"code": 0,
"msg": "success",
"data": {
"transfer_no": "TI20260614001",
"direction": "in",
"currency_code": "NPR",
"amount": 2000,
"status": "success",
"external_ref_no": "debit-for-lottery-1718360000-a1b2c3",
"balance": 2000,
"log_id": "WX_20260614_0001",
"lottery_balance_after": 2000,
"lottery_available_after": 2000,
"finished_at": "2026-06-14T10:00:00+00:00"
}
}`;

View File

@@ -0,0 +1,314 @@
"use client";
import {
DocCode,
DocEndpoint,
DocList,
DocNote,
DocOrderedList,
DocPageHeader,
DocSection,
DocTable,
} from "@/components/docs/doc-ui";
import {
SSO_ENTRY_URL,
SSO_JWT_PAYLOAD_EXAMPLE,
SSO_JWT_SIGN_EXAMPLE,
SSO_POSTMESSAGE,
ACCEPTANCE_PLAYER_ME,
ACCEPTANCE_WALLET_DEBIT,
IFRAME_CHILD_READY,
IFRAME_PARENT_INIT,
PLAYER_AUTH_ERROR,
PLAYER_ME_REQUEST,
PLAYER_ME_SUCCESS,
TRANSFER_IN,
TRANSFER_OUT,
TRANSFER_SUCCESS,
WALLET_BALANCE_RESPONSE,
WALLET_CREDIT_REQUEST,
WALLET_DEBIT_REQUEST,
WALLET_FAIL,
WALLET_SUCCESS,
} from "@/modules/docs/integration/integration-doc-data";
import { useIntegrationDoc } from "@/modules/docs/integration/use-integration-doc";
function DocPage({ children }: { children: React.ReactNode }): React.ReactElement {
return <div className="space-y-8">{children}</div>;
}
export function OverviewDocScreen(): React.ReactElement {
const { p, rows, list, header } = useIntegrationDoc("overview");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("roles")}>
<DocTable compact headers={header("component")} rows={rows("matrix")} />
</DocSection>
<DocSection title={p("flow")}>
<DocOrderedList items={list("flowItems")} />
</DocSection>
<DocSection title={p("e2eSequence")}>
<DocTable compact headers={header("sequence")} rows={rows("e2eRows")} />
</DocSection>
<DocSection title={p("conventions")}>
<DocTable compact headers={header("convention")} rows={rows("conventionRows")} />
</DocSection>
<DocSection title={p("readingOrder")}>
<DocList items={list("readingItems")} />
</DocSection>
</DocPage>
);
}
export function QuickstartDocScreen(): React.ReactElement {
const { p, rows, list, header } = useIntegrationDoc("quickstart");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("prereq")}>
<DocList items={list("prereqItems")} />
</DocSection>
<DocSection title={p("steps")}>
<DocOrderedList items={list("stepItems")} />
</DocSection>
<DocSection title={p("testAccounts")}>
<DocTable compact headers={header("account")} rows={rows("accountRows")} />
</DocSection>
<DocSection title={p("reference")}>
<DocList items={list("referenceItems")} />
</DocSection>
<DocSection title={p("acceptance")}>
<DocOrderedList items={list("acceptanceItems")} />
<DocCode language="bash">{ACCEPTANCE_PLAYER_ME}</DocCode>
<DocCode language="bash">{ACCEPTANCE_WALLET_DEBIT}</DocCode>
</DocSection>
<DocNote>{p("note")}</DocNote>
</DocPage>
);
}
export function FundamentalsDocScreen(): React.ReactElement {
const { p, rows, header } = useIntegrationDoc("fundamentals");
return (
<DocPage>
<DocPageHeader title={p("title")} />
<DocSection title={p("balances")}>
<DocTable compact headers={header("balance")} rows={rows("balanceRows")} />
</DocSection>
<DocSection title={p("calls")}>
<DocTable compact headers={header("call")} rows={rows("callRows")} />
</DocSection>
<DocNote>{p("note")}</DocNote>
</DocPage>
);
}
export function SetupDocScreen(): React.ReactElement {
const { p, rows, list, header } = useIntegrationDoc("setup");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("weProvide")}>
<DocTable compact headers={header("param")} rows={rows("receiveRows")} />
</DocSection>
<DocSection title={p("youProvide")}>
<DocTable compact headers={header("param")} rows={rows("provideRows")} />
</DocSection>
<DocSection title={p("defaultPaths")}>
<DocTable compact headers={header("methodPath")} rows={rows("pathRows")} />
</DocSection>
<DocSection title={p("envMapping")}>
<DocTable compact headers={header("envMap")} rows={rows("envMappingRows")} />
</DocSection>
<DocSection title={p("adminSop")}>
<DocOrderedList items={list("adminSopSteps")} />
<DocTable compact headers={header("adminField")} rows={rows("adminFieldRows")} />
</DocSection>
<DocSection title={p("network")}>
<DocList items={list("networkItems")} />
</DocSection>
<DocNote>{p("note")}</DocNote>
</DocPage>
);
}
export function SsoDocScreen(): React.ReactElement {
const { p, rows, header } = useIntegrationDoc("sso");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("claims")}>
<DocTable compact headers={header("claim")} rows={rows("claimRows")} />
<DocCode>{SSO_JWT_PAYLOAD_EXAMPLE}</DocCode>
</DocSection>
<DocSection title={p("sign")}>
<DocCode language="typescript">{SSO_JWT_SIGN_EXAMPLE}</DocCode>
</DocSection>
<DocSection title={p("entryA")}>
<DocCode>{SSO_ENTRY_URL}</DocCode>
</DocSection>
<DocSection title={p("entryB")}>
<DocTable compact headers={header("message")} rows={rows("messageRows")} />
<DocCode language="typescript">{SSO_POSTMESSAGE}</DocCode>
<DocNote>{p("iframeNote")}</DocNote>
</DocSection>
<DocNote>{p("noExchangeNote")}</DocNote>
<DocSection title={p("entryApi")}>
<DocEndpoint method="GET" path="/api/v1/player/me" />
<DocNote>{p("entryApiNote")}</DocNote>
<DocCode language="http">{PLAYER_ME_REQUEST}</DocCode>
<DocCode>{PLAYER_ME_SUCCESS}</DocCode>
</DocSection>
<DocSection title={p("publicApis")}>
<DocTable compact headers={header("methodPath")} rows={rows("publicApiRows")} />
</DocSection>
<DocNote>{p("h5ScopeNote")}</DocNote>
<DocSection title={p("partnerApis")}>
<DocTable compact headers={header("methodPath")} rows={rows("partnerApiRows")} />
<DocNote>{p("refreshNote")}</DocNote>
</DocSection>
<DocSection title={p("authResponse")}>
<DocCode language="http">{PLAYER_AUTH_ERROR}</DocCode>
</DocSection>
<DocSection title={p("errors")}>
<DocTable compact headers={header("code")} rows={rows("errorRows")} />
</DocSection>
</DocPage>
);
}
export function IframeDocScreen(): React.ReactElement {
const { p, rows, list, header } = useIntegrationDoc("iframe");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("sequence")}>
<DocOrderedList items={list("sequenceSteps")} />
</DocSection>
<DocSection title={p("envelope")}>
<DocNote>{p("envelopeNote")}</DocNote>
</DocSection>
<DocSection title={p("childMessages")}>
<DocTable compact headers={header("message")} rows={rows("childMessageRows")} />
<DocCode>{IFRAME_CHILD_READY}</DocCode>
</DocSection>
<DocSection title={p("parentMessages")}>
<DocTable compact headers={header("message")} rows={rows("parentMessageRows")} />
<DocCode>{IFRAME_PARENT_INIT}</DocCode>
</DocSection>
<DocSection title={p("targetOrigin")}>
<DocNote>{p("targetOriginNote")}</DocNote>
</DocSection>
<DocNote>{p("timingNote")}</DocNote>
</DocPage>
);
}
export function WalletDocScreen(): React.ReactElement {
const { p, rows, header } = useIntegrationDoc("wallet");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("balance")}>
<DocEndpoint method="GET" path="/wallet/balance" />
<DocTable compact headers={header("query")} rows={rows("queryRows")} />
<DocCode>{WALLET_BALANCE_RESPONSE}</DocCode>
</DocSection>
<DocSection title={p("debit")}>
<DocEndpoint method="POST" path="/wallet/debit-for-lottery" />
<DocTable compact headers={header("field")} rows={rows("fieldRows")} />
<DocCode language="http">{WALLET_DEBIT_REQUEST}</DocCode>
</DocSection>
<DocSection title={p("credit")}>
<DocEndpoint method="POST" path="/wallet/credit-from-lottery" />
<DocCode language="http">{WALLET_CREDIT_REQUEST}</DocCode>
<DocNote>{p("creditNote")}</DocNote>
</DocSection>
<DocSection title={p("httpContract")}>
<DocTable compact headers={header("contract")} rows={rows("httpContractRows")} />
</DocSection>
<DocSection title={p("response")}>
<div className="grid gap-3 lg:grid-cols-2">
<DocCode>{WALLET_SUCCESS}</DocCode>
<DocCode>{WALLET_FAIL}</DocCode>
</div>
</DocSection>
<DocSection title={p("httpErrors")}>
<DocTable compact headers={header("http")} rows={rows("httpErrorRows")} />
</DocSection>
<DocNote>{p("idempotentNote")}</DocNote>
</DocPage>
);
}
export function TransferDocScreen(): React.ReactElement {
const { p, rows, header } = useIntegrationDoc("transfer");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocNote>{p("outOfScopeNote")}</DocNote>
<DocSection title={p("requestFields")}>
<DocTable compact headers={header("field")} rows={rows("requestFieldRows")} />
</DocSection>
<DocSection title={p("transferIn")}>
<DocEndpoint method="POST" path="/api/v1/wallet/transfer-in" />
<DocNote>{p("inNote")}</DocNote>
<DocCode language="http">{TRANSFER_IN}</DocCode>
</DocSection>
<DocSection title={p("transferOut")}>
<DocEndpoint method="POST" path="/api/v1/wallet/transfer-out" />
<DocNote>{p("outNote")}</DocNote>
<DocCode language="http">{TRANSFER_OUT}</DocCode>
</DocSection>
<DocSection title={p("transferResponse")}>
<DocNote>{p("responseNote")}</DocNote>
<DocCode>{TRANSFER_SUCCESS}</DocCode>
</DocSection>
<DocSection title={p("errors")}>
<DocTable compact headers={header("code")} rows={rows("errorRows")} />
</DocSection>
</DocPage>
);
}
export function ErrorsDocScreen(): React.ReactElement {
const { p, rows, header } = useIntegrationDoc("errors");
return (
<DocPage>
<DocPageHeader title={p("title")} />
<DocSection title={p("sso")}>
<DocTable compact headers={header("code")} rows={rows("ssoRows")} />
</DocSection>
<DocSection title={p("lotteryWallet")}>
<DocTable compact headers={header("code")} rows={rows("lotteryRows")} />
</DocSection>
<DocSection title={p("gateway")}>
<DocTable compact headers={header("http")} rows={rows("gatewayRows")} />
</DocSection>
<DocNote>{p("idempotentNote")}</DocNote>
</DocPage>
);
}
export function GoLiveDocScreen(): React.ReactElement {
const { p, list } = useIntegrationDoc("golive");
return (
<DocPage>
<DocPageHeader title={p("title")} />
<DocSection title={p("checklist")}>
<DocList items={list("items")} />
</DocSection>
</DocPage>
);
}

View File

@@ -0,0 +1,27 @@
"use client";
import { useTranslation } from "react-i18next";
type DocPageKey =
| "overview"
| "quickstart"
| "fundamentals"
| "setup"
| "sso"
| "iframe"
| "wallet"
| "transfer"
| "errors"
| "golive";
export function useIntegrationDoc(page: DocPageKey) {
const { t } = useTranslation("integrationDocs");
return {
t,
p: (key: string) => t(`pages.${page}.${key}`),
rows: (key: string) => t(`pages.${page}.${key}`, { returnObjects: true }) as string[][],
list: (key: string) => t(`pages.${page}.${key}`, { returnObjects: true }) as string[],
header: (key: string) => t(`headers.${key}`, { returnObjects: true }) as string[],
};
}