Skip to content

Commit 00a89a7

Browse files
Shane Austriesaustrie-consensys
authored andcommitted
fix(ramp): disable Get Quotes button for out-of-bounds amounts and update limit wording
Disable the Get Quotes button when the entered amount is below minimum, above maximum, exceeds balance, or exceeds balance minus gas. Update limit error messages from "Minimum/Maximum deposit is" to "Minimum/Maximum purchase is" to match Figma designs. Surface provider-specific error messages on the Quotes screen when all providers return errors instead of showing only the generic fallback text. Fix pre-existing test failure by adding a mock for @metamask/react-native-button (incompatible with RN 0.76).
1 parent 09f76fa commit 00a89a7

File tree

9 files changed

+199
-41
lines changed

9 files changed

+199
-41
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from 'react';
2+
import { TouchableOpacity, Text } from 'react-native';
3+
4+
const Button = ({ children, onPress, disabled, style, ...props }) => (
5+
<TouchableOpacity onPress={onPress} disabled={disabled} {...props}>
6+
{typeof children === 'string' ? (
7+
<Text style={style}>{children}</Text>
8+
) : (
9+
children
10+
)}
11+
</TouchableOpacity>
12+
);
13+
14+
export default Button;

app/components/UI/Ramp/Aggregator/Views/BuildQuote/BuildQuote.test.tsx

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,11 @@ const mockUseLimitsInitialValues: Partial<ReturnType<typeof useLimits>> = {
205205
isAmountAboveMaximum: jest
206206
.fn()
207207
.mockImplementation((amount) => amount > MAX_LIMIT),
208-
isAmountValid: jest.fn(),
208+
isAmountValid: jest
209+
.fn()
210+
.mockImplementation(
211+
(amount) => amount >= MIN_LIMIT && amount <= MAX_LIMIT,
212+
),
209213
};
210214

211215
let mockUseLimitsValues = {
@@ -735,7 +739,7 @@ describe('BuildQuote View', () => {
735739
fireEvent.press(getByRoleButton(`${denomSymbol}${initialAmount}`));
736740
fireEvent.press(getByRoleButton(invalidMaxAmount));
737741
expect(
738-
screen.getByText(`Maximum deposit is ${denomSymbol}${MAX_LIMIT}`),
742+
screen.getByText(`Maximum purchase is ${denomSymbol}${MAX_LIMIT}`),
739743
).toBeTruthy();
740744
});
741745

@@ -748,7 +752,7 @@ describe('BuildQuote View', () => {
748752
fireEvent.press(getByRoleButton(`${denomSymbol}${initialAmount}`));
749753
fireEvent.press(getByRoleButton(invalidMinAmount));
750754
expect(
751-
screen.getByText(`Minimum deposit is ${denomSymbol}${MIN_LIMIT}`),
755+
screen.getByText(`Minimum purchase is ${denomSymbol}${MIN_LIMIT}`),
752756
).toBeTruthy();
753757
});
754758

@@ -986,6 +990,72 @@ describe('BuildQuote View', () => {
986990
});
987991
});
988992

993+
//
994+
// SUBMIT BUTTON DISABLED STATE TESTS
995+
//
996+
describe('Get Quotes button disabled states', () => {
997+
it('is disabled when amount is below minimum limit', () => {
998+
render(BuildQuote);
999+
const submitBtn = getByRoleButton('Get quotes');
1000+
const initialAmount = '0';
1001+
const invalidMinAmount = (MIN_LIMIT - 1).toString();
1002+
const denomSymbol =
1003+
mockUseFiatCurrenciesValues.currentFiatCurrency?.denomSymbol;
1004+
fireEvent.press(getByRoleButton(`${denomSymbol}${initialAmount}`));
1005+
fireEvent.press(getByRoleButton(invalidMinAmount));
1006+
fireEvent.press(getByRoleButton('Done'));
1007+
expect(submitBtn.props.disabled).toBe(true);
1008+
});
1009+
1010+
it('is disabled when amount is above maximum limit', () => {
1011+
render(BuildQuote);
1012+
const submitBtn = getByRoleButton('Get quotes');
1013+
const initialAmount = '0';
1014+
const invalidMaxAmount = (MAX_LIMIT + 1).toString();
1015+
const denomSymbol =
1016+
mockUseFiatCurrenciesValues.currentFiatCurrency?.denomSymbol;
1017+
fireEvent.press(getByRoleButton(`${denomSymbol}${initialAmount}`));
1018+
fireEvent.press(getByRoleButton(invalidMaxAmount));
1019+
fireEvent.press(getByRoleButton('Done'));
1020+
expect(submitBtn.props.disabled).toBe(true);
1021+
});
1022+
1023+
it('is disabled when user has insufficient balance (sell)', () => {
1024+
mockUseRampSDKValues.isBuy = false;
1025+
mockUseRampSDKValues.isSell = true;
1026+
mockUseLimitsValues.limits = {
1027+
...mockUseLimitsValues.limits,
1028+
maxAmount: 10,
1029+
} as Limits;
1030+
mockUseBalanceValues.balanceBN = toTokenMinimalUnit(
1031+
'5',
1032+
mockUseRampSDKValues.selectedAsset?.decimals || 18,
1033+
) as BN4;
1034+
render(BuildQuote);
1035+
const submitBtn = getByRoleButton('Get quotes');
1036+
const initialAmount = '0';
1037+
const overBalanceAmount = '6';
1038+
const symbol = mockUseRampSDKValues.selectedAsset?.symbol;
1039+
fireEvent.press(getByRoleButton(`${initialAmount} ${symbol}`));
1040+
fireEvent.press(getByRoleButton(overBalanceAmount));
1041+
fireEvent.press(getByRoleButton('Done'));
1042+
expect(submitBtn.props.disabled).toBe(true);
1043+
});
1044+
1045+
it('is enabled when amount is valid and within limits', () => {
1046+
render(BuildQuote);
1047+
const submitBtn = getByRoleButton('Get quotes');
1048+
const initialAmount = '0';
1049+
const validAmount = VALID_AMOUNT.toString();
1050+
const denomSymbol =
1051+
mockUseFiatCurrenciesValues.currentFiatCurrency?.denomSymbol;
1052+
fireEvent.press(getByRoleButton(`${denomSymbol}${initialAmount}`));
1053+
fireEvent.press(getByRoleButton(validAmount));
1054+
fireEvent.press(getByRoleButton('Done'));
1055+
expect(submitBtn.props.disabled).toBe(false);
1056+
});
1057+
});
1058+
9891059
//
9901060
// SUBMIT BUTTON TEST
9911061
//

app/components/UI/Ramp/Aggregator/Views/BuildQuote/BuildQuote.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1112,7 +1112,13 @@ const BuildQuote = () => {
11121112
label={strings('fiat_on_ramp_aggregator.get_quotes')}
11131113
variant={ButtonVariants.Primary}
11141114
width={ButtonWidthTypes.Full}
1115-
isDisabled={amountNumber <= 0 || isFetching}
1115+
isDisabled={
1116+
amountNumber <= 0 ||
1117+
isFetching ||
1118+
!amountIsValid ||
1119+
amountIsOverGas ||
1120+
hasInsufficientBalance
1121+
}
11161122
accessibilityRole="button"
11171123
/>
11181124
</Row>

app/components/UI/Ramp/Aggregator/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,7 +1195,7 @@ exports[`BuildQuote View Balance display displays balance from useBalance for no
11951195
}
11961196
testID="min-limit-error"
11971197
>
1198-
Minimum deposit is
1198+
Minimum purchase is
11991199

12001200
$
12011201
2
@@ -5453,7 +5453,7 @@ exports[`BuildQuote View Crypto Currency Data renders the loading page when cryp
54535453
}
54545454
testID="min-limit-error"
54555455
>
5456-
Minimum deposit is
5456+
Minimum purchase is
54575457

54585458
$
54595459
2
@@ -8392,7 +8392,7 @@ exports[`BuildQuote View Fiat Currency Data renders the loading page when fiats
83928392
}
83938393
testID="min-limit-error"
83948394
>
8395-
Minimum deposit is
8395+
Minimum purchase is
83968396

83978397
$
83988398
2
@@ -11455,7 +11455,7 @@ exports[`BuildQuote View Payment Method Data renders no icons if there are no pa
1145511455
}
1145611456
testID="min-limit-error"
1145711457
>
11458-
Minimum deposit is
11458+
Minimum purchase is
1145911459

1146011460
$
1146111461
2
@@ -13866,7 +13866,7 @@ exports[`BuildQuote View Payment Method Data renders the loading page when payme
1386613866
}
1386713867
testID="min-limit-error"
1386813868
>
13869-
Minimum deposit is
13869+
Minimum purchase is
1387013870

1387113871
$
1387213872
2
@@ -16781,7 +16781,7 @@ exports[`BuildQuote View Regions data renders the loading page when regions are
1678116781
}
1678216782
testID="min-limit-error"
1678316783
>
16784-
Minimum deposit is
16784+
Minimum purchase is
1678516785

1678616786
$
1678716787
2
@@ -19185,7 +19185,7 @@ exports[`BuildQuote View renders correctly 1`] = `
1918519185
}
1918619186
testID="min-limit-error"
1918719187
>
19188-
Minimum deposit is
19188+
Minimum purchase is
1918919189

1919019190
$
1919119191
2

app/components/UI/Ramp/Aggregator/Views/Quotes/Quotes.test.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,66 @@ describe('Quotes', () => {
300300
});
301301
});
302302

303+
it('shows provider error message when all quotes have errors', async () => {
304+
const providerErrorMessage = 'Minimum amount is $50 for this provider';
305+
mockUseQuotesAndCustomActionsValues = {
306+
...mockUseQuotesAndCustomActionsInitialValues,
307+
customActions: [],
308+
quotesWithoutError: [],
309+
quotesByPriceWithoutError: [],
310+
quotesByReliabilityWithoutError: [],
311+
recommendedQuote: undefined,
312+
quotesWithError: [
313+
{
314+
error: true,
315+
message: providerErrorMessage,
316+
provider: { name: 'TestProvider' },
317+
},
318+
] as unknown as QuoteError[],
319+
};
320+
render(Quotes);
321+
act(() => {
322+
jest.advanceTimersByTime(3000);
323+
jest.clearAllTimers();
324+
});
325+
expect(screen.getByText('No providers available')).toBeTruthy();
326+
expect(screen.getByText(providerErrorMessage)).toBeTruthy();
327+
act(() => {
328+
jest.useFakeTimers({ legacyFakeTimers: true });
329+
});
330+
});
331+
332+
it('shows generic fallback when all quotes error but have no message', async () => {
333+
mockUseQuotesAndCustomActionsValues = {
334+
...mockUseQuotesAndCustomActionsInitialValues,
335+
customActions: [],
336+
quotesWithoutError: [],
337+
quotesByPriceWithoutError: [],
338+
quotesByReliabilityWithoutError: [],
339+
recommendedQuote: undefined,
340+
quotesWithError: [
341+
{
342+
error: true,
343+
provider: { name: 'TestProvider' },
344+
},
345+
] as unknown as QuoteError[],
346+
};
347+
render(Quotes);
348+
act(() => {
349+
jest.advanceTimersByTime(3000);
350+
jest.clearAllTimers();
351+
});
352+
expect(screen.getByText('No providers available')).toBeTruthy();
353+
expect(
354+
screen.getByText(
355+
'Try choosing a different payment method or try to increase or reduce the amount you want to buy!',
356+
),
357+
).toBeTruthy();
358+
act(() => {
359+
jest.useFakeTimers({ legacyFakeTimers: true });
360+
});
361+
});
362+
303363
it('renders correctly after animation with the recommended quote', async () => {
304364
render(Quotes);
305365
act(() => {

app/components/UI/Ramp/Aggregator/Views/Quotes/Quotes.tsx

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useEffect, useRef, useState } from 'react';
1+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
22
import { BackHandler } from 'react-native';
33
import { useSelector } from 'react-redux';
44
import { useFocusEffect, useNavigation } from '@react-navigation/native';
@@ -577,6 +577,17 @@ function Quotes() {
577577
}
578578
}, [ErrorFetchingQuotes, pollingCyclesLeft]);
579579

580+
const providerErrorDescription = useMemo(() => {
581+
if (
582+
!isFetchingQuotes &&
583+
quotesByPriceWithoutError.length === 0 &&
584+
quotesWithError.length > 0
585+
) {
586+
return quotesWithError[0]?.message;
587+
}
588+
return undefined;
589+
}, [isFetchingQuotes, quotesByPriceWithoutError.length, quotesWithError]);
590+
580591
useEffect(() => {
581592
navigation.setOptions(
582593
getDepositNavbarOptions(
@@ -928,16 +939,18 @@ function Quotes() {
928939
quotesByPriceWithoutError.length === 0 &&
929940
(!customActions || customActions.length === 0)
930941
) {
942+
const fallbackDescription = strings(
943+
isBuy
944+
? 'fiat_on_ramp_aggregator.try_different_amount_to_buy_input'
945+
: 'fiat_on_ramp_aggregator.try_different_amount_to_sell_input',
946+
);
947+
931948
if (!isExpanded) {
932949
return (
933950
<BottomSheet>
934951
<ErrorView
935952
title={strings('fiat_on_ramp_aggregator.no_providers_available')}
936-
description={strings(
937-
isBuy
938-
? 'fiat_on_ramp_aggregator.try_different_amount_to_buy_input'
939-
: 'fiat_on_ramp_aggregator.try_different_amount_to_sell_input',
940-
)}
953+
description={providerErrorDescription || fallbackDescription}
941954
ctaOnPress={() => navigation.goBack()}
942955
location={'Quotes Screen'}
943956
asScreen={false}
@@ -954,11 +967,7 @@ function Quotes() {
954967
<ScreenLayout>
955968
<ErrorView
956969
title={strings('fiat_on_ramp_aggregator.no_providers_available')}
957-
description={strings(
958-
isBuy
959-
? 'fiat_on_ramp_aggregator.try_different_amount_to_buy_input'
960-
: 'fiat_on_ramp_aggregator.try_different_amount_to_sell_input',
961-
)}
970+
description={providerErrorDescription || fallbackDescription}
962971
ctaOnPress={() => navigation.goBack()}
963972
location={'Quotes Screen'}
964973
/>

0 commit comments

Comments
 (0)