diff --git a/.android.env.example b/.android.env.example index 93b4a8c334e..5c769312cca 100644 --- a/.android.env.example +++ b/.android.env.example @@ -9,7 +9,7 @@ export FCM_CONFIG_PROJECT_ID= export FCM_CONFIG_STORAGE_BUCKET= export FCM_CONFIG_MESSAGING_SENDER_ID= export FCM_CONFIG_APP_ID= -export GOOGLE_SERVICES_B64= +export GOOGLE_SERVICES_B64_ANDROID= #Notifications Feature Announcements export FEATURES_ANNOUNCEMENTS_ACCESS_TOKEN= export FEATURES_ANNOUNCEMENTS_SPACE_ID= 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 ba2cdd54f4b..c67f120be41 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ android/app/_build* # if we ever want to add google services android/app/google-services.json +ios/GoogleService-Info.plist # node.js node_modules/ @@ -132,4 +133,7 @@ android/app/src/main/assets/modules.json # Expo .expo dist/ -web-build/ \ No newline at end of file +web-build/ + +# Google firebase base64 derived configs +**/GoogleService-Info.plist diff --git a/.ios.env.example b/.ios.env.example index 05aadc9b359..cc449b8b6e1 100644 --- a/.ios.env.example +++ b/.ios.env.example @@ -8,7 +8,7 @@ FCM_CONFIG_PROJECT_ID= FCM_CONFIG_STORAGE_BUCKET= FCM_CONFIG_MESSAGING_SENDER_ID= FCM_CONFIG_APP_ID= -GOOGLE_SERVICES_B64= +GOOGLE_SERVICES_B64_IOS= #Notifications Feature Announcements FEATURES_ANNOUNCEMENTS_ACCESS_TOKEN= FEATURES_ANNOUNCEMENTS_SPACE_ID= diff --git a/.iyarc b/.iyarc index abbf0e189d2..e69de29bb2d 100644 --- a/.iyarc +++ b/.iyarc @@ -1,3 +0,0 @@ -# ReDoS vulnerability, no impact to this application, and fix not backported yet to the versions we use - -GHSA-c2qf-rxjj-qqgw diff --git a/.js.env.example b/.js.env.example index 81d9fea44a1..6c0d0e327b0 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="" @@ -79,7 +79,8 @@ export FCM_CONFIG_PROJECT_ID="" export FCM_CONFIG_STORAGE_BUCKET="" export FCM_CONFIG_MESSAGING_SENDER_ID="" export FCM_CONFIG_APP_ID="" -export GOOGLE_SERVICES_B64="" +export GOOGLE_SERVICES_B64_ANDROID="" +export GOOGLE_SERVICES_B64_IOS="" #Notifications Feature Announcements export FEATURES_ANNOUNCEMENTS_ACCESS_TOKEN= export FEATURES_ANNOUNCEMENTS_SPACE_ID= 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 2e992dae281..b04b084a18a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,123 @@ ## Current Main Branch -## 7.32.0 - Oct 7, 2024 +## 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 3, 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) +- [#11347](https://github.com/MetaMask/metamask-mobile/pull/11347): "chore: revert ""feat: react native fast crypto for notifications"" (#11347)" +- [#11318](https://github.com/MetaMask/metamask-mobile/pull/11318): feat: app event manager and attribution id parameters (#11318) +- [#11445](https://github.com/MetaMask/metamask-mobile/pull/11445): feat: add the abilty to hide the disconnect all button as well as showing and hiding the accounts row when necessary (#11445) +- [#11386](https://github.com/MetaMask/metamask-mobile/pull/11386): feat(3299): add tracking to network switching and confirmation (#11386) +- [#11379](https://github.com/MetaMask/metamask-mobile/pull/11379): refactor: rename the feature flag since it had a typo (#11379) +- [#11239](https://github.com/MetaMask/metamask-mobile/pull/11239): feat(2739): permission summary view gets the ability to disconnect all (#11239) +- [#11382](https://github.com/MetaMask/metamask-mobile/pull/11382): chore(runway): cherry-pick feat: app event manager and attribution id parameters (#11382) +- [#11497](https://github.com/MetaMask/metamask-mobile/pull/11497): feat: simple key-> value component for text value type (#11497) +- [#11478](https://github.com/MetaMask/metamask-mobile/pull/11478): feat: adding title to confirmation page (#11478) +- [#11477](https://github.com/MetaMask/metamask-mobile/pull/11477): feat: adding footer section to confirmation page (#11477) +- [#11454](https://github.com/MetaMask/metamask-mobile/pull/11454): feat: adding very basic confirmation page hidden behind env var (#11454) +- [#11083](https://github.com/MetaMask/metamask-mobile/pull/11083): feat: bundle size check (#11083) +- [#11452](https://github.com/MetaMask/metamask-mobile/pull/11452): feat(ds): add ListItem top and bottom accessories (#11452) +- [#11387](https://github.com/MetaMask/metamask-mobile/pull/11387): feat: 10550 Re-introduce test for measuring cold app start + JS bundle load time (#11387) +- [#11464](https://github.com/MetaMask/metamask-mobile/pull/11464): feat: STAKE-804: build pooled staking empty state component (#11464) +- [#11399](https://github.com/MetaMask/metamask-mobile/pull/11399): feat: add learn more modal component for staking (#11399) +- [#11261](https://github.com/MetaMask/metamask-mobile/pull/11261): feat: STAKE-822 build your balance component (#11261) +- [#11294](https://github.com/MetaMask/metamask-mobile/pull/11294): feat: added KeyValueRow to component-library/components-temp (#11294) +- [#11185](https://github.com/MetaMask/metamask-mobile/pull/11185): "feat: display ""Snaps (Beta)"" decorator tag in accounts list (#11185)" +- [#10829](https://github.com/MetaMask/metamask-mobile/pull/10829): feat: Eth snap keyring (#10829) +- [#11455](https://github.com/MetaMask/metamask-mobile/pull/11455): feat: bootstrap a reset notifications feat (#11455) +- [#11466](https://github.com/MetaMask/metamask-mobile/pull/11466): feat: add support for external links (#11466) +- [#11429](https://github.com/MetaMask/metamask-mobile/pull/11429): feat: add timeout handler (#11429) +- [#11427](https://github.com/MetaMask/metamask-mobile/pull/11427): feat: add feature announcements channel for android (#11427) +- [#11069](https://github.com/MetaMask/metamask-mobile/pull/11069): feat: react native fast crypto for notifications (#11069) +### Changed +- [#11379](https://github.com/MetaMask/metamask-mobile/pull/11379): refactor: rename the feature flag since it had a typo (#11379) +- [#11615](https://github.com/MetaMask/metamask-mobile/pull/11615): chore: exclude temporarily sentry SDK advisory (#11615) +- [#11577](https://github.com/MetaMask/metamask-mobile/pull/11577): ci: disable swaps e2e workflow (#11577) +- [#11350](https://github.com/MetaMask/metamask-mobile/pull/11350): chore: replace Segment patch by plugin (#11350) +- [#11287](https://github.com/MetaMask/metamask-mobile/pull/11287): chore: remove unused events (#11287) +- [#11517](https://github.com/MetaMask/metamask-mobile/pull/11517): chore: delete swaps token charts test (#11517) +- [#11515](https://github.com/MetaMask/metamask-mobile/pull/11515): test: disable swaps token charts regression tests (#11515) +- [#11504](https://github.com/MetaMask/metamask-mobile/pull/11504): chore: revert measuring with react native performance (#11504) +- [#11458](https://github.com/MetaMask/metamask-mobile/pull/11458): test: Merge Import Token flow methods and ids in just one folder and files (#11458) +- [#11492](https://github.com/MetaMask/metamask-mobile/pull/11492): chore(revert): Prevent redundant Sentry sourcemap uploads (#11492) +- [#11469](https://github.com/MetaMask/metamask-mobile/pull/11469): test: Remove E2E Tests for Features No Longer Present in the App (#11469) +- [#11425](https://github.com/MetaMask/metamask-mobile/pull/11425): chore: Add skip label to bypass sonarcloud (#11425) +- [#11275](https://github.com/MetaMask/metamask-mobile/pull/11275): chore(js-ts): Convert app/util/bytes.js to TypeScript (#11275) +- [#11418](https://github.com/MetaMask/metamask-mobile/pull/11418): chore: revert chore(js-ts): Migrate 37 0-error js files (#11418) +- [#10880](https://github.com/MetaMask/metamask-mobile/pull/10880): chore: Enable linting Pods and re-organize setup.mjs file (#10880) +- [#11311](https://github.com/MetaMask/metamask-mobile/pull/11311): chore(js-ts): Convert app/components/UI/Swaps/components/Ratio.js to TypeScript (#11311) +- [#11357](https://github.com/MetaMask/metamask-mobile/pull/11357): chore(js-ts): Convert app/components/Views/PickComponent/index.js to TypeScript (#11357) +- [#11446](https://github.com/MetaMask/metamask-mobile/pull/11446): chore(js-ts): Convert app/components/Views/MediaPlayer/Loader.js to TypeScript (#11446) +- [#11473](https://github.com/MetaMask/metamask-mobile/pull/11473): chore(js-ts): Convert app/components/Base/ModalHandler.js to TypeScript (#11473) +- [#11601](https://github.com/MetaMask/metamask-mobile/pull/11601): chore(js-ts): Convert app/components/Base/ListItem.js to TypeScript (#11601) +- [#11407](https://github.com/MetaMask/metamask-mobile/pull/11407): chore(js-ts): Convert app/util/jsonRpcRequest.js to TypeScript (#11407) +- [#11594](https://github.com/MetaMask/metamask-mobile/pull/11594): chore(js-ts): Convert app/component-library/components/Icons/Icon/scripts/generate-assets.js to TypeScript (#11594) +- [#11523](https://github.com/MetaMask/metamask-mobile/pull/11523): chore(js-ts): Convert app/components/UI/GenericButton/index.ios.js to TypeScript (#11523) +- [#11472](https://github.com/MetaMask/metamask-mobile/pull/11472): chore(js-ts): Convert app/components/UI/FadeView/index.js to TypeScript (#11472) +- [#11476](https://github.com/MetaMask/metamask-mobile/pull/11476): chore(js-ts): Convert app/components/UI/OnboardingProgress/index.js to TypeScript (#11476) +- [#11405](https://github.com/MetaMask/metamask-mobile/pull/11405): chore(js-ts): Convert app/util/browserScripts.js to TypeScript (#11405) +- [#11214](https://github.com/MetaMask/metamask-mobile/pull/11214): chore(js-ts): Migrate 37 0-error js files (#11214) +- [#11271](https://github.com/MetaMask/metamask-mobile/pull/11271): chore(js-ts): Convert app/components/UI/ComponentErrorBoundary/index.js to TypeScript (#11271) +- [#11299](https://github.com/MetaMask/metamask-mobile/pull/11299): chore(js-ts): Convert app/util/validators/index.js to TypeScript (#11299) +- [#11303](https://github.com/MetaMask/metamask-mobile/pull/11303): chore(js-ts): Convert app/components/Base/SelectorButton.js to TypeScript (#11303) +- [#11280](https://github.com/MetaMask/metamask-mobile/pull/11280): chore(js-ts): Convert app/components/UI/GenericButton/index.android.js to TypeScript (#11280) +- [#11273](https://github.com/MetaMask/metamask-mobile/pull/11273): chore(js-ts): Convert app/components/UI/OnboardingScreenWithBg/index.js to TypeScript (#11273) +- [#11272](https://github.com/MetaMask/metamask-mobile/pull/11272): chore(js-ts): Convert app/components/Base/ModalDragger.js to TypeScript (#11272) +- [#11308](https://github.com/MetaMask/metamask-mobile/pull/11308): chore(js-ts): Convert app/components/UI/BlockingActionModal/index.js to TypeScript (#11308) +- [#11305](https://github.com/MetaMask/metamask-mobile/pull/11305): chore(js-ts): Convert app/components/Base/Summary.js to TypeScript (#11305) +- [#11274](https://github.com/MetaMask/metamask-mobile/pull/11274): chore(js-ts): Convert app/components/UI/ConnectHeader/index.js to TypeScript (#11274) +- [#11334](https://github.com/MetaMask/metamask-mobile/pull/11334): chore: chore/7.32.0-Changelog (#11334) +- [#11483](https://github.com/MetaMask/metamask-mobile/pull/11483): chore: refactor e2e (#11483) +- [#11491](https://github.com/MetaMask/metamask-mobile/pull/11491): chore: Add UX CodeOwners responsibilities (#11491) +- [#11364](https://github.com/MetaMask/metamask-mobile/pull/11364): refactor(1702-2): auto detect nft component (#11364) +- [#11363](https://github.com/MetaMask/metamask-mobile/pull/11363): refactor(1702-1): auto detect tokens component (#11363) +- [#11329](https://github.com/MetaMask/metamask-mobile/pull/11329): chore: Add `@MetaMask/metamask-assets` to `CODEOWNERS` (#11329) +- [#10449](https://github.com/MetaMask/metamask-mobile/pull/10449): chore: remove installation of redundant detox-cli in bitrise (#10449) +- [#11111](https://github.com/MetaMask/metamask-mobile/pull/11111): chore(deps): Bump `@metamask/phishing-controller` from `^9.0.0` to `^12.0.1` (#11111) +- [#11375](https://github.com/MetaMask/metamask-mobile/pull/11375): chore(deps): Bump `@metamask/controller-utils` from `^10.0.0` to `^11.3.0` (#11375) +- [#11140](https://github.com/MetaMask/metamask-mobile/pull/11140): chore(deps): Bump `@metamask/smart-transactions-controller` from `11.0.0` to `^13.0.0` (#11140) +- [#11351](https://github.com/MetaMask/metamask-mobile/pull/11351): chore(deps): Bump `@metamask/keyring-controller` from `^16.1.0` to `^17.2.1` (#11351) +- [#11104](https://github.com/MetaMask/metamask-mobile/pull/11104): chore(deps): Bump `@metamask/address-book-controller` from `^4.0.1` to `^6.0.1` (#11104) +- [#10917](https://github.com/MetaMask/metamask-mobile/pull/10917): chore(ci): split out ci scripts and devDeps into separate project (#10917) +- [#11081](https://github.com/MetaMask/metamask-mobile/pull/11081): chore: Prevent redundant Sentry sourcemap uploads (#11081) +- [#11470](https://github.com/MetaMask/metamask-mobile/pull/11470): chore: [Design quality] Update token details (#11470) +- [#11439](https://github.com/MetaMask/metamask-mobile/pull/11439): chore: cherry-pick fix: ""chore(deps): Bump @metamask/base-controller from ^6.0.0 to ^7.0.0 (#11207)"" (#11439) +- [#11169](https://github.com/MetaMask/metamask-mobile/pull/11169): chore(deps): Bump `@metamask/signature-controller` from `^17.0.0` to `^19.1.0` +- [#11352](https://github.com/MetaMask/metamask-mobile/pull/11352): chore(deps): Bump `@metamask/accounts-controller` to `^18.2.1` + +### Fixed +- [#11512](https://github.com/MetaMask/metamask-mobile/pull/11512): fix: android firebase docs template (#11512) +- [#11430](https://github.com/MetaMask/metamask-mobile/pull/11430): fix: refactor Logger usage (#11430) +- [#11250](https://github.com/MetaMask/metamask-mobile/pull/11250): fix: push notifications (#11250) +- [#11581](https://github.com/MetaMask/metamask-mobile/pull/11581): fix: Fix invalid browser url crash (#11581) +- [#11467](https://github.com/MetaMask/metamask-mobile/pull/11467): fix: Reorder prep_environment (#11467) +- [#11367](https://github.com/MetaMask/metamask-mobile/pull/11367): fix: Update steps of the methods that are no longer valid (#11367) +- [#11400](https://github.com/MetaMask/metamask-mobile/pull/11400): fix: Stop crowdin action from creating branches (#11400) +- [#11348](https://github.com/MetaMask/metamask-mobile/pull/11348): fix: splash screen image on android (#11348) +- [#11346](https://github.com/MetaMask/metamask-mobile/pull/11346): fix: splash screen image on android (#11346) +- [#11554](https://github.com/MetaMask/metamask-mobile/pull/11554): fix: Fix/use portfolio home page (#11554) +- [#11443](https://github.com/MetaMask/metamask-mobile/pull/11443): fix: react native quick crypto ios build bug (#11443) +- [#11325](https://github.com/MetaMask/metamask-mobile/pull/11325): fix: loader can display on top of login screen (#11325) +- [#11372](https://github.com/MetaMask/metamask-mobile/pull/11372): fix: origin spoofing vulnerability in signature prompts on iOS (#11372) +- [#11076](https://github.com/MetaMask/metamask-mobile/pull/11076): fix(2453): adjust UI details for account cell on wallet screen (#11076) +- [#11524](https://github.com/MetaMask/metamask-mobile/pull/11524): fix: fix duplicated network select (#11524) +- [#11411](https://github.com/MetaMask/metamask-mobile/pull/11411): fix: fix fixture builder network state (#11411) +- [#11380](https://github.com/MetaMask/metamask-mobile/pull/11380): fix: Unreadable Asset options (#11380) +- [#11321](https://github.com/MetaMask/metamask-mobile/pull/11321): fix: fix detect tokens performance (#11321) +- [#11401](https://github.com/MetaMask/metamask-mobile/pull/11401): fix: replace decomissioned cloudflare-ipfs.com with gateway.pinata.cloud (#11401) +- [#11552](https://github.com/MetaMask/metamask-mobile/pull/11552): fix(11481): android system alert respects dark mode themes (#11552) +- [#11518](https://github.com/MetaMask/metamask-mobile/pull/11518): fix(11482): incorrect QR code error (#11518) + +## 7.32.0 - Oct 7, 2024 ### Added - [#10294](https://github.com/MetaMask/metamask-mobile/pull/10294): feat: create redux slice for featureFlags (#10294) diff --git a/README.md b/README.md index 2d19b29c7f8..815748fcafd 100644 --- a/README.md +++ b/README.md @@ -41,18 +41,31 @@ cd metamask-mobile **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` directory as well your `.env` files (`.ios.env`, `.js.env`, `.android.env`), adding `GOOGLE_SERVICES_B64` variable depending on the environment you are running the app (ios/android). +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. -ATTENTION: In case you don't provide your own Firebase project config file, you can make use of a mock file at `android/app/google-services-example.json`, following the steps below from the root of the project: +**External Contributors** +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). +**Internal Contributors** + +We should access the Firebase project config file from 1Password. + +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: + +**For Android** ```bash -echo "export GOOGLE_SERVICES_B64=\"$(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 ``` -You can make usage of a mock file at `android/app/google-services-example.json`, following the same steps above from the root of the project. +**For iOS** +```bash +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 +``` -In case of any doubt, please follow the instructions in the link below to get your Firebase project config file. +[!CAUTION] +> In case you don't provide your own Firebase project config file or run the steps above, you will face the error `No matching client found for package name 'io.metamask'`. +In case of any doubt, please follow the instructions in the link below to get your Firebase project config file. [Firebase Project Quickstart](https://firebaseopensource.com/projects/firebase/quickstart-js/messaging/readme/#getting_started) **Install dependencies** diff --git a/android/app/build.gradle b/android/app/build.gradle index 8c2298c7029..9365892ad6c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -178,8 +178,8 @@ android { applicationId "io.metamask" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 1450 - versionName "7.32.0" + versionName "7.33.2" + versionCode 1474 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/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/Texts/SensitiveText/SensitiveText.types.ts b/app/component-library/components/Texts/SensitiveText/SensitiveText.types.ts index 1c6f4688b78..927acd91279 100644 --- a/app/component-library/components/Texts/SensitiveText/SensitiveText.types.ts +++ b/app/component-library/components/Texts/SensitiveText/SensitiveText.types.ts @@ -1,4 +1,5 @@ // External dependencies. +import React from 'react'; import { TextProps } from '../Text/Text.types'; /** @@ -42,5 +43,5 @@ export interface SensitiveTextProps extends TextProps { /** * The text content to be displayed or hidden. */ - children: string; + children: React.ReactNode; } diff --git a/app/components/Approvals/ApprovalModal/ApprovalModal.test.tsx b/app/components/Approvals/ApprovalModal/ApprovalModal.test.tsx index 879cf8a5f16..f16fe201007 100644 --- a/app/components/Approvals/ApprovalModal/ApprovalModal.test.tsx +++ b/app/components/Approvals/ApprovalModal/ApprovalModal.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { render } from '@testing-library/react-native'; import ApprovalModal from './ApprovalModal'; describe('ApprovalModal', () => { @@ -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 429dfeb7ca5..4ea9f0beaa0 100644 --- a/app/components/Base/RemoteImage/index.test.tsx +++ b/app/components/Base/RemoteImage/index.test.tsx @@ -1,16 +1,24 @@ 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'), useSelector: jest .fn() - .mockImplementation(() => 'https://gateway.pinata.cloud/ipfs/'), + .mockImplementation(() => 'https://dweb.link/ipfs/'), })); 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 dbed500c1a1..2965348a569 100644 --- a/app/components/Nav/App/index.js +++ b/app/components/Nav/App/index.js @@ -58,6 +58,7 @@ import Toast, { ToastContext, } from '../../../component-library/components/Toast'; import AccountSelector from '../../../components/Views/AccountSelector'; +import TokenSortBottomSheet from '../../../components/UI/Tokens/TokensBottomSheet/TokenSortBottomSheet.tsx'; import AccountConnect from '../../../components/Views/AccountConnect'; import AccountPermissions from '../../../components/Views/AccountPermissions'; import { AccountPermissionsScreens } from '../../../components/Views/AccountPermissions/AccountPermissions.types'; @@ -123,6 +124,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'; @@ -131,6 +133,10 @@ import OptionsSheet from '../../UI/SelectOptionSheet/OptionsSheet'; import FoxLoader from '../../../components/UI/FoxLoader'; import { AppStateEventProcessor } from '../../../core/AppStateEventListener'; import MultiRpcModal from '../../../components/Views/MultiRpcModal/MultiRpcModal'; +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'; const clearStackNavigatorOptions = { @@ -422,6 +428,10 @@ const RootModalFlow = () => ( name={Routes.SHEET.NETWORK_SELECTOR} component={NetworkSelector} /> + ( name={Routes.SHEET.ORIGIN_SPAM_MODAL} component={OriginSpamModal} /> + ); @@ -762,6 +776,46 @@ const App = (props) => { useEffect(() => { async function startApp() { const existingUser = await StorageWrapper.getItem(EXISTING_USER); + if (!existingUser) { + // List of chainIds to add (as hex strings) + const chainIdsToAdd = [ + CHAIN_IDS.ARBITRUM, + CHAIN_IDS.BASE, + CHAIN_IDS.BSC, + CHAIN_IDS.OPTIMISM, + CHAIN_IDS.POLYGON, + ]; + + // Filter the PopularList to get only the specified networks based on chainId + const selectedNetworks = PopularList.filter((network) => + chainIdsToAdd.includes(network.chainId), + ); + const { NetworkController } = Engine.context; + + // Loop through each selected network and call NetworkController.addNetwork + for (const network of selectedNetworks) { + try { + await NetworkController.addNetwork({ + chainId: network.chainId, + blockExplorerUrls: [network.rpcPrefs.blockExplorerUrl], + defaultRpcEndpointIndex: 0, + defaultBlockExplorerUrlIndex: 0, + name: network.nickname, + nativeCurrency: network.ticker, + rpcEndpoints: [ + { + url: network.rpcUrl, + name: network.nickname, + type: RpcEndpointType.Custom, + }, + ], + }); + } catch (error) { + Logger.error(error); + } + } + } + try { const currentVersion = getVersion(); const savedVersion = await StorageWrapper.getItem(CURRENT_APP_VERSION); diff --git a/app/components/UI/AccountApproval/__snapshots__/index.test.tsx.snap b/app/components/UI/AccountApproval/__snapshots__/index.test.tsx.snap index c36975e744a..6418bc869d5 100644 --- a/app/components/UI/AccountApproval/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/AccountApproval/__snapshots__/index.test.tsx.snap @@ -97,6 +97,7 @@ exports[`AccountApproval should render correctly 1`] = ` "marginTop": 10, } } + testID="transaction-header-origin" > diff --git a/app/components/UI/AccountSelectorList/AccountSelector.test.tsx b/app/components/UI/AccountSelectorList/AccountSelector.test.tsx index 306c88825c4..4403184965b 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 { @@ -128,10 +128,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 +171,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(); 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..f8212feda79 100644 --- a/app/components/UI/AccountSelectorList/AccountSelectorList.tsx +++ b/app/components/UI/AccountSelectorList/AccountSelectorList.tsx @@ -1,6 +1,6 @@ // Third party dependencies. import React, { useCallback, useRef } from 'react'; -import { Alert, ListRenderItem, Platform, View } from 'react-native'; +import { Alert, ListRenderItem, View } from 'react-native'; import { FlatList } from 'react-native-gesture-handler'; import { useSelector } from 'react-redux'; import { KeyringTypes } from '@metamask/keyring-controller'; @@ -11,7 +11,11 @@ import Cell, { CellVariant, } from '../../../component-library/components/Cells/Cell'; import { useStyles } from '../../../component-library/hooks'; -import Text from '../../../component-library/components/Texts/Text'; +import { selectPrivacyMode } from '../../../selectors/preferencesController'; +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, @@ -29,8 +33,7 @@ import { removeAccountsFromPermissions } from '../../../core/Permissions'; // 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'; const AccountSelectorList = ({ onSelectAccount, @@ -61,30 +64,46 @@ const AccountSelectorList = ({ ? AvatarAccountType.Blockies : AvatarAccountType.JazzIcon, ); - + const privacyMode = useSelector(selectPrivacyMode); 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( diff --git a/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap b/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap index 49c9d6e96b1..036a2be8d53 100644 --- a/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap +++ b/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap @@ -290,6 +290,7 @@ exports[`AccountSelectorList renders all accounts with balances 1`] = ` style={ { "alignItems": "flex-end", + "flexDirection": "column", } } testID="account-balance-by-address-0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272" @@ -309,7 +310,22 @@ exports[`AccountSelectorList renders all accounts with balances 1`] = ` } > $3200.00 -1 ETH + + + 1 ETH
@@ -583,6 +599,7 @@ exports[`AccountSelectorList renders all accounts with balances 1`] = ` style={ { "alignItems": "flex-end", + "flexDirection": "column", } } testID="account-balance-by-address-0xd018538C87232FF95acbCe4870629b75640a78E7" @@ -602,7 +619,22 @@ exports[`AccountSelectorList renders all accounts with balances 1`] = ` } > $6400.00 -2 ETH + + + 2 ETH @@ -1453,6 +1485,7 @@ exports[`AccountSelectorList renders correctly 1`] = ` style={ { "alignItems": "flex-end", + "flexDirection": "column", } } testID="account-balance-by-address-0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272" @@ -1472,7 +1505,22 @@ exports[`AccountSelectorList renders correctly 1`] = ` } > $3200.00 -1 ETH + + + 1 ETH @@ -1746,6 +1794,7 @@ exports[`AccountSelectorList renders correctly 1`] = ` style={ { "alignItems": "flex-end", + "flexDirection": "column", } } testID="account-balance-by-address-0xd018538C87232FF95acbCe4870629b75640a78E7" @@ -1765,7 +1814,22 @@ exports[`AccountSelectorList renders correctly 1`] = ` } > $6400.00 -2 ETH + + + 2 ETH @@ -1949,6 +2013,7 @@ exports[`AccountSelectorList should render all accounts but only the balance for style={ { "alignItems": "flex-end", + "flexDirection": "column", } } testID="account-balance-by-address-0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272" @@ -1968,7 +2033,22 @@ exports[`AccountSelectorList should render all accounts but only the balance for } > $3200.00 -1 ETH + + + 1 ETH 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/AssetIcon/index.test.tsx b/app/components/UI/AssetIcon/index.test.tsx index 84c0941722a..b6541f8ffa8 100644 --- a/app/components/UI/AssetIcon/index.test.tsx +++ b/app/components/UI/AssetIcon/index.test.tsx @@ -10,7 +10,7 @@ const mockInitialState = { ...backgroundState, PreferencesController: { featureFlags: {}, - ipfsGateway: 'https://gateway.pinata.cloud/ipfs/', + ipfsGateway: 'https://dweb.link/ipfs/', lostIdentities: {}, selectedAddress: '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3', useTokenDetection: true, @@ -25,7 +25,7 @@ const mockInitialState = { _W: { featureFlags: {}, frequentRpcList: [], - ipfsGateway: 'https://gateway.pinata.cloud/ipfs/', + ipfsGateway: 'https://dweb.link/ipfs/', lostIdentities: {}, selectedAddress: '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3', useTokenDetection: true, 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 d6266bfbd57..995d61584c0 100644 --- a/app/components/UI/AssetOverview/AssetOverview.tsx +++ b/app/components/UI/AssetOverview/AssetOverview.tsx @@ -1,10 +1,9 @@ import { zeroAddress } from 'ethereumjs-util'; import React, { useCallback, useEffect } from 'react'; -import { Platform, TouchableOpacity, View } from 'react-native'; +import { TouchableOpacity, View } from 'react-native'; import { useDispatch, useSelector } from 'react-redux'; import { strings } from '../../../../locales/i18n'; -import { TOKEN_ASSET_OVERVIEW } from '../../../../wdio/screen-objects/testIDs/Screens/TokenOverviewScreen.testIds'; -import generateTestId from '../../../../wdio/utils/generateTestId'; +import { TokenOverviewSelectorsIDs } from '../../../../e2e/selectors/TokenOverview.selectors'; import { newAssetTransaction } from '../../../actions/transaction'; import AppConstants from '../../../core/AppConstants'; import Engine from '../../../core/Engine'; @@ -259,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/Price/Price.tsx b/app/components/UI/AssetOverview/Price/Price.tsx index f297a40115f..9d95b9e17b5 100644 --- a/app/components/UI/AssetOverview/Price/Price.tsx +++ b/app/components/UI/AssetOverview/Price/Price.tsx @@ -17,7 +17,7 @@ import Text, { import PriceChart from '../PriceChart/PriceChart'; import { distributeDataPoints } from '../PriceChart/utils'; import styleSheet from './Price.styles'; -import { TOKEN_PRICE } from '../../../../../wdio/screen-objects/testIDs/Screens/TokenOverviewScreen.testIds'; +import { TokenOverviewSelectorsIDs } from '../../../../../e2e/selectors/TokenOverview.selectors'; import { TokenI } from '../../Tokens/types'; interface PriceProps { @@ -90,7 +90,7 @@ const Price = ({ {asset.symbol} )} {!isNaN(price) && ( - + {isLoading ? ( 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/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/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/Navbar/index.js b/app/components/UI/Navbar/index.js index fd8aa98b1f8..1e31cfc43d2 100644 --- a/app/components/UI/Navbar/index.js +++ b/app/components/UI/Navbar/index.js @@ -28,7 +28,6 @@ import PickerNetwork from '../../../component-library/components/Pickers/PickerN import BrowserUrlBar from '../BrowserUrlBar'; import generateTestId from '../../../../wdio/utils/generateTestId'; import { NAV_ANDROID_BACK_BUTTON } from '../../../../wdio/screen-objects/testIDs/Screens/NetworksScreen.testids'; -import { REQUEST_SEARCH_RESULTS_BACK_BUTTON } from '../../../../wdio/screen-objects/testIDs/Screens/RequestToken.testIds'; import { BACK_BUTTON_SIMPLE_WEBVIEW } from '../../../../wdio/screen-objects/testIDs/Components/SimpleWebView.testIds'; import Routes from '../../../constants/navigation/Routes'; @@ -43,7 +42,7 @@ import { import { CommonSelectorsIDs } from '../../../../e2e/selectors/Common.selectors'; import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors'; import { NetworksViewSelectorsIDs } from '../../../../e2e/selectors/Settings/NetworksView.selectors'; -import { SendLinkViewSelectorsIDs } from '../../../../e2e/selectors/SendLinkView.selectors'; +import { SendLinkViewSelectorsIDs } from '../../../../e2e/selectors/Receive/SendLinkView.selectors'; import { SendViewSelectorsIDs } from '../../../../e2e/selectors/SendView.selectors'; import { getBlockaidTransactionMetricsParams } from '../../../util/blockaid'; import Icon, { @@ -52,7 +51,7 @@ import Icon, { IconColor, } from '../../../component-library/components/Icons/Icon'; import { AddContactViewSelectorsIDs } from '../../../../e2e/selectors/Settings/Contacts/AddContactView.selectors'; -import { ImportTokenViewSelectorsIDs } from '../../../../e2e/selectors/wallet/ImportTokenView.selectors'; +import { RequestPaymentViewSelectors } from '../../../../e2e/selectors/Receive/RequestPaymentView.selectors'; const trackEvent = (event, params = {}) => { MetaMetrics.getInstance().trackEvent(event, params); @@ -361,7 +360,7 @@ export function getPaymentRequestOptionsTitle( navigation.pop()} style={styles.backButton} - testID={ImportTokenViewSelectorsIDs.BACK_BUTTON} + testID={CommonSelectorsIDs.BACK_ARROW_BUTTON} > navigation.pop()} style={styles.backButton} - testID={ImportTokenViewSelectorsIDs.BACK_BUTTON} + testID={CommonSelectorsIDs.BACK_ARROW_BUTTON} > ( - {title} + + {title} + ), headerStyle: innerStyles.headerStyle, headerLeft: () => @@ -1868,7 +1872,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/index.tsx b/app/components/UI/NetworkModal/index.tsx index 4da8d8d5f1b..b2c4e5f797b 100644 --- a/app/components/UI/NetworkModal/index.tsx +++ b/app/components/UI/NetworkModal/index.tsx @@ -23,7 +23,7 @@ import { import { useTheme } from '../../../util/theme'; import { networkSwitched } from '../../../actions/onboardNetwork'; -import { NetworkApprovalModalSelectorsIDs } from '../../../../e2e/selectors/Modals/NetworkApprovalModal.selectors'; +import { NetworkApprovalBottomSheetSelectorsIDs } from '../../../../e2e/selectors/Network/NetworkApprovalBottomSheet.selectors'; import { selectUseSafeChainsListValidation } from '../../../selectors/preferencesController'; import BottomSheetFooter, { ButtonsAlignment, @@ -150,7 +150,7 @@ const NetworkModals = (props: NetworkProps) => { 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( 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/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.tsx b/app/components/UI/PermissionsSummary/PermissionsSummary.tsx index 9fea3eecabf..9c9015d261f 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, @@ -57,6 +56,8 @@ const PermissionsSummary = ({ isDisconnectAllShown = true, isNetworkSwitch = false, accountAddresses = [], + accounts = [], + networkAvatars = [], }: PermissionsSummaryProps) => { const { colors } = useTheme(); const { styles } = useStyles(styleSheet, { isRenderedAsBottomSheet }); @@ -169,27 +170,56 @@ const PermissionsSummary = ({ 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 +310,24 @@ const PermissionsSummary = ({ )} {!isNetworkSwitch && ( - - - + <> + + + + {getNetworkLabel()} + + + + + ({ + ...avatar, + variant: AvatarVariant.Network, + }))} + /> + + )}
diff --git a/app/components/UI/PermissionsSummary/PermissionsSummary.types.ts b/app/components/UI/PermissionsSummary/PermissionsSummary.types.ts index 470f893f93d..3bf71acd277 100644 --- a/app/components/UI/PermissionsSummary/PermissionsSummary.types.ts +++ b/app/components/UI/PermissionsSummary/PermissionsSummary.types.ts @@ -1,4 +1,5 @@ import { USER_INTENT } from '../../../constants/permissions'; +import { Account } from '../../hooks/useAccounts'; export interface PermissionsSummaryProps { currentPageInformation: { @@ -21,5 +22,7 @@ export interface PermissionsSummaryProps { chainName: string; chainId: string; }; + accounts?: Account[]; accountAddresses?: string[]; + networkAvatars?: ({ name: string; imageSource: string } | null)[]; } diff --git a/app/components/UI/PermissionsSummary/__snapshots__/PermissionsSummary.test.tsx.snap b/app/components/UI/PermissionsSummary/__snapshots__/PermissionsSummary.test.tsx.snap index 2543543d2a1..5fb871e2b3d 100644 --- a/app/components/UI/PermissionsSummary/__snapshots__/PermissionsSummary.test.tsx.snap +++ b/app/components/UI/PermissionsSummary/__snapshots__/PermissionsSummary.test.tsx.snap @@ -488,310 +488,59 @@ exports[`PermissionsSummary should render correctly 1`] = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +1 + 0 networks connected - +
+
+ +
diff --git a/app/components/UI/Ramp/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap b/app/components/UI/Ramp/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap index d8474b6cb32..c8fc6d56cfe 100644 --- a/app/components/UI/Ramp/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap +++ b/app/components/UI/Ramp/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap @@ -8717,7 +8717,7 @@ exports[`BuildQuote View renders correctly 1`] = ` onLoadEnd={[Function]} source={ { - "uri": undefined, + "uri": "", } } style={ @@ -11839,7 +11839,7 @@ exports[`BuildQuote View renders correctly 2`] = ` onLoadEnd={[Function]} source={ { - "uri": undefined, + "uri": "", } } style={ diff --git a/app/components/UI/ReceiveRequest/index.js b/app/components/UI/ReceiveRequest/index.js index adeb81bc78c..8cdbcb0f5cb 100644 --- a/app/components/UI/ReceiveRequest/index.js +++ b/app/components/UI/ReceiveRequest/index.js @@ -28,7 +28,7 @@ import { isNetworkRampSupported } from '../Ramp/utils'; import { createBuyNavigationDetails } from '../Ramp/routes/utils'; import { selectSelectedInternalAccountChecksummedAddress } from '../../../selectors/accountsController'; import { getRampNetworks } from '../../../reducers/fiatOrders'; -import { RequestPaymentModalSelectorsIDs } from '../../../../e2e/selectors/Modals/RequestPaymentModal.selectors'; +import { RequestPaymentModalSelectorsIDs } from '../../../../e2e/selectors/Receive/RequestPaymentModal.selectors'; import { withMetricsAwareness } from '../../../components/hooks/useMetrics'; import { getDecimalChainId } from '../../../util/networks'; import QRAccountDisplay from '../../Views/QRAccountDisplay'; diff --git a/app/components/UI/ReusableModal/__snapshots__/index.test.tsx.snap b/app/components/UI/ReusableModal/__snapshots__/index.test.tsx.snap index 840c56c6f50..b46a1d36dd2 100644 --- a/app/components/UI/ReusableModal/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/ReusableModal/__snapshots__/index.test.tsx.snap @@ -2,6 +2,74 @@ exports[`ReusableModal should render correctly 1`] = ` - + + + + + + `; diff --git a/app/components/UI/ReusableModal/index.test.tsx b/app/components/UI/ReusableModal/index.test.tsx index 2c93fa2b437..f97cf05d210 100644 --- a/app/components/UI/ReusableModal/index.test.tsx +++ b/app/components/UI/ReusableModal/index.test.tsx @@ -1,15 +1,28 @@ import React from 'react'; import { SafeAreaView } from 'react-native'; -import { shallow } from 'enzyme'; +import { render, screen } from '@testing-library/react-native'; import ReusableModal from './'; +import { useNavigation } from '@react-navigation/native'; + +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: jest.fn(), +})); describe('ReusableModal', () => { + beforeEach(() => { + (useNavigation as jest.Mock).mockReturnValue({ + navigate: jest.fn(), + goBack: jest.fn(), + }); + }); + it('should render correctly', () => { - const wrapper = shallow( + render( {null} , ); - expect(wrapper).toMatchSnapshot(); + expect(screen.toJSON()).toMatchSnapshot(); }); }); diff --git a/app/components/UI/Screen/index.test.tsx b/app/components/UI/Screen/index.test.tsx index 2707be55149..a32016dc0d9 100644 --- a/app/components/UI/Screen/index.test.tsx +++ b/app/components/UI/Screen/index.test.tsx @@ -1,15 +1,15 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { render } from '@testing-library/react-native'; import { View } from 'react-native'; import Screen from './'; describe('Screen', () => { it('should render correctly', () => { - const wrapper = shallow( + const { toJSON } = render( Foobar , ); - expect(wrapper).toMatchSnapshot(); + expect(toJSON()).toMatchSnapshot(); }); }); diff --git a/app/components/UI/SliderButton/__snapshots__/index.test.tsx.snap b/app/components/UI/SliderButton/__snapshots__/index.test.tsx.snap index f1d879adf06..ce6e987c71f 100644 --- a/app/components/UI/SliderButton/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/SliderButton/__snapshots__/index.test.tsx.snap @@ -54,7 +54,8 @@ exports[`SliderButton should render correctly 1`] = ` ] } /> - - Incomplete Text - + - - - + diff --git a/app/components/UI/SliderButton/index.test.tsx b/app/components/UI/SliderButton/index.test.tsx index 609b8615192..4a41f222ba4 100644 --- a/app/components/UI/SliderButton/index.test.tsx +++ b/app/components/UI/SliderButton/index.test.tsx @@ -1,15 +1,15 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { render, screen } from '@testing-library/react-native'; import SliderButton from './index'; describe('SliderButton', () => { it('should render correctly', () => { - const wrapper = shallow( + render( , ); - expect(wrapper).toMatchSnapshot(); + expect(screen.toJSON()).toMatchSnapshot(); }); }); diff --git a/app/components/UI/SlippageSlider/__snapshots__/index.test.tsx.snap b/app/components/UI/SlippageSlider/__snapshots__/index.test.tsx.snap index 1ab792850fd..14cdaa1c8f1 100644 --- a/app/components/UI/SlippageSlider/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/SlippageSlider/__snapshots__/index.test.tsx.snap @@ -44,7 +44,6 @@ exports[`SlippageSlider should render correctly 1`] = ` } > - - - + - - - + + + + undefined% - - + diff --git a/app/components/UI/SlippageSlider/index.test.tsx b/app/components/UI/SlippageSlider/index.test.tsx index 75f7754b500..9aea690ccce 100644 --- a/app/components/UI/SlippageSlider/index.test.tsx +++ b/app/components/UI/SlippageSlider/index.test.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { render, screen } from '@testing-library/react-native'; import SlippageSlider from './index'; describe('SlippageSlider', () => { it('should render correctly', () => { - const wrapper = shallow( + render( { formatTooltipText={(text) => `${text}%`} />, ); - expect(wrapper).toMatchSnapshot(); + expect(screen.toJSON()).toMatchSnapshot(); }); }); diff --git a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx index 6f53a175524..d0341d0b444 100644 --- a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx +++ b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx @@ -11,6 +11,7 @@ import ConfirmationFooter from '../../components/StakingConfirmation/Confirmatio import { StakeConfirmationViewProps } from './StakeConfirmationView.types'; import { strings } from '../../../../../../locales/i18n'; import { FooterButtonGroupActions } from '../../components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.types'; +import UnstakingTimeCard from '../../components/StakingConfirmation/UnstakeTimeCard/UnstakeTimeCard'; const MOCK_STAKING_CONTRACT_NAME = 'MM Pooled Staking'; @@ -47,6 +48,7 @@ const StakeConfirmationView = ({ route }: StakeConfirmationViewProps) => { rewardsEth={route.params.annualRewardsETH} rewardsFiat={route.params.annualRewardsFiat} /> + + + + + + + + Unstaking time + + + + + + + + + + + + 1 to 11 days + + + + + + ({ selectCurrentCurrency: jest.fn(() => 'USD'), })); -const mockBalanceBN = new BN('1500000000000000000'); +const mockBalanceBN = toWei('1.5'); // 1.5 ETH const mockPooledStakingContractService: PooledStakingContract = { chainId: ChainId.ETHEREUM, @@ -84,12 +84,25 @@ jest.mock('../../hooks/useStakeContext.ts', () => ({ jest.mock('../../hooks/useBalance', () => ({ __esModule: true, default: () => ({ - balance: '1.5', + balanceETH: '1.5', balanceWei: mockBalanceBN, balanceFiatNumber: '3000', }), })); +const mockGasFee = toWei('0.0001'); + +jest.mock('../../hooks/useStakingGasFee', () => ({ + __esModule: true, + default: () => ({ + estimatedGasFeeWei: mockGasFee, + gasLimit: 70122, + isLoadingStakingGasFee: false, + isStakingGasFeeError: false, + refreshGasValues: jest.fn(), + }), +})); + const mockVaultData = MOCK_GET_VAULT_RESPONSE; // Mock hooks diff --git a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx index 9351d57b785..dd45e400286 100644 --- a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx +++ b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx @@ -17,14 +17,14 @@ import EstimatedAnnualRewardsCard from '../../components/EstimatedAnnualRewardsC import Routes from '../../../../../constants/navigation/Routes'; import styleSheet from './StakeInputView.styles'; import useStakingInputHandlers from '../../hooks/useStakingInput'; -import useBalance from '../../hooks/useBalance'; import InputDisplay from '../../components/InputDisplay'; +import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics'; const StakeInputView = () => { const title = strings('stake.stake_eth'); const navigation = useNavigation(); const { styles, theme } = useStyles(styleSheet, {}); - const { balance, balanceFiatNumber, balanceWei } = useBalance(); + const { trackEvent, createEventBuilder } = useMetrics(); const { isEth, @@ -45,12 +45,23 @@ const StakeInputView = () => { annualRewardsFiat, annualRewardRate, isLoadingVaultData, - } = useStakingInputHandlers(balanceWei); + handleMax, + balanceValue, + } = useStakingInputHandlers(); const navigateToLearnMoreModal = () => { navigation.navigate('StakeModals', { screen: Routes.STAKING.MODALS.LEARN_MORE, }); + trackEvent( + createEventBuilder(MetaMetricsEvents.STAKE_LEARN_MORE_CLICKED) + .addProperties({ + selected_provider: 'consensys', + text: 'Tooltip Question Mark Trigger', + location: 'Stake Input View' + }) + .build() + ); }; const handleStakePress = useCallback(() => { @@ -64,15 +75,36 @@ const StakeInputView = () => { annualRewardRate, }, }); + trackEvent( + createEventBuilder(MetaMetricsEvents.REVIEW_STAKE_BUTTON_CLICKED) + .addProperties({ + selected_provider: 'consensys', + tokens_to_stake_native_value: amountEth, + tokens_to_stake_usd_value: fiatAmount, + }) + .build(), + ); }, [ + amountEth, navigation, amountWei, fiatAmount, annualRewardsETH, annualRewardsFiat, annualRewardRate, + trackEvent, + createEventBuilder ]); + const handleMaxButtonPress = () => { + navigation.navigate('StakeModals', { + screen: Routes.STAKING.MODALS.MAX_INPUT, + params: { + handleMaxPress: handleMax, + }, + }); + }; + const balanceText = strings('stake.balance'); const buttonLabel = !isNonZeroAmount @@ -81,10 +113,6 @@ const StakeInputView = () => { ? strings('stake.not_enough_eth') : strings('stake.review'); - const balanceValue = isEth - ? `${balance} ETH` - : `${balanceFiatNumber?.toString()} ${currentCurrency.toUpperCase()}`; - useEffect(() => { navigation.setOptions( getStakingNavbar(title, navigation, theme.colors, { @@ -121,6 +149,7 @@ const StakeInputView = () => { + - - Stake ETH - + + Stake ETH + + { tokenSymbol="ETH" /> - + - - - Estimated changes - - - - - You receive - - - - + 4.99982 - - - - - + - - - - - - ETH - - + - + + - $ - 12894.52 - + + + 1 to 11 days + + + @@ -1198,157 +1124,6 @@ exports[`UnstakeConfirmationView render matches snapshot 1`] = ` - - - - - - - Unstaking time - - - - - - - - - - - - Up to 11 days - - - - - - ({ default: () => ({ stakedBalanceWei: mockPooledStakeData.assets, stakedBalanceFiat: MOCK_STAKED_ETH_ASSET.balanceFiat, + formattedStakedBalanceETH: '5.79133 ETH', }), })); diff --git a/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx b/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx index ddfa676d158..c9f6149b8fa 100644 --- a/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx +++ b/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx @@ -1,6 +1,5 @@ import { useNavigation } from '@react-navigation/native'; import React, { useCallback, useEffect } from 'react'; -import { BN } from 'ethereumjs-util'; import UnstakeInputViewBanner from './UnstakeBanner'; import { strings } from '../../../../../../locales/i18n'; import Button, { @@ -9,25 +8,23 @@ import Button, { ButtonWidthTypes, } from '../../../../../component-library/components/Buttons/Button'; import { TextVariant } from '../../../../../component-library/components/Texts/Text'; -import { renderFromWei, weiToFiatNumber } from '../../../../../util/number'; import Keypad from '../../../../Base/Keypad'; import { useStyles } from '../../../../hooks/useStyles'; import { getStakingNavbar } from '../../../Navbar'; import ScreenLayout from '../../../Ramp/components/ScreenLayout'; import QuickAmounts from '../../components/QuickAmounts'; import { View } from 'react-native'; -import useStakingInputHandlers from '../../hooks/useStakingInput'; import styleSheet from './UnstakeInputView.styles'; import InputDisplay from '../../components/InputDisplay'; -import useBalance from '../../hooks/useBalance'; import Routes from '../../../../../constants/navigation/Routes'; +import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics'; +import useUnstakingInputHandlers from '../../hooks/useUnstakingInput'; const UnstakeInputView = () => { const title = strings('stake.unstake_eth'); const navigation = useNavigation(); const { styles, theme } = useStyles(styleSheet, {}); - - const { stakedBalanceWei } = useBalance(); + const { trackEvent, createEventBuilder } = useMetrics(); const { isEth, @@ -42,19 +39,10 @@ const UnstakeInputView = () => { percentageOptions, handleAmountPress, handleKeypadChange, - conversionRate, - } = useStakingInputHandlers(new BN(stakedBalanceWei)); - - const stakeBalanceInEth = renderFromWei(stakedBalanceWei, 5); - const stakeBalanceFiatNumber = weiToFiatNumber( - stakedBalanceWei, - conversionRate, - ); + stakedBalanceValue, + } = useUnstakingInputHandlers(); const stakedBalanceText = strings('stake.staked_balance'); - const stakedBalanceValue = isEth - ? `${stakeBalanceInEth} ETH` - : `${stakeBalanceFiatNumber?.toString()} ${currentCurrency.toUpperCase()}`; const buttonLabel = !isNonZeroAmount ? strings('stake.enter_amount') @@ -78,7 +66,16 @@ const UnstakeInputView = () => { amountFiat: fiatAmount, }, }); - }, [amountWei, fiatAmount, navigation]); + trackEvent( + createEventBuilder(MetaMetricsEvents.REVIEW_UNSTAKE_BUTTON_CLICKED) + .addProperties({ + selected_provider: 'consensys', + tokens_to_stake_native_value: amountEth, + tokens_to_stake_usd_value: fiatAmount, + }) + .build(), + ); + }, [amountEth, amountWei, createEventBuilder, fiatAmount, navigation, trackEvent]); return ( diff --git a/app/components/UI/Stake/Views/UnstakeInputView/__snapshots__/UnstakeInputView.test.tsx.snap b/app/components/UI/Stake/Views/UnstakeInputView/__snapshots__/UnstakeInputView.test.tsx.snap index 0e1816f6621..263d1be6b4e 100644 --- a/app/components/UI/Stake/Views/UnstakeInputView/__snapshots__/UnstakeInputView.test.tsx.snap +++ b/app/components/UI/Stake/Views/UnstakeInputView/__snapshots__/UnstakeInputView.test.tsx.snap @@ -102,26 +102,49 @@ exports[`UnstakeInputView render matches snapshot 1`] = ` pointerEvents="box-none" style={ { - "marginHorizontal": 16, + "alignItems": "flex-start", + "bottom": 0, + "justifyContent": "center", + "left": 0, + "opacity": 1, + "position": "absolute", + "top": 0, + } + } + /> + - - Unstake ETH - + + Unstake ETH + + - Unstake anytime. Typically takes up to 11 days to process. + Unstake anytime. Typically takes less than 3 days, but can take up to 11 days to process. { const sheetRef = useRef(null); const navigation = useNavigation(); + const { trackEvent, createEventBuilder } = useMetrics(); const handleClose = () => { sheetRef.current?.onCloseBottomSheet(); @@ -91,6 +93,15 @@ const LearnMoreModal = () => { url: POOLED_STAKING_FAQ_URL, }, }); + trackEvent( + createEventBuilder(MetaMetricsEvents.STAKE_LEARN_MORE_CLICKED) + .addProperties({ + selected_provider: 'consensys', + text: 'Learn More', + location: 'Learn More Modal' + }) + .build() + ); }} // Take to the faq page label={strings('stake.learn_more')} variant={ButtonVariants.Secondary} diff --git a/app/components/UI/Stake/components/MaxInputModal/MaxInputModal.styles.ts b/app/components/UI/Stake/components/MaxInputModal/MaxInputModal.styles.ts new file mode 100644 index 00000000000..ae5cd275a1e --- /dev/null +++ b/app/components/UI/Stake/components/MaxInputModal/MaxInputModal.styles.ts @@ -0,0 +1,23 @@ +import { StyleSheet } from 'react-native'; + +const createMaxInputModalStyles = () => + StyleSheet.create({ + container: { + paddingHorizontal: 16, + }, + textContainer: { + paddingBottom: 16, + paddingRight: 16, + }, + buttonContainer: { + flexDirection: 'row', + gap: 16, + paddingHorizontal: 16, + paddingBottom: 16, + }, + button: { + flex: 1, + }, + }); + +export default createMaxInputModalStyles; diff --git a/app/components/UI/Stake/components/MaxInputModal/MaxInputModal.test.tsx b/app/components/UI/Stake/components/MaxInputModal/MaxInputModal.test.tsx new file mode 100644 index 00000000000..8092f9d9fc5 --- /dev/null +++ b/app/components/UI/Stake/components/MaxInputModal/MaxInputModal.test.tsx @@ -0,0 +1,67 @@ +import { renderScreen } from '../../../../../util/test/renderWithProvider'; +import MaxInputModal from '.'; +import { fireEvent } from '@testing-library/react-native'; +import Routes from '../../../../../constants/navigation/Routes'; + +const mockNavigate = jest.fn(); +const mockGoBack = jest.fn(); +const mockHandleMaxPress = jest.fn(); + +jest.mock('@react-navigation/native', () => { + const actualReactNavigation = jest.requireActual('@react-navigation/native'); + return { + ...actualReactNavigation, + useRoute: () => ({ + params: { + handleMaxPress: mockHandleMaxPress, + }, + }), + useNavigation: () => ({ + navigate: mockNavigate, + goBack: mockGoBack, + }), + }; +}); + +const renderMaxInputModal = () => + renderScreen(MaxInputModal, { name: Routes.STAKING.MODALS.MAX_INPUT }); + +describe('MaxInputModal', () => { + it('render matches snapshot', () => { + const { toJSON } = renderMaxInputModal(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('calls handleMaxPress when "Use max" button is pressed', () => { + const { getByText } = renderMaxInputModal(); + + // Press the "Use Max" button + const useMaxButton = getByText('Use max'); + fireEvent.press(useMaxButton); + + // Check if handleMaxPress was called + expect(mockHandleMaxPress).toHaveBeenCalledTimes(1); + }); + + it('closes the BottomSheet when "Cancel" button is pressed', () => { + const { getByText } = renderMaxInputModal(); + + // Press the "Cancel" button + const cancelButton = getByText('Cancel'); + fireEvent.press(cancelButton); + + // Check if the BottomSheet's close function was called + expect(mockGoBack).toHaveBeenCalled(); + }); + + it('closes the BottomSheet when "Use Max" button is pressed', () => { + const { getByText } = renderMaxInputModal(); + + // Press the "Use Max" button + const useMaxButton = getByText('Use max'); + fireEvent.press(useMaxButton); + + // Check if the BottomSheet's close function was called + expect(mockGoBack).toHaveBeenCalled(); + }); +}); diff --git a/app/components/UI/Stake/components/MaxInputModal/__snapshots__/MaxInputModal.test.tsx.snap b/app/components/UI/Stake/components/MaxInputModal/__snapshots__/MaxInputModal.test.tsx.snap new file mode 100644 index 00000000000..c3aa8b2abf5 --- /dev/null +++ b/app/components/UI/Stake/components/MaxInputModal/__snapshots__/MaxInputModal.test.tsx.snap @@ -0,0 +1,656 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MaxInputModal render matches snapshot 1`] = ` + + + + + + + + + + + + + MaxInput + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Max + + + + + + + + + + + + + Max is the total amount of ETH you have, minus the gas fee required to stake. It’s a good idea to keep some extra ETH in your wallet for future transactions. + + + + + + + + Cancel + + + + + + + Use max + + + + + + + + + + + + + + + + + +`; diff --git a/app/components/UI/Stake/components/MaxInputModal/index.tsx b/app/components/UI/Stake/components/MaxInputModal/index.tsx new file mode 100644 index 00000000000..a7534a2214c --- /dev/null +++ b/app/components/UI/Stake/components/MaxInputModal/index.tsx @@ -0,0 +1,79 @@ +import React, { useRef } from 'react'; +import { View } from 'react-native'; +import BottomSheet, { + type BottomSheetRef, +} from '../../../../../component-library/components/BottomSheets/BottomSheet'; +import Text, { + TextVariant, +} from '../../../../../component-library/components/Texts/Text'; +import Button, { + ButtonSize, + ButtonVariants, + ButtonWidthTypes, +} from '../../../../../component-library/components/Buttons/Button'; +import { strings } from '../../../../../../locales/i18n'; +import BottomSheetHeader from '../../../../../component-library/components/BottomSheets/BottomSheetHeader'; +import createMaxInputModalStyles from './MaxInputModal.styles'; +import { useRoute, RouteProp } from '@react-navigation/native'; + +const styles = createMaxInputModalStyles(); + +interface MaxInputModalRouteParams { + handleMaxPress: () => void; +} + +const MaxInputModal = () => { + const route = + useRoute>(); + const sheetRef = useRef(null); + + const { handleMaxPress } = route.params; + + const handleCancel = () => { + sheetRef.current?.onCloseBottomSheet(); + }; + + const handleConfirm = () => { + sheetRef.current?.onCloseBottomSheet(); + handleMaxPress(); + }; + + return ( + + + + + {strings('stake.max_modal.title')} + + + + + {strings('stake.max_modal.description')} + + + + + + + ); + /** * 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()}