Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
65d3bd72
Commit
65d3bd72
authored
Apr 21, 2026
by
IanShaw027
Browse files
frontend: normalize payment error presentation
parent
20062b44
Changes
3
Hide whitespace changes
Inline
Side-by-side
frontend/src/views/user/PaymentView.vue
View file @
65d3bd72
...
...
@@ -86,10 +86,6 @@
<
/span
>
<
span
v
-
else
>
{{
t
(
'
payment.createOrder
'
)
}}
¥
{{
totalAmount
.
toFixed
(
2
)
}}
<
/span
>
<
/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
>
<!--
Subscribe
Tab
-->
...
...
@@ -173,10 +169,6 @@
<
span
v
-
else
>
{{
t
(
'
payment.createOrder
'
)
}}
¥
{{
(
feeRate
>
0
?
subTotalAmount
:
selectedPlan
.
price
).
toFixed
(
2
)
}}
<
/span
>
<
/button
>
<
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
-->
<
template
v
-
else
>
...
...
@@ -196,7 +188,7 @@
<
div
:
class
=
"
['h-6 w-1 shrink-0 rounded-full', platformAccentBarClass(sub.group?.platform || '')]
"
/>
<
div
class
=
"
min-w-0 flex-1
"
>
<
div
class
=
"
flex items-center gap-1.5
"
>
<
span
class
=
"
truncate text-xs font-semibold text-gray-900 dark:text-white
"
>
{{
sub
.
group
?.
name
||
`Group #${
sub.group_id
}
`
}}
<
/span
>
<
span
class
=
"
truncate text-xs font-semibold text-gray-900 dark:text-white
"
>
{{
sub
.
group
?.
name
||
t
(
'
payment.groupFallback
'
,
{
id
:
sub
.
group_id
}
)
}}
<
/span
>
<
span
:
class
=
"
['shrink-0 rounded-full px-1.5 py-0.5 text-[9px] font-medium', platformBadgeLightClass(sub.group?.platform || '')]
"
>
{{
platformLabel
(
sub
.
group
?.
platform
||
''
)
}}
<
/span
>
<
/div
>
<
div
class
=
"
flex flex-wrap gap-x-3 text-[11px] text-gray-400 dark:text-gray-500
"
>
...
...
@@ -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<WeixinJSBridgeLike | n
async
function
invokeWechatJsapiPayment
(
payload
:
Record
<
string
,
unknown
>
):
Promise
<
Record
<
string
,
unknown
>>
{
const
bridge
=
await
waitForWeixinJSBridge
()
if
(
!
bridge
)
{
throw
new
Error
(
'
W
eixinJSBridge is unavailable
'
)
throw
new
Error
(
'
W
ECHAT_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
()
{
...
...
frontend/src/views/user/__tests__/paymentUx.spec.ts
View file @
65d3bd72
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.
'
)
})
})
frontend/src/views/user/paymentUx.ts
View file @
65d3bd72
...
...
@@ -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
'
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment