Refine payment UX for wallet flows

This commit is contained in:
IanShaw027
2026-04-21 00:05:09 +08:00
parent 4ebdfcd13a
commit f83fd59dca
9 changed files with 373 additions and 18 deletions

View File

@@ -88,6 +88,7 @@
</button>
<div v-if="errorMessage" class="rounded-xl border border-red-200 bg-red-50 p-4 dark:border-red-800/50 dark:bg-red-900/20">
<p class="text-sm text-red-700 dark:text-red-400">{{ errorMessage }}</p>
<p v-if="errorHintMessage" class="mt-2 text-xs text-red-600 dark:text-red-300">{{ errorHintMessage }}</p>
</div>
</template>
</template>
@@ -174,6 +175,7 @@
<button class="btn btn-secondary w-full" @click="selectedPlan = null">{{ t('common.cancel') }}</button>
<div v-if="errorMessage" class="rounded-xl border border-red-200 bg-red-50 p-4 dark:border-red-800/50 dark:bg-red-900/20">
<p class="text-sm text-red-700 dark:text-red-400">{{ errorMessage }}</p>
<p v-if="errorHintMessage" class="mt-2 text-xs text-red-600 dark:text-red-300">{{ errorHintMessage }}</p>
</div>
</template>
<!-- Plan list -->
@@ -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<number | null>(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<string, unknown> | 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) {