@imtbl/checkout-widgets 2.1.7 → 2.1.8-alpha.1

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 (61) hide show
  1. package/.eslintrc.cjs +1 -0
  2. package/dist/browser/{AddTokensWidget-BrzH8SRQ.js → AddTokensWidget-BgxHdzNs.js} +9 -9
  3. package/dist/browser/{BridgeWidget-NQ3CYgVo.js → BridgeWidget-BfbLqaO5.js} +11 -11
  4. package/dist/browser/CommerceWidget-p5IkeBXI.js +1090 -0
  5. package/dist/browser/{FeesBreakdown-k5cJIAW0.js → FeesBreakdown-BD01pqY5.js} +1 -1
  6. package/dist/browser/{OnRampWidget-n5iMTapr.js → OnRampWidget-BjqBmlbY.js} +3 -3
  7. package/dist/browser/{SaleWidget-DxUyJHON.js → SaleWidget-gaWZAAC6.js} +13 -13
  8. package/dist/browser/{SpendingCapHero-PAyXt-5z.js → SpendingCapHero-CXVXHWU7.js} +1 -1
  9. package/dist/browser/{SwapWidget-hFFcGluR.js → SwapWidget-uiItZGHN.js} +16 -13
  10. package/dist/browser/{TokenImage-DgghMSHe.js → TokenImage-DWdvPYOy.js} +1 -1
  11. package/dist/browser/{TopUpView-BacX_7nQ.js → TopUpView-Dxos0g-A.js} +1 -1
  12. package/dist/browser/{WalletApproveHero-C-T5NtEx.js → WalletApproveHero-D3-G7a0z.js} +4 -4
  13. package/dist/browser/{WalletWidget-DecM6mQw.js → WalletWidget-Be_FeuLE.js} +3 -3
  14. package/dist/browser/{auto-track-DxotwgaF.js → auto-track-CLBbzSZu.js} +1 -1
  15. package/dist/browser/{index-D-BPFjGK.js → index-BJ7h0TZi.js} +1 -1
  16. package/dist/browser/{index-BXjP25qz.js → index-BetzT5p3.js} +1 -1
  17. package/dist/browser/{index-DLx3MMbe.js → index-BvRUC_LF.js} +1 -1
  18. package/dist/browser/{index-Cs2j3TUw.js → index-C-mNrq1P.js} +244 -186
  19. package/dist/browser/{index-DidpXH8g.js → index-C6YmDAJM.js} +1 -1
  20. package/dist/browser/{index-DU1AneOh.js → index-C82zcUlQ.js} +1 -1
  21. package/dist/browser/{index-DNzOH_wV.js → index-DJgjFIWN.js} +2 -2
  22. package/dist/browser/{index-BV23I7EL.js → index-ll7Y5dyS.js} +1 -1
  23. package/dist/browser/index.js +1 -1
  24. package/dist/browser/{index.umd-CgfCangZ.js → index.umd-k71QYNi5.js} +1 -1
  25. package/dist/browser/{useInterval-DdQHGT9W.js → useInterval-CDxgcGMG.js} +1 -1
  26. package/dist/types/context/analytics-provider/SegmentAnalyticsProvider.d.ts +2 -1
  27. package/dist/types/context/view-context/CheckoutWidgetViewContextTypes.d.ts +9 -2
  28. package/dist/types/context/view-context/TransferViewContextTypes.d.ts +9 -0
  29. package/dist/types/context/view-context/ViewContext.d.ts +2 -1
  30. package/dist/types/widgets/immutable-commerce/CommerceWidgetRoot.d.ts +9 -1
  31. package/dist/types/widgets/swap/context/SwapContext.test.d.ts +0 -5
  32. package/dist/types/widgets/transfer/AwaitingApproval.d.ts +4 -0
  33. package/dist/types/widgets/transfer/SendingTokens.d.ts +4 -0
  34. package/dist/types/widgets/transfer/TransferComplete.d.ts +7 -0
  35. package/dist/types/widgets/transfer/TransferForm.d.ts +10 -0
  36. package/dist/types/widgets/transfer/TransferWidget.d.ts +6 -0
  37. package/dist/types/widgets/transfer/context.d.ts +35 -0
  38. package/dist/types/widgets/transfer/events.d.ts +3 -0
  39. package/dist/types/widgets/transfer/functions.d.ts +7 -0
  40. package/package.json +7 -7
  41. package/src/components/ConnectLoader/ConnectLoader.tsx +1 -1
  42. package/src/context/analytics-provider/SegmentAnalyticsProvider.ts +1 -0
  43. package/src/context/view-context/CheckoutWidgetViewContextTypes.ts +12 -1
  44. package/src/context/view-context/TransferViewContextTypes.ts +11 -0
  45. package/src/context/view-context/ViewContext.tsx +3 -1
  46. package/src/locales/en.json +23 -0
  47. package/src/widgets/immutable-commerce/CommerceWidget.tsx +9 -0
  48. package/src/widgets/immutable-commerce/CommerceWidgetRoot.tsx +28 -0
  49. package/src/widgets/immutable-commerce/functions/getConnectLoaderParams.ts +1 -0
  50. package/src/widgets/immutable-commerce/functions/getFlowRequiresContext.ts +1 -0
  51. package/src/widgets/immutable-commerce/functions/isValidCommerceFlow.ts +1 -0
  52. package/src/widgets/swap/context/SwapContext.test.ts +0 -8
  53. package/src/widgets/transfer/AwaitingApproval.tsx +49 -0
  54. package/src/widgets/transfer/SendingTokens.tsx +44 -0
  55. package/src/widgets/transfer/TransferComplete.tsx +73 -0
  56. package/src/widgets/transfer/TransferForm.tsx +236 -0
  57. package/src/widgets/transfer/TransferWidget.tsx +264 -0
  58. package/src/widgets/transfer/context.ts +53 -0
  59. package/src/widgets/transfer/events.ts +59 -0
  60. package/src/widgets/transfer/functions.ts +84 -0
  61. package/dist/browser/CommerceWidget-CXciNRjF.js +0 -714
@@ -0,0 +1,73 @@
1
+ import {
2
+ Stack, Box, Heading, Link, Button,
3
+ } from '@biom3/react';
4
+ import { BlockExplorerService } from '@imtbl/checkout-sdk';
5
+ import { useRive, useStateMachineInput } from '@rive-app/react-canvas-lite';
6
+ import { useEffect, useMemo } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { SimpleLayout } from '../../components/SimpleLayout/SimpleLayout';
9
+ import { getRemoteRive } from '../../lib/utils';
10
+ import { StrongCheckoutWidgetsConfig } from '../../lib/withDefaultWidgetConfig';
11
+ import { TransferCompleteState } from './context';
12
+
13
+ export function TransferComplete({
14
+ config,
15
+ viewState,
16
+ onContinue,
17
+ }: {
18
+ config: StrongCheckoutWidgetsConfig;
19
+ viewState: TransferCompleteState;
20
+ onContinue: () => void;
21
+ }) {
22
+ const { t } = useTranslation();
23
+
24
+ const { RiveComponent, rive } = useRive({
25
+ src: getRemoteRive(config.environment, '/purchasing_items.riv'),
26
+ stateMachines: 'State',
27
+ autoplay: true,
28
+ });
29
+
30
+ const input = useStateMachineInput(rive, 'State', 'mode');
31
+
32
+ useEffect(() => {
33
+ if (!input) return;
34
+ input.value = 3;
35
+ }, [input]);
36
+
37
+ const explorerLink = useMemo(
38
+ () =>
39
+ BlockExplorerService.getTransactionLink(
40
+ viewState.chainId,
41
+ viewState.receipt.hash,
42
+ ),
43
+ [viewState.chainId, viewState.receipt.hash],
44
+ );
45
+
46
+ return (
47
+ <SimpleLayout containerSx={{ bg: 'transparent' }}>
48
+ <Stack
49
+ justifyContent="space-between"
50
+ sx={{
51
+ height: '100%',
52
+ mb: 'base.spacing.x10',
53
+ textAlign: 'center',
54
+ }}
55
+ >
56
+ <Box>
57
+ <Box sx={{ height: '240px' }} rc={<RiveComponent />} />
58
+ <Heading sx={{ my: 'base.spacing.x4', mx: 'base.spacing.x4' }}>
59
+ {t('views.TRANSFER.content.tokensSentSuccessfully')}
60
+ </Heading>
61
+ <Link rc={<a target="_blank" href={explorerLink} rel="noreferrer" />}>
62
+ {t('views.TRANSFER.content.seeTransactionOnImmutableZkEVM')}
63
+ </Link>
64
+ </Box>
65
+ <Box sx={{ mx: 'base.spacing.x4' }}>
66
+ <Button sx={{ width: '100%' }} onClick={onContinue} size="large">
67
+ {t('views.TRANSFER.form.continueButtonText')}
68
+ </Button>
69
+ </Box>
70
+ </Stack>
71
+ </SimpleLayout>
72
+ );
73
+ }
@@ -0,0 +1,236 @@
1
+ import {
2
+ OptionKey, Stack, Heading, Box, Body, Button,
3
+ } from '@biom3/react';
4
+ import { IMTBLWidgetEvents } from '@imtbl/checkout-sdk';
5
+ import {
6
+ Dispatch, SetStateAction, useContext, useMemo, useCallback,
7
+ } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { CoinSelectorOptionProps } from '../../components/CoinSelector/CoinSelectorOption';
10
+ import { SelectInput } from '../../components/FormComponents/SelectInput/SelectInput';
11
+ import { TextInputForm } from '../../components/FormComponents/TextInputForm/TextInputForm';
12
+ import { HeaderNavigation } from '../../components/Header/HeaderNavigation';
13
+ import { SimpleLayout } from '../../components/SimpleLayout/SimpleLayout';
14
+ import { useAnalytics, UserJourney } from '../../context/analytics-provider/SegmentAnalyticsProvider';
15
+ import { CryptoFiatContext } from '../../context/crypto-fiat-context/CryptoFiatContext';
16
+ import { EventTargetContext } from '../../context/event-target-context/EventTargetContext';
17
+ import { orchestrationEvents } from '../../lib/orchestrationEvents';
18
+ import {
19
+ tokenValueFormat, getDefaultTokenImage, calculateCryptoToFiat, formatZeroAmount,
20
+ } from '../../lib/utils';
21
+ import { amountInputValidation } from '../../lib/validations/amountInputValidations';
22
+ import { StrongCheckoutWidgetsConfig } from '../../lib/withDefaultWidgetConfig';
23
+ import { TransferFormState, TransferState } from './context';
24
+ import { getOptionKey, getFiatAmount, validatePartialAddress } from './functions';
25
+ import { sendCloseWidgetEvent } from '../connect/connectWidgetEvents';
26
+
27
+ export function TransferForm({
28
+ config,
29
+ viewState,
30
+ setViewState,
31
+ onSend,
32
+ showBackButton,
33
+ }: {
34
+ config: StrongCheckoutWidgetsConfig;
35
+ viewState: TransferFormState;
36
+ setViewState: Dispatch<SetStateAction<TransferState>>;
37
+ onSend: () => void;
38
+ showBackButton: boolean | undefined;
39
+ }) {
40
+ const { t } = useTranslation();
41
+ const { track } = useAnalytics();
42
+ const { eventTargetState: { eventTarget } } = useContext(EventTargetContext);
43
+ const { cryptoFiatState } = useContext(CryptoFiatContext);
44
+
45
+ const tokenOptions = useMemo(
46
+ () =>
47
+ viewState.allowedBalances.map<CoinSelectorOptionProps>(
48
+ (tokenBalance) => ({
49
+ id: getOptionKey(tokenBalance.token),
50
+ name: tokenBalance.token.name,
51
+ symbol: tokenBalance.token.symbol,
52
+ defaultTokenImage: tokenBalance.token.icon || 'TODO',
53
+ balance: {
54
+ formattedAmount: tokenValueFormat(tokenBalance.formattedBalance),
55
+ formattedFiatAmount: getFiatAmount(cryptoFiatState, tokenBalance),
56
+ },
57
+ }),
58
+ ),
59
+ [viewState.allowedBalances, cryptoFiatState],
60
+ );
61
+
62
+ const token = useMemo(
63
+ () => tokenOptions.find((option) => option.id === viewState.tokenAddress),
64
+ [tokenOptions, viewState.tokenAddress],
65
+ );
66
+
67
+ const defaultTokenImage = useMemo(
68
+ () =>
69
+ getDefaultTokenImage(viewState.checkout.config.environment, config.theme),
70
+ [viewState.checkout.config.environment, config.theme],
71
+ );
72
+
73
+ const fromFiatValue = useMemo(
74
+ () =>
75
+ calculateCryptoToFiat(
76
+ viewState.amount,
77
+ token?.symbol || '',
78
+ cryptoFiatState.conversions,
79
+ ),
80
+ [viewState.amount, token, cryptoFiatState.conversions],
81
+ );
82
+
83
+ const handleTokenChange = useCallback(
84
+ (optionKey: OptionKey) => {
85
+ track({
86
+ screen: 'TransferToken',
87
+ userJourney: UserJourney.TRANSFER,
88
+ control: 'SelectToken',
89
+ controlType: 'Select',
90
+ extras: { token: optionKey },
91
+ });
92
+ if (typeof optionKey !== 'string') throw new Error('Invalid token address');
93
+ setViewState((s) => ({ ...s, tokenAddress: optionKey, amountError: '' }));
94
+ },
95
+ [tokenOptions, token],
96
+ );
97
+
98
+ const handleMaxButtonClick = useCallback(() => {
99
+ if (!token) throw new Error('Token not found');
100
+ if (!token.balance) throw new Error('Token balance not found');
101
+
102
+ track({
103
+ screen: 'TransferToken',
104
+ userJourney: UserJourney.TRANSFER,
105
+ control: 'Max',
106
+ controlType: 'Button',
107
+ extras: { token: token.id, amount: token.balance.formattedAmount },
108
+ });
109
+
110
+ setViewState((s) => {
111
+ if (!token.balance) throw new Error('Token balance not found');
112
+ return { ...s, amount: token.balance.formattedAmount };
113
+ });
114
+ }, [tokenOptions, token]);
115
+
116
+ const handleRecipientAddressChange = useCallback((value: string) => {
117
+ setViewState((s) => ({ ...s, toAddress: value, toAddressError: '' }));
118
+ }, []);
119
+
120
+ const handleAmountChange = useCallback((value: string) => {
121
+ setViewState((s) => ({ ...s, amount: value, amountError: '' }));
122
+ }, []);
123
+
124
+ const selectSubtext = useMemo(() => {
125
+ if (!token) return '';
126
+ return `${t('views.TRANSFER.content.availableBalancePrefix')} ${
127
+ token.balance?.formattedAmount
128
+ }`;
129
+ }, [token]);
130
+
131
+ const isButtonDisabled = useMemo(
132
+ () => !viewState.amount || !viewState.toAddress || !token,
133
+ [viewState.amount, viewState.toAddress, token],
134
+ );
135
+
136
+ return (
137
+ <SimpleLayout
138
+ header={(
139
+ <HeaderNavigation
140
+ title={t('views.TRANSFER.header.title')}
141
+ onCloseButtonClick={() => sendCloseWidgetEvent(eventTarget)}
142
+ showBack={showBackButton}
143
+ onBackButtonClick={() => {
144
+ orchestrationEvents.sendRequestGoBackEvent(
145
+ eventTarget,
146
+ IMTBLWidgetEvents.IMTBL_TRANSFER_WIDGET_EVENT,
147
+ {},
148
+ );
149
+ }}
150
+ />
151
+ )}
152
+ >
153
+ <Stack
154
+ justifyContent="space-between"
155
+ sx={{
156
+ height: '100%',
157
+ mt: 'base.spacing.x6',
158
+ mx: 'base.spacing.x4',
159
+ mb: 'base.spacing.x10',
160
+ }}
161
+ >
162
+ <Stack gap="base.spacing.x9">
163
+ <Stack gap="base.spacing.x1">
164
+ <Heading size="xSmall">
165
+ {t('views.TRANSFER.form.coinAmountHeading')}
166
+ </Heading>
167
+ <SelectInput
168
+ testId="transfer-token-select"
169
+ options={tokenOptions}
170
+ textInputValue={viewState.amount}
171
+ textInputPlaceholder="0"
172
+ textInputTextAlign="right"
173
+ coinSelectorHeading={t('views.TRANSFER.form.coinSelectorHeading')}
174
+ textInputMaxButtonClick={token ? handleMaxButtonClick : undefined}
175
+ textInputValidator={amountInputValidation}
176
+ textInputErrorMessage={viewState.amountError}
177
+ selectSubtext={selectSubtext}
178
+ textInputSubtext={`${t(
179
+ 'views.TRANSFER.content.fiatPricePrefix',
180
+ )} $${formatZeroAmount(fromFiatValue, true)}`}
181
+ onTextInputChange={handleAmountChange}
182
+ onSelectChange={handleTokenChange}
183
+ selectedOption={token?.id}
184
+ defaultTokenImage={defaultTokenImage}
185
+ userJourney={UserJourney.TRANSFER}
186
+ screen="TransferToken"
187
+ control="Token"
188
+ />
189
+ </Stack>
190
+ <Stack gap="base.spacing.x1">
191
+ <Heading size="xSmall">
192
+ {t('views.TRANSFER.form.toAddressHeading')}
193
+ </Heading>
194
+ <TextInputForm
195
+ testId="transfer-to-address-input"
196
+ value={viewState.toAddress}
197
+ placeholder="0x"
198
+ validator={validatePartialAddress}
199
+ onTextInputChange={handleRecipientAddressChange}
200
+ errorMessage={viewState.toAddressError}
201
+ />
202
+ </Stack>
203
+ </Stack>
204
+ <Box>
205
+ <Body
206
+ rc={<div />}
207
+ size="xSmall"
208
+ weight="bold"
209
+ sx={{ color: 'base.color.text.status.fatal.primary' }}
210
+ >
211
+ {t('views.TRANSFER.content.notAllExchangesSupportImmutableZkEVM')}
212
+ </Body>
213
+ <Body
214
+ rc={<div />}
215
+ size="xxSmall"
216
+ weight="regular"
217
+ sx={{ mb: 'base.spacing.x4' }}
218
+ >
219
+ {t(
220
+ 'views.TRANSFER.content.notAllExchangesSupportImmutableZkEVMDescription',
221
+ )}
222
+ </Body>
223
+ <Button
224
+ sx={{ width: '100%' }}
225
+ variant="primary"
226
+ size="large"
227
+ disabled={isButtonDisabled}
228
+ onClick={onSend}
229
+ >
230
+ {t('views.TRANSFER.form.buttonText')}
231
+ </Button>
232
+ </Box>
233
+ </Stack>
234
+ </SimpleLayout>
235
+ );
236
+ }
@@ -0,0 +1,264 @@
1
+ /* eslint-disable react/destructuring-assignment */
2
+ /* eslint-disable max-len */
3
+ import { TransferWidgetParams } from '@imtbl/checkout-sdk';
4
+ import {
5
+ useCallback, useContext, useEffect, useState,
6
+ } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { CloudImage, Stack, useTheme } from '@biom3/react';
9
+
10
+ import { isAddress, isError, parseUnits } from 'ethers';
11
+ import { StrongCheckoutWidgetsConfig } from '../../lib/withDefaultWidgetConfig';
12
+ import { LoadingView } from '../../views/loading/LoadingView';
13
+ import {
14
+ useAnalytics,
15
+ UserJourney,
16
+ } from '../../context/analytics-provider/SegmentAnalyticsProvider';
17
+ import { ConnectLoaderContext } from '../../context/connect-loader-context/ConnectLoaderContext';
18
+ import { getRemoteImage } from '../../lib/utils';
19
+ import {
20
+ CryptoFiatActions,
21
+ CryptoFiatContext,
22
+ } from '../../context/crypto-fiat-context/CryptoFiatContext';
23
+ import { CryptoFiatProvider } from '../../context/crypto-fiat-context/CryptoFiatProvider';
24
+ import { sendTokens, loadBalances } from './functions';
25
+ import { getL2ChainId } from '../../lib';
26
+ import { TransferComplete } from './TransferComplete';
27
+ import { SendingTokens } from './SendingTokens';
28
+ import { AwaitingApproval } from './AwaitingApproval';
29
+ import { TransferState } from './context';
30
+ import { TransferForm } from './TransferForm';
31
+ import { sendFailedEvent, sendRejectedEvent, sendSuccessEvent } from './events';
32
+ import { EventTargetContext } from '../../context/event-target-context/EventTargetContext';
33
+
34
+ export type TransferWidgetInputs = TransferWidgetParams & {
35
+ config: StrongCheckoutWidgetsConfig;
36
+ };
37
+
38
+ const TRANSACTION_CANCELLED_ERROR_CODE = -32003;
39
+
40
+ function TransferWidgetInner(props: TransferWidgetInputs) {
41
+ const { t } = useTranslation();
42
+ const { cryptoFiatDispatch } = useContext(CryptoFiatContext);
43
+ const { eventTargetState: { eventTarget } } = useContext(EventTargetContext);
44
+ const { track } = useAnalytics();
45
+
46
+ const {
47
+ connectLoaderState: { checkout, provider },
48
+ } = useContext(ConnectLoaderContext);
49
+
50
+ const [viewState, setViewState] = useState<TransferState>({
51
+ type: 'INITIALISING',
52
+ });
53
+
54
+ useEffect(() => {
55
+ const x = async () => {
56
+ if (viewState.type !== 'INITIALISING') return;
57
+ if (!checkout || !provider) return;
58
+
59
+ const tokensAndBalances = await loadBalances(checkout, provider);
60
+
61
+ cryptoFiatDispatch({
62
+ payload: {
63
+ type: CryptoFiatActions.SET_TOKEN_SYMBOLS,
64
+ tokenSymbols: tokensAndBalances.allowedBalances.map(
65
+ (tb) => tb.token.symbol,
66
+ ),
67
+ },
68
+ });
69
+
70
+ setViewState({
71
+ type: 'FORM',
72
+ allowedBalances: tokensAndBalances.allowedBalances,
73
+ checkout,
74
+ provider,
75
+ amount: props.amount || '',
76
+ amountError: '',
77
+ tokenAddress: props.tokenAddress || '',
78
+ toAddress: props.toAddress || '',
79
+ toAddressError: '',
80
+ });
81
+ };
82
+
83
+ x();
84
+ }, [checkout]);
85
+
86
+ const resetForm = useCallback(() => {
87
+ if (viewState.type === 'INITIALISING') return;
88
+
89
+ setViewState({
90
+ type: 'FORM',
91
+ allowedBalances: viewState.allowedBalances,
92
+ checkout: viewState.checkout,
93
+ provider: viewState.provider,
94
+ amount: '',
95
+ amountError: '',
96
+ tokenAddress: '',
97
+ toAddress: '',
98
+ toAddressError: '',
99
+ });
100
+ }, [checkout, provider, viewState]);
101
+
102
+ const onSend = useCallback(async () => {
103
+ if (viewState.type !== 'FORM') throw new Error('Unexpected state');
104
+
105
+ track({
106
+ screen: 'TransferToken',
107
+ userJourney: UserJourney.TRANSFER,
108
+ control: 'Send',
109
+ controlType: 'Button',
110
+ extras: { token: viewState.tokenAddress, amount: viewState.amount },
111
+ });
112
+
113
+ if (!isAddress(viewState.toAddress)) {
114
+ setViewState((s) => ({ ...s, toAddressError: 'Invalid wallet address' }));
115
+ return;
116
+ }
117
+
118
+ const tokenInfo = viewState.allowedBalances.find(
119
+ (tb) => tb.token.address === viewState.tokenAddress,
120
+ );
121
+ if (!tokenInfo) throw new Error('Token not found');
122
+
123
+ if (
124
+ tokenInfo.balance < parseUnits(viewState.amount, tokenInfo.token.decimals)
125
+ ) {
126
+ setViewState((s) => ({ ...s, amountError: 'Insufficient balance' }));
127
+ return;
128
+ }
129
+
130
+ setViewState({
131
+ type: 'AWAITING_APPROVAL',
132
+ checkout: viewState.checkout,
133
+ provider: viewState.provider,
134
+ allowedBalances: viewState.allowedBalances,
135
+ });
136
+
137
+ try {
138
+ const txResponse = await sendTokens(
139
+ viewState.provider,
140
+ tokenInfo,
141
+ viewState.toAddress,
142
+ viewState.amount,
143
+ );
144
+
145
+ setViewState({
146
+ type: 'TRANSFERRING',
147
+ checkout: viewState.checkout,
148
+ provider: viewState.provider,
149
+ allowedBalances: viewState.allowedBalances,
150
+ });
151
+
152
+ const receipt = await txResponse.wait();
153
+ if (!receipt) {
154
+ sendFailedEvent(eventTarget, 'Transaction failed');
155
+ setViewState({ ...viewState, type: 'FORM' }); // TODO: We should be showing a failed view here
156
+ return;
157
+ }
158
+
159
+ sendSuccessEvent(eventTarget, receipt.hash);
160
+
161
+ setViewState({
162
+ type: 'COMPLETE',
163
+ receipt,
164
+ chainId: getL2ChainId(viewState.checkout.config),
165
+ checkout: viewState.checkout,
166
+ provider: viewState.provider,
167
+ allowedBalances: viewState.allowedBalances,
168
+ });
169
+ } catch (e) {
170
+ setViewState({
171
+ type: 'FORM',
172
+ allowedBalances: viewState.allowedBalances,
173
+ checkout: viewState.checkout,
174
+ provider: viewState.provider,
175
+ amount: viewState.amount,
176
+ amountError: viewState.amountError,
177
+ tokenAddress: viewState.tokenAddress,
178
+ toAddress: viewState.toAddress,
179
+ toAddressError: viewState.toAddressError,
180
+ });
181
+ if (
182
+ isError(e, 'UNKNOWN_ERROR')
183
+ && e.error
184
+ && 'code' in e.error
185
+ && e.error.code === TRANSACTION_CANCELLED_ERROR_CODE
186
+ ) {
187
+ track({
188
+ screen: 'TransferToken',
189
+ userJourney: UserJourney.TRANSFER,
190
+ control: 'TranactionCancel',
191
+ controlType: 'Event',
192
+ extras: { token: viewState.tokenAddress, amount: viewState.amount },
193
+ });
194
+ sendRejectedEvent(eventTarget, 'Transaction cancelled');
195
+ } else {
196
+ // eslint-disable-next-line no-console
197
+ console.error(e); // TODO: where can we send these?
198
+ sendFailedEvent(eventTarget, 'Transaction failed');
199
+ setViewState({ ...viewState, type: 'FORM' }); // TODO: We should be showing a failed view here
200
+ }
201
+ }
202
+ }, [viewState, eventTarget]);
203
+
204
+ switch (viewState.type) {
205
+ case 'INITIALISING':
206
+ return <LoadingView loadingText={t('views.LOADING_VIEW.text')} />;
207
+ case 'FORM':
208
+ return (
209
+ <TransferForm
210
+ config={props.config}
211
+ viewState={viewState}
212
+ setViewState={setViewState}
213
+ onSend={onSend}
214
+ showBackButton={props.showBackButton}
215
+ />
216
+ );
217
+ case 'AWAITING_APPROVAL':
218
+ return <AwaitingApproval config={props.config} />;
219
+ case 'TRANSFERRING':
220
+ return <SendingTokens config={props.config} />;
221
+ case 'COMPLETE':
222
+ return (
223
+ <TransferComplete
224
+ config={props.config}
225
+ viewState={viewState}
226
+ onContinue={resetForm}
227
+ />
228
+ );
229
+ default:
230
+ throw new Error('Invalid view state');
231
+ }
232
+ }
233
+
234
+ export default function TransferWidget(props: TransferWidgetInputs) {
235
+ const {
236
+ base: { colorMode },
237
+ } = useTheme();
238
+
239
+ return (
240
+ <CryptoFiatProvider environment={props.config.environment}>
241
+ <Stack sx={{ pos: 'relative' }}>
242
+ <CloudImage
243
+ use={(
244
+ <img
245
+ src={getRemoteImage(
246
+ props.config.environment,
247
+ `/add-tokens-bg-texture-${colorMode}.webp`,
248
+ )}
249
+ alt="blurry bg texture"
250
+ />
251
+ )}
252
+ sx={{
253
+ pos: 'absolute',
254
+ h: '100%',
255
+ w: '100%',
256
+ objectFit: 'cover',
257
+ objectPosition: 'center',
258
+ }}
259
+ />
260
+ <TransferWidgetInner {...props} />
261
+ </Stack>
262
+ </CryptoFiatProvider>
263
+ );
264
+ }
@@ -0,0 +1,53 @@
1
+ import { Checkout, WrappedBrowserProvider, GetBalanceResult } from '@imtbl/checkout-sdk';
2
+ import { TransactionReceipt } from 'ethers';
3
+ import { createContext, useContext } from 'react';
4
+
5
+ export type TransferFormState = {
6
+ type: 'FORM';
7
+ allowedBalances: GetBalanceResult[];
8
+ checkout: Checkout;
9
+ provider: WrappedBrowserProvider;
10
+ amount: string;
11
+ amountError: string;
12
+ tokenAddress: string;
13
+ toAddress: string;
14
+ toAddressError: string;
15
+ };
16
+
17
+ export type TransferCompleteState = {
18
+ type: 'COMPLETE';
19
+ receipt: TransactionReceipt;
20
+ chainId: number;
21
+ checkout: Checkout;
22
+ provider: WrappedBrowserProvider;
23
+ allowedBalances: GetBalanceResult[];
24
+ };
25
+
26
+ export type TransferState =
27
+ | { type: 'INITIALISING' }
28
+ | TransferFormState
29
+ | {
30
+ type: 'AWAITING_APPROVAL';
31
+ checkout: Checkout;
32
+ provider: WrappedBrowserProvider;
33
+ allowedBalances: GetBalanceResult[];
34
+ }
35
+ | {
36
+ type: 'TRANSFERRING';
37
+ checkout: Checkout;
38
+ provider: WrappedBrowserProvider;
39
+ allowedBalances: GetBalanceResult[];
40
+ }
41
+ | TransferCompleteState;
42
+
43
+ const transferContext = createContext<TransferState | null>(null);
44
+
45
+ export const useTransferContext = () => {
46
+ const context = useContext(transferContext);
47
+ if (!context) {
48
+ throw new Error(
49
+ 'useTransferContext must be used within a TransferContextProvider',
50
+ );
51
+ }
52
+ return context;
53
+ };
@@ -0,0 +1,59 @@
1
+ import {
2
+ WidgetEvent,
3
+ TransferEventType,
4
+ IMTBLWidgetEvents,
5
+ WidgetType,
6
+ } from '@imtbl/checkout-sdk';
7
+
8
+ export const sendSuccessEvent = (eventTarget: Window | EventTarget, transactionHash: string) => {
9
+ const successEvent = new CustomEvent<WidgetEvent<WidgetType.TRANSFER, TransferEventType.SUCCESS>>(
10
+ IMTBLWidgetEvents.IMTBL_TRANSFER_WIDGET_EVENT,
11
+ {
12
+ detail: {
13
+ type: TransferEventType.SUCCESS,
14
+ data: {
15
+ transactionHash,
16
+ },
17
+ },
18
+ },
19
+ );
20
+ // eslint-disable-next-line no-console
21
+ console.log('transfer success event:', eventTarget, successEvent);
22
+ if (eventTarget !== undefined) eventTarget.dispatchEvent(successEvent);
23
+ };
24
+
25
+ export const sendFailedEvent = (eventTarget: Window | EventTarget, reason: string) => {
26
+ const failedEvent = new CustomEvent<WidgetEvent<WidgetType.TRANSFER, TransferEventType.FAILURE>>(
27
+ IMTBLWidgetEvents.IMTBL_TRANSFER_WIDGET_EVENT,
28
+ {
29
+ detail: {
30
+ type: TransferEventType.FAILURE,
31
+ data: {
32
+ reason,
33
+ timestamp: new Date().getTime(),
34
+ },
35
+ },
36
+ },
37
+ );
38
+ // eslint-disable-next-line no-console
39
+ console.log('transfer failed event:', eventTarget, failedEvent);
40
+ if (eventTarget !== undefined) eventTarget.dispatchEvent(failedEvent);
41
+ };
42
+
43
+ export const sendRejectedEvent = (eventTarget: Window | EventTarget, reason: string) => {
44
+ const rejectedEvent = new CustomEvent<WidgetEvent<WidgetType.TRANSFER, TransferEventType.REJECTED>>(
45
+ IMTBLWidgetEvents.IMTBL_TRANSFER_WIDGET_EVENT,
46
+ {
47
+ detail: {
48
+ type: TransferEventType.REJECTED,
49
+ data: {
50
+ reason,
51
+ timestamp: new Date().getTime(),
52
+ },
53
+ },
54
+ },
55
+ );
56
+ // eslint-disable-next-line no-console
57
+ console.log('transfer rejected event:', eventTarget, rejectedEvent);
58
+ if (eventTarget !== undefined) eventTarget.dispatchEvent(rejectedEvent);
59
+ };