Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions suite-native/intl/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2315,6 +2315,7 @@ export const messages = {
providerNamePlaceholder: 'Provider',
providerReceiveAddressLabel: "{providerName}'s receive address",
confirmationAlertTitle: 'Failed to confirm offer.',
approvalSuccessAlert: 'Spending approval confirmed.',
},
tradingExchangeApprovalScreen: {
title: 'Set {symbol} spending',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,45 @@
import { memo, useEffect } from 'react';
import { useSelector } from 'react-redux';

import { DexApprovalType, ExchangeTrade } from 'invity-api';

import {
TradingRootState,
cryptoIdToNetworkSymbolAndContractAddress,
selectTradingCoinSymbolByCryptoId,
selectTradingExchangeSelectedQuote,
selectTradingProviderByNameAndTradeType,
} from '@suite-common/trading';
import { isNetworkSymbol } from '@suite-common/wallet-config';
import { TokenSymbol } from '@suite-common/wallet-types';
import { BottomSheetModal, VStack, useBottomSheetModal } from '@suite-native/atoms';
import { CryptoAmountFormatter, TokenAmountFormatter } from '@suite-native/formatters';
import { Translation } from '@suite-native/intl';

import { ExchangeApprovalLimitCard } from './ExchangeApprovalLimitCard';

type ExchangeApprovalLimitSheetProps = {
export type ExchangeApprovalLimitSheetProps = {
isVisible: boolean;
onDismiss: () => void;
onApprovalTypeSelect: (type: DexApprovalType) => void;
selectedApprovalType: DexApprovalType;
quote: ExchangeTrade;
};

export const ExchangeApprovalLimitSheet = memo(
({ isVisible, onDismiss }: ExchangeApprovalLimitSheetProps) => {
const { bottomSheetRef, openModal } = useBottomSheetModal();
({
isVisible,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about moving useBottomSheetModal one level up instead of using isVisible? It feels redundant.. maybe we could change the logic directly in useBottomSheetControls What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then you could remove the the useEffect logic below as well

onDismiss,
onApprovalTypeSelect,
selectedApprovalType,
quote,
}: ExchangeApprovalLimitSheetProps) => {
const { bottomSheetRef, openModal, closeModal } = useBottomSheetModal();

useEffect(() => {
if (isVisible) {
openModal();
}
}, [isVisible, openModal]);
useEffect(
() => (isVisible ? openModal() : closeModal()),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This calls openModal when removing from parent, right? Do we want it? Why did the previous version not work?

[isVisible, openModal, closeModal],
);

const quote = useSelector(selectTradingExchangeSelectedQuote);
const providerInfo = useSelector((state: TradingRootState) =>
selectTradingProviderByNameAndTradeType(state, quote?.exchange, 'exchange'),
);
Expand All @@ -37,10 +48,6 @@ export const ExchangeApprovalLimitSheet = memo(
selectTradingCoinSymbolByCryptoId(state, quote?.send),
);

if (!quote) {
return null;
}

const { symbol, contractAddress } = quote.send
? cryptoIdToNetworkSymbolAndContractAddress(quote.send)
: {};
Expand All @@ -49,7 +56,22 @@ export const ExchangeApprovalLimitSheet = memo(
return null;
}

const limitAmount = `200.32 ${coinSymbol}`; //TODO
const limitAmount =
Copy link
Contributor

@vytick vytick Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
const limitAmount =
const limitAmountFormatter =

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would avoid unnecessary re-renders for this one. Could we use a useMemo or useCallback here, please?

!!coinSymbol &&
(isNetworkSymbol(coinSymbol) ? (
<CryptoAmountFormatter
value={quote.sendStringAmount ?? '0'}
symbol={coinSymbol}
isBalance={false}
variant="callout"
/>
) : (
<TokenAmountFormatter
value={quote.sendStringAmount ?? '0'}
tokenSymbol={coinSymbol as TokenSymbol}
variant="callout"
/>
));

return (
<BottomSheetModal
Expand All @@ -74,9 +96,9 @@ export const ExchangeApprovalLimitSheet = memo(
}
symbol={symbol}
contractAddress={contractAddress}
isChecked={true}
isChecked={selectedApprovalType === 'INFINITE'}
onChange={() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

Suggested change
onChange={() => {
onChange={() => onApprovalTypeSelect('INFINITE')}

// TODO
onApprovalTypeSelect('INFINITE');
}}
/>

Expand All @@ -90,9 +112,9 @@ export const ExchangeApprovalLimitSheet = memo(
}
symbol={symbol}
contractAddress={contractAddress}
isChecked={false}
isChecked={selectedApprovalType === 'MINIMAL'}
onChange={() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

Suggested change
onChange={() => {
onChange={() => onApprovalTypeSelect('MINIMAL')}

// TODO
onApprovalTypeSelect('MINIMAL');
}}
/>
</VStack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getInitializedTradingState } from '../../../../__fixtures__/tradingStat
import { ExchangeApprovalLimitSheet } from '../ExchangeApprovalLimitSheet';

const mockOnDismiss = jest.fn();
const mockOnApprovalTypeSelect = jest.fn();

const testQuote = exchangeQuotes[0];

Expand All @@ -20,40 +21,32 @@ const getPreloadedState = (): PreloadedState => ({
},
});

const getPreloadedStateWithoutQuote = (): PreloadedState => ({
wallet: {
trading: {
...getInitializedTradingState('exchange'),
exchange: {
...getInitializedTradingState('exchange').exchange,
selectedQuote: undefined,
},
},
},
});

const renderSheet = (isVisible = true, preloadedState = getPreloadedState()) =>
const renderSheet = (
isVisible = true,
quote = testQuote,
selectedApprovalType: 'INFINITE' | 'MINIMAL' = 'INFINITE',
) =>
renderWithStoreProviderAsync(
<ExchangeApprovalLimitSheet isVisible={isVisible} onDismiss={mockOnDismiss} />,
{ preloadedState },
<ExchangeApprovalLimitSheet
isVisible={isVisible}
onDismiss={mockOnDismiss}
quote={quote}
onApprovalTypeSelect={mockOnApprovalTypeSelect}
selectedApprovalType={selectedApprovalType}
/>,
{ preloadedState: getPreloadedState() },
);

describe('ExchangeApprovalLimitSheet', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should render nothing when quote is not available', async () => {
const { toJSON } = await renderSheet(true, getPreloadedStateWithoutQuote());

expect(toJSON()).toBeNull();
});

it('should render the sheet when visible', async () => {
const { getByText } = await renderSheet();

expect(getByText('Unlimited')).toBeTruthy();
expect(getByText('200.32 USDC')).toBeTruthy();
expect(getByText('100 USDC')).toBeTruthy();
});

it('should render unlimited approval option with correct details', async () => {
Expand All @@ -70,7 +63,7 @@ describe('ExchangeApprovalLimitSheet', () => {
it('should render limited approval option with correct amount', async () => {
const { getByText } = await renderSheet();

expect(getByText('200.32 USDC')).toBeTruthy();
expect(getByText('100 USDC')).toBeTruthy();
expect(
getByText(
"Approve only the amount needed for this swap. You'll need to approve again (and pay a fee) for future swaps, but this reduces risk by keeping you in full control of your USDC.",
Expand All @@ -94,4 +87,26 @@ describe('ExchangeApprovalLimitSheet', () => {
),
).toBeTruthy();
});

it('should display quote sendStringAmount in limited approval option', async () => {
const customQuote = {
...testQuote,
sendStringAmount: '250',
};
const { getByText } = await renderSheet(true, customQuote);

expect(getByText('250 USDC')).toBeTruthy();
});

it('should pass correct props when INFINITE is selected', async () => {
await renderSheet(true, testQuote, 'INFINITE');

expect(mockOnApprovalTypeSelect).toBeDefined();
});

it('should pass correct props when MINIMAL is selected', async () => {
await renderSheet(true, testQuote, 'MINIMAL');

expect(mockOnApprovalTypeSelect).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { memo } from 'react';
import { ScrollView } from 'react-native-gesture-handler';
import Animated from 'react-native-reanimated';

import { ExchangeTrade } from 'invity-api';

import { InlineAlertBox, VStack } from '@suite-native/atoms';
import { InlineAlertBox } from '@suite-native/atoms';

import { ExchangeFeePickerCard } from './ExchangeFeePickerCard';
import { ExchangeFromAccountTradePreviewCard } from './ExchangeFromAccountTradePreviewCard';
Expand All @@ -21,20 +20,15 @@ export const ExchangePreviewView = memo(({ quote, txnErrorString }: ExchangePrev
const isTxnError = !!txnErrorString;

return (
<ScrollView>
<VStack spacing="sp20" paddingVertical="sp20">
{isTxnError && (
<Animated.View>
<InlineAlertBox variant="critical" title={txnErrorString} />
</Animated.View>
)}
<ExchangeFromAccountTradePreviewCard
quote={quote}
fromStringValue={fromStringValue}
/>
<ExchangeToAccountTradePreviewCard quote={quote} toStringValue={toStringValue} />
<ExchangeFeePickerCard quote={quote} isTxnError={isTxnError} />
</VStack>
</ScrollView>
<>
{isTxnError && (
<Animated.View>
<InlineAlertBox variant="critical" title={txnErrorString} />
</Animated.View>
)}
<ExchangeFromAccountTradePreviewCard quote={quote} fromStringValue={fromStringValue} />
<ExchangeToAccountTradePreviewCard quote={quote} toStringValue={toStringValue} />
<ExchangeFeePickerCard quote={quote} isTxnError={isTxnError} />
</>
);
});
Loading
Loading