diff --git a/packages/checkout/widgets-lib/src/resources/text/textConfig.ts b/packages/checkout/widgets-lib/src/resources/text/textConfig.ts
index 95d9436e10..bc0dedeac3 100644
--- a/packages/checkout/widgets-lib/src/resources/text/textConfig.ts
+++ b/packages/checkout/widgets-lib/src/resources/text/textConfig.ts
@@ -460,6 +460,24 @@ export const text = {
text: 'Next',
},
},
+ [XBridgeWidgetViews.BRIDGE_REVIEW]: {
+ layoutHeading: 'Move',
+ heading: 'Ok, how does this look?',
+ fromLabel: {
+ amountHeading: 'Moving',
+ heading: 'From',
+ },
+ toLabel: {
+ heading: 'To',
+ },
+ fees: {
+ heading: 'Gas fee',
+ },
+ footer: {
+ buttonText: 'Confirm move',
+ },
+ fiatPricePrefix: '~ USD $',
+ },
},
footers: {
quickswapFooter: {
diff --git a/packages/checkout/widgets-lib/src/widgets/x-bridge/XBridgeWidget.cy.tsx b/packages/checkout/widgets-lib/src/widgets/x-bridge/XBridgeWidget.cy.tsx
index bfa1bb9492..eca9ffba68 100644
--- a/packages/checkout/widgets-lib/src/widgets/x-bridge/XBridgeWidget.cy.tsx
+++ b/packages/checkout/widgets-lib/src/widgets/x-bridge/XBridgeWidget.cy.tsx
@@ -7,6 +7,7 @@ import {
import { Environment } from '@imtbl/config';
import { StrongCheckoutWidgetsConfig } from 'lib/withDefaultWidgetConfig';
import { Passport } from '@imtbl/passport';
+import { BigNumber } from 'ethers';
import { XBridgeWidget } from './XBridgeWidget';
import { text } from '../../resources/text/textConfig';
@@ -19,6 +20,8 @@ describe('XBridgeWidget', () => {
let checkIsWalletConnectedStub;
let connectStub;
let switchNetworkStub;
+ let getNetworkInfoStub;
+ let getAllBalancesStub;
beforeEach(() => {
cy.viewport('ipad-2');
@@ -28,11 +31,15 @@ describe('XBridgeWidget', () => {
checkIsWalletConnectedStub = cy.stub().as('checkIsWalletConnectedStub');
connectStub = cy.stub().as('connectStub');
switchNetworkStub = cy.stub().as('switchNetworkStub');
+ getNetworkInfoStub = cy.stub().as('getNetworkInfoStub');
+ getAllBalancesStub = cy.stub().as('getAllBalancesStub');
Checkout.prototype.createProvider = createProviderStub;
Checkout.prototype.checkIsWalletConnected = checkIsWalletConnectedStub;
Checkout.prototype.connect = connectStub;
Checkout.prototype.switchNetwork = switchNetworkStub;
+ Checkout.prototype.getNetworkInfo = getNetworkInfoStub;
+ Checkout.prototype.getAllBalances = getAllBalancesStub;
getNetworkSepoliaStub = cy.stub().as('getNetworkSepoliaStub').resolves({ chainId: ChainId.SEPOLIA });
@@ -55,7 +62,7 @@ describe('XBridgeWidget', () => {
},
getNetwork: getNetworkImmutableZkEVMStub,
getSigner: () => ({
- getAddress: () => Promise.resolve('0x1234567890123456789012345678901234567890'),
+ getAddress: () => Promise.resolve('0x0987654321098765432109876543210987654321'),
}),
};
});
@@ -493,4 +500,62 @@ describe('XBridgeWidget', () => {
cySmartGet('bridge-form').should('be.visible');
});
});
+
+ describe('Happy path', () => {
+ it('should complete the full move flow', () => {
+ createProviderStub
+ .onFirstCall()
+ .returns({ provider: mockPassportProvider })
+ .onSecondCall()
+ .returns({ provider: mockMetaMaskProvider });
+ checkIsWalletConnectedStub.resolves({ isConnected: false });
+ connectStub
+ .onFirstCall()
+ .returns({ provider: mockPassportProvider })
+ .onSecondCall()
+ .returns({ provider: mockMetaMaskProvider });
+ getNetworkInfoStub.resolves({ chainId: ChainId.IMTBL_ZKEVM_TESTNET });
+ getAllBalancesStub.resolves({
+ balances: [
+ {
+ balance: BigNumber.from('1000000000000000000'),
+ formattedBalance: '1.0',
+ token: {
+ name: 'IMX',
+ symbol: 'IMX',
+ decimals: 18,
+ },
+ },
+ ],
+ });
+
+ switchNetworkStub.resolves({
+ provider: mockMetaMaskProvider,
+ network: { chainId: ChainId.IMTBL_ZKEVM_TESTNET },
+ } as SwitchNetworkResult);
+
+ mount();
+
+ // Wallet & Network Selector
+ cySmartGet('wallet-network-selector-from-wallet-select__target').click();
+ cySmartGet('wallet-network-selector-from-wallet-list-passport').click();
+ cySmartGet('wallet-network-selector-to-wallet-select__target').click();
+ cySmartGet('wallet-network-selector-to-wallet-list-metamask').click();
+ cySmartGet('wallet-network-selector-submit-button').click();
+
+ // Bridge form
+ cySmartGet('bridge-token-select__target').click();
+ cySmartGet('bridge-token-coin-selector__option-imx').click();
+ cySmartGet('bridge-amount-text__input').type('1');
+ cySmartGet('bridge-form-button').click();
+
+ // Review screen
+ cySmartGet('bridge-review-summary-from-amount__priceDisplay__price').should('have.text', 'IMX 1');
+ cySmartGet('bridge-review-summary-from-amount__priceDisplay__fiatAmount').should('have.text', '~ USD $1.50');
+ cySmartGet('bridge-review-summary-gas-amount__priceDisplay__price').should('have.text', 'ETH 0.007984');
+ cySmartGet('bridge-review-summary-gas-amount__priceDisplay__fiatAmount').should('have.text', '~ USD $15.00');
+ cySmartGet('bridge-review-summary-from-address__label').should('include.text', '0x0987...4321');
+ cySmartGet('bridge-review-summary-to-address__label').should('include.text', '0x1234...7890');
+ });
+ });
});
diff --git a/packages/checkout/widgets-lib/src/widgets/x-bridge/XBridgeWidget.tsx b/packages/checkout/widgets-lib/src/widgets/x-bridge/XBridgeWidget.tsx
index 89d5d51201..c4c4696572 100644
--- a/packages/checkout/widgets-lib/src/widgets/x-bridge/XBridgeWidget.tsx
+++ b/packages/checkout/widgets-lib/src/widgets/x-bridge/XBridgeWidget.tsx
@@ -23,6 +23,7 @@ import {
import { widgetTheme } from '../../lib/theme';
import { WalletNetworkSelectionView } from './views/WalletNetworkSelectionView';
import { Bridge } from './views/Bridge';
+import { BridgeReview } from './views/BridgeReview';
export type BridgeWidgetInputs = BridgeWidgetParams & {
config: StrongCheckoutWidgetsConfig,
@@ -69,6 +70,9 @@ export function XBridgeWidget({
{viewState.view.type === XBridgeWidgetViews.BRIDGE_FORM && (
)}
+ {viewState.view.type === XBridgeWidgetViews.BRIDGE_REVIEW && (
+
+ )}
diff --git a/packages/checkout/widgets-lib/src/widgets/x-bridge/components/BridgeForm.tsx b/packages/checkout/widgets-lib/src/widgets/x-bridge/components/BridgeForm.tsx
index 5742db0571..3c04706660 100644
--- a/packages/checkout/widgets-lib/src/widgets/x-bridge/components/BridgeForm.tsx
+++ b/packages/checkout/widgets-lib/src/widgets/x-bridge/components/BridgeForm.tsx
@@ -57,6 +57,8 @@ export function BridgeForm(props: BridgeFormProps) {
allowedTokens,
checkout,
web3Provider,
+ amount,
+ token,
},
} = useContext(XBridgeContext);
@@ -76,16 +78,16 @@ export function BridgeForm(props: BridgeFormProps) {
} = text.views[BridgeWidgetViews.BRIDGE];
// Form state
- const [amount, setAmount] = useState(defaultAmount || '');
+ const [formAmount, setFormAmount] = useState(defaultAmount || '');
const [amountError, setAmountError] = useState('');
- const [token, setToken] = useState();
+ const [formToken, setFormToken] = useState();
const [tokenError, setTokenError] = useState('');
const [amountFiatValue, setAmountFiatValue] = useState('');
const [editing, setEditing] = useState(false);
const [loading, setLoading] = useState(false);
const hasSetDefaultState = useRef(false);
- const tokenBalanceSubtext = token
- ? `${content.availableBalancePrefix} ${tokenValueFormat(token?.formattedBalance)}`
+ const tokenBalanceSubtext = formToken
+ ? `${content.availableBalancePrefix} ${tokenValueFormat(formToken?.formattedBalance)}`
: '';
// Fee estimates & transactions
@@ -135,7 +137,7 @@ export function BridgeForm(props: BridgeFormProps) {
if (!hasSetDefaultState.current) {
hasSetDefaultState.current = true;
if (defaultFromContractAddress) {
- setToken(
+ setFormToken(
tokenBalances.find(
(b) => (isNativeToken(b.token.address) && defaultFromContractAddress?.toLocaleUpperCase() === NATIVE)
|| (b.token.address?.toLowerCase() === defaultFromContractAddress?.toLowerCase()),
@@ -148,23 +150,36 @@ export function BridgeForm(props: BridgeFormProps) {
cryptoFiatState.conversions,
defaultFromContractAddress,
hasSetDefaultState.current,
- setToken,
- setTokensOptions,
formatTokenOptionsId,
formatZeroAmount,
]);
+ useEffect(() => {
+ // This useEffect is for populating the form
+ // with values from context when the user
+ // has selected the back button from the review screen
+ if (!amount || !token) return;
+ setFormAmount(amount);
+ for (let i = 0; i < tokenBalances.length; i++) {
+ const balance = tokenBalances[i];
+ if (balance.token.address === token.address) {
+ setFormToken(balance);
+ break;
+ }
+ }
+ }, [amount, token, tokenBalances]);
+
const selectedOption = useMemo(
- () => (token && token.token
- ? formatTokenOptionsId(token.token.symbol, token.token.address)
+ () => (formToken && formToken.token
+ ? formatTokenOptionsId(formToken.token.symbol, formToken.token.address)
: undefined),
- [token, tokenBalances, cryptoFiatState.conversions, formatTokenOptionsId],
+ [formToken, tokenBalances, cryptoFiatState.conversions, formatTokenOptionsId],
);
const canFetchEstimates = (): boolean => {
- if (Number.isNaN(parseFloat(amount))) return false;
- if (parseFloat(amount) <= 0) return false;
- if (!token) return false;
+ if (Number.isNaN(parseFloat(formAmount))) return false;
+ if (parseFloat(formAmount) <= 0) return false;
+ if (!formToken) return false;
if (isFetching) return false;
return true;
};
@@ -230,14 +245,14 @@ export function BridgeForm(props: BridgeFormProps) {
return true;
}
- const tokenIsEth = isNativeToken(token?.token.address);
+ const tokenIsEth = isNativeToken(formToken?.token.address);
const gasAmount = utils.parseEther(gasFee.length !== 0 ? gasFee : '0');
- const additionalAmount = tokenIsEth && !Number.isNaN(parseFloat(amount))
- ? utils.parseEther(amount)
+ const additionalAmount = tokenIsEth && !Number.isNaN(parseFloat(formAmount))
+ ? utils.parseEther(formAmount)
: BigNumber.from('0');
return gasAmount.add(additionalAmount).gt(ethBalance.balance);
- }, [gasFee, tokenBalances, token, amount]);
+ }, [gasFee, tokenBalances, formToken, formAmount]);
// Silently refresh the quote
useInterval(() => fetchEstimates(true), DEFAULT_QUOTE_REFRESH_INTERVAL);
@@ -245,39 +260,39 @@ export function BridgeForm(props: BridgeFormProps) {
useEffect(() => {
if (editing) return;
(async () => await fetchEstimates())();
- }, [amount, token, editing]);
+ }, [formAmount, formToken, editing]);
const onTextInputFocus = () => {
setEditing(true);
};
const handleBridgeAmountChange = (value: string) => {
- setAmount(value);
+ setFormAmount(value);
if (amountError) {
- const validateAmountError = validateAmount(value, token?.formattedBalance);
+ const validateAmountError = validateAmount(value, formToken?.formattedBalance);
setAmountError(validateAmountError);
}
- if (!token) return;
+ if (!formToken) return;
setAmountFiatValue(calculateCryptoToFiat(
value,
- token.token.symbol,
+ formToken.token.symbol,
cryptoFiatState.conversions,
));
};
const handleAmountInputBlur = (value: string) => {
setEditing(false);
- setAmount(value);
+ setFormAmount(value);
if (amountError) {
- const validateAmountError = validateAmount(value, token?.formattedBalance);
+ const validateAmountError = validateAmount(value, formToken?.formattedBalance);
setAmountError(validateAmountError);
}
- if (!token) return;
+ if (!formToken) return;
setAmountFiatValue(calculateCryptoToFiat(
value,
- token.token.symbol,
+ formToken.token.symbol,
cryptoFiatState.conversions,
));
};
@@ -286,7 +301,7 @@ export function BridgeForm(props: BridgeFormProps) {
const selected = tokenBalances.find((t) => value === formatTokenOptionsId(t.token.symbol, t.token.address));
if (!selected) return;
- setToken(selected);
+ setFormToken(selected);
setTokenError('');
};
@@ -300,15 +315,15 @@ export function BridgeForm(props: BridgeFormProps) {
}, [cryptoFiatDispatch, allowedTokens]);
useEffect(() => {
- if (!amount) return;
- if (!token) return;
+ if (!formAmount) return;
+ if (!formToken) return;
setAmountFiatValue(calculateCryptoToFiat(
- amount,
- token.token.symbol,
+ formAmount,
+ formToken.token.symbol,
cryptoFiatState.conversions,
));
- }, [amount, token]);
+ }, [formAmount, formToken]);
useEffect(() => {
(async () => {
@@ -316,7 +331,7 @@ export function BridgeForm(props: BridgeFormProps) {
const address = await web3Provider.getSigner().getAddress();
setWalletAddress((previous) => {
if (previous !== '' && previous !== address) {
- setToken(undefined);
+ setFormToken(undefined);
}
return address;
});
@@ -324,17 +339,17 @@ export function BridgeForm(props: BridgeFormProps) {
}, [web3Provider, tokenBalances]);
const bridgeFormValidator = useCallback((): boolean => {
- const validateTokenError = validateToken(token);
- const validateAmountError = validateAmount(amount, token?.formattedBalance);
+ const validateTokenError = validateToken(formToken);
+ const validateAmountError = validateAmount(formAmount, formToken?.formattedBalance);
if (validateTokenError) setTokenError(validateTokenError);
if (validateAmountError) setAmountError(validateAmountError);
if (validateTokenError || validateAmountError) return false;
return true;
- }, [token, amount, setTokenError, setAmountError]);
+ }, [formToken, formAmount, setTokenError, setAmountError]);
const submitBridge = useCallback(async () => {
if (!bridgeFormValidator()) return;
- if (!checkout || !web3Provider || !token) return;
+ if (!checkout || !web3Provider || !formToken) return;
if (insufficientFundsForGas) {
setShowNotEnoughGasDrawer(true);
@@ -344,8 +359,8 @@ export function BridgeForm(props: BridgeFormProps) {
bridgeDispatch({
payload: {
type: BridgeActions.SET_TOKEN_AND_AMOUNT,
- token: token.token,
- amount,
+ token: formToken.token,
+ amount: formAmount,
},
});
@@ -362,7 +377,7 @@ export function BridgeForm(props: BridgeFormProps) {
web3Provider,
bridgeFormValidator,
insufficientFundsForGas,
- token]);
+ formToken]);
const retrySubmitBridge = async () => {
setShowTxnRejectedState(false);
@@ -401,7 +416,7 @@ export function BridgeForm(props: BridgeFormProps) {
/>
setShowNotEnoughGasDrawer(false)}
walletAddress={walletAddress}
- showAdjustAmount={isNativeToken(token?.token.address)}
+ showAdjustAmount={isNativeToken(formToken?.token.address)}
/>
diff --git a/packages/checkout/widgets-lib/src/widgets/x-bridge/components/BridgeReviewSummary.tsx b/packages/checkout/widgets-lib/src/widgets/x-bridge/components/BridgeReviewSummary.tsx
new file mode 100644
index 0000000000..a990964f9a
--- /dev/null
+++ b/packages/checkout/widgets-lib/src/widgets/x-bridge/components/BridgeReviewSummary.tsx
@@ -0,0 +1,210 @@
+import { text } from 'resources/text/textConfig';
+import { useContext, useMemo } from 'react';
+import { XBridgeWidgetViews } from 'context/view-context/XBridgeViewContextTypes';
+import {
+ Body,
+ Box, Heading, Icon, MenuItem,
+} from '@biom3/react';
+import { ChainId, WalletProviderName } from '@imtbl/checkout-sdk';
+import { abbreviateAddress } from 'lib/addressUtils';
+import { CryptoFiatContext } from 'context/crypto-fiat-context/CryptoFiatContext';
+import { isPassportProvider } from 'lib/providerUtils';
+import { calculateCryptoToFiat } from 'lib/utils';
+import { Web3Provider } from '@ethersproject/providers';
+import { DEFAULT_QUOTE_REFRESH_INTERVAL } from 'lib';
+import { useInterval } from 'lib/hooks/useInterval';
+import { networkIconStyles } from './WalletNetworkButtonStyles';
+import {
+ arrowIconStyles,
+ arrowIconWrapperStyles,
+ bottomMenuItemStyles,
+ bridgeReviewHeadingStyles,
+ bridgeReviewWrapperStyles,
+ gasAmountHeadingStyles,
+ topMenuItemStyles,
+ walletLogoStyles,
+} from './BridgeReviewSummaryStyles';
+import { XBridgeContext } from '../context/XBridgeContext';
+
+const networkIcon = {
+ [ChainId.IMTBL_ZKEVM_DEVNET]: 'Immutable',
+ [ChainId.IMTBL_ZKEVM_MAINNET]: 'Immutable',
+ [ChainId.IMTBL_ZKEVM_TESTNET]: 'Immutable',
+ [ChainId.ETHEREUM]: 'EthToken',
+ [ChainId.SEPOLIA]: 'EthToken',
+};
+
+const logo = {
+ [WalletProviderName.PASSPORT]: 'PassportSymbolOutlined',
+ [WalletProviderName.METAMASK]: 'MetaMaskSymbol',
+};
+
+const testId = 'bridge-review-summary';
+
+export function BridgeReviewSummary() {
+ const {
+ heading, fromLabel, toLabel, fees, fiatPricePrefix,
+ } = text.views[XBridgeWidgetViews.BRIDGE_REVIEW];
+
+ const {
+ bridgeState: {
+ from,
+ to,
+ token,
+ amount,
+ },
+ } = useContext(XBridgeContext);
+
+ const { cryptoFiatState } = useContext(CryptoFiatContext);
+
+ const walletProviderName = (provider: Web3Provider | undefined) => (isPassportProvider(provider)
+ ? WalletProviderName.PASSPORT
+ : WalletProviderName.METAMASK);
+
+ const fromAmount = useMemo(() => (token?.symbol ? `${token?.symbol} ${amount}` : `${amount}`), [token, amount]);
+ const fromFiatAmount = useMemo(() => {
+ if (!amount || !token) return '';
+ return calculateCryptoToFiat(amount, token.symbol, cryptoFiatState.conversions);
+ }, [token, amount]);
+ const fromAddress = useMemo(() => {
+ if (!from) return '-';
+ return from.walletAddress;
+ }, [from]);
+
+ const fromWalletProviderName = useMemo(() => walletProviderName(from?.web3Provider), [from]);
+ const fromNetwork = useMemo(() => from && from.network, [from]);
+
+ const toAddress = useMemo(() => {
+ if (!to) return '-';
+ return to.walletAddress;
+ }, [to]);
+ const toWalletProviderName = useMemo(() => walletProviderName(to?.web3Provider), [to]);
+ const toNetwork = useMemo(() => to?.network, [to]);
+
+ const fetchGasEstimate = () => {
+ // eslint-disable-next-line no-console
+ console.log('fetch gas estimate');
+ };
+ useInterval(() => fetchGasEstimate(), DEFAULT_QUOTE_REFRESH_INTERVAL);
+
+ // Fetch on useInterval interval when available
+ const gasEstimate = 'ETH 0.007984';
+ const gasFiatEstimate = '15.00';
+
+ return (
+
+
+ {heading}
+
+
+ {/* From review */}
+
+
+
+
+
+
+
+ {/* To review */}
+
+
+
+ );
+}
diff --git a/packages/checkout/widgets-lib/src/widgets/x-bridge/components/BridgeReviewSummaryStyles.ts b/packages/checkout/widgets-lib/src/widgets/x-bridge/components/BridgeReviewSummaryStyles.ts
new file mode 100644
index 0000000000..836f61e004
--- /dev/null
+++ b/packages/checkout/widgets-lib/src/widgets/x-bridge/components/BridgeReviewSummaryStyles.ts
@@ -0,0 +1,48 @@
+import { WalletProviderName } from '@imtbl/checkout-sdk';
+
+export const topMenuItemStyles = {
+ borderBottomLeftRadius: '0px',
+ borderBottomRightRadius: '0px',
+ marginBottom: '2px',
+};
+
+export const bottomMenuItemStyles = {
+ borderTopLeftRadius: '0px',
+ borderTopRightRadius: '0px',
+};
+
+export const bridgeReviewWrapperStyles = {
+ height: '100%',
+ display: 'flex',
+ flexDirection: 'column',
+ paddingX: 'base.spacing.x4',
+};
+
+export const bridgeReviewHeadingStyles = {
+ paddingTop: 'base.spacing.x10',
+ paddingBottom: 'base.spacing.x4',
+};
+
+export const arrowIconWrapperStyles = {
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ paddingY: 'base.spacing.x1',
+};
+
+export const arrowIconStyles = {
+ width: 'base.icon.size.300',
+ transform: 'rotate(270deg)',
+};
+
+export const walletLogoStyles = (walletName: WalletProviderName) => ({
+ minWidth: 'base.icon.size.400',
+ padding: walletName === WalletProviderName.PASSPORT ? 'base.spacing.x1' : '',
+ backgroundColor: 'base.color.translucent.standard.200',
+ borderRadius: 'base.borderRadius.x2',
+});
+
+export const gasAmountHeadingStyles = {
+ marginBottom: 'base.spacing.x4',
+ color: 'base.color.text.secondary',
+};
diff --git a/packages/checkout/widgets-lib/src/widgets/x-bridge/views/BridgeReview.tsx b/packages/checkout/widgets-lib/src/widgets/x-bridge/views/BridgeReview.tsx
new file mode 100644
index 0000000000..8b48269d7b
--- /dev/null
+++ b/packages/checkout/widgets-lib/src/widgets/x-bridge/views/BridgeReview.tsx
@@ -0,0 +1,51 @@
+import { HeaderNavigation } from 'components/Header/HeaderNavigation';
+import { SimpleLayout } from 'components/SimpleLayout/SimpleLayout';
+import { FooterLogo } from 'components/Footer/FooterLogo';
+import { useContext } from 'react';
+import { EventTargetContext } from 'context/event-target-context/EventTargetContext';
+import { text } from 'resources/text/textConfig';
+import { XBridgeWidgetViews } from 'context/view-context/XBridgeViewContextTypes';
+import {
+ Box, Button,
+} from '@biom3/react';
+import { sendBridgeWidgetCloseEvent } from '../BridgeWidgetEvents';
+
+import { BridgeReviewSummary } from '../components/BridgeReviewSummary';
+
+export function BridgeReview() {
+ const { eventTargetState: { eventTarget } } = useContext(EventTargetContext);
+
+ const { layoutHeading, footer } = text.views[XBridgeWidgetViews.BRIDGE_REVIEW];
+
+ return (
+ sendBridgeWidgetCloseEvent(eventTarget)}
+ />
+ )}
+ footer={(
+
+
+
+
+ )}
+ >
+
+
+ );
+}