Pairs with the backend structured payment errors (reason + metadata). The
frontend now maps reason codes to localized messages with metadata as
interpolation variables, and automatically localizes raw config-field names
(e.g. "certSerial" → "证书序列号") using the existing UI-label i18n
namespace.
- frontend/src/utils/apiError.ts
- extractApiErrorCode now prefers the string `reason` over the numeric HTTP
`code`; reason is granular enough to drive i18n lookup, HTTP code is not.
- New extractApiErrorMetadata to pull interpolation params off the error.
- New extractI18nErrorMessage(err, t, namespace, fallback): looks up
`<namespace>.<REASON>` in i18n and substitutes metadata. Before
substitution, `metadata.key` and `metadata.keys` (slash-joined) are
re-translated through `admin.settings.payment.field_<key>` so users see
"缺少必填项:证书序列号" instead of "缺少必填项:certSerial".
- frontend/src/i18n/locales/{zh,en}.ts
- Add payment.errors entries for every structured reason code returned by
the backend (PAYMENT_DISABLED, INVALID_AMOUNT, TOO_MANY_PENDING,
DAILY_LIMIT_EXCEEDED, NO_AVAILABLE_INSTANCE, PAYMENT_PROVIDER_MISCONFIGURED,
WXPAY_CONFIG_MISSING_KEY / INVALID_KEY_LENGTH / INVALID_KEY, NOT_FOUND,
FORBIDDEN, CONFLICT, INVALID_ORDER_TYPE, INVALID_STATUS,
BALANCE_NOT_ENOUGH, REFUND_AMOUNT_EXCEEDED, REFUND_FAILED, and more),
with placeholders for template variables.
- 13 payment-related Vue files
- Migrate catch-block error reporting from extractApiErrorMessage to
extractI18nErrorMessage(err, t, 'payment.errors', fallback).
- Remove the ad-hoc paymentErrorMap computed in SettingsView.vue, which the
new helper supersedes (it reads i18n directly via t).
- frontend/src/components/payment/providerConfig.ts
- wxpay: publicKey and publicKeyId are now required (was optional), matching
the pubkey-only verifier direction; certSerial is already required.
This PR is drop-in safe: reason-preferring extractApiErrorCode is backward
compatible with callers that pass their own i18nMap, and error codes missing
from i18n fall back to the existing message-based path.
Motivation: platform-certificate mode is being phased out by WeChat (2024-10+,
newly-provisioned merchants already cannot download platform certificates at
all), and wxpay config errors currently surface only when an order is being
created — admins have no feedback at save time. Also, errors were returned as
natural-language strings, leaving the frontend no way to localize them.
Changes:
- backend/internal/payment/provider/wxpay.go
- Replace fmt.Errorf with structured infraerrors.BadRequest errors:
- WXPAY_CONFIG_MISSING_KEY (metadata: key)
- WXPAY_CONFIG_INVALID_KEY_LENGTH (metadata: key, expected, actual)
- WXPAY_CONFIG_INVALID_KEY (metadata: key) for malformed PEMs
- Parse privateKey and publicKey PEMs in NewWxpay so malformed keys fail
at save time instead of at order creation.
- Keep the pubkey verifier (NewSHA256WithRSAPubkeyVerifier) as the single
supported verifier; no more loadKeyPair helper.
- backend/internal/service/payment_order.go invokeProvider
- If CreateProvider or CreatePayment returns a structured ApplicationError,
pass it through (optionally enriching metadata with provider/instance_id)
instead of wrapping it as generic PAYMENT_GATEWAY_ERROR — so clients see
the actual reason code (e.g. WXPAY_CONFIG_MISSING_KEY) and can localize.
- Simplify a few messages (TOO_MANY_PENDING, DAILY_LIMIT_EXCEEDED,
PAYMENT_GATEWAY_ERROR, NO_AVAILABLE_INSTANCE) to keyword form with
metadata for template variables.
- backend/internal/service/payment_config_providers.go
- New helper validateProviderConfig calls provider.CreateProvider at save
time. Enabled instances are validated on both Create and Update so admins
see config errors immediately in the dialog, not later at order creation.
- Disabled instances are not validated (half-filled drafts are allowed).
- backend/internal/payment/provider/wxpay_test.go
- Add generateTestKeyPair helper that produces valid RSA-2048 PKCS8/PKIX
PEMs per test, used by the valid-config baseline (prior fake strings no
longer pass the eager PEM check).
- Cover each structured-error branch (missing/invalid-length/malformed PEM).
Port A-group deltas from design zip (excluding bonus/pricing which
are explicitly out of scope):
- Narrative: N (not 5) 个订阅; add '// 5 分钟开始用' n-kicker;
SVG hexagon logo (was emoji); n-bottom live status bar
- Add 3-step onboarding panel (创建账户 → 绑定订阅 → 生成 key)
in narrative, active-step highlighted
- Add password strength meter (4 bars + text label 弱/中/强/极强)
- Add confirm-password field with live // matched/mismatch hint
- Add Terms & Privacy consent checkbox (submit gated)
- New i18n keys: confirmPasswordLabel/Placeholder, passwordsDoNotMatch
All existing Vue logic preserved (OAuth/Turnstile/verify code/
invitation+promo codes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three regressions from Task 7-9 caused /login /register to render broken:
- bg-glow not rendering: puro.css scopes .bg-glow to .puro-page,
AuthLayout isn't inside one. Fix: duplicate bg-glow rules into
AuthLayout scoped CSS keyed on .auth-shell-split.
- .auth-main had no background: right side showed naked body bg.
Fix: .auth-shell-split now sets var(--bg-0) for whole shell.
- Heading/label colors used text-gray-900 light-mode classes,
invisible on dark bg. Fix: switch to explicit text-slate-50/400,
and :deep() override for form inputs via AuthLayout split scope.
Legacy (non-split) mode unaffected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three regressions from Task 7-9 caused /login /register to render broken:
- bg-glow not rendering: puro.css scopes .bg-glow to .puro-page,
AuthLayout isn't inside one. Fix: duplicate bg-glow rules into
AuthLayout scoped CSS keyed on .auth-shell-split.
- .auth-main had no background: right side showed naked body bg.
Fix: .auth-shell-split now sets var(--bg-0) for whole shell.
- Heading/label colors used text-gray-900 light-mode classes,
invisible on dark bg. Fix: switch to explicit text-slate-50/400,
and :deep() override for form inputs via AuthLayout split scope.
Legacy (non-split) mode unaffected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Login and Register heading strings moved from hardcoded Chinese to
auth.puroLoginTitle / puroLoginSub / puroRegisterTitle / puroRegisterSub.
Landing (LandingView) and Docs (DocsView) intentionally keep hardcoded
Chinese this cycle (see spec §6 note 5 — English version deferred).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Route /docs (no auth). Six sections: get key, codex CLI, claude code,
curl, supported models, feedback. Uses puro.css design system.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same split layout as LoginView: left narrative, right form.
Heading: '创建账户' + '5 分钟开始用 PURO AI'.
All form logic preserved (OAuth, Turnstile, email verify code, password fields).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Left: ⬢ PURO AI brand + '5→1' headline + three-line value props
- Right: existing form (OAuth, Turnstile, 2FA all preserved unchanged)
- Heading changed from t('auth.welcomeBack') to '登录' — i18n key consolidation in Task 11
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New slot 'narrative' enables split-screen layout (50/50 desktop, collapses
to single column on mobile <900px).
Backward compatibility:
- Pages that don't pass a narrative slot still render the original
centered-card layout with siteName + logo + copyright
- ForgotPassword, ResetPassword, EmailVerify unaffected
To be used in Tasks 8 and 9 (LoginView, RegisterView).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>