- {{ sub.group?.name || `Group #${sub.group_id}` }}
+ {{ sub.group?.name || t('payment.groupFallback', { id: sub.group_id }) }}
{{ platformLabel(sub.group?.platform || '') }}
@@ -283,7 +275,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'
+import { buildPaymentErrorToastMessage, describePaymentScenarioError } from './paymentUx'
import { parseWechatResumeRoute, stripWechatResumeQuery } from './paymentWechatResume'
const { t } = useI18n()
@@ -374,7 +366,7 @@ function waitForWeixinJSBridge(timeoutMs = 4000): Promise): Promise> {
const bridge = await waitForWeixinJSBridge()
if (!bridge) {
- throw new Error('WeixinJSBridge is unavailable')
+ throw new Error('WECHAT_JSAPI_UNAVAILABLE')
}
return new Promise((resolve) => {
bridge.invoke('getBrandWCPayRequest', payload, (result) => resolve(result || {}))
@@ -714,19 +706,25 @@ async function createOrder(orderAmount: number, orderType: OrderType, planId?: n
errorMessage.value = t('payment.errors.cancelRateLimited')
errorHintMessage.value = ''
} else {
- applyScenarioError(err, normalizeVisibleMethod(options.paymentType || selectedMethod.value) || selectedMethod.value)
+ const handled = applyScenarioError(
+ err,
+ normalizeVisibleMethod(options.paymentType || selectedMethod.value) || selectedMethod.value,
+ )
if (!errorMessage.value) {
errorMessage.value = extractApiErrorMessage(err, t('payment.result.failed'))
errorHintMessage.value = ''
}
+ if (handled) {
+ return
+ }
}
- appStore.showError(errorMessage.value)
+ appStore.showError(buildPaymentErrorToastMessage(errorMessage.value, errorHintMessage.value))
} finally {
submitting.value = false
}
}
-function applyScenarioError(err: unknown, paymentMethod: string) {
+function applyScenarioError(err: unknown, paymentMethod: string): boolean {
const descriptor = describePaymentScenarioError(err, {
paymentMethod,
isMobile: isMobileDevice(),
@@ -735,10 +733,12 @@ function applyScenarioError(err: unknown, paymentMethod: string) {
if (!descriptor) {
errorMessage.value = ''
errorHintMessage.value = ''
- return
+ return false
}
errorMessage.value = t(descriptor.messageKey)
errorHintMessage.value = descriptor.hintKey ? t(descriptor.hintKey) : ''
+ appStore.showError(buildPaymentErrorToastMessage(errorMessage.value, errorHintMessage.value))
+ return true
}
async function resumeWechatPaymentFromQuery() {
diff --git a/frontend/src/views/user/__tests__/paymentUx.spec.ts b/frontend/src/views/user/__tests__/paymentUx.spec.ts
index 6cf105f2..c2a4ac59 100644
--- a/frontend/src/views/user/__tests__/paymentUx.spec.ts
+++ b/frontend/src/views/user/__tests__/paymentUx.spec.ts
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import {
+ buildPaymentErrorToastMessage,
describePaymentScenarioError,
normalizePaymentMethodForDisplay,
} from '../paymentUx'
@@ -37,6 +38,16 @@ describe('describePaymentScenarioError', () => {
})
})
+ it('maps the internal JSAPI unavailable marker to the same prompt', () => {
+ expect(describePaymentScenarioError(
+ new Error('WECHAT_JSAPI_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' },
@@ -47,3 +58,15 @@ describe('describePaymentScenarioError', () => {
})
})
})
+
+describe('buildPaymentErrorToastMessage', () => {
+ it('returns the main message when no hint is present', () => {
+ expect(buildPaymentErrorToastMessage('Payment failed')).toBe('Payment failed')
+ })
+
+ it('appends the hint to the toast body when present', () => {
+ expect(buildPaymentErrorToastMessage('Payment failed', 'Open WeChat to continue.')).toBe(
+ 'Payment failed Open WeChat to continue.'
+ )
+ })
+})
diff --git a/frontend/src/views/user/paymentUx.ts b/frontend/src/views/user/paymentUx.ts
index 443529a7..04fc7978 100644
--- a/frontend/src/views/user/paymentUx.ts
+++ b/frontend/src/views/user/paymentUx.ts
@@ -28,6 +28,11 @@ export function paymentMethodI18nKey(paymentType: string): string {
return `payment.methods.${normalizePaymentMethodForDisplay(paymentType)}`
}
+export function buildPaymentErrorToastMessage(message: string, hint?: string): string {
+ if (!hint) return message
+ return `${message} ${hint}`.trim()
+}
+
function defaultWechatHint(context: PaymentScenarioContext): string {
if (!context.isMobile) return 'payment.errors.wechatScanOnDesktopHint'
return 'payment.errors.wechatOpenInWeChatHint'
@@ -78,7 +83,10 @@ export function describePaymentScenarioError(
hintKey: defaultWechatHint(context),
}
}
- if (normalizedMessage.includes('weixinjsbridge is unavailable')) {
+ if (
+ normalizedMessage.includes('weixinjsbridge is unavailable') ||
+ normalizedMessage.includes('wechat_jsapi_unavailable')
+ ) {
return {
messageKey: 'payment.errors.wechatJsapiUnavailable',
hintKey: 'payment.errors.wechatOpenInWeChatHint',