@imtbl/checkout-widgets 2.13.1-alpha.1 → 2.13.1-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/browser/{AddTokensWidget-CVbFWAot.js → AddTokensWidget-Cg3kNYwE.js} +3 -3
  2. package/dist/browser/{BridgeWidget-Cefk1gkw.js → BridgeWidget-0flgb-N1.js} +7 -7
  3. package/dist/browser/{CommerceWidget-NXthYSpx.js → CommerceWidget-2VhyeA02.js} +13 -13
  4. package/dist/browser/{FeesBreakdown-tg2lAOnO.js → FeesBreakdown-BJrZJImT.js} +1 -1
  5. package/dist/browser/{OnRampWidget-lvqcozvT.js → OnRampWidget-BozHdNU4.js} +3 -3
  6. package/dist/browser/{SaleWidget-Dvc0_EEn.js → SaleWidget-BB_xMBGx.js} +101 -31
  7. package/dist/browser/{SpendingCapHero-BFHqF1dX.js → SpendingCapHero-BLrvI0li.js} +1 -1
  8. package/dist/browser/{SwapWidget-CYTjf6ud.js → SwapWidget-D23IWAO5.js} +6 -6
  9. package/dist/browser/{TokenImage-CVz0lGmE.js → TokenImage-_WdUg8XS.js} +1 -1
  10. package/dist/browser/{TopUpView-DYT0f-9N.js → TopUpView-Cjdwb3R3.js} +1 -1
  11. package/dist/browser/{WalletApproveHero-BUVcdQWv.js → WalletApproveHero-jf_bmGlB.js} +2 -2
  12. package/dist/browser/{WalletWidget-CyhEc7dO.js → WalletWidget-Ij5pSSxI.js} +3 -3
  13. package/dist/browser/{auto-track-ClvhG7YB.js → auto-track-McIwD5Q7.js} +1 -1
  14. package/dist/browser/{index-W0eJIqld.js → index-B6WTkoit.js} +1 -1
  15. package/dist/browser/{index-DA458Pd9.js → index-Be6rOcLe.js} +40 -24
  16. package/dist/browser/{index-z0tG99pl.js → index-BfpyrGAT.js} +2 -2
  17. package/dist/browser/{index--h6CR_Q1.js → index-C8z8zOXm.js} +1 -1
  18. package/dist/browser/{index-DUqwg_qY.js → index-CIa-L5PJ.js} +1 -1
  19. package/dist/browser/{index-BBed5GAI.js → index-CgmC5hvC.js} +1 -1
  20. package/dist/browser/{index-CdOujJbj.js → index-DSvEZTRq.js} +1 -1
  21. package/dist/browser/{index-KXdtYT2Q.js → index-DyTPoz4t.js} +1 -1
  22. package/dist/browser/index.js +1 -1
  23. package/dist/browser/{index.umd-RYK-K6t5.js → index.umd-CgoHQll9.js} +1 -1
  24. package/dist/browser/{useInterval-BmM93dLg.js → useInterval-Bg8_SxK2.js} +1 -1
  25. package/dist/types/context/view-context/SaleViewContextTypes.d.ts +4 -0
  26. package/dist/types/widgets/sale/context/SaleContextProvider.d.ts +1 -1
  27. package/dist/types/widgets/sale/hooks/useFundingBalances.d.ts +1 -0
  28. package/dist/types/widgets/sale/hooks/useQuoteOrder.d.ts +1 -1
  29. package/dist/types/widgets/sale/types.d.ts +8 -2
  30. package/dist/types/widgets/sale/views/SaleErrorView.d.ts +2 -1
  31. package/package.json +7 -7
  32. package/src/context/view-context/SaleViewContextTypes.ts +1 -0
  33. package/src/locales/en.json +4 -0
  34. package/src/locales/ja.json +4 -0
  35. package/src/locales/ko.json +4 -0
  36. package/src/locales/zh.json +4 -0
  37. package/src/widgets/sale/SaleWidget.tsx +1 -0
  38. package/src/widgets/sale/context/SaleContextProvider.tsx +13 -3
  39. package/src/widgets/sale/functions/fetchFundingBalances.ts +0 -8
  40. package/src/widgets/sale/hooks/useFundingBalances.ts +6 -5
  41. package/src/widgets/sale/hooks/useQuoteOrder.ts +55 -2
  42. package/src/widgets/sale/hooks/useSignOrder.ts +9 -3
  43. package/src/widgets/sale/types.ts +4 -1
  44. package/src/widgets/sale/views/OrderSummary.tsx +10 -0
  45. package/src/widgets/sale/views/SaleErrorView.tsx +14 -3
@@ -47,7 +47,12 @@ export type SignOrderInput = {
47
47
  };
48
48
  export type SignOrderError = {
49
49
  type: SaleErrorTypes;
50
- data?: Record<string, string>;
50
+ data?: Record<string, string> | {
51
+ vendorError: {
52
+ code: string;
53
+ message: string;
54
+ };
55
+ };
51
56
  };
52
57
  export type ExecutedTransaction = {
53
58
  method: string;
@@ -69,7 +74,8 @@ export declare enum SaleErrorTypes {
69
74
  WALLET_REJECTED = "WALLET_REJECTED",
70
75
  WALLET_REJECTED_NO_FUNDS = "WALLET_REJECTED_NO_FUNDS",
71
76
  WALLET_POPUP_BLOCKED = "WALLET_POPUP_BLOCKED",
72
- FUNDING_ROUTE_EXECUTE_ERROR = "FUNDING_ROUTE_EXECUTE_ERROR"
77
+ FUNDING_ROUTE_EXECUTE_ERROR = "FUNDING_ROUTE_EXECUTE_ERROR",
78
+ SALE_AUTHORIZATION_REJECTED = "SALE_AUTHORIZATION_REJECTED"
73
79
  }
74
80
  export type OrderQuoteCurrency = {
75
81
  base: boolean;
@@ -5,6 +5,7 @@ type SaleErrorViewProps = {
5
5
  errorType: SaleErrorTypes | undefined;
6
6
  transactionHash?: string;
7
7
  blockExplorerLink?: string;
8
+ vendorMessage?: string;
8
9
  };
9
- export declare function SaleErrorView({ biomeTheme, transactionHash, blockExplorerLink, errorType, }: SaleErrorViewProps): null;
10
+ export declare function SaleErrorView({ biomeTheme, transactionHash, blockExplorerLink, errorType, vendorMessage, }: SaleErrorViewProps): null;
10
11
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imtbl/checkout-widgets",
3
- "version": "2.13.1-alpha.1",
3
+ "version": "2.13.1-alpha.3",
4
4
  "browserslist": {
5
5
  "production": [
6
6
  ">0.2%",
@@ -18,12 +18,12 @@
18
18
  "@biom3/design-tokens": "^0.4.5",
19
19
  "@biom3/react": "^0.29.4",
20
20
  "@emotion/react": "^11.11.3",
21
- "@imtbl/bridge-sdk": "2.13.1-alpha.1",
22
- "@imtbl/checkout-sdk": "2.13.1-alpha.1",
23
- "@imtbl/config": "2.13.1-alpha.1",
24
- "@imtbl/cryptofiat": "2.13.1-alpha.1",
25
- "@imtbl/dex-sdk": "2.13.1-alpha.1",
26
- "@imtbl/passport": "2.13.1-alpha.1",
21
+ "@imtbl/bridge-sdk": "2.13.1-alpha.3",
22
+ "@imtbl/checkout-sdk": "2.13.1-alpha.3",
23
+ "@imtbl/config": "2.13.1-alpha.3",
24
+ "@imtbl/cryptofiat": "2.13.1-alpha.3",
25
+ "@imtbl/dex-sdk": "2.13.1-alpha.3",
26
+ "@imtbl/passport": "2.13.1-alpha.3",
27
27
  "@imtbl/react-analytics": "0.3.4-alpha",
28
28
  "@rive-app/react-canvas-lite": "^4.9.0",
29
29
  "@walletconnect/ethereum-provider": "^2.11.1",
@@ -43,6 +43,7 @@ interface SaleFailView extends ViewType {
43
43
  data?: {
44
44
  errorType: SaleErrorTypes;
45
45
  transactionHash?: string;
46
+ vendorError?: { code: string; message?: string };
46
47
  [key: string]: unknown;
47
48
  };
48
49
  }
@@ -499,6 +499,10 @@
499
499
  "primaryAction": "Try again",
500
500
  "secondaryAction": "Cancel"
501
501
  },
502
+ "SALE_AUTHORIZATION_REJECTED": {
503
+ "description": "Sorry, your purchase could not be completed.",
504
+ "secondaryAction": "Dismiss"
505
+ },
502
506
  "DEFAULT_ERROR": {
503
507
  "description": "Sorry, something went wrong. Please try again.",
504
508
  "primaryAction": "Try again",
@@ -459,6 +459,10 @@
459
459
  "primaryAction": "もう一度試す",
460
460
  "secondaryAction": "キャンセル"
461
461
  },
462
+ "SALE_AUTHORIZATION_REJECTED": {
463
+ "description": "申し訳ございません。購入を完了できませんでした。",
464
+ "secondaryAction": "閉じる"
465
+ },
462
466
  "DEFAULT_ERROR": {
463
467
  "description": "申し訳ありませんが、何かがうまくいかなかったようです。もう一度お試しください。",
464
468
  "primaryAction": "もう一度試す",
@@ -456,6 +456,10 @@
456
456
  "primaryAction": "다시 시도",
457
457
  "secondaryAction": "취소"
458
458
  },
459
+ "SALE_AUTHORIZATION_REJECTED": {
460
+ "description": "죄송합니다, 구매를 완료할 수 없습니다.",
461
+ "secondaryAction": "닫기"
462
+ },
459
463
  "DEFAULT_ERROR": {
460
464
  "description": "죄송합니다, 문제가 발생했습니다. 다시 시도하세요.",
461
465
  "primaryAction": "다시 시도",
@@ -456,6 +456,10 @@
456
456
  "primaryAction": "再试一次",
457
457
  "secondaryAction": "取消"
458
458
  },
459
+ "SALE_AUTHORIZATION_REJECTED": {
460
+ "description": "抱歉,您的购买无法完成。",
461
+ "secondaryAction": "关闭"
462
+ },
459
463
  "DEFAULT_ERROR": {
460
464
  "description": "抱歉,出了点问题。请再试一次。",
461
465
  "primaryAction": "再试一次",
@@ -159,6 +159,7 @@ export default function SaleWidget(props: SaleWidgetProps) {
159
159
  biomeTheme={biomeTheme}
160
160
  errorType={viewState.view.data?.errorType}
161
161
  transactionHash={viewState.view.data?.transactionHash}
162
+ vendorMessage={viewState.view.data?.vendorError?.message}
162
163
  blockExplorerLink={BlockExplorerService.getTransactionLink(
163
164
  chainId.current as ChainId,
164
165
  viewState.view.data?.transactionHash!,
@@ -87,7 +87,7 @@ type SaleContextValues = SaleContextProps & {
87
87
  paymentMethod?: SalePaymentTypes | undefined,
88
88
  data?: Record<string, unknown>
89
89
  ) => void;
90
- goToErrorView: (type: SaleErrorTypes, data?: Record<string, string>) => void;
90
+ goToErrorView: (type: SaleErrorTypes, data?: Record<string, unknown>) => void;
91
91
  goToSuccessView: (data?: Record<string, unknown>) => void;
92
92
  fundingRoutes: FundingRoute[];
93
93
  disabledPaymentTypes: SalePaymentTypes[];
@@ -284,14 +284,24 @@ export function SaleContextProvider(props: {
284
284
  );
285
285
 
286
286
  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
+ ) => {
288
291
  errorRetries.current += 1;
289
292
  if (errorRetries.current > MAX_ERROR_RETRIES) {
290
293
  errorRetries.current = 0;
291
294
  setPaymentMethod(undefined);
292
295
  }
293
296
 
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
+ });
302
+
303
+ // eslint-disable-next-line no-console
304
+ console.error('[IMTBL]: Sale error', errorType, data);
295
305
 
296
306
  viewDispatch({
297
307
  payload: {
@@ -47,7 +47,6 @@ export const fetchFundingBalances = async (
47
47
  onFundingBalance,
48
48
  getAmountByCurrency,
49
49
  getIsGasless,
50
- onComplete,
51
50
  onFundingRequirement,
52
51
  onUpdateGasFees,
53
52
  } = params;
@@ -85,10 +84,6 @@ export const fetchFundingBalances = async (
85
84
  ? undefined
86
85
  : getGasEstimate();
87
86
 
88
- const handleOnComplete = () => {
89
- onComplete?.(pushToFoundBalances([]));
90
- };
91
-
92
87
  const handleOnFundingRoute = (route) => {
93
88
  updateFundingBalances(getAlternativeFundingSteps([route], environment));
94
89
  };
@@ -99,9 +94,6 @@ export const fetchFundingBalances = async (
99
94
  transactionOrGasAmount,
100
95
  routingOptions: { bridge: false, onRamp: false, swap: true },
101
96
  fundingRouteFullAmount: true,
102
- onComplete: isBaseCurrency(currency.name)
103
- ? handleOnComplete
104
- : undefined,
105
97
  onFundingRoute: isBaseCurrency(currency.name)
106
98
  ? handleOnFundingRoute
107
99
  : undefined,
@@ -25,6 +25,7 @@ export const useFundingBalances = () => {
25
25
  >([]);
26
26
  const [loadingBalances, setLoadingBalances] = useState(false);
27
27
  const [gasFees, setGasFees] = useState<TokenBalance | undefined>();
28
+ const [fundingBalancesError, setFundingBalancesError] = useState<Error | null>(null);
28
29
 
29
30
  const queryFundingBalances = () => {
30
31
  if (
@@ -40,6 +41,7 @@ export const useFundingBalances = () => {
40
41
  (async () => {
41
42
  fetching.current = true;
42
43
  setLoadingBalances(true);
44
+ setFundingBalancesError(null);
43
45
  try {
44
46
  const results = await fetchFundingBalances({
45
47
  provider,
@@ -55,9 +57,6 @@ export const useFundingBalances = () => {
55
57
  onFundingBalance: (foundBalances) => {
56
58
  setFundingBalances([...foundBalances]);
57
59
  },
58
- onComplete: () => {
59
- setLoadingBalances(false);
60
- },
61
60
  onFundingRequirement: (requirement) => {
62
61
  setTransactionRequirement(requirement);
63
62
  },
@@ -67,9 +66,10 @@ export const useFundingBalances = () => {
67
66
  });
68
67
 
69
68
  setFundingBalancesResult(results);
70
- } catch {
71
- setLoadingBalances(false);
69
+ } catch (err) {
70
+ setFundingBalancesError(err instanceof Error ? err : new Error(String(err)));
72
71
  } finally {
72
+ setLoadingBalances(false);
73
73
  fetching.current = false;
74
74
  }
75
75
  })();
@@ -79,6 +79,7 @@ export const useFundingBalances = () => {
79
79
  fundingBalances,
80
80
  loadingBalances,
81
81
  fundingBalancesResult,
82
+ fundingBalancesError,
82
83
  transactionRequirement,
83
84
  gasFees,
84
85
  queryFundingBalances,
@@ -26,9 +26,38 @@ export const defaultOrderQuote: OrderQuote = {
26
26
 
27
27
  export type ConfigError = {
28
28
  type: SaleErrorTypes;
29
- data?: Record<string, string>;
29
+ data?: Record<string, unknown>;
30
30
  };
31
31
 
32
+ /**
33
+ * Validates the order quote response before use. Ensures:
34
+ * - Currencies are non-empty (empty usually indicates wrong project config or endpoint).
35
+ * - Products are non-empty.
36
+ * - Every sale item has a matching product in the quote (no missing productIds).
37
+ */
38
+ function validateOrderQuote(
39
+ config: OrderQuote,
40
+ items: SaleItem[],
41
+ ): { valid: true } | { valid: false; reason: string } {
42
+ if (!config.currencies?.length) {
43
+ return { valid: false, reason: 'Quote returned no currencies' };
44
+ }
45
+
46
+ const productIds = Object.keys(config.products || {});
47
+ if (productIds.length === 0) {
48
+ return { valid: false, reason: 'Quote returned no products' };
49
+ }
50
+
51
+ const missing = items.filter((item) => !config.products![item.productId]);
52
+ if (missing.length > 0) {
53
+ return {
54
+ valid: false,
55
+ reason: `Quote missing products for: ${missing.map((m) => m.productId).join(', ')}`,
56
+ };
57
+ }
58
+ return { valid: true };
59
+ }
60
+
32
61
  export const useQuoteOrder = ({
33
62
  items,
34
63
  environment,
@@ -53,6 +82,13 @@ export const useQuoteOrder = ({
53
82
  });
54
83
  };
55
84
 
85
+ const setQuoteValidationError = (reason: string) => {
86
+ setOrderQuoteError({
87
+ type: SaleErrorTypes.SERVICE_BREAKDOWN,
88
+ data: { reason: 'Invalid order quote response', error: reason },
89
+ });
90
+ };
91
+
56
92
  useEffect(() => {
57
93
  // Set request params
58
94
  if (!items?.length || !provider) return;
@@ -93,6 +129,16 @@ export const useQuoteOrder = ({
93
129
  });
94
130
 
95
131
  if (!response.ok) {
132
+ if (response.status === 400) {
133
+ const { code, message } = await response.json();
134
+ setOrderQuoteError({
135
+ type: SaleErrorTypes.SALE_AUTHORIZATION_REJECTED,
136
+ data: {
137
+ vendorError: { code: code || '', message: message || undefined },
138
+ },
139
+ });
140
+ return;
141
+ }
96
142
  throw new Error(`${response.status} - ${response.statusText}`);
97
143
  }
98
144
 
@@ -100,6 +146,13 @@ export const useQuoteOrder = ({
100
146
  await response.json(),
101
147
  preferredCurrency,
102
148
  );
149
+
150
+ const validation = validateOrderQuote(config, items);
151
+ if (!validation.valid) {
152
+ setQuoteValidationError(validation.reason);
153
+ return;
154
+ }
155
+
103
156
  setOrderQuote(config);
104
157
  } catch (error) {
105
158
  setError(errorToString(error));
@@ -107,7 +160,7 @@ export const useQuoteOrder = ({
107
160
  fetching.current = false;
108
161
  }
109
162
  })();
110
- }, [environment, environmentId, queryParams]);
163
+ }, [environment, environmentId, queryParams, items]);
111
164
 
112
165
  useEffect(() => {
113
166
  // Set default currency
@@ -291,11 +291,17 @@ export const useSignOrder = (input: SignOrderInput) => {
291
291
 
292
292
  const { ok, status } = response;
293
293
  if (!ok) {
294
- const { code } = (await response.json()) as SignApiError;
294
+ const { code, message } = (await response.json()) as SignApiError;
295
295
  let errorType: SaleErrorTypes;
296
+ let errorData: { code: string; message: string } | undefined;
297
+
296
298
  switch (status) {
297
299
  case 400:
298
- errorType = SaleErrorTypes.SERVICE_BREAKDOWN;
300
+ errorType = SaleErrorTypes.SALE_AUTHORIZATION_REJECTED;
301
+ errorData = {
302
+ code,
303
+ message,
304
+ };
299
305
  break;
300
306
  case 404:
301
307
  if (code === 'insufficient_stock') {
@@ -312,7 +318,7 @@ export const useSignOrder = (input: SignOrderInput) => {
312
318
  throw new Error('Unknown error');
313
319
  }
314
320
 
315
- setSignError({ type: errorType });
321
+ setSignError({ type: errorType, data: errorData });
316
322
  return undefined;
317
323
  }
318
324
 
@@ -59,7 +59,9 @@ export type SignOrderInput = {
59
59
 
60
60
  export type SignOrderError = {
61
61
  type: SaleErrorTypes;
62
- data?: Record<string, string>;
62
+ data?:
63
+ | Record<string, string>
64
+ | { vendorError: { code: string; message: string } };
63
65
  };
64
66
 
65
67
  export type ExecutedTransaction = {
@@ -85,6 +87,7 @@ export enum SaleErrorTypes {
85
87
  WALLET_REJECTED_NO_FUNDS = 'WALLET_REJECTED_NO_FUNDS',
86
88
  WALLET_POPUP_BLOCKED = 'WALLET_POPUP_BLOCKED',
87
89
  FUNDING_ROUTE_EXECUTE_ERROR = 'FUNDING_ROUTE_EXECUTE_ERROR',
90
+ SALE_AUTHORIZATION_REJECTED = 'SALE_AUTHORIZATION_REJECTED',
88
91
  }
89
92
 
90
93
  export type OrderQuoteCurrency = {
@@ -185,11 +185,21 @@ export function OrderSummary({ subView }: OrderSummaryProps) {
185
185
  fundingBalances,
186
186
  loadingBalances,
187
187
  fundingBalancesResult,
188
+ fundingBalancesError,
188
189
  transactionRequirement,
189
190
  gasFees,
190
191
  queryFundingBalances,
191
192
  } = useFundingBalances();
192
193
 
194
+ // If funding balances failed to load, transition to error view
195
+ useEffect(() => {
196
+ if (!fundingBalancesError) return;
197
+ closeHandover();
198
+ goToErrorView(SaleErrorTypes.SERVICE_BREAKDOWN, {
199
+ error: errorToString(fundingBalancesError),
200
+ });
201
+ }, [fundingBalancesError, goToErrorView, closeHandover]);
202
+
193
203
  // Initialise funding balances
194
204
  useEffect(() => {
195
205
  if (subView !== OrderSummarySubViews.INIT || !fromTokenAddress) return;
@@ -36,6 +36,7 @@ type SaleErrorViewProps = {
36
36
  errorType: SaleErrorTypes | undefined;
37
37
  transactionHash?: string;
38
38
  blockExplorerLink?: string;
39
+ vendorMessage?: string;
39
40
  };
40
41
 
41
42
  export function SaleErrorView({
@@ -43,6 +44,7 @@ export function SaleErrorView({
43
44
  transactionHash,
44
45
  blockExplorerLink,
45
46
  errorType,
47
+ vendorMessage,
46
48
  }: SaleErrorViewProps) {
47
49
  const { t } = useTranslation();
48
50
  const {
@@ -198,6 +200,13 @@ export function SaleErrorView({
198
200
  onSecondaryActionClick: closeWidget,
199
201
  statusType: StatusType.INFORMATION,
200
202
  },
203
+ [SaleErrorTypes.SALE_AUTHORIZATION_REJECTED]: {
204
+ onSecondaryActionClick: closeWidget,
205
+ statusType: StatusType.INFORMATION,
206
+ statusIconStyles: {
207
+ fill: biomeTheme.color.status.fatal.dim,
208
+ },
209
+ },
201
210
  [SaleErrorTypes.INVALID_PARAMETERS]: {
202
211
  onSecondaryActionClick: closeWidget,
203
212
  statusType: StatusType.ALERT,
@@ -222,11 +231,13 @@ export function SaleErrorView({
222
231
  ? t(`views.SALE_FAIL.errors.${currentErrorType}.secondaryAction`)
223
232
  : t(`views.SALE_FAIL.errors.${SaleErrorTypes.DEFAULT}.secondaryAction`);
224
233
 
234
+ const useVendorMessage = currentErrorType === SaleErrorTypes.SALE_AUTHORIZATION_REJECTED
235
+ && vendorMessage;
236
+
225
237
  return {
226
238
  headingText: t('views.PAYMENT_METHODS.handover.error.heading'),
227
- subheadingText: t(
228
- `views.SALE_FAIL.errors.${currentErrorType}.description`,
229
- ),
239
+ subheadingText: useVendorMessage
240
+ || t(`views.SALE_FAIL.errors.${currentErrorType}.description`),
230
241
  primaryButtonText: t(
231
242
  `views.SALE_FAIL.errors.${currentErrorType}.primaryAction`,
232
243
  ),