fix(payment): stop Airwallex checkout after leaving the page

This commit is contained in:
chihao.ou
2026-10-07 14:51:46 +08:00
parent 3f1a2ea0a7
commit 795a7e2783
2 changed files with 60 additions and 1 deletions
@@ -25,7 +25,7 @@
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import AppLayout from '@/components/layout/AppLayout.vue'
@@ -42,6 +42,9 @@ const router = useRouter()
const loading = ref(true)
const errorMessage = ref('')
let disposed = false
onUnmounted(() => { disposed = true })
function queryString(key: string): string {
const value = route.query[key]
@@ -101,11 +104,13 @@ onMounted(async () => {
try {
const airwallex = await import('@airwallex/components-sdk')
if (disposed) return
const result = await airwallex.init({
env: snapshot.paymentEnv === 'prod' ? 'prod' : 'demo',
enabledElements: ['payments'],
locale: checkoutLocale,
})
if (disposed) return
loading.value = false
const checkoutOptions = {
@@ -0,0 +1,54 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { enableAutoUnmount, flushPromises, shallowMount } from '@vue/test-utils'
import AirwallexPaymentView from '../AirwallexPaymentView.vue'
import { PAYMENT_RECOVERY_STORAGE_KEY } from '@/components/payment/paymentFlow'
const mocks = vi.hoisted(() => ({ init: vi.fn(), redirectToCheckout: vi.fn() }))
vi.mock('@airwallex/components-sdk', () => ({ init: mocks.init }))
vi.mock('vue-router', () => ({ useRoute: () => ({ query: {} }), useRouter: () => ({ push: vi.fn() }) }))
vi.mock('vue-i18n', async () => ({
...await vi.importActual<typeof import('vue-i18n')>('vue-i18n'),
useI18n: () => ({ t: (key: string) => key, locale: { value: 'zh-CN' } })
}))
enableAutoUnmount(afterEach)
afterEach(() => window.localStorage.clear())
beforeEach(() => {
vi.resetAllMocks()
window.localStorage.setItem(PAYMENT_RECOVERY_STORAGE_KEY, JSON.stringify({
orderId: 101, amount: 88, qrCode: '', expiresAt: '2099-01-01T00:10:00.000Z',
paymentType: 'airwallex', payUrl: '/payment/airwallex', outTradeNo: 'sub2_awx_101',
clientSecret: 'awx_client_secret', intentId: 'int_awx_101', currency: 'CNY',
countryCode: 'CN', paymentEnv: 'demo', payAmount: 88, orderType: 'balance',
paymentMode: '', resumeToken: 'resume-awx', createdAt: Date.UTC(2099, 0, 1)
}))
})
function mountView() {
return shallowMount(AirwallexPaymentView, {
global: { stubs: { AppLayout: { template: '<div><slot /></div>' }, Icon: true } }
})
}
describe('Airwallex checkout lifecycle', () => {
it('does not initialize checkout after leaving during the SDK import', async () => {
mocks.init.mockResolvedValue({ payments: { redirectToCheckout: mocks.redirectToCheckout } })
const wrapper = mountView()
wrapper.unmount()
await flushPromises()
expect(mocks.init).not.toHaveBeenCalled()
expect(mocks.redirectToCheckout).not.toHaveBeenCalled()
})
it.each([true, false])('redirects after SDK initialization only while mounted (leave=%s)', async (leave) => {
let finish!: (result: unknown) => void
mocks.init.mockReturnValue(new Promise(resolve => { finish = resolve }))
const wrapper = mountView()
await flushPromises()
expect(mocks.init).toHaveBeenCalledOnce()
if (leave) wrapper.unmount()
finish({ payments: { redirectToCheckout: mocks.redirectToCheckout } })
await flushPromises()
expect(mocks.redirectToCheckout).toHaveBeenCalledTimes(leave ? 0 : 1)
})
})