Skip to content

Commit 06447db

Browse files
committed
Merge branch 'main' into test/temp-nightly
2 parents d7eda9a + a974cf2 commit 06447db

237 files changed

Lines changed: 8582 additions & 10901 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/scripts/collect-qa-stats.mjs

Lines changed: 389 additions & 2 deletions
Large diffs are not rendered by default.

app/component-library/components/Buttons/Button/variants/ButtonLink/ButtonLink.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,17 @@ import {
2323
} from './ButtonLink.constants';
2424

2525
/**
26-
* @deprecated Please update your code to use `TextButton` from `@metamask/design-system-react-native`.
27-
* The API may have changed — compare props before migrating.
28-
* @see {@link https://github.com/MetaMask/metamask-design-system/blob/main/packages/design-system-react-native/src/components/TextButton/README.md}
26+
* @deprecated ButtonLink has been replaced by design-system components.
27+
*
28+
* - Use `TextButton` for inline links within text flows.
29+
* - Use `Button` with `variant={ButtonVariant.Tertiary}` for standalone link‑style buttons (e.g., CTAs, headers, separators).
30+
*
31+
* Examples:
32+
* Inline: <Text>Forgot your password? <TextButton onPress={...}>Reset</TextButton></Text>
33+
* Standalone: <Button variant={ButtonVariant.Tertiary} onPress={...}>Forgot password?</Button>
34+
*
35+
* @see {@link https://github.com/MetaMask/metamask-design-system/blob/main/packages/design-system-react-native/src/components/TextButton/README.md | TextButton}
36+
* @see {@link https://github.com/MetaMask/metamask-design-system/blob/main/packages/design-system-react-native/src/components/Button/README.md | Button}
2937
*/
3038
const ButtonLink: React.FC<ButtonLinkProps> = ({
3139
style,

app/component-library/providers/ThemeProvider/ThemeProvider.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import renderWithProvider from '../../../util/test/renderWithProvider';
77

88
// Internal dependencies
99
import ThemeProvider from './ThemeProvider';
10+
import { brandColor } from '@metamask/design-tokens';
1011

1112
describe('ThemeProvider', () => {
1213
it('renders children correctly', () => {
@@ -36,6 +37,6 @@ describe('ThemeProvider', () => {
3637
</ThemeProvider>,
3738
);
3839

39-
expect(themeValue.brandColors.black).toStrictEqual('#000000');
40+
expect(themeValue.brandColors.black).toStrictEqual(brandColor.black);
4041
});
4142
});

app/components/UI/Bridge/Views/BridgeView/BridgeView.styles.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const createStyles = (params: { theme: Theme }) => {
1212
},
1313
inputsContainer: {
1414
paddingVertical: 12,
15-
paddingHorizontal: 24,
15+
paddingHorizontal: 16,
1616
},
1717
buttonContainer: {
1818
width: '100%',

app/components/UI/Bridge/Views/BridgeView/BridgeView.test.tsx

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,6 @@ jest.mock('../../../../../core/Engine', () => {
9898
unsubscribe: jest.fn(),
9999
},
100100
context: {
101-
SwapsController: {
102-
fetchAggregatorMetadataWithCache: jest.fn(),
103-
fetchTopAssetsWithCache: jest.fn(),
104-
fetchTokenWithCache: jest.fn(),
105-
},
106101
KeyringController: {
107102
state: {
108103
keyrings: [
@@ -158,6 +153,7 @@ jest.mock('../../../../../core/Engine', () => {
158153
resetState: jest.fn(),
159154
setBridgeFeatureFlags: jest.fn().mockResolvedValue(undefined),
160155
updateBridgeQuoteRequestParams: jest.fn(),
156+
trackUnifiedSwapBridgeEvent: jest.fn(),
161157
},
162158
},
163159
getTotalEvmFiatAccountBalance: jest.fn().mockReturnValue({
@@ -491,6 +487,43 @@ describe('BridgeView', () => {
491487
expect(getByText('$19,000.00')).toBeTruthy();
492488
});
493489

490+
it('should update source token amount when selecting a quick-pick preset', async () => {
491+
jest
492+
.mocked(useBridgeQuoteData as unknown as jest.Mock)
493+
.mockImplementation(() => ({
494+
...mockUseBridgeQuoteData,
495+
activeQuote: null,
496+
bestQuote: null,
497+
sourceAmount: undefined,
498+
isLoading: false,
499+
destTokenAmount: undefined,
500+
formattedQuoteData: undefined,
501+
}));
502+
503+
const { getByTestId, getByText } = renderScreen(
504+
BridgeView,
505+
{
506+
name: Routes.BRIDGE.ROOT,
507+
},
508+
{ state: mockState },
509+
);
510+
511+
const sourceInput = getByTestId('source-token-area-input');
512+
await act(async () => {
513+
sourceInput.props.onPressIn();
514+
});
515+
516+
await waitFor(() => {
517+
expect(getByText('25%')).toBeTruthy();
518+
});
519+
520+
fireEvent.press(getByText('25%'));
521+
522+
await waitFor(() => {
523+
expect(getByTestId('source-token-area-input').props.value).toBe('0.5');
524+
});
525+
});
526+
494527
it('should display source token symbol and balance', async () => {
495528
const stateWithAmount = {
496529
...mockState,

app/components/UI/Bridge/Views/BridgeView/index.tsx

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ import { Hex } from '@metamask/utils';
7474
import { useBridgeQuoteEvents } from '../../hooks/useBridgeQuoteEvents/index.ts';
7575
import { SwapsKeypad } from '../../components/SwapsKeypad/index.tsx';
7676
import { getGasFeesSponsoredNetworkEnabled } from '../../../../../selectors/featureFlagController/gasFeesSponsored';
77-
import { trimTrailingZeros } from '../../utils/trimTrailingZeros.ts';
77+
import { normalizeSourceAmountToMaxLength } from '../../utils/normalizeSourceAmountToMaxLength.ts';
7878
import { FLipQuoteButton } from '../../components/FlipQuoteButton/index.tsx';
7979
import { useIsGasIncludedSTXSendBundleSupported } from '../../hooks/useIsGasIncludedSTXSendBundleSupported/index.ts';
8080
import { useIsGasIncluded7702Supported } from '../../hooks/useIsGasIncluded7702Supported/index.ts';
@@ -89,10 +89,12 @@ import { type BridgeRouteParams } from '../../hooks/useSwapBridgeNavigation/inde
8989
import BridgeTrendingTokensSection from '../../components/BridgeTrendingTokensSection/BridgeTrendingTokensSection';
9090
import { selectRemoteFeatureFlags } from '../../../../../selectors/featureFlagController';
9191
import type { RootState } from '../../../../../reducers';
92-
const SCROLL_NEAR_BOTTOM_PX = 160;
9392
import { useTrackSwapPageViewed } from '../../hooks/useTrackSwapPageViewed/index.ts';
93+
import { useSourceAmountCursor } from '../../hooks/useSourceAmountCursor.ts';
9494
import { BridgeViewFooter } from './BridgeViewFooter.tsx';
9595

96+
const SCROLL_NEAR_BOTTOM_PX = 160;
97+
9698
const BridgeView = () => {
9799
const [isErrorBannerVisible, setIsErrorBannerVisible] = useState(true);
98100
const [isNearBottom, setIsNearBottom] = useState(false);
@@ -137,6 +139,23 @@ const BridgeView = () => {
137139
const isNonEvmNonEvmBridge = useSelector(selectIsNonEvmNonEvmBridge);
138140
const isSolanaSourced = useSelector(selectIsSolanaSourced);
139141
const isDestNetworkEnabled = useIsNetworkEnabled(destToken?.chainId);
142+
const handleSourceAmountChange = useCallback(
143+
(value: string | undefined) => {
144+
dispatch(setSourceAmount(value));
145+
},
146+
[dispatch],
147+
);
148+
const {
149+
sourceSelection,
150+
handleSourceSelectionChange,
151+
handleKeypadChange,
152+
resetSourceAmountCursorPosition,
153+
} = useSourceAmountCursor({
154+
sourceAmount,
155+
sourceTokenDecimals: sourceToken?.decimals,
156+
maxInputLength: MAX_INPUT_LENGTH,
157+
onSourceAmountChange: handleSourceAmountChange,
158+
});
140159

141160
/** The entry point location for analytics (e.g. Main View, Token View, Trending Explore) */
142161
const location = route.params?.location;
@@ -299,33 +318,43 @@ const BridgeView = () => {
299318
}
300319
}, [isError]);
301320

302-
// Keypad already handles max token decimals, so we don't need to check here
303-
const handleKeypadChange = ({
304-
value,
305-
}: {
306-
value: string;
307-
valueAsNumber: number;
308-
pressedKey: string;
309-
}) => {
310-
if (value.length >= MAX_INPUT_LENGTH) {
311-
return;
312-
}
313-
dispatch(setSourceAmount(value || undefined));
314-
};
315-
316321
const handleSourceMaxPress = () => {
317322
if (latestSourceBalance?.displayBalance) {
318323
const balance = latestSourceBalance.displayBalance;
319-
const cleaned = trimTrailingZeros(balance);
324+
const cleaned = normalizeSourceAmountToMaxLength(
325+
balance,
326+
MAX_INPUT_LENGTH,
327+
);
328+
resetSourceAmountCursorPosition();
320329
dispatch(setSourceAmountAsMax(cleaned));
321330
}
322331
};
323332

333+
const handleSourcePresetAmountSelect = useCallback(
334+
(value: string) => {
335+
// Quick-pick presets replace the full amount rather than editing at the
336+
// current cursor position, so clear the cursor state before updating.
337+
resetSourceAmountCursorPosition();
338+
dispatch(
339+
setSourceAmount(
340+
normalizeSourceAmountToMaxLength(value, MAX_INPUT_LENGTH) ||
341+
undefined,
342+
),
343+
);
344+
},
345+
[dispatch, resetSourceAmountCursorPosition],
346+
);
347+
324348
const handleSourceTokenPress = () =>
325349
navigation.navigate(Routes.BRIDGE.TOKEN_SELECTOR, {
326350
type: 'source',
327351
});
328352

353+
const handleFlipTokensPress = useCallback(() => {
354+
resetSourceAmountCursorPosition();
355+
void handleSwitchTokens(destTokenAmount)();
356+
}, [destTokenAmount, handleSwitchTokens, resetSourceAmountCursorPosition]);
357+
329358
const handleDestTokenPress = () =>
330359
navigation.navigate(Routes.BRIDGE.TOKEN_SELECTOR, {
331360
type: 'dest',
@@ -385,6 +414,7 @@ const BridgeView = () => {
385414
<TokenInputArea
386415
ref={inputRef}
387416
amount={sourceAmount}
417+
selection={sourceSelection}
388418
token={sourceToken}
389419
tokenBalance={latestSourceBalance?.displayBalance}
390420
networkImageSource={
@@ -397,14 +427,15 @@ const BridgeView = () => {
397427
testID={BridgeViewSelectorsIDs.SOURCE_TOKEN_AREA}
398428
tokenType={TokenInputAreaType.Source}
399429
onInputPress={() => keypadRef.current?.open()}
430+
onSelectionChange={handleSourceSelectionChange}
400431
onTokenPress={handleSourceTokenPress}
401432
onMaxPress={handleSourceMaxPress}
402433
latestAtomicBalance={latestSourceBalance?.atomicBalance}
403434
isSourceToken
404435
isQuoteSponsored={isQuoteSponsored}
405436
/>
406437
<FLipQuoteButton
407-
onPress={handleSwitchTokens(destTokenAmount)}
438+
onPress={handleFlipTokensPress}
408439
disabled={
409440
!destChainId ||
410441
!destToken ||
@@ -484,9 +515,10 @@ const BridgeView = () => {
484515
) : (
485516
<GaslessQuickPickOptions
486517
token={sourceToken}
518+
tokenBalance={latestSourceBalance?.displayBalance}
487519
onMaxPress={handleSourceMaxPress}
488520
isQuoteSponsored={isQuoteSponsored}
489-
onChange={handleKeypadChange}
521+
onAmountSelect={handleSourcePresetAmountSelect}
490522
/>
491523
)}
492524
</SwapsKeypad>

app/components/UI/Bridge/_mocks_/initialState.ts

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -609,35 +609,6 @@ export const initialState = {
609609
},
610610
},
611611
},
612-
SwapsController: {
613-
chainCache: {
614-
[ethChainId]: {
615-
aggregatorMetadata: null,
616-
tokens: null,
617-
topAssets: [
618-
{
619-
address: ethToken1Address,
620-
symbol: 'TOKEN1',
621-
},
622-
{
623-
address: ethToken2Address,
624-
symbol: 'HELLO',
625-
},
626-
],
627-
aggregatorMetadataLastFetched: 0,
628-
topAssetsLastFetched: 0,
629-
tokensLastFetched: 0,
630-
},
631-
[optimismChainId]: {
632-
aggregatorMetadata: null,
633-
tokens: null,
634-
topAssets: null,
635-
aggregatorMetadataLastFetched: 0,
636-
topAssetsLastFetched: 0,
637-
tokensLastFetched: 0,
638-
},
639-
},
640-
},
641612
KeyringController: {
642613
vault: '',
643614
isUnlocked: true,

0 commit comments

Comments
 (0)