You've already forked AstralRinth
feat: improve error handling for withdraw modal (#5054)
* feat: improve error handling for withdraw modal * fix: add headers to error info * prepr --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -128,6 +128,7 @@ import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from 'vue'
|
||||
|
||||
import {
|
||||
createWithdrawContext,
|
||||
type PaymentProvider,
|
||||
type PayoutMethod,
|
||||
provideWithdrawContext,
|
||||
TAX_THRESHOLD_ACTUAL,
|
||||
@@ -332,51 +333,76 @@ function continueWithLimit() {
|
||||
setStage(nextStep.value)
|
||||
}
|
||||
|
||||
// TODO: God we need better errors from the backend (e.g error ids), this shit is insane
|
||||
function getWithdrawalError(error: any): { title: string; text: string } {
|
||||
const description = error?.data?.description?.toLowerCase() || ''
|
||||
function buildSupportData(error: any): Record<string, unknown> {
|
||||
// Extract response headers, excluding sensitive ones
|
||||
const responseHeaders: Record<string, string> = {}
|
||||
if (error?.response?.headers) {
|
||||
const headers = error.response.headers
|
||||
const entries =
|
||||
typeof headers.entries === 'function' ? [...headers.entries()] : Object.entries(headers)
|
||||
for (const [key, value] of entries) {
|
||||
const lowerKey = key.toLowerCase()
|
||||
// Exclude sensitive headers
|
||||
if (!['authorization', 'cookie', 'set-cookie'].includes(lowerKey)) {
|
||||
responseHeaders[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
timestamp: new Date().toISOString(),
|
||||
provider: withdrawData.value.selection.provider,
|
||||
method: withdrawData.value.selection.method,
|
||||
methodId: withdrawData.value.selection.methodId,
|
||||
country: withdrawData.value.selection.country?.id,
|
||||
amount: withdrawData.value.calculation?.amount,
|
||||
fee: withdrawData.value.calculation?.fee,
|
||||
request: {
|
||||
url: 'POST /api/v3/payout',
|
||||
},
|
||||
response: {
|
||||
status: error?.response?.status ?? error?.statusCode,
|
||||
statusText: error?.response?.statusText,
|
||||
headers: responseHeaders,
|
||||
body: error?.data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function formatBilingualText(
|
||||
messageDescriptor: { id: string; defaultMessage: string },
|
||||
values?: Record<string, string>,
|
||||
): string {
|
||||
const localized = formatMessage(messageDescriptor, values)
|
||||
// Interpolate values into the English default message
|
||||
let english = messageDescriptor.defaultMessage
|
||||
if (values) {
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
english = english.replace(`{${key}}`, value)
|
||||
}
|
||||
}
|
||||
|
||||
if (localized === english) {
|
||||
return localized
|
||||
}
|
||||
|
||||
return `${localized}\n${english}`
|
||||
}
|
||||
|
||||
function getWithdrawalError(
|
||||
error: any,
|
||||
provider: PaymentProvider | null,
|
||||
): { title: string; text: string; supportData: Record<string, unknown> } {
|
||||
const description = error?.data?.description?.toLowerCase() || ''
|
||||
const supportData = buildSupportData(error)
|
||||
|
||||
// === Common patterns (all providers) ===
|
||||
// Tax form error
|
||||
if (description.includes('tax form')) {
|
||||
return {
|
||||
title: formatMessage(messages.errorTaxFormTitle),
|
||||
text: formatMessage(messages.errorTaxFormText),
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid crypto wallet address
|
||||
if (
|
||||
(description.includes('wallet') && description.includes('invalid')) ||
|
||||
description.includes('wallet_address') ||
|
||||
(description.includes('blockchain') && description.includes('invalid'))
|
||||
) {
|
||||
return {
|
||||
title: formatMessage(messages.errorInvalidWalletTitle),
|
||||
text: formatMessage(messages.errorInvalidWalletText),
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid bank details
|
||||
if (
|
||||
(description.includes('bank') || description.includes('account')) &&
|
||||
(description.includes('invalid') || description.includes('failed'))
|
||||
) {
|
||||
return {
|
||||
title: formatMessage(messages.errorInvalidBankTitle),
|
||||
text: formatMessage(messages.errorInvalidBankText),
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid/fraudulent address
|
||||
if (
|
||||
description.includes('address') &&
|
||||
(description.includes('invalid') ||
|
||||
description.includes('verification') ||
|
||||
description.includes('fraudulent'))
|
||||
) {
|
||||
return {
|
||||
title: formatMessage(messages.errorInvalidAddressTitle),
|
||||
text: formatMessage(messages.errorInvalidAddressText),
|
||||
text: formatBilingualText(messages.errorTaxFormText),
|
||||
supportData,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,14 +414,106 @@ function getWithdrawalError(error: any): { title: string; text: string } {
|
||||
) {
|
||||
return {
|
||||
title: formatMessage(messages.errorMinimumNotMetTitle),
|
||||
text: formatMessage(messages.errorMinimumNotMetText),
|
||||
text: formatBilingualText(messages.errorMinimumNotMetText),
|
||||
supportData,
|
||||
}
|
||||
}
|
||||
|
||||
// Insufficient balance
|
||||
if (description.includes('enough funds') || description.includes('insufficient')) {
|
||||
return {
|
||||
title: formatMessage(messages.errorInsufficientBalanceTitle),
|
||||
text: formatBilingualText(messages.errorInsufficientBalanceText),
|
||||
supportData,
|
||||
}
|
||||
}
|
||||
|
||||
// Email verification required
|
||||
if (description.includes('verify your email') || description.includes('email_verified')) {
|
||||
return {
|
||||
title: formatMessage(messages.errorEmailVerificationTitle),
|
||||
text: formatBilingualText(messages.errorEmailVerificationText),
|
||||
supportData,
|
||||
}
|
||||
}
|
||||
|
||||
// === MuralPay-only patterns ===
|
||||
if (provider === 'muralpay') {
|
||||
// Invalid crypto wallet address
|
||||
if (
|
||||
(description.includes('wallet') && description.includes('invalid')) ||
|
||||
description.includes('wallet_address') ||
|
||||
(description.includes('blockchain') && description.includes('invalid'))
|
||||
) {
|
||||
return {
|
||||
title: formatMessage(messages.errorInvalidWalletTitle),
|
||||
text: formatBilingualText(messages.errorInvalidWalletText),
|
||||
supportData,
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid bank details
|
||||
if (
|
||||
(description.includes('bank') || description.includes('account')) &&
|
||||
(description.includes('invalid') || description.includes('failed'))
|
||||
) {
|
||||
return {
|
||||
title: formatMessage(messages.errorInvalidBankTitle),
|
||||
text: formatBilingualText(messages.errorInvalidBankText),
|
||||
supportData,
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid/fraudulent address (physical address for KYC)
|
||||
if (
|
||||
description.includes('address') &&
|
||||
(description.includes('invalid') ||
|
||||
description.includes('verification') ||
|
||||
description.includes('fraudulent'))
|
||||
) {
|
||||
return {
|
||||
title: formatMessage(messages.errorInvalidAddressTitle),
|
||||
text: formatBilingualText(messages.errorInvalidAddressText),
|
||||
supportData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === PayPal/Venmo-only patterns ===
|
||||
if (provider === 'paypal' || provider === 'venmo') {
|
||||
// Account not linked
|
||||
if (
|
||||
description.includes('not linked') ||
|
||||
description.includes('link') ||
|
||||
description.includes('paypal account') ||
|
||||
description.includes('venmo')
|
||||
) {
|
||||
return {
|
||||
title: formatMessage(messages.errorAccountNotLinkedTitle),
|
||||
text: formatBilingualText(messages.errorAccountNotLinkedText),
|
||||
supportData,
|
||||
}
|
||||
}
|
||||
|
||||
// Country mismatch for PayPal
|
||||
if (
|
||||
provider === 'paypal' &&
|
||||
(description.includes('us paypal') || description.includes('international paypal'))
|
||||
) {
|
||||
return {
|
||||
title: formatMessage(messages.errorPaypalCountryMismatchTitle),
|
||||
text: formatBilingualText(messages.errorPaypalCountryMismatchText),
|
||||
supportData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generic fallback
|
||||
const errorDescription = error?.data?.description || ''
|
||||
return {
|
||||
title: formatMessage(messages.errorGenericTitle),
|
||||
text: formatMessage(messages.errorGenericText),
|
||||
text: formatBilingualText(messages.errorGenericText, { error: errorDescription }),
|
||||
supportData,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,11 +527,15 @@ async function handleWithdraw() {
|
||||
} catch (error) {
|
||||
console.error('Withdrawal failed:', error)
|
||||
|
||||
const { title, text } = getWithdrawalError(error)
|
||||
const { title, text, supportData } = getWithdrawalError(
|
||||
error,
|
||||
withdrawData.value.selection.provider,
|
||||
)
|
||||
addNotification({
|
||||
title,
|
||||
text,
|
||||
type: 'error',
|
||||
supportData,
|
||||
})
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
@@ -570,7 +692,40 @@ const messages = defineMessages({
|
||||
errorGenericText: {
|
||||
id: 'dashboard.withdraw.error.generic.text',
|
||||
defaultMessage:
|
||||
'We were unable to submit your withdrawal request, please check your details or contact support.',
|
||||
'We were unable to submit your withdrawal request, please check your details or contact support.\n{error}',
|
||||
},
|
||||
errorInsufficientBalanceTitle: {
|
||||
id: 'dashboard.withdraw.error.insufficient-balance.title',
|
||||
defaultMessage: 'Insufficient balance',
|
||||
},
|
||||
errorInsufficientBalanceText: {
|
||||
id: 'dashboard.withdraw.error.insufficient-balance.text',
|
||||
defaultMessage: 'You do not have enough funds to make this withdrawal.',
|
||||
},
|
||||
errorEmailVerificationTitle: {
|
||||
id: 'dashboard.withdraw.error.email-verification.title',
|
||||
defaultMessage: 'Email verification required',
|
||||
},
|
||||
errorEmailVerificationText: {
|
||||
id: 'dashboard.withdraw.error.email-verification.text',
|
||||
defaultMessage: 'You must verify your email address before withdrawing funds.',
|
||||
},
|
||||
errorAccountNotLinkedTitle: {
|
||||
id: 'dashboard.withdraw.error.account-not-linked.title',
|
||||
defaultMessage: 'Account not linked',
|
||||
},
|
||||
errorAccountNotLinkedText: {
|
||||
id: 'dashboard.withdraw.error.account-not-linked.text',
|
||||
defaultMessage: 'Please link your payment account before withdrawing.',
|
||||
},
|
||||
errorPaypalCountryMismatchTitle: {
|
||||
id: 'dashboard.withdraw.error.paypal-country-mismatch.title',
|
||||
defaultMessage: 'PayPal region mismatch',
|
||||
},
|
||||
errorPaypalCountryMismatchText: {
|
||||
id: 'dashboard.withdraw.error.paypal-country-mismatch.text',
|
||||
defaultMessage:
|
||||
'Please use the correct PayPal transfer option for your region (US or International).',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -980,12 +980,30 @@
|
||||
"dashboard.withdraw.completion.wallet": {
|
||||
"message": "Wallet"
|
||||
},
|
||||
"dashboard.withdraw.error.account-not-linked.text": {
|
||||
"message": "Please link your payment account before withdrawing."
|
||||
},
|
||||
"dashboard.withdraw.error.account-not-linked.title": {
|
||||
"message": "Account not linked"
|
||||
},
|
||||
"dashboard.withdraw.error.email-verification.text": {
|
||||
"message": "You must verify your email address before withdrawing funds."
|
||||
},
|
||||
"dashboard.withdraw.error.email-verification.title": {
|
||||
"message": "Email verification required"
|
||||
},
|
||||
"dashboard.withdraw.error.generic.text": {
|
||||
"message": "We were unable to submit your withdrawal request, please check your details or contact support."
|
||||
"message": "We were unable to submit your withdrawal request, please check your details or contact support.\n{error}"
|
||||
},
|
||||
"dashboard.withdraw.error.generic.title": {
|
||||
"message": "Unable to withdraw"
|
||||
},
|
||||
"dashboard.withdraw.error.insufficient-balance.text": {
|
||||
"message": "You do not have enough funds to make this withdrawal."
|
||||
},
|
||||
"dashboard.withdraw.error.insufficient-balance.title": {
|
||||
"message": "Insufficient balance"
|
||||
},
|
||||
"dashboard.withdraw.error.invalid-address.text": {
|
||||
"message": "The address you provided could not be verified. Please check your address details."
|
||||
},
|
||||
@@ -1010,6 +1028,12 @@
|
||||
"dashboard.withdraw.error.minimum-not-met.title": {
|
||||
"message": "Amount too low"
|
||||
},
|
||||
"dashboard.withdraw.error.paypal-country-mismatch.text": {
|
||||
"message": "Please use the correct PayPal transfer option for your region (US or International)."
|
||||
},
|
||||
"dashboard.withdraw.error.paypal-country-mismatch.title": {
|
||||
"message": "PayPal region mismatch"
|
||||
},
|
||||
"dashboard.withdraw.error.tax-form.text": {
|
||||
"message": "You must complete a tax form to submit your withdrawal request."
|
||||
},
|
||||
|
||||
@@ -49,8 +49,13 @@
|
||||
x{{ item.count }}
|
||||
</div>
|
||||
<ButtonStyled circular size="small">
|
||||
<button v-tooltip="'Copy to clipboard'" @click="copyToClipboard(item)">
|
||||
<CheckIcon v-if="copied[createNotifText(item)]" />
|
||||
<button
|
||||
v-tooltip="
|
||||
item.supportData ? 'Copy error details for support' : 'Copy to clipboard'
|
||||
"
|
||||
@click="copyToClipboard(item)"
|
||||
>
|
||||
<CheckIcon v-if="copied[getCopyKey(item)]" />
|
||||
<CopyIcon v-else />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
@@ -106,18 +111,26 @@ function createNotifText(notif: WebNotification): string {
|
||||
return [notif.title, notif.text, notif.errorCode].filter(Boolean).join('\n')
|
||||
}
|
||||
|
||||
function getCopyKey(notif: WebNotification): string {
|
||||
return notif.supportData ? `support-${notif.id}` : createNotifText(notif)
|
||||
}
|
||||
|
||||
function checkIntercomPresence(): void {
|
||||
isIntercomPresent.value = !!document.querySelector('.intercom-lightweight-app')
|
||||
}
|
||||
|
||||
function copyToClipboard(notif: WebNotification): void {
|
||||
const text = createNotifText(notif)
|
||||
// If supportData is present, copy the full JSON for support; otherwise copy plain text
|
||||
const text = notif.supportData
|
||||
? JSON.stringify(notif.supportData, null, 2)
|
||||
: createNotifText(notif)
|
||||
|
||||
copied.value[text] = true
|
||||
const key = getCopyKey(notif)
|
||||
copied.value[key] = true
|
||||
navigator.clipboard.writeText(text)
|
||||
|
||||
setTimeout(() => {
|
||||
const { [text]: _, ...rest } = copied.value
|
||||
const { [key]: _, ...rest } = copied.value
|
||||
copied.value = rest
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface WebNotification {
|
||||
errorCode?: string
|
||||
count?: number
|
||||
timer?: NodeJS.Timeout
|
||||
supportData?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type NotificationPanelLocation = 'left' | 'right'
|
||||
|
||||
Reference in New Issue
Block a user