fix: share built-in payment method matching

This commit is contained in:
Albert Coady
2026-07-06 15:00:29 +08:00
parent 22ec77b570
commit 27cb485d55
8 changed files with 69 additions and 25 deletions
@@ -39,7 +39,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { METHOD_ORDER } from './providerConfig'
import { METHOD_ORDER, isBuiltInAlipayMethod, isBuiltInWxpayMethod } from './providerConfig'
import alipayIcon from '@/assets/icons/alipay.svg'
import wxpayIcon from '@/assets/icons/wxpay.svg'
import stripeIcon from '@/assets/icons/stripe.svg'
@@ -82,8 +82,8 @@ const sortedMethods = computed(() => {
})
function methodIcon(type: string): string {
if (isAlipayMethod(type)) return METHOD_ICONS.alipay
if (isWxpayMethod(type)) return METHOD_ICONS.wxpay
if (isBuiltInAlipayMethod(type)) return METHOD_ICONS.alipay
if (isBuiltInWxpayMethod(type)) return METHOD_ICONS.wxpay
if (type === 'airwallex') return METHOD_ICONS.airwallex
return METHOD_ICONS[type] || paymentIcon
}
@@ -93,18 +93,10 @@ function methodLabel(method: PaymentMethodOption): string {
}
function methodSelectedClass(type: string): string {
if (isAlipayMethod(type)) return 'border-[#02A9F1] bg-blue-50 text-gray-900 shadow-sm dark:bg-blue-950 dark:text-gray-100'
if (isWxpayMethod(type)) return 'border-[#09BB07] bg-green-50 text-gray-900 shadow-sm dark:bg-green-950 dark:text-gray-100'
if (isBuiltInAlipayMethod(type)) return 'border-[#02A9F1] bg-blue-50 text-gray-900 shadow-sm dark:bg-blue-950 dark:text-gray-100'
if (isBuiltInWxpayMethod(type)) return 'border-[#09BB07] bg-green-50 text-gray-900 shadow-sm dark:bg-green-950 dark:text-gray-100'
if (type === 'stripe') return 'border-[#676BE5] bg-indigo-50 text-gray-900 shadow-sm dark:bg-indigo-950 dark:text-gray-100'
if (type === 'airwallex') return 'border-[#FF6B3D] bg-orange-50 text-gray-900 shadow-sm dark:border-[#FF8E3C] dark:bg-orange-950 dark:text-gray-100'
return 'border-primary-500 bg-primary-50 text-gray-900 shadow-sm dark:bg-primary-950 dark:text-gray-100'
}
function isAlipayMethod(type: string): boolean {
return type === 'alipay' || type === 'alipay_direct'
}
function isWxpayMethod(type: string): boolean {
return type === 'wxpay' || type === 'wxpay_direct'
}
</script>
@@ -79,7 +79,7 @@ import { usePaymentStore } from '@/stores/payment'
import { useAppStore } from '@/stores'
import { paymentAPI } from '@/api/payment'
import { extractI18nErrorMessage } from '@/utils/apiError'
import { getPaymentPopupFeatures } from '@/components/payment/providerConfig'
import { getPaymentPopupFeatures, isBuiltInAlipayMethod, isBuiltInWxpayMethod } from '@/components/payment/providerConfig'
import type { PaymentOrder } from '@/types/payment'
import { currencySymbol } from '@/components/payment/currency'
import QRCode from 'qrcode'
@@ -122,8 +122,8 @@ let lastVerifyAt = 0
const VERIFY_RETRY_INTERVAL_MS = 15000
const VERIFY_RETRY_MAX_ATTEMPTS = 6
const isAlipay = computed(() => props.paymentType.includes('alipay'))
const isWxpay = computed(() => props.paymentType.includes('wxpay'))
const isAlipay = computed(() => isBuiltInAlipayMethod(props.paymentType))
const isWxpay = computed(() => isBuiltInWxpayMethod(props.paymentType))
const dialogTitle = computed(() => {
if (success.value) return t('payment.result.success')
@@ -79,7 +79,7 @@
<!-- Brand logo overlay -->
<div class="pointer-events-none absolute inset-0 flex items-center justify-center">
<span :class="['rounded-full p-2 shadow ring-2 ring-white', qrLogoBgClass]">
<img :src="isAlipay ? alipayIcon : wxpayIcon" alt="" class="h-5 w-5 brightness-0 invert" />
<img :src="qrLogoIcon" alt="" class="h-5 w-5 brightness-0 invert" />
</span>
</div>
</div>
@@ -128,13 +128,14 @@ import { usePaymentStore } from '@/stores/payment'
import { useAppStore } from '@/stores'
import { paymentAPI } from '@/api/payment'
import { extractI18nErrorMessage } from '@/utils/apiError'
import { getPaymentPopupFeatures } from '@/components/payment/providerConfig'
import { getPaymentPopupFeatures, isBuiltInAlipayMethod, isBuiltInWxpayMethod } from '@/components/payment/providerConfig'
import { currencySymbol, formatPaymentAmount, normalizePaymentCurrency } from '@/components/payment/currency'
import type { PaymentOrder } from '@/types/payment'
import Icon from '@/components/icons/Icon.vue'
import QRCode from 'qrcode'
import alipayIcon from '@/assets/icons/alipay.svg'
import wxpayIcon from '@/assets/icons/wxpay.svg'
import paymentIcon from '@/assets/icons/payment.svg'
const props = defineProps<{
orderId: number
@@ -182,8 +183,8 @@ let lastVerifyAt = 0
const VERIFY_RETRY_INTERVAL_MS = 15000
const VERIFY_RETRY_MAX_ATTEMPTS = 6
const isAlipay = computed(() => props.paymentType.includes('alipay'))
const isWxpay = computed(() => props.paymentType.includes('wxpay'))
const isAlipay = computed(() => isBuiltInAlipayMethod(props.paymentType))
const isWxpay = computed(() => isBuiltInWxpayMethod(props.paymentType))
const qrBorderClass = computed(() => {
if (isAlipay.value) return 'border-[#00AEEF] bg-blue-50 dark:border-[#00AEEF]/70 dark:bg-blue-950/20'
@@ -197,6 +198,12 @@ const qrLogoBgClass = computed(() => {
return 'bg-gray-400'
})
const qrLogoIcon = computed(() => {
if (isAlipay.value) return alipayIcon
if (isWxpay.value) return wxpayIcon
return paymentIcon
})
const scanTitle = computed(() => {
if (isAlipay.value) return t('payment.qr.scanAlipay')
if (isWxpay.value) return t('payment.qr.scanWxpay')
@@ -132,6 +132,28 @@ describe('PaymentStatusPanel', () => {
openSpy.mockRestore()
})
it('uses generic QR copy for custom methods that contain built-in names', async () => {
const wrapper = mount(PaymentStatusPanel, {
props: {
orderId: 42,
qrCode: 'https://pay.example.com/qr/42',
expiresAt: '2099-01-01T12:30:00Z',
paymentType: 'card_alipay',
orderType: 'balance',
},
global: {
stubs: {
Icon: true,
},
},
})
await flushPromises()
expect(wrapper.text()).toContain('payment.qr.scanToPay')
expect(wrapper.text()).not.toContain('payment.qr.scanAlipay')
})
it('actively verifies a stuck pending order and settles it when upstream confirms payment', async () => {
pollOrderStatus.mockResolvedValue(orderFactory('PENDING'))
verifyOrder.mockResolvedValue({
@@ -2,6 +2,8 @@ import { describe, expect, it } from 'vitest'
import {
PAYMENT_CURRENCY_OPTIONS,
PROVIDER_CONFIG_FIELDS,
isBuiltInAlipayMethod,
isBuiltInWxpayMethod,
parseEasyPayCustomMethods,
serializeEasyPayCustomMethods,
} from '@/components/payment/providerConfig'
@@ -79,3 +81,15 @@ describe('EasyPay custom methods config', () => {
expect(serializeEasyPayCustomMethods([{ type: '', upstreamType: 'epay', displayName: 'LDC' }])).toBe('')
})
})
describe('built-in payment method helpers', () => {
it('only treats exact built-in aliases as Alipay or WeChat Pay', () => {
expect(isBuiltInAlipayMethod('alipay')).toBe(true)
expect(isBuiltInAlipayMethod('alipay_direct')).toBe(true)
expect(isBuiltInAlipayMethod('card_alipay')).toBe(false)
expect(isBuiltInWxpayMethod('wxpay')).toBe(true)
expect(isBuiltInWxpayMethod('wxpay_direct')).toBe(true)
expect(isBuiltInWxpayMethod('card_wxpay')).toBe(false)
})
})
@@ -50,6 +50,14 @@ export const EASYPAY_PAYMENT_MODES = ['qrcode', 'popup'] as const
/** Fixed display order for user-facing payment methods */
export const METHOD_ORDER = ['alipay', 'alipay_direct', 'wxpay', 'wxpay_direct', 'stripe', 'airwallex'] as const
export function isBuiltInAlipayMethod(type: string): boolean {
return type === 'alipay' || type === 'alipay_direct'
}
export function isBuiltInWxpayMethod(type: string): boolean {
return type === 'wxpay' || type === 'wxpay_direct'
}
/** Payment mode constants */
export const PAYMENT_MODE_QRCODE = 'qrcode'
export const PAYMENT_MODE_POPUP = 'popup'
@@ -41,6 +41,7 @@ import { usePaymentStore } from '@/stores/payment'
import { paymentAPI } from '@/api/payment'
import { extractI18nErrorMessage } from '@/utils/apiError'
import { useAppStore } from '@/stores'
import { isBuiltInAlipayMethod, isBuiltInWxpayMethod } from '@/components/payment/providerConfig'
import QRCode from 'qrcode'
import alipayIcon from '@/assets/icons/alipay.svg'
import wxpayIcon from '@/assets/icons/wxpay.svg'
@@ -69,8 +70,8 @@ const countdownDisplay = computed(() => {
return m.toString().padStart(2, '0') + ':' + s.toString().padStart(2, '0')
})
const isAlipay = computed(() => paymentType.value.includes('alipay'))
const isWxpay = computed(() => paymentType.value.includes('wxpay'))
const isAlipay = computed(() => isBuiltInAlipayMethod(paymentType.value))
const isWxpay = computed(() => isBuiltInWxpayMethod(paymentType.value))
const scanTitle = computed(() => {
if (isAlipay.value) return t('payment.qr.scanAlipay')
+3 -3
View File
@@ -267,7 +267,7 @@ import type { SubscriptionPlan, CheckoutInfoResponse, CreateOrderResult, OrderTy
import AppLayout from '@/components/layout/AppLayout.vue'
import AmountInput from '@/components/payment/AmountInput.vue'
import PaymentMethodSelector from '@/components/payment/PaymentMethodSelector.vue'
import { METHOD_ORDER, getPaymentPopupFeatures } from '@/components/payment/providerConfig'
import { METHOD_ORDER, getPaymentPopupFeatures, isBuiltInAlipayMethod, isBuiltInWxpayMethod } from '@/components/payment/providerConfig'
import {
PAYMENT_RECOVERY_STORAGE_KEY,
buildCreateOrderPayload,
@@ -697,8 +697,8 @@ watch(() => [validAmount.value, selectedMethod.value] as const, ([amt, method])
const paymentButtonClass = computed(() => {
const m = selectedMethod.value
if (!m) return 'btn-primary'
if (m.includes('alipay')) return 'btn-alipay'
if (m.includes('wxpay')) return 'btn-wxpay'
if (isBuiltInAlipayMethod(m)) return 'btn-alipay'
if (isBuiltInWxpayMethod(m)) return 'btn-wxpay'
if (m === 'stripe') return 'btn-stripe'
if (m === 'airwallex') return 'btn-airwallex'
return 'btn-primary'