Skip to content

Commit

Permalink
chore: display quote loading state
Browse files Browse the repository at this point in the history
  • Loading branch information
micaelae committed Oct 21, 2024
1 parent 75470e8 commit 55aef81
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 6 deletions.
9 changes: 9 additions & 0 deletions app/_locales/en/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ui/pages/bridge/__snapshots__/index.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ exports[`Bridge renders the component with initial props 1`] = `
data-theme="light"
disabled=""
>
Select token
Select token and amount
</button>
</div>
</div>
Expand Down
112 changes: 110 additions & 2 deletions ui/pages/bridge/prepare/bridge-cta-button.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { renderWithProvider } from '../../../../test/jest';
import configureStore from '../../../store/store';
import { createBridgeMockStore } from '../../../../test/jest/mock-store';
import { CHAIN_IDS } from '../../../../shared/constants/network';
import { mockBridgeQuotesNativeErc20 } from '../../../../test/data/bridge/mock-quotes-native-erc20';
// TODO: Remove restricted import
// eslint-disable-next-line import/no-restricted-paths
import { RequestStatus } from '../../../../app/scripts/controllers/bridge/constants';
import { BridgeCTAButton } from './bridge-cta-button';

describe('BridgeCTAButton', () => {
Expand All @@ -25,6 +29,52 @@ describe('BridgeCTAButton', () => {
expect(getByRole('button')).toBeDisabled();
});

it('should render the component when amount is missing', () => {
const mockStore = createBridgeMockStore(
{
srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM],
destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET],
},
{
fromTokenInputValue: null,
fromToken: 'ETH',
toToken: 'ETH',
toChainId: CHAIN_IDS.LINEA_MAINNET,
},
{},
);
const { getByText, getByRole } = renderWithProvider(
<BridgeCTAButton />,
configureStore(mockStore),
);

expect(getByText('Enter amount')).toBeInTheDocument();
expect(getByRole('button')).toBeDisabled();
});

it('should render the component when amount and dest token is missing', () => {
const mockStore = createBridgeMockStore(
{
srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM],
destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET],
},
{
fromTokenInputValue: null,
fromToken: 'ETH',
toToken: null,
toChainId: CHAIN_IDS.LINEA_MAINNET,
},
{},
);
const { getByText, getByRole } = renderWithProvider(
<BridgeCTAButton />,
configureStore(mockStore),
);

expect(getByText('Select token and amount')).toBeInTheDocument();
expect(getByRole('button')).toBeDisabled();
});

it('should render the component when tx is submittable', () => {
const mockStore = createBridgeMockStore(
{
Expand All @@ -37,14 +87,72 @@ describe('BridgeCTAButton', () => {
toToken: 'ETH',
toChainId: CHAIN_IDS.LINEA_MAINNET,
},
{},
{
quotes: mockBridgeQuotesNativeErc20,
quotesLastFetched: Date.now(),
quotesLoadingStatus: RequestStatus.FETCHED,
},
);
const { getByText, getByRole } = renderWithProvider(
<BridgeCTAButton />,
configureStore(mockStore),
);

expect(getByText('Confirm')).toBeInTheDocument();
expect(getByRole('button')).not.toBeDisabled();
});

it('should disable the component when quotes are loading and there are no existing quotes', () => {
const mockStore = createBridgeMockStore(
{
srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM],
destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET],
},
{
fromTokenInputValue: 1,
fromToken: 'ETH',
toToken: 'ETH',
toChainId: CHAIN_IDS.LINEA_MAINNET,
},
{
quotes: [],
quotesLastFetched: Date.now(),
quotesLoadingStatus: RequestStatus.LOADING,
},
);
const { getByText, getByRole } = renderWithProvider(
<BridgeCTAButton />,
configureStore(mockStore),
);

expect(getByText('Fetching quotes...')).toBeInTheDocument();
expect(getByRole('button')).toBeDisabled();
});

it('should enable the component when quotes are loading and there are existing quotes', () => {
const mockStore = createBridgeMockStore(
{
srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM],
destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET],
},
{
fromTokenInputValue: 1,
fromToken: 'ETH',
toToken: 'ETH',
toChainId: CHAIN_IDS.LINEA_MAINNET,
},
{
quotes: mockBridgeQuotesNativeErc20,
quotesLastFetched: Date.now(),
quotesLoadingStatus: RequestStatus.LOADING,
},
);
const { getByText, getByRole } = renderWithProvider(
<BridgeCTAButton />,
configureStore(mockStore),
);

expect(getByText('Bridge')).toBeInTheDocument();
expect(getByText('Confirm')).toBeInTheDocument();
expect(getByRole('button')).not.toBeDisabled();
});
});
18 changes: 16 additions & 2 deletions ui/pages/bridge/prepare/bridge-cta-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { Button } from '../../../components/component-library';
import {
getBridgeQuotes,
getFromAmount,
getFromChain,
getFromToken,
Expand All @@ -22,16 +23,29 @@ export const BridgeCTAButton = () => {
const fromAmount = useSelector(getFromAmount);
const toAmount = useSelector(getToAmount);

const { isLoading } = useSelector(getBridgeQuotes);

const isTxSubmittable =
fromToken && toToken && fromChain && toChain && fromAmount && toAmount;

const label = useMemo(() => {
if (isLoading && !isTxSubmittable) {
return t('swapFetchingQuotes');
}

if (!fromAmount) {
if (!toToken) {
return t('bridgeSelectTokenAndAmount');
}
return t('bridgeEnterAmount');
}

if (isTxSubmittable) {
return t('bridge');
return t('confirm');
}

return t('swapSelectToken');
}, [isTxSubmittable]);
}, [isLoading, fromAmount, toToken, isTxSubmittable]);

return (
<Button
Expand Down
14 changes: 13 additions & 1 deletion ui/pages/bridge/prepare/bridge-input-group.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { Hex } from '@metamask/utils';
import { useSelector } from 'react-redux';
import { SwapsTokenObject } from '../../../../shared/constants/swaps';
import {
Box,
Expand Down Expand Up @@ -27,6 +28,10 @@ import {
CHAIN_ID_TOKEN_IMAGE_MAP,
} from '../../../../shared/constants/network';
import useLatestBalance from '../../../hooks/bridge/useLatestBalance';
import {
getBridgeQuotes,
getRecommendedQuote,
} from '../../../ducks/bridge/selectors';

const generateAssetFromToken = (
chainId: Hex,
Expand Down Expand Up @@ -77,6 +82,9 @@ export const BridgeInputGroup = ({
>) => {
const t = useI18nContext();

const { isLoading } = useSelector(getBridgeQuotes);
const recommendedQuote = useSelector(getRecommendedQuote);

const tokenFiatValue = useTokenFiatAmount(
token?.address || undefined,
amountFieldProps?.value?.toString() || '0x0',
Expand Down Expand Up @@ -125,7 +133,11 @@ export const BridgeInputGroup = ({
<TextField
type={TextFieldType.Number}
className="amount-input"
placeholder="0"
placeholder={
isLoading && !recommendedQuote
? t('bridgeCalculatingAmount')
: '0'
}
onChange={(e) => {
onAmountChange?.(e.target.value);
}}
Expand Down

0 comments on commit 55aef81

Please sign in to comment.