From 76a890dae4a1fc28cea8908db361c372cfb0859f Mon Sep 17 00:00:00 2001 From: worable Date: Sun, 25 Oct 2026 13:03:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(wechatpay):=20=E5=BE=AE=E4=BF=A1=E6=94=AF?= =?UTF-8?q?=E4=BB=98=20v3=20Native=20=E7=9B=B4=E8=BF=9E=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=EF=BC=88=E5=AF=B9=E6=A0=87=20#12.2=20=E5=BE=AE=E4=BF=A1?= =?UTF-8?q?=E4=BE=A7=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补上 12.2 的微信半边。与支付宝那半边的结构差异决定了这不是「照着抄」: - 支付宝把 `sign` 放在表单里;微信是 `Authorization: WECHATPAY2-SHA256-RSA2048 ...` 头; - 支付宝的应答是表单文本;微信是 JSON + **响应头签名**,要用原始报文验; - 支付宝回调用自己的签名串;微信回调是 **AES-256-GCM 密文**,要用 APIv3 密钥解。 ## 三个必然踩到的坑 1. 请求签名串 `METHOD\nURL\nTIMESTAMP\nNONCE\nBODY\n` —— 末尾那个换行属于签名内容。 少一个就是 401 `SIGN_ERROR`,而错误码不会告诉你是哪一段错了。 2. 应答/回调签名串只有三段 `TIMESTAMP\nNONCE\nBODY\n`,与请求的不同;且必须用**原始字节**。 所以回调用 `kind: 'raw'` 路由拿未解析流 —— 内核 JSON 解析会重新序列化,字段顺序/ 空格/转义一变,签名就永远验不过(这不是偶发,是必然)。 3. `ciphertext` 是 base64 的 `密文 || 16 字节认证标签`,不是纯密文;整段喂给 decipher 会在 `final()` 抛「unable to authenticate data」。 ## 设计取舍 - **应答无签名即拒绝**:`code_url` 决定用户把钱扫给谁,伪造的应答等于劫持订单。 - **回调时间戳 5 分钟容差**:微信会重推,但重推必须带新签名;旧报文原样重放不能通过。 - **非 SUCCESS 状态也回 `{"code":"SUCCESS"}`**:`CLOSED`/`PROCESSING` 不该结算, 但回 FAIL 会让微信一直重推。 - **验签/解密失败回 401 + `FAIL`**:这类报文不可信,必须让它重试。 - **AppID 用常量时间比较**。 - **密钥留空 = 保持不变**:后台不回显密钥材料,改个商户号不该逼管理员重贴三份密钥。 - **API 基址只允许 https 域名**(可配置是为了测试,那就必须按 SSRF 入口来防)。 ## 验证 `pnpm test` **37 例**:协议 24(签名串形状、Authorization 头、裸 base64 私钥、应答验签、 篡改/错密钥/过期时间戳拒绝、AES-GCM 往返与篡改拒绝、错误密钥拒绝、金额单位是分、 description 截断 127、错误体解析、配置校验、SSRF 校验、常量时间 AppID 比较) + 接线 13(未配置不给支付方式、下单并验应答签名、**未签名应答被拒**、CNY 之外不出网、 原始报文回调结算、非 SUCCESS 不结算、篡改回 401 FAIL、AppID 不符拒绝、金额缺失拒绝、 密钥留空继承、坏配置不落盘)。 `pnpm typecheck` / `pnpm build` 通过;`frontend/` 配置页(四语言)构建出 `frontend/dist/index.js`;`install-into-kernel.mjs` 成功装入内核 `data/plugins`。 ## 未完成 - **未用真实商户号跑过真单**:需 mchid + 商户 API 证书序列号 + 私钥 + APIv3 密钥 + 平台公钥(或平台证书)+ 可公网回调的 https 域名。 - **平台证书自动轮换未做**:现在直接配平台公钥/证书内容;微信平台证书会轮换, 生产上还需要定时下载 `/v3/certificates`(用 APIv3 密钥解密)并替换。 --- .../plugins/wechatpay/frontend/manifest.json | 160 +++++++ .../wechatpay/frontend/messages/en-US.json | 29 ++ .../wechatpay/frontend/messages/zh-CN.json | 29 ++ .../wechatpay/frontend/messages/zh-HK.json | 29 ++ .../wechatpay/frontend/messages/zh-TW.json | 29 ++ .../plugins/wechatpay/frontend/package.json | 14 + .../plugins/wechatpay/frontend/src/admin.tsx | 241 ++++++++++ .../plugins/wechatpay/frontend/src/i18n.ts | 20 + .../plugins/wechatpay/frontend/src/index.tsx | 20 + .../plugins/wechatpay/frontend/tsup.config.ts | 35 ++ packages/plugins/wechatpay/package.json | 35 ++ packages/plugins/wechatpay/src/index.test.ts | 333 +++++++++++++ packages/plugins/wechatpay/src/index.ts | 438 ++++++++++++++++++ .../wechatpay/src/wechatpay-client.test.ts | 327 +++++++++++++ .../plugins/wechatpay/src/wechatpay-client.ts | 397 ++++++++++++++++ .../plugins/wechatpay/tsconfig.build.json | 11 + packages/plugins/wechatpay/tsconfig.json | 10 + pnpm-lock.yaml | 28 ++ 18 files changed, 2185 insertions(+) create mode 100644 packages/plugins/wechatpay/frontend/manifest.json create mode 100644 packages/plugins/wechatpay/frontend/messages/en-US.json create mode 100644 packages/plugins/wechatpay/frontend/messages/zh-CN.json create mode 100644 packages/plugins/wechatpay/frontend/messages/zh-HK.json create mode 100644 packages/plugins/wechatpay/frontend/messages/zh-TW.json create mode 100644 packages/plugins/wechatpay/frontend/package.json create mode 100644 packages/plugins/wechatpay/frontend/src/admin.tsx create mode 100644 packages/plugins/wechatpay/frontend/src/i18n.ts create mode 100644 packages/plugins/wechatpay/frontend/src/index.tsx create mode 100644 packages/plugins/wechatpay/frontend/tsup.config.ts create mode 100644 packages/plugins/wechatpay/package.json create mode 100644 packages/plugins/wechatpay/src/index.test.ts create mode 100644 packages/plugins/wechatpay/src/index.ts create mode 100644 packages/plugins/wechatpay/src/wechatpay-client.test.ts create mode 100644 packages/plugins/wechatpay/src/wechatpay-client.ts create mode 100644 packages/plugins/wechatpay/tsconfig.build.json create mode 100644 packages/plugins/wechatpay/tsconfig.json diff --git a/packages/plugins/wechatpay/frontend/manifest.json b/packages/plugins/wechatpay/frontend/manifest.json new file mode 100644 index 0000000..4e3e88c --- /dev/null +++ b/packages/plugins/wechatpay/frontend/manifest.json @@ -0,0 +1,160 @@ +{ + "version": "0.1.0", + "entry": "frontend/dist/index.js", + "pages": [], + "layouts": [], + "finders": [], + "adminRoutes": [ + { + "path": "/settings", + "component": "admin/settings", + "permission": "wechatpay.admin", + "nav": { + "labelKey": "nav.settings", + "groupKey": "nav.group", + "order": 31 + }, + "data": [ + { + "fetch": { + "path": "/wechatpay/admin/config" + }, + "as": "status" + } + ] + } + ], + "adminActions": [], + "actions": [], + "accountRoutes": [], + "accountWidgets": [], + "files": [ + "frontend/dist/admin.js", + "frontend/dist/chunk-ZRC5Q7IN.js", + "frontend/dist/index.js" + ], + "locales": [ + "zh-CN", + "zh-TW", + "zh-HK", + "en-US" + ], + "messages": { + "zh-CN": { + "nav.group": "微信支付", + "nav.settings": "配置", + "page.title": "微信支付(Native)", + "page.noSecrets": "未配置平台加密密钥,无法保存商户 API 私钥。", + "page.callbackHint": "回调地址:{path}(必须在商户平台配置为可公网访问的 https 地址)。", + "summary.appId": "AppID", + "summary.mchId": "商户号", + "summary.serialNo": "证书序列号", + "summary.apiBase": "API 基址", + "field.appId": "AppID(公众号/小程序)", + "field.mchId": "商户号 mchid", + "field.serialNo": "商户 API 证书序列号", + "field.privateKey": "商户 API 私钥(apiclient_key.pem)", + "field.apiV3Key": "APIv3 密钥(32 位)", + "field.platformPublicKey": "微信支付平台公钥", + "field.apiBase": "API 基址", + "field.apiBaseHint": "默认 {default},一般不需要改。只接受 https 域名。", + "field.keyKeep": "已配置,留空则保持不变", + "action.save": "保存配置", + "action.saving": "保存中…", + "notice.saved": "微信支付配置已保存。", + "notice.saveFailed": "保存失败,请稍后重试。", + "notice.apiV3KeyRequired": "首次配置必须填写 32 位 APIv3 密钥。", + "error.unauthenticated": "登录状态已失效,请重新登录。", + "error.forbidden": "无权执行此操作。", + "error.withCode": "保存失败({code})。", + "error.withStatus": "保存失败(HTTP {status})。" + }, + "zh-TW": { + "nav.group": "微信支付", + "nav.settings": "配置", + "page.title": "微信支付(Native)", + "page.noSecrets": "未配置平臺加密金鑰,無法儲存商戶 API 私鑰。", + "page.callbackHint": "回撥地址:{path}(必須在商戶平臺配置為可公網訪問的 https 地址)。", + "summary.appId": "AppID", + "summary.mchId": "商戶號", + "summary.serialNo": "證書序列號", + "summary.apiBase": "API 基址", + "field.appId": "AppID(公眾號/小程式)", + "field.mchId": "商戶號 mchid", + "field.serialNo": "商戶 API 證書序列號", + "field.privateKey": "商戶 API 私鑰(apiclient_key.pem)", + "field.apiV3Key": "APIv3 金鑰(32 位)", + "field.platformPublicKey": "微信支付平臺公鑰", + "field.apiBase": "API 基址", + "field.apiBaseHint": "預設 {default},一般不需要改。只接受 https 域名。", + "field.keyKeep": "已配置,留空則保持不變", + "action.save": "儲存配置", + "action.saving": "儲存中…", + "notice.saved": "微信支付配置已儲存。", + "notice.saveFailed": "儲存失敗,請稍後重試。", + "notice.apiV3KeyRequired": "首次配置必須填寫 32 位 APIv3 金鑰。", + "error.unauthenticated": "登入狀態已失效,請重新登入。", + "error.forbidden": "無權執行此操作。", + "error.withCode": "儲存失敗({code})。", + "error.withStatus": "儲存失敗(HTTP {status})。" + }, + "zh-HK": { + "nav.group": "微信支付", + "nav.settings": "配置", + "page.title": "微信支付(Native)", + "page.noSecrets": "未配置平臺加密金鑰,無法儲存商戶 API 私鑰。", + "page.callbackHint": "回撥地址:{path}(必須在商戶平臺配置為可公網訪問的 https 地址)。", + "summary.appId": "AppID", + "summary.mchId": "商戶號", + "summary.serialNo": "證書序列號", + "summary.apiBase": "API 基址", + "field.appId": "AppID(公眾號/小程序)", + "field.mchId": "商戶號 mchid", + "field.serialNo": "商戶 API 證書序列號", + "field.privateKey": "商戶 API 私鑰(apiclient_key.pem)", + "field.apiV3Key": "APIv3 金鑰(32 位)", + "field.platformPublicKey": "微信支付平臺公鑰", + "field.apiBase": "API 基址", + "field.apiBaseHint": "默認 {default},一般不需要改。只接受 https 域名。", + "field.keyKeep": "已配置,留空則保持不變", + "action.save": "儲存配置", + "action.saving": "儲存中…", + "notice.saved": "微信支付配置已儲存。", + "notice.saveFailed": "儲存失敗,請稍後重試。", + "notice.apiV3KeyRequired": "首次配置必須填寫 32 位 APIv3 金鑰。", + "error.unauthenticated": "登錄狀態已失效,請重新登錄。", + "error.forbidden": "無權執行此操作。", + "error.withCode": "儲存失敗({code})。", + "error.withStatus": "儲存失敗(HTTP {status})。" + }, + "en-US": { + "nav.group": "WeChat Pay", + "nav.settings": "Settings", + "page.title": "WeChat Pay (Native)", + "page.noSecrets": "Platform encryption keys are not configured, so the merchant API private key cannot be stored.", + "page.callbackHint": "Notification URL: {path} (must be registered on the merchant platform as a publicly reachable https address).", + "summary.appId": "AppID", + "summary.mchId": "Merchant ID", + "summary.serialNo": "Certificate serial", + "summary.apiBase": "API base", + "field.appId": "AppID (official account / mini program)", + "field.mchId": "Merchant ID (mchid)", + "field.serialNo": "Merchant API certificate serial number", + "field.privateKey": "Merchant API private key (apiclient_key.pem)", + "field.apiV3Key": "APIv3 key (32 characters)", + "field.platformPublicKey": "WeChat Pay platform public key", + "field.apiBase": "API base", + "field.apiBaseHint": "Defaults to {default}; you normally do not change it. Only https hosts are accepted.", + "field.keyKeep": "Already configured — leave empty to keep it", + "action.save": "Save", + "action.saving": "Saving…", + "notice.saved": "WeChat Pay configuration saved.", + "notice.saveFailed": "Could not save. Please retry.", + "notice.apiV3KeyRequired": "The 32-character APIv3 key is required for the initial setup.", + "error.unauthenticated": "Your session expired. Please sign in again.", + "error.forbidden": "You are not allowed to do this.", + "error.withCode": "Save failed ({code}).", + "error.withStatus": "Save failed (HTTP {status})." + } + } +} diff --git a/packages/plugins/wechatpay/frontend/messages/en-US.json b/packages/plugins/wechatpay/frontend/messages/en-US.json new file mode 100644 index 0000000..4aa6795 --- /dev/null +++ b/packages/plugins/wechatpay/frontend/messages/en-US.json @@ -0,0 +1,29 @@ +{ + "nav.group": "WeChat Pay", + "nav.settings": "Settings", + "page.title": "WeChat Pay (Native)", + "page.noSecrets": "Platform encryption keys are not configured, so the merchant API private key cannot be stored.", + "page.callbackHint": "Notification URL: {path} (must be registered on the merchant platform as a publicly reachable https address).", + "summary.appId": "AppID", + "summary.mchId": "Merchant ID", + "summary.serialNo": "Certificate serial", + "summary.apiBase": "API base", + "field.appId": "AppID (official account / mini program)", + "field.mchId": "Merchant ID (mchid)", + "field.serialNo": "Merchant API certificate serial number", + "field.privateKey": "Merchant API private key (apiclient_key.pem)", + "field.apiV3Key": "APIv3 key (32 characters)", + "field.platformPublicKey": "WeChat Pay platform public key", + "field.apiBase": "API base", + "field.apiBaseHint": "Defaults to {default}; you normally do not change it. Only https hosts are accepted.", + "field.keyKeep": "Already configured — leave empty to keep it", + "action.save": "Save", + "action.saving": "Saving…", + "notice.saved": "WeChat Pay configuration saved.", + "notice.saveFailed": "Could not save. Please retry.", + "notice.apiV3KeyRequired": "The 32-character APIv3 key is required for the initial setup.", + "error.unauthenticated": "Your session expired. Please sign in again.", + "error.forbidden": "You are not allowed to do this.", + "error.withCode": "Save failed ({code}).", + "error.withStatus": "Save failed (HTTP {status})." +} diff --git a/packages/plugins/wechatpay/frontend/messages/zh-CN.json b/packages/plugins/wechatpay/frontend/messages/zh-CN.json new file mode 100644 index 0000000..c1a0478 --- /dev/null +++ b/packages/plugins/wechatpay/frontend/messages/zh-CN.json @@ -0,0 +1,29 @@ +{ + "nav.group": "微信支付", + "nav.settings": "配置", + "page.title": "微信支付(Native)", + "page.noSecrets": "未配置平台加密密钥,无法保存商户 API 私钥。", + "page.callbackHint": "回调地址:{path}(必须在商户平台配置为可公网访问的 https 地址)。", + "summary.appId": "AppID", + "summary.mchId": "商户号", + "summary.serialNo": "证书序列号", + "summary.apiBase": "API 基址", + "field.appId": "AppID(公众号/小程序)", + "field.mchId": "商户号 mchid", + "field.serialNo": "商户 API 证书序列号", + "field.privateKey": "商户 API 私钥(apiclient_key.pem)", + "field.apiV3Key": "APIv3 密钥(32 位)", + "field.platformPublicKey": "微信支付平台公钥", + "field.apiBase": "API 基址", + "field.apiBaseHint": "默认 {default},一般不需要改。只接受 https 域名。", + "field.keyKeep": "已配置,留空则保持不变", + "action.save": "保存配置", + "action.saving": "保存中…", + "notice.saved": "微信支付配置已保存。", + "notice.saveFailed": "保存失败,请稍后重试。", + "notice.apiV3KeyRequired": "首次配置必须填写 32 位 APIv3 密钥。", + "error.unauthenticated": "登录状态已失效,请重新登录。", + "error.forbidden": "无权执行此操作。", + "error.withCode": "保存失败({code})。", + "error.withStatus": "保存失败(HTTP {status})。" +} diff --git a/packages/plugins/wechatpay/frontend/messages/zh-HK.json b/packages/plugins/wechatpay/frontend/messages/zh-HK.json new file mode 100644 index 0000000..632cc07 --- /dev/null +++ b/packages/plugins/wechatpay/frontend/messages/zh-HK.json @@ -0,0 +1,29 @@ +{ + "nav.group": "微信支付", + "nav.settings": "配置", + "page.title": "微信支付(Native)", + "page.noSecrets": "未配置平臺加密金鑰,無法儲存商戶 API 私鑰。", + "page.callbackHint": "回撥地址:{path}(必須在商戶平臺配置為可公網訪問的 https 地址)。", + "summary.appId": "AppID", + "summary.mchId": "商戶號", + "summary.serialNo": "證書序列號", + "summary.apiBase": "API 基址", + "field.appId": "AppID(公眾號/小程序)", + "field.mchId": "商戶號 mchid", + "field.serialNo": "商戶 API 證書序列號", + "field.privateKey": "商戶 API 私鑰(apiclient_key.pem)", + "field.apiV3Key": "APIv3 金鑰(32 位)", + "field.platformPublicKey": "微信支付平臺公鑰", + "field.apiBase": "API 基址", + "field.apiBaseHint": "默認 {default},一般不需要改。只接受 https 域名。", + "field.keyKeep": "已配置,留空則保持不變", + "action.save": "儲存配置", + "action.saving": "儲存中…", + "notice.saved": "微信支付配置已儲存。", + "notice.saveFailed": "儲存失敗,請稍後重試。", + "notice.apiV3KeyRequired": "首次配置必須填寫 32 位 APIv3 金鑰。", + "error.unauthenticated": "登錄狀態已失效,請重新登錄。", + "error.forbidden": "無權執行此操作。", + "error.withCode": "儲存失敗({code})。", + "error.withStatus": "儲存失敗(HTTP {status})。" +} diff --git a/packages/plugins/wechatpay/frontend/messages/zh-TW.json b/packages/plugins/wechatpay/frontend/messages/zh-TW.json new file mode 100644 index 0000000..9d24178 --- /dev/null +++ b/packages/plugins/wechatpay/frontend/messages/zh-TW.json @@ -0,0 +1,29 @@ +{ + "nav.group": "微信支付", + "nav.settings": "配置", + "page.title": "微信支付(Native)", + "page.noSecrets": "未配置平臺加密金鑰,無法儲存商戶 API 私鑰。", + "page.callbackHint": "回撥地址:{path}(必須在商戶平臺配置為可公網訪問的 https 地址)。", + "summary.appId": "AppID", + "summary.mchId": "商戶號", + "summary.serialNo": "證書序列號", + "summary.apiBase": "API 基址", + "field.appId": "AppID(公眾號/小程式)", + "field.mchId": "商戶號 mchid", + "field.serialNo": "商戶 API 證書序列號", + "field.privateKey": "商戶 API 私鑰(apiclient_key.pem)", + "field.apiV3Key": "APIv3 金鑰(32 位)", + "field.platformPublicKey": "微信支付平臺公鑰", + "field.apiBase": "API 基址", + "field.apiBaseHint": "預設 {default},一般不需要改。只接受 https 域名。", + "field.keyKeep": "已配置,留空則保持不變", + "action.save": "儲存配置", + "action.saving": "儲存中…", + "notice.saved": "微信支付配置已儲存。", + "notice.saveFailed": "儲存失敗,請稍後重試。", + "notice.apiV3KeyRequired": "首次配置必須填寫 32 位 APIv3 金鑰。", + "error.unauthenticated": "登入狀態已失效,請重新登入。", + "error.forbidden": "無權執行此操作。", + "error.withCode": "儲存失敗({code})。", + "error.withStatus": "儲存失敗(HTTP {status})。" +} diff --git a/packages/plugins/wechatpay/frontend/package.json b/packages/plugins/wechatpay/frontend/package.json new file mode 100644 index 0000000..7abed59 --- /dev/null +++ b/packages/plugins/wechatpay/frontend/package.json @@ -0,0 +1,14 @@ +{ + "name": "@stackpanel/plugin-wechatpay-frontend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsup" + }, + "dependencies": { + "sonner": "^2.0.8", + "zod": "^4.4.3" + }, + "license": "MIT" +} diff --git a/packages/plugins/wechatpay/frontend/src/admin.tsx b/packages/plugins/wechatpay/frontend/src/admin.tsx new file mode 100644 index 0000000..c6c71b7 --- /dev/null +++ b/packages/plugins/wechatpay/frontend/src/admin.tsx @@ -0,0 +1,241 @@ +'use client'; + +import type { AdminPageComponentProps } from '@stackpanel/sdk'; +import { PageHeader } from '@stackpanel/ui'; +import { useRouter } from 'next/navigation'; +import { useState, useTransition, type ReactElement } from 'react'; +import { toast } from 'sonner'; +import { t } from './i18n.js'; + +interface WechatPayStatus { + configured: boolean; + secretsEnabled: boolean; + appId?: string; + mchId?: string; + serialNo?: string; + apiBase?: string; + methods?: Array<{ id: string; label: string }>; +} + +interface ProblemDetails { + code?: string; + title?: string; + detail?: string; +} + +const DEFAULT_API_BASE = 'https://api.mch.weixin.qq.com'; + +/** + * 微信支付 v3 配置表单。 + * + * 与支付宝页的两处差别: + * - 密钥材料多两样(证书序列号、APIv3 密钥)。APIv3 密钥**不预填也不回显**: + * 它同时是回调解密密钥,能读它就能伪造回调。 + * - 已配置后私钥/平台公钥留空表示「保持不变」,但 APIv3 密钥首次必填。 + */ +function WechatPayAdminPage(props: AdminPageComponentProps): ReactElement { + const tr = t(props.locale); + const status = props.data.status as WechatPayStatus | undefined; + const router = useRouter(); + const [pending, startTransition] = useTransition(); + const [appId, setAppId] = useState(status?.appId ?? ''); + const [mchId, setMchId] = useState(status?.mchId ?? ''); + const [serialNo, setSerialNo] = useState(status?.serialNo ?? ''); + const [apiBase, setApiBase] = useState(status?.apiBase ?? DEFAULT_API_BASE); + const [privateKey, setPrivateKey] = useState(''); + const [apiV3Key, setApiV3Key] = useState(''); + const [platformPublicKey, setPlatformPublicKey] = useState(''); + + function handleSubmit(event: React.FormEvent): void { + event.preventDefault(); + if (!status?.configured && apiV3Key.trim().length === 0) { + toast.error(tr('notice.apiV3KeyRequired')); + return; + } + const payload = { + appId: appId.trim(), + mchId: mchId.trim(), + serialNo: serialNo.trim(), + privateKey: privateKey.trim(), + apiV3Key: apiV3Key.trim(), + platformPublicKey: platformPublicKey.trim(), + apiBase: apiBase.trim() || DEFAULT_API_BASE, + }; + startTransition(async () => { + try { + const response = await fetch('/api/plugins/wechatpay/admin/config', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(payload), + }); + if (!response.ok) { + toast.error(await readErrorMessage(response, tr)); + return; + } + toast.success(tr('notice.saved')); + setPrivateKey(''); + setApiV3Key(''); + setPlatformPublicKey(''); + router.refresh(); + } catch { + toast.error(tr('notice.saveFailed')); + } + }); + } + + return ( +
+ + {status?.secretsEnabled === false ? ( +

+ {tr('page.noSecrets')} +

+ ) : null} +

+ {tr('page.callbackHint', { path: '/callbacks/wechatpay' })} +

+ + {status?.configured ? ( +
+
+
+
{tr('summary.mchId')}
+
{status.mchId}
+
+
+
{tr('summary.serialNo')}
+
{status.serialNo}
+
+
+
{tr('summary.appId')}
+
{status.appId}
+
+
+
{tr('summary.apiBase')}
+
{status.apiBase}
+
+
+
+ ) : null} + +
+ + + + + + + + + +
+ ); +} + +function Field(props: { + label: string; + name: string; + value: string; + onChange: (value: string) => void; + hint?: string; + placeholder?: string; + textarea?: boolean; + required?: boolean; +}): ReactElement { + const id = `wechatpay-${props.name}`; + const className = + 'w-full rounded-lg border bg-background px-3 py-2 text-sm outline-none focus:border-primary'; + return ( +
+ + {props.textarea ? ( +