Skip to content

Commit 5c69eda

Browse files
authored
feat(sale): Surface vendor errors from custom webooks (#2802)
1 parent 609739c commit 5c69eda

12 files changed

Lines changed: 83 additions & 13 deletions

File tree

packages/checkout/sdk/src/widgets/definitions/events/sale.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,21 @@ export type SaleSuccess = {
3636
[key: string]: unknown;
3737
};
3838

39+
/**
40+
* Vendor error from custom authorization or quote webhooks.
41+
* Present when the failure is due to a vendor-specific rejection.
42+
*/
43+
export type VendorError = {
44+
code: string;
45+
message?: string;
46+
};
47+
48+
export type SaleFailedError = {
49+
type?: string;
50+
data?: { vendorError?: VendorError };
51+
[key: string]: unknown;
52+
};
53+
3954
/**
4055
* Type representing a Sale Widget with type FAILURE.
4156
* @property {string} reason
@@ -45,8 +60,8 @@ export type SaleSuccess = {
4560
export type SaleFailed = {
4661
/** The reason why sale transaction failed. */
4762
reason: string;
48-
/** The error object. */
49-
error: Record<string, unknown>;
63+
/** Error details. Will include vendorError if the failure is due to a vendor-specific rejection. */
64+
error: SaleFailedError;
5065
/** The timestamp of the failed swap. */
5166
timestamp: number;
5267
/** Chosen payment method */

packages/checkout/widgets-lib/src/context/view-context/SaleViewContextTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ interface SaleFailView extends ViewType {
4343
data?: {
4444
errorType: SaleErrorTypes;
4545
transactionHash?: string;
46+
vendorError?: { code: string; message?: string };
4647
[key: string]: unknown;
4748
};
4849
}

packages/checkout/widgets-lib/src/locales/en.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,10 @@
499499
"primaryAction": "Try again",
500500
"secondaryAction": "Cancel"
501501
},
502+
"SALE_AUTHORIZATION_REJECTED": {
503+
"description": "Sorry, your purchase could not be completed.",
504+
"secondaryAction": "Dismiss"
505+
},
502506
"DEFAULT_ERROR": {
503507
"description": "Sorry, something went wrong. Please try again.",
504508
"primaryAction": "Try again",

packages/checkout/widgets-lib/src/locales/ja.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,10 @@
459459
"primaryAction": "もう一度試す",
460460
"secondaryAction": "キャンセル"
461461
},
462+
"SALE_AUTHORIZATION_REJECTED": {
463+
"description": "申し訳ございません。購入を完了できませんでした。",
464+
"secondaryAction": "閉じる"
465+
},
462466
"DEFAULT_ERROR": {
463467
"description": "申し訳ありませんが、何かがうまくいかなかったようです。もう一度お試しください。",
464468
"primaryAction": "もう一度試す",

packages/checkout/widgets-lib/src/locales/ko.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,10 @@
456456
"primaryAction": "다시 시도",
457457
"secondaryAction": "취소"
458458
},
459+
"SALE_AUTHORIZATION_REJECTED": {
460+
"description": "죄송합니다, 구매를 완료할 수 없습니다.",
461+
"secondaryAction": "닫기"
462+
},
459463
"DEFAULT_ERROR": {
460464
"description": "죄송합니다, 문제가 발생했습니다. 다시 시도하세요.",
461465
"primaryAction": "다시 시도",

packages/checkout/widgets-lib/src/locales/zh.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,10 @@
456456
"primaryAction": "再试一次",
457457
"secondaryAction": "取消"
458458
},
459+
"SALE_AUTHORIZATION_REJECTED": {
460+
"description": "抱歉,您的购买无法完成。",
461+
"secondaryAction": "关闭"
462+
},
459463
"DEFAULT_ERROR": {
460464
"description": "抱歉,出了点问题。请再试一次。",
461465
"primaryAction": "再试一次",

packages/checkout/widgets-lib/src/widgets/sale/SaleWidget.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ export default function SaleWidget(props: SaleWidgetProps) {
159159
biomeTheme={biomeTheme}
160160
errorType={viewState.view.data?.errorType}
161161
transactionHash={viewState.view.data?.transactionHash}
162+
vendorMessage={viewState.view.data?.vendorError?.message}
162163
blockExplorerLink={BlockExplorerService.getTransactionLink(
163164
chainId.current as ChainId,
164165
viewState.view.data?.transactionHash!,

packages/checkout/widgets-lib/src/widgets/sale/context/SaleContextProvider.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ type SaleContextValues = SaleContextProps & {
8787
paymentMethod?: SalePaymentTypes | undefined,
8888
data?: Record<string, unknown>
8989
) => void;
90-
goToErrorView: (type: SaleErrorTypes, data?: Record<string, string>) => void;
90+
goToErrorView: (type: SaleErrorTypes, data?: Record<string, unknown>) => void;
9191
goToSuccessView: (data?: Record<string, unknown>) => void;
9292
fundingRoutes: FundingRoute[];
9393
disabledPaymentTypes: SalePaymentTypes[];
@@ -284,14 +284,21 @@ export function SaleContextProvider(props: {
284284
);
285285

286286
const goToErrorView = useCallback(
287-
(errorType: SaleErrorTypes, data: Record<string, string> = {}) => {
287+
(
288+
errorType: SaleErrorTypes,
289+
data: Record<string, unknown> & { vendorError?: { code: string; message?: string } } = {},
290+
) => {
288291
errorRetries.current += 1;
289292
if (errorRetries.current > MAX_ERROR_RETRIES) {
290293
errorRetries.current = 0;
291294
setPaymentMethod(undefined);
292295
}
293296

294-
trackError('commerce', 'saleError', new Error(errorType), data);
297+
const { vendorError, ...errorData } = data;
298+
trackError('commerce', 'saleError', new Error(errorType), {
299+
...errorData,
300+
...(vendorError ? { vendorCode: vendorError.code, vendorMessage: vendorError.message || '' } : {}),
301+
});
295302

296303
viewDispatch({
297304
payload: {

packages/checkout/widgets-lib/src/widgets/sale/hooks/useQuoteOrder.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const defaultOrderQuote: OrderQuote = {
2626

2727
export type ConfigError = {
2828
type: SaleErrorTypes;
29-
data?: Record<string, string>;
29+
data?: Record<string, unknown>;
3030
};
3131

3232
export const useQuoteOrder = ({
@@ -93,6 +93,16 @@ export const useQuoteOrder = ({
9393
});
9494

9595
if (!response.ok) {
96+
if (response.status === 400) {
97+
const { code, message } = await response.json();
98+
setOrderQuoteError({
99+
type: SaleErrorTypes.SALE_AUTHORIZATION_REJECTED,
100+
data: {
101+
vendorError: { code: code || '', message: message || undefined },
102+
},
103+
});
104+
return;
105+
}
96106
throw new Error(`${response.status} - ${response.statusText}`);
97107
}
98108

packages/checkout/widgets-lib/src/widgets/sale/hooks/useSignOrder.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,11 +291,17 @@ export const useSignOrder = (input: SignOrderInput) => {
291291

292292
const { ok, status } = response;
293293
if (!ok) {
294-
const { code } = (await response.json()) as SignApiError;
294+
const { code, message } = (await response.json()) as SignApiError;
295295
let errorType: SaleErrorTypes;
296+
let errorData: { code: string; message: string } | undefined;
297+
296298
switch (status) {
297299
case 400:
298-
errorType = SaleErrorTypes.SERVICE_BREAKDOWN;
300+
errorType = SaleErrorTypes.SALE_AUTHORIZATION_REJECTED;
301+
errorData = {
302+
code,
303+
message,
304+
};
299305
break;
300306
case 404:
301307
if (code === 'insufficient_stock') {
@@ -312,7 +318,7 @@ export const useSignOrder = (input: SignOrderInput) => {
312318
throw new Error('Unknown error');
313319
}
314320

315-
setSignError({ type: errorType });
321+
setSignError({ type: errorType, data: errorData });
316322
return undefined;
317323
}
318324

0 commit comments

Comments
 (0)