- Add docs/PAYMENT.md and docs/PAYMENT_CN.md with full payment configuration guide
- Mark Sub2ApiPay as deprecated (strikethrough) in all 3 READMEs, link to built-in payment docs
- Add built-in payment system to features list
- Remove sora_client_enabled from PublicSettings type and store defaults
- Remove purchase_subscription form defaults and save payload from SettingsView
- Remove dead 'data' tab type from SettingsTab union
Upstream removed sora feature (090_drop_sora.sql) but left i18n keys
and wire.go references. Clean up:
- Remove entire sora i18n block from en.ts and zh.ts (~190 lines)
- Remove sora nav key and unused 'data' settings tab key
- Remove sora_client_enabled from settings (fork-specific)
- Remove SoraMediaCleanupService from wire.go
The API client's error interceptor was dropping the reason and metadata
fields from backend error responses. This caused PaymentView to miss
specific error codes (TOO_MANY_PENDING, CANCEL_RATE_LIMITED) and fall
back to generic error messages.
- Add POST /payment/orders/:id/sync endpoint that queries upstream
provider on each poll, complementing webhooks for timely payment
detection when webhooks are delayed or unreachable
- Fix Stripe payment countdown: pass expires_at to PaymentStatusPanel
instead of empty string (was falling back to hardcoded 30 minutes)
- Idempotent: toPaid uses atomic UPDATE WHERE status=PENDING, so
concurrent webhook + sync calls won't double-credit
Cherry-picked from PR branch (feat/payment-system-v2).
- EasyPay: parse payurl2 for H5 mobile links, prefer on mobile
- EasyPay: add device=mobile for popup mode on mobile
- Backend: expand isMobile() to detect iPad/iPod
- Frontend: auto-redirect on mobile instead of popup
- Frontend: fallback to redirect when popup blocked
- Stripe: use mobile_web client for WeChat Pay on mobile
- StripePopup: typed interface, extractApiErrorMessage
The db139191 audit incorrectly removed the second URL decode pass.
EasyPay providers send values that need decoding after url.ParseQuery,
causing signature verification to fail on .147+.
Production .146 worked because it still had decodeURLValue.
The PaymentResultView only checked COMPLETED and PAID status, but orders
in RECHARGING state (balance being applied after payment) were incorrectly
shown as failed.
- Replace gradient header in subscription confirm with clean card layout
matching sub2apipay design (platform accent text instead of full gradient)
- Add renewal plan selection modal: when group has multiple plans show
picker, single plan skips directly to payment method selection
- Restyle SubscriptionsView with platform-specific colors (badge, border,
button) instead of hardcoded purple
- Update platformColors to match sub2apipay style (transparent badges,
subtle borders with /20 opacity)
EasyPay callbacks arrive with double-encoded query values (e.g. %25E5 instead
of %E5) due to redirect chains. url.ParseQuery decodes once; decodeURLValue
applies a second safe decode so the sign matches what EasyPay computed.
Also removes temporary debug logging.
When multiple provider instances exist (e.g. 3 EasyPay accounts), the webhook
handler now extracts out_trade_no from the callback, looks up the order, and
uses the order's original provider instance for verification instead of picking
an arbitrary instance from the registry.
- Add btn-alipay/btn-wxpay CSS classes; confirm button color follows payment method
- Subscription confirm header uses platform gradient (Anthropic orange, Gemini blue, etc.)
- Subscription confirm page shows plan details (rate, limits, validity)
- SubscriptionPlanCard: show "Renew" button when user has active subscription
- SubscriptionsView: add renewal button on active subscription cards
- QR code display: brand-color border + center logo overlay (Alipay blue, WeChat green)
- StripePaymentView: WeChat QR green border + logo, Alipay spinner brand color
- Backend: fix subscription refund to deduct days (ExtendSubscription -days or Revoke)
- Backend: rollback subscription days on gateway failure
UpdateProvider API returns raw DB entity (encrypted config, string types),
but frontend needs ProviderInstanceResponse (decrypted, array types).
Reload full list after save to ensure correct data and sort order.