diff --git a/packages/checkout/sdk/src/widgets/definitions/events/sale.ts b/packages/checkout/sdk/src/widgets/definitions/events/sale.ts index 6c5d2281d0..fd9648edff 100644 --- a/packages/checkout/sdk/src/widgets/definitions/events/sale.ts +++ b/packages/checkout/sdk/src/widgets/definitions/events/sale.ts @@ -36,6 +36,21 @@ export type SaleSuccess = { [key: string]: unknown; }; +/** + * Vendor error from custom authorization or quote webhooks. + * Present when the failure is due to a vendor-specific rejection. + */ +export type VendorError = { + code: string; + message?: string; +}; + +export type SaleFailedError = { + type?: string; + data?: { vendorError?: VendorError }; + [key: string]: unknown; +}; + /** * Type representing a Sale Widget with type FAILURE. * @property {string} reason @@ -45,8 +60,8 @@ export type SaleSuccess = { export type SaleFailed = { /** The reason why sale transaction failed. */ reason: string; - /** The error object. */ - error: Record; + /** Error details. Will include vendorError if the failure is due to a vendor-specific rejection. */ + error: SaleFailedError; /** The timestamp of the failed swap. */ timestamp: number; /** Chosen payment method */ diff --git a/packages/checkout/widgets-lib/src/context/view-context/SaleViewContextTypes.ts b/packages/checkout/widgets-lib/src/context/view-context/SaleViewContextTypes.ts index 0197505a7c..bca8e28e8b 100644 --- a/packages/checkout/widgets-lib/src/context/view-context/SaleViewContextTypes.ts +++ b/packages/checkout/widgets-lib/src/context/view-context/SaleViewContextTypes.ts @@ -43,6 +43,7 @@ interface SaleFailView extends ViewType { data?: { errorType: SaleErrorTypes; transactionHash?: string; + vendorError?: { code: string; message?: string }; [key: string]: unknown; }; } diff --git a/packages/checkout/widgets-lib/src/locales/en.json b/packages/checkout/widgets-lib/src/locales/en.json index b1bddf0436..0f74ee9bd5 100644 --- a/packages/checkout/widgets-lib/src/locales/en.json +++ b/packages/checkout/widgets-lib/src/locales/en.json @@ -499,6 +499,10 @@ "primaryAction": "Try again", "secondaryAction": "Cancel" }, + "SALE_AUTHORIZATION_REJECTED": { + "description": "Sorry, your purchase could not be completed.", + "secondaryAction": "Dismiss" + }, "DEFAULT_ERROR": { "description": "Sorry, something went wrong. Please try again.", "primaryAction": "Try again", diff --git a/packages/checkout/widgets-lib/src/locales/ja.json b/packages/checkout/widgets-lib/src/locales/ja.json index 1aaed4ea55..f1dd4e2d8f 100644 --- a/packages/checkout/widgets-lib/src/locales/ja.json +++ b/packages/checkout/widgets-lib/src/locales/ja.json @@ -459,6 +459,10 @@ "primaryAction": "もう一度試す", "secondaryAction": "キャンセル" }, + "SALE_AUTHORIZATION_REJECTED": { + "description": "申し訳ございません。購入を完了できませんでした。", + "secondaryAction": "閉じる" + }, "DEFAULT_ERROR": { "description": "申し訳ありませんが、何かがうまくいかなかったようです。もう一度お試しください。", "primaryAction": "もう一度試す", diff --git a/packages/checkout/widgets-lib/src/locales/ko.json b/packages/checkout/widgets-lib/src/locales/ko.json index a06e044f8c..366a33e8e5 100644 --- a/packages/checkout/widgets-lib/src/locales/ko.json +++ b/packages/checkout/widgets-lib/src/locales/ko.json @@ -456,6 +456,10 @@ "primaryAction": "다시 시도", "secondaryAction": "취소" }, + "SALE_AUTHORIZATION_REJECTED": { + "description": "죄송합니다, 구매를 완료할 수 없습니다.", + "secondaryAction": "닫기" + }, "DEFAULT_ERROR": { "description": "죄송합니다, 문제가 발생했습니다. 다시 시도하세요.", "primaryAction": "다시 시도", diff --git a/packages/checkout/widgets-lib/src/locales/zh.json b/packages/checkout/widgets-lib/src/locales/zh.json index 0252b7b050..f389943b9f 100644 --- a/packages/checkout/widgets-lib/src/locales/zh.json +++ b/packages/checkout/widgets-lib/src/locales/zh.json @@ -456,6 +456,10 @@ "primaryAction": "再试一次", "secondaryAction": "取消" }, + "SALE_AUTHORIZATION_REJECTED": { + "description": "抱歉,您的购买无法完成。", + "secondaryAction": "关闭" + }, "DEFAULT_ERROR": { "description": "抱歉,出了点问题。请再试一次。", "primaryAction": "再试一次", diff --git a/packages/checkout/widgets-lib/src/widgets/sale/SaleWidget.tsx b/packages/checkout/widgets-lib/src/widgets/sale/SaleWidget.tsx index e56271234e..ae9a4a54c4 100644 --- a/packages/checkout/widgets-lib/src/widgets/sale/SaleWidget.tsx +++ b/packages/checkout/widgets-lib/src/widgets/sale/SaleWidget.tsx @@ -159,6 +159,7 @@ export default function SaleWidget(props: SaleWidgetProps) { biomeTheme={biomeTheme} errorType={viewState.view.data?.errorType} transactionHash={viewState.view.data?.transactionHash} + vendorMessage={viewState.view.data?.vendorError?.message} blockExplorerLink={BlockExplorerService.getTransactionLink( chainId.current as ChainId, viewState.view.data?.transactionHash!, diff --git a/packages/checkout/widgets-lib/src/widgets/sale/context/SaleContextProvider.tsx b/packages/checkout/widgets-lib/src/widgets/sale/context/SaleContextProvider.tsx index a20547f59e..2a765a6dcf 100644 --- a/packages/checkout/widgets-lib/src/widgets/sale/context/SaleContextProvider.tsx +++ b/packages/checkout/widgets-lib/src/widgets/sale/context/SaleContextProvider.tsx @@ -87,7 +87,7 @@ type SaleContextValues = SaleContextProps & { paymentMethod?: SalePaymentTypes | undefined, data?: Record ) => void; - goToErrorView: (type: SaleErrorTypes, data?: Record) => void; + goToErrorView: (type: SaleErrorTypes, data?: Record) => void; goToSuccessView: (data?: Record) => void; fundingRoutes: FundingRoute[]; disabledPaymentTypes: SalePaymentTypes[]; @@ -284,14 +284,21 @@ export function SaleContextProvider(props: { ); const goToErrorView = useCallback( - (errorType: SaleErrorTypes, data: Record = {}) => { + ( + errorType: SaleErrorTypes, + data: Record & { vendorError?: { code: string; message?: string } } = {}, + ) => { errorRetries.current += 1; if (errorRetries.current > MAX_ERROR_RETRIES) { errorRetries.current = 0; setPaymentMethod(undefined); } - trackError('commerce', 'saleError', new Error(errorType), data); + const { vendorError, ...errorData } = data; + trackError('commerce', 'saleError', new Error(errorType), { + ...errorData, + ...(vendorError ? { vendorCode: vendorError.code, vendorMessage: vendorError.message || '' } : {}), + }); viewDispatch({ payload: { diff --git a/packages/checkout/widgets-lib/src/widgets/sale/hooks/useQuoteOrder.ts b/packages/checkout/widgets-lib/src/widgets/sale/hooks/useQuoteOrder.ts index 3aa958f49e..da473e5b49 100644 --- a/packages/checkout/widgets-lib/src/widgets/sale/hooks/useQuoteOrder.ts +++ b/packages/checkout/widgets-lib/src/widgets/sale/hooks/useQuoteOrder.ts @@ -26,7 +26,7 @@ export const defaultOrderQuote: OrderQuote = { export type ConfigError = { type: SaleErrorTypes; - data?: Record; + data?: Record; }; export const useQuoteOrder = ({ @@ -93,6 +93,16 @@ export const useQuoteOrder = ({ }); if (!response.ok) { + if (response.status === 400) { + const { code, message } = await response.json(); + setOrderQuoteError({ + type: SaleErrorTypes.SALE_AUTHORIZATION_REJECTED, + data: { + vendorError: { code: code || '', message: message || undefined }, + }, + }); + return; + } throw new Error(`${response.status} - ${response.statusText}`); } diff --git a/packages/checkout/widgets-lib/src/widgets/sale/hooks/useSignOrder.ts b/packages/checkout/widgets-lib/src/widgets/sale/hooks/useSignOrder.ts index d4d7a8435b..ef7ded33df 100644 --- a/packages/checkout/widgets-lib/src/widgets/sale/hooks/useSignOrder.ts +++ b/packages/checkout/widgets-lib/src/widgets/sale/hooks/useSignOrder.ts @@ -291,11 +291,17 @@ export const useSignOrder = (input: SignOrderInput) => { const { ok, status } = response; if (!ok) { - const { code } = (await response.json()) as SignApiError; + const { code, message } = (await response.json()) as SignApiError; let errorType: SaleErrorTypes; + let errorData: { code: string; message: string } | undefined; + switch (status) { case 400: - errorType = SaleErrorTypes.SERVICE_BREAKDOWN; + errorType = SaleErrorTypes.SALE_AUTHORIZATION_REJECTED; + errorData = { + code, + message, + }; break; case 404: if (code === 'insufficient_stock') { @@ -312,7 +318,7 @@ export const useSignOrder = (input: SignOrderInput) => { throw new Error('Unknown error'); } - setSignError({ type: errorType }); + setSignError({ type: errorType, data: errorData }); return undefined; } diff --git a/packages/checkout/widgets-lib/src/widgets/sale/types.ts b/packages/checkout/widgets-lib/src/widgets/sale/types.ts index 77e0632e85..35376a9372 100644 --- a/packages/checkout/widgets-lib/src/widgets/sale/types.ts +++ b/packages/checkout/widgets-lib/src/widgets/sale/types.ts @@ -59,7 +59,9 @@ export type SignOrderInput = { export type SignOrderError = { type: SaleErrorTypes; - data?: Record; + data?: + | Record + | { vendorError: { code: string; message: string } }; }; export type ExecutedTransaction = { @@ -85,6 +87,7 @@ export enum SaleErrorTypes { WALLET_REJECTED_NO_FUNDS = 'WALLET_REJECTED_NO_FUNDS', WALLET_POPUP_BLOCKED = 'WALLET_POPUP_BLOCKED', FUNDING_ROUTE_EXECUTE_ERROR = 'FUNDING_ROUTE_EXECUTE_ERROR', + SALE_AUTHORIZATION_REJECTED = 'SALE_AUTHORIZATION_REJECTED', } export type OrderQuoteCurrency = { diff --git a/packages/checkout/widgets-lib/src/widgets/sale/views/SaleErrorView.tsx b/packages/checkout/widgets-lib/src/widgets/sale/views/SaleErrorView.tsx index 08cf53f0fa..e7adb2eaa2 100644 --- a/packages/checkout/widgets-lib/src/widgets/sale/views/SaleErrorView.tsx +++ b/packages/checkout/widgets-lib/src/widgets/sale/views/SaleErrorView.tsx @@ -36,6 +36,7 @@ type SaleErrorViewProps = { errorType: SaleErrorTypes | undefined; transactionHash?: string; blockExplorerLink?: string; + vendorMessage?: string; }; export function SaleErrorView({ @@ -43,6 +44,7 @@ export function SaleErrorView({ transactionHash, blockExplorerLink, errorType, + vendorMessage, }: SaleErrorViewProps) { const { t } = useTranslation(); const { @@ -198,6 +200,13 @@ export function SaleErrorView({ onSecondaryActionClick: closeWidget, statusType: StatusType.INFORMATION, }, + [SaleErrorTypes.SALE_AUTHORIZATION_REJECTED]: { + onSecondaryActionClick: closeWidget, + statusType: StatusType.INFORMATION, + statusIconStyles: { + fill: biomeTheme.color.status.fatal.dim, + }, + }, [SaleErrorTypes.INVALID_PARAMETERS]: { onSecondaryActionClick: closeWidget, statusType: StatusType.ALERT, @@ -222,11 +231,13 @@ export function SaleErrorView({ ? t(`views.SALE_FAIL.errors.${currentErrorType}.secondaryAction`) : t(`views.SALE_FAIL.errors.${SaleErrorTypes.DEFAULT}.secondaryAction`); + const useVendorMessage = currentErrorType === SaleErrorTypes.SALE_AUTHORIZATION_REJECTED + && vendorMessage; + return { headingText: t('views.PAYMENT_METHODS.handover.error.heading'), - subheadingText: t( - `views.SALE_FAIL.errors.${currentErrorType}.description`, - ), + subheadingText: useVendorMessage + || t(`views.SALE_FAIL.errors.${currentErrorType}.description`), primaryButtonText: t( `views.SALE_FAIL.errors.${currentErrorType}.primaryAction`, ),