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:
154
src/modules/docs/integration/integration-doc-data.ts
Normal file
154
src/modules/docs/integration/integration-doc-data.ts
Normal 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"
|
||||
}
|
||||
}`;
|
||||
314
src/modules/docs/integration/integration-doc-screens.tsx
Normal file
314
src/modules/docs/integration/integration-doc-screens.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
27
src/modules/docs/integration/use-integration-doc.ts
Normal file
27
src/modules/docs/integration/use-integration-doc.ts
Normal 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[],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user