From 9e3d2684acfaf193cc1781680beaf7ea29f5edb8 Mon Sep 17 00:00:00 2001 From: Nick Gambino <35090461+gambinish@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:06:14 -0700 Subject: [PATCH 01/46] fix: Duplicate key in Settings and Privacy (#11664) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** ``` ERROR Warning: Encountered two children with the same key, `0x89`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version. ``` ## **Related issues** Fixes: No issue, found while developing ## **Manual testing steps** 1. Add Polygon, and add a custom RPC Polygon 2. Go to Network Settings 3. Should not see duplicate key error in logs ## **Screenshots/Recordings** N/A ## **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. --- .../Views/Settings/IncomingTransactionsSettings/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/Views/Settings/IncomingTransactionsSettings/index.tsx b/app/components/Views/Settings/IncomingTransactionsSettings/index.tsx index c50618402510..34e0e70fa359 100644 --- a/app/components/Views/Settings/IncomingTransactionsSettings/index.tsx +++ b/app/components/Views/Settings/IncomingTransactionsSettings/index.tsx @@ -94,7 +94,7 @@ const IncomingTransactionsSettings = () => { supportedNetworks[chainId as keyof typeof supportedNetworks].domain; return ( { supportedNetworks[chainId as keyof typeof supportedNetworks].domain; return ( Date: Wed, 9 Oct 2024 15:24:57 +0530 Subject: [PATCH 02/46] feat: add account_network section to re-designed confirmation page (#11698) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Add account and network section to re-designed confirmation pages. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/11680 ## **Manual testing steps** 1. Enable re-designs locally 2. Go to test dapp 3. Submit personal sign and check page ## **Screenshots/Recordings** https://github.com/user-attachments/assets/a47c1f1b-4f69-472c-a35c-5804d46ed6ec ## **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 - [ ] 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. --- .../confirmations/Confirm/Confirm.styles.ts | 3 +- .../Views/confirmations/Confirm/Confirm.tsx | 2 + .../__snapshots__/Confirm.test.tsx.snap | 455 +++++++++++++----- .../AccountNetworkInfo.test.tsx | 14 + .../AccountNetworkInfo/AccountNetworkInfo.tsx | 25 + .../AccountNetworkInfoCollapsed.styles.ts | 33 ++ .../AccountNetworkInfoCollapsed.test.tsx | 14 + .../AccountNetworkInfoCollapsed.tsx | 64 +++ .../AccountNetworkInfoCollapsed.test.tsx.snap | 161 +++++++ .../AccountNetworkInfoCollapsed/index.ts | 1 + .../AccountNetworkInfoExpanded.test.tsx | 14 + .../AccountNetworkInfoExpanded.tsx | 44 ++ .../AccountNetworkInfoExpanded.test.tsx.snap | 299 ++++++++++++ .../AccountNetworkInfoExpanded/index.ts | 1 + .../AccountNetworkInfo.test.tsx.snap | 210 ++++++++ .../Confirm/AccountNetworkInfo/index.ts | 1 + .../Info/PersonalSign/PersonalSign.styles.ts | 19 - .../Info/PersonalSign/PersonalSign.tsx | 10 +- .../__snapshots__/PersonalSign.test.tsx.snap | 16 +- .../Info/__snapshots__/Info.test.tsx.snap | 16 +- .../ExpandableSection.styles.ts | 1 + .../ExpandableSection/ExpandableSection.tsx | 4 +- .../ExpandableSection.test.tsx.snap | 85 ++-- .../InfoRow/InfoSection/InfoSection.styles.ts | 1 + .../__snapshots__/InfoSection.test.tsx.snap | 1 + .../hooks/useAccountInfo.test.ts | 51 ++ .../confirmations/hooks/useAccountInfo.ts | 26 + .../useAddressBalance/useAddressBalance.ts | 2 +- locales/languages/en.json | 7 +- 29 files changed, 1356 insertions(+), 224 deletions(-) create mode 100644 app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfo.test.tsx create mode 100644 app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfo.tsx create mode 100644 app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoCollapsed/AccountNetworkInfoCollapsed.styles.ts create mode 100644 app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoCollapsed/AccountNetworkInfoCollapsed.test.tsx create mode 100644 app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoCollapsed/AccountNetworkInfoCollapsed.tsx create mode 100644 app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoCollapsed/__snapshots__/AccountNetworkInfoCollapsed.test.tsx.snap create mode 100644 app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoCollapsed/index.ts create mode 100644 app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/AccountNetworkInfoExpanded.test.tsx create mode 100644 app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/AccountNetworkInfoExpanded.tsx create mode 100644 app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/__snapshots__/AccountNetworkInfoExpanded.test.tsx.snap create mode 100644 app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/index.ts create mode 100644 app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/__snapshots__/AccountNetworkInfo.test.tsx.snap create mode 100644 app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/index.ts delete mode 100644 app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.styles.ts create mode 100644 app/components/Views/confirmations/hooks/useAccountInfo.test.ts create mode 100644 app/components/Views/confirmations/hooks/useAccountInfo.ts diff --git a/app/components/Views/confirmations/Confirm/Confirm.styles.ts b/app/components/Views/confirmations/Confirm/Confirm.styles.ts index b8bc55770eaa..b1fce0866eaf 100644 --- a/app/components/Views/confirmations/Confirm/Confirm.styles.ts +++ b/app/components/Views/confirmations/Confirm/Confirm.styles.ts @@ -15,8 +15,7 @@ const styleSheet = (params: { theme: Theme }) => { borderTopLeftRadius: 20, borderTopRightRadius: 20, paddingBottom: Device.isIphoneX() ? 20 : 0, - alignItems: 'center', - justifyContent: 'space-between' + justifyContent: 'space-between', }, }); }; diff --git a/app/components/Views/confirmations/Confirm/Confirm.tsx b/app/components/Views/confirmations/Confirm/Confirm.tsx index 04f1e8cd138a..e1a06fb45b22 100644 --- a/app/components/Views/confirmations/Confirm/Confirm.tsx +++ b/app/components/Views/confirmations/Confirm/Confirm.tsx @@ -3,6 +3,7 @@ import { View } from 'react-native'; import { useStyles } from '../../../../component-library/hooks'; import BottomModal from '../components/UI/BottomModal'; +import AccountNetworkInfo from '../components/Confirm/AccountNetworkInfo'; import Footer from '../components/Confirm/Footer'; import Info from '../components/Confirm/Info'; import Title from '../components/Confirm/Title'; @@ -22,6 +23,7 @@ const Confirm = () => { + <AccountNetworkInfo /> <Info /> </View> <Footer /> diff --git a/app/components/Views/confirmations/Confirm/__snapshots__/Confirm.test.tsx.snap b/app/components/Views/confirmations/Confirm/__snapshots__/Confirm.test.tsx.snap index f0b9e941e260..205601701b09 100644 --- a/app/components/Views/confirmations/Confirm/__snapshots__/Confirm.test.tsx.snap +++ b/app/components/Views/confirmations/Confirm/__snapshots__/Confirm.test.tsx.snap @@ -117,7 +117,6 @@ exports[`Confirm should match snapshot for personal sign 1`] = ` <View style={ { - "alignItems": "center", "backgroundColor": "#f2f4f6", "borderTopLeftRadius": 20, "borderTopRightRadius": 20, @@ -154,213 +153,413 @@ exports[`Confirm should match snapshot for personal sign 1`] = ` <View style={ { - "marginBottom": 10, + "alignItems": "center", + "backgroundColor": "#ffffff", + "borderColor": "#bbc0c566", + "borderRadius": 8, + "borderWidth": 1, + "display": "flex", + "flexDirection": "row", + "justifyContent": "space-between", + "marginBottom": 8, + "padding": 16, } } > <View style={ { - "backgroundColor": "#ffffff", - "borderColor": "#bbc0c566", - "borderRadius": 8, - "borderWidth": 1, - "padding": 8, + "display": "flex", + "flexDirection": "row", } } > <View + onLayout={[Function]} style={ { - "display": "flex", - "flexDirection": "row", - "flexWrap": "wrap", - "justifyContent": "space-between", - "paddingBottom": 8, - "paddingHorizontal": 8, + "alignSelf": "center", + "marginRight": 16, + "position": "relative", } } + testID="badge-wrapper-badge" > + <View> + <View + style={ + { + "backgroundColor": "#ffffff", + "borderRadius": 16, + "height": 32, + "overflow": "hidden", + "width": 32, + } + } + > + <Image + source={ + { + "uri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAACVBMVEX50jXBSBE1gSvxQ/7HAAAAA3RSTlP////6yOLMAABAi0lEQVR42gGAQH+/AAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBosMwASRXnygAAAAASUVORK5CYII=", + } + } + style={ + { + "flex": 1, + } + } + /> + </View> + </View> <View style={ { "alignItems": "center", - "display": "flex", - "flexDirection": "row", - "marginTop": 8, + "aspectRatio": 1, + "height": 0, + "justifyContent": "center", + "position": "absolute", + "right": 0, + "top": 0, + "transform": [ + { + "translateX": 0, + }, + { + "translateY": -0, + }, + ], } } > - <Text + <View + onLayout={[Function]} style={ { - "color": "#141618", - "fontFamily": "EuclidCircularB-Bold", - "fontSize": 14, - "fontWeight": "500", + "alignItems": "center", + "aspectRatio": 1, + "height": "50%", + "justifyContent": "center", + "maxHeight": 24, + "minHeight": 8, + "opacity": 0, } } + testID="badgenetwork" > - Request from - </Text> - <View> - <TouchableOpacity - accessible={true} - activeOpacity={1} - disabled={false} - onPress={[Function]} - onPressIn={[Function]} - onPressOut={[Function]} + <View style={ { "alignItems": "center", - "borderRadius": 8, - "height": 24, + "backgroundColor": "#ffffff", + "borderColor": "#ffffff", + "borderRadius": 16, + "borderWidth": 2, + "height": 32, "justifyContent": "center", - "opacity": 1, - "width": 24, + "overflow": "hidden", + "shadowColor": "#0000001a", + "shadowOffset": { + "height": 2, + "width": 0, + }, + "shadowOpacity": 1, + "shadowRadius": 4, + "transform": [ + { + "scale": 1, + }, + ], + "width": 32, } } - testID="tooltipTestId" > - <SvgMock - color="#9fa6ae" - height={16} - name="Info" + <Image + onError={[Function]} + resizeMode="contain" + source={ + { + "default": { + "uri": "MockImage", + }, + } + } style={ { - "height": 16, - "width": 16, + "height": 32, + "width": 32, } } - width={16} + testID="network-avatar-image" /> - </TouchableOpacity> - <Modal - animationType="none" - deviceHeight={null} - deviceWidth={null} - hardwareAccelerated={false} - hideModalContentWhileAnimating={false} - onBackdropPress={[Function]} - onModalHide={[Function]} - onModalWillHide={[Function]} - onModalWillShow={[Function]} - onRequestClose={[Function]} - onSwipeComplete={[Function]} - panResponderThreshold={4} - scrollHorizontal={false} - scrollOffset={0} - scrollOffsetMax={0} - scrollTo={null} - statusBarTranslucent={false} - supportedOrientations={ - [ - "portrait", - "landscape", - ] - } - swipeDirection="down" - swipeThreshold={100} - transparent={true} - visible={false} - /> + </View> </View> </View> - <View + </View> + <View> + <Text style={ { - "alignItems": "center", - "display": "flex", - "flexDirection": "row", + "color": "#141618", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "500", } } > - <Text - style={ - { - "color": "#141618", - "fontFamily": "EuclidCircularB-Regular", - "fontSize": 14, - "fontWeight": "400", - "marginTop": 8, - } + 0x935E...5477 + </Text> + <Text + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", } - > - metamask.github.io - </Text> - </View> + } + > + Ethereum Main Network + </Text> </View> </View> - <View + <TouchableOpacity + accessible={true} + activeOpacity={1} + disabled={false} + onPress={[Function]} + onPressIn={[Function]} + onPressOut={[Function]} style={ { - "backgroundColor": "#ffffff", - "borderColor": "#bbc0c566", + "alignItems": "center", "borderRadius": 8, - "borderWidth": 1, - "padding": 8, + "height": 24, + "justifyContent": "center", + "opacity": 1, + "width": 24, + } + } + testID="openButtonTestId" + > + <SvgMock + color="#9fa6ae" + height={16} + name="ArrowRight" + style={ + { + "height": 16, + "width": 16, + } + } + width={16} + /> + </TouchableOpacity> + </View> + <View + style={ + { + "backgroundColor": "#ffffff", + "borderColor": "#bbc0c566", + "borderRadius": 8, + "borderWidth": 1, + "marginBottom": 8, + "padding": 8, + } + } + > + <View + style={ + { + "display": "flex", + "flexDirection": "row", + "flexWrap": "wrap", + "justifyContent": "space-between", + "paddingBottom": 8, + "paddingHorizontal": 8, } } > <View style={ { + "alignItems": "center", "display": "flex", "flexDirection": "row", - "flexWrap": "wrap", - "justifyContent": "space-between", - "paddingBottom": 8, - "paddingHorizontal": 8, + "marginTop": 8, } } > - <View + <Text style={ { - "alignItems": "center", - "display": "flex", - "flexDirection": "row", - "marginTop": 8, + "color": "#141618", + "fontFamily": "EuclidCircularB-Bold", + "fontSize": 14, + "fontWeight": "500", } } > - <Text + Request from + </Text> + <View> + <TouchableOpacity + accessible={true} + activeOpacity={1} + disabled={false} + onPress={[Function]} + onPressIn={[Function]} + onPressOut={[Function]} style={ { - "color": "#141618", - "fontFamily": "EuclidCircularB-Bold", - "fontSize": 14, - "fontWeight": "500", + "alignItems": "center", + "borderRadius": 8, + "height": 24, + "justifyContent": "center", + "opacity": 1, + "width": 24, } } + testID="tooltipTestId" > - Message - </Text> + <SvgMock + color="#9fa6ae" + height={16} + name="Info" + style={ + { + "height": 16, + "width": 16, + } + } + width={16} + /> + </TouchableOpacity> + <Modal + animationType="none" + deviceHeight={null} + deviceWidth={null} + hardwareAccelerated={false} + hideModalContentWhileAnimating={false} + onBackdropPress={[Function]} + onModalHide={[Function]} + onModalWillHide={[Function]} + onModalWillShow={[Function]} + onRequestClose={[Function]} + onSwipeComplete={[Function]} + panResponderThreshold={4} + scrollHorizontal={false} + scrollOffset={0} + scrollOffsetMax={0} + scrollTo={null} + statusBarTranslucent={false} + supportedOrientations={ + [ + "portrait", + "landscape", + ] + } + swipeDirection="down" + swipeThreshold={100} + transparent={true} + visible={false} + /> </View> - <View + </View> + <View + style={ + { + "alignItems": "center", + "display": "flex", + "flexDirection": "row", + } + } + > + <Text style={ { - "alignItems": "center", - "display": "flex", - "flexDirection": "row", + "color": "#141618", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + "marginTop": 8, } } > - <Text - style={ - { - "color": "#141618", - "fontFamily": "EuclidCircularB-Regular", - "fontSize": 14, - "fontWeight": "400", - "marginTop": 8, - } + metamask.github.io + </Text> + </View> + </View> + </View> + <View + style={ + { + "backgroundColor": "#ffffff", + "borderColor": "#bbc0c566", + "borderRadius": 8, + "borderWidth": 1, + "marginBottom": 8, + "padding": 8, + } + } + > + <View + style={ + { + "display": "flex", + "flexDirection": "row", + "flexWrap": "wrap", + "justifyContent": "space-between", + "paddingBottom": 8, + "paddingHorizontal": 8, + } + } + > + <View + style={ + { + "alignItems": "center", + "display": "flex", + "flexDirection": "row", + "marginTop": 8, + } + } + > + <Text + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Bold", + "fontSize": 14, + "fontWeight": "500", } - > - Example \`personal_sign\` message - </Text> - </View> + } + > + Message + </Text> + </View> + <View + style={ + { + "alignItems": "center", + "display": "flex", + "flexDirection": "row", + } + } + > + <Text + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + "marginTop": 8, + } + } + > + Example \`personal_sign\` message + </Text> </View> </View> </View> diff --git a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfo.test.tsx b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfo.test.tsx new file mode 100644 index 000000000000..80feb4c447e9 --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfo.test.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import renderWithProvider from '../../../../../../util/test/renderWithProvider'; +import { personalSignatureConfirmationState } from '../../../../../../util/test/confirm-data-helpers'; +import AccountNetworkInfo from './AccountNetworkInfo'; + +describe('AccountNetworkInfo', () => { + it('should match snapshot for personal sign', async () => { + const container = renderWithProvider(<AccountNetworkInfo />, { + state: personalSignatureConfirmationState, + }); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfo.tsx b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfo.tsx new file mode 100644 index 000000000000..422b3829b71b --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfo.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +import { strings } from '../../../../../../../locales/i18n'; +import useApprovalRequest from '../../../hooks/useApprovalRequest'; +import ExpandableSection from '../../UI/ExpandableSection'; +import AccountNetworkInfoCollapsed from './AccountNetworkInfoCollapsed'; +import AccountNetworkInfoExpanded from './AccountNetworkInfoExpanded'; + +const AccountNetworkInfo = () => { + const { approvalRequest } = useApprovalRequest(); + + if (!approvalRequest) { + return null; + } + + return ( + <ExpandableSection + collapsedContent={<AccountNetworkInfoCollapsed />} + expandedContent={<AccountNetworkInfoExpanded />} + modalTitle={strings('confirm.details')} + /> + ); +}; + +export default AccountNetworkInfo; diff --git a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoCollapsed/AccountNetworkInfoCollapsed.styles.ts b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoCollapsed/AccountNetworkInfoCollapsed.styles.ts new file mode 100644 index 000000000000..c16028a06139 --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoCollapsed/AccountNetworkInfoCollapsed.styles.ts @@ -0,0 +1,33 @@ +import { StyleSheet } from 'react-native'; + +import { Theme } from '../../../../../../../util/theme/models'; +import { fontStyles } from '../../../../../../../styles/common'; + +const styleSheet = (params: { theme: Theme }) => { + const { theme } = params; + + return StyleSheet.create({ + container: { + display: 'flex', + flexDirection: 'row', + }, + badgeWrapper: { + marginRight: 16, + alignSelf: 'center', + }, + accountName: { + color: theme.colors.text.default, + ...fontStyles.normal, + fontSize: 14, + fontWeight: '500', + }, + networkName: { + color: theme.colors.text.default, + ...fontStyles.normal, + fontSize: 14, + fontWeight: '400', + }, + }); +}; + +export default styleSheet; diff --git a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoCollapsed/AccountNetworkInfoCollapsed.test.tsx b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoCollapsed/AccountNetworkInfoCollapsed.test.tsx new file mode 100644 index 000000000000..34e89e37ff95 --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoCollapsed/AccountNetworkInfoCollapsed.test.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import renderWithProvider from '../../../../../../../util/test/renderWithProvider'; +import { personalSignatureConfirmationState } from '../../../../../../../util/test/confirm-data-helpers'; +import AccountNetworkInfoCollapsed from './AccountNetworkInfoCollapsed'; + +describe('AccountNetworkInfoCollapsed', () => { + it('should match snapshot for personal sign', async () => { + const container = renderWithProvider(<AccountNetworkInfoCollapsed />, { + state: personalSignatureConfirmationState, + }); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoCollapsed/AccountNetworkInfoCollapsed.tsx b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoCollapsed/AccountNetworkInfoCollapsed.tsx new file mode 100644 index 000000000000..023f032cb61d --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoCollapsed/AccountNetworkInfoCollapsed.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { Text, View } from 'react-native'; +import { useSelector } from 'react-redux'; + +import Avatar, { + AvatarAccountType, + AvatarVariant, +} from '../../../../../../../component-library/components/Avatars/Avatar'; +import Badge, { + BadgeVariant, +} from '../../../../../../../component-library/components/Badges/Badge'; +import BadgeWrapper from '../../../../../../../component-library/components/Badges/BadgeWrapper'; +import { RootState } from '../../../../../../UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal.test'; +import { useStyles } from '../../../../../../../component-library/hooks'; +import { + selectNetworkImageSource, + selectNetworkName, +} from '../../../../../../../selectors/networkInfos'; +import useAccountInfo from '../../../../hooks/useAccountInfo'; +import useApprovalRequest from '../../../../hooks/useApprovalRequest'; +import styleSheet from './AccountNetworkInfoCollapsed.styles'; + +const AccountNetworkInfoCollapsed = () => { + const { approvalRequest } = useApprovalRequest(); + const networkName = useSelector(selectNetworkName); + const networkImage = useSelector(selectNetworkImageSource); + const useBlockieIcon = useSelector( + (state: RootState) => state.settings.useBlockieIcon, + ); + const fromAddress = approvalRequest?.requestData?.from; + const { accountName } = useAccountInfo(fromAddress); + const { styles } = useStyles(styleSheet, {}); + + return ( + <View style={styles.container}> + <BadgeWrapper + badgeElement={ + <Badge + variant={BadgeVariant.Network} + name={networkName} + imageSource={networkImage} + /> + } + style={styles.badgeWrapper} + > + <Avatar + variant={AvatarVariant.Account} + type={ + useBlockieIcon + ? AvatarAccountType.Blockies + : AvatarAccountType.JazzIcon + } + accountAddress={fromAddress} + /> + </BadgeWrapper> + <View> + <Text style={styles.accountName}>{accountName}</Text> + <Text style={styles.networkName}>{networkName}</Text> + </View> + </View> + ); +}; + +export default AccountNetworkInfoCollapsed; diff --git a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoCollapsed/__snapshots__/AccountNetworkInfoCollapsed.test.tsx.snap b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoCollapsed/__snapshots__/AccountNetworkInfoCollapsed.test.tsx.snap new file mode 100644 index 000000000000..ccdef8c4a1ff --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoCollapsed/__snapshots__/AccountNetworkInfoCollapsed.test.tsx.snap @@ -0,0 +1,161 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AccountNetworkInfoCollapsed should match snapshot for personal sign 1`] = ` +<View + style={ + { + "display": "flex", + "flexDirection": "row", + } + } +> + <View + onLayout={[Function]} + style={ + { + "alignSelf": "center", + "marginRight": 16, + "position": "relative", + } + } + testID="badge-wrapper-badge" + > + <View> + <View + style={ + { + "backgroundColor": "#ffffff", + "borderRadius": 16, + "height": 32, + "overflow": "hidden", + "width": 32, + } + } + > + <Image + source={ + { + "uri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAACVBMVEX50jXBSBE1gSvxQ/7HAAAAA3RSTlP////6yOLMAABAi0lEQVR42gGAQH+/AAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBosMwASRXnygAAAAASUVORK5CYII=", + } + } + style={ + { + "flex": 1, + } + } + /> + </View> + </View> + <View + style={ + { + "alignItems": "center", + "aspectRatio": 1, + "height": 0, + "justifyContent": "center", + "position": "absolute", + "right": 0, + "top": 0, + "transform": [ + { + "translateX": 0, + }, + { + "translateY": -0, + }, + ], + } + } + > + <View + onLayout={[Function]} + style={ + { + "alignItems": "center", + "aspectRatio": 1, + "height": "50%", + "justifyContent": "center", + "maxHeight": 24, + "minHeight": 8, + "opacity": 0, + } + } + testID="badgenetwork" + > + <View + style={ + { + "alignItems": "center", + "backgroundColor": "#ffffff", + "borderColor": "#ffffff", + "borderRadius": 16, + "borderWidth": 2, + "height": 32, + "justifyContent": "center", + "overflow": "hidden", + "shadowColor": "#0000001a", + "shadowOffset": { + "height": 2, + "width": 0, + }, + "shadowOpacity": 1, + "shadowRadius": 4, + "transform": [ + { + "scale": 1, + }, + ], + "width": 32, + } + } + > + <Image + onError={[Function]} + resizeMode="contain" + source={ + { + "default": { + "uri": "MockImage", + }, + } + } + style={ + { + "height": 32, + "width": 32, + } + } + testID="network-avatar-image" + /> + </View> + </View> + </View> + </View> + <View> + <Text + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "500", + } + } + > + 0x935E...5477 + </Text> + <Text + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + } + } + > + Ethereum Main Network + </Text> + </View> +</View> +`; diff --git a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoCollapsed/index.ts b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoCollapsed/index.ts new file mode 100644 index 000000000000..aaf4b2c12a14 --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoCollapsed/index.ts @@ -0,0 +1 @@ +export { default } from './AccountNetworkInfoCollapsed'; diff --git a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/AccountNetworkInfoExpanded.test.tsx b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/AccountNetworkInfoExpanded.test.tsx new file mode 100644 index 000000000000..49d6476b76fb --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/AccountNetworkInfoExpanded.test.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import renderWithProvider from '../../../../../../../util/test/renderWithProvider'; +import { personalSignatureConfirmationState } from '../../../../../../../util/test/confirm-data-helpers'; +import AccountNetworkInfoExpanded from './AccountNetworkInfoExpanded'; + +describe('AccountNetworkInfoExpanded', () => { + it('should match snapshot for personal sign', async () => { + const container = renderWithProvider(<AccountNetworkInfoExpanded />, { + state: personalSignatureConfirmationState, + }); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/AccountNetworkInfoExpanded.tsx b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/AccountNetworkInfoExpanded.tsx new file mode 100644 index 000000000000..8dd9eb4c6267 --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/AccountNetworkInfoExpanded.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { View } from 'react-native'; +import { useSelector } from 'react-redux'; + +import { strings } from '../../../../../../../../locales/i18n'; +import { selectRpcUrl } from '../../../../../../../selectors/networkController'; +import { selectNetworkName } from '../../../../../../../selectors/networkInfos'; +import useAccountInfo from '../../../../hooks/useAccountInfo'; +import useApprovalRequest from '../../../../hooks/useApprovalRequest'; +import InfoSection from '../../../UI/InfoRow/InfoSection'; +import InfoRow from '../../../UI/InfoRow'; +import InfoURL from '../../../UI/InfoRow/InfoValue/InfoURL'; + +// todo: use value component for address, network, currency value +const AccountNetworkInfoExpanded = () => { + const { approvalRequest } = useApprovalRequest(); + const networkName = useSelector(selectNetworkName); + const networkRpcUrl = useSelector(selectRpcUrl); + const fromAddress = approvalRequest?.requestData?.from; + const { accountAddress, accountBalance } = useAccountInfo(fromAddress); + + return ( + <View> + <InfoSection> + <InfoRow label={strings('confirm.account')}>{accountAddress}</InfoRow> + <InfoRow label={strings('confirm.balance')}>{accountBalance}</InfoRow> + </InfoSection> + <InfoSection> + <InfoRow + label={strings('confirm.network')} + // todo: add tooltip content when available + tooltip={strings('confirm.network')} + > + {networkName} + </InfoRow> + <InfoRow label={strings('confirm.rpc_url')}> + <InfoURL url={networkRpcUrl} /> + </InfoRow> + </InfoSection> + </View> + ); +}; + +export default AccountNetworkInfoExpanded; diff --git a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/__snapshots__/AccountNetworkInfoExpanded.test.tsx.snap b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/__snapshots__/AccountNetworkInfoExpanded.test.tsx.snap new file mode 100644 index 000000000000..367b688358bc --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/__snapshots__/AccountNetworkInfoExpanded.test.tsx.snap @@ -0,0 +1,299 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AccountNetworkInfoExpanded should match snapshot for personal sign 1`] = ` +<View> + <View + style={ + { + "backgroundColor": "#ffffff", + "borderColor": "#bbc0c566", + "borderRadius": 8, + "borderWidth": 1, + "marginBottom": 8, + "padding": 8, + } + } + > + <View + style={ + { + "display": "flex", + "flexDirection": "row", + "flexWrap": "wrap", + "justifyContent": "space-between", + "paddingBottom": 8, + "paddingHorizontal": 8, + } + } + > + <View + style={ + { + "alignItems": "center", + "display": "flex", + "flexDirection": "row", + "marginTop": 8, + } + } + > + <Text + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Bold", + "fontSize": 14, + "fontWeight": "500", + } + } + > + Account + </Text> + </View> + <Text + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + "marginTop": 8, + } + } + > + 0x935E73EDb9fF52E23BaC7F7e043A1ecD06d05477 + </Text> + </View> + <View + style={ + { + "display": "flex", + "flexDirection": "row", + "flexWrap": "wrap", + "justifyContent": "space-between", + "paddingBottom": 8, + "paddingHorizontal": 8, + } + } + > + <View + style={ + { + "alignItems": "center", + "display": "flex", + "flexDirection": "row", + "marginTop": 8, + } + } + > + <Text + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Bold", + "fontSize": 14, + "fontWeight": "500", + } + } + > + Balance + </Text> + </View> + <Text + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + "marginTop": 8, + } + } + > + 0 ETH + </Text> + </View> + </View> + <View + style={ + { + "backgroundColor": "#ffffff", + "borderColor": "#bbc0c566", + "borderRadius": 8, + "borderWidth": 1, + "marginBottom": 8, + "padding": 8, + } + } + > + <View + style={ + { + "display": "flex", + "flexDirection": "row", + "flexWrap": "wrap", + "justifyContent": "space-between", + "paddingBottom": 8, + "paddingHorizontal": 8, + } + } + > + <View + style={ + { + "alignItems": "center", + "display": "flex", + "flexDirection": "row", + "marginTop": 8, + } + } + > + <Text + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Bold", + "fontSize": 14, + "fontWeight": "500", + } + } + > + Network + </Text> + <View> + <TouchableOpacity + accessible={true} + activeOpacity={1} + disabled={false} + onPress={[Function]} + onPressIn={[Function]} + onPressOut={[Function]} + style={ + { + "alignItems": "center", + "borderRadius": 8, + "height": 24, + "justifyContent": "center", + "opacity": 1, + "width": 24, + } + } + testID="tooltipTestId" + > + <SvgMock + color="#9fa6ae" + height={16} + name="Info" + style={ + { + "height": 16, + "width": 16, + } + } + width={16} + /> + </TouchableOpacity> + <Modal + animationType="none" + deviceHeight={null} + deviceWidth={null} + hardwareAccelerated={false} + hideModalContentWhileAnimating={false} + onBackdropPress={[Function]} + onModalHide={[Function]} + onModalWillHide={[Function]} + onModalWillShow={[Function]} + onRequestClose={[Function]} + onSwipeComplete={[Function]} + panResponderThreshold={4} + scrollHorizontal={false} + scrollOffset={0} + scrollOffsetMax={0} + scrollTo={null} + statusBarTranslucent={false} + supportedOrientations={ + [ + "portrait", + "landscape", + ] + } + swipeDirection="down" + swipeThreshold={100} + transparent={true} + visible={false} + /> + </View> + </View> + <Text + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + "marginTop": 8, + } + } + > + Ethereum Main Network + </Text> + </View> + <View + style={ + { + "display": "flex", + "flexDirection": "row", + "flexWrap": "wrap", + "justifyContent": "space-between", + "paddingBottom": 8, + "paddingHorizontal": 8, + } + } + > + <View + style={ + { + "alignItems": "center", + "display": "flex", + "flexDirection": "row", + "marginTop": 8, + } + } + > + <Text + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Bold", + "fontSize": 14, + "fontWeight": "500", + } + } + > + RPC URL + </Text> + </View> + <View + style={ + { + "alignItems": "center", + "display": "flex", + "flexDirection": "row", + } + } + > + <Text + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + "marginTop": 8, + } + } + /> + </View> + </View> + </View> +</View> +`; diff --git a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/index.ts b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/index.ts new file mode 100644 index 000000000000..1a50f1b35e2e --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/index.ts @@ -0,0 +1 @@ +export { default } from './AccountNetworkInfoExpanded'; diff --git a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/__snapshots__/AccountNetworkInfo.test.tsx.snap b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/__snapshots__/AccountNetworkInfo.test.tsx.snap new file mode 100644 index 000000000000..50edfd8c5423 --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/__snapshots__/AccountNetworkInfo.test.tsx.snap @@ -0,0 +1,210 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AccountNetworkInfo should match snapshot for personal sign 1`] = ` +<View + style={ + { + "alignItems": "center", + "backgroundColor": "#ffffff", + "borderColor": "#bbc0c566", + "borderRadius": 8, + "borderWidth": 1, + "display": "flex", + "flexDirection": "row", + "justifyContent": "space-between", + "marginBottom": 8, + "padding": 16, + } + } +> + <View + style={ + { + "display": "flex", + "flexDirection": "row", + } + } + > + <View + onLayout={[Function]} + style={ + { + "alignSelf": "center", + "marginRight": 16, + "position": "relative", + } + } + testID="badge-wrapper-badge" + > + <View> + <View + style={ + { + "backgroundColor": "#ffffff", + "borderRadius": 16, + "height": 32, + "overflow": "hidden", + "width": 32, + } + } + > + <Image + source={ + { + "uri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAACVBMVEX50jXBSBE1gSvxQ/7HAAAAA3RSTlP////6yOLMAABAi0lEQVR42gGAQH+/AAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBosMwASRXnygAAAAASUVORK5CYII=", + } + } + style={ + { + "flex": 1, + } + } + /> + </View> + </View> + <View + style={ + { + "alignItems": "center", + "aspectRatio": 1, + "height": 0, + "justifyContent": "center", + "position": "absolute", + "right": 0, + "top": 0, + "transform": [ + { + "translateX": 0, + }, + { + "translateY": -0, + }, + ], + } + } + > + <View + onLayout={[Function]} + style={ + { + "alignItems": "center", + "aspectRatio": 1, + "height": "50%", + "justifyContent": "center", + "maxHeight": 24, + "minHeight": 8, + "opacity": 0, + } + } + testID="badgenetwork" + > + <View + style={ + { + "alignItems": "center", + "backgroundColor": "#ffffff", + "borderColor": "#ffffff", + "borderRadius": 16, + "borderWidth": 2, + "height": 32, + "justifyContent": "center", + "overflow": "hidden", + "shadowColor": "#0000001a", + "shadowOffset": { + "height": 2, + "width": 0, + }, + "shadowOpacity": 1, + "shadowRadius": 4, + "transform": [ + { + "scale": 1, + }, + ], + "width": 32, + } + } + > + <Image + onError={[Function]} + resizeMode="contain" + source={ + { + "default": { + "uri": "MockImage", + }, + } + } + style={ + { + "height": 32, + "width": 32, + } + } + testID="network-avatar-image" + /> + </View> + </View> + </View> + </View> + <View> + <Text + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "500", + } + } + > + 0x935E...5477 + </Text> + <Text + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + } + } + > + Ethereum Main Network + </Text> + </View> + </View> + <TouchableOpacity + accessible={true} + activeOpacity={1} + disabled={false} + onPress={[Function]} + onPressIn={[Function]} + onPressOut={[Function]} + style={ + { + "alignItems": "center", + "borderRadius": 8, + "height": 24, + "justifyContent": "center", + "opacity": 1, + "width": 24, + } + } + testID="openButtonTestId" + > + <SvgMock + color="#9fa6ae" + height={16} + name="ArrowRight" + style={ + { + "height": 16, + "width": 16, + } + } + width={16} + /> + </TouchableOpacity> +</View> +`; diff --git a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/index.ts b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/index.ts new file mode 100644 index 000000000000..27b69126baf4 --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/index.ts @@ -0,0 +1 @@ +export { default } from './AccountNetworkInfo'; diff --git a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.styles.ts b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.styles.ts deleted file mode 100644 index ed5f996741ae..000000000000 --- a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.styles.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { StyleSheet } from 'react-native'; - -import { Colors } from '../../../../../../../util/theme/models'; -import { fontStyles } from '../../../../../../../styles/common'; - -const createStyles = (colors: Colors) => - StyleSheet.create({ - titleContainer: { - marginBottom: 10, - }, - title: { - color: colors.text.default, - ...fontStyles.bold, - fontSize: 18, - fontWeight: '700' - } - }); - -export default createStyles; diff --git a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.tsx b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.tsx index 9da6c29f1ab4..84002f9a3e8e 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.tsx +++ b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.tsx @@ -1,28 +1,22 @@ import React from 'react'; -import { View } from 'react-native'; import { hexToText } from '@metamask/controller-utils'; import { sanitizeString } from '../../../../../../../util/string'; import { strings } from '../../../../../../../../locales/i18n'; -import { useTheme } from '../../../../../../../util/theme'; import useApprovalRequest from '../../../../hooks/useApprovalRequest'; import InfoSection from '../../../UI/InfoRow/InfoSection'; import InfoRow from '../../../UI/InfoRow'; import InfoURL from '../../../UI/InfoRow/InfoValue/InfoURL'; -import createStyles from './PersonalSign.styles'; const PersonalSign = () => { const { approvalRequest } = useApprovalRequest(); - const { colors } = useTheme(); - - const styles = createStyles(colors); if (!approvalRequest) { return null; } return ( - <View style={styles.titleContainer}> + <> <InfoSection> <InfoRow label={strings('confirm.request_from')} @@ -38,7 +32,7 @@ const PersonalSign = () => { /> </InfoRow> </InfoSection> - </View> + </> ); }; diff --git a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/__snapshots__/PersonalSign.test.tsx.snap b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/__snapshots__/PersonalSign.test.tsx.snap index b85a77a49796..8688f2e7a9f4 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/__snapshots__/PersonalSign.test.tsx.snap +++ b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/__snapshots__/PersonalSign.test.tsx.snap @@ -1,13 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Title should match snapshot 1`] = ` -<View - style={ - { - "marginBottom": 10, - } - } -> +[ <View style={ { @@ -15,6 +9,7 @@ exports[`Title should match snapshot 1`] = ` "borderColor": "#bbc0c566", "borderRadius": 8, "borderWidth": 1, + "marginBottom": 8, "padding": 8, } } @@ -141,7 +136,7 @@ exports[`Title should match snapshot 1`] = ` </Text> </View> </View> - </View> + </View>, <View style={ { @@ -149,6 +144,7 @@ exports[`Title should match snapshot 1`] = ` "borderColor": "#bbc0c566", "borderRadius": 8, "borderWidth": 1, + "marginBottom": 8, "padding": 8, } } @@ -212,6 +208,6 @@ exports[`Title should match snapshot 1`] = ` </Text> </View> </View> - </View> -</View> + </View>, +] `; diff --git a/app/components/Views/confirmations/components/Confirm/Info/__snapshots__/Info.test.tsx.snap b/app/components/Views/confirmations/components/Confirm/Info/__snapshots__/Info.test.tsx.snap index acdb8c80bab3..b5021db7505c 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/__snapshots__/Info.test.tsx.snap +++ b/app/components/Views/confirmations/components/Confirm/Info/__snapshots__/Info.test.tsx.snap @@ -1,13 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Info should match snapshot for personal sign 1`] = ` -<View - style={ - { - "marginBottom": 10, - } - } -> +[ <View style={ { @@ -15,6 +9,7 @@ exports[`Info should match snapshot for personal sign 1`] = ` "borderColor": "#bbc0c566", "borderRadius": 8, "borderWidth": 1, + "marginBottom": 8, "padding": 8, } } @@ -141,7 +136,7 @@ exports[`Info should match snapshot for personal sign 1`] = ` </Text> </View> </View> - </View> + </View>, <View style={ { @@ -149,6 +144,7 @@ exports[`Info should match snapshot for personal sign 1`] = ` "borderColor": "#bbc0c566", "borderRadius": 8, "borderWidth": 1, + "marginBottom": 8, "padding": 8, } } @@ -212,6 +208,6 @@ exports[`Info should match snapshot for personal sign 1`] = ` </Text> </View> </View> - </View> -</View> + </View>, +] `; diff --git a/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.styles.ts b/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.styles.ts index ef76fe538228..f71a429a3f53 100644 --- a/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.styles.ts +++ b/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.styles.ts @@ -17,6 +17,7 @@ const styleSheet = (params: { theme: Theme }) => { justifyContent: 'space-between', alignItems: 'center', padding: 16, + marginBottom: 8, }, modalContent: { backgroundColor: theme.colors.background.alternative, diff --git a/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.tsx b/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.tsx index 9f7f65b308ad..f0dd5b89a106 100644 --- a/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.tsx +++ b/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.tsx @@ -31,7 +31,7 @@ const ExpandableSection = ({ const [expanded, setExpanded] = useState(false); return ( - <View> + <> <View style={styles.container}> {collapsedContent} <ButtonIcon @@ -59,7 +59,7 @@ const ExpandableSection = ({ </View> </BottomModal> )} - </View> + </> ); }; diff --git a/app/components/Views/confirmations/components/UI/ExpandableSection/__snapshots__/ExpandableSection.test.tsx.snap b/app/components/Views/confirmations/components/UI/ExpandableSection/__snapshots__/ExpandableSection.test.tsx.snap index 2ea8c40eae77..c0f52f65f01b 100644 --- a/app/components/Views/confirmations/components/UI/ExpandableSection/__snapshots__/ExpandableSection.test.tsx.snap +++ b/app/components/Views/confirmations/components/UI/ExpandableSection/__snapshots__/ExpandableSection.test.tsx.snap @@ -1,59 +1,58 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ExpandableSection should match snapshot for simple ExpandableSection 1`] = ` -<View> - <View +<View + style={ + { + "alignItems": "center", + "backgroundColor": "#ffffff", + "borderColor": "#bbc0c566", + "borderRadius": 8, + "borderWidth": 1, + "display": "flex", + "flexDirection": "row", + "justifyContent": "space-between", + "marginBottom": 8, + "padding": 16, + } + } +> + <View> + <Text> + Open + </Text> + </View> + <TouchableOpacity + accessible={true} + activeOpacity={1} + disabled={false} + onPress={[Function]} + onPressIn={[Function]} + onPressOut={[Function]} style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderColor": "#bbc0c566", "borderRadius": 8, - "borderWidth": 1, - "display": "flex", - "flexDirection": "row", - "justifyContent": "space-between", - "padding": 16, + "height": 24, + "justifyContent": "center", + "opacity": 1, + "width": 24, } } + testID="openButtonTestId" > - <View> - <Text> - Open - </Text> - </View> - <TouchableOpacity - accessible={true} - activeOpacity={1} - disabled={false} - onPress={[Function]} - onPressIn={[Function]} - onPressOut={[Function]} + <SvgMock + color="#9fa6ae" + height={16} + name="ArrowRight" style={ { - "alignItems": "center", - "borderRadius": 8, - "height": 24, - "justifyContent": "center", - "opacity": 1, - "width": 24, + "height": 16, + "width": 16, } } - testID="openButtonTestId" - > - <SvgMock - color="#9fa6ae" - height={16} - name="ArrowRight" - style={ - { - "height": 16, - "width": 16, - } - } - width={16} - /> - </TouchableOpacity> - </View> + width={16} + /> + </TouchableOpacity> </View> `; diff --git a/app/components/Views/confirmations/components/UI/InfoRow/InfoSection/InfoSection.styles.ts b/app/components/Views/confirmations/components/UI/InfoRow/InfoSection/InfoSection.styles.ts index 46a075bcf362..09d645493f58 100644 --- a/app/components/Views/confirmations/components/UI/InfoRow/InfoSection/InfoSection.styles.ts +++ b/app/components/Views/confirmations/components/UI/InfoRow/InfoSection/InfoSection.styles.ts @@ -12,6 +12,7 @@ const styleSheet = (params: { theme: Theme }) => { borderRadius: 8, borderWidth: 1, padding: 8, + marginBottom: 8, }, }); }; diff --git a/app/components/Views/confirmations/components/UI/InfoRow/InfoSection/__snapshots__/InfoSection.test.tsx.snap b/app/components/Views/confirmations/components/UI/InfoRow/InfoSection/__snapshots__/InfoSection.test.tsx.snap index 1e393db2e3b2..af5a0b9e505c 100644 --- a/app/components/Views/confirmations/components/UI/InfoRow/InfoSection/__snapshots__/InfoSection.test.tsx.snap +++ b/app/components/Views/confirmations/components/UI/InfoRow/InfoSection/__snapshots__/InfoSection.test.tsx.snap @@ -8,6 +8,7 @@ exports[`InfoSection should match snapshot for simple text value 1`] = ` "borderColor": "#bbc0c566", "borderRadius": 8, "borderWidth": 1, + "marginBottom": 8, "padding": 8, } } diff --git a/app/components/Views/confirmations/hooks/useAccountInfo.test.ts b/app/components/Views/confirmations/hooks/useAccountInfo.test.ts new file mode 100644 index 000000000000..7e4c64c233fc --- /dev/null +++ b/app/components/Views/confirmations/hooks/useAccountInfo.test.ts @@ -0,0 +1,51 @@ +import { + DeepPartial, + renderHookWithProvider, +} from '../../../../util/test/renderWithProvider'; +import { backgroundState } from '../../../../util/test/initial-root-state'; +import { createMockAccountsControllerState } from '../../../../util/test/accountsControllerTestUtils'; +import { RootState } from '../../../../reducers'; +import useAccountInfo from './useAccountInfo'; + +const MOCK_ADDRESS = '0x0'; + +const MOCK_ACCOUNTS_CONTROLLER_STATE = createMockAccountsControllerState([ + MOCK_ADDRESS, +]); + +const mockInitialState: DeepPartial<RootState> = { + settings: {}, + engine: { + backgroundState: { + ...backgroundState, + AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE, + AccountTrackerController: { + accounts: { + [MOCK_ADDRESS]: { + balance: '0x5', + }, + }, + }, + }, + }, +}; + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: (fn: (state: DeepPartial<RootState>) => unknown) => + fn(mockInitialState), +})); + +describe('useAccountInfo', () => { + it('should return existing address from accounts controller', async () => { + const { result } = renderHookWithProvider( + () => useAccountInfo(MOCK_ADDRESS), + { + state: mockInitialState, + }, + ); + expect(result?.current?.accountName).toEqual('Account 1'); + expect(result?.current?.accountAddress).toEqual('0x0'); + expect(result?.current?.accountBalance).toEqual('< 0.00001 ETH'); + }); +}); diff --git a/app/components/Views/confirmations/hooks/useAccountInfo.ts b/app/components/Views/confirmations/hooks/useAccountInfo.ts new file mode 100644 index 000000000000..d07991f2f7d7 --- /dev/null +++ b/app/components/Views/confirmations/hooks/useAccountInfo.ts @@ -0,0 +1,26 @@ +import { toChecksumAddress } from 'ethereumjs-util'; +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; + +import useAddressBalance from '../../../../components/hooks/useAddressBalance/useAddressBalance'; +import { selectInternalAccounts } from '../../../../selectors/accountsController'; +import { renderAccountName } from '../../../../util/address'; + +const useAccountInfo = (address: string) => { + const internalAccounts = useSelector(selectInternalAccounts); + const activeAddress = toChecksumAddress(address); + const { addressBalance: accountBalance } = useAddressBalance( + undefined, + address, + ); + + const accountName = useMemo( + () => + activeAddress ? renderAccountName(activeAddress, internalAccounts) : '', + [internalAccounts, activeAddress], + ); + + return { accountName, accountAddress: activeAddress, accountBalance }; +}; + +export default useAccountInfo; diff --git a/app/components/hooks/useAddressBalance/useAddressBalance.ts b/app/components/hooks/useAddressBalance/useAddressBalance.ts index 40591640db73..af95e0187e32 100644 --- a/app/components/hooks/useAddressBalance/useAddressBalance.ts +++ b/app/components/hooks/useAddressBalance/useAddressBalance.ts @@ -16,7 +16,7 @@ import { selectSelectedInternalAccountChecksummedAddress } from '../../../select import { Asset } from './useAddressBalance.types'; const useAddressBalance = ( - asset: Asset, + asset?: Asset, address?: string, dontWatchAsset?: boolean, ) => { diff --git a/locales/languages/en.json b/locales/languages/en.json index fba94457215d..5d23b38caf39 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -3409,6 +3409,11 @@ }, "request_from": "Request from", "message": "Message", - "personal_sign_tooltip": "This site is asking for your signature" + "personal_sign_tooltip": "This site is asking for your signature", + "details": "Details", + "account": "Account", + "balance": "Balance", + "network": "Network", + "rpc_url": "RPC URL" } } From f35f1023f9d48449c820cbfbb375d6fd689755dc Mon Sep 17 00:00:00 2001 From: sahar-fehri <sahar.fehri@consensys.net> Date: Wed, 9 Oct 2024 12:26:25 +0200 Subject: [PATCH 03/46] fix: fix asset symbol for incoming tx (#11495) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR fixes the assets symbol displayed in the incoming transaction notification. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/7669 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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. --- app/core/NotificationManager.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/core/NotificationManager.js b/app/core/NotificationManager.js index 3bcf35ee01b1..568d3e97d6d5 100644 --- a/app/core/NotificationManager.js +++ b/app/core/NotificationManager.js @@ -16,8 +16,10 @@ import { import { safeToChecksumAddress } from '../util/address'; import ReviewManager from './ReviewManager'; -import { selectChainId } from '../selectors/networkController'; +import { selectChainId, selectTicker } from '../selectors/networkController'; import { store } from '../store'; +import { useSelector } from 'react-redux'; +import { getTicker } from '../../app/util/transactions'; export const constructTitleAndMessage = (notification) => { let title, message; switch (notification.type) { @@ -412,6 +414,7 @@ class NotificationManager { ); const chainId = selectChainId(store.getState()); + const ticker = useSelector(selectTicker); /// Find the incoming TX const transactions = TransactionController.getTransactions({ @@ -442,7 +445,7 @@ class NotificationManager { nonce: `${hexToBN(txs[0].txParams.nonce).toString()}`, amount: `${renderFromWei(hexToBN(txs[0].txParams.value))}`, id: txs[0]?.id, - assetType: strings('unit.eth'), + assetType: getTicker(ticker), }, autoHide: true, duration: 7000, From ea3948cf72aac80c3daa0fc80cd70ace5d523d57 Mon Sep 17 00:00:00 2001 From: Salim TOUBAL <salim.toubal@outlook.com> Date: Wed, 9 Oct 2024 13:51:19 +0200 Subject: [PATCH 04/46] feat: multi rpc modal (#11685) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- 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](https://github.com/MetaMask/metamask-mobile/pull/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. --- app/actions/security/index.ts | 16 +- .../CellSelectWithMenu/CellSelectWithMenu.tsx | 16 +- .../CellSelectWithMenu.test.tsx.snap | 10 +- .../ListItemMultiSelectButton.styles.ts | 5 +- .../ListItemMultiSelectButton.test.tsx | 8 +- .../ListItemMultiSelectButton.tsx | 40 +- .../ListItemMultiSelectButton.types.ts | 24 +- .../ListItemMultiSelectButton.test.tsx.snap | 10 +- app/components/Nav/App/index.js | 10 +- app/components/Nav/Main/index.js | 39 ++ app/components/UI/AssetIcon/index.test.tsx | 1 + .../MultiRpcModal/MultiRpcModal.constants.ts | 30 + .../MultiRpcModal/MultiRpcModal.styles.ts | 40 ++ .../MultiRpcModal/MultiRpcModal.test.tsx | 61 ++ .../Views/MultiRpcModal/MultiRpcModal.tsx | 139 ++++ .../__snapshots__/MultiRpcModal.test.tsx.snap | 595 ++++++++++++++++++ app/components/Views/MultiRpcModal/index.ts | 1 + .../NetworkSelector/NetworkSelector.styles.ts | 2 +- .../Views/NetworkSelector/NetworkSelector.tsx | 24 +- app/components/Views/Wallet/index.tsx | 3 +- app/constants/navigation/Routes.ts | 1 + app/core/Analytics/MetaMetrics.events.ts | 6 + app/images/networks1.png | Bin 0 -> 5923 bytes app/selectors/preferencesController.ts | 6 + app/util/test/initial-background-state.json | 1 + index.js | 1 - locales/languages/en.json | 5 + ...tamask+preferences-controller+11.0.0.patch | 58 +- 28 files changed, 1095 insertions(+), 57 deletions(-) create mode 100644 app/components/Views/MultiRpcModal/MultiRpcModal.constants.ts create mode 100644 app/components/Views/MultiRpcModal/MultiRpcModal.styles.ts create mode 100644 app/components/Views/MultiRpcModal/MultiRpcModal.test.tsx create mode 100644 app/components/Views/MultiRpcModal/MultiRpcModal.tsx create mode 100644 app/components/Views/MultiRpcModal/__snapshots__/MultiRpcModal.test.tsx.snap create mode 100644 app/components/Views/MultiRpcModal/index.ts create mode 100644 app/images/networks1.png diff --git a/app/actions/security/index.ts b/app/actions/security/index.ts index e0e87a0ef550..661c1d1119d0 100644 --- a/app/actions/security/index.ts +++ b/app/actions/security/index.ts @@ -8,6 +8,7 @@ export enum ActionType { SET_AUTOMATIC_SECURITY_CHECKS_MODAL_OPEN = 'SET_AUTOMATIC_SECURITY_CHECKS_MODAL_OPEN', SET_DATA_COLLECTION_FOR_MARKETING = 'SET_DATA_COLLECTION_FOR_MARKETING', SET_NFT_AUTO_DETECTION_MODAL_OPEN = 'SET_NFT_AUTO_DETECTION_MODAL_OPEN', + SET_MULTI_RPC_MIGRATION_MODAL_OPEN = 'SET_MULTI_RPC_MIGRATION_MODAL_OPEN', } export interface AllowLoginWithRememberMeUpdated @@ -35,6 +36,11 @@ export interface SetNftAutoDetectionModalOpen open: boolean; } +export interface SetMultiRpcMigrationModalOpen + extends ReduxAction<ActionType.SET_MULTI_RPC_MIGRATION_MODAL_OPEN> { + open: boolean; +} + export interface SetDataCollectionForMarketing extends ReduxAction<ActionType.SET_DATA_COLLECTION_FOR_MARKETING> { enabled: boolean; @@ -46,7 +52,8 @@ export type Action = | UserSelectedAutomaticSecurityChecksOptions | SetAutomaticSecurityChecksModalOpen | SetDataCollectionForMarketing - | SetNftAutoDetectionModalOpen; + | SetNftAutoDetectionModalOpen + | SetMultiRpcMigrationModalOpen; export const setAllowLoginWithRememberMe = ( enabled: boolean, @@ -82,6 +89,13 @@ export const setNftAutoDetectionModalOpen = ( open, }); +export const setMultiRpcMigrationModalOpen = ( + open: boolean, +): SetMultiRpcMigrationModalOpen => ({ + type: ActionType.SET_MULTI_RPC_MIGRATION_MODAL_OPEN, + open, +}); + export const setDataCollectionForMarketing = (enabled: boolean) => ({ type: ActionType.SET_DATA_COLLECTION_FOR_MARKETING, enabled, diff --git a/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.tsx b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.tsx index ff8df60913ae..e18ed0465956 100644 --- a/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.tsx +++ b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.tsx @@ -33,6 +33,7 @@ const CellSelectWithMenu = ({ tagLabel, isSelected = false, children, + withAvatar = true, ...props }: CellSelectWithMenuProps) => { const { styles } = useStyles(styleSheet, { style }); @@ -46,12 +47,15 @@ const CellSelectWithMenu = ({ > <View style={styles.cellBase}> {/* DEV Note: Account Avatar should be replaced with Avatar with Badge whenever available */} - <Avatar - style={styles.avatar} - testID={CellModalSelectorsIDs.BASE_AVATAR} - size={DEFAULT_CELLBASE_AVATAR_SIZE} - {...avatarProps} - /> + {withAvatar ? ( + <Avatar + style={styles.avatar} + testID={CellModalSelectorsIDs.BASE_AVATAR} + size={DEFAULT_CELLBASE_AVATAR_SIZE} + {...avatarProps} + /> + ) : null} + <View style={styles.cellBaseInfo}> <Text numberOfLines={1} diff --git a/app/component-library/components-temp/CellSelectWithMenu/__snapshots__/CellSelectWithMenu.test.tsx.snap b/app/component-library/components-temp/CellSelectWithMenu/__snapshots__/CellSelectWithMenu.test.tsx.snap index 3e2ad44c55f9..f86327146856 100644 --- a/app/component-library/components-temp/CellSelectWithMenu/__snapshots__/CellSelectWithMenu.test.tsx.snap +++ b/app/component-library/components-temp/CellSelectWithMenu/__snapshots__/CellSelectWithMenu.test.tsx.snap @@ -19,7 +19,7 @@ exports[`CellSelectWithMenu should render with default settings correctly 1`] = "opacity": 1, "padding": 16, "position": "relative", - "width": "95%", + "width": "90%", "zIndex": 1, } } @@ -287,7 +287,13 @@ exports[`CellSelectWithMenu should render with default settings correctly 1`] = </View> </View> </TouchableOpacity> - <View> + <View + style={ + { + "paddingHorizontal": 20, + } + } + > <TouchableOpacity accessibilityRole="button" accessible={true} diff --git a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.styles.ts b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.styles.ts index b3a9a9bde27b..d3ff43c9cb2a 100644 --- a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.styles.ts +++ b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.styles.ts @@ -28,7 +28,7 @@ const styleSheet = (params: { position: 'relative', opacity: isDisabled ? 0.5 : 1, padding: 16, - width: '95%', + width: '90%', zIndex: 1, } as ViewStyle, style, @@ -86,6 +86,9 @@ const styleSheet = (params: { paddingLeft: 8, paddingTop: 32, }, + buttonIcon: { + paddingHorizontal: 20, + }, }); }; diff --git a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.test.tsx b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.test.tsx index 7e73e164d40d..8ad0d0016d8b 100644 --- a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.test.tsx +++ b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.test.tsx @@ -41,7 +41,9 @@ describe('ListItemMultiSelectButton', () => { const { getByRole } = render( <ListItemMultiSelectButton onPress={mockOnPress} - onButtonClick={mockOnPress} + buttonProps={{ + onButtonClick: mockOnPress, + }} > <View /> </ListItemMultiSelectButton>, @@ -64,7 +66,9 @@ describe('ListItemMultiSelectButton', () => { const { getByTestId } = render( <ListItemMultiSelectButton buttonIcon={IconName.Check} - onButtonClick={mockOnButtonClick} + buttonProps={{ + onButtonClick: mockOnButtonClick, + }} > <View /> </ListItemMultiSelectButton>, diff --git a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.tsx b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.tsx index e8610e457352..a3cb0d079ffc 100644 --- a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.tsx +++ b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.tsx @@ -20,6 +20,12 @@ import { IconColor, IconName, } from '../../../component-library/components/Icons/Icon'; +import Button, { + ButtonSize, + ButtonVariants, + ButtonWidthTypes, +} from '../../../component-library/components/Buttons/Button'; +import { TextVariant } from '../../../component-library/components/Texts/Text'; const ListItemMultiSelectButton: React.FC<ListItemMultiSelectButtonProps> = ({ style, @@ -27,7 +33,9 @@ const ListItemMultiSelectButton: React.FC<ListItemMultiSelectButtonProps> = ({ isDisabled = false, children, gap = DEFAULT_LISTITEMMULTISELECT_GAP, + showButtonIcon = true, buttonIcon = IconName.MoreVertical, + buttonProps, ...props }) => { const { styles } = useStyles(styleSheet, { @@ -55,15 +63,29 @@ const ListItemMultiSelectButton: React.FC<ListItemMultiSelectButtonProps> = ({ </View> )} </TouchableOpacity> - <View> - <ButtonIcon - iconName={buttonIcon} - iconColor={IconColor.Default} - testID={BUTTON_TEST_ID} - onPress={props.onButtonClick} - accessibilityRole="button" - /> - </View> + {showButtonIcon ? ( + <View style={styles.buttonIcon}> + <ButtonIcon + iconName={buttonIcon} + iconColor={IconColor.Default} + testID={BUTTON_TEST_ID} + onPress={buttonProps?.onButtonClick} + accessibilityRole="button" + /> + </View> + ) : null} + {buttonProps?.textButton ? ( + <View> + <Button + variant={ButtonVariants.Link} + onPress={buttonProps?.onButtonClick as () => void} + labelTextVariant={TextVariant.BodyMD} + size={ButtonSize.Lg} + width={ButtonWidthTypes.Auto} + label={buttonProps?.textButton} + /> + </View> + ) : null} </View> ); }; diff --git a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.types.ts b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.types.ts index 7178221c24cc..f92853d98758 100644 --- a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.types.ts +++ b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.types.ts @@ -27,14 +27,30 @@ export interface ListItemMultiSelectButtonProps buttonIcon?: IconName; /** - * Optional button onClick function + * Optional button onClick rpc modal function */ - onButtonClick?: ((event: GestureResponderEvent) => void) | undefined; + onTextClick?: (() => void) | undefined; /** - * Optional button onClick rpc modal function + * Optional property to add avatar */ - onTextClick?: (() => void) | undefined; + withAvatar?: boolean; + + /** + * Optional property to show icon + */ + showButtonIcon?: boolean; + + buttonProps?: { + /** + * Optional button onClick function + */ + onButtonClick?: ((event: GestureResponderEvent) => void) | undefined; + /** + * Optional property to show text button + */ + textButton?: string | null; + }; } /** diff --git a/app/component-library/components-temp/ListItemMultiSelectButton/__snapshots__/ListItemMultiSelectButton.test.tsx.snap b/app/component-library/components-temp/ListItemMultiSelectButton/__snapshots__/ListItemMultiSelectButton.test.tsx.snap index 0baaf1dd60bb..a1a0b239ae69 100644 --- a/app/component-library/components-temp/ListItemMultiSelectButton/__snapshots__/ListItemMultiSelectButton.test.tsx.snap +++ b/app/component-library/components-temp/ListItemMultiSelectButton/__snapshots__/ListItemMultiSelectButton.test.tsx.snap @@ -19,7 +19,7 @@ exports[`ListItemMultiSelectButton should render correctly with default props 1` "opacity": 1, "padding": 16, "position": "relative", - "width": "95%", + "width": "90%", "zIndex": 1, } } @@ -52,7 +52,13 @@ exports[`ListItemMultiSelectButton should render correctly with default props 1` </View> </View> </TouchableOpacity> - <View> + <View + style={ + { + "paddingHorizontal": 20, + } + } + > <TouchableOpacity accessibilityRole="button" accessible={true} diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js index 27bc4c34d8d0..fd14e13f8579 100644 --- a/app/components/Nav/App/index.js +++ b/app/components/Nav/App/index.js @@ -127,6 +127,7 @@ import { SnapsExecutionWebView } from '../../../lib/snaps'; import OptionsSheet from '../../UI/SelectOptionSheet/OptionsSheet'; import FoxLoader from '../../../components/UI/FoxLoader'; import { AppStateEventProcessor } from '../../../core/AppStateEventListener'; +import MultiRpcModal from '../../../components/Views/MultiRpcModal/MultiRpcModal'; const clearStackNavigatorOptions = { headerShown: false, @@ -396,7 +397,6 @@ const App = (props) => { }); }, [handleDeeplink]); - useEffect(() => { if (navigator) { // Initialize deep link manager @@ -687,11 +687,17 @@ const App = (props) => { name={Routes.MODAL.NFT_AUTO_DETECTION_MODAL} component={NFTAutoDetectionModal} /> + {isNetworkUiRedesignEnabled() ? ( + <Stack.Screen + name={Routes.MODAL.MULTI_RPC_MIGRATION_MODAL} + component={MultiRpcModal} + /> + ) : null} + <Stack.Screen name={Routes.SHEET.SHOW_TOKEN_ID} component={ShowTokenIdSheet} /> - <Stack.Screen name={Routes.SHEET.ORIGIN_SPAM_MODAL} component={OriginSpamModal} diff --git a/app/components/Nav/Main/index.js b/app/components/Nav/Main/index.js index 5ee817d1970c..c2c9fad44ba3 100644 --- a/app/components/Nav/Main/index.js +++ b/app/components/Nav/Main/index.js @@ -59,6 +59,7 @@ import { useMinimumVersions } from '../../hooks/MinimumVersions'; import navigateTermsOfUse from '../../../util/termsOfUse/termsOfUse'; import { selectChainId, + selectNetworkConfigurations, selectProviderConfig, selectProviderType, } from '../../../selectors/networkController'; @@ -80,6 +81,7 @@ import { startIncomingTransactionPolling, stopIncomingTransactionPolling, } from '../../../util/transaction-controller'; +import isNetworkUiRedesignEnabled from '../../../util/networks/isNetworkUiRedesignEnabled'; const Stack = createStackNavigator(); @@ -232,8 +234,10 @@ const Main = (props) => { * Current network */ const providerConfig = useSelector(selectProviderConfig); + const networkConfigurations = useSelector(selectNetworkConfigurations); const networkName = useSelector(selectNetworkName); const previousProviderConfig = useRef(undefined); + const previousNetworkConfigurations = useRef(undefined); const { toastRef } = useContext(ToastContext); const networkImage = useSelector(selectNetworkImageSource); @@ -259,6 +263,41 @@ const Main = (props) => { previousProviderConfig.current = providerConfig; }, [providerConfig, networkName, networkImage, toastRef]); + // Show add network confirmation. + useEffect(() => { + if (!isNetworkUiRedesignEnabled()) return; + + // Memoized values to avoid recalculations + const currentNetworkValues = Object.values(networkConfigurations); + const previousNetworkValues = Object.values( + previousNetworkConfigurations.current ?? {}, + ); + + if ( + previousNetworkValues.length && + currentNetworkValues.length !== previousNetworkValues.length + ) { + // Find the newly added network + const newNetwork = currentNetworkValues.find( + (network) => !previousNetworkValues.includes(network), + ); + + toastRef?.current?.showToast({ + variant: ToastVariants.Plain, + labelOptions: [ + { + label: `${newNetwork?.name ?? strings('asset_details.network')} `, + isBold: true, + }, + { label: strings('toast.network_added') }, + ], + networkImageSource: networkImage, + }); + } + + previousNetworkConfigurations.current = networkConfigurations; + }, [networkConfigurations, networkName, networkImage, toastRef]); + useEffect(() => { if (locale.current !== I18n.locale) { locale.current = I18n.locale; diff --git a/app/components/UI/AssetIcon/index.test.tsx b/app/components/UI/AssetIcon/index.test.tsx index f0451a2a5f5a..84c0941722a7 100644 --- a/app/components/UI/AssetIcon/index.test.tsx +++ b/app/components/UI/AssetIcon/index.test.tsx @@ -15,6 +15,7 @@ const mockInitialState = { selectedAddress: '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3', useTokenDetection: true, useNftDetection: false, + showMultiRpcModal: false, displayNftMedia: true, useSafeChainsListValidation: false, isMultiAccountBalancesEnabled: true, diff --git a/app/components/Views/MultiRpcModal/MultiRpcModal.constants.ts b/app/components/Views/MultiRpcModal/MultiRpcModal.constants.ts new file mode 100644 index 000000000000..8746e74f4dd7 --- /dev/null +++ b/app/components/Views/MultiRpcModal/MultiRpcModal.constants.ts @@ -0,0 +1,30 @@ +export const SAMPLE_NETWORK_CONFIGURATIONS = { + '0x1': { + blockExplorerUrls: [], + chainId: '0x1', + defaultRpcEndpointIndex: 0, + name: 'Mainnet', + nativeCurrency: 'ETH', + rpcEndpoints: [ + { + networkClientId: 'mainnet', + type: 'infura', + url: 'https://mainnet.infura.io/v3/{infuraProjectId}', + }, + ], + }, + '0x5': { + blockExplorerUrls: [], + chainId: '0x5', + defaultRpcEndpointIndex: 0, + name: 'Goerli', + nativeCurrency: 'GoerliETH', + rpcEndpoints: [ + { + networkClientId: 'goerli', + type: 'infura', + url: 'https://goerli.infura.io/v3/{infuraProjectId}', + }, + ], + }, +}; diff --git a/app/components/Views/MultiRpcModal/MultiRpcModal.styles.ts b/app/components/Views/MultiRpcModal/MultiRpcModal.styles.ts new file mode 100644 index 000000000000..45839816a57e --- /dev/null +++ b/app/components/Views/MultiRpcModal/MultiRpcModal.styles.ts @@ -0,0 +1,40 @@ +import { StyleSheet } from 'react-native'; + +/** + * Style sheet function for NFT auto detection modal component. + * + * @returns StyleSheet object. + */ +const styleSheet = () => + StyleSheet.create({ + container: { + alignItems: 'center', + }, + image: { + width: 102.64, + height: 102.64, + }, + description: { + marginLeft: 32, + marginRight: 32, + }, + content: { + height: '80%', + }, + textDescription: { + textAlign: 'center', + }, + textContainer: { + marginLeft: 16, + marginRight: 16, + paddingVertical: 16, + }, + buttonsContainer: { + marginLeft: 16, + marginRight: 16, + paddingVertical: 16, + }, + spacer: { height: 8 }, + }); + +export default styleSheet; diff --git a/app/components/Views/MultiRpcModal/MultiRpcModal.test.tsx b/app/components/Views/MultiRpcModal/MultiRpcModal.test.tsx new file mode 100644 index 000000000000..60d6cc8789aa --- /dev/null +++ b/app/components/Views/MultiRpcModal/MultiRpcModal.test.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import MultiRpcModal from './MultiRpcModal'; +import renderWithProvider, { + DeepPartial, +} from '../../../util/test/renderWithProvider'; +import { createStackNavigator } from '@react-navigation/stack'; +import Routes from '../../../constants/navigation/Routes'; +import Engine from '../../../core/Engine'; +import { fireEvent } from '@testing-library/react-native'; +import { RootState } from 'app/reducers'; +import { backgroundState } from '../../../util/test/initial-root-state'; + +const setShowMultiRpcModalSpy = jest.spyOn( + Engine.context.PreferencesController, + 'setShowMultiRpcModal', +); + +jest.mock('../../../core/Engine', () => ({ + context: { + PreferencesController: { + setShowMultiRpcModal: jest.fn(), + }, + }, +})); + +const initialState = { + engine: { + backgroundState, + }, +}; + +const Stack = createStackNavigator(); + +const renderComponent = (state: DeepPartial<RootState> = {}) => + renderWithProvider( + <Stack.Navigator> + <Stack.Screen name={Routes.MODAL.MULTI_RPC_MIGRATION_MODAL}> + {() => <MultiRpcModal />} + </Stack.Screen> + </Stack.Navigator>, + { state }, + ); + +describe('MultiRpcModal', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('render matches snapshot', () => { + const { toJSON } = renderComponent(initialState); + expect(toJSON()).toMatchSnapshot(); + }); + + it('calls setShowMultiRpcModal and trackEvent when clicking on allow button', () => { + const { getByTestId } = renderComponent(initialState); + const allowButton = getByTestId('allow'); + + fireEvent.press(allowButton); + expect(setShowMultiRpcModalSpy).toHaveBeenCalledWith(false); + }); +}); diff --git a/app/components/Views/MultiRpcModal/MultiRpcModal.tsx b/app/components/Views/MultiRpcModal/MultiRpcModal.tsx new file mode 100644 index 000000000000..71845b62a199 --- /dev/null +++ b/app/components/Views/MultiRpcModal/MultiRpcModal.tsx @@ -0,0 +1,139 @@ +import React, { useRef, useCallback } from 'react'; +import { ScrollView } from 'react-native-gesture-handler'; +import BottomSheet, { + BottomSheetRef, +} from '../../../component-library/components/BottomSheets/BottomSheet'; +import { strings } from '../../../../locales/i18n'; +import { useStyles } from '../../../component-library/hooks'; +import styleSheet from './MultiRpcModal.styles'; +import SheetHeader from '../../../component-library/components/Sheet/SheetHeader'; +import Text from '../../../component-library/components/Texts/Text'; +import { View, Image } from 'react-native'; +import { NftDetectionModalSelectorsIDs } from '../../../../e2e/selectors/Modals/NftDetectionModal.selectors'; + +import Button, { + ButtonSize, + ButtonVariants, + ButtonWidthTypes, +} from '../../../component-library/components/Buttons/Button'; +import { useNavigation } from '@react-navigation/native'; +import Engine from '../../../core/Engine'; +import { useMetrics } from '../../../components/hooks/useMetrics'; +import { MetaMetricsEvents } from '../../../core/Analytics'; +import { + selectChainId, + selectNetworkConfigurations, +} from '../../../selectors/networkController'; +import { useSelector } from 'react-redux'; +import Cell, { + CellVariant, +} from '../../../component-library/components/Cells/Cell'; +import { + AvatarSize, + AvatarVariant, +} from '../../../component-library/components/Avatars/Avatar'; +import { IconName } from '../../../component-library/components/Icons/Icon'; +import { getNetworkImageSource } from '../../../util/networks'; +import Routes from '../../../constants/navigation/Routes'; + +// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires, import/no-commonjs +const networkImage = require('../../../images/networks1.png'); + +const MultiRpcModal = () => { + const { styles } = useStyles(styleSheet, {}); + const sheetRef = useRef<BottomSheetRef>(null); + const navigation = useNavigation(); + const chainId = useSelector(selectChainId); + const networkConfigurations = useSelector(selectNetworkConfigurations); + const { trackEvent } = useMetrics(); + const { navigate } = useNavigation(); + + const dismissMultiRpcModalMigration = useCallback(() => { + const { PreferencesController } = Engine.context; + PreferencesController.setShowMultiRpcModal(false); + trackEvent(MetaMetricsEvents.MULTI_RPC_MIGRATION_MODAL_ACCEPTED, { + chainId, + }); + + if (sheetRef?.current) { + sheetRef.current.onCloseBottomSheet(); + } else { + navigation.goBack(); + } + }, [trackEvent, chainId, navigation]); + + return ( + <BottomSheet ref={sheetRef}> + <SheetHeader title={'Network RPCs Updated'} /> + <View + testID={NftDetectionModalSelectorsIDs.CONTAINER} + style={styles.textContainer} + > + <ScrollView style={styles.content}> + <View style={styles.description}> + <Text style={styles.textDescription}> + {strings('multi_rpc_migration_modal.description')} + </Text> + </View> + <View style={styles.container}> + <Image source={networkImage} style={styles.image} /> + </View> + + <View> + {Object.values(networkConfigurations).map( + (networkConfiguration, index) => ( + <Cell + key={index} + variant={CellVariant.SelectWithMenu} + title={ + networkConfiguration.nickname || + networkConfiguration.chainId + } + secondaryText={networkConfiguration.rpcUrl} + avatarProps={{ + variant: AvatarVariant.Network, + name: + networkConfiguration.nickname || + networkConfiguration.chainId, + //@ts-expect-error - The utils/network file is still JS and this function expects a networkType, and should be optional + imageSource: getNetworkImageSource({ + chainId: networkConfiguration.chainId, + }), + size: AvatarSize.Sm, + }} + isSelected={false} + buttonIcon={IconName.MoreVertical} + showButtonIcon={false} + buttonProps={{ + textButton: strings('transaction.edit'), + onButtonClick: () => { + sheetRef.current?.onCloseBottomSheet(() => { + navigate(Routes.ADD_NETWORK, { + shouldNetworkSwitchPopToWallet: false, + shouldShowPopularNetworks: false, + network: networkConfiguration.rpcUrl, + }); + }); + }, + }} + /> + ), + )} + </View> + </ScrollView> + <View style={styles.buttonsContainer}> + <Button + testID={NftDetectionModalSelectorsIDs.ALLOW_BUTTON} + variant={ButtonVariants.Primary} + size={ButtonSize.Lg} + width={ButtonWidthTypes.Full} + label={strings('multi_rpc_migration_modal.accept')} + onPress={() => dismissMultiRpcModalMigration()} + /> + </View> + </View> + </BottomSheet> + ); +}; + +export default MultiRpcModal; diff --git a/app/components/Views/MultiRpcModal/__snapshots__/MultiRpcModal.test.tsx.snap b/app/components/Views/MultiRpcModal/__snapshots__/MultiRpcModal.test.tsx.snap new file mode 100644 index 000000000000..7d48d5e290ad --- /dev/null +++ b/app/components/Views/MultiRpcModal/__snapshots__/MultiRpcModal.test.tsx.snap @@ -0,0 +1,595 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MultiRpcModal render matches snapshot 1`] = ` +<View + style={ + { + "flex": 1, + } + } +> + <RNCSafeAreaProvider + onInsetsChange={[Function]} + style={ + [ + { + "flex": 1, + }, + undefined, + ] + } + > + <View + collapsable={false} + pointerEvents="box-none" + style={ + { + "zIndex": 1, + } + } + > + <View + accessibilityElementsHidden={false} + importantForAccessibility="auto" + onLayout={[Function]} + pointerEvents="box-none" + style={null} + > + <View + collapsable={false} + pointerEvents="box-none" + style={ + { + "bottom": 0, + "left": 0, + "opacity": 1, + "position": "absolute", + "right": 0, + "top": 0, + "zIndex": 0, + } + } + > + <View + collapsable={false} + style={ + { + "backgroundColor": "rgb(255, 255, 255)", + "borderBottomColor": "rgb(216, 216, 216)", + "flex": 1, + "shadowColor": "rgb(216, 216, 216)", + "shadowOffset": { + "height": 0.5, + "width": 0, + }, + "shadowOpacity": 0.85, + "shadowRadius": 0, + } + } + /> + </View> + <View + collapsable={false} + pointerEvents="box-none" + style={ + { + "height": 64, + "maxHeight": undefined, + "minHeight": undefined, + "opacity": undefined, + "transform": undefined, + } + } + > + <View + pointerEvents="none" + style={ + { + "height": 20, + } + } + /> + <View + pointerEvents="box-none" + style={ + { + "alignItems": "center", + "flex": 1, + "flexDirection": "row", + "justifyContent": "center", + } + } + > + <View + collapsable={false} + pointerEvents="box-none" + style={ + { + "marginHorizontal": 16, + "opacity": 1, + } + } + > + <Text + accessibilityRole="header" + aria-level="1" + collapsable={false} + numberOfLines={1} + onLayout={[Function]} + style={ + { + "color": "rgb(28, 28, 30)", + "fontSize": 17, + "fontWeight": "600", + } + } + > + MultiRPcMigrationModal + </Text> + </View> + </View> + </View> + </View> + </View> + <RNSScreenContainer + onLayout={[Function]} + style={ + { + "flex": 1, + } + } + > + <RNSScreen + activityState={2} + collapsable={false} + gestureResponseDistance={ + { + "bottom": -1, + "end": -1, + "start": -1, + "top": -1, + } + } + pointerEvents="box-none" + style={ + { + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + } + } + > + <View + collapsable={false} + style={ + { + "opacity": 1, + } + } + /> + <View + accessibilityElementsHidden={false} + closing={false} + gestureVelocityImpact={0.3} + importantForAccessibility="auto" + onClose={[Function]} + onGestureBegin={[Function]} + onGestureCanceled={[Function]} + onGestureEnd={[Function]} + onOpen={[Function]} + onTransition={[Function]} + pointerEvents="box-none" + style={ + [ + { + "overflow": undefined, + }, + { + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + }, + ] + } + transitionSpec={ + { + "close": { + "animation": "spring", + "config": { + "damping": 500, + "mass": 3, + "overshootClamping": true, + "restDisplacementThreshold": 10, + "restSpeedThreshold": 10, + "stiffness": 1000, + }, + }, + "open": { + "animation": "spring", + "config": { + "damping": 500, + "mass": 3, + "overshootClamping": true, + "restDisplacementThreshold": 10, + "restSpeedThreshold": 10, + "stiffness": 1000, + }, + }, + } + } + > + <View + collapsable={false} + needsOffscreenAlphaCompositing={false} + pointerEvents="box-none" + style={ + { + "flex": 1, + } + } + > + <View + collapsable={false} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + style={ + { + "flex": 1, + "transform": [ + { + "translateX": 0, + }, + { + "translateX": 0, + }, + ], + } + } + > + <View + collapsable={false} + pointerEvents="none" + style={ + { + "backgroundColor": "rgb(242, 242, 242)", + "bottom": 0, + "left": 0, + "position": "absolute", + "shadowColor": "#000", + "shadowOffset": { + "height": 1, + "width": -1, + }, + "shadowOpacity": 0.3, + "shadowRadius": 5, + "top": 0, + "width": 3, + } + } + /> + <View + style={ + [ + { + "flex": 1, + "overflow": "hidden", + }, + [ + { + "backgroundColor": "rgb(242, 242, 242)", + }, + undefined, + ], + ] + } + > + <View + style={ + { + "flex": 1, + "flexDirection": "column-reverse", + } + } + > + <View + style={ + { + "flex": 1, + } + } + > + <View + onLayout={[Function]} + style={ + [ + { + "bottom": 0, + "justifyContent": "flex-end", + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + }, + { + "paddingBottom": 0, + }, + ] + } + > + <View + style={ + [ + { + "backgroundColor": "#00000099", + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + }, + { + "opacity": 0, + }, + ] + } + > + <TouchableOpacity + onPress={[Function]} + style={ + { + "flex": 1, + } + } + /> + </View> + <View + onLayout={[Function]} + style={ + [ + { + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + }, + { + "paddingBottom": 0, + }, + ] + } + > + <View + collapsable={false} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onLayout={[Function]} + style={ + [ + { + "backgroundColor": "#ffffff", + "borderColor": "#bbc0c566", + "borderTopLeftRadius": 8, + "borderTopRightRadius": 8, + "borderWidth": 1, + "maxHeight": 1314, + "overflow": "hidden", + "paddingBottom": 0, + "shadowColor": "#0000001a", + "shadowOffset": { + "height": 2, + "width": 0, + }, + "shadowOpacity": 1, + "shadowRadius": 40, + }, + { + "transform": [ + { + "translateY": 1334, + }, + ], + }, + ] + } + > + <View + style={ + { + "alignItems": "center", + "alignSelf": "stretch", + "padding": 4, + } + } + > + <View + style={ + { + "backgroundColor": "#bbc0c566", + "borderRadius": 2, + "height": 4, + "width": 40, + } + } + /> + </View> + <View + style={ + { + "alignItems": "center", + "backgroundColor": "#ffffff", + "flexDirection": "row", + "height": 32, + "justifyContent": "space-between", + "margin": 16, + } + } + > + <View + style={ + { + "flex": 1, + } + } + /> + <Text + accessibilityRole="text" + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Bold", + "fontSize": 18, + "fontWeight": "700", + "letterSpacing": 0, + "lineHeight": 24, + } + } + > + Network RPCs Updated + </Text> + <View + style={ + { + "alignItems": "flex-end", + "flex": 1, + } + } + /> + </View> + <View + style={ + { + "marginLeft": 16, + "marginRight": 16, + "paddingVertical": 16, + } + } + testID="nft-detection-modal" + > + <RCTScrollView + collapsable={false} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + style={ + { + "height": "80%", + } + } + > + <View> + <View + style={ + { + "marginLeft": 32, + "marginRight": 32, + } + } + > + <Text + accessibilityRole="text" + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + "letterSpacing": 0, + "lineHeight": 22, + "textAlign": "center", + } + } + > + We now support multiple RPCs for a single network. Your most recent RPC has been selected as the default one to resolve conflicting information + </Text> + </View> + <View + style={ + { + "alignItems": "center", + } + } + > + <Image + source={ + { + "default": { + "uri": "MockImage", + }, + } + } + style={ + { + "height": 102.64, + "width": 102.64, + } + } + /> + </View> + <View /> + </View> + </RCTScrollView> + <View + style={ + { + "marginLeft": 16, + "marginRight": 16, + "paddingVertical": 16, + } + } + > + <TouchableOpacity + accessibilityRole="button" + accessible={true} + activeOpacity={1} + onPress={[Function]} + onPressIn={[Function]} + onPressOut={[Function]} + style={ + { + "alignItems": "center", + "alignSelf": "stretch", + "backgroundColor": "#0376c9", + "borderRadius": 24, + "flexDirection": "row", + "height": 48, + "justifyContent": "center", + "paddingHorizontal": 16, + } + } + testID="allow" + > + <Text + accessibilityRole="text" + style={ + { + "color": "#ffffff", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + "letterSpacing": 0, + "lineHeight": 22, + } + } + > + Accept + </Text> + </TouchableOpacity> + </View> + </View> + </View> + </View> + </View> + </View> + </View> + </View> + </View> + </View> + </View> + </RNSScreen> + </RNSScreenContainer> + </RNCSafeAreaProvider> +</View> +`; diff --git a/app/components/Views/MultiRpcModal/index.ts b/app/components/Views/MultiRpcModal/index.ts new file mode 100644 index 000000000000..5da9eeeddaa8 --- /dev/null +++ b/app/components/Views/MultiRpcModal/index.ts @@ -0,0 +1 @@ +export { default } from './MultiRpcModal'; diff --git a/app/components/Views/NetworkSelector/NetworkSelector.styles.ts b/app/components/Views/NetworkSelector/NetworkSelector.styles.ts index 583517830a60..f04457d2b89a 100644 --- a/app/components/Views/NetworkSelector/NetworkSelector.styles.ts +++ b/app/components/Views/NetworkSelector/NetworkSelector.styles.ts @@ -24,7 +24,7 @@ const createStyles = (colors: Colors) => }, rpcMenu: { display: 'flex', - flexDirection: 'row', + flexDirection: 'column', justifyContent: 'center', }, cellBorder: { borderWidth: 0, paddingVertical: 4 }, diff --git a/app/components/Views/NetworkSelector/NetworkSelector.tsx b/app/components/Views/NetworkSelector/NetworkSelector.tsx index 8c95314ef17d..87e6b01901cd 100644 --- a/app/components/Views/NetworkSelector/NetworkSelector.tsx +++ b/app/components/Views/NetworkSelector/NetworkSelector.tsx @@ -386,8 +386,10 @@ const NetworkSelector = () => { onPress={() => onNetworkChange(MAINNET)} style={styles.networkCell} buttonIcon={IconName.MoreVertical} - onButtonClick={() => { - openModal(chainId, false, MAINNET, true); + buttonProps={{ + onButtonClick: () => { + openModal(chainId, false, MAINNET, true); + }, }} // TODO: Substitute with the new network controller's RPC array. onTextClick={() => @@ -441,8 +443,10 @@ const NetworkSelector = () => { style={styles.networkCell} buttonIcon={IconName.MoreVertical} secondaryText={hideKeyFromUrl(LINEA_DEFAULT_RPC_URL)} - onButtonClick={() => { - openModal(chainId, false, LINEA_MAINNET, true); + buttonProps={{ + onButtonClick: () => { + openModal(chainId, false, LINEA_MAINNET, true); + }, }} // TODO: Substitute with the new network controller's RPC array. onTextClick={() => @@ -503,8 +507,10 @@ const NetworkSelector = () => { style={styles.networkCell} buttonIcon={IconName.MoreVertical} secondaryText={hideProtocolFromUrl(hideKeyFromUrl(rpcUrl))} - onButtonClick={() => { - openModal(chainId, true, rpcUrl, false); + buttonProps={{ + onButtonClick: () => { + openModal(chainId, true, rpcUrl, false); + }, }} // TODO: Substitute with the new network controller's RPC array. onTextClick={() => @@ -573,8 +579,10 @@ const NetworkSelector = () => { onPress={() => onNetworkChange(networkType)} style={styles.networkCell} buttonIcon={IconName.MoreVertical} - onButtonClick={() => { - openModal(chainId, false, networkType, true); + buttonProps={{ + onButtonClick: () => { + openModal(chainId, false, networkType, true); + }, }} /> ); diff --git a/app/components/Views/Wallet/index.tsx b/app/components/Views/Wallet/index.tsx index 886a36d645af..88a4be334bb6 100644 --- a/app/components/Views/Wallet/index.tsx +++ b/app/components/Views/Wallet/index.tsx @@ -406,8 +406,7 @@ const Wallet = ({ useEffect( () => { requestAnimationFrame(async () => { - const { AccountTrackerController } = - Engine.context; + const { AccountTrackerController } = Engine.context; AccountTrackerController.refresh(); }); }, diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts index 75d0fcdd28d4..acfa58343545 100644 --- a/app/constants/navigation/Routes.ts +++ b/app/constants/navigation/Routes.ts @@ -51,6 +51,7 @@ const Routes = { SRP_REVEAL_QUIZ: 'SRPRevealQuiz', WALLET_ACTIONS: 'WalletActions', NFT_AUTO_DETECTION_MODAL: 'NFTAutoDetectionModal', + MULTI_RPC_MIGRATION_MODAL: 'MultiRPcMigrationModal', }, ONBOARDING: { ROOT_NAV: 'OnboardingRootNav', diff --git a/app/core/Analytics/MetaMetrics.events.ts b/app/core/Analytics/MetaMetrics.events.ts index 9752f08c64d5..d15622a6aac1 100644 --- a/app/core/Analytics/MetaMetrics.events.ts +++ b/app/core/Analytics/MetaMetrics.events.ts @@ -368,6 +368,9 @@ enum EVENT_NAME { NFT_AUTO_DETECTION_ENABLED = 'nft_autodetection_enabled', PRIMARY_CURRENCY_TOGGLE = 'primary_currency_toggle', LOGIN_DOWNLOAD_LOGS = 'Download State Logs Button Clicked', + + // network + MULTI_RPC_MIGRATION_MODAL_ACCEPTED = 'multi_rpc_migration_modal_accepted', } enum ACTIONS { @@ -839,6 +842,9 @@ const events = { NFT_AUTO_DETECTION_MODAL_ENABLE: generateOpt( EVENT_NAME.NFT_AUTO_DETECTION_ENABLED_MODAL, ), + MULTI_RPC_MIGRATION_MODAL_ACCEPTED: generateOpt( + EVENT_NAME.MULTI_RPC_MIGRATION_MODAL_ACCEPTED, + ), NFT_AUTO_DETECTION_MODAL_DISABLE: generateOpt( EVENT_NAME.NFT_AUTO_DETECTION_DISBLED_MODAL, ), diff --git a/app/images/networks1.png b/app/images/networks1.png new file mode 100644 index 0000000000000000000000000000000000000000..780470e87810c91b5e531e635e89229bf8406aae GIT binary patch literal 5923 zcma)=Ra+YjkcH9W6e#Xi+#QM(D-tvWcXzkq5VW{!D8+)i2X~j^E+0?`PSFB|?LN=` zfW0_#X69nf-CVriG}IJuFv&3y5D;*b6y<>b-1eXB(BJ*5XCxOK{|v)TQQs2*0qev6 z3K1a}Li%rs=m}JiL8zUfg#Q~*Y^7DD5fB=ZuwKkj5fF&mmE@#BUlGrZu+ojS{a)|= zwnsMuj%HMBWSAHkFzAxyP(Ijy){rAtM&-=lFk?M702&x_pb}bhZdB=*(;3A5kTGwl zE+T3{av-$gL^;q-OD6ptEOnIPx3l7R-u-+T<T7v9z{u*$7k1M>p5KuqYJ1=Iw({_1 z8;XKKz^Ir^D-HhtH2WCp5+7@(CxtqJ%2C1?Ow-(zB!5fyC^uE|Y`^e332zPJEPtTS zhC{2V$K%P~>FJ=0DqfmQq8MUq=m|<1?bL6XuC%%B4dA)*sE~U+`U(YaJq)l<d%<W8 zjN&?k^hP3ha1=>-mLA@9r{E6q!D-%&lhShq>es&a4|raQZD`2SKdvFfHFXB@cRQ~X zXxqdqQPTBvHD1O}t5^RS4l3wz!yd&;i-P=KF*V7f#!|D4MKtg-07bCU@_kepMdjzb z66m%`+gc@#D9pCNGXh5=#ye0&=^`fuCVMN9UGF)R!n}_@zb)K3J$V8yx?uhyE7!X| zIc&1k1~<oPy{~N)nJ<EgbQ~&3ELp_c-O#}pl<f3eN(1lgVD#v&{AOhT85A;nkq>yR zFsDXpWJ*br@}*Oj_wVO4BTeHp>ad8HzkbSU%<uTMk<ay(=aqXN?aA3fm>C<IZ()n& zsq8Cx-!ea)<<iGHP(q8=3-u<cE~lD9WR|nifJmbw0`T-xPX}p#s}wl4MJZDSwBDO< z=#hCpE%O2Et0}v!`n>VdoG`f4ETa#<D+9?b>RuSn@k%~<I=#(s7C_6Sz?LY;X-J1U zC<DtTnXgBLK%|u+z-3NC0hc6x_;+<Ve0@{{OIwuq>S640a9~(pn%fx?Fb7uzF3vGZ zCu}$PgVefQVU*0bk~!o35u1UJHa9xm5f**fR~d>EL`g44cwyTfskuioL%vVM1Rn_9 ziU%b{7Ie-5tuLDc*R=eNfjyfWy$U#ZW=TiLUXQYD%YnC@a?OZyV$VZ6xbXLn2aEKq zMd4wQ4^$>hSsq#;%wy{y@yTZ2(2Yd>D*3kk|2;lfGCLjne$n1MFqV<7ktnRGU@Ox1 zC6H5%x{Y-#fba@gkz39WGPVZ+OEG)IFvc~CZbYbl1ItIZQa)`rb4;`zZVz@u1o%v@ z88yabu!HnzeK||SHZ*RAQfC8CcDM(5`$}lbV8_>Mf$%i7zu-~LoEwg>!Ke-%$YKG) z7KQrKg`d!DO_?8)W@R7GHZ<JSOCqFY@c13d@+LCyrP+psV(`4{M-UBNK0zGKY;J{9 z2l8<J_;7<}npwp%-qXJ&0^zShDm+Z`GG-Q5FfCG>kw8S>U>pw8cO$K|B_2)NS?`|K zY0=^oGs=F<3BHF6q~R-Tz_4pmKLI)fJ)#d1>C^GAq$8~C5vwK!yl#3;PG3q}kU7Hy zxWWZ)T_lpTO3Kuwa}>);+%xw#vKF)+ERc9v$J}Y5q|MI#G7}>#_p9m7E&O73DZg}% z?LBN?dAd?`@vu`nPq>R<>~Rg!$d+n~Gj4<?^)$gYgf^Y1ZNqX*1quMk;RtrEI`7v& z|B2!B>W6H$13^B&yVdojL=$Zv{hTqYwKg;GzoflFsSJEph7lF|(J$29X3aO=znNIQ z`6b3w64I$K*B8c%@u{vpg1s%Tbo0v1nHqCAJl1lOLDl4-cJg;Air>J4)|ZaorLHLX z2GBh0y8y}oK63DIK!Dsw7A&<8$G30JkpUCouiU)-*_1BRnfWmsXXTK@yCu<ViB|xJ z!xon0HZH$mSPD%EY8jE%31Zo9odbvrxZamMlj0{zMaZ9CQjDps#asC;;rQH9P4bOW zY<Tvy?#8!;ylOtv-FrvTZT^ad759K$pXD|w1#WNLiN;=VIng#%+-4GnF;qeuEpPq_ zO!;eL)&lxWFeMYId?)@G`vETjD7Kj!wI+lqR7&D8!4kiWX?08&%fdQ0%(f88Yzbw$ z3Vo+rw&?G9daEAfrO<rWPks6-<@0*iS>##a0h_~PH9n|}H>o@T6C{E!&2`kH?Zypv z`cN!^$shVTB_F1S%j3jtyw1NU9N|7vg)16&Az6$`udX1uhO)gaEsUYI3`qGAt*dLV zk&_Vs8|0}x6gkO&OeFeubD4Sc_JQzuoF(m~>1nQzmKzf6w}AbnP3D+i9h=EwWV^~P zHBV#fCB%56mK4EHluV>)GLN?km#e^YZ`<?j?J%nDEmCsU`AA+BPO)QnAa2vFfh-*p zDswqv@xc^_w$^fkUr<c`^HK8WbtUTQW%npneT5Y^Ici~_m#6sNK=sm(H$(NVu;8us z<DY+=wyKN=Yt)x(V>tHdDuThU;`-`u=xWpGQ`y`1{aEV0A@LX^yls>p{l6S?w~@pJ zXgX~~`8Ahso(}E4a`@a^^E4J%3Y?5!Z9kh`<3loDG1h?$=!(f0fvNb4P@$H_&s;bS zOFToUka4}8d{4tR(>84rd&3tJZLOEvd6MVZ;4^jD)7zC8WDBm)MfP*yU}Z)ZYFS=P z(63@>_(e!8X_s_2JFUrO>;1VFY`oy71$yl5yAFnhi{WN_A!xp0Zf>*pvX@W*=`$PS zv{iZ(Yk3lb(sjdsrqZJn(3+?U9V9JS`IDFq+C~0wrfJW3=Y+u4TQ#Y(l>SRsm)UsI zz438^p>Q-rB{3(Ul3QCyANhXd*m?O6ojz2)1!Ub0=G@QeeEV}rHp<(_%WS*dRVwYh zp({l(wwwg3a7c1|$~3U!QO_Z&X_}ZvS?BMz;yqz)#G7CEI)oRIZkR`wUrH8Fh)CoK zG7ZB}O?EbT#)TY3djfl5RyA*hW9T$Fe4zy^mCF`NQkvP~J{J%HK?gQc=i61wzG9Fo z<cyuXk*o6}_pc`>vCC9RF_CwOo8xHwe{L0v`=j>eH$3*a&XZYh8C%vf7y3IryG&_h zEBWJDmtz#NTT{5sBSl}56oy>lqDjwdEVgp#Cp@?qS?8>X%o0m)1(rn4nZGa34TL;_ z<0QL&&)pdP`RX-`(K|f45JdQhvt+LwM|;PYMjuiB@w0aWK%xk}r7UXhkcju<gLyDj zmt?gN7JT<-{a!@SpXK9PSIPFP4LbrR1*zzIx4oc}<w;H&vce@UG)LXbNlw^ENbA<c zfXufOzgEr}&+3yv8;q<ja@Ga55j|L~d8?MMlP{IMy?Sa}Kv#mbZui~nj_~j3qxQd< zHK>-QkPoTl_A~LmHi1Jmn0KB^d3~RIhZMI)(`67j&go@=nn8=vx~;5+gsgnW_VeuO z0P7K#V)gFpam{wv`#1;6%XFgsD2iFh&R~XmwCh47zKzFK)pKL!l35;{XUOmR>?}1R z1**2IcCWPwD#3uG`N_=JpQ{>dN^#NKRD3);02Hn4lyw}3mBL^6b5~IZ+aA_mh>JH% zB0qnzs>dgSg2f1Q1|3sfxF+@5kF8$&WbMeAh(reqA2-%XDLiD~(CU6SiXYC`B@@P) z3MLLlv9Sa8`$;4=I+lw{1~GH^2=eDEn5s@_;EVbaW!R}xb>SRizKnr)Vt0}A>2_Dd zKt$Gg(8J{rYg3cmrJ}H)X12gVS3vREsxXt31`UyvUWki}l!?NokL9_g5v<_9iLeUB zaDLTzCfRAmADSy;tGP(?=UvL5-KV08(~$(P(>&7c5|jnNpjHxoTS&P9A%1R*lC0Ve zsidSvmZ;=$2v!|KkNbw5dzZx<R-LA%^47AVy(^PfN+pM!sDS;;>@nD(`knWedAyHU z=8%^hHvL_EM(g)We>dFL!Pwq<6)8tBB-MZ4xy*S@Blw}deVqHowrH}6{aTDZgS$Ch zfq|Lfzn-GS8Z8Ma8RM1qk2TGrh2e?RPjnd&j`*0E#efWKxgK?8EZJw@AH_9M#K1mh zhwu&fqIrTL6(<v_Xz|^qppD+_WMhc1iEW7tiWLssd$TGs;YC&q9^_d53DuOPRmkRt z1^Fr;Zy;wZ5c-W0pBWQETBYNFUPbY9EiL*K8bgYf*Xl|m|H1uiJmmn?b8b2tXnXWL zJ1r?}Eug0;)~BSi43SQ>W8*x-=sF46zi6G(#7HWPjCuN+BAdB@^M1>QMkoGA6R?qs z(Q?J7p`V9eHm`D$8mq}cKf`jt6Q2-3y48u=1amIQh?SNoehov6mv2GJjlp0(+TzAl zoh@xF(-hkrq=eWw4U%PePN!j^h9lwbM2~&>tV`rKu`}A8hfR4XnHURUtwBeNVNjEU z7_LU<n9%s(Mvy|<@c8|OijCZP_#ok?+`y`_B?i5ToQe`@f~v~zn5T%q;Lz$=emRvP zQ6cB;NO=-2e@jLbTXA8&rsVNpg2XPSU7a|<r?r^>j153{;Us(&7NP4kY#ei6)KF8w zyi5CGXMLLBWfQm*2#S7Iu0IA8+SR&%403S<KVOz-Pz`uVEIQ(jOfohn9`Fb(#!hc^ z@+@u?#{r3wy#{^=VF}*(T&yMNdqUYcFCW!PKhb0u59=7QJN>~Rpf3|C(O(m%?)0|v z6*EftOj1;4Q$qf?<aSAeLG_2u1QR{nu_ovDgIjugAl1vPZBhS|$Z6or>B!9zalEzY zR4~cIj&8{vwLBYDQH?bUmGR&)3vSU-L@?nd)mJ9~ZqAf|F`9cgf%_*J*BVU0*KB9H zBgb$13m&oWC&8nYD;|@*tg)LFsXjCe02Y6cy#jCrm&s}~gl=aT&Qk%^%5ky#wiLzc zQsuIKH`bAnRV=^ca6#<ANjXi0Tu?kH8Lr<_L(R5`MWBs_KaK|vHCS&96%f;`NsC-U zIwSvax=a1760|=D<7byQRd8o0;+<VqXt0?HY-EXZp!E?qm<f9A^5A?)$(fiEOt=1u zFE*mu9Q}^cgZsO~;N0b~lJ(5IrAdd7FLuo!gY@ZZ%jC>g>oxLXN&gfwd75GwHygp- z^V#030bRJ3=@5Pq$l`Ya^)<av`20?e*qLl(2wVYVm&&09T`yxY!*ka(9>8QtXf<l4 zRbQUoO-ssP6WztVzfCG~RMQkTx`RNz?I%G5j%w)Cr@c=p^R+){*=Ipn+A9ad-9q)9 z(A032=_MLTobR@hEuWKb*m;na((a{bORf>e1X}7M6P2I@-9RdU8{xs{NbbS=_>pt^ zBDx_!C6_*X=|atc365Z0W+b2%go4zP8=Vq_ctV65jX8g^D)MEBXFIht(_5&NS&5Z9 z)KP!5!0=N|$kt>hmaWTo6H5-}N1=t?4dkqIm`Hb{Ek?kvzYC9*Gf%autDOsTEfM6E zKb#O!#{UcR7Vv)r+j}sgS`%xNNNBT_&TwalQ_^u5E^7yQ+C?AyIId9A(r*UhMG#Bn z6Xx8WVGm1q^vmTfsb|ba(Ruv}@=xq3E9s;6aJEX4^3X{q1~$=nz9Zj>e%NtO6!4dt zUEO}@D_<x|t2{mKrc~E!MAphgBi6uY@;dLAGaAO~&9u-C!S8<LNW_CWFXR4wHOoUV z=>M5ZPLAUeKCBD|Vl&zrB<Q3urcVv!>-ZPQClMQ`S$>liz@MI4&VHGxxx|sb^L%Gw zGNlntP6=ufQZDi}5}Y&&yS+^i@V8%dF07_29!d}_#Lz<NNPjUX{+%y^<iYZd1$6pS zWQS~Hp?Gp)%6%EuT;$H>vwIOGJCAeMGAD--WDNVPy$@r{*U8sixaywk#Zi@87^{%_ zI@pP_^Nhm32)(m3maFS36!d4d9{hNEeR>^>PF6MOzjHE9*5cE;UUsGEKnAqTR=ZL- zr4nTMO=9?xW;G(hy<*W5hjcH>K-sh)r1Td)f+QSc9zB-f@0Psx5F>gkcz%D2k*=Nd z^qq51gH`H6uaR+Qv2#23OQNg6Cr-g<4%~P)x>oFN?fo!!g`IfElMN9W`=Sy4#m+X( zk8rb9HhLA`qVE?Y?Hf2vp6{xi9?$@#02kJbnWS<*o4~eCNls6&r;V7QVPJpdaS$h2 zK(JU$a%C*<hSi3uY2f6qpN&)5K}+ZdMAv^Peya>B-yKqUIrk<#Lj5aD7sz@&ti5W8 zla~F=7eQ5;cI2Xw?{_-FLmno0*P)T&VDY%|Wbx=d+9cKH52ky5@vHLlFzA4yx^%W< zbb)iJ7`?*ZAgxk^TMYKrim~qvyarzZqthl2680S;gJ&Ha7OeOs(dW5Fw$ZQWa7FR0 z`CQ|zC!Qz3M}X^7r^#*zF@I=yF>F1e#GOa}ac7b8xMzICEkLERKPLv9LQ!V``;o|0 zY=6LFc}>k%z$<@95Psl0GTuE#=|2j!=r#&{t+@PrxhUkc(MaK~-hmMZu4YryYN#y@ zj)Cdx;+U6%bQ3l!M`8(0e?*@9BKNRq4L^*}d<PtQ=25?CVS`2M3`$bT8wN7I9DnF^ z@QfJ&mC0)sG>PEC-(GhYa;3J+V|sbK)x(0`PEKb7kUoS$CxmV6xXX|ZQ+YVdE_bn= z(0>)T-Eb_5ee!O?EOvm6(d}d2PPd<|lnIu|H<7P_Hr0R5#3;tvwb)p{ELHt|`ScK& zN#(<R_|#N`rYx1JZq4{f(99zYJ$#|Vo@+OXH$OEY(DD&#u4$+8G~|*XNPsG#ZmWk3 z3J)T#v$0eCtTmoub5z{B(XzX&dMJ8PJFu2a<+sY3s`}C{?nap>AecA5luJxRwD=^) zG%mshSlEjdetL1=Y}6CDiMr=K;$NJQ0t2Ms;VN5u(8L0ouNSq}d^^IVQ$KLw6$i4j zco6O%298`EKkXM|CRucDC5oQJRKA)#W>ECFZTqO;<tooGT%6ZA-GO6m4Qto^z6>=J z)Laz)w1xgXYIiXL2)Hn7X@&~tnnd;WPoO=yD(F>m+1JGErNMJnx-8FIuD->siAZ$K ziv*W{?|j%kyoY)`bEHi7ELq$m;W~y<ecd`h2+6x<dyB=8(;C-@rC*11bx4w~+SxAB zYX~z|1eo4>mFkq1Z@crYhoCP>^6XRU4wI~nrCQ7!^gj!HbhdWk@t&Q2%Y*0+dauAg z)d>qEM!RGGyru9-K;Q<8B~)&(C_Nq&E_VBsdj6Erx|vwwl9edKwU$3gPI!nJ+3!CB zVTtn14(B1LGLKHL_G;+#%X$YC5D=GschHQh-OMOeNCNq4PD_nO$FR(})iRqzI4~@% z@G-)_V`}o348EV~I(@R{+TOMEZ>sq(=`~7zxG_dA(SQNeSE0rnC(nrmYqHa_=o{@I zmRdTejM4}xl>LiHUr6Cb#{aqSV?rW?4%qlaul=~wbq^qU{u*8kKY;X<b-QY3tpZLZ zglnHIs8ok@6h|z|04)is7l{O-R`b~B_7qwki|^QBV<bQpo57yuyr&%aFKP$XQj@^R zKW8x+@O}Ib9$C_GngdyHe%QpAzFRr-e|U&c09Rc4AL7Q$+hdWIRvAJ=rR^z-vAIn5 zcsC>3Kx&;^swxw||Iz{K_LWHDWW039)D_F?t`YUMzz{b@92cu`WADR7=SWFd_Qx}O zxVb{T1pdh>!=xeNtij<@w_us6Pym9fg$lZ8jq$y!y!KcifOPtdFQBY!m1%{zu0FI* zXxeP)wZ8H68wm|5XNIyWm8--y51otZo)mQz7aMG3_qM?~;D|Gy<o`*5|Bn>+@9p&> ZqRf+lv7bhf{xNL?C3!WuS{XC&{{TXuTB-m5 literal 0 HcmV?d00001 diff --git a/app/selectors/preferencesController.ts b/app/selectors/preferencesController.ts index 65f06c8399b3..ee7893d1d4bf 100644 --- a/app/selectors/preferencesController.ts +++ b/app/selectors/preferencesController.ts @@ -17,6 +17,12 @@ export const selectUseNftDetection = createSelector( preferencesControllerState.useNftDetection, ); +export const selectShowMultiRpcModal = createSelector( + selectPreferencesControllerState, + (preferencesControllerState: PreferencesState) => + preferencesControllerState.showMultiRpcModal, +); + export const selectUseTokenDetection = createSelector( selectPreferencesControllerState, (preferencesControllerState: PreferencesState) => diff --git a/app/util/test/initial-background-state.json b/app/util/test/initial-background-state.json index 43b772a055b7..885d1465c252 100644 --- a/app/util/test/initial-background-state.json +++ b/app/util/test/initial-background-state.json @@ -88,6 +88,7 @@ "disabledRpcMethodPreferences": { "eth_sign": false }, + "showMultiRpcModal": false, "showTestNetworks": false, "showIncomingTransactions": { "0x1": true, diff --git a/index.js b/index.js index ece1f59ea50a..50460ab3fe0b 100644 --- a/index.js +++ b/index.js @@ -19,7 +19,6 @@ import Root from './app/components/Views/Root'; import { name } from './app.json'; import { isTest } from './app/util/test/utils.js'; - import { Performance } from './app/core/Performance'; Performance.setupPerformanceObservers(); diff --git a/locales/languages/en.json b/locales/languages/en.json index 5d23b38caf39..0139c2a29f94 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -375,6 +375,10 @@ "accept": "I agree", "cancel": "No thanks" }, + "multi_rpc_migration_modal": { + "description": "We now support multiple RPCs for a single network. Your most recent RPC has been selected as the default one to resolve conflicting information", + "accept": "Accept" + }, "nft_details": { "bought_for": "Bought for", "highest_floor_price": "Highest floor price", @@ -724,6 +728,7 @@ "toast": { "connected_and_active": "connected and active.", "now_active": "now active.", + "network_added": "was successfully added", "revoked": "revoked.", "revoked_all": "All accounts revoked.", "accounts_connected": "accounts connected.", diff --git a/patches/@metamask+preferences-controller+11.0.0.patch b/patches/@metamask+preferences-controller+11.0.0.patch index 234fd7dcf7ee..bc5402698b7b 100644 --- a/patches/@metamask+preferences-controller+11.0.0.patch +++ b/patches/@metamask+preferences-controller+11.0.0.patch @@ -1,8 +1,8 @@ diff --git a/node_modules/@metamask/preferences-controller/dist/chunk-FSWGV6H6.js b/node_modules/@metamask/preferences-controller/dist/chunk-FSWGV6H6.js -index 30e985c..41106f3 100644 +index 30e985c..d03fa2f 100644 --- a/node_modules/@metamask/preferences-controller/dist/chunk-FSWGV6H6.js +++ b/node_modules/@metamask/preferences-controller/dist/chunk-FSWGV6H6.js -@@ -17,13 +17,16 @@ var metadata = { +@@ -17,13 +17,17 @@ var metadata = { isIpfsGatewayEnabled: { persist: true, anonymous: true }, isMultiAccountBalancesEnabled: { persist: true, anonymous: true }, lostIdentities: { persist: true, anonymous: false }, @@ -18,10 +18,11 @@ index 30e985c..41106f3 100644 + useTokenDetection: { persist: true, anonymous: true }, + smartTransactionsOptInStatus: { persist: true, anonymous: true }, + useTransactionSimulations: { persist: true, anonymous: true }, ++ showMultiRpcModal: { persist: false, anonymous: false }, }; var name = "PreferencesController"; function getDefaultPreferencesState() { -@@ -37,7 +40,7 @@ function getDefaultPreferencesState() { +@@ -37,7 +41,7 @@ function getDefaultPreferencesState() { isIpfsGatewayEnabled: true, isMultiAccountBalancesEnabled: true, lostIdentities: {}, @@ -30,7 +31,7 @@ index 30e985c..41106f3 100644 securityAlertsEnabled: false, selectedAddress: "", showIncomingTransactions: { -@@ -64,7 +67,10 @@ function getDefaultPreferencesState() { +@@ -64,7 +68,11 @@ function getDefaultPreferencesState() { }, showTestNetworks: false, useNftDetection: false, @@ -39,10 +40,11 @@ index 30e985c..41106f3 100644 + useSafeChainsListValidation: true, + smartTransactionsOptInStatus: false, + useTransactionSimulations: true, ++ showMultiRpcModal: false, }; } var _syncIdentities, syncIdentities_fn; -@@ -213,9 +219,9 @@ var PreferencesController = class extends _basecontroller.BaseController { +@@ -213,9 +221,9 @@ var PreferencesController = class extends _basecontroller.BaseController { * @param useNftDetection - Boolean indicating user preference on NFT detection. */ setUseNftDetection(useNftDetection) { @@ -54,7 +56,7 @@ index 30e985c..41106f3 100644 ); } this.update((state) => { -@@ -223,18 +229,33 @@ var PreferencesController = class extends _basecontroller.BaseController { +@@ -223,18 +231,33 @@ var PreferencesController = class extends _basecontroller.BaseController { }); } /** @@ -75,7 +77,7 @@ index 30e985c..41106f3 100644 } }); } -+ ++ + /** + * Toggle the use safe chains list validation. + * @@ -87,18 +89,32 @@ index 30e985c..41106f3 100644 + if (!useSafeChainsListValidation) { + state.useSafeChainsListValidation = false; + } -+ }) ++ }) + } + /** * Toggle the security alert enabled setting. * -@@ -245,6 +266,29 @@ var PreferencesController = class extends _basecontroller.BaseController { +@@ -245,6 +268,43 @@ var PreferencesController = class extends _basecontroller.BaseController { state.securityAlertsEnabled = securityAlertsEnabled; }); } + + /** ++ * Toggle multi rpc migration modal. ++ * ++ * @param showMultiRpcModal - Boolean indicating if the multi rpc modal will be displayed or not. ++ */ ++ setShowMultiRpcModal(showMultiRpcModal) { ++ this.update((state) => { ++ state.showMultiRpcModal = showMultiRpcModal; ++ if (showMultiRpcModal) { ++ state.showMultiRpcModal = false; ++ } ++ }); ++ } ++ ++ /** + * A setter for the user to opt into smart transactions + * + * @param smartTransactionsOptInStatus - true to opt into smart transactions @@ -124,7 +140,7 @@ index 30e985c..41106f3 100644 * A setter for the user preferences to enable/disable rpc methods. * diff --git a/node_modules/@metamask/preferences-controller/dist/types/PreferencesController.d.ts b/node_modules/@metamask/preferences-controller/dist/types/PreferencesController.d.ts -index 7e3ba15..6fe9c9e 100644 +index 7e3ba15..c3c7ec6 100644 --- a/node_modules/@metamask/preferences-controller/dist/types/PreferencesController.d.ts +++ b/node_modules/@metamask/preferences-controller/dist/types/PreferencesController.d.ts @@ -69,9 +69,10 @@ export type PreferencesState = { @@ -140,7 +156,7 @@ index 7e3ba15..6fe9c9e 100644 /** * Controls whether "security alerts" are enabled */ -@@ -98,6 +99,14 @@ export type PreferencesState = { +@@ -98,6 +99,18 @@ export type PreferencesState = { * Controls whether token detection is enabled */ useTokenDetection: boolean; @@ -152,10 +168,14 @@ index 7e3ba15..6fe9c9e 100644 + * Controls whether transaction simulations are opted into + */ + useTransactionSimulations: boolean; ++ /** ++ * Controls whether Multi rpc modal is displayed or not ++ */ ++ showMultiRpcModal: boolean; }; declare const name = "PreferencesController"; export type PreferencesControllerGetStateAction = ControllerGetStateAction<typeof name, PreferencesState>; -@@ -121,7 +130,7 @@ export declare function getDefaultPreferencesState(): { +@@ -121,7 +134,7 @@ export declare function getDefaultPreferencesState(): { isIpfsGatewayEnabled: boolean; isMultiAccountBalancesEnabled: boolean; lostIdentities: {}; @@ -164,16 +184,17 @@ index 7e3ba15..6fe9c9e 100644 securityAlertsEnabled: boolean; selectedAddress: string; showIncomingTransactions: { -@@ -149,6 +158,8 @@ export declare function getDefaultPreferencesState(): { +@@ -149,6 +162,9 @@ export declare function getDefaultPreferencesState(): { showTestNetworks: boolean; useNftDetection: boolean; useTokenDetection: boolean; + smartTransactionsOptInStatus: boolean; + useTransactionSimulations: boolean; ++ showMultiRpcModal: boolean; }; /** * Controller that stores shared settings and exposes convenience methods -@@ -217,11 +228,16 @@ export declare class PreferencesController extends BaseController<typeof name, P +@@ -217,11 +233,16 @@ export declare class PreferencesController extends BaseController<typeof name, P */ setUseNftDetection(useNftDetection: boolean): void; /** @@ -193,7 +214,7 @@ index 7e3ba15..6fe9c9e 100644 /** * Toggle the security alert enabled setting. * -@@ -260,6 +276,18 @@ export declare class PreferencesController extends BaseController<typeof name, P +@@ -260,6 +281,24 @@ export declare class PreferencesController extends BaseController<typeof name, P * @param isIncomingTransactionNetworkEnable - true to enable incoming transactions */ setEnableNetworkIncomingTransactions(chainId: EtherscanSupportedHexChainId, isIncomingTransactionNetworkEnable: boolean): void; @@ -209,7 +230,12 @@ index 7e3ba15..6fe9c9e 100644 + * @param useTransactionSimulations - true to opt into transaction simulations + */ + setUseTransactionSimulations(useTransactionSimulations: boolean): void; ++ /** ++ * Toggle multi rpc migration modal. ++ * ++ * @param showMultiRpcModal - Boolean indicating if the multi rpc modal will be displayed or not. ++ */ ++ setShowMultiRpcModal(showMultiRpcModal: boolean): void; } export default PreferencesController; //# sourceMappingURL=PreferencesController.d.ts.map -\ No newline at end of file From 5b7b39ba7449566419597c171d3cf85b806a8541 Mon Sep 17 00:00:00 2001 From: tommasini <46944231+tommasini@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:32:16 +0100 Subject: [PATCH 05/46] fix: Reuse mmkv instance once created (#11139) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** We should reuse MMKV instance instead of creating a new one each time we want to access storage. ## **Related issues** N/A ## **Manual testing steps** 1. Fresh install the app 2. Press import with SRP 3. Accept share data and terms of use 4. kill the app 5. Open it 6. go to import with SRP again 7. Should not show the same modals (This means it's being saved and read under MMKV) ## **Screenshots/Recordings** https://github.com/user-attachments/assets/2d342876-9675-482c-8d54-f6b4e1258100 ### **Before** N/A ### **After** N/A ## **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. --------- Co-authored-by: Aslau Mario-Daniel <marioaslau@gmail.com> --- app/store/storage-wrapper.js | 69 ------------- app/store/storage-wrapper.test.ts | 95 ++++++++++++++++++ app/store/storage-wrapper.ts | 158 ++++++++++++++++++++++++++++++ app/util/test/network-store.js | 5 + 4 files changed, 258 insertions(+), 69 deletions(-) delete mode 100644 app/store/storage-wrapper.js create mode 100644 app/store/storage-wrapper.test.ts create mode 100644 app/store/storage-wrapper.ts diff --git a/app/store/storage-wrapper.js b/app/store/storage-wrapper.js deleted file mode 100644 index 5d39cd1a993c..000000000000 --- a/app/store/storage-wrapper.js +++ /dev/null @@ -1,69 +0,0 @@ -import ReadOnlyNetworkStore from '../util/test/network-store'; -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { isE2E } from '../util/test/utils'; -import { MMKV } from 'react-native-mmkv'; - -/** - * Wrapper class for AsyncStorage. - * (Will want to eventuall re-name since no longer async once migratted to mmkv) - */ -class StorageWrapper { - constructor() { - /** - * The underlying storage implementation. - * Use `ReadOnlyNetworkStore` in test mode otherwise use `AsyncStorage`. - */ - this.storage = isE2E ? ReadOnlyNetworkStore : new MMKV(); - } - - async getItem(key) { - try { - // asyncStorage returns null for no value - // mmkv returns undefined for no value - // therefore must return null if no value is found - // to keep app behavior consistent - const value = (await this.storage.getString(key)) ?? null; - return value; - } catch (error) { - if (isE2E) { - // Fall back to AsyncStorage in test mode if ReadOnlyNetworkStore fails - return await AsyncStorage.getItem(key); - } - throw error; - } - } - - async setItem(key, value) { - try { - if (typeof value !== 'string') - throw new Error( - `MMKV value must be a string, received value ${value} for key ${key}`, - ); - return await this.storage.set(key, value); - } catch (error) { - if (isE2E) { - // Fall back to AsyncStorage in test mode if ReadOnlyNetworkStore fails - return await AsyncStorage.setItem(key, value); - } - throw error; - } - } - - async removeItem(key) { - try { - return await this.storage.delete(key); - } catch (error) { - if (isE2E) { - // Fall back to AsyncStorage in test mode if ReadOnlyNetworkStore fails - return await AsyncStorage.removeItem(key); - } - throw error; - } - } - - async clearAll() { - await this.storage.clearAll(); - } -} - -export default new StorageWrapper(); diff --git a/app/store/storage-wrapper.test.ts b/app/store/storage-wrapper.test.ts new file mode 100644 index 000000000000..1b00a18948cb --- /dev/null +++ b/app/store/storage-wrapper.test.ts @@ -0,0 +1,95 @@ +// Unmocking storage-wrapper as it's mocked in testSetup directly +// to allow easy testing of other parts of the app +// but here we want to actually test storage-wrapper +jest.unmock('./storage-wrapper'); +import StorageWrapper from './storage-wrapper'; + +describe('StorageWrapper', () => { + it('return the value from Storage Wrapper', async () => { + const setItemSpy = jest.spyOn(StorageWrapper, 'setItem'); + const getItemSpy = jest.spyOn(StorageWrapper, 'getItem'); + + await StorageWrapper.setItem('test-key', 'test-value'); + + const result = await StorageWrapper.getItem('test-key'); + + expect(setItemSpy).toHaveBeenCalledWith('test-key', 'test-value'); + expect(getItemSpy).toHaveBeenCalledWith('test-key'); + expect(result).toBe('test-value'); + }); + it('throws when setItem value param is not a string', async () => { + const setItemSpy = jest.spyOn(StorageWrapper, 'setItem'); + + try { + //@ts-expect-error - Expected to test non string scenario + await StorageWrapper.setItem('test-key', 123); + } catch (error) { + const e = error as unknown as Error; + expect(e).toBeInstanceOf(Error); + expect(e.message).toBe( + 'MMKV value must be a string, received value 123 for key test-key', + ); + } + + expect(setItemSpy).toHaveBeenCalledWith('test-key', 123); + }); + + it('removes value from the store', async () => { + const removeItemSpy = jest.spyOn(StorageWrapper, 'removeItem'); + await StorageWrapper.setItem('test-key', 'test-value'); + + const resultBeforeRemove = await StorageWrapper.getItem('test-key'); + expect(resultBeforeRemove).toBe('test-value'); + + await StorageWrapper.removeItem('test-key'); + expect(removeItemSpy).toHaveBeenCalledWith('test-key'); + + const resultAfterRemoval = await StorageWrapper.getItem('test-key'); + expect(resultAfterRemoval).toBeNull(); + }); + + it('removes all values from the store', async () => { + const clearAllSpy = jest.spyOn(StorageWrapper, 'clearAll'); + await StorageWrapper.setItem('test-key', 'test-value'); + await StorageWrapper.setItem('test-key-2', 'test-value'); + + const resultBeforeRemove = await StorageWrapper.getItem('test-key'); + const result2BeforeRemove = await StorageWrapper.getItem('test-key-2'); + + expect(resultBeforeRemove).toBe('test-value'); + expect(result2BeforeRemove).toBe('test-value'); + + await StorageWrapper.clearAll(); + expect(clearAllSpy).toHaveBeenCalled(); + + const result = await StorageWrapper.getItem('test-key'); + const result2 = await StorageWrapper.getItem('test-key-2'); + expect(result).toBeNull(); + expect(result2).toBeNull(); + }); + + it('singleton instance is defined and unique', () => { + expect(StorageWrapper).toBeDefined(); + expect(StorageWrapper.getItem).toBeDefined(); + expect(StorageWrapper.setItem).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires + const storageWrapper = require('./storage-wrapper').default; + + expect(StorageWrapper).toBe(storageWrapper); + }); + + it('use ReadOnlyStore on E2E', async () => { + process.env.IS_TEST = 'true'; + + const getItemSpy = jest.spyOn(StorageWrapper, 'getItem'); + const setItemSpy = jest.spyOn(StorageWrapper, 'setItem'); + + await StorageWrapper.setItem('test-key', 'test-value'); + + const result = await StorageWrapper.getItem('test-key'); + + expect(setItemSpy).toHaveBeenCalledWith('test-key', 'test-value'); + expect(getItemSpy).toHaveBeenCalledWith('test-key'); + expect(result).toBe('test-value'); + }); +}); diff --git a/app/store/storage-wrapper.ts b/app/store/storage-wrapper.ts new file mode 100644 index 000000000000..dd7d8ca90c10 --- /dev/null +++ b/app/store/storage-wrapper.ts @@ -0,0 +1,158 @@ +import ReadOnlyNetworkStore from '../util/test/network-store'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { isE2E } from '../util/test/utils'; +import { MMKV } from 'react-native-mmkv'; + +/** + * Wrapper class for MMKV. + * Provides a unified interface for storage operations, with fallback to AsyncStorage in E2E test mode. + * + * @example + * // Import the StorageWrapper instance + * import StorageWrapper from './StorageWrapper'; + * + * // Set an item + * await StorageWrapper.setItem('user_id', '12345'); + * + * // Get an item + * const userId = await StorageWrapper.getItem('user_id'); + * console.log(userId); // Outputs: '12345' + * + * // Remove an item + * await StorageWrapper.removeItem('user_id'); + * + * // Clear all items + * await StorageWrapper.clearAll(); + */ +class StorageWrapper { + private static instance: StorageWrapper | null = null; + private storage: typeof ReadOnlyNetworkStore | MMKV; + + /** + * Private constructor to enforce singleton pattern. + * Initializes the storage based on the environment (E2E test or production). + */ + private constructor() { + /** + * The underlying storage implementation. + * Use `ReadOnlyNetworkStore` in test mode otherwise use `AsyncStorage`. + */ + this.storage = isE2E ? ReadOnlyNetworkStore : new MMKV(); + } + + /** + * Retrieves an item from storage. + * @param key - The key of the item to retrieve. + * @returns A promise that resolves with the value of the item, or null if not found. + * @throws Will throw an error if retrieval fails (except in E2E mode, where it falls back to AsyncStorage). + * + * @example + * const value = await StorageWrapper.getItem('my_key'); + * if (value !== null) { + * console.log('Retrieved value:', value); + * } else { + * console.log('No value found for key: my_key'); + * } + */ + async getItem(key: string) { + try { + // asyncStorage returns null for no value + // mmkv returns undefined for no value + // therefore must return null if no value is found + // to keep app behavior consistent + const value = (await this.storage.getString(key)) ?? null; + return value; + } catch (error) { + if (isE2E) { + // Fall back to AsyncStorage in test mode if ReadOnlyNetworkStore fails + return await AsyncStorage.getItem(key); + } + throw error; + } + } + + /** + * Sets an item in storage. + * @param key - The key under which to store the value. + * @param value - The value to store. Must be a string. + * @throws Will throw an error if the value is not a string or if setting fails (except in E2E mode, where it falls back to AsyncStorage). + * + * @example + * try { + * await StorageWrapper.setItem('user_preferences', JSON.stringify({ theme: 'dark' })); + * console.log('User preferences saved successfully'); + * } catch (error) { + * console.error('Failed to save user preferences:', error); + * } + */ + async setItem(key: string, value: string) { + try { + if (typeof value !== 'string') + throw new Error( + `MMKV value must be a string, received value ${value} for key ${key}`, + ); + return await this.storage.set(key, value); + } catch (error) { + if (isE2E) { + // Fall back to AsyncStorage in test mode if ReadOnlyNetworkStore fails + return await AsyncStorage.setItem(key, value); + } + throw error; + } + } + + /** + * Removes an item from storage. + * @param key - The key of the item to remove. + * @throws Will throw an error if removal fails (except in E2E mode, where it falls back to AsyncStorage). + * + * @example + * try { + * await StorageWrapper.removeItem('temporary_data'); + * console.log('Temporary data removed successfully'); + * } catch (error) { + * console.error('Failed to remove temporary data:', error); + * } + */ + async removeItem(key: string) { + try { + return await this.storage.delete(key); + } catch (error) { + if (isE2E) { + // Fall back to AsyncStorage in test mode if ReadOnlyNetworkStore fails + return await AsyncStorage.removeItem(key); + } + throw error; + } + } + + /** + * Removes an item from storage. + * @param key - The key of the item to remove. + * @throws Will throw an error if removal fails (except in E2E mode, where it falls back to AsyncStorage). + * + * @example + * try { + * await StorageWrapper.clearAll(); + * console.log('All storage data cleared successfully'); + * } catch (error) { + * console.error('Failed to clear storage data:', error); + * } + */ + async clearAll() { + await this.storage.clearAll(); + } + + /** + * Gets the singleton instance of StorageWrapper. + * @returns The StorageWrapper instance. + */ + static getInstance() { + if (!StorageWrapper.instance) { + StorageWrapper.instance = new StorageWrapper(); + } + return StorageWrapper.instance; + } +} + +export default StorageWrapper.getInstance(); diff --git a/app/util/test/network-store.js b/app/util/test/network-store.js index 1475cc40abe4..10a6078c7421 100644 --- a/app/util/test/network-store.js +++ b/app/util/test/network-store.js @@ -62,6 +62,11 @@ class ReadOnlyNetworkStore { delete this._asyncState[key]; } + async clearAll() { + await this._initIfRequired(); + delete this._asyncState; + } + async _initIfRequired() { if (!this._initialized) { await this._init(); From 1b6a814b67e4ba498bd7152286766cb44d0163aa Mon Sep 17 00:00:00 2001 From: Jyoti Puri <jyotipuri@gmail.com> Date: Wed, 9 Oct 2024 21:29:03 +0530 Subject: [PATCH 06/46] feat: Adding expandable message section to personal sign page (#11703) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Expandable message section on personal sign page. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/11679 ## **Manual testing steps** 1. Enable confirmation re-designs locally 2. Go to test dapp 3. Submit a personal sign request and check message section on the page ## **Screenshots/Recordings** https://github.com/user-attachments/assets/7272beb6-7758-4e1c-b2b5-ad2b4dd31e5a ## **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 - [ ] 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. --- .../__snapshots__/Confirm.test.tsx.snap | 335 +++++++++--------- .../AccountNetworkInfo/AccountNetworkInfo.tsx | 2 +- .../AccountNetworkInfo.test.tsx.snap | 276 +++++++-------- .../PersonalSign/Message/Message.styles.ts | 46 +++ .../PersonalSign/Message/Message.test.tsx | 30 ++ .../Info/PersonalSign/Message/Message.tsx | 63 ++++ .../__snapshots__/Message.test.tsx.snap | 75 ++++ .../Info/PersonalSign/Message/index.ts | 1 + .../Info/PersonalSign/PersonalSign.tsx | 11 +- .../__snapshots__/PersonalSign.test.tsx.snap | 59 ++- .../Info/__snapshots__/Info.test.tsx.snap | 59 ++- .../ExpandableSection.stories.tsx | 2 +- .../ExpandableSection.styles.ts | 2 +- .../ExpandableSection.test.tsx | 8 +- .../ExpandableSection/ExpandableSection.tsx | 41 ++- .../ExpandableSection.test.tsx.snap | 56 ++- 16 files changed, 621 insertions(+), 445 deletions(-) create mode 100644 app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/Message.styles.ts create mode 100644 app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/Message.test.tsx create mode 100644 app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/Message.tsx create mode 100644 app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/__snapshots__/Message.test.tsx.snap create mode 100644 app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/index.ts diff --git a/app/components/Views/confirmations/Confirm/__snapshots__/Confirm.test.tsx.snap b/app/components/Views/confirmations/Confirm/__snapshots__/Confirm.test.tsx.snap index 205601701b09..39ec20d3383e 100644 --- a/app/components/Views/confirmations/Confirm/__snapshots__/Confirm.test.tsx.snap +++ b/app/components/Views/confirmations/Confirm/__snapshots__/Confirm.test.tsx.snap @@ -150,198 +150,186 @@ exports[`Confirm should match snapshot for personal sign 1`] = ` Signature request </Text> </View> - <View - style={ - { - "alignItems": "center", - "backgroundColor": "#ffffff", - "borderColor": "#bbc0c566", - "borderRadius": 8, - "borderWidth": 1, - "display": "flex", - "flexDirection": "row", - "justifyContent": "space-between", - "marginBottom": 8, - "padding": 16, - } - } + <TouchableOpacity + accessible={true} + activeOpacity={1} + onPress={[Function]} + onPressIn={[Function]} + onPressOut={[Function]} > <View style={ { + "alignItems": "center", + "backgroundColor": "#ffffff", + "borderColor": "#bbc0c566", + "borderRadius": 8, + "borderWidth": 1, "display": "flex", "flexDirection": "row", + "justifyContent": "space-between", + "marginBottom": 8, + "padding": 16, } } > <View - onLayout={[Function]} style={ { - "alignSelf": "center", - "marginRight": 16, - "position": "relative", + "display": "flex", + "flexDirection": "row", } } - testID="badge-wrapper-badge" > - <View> - <View - style={ - { - "backgroundColor": "#ffffff", - "borderRadius": 16, - "height": 32, - "overflow": "hidden", - "width": 32, - } - } - > - <Image - source={ - { - "uri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAACVBMVEX50jXBSBE1gSvxQ/7HAAAAA3RSTlP////6yOLMAABAi0lEQVR42gGAQH+/AAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBosMwASRXnygAAAAASUVORK5CYII=", - } - } - style={ - { - "flex": 1, - } - } - /> - </View> - </View> <View + onLayout={[Function]} style={ { - "alignItems": "center", - "aspectRatio": 1, - "height": 0, - "justifyContent": "center", - "position": "absolute", - "right": 0, - "top": 0, - "transform": [ - { - "translateX": 0, - }, - { - "translateY": -0, - }, - ], + "alignSelf": "center", + "marginRight": 16, + "position": "relative", } } + testID="badge-wrapper-badge" > + <View> + <View + style={ + { + "backgroundColor": "#ffffff", + "borderRadius": 16, + "height": 32, + "overflow": "hidden", + "width": 32, + } + } + > + <Image + source={ + { + "uri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAACVBMVEX50jXBSBE1gSvxQ/7HAAAAA3RSTlP////6yOLMAABAi0lEQVR42gGAQH+/AAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBosMwASRXnygAAAAASUVORK5CYII=", + } + } + style={ + { + "flex": 1, + } + } + /> + </View> + </View> <View - onLayout={[Function]} style={ { "alignItems": "center", "aspectRatio": 1, - "height": "50%", + "height": 0, "justifyContent": "center", - "maxHeight": 24, - "minHeight": 8, - "opacity": 0, + "position": "absolute", + "right": 0, + "top": 0, + "transform": [ + { + "translateX": 0, + }, + { + "translateY": -0, + }, + ], } } - testID="badgenetwork" > <View + onLayout={[Function]} style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderColor": "#ffffff", - "borderRadius": 16, - "borderWidth": 2, - "height": 32, + "aspectRatio": 1, + "height": "50%", "justifyContent": "center", - "overflow": "hidden", - "shadowColor": "#0000001a", - "shadowOffset": { - "height": 2, - "width": 0, - }, - "shadowOpacity": 1, - "shadowRadius": 4, - "transform": [ - { - "scale": 1, - }, - ], - "width": 32, + "maxHeight": 24, + "minHeight": 8, + "opacity": 0, } } + testID="badgenetwork" > - <Image - onError={[Function]} - resizeMode="contain" - source={ - { - "default": { - "uri": "MockImage", - }, - } - } + <View style={ { + "alignItems": "center", + "backgroundColor": "#ffffff", + "borderColor": "#ffffff", + "borderRadius": 16, + "borderWidth": 2, "height": 32, + "justifyContent": "center", + "overflow": "hidden", + "shadowColor": "#0000001a", + "shadowOffset": { + "height": 2, + "width": 0, + }, + "shadowOpacity": 1, + "shadowRadius": 4, + "transform": [ + { + "scale": 1, + }, + ], "width": 32, } } - testID="network-avatar-image" - /> + > + <Image + onError={[Function]} + resizeMode="contain" + source={ + { + "default": { + "uri": "MockImage", + }, + } + } + style={ + { + "height": 32, + "width": 32, + } + } + testID="network-avatar-image" + /> + </View> </View> </View> </View> - </View> - <View> - <Text - style={ - { - "color": "#141618", - "fontFamily": "EuclidCircularB-Regular", - "fontSize": 14, - "fontWeight": "500", + <View> + <Text + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "500", + } } - } - > - 0x935E...5477 - </Text> - <Text - style={ - { - "color": "#141618", - "fontFamily": "EuclidCircularB-Regular", - "fontSize": 14, - "fontWeight": "400", + > + 0x935E...5477 + </Text> + <Text + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + } } - } - > - Ethereum Main Network - </Text> + > + Ethereum Main Network + </Text> + </View> </View> - </View> - <TouchableOpacity - accessible={true} - activeOpacity={1} - disabled={false} - onPress={[Function]} - onPressIn={[Function]} - onPressOut={[Function]} - style={ - { - "alignItems": "center", - "borderRadius": 8, - "height": 24, - "justifyContent": "center", - "opacity": 1, - "width": 24, - } - } - testID="openButtonTestId" - > <SvgMock color="#9fa6ae" height={16} @@ -354,8 +342,8 @@ exports[`Confirm should match snapshot for personal sign 1`] = ` } width={16} /> - </TouchableOpacity> - </View> + </View> + </TouchableOpacity> <View style={ { @@ -491,37 +479,33 @@ exports[`Confirm should match snapshot for personal sign 1`] = ` </View> </View> </View> - <View - style={ - { - "backgroundColor": "#ffffff", - "borderColor": "#bbc0c566", - "borderRadius": 8, - "borderWidth": 1, - "marginBottom": 8, - "padding": 8, - } - } + <TouchableOpacity + accessible={true} + activeOpacity={1} + onPress={[Function]} + onPressIn={[Function]} + onPressOut={[Function]} > <View style={ { + "alignItems": "center", + "backgroundColor": "#ffffff", + "borderColor": "#bbc0c566", + "borderRadius": 8, + "borderWidth": 1, "display": "flex", "flexDirection": "row", - "flexWrap": "wrap", "justifyContent": "space-between", - "paddingBottom": 8, - "paddingHorizontal": 8, + "marginBottom": 8, + "padding": 16, } } > <View style={ { - "alignItems": "center", "display": "flex", - "flexDirection": "row", - "marginTop": 8, } } > @@ -529,40 +513,43 @@ exports[`Confirm should match snapshot for personal sign 1`] = ` style={ { "color": "#141618", - "fontFamily": "EuclidCircularB-Bold", + "fontFamily": "EuclidCircularB-Regular", "fontSize": 14, "fontWeight": "500", + "marginBottom": 4, } } > Message </Text> - </View> - <View - style={ - { - "alignItems": "center", - "display": "flex", - "flexDirection": "row", - } - } - > <Text + numberOfLines={1} style={ { "color": "#141618", "fontFamily": "EuclidCircularB-Regular", "fontSize": 14, "fontWeight": "400", - "marginTop": 8, } } > Example \`personal_sign\` message </Text> </View> + <SvgMock + color="#9fa6ae" + height={16} + name="ArrowRight" + style={ + { + "height": 16, + "width": 16, + } + } + width={16} + /> </View> - </View> + </TouchableOpacity> </View> <View style={ diff --git a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfo.tsx b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfo.tsx index 422b3829b71b..5229d7812f67 100644 --- a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfo.tsx +++ b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfo.tsx @@ -17,7 +17,7 @@ const AccountNetworkInfo = () => { <ExpandableSection collapsedContent={<AccountNetworkInfoCollapsed />} expandedContent={<AccountNetworkInfoExpanded />} - modalTitle={strings('confirm.details')} + expandedContentTitle={strings('confirm.details')} /> ); }; diff --git a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/__snapshots__/AccountNetworkInfo.test.tsx.snap b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/__snapshots__/AccountNetworkInfo.test.tsx.snap index 50edfd8c5423..488d024518c3 100644 --- a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/__snapshots__/AccountNetworkInfo.test.tsx.snap +++ b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/__snapshots__/AccountNetworkInfo.test.tsx.snap @@ -1,198 +1,186 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AccountNetworkInfo should match snapshot for personal sign 1`] = ` -<View - style={ - { - "alignItems": "center", - "backgroundColor": "#ffffff", - "borderColor": "#bbc0c566", - "borderRadius": 8, - "borderWidth": 1, - "display": "flex", - "flexDirection": "row", - "justifyContent": "space-between", - "marginBottom": 8, - "padding": 16, - } - } +<TouchableOpacity + accessible={true} + activeOpacity={1} + onPress={[Function]} + onPressIn={[Function]} + onPressOut={[Function]} > <View style={ { + "alignItems": "center", + "backgroundColor": "#ffffff", + "borderColor": "#bbc0c566", + "borderRadius": 8, + "borderWidth": 1, "display": "flex", "flexDirection": "row", + "justifyContent": "space-between", + "marginBottom": 8, + "padding": 16, } } > <View - onLayout={[Function]} style={ { - "alignSelf": "center", - "marginRight": 16, - "position": "relative", + "display": "flex", + "flexDirection": "row", } } - testID="badge-wrapper-badge" > - <View> - <View - style={ - { - "backgroundColor": "#ffffff", - "borderRadius": 16, - "height": 32, - "overflow": "hidden", - "width": 32, - } - } - > - <Image - source={ - { - "uri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAACVBMVEX50jXBSBE1gSvxQ/7HAAAAA3RSTlP////6yOLMAABAi0lEQVR42gGAQH+/AAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBosMwASRXnygAAAAASUVORK5CYII=", - } - } - style={ - { - "flex": 1, - } - } - /> - </View> - </View> <View + onLayout={[Function]} style={ { - "alignItems": "center", - "aspectRatio": 1, - "height": 0, - "justifyContent": "center", - "position": "absolute", - "right": 0, - "top": 0, - "transform": [ - { - "translateX": 0, - }, - { - "translateY": -0, - }, - ], + "alignSelf": "center", + "marginRight": 16, + "position": "relative", } } + testID="badge-wrapper-badge" > + <View> + <View + style={ + { + "backgroundColor": "#ffffff", + "borderRadius": 16, + "height": 32, + "overflow": "hidden", + "width": 32, + } + } + > + <Image + source={ + { + "uri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAACVBMVEX50jXBSBE1gSvxQ/7HAAAAA3RSTlP////6yOLMAABAi0lEQVR42gGAQH+/AAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgICAgICAgICAgICAgICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgICAgICAgICAgICAgIAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBosMwASRXnygAAAAASUVORK5CYII=", + } + } + style={ + { + "flex": 1, + } + } + /> + </View> + </View> <View - onLayout={[Function]} style={ { "alignItems": "center", "aspectRatio": 1, - "height": "50%", + "height": 0, "justifyContent": "center", - "maxHeight": 24, - "minHeight": 8, - "opacity": 0, + "position": "absolute", + "right": 0, + "top": 0, + "transform": [ + { + "translateX": 0, + }, + { + "translateY": -0, + }, + ], } } - testID="badgenetwork" > <View + onLayout={[Function]} style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderColor": "#ffffff", - "borderRadius": 16, - "borderWidth": 2, - "height": 32, + "aspectRatio": 1, + "height": "50%", "justifyContent": "center", - "overflow": "hidden", - "shadowColor": "#0000001a", - "shadowOffset": { - "height": 2, - "width": 0, - }, - "shadowOpacity": 1, - "shadowRadius": 4, - "transform": [ - { - "scale": 1, - }, - ], - "width": 32, + "maxHeight": 24, + "minHeight": 8, + "opacity": 0, } } + testID="badgenetwork" > - <Image - onError={[Function]} - resizeMode="contain" - source={ - { - "default": { - "uri": "MockImage", - }, - } - } + <View style={ { + "alignItems": "center", + "backgroundColor": "#ffffff", + "borderColor": "#ffffff", + "borderRadius": 16, + "borderWidth": 2, "height": 32, + "justifyContent": "center", + "overflow": "hidden", + "shadowColor": "#0000001a", + "shadowOffset": { + "height": 2, + "width": 0, + }, + "shadowOpacity": 1, + "shadowRadius": 4, + "transform": [ + { + "scale": 1, + }, + ], "width": 32, } } - testID="network-avatar-image" - /> + > + <Image + onError={[Function]} + resizeMode="contain" + source={ + { + "default": { + "uri": "MockImage", + }, + } + } + style={ + { + "height": 32, + "width": 32, + } + } + testID="network-avatar-image" + /> + </View> </View> </View> </View> - </View> - <View> - <Text - style={ - { - "color": "#141618", - "fontFamily": "EuclidCircularB-Regular", - "fontSize": 14, - "fontWeight": "500", + <View> + <Text + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "500", + } } - } - > - 0x935E...5477 - </Text> - <Text - style={ - { - "color": "#141618", - "fontFamily": "EuclidCircularB-Regular", - "fontSize": 14, - "fontWeight": "400", + > + 0x935E...5477 + </Text> + <Text + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + } } - } - > - Ethereum Main Network - </Text> + > + Ethereum Main Network + </Text> + </View> </View> - </View> - <TouchableOpacity - accessible={true} - activeOpacity={1} - disabled={false} - onPress={[Function]} - onPressIn={[Function]} - onPressOut={[Function]} - style={ - { - "alignItems": "center", - "borderRadius": 8, - "height": 24, - "justifyContent": "center", - "opacity": 1, - "width": 24, - } - } - testID="openButtonTestId" - > <SvgMock color="#9fa6ae" height={16} @@ -205,6 +193,6 @@ exports[`AccountNetworkInfo should match snapshot for personal sign 1`] = ` } width={16} /> - </TouchableOpacity> -</View> + </View> +</TouchableOpacity> `; diff --git a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/Message.styles.ts b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/Message.styles.ts new file mode 100644 index 000000000000..3fde7499b126 --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/Message.styles.ts @@ -0,0 +1,46 @@ +import { StyleSheet } from 'react-native'; + +import { Theme } from '../../../../../../../../util/theme/models'; +import { fontStyles } from '../../../../../../../../styles/common'; + +const styleSheet = (params: { theme: Theme }) => { + const { theme } = params; + + return StyleSheet.create({ + container: { + display: 'flex', + }, + title: { + color: theme.colors.text.default, + ...fontStyles.normal, + fontSize: 14, + fontWeight: '500', + marginBottom: 4, + }, + description: { + color: theme.colors.text.default, + ...fontStyles.normal, + fontSize: 14, + fontWeight: '400', + }, + messageContainer: { + backgroundColor: theme.colors.background.default, + padding: 16, + borderRadius: 8, + minHeight: 200, + }, + messageExpanded: { + color: theme.colors.text.default, + ...fontStyles.normal, + fontSize: 14, + fontWeight: '400', + }, + copyButton: { + position: 'absolute', + top: -40, + right: 0, + }, + }); +}; + +export default styleSheet; diff --git a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/Message.test.tsx b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/Message.test.tsx new file mode 100644 index 000000000000..d07b1373ea29 --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/Message.test.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +import renderWithProvider from '../../../../../../../../util/test/renderWithProvider'; +import { personalSignatureConfirmationState } from '../../../../../../../../util/test/confirm-data-helpers'; +import Message from './index'; +import { fireEvent } from '@testing-library/react-native'; + +describe('Message', () => { + it('should match snapshot', async () => { + const container = renderWithProvider(<Message />, { + state: personalSignatureConfirmationState, + }); + expect(container).toMatchSnapshot(); + }); + + it('should show expanded view when open button is clicked', async () => { + const { getByTestId, getByText, getAllByText } = renderWithProvider( + <Message />, + { + state: personalSignatureConfirmationState, + }, + ); + expect(getAllByText('Message')).toHaveLength(1); + expect(getAllByText('Example `personal_sign` message')).toHaveLength(1); + fireEvent.press(getByText('Message')); + expect(getAllByText('Message')).toHaveLength(2); + expect(getAllByText('Example `personal_sign` message')).toHaveLength(2); + expect(getByTestId('copyButtonTestId')).toBeDefined(); + }); +}); diff --git a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/Message.tsx b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/Message.tsx new file mode 100644 index 000000000000..5a2768e53b61 --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/Message.tsx @@ -0,0 +1,63 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { Text, View } from 'react-native'; +import { hexToText } from '@metamask/controller-utils'; + +import ButtonIcon, { + ButtonIconSizes, +} from '../../../../../../../../component-library/components/Buttons/ButtonIcon'; +import ClipboardManager from '../../../../../../../../core/ClipboardManager'; +import { + IconColor, + IconName, +} from '../../../../../../../../component-library/components/Icons/Icon'; +import { sanitizeString } from '../../../../../../../../util/string'; +import { strings } from '../../../../../../../../../locales/i18n'; +import { useStyles } from '../../../../../../../../component-library/hooks'; +import useApprovalRequest from '../../../../../hooks/useApprovalRequest'; +import ExpandableSection from '../../../../UI/ExpandableSection'; +import styleSheet from './Message.styles'; + +const Message = () => { + const { approvalRequest } = useApprovalRequest(); + const [copied, setCopied] = useState(false); + const { styles } = useStyles(styleSheet, {}); + + const message = useMemo( + () => sanitizeString(hexToText(approvalRequest?.requestData?.data)), + [approvalRequest?.requestData?.data], + ); + + const copyMessage = useCallback(async () => { + await ClipboardManager.setString(message); + setCopied(true); + }, [message, setCopied]); + + return ( + <ExpandableSection + collapsedContent={ + <View style={styles.container}> + <Text style={styles.title}>{strings('confirm.message')}</Text> + <Text style={styles.description} numberOfLines={1}> + {message} + </Text> + </View> + } + expandedContent={ + <View style={styles.messageContainer}> + <ButtonIcon + iconColor={IconColor.Muted} + size={ButtonIconSizes.Sm} + onPress={copyMessage} + iconName={copied ? IconName.CopySuccess : IconName.Copy} + style={styles.copyButton} + testID="copyButtonTestId" + /> + <Text style={styles.messageExpanded}>{message}</Text> + </View> + } + expandedContentTitle={strings('confirm.message')} + /> + ); +}; + +export default Message; diff --git a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/__snapshots__/Message.test.tsx.snap b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/__snapshots__/Message.test.tsx.snap new file mode 100644 index 000000000000..3a9ef6f8132a --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/__snapshots__/Message.test.tsx.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Message should match snapshot 1`] = ` +<TouchableOpacity + accessible={true} + activeOpacity={1} + onPress={[Function]} + onPressIn={[Function]} + onPressOut={[Function]} +> + <View + style={ + { + "alignItems": "center", + "backgroundColor": "#ffffff", + "borderColor": "#bbc0c566", + "borderRadius": 8, + "borderWidth": 1, + "display": "flex", + "flexDirection": "row", + "justifyContent": "space-between", + "marginBottom": 8, + "padding": 16, + } + } + > + <View + style={ + { + "display": "flex", + } + } + > + <Text + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "500", + "marginBottom": 4, + } + } + > + Message + </Text> + <Text + numberOfLines={1} + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + } + } + > + Example \`personal_sign\` message + </Text> + </View> + <SvgMock + color="#9fa6ae" + height={16} + name="ArrowRight" + style={ + { + "height": 16, + "width": 16, + } + } + width={16} + /> + </View> +</TouchableOpacity> +`; diff --git a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/index.ts b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/index.ts new file mode 100644 index 000000000000..a8132bac3b14 --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/index.ts @@ -0,0 +1 @@ +export { default } from './Message'; diff --git a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.tsx b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.tsx index 84002f9a3e8e..74b713745df5 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.tsx +++ b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.tsx @@ -1,12 +1,11 @@ import React from 'react'; -import { hexToText } from '@metamask/controller-utils'; -import { sanitizeString } from '../../../../../../../util/string'; import { strings } from '../../../../../../../../locales/i18n'; import useApprovalRequest from '../../../../hooks/useApprovalRequest'; import InfoSection from '../../../UI/InfoRow/InfoSection'; import InfoRow from '../../../UI/InfoRow'; import InfoURL from '../../../UI/InfoRow/InfoValue/InfoURL'; +import Message from './Message'; const PersonalSign = () => { const { approvalRequest } = useApprovalRequest(); @@ -25,13 +24,7 @@ const PersonalSign = () => { <InfoURL url={approvalRequest.origin} /> </InfoRow> </InfoSection> - <InfoSection> - <InfoRow label={strings('confirm.message')}> - <InfoURL - url={sanitizeString(hexToText(approvalRequest.requestData?.data))} - /> - </InfoRow> - </InfoSection> + <Message /> </> ); }; diff --git a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/__snapshots__/PersonalSign.test.tsx.snap b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/__snapshots__/PersonalSign.test.tsx.snap index 8688f2e7a9f4..c12fabda20ac 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/__snapshots__/PersonalSign.test.tsx.snap +++ b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/__snapshots__/PersonalSign.test.tsx.snap @@ -137,37 +137,33 @@ exports[`Title should match snapshot 1`] = ` </View> </View> </View>, - <View - style={ - { - "backgroundColor": "#ffffff", - "borderColor": "#bbc0c566", - "borderRadius": 8, - "borderWidth": 1, - "marginBottom": 8, - "padding": 8, - } - } + <TouchableOpacity + accessible={true} + activeOpacity={1} + onPress={[Function]} + onPressIn={[Function]} + onPressOut={[Function]} > <View style={ { + "alignItems": "center", + "backgroundColor": "#ffffff", + "borderColor": "#bbc0c566", + "borderRadius": 8, + "borderWidth": 1, "display": "flex", "flexDirection": "row", - "flexWrap": "wrap", "justifyContent": "space-between", - "paddingBottom": 8, - "paddingHorizontal": 8, + "marginBottom": 8, + "padding": 16, } } > <View style={ { - "alignItems": "center", "display": "flex", - "flexDirection": "row", - "marginTop": 8, } } > @@ -175,39 +171,42 @@ exports[`Title should match snapshot 1`] = ` style={ { "color": "#141618", - "fontFamily": "EuclidCircularB-Bold", + "fontFamily": "EuclidCircularB-Regular", "fontSize": 14, "fontWeight": "500", + "marginBottom": 4, } } > Message </Text> - </View> - <View - style={ - { - "alignItems": "center", - "display": "flex", - "flexDirection": "row", - } - } - > <Text + numberOfLines={1} style={ { "color": "#141618", "fontFamily": "EuclidCircularB-Regular", "fontSize": 14, "fontWeight": "400", - "marginTop": 8, } } > Example \`personal_sign\` message </Text> </View> + <SvgMock + color="#9fa6ae" + height={16} + name="ArrowRight" + style={ + { + "height": 16, + "width": 16, + } + } + width={16} + /> </View> - </View>, + </TouchableOpacity>, ] `; diff --git a/app/components/Views/confirmations/components/Confirm/Info/__snapshots__/Info.test.tsx.snap b/app/components/Views/confirmations/components/Confirm/Info/__snapshots__/Info.test.tsx.snap index b5021db7505c..6e7b948f55db 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/__snapshots__/Info.test.tsx.snap +++ b/app/components/Views/confirmations/components/Confirm/Info/__snapshots__/Info.test.tsx.snap @@ -137,37 +137,33 @@ exports[`Info should match snapshot for personal sign 1`] = ` </View> </View> </View>, - <View - style={ - { - "backgroundColor": "#ffffff", - "borderColor": "#bbc0c566", - "borderRadius": 8, - "borderWidth": 1, - "marginBottom": 8, - "padding": 8, - } - } + <TouchableOpacity + accessible={true} + activeOpacity={1} + onPress={[Function]} + onPressIn={[Function]} + onPressOut={[Function]} > <View style={ { + "alignItems": "center", + "backgroundColor": "#ffffff", + "borderColor": "#bbc0c566", + "borderRadius": 8, + "borderWidth": 1, "display": "flex", "flexDirection": "row", - "flexWrap": "wrap", "justifyContent": "space-between", - "paddingBottom": 8, - "paddingHorizontal": 8, + "marginBottom": 8, + "padding": 16, } } > <View style={ { - "alignItems": "center", "display": "flex", - "flexDirection": "row", - "marginTop": 8, } } > @@ -175,39 +171,42 @@ exports[`Info should match snapshot for personal sign 1`] = ` style={ { "color": "#141618", - "fontFamily": "EuclidCircularB-Bold", + "fontFamily": "EuclidCircularB-Regular", "fontSize": 14, "fontWeight": "500", + "marginBottom": 4, } } > Message </Text> - </View> - <View - style={ - { - "alignItems": "center", - "display": "flex", - "flexDirection": "row", - } - } - > <Text + numberOfLines={1} style={ { "color": "#141618", "fontFamily": "EuclidCircularB-Regular", "fontSize": 14, "fontWeight": "400", - "marginTop": 8, } } > Example \`personal_sign\` message </Text> </View> + <SvgMock + color="#9fa6ae" + height={16} + name="ArrowRight" + style={ + { + "height": 16, + "width": 16, + } + } + width={16} + /> </View> - </View>, + </TouchableOpacity>, ] `; diff --git a/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.stories.tsx b/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.stories.tsx index a84d6da658ce..fd3a5478bbf3 100644 --- a/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.stories.tsx +++ b/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.stories.tsx @@ -20,6 +20,6 @@ storiesOf('Confirmations / ExpandableSection', module) <InfoRow label="label-Key">Value-Text</InfoRow> </InfoSection> } - modalTitle={'Title'} + expandedContentTitle={'Title'} /> )); diff --git a/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.styles.ts b/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.styles.ts index f71a429a3f53..e2a26c4cc372 100644 --- a/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.styles.ts +++ b/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.styles.ts @@ -33,7 +33,7 @@ const styleSheet = (params: { theme: Theme }) => { alignItems: 'center', paddingBottom: 16, }, - modalTitle: { + expandedContentTitle: { color: theme.colors.text.default, ...fontStyles.bold, fontSize: 14, diff --git a/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.test.tsx b/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.test.tsx index 052dfed8470c..a1e1cf7ac395 100644 --- a/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.test.tsx +++ b/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.test.tsx @@ -20,7 +20,7 @@ describe('ExpandableSection', () => { <InfoRow label="label-Key">Value-Text</InfoRow> </InfoSection> } - modalTitle={'Title'} + expandedContentTitle={'Title'} />, ); expect(container).toMatchSnapshot(); @@ -39,7 +39,7 @@ describe('ExpandableSection', () => { <InfoRow label="label-Key">Value-Text</InfoRow> </InfoSection> } - modalTitle={'Title'} + expandedContentTitle={'Title'} />, ); expect(getByText('Open')).toBeDefined(); @@ -58,11 +58,11 @@ describe('ExpandableSection', () => { <InfoRow label="label-Key">Value-Text</InfoRow> </InfoSection> } - modalTitle={'Title'} + expandedContentTitle={'Title'} />, ); expect(getByText('Open')).toBeDefined(); - fireEvent.press(getByTestId('openButtonTestId')); + fireEvent.press(getByText('Open')); expect(getByText('Value-Text')).toBeDefined(); fireEvent.press(getByTestId('closeButtonTestId')); expect(getByText('Open')).toBeDefined(); diff --git a/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.tsx b/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.tsx index f0dd5b89a106..ad917a322bdc 100644 --- a/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.tsx +++ b/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.tsx @@ -1,12 +1,13 @@ import React, { ReactNode, useState } from 'react'; -import { Text, View } from 'react-native'; +import { Text, TouchableOpacity, View } from 'react-native'; import ButtonIcon, { ButtonIconSizes, } from '../../../../../../component-library/components/Buttons/ButtonIcon'; -import { +import Icon, { IconColor, IconName, + IconSize, } from '../../../../../../component-library/components/Icons/Icon'; import { useStyles } from '../../../../../../component-library/hooks'; import BottomModal from '../BottomModal'; @@ -15,16 +16,14 @@ import styleSheet from './ExpandableSection.styles'; interface ExpandableSectionProps { collapsedContent: ReactNode; expandedContent: ReactNode; - modalTitle: string; - openButtonTestId?: string; + expandedContentTitle: string; closeButtonTestId?: string; } const ExpandableSection = ({ collapsedContent, expandedContent, - modalTitle, - openButtonTestId, + expandedContentTitle, closeButtonTestId, }: ExpandableSectionProps) => { const { styles } = useStyles(styleSheet, {}); @@ -32,16 +31,22 @@ const ExpandableSection = ({ return ( <> - <View style={styles.container}> - {collapsedContent} - <ButtonIcon - iconColor={IconColor.Muted} - size={ButtonIconSizes.Sm} - onPress={() => setExpanded(true)} - iconName={IconName.ArrowRight} - testID={openButtonTestId ?? 'openButtonTestId'} - /> - </View> + <TouchableOpacity + onPress={() => setExpanded(true)} + onPressIn={() => setExpanded(true)} + onPressOut={() => setExpanded(true)} + accessible + activeOpacity={1} + > + <View style={styles.container}> + {collapsedContent} + <Icon + color={IconColor.Muted} + size={IconSize.Sm} + name={IconName.ArrowRight} + /> + </View> + </TouchableOpacity> {expanded && ( <BottomModal hideBackground> <View style={styles.modalContent}> @@ -53,7 +58,9 @@ const ExpandableSection = ({ iconName={IconName.ArrowLeft} testID={closeButtonTestId ?? 'closeButtonTestId'} /> - <Text style={styles.modalTitle}>{modalTitle}</Text> + <Text style={styles.expandedContentTitle}> + {expandedContentTitle} + </Text> </View> {expandedContent} </View> diff --git a/app/components/Views/confirmations/components/UI/ExpandableSection/__snapshots__/ExpandableSection.test.tsx.snap b/app/components/Views/confirmations/components/UI/ExpandableSection/__snapshots__/ExpandableSection.test.tsx.snap index c0f52f65f01b..ce571ced5fb8 100644 --- a/app/components/Views/confirmations/components/UI/ExpandableSection/__snapshots__/ExpandableSection.test.tsx.snap +++ b/app/components/Views/confirmations/components/UI/ExpandableSection/__snapshots__/ExpandableSection.test.tsx.snap @@ -1,46 +1,34 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ExpandableSection should match snapshot for simple ExpandableSection 1`] = ` -<View - style={ - { - "alignItems": "center", - "backgroundColor": "#ffffff", - "borderColor": "#bbc0c566", - "borderRadius": 8, - "borderWidth": 1, - "display": "flex", - "flexDirection": "row", - "justifyContent": "space-between", - "marginBottom": 8, - "padding": 16, - } - } +<TouchableOpacity + accessible={true} + activeOpacity={1} + onPress={[Function]} + onPressIn={[Function]} + onPressOut={[Function]} > - <View> - <Text> - Open - </Text> - </View> - <TouchableOpacity - accessible={true} - activeOpacity={1} - disabled={false} - onPress={[Function]} - onPressIn={[Function]} - onPressOut={[Function]} + <View style={ { "alignItems": "center", + "backgroundColor": "#ffffff", + "borderColor": "#bbc0c566", "borderRadius": 8, - "height": 24, - "justifyContent": "center", - "opacity": 1, - "width": 24, + "borderWidth": 1, + "display": "flex", + "flexDirection": "row", + "justifyContent": "space-between", + "marginBottom": 8, + "padding": 16, } } - testID="openButtonTestId" > + <View> + <Text> + Open + </Text> + </View> <SvgMock color="#9fa6ae" height={16} @@ -53,6 +41,6 @@ exports[`ExpandableSection should match snapshot for simple ExpandableSection 1` } width={16} /> - </TouchableOpacity> -</View> + </View> +</TouchableOpacity> `; From a7bd86700ff2241912a43875bb4664542b3885e7 Mon Sep 17 00:00:00 2001 From: George Marshall <george.marshall@consensys.net> Date: Wed, 9 Oct 2024 12:41:12 -0700 Subject: [PATCH 07/46] chore: updating codeowners to remove mobile-devs from component library (#11709) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR updates the `CODEOWNERS` file by removing mobile developers from the `component-library` folder. The goal is to: - **Prevent Prop Bloat**: Streamline components and avoid unnecessary complexity. - **Maintain Consistency**: Ensure that we achieve 1:1 consistency across Figma, Extension, and Mobile environments. - **Reduce Tech Debt**: Centralize control over merge permissions to avoid inconsistencies and maintain clean component architecture. ## **Related Issues** Fixes: N/A ## **Manual Testing Steps** 1. Review the changes made to the `CODEOWNERS` file. 2. Verify that mobile developers no longer have ownership permissions over the `component-library` folder. 3. Confirm that merge permissions reflect the correct teams for maintaining the design system and consistency. ## **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)). ## **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 and includes necessary testing evidence, such as recordings or screenshots. --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ade0e02ea85d..a483a7157ca7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,7 +4,7 @@ * @MetaMask/mobile-devs # Design System Team -app/component-library/ @MetaMask/design-system-engineers @MetaMask/mobile-platform +app/component-library/ @MetaMask/design-system-engineers # Platform Team patches/ @MetaMask/mobile-platform From d28d3f3b194405a3f609e04f8e51d437afaedb6e Mon Sep 17 00:00:00 2001 From: SamuelSalas <samuel.salas.reyes@gmail.com> Date: Wed, 9 Oct 2024 14:29:57 -0600 Subject: [PATCH 08/46] test: Refactor ImportAccountView.js and LoginView.js files (#11694) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- 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** We are aiming to refactor the page objects in the modal folder so that they strictly follow the page object model pattern. This would aide in providing more readable and help standardize the way we create our tests. Because of the amount of files remaining, this issue will focus on working on three files to refactor, as well as their respective testIDS. ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** Regression Test run: https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/565bdc5f-1994-4751-9bbe-5681ebd83ea7 ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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. --- .../LoginOptionsSwitch/LoginOptionsSwitch.tsx | 7 ++- .../Views/ImportPrivateKey/index.tsx | 10 ++-- app/components/Views/Login/index.js | 5 +- e2e/pages/CommonView.js | 13 ++++- e2e/pages/ImportAccountView.js | 53 ------------------- e2e/pages/LoginView.js | 37 +++++++------ .../RevealSecretRecoveryPhrase.js | 18 ------- e2e/pages/importAccount/ImportAccountView.js | 32 +++++++++++ .../importAccount/SuccessImportAccountView.js | 19 +++++++ e2e/pages/wallet/WalletView.js | 8 --- .../ImportAccountFromPrivateKey.selectors.js | 6 +++ .../SuccessImportAccount.selectors.js | 4 ++ .../ImportAccountFromPrivateKey.selectors.js | 6 --- e2e/selectors/LoginView.selectors.js | 4 +- .../RevealSeedView.selectors.js | 1 - e2e/specs/accounts/auto-lock.spec.js | 2 +- .../accounts/change-account-name.spec.js | 2 +- .../accounts/import-wallet-account.spec.js | 12 +++-- ...imported-account-remove-and-import.spec.js | 9 ++-- .../onboarding-wizard-opt-in.spec.js | 3 +- .../permission-system-delete-wallet.spec.js | 2 +- e2e/specs/quarantine/deeplinks.failing.js | 10 ++-- ...ystem-removing-imported-account.failing.js | 13 ++--- e2e/specs/settings/delete-wallet.spec.js | 2 +- e2e/specs/wallet/suggestedGasApi.mock.spec.js | 9 ++-- e2e/utils/Gestures.js | 2 +- e2e/viewHelper.js | 2 +- wdio/screen-objects/ImportAccountScreen.js | 15 ++---- wdio/screen-objects/ImportSuccessScreen.js | 9 ++-- wdio/screen-objects/LoginScreen.js | 25 +++------ .../Screens/ImportAccountScreen.testIds.js | 5 -- .../Screens/ImportSuccessScreen.testIds.js | 2 - .../testIDs/Screens/LoginScreen.testIds.js | 11 ---- .../RevelSecretRecoveryPhrase.testIds.js | 16 ------ 34 files changed, 156 insertions(+), 218 deletions(-) delete mode 100644 e2e/pages/ImportAccountView.js create mode 100644 e2e/pages/importAccount/ImportAccountView.js create mode 100644 e2e/pages/importAccount/SuccessImportAccountView.js create mode 100644 e2e/selectors/ImportAccount/ImportAccountFromPrivateKey.selectors.js create mode 100644 e2e/selectors/ImportAccount/SuccessImportAccount.selectors.js delete mode 100644 e2e/selectors/ImportAccountFromPrivateKey.selectors.js delete mode 100644 wdio/screen-objects/testIDs/Screens/ImportAccountScreen.testIds.js delete mode 100644 wdio/screen-objects/testIDs/Screens/ImportSuccessScreen.testIds.js delete mode 100644 wdio/screen-objects/testIDs/Screens/LoginScreen.testIds.js delete mode 100644 wdio/screen-objects/testIDs/Screens/RevelSecretRecoveryPhrase.testIds.js diff --git a/app/components/UI/LoginOptionsSwitch/LoginOptionsSwitch.tsx b/app/components/UI/LoginOptionsSwitch/LoginOptionsSwitch.tsx index aff83a437bcc..3deb0c478372 100644 --- a/app/components/UI/LoginOptionsSwitch/LoginOptionsSwitch.tsx +++ b/app/components/UI/LoginOptionsSwitch/LoginOptionsSwitch.tsx @@ -1,11 +1,10 @@ import React, { useCallback, useState } from 'react'; -import { Platform, Switch, Text, View } from 'react-native'; +import { Switch, Text, View } from 'react-native'; import { strings } from '../../../../locales/i18n'; import { BIOMETRY_TYPE } from 'react-native-keychain'; import { createStyles } from './styles'; -import { LOGIN_WITH_REMEMBER_ME_SWITCH } from '../../../../wdio/screen-objects/testIDs/Screens/LoginScreen.testIds'; +import { LoginViewSelectors } from '../../../../e2e/selectors/LoginView.selectors'; import { useSelector } from 'react-redux'; -import generateTestId from '../../../../wdio/utils/generateTestId'; import { LoginOptionsSwitchSelectorsIDs } from '../../../../e2e/selectors/LoginOptionsSwitch.selectors'; import { useTheme } from '../../../util/theme'; @@ -90,7 +89,7 @@ const LoginOptionsSwitch = ({ }} thumbColor={theme.brandColors.white} ios_backgroundColor={colors.border.muted} - {...generateTestId(Platform, LOGIN_WITH_REMEMBER_ME_SWITCH)} + testID={LoginViewSelectors.REMEMBER_ME_SWITCH} /> </View> ); diff --git a/app/components/Views/ImportPrivateKey/index.tsx b/app/components/Views/ImportPrivateKey/index.tsx index 75e45fb4147a..38f6e614c279 100644 --- a/app/components/Views/ImportPrivateKey/index.tsx +++ b/app/components/Views/ImportPrivateKey/index.tsx @@ -19,7 +19,7 @@ import Device from '../../../util/device'; import { importAccountFromPrivateKey } from '../../../util/address'; import { useAppTheme } from '../../../util/theme'; import { createStyles } from './styles'; -import { ImportAccountFromPrivateKeySelectorsIDs } from '../../../../e2e/selectors/ImportAccountFromPrivateKey.selectors'; +import { ImportAccountFromPrivateKeyIDs } from '../../../../e2e/selectors/ImportAccount/ImportAccountFromPrivateKey.selectors'; import { QRTabSwitcherScreens } from '../QRTabSwitcher'; import Routes from '../../../constants/navigation/Routes'; @@ -129,7 +129,7 @@ const ImportPrivateKey = () => { <View style={styles.content} testID={ - ImportAccountFromPrivateKeySelectorsIDs.IMPORT_ACCOUNT_SCREEN_ID + ImportAccountFromPrivateKeyIDs.CONTAINER } > <TouchableOpacity onPress={dismiss} style={styles.navbarRightButton}> @@ -138,7 +138,7 @@ const ImportPrivateKey = () => { size={15} style={styles.closeIcon} testID={ - ImportAccountFromPrivateKeySelectorsIDs.CLOSE_BUTTON_ON_IMPORT_ACCOUNT_SCREEN_ID + ImportAccountFromPrivateKeyIDs.CLOSE_BUTTON } /> </TouchableOpacity> @@ -171,7 +171,7 @@ const ImportPrivateKey = () => { style={[styles.input, inputWidth ? { width: inputWidth } : {}]} onChangeText={setPrivateKey} testID={ - ImportAccountFromPrivateKeySelectorsIDs.PRIVATE_KEY_INPUT_BOX_ID + ImportAccountFromPrivateKeyIDs.PRIVATE_KEY_INPUT_BOX } blurOnSubmit onSubmitEditing={() => goNext()} @@ -196,7 +196,7 @@ const ImportPrivateKey = () => { type={'confirm'} onPress={() => goNext()} testID={ - ImportAccountFromPrivateKeySelectorsIDs.IMPORT_PRIVATE_KEY_BUTTON_ID + ImportAccountFromPrivateKeyIDs.IMPORT_BUTTON } > {loading ? ( diff --git a/app/components/Views/Login/index.js b/app/components/Views/Login/index.js index 7820e848bb50..488804f7a367 100644 --- a/app/components/Views/Login/index.js +++ b/app/components/Views/Login/index.js @@ -54,7 +54,6 @@ import { parseVaultValue } from '../../../util/validators'; import { getVaultFromBackup } from '../../../core/BackupVault'; import { containsErrorMessage } from '../../../util/errorHandling'; import { MetaMetricsEvents } from '../../../core/Analytics'; -import { RevealSeedViewSelectorsIDs } from '../../../../e2e/selectors/Settings/SecurityAndPrivacy/RevealSeedView.selectors'; import { LoginViewSelectors } from '../../../../e2e/selectors/LoginView.selectors'; import { withMetricsAwareness } from '../../../components/hooks/useMetrics'; import trackErrorAsAnalytics from '../../../util/metrics/TrackError/trackErrorAsAnalytics'; @@ -539,7 +538,7 @@ class Login extends PureComponent { <Text style={styles.title} - testID={LoginViewSelectors.LOGIN_VIEW_TITLE_ID} + testID={LoginViewSelectors.TITLE_ID} > {strings('login.title')} </Text> @@ -554,7 +553,7 @@ class Login extends PureComponent { style={styles.input} placeholder={strings('login.password')} placeholderTextColor={colors.text.muted} - testID={RevealSeedViewSelectorsIDs.PASSWORD_INPUT} + testID={LoginViewSelectors.PASSWORD_INPUT} returnKeyType={'done'} autoCapitalize="none" secureTextEntry diff --git a/e2e/pages/CommonView.js b/e2e/pages/CommonView.js index bd7a985e8592..cf79ce51d430 100644 --- a/e2e/pages/CommonView.js +++ b/e2e/pages/CommonView.js @@ -1,6 +1,9 @@ import Matchers from '../utils/Matchers'; import Gestures from '../utils/Gestures'; -import { CommonSelectorsIDs } from '../selectors/Common.selectors'; +import { + CommonSelectorsIDs, + CommonSelectorsText +} from '../selectors/Common.selectors'; class CommonView { get okAlertByText() { @@ -15,6 +18,10 @@ class CommonView { return Matchers.getElementByID(CommonSelectorsIDs.ERROR_MESSAGE); } + get okAlertButton() { + return Matchers.getElementByText(CommonSelectorsText.OK_ALERT_BUTTON); + } + async tapBackButton() { await Gestures.waitAndTap(this.backButton); } @@ -22,6 +29,10 @@ class CommonView { async tapOkAlert() { await Gestures.waitAndTap(this.okAlertByText); } + + async tapOKAlertButton() { + await Gestures.waitAndTap(this.okAlertButton); + } } export default new CommonView(); diff --git a/e2e/pages/ImportAccountView.js b/e2e/pages/ImportAccountView.js deleted file mode 100644 index 50bfe0b46ab0..000000000000 --- a/e2e/pages/ImportAccountView.js +++ /dev/null @@ -1,53 +0,0 @@ -import TestHelpers from '../helpers'; -import { - IMPORT_ACCOUNT_SCREEN_ID, - PRIVATE_KEY_INPUT_BOX_ID, - IMPORT_PRIVATE_KEY_BUTTON_ID, -} from '../../wdio/screen-objects/testIDs/Screens/ImportAccountScreen.testIds'; -import { - IMPORT_SUCESS_SCREEN_ID, - IMPORT_SUCESS_SCREEN_CLOSE_BUTTON_ID, -} from '../../wdio/screen-objects/testIDs/Screens/ImportSuccessScreen.testIds'; -import { CommonSelectorsText } from '../selectors/Common.selectors'; - -export default class ImportAccountView { - static async tapImportButton() { - if (device.getPlatform() === 'ios') { - await TestHelpers.waitAndTap(IMPORT_PRIVATE_KEY_BUTTON_ID); - } else { - await TestHelpers.waitAndTapByLabel(IMPORT_PRIVATE_KEY_BUTTON_ID); - } - } - - static async tapOKAlertButton() { - await TestHelpers.tapAlertWithButton(CommonSelectorsText.OK_ALERT_BUTTON); - } - - static async enterPrivateKey(privateKey) { - if (device.getPlatform() === 'android') { - await TestHelpers.replaceTextInField( - PRIVATE_KEY_INPUT_BOX_ID, - privateKey, - ); - await element(by.id(PRIVATE_KEY_INPUT_BOX_ID)).tapReturnKey(); - } else { - await TestHelpers.typeTextAndHideKeyboard( - PRIVATE_KEY_INPUT_BOX_ID, - privateKey, - ); - } - } - - // Closing import success view - static async tapCloseButtonOnImportSuccess() { - await TestHelpers.tap(IMPORT_SUCESS_SCREEN_CLOSE_BUTTON_ID); - } - - static async isVisible() { - await TestHelpers.checkIfVisible(IMPORT_ACCOUNT_SCREEN_ID); - } - - static async isImportSuccessSreenVisible() { - await TestHelpers.checkIfVisible(IMPORT_SUCESS_SCREEN_ID); - } -} diff --git a/e2e/pages/LoginView.js b/e2e/pages/LoginView.js index c3eb6f1d0670..acf5dbbadb85 100644 --- a/e2e/pages/LoginView.js +++ b/e2e/pages/LoginView.js @@ -1,30 +1,35 @@ -import TestHelpers from '../helpers'; -import { LOGIN_WITH_REMEMBER_ME_SWITCH } from '../../wdio/screen-objects/testIDs/Screens/LoginScreen.testIds'; -import { RevealSeedViewSelectorsIDs } from '../selectors/Settings/SecurityAndPrivacy/RevealSeedView.selectors'; import { LoginViewSelectors } from '../selectors/LoginView.selectors'; import Matchers from '../utils/Matchers'; +import Gestures from '../utils/Gestures'; -export default class LoginView { - static async getContainer() { +class LoginView { + get container() { return Matchers.getElementByID(LoginViewSelectors.CONTAINER); } - static async enterPassword(password) { - await TestHelpers.typeTextAndHideKeyboard( - RevealSeedViewSelectorsIDs.PASSWORD_INPUT, - password, - ); + get passwordInput() { + return Matchers.getElementByID(LoginViewSelectors.PASSWORD_INPUT); } - static async tapResetWalletButton() { - await TestHelpers.tap(LoginViewSelectors.RESET_WALLET); + get resetWalletButton() { + return Matchers.getElementByID(LoginViewSelectors.RESET_WALLET); } - static async toggleRememberMe() { - await TestHelpers.tap(LOGIN_WITH_REMEMBER_ME_SWITCH); + get rememberMeSwitch() { + return Matchers.getElementByID(LoginViewSelectors.REMEMBER_ME_SWITCH); } - static async isVisible() { - await TestHelpers.checkIfVisible(LoginViewSelectors.CONTAINER); + async enterPassword(password) { + await Gestures.typeTextAndHideKeyboard(this.passwordInput, password); + } + + async tapResetWalletButton() { + await Gestures.waitAndTap(this.resetWalletButton); + } + + async toggleRememberMeSwitch() { + await Gestures.waitAndTap(this.rememberMeSwitch); } } + +export default new LoginView(); diff --git a/e2e/pages/Settings/SecurityAndPrivacy/RevealSecretRecoveryPhrase.js b/e2e/pages/Settings/SecurityAndPrivacy/RevealSecretRecoveryPhrase.js index c4e87076984e..539fdfed4124 100644 --- a/e2e/pages/Settings/SecurityAndPrivacy/RevealSecretRecoveryPhrase.js +++ b/e2e/pages/Settings/SecurityAndPrivacy/RevealSecretRecoveryPhrase.js @@ -12,19 +12,12 @@ class RevealSecretRecoveryPhrase { ); } - // This is the password requested at login - // and should probably be moved eventually into LoginView.js - get passwordInput() { - return Matchers.getElementByID(RevealSeedViewSelectorsIDs.PASSWORD_INPUT); - } - get passwordWarning() { return Matchers.getElementByID( RevealSeedViewSelectorsIDs.PASSWORD_WARNING_ID, ); } - // This is the password requested to expose secret credentials get passwordInputToRevealCredential() { return Matchers.getElementByID( RevealSeedViewSelectorsIDs.PASSWORD_INPUT_BOX_ID, @@ -37,11 +30,6 @@ class RevealSecretRecoveryPhrase { ); } - get recoveryPhrase() { - return Matchers.getElementByText( - RevealSeedViewSelectorsIDs.REVEAL_CREDENTIAL_TEXT, - ); - } get revealSecretRecoveryPhraseButton() { return Matchers.getElementByID( RevealSeedViewSelectorsIDs.REVEAL_CREDENTIAL_BUTTON_ID, @@ -72,12 +60,6 @@ class RevealSecretRecoveryPhrase { ); } - // This is the password requested at login view - // and should probably be moved eventually into LoginView.js - async enterPassword(password) { - await Gestures.typeTextAndHideKeyboard(this.passwordInput, password); - } - // This is the password requested to expose secret credentials async enterPasswordToRevealSecretCredential(password) { await Gestures.typeTextAndHideKeyboard( this.passwordInputToRevealCredential, diff --git a/e2e/pages/importAccount/ImportAccountView.js b/e2e/pages/importAccount/ImportAccountView.js new file mode 100644 index 000000000000..06fded496b34 --- /dev/null +++ b/e2e/pages/importAccount/ImportAccountView.js @@ -0,0 +1,32 @@ +import Matchers from '../../utils/Matchers'; +import Gestures from '../../utils/Gestures'; +import { ImportAccountFromPrivateKeyIDs } from '../../selectors/ImportAccount/ImportAccountFromPrivateKey.selectors'; + +class ImportAccountView { + get container() { + return Matchers.getElementByID(ImportAccountFromPrivateKeyIDs.CONTAINER); + } + + get importButton() { + return device.getPlatform() === 'ios' + ? Matchers.getElementByID(ImportAccountFromPrivateKeyIDs.IMPORT_BUTTON) + : Matchers.getElementByLabel(ImportAccountFromPrivateKeyIDs.IMPORT_BUTTON); + } + + get privateKeyField() { + return Matchers.getElementByID(ImportAccountFromPrivateKeyIDs.PRIVATE_KEY_INPUT_BOX); + } + + async tapImportButton() { + await Gestures.waitAndTap(this.importButton); + } + + async enterPrivateKey(privateKey) { + await Gestures.typeTextAndHideKeyboard( + this.privateKeyField, + privateKey, + ); + } +} + +export default new ImportAccountView(); diff --git a/e2e/pages/importAccount/SuccessImportAccountView.js b/e2e/pages/importAccount/SuccessImportAccountView.js new file mode 100644 index 000000000000..647124535c70 --- /dev/null +++ b/e2e/pages/importAccount/SuccessImportAccountView.js @@ -0,0 +1,19 @@ +import Matchers from '../../utils/Matchers'; +import Gestures from '../../utils/Gestures'; +import { SuccessImportAccountIDs } from '../../selectors/ImportAccount/SuccessImportAccount.selectors'; + +class SuccessImportAccountView { + get container() { + return Matchers.getElementByID(SuccessImportAccountIDs.CONTAINER); + } + + get closeButton() { + return Matchers.getElementByID(SuccessImportAccountIDs.CLOSE_BUTTON); + } + + async tapCloseButton() { + await Gestures.waitAndTap(this.closeButton); + } +} + +export default new SuccessImportAccountView(); diff --git a/e2e/pages/wallet/WalletView.js b/e2e/pages/wallet/WalletView.js index 97534960ef53..d89d168e18b9 100644 --- a/e2e/pages/wallet/WalletView.js +++ b/e2e/pages/wallet/WalletView.js @@ -21,10 +21,6 @@ class WalletView { ); } - get okAlertButton() { - return Matchers.getElementByText(CommonSelectorsText.OK_ALERT_BUTTON); - } - get accountIcon() { return Matchers.getElementByID(WalletViewSelectorsIDs.ACCOUNT_ICON); } @@ -83,10 +79,6 @@ class WalletView { await Gestures.waitAndTap(this.mainWalletAccountActions); } - async tapOKAlertButton() { - await Gestures.waitAndTap(this.okAlertButton); - } - async tapOnToken(token) { const element = Matchers.getElementByText( token || WalletViewSelectorsText.DEFAULT_TOKEN, diff --git a/e2e/selectors/ImportAccount/ImportAccountFromPrivateKey.selectors.js b/e2e/selectors/ImportAccount/ImportAccountFromPrivateKey.selectors.js new file mode 100644 index 000000000000..f4178eaa770e --- /dev/null +++ b/e2e/selectors/ImportAccount/ImportAccountFromPrivateKey.selectors.js @@ -0,0 +1,6 @@ +export const ImportAccountFromPrivateKeyIDs = { + CONTAINER: 'import-account-screen', + PRIVATE_KEY_INPUT_BOX: 'input-private-key', + IMPORT_BUTTON: 'import-button', + CLOSE_BUTTON: 'close-button-on-account-screen', +}; diff --git a/e2e/selectors/ImportAccount/SuccessImportAccount.selectors.js b/e2e/selectors/ImportAccount/SuccessImportAccount.selectors.js new file mode 100644 index 000000000000..e09abf7e0cf9 --- /dev/null +++ b/e2e/selectors/ImportAccount/SuccessImportAccount.selectors.js @@ -0,0 +1,4 @@ +export const SuccessImportAccountIDs = { + CONTAINER: 'import-success-screen', + CLOSE_BUTTON: 'import-close-button', +}; diff --git a/e2e/selectors/ImportAccountFromPrivateKey.selectors.js b/e2e/selectors/ImportAccountFromPrivateKey.selectors.js deleted file mode 100644 index c880ac502bec..000000000000 --- a/e2e/selectors/ImportAccountFromPrivateKey.selectors.js +++ /dev/null @@ -1,6 +0,0 @@ -export const ImportAccountFromPrivateKeySelectorsIDs = { - IMPORT_ACCOUNT_SCREEN_ID: 'import-account-screen', - PRIVATE_KEY_INPUT_BOX_ID: 'input-private-key', - IMPORT_PRIVATE_KEY_BUTTON_ID: 'import-button', - CLOSE_BUTTON_ON_IMPORT_ACCOUNT_SCREEN_ID: 'close-button-on-account-screen', -}; diff --git a/e2e/selectors/LoginView.selectors.js b/e2e/selectors/LoginView.selectors.js index 1465d83322bf..2bcc8e46f25b 100644 --- a/e2e/selectors/LoginView.selectors.js +++ b/e2e/selectors/LoginView.selectors.js @@ -3,5 +3,7 @@ export const LoginViewSelectors = { PASSWORD_ERROR: 'invalid-password-error', RESET_WALLET: 'reset-wallet-button', LOGIN_BUTTON_ID: 'log-in-button', - LOGIN_VIEW_TITLE_ID: 'login-title', + TITLE_ID: 'login-title', + REMEMBER_ME_SWITCH: 'login-with-remember-me-switch', + PASSWORD_INPUT: 'login-password-input', }; diff --git a/e2e/selectors/Settings/SecurityAndPrivacy/RevealSeedView.selectors.js b/e2e/selectors/Settings/SecurityAndPrivacy/RevealSeedView.selectors.js index 38884d0a01db..955a8217b1c4 100644 --- a/e2e/selectors/Settings/SecurityAndPrivacy/RevealSeedView.selectors.js +++ b/e2e/selectors/Settings/SecurityAndPrivacy/RevealSeedView.selectors.js @@ -3,7 +3,6 @@ import enContent from '../../../../locales/languages/en.json'; export const RevealSeedViewSelectorsIDs = { REVEAL_CREDENTIAL_CONTAINER_ID: 'reveal-private-credential-screen', REVEAL_CREDENTIAL_SCROLL_ID: 'reveal-credential-scroll', - PASSWORD_INPUT: 'login-password-input', PASSWORD_WARNING_ID: 'password-warning', REVEAL_CREDENTIAL_COPY_TO_CLIPBOARD_BUTTON: 'reveal-credential-copy-to-clipboard-button', diff --git a/e2e/specs/accounts/auto-lock.spec.js b/e2e/specs/accounts/auto-lock.spec.js index 8923a688efe1..7eb61c6ba6a7 100644 --- a/e2e/specs/accounts/auto-lock.spec.js +++ b/e2e/specs/accounts/auto-lock.spec.js @@ -55,6 +55,6 @@ describe(Regression('Auto-Lock'), () => { await device.sendToHome(); await device.launchApp(); await Assertions.checkIfNotVisible(WalletView.container); - await Assertions.checkIfVisible(LoginView.getContainer()); + await Assertions.checkIfVisible(LoginView.container); }); }); diff --git a/e2e/specs/accounts/change-account-name.spec.js b/e2e/specs/accounts/change-account-name.spec.js index eaae835df2ed..f27e61fee52a 100644 --- a/e2e/specs/accounts/change-account-name.spec.js +++ b/e2e/specs/accounts/change-account-name.spec.js @@ -64,7 +64,7 @@ describe(Regression('Change Account Name'), () => { await SettingsView.scrollToLockButton(); await SettingsView.tapLock(); await SettingsView.tapYesAlertButton(); - await LoginView.isVisible(); + await Assertions.checkIfVisible(LoginView.container); // Unlock wallet and verify updated name persists await loginToApp(); diff --git a/e2e/specs/accounts/import-wallet-account.spec.js b/e2e/specs/accounts/import-wallet-account.spec.js index e69baffdef94..39e0bbe8da83 100644 --- a/e2e/specs/accounts/import-wallet-account.spec.js +++ b/e2e/specs/accounts/import-wallet-account.spec.js @@ -3,9 +3,11 @@ import { SmokeAccounts } from '../../tags'; import WalletView from '../../pages/wallet/WalletView'; import { importWalletWithRecoveryPhrase } from '../../viewHelper'; import AccountListView from '../../pages/AccountListView'; -import ImportAccountView from '../../pages/ImportAccountView'; +import ImportAccountView from '../../pages/importAccount/ImportAccountView'; import Assertions from '../../utils/Assertions'; import AddAccountModal from '../../pages/modals/AddAccountModal'; +import CommonView from '../../pages/CommonView'; +import SuccessImportAccountView from '../../pages/importAccount/SuccessImportAccountView'; describe(SmokeAccounts('Import account via private to wallet'), () => { // This key is for testing private key import only @@ -27,13 +29,13 @@ describe(SmokeAccounts('Import account via private to wallet'), () => { await Assertions.checkIfVisible(AccountListView.accountList); await AccountListView.tapAddAccountButton(); await AddAccountModal.tapImportAccount(); - await ImportAccountView.isVisible(); + await Assertions.checkIfVisible(ImportAccountView.container); // Tap on import button to make sure alert pops up await ImportAccountView.tapImportButton(); - await ImportAccountView.tapOKAlertButton(); + await CommonView.tapOKAlertButton(); await ImportAccountView.enterPrivateKey(TEST_PRIVATE_KEY); - await ImportAccountView.isImportSuccessSreenVisible(); - await ImportAccountView.tapCloseButtonOnImportSuccess(); + await Assertions.checkIfVisible(SuccessImportAccountView.container); + await SuccessImportAccountView.tapCloseButton(); await AccountListView.swipeToDismissAccountsModal(); await Assertions.checkIfVisible(WalletView.container); await Assertions.checkIfElementNotToHaveText( diff --git a/e2e/specs/accounts/imported-account-remove-and-import.spec.js b/e2e/specs/accounts/imported-account-remove-and-import.spec.js index 971ad149e3b7..84847df1141b 100644 --- a/e2e/specs/accounts/imported-account-remove-and-import.spec.js +++ b/e2e/specs/accounts/imported-account-remove-and-import.spec.js @@ -13,10 +13,11 @@ import { getFixturesServerPort } from '../../fixtures/utils'; import { loginToApp } from '../../viewHelper.js'; import WalletView from '../../pages/wallet/WalletView.js'; import AccountListView from '../../pages/AccountListView.js'; -import ImportAccountView from '../../pages/ImportAccountView.js'; +import ImportAccountView from '../../pages/importAccount/ImportAccountView.js'; import Assertions from '../../utils/Assertions.js'; import { AccountListViewSelectorsText } from '../../selectors/AccountListView.selectors.js'; import AddAccountModal from '../../pages/modals/AddAccountModal.js'; +import SuccessImportAccountView from '../../pages/importAccount/SuccessImportAccountView'; const fixtureServer = new FixtureServer(); // This key is for testing private key import only @@ -56,10 +57,10 @@ describe( // Import account again await AccountListView.tapAddAccountButton(); await AddAccountModal.tapImportAccount(); - await ImportAccountView.isVisible(); + await Assertions.checkIfVisible(ImportAccountView.container); await ImportAccountView.enterPrivateKey(TEST_PRIVATE_KEY); - await ImportAccountView.isImportSuccessSreenVisible(); - await ImportAccountView.tapCloseButtonOnImportSuccess(); + await Assertions.checkIfVisible(SuccessImportAccountView.container); + await SuccessImportAccountView.tapCloseButton(); await Assertions.checkIfElementToHaveText( AccountListView.accountTypeLabel, AccountListViewSelectorsText.ACCOUNT_TYPE_LABEL_TEXT, diff --git a/e2e/specs/onboarding/onboarding-wizard-opt-in.spec.js b/e2e/specs/onboarding/onboarding-wizard-opt-in.spec.js index 2f9d2ccc7a50..15aec2290e5e 100644 --- a/e2e/specs/onboarding/onboarding-wizard-opt-in.spec.js +++ b/e2e/specs/onboarding/onboarding-wizard-opt-in.spec.js @@ -118,8 +118,7 @@ describe( it('should relaunch the app and log in', async () => { // Relaunch app await TestHelpers.relaunchApp(); - await TestHelpers.delay(4500); - await LoginView.isVisible(); + await Assertions.checkIfVisible(LoginView.container); await LoginView.enterPassword(PASSWORD); await Assertions.checkIfVisible(WalletView.container); }); diff --git a/e2e/specs/permission-systems/permission-system-delete-wallet.spec.js b/e2e/specs/permission-systems/permission-system-delete-wallet.spec.js index 597773113b53..53a16cb43ff0 100644 --- a/e2e/specs/permission-systems/permission-system-delete-wallet.spec.js +++ b/e2e/specs/permission-systems/permission-system-delete-wallet.spec.js @@ -57,7 +57,7 @@ describe( await TabBarComponent.tapSettings(); await SettingsView.tapLock(); await SettingsView.tapYesAlertButton(); - await LoginView.isVisible(); + await Assertions.checkIfVisible(LoginView.container); // should tap reset wallet button await LoginView.tapResetWalletButton(); diff --git a/e2e/specs/quarantine/deeplinks.failing.js b/e2e/specs/quarantine/deeplinks.failing.js index 0f6533a03f65..468de71d89b0 100644 --- a/e2e/specs/quarantine/deeplinks.failing.js +++ b/e2e/specs/quarantine/deeplinks.failing.js @@ -13,7 +13,7 @@ import LoginView from '../../pages/LoginView'; import TransactionConfirmationView from '../../pages/Send/TransactionConfirmView'; import SecurityAndPrivacy from '../../pages/Settings/SecurityAndPrivacy/SecurityAndPrivacyView'; - +import CommonView from '../../pages/CommonView'; import WalletView from '../../pages/wallet/WalletView'; import { importWalletWithRecoveryPhrase } from '../../viewHelper'; import Accounts from '../../../wdio/helpers/Accounts'; @@ -68,9 +68,8 @@ describe(Regression('Deep linking Tests'), () => { it('should relaunch the app then enable remember me', async () => { // Relaunch app await TestHelpers.relaunchApp(); - await LoginView.isVisible(); - await LoginView.toggleRememberMe(); - + await Assertions.checkIfVisible(LoginView.container); + await LoginView.toggleRememberMeSwitch(); await LoginView.enterPassword(validAccount.password); await Assertions.checkIfVisible(WalletView.container); }); @@ -80,8 +79,7 @@ describe(Regression('Deep linking Tests'), () => { await TestHelpers.delay(3000); await TestHelpers.checkIfElementWithTextIsVisible(networkNotFoundText); await TestHelpers.checkIfElementWithTextIsVisible(networkErrorBodyMessage); - - await WalletView.tapOKAlertButton(); + await CommonView.tapOKAlertButton(); }); it('should go to settings then networks', async () => { diff --git a/e2e/specs/quarantine/permission-system-removing-imported-account.failing.js b/e2e/specs/quarantine/permission-system-removing-imported-account.failing.js index e60884150595..b0ae125869f8 100644 --- a/e2e/specs/quarantine/permission-system-removing-imported-account.failing.js +++ b/e2e/specs/quarantine/permission-system-removing-imported-account.failing.js @@ -2,7 +2,7 @@ import TestHelpers from '../../helpers'; import { Regression } from '../../tags'; import WalletView from '../../pages/wallet/WalletView'; -import ImportAccountView from '../../pages/ImportAccountView'; +import ImportAccountView from '../../pages/importAccount/ImportAccountView'; import TabBarComponent from '../../pages/TabBarComponent'; import Browser from '../../pages/Browser/BrowserView'; @@ -19,6 +19,7 @@ import { TestDApp } from '../../pages/Browser/TestDApp'; import { importWalletWithRecoveryPhrase } from '../../viewHelper'; import AddAccountModal from '../../pages/modals/AddAccountModal'; import Assertions from '../../utils/Assertions'; +import SuccessImportAccountView from '../../pages/importAccount/SuccessImportAccountView'; const SEPOLIA = 'Sepolia'; @@ -45,7 +46,7 @@ describe( it('should trigger connect modal in the test dapp', async () => { await TestHelpers.delay(3000); //TODO: Create goToTestDappAndTapConnectButton method. - await TestDApp.goToTestDappAndTapConnectButton(); + // await TestDApp.goToTestDappAndTapConnectButton(); }); it('should go to multiconnect in the connect account modal', async () => { @@ -56,10 +57,10 @@ describe( it('should import account', async () => { await ConnectModal.tapImportAccountOrHWButton(); await AddAccountModal.tapImportAccount(); - await ImportAccountView.isVisible(); + await Assertions.checkIfVisible(ImportAccountView.container); await ImportAccountView.enterPrivateKey(accountPrivateKey.keys); - await ImportAccountView.isImportSuccessSreenVisible(); - await ImportAccountView.tapCloseButtonOnImportSuccess(); + await Assertions.checkIfVisible(SuccessImportAccountView.container); + await SuccessImportAccountView.tapCloseButton(); }); it('should connect multiple accounts to a dapp', async () => { @@ -73,7 +74,7 @@ describe( await ConnectedAccountsModal.tapNetworksPicker(); await Assertions.checkIfVisible(NetworkListModal.networkScroll); await NetworkListModal.tapTestNetworkSwitch(); - await NetworkListModal.changeNetwork(SEPOLIA); + await NetworkListModal.changeNetworkTo(SEPOLIA); }); it('should dismiss the network education modal', async () => { diff --git a/e2e/specs/settings/delete-wallet.spec.js b/e2e/specs/settings/delete-wallet.spec.js index fa11374fb5d1..e2a730f84f71 100644 --- a/e2e/specs/settings/delete-wallet.spec.js +++ b/e2e/specs/settings/delete-wallet.spec.js @@ -53,7 +53,7 @@ describe( await CommonView.tapBackButton(); await SettingsView.tapLock(); await SettingsView.tapYesAlertButton(); - await LoginView.isVisible(); + await Assertions.checkIfVisible(LoginView.container); // should tap reset wallet button await LoginView.tapResetWalletButton(); diff --git a/e2e/specs/wallet/suggestedGasApi.mock.spec.js b/e2e/specs/wallet/suggestedGasApi.mock.spec.js index e0433a692034..ef919eaf9e00 100644 --- a/e2e/specs/wallet/suggestedGasApi.mock.spec.js +++ b/e2e/specs/wallet/suggestedGasApi.mock.spec.js @@ -11,12 +11,13 @@ import WalletView from '../../pages/wallet/WalletView.js'; import Assertions from '../../utils/Assertions.js'; import AccountListView from '../../pages/AccountListView.js'; import AddAccountModal from '../../pages/modals/AddAccountModal.js'; -import ImportAccountView from '../../pages/ImportAccountView.js'; +import ImportAccountView from '../../pages/importAccount/ImportAccountView.js'; import Accounts from '../../../wdio/helpers/Accounts.js'; import { withFixtures } from '../../fixtures/fixture-helper.js'; import FixtureBuilder from '../../fixtures/fixture-builder.js'; import TestHelpers from '../../helpers.js'; import { urls } from '../../mockServer/mockUrlCollection.json'; +import SuccessImportAccountView from '../../pages/importAccount/SuccessImportAccountView'; describe(SmokeCore('Mock suggestedGasApi fallback to legacy gas endpoint when EIP1559 endpoint is down'), () => { let mockServer; @@ -59,10 +60,10 @@ describe(SmokeCore('Mock suggestedGasApi fallback to legacy gas endpoint when E await Assertions.checkIfVisible(AccountListView.accountList); await AccountListView.tapAddAccountButton(); await AddAccountModal.tapImportAccount(); - await ImportAccountView.isVisible(); + await Assertions.checkIfVisible(ImportAccountView.container); await ImportAccountView.enterPrivateKey(validPrivateKey.keys); - await ImportAccountView.isImportSuccessSreenVisible(); - await ImportAccountView.tapCloseButtonOnImportSuccess(); + await Assertions.checkIfVisible(SuccessImportAccountView.container); + await SuccessImportAccountView.tapCloseButton(); if (device.getPlatform() === 'ios') { await AccountListView.swipeToDismissAccountsModal(); await Assertions.checkIfNotVisible(AccountListView.title); diff --git a/e2e/utils/Gestures.js b/e2e/utils/Gestures.js index de713fb613a7..bac9b420f0d6 100644 --- a/e2e/utils/Gestures.js +++ b/e2e/utils/Gestures.js @@ -50,7 +50,7 @@ class Gestures { /** * Wait for an element to be visible and then tap it. * - * @param {Promise<Detox.IndexableNativeElement>} elementID - ID of the element to tap + * @param {Promise<Detox.IndexableNativeElement | Detox.SystemElement>} elementID - ID of the element to tap * @param {number} timeout - Timeout for waiting (default: 8000ms) */ static async waitAndTap(elementID, timeout = 15000) { diff --git a/e2e/viewHelper.js b/e2e/viewHelper.js index ad7438dadc97..6daccd31fc6c 100644 --- a/e2e/viewHelper.js +++ b/e2e/viewHelper.js @@ -210,6 +210,6 @@ export const switchToSepoliaNetwork = async () => { export const loginToApp = async () => { const PASSWORD = '123123123'; - await LoginView.isVisible(); + await Assertions.checkIfVisible(LoginView.container); await LoginView.enterPassword(PASSWORD); }; diff --git a/wdio/screen-objects/ImportAccountScreen.js b/wdio/screen-objects/ImportAccountScreen.js index 7388d978cb50..8f19e63591b1 100644 --- a/wdio/screen-objects/ImportAccountScreen.js +++ b/wdio/screen-objects/ImportAccountScreen.js @@ -1,26 +1,19 @@ /* eslint-disable no-undef */ import Gestures from '../helpers/Gestures'; import Selectors from '../helpers/Selectors'; -import { - IMPORT_PRIVATE_KEY_BUTTON_ID, - PRIVATE_KEY_INPUT_BOX_ID, - IMPORT_ACCOUNT_SCREEN_ID, - CLOSE_BUTTON_ON_IMPORT_ACCOUNT_SCREEN_ID, -} from './testIDs/Screens/ImportAccountScreen.testIds'; +import { ImportAccountFromPrivateKeyIDs } from '../../e2e/selectors/ImportAccount/ImportAccountFromPrivateKey.selectors'; class ImportAccountScreen { get importAccountContainer() { - return Selectors.getXpathElementByResourceId(IMPORT_ACCOUNT_SCREEN_ID); + return Selectors.getXpathElementByResourceId(ImportAccountFromPrivateKeyIDs.CONTAINER); } get closeButton() { - return Selectors.getElementByPlatform( - CLOSE_BUTTON_ON_IMPORT_ACCOUNT_SCREEN_ID, - ); + return Selectors.getElementByPlatform(ImportAccountFromPrivateKeyIDs.CLOSE_BUTTON); } get privateKeyInputBox() { - return Selectors.getXpathElementByResourceId(PRIVATE_KEY_INPUT_BOX_ID); + return Selectors.getXpathElementByResourceId(ImportAccountFromPrivateKeyIDs.PRIVATE_KEY_INPUT_BOX); } async typePrivateKeyAndDismissKeyboard(privateKey) { diff --git a/wdio/screen-objects/ImportSuccessScreen.js b/wdio/screen-objects/ImportSuccessScreen.js index e0ec4132162e..155939307f67 100644 --- a/wdio/screen-objects/ImportSuccessScreen.js +++ b/wdio/screen-objects/ImportSuccessScreen.js @@ -1,17 +1,14 @@ import Gestures from '../helpers/Gestures'; import Selectors from '../helpers/Selectors'; -import { - IMPORT_SUCESS_SCREEN_CLOSE_BUTTON_ID, - IMPORT_SUCESS_SCREEN_ID, -} from './testIDs/Screens/ImportSuccessScreen.testIds'; +import { SuccessImportAccountIDs } from '../../e2e/selectors/ImportAccount/SuccessImportAccount.selectors'; class ImportAccountScreen { get container() { - return Selectors.getXpathElementByResourceId(IMPORT_SUCESS_SCREEN_ID); + return Selectors.getXpathElementByResourceId(SuccessImportAccountIDs.CONTAINER); } get closeButton() { - return Selectors.getXpathElementByResourceId(IMPORT_SUCESS_SCREEN_CLOSE_BUTTON_ID); + return Selectors.getXpathElementByResourceId(SuccessImportAccountIDs.CLOSE_BUTTON); } async tapCloseButton() { diff --git a/wdio/screen-objects/LoginScreen.js b/wdio/screen-objects/LoginScreen.js index 702c4156e2c9..0784c7eb92d1 100644 --- a/wdio/screen-objects/LoginScreen.js +++ b/wdio/screen-objects/LoginScreen.js @@ -1,37 +1,30 @@ import Gestures from '../helpers/Gestures'; import Selectors from '../helpers/Selectors'; -import { - LOGIN_VIEW_PASSWORD_INPUT_ID, - LOGIN_VIEW_RESET_WALLET_ID, - LOGIN_VIEW_SCREEN_ID, - LOGIN_VIEW_TITLE_ID, - LOGIN_VIEW_UNLOCK_BUTTON_ID, - LOGIN_WITH_REMEMBER_ME_SWITCH, -} from './testIDs/Screens/LoginScreen.testIds'; +import { LoginViewSelectors } from '../../e2e/selectors/LoginView.selectors'; class LoginScreen { get loginScreen() { - return Selectors.getXpathElementByResourceId(LOGIN_VIEW_SCREEN_ID); + return Selectors.getXpathElementByResourceId(LoginViewSelectors.CONTAINER); } get resetWalletButton() { - return Selectors.getXpathElementByResourceId(LOGIN_VIEW_RESET_WALLET_ID); + return Selectors.getXpathElementByResourceId(LoginViewSelectors.RESET_WALLET); } get passwordInput() { - return Selectors.getXpathElementByResourceId(LOGIN_VIEW_PASSWORD_INPUT_ID); + return Selectors.getXpathElementByResourceId(LoginViewSelectors.PASSWORD_INPUT); } get unlockButton() { - return Selectors.getXpathElementByResourceId(LOGIN_VIEW_UNLOCK_BUTTON_ID); + return Selectors.getXpathElementByResourceId(LoginViewSelectors.LOGIN_BUTTON_ID); } get title() { - return Selectors.getXpathElementByResourceId(LOGIN_VIEW_TITLE_ID); + return Selectors.getXpathElementByResourceId(LoginViewSelectors.TITLE_ID); } get rememberMeToggle() { - return Selectors.getXpathElementByResourceId(LOGIN_WITH_REMEMBER_ME_SWITCH); + return Selectors.getXpathElementByResourceId(LoginViewSelectors.REMEMBER_ME_SWITCH); } async isLoginScreenVisible() { @@ -63,10 +56,6 @@ class LoginScreen { async tapRememberMeToggle() { await Gestures.waitAndTap(this.rememberMeToggle); } - - async isRememberMeToggle(value) { - await expect(this.rememberMeToggle).toHaveText(value); - } } export default new LoginScreen(); diff --git a/wdio/screen-objects/testIDs/Screens/ImportAccountScreen.testIds.js b/wdio/screen-objects/testIDs/Screens/ImportAccountScreen.testIds.js deleted file mode 100644 index 3208ad0b485c..000000000000 --- a/wdio/screen-objects/testIDs/Screens/ImportAccountScreen.testIds.js +++ /dev/null @@ -1,5 +0,0 @@ -export const IMPORT_ACCOUNT_SCREEN_ID = 'import-account-screen'; -export const PRIVATE_KEY_INPUT_BOX_ID = 'input-private-key'; -export const IMPORT_PRIVATE_KEY_BUTTON_ID = 'import-button'; -export const CLOSE_BUTTON_ON_IMPORT_ACCOUNT_SCREEN_ID = - 'close-button-on-account-screen'; diff --git a/wdio/screen-objects/testIDs/Screens/ImportSuccessScreen.testIds.js b/wdio/screen-objects/testIDs/Screens/ImportSuccessScreen.testIds.js deleted file mode 100644 index 70c622a0a529..000000000000 --- a/wdio/screen-objects/testIDs/Screens/ImportSuccessScreen.testIds.js +++ /dev/null @@ -1,2 +0,0 @@ -export const IMPORT_SUCESS_SCREEN_ID = 'import-success-screen'; -export const IMPORT_SUCESS_SCREEN_CLOSE_BUTTON_ID = 'import-close-button'; diff --git a/wdio/screen-objects/testIDs/Screens/LoginScreen.testIds.js b/wdio/screen-objects/testIDs/Screens/LoginScreen.testIds.js deleted file mode 100644 index 5a474c5c8b69..000000000000 --- a/wdio/screen-objects/testIDs/Screens/LoginScreen.testIds.js +++ /dev/null @@ -1,11 +0,0 @@ -export const LOGIN_VIEW_RESET_WALLET_ID = 'reset-wallet-button'; - -export const LOGIN_VIEW_PASSWORD_INPUT_ID = 'login-password-input'; - -export const LOGIN_VIEW_UNLOCK_BUTTON_ID = 'log-in-button'; - -export const LOGIN_VIEW_SCREEN_ID = 'login'; - -export const LOGIN_VIEW_TITLE_ID = 'login-title'; - -export const LOGIN_WITH_REMEMBER_ME_SWITCH = 'login-with-remember-me-switch'; diff --git a/wdio/screen-objects/testIDs/Screens/RevelSecretRecoveryPhrase.testIds.js b/wdio/screen-objects/testIDs/Screens/RevelSecretRecoveryPhrase.testIds.js deleted file mode 100644 index f733e0d34ce6..000000000000 --- a/wdio/screen-objects/testIDs/Screens/RevelSecretRecoveryPhrase.testIds.js +++ /dev/null @@ -1,16 +0,0 @@ -export const SECRET_RECOVERY_PHRASE_CONTAINER_ID = - 'reveal-private-credential-screen'; -export const PASSWORD_INPUT_BOX_ID = 'private-credential-password-text-input'; -export const PASSWORD_WARNING_ID = 'password-warning'; -export const REVEAL_SECRET_RECOVERY_PHRASE_TOUCHABLE_BOX_ID = - 'private-credential-touchable'; -export const SECRET_RECOVERY_PHRASE_TEXT = 'private-credential-text'; - -export const SECRET_RECOVERY_PHRASE_CANCEL_BUTTON_ID = - 'reveal-private-credential-cancel-button'; - -export const SECRET_RECOVERY_PHRASE_NEXT_BUTTON_ID = - 'reveal-private-credential-next-button'; - -export const SECRET_RECOVERY_PHRASE_LONG_PRESS_BUTTON_ID = - 'reveal-private-long-press-button'; From 464fcacb10ed8860850772254534af579caed3f5 Mon Sep 17 00:00:00 2001 From: Vince Howard <vincenguyenhoward@gmail.com> Date: Wed, 9 Oct 2024 14:52:02 -0600 Subject: [PATCH 09/46] feat(1702): enhanced onboarding settings config (#11127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR enhances the onboarding experience by allowing users to configure key settings before accessing their wallet. This change aims to improve user control and understanding of the app's features from the outset. Some copy was also added or changed ### New Screens Added the following screens to the `OnboardingSuccessFlow`: - `OnboardingGeneralSettings` - `OnboardingAssetsSettings` - `OnboardingSecuritySettings` ## **Related issues** Feature: [#1702](https://github.com/MetaMask/mobile-planning/issues/1702) ## **Manual testing steps** 1. Fresh install application 2. Create/import new wallet 3. Success screen will appear with the title "Your Wallet is ready" and below the title and description, click on the text that says "Manage default settings" 4. You will be routed to the onboarded settings screen with the options General, Assets, and Security ## **Screenshots/Recordings** | Before | After | |:---:|:---:| |![before](https://github.com/user-attachments/assets/578fe31d-700b-4e62-a151-755074865250)|![after](https://github.com/user-attachments/assets/2c29a1e9-2e4e-474f-8a0a-2450c7c7e352)| ### **Before** NA ### **After** NA ## **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** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] 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. --- app/components/Nav/App/index.js | 18 + .../__snapshots__/ManageNetworks.test.js.snap | 2 +- .../__snapshots__/index.test.tsx.snap | 330 +++++++ .../DefaultSettings/index.styles.ts | 43 +- .../DefaultSettings/index.test.tsx | 76 ++ .../DefaultSettings/index.tsx | 110 +-- .../__snapshots__/index.test.tsx.snap | 824 ++++++++++++++++++ .../OnboardingAssetsSettings/index.styles.ts | 16 + .../OnboardingAssetsSettings/index.test.tsx | 55 ++ .../OnboardingAssetsSettings/index.tsx | 33 + .../__snapshots__/index.test.tsx.snap} | 36 +- .../index.test.tsx} | 16 +- .../OnboardingGeneralSettings/index.tsx | 73 ++ .../__snapshots__/index.test.tsx.snap | 120 +++ .../OnboardingSecuritySettings/index.test.tsx | 42 + .../OnboardingSecuritySettings/index.tsx | 19 + .../SecuritySettings.constants.ts | 5 - .../SecuritySettings.styles.ts | 5 - .../SecuritySettings.test.tsx | 4 - .../SecuritySettings/SecuritySettings.tsx | 6 +- .../SecuritySettings.types.ts | 14 - app/components/hooks/useOnboardingHeader.tsx | 44 + app/constants/navigation/Routes.ts | 3 + .../Settings/AdvancedView.selectors.js | 1 + locales/languages/en.json | 11 +- 25 files changed, 1705 insertions(+), 201 deletions(-) create mode 100644 app/components/Views/OnboardingSuccess/DefaultSettings/__snapshots__/index.test.tsx.snap create mode 100644 app/components/Views/OnboardingSuccess/DefaultSettings/index.test.tsx create mode 100644 app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/__snapshots__/index.test.tsx.snap create mode 100644 app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/index.styles.ts create mode 100644 app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/index.test.tsx create mode 100644 app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/index.tsx rename app/components/Views/OnboardingSuccess/{DefaultSettings/__snapshots__/index.test.js.snap => OnboardingGeneralSettings/__snapshots__/index.test.tsx.snap} (87%) rename app/components/Views/OnboardingSuccess/{DefaultSettings/index.test.js => OnboardingGeneralSettings/index.test.tsx} (73%) create mode 100644 app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/index.tsx create mode 100644 app/components/Views/OnboardingSuccess/OnboardingSecuritySettings/__snapshots__/index.test.tsx.snap create mode 100644 app/components/Views/OnboardingSuccess/OnboardingSecuritySettings/index.test.tsx create mode 100644 app/components/Views/OnboardingSuccess/OnboardingSecuritySettings/index.tsx create mode 100644 app/components/hooks/useOnboardingHeader.tsx diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js index fd14e13f8579..a2a8b16d4850 100644 --- a/app/components/Nav/App/index.js +++ b/app/components/Nav/App/index.js @@ -111,6 +111,9 @@ import generateUserSettingsAnalyticsMetaData from '../../../util/metrics/UserSet import LedgerSelectAccount from '../../Views/LedgerSelectAccount'; import OnboardingSuccess from '../../Views/OnboardingSuccess'; import DefaultSettings from '../../Views/OnboardingSuccess/DefaultSettings'; +import OnboardingGeneralSettings from '../../Views/OnboardingSuccess/OnboardingGeneralSettings'; +import OnboardingAssetsSettings from '../../Views/OnboardingSuccess/OnboardingAssetsSettings'; +import OnboardingSecuritySettings from '../../Views/OnboardingSuccess/OnboardingSecuritySettings'; import BasicFunctionalityModal from '../../UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal'; import SmartTransactionsOptInModal from '../../Views/SmartTransactionsOptInModal/SmartTranactionsOptInModal'; import ProfileSyncingModal from '../../UI/ProfileSyncing/ProfileSyncingModal/ProfileSyncingModal'; @@ -178,6 +181,21 @@ const OnboardingSuccessFlow = () => ( component={DefaultSettings} options={DefaultSettings.navigationOptions} /> + <Stack.Screen + name={Routes.ONBOARDING.GENERAL_SETTINGS} + component={OnboardingGeneralSettings} + options={DefaultSettings.navigationOptions} + /> + <Stack.Screen + name={Routes.ONBOARDING.ASSETS_SETTINGS} + component={OnboardingAssetsSettings} + options={DefaultSettings.navigationOptions} + /> + <Stack.Screen + name={Routes.ONBOARDING.SECURITY_SETTINGS} + component={OnboardingSecuritySettings} + options={DefaultSettings.navigationOptions} + /> </Stack.Navigator> ); /** diff --git a/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap b/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap index 76aca858c91b..d86999527a36 100644 --- a/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap +++ b/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap @@ -30,7 +30,7 @@ exports[`ManageNetworks should render correctly 1`] = ` } } > - Manage networks + Choose your network </Text> </View> <Text diff --git a/app/components/Views/OnboardingSuccess/DefaultSettings/__snapshots__/index.test.tsx.snap b/app/components/Views/OnboardingSuccess/DefaultSettings/__snapshots__/index.test.tsx.snap new file mode 100644 index 000000000000..889294a0f8ee --- /dev/null +++ b/app/components/Views/OnboardingSuccess/DefaultSettings/__snapshots__/index.test.tsx.snap @@ -0,0 +1,330 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DefaultSettings should render correctly 1`] = ` +<RCTScrollView + style={ + { + "flex": 1, + "paddingHorizontal": 16, + "paddingTop": 16, + } + } +> + <View> + <Text + accessibilityRole="text" + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + "letterSpacing": 0, + "lineHeight": 22, + } + } + > + MetaMask uses default settings to best balance safety and ease of use. Change these settings to further increase your privacy. + <Text + accessibilityRole="text" + onPress={[Function]} + style={ + { + "color": "#0376c9", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + "letterSpacing": 0, + "lineHeight": 22, + } + } + > + + Learn more about privacy best practices. + </Text> + </Text> + <TouchableOpacity + onPress={[Function]} + > + <View + accessibilityRole="none" + accessible={true} + style={ + { + "backgroundColor": "#ffffff", + "padding": 16, + } + } + > + <View + style={ + { + "alignItems": "center", + "flexDirection": "row", + } + } + > + <View + style={ + { + "flex": 1, + } + } + testID="listitemcolumn" + > + <Text + accessibilityRole="text" + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Medium", + "fontSize": 16, + "fontWeight": "500", + "letterSpacing": 0, + "lineHeight": 24, + } + } + > + General + </Text> + <Text + accessibilityRole="text" + style={ + { + "color": "#6a737d", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + "letterSpacing": 0, + "lineHeight": 22, + } + } + > + Sync settings across devices, select network preferences, and track token data + </Text> + </View> + <View + accessible={false} + style={ + { + "width": 16, + } + } + testID="listitem-gap" + /> + <View + style={ + { + "flex": -1, + } + } + testID="listitemcolumn" + > + <SvgMock + color="#141618" + height={20} + name="ArrowRight" + style={ + { + "height": 20, + "paddingLeft": 16, + "width": 20, + } + } + width={20} + /> + </View> + </View> + </View> + </TouchableOpacity> + <TouchableOpacity + onPress={[Function]} + > + <View + accessibilityRole="none" + accessible={true} + style={ + { + "backgroundColor": "#ffffff", + "padding": 16, + } + } + > + <View + style={ + { + "alignItems": "center", + "flexDirection": "row", + } + } + > + <View + style={ + { + "flex": 1, + } + } + testID="listitemcolumn" + > + <Text + accessibilityRole="text" + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Medium", + "fontSize": 16, + "fontWeight": "500", + "letterSpacing": 0, + "lineHeight": 24, + } + } + > + Assets + </Text> + <Text + accessibilityRole="text" + style={ + { + "color": "#6a737d", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + "letterSpacing": 0, + "lineHeight": 22, + } + } + > + Autodetect tokens in your wallet, display NFTs, and get batched account balance updates + </Text> + </View> + <View + accessible={false} + style={ + { + "width": 16, + } + } + testID="listitem-gap" + /> + <View + style={ + { + "flex": -1, + } + } + testID="listitemcolumn" + > + <SvgMock + color="#141618" + height={20} + name="ArrowRight" + style={ + { + "height": 20, + "paddingLeft": 16, + "width": 20, + } + } + width={20} + /> + </View> + </View> + </View> + </TouchableOpacity> + <TouchableOpacity + onPress={[Function]} + > + <View + accessibilityRole="none" + accessible={true} + style={ + { + "backgroundColor": "#ffffff", + "padding": 16, + } + } + > + <View + style={ + { + "alignItems": "center", + "flexDirection": "row", + } + } + > + <View + style={ + { + "flex": 1, + } + } + testID="listitemcolumn" + > + <Text + accessibilityRole="text" + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Medium", + "fontSize": 16, + "fontWeight": "500", + "letterSpacing": 0, + "lineHeight": 24, + } + } + > + Security + </Text> + <Text + accessibilityRole="text" + style={ + { + "color": "#6a737d", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + "letterSpacing": 0, + "lineHeight": 22, + } + } + > + Reduce your chances of joining unsafe networks and protect your accounts + </Text> + </View> + <View + accessible={false} + style={ + { + "width": 16, + } + } + testID="listitem-gap" + /> + <View + style={ + { + "flex": -1, + } + } + testID="listitemcolumn" + > + <SvgMock + color="#141618" + height={20} + name="ArrowRight" + style={ + { + "height": 20, + "paddingLeft": 16, + "width": 20, + } + } + width={20} + /> + </View> + </View> + </View> + </TouchableOpacity> + </View> +</RCTScrollView> +`; diff --git a/app/components/Views/OnboardingSuccess/DefaultSettings/index.styles.ts b/app/components/Views/OnboardingSuccess/DefaultSettings/index.styles.ts index f3e8d28dc93d..c8eb08462674 100644 --- a/app/components/Views/OnboardingSuccess/DefaultSettings/index.styles.ts +++ b/app/components/Views/OnboardingSuccess/DefaultSettings/index.styles.ts @@ -1,37 +1,12 @@ import { StyleSheet } from 'react-native'; -const styles = StyleSheet.create({ - root: { - flex: 1, - paddingHorizontal: 16, - paddingTop: 16, - }, - description: { - fontSize: 14, - textAlign: 'left', - marginTop: 10, - lineHeight: 22, - fontWeight: '400', - }, - setting: { - marginTop: 32, - }, - toggle: { - flexDirection: 'row', - marginLeft: 16, - }, - heading: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, - networkPicker: { - marginVertical: 16, - alignSelf: 'flex-start', - }, - backButton: { - padding: 10, - }, -}); +const styleSheet = () => + StyleSheet.create({ + root: { + flex: 1, + paddingHorizontal: 16, + paddingTop: 16, + }, + }); -export default styles; +export default styleSheet; diff --git a/app/components/Views/OnboardingSuccess/DefaultSettings/index.test.tsx b/app/components/Views/OnboardingSuccess/DefaultSettings/index.test.tsx new file mode 100644 index 000000000000..8c14e187a3d4 --- /dev/null +++ b/app/components/Views/OnboardingSuccess/DefaultSettings/index.test.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react-native'; +import { useNavigation } from '@react-navigation/native'; +import { strings } from '../../../../../locales/i18n'; +import Routes from '../../../../constants/navigation/Routes'; +import { Linking } from 'react-native'; +import AppConstants from '../../../../core/AppConstants'; +import DefaultSettings from './'; + +jest.mock('@react-navigation/native', () => ({ + useNavigation: jest.fn(), +})); + +jest.mock('react-native/Libraries/Linking/Linking', () => ({ + openURL: jest.fn(), +})); + +describe('DefaultSettings', () => { + const mockNavigation = { + goBack: jest.fn(), + setOptions: jest.fn(), + navigate: jest.fn(), + }; + beforeEach(() => { + (useNavigation as jest.Mock).mockReturnValue(mockNavigation); + }); + + it('should render correctly', () => { + const tree = render(<DefaultSettings />).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('opens privacy best practices link when "Learn more" is pressed', () => { + const { getByText } = render(<DefaultSettings />); + const learnMoreText = getByText( + strings('default_settings.learn_more_about_privacy'), + ); + fireEvent.press(learnMoreText); + expect(Linking.openURL).toHaveBeenCalledWith( + AppConstants.URLS.PRIVACY_BEST_PRACTICES, + ); + }); + + it('navigates to General Settings when pressed', () => { + const { getByText } = render(<DefaultSettings />); + const generalSettingsDrawer = getByText( + strings('default_settings.drawer_general_title'), + ); + fireEvent.press(generalSettingsDrawer); + expect(mockNavigation.navigate).toHaveBeenCalledWith( + Routes.ONBOARDING.GENERAL_SETTINGS, + ); + }); + + it('navigates to Assets Settings when pressed', () => { + const { getByText } = render(<DefaultSettings />); + const assetsSettingsDrawer = getByText( + strings('default_settings.drawer_assets_title'), + ); + fireEvent.press(assetsSettingsDrawer); + expect(mockNavigation.navigate).toHaveBeenCalledWith( + Routes.ONBOARDING.ASSETS_SETTINGS, + ); + }); + + it('navigates to Security Settings when pressed', () => { + const { getByText } = render(<DefaultSettings />); + const securitySettingsDrawer = getByText( + strings('default_settings.drawer_security_title'), + ); + fireEvent.press(securitySettingsDrawer); + expect(mockNavigation.navigate).toHaveBeenCalledWith( + Routes.ONBOARDING.SECURITY_SETTINGS, + ); + }); +}); diff --git a/app/components/Views/OnboardingSuccess/DefaultSettings/index.tsx b/app/components/Views/OnboardingSuccess/DefaultSettings/index.tsx index caebda622d0b..04398e444e9c 100644 --- a/app/components/Views/OnboardingSuccess/DefaultSettings/index.tsx +++ b/app/components/Views/OnboardingSuccess/DefaultSettings/index.tsx @@ -1,95 +1,27 @@ -import React, { useCallback, useLayoutEffect } from 'react'; -import { ScrollView, TouchableOpacity, Linking } from 'react-native'; +import React from 'react'; +import { ScrollView, Linking } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { useOnboardingHeader } from '../../../hooks/useOnboardingHeader'; +import { useStyles } from '../../../../component-library/hooks'; import Text, { TextVariant, TextColor, } from '../../../../component-library/components/Texts/Text'; -import Icon, { - IconSize, - IconName, -} from '../../../../component-library/components/Icons/Icon'; -import { useNavigation } from '@react-navigation/native'; import Routes from '../../../../constants/navigation/Routes'; import { strings } from '../../../../../locales/i18n'; -import BasicFunctionalityComponent from '../../../UI/BasicFunctionality/BasicFunctionality'; -import ManageNetworksComponent from '../../../UI/ManageNetworks/ManageNetworks'; import AppConstants from '../../../../core/AppConstants'; -import styles from './index.styles'; -import ProfileSyncingComponent from '../../../../components/UI/ProfileSyncing/ProfileSyncing'; -import { useSelector } from 'react-redux'; -import { selectIsProfileSyncingEnabled } from '../../../../selectors/notifications'; -import { isNotificationsFeatureEnabled } from '../../../../util/notifications'; -import { enableProfileSyncing } from '../../../../actions/notification/helpers'; -import { RootState } from '../../../../reducers'; -import { MetaMetricsEvents, useMetrics } from '../../../hooks/useMetrics'; +import SettingsDrawer from '../../../UI/SettingsDrawer'; +import styleSheet from './index.styles'; const DefaultSettings = () => { + useOnboardingHeader(strings('default_settings.default_settings')); + const { styles } = useStyles(styleSheet, {}); const navigation = useNavigation(); - const { trackEvent } = useMetrics(); - const isBasicFunctionalityEnabled = useSelector( - (state: RootState) => state?.settings?.basicFunctionalityEnabled, - ); - const isProfileSyncingEnabled = useSelector(selectIsProfileSyncingEnabled); - const renderBackButton = useCallback( - () => ( - <TouchableOpacity - onPress={() => navigation.goBack()} - style={styles.backButton} - > - <Icon name={IconName.ArrowLeft} size={IconSize.Lg} /> - </TouchableOpacity> - ), - [navigation], - ); - const renderTitle = useCallback( - () => ( - <Text variant={TextVariant.HeadingMD}> - {strings('onboarding_success.default_settings')} - </Text> - ), - [], - ); - - useLayoutEffect(() => { - navigation.setOptions({ - headerLeft: renderBackButton, - headerTitle: renderTitle, - }); - }, [navigation, renderBackButton, renderTitle]); - - const handleSwitchToggle = () => { - navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, { - screen: Routes.SHEET.BASIC_FUNCTIONALITY, - }); - trackEvent(MetaMetricsEvents.SETTINGS_UPDATED, { - settings_group: 'onboarding_advanced_configuration', - settings_type: 'basic_functionality', - old_value: isBasicFunctionalityEnabled, - new_value: !isBasicFunctionalityEnabled, - was_profile_syncing_on: isProfileSyncingEnabled, - }); - }; const handleLink = () => { Linking.openURL(AppConstants.URLS.PRIVACY_BEST_PRACTICES); }; - const toggleProfileSyncing = async () => { - if (isProfileSyncingEnabled) { - navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, { - screen: Routes.SHEET.PROFILE_SYNCING, - }); - } else { - await enableProfileSyncing(); - } - trackEvent(MetaMetricsEvents.SETTINGS_UPDATED, { - settings_group: 'onboarding_advanced_configuration', - settings_type: 'profile_syncing', - old_value: isProfileSyncingEnabled, - new_value: !isProfileSyncingEnabled, - }); - }; - return ( <ScrollView style={styles.root}> <Text variant={TextVariant.BodyMD}> @@ -99,15 +31,21 @@ const DefaultSettings = () => { {strings('default_settings.learn_more_about_privacy')} </Text> </Text> - <BasicFunctionalityComponent handleSwitchToggle={handleSwitchToggle} /> - {isNotificationsFeatureEnabled() && ( - <ProfileSyncingComponent - handleSwitchToggle={toggleProfileSyncing} - isBasicFunctionalityEnabled={isBasicFunctionalityEnabled} - isProfileSyncingEnabled={isProfileSyncingEnabled} - /> - )} - <ManageNetworksComponent /> + <SettingsDrawer + title={strings('default_settings.drawer_general_title')} + description={strings('default_settings.drawer_general_title_desc')} + onPress={() => navigation.navigate(Routes.ONBOARDING.GENERAL_SETTINGS)} + /> + <SettingsDrawer + title={strings('default_settings.drawer_assets_title')} + description={strings('default_settings.drawer_assets_desc')} + onPress={() => navigation.navigate(Routes.ONBOARDING.ASSETS_SETTINGS)} + /> + <SettingsDrawer + title={strings('default_settings.drawer_security_title')} + description={strings('default_settings.drawer_security_desc')} + onPress={() => navigation.navigate(Routes.ONBOARDING.SECURITY_SETTINGS)} + /> </ScrollView> ); }; diff --git a/app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/__snapshots__/index.test.tsx.snap b/app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/__snapshots__/index.test.tsx.snap new file mode 100644 index 000000000000..cb7709a24ac0 --- /dev/null +++ b/app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/__snapshots__/index.test.tsx.snap @@ -0,0 +1,824 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OnboardingAssetSettings should render correctly 1`] = ` +<RCTScrollView + contentContainerStyle={ + { + "paddingBottom": 75, + } + } + style={ + { + "flex": 1, + "paddingBottom": 16, + "paddingHorizontal": 16, + "paddingVertical": 8, + } + } +> + <View> + <View + style={ + { + "marginTop": 32, + } + } + > + <View + style={ + { + "alignItems": "center", + "flexDirection": "row", + } + } + > + <Text + accessibilityRole="text" + style={ + { + "color": "#141618", + "flex": 1, + "fontFamily": "EuclidCircularB-Medium", + "fontSize": 16, + "fontWeight": "500", + "letterSpacing": 0, + "lineHeight": 24, + } + } + > + Autodetect tokens + </Text> + <View + style={ + { + "alignItems": "center", + "flexDirection": "row", + "marginLeft": 16, + } + } + > + <RCTSwitch + accessibilityRole="switch" + onChange={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + onTintColor="#0376c9" + style={ + [ + { + "height": 31, + "width": 51, + }, + [ + { + "alignSelf": "flex-start", + }, + { + "backgroundColor": "#bbc0c566", + "borderRadius": 16, + }, + ], + ] + } + testID="token-detection-toggle" + thumbTintColor="#ffffff" + tintColor="#bbc0c566" + value={true} + /> + </View> + </View> + <Text + accessibilityRole="text" + style={ + { + "color": "#6a737d", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + "letterSpacing": 0, + "lineHeight": 22, + "marginTop": 8, + } + } + > + We use third-party APIs to detect and display new tokens sent to your wallet. Turn off if you don’t want the app to pull data from those services. + </Text> + </View> + <View + style={ + { + "marginTop": 16, + } + } + > + <View + style={ + { + "alignItems": "center", + "flexDirection": "row", + } + } + > + <Text + accessibilityRole="text" + style={ + { + "color": "#141618", + "flex": 1, + "fontFamily": "EuclidCircularB-Medium", + "fontSize": 16, + "fontWeight": "500", + "letterSpacing": 0, + "lineHeight": 24, + } + } + > + Display NFT Media + </Text> + <View + style={ + { + "marginLeft": 16, + } + } + > + <RCTSwitch + accessibilityRole="switch" + onChange={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + onTintColor="#0376c9" + style={ + [ + { + "height": 31, + "width": 51, + }, + [ + { + "alignSelf": "flex-start", + }, + { + "backgroundColor": "#bbc0c566", + "borderRadius": 16, + }, + ], + ] + } + testID="nft-display-media-mode-section" + thumbTintColor="#ffffff" + tintColor="#bbc0c566" + value={false} + /> + </View> + </View> + <Text + accessibilityRole="text" + style={ + { + "color": "#6a737d", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + "letterSpacing": 0, + "lineHeight": 22, + "marginTop": 8, + } + } + > + Displaying NFT media and data exposes your IP address to OpenSea or other third parties. NFT autodetection relies on this feature, and won't be available when turned off. If NFT media is fully located on IPFS, it can still be displayed even when this feature is turned off. + </Text> + </View> + <View + style={ + { + "marginTop": 32, + } + } + > + <View + style={ + { + "alignItems": "center", + "flexDirection": "row", + } + } + > + <Text + accessibilityRole="text" + style={ + { + "color": "#141618", + "flex": 1, + "fontFamily": "EuclidCircularB-Medium", + "fontSize": 16, + "fontWeight": "500", + "letterSpacing": 0, + "lineHeight": 24, + } + } + > + Autodetect NFTs + </Text> + <View + style={ + { + "marginLeft": 16, + } + } + > + <RCTSwitch + accessibilityRole="switch" + onChange={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + onTintColor="#0376c9" + style={ + [ + { + "height": 31, + "width": 51, + }, + [ + { + "alignSelf": "flex-start", + }, + { + "backgroundColor": "#bbc0c566", + "borderRadius": 16, + }, + ], + ] + } + testID="nft-opensea-autodetect-mode-section" + thumbTintColor="#ffffff" + tintColor="#bbc0c566" + value={false} + /> + </View> + </View> + <Text + accessibilityRole="text" + style={ + { + "color": "#6a737d", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + "letterSpacing": 0, + "lineHeight": 22, + "marginTop": 8, + } + } + > + Let MetaMask add NFTs you own using third-party services (like OpenSea). Autodetecting NFTs exposes your IP and account address to these services. Enabling this feature could associate your IP address with your Ethereum address and display fake NFTs airdropped by scammers. You can add tokens manually to avoid this risk. + </Text> + </View> + <View + style={ + { + "marginTop": 32, + } + } + > + <View + style={ + { + "alignItems": "center", + "flexDirection": "row", + } + } + > + <Text + accessibilityRole="text" + style={ + { + "color": "#141618", + "flex": 1, + "fontFamily": "EuclidCircularB-Medium", + "fontSize": 16, + "fontWeight": "500", + "letterSpacing": 0, + "lineHeight": 24, + } + } + > + IPFS Gateway + </Text> + <View + style={ + { + "marginLeft": 16, + } + } + > + <RCTSwitch + accessibilityRole="switch" + onChange={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + onTintColor="#0376c9" + style={ + [ + { + "height": 31, + "width": 51, + }, + [ + { + "alignSelf": "flex-start", + }, + { + "backgroundColor": "#bbc0c566", + "borderRadius": 16, + }, + ], + ] + } + testID="IPFS_GATEWAY_SECTION" + thumbTintColor="#ffffff" + tintColor="#bbc0c566" + value={true} + /> + </View> + </View> + <Text + accessibilityRole="text" + style={ + { + "color": "#6a737d", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + "letterSpacing": 0, + "lineHeight": 22, + "marginTop": 8, + } + } + > + MetaMask uses third-party services to show images of your NFTs stored on IPFS, display information related to ENS addresses entered in your browser's address bar, and fetch icons for different tokens. Your IP address may be exposed to these services when you’re using them. + </Text> + <View + style={ + { + "marginTop": 16, + } + } + > + <Text + accessibilityRole="text" + style={ + { + "color": "#6a737d", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + "letterSpacing": 0, + "lineHeight": 22, + "marginTop": 8, + } + } + > + Choose your preferred IPFS gateway. + </Text> + <View + style={ + { + "borderColor": "#bbc0c5", + "borderRadius": 5, + "borderWidth": 2, + "marginTop": 16, + } + } + > + <View> + <ActivityIndicator + size="small" + /> + </View> + </View> + </View> + </View> + <View + style={ + { + "marginTop": 32, + } + } + testID="incoming-transactions-section" + > + <Text + accessibilityRole="text" + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Medium", + "fontSize": 16, + "fontWeight": "500", + "letterSpacing": 0, + "lineHeight": 24, + } + } + > + Show incoming transactions + </Text> + <Text + accessibilityRole="text" + style={ + { + "color": "#6a737d", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + "letterSpacing": 0, + "lineHeight": 22, + "marginTop": 8, + } + } + > + This relies on the network you select which will expose your Ethereum address and your IP address. + </Text> + <View + style={ + { + "marginLeft": -16, + "marginRight": -16, + "marginTop": 24, + } + } + > + <TouchableOpacity + avatarProps={ + { + "imageSource": { + "uri": "MockImage", + }, + "name": "Ethereum Main Network", + "variant": "Network", + } + } + secondaryText="etherscan.io" + style={ + { + "backgroundColor": "#ffffff", + "borderColor": "#bbc0c5", + "borderRadius": 4, + "borderWidth": 0, + "padding": 16, + } + } + testID="celldisplay" + title="Ethereum Main Network" + > + <View + style={ + { + "flexDirection": "row", + } + } + > + <View + style={ + { + "alignItems": "center", + "backgroundColor": "#ffffff", + "borderRadius": 16, + "height": 32, + "justifyContent": "center", + "marginRight": 16, + "overflow": "hidden", + "width": 32, + } + } + testID="cellbase-avatar" + > + <Image + onError={[Function]} + resizeMode="contain" + source={ + { + "uri": "MockImage", + } + } + style={ + { + "height": 32, + "width": 32, + } + } + testID="network-avatar-image" + /> + </View> + <View + style={ + { + "alignItems": "flex-start", + "flex": 1, + } + } + > + <Text + accessibilityRole="text" + numberOfLines={1} + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 16, + "fontWeight": "400", + "letterSpacing": 0, + "lineHeight": 24, + } + } + testID="cellbase-avatar-title" + > + Ethereum Main Network + </Text> + <Text + accessibilityRole="text" + numberOfLines={1} + style={ + { + "color": "#6a737d", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + "letterSpacing": 0, + "lineHeight": 22, + } + } + > + etherscan.io + </Text> + </View> + <View + style={ + { + "marginLeft": 16, + } + } + > + <RCTSwitch + accessibilityRole="switch" + onChange={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + onTintColor="#0376c9" + style={ + [ + { + "height": 31, + "width": 51, + }, + [ + { + "alignSelf": "flex-start", + }, + { + "backgroundColor": "#bbc0c566", + "borderRadius": 16, + }, + ], + ] + } + testID="incoming-mainnet-toggle" + thumbTintColor="#ffffff" + tintColor="#bbc0c566" + value={true} + /> + </View> + </View> + </TouchableOpacity> + <TouchableOpacity + avatarProps={ + { + "imageSource": { + "uri": "MockImage", + }, + "name": "Linea Main Network", + "variant": "Network", + } + } + secondaryText="lineascan.build" + style={ + { + "backgroundColor": "#ffffff", + "borderColor": "#bbc0c5", + "borderRadius": 4, + "borderWidth": 0, + "padding": 16, + } + } + testID="celldisplay" + title="Linea Main Network" + > + <View + style={ + { + "flexDirection": "row", + } + } + > + <View + style={ + { + "alignItems": "center", + "backgroundColor": "#ffffff", + "borderRadius": 16, + "height": 32, + "justifyContent": "center", + "marginRight": 16, + "overflow": "hidden", + "width": 32, + } + } + testID="cellbase-avatar" + > + <Image + onError={[Function]} + resizeMode="contain" + source={ + { + "uri": "MockImage", + } + } + style={ + { + "height": 32, + "width": 32, + } + } + testID="network-avatar-image" + /> + </View> + <View + style={ + { + "alignItems": "flex-start", + "flex": 1, + } + } + > + <Text + accessibilityRole="text" + numberOfLines={1} + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 16, + "fontWeight": "400", + "letterSpacing": 0, + "lineHeight": 24, + } + } + testID="cellbase-avatar-title" + > + Linea Main Network + </Text> + <Text + accessibilityRole="text" + numberOfLines={1} + style={ + { + "color": "#6a737d", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + "letterSpacing": 0, + "lineHeight": 22, + } + } + > + lineascan.build + </Text> + </View> + <View + style={ + { + "marginLeft": 16, + } + } + > + <RCTSwitch + accessibilityRole="switch" + onChange={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + onTintColor="#0376c9" + style={ + [ + { + "height": 31, + "width": 51, + }, + [ + { + "alignSelf": "flex-start", + }, + { + "backgroundColor": "#bbc0c566", + "borderRadius": 16, + }, + ], + ] + } + testID="incoming-linea-mainnet-toggle" + thumbTintColor="#ffffff" + tintColor="#bbc0c566" + value={true} + /> + </View> + </View> + </TouchableOpacity> + </View> + </View> + <View + style={ + { + "marginTop": 16, + } + } + testID="batch-balance-requests-section" + > + <View + style={ + { + "alignItems": "center", + "flexDirection": "row", + } + } + > + <Text + accessibilityRole="text" + style={ + { + "color": "#141618", + "flex": 1, + "fontFamily": "EuclidCircularB-Medium", + "fontSize": 16, + "fontWeight": "500", + "letterSpacing": 0, + "lineHeight": 24, + } + } + > + Batch account balance requests + </Text> + <View + style={ + { + "marginLeft": 16, + } + } + > + <RCTSwitch + accessibilityRole="switch" + onChange={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + onTintColor="#0376c9" + style={ + [ + { + "height": 31, + "width": 51, + }, + [ + { + "alignSelf": "flex-start", + }, + { + "backgroundColor": "#bbc0c566", + "borderRadius": 16, + }, + ], + ] + } + testID="security-settings-multi-account-balances-switch" + thumbTintColor="#ffffff" + tintColor="#bbc0c566" + value={true} + /> + </View> + </View> + <Text + accessibilityRole="text" + style={ + { + "color": "#6a737d", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + "letterSpacing": 0, + "lineHeight": 22, + "marginTop": 8, + } + } + > + We batch accounts and query Infura to responsively show your balances. If you turn this off, only active accounts will be queried. Some dApps won’t work unless you connect your wallet. + </Text> + </View> + </View> +</RCTScrollView> +`; diff --git a/app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/index.styles.ts b/app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/index.styles.ts new file mode 100644 index 000000000000..59542844a682 --- /dev/null +++ b/app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/index.styles.ts @@ -0,0 +1,16 @@ +import { StyleSheet } from 'react-native'; + +const styleSheet = () => + StyleSheet.create({ + root: { + flex: 1, + paddingHorizontal: 16, + paddingVertical: 8, + paddingBottom: 16, + }, + contentContainerStyle: { + paddingBottom: 75, + }, + }); + +export default styleSheet; diff --git a/app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/index.test.tsx b/app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/index.test.tsx new file mode 100644 index 000000000000..4c1a524457f7 --- /dev/null +++ b/app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/index.test.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { useNavigation } from '@react-navigation/native'; +import renderWithProvider from '../../../../util/test/renderWithProvider'; +import { backgroundState } from '../../../../util/test/initial-root-state'; +import OnboardingAssetSettings from '.'; + +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: jest.fn(), +})); + +describe('OnboardingAssetSettings', () => { + const mockNavigation = { + goBack: jest.fn(), + setOptions: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + (useNavigation as jest.Mock).mockReturnValue(mockNavigation); + }); + + const initialState = { + engine: { + backgroundState: { + ...backgroundState, + PreferencesController: { + ...backgroundState.PreferencesController, + useTokenDetection: true, + displayNftMedia: false, + useNftDetection: false, + }, + }, + }, + network: { + provider: { + chainId: '1', + }, + }, + }; + + it('should render correctly', () => { + const tree = renderWithProvider(<OnboardingAssetSettings />, { + state: initialState, + }); + expect(tree).toMatchSnapshot(); + }); + + it('sets navigation options', () => { + renderWithProvider(<OnboardingAssetSettings />, { + state: initialState, + }); + expect(mockNavigation.setOptions).toHaveBeenCalled(); + }); +}); diff --git a/app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/index.tsx b/app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/index.tsx new file mode 100644 index 000000000000..7ff3784c33c1 --- /dev/null +++ b/app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/index.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { ScrollView } from 'react-native'; +import { useOnboardingHeader } from '../../../hooks/useOnboardingHeader'; +import { strings } from '../../../../../locales/i18n'; +import { useStyles } from '../../../../component-library/hooks'; +import AutoDetectTokensSettings from '../../Settings/AutoDetectTokensSettings'; +import DisplayNFTMediaSettings from '../../Settings/DisplayNFTMediaSettings'; +import AutoDetectNFTSettings from '../../Settings/AutoDetectNFTSettings'; +import IPFSGatewaySettings from '../../Settings/IPFSGatewaySettings'; +import IncomingTransactionsSettings from '../../Settings/IncomingTransactionsSettings'; +import BatchAccountBalanceSettings from '../../Settings/BatchAccountBalanceSettings'; +import styleSheet from './index.styles'; + +const AssetSettings = () => { + useOnboardingHeader(strings('default_settings.drawer_assets_title')); + const { styles } = useStyles(styleSheet, {}); + + return ( + <ScrollView + contentContainerStyle={styles.contentContainerStyle} + style={styles.root} + > + <AutoDetectTokensSettings /> + <DisplayNFTMediaSettings /> + <AutoDetectNFTSettings /> + <IPFSGatewaySettings /> + <IncomingTransactionsSettings /> + <BatchAccountBalanceSettings /> + </ScrollView> + ); +}; + +export default AssetSettings; diff --git a/app/components/Views/OnboardingSuccess/DefaultSettings/__snapshots__/index.test.js.snap b/app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/__snapshots__/index.test.tsx.snap similarity index 87% rename from app/components/Views/OnboardingSuccess/DefaultSettings/__snapshots__/index.test.js.snap rename to app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/__snapshots__/index.test.tsx.snap index 485c0749e0b2..eba5f4280cd3 100644 --- a/app/components/Views/OnboardingSuccess/DefaultSettings/__snapshots__/index.test.js.snap +++ b/app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/__snapshots__/index.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`DefaultSettings should render correctly 1`] = ` +exports[`OnboardingGeneralSettings should render correctly 1`] = ` <RCTScrollView style={ { @@ -11,38 +11,6 @@ exports[`DefaultSettings should render correctly 1`] = ` } > <View> - <Text - accessibilityRole="text" - style={ - { - "color": "#141618", - "fontFamily": "EuclidCircularB-Regular", - "fontSize": 14, - "fontWeight": "400", - "letterSpacing": 0, - "lineHeight": 22, - } - } - > - MetaMask uses default settings to best balance safety and ease of use. Change these settings to further increase your privacy. - <Text - accessibilityRole="text" - onPress={[Function]} - style={ - { - "color": "#0376c9", - "fontFamily": "EuclidCircularB-Regular", - "fontSize": 14, - "fontWeight": "400", - "letterSpacing": 0, - "lineHeight": 22, - } - } - > - - Learn more about privacy best practices. - </Text> - </Text> <View style={ { @@ -160,7 +128,7 @@ exports[`DefaultSettings should render correctly 1`] = ` } } > - Manage networks + Choose your network </Text> </View> <Text diff --git a/app/components/Views/OnboardingSuccess/DefaultSettings/index.test.js b/app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/index.test.tsx similarity index 73% rename from app/components/Views/OnboardingSuccess/DefaultSettings/index.test.js rename to app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/index.test.tsx index 1b393b23783d..354be5cb5267 100644 --- a/app/components/Views/OnboardingSuccess/DefaultSettings/index.test.js +++ b/app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/index.test.tsx @@ -1,12 +1,8 @@ -// Third party dependencies. import React from 'react'; - -// Internal dependencies. -import DefaultSettings from '.'; -import renderWithProvider from '../../../../util/test/renderWithProvider'; -import { useNavigation } from '@react-navigation/native'; import { useSelector } from 'react-redux'; +import renderWithProvider from '../../../../util/test/renderWithProvider'; import { selectNetworkName } from '../../../../selectors/networkInfos'; +import OnboardingGeneralSettings from '.'; jest.mock('@react-navigation/native', () => { const actualReactNavigation = jest.requireActual('@react-navigation/native'); @@ -31,14 +27,12 @@ jest.mock('react-redux', () => ({ const mockNetworkName = 'Ethereum Main Network'; -describe('DefaultSettings', () => { +describe('OnboardingGeneralSettings', () => { it('should render correctly', () => { - useSelector.mockImplementation((selector) => { + (useSelector as jest.Mock).mockImplementation((selector) => { if (selector === selectNetworkName) return mockNetworkName; }); - const { toJSON } = renderWithProvider( - <DefaultSettings navigation={useNavigation()} />, - ); + const { toJSON } = renderWithProvider(<OnboardingGeneralSettings />); expect(toJSON()).toMatchSnapshot(); }); }); diff --git a/app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/index.tsx b/app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/index.tsx new file mode 100644 index 000000000000..6dc4007b32a8 --- /dev/null +++ b/app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/index.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { ScrollView } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { useSelector } from 'react-redux'; +import { useOnboardingHeader } from '../../../hooks/useOnboardingHeader'; +import Routes from '../../../../constants/navigation/Routes'; +import { strings } from '../../../../../locales/i18n'; +import BasicFunctionalityComponent from '../../../UI/BasicFunctionality/BasicFunctionality'; +import ManageNetworksComponent from '../../../UI/ManageNetworks/ManageNetworks'; +import { useStyles } from '../../../../component-library/hooks'; +import ProfileSyncingComponent from '../../../UI/ProfileSyncing/ProfileSyncing'; +import { selectIsProfileSyncingEnabled } from '../../../../selectors/notifications'; +import { isNotificationsFeatureEnabled } from '../../../../util/notifications'; +import { enableProfileSyncing } from '../../../../actions/notification/helpers'; +import { RootState } from '../../../../reducers'; +import { MetaMetricsEvents, useMetrics } from '../../../hooks/useMetrics'; +import styleSheet from '../DefaultSettings/index.styles'; + +const GeneralSettings = () => { + useOnboardingHeader(strings('default_settings.drawer_general_title')); + const { styles } = useStyles(styleSheet, {}); + const navigation = useNavigation(); + const { trackEvent } = useMetrics(); + const isBasicFunctionalityEnabled = useSelector( + (state: RootState) => state?.settings?.basicFunctionalityEnabled, + ); + const isProfileSyncingEnabled = useSelector(selectIsProfileSyncingEnabled); + + const handleSwitchToggle = () => { + navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, { + screen: Routes.SHEET.BASIC_FUNCTIONALITY, + }); + trackEvent(MetaMetricsEvents.SETTINGS_UPDATED, { + settings_group: 'onboarding_advanced_configuration', + settings_type: 'basic_functionality', + old_value: isBasicFunctionalityEnabled, + new_value: !isBasicFunctionalityEnabled, + was_profile_syncing_on: isProfileSyncingEnabled, + }); + }; + + const toggleProfileSyncing = async () => { + if (isProfileSyncingEnabled) { + navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, { + screen: Routes.SHEET.PROFILE_SYNCING, + }); + } else { + await enableProfileSyncing(); + } + trackEvent(MetaMetricsEvents.SETTINGS_UPDATED, { + settings_group: 'onboarding_advanced_configuration', + settings_type: 'profile_syncing', + old_value: isProfileSyncingEnabled, + new_value: !isProfileSyncingEnabled, + }); + }; + + return ( + <ScrollView style={styles.root}> + <BasicFunctionalityComponent handleSwitchToggle={handleSwitchToggle} /> + {isNotificationsFeatureEnabled() && ( + <ProfileSyncingComponent + handleSwitchToggle={toggleProfileSyncing} + isBasicFunctionalityEnabled={isBasicFunctionalityEnabled} + isProfileSyncingEnabled={isProfileSyncingEnabled} + /> + )} + <ManageNetworksComponent /> + </ScrollView> + ); +}; + +export default GeneralSettings; diff --git a/app/components/Views/OnboardingSuccess/OnboardingSecuritySettings/__snapshots__/index.test.tsx.snap b/app/components/Views/OnboardingSuccess/OnboardingSecuritySettings/__snapshots__/index.test.tsx.snap new file mode 100644 index 000000000000..ab1dc767165e --- /dev/null +++ b/app/components/Views/OnboardingSuccess/OnboardingSecuritySettings/__snapshots__/index.test.tsx.snap @@ -0,0 +1,120 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OnboardingSecuritySettings should render correctly 1`] = ` +<RCTScrollView + style={ + { + "flex": 1, + "paddingHorizontal": 16, + "paddingTop": 16, + } + } +> + <View> + <View + style={ + { + "marginTop": 16, + } + } + testID="use-chains-list-validation" + > + <View + style={ + { + "alignItems": "center", + "flexDirection": "row", + } + } + > + <Text + accessibilityRole="text" + style={ + { + "color": "#141618", + "flex": 1, + "fontFamily": "EuclidCircularB-Medium", + "fontSize": 16, + "fontWeight": "500", + "letterSpacing": 0, + "lineHeight": 24, + } + } + > + Network details check + </Text> + <View + style={ + { + "marginLeft": 16, + } + } + > + <RCTSwitch + accessibilityRole="switch" + onChange={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + onTintColor="#0376c9" + style={ + [ + { + "height": 31, + "width": 51, + }, + [ + { + "alignSelf": "flex-start", + }, + { + "backgroundColor": "#bbc0c566", + "borderRadius": 16, + }, + ], + ] + } + testID="display-use-safe-list-validation" + thumbTintColor="#ffffff" + tintColor="#bbc0c566" + value={false} + /> + </View> + </View> + <Text + accessibilityRole="text" + style={ + { + "color": "#6a737d", + "fontFamily": "EuclidCircularB-Regular", + "fontSize": 14, + "fontWeight": "400", + "letterSpacing": 0, + "lineHeight": 22, + "marginTop": 8, + } + } + > + MetaMask uses a third-party service called + <Text + accessibilityRole="text" + style={ + { + "color": "#141618", + "fontFamily": "EuclidCircularB-Bold", + "fontSize": 14, + "fontWeight": "700", + "letterSpacing": 0, + "lineHeight": 22, + } + } + > + chainid.network + </Text> + to show accurate and standardized network details. This reduces your chances of connecting to malicious or incorrect network. When using this feature, your IP address is exposed to + + chainid.network + </Text> + </View> + </View> +</RCTScrollView> +`; diff --git a/app/components/Views/OnboardingSuccess/OnboardingSecuritySettings/index.test.tsx b/app/components/Views/OnboardingSuccess/OnboardingSecuritySettings/index.test.tsx new file mode 100644 index 000000000000..0d01bcc47da8 --- /dev/null +++ b/app/components/Views/OnboardingSuccess/OnboardingSecuritySettings/index.test.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { useNavigation } from '@react-navigation/native'; +import { useSelector } from 'react-redux'; +import renderWithProvider from '../../../../util/test/renderWithProvider'; +import { selectUseSafeChainsListValidation } from '../../../../selectors/preferencesController'; +import OnboardingSecuritySettings from './'; + +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: jest.fn(), +})); + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: jest.fn(), +})); + +jest.mock('../../../../util/networks', () => ({ + toggleUseSafeChainsListValidation: jest.fn(), +})); + +describe('OnboardingSecuritySettings', () => { + const mockNavigation = { + goBack: jest.fn(), + setOptions: jest.fn(), + navigate: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + (useNavigation as jest.Mock).mockReturnValue(mockNavigation); + }); + + it('should render correctly', () => { + (useSelector as jest.Mock).mockImplementation((selector) => { + if (selector === selectUseSafeChainsListValidation) return false; + return null; + }); + const { toJSON } = renderWithProvider(<OnboardingSecuritySettings />); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/app/components/Views/OnboardingSuccess/OnboardingSecuritySettings/index.tsx b/app/components/Views/OnboardingSuccess/OnboardingSecuritySettings/index.tsx new file mode 100644 index 000000000000..7076994c9125 --- /dev/null +++ b/app/components/Views/OnboardingSuccess/OnboardingSecuritySettings/index.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { ScrollView } from 'react-native'; +import { useStyles } from '../../../../component-library/hooks'; +import { useOnboardingHeader } from '../../../hooks/useOnboardingHeader'; +import { strings } from '../../../../../locales/i18n'; +import NetworkDetailsCheckSettings from '../../Settings/NetworkDetailsCheckSettings'; +import styleSheet from '../DefaultSettings/index.styles'; + +const SecuritySettings = () => { + const { styles } = useStyles(styleSheet, {}); + useOnboardingHeader(strings('default_settings.drawer_security_title')); + return ( + <ScrollView style={styles.root}> + <NetworkDetailsCheckSettings /> + </ScrollView> + ); +}; + +export default SecuritySettings; diff --git a/app/components/Views/Settings/SecuritySettings/SecuritySettings.constants.ts b/app/components/Views/Settings/SecuritySettings/SecuritySettings.constants.ts index 6c35ca961272..85c5d58ef7e9 100644 --- a/app/components/Views/Settings/SecuritySettings/SecuritySettings.constants.ts +++ b/app/components/Views/Settings/SecuritySettings/SecuritySettings.constants.ts @@ -13,8 +13,3 @@ export const DELETE_METRICS_BUTTON = 'delete-metrics-button'; export const SECURITY_SETTINGS_DELETE_WALLET_BUTTON = 'security-settings-delete-wallet-buttons'; export const THIRD_PARTY_SECTION = 'third-party-section'; -export const NFT_AUTO_DETECT_MODE_SECTION = - 'nft-opensea-autodetect-mode-section'; -export const USE_SAFE_CHAINS_LIST_VALIDATION = 'use-chains-list-validation'; -export const DISPLAY_SAFE_CHAINS_LIST_VALIDATION = - 'display-use-safe-list-validation'; diff --git a/app/components/Views/Settings/SecuritySettings/SecuritySettings.styles.ts b/app/components/Views/Settings/SecuritySettings/SecuritySettings.styles.ts index 190c50b26f8b..43a5db305cd5 100644 --- a/app/components/Views/Settings/SecuritySettings/SecuritySettings.styles.ts +++ b/app/components/Views/Settings/SecuritySettings/SecuritySettings.styles.ts @@ -18,11 +18,6 @@ const createStyles = (colors: Colors) => accessory: { marginTop: 16, }, - transactionsContainer: { - marginTop: 24, - marginLeft: -16, - marginRight: -16, - }, titleContainer: { flexDirection: 'row', alignItems: 'center', diff --git a/app/components/Views/Settings/SecuritySettings/SecuritySettings.test.tsx b/app/components/Views/Settings/SecuritySettings/SecuritySettings.test.tsx index 5b1737cba399..07dfc2dbdd67 100644 --- a/app/components/Views/Settings/SecuritySettings/SecuritySettings.test.tsx +++ b/app/components/Views/Settings/SecuritySettings/SecuritySettings.test.tsx @@ -11,12 +11,10 @@ import { LOGIN_OPTIONS, META_METRICS_DATA_MARKETING_SECTION, META_METRICS_SECTION, - NFT_AUTO_DETECT_MODE_SECTION, REVEAL_PRIVATE_KEY_SECTION, SDK_SECTION, SECURITY_SETTINGS_DELETE_WALLET_BUTTON, TURN_ON_REMEMBER_ME, - USE_SAFE_CHAINS_LIST_VALIDATION, } from './SecuritySettings.constants'; import { SecurityPrivacyViewSelectorsIDs } from '../../../../../e2e/selectors/Settings/SecurityAndPrivacy/SecurityPrivacyView.selectors'; import SECURITY_ALERTS_TOGGLE_TEST_ID from './constants'; @@ -105,9 +103,7 @@ describe('SecuritySettings', () => { expect(getByTestId(DELETE_METRICS_BUTTON)).toBeTruthy(); expect(getByTestId(META_METRICS_DATA_MARKETING_SECTION)).toBeTruthy(); expect(getByTestId(SECURITY_SETTINGS_DELETE_WALLET_BUTTON)).toBeTruthy(); - expect(getByTestId(NFT_AUTO_DETECT_MODE_SECTION)).toBeTruthy(); expect(getByText('Automatic security checks')).toBeTruthy(); - expect(getByTestId(USE_SAFE_CHAINS_LIST_VALIDATION)).toBeTruthy(); }); it('renders Blockaid settings', async () => { diff --git a/app/components/Views/Settings/SecuritySettings/SecuritySettings.tsx b/app/components/Views/Settings/SecuritySettings/SecuritySettings.tsx index dce67222d07a..df3b897cc95e 100644 --- a/app/components/Views/Settings/SecuritySettings/SecuritySettings.tsx +++ b/app/components/Views/Settings/SecuritySettings/SecuritySettings.tsx @@ -52,11 +52,7 @@ import { selectProviderType } from '../../../../selectors/networkController'; import { selectUseTransactionSimulations } from '../../../../selectors/preferencesController'; import { SECURITY_PRIVACY_VIEW_ID } from '../../../../../wdio/screen-objects/testIDs/Screens/SecurityPrivacy.testIds'; import createStyles from './SecuritySettings.styles'; -import { - HeadingProps, - // NetworksI, - SecuritySettingsParams, -} from './SecuritySettings.types'; +import { HeadingProps, SecuritySettingsParams } from './SecuritySettings.types'; import { useFocusEffect, useNavigation } from '@react-navigation/native'; import { useParams } from '../../../../util/navigation/navUtils'; import { diff --git a/app/components/Views/Settings/SecuritySettings/SecuritySettings.types.ts b/app/components/Views/Settings/SecuritySettings/SecuritySettings.types.ts index 8b1ffbda5ef5..3a4696aded71 100644 --- a/app/components/Views/Settings/SecuritySettings/SecuritySettings.types.ts +++ b/app/components/Views/Settings/SecuritySettings/SecuritySettings.types.ts @@ -1,5 +1,3 @@ -import { ImageSourcePropType } from 'react-native'; - export interface GatewayWithAvailability { key: string; value: string; @@ -19,15 +17,3 @@ export interface SecuritySettingsParams { export interface EtherscanNetworksType { [key: string]: { domain: string; subdomain: string; networkId: string }; } - -export interface NetworksI { - [key: string]: { - name: string; - imageSource?: ImageSourcePropType; - shortName: string; - networkId?: number; - chainId?: string; - color: string; - networkType: string; - }; -} diff --git a/app/components/hooks/useOnboardingHeader.tsx b/app/components/hooks/useOnboardingHeader.tsx new file mode 100644 index 000000000000..ad5969a62791 --- /dev/null +++ b/app/components/hooks/useOnboardingHeader.tsx @@ -0,0 +1,44 @@ +import React, { useCallback, useLayoutEffect } from 'react'; +import { useNavigation } from '@react-navigation/native'; +import Text, { + TextVariant, +} from '../../component-library/components/Texts/Text'; +import { IconName } from '../../component-library/components/Icons/Icon'; +import ButtonIcon from '../../component-library/components/Buttons/ButtonIcon'; +import { ButtonIconSizes } from '../../component-library/components/Buttons/ButtonIcon/ButtonIcon.types'; + +const styles = { + backButtonContainer: { + marginLeft: 6, + }, +}; + +export const useOnboardingHeader = (title: string) => { + const navigation = useNavigation(); + + const renderBackButton = useCallback( + () => ( + <ButtonIcon + size={ButtonIconSizes.Lg} + iconName={IconName.ArrowLeft} + accessibilityRole="button" + accessibilityLabel="back" + onPress={() => navigation.goBack()} + style={styles.backButtonContainer} + /> + ), + [navigation], + ); + + const renderTitle = useCallback( + () => <Text variant={TextVariant.HeadingMD}>{title}</Text>, + [title], + ); + + useLayoutEffect(() => { + navigation.setOptions({ + headerLeft: renderBackButton, + headerTitle: renderTitle, + }); + }, [navigation, renderBackButton, renderTitle]); +}; diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts index acfa58343545..0becb6358b78 100644 --- a/app/constants/navigation/Routes.ts +++ b/app/constants/navigation/Routes.ts @@ -58,6 +58,9 @@ const Routes = { SUCCESS_FLOW: 'OnboardingSuccessFlow', SUCCESS: 'OnboardingSuccess', DEFAULT_SETTINGS: 'DefaultSettings', + GENERAL_SETTINGS: 'GeneralSettings', + ASSETS_SETTINGS: 'AssetsSettings', + SECURITY_SETTINGS: 'SecuritySettings', HOME_NAV: 'HomeNav', ONBOARDING: 'Onboarding', LOGIN: 'Login', diff --git a/e2e/selectors/Settings/AdvancedView.selectors.js b/e2e/selectors/Settings/AdvancedView.selectors.js index 2bc17cc7a1aa..bfb05ba34d7a 100644 --- a/e2e/selectors/Settings/AdvancedView.selectors.js +++ b/e2e/selectors/Settings/AdvancedView.selectors.js @@ -1,5 +1,6 @@ export const AdvancedViewSelectorsIDs = { CONTAINER: 'advanced-settings', + ETH_SIGN_SWITCH: 'eth-sign-switch', TOKEN_DETECTION_TOGGLE: 'token-detection-toggle', SHOW_FIAT_ON_TESTNETS: 'show-fiat-on-testnets', ADVANCED_SETTINGS_SCROLLVIEW: 'advanced-settings-scrollview', diff --git a/locales/languages/en.json b/locales/languages/en.json index 0139c2a29f94..19ae9bc3ee5d 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -3356,7 +3356,7 @@ "default_settings": "Default Settings", "done": "Done", "basic_functionality": "Basic functionality", - "manage_networks": "Manage networks", + "manage_networks": "Choose your network", "manage_networks_body": "We use Infura as our remote procedure call (RPC) provider to offer the most reliable and private access to Ethereum data we can. You can choose your own RPC, but remember that any RPC will receive your IP address and Ethereum wallet to make transactions. Read our ", "manage_networks_body2": " to learn more about how Infura handles data.", "functionality_body": "MetaMask offers basic features like token details and gas settings through internet services. When you use internet services, your IP address is shared, in this case with MetaMask. This is just like when you visit any website. MetaMask uses this data temporarily and never sells your data. You can use a VPN or turn off these services, but it may affect your MetaMask experience. Read our ", @@ -3373,7 +3373,14 @@ "turn_off": "Turn off", "reset": "Reset" } - } + }, + "drawer_general_title": "General", + "drawer_general_title_desc": "Sync settings across devices, select network preferences, and track token data", + "drawer_assets_title": "Assets", + "drawer_assets_desc": "Autodetect tokens in your wallet, display NFTs, and get batched account balance updates", + "drawer_security_title": "Security", + "drawer_security_desc": "Reduce your chances of joining unsafe networks and protect your accounts", + "network_details_check_desc": "MetaMask uses a third-party service called chainid.network to show accurate and standardized network details. This reduces your chances of connecting to malicious or incorrect network. When using this feature, your IP address is exposed to chainid.network." }, "simulation_details": { "failed": "There was an error loading your estimation.", From 64807c1b4cd367c00f7f0a49d18f5a97a9c729df Mon Sep 17 00:00:00 2001 From: Jonathan Ferreira <44679989+Jonathansoufer@users.noreply.github.com> Date: Wed, 9 Oct 2024 22:37:58 +0100 Subject: [PATCH 10/46] fix: add safe space on the left of bell icon (#11722) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- 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 fix a small misalignment on the notifications icons on small devices. ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** ![image (1)](https://github.com/user-attachments/assets/1e9e49ee-d0cc-43ff-a251-0a92d23df61c) ### **After** <img width="504" alt="Screenshot 2024-10-09 at 22 02 18" src="https://github.com/user-attachments/assets/74145723-3375-4cba-9806-b38cc41772ec"> ## **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** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] 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. --- app/components/UI/Navbar/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/UI/Navbar/index.js b/app/components/UI/Navbar/index.js index 708cc6980304..8127d67f23ce 100644 --- a/app/components/UI/Navbar/index.js +++ b/app/components/UI/Navbar/index.js @@ -91,7 +91,7 @@ const styles = StyleSheet.create({ paddingVertical: Device.isAndroid() ? 14 : 8, }, notificationButton: { - marginRight: 4, + marginHorizontal: 4, }, disabled: { opacity: 0.3, From 3ebaa66fd876e13d06b6859c6c41af96f02de353 Mon Sep 17 00:00:00 2001 From: Brian Bergeron <brian.e.bergeron@gmail.com> Date: Wed, 9 Oct 2024 15:27:56 -0700 Subject: [PATCH 11/46] fix: token list after switching networks (#11718) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- 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** Fixes an issue where after switching networks, importing a token populates search results from the old network. The fix is to patch https://github.com/MetaMask/core/pull/4316, which will be introduced in asset controllers version 33. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/11696 ## **Manual testing steps** 1. Force close MM app 2. Open MM 3. Switch networks 4. click import tokens 5. search for a token by name, like usdt 6. the imported token should have prices available and have the correct contract address ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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. --- app/core/Engine.ts | 2 +- .../@metamask+assets-controllers+31.0.0.patch | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/app/core/Engine.ts b/app/core/Engine.ts index 4a224f09191f..6ad2dec93703 100644 --- a/app/core/Engine.ts +++ b/app/core/Engine.ts @@ -683,7 +683,7 @@ class Engine { // @ts-expect-error TODO: Resolve mismatch between base-controller versions. messenger: this.controllerMessenger.getRestricted({ name: 'TokenListController', - allowedActions: [], + allowedActions: [`${networkController.name}:getNetworkClientById`], allowedEvents: [`${networkController.name}:stateChange`], }), }); diff --git a/patches/@metamask+assets-controllers+31.0.0.patch b/patches/@metamask+assets-controllers+31.0.0.patch index 4ff3c0e02fc6..7de2ffecb6a1 100644 --- a/patches/@metamask+assets-controllers+31.0.0.patch +++ b/patches/@metamask+assets-controllers+31.0.0.patch @@ -598,6 +598,28 @@ index f7509a1..52bc67e 100644 var TokenBalancesController_default = TokenBalancesController; +diff --git a/node_modules/@metamask/assets-controllers/dist/chunk-JUI3XNEF.js b/node_modules/@metamask/assets-controllers/dist/chunk-JUI3XNEF.js +index 44804c8..911a6e6 100644 +--- a/node_modules/@metamask/assets-controllers/dist/chunk-JUI3XNEF.js ++++ b/node_modules/@metamask/assets-controllers/dist/chunk-JUI3XNEF.js +@@ -247,10 +247,15 @@ var TokenListController = class extends _pollingcontroller.StaticIntervalPolling + }; + _onNetworkControllerStateChange = new WeakSet(); + onNetworkControllerStateChange_fn = async function(networkControllerState) { +- if (this.chainId !== networkControllerState.providerConfig.chainId) { ++ const selectedNetworkClient = this.messagingSystem.call( ++ "NetworkController:getNetworkClientById", ++ networkControllerState.selectedNetworkClientId ++ ); ++ const { chainId } = selectedNetworkClient.configuration; ++ if (this.chainId !== chainId) { + this.abortController.abort(); + this.abortController = new AbortController(); +- this.chainId = networkControllerState.providerConfig.chainId; ++ this.chainId = chainId; + if (this.state.preventPollingOnNetworkRestart) { + this.clearingTokenListData(); + } else { diff --git a/node_modules/@metamask/assets-controllers/dist/chunk-QFDTOEYR.js b/node_modules/@metamask/assets-controllers/dist/chunk-QFDTOEYR.js index 5335fa5..ae37683 100644 --- a/node_modules/@metamask/assets-controllers/dist/chunk-QFDTOEYR.js From 678d468f828add889eaf3fa24a596b91d9b861f3 Mon Sep 17 00:00:00 2001 From: Frederik Bolding <frederik.bolding@gmail.com> Date: Thu, 10 Oct 2024 10:59:15 +0200 Subject: [PATCH 12/46] feat: Implement partially local Snaps execution environment (#11653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- 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** Replaces the remote WebView with a locally bundled WebView. This WebView still uses a remote iframe for execution, but the initial load will be using all local code. This is attempt number two since https://github.com/MetaMask/metamask-mobile/pull/10214 was reverted. This PR is slightly different from the aforementioned PR since the HTML is inlined using `babel-plugin-inline-import` AND the execution environment is considered a secure context due to the `baseUrl` being set. ## **Related issues** Fixes: ## **Manual testing steps** 1. Install any Snap 2. See that it works ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [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** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] 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. --------- Co-authored-by: JSoufer <jonathan.ferreira@consensys.net> --- .depcheckrc.yml | 1 + app/lib/snaps/SnapsExecutionWebView.tsx | 10 +++++----- babel.config.js | 4 ++++ package.json | 1 + yarn.lock | 26 +++++++++++++++++++++++++ 5 files changed, 37 insertions(+), 5 deletions(-) diff --git a/.depcheckrc.yml b/.depcheckrc.yml index e8fe7a446ec7..c90705901b45 100644 --- a/.depcheckrc.yml +++ b/.depcheckrc.yml @@ -6,6 +6,7 @@ ignores: - '@react-native-community/slider' - 'patch-package' - '@lavamoat/allow-scripts' + - 'babel-plugin-inline-import' # This is used on the patch for TokenRatesController of Assets controllers, for we to be able to use the last version of it - cockatiel diff --git a/app/lib/snaps/SnapsExecutionWebView.tsx b/app/lib/snaps/SnapsExecutionWebView.tsx index 24b8e99a0878..ae1b4ebfaeee 100644 --- a/app/lib/snaps/SnapsExecutionWebView.tsx +++ b/app/lib/snaps/SnapsExecutionWebView.tsx @@ -3,11 +3,11 @@ import React, { Component, RefObject } from 'react'; import { View, ScrollView, NativeSyntheticEvent } from 'react-native'; import { WebViewMessageEvent, WebView } from '@metamask/react-native-webview'; import { createStyles } from './styles'; -import { WebViewError } from '@metamask/react-native-webview/lib/WebViewTypes'; import { WebViewInterface } from '@metamask/snaps-controllers/react-native'; +import { WebViewError } from '@metamask/react-native-webview/lib/WebViewTypes'; import { PostMessageEvent } from '@metamask/post-message-stream'; - -const SNAPS_EE_URL = 'https://execution.metamask.io/webview/6.7.1/index.html'; +// @ts-expect-error Types are currently broken for this. +import WebViewHTML from '@metamask/snaps-execution-environments/dist/browserify/webview/index.html'; const styles = createStyles(); @@ -86,11 +86,11 @@ export class SnapsExecutionWebView extends Component { ref={ this.setWebViewRef as unknown as React.RefObject<WebView> | null } - source={{ uri: SNAPS_EE_URL}} + source={{ html: WebViewHTML, baseUrl: 'https://localhost' }} onMessage={this.onWebViewMessage} onError={this.onWebViewError} onLoadEnd={this.onWebViewLoad} - originWhitelist={['https://execution.metamask.io*']} + originWhitelist={['https://localhost*']} javaScriptEnabled /> </View> diff --git a/babel.config.js b/babel.config.js index da1d15c592f4..2d59074d9c9e 100644 --- a/babel.config.js +++ b/babel.config.js @@ -19,6 +19,10 @@ module.exports = { test: './node_modules/@metamask/notification-services-controller', plugins: [['@babel/plugin-transform-private-methods', { loose: true }]], }, + { + test: './app/lib/snaps', + plugins: [['babel-plugin-inline-import', { extensions: ['.html'] }]], + }, ], env: { production: { diff --git a/package.json b/package.json index dad216e5954a..0dca731ef3d3 100644 --- a/package.json +++ b/package.json @@ -435,6 +435,7 @@ "assert": "^1.5.0", "babel-jest": "^29.7.0", "babel-loader": "^9.1.3", + "babel-plugin-inline-import": "^3.0.0", "babel-plugin-transform-inline-environment-variables": "^0.4.4", "babel-plugin-transform-remove-console": "6.9.4", "browserstack-local": "^1.5.1", diff --git a/yarn.lock b/yarn.lock index eeb198f65851..29605d091316 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13120,6 +13120,13 @@ babel-plugin-emotion@^10.0.27: find-root "^1.1.0" source-map "^0.5.7" +babel-plugin-inline-import@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/babel-plugin-inline-import/-/babel-plugin-inline-import-3.0.0.tgz#220eb2a52f8e779d8fb89447f950275e1e3f5981" + integrity sha512-thnykl4FMb8QjMjVCuZoUmAM7r2mnTn5qJwrryCvDv6rugbJlTHZMctdjDtEgD0WBAXJOLJSGXN3loooEwx7UQ== + dependencies: + require-resolve "0.0.2" + babel-plugin-istanbul@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" @@ -24306,6 +24313,11 @@ path-exists@^5.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-5.0.0.tgz#a6aad9489200b21fab31e49cf09277e5116fb9e7" integrity sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ== +path-extra@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/path-extra/-/path-extra-1.0.3.tgz#7c112189a6e50d595790e7ad2037e44e410c1166" + integrity sha512-vYm3+GCkjUlT1rDvZnDVhNLXIRvwFPaN8ebHAFcuMJM/H0RBOPD7JrcldiNLd9AS3dhAyUHLa4Hny5wp1A+Ffw== + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -26792,6 +26804,13 @@ require-package-name@^2.0.1: resolved "https://registry.yarnpkg.com/require-package-name/-/require-package-name-2.0.1.tgz#c11e97276b65b8e2923f75dabf5fb2ef0c3841b9" integrity sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q== +require-resolve@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/require-resolve/-/require-resolve-0.0.2.tgz#bab410ab1aee2f3f55b79317451dd3428764e6f3" + integrity sha512-eafQVaxdQsWUB8HybwognkdcIdKdQdQBwTxH48FuE6WI0owZGKp63QYr1MRp73PoX0AcyB7MDapZThYUY8FD0A== + dependencies: + x-path "^0.0.2" + requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" @@ -30222,6 +30241,13 @@ ws@^6.2.2, ws@^6.2.3: dependencies: async-limiter "~1.0.0" +x-path@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/x-path/-/x-path-0.0.2.tgz#294d076bb97a7706cc070bbb2a6fd8c54df67b12" + integrity sha512-zQ4WFI0XfJN1uEkkrB19Y4TuXOlHqKSxUJo0Yt+axPjRm8tCG6SJ6+Wo3/+Kjg4c2c8IvBXuJ0uYoshxNn4qMw== + dependencies: + path-extra "^1.0.2" + xcode@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/xcode/-/xcode-3.0.1.tgz#3efb62aac641ab2c702458f9a0302696146aa53c" From a133d1c72601f0cf82a784eea0e3687d6de0b343 Mon Sep 17 00:00:00 2001 From: Jyoti Puri <jyotipuri@gmail.com> Date: Thu, 10 Oct 2024 19:31:05 +0530 Subject: [PATCH 13/46] feat: add copy button component (#11736) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Adding re-usable copy button component and some code cleanup. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/11735 ## **Manual testing steps** 1. Run storybook locally 2. Check copy button component ## **Screenshots/Recordings** <img width="382" alt="Screenshot 2024-10-10 at 4 29 36 PM" src="https://github.com/user-attachments/assets/7d5e08ef-37b8-4236-9d6c-19a2afc3881f"> ## **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 - [ ] 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. --- .storybook/storybook.requires.js | 1 + .../AccountNetworkInfoExpanded.tsx | 9 +++-- .../AccountNetworkInfoExpanded.test.tsx.snap | 4 ++- .../PersonalSign/Message/Message.styles.ts | 2 +- .../Info/PersonalSign/Message/Message.tsx | 28 +++------------ .../UI/CopyButton/CopyButton.stories.tsx | 8 +++++ .../UI/CopyButton/CopyButton.test.tsx | 20 +++++++++++ .../components/UI/CopyButton/CopyButton.tsx | 36 +++++++++++++++++++ .../__snapshots__/CopyButton.test.tsx.snap | 36 +++++++++++++++++++ .../components/UI/CopyButton/index.tsx | 1 + .../Tooltip/{style.ts => Tooltip.styles.ts} | 21 ++++++----- .../components/UI/Tooltip/Tooltip.tsx | 11 +++--- 12 files changed, 135 insertions(+), 42 deletions(-) create mode 100644 app/components/Views/confirmations/components/UI/CopyButton/CopyButton.stories.tsx create mode 100644 app/components/Views/confirmations/components/UI/CopyButton/CopyButton.test.tsx create mode 100644 app/components/Views/confirmations/components/UI/CopyButton/CopyButton.tsx create mode 100644 app/components/Views/confirmations/components/UI/CopyButton/__snapshots__/CopyButton.test.tsx.snap create mode 100644 app/components/Views/confirmations/components/UI/CopyButton/index.tsx rename app/components/Views/confirmations/components/UI/Tooltip/{style.ts => Tooltip.styles.ts} (63%) diff --git a/.storybook/storybook.requires.js b/.storybook/storybook.requires.js index c71bdd13c825..a1ca7774ac88 100644 --- a/.storybook/storybook.requires.js +++ b/.storybook/storybook.requires.js @@ -126,6 +126,7 @@ const getStories = () => { "./app/components/Views/confirmations/components/UI/InfoRow/InfoRow.stories.tsx": require("../app/components/Views/confirmations/components/UI/InfoRow/InfoRow.stories.tsx"), "./app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.stories.tsx": require("../app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.stories.tsx"), "./app/components/Views/confirmations/components/UI/Tooltip/Tooltip.stories.tsx": require("../app/components/Views/confirmations/components/UI/Tooltip/Tooltip.stories.tsx"), + "./app/components/Views/confirmations/components/UI/CopyButton/CopyButton.stories.tsx": require("../app/components/Views/confirmations/components/UI/CopyButton/CopyButton.stories.tsx"), }; }; diff --git a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/AccountNetworkInfoExpanded.tsx b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/AccountNetworkInfoExpanded.tsx index 8dd9eb4c6267..a4750a7f1911 100644 --- a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/AccountNetworkInfoExpanded.tsx +++ b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/AccountNetworkInfoExpanded.tsx @@ -3,7 +3,7 @@ import { View } from 'react-native'; import { useSelector } from 'react-redux'; import { strings } from '../../../../../../../../locales/i18n'; -import { selectRpcUrl } from '../../../../../../../selectors/networkController'; +import { selectProviderConfig } from '../../../../../../../selectors/networkController'; import { selectNetworkName } from '../../../../../../../selectors/networkInfos'; import useAccountInfo from '../../../../hooks/useAccountInfo'; import useApprovalRequest from '../../../../hooks/useApprovalRequest'; @@ -15,7 +15,8 @@ import InfoURL from '../../../UI/InfoRow/InfoValue/InfoURL'; const AccountNetworkInfoExpanded = () => { const { approvalRequest } = useApprovalRequest(); const networkName = useSelector(selectNetworkName); - const networkRpcUrl = useSelector(selectRpcUrl); + const { rpcUrl: networkRpcUrl, type: networkType } = + useSelector(selectProviderConfig); const fromAddress = approvalRequest?.requestData?.from; const { accountAddress, accountBalance } = useAccountInfo(fromAddress); @@ -34,7 +35,9 @@ const AccountNetworkInfoExpanded = () => { {networkName} </InfoRow> <InfoRow label={strings('confirm.rpc_url')}> - <InfoURL url={networkRpcUrl} /> + <InfoURL + url={networkRpcUrl ?? `https://${networkType}.infura.io/v3/`} + /> </InfoRow> </InfoSection> </View> diff --git a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/__snapshots__/AccountNetworkInfoExpanded.test.tsx.snap b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/__snapshots__/AccountNetworkInfoExpanded.test.tsx.snap index 367b688358bc..cae8ebe4e328 100644 --- a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/__snapshots__/AccountNetworkInfoExpanded.test.tsx.snap +++ b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/__snapshots__/AccountNetworkInfoExpanded.test.tsx.snap @@ -291,7 +291,9 @@ exports[`AccountNetworkInfoExpanded should match snapshot for personal sign 1`] "marginTop": 8, } } - /> + > + mainnet.infura.io/v3/ + </Text> </View> </View> </View> diff --git a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/Message.styles.ts b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/Message.styles.ts index 3fde7499b126..773c248e396d 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/Message.styles.ts +++ b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/Message.styles.ts @@ -35,7 +35,7 @@ const styleSheet = (params: { theme: Theme }) => { fontSize: 14, fontWeight: '400', }, - copyButton: { + copyButtonContainer: { position: 'absolute', top: -40, right: 0, diff --git a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/Message.tsx b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/Message.tsx index 5a2768e53b61..b80f2d23eed3 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/Message.tsx +++ b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Message/Message.tsx @@ -1,25 +1,17 @@ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import { Text, View } from 'react-native'; import { hexToText } from '@metamask/controller-utils'; -import ButtonIcon, { - ButtonIconSizes, -} from '../../../../../../../../component-library/components/Buttons/ButtonIcon'; -import ClipboardManager from '../../../../../../../../core/ClipboardManager'; -import { - IconColor, - IconName, -} from '../../../../../../../../component-library/components/Icons/Icon'; import { sanitizeString } from '../../../../../../../../util/string'; import { strings } from '../../../../../../../../../locales/i18n'; import { useStyles } from '../../../../../../../../component-library/hooks'; import useApprovalRequest from '../../../../../hooks/useApprovalRequest'; import ExpandableSection from '../../../../UI/ExpandableSection'; import styleSheet from './Message.styles'; +import CopyButton from '../../../../UI/CopyButton'; const Message = () => { const { approvalRequest } = useApprovalRequest(); - const [copied, setCopied] = useState(false); const { styles } = useStyles(styleSheet, {}); const message = useMemo( @@ -27,11 +19,6 @@ const Message = () => { [approvalRequest?.requestData?.data], ); - const copyMessage = useCallback(async () => { - await ClipboardManager.setString(message); - setCopied(true); - }, [message, setCopied]); - return ( <ExpandableSection collapsedContent={ @@ -44,14 +31,9 @@ const Message = () => { } expandedContent={ <View style={styles.messageContainer}> - <ButtonIcon - iconColor={IconColor.Muted} - size={ButtonIconSizes.Sm} - onPress={copyMessage} - iconName={copied ? IconName.CopySuccess : IconName.Copy} - style={styles.copyButton} - testID="copyButtonTestId" - /> + <View style={styles.copyButtonContainer}> + <CopyButton copyText={message} /> + </View> <Text style={styles.messageExpanded}>{message}</Text> </View> } diff --git a/app/components/Views/confirmations/components/UI/CopyButton/CopyButton.stories.tsx b/app/components/Views/confirmations/components/UI/CopyButton/CopyButton.stories.tsx new file mode 100644 index 000000000000..caf637be0675 --- /dev/null +++ b/app/components/Views/confirmations/components/UI/CopyButton/CopyButton.stories.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react-native'; + +import CopyButton from './CopyButton'; + +storiesOf('Confirmations / CopyButton', module) + .addDecorator((getStory) => getStory()) + .add('Default', () => <CopyButton copyText="DUMMY" />); diff --git a/app/components/Views/confirmations/components/UI/CopyButton/CopyButton.test.tsx b/app/components/Views/confirmations/components/UI/CopyButton/CopyButton.test.tsx new file mode 100644 index 000000000000..c88186871bd7 --- /dev/null +++ b/app/components/Views/confirmations/components/UI/CopyButton/CopyButton.test.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { fireEvent, render } from '@testing-library/react-native'; + +import ClipboardManager from '../../../../../../core/ClipboardManager'; +import CopyButton from './CopyButton'; + +jest.mock('../../../../../../core/ClipboardManager'); + +describe('CopyButton', () => { + it('should match snapshot', async () => { + const container = render(<CopyButton copyText={'DUMMY'} />); + expect(container).toMatchSnapshot(); + }); + + it('should copy text to clipboard when pressed', async () => { + const { getByTestId } = render(<CopyButton copyText={'DUMMY'} />); + fireEvent.press(getByTestId('copyButtonTestId')); + expect(ClipboardManager.setString).toHaveBeenCalledTimes(1); + }); +}); diff --git a/app/components/Views/confirmations/components/UI/CopyButton/CopyButton.tsx b/app/components/Views/confirmations/components/UI/CopyButton/CopyButton.tsx new file mode 100644 index 000000000000..a08999eb3b13 --- /dev/null +++ b/app/components/Views/confirmations/components/UI/CopyButton/CopyButton.tsx @@ -0,0 +1,36 @@ +import React, { useCallback, useState } from 'react'; + +import ButtonIcon, { + ButtonIconSizes, +} from '../../../../../../component-library/components/Buttons/ButtonIcon'; +import ClipboardManager from '../../../../../../core/ClipboardManager'; +import { + IconColor, + IconName, +} from '../../../../../../component-library/components/Icons/Icon'; + +interface CopyButtonProps { + copyText: string; + testID?: string; +} + +const CopyButton = ({ copyText, testID }: CopyButtonProps) => { + const [copied, setCopied] = useState(false); + + const copyMessage = useCallback(async () => { + await ClipboardManager.setString(copyText); + setCopied(true); + }, [copyText, setCopied]); + + return ( + <ButtonIcon + iconColor={IconColor.Alternative} + size={ButtonIconSizes.Sm} + onPress={copyMessage} + iconName={copied ? IconName.CopySuccess : IconName.Copy} + testID={testID ?? 'copyButtonTestId'} + /> + ); +}; + +export default CopyButton; diff --git a/app/components/Views/confirmations/components/UI/CopyButton/__snapshots__/CopyButton.test.tsx.snap b/app/components/Views/confirmations/components/UI/CopyButton/__snapshots__/CopyButton.test.tsx.snap new file mode 100644 index 000000000000..8e93c909f650 --- /dev/null +++ b/app/components/Views/confirmations/components/UI/CopyButton/__snapshots__/CopyButton.test.tsx.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CopyButton should match snapshot 1`] = ` +<TouchableOpacity + accessible={true} + activeOpacity={1} + disabled={false} + onPress={[Function]} + onPressIn={[Function]} + onPressOut={[Function]} + style={ + { + "alignItems": "center", + "borderRadius": 8, + "height": 24, + "justifyContent": "center", + "opacity": 1, + "width": 24, + } + } + testID="copyButtonTestId" +> + <SvgMock + color="#6a737d" + height={16} + name="Copy" + style={ + { + "height": 16, + "width": 16, + } + } + width={16} + /> +</TouchableOpacity> +`; diff --git a/app/components/Views/confirmations/components/UI/CopyButton/index.tsx b/app/components/Views/confirmations/components/UI/CopyButton/index.tsx new file mode 100644 index 000000000000..90e9e6d2043d --- /dev/null +++ b/app/components/Views/confirmations/components/UI/CopyButton/index.tsx @@ -0,0 +1 @@ +export { default } from './CopyButton'; diff --git a/app/components/Views/confirmations/components/UI/Tooltip/style.ts b/app/components/Views/confirmations/components/UI/Tooltip/Tooltip.styles.ts similarity index 63% rename from app/components/Views/confirmations/components/UI/Tooltip/style.ts rename to app/components/Views/confirmations/components/UI/Tooltip/Tooltip.styles.ts index d5bac9213d34..91f9208b8bb9 100644 --- a/app/components/Views/confirmations/components/UI/Tooltip/style.ts +++ b/app/components/Views/confirmations/components/UI/Tooltip/Tooltip.styles.ts @@ -1,20 +1,22 @@ import { StyleSheet } from 'react-native'; -import { Colors, Theme } from '../../../../../../util/theme/models'; +import { Theme } from '../../../../../../util/theme/models'; import { fontStyles } from '../../../../../../styles/common'; -const createStyles = (colors: Colors, shadows: Theme['shadows']) => - StyleSheet.create({ +const styleSheet = (params: { theme: Theme }) => { + const { theme } = params; + + return StyleSheet.create({ modal: { margin: 0, }, modalView: { - backgroundColor: colors.background.default, + backgroundColor: theme.colors.background.default, justifyContent: 'center', alignItems: 'center', marginHorizontal: 16, borderRadius: 8, - ...shadows.size.sm, + ...theme.shadows.size.sm, elevation: 11, paddingHorizontal: 16, paddingVertical: 24, @@ -25,17 +27,18 @@ const createStyles = (colors: Colors, shadows: Theme['shadows']) => right: 10, }, modalTitle: { - color: colors.text.default, + color: theme.colors.text.default, ...fontStyles.bold, fontSize: 16, fontWeight: '700', marginBottom: 16, }, modalContent: { - color: colors.text.default, + color: theme.colors.text.default, ...fontStyles.normal, fontSize: 14, - } + }, }); +}; -export default createStyles; +export default styleSheet; diff --git a/app/components/Views/confirmations/components/UI/Tooltip/Tooltip.tsx b/app/components/Views/confirmations/components/UI/Tooltip/Tooltip.tsx index a4575fa4862e..e779ed935a54 100644 --- a/app/components/Views/confirmations/components/UI/Tooltip/Tooltip.tsx +++ b/app/components/Views/confirmations/components/UI/Tooltip/Tooltip.tsx @@ -9,8 +9,9 @@ import { IconColor, IconName, } from '../../../../../../component-library/components/Icons/Icon'; +import { useStyles } from '../../../../../../component-library/hooks'; import { useTheme } from '../../../../../../util/theme'; -import createStyles from './style'; +import styleSheet from './Tooltip.styles'; interface TooltipProps { content: ReactNode; @@ -20,8 +21,8 @@ interface TooltipProps { const Tooltip = ({ content, title, tooltipTestId }: TooltipProps) => { const [open, setOpen] = useState(false); - const { colors, shadows } = useTheme(); - const styles = createStyles(colors, shadows); + const { colors } = useTheme(); + const { styles } = useStyles(styleSheet, {}); return ( <View> @@ -52,8 +53,8 @@ const Tooltip = ({ content, title, tooltipTestId }: TooltipProps) => { style={styles.closeModalBtn} testID={tooltipTestId ?? 'tooltipTestId'} /> - {title && <Text style={styles.modalTitle}>{title}</Text>} - <Text>{content}</Text> + {title && <Text style={styles.modalTitle}>{title}</Text>} + <Text>{content}</Text> </View> </Modal> </View> From b0ef1a7064e2593615f62154d556d03793d320bd Mon Sep 17 00:00:00 2001 From: sahar-fehri <sahar.fehri@consensys.net> Date: Thu, 10 Oct 2024 17:50:30 +0200 Subject: [PATCH 14/46] fix: fix approve flow on swap (#11532) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** PR to fix the approve flow when users swap tokens. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/10834 ## **Manual testing steps** 1. Switch to base network 2. Swap WETH against any other token, exp USDC 3. See approval screen; it should have the buttons enabled and you should be able to see token icon and spending cap ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> https://github.com/user-attachments/assets/d8be6306-be74-4c6c-8463-567f35ee891e ### **After** <!-- [screenshots/recordings] --> https://github.com/user-attachments/assets/8382cc83-e4e7-42ce-b913-fcac7e906d4b ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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. --- .../__snapshots__/index.test.tsx.snap | 2 +- .../ApproveTransactionReview/index.js | 16 +- .../ApproveTransactionReview/index.test.tsx | 142 ++++++++++++++++++ 3 files changed, 151 insertions(+), 9 deletions(-) diff --git a/app/components/Views/confirmations/components/ApproveTransactionReview/__snapshots__/index.test.tsx.snap b/app/components/Views/confirmations/components/ApproveTransactionReview/__snapshots__/index.test.tsx.snap index 1d8f9d791057..da3fb4c98034 100644 --- a/app/components/Views/confirmations/components/ApproveTransactionReview/__snapshots__/index.test.tsx.snap +++ b/app/components/Views/confirmations/components/ApproveTransactionReview/__snapshots__/index.test.tsx.snap @@ -701,7 +701,7 @@ exports[`ApproveTransactionModal render matches snapshot 1`] = ` }, ] } - testID="" + testID="Confirm" > <Text style={ diff --git a/app/components/Views/confirmations/components/ApproveTransactionReview/index.js b/app/components/Views/confirmations/components/ApproveTransactionReview/index.js index 5814390edb81..6afec01a82a8 100644 --- a/app/components/Views/confirmations/components/ApproveTransactionReview/index.js +++ b/app/components/Views/confirmations/components/ApproveTransactionReview/index.js @@ -13,7 +13,6 @@ import { getApproveNavbar } from '../../../../UI/Navbar'; import { connect } from 'react-redux'; import { getHost } from '../../../../../util/browser'; import { - safeToChecksumAddress, getAddressAccountType, getTokenDetails, shouldShowBlockExplorer, @@ -389,7 +388,10 @@ class ApproveTransactionReview extends PureComponent { decodeApproveData(data); const encodedDecimalAmount = hexToBN(encodedHexAmount).toString(); - const contract = tokenList[safeToChecksumAddress(to)]; + // The tokenList addresses we get from state are not checksum addresses + // also, the tokenList we get does not contain the tokenStandard, so even if the token exists in tokenList we will + // need to fetch it using getTokenDetails + const contract = tokenList[to]; if (tokenAllowanceState) { const { tokenSymbol: symbol, @@ -405,7 +407,7 @@ class ApproveTransactionReview extends PureComponent { tokenBalance = balance; tokenStandard = standard; createdSpendCap = isReadyToApprove; - } else if (!contract) { + } else { try { const result = await getTokenDetails(to, from, encodedDecimalAmount); @@ -428,12 +430,9 @@ class ApproveTransactionReview extends PureComponent { ); } } catch (e) { - tokenSymbol = 'ERC20 Token'; - tokenDecimals = 18; + tokenSymbol = contract?.symbol || 'ERC20 Token'; + tokenDecimals = contract?.decimals || 18; } - } else { - tokenSymbol = contract.symbol; - tokenDecimals = contract.decimals; } const approveAmount = fromTokenMinimalUnit( @@ -895,6 +894,7 @@ class ApproveTransactionReview extends PureComponent { onConfirmPress={this.onConfirmPress} confirmDisabled={shouldDisableConfirmButton} confirmButtonState={this.getConfirmButtonState()} + confirmTestID="Confirm" > <View style={styles.actionViewChildren}> <ScrollView nestedScrollEnabled> diff --git a/app/components/Views/confirmations/components/ApproveTransactionReview/index.test.tsx b/app/components/Views/confirmations/components/ApproveTransactionReview/index.test.tsx index 6d7463b7d1b6..f1cf84e00e49 100644 --- a/app/components/Views/confirmations/components/ApproveTransactionReview/index.test.tsx +++ b/app/components/Views/confirmations/components/ApproveTransactionReview/index.test.tsx @@ -4,6 +4,19 @@ import ApproveTransactionModal from '.'; import { backgroundState } from '../../../../../util/test/initial-root-state'; import { renderScreen } from '../../../../../util/test/renderWithProvider'; import { SET_APPROVAL_FOR_ALL_SIGNATURE } from '../../../../../util/transactions'; +import { cloneDeep } from 'lodash'; +import { fireEvent, waitFor } from '@testing-library/react-native'; + +import { + getTokenDetails, +} from '../../../../../util/address'; + +jest.mock('../../../../../util/address', () => ({ + ...jest.requireActual('../../../../../util/address'), + getTokenDetails: jest.fn() +})); + + jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), @@ -19,6 +32,11 @@ jest.mock('../../../../../core/Engine', () => ({ KeyringController: { getOrAddQRKeyring: async () => ({ subscribe: () => ({}) }), }, + AssetsContractController: { + getERC20BalanceOf: jest + .fn() + .mockResolvedValue(0x0186a0), + } }, controllerMessenger: { subscribe: jest.fn(), @@ -74,4 +92,128 @@ describe('ApproveTransactionModal', () => { ); expect(toJSON()).toMatchSnapshot(); }); + + it('Approve button is enabled when standard is defined', async () => { + const mockGetTokenDetails = getTokenDetails as jest.Mock; + mockGetTokenDetails.mockReturnValue({ + standard: 'ERC20' + }); + const state = cloneDeep(initialState); + state.engine.backgroundState.AccountTrackerController.accounts = []; + state.engine.backgroundState.TokenListController = { + tokenList: { + '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f': { + address: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', + symbol: 'SNX', + decimals: 18, + name: 'Synthetix Network Token', + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f.png', + type: 'erc20', + aggregators: [ + 'Aave', + ], + occurrences: 10, + fees: { + '0x5fd79d46eba7f351fe49bff9e87cdea6c821ef9f': 0, + '0xda4ef8520b1a57d7d63f1e249606d1a459698876': 0, + }, + }, + } + }; + + state.transaction = { + to: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', + origin: 'test-dapp', + chainId: '0x1', + txParams: { + to: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', + from: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', + data, + origin: 'test-dapp', + }, + data, + }; + const mockOnConfirm = jest.fn(); + const {getByTestId } = renderScreen( + + () => ( + // eslint-disable-next-line react/react-in-jsx-scope + <ApproveTransactionModal + onConfirm={mockOnConfirm} + /> + ), + { name: 'Approve' }, + { state }, + ); + + expect(mockGetTokenDetails).toHaveBeenCalled(); + fireEvent.press(getByTestId('Confirm')); + expect(mockOnConfirm).toHaveBeenCalled(); + + await waitFor(() => { + const isDisabled = getByTestId('Confirm').props.disabled; + expect(isDisabled).toBe(false); + }); + }); + + it('Approve button is disabled when standard is undefined', async () => { + const mockGetTokenDetails = getTokenDetails as jest.Mock; + mockGetTokenDetails.mockReturnValue({}); + const state = cloneDeep(initialState); + state.engine.backgroundState.AccountTrackerController.accounts = []; + state.engine.backgroundState.TokenListController = { + tokenList: { + '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f': { + address: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', + symbol: 'SNX', + decimals: 18, + name: 'Synthetix Network Token', + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f.png', + type: 'erc20', + aggregators: [ + 'Aave', + ], + occurrences: 10, + fees: { + '0x5fd79d46eba7f351fe49bff9e87cdea6c821ef9f': 0, + '0xda4ef8520b1a57d7d63f1e249606d1a459698876': 0, + }, + }, + } + }; + + state.transaction = { + to: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', + origin: 'test-dapp', + chainId: '0x1', + txParams: { + to: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', + from: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', + data, + origin: 'test-dapp', + }, + data, + }; + const mockOnConfirm = jest.fn(); + const {getByTestId } = renderScreen( + + () => ( + // eslint-disable-next-line react/react-in-jsx-scope + <ApproveTransactionModal + onConfirm={mockOnConfirm} + /> + ), + { name: 'Approve' }, + { state }, + ); + + expect(mockGetTokenDetails).toHaveBeenCalled(); + await waitFor(() => { + const isDisabled = getByTestId('Confirm').props.disabled; + expect(isDisabled).toBe(true); + }); + }); + }); From 43505e98b3b01deb11a3ee75834361561391cef5 Mon Sep 17 00:00:00 2001 From: Jonathan Ferreira <44679989+Jonathansoufer@users.noreply.github.com> Date: Thu, 10 Oct 2024 17:12:40 +0100 Subject: [PATCH 15/46] fix: rollback originWhitelist (#11741) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- 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 fix a bug where snaps execution webview opens blocking the app instead of being on background. ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> https://github.com/user-attachments/assets/75e8a950-9cc7-4846-87f2-e456741b27c7 ### **After** <!-- [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** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] 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. --- app/lib/snaps/SnapsExecutionWebView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/snaps/SnapsExecutionWebView.tsx b/app/lib/snaps/SnapsExecutionWebView.tsx index ae1b4ebfaeee..d7072712e041 100644 --- a/app/lib/snaps/SnapsExecutionWebView.tsx +++ b/app/lib/snaps/SnapsExecutionWebView.tsx @@ -90,7 +90,7 @@ export class SnapsExecutionWebView extends Component { onMessage={this.onWebViewMessage} onError={this.onWebViewError} onLoadEnd={this.onWebViewLoad} - originWhitelist={['https://localhost*']} + originWhitelist={['*']} javaScriptEnabled /> </View> From 4a2ceea30cb085302f67eb23b387e0a15f82632e Mon Sep 17 00:00:00 2001 From: Jonathan Ferreira <44679989+Jonathansoufer@users.noreply.github.com> Date: Thu, 10 Oct 2024 19:14:55 +0100 Subject: [PATCH 16/46] feat: add references and utils for delete storage key (#11733) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- 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 implements a feature for our users get access to delete their Notification's storage keys if need. ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to Settings 2. Choose Notifications 3. Make sure its enabled 4. Click on Reset Notifications 5. Proceed 6. Check if Notifications remain enabled and a toast is shown. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** https://github.com/user-attachments/assets/07ed0870-a6f2-442c-a692-880037f03f8d <!-- [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** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] 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. --- app/actions/notification/constants/index.ts | 1 + app/actions/notification/helpers/index.ts | 17 +++++++++ .../ResetNotificationsModal.test.tsx.snap | 2 +- .../ResetNotificationsModal/index.tsx | 31 ++++++++++++---- .../NotificationsSettings.styles.ts | 1 + .../Settings/NotificationsSettings/index.tsx | 5 --- app/util/notifications/hooks/types.ts | 4 +- .../hooks/useNotifications.test.tsx | 37 +++++++++++++++++++ .../notifications/hooks/useNotifications.ts | 16 ++++---- locales/languages/en.json | 3 +- package.json | 2 +- yarn.lock | 8 ++-- 12 files changed, 97 insertions(+), 30 deletions(-) diff --git a/app/actions/notification/constants/index.ts b/app/actions/notification/constants/index.ts index c75c9eb00820..ee8c63fdffa7 100644 --- a/app/actions/notification/constants/index.ts +++ b/app/actions/notification/constants/index.ts @@ -19,6 +19,7 @@ export enum notificationsErrors { UPDATE_TRIGGER_PUSH_NOTIFICATIONS = 'Error while trying to update trigger push notifications', ENABLE_NOTIFICATIONS_SERVICES = 'Error while trying to enable notifications services', DISABLE_NOTIFICATIONS_SERVICES = 'Error while trying to disable notifications services', + DELETE_STORAGE_KEY = 'Error while trying to delete storage key', } export default notificationsErrors; diff --git a/app/actions/notification/helpers/index.ts b/app/actions/notification/helpers/index.ts index 0cd827ad3ef5..9f47da07f88e 100644 --- a/app/actions/notification/helpers/index.ts +++ b/app/actions/notification/helpers/index.ts @@ -171,3 +171,20 @@ export const markMetamaskNotificationsAsRead = async ( return getErrorMessage(error); } }; +/** + * Perform the deletion of the notifications storage key and the creation of on chain triggers to reset the notifications. + * + * @returns {Promise<string | undefined>} A promise that resolves to a string error message or undefined if successful. + */ +export const performDeleteStorage = async (): Promise<string | undefined> => { + try { + await Engine.context.UserStorageController.performDeleteStorage('notifications.notification_settings'); + await Engine.context.NotificationServicesController.createOnChainTriggers( + { + resetNotifications: true, + }, + ); + } catch (error) { + return getErrorMessage(error); + } +}; diff --git a/app/components/UI/Notification/ResetNotificationsModal/__snapshots__/ResetNotificationsModal.test.tsx.snap b/app/components/UI/Notification/ResetNotificationsModal/__snapshots__/ResetNotificationsModal.test.tsx.snap index a16e934ae957..ba1915f5934a 100644 --- a/app/components/UI/Notification/ResetNotificationsModal/__snapshots__/ResetNotificationsModal.test.tsx.snap +++ b/app/components/UI/Notification/ResetNotificationsModal/__snapshots__/ResetNotificationsModal.test.tsx.snap @@ -170,7 +170,7 @@ exports[`ProfileSyncingModal should render correctly 1`] = ` } } > - Resetting notifications, means you're overwritting your notifications storage keys and resetting all your notification history. Are you sure you want to do this? + Resetting notifications, means you're deleting your notifications storage keys and resetting all your notification history. Are you sure you want to do this? </Text> <View style={ diff --git a/app/components/UI/Notification/ResetNotificationsModal/index.tsx b/app/components/UI/Notification/ResetNotificationsModal/index.tsx index 611339d3ee7f..ff45a3da4b9a 100644 --- a/app/components/UI/Notification/ResetNotificationsModal/index.tsx +++ b/app/components/UI/Notification/ResetNotificationsModal/index.tsx @@ -1,5 +1,5 @@ // Third party dependencies. -import React, { useEffect, useRef } from 'react'; +import React, { useContext, useEffect, useRef } from 'react'; // External dependencies. import { useMetrics } from '../../../hooks/useMetrics'; @@ -14,23 +14,38 @@ import { IconSize, } from '../../../../component-library/components/Icons/Icon'; import { MetaMetricsEvents } from '../../../../core/Analytics'; -import { useResetNotificationsStorageKey } from '../../../../util/notifications/hooks/useNotifications'; +import { useDeleteNotificationsStorageKey } from '../../../../util/notifications/hooks/useNotifications'; import ModalContent from '../Modal'; - +import { ToastContext } from '../../../../component-library/components/Toast'; +import { ToastVariants } from '../../../../component-library/components/Toast/Toast.types'; const ResetNotificationsModal = () => { const { trackEvent } = useMetrics(); const bottomSheetRef = useRef<BottomSheetRef>(null); const [isChecked, setIsChecked] = React.useState(false); - const { resetNotificationsStorageKey, loading } = useResetNotificationsStorageKey(); - - + const { deleteNotificationsStorageKey, loading } = useDeleteNotificationsStorageKey(); + const { toastRef } = useContext(ToastContext); const closeBottomSheet = () => bottomSheetRef.current?.onCloseBottomSheet(); + const showResultToast = () => { + toastRef?.current?.showToast({ + variant: ToastVariants.Plain, + labelOptions: [ + { + label: strings('app_settings.reset_notifications_success'), + isBold: false, + }, + ], + hasNoTimeout: false, + }); + }; + const handleCta = async () => { - await resetNotificationsStorageKey(); + await deleteNotificationsStorageKey().then(() => { + showResultToast(); trackEvent(MetaMetricsEvents.NOTIFICATION_STORAGE_KEY_DELETED, { - settings_type: 'reset_notifications_storage_key', + settings_type: 'delete_notifications_storage_key', }); + }); }; const prevLoading = useRef(loading); diff --git a/app/components/Views/Settings/NotificationsSettings/NotificationsSettings.styles.ts b/app/components/Views/Settings/NotificationsSettings/NotificationsSettings.styles.ts index 878ccfa89145..6fbdbb9dd873 100644 --- a/app/components/Views/Settings/NotificationsSettings/NotificationsSettings.styles.ts +++ b/app/components/Views/Settings/NotificationsSettings/NotificationsSettings.styles.ts @@ -60,6 +60,7 @@ const styleSheet = (params: { theme: Theme }) => }, button: { alignSelf: 'stretch', + marginBottom: 16, }, }); diff --git a/app/components/Views/Settings/NotificationsSettings/index.tsx b/app/components/Views/Settings/NotificationsSettings/index.tsx index 9671e5d7b456..34e9da180471 100644 --- a/app/components/Views/Settings/NotificationsSettings/index.tsx +++ b/app/components/Views/Settings/NotificationsSettings/index.tsx @@ -193,11 +193,6 @@ const NotificationsSettings = ({ navigation, route }: Props) => { if (permission !== 'authorized') { return; } - - /** - * Although this is an async function, we are dispatching an action (firing & forget) - * to emulate optimistic UI. - */ enableNotifications(); setUiNotificationStatus(true); } diff --git a/app/util/notifications/hooks/types.ts b/app/util/notifications/hooks/types.ts index df8d2702285f..d2b49c8768b9 100644 --- a/app/util/notifications/hooks/types.ts +++ b/app/util/notifications/hooks/types.ts @@ -47,8 +47,8 @@ export interface DisableNotificationsReturn { error?: string; } -export interface ResetNotificationsStorageKeyReturn { - resetNotificationsStorageKey: () => Promise<string | undefined>; +export interface deleteNotificationsStorageKeyReturn { + deleteNotificationsStorageKey: () => Promise<string | undefined>; loading: boolean; error?: string; } diff --git a/app/util/notifications/hooks/useNotifications.test.tsx b/app/util/notifications/hooks/useNotifications.test.tsx index 95a7cd41a148..3e6c6581bf34 100644 --- a/app/util/notifications/hooks/useNotifications.test.tsx +++ b/app/util/notifications/hooks/useNotifications.test.tsx @@ -11,6 +11,7 @@ import { useEnableNotifications, useDisableNotifications, useMarkNotificationAsRead, + useDeleteNotificationsStorageKey, } from './useNotifications'; import { TRIGGER_TYPES } from '../constants'; import createMockStore from 'redux-mock-store'; @@ -272,3 +273,39 @@ describe('useMarkNotificationAsRead', () => { ]); }); }); + +describe('useDeleteNotificationsStorageKey', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + function arrangeActions() { + const deleteNotificationsStorageKey = jest + .spyOn(Actions, 'performDeleteStorage') + .mockResolvedValue(undefined); + + return { + deleteNotificationsStorageKey, + }; + } + + function arrangeHook() { + const store = arrangeStore(); + const hook = renderHook(() => useDeleteNotificationsStorageKey(), { + wrapper: ({ children }) => <Provider store={store}>{children}</Provider>, + }); + + return hook; + } + + it('deletes notifications storage key', async () => { + const mockActions = arrangeActions(); + const { result } = arrangeHook(); + + await act(async () => { + await result.current.deleteNotificationsStorageKey(); + }); + + expect(mockActions.deleteNotificationsStorageKey).toHaveBeenCalledTimes(1); + }); +}); diff --git a/app/util/notifications/hooks/useNotifications.ts b/app/util/notifications/hooks/useNotifications.ts index b3c21420bcaf..c5b101784886 100644 --- a/app/util/notifications/hooks/useNotifications.ts +++ b/app/util/notifications/hooks/useNotifications.ts @@ -7,12 +7,12 @@ import { EnableNotificationsReturn, DisableNotificationsReturn, MarkNotificationAsReadReturn, - ResetNotificationsStorageKeyReturn, + deleteNotificationsStorageKeyReturn, } from './types'; import { getErrorMessage } from '../../../util/errorHandling'; import { MarkAsReadNotificationsParam, - createOnChainTriggersByAccount, + performDeleteStorage, disableNotificationServices, enableNotificationServices, fetchAndUpdateMetamaskNotifications, @@ -206,20 +206,20 @@ export function useMarkNotificationAsRead(): MarkNotificationAsReadReturn { } /** - * Custom hook to enable notifications by creating on-chain triggers. + * Custom hook to delete notifications storage key. * It manages loading and error states internally. * - * @returns An object containing the `enableNotifications` function, loading state, and error state. + * @returns An object containing the `deleteNotificationsStorageKey` function, loading state, and error state. */ -export function useResetNotificationsStorageKey(): ResetNotificationsStorageKeyReturn { +export function useDeleteNotificationsStorageKey(): deleteNotificationsStorageKeyReturn { const [loading, setLoading] = useState<boolean>(false); const [error, setError] = useState<string>(); - const resetNotificationsStorageKey = useCallback(async () => { + const deleteNotificationsStorageKey = useCallback(async () => { setLoading(true); setError(undefined); try { - const errorMessage = await createOnChainTriggersByAccount(true); + const errorMessage = await performDeleteStorage(); if (errorMessage) { setError(getErrorMessage(errorMessage)); return errorMessage; @@ -234,7 +234,7 @@ export function useResetNotificationsStorageKey(): ResetNotificationsStorageKeyR }, []); return { - resetNotificationsStorageKey, + deleteNotificationsStorageKey, loading, error, }; diff --git a/locales/languages/en.json b/locales/languages/en.json index 19ae9bc3ee5d..7bb7eb9a5776 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -770,8 +770,9 @@ "enabling_notifications": "Enabling notifications...", "disabling_notifications": "Disabling notifications...", "reset_notifications_title": "Reset notifications", - "reset_notifications_description": "Resetting notifications, means you're overwritting your notifications storage keys and resetting all your notification history. Are you sure you want to do this?", + "reset_notifications_description": "Resetting notifications, means you're deleting your notifications storage keys and resetting all your notification history. Are you sure you want to do this?", "reset_notifications": "Reset notifications", + "reset_notifications_success": "Notifications storage key deleted/recreated, and notifications history reset.", "enabling_profile_sync": "Enabling profile syncing...", "disabling_profile_sync": "Disabling profile syncing...", "notifications_dismiss_modal": "Dismiss", diff --git a/package.json b/package.json index 0dca731ef3d3..e3b583dc2c10 100644 --- a/package.json +++ b/package.json @@ -169,7 +169,7 @@ "@metamask/post-message-stream": "^8.0.0", "@metamask/ppom-validator": "0.32.0", "@metamask/preferences-controller": "^11.0.0", - "@metamask/profile-sync-controller": "^0.9.5", + "@metamask/profile-sync-controller": "^0.9.7", "@metamask/react-native-actionsheet": "2.4.2", "@metamask/react-native-button": "^3.0.0", "@metamask/react-native-payments": "^2.0.0", diff --git a/yarn.lock b/yarn.lock index 29605d091316..4b65dfd7f171 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5247,10 +5247,10 @@ "@metamask/base-controller" "^5.0.2" "@metamask/controller-utils" "^10.0.0" -"@metamask/profile-sync-controller@^0.9.5": - version "0.9.5" - resolved "https://registry.yarnpkg.com/@metamask/profile-sync-controller/-/profile-sync-controller-0.9.5.tgz#b236153a59e43c0656f4fb2b76cb495dab38814b" - integrity sha512-3eMUe8rf1fFlrOzHiL4Vt6QQMpZVFsfPuFssJ6eIfMqUFoRVxbHSq1Ar8T9v2uKteYO7oTHm4JtnsuOGQxGvEg== +"@metamask/profile-sync-controller@^0.9.7": + version "0.9.7" + resolved "https://registry.yarnpkg.com/@metamask/profile-sync-controller/-/profile-sync-controller-0.9.7.tgz#d5e78cb8004f0dcb8637410bb8b54911e8f2c0a7" + integrity sha512-1R4P1/9VdGEHGPb68gc2oNM9/95xc84hNqIlZDL/OISSRgvW3wJguXwEVLfW6GE91gHmzHtMe4MxDM3pXQWc9w== dependencies: "@metamask/base-controller" "^7.0.1" "@metamask/keyring-api" "^8.1.3" From 562e5edd0bd635cbe1fac923596f7e618ea7fd20 Mon Sep 17 00:00:00 2001 From: Mpendulo Ndlovu <mpendulo@elefantel.com> Date: Thu, 10 Oct 2024 20:17:57 +0200 Subject: [PATCH 17/46] fix: connect request completed source validation (#11701) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- 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** The `Connect Request Completed` analytics event had a wrong source `walletconnect` when a dapp was connecting to the wallet from the in-app browser. This PR fixes source the validation. ## **Related issues** Fixes: [SDK-81](https://consensyssoftware.atlassian.net/browse/SDK-81) ## **Manual testing steps** 1. Connect from a dapp within the in-app browser 2. Observe Request Completed` analytics event being logged ## **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 - [ ] I’ve included tests if applicable - [ ] 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. [SDK-81]: https://consensyssoftware.atlassian.net/browse/SDK-81?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- .../Views/AccountConnect/AccountConnect.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/app/components/Views/AccountConnect/AccountConnect.tsx b/app/components/Views/AccountConnect/AccountConnect.tsx index a2a348c4dd92..5ed77fe0df56 100644 --- a/app/components/Views/AccountConnect/AccountConnect.tsx +++ b/app/components/Views/AccountConnect/AccountConnect.tsx @@ -138,6 +138,7 @@ const AccountConnect = (props: AccountConnectProps) => { const sdkConnection = SDKConnect.getInstance().getConnection({ channelId: channelIdOrHostname, }); + const isOriginMMSDKRemoteConn = sdkConnection !== undefined; const dappIconUrl = sdkConnection?.originatorInfo?.icon; @@ -174,9 +175,10 @@ const AccountConnect = (props: AccountConnectProps) => { channelIdOrHostname, ]); - const urlWithProtocol = (hostname && !isUUID(hostname)) - ? prefixUrlWithProtocol(hostname) - : domainTitle; + const urlWithProtocol = + hostname && !isUUID(hostname) + ? prefixUrlWithProtocol(hostname) + : domainTitle; const isAllowedOrigin = useCallback((origin: string) => { const { PhishingController } = Engine.context; @@ -236,15 +238,16 @@ const AccountConnect = (props: AccountConnectProps) => { // walletconnect channelId format: app.name.org // sdk channelId format: uuid // inappbrowser channelId format: app.name.org but origin is set - if (channelIdOrHostname) { - if (sdkConnection) { - return SourceType.SDK; - } + if (isOriginWalletConnect) { return SourceType.WALLET_CONNECT; } + if (sdkConnection) { + return SourceType.SDK; + } + return SourceType.IN_APP_BROWSER; - }, [sdkConnection, channelIdOrHostname]); + }, [isOriginWalletConnect, sdkConnection]); // Refreshes selected addresses based on the addition and removal of accounts. useEffect(() => { From df6255ab9ad1e359a2e27561273f2cd1a56fe838 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 10 Oct 2024 20:15:26 +0000 Subject: [PATCH 18/46] chore(js-ts): Convert app/components/UI/StyledButton/styledButtonStyles.js to TypeScript (#11525) This PR updates the TypeScript conversion for the StyledButton component. It removes type annotations for fontStyle and containerStyle, ensuring alignment with the latest requirements. --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Kylan Hurt <kylanhurt@users.noreply.github.com> --- .../{styledButtonStyles.js => styledButtonStyles.ts} | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) rename app/components/UI/StyledButton/{styledButtonStyles.js => styledButtonStyles.ts} (97%) diff --git a/app/components/UI/StyledButton/styledButtonStyles.js b/app/components/UI/StyledButton/styledButtonStyles.ts similarity index 97% rename from app/components/UI/StyledButton/styledButtonStyles.js rename to app/components/UI/StyledButton/styledButtonStyles.ts index b2453c147f43..bf55b8bee8e3 100644 --- a/app/components/UI/StyledButton/styledButtonStyles.js +++ b/app/components/UI/StyledButton/styledButtonStyles.ts @@ -1,7 +1,8 @@ import { StyleSheet } from 'react-native'; import { colors as importedColors, fontStyles } from '../../../styles/common'; +import { Theme } from '@metamask/design-tokens'; -const createStyles = (colors) => +const createStyles = (colors: Theme['colors']) => StyleSheet.create({ container: { padding: 15, @@ -138,7 +139,7 @@ const createStyles = (colors) => }, }); -function getStyles(type, colors) { +function getStyles(type: string, colors: Theme['colors']) { const styles = createStyles(colors); let fontStyle, containerStyle; From 22c83cd7dc7702baa2b638b7d8355489611f0454 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 10 Oct 2024 20:17:08 +0000 Subject: [PATCH 19/46] chore(js-ts): Convert app/components/UI/Fox/index.js to TypeScript (#11556) # PR Title chore(js-ts): Convert app/components/UI/Fox/index.js to TypeScript # Description This PR converts the `app/components/UI/Fox/index.js` file to TypeScript. The conversion involved renaming the file to `index.tsx`, creating a `FoxProps` interface for the component props, and updating the `forwardedRef` type for compatibility with `WebView`. Additionally, the `Theme` import was adjusted to ensure correct usage of theme colors. # Related Issues - N/A # Manual Testing Steps 1. Verify that the `Fox` component renders correctly in the application. 2. Ensure that the component's functionality remains unchanged after the conversion. 3. Check for any TypeScript errors or warnings in the console. # Checklist - [x] Converted the file to TypeScript. - [x] Created a `FoxProps` interface for component props. - [x] Updated the `forwardedRef` type for compatibility with `WebView`. - [x] Adjusted the `Theme` import for correct usage of theme colors. - [x] Verified that the component renders correctly and functions as expected. - [x] Ensured no TypeScript errors or warnings are present. [This Devin run](https://preview.devin.ai/devin/99a5c8fdc33c4d4ca62c8e1cdecb7d09) was requested by naveen. If you have any feedback, you can leave comments in the PR and I'll address them in the app! --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Kylan Hurt <kylanhurt@users.noreply.github.com> --- app/components/UI/Fox/{index.js => index.tsx} | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) rename app/components/UI/Fox/{index.js => index.tsx} (98%) diff --git a/app/components/UI/Fox/index.js b/app/components/UI/Fox/index.tsx similarity index 98% rename from app/components/UI/Fox/index.js rename to app/components/UI/Fox/index.tsx index 8767c05f41f6..1f08a60e74f1 100644 --- a/app/components/UI/Fox/index.js +++ b/app/components/UI/Fox/index.tsx @@ -1,15 +1,15 @@ import React, { forwardRef } from 'react'; -import PropTypes from 'prop-types'; -import { StyleSheet } from 'react-native'; -import { WebView } from '@metamask/react-native-webview'; +import { StyleProp, StyleSheet, ViewStyle } from 'react-native'; +import { WebView, WebViewProps } from '@metamask/react-native-webview'; import { useTheme } from '../../../util/theme'; import Animated, { useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated'; +import { Theme } from '@metamask/design-tokens'; -const createStyles = (colors) => +const createStyles = (colors: Theme['colors']) => StyleSheet.create({ webView: { flex: 1, @@ -17,13 +17,20 @@ const createStyles = (colors) => }, }); +interface FoxProps extends Omit<WebViewProps, 'ref'> { + style?: StyleProp<ViewStyle>; + customStyle?: string; + customContent?: string; + forwardedRef?: React.Ref<WebView>; +} + function Fox({ style, customStyle, customContent = '', forwardedRef, ...props -}) { +}: FoxProps) { const { colors } = useTheme(); const styles = createStyles(colors); const opacityControl = useSharedValue(0); @@ -1543,16 +1550,10 @@ function Fox({ ); } -Fox.propTypes = { - style: PropTypes.object, - customStyle: PropTypes.string, - customContent: PropTypes.string, - forwardedRef: PropTypes.any, -}; +const FoxWithRef = forwardRef<WebView, Omit<FoxProps, 'forwardedRef'>>( + (props, ref) => <Fox {...props} forwardedRef={ref} />, +); -const FoxWithRef = forwardRef((props, ref) => ( - <Fox {...props} forwardedRef={ref} /> -)); FoxWithRef.displayName = 'FoxWithRef'; export default FoxWithRef; From 5e1eaf7cb3eb5a2496e9be3b3f6ce7f47c43e259 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 10 Oct 2024 20:39:00 +0000 Subject: [PATCH 20/46] chore(js-ts): Convert app/util/test/ganache-contract-address-registry.js to TypeScript (#11406) Converted the file to TypeScript and removed return types from methods. [This Devin run](https://preview.devin.ai/devin/6894e270d7384f76931ae901e99f132e) was requested by Jeffrey. If you have any feedback, you can leave comments in the PR and I'll address them in the app! --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Kylan Hurt <kylanhurt@users.noreply.github.com> Co-authored-by: Jonathan Ferreira <44679989+Jonathansoufer@users.noreply.github.com> Co-authored-by: Cal Leung <cal.leung@consensys.net> Co-authored-by: EtherWizard33 <165834542+EtherWizard33@users.noreply.github.com> --- ...ess-registry.js => ganache-contract-address-registry.ts} | 6 +++--- locales/languages/en.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename app/util/test/{ganache-contract-address-registry.js => ganache-contract-address-registry.ts} (77%) diff --git a/app/util/test/ganache-contract-address-registry.js b/app/util/test/ganache-contract-address-registry.ts similarity index 77% rename from app/util/test/ganache-contract-address-registry.js rename to app/util/test/ganache-contract-address-registry.ts index 5659401a2361..517f5c0e6138 100644 --- a/app/util/test/ganache-contract-address-registry.js +++ b/app/util/test/ganache-contract-address-registry.ts @@ -3,7 +3,7 @@ * a local blockchain instance ran by Ganache. */ class GanacheContractAddressRegistry { - #addresses = {}; + #addresses: Record<string, string> = {}; /** * Store new contract address in key:value pair. @@ -11,7 +11,7 @@ class GanacheContractAddressRegistry { * @param contractName * @param contractAddress */ - storeNewContractAddress(contractName, contractAddress) { + storeNewContractAddress(contractName: string, contractAddress: string) { this.#addresses[contractName] = contractAddress; } @@ -20,7 +20,7 @@ class GanacheContractAddressRegistry { * * @param contractName */ - getContractAddress(contractName) { + getContractAddress(contractName: string) { return this.#addresses[contractName]; } } diff --git a/locales/languages/en.json b/locales/languages/en.json index 7bb7eb9a5776..b439f1cd1d43 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -3429,4 +3429,4 @@ "network": "Network", "rpc_url": "RPC URL" } -} +} \ No newline at end of file From efba2535d1acb1bada912bfed8c150e8b5b10147 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 10 Oct 2024 20:44:06 +0000 Subject: [PATCH 21/46] chore(js-ts): Convert app/components/UI/Swaps/components/InfoModal.js to TypeScript (#11650) This PR converts the InfoModal.js file to TypeScript (InfoModal.tsx) in the Swaps component and deletes the original JavaScript file. Changes made: - Converted InfoModal.js to InfoModal.tsx - Added TypeScript types for props and other variables - Updated imports and component structure to follow TypeScript conventions - Deleted the original InfoModal.js file Link to Devin run: https://preview.devin.ai/devin/d66629e78f134b0daa3e306ab4a2b05e This PR is a re-creation of the previously closed PR #11597 with the same changes, focusing only on the InfoModal.tsx file as requested. If you have any feedback, you can leave comments in the PR and I'll address them in the app! --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../{InfoModal.js => InfoModal.tsx} | 114 +++++++++++------- 1 file changed, 70 insertions(+), 44 deletions(-) rename app/components/UI/Swaps/components/{InfoModal.js => InfoModal.tsx} (56%) diff --git a/app/components/UI/Swaps/components/InfoModal.js b/app/components/UI/Swaps/components/InfoModal.tsx similarity index 56% rename from app/components/UI/Swaps/components/InfoModal.js rename to app/components/UI/Swaps/components/InfoModal.tsx index be78a8c6c756..7edd24ccb487 100644 --- a/app/components/UI/Swaps/components/InfoModal.js +++ b/app/components/UI/Swaps/components/InfoModal.tsx @@ -1,13 +1,13 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { StyleSheet, View, TouchableOpacity, SafeAreaView } from 'react-native'; import Modal from 'react-native-modal'; import IonicIcon from 'react-native-vector-icons/Ionicons'; import Text from '../../../Base/Text'; import Title from '../../../Base/Title'; import { useTheme } from '../../../../util/theme'; +import { Theme } from '@metamask/design-tokens'; -const createStyles = (colors, shadows) => +const createStyles = (colors: Theme['colors'], shadows: Theme['shadows']) => StyleSheet.create({ modalView: { backgroundColor: colors.background.default, @@ -51,6 +51,66 @@ const createStyles = (colors, shadows) => }, }); +interface InfoModalProps { + isVisible: boolean | undefined; + title?: React.ReactNode; + body?: React.ReactNode; + toggleModal: () => void; + propagateSwipe?: boolean; + message?: string; + urlText?: string; + url?: () => void; + testID?: string; +} + +interface CloseButtonProps { + onPress: () => void; + style: { + closeIcon: object; + }; +} + +const CloseButton: React.FC<CloseButtonProps> = ({ onPress, style }) => ( + <TouchableOpacity + onPress={onPress} + hitSlop={{ top: 20, left: 20, right: 20, bottom: 20 }} + > + <IonicIcon name="ios-close" style={style.closeIcon} size={30} /> + </TouchableOpacity> +); + +interface InfoViewProps { + message?: string; + urlText?: string; + url?: () => void; + onClose: () => void; + style: { + infoContainer: object; + messageLimit: object; + closeIcon: object; + }; +} + +const InfoView: React.FC<InfoViewProps> = ({ message, urlText, url, onClose, style }) => { + if (!message) { + return <CloseButton onPress={onClose} style={style} />; + } + + return ( + <View style={style.infoContainer}> + <Text style={style.messageLimit}> + <Text>{message} </Text> + {urlText && ( + <Text link onPress={url}> + {urlText} + </Text> + )} + </Text> + <CloseButton onPress={onClose} style={style} /> + </View> + ); +}; + function InfoModal({ title, body, @@ -61,39 +121,10 @@ function InfoModal({ urlText, url, testID, -}) { +}: InfoModalProps) { const { colors, shadows } = useTheme(); const styles = createStyles(colors, shadows); - const CloseButton = () => ( - <TouchableOpacity - onPress={toggleModal} - hitSlop={{ top: 20, left: 20, right: 20, bottom: 20 }} - > - <IonicIcon name="ios-close" style={styles.closeIcon} size={30} /> - </TouchableOpacity> - ); - - const InfoView = () => { - if (!message) { - return <CloseButton />; - } - - return ( - <View style={styles.infoContainer}> - <Text style={styles.messageLimit}> - <Text>{message} </Text> - {urlText && ( - <Text link onPress={url}> - {urlText} - </Text> - )} - </Text> - <CloseButton /> - </View> - ); - }; - return ( <Modal isVisible={isVisible} @@ -110,23 +141,18 @@ function InfoModal({ <SafeAreaView style={styles.modalView}> <View style={styles.title}> {title && <Title>{title}} - + {body && {body}} ); } -InfoModal.propTypes = { - isVisible: PropTypes.bool, - title: PropTypes.node, - body: PropTypes.node, - toggleModal: PropTypes.func, - propagateSwipe: PropTypes.bool, - message: PropTypes.string, - urlText: PropTypes.string, - url: PropTypes.func, - testID: PropTypes.string, -}; export default InfoModal; From a35d7bd1221140bc3407988a4dd4a664bdfb64c9 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 10 Oct 2024 21:23:06 +0000 Subject: [PATCH 22/46] chore(js-ts): Convert app/components/Base/HorizontalSelector/index.js to TypeScript (#11661) ## Description This PR converts the HorizontalSelector component from JavaScript to TypeScript. ## Motivation and Context This change is part of the ongoing effort to migrate the codebase to TypeScript, improving type safety and developer experience. ## Changes - Renamed index.js to index.tsx - Added TypeScript interfaces for component props and styles - Removed PropTypes and replaced with TypeScript types - Updated imports to include necessary TypeScript types - Refactored createStyles function for better type inference ## Testing Instructions 1. Run `yarn lint:tsc` to ensure no TypeScript errors 2. Test the HorizontalSelector component in various scenarios to ensure functionality remains unchanged ## Screenshots/Recordings N/A ## Types of changes - [ ] Bugfix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [x] Breaking change (fix or feature that would cause existing functionality to change) ## Checklist - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes ## Link to Devin run https://preview.devin.ai/devin/8bb5194eef49468e8d7c4fea51faaa3b If you have any feedback, you can leave comments in the PR and I'll address them in the app! --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../{index.js => index.tsx} | 160 ++++++++++-------- 1 file changed, 85 insertions(+), 75 deletions(-) rename app/components/Base/HorizontalSelector/{index.js => index.tsx} (73%) diff --git a/app/components/Base/HorizontalSelector/index.js b/app/components/Base/HorizontalSelector/index.tsx similarity index 73% rename from app/components/Base/HorizontalSelector/index.js rename to app/components/Base/HorizontalSelector/index.tsx index aa90cd87e945..e06490540ba3 100644 --- a/app/components/Base/HorizontalSelector/index.js +++ b/app/components/Base/HorizontalSelector/index.tsx @@ -1,12 +1,34 @@ -import React, { Fragment, useCallback, useMemo } from 'react'; -import PropTypes from 'prop-types'; +import React, { Fragment, ReactNode, useCallback, useMemo } from 'react'; import { View, StyleSheet, TouchableOpacity } from 'react-native'; import Text from '../Text'; import { useTheme } from '../../../util/theme'; +import { Theme } from '@metamask/design-tokens'; const INNER_CIRCLE_SCALE = 0.445; const OPTION_WIDTH = 110; -const createStyles = (colors) => + +const createCircleStyle = (size: number, colors: Theme['colors']) => ({ + width: size, + height: size, + flexShrink: 0, + flexGrow: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + borderWidth: 2, + borderRadius: 9999, + borderColor: colors.border.muted, +}); + +const createInnerCircleStyle = (size: number, colors: Theme['colors']) => ({ + width: size * INNER_CIRCLE_SCALE, + height: size * INNER_CIRCLE_SCALE, + flexShrink: 0, + flexGrow: 0, + backgroundColor: colors.primary.default, + borderRadius: 999, +}); +const createStyles = (colors: Theme['colors']) => StyleSheet.create({ selector: { display: 'flex', @@ -27,18 +49,6 @@ const createStyles = (colors) => flex: 0, flexDirection: 'column', }, - circle: (size) => ({ - width: size, - height: size, - flexShrink: 0, - flexGrow: 0, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - borderWidth: 2, - borderRadius: 9999, - borderColor: colors.border.muted, - }), circleSelected: { borderColor: colors.primary.default, }, @@ -48,14 +58,6 @@ const createStyles = (colors) => circleDisabled: { opacity: 0.4, }, - innerCircle: (size) => ({ - width: size * INNER_CIRCLE_SCALE, - height: size * INNER_CIRCLE_SCALE, - flexShrink: 0, - flexGrow: 0, - backgroundColor: colors.primary.default, - borderRadius: 999, - }), innerCircleError: { backgroundColor: colors.error.default, }, @@ -102,14 +104,14 @@ const createStyles = (colors) => }, }); -function Circle({ size = 22, selected, disabled, error }) { +function Circle({ size = 22, selected, disabled, error }: CircleProps) { const { colors } = useTheme(); const styles = createStyles(colors); return ( ); } -Circle.propTypes = { - size: PropTypes.number, - selected: PropTypes.bool, - disabled: PropTypes.bool, - error: PropTypes.bool, -}; -function Option({ onPress, name, ...props }) { +function Option({ onPress, name, ...props }: OptionProps) { const { colors } = useTheme(); const styles = createStyles(colors); const handlePress = useCallback(() => onPress(name), [name, onPress]); @@ -146,11 +142,6 @@ function Option({ onPress, name, ...props }) { ); } -Option.propTypes = { - onPress: PropTypes.func, - name: PropTypes.string, -}; - function HorizontalSelector({ options = [], selected, @@ -158,7 +149,7 @@ function HorizontalSelector({ onPress, disabled, ...props -}) { +}: HorizontalSelectorProps) { const { colors } = useTheme(); const styles = createStyles(colors); const hasTopLabels = useMemo( @@ -265,51 +256,70 @@ function HorizontalSelector({ ); } -HorizontalSelector.propTypes = { +interface CircleProps { /** - * Array of options + * Size of the option circle */ - options: PropTypes.arrayOf( - PropTypes.shape({ - /** - * Label of the option. It can be a string, component or a render - * function, which will be called with arguments (selected, disabled). - */ - label: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), - /** - * Top label of the option. It can be a string, component or a render function. - */ - topLabel: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), - /** - * Option name string, this is used as argument when calling the onPress function. - */ - name: PropTypes.string, - /** - * Boolean value to determine whether if option is disabled or not. - */ - disabled: PropTypes.bool, - /** - * Boolean value to determine if the option should represent an error - */ - error: PropTypes.bool, - }), - ), + size?: number; /** - * Boolean value to determine whether the options are disabked or not. + * Current option name selected */ - disabled: PropTypes.bool, + selected?: boolean; + disabled?: boolean; + error?: boolean; +} + +interface OptionProps { + onPress: (name: string) => void; + name: string; + [key: string]: unknown; +} + +interface OptionType { /** - * Function that is called when pressing an option. The function is called with option.name argument. + * Label of the option. It can be a string, component or a render + * function, which will be called with arguments (selected, disabled). */ - onPress: PropTypes.func, + label: + | string + | ReactNode + | ((selected: boolean, disabled: boolean) => ReactNode); /** - * Size of the option circle + * Top label of the option. It can be a string, component or a render function. */ - circleSize: PropTypes.number, + topLabel?: + | string + | ReactNode + | ((selected: boolean, disabled: boolean) => ReactNode); /** - * Current option name selected + * Option name string, this is used as argument when calling the onPress function. */ - selected: PropTypes.string, -}; + name: string; + /** + * Boolean value to determine whether if option is disabled or not. + */ + disabled?: boolean; + /** + * Boolean value to determine if the option should represent an error + */ + error?: boolean; +} + +interface HorizontalSelectorProps { + /** + * Array of options + */ + options: OptionType[]; + selected?: string; + circleSize?: number; + /** + * Function that is called when pressing an option. The function is called with option.name argument. + */ + onPress: (name: string) => void; + /** + * Boolean value to determine whether the options are disabled or not. + */ + disabled: boolean; +} export default HorizontalSelector; From 31735872324c222e1ed723ccba7d343dd5247d58 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 10 Oct 2024 16:27:24 -0500 Subject: [PATCH 23/46] chore(js-ts): Convert app/util/transaction-reducer-helpers.js to TypeScript (#11629) This PR converts `app/util/transaction-reducer-helpers.js` to TypeScript. All types have been updated accordingly. The `notes.md` file has been removed as per request. --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- ...pers.js => transaction-reducer-helpers.ts} | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) rename app/util/{transaction-reducer-helpers.js => transaction-reducer-helpers.ts} (64%) diff --git a/app/util/transaction-reducer-helpers.js b/app/util/transaction-reducer-helpers.ts similarity index 64% rename from app/util/transaction-reducer-helpers.js rename to app/util/transaction-reducer-helpers.ts index d4e9990ed46c..562fa6db9a7a 100644 --- a/app/util/transaction-reducer-helpers.js +++ b/app/util/transaction-reducer-helpers.ts @@ -1,7 +1,22 @@ -function getDefinedProperties(object) { +import { SecurityAlertResponse } from '@metamask/transaction-controller'; +import { BN } from 'ethereumjs-util'; + +interface TxMeta { + data?: string; + from?: string; + gas?: BN; + gasPrice?: BN; + to?: string; + value?: BN; + maxFeePerGas?: BN; + maxPriorityFeePerGas?: BN; + securityAlertResponse?: SecurityAlertResponse; +} + +function getDefinedProperties(object: T): Partial { return Object.entries(object).reduce( (obj, [key, val]) => (val !== undefined ? { ...obj, [key]: val } : obj), - {}, + {} as Partial, ); } @@ -11,7 +26,7 @@ function getDefinedProperties(object) { * @param {object} txMeta - An object containing data about a transaction * @returns {object} - An object containing the standard properties of a transaction */ -export function getTxData(txMeta = {}) { +export function getTxData(txMeta: TxMeta = {}): Partial { const { data, from, @@ -22,8 +37,8 @@ export function getTxData(txMeta = {}) { maxFeePerGas, maxPriorityFeePerGas, securityAlertResponse, - } = txMeta; // eslint-disable-line no-unused-vars - const txData = { + } = txMeta; + const txData: Partial = { data, from, gas, @@ -43,7 +58,7 @@ export function getTxData(txMeta = {}) { * @param {object} txMeta - An object containing data about a transaction * @returns {object} - An object containing the standard properties of a transaction */ -export function getTxMeta(txMeta = {}) { +export function getTxMeta(txMeta: TxMeta = {}): Partial { const { data, from, @@ -54,6 +69,6 @@ export function getTxMeta(txMeta = {}) { maxFeePerGas, maxPriorityFeePerGas, ...rest - } = txMeta; // eslint-disable-line no-unused-vars + } = txMeta; return getDefinedProperties(rest); } From f6d0a1a48965a51360d22475a24ed22768cacfb6 Mon Sep 17 00:00:00 2001 From: Kylan Hurt Date: Thu, 10 Oct 2024 17:09:09 -0500 Subject: [PATCH 24/46] chore: Revert "chore(js-ts): Convert app/util/test/ganache-contract-address-registry.js to TypeScript" (#11746) Reverts MetaMask/metamask-mobile#11406 --- ...ess-registry.ts => ganache-contract-address-registry.js} | 6 +++--- locales/languages/en.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename app/util/test/{ganache-contract-address-registry.ts => ganache-contract-address-registry.js} (77%) diff --git a/app/util/test/ganache-contract-address-registry.ts b/app/util/test/ganache-contract-address-registry.js similarity index 77% rename from app/util/test/ganache-contract-address-registry.ts rename to app/util/test/ganache-contract-address-registry.js index 517f5c0e6138..5659401a2361 100644 --- a/app/util/test/ganache-contract-address-registry.ts +++ b/app/util/test/ganache-contract-address-registry.js @@ -3,7 +3,7 @@ * a local blockchain instance ran by Ganache. */ class GanacheContractAddressRegistry { - #addresses: Record = {}; + #addresses = {}; /** * Store new contract address in key:value pair. @@ -11,7 +11,7 @@ class GanacheContractAddressRegistry { * @param contractName * @param contractAddress */ - storeNewContractAddress(contractName: string, contractAddress: string) { + storeNewContractAddress(contractName, contractAddress) { this.#addresses[contractName] = contractAddress; } @@ -20,7 +20,7 @@ class GanacheContractAddressRegistry { * * @param contractName */ - getContractAddress(contractName: string) { + getContractAddress(contractName) { return this.#addresses[contractName]; } } diff --git a/locales/languages/en.json b/locales/languages/en.json index b439f1cd1d43..7bb7eb9a5776 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -3429,4 +3429,4 @@ "network": "Network", "rpc_url": "RPC URL" } -} \ No newline at end of file +} From 092d567f2cfd5640e933d42e61c9eb2c03e0e9ca Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 10 Oct 2024 23:28:43 +0000 Subject: [PATCH 25/46] chore(js-ts): Convert app/components/Views/AndroidBackHandler/index.js to TypeScript (#11546) chore(js-ts): Convert app/components/Views/AndroidBackHandler/index.js to TypeScript ## Description This PR converts the `AndroidBackHandler` component from JavaScript to TypeScript. The `navigation` prop is now typed using `NavigationProp`, and `propTypes` have been removed. ## Labels - needs-dev-review - team-mobile-platform - skip-sonar-cloud - Run Smoke E2E [This Devin run](https://preview.devin.ai/devin/fe052a3553114a6dbeec9dc4aecd3384) was requested by naveen. If you have any feedback, you can leave comments in the PR and I'll address them in the app! --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Kylan Hurt --- .../{index.js => index.tsx} | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) rename app/components/Views/AndroidBackHandler/{index.js => index.tsx} (67%) diff --git a/app/components/Views/AndroidBackHandler/index.js b/app/components/Views/AndroidBackHandler/index.tsx similarity index 67% rename from app/components/Views/AndroidBackHandler/index.js rename to app/components/Views/AndroidBackHandler/index.tsx index 74c4e8651b38..aa680d45c7a1 100644 --- a/app/components/Views/AndroidBackHandler/index.js +++ b/app/components/Views/AndroidBackHandler/index.tsx @@ -1,22 +1,22 @@ +import { NavigationProp, ParamListBase } from '@react-navigation/native'; import { PureComponent } from 'react'; -import PropTypes from 'prop-types'; import { BackHandler, InteractionManager } from 'react-native'; +interface AndroidBackHandlerProps { + /** + * react-navigation object used to switch between screens + */ + navigation?: NavigationProp; + /** + * Custom callback to call on back press event + */ + customBackPress: () => void; +} + /** * PureComponent that handles android hardware back button */ -export default class AndroidBackHandler extends PureComponent { - static propTypes = { - /** - * react-navigation object used to switch between screens - */ - navigation: PropTypes.object, - /** - * Custom callback to call on back press event - */ - customBackPress: PropTypes.func, - }; - +export default class AndroidBackHandler extends PureComponent { pressed = false; componentDidMount() { @@ -46,6 +46,7 @@ export default class AndroidBackHandler extends PureComponent { } setTimeout(() => (this.pressed = false), 300); } + return undefined; }; render() { From 37747293f215384984b25694a8dff8ba839d0bd1 Mon Sep 17 00:00:00 2001 From: Vinicius Stevam <45455812+vinistevam@users.noreply.github.com> Date: Fri, 11 Oct 2024 12:46:56 +0100 Subject: [PATCH 26/46] fix: Use domain for origin pill component (#11730) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR fixes an issue where the full URL is displayed in the origin pill for transaction requests triggered from a dapp within the in-app browser, rather than just the domain. The expected behavior is to display only the domain, similar to how signature requests are handled. This update ensures consistency across both transaction and signature requests. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/11690 ## **Manual testing steps** 1. Open a dapp within the in app browser 2. Trigger a transaction request 3. See the origin pill ## **Screenshots/Recordings** [origin.webm](https://github.com/user-attachments/assets/31736e36-9054-496e-992e-5ab55688361c) ### **Before** ### **After** ## **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. --- .../UI/ApprovalTagUrl/ApprovalTagUrl.test.tsx | 34 +++++++++ .../UI/ApprovalTagUrl/ApprovalTagUrl.tsx | 6 +- .../ApprovalTagUrl.test.tsx.snap | 69 +++++++++++++++++++ .../Views/AccountConnect/AccountConnect.tsx | 4 +- .../ApproveTransactionHeader.tsx | 9 +-- 5 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 app/components/UI/ApprovalTagUrl/ApprovalTagUrl.test.tsx create mode 100644 app/components/UI/ApprovalTagUrl/__snapshots__/ApprovalTagUrl.test.tsx.snap diff --git a/app/components/UI/ApprovalTagUrl/ApprovalTagUrl.test.tsx b/app/components/UI/ApprovalTagUrl/ApprovalTagUrl.test.tsx new file mode 100644 index 000000000000..415cff34f013 --- /dev/null +++ b/app/components/UI/ApprovalTagUrl/ApprovalTagUrl.test.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import renderWithProvider from '../../../util/test/renderWithProvider'; +import ApprovalTagUrl from './ApprovalTagUrl'; +import { backgroundState } from '../../../util/test/initial-root-state'; + +const ADDRESS_MOCK = '0x1234567890abcdef1234567890abcdef12345678'; +const DOMAIN_MOCK = 'metamask.github.io'; +const mockInitialState = { + settings: {}, + engine: { + backgroundState: { + ...backgroundState, + PreferencesController: { + selectedAddress: ADDRESS_MOCK, + }, + }, + }, +}; + +describe('ApprovalTagUrl', () => { + it('renders correctly', () => { + const { toJSON } = renderWithProvider( + , + { state: mockInitialState }, + ); + + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/ApprovalTagUrl/ApprovalTagUrl.tsx b/app/components/UI/ApprovalTagUrl/ApprovalTagUrl.tsx index e7980f2848d2..126f3132b170 100644 --- a/app/components/UI/ApprovalTagUrl/ApprovalTagUrl.tsx +++ b/app/components/UI/ApprovalTagUrl/ApprovalTagUrl.tsx @@ -8,7 +8,7 @@ import { useStyles } from '../../../component-library/hooks'; import AppConstants from '../../../core/AppConstants'; import { selectInternalAccounts } from '../../../selectors/accountsController'; import { selectAccountsByChainId } from '../../../selectors/accountTrackerController'; -import { prefixUrlWithProtocol } from '../../../util/browser'; +import { getHost, prefixUrlWithProtocol } from '../../../util/browser'; import useFavicon from '../../hooks/useFavicon/useFavicon'; import stylesheet from './ApprovalTagUrl.styles'; @@ -51,8 +51,8 @@ const ApprovalTagUrl = ({ const domainTitle = useMemo(() => { let title = ''; - if (url || currentEnsName || origin) { - title = prefixUrlWithProtocol(currentEnsName || origin || url); + if (currentEnsName || origin || url) { + title = prefixUrlWithProtocol(currentEnsName || origin || getHost(url)); } else { title = ''; } diff --git a/app/components/UI/ApprovalTagUrl/__snapshots__/ApprovalTagUrl.test.tsx.snap b/app/components/UI/ApprovalTagUrl/__snapshots__/ApprovalTagUrl.test.tsx.snap new file mode 100644 index 000000000000..b68dc550e4d9 --- /dev/null +++ b/app/components/UI/ApprovalTagUrl/__snapshots__/ApprovalTagUrl.test.tsx.snap @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApprovalTagUrl renders correctly 1`] = ` + + + + + + https://metamask.github.io + + +`; diff --git a/app/components/Views/AccountConnect/AccountConnect.tsx b/app/components/Views/AccountConnect/AccountConnect.tsx index 5ed77fe0df56..6bb65b3e2d0d 100644 --- a/app/components/Views/AccountConnect/AccountConnect.tsx +++ b/app/components/Views/AccountConnect/AccountConnect.tsx @@ -38,7 +38,7 @@ import { getAddressAccountType, safeToChecksumAddress, } from '../../../util/address'; -import { getUrlObj, prefixUrlWithProtocol } from '../../../util/browser'; +import { getHost, getUrlObj, prefixUrlWithProtocol } from '../../../util/browser'; import { getActiveTabUrl } from '../../../util/transactions'; import { Account, useAccounts } from '../../hooks/useAccounts'; @@ -177,7 +177,7 @@ const AccountConnect = (props: AccountConnectProps) => { const urlWithProtocol = hostname && !isUUID(hostname) - ? prefixUrlWithProtocol(hostname) + ? prefixUrlWithProtocol(getHost(hostname)) : domainTitle; const isAllowedOrigin = useCallback((origin: string) => { diff --git a/app/components/Views/confirmations/components/ApproveTransactionHeader/ApproveTransactionHeader.tsx b/app/components/Views/confirmations/components/ApproveTransactionHeader/ApproveTransactionHeader.tsx index 5870aae52daf..6db2535f4e92 100644 --- a/app/components/Views/confirmations/components/ApproveTransactionHeader/ApproveTransactionHeader.tsx +++ b/app/components/Views/confirmations/components/ApproveTransactionHeader/ApproveTransactionHeader.tsx @@ -1,5 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck - Confirmations team or Transactions team import { toChecksumAddress } from 'ethereumjs-util'; import React, { useEffect, useState } from 'react'; import { View } from 'react-native'; @@ -27,6 +25,7 @@ import stylesheet from './ApproveTransactionHeader.styles'; import { ApproveTransactionHeaderI } from './ApproveTransactionHeader.types'; import { selectInternalAccounts } from '../../../../../selectors/accountsController'; import ApprovalTagUrl from '../../../../UI/ApprovalTagUrl'; +import { RootState } from '../../../../../reducers'; const ApproveTransactionHeader = ({ from, @@ -51,9 +50,7 @@ const ApproveTransactionHeader = ({ const networkName = useSelector(selectNetworkName); const useBlockieIcon = useSelector( - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (state: any) => state.settings.useBlockieIcon, + (state: RootState) => state.settings.useBlockieIcon, ); useEffect(() => { @@ -70,7 +67,7 @@ const ApproveTransactionHeader = ({ const networkImage = useSelector(selectNetworkImageSource); - const accountTypeLabel = getLabelTextByAddress(activeAddress); + const accountTypeLabel = getLabelTextByAddress(activeAddress) ?? undefined; return ( From 81f78396bcaad8b28a159d64ea89a58b82f99459 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Fri, 11 Oct 2024 11:02:28 -0230 Subject: [PATCH 27/46] chore: Remove obsolete CI step (#11756) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** CI has a step where it sets up an alternative registry for `@metamask`- scoped packages specifically for draft PRs. This was intended to support the older stype of `MetaMask/core` preview builds, which used an alternative npm registry. Preview builds today now use the normal npm registry, so this step is no longer needed. ## **Related issues** Originally added here: https://github.com/MetaMask/metamask-mobile/pull/5270 Some related changes here: https://github.com/MetaMask/metamask-mobile/pull/5807 (these were already removed) The old preview build method was replaced here: https://github.com/MetaMask/core/pull/1622 ## **Manual testing steps** N/A ## **Screenshots/Recordings** N/A ## **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. --- .github/workflows/ci.yml | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e1b92a4d58d6..584e983f57cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,18 +21,6 @@ jobs: bundler-cache: true env: BUNDLE_GEMFILE: ios/Gemfile - - name: Determine whether the current PR is a draft - id: set-is-draft - if: github.event_name == 'pull_request' && github.event.pull_request.number - run: echo "IS_DRAFT=$(gh pr view --json isDraft --jq '.isDraft' "${PR_NUMBER}")" >> "$GITHUB_OUTPUT" - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Setup registry config for using package previews on draft PRs - if: github.event_name == 'pull_request' && steps.set-is-draft.outputs.IS_DRAFT == 'true' - run: printf '%s\n\n%s' '@metamask:registry=https://npm.pkg.github.com' "//npm.pkg.github.com/:_authToken=${PACKAGE_READ_TOKEN}" > .npmrc - env: - PACKAGE_READ_TOKEN: ${{ secrets.PACKAGE_READ_TOKEN }} - run: yarn setup - name: Require clean working directory shell: bash @@ -179,14 +167,14 @@ jobs: with: name: ios-bundle path: ios/main.jsbundle - + ship-js-bundle-size-check: runs-on: ubuntu-latest needs: [js-bundle-size-check] if: ${{ github.ref == 'refs/heads/main' }} steps: - uses: actions/checkout@v4 - + - name: Download iOS bundle uses: actions/download-artifact@v4 with: From 922ffbbd6fd53d104a42073a84fd6cdeca8b5e1e Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Fri, 11 Oct 2024 20:14:05 +0530 Subject: [PATCH 28/46] feat: Adding simulation section to personal sign page (#11737) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Add simulation section on re-designed personal sign page. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/11678 ## **Manual testing steps** 1. Enable re-designs locally 2. Go to test dapp 3. Submit personal sign page and check simulation section ## **Screenshots/Recordings** Screenshot 2024-10-10 at 6 00 14 PM ## **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 - [ ] 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. --- .../confirmations/Confirm/Confirm.styles.ts | 2 +- .../__snapshots__/Confirm.test.tsx.snap | 127 ++++++++++++++++- .../PersonalSign/Message/Message.test.tsx | 2 +- .../Info/PersonalSign/PersonalSign.tsx | 2 + .../Simulation/Simulation.test.tsx | 32 +++++ .../PersonalSign/Simulation/Simulation.tsx | 30 ++++ .../__snapshots__/Simulation.test.tsx.snap | 131 ++++++++++++++++++ .../Info/PersonalSign/Simulation/index.ts | 1 + .../__snapshots__/PersonalSign.test.tsx.snap | 125 +++++++++++++++++ .../Info/__snapshots__/Info.test.tsx.snap | 125 +++++++++++++++++ .../components/UI/Tooltip/Tooltip.styles.ts | 3 +- .../components/UI/Tooltip/Tooltip.tsx | 4 +- locales/languages/en.json | 7 +- 13 files changed, 584 insertions(+), 7 deletions(-) create mode 100644 app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Simulation/Simulation.test.tsx create mode 100644 app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Simulation/Simulation.tsx create mode 100644 app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Simulation/__snapshots__/Simulation.test.tsx.snap create mode 100644 app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Simulation/index.ts diff --git a/app/components/Views/confirmations/Confirm/Confirm.styles.ts b/app/components/Views/confirmations/Confirm/Confirm.styles.ts index b1fce0866eaf..334347964a13 100644 --- a/app/components/Views/confirmations/Confirm/Confirm.styles.ts +++ b/app/components/Views/confirmations/Confirm/Confirm.styles.ts @@ -11,7 +11,7 @@ const styleSheet = (params: { theme: Theme }) => { backgroundColor: theme.colors.background.alternative, paddingHorizontal: 16, paddingVertical: 24, - minHeight: '60%', + minHeight: '70%', borderTopLeftRadius: 20, borderTopRightRadius: 20, paddingBottom: Device.isIphoneX() ? 20 : 0, diff --git a/app/components/Views/confirmations/Confirm/__snapshots__/Confirm.test.tsx.snap b/app/components/Views/confirmations/Confirm/__snapshots__/Confirm.test.tsx.snap index 39ec20d3383e..4b8a5e2e9902 100644 --- a/app/components/Views/confirmations/Confirm/__snapshots__/Confirm.test.tsx.snap +++ b/app/components/Views/confirmations/Confirm/__snapshots__/Confirm.test.tsx.snap @@ -121,7 +121,7 @@ exports[`Confirm should match snapshot for personal sign 1`] = ` "borderTopLeftRadius": 20, "borderTopRightRadius": 20, "justifyContent": "space-between", - "minHeight": "60%", + "minHeight": "70%", "paddingBottom": 20, "paddingHorizontal": 16, "paddingVertical": 24, @@ -344,6 +344,131 @@ exports[`Confirm should match snapshot for personal sign 1`] = ` /> + + + + + Estimated changes + + + + + + + + + + You’re signing into a site and there are no predicted changes to your account. + + + { it('should match snapshot', async () => { diff --git a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.tsx b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.tsx index 74b713745df5..62b28b911f4f 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.tsx +++ b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.tsx @@ -6,6 +6,7 @@ import InfoSection from '../../../UI/InfoRow/InfoSection'; import InfoRow from '../../../UI/InfoRow'; import InfoURL from '../../../UI/InfoRow/InfoValue/InfoURL'; import Message from './Message'; +import Simulation from './Simulation'; const PersonalSign = () => { const { approvalRequest } = useApprovalRequest(); @@ -16,6 +17,7 @@ const PersonalSign = () => { return ( <> + { + it('should match snapshot', async () => { + const container = renderWithProvider(, { + state: personalSignatureConfirmationState, + }); + expect(container).toMatchSnapshot(); + }); + + it('should return null if preference useTransactionSimulations is not enabled', async () => { + const container = renderWithProvider(, { + state: { + engine: { + backgroundState: { + ...personalSignatureConfirmationState, + PreferencesController: { + ...personalSignatureConfirmationState.engine.backgroundState + .PreferencesController, + useTransactionSimulations: false, + }, + }, + }, + }, + }); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Simulation/Simulation.tsx b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Simulation/Simulation.tsx new file mode 100644 index 000000000000..248be3bbcad8 --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Simulation/Simulation.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; + +import { strings } from '../../../../../../../../../locales/i18n'; +import { selectUseTransactionSimulations } from '../../../../../../../../selectors/preferencesController'; +import InfoSection from '../../../../UI/InfoRow/InfoSection'; +import InfoRow from '../../../../UI/InfoRow'; + +const Simulation = () => { + const useTransactionSimulations = useSelector( + selectUseTransactionSimulations, + ); + + if (useTransactionSimulations !== true) { + return null; + } + + return ( + + + {strings('confirm.simulation.personal_sign_info')} + + + ); +}; + +export default Simulation; diff --git a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Simulation/__snapshots__/Simulation.test.tsx.snap b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Simulation/__snapshots__/Simulation.test.tsx.snap new file mode 100644 index 000000000000..ec9301c9d736 --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Simulation/__snapshots__/Simulation.test.tsx.snap @@ -0,0 +1,131 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Simulation should match snapshot 1`] = ` + + + + + Estimated changes + + + + + + + + + + You’re signing into a site and there are no predicted changes to your account. + + + +`; + +exports[`Simulation should return null if preference useTransactionSimulations is not enabled 1`] = `null`; diff --git a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Simulation/index.ts b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Simulation/index.ts new file mode 100644 index 000000000000..50cee91255fc --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/Simulation/index.ts @@ -0,0 +1 @@ +export { default } from './Simulation'; diff --git a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/__snapshots__/PersonalSign.test.tsx.snap b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/__snapshots__/PersonalSign.test.tsx.snap index c12fabda20ac..863fe1c904b9 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/__snapshots__/PersonalSign.test.tsx.snap +++ b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/__snapshots__/PersonalSign.test.tsx.snap @@ -2,6 +2,131 @@ exports[`Title should match snapshot 1`] = ` [ + + + + + Estimated changes + + + + + + + + + + You’re signing into a site and there are no predicted changes to your account. + + + , + + + + Estimated changes + + + + + + + + + + You’re signing into a site and there are no predicted changes to your account. + + + , { ...fontStyles.bold, fontSize: 16, fontWeight: '700', - marginBottom: 16, + marginTop: 8, }, modalContent: { + marginTop: 8, color: theme.colors.text.default, ...fontStyles.normal, fontSize: 14, diff --git a/app/components/Views/confirmations/components/UI/Tooltip/Tooltip.tsx b/app/components/Views/confirmations/components/UI/Tooltip/Tooltip.tsx index e779ed935a54..1eb82fb1988f 100644 --- a/app/components/Views/confirmations/components/UI/Tooltip/Tooltip.tsx +++ b/app/components/Views/confirmations/components/UI/Tooltip/Tooltip.tsx @@ -49,12 +49,12 @@ const Tooltip = ({ content, title, tooltipTestId }: TooltipProps) => { iconColor={IconColor.Default} iconName={IconName.Close} onPress={() => setOpen(false)} - size={ButtonIconSizes.Md} + size={ButtonIconSizes.Sm} style={styles.closeModalBtn} testID={tooltipTestId ?? 'tooltipTestId'} /> {title && {title}} - {content} + {content} diff --git a/locales/languages/en.json b/locales/languages/en.json index 7bb7eb9a5776..846559d45894 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -3427,6 +3427,11 @@ "account": "Account", "balance": "Balance", "network": "Network", - "rpc_url": "RPC URL" + "rpc_url": "RPC URL", + "simulation": { + "title": "Estimated changes", + "personal_sign_info": "You’re signing into a site and there are no predicted changes to your account.", + "tooltip": "Estimated changes are what might happen if you go through with this transaction. This is just a prediction, not a guarantee." + } } } From 5c66cee28d5f17959a0d359dd421cb665b708a42 Mon Sep 17 00:00:00 2001 From: tommasini <46944231+tommasini@users.noreply.github.com> Date: Fri, 11 Oct 2024 18:54:57 +0100 Subject: [PATCH 29/46] fix: snapshot of test to enable ci (#11762) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Updated snapshot, the unit test still needs refactor to not use an hardcoded date ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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. --- .../NotificationMenuItem/__snapshots__/Content.test.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/UI/Notification/NotificationMenuItem/__snapshots__/Content.test.tsx.snap b/app/components/UI/Notification/NotificationMenuItem/__snapshots__/Content.test.tsx.snap index 270e63dce71b..a0c54c2557d5 100644 --- a/app/components/UI/Notification/NotificationMenuItem/__snapshots__/Content.test.tsx.snap +++ b/app/components/UI/Notification/NotificationMenuItem/__snapshots__/Content.test.tsx.snap @@ -47,7 +47,7 @@ exports[`NotificationContent renders correctly 1`] = ` } } > - 5 months ago + 6 months ago Date: Fri, 11 Oct 2024 20:23:20 +0200 Subject: [PATCH 30/46] feat: Transition from Multiple Networks with Same ChainID to Unique Networks with Distinct ChainIDs and Multiple RPC URLs (#11705) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR refactors our network configuration to eliminate the use of multiple networks with the same ChainID but different RPC URLs. Instead, we are moving towards a setup where each network is uniquely identified by a distinct ChainID and can have multiple RPC URLs associated with it. This PR includes three merge commits. The first primarily addresses the Network Controller upgrade, as outlined in issue #[11229](https://github.com/MetaMask/metamask-mobile/issues/11229). The second commit contains the script for migrating the state to v21, and the third commit includes all the UI changes along with the fix for the e2e tests. For more details, please refer to [this](https://github.com/orgs/MetaMask/projects/120/views/1) . related PRs: - https://github.com/MetaMask/metamask-mobile/pull/11292 - https://github.com/MetaMask/metamask-mobile/pull/11622 - https://github.com/MetaMask/metamask-mobile/pull/11436 ## **Related issues** Fixes: #[11229](https://github.com/MetaMask/metamask-mobile/issues/11229) #[11232](https://github.com/MetaMask/metamask-mobile/issues/11232) #[11234](https://github.com/MetaMask/metamask-mobile/issues/11234) #11233 ## **Manual testing steps** 1. Go to add network flow and test ## **Screenshots/Recordings** ### **Before** ### **After** https://drive.google.com/drive/folders/149Xji42k5of5Vl8nBlI0pFYFgPnWqILH?usp=drive_link ## **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. --- .../CellSelectWithMenu.test.tsx.snap | 4 +- .../ListItemMultiSelectButton.constants.ts | 1 + .../ListItemMultiSelectButton.styles.ts | 4 +- .../ListItemMultiSelectButton.test.tsx.snap | 4 +- app/components/Nav/App/index.js | 1 - app/components/Nav/Main/index.js | 1 - .../UI/AccountInfoCard/index.test.tsx | 23 +- .../UI/NetworkModal/NetworkAdded/index.tsx | 5 +- app/components/UI/NetworkModal/index.tsx | 178 +- .../NetworkSwitcher/NetworkSwitcher.test.tsx | 2 +- .../Views/NetworkSwitcher/NetworkSwitcher.tsx | 18 +- .../NetworkSwitcher.test.tsx.snap | 72 +- .../UI/ReceiveRequest/index.test.tsx | 20 +- app/components/Views/AssetDetails/index.tsx | 4 +- .../Views/MultiRpcModal/MultiRpcModal.tsx | 79 +- .../NetworkSelector/NetworkSelector.test.tsx | 254 +- .../Views/NetworkSelector/NetworkSelector.tsx | 357 ++- .../Details/Footers/BlockExplorerFooter.tsx | 2 +- .../__snapshots__/index.test.tsx.snap | 858 ++++++ .../__snapshots__/index.test.tsx.snap | 858 ++++++ .../IncomingTransactionsSettings/index.tsx | 3 +- .../NetworksSettings/NetworkSettings/index.js | 1068 +++++-- .../NetworkSettings/index.test.tsx | 167 +- .../__snapshots__/index.test.tsx.snap | 22 +- .../Views/Settings/NetworksSettings/index.js | 53 +- .../SecuritySettings.test.tsx.snap | 858 ++++++ app/components/Views/Wallet/index.tsx | 32 +- .../Send/__snapshots__/index.test.tsx.snap | 4 +- .../SendFlow/Amount/index.test.tsx | 31 +- .../Confirm/__snapshots__/index.test.tsx.snap | 2 +- .../SendFlow/Confirm/index.test.tsx | 25 +- .../ApproveTransactionHeader.test.tsx.snap | 8 +- .../AddNickname/types.ts | 2 +- .../ShowBlockExplorer/index.tsx | 6 +- .../VerifyContractDetails.types.ts | 2 +- .../components/PersonalSign/index.test.tsx | 4 +- .../components/TypedSign/index.test.tsx | 4 +- app/components/hooks/useBlockExplorer.test.ts | 30 +- app/components/hooks/useBlockExplorer.ts | 3 +- app/core/Engine.test.js | 11 +- app/core/Engine.ts | 4 +- .../RPCMethods/RPCMethodMiddleware.test.ts | 209 +- .../RPCMethods/wallet_addEthereumChain.js | 121 +- .../wallet_addEthereumChain.test.js | 22 +- .../RPCMethods/wallet_switchEthereumChain.js | 15 +- app/core/RPCMethods/wallet_watchAsset.test.ts | 24 +- .../handlers/handleConnectionMessage.test.ts | 10 +- app/lib/ppom/ppom-util.test.ts | 52 +- app/reducers/fiatOrders/index.test.ts | 243 +- .../accountTrackerController.test.ts | 18 +- ...accountTrackerControllerReRenders.test.tsx | 141 +- app/selectors/currencyRateController.ts | 18 +- app/selectors/networkController.ts | 140 +- app/selectors/selectedNetworkController.ts | 38 +- app/store/migrations/023.test.js | 22 + app/store/migrations/029.ts | 18 +- app/store/migrations/035.test.ts | 6 +- app/store/migrations/043.test.ts | 5 +- app/store/migrations/051.test.ts | 1 + app/store/migrations/055.test.ts | 584 ++++ app/store/migrations/055.ts | 403 +++ app/store/migrations/index.ts | 2 + app/util/address/index.test.ts | 50 +- app/util/address/index.ts | 2 +- app/util/hideKeyFromUrl.test.ts | 70 +- app/util/hideKeyFromUrl.ts | 8 + app/util/networks/handleNetworkSwitch.test.ts | 92 +- app/util/networks/handleNetworkSwitch.ts | 28 +- app/util/networks/index.js | 30 +- app/util/networks/index.test.ts | 96 +- .../networks/isNetworkUiRedesignEnabled.ts | 3 +- .../sentry/__snapshots__/utils.test.ts.snap | 2 +- app/util/sentry/utils.test.ts | 2 +- app/util/test/initial-background-state.json | 94 +- app/util/test/network.ts | 76 +- e2e/fixtures/fixture-builder.js | 220 +- e2e/pages/Settings/NetworksView.js | 35 + e2e/pages/modals/NetworkListModal.js | 51 + .../Modals/NetworkListModal.selectors.js | 2 + .../Settings/NetworksView.selectors.js | 4 + e2e/specs/networks/add-custom-rpc.spec.js | 92 +- .../networks/add-popular-networks.spec.js | 17 +- .../networks/connect-test-network.spec.js | 4 + e2e/specs/networks/networks-search.spec.js | 42 +- e2e/specs/wallet/send-ERC-token.spec.js | 2 + e2e/viewHelper.js | 12 +- locales/languages/en.json | 4 + package.json | 5 +- patches/@metamask+nonce-tracker+5.0.0.patch | 30 - ...tamask+transaction-controller+35.0.0.patch | 2588 ----------------- yarn.lock | 136 +- 91 files changed, 6923 insertions(+), 4050 deletions(-) create mode 100644 app/store/migrations/055.test.ts create mode 100644 app/store/migrations/055.ts delete mode 100644 patches/@metamask+nonce-tracker+5.0.0.patch delete mode 100644 patches/@metamask+transaction-controller+35.0.0.patch diff --git a/app/component-library/components-temp/CellSelectWithMenu/__snapshots__/CellSelectWithMenu.test.tsx.snap b/app/component-library/components-temp/CellSelectWithMenu/__snapshots__/CellSelectWithMenu.test.tsx.snap index f86327146856..315ea9c6914c 100644 --- a/app/component-library/components-temp/CellSelectWithMenu/__snapshots__/CellSelectWithMenu.test.tsx.snap +++ b/app/component-library/components-temp/CellSelectWithMenu/__snapshots__/CellSelectWithMenu.test.tsx.snap @@ -7,8 +7,6 @@ exports[`CellSelectWithMenu should render with default settings correctly 1`] = "alignItems": "center", "backgroundColor": "#ffffff", "flexDirection": "row", - "paddingRight": 20, - "width": "100%", } } > @@ -16,10 +14,10 @@ exports[`CellSelectWithMenu should render with default settings correctly 1`] = disabled={false} style={ { + "flex": 1, "opacity": 1, "padding": 16, "position": "relative", - "width": "90%", "zIndex": 1, } } diff --git a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.constants.ts b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.constants.ts index fa1de756de16..06fa3a2118c9 100644 --- a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.constants.ts +++ b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.constants.ts @@ -8,6 +8,7 @@ import { ListItemMultiSelectButtonProps } from './ListItemMultiSelectButton.type // Defaults export const DEFAULT_LISTITEMMULTISELECT_GAP = 16; export const BUTTON_TEST_ID = 'button-menu-select-test-id'; +export const BUTTON_TEXT_TEST_ID = 'button-text-select-test-id'; // Sample consts export const SAMPLE_LISTITEMMULTISELECT_PROPS: ListItemMultiSelectButtonProps = diff --git a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.styles.ts b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.styles.ts index d3ff43c9cb2a..4af6d6f86a92 100644 --- a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.styles.ts +++ b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.styles.ts @@ -25,10 +25,10 @@ const styleSheet = (params: { return StyleSheet.create({ base: Object.assign( { + flex: 1, position: 'relative', opacity: isDisabled ? 0.5 : 1, padding: 16, - width: '90%', zIndex: 1, } as ViewStyle, style, @@ -71,10 +71,8 @@ const styleSheet = (params: { backgroundColor: isSelected ? colors.primary.muted : colors.background.default, - paddingRight: 20, flexDirection: 'row', alignItems: 'center', - width: '100%', }, itemColumn: { display: 'flex', diff --git a/app/component-library/components-temp/ListItemMultiSelectButton/__snapshots__/ListItemMultiSelectButton.test.tsx.snap b/app/component-library/components-temp/ListItemMultiSelectButton/__snapshots__/ListItemMultiSelectButton.test.tsx.snap index a1a0b239ae69..0d1b7d3f4f59 100644 --- a/app/component-library/components-temp/ListItemMultiSelectButton/__snapshots__/ListItemMultiSelectButton.test.tsx.snap +++ b/app/component-library/components-temp/ListItemMultiSelectButton/__snapshots__/ListItemMultiSelectButton.test.tsx.snap @@ -7,8 +7,6 @@ exports[`ListItemMultiSelectButton should render correctly with default props 1` "alignItems": "center", "backgroundColor": "#ffffff", "flexDirection": "row", - "paddingRight": 20, - "width": "100%", } } > @@ -16,10 +14,10 @@ exports[`ListItemMultiSelectButton should render correctly with default props 1` disabled={false} style={ { + "flex": 1, "opacity": 1, "padding": 16, "position": "relative", - "width": "90%", "zIndex": 1, } } diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js index a2a8b16d4850..a587ef6c3ae6 100644 --- a/app/components/Nav/App/index.js +++ b/app/components/Nav/App/index.js @@ -711,7 +711,6 @@ const App = (props) => { component={MultiRpcModal} /> ) : null} - { networkImageSource: networkImage, }); } - previousNetworkConfigurations.current = networkConfigurations; }, [networkConfigurations, networkName, networkImage, toastRef]); diff --git a/app/components/UI/AccountInfoCard/index.test.tsx b/app/components/UI/AccountInfoCard/index.test.tsx index fe727c0ee84c..a2f1c1bcdde6 100644 --- a/app/components/UI/AccountInfoCard/index.test.tsx +++ b/app/components/UI/AccountInfoCard/index.test.tsx @@ -9,6 +9,8 @@ import { MOCK_ADDRESS_1, } from '../../../util/test/accountsControllerTestUtils'; import { RootState } from '../../../reducers'; +import { RpcEndpointType } from '@metamask/network-controller'; +import { mockNetworkState } from '../../../util/test/network'; jest.mock('../../../core/Engine', () => ({ resetState: jest.fn(), @@ -48,20 +50,13 @@ const mockInitialState: DeepPartial = { }, }, NetworkController: { - selectedNetworkClientId: 'sepolia', - networksMetadata: {}, - networkConfigurations: { - sepolia: { - id: 'sepolia', - rpcUrl: 'http://localhost/v3/', - chainId: '0xaa36a7', - ticker: 'ETH', - nickname: 'sepolia', - rpcPrefs: { - blockExplorerUrl: 'https://etherscan.com', - }, - }, - }, + ...mockNetworkState({ + chainId: '0xaa36a7', + id: 'mainnet', + nickname: 'Sepolia', + ticker: 'SepoliaETH', + type: RpcEndpointType.Infura, + }), }, TokenBalancesController: { contractBalances: {}, diff --git a/app/components/UI/NetworkModal/NetworkAdded/index.tsx b/app/components/UI/NetworkModal/NetworkAdded/index.tsx index 17a9e360336b..d4dc06b8dc68 100644 --- a/app/components/UI/NetworkModal/NetworkAdded/index.tsx +++ b/app/components/UI/NetworkModal/NetworkAdded/index.tsx @@ -14,6 +14,9 @@ const createStyles = (colors: any) => flexDirection: 'row', paddingVertical: 16, }, + base: { + padding: 16, + }, button: { flex: 1, }, @@ -41,7 +44,7 @@ const NetworkAdded = (props: NetworkAddedProps) => { const styles = createStyles(colors); return ( - + {strings('networks.new_network')} diff --git a/app/components/UI/NetworkModal/index.tsx b/app/components/UI/NetworkModal/index.tsx index 61493f669455..4da8d8d5f1b6 100644 --- a/app/components/UI/NetworkModal/index.tsx +++ b/app/components/UI/NetworkModal/index.tsx @@ -36,6 +36,12 @@ import { useMetrics } from '../../../components/hooks/useMetrics'; import { toHex } from '@metamask/controller-utils'; import { rpcIdentifierUtility } from '../../../components/hooks/useSafeChains'; import Logger from '../../../util/Logger'; +import { selectNetworkConfigurations } from '../../../selectors/networkController'; +import { + NetworkConfiguration, + RpcEndpointType, + AddNetworkFields, +} from '@metamask/network-controller'; export interface SafeChain { chainId: string; @@ -162,6 +168,10 @@ const NetworkModals = (props: NetworkProps) => { selectUseSafeChainsListValidation, ); + const networkConfigurationByChainId = useSelector( + selectNetworkConfigurations, + ); + const customNetworkInformation = { chainId, blockExplorerUrl, @@ -189,52 +199,154 @@ const NetworkModals = (props: NetworkProps) => { checkNetwork(); }, [checkNetwork]); - const closeModal = () => { + const closeModal = async () => { const { NetworkController } = Engine.context; const url = new URLPARSE(rpcUrl); !isPrivateConnection(url.hostname) && url.set('protocol', 'https:'); - NetworkController.upsertNetworkConfiguration( - { - rpcUrl: url.href, + + const existingNetwork = networkConfigurationByChainId[chainId]; + let networkClientId; + + if (existingNetwork) { + const updatedNetwork = await NetworkController.updateNetwork( + existingNetwork.chainId, + existingNetwork, + existingNetwork.chainId === chainId + ? { + replacementSelectedRpcEndpointIndex: + existingNetwork.defaultRpcEndpointIndex, + } + : undefined, + ); + + networkClientId = + updatedNetwork?.rpcEndpoints?.[updatedNetwork.defaultRpcEndpointIndex] + ?.networkClientId; + } else { + const addedNetwork = await NetworkController.addNetwork({ chainId, - ticker, - nickname, - rpcPrefs: { blockExplorerUrl }, - }, - { - // Metrics-related properties required, but the metric event is a no-op - // TODO: Use events for controller metric events - referrer: 'ignored', - source: 'ignored', - }, - ); + blockExplorerUrls: [blockExplorerUrl], + defaultRpcEndpointIndex: 0, + defaultBlockExplorerUrlIndex: 0, + name: nickname, + nativeCurrency: ticker, + rpcEndpoints: [ + { + url: rpcUrl, + name: nickname, + type: RpcEndpointType.Custom, + }, + ], + }); + + networkClientId = + addedNetwork?.rpcEndpoints?.[addedNetwork.defaultRpcEndpointIndex] + ?.networkClientId; + } + + if (networkClientId) { + await NetworkController.setActiveNetwork(networkClientId); + } + onClose(); }; - const switchNetwork = () => { + const handleExistingNetwork = async ( + existingNetwork: NetworkConfiguration, + networkId: string, + ) => { + const { NetworkController } = Engine.context; + const updatedNetwork = await NetworkController.updateNetwork( + existingNetwork.chainId, + existingNetwork, + existingNetwork.chainId === networkId + ? { + replacementSelectedRpcEndpointIndex: + existingNetwork.defaultRpcEndpointIndex, + } + : undefined, + ); + + const { networkClientId } = + updatedNetwork?.rpcEndpoints?.[updatedNetwork.defaultRpcEndpointIndex] ?? + {}; + + await NetworkController.setActiveNetwork(networkClientId); + }; + + const handleNewNetwork = async ( + networkId: `0x${string}`, + networkRpcUrl: string, + name: string, + nativeCurrency: string, + networkBlockExplorerUrl: string, + ) => { + const { NetworkController } = Engine.context; + const networkConfig = { + chainId: networkId, + blockExplorerUrls: networkBlockExplorerUrl + ? [networkBlockExplorerUrl] + : [], + defaultRpcEndpointIndex: 0, + defaultBlockExplorerUrlIndex: blockExplorerUrl ? 0 : undefined, + name, + nativeCurrency, + rpcEndpoints: [ + { + url: networkRpcUrl, + name, + type: RpcEndpointType.Custom, + }, + ], + } as AddNetworkFields; + + return NetworkController.addNetwork(networkConfig); + }; + + const handleNavigation = ( + onSwitchNetwork: () => void, + networkSwitchPopToWallet: boolean, + ) => { + if (onSwitchNetwork) { + onSwitchNetwork(); + } else { + networkSwitchPopToWallet + ? navigation.navigate('WalletView') + : navigation.goBack(); + } + }; + + const switchNetwork = async () => { const { NetworkController, CurrencyRateController } = Engine.context; const url = new URLPARSE(rpcUrl); + const existingNetwork = networkConfigurationByChainId[chainId]; + CurrencyRateController.updateExchangeRate(ticker); - !isPrivateConnection(url.hostname) && url.set('protocol', 'https:'); - NetworkController.upsertNetworkConfiguration( - { - rpcUrl: url.href, + + if (!isPrivateConnection(url.hostname)) { + url.set('protocol', 'https:'); + } + + if (existingNetwork) { + await handleExistingNetwork(existingNetwork, chainId); + } else { + const addedNetwork = await handleNewNetwork( chainId, - ticker, + rpcUrl, nickname, - rpcPrefs: { blockExplorerUrl }, - }, - { - setActive: true, - // Metrics-related properties required, but the metric event is a no-op - // TODO: Use events for controller metric events - referrer: 'ignored', - source: 'ignored', - }, - ); - closeModal(); + ticker, + blockExplorerUrl, + ); + const { networkClientId } = + addedNetwork?.rpcEndpoints?.[addedNetwork.defaultRpcEndpointIndex] ?? + {}; + + NetworkController.setActiveNetwork(networkClientId); + } + onClose(); + if (onNetworkSwitch) { - onNetworkSwitch(); + handleNavigation(onNetworkSwitch, shouldNetworkSwitchPopToWallet); } else { shouldNetworkSwitchPopToWallet ? navigation.navigate('WalletView') diff --git a/app/components/UI/Ramp/Views/NetworkSwitcher/NetworkSwitcher.test.tsx b/app/components/UI/Ramp/Views/NetworkSwitcher/NetworkSwitcher.test.tsx index d4921f31148e..c79c4001d27c 100644 --- a/app/components/UI/Ramp/Views/NetworkSwitcher/NetworkSwitcher.test.tsx +++ b/app/components/UI/Ramp/Views/NetworkSwitcher/NetworkSwitcher.test.tsx @@ -90,7 +90,7 @@ function render(Component: React.ComponentType, chainId?: `0x${string}`) { chainId: '0x89', id: 'networkId1', nickname: 'Polygon Mainnet', - ticker: 'MATIC', + ticker: 'POL', }, ), }, diff --git a/app/components/UI/Ramp/Views/NetworkSwitcher/NetworkSwitcher.tsx b/app/components/UI/Ramp/Views/NetworkSwitcher/NetworkSwitcher.tsx index cf30293c73be..d1235a4f8b4f 100644 --- a/app/components/UI/Ramp/Views/NetworkSwitcher/NetworkSwitcher.tsx +++ b/app/components/UI/Ramp/Views/NetworkSwitcher/NetworkSwitcher.tsx @@ -154,16 +154,22 @@ function NetworkSwitcher() { const switchNetwork = useCallback( (networkConfiguration) => { const { CurrencyRateController, NetworkController } = Engine.context; - const entry = Object.entries(networkConfigurations).find( - ([_a, { chainId }]) => chainId === networkConfiguration.chainId, + const config = Object.values(networkConfigurations).find( + ({ chainId }) => chainId === networkConfiguration.chainId, ); - if (entry) { - const [networkConfigurationId] = entry; - const { ticker } = networkConfiguration; + if (config) { + const { + nativeCurrency: ticker, + rpcEndpoints, + defaultRpcEndpointIndex, + } = config; + + const { networkClientId } = + rpcEndpoints?.[defaultRpcEndpointIndex] ?? {}; CurrencyRateController.updateExchangeRate(ticker); - NetworkController.setActiveNetwork(networkConfigurationId); + NetworkController.setActiveNetwork(networkClientId); navigateToGetStarted(); } }, diff --git a/app/components/UI/Ramp/Views/NetworkSwitcher/__snapshots__/NetworkSwitcher.test.tsx.snap b/app/components/UI/Ramp/Views/NetworkSwitcher/__snapshots__/NetworkSwitcher.test.tsx.snap index 7f21f4f59075..92cb998fc5d8 100644 --- a/app/components/UI/Ramp/Views/NetworkSwitcher/__snapshots__/NetworkSwitcher.test.tsx.snap +++ b/app/components/UI/Ramp/Views/NetworkSwitcher/__snapshots__/NetworkSwitcher.test.tsx.snap @@ -549,7 +549,7 @@ exports[`NetworkSwitcher View renders and dismisses network modal when pressing { "borderRadius": 10, "height": 20, - "marginRight": 10, + "marginRight": 20, "width": 20, } } @@ -703,7 +703,7 @@ exports[`NetworkSwitcher View renders and dismisses network modal when pressing { "borderRadius": 10, "height": 20, - "marginRight": 10, + "marginRight": 20, "width": 20, } } @@ -1444,7 +1444,7 @@ exports[`NetworkSwitcher View renders and dismisses network modal when pressing { "borderRadius": 10, "height": 20, - "marginRight": 10, + "marginRight": 20, "width": 20, } } @@ -1492,10 +1492,7 @@ exports[`NetworkSwitcher View renders and dismisses network modal when pressing }, undefined, undefined, - { - "fontFamily": "EuclidCircularB-Bold", - "fontWeight": "600", - }, + false, undefined, undefined, undefined, @@ -1598,7 +1595,7 @@ exports[`NetworkSwitcher View renders and dismisses network modal when pressing { "borderRadius": 10, "height": 20, - "marginRight": 10, + "marginRight": 20, "width": 20, } } @@ -1648,10 +1645,7 @@ exports[`NetworkSwitcher View renders and dismisses network modal when pressing }, undefined, undefined, - { - "fontFamily": "EuclidCircularB-Bold", - "fontWeight": "600", - }, + false, undefined, undefined, undefined, @@ -2297,7 +2291,7 @@ exports[`NetworkSwitcher View renders and dismisses network modal when pressing { "borderRadius": 10, "height": 20, - "marginRight": 10, + "marginRight": 20, "width": 20, } } @@ -2451,7 +2445,7 @@ exports[`NetworkSwitcher View renders and dismisses network modal when pressing { "borderRadius": 10, "height": 20, - "marginRight": 10, + "marginRight": 20, "width": 20, } } @@ -2605,7 +2599,7 @@ exports[`NetworkSwitcher View renders and dismisses network modal when pressing { "borderRadius": 10, "height": 20, - "marginRight": 10, + "marginRight": 20, "width": 20, } } @@ -2653,10 +2647,7 @@ exports[`NetworkSwitcher View renders and dismisses network modal when pressing }, undefined, undefined, - { - "fontFamily": "EuclidCircularB-Bold", - "fontWeight": "600", - }, + false, undefined, undefined, undefined, @@ -2759,7 +2750,7 @@ exports[`NetworkSwitcher View renders and dismisses network modal when pressing { "borderRadius": 10, "height": 20, - "marginRight": 10, + "marginRight": 20, "width": 20, } } @@ -2809,10 +2800,7 @@ exports[`NetworkSwitcher View renders and dismisses network modal when pressing }, undefined, undefined, - { - "fontFamily": "EuclidCircularB-Bold", - "fontWeight": "600", - }, + false, undefined, undefined, undefined, @@ -3458,7 +3446,7 @@ exports[`NetworkSwitcher View renders correctly 1`] = ` { "borderRadius": 10, "height": 20, - "marginRight": 10, + "marginRight": 20, "width": 20, } } @@ -3612,7 +3600,7 @@ exports[`NetworkSwitcher View renders correctly 1`] = ` { "borderRadius": 10, "height": 20, - "marginRight": 10, + "marginRight": 20, "width": 20, } } @@ -3766,7 +3754,7 @@ exports[`NetworkSwitcher View renders correctly 1`] = ` { "borderRadius": 10, "height": 20, - "marginRight": 10, + "marginRight": 20, "width": 20, } } @@ -3814,10 +3802,7 @@ exports[`NetworkSwitcher View renders correctly 1`] = ` }, undefined, undefined, - { - "fontFamily": "EuclidCircularB-Bold", - "fontWeight": "600", - }, + false, undefined, undefined, undefined, @@ -3920,7 +3905,7 @@ exports[`NetworkSwitcher View renders correctly 1`] = ` { "borderRadius": 10, "height": 20, - "marginRight": 10, + "marginRight": 20, "width": 20, } } @@ -3970,10 +3955,7 @@ exports[`NetworkSwitcher View renders correctly 1`] = ` }, undefined, undefined, - { - "fontFamily": "EuclidCircularB-Bold", - "fontWeight": "600", - }, + false, undefined, undefined, undefined, @@ -4619,7 +4601,7 @@ exports[`NetworkSwitcher View renders correctly 2`] = ` { "borderRadius": 10, "height": 20, - "marginRight": 10, + "marginRight": 20, "width": 20, } } @@ -4773,7 +4755,7 @@ exports[`NetworkSwitcher View renders correctly 2`] = ` { "borderRadius": 10, "height": 20, - "marginRight": 10, + "marginRight": 20, "width": 20, } } @@ -4927,7 +4909,7 @@ exports[`NetworkSwitcher View renders correctly 2`] = ` { "borderRadius": 10, "height": 20, - "marginRight": 10, + "marginRight": 20, "width": 20, } } @@ -4975,10 +4957,7 @@ exports[`NetworkSwitcher View renders correctly 2`] = ` }, undefined, undefined, - { - "fontFamily": "EuclidCircularB-Bold", - "fontWeight": "600", - }, + false, undefined, undefined, undefined, @@ -5081,7 +5060,7 @@ exports[`NetworkSwitcher View renders correctly 2`] = ` { "borderRadius": 10, "height": 20, - "marginRight": 10, + "marginRight": 20, "width": 20, } } @@ -5131,10 +5110,7 @@ exports[`NetworkSwitcher View renders correctly 2`] = ` }, undefined, undefined, - { - "fontFamily": "EuclidCircularB-Bold", - "fontWeight": "600", - }, + false, undefined, undefined, undefined, diff --git a/app/components/UI/ReceiveRequest/index.test.tsx b/app/components/UI/ReceiveRequest/index.test.tsx index 6431db9c31a6..cc627eb8e64f 100644 --- a/app/components/UI/ReceiveRequest/index.test.tsx +++ b/app/components/UI/ReceiveRequest/index.test.tsx @@ -1,19 +1,23 @@ import { cloneDeep } from 'lodash'; +import { RpcEndpointType } from '@metamask/network-controller'; import ReceiveRequest from './'; import { renderScreen } from '../../../util/test/renderWithProvider'; import { backgroundState } from '../../../util/test/initial-root-state'; import { MOCK_ACCOUNTS_CONTROLLER_STATE } from '../../../util/test/accountsControllerTestUtils'; +import { mockNetworkState } from '../../../util/test/network'; const initialState = { engine: { backgroundState: { ...backgroundState, NetworkController: { - providerConfig: { - type: 'mainnet', - chainId: '0x1', + ...mockNetworkState({ + id: 'mainnet', + nickname: 'Ethereum', ticker: 'ETH', - }, + chainId: '0x1', + type: RpcEndpointType.Infura, + }), }, AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE, }, @@ -47,7 +51,6 @@ describe('ReceiveRequest', () => { const { toJSON } = renderScreen( ReceiveRequest, { name: 'ReceiveRequest' }, - // @ts-expect-error initialBackgroundState throws error { state: initialState }, ); expect(toJSON()).toMatchSnapshot(); @@ -55,12 +58,12 @@ describe('ReceiveRequest', () => { it('render with different ticker matches snapshot', () => { const state = cloneDeep(initialState); - state.engine.backgroundState.NetworkController.providerConfig.ticker = - 'DIFF'; + state.engine.backgroundState.NetworkController.networkConfigurationsByChainId[ + '0x1' + ].nativeCurrency = 'DIFF'; const { toJSON } = renderScreen( ReceiveRequest, { name: 'ReceiveRequest' }, - // @ts-expect-error initialBackgroundState throws error { state }, ); expect(toJSON()).toMatchSnapshot(); @@ -74,7 +77,6 @@ describe('ReceiveRequest', () => { const { toJSON } = renderScreen( ReceiveRequest, { name: 'ReceiveRequest' }, - // @ts-expect-error initialBackgroundState throws error { state }, ); expect(toJSON()).toMatchSnapshot(); diff --git a/app/components/Views/AssetDetails/index.tsx b/app/components/Views/AssetDetails/index.tsx index a67d3920e0db..56d48e62bd2e 100644 --- a/app/components/Views/AssetDetails/index.tsx +++ b/app/components/Views/AssetDetails/index.tsx @@ -46,6 +46,7 @@ import { useMetrics } from '../../../components/hooks/useMetrics'; import { RootState } from 'app/reducers'; import { Colors } from '../../../util/theme/models'; import { Hex } from '@metamask/utils'; +import { RpcEndpointType } from '@metamask/network-controller'; const createStyles = (colors: Colors) => StyleSheet.create({ @@ -138,8 +139,7 @@ const AssetDetails = (props: Props) => { * removes goerli from provider config types */ // eslint-disable-next-line @typescript-eslint/no-explicit-any - (Networks as any)[providerConfig.type]?.name || - { ...Networks.rpc, color: null }.name; + (Networks as any)[providerConfig?.type ?? RpcEndpointType.Custom]; } return name; }; diff --git a/app/components/Views/MultiRpcModal/MultiRpcModal.tsx b/app/components/Views/MultiRpcModal/MultiRpcModal.tsx index 71845b62a199..7925bd6c862e 100644 --- a/app/components/Views/MultiRpcModal/MultiRpcModal.tsx +++ b/app/components/Views/MultiRpcModal/MultiRpcModal.tsx @@ -28,6 +28,7 @@ import { useSelector } from 'react-redux'; import Cell, { CellVariant, } from '../../../component-library/components/Cells/Cell'; +import { NetworkConfiguration } from '@metamask/network-controller'; import { AvatarSize, AvatarVariant, @@ -81,43 +82,49 @@ const MultiRpcModal = () => { {Object.values(networkConfigurations).map( - (networkConfiguration, index) => ( - { - sheetRef.current?.onCloseBottomSheet(() => { - navigate(Routes.ADD_NETWORK, { - shouldNetworkSwitchPopToWallet: false, - shouldShowPopularNetworks: false, - network: networkConfiguration.rpcUrl, + (networkConfiguration: NetworkConfiguration, index) => + networkConfiguration.rpcEndpoints.length > 1 ? ( + { + sheetRef.current?.onCloseBottomSheet(() => { + navigate(Routes.ADD_NETWORK, { + shouldNetworkSwitchPopToWallet: false, + shouldShowPopularNetworks: false, + network: + networkConfiguration?.rpcEndpoints?.[ + networkConfiguration?.defaultRpcEndpointIndex + ].url, + }); }); - }); - }, - }} - /> - ), + }, + }} + /> + ) : null, )} diff --git a/app/components/Views/NetworkSelector/NetworkSelector.test.tsx b/app/components/Views/NetworkSelector/NetworkSelector.test.tsx index 9e53784fbb28..6dacf921a1a9 100644 --- a/app/components/Views/NetworkSelector/NetworkSelector.test.tsx +++ b/app/components/Views/NetworkSelector/NetworkSelector.test.tsx @@ -12,6 +12,8 @@ import NetworkSelector from './NetworkSelector'; import { CHAIN_IDS } from '@metamask/transaction-controller'; import { NetworkListModalSelectorsIDs } from '../../../../e2e/selectors/Modals/NetworkListModal.selectors'; import { isNetworkUiRedesignEnabled } from '../../../util/networks/isNetworkUiRedesignEnabled'; +import { mockNetworkState } from '../../../util/test/network'; + const mockEngine = Engine; const setShowTestNetworksSpy = jest.spyOn( @@ -35,6 +37,23 @@ jest.mock('../../../core/Engine', () => ({ setActiveNetwork: jest.fn(), setProviderType: jest.fn(), getNetworkClientById: jest.fn().mockReturnValue({ chainId: '0x1' }), + findNetworkClientIdByChainId: jest + .fn() + .mockReturnValue({ chainId: '0x1' }), + getNetworkConfigurationByChainId: jest.fn().mockReturnValue({ + blockExplorerUrls: [], + chainId: '0x1', + defaultRpcEndpointIndex: 0, + name: 'Mainnet', + nativeCurrency: 'ETH', + rpcEndpoints: [ + { + networkClientId: 'mainnet', + type: 'infura', + url: 'https://mainnet.infura.io/v3/{infuraProjectId}', + }, + ], + }), }, PreferencesController: { setShowTestNetworks: jest.fn(), @@ -63,37 +82,65 @@ const initialState = { }, NetworkController: { selectedNetworkClientId: 'mainnet', - networksMetadata: {}, - networkConfigurations: { - networkId1: { + networksMetadata: { + mainnet: { status: 'available', EIPS: { '1559': true } }, + }, + networkConfigurationsByChainId: { + '0xa86a': { + blockExplorerUrls: ['https://snowtrace.io'], chainId: '0xa86a', - nickname: 'Avalanche Mainnet C-Chain', - rpcPrefs: { blockExplorerUrl: 'https://snowtrace.io' }, - rpcUrl: 'https://api.avax.network/ext/bc/C/rpc', - ticker: 'AVAX', + defaultRpcEndpointIndex: 0, + name: 'Avalanche Mainnet C-Chain', + nativeCurrency: 'AVAX', + rpcEndpoints: [ + { + networkClientId: 'networkId1', + type: 'custom', + url: 'https://api.avax.network/ext/bc/C/rpc', + }, + ], }, - networkId2: { + '0x89': { + blockExplorerUrls: ['https://polygonscan.com'], chainId: '0x89', - nickname: 'Polygon Mainnet', - rpcPrefs: { blockExplorerUrl: 'https://polygonscan.com' }, - rpcUrl: 'https://polygon-mainnet.infura.io/v3/12345', - ticker: 'MATIC', + defaultRpcEndpointIndex: 0, + name: 'Polygon Mainnet', + nativeCurrency: 'MATIC', + rpcEndpoints: [ + { + networkClientId: 'networkId2', + type: 'infura', + url: 'https://polygon-mainnet.infura.io/v3/12345', + }, + ], }, - networkId3: { + '0xa': { + blockExplorerUrls: ['https://optimistic.etherscan.io'], chainId: '0xa', - nickname: 'Optimism', - rpcPrefs: { blockExplorerUrl: 'https://optimistic.etherscan.io' }, - rpcUrl: 'https://optimism-mainnet.infura.io/v3/12345', - ticker: 'ETH', + defaultRpcEndpointIndex: 0, + name: 'Optimism', + nativeCurrency: 'ETH', + rpcEndpoints: [ + { + networkClientId: 'networkId3', + type: 'infura', + url: 'https://optimism-mainnet.infura.io/v3/12345', + }, + ], }, - networkId4: { + '0x64': { + blockExplorerUrls: ['https://blockscout.com/xdai/mainnet/'], chainId: '0x64', - nickname: 'Gnosis Chain', - rpcPrefs: { - blockExplorerUrl: 'https://blockscout.com/xdai/mainnet/', - }, - rpcUrl: 'https://rpc.gnosischain.com/', - ticker: 'XDAI', + defaultRpcEndpointIndex: 0, + name: 'Gnosis Chain', + nativeCurrency: 'XDAI', + rpcEndpoints: [ + { + networkClientId: 'networkId4', + type: 'custom', + url: 'https://rpc.gnosischain.com/', + }, + ], }, }, }, @@ -171,29 +218,39 @@ describe('Network Selector', () => { backgroundState: { ...initialState.engine.backgroundState, NetworkController: { - ...initialState.engine.backgroundState.NetworkController, selectedNetworkClientId: 'sepolia', - networksMetadata: {}, - networkConfigurations: { - mainnet: { - id: 'mainnet', - rpcUrl: 'http://mainnet.infura.io', + networksMetadata: { + mainnet: { status: 'available', EIPS: { '1559': true } }, + sepolia: { status: 'available', EIPS: { '1559': true } }, + }, + networkConfigurationsByChainId: { + [CHAIN_IDS.MAINNET]: { + blockExplorerUrls: ['https://etherscan.com'], chainId: CHAIN_IDS.MAINNET, - ticker: 'ETH', - nickname: 'Ethereum Mainnet', - rpcPrefs: { - blockExplorerUrl: 'https://etherscan.com', - }, + defaultRpcEndpointIndex: 0, + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + rpcEndpoints: [ + { + networkClientId: 'mainnet', + type: 'infura', + url: 'http://mainnet.infura.io', + }, + ], }, - sepolia: { - id: 'sepolia', - rpcUrl: 'http://sepolia.infura.io', + [CHAIN_IDS.SEPOLIA]: { + blockExplorerUrls: ['https://etherscan.com'], chainId: CHAIN_IDS.SEPOLIA, - ticker: 'ETH', - nickname: 'Sepolia', - rpcPrefs: { - blockExplorerUrl: 'https://etherscan.com', - }, + defaultRpcEndpointIndex: 0, + name: 'Sepolia', + nativeCurrency: 'ETH', + rpcEndpoints: [ + { + networkClientId: 'sepolia', + type: 'infura', + url: 'http://sepolia.infura.io', + }, + ], }, }, }, @@ -229,6 +286,43 @@ describe('Network Selector', () => { PreferencesController: { showTestNetworks: true, }, + NetworkController: { + selectedNetworkClientId: 'sepolia', + networksMetadata: { + mainnet: { status: 'available', EIPS: { '1559': true } }, + sepolia: { status: 'available', EIPS: { '1559': true } }, + }, + networkConfigurationsByChainId: { + [CHAIN_IDS.MAINNET]: { + blockExplorerUrls: ['https://etherscan.com'], + chainId: CHAIN_IDS.MAINNET, + defaultRpcEndpointIndex: 0, + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + rpcEndpoints: [ + { + networkClientId: 'mainnet', + type: 'infura', + url: 'http://mainnet.infura.io', + }, + ], + }, + [CHAIN_IDS.SEPOLIA]: { + blockExplorerUrls: ['https://etherscan.com'], + chainId: CHAIN_IDS.SEPOLIA, + defaultRpcEndpointIndex: 0, + name: 'Sepolia', + nativeCurrency: 'ETH', + rpcEndpoints: [ + { + networkClientId: 'sepolia', + type: 'infura', + url: 'http://sepolia.infura.io', + }, + ], + }, + }, + }, }, }, }); @@ -248,8 +342,12 @@ describe('Network Selector', () => { backgroundState: { ...initialState.engine.backgroundState, NetworkController: { - ...initialState.engine.backgroundState.NetworkController, - networkConfigurations: {}, + ...mockNetworkState({ + chainId: '0x1', + id: 'Mainnet', + nickname: 'Ethereum Main Network', + ticker: 'ETH', + }), }, }, }, @@ -289,70 +387,4 @@ describe('Network Selector', () => { fireEvent.press(rpcOption); }); }); - - // Add this test for selecting between two Polygon networks - it('should select only one Polygon network when two networks with different RPC URLs exist', async () => { - jest.clearAllMocks(); // Clears mock data, ensuring that no mock has been called - jest.resetAllMocks(); // Resets mock implementation and mock instances - - const customState = { - ...initialState, - engine: { - backgroundState: { - ...initialState.engine.backgroundState, - NetworkController: { - networkConfigurations: { - polygonNetwork1: { - chainId: '0x89', // Polygon Mainnet - nickname: 'Polygon Mainnet 1', - rpcUrl: 'https://polygon-mainnet-1.rpc', - ticker: 'POL', - }, - polygonNetwork2: { - chainId: '0x89', // Polygon Mainnet (same chainId, different RPC URL) - nickname: 'Polygon Mainnet 2', - rpcUrl: 'https://polygon-mainnet-2.rpc', - ticker: 'POL', - }, - }, - }, - }, - }, - }; - - ( - Engine.context.NetworkController.getNetworkClientById as jest.Mock - ).mockReturnValue({ - configuration: { - chainId: '0x89', // Polygon Mainnet - nickname: 'Polygon Mainnet 1', - rpcUrl: 'https://polygon-mainnet-1.rpc', - ticker: 'POL', - type: 'custom', - }, - }); - - const { getByText, queryByTestId } = renderComponent(customState); - - // Ensure both networks are rendered - const polygonNetwork1 = getByText('Polygon Mainnet 1'); - const polygonNetwork2 = getByText('Polygon Mainnet 2'); - expect(polygonNetwork1).toBeTruthy(); - expect(polygonNetwork2).toBeTruthy(); - - // Select the first network - fireEvent.press(polygonNetwork1); - - // Wait for the selection to be applied - await waitFor(() => { - const polygonNetwork1Selected = queryByTestId( - 'Polygon Mainnet 1-selected', - ); - expect(polygonNetwork1Selected).toBeTruthy(); - }); - - // Assert that the second network is NOT selected - const polygonNetwork2Selected = queryByTestId('Polygon Mainnet 2-selected'); - expect(polygonNetwork2Selected).toBeNull(); // Not selected - }); }); diff --git a/app/components/Views/NetworkSelector/NetworkSelector.tsx b/app/components/Views/NetworkSelector/NetworkSelector.tsx index 87e6b01901cd..8dbd03768f67 100644 --- a/app/components/Views/NetworkSelector/NetworkSelector.tsx +++ b/app/components/Views/NetworkSelector/NetworkSelector.tsx @@ -10,7 +10,6 @@ import React, { useCallback, useRef, useState } from 'react'; import { ScrollView } from 'react-native-gesture-handler'; import images from 'images/image-icons'; import { useNavigation, useRoute, RouteProp } from '@react-navigation/native'; -import { NetworkConfiguration } from '@metamask/network-controller'; // External dependencies. import SheetHeader from '../../../component-library/components/Sheet/SheetHeader'; @@ -34,11 +33,11 @@ import { } from '../../../selectors/networkController'; import { selectShowTestNetworks } from '../../../selectors/preferencesController'; import Networks, { - compareRpcUrls, getAllNetworks, getDecimalChainId, isTestNet, getNetworkImageSource, + isMainNet, } from '../../../util/networks'; import { LINEA_MAINNET, @@ -68,6 +67,7 @@ import { useMetrics } from '../../../components/hooks/useMetrics'; // Internal dependencies import createStyles from './NetworkSelector.styles'; import { + BUILT_IN_NETWORKS, InfuraNetworkType, TESTNET_TICKER_SYMBOLS, } from '@metamask/controller-utils'; @@ -89,11 +89,10 @@ import { Hex } from '@metamask/utils'; import ListItemSelect from '../../../component-library/components/List/ListItemSelect'; import hideProtocolFromUrl from '../../../util/hideProtocolFromUrl'; import { CHAIN_IDS } from '@metamask/transaction-controller'; -import { - LINEA_DEFAULT_RPC_URL, - MAINNET_DEFAULT_RPC_URL, -} from '../../../constants/urls'; +import { LINEA_DEFAULT_RPC_URL } from '../../../constants/urls'; import { useNetworkInfo } from '../../../selectors/selectedNetworkController'; +import { NetworkConfiguration } from '@metamask/network-controller'; +import Logger from '../../../util/Logger'; interface infuraNetwork { name: string; @@ -104,7 +103,7 @@ interface infuraNetwork { interface ShowConfirmDeleteModalState { isVisible: boolean; networkName: string; - entry?: [string, NetworkConfiguration & { id: string }]; + chainId?: `0x${string}`; } interface NetworkSelectorRouteParams { @@ -158,7 +157,6 @@ const NetworkSelector = () => { useState({ isVisible: false, networkName: '', - entry: undefined, }); const [showNetworkMenuModal, setNetworkMenuModal] = useState({ @@ -169,14 +167,49 @@ const NetworkSelector = () => { isReadOnly: false, }); + const onRpcSelect = useCallback( + async (clientId: string, chainId: `0x${string}`) => { + const { NetworkController } = Engine.context; + + const existingNetwork = networkConfigurations[chainId]; + if (!existingNetwork) { + Logger.error( + new Error(`No existing network found for chainId: ${chainId}`), + ); + return; + } + + const indexOfRpc = existingNetwork.rpcEndpoints.findIndex( + ({ networkClientId }) => clientId === networkClientId, + ); + + if (indexOfRpc === -1) { + Logger.error( + new Error( + `RPC endpoint with clientId: ${clientId} not found for chainId: ${chainId}`, + ), + ); + return; + } + + // Proceed to update the network with the correct index + await NetworkController.updateNetwork(existingNetwork.chainId, { + ...existingNetwork, + defaultRpcEndpointIndex: indexOfRpc, + }); + + // Set the active network + NetworkController.setActiveNetwork(clientId); + }, + [networkConfigurations], + ); + const [showMultiRpcSelectModal, setShowMultiRpcSelectModal] = useState<{ isVisible: boolean; chainId: string; - rpcUrls: string[]; networkName: string; }>({ isVisible: false, - rpcUrls: [], chainId: CHAIN_IDS.MAINNET, networkName: '', }); @@ -207,8 +240,18 @@ const NetworkSelector = () => { ticker = TESTNET_TICKER_SYMBOLS.SEPOLIA as InfuraNetworkType; } + const networkConfiguration = + NetworkController.getNetworkConfigurationByChainId( + BUILT_IN_NETWORKS[type].chainId, + ); + + const clientId = + networkConfiguration?.rpcEndpoints[ + networkConfiguration.defaultRpcEndpointIndex + ].networkClientId ?? type; + CurrencyRateController.updateExchangeRate(ticker); - NetworkController.setProviderType(type); + NetworkController.setActiveNetwork(clientId); AccountTrackerController.refresh(); setTimeout(async () => { @@ -225,20 +268,24 @@ const NetworkSelector = () => { }); }; - const onSetRpcTarget = async (rpcTarget: string) => { + const onSetRpcTarget = async (networkConfiguration: NetworkConfiguration) => { const { CurrencyRateController, NetworkController, SelectedNetworkController, } = Engine.context; - const entry = Object.entries(networkConfigurations).find(([, { rpcUrl }]) => - compareRpcUrls(rpcUrl, rpcTarget), - ); + if (networkConfiguration) { + const { + name: nickname, + chainId, + nativeCurrency: ticker, + rpcEndpoints, + defaultRpcEndpointIndex, + } = networkConfiguration; - if (entry) { - const [networkConfigurationId, networkConfiguration] = entry; - const { ticker, nickname } = networkConfiguration; + const networkConfigurationId = + rpcEndpoints[defaultRpcEndpointIndex].networkClientId; if (domainIsConnectedDapp && process.env.MULTICHAIN_V1) { SelectedNetworkController.setNetworkClientIdForDomain( @@ -247,22 +294,24 @@ const NetworkSelector = () => { ); } else { CurrencyRateController.updateExchangeRate(ticker); - NetworkController.setActiveNetwork(networkConfigurationId); + + const { networkClientId } = rpcEndpoints[defaultRpcEndpointIndex]; + + await NetworkController.setActiveNetwork(networkClientId); } sheetRef.current?.onCloseBottomSheet(); trackEvent(MetaMetricsEvents.NETWORK_SWITCHED, { - chain_id: getDecimalChainId(providerConfig.chainId), + chain_id: getDecimalChainId(chainId), from_network: selectedNetworkName, to_network: nickname, }); } }; - const openRpcModal = useCallback(({ rpcUrls, chainId, networkName }) => { + const openRpcModal = useCallback(({ chainId, networkName }) => { setShowMultiRpcSelectModal({ isVisible: true, - rpcUrls: [...rpcUrls], chainId, networkName, }); @@ -272,7 +321,6 @@ const NetworkSelector = () => { const closeRpcModal = useCallback(() => { setShowMultiRpcSelectModal({ isVisible: false, - rpcUrls: [], chainId: CHAIN_IDS.MAINNET, networkName: '', }); @@ -367,6 +415,12 @@ const NetworkSelector = () => { const renderMainnet = () => { const { name: mainnetName, chainId } = Networks.mainnet; + const rpcUrl = + networkConfigurations?.[chainId]?.rpcEndpoints?.[ + networkConfigurations?.[chainId]?.defaultRpcEndpointIndex + ].url; + const name = networkConfigurations?.[chainId]?.name ?? mainnetName; + if (isNetworkUiRedesignEnabled() && isNoSearchResults(MAINNET)) return null; if (isNetworkUiRedesignEnabled()) { @@ -374,15 +428,15 @@ const NetworkSelector = () => { onNetworkChange(MAINNET)} style={styles.networkCell} buttonIcon={IconName.MoreVertical} @@ -391,14 +445,15 @@ const NetworkSelector = () => { openModal(chainId, false, MAINNET, true); }, }} - // TODO: Substitute with the new network controller's RPC array. onTextClick={() => openRpcModal({ - rpcUrls: [hideKeyFromUrl(MAINNET_DEFAULT_RPC_URL)], chainId, networkName: mainnetName, }) } + onLongPress={() => { + openModal(chainId, false, MAINNET, true); + }} /> ); } @@ -406,14 +461,14 @@ const NetworkSelector = () => { return ( onNetworkChange(MAINNET)} style={styles.networkCell} /> @@ -422,6 +477,7 @@ const NetworkSelector = () => { const renderLineaMainnet = () => { const { name: lineaMainnetName, chainId } = Networks['linea-mainnet']; + const name = networkConfigurations?.[chainId]?.name ?? lineaMainnetName; if (isNetworkUiRedesignEnabled() && isNoSearchResults('linea-mainnet')) return null; @@ -431,7 +487,7 @@ const NetworkSelector = () => { { openModal(chainId, false, LINEA_MAINNET, true); }, }} - // TODO: Substitute with the new network controller's RPC array. onTextClick={() => openRpcModal({ - rpcUrls: [LINEA_DEFAULT_RPC_URL], chainId, networkName: lineaMainnetName, }) } + onLongPress={() => { + openModal(chainId, false, LINEA_MAINNET, true); + }} /> ); } @@ -463,92 +520,106 @@ const NetworkSelector = () => { return ( onNetworkChange(LINEA_MAINNET)} /> ); }; const renderRpcNetworks = () => - Object.values(networkConfigurations).map( - ({ nickname, rpcUrl, chainId }) => { - if (!chainId) return null; - const { name } = { name: nickname || rpcUrl }; - - if (isNetworkUiRedesignEnabled() && isNoSearchResults(name)) - return null; - - //@ts-expect-error - The utils/network file is still JS and this function expects a networkType, and should be optional - const image = getNetworkImageSource({ chainId: chainId?.toString() }); - - if (isNetworkUiRedesignEnabled()) { - return ( - onSetRpcTarget(rpcUrl)} - style={styles.networkCell} - buttonIcon={IconName.MoreVertical} - secondaryText={hideProtocolFromUrl(hideKeyFromUrl(rpcUrl))} - buttonProps={{ - onButtonClick: () => { - openModal(chainId, true, rpcUrl, false); - }, - }} - // TODO: Substitute with the new network controller's RPC array. - onTextClick={() => - openRpcModal({ - rpcUrls: [hideKeyFromUrl(rpcUrl)], - chainId, - networkName: name, - }) - } - /> - ); - } + Object.values(networkConfigurations).map((networkConfiguration) => { + const { + name: nickname, + rpcEndpoints, + chainId, + defaultRpcEndpointIndex, + } = networkConfiguration; + if ( + !chainId || + isTestNet(chainId) || + isMainNet(chainId) || + chainId === CHAIN_IDS.LINEA_MAINNET || + chainId === CHAIN_IDS.GOERLI + ) { + return null; + } + + const rpcUrl = rpcEndpoints[defaultRpcEndpointIndex].url; + const rpcName = rpcEndpoints[defaultRpcEndpointIndex].name ?? rpcUrl; + + const name = nickname || rpcName; + + if (isNetworkUiRedesignEnabled() && isNoSearchResults(name)) return null; + + //@ts-expect-error - The utils/network file is still JS and this function expects a networkType, and should be optional + const image = getNetworkImageSource({ chainId: chainId?.toString() }); + if (isNetworkUiRedesignEnabled()) { return ( onSetRpcTarget(rpcUrl)} + isSelected={Boolean(chainId === selectedChainId && selectedRpcUrl)} + onPress={() => onSetRpcTarget(networkConfiguration)} style={styles.networkCell} - > - {Boolean( - chainId === selectedChainId && selectedRpcUrl === rpcUrl, - ) && } - + buttonIcon={IconName.MoreVertical} + secondaryText={hideProtocolFromUrl(hideKeyFromUrl(rpcUrl))} + buttonProps={{ + onButtonClick: () => { + openModal(chainId, true, rpcUrl, false); + }, + }} + onTextClick={() => + openRpcModal({ + chainId, + networkName: name, + }) + } + onLongPress={() => { + openModal(chainId, true, rpcUrl, false); + }} + /> ); - }, - ); + } + + return ( + onSetRpcTarget(networkConfiguration)} + style={styles.networkCell} + > + {Boolean( + chainId === selectedChainId && selectedRpcUrl === rpcUrl, + ) && } + + ); + }); const renderOtherNetworks = () => { const getAllNetworksTyped = @@ -561,6 +632,15 @@ const NetworkSelector = () => { >; const { name, imageSource, chainId } = TypedNetworks[networkType]; + const networkConfiguration = Object.values(networkConfigurations).find( + ({ chainId: networkId }) => networkId === chainId, + ); + + const rpcUrl = + networkConfiguration?.rpcEndpoints?.[ + networkConfiguration?.defaultRpcEndpointIndex + ].url; + if (isNetworkUiRedesignEnabled() && isNoSearchResults(name)) return null; if (isNetworkUiRedesignEnabled()) { @@ -568,6 +648,7 @@ const NetworkSelector = () => { { openModal(chainId, false, networkType, true); }, }} + onTextClick={() => + openRpcModal({ + chainId, + networkName: name, + }) + } + onLongPress={() => { + openModal(chainId, false, networkType, true); + }} /> ); } @@ -704,39 +794,34 @@ const NetworkSelector = () => { setSearchString(''); }; - const removeRpcUrl = (networkId: string) => { - const entry = Object.entries(networkConfigurations).find( - ([, { chainId }]) => chainId === networkId, + const removeRpcUrl = (chainId: string) => { + const networkConfiguration = Object.values(networkConfigurations).find( + (config) => config.chainId === chainId, ); - if (!entry) { - throw new Error(`Unable to find network with chain id ${networkId}`); + if (!networkConfiguration) { + throw new Error(`Unable to find network with chain id ${chainId}`); } - const [, { nickname }] = entry; - closeModal(); closeRpcModal(); setShowConfirmDeleteModal({ isVisible: true, - networkName: nickname ?? '', - entry, + networkName: networkConfiguration.name ?? '', + chainId: networkConfiguration.chainId, }); }; const confirmRemoveRpc = () => { - if (showConfirmDeleteModal.entry) { - const [networkConfigurationId] = showConfirmDeleteModal.entry; - + if (showConfirmDeleteModal.chainId) { + const { chainId } = showConfirmDeleteModal; const { NetworkController } = Engine.context; - - NetworkController.removeNetworkConfiguration(networkConfigurationId); + NetworkController.removeNetwork(chainId); setShowConfirmDeleteModal({ isVisible: false, networkName: '', - entry: undefined, }); } }; @@ -771,6 +856,11 @@ const NetworkSelector = () => { if (!showMultiRpcSelectModal.isVisible) return null; + const chainId = showMultiRpcSelectModal.chainId; + + const rpcEndpoints = + networkConfigurations[chainId as `0x${string}`]?.rpcEndpoints || []; + return ( { - {showMultiRpcSelectModal.rpcUrls.map((rpcUrl) => ( + {rpcEndpoints.map(({ url, networkClientId }, index) => ( { + onRpcSelect(networkClientId, chainId as `0x${string}`); + closeRpcModal(); + }} > - {hideProtocolFromUrl(rpcUrl)} + {hideKeyFromUrl(hideProtocolFromUrl(url))} @@ -817,7 +916,14 @@ const NetworkSelector = () => { ); - }, [showMultiRpcSelectModal, rpcMenuSheetRef, closeRpcModal, styles]); + }, [ + showMultiRpcSelectModal, + rpcMenuSheetRef, + closeRpcModal, + styles, + networkConfigurations, + onRpcSelect, + ]); const renderBottomSheetContent = () => ( <> @@ -896,17 +1002,15 @@ const NetworkSelector = () => { > { - navigate(Routes.ADD_NETWORK, { - shouldNetworkSwitchPopToWallet: false, - shouldShowPopularNetworks: false, - network: showNetworkMenuModal.networkTypeOrRpcUrl, + sheetRef.current?.onCloseBottomSheet(() => { + navigate(Routes.ADD_NETWORK, { + shouldNetworkSwitchPopToWallet: false, + shouldShowPopularNetworks: false, + network: showNetworkMenuModal.networkTypeOrRpcUrl, + }); }); }} /> @@ -916,6 +1020,7 @@ const NetworkSelector = () => { actionTitle={strings('app_settings.delete')} iconName={IconName.Trash} onPress={() => removeRpcUrl(showNetworkMenuModal.chainId)} + testID={`delete-network-button-${showNetworkMenuModal.chainId}`} /> ) : null} diff --git a/app/components/Views/Notifications/Details/Footers/BlockExplorerFooter.tsx b/app/components/Views/Notifications/Details/Footers/BlockExplorerFooter.tsx index 905e9846addd..8e7cfa8cd30f 100644 --- a/app/components/Views/Notifications/Details/Footers/BlockExplorerFooter.tsx +++ b/app/components/Views/Notifications/Details/Footers/BlockExplorerFooter.tsx @@ -32,7 +32,7 @@ export default function BlockExplorerFooter(props: BlockExplorerFooterProps) { const hexChainId = toHex(props.chainId); return Object.values(networkConfigurations).find( (networkConfig) => networkConfig.chainId === hexChainId, - )?.rpcPrefs?.blockExplorerUrl; + )?.blockExplorerUrls?.[0]; }, [networkConfigurations, props.chainId]); const url = networkBlockExplorer ?? defaultBlockExplorer; diff --git a/app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/__snapshots__/index.test.tsx.snap b/app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/__snapshots__/index.test.tsx.snap index cb7709a24ac0..3b81aeecbadc 100644 --- a/app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/__snapshots__/index.test.tsx.snap +++ b/app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/__snapshots__/index.test.tsx.snap @@ -731,6 +731,864 @@ exports[`OnboardingAssetSettings should render correctly 1`] = ` + + + + + + + + Mainnet + + + etherscan.io + + + + + + + + + + + + G + + + + + Goerli + + + etherscan.io + + + + + + + + + + + + + + + Sepolia + + + etherscan.io + + + + + + + + + + + + L + + + + + Linea Goerli + + + lineascan.build + + + + + + + + + + + + + + + Linea Sepolia + + + lineascan.build + + + + + + + + + + + + + + + Linea Mainnet + + + lineascan.build + + + + + + + + + + + + + + + Mainnet + + + etherscan.io + + + + + + + + + + + + G + + + + + Goerli + + + etherscan.io + + + + + + + + + + + + + + + Sepolia + + + etherscan.io + + + + + + + + + + + + L + + + + + Linea Goerli + + + lineascan.build + + + + + + + + + + + + + + + Linea Sepolia + + + lineascan.build + + + + + + + + + + + + + + + Linea Mainnet + + + lineascan.build + + + + + + + `; diff --git a/app/components/Views/Settings/IncomingTransactionsSettings/index.tsx b/app/components/Views/Settings/IncomingTransactionsSettings/index.tsx index 34e0e70fa359..9bdc4278829f 100644 --- a/app/components/Views/Settings/IncomingTransactionsSettings/index.tsx +++ b/app/components/Views/Settings/IncomingTransactionsSettings/index.tsx @@ -84,7 +84,8 @@ const IncomingTransactionsSettings = () => { const renderRpcNetworks = () => Object.values(networkConfigurations).map( - ({ nickname, rpcUrl, chainId }) => { + ({ name: nickname, rpcEndpoints, chainId, defaultRpcEndpointIndex }) => { + const rpcUrl = rpcEndpoints[defaultRpcEndpointIndex].url; if (!chainId || !Object.keys(supportedNetworks).includes(chainId)) return null; const { name } = { name: nickname || rpcUrl }; diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js index 94b41f1050f9..e2ee56493dca 100644 --- a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js +++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js @@ -10,6 +10,7 @@ import { } from 'react-native'; import { connect } from 'react-redux'; import { typography } from '@metamask/design-tokens'; +import isUrl from 'is-url'; import { fontStyles, colors as staticColors, @@ -21,7 +22,6 @@ import Networks, { getAllNetworks, getIsNetworkOnboarded, } from '../../../../../util/networks'; -import { getEtherscanBaseUrl } from '../../../../../util/etherscan'; import Engine from '../../../../../core/Engine'; import { isWebUri } from 'valid-url'; import URL from 'url-parse'; @@ -34,15 +34,8 @@ import AppConstants from '../../../../../core/AppConstants'; import ScrollableTabView from 'react-native-scrollable-tab-view'; import DefaultTabBar from 'react-native-scrollable-tab-view/DefaultTabBar'; import { PopularList } from '../../../../../util/networks/customNetworks'; -import WarningMessage from '../../../confirmations/SendFlow/WarningMessage'; import InfoModal from '../../../../UI/Swaps/components/InfoModal'; -import { - DEFAULT_MAINNET_CUSTOM_NAME, - MAINNET, - NETWORKS_CHAIN_ID, - PRIVATENETWORK, - RPC, -} from '../../../../../constants/network'; +import { PRIVATENETWORK, RPC } from '../../../../../constants/network'; import { ThemeContext, mockTheme } from '../../../../../util/theme'; import { showNetworkOnboardingAction } from '../../../../../actions/onboardNetwork'; import sanitizeUrl, { @@ -82,12 +75,34 @@ import Icon, { IconSize, } from '../../../../../component-library/components/Icons/Icon'; import { isNetworkUiRedesignEnabled } from '../../../../../util/networks/isNetworkUiRedesignEnabled'; +import Cell, { + CellVariant, +} from '../../../../../component-library/components/Cells/Cell'; +import BottomSheet from '../../../../../component-library/components/BottomSheets/BottomSheet'; +import BottomSheetHeader from '../../../../../component-library/components/BottomSheets/BottomSheetHeader'; +import { TextVariant } from '../../../../../component-library/components/Texts/Text'; +import ButtonLink from '../../../../../component-library/components/Buttons/Button/variants/ButtonLink'; +import ButtonPrimary from '../../../../../component-library/components/Buttons/Button/variants/ButtonPrimary'; +import { RpcEndpointType } from '@metamask/network-controller'; +import { AvatarVariant } from '../../../../../component-library/components/Avatars/Avatar'; const createStyles = (colors) => StyleSheet.create({ base: { paddingHorizontal: 16, }, + addRpcButton: { + alignSelf: 'center', + }, + addRpcNameButton: { + alignSelf: 'center', + paddingHorizontal: 16, + paddingVertical: 16, + width: '100%', + }, + rpcMenu: { + paddingHorizontal: 16, + }, wrapper: { backgroundColor: colors.background.default, flexGrow: 1, @@ -103,6 +118,11 @@ const createStyles = (colors) => flex: 1, paddingVertical: 12, }, + scrollWrapperOverlay: { + flex: 1, + paddingVertical: 12, + opacity: 0.5, + }, onboardingInput: { borderColor: staticColors.transparent, padding: 0, @@ -115,6 +135,11 @@ const createStyles = (colors) => padding: 10, color: colors.text.default, }, + dropDownInput: { + borderColor: colors.border.default, + borderRadius: 5, + borderWidth: 2, + }, inputWithError: { ...typography.sBodyMD, borderColor: colors.error.default, @@ -151,6 +176,12 @@ const createStyles = (colors) => flexGrow: 1, flexShrink: 1, }, + heading: { + fontSize: 16, + paddingVertical: 12, + color: colors.text.default, + ...fontStyles.bold, + }, label: { fontSize: 14, paddingVertical: 12, @@ -266,8 +297,9 @@ const createStyles = (colors) => }); const allNetworks = getAllNetworks(); -const allNetworksblockExplorerUrl = (networkName) => - `https://${networkName}.infura.io/v3/`; + +const InfuraKey = process.env.MM_INFURA_PROJECT_ID; +const infuraProjectId = InfuraKey === 'null' ? '' : InfuraKey; /** * Main view for app configurations @@ -320,6 +352,12 @@ export class NetworkSettings extends PureComponent { state = { rpcUrl: undefined, + rpcName: undefined, + rpcUrlFrom: undefined, + rpcNameForm: '', + rpcUrls: [], + blockExplorerUrls: [], + selectedRpcEndpointIndex: 0, blockExplorerUrl: undefined, nickname: undefined, chainId: undefined, @@ -344,15 +382,37 @@ export class NetworkSettings extends PureComponent { isRpcUrlFieldFocused: false, isChainIdFieldFocused: false, networkList: [], + showMultiRpcAddModal: { + isVisible: false, + }, + showMultiBlockExplorerAddModal: { + isVisible: false, + }, + showAddRpcForm: { + isVisible: false, + }, + showAddBlockExplorerForm: { + isVisible: false, + }, }; inputRpcURL = React.createRef(); + inputNameRpcURL = React.createRef(); inputChainId = React.createRef(); inputSymbol = React.createRef(); inputBlockExplorerURL = React.createRef(); + rpcAddMenuSheetRef = React.createRef(); + addBlockExplorerMenuSheetRef = React.createRef(); + rpcAddFormSheetRef = React.createRef(); + blockExplorerAddFormSheetRef = React.createRef(); getOtherNetworks = () => allNetworks.slice(1); + templateInfuraRpc = (endpoint) => + endpoint.endsWith('{infuraProjectId}') + ? endpoint.replace('{infuraProjectId}', infuraProjectId ?? '') + : endpoint; + updateNavBar = () => { const { navigation, route } = this.props; const isCustomMainnet = route.params?.isCustomMainnet; @@ -369,65 +429,98 @@ export class NetworkSettings extends PureComponent { ); }; - /** - * Gets the custom mainnet RPC URL from the frequent RPC list. - * - * @returns Custom mainnet RPC URL. - */ - getCustomMainnetRPCURL = () => { - const { networkConfigurations } = this.props; - const networkConfiguration = Object.values(networkConfigurations).find( - ({ chainId: id }) => String(id) === String(Networks.mainnet.chainId), - ); - return networkConfiguration?.rpcUrl || ''; - }; - componentDidMount = () => { this.updateNavBar(); const { route, networkConfigurations } = this.props; - const isCustomMainnet = route.params?.isCustomMainnet; const networkTypeOrRpcUrl = route.params?.network; // if network is main, don't show popular network - let blockExplorerUrl, chainId, nickname, ticker, editable, rpcUrl; + let blockExplorerUrl, + chainId, + nickname, + ticker, + editable, + rpcUrl, + rpcUrls, + blockExplorerUrls, + rpcName, + selectedRpcEndpointIndex; // If no navigation param, user clicked on add network if (networkTypeOrRpcUrl) { if (allNetworks.find((net) => networkTypeOrRpcUrl === net)) { - blockExplorerUrl = getEtherscanBaseUrl(networkTypeOrRpcUrl); const networkInformation = Networks[networkTypeOrRpcUrl]; - nickname = networkInformation.name; chainId = networkInformation.chainId.toString(); + + nickname = networkConfigurations?.[chainId]?.name; editable = false; - rpcUrl = allNetworksblockExplorerUrl(networkTypeOrRpcUrl); - ticker = ![ - NETWORKS_CHAIN_ID.LINEA_GOERLI, - NETWORKS_CHAIN_ID.LINEA_SEPOLIA, - ].includes(networkInformation.chainId.toString()) - ? strings('unit.eth') - : 'LineaETH'; - // Override values if UI is updating custom mainnet RPC URL. - if (isCustomMainnet) { - nickname = DEFAULT_MAINNET_CUSTOM_NAME; - rpcUrl = this.getCustomMainnetRPCURL(); - } + blockExplorerUrl = + networkConfigurations?.[chainId]?.blockExplorerUrls[ + networkConfigurations?.[chainId]?.defaultBlockExplorerUrlIndex + ]; + rpcUrl = + networkConfigurations?.[chainId]?.rpcEndpoints[ + networkConfigurations?.[chainId]?.defaultRpcEndpointIndex + ]?.url; + rpcName = + networkConfigurations?.[chainId]?.rpcEndpoints[ + networkConfigurations?.[chainId]?.defaultRpcEndpointIndex + ]?.type ?? + networkConfigurations?.[chainId]?.rpcEndpoints[ + networkConfigurations?.[chainId]?.defaultRpcEndpointIndex + ]?.name; + rpcUrls = networkConfigurations?.[chainId]?.rpcEndpoints; + blockExplorerUrls = networkConfigurations?.[chainId]?.blockExplorerUrls; + + ticker = networkConfigurations?.[chainId]?.nativeCurrency; } else { const networkConfiguration = Object.values(networkConfigurations).find( - ({ rpcUrl }) => rpcUrl === networkTypeOrRpcUrl, + ({ rpcEndpoints, defaultRpcEndpointIndex }) => + rpcEndpoints[defaultRpcEndpointIndex].url === networkTypeOrRpcUrl || + rpcEndpoints[defaultRpcEndpointIndex].networkClientId === + networkTypeOrRpcUrl, ); - nickname = networkConfiguration.nickname; - chainId = networkConfiguration.chainId; + nickname = networkConfiguration?.name; + chainId = networkConfiguration?.chainId; blockExplorerUrl = - networkConfiguration.rpcPrefs && - networkConfiguration.rpcPrefs.blockExplorerUrl; - ticker = networkConfiguration.ticker; + networkConfiguration?.blockExplorerUrls[ + networkConfiguration?.defaultBlockExplorerUrlIndex + ]; + ticker = networkConfiguration?.nativeCurrency; editable = true; - rpcUrl = networkTypeOrRpcUrl; + rpcUrl = + networkConfigurations?.[chainId]?.rpcEndpoints[ + networkConfigurations?.[chainId]?.defaultRpcEndpointIndex + ]?.url; + rpcUrls = networkConfiguration?.rpcEndpoints; + blockExplorerUrls = networkConfiguration?.blockExplorerUrls; + rpcName = + networkConfiguration?.rpcEndpoints[ + networkConfiguration?.defaultRpcEndpointIndex + ]?.name ?? + networkConfiguration?.rpcEndpoints[ + networkConfiguration?.defaultRpcEndpointIndex + ]?.type; + + selectedRpcEndpointIndex = + networkConfiguration?.defaultRpcEndpointIndex; } + const initialState = - rpcUrl + blockExplorerUrl + nickname + chainId + ticker + editable; + rpcUrl + + blockExplorerUrl + + nickname + + chainId + + ticker + + editable + + rpcUrls + + blockExplorerUrls; this.setState({ rpcUrl, + rpcName, + rpcUrls, + blockExplorerUrls, + selectedRpcEndpointIndex, blockExplorerUrl, nickname, chainId, @@ -476,6 +569,12 @@ export class NetworkSettings extends PureComponent { return parseInt(chainId, 16).toString(10); } + isAnyModalVisible = () => + this.state.showMultiRpcAddModal.isVisible || + this.state.showMultiBlockExplorerAddModal.isVisible || + this.state.showAddRpcForm.isVisible || + this.state.showAddBlockExplorerForm.isVisible; + validateRpcAndChainId = () => { const { rpcUrl, chainId } = this.state; @@ -513,7 +612,10 @@ export class NetworkSettings extends PureComponent { let providerError; try { - endpointChainId = await jsonRpcRequest(rpcUrl, 'eth_chainId'); + endpointChainId = await jsonRpcRequest( + this.templateInfuraRpc(rpcUrl), + 'eth_chainId', + ); } catch (err) { Logger.error(err, 'Failed to fetch the chainId from the endpoint.'); providerError = err; @@ -592,43 +694,113 @@ export class NetworkSettings extends PureComponent { return []; }; + checkIfNetworkNotExistsByChainId = async (chainId) => + Object.values(this.props.networkConfigurations).filter( + (item) => item.chainId !== chainId, + ); + + handleNetworkUpdate = async ({ + rpcUrl, + chainId, + nickname, + ticker, + blockExplorerUrl, + blockExplorerUrls, + rpcUrls, + isNetworkExists, + isCustomMainnet, + shouldNetworkSwitchPopToWallet, + navigation, + }) => { + const { NetworkController, CurrencyRateController } = Engine.context; + + const url = new URL(rpcUrl); + if (!isPrivateConnection(url.hostname)) { + url.set('protocol', 'https:'); + } + + CurrencyRateController.updateExchangeRate(ticker); + const existingNetwork = this.props.networkConfigurations[chainId]; + + const indexRpc = rpcUrls.findIndex(({ url }) => url === rpcUrl); + + const blockExplorerIndex = blockExplorerUrls.findIndex( + (url) => url === blockExplorerUrl, + ); + + const networkConfig = { + blockExplorerUrls, + chainId, + rpcEndpoints: rpcUrls, + nativeCurrency: ticker, + name: nickname, + defaultRpcEndpointIndex: indexRpc, + defaultBlockExplorerUrlIndex: + blockExplorerIndex !== -1 ? blockExplorerIndex : undefined, + }; + + if (isNetworkExists.length === 0) { + await NetworkController.updateNetwork( + existingNetwork.chainId, + networkConfig, + existingNetwork.chainId === chainId + ? { + replacementSelectedRpcEndpointIndex: indexRpc, + } + : undefined, + ); + } else { + await NetworkController.addNetwork({ + ...networkConfig, + }); + } + + isCustomMainnet + ? navigation.navigate('OptinMetrics') + : shouldNetworkSwitchPopToWallet + ? navigation.navigate('WalletView') + : navigation.goBack(); + }; + /** * Add or update network configuration, then switch networks */ addRpcUrl = async () => { - const { NetworkController, CurrencyRateController } = Engine.context; const { rpcUrl, chainId: stateChainId, nickname, + blockExplorerUrls, blockExplorerUrl, - editable, enableAction, + rpcUrls, + addMode, + editable, } = this.state; + const ticker = this.state.ticker && this.state.ticker.toUpperCase(); const { navigation, networkOnboardedState, route } = this.props; const isCustomMainnet = route.params?.isCustomMainnet; - // This must be defined before NetworkController.upsertNetworkConfiguration. - const prevRPCURL = isCustomMainnet - ? this.getCustomMainnetRPCURL() - : route.params?.network; const shouldNetworkSwitchPopToWallet = route.params?.shouldNetworkSwitchPopToWallet ?? true; // Check if CTA is disabled const isCtaDisabled = - !enableAction || - this.disabledByRpcUrl() || - this.disabledByChainId() || - this.disabledBySymbol(); + !enableAction || this.disabledByChainId() || this.disabledBySymbol(); if (isCtaDisabled) { return; } + // Conditionally check existence of network (Only check in Add Mode) - const isNetworkExists = editable - ? [] - : await this.checkIfNetworkExists(rpcUrl); + let isNetworkExists; + if (isNetworkUiRedesignEnabled()) { + isNetworkExists = addMode + ? await this.checkIfNetworkNotExistsByChainId(stateChainId) + : []; + } else { + isNetworkExists = editable ? [] : await this.checkIfNetworkExists(rpcUrl); + } const isOnboarded = getIsNetworkOnboarded( stateChainId, @@ -653,70 +825,30 @@ export class NetworkSettings extends PureComponent { return; } - if (this.validateRpcUrl() && isNetworkExists.length === 0) { - const url = new URL(rpcUrl); - - !isPrivateConnection(url.hostname) && url.set('protocol', 'https:'); - CurrencyRateController.updateExchangeRate(ticker); - // Remove trailing slashes - NetworkController.upsertNetworkConfiguration( - { - rpcUrl: url.href, - chainId, - ticker, - nickname, - rpcPrefs: { - blockExplorerUrl, - }, - }, - { - setActive: true, - // Metrics-related properties required, but the metric event is a no-op - // TODO: Use events for controller metric events - referrer: 'ignored', - source: 'ignored', - }, - ); - // TODO: Use network configuration ID to update existing entries - // Temporary solution is to manually remove the existing network using the old RPC URL. - const isRPCDifferent = url.href !== prevRPCURL; - if ((editable || isCustomMainnet) && isRPCDifferent) { - // Only remove from frequent list if RPC URL is different. - const foundNetworkConfiguration = Object.entries( - this.props.networkConfigurations, - ).find( - ([, networkConfiguration]) => - networkConfiguration.rpcUrl === prevRPCURL, - ); - - if (foundNetworkConfiguration) { - const [prevNetworkConfigurationId] = foundNetworkConfiguration; - NetworkController.removeNetworkConfiguration( - prevNetworkConfigurationId, - ); - } - } - - this.props.showNetworkOnboardingAction({ - networkUrl, - networkType, - nativeToken, - showNetworkOnboarding, - }); - isCustomMainnet - ? navigation.navigate('OptinMetrics') - : shouldNetworkSwitchPopToWallet - ? navigation.navigate('WalletView') - : navigation.goBack(); - } + await this.handleNetworkUpdate({ + rpcUrl, + chainId, + nickname, + ticker, + blockExplorerUrl, + blockExplorerUrls, + rpcUrls, + isNetworkExists, + isCustomMainnet, + shouldNetworkSwitchPopToWallet, + navigation, + nativeToken, + networkType, + networkUrl, + showNetworkOnboarding, + }); }; /** * Validates rpc url, setting a warningRpcUrl if is invalid * It also changes validatedRpcURL to true, indicating that was validated */ - validateRpcUrl = async () => { - const { rpcUrl } = this.state; + validateRpcUrl = async (rpcUrl) => { const isNetworkExists = await this.checkIfNetworkExists(rpcUrl); if (!isWebUri(rpcUrl)) { const appendedRpc = `http://${rpcUrl}`; @@ -766,7 +898,6 @@ export class NetworkSettings extends PureComponent { */ validateChainId = async () => { const { chainId, rpcUrl, editable } = this.state; - const isChainIdExists = await this.checkIfChainIdExists(chainId); const isNetworkExists = await this.checkIfNetworkExists(rpcUrl); @@ -838,7 +969,10 @@ export class NetworkSettings extends PureComponent { let endpointChainId; let providerError; try { - endpointChainId = await jsonRpcRequest(rpcUrl, 'eth_chainId'); + endpointChainId = await jsonRpcRequest( + this.templateInfuraRpc(rpcUrl), + 'eth_chainId', + ); } catch (err) { Logger.error(err, 'Failed to fetch the chainId from the endpoint.'); providerError = err; @@ -928,10 +1062,18 @@ export class NetworkSettings extends PureComponent { chainId, ticker, editable, + rpcUrls, initialState, } = this.state; const actualState = - rpcUrl + blockExplorerUrl + nickname + chainId + ticker + editable; + rpcUrl + + blockExplorerUrl + + nickname + + chainId + + ticker + + editable + + rpcUrls; + let enableAction; // If concstenation of parameters changed, user changed something so we are going to enable the action button if (actualState !== initialState) { @@ -942,17 +1084,6 @@ export class NetworkSettings extends PureComponent { this.setState({ enableAction }); }; - /** - * Returns if action button should be disabled because of the rpc url - * No rpc url set or rpc url set but, rpc url has not been validated yet or there is a warning for rpc url - */ - disabledByRpcUrl = () => { - const { rpcUrl, validatedRpcURL, warningRpcUrl } = this.state; - return ( - !rpcUrl || (rpcUrl && (!validatedRpcURL || warningRpcUrl !== undefined)) - ); - }; - /** * Returns if action button should be disabled because of the rpc url * Chain ID set but, chain id has not been validated yet or there is a warning for chain id @@ -982,7 +1113,68 @@ export class NetworkSettings extends PureComponent { return false; }; + onRpcUrlAdd = async (url) => { + await this.setState({ + rpcUrlForm: url, + validatedRpcURL: false, + warningRpcUrl: undefined, + warningChainId: undefined, + warningSymbol: undefined, + warningName: undefined, + }); + this.validateRpcUrl(this.state.rpcUrlForm); + }; + + onRpcNameAdd = async (name) => { + await this.setState({ + rpcNameForm: name, + }); + }; + + onRpcItemAdd = async (url, name) => { + if (!url) { + return; + } + + const rpcName = name ?? ''; + + await this.setState((prevState) => ({ + rpcUrls: [ + ...prevState.rpcUrls, + { url, name: rpcName, type: RpcEndpointType.Custom }, + ], + })); + + await this.setState({ + rpcUrl: url, + rpcName: name, + }); + + this.closeAddRpcForm(); + this.closeRpcModal(); + this.getCurrentState(); + }; + + onBlockExplorerItemAdd = async (url) => { + if (!url) { + return; + } + + await this.setState((prevState) => ({ + blockExplorerUrls: [...prevState.blockExplorerUrls, url], + })); + + await this.setState({ + blockExplorerUrl: url, + }); + + this.closeAddBlockExplorerRpcForm(); + this.closeBlockExplorerModal(); + this.getCurrentState(); + }; + onRpcUrlChange = async (url) => { + const { addMode } = this.state; await this.setState({ rpcUrl: url, validatedRpcURL: false, @@ -991,6 +1183,78 @@ export class NetworkSettings extends PureComponent { warningSymbol: undefined, warningName: undefined, }); + + this.validateName(); + if (addMode) { + this.validateChainId(); + } + this.validateSymbol(); + this.getCurrentState(); + }; + + onRpcUrlChangeWithName = async (url, name, type) => { + const nameToUse = name ?? type; + const { addMode } = this.state; + await this.setState({ + rpcUrl: url, + validatedRpcURL: false, + warningRpcUrl: undefined, + warningChainId: undefined, + warningSymbol: undefined, + warningName: undefined, + }); + + await this.setState({ + rpcName: nameToUse, + }); + + this.validateName(); + if (addMode) { + this.validateChainId(); + } + this.validateSymbol(); + this.getCurrentState(); + }; + + onBlockExplorerUrlChange = async (url) => { + const { addMode } = this.state; + await this.setState({ + blockExplorerUrl: url, + }); + + this.validateName(); + if (addMode) { + this.validateChainId(); + } + this.validateSymbol(); + this.getCurrentState(); + }; + + onRpcUrlDelete = async (url) => { + const { addMode } = this.state; + await this.setState((prevState) => ({ + rpcUrls: prevState.rpcUrls.filter((rpcUrl) => rpcUrl.url !== url), + })); + this.validateName(); + if (addMode) { + this.validateChainId(); + } + this.validateSymbol(); + this.getCurrentState(); + }; + + onBlockExplorerUrlDelete = async (url) => { + const { addMode } = this.state; + await this.setState((prevState) => ({ + blockExplorerUrls: prevState.blockExplorerUrls.filter( + (explorerUrl) => explorerUrl !== url, + ), + })); + this.validateName(); + if (addMode) { + this.validateChainId(); + } + this.validateSymbol(); this.getCurrentState(); }; @@ -1025,11 +1289,6 @@ export class NetworkSettings extends PureComponent { }); }; - onBlockExplorerUrlChange = async (blockExplorerUrl) => { - await this.setState({ blockExplorerUrl }); - this.getCurrentState(); - }; - onNameFocused = () => { this.setState({ isNameFieldFocused: true }); }; @@ -1079,11 +1338,61 @@ export class NetworkSettings extends PureComponent { current && current.focus(); }; + openAddRpcForm = () => { + this.setState({ showAddRpcForm: { isVisible: true } }); + this.rpcAddFormSheetRef.current?.onOpenBottomSheet(); + }; + + closeAddRpcForm = () => { + this.setState({ showAddRpcForm: { isVisible: false } }); + this.rpcAddFormSheetRef.current?.onCloseBottomSheet(); + }; + + openAddBlockExplorerForm = () => { + this.setState({ showAddBlockExplorerForm: { isVisible: true } }); + this.blockExplorerAddFormSheetRef.current?.onOpenBottomSheet(); + }; + + closeAddBlockExplorerRpcForm = () => { + this.setState({ showAddBlockExplorerForm: { isVisible: false } }); + this.blockExplorerAddFormSheetRef.current?.onCloseBottomSheet(); + }; + + closeRpcModal = () => { + this.setState({ + showMultiRpcAddModal: { isVisible: false }, + rpcUrlForm: '', + rpcNameForm: '', + }); + this.rpcAddMenuSheetRef.current?.onCloseBottomSheet(); + }; + + openRpcModal = () => { + this.setState({ showMultiRpcAddModal: { isVisible: true } }); + this.rpcAddMenuSheetRef.current?.onOpenBottomSheet(); + }; + + openBlockExplorerModal = () => { + this.setState({ showMultiBlockExplorerAddModal: { isVisible: true } }); + this.addBlockExplorerMenuSheetRef.current?.onOpenBottomSheet(); + }; + + closeBlockExplorerModal = () => { + this.setState({ showMultiBlockExplorerAddModal: { isVisible: false } }); + this.addBlockExplorerMenuSheetRef.current?.onCloseBottomSheet(); + }; + switchToMainnet = () => { const { NetworkController, CurrencyRateController } = Engine.context; + const { networkConfigurations } = this.props; + + const { networkClientId } = + networkConfigurations?.rpcEndpoints?.[ + networkConfigurations.defaultRpcEndpointIndex + ] ?? {}; CurrencyRateController.updateExchangeRate(NetworksTicker.mainnet); - NetworkController.setProviderType(MAINNET); + NetworkController.setActiveNetwork(networkClientId); setTimeout(async () => { await updateIncomingTransactions(); @@ -1101,14 +1410,18 @@ export class NetworkSettings extends PureComponent { } const entry = Object.entries(networkConfigurations).find( - ([, networkConfiguration]) => networkConfiguration.rpcUrl === rpcUrl, + ([, networkConfiguration]) => + networkConfiguration.rpcEndpoints[ + networkConfiguration.defaultRpcEndpointIndex + ].url === rpcUrl, ); + if (!entry) { throw new Error(`Unable to find network with RPC URL ${rpcUrl}`); } - const [networkConfigurationId] = entry; + const [, networkConfiguration] = entry; const { NetworkController } = Engine.context; - NetworkController.removeNetworkConfiguration(networkConfigurationId); + NetworkController.removeNetwork(networkConfiguration.chainId); navigation.goBack(); }; @@ -1138,6 +1451,8 @@ export class NetworkSettings extends PureComponent { customNetwork = (networkTypeOrRpcUrl) => { const { rpcUrl, + rpcUrls, + blockExplorerUrls, blockExplorerUrl, nickname, chainId, @@ -1154,8 +1469,15 @@ export class NetworkSettings extends PureComponent { isSymbolFieldFocused, isRpcUrlFieldFocused, isChainIdFieldFocused, + showMultiRpcAddModal, + showMultiBlockExplorerAddModal, + showAddRpcForm, + showAddBlockExplorerForm, + rpcUrlForm, + rpcNameForm, + rpcName, } = this.state; - const { route } = this.props; + const { route, networkConfigurations } = this.props; const isCustomMainnet = route.params?.isCustomMainnet; const colors = this.context.colors || mockTheme.colors; const themeAppearance = @@ -1221,10 +1543,7 @@ export class NetworkSettings extends PureComponent { const isRPCEditable = isCustomMainnet || editable; const isActionDisabled = - !enableAction || - this.disabledByRpcUrl() || - this.disabledByChainId() || - this.disabledBySymbol(); + !enableAction || this.disabledByChainId() || this.disabledBySymbol(); const rpcActionStyle = isActionDisabled ? { ...styles.button, ...styles.disabledButton } @@ -1346,6 +1665,24 @@ export class NetworkSettings extends PureComponent { }; const renderButtons = () => { + if (isNetworkUiRedesignEnabled()) { + return ( + + +