Skip to content

Commit 9743e13

Browse files
authored
Merge branch 'main' into fix/account-connect-origin-spoof
2 parents db6daed + e93ab69 commit 9743e13

File tree

472 files changed

+9107
-14647
lines changed

Some content is hidden

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

472 files changed

+9107
-14647
lines changed

.github/CODEOWNERS

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ app/reducers/fiatOrders/ @MetaMask/ramp
4242
# Confirmation Team
4343
app/components/Views/confirmations @MetaMask/confirmations
4444
app/core/Engine/controllers/gas-fee-controller @MetaMask/confirmations
45+
app/core/Engine/controllers/signature-controller @MetaMask/confirmations
4546
app/core/Engine/controllers/transaction-controller @MetaMask/confirmations
4647
app/core/Analytics/events/confirmations @MetaMask/confirmations
4748
ppom @MetaMask/confirmations

.github/workflows/automated-rca.yml

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: Automated RCA
2+
3+
on:
4+
issues:
5+
types: [closed]
6+
7+
permissions:
8+
issues: write
9+
contents: read
10+
11+
jobs:
12+
automated-rca:
13+
uses: MetaMask/github-tools/.github/workflows/post-gh-rca.yml@115cc6dce7aa32c85cbd77a19e9c04db85fb7920
14+
with:
15+
google-form-base-url: 'https://docs.google.com/forms/d/e/1FAIpQLSdnPbJISzFlR_aQD2uRpnMKSoGAopuTd_yeZK7J4Q5GzgbsOA/viewform?usp=pp_url&entry.340898780='
16+
repo-owner: ${{ github.repository_owner }}
17+
repo-name: ${{ github.event.repository.name }}
18+
issue-number: ${{ github.event.issue.number }}
19+
issue-labels: '["Sev0-urgent", "Sev1-high"]'

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- fix(browser): fix browser PermissionsSummary origin spoofing when browser redirects issue ([#13394](https://github.com/MetaMask/metamask-mobile/pull/13394))
13+
- feat(bridge): add error handling and input management for bridge quotes ([#14693](https://github.com/MetaMask/metamask-mobile/pull/14693))
1314
- feat(multi-srp): enable multi-srp in main and beta ([#14558](https://github.com/MetaMask/metamask-mobile/pull/14558))
15+
- feat(ramp): Update ramp data flow to fetch cryptos before payment methods ([#14437](https://github.com/MetaMask/metamask-mobile/pull/14437))
1416
- feat(bridge): add destination account picker ([#14656](https://github.com/MetaMask/metamask-mobile/pull/14656))
1517
- feat(bridge): add Solana assets to bridge token pickers ([#14365](https://github.com/MetaMask/metamask-mobile/pull/14365))
1618
- feat: add AppMetadataController controller ([#14513](https://github.com/MetaMask/metamask-mobile/pull/14513))
1719
- feat(bridge): implement bridge quote fetching ([#14413](https://github.com/MetaMask/metamask-mobile/pull/14413))
1820

21+
22+
### Changed
23+
24+
- feat(bridge): Handle Solana vs EVM swap and bridge routing ([#14614](https://github.com/MetaMask/metamask-mobile/pull/14614))
25+
1926
## [7.44.0]
2027

2128
### Added

app/components/Nav/Main/MainNavigator.js

+2
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ import NftDetailsFullImage from '../../Views/NftDetails/NFtDetailsFullImage';
8989
import AccountPermissions from '../../../components/Views/AccountPermissions';
9090
import { AccountPermissionsScreens } from '../../../components/Views/AccountPermissions/AccountPermissions.types';
9191
import { StakeModalStack, StakeScreenStack } from '../../UI/Stake/routes';
92+
import { BridgeTransactionDetails } from '../../UI/Bridge/components/TransactionDetails/TransactionDetails';
9293
import { BridgeModalStack, BridgeScreenStack } from '../../UI/Bridge/routes';
9394

9495
const Stack = createStackNavigator();
@@ -205,6 +206,7 @@ const TransactionsHome = () => (
205206
name={Routes.RAMP.SEND_TRANSACTION}
206207
component={SendTransaction}
207208
/>
209+
<Stack.Screen name={Routes.BRIDGE.BRIDGE_TRANSACTION_DETAILS} component={BridgeTransactionDetails} />
208210
</Stack.Navigator>
209211
);
210212

app/components/Snaps/SnapUIAddressInput/SnapUIAddressInput.test.tsx

+3-7
Original file line numberDiff line numberDiff line change
@@ -195,21 +195,17 @@ describe('SnapUIAddressInput', () => {
195195
expect(toJSON()).toMatchSnapshot();
196196
});
197197

198-
it('does not render the matched address info if there is an error', () => {
198+
it('renders the matched address info with an error', () => {
199199
mockGetValue.mockReturnValue(`${testChainId}:${testAddress}`);
200200
const displayName = 'Vitalik.eth';
201201
(useDisplayName as jest.Mock).mockReturnValue(displayName);
202202

203-
const { queryByText, queryByTestId, getByText, toJSON } = renderWithProvider(
203+
const { queryByText, getByText, toJSON } = renderWithProvider(
204204
<SnapUIAddressInput name="testAddress" chainId={testChainId} error="Error" />,
205205
{ state: mockInitialState },
206206
);
207207

208-
expect(queryByText(displayName)).toBeFalsy();
209-
210-
const input = queryByTestId(INPUT_TEST_ID);
211-
212-
expect(input.props.value).toBe(testAddress);
208+
expect(queryByText(displayName)).toBeTruthy();
213209
expect(getByText('Error')).toBeTruthy();
214210

215211
const tree = JSON.stringify(toJSON());

app/components/Snaps/SnapUIAddressInput/SnapUIAddressInput.tsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ interface MatchedAccountInfoProps {
5151
displayName: string;
5252
handleClear: () => void;
5353
disabled?: boolean;
54+
error?: string;
5455
}
5556

5657
const MatchedAccountInfo = ({
@@ -61,6 +62,7 @@ const MatchedAccountInfo = ({
6162
displayName,
6263
handleClear,
6364
disabled,
65+
error,
6466
}: MatchedAccountInfoProps) => {
6567
const { colors } = useTheme();
6668
const styles = StyleSheet.create({
@@ -115,6 +117,12 @@ const MatchedAccountInfo = ({
115117
testID="snap-ui-address-input__clear-button"
116118
/>
117119
</Box>
120+
{error && (
121+
// eslint-disable-next-line react-native/no-inline-styles
122+
<HelpText severity={HelpTextSeverity.Error} style={{ marginTop: 4 }}>
123+
{error}
124+
</HelpText>
125+
)}
118126
</Box>
119127
);
120128
};
@@ -204,7 +212,7 @@ export const SnapUIAddressInput = ({
204212
handleInputChange(name, '', form);
205213
};
206214

207-
if (displayName && !error) {
215+
if (displayName) {
208216
return (
209217
<MatchedAccountInfo
210218
label={label}
@@ -214,6 +222,7 @@ export const SnapUIAddressInput = ({
214222
displayName={displayName}
215223
handleClear={handleClear}
216224
disabled={disabled}
225+
error={error}
217226
/>
218227
);
219228
}

app/components/Snaps/SnapUILink/SnapUILink.test.tsx

+91-12
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import React from 'react';
22
import { render, fireEvent } from '@testing-library/react-native';
3-
import { Linking } from 'react-native';
3+
import { Linking, Text, View } from 'react-native';
44
import { SnapUILink } from './SnapUILink';
5-
import ButtonLink from '../../../component-library/components/Buttons/Button/variants/ButtonLink';
65
import Icon, {
76
IconColor,
87
IconName,
98
IconSize,
109
} from '../../../component-library/components/Icons/Icon';
11-
import { TextColor } from '../../../component-library/components/Texts/Text';
1210

1311
jest.mock('react-native/Libraries/Linking/Linking', () => ({
1412
openURL: jest.fn(),
@@ -25,14 +23,19 @@ describe('SnapUILink', () => {
2523
};
2624

2725
it('renders correctly with valid props', () => {
28-
const { UNSAFE_getByType } = render(<SnapUILink {...validProps} />);
26+
const { UNSAFE_getByType, getByTestId } = render(
27+
<SnapUILink {...validProps} />,
28+
);
2929

30-
const button = UNSAFE_getByType(ButtonLink);
30+
const linkText = getByTestId('snaps-ui-link');
31+
const spacer = UNSAFE_getByType(View);
3132
const icon = UNSAFE_getByType(Icon);
3233

33-
expect(button).toBeTruthy();
34-
expect(button.props.testID).toBe('snaps-ui-link');
35-
expect(button.props.color).toBe(TextColor.Info);
34+
expect(linkText).toBeTruthy();
35+
expect(linkText.props.children[0]).toBe('Visit MetaMask');
36+
expect(spacer).toBeTruthy();
37+
expect(spacer.props.style).toEqual({ width: 4 });
38+
expect(linkText.props.accessibilityRole).toBe('link');
3639
expect(icon.props.name).toBe(IconName.Export);
3740
expect(icon.props.color).toBe(IconColor.Primary);
3841
expect(icon.props.size).toBe(IconSize.Sm);
@@ -41,8 +44,8 @@ describe('SnapUILink', () => {
4144
it('opens URL when pressed with valid https URL', () => {
4245
const { getByTestId } = render(<SnapUILink {...validProps} />);
4346

44-
const button = getByTestId('snaps-ui-link');
45-
fireEvent.press(button);
47+
const link = getByTestId('snaps-ui-link');
48+
fireEvent.press(link);
4649

4750
expect(Linking.openURL).toHaveBeenCalledWith(validProps.href);
4851
expect(Linking.openURL).toHaveBeenCalledTimes(1);
@@ -56,12 +59,88 @@ describe('SnapUILink', () => {
5659

5760
const { getByTestId } = render(<SnapUILink {...invalidProps} />);
5861

59-
const button = getByTestId('snaps-ui-link');
62+
const link = getByTestId('snaps-ui-link');
6063

6164
expect(() => {
62-
fireEvent.press(button);
65+
fireEvent.press(link);
6366
}).toThrow('Invalid URL');
6467

6568
expect(Linking.openURL).not.toHaveBeenCalled();
6669
});
70+
71+
it('can be nested inside another Text component', () => {
72+
const { toJSON } = render(
73+
<Text>
74+
Before <SnapUILink href="https://metamask.io">MetaMask</SnapUILink>{' '}
75+
After
76+
</Text>,
77+
);
78+
79+
const textContent = JSON.stringify(toJSON());
80+
expect(textContent).toContain('Before');
81+
expect(textContent).toContain('MetaMask');
82+
expect(textContent).toContain('After');
83+
});
84+
85+
it('handles array children correctly', () => {
86+
const { getByTestId } = render(
87+
<SnapUILink href="https://metamask.io">
88+
{'Part 1 '}
89+
{'Part 2'}
90+
</SnapUILink>,
91+
);
92+
93+
const link = getByTestId('snaps-ui-link');
94+
const childrenArray = link.props.children;
95+
const textContent = childrenArray[0].toString();
96+
97+
expect(textContent).toBe('Part 1 ,Part 2');
98+
});
99+
100+
it('renders correctly with complex children', () => {
101+
const { UNSAFE_getAllByType, toJSON } = render(
102+
<SnapUILink href="https://metamask.io">
103+
Normal text
104+
{/* eslint-disable-next-line react-native/no-inline-styles */}
105+
<Text style={{ fontWeight: 'bold' }}>Bold text</Text>
106+
</SnapUILink>,
107+
);
108+
109+
const textContent = JSON.stringify(toJSON());
110+
expect(textContent).toContain('Normal text');
111+
expect(textContent).toContain('Bold text');
112+
113+
// Should have our Icon plus any Text components
114+
const allIcons = UNSAFE_getAllByType(Icon);
115+
expect(allIcons.length).toBe(1);
116+
});
117+
118+
it('validates URL format correctly', () => {
119+
// Valid HTTPS URL
120+
expect(() => {
121+
fireEvent.press(
122+
render(
123+
<SnapUILink href="https://example.com">Link</SnapUILink>,
124+
).getByTestId('snaps-ui-link'),
125+
);
126+
}).not.toThrow();
127+
128+
// Invalid HTTP URL
129+
expect(() => {
130+
fireEvent.press(
131+
render(
132+
<SnapUILink href="http://example.com">Link</SnapUILink>,
133+
).getByTestId('snaps-ui-link'),
134+
);
135+
}).toThrow('Invalid URL');
136+
137+
// Invalid non-HTTP URL
138+
expect(() => {
139+
fireEvent.press(
140+
render(
141+
<SnapUILink href="ftp://example.com">Link</SnapUILink>,
142+
).getByTestId('snaps-ui-link'),
143+
);
144+
}).toThrow('Invalid URL');
145+
});
67146
});
+26-36
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
11
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
22
import { LinkChildren } from '@metamask/snaps-sdk/jsx';
33
import React from 'react';
4-
import { Linking, View } from 'react-native';
5-
import ButtonLink from '../../../component-library/components/Buttons/Button/variants/ButtonLink';
6-
import { TextColor } from '../../../component-library/components/Texts/Text';
4+
import { Text, StyleSheet, Linking, View } from 'react-native';
75
import Icon, {
86
IconColor,
97
IconName,
108
IconSize,
119
} from '../../../component-library/components/Icons/Icon';
10+
import { strings } from '../../../../locales/i18n';
11+
12+
const styles = StyleSheet.create({
13+
container: {
14+
flexDirection: 'row',
15+
alignItems: 'center',
16+
alignSelf: 'flex-start',
17+
},
18+
spacer: {
19+
width: 4,
20+
},
21+
});
1222

1323
export interface SnapUILinkProps {
1424
children: LinkChildren;
@@ -27,38 +37,18 @@ const onPress = (href: string) => {
2737
};
2838

2939
// TODO: This component should show a modal for links when not using preinstalled Snaps
30-
export const SnapUILink: React.FC<SnapUILinkProps> = ({ href, children }) => {
31-
const label = (
32-
<>
33-
{children}
34-
<View
35-
/* eslint-disable-next-line react-native/no-inline-styles */
36-
style={{
37-
width: 4,
38-
}}
39-
/>
40-
<Icon
41-
name={IconName.Export}
42-
color={IconColor.Primary}
43-
size={IconSize.Sm}
44-
/* eslint-disable-next-line react-native/no-inline-styles */
45-
style={{
46-
justifyContent: 'center',
47-
alignItems: 'center',
48-
alignSelf: 'center',
49-
}}
50-
/>
51-
</>
52-
);
40+
export const SnapUILink: React.FC<SnapUILinkProps> = ({ href, children }) => (
41+
<Text
42+
testID="snaps-ui-link"
43+
style={styles.container}
44+
onPress={() => onPress(href)}
45+
accessibilityRole="link"
46+
accessibilityHint={strings('snaps.snap_ui.link.accessibilityHint')}
47+
>
48+
{children}
49+
<View style={styles.spacer} />
50+
<Icon name={IconName.Export} color={IconColor.Primary} size={IconSize.Sm} />
51+
</Text>
52+
);
5353

54-
return (
55-
<ButtonLink
56-
testID="snaps-ui-link"
57-
// @ts-expect-error This prop is not part of the type but it works.
58-
color={TextColor.Info}
59-
onPress={() => onPress(href)}
60-
label={label}
61-
/>
62-
);
63-
};
6454
///: END:ONLY_INCLUDE_IF

0 commit comments

Comments
 (0)