diff --git a/.github/workflows/add-team-label.yml b/.github/workflows/add-team-label.yml index 2046456ef42..b5d9eaa1860 100644 --- a/.github/workflows/add-team-label.yml +++ b/.github/workflows/add-team-label.yml @@ -7,8 +7,6 @@ on: jobs: add-team-label: - uses: metamask/github-tools/.github/workflows/add-team-label.yml@058012b49ff2fbd9649c566ba43b29497f93b21d - permissions: - pull-requests: write + uses: metamask/github-tools/.github/workflows/add-team-label.yml@18af6e4b56a18230d1792480e249ebc50b324927 secrets: - PERSONAL_ACCESS_TOKEN: ${{ secrets.RELEASE_LABEL_TOKEN }} + TEAM_LABEL_TOKEN: ${{ secrets.TEAM_LABEL_TOKEN }} diff --git a/.github/workflows/bump-version-name.yml b/.github/workflows/bump-version-name.yml deleted file mode 100644 index 9b12c7e2793..00000000000 --- a/.github/workflows/bump-version-name.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Bump version name -on: - pull_request: - branches: - - develop - - main - types: [opened] - merge_group: - types: [checks_requested] - -jobs: - bump-version-name: - runs-on: ubuntu-latest - if: "contains(github.head_ref, 'release/')" - permissions: - contents: write - steps: - - uses: actions/checkout@v3 - - name: Bump script - env: - HEAD_REF: ${{ github.head_ref }} - run: | - ./scripts/bump-version.sh "$HEAD_REF" - git diff - git config user.name metamaskbot - git config user.email metamaskbot@users.noreply.github.com - git add bitrise.yml - git add package.json - git commit -m "Bump version name" - git push origin HEAD:"$HEAD_REF" --force diff --git a/.github/workflows/create-release-pr.yml b/.github/workflows/create-release-pr.yml index 6065f84b868..ce37698ff0f 100644 --- a/.github/workflows/create-release-pr.yml +++ b/.github/workflows/create-release-pr.yml @@ -9,15 +9,18 @@ on: semver-version: description: 'A semantic version. eg: x.x.x' required: true - version-number: - description: 'A natural version number. eg: 862' - required: true previous-version-tag: description: 'Previous release version tag. eg: v7.7.0' required: true jobs: + generate-build-version: + uses: MetaMask/metamask-mobile-build-version/.github/workflows/metamask-mobile-build-version.yml@v0.2.0 + permissions: + id-token: write + create-release-pr: runs-on: ubuntu-latest + needs: generate-build-version permissions: contents: write pull-requests: write @@ -36,6 +39,7 @@ jobs: # The workaround is to use a personal access token (BUG_REPORT_TOKEN) instead of # the default GITHUB_TOKEN for the checkout action. token: ${{ secrets.BUG_REPORT_TOKEN }} + - name: Set up Node.js uses: actions/setup-node@v3 with: @@ -48,5 +52,6 @@ jobs: shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BASE_BRANCH: ${{ github.event.inputs.base-branch }} run: | - ./scripts/create-release-pr.sh ${{ github.event.inputs.previous-version-tag }} ${{ github.event.inputs.semver-version }} ${{ github.event.inputs.version-number }} \ No newline at end of file + ./scripts/create-release-pr.sh ${{ github.event.inputs.previous-version-tag }} ${{ github.event.inputs.semver-version }} ${{ needs.generate-build-version.outputs.build-version }} \ No newline at end of file diff --git a/.github/workflows/update-latest-build-version.yml b/.github/workflows/update-latest-build-version.yml new file mode 100644 index 00000000000..4d43acd8881 --- /dev/null +++ b/.github/workflows/update-latest-build-version.yml @@ -0,0 +1,51 @@ +############################################################################################## +# +# This Workflow is responsible for updating the latest build version of the project. +# You can provide your own base branch, tag, or SHA for git operations and the pull request. +# and it will generate the latest build version & update the neccessary files for you. +# +############################################################################################## +name: Update Latest Build Version + + +on: + workflow_dispatch: + inputs: + base-branch: + description: 'The base branch, tag, or SHA for git operations and the pull request.' + required: true +jobs: + generate-build-version: + uses: MetaMask/metamask-mobile-build-version/.github/workflows/metamask-mobile-build-version.yml@v0.2.0 + permissions: + id-token: write + + bump-version: + runs-on: ubuntu-latest + needs: generate-build-version + permissions: + contents: write + id-token: write + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + ref: ${{ inputs.base-branch }} + token: ${{ secrets.PR_TOKEN }} + + - name: Bump script + env: + HEAD_REF: ${{ inputs.base-branch }} + run: | + ./scripts/set-build-version.sh ${{ needs.generate-build-version.outputs.build-version }} + git diff + git config user.name metamaskbot + git config user.email metamaskbot@users.noreply.github.com + git add bitrise.yml + git add package.json + git add ios/MetaMask.xcodeproj/project.pbxproj + git add android/app/build.gradle + git commit -m "Bump version number to ${{ needs.generate-build-version.outputs.build-version }}" + git push origin HEAD:"$HEAD_REF" --force + + \ No newline at end of file diff --git a/.gitignore b/.gitignore index c98f331e0da..f25f95d5af6 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,6 @@ docs/assets/termsOfUse.html # build metadata android/app/src/main/assets/modules.json + +# Google firebase base64 derived configs +**/GoogleService-Info.plist \ No newline at end of file diff --git a/.js.env.example b/.js.env.example index c151b882877..1c11f591536 100644 --- a/.js.env.example +++ b/.js.env.example @@ -68,10 +68,10 @@ export SEGMENT_FLUSH_INTERVAL="1" export SEGMENT_FLUSH_EVENT_LIMIT="1" # URL of security alerts API used to validate dApp requests. -export SECURITY_ALERTS_API_URL="http://localhost:3000" +export SECURITY_ALERTS_API_URL="https://security-alerts.api.cx.metamask.io" # Temporary mechanism to enable security alerts API prior to release. -export SECURITY_ALERTS_API_ENABLED="true" +export MM_SECURITY_ALERTS_API_ENABLED="true" # Firebase export FCM_CONFIG_API_KEY="" export FCM_CONFIG_AUTH_DOMAIN="" @@ -98,3 +98,6 @@ export MM_CHAIN_PERMISSIONS="" #Multichain feature flag specific to UI changes export MM_MULTICHAIN_V1_ENABLED="" + +#Permissions Settings feature flag specific to UI changes +export MM_PERMISSIONS_SETTINGS_V1_ENABLED="" diff --git a/.nvmrc b/.nvmrc index 48b14e6b2b5..3516580bbbc 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.14.0 +20.17.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8db93d47c3a..7d2f19a48bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,109 @@ ## Current Main Branch +## 7.34.0 - Oct 28, 2024 +### Added +- [#11578](https://github.com/MetaMask/metamask-mobile/pull/11578): feat: 1653 first feature flag poc (#11578) +- [#11705](https://github.com/MetaMask/metamask-mobile/pull/11705): feat: Transition from Multiple Networks with Same ChainID to Unique Networks with Distinct ChainIDs and Multiple RPC URLs (#11705) +- [#11685](https://github.com/MetaMask/metamask-mobile/pull/11685): feat: multi rpc modal (#11685) +- [#11651](https://github.com/MetaMask/metamask-mobile/pull/11651): feat: add utm field to app_open event (#11651) +- [#11579](https://github.com/MetaMask/metamask-mobile/pull/11579): feat: 1940 Add custom traces (#11579) +- [#11815](https://github.com/MetaMask/metamask-mobile/pull/11815): feat: Address value component for use in re-designed confirmation pages (#11815) +- [#11737](https://github.com/MetaMask/metamask-mobile/pull/11737): feat: Adding simulation section to personal sign page (#11737) +- [#11736](https://github.com/MetaMask/metamask-mobile/pull/11736): feat: add copy button component (#11736) +- [#11703](https://github.com/MetaMask/metamask-mobile/pull/11703): feat: Adding expandable message section to personal sign page (#11703) +- [#11698](https://github.com/MetaMask/metamask-mobile/pull/11698): feat: add account_network section to re-designed confirmation page (#11698) +- [#11453](https://github.com/MetaMask/metamask-mobile/pull/11453): feat: Add signature tracing (#11453) +- [#11456](https://github.com/MetaMask/metamask-mobile/pull/11456): feat: Add metric for insufficient funds for gas (#11456) +- [#11602](https://github.com/MetaMask/metamask-mobile/pull/11602): feat: adding a basic personal sign page re-designed (#11602) +- [#11572](https://github.com/MetaMask/metamask-mobile/pull/11572): feat: add tooltip component for re-designed confirmation pages (#11572) +- [#11567](https://github.com/MetaMask/metamask-mobile/pull/11567): feat: add url value component for use in re-designed confirmation pages (#11567) +- [#11528](https://github.com/MetaMask/metamask-mobile/pull/11528): feat: expandable section for use in confirmation pages (#11528) +- [#11605](https://github.com/MetaMask/metamask-mobile/pull/11605): feat: STAKE-824: [FE] build staking input confirmation screen (#11605) +- [#11607](https://github.com/MetaMask/metamask-mobile/pull/11607): feat: add unstake screen for mobile staking (#11607) +- [#11660](https://github.com/MetaMask/metamask-mobile/pull/11660): feat: added ReactNode support for KeyValueRow tooltip and label (#11660) +- [#11733](https://github.com/MetaMask/metamask-mobile/pull/11733): feat: add references and utils for delete storage key (#11733) +- [#11653](https://github.com/MetaMask/metamask-mobile/pull/11653): feat: Implement partially local Snaps execution environment (#11653) +- [#11127](https://github.com/MetaMask/metamask-mobile/pull/11127): feat(1702): enhanced onboarding settings config (#11127) + +### Changed +- [#11852](https://github.com/MetaMask/metamask-mobile/pull/11852): chore: readd date time picker (#11852) +- [#11831](https://github.com/MetaMask/metamask-mobile/pull/11831): chore: Add conditional for api specification test (#11831) +- [#11838](https://github.com/MetaMask/metamask-mobile/pull/11838): chore: revert tags commit (#11838) +- [#11839](https://github.com/MetaMask/metamask-mobile/pull/11839): chore: Revert custom span (#11839) +- [#11824](https://github.com/MetaMask/metamask-mobile/pull/11824): chore: enable Sentry performance reports on QA builds (#11824) +- [#11825](https://github.com/MetaMask/metamask-mobile/pull/11825): chore: fix create qa build pipeline (#11825) +- [#11805](https://github.com/MetaMask/metamask-mobile/pull/11805): chore: Update Sentry Performance Sampling utils.js (#11805) +- [#11710](https://github.com/MetaMask/metamask-mobile/pull/11710): ci: Enable Detox E2E in Release mode (#11710) +- [#11757](https://github.com/MetaMask/metamask-mobile/pull/11757): docs: update e2e best practices link in readme (#11757) +- [#11746](https://github.com/MetaMask/metamask-mobile/pull/11746): "chore: Revert ""chore(js-ts): Convert app/util/test/ganache-contract-address-registry.js to TypeScript"" (#11746)" +- [#11694](https://github.com/MetaMask/metamask-mobile/pull/11694): test: Refactor ImportAccountView.js and LoginView.js files (#11694) +- [#11136](https://github.com/MetaMask/metamask-mobile/pull/11136): chore(build): let adb server live between builds (#11136) +- [#11669](https://github.com/MetaMask/metamask-mobile/pull/11669): chore: Merge 7.32.0 back into main (#11669) +- [#11667](https://github.com/MetaMask/metamask-mobile/pull/11667): chore: add 9f9a11 to git blame ignore (#11667) +- [#11262](https://github.com/MetaMask/metamask-mobile/pull/11262): chore: Refactor event tracking method (#11262) +- [#11520](https://github.com/MetaMask/metamask-mobile/pull/11520): chore: Reorganize `PortfolioBalance` (#11520) +- [#11761](https://github.com/MetaMask/metamask-mobile/pull/11761): chore: Add support for custom network images (#11761) +- [#11687](https://github.com/MetaMask/metamask-mobile/pull/11687): chore: upgrade assets controllers v32.0.0 (#11687) +- [#11471](https://github.com/MetaMask/metamask-mobile/pull/11471): chore: Componetize `Tokens` screen (#11471) +- [#11751](https://github.com/MetaMask/metamask-mobile/pull/11751): chore(devDeps): remove unused react-native-cli (#11751) +- [#11623](https://github.com/MetaMask/metamask-mobile/pull/11623): chore: Add tags to custom traces (#11623) +- [#11788](https://github.com/MetaMask/metamask-mobile/pull/11788): chore(ramp): upgrade sdk to 1.28.5 (#11788) +- [#11421](https://github.com/MetaMask/metamask-mobile/pull/11421): refactor(ramp): use list item from the components library (#11421) +- [#11086](https://github.com/MetaMask/metamask-mobile/pull/11086): test: initial ramps E2E test to build quote (#11086) +- [#11745](https://github.com/MetaMask/metamask-mobile/pull/11745): ci: fix get-next-semver-version invocation in add-release-label (#11745) +- [#11756](https://github.com/MetaMask/metamask-mobile/pull/11756): chore: Remove obsolete CI step (#11756) +- [#11656](https://github.com/MetaMask/metamask-mobile/pull/11656): chore: rename `isprivateConnection` to `isPrivateConnection ` (#11656) +- [#11546](https://github.com/MetaMask/metamask-mobile/pull/11546): chore(js-ts): Convert app/components/Views/AndroidBackHandler/index.js to TypeScript (#11546) +- [#11629](https://github.com/MetaMask/metamask-mobile/pull/11629): chore(js-ts): Convert app/util/transaction-reducer-helpers.js to TypeScript (#11629) +- [#11661](https://github.com/MetaMask/metamask-mobile/pull/11661): chore(js-ts): Convert app/components/Base/HorizontalSelector/index.js to TypeScript (#11661) +- [#11650](https://github.com/MetaMask/metamask-mobile/pull/11650): chore(js-ts): Convert app/components/UI/Swaps/components/InfoModal.js to TypeScript (#11650) +- [#11406](https://github.com/MetaMask/metamask-mobile/pull/11406): chore(js-ts): Convert app/util/test/ganache-contract-address-registry.js to TypeScript (#11406) +- [#11556](https://github.com/MetaMask/metamask-mobile/pull/11556): chore(js-ts): Convert app/components/UI/Fox/index.js to TypeScript (#11556) +- [#11525](https://github.com/MetaMask/metamask-mobile/pull/11525): chore(js-ts): Convert app/components/UI/StyledButton/styledButtonStyles.js to TypeScript (#11525) +- [#11385](https://github.com/MetaMask/metamask-mobile/pull/11385): refactor(1702-5): incoming transactions and network details settings into reusable components (#11385) +- [#11381](https://github.com/MetaMask/metamask-mobile/pull/11381): refactor(1702-4): ipfs gateway settings and display nft media settings components (#11381) +- [#11614](https://github.com/MetaMask/metamask-mobile/pull/11614): chore: Update UX CodeOwners responsibilities (#11614) +- [#11709](https://github.com/MetaMask/metamask-mobile/pull/11709): chore: updating codeowners to remove mobile-devs from component library (#11709) +- [#11617](https://github.com/MetaMask/metamask-mobile/pull/11617): chore: chore/7.33.0-Changelog (#11617) + +### Fixed +- [#11787](https://github.com/MetaMask/metamask-mobile/pull/11787): fix: non deterministic date in test (#11787) +- [#11762](https://github.com/MetaMask/metamask-mobile/pull/11762): fix: snapshot of test to enable ci (#11762) +- [#11632](https://github.com/MetaMask/metamask-mobile/pull/11632): fix: add custom and add popular network detox test (#11632) +- [#11794](https://github.com/MetaMask/metamask-mobile/pull/11794): fix(action): github action to apply release label is broken (#11794) +- [#11760](https://github.com/MetaMask/metamask-mobile/pull/11760): fix: Android: Splash screen always showing behind other screens (#11760) +- [#11741](https://github.com/MetaMask/metamask-mobile/pull/11741): fix: rollback originWhitelist (#11741) +- [#11722](https://github.com/MetaMask/metamask-mobile/pull/11722): fix: add safe space on the left of bell icon (#11722) +- [#11139](https://github.com/MetaMask/metamask-mobile/pull/11139): fix: Reuse mmkv instance once created (#11139) +- [#11532](https://github.com/MetaMask/metamask-mobile/pull/11532): fix: fix approve flow on swap (#11532) +- [#11718](https://github.com/MetaMask/metamask-mobile/pull/11718): fix: token list after switching networks (#11718) +- [#11495](https://github.com/MetaMask/metamask-mobile/pull/11495): fix: fix asset symbol for incoming tx (#11495) +- [#11664](https://github.com/MetaMask/metamask-mobile/pull/11664): fix: Duplicate key in Settings and Privacy (#11664) +- [#11412](https://github.com/MetaMask/metamask-mobile/pull/11412): fix: upgrade assets-controllers to v31 (#11412) +- [#11631](https://github.com/MetaMask/metamask-mobile/pull/11631): fix: hotfix/issue-11539 (#11631) +- [#11834](https://github.com/MetaMask/metamask-mobile/pull/11834): fix: invalid wallet connect detection (wrong hostname in connect modal) (#11834) +- [#11701](https://github.com/MetaMask/metamask-mobile/pull/11701): fix: connect request completed source validation (#11701) +- [#11603](https://github.com/MetaMask/metamask-mobile/pull/11603): fix: await for detect origin before logging analytics (#11603) +- [#11595](https://github.com/MetaMask/metamask-mobile/pull/11595): fix: fix signature requested analytics (#11595) +- [#11592](https://github.com/MetaMask/metamask-mobile/pull/11592): fix: fix walletconnect source validation (#11592) +- [#11802](https://github.com/MetaMask/metamask-mobile/pull/11802): fix: persist token and phishing list (#11802) +- [#11729](https://github.com/MetaMask/metamask-mobile/pull/11729): fix: support for batch of signature requests (#11729) +- [#11771](https://github.com/MetaMask/metamask-mobile/pull/11771): fix: Add `preferContractSymbol` to Name components (#11771) +- [#11730](https://github.com/MetaMask/metamask-mobile/pull/11730): fix: Use domain for origin pill component (#11730) +- [#11620](https://github.com/MetaMask/metamask-mobile/pull/11620): fix: Fix unwanted `IncompleteAssetDisplayed` events (#11620) +- [#11792](https://github.com/MetaMask/metamask-mobile/pull/11792): fix: use object styling in Title component (#11792) +- [#11633](https://github.com/MetaMask/metamask-mobile/pull/11633): fix(ramp): disable button while confirming off-ramp transaction (#11633) +- [#11431](https://github.com/MetaMask/metamask-mobile/pull/11431): fix: refactor notifications unit tests (#11431) + +## 7.33.2 - Nov 1, 2024 +### Fixed +- [#10952](https://github.com/MetaMask/metamask-mobile/pull/10952): refactor(ramp): update ramp copy (#10952) + +## 7.33.2 - Oct 29, 2024 +### Fixed +- [#12073](https://github.com/MetaMask/metamask-mobile/pull/12073): feat: Simulation re-trigger (#12073) + ## 7.33.0 - Oct 17, 2024 ### Added - [#11507](https://github.com/MetaMask/metamask-mobile/pull/11507): feat: 10550 Re-introduce test for measuring cold app start + JS bundle load time (#11507) @@ -109,7 +212,6 @@ ## 7.32.0 - Oct 7, 2024 ### Added - - [#10294](https://github.com/MetaMask/metamask-mobile/pull/10294): feat: create redux slice for featureFlags (#10294) - [#11314](https://github.com/MetaMask/metamask-mobile/pull/11314): feat: reject connection properly (#11314) - [#11132](https://github.com/MetaMask/metamask-mobile/pull/11132): feat: Add performance tracing infrastructure (#11132) diff --git a/README.md b/README.md index 587898dcbd4..08173a32791 100644 --- a/README.md +++ b/README.md @@ -39,20 +39,44 @@ git clone git@github.com:MetaMask/metamask-mobile.git && \ cd metamask-mobile ``` -**Firebase Messaging Setup** +### **Firebase Messaging Setup** -Before running the app, keep in mind that MetaMask uses FCM (Firebase Cloud Message) to empower communications. Based on this, as an external contributor you would preferably need to provide your own FREE Firebase project config file with a matching client for package name `io.metamask`, and update your `google-services.json` file in the `android/app` or `GoogleService-Info.plist` file in the `ios` directory. In case you don't have FCM account, you can use `./android/app/google-services-example.json` for Android or `./ios/GoogleServices/GoogleService-Info-example.plist` for iOS and follow the steps below to populate the correct environment variables in the `.env` files (`.ios.env`, `.js.env`, `.android.env`), adding `GOOGLE_SERVICES_B64_ANDROID` or `GOOGLE_SERVICES_B64_IOS` variable depending on the environment you are running the app (ios/android). +MetaMask uses Firebase Cloud Messaging (FCM) to enable app communications. To integrate FCM, you’ll need configuration files for both iOS and Android platforms. -The value you should provide to `GOOGLE_SERVICES_B64_ANDROID` or `GOOGLE_SERVICES_B64_IOS` is the base64 encoded version of your Firebase project config file, which can be generated as follows: +#### **Configuration Files Required** +- **`GoogleService-Info.plist`** (iOS) +- **`google-services.json`** (Android) + +These files are essential for FCM integration and are automatically generated when running: `yarn start:ios` or `yarn start:android` + +**External Contributors** + +As an external contributor, you need to provide your own Firebase project configuration files: + +1. Create a Free Firebase Project + * Set up a Firebase project in the Firebase Console. + * Configure the project with a client package name matching `io.metamask`. +2. Add Configuration Files + * Update the `google-services.json` and `GoogleService-Info.plist` files in: + * `android/app` (for Android) + * `ios` directory (for iOS) + +In case you don't have FCM account, you can reference the instructions below. These instructions will generate the required `GOOGLE_SERVICES_B64_ANDROID` or `GOOGLE_SERVICES_B64_IOS` environment variables found in: `.ios.env`, `.js.env`, `.android.env`. They can be locally generated from examples files. + +**Internal Contributors** + +As an internal contributor, you can access the shared Firebase project config file from 1Password. Ask around for the correct vault. + +The values you should provide to `GOOGLE_SERVICES_B64_ANDROID` or `GOOGLE_SERVICES_B64_IOS` are the base64 encoded versions of the example Firebase project config files. These can also be generated locally: **For Android** ```bash -echo "export GOOGLE_SERVICES_B64_ANDROID=\"$(base64 -w0 -i ./android/app/google-services-example.json)\"" | tee -a .js.env .ios.env .android.env +export GOOGLE_SERVICES_B64_ANDROID="$(base64 -w0 -i ./android/app/google-services-example.json)" && echo "export GOOGLE_SERVICES_B64_ANDROID=\"$GOOGLE_SERVICES_B64_ANDROID\"" | tee -a .js.env .ios.env ``` **For iOS** ```bash -echo "export GOOGLE_SERVICES_B64_IOS=\"$(base64 -w0 -i ./ios/GoogleServices/GoogleService-Info-example.plist)\"" | tee -a .js.env .ios.env +export GOOGLE_SERVICES_B64_IOS="$(base64 -w0 -i ./ios/GoogleServices/GoogleService-Info-example.plist)" && echo "export GOOGLE_SERVICES_B64_IOS=\"$GOOGLE_SERVICES_B64_IOS\"" | tee -a .js.env .ios.env ``` [!CAUTION] diff --git a/android/app/build.gradle b/android/app/build.gradle index f2379b86e80..3e466452e54 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -173,8 +173,8 @@ android { applicationId "io.metamask" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionName "7.33.0" - versionCode 1460 + versionCode 1475 + versionName "7.34.0" testBuildType System.getProperty('testBuildType', 'debug') missingDimensionStrategy 'react-native-camera', 'general' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/actions/notification/helpers/index.test.tsx b/app/actions/notification/helpers/index.test.tsx new file mode 100644 index 00000000000..8fd7a7ea805 --- /dev/null +++ b/app/actions/notification/helpers/index.test.tsx @@ -0,0 +1,63 @@ +// Import necessary libraries and modules +import { signIn, signOut, enableNotificationServices, disableNotificationServices } from '.'; +import Engine from '../../../core/Engine'; + +jest.mock('../../../core/Engine', () => ({ + resetState: jest.fn(), + context: { + AuthenticationController: { + performSignIn: jest.fn(), + performSignOut: jest.fn(), + getSessionProfile: jest.fn(), + }, + NotificationServicesController: { + enableMetamaskNotifications:jest.fn(), + disableNotificationServices:jest.fn(), + checkAccountsPresence: jest.fn(), + } + }, +})); + +describe('Notification Helpers', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('signs in successfully and obtain profile', async () => { + (Engine.context.AuthenticationController.performSignIn as jest.Mock).mockResolvedValue('valid-access-token'); + (Engine.context.AuthenticationController.getSessionProfile as jest.Mock).mockResolvedValue('valid-profile'); + + const result = await signIn(); + + expect(Engine.context.AuthenticationController.performSignIn).toHaveBeenCalled(); + expect(Engine.context.AuthenticationController.getSessionProfile).toHaveBeenCalled(); + expect(result).toBeUndefined(); + }); + + it('signs out successfully', async () => { + (Engine.context.AuthenticationController.performSignOut as jest.Mock).mockResolvedValue(undefined); + + const result = await signOut(); + + expect(Engine.context.AuthenticationController.performSignOut).toHaveBeenCalled(); + expect(result).toBeUndefined(); + }); + + it('enables notification services successfully', async () => { + (Engine.context.NotificationServicesController.enableMetamaskNotifications as jest.Mock).mockResolvedValue(undefined); + + const result = await enableNotificationServices(); + + expect(Engine.context.NotificationServicesController.enableMetamaskNotifications).toHaveBeenCalled(); + expect(result).toBeUndefined(); + }); + + it('disables notification services successfully', async () => { + (Engine.context.NotificationServicesController.disableNotificationServices as jest.Mock).mockResolvedValue(undefined); + + const result = await disableNotificationServices(); + + expect(Engine.context.NotificationServicesController.disableNotificationServices).toHaveBeenCalled(); + expect(result).toBeUndefined(); + }); +}); diff --git a/app/actions/notification/helpers/index.ts b/app/actions/notification/helpers/index.ts index 9f47da07f88..a52e524009a 100644 --- a/app/actions/notification/helpers/index.ts +++ b/app/actions/notification/helpers/index.ts @@ -2,7 +2,8 @@ import { getErrorMessage } from '@metamask/utils'; import { notificationsErrors } from '../constants'; import Engine from '../../../core/Engine'; -import { Notification } from '../../../util/notifications'; +import { Notification, mmStorage, getAllUUIDs } from '../../../util/notifications'; +import { UserStorage } from '@metamask/notification-services-controller/dist/NotificationServicesController/types/user-storage/index.cjs'; export type MarkAsReadNotificationsParam = Pick< Notification, @@ -83,7 +84,7 @@ export const checkAccountsPresence = async (accounts: string[]) => { export const deleteOnChainTriggersByAccount = async (accounts: string[]) => { try { - const { userStorage } = + const userStorage = await Engine.context.NotificationServicesController.deleteOnChainTriggersByAccount( accounts, ); @@ -92,6 +93,7 @@ export const deleteOnChainTriggersByAccount = async (accounts: string[]) => { notificationsErrors.DELETE_ON_CHAIN_TRIGGERS_BY_ACCOUNT, ); } + mmStorage.saveLocal('pnUserStorage', userStorage); } catch (error) { return getErrorMessage(error); } @@ -99,7 +101,7 @@ export const deleteOnChainTriggersByAccount = async (accounts: string[]) => { export const updateOnChainTriggersByAccount = async (accounts: string[]) => { try { - const { userStorage } = + const userStorage = await Engine.context.NotificationServicesController.updateOnChainTriggersByAccount( accounts, ); @@ -108,6 +110,7 @@ export const updateOnChainTriggersByAccount = async (accounts: string[]) => { notificationsErrors.UPDATE_ON_CHAIN_TRIGGERS_BY_ACCOUNT, ); } + mmStorage.saveLocal('pnUserStorage', userStorage); } catch (error) { return getErrorMessage(error); } @@ -117,7 +120,7 @@ export const createOnChainTriggersByAccount = async ( resetNotifications: boolean, ) => { try { - const { userStorage } = + const userStorage = await Engine.context.NotificationServicesController.createOnChainTriggers( { resetNotifications, @@ -129,6 +132,7 @@ export const createOnChainTriggersByAccount = async ( notificationsErrors.CREATE_ON_CHAIN_TRIGGERS_BY_ACCOUNT, ); } + mmStorage.saveLocal('pnUserStorage', userStorage); } catch (error) { return getErrorMessage(error); } @@ -171,6 +175,15 @@ export const markMetamaskNotificationsAsRead = async ( return getErrorMessage(error); } }; + +export const syncInternalAccountsWithUserStorage = async () => { + try { + await Engine.context.UserStorageController.syncInternalAccountsWithUserStorage(); + } catch (error) { + return getErrorMessage(error); + } +}; + /** * Perform the deletion of the notifications storage key and the creation of on chain triggers to reset the notifications. * @@ -178,12 +191,45 @@ export const markMetamaskNotificationsAsRead = async ( */ export const performDeleteStorage = async (): Promise => { try { - await Engine.context.UserStorageController.performDeleteStorage('notifications.notification_settings'); - await Engine.context.NotificationServicesController.createOnChainTriggers( - { + await Engine.context.UserStorageController.performDeleteStorage( + 'notifications.notification_settings', + ); + await Engine.context.NotificationServicesController.createOnChainTriggers({ resetNotifications: true, - }, - ); + }); + } catch (error) { + return getErrorMessage(error); + } +}; +export const enablePushNotifications = async (userStorage: UserStorage, fcmToken?: string) => { + try { + const uuids = getAllUUIDs(userStorage); + await Engine.context.NotificationServicesPushController.enablePushNotifications( + uuids, + fcmToken, + ); + } catch (error) { + return getErrorMessage(error); + } +}; + +export const disablePushNotifications = async (userStorage: UserStorage) => { + try { + const uuids = getAllUUIDs(userStorage); + await Engine.context.NotificationServicesPushController.disablePushNotifications( + uuids, + ); + } catch (error) { + return getErrorMessage(error); + } +}; + +export const updateTriggerPushNotifications = async (userStorage: UserStorage) => { + try { + const uuids = getAllUUIDs(userStorage); + await Engine.context.NotificationServicesPushController.updateTriggerPushNotifications( + uuids, + ); } catch (error) { return getErrorMessage(error); } diff --git a/app/actions/onboarding/index.ts b/app/actions/onboarding/index.ts index e85dd902fe3..641bf5568bd 100644 --- a/app/actions/onboarding/index.ts +++ b/app/actions/onboarding/index.ts @@ -14,7 +14,9 @@ interface ClearEventsAction { export type OnboardingActionTypes = SaveEventAction | ClearEventsAction; -export function saveOnboardingEvent(eventArgs: [IMetaMetricsEvent]): SaveEventAction { +export function saveOnboardingEvent( + eventArgs: [IMetaMetricsEvent], +): SaveEventAction { return { type: SAVE_EVENT, event: eventArgs, diff --git a/app/component-library/components-temp/Accounts/AccountBase/AccountBase.tsx b/app/component-library/components-temp/Accounts/AccountBase/AccountBase.tsx index 88e0f809d05..c2313af3bda 100644 --- a/app/component-library/components-temp/Accounts/AccountBase/AccountBase.tsx +++ b/app/component-library/components-temp/Accounts/AccountBase/AccountBase.tsx @@ -11,7 +11,6 @@ import { ACCOUNT_BASE_TEST_ID, } from './AccountBase.constants'; import styles from './AccountBase.styles'; -import { strings } from '../../../../../locales/i18n'; import { AccountBaseProps } from './AccountBase.types'; const AccountBase = ({ @@ -56,7 +55,7 @@ const AccountBase = ({ variant={TextVariant.BodyMDBold} style={styles.accountNameLabelText} > - {strings(accountTypeLabel)} + {accountTypeLabel} )} diff --git a/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.stories.tsx b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.stories.tsx index 46e8a658e40..990704840a9 100644 --- a/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.stories.tsx +++ b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.stories.tsx @@ -30,8 +30,20 @@ const CellSelectWithMenuMeta = { control: { type: 'boolean' }, defaultValue: SAMPLE_CELLSELECT_WITH_BUTTON_PROPS.isDisabled, }, + withAvatar: { + control: { type: 'boolean' }, + defaultValue: true, + }, + showSecondaryTextIcon: { + control: { type: 'boolean' }, + defaultValue: true, + }, + onTextClick: { + action: 'clicked', + }, }, }; + export default CellSelectWithMenuMeta; export const CellMultiSelectWithMenu = { diff --git a/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.styles.ts b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.styles.ts index 99dfa994910..97a8c23a18d 100644 --- a/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.styles.ts +++ b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.styles.ts @@ -55,6 +55,9 @@ const styleSheet = (params: { tagLabel: { marginTop: 4, }, + selectedTag: { + backgroundColor: colors.primary.muted, + }, containerRow: { flexDirection: 'row', alignItems: 'flex-start', diff --git a/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.tsx b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.tsx index e18ed046595..144f2408e20 100644 --- a/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.tsx +++ b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.tsx @@ -6,6 +6,7 @@ import { TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native'; // External dependencies. import { useStyles } from '../../hooks'; +import Tag from '../../../component-library/components/Tags/Tag'; // Internal dependencies. import styleSheet from './CellSelectWithMenu.styles'; @@ -34,6 +35,7 @@ const CellSelectWithMenu = ({ isSelected = false, children, withAvatar = true, + showSecondaryTextIcon = true, ...props }: CellSelectWithMenuProps) => { const { styles } = useStyles(styleSheet, { style }); @@ -77,14 +79,27 @@ const CellSelectWithMenu = ({ > {secondaryText} - + {showSecondaryTextIcon && ( + + )} )} + {!!tagLabel && ( + + )} {children && {children}} 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 315ea9c6914..f0bc6875e36 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 @@ -280,6 +280,37 @@ exports[`CellSelectWithMenu should render with default settings correctly 1`] = width={10} /> + + + Imported + + @@ -288,7 +319,7 @@ exports[`CellSelectWithMenu should render with default settings correctly 1`] = diff --git a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.stories.tsx b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.stories.tsx index f1db9cbef12..2abe8bf0b1f 100644 --- a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.stories.tsx +++ b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.stories.tsx @@ -14,7 +14,11 @@ import Text, { // Internal dependencies. import { default as ListItemSelectWithButtonComponent } from './ListItemMultiSelectButton'; -import { SAMPLE_LISTITEMMULTISELECT_PROPS } from './ListItemMultiSelectButton.constants'; +import { + BUTTON_TEST_ID, + DEFAULT_LISTITEMMULTISELECT_GAP, + SAMPLE_LISTITEMMULTISELECT_PROPS, +} from './ListItemMultiSelectButton.constants'; import { ListItemMultiSelectButtonProps } from './ListItemMultiSelectButton.types'; const ListItemSelectWithButtonMeta = { @@ -29,6 +33,27 @@ const ListItemSelectWithButtonMeta = { control: { type: 'boolean' }, defaultValue: SAMPLE_LISTITEMMULTISELECT_PROPS.isDisabled, }, + showButtonIcon: { + control: { type: 'boolean' }, + defaultValue: true, + }, + buttonIcon: { + control: { type: 'select' }, + options: Object.values(IconName), + defaultValue: IconName.MoreVertical, + }, + gap: { + control: { type: 'number' }, + defaultValue: DEFAULT_LISTITEMMULTISELECT_GAP, + }, + buttonProps: { + control: 'object', + defaultValue: { + textButton: '', + onButtonClick: () => null, + buttonTestId: BUTTON_TEST_ID, + }, + }, }, }; export default ListItemSelectWithButtonMeta; diff --git a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.styles.ts b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.styles.ts index 4af6d6f86a9..9f00528c58d 100644 --- a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.styles.ts +++ b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.styles.ts @@ -85,7 +85,7 @@ const styleSheet = (params: { paddingTop: 32, }, buttonIcon: { - paddingHorizontal: 20, + paddingRight: 20, }, }); }; diff --git a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.tsx b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.tsx index a3cb0d079ff..7f258b175df 100644 --- a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.tsx +++ b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.tsx @@ -68,7 +68,7 @@ const ListItemMultiSelectButton: React.FC = ({ diff --git a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.types.ts b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.types.ts index f92853d9875..4f7ad98ca84 100644 --- a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.types.ts +++ b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.types.ts @@ -41,6 +41,9 @@ export interface ListItemMultiSelectButtonProps */ showButtonIcon?: boolean; + /** + * Optional button props + */ buttonProps?: { /** * Optional button onClick function @@ -50,6 +53,16 @@ export interface ListItemMultiSelectButtonProps * Optional property to show text button */ textButton?: string | null; + + /** + * Optional property to show button icon + */ + showButtonIcon?: boolean; + + /** + * Optional property for button test ID + */ + buttonTestId?: string; }; } 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 0d1b7d3f4f5..6afd161f5db 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 @@ -53,7 +53,7 @@ exports[`ListItemMultiSelectButton should render correctly with default props 1` diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.constants.ts b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.constants.ts new file mode 100644 index 00000000000..52b0b69960a --- /dev/null +++ b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.constants.ts @@ -0,0 +1,2 @@ +export const FORMATTED_VALUE_PRICE_TEST_ID = 'formatted-value-price-test-id'; +export const FORMATTED_PERCENTAGE_TEST_ID = 'formatted-percentage-test-id'; diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.test.tsx b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.test.tsx index affec361321..ac0bdb30a2e 100644 --- a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.test.tsx +++ b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.test.tsx @@ -4,6 +4,10 @@ import AggregatedPercentage from './AggregatedPercentage'; import { mockTheme } from '../../../../util/theme'; import { useSelector } from 'react-redux'; import { selectCurrentCurrency } from '../../../../selectors/currencyRateController'; +import { + FORMATTED_VALUE_PRICE_TEST_ID, + FORMATTED_PERCENTAGE_TEST_ID, +} from './AggregatedPercentage.constants'; jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), @@ -65,4 +69,22 @@ describe('AggregatedPercentage', () => { color: mockTheme.colors.error.default, }); }); + + it('renders correctly with privacy mode on', () => { + const { getByTestId } = render( + , + ); + + const formattedPercentage = getByTestId(FORMATTED_PERCENTAGE_TEST_ID); + const formattedValuePrice = getByTestId(FORMATTED_VALUE_PRICE_TEST_ID); + + expect(formattedPercentage.props.children).toBe('••••••••••'); + expect(formattedValuePrice.props.children).toBe('••••••••••'); + }); }); diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.tsx b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.tsx index c2a94c1bc1a..715587f6ddc 100644 --- a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.tsx +++ b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.tsx @@ -1,14 +1,19 @@ import React from 'react'; -import Text, { +import { TextColor, TextVariant, } from '../../../../component-library/components/Texts/Text'; +import SensitiveText from '../../../../component-library/components/Texts/SensitiveText'; import { View } from 'react-native'; import { renderFiat } from '../../../../util/number'; import { useSelector } from 'react-redux'; import { selectCurrentCurrency } from '../../../../selectors/currencyRateController'; import styleSheet from './AggregatedPercentage.styles'; import { useStyles } from '../../../hooks'; +import { + FORMATTED_VALUE_PRICE_TEST_ID, + FORMATTED_PERCENTAGE_TEST_ID, +} from './AggregatedPercentage.constants'; export interface AggregatedPercentageProps { ethFiat: number; @@ -25,11 +30,13 @@ const AggregatedPercentage = ({ tokenFiat, tokenFiat1dAgo, ethFiat1dAgo, + privacyMode = false, }: { ethFiat: number; tokenFiat: number; tokenFiat1dAgo: number; ethFiat1dAgo: number; + privacyMode?: boolean; }) => { const { styles } = useStyles(styleSheet, {}); @@ -46,12 +53,16 @@ const AggregatedPercentage = ({ let percentageTextColor = TextColor.Default; - if (percentageChange === 0) { - percentageTextColor = TextColor.Default; - } else if (percentageChange > 0) { - percentageTextColor = TextColor.Success; + if (!privacyMode) { + if (percentageChange === 0) { + percentageTextColor = TextColor.Default; + } else if (percentageChange > 0) { + percentageTextColor = TextColor.Success; + } else { + percentageTextColor = TextColor.Error; + } } else { - percentageTextColor = TextColor.Error; + percentageTextColor = TextColor.Alternative; } const formattedPercentage = isValidAmount(percentageChange) @@ -70,12 +81,24 @@ const AggregatedPercentage = ({ return ( - + {formattedValuePrice} - - + + {formattedPercentage} - + ); }; diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/__snapshots__/AggregatedPercentage.test.tsx.snap b/app/component-library/components-temp/Price/AggregatedPercentage/__snapshots__/AggregatedPercentage.test.tsx.snap index 16b825b0e5d..1066d19a41b 100644 --- a/app/component-library/components-temp/Price/AggregatedPercentage/__snapshots__/AggregatedPercentage.test.tsx.snap +++ b/app/component-library/components-temp/Price/AggregatedPercentage/__snapshots__/AggregatedPercentage.test.tsx.snap @@ -21,6 +21,7 @@ exports[`AggregatedPercentage should render correctly 1`] = ` "lineHeight": 22, } } + testID="formatted-value-price-test-id" > +20 USD @@ -36,6 +37,7 @@ exports[`AggregatedPercentage should render correctly 1`] = ` "lineHeight": 22, } } + testID="formatted-percentage-test-id" > (+11.11%) diff --git a/app/component-library/components/Avatars/Avatar/variants/AvatarIcon/AvatarIcon.test.tsx b/app/component-library/components/Avatars/Avatar/variants/AvatarIcon/AvatarIcon.test.tsx index f6366e5ff33..f6ab447037e 100644 --- a/app/component-library/components/Avatars/Avatar/variants/AvatarIcon/AvatarIcon.test.tsx +++ b/app/component-library/components/Avatars/Avatar/variants/AvatarIcon/AvatarIcon.test.tsx @@ -1,6 +1,6 @@ // Third party dependencies. import React from 'react'; -import { shallow } from 'enzyme'; +import { render } from '@testing-library/react-native'; // External dependencies. import AvatarIcon from './AvatarIcon'; @@ -10,7 +10,7 @@ import { SAMPLE_AVATARICON_PROPS } from './AvatarIcon.constants'; describe('AvatarIcon', () => { it('should render correctly', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); + const { toJSON } = render(); + expect(toJSON()).toMatchSnapshot(); }); }); diff --git a/app/component-library/components/Avatars/Avatar/variants/AvatarIcon/__snapshots__/AvatarIcon.test.tsx.snap b/app/component-library/components/Avatars/Avatar/variants/AvatarIcon/__snapshots__/AvatarIcon.test.tsx.snap index 6c4f0afa310..1d343e9fea0 100644 --- a/app/component-library/components/Avatars/Avatar/variants/AvatarIcon/__snapshots__/AvatarIcon.test.tsx.snap +++ b/app/component-library/components/Avatars/Avatar/variants/AvatarIcon/__snapshots__/AvatarIcon.test.tsx.snap @@ -1,20 +1,30 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AvatarIcon should render correctly 1`] = ` - - - + `; diff --git a/app/component-library/components/Buttons/Button/variants/ButtonLink/ButtonLink.test.tsx b/app/component-library/components/Buttons/Button/variants/ButtonLink/ButtonLink.test.tsx index e8d3d91de54..f472c0f90d3 100644 --- a/app/component-library/components/Buttons/Button/variants/ButtonLink/ButtonLink.test.tsx +++ b/app/component-library/components/Buttons/Button/variants/ButtonLink/ButtonLink.test.tsx @@ -1,15 +1,13 @@ // Third party dependencies. import React from 'react'; -import { shallow } from 'enzyme'; +import { render, screen } from '@testing-library/react-native'; // Internal dependencies. import ButtonLink from './ButtonLink'; -describe('Link', () => { +describe('ButtonLink', () => { it('should render correctly', () => { - const wrapper = shallow( - , - ); - expect(wrapper).toMatchSnapshot(); + render(); + expect(screen.toJSON()).toMatchSnapshot(); }); }); diff --git a/app/component-library/components/Buttons/Button/variants/ButtonLink/__snapshots__/ButtonLink.test.tsx.snap b/app/component-library/components/Buttons/Button/variants/ButtonLink/__snapshots__/ButtonLink.test.tsx.snap index ff32c2ade80..1c09de8a64e 100644 --- a/app/component-library/components/Buttons/Button/variants/ButtonLink/__snapshots__/ButtonLink.test.tsx.snap +++ b/app/component-library/components/Buttons/Button/variants/ButtonLink/__snapshots__/ButtonLink.test.tsx.snap @@ -1,27 +1,39 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Link should render correctly 1`] = ` - +exports[`ButtonLink should render correctly 1`] = ` + - - I'm a Link! - + I'm a Link! - + `; diff --git a/app/component-library/components/Cells/Cell/foundation/CellBase/CellBase.types.ts b/app/component-library/components/Cells/Cell/foundation/CellBase/CellBase.types.ts index 88b9c4852d6..172cb99124d 100644 --- a/app/component-library/components/Cells/Cell/foundation/CellBase/CellBase.types.ts +++ b/app/component-library/components/Cells/Cell/foundation/CellBase/CellBase.types.ts @@ -37,6 +37,11 @@ export interface CellBaseProps { * Optional prop to control the style of the CellBase. */ style?: StyleProp | undefined; + + /** + * Optional prop to control the visibility of the secondary text icon. + */ + showSecondaryTextIcon?: boolean; } /** diff --git a/app/component-library/components/Pickers/PickerAccount/PickerAccount.styles.ts b/app/component-library/components/Pickers/PickerAccount/PickerAccount.styles.ts index 9bf03d0adbc..d4d99c40475 100644 --- a/app/component-library/components/Pickers/PickerAccount/PickerAccount.styles.ts +++ b/app/component-library/components/Pickers/PickerAccount/PickerAccount.styles.ts @@ -3,7 +3,6 @@ import { StyleSheet, ViewStyle } from 'react-native'; // External dependencies. import { Theme } from '../../../../util/theme/models'; -import { fontStyles } from '../../../../styles/common'; // Internal dependencies. import { PickerAccountStyleSheetVars } from './PickerAccount.types'; @@ -24,34 +23,39 @@ const styleSheet = (params: { const { colors } = theme; const { style, cellAccountContainerStyle } = vars; return StyleSheet.create({ - base: Object.assign({} as ViewStyle, style) as ViewStyle, + base: { + ...(style as ViewStyle), + flexDirection: 'row', + padding: 0, + borderWidth: 0, + }, accountAvatar: { - marginRight: 16, + marginRight: 8, }, accountAddressLabel: { color: colors.text.alternative, + textAlign: 'center', }, cellAccount: { - flex: 1, flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', ...cellAccountContainerStyle, }, accountNameLabel: { + alignItems: 'center', + justifyContent: 'center', + }, + accountNameAvatar: { flexDirection: 'row', alignItems: 'center', - justifyContent: 'flex-start', }, - accountNameLabelText: { - marginTop: 4, - marginHorizontal: 5, - paddingHorizontal: 5, - ...fontStyles.bold, - color: colors.text.alternative, - borderWidth: 1, - borderRadius: 10, - borderColor: colors.border.default, + pickerAccountContainer: { justifyContent: 'center', - textAlign: 'center', + alignItems: 'center', + }, + dropDownIcon: { + marginLeft: 8, }, }); }; diff --git a/app/component-library/components/Pickers/PickerAccount/PickerAccount.tsx b/app/component-library/components/Pickers/PickerAccount/PickerAccount.tsx index ce9a9ab9014..0bec81483f0 100644 --- a/app/component-library/components/Pickers/PickerAccount/PickerAccount.tsx +++ b/app/component-library/components/Pickers/PickerAccount/PickerAccount.tsx @@ -9,14 +9,13 @@ import Avatar, { AvatarSize, AvatarVariant } from '../../Avatars/Avatar'; import Text, { TextVariant } from '../../Texts/Text'; import { formatAddress } from '../../../../util/address'; import { useStyles } from '../../../hooks'; -import { strings } from '../../../../../locales/i18n'; +import { IconSize } from '../../Icons/Icon'; // Internal dependencies. import PickerBase from '../PickerBase'; import { PickerAccountProps } from './PickerAccount.types'; import styleSheet from './PickerAccount.styles'; import { WalletViewSelectorsIDs } from '../../../../../e2e/selectors/wallet/WalletView.selectors'; -import { AccountListViewSelectorsIDs } from '../../../../../e2e/selectors/AccountListView.selectors'; const PickerAccount: React.ForwardRefRenderFunction< TouchableOpacity, @@ -27,7 +26,6 @@ const PickerAccount: React.ForwardRefRenderFunction< accountAddress, accountName, accountAvatarType, - accountTypeLabel, showAddress = true, cellAccountContainerStyle = {}, ...props @@ -42,42 +40,46 @@ const PickerAccount: React.ForwardRefRenderFunction< const renderCellAccount = () => ( - - - {accountName} - - {accountTypeLabel && ( + + - {strings(accountTypeLabel)} - - )} - {showAddress && ( - - {shortenedAddress} + {accountName} - )} + ); return ( - - {renderCellAccount()} - + + + {renderCellAccount()} + + {showAddress && ( + + {shortenedAddress} + + )} + ); }; diff --git a/app/component-library/components/Pickers/PickerAccount/__snapshots__/PickerAccount.test.tsx.snap b/app/component-library/components/Pickers/PickerAccount/__snapshots__/PickerAccount.test.tsx.snap index 0afdb8affeb..a078072c4da 100644 --- a/app/component-library/components/Pickers/PickerAccount/__snapshots__/PickerAccount.test.tsx.snap +++ b/app/component-library/components/Pickers/PickerAccount/__snapshots__/PickerAccount.test.tsx.snap @@ -1,223 +1,242 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`PickerAccount should render correctly 1`] = ` - - - - - - - + + + + + + + + + + - - + } + testID="account-label" + > + Orangefox.eth + + - - - Orangefox.eth - - - 0x2990...a21a - - - - + + - + > + 0x2990...a21a + + `; diff --git a/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.test.tsx b/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.test.tsx index 948721d6ac4..65666c2e125 100644 --- a/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.test.tsx +++ b/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.test.tsx @@ -45,6 +45,20 @@ describe('PickerNetwork', () => { ).toBeNull(); }); + it('shows network name when hideNetworkName is false', () => { + const { queryByTestId } = render( + , + ); + + expect( + queryByTestId(WalletViewSelectorsIDs.NAVBAR_NETWORK_TEXT), + ).not.toBeNull(); + }); + it('calls onPress when pressed', () => { const onPress = jest.fn(); const { getByTestId } = render( diff --git a/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx b/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx index 1b4642ba967..29c48def333 100644 --- a/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx +++ b/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx @@ -34,6 +34,8 @@ const PickerNetwork = ({ size={AvatarSize.Xs} name={label} imageSource={imageSource} + testID={WalletViewSelectorsIDs.NAVBAR_NETWORK_PICKER} + accessibilityLabel={label} /> {hideNetworkName ? null : ( diff --git a/app/component-library/components/Pickers/PickerNetwork/__snapshots__/PickerNetwork.test.tsx.snap b/app/component-library/components/Pickers/PickerNetwork/__snapshots__/PickerNetwork.test.tsx.snap index f59fab74933..c2965c38bb0 100644 --- a/app/component-library/components/Pickers/PickerNetwork/__snapshots__/PickerNetwork.test.tsx.snap +++ b/app/component-library/components/Pickers/PickerNetwork/__snapshots__/PickerNetwork.test.tsx.snap @@ -19,6 +19,7 @@ exports[`PickerNetwork renders correctly 1`] = ` style={null} > { @@ -8,11 +8,11 @@ describe('ApprovalModal', () => { }); it('renders', () => { - const wrapper = shallow( + const { toJSON } = render( undefined}>
test
, ); - expect(wrapper).toMatchSnapshot(); + expect(toJSON()).toMatchSnapshot(); }); }); diff --git a/app/components/Approvals/ApprovalModal/__snapshots__/ApprovalModal.test.tsx.snap b/app/components/Approvals/ApprovalModal/__snapshots__/ApprovalModal.test.tsx.snap index cc6203e74a7..323137ebfa6 100644 --- a/app/components/Approvals/ApprovalModal/__snapshots__/ApprovalModal.test.tsx.snap +++ b/app/components/Approvals/ApprovalModal/__snapshots__/ApprovalModal.test.tsx.snap @@ -1,43 +1,24 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ApprovalModal renders 1`] = ` - -
- test -
-
+ + +
+ test +
+
+ `; diff --git a/app/components/Base/DetailsModal.js b/app/components/Base/DetailsModal.js index da1b0096359..92b17d86e89 100644 --- a/app/components/Base/DetailsModal.js +++ b/app/components/Base/DetailsModal.js @@ -5,7 +5,7 @@ import Ionicons from 'react-native-vector-icons/Ionicons'; import { fontStyles } from '../../styles/common'; import Text from './Text'; import { useTheme } from '../../util/theme'; -import { TransactionDetailsModalSelectorsIDs } from '../../../e2e/selectors/Modals/TransactionDetailsModal.selectors'; +import { TransactionDetailsModalSelectorsIDs } from '../../../e2e/selectors/Transactions/TransactionDetailsModal.selectors'; const createStyles = (colors) => StyleSheet.create({ diff --git a/app/components/Base/RemoteImage/index.js b/app/components/Base/RemoteImage/index.js index 12e7d1721b6..5c62258dc27 100644 --- a/app/components/Base/RemoteImage/index.js +++ b/app/components/Base/RemoteImage/index.js @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { Image, @@ -67,23 +67,36 @@ const RemoteImage = (props) => { const chainId = useSelector(selectChainId); const ticker = useSelector(selectTicker); const networkName = useSelector(selectNetworkName); - const resolvedIpfsUrl = useMemo(() => { - try { - const url = new URL(props.source.uri); - if (url.protocol !== 'ipfs:') return false; - const ipfsUrl = getFormattedIpfsUrl(ipfsGateway, props.source.uri, false); - return ipfsUrl; - } catch { - return false; - } - }, [props.source.uri, ipfsGateway]); + const [resolvedIpfsUrl, setResolvedIpfsUrl] = useState(false); - const uri = resolvedIpfsUrl || source.uri; + const uri = + resolvedIpfsUrl || + (source.uri === undefined || source.uri?.startsWith('ipfs') + ? '' + : source.uri); const onError = ({ nativeEvent: { error } }) => setError(error); const [dimensions, setDimensions] = useState(null); + useEffect(() => { + resolveIpfsUrl(); + async function resolveIpfsUrl() { + try { + const url = new URL(props.source.uri); + if (url.protocol !== 'ipfs:') setResolvedIpfsUrl(false); + const ipfsUrl = await getFormattedIpfsUrl( + ipfsGateway, + props.source.uri, + false, + ); + setResolvedIpfsUrl(ipfsUrl); + } catch (err) { + setResolvedIpfsUrl(false); + } + } + }, [props.source.uri, ipfsGateway]); + useEffect(() => { const calculateImageDimensions = (imageWidth, imageHeight) => { const deviceWidth = Dimensions.get('window').width; diff --git a/app/components/Base/RemoteImage/index.test.tsx b/app/components/Base/RemoteImage/index.test.tsx index 65691852ebe..4ea9f0beaa0 100644 --- a/app/components/Base/RemoteImage/index.test.tsx +++ b/app/components/Base/RemoteImage/index.test.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { shallow } from 'enzyme'; import RemoteImage from './'; +import { getFormattedIpfsUrl } from '@metamask/assets-controllers'; +import { act, render } from '@testing-library/react-native'; jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), @@ -11,6 +13,12 @@ jest.mock('react-redux', () => ({ jest.mock('../../../components/hooks/useIpfsGateway', () => jest.fn()); +jest.mock('@metamask/assets-controllers', () => ({ + getFormattedIpfsUrl: jest.fn(), +})); + +const mockGetFormattedIpfsUrl = getFormattedIpfsUrl as jest.Mock; + describe('RemoteImage', () => { it('should render svg correctly', () => { const wrapper = shallow( @@ -34,14 +42,18 @@ describe('RemoteImage', () => { expect(wrapper).toMatchSnapshot(); }); - it('should render ipfs sources', () => { - const wrapper = shallow( + it('should render ipfs sources', async () => { + const testIpfsUri = 'ipfs://QmeE94srcYV9WwJb1p42eM4zncdLUai2N9zmMxxukoEQ23'; + mockGetFormattedIpfsUrl.mockResolvedValue(testIpfsUri); + const wrapper = render( , ); + // eslint-disable-next-line no-empty-function + await act(async () => {}); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js index 82d6268f1ed..32ec40e7203 100644 --- a/app/components/Nav/App/index.js +++ b/app/components/Nav/App/index.js @@ -34,7 +34,6 @@ import SharedDeeplinkManager from '../../../core/DeeplinkManager/SharedDeeplinkM import branch from 'react-native-branch'; import AppConstants from '../../../core/AppConstants'; import Logger from '../../../util/Logger'; -import { routingInstrumentation } from '../../../util/sentry/utils'; import { connect, useDispatch } from 'react-redux'; import { CURRENT_APP_VERSION, @@ -58,10 +57,13 @@ import Toast, { ToastContext, } from '../../../component-library/components/Toast'; import AccountSelector from '../../../components/Views/AccountSelector'; +import { TokenSortBottomSheet } from '../../../components/UI/Tokens/TokensBottomSheet/TokenSortBottomSheet.tsx'; +import { TokenFilterBottomSheet } from '../../../components/UI/Tokens/TokensBottomSheet/TokenFilterBottomSheet.tsx'; import AccountConnect from '../../../components/Views/AccountConnect'; import AccountPermissions from '../../../components/Views/AccountPermissions'; import { AccountPermissionsScreens } from '../../../components/Views/AccountPermissions/AccountPermissions.types'; import AccountPermissionsConfirmRevokeAll from '../../../components/Views/AccountPermissions/AccountPermissionsConfirmRevokeAll'; +import ConnectionDetails from '../../../components/Views/AccountPermissions/ConnectionDetails'; import { SRPQuiz } from '../../Views/Quiz'; import { TurnOffRememberMeModal } from '../../../components/UI/TurnOffRememberMeModal'; import AssetHideConfirmation from '../../Views/AssetHideConfirmation'; @@ -123,6 +125,7 @@ import NftOptions from '../../../components/Views/NftOptions'; import ShowTokenIdSheet from '../../../components/Views/ShowTokenIdSheet'; import OriginSpamModal from '../../Views/OriginSpamModal/OriginSpamModal'; import { isNetworkUiRedesignEnabled } from '../../../util/networks/isNetworkUiRedesignEnabled'; +import ChangeInSimulationModal from '../../Views/ChangeInSimulationModal/ChangeInSimulationModal'; import TooltipModal from '../../../components/Views/TooltipModal'; ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) import { SnapsExecutionWebView } from '../../../lib/snaps'; @@ -135,7 +138,13 @@ import Engine from '../../../core/Engine'; import { CHAIN_IDS } from '@metamask/transaction-controller'; import { PopularList } from '../../../util/networks/customNetworks'; import { RpcEndpointType } from '@metamask/network-controller'; -import { trace, TraceName, TraceOperation } from '../../../util/trace'; +import { + endTrace, + trace, + TraceName, + TraceOperation, +} from '../../../util/trace'; +import getUIStartupSpan from '../../../core/Performance/UIStartup'; const clearStackNavigatorOptions = { headerShown: false, @@ -422,10 +431,22 @@ const RootModalFlow = () => ( name={Routes.SHEET.REVOKE_ALL_ACCOUNT_PERMISSIONS} component={AccountPermissionsConfirmRevokeAll} /> + + + ( name={Routes.SHEET.ORIGIN_SPAM_MODAL} component={OriginSpamModal} /> + ); @@ -569,6 +594,12 @@ const App = (props) => { const sdkInit = useRef(); const [onboarded, setOnboarded] = useState(false); + trace({ + name: TraceName.NavInit, + parentContext: getUIStartupSpan(), + op: TraceOperation.NavInit, + }); + const triggerSetCurrentRoute = (route) => { dispatch(setCurrentRoute(route)); if (route === 'Wallet' || route === 'BrowserView') { @@ -584,9 +615,10 @@ const App = (props) => { setOnboarded(!!existingUser); try { if (existingUser) { + // This should only be called if the auth type is not password, which is not the case so consider removing it await trace( { - name: TraceName.BiometricAuthentication, + name: TraceName.AppStartBiometricAuthentication, op: TraceOperation.BiometricAuthentication, }, async () => { @@ -609,6 +641,7 @@ const App = (props) => { }), ); } + await Authentication.lockApp({ reset: false }); trackErrorAsAnalytics( 'App: Max Attempts Reached', @@ -617,9 +650,15 @@ const App = (props) => { ); } }; - appTriggeredAuth().catch((error) => { - Logger.error(error, 'App: Error in appTriggeredAuth'); - }); + appTriggeredAuth() + .catch((error) => { + Logger.error(error, 'App: Error in appTriggeredAuth'); + }) + .finally(() => { + endTrace({ name: TraceName.NavInit }); + + endTrace({ name: TraceName.UIStartup }); + }); }, [navigator, queueOfHandleDeeplinkFunctions]); const handleDeeplink = useCallback(({ error, params, uri }) => { @@ -669,8 +708,6 @@ const App = (props) => { }); if (!prevNavigator.current) { - // Setup navigator with Sentry instrumentation - routingInstrumentation.registerNavigationContainer(navigator); // Subscribe to incoming deeplinks // Branch.io documentation: https://help.branch.io/developers-hub/docs/react-native branch.subscribe((opts) => { @@ -955,7 +992,7 @@ const App = (props) => { diff --git a/app/components/UI/AccountApproval/index.test.tsx b/app/components/UI/AccountApproval/index.test.tsx index 231845fa1f8..0afe79dd39e 100644 --- a/app/components/UI/AccountApproval/index.test.tsx +++ b/app/components/UI/AccountApproval/index.test.tsx @@ -4,27 +4,35 @@ import { backgroundState } from '../../../util/test/initial-root-state'; import renderWithProvider from '../../../util/test/renderWithProvider'; import { MOCK_ACCOUNTS_CONTROLLER_STATE } from '../../../util/test/accountsControllerTestUtils'; -jest.mock('../../../core/Engine', () => ({ - context: { - PhishingController: { - maybeUpdateState: jest.fn(), - test: jest.fn((url: string) => { - if (url === 'phishing.com') return { result: true }; - return { result: false }; - }), - }, - KeyringController: { - getAccountKeyringType: () => Promise.resolve('HD Key Tree'), - state: { - keyrings: [ - { - accounts: ['0xC4966c0D659D99699BFD7EB54D8fafEE40e4a756'], - }, - ], +jest.mock('../../../core/Engine', () => { + const { MOCK_ACCOUNTS_CONTROLLER_STATE: mockAccountsControllerState } = + jest.requireActual('../../../util/test/accountsControllerTestUtils'); + return { + context: { + PhishingController: { + maybeUpdateState: jest.fn(), + test: jest.fn((url: string) => { + if (url === 'phishing.com') return { result: true }; + return { result: false }; + }), + }, + KeyringController: { + getAccountKeyringType: () => Promise.resolve('HD Key Tree'), + state: { + keyrings: [ + { + accounts: ['0xC4966c0D659D99699BFD7EB54D8fafEE40e4a756'], + }, + ], + }, + }, + AccountsController: { + ...mockAccountsControllerState, + state: mockAccountsControllerState, }, }, - }, -})); + }; +}); const mockInitialState = { engine: { diff --git a/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx b/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx index b62c5c2c57d..82014a8debc 100644 --- a/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx +++ b/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx @@ -53,30 +53,37 @@ jest.mock('../../../util/address', () => ({ })); const mockGetERC20BalanceOf = jest.fn().mockReturnValue(0x0186a0); -jest.mock('../../../core/Engine', () => ({ - context: { - TokensController: { - addToken: () => undefined, - }, - KeyringController: { - state: { - keyrings: [ - { - accounts: [ - '0xe64dD0AB5ad7e8C5F2bf6Ce75C34e187af8b920A', - '0x519d2CE57898513F676a5C3b66496c3C394c9CC7', - '0x07Be9763a718C0539017E2Ab6fC42853b4aEeb6B', - ], - }, - ], +jest.mock('../../../core/Engine', () => { + const { MOCK_ACCOUNTS_CONTROLLER_STATE: mockAccountsControllerState } = + jest.requireActual('../../../util/test/accountsControllerTestUtils'); + return { + context: { + TokensController: { + addToken: () => undefined, + }, + KeyringController: { + state: { + keyrings: [ + { + accounts: [ + '0xe64dD0AB5ad7e8C5F2bf6Ce75C34e187af8b920A', + '0x519d2CE57898513F676a5C3b66496c3C394c9CC7', + '0x07Be9763a718C0539017E2Ab6fC42853b4aEeb6B', + ], + }, + ], + }, }, + AccountsController: { + ...mockAccountsControllerState, + state: mockAccountsControllerState, + }, + AssetsContractController: { + getERC20BalanceOf: mockGetERC20BalanceOf, + } as Partial as AssetsContractController, }, - AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE, - AssetsContractController: { - getERC20BalanceOf: mockGetERC20BalanceOf, - } as Partial as AssetsContractController, - }, -})); + }; +}); jest.mock('../../../util/ENSUtils', () => ({ ...jest.requireActual('../../../util/ENSUtils'), diff --git a/app/components/UI/AccountInfoCard/index.js b/app/components/UI/AccountInfoCard/index.js index 321428314fb..acc0ca47a9e 100644 --- a/app/components/UI/AccountInfoCard/index.js +++ b/app/components/UI/AccountInfoCard/index.js @@ -233,7 +233,7 @@ class AccountInfoCard extends PureComponent { {accountLabelTag && ( - {strings(accountLabelTag)} + {accountLabelTag} )} diff --git a/app/components/UI/AccountInfoCard/index.test.tsx b/app/components/UI/AccountInfoCard/index.test.tsx index a2f1c1bcdde..4806be74eaf 100644 --- a/app/components/UI/AccountInfoCard/index.test.tsx +++ b/app/components/UI/AccountInfoCard/index.test.tsx @@ -12,19 +12,27 @@ import { RootState } from '../../../reducers'; import { RpcEndpointType } from '@metamask/network-controller'; import { mockNetworkState } from '../../../util/test/network'; -jest.mock('../../../core/Engine', () => ({ - resetState: jest.fn(), - context: { - KeyringController: { - state: { - keyrings: [], +jest.mock('../../../core/Engine', () => { + const { MOCK_ACCOUNTS_CONTROLLER_STATE: mockAccountsControllerState } = + jest.requireActual('../../../util/test/accountsControllerTestUtils'); + return { + resetState: jest.fn(), + context: { + KeyringController: { + state: { + keyrings: [], + }, + createNewVaultAndKeychain: () => jest.fn(), + setLocked: () => jest.fn(), + getAccountKeyringType: () => Promise.resolve('HD Key Tree'), + }, + AccountsController: { + ...mockAccountsControllerState, + state: mockAccountsControllerState, }, - createNewVaultAndKeychain: () => jest.fn(), - setLocked: () => jest.fn(), - getAccountKeyringType: () => Promise.resolve('HD Key Tree'), }, - }, -})); + }; +}); const mockInitialState: DeepPartial = { settings: { diff --git a/app/components/UI/AccountOverview/index.js b/app/components/UI/AccountOverview/index.js index 73e326e6c8e..1a73bb38746 100644 --- a/app/components/UI/AccountOverview/index.js +++ b/app/components/UI/AccountOverview/index.js @@ -417,7 +417,7 @@ class AccountOverview extends PureComponent { variant={TextVariant.BodySMBold} style={styles.tagText} > - {strings(accountLabelTag)} + {accountLabelTag}
)} diff --git a/app/components/UI/AccountOverview/index.test.tsx b/app/components/UI/AccountOverview/index.test.tsx index 303d3fe177d..49fb5077401 100644 --- a/app/components/UI/AccountOverview/index.test.tsx +++ b/app/components/UI/AccountOverview/index.test.tsx @@ -11,23 +11,31 @@ import { const mockedEngine = Engine; -jest.mock('../../../core/Engine.ts', () => ({ - init: () => mockedEngine.init({}), - context: { - KeyringController: { - getQRKeyringState: async () => ({ subscribe: () => ({}) }), - state: { - keyrings: [ - { - accounts: ['0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272'], - index: 0, - type: 'HD Key Tree', - }, - ], +jest.mock('../../../core/Engine.ts', () => { + const { MOCK_ACCOUNTS_CONTROLLER_STATE: mockAccountsControllerState } = + jest.requireActual('../../../util/test/accountsControllerTestUtils'); + return { + init: () => mockedEngine.init({}), + context: { + KeyringController: { + getQRKeyringState: async () => ({ subscribe: () => ({}) }), + state: { + keyrings: [ + { + accounts: ['0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272'], + index: 0, + type: 'HD Key Tree', + }, + ], + }, + }, + AccountsController: { + ...mockAccountsControllerState, + state: mockAccountsControllerState, }, }, - }, -})); + }; +}); const mockInitialState = { settings: {}, diff --git a/app/components/UI/AccountSelectorList/AccountSelector.test.tsx b/app/components/UI/AccountSelectorList/AccountSelector.test.tsx index 306c88825c4..506d7e6e37d 100644 --- a/app/components/UI/AccountSelectorList/AccountSelector.test.tsx +++ b/app/components/UI/AccountSelectorList/AccountSelector.test.tsx @@ -5,7 +5,7 @@ import renderWithProvider from '../../../util/test/renderWithProvider'; import AccountSelectorList from './AccountSelectorList'; import { useAccounts } from '../../../components/hooks/useAccounts'; import { View } from 'react-native'; -import { ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID } from '../../../../wdio/screen-objects/testIDs/Components/AccountListComponent.testIds'; +import { AccountListViewSelectorsIDs } from '../../../../e2e/selectors/AccountListView.selectors'; import { backgroundState } from '../../../util/test/initial-root-state'; import { regex } from '../../../../app/util/regex'; import { @@ -16,6 +16,7 @@ import { } from '../../../util/test/accountsControllerTestUtils'; import { mockNetworkState } from '../../../util/test/network'; import { CHAIN_IDS } from '@metamask/transaction-controller'; +import { AccountSelectorListProps } from './AccountSelectorList.types'; const BUSINESS_ACCOUNT = '0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272'; const PERSONAL_ACCOUNT = '0xd018538C87232FF95acbCe4870629b75640a78E7'; @@ -33,6 +34,15 @@ jest.mock('../../../util/address', () => { }; }); +const mockNavigate = jest.fn(); + +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: () => ({ + navigate: mockNavigate, + }), +})); + const initialState = { engine: { backgroundState: { @@ -73,8 +83,9 @@ const initialState = { const onSelectAccount = jest.fn(); const onRemoveImportedAccount = jest.fn(); - -const AccountSelectorListUseAccounts = () => { +const AccountSelectorListUseAccounts: React.FC = ({ + privacyMode = false, +}) => { const { accounts, ensByAccountAddress } = useAccounts(); return ( { accounts={accounts} ensByAccountAddress={ensByAccountAddress} isRemoveAccountEnabled + privacyMode={privacyMode} /> ); }; @@ -109,7 +121,7 @@ const renderComponent = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any state: any = {}, AccountSelectorListTest = AccountSelectorListUseAccounts, -) => renderWithProvider(, { state }); +) => renderWithProvider(, { state }); describe('AccountSelectorList', () => { beforeEach(() => { @@ -128,10 +140,10 @@ describe('AccountSelectorList', () => { await waitFor(async () => { const businessAccountItem = await queryByTestId( - `${ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, + `${AccountListViewSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, ); const personalAccountItem = await queryByTestId( - `${ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${PERSONAL_ACCOUNT}`, + `${AccountListViewSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${PERSONAL_ACCOUNT}`, ); expect(within(businessAccountItem).getByText(regex.eth(1))).toBeDefined(); @@ -171,7 +183,7 @@ describe('AccountSelectorList', () => { expect(accounts.length).toBe(1); const businessAccountItem = await queryByTestId( - `${ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, + `${AccountListViewSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, ); expect(within(businessAccountItem).getByText(regex.eth(1))).toBeDefined(); @@ -205,11 +217,11 @@ describe('AccountSelectorList', () => { expect(within(accountNameItems[1]).getByText('Account 2')).toBeDefined(); }); }); - it('renders "Snaps (beta)" tag for Snap accounts', async () => { - const mockAccountsWithSnap = createMockAccountsControllerStateWithSnap([ - MOCK_ADDRESS_1, - MOCK_ADDRESS_2, - ]); + it('renders the snap name tag for Snap accounts', async () => { + const mockAccountsWithSnap = createMockAccountsControllerStateWithSnap( + [MOCK_ADDRESS_1, MOCK_ADDRESS_2], + 'MetaMask Simple Snap Keyring', + ); const stateWithSnapAccount = { ...initialState, @@ -225,8 +237,50 @@ describe('AccountSelectorList', () => { const { queryByText } = renderComponent(stateWithSnapAccount); await waitFor(async () => { - const snapTag = await queryByText('Snaps (beta)'); + const snapTag = await queryByText('MetaMask Simple Snap Keyring'); expect(snapTag).toBeDefined(); }); }); + it('Text is not hidden when privacy mode is off', async () => { + const state = { + ...initialState, + privacyMode: false, + }; + + const { queryByTestId } = renderComponent(state); + + await waitFor(() => { + const businessAccountItem = queryByTestId( + `${AccountListViewSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, + ); + + expect(within(businessAccountItem).getByText(regex.eth(1))).toBeDefined(); + expect( + within(businessAccountItem).getByText(regex.usd(3200)), + ).toBeDefined(); + + expect(within(businessAccountItem).queryByText('••••••')).toBeNull(); + }); + }); + it('Text is hidden when privacy mode is on', async () => { + const state = { + ...initialState, + privacyMode: true, + }; + + const { queryByTestId } = renderComponent(state); + + await waitFor(() => { + const businessAccountItem = queryByTestId( + `${AccountListViewSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, + ); + + expect(within(businessAccountItem).queryByText(regex.eth(1))).toBeNull(); + expect( + within(businessAccountItem).queryByText(regex.usd(3200)), + ).toBeNull(); + + expect(within(businessAccountItem).getByText('••••••')).toBeDefined(); + }); + }); }); diff --git a/app/components/UI/AccountSelectorList/AccountSelectorList.styles.ts b/app/components/UI/AccountSelectorList/AccountSelectorList.styles.ts index 90c0ffedf6d..a1103280571 100644 --- a/app/components/UI/AccountSelectorList/AccountSelectorList.styles.ts +++ b/app/components/UI/AccountSelectorList/AccountSelectorList.styles.ts @@ -13,6 +13,7 @@ const styleSheet = () => StyleSheet.create({ balancesContainer: { alignItems: 'flex-end', + flexDirection: 'column', }, balanceLabel: { textAlign: 'right' }, }); diff --git a/app/components/UI/AccountSelectorList/AccountSelectorList.tsx b/app/components/UI/AccountSelectorList/AccountSelectorList.tsx index 75592d6684b..30b8241836f 100644 --- a/app/components/UI/AccountSelectorList/AccountSelectorList.tsx +++ b/app/components/UI/AccountSelectorList/AccountSelectorList.tsx @@ -1,22 +1,28 @@ // Third party dependencies. import React, { useCallback, useRef } from 'react'; -import { Alert, ListRenderItem, Platform, View } from 'react-native'; +import { Alert, ListRenderItem, View, ViewStyle } from 'react-native'; import { FlatList } from 'react-native-gesture-handler'; import { useSelector } from 'react-redux'; +import { useNavigation } from '@react-navigation/native'; import { KeyringTypes } from '@metamask/keyring-controller'; import type { Hex } from '@metamask/utils'; // External dependencies. +import { selectInternalAccounts } from '../../../selectors/accountsController'; import Cell, { CellVariant, } from '../../../component-library/components/Cells/Cell'; +import { InternalAccount } from '@metamask/keyring-api'; import { useStyles } from '../../../component-library/hooks'; -import Text from '../../../component-library/components/Texts/Text'; +import { TextColor } from '../../../component-library/components/Texts/Text'; +import SensitiveText, { + SensitiveTextLength, +} from '../../../component-library/components/Texts/SensitiveText'; import AvatarGroup from '../../../component-library/components/Avatars/AvatarGroup'; import { formatAddress, - safeToChecksumAddress, getLabelTextByAddress, + safeToChecksumAddress, } from '../../../util/address'; import { AvatarAccountType } from '../../../component-library/components/Avatars/Avatar/variants/AvatarAccount'; import { isDefaultAccountName } from '../../../util/ENSUtils'; @@ -25,12 +31,13 @@ import { AvatarVariant } from '../../../component-library/components/Avatars/Ava import { Account, Assets } from '../../hooks/useAccounts'; import UntypedEngine from '../../../core/Engine'; import { removeAccountsFromPermissions } from '../../../core/Permissions'; +import Routes from '../../../constants/navigation/Routes'; // Internal dependencies. import { AccountSelectorListProps } from './AccountSelectorList.types'; import styleSheet from './AccountSelectorList.styles'; -import generateTestId from '../../../../wdio/utils/generateTestId'; -import { ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID } from '../../../../wdio/screen-objects/testIDs/Components/AccountListComponent.testIds.js'; +import { AccountListViewSelectorsIDs } from '../../../../e2e/selectors/AccountListView.selectors'; +import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors'; const AccountSelectorList = ({ onSelectAccount, @@ -44,8 +51,10 @@ const AccountSelectorList = ({ isSelectionDisabled, isRemoveAccountEnabled = false, isAutoScrollEnabled = true, + privacyMode = false, ...props }: AccountSelectorListProps) => { + const { navigate } = useNavigation(); // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any const Engine = UntypedEngine as any; @@ -62,29 +71,46 @@ const AccountSelectorList = ({ : AvatarAccountType.JazzIcon, ); + const internalAccounts = useSelector(selectInternalAccounts); const getKeyExtractor = ({ address }: Account) => address; const renderAccountBalances = useCallback( - ({ fiatBalance, tokens }: Assets, address: string) => ( - - {fiatBalance} - {tokens && ( - ({ - ...tokenObj, - variant: AvatarVariant.Token, - }))} - /> - )} - - ), - [styles.balancesContainer, styles.balanceLabel], + ({ fiatBalance, tokens }: Assets, address: string) => { + const fiatBalanceStrSplit = fiatBalance.split('\n'); + const fiatBalanceAmount = fiatBalanceStrSplit[0] || ''; + const tokenTicker = fiatBalanceStrSplit[1] || ''; + return ( + + + {fiatBalanceAmount} + + + {tokenTicker} + + {tokens && ( + ({ + ...tokenObj, + variant: AvatarVariant.Token, + }))} + /> + )} + + ); + }, + [styles.balancesContainer, styles.balanceLabel, privacyMode], ); const onLongPress = useCallback( @@ -151,6 +177,23 @@ const AccountSelectorList = ({ ], ); + const onNavigateToAccountActions = useCallback( + (selectedAccount: string) => { + const account = internalAccounts.find( + (accountData: InternalAccount) => + accountData.address.toLowerCase() === selectedAccount.toLowerCase(), + ); + + if (!account) return; + + navigate(Routes.MODAL.ROOT_MODAL_FLOW, { + screen: Routes.SHEET.ACCOUNT_ACTIONS, + params: { selectedAccount: account }, + }); + }, + [navigate, internalAccounts], + ); + const renderAccountItem: ListRenderItem = useCallback( ({ item: { name, address, assets, type, isSelected, balanceError }, @@ -164,7 +207,7 @@ const AccountSelectorList = ({ const isDisabled = !!balanceError || isLoading || isSelectionDisabled; const cellVariant = isMultiSelect ? CellVariant.MultiSelect - : CellVariant.Select; + : CellVariant.SelectWithMenu; let isSelectedAccount = isSelected; if (selectedAddresses) { const lowercasedSelectedAddresses = selectedAddresses.map( @@ -175,12 +218,16 @@ const AccountSelectorList = ({ ); } - const cellStyle = { + const cellStyle: ViewStyle = { opacity: isLoading ? 0.5 : 1, }; + if (!isMultiSelect) { + cellStyle.alignItems = 'center'; + } return ( { onLongPress({ address, @@ -193,6 +240,7 @@ const AccountSelectorList = ({ isSelected={isSelectedAccount} title={accountName} secondaryText={shortAddress} + showSecondaryTextIcon={false} tertiaryText={balanceError} onPress={() => onSelectAccount?.(address, isSelectedAccount)} avatarProps={{ @@ -200,9 +248,13 @@ const AccountSelectorList = ({ type: accountAvatarType, accountAddress: address, }} - tagLabel={tagLabel ? strings(tagLabel) : tagLabel} + tagLabel={tagLabel} disabled={isDisabled} style={cellStyle} + buttonProps={{ + onButtonClick: () => onNavigateToAccountActions(address), + buttonTestId: `${WalletViewSelectorsIDs.ACCOUNT_ACTIONS}-${index}`, + }} > {renderRightAccessory?.(address, accountName) || (assets && renderAccountBalances(assets, address))} @@ -210,6 +262,7 @@ const AccountSelectorList = ({ ); }, [ + onNavigateToAccountActions, accountAvatarType, onSelectAccount, renderAccountBalances, diff --git a/app/components/UI/AccountSelectorList/AccountSelectorList.types.ts b/app/components/UI/AccountSelectorList/AccountSelectorList.types.ts index 4059c710cc9..a2f651c718e 100644 --- a/app/components/UI/AccountSelectorList/AccountSelectorList.types.ts +++ b/app/components/UI/AccountSelectorList/AccountSelectorList.types.ts @@ -56,4 +56,8 @@ export interface AccountSelectorListProps * Optional boolean to enable removing accounts. */ isRemoveAccountEnabled?: boolean; + /** + * Optional boolean to indicate if privacy mode is enabled. + */ + privacyMode?: boolean; } diff --git a/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap b/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap index 49c9d6e96b1..dd4954812d8 100644 --- a/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap +++ b/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap @@ -57,559 +57,772 @@ exports[`AccountSelectorList renders all accounts with balances 1`] = ` onLayout={[Function]} style={null} > - - - - - - + - - - + propList={ + [ + "fill", + ] + } + width={32} + x={0} + y={0} + /> + + + + + - - - - Account 1 - - - 0xC495...D272 - - - - $3200.00 -1 ETH + Account 1 + + + 0xC495...D272 + + + + + + + $3200.00 + + + 1 ETH + + -
+ + + + - + testID="main-wallet-account-actions-0" + > + + - + - - - - - - + - - - + propList={ + [ + "fill", + ] + } + width={32} + x={0} + y={0} + /> + + + + + - - - - Account 2 - - - 0xd018...78E7 - - - - $6400.00 -2 ETH + Account 2 + + + 0xd018...78E7 + + + + + + + $6400.00 + + + 2 ETH + + + + + + + - + @@ -672,492 +885,672 @@ exports[`AccountSelectorList renders all accounts with right accessory 1`] = ` onLayout={[Function]} style={null} > - - - - - - + - - - + propList={ + [ + "fill", + ] + } + width={32} + x={0} + y={0} + /> + + + + + - - - - Account 1 - - + Account 1 + + + + 0xC495...D272 + + + + - 0xC495...D272 - - - - - 0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272 - Account 1 + + 0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272 - Account 1 + + + + + + - + - - - - - - + - - - + propList={ + [ + "fill", + ] + } + width={32} + x={0} + y={0} + /> + + + + + - - - - Account 2 - - + Account 2 + + + + 0xd018...78E7 + + + + - 0xd018...78E7 - - - - - 0xd018538C87232FF95acbCe4870629b75640a78E7 - Account 2 + + 0xd018538C87232FF95acbCe4870629b75640a78E7 - Account 2 + + + + + + - + @@ -1220,559 +1613,772 @@ exports[`AccountSelectorList renders correctly 1`] = ` onLayout={[Function]} style={null} > - - - - - - + - - - + propList={ + [ + "fill", + ] + } + width={32} + x={0} + y={0} + /> + + + + + - - - - Account 1 - - - 0xC495...D272 - - - - $3200.00 -1 ETH + Account 1 + + + 0xC495...D272 + + + + + + + $3200.00 + + + 1 ETH + + - - + > + + + + + + + - + - - - - - - + - - - + propList={ + [ + "fill", + ] + } + width={32} + x={0} + y={0} + /> + + + + + - - - - Account 2 - - - 0xd018...78E7 - - - - $6400.00 -2 ETH + Account 2 + + + 0xd018...78E7 + + + + + + + $6400.00 + + + 2 ETH + + + + + + + - + @@ -1832,293 +2438,490 @@ exports[`AccountSelectorList should render all accounts but only the balance for onLayout={[Function]} style={null} > - - - - - - - Account 1 - - + style={ + { + "flex": 1, + } + } + /> + - $3200.00 -1 ETH + Account 1 + + + 0xC495...D272 + + + + + + + $3200.00 + + + 1 ETH + + - + + + + - + testID="main-wallet-account-actions-0" + > + + - + - - - - - - - Account 2 - - + + - 0xd018...78E7 - + + Account 2 + + + + 0xd018...78E7 + + + + + + + + - + diff --git a/app/components/UI/AddressCopy/AddressCopy.styles.ts b/app/components/UI/AddressCopy/AddressCopy.styles.ts index 089c48d5136..46d5ba9d066 100644 --- a/app/components/UI/AddressCopy/AddressCopy.styles.ts +++ b/app/components/UI/AddressCopy/AddressCopy.styles.ts @@ -1,26 +1,14 @@ import { StyleSheet } from 'react-native'; -// External dependencies. -import { Theme } from '../../../util/theme/models'; -const styleSheet = (params: { theme: Theme }) => { - const { theme } = params; - const { colors } = theme; - - return StyleSheet.create({ +const styleSheet = () => + StyleSheet.create({ address: { flexDirection: 'row', alignItems: 'center', }, copyButton: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: colors.primary.muted, - borderRadius: 20, - paddingHorizontal: 12, padding: 4, - marginLeft: 12, }, - icon: { marginLeft: 4 }, }); -}; + export default styleSheet; diff --git a/app/components/UI/AddressCopy/AddressCopy.tsx b/app/components/UI/AddressCopy/AddressCopy.tsx index d295c5f75ad..a99e46a4727 100644 --- a/app/components/UI/AddressCopy/AddressCopy.tsx +++ b/app/components/UI/AddressCopy/AddressCopy.tsx @@ -3,12 +3,7 @@ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; // External dependencies -import Text, { - TextColor, - TextVariant, -} from '../../../component-library/components/Texts/Text'; import { TouchableOpacity } from 'react-native-gesture-handler'; -import { formatAddress } from '../../../util/address'; import Icon, { IconColor, IconName, @@ -25,12 +20,11 @@ import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletV // Internal dependencies import styleSheet from './AddressCopy.styles'; -import { AddressCopyProps } from './AddressCopy.types'; import { selectSelectedInternalAccount } from '../../../selectors/accountsController'; import { useMetrics } from '../../../components/hooks/useMetrics'; import { toChecksumHexAddress } from '@metamask/controller-utils'; -const AddressCopy = ({ formatAddressType = 'full' }: AddressCopyProps) => { +const AddressCopy = () => { const { styles } = useStyles(styleSheet, {}); const dispatch = useDispatch(); @@ -69,28 +63,15 @@ const AddressCopy = ({ formatAddressType = 'full' }: AddressCopyProps) => { }; return ( - - {strings('asset_overview.address')}: - - - {selectedInternalAccount - ? formatAddress(selectedInternalAccount.address, formatAddressType) - : null} - diff --git a/app/components/UI/AddressCopy/AddressCopy.types.ts b/app/components/UI/AddressCopy/AddressCopy.types.ts deleted file mode 100644 index 6efa05bbea0..00000000000 --- a/app/components/UI/AddressCopy/AddressCopy.types.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface AddressCopyProps { - formatAddressType?: 'short' | 'mid' | 'full'; -} diff --git a/app/components/UI/AddressInputs/index.js b/app/components/UI/AddressInputs/index.js index 3f2ad375619..f1f59b4af4f 100644 --- a/app/components/UI/AddressInputs/index.js +++ b/app/components/UI/AddressInputs/index.js @@ -178,11 +178,7 @@ const createStyles = (colors, layout = 'horizontal') => { }); }; -const AddressName = ({ - toAddressName, - confusableCollection = [], - accountLabel, -}) => { +const AddressName = ({ toAddressName, confusableCollection = [] }) => { const { colors } = useTheme(); const styles = createStyles(colors); if (confusableCollection.length) { @@ -214,14 +210,6 @@ const AddressName = ({ {toAddressName} - {accountLabel && ( - - {strings(accountLabel)} - - )} ); }; @@ -229,7 +217,6 @@ const AddressName = ({ AddressName.propTypes = { toAddressName: PropTypes.string, confusableCollection: PropTypes.array, - accountLabel: PropTypes.string, }; export const AddressTo = (props) => { @@ -252,7 +239,6 @@ export const AddressTo = (props) => { isFromAddressBook = false, layout = 'horizontal', } = props; - const accountLabel = getLabelTextByAddress(toSelectedAddress); const { colors, themeAppearance } = useTheme(); const styles = createStyles(colors, layout); @@ -292,7 +278,6 @@ export const AddressTo = (props) => { )} @@ -414,7 +399,6 @@ export const AddressTo = (props) => { @@ -582,7 +566,6 @@ export const AddressFrom = (props) => { fromAccountAddress, layout = 'horizontal', } = props; - const accountLabel = getLabelTextByAddress(fromAccountAddress); const { colors } = useTheme(); const styles = createStyles(colors, layout); @@ -603,14 +586,6 @@ export const AddressFrom = (props) => { {fromAccountName} - {accountLabel && ( - - {strings(accountLabel)} - - )} {`${strings( 'transactions.address_from_balance', diff --git a/app/components/UI/AnimatedTransactionModal/__snapshots__/index.test.tsx.snap b/app/components/UI/AnimatedTransactionModal/__snapshots__/index.test.tsx.snap index e99df64bf59..edb1bdd219f 100644 --- a/app/components/UI/AnimatedTransactionModal/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/AnimatedTransactionModal/__snapshots__/index.test.tsx.snap @@ -1,28 +1,23 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AnimatedTransactionModal should render correctly 1`] = ` - `; diff --git a/app/components/UI/AnimatedTransactionModal/index.test.tsx b/app/components/UI/AnimatedTransactionModal/index.test.tsx index 9d8108ca3ef..4a22108e3df 100644 --- a/app/components/UI/AnimatedTransactionModal/index.test.tsx +++ b/app/components/UI/AnimatedTransactionModal/index.test.tsx @@ -1,15 +1,23 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { render, screen } from '@testing-library/react-native'; import AnimatedTransactionModal from './'; import { View } from 'react-native'; +import { ThemeContext } from '../../../util/theme'; + +const mockTheme = { + colors: { background: { default: 'white' } }, + themeAppearance: 'light', +}; describe('AnimatedTransactionModal', () => { it('should render correctly', () => { - const wrapper = shallow( - - - , + render( + + + + + , ); - expect(wrapper).toMatchSnapshot(); + expect(screen.toJSON()).toMatchSnapshot(); }); }); diff --git a/app/components/UI/AssetElement/index.constants.ts b/app/components/UI/AssetElement/index.constants.ts new file mode 100644 index 00000000000..1b14c68f51c --- /dev/null +++ b/app/components/UI/AssetElement/index.constants.ts @@ -0,0 +1,2 @@ +export const FIAT_BALANCE_TEST_ID = 'fiat-balance-test-id'; +export const MAIN_BALANCE_TEST_ID = 'main-balance-test-id'; diff --git a/app/components/UI/AssetElement/index.test.tsx b/app/components/UI/AssetElement/index.test.tsx index 1d178a7f4c3..e664027b309 100644 --- a/app/components/UI/AssetElement/index.test.tsx +++ b/app/components/UI/AssetElement/index.test.tsx @@ -3,6 +3,7 @@ import { shallow } from 'enzyme'; import { render, fireEvent } from '@testing-library/react-native'; import AssetElement from './'; import { getAssetTestId } from '../../../../wdio/screen-objects/testIDs/Screens/WalletView.testIds'; +import { FIAT_BALANCE_TEST_ID, MAIN_BALANCE_TEST_ID } from './index.constants'; describe('AssetElement', () => { const onPressMock = jest.fn(); @@ -54,4 +55,34 @@ describe('AssetElement', () => { expect(onLongPressMock).toHaveBeenCalledWith(erc20Token); }); + + it('renders the fiat and token balance', () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId(FIAT_BALANCE_TEST_ID)).toBeDefined(); + expect(getByTestId(MAIN_BALANCE_TEST_ID)).toBeDefined(); + }); + + it('renders the fiat balance with privacy mode', () => { + const { getByTestId } = render( + , + ); + + const fiatBalance = getByTestId(FIAT_BALANCE_TEST_ID); + const mainBalance = getByTestId(MAIN_BALANCE_TEST_ID); + + expect(fiatBalance.props.children).toBe('•••••••••'); + expect(mainBalance.props.children).toBe('••••••'); + }); }); diff --git a/app/components/UI/AssetElement/index.tsx b/app/components/UI/AssetElement/index.tsx index a2810c48db0..ff39a4eac1a 100644 --- a/app/components/UI/AssetElement/index.tsx +++ b/app/components/UI/AssetElement/index.tsx @@ -1,9 +1,7 @@ /* eslint-disable react/prop-types */ import React from 'react'; import { TouchableOpacity, StyleSheet, Platform, View } from 'react-native'; -import Text, { - TextVariant, -} from '../../../component-library/components/Texts/Text'; +import { TextVariant } from '../../../component-library/components/Texts/Text'; import SkeletonText from '../Ramp/components/SkeletonText'; import { TokenI } from '../Tokens/types'; import generateTestId from '../../../../wdio/utils/generateTestId'; @@ -15,6 +13,10 @@ import { import { Colors } from '../../../util/theme/models'; import { fontStyles } from '../../../styles/common'; import { useTheme } from '../../../util/theme'; +import SensitiveText, { + SensitiveTextLength, +} from '../../../component-library/components/Texts/SensitiveText'; +import { FIAT_BALANCE_TEST_ID, MAIN_BALANCE_TEST_ID } from './index.constants'; interface AssetElementProps { children?: React.ReactNode; @@ -23,6 +25,7 @@ interface AssetElementProps { onLongPress?: ((asset: TokenI) => void) | null; balance?: string; mainBalance?: string | null; + privacyMode?: boolean; } const createStyles = (colors: Colors) => @@ -63,6 +66,7 @@ const AssetElement: React.FC = ({ mainBalance = null, onPress, onLongPress, + privacyMode = false, }) => { const { colors } = useTheme(); const styles = createStyles(colors); @@ -75,6 +79,8 @@ const AssetElement: React.FC = ({ onLongPress?.(asset); }; + // TODO: Use the SensitiveText component when it's available + // when privacyMode is true, we should hide the balance and the fiat return ( = ({ {balance && ( - {balance === TOKEN_BALANCE_LOADING ? ( ) : ( balance )} - + )} {mainBalance ? ( - + {mainBalance === TOKEN_BALANCE_LOADING ? ( ) : ( mainBalance )} - + ) : null} diff --git a/app/components/UI/AssetList/index.test.tsx b/app/components/UI/AssetList/index.test.tsx index cde9e86a876..b3a7e119ad1 100644 --- a/app/components/UI/AssetList/index.test.tsx +++ b/app/components/UI/AssetList/index.test.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { render } from '@testing-library/react-native'; import AssetList from './'; describe('AssetList', () => { it('should render correctly', () => { - const wrapper = shallow( + const { toJSON } = render( { selectedAsset={{ address: '0xABC', symbol: 'ABC', decimals: 0 }} />, ); - expect(wrapper).toMatchSnapshot(); + expect(toJSON()).toMatchSnapshot(); }); }); diff --git a/app/components/UI/AssetOverview/AssetOverview.tsx b/app/components/UI/AssetOverview/AssetOverview.tsx index 2833c18f8ef..995d61584c0 100644 --- a/app/components/UI/AssetOverview/AssetOverview.tsx +++ b/app/components/UI/AssetOverview/AssetOverview.tsx @@ -258,10 +258,7 @@ const AssetOverview: React.FC = ({ } return ( - + {asset.hasBalanceError ? ( renderWarning() ) : ( diff --git a/app/components/UI/AssetOverview/Balance/__snapshots__/index.test.tsx.snap b/app/components/UI/AssetOverview/Balance/__snapshots__/index.test.tsx.snap index d233e8e44fd..0a66cea0de2 100644 --- a/app/components/UI/AssetOverview/Balance/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/AssetOverview/Balance/__snapshots__/index.test.tsx.snap @@ -203,6 +203,7 @@ exports[`Balance should render correctly with a fiat balance 1`] = ` "lineHeight": 24, } } + testID="fiat-balance-test-id" > 456 @@ -220,6 +221,7 @@ exports[`Balance should render correctly with a fiat balance 1`] = ` "textTransform": "uppercase", } } + testID="main-balance-test-id" > 123 @@ -433,6 +435,7 @@ exports[`Balance should render correctly without a fiat balance 1`] = ` "textTransform": "uppercase", } } + testID="main-balance-test-id" > 123 diff --git a/app/components/UI/AssetOverview/__snapshots__/AssetOverview.test.tsx.snap b/app/components/UI/AssetOverview/__snapshots__/AssetOverview.test.tsx.snap index 5ec60e73b6f..cc3cbd21a25 100644 --- a/app/components/UI/AssetOverview/__snapshots__/AssetOverview.test.tsx.snap +++ b/app/components/UI/AssetOverview/__snapshots__/AssetOverview.test.tsx.snap @@ -390,16 +390,27 @@ exports[`AssetOverview should render correctly 1`] = ` } > @@ -485,16 +496,27 @@ exports[`AssetOverview should render correctly 1`] = ` } > @@ -580,16 +602,27 @@ exports[`AssetOverview should render correctly 1`] = ` } > @@ -675,16 +708,27 @@ exports[`AssetOverview should render correctly 1`] = ` } > @@ -770,16 +814,27 @@ exports[`AssetOverview should render correctly 1`] = ` } > @@ -1060,6 +1115,7 @@ exports[`AssetOverview should render correctly 1`] = ` "textTransform": "uppercase", } } + testID="main-balance-test-id" > 0 ETH diff --git a/app/components/UI/BlockingActionModal/__snapshots__/index.test.tsx.snap b/app/components/UI/BlockingActionModal/__snapshots__/index.test.tsx.snap index 757058bdf97..80fd1c281a9 100644 --- a/app/components/UI/BlockingActionModal/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/BlockingActionModal/__snapshots__/index.test.tsx.snap @@ -1,42 +1,23 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`BlockingActionModal should render correctly 1`] = ` - + - - Please wait - - + > + + Please wait + + + - + `; diff --git a/app/components/UI/BlockingActionModal/index.test.tsx b/app/components/UI/BlockingActionModal/index.test.tsx index bb386a79554..c48dfcb4cc8 100644 --- a/app/components/UI/BlockingActionModal/index.test.tsx +++ b/app/components/UI/BlockingActionModal/index.test.tsx @@ -1,15 +1,15 @@ import React from 'react'; import { Text } from 'react-native'; -import { shallow } from 'enzyme'; +import { render, screen } from '@testing-library/react-native'; import BlockingActionModal from './'; describe('BlockingActionModal', () => { it('should render correctly', () => { - const wrapper = shallow( + render( {'Please wait'} , ); - expect(wrapper).toMatchSnapshot(); + expect(screen.toJSON()).toMatchSnapshot(); }); }); diff --git a/app/components/UI/BrowserBottomBar/index.js b/app/components/UI/BrowserBottomBar/index.js index b005dfe0c15..875bf6f93ac 100644 --- a/app/components/UI/BrowserBottomBar/index.js +++ b/app/components/UI/BrowserBottomBar/index.js @@ -11,15 +11,7 @@ import { MetaMetricsEvents } from '../../../core/Analytics'; import Device from '../../../util/device'; import { ThemeContext, mockTheme } from '../../../util/theme'; -import generateTestId from '../../../../wdio/utils/generateTestId'; -import { - HOME_BUTTON, - TABS_BUTTON, - FORWARD_BUTTON, - BACK_BUTTON, - OPTIONS_BUTTON, - SEARCH_BUTTON, -} from '../../../../wdio/screen-objects/testIDs/BrowserScreen/BrowserScreen.testIds'; +import { BrowserViewSelectorsIDs } from '../../../../e2e/selectors/Browser/BrowserView.selectors'; import { withMetricsAwareness } from '../../../components/hooks/useMetrics'; // NOTE: not needed anymore. The use of BottomTabBar already accomodates the home indicator height @@ -160,7 +152,7 @@ class BrowserBottomBar extends PureComponent { @@ -192,14 +184,14 @@ class BrowserBottomBar extends PureComponent { @@ -207,7 +199,7 @@ class BrowserBottomBar extends PureComponent { diff --git a/app/components/UI/ConfirmAddAsset/ConfirmAddAsset.test.tsx b/app/components/UI/ConfirmAddAsset/ConfirmAddAsset.test.tsx index ed1a2f5fdc3..99054057d7e 100644 --- a/app/components/UI/ConfirmAddAsset/ConfirmAddAsset.test.tsx +++ b/app/components/UI/ConfirmAddAsset/ConfirmAddAsset.test.tsx @@ -44,6 +44,7 @@ jest.mock('../../../util/navigation/navUtils', () => ({ ticker: 'ETH', addTokenList: jest.fn(), }), + createNavigationDetails: jest.fn(), })); const mockUseBalanceInitialValue: Partial> = { @@ -101,14 +102,12 @@ describe('ConfirmAddAsset', () => { expect(getByText('USDT')).toBeTruthy(); expect(getByText('$27.02')).toBeTruthy(); }); - it('handles cancel button click', () => { const { getByText } = renderWithProvider(, { state: mockInitialState, }); const cancelButton = getByText('Cancel'); fireEvent.press(cancelButton); - expect(getByText('Are you sure you want to exit?')).toBeTruthy(); expect( getByText('Your search information will not be saved.'), diff --git a/app/components/UI/DeleteWalletModal/index.tsx b/app/components/UI/DeleteWalletModal/index.tsx index fb518e737c1..843fad7db96 100644 --- a/app/components/UI/DeleteWalletModal/index.tsx +++ b/app/components/UI/DeleteWalletModal/index.tsx @@ -21,7 +21,7 @@ import { tlc } from '../../../util/general'; import { useTheme } from '../../../util/theme'; import Device from '../../../util/device'; import Routes from '../../../constants/navigation/Routes'; -import { DeleteWalletModalSelectorsIDs } from '../../../../e2e/selectors/Modals/DeleteWalletModal.selectors'; +import { DeleteWalletModalSelectorsIDs } from '../../../../e2e/selectors/Settings/SecurityAndPrivacy/DeleteWalletModal.selectors'; import generateTestId from '../../../../wdio/utils/generateTestId'; import { MetaMetricsEvents } from '../../../core/Analytics'; import { useMetrics } from '../../../components/hooks/useMetrics'; diff --git a/app/components/UI/DrawerView/index.js b/app/components/UI/DrawerView/index.js index 7b3ba218e01..d3aea2d1ba5 100644 --- a/app/components/UI/DrawerView/index.js +++ b/app/components/UI/DrawerView/index.js @@ -490,7 +490,7 @@ class DrawerView extends PureComponent { return label ? ( - {strings(label)} + {label} ) : null; diff --git a/app/components/UI/DrawerView/index.test.tsx b/app/components/UI/DrawerView/index.test.tsx index 811302bac26..11a92223e2a 100644 --- a/app/components/UI/DrawerView/index.test.tsx +++ b/app/components/UI/DrawerView/index.test.tsx @@ -14,16 +14,24 @@ const mockInitialState = { }, }; -jest.mock('../../../core/Engine', () => ({ - getTotalFiatAccountBalance: () => ({ ethFiat: 0, tokenFiat: 0 }), - context: { - KeyringController: { - state: { - keyrings: [], +jest.mock('../../../core/Engine', () => { + const { MOCK_ACCOUNTS_CONTROLLER_STATE: mockAccountsControllerState } = + jest.requireActual('../../../util/test/accountsControllerTestUtils'); + return { + getTotalFiatAccountBalance: () => ({ ethFiat: 0, tokenFiat: 0 }), + context: { + KeyringController: { + state: { + keyrings: [], + }, + }, + AccountsController: { + ...mockAccountsControllerState, + state: mockAccountsControllerState, }, }, - }, -})); + }; +}); describe('DrawerView', () => { it('should render correctly', () => { diff --git a/app/components/UI/ManageNetworks/ManageNetworks.tsx b/app/components/UI/ManageNetworks/ManageNetworks.tsx index e5ac44ab8ea..56fed1d3a22 100644 --- a/app/components/UI/ManageNetworks/ManageNetworks.tsx +++ b/app/components/UI/ManageNetworks/ManageNetworks.tsx @@ -20,7 +20,7 @@ import Routes from '../../../constants/navigation/Routes'; import getDecimalChainId from '../../../util/networks/getDecimalChainId'; import { useMetrics } from '../../../components/hooks/useMetrics'; import { MetaMetricsEvents } from '../../../core/Analytics'; -import { ConnectedAccountsSelectorsIDs } from '../../../../e2e/selectors/Modals/ConnectedAccountModal.selectors'; +import { ConnectedAccountsSelectorsIDs } from '../../../../e2e/selectors/Browser/ConnectedAccountModal.selectors'; import AppConstants from '../../../core/AppConstants'; import styles from './ManageNetworks.styles'; diff --git a/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap b/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap index 4d89a952349..3aef46b9799 100644 --- a/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap +++ b/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap @@ -87,6 +87,7 @@ exports[`ManageNetworks should render correctly 1`] = ` style={null} > { MetaMetrics.getInstance().trackEvent(event, params); @@ -96,7 +101,7 @@ const styles = StyleSheet.create({ disabled: { opacity: 0.3, }, - leftButtonContainer: { + rightElementContainer: { marginRight: 12, flexDirection: 'row', alignItems: 'flex-end', @@ -114,16 +119,11 @@ const styles = StyleSheet.create({ metamaskNameWrapper: { marginLeft: Device.isAndroid() ? 20 : 0, }, - fox: { - width: 24, - height: 24, + leftElementContainer: { marginLeft: 16, }, notificationsWrapper: { - position: 'relative', - flex: 1, - justifyContent: 'center', - alignItems: 'center', + marginHorizontal: 4, }, notificationsBadge: { width: 8, @@ -134,6 +134,9 @@ const styles = StyleSheet.create({ top: 2, right: 10, }, + addressCopyWrapper: { + marginHorizontal: 4, + }, }); const metamask_name = require('../../../images/metamask-name.png'); // eslint-disable-line @@ -361,7 +364,7 @@ export function getPaymentRequestOptionsTitle( ( + { + navigation.navigate(...createAccountSelectorNavDetails({})); + }} + accountTypeLabel={getLabelTextByAddress(selectedAddress) || undefined} + showAddress + cellAccountContainerStyle={styles.account} + testID={WalletViewSelectorsIDs.ACCOUNT_ICON} + /> + + ), + headerLeft: () => ( + ), - headerLeft: () => ( - - ), headerRight: () => ( - + + + + {isNotificationsFeatureEnabled() && ( @@ -1849,6 +1884,9 @@ export function getStakingNavbar(title, navigation, themeColors, options) { fontSize: 14, ...fontStyles.normal, }, + headerTitle: { + alignItems: 'center', + }, }); function navigationPop() { @@ -1857,7 +1895,9 @@ export function getStakingNavbar(title, navigation, themeColors, options) { return { headerTitle: () => ( - {title} + + {title} + ), headerStyle: innerStyles.headerStyle, headerLeft: () => @@ -1868,7 +1908,9 @@ export function getStakingNavbar(title, navigation, themeColors, options) { onPress={navigationPop} style={innerStyles.headerLeft} /> - ) : null, + ) : ( + <> + ), headerRight: () => hasCancelButton ? ( - ) : null, + ) : ( + <> + ), }; } diff --git a/app/components/UI/NetworkInfo/index.tsx b/app/components/UI/NetworkInfo/index.tsx index c4a55151b23..bc1a39f8f78 100644 --- a/app/components/UI/NetworkInfo/index.tsx +++ b/app/components/UI/NetworkInfo/index.tsx @@ -19,7 +19,7 @@ import { selectUseTokenDetection } from '../../../selectors/preferencesControlle import Avatar, { AvatarVariant, } from '../../../component-library/components/Avatars/Avatar'; -import { NetworkEducationModalSelectorsIDs } from '../../../../e2e/selectors/Modals/NetworkEducationModal.selectors'; +import { NetworkEducationModalSelectorsIDs } from '../../../../e2e/selectors/Network/NetworkEducationModal.selectors'; const createStyles = (colors: { background: { default: string }; diff --git a/app/components/UI/NetworkModal/NetworkAdded/index.tsx b/app/components/UI/NetworkModal/NetworkAdded/index.tsx index d4dc06b8dc6..299781879c2 100644 --- a/app/components/UI/NetworkModal/NetworkAdded/index.tsx +++ b/app/components/UI/NetworkModal/NetworkAdded/index.tsx @@ -4,7 +4,7 @@ import StyledButton from '../../StyledButton'; import { strings } from '../../../../../locales/i18n'; import Text from '../../../Base/Text'; import { useTheme } from '../../../../util/theme'; -import { NetworkAddedModalSelectorsIDs } from '../../../../../e2e/selectors/Modals/NetworkAddedModal.selectors'; +import { NetworkAddedBottomSheetSelectorsIDs } from '../../../../../e2e/selectors/Network/NetworkAddedBottomSheet.selectors'; // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -57,7 +57,7 @@ const NetworkAdded = (props: NetworkAddedProps) => { @@ -66,7 +66,7 @@ const NetworkAdded = (props: NetworkAddedProps) => { {strings('networks.switch_network')} diff --git a/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap b/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap index 8099909473b..fa6b7d2bf5b 100644 --- a/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap @@ -233,6 +233,7 @@ exports[`NetworkDetails renders correctly 1`] = ` style={null} > { label: strings('accountApproval.cancel'), size: ButtonSize.Lg, onPress: showCheckNetworkModal, - testID: NetworkApprovalModalSelectorsIDs.CANCEL_BUTTON, + testID: NetworkApprovalBottomSheetSelectorsIDs.CANCEL_BUTTON, }; const confirmButtonProps: ButtonProps = { @@ -161,7 +161,7 @@ const NetworkModals = (props: NetworkProps) => { toggleUseSafeChainsListValidation(true); showCheckNetworkModal(); }, - testID: NetworkApprovalModalSelectorsIDs.CONFIRM_NETWORK_CHECK, + testID: NetworkApprovalBottomSheetSelectorsIDs.CONFIRM_NETWORK_CHECK, }; const useSafeChainsListValidation = useSelector( @@ -321,7 +321,7 @@ const NetworkModals = (props: NetworkProps) => { const url = new URLPARSE(rpcUrl); const existingNetwork = networkConfigurationByChainId[chainId]; - CurrencyRateController.updateExchangeRate(ticker); + CurrencyRateController.updateExchangeRate([ticker]); if (!isPrivateConnection(url.hostname)) { url.set('protocol', 'https:'); diff --git a/app/components/UI/NetworkSelectorList/NetworkSelectorList.styles.ts b/app/components/UI/NetworkSelectorList/NetworkSelectorList.styles.ts index 133a87f969d..e279b943357 100644 --- a/app/components/UI/NetworkSelectorList/NetworkSelectorList.styles.ts +++ b/app/components/UI/NetworkSelectorList/NetworkSelectorList.styles.ts @@ -3,9 +3,8 @@ import { StyleSheet } from 'react-native'; const styleSheet = () => StyleSheet.create({ networkItemContainer: { - flexDirection: 'row', - alignItems: 'center', - padding: 10, + paddingHorizontal: 10, + paddingVertical: 14, }, networkAvatar: { marginHorizontal: 10, diff --git a/app/components/UI/NetworkSelectorList/NetworkSelectorList.tsx b/app/components/UI/NetworkSelectorList/NetworkSelectorList.tsx index 59fd85ddfef..152348b8573 100644 --- a/app/components/UI/NetworkSelectorList/NetworkSelectorList.tsx +++ b/app/components/UI/NetworkSelectorList/NetworkSelectorList.tsx @@ -24,7 +24,7 @@ const NetworkSelectorList = ({ onSelectNetwork, networks = [], isLoading = false, - selectedNetworkIds, + selectedChainIds, isMultiSelect = true, renderRightAccessory, isSelectionDisabled, @@ -33,7 +33,6 @@ const NetworkSelectorList = ({ }: NetworkConnectMultiSelectorProps) => { const networksLengthRef = useRef(0); const { styles } = useStyles(styleSheet, {}); - /** * Ref for the FlatList component. * The type of the ref is not explicitly defined. @@ -51,8 +50,8 @@ const NetworkSelectorList = ({ ? CellVariant.MultiSelect : CellVariant.Select; let isSelectedNetwork = isSelected; - if (selectedNetworkIds) { - isSelectedNetwork = selectedNetworkIds.includes(id); + if (selectedChainIds) { + isSelectedNetwork = selectedChainIds.includes(id); } return ( @@ -76,7 +75,7 @@ const NetworkSelectorList = ({ }, [ isLoading, - selectedNetworkIds, + selectedChainIds, renderRightAccessory, isSelectionDisabled, onSelectNetwork, diff --git a/app/components/UI/NetworkSelectorList/NetworkSelectorList.types.ts b/app/components/UI/NetworkSelectorList/NetworkSelectorList.types.ts index ebc0e3b4d6c..48b3bd6ab07 100644 --- a/app/components/UI/NetworkSelectorList/NetworkSelectorList.types.ts +++ b/app/components/UI/NetworkSelectorList/NetworkSelectorList.types.ts @@ -12,7 +12,7 @@ export interface NetworkConnectMultiSelectorProps { onSelectNetwork?: (id: string, isSelected: boolean) => void; networks?: Network[]; isLoading?: boolean; - selectedNetworkIds?: string[]; + selectedChainIds?: string[]; isMultiSelect?: boolean; renderRightAccessory?: (id: string, name: string) => React.ReactNode; isSelectionDisabled?: boolean; diff --git a/app/components/UI/NetworkVerificationInfo/NetworkVerificationInfo.tsx b/app/components/UI/NetworkVerificationInfo/NetworkVerificationInfo.tsx index 569f0c3fefc..96761b37689 100644 --- a/app/components/UI/NetworkVerificationInfo/NetworkVerificationInfo.tsx +++ b/app/components/UI/NetworkVerificationInfo/NetworkVerificationInfo.tsx @@ -40,7 +40,7 @@ import { toggleUseSafeChainsListValidation, isMultichainVersion1Enabled, } from '../../../util/networks'; -import { NetworkApprovalModalSelectorsIDs } from '../../../../e2e/selectors/Modals/NetworkApprovalModal.selectors'; +import { NetworkApprovalBottomSheetSelectorsIDs } from '../../../../e2e/selectors/Network/NetworkApprovalBottomSheet.selectors'; import hideKeyFromUrl from '../../../util/hideKeyFromUrl'; import { convertHexToDecimal } from '@metamask/controller-utils'; @@ -100,6 +100,20 @@ const NetworkVerificationInfo = ({ [customNetworkInformation], ); + const dappOrigin = useMemo(() => { + // @ts-expect-error - The CustomNetworkInformation type is missing the pageMeta property + const customNetworkUrl = customNetworkInformation.pageMeta?.url; + const url = customNetworkUrl ? new URL(customNetworkUrl) : null; + if (url) { + try { + return url.hostname; + } catch (error) { + console.error('Invalid URL:', error); + } + } + return 'Undefined dapp origin'; + }, [customNetworkInformation]); + const renderCurrencySymbol = () => ( <> ) : ( - + {isCustomNetwork @@ -437,9 +451,7 @@ const NetworkVerificationInfo = ({ {strings( 'switch_custom_network.add_network_and_give_dapp_permission_warning', { - // @ts-expect-error let's adjust the CustomNetworkInformation after multichain controllers have been updated by the api team - dapp_origin: new URL(customNetworkInformation.pageMeta.url) - ?.hostname, + dapp_origin: dappOrigin, }, )} @@ -461,14 +473,14 @@ const NetworkVerificationInfo = ({ label: strings('confirmation_modal.cancel_cta'), variant: ButtonVariants.Secondary, size: ButtonSize.Lg, - testID: NetworkApprovalModalSelectorsIDs.CANCEL_BUTTON, + testID: NetworkApprovalBottomSheetSelectorsIDs.CANCEL_BUTTON, }, { onPress: onConfirm, label: strings('confirmation_modal.confirm_cta'), variant: ButtonVariants.Primary, size: ButtonSize.Lg, - testID: NetworkApprovalModalSelectorsIDs.APPROVE_BUTTON, + testID: NetworkApprovalBottomSheetSelectorsIDs.APPROVE_BUTTON, }, ]} buttonsAlignment={ButtonsAlignment.Horizontal} diff --git a/app/components/UI/NetworkVerificationInfo/__snapshots__/NetworkVerificationInfo.test.tsx.snap b/app/components/UI/NetworkVerificationInfo/__snapshots__/NetworkVerificationInfo.test.tsx.snap index bbcf6abbafd..7b3fccd6bdf 100644 --- a/app/components/UI/NetworkVerificationInfo/__snapshots__/NetworkVerificationInfo.test.tsx.snap +++ b/app/components/UI/NetworkVerificationInfo/__snapshots__/NetworkVerificationInfo.test.tsx.snap @@ -94,6 +94,7 @@ exports[`NetworkVerificationInfo renders correctly 1`] = ` style={null} > + + + + + + +  + + + + + + + Testing Title + + + Testing description + + + + + + +`; + +exports[`BaseNotification gets icon correctly for each status 2`] = ` + + + + + + + +  + + + + + + + Testing Title + + + Testing description + + + + + + +`; + +exports[`BaseNotification gets icon correctly for each status 3`] = ` + + + + + + + +  + + + + + + + Testing Title + + + Testing description + + + + + + +`; + +exports[`BaseNotification gets icon correctly for each status 4`] = ` + + + + + +  + + + + + Testing Title + + + Testing description + + + + + + +`; + +exports[`BaseNotification gets icon correctly for each status 5`] = ` + + + + + +  + + + + + Testing Title + + + Testing description + + + + + + +`; + +exports[`BaseNotification gets icon correctly for each status 6`] = ` + + + + + +  + + + + + Testing Title + + + Testing description + + + + + + +`; + +exports[`BaseNotification gets icon correctly for each status 7`] = ` + + + + + +  + + + + + Testing Title + + + Testing description + + + + + + +`; + +exports[`BaseNotification gets icon correctly for each status 8`] = ` + + + + + +  + + + + + Testing Title + + + Testing description + + + + + + +`; + +exports[`BaseNotification gets icon correctly for each status 9`] = ` + + + + + +  + + + + + Testing Title + + + Testing description + + + + + + +`; + +exports[`BaseNotification gets icon correctly for each status 10`] = ` + + + + + +  + + + + + Testing Title + + + Testing description + + + + + + +`; diff --git a/app/components/UI/Notification/BaseNotification/index.js b/app/components/UI/Notification/BaseNotification/index.js index 96a11304d75..79b1a7dfc2e 100644 --- a/app/components/UI/Notification/BaseNotification/index.js +++ b/app/components/UI/Notification/BaseNotification/index.js @@ -72,6 +72,7 @@ export const getIcon = (status, colors, styles) => { case 'success': case 'received': case 'received_payment': + case 'eth_received': return ( { return strings('notifications.cancelled_title'); case 'error': return strings('notifications.error_title'); + case 'eth_received': + return strings('notifications.eth_received_title'); } }; -const getDescription = (status, { amount = null }) => { - if (amount) { - return strings(`notifications.${status}_message`, { amount }); +export const getDescription = (status, { amount = null, type = null }) => { + if (amount && typeof amount !== 'object') { + return strings(`notifications.${type}_${status}_message`, { amount }); } return strings(`notifications.${status}_message`); }; diff --git a/app/components/UI/Notification/BaseNotification/index.test.jsx b/app/components/UI/Notification/BaseNotification/index.test.jsx new file mode 100644 index 00000000000..11cb5aa6a17 --- /dev/null +++ b/app/components/UI/Notification/BaseNotification/index.test.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import BaseNotification, { getDescription } from './'; +import renderWithProvider from '../../../../util/test/renderWithProvider'; +import { strings } from '../../../../../locales/i18n'; + +const defaultProps = [ + { status: 'pending', data: { description: 'Testing description', title: 'Testing Title' } }, + { status: 'pending_withdrawal', data: { description: 'Testing description', title: 'Testing Title' } }, + { status: 'pending_deposit', data: { description: 'Testing description', title: 'Testing Title' } }, + { status: 'success_deposit', data: { description: 'Testing description', title: 'Testing Title' } }, + { status: 'success_withdrawal', data: { description: 'Testing description', title: 'Testing Title' } }, + { status: 'received', data: { description: 'Testing description', title: 'Testing Title' } }, + { status: 'received_payment', data: { description: 'Testing description', title: 'Testing Title' } }, + { status: 'eth_received', data: { description: 'Testing description', title: 'Testing Title' } }, + { status: 'cancelled', data: { description: 'Testing description', title: 'Testing Title' } }, + { status: 'error', data: { description: 'Testing description', title: 'Testing Title' } }, + ]; + +describe('BaseNotification', () => { + it('gets icon correctly for each status', () => { + defaultProps.forEach(({ status, data}) => { + const { toJSON } = renderWithProvider(); + expect(toJSON()).toMatchSnapshot(); + }); + }); + + it('gets titles correctly for each status', () => { + defaultProps.forEach(({ status }) => { + const { getByText } = renderWithProvider(); + expect(getByText(strings(`notifications.${status}_title`))).toBeTruthy(); + }); + }); + + it('gets descriptions correctly for if they are provided', () => { + defaultProps.forEach(({ status, data }) => { + const { getByText } = renderWithProvider(); + expect(getByText(data.description)).toBeTruthy(); + }); + }); + + it('constructs the correct description using getDescription when no description is provided', () => { + defaultProps.forEach(({ status }) => { + const { getByText } = renderWithProvider(); + expect(getByText(getDescription(status, {}))).toBeTruthy(); + }); + }); +}); diff --git a/app/components/UI/Notification/List/__snapshots__/index.test.tsx.snap b/app/components/UI/Notification/List/__snapshots__/index.test.tsx.snap index 3d46759d65b..6109381f4d0 100644 --- a/app/components/UI/Notification/List/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/Notification/List/__snapshots__/index.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`NotificationsList should render correctly 1`] = ` +exports[`NotificationsList renders correctly 1`] = ` `; + +exports[`NotificationsList renders empty state 1`] = ` + + + } + contentContainerStyle={ + { + "flexGrow": 1, + } + } + data={[]} + getItem={[Function]} + getItemCount={[Function]} + initialNumToRender={10} + keyExtractor={[Function]} + maxToRenderPerBatch={2} + onContentSizeChange={[Function]} + onEndReachedThreshold={0.5} + onLayout={[Function]} + onMomentumScrollBegin={[Function]} + onMomentumScrollEnd={[Function]} + onRefresh={[Function]} + onScroll={[Function]} + onScrollBeginDrag={[Function]} + onScrollEndDrag={[Function]} + refreshControl={ + + } + refreshing={false} + removeClippedSubviews={false} + renderItem={[Function]} + scrollEventThrottle={50} + stickyHeaderIndices={[]} + tabLabel="" + viewabilityConfigCallbackPairs={[]} + > + + + + + + Nothing to see here + + + This is where you can find notifications once there’s activity in your wallet. + + + + + +`; diff --git a/app/components/UI/Notification/List/index.test.tsx b/app/components/UI/Notification/List/index.test.tsx index 895f1120775..5908fb62a6e 100644 --- a/app/components/UI/Notification/List/index.test.tsx +++ b/app/components/UI/Notification/List/index.test.tsx @@ -1,18 +1,111 @@ import React from 'react'; -import NotificationsList from './'; -import renderWithProvider from '../../../../util/test/renderWithProvider'; +import { renderHook, act } from '@testing-library/react-hooks'; +import { NotificationServicesController } from '@metamask/notification-services-controller'; +import { Provider } from 'react-redux'; +import createMockStore from 'redux-mock-store'; +import NotificationsList, { NotificationsListItem } from './'; +import NotificationsService from '../../../../util/notifications/services/NotificationService'; +import renderWithProvider, { DeepPartial } from '../../../../util/test/renderWithProvider'; import MOCK_NOTIFICATIONS from '../__mocks__/mock_notifications'; -import { NavigationProp, ParamListBase } from '@react-navigation/native'; +import initialRootState, { backgroundState } from '../../../../util/test/initial-root-state'; +import { RootState } from '../../../../reducers'; +import { createNavigationProps } from '../../../../util/testUtils'; +import { hasNotificationModal, hasNotificationComponents, NotificationComponentState } from '../../../../util/notifications/notification-states'; +import { useMarkNotificationAsRead } from '../../../../util/notifications/hooks/useNotifications'; +import { Notification } from '../../../../util/notifications/types'; +// eslint-disable-next-line import/no-namespace +import * as Actions from '../../../../actions/notification/helpers'; +import { NotificationState } from '../../../../util/notifications/notification-states/types/NotificationState'; +import { TRIGGER_TYPES } from '../../../../util/notifications/constants'; +const mockNavigation = createNavigationProps({}); -const navigationMock = { +const mockTrackEvent = jest.fn(); + +jest.mock('../../../../util/notifications/services/NotificationService', () => ({ + ...jest.requireActual('../../../../util/notifications/services/NotificationService'), + getBadgeCount: jest.fn(), + decrementBadgeCount: jest.fn(), + setBadgeCount: jest.fn(), +})); + +jest.mock('../../../../util/notifications/notification-states', () => ({ + hasNotificationModal: jest.fn(), + hasNotificationComponents: jest.fn(), + NotificationComponentState: {}, +})); + +jest.mock('../../../hooks/useMetrics', () => ({ + useMetrics: () => ({ + trackEvent: mockTrackEvent, + }), + MetaMetricsEvents: { + NOTIFICATION_CLICKED: 'NOTIFICATION_CLICKED', + }, +})); + +const navigation = { navigate: jest.fn(), -} as unknown as NavigationProp; +}; + +const mockInitialState: DeepPartial = { + engine: { + backgroundState: { + ...backgroundState, + NotificationServicesController: { + metamaskNotificationsList: [], + }, + }, + }, +}; + +jest.mock('../NotificationMenuItem', () => ({ + NotificationMenuItem: { + Root: ({ children }: { children: React.ReactNode }) =>
{children}
, + Icon: jest.fn(({ isRead }: { isRead: boolean }) =>
{isRead ? 'Read Icon' : 'Unread Icon'}
), + Content: jest.fn(() =>
Mocked Content
), + }, +})); + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: (fn: (state: DeepPartial) => unknown) => fn(mockInitialState), +})); + +function arrangeStore() { + const store = createMockStore()(initialRootState); + // Ensure dispatch mocks are handled correctly + store.dispatch = jest.fn().mockImplementation((action) => { + if (typeof action === 'function') { + return action(store.dispatch, store.getState); + } + return Promise.resolve(); + }); + + return store; +} + +function arrangeActions() { + const mockMarkNotificationAsRead = jest.spyOn(Actions, 'markMetamaskNotificationsAsRead').mockResolvedValue(undefined); + + return { + mockMarkNotificationAsRead, + }; +} + +function arrangeHook() { + const store = arrangeStore(); + const hook = renderHook(() => useMarkNotificationAsRead(), { + wrapper: ({ children }) => {children}, + }); + + return hook; +} describe('NotificationsList', () => { - it('should render correctly', () => { + it('renders correctly', () => { const { toJSON } = renderWithProvider( { ); expect(toJSON()).toMatchSnapshot(); }); + + it('renders empty state', () => { + const { toJSON } = renderWithProvider( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('marks notification as read and not navigates if modal does not exist', async () => { + (hasNotificationModal as jest.Mock).mockReturnValue(false); + (NotificationsService.getBadgeCount as jest.Mock).mockResolvedValue(0); + const mockActions = arrangeActions(); + const { result } = arrangeHook(); + await act(async () => { + await result.current.markNotificationAsRead([ + { + id: MOCK_NOTIFICATIONS[2].id, + type: MOCK_NOTIFICATIONS[2].type, + isRead: MOCK_NOTIFICATIONS[2].isRead, + }, + ]); + }); + + expect(mockActions.mockMarkNotificationAsRead).toHaveBeenCalledWith([ + { + id: MOCK_NOTIFICATIONS[2].id, + type: MOCK_NOTIFICATIONS[2].type, + isRead: MOCK_NOTIFICATIONS[2].isRead, + }, + ]); + expect(navigation.navigate).not.toHaveBeenCalled(); + }); + + it('derives notificationState correctly based on notification type', () => { + (hasNotificationComponents as unknown as jest.Mock).mockReturnValue(true); + (NotificationComponentState as Record>)[MOCK_NOTIFICATIONS[2].type] = { + createMenuItem: jest.fn().mockReturnValue({ + title: MOCK_NOTIFICATIONS[2].type, + description: { + start: MOCK_NOTIFICATIONS[2].type, + }, + image: { + url: MOCK_NOTIFICATIONS[2].type, + variant: 'circle', + }, + badgeIcon: MOCK_NOTIFICATIONS[2].type, + createdAt: MOCK_NOTIFICATIONS[2].createdAt, + isRead: MOCK_NOTIFICATIONS[2].isRead, + }), + guardFn: (n): n is NotificationServicesController.Types.INotification => true, + }; + + renderWithProvider( + + ); + + expect((NotificationComponentState as Record>)[MOCK_NOTIFICATIONS[2].type].createMenuItem).toHaveBeenCalledWith(MOCK_NOTIFICATIONS[2]); + }); }); + diff --git a/app/components/UI/Notification/List/index.tsx b/app/components/UI/Notification/List/index.tsx index fcfd61a8002..afa08f1bc58 100644 --- a/app/components/UI/Notification/List/index.tsx +++ b/app/components/UI/Notification/List/index.tsx @@ -16,7 +16,7 @@ import { } from '../../../../util/notifications/notification-states'; import Routes from '../../../../constants/navigation/Routes'; import { MetaMetricsEvents } from '../../../../core/Analytics'; -import { Notification, TRIGGER_TYPES } from '../../../../util/notifications'; +import { Notification } from '../../../../util/notifications'; import { useListNotifications, useMarkNotificationAsRead, @@ -56,7 +56,7 @@ function Loading() { ); } -function NotificationsListItem(props: NotificationsListItemProps) { +export function NotificationsListItem(props: NotificationsListItemProps) { const { styles } = useStyles(); const { markNotificationAsRead } = useMarkNotificationAsRead(); const { trackEvent } = useMetrics(); @@ -69,7 +69,7 @@ function NotificationsListItem(props: NotificationsListItemProps) { isRead: item.isRead, }, ]); - if (hasNotificationModal(item.type)) { + if (hasNotificationModal(item?.type)) { props.navigation.navigate(Routes.NOTIFICATIONS.DETAILS, { notification: item, }); @@ -86,9 +86,9 @@ function NotificationsListItem(props: NotificationsListItemProps) { trackEvent(MetaMetricsEvents.NOTIFICATION_CLICKED, { notification_id: item.id, notification_type: item.type, - ...(item.type !== TRIGGER_TYPES.FEATURES_ANNOUNCEMENT - ? { chain_id: item?.chain_id } - : {}), + ...('chain_id' in item && { + chain_id: item.chain_id, + }), previously_read: item.isRead, }); }, @@ -97,11 +97,14 @@ function NotificationsListItem(props: NotificationsListItemProps) { const menuItemState = useMemo(() => { const notificationState = - NotificationComponentState[props.notification.type]; - return notificationState.createMenuItem(props.notification); + props.notification?.type && hasNotificationComponents(props.notification.type) + ? NotificationComponentState[props.notification.type] + : undefined; + + return notificationState?.createMenuItem(props.notification); }, [props.notification]); - if (!hasNotificationComponents(props.notification.type)) { + if (!hasNotificationComponents(props.notification.type) || !menuItemState) { return null; } @@ -129,7 +132,7 @@ function useNotificationListProps(props: { const getListProps = useCallback( (data: Notification[], tabLabel?: string) => { const listProps: FlatListProps = { - keyExtractor: (item) => item.id, + keyExtractor: (item: Notification) => item.id, data, ListEmptyComponent: ( ), contentContainerStyle: styles.list, - renderItem: ({ item }) => ( + renderItem: ({ item }: { item: Notification }) => ( ), diff --git a/app/components/UI/PaymentRequest/__snapshots__/index.test.tsx.snap b/app/components/UI/PaymentRequest/__snapshots__/index.test.tsx.snap index 2f8efc7e4ba..9db1d0309f3 100644 --- a/app/components/UI/PaymentRequest/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/PaymentRequest/__snapshots__/index.test.tsx.snap @@ -1,32 +1,460 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PaymentRequest should render correctly 1`] = ` - - - + + + + + + Choose an asset to request + + + + +  + + + + + + Top picks + + + + + + + + + + + + + + + ETH + + + Ether + + + + + + + + + + + + + + + + + SAI + + + Sai Stablecoin v1.0 + + + + + + + + + + `; diff --git a/app/components/UI/PaymentRequest/index.js b/app/components/UI/PaymentRequest/index.js index ac632e49634..bdbb3bb8f90 100644 --- a/app/components/UI/PaymentRequest/index.js +++ b/app/components/UI/PaymentRequest/index.js @@ -60,7 +60,7 @@ import { selectTokens } from '../../../selectors/tokensController'; import { selectContractExchangeRates } from '../../../selectors/tokenRatesController'; import { selectSelectedInternalAccountChecksummedAddress } from '../../../selectors/accountsController'; -import { RequestPaymentViewSelectors } from '../../../../e2e/selectors/RequestPaymentView.selectors'; +import { RequestPaymentViewSelectors } from '../../../../e2e/selectors/Receive/RequestPaymentView.selectors'; const KEYBOARD_OFFSET = 120; const createStyles = (colors) => diff --git a/app/components/UI/PaymentRequest/index.test.tsx b/app/components/UI/PaymentRequest/index.test.tsx index 5915fdecd57..391fe7e3b89 100644 --- a/app/components/UI/PaymentRequest/index.test.tsx +++ b/app/components/UI/PaymentRequest/index.test.tsx @@ -1,18 +1,198 @@ import React from 'react'; -import { shallow } from 'enzyme'; -import PaymentRequest from './'; +import { render, fireEvent, act } from '@testing-library/react-native'; +import PaymentRequest from './index'; import { Provider } from 'react-redux'; import configureMockStore from 'redux-mock-store'; +import { ThemeContext, mockTheme } from '../../../util/theme'; +import { MOCK_ACCOUNTS_CONTROLLER_STATE } from '../../../util/test/accountsControllerTestUtils'; + +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useState: jest.fn(), +})); const mockStore = configureMockStore(); -const store = mockStore({}); + +const initialState = { + engine: { + backgroundState: { + CurrencyRateController: { + conversionRate: 1, + currentCurrency: 'USD', + }, + TokenRatesController: { + contractExchangeRates: {}, + marketData: { + '0x1': { + '0x0d8775f59023cbe76e541b6497bbed3cd21acbdc': { + price: 1, + }, + }, + }, + }, + TokensController: { + marketData: { + '0x1': { + '0x0d8775f59023cbe76e541b6497bbed3cd21acbdc': { + price: 1, + }, + }, + }, + tokens: [], + }, + NetworkController: { + provider: { + ticker: 'ETH', + chainId: '1', + }, + }, + AccountsController: { + ...MOCK_ACCOUNTS_CONTROLLER_STATE, + internalAccounts: { + ...MOCK_ACCOUNTS_CONTROLLER_STATE.internalAccounts, + selectedAccount: {}, + }, + }, + TokenListController: { + tokenList: { + '0x1': { + '0x0d8775f59023cbe76e541b6497bbed3cd21acbdc': { + address: '0x0d8775f59023cbe76e541b6497bbed3cd21acbdc', + symbol: 'BAT', + decimals: 18, + name: 'Basic Attention Token', + iconUrl: + 'https://assets.coingecko.com/coins/images/677/thumb/basic-attention-token.png?1547034427', + type: 'erc20', + }, + }, + }, + }, + PreferencesController: { + ipfsGateway: {}, + }, + }, + }, + settings: { + primaryCurrency: 'ETH', + }, +}; + +let mockSetShowError: jest.Mock; +let mockShowError = false; + +beforeEach(() => { + mockSetShowError = jest.fn((value) => { + mockShowError = value; + }); + (React.useState as jest.Mock).mockImplementation((state) => [ + state, + mockSetShowError, + ]); +}); + +const store = mockStore(initialState); + +const mockNavigation = { + setOptions: jest.fn(), + setParams: jest.fn(), + navigate: jest.fn(), + goBack: jest.fn(), +}; + +const mockRoute = { + params: { + dispatch: jest.fn(), + }, +}; + +const renderComponent = (props = {}) => + render( + + + + + , + ); + describe('PaymentRequest', () => { - it('should render correctly', () => { - const wrapper = shallow( - - - , - ); - expect(wrapper).toMatchSnapshot(); + it('renders correctly', () => { + const { toJSON } = renderComponent(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('displays the correct title for asset selection', () => { + const { getByText } = renderComponent(); + expect(getByText('Choose an asset to request')).toBeTruthy(); + }); + + it('allows searching for assets', () => { + const { getByPlaceholderText } = renderComponent(); + const searchInput = getByPlaceholderText('Search assets'); + fireEvent.changeText(searchInput, 'ETH'); + expect(searchInput.props.value).toBe('ETH'); + }); + + it('switches to amount input mode when an asset is selected', async () => { + const { getByText } = renderComponent({ navigation: mockNavigation }); + + await act(async () => { + fireEvent.press(getByText('ETH')); + }); + + expect(getByText('Enter amount')).toBeTruthy(); + expect(mockNavigation.setParams).toHaveBeenCalledWith({ + mode: 'amount', + dispatch: expect.any(Function), + }); + }); + + it('updates amount when input changes', async () => { + const { getByText, getByPlaceholderText } = renderComponent(); + + // First, select an asset + await act(async () => { + fireEvent.press(getByText('ETH')); + }); + + const amountInput = getByPlaceholderText('0.00'); + await act(async () => { + fireEvent.changeText(amountInput, '1.5'); + }); + + expect(amountInput.props.value).toBe('1.5'); + }); + + it('displays an error when an invalid amount is entered', async () => { + const { getByText, getByPlaceholderText, debug, queryByText } = + renderComponent(); + + (React.useState as jest.Mock).mockImplementation(() => [ + mockShowError, + mockSetShowError, + ]); + + mockSetShowError(true); + + await act(async () => { + fireEvent.press(getByText('ETH')); + }); + + const amountInput = getByPlaceholderText('0.00'); + const nextButton = getByText('Next'); + + await act(async () => { + fireEvent.changeText(amountInput, '0'); + fireEvent.press(nextButton); + }); + + debug(); + + expect(mockSetShowError).toHaveBeenCalledWith(true); + expect(queryByText('Invalid request, please try again')).toBeTruthy(); }); }); diff --git a/app/components/UI/PaymentRequestSuccess/index.js b/app/components/UI/PaymentRequestSuccess/index.js index 1310dd135d8..08a3925ab65 100644 --- a/app/components/UI/PaymentRequestSuccess/index.js +++ b/app/components/UI/PaymentRequestSuccess/index.js @@ -30,7 +30,7 @@ import { protectWalletModalVisible } from '../../../actions/user'; import ClipboardManager from '../../../core/ClipboardManager'; import { ThemeContext, mockTheme } from '../../../util/theme'; import generateTestId from '../../../../wdio/utils/generateTestId'; -import { SendLinkViewSelectorsIDs } from '../../../../e2e/selectors/SendLinkView.selectors'; +import { SendLinkViewSelectorsIDs } from '../../../../e2e/selectors/Receive/SendLinkView.selectors'; const isIos = Device.isIos(); diff --git a/app/components/UI/PermissionsSummary/PermissionsSummary.styles.ts b/app/components/UI/PermissionsSummary/PermissionsSummary.styles.ts index a05ccb8a423..3c25f4830a7 100644 --- a/app/components/UI/PermissionsSummary/PermissionsSummary.styles.ts +++ b/app/components/UI/PermissionsSummary/PermissionsSummary.styles.ts @@ -30,6 +30,9 @@ const createStyles = (params: { marginRight: 24, marginLeft: 24, }, + bottomButtonsContainer: { + marginTop: 16, + }, actionButtonsContainer: { flex: 0, flexDirection: 'row', @@ -107,14 +110,17 @@ const createStyles = (params: { justifyContent: 'space-between', }, startAccessory: { flex: 1, paddingLeft: 16 }, - endAccessory: { flex: 1, paddingRight: 16 }, + endAccessory: { + flex: 1, + paddingRight: 16, + alignItems: 'flex-end', + }, editArrow: { marginHorizontal: 16, }, walletIcon: { alignSelf: 'flex-start' }, dataIcon: { alignSelf: 'flex-start' }, disconnectAllContainer: { - marginTop: 16, marginHorizontal: 24, flexDirection: 'row', }, diff --git a/app/components/UI/PermissionsSummary/PermissionsSummary.tsx b/app/components/UI/PermissionsSummary/PermissionsSummary.tsx index 9fea3eecabf..f48936e74ef 100644 --- a/app/components/UI/PermissionsSummary/PermissionsSummary.tsx +++ b/app/components/UI/PermissionsSummary/PermissionsSummary.tsx @@ -24,7 +24,6 @@ import TextComponent, { TextVariant, } from '../../../component-library/components/Texts/Text'; import AvatarGroup from '../../../component-library/components/Avatars/AvatarGroup'; -import { SAMPLE_AVATARGROUP_PROPS } from '../../../component-library/components/Avatars/AvatarGroup/AvatarGroup.constants'; import Button, { ButtonSize, ButtonVariants, @@ -41,6 +40,8 @@ import ButtonIcon, { ButtonIconSizes, } from '../../../component-library/components/Buttons/ButtonIcon'; import { getNetworkImageSource } from '../../../util/networks'; +import Engine from '../../../core/Engine'; +import { SDKSelectorsIDs } from '../../../../e2e/selectors/Settings/SDK.selectors'; const PermissionsSummary = ({ currentPageInformation, @@ -57,12 +58,16 @@ const PermissionsSummary = ({ isDisconnectAllShown = true, isNetworkSwitch = false, accountAddresses = [], + accounts = [], + networkAvatars = [], }: PermissionsSummaryProps) => { const { colors } = useTheme(); const { styles } = useStyles(styleSheet, { isRenderedAsBottomSheet }); const { navigate } = useNavigation(); const selectedAccount = useSelectedAccount(); + const hostname = new URL(currentPageInformation.url).hostname; + // if network switch, we get the chain name from the customNetworkInformation let chainName = ''; let chainImage: ImageSourcePropType; @@ -123,7 +128,31 @@ const PermissionsSummary = ({
{renderTopIcon()} - + + {!isRenderedAsBottomSheet && ( + { + navigate(Routes.MODAL.ROOT_MODAL_FLOW, { + screen: Routes.SHEET.CONNECTION_DETAILS, + params: { + hostInfo: { + metadata: { + origin: + currentPageInformation?.url && + new URL(currentPageInformation?.url).hostname, + }, + }, + connectionDateTime: new Date().getTime(), + }, + }); + }} + testID={SDKSelectorsIDs.CONNECTION_DETAILS_BUTTON} + /> + )} +
); } @@ -149,47 +178,80 @@ const PermissionsSummary = ({
); + const onRevokeAllHandler = useCallback(async () => { + await Engine.context.PermissionController.revokeAllPermissions(hostname); + navigate('PermissionsManager'); + }, [hostname, navigate]); + const toggleRevokeAllPermissionsModal = useCallback(() => { navigate(Routes.MODAL.ROOT_MODAL_FLOW, { screen: Routes.SHEET.REVOKE_ALL_ACCOUNT_PERMISSIONS, params: { hostInfo: { metadata: { - origin: - currentPageInformation?.url && - new URL(currentPageInformation?.url).hostname, + origin: hostname, }, }, + onRevokeAll: !isRenderedAsBottomSheet && onRevokeAllHandler, }, }); - }, [navigate, currentPageInformation?.url]); + }, [navigate, isRenderedAsBottomSheet, onRevokeAllHandler, hostname]); const getAccountLabel = useCallback(() => { if (isAlreadyConnected) { if (accountAddresses.length === 0 && selectedAccount) { return `${strings('permissions.connected_to')} ${selectedAccount.name}`; } - return accountAddresses.length === 1 - ? `1 ${strings('accounts.account_connected')}` - : `${accountAddresses.length} ${strings( - 'accounts.accounts_connected', - )}`; + if (accountAddresses.length === 1) { + const matchedConnectedAccount = accounts.find( + (account) => account.address === accountAddresses[0], + ); + return matchedConnectedAccount?.name; + } + + return `${accountAddresses.length} ${strings( + 'accounts.accounts_connected', + )}`; } - if ( - accountAddresses.length === 1 || - (accountAddresses.length === 0 && selectedAccount) - ) { - return ( - selectedAccount?.name && - `${strings('permissions.requesting_for')}${selectedAccount?.name}` + if (accountAddresses.length === 1 && accounts?.length >= 1) { + const matchedAccount = accounts.find( + (account) => account.address === accountAddresses[0], ); + return `${strings('permissions.requesting_for')}${ + matchedAccount?.name ? matchedAccount.name : accountAddresses[0] + }`; + } + + if (accountAddresses.length === 0 && selectedAccount) { + return `${strings('permissions.requesting_for')}${selectedAccount?.name}`; } return strings('permissions.requesting_for_accounts', { numberOfAccounts: accountAddresses.length, }); - }, [accountAddresses, isAlreadyConnected, selectedAccount]); + }, [accountAddresses, isAlreadyConnected, selectedAccount, accounts]); + + const getNetworkLabel = useCallback(() => { + if (isAlreadyConnected) { + return networkAvatars.length === 1 + ? networkAvatars[0]?.name + : `${strings('permissions.n_networks_connect', { + numberOfNetworks: networkAvatars.length, + })}`; + } + + if (networkAvatars.length === 1) { + return ( + networkAvatars[0]?.name && + `${strings('permissions.requesting_for')}${networkAvatars[0]?.name}` + ); + } + + return strings('permissions.requesting_for_networks', { + numberOfNetworks: networkAvatars.length, + }); + }, [networkAvatars, isAlreadyConnected]); function renderAccountPermissionsRequestInfoCard() { return ( @@ -280,11 +342,24 @@ const PermissionsSummary = ({ )} {!isNetworkSwitch && ( - - - + <> + + + + {getNetworkLabel()} + + + + + ({ + ...avatar, + variant: AvatarVariant.Network, + }))} + /> + + )}
@@ -303,10 +378,10 @@ const PermissionsSummary = ({ {!isAlreadyConnected || isNetworkSwitch ? strings('permissions.title_dapp_url_wants_to', { - dappUrl: new URL(currentPageInformation.url).hostname, + dappUrl: hostname, }) : strings('permissions.title_dapp_url_has_approval_to', { - dappUrl: new URL(currentPageInformation.url).hostname, + dappUrl: hostname, })}
@@ -314,7 +389,7 @@ const PermissionsSummary = ({ {!isNetworkSwitch && renderAccountPermissionsRequestInfoCard()} {renderNetworkPermissionsRequestInfoCard()}
- + {isAlreadyConnected && isDisconnectAllShown && ( + ); + /** * Render non-homepage options menu */ const renderNonHomeOptions = () => { - if (isHomepage()) return null; + if (isHomepage()) return renderGoToFavorites(); return ( @@ -1311,7 +1343,7 @@ export const BrowserTab = (props) => { {!isBookmark() && ( )} + {renderGoToFavorites()}