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 ? ( +