diff --git a/backend/internal/payment/provider/alipay.go b/backend/internal/payment/provider/alipay.go index af8a90c6..4f87e5a7 100644 --- a/backend/internal/payment/provider/alipay.go +++ b/backend/internal/payment/provider/alipay.go @@ -26,6 +26,18 @@ const ( alipayRefundSuffix = "-refund" ) +var ( + alipayTradeWapPay = func(client *alipay.Client, param alipay.TradeWapPay) (*url.URL, error) { + return client.TradeWapPay(param) + } + alipayTradePagePay = func(client *alipay.Client, param alipay.TradePagePay) (*url.URL, error) { + return client.TradePagePay(param) + } + alipayTradePreCreate = func(ctx context.Context, client *alipay.Client, param alipay.TradePreCreate) (*alipay.TradePreCreateRsp, error) { + return client.TradePreCreate(ctx, param) + } +) + // Alipay implements payment.Provider and payment.CancelableProvider using the smartwalle/alipay SDK. type Alipay struct { instanceID string @@ -80,7 +92,7 @@ func (a *Alipay) SupportedTypes() []payment.PaymentType { } // CreatePayment creates an Alipay payment page URL. -func (a *Alipay) CreatePayment(_ context.Context, req payment.CreatePaymentRequest) (*payment.CreatePaymentResponse, error) { +func (a *Alipay) CreatePayment(ctx context.Context, req payment.CreatePaymentRequest) (*payment.CreatePaymentResponse, error) { client, err := a.getClient() if err != nil { return nil, err @@ -96,12 +108,12 @@ func (a *Alipay) CreatePayment(_ context.Context, req payment.CreatePaymentReque } if req.IsMobile { - return a.createTrade(client, req, notifyURL, returnURL, true) + return a.createTrade(ctx, client, req, notifyURL, returnURL, true) } - return a.createTrade(client, req, notifyURL, returnURL, false) + return a.createTrade(ctx, client, req, notifyURL, returnURL, false) } -func (a *Alipay) createTrade(client *alipay.Client, req payment.CreatePaymentRequest, notifyURL, returnURL string, isMobile bool) (*payment.CreatePaymentResponse, error) { +func (a *Alipay) createTrade(ctx context.Context, client *alipay.Client, req payment.CreatePaymentRequest, notifyURL, returnURL string, isMobile bool) (*payment.CreatePaymentResponse, error) { if isMobile { param := alipay.TradeWapPay{} param.OutTradeNo = req.OrderID @@ -111,7 +123,7 @@ func (a *Alipay) createTrade(client *alipay.Client, req payment.CreatePaymentReq param.NotifyURL = notifyURL param.ReturnURL = returnURL - payURL, err := client.TradeWapPay(param) + payURL, err := alipayTradeWapPay(client, param) if err != nil { return nil, fmt.Errorf("alipay TradeWapPay: %w", err) } @@ -121,22 +133,19 @@ func (a *Alipay) createTrade(client *alipay.Client, req payment.CreatePaymentReq }, nil } - param := alipay.TradePagePay{} + param := alipay.TradePreCreate{} param.OutTradeNo = req.OrderID param.TotalAmount = req.Amount param.Subject = req.Subject - param.ProductCode = alipayProductCodePagePay param.NotifyURL = notifyURL - param.ReturnURL = returnURL - payURL, err := client.TradePagePay(param) + resp, err := alipayTradePreCreate(ctx, client, param) if err != nil { - return nil, fmt.Errorf("alipay TradePagePay: %w", err) + return nil, fmt.Errorf("alipay TradePreCreate: %w", err) } return &payment.CreatePaymentResponse{ TradeNo: req.OrderID, - PayURL: payURL.String(), - QRCode: payURL.String(), + QRCode: strings.TrimSpace(resp.QRCode), }, nil } diff --git a/backend/internal/payment/provider/alipay_test.go b/backend/internal/payment/provider/alipay_test.go index 7b0ce0d8..6cc4246c 100644 --- a/backend/internal/payment/provider/alipay_test.go +++ b/backend/internal/payment/provider/alipay_test.go @@ -3,9 +3,14 @@ package provider import ( + "context" "errors" + "net/url" "strings" "testing" + + "github.com/Wei-Shaw/sub2api/internal/payment" + "github.com/smartwalle/alipay/v3" ) func TestIsTradeNotExist(t *testing.T) { @@ -130,3 +135,111 @@ func TestNewAlipay(t *testing.T) { }) } } + +func TestCreateTradeUsesPreCreateForDesktop(t *testing.T) { + origPreCreate := alipayTradePreCreate + origPagePay := alipayTradePagePay + origWapPay := alipayTradeWapPay + t.Cleanup(func() { + alipayTradePreCreate = origPreCreate + alipayTradePagePay = origPagePay + alipayTradeWapPay = origWapPay + }) + + preCreateCalls := 0 + pagePayCalls := 0 + wapPayCalls := 0 + alipayTradePreCreate = func(ctx context.Context, client *alipay.Client, param alipay.TradePreCreate) (*alipay.TradePreCreateRsp, error) { + preCreateCalls++ + if param.OutTradeNo != "sub2_100" { + t.Fatalf("out_trade_no = %q, want %q", param.OutTradeNo, "sub2_100") + } + if param.NotifyURL != "https://merchant.example.com/api/v1/payment/webhook/alipay" { + t.Fatalf("notify_url = %q", param.NotifyURL) + } + return &alipay.TradePreCreateRsp{ + OutTradeNo: "sub2_100", + QRCode: "https://qr.alipay.example.com/precreate-token", + }, nil + } + alipayTradePagePay = func(client *alipay.Client, param alipay.TradePagePay) (*url.URL, error) { + pagePayCalls++ + return url.Parse("https://openapi.alipay.com/gateway.do?page-pay") + } + alipayTradeWapPay = func(client *alipay.Client, param alipay.TradeWapPay) (*url.URL, error) { + wapPayCalls++ + return url.Parse("https://openapi.alipay.com/gateway.do?wap-pay") + } + + provider := &Alipay{} + resp, err := provider.createTrade(context.Background(), &alipay.Client{}, payment.CreatePaymentRequest{ + OrderID: "sub2_100", + Amount: "88.00", + Subject: "Balance recharge", + }, "https://merchant.example.com/api/v1/payment/webhook/alipay", "https://merchant.example.com/payment/result", false) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if preCreateCalls != 1 { + t.Fatalf("precreate calls = %d, want 1", preCreateCalls) + } + if pagePayCalls != 0 { + t.Fatalf("page pay calls = %d, want 0", pagePayCalls) + } + if wapPayCalls != 0 { + t.Fatalf("wap pay calls = %d, want 0", wapPayCalls) + } + if resp.QRCode != "https://qr.alipay.example.com/precreate-token" { + t.Fatalf("qr_code = %q", resp.QRCode) + } + if resp.PayURL != "" { + t.Fatalf("pay_url = %q, want empty", resp.PayURL) + } +} + +func TestCreateTradeUsesWapPayForMobile(t *testing.T) { + origPreCreate := alipayTradePreCreate + origWapPay := alipayTradeWapPay + t.Cleanup(func() { + alipayTradePreCreate = origPreCreate + alipayTradeWapPay = origWapPay + }) + + preCreateCalls := 0 + alipayTradePreCreate = func(ctx context.Context, client *alipay.Client, param alipay.TradePreCreate) (*alipay.TradePreCreateRsp, error) { + preCreateCalls++ + return &alipay.TradePreCreateRsp{}, nil + } + + wapPayCalls := 0 + alipayTradeWapPay = func(client *alipay.Client, param alipay.TradeWapPay) (*url.URL, error) { + wapPayCalls++ + if param.ReturnURL != "https://merchant.example.com/payment/result" { + t.Fatalf("return_url = %q", param.ReturnURL) + } + return url.Parse("https://openapi.alipay.com/gateway.do?wap-pay") + } + + provider := &Alipay{} + resp, err := provider.createTrade(context.Background(), &alipay.Client{}, payment.CreatePaymentRequest{ + OrderID: "sub2_101", + Amount: "18.00", + Subject: "Balance recharge", + IsMobile: true, + }, "https://merchant.example.com/api/v1/payment/webhook/alipay", "https://merchant.example.com/payment/result", true) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if preCreateCalls != 0 { + t.Fatalf("precreate calls = %d, want 0", preCreateCalls) + } + if wapPayCalls != 1 { + t.Fatalf("wap pay calls = %d, want 1", wapPayCalls) + } + if resp.PayURL == "" { + t.Fatal("expected pay_url for mobile wap pay") + } + if resp.QRCode != "" { + t.Fatalf("qr_code = %q, want empty", resp.QRCode) + } +} diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 7d058a74..e17ed616 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -5453,6 +5453,18 @@ export default { errors: { tooManyPending: 'Too many pending orders (max {max}). Please complete or cancel existing orders first.', cancelRateLimited: 'Too many cancellations. Please try again later.', + wechatH5NotAuthorized: 'This merchant has not enabled WeChat H5 payment. Open this page in WeChat to continue.', + wechatPaymentMpNotConfigured: 'This site has not completed WeChat MP/JSAPI payment setup, so in-app WeChat payment is unavailable right now.', + wechatJsapiUnavailable: 'WeChat payment could not be invoked in the current environment. Reopen this page inside WeChat and try again.', + wechatJsapiFailed: 'WeChat payment did not complete. Try invoking it again or switch to QR payment.', + wechatUnavailable: 'WeChat payment is temporarily unavailable. Please try again later.', + wechatOpenInWeChatHint: 'Open the current page inside WeChat, or switch to desktop WeChat QR payment.', + wechatScanOnDesktopHint: 'On desktop, use WeChat Scan to pay; on mobile, reopen the current page inside WeChat.', + wechatSwitchBrowserHint: 'Switch to desktop WeChat QR payment, or reopen this page in an external browser and retry.', + alipayDesktopUnavailable: 'The desktop Alipay flow could not generate a QR code.', + alipayDesktopQrHint: 'Desktop Alipay should render a QR code. Refresh and retry, or make sure the payment page was not blocked.', + alipayMobileUnavailable: 'This page could not hand off to Alipay.', + alipayMobileOpenHint: 'Allow the current page to open the Alipay app, or retry from the system browser.', PENDING_ORDERS: 'This provider has pending orders. Please wait for them to complete before making changes.', }, stripePay: 'Pay Now', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 6dd74334..d54c0aba 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -5641,6 +5641,18 @@ export default { errors: { tooManyPending: '待支付订单过多(最多 {max} 个),请先完成或取消现有订单', cancelRateLimited: '取消订单过于频繁,请稍后再试', + wechatH5NotAuthorized: '当前商户未开通微信 H5 支付,请在微信中打开当前页面继续支付。', + wechatPaymentMpNotConfigured: '当前站点未完成公众号/JSAPI 支付配置,暂时无法在微信内直接拉起支付。', + wechatJsapiUnavailable: '当前环境未能拉起微信支付,请确认正在微信内打开本页后重试。', + wechatJsapiFailed: '微信支付未完成,请重新拉起支付或改用扫码支付。', + wechatUnavailable: '当前微信支付暂不可用,请稍后重试。', + wechatOpenInWeChatHint: '请复制当前页面链接到微信内打开,或直接改用电脑端微信扫码支付。', + wechatScanOnDesktopHint: '电脑端请直接使用微信扫一扫完成支付;移动端请在微信内打开当前页面。', + wechatSwitchBrowserHint: '请改用电脑端微信扫码,或在外部浏览器重新打开本页后再试。', + alipayDesktopUnavailable: '当前支付宝桌面支付未成功生成二维码。', + alipayDesktopQrHint: '电脑端支付宝应展示扫码单,请刷新后重试,或确认浏览器未拦截当前支付页。', + alipayMobileUnavailable: '当前页面未成功跳转到支付宝。', + alipayMobileOpenHint: '请允许当前页面打开支付宝 App,或改用系统浏览器重新发起支付。', PENDING_ORDERS: '该服务商有未完成的订单,请等待订单完成后再操作', }, stripePay: '立即支付', diff --git a/frontend/src/views/user/PaymentResultView.vue b/frontend/src/views/user/PaymentResultView.vue index 9687d1c7..53bbb550 100644 --- a/frontend/src/views/user/PaymentResultView.vue +++ b/frontend/src/views/user/PaymentResultView.vue @@ -54,7 +54,7 @@
{{ t('payment.orders.paymentMethod') }} - {{ t('payment.methods.' + order.payment_type, order.payment_type) }} + {{ t(paymentMethodI18nKey(order.payment_type), normalizedOrderPaymentType(order.payment_type)) }}
{{ t('payment.orders.status') }} @@ -75,7 +75,7 @@
{{ t('payment.orders.paymentMethod') }} - {{ t('payment.methods.' + returnInfo.type, returnInfo.type) }} + {{ t(paymentMethodI18nKey(returnInfo.type), normalizedOrderPaymentType(returnInfo.type)) }}
@@ -98,6 +98,7 @@ import { PAYMENT_RECOVERY_STORAGE_KEY, readPaymentRecoverySnapshot } from '@/com import { usePaymentStore } from '@/stores/payment' import { paymentAPI } from '@/api/payment' import type { PaymentOrder } from '@/types/payment' +import { normalizePaymentMethodForDisplay, paymentMethodI18nKey } from './paymentUx' const { t } = useI18n() const route = useRoute() @@ -133,6 +134,10 @@ const isSuccess = computed(() => { return !!order.value && SUCCESS_STATUSES.has(order.value.status) }) +function normalizedOrderPaymentType(paymentType: string): string { + return normalizePaymentMethodForDisplay(paymentType) || paymentType +} + onMounted(async () => { const resumeToken = typeof route.query.resume_token === 'string' ? route.query.resume_token diff --git a/frontend/src/views/user/PaymentView.vue b/frontend/src/views/user/PaymentView.vue index bfb9dae2..019de16a 100644 --- a/frontend/src/views/user/PaymentView.vue +++ b/frontend/src/views/user/PaymentView.vue @@ -88,6 +88,7 @@

{{ errorMessage }}

+

{{ errorHintMessage }}

@@ -174,6 +175,7 @@

{{ errorMessage }}

+

{{ errorHintMessage }}

@@ -281,6 +283,7 @@ import SubscriptionPlanCard from '@/components/payment/SubscriptionPlanCard.vue' import PaymentStatusPanel from '@/components/payment/PaymentStatusPanel.vue' import Icon from '@/components/icons/Icon.vue' import type { PaymentMethodOption } from '@/components/payment/PaymentMethodSelector.vue' +import { describePaymentScenarioError } from './paymentUx' const { t } = useI18n() const route = useRoute() @@ -301,6 +304,7 @@ function getDaysRemaining(expiresAt: string): number { const loading = ref(true) const submitting = ref(false) const errorMessage = ref('') +const errorHintMessage = ref('') const activeTab = ref<'recharge' | 'subscription'>('recharge') const amount = ref(null) const selectedMethod = ref('') @@ -619,6 +623,7 @@ async function confirmSubscribe() { async function createOrder(orderAmount: number, orderType: OrderType, planId?: number, options: CreateOrderOptions = {}) { submitting.value = true errorMessage.value = '' + errorHintMessage.value = '' try { const requestType = normalizeVisibleMethod(options.paymentType || selectedMethod.value) || options.paymentType || selectedMethod.value const payload = buildCreateOrderPayload({ @@ -668,8 +673,7 @@ async function createOrder(orderAmount: number, orderType: OrderType, planId?: n } if (decision.kind === 'unhandled') { - errorMessage.value = t('payment.result.failed') - appStore.showError(errorMessage.value) + applyScenarioError({ reason: 'UNHANDLED_PAYMENT_SCENARIO' }, visibleMethod) return } @@ -691,7 +695,7 @@ async function createOrder(orderAmount: number, orderType: OrderType, planId?: n if (errMsg.includes('cancel')) { appStore.showInfo(t('payment.qr.cancelled')) } else if (errMsg && !errMsg.includes('ok')) { - appStore.showError(t('payment.result.failed')) + applyScenarioError({ reason: 'WECHAT_JSAPI_FAILED', message: errMsg }, visibleMethod) } return } @@ -707,10 +711,16 @@ async function createOrder(orderAmount: number, orderType: OrderType, planId?: n if (apiErr.reason === 'TOO_MANY_PENDING') { const metadata = apiErr.metadata as Record | undefined errorMessage.value = t('payment.errors.tooManyPending', { max: metadata?.max || '' }) + errorHintMessage.value = '' } else if (apiErr.reason === 'CANCEL_RATE_LIMITED') { errorMessage.value = t('payment.errors.cancelRateLimited') + errorHintMessage.value = '' } else { - errorMessage.value = extractApiErrorMessage(err, t('payment.result.failed')) + applyScenarioError(err, normalizeVisibleMethod(options.paymentType || selectedMethod.value) || selectedMethod.value) + if (!errorMessage.value) { + errorMessage.value = extractApiErrorMessage(err, t('payment.result.failed')) + errorHintMessage.value = '' + } } appStore.showError(errorMessage.value) } finally { @@ -718,6 +728,21 @@ async function createOrder(orderAmount: number, orderType: OrderType, planId?: n } } +function applyScenarioError(err: unknown, paymentMethod: string) { + const descriptor = describePaymentScenarioError(err, { + paymentMethod, + isMobile: isMobileDevice(), + isWechatBrowser: typeof window !== 'undefined' && /MicroMessenger/i.test(window.navigator.userAgent), + }) + if (!descriptor) { + errorMessage.value = '' + errorHintMessage.value = '' + return + } + errorMessage.value = t(descriptor.messageKey) + errorHintMessage.value = descriptor.hintKey ? t(descriptor.hintKey) : '' +} + async function resumeWechatPaymentFromQuery() { const openid = readRouteQueryValue(route.query.openid) if (readRouteQueryValue(route.query.wechat_resume) !== '1' || !openid) { diff --git a/frontend/src/views/user/__tests__/PaymentResultView.spec.ts b/frontend/src/views/user/__tests__/PaymentResultView.spec.ts index d23a60d9..b1caa526 100644 --- a/frontend/src/views/user/__tests__/PaymentResultView.spec.ts +++ b/frontend/src/views/user/__tests__/PaymentResultView.spec.ts @@ -155,4 +155,29 @@ describe('PaymentResultView', () => { expect(wrapper.text()).toContain('payment.result.success') expect(verifyOrderPublic).not.toHaveBeenCalled() }) + + it('normalizes aliased payment methods before rendering the label', async () => { + routeState.query = { + resume_token: 'resume-88', + } + resolveOrderPublicByResumeToken.mockResolvedValueOnce({ + data: { + ...orderFactory('PAID'), + payment_type: 'alipay_direct', + }, + }) + + const wrapper = mount(PaymentResultView, { + global: { + stubs: { + OrderStatusBadge: true, + }, + }, + }) + + await flushPromises() + + expect(wrapper.text()).toContain('payment.methods.alipay') + expect(wrapper.text()).not.toContain('payment.methods.alipay_direct') + }) }) diff --git a/frontend/src/views/user/__tests__/paymentUx.spec.ts b/frontend/src/views/user/__tests__/paymentUx.spec.ts new file mode 100644 index 00000000..6cf105f2 --- /dev/null +++ b/frontend/src/views/user/__tests__/paymentUx.spec.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'vitest' +import { + describePaymentScenarioError, + normalizePaymentMethodForDisplay, +} from '../paymentUx' + +describe('normalizePaymentMethodForDisplay', () => { + it('collapses visible payment aliases to canonical method ids', () => { + expect(normalizePaymentMethodForDisplay(' alipay_direct ')).toBe('alipay') + expect(normalizePaymentMethodForDisplay('wxpay_direct')).toBe('wxpay') + expect(normalizePaymentMethodForDisplay('wechat_pay')).toBe('wxpay') + }) + + it('leaves non-aliased methods untouched', () => { + expect(normalizePaymentMethodForDisplay('stripe')).toBe('stripe') + }) +}) + +describe('describePaymentScenarioError', () => { + it('maps WeChat H5 authorization errors to explicit in-app guidance', () => { + expect(describePaymentScenarioError( + { reason: 'WECHAT_H5_NOT_AUTHORIZED' }, + { paymentMethod: 'wxpay', isMobile: true, isWechatBrowser: false }, + )).toEqual({ + messageKey: 'payment.errors.wechatH5NotAuthorized', + hintKey: 'payment.errors.wechatOpenInWeChatHint', + }) + }) + + it('maps missing WeixinJSBridge to a JSAPI-specific prompt', () => { + expect(describePaymentScenarioError( + new Error('WeixinJSBridge is unavailable'), + { paymentMethod: 'wxpay', isMobile: true, isWechatBrowser: true }, + )).toEqual({ + messageKey: 'payment.errors.wechatJsapiUnavailable', + hintKey: 'payment.errors.wechatOpenInWeChatHint', + }) + }) + + it('maps generic desktop Alipay failures to QR guidance', () => { + expect(describePaymentScenarioError( + { reason: 'PAYMENT_GATEWAY_ERROR' }, + { paymentMethod: 'alipay', isMobile: false, isWechatBrowser: false }, + )).toEqual({ + messageKey: 'payment.errors.alipayDesktopUnavailable', + hintKey: 'payment.errors.alipayDesktopQrHint', + }) + }) +}) diff --git a/frontend/src/views/user/paymentUx.ts b/frontend/src/views/user/paymentUx.ts new file mode 100644 index 00000000..443529a7 --- /dev/null +++ b/frontend/src/views/user/paymentUx.ts @@ -0,0 +1,105 @@ +import { normalizeVisibleMethod } from '@/components/payment/paymentFlow' +import { extractApiErrorCode } from '@/utils/apiError' + +const DISPLAY_METHOD_ALIASES: Record = { + wechat: 'wxpay', + wechat_pay: 'wxpay', +} + +export interface PaymentScenarioContext { + paymentMethod: string + isMobile: boolean + isWechatBrowser: boolean +} + +export interface PaymentScenarioErrorDescriptor { + messageKey: string + hintKey?: string +} + +export function normalizePaymentMethodForDisplay(paymentType: string): string { + const trimmed = paymentType.trim().toLowerCase() + const visibleMethod = normalizeVisibleMethod(trimmed) + if (visibleMethod) return visibleMethod + return DISPLAY_METHOD_ALIASES[trimmed] ?? trimmed +} + +export function paymentMethodI18nKey(paymentType: string): string { + return `payment.methods.${normalizePaymentMethodForDisplay(paymentType)}` +} + +function defaultWechatHint(context: PaymentScenarioContext): string { + if (!context.isMobile) return 'payment.errors.wechatScanOnDesktopHint' + return 'payment.errors.wechatOpenInWeChatHint' +} + +function defaultAlipayHint(context: PaymentScenarioContext): string { + if (context.isMobile) return 'payment.errors.alipayMobileOpenHint' + return 'payment.errors.alipayDesktopQrHint' +} + +export function describePaymentScenarioError( + error: unknown, + context: PaymentScenarioContext, +): PaymentScenarioErrorDescriptor | null { + const method = normalizePaymentMethodForDisplay(context.paymentMethod) + const code = extractApiErrorCode(error) + const message = error instanceof Error + ? error.message + : (typeof error === 'object' && error && 'message' in error && typeof error.message === 'string' + ? error.message + : String(error || '')) + const normalizedMessage = message.toLowerCase() + + if (method === 'wxpay') { + if (code === 'WECHAT_H5_NOT_AUTHORIZED') { + return { + messageKey: 'payment.errors.wechatH5NotAuthorized', + hintKey: defaultWechatHint(context), + } + } + if (code === 'WECHAT_PAYMENT_MP_NOT_CONFIGURED') { + return { + messageKey: 'payment.errors.wechatPaymentMpNotConfigured', + hintKey: context.isWechatBrowser + ? 'payment.errors.wechatSwitchBrowserHint' + : defaultWechatHint(context), + } + } + if (code === 'NO_AVAILABLE_INSTANCE') { + return { + messageKey: 'payment.errors.wechatUnavailable', + hintKey: defaultWechatHint(context), + } + } + if (code === 'WECHAT_JSAPI_FAILED' || normalizedMessage.includes('get_brand_wcpay_request:fail')) { + return { + messageKey: 'payment.errors.wechatJsapiFailed', + hintKey: defaultWechatHint(context), + } + } + if (normalizedMessage.includes('weixinjsbridge is unavailable')) { + return { + messageKey: 'payment.errors.wechatJsapiUnavailable', + hintKey: 'payment.errors.wechatOpenInWeChatHint', + } + } + if (code === 'PAYMENT_GATEWAY_ERROR' || code === 'UNHANDLED_PAYMENT_SCENARIO') { + return { + messageKey: 'payment.errors.wechatUnavailable', + hintKey: defaultWechatHint(context), + } + } + } + + if (method === 'alipay' && (code === 'PAYMENT_GATEWAY_ERROR' || code === 'UNHANDLED_PAYMENT_SCENARIO')) { + return { + messageKey: context.isMobile + ? 'payment.errors.alipayMobileUnavailable' + : 'payment.errors.alipayDesktopUnavailable', + hintKey: defaultAlipayHint(context), + } + } + + return null +}