Skip to content

Commit ea3948c

Browse files
authored
feat: multi rpc modal (#11685)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** This PR introduces the UI implementation for the Multi-RPC Modal, which is part of the broader initiative to support multiple RPC endpoints for networks. The modal allows users to view and select from multiple RPC options for a specific network, ensuring a smoother transition or switch between different endpoints. **UI Only: This PR focuses solely on the implementation of the UI. The modal is not yet integrated or functional in the application as it requires the upgrade of the Network Controller to version v21.** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ## **Related issues** Fixes: ## **Manual testing steps** 1. this Modal is not called yet , it's require the network controller v21 to be used 2. the use of this modal will be done on this [PR](#11622) ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <img width="438" alt="Screenshot 2024-10-04 at 11 44 15" src="https://github.com/user-attachments/assets/f558a5a8-796a-423c-b957-f7e7aee8d0b2"> <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.
1 parent f35f102 commit ea3948c

File tree

28 files changed

+1095
-57
lines changed

28 files changed

+1095
-57
lines changed

app/actions/security/index.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export enum ActionType {
88
SET_AUTOMATIC_SECURITY_CHECKS_MODAL_OPEN = 'SET_AUTOMATIC_SECURITY_CHECKS_MODAL_OPEN',
99
SET_DATA_COLLECTION_FOR_MARKETING = 'SET_DATA_COLLECTION_FOR_MARKETING',
1010
SET_NFT_AUTO_DETECTION_MODAL_OPEN = 'SET_NFT_AUTO_DETECTION_MODAL_OPEN',
11+
SET_MULTI_RPC_MIGRATION_MODAL_OPEN = 'SET_MULTI_RPC_MIGRATION_MODAL_OPEN',
1112
}
1213

1314
export interface AllowLoginWithRememberMeUpdated
@@ -35,6 +36,11 @@ export interface SetNftAutoDetectionModalOpen
3536
open: boolean;
3637
}
3738

39+
export interface SetMultiRpcMigrationModalOpen
40+
extends ReduxAction<ActionType.SET_MULTI_RPC_MIGRATION_MODAL_OPEN> {
41+
open: boolean;
42+
}
43+
3844
export interface SetDataCollectionForMarketing
3945
extends ReduxAction<ActionType.SET_DATA_COLLECTION_FOR_MARKETING> {
4046
enabled: boolean;
@@ -46,7 +52,8 @@ export type Action =
4652
| UserSelectedAutomaticSecurityChecksOptions
4753
| SetAutomaticSecurityChecksModalOpen
4854
| SetDataCollectionForMarketing
49-
| SetNftAutoDetectionModalOpen;
55+
| SetNftAutoDetectionModalOpen
56+
| SetMultiRpcMigrationModalOpen;
5057

5158
export const setAllowLoginWithRememberMe = (
5259
enabled: boolean,
@@ -82,6 +89,13 @@ export const setNftAutoDetectionModalOpen = (
8289
open,
8390
});
8491

92+
export const setMultiRpcMigrationModalOpen = (
93+
open: boolean,
94+
): SetMultiRpcMigrationModalOpen => ({
95+
type: ActionType.SET_MULTI_RPC_MIGRATION_MODAL_OPEN,
96+
open,
97+
});
98+
8599
export const setDataCollectionForMarketing = (enabled: boolean) => ({
86100
type: ActionType.SET_DATA_COLLECTION_FOR_MARKETING,
87101
enabled,

app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const CellSelectWithMenu = ({
3333
tagLabel,
3434
isSelected = false,
3535
children,
36+
withAvatar = true,
3637
...props
3738
}: CellSelectWithMenuProps) => {
3839
const { styles } = useStyles(styleSheet, { style });
@@ -46,12 +47,15 @@ const CellSelectWithMenu = ({
4647
>
4748
<View style={styles.cellBase}>
4849
{/* DEV Note: Account Avatar should be replaced with Avatar with Badge whenever available */}
49-
<Avatar
50-
style={styles.avatar}
51-
testID={CellModalSelectorsIDs.BASE_AVATAR}
52-
size={DEFAULT_CELLBASE_AVATAR_SIZE}
53-
{...avatarProps}
54-
/>
50+
{withAvatar ? (
51+
<Avatar
52+
style={styles.avatar}
53+
testID={CellModalSelectorsIDs.BASE_AVATAR}
54+
size={DEFAULT_CELLBASE_AVATAR_SIZE}
55+
{...avatarProps}
56+
/>
57+
) : null}
58+
5559
<View style={styles.cellBaseInfo}>
5660
<Text
5761
numberOfLines={1}

app/component-library/components-temp/CellSelectWithMenu/__snapshots__/CellSelectWithMenu.test.tsx.snap

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ exports[`CellSelectWithMenu should render with default settings correctly 1`] =
1919
"opacity": 1,
2020
"padding": 16,
2121
"position": "relative",
22-
"width": "95%",
22+
"width": "90%",
2323
"zIndex": 1,
2424
}
2525
}
@@ -287,7 +287,13 @@ exports[`CellSelectWithMenu should render with default settings correctly 1`] =
287287
</View>
288288
</View>
289289
</TouchableOpacity>
290-
<View>
290+
<View
291+
style={
292+
{
293+
"paddingHorizontal": 20,
294+
}
295+
}
296+
>
291297
<TouchableOpacity
292298
accessibilityRole="button"
293299
accessible={true}

app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.styles.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const styleSheet = (params: {
2828
position: 'relative',
2929
opacity: isDisabled ? 0.5 : 1,
3030
padding: 16,
31-
width: '95%',
31+
width: '90%',
3232
zIndex: 1,
3333
} as ViewStyle,
3434
style,
@@ -86,6 +86,9 @@ const styleSheet = (params: {
8686
paddingLeft: 8,
8787
paddingTop: 32,
8888
},
89+
buttonIcon: {
90+
paddingHorizontal: 20,
91+
},
8992
});
9093
};
9194

app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.test.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ describe('ListItemMultiSelectButton', () => {
4141
const { getByRole } = render(
4242
<ListItemMultiSelectButton
4343
onPress={mockOnPress}
44-
onButtonClick={mockOnPress}
44+
buttonProps={{
45+
onButtonClick: mockOnPress,
46+
}}
4547
>
4648
<View />
4749
</ListItemMultiSelectButton>,
@@ -64,7 +66,9 @@ describe('ListItemMultiSelectButton', () => {
6466
const { getByTestId } = render(
6567
<ListItemMultiSelectButton
6668
buttonIcon={IconName.Check}
67-
onButtonClick={mockOnButtonClick}
69+
buttonProps={{
70+
onButtonClick: mockOnButtonClick,
71+
}}
6872
>
6973
<View />
7074
</ListItemMultiSelectButton>,

app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.tsx

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,22 @@ import {
2020
IconColor,
2121
IconName,
2222
} from '../../../component-library/components/Icons/Icon';
23+
import Button, {
24+
ButtonSize,
25+
ButtonVariants,
26+
ButtonWidthTypes,
27+
} from '../../../component-library/components/Buttons/Button';
28+
import { TextVariant } from '../../../component-library/components/Texts/Text';
2329

2430
const ListItemMultiSelectButton: React.FC<ListItemMultiSelectButtonProps> = ({
2531
style,
2632
isSelected = false,
2733
isDisabled = false,
2834
children,
2935
gap = DEFAULT_LISTITEMMULTISELECT_GAP,
36+
showButtonIcon = true,
3037
buttonIcon = IconName.MoreVertical,
38+
buttonProps,
3139
...props
3240
}) => {
3341
const { styles } = useStyles(styleSheet, {
@@ -55,15 +63,29 @@ const ListItemMultiSelectButton: React.FC<ListItemMultiSelectButtonProps> = ({
5563
</View>
5664
)}
5765
</TouchableOpacity>
58-
<View>
59-
<ButtonIcon
60-
iconName={buttonIcon}
61-
iconColor={IconColor.Default}
62-
testID={BUTTON_TEST_ID}
63-
onPress={props.onButtonClick}
64-
accessibilityRole="button"
65-
/>
66-
</View>
66+
{showButtonIcon ? (
67+
<View style={styles.buttonIcon}>
68+
<ButtonIcon
69+
iconName={buttonIcon}
70+
iconColor={IconColor.Default}
71+
testID={BUTTON_TEST_ID}
72+
onPress={buttonProps?.onButtonClick}
73+
accessibilityRole="button"
74+
/>
75+
</View>
76+
) : null}
77+
{buttonProps?.textButton ? (
78+
<View>
79+
<Button
80+
variant={ButtonVariants.Link}
81+
onPress={buttonProps?.onButtonClick as () => void}
82+
labelTextVariant={TextVariant.BodyMD}
83+
size={ButtonSize.Lg}
84+
width={ButtonWidthTypes.Auto}
85+
label={buttonProps?.textButton}
86+
/>
87+
</View>
88+
) : null}
6789
</View>
6890
);
6991
};

app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.types.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,30 @@ export interface ListItemMultiSelectButtonProps
2727
buttonIcon?: IconName;
2828

2929
/**
30-
* Optional button onClick function
30+
* Optional button onClick rpc modal function
3131
*/
32-
onButtonClick?: ((event: GestureResponderEvent) => void) | undefined;
32+
onTextClick?: (() => void) | undefined;
3333

3434
/**
35-
* Optional button onClick rpc modal function
35+
* Optional property to add avatar
3636
*/
37-
onTextClick?: (() => void) | undefined;
37+
withAvatar?: boolean;
38+
39+
/**
40+
* Optional property to show icon
41+
*/
42+
showButtonIcon?: boolean;
43+
44+
buttonProps?: {
45+
/**
46+
* Optional button onClick function
47+
*/
48+
onButtonClick?: ((event: GestureResponderEvent) => void) | undefined;
49+
/**
50+
* Optional property to show text button
51+
*/
52+
textButton?: string | null;
53+
};
3854
}
3955

4056
/**

app/component-library/components-temp/ListItemMultiSelectButton/__snapshots__/ListItemMultiSelectButton.test.tsx.snap

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ exports[`ListItemMultiSelectButton should render correctly with default props 1`
1919
"opacity": 1,
2020
"padding": 16,
2121
"position": "relative",
22-
"width": "95%",
22+
"width": "90%",
2323
"zIndex": 1,
2424
}
2525
}
@@ -52,7 +52,13 @@ exports[`ListItemMultiSelectButton should render correctly with default props 1`
5252
</View>
5353
</View>
5454
</TouchableOpacity>
55-
<View>
55+
<View
56+
style={
57+
{
58+
"paddingHorizontal": 20,
59+
}
60+
}
61+
>
5662
<TouchableOpacity
5763
accessibilityRole="button"
5864
accessible={true}

app/components/Nav/App/index.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ import { SnapsExecutionWebView } from '../../../lib/snaps';
127127
import OptionsSheet from '../../UI/SelectOptionSheet/OptionsSheet';
128128
import FoxLoader from '../../../components/UI/FoxLoader';
129129
import { AppStateEventProcessor } from '../../../core/AppStateEventListener';
130+
import MultiRpcModal from '../../../components/Views/MultiRpcModal/MultiRpcModal';
130131

131132
const clearStackNavigatorOptions = {
132133
headerShown: false,
@@ -396,7 +397,6 @@ const App = (props) => {
396397
});
397398
}, [handleDeeplink]);
398399

399-
400400
useEffect(() => {
401401
if (navigator) {
402402
// Initialize deep link manager
@@ -687,11 +687,17 @@ const App = (props) => {
687687
name={Routes.MODAL.NFT_AUTO_DETECTION_MODAL}
688688
component={NFTAutoDetectionModal}
689689
/>
690+
{isNetworkUiRedesignEnabled() ? (
691+
<Stack.Screen
692+
name={Routes.MODAL.MULTI_RPC_MIGRATION_MODAL}
693+
component={MultiRpcModal}
694+
/>
695+
) : null}
696+
690697
<Stack.Screen
691698
name={Routes.SHEET.SHOW_TOKEN_ID}
692699
component={ShowTokenIdSheet}
693700
/>
694-
695701
<Stack.Screen
696702
name={Routes.SHEET.ORIGIN_SPAM_MODAL}
697703
component={OriginSpamModal}

app/components/Nav/Main/index.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import { useMinimumVersions } from '../../hooks/MinimumVersions';
5959
import navigateTermsOfUse from '../../../util/termsOfUse/termsOfUse';
6060
import {
6161
selectChainId,
62+
selectNetworkConfigurations,
6263
selectProviderConfig,
6364
selectProviderType,
6465
} from '../../../selectors/networkController';
@@ -80,6 +81,7 @@ import {
8081
startIncomingTransactionPolling,
8182
stopIncomingTransactionPolling,
8283
} from '../../../util/transaction-controller';
84+
import isNetworkUiRedesignEnabled from '../../../util/networks/isNetworkUiRedesignEnabled';
8385

8486
const Stack = createStackNavigator();
8587

@@ -232,8 +234,10 @@ const Main = (props) => {
232234
* Current network
233235
*/
234236
const providerConfig = useSelector(selectProviderConfig);
237+
const networkConfigurations = useSelector(selectNetworkConfigurations);
235238
const networkName = useSelector(selectNetworkName);
236239
const previousProviderConfig = useRef(undefined);
240+
const previousNetworkConfigurations = useRef(undefined);
237241
const { toastRef } = useContext(ToastContext);
238242
const networkImage = useSelector(selectNetworkImageSource);
239243

@@ -259,6 +263,41 @@ const Main = (props) => {
259263
previousProviderConfig.current = providerConfig;
260264
}, [providerConfig, networkName, networkImage, toastRef]);
261265

266+
// Show add network confirmation.
267+
useEffect(() => {
268+
if (!isNetworkUiRedesignEnabled()) return;
269+
270+
// Memoized values to avoid recalculations
271+
const currentNetworkValues = Object.values(networkConfigurations);
272+
const previousNetworkValues = Object.values(
273+
previousNetworkConfigurations.current ?? {},
274+
);
275+
276+
if (
277+
previousNetworkValues.length &&
278+
currentNetworkValues.length !== previousNetworkValues.length
279+
) {
280+
// Find the newly added network
281+
const newNetwork = currentNetworkValues.find(
282+
(network) => !previousNetworkValues.includes(network),
283+
);
284+
285+
toastRef?.current?.showToast({
286+
variant: ToastVariants.Plain,
287+
labelOptions: [
288+
{
289+
label: `${newNetwork?.name ?? strings('asset_details.network')} `,
290+
isBold: true,
291+
},
292+
{ label: strings('toast.network_added') },
293+
],
294+
networkImageSource: networkImage,
295+
});
296+
}
297+
298+
previousNetworkConfigurations.current = networkConfigurations;
299+
}, [networkConfigurations, networkName, networkImage, toastRef]);
300+
262301
useEffect(() => {
263302
if (locale.current !== I18n.locale) {
264303
locale.current = I18n.locale;

0 commit comments

Comments
 (0)