diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3f3edeb14c6..24aac9a37f9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -46,6 +46,7 @@ app/core/DeeplinkManager @MetaMask/mobile-pla scripts/build.sh @MetaMask/mobile-platform fingerprint.config.js @MetaMask/mobile-platform builds.yml @MetaMask/mobile-platform +.github/workflows/create-build-branch.yml @MetaMask/mobile-platform .github/workflows/push-eas-update.yml @MetaMask/mobile-admins .github/workflows/upload-to-testflight.yml @MetaMask/mobile-admins scripts/update-expo-channel.js @MetaMask/mobile-admins diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a7e91c86e4d..7cd8693349e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -84,6 +84,7 @@ jobs: outputs: github_environment: ${{ steps.config.outputs.github_environment }} secrets_json: ${{ steps.config.outputs.secrets_json }} + tap_and_pay_sdk_repo_ssh: ${{ steps.config.outputs.tap_and_pay_sdk_repo_ssh }} signing_aws_role: ${{ steps.config.outputs.signing_aws_role }} signing_aws_secret: ${{ steps.config.outputs.signing_aws_secret }} signing_android_keystore_path: ${{ steps.config.outputs.signing_android_keystore_path }} @@ -110,6 +111,8 @@ jobs: const build = config.builds['${{ inputs.build_name }}']; fs.appendFileSync(process.env.GITHUB_OUTPUT, 'github_environment=' + build.github_environment + '\n'); fs.appendFileSync(process.env.GITHUB_OUTPUT, 'secrets_json=' + JSON.stringify(build.secrets || {}) + '\n'); + const env = build.env || {}; + fs.appendFileSync(process.env.GITHUB_OUTPUT, 'tap_and_pay_sdk_repo_ssh=' + (env.TAP_AND_PAY_SDK_REPO_SSH || '') + '\n'); const signing = build.signing; fs.appendFileSync(process.env.GITHUB_OUTPUT, 'signing_aws_role=' + (signing ? signing.aws_role || '' : '') + '\n'); fs.appendFileSync(process.env.GITHUB_OUTPUT, 'signing_aws_secret=' + (signing ? signing.aws_secret || '' : '') + '\n'); @@ -241,6 +244,35 @@ jobs: XCODE_VERSION: '16.3' run: sudo xcode-select -s "/Applications/Xcode_$XCODE_VERSION.app" + # TapAndPay SDK Setup (Android only) + - name: Clone TapAndPay SDK + if: | + matrix.platform == 'android' && + needs.prepare.outputs.tap_and_pay_sdk_repo_ssh != '' + run: | + if [ -z "$TAP_AND_PAY_SDK_SSH_KEY" ]; then + echo "โš ๏ธ TAP_AND_PAY_SDK_SSH_KEY not set, skipping TapAndPay SDK clone" + exit 0 + fi + mkdir -p ~/.ssh + echo "$TAP_AND_PAY_SDK_SSH_KEY" | base64 -d > ~/.ssh/tap_and_pay_key + chmod 600 ~/.ssh/tap_and_pay_key + trap 'rm -f ~/.ssh/tap_and_pay_key' EXIT + ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null + eval "$(ssh-agent -s)" + ssh-add ~/.ssh/tap_and_pay_key + echo "๐Ÿ“ฆ Cloning TapAndPay SDK into android/libs/..." + git clone --depth 1 "$TAP_AND_PAY_SDK_REPO_SSH" /tmp/tap-and-pay-sdk + mkdir -p android/libs + cp -r /tmp/tap-and-pay-sdk/* android/libs/ + rm -rf /tmp/tap-and-pay-sdk + ssh-add -D + eval "$(ssh-agent -k)" + echo "โœ… TapAndPay SDK installed to android/libs/" + env: + TAP_AND_PAY_SDK_SSH_KEY: ${{ secrets.TAP_AND_PAY_SDK_SSH_KEY }} + TAP_AND_PAY_SDK_REPO_SSH: ${{ needs.prepare.outputs.tap_and_pay_sdk_repo_ssh }} + - name: Apply build config run: | # Load env vars from builds.yml (this step only) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 62e889b089d..f9db7039365 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -214,7 +214,7 @@ jobs: NODE_OPTIONS: --max_old_space_size=12288 - name: Check bundle size - run: ./scripts/js-bundle-stats.sh ios/main.jsbundle 53 + run: ./scripts/js-bundle-stats.sh ios/main.jsbundle 54 - name: Upload iOS bundle uses: actions/upload-artifact@v4 diff --git a/.github/workflows/create-build-branch.yml b/.github/workflows/create-build-branch.yml new file mode 100644 index 00000000000..16ceda52549 --- /dev/null +++ b/.github/workflows/create-build-branch.yml @@ -0,0 +1,44 @@ +name: Create Build Branch + +# Reusable workflow that creates an ephemeral build/- branch +# from a source ref. This avoids pushing version-bump commits directly to +# protected branches (e.g. main). +# +# The caller is responsible for deleting the branch after use. + +on: + workflow_call: + inputs: + source_branch: + description: 'Branch, tag, or SHA to branch from' + required: true + type: string + outputs: + build_branch: + description: 'Name of the created ephemeral build branch' + value: ${{ jobs.create.outputs.branch_name }} + +jobs: + create: + name: Create build branch + runs-on: ubuntu-latest + outputs: + branch_name: ${{ steps.create-branch.outputs.branch_name }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ inputs.source_branch }} + token: ${{ secrets.PR_TOKEN || github.token }} + - name: Create temporary build branch + id: create-branch + env: + SOURCE_BRANCH: ${{ inputs.source_branch }} + run: | + TIMESTAMP=$(date +%s%3N) + SANITIZED=$(echo "$SOURCE_BRANCH" | tr '/' '-') + BRANCH_NAME="build/${SANITIZED}-${TIMESTAMP}" + git checkout -b "$BRANCH_NAME" + git push origin "$BRANCH_NAME" + echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT" + echo "โœ… Created build branch: $BRANCH_NAME" diff --git a/.github/workflows/crowdin_download_translations.yml b/.github/workflows/crowdin_download_translations.yml new file mode 100644 index 00000000000..26b56c09d10 --- /dev/null +++ b/.github/workflows/crowdin_download_translations.yml @@ -0,0 +1,34 @@ +name: Crowdin Download Approved Translations Action + +on: + schedule: + - cron: '0 */12 * * *' + workflow_dispatch: + +jobs: + download-translations: + runs-on: ubuntu-latest + timeout-minutes: 3 + + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + # Use PAT to ensure that the commit later can trigger status check workflows + token: ${{ secrets.METAMASKBOT_CROWDIN_TOKEN }} + + - name: crowdin download approved translations + uses: crowdin/github-action@a3160b9e5a9e00739392c23da5e580c6cabe526d + with: + upload_sources: false + upload_translations: false # disabled to prevent translations overwriting Blends translations + download_translations: true # created separate action to pull down completed translations + export_only_approved: true + pull_request_title: 'chore: New Crowdin Translations by GitHub Action' + github_user_name: metamaskbot + github_user_email: metamaskbot@users.noreply.github.com + env: + GITHUB_TOKEN: ${{ secrets.METAMASKBOT_CROWDIN_TOKEN }} + GITHUB_ACTOR: metamaskbot + CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/.github/workflows/crowdin_action.yml b/.github/workflows/crowdin_upload_sources.yml similarity index 77% rename from .github/workflows/crowdin_action.yml rename to .github/workflows/crowdin_upload_sources.yml index 51ce96a1457..a15e453300d 100644 --- a/.github/workflows/crowdin_action.yml +++ b/.github/workflows/crowdin_upload_sources.yml @@ -1,15 +1,14 @@ -name: Crowdin Action +name: Crowdin Upload Sources Action on: push: branches: - main - schedule: - - cron: '0 */12 * * *' jobs: - synchronize-with-crowdin: + upload-sources: runs-on: ubuntu-latest + timeout-minutes: 3 steps: - name: Checkout @@ -18,11 +17,12 @@ jobs: # Use PAT to ensure that the commit later can trigger status check workflows token: ${{ secrets.METAMASKBOT_CROWDIN_TOKEN }} - - name: crowdin action + - name: crowdin upload sources uses: crowdin/github-action@a3160b9e5a9e00739392c23da5e580c6cabe526d with: + upload_sources: true upload_translations: false # disabled to prevent translations overwriting Blends translations - download_translations: true + download_translations: false # created separate action to pull down completed translations github_user_name: metamaskbot github_user_email: metamaskbot@users.noreply.github.com env: diff --git a/.github/workflows/nightly-build-temp.yml b/.github/workflows/nightly-build-temp.yml new file mode 100644 index 00000000000..fbf4e79f44e --- /dev/null +++ b/.github/workflows/nightly-build-temp.yml @@ -0,0 +1,105 @@ +name: Nightly Build (temp) + +# Runs on a schedule (4 AM UTC) like the old nightly-temp-branch-sync. +# Each build creates its own ephemeral branch via create-build-branch.yml, +# so the persistent chore/temp-nightly branch is no longer needed. +# +# iOS builds are handled end-to-end by upload-to-testflight-temp.yml (build + upload). +# Android builds use create-build-branch.yml + build.yml (build artifacts only). +# +# rc depends on exp within each platform stream to ensure the external version +# service produces sequential numbers (N for exp, N+1 for rc). +# iOS and Android streams run in parallel; their version numbers will differ +# but remain unique, which is all TestFlight / Play Store require. + +on: + schedule: + # NOTE: Scheduled workflows ALWAYS run from the default branch (main) + - cron: '0 4 * * *' + workflow_dispatch: + +permissions: + contents: write + id-token: write + +jobs: + # โ”€โ”€ iOS exp: build + TestFlight upload โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + ios-exp: + name: Nightly iOS exp + uses: ./.github/workflows/upload-to-testflight-temp.yml + with: + source_branch: main + environment: exp + testflight_group: 'MetaMask BETA & Release Candidates' + secrets: inherit + + # โ”€โ”€ iOS rc: build + TestFlight upload (after exp for sequential versions) โ”€ + ios-rc: + name: Nightly iOS rc + needs: [ios-exp] + uses: ./.github/workflows/upload-to-testflight-temp.yml + with: + source_branch: main + environment: rc + testflight_group: 'MetaMask BETA & Release Candidates' + secrets: inherit + + # โ”€โ”€ Android exp: ephemeral branch + build โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + android-exp-branch: + uses: ./.github/workflows/create-build-branch.yml + with: + source_branch: main + secrets: inherit + + android-exp: + name: Nightly Android exp + needs: [android-exp-branch] + uses: ./.github/workflows/build.yml + with: + build_name: main-exp + platform: android + skip_version_bump: false + source_branch: ${{ needs.android-exp-branch.outputs.build_branch }} + secrets: inherit + + # โ”€โ”€ Android rc: ephemeral branch + build (after exp for sequential versions) โ”€ + android-rc-branch: + needs: [android-exp] + uses: ./.github/workflows/create-build-branch.yml + with: + source_branch: main + secrets: inherit + + android-rc: + name: Nightly Android rc + needs: [android-rc-branch] + uses: ./.github/workflows/build.yml + with: + build_name: main-rc + platform: android + skip_version_bump: false + source_branch: ${{ needs.android-rc-branch.outputs.build_branch }} + secrets: inherit + + # โ”€โ”€ Cleanup Android ephemeral branches โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + # iOS branches are cleaned up by upload-to-testflight-temp.yml internally. + cleanup: + name: Cleanup Android build branches + needs: [android-exp-branch, android-rc-branch, android-exp, android-rc] + if: always() + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.PR_TOKEN || github.token }} + - name: Delete ephemeral branches + env: + EXP_BRANCH: ${{ needs.android-exp-branch.outputs.build_branch }} + RC_BRANCH: ${{ needs.android-rc-branch.outputs.build_branch }} + run: | + for branch in "$EXP_BRANCH" "$RC_BRANCH"; do + if [ -n "$branch" ]; then + git push origin --delete "$branch" || true + echo "๐Ÿงน Deleted: $branch" + fi + done diff --git a/.github/workflows/security-code-scanner.yml b/.github/workflows/security-code-scanner.yml index 50e3b2e5c7a..68b8b8a3fba 100644 --- a/.github/workflows/security-code-scanner.yml +++ b/.github/workflows/security-code-scanner.yml @@ -24,6 +24,7 @@ jobs: paths_ignored: | tests/ docs/ + scripts/money-movement/debug-dashboard/ .storybook/ '**/*.test.js' '**/*.test.ts' diff --git a/.github/workflows/upload-to-testflight-temp.yml b/.github/workflows/upload-to-testflight-temp.yml new file mode 100644 index 00000000000..64d57f920a3 --- /dev/null +++ b/.github/workflows/upload-to-testflight-temp.yml @@ -0,0 +1,217 @@ +name: Upload to TestFlight (temp) + +# Dedicated workflow to build iOS (via build.yml) and upload to TestFlight. +# All TestFlight logic lives here; build.yml only builds and uploads artifacts. +# +on: + workflow_call: + inputs: + source_branch: + description: 'Branch, tag, or SHA to build' + required: true + type: string + environment: + description: 'Build environment / track (exp, beta, rc)' + required: true + type: string + testflight_group: + description: 'TestFlight external testing group' + required: false + type: string + default: 'MetaMask BETA & Release Candidates' + workflow_dispatch: + inputs: + source_branch: + description: 'Branch, tag, or SHA to build' + required: true + type: string + default: 'main' + environment: + description: 'Build environment / track' + required: true + type: choice + options: + - exp + - beta + - rc + default: rc + testflight_group: + description: 'TestFlight external testing group' + required: true + type: choice + default: 'MetaMask BETA & Release Candidates' + options: + - 'MetaMask BETA & Release Candidates' + - 'MM Card Team' + - 'Ramp Provider Testing' + +# contents: write required by build.yml update-build-version job (version bump) +permissions: + contents: write + id-token: write + +jobs: + # Create a temporary branch so the version-bump commit can be pushed without + # hitting branch-protection rules on the source branch (e.g. main). + prepare-build-branch: + uses: ./.github/workflows/create-build-branch.yml + with: + source_branch: ${{ inputs.source_branch }} + secrets: inherit + + build: + name: Build iOS (${{ inputs.environment || 'rc' }}) + needs: [prepare-build-branch] + uses: ./.github/workflows/build.yml + with: + build_name: main-${{ inputs.environment || 'rc' }} + platform: ios + skip_version_bump: false + source_branch: ${{ needs.prepare-build-branch.outputs.build_branch }} + secrets: inherit + + testflight-upload-summary: + name: TestFlight upload summary + needs: [build, prepare-build-branch] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ needs.prepare-build-branch.outputs.build_branch }} + - name: Display TestFlight upload summary + run: | + BUILD_VERSION=$(node -p "require('./package.json').version") + BUILD_NUMBER=$(awk '/versionCode/{print $2}' android/app/build.gradle) + { + echo "### ๐Ÿ“ฒ TestFlight Upload" + echo "" + echo "| Field | Value |" + echo "| --- | --- |" + echo "| **Source branch** | \`${{ inputs.source_branch }}\` |" + echo "| **Build branch** | \`${{ needs.prepare-build-branch.outputs.build_branch }}\` |" + echo "| **Build name** | \`main-${{ inputs.environment || 'rc' }}\` |" + echo "| **Build version** | \`${BUILD_VERSION}\` |" + echo "| **Build number** | \`${BUILD_NUMBER}\` |" + echo "| **TestFlight group** | ${{ inputs.testflight_group || 'MetaMask BETA & Release Candidates' }} |" + echo "| **Workflow ref** | \`${{ github.ref_name }}\` (required for AWS) |" + } >> "$GITHUB_STEP_SUMMARY" + + # Pulls App Store Connect API keys from AWS Secrets Manager (OIDC). + # Workflow must run from main; build uses the temporary build branch. + upload-ios-testflight: + name: Upload iOS to TestFlight + needs: [build, testflight-upload-summary] + runs-on: ghcr.io/cirruslabs/macos-runner:sequoia-xl + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Ruby (iOS) + uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb #v1 + with: + ruby-version: '3.2.9' + working-directory: ios + bundler-cache: true + + - name: Download iOS IPA artifact + uses: actions/download-artifact@v4 + with: + name: ios-ipa-main-${{ inputs.environment || 'rc' }} + + - name: Find IPA path + id: ipa + run: | + IPA=$(find . -name '*.ipa' -type f | head -1) + if [ -z "$IPA" ]; then + echo "::error::No .ipa file found in artifact" + exit 1 + fi + case "$IPA" in /*) ABS="$IPA" ;; *) ABS="$PWD/$IPA" ;; esac + echo "path=$ABS" >> "$GITHUB_OUTPUT" + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_APPLE_TESTFLIGHT }} + aws-region: 'us-east-2' + + - name: Fetch Apple API keys from AWS Secrets Manager + run: | + echo "๐Ÿ” Fetching App Store Connect API keys from Secrets Manager..." + secret_id="metamask-mobile-main-apple-api-keys" + secret_json=$(aws secretsmanager get-secret-value \ + --region 'us-east-2' \ + --secret-id "$secret_id" \ + --query SecretString \ + --output text) + + for key in APP_STORE_CONNECT_API_KEY_ISSUER_ID APP_STORE_CONNECT_API_KEY_KEY_ID; do + value=$(echo "$secret_json" | jq -r --arg k "$key" '.[$k] // empty') + if [ -z "$value" ]; then + echo "::error::Missing key in secret: $key" + exit 1 + fi + echo "::add-mask::$value" + echo "${key}=${value}" >> "$GITHUB_ENV" + done + + key=APP_STORE_CONNECT_API_KEY_KEY_CONTENT + value=$(echo "$secret_json" | jq -r --arg k "$key" '.[$k] // empty') + if [ -z "$value" ]; then + echo "::error::Missing key in secret: $key" + exit 1 + fi + while IFS= read -r line || [ -n "$line" ]; do + [ -n "$line" ] && echo "::add-mask::$line" + done <<< "$(printf '%s\n' "$value")" + + delim="APPLEP8$(openssl rand -hex 16)" + { + printf '%s<<%s\n' "$key" "$delim" + printf '%s\n' "$value" + printf '%s\n' "$delim" + } >> "$GITHUB_ENV" + + echo "โœ… Apple API keys loaded from AWS" + + - name: Setup App Store Connect API Key + run: | + bash scripts/setup-app-store-connect-api-key.sh \ + "$APP_STORE_CONNECT_API_KEY_ISSUER_ID" \ + "$APP_STORE_CONNECT_API_KEY_KEY_ID" \ + "$APP_STORE_CONNECT_API_KEY_KEY_CONTENT" + + - name: Upload to TestFlight + run: | + bash scripts/upload-to-testflight.sh \ + "github_actions_main-${{ inputs.environment || 'rc' }}" \ + "${{ inputs.source_branch }}" \ + "${{ steps.ipa.outputs.path }}" \ + "${{ inputs.testflight_group || 'MetaMask BETA & Release Candidates' }}" + + - name: Cleanup API Key + if: always() + run: | + rm -f ios/AuthKey.p8 + echo "๐Ÿงน Cleaned up API key file" + + cleanup-build-branch: + name: Cleanup build branch + needs: [prepare-build-branch, upload-ios-testflight] + if: always() + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.PR_TOKEN || github.token }} + - name: Delete temporary build branch + env: + BRANCH: ${{ needs.prepare-build-branch.outputs.build_branch }} + run: | + if [ -n "$BRANCH" ]; then + git push origin --delete "$BRANCH" || true + echo "๐Ÿงน Deleted build branch: $BRANCH" + fi diff --git a/.gitignore b/.gitignore index b9a3b495831..612ee998e9f 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ android/app/.project android/app/bin/ android/app/gradle* android/app/_build* +android/libs .cxx/ # if we ever want to add google services diff --git a/.js.env.example b/.js.env.example index 13cec147ce2..4d465521867 100644 --- a/.js.env.example +++ b/.js.env.example @@ -54,6 +54,10 @@ export METAMASK_ENVIRONMENT="dev" # Build type: "main" or "flask" or "beta" export METAMASK_BUILD_TYPE="main" +# Optional: enable Ramps debug dashboard bridge in __DEV__ (WebSocket + fetch instrumentation). +# See app/components/UI/Ramp/debug/README.md +# export RAMPS_DEBUG_DASHBOARD="true" + # Segment SDK proxy endpoint and write key export SEGMENT_WRITE_KEY_DEV="" export SEGMENT_PROXY_URL_DEV="" diff --git a/CHANGELOG.md b/CHANGELOG.md index 54895ba1644..d2d8a238dd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [7.71.1] + +### Changed + +- Pointed Market Insights digest fallback URL at the production endpoint when `DIGEST_API_URL` is not set at build time (#28098) + ## [7.71.0] ### Added @@ -11072,7 +11078,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#957](https://github.com/MetaMask/metamask-mobile/pull/957): fix timeouts (#957) - [#954](https://github.com/MetaMask/metamask-mobile/pull/954): Bugfix: onboarding navigation (#954) -[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.71.0...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.71.1...HEAD +[7.71.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.71.0...v7.71.1 [7.71.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.70.1...v7.71.0 [7.70.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.70.0...v7.70.1 [7.70.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.69.1...v7.70.0 diff --git a/android/build.gradle b/android/build.gradle index c700511130b..10517268751 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -35,6 +35,7 @@ buildscript { url(new File(['node', '--print', "require.resolve('@notifee/react-native/package.json')"].execute(null, rootDir).text.trim(), '../android/libs')) } maven { url "https://jitpack.io" } + maven { url "file://${rootDir}/libs" } maven { url "https://cdn.veriff.me/android/" } } } diff --git a/app/__mocks__/react-native-video.tsx b/app/__mocks__/react-native-video.tsx index 9242ec6bb17..03d75e80863 100644 --- a/app/__mocks__/react-native-video.tsx +++ b/app/__mocks__/react-native-video.tsx @@ -1,8 +1,10 @@ import React from 'react'; import { View } from 'react-native'; -const VideoMock = ({ testID }: { testID?: string }) => ( - +type VideoMockProps = { testID?: string } & Record; + +const VideoMock = ({ testID, ...rest }: VideoMockProps) => ( + ); VideoMock.displayName = 'VideoMock'; diff --git a/app/component-library/components-temp/MultichainAccounts/MultichainAddWalletActions/__snapshots__/MultichainAddWalletActions.test.tsx.snap b/app/component-library/components-temp/MultichainAccounts/MultichainAddWalletActions/__snapshots__/MultichainAddWalletActions.test.tsx.snap index 63cfae3a63e..b69ebaa1767 100644 --- a/app/component-library/components-temp/MultichainAccounts/MultichainAddWalletActions/__snapshots__/MultichainAddWalletActions.test.tsx.snap +++ b/app/component-library/components-temp/MultichainAccounts/MultichainAddWalletActions/__snapshots__/MultichainAddWalletActions.test.tsx.snap @@ -20,365 +20,371 @@ exports[`MultichainAddWalletActions renders correctly 1`] = ` } > - - - + + /> + + - + - MultichainAddWalletActions - + + MultichainAddWalletActions + + + - - - - - + - - - + + - - - - - + + + - Import a wallet - + + Import a wallet + + - - - - - - - + + + - Import an account - + + Import an account + + - - - - - - - + + + - Add a hardware wallet - + + Add a hardware wallet + + - - + + - - - + + + `; diff --git a/app/component-library/components/Navigation/TabBar/TabBar.test.tsx b/app/component-library/components/Navigation/TabBar/TabBar.test.tsx index 3935b9a2c3d..2c44dd81193 100644 --- a/app/component-library/components/Navigation/TabBar/TabBar.test.tsx +++ b/app/component-library/components/Navigation/TabBar/TabBar.test.tsx @@ -22,6 +22,9 @@ interface TestTabDescriptor { tabBarIconKey: TabBarIconKey; rootScreenName: string; callback?: () => void; + isHidden?: boolean; + onLeave?: () => void; + isSelected?: (rootScreenName: string) => boolean; }; } @@ -30,7 +33,7 @@ interface TestDescriptors { } // Mock the navigation object with proper typing -const navigation: NavigationHelpers = { +const navigation = { navigate: jest.fn(), goBack: jest.fn(), reset: jest.fn(), @@ -38,10 +41,11 @@ const navigation: NavigationHelpers = { dispatch: jest.fn(), isFocused: jest.fn(), canGoBack: jest.fn(), - dangerouslyGetParent: jest.fn(), - dangerouslyGetState: jest.fn(), + getParent: jest.fn(), + getState: jest.fn(), emit: jest.fn(), -}; + getId: jest.fn(), +} as unknown as NavigationHelpers; const mockInitialState = { engine: { @@ -218,4 +222,256 @@ describe('TabBar', () => { fireEvent.press(getByTestId(`tab-bar-item-${TabBarIconKey.Trending}`)); expect(navigation.navigate).toHaveBeenCalledWith(Routes.TRENDING_VIEW); }); + + it('does not render hidden tabs', () => { + const stateWithHidden = { + index: 0, + routes: [ + { key: '1', name: 'Tab 1' }, + { key: '2', name: 'Tab 2' }, + ], + }; + const descriptorsWithHidden: TestDescriptors = { + '1': { + options: { + tabBarIconKey: TabBarIconKey.Wallet, + rootScreenName: Routes.WALLET_VIEW, + }, + }, + '2': { + options: { + tabBarIconKey: TabBarIconKey.Browser, + rootScreenName: Routes.BROWSER.VIEW, + isHidden: true, + }, + }, + }; + + const { queryByTestId, getByTestId } = renderWithProvider( + } + descriptors={ + descriptorsWithHidden as unknown as Record< + string, + ExtendedBottomTabDescriptor + > + } + navigation={navigation} + />, + { state: mockInitialState }, + ); + + expect(getByTestId(`tab-bar-item-${TabBarIconKey.Wallet}`)).toBeTruthy(); + expect(queryByTestId(`tab-bar-item-${TabBarIconKey.Browser}`)).toBeNull(); + }); + + it('calls callback when tab is pressed', () => { + const mockCallback = jest.fn(); + const stateWithCallback = { + index: 0, + routes: [{ key: '1', name: 'Tab 1' }], + }; + const descriptorsWithCallback: TestDescriptors = { + '1': { + options: { + tabBarIconKey: TabBarIconKey.Wallet, + rootScreenName: Routes.WALLET_VIEW, + callback: mockCallback, + }, + }, + }; + + const { getByTestId } = renderWithProvider( + } + descriptors={ + descriptorsWithCallback as Record + } + navigation={navigation} + />, + { state: mockInitialState }, + ); + + fireEvent.press(getByTestId(`tab-bar-item-${TabBarIconKey.Wallet}`)); + expect(mockCallback).toHaveBeenCalledTimes(1); + }); + + it('calls onLeave when switching tabs', () => { + const mockOnLeave = jest.fn(); + const stateWithTwoTabs = { + index: 0, + routes: [ + { key: '1', name: 'Tab 1' }, + { key: '2', name: 'Tab 2' }, + ], + routeNames: ['Tab 1', 'Tab 2'], + }; + const descriptorsWithOnLeave: TestDescriptors = { + '1': { + options: { + tabBarIconKey: TabBarIconKey.Wallet, + rootScreenName: Routes.WALLET_VIEW, + onLeave: mockOnLeave, + }, + }, + '2': { + options: { + tabBarIconKey: TabBarIconKey.Activity, + rootScreenName: Routes.TRANSACTIONS_VIEW, + }, + }, + }; + + const { getByTestId } = renderWithProvider( + } + descriptors={ + descriptorsWithOnLeave as unknown as Record< + string, + ExtendedBottomTabDescriptor + > + } + navigation={navigation} + />, + { state: mockInitialState }, + ); + + fireEvent.press(getByTestId(`tab-bar-item-${TabBarIconKey.Activity}`)); + expect(mockOnLeave).toHaveBeenCalledTimes(1); + }); + + it('uses custom isSelected function when provided', () => { + const customIsSelected = jest.fn(() => true); + const stateWithCustomSelected = { + index: 1, + routes: [ + { key: '1', name: 'Tab 1' }, + { key: '2', name: 'Tab 2' }, + ], + routeNames: ['Tab 1', 'Tab 2'], + }; + const descriptorsWithCustomSelected: TestDescriptors = { + '1': { + options: { + tabBarIconKey: TabBarIconKey.Wallet, + rootScreenName: Routes.WALLET_VIEW, + isSelected: customIsSelected, + }, + }, + '2': { + options: { + tabBarIconKey: TabBarIconKey.Activity, + rootScreenName: Routes.TRANSACTIONS_VIEW, + }, + }, + }; + + renderWithProvider( + } + descriptors={ + descriptorsWithCustomSelected as unknown as Record< + string, + ExtendedBottomTabDescriptor + > + } + navigation={navigation} + />, + { state: mockInitialState }, + ); + + expect(customIsSelected).toHaveBeenCalled(); + }); + + it('handles trade button (wallet actions) navigation', () => { + const tradeState = { + index: 0, + routes: [{ key: '1', name: 'Tab 1' }], + }; + const tradeDescriptors: TestDescriptors = { + '1': { + options: { + tabBarIconKey: TabBarIconKey.Trade, + rootScreenName: Routes.MODAL.TRADE_WALLET_ACTIONS, + }, + }, + }; + + const { getByTestId } = renderWithProvider( + } + descriptors={ + tradeDescriptors as Record + } + navigation={navigation} + />, + { state: mockInitialState }, + ); + + expect(getByTestId(`tab-bar-item-${TabBarIconKey.Trade}`)).toBeTruthy(); + }); + + it('returns null for undefined descriptor', () => { + const stateWithMissingDescriptor = { + index: 0, + routes: [ + { key: '1', name: 'Tab 1' }, + { key: '2', name: 'Tab 2' }, + ], + }; + const partialDescriptors: TestDescriptors = { + '1': { + options: { + tabBarIconKey: TabBarIconKey.Wallet, + rootScreenName: Routes.WALLET_VIEW, + }, + }, + }; + + const { queryByTestId, getByTestId } = renderWithProvider( + } + descriptors={ + partialDescriptors as Record + } + navigation={navigation} + />, + { state: mockInitialState }, + ); + + expect(getByTestId(`tab-bar-item-${TabBarIconKey.Wallet}`)).toBeTruthy(); + expect(queryByTestId(`tab-bar-item-undefined`)).toBeNull(); + }); + + it('tracks analytics when actions button is clicked', () => { + const actionsState = { + index: 0, + routes: [{ key: '1', name: 'Tab 1' }], + }; + const actionsDescriptors: TestDescriptors = { + '1': { + options: { + tabBarIconKey: TabBarIconKey.Actions, + rootScreenName: Routes.MODAL.WALLET_ACTIONS, + }, + }, + }; + + const { getByTestId } = renderWithProvider( + } + descriptors={ + actionsDescriptors as Record + } + navigation={navigation} + />, + { state: mockInitialState }, + ); + + fireEvent.press(getByTestId(`tab-bar-item-${TabBarIconKey.Actions}`)); + expect(navigation.navigate).toHaveBeenCalledWith( + Routes.MODAL.ROOT_MODAL_FLOW, + { screen: Routes.MODAL.WALLET_ACTIONS }, + ); + }); }); diff --git a/app/component-library/components/Navigation/TabBar/TabBar.tsx b/app/component-library/components/Navigation/TabBar/TabBar.tsx index c533bf28336..5fce65f79ef 100644 --- a/app/component-library/components/Navigation/TabBar/TabBar.tsx +++ b/app/component-library/components/Navigation/TabBar/TabBar.tsx @@ -46,7 +46,9 @@ const TabBar = ({ state, descriptors, navigation }: TabBarProps) => { const renderTabBarItem = useCallback( (route: { name: string; key: string }, index: number) => { - const { options } = descriptors[route.key]; + const descriptor = descriptors[route.key]; + if (!descriptor) return null; + const { options } = descriptor; const tabBarIconKey = options.tabBarIconKey; //TODO: use another option on add it to the prop interface const callback = options.callback; diff --git a/app/component-library/components/Navigation/TabBar/TabBar.types.ts b/app/component-library/components/Navigation/TabBar/TabBar.types.ts index 8a69ad06270..3c986b1a587 100644 --- a/app/component-library/components/Navigation/TabBar/TabBar.types.ts +++ b/app/component-library/components/Navigation/TabBar/TabBar.types.ts @@ -1,12 +1,7 @@ // Third party dependencies. -import { - BottomTabBarOptions, - BottomTabBarProps, -} from '@react-navigation/bottom-tabs'; -import { - BottomTabDescriptor, - BottomTabNavigationOptions, -} from '@react-navigation/bottom-tabs/lib/typescript/src/types'; +import { BottomTabBarProps } from '@react-navigation/bottom-tabs'; +import { BottomTabNavigationOptions } from '@react-navigation/bottom-tabs/lib/typescript/src/types'; +import { TabNavigationState, ParamListBase } from '@react-navigation/native'; // External dependencies. import { IconName } from '../../Icons/Icon'; @@ -32,28 +27,29 @@ export type IconByTabBarIconKey = { [key in TabBarIconKey]: IconName; }; -export interface ExtendedBottomTabDescriptor extends BottomTabDescriptor { - options: BottomTabNavigationOptions & { - tabBarIconKey: TabBarIconKey; - callback: () => void; - rootScreenName: string; - isSelected?: (rootScreenName: string) => boolean; - isHidden?: boolean; - /** - * Callback fired when leaving this tab (switching to another tab). - * Useful for cleanup actions like ending analytics sessions. - */ - onLeave?: () => void; - }; +export interface ExtendedBottomTabNavigationOptions + extends BottomTabNavigationOptions { + tabBarIconKey: TabBarIconKey; + callback?: () => void; + rootScreenName: string; + isSelected?: (rootScreenName: string) => boolean; + isHidden?: boolean; + /** + * Callback fired when leaving this tab (switching to another tab). + * Useful for cleanup actions like ending analytics sessions. + */ + onLeave?: () => void; } -type TabBarOptions = BottomTabBarOptions & { - descriptors: { - [key: string]: ExtendedBottomTabDescriptor; - }; -}; +export interface ExtendedBottomTabDescriptor { + options: ExtendedBottomTabNavigationOptions; +} /** * TabBar component props. */ -export type TabBarProps = BottomTabBarProps; +export interface TabBarProps { + state: TabNavigationState; + descriptors: Record; + navigation: BottomTabBarProps['navigation']; +} diff --git a/app/components/Nav/App/App.test.tsx b/app/components/Nav/App/App.test.tsx index a6ff20b3b89..1110d1f1c2e 100644 --- a/app/components/Nav/App/App.test.tsx +++ b/app/components/Nav/App/App.test.tsx @@ -442,5 +442,726 @@ describe('App', () => { expect(getByTestId('ramp-unsupported-modal')).toBeOnTheScreen(); }); }); + + it('has wallet action modal routes defined', () => { + expect(Routes.MODAL.WALLET_ACTIONS).toBeDefined(); + expect(Routes.MODAL.TRADE_WALLET_ACTIONS).toBeDefined(); + expect(Routes.MODAL.DELETE_WALLET).toBeDefined(); + }); + + it('has sheet routes defined', () => { + expect(Routes.SHEET.ACCOUNT_SELECTOR).toBeDefined(); + expect(Routes.SHEET.NETWORK_SELECTOR).toBeDefined(); + expect(Routes.SHEET.ONBOARDING_SHEET).toBeDefined(); + expect(Routes.SHEET.SDK_LOADING).toBeDefined(); + }); + + it('has fund action menu route defined', () => { + expect(Routes.MODAL.FUND_ACTION_MENU).toBeDefined(); + }); + + it('has more token actions menu route defined', () => { + expect(Routes.MODAL.MORE_TOKEN_ACTIONS_MENU).toBeDefined(); + }); + + it('has modal confirmation routes defined', () => { + expect(Routes.MODAL.MODAL_CONFIRMATION).toBeDefined(); + expect(Routes.MODAL.MODAL_MANDATORY).toBeDefined(); + }); + + it('has sdk related routes defined', () => { + expect(Routes.SHEET.SDK_FEEDBACK).toBeDefined(); + expect(Routes.SHEET.SDK_MANAGE_CONNECTIONS).toBeDefined(); + expect(Routes.SHEET.SDK_DISCONNECT).toBeDefined(); + }); + }); + + describe('app route constants', () => { + it('has onboarding flow routes defined', () => { + expect(Routes.ONBOARDING.NAV).toBeDefined(); + expect(Routes.ONBOARDING.HOME_NAV).toBeDefined(); + expect(Routes.ONBOARDING.LOGIN).toBeDefined(); + }); + + it('has hardware wallet routes defined', () => { + expect(Routes.HW.CONNECT_LEDGER).toBeDefined(); + expect(Routes.HW.CONNECT).toBeDefined(); + expect(Routes.HW.SELECT_DEVICE).toBeDefined(); + expect(Routes.HW.LEDGER_CONNECT).toBeDefined(); + }); + + it('has vault recovery routes defined', () => { + expect(Routes.VAULT_RECOVERY.RESTORE_WALLET).toBeDefined(); + expect(Routes.VAULT_RECOVERY.WALLET_RESTORED).toBeDefined(); + expect(Routes.VAULT_RECOVERY.WALLET_RESET_NEEDED).toBeDefined(); + }); + + it('has network routes defined', () => { + expect(Routes.ADD_NETWORK).toBeDefined(); + expect(Routes.EDIT_NETWORK).toBeDefined(); + }); + + it('has lock screen route defined', () => { + expect(Routes.LOCK_SCREEN).toBeDefined(); + }); + + it('has confirmation routes defined', () => { + expect(Routes.CONFIRMATION_REQUEST_MODAL).toBeDefined(); + expect(Routes.CONFIRMATION_SWITCH_ACCOUNT_TYPE).toBeDefined(); + expect(Routes.CONFIRMATION_PAY_WITH_MODAL).toBeDefined(); + }); + + it('has multichain account routes defined', () => { + expect(Routes.MULTICHAIN_ACCOUNTS.ACCOUNT_DETAILS).toBeDefined(); + expect(Routes.MULTICHAIN_ACCOUNTS.ACCOUNT_GROUP_DETAILS).toBeDefined(); + expect(Routes.MULTICHAIN_ACCOUNTS.ADDRESS_LIST).toBeDefined(); + expect(Routes.MULTICHAIN_ACCOUNTS.PRIVATE_KEY_LIST).toBeDefined(); + }); + + it('has ledger transaction modal routes defined', () => { + expect(Routes.LEDGER_TRANSACTION_MODAL).toBeDefined(); + expect(Routes.LEDGER_MESSAGE_SIGN_MODAL).toBeDefined(); + }); + + it('has QR signing routes defined', () => { + expect(Routes.QR_SIGNING_TRANSACTION_MODAL).toBeDefined(); + expect(Routes.QR_TAB_SWITCHER).toBeDefined(); + }); + + it('has edit account name route defined', () => { + expect(Routes.EDIT_ACCOUNT_NAME).toBeDefined(); + }); + + it('has multi SRP routes defined', () => { + expect(Routes.MULTI_SRP.IMPORT).toBeDefined(); + }); + + it('has options sheet route defined', () => { + expect(Routes.OPTIONS_SHEET).toBeDefined(); + }); + + it('has fox loader route defined', () => { + expect(Routes.FOX_LOADER).toBeDefined(); + }); + + it('has webview routes defined', () => { + expect(Routes.WEBVIEW.SIMPLE).toBeDefined(); + expect(Routes.WEBVIEW.MAIN).toBeDefined(); + }); + }); + + describe('App version handling', () => { + it('should handle version storage operations', async () => { + const mockStore = configureMockStore(); + const store = mockStore(initialState); + + const Providers = ({ children }: { children: React.ReactElement }) => ( + + + + {children} + + + + ); + + render(, { wrapper: Providers }); + + await waitFor(() => { + expect(StorageWrapper.getItem).toHaveBeenCalled(); + }); + }); + }); + + describe('AppFlow navigation structure', () => { + it('has import private key view route defined', () => { + expect(Routes.QR_TAB_SWITCHER).toBeDefined(); + }); + + it('has max browser tabs modal route defined', () => { + expect(Routes.MODAL.MAX_BROWSER_TABS_MODAL).toBeDefined(); + }); + + it('has settings reveal private credential route defined', () => { + expect(Routes.SETTINGS.REVEAL_PRIVATE_CREDENTIAL).toBeDefined(); + }); + + it('has multichain account cell actions route defined', () => { + expect(Routes.MULTICHAIN_ACCOUNTS.ACCOUNT_CELL_ACTIONS).toBeDefined(); + }); + }); + + describe('Onboarding navigation', () => { + it('has onboarding success flow route defined', () => { + expect(Routes.ONBOARDING.SUCCESS_FLOW).toBeDefined(); + }); + + it('has onboarding success route defined', () => { + expect(Routes.ONBOARDING.SUCCESS).toBeDefined(); + }); + + it('has onboarding default settings route defined', () => { + expect(Routes.ONBOARDING.DEFAULT_SETTINGS).toBeDefined(); + }); + + it('has onboarding general settings route defined', () => { + expect(Routes.ONBOARDING.GENERAL_SETTINGS).toBeDefined(); + }); + + it('has onboarding assets settings route defined', () => { + expect(Routes.ONBOARDING.ASSETS_SETTINGS).toBeDefined(); + }); + + it('has onboarding security settings route defined', () => { + expect(Routes.ONBOARDING.SECURITY_SETTINGS).toBeDefined(); + }); + + it('has onboarding import from secret recovery phrase route defined', () => { + expect( + Routes.ONBOARDING.IMPORT_FROM_SECRET_RECOVERY_PHRASE, + ).toBeDefined(); + }); + + it('has social login routes defined', () => { + expect(Routes.ONBOARDING.SOCIAL_LOGIN_SUCCESS_NEW_USER).toBeDefined(); + expect( + Routes.ONBOARDING.SOCIAL_LOGIN_SUCCESS_EXISTING_USER, + ).toBeDefined(); + }); + + it('has wallet creation error route defined', () => { + expect(Routes.ONBOARDING.WALLET_CREATION_ERROR).toBeDefined(); + }); + }); + + describe('Detected tokens flow', () => { + it('has detected tokens routes defined', () => { + expect(Routes.SHEET.BASIC_FUNCTIONALITY).toBeDefined(); + expect(Routes.SHEET.CONFIRM_TURN_ON_BACKUP_AND_SYNC).toBeDefined(); + }); + }); + + describe('Multichain account details actions', () => { + it('has multichain account details action routes defined', () => { + expect( + Routes.SHEET.MULTICHAIN_ACCOUNT_DETAILS.ACCOUNT_ACTIONS, + ).toBeDefined(); + expect( + Routes.SHEET.MULTICHAIN_ACCOUNT_DETAILS.LEGACY_EDIT_ACCOUNT_NAME, + ).toBeDefined(); + expect( + Routes.SHEET.MULTICHAIN_ACCOUNT_DETAILS.SHARE_ADDRESS, + ).toBeDefined(); + expect( + Routes.SHEET.MULTICHAIN_ACCOUNT_DETAILS.SHARE_ADDRESS_QR, + ).toBeDefined(); + expect( + Routes.SHEET.MULTICHAIN_ACCOUNT_DETAILS.DELETE_ACCOUNT, + ).toBeDefined(); + expect( + Routes.SHEET.MULTICHAIN_ACCOUNT_DETAILS.SRP_REVEAL_QUIZ, + ).toBeDefined(); + expect( + Routes.SHEET.MULTICHAIN_ACCOUNT_DETAILS.REVEAL_PRIVATE_CREDENTIAL, + ).toBeDefined(); + expect( + Routes.SHEET.MULTICHAIN_ACCOUNT_DETAILS.REVEAL_SRP_CREDENTIAL, + ).toBeDefined(); + }); + }); + + describe('Root modal flow screens', () => { + it('has seedphrase modal route defined', () => { + expect(Routes.SHEET.SEEDPHRASE_MODAL).toBeDefined(); + }); + + it('has skip account security modal route defined', () => { + expect(Routes.SHEET.SKIP_ACCOUNT_SECURITY_MODAL).toBeDefined(); + }); + + it('has success error sheet route defined', () => { + expect(Routes.SHEET.SUCCESS_ERROR_SHEET).toBeDefined(); + }); + + it('has add account route defined', () => { + expect(Routes.SHEET.ADD_ACCOUNT).toBeDefined(); + }); + + it('has experience enhancer route defined', () => { + expect(Routes.SHEET.EXPERIENCE_ENHANCER).toBeDefined(); + }); + + it('has data collection route defined', () => { + expect(Routes.SHEET.DATA_COLLECTION).toBeDefined(); + }); + + it('has account connect route defined', () => { + expect(Routes.SHEET.ACCOUNT_CONNECT).toBeDefined(); + }); + + it('has account permissions route defined', () => { + expect(Routes.SHEET.ACCOUNT_PERMISSIONS).toBeDefined(); + }); + + it('has revoke all account permissions route defined', () => { + expect(Routes.SHEET.REVOKE_ALL_ACCOUNT_PERMISSIONS).toBeDefined(); + }); + + it('has connection details route defined', () => { + expect(Routes.SHEET.CONNECTION_DETAILS).toBeDefined(); + }); + + it('has permitted networks info sheet route defined', () => { + expect(Routes.SHEET.PERMITTED_NETWORKS_INFO_SHEET).toBeDefined(); + }); + + it('has token sort route defined', () => { + expect(Routes.SHEET.TOKEN_SORT).toBeDefined(); + }); + + it('has network manager route defined', () => { + expect(Routes.SHEET.NETWORK_MANAGER).toBeDefined(); + }); + + it('has ambiguous address route defined', () => { + expect(Routes.SHEET.AMBIGUOUS_ADDRESS).toBeDefined(); + }); + + it('has turn off remember me route defined', () => { + expect(Routes.MODAL.TURN_OFF_REMEMBER_ME).toBeDefined(); + }); + + it('has srp reveal quiz route defined', () => { + expect(Routes.MODAL.SRP_REVEAL_QUIZ).toBeDefined(); + }); + + it('has account actions route defined', () => { + expect(Routes.SHEET.ACCOUNT_ACTIONS).toBeDefined(); + }); + + it('has fiat on testnets friction route defined', () => { + expect(Routes.SHEET.FIAT_ON_TESTNETS_FRICTION).toBeDefined(); + }); + + it('has show ipfs route defined', () => { + expect(Routes.SHEET.SHOW_IPFS).toBeDefined(); + }); + + it('has show nft display media route defined', () => { + expect(Routes.SHEET.SHOW_NFT_DISPLAY_MEDIA).toBeDefined(); + }); + + it('has nft auto detection modal route defined', () => { + expect(Routes.MODAL.NFT_AUTO_DETECTION_MODAL).toBeDefined(); + }); + + it('has whats new route defined', () => { + expect(Routes.MODAL.WHATS_NEW).toBeDefined(); + }); + + it('has multi rpc migration modal route defined', () => { + expect(Routes.MODAL.MULTI_RPC_MIGRATION_MODAL).toBeDefined(); + }); + + it('has show token id route defined', () => { + expect(Routes.SHEET.SHOW_TOKEN_ID).toBeDefined(); + }); + + it('has origin spam modal route defined', () => { + expect(Routes.SHEET.ORIGIN_SPAM_MODAL).toBeDefined(); + }); + + it('has change in simulation modal route defined', () => { + expect(Routes.SHEET.CHANGE_IN_SIMULATION_MODAL).toBeDefined(); + }); + + it('has tooltip modal route defined', () => { + expect(Routes.SHEET.TOOLTIP_MODAL).toBeDefined(); + }); + + it('has deep link modal route defined', () => { + expect(Routes.MODAL.DEEP_LINK_MODAL).toBeDefined(); + }); + + it('has multichain accounts intro route defined', () => { + expect(Routes.MODAL.MULTICHAIN_ACCOUNTS_INTRO).toBeDefined(); + }); + + it('has multichain accounts learn more route defined', () => { + expect(Routes.MODAL.MULTICHAIN_ACCOUNTS_LEARN_MORE).toBeDefined(); + }); + + it('has pna25 notice bottom sheet route defined', () => { + expect(Routes.MODAL.PNA25_NOTICE_BOTTOM_SHEET).toBeDefined(); + }); + + it('has sdk return to dapp notification route defined', () => { + expect(Routes.SDK.RETURN_TO_DAPP_NOTIFICATION).toBeDefined(); + }); + + it('has card notification route defined', () => { + expect(Routes.CARD.NOTIFICATION).toBeDefined(); + }); + + it('has multichain transaction details route defined', () => { + expect(Routes.SHEET.MULTICHAIN_TRANSACTION_DETAILS).toBeDefined(); + }); + + it('has transaction details route defined', () => { + expect(Routes.SHEET.TRANSACTION_DETAILS).toBeDefined(); + }); + + it('has import wallet tip route defined', () => { + expect(Routes.SHEET.IMPORT_WALLET_TIP).toBeDefined(); + }); + + it('has select srp route defined', () => { + expect(Routes.SHEET.SELECT_SRP).toBeDefined(); + }); + }); + + describe('Flow navigators', () => { + it('has onboarding success flow screens defined', () => { + expect(Routes.ONBOARDING.SUCCESS).toBeDefined(); + expect(Routes.ONBOARDING.DEFAULT_SETTINGS).toBeDefined(); + expect(Routes.ONBOARDING.GENERAL_SETTINGS).toBeDefined(); + expect(Routes.ONBOARDING.ASSETS_SETTINGS).toBeDefined(); + expect(Routes.ONBOARDING.SECURITY_SETTINGS).toBeDefined(); + }); + + it('has vault recovery flow screens defined', () => { + expect(Routes.VAULT_RECOVERY.RESTORE_WALLET).toBeDefined(); + expect(Routes.VAULT_RECOVERY.WALLET_RESTORED).toBeDefined(); + expect(Routes.VAULT_RECOVERY.WALLET_RESET_NEEDED).toBeDefined(); + }); + + it('has detected tokens flow screens defined', () => { + expect(Routes.SHEET.BASIC_FUNCTIONALITY).toBeDefined(); + }); + + it('has multichain account group details screens defined', () => { + expect(Routes.MULTICHAIN_ACCOUNTS.ACCOUNT_GROUP_DETAILS).toBeDefined(); + expect(Routes.MULTICHAIN_ACCOUNTS.WALLET_DETAILS).toBeDefined(); + }); + + it('has multichain account details action screens defined', () => { + expect( + Routes.SHEET.MULTICHAIN_ACCOUNT_DETAILS.ACCOUNT_ACTIONS, + ).toBeDefined(); + expect( + Routes.SHEET.MULTICHAIN_ACCOUNT_DETAILS.LEGACY_EDIT_ACCOUNT_NAME, + ).toBeDefined(); + expect( + Routes.SHEET.MULTICHAIN_ACCOUNT_DETAILS.SHARE_ADDRESS, + ).toBeDefined(); + expect( + Routes.SHEET.MULTICHAIN_ACCOUNT_DETAILS.DELETE_ACCOUNT, + ).toBeDefined(); + }); + }); + + describe('Import flows', () => { + it('has import private key routes defined', () => { + expect(Routes.QR_TAB_SWITCHER).toBeDefined(); + }); + + it('has import SRP routes defined', () => { + expect(Routes.MULTI_SRP.IMPORT).toBeDefined(); + }); + + it('has connect QR hardware flow defined', () => { + expect(Routes.HW.CONNECT).toBeDefined(); + }); + + it('has ledger connect flow defined', () => { + expect(Routes.HW.LEDGER_CONNECT).toBeDefined(); + }); + }); + + describe('Modal stacks', () => { + it('has root modal flow route defined', () => { + expect(Routes.MODAL.ROOT_MODAL_FLOW).toBeDefined(); + }); + + it('has confirmation modal routes defined', () => { + expect(Routes.CONFIRMATION_REQUEST_MODAL).toBeDefined(); + expect(Routes.CONFIRMATION_SWITCH_ACCOUNT_TYPE).toBeDefined(); + expect(Routes.CONFIRMATION_PAY_WITH_MODAL).toBeDefined(); + }); + + it('has ledger modal routes defined', () => { + expect(Routes.LEDGER_TRANSACTION_MODAL).toBeDefined(); + expect(Routes.LEDGER_MESSAGE_SIGN_MODAL).toBeDefined(); + }); + + it('has QR signing modal route defined', () => { + expect(Routes.QR_SIGNING_TRANSACTION_MODAL).toBeDefined(); + }); + }); + + describe('Additional sheets and modals', () => { + it('has security badge bottom sheet route defined', () => { + expect(Routes.MODAL.SECURITY_BADGE_BOTTOM_SHEET).toBeDefined(); + }); + + it('has more token actions menu route defined', () => { + expect(Routes.MODAL.MORE_TOKEN_ACTIONS_MENU).toBeDefined(); + }); + + it('has fund action menu route defined', () => { + expect(Routes.MODAL.FUND_ACTION_MENU).toBeDefined(); + }); + + it('has update needed modal route defined', () => { + expect(Routes.MODAL.UPDATE_NEEDED).toBeDefined(); + }); + + it('has OTA updates modal route defined', () => { + expect(Routes.MODAL.OTA_UPDATES_MODAL).toBeDefined(); + }); + }); + + describe('Account management screens', () => { + it('has account selector route defined', () => { + expect(Routes.SHEET.ACCOUNT_SELECTOR).toBeDefined(); + }); + + it('has address selector route defined', () => { + expect(Routes.SHEET.ADDRESS_SELECTOR).toBeDefined(); + }); + + it('has add account route defined', () => { + expect(Routes.SHEET.ADD_ACCOUNT).toBeDefined(); + }); + + it('has account actions route defined', () => { + expect(Routes.SHEET.ACCOUNT_ACTIONS).toBeDefined(); + }); + + it('has edit account name route defined', () => { + expect(Routes.EDIT_ACCOUNT_NAME).toBeDefined(); + }); + + it('has network selector route defined', () => { + expect(Routes.SHEET.NETWORK_SELECTOR).toBeDefined(); + }); + + it('has network manager route defined', () => { + expect(Routes.SHEET.NETWORK_MANAGER).toBeDefined(); + }); + }); + + describe('Permission screens', () => { + it('has account connect route defined', () => { + expect(Routes.SHEET.ACCOUNT_CONNECT).toBeDefined(); + }); + + it('has account permissions route defined', () => { + expect(Routes.SHEET.ACCOUNT_PERMISSIONS).toBeDefined(); + }); + + it('has connection details route defined', () => { + expect(Routes.SHEET.CONNECTION_DETAILS).toBeDefined(); + }); + + it('has permitted networks info sheet route defined', () => { + expect(Routes.SHEET.PERMITTED_NETWORKS_INFO_SHEET).toBeDefined(); + }); + }); + + describe('SDK screens', () => { + it('has SDK loading route defined', () => { + expect(Routes.SHEET.SDK_LOADING).toBeDefined(); + }); + + it('has SDK feedback route defined', () => { + expect(Routes.SHEET.SDK_FEEDBACK).toBeDefined(); + }); + + it('has SDK manage connections route defined', () => { + expect(Routes.SHEET.SDK_MANAGE_CONNECTIONS).toBeDefined(); + }); + + it('has SDK disconnect route defined', () => { + expect(Routes.SHEET.SDK_DISCONNECT).toBeDefined(); + }); + + it('has SDK return to dapp notification route defined', () => { + expect(Routes.SDK.RETURN_TO_DAPP_NOTIFICATION).toBeDefined(); + }); + }); + + describe('Settings and preference screens', () => { + it('has basic functionality route defined', () => { + expect(Routes.SHEET.BASIC_FUNCTIONALITY).toBeDefined(); + }); + + it('has confirm turn on backup and sync route defined', () => { + expect(Routes.SHEET.CONFIRM_TURN_ON_BACKUP_AND_SYNC).toBeDefined(); + }); + + it('has experience enhancer route defined', () => { + expect(Routes.SHEET.EXPERIENCE_ENHANCER).toBeDefined(); + }); + + it('has data collection route defined', () => { + expect(Routes.SHEET.DATA_COLLECTION).toBeDefined(); + }); + + it('has fiat on testnets friction route defined', () => { + expect(Routes.SHEET.FIAT_ON_TESTNETS_FRICTION).toBeDefined(); + }); + }); + + describe('NFT and token screens', () => { + it('has show IPFS route defined', () => { + expect(Routes.SHEET.SHOW_IPFS).toBeDefined(); + }); + + it('has show NFT display media route defined', () => { + expect(Routes.SHEET.SHOW_NFT_DISPLAY_MEDIA).toBeDefined(); + }); + + it('has NFT auto detection modal route defined', () => { + expect(Routes.MODAL.NFT_AUTO_DETECTION_MODAL).toBeDefined(); + }); + + it('has show token ID route defined', () => { + expect(Routes.SHEET.SHOW_TOKEN_ID).toBeDefined(); + }); + + it('has token sort route defined', () => { + expect(Routes.SHEET.TOKEN_SORT).toBeDefined(); + }); + }); + + describe('Security screens', () => { + it('has SRP reveal quiz route defined', () => { + expect(Routes.MODAL.SRP_REVEAL_QUIZ).toBeDefined(); + }); + + it('has turn off remember me route defined', () => { + expect(Routes.MODAL.TURN_OFF_REMEMBER_ME).toBeDefined(); + }); + + it('has seedphrase modal route defined', () => { + expect(Routes.SHEET.SEEDPHRASE_MODAL).toBeDefined(); + }); + + it('has skip account security modal route defined', () => { + expect(Routes.SHEET.SKIP_ACCOUNT_SECURITY_MODAL).toBeDefined(); + }); + + it('has reveal private credential route defined', () => { + expect(Routes.SETTINGS.REVEAL_PRIVATE_CREDENTIAL).toBeDefined(); + }); + }); + + describe('Notification and alert screens', () => { + it('has origin spam modal route defined', () => { + expect(Routes.SHEET.ORIGIN_SPAM_MODAL).toBeDefined(); + }); + + it('has change in simulation modal route defined', () => { + expect(Routes.SHEET.CHANGE_IN_SIMULATION_MODAL).toBeDefined(); + }); + + it('has ambiguous address route defined', () => { + expect(Routes.SHEET.AMBIGUOUS_ADDRESS).toBeDefined(); + }); + + it('has tooltip modal route defined', () => { + expect(Routes.SHEET.TOOLTIP_MODAL).toBeDefined(); + }); + + it('has whats new route defined', () => { + expect(Routes.MODAL.WHATS_NEW).toBeDefined(); + }); + }); + + describe('Multichain introduction screens', () => { + it('has multichain accounts intro route defined', () => { + expect(Routes.MODAL.MULTICHAIN_ACCOUNTS_INTRO).toBeDefined(); + }); + + it('has multichain accounts learn more route defined', () => { + expect(Routes.MODAL.MULTICHAIN_ACCOUNTS_LEARN_MORE).toBeDefined(); + }); + + it('has PNA25 notice bottom sheet route defined', () => { + expect(Routes.MODAL.PNA25_NOTICE_BOTTOM_SHEET).toBeDefined(); + }); + }); + + describe('Transaction screens', () => { + it('has multichain transaction details route defined', () => { + expect(Routes.SHEET.MULTICHAIN_TRANSACTION_DETAILS).toBeDefined(); + }); + + it('has transaction details route defined', () => { + expect(Routes.SHEET.TRANSACTION_DETAILS).toBeDefined(); + }); + }); + + describe('Ramp screens', () => { + it('has eligibility failed modal route defined', () => { + expect(Routes.SHEET.ELIGIBILITY_FAILED_MODAL).toBeDefined(); + }); + + it('has unsupported region modal route defined', () => { + expect(Routes.SHEET.UNSUPPORTED_REGION_MODAL).toBeDefined(); + }); + }); + + describe('Wallet action screens', () => { + it('has wallet actions route defined', () => { + expect(Routes.MODAL.WALLET_ACTIONS).toBeDefined(); + }); + + it('has trade wallet actions route defined', () => { + expect(Routes.MODAL.TRADE_WALLET_ACTIONS).toBeDefined(); + }); + + it('has delete wallet route defined', () => { + expect(Routes.MODAL.DELETE_WALLET).toBeDefined(); + }); + }); + + describe('Card screens', () => { + it('has card notification route defined', () => { + expect(Routes.CARD.NOTIFICATION).toBeDefined(); + }); + }); + + describe('Deep link screens', () => { + it('has deep link modal route defined', () => { + expect(Routes.MODAL.DEEP_LINK_MODAL).toBeDefined(); + }); + }); + + describe('Options screens', () => { + it('has options sheet route defined', () => { + expect(Routes.OPTIONS_SHEET).toBeDefined(); + }); + }); + + describe('Browser tabs', () => { + it('has max browser tabs modal route defined', () => { + expect(Routes.MODAL.MAX_BROWSER_TABS_MODAL).toBeDefined(); + }); + }); + + describe('Network screens', () => { + it('has add network route defined', () => { + expect(Routes.ADD_NETWORK).toBeDefined(); + }); + + it('has edit network route defined', () => { + expect(Routes.EDIT_NETWORK).toBeDefined(); + }); + + it('has multi RPC migration modal route defined', () => { + expect(Routes.MODAL.MULTI_RPC_MIGRATION_MODAL).toBeDefined(); + }); }); }); diff --git a/app/components/Nav/App/App.tsx b/app/components/Nav/App/App.tsx index 0a373c9f479..17f756c31ad 100644 --- a/app/components/Nav/App/App.tsx +++ b/app/components/Nav/App/App.tsx @@ -27,6 +27,7 @@ import { getVersion } from 'react-native-device-info'; import { Authentication } from '../../../core/'; import { colors as importedColors } from '../../../styles/common'; import Routes from '../../../constants/navigation/Routes'; +import { clearStackNavigatorOptions } from '../../../constants/navigation/clearStackNavigatorOptions'; import ModalConfirmation from '../../../component-library/components/Modals/ModalConfirmation'; import Toast, { ToastContext, @@ -116,6 +117,7 @@ import { } from '../../../util/trace'; import getUIStartupSpan from '../../../core/Performance/UIStartup'; import { selectExistingUser } from '../../../reducers/user/selectors'; +import { useTheme } from '../../../util/theme'; import { Confirm } from '../../Views/confirmations/components/confirm'; import ImportNewSecretRecoveryPhrase from '../../Views/ImportNewSecretRecoveryPhrase'; import { SelectSRPBottomSheet } from '../../Views/SelectSRP/SelectSRPBottomSheet'; @@ -161,21 +163,14 @@ import TransactionDetailsSheet from '../../UI/TransactionElement/TransactionDeta import ImportWalletTipBottomSheet from '../../UI/TransactionElement/ImportWalletTipBottomSheet'; import { AccessRestrictedProvider } from '../../UI/Compliance'; -const clearStackNavigatorOptions = { - headerShown: false, - cardStyle: { - backgroundColor: 'transparent', - cardStyleInterpolator: () => ({ - overlayStyle: { - opacity: 0, - }, - }), - }, - animationEnabled: false, -}; - const Stack = createStackNavigator(); +// Type helper for screen components that use v5 pattern of requiring route props +// In React Navigation v6, screen components should ideally use useRoute() hook, +// but for migration compatibility, we cast these components to satisfy the type checker. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ScreenComponent = React.ComponentType; + const SocialLoginSuccessNewUser = () => ; const SocialLoginSuccessExistingUser = () => ( @@ -186,13 +181,13 @@ const OnboardingSuccessFlow = () => ( ( /> @@ -257,7 +252,7 @@ const OnboardingNav = () => ( /> ( /> @@ -295,7 +290,7 @@ const OnboardingNav = () => ( * child OnboardingNav navigator to push modals on top of it */ const SimpleWebviewScreen = () => ( - + ); @@ -303,35 +298,50 @@ const SimpleWebviewScreen = () => ( const OnboardingRootNav = () => ( - - - -); - -const VaultRecoveryFlow = () => ( - - ); +const VaultRecoveryFlow = () => { + const { colors } = useTheme(); + + return ( + + + + + + ); +}; + const AddNetworkFlow = () => { const route = useRoute(); @@ -348,14 +358,14 @@ const AddNetworkFlow = () => { const DetectedTokensFlow = () => ( ); @@ -366,7 +376,9 @@ interface RootModalFlowProps { }; } const RootModalFlow = (props: RootModalFlowProps) => ( - + ( /> ( /> ( /> ( /> ( /> ( /> ( /> - - + + ( } ( /> ( /> ( /> ( ); -const ImportPrivateKeyView = () => ( - - - - - -); +const ImportPrivateKeyView = () => { + const { colors } = useTheme(); + + return ( + + + + + + ); +}; const ImportSRPView = () => ( ( name={Routes.MULTI_SRP.IMPORT} component={ImportNewSecretRecoveryPhrase} /> - + ({ overlayStyle: { @@ -706,7 +733,7 @@ const MultichainAccountDetails = () => { > { > { /> { /> @@ -845,7 +872,6 @@ const MultichainAddressList = () => { headerShown: false, animationEnabled: true, }} - mode={'modal'} > { const route = useRoute(); return ( - + ( headerShown: false, cardStyle: { backgroundColor: importedColors.transparent }, }} - mode={'modal'} > ); -const AppFlow = () => ( - - - - - - - - - - - - { +const AppFlow = () => { + const { colors } = useTheme(); + + return ( + + + + + + + + + + + { + + } + - } - - - - - ({ - cardStyle: { - transform: [ - { - translateX: current.progress.interpolate({ - inputRange: [0, 1], - outputRange: [layouts.screen.width, 0], - }), - }, - ], - }, - }), - }} - /> - ({ - cardStyle: { - transform: [ - { - translateX: current.progress.interpolate({ - inputRange: [0, 1], - outputRange: [layouts.screen.width, 0], - }), - }, - ], - }, - }), - }} - /> - - - - - ({ - overlayStyle: { - opacity: 0, - }, - }), - }} - name={Routes.LEDGER_TRANSACTION_MODAL} - component={LedgerTransactionModal} - /> - ({ - overlayStyle: { - opacity: 0, - }, - }), - }} - name={Routes.QR_SIGNING_TRANSACTION_MODAL} - component={QRSigningTransactionModal} - /> - ({ - overlayStyle: { - opacity: 0, - }, - }), - }} - name={Routes.LEDGER_MESSAGE_SIGN_MODAL} - component={LedgerMessageSignModal} - /> - - - - {isNetworkUiRedesignEnabled() ? ( + + + ({ + cardStyle: { + transform: [ + { + translateX: current.progress.interpolate({ + inputRange: [0, 1], + outputRange: [layouts.screen.width, 0], + }), + }, + ], + }, + }), + }} + /> + ({ + cardStyle: { + transform: [ + { + translateX: current.progress.interpolate({ + inputRange: [0, 1], + outputRange: [layouts.screen.width, 0], + }), + }, + ], + }, + }), + }} + /> + + + + + ({ + overlayStyle: { + opacity: 0, + }, + }), + }} + name={Routes.LEDGER_TRANSACTION_MODAL} + component={LedgerTransactionModal} + /> + ({ + overlayStyle: { + opacity: 0, + }, + }), + }} + name={Routes.QR_SIGNING_TRANSACTION_MODAL} + component={QRSigningTransactionModal} + /> + ({ + overlayStyle: { + opacity: 0, + }, + }), + }} + name={Routes.LEDGER_MESSAGE_SIGN_MODAL} + component={LedgerMessageSignModal} + /> + + + ( gestureEnabled: true, }} /> - ) : null} - - - - - -); + {isNetworkUiRedesignEnabled() ? ( + + ) : null} + + + + + + ); +}; const App: React.FC = () => { const { toastRef } = useContext(ToastContext); diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js index f2f0f3c897e..1229dca2a6f 100644 --- a/app/components/Nav/Main/MainNavigator.js +++ b/app/components/Nav/Main/MainNavigator.js @@ -78,6 +78,7 @@ import { SnapsSettingsList } from '../../Views/Snaps/SnapsSettingsList'; import { SnapSettings } from '../../Views/Snaps/SnapSettings'; ///: END:ONLY_INCLUDE_IF import Routes from '../../../constants/navigation/Routes'; +import { clearStackNavigatorOptions } from '../../../constants/navigation/clearStackNavigatorOptions'; import { MetaMetricsEvents } from '../../../core/Analytics'; import { TabBarIconKey } from '../../../component-library/components/Navigation/TabBar/TabBar.types'; import { selectProviderConfig } from '../../../selectors/networkController'; @@ -86,6 +87,7 @@ import SDKSessionsManager from '../../Views/SDK/SDKSessionsManager/SDKSessionsMa import PermissionsManager from '../../Views/Settings/PermissionsSettings/PermissionsManager'; import { getDecimalChainId } from '../../../util/networks'; import { useAnalytics } from '../../../components/hooks/useAnalytics/useAnalytics'; +import { useTheme } from '../../../util/theme'; import DeprecatedNetworkDetails from '../../UI/DeprecatedNetworkModal'; import ConfirmAddAsset from '../../Views/AddAsset/Views/ConfirmAddTokenView/ConfirmAddAsset'; import { AesCryptoTestForm } from '../../Views/AesCryptoTestForm'; @@ -166,21 +168,8 @@ const slideFromRightAnimation = { }), }; -const clearStackNavigatorOptions = { - headerShown: false, - cardStyle: { - backgroundColor: 'transparent', - cardStyleInterpolator: () => ({ - overlayStyle: { - opacity: 0, - }, - }), - }, - animationEnabled: false, -}; - const WalletModalFlow = () => ( - + ( ); -const WalletTabModalFlow = () => ( - - - -); +const WalletTabModalFlow = () => { + const { colors } = useTheme(); + return ( + + + + ); +}; -const TransactionsHome = () => ( - - - - - - - - - - -); +const TransactionsHome = () => { + const { colors } = useTheme(); + return ( + + + + + + + + + + + ); +}; -const RewardsHome = () => ( - - - - - - - - -); +const RewardsHome = () => { + const { colors } = useTheme(); + return ( + + + + + + + + + ); +}; /* eslint-disable react/prop-types */ -const BrowserFlow = (props) => ( - - - - - -); +const BrowserFlow = (props) => { + const { colors } = useTheme(); + return ( + + + + + + ); +}; -const ExploreHome = () => ( - - - -); +const ExploreHome = () => { + const { colors } = useTheme(); + return ( + + + + ); +}; ///: BEGIN:ONLY_INCLUDE_IF(external-snaps) const SnapsSettingsStack = () => ( @@ -375,7 +410,6 @@ const SnapsSettingsStack = () => ( const NotificationsOptInStack = () => ( { }; return ( - + {/* Home Tab */} { const Webview = () => ( - + ); @@ -857,7 +891,6 @@ const NotificationsModeView = (props) => ( options={NotificationsSettings.navigationOptions} /> { const isMarketInsightsPerpsEnabled = useSelector( selectMarketInsightsPerpsEnabled, ); + const { colors } = useTheme(); const isSocialLeaderboardEnabled = useSelector( selectSocialLeaderboardEnabled, ); @@ -953,13 +987,14 @@ const MainNavigator = () => { screenOptions={{ headerShown: false, }} - mode={'modal'} initialRouteName={'Home'} > + ({ @@ -973,6 +1008,7 @@ const MainNavigator = () => { name={Routes.DEPRECATED_NETWORK_DETAILS} component={DeprecatedNetworkDetails} options={{ + presentation: 'modal', //Refer to - https://reactnavigation.org/docs/stack-navigator/#animations cardStyle: { backgroundColor: importedColors.transparent }, cardStyleInterpolator: () => ({ @@ -982,7 +1018,6 @@ const MainNavigator = () => { }), }} /> - { @@ -1095,7 +1133,10 @@ const MainNavigator = () => { {isPerpsEnabled && ( <> @@ -1156,7 +1197,10 @@ const MainNavigator = () => { )} diff --git a/app/components/Nav/Main/MainNavigator.test.js b/app/components/Nav/Main/MainNavigator.test.js index 27bb1833d60..0cc1f4e3bd9 100644 --- a/app/components/Nav/Main/MainNavigator.test.js +++ b/app/components/Nav/Main/MainNavigator.test.js @@ -1,165 +1,203 @@ -/* eslint-disable react/prop-types */ -import React from 'react'; -import { View } from 'react-native'; -import { render } from '@testing-library/react-native'; -import { TabBarIconKey } from '../../../component-library/components/Navigation/TabBar/TabBar.types'; import Routes from '../../../constants/navigation/Routes'; -// Mock the MainNavigator component directly -jest.mock('./MainNavigator', () => { - const React = require('react'); - const { View } = require('react-native'); - const { - TabBarIconKey, - } = require('../../../component-library/components/Navigation/TabBar/TabBar.types'); - const { selectBrowserFullscreen } = require('../../../selectors/browser'); - const Routes = require('../../../constants/navigation/Routes').default; - - // Mock implementation that tests tab visibility based on browser fullscreen state - return function MockMainNavigator({ route }) { - const isBrowserFullscreen = selectBrowserFullscreen(); - - // Simulate hidding tab bar when browser is in fullscreen mode AND on browser route - if (isBrowserFullscreen && route?.name?.startsWith(Routes.BROWSER.HOME)) { - return null; - } - - // Build tabs array - const tabs = [ - React.createElement(View, { - key: 'wallet', - testID: `tab-bar-item-${TabBarIconKey.Wallet}`, - }), - React.createElement(View, { - key: 'trending', - testID: `tab-bar-item-${TabBarIconKey.Trending}`, - }), - React.createElement(View, { - key: 'trade', - testID: `tab-bar-item-${TabBarIconKey.Trade}`, - }), - ]; +describe('MainNavigator Route Constants', () => { + it('has home route defined', () => { + expect(Routes.WALLET.HOME).toBeDefined(); + }); - // Add Activity tab (always shown) - tabs.push( - React.createElement(View, { - key: 'activity', - testID: `tab-bar-item-${TabBarIconKey.Activity}`, - }), - ); - - // Add Rewards tab - tabs.push( - React.createElement(View, { - key: 'rewards', - testID: `tab-bar-item-${TabBarIconKey.Rewards}`, - }), - ); - - return React.createElement(View, { testID: 'main-navigator' }, tabs); - }; -}); + it('has browser routes defined', () => { + expect(Routes.BROWSER.VIEW).toBeDefined(); + expect(Routes.BROWSER.HOME).toBeDefined(); + }); -// Mock the rewards selector -jest.mock('../../../selectors/featureFlagController/rewards', () => ({ - selectRewardsSubscriptionId: jest.fn().mockReturnValue(null), -})); + it('has settings routes defined', () => { + expect(Routes.SETTINGS_VIEW).toBeDefined(); + expect(Routes.SETTINGS.NOTIFICATIONS).toBeDefined(); + expect(Routes.SETTINGS.REVEAL_PRIVATE_CREDENTIAL).toBeDefined(); + }); -// Mock the browser selector -jest.mock('../../../selectors/browser', () => ({ - selectBrowserFullscreen: jest.fn(), -})); + it('has transactions view route defined', () => { + expect(Routes.TRANSACTIONS_VIEW).toBeDefined(); + }); -import { selectBrowserFullscreen } from '../../../selectors/browser'; -import MainNavigator from './MainNavigator'; + it('has rewards view route defined', () => { + expect(Routes.REWARDS_VIEW).toBeDefined(); + }); -describe('MainNavigator', () => { - beforeEach(() => { - jest.clearAllMocks(); - selectBrowserFullscreen.mockReturnValue(false); + it('has trending view route defined', () => { + expect(Routes.TRENDING_VIEW).toBeDefined(); }); - it('shows Trending tab', () => { - const { getByTestId } = render(); + it('has ramp routes defined', () => { + expect(Routes.RAMP.BUY).toBeDefined(); + expect(Routes.RAMP.SELL).toBeDefined(); + expect(Routes.RAMP.SETTINGS).toBeDefined(); + expect(Routes.RAMP.TOKEN_SELECTION).toBeDefined(); + expect(Routes.RAMP.ORDER_DETAILS).toBeDefined(); + }); - expect(getByTestId('tab-bar-item-Trending')).toBeDefined(); - expect(getByTestId('tab-bar-item-Wallet')).toBeDefined(); - expect(getByTestId('tab-bar-item-Trade')).toBeDefined(); - expect(getByTestId('tab-bar-item-Rewards')).toBeDefined(); + it('has deposit routes defined', () => { + expect(Routes.DEPOSIT.ID).toBeDefined(); + expect(Routes.DEPOSIT.ORDER_DETAILS).toBeDefined(); }); - it('shows Rewards tab with core tabs', () => { - const { getByTestId } = render(); + it('has bridge routes defined', () => { + expect(Routes.BRIDGE.ROOT).toBeDefined(); + expect(Routes.BRIDGE.MODALS.ROOT).toBeDefined(); + expect(Routes.BRIDGE.BRIDGE_TRANSACTION_DETAILS).toBeDefined(); + }); - expect(getByTestId('tab-bar-item-Rewards')).toBeDefined(); - expect(getByTestId('tab-bar-item-Wallet')).toBeDefined(); - expect(getByTestId('tab-bar-item-Trending')).toBeDefined(); - expect(getByTestId('tab-bar-item-Trade')).toBeDefined(); + it('has earn routes defined', () => { + expect(Routes.EARN.ROOT).toBeDefined(); + expect(Routes.EARN.MODALS.ROOT).toBeDefined(); }); - it('should not show navbar when browser is in fullscreen mode', () => { - // Given browser is in fullscreen mode on browser route - selectBrowserFullscreen.mockReturnValue(true); + it('has notification routes defined', () => { + expect(Routes.NOTIFICATIONS.VIEW).toBeDefined(); + expect(Routes.NOTIFICATIONS.OPT_IN).toBeDefined(); + expect(Routes.NOTIFICATIONS.OPT_IN_STACK).toBeDefined(); + expect(Routes.NOTIFICATIONS.DETAILS).toBeDefined(); + }); - // When rendering MainNavigator on browser route - const { queryByTestId } = render( - , - ); + it('has QR tab switcher route defined', () => { + expect(Routes.QR_TAB_SWITCHER).toBeDefined(); + }); - // Then navbar tabs should not be visible - expect(queryByTestId('tab-bar-item-Wallet')).toBeNull(); - expect(queryByTestId('tab-bar-item-Browser')).toBeNull(); - expect(queryByTestId('tab-bar-item-Trade')).toBeNull(); - expect(queryByTestId('tab-bar-item-Rewards')).toBeNull(); + it('has wallet routes defined', () => { + expect(Routes.WALLET.TAB_STACK_FLOW).toBeDefined(); + expect(Routes.WALLET.TOKENS_FULL_VIEW).toBeDefined(); + expect(Routes.WALLET.NFTS_FULL_VIEW).toBeDefined(); }); - it('should return null when isBrowserFullscreen is true AND route starts with BrowserTabHome', () => { - // Given browser is in fullscreen mode - selectBrowserFullscreen.mockReturnValue(true); + it('has security trust route defined', () => { + expect(Routes.SECURITY_TRUST).toBeDefined(); + }); - // When rendering MainNavigator with exact BrowserTabHome route (matches Routes.BROWSER.HOME) - const rendered = render( - , - ); + it('has snaps routes defined', () => { + expect(Routes.SNAPS.SNAPS_SETTINGS_LIST).toBeDefined(); + expect(Routes.SNAPS.SNAP_SETTINGS).toBeDefined(); + }); - // Then component should return null (empty container validates return null behavior) - expect(rendered.toJSON()).toBe(null); + it('has explore search route defined', () => { + expect(Routes.EXPLORE_SEARCH).toBeDefined(); }); - it('should match snapshot when browser is not infullscreen mode on BrowserTabHome route', () => { - // Given browser is in fullscreen mode - selectBrowserFullscreen.mockReturnValue(false); + it('has sites full view route defined', () => { + expect(Routes.SITES_FULL_VIEW).toBeDefined(); + }); + + it('has card routes defined', () => { + expect(Routes.CARD.ROOT).toBeDefined(); + }); + + it('has feature flag override route defined', () => { + expect(Routes.FEATURE_FLAG_OVERRIDE).toBeDefined(); + }); + + it('has transaction details route defined', () => { + expect(Routes.TRANSACTION_DETAILS).toBeDefined(); + }); - // When rendering MainNavigator on BrowserTabHome subpath route - const component = render( - , - ); + it('has deprecated network details route defined', () => { + expect(Routes.DEPRECATED_NETWORK_DETAILS).toBeDefined(); + }); - // Then component should match fullscreen snapshot (returns null) - expect(component.toJSON()).toMatchSnapshot(); + it('has accounts menu view route defined', () => { + expect(Routes.ACCOUNTS_MENU_VIEW).toBeDefined(); }); - it('shows Activity tab in tab bar', () => { - const { getByTestId } = render(); + it('has wallet connect sessions route defined', () => { + expect(Routes.WALLET.WALLET_CONNECT_SESSIONS_VIEW).toBeDefined(); + }); - expect(getByTestId('tab-bar-item-Activity')).toBeOnTheScreen(); + it('has perps routes defined', () => { + expect(Routes.PERPS.ROOT).toBeDefined(); + expect(Routes.PERPS.MODALS.ROOT).toBeDefined(); + expect(Routes.PERPS.TUTORIAL).toBeDefined(); + expect(Routes.PERPS.POSITION_TRANSACTION).toBeDefined(); + expect(Routes.PERPS.ORDER_TRANSACTION).toBeDefined(); + expect(Routes.PERPS.FUNDING_TRANSACTION).toBeDefined(); }); - it('shows all core tabs when no feature flags are enabled', () => { - selectBrowserFullscreen.mockReturnValue(false); + it('has predict routes defined', () => { + expect(Routes.PREDICT.ROOT).toBeDefined(); + expect(Routes.PREDICT.MODALS.ROOT).toBeDefined(); + }); - const { getByTestId } = render(); + it('has market insights routes defined', () => { + expect(Routes.MARKET_INSIGHTS.VIEW).toBeDefined(); + }); +}); - expect(getByTestId('tab-bar-item-Wallet')).toBeOnTheScreen(); - expect(getByTestId('tab-bar-item-Trending')).toBeOnTheScreen(); - expect(getByTestId('tab-bar-item-Trade')).toBeOnTheScreen(); - expect(getByTestId('tab-bar-item-Activity')).toBeOnTheScreen(); - expect(getByTestId('tab-bar-item-Rewards')).toBeOnTheScreen(); +describe('MainNavigator Tab Options', () => { + it('defines wallet tab icon key', () => { + expect(Routes.WALLET_VIEW).toBeDefined(); }); - it('renders main-navigator container', () => { - const { getByTestId } = render(); + it('defines browser tab navigation', () => { + expect(Routes.BROWSER_VIEW).toBeDefined(); + }); +}); + +describe('Route Constants Validation', () => { + it('has all main navigation screens defined', () => { + const mainRoutes = [ + Routes.WALLET.HOME, + Routes.BROWSER.HOME, + Routes.TRANSACTIONS_VIEW, + Routes.REWARDS_VIEW, + Routes.TRENDING_VIEW, + Routes.SETTINGS_VIEW, + ]; + + mainRoutes.forEach((route) => { + expect(route).toBeDefined(); + expect(typeof route).toBe('string'); + }); + }); + + it('has all modal navigation routes defined', () => { + const modalRoutes = [ + Routes.MODAL.WALLET_ACTIONS, + Routes.MODAL.ROOT_MODAL_FLOW, + Routes.MODAL.REWARDS_BOTTOM_SHEET_MODAL, + ]; + + modalRoutes.forEach((route) => { + expect(route).toBeDefined(); + expect(typeof route).toBe('string'); + }); + }); +}); + +describe('Stack Navigator Routes', () => { + it('has wallet tab stack flow route defined', () => { + expect(Routes.WALLET.TAB_STACK_FLOW).toBeDefined(); + }); + + it('has add asset route defined', () => { + expect(Routes.WALLET.HOME).toBeDefined(); + }); + + it('has asset route constant', () => { + expect(typeof Routes.WALLET.HOME).toBe('string'); + }); +}); + +describe('Full View Routes', () => { + it('has tokens full view route defined', () => { + expect(Routes.WALLET.TOKENS_FULL_VIEW).toBeDefined(); + }); + + it('has NFTs full view route defined', () => { + expect(Routes.WALLET.NFTS_FULL_VIEW).toBeDefined(); + }); + + it('has DeFi full view route defined', () => { + expect(Routes.WALLET.DEFI_FULL_VIEW).toBeDefined(); + }); - expect(getByTestId('main-navigator')).toBeOnTheScreen(); + it('has cash tokens full view route defined', () => { + expect(Routes.WALLET.CASH_TOKENS_FULL_VIEW).toBeDefined(); }); }); diff --git a/app/components/Nav/Main/MainNavigator.test.tsx b/app/components/Nav/Main/MainNavigator.test.tsx index ffd6e33fc44..3aacfc68462 100644 --- a/app/components/Nav/Main/MainNavigator.test.tsx +++ b/app/components/Nav/Main/MainNavigator.test.tsx @@ -16,6 +16,43 @@ jest.mock('@react-navigation/stack', () => ({ }), })); +jest.mock('@react-navigation/bottom-tabs', () => ({ + createBottomTabNavigator: jest.fn().mockReturnValue({ + Navigator: 'TabNavigator', + Screen: 'TabScreen', + }), +})); + +const mockSelectPerpsEnabledFlag = jest.fn(); +const mockSelectPredictEnabledFlag = jest.fn(); +const mockSelectMarketInsightsEnabled = jest.fn(); +const mockSelectMarketInsightsPerpsEnabled = jest.fn(); + +jest.mock('../../UI/Perps', () => ({ + PerpsScreenStack: () => 'PerpsScreenStack', + PerpsModalStack: () => 'PerpsModalStack', + PerpsTutorialCarousel: () => 'PerpsTutorialCarousel', + selectPerpsEnabledFlag: (state: unknown) => mockSelectPerpsEnabledFlag(state), +})); + +jest.mock('../../UI/Predict', () => ({ + PredictScreenStack: () => 'PredictScreenStack', + PredictModalStack: () => 'PredictModalStack', + selectPredictEnabledFlag: (state: unknown) => + mockSelectPredictEnabledFlag(state), +})); + +jest.mock('../../UI/MarketInsights', () => ({ + MarketInsightsView: () => 'MarketInsightsView', + selectMarketInsightsEnabled: (state: unknown) => + mockSelectMarketInsightsEnabled(state), +})); + +jest.mock('../../../selectors/featureFlagController/marketInsights', () => ({ + selectMarketInsightsPerpsEnabled: (state: unknown) => + mockSelectMarketInsightsPerpsEnabled(state), +})); + describe('MainNavigator', () => { const originalEnv = process.env.METAMASK_ENVIRONMENT; @@ -150,6 +187,796 @@ describe('MainNavigator', () => { ); }); + describe('Screen Registration', () => { + const getScreenProps = ( + container: ReturnType, + ) => { + interface ScreenChild { + name: string; + component: { name: string }; + } + return container.root.children + .filter( + (child): child is ReactTestInstance => + typeof child === 'object' && + 'type' in child && + 'props' in child && + child.type?.toString() === 'Screen', + ) + .map((child) => ({ + name: child.props.name, + component: child.props.component, + })) as ScreenChild[]; + }; + + it('includes Home screen', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const homeScreen = screenProps?.find((screen) => screen?.name === 'Home'); + + expect(homeScreen).toBeDefined(); + }); + + it('includes TokensFullView screen', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const tokensScreen = screenProps?.find( + (screen) => screen?.name === Routes.WALLET.TOKENS_FULL_VIEW, + ); + + expect(tokensScreen).toBeDefined(); + }); + + it('includes DeFiFullView screen', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const defiScreen = screenProps?.find( + (screen) => screen?.name === Routes.WALLET.DEFI_FULL_VIEW, + ); + + expect(defiScreen).toBeDefined(); + }); + + it('includes CashTokensFullView screen', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const cashTokensScreen = screenProps?.find( + (screen) => screen?.name === Routes.WALLET.CASH_TOKENS_FULL_VIEW, + ); + + expect(cashTokensScreen).toBeDefined(); + }); + + it('includes Bridge routes', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const bridgeScreen = screenProps?.find( + (screen) => screen?.name === Routes.BRIDGE.ROOT, + ); + + expect(bridgeScreen).toBeDefined(); + }); + + it('includes Earn routes', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const earnScreen = screenProps?.find( + (screen) => screen?.name === Routes.EARN.ROOT, + ); + + expect(earnScreen).toBeDefined(); + }); + + it('includes Card routes', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const cardScreen = screenProps?.find( + (screen) => screen?.name === Routes.CARD.ROOT, + ); + + expect(cardScreen).toBeDefined(); + }); + + it('includes Ramp BUY route', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const rampBuyScreen = screenProps?.find( + (screen) => screen?.name === Routes.RAMP.BUY, + ); + + expect(rampBuyScreen).toBeDefined(); + }); + + it('includes Ramp SELL route', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const rampSellScreen = screenProps?.find( + (screen) => screen?.name === Routes.RAMP.SELL, + ); + + expect(rampSellScreen).toBeDefined(); + }); + + it('includes Deposit route', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const depositScreen = screenProps?.find( + (screen) => screen?.name === Routes.DEPOSIT.ID, + ); + + expect(depositScreen).toBeDefined(); + }); + + it('includes Settings view route', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const settingsScreen = screenProps?.find( + (screen) => screen?.name === Routes.SETTINGS_VIEW, + ); + + expect(settingsScreen).toBeDefined(); + }); + + it('includes QRTabSwitcher route', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const qrScreen = screenProps?.find( + (screen) => screen?.name === Routes.QR_TAB_SWITCHER, + ); + + expect(qrScreen).toBeDefined(); + }); + + it('includes Notifications view route', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const notificationsScreen = screenProps?.find( + (screen) => screen?.name === Routes.NOTIFICATIONS.VIEW, + ); + + expect(notificationsScreen).toBeDefined(); + }); + + it('includes Explore Search route', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const exploreScreen = screenProps?.find( + (screen) => screen?.name === Routes.EXPLORE_SEARCH, + ); + + expect(exploreScreen).toBeDefined(); + }); + }); + + describe('Conditional Screen Rendering', () => { + const getScreenProps = ( + container: ReturnType, + ) => { + interface ScreenChild { + name: string; + component: { name: string }; + } + return container.root.children + .filter( + (child): child is ReactTestInstance => + typeof child === 'object' && + 'type' in child && + 'props' in child && + child.type?.toString() === 'Screen', + ) + .map((child) => ({ + name: child.props.name, + component: child.props.component, + })) as ScreenChild[]; + }; + + beforeEach(() => { + mockSelectPerpsEnabledFlag.mockReturnValue(false); + mockSelectPredictEnabledFlag.mockReturnValue(false); + mockSelectMarketInsightsEnabled.mockReturnValue(false); + }); + + it('includes Perps routes when perps feature flag is enabled', () => { + mockSelectPerpsEnabledFlag.mockReturnValue(true); + + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const perpsRootScreen = screenProps?.find( + (screen) => screen?.name === Routes.PERPS.ROOT, + ); + + expect(perpsRootScreen).toBeDefined(); + }); + + it('excludes Perps routes when perps feature flag is disabled', () => { + mockSelectPerpsEnabledFlag.mockReturnValue(false); + + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const perpsRootScreen = screenProps?.find( + (screen) => screen?.name === Routes.PERPS.ROOT, + ); + + expect(perpsRootScreen).toBeUndefined(); + }); + + it('includes Perps tutorial route when perps is enabled', () => { + mockSelectPerpsEnabledFlag.mockReturnValue(true); + + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const perpsTutorialScreen = screenProps?.find( + (screen) => screen?.name === Routes.PERPS.TUTORIAL, + ); + + expect(perpsTutorialScreen).toBeDefined(); + }); + + it('includes Perps transaction routes when perps is enabled', () => { + mockSelectPerpsEnabledFlag.mockReturnValue(true); + + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const positionScreen = screenProps?.find( + (screen) => screen?.name === Routes.PERPS.POSITION_TRANSACTION, + ); + const orderScreen = screenProps?.find( + (screen) => screen?.name === Routes.PERPS.ORDER_TRANSACTION, + ); + const fundingScreen = screenProps?.find( + (screen) => screen?.name === Routes.PERPS.FUNDING_TRANSACTION, + ); + + expect(positionScreen).toBeDefined(); + expect(orderScreen).toBeDefined(); + expect(fundingScreen).toBeDefined(); + }); + + it('includes Predict routes when predict feature flag is enabled', () => { + mockSelectPredictEnabledFlag.mockReturnValue(true); + + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const predictRootScreen = screenProps?.find( + (screen) => screen?.name === Routes.PREDICT.ROOT, + ); + + expect(predictRootScreen).toBeDefined(); + }); + + it('excludes Predict routes when predict feature flag is disabled', () => { + mockSelectPredictEnabledFlag.mockReturnValue(false); + + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const predictRootScreen = screenProps?.find( + (screen) => screen?.name === Routes.PREDICT.ROOT, + ); + + expect(predictRootScreen).toBeUndefined(); + }); + + it('includes Market Insights view when feature flag is enabled', () => { + mockSelectMarketInsightsEnabled.mockReturnValue(true); + + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const marketInsightsScreen = screenProps?.find( + (screen) => screen?.name === Routes.MARKET_INSIGHTS.VIEW, + ); + + expect(marketInsightsScreen).toBeDefined(); + }); + + it('excludes Market Insights view when feature flag is disabled', () => { + mockSelectMarketInsightsEnabled.mockReturnValue(false); + + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const marketInsightsScreen = screenProps?.find( + (screen) => screen?.name === Routes.MARKET_INSIGHTS.VIEW, + ); + + expect(marketInsightsScreen).toBeUndefined(); + }); + + it('includes multiple conditional routes when all flags are enabled', () => { + mockSelectPerpsEnabledFlag.mockReturnValue(true); + mockSelectPredictEnabledFlag.mockReturnValue(true); + mockSelectMarketInsightsEnabled.mockReturnValue(true); + + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + + expect( + screenProps?.find((screen) => screen?.name === Routes.PERPS.ROOT), + ).toBeDefined(); + expect( + screenProps?.find((screen) => screen?.name === Routes.PREDICT.ROOT), + ).toBeDefined(); + expect( + screenProps?.find( + (screen) => screen?.name === Routes.MARKET_INSIGHTS.VIEW, + ), + ).toBeDefined(); + }); + + it('includes Market Insights when perps insights flag is enabled', () => { + mockSelectMarketInsightsEnabled.mockReturnValue(false); + mockSelectMarketInsightsPerpsEnabled.mockReturnValue(true); + + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const marketInsightsScreen = screenProps?.find( + (screen) => screen?.name === Routes.MARKET_INSIGHTS.VIEW, + ); + + expect(marketInsightsScreen).toBeDefined(); + }); + + it('excludes Market Insights when both insights flags are disabled', () => { + mockSelectMarketInsightsEnabled.mockReturnValue(false); + mockSelectMarketInsightsPerpsEnabled.mockReturnValue(false); + + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const marketInsightsScreen = screenProps?.find( + (screen) => screen?.name === Routes.MARKET_INSIGHTS.VIEW, + ); + + expect(marketInsightsScreen).toBeUndefined(); + }); + + it('includes Perps modal routes when perps is enabled', () => { + mockSelectPerpsEnabledFlag.mockReturnValue(true); + + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const perpsModalScreen = screenProps?.find( + (screen) => screen?.name === Routes.PERPS.MODALS.ROOT, + ); + + expect(perpsModalScreen).toBeDefined(); + }); + + it('includes Predict modal routes when predict is enabled', () => { + mockSelectPredictEnabledFlag.mockReturnValue(true); + + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const predictModalScreen = screenProps?.find( + (screen) => screen?.name === Routes.PREDICT.MODALS.ROOT, + ); + + expect(predictModalScreen).toBeDefined(); + }); + }); + + describe('Additional Screen Routes', () => { + const getScreenProps = ( + container: ReturnType, + ) => { + interface ScreenChild { + name: string; + component: { name: string }; + } + return container.root.children + .filter( + (child): child is ReactTestInstance => + typeof child === 'object' && + 'type' in child && + 'props' in child && + child.type?.toString() === 'Screen', + ) + .map((child) => ({ + name: child.props.name, + component: child.props.component, + })) as ScreenChild[]; + }; + + it('includes CollectiblesDetails screen', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find( + (s) => s?.name === 'CollectiblesDetails', + ); + + expect(screen).toBeDefined(); + }); + + it('includes DeprecatedNetworkDetails screen', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find( + (s) => s?.name === Routes.DEPRECATED_NETWORK_DETAILS, + ); + + expect(screen).toBeDefined(); + }); + + it('includes TrendingTokensFullView screen', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find( + (s) => s?.name === 'TrendingTokensFullView', + ); + + expect(screen).toBeDefined(); + }); + + it('includes RWATokensFullView screen', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find((s) => s?.name === 'RWATokensFullView'); + + expect(screen).toBeDefined(); + }); + + it('includes Webview screen', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find((s) => s?.name === 'Webview'); + + expect(screen).toBeDefined(); + }); + + it('includes Send screen', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find((s) => s?.name === 'Send'); + + expect(screen).toBeDefined(); + }); + + it('includes AddBookmarkView screen', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find((s) => s?.name === 'AddBookmarkView'); + + expect(screen).toBeDefined(); + }); + + it('includes OfflineModeView screen', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find((s) => s?.name === 'OfflineModeView'); + + expect(screen).toBeDefined(); + }); + + it('includes NftDetails screen', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find((s) => s?.name === 'NftDetails'); + + expect(screen).toBeDefined(); + }); + + it('includes NftDetailsFullImage screen', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find( + (s) => s?.name === 'NftDetailsFullImage', + ); + + expect(screen).toBeDefined(); + }); + + it('includes AddAsset screen', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find((s) => s?.name === 'AddAsset'); + + expect(screen).toBeDefined(); + }); + + it('includes ConfirmAddAsset screen', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find((s) => s?.name === 'ConfirmAddAsset'); + + expect(screen).toBeDefined(); + }); + + it('includes StakeScreens route', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find((s) => s?.name === 'StakeScreens'); + + expect(screen).toBeDefined(); + }); + + it('includes StakeModals route', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find((s) => s?.name === 'StakeModals'); + + expect(screen).toBeDefined(); + }); + + it('includes Bridge modal routes', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find( + (s) => s?.name === Routes.BRIDGE.MODALS.ROOT, + ); + + expect(screen).toBeDefined(); + }); + + it('includes Earn modal routes', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find( + (s) => s?.name === Routes.EARN.MODALS.ROOT, + ); + + expect(screen).toBeDefined(); + }); + + it('includes SetPasswordFlow screen', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find((s) => s?.name === 'SetPasswordFlow'); + + expect(screen).toBeDefined(); + }); + + it('includes GeneralSettings screen', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find((s) => s?.name === 'GeneralSettings'); + + expect(screen).toBeDefined(); + }); + + it('includes NotificationsOptInStack screen', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find( + (s) => s?.name === Routes.NOTIFICATIONS.OPT_IN_STACK, + ); + + expect(screen).toBeDefined(); + }); + + it('includes DeFiProtocolPositionDetails screen', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find( + (s) => s?.name === 'DeFiProtocolPositionDetails', + ); + + expect(screen).toBeDefined(); + }); + + it('includes Asset screen', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find((s) => s?.name === 'Asset'); + + expect(screen).toBeDefined(); + }); + + it('includes SitesFullView screen', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find( + (s) => s?.name === Routes.SITES_FULL_VIEW, + ); + + expect(screen).toBeDefined(); + }); + + it('includes Browser home screen in main navigator', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find((s) => s?.name === Routes.BROWSER.HOME); + + expect(screen).toBeDefined(); + }); + + it('includes Ramp processing info modal route', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find( + (s) => s?.name === Routes.RAMP.MODALS.PROCESSING_INFO, + ); + + expect(screen).toBeDefined(); + }); + + it('includes Card routes', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find((s) => s?.name === Routes.CARD.ROOT); + + expect(screen).toBeDefined(); + }); + + it('includes NFTs full view route', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find( + (s) => s?.name === Routes.WALLET.NFTS_FULL_VIEW, + ); + + expect(screen).toBeDefined(); + }); + + it('includes Token Selection route', () => { + const container = renderWithProvider(, { + state: initialRootState, + }); + + const screenProps = getScreenProps(container); + const screen = screenProps?.find( + (s) => s?.name === Routes.RAMP.TOKEN_SELECTION, + ); + + expect(screen).toBeDefined(); + }); + }); + it('includes TopTradersView screen when Social Leaderboard remote flag is enabled', () => { const stateWithSocialLeaderboard = { ...initialRootState, diff --git a/app/components/Nav/Main/__snapshots__/MainNavigator.test.js.snap b/app/components/Nav/Main/__snapshots__/MainNavigator.test.js.snap deleted file mode 100644 index 55d9945622c..00000000000 --- a/app/components/Nav/Main/__snapshots__/MainNavigator.test.js.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MainNavigator should match snapshot when browser is not infullscreen mode on BrowserTabHome route 1`] = ` - - - - - - - -`; diff --git a/app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap b/app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap index a08d1fcaf50..d451bd29188 100644 --- a/app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap +++ b/app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap @@ -3,13 +3,16 @@ exports[`MainNavigator Tab Bar Visibility hides tab bar when browser is active 1`] = ` + @@ -31,13 +35,10 @@ exports[`MainNavigator Tab Bar Visibility hides tab bar when browser is active 1 "backgroundColor": "transparent", }, "cardStyleInterpolator": [Function], + "presentation": "modal", } } /> - - - @@ -403,8 +384,8 @@ exports[`MainNavigator Tab Bar Visibility hides tab bar when browser is active 1 "animationEnabled": false, "cardStyle": { "backgroundColor": "transparent", - "cardStyleInterpolator": [Function], }, + "cardStyleInterpolator": [Function], "headerShown": false, } } @@ -415,13 +396,16 @@ exports[`MainNavigator Tab Bar Visibility hides tab bar when browser is active 1 exports[`MainNavigator Tab Bar Visibility shows tab bar when not in browser 1`] = ` + @@ -443,13 +428,10 @@ exports[`MainNavigator Tab Bar Visibility shows tab bar when not in browser 1`] "backgroundColor": "transparent", }, "cardStyleInterpolator": [Function], + "presentation": "modal", } } /> - - - @@ -815,8 +777,8 @@ exports[`MainNavigator Tab Bar Visibility shows tab bar when not in browser 1`] "animationEnabled": false, "cardStyle": { "backgroundColor": "transparent", - "cardStyleInterpolator": [Function], }, + "cardStyleInterpolator": [Function], "headerShown": false, } } @@ -827,13 +789,16 @@ exports[`MainNavigator Tab Bar Visibility shows tab bar when not in browser 1`] exports[`MainNavigator matches rendered snapshot 1`] = ` + @@ -855,13 +821,10 @@ exports[`MainNavigator matches rendered snapshot 1`] = ` "backgroundColor": "transparent", }, "cardStyleInterpolator": [Function], + "presentation": "modal", } } /> - - - @@ -1227,8 +1170,8 @@ exports[`MainNavigator matches rendered snapshot 1`] = ` "animationEnabled": false, "cardStyle": { "backgroundColor": "transparent", - "cardStyleInterpolator": [Function], }, + "cardStyleInterpolator": [Function], "headerShown": false, } } diff --git a/app/components/Nav/Main/__snapshots__/index.test.tsx.snap b/app/components/Nav/Main/__snapshots__/index.test.tsx.snap index dd79f589d91..ad1111c118b 100644 --- a/app/components/Nav/Main/__snapshots__/index.test.tsx.snap +++ b/app/components/Nav/Main/__snapshots__/index.test.tsx.snap @@ -13,9 +13,9 @@ exports[`Main should render correctly 1`] = ` } } > - + - + `; @@ -32,8 +32,8 @@ exports[`Main should render correctly with isConnectionRemoved true 1`] = ` } } > - + - + `; diff --git a/app/components/Nav/Main/index.js b/app/components/Nav/Main/index.js index 74bd05be919..7adc3b2814d 100644 --- a/app/components/Nav/Main/index.js +++ b/app/components/Nav/Main/index.js @@ -514,7 +514,6 @@ const ConnectedMain = connect(mapStateToProps, mapDispatchToProps)(Main); const MainFlow = () => ( ( ); diff --git a/app/components/Nav/NavigationProvider/NavigationProvider.test.tsx b/app/components/Nav/NavigationProvider/NavigationProvider.test.tsx index 39692394142..4c3269af8a2 100644 --- a/app/components/Nav/NavigationProvider/NavigationProvider.test.tsx +++ b/app/components/Nav/NavigationProvider/NavigationProvider.test.tsx @@ -5,9 +5,16 @@ import { useDispatch } from 'react-redux'; import { View, Text } from 'react-native'; import { onNavigationReady } from '../../../actions/navigation'; import NavigationService from '../../../core/NavigationService'; -import { NavigationContainerRef } from '@react-navigation/native'; +import { + NavigationContainerRef, + ParamListBase, +} from '@react-navigation/native'; import { endTrace, trace, TraceName } from '../../../util/trace'; +const navigationContainerThemeCapture: { + theme?: { colors?: { background?: string } }; +} = {}; + jest.mock('../../../util/trace', () => { const actual = jest.requireActual('../../../util/trace'); return { @@ -17,6 +24,13 @@ jest.mock('../../../util/trace', () => { }; }); +jest.mock('../../../util/theme', () => { + const { mockTheme } = jest.requireActual('../../../util/theme'); + return { + useTheme: jest.fn(() => mockTheme), + }; +}); + // Mock UIStartup to prevent second trace from being called (for testing purposes) jest.mock('../../../core/Performance/UIStartup', () => jest.fn()); @@ -28,20 +42,14 @@ jest.mock('react-redux', () => ({ useDispatch: jest.fn(), })); -jest.mock('../../../util/theme', () => { - const { mockTheme } = jest.requireActual('../../../util/theme'); - return { - useTheme: jest.fn(() => mockTheme), - }; -}); - describe('NavigationProvider', () => { const mockDispatch = jest.fn(); beforeEach(() => { jest.clearAllMocks(); + navigationContainerThemeCapture.theme = undefined; NavigationService.navigation = - undefined as unknown as NavigationContainerRef; + undefined as unknown as NavigationContainerRef; (useDispatch as jest.Mock).mockReturnValue(mockDispatch); }); diff --git a/app/components/Nav/NavigationProvider/NavigationProvider.tsx b/app/components/Nav/NavigationProvider/NavigationProvider.tsx index c67754c434e..c0c089724fd 100644 --- a/app/components/Nav/NavigationProvider/NavigationProvider.tsx +++ b/app/components/Nav/NavigationProvider/NavigationProvider.tsx @@ -2,10 +2,10 @@ import React, { useRef } from 'react'; import { NavigationContainer, NavigationContainerRef, + ParamListBase, Theme, } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; -import { useTheme } from '../../../util/theme'; import { onNavigationReady } from '../../../actions/navigation'; import { useDispatch } from 'react-redux'; import NavigationService from '../../../core/NavigationService'; @@ -26,7 +26,6 @@ const Stack = createStackNavigator(); const NavigationProvider: React.FC = ({ children, }) => { - const { colors } = useTheme(); const dispatch = useDispatch(); const hasInitialized = useRef(false); @@ -53,7 +52,7 @@ const NavigationProvider: React.FC = ({ /** * Sets the navigation ref on the NavigationService */ - const setNavigationRef = (ref: NavigationContainerRef) => { + const setNavigationRef = (ref: NavigationContainerRef) => { // This condition only happens on unmount. But that should never happen since this is meant to always be mounted. if (!ref) { return; @@ -63,8 +62,9 @@ const NavigationProvider: React.FC = ({ return ( diff --git a/app/components/UI/AccountNetworkIndicator/__snapshots__/AccountNetworkIndicator.test.tsx.snap b/app/components/UI/AccountNetworkIndicator/__snapshots__/AccountNetworkIndicator.test.tsx.snap index ed18f0e8f12..4cadd54bfb3 100644 --- a/app/components/UI/AccountNetworkIndicator/__snapshots__/AccountNetworkIndicator.test.tsx.snap +++ b/app/components/UI/AccountNetworkIndicator/__snapshots__/AccountNetworkIndicator.test.tsx.snap @@ -20,391 +20,414 @@ exports[`AccountNetworkIndicator should render correctly 1`] = ` } > - - - + + /> + + - + - AccountNetworkIndicator - + + AccountNetworkIndicator + + + - - - - - + - - + testID="avatargroup-avatar" + > + + - - - + testID="avatargroup-avatar" + > + + @@ -414,9 +437,9 @@ exports[`AccountNetworkIndicator should render correctly 1`] = ` - - - + + + `; diff --git a/app/components/UI/AccountRightButton/__snapshots__/index.test.tsx.snap b/app/components/UI/AccountRightButton/__snapshots__/index.test.tsx.snap index 8d8782be1db..0664ec0f626 100644 --- a/app/components/UI/AccountRightButton/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/AccountRightButton/__snapshots__/index.test.tsx.snap @@ -20,350 +20,373 @@ exports[`AccountRightButton should render account avatar when selectedAddress is } > - - - + + /> + + - + - AccountRightButton - + + AccountRightButton + + + - - - - - + - - - - + - - + > + + + + - - - + + + `; @@ -388,356 +411,379 @@ exports[`AccountRightButton should render correct network name for non-EVM netwo } > - - - + + /> + + - + - AccountRightButton - + + AccountRightButton + + + - - - - - + - - - - - + - - + > + + + + - - - + + + `; @@ -762,350 +808,373 @@ exports[`AccountRightButton should render correctly 1`] = ` } > - - - + + /> + + - + - AccountRightButton - + + AccountRightButton + + + - - - - - + - - - - + - - + > + + + + - - - + + + `; @@ -1130,350 +1199,373 @@ exports[`AccountRightButton should render correctly when a EVM network is select } > - - - + + /> + + - + - AccountRightButton - + + AccountRightButton + + + - - - - - + - - - - + - - + > + + + + - - - + + + `; @@ -1498,350 +1590,373 @@ exports[`AccountRightButton should render correctly when a non-EVM network is se } > - - - + + /> + + - + - AccountRightButton - + + AccountRightButton + + + - - - - - + - - - - + - - + > + + + + - - - + + + `; @@ -1866,356 +1981,379 @@ exports[`AccountRightButton should render network avatar when selectedAddress is } > - - - + + /> + + - + - AccountRightButton - + + AccountRightButton + + + - - - - - + - - - - - + - - + > + + + + - - - + + + `; @@ -2240,356 +2378,379 @@ exports[`AccountRightButton should render network avatar when selectedAddress is } > - - - + + /> + + - + - AccountRightButton - + + AccountRightButton + + + - - - - - + - - - - - + - - + > + + + + - - - + + + `; diff --git a/app/components/UI/BackupAlert/BackupAlert.test.tsx b/app/components/UI/BackupAlert/BackupAlert.test.tsx index d7ba6a1989c..6a8e4a533f4 100644 --- a/app/components/UI/BackupAlert/BackupAlert.test.tsx +++ b/app/components/UI/BackupAlert/BackupAlert.test.tsx @@ -15,7 +15,7 @@ const initialState = { }; const mockNavigation = { navigate: jest.fn(), - dangerouslyGetState: jest.fn(() => ({ routes: [{ name: 'WalletView' }] })), + getState: jest.fn(() => ({ routes: [{ name: 'WalletView' }] })), }; jest.mock('react-redux', () => ({ diff --git a/app/components/UI/BackupAlert/BackupAlert.tsx b/app/components/UI/BackupAlert/BackupAlert.tsx index 725b84ce70e..28e7f01cda9 100644 --- a/app/components/UI/BackupAlert/BackupAlert.tsx +++ b/app/components/UI/BackupAlert/BackupAlert.tsx @@ -21,7 +21,7 @@ import Icon, { import Text, { TextVariant, } from '../../../component-library/components/Texts/Text'; -import { useMetrics } from '../../../components/hooks/useMetrics'; +import { useAnalytics } from '../../../components/hooks/useAnalytics/useAnalytics'; import Routes from '../../../constants/navigation/Routes'; import { selectSeedlessOnboardingLoginFlow } from '../../../selectors/seedlessOnboardingController'; import { RootState } from '../../../reducers'; @@ -44,7 +44,7 @@ const BLOCKED_LIST = [ const BackupAlert = ({ navigation, onDismiss }: BackupAlertI) => { const { styles } = useStyles(styleSheet, {}); - const { trackEvent, createEventBuilder } = useMetrics(); + const { trackEvent, createEventBuilder } = useAnalytics(); const [inBrowserView, setInBrowserView] = useState(false); const [inBlockedView, setInBlockedView] = useState(false); const [isVisible, setIsVisible] = useState(true); @@ -56,7 +56,7 @@ const BackupAlert = ({ navigation, onDismiss }: BackupAlertI) => { const dispatch = useDispatch(); const currentRouteName = findRouteNameFromNavigatorState( - navigation.dangerouslyGetState().routes, + navigation.getState().routes, ); const isSeedlessOnboardingLoginFlow = useSelector( diff --git a/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx b/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx index 2e2f9ce81c4..883eb5a9214 100644 --- a/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx +++ b/app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx @@ -5,7 +5,9 @@ import { backgroundState } from '../../../util/test/initial-root-state'; import BalanceEmptyState from './BalanceEmptyState'; import { BalanceEmptyStateProps } from './BalanceEmptyState.types'; import { RampsButtonClickData } from '../Ramp/hooks/useRampsButtonClickData'; -import { useMetrics } from '../../hooks/useMetrics'; +import { useAnalytics } from '../../hooks/useAnalytics/useAnalytics'; +import { createMockUseAnalyticsHook } from '../../../util/test/analyticsMock'; +import { MetaMetricsEvents } from '../../../core/Analytics'; // Mock useRampNavigation hook const mockGoToBuy = jest.fn(); @@ -39,12 +41,7 @@ const mockEventBuilder = { build: jest.fn().mockReturnValue({ event: 'built' }), }; -jest.mock('../../hooks/useMetrics', () => ({ - useMetrics: jest.fn(), - MetaMetricsEvents: { - RAMPS_BUTTON_CLICKED: 'ramps_button_clicked', - }, -})); +jest.mock('../../hooks/useAnalytics/useAnalytics'); jest.mock('../../../util/networks', () => ({ getDecimalChainId: jest.fn(() => 1), @@ -54,10 +51,12 @@ describe('BalanceEmptyState', () => { beforeEach(() => { jest.clearAllMocks(); mockCreateEventBuilder.mockReturnValue(mockEventBuilder); - (useMetrics as jest.Mock).mockReturnValue({ - trackEvent: mockTrackEvent, - createEventBuilder: mockCreateEventBuilder, - }); + jest.mocked(useAnalytics).mockReturnValue( + createMockUseAnalyticsHook({ + trackEvent: mockTrackEvent, + createEventBuilder: mockCreateEventBuilder, + }), + ); mockUseRampsUnifiedV1Enabled.mockReturnValue(false); }); @@ -103,7 +102,9 @@ describe('BalanceEmptyState', () => { fireEvent.press(actionButton); - expect(mockCreateEventBuilder).toHaveBeenCalledWith('ramps_button_clicked'); + expect(mockCreateEventBuilder).toHaveBeenCalledWith( + MetaMetricsEvents.RAMPS_BUTTON_CLICKED, + ); expect(mockEventBuilder.addProperties).toHaveBeenCalledWith( expect.objectContaining({ button_text: 'Add funds', @@ -126,7 +127,9 @@ describe('BalanceEmptyState', () => { fireEvent.press(actionButton); - expect(mockCreateEventBuilder).toHaveBeenCalledWith('ramps_button_clicked'); + expect(mockCreateEventBuilder).toHaveBeenCalledWith( + MetaMetricsEvents.RAMPS_BUTTON_CLICKED, + ); expect(mockEventBuilder.addProperties).toHaveBeenCalledWith( expect.objectContaining({ button_text: 'Add funds', diff --git a/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx b/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx index ee5b1beb84b..20e19f07a68 100644 --- a/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx +++ b/app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx @@ -16,7 +16,8 @@ import { } from '@metamask/design-system-react-native'; import { useTailwind } from '@metamask/design-system-twrnc-preset'; import { strings } from '../../../../locales/i18n'; -import { MetaMetricsEvents, useMetrics } from '../../hooks/useMetrics'; +import { MetaMetricsEvents } from '../../../core/Analytics'; +import { useAnalytics } from '../../hooks/useAnalytics/useAnalytics'; import { getDecimalChainId } from '../../../util/networks'; import { selectChainId } from '../../../selectors/networkController'; import { trace, TraceName } from '../../../util/trace'; @@ -38,7 +39,7 @@ const BalanceEmptyState: React.FC = ({ }) => { const tw = useTailwind(); const chainId = useSelector(selectChainId); - const { trackEvent, createEventBuilder } = useMetrics(); + const { trackEvent, createEventBuilder } = useAnalytics(); const rampGeodetectedRegion = useSelector(getDetectedGeolocation); const { goToBuy } = useRampNavigation(); const buttonClickData = useRampsButtonClickData(); diff --git a/app/components/UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal.test.js b/app/components/UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal.test.js index 2bd910d1add..7fd9b3fd7e8 100644 --- a/app/components/UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal.test.js +++ b/app/components/UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal.test.js @@ -52,7 +52,7 @@ jest.mock('@react-navigation/native', () => { setOptions: jest.fn(), goBack: jest.fn(), reset: jest.fn(), - dangerouslyGetParent: () => ({ + getParent: () => ({ pop: jest.fn(), }), isFocused: jest.fn(() => true), diff --git a/app/components/UI/Bridge/components/ApprovalText/__snapshots__/ApprovalTooltip.test.tsx.snap b/app/components/UI/Bridge/components/ApprovalText/__snapshots__/ApprovalTooltip.test.tsx.snap index 5acdeb91310..ba605859c22 100644 --- a/app/components/UI/Bridge/components/ApprovalText/__snapshots__/ApprovalTooltip.test.tsx.snap +++ b/app/components/UI/Bridge/components/ApprovalText/__snapshots__/ApprovalTooltip.test.tsx.snap @@ -20,340 +20,363 @@ exports[`ApprovalTooltip renders correctly with given props 1`] = ` } > - - - + + /> + + - + - Bridge - + + Bridge + + + - - - - - + - - - - + > + + + - - - + + + `; diff --git a/app/components/UI/Bridge/components/QuoteDetailsCard/__snapshots__/QuoteDetailsCard.test.tsx.snap b/app/components/UI/Bridge/components/QuoteDetailsCard/__snapshots__/QuoteDetailsCard.test.tsx.snap index f846b5cff78..9a7f76753ac 100644 --- a/app/components/UI/Bridge/components/QuoteDetailsCard/__snapshots__/QuoteDetailsCard.test.tsx.snap +++ b/app/components/UI/Bridge/components/QuoteDetailsCard/__snapshots__/QuoteDetailsCard.test.tsx.snap @@ -20,306 +20,318 @@ exports[`QuoteDetailsCard renders initial state 1`] = ` } > - - - + + /> + + - + - Bridge - + + Bridge + + + - - - - - + - @@ -343,347 +348,219 @@ exports[`QuoteDetailsCard renders initial state 1`] = ` style={ [ { - "alignItems": "center", "display": "flex", - "flexDirection": "row", - "gap": 4, }, - undefined, + { + "backgroundColor": "#ffffff", + "gap": 12, + "overflow": "hidden", + "paddingBottom": 16, + "paddingHorizontal": 16, + "paddingTop": 12, + }, ] } > - - Rate - - 0:30 + Rate + + + 0:30 + + - - - - - - - - - 1 ETH = 24.4 USDC - - + } + } + width={16} + /> + - - - - - - Network fee + 1 ETH = 24.4 USDC - - - + }, + undefined, + ] + } + /> - + - - - - 0.01 - - - - - - - @@ -692,75 +569,75 @@ exports[`QuoteDetailsCard renders initial state 1`] = ` { "alignItems": "center", "flexDirection": "row", + "gap": 8, } } > - - Slippage - - - + Network fee + + - + > + + + - - @@ -769,87 +646,57 @@ exports[`QuoteDetailsCard renders initial state 1`] = ` { "alignItems": "center", "flexDirection": "row", + "gap": 8, } } > - - 0.5% + 0.01 - - + - - @@ -858,75 +705,75 @@ exports[`QuoteDetailsCard renders initial state 1`] = ` { "alignItems": "center", "flexDirection": "row", + "gap": 8, } } > - - Price impact - - - + Slippage + + - + > + + + - - @@ -935,26 +782,29 @@ exports[`QuoteDetailsCard renders initial state 1`] = ` { "alignItems": "center", "flexDirection": "row", + "gap": 8, } } > - - - 0% + 0.5% - - + + + - - - + + + + Price impact + + + + + + + + - Recipient - + + + + + + 0% + + + + + + - - Select recipient + Recipient - + + - + testID="recipient-selector-button" + > + + Select recipient + + + + @@ -1089,9 +1112,9 @@ exports[`QuoteDetailsCard renders initial state 1`] = ` - - - + + + `; diff --git a/app/components/UI/Bridge/routes.tsx b/app/components/UI/Bridge/routes.tsx index 7cdcaac9460..2f623f6180f 100644 --- a/app/components/UI/Bridge/routes.tsx +++ b/app/components/UI/Bridge/routes.tsx @@ -21,10 +21,12 @@ const clearStackNavigatorOptions = { animationEnabled: false, }; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ScreenComponent = React.ComponentType; + const Stack = createStackNavigator(); export const BridgeScreenStack = () => ( ( const ModalStack = createStackNavigator(); export const BridgeModalStack = () => ( ( /> ; + navigation: AppNavigationProp; evmTxMeta?: TransactionMeta; multiChainTx?: Transaction; bridgeTxHistoryItem?: BridgeHistoryItem; diff --git a/app/components/UI/Card/Views/CardAuthentication/CardAuthentication.test.tsx b/app/components/UI/Card/Views/CardAuthentication/CardAuthentication.test.tsx index efaf4469b44..021953cc570 100644 --- a/app/components/UI/Card/Views/CardAuthentication/CardAuthentication.test.tsx +++ b/app/components/UI/Card/Views/CardAuthentication/CardAuthentication.test.tsx @@ -28,12 +28,6 @@ jest.mock('@react-navigation/native', () => ({ }), })); -jest.mock('@react-navigation/compat', () => ({ - NavigationActions: { - navigate: jest.fn((params) => ({ type: 'NAVIGATE', ...params })), - }, -})); - const mockLogin = jest.fn(); const mockClearError = jest.fn(); const mockSendOtpLogin = jest.fn(); diff --git a/app/components/UI/Card/Views/CardAuthentication/__snapshots__/CardAuthentication.test.tsx.snap b/app/components/UI/Card/Views/CardAuthentication/__snapshots__/CardAuthentication.test.tsx.snap index a2f83f32ef6..5d4edc1cc3c 100644 --- a/app/components/UI/Card/Views/CardAuthentication/__snapshots__/CardAuthentication.test.tsx.snap +++ b/app/components/UI/Card/Views/CardAuthentication/__snapshots__/CardAuthentication.test.tsx.snap @@ -20,886 +20,738 @@ exports[`CardAuthentication Component Login Step - Component Rendering matches l } > - - - + + /> + + - + - CardAuthentication - + + CardAuthentication + + + - - - - - + - - - - - - - - Log in to your card account - - - + } + contentInset={ + { + "bottom": 0, + } + } + enableAutomaticScroll={true} + enableOnAndroid={true} + enableResetScrollToCoords={true} + extraHeight={75} + extraScrollHeight={20} + getScrollResponder={[Function]} + handleOnScroll={[Function]} + keyboardDismissMode="interactive" + keyboardOpeningTime={250} + keyboardShouldPersistTaps="handled" + keyboardSpace={0} + onScroll={[Function]} + resetKeyboardSpace={[Function]} + scrollEventThrottle={1} + scrollForExtraHeightOnAndroid={[Function]} + scrollIntoView={[Function]} + scrollToEnd={[Function]} + scrollToFocusedInput={[Function]} + scrollToPosition={[Function]} + showsVerticalScrollIndicator={false} + update={[Function]} + viewIsInsideTabBar={false} + > + - + Log in to your card account + + + + + - - - - International - - - - - + + International + + + + - - ๐Ÿ‡บ๐Ÿ‡ธ - - - United States - - - - - - + ๐Ÿ‡บ๐Ÿ‡ธ + + + United States + + + + + - Email - - - - - - - - - - Password - - + Email + + - - - - - - - + - - - - + + Password + - + + + - Log in - + + + + - + + + + - - I don't have an account - - + + Log in + + + + + I don't have an account + + + - - - + + + - - - + + + `; diff --git a/app/components/UI/Card/Views/CardHome/CardHome.test.tsx b/app/components/UI/Card/Views/CardHome/CardHome.test.tsx index 38e390b7ddb..13621023c78 100644 --- a/app/components/UI/Card/Views/CardHome/CardHome.test.tsx +++ b/app/components/UI/Card/Views/CardHome/CardHome.test.tsx @@ -5354,6 +5354,8 @@ describe('CardHome Component', () => { const mockUserDetailsForProvisioning = { id: 'user-123', + firstName: 'John', + lastName: 'Doe', addressLine1: '123 Main St', addressLine2: 'Apt 4B', city: 'New York', @@ -5441,7 +5443,7 @@ describe('CardHome Component', () => { // Verify userAddress uses physical address fields in provisioning format expect(options.userAddress).toEqual({ - name: 'Card Holder', // Uses default since userDetails doesn't have firstName/lastName + name: 'John Doe', // Derived from KYC userDetails firstName/lastName addressOne: '123 Main St', addressTwo: 'Apt 4B', locality: 'New York', @@ -5536,11 +5538,17 @@ describe('CardHome Component', () => { expect(typeof options.onError).toBe('function'); }); - it('uses holderName from cardDetails for provisioning', async () => { - // Given: card with holder name from card status API + it('uses holderName from KYC userDetails for provisioning', async () => { + // Given: card with different holder name, but KYC has specific names const cardWithHolderName = { ...mockCardDetailsWithHolder, - holderName: 'Jane Smith', + holderName: 'Card API Name', + }; + + const userDetailsWithName = { + ...mockUserDetailsForProvisioning, + firstName: 'Jane', + lastName: 'Smith', }; setupMockSelectors({ isAuthenticated: true, userLocation: 'us' }); @@ -5552,7 +5560,7 @@ describe('CardHome Component', () => { kycStatus: { verificationState: 'VERIFIED', userId: 'user-123', - userDetails: mockUserDetailsForProvisioning, + userDetails: userDetailsWithName, }, }); @@ -5563,7 +5571,7 @@ describe('CardHome Component', () => { expect(mockUsePushProvisioning).toHaveBeenCalled(); }); - // Then: holderName should come from cardDetails + // Then: holderName should come from KYC userDetails, not cardDetails const options = getLastCallOptions(); const cardDetails = options.cardDetails as { holderName: string }; diff --git a/app/components/UI/Card/Views/CardHome/CardHome.tsx b/app/components/UI/Card/Views/CardHome/CardHome.tsx index a4f022a2ef0..4d8f7735993 100644 --- a/app/components/UI/Card/Views/CardHome/CardHome.tsx +++ b/app/components/UI/Card/Views/CardHome/CardHome.tsx @@ -107,7 +107,7 @@ import { getWalletName, type ProvisioningError, } from '../../pushProvisioning'; -import { AddToWalletButton } from '@expensify/react-native-wallet'; +import { AddToWalletButton } from '../../pushProvisioning/components/AddToWalletButton'; import { CardScreenshotDeterrent } from '../../components/CardScreenshotDeterrent'; import { createPasswordBottomSheetNavigationDetails } from '../../components/PasswordBottomSheet'; import { createViewPinBottomSheetNavigationDetails } from '../../components/ViewPinBottomSheet'; @@ -273,12 +273,12 @@ const CardHome = () => { cardDetails ? { id: cardDetails.id, - holderName: cardDetails.holderName, + holderName: cardholderName, panLast4: cardDetails.panLast4, status: cardDetails.status, } : null, - [cardDetails], + [cardDetails, cardholderName], ); const { @@ -288,6 +288,7 @@ const CardHome = () => { } = usePushProvisioning({ cardDetails: cardDetailsForProvisioning, userAddress: userAddressForProvisioning, + accountCreatedAt: kycStatus?.userDetails?.createdAt, onSuccess: () => { toastRef?.current?.showToast({ variant: ToastVariants.Icon, diff --git a/app/components/UI/Card/Views/CardHome/__snapshots__/CardHome.test.tsx.snap b/app/components/UI/Card/Views/CardHome/__snapshots__/CardHome.test.tsx.snap index 5bdc87f63ed..5ab24a8e885 100644 --- a/app/components/UI/Card/Views/CardHome/__snapshots__/CardHome.test.tsx.snap +++ b/app/components/UI/Card/Views/CardHome/__snapshots__/CardHome.test.tsx.snap @@ -20,379 +20,389 @@ exports[`CardHome Component renders correctly and matches snapshot 1`] = ` } > - - - + + /> + + - + - CardHome - + + CardHome + + + - - - - - + - - - } - showsVerticalScrollIndicator={false} + - - - - card.card_home.title - - + } + showsVerticalScrollIndicator={false} + style={ + { + "backgroundColor": "#ffffff", + "flexBasis": "0%", + "flexGrow": 1, + "flexShrink": 1, } - > + } + testID="card-view-title" + > + + + + card.card_home.title + - - - - - - - - - - + + + + + + - - + - - + - - - - + - - - - + - - + propList={ + [ + "fill", + ] + } + /> + - - - - + + + + + + + + + + + + + + + - - + + + + $1,000.00 + + + + + + + card.card_home.available_balance + + + + - - $1,000.00 - - - - + > + Add funds + + + + + card.card_home.change_asset + + - - card.card_home.available_balance - - - - Add funds - - - + - - card.card_home.change_asset - + + + card.card_home.manage_card_options.manage_spending_limit + + + card.card_home.manage_card_options.manage_spending_limit_description_full + + + + + + + - + - - - card.card_home.manage_card_options.manage_spending_limit + Manage card - card.card_home.manage_card_options.manage_spending_limit_description_full + See detailed transactions, freeze your card, etc. - - - - - Manage card - - - See detailed transactions, freeze your card, etc. - - - - - - - - - - - - - - - card.card_home.manage_card_options.travel_title - - - card.card_home.manage_card_options.travel_description - - - - - + + card.card_home.manage_card_options.travel_title + + + card.card_home.manage_card_options.travel_description + + + + + + - - - - - + + + + + - - - + + + `; @@ -1484,379 +1507,389 @@ exports[`CardHome Component renders correctly with privacy mode enabled 1`] = ` } > - - - + + /> + + - + - CardHome - + + CardHome + + + - - - - - + - - - } - showsVerticalScrollIndicator={false} + - - - - card.card_home.title - - + } + showsVerticalScrollIndicator={false} + style={ + { + "backgroundColor": "#ffffff", + "flexBasis": "0%", + "flexGrow": 1, + "flexShrink": 1, } - > + } + testID="card-view-title" + > + + + + card.card_home.title + - + - - - - - + + + + + + + - - - - + - - + - - + - - - - + - - - + - + - - + - + + + + + + - - - - + name="clip1_4219_2177" + > + + + + + + - - @@ -2318,90 +2352,86 @@ exports[`CardHome Component renders correctly with privacy mode enabled 1`] = ` style={ [ { - "alignItems": "center", "display": "flex", - "flexDirection": "row", - "gap": 8, + "flexDirection": "column", }, undefined, ] } > - - โ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข - - - - + > + โ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข + + + + + + + card.card_home.available_balance + - - card.card_home.available_balance - - - + + - + Add funds + + + - Add funds - + + card.card_home.change_asset + + + + + + + - - card.card_home.change_asset - + + + card.card_home.manage_card_options.manage_spending_limit + + + card.card_home.manage_card_options.manage_spending_limit_description_full + + + + + + + - + - - - card.card_home.manage_card_options.manage_spending_limit + Manage card - card.card_home.manage_card_options.manage_spending_limit_description_full + See detailed transactions, freeze your card, etc. - - - - - Manage card - - - See detailed transactions, freeze your card, etc. - - - - - + card.card_home.manage_card_options.travel_title + + + card.card_home.manage_card_options.travel_description + + + - - - - - - - - - - card.card_home.manage_card_options.travel_title - - - card.card_home.manage_card_options.travel_description - - - - - + width={20} + /> + - - - - - + + + + + - - - + + + `; diff --git a/app/components/UI/Card/Views/SpendingLimit/components/SpendingLimitOptionsSheet.test.tsx b/app/components/UI/Card/Views/SpendingLimit/components/SpendingLimitOptionsSheet.test.tsx new file mode 100644 index 00000000000..7f8e5e1358d --- /dev/null +++ b/app/components/UI/Card/Views/SpendingLimit/components/SpendingLimitOptionsSheet.test.tsx @@ -0,0 +1,215 @@ +import React from 'react'; +import { fireEvent } from '@testing-library/react-native'; +import SpendingLimitOptionsSheet, { + createSpendingLimitOptionsNavigationDetails, +} from './SpendingLimitOptionsSheet'; +import renderWithProvider from '../../../../../../util/test/renderWithProvider'; +import Routes from '../../../../../../constants/navigation/Routes'; + +const mockNavigate = jest.fn(); +const mockUseParams = jest.fn(); +const mockCloseSheet = jest.fn(); + +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: () => ({ + navigate: mockNavigate, + }), +})); + +jest.mock('../../../../../../util/navigation/navUtils', () => ({ + useParams: () => mockUseParams(), + createNavigationDetails: jest.fn((stackId: string, screenName: string) => + jest.fn((params?: unknown) => [stackId, { screen: screenName, params }]), + ), +})); + +jest.mock( + '../../../../../../component-library/components/BottomSheets/BottomSheet', + () => { + // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-shadow + const React = require('react'); + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { View } = require('react-native'); + return React.forwardRef( + ( + { children }: { children: React.ReactNode }, + ref: React.Ref<{ onCloseBottomSheet: (cb?: () => void) => void }>, + ) => { + React.useImperativeHandle(ref, () => ({ + onCloseBottomSheet: (cb?: () => void) => { + mockCloseSheet(cb); + cb?.(); + }, + })); + return {children}; + }, + ); + }, +); + +jest.mock( + '../../../../../../component-library/components/BottomSheets/BottomSheetHeader', + () => { + // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-shadow + const React = require('react'); + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { View, Pressable } = require('react-native'); + return ({ + children, + onClose, + }: { + children: React.ReactNode; + onClose: () => void; + }) => ( + + + {children} + + ); + }, +); + +describe('SpendingLimitOptionsSheet', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockUseParams.mockReturnValue({ + currentLimitType: 'full', + currentCustomLimit: '', + callerRoute: Routes.CARD.SPENDING_LIMIT, + callerParams: { foo: 'bar' }, + }); + }); + + it('exports navigation details factory for card modals stack', () => { + const details = createSpendingLimitOptionsNavigationDetails({ + currentLimitType: 'restricted', + currentCustomLimit: '100', + callerRoute: Routes.CARD.SPENDING_LIMIT, + callerParams: { a: 1 }, + }); + expect(details).toEqual([ + Routes.CARD.MODALS.ID, + { + screen: Routes.CARD.MODALS.SPENDING_LIMIT_OPTIONS, + params: { + currentLimitType: 'restricted', + currentCustomLimit: '100', + callerRoute: Routes.CARD.SPENDING_LIMIT, + callerParams: { a: 1 }, + }, + }, + ]); + }); + + it('navigates back to caller with selected full limit on confirm', () => { + const { getByText } = renderWithProvider( + , + {}, + true, + false, + ); + + fireEvent.press(getByText('Confirm')); + + expect(mockCloseSheet).toHaveBeenCalledWith(expect.any(Function)); + expect(mockNavigate).toHaveBeenCalledWith(Routes.CARD.SPENDING_LIMIT, { + foo: 'bar', + returnedLimitType: 'full', + returnedCustomLimit: '', + }); + }); + + it('navigates with restricted limit and sanitized custom amount', () => { + mockUseParams.mockReturnValue({ + currentLimitType: 'restricted', + currentCustomLimit: '50', + callerRoute: Routes.CARD.SPENDING_LIMIT, + callerParams: undefined, + }); + + const { getByTestId, getByText } = renderWithProvider( + , + {}, + true, + false, + ); + + fireEvent.changeText( + getByTestId('limit-option-restricted-input'), + '123.45', + ); + fireEvent.press(getByText('Confirm')); + + expect(mockNavigate).toHaveBeenCalledWith(Routes.CARD.SPENDING_LIMIT, { + returnedLimitType: 'restricted', + returnedCustomLimit: '123.45', + }); + }); + + it('closes sheet when header close is pressed', () => { + const { getByTestId } = renderWithProvider( + , + {}, + true, + false, + ); + + fireEvent.press(getByTestId('sheet-header-close')); + + expect(mockCloseSheet).toHaveBeenCalledWith(undefined); + expect(mockNavigate).not.toHaveBeenCalled(); + }); + + it('switches from restricted back to full and confirms', () => { + mockUseParams.mockReturnValue({ + currentLimitType: 'restricted', + currentCustomLimit: '100', + callerRoute: Routes.CARD.SPENDING_LIMIT, + callerParams: { baz: 'qux' }, + }); + + const { getByTestId, getByText } = renderWithProvider( + , + {}, + true, + false, + ); + + fireEvent.press(getByTestId('limit-option-full')); + fireEvent.press(getByText('Confirm')); + + expect(mockNavigate).toHaveBeenCalledWith(Routes.CARD.SPENDING_LIMIT, { + baz: 'qux', + returnedLimitType: 'full', + returnedCustomLimit: '100', + }); + }); + + it('sanitizes non-numeric characters from custom limit input', () => { + mockUseParams.mockReturnValue({ + currentLimitType: 'restricted', + currentCustomLimit: '', + callerRoute: Routes.CARD.SPENDING_LIMIT, + callerParams: undefined, + }); + + const { getByTestId, getByText } = renderWithProvider( + , + {}, + true, + false, + ); + + fireEvent.changeText( + getByTestId('limit-option-restricted-input'), + 'abc$50.00xyz', + ); + fireEvent.press(getByText('Confirm')); + + expect(mockNavigate).toHaveBeenCalledWith(Routes.CARD.SPENDING_LIMIT, { + returnedLimitType: 'restricted', + returnedCustomLimit: expect.stringMatching(/^[\d.]*$/), + }); + }); +}); diff --git a/app/components/UI/Card/Views/SpendingLimit/components/SpendingLimitOptionsSheet.tsx b/app/components/UI/Card/Views/SpendingLimit/components/SpendingLimitOptionsSheet.tsx index 948b583e5e1..748b229a39c 100644 --- a/app/components/UI/Card/Views/SpendingLimit/components/SpendingLimitOptionsSheet.tsx +++ b/app/components/UI/Card/Views/SpendingLimit/components/SpendingLimitOptionsSheet.tsx @@ -21,6 +21,7 @@ import { strings } from '../../../../../../../locales/i18n'; import { LimitType } from '../../../hooks/useSpendingLimit'; import { sanitizeCustomLimit } from '../../../util/sanitizeCustomLimit'; import LimitOptionItem from './LimitOptionItem'; +import { AppNavigationProp } from '../../../../../../core/NavigationService/types'; interface SpendingLimitOptionsNavigationDetails { currentLimitType: LimitType; @@ -37,7 +38,7 @@ export const createSpendingLimitOptionsNavigationDetails = const SpendingLimitOptionsSheet: React.FC = () => { const sheetRef = useRef(null); - const navigation = useNavigation(); + const navigation = useNavigation(); const { currentLimitType, currentCustomLimit, callerRoute, callerParams } = useParams(); @@ -54,14 +55,11 @@ const SpendingLimitOptionsSheet: React.FC = () => { const handleConfirm = useCallback(() => { sheetRef.current?.onCloseBottomSheet(() => { - navigation.navigate( - callerRoute as never, - { - ...callerParams, - returnedLimitType: limitType, - returnedCustomLimit: customLimit, - } as never, - ); + navigation.navigate(callerRoute, { + ...callerParams, + returnedLimitType: limitType, + returnedCustomLimit: customLimit, + }); }); }, [navigation, callerRoute, callerParams, limitType, customLimit]); diff --git a/app/components/UI/Card/components/AddFundsBottomSheet/__snapshots__/AddFundsBottomSheet.test.tsx.snap b/app/components/UI/Card/components/AddFundsBottomSheet/__snapshots__/AddFundsBottomSheet.test.tsx.snap index 625e3d7884e..351e0fb08e9 100644 --- a/app/components/UI/Card/components/AddFundsBottomSheet/__snapshots__/AddFundsBottomSheet.test.tsx.snap +++ b/app/components/UI/Card/components/AddFundsBottomSheet/__snapshots__/AddFundsBottomSheet.test.tsx.snap @@ -20,436 +20,436 @@ exports[`AddFundsBottomSheet renders with both options enabled and matches snaps } > - - - + + /> + + - + - AddFundsBottomSheet - + + AddFundsBottomSheet + + + - - - - - + - - - - - - - + /> + + - + - + + + - Select method - - - - - - + + + + - + > + + + - - - - - + + - - + + + + + + - - - - - + Fund with cash + + - Fund with cash - - - Low-cost card or bank transfer - + > + Low-cost card or bank transfer + + - - - - - + + - + + + + + + - - - - - - + Fund with crypto + + - Fund with crypto - - - Swap tokens into USDC on Linea - + > + Swap tokens into USDC on Linea + + - - + + - - + + @@ -807,9 +830,9 @@ exports[`AddFundsBottomSheet renders with both options enabled and matches snaps - - - + + + `; @@ -834,436 +857,436 @@ exports[`AddFundsBottomSheet renders with no options when both are disabled and } > - - - + + /> + + - + - AddFundsBottomSheet - + + AddFundsBottomSheet + + + - - - - - + - - - - - - - + /> + + - + - + + + - Select method - - - - - - + + + + - + > + + + + + + + + + - - - - - - @@ -1399,9 +1445,9 @@ exports[`AddFundsBottomSheet renders with no options when both are disabled and - - - + + + `; @@ -1426,436 +1472,436 @@ exports[`AddFundsBottomSheet renders with only deposit option when swaps are not } > - - - + + /> + + - + - AddFundsBottomSheet - + + AddFundsBottomSheet + + + - - - - - + + - - - - - - - + /> + + - + - + + + - Select method - - - - - - + + + + - + > + + + - - - - - + + - + + + + + + - - - - - - + Fund with cash + + - Fund with cash - - - Low-cost card or bank transfer - + > + Low-cost card or bank transfer + + - - + + + - - - + + @@ -2102,9 +2171,9 @@ exports[`AddFundsBottomSheet renders with only deposit option when swaps are not - - - + + + `; @@ -2129,436 +2198,436 @@ exports[`AddFundsBottomSheet renders with only swap option when deposit is disab } > - - - + + /> + + - + - AddFundsBottomSheet - + + AddFundsBottomSheet + + + - - - - - + - - - - - - - + /> + + - + - + + + - Select method - - - - - - + + + + - + > + + + - - - - - - + + + - + + + + + + - - - - - - + Fund with crypto + + - Fund with crypto - - - Swap tokens into USDC on Linea - + > + Swap tokens into USDC on Linea + + - - + + - - + + @@ -2805,9 +2897,9 @@ exports[`AddFundsBottomSheet renders with only swap option when deposit is disab - - - + + + `; diff --git a/app/components/UI/Card/components/CardAssetItem/__snapshots__/CardAssetItem.test.tsx.snap b/app/components/UI/Card/components/CardAssetItem/__snapshots__/CardAssetItem.test.tsx.snap index 823b85d8cc3..4519f44ed1a 100644 --- a/app/components/UI/Card/components/CardAssetItem/__snapshots__/CardAssetItem.test.tsx.snap +++ b/app/components/UI/Card/components/CardAssetItem/__snapshots__/CardAssetItem.test.tsx.snap @@ -20,413 +20,436 @@ exports[`CardAssetItem Component handles test network correctly 1`] = ` } > - - - + + /> + + - + - CardAssetItem - + + CardAssetItem + + + - - - - - + - - - - + + + - + > + + @@ -436,9 +459,9 @@ exports[`CardAssetItem Component handles test network correctly 1`] = ` - - - + + + `; @@ -463,369 +486,392 @@ exports[`CardAssetItem Component renders non-native token and matches snapshot 1 } > - - - + + /> + + - - CardAssetItem - - - - + /> + + + CardAssetItem + + + + + + - - - - - + - - - - + + + > + + - - + /> + - - - + + + `; @@ -850,364 +896,387 @@ exports[`CardAssetItem Component renders with required props and matches snapsho } > - - - + + /> + + - + - CardAssetItem - + + CardAssetItem + + + - - - - - + - - - - + testID="badge-wrapper-badge" + > + + + + + - - - + + + `; diff --git a/app/components/UI/Card/components/CardButton/__snapshots__/CardButton.test.tsx.snap b/app/components/UI/Card/components/CardButton/__snapshots__/CardButton.test.tsx.snap index c1f18ca9a80..277046d377e 100644 --- a/app/components/UI/Card/components/CardButton/__snapshots__/CardButton.test.tsx.snap +++ b/app/components/UI/Card/components/CardButton/__snapshots__/CardButton.test.tsx.snap @@ -20,431 +20,454 @@ exports[`CardButton Component renders with badge (not yet viewed) and matches sn } > - - - + + /> + + - + - CardButton - + + CardButton + + + - - - - - + - + - + testID="card-button" + > + + - - + testID="card-button-badge" + > + + @@ -453,9 +476,9 @@ exports[`CardButton Component renders with badge (not yet viewed) and matches sn - - - + + + `; @@ -480,412 +503,435 @@ exports[`CardButton Component renders without badge when already viewed 1`] = ` } > - - - + + /> + + - + - CardButton - + + CardButton + + + - - - - - + - + - + testID="card-button" + > + + + - - - - + + + `; diff --git a/app/components/UI/Card/components/DaimoPayModal/DaimoPayModal.test.tsx b/app/components/UI/Card/components/DaimoPayModal/DaimoPayModal.test.tsx index 318c7025cbf..d91c903a11e 100644 --- a/app/components/UI/Card/components/DaimoPayModal/DaimoPayModal.test.tsx +++ b/app/components/UI/Card/components/DaimoPayModal/DaimoPayModal.test.tsx @@ -1,71 +1,34 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +/* eslint-disable @typescript-eslint/no-var-requires */ import React from 'react'; -import { render, waitFor, act, fireEvent } from '@testing-library/react-native'; -import { Linking } from 'react-native'; +import { render, fireEvent, waitFor, act } from '@testing-library/react-native'; +import { Provider } from 'react-redux'; +import { configureStore } from '@reduxjs/toolkit'; import DaimoPayModal from './DaimoPayModal'; import { DaimoPayModalSelectors } from './DaimoPayModal.testIds'; -import { MetaMetricsEvents } from '../../../../../core/Analytics'; -import { CardScreens } from '../../util/metrics'; -import { cardQueries } from '../../queries'; +import DaimoPayService from '../../services/DaimoPayService'; +import Routes from '../../../../../constants/navigation/Routes'; -const mockNavigate = jest.fn(); const mockGoBack = jest.fn(); +const mockNavigate = jest.fn(); const mockDispatch = jest.fn(); -const mockDangerouslyGetParent = jest.fn< - { dispatch: jest.Mock } | undefined, - [] ->(); -const mockTrackEvent = jest.fn(); -const mockBuild = jest.fn(); -const mockAddProperties = jest.fn(() => ({ build: mockBuild })); -const mockCreateEventBuilder = jest.fn(() => ({ - addProperties: mockAddProperties, +const mockGetParent = jest.fn<{ dispatch: jest.Mock } | null, []>(() => ({ + dispatch: mockDispatch, })); - jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), useNavigation: () => ({ - navigate: mockNavigate, goBack: mockGoBack, - dangerouslyGetParent: mockDangerouslyGetParent, - }), - CommonActions: { - reset: jest.fn((config) => ({ type: 'RESET', ...config })), - }, -})); - -const mockInvalidateQueries = jest.fn(); -jest.mock('@tanstack/react-query', () => ({ - useQueryClient: jest.fn(), -})); - -jest.mock('../../../../../util/navigation/navUtils', () => ({ - useParams: () => ({ - payId: 'test-pay-id-123', - fromUpgrade: false, - orderId: 'test-order-id-123', - }), -})); - -const mockGetDaimoEnvironment = jest.fn(() => 'demo'); -jest.mock('../../util/getDaimoEnvironment', () => ({ - getDaimoEnvironment: (isDaimoDemo: boolean) => - mockGetDaimoEnvironment(isDaimoDemo), -})); - -jest.mock('../../sdk', () => ({ - useCardSDK: () => ({ - sdk: { - createOrder: jest.fn(), - getOrderStatus: jest.fn(), - }, + navigate: mockNavigate, + getParent: mockGetParent, }), })); -const mockReduxDispatch = jest.fn(); -jest.mock('react-redux', () => ({ - useSelector: jest.fn(() => []), - useDispatch: () => mockReduxDispatch, +const mockTrackEvent = jest.fn(); +const mockCreateEventBuilder = jest.fn(() => ({ + addProperties: jest.fn().mockReturnThis(), + build: jest.fn().mockReturnValue({}), })); - jest.mock('../../../../hooks/useAnalytics/useAnalytics', () => ({ useAnalytics: () => ({ trackEvent: mockTrackEvent, @@ -73,32 +36,21 @@ jest.mock('../../../../hooks/useAnalytics/useAnalytics', () => ({ }), })); -const mockPollPaymentStatus = jest.fn(); +jest.mock('../../../../../util/navigation/navUtils', () => ({ + useParams: () => ({ + payId: 'test-pay-id', + fromUpgrade: false, + orderId: 'test-order-id', + }), +})); jest.mock('../../services/DaimoPayService', () => ({ __esModule: true, default: { - buildWebViewUrl: jest.fn( - (payId: string) => - `https://miniapp.daimo.com/metamask/embed?payId=${payId}&paymentOptions=Metamask`, - ), - parseWebViewEvent: jest.fn((data: string) => { - try { - const parsed = JSON.parse(data); - if (parsed?.source === 'daimo-pay') { - return parsed; - } - return null; - } catch { - return null; - } - }), - shouldLoadInWebView: jest.fn((url: string) => - url.includes('miniapp.daimo.com'), - ), - isProduction: jest.fn(() => false), - pollPaymentStatus: (...args: unknown[]) => mockPollPaymentStatus(...args), + buildWebViewUrl: jest.fn(() => 'https://pay.daimo.com/test'), + pollPaymentStatus: jest.fn(), isValidMessageOrigin: jest.fn(() => true), + shouldLoadInWebView: jest.fn((url: string) => url.includes('daimo.com')), }, })); @@ -138,15 +90,16 @@ jest.mock('../../../../../util/browserScripts', () => ({ })); jest.mock('../../../../../core/BackgroundBridge/BackgroundBridge', () => - jest.fn().mockImplementation(() => ({ - onMessage: jest.fn(), - onDisconnect: jest.fn(), + jest.fn(() => ({ sendNotificationEip1193: jest.fn(), + onDisconnect: jest.fn(), + onMessage: jest.fn(), + url: 'https://pay.daimo.com', })), ); jest.mock('../../../../../core/RPCMethods/RPCMethodMiddleware', () => ({ - getRpcMethodMiddleware: jest.fn(), + getRpcMethodMiddleware: jest.fn(() => ({})), })); jest.mock('../../../../../core/Engine', () => ({ @@ -161,421 +114,307 @@ jest.mock('../../../../../core/Permissions', () => ({ getPermittedEvmAddressesByHostname: jest.fn(() => []), })); -jest.mock('../../../../../selectors/snaps/permissionController', () => ({ - selectPermissionControllerState: jest.fn(() => ({})), +jest.mock('../../sdk', () => ({ + useCardSDK: () => ({ + sdk: null, + }), })); -jest.mock('../../../../../core/redux/slices/card', () => ({ - selectIsDaimoDemo: jest.fn(), +jest.mock('@tanstack/react-query', () => ({ + useQueryClient: () => ({ + invalidateQueries: jest.fn(), + }), })); -jest.mock('../../../../../util/Logger', () => ({ - error: jest.fn(), - log: jest.fn(), +jest.mock('../../util/getDaimoEnvironment', () => ({ + getDaimoEnvironment: jest.fn(() => 'demo'), })); -jest.mock('../../../../../core/AppConstants', () => ({ - NOTIFICATION_NAMES: { - accountsChanged: 'metamask_accountsChanged', - }, - BUNDLE_IDS: { - ANDROID: 'io.metamask', - IOS: 'io.metamask.MetaMask', +const mockStore = configureStore({ + reducer: { + engine: () => ({ + backgroundState: { + PermissionController: {}, + }, + }), + card: () => ({ + isDaimoDemo: false, + }), }, - MM_UNIVERSAL_LINK_HOST: 'metamask.app.link', -})); - -jest.mock('../../../../../constants/dapp', () => ({ - MAX_MESSAGE_LENGTH: 1000000, -})); - -jest.mock('@metamask/design-system-react-native', () => { - // eslint-disable-next-line @typescript-eslint/no-shadow - const React = jest.requireActual('react'); - const { Text: RNText, TouchableOpacity } = jest.requireActual('react-native'); - - return { - Text: ({ - children, - ...props - }: React.PropsWithChildren>) => - React.createElement(RNText, props, children), - Button: ({ - children, - onPress, - testID, - ...props - }: React.PropsWithChildren<{ - onPress?: () => void; - testID?: string; - }>) => - React.createElement( - TouchableOpacity, - { onPress, testID, ...props }, - children, - ), - TextVariant: { - BodyMd: 'BodyMd', - }, - FontWeight: { - Regular: 'Regular', - }, - ButtonVariant: { - Primary: 'Primary', - Secondary: 'Secondary', - }, - ButtonSize: { - Md: 'Md', - }, - }; }); -// Mock WebView -let mockOnMessage: ((event: { nativeEvent: { data: string } }) => void) | null = - null; -let mockOnError: (() => void) | null = null; -let mockOnShouldStartLoadWithRequest: - | ((request: { url: string }) => boolean) - | null = null; - -jest.mock('@metamask/react-native-webview', () => { - // eslint-disable-next-line @typescript-eslint/no-shadow - const React = jest.requireActual('react'); - const { View } = jest.requireActual('react-native'); - - return { - WebView: React.forwardRef( - ( - props: { - onMessage?: (event: { nativeEvent: { data: string } }) => void; - onError?: () => void; - onShouldStartLoadWithRequest?: (request: { url: string }) => boolean; - testID?: string; - source?: { uri: string }; - }, - _ref: React.Ref, - ) => { - mockOnMessage = props.onMessage || null; - mockOnError = props.onError || null; - mockOnShouldStartLoadWithRequest = - props.onShouldStartLoadWithRequest || null; - - return React.createElement(View, { - testID: props.testID, - 'data-source': props.source?.uri, - }); - }, - ), - }; -}); +const renderWithProvider = (component: React.ReactElement) => + render({component}); describe('DaimoPayModal', () => { beforeEach(() => { jest.clearAllMocks(); - jest.useFakeTimers(); - - mockInvalidateQueries.mockResolvedValue(undefined); - ( - jest.requireMock('@tanstack/react-query') as { - useQueryClient: jest.Mock; - } - ).useQueryClient.mockReturnValue({ - invalidateQueries: mockInvalidateQueries, - }); - - mockOnMessage = null; - mockOnError = null; - mockOnShouldStartLoadWithRequest = null; - mockGetDaimoEnvironment.mockReturnValue('demo'); - jest.spyOn(Linking, 'openURL').mockResolvedValue(undefined); - mockDangerouslyGetParent.mockReturnValue({ - dispatch: mockDispatch, - }); }); - afterEach(() => { - jest.useRealTimers(); + it('renders WebView when no error', async () => { + const { getByTestId } = renderWithProvider(); + + await waitFor(() => { + expect(getByTestId(DaimoPayModalSelectors.CONTAINER)).toBeTruthy(); + expect(getByTestId(DaimoPayModalSelectors.WEBVIEW)).toBeTruthy(); + }); }); - describe('Render', () => { - it('renders container and WebView', async () => { - const { getByTestId } = render(); + it('builds correct webview URL from payId', () => { + renderWithProvider(); - await waitFor(() => { - expect(getByTestId(DaimoPayModalSelectors.CONTAINER)).toBeTruthy(); - expect(getByTestId(DaimoPayModalSelectors.WEBVIEW)).toBeTruthy(); - }); - }); + expect(DaimoPayService.buildWebViewUrl).toHaveBeenCalledWith('test-pay-id'); + }); - it('displays error message when WebView fails to load', async () => { - const { getByTestId } = render(); + describe('error handling', () => { + it('displays error view when error state is set', async () => { + const { getByTestId } = renderWithProvider(); - await waitFor(() => { - expect(mockOnError).not.toBeNull(); - }); + const webView = getByTestId(DaimoPayModalSelectors.WEBVIEW); await act(async () => { - if (mockOnError) { - mockOnError(); - } + webView.props.onError(); }); await waitFor(() => { expect(getByTestId(DaimoPayModalSelectors.ERROR_TEXT)).toBeTruthy(); + expect(getByTestId(DaimoPayModalSelectors.CLOSE_BUTTON)).toBeTruthy(); + expect(getByTestId(DaimoPayModalSelectors.RETRY_BUTTON)).toBeTruthy(); }); }); - it('displays close and retry buttons when error occurs', async () => { - const { getByTestId } = render(); + it('allows retry after error', async () => { + const { getByTestId } = renderWithProvider(); - await waitFor(() => { - expect(mockOnError).not.toBeNull(); - }); + const webView = getByTestId(DaimoPayModalSelectors.WEBVIEW); await act(async () => { - if (mockOnError) { - mockOnError(); - } + webView.props.onError(); }); await waitFor(() => { - expect(getByTestId(DaimoPayModalSelectors.CLOSE_BUTTON)).toBeTruthy(); expect(getByTestId(DaimoPayModalSelectors.RETRY_BUTTON)).toBeTruthy(); }); - }); - it('closes modal when close button is pressed in error state', async () => { - const { getByTestId } = render(); + fireEvent.press(getByTestId(DaimoPayModalSelectors.RETRY_BUTTON)); await waitFor(() => { - expect(mockOnError).not.toBeNull(); + expect(getByTestId(DaimoPayModalSelectors.WEBVIEW)).toBeTruthy(); }); + }); + + it('closes modal when close button pressed in error state', async () => { + const { getByTestId } = renderWithProvider(); + + const webView = getByTestId(DaimoPayModalSelectors.WEBVIEW); await act(async () => { - if (mockOnError) { - mockOnError(); - } + webView.props.onError(); }); await waitFor(() => { expect(getByTestId(DaimoPayModalSelectors.CLOSE_BUTTON)).toBeTruthy(); }); - await act(async () => { - fireEvent.press(getByTestId(DaimoPayModalSelectors.CLOSE_BUTTON)); - }); + fireEvent.press(getByTestId(DaimoPayModalSelectors.CLOSE_BUTTON)); expect(mockGoBack).toHaveBeenCalled(); }); + }); - it('retries loading WebView when retry button is pressed', async () => { - const { getByTestId, queryByTestId } = render(); + describe('message handling', () => { + it('handles daimo-pay modalClosed event', async () => { + const { getByTestId } = renderWithProvider(); - await waitFor(() => { - expect(mockOnError).not.toBeNull(); - }); + const webView = getByTestId(DaimoPayModalSelectors.WEBVIEW); await act(async () => { - if (mockOnError) { - mockOnError(); - } + webView.props.onMessage({ + nativeEvent: { + data: JSON.stringify({ + source: 'daimo-pay', + type: 'modalClosed', + }), + }, + }); }); - await waitFor(() => { - expect(getByTestId(DaimoPayModalSelectors.RETRY_BUTTON)).toBeTruthy(); - }); + expect(mockGoBack).toHaveBeenCalled(); + }); + + it('handles daimo-pay modalOpened event', async () => { + const { getByTestId } = renderWithProvider(); + + const webView = getByTestId(DaimoPayModalSelectors.WEBVIEW); await act(async () => { - fireEvent.press(getByTestId(DaimoPayModalSelectors.RETRY_BUTTON)); + webView.props.onMessage({ + nativeEvent: { + data: JSON.stringify({ + source: 'daimo-pay', + type: 'modalOpened', + }), + }, + }); }); - // After retry, error should be cleared and WebView should be shown - await waitFor(() => { - expect(queryByTestId(DaimoPayModalSelectors.ERROR_TEXT)).toBeNull(); - expect(getByTestId(DaimoPayModalSelectors.WEBVIEW)).toBeTruthy(); - }); + expect(mockTrackEvent).toHaveBeenCalled(); }); - }); - describe('Interactions', () => { - it('opens external URLs via Linking', async () => { - render(); + it('handles daimo-pay paymentStarted event', async () => { + const { getByTestId } = renderWithProvider(); - await waitFor(() => { - expect(mockOnShouldStartLoadWithRequest).not.toBeNull(); - }); + const webView = getByTestId(DaimoPayModalSelectors.WEBVIEW); - if (mockOnShouldStartLoadWithRequest) { - const result = mockOnShouldStartLoadWithRequest({ - url: 'https://metamask.io/download', + await act(async () => { + webView.props.onMessage({ + nativeEvent: { + data: JSON.stringify({ + source: 'daimo-pay', + type: 'paymentStarted', + }), + }, }); + }); - expect(result).toBe(false); - expect(Linking.openURL).toHaveBeenCalledWith( - 'https://metamask.io/download', - ); - } + expect(mockTrackEvent).toHaveBeenCalled(); }); - it('allows Daimo URLs to load in WebView', async () => { - render(); + it('ignores messages exceeding MAX_MESSAGE_LENGTH', async () => { + const { getByTestId } = renderWithProvider(); - await waitFor(() => { - expect(mockOnShouldStartLoadWithRequest).not.toBeNull(); - }); + const webView = getByTestId(DaimoPayModalSelectors.WEBVIEW); + const longMessage = 'a'.repeat(100001); - if (mockOnShouldStartLoadWithRequest) { - const result = mockOnShouldStartLoadWithRequest({ - url: 'https://miniapp.daimo.com/metamask/embed?payId=123', + await act(async () => { + webView.props.onMessage({ + nativeEvent: { + data: longMessage, + }, }); + }); - expect(result).toBe(true); - expect(Linking.openURL).not.toHaveBeenCalled(); - } + expect(mockGoBack).not.toHaveBeenCalled(); }); - }); - describe('WebView Events', () => { - it('handles modalClosed event by navigating back', async () => { - render(); + it('ignores invalid JSON messages', async () => { + const { getByTestId } = renderWithProvider(); - await waitFor(() => { - expect(mockOnMessage).not.toBeNull(); - }); + const webView = getByTestId(DaimoPayModalSelectors.WEBVIEW); await act(async () => { - if (mockOnMessage) { - mockOnMessage({ - nativeEvent: { - data: JSON.stringify({ - source: 'daimo-pay', - version: 1, - type: 'modalClosed', - payload: {}, - }), - }, - }); - } + webView.props.onMessage({ + nativeEvent: { + data: 'invalid json', + }, + }); }); - expect(mockGoBack).toHaveBeenCalled(); + expect(mockGoBack).not.toHaveBeenCalled(); }); + }); - it('navigates immediately on paymentCompleted in demo mode', async () => { - mockGetDaimoEnvironment.mockReturnValue('demo'); + describe('URL loading', () => { + it('allows Daimo URLs to load in webview', async () => { + const { getByTestId } = renderWithProvider(); - render(); + const webView = getByTestId(DaimoPayModalSelectors.WEBVIEW); - await waitFor(() => { - expect(mockOnMessage).not.toBeNull(); + const result = webView.props.onShouldStartLoadWithRequest({ + url: 'https://pay.daimo.com/checkout', }); - await act(async () => { - if (mockOnMessage) { - mockOnMessage({ - nativeEvent: { - data: JSON.stringify({ - source: 'daimo-pay', - version: 1, - type: 'paymentCompleted', - payload: { - txHash: '0x123', - chainId: 59144, - }, - }), - }, - }); - } - }); - - // In demo mode, paymentCompleted should trigger navigation immediately - expect(mockDispatch).toHaveBeenCalled(); + expect(result).toBe(true); }); - it('clears card-details cache on payment success', async () => { - mockGetDaimoEnvironment.mockReturnValue('demo'); + it('opens external URLs via Linking', async () => { + const { Linking } = require('react-native'); + jest.spyOn(Linking, 'openURL').mockResolvedValue(undefined); - render(); + const { getByTestId } = renderWithProvider(); - await waitFor(() => { - expect(mockOnMessage).not.toBeNull(); + const webView = getByTestId(DaimoPayModalSelectors.WEBVIEW); + + const result = webView.props.onShouldStartLoadWithRequest({ + url: 'https://external-site.com/page', }); + expect(result).toBe(false); + expect(Linking.openURL).toHaveBeenCalledWith( + 'https://external-site.com/page', + ); + }); + }); + + describe('payment completion', () => { + it('handles paymentCompleted event in demo mode and navigates to success', async () => { + const { getByTestId } = renderWithProvider(); + + const webView = getByTestId(DaimoPayModalSelectors.WEBVIEW); + await act(async () => { - if (mockOnMessage) { - mockOnMessage({ - nativeEvent: { - data: JSON.stringify({ - source: 'daimo-pay', - version: 1, - type: 'paymentCompleted', - payload: { - txHash: '0x123', - chainId: 59144, - }, - }), - }, - }); - } - }); - - expect(mockInvalidateQueries).toHaveBeenCalledWith({ - queryKey: cardQueries.dashboard.keys.cardDetails(), + webView.props.onMessage({ + nativeEvent: { + data: JSON.stringify({ + source: 'daimo-pay', + type: 'paymentCompleted', + payload: { + txHash: '0x123abc', + chainId: 1, + }, + }), + }, + }); }); + + expect(mockTrackEvent).toHaveBeenCalled(); + expect(mockGetParent).toHaveBeenCalled(); + expect(mockDispatch).toHaveBeenCalled(); }); - it('does not navigate on paymentCompleted in production mode - lets polling handle navigation', async () => { - mockGetDaimoEnvironment.mockReturnValue('production'); + it('handles paymentBounced event and shows error', async () => { + const { getByTestId } = renderWithProvider(); - render(); + const webView = getByTestId(DaimoPayModalSelectors.WEBVIEW); + + await act(async () => { + webView.props.onMessage({ + nativeEvent: { + data: JSON.stringify({ + source: 'daimo-pay', + type: 'paymentBounced', + payload: { + errorMessage: 'Payment failed', + }, + }), + }, + }); + }); await waitFor(() => { - expect(mockOnMessage).not.toBeNull(); + expect(getByTestId(DaimoPayModalSelectors.ERROR_TEXT)).toBeTruthy(); }); - await act(async () => { - if (mockOnMessage) { - mockOnMessage({ - nativeEvent: { - data: JSON.stringify({ - source: 'daimo-pay', - version: 1, - type: 'paymentCompleted', - payload: { - txHash: '0x123', - chainId: 59144, - }, - }), - }, - }); - } - }); - - // In production mode, paymentCompleted should NOT trigger navigation - polling handles it - expect(mockDispatch).not.toHaveBeenCalled(); - expect(mockNavigate).not.toHaveBeenCalled(); + expect(mockTrackEvent).toHaveBeenCalled(); }); - it('displays error when paymentBounced event received', async () => { - const { getByTestId } = render(); + it('handles paymentBounced event with error field', async () => { + const { getByTestId } = renderWithProvider(); - await waitFor(() => { - expect(mockOnMessage).not.toBeNull(); - }); + const webView = getByTestId(DaimoPayModalSelectors.WEBVIEW); await act(async () => { - if (mockOnMessage) { - mockOnMessage({ - nativeEvent: { - data: JSON.stringify({ - source: 'daimo-pay', - version: 1, - type: 'paymentBounced', - payload: {}, - }), - }, - }); - } + webView.props.onMessage({ + nativeEvent: { + data: JSON.stringify({ + source: 'daimo-pay', + type: 'paymentBounced', + payload: { + error: 'Transaction reverted', + }, + }), + }, + }); }); await waitFor(() => { @@ -583,345 +422,210 @@ describe('DaimoPayModal', () => { }); }); - it('ignores non-Daimo events', async () => { - render(); + it('handles paymentBounced event with reason field', async () => { + const { getByTestId } = renderWithProvider(); - await waitFor(() => { - expect(mockOnMessage).not.toBeNull(); - }); + const webView = getByTestId(DaimoPayModalSelectors.WEBVIEW); await act(async () => { - if (mockOnMessage) { - mockOnMessage({ - nativeEvent: { - data: JSON.stringify({ - source: 'other-source', - type: 'someEvent', - }), - }, - }); - } + webView.props.onMessage({ + nativeEvent: { + data: JSON.stringify({ + source: 'daimo-pay', + type: 'paymentBounced', + payload: { + reason: 'Insufficient funds', + }, + }), + }, + }); }); - expect(mockGoBack).not.toHaveBeenCalled(); - expect(mockNavigate).not.toHaveBeenCalled(); - expect(mockDispatch).not.toHaveBeenCalled(); + await waitFor(() => { + expect(getByTestId(DaimoPayModalSelectors.ERROR_TEXT)).toBeTruthy(); + }); }); }); - describe('Polling', () => { - it('navigates to success when polling returns completed status', async () => { - mockGetDaimoEnvironment.mockReturnValue('production'); - mockPollPaymentStatus.mockResolvedValue({ - status: 'completed', - transactionHash: '0xabc123', - chainId: 1, - }); + describe('webview lifecycle', () => { + it('initializes background bridge on load end', async () => { + const BackgroundBridge = require('../../../../../core/BackgroundBridge/BackgroundBridge'); - render(); + const { getByTestId } = renderWithProvider(); - await waitFor(() => { - expect(mockOnMessage).not.toBeNull(); - }); + const webView = getByTestId(DaimoPayModalSelectors.WEBVIEW); - // Trigger paymentStarted to start polling - await act(async () => { - if (mockOnMessage) { - mockOnMessage({ - nativeEvent: { - data: JSON.stringify({ - source: 'daimo-pay', - version: 1, - type: 'paymentStarted', - payload: {}, - }), - }, - }); - } - }); - - // Advance timers to trigger first poll (5 seconds) await act(async () => { - jest.advanceTimersByTime(5000); + webView.props.onLoadEnd(); }); await waitFor(() => { - expect(mockPollPaymentStatus).toHaveBeenCalledWith( - 'test-order-id-123', - { - cardSDK: expect.any(Object), - }, - ); + expect(BackgroundBridge).toHaveBeenCalled(); }); - - expect(mockDangerouslyGetParent).toHaveBeenCalled(); - expect(mockDispatch).toHaveBeenCalled(); }); - it('shows error when polling returns failed status', async () => { - mockGetDaimoEnvironment.mockReturnValue('production'); - mockPollPaymentStatus.mockResolvedValue({ - status: 'failed', - errorMessage: 'Payment was rejected', - }); - - const { getByTestId } = render(); + it('handles http error', async () => { + const { getByTestId } = renderWithProvider(); - await waitFor(() => { - expect(mockOnMessage).not.toBeNull(); - }); + const webView = getByTestId(DaimoPayModalSelectors.WEBVIEW); - // Trigger paymentStarted to start polling await act(async () => { - if (mockOnMessage) { - mockOnMessage({ - nativeEvent: { - data: JSON.stringify({ - source: 'daimo-pay', - version: 1, - type: 'paymentStarted', - payload: {}, - }), - }, - }); - } - }); - - // Advance timers to trigger first poll (5 seconds) - await act(async () => { - jest.advanceTimersByTime(5000); - }); - - await waitFor(() => { - expect(mockPollPaymentStatus).toHaveBeenCalled(); + webView.props.onHttpError(); }); await waitFor(() => { expect(getByTestId(DaimoPayModalSelectors.ERROR_TEXT)).toBeTruthy(); }); }); + }); - it('continues polling when status is pending', async () => { - mockGetDaimoEnvironment.mockReturnValue('production'); - mockPollPaymentStatus - .mockResolvedValueOnce({ status: 'pending' }) - .mockResolvedValueOnce({ status: 'pending' }) - .mockResolvedValueOnce({ - status: 'completed', - transactionHash: '0xdef456', - chainId: 137, - }); + describe('message filtering', () => { + it('ignores messages from invalid origins', async () => { + (DaimoPayService.isValidMessageOrigin as jest.Mock).mockReturnValueOnce( + false, + ); - render(); + const BackgroundBridge = require('../../../../../core/BackgroundBridge/BackgroundBridge'); + const mockOnMessage = jest.fn(); + BackgroundBridge.mockImplementationOnce(() => ({ + sendNotificationEip1193: jest.fn(), + onDisconnect: jest.fn(), + onMessage: mockOnMessage, + url: 'https://pay.daimo.com', + })); - await waitFor(() => { - expect(mockOnMessage).not.toBeNull(); - }); + const { getByTestId } = renderWithProvider(); + + const webView = getByTestId(DaimoPayModalSelectors.WEBVIEW); - // Trigger paymentStarted to start polling - await act(async () => { - if (mockOnMessage) { - mockOnMessage({ - nativeEvent: { - data: JSON.stringify({ - source: 'daimo-pay', - version: 1, - type: 'paymentStarted', - payload: {}, - }), - }, - }); - } - }); - - // First poll - pending await act(async () => { - jest.advanceTimersByTime(5000); + webView.props.onMessage({ + nativeEvent: { + data: JSON.stringify({ + name: 'metamask-provider', + origin: 'https://malicious-site.com', + }), + }, + }); }); - await waitFor(() => { - expect(mockPollPaymentStatus).toHaveBeenCalledTimes(1); - }); + expect(mockGoBack).not.toHaveBeenCalled(); + }); - expect(mockDispatch).not.toHaveBeenCalled(); + it('ignores non-object messages', async () => { + const { getByTestId } = renderWithProvider(); + + const webView = getByTestId(DaimoPayModalSelectors.WEBVIEW); - // Second poll - pending await act(async () => { - jest.advanceTimersByTime(5000); + webView.props.onMessage({ + nativeEvent: { + data: JSON.stringify(null), + }, + }); }); - await waitFor(() => { - expect(mockPollPaymentStatus).toHaveBeenCalledTimes(2); - }); + expect(mockGoBack).not.toHaveBeenCalled(); + }); - expect(mockDispatch).not.toHaveBeenCalled(); + it('ignores string-only messages', async () => { + const { getByTestId } = renderWithProvider(); - // Third poll - completed - await act(async () => { - jest.advanceTimersByTime(5000); - }); + const webView = getByTestId(DaimoPayModalSelectors.WEBVIEW); - await waitFor(() => { - expect(mockPollPaymentStatus).toHaveBeenCalledTimes(3); + await act(async () => { + webView.props.onMessage({ + nativeEvent: { + data: JSON.stringify('just a string'), + }, + }); }); - expect(mockDispatch).toHaveBeenCalled(); + expect(mockGoBack).not.toHaveBeenCalled(); }); + }); - it('does not start polling in non-production environment', async () => { - mockGetDaimoEnvironment.mockReturnValue('demo'); + describe('navigation fallback', () => { + it('navigates directly when parentNavigator is null', async () => { + mockGetParent.mockReturnValueOnce(null); - render(); + const { getByTestId } = renderWithProvider(); - await waitFor(() => { - expect(mockOnMessage).not.toBeNull(); - }); + const webView = getByTestId(DaimoPayModalSelectors.WEBVIEW); - // Trigger paymentStarted - await act(async () => { - if (mockOnMessage) { - mockOnMessage({ - nativeEvent: { - data: JSON.stringify({ - source: 'daimo-pay', - version: 1, - type: 'paymentStarted', - payload: {}, - }), - }, - }); - } - }); - - // Advance timers await act(async () => { - jest.advanceTimersByTime(10000); + webView.props.onMessage({ + nativeEvent: { + data: JSON.stringify({ + source: 'daimo-pay', + type: 'paymentCompleted', + payload: { + txHash: '0x123abc', + chainId: 1, + }, + }), + }, + }); }); - // Polling should not have been called in demo mode - expect(mockPollPaymentStatus).not.toHaveBeenCalled(); + expect(mockGetParent).toHaveBeenCalled(); + expect(mockNavigate).toHaveBeenCalledWith( + Routes.CARD.ORDER_COMPLETED, + expect.objectContaining({ + paymentMethod: 'crypto', + transactionHash: '0x123abc', + fromUpgrade: false, + }), + ); + expect(mockDispatch).not.toHaveBeenCalled(); }); }); - describe('Analytics', () => { - it('tracks modalClosed event', async () => { - render(); + describe('backgroundBridge messages', () => { + it('processes bridge messages with name property', async () => { + const { getByTestId } = renderWithProvider(); - await waitFor(() => { - expect(mockOnMessage).not.toBeNull(); - }); + const webView = getByTestId(DaimoPayModalSelectors.WEBVIEW); await act(async () => { - if (mockOnMessage) { - mockOnMessage({ - nativeEvent: { - data: JSON.stringify({ - source: 'daimo-pay', - version: 1, - type: 'modalClosed', - payload: {}, - }), - }, - }); - } - }); - - expect(mockCreateEventBuilder).toHaveBeenCalledWith( - MetaMetricsEvents.CARD_METAL_CHECKOUT_USER_CANCELED, - ); - expect(mockAddProperties).toHaveBeenCalledWith({ - screen: CardScreens.DAIMO_PAY, - }); - }); - - it('tracks modalOpened event', async () => { - render(); - - await waitFor(() => { - expect(mockOnMessage).not.toBeNull(); + webView.props.onLoadEnd(); }); await act(async () => { - if (mockOnMessage) { - mockOnMessage({ - nativeEvent: { - data: JSON.stringify({ - source: 'daimo-pay', - version: 1, - type: 'modalOpened', - payload: {}, - }), - }, - }); - } - }); - - expect(mockCreateEventBuilder).toHaveBeenCalledWith( - MetaMetricsEvents.CARD_METAL_CHECKOUT_VIEWED, - ); - expect(mockAddProperties).toHaveBeenCalledWith({ - screen: CardScreens.DAIMO_PAY, + webView.props.onMessage({ + nativeEvent: { + data: JSON.stringify({ + name: 'metamask-provider', + data: { method: 'eth_accounts' }, + }), + }, + }); }); + + expect(mockGoBack).not.toHaveBeenCalled(); }); + }); - it('tracks paymentStarted event', async () => { - render(); + describe('paymentBounced without error details', () => { + it('uses default error message when no error details provided', async () => { + const { getByTestId } = renderWithProvider(); - await waitFor(() => { - expect(mockOnMessage).not.toBeNull(); - }); + const webView = getByTestId(DaimoPayModalSelectors.WEBVIEW); await act(async () => { - if (mockOnMessage) { - mockOnMessage({ - nativeEvent: { - data: JSON.stringify({ - source: 'daimo-pay', - version: 1, - type: 'paymentStarted', - payload: {}, - }), - }, - }); - } - }); - - expect(mockCreateEventBuilder).toHaveBeenCalledWith( - MetaMetricsEvents.CARD_METAL_CHECKOUT_STARTED, - ); - expect(mockAddProperties).toHaveBeenCalledWith({ - screen: CardScreens.DAIMO_PAY, + webView.props.onMessage({ + nativeEvent: { + data: JSON.stringify({ + source: 'daimo-pay', + type: 'paymentBounced', + payload: {}, + }), + }, + }); }); - }); - - it('tracks paymentBounced event', async () => { - render(); await waitFor(() => { - expect(mockOnMessage).not.toBeNull(); - }); - - await act(async () => { - if (mockOnMessage) { - mockOnMessage({ - nativeEvent: { - data: JSON.stringify({ - source: 'daimo-pay', - version: 1, - type: 'paymentBounced', - payload: {}, - }), - }, - }); - } - }); - - expect(mockCreateEventBuilder).toHaveBeenCalledWith( - MetaMetricsEvents.CARD_METAL_CHECKOUT_FAILED, - ); - expect(mockAddProperties).toHaveBeenCalledWith({ - screen: CardScreens.DAIMO_PAY, - error: undefined, + expect(getByTestId(DaimoPayModalSelectors.ERROR_TEXT)).toBeTruthy(); }); }); }); diff --git a/app/components/UI/Card/components/DaimoPayModal/DaimoPayModal.tsx b/app/components/UI/Card/components/DaimoPayModal/DaimoPayModal.tsx index 24b8ff4f049..57b99e7c905 100644 --- a/app/components/UI/Card/components/DaimoPayModal/DaimoPayModal.tsx +++ b/app/components/UI/Card/components/DaimoPayModal/DaimoPayModal.tsx @@ -173,7 +173,7 @@ const DaimoPayModal: React.FC = () => { queryKey: cardQueries.dashboard.keys.cardDetails(), }); - const parentNavigator = navigation.dangerouslyGetParent(); + const parentNavigator = navigation.getParent(); if (parentNavigator) { parentNavigator.dispatch( CommonActions.reset({ @@ -200,14 +200,11 @@ const DaimoPayModal: React.FC = () => { }), ); } else { - navigation.navigate( - Routes.CARD.ORDER_COMPLETED as never, - { - paymentMethod: 'crypto', - transactionHash: txHash, - fromUpgrade, - } as never, - ); + navigation.navigate(Routes.CARD.ORDER_COMPLETED, { + paymentMethod: 'crypto', + transactionHash: txHash, + fromUpgrade, + }); } }, [trackEvent, createEventBuilder, navigation, fromUpgrade, queryClient], diff --git a/app/components/UI/Card/components/ManageCardListItem/__snapshots__/ManageCardListItem.test.tsx.snap b/app/components/UI/Card/components/ManageCardListItem/__snapshots__/ManageCardListItem.test.tsx.snap index 405e441c4e2..70e3970a8ff 100644 --- a/app/components/UI/Card/components/ManageCardListItem/__snapshots__/ManageCardListItem.test.tsx.snap +++ b/app/components/UI/Card/components/ManageCardListItem/__snapshots__/ManageCardListItem.test.tsx.snap @@ -20,363 +20,386 @@ exports[`ManageCardListItem Component renders with React.ReactNode description a } > - - - + + /> + + - + - ManageCardListItem - + + ManageCardListItem + + + - - - - - + - - - + - - Title with React Node - - - Custom + + Title with React Node + + + Custom + - - + + - - - + + + `; @@ -401,411 +424,434 @@ exports[`ManageCardListItem Component renders with all props and matches snapsho } > - - - + + /> + + - + - ManageCardListItem - + + ManageCardListItem + + + - - - - - + - - - + - - Custom Title - - + Custom Title + + + Custom description + + + - Custom description - - - - - + + testID="listitemcolumn" + > + + - - + + - - - + + + `; @@ -830,410 +876,433 @@ exports[`ManageCardListItem Component renders with custom right icon when rightI } > - - - + + /> + + - + - ManageCardListItem - + + ManageCardListItem + + + - - - - - + - - - + - - Custom Icon Test - - + Custom Icon Test + + + Should use Edit icon + + + - Should use Edit icon - - - - - + + testID="listitemcolumn" + > + + - - + + - - - + + + `; @@ -1258,378 +1327,401 @@ exports[`ManageCardListItem Component renders with required props and matches sn } > - - - + + /> + + + + - + + ManageCardListItem + + + - ManageCardListItem - + /> - - - - - + - - - + - - Test Title - - - Test description - + + Test Title + + + Test description + + - - + + - - - + + + `; @@ -1654,378 +1746,401 @@ exports[`ManageCardListItem Component renders without right icon when rightIcon } > - - - + + /> + + - + - ManageCardListItem - + + ManageCardListItem + + + - - - - - + - - - + - - No Icon Test - - - Should render without any right icon - + + No Icon Test + + + Should render without any right icon + + - - + + - - - + + + `; diff --git a/app/components/UI/Card/components/PasswordBottomSheet/__snapshots__/PasswordBottomSheet.test.tsx.snap b/app/components/UI/Card/components/PasswordBottomSheet/__snapshots__/PasswordBottomSheet.test.tsx.snap index eebf4e0e963..7a5b2b2bcb6 100644 --- a/app/components/UI/Card/components/PasswordBottomSheet/__snapshots__/PasswordBottomSheet.test.tsx.snap +++ b/app/components/UI/Card/components/PasswordBottomSheet/__snapshots__/PasswordBottomSheet.test.tsx.snap @@ -20,436 +20,436 @@ exports[`PasswordBottomSheet renders correctly and matches snapshot 1`] = ` } > - - - + + /> + + - + - PasswordBottomSheet - + + PasswordBottomSheet + + + - - - - - + - - - - - - - + /> + + - + - + + + - Enter password - - - - - - + + + + - + > + + + - - - - Enter your wallet password to view card details. - - - - - - Cancel + Enter your wallet password to view card details. - - + + - - Confirm - - + + Cancel + + + + + Confirm + + + @@ -669,9 +692,9 @@ exports[`PasswordBottomSheet renders correctly and matches snapshot 1`] = ` - - - + + + `; diff --git a/app/components/UI/Card/components/ViewPinBottomSheet/__snapshots__/ViewPinBottomSheet.test.tsx.snap b/app/components/UI/Card/components/ViewPinBottomSheet/__snapshots__/ViewPinBottomSheet.test.tsx.snap index 28d2994ce8f..608f5e9d180 100644 --- a/app/components/UI/Card/components/ViewPinBottomSheet/__snapshots__/ViewPinBottomSheet.test.tsx.snap +++ b/app/components/UI/Card/components/ViewPinBottomSheet/__snapshots__/ViewPinBottomSheet.test.tsx.snap @@ -20,436 +20,436 @@ exports[`ViewPinBottomSheet renders correctly and matches snapshot 1`] = ` } > - - - + + /> + + - + - ViewPinBottomSheet - + + ViewPinBottomSheet + + + - - - - - + - - - - - - - + /> + + - + - + + + - Your Card PIN - - - - - - + + + + - + > + + + - - - - + + + + testID="view-pin-image" + /> + + - @@ -618,9 +641,9 @@ exports[`ViewPinBottomSheet renders correctly and matches snapshot 1`] = ` - - - + + + `; diff --git a/app/components/UI/Card/hooks/useNavigateToCardPage.tsx b/app/components/UI/Card/hooks/useNavigateToCardPage.tsx index 567e4a2f269..eda21c7cba3 100644 --- a/app/components/UI/Card/hooks/useNavigateToCardPage.tsx +++ b/app/components/UI/Card/hooks/useNavigateToCardPage.tsx @@ -4,12 +4,12 @@ import { RootState } from '../../../../reducers'; import { BrowserTab } from '../../Tokens/types'; import { isCardUrl, isCardTravelUrl, isCardTosUrl } from '../../../../util/url'; import AppConstants from '../../../../core/AppConstants'; -import { NavigationProp, ParamListBase } from '@react-navigation/native'; import Routes from '../../../../constants/navigation/Routes'; import { useAnalytics } from '../../../hooks/useAnalytics/useAnalytics'; import { MetaMetricsEvents } from '../../../../core/Analytics'; import { CardActions } from '../util/metrics'; import { Linking } from 'react-native'; +import type { AppNavigationProp } from '../../../../core/NavigationService/types'; export enum CardInternalBrowserPage { TRAVEL = 'travel', @@ -43,7 +43,7 @@ const PAGE_CONFIG: Record< }; export const useNavigateToInternalBrowserPage = ( - navigation: NavigationProp, + navigation: AppNavigationProp, ) => { const browserTabs = useSelector((state: RootState) => state.browser.tabs); const { trackEvent, createEventBuilder } = useAnalytics(); @@ -107,9 +107,7 @@ export const useNavigateToInternalBrowserPage = ( * Hook that provides navigation functions for Card-related internal browser pages. * Returns convenience methods for navigating to Card, Travel, and TOS pages. */ -export const useNavigateToCardPage = ( - navigation: NavigationProp, -) => { +export const useNavigateToCardPage = (navigation: AppNavigationProp) => { const { navigateToInternalBrowserPage } = useNavigateToInternalBrowserPage(navigation); diff --git a/app/components/UI/Card/pushProvisioning/adapters/card/GalileoCardAdapter.test.ts b/app/components/UI/Card/pushProvisioning/adapters/card/GalileoCardAdapter.test.ts index 23e5d2c51fc..5195162e477 100644 --- a/app/components/UI/Card/pushProvisioning/adapters/card/GalileoCardAdapter.test.ts +++ b/app/components/UI/Card/pushProvisioning/adapters/card/GalileoCardAdapter.test.ts @@ -27,13 +27,9 @@ describe('GalileoCardAdapter', () => { }); describe('getOpaquePaymentCard', () => { - it('returns successful response with encrypted payload', async () => { + it('returns opaquePaymentCard from SDK response', async () => { const mockResponse = { opaquePaymentCard: 'encrypted-opaque-card-data', - cardNetwork: 'MASTERCARD', - lastFourDigits: '1234', - cardholderName: 'John Doe', - cardDescription: 'MetaMask Card', }; mockCardSDK.createGoogleWalletProvisioningRequest.mockResolvedValue( mockResponse, @@ -41,22 +37,15 @@ describe('GalileoCardAdapter', () => { const result = await adapter.getOpaquePaymentCard(); - expect(result.success).toBe(true); - expect(result.encryptedPayload?.opaquePaymentCard).toBe( - 'encrypted-opaque-card-data', - ); - expect(result.cardNetwork).toBe('MASTERCARD'); - expect(result.lastFourDigits).toBe('1234'); - expect(result.cardholderName).toBe('John Doe'); + expect(result).toEqual({ + opaquePaymentCard: 'encrypted-opaque-card-data', + }); }); it('throws ProvisioningError when opaquePaymentCard is missing', async () => { // Test edge case where SDK returns invalid data mockCardSDK.createGoogleWalletProvisioningRequest.mockResolvedValue({ opaquePaymentCard: '', - cardNetwork: 'MASTERCARD', - lastFourDigits: '1234', - cardholderName: 'John Doe', }); await expect(adapter.getOpaquePaymentCard()).rejects.toThrow(); @@ -66,9 +55,6 @@ describe('GalileoCardAdapter', () => { // Test edge case where SDK returns null/undefined - use type assertion for testing mockCardSDK.createGoogleWalletProvisioningRequest.mockResolvedValue( undefined as unknown as { - cardNetwork: string; - lastFourDigits: string; - cardholderName: string; opaquePaymentCard: string; }, ); diff --git a/app/components/UI/Card/pushProvisioning/adapters/card/GalileoCardAdapter.ts b/app/components/UI/Card/pushProvisioning/adapters/card/GalileoCardAdapter.ts index d54dd64e249..363e102f221 100644 --- a/app/components/UI/Card/pushProvisioning/adapters/card/GalileoCardAdapter.ts +++ b/app/components/UI/Card/pushProvisioning/adapters/card/GalileoCardAdapter.ts @@ -2,37 +2,20 @@ * Galileo Card Provider Adapter * * Implementation of ICardProviderAdapter for Galileo card issuing platform. - * Handles payload encryption for Google Wallet provisioning. * * @see https://docs.galileo-ft.com/pro/docs/setup-for-push-provisioning */ import { CardProviderId, - ProvisioningResponse, ProvisioningError, ProvisioningErrorCode, - CardNetwork, ApplePayEncryptedPayload, } from '../../types'; import { ICardProviderAdapter } from './ICardProviderAdapter'; import { CardSDK } from '../../../sdk/CardSDK'; import { strings } from '../../../../../../../locales/i18n'; -/** - * Galileo Card Provider Adapter - * - * This adapter interfaces with the Galileo API to encrypt card data - * for push provisioning to Google Wallet. - * - * Galileo Push Provisioning Flow: - * 1. App initiates provisioning and gets card ID - * 2. App sends data to Galileo's Create Provisioning Request API - * 3. Galileo encrypts the payload and returns it - * 4. App passes encrypted payload to wallet SDK - * - * @see https://docs.galileo-ft.com/pro/docs/creating-a-provisioning-request - */ export class GalileoCardAdapter implements ICardProviderAdapter { readonly providerId: CardProviderId = 'galileo'; @@ -42,13 +25,7 @@ export class GalileoCardAdapter implements ICardProviderAdapter { this.cardSDK = cardSDK; } - /** - * Get pre-encrypted opaque payment card data for Google Wallet - * - * For Google Wallet, we need to send the card ID - * to Galileo, which returns the pre-encrypted opaque payment card data. - */ - async getOpaquePaymentCard(): Promise { + async getOpaquePaymentCard(): Promise<{ opaquePaymentCard: string }> { try { const response = await this.cardSDK.createGoogleWalletProvisioningRequest(); @@ -61,14 +38,7 @@ export class GalileoCardAdapter implements ICardProviderAdapter { } return { - success: true, - encryptedPayload: { - opaquePaymentCard: response.opaquePaymentCard, - }, - cardNetwork: response.cardNetwork as CardNetwork, - lastFourDigits: response.lastFourDigits, - cardholderName: response.cardholderName, - cardDescription: response.cardDescription, + opaquePaymentCard: response.opaquePaymentCard, }; } catch (error) { if (error instanceof ProvisioningError) { @@ -83,33 +53,14 @@ export class GalileoCardAdapter implements ICardProviderAdapter { } } - /** - * Convert Base64-encoded string to hex-encoded string - * - * PassKit provides data as Base64-encoded strings, but the API - * expects hex-encoded strings. - * - * @param base64 - Base64-encoded string - * @returns Hex-encoded string - */ + /** Convert Base64-encoded string to hex (PassKit provides Base64, API expects hex) */ private base64ToHex(base64: string): string { - // Use Buffer to decode Base64 and convert to hex return Buffer.from(base64, 'base64').toString('hex'); } /** - * Get encrypted payload for Apple Pay in-app provisioning - * - * For Apple Pay, PassKit provides cryptographic data (nonce, certificates) - * as Base64-encoded strings. This method converts them to hex and sends - * to Galileo to get the encrypted payload. - * - * @param nonce - Cryptographic nonce from PassKit (Base64-encoded) - * @param nonceSignature - Signature of the nonce from PassKit (Base64-encoded) - * @param certificates - Array of certificate strings from PassKit (Base64-encoded) - * @param certificates[0] - leaf certificate - * @param certificates[1] - intermediate certificate - * @returns Promise resolving to the encrypted Apple Pay payload + * Get encrypted payload for Apple Pay in-app provisioning. + * Converts PassKit's Base64-encoded nonce/certificates to hex before sending to Galileo. */ async getApplePayEncryptedPayload( nonce: string, @@ -117,7 +68,6 @@ export class GalileoCardAdapter implements ICardProviderAdapter { certificates: string[], ): Promise { try { - // Validate certificates array if (!certificates || certificates.length < 2) { throw new ProvisioningError( ProvisioningErrorCode.ENCRYPTION_FAILED, @@ -125,7 +75,6 @@ export class GalileoCardAdapter implements ICardProviderAdapter { ); } - // Convert Base64 to hex as required by the API const leafCertificate = this.base64ToHex(certificates[0]); const intermediateCertificate = this.base64ToHex(certificates[1]); const nonceHex = this.base64ToHex(nonce); diff --git a/app/components/UI/Card/pushProvisioning/adapters/card/ICardProviderAdapter.ts b/app/components/UI/Card/pushProvisioning/adapters/card/ICardProviderAdapter.ts index aae44c8887c..673ca64413d 100644 --- a/app/components/UI/Card/pushProvisioning/adapters/card/ICardProviderAdapter.ts +++ b/app/components/UI/Card/pushProvisioning/adapters/card/ICardProviderAdapter.ts @@ -1,66 +1,19 @@ /** * Card Provider Adapter Interface * - * Defines the contract for card provider adapters that handle - * payload encryption for push provisioning. + * Abstracts card provider differences (APIs, encryption requirements) + * for push provisioning payload encryption. */ -import { - CardProviderId, - ProvisioningResponse, - ApplePayEncryptedPayload, -} from '../../types'; +import { CardProviderId, ApplePayEncryptedPayload } from '../../types'; -/** - * Interface for card provider adapters. - * - * Card providers (e.g., Galileo) are responsible for: - * - Encrypting card data for wallet providers - * - * Each card provider has different APIs and encryption requirements, - * so this interface abstracts those differences. - */ export interface ICardProviderAdapter { - /** - * Unique identifier for this card provider - */ readonly providerId: CardProviderId; - /** - * Get pre-encrypted opaque payment card data for Google Wallet - * - * The card provider pre-encrypts the card data before it's passed - * to the Tap and Pay SDK. - * - * Flow: - * 1. Send card ID to card provider API - * 3. Card provider returns encrypted opaque payment card - * 4. Opaque payment card is passed to Google Tap and Pay SDK - * - * @returns Promise resolving to the provisioning response with encrypted payload - */ - getOpaquePaymentCard(): Promise; + /** Get pre-encrypted opaque payment card data for Google Wallet */ + getOpaquePaymentCard(): Promise<{ opaquePaymentCard: string }>; - /** - * Get encrypted payload for Apple Pay in-app provisioning - * - * This method is called during the Apple Pay provisioning flow when PassKit - * provides cryptographic data (nonce, certificates) that must be sent to - * the card provider to get the encrypted payload. - * - * Flow: - * 1. User initiates Add to Apple Wallet - * 2. PassKit presents the provisioning UI - * 3. PassKit returns nonce, nonceSignature, and certificates - * 4. This method sends those to the card provider API - * 5. Card provider returns encrypted payload - * 6. Encrypted payload is returned to PassKit to complete provisioning - * - * @param nonce - Cryptographic nonce from PassKit - * @param nonceSignature - Signature of the nonce from PassKit - * @param certificates - Array of certificate strings from PassKit - * @returns Promise resolving to the encrypted Apple Pay payload - */ + /** Get encrypted payload for Apple Pay in-app provisioning via PassKit nonce/certificates exchange */ getApplePayEncryptedPayload?( nonce: string, nonceSignature: string, diff --git a/app/components/UI/Card/pushProvisioning/adapters/index.ts b/app/components/UI/Card/pushProvisioning/adapters/index.ts index 93fe13cf887..794c9835b5f 100644 --- a/app/components/UI/Card/pushProvisioning/adapters/index.ts +++ b/app/components/UI/Card/pushProvisioning/adapters/index.ts @@ -2,9 +2,6 @@ * Push Provisioning Adapters * * Re-exports all adapter interfaces and implementations. - * - * NOTE: This is the base module. Platform-specific adapters - * (GoogleWalletAdapter, AppleWalletAdapter) are added in platform-specific branches. */ // Card provider adapters @@ -13,7 +10,6 @@ export { type ICardProviderAdapter, GalileoCardAdapter } from './card'; // Wallet provider adapters export { type IWalletProviderAdapter, + GoogleWalletAdapter, type TokenInfo, - // NOTE: Platform-specific adapters (GoogleWalletAdapter, AppleWalletAdapter) - // are exported from platform-specific branches } from './wallet'; diff --git a/app/components/UI/Card/pushProvisioning/adapters/wallet/BaseWalletAdapter.ts b/app/components/UI/Card/pushProvisioning/adapters/wallet/BaseWalletAdapter.ts index a2ee4707ca4..2d313f17609 100644 --- a/app/components/UI/Card/pushProvisioning/adapters/wallet/BaseWalletAdapter.ts +++ b/app/components/UI/Card/pushProvisioning/adapters/wallet/BaseWalletAdapter.ts @@ -1,8 +1,8 @@ /** * Base Wallet Provider Adapter * - * Abstract base class providing common functionality for wallet adapters. - * Handles module loading, activation listeners, and eligibility determination. + * Abstract base class providing common functionality for wallet adapters: + * module loading, activation listeners, and eligibility determination. */ import { Platform, PlatformOSType } from 'react-native'; @@ -19,14 +19,6 @@ import Logger from '../../../../../../util/Logger'; import { strings } from '../../../../../../../locales/i18n'; import { getWalletName } from '../../constants'; -/** - * Base class for wallet adapters providing common functionality - * - * Subclasses must implement: - * - walletType, platform properties - * - provisionCard method - * - onActivationEvent method (for handling activation events) - */ export abstract class BaseWalletAdapter { abstract readonly walletType: WalletType; abstract readonly platform: PlatformOSType; @@ -43,24 +35,10 @@ export abstract class BaseWalletAdapter { this.activationListeners = new Set(); } - /** - * Get the adapter name for logging - */ protected abstract getAdapterName(): string; - - /** - * Get the expected platform for this adapter - */ protected abstract getExpectedPlatform(): PlatformOSType; - - /** - * Handle raw activation event data from native module - */ protected abstract handleNativeActivationEvent(data: unknown): void; - /** - * Initialize the wallet module lazily - */ protected async initializeWalletModule(): Promise { if (Platform.OS !== this.getExpectedPlatform()) { return; @@ -79,9 +57,6 @@ export abstract class BaseWalletAdapter { } } - /** - * Get the wallet module, ensuring it's loaded - */ protected async getWalletModule(): Promise< typeof import('@expensify/react-native-wallet') > { @@ -120,9 +95,6 @@ export abstract class BaseWalletAdapter { return this.walletModule; } - /** - * Check if wallet is available on this device - */ async checkAvailability(): Promise { if (Platform.OS !== this.getExpectedPlatform()) { Logger.log( @@ -148,9 +120,6 @@ export abstract class BaseWalletAdapter { } } - /** - * Check the status of a specific card in the wallet - */ async getCardStatus(lastFourDigits: string): Promise { try { const wallet = await this.getWalletModule(); @@ -162,9 +131,6 @@ export abstract class BaseWalletAdapter { } } - /** - * Get detailed wallet eligibility information - */ async getEligibility(lastFourDigits?: string): Promise { const isAvailable = await this.checkAvailability(); @@ -186,13 +152,11 @@ export abstract class BaseWalletAdapter { existingCardStatus = await this.getCardStatus(lastFourDigits); } - // Get additional eligibility info (e.g., tokenReferenceId for Google) const additionalInfo = await this.getAdditionalEligibilityInfo( lastFourDigits, existingCardStatus, ); - // Determine recommended action based on card status const { canAddCard, recommendedAction, ineligibilityReason } = this.determineActionForStatus(existingCardStatus); @@ -206,9 +170,7 @@ export abstract class BaseWalletAdapter { }; } - /** - * Get additional eligibility information (override in subclasses) - */ + /** Override in subclasses to provide extra eligibility data (e.g., tokenReferenceId) */ protected async getAdditionalEligibilityInfo( _lastFourDigits?: string, _existingCardStatus?: CardTokenStatus, @@ -216,10 +178,7 @@ export abstract class BaseWalletAdapter { return {}; } - /** - * Determine the recommended action based on card status - * Can be overridden by subclasses for platform-specific behavior - */ + /** Determine recommended action based on card status. Override for platform-specific behavior. */ protected determineActionForStatus(status?: CardTokenStatus): { canAddCard: boolean; recommendedAction: WalletEligibility['recommendedAction']; @@ -234,7 +193,6 @@ export abstract class BaseWalletAdapter { }; case 'requires_activation': - // Default behavior - subclasses can override return { canAddCard: true, recommendedAction: 'add_card', @@ -279,22 +237,15 @@ export abstract class BaseWalletAdapter { } } - /** - * Add a listener for card activation events - */ addActivationListener( callback: (event: CardActivationEvent) => void, ): () => void { this.activationListeners.add(callback); - - // Set up the native listener asynchronously once the module is loaded this.setupNativeListenerIfNeeded(); - // Return unsubscribe function return () => { this.activationListeners.delete(callback); - // Remove native listener if no more listeners if (this.activationListeners.size === 0 && this.listenerSubscription) { this.listenerSubscription.remove(); this.listenerSubscription = undefined; @@ -302,25 +253,18 @@ export abstract class BaseWalletAdapter { }; } - /** - * Set up the native event listener once the module is loaded - */ protected async setupNativeListenerIfNeeded(): Promise { - // Already have a listener subscription if (this.listenerSubscription) { return; } - // No listeners registered, don't set up native listener if (this.activationListeners.size === 0) { return; } try { - // Wait for the module to load const wallet = await this.getWalletModule(); - // Check again after await - subscription might have been set up or listeners removed if (this.listenerSubscription || this.activationListeners.size === 0) { return; } @@ -339,9 +283,6 @@ export abstract class BaseWalletAdapter { } } - /** - * Notify all activation listeners - */ protected notifyActivationListeners(event: CardActivationEvent): void { this.activationListeners.forEach((callback) => { try { @@ -354,9 +295,6 @@ export abstract class BaseWalletAdapter { }); } - /** - * Create a platform not supported error result - */ protected createPlatformNotSupportedError(): ProvisioningError { return new ProvisioningError( ProvisioningErrorCode.PLATFORM_NOT_SUPPORTED, @@ -364,9 +302,6 @@ export abstract class BaseWalletAdapter { ); } - /** - * Create an invalid card data error result - */ protected createInvalidCardDataError(): ProvisioningError { return new ProvisioningError( ProvisioningErrorCode.INVALID_CARD_DATA, diff --git a/app/components/UI/Card/pushProvisioning/adapters/wallet/GoogleWalletAdapter.test.ts b/app/components/UI/Card/pushProvisioning/adapters/wallet/GoogleWalletAdapter.test.ts new file mode 100644 index 00000000000..8d839f1ea5c --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/adapters/wallet/GoogleWalletAdapter.test.ts @@ -0,0 +1,780 @@ +import { Platform } from 'react-native'; +import { GoogleWalletAdapter } from './GoogleWalletAdapter'; +import { + ProvisionCardParams, + ProvisioningErrorCode, + UserAddress, +} from '../../types'; + +// Mock react-native-wallet module functions +const mockAddCardToGoogleWallet = jest.fn(); +const mockResumeAddCardToGoogleWallet = jest.fn(); +const mockGetCardStatusBySuffix = jest.fn(); +const mockListTokens = jest.fn(); +const mockCheckWalletAvailability = jest.fn(); +const mockAddListener = jest.fn(); + +const mockWalletModule = { + addCardToGoogleWallet: mockAddCardToGoogleWallet, + resumeAddCardToGoogleWallet: mockResumeAddCardToGoogleWallet, + getCardStatusBySuffix: mockGetCardStatusBySuffix, + listTokens: mockListTokens, + checkWalletAvailability: mockCheckWalletAvailability, + addListener: mockAddListener, +}; + +// Mock Logger +jest.mock('../../../../../../util/Logger', () => ({ + log: jest.fn(), + error: jest.fn(), +})); + +// Mock i18n +jest.mock('../../../../../../../locales/i18n', () => ({ + strings: (key: string) => key, +})); + +// Mock constants +jest.mock('../../constants', () => ({ + getWalletName: () => 'Google Wallet', +})); + +/** + * Helper to inject mock wallet module into adapter + * Since the adapter uses dynamic imports, we need to manually inject the mock + */ +function injectMockModule(adapter: GoogleWalletAdapter): void { + // Access private property using type assertion + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (adapter as any).walletModule = mockWalletModule; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (adapter as any).moduleLoadPromise = Promise.resolve(); +} + +describe('GoogleWalletAdapter', () => { + let adapter: GoogleWalletAdapter; + const originalPlatform = Platform.OS; + + const mockUserAddress: UserAddress = { + name: 'John Doe', + addressOne: '123 Main St', + addressTwo: 'Apt 4B', + administrativeArea: 'NY', + locality: 'New York', + countryCode: 'US', + postalCode: '10001', + phoneNumber: '5551234567', + }; + + const mockProvisionParams: ProvisionCardParams = { + cardNetwork: 'MASTERCARD', + cardholderName: 'John Doe', + lastFourDigits: '1234', + cardDescription: 'MetaMask Card ending in 1234', + encryptedPayload: { + opaquePaymentCard: 'encrypted-opaque-card-data', + }, + userAddress: mockUserAddress, + }; + + beforeEach(() => { + jest.clearAllMocks(); + // Set platform to android for most tests + Object.defineProperty(Platform, 'OS', { + value: 'android', + writable: true, + }); + adapter = new GoogleWalletAdapter(); + // Inject mock module for tests that need it + injectMockModule(adapter); + }); + + afterEach(() => { + Object.defineProperty(Platform, 'OS', { + value: originalPlatform, + writable: true, + }); + }); + + describe('properties', () => { + it('has correct walletType', () => { + expect(adapter.walletType).toBe('google_wallet'); + }); + + it('has correct platform', () => { + expect(adapter.platform).toBe('android'); + }); + }); + + describe('checkAvailability', () => { + it('returns false on non-Android platforms', async () => { + Object.defineProperty(Platform, 'OS', { + value: 'ios', + writable: true, + }); + const iosAdapter = new GoogleWalletAdapter(); + + const result = await iosAdapter.checkAvailability(); + expect(result).toBe(false); + }); + + it('returns true when wallet is available', async () => { + mockCheckWalletAvailability.mockResolvedValue(true); + + const result = await adapter.checkAvailability(); + expect(result).toBe(true); + }); + + it('returns false when wallet is not available', async () => { + mockCheckWalletAvailability.mockResolvedValue(false); + + const result = await adapter.checkAvailability(); + expect(result).toBe(false); + }); + + it('returns false on error', async () => { + mockCheckWalletAvailability.mockRejectedValue(new Error('SDK error')); + + const result = await adapter.checkAvailability(); + expect(result).toBe(false); + }); + }); + + describe('getCardStatus', () => { + it('returns mapped card status', async () => { + mockGetCardStatusBySuffix.mockResolvedValue('active'); + + const result = await adapter.getCardStatus('1234'); + expect(result).toBe('active'); + expect(mockGetCardStatusBySuffix).toHaveBeenCalledWith('1234'); + }); + + it('returns not_found on error', async () => { + mockGetCardStatusBySuffix.mockRejectedValue(new Error('Card not found')); + + const result = await adapter.getCardStatus('1234'); + expect(result).toBe('not_found'); + }); + + it('maps requireActivation to requires_activation', async () => { + mockGetCardStatusBySuffix.mockResolvedValue('requireActivation'); + + const result = await adapter.getCardStatus('1234'); + expect(result).toBe('requires_activation'); + }); + + it('maps "not found" to not_found', async () => { + mockGetCardStatusBySuffix.mockResolvedValue('not found'); + + const result = await adapter.getCardStatus('1234'); + expect(result).toBe('not_found'); + }); + + it('maps pending status', async () => { + mockGetCardStatusBySuffix.mockResolvedValue('pending'); + + const result = await adapter.getCardStatus('1234'); + expect(result).toBe('pending'); + }); + + it('maps suspended status', async () => { + mockGetCardStatusBySuffix.mockResolvedValue('suspended'); + + const result = await adapter.getCardStatus('1234'); + expect(result).toBe('suspended'); + }); + + it('maps deactivated status', async () => { + mockGetCardStatusBySuffix.mockResolvedValue('deactivated'); + + const result = await adapter.getCardStatus('1234'); + expect(result).toBe('deactivated'); + }); + }); + + describe('provisionCard', () => { + describe('platform checks', () => { + it('returns error on non-Android platforms', async () => { + Object.defineProperty(Platform, 'OS', { + value: 'ios', + writable: true, + }); + const iosAdapter = new GoogleWalletAdapter(); + injectMockModule(iosAdapter); + + const result = await iosAdapter.provisionCard(mockProvisionParams); + + expect(result.status).toBe('error'); + expect(result.error?.code).toBe( + ProvisioningErrorCode.PLATFORM_NOT_SUPPORTED, + ); + }); + }); + + describe('validation', () => { + it('returns error when opaquePaymentCard is missing', async () => { + const params = { + ...mockProvisionParams, + encryptedPayload: {}, + }; + + const result = await adapter.provisionCard(params); + + expect(result.status).toBe('error'); + expect(result.error?.code).toBe( + ProvisioningErrorCode.INVALID_CARD_DATA, + ); + }); + + it('returns error when opaquePaymentCard is empty string', async () => { + const params = { + ...mockProvisionParams, + encryptedPayload: { opaquePaymentCard: '' }, + }; + + const result = await adapter.provisionCard(params); + + expect(result.status).toBe('error'); + expect(result.error?.code).toBe( + ProvisioningErrorCode.INVALID_CARD_DATA, + ); + }); + }); + + describe('normal flow (card not in wallet)', () => { + beforeEach(() => { + mockGetCardStatusBySuffix.mockResolvedValue('not found'); + mockAddCardToGoogleWallet.mockResolvedValue('success'); + }); + + it('calls addCardToGoogleWallet with correct data', async () => { + const result = await adapter.provisionCard(mockProvisionParams); + + expect(result.status).toBe('success'); + expect(mockAddCardToGoogleWallet).toHaveBeenCalledWith({ + network: 'MASTERCARD', + opaquePaymentCard: 'encrypted-opaque-card-data', + cardHolderName: 'John Doe', + lastDigits: '1234', + userAddress: { + name: 'John Doe', + addressOne: '123 Main St', + addressTwo: 'Apt 4B', + administrativeArea: 'NY', + locality: 'New York', + countryCode: 'US', + postalCode: '10001', + phoneNumber: '5551234567', + }, + }); + }); + + it('uses default address when userAddress is not provided', async () => { + const params = { + ...mockProvisionParams, + userAddress: undefined, + }; + + await adapter.provisionCard(params); + + expect(mockAddCardToGoogleWallet).toHaveBeenCalledWith( + expect.objectContaining({ + userAddress: { + name: 'John Doe', + addressOne: '', + addressTwo: '', + administrativeArea: '', + locality: '', + countryCode: 'US', + postalCode: '', + phoneNumber: '', + }, + }), + ); + }); + + it('returns canceled status when user cancels', async () => { + mockAddCardToGoogleWallet.mockResolvedValue('canceled'); + + const result = await adapter.provisionCard(mockProvisionParams); + + expect(result.status).toBe('canceled'); + }); + + it('returns error status when SDK returns error', async () => { + mockAddCardToGoogleWallet.mockResolvedValue('error'); + + const result = await adapter.provisionCard(mockProvisionParams); + + expect(result.status).toBe('error'); + }); + }); + + describe('auto-resume flow (Yellow Path)', () => { + const mockToken = { + identifier: 'token-123', + lastDigits: '1234', + tokenState: 1, + }; + + beforeEach(() => { + mockGetCardStatusBySuffix.mockResolvedValue('requireActivation'); + mockListTokens.mockResolvedValue([mockToken]); + mockResumeAddCardToGoogleWallet.mockResolvedValue('success'); + }); + + it('automatically resumes provisioning when card requires activation', async () => { + const result = await adapter.provisionCard(mockProvisionParams); + + expect(result.status).toBe('success'); + expect(mockResumeAddCardToGoogleWallet).toHaveBeenCalledWith({ + network: 'MASTERCARD', + tokenReferenceID: 'token-123', + cardHolderName: 'John Doe', + lastDigits: '1234', + }); + expect(mockAddCardToGoogleWallet).not.toHaveBeenCalled(); + }); + + it('falls back to addNewCard when token not found', async () => { + mockListTokens.mockResolvedValue([]); + mockAddCardToGoogleWallet.mockResolvedValue('success'); + + const result = await adapter.provisionCard(mockProvisionParams); + + expect(result.status).toBe('success'); + expect(mockAddCardToGoogleWallet).toHaveBeenCalled(); + expect(mockResumeAddCardToGoogleWallet).not.toHaveBeenCalled(); + }); + + it('falls back to addNewCard when token has different lastDigits', async () => { + mockListTokens.mockResolvedValue([ + { identifier: 'token-456', lastDigits: '5678', tokenState: 1 }, + ]); + mockAddCardToGoogleWallet.mockResolvedValue('success'); + + const result = await adapter.provisionCard(mockProvisionParams); + + expect(result.status).toBe('success'); + expect(mockAddCardToGoogleWallet).toHaveBeenCalled(); + expect(mockResumeAddCardToGoogleWallet).not.toHaveBeenCalled(); + }); + + it('handles resume canceled status', async () => { + mockResumeAddCardToGoogleWallet.mockResolvedValue('canceled'); + + const result = await adapter.provisionCard(mockProvisionParams); + + expect(result.status).toBe('canceled'); + }); + + it('handles resume error status', async () => { + mockResumeAddCardToGoogleWallet.mockResolvedValue('error'); + + const result = await adapter.provisionCard(mockProvisionParams); + + expect(result.status).toBe('error'); + }); + }); + + describe('error handling', () => { + beforeEach(() => { + mockGetCardStatusBySuffix.mockResolvedValue('not found'); + }); + + it('returns error result when SDK returns error status', async () => { + mockAddCardToGoogleWallet.mockResolvedValue('error'); + + const result = await adapter.provisionCard(mockProvisionParams); + + expect(result.status).toBe('error'); + }); + + it('returns error result when getCardStatus throws', async () => { + mockGetCardStatusBySuffix.mockImplementationOnce(async () => { + throw new Error('Status failed'); + }); + mockAddCardToGoogleWallet.mockResolvedValue('success'); + + // getCardStatus error is caught and returns 'not_found', so flow continues + const result = await adapter.provisionCard(mockProvisionParams); + + expect(result.status).toBe('success'); + }); + }); + }); + + describe('resumeProvisioning', () => { + it('returns error on non-Android platforms', async () => { + Object.defineProperty(Platform, 'OS', { + value: 'ios', + writable: true, + }); + const iosAdapter = new GoogleWalletAdapter(); + injectMockModule(iosAdapter); + + const result = await iosAdapter.resumeProvisioning( + 'token-123', + 'MASTERCARD', + 'John Doe', + '1234', + ); + + expect(result.status).toBe('error'); + expect(result.error?.code).toBe( + ProvisioningErrorCode.PLATFORM_NOT_SUPPORTED, + ); + }); + + it('calls resumeAddCardToGoogleWallet with correct data', async () => { + mockResumeAddCardToGoogleWallet.mockResolvedValue('success'); + + const result = await adapter.resumeProvisioning( + 'token-123', + 'MASTERCARD', + 'John Doe', + '1234', + ); + + expect(result.status).toBe('success'); + expect(mockResumeAddCardToGoogleWallet).toHaveBeenCalledWith({ + network: 'MASTERCARD', + tokenReferenceID: 'token-123', + cardHolderName: 'John Doe', + lastDigits: '1234', + }); + }); + + it('handles optional parameters', async () => { + mockResumeAddCardToGoogleWallet.mockResolvedValue('success'); + + await adapter.resumeProvisioning('token-123', 'MASTERCARD'); + + expect(mockResumeAddCardToGoogleWallet).toHaveBeenCalledWith({ + network: 'MASTERCARD', + tokenReferenceID: 'token-123', + cardHolderName: undefined, + lastDigits: undefined, + }); + }); + + it('returns canceled status when user cancels resume', async () => { + mockResumeAddCardToGoogleWallet.mockResolvedValue('canceled'); + + const result = await adapter.resumeProvisioning( + 'token-123', + 'MASTERCARD', + ); + + expect(result.status).toBe('canceled'); + }); + + it('returns error result when SDK throws', async () => { + mockResumeAddCardToGoogleWallet.mockRejectedValue( + new Error('Resume failed'), + ); + + const result = await adapter.resumeProvisioning( + 'token-123', + 'MASTERCARD', + ); + + expect(result.status).toBe('error'); + expect(result.error?.code).toBe(ProvisioningErrorCode.UNKNOWN_ERROR); + }); + }); + + describe('listTokens', () => { + it('returns validated tokens', async () => { + const mockTokens = [ + { identifier: 'token-1', lastDigits: '1234', tokenState: 1 }, + { identifier: 'token-2', lastDigits: '5678', tokenState: 2 }, + ]; + mockListTokens.mockResolvedValue(mockTokens); + + const result = await adapter.listTokens(); + + expect(result).toEqual(mockTokens); + }); + + it('filters out invalid tokens', async () => { + const mockTokens = [ + { identifier: 'token-1', lastDigits: '1234', tokenState: 1 }, + { invalid: true }, + null, + ]; + mockListTokens.mockResolvedValue(mockTokens); + + const result = await adapter.listTokens(); + + expect(result).toEqual([ + { identifier: 'token-1', lastDigits: '1234', tokenState: 1 }, + ]); + }); + + it('returns empty array on error', async () => { + mockListTokens.mockRejectedValue(new Error('SDK error')); + + const result = await adapter.listTokens(); + + expect(result).toEqual([]); + }); + + it('returns empty array when SDK returns non-array', async () => { + mockListTokens.mockResolvedValue('not an array'); + + const result = await adapter.listTokens(); + + expect(result).toEqual([]); + }); + + it('returns empty array when SDK returns null', async () => { + mockListTokens.mockResolvedValue(null); + + const result = await adapter.listTokens(); + + expect(result).toEqual([]); + }); + }); + + describe('findTokenByLastDigits', () => { + it('finds token with matching lastDigits', async () => { + const mockTokens = [ + { identifier: 'token-1', lastDigits: '1234', tokenState: 1 }, + { identifier: 'token-2', lastDigits: '5678', tokenState: 2 }, + ]; + mockListTokens.mockResolvedValue(mockTokens); + + const result = await adapter.findTokenByLastDigits('5678'); + + expect(result).toEqual({ + identifier: 'token-2', + lastDigits: '5678', + tokenState: 2, + }); + }); + + it('returns null when no matching token found', async () => { + mockListTokens.mockResolvedValue([ + { identifier: 'token-1', lastDigits: '1234', tokenState: 1 }, + ]); + + const result = await adapter.findTokenByLastDigits('9999'); + + expect(result).toBeNull(); + }); + + it('returns null when listTokens fails', async () => { + mockListTokens.mockRejectedValue(new Error('SDK error')); + + const result = await adapter.findTokenByLastDigits('1234'); + + expect(result).toBeNull(); + }); + + it('returns first matching token when duplicates exist', async () => { + const mockTokens = [ + { identifier: 'token-1', lastDigits: '1234', tokenState: 1 }, + { identifier: 'token-2', lastDigits: '1234', tokenState: 2 }, + ]; + mockListTokens.mockResolvedValue(mockTokens); + + const result = await adapter.findTokenByLastDigits('1234'); + + expect(result?.identifier).toBe('token-1'); + }); + }); + + describe('getEligibility', () => { + beforeEach(() => { + mockCheckWalletAvailability.mockResolvedValue(true); + }); + + it('returns not available when wallet is not available', async () => { + mockCheckWalletAvailability.mockResolvedValue(false); + + const result = await adapter.getEligibility('1234'); + + expect(result.isAvailable).toBe(false); + expect(result.canAddCard).toBe(false); + }); + + it('returns canAddCard true when card not found', async () => { + mockGetCardStatusBySuffix.mockResolvedValue('not found'); + + const result = await adapter.getEligibility('1234'); + + expect(result.isAvailable).toBe(true); + expect(result.canAddCard).toBe(true); + expect(result.recommendedAction).toBe('add_card'); + }); + + it('returns canAddCard true with resume action when requires activation', async () => { + mockGetCardStatusBySuffix.mockResolvedValue('requireActivation'); + mockListTokens.mockResolvedValue([ + { identifier: 'token-123', lastDigits: '1234', tokenState: 1 }, + ]); + + const result = await adapter.getEligibility('1234'); + + expect(result.isAvailable).toBe(true); + expect(result.canAddCard).toBe(true); + expect(result.recommendedAction).toBe('resume'); + expect(result.tokenReferenceId).toBe('token-123'); + }); + + it('returns resume action without tokenReferenceId when token not found', async () => { + mockGetCardStatusBySuffix.mockResolvedValue('requireActivation'); + mockListTokens.mockResolvedValue([]); + + const result = await adapter.getEligibility('1234'); + + expect(result.recommendedAction).toBe('resume'); + expect(result.tokenReferenceId).toBeUndefined(); + }); + + it('returns canAddCard false when card is active', async () => { + mockGetCardStatusBySuffix.mockResolvedValue('active'); + + const result = await adapter.getEligibility('1234'); + + expect(result.isAvailable).toBe(true); + expect(result.canAddCard).toBe(false); + expect(result.recommendedAction).toBe('none'); + }); + + it('returns wait action when card is pending', async () => { + mockGetCardStatusBySuffix.mockResolvedValue('pending'); + + const result = await adapter.getEligibility('1234'); + + expect(result.canAddCard).toBe(false); + expect(result.recommendedAction).toBe('wait'); + }); + + it('returns contact_support action when card is suspended', async () => { + mockGetCardStatusBySuffix.mockResolvedValue('suspended'); + + const result = await adapter.getEligibility('1234'); + + expect(result.canAddCard).toBe(false); + expect(result.recommendedAction).toBe('contact_support'); + }); + + it('returns contact_support action when card is deactivated', async () => { + mockGetCardStatusBySuffix.mockResolvedValue('deactivated'); + + const result = await adapter.getEligibility('1234'); + + expect(result.canAddCard).toBe(false); + expect(result.recommendedAction).toBe('contact_support'); + }); + + it('returns eligibility without checking card status when no lastFourDigits', async () => { + const result = await adapter.getEligibility(); + + expect(result.isAvailable).toBe(true); + expect(result.canAddCard).toBe(true); + expect(mockGetCardStatusBySuffix).not.toHaveBeenCalled(); + }); + }); + + describe('addActivationListener', () => { + it('returns unsubscribe function', () => { + const callback = jest.fn(); + + const unsubscribe = adapter.addActivationListener(callback); + + expect(typeof unsubscribe).toBe('function'); + }); + + it('removes listener on unsubscribe', () => { + const callback = jest.fn(); + const unsubscribe = adapter.addActivationListener(callback); + + unsubscribe(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((adapter as any).activationListeners.size).toBe(0); + }); + + it('sets up native listener when listener is added', async () => { + mockAddListener.mockReturnValue({ remove: jest.fn() }); + + adapter.addActivationListener(jest.fn()); + + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(mockAddListener).toHaveBeenCalledWith( + 'onCardActivated', + expect.any(Function), + ); + }); + }); + + describe('activation event handling', () => { + beforeEach(() => { + mockAddListener.mockReturnValue({ remove: jest.fn() }); + }); + + it('notifies listeners on activated status', async () => { + let nativeCallback: (data: unknown) => void = () => undefined; + mockAddListener.mockImplementation((_event, callback) => { + nativeCallback = callback; + return { remove: jest.fn() }; + }); + + const listener = jest.fn(); + adapter.addActivationListener(listener); + await new Promise((resolve) => setTimeout(resolve, 0)); + + // Android SDK sends 'status' property with 'active' value + nativeCallback({ tokenId: 'token-123', status: 'active' }); + + expect(listener).toHaveBeenCalledWith({ + tokenId: 'token-123', + status: 'activated', + }); + }); + + it('notifies listeners on canceled status', async () => { + let nativeCallback: (data: unknown) => void = () => undefined; + mockAddListener.mockImplementation((_event, callback) => { + nativeCallback = callback; + return { remove: jest.fn() }; + }); + + const listener = jest.fn(); + adapter.addActivationListener(listener); + await new Promise((resolve) => setTimeout(resolve, 0)); + + // Android SDK sends 'status' property with 'canceled' value + nativeCallback({ tokenId: 'token-123', status: 'canceled' }); + + expect(listener).toHaveBeenCalledWith({ + tokenId: 'token-123', + status: 'canceled', + }); + }); + + it('notifies listeners on failed status (unknown status)', async () => { + let nativeCallback: (data: unknown) => void = () => undefined; + mockAddListener.mockImplementation((_event, callback) => { + nativeCallback = callback; + return { remove: jest.fn() }; + }); + + const listener = jest.fn(); + adapter.addActivationListener(listener); + await new Promise((resolve) => setTimeout(resolve, 0)); + + // Unknown status falls through to 'failed' + nativeCallback({ tokenId: 'token-123', status: 'unknown' }); + + expect(listener).toHaveBeenCalledWith({ + tokenId: 'token-123', + status: 'failed', + }); + }); + }); +}); diff --git a/app/components/UI/Card/pushProvisioning/adapters/wallet/GoogleWalletAdapter.ts b/app/components/UI/Card/pushProvisioning/adapters/wallet/GoogleWalletAdapter.ts new file mode 100644 index 00000000000..c808a6308af --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/adapters/wallet/GoogleWalletAdapter.ts @@ -0,0 +1,336 @@ +/** + * Google Wallet Provider Adapter + * + * Implementation of IWalletProviderAdapter for Google Wallet on Android. + * Wraps the react-native-wallet library's Android-specific methods. + * + * Currently only Mastercard is supported. + */ + +import { Platform, PlatformOSType } from 'react-native'; +import { + WalletType, + WalletEligibility, + CardTokenStatus, + ProvisionCardParams, + ProvisioningResult, + CardActivationEvent, + ProvisioningErrorCode, + UserAddress, +} from '../../types'; +import { IWalletProviderAdapter } from './IWalletProviderAdapter'; +import { BaseWalletAdapter } from './BaseWalletAdapter'; +import { + mapTokenizationStatus, + validateTokenArray, + createErrorResult, + logAdapterError, + TokenInfo, + TokenizationStatus, +} from './utils'; +import { strings } from '../../../../../../../locales/i18n'; + +// Types from react-native-wallet for Android +interface AndroidCardData { + network: string; + opaquePaymentCard: string; + cardHolderName: string; + lastDigits: string; + userAddress: { + name: string; + addressOne: string; + addressTwo?: string; + administrativeArea: string; + locality: string; + countryCode: string; + postalCode: string; + phoneNumber: string; + }; +} + +interface AndroidResumeCardData { + network: string; + tokenReferenceID: string; + cardHolderName?: string; + lastDigits?: string; +} + +/** + * Convert our UserAddress to Android format. + * Defensively defaults every field to a safe value so the Tap and Pay SDK + * never receives undefined/null, which would cause an opaque provisioning failure. + */ +function toAndroidUserAddress( + address: UserAddress, +): AndroidCardData['userAddress'] { + return { + name: address.name ?? '', + addressOne: address.addressOne ?? '', + addressTwo: address.addressTwo ?? '', + administrativeArea: address.administrativeArea ?? '', + locality: address.locality ?? '', + countryCode: address.countryCode ?? 'US', + postalCode: address.postalCode ?? '', + phoneNumber: address.phoneNumber ?? '', + }; +} + +/** + * Google Wallet Provider Adapter + * + * This adapter handles card provisioning to Google Wallet on Android devices. + * It uses the react-native-wallet library to interact with the Tap and Pay SDK. + * + * Google Wallet Provisioning Flow: + * 1. Check if Google Wallet is available on the device + * 3. Get opaque payment card from card provider + * 4. Call pushTokenize with the opaque payment card + * 5. Google/network creates and provisions the token + */ +export class GoogleWalletAdapter + extends BaseWalletAdapter + implements IWalletProviderAdapter +{ + readonly walletType: WalletType = 'google_wallet'; + readonly platform: PlatformOSType = 'android'; + + constructor() { + super(); + // Start loading the module immediately but don't block + this.moduleLoadPromise = this.initializeWalletModule(); + } + + protected getAdapterName(): string { + return 'GoogleWalletAdapter'; + } + + protected getExpectedPlatform(): PlatformOSType { + return 'android'; + } + + /** + * Handle activation event from native module + * + * Android SDK sends events with 'status' property (not 'actionStatus'). + * Possible values: 'active' (success), 'canceled' (user canceled). + * Note: The SDK never sends a 'failed' status - errors are handled + * via the function return value, not the activation event. + */ + protected handleNativeActivationEvent(data: unknown): void { + const typedData = data as { status?: string; tokenId?: string }; + const event: CardActivationEvent = { + tokenId: typedData.tokenId, + status: + typedData.status === 'active' + ? 'activated' + : typedData.status === 'canceled' + ? 'canceled' + : 'failed', // Defensive fallback for unknown statuses + }; + this.notifyActivationListeners(event); + } + + /** + * Override determineActionForStatus to support Yellow Path (resume) + */ + protected determineActionForStatus(status?: CardTokenStatus): { + canAddCard: boolean; + recommendedAction: WalletEligibility['recommendedAction']; + ineligibilityReason?: string; + } { + // Google Wallet supports resume flow for requires_activation + if (status === 'requires_activation') { + return { + canAddCard: true, + recommendedAction: 'resume', + }; + } + + // Use base class implementation for other statuses + return super.determineActionForStatus(status); + } + + /** + * Get additional eligibility info for Google Wallet (token reference ID for resume) + */ + protected async getAdditionalEligibilityInfo( + lastFourDigits?: string, + existingCardStatus?: CardTokenStatus, + ): Promise> { + // If card requires activation, find its token ID for resume flow + if (existingCardStatus === 'requires_activation' && lastFourDigits) { + const token = await this.findTokenByLastDigits(lastFourDigits); + if (token) { + return { tokenReferenceId: token.identifier }; + } + } + return {}; + } + + /** + * Provision a card to Google Wallet + * + * This method automatically handles the Yellow Path (requires activation) case: + * 1. Checks if the card requires activation + * 2. If so, automatically resumes provisioning + * 3. Otherwise, adds the card normally + * + * This makes the resume flow transparent to the user. + */ + async provisionCard( + params: ProvisionCardParams, + ): Promise { + if (Platform.OS !== 'android') { + return { + status: 'error', + error: this.createPlatformNotSupportedError(), + }; + } + + if (!params.encryptedPayload.opaquePaymentCard) { + return { + status: 'error', + error: this.createInvalidCardDataError(), + }; + } + + try { + // Check if card requires activation (Yellow Path) + const cardStatus = await this.getCardStatus(params.lastFourDigits); + + if (cardStatus === 'requires_activation') { + const existingToken = await this.findTokenByLastDigits( + params.lastFourDigits, + ); + + if (existingToken) { + return await this.resumeProvisioning( + existingToken.identifier, + params.cardNetwork, + params.cardholderName, + params.lastFourDigits, + ); + } + } + + return await this.addNewCard(params); + } catch (error) { + logAdapterError('GoogleWalletAdapter', 'provisionCard', error); + return createErrorResult( + error, + ProvisioningErrorCode.UNKNOWN_ERROR, + strings('card.push_provisioning.error_unknown'), + ); + } + } + + /** + * Add a new card to Google Wallet + * + * Internal method that handles the actual card addition via the Tap and Pay SDK. + */ + private async addNewCard( + params: ProvisionCardParams, + ): Promise { + const wallet = await this.getWalletModule(); + + // Build user address with defaults if not provided + const userAddress = params.userAddress ?? { + name: params.cardholderName, + addressOne: '', + administrativeArea: '', + locality: '', + countryCode: 'US', + postalCode: '', + phoneNumber: '', + }; + + // opaquePaymentCard is validated in provisionCard() before calling this method + const cardData: AndroidCardData = { + network: params.cardNetwork, + opaquePaymentCard: params.encryptedPayload.opaquePaymentCard as string, + cardHolderName: params.cardholderName, + lastDigits: params.lastFourDigits, + userAddress: toAndroidUserAddress(userAddress), + }; + + const status = await wallet.addCardToGoogleWallet(cardData); + + return { + status: mapTokenizationStatus(status as TokenizationStatus), + }; + } + + /** + * Resume provisioning for a card that requires activation (Yellow Path) + * + * On Android, if a card was previously added but requires additional + * verification, this method resumes the provisioning flow. + */ + async resumeProvisioning( + tokenReferenceId: string, + cardNetwork: string, + cardholderName?: string, + lastFourDigits?: string, + ): Promise { + if (Platform.OS !== 'android') { + return { + status: 'error', + error: this.createPlatformNotSupportedError(), + }; + } + + try { + const wallet = await this.getWalletModule(); + + const resumeData: AndroidResumeCardData = { + network: cardNetwork, + tokenReferenceID: tokenReferenceId, + cardHolderName: cardholderName, + lastDigits: lastFourDigits, + }; + + const status = await wallet.resumeAddCardToGoogleWallet(resumeData); + + return { + status: mapTokenizationStatus(status), + }; + } catch (error) { + logAdapterError('GoogleWalletAdapter', 'resumeProvisioning', error); + return createErrorResult( + error, + ProvisioningErrorCode.UNKNOWN_ERROR, + strings('card.push_provisioning.error_unknown'), + ); + } + } + + /** + * List all tokens in Google Wallet + * + * Returns information about all tokenized cards. + * Useful for finding token IDs for the resume flow. + */ + async listTokens(): Promise { + try { + const wallet = await this.getWalletModule(); + const tokens = await wallet.listTokens(); + return validateTokenArray(tokens); + } catch (error) { + logAdapterError('GoogleWalletAdapter', 'listTokens', error); + return []; + } + } + + /** + * Find token ID for a card by its last four digits + * + * Helper method to find the token reference ID for resume flow. + */ + async findTokenByLastDigits( + lastFourDigits: string, + ): Promise { + const tokens = await this.listTokens(); + return tokens.find((t) => t.lastDigits === lastFourDigits) ?? null; + } +} diff --git a/app/components/UI/Card/pushProvisioning/adapters/wallet/IWalletProviderAdapter.ts b/app/components/UI/Card/pushProvisioning/adapters/wallet/IWalletProviderAdapter.ts index 3255e2ccb01..3726d4a7669 100644 --- a/app/components/UI/Card/pushProvisioning/adapters/wallet/IWalletProviderAdapter.ts +++ b/app/components/UI/Card/pushProvisioning/adapters/wallet/IWalletProviderAdapter.ts @@ -1,8 +1,7 @@ /** * Wallet Provider Adapter Interface * - * Defines the contract for mobile wallet adapters that handle - * card tokenization and provisioning to mobile wallets. + * Abstracts wallet SDK interactions for card tokenization and provisioning. */ import { PlatformOSType } from 'react-native'; @@ -16,89 +15,23 @@ import { } from '../../types'; import { TokenInfo } from './utils'; -/** - * Interface for mobile wallet provider adapters. - * - * Wallet providers handle the device-side tokenization of cards. - * This interface abstracts the wallet SDK interactions. - * - * Responsibilities: - * - Check wallet availability on device - * - Check existing card status in wallet - * - Retrieve wallet-specific data for provisioning - * - Handle the provisioning flow with the native SDK - * - Resume provisioning for cards requiring activation - * - Listen for card activation events - */ export interface IWalletProviderAdapter { - /** - * The type of wallet this adapter handles - */ readonly walletType: WalletType; - - /** - * The platform this adapter is for - */ readonly platform: PlatformOSType; - /** - * Check if the wallet is available on this device - * - * This checks if: - * - The device supports the wallet (hardware/software requirements) - * - The wallet app is installed - * - The user can add payment cards - * - * @returns Promise resolving to true if wallet is available - */ + /** Check if the wallet is available on this device */ checkAvailability(): Promise; - /** - * Get detailed wallet eligibility information - * - * This provides more details than checkAvailability(), including - * whether a specific card is already in the wallet and its status. - * - * @param lastFourDigits - Optional card last 4 digits to check for existing card - * @returns Promise resolving to wallet eligibility details - */ + /** Get detailed wallet eligibility including existing card status */ getEligibility(lastFourDigits?: string): Promise; - /** - * Check the status of a specific card in the wallet - * - * @param lastFourDigits - The last 4 digits of the card to check - * @returns Promise resolving to the card's token status - */ + /** Check the status of a specific card in the wallet */ getCardStatus(lastFourDigits: string): Promise; - /** - * Provision a card to the wallet - * - * This initiates the native provisioning flow which presents - * the wallet UI to the user. - * - * For Android/Google Wallet: - * - Uses the pre-encrypted opaque payment card - * - Calls pushTokenize on the Tap and Pay SDK - * - * @param params - The provisioning parameters including encrypted payload - * @returns Promise resolving to the provisioning result - */ + /** Initiate the native provisioning flow */ provisionCard(params: ProvisionCardParams): Promise; - /** - * Resume provisioning for a card that requires activation (Yellow Path) - * - * On Android, if a card was previously added but requires additional - * verification, this method resumes the provisioning flow. - * - * @param tokenReferenceId - The token reference ID from Google - * @param cardNetwork - The card network (e.g., 'MASTERCARD') - * @param cardholderName - Optional cardholder name - * @param lastFourDigits - Optional last 4 digits of the card - * @returns Promise resolving to the provisioning result - */ + /** Resume provisioning for a card requiring activation (Yellow Path, Android) */ resumeProvisioning?( tokenReferenceId: string, cardNetwork: string, @@ -106,25 +39,10 @@ export interface IWalletProviderAdapter { lastFourDigits?: string, ): Promise; - /** - * List all tokens in the wallet - * - * Returns information about all tokenized cards in the wallet. - * Useful for syncing card status and finding token IDs for resume flow. - * - * @returns Promise resolving to array of token information - */ + /** List all tokenized cards in the wallet */ listTokens?(): Promise; - /** - * Add a listener for card activation events - * - * The wallet SDK may emit events when a card is activated - * after provisioning (especially for Yellow Path flows). - * - * @param callback - The callback to invoke on activation events - * @returns A function to remove the listener - */ + /** Add a listener for card activation events; returns unsubscribe function */ addActivationListener( callback: (event: CardActivationEvent) => void, ): () => void; diff --git a/app/components/UI/Card/pushProvisioning/adapters/wallet/index.ts b/app/components/UI/Card/pushProvisioning/adapters/wallet/index.ts index 799e4d3f27d..f825c86b4ed 100644 --- a/app/components/UI/Card/pushProvisioning/adapters/wallet/index.ts +++ b/app/components/UI/Card/pushProvisioning/adapters/wallet/index.ts @@ -2,14 +2,8 @@ * Wallet Provider Adapters * * Exports for wallet provider adapter interfaces and implementations. - * - * NOTE: This is the base module. Platform-specific adapters - * (GoogleWalletAdapter, AppleWalletAdapter) are added in platform-specific branches. */ export type { IWalletProviderAdapter } from './IWalletProviderAdapter'; +export { GoogleWalletAdapter } from './GoogleWalletAdapter'; export type { TokenInfo } from './utils'; - -// NOTE: Platform-specific adapters are exported from platform-specific branches: -// - feat/google-in-app-provisioning: GoogleWalletAdapter -// - feat/apple-in-app-provisioning: AppleWalletAdapter diff --git a/app/components/UI/Card/pushProvisioning/adapters/wallet/utils.test.ts b/app/components/UI/Card/pushProvisioning/adapters/wallet/utils.test.ts index 0e3bb2a107a..0d7ba8f9706 100644 --- a/app/components/UI/Card/pushProvisioning/adapters/wallet/utils.test.ts +++ b/app/components/UI/Card/pushProvisioning/adapters/wallet/utils.test.ts @@ -1,11 +1,7 @@ import { mapCardStatus, mapTokenizationStatus, - isValidCardStatus, - isValidTokenizationStatus, - isValidTokenInfo, validateTokenArray, - createProvisioningError, createErrorResult, logAdapterError, } from './utils'; @@ -23,42 +19,6 @@ describe('Wallet Adapter Utils', () => { jest.clearAllMocks(); }); - describe('isValidCardStatus', () => { - it('returns true for valid card statuses', () => { - expect(isValidCardStatus('not found')).toBe(true); - expect(isValidCardStatus('active')).toBe(true); - expect(isValidCardStatus('pending')).toBe(true); - expect(isValidCardStatus('suspended')).toBe(true); - expect(isValidCardStatus('deactivated')).toBe(true); - expect(isValidCardStatus('requireActivation')).toBe(true); - }); - - it('returns false for invalid card statuses', () => { - expect(isValidCardStatus('invalid')).toBe(false); - expect(isValidCardStatus('')).toBe(false); - expect(isValidCardStatus(null)).toBe(false); - expect(isValidCardStatus(undefined)).toBe(false); - expect(isValidCardStatus(123)).toBe(false); - expect(isValidCardStatus({})).toBe(false); - }); - }); - - describe('isValidTokenizationStatus', () => { - it('returns true for valid tokenization statuses', () => { - expect(isValidTokenizationStatus('success')).toBe(true); - expect(isValidTokenizationStatus('canceled')).toBe(true); - expect(isValidTokenizationStatus('error')).toBe(true); - }); - - it('returns false for invalid tokenization statuses', () => { - expect(isValidTokenizationStatus('invalid')).toBe(false); - expect(isValidTokenizationStatus('')).toBe(false); - expect(isValidTokenizationStatus(null)).toBe(false); - expect(isValidTokenizationStatus(undefined)).toBe(false); - expect(isValidTokenizationStatus(123)).toBe(false); - }); - }); - describe('mapCardStatus', () => { it('maps valid card statuses correctly', () => { expect(mapCardStatus('not found')).toBe('not_found'); @@ -104,49 +64,6 @@ describe('Wallet Adapter Utils', () => { }); }); - describe('isValidTokenInfo', () => { - it('returns true for valid token info', () => { - expect( - isValidTokenInfo({ - identifier: 'token-123', - lastDigits: '1234', - tokenState: 1, - }), - ).toBe(true); - }); - - it('returns false for invalid token info', () => { - expect(isValidTokenInfo(null)).toBe(false); - expect(isValidTokenInfo(undefined)).toBe(false); - expect(isValidTokenInfo({})).toBe(false); - expect(isValidTokenInfo({ identifier: 'test' })).toBe(false); - expect(isValidTokenInfo({ identifier: 'test', lastDigits: '1234' })).toBe( - false, - ); - expect( - isValidTokenInfo({ - identifier: 123, // wrong type - lastDigits: '1234', - tokenState: 1, - }), - ).toBe(false); - expect( - isValidTokenInfo({ - identifier: 'test', - lastDigits: 1234, // wrong type - tokenState: 1, - }), - ).toBe(false); - expect( - isValidTokenInfo({ - identifier: 'test', - lastDigits: '1234', - tokenState: '1', // wrong type - }), - ).toBe(false); - }); - }); - describe('validateTokenArray', () => { it('returns valid tokens from array', () => { const tokens = [ @@ -182,79 +99,26 @@ describe('Wallet Adapter Utils', () => { }); }); - describe('createProvisioningError', () => { + describe('createErrorResult', () => { it('returns existing ProvisioningError unchanged', () => { const existingError = new ProvisioningError( ProvisioningErrorCode.WALLET_NOT_AVAILABLE, 'Wallet not available', ); - expect(createProvisioningError(existingError)).toBe(existingError); - }); - - it('wraps Error with default code', () => { - const error = new Error('Something went wrong'); - const result = createProvisioningError(error); - - expect(result).toBeInstanceOf(ProvisioningError); - expect(result.code).toBe(ProvisioningErrorCode.UNKNOWN_ERROR); - expect(result.message).toBe('Something went wrong'); - expect(result.originalError).toBe(error); - }); - - it('wraps Error with custom code', () => { - const error = new Error('Invalid card'); - const result = createProvisioningError( - error, - ProvisioningErrorCode.INVALID_CARD_DATA, - ); - - expect(result.code).toBe(ProvisioningErrorCode.INVALID_CARD_DATA); - }); - - it('creates error from non-Error with default message', () => { - const result = createProvisioningError('string error'); - - expect(result).toBeInstanceOf(ProvisioningError); - // Non-Error values use the default message 'An unknown error occurred' - expect(result.message).toBe('An unknown error occurred'); - expect(result.originalError).toBeUndefined(); - }); + const result = createErrorResult(existingError); - it('uses custom default message for non-Error', () => { - const result = createProvisioningError( - null, - ProvisioningErrorCode.UNKNOWN_ERROR, - 'Custom message', - ); - - expect(result.message).toBe('Custom message'); - }); - - it('prefers defaultMessage over Error.message for user-facing errors', () => { - const error = new Error('PKPassKitErrorDomain error 2'); - const result = createProvisioningError( - error, - ProvisioningErrorCode.UNKNOWN_ERROR, - 'Something went wrong. Please try again.', - ); - - expect(result).toBeInstanceOf(ProvisioningError); - expect(result.code).toBe(ProvisioningErrorCode.UNKNOWN_ERROR); - // defaultMessage takes precedence to avoid exposing raw SDK errors - expect(result.message).toBe('Something went wrong. Please try again.'); - // Original error is preserved for debugging - expect(result.originalError).toBe(error); + expect(result.status).toBe('error'); + expect(result.error).toBe(existingError); }); - }); - describe('createErrorResult', () => { - it('creates error result from Error', () => { + it('wraps Error with default code', () => { const error = new Error('Test error'); const result = createErrorResult(error); expect(result.status).toBe('error'); expect(result.error).toBeInstanceOf(ProvisioningError); expect(result.error?.message).toBe('Test error'); + expect(result.error?.code).toBe(ProvisioningErrorCode.UNKNOWN_ERROR); }); it('creates error result with custom code and message', () => { @@ -270,6 +134,29 @@ describe('Wallet Adapter Utils', () => { ); expect(result.error?.message).toBe('Wallet not found'); }); + + it('prefers defaultMessage over Error.message for user-facing errors', () => { + const error = new Error('PKPassKitErrorDomain error 2'); + const result = createErrorResult( + error, + ProvisioningErrorCode.UNKNOWN_ERROR, + 'Something went wrong. Please try again.', + ); + + expect(result.status).toBe('error'); + expect(result.error?.message).toBe( + 'Something went wrong. Please try again.', + ); + expect(result.error?.originalError).toBe(error); + }); + + it('creates error from non-Error with default message', () => { + const result = createErrorResult('string error'); + + expect(result.status).toBe('error'); + expect(result.error?.message).toBe('An unknown error occurred'); + expect(result.error?.originalError).toBeUndefined(); + }); }); describe('logAdapterError', () => { diff --git a/app/components/UI/Card/pushProvisioning/adapters/wallet/utils.ts b/app/components/UI/Card/pushProvisioning/adapters/wallet/utils.ts index 6e3ff243e74..925017661eb 100644 --- a/app/components/UI/Card/pushProvisioning/adapters/wallet/utils.ts +++ b/app/components/UI/Card/pushProvisioning/adapters/wallet/utils.ts @@ -12,13 +12,7 @@ import { } from '../../types'; import Logger from '../../../../../../util/Logger'; -// ============================================================================ -// Types from react-native-wallet library -// ============================================================================ - -/** - * Card status values from react-native-wallet library - */ +/** Card status values from react-native-wallet library */ export type RNWalletCardStatus = | 'not found' | 'active' @@ -27,73 +21,18 @@ export type RNWalletCardStatus = | 'deactivated' | 'requireActivation'; -/** - * Tokenization status values from react-native-wallet library - */ +/** Tokenization status values from react-native-wallet library */ export type TokenizationStatus = 'success' | 'canceled' | 'error'; -/** - * Token info from react-native-wallet listTokens - */ +/** Token info from react-native-wallet listTokens */ export interface TokenInfo { identifier: string; lastDigits: string; tokenState: number; } -// ============================================================================ -// Status Mapping Functions -// ============================================================================ - -const VALID_CARD_STATUSES: RNWalletCardStatus[] = [ - 'not found', - 'active', - 'pending', - 'suspended', - 'deactivated', - 'requireActivation', -]; - -const VALID_TOKENIZATION_STATUSES: TokenizationStatus[] = [ - 'success', - 'canceled', - 'error', -]; - -/** - * Validate that a value is a valid RNWalletCardStatus - */ -export function isValidCardStatus( - status: unknown, -): status is RNWalletCardStatus { - return ( - typeof status === 'string' && - VALID_CARD_STATUSES.includes(status as RNWalletCardStatus) - ); -} - -/** - * Validate that a value is a valid TokenizationStatus - */ -export function isValidTokenizationStatus( - status: unknown, -): status is TokenizationStatus { - return ( - typeof status === 'string' && - VALID_TOKENIZATION_STATUSES.includes(status as TokenizationStatus) - ); -} - -/** - * Map react-native-wallet card status to our CardTokenStatus type - * Includes runtime validation - */ +/** Map react-native-wallet card status to our CardTokenStatus type */ export function mapCardStatus(status: unknown): CardTokenStatus { - if (!isValidCardStatus(status)) { - Logger.log('mapCardStatus: Invalid status received', { status }); - return 'not_found'; - } - switch (status) { case 'not found': return 'not_found'; @@ -108,22 +47,15 @@ export function mapCardStatus(status: unknown): CardTokenStatus { case 'requireActivation': return 'requires_activation'; default: + Logger.log('mapCardStatus: Invalid status received', { status }); return 'not_found'; } } -/** - * Map tokenization status to our ProvisioningResult status - * Includes runtime validation - */ +/** Map tokenization status to our ProvisioningResult status */ export function mapTokenizationStatus( status: unknown, ): ProvisioningResult['status'] { - if (!isValidTokenizationStatus(status)) { - Logger.log('mapTokenizationStatus: Invalid status received', { status }); - return 'error'; - } - switch (status) { case 'success': return 'success'; @@ -132,59 +64,36 @@ export function mapTokenizationStatus( case 'error': return 'error'; default: + Logger.log('mapTokenizationStatus: Invalid status received', { status }); return 'error'; } } -// ============================================================================ -// Error Handling Utilities -// ============================================================================ - -/** - * Create a standardized ProvisioningError from an unknown error - * - * When a defaultMessage is provided, it is used as the user-facing message - * to avoid exposing raw SDK error details to end users. The original error - * is preserved for debugging purposes but should be logged to Sentry separately. - */ -export function createProvisioningError( +/** Create a standardized error result for wallet adapter methods */ +export function createErrorResult( error: unknown, defaultCode: ProvisioningErrorCode = ProvisioningErrorCode.UNKNOWN_ERROR, defaultMessage?: string, -): ProvisioningError { +): ProvisioningResult { if (error instanceof ProvisioningError) { - return error; + return { status: 'error', error }; } - // Prefer defaultMessage for user-facing errors to avoid exposing raw SDK errors - // The original error details are logged to Sentry via logAdapterError const message = defaultMessage ?? (error instanceof Error ? error.message : 'An unknown error occurred'); - const originalError = error instanceof Error ? error : undefined; - return new ProvisioningError(defaultCode, message, originalError); -} - -/** - * Create a standardized error result for wallet adapter methods - */ -export function createErrorResult( - error: unknown, - defaultCode: ProvisioningErrorCode = ProvisioningErrorCode.UNKNOWN_ERROR, - defaultMessage?: string, -): ProvisioningResult { return { status: 'error', - error: createProvisioningError(error, defaultCode, defaultMessage), + error: new ProvisioningError(defaultCode, message, originalError), }; } /** * Log an error with standardized format and send to Sentry * - * This logs native SDK errors (like PKPassKitErrorDomain) to Sentry for debugging + * Logs native SDK errors (like PKPassKitErrorDomain) to Sentry for debugging * while keeping them out of user-facing error messages. */ export function logAdapterError( @@ -196,13 +105,11 @@ export function logAdapterError( const errorCode = error instanceof ProvisioningError ? error.code : 'NATIVE_SDK_ERROR'; - // Create an Error object for Sentry if not already one const errorForSentry = error instanceof Error ? error : new Error(`${adapterName}.${methodName}: ${errorMessage}`); - // Log to Sentry with searchable tags and context Logger.error(errorForSentry, { tags: { feature: 'push_provisioning', @@ -222,14 +129,8 @@ export function logAdapterError( }); } -// ============================================================================ -// Validation Utilities -// ============================================================================ - -/** - * Validate TokenInfo from listTokens - */ -export function isValidTokenInfo(token: unknown): token is TokenInfo { +/** Validate TokenInfo from listTokens */ +function isValidTokenInfo(token: unknown): token is TokenInfo { if (!token || typeof token !== 'object') { return false; } @@ -242,9 +143,7 @@ export function isValidTokenInfo(token: unknown): token is TokenInfo { ); } -/** - * Validate and filter token array from listTokens - */ +/** Validate and filter token array from listTokens */ export function validateTokenArray(tokens: unknown): TokenInfo[] { if (!Array.isArray(tokens)) { Logger.log('validateTokenArray: Expected array, got', { diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/AddToWalletButton.test.tsx b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/AddToWalletButton.test.tsx new file mode 100644 index 00000000000..bf58a1975e2 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/AddToWalletButton.test.tsx @@ -0,0 +1,397 @@ +import React from 'react'; +import { Platform } from 'react-native'; +import { render, fireEvent } from '@testing-library/react-native'; + +jest.mock('react-native/Libraries/Components/Touchable/TouchableOpacity', () => + jest.requireActual( + 'react-native/Libraries/Components/Touchable/TouchableOpacity', + ), +); + +const mockNativeAddToWalletButton = jest.fn( + (_props: Record) => null, +); +jest.mock( + '@expensify/react-native-wallet', + () => ({ + AddToWalletButton: (props: Record) => { + mockNativeAddToWalletButton(props); + return null; + }, + }), + { virtual: true }, +); + +import AddToWalletButton, { + getGoogleWalletButtonSvg, + GOOGLE_WALLET_BUTTON_BY_REGION, + GOOGLE_WALLET_BUTTON_BY_LANGUAGE, +} from './AddToWalletButton'; + +const makeMockSvg = (id: string) => + Object.assign(() => null, { displayName: `GWB_${id}` }); + +const regionEntries: Record = { + en_au: 'enAU', + en_ca: 'enCA', + en_gb: 'enGB', + en_in: 'enIN', + en_sg: 'enSG', + en_us: 'enUS', + en_za: 'enZA', + es_419: 'es419', + es_es: 'esES', + es_us: 'esUS', + fr_ca: 'frCA', + fr_fr: 'frFR', + zh_hk: 'zhHK', + zh_tw: 'zhTW', +}; + +Object.entries(regionEntries).forEach(([key, label]) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (GOOGLE_WALLET_BUTTON_BY_REGION as any)[key] = makeMockSvg(label); +}); + +const languageEntries: Record = { + af: 'af', + am: 'am', + ar: 'ar', + az: 'az', + be: 'by', + bg: 'bg', + bn: 'bn', + br: 'br', + bs: 'bs', + by: 'by', + ca: 'ca', + cs: 'cz', + cz: 'cz', + da: 'dk', + de: 'de', + dk: 'dk', + el: 'gr', + en: 'enUS', + es: 'es419', + et: 'et', + fa: 'fa', + fl: 'fl', + fp: 'fp', + fr: 'frFR', + gr: 'gr', + he: 'he', + hi: 'enUS', + hr: 'hr', + hu: 'hu', + hy: 'hy', + id: 'id', + is: 'is', + it: 'it', + ja: 'jp', + jp: 'jp', + ka: 'ka', + kh: 'kh', + kk: 'kk', + km: 'kh', + ko: 'enUS', + ky: 'ky', + lo: 'lo', + lt: 'lt', + lv: 'lv', + mk: 'mk', + mn: 'mn', + my: 'my', + ne: 'ne', + nl: 'nl', + no: 'no', + pl: 'pl', + pt: 'pt', + ro: 'ro', + ru: 'ru', + se: 'se', + si: 'si', + sk: 'sk', + sl: 'sl', + sq: 'sq', + sr: 'sr', + sv: 'se', + sw: 'sw', + th: 'th', + tl: 'fl', + tr: 'tr', + uk: 'uk', + ur: 'ur', + uz: 'uz', + vi: 'vi', + zh: 'zhTW', +}; + +Object.entries(languageEntries).forEach(([key, label]) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (GOOGLE_WALLET_BUTTON_BY_LANGUAGE as any)[key] = makeMockSvg(label); +}); + +describe('AddToWalletButton', () => { + const originalPlatform = Platform.OS; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + Object.defineProperty(Platform, 'OS', { + value: originalPlatform, + writable: true, + }); + }); + + describe('iOS', () => { + beforeEach(() => { + Object.defineProperty(Platform, 'OS', { + value: 'ios', + writable: true, + }); + }); + + it('renders the native AddToWalletButton', () => { + render(); + + expect(mockNativeAddToWalletButton).toHaveBeenCalledWith( + expect.objectContaining({ + buttonStyle: 'blackOutline', + buttonType: 'basic', + borderRadius: 4, + }), + ); + }); + + it('passes onPress to the native button', () => { + const onPress = jest.fn(); + render(); + + expect(mockNativeAddToWalletButton).toHaveBeenCalledWith( + expect.objectContaining({ onPress }), + ); + }); + + it('passes custom buttonStyle to the native button', () => { + render(); + + expect(mockNativeAddToWalletButton).toHaveBeenCalledWith( + expect.objectContaining({ buttonStyle: 'black' }), + ); + }); + }); + + describe('Android', () => { + beforeEach(() => { + Object.defineProperty(Platform, 'OS', { + value: 'android', + writable: true, + }); + }); + + it('does not render the native button', () => { + render(); + expect(mockNativeAddToWalletButton).not.toHaveBeenCalled(); + }); + + it('calls onPress when the button is tapped', () => { + const onPress = jest.fn(); + const { getByRole } = render(); + fireEvent.press(getByRole('button')); + expect(onPress).toHaveBeenCalledTimes(1); + }); + + it('has correct accessibility attributes', () => { + const { getByRole } = render(); + const button = getByRole('button'); + expect(button.props.accessibilityLabel).toBe('Add to Google Wallet'); + }); + }); + + describe('getGoogleWalletButtonSvg', () => { + describe('exact region match', () => { + it.each([ + ['en-AU', 'enAU'], + ['en-CA', 'enCA'], + ['en-GB', 'enGB'], + ['en-IN', 'enIN'], + ['en-SG', 'enSG'], + ['en-US', 'enUS'], + ['en-ZA', 'enZA'], + ['es-ES', 'esES'], + ['es-US', 'esUS'], + ['fr-CA', 'frCA'], + ['fr-FR', 'frFR'], + ['zh-HK', 'zhHK'], + ['zh-TW', 'zhTW'], + ])('resolves "%s" to %s', (locale, expected) => { + expect(getGoogleWalletButtonSvg(locale).displayName).toBe( + `GWB_${expected}`, + ); + }); + + it('handles underscore separator (zh_TW)', () => { + expect(getGoogleWalletButtonSvg('zh_TW').displayName).toBe('GWB_zhTW'); + }); + }); + + describe('base language fallback', () => { + it.each([ + ['af', 'af'], + ['am', 'am'], + ['ar', 'ar'], + ['az', 'az'], + ['bg', 'bg'], + ['bn', 'bn'], + ['bs', 'bs'], + ['ca', 'ca'], + ['de', 'de'], + ['el', 'gr'], + ['en', 'enUS'], + ['es', 'es419'], + ['et', 'et'], + ['fa', 'fa'], + ['fr', 'frFR'], + ['he', 'he'], + ['hr', 'hr'], + ['hu', 'hu'], + ['hy', 'hy'], + ['id', 'id'], + ['is', 'is'], + ['it', 'it'], + ['ja', 'jp'], + ['ka', 'ka'], + ['kk', 'kk'], + ['ky', 'ky'], + ['lo', 'lo'], + ['lt', 'lt'], + ['lv', 'lv'], + ['mk', 'mk'], + ['mn', 'mn'], + ['my', 'my'], + ['ne', 'ne'], + ['nl', 'nl'], + ['no', 'no'], + ['pl', 'pl'], + ['pt', 'pt'], + ['ro', 'ro'], + ['ru', 'ru'], + ['si', 'si'], + ['sk', 'sk'], + ['sl', 'sl'], + ['sq', 'sq'], + ['sr', 'sr'], + ['sw', 'sw'], + ['th', 'th'], + ['tl', 'fl'], + ['tr', 'tr'], + ['uk', 'uk'], + ['ur', 'ur'], + ['uz', 'uz'], + ['vi', 'vi'], + ['zh', 'zhTW'], + ])('resolves "%s" to %s', (locale, expected) => { + expect(getGoogleWalletButtonSvg(locale).displayName).toBe( + `GWB_${expected}`, + ); + }); + }); + + describe('ISO aliases', () => { + it.each([ + ['cs', 'cz'], + ['da', 'dk'], + ['sv', 'se'], + ['be', 'by'], + ['km', 'kh'], + ])('maps "%s" to %s', (locale, expected) => { + expect(getGoogleWalletButtonSvg(locale).displayName).toBe( + `GWB_${expected}`, + ); + }); + }); + + describe('fallback to English US', () => { + it('falls back for unsupported locale "xx"', () => { + const result = getGoogleWalletButtonSvg('xx'); + expect(result).toBeTruthy(); + }); + + it('falls back when locale is undefined', () => { + const result = getGoogleWalletButtonSvg(undefined); + expect(result).toBeTruthy(); + }); + + it.each([ + ['pt-BR', 'pt'], + ['de-AT', 'de'], + ])( + 'uses base language for "%s" (no region match)', + (locale, expected) => { + expect(getGoogleWalletButtonSvg(locale).displayName).toBe( + `GWB_${expected}`, + ); + }, + ); + }); + }); + + describe('locale maps completeness', () => { + it('has all expected region keys', () => { + const expected = [ + 'en_au', + 'en_ca', + 'en_gb', + 'en_in', + 'en_sg', + 'en_us', + 'en_za', + 'es_419', + 'es_es', + 'es_us', + 'fr_ca', + 'fr_fr', + 'zh_hk', + 'zh_tw', + ]; + expected.forEach((r) => { + expect(GOOGLE_WALLET_BUTTON_BY_REGION).toHaveProperty(r); + }); + }); + + it('covers all MetaMask app-supported locales', () => { + const appLocales = [ + 'de', + 'el', + 'en', + 'es', + 'fr', + 'id', + 'ja', + 'pt', + 'ru', + 'tl', + 'tr', + 'vi', + 'zh', + ]; + appLocales.forEach((l) => { + expect(GOOGLE_WALLET_BUTTON_BY_LANGUAGE).toHaveProperty(l); + }); + }); + + it('every region entry is a function', () => { + Object.values(GOOGLE_WALLET_BUTTON_BY_REGION).forEach((svg) => { + expect(typeof svg).toBe('function'); + }); + }); + + it('every language entry is a function', () => { + Object.values(GOOGLE_WALLET_BUTTON_BY_LANGUAGE).forEach((svg) => { + expect(typeof svg).toBe('function'); + }); + }); + }); +}); diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/AddToWalletButton.tsx b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/AddToWalletButton.tsx new file mode 100644 index 00000000000..9e628d737be --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/AddToWalletButton.tsx @@ -0,0 +1,253 @@ +import React, { useMemo } from 'react'; +import { + Platform, + TouchableOpacity, + type GestureResponderEvent, +} from 'react-native'; +import type { SvgProps } from 'react-native-svg'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import I18n from 'react-native-i18n'; + +type SvgComponent = React.FC; + +import GoogleWalletButtonAf from './assets/google-wallet-button-af.svg'; +import GoogleWalletButtonAm from './assets/google-wallet-button-am.svg'; +import GoogleWalletButtonAr from './assets/google-wallet-button-ar.svg'; +import GoogleWalletButtonAz from './assets/google-wallet-button-az.svg'; +import GoogleWalletButtonBg from './assets/google-wallet-button-bg.svg'; +import GoogleWalletButtonBn from './assets/google-wallet-button-bn.svg'; +import GoogleWalletButtonBr from './assets/google-wallet-button-br.svg'; +import GoogleWalletButtonBs from './assets/google-wallet-button-bs.svg'; +import GoogleWalletButtonBy from './assets/google-wallet-button-by.svg'; +import GoogleWalletButtonCa from './assets/google-wallet-button-ca.svg'; +import GoogleWalletButtonCz from './assets/google-wallet-button-cz.svg'; +import GoogleWalletButtonDe from './assets/google-wallet-button-de.svg'; +import GoogleWalletButtonDk from './assets/google-wallet-button-dk.svg'; +import GoogleWalletButtonEnAU from './assets/google-wallet-button-enAU.svg'; +import GoogleWalletButtonEnCA from './assets/google-wallet-button-enCA.svg'; +import GoogleWalletButtonEnGB from './assets/google-wallet-button-enGB.svg'; +import GoogleWalletButtonEnIN from './assets/google-wallet-button-enIN.svg'; +import GoogleWalletButtonEnSG from './assets/google-wallet-button-enSG.svg'; +import GoogleWalletButtonEnUS from './assets/google-wallet-button-enUS.svg'; +import GoogleWalletButtonEnZA from './assets/google-wallet-button-enZA.svg'; +import GoogleWalletButtonEs419 from './assets/google-wallet-button-es419.svg'; +import GoogleWalletButtonEsES from './assets/google-wallet-button-esES.svg'; +import GoogleWalletButtonEsUS from './assets/google-wallet-button-esUS.svg'; +import GoogleWalletButtonEt from './assets/google-wallet-button-et.svg'; +import GoogleWalletButtonFa from './assets/google-wallet-button-fa.svg'; +import GoogleWalletButtonFl from './assets/google-wallet-button-fl.svg'; +import GoogleWalletButtonFp from './assets/google-wallet-button-fp.svg'; +import GoogleWalletButtonFrCA from './assets/google-wallet-button-frCA.svg'; +import GoogleWalletButtonFrFR from './assets/google-wallet-button-frFR.svg'; +import GoogleWalletButtonGr from './assets/google-wallet-button-gr.svg'; +import GoogleWalletButtonHe from './assets/google-wallet-button-he.svg'; +import GoogleWalletButtonHr from './assets/google-wallet-button-hr.svg'; +import GoogleWalletButtonHu from './assets/google-wallet-button-hu.svg'; +import GoogleWalletButtonHy from './assets/google-wallet-button-hy.svg'; +import GoogleWalletButtonId from './assets/google-wallet-button-id.svg'; +import GoogleWalletButtonIs from './assets/google-wallet-button-is.svg'; +import GoogleWalletButtonIt from './assets/google-wallet-button-it.svg'; +import GoogleWalletButtonJp from './assets/google-wallet-button-jp.svg'; +import GoogleWalletButtonKa from './assets/google-wallet-button-ka.svg'; +import GoogleWalletButtonKh from './assets/google-wallet-button-kh.svg'; +import GoogleWalletButtonKk from './assets/google-wallet-button-kk.svg'; +import GoogleWalletButtonKy from './assets/google-wallet-button-ky.svg'; +import GoogleWalletButtonLo from './assets/google-wallet-button-lo.svg'; +import GoogleWalletButtonLt from './assets/google-wallet-button-lt.svg'; +import GoogleWalletButtonLv from './assets/google-wallet-button-lv.svg'; +import GoogleWalletButtonMk from './assets/google-wallet-button-mk.svg'; +import GoogleWalletButtonMn from './assets/google-wallet-button-mn.svg'; +import GoogleWalletButtonMy from './assets/google-wallet-button-my.svg'; +import GoogleWalletButtonNe from './assets/google-wallet-button-ne.svg'; +import GoogleWalletButtonNl from './assets/google-wallet-button-nl.svg'; +import GoogleWalletButtonNo from './assets/google-wallet-button-no.svg'; +import GoogleWalletButtonPl from './assets/google-wallet-button-pl.svg'; +import GoogleWalletButtonPt from './assets/google-wallet-button-pt.svg'; +import GoogleWalletButtonRo from './assets/google-wallet-button-ro.svg'; +import GoogleWalletButtonRu from './assets/google-wallet-button-ru.svg'; +import GoogleWalletButtonSe from './assets/google-wallet-button-se.svg'; +import GoogleWalletButtonSi from './assets/google-wallet-button-si.svg'; +import GoogleWalletButtonSk from './assets/google-wallet-button-sk.svg'; +import GoogleWalletButtonSl from './assets/google-wallet-button-sl.svg'; +import GoogleWalletButtonSq from './assets/google-wallet-button-sq.svg'; +import GoogleWalletButtonSr from './assets/google-wallet-button-sr.svg'; +import GoogleWalletButtonSw from './assets/google-wallet-button-sw.svg'; +import GoogleWalletButtonTh from './assets/google-wallet-button-th.svg'; +import GoogleWalletButtonTr from './assets/google-wallet-button-tr.svg'; +import GoogleWalletButtonUk from './assets/google-wallet-button-uk.svg'; +import GoogleWalletButtonUr from './assets/google-wallet-button-ur.svg'; +import GoogleWalletButtonUz from './assets/google-wallet-button-uz.svg'; +import GoogleWalletButtonVi from './assets/google-wallet-button-vi.svg'; +import GoogleWalletButtonZhHK from './assets/google-wallet-button-zhHK.svg'; +import GoogleWalletButtonZhTW from './assets/google-wallet-button-zhTW.svg'; + +export const GOOGLE_WALLET_BUTTON_BY_REGION: Record = { + en_au: GoogleWalletButtonEnAU, + en_ca: GoogleWalletButtonEnCA, + en_gb: GoogleWalletButtonEnGB, + en_in: GoogleWalletButtonEnIN, + en_sg: GoogleWalletButtonEnSG, + en_us: GoogleWalletButtonEnUS, + en_za: GoogleWalletButtonEnZA, + es_419: GoogleWalletButtonEs419, + es_es: GoogleWalletButtonEsES, + es_us: GoogleWalletButtonEsUS, + fr_ca: GoogleWalletButtonFrCA, + fr_fr: GoogleWalletButtonFrFR, + zh_hk: GoogleWalletButtonZhHK, + zh_tw: GoogleWalletButtonZhTW, +}; + +export const GOOGLE_WALLET_BUTTON_BY_LANGUAGE: Record = { + af: GoogleWalletButtonAf, + am: GoogleWalletButtonAm, + ar: GoogleWalletButtonAr, + az: GoogleWalletButtonAz, + be: GoogleWalletButtonBy, + bg: GoogleWalletButtonBg, + bn: GoogleWalletButtonBn, + br: GoogleWalletButtonBr, + bs: GoogleWalletButtonBs, + by: GoogleWalletButtonBy, + ca: GoogleWalletButtonCa, + cs: GoogleWalletButtonCz, + cz: GoogleWalletButtonCz, + da: GoogleWalletButtonDk, + de: GoogleWalletButtonDe, + dk: GoogleWalletButtonDk, + el: GoogleWalletButtonGr, + en: GoogleWalletButtonEnUS, + es: GoogleWalletButtonEs419, + et: GoogleWalletButtonEt, + fa: GoogleWalletButtonFa, + fl: GoogleWalletButtonFl, + fp: GoogleWalletButtonFp, + fr: GoogleWalletButtonFrFR, + gr: GoogleWalletButtonGr, + he: GoogleWalletButtonHe, + hi: GoogleWalletButtonEnUS, + hr: GoogleWalletButtonHr, + hu: GoogleWalletButtonHu, + hy: GoogleWalletButtonHy, + id: GoogleWalletButtonId, + is: GoogleWalletButtonIs, + it: GoogleWalletButtonIt, + ja: GoogleWalletButtonJp, + jp: GoogleWalletButtonJp, + ka: GoogleWalletButtonKa, + kh: GoogleWalletButtonKh, + kk: GoogleWalletButtonKk, + km: GoogleWalletButtonKh, + ko: GoogleWalletButtonEnUS, + ky: GoogleWalletButtonKy, + lo: GoogleWalletButtonLo, + lt: GoogleWalletButtonLt, + lv: GoogleWalletButtonLv, + mk: GoogleWalletButtonMk, + mn: GoogleWalletButtonMn, + my: GoogleWalletButtonMy, + ne: GoogleWalletButtonNe, + nl: GoogleWalletButtonNl, + no: GoogleWalletButtonNo, + pl: GoogleWalletButtonPl, + pt: GoogleWalletButtonPt, + ro: GoogleWalletButtonRo, + ru: GoogleWalletButtonRu, + se: GoogleWalletButtonSe, + si: GoogleWalletButtonSi, + sk: GoogleWalletButtonSk, + sl: GoogleWalletButtonSl, + sq: GoogleWalletButtonSq, + sr: GoogleWalletButtonSr, + sv: GoogleWalletButtonSe, + sw: GoogleWalletButtonSw, + th: GoogleWalletButtonTh, + tl: GoogleWalletButtonFl, + tr: GoogleWalletButtonTr, + uk: GoogleWalletButtonUk, + ur: GoogleWalletButtonUr, + uz: GoogleWalletButtonUz, + vi: GoogleWalletButtonVi, + zh: GoogleWalletButtonZhTW, +}; + +/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports */ +const NativeAddToWalletButton: React.ComponentType | null = + Platform.OS === 'ios' + ? require('@expensify/react-native-wallet').AddToWalletButton + : null; + +const ANDROID_BUTTON_WIDTH = 300; +const ANDROID_BUTTON_HEIGHT = 48; + +interface AddToWalletButtonProps { + onPress?: (e: GestureResponderEvent) => void; + buttonStyle?: 'black' | 'blackOutline'; + buttonType?: 'basic' | 'badge'; + borderRadius?: number; + testID?: string; +} + +export function getGoogleWalletButtonSvg( + localeOverride?: string, +): SvgComponent { + const locale: string = localeOverride ?? I18n.locale ?? 'en'; + const normalized = locale.replace('-', '_').toLowerCase(); + + const regionMatch = GOOGLE_WALLET_BUTTON_BY_REGION[normalized]; + if (regionMatch) { + return regionMatch; + } + + const baseLanguage = normalized.split('_')[0]; + const languageMatch = GOOGLE_WALLET_BUTTON_BY_LANGUAGE[baseLanguage]; + if (languageMatch) { + return languageMatch; + } + + return GoogleWalletButtonEnUS; +} + +const AddToWalletButton: React.FC = ({ + onPress, + buttonStyle = 'blackOutline', + buttonType = 'basic', + borderRadius = 4, + testID, +}) => { + const locale: string = I18n.locale ?? 'en'; + const GoogleWalletSvg = useMemo( + () => getGoogleWalletButtonSvg(locale), + [locale], + ); + + if (Platform.OS === 'ios' && NativeAddToWalletButton) { + return ( + + ); + } + + return ( + + + + ); +}; + +export default AddToWalletButton; diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-af.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-af.svg new file mode 100644 index 00000000000..ad955fb487e --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-af.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-am.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-am.svg new file mode 100644 index 00000000000..ca47bb1bcb9 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-am.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ar.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ar.svg new file mode 100644 index 00000000000..1e4694e38f3 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ar.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-az.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-az.svg new file mode 100644 index 00000000000..a2cd6f23375 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-az.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-bg.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-bg.svg new file mode 100644 index 00000000000..0d4e312882b --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-bg.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-bn.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-bn.svg new file mode 100644 index 00000000000..78289380547 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-bn.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-br.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-br.svg new file mode 100644 index 00000000000..fdca8fc6667 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-br.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-bs.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-bs.svg new file mode 100644 index 00000000000..73631d73071 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-bs.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-by.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-by.svg new file mode 100644 index 00000000000..f9ef11fe286 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-by.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ca.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ca.svg new file mode 100644 index 00000000000..17af76a3abe --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ca.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-cz.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-cz.svg new file mode 100644 index 00000000000..ca4f0beca3e --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-cz.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-de.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-de.svg new file mode 100644 index 00000000000..e3149c5003a --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-de.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-dk.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-dk.svg new file mode 100644 index 00000000000..c41594c49dd --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-dk.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-enAU.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-enAU.svg new file mode 100644 index 00000000000..a5d3387a7fd --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-enAU.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-enCA.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-enCA.svg new file mode 100644 index 00000000000..6bb3998a139 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-enCA.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-enGB.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-enGB.svg new file mode 100644 index 00000000000..e71c60a9469 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-enGB.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-enIN.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-enIN.svg new file mode 100644 index 00000000000..5d6d7c0699f --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-enIN.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-enSG.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-enSG.svg new file mode 100644 index 00000000000..d21226fb0a5 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-enSG.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-enUS.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-enUS.svg new file mode 100644 index 00000000000..40eb150d0a4 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-enUS.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-enZA.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-enZA.svg new file mode 100644 index 00000000000..fc4ae740551 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-enZA.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-es419.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-es419.svg new file mode 100644 index 00000000000..3d979a59d16 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-es419.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-esES.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-esES.svg new file mode 100644 index 00000000000..fce59713ef2 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-esES.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-esUS.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-esUS.svg new file mode 100644 index 00000000000..2fc215ef27d --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-esUS.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-et.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-et.svg new file mode 100644 index 00000000000..1d3f2426707 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-et.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-fa.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-fa.svg new file mode 100644 index 00000000000..2361706d685 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-fa.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-fl.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-fl.svg new file mode 100644 index 00000000000..b7babf1262f --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-fl.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-fp.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-fp.svg new file mode 100644 index 00000000000..65c7bd1f1b1 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-fp.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-frCA.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-frCA.svg new file mode 100644 index 00000000000..d122b7a5afc --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-frCA.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-frFR.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-frFR.svg new file mode 100644 index 00000000000..f02b29b469e --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-frFR.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-gr.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-gr.svg new file mode 100644 index 00000000000..6e4ecb0bd06 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-gr.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-he.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-he.svg new file mode 100644 index 00000000000..d8df1497668 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-he.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-hr.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-hr.svg new file mode 100644 index 00000000000..d6ca119e136 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-hr.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-hu.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-hu.svg new file mode 100644 index 00000000000..dd9bced82f4 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-hu.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-hy.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-hy.svg new file mode 100644 index 00000000000..473508959b6 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-hy.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-id.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-id.svg new file mode 100644 index 00000000000..38669c6729b --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-id.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-is.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-is.svg new file mode 100644 index 00000000000..803d7693680 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-is.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-it.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-it.svg new file mode 100644 index 00000000000..bb91bc9fa64 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-it.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-jp.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-jp.svg new file mode 100644 index 00000000000..bf9d711e6a9 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-jp.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ka.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ka.svg new file mode 100644 index 00000000000..c705baed41d --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ka.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-kh.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-kh.svg new file mode 100644 index 00000000000..96424e26a2d --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-kh.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-kk.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-kk.svg new file mode 100644 index 00000000000..5a441e2e488 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-kk.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ky.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ky.svg new file mode 100644 index 00000000000..d799c6b0540 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ky.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-lo.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-lo.svg new file mode 100644 index 00000000000..8562dc28bfe --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-lo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-lt.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-lt.svg new file mode 100644 index 00000000000..20179bf4d36 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-lt.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-lv.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-lv.svg new file mode 100644 index 00000000000..ee3c7f9d400 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-lv.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-mk.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-mk.svg new file mode 100644 index 00000000000..037f638b13a --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-mk.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-mn.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-mn.svg new file mode 100644 index 00000000000..8c9fe9b5bc4 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-mn.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-my.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-my.svg new file mode 100644 index 00000000000..80ba9579961 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-my.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ne.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ne.svg new file mode 100644 index 00000000000..cb9735ec559 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ne.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-nl.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-nl.svg new file mode 100644 index 00000000000..f42e3f4f86d --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-nl.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-no.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-no.svg new file mode 100644 index 00000000000..56785720a41 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-no.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-pl.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-pl.svg new file mode 100644 index 00000000000..16237c80359 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-pl.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-pt.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-pt.svg new file mode 100644 index 00000000000..021f79ca752 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-pt.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ro.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ro.svg new file mode 100644 index 00000000000..8589e8bfea5 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ro.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ru.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ru.svg new file mode 100644 index 00000000000..95d188429c1 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ru.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-se.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-se.svg new file mode 100644 index 00000000000..aa936531b4c --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-se.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-si.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-si.svg new file mode 100644 index 00000000000..16d2d74d8b4 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-si.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-sk.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-sk.svg new file mode 100644 index 00000000000..8a31e57daf4 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-sk.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-sl.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-sl.svg new file mode 100644 index 00000000000..ea65a8faf6c --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-sl.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-sq.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-sq.svg new file mode 100644 index 00000000000..d63d9d89261 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-sq.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-sr.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-sr.svg new file mode 100644 index 00000000000..5b4b71e6454 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-sr.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-sw.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-sw.svg new file mode 100644 index 00000000000..d6185803368 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-sw.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-th.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-th.svg new file mode 100644 index 00000000000..dcf90ddc9e7 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-th.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-tr.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-tr.svg new file mode 100644 index 00000000000..70f40df7b50 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-tr.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-uk.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-uk.svg new file mode 100644 index 00000000000..9154fb58bd7 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-uk.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ur.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ur.svg new file mode 100644 index 00000000000..733a302dfd5 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-ur.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-uz.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-uz.svg new file mode 100644 index 00000000000..2655dc608ed --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-uz.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-vi.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-vi.svg new file mode 100644 index 00000000000..9a58a1d2555 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-vi.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-zhHK.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-zhHK.svg new file mode 100644 index 00000000000..f398a6e4d5d --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-zhHK.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-zhTW.svg b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-zhTW.svg new file mode 100644 index 00000000000..e895a25c919 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/assets/google-wallet-button-zhTW.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/index.ts b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/index.ts new file mode 100644 index 00000000000..d861cdf1310 --- /dev/null +++ b/app/components/UI/Card/pushProvisioning/components/AddToWalletButton/index.ts @@ -0,0 +1 @@ +export { default as AddToWalletButton } from './AddToWalletButton'; diff --git a/app/components/UI/Card/pushProvisioning/constants.test.ts b/app/components/UI/Card/pushProvisioning/constants.test.ts index 6ee211787b1..0cc563b5e44 100644 --- a/app/components/UI/Card/pushProvisioning/constants.test.ts +++ b/app/components/UI/Card/pushProvisioning/constants.test.ts @@ -1,5 +1,9 @@ import { Platform } from 'react-native'; -import { getWalletName } from './constants'; +import { + getWalletName, + isAccountEligibleForProvisioning, + PROVISIONING_ELIGIBLE_AFTER, +} from './constants'; describe('Push Provisioning Constants', () => { const originalPlatform = Platform.OS; @@ -40,4 +44,44 @@ describe('Push Provisioning Constants', () => { expect(getWalletName()).toBe('Google Wallet'); }); }); + + describe('isAccountEligibleForProvisioning', () => { + it('returns false for null or undefined', () => { + expect(isAccountEligibleForProvisioning(null)).toBe(false); + expect(isAccountEligibleForProvisioning(undefined)).toBe(false); + }); + + it('returns false for invalid date strings', () => { + expect(isAccountEligibleForProvisioning('')).toBe(false); + expect(isAccountEligibleForProvisioning('not-a-date')).toBe(false); + }); + + it('returns false for accounts created before November 10, 2025', () => { + expect(isAccountEligibleForProvisioning('2025-11-09T23:59:59.999Z')).toBe( + false, + ); + expect(isAccountEligibleForProvisioning('2025-09-15T12:00:00.000Z')).toBe( + false, + ); + expect(isAccountEligibleForProvisioning('2025-08-01T00:00:00.000Z')).toBe( + false, + ); + }); + + it('returns true for accounts created on or after November 10, 2025', () => { + expect(isAccountEligibleForProvisioning('2025-12-01T00:00:00.000Z')).toBe( + true, + ); + expect(isAccountEligibleForProvisioning('2025-11-11T10:30:00.000Z')).toBe( + true, + ); + expect(isAccountEligibleForProvisioning('2026-06-01T00:00:00.000Z')).toBe( + true, + ); + }); + + it('uses the PROVISIONING_ELIGIBLE_AFTER constant as cutoff', () => { + expect(PROVISIONING_ELIGIBLE_AFTER).toBe('2025-11-10T00:00:00.000Z'); + }); + }); }); diff --git a/app/components/UI/Card/pushProvisioning/constants.ts b/app/components/UI/Card/pushProvisioning/constants.ts index f56d0d7a861..45f819c6312 100644 --- a/app/components/UI/Card/pushProvisioning/constants.ts +++ b/app/components/UI/Card/pushProvisioning/constants.ts @@ -6,6 +6,12 @@ import { Platform } from 'react-native'; +/** + * Minimum account creation date for push provisioning eligibility. + * Accounts created before this date are not eligible. + */ +export const PROVISIONING_ELIGIBLE_AFTER = '2025-11-10T00:00:00.000Z'; + /** * Get the wallet name for the current platform * @@ -14,3 +20,25 @@ import { Platform } from 'react-native'; export function getWalletName(): string { return Platform.OS === 'ios' ? 'Apple Wallet' : 'Google Wallet'; } + +/** + * Check whether an account is eligible for push provisioning based on its + * creation date. Accounts created before January 2026 are not eligible. + * + * @param accountCreatedAt - ISO 8601 date string from UserResponse.createdAt + * @returns true if the account was created on or after the cutoff date + */ +export function isAccountEligibleForProvisioning( + accountCreatedAt: string | null | undefined, +): boolean { + if (!accountCreatedAt) { + return false; + } + + const createdDate = new Date(accountCreatedAt); + if (isNaN(createdDate.getTime())) { + return false; + } + + return createdDate >= new Date(PROVISIONING_ELIGIBLE_AFTER); +} diff --git a/app/components/UI/Card/pushProvisioning/hooks/usePushProvisioning.test.ts b/app/components/UI/Card/pushProvisioning/hooks/usePushProvisioning.test.ts index 923564e0a8e..077227c8130 100644 --- a/app/components/UI/Card/pushProvisioning/hooks/usePushProvisioning.test.ts +++ b/app/components/UI/Card/pushProvisioning/hooks/usePushProvisioning.test.ts @@ -106,6 +106,7 @@ describe('usePushProvisioning', () => { const defaultOptions = { cardDetails: mockCardDetails, + accountCreatedAt: '2026-02-01T00:00:00.000Z', onSuccess: jest.fn(), onError: jest.fn(), onCancel: jest.fn(), @@ -285,6 +286,38 @@ describe('usePushProvisioning', () => { unmount(); }); + it('returns false when account was created before November 10, 2025', async () => { + const { result, unmount } = renderHook(() => + usePushProvisioning({ + ...defaultOptions, + accountCreatedAt: '2025-11-09T23:59:59.999Z', + }), + ); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(result.current.canAddToWallet).toBe(false); + unmount(); + }); + + it('returns false when accountCreatedAt is null', async () => { + const { result, unmount } = renderHook(() => + usePushProvisioning({ + ...defaultOptions, + accountCreatedAt: null, + }), + ); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(result.current.canAddToWallet).toBe(false); + unmount(); + }); + it('returns false when card status is not ACTIVE', async () => { const inactiveCard = { ...mockCardDetails, status: 'INACTIVE' }; @@ -743,6 +776,74 @@ describe('usePushProvisioning', () => { unmount(); }); + it('sets status to success when service returns success directly (Apple Wallet flow)', async () => { + mockInitiateProvisioning.mockResolvedValue({ + status: 'success', + tokenId: 'token-abc', + }); + const onSuccess = jest.fn(); + + const { result, unmount } = renderHook(() => + usePushProvisioning({ ...defaultOptions, onSuccess }), + ); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + await act(async () => { + await result.current.initiateProvisioning(); + }); + + expect(result.current.status).toBe('success'); + expect(result.current.isProvisioning).toBe(false); + expect(result.current.isSuccess).toBe(true); + expect(onSuccess).toHaveBeenCalledWith({ + status: 'success', + tokenId: 'token-abc', + }); + unmount(); + }); + + it('does not double-handle success when activation listener fires after direct success', async () => { + let activationCallback: + | ((event: { status: string; tokenId?: string }) => void) + | undefined; + mockAddActivationListener.mockImplementation((callback) => { + activationCallback = callback; + return () => undefined; + }); + + mockInitiateProvisioning.mockResolvedValue({ status: 'success' }); + const onSuccess = jest.fn(); + + const { result, unmount } = renderHook(() => + usePushProvisioning({ ...defaultOptions, onSuccess }), + ); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + await act(async () => { + await result.current.initiateProvisioning(); + }); + + // Success already handled from direct return + expect(result.current.status).toBe('success'); + expect(onSuccess).toHaveBeenCalledTimes(1); + + // Simulate activation listener firing after status is already 'success' + // It should be ignored since statusRef.current is no longer 'provisioning' + await act(async () => { + activationCallback?.({ status: 'activated', tokenId: 'token-123' }); + }); + + // onSuccess should NOT be called again + expect(onSuccess).toHaveBeenCalledTimes(1); + unmount(); + }); + it('tracks analytics on cancel', async () => { mockInitiateProvisioning.mockResolvedValue({ status: 'canceled' }); @@ -828,20 +929,28 @@ describe('usePushProvisioning', () => { }); describe('computed states', () => { - it('isSuccess is true when status is success', async () => { + it('isSuccess is true when service returns success', async () => { mockInitiateProvisioning.mockResolvedValue({ status: 'success' }); + const onSuccess = jest.fn(); const { result, unmount } = renderHook(() => - usePushProvisioning(defaultOptions), + usePushProvisioning({ ...defaultOptions, onSuccess }), ); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); - // Note: success status is set via activation listener, not directly - // This test validates the computed property logic - expect(result.current.isSuccess).toBe(false); // Initially false + await act(async () => { + await result.current.initiateProvisioning(); + }); + + expect(result.current.status).toBe('success'); + expect(result.current.isSuccess).toBe(true); + expect(onSuccess).toHaveBeenCalledWith({ + status: 'success', + tokenId: undefined, + }); unmount(); }); diff --git a/app/components/UI/Card/pushProvisioning/hooks/usePushProvisioning.ts b/app/components/UI/Card/pushProvisioning/hooks/usePushProvisioning.ts index f8c1dc0c580..945e9987a09 100644 --- a/app/components/UI/Card/pushProvisioning/hooks/usePushProvisioning.ts +++ b/app/components/UI/Card/pushProvisioning/hooks/usePushProvisioning.ts @@ -19,6 +19,7 @@ import { } from '../types'; import { createPushProvisioningService, ProvisioningOptions } from '../service'; import { getCardProvider, getWalletProvider } from '../providers'; +import { isAccountEligibleForProvisioning } from '../constants'; import { MetaMetricsEvents } from '../../../../../core/Analytics'; import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics'; import { CardActions } from '../../util/metrics'; @@ -62,7 +63,14 @@ import { strings } from '../../../../../../locales/i18n'; export function usePushProvisioning( options: UsePushProvisioningOptions, ): UsePushProvisioningReturn { - const { cardDetails, userAddress, onSuccess, onError, onCancel } = options; + const { + cardDetails, + userAddress, + accountCreatedAt, + onSuccess, + onError, + onCancel, + } = options; const [status, setStatus] = useState('idle'); const [error, setError] = useState(null); @@ -94,22 +102,15 @@ export function usePushProvisioning( : false; // Create the adapters based on user location and platform - const cardAdapter = useMemo(() => { - if (isSDKLoading) { - return null; - } - if (!cardSDK) { - return null; - } - - const adapter = getCardProvider(userCardLocation, cardSDK); - return adapter; - }, [cardSDK, userCardLocation, isSDKLoading]); + const cardAdapter = useMemo( + () => + isSDKLoading || !cardSDK + ? null + : getCardProvider(userCardLocation, cardSDK), + [cardSDK, userCardLocation, isSDKLoading], + ); - const walletAdapter = useMemo(() => { - const adapter = getWalletProvider(); - return adapter; - }, []); + const walletAdapter = useMemo(() => getWalletProvider(), []); // Check wallet eligibility (async) - includes availability and canAddCard checks const [eligibility, setEligibility] = useState( @@ -341,8 +342,10 @@ export function usePushProvisioning( /** * Initiate provisioning * - * Note: Success events are handled by the activation listener (onCardActivated). - * Cancel and error events are handled here since they come directly from the SDK. + * Handles all terminal results (success, cancel, error) from the service directly. + * The activation listener is a secondary mechanism for SDKs that also emit async + * activation events (e.g. Google Wallet); it ignores events once statusRef is no + * longer 'provisioning', so there is no double-handling. */ const initiateProvisioning = useCallback(async (): Promise => { @@ -380,8 +383,13 @@ export function usePushProvisioning( setStatus('provisioning'); const result = await service.initiateProvisioning(provisioningOptions); - // Handle cancel and error - success is handled by the activation listener - if (result.status === 'canceled') { + if (result.status === 'success') { + setStatus('success'); + onSuccessRef.current?.({ + status: 'success', + tokenId: result.tokenId, + }); + } else if (result.status === 'canceled') { setStatus('idle'); trackAnalyticsEvent( MetaMetricsEvents.CARD_PUSH_PROVISIONING_CANCELED, @@ -435,23 +443,22 @@ export function usePushProvisioning( setError(null); }, []); - // Simplified availability checks - const isCardProviderAvailable = cardAdapter !== null; - const isWalletProviderAvailable = walletAdapter !== null; - const isLoading = isSDKLoading || isEligibilityCheckLoading; // Check if card is eligible (status must be 'ACTIVE') const isCardEligible = cardDetails?.status === 'ACTIVE'; + const isAccountEligible = isAccountEligibleForProvisioning(accountCreatedAt); + const canAddToWallet = isPushProvisioningFeatureEnabled && isAuthenticated && !isLoading && !!cardDetails && isCardEligible && - isCardProviderAvailable && - isWalletProviderAvailable && + isAccountEligible && + !!cardAdapter && + !!walletAdapter && eligibility?.isAvailable === true && eligibility?.canAddCard === true; diff --git a/app/components/UI/Card/pushProvisioning/index.ts b/app/components/UI/Card/pushProvisioning/index.ts index 07090a2e35b..fdc268c2e45 100644 --- a/app/components/UI/Card/pushProvisioning/index.ts +++ b/app/components/UI/Card/pushProvisioning/index.ts @@ -20,8 +20,7 @@ * import { usePushProvisioning } from '@app/components/UI/Card/pushProvisioning'; * * const { initiateProvisioning, isProvisioning, canAddToWallet } = usePushProvisioning({ - * cardId: 'card-123', - * cardholderName: 'John Doe', + * cardDetails: { id: 'card-123', holderName: 'John Doe', panLast4: '1234', status: 'active' }, * }); * ``` */ @@ -42,8 +41,7 @@ export { GalileoCardAdapter, // Wallet provider adapters type IWalletProviderAdapter, - // NOTE: Platform-specific adapters (GoogleWalletAdapter, AppleWalletAdapter) - // are exported from platform-specific branches + GoogleWalletAdapter, } from './adapters'; // Service diff --git a/app/components/UI/Card/pushProvisioning/providers.test.ts b/app/components/UI/Card/pushProvisioning/providers.test.ts index 195ed8a5eb4..c0d089e0c2e 100644 --- a/app/components/UI/Card/pushProvisioning/providers.test.ts +++ b/app/components/UI/Card/pushProvisioning/providers.test.ts @@ -1,6 +1,7 @@ import { Platform } from 'react-native'; import { getCardProvider, getWalletProvider } from './providers'; import { GalileoCardAdapter } from './adapters/card'; +import { GoogleWalletAdapter } from './adapters/wallet'; import { CardSDK } from '../sdk/CardSDK'; // Mock the adapters @@ -10,6 +11,13 @@ jest.mock('./adapters/card', () => ({ })), })); +jest.mock('./adapters/wallet', () => ({ + GoogleWalletAdapter: jest.fn().mockImplementation(() => ({ + walletType: 'google_wallet', + platform: 'android', + })), +})); + describe('Push Provisioning Providers', () => { const mockCardSDK = {} as CardSDK; const originalPlatform = Platform.OS; @@ -48,7 +56,7 @@ describe('Push Provisioning Providers', () => { }); describe('getWalletProvider', () => { - it('returns null for Android (base branch has no platform adapters)', () => { + it('returns GoogleWalletAdapter for Android', () => { Object.defineProperty(Platform, 'OS', { value: 'android', writable: true, @@ -56,10 +64,11 @@ describe('Push Provisioning Providers', () => { const result = getWalletProvider(); - expect(result).toBeNull(); + expect(result).toBeDefined(); + expect(GoogleWalletAdapter).toHaveBeenCalled(); }); - it('returns null for iOS (base branch has no platform adapters)', () => { + it('returns null for iOS (Google branch does not include Apple Wallet)', () => { Object.defineProperty(Platform, 'OS', { value: 'ios', writable: true, diff --git a/app/components/UI/Card/pushProvisioning/providers.ts b/app/components/UI/Card/pushProvisioning/providers.ts index d5c4cd3136b..33c930fddbd 100644 --- a/app/components/UI/Card/pushProvisioning/providers.ts +++ b/app/components/UI/Card/pushProvisioning/providers.ts @@ -3,14 +3,12 @@ * * Simple factory functions that return the appropriate card and wallet providers * based on user location and platform OS. - * - * NOTE: This is the base module. Platform-specific branches will override - * getWalletProvider to return the appropriate adapter (GoogleWalletAdapter or AppleWalletAdapter). */ +import { Platform } from 'react-native'; import { CardSDK } from '../sdk/CardSDK'; import { GalileoCardAdapter, ICardProviderAdapter } from './adapters/card'; -import { IWalletProviderAdapter } from './adapters/wallet'; +import { GoogleWalletAdapter, IWalletProviderAdapter } from './adapters/wallet'; import { CardLocation } from '../types'; /** @@ -36,13 +34,11 @@ export function getCardProvider( /** * Get the appropriate wallet provider adapter based on platform OS * - * NOTE: Base implementation returns null. Platform-specific branches - * (feat/apple-in-app-provisioning, feat/google-in-app-provisioning) - * will override this to return the appropriate adapter. - * * @returns The wallet provider adapter for the current platform, or null if not supported */ export function getWalletProvider(): IWalletProviderAdapter | null { - // Platform-specific branches will implement this + if (Platform.OS === 'android') { + return new GoogleWalletAdapter(); + } return null; } diff --git a/app/components/UI/Card/pushProvisioning/service/PushProvisioningService.test.ts b/app/components/UI/Card/pushProvisioning/service/PushProvisioningService.test.ts index 044446b0d3e..a480a70e98f 100644 --- a/app/components/UI/Card/pushProvisioning/service/PushProvisioningService.test.ts +++ b/app/components/UI/Card/pushProvisioning/service/PushProvisioningService.test.ts @@ -136,11 +136,7 @@ describe('PushProvisioningService', () => { it('provisions card successfully', async () => { mockCardAdapter.getOpaquePaymentCard.mockResolvedValue({ - success: true, - encryptedPayload: { opaquePaymentCard: 'encrypted-data' }, - cardNetwork: 'MASTERCARD', - lastFourDigits: '1234', - cardholderName: 'John Doe', + opaquePaymentCard: 'encrypted-data', }); mockWalletAdapter.provisionCard.mockResolvedValue({ status: 'success', @@ -162,51 +158,21 @@ describe('PushProvisioningService', () => { }); }); - it('returns error when getOpaquePaymentCard fails', async () => { - mockCardAdapter.getOpaquePaymentCard.mockResolvedValue({ - success: false, - encryptedPayload: {}, - cardNetwork: 'MASTERCARD', - lastFourDigits: '1234', - cardholderName: 'John Doe', - }); - - const result = await service.initiateProvisioning( - mockProvisioningOptions, + it('returns error when getOpaquePaymentCard throws', async () => { + mockCardAdapter.getOpaquePaymentCard.mockRejectedValue( + new Error('Network error'), ); - expect(result.status).toBe('error'); - expect(result.error?.code).toBe( - ProvisioningErrorCode.ENCRYPTION_FAILED, - ); - }); - - it('returns error when opaquePaymentCard is missing', async () => { - mockCardAdapter.getOpaquePaymentCard.mockResolvedValue({ - success: true, - encryptedPayload: {}, // Missing opaquePaymentCard - cardNetwork: 'MASTERCARD', - lastFourDigits: '1234', - cardholderName: 'John Doe', - }); - const result = await service.initiateProvisioning( mockProvisioningOptions, ); expect(result.status).toBe('error'); - expect(result.error?.code).toBe( - ProvisioningErrorCode.ENCRYPTION_FAILED, - ); }); it('returns canceled when user cancels provisioning', async () => { mockCardAdapter.getOpaquePaymentCard.mockResolvedValue({ - success: true, - encryptedPayload: { opaquePaymentCard: 'encrypted-data' }, - cardNetwork: 'MASTERCARD', - lastFourDigits: '1234', - cardholderName: 'John Doe', + opaquePaymentCard: 'encrypted-data', }); mockWalletAdapter.provisionCard.mockResolvedValue({ status: 'canceled', @@ -221,11 +187,7 @@ describe('PushProvisioningService', () => { it('works without userAddress', async () => { mockCardAdapter.getOpaquePaymentCard.mockResolvedValue({ - success: true, - encryptedPayload: { opaquePaymentCard: 'encrypted-data' }, - cardNetwork: 'MASTERCARD', - lastFourDigits: '1234', - cardholderName: 'John Doe', + opaquePaymentCard: 'encrypted-data', }); mockWalletAdapter.provisionCard.mockResolvedValue({ status: 'success', diff --git a/app/components/UI/Card/pushProvisioning/service/PushProvisioningService.ts b/app/components/UI/Card/pushProvisioning/service/PushProvisioningService.ts index b2871169b7f..2b431068d91 100644 --- a/app/components/UI/Card/pushProvisioning/service/PushProvisioningService.ts +++ b/app/components/UI/Card/pushProvisioning/service/PushProvisioningService.ts @@ -101,7 +101,7 @@ export class PushProvisioningService { cardDescription: `MetaMask Card ending in ${panLast4}`, }; - // 5. Provision the card + // 4. Provision the card return await this.provisionCard( this.cardAdapter, this.walletAdapter, @@ -178,22 +178,14 @@ export class PushProvisioningService { cardDisplayInfo: CardDisplayInfo, userAddress?: UserAddress, ): Promise { - const response = await cardAdapter.getOpaquePaymentCard(); + const { opaquePaymentCard } = await cardAdapter.getOpaquePaymentCard(); - if (!response.success || !response.encryptedPayload?.opaquePaymentCard) { - throw new ProvisioningError( - ProvisioningErrorCode.ENCRYPTION_FAILED, - strings('card.push_provisioning.error_encryption_failed'), - ); - } - - // 3. Provision the card with user address from CardHome return await walletAdapter.provisionCard({ cardNetwork: cardDisplayInfo.cardNetwork, cardholderName: cardDisplayInfo.cardholderName, lastFourDigits: cardDisplayInfo.lastFourDigits, cardDescription: cardDisplayInfo.cardDescription, - encryptedPayload: response.encryptedPayload, + encryptedPayload: { opaquePaymentCard }, userAddress, }); } @@ -217,17 +209,9 @@ export class PushProvisioningService { ); } - const getEncryptedPayload = + const issuerEncryptCallback = cardAdapter.getApplePayEncryptedPayload.bind(cardAdapter); - const issuerEncryptCallback = async ( - nonce: string, - nonceSignature: string, - certificates: string[], - ) => { - return await getEncryptedPayload(nonce, nonceSignature, certificates); - }; - return await walletAdapter.provisionCard({ cardNetwork: cardDisplayInfo.cardNetwork, cardholderName: cardDisplayInfo.cardholderName, diff --git a/app/components/UI/Card/pushProvisioning/types.ts b/app/components/UI/Card/pushProvisioning/types.ts index d384b5ff47e..f3c6abd2606 100644 --- a/app/components/UI/Card/pushProvisioning/types.ts +++ b/app/components/UI/Card/pushProvisioning/types.ts @@ -2,33 +2,20 @@ * Push Provisioning Types * * Core types and interfaces for the push provisioning feature. - * This module supports adding cards to mobile wallets (Google Wallet, Apple Pay) + * Supports adding cards to mobile wallets (Google Wallet, Apple Pay) * from card providers (Galileo, etc.). */ -// ============================================================================ -// Enums and Constants -// ============================================================================ - -/** - * Supported card provider identifiers - */ +/** Supported card provider identifiers */ export type CardProviderId = 'galileo' | 'monavate'; -/** - * Supported mobile wallet types - */ +/** Supported mobile wallet types */ export type WalletType = 'google_wallet' | 'apple_wallet'; -/** - * Supported card networks - * Currently only Mastercard is supported. - */ +/** Supported card networks (currently only Mastercard) */ export type CardNetwork = 'MASTERCARD'; -/** - * Card token status in the wallet - */ +/** Card token status in the wallet */ export type CardTokenStatus = | 'not_found' | 'active' @@ -37,9 +24,7 @@ export type CardTokenStatus = | 'deactivated' | 'requires_activation'; -/** - * Provisioning operation status - */ +/** Provisioning operation status */ export type ProvisioningStatus = | 'idle' | 'checking_eligibility' @@ -48,36 +33,18 @@ export type ProvisioningStatus = | 'error' | 'canceled'; -/** - * Provisioning error codes - */ +/** Provisioning error codes */ export enum ProvisioningErrorCode { - // Wallet-related errors WALLET_NOT_AVAILABLE = 'WALLET_NOT_AVAILABLE', - WALLET_NOT_INITIALIZED = 'WALLET_NOT_INITIALIZED', - CARD_ALREADY_IN_WALLET = 'CARD_ALREADY_IN_WALLET', - - // Card provider errors CARD_PROVIDER_NOT_FOUND = 'CARD_PROVIDER_NOT_FOUND', CARD_NOT_ELIGIBLE = 'CARD_NOT_ELIGIBLE', ENCRYPTION_FAILED = 'ENCRYPTION_FAILED', INVALID_CARD_DATA = 'INVALID_CARD_DATA', - - // User actions - USER_CANCELED = 'USER_CANCELED', - - // Generic errors UNKNOWN_ERROR = 'UNKNOWN_ERROR', PLATFORM_NOT_SUPPORTED = 'PLATFORM_NOT_SUPPORTED', } -// ============================================================================ -// Device and Wallet Data Types -// ============================================================================ - -/** - * User address for card provisioning - */ +/** User address for card provisioning */ export interface UserAddress { name: string; addressOne: string; @@ -89,13 +56,7 @@ export interface UserAddress { phoneNumber: string; } -// ============================================================================ -// Card Display Types -// ============================================================================ - -/** - * Card information for display purposes - */ +/** Card information for display during provisioning */ export interface CardDisplayInfo { cardId: string; cardholderName: string; @@ -104,48 +65,22 @@ export interface CardDisplayInfo { cardDescription?: string; } -// ============================================================================ -// Provisioning Request/Response Types -// ============================================================================ - -/** - * Encrypted payload for wallet provisioning - */ +/** Encrypted payload for wallet provisioning */ export interface EncryptedPayload { opaquePaymentCard?: string; } /** * Apple Pay encrypted payload returned by the card provider - * - * This data is returned after sending nonce, nonceSignature, and certificates - * to the card provider's Apple Pay provisioning endpoint. + * after sending nonce, nonceSignature, and certificates. */ export interface ApplePayEncryptedPayload { - /** Encrypted card data for PassKit */ encryptedPassData: string; - /** Activation data for the pass */ activationData: string; - /** Ephemeral public key used for encryption */ ephemeralPublicKey: string; } -/** - * Response from card provider after encrypting payload - */ -export interface ProvisioningResponse { - success: boolean; - encryptedPayload?: EncryptedPayload; - cardNetwork: CardNetwork; - lastFourDigits: string; - cardholderName: string; - cardDescription?: string; - error?: ProvisioningError; -} - -/** - * Parameters for provisioning a card to a wallet - */ +/** Parameters for provisioning a card to a wallet */ export interface ProvisionCardParams { cardNetwork: CardNetwork; cardholderName: string; @@ -153,18 +88,7 @@ export interface ProvisionCardParams { cardDescription?: string; encryptedPayload: EncryptedPayload; userAddress?: UserAddress; - /** - * Callback for Apple Pay in-app provisioning - * - * When provisioning to Apple Wallet, PassKit provides nonce, nonceSignature, - * and certificates that must be sent to the card provider to get the - * encrypted payload. This callback handles that exchange. - * - * @param nonce - Cryptographic nonce from PassKit - * @param nonceSignature - Signature of the nonce - * @param certificates - Array of certificate strings from PassKit - * @returns Promise resolving to the encrypted payload from card provider - */ + /** Callback for Apple Pay: PassKit provides nonce/certs, returns encrypted payload */ issuerEncryptCallback?: ( nonce: string, nonceSignature: string, @@ -172,67 +96,40 @@ export interface ProvisionCardParams { ) => Promise; } -/** - * Result of a provisioning operation - */ +/** Result of a provisioning operation */ export interface ProvisioningResult { status: 'success' | 'canceled' | 'error'; tokenId?: string; error?: ProvisioningError; } -// ============================================================================ -// Wallet Eligibility Types -// ============================================================================ - -/** - * Recommended action based on card status - */ +/** Recommended action based on card status */ export type WalletAction = - | 'add_card' // Card not in wallet, show "Add to Wallet" button - | 'resume' // Card requires activation (Yellow Path), show "Continue Setup" - | 'none' // Card is active, hide button - | 'contact_support' // Card is suspended/deactivated, show help option - | 'wait'; // Card is pending, show status message + | 'add_card' + | 'resume' + | 'none' + | 'contact_support' + | 'wait'; -/** - * Wallet eligibility check result - */ +/** Wallet eligibility check result */ export interface WalletEligibility { - /** Whether the wallet SDK is available on the device */ isAvailable: boolean; - /** Whether a card can be added to the wallet */ canAddCard: boolean; - /** Status of existing card in wallet (if any) */ existingCardStatus?: CardTokenStatus; - /** Reason if card cannot be added */ ineligibilityReason?: string; - /** Recommended action based on card status */ recommendedAction?: WalletAction; - /** Token reference ID for resume flow (if status is 'requires_activation') */ + /** Token reference ID for resume flow (requires_activation status) */ tokenReferenceId?: string; } -// ============================================================================ -// Event Types -// ============================================================================ - -/** - * Card activation event from wallet - */ +/** Card activation event from wallet */ export interface CardActivationEvent { tokenId?: string; serialNumber?: string; status: 'activated' | 'canceled' | 'failed'; } -// ============================================================================ -// Error Types -// ============================================================================ - -/** - * Provisioning error with detailed information - */ +/** Provisioning error with detailed information */ export class ProvisioningError extends Error { public code: ProvisioningErrorCode; public originalError?: Error; @@ -252,13 +149,7 @@ export class ProvisioningError extends Error { } } -// ============================================================================ -// Hook Types -// ============================================================================ - -/** - * Card details from CardHome (to avoid duplicate API calls) - */ +/** Card details from CardHome (to avoid duplicate API calls) */ export interface CardDetails { id: string; holderName: string; @@ -266,35 +157,25 @@ export interface CardDetails { status: string; } -/** - * Options for usePushProvisioning hook - */ +/** Options for usePushProvisioning hook */ export interface UsePushProvisioningOptions { - /** Card details from CardHome (includes holderName, panLast4, status, etc.) */ cardDetails?: CardDetails | null; - /** User address for Google Wallet provisioning (from user profile) */ userAddress?: UserAddress; + accountCreatedAt?: string | null; onSuccess?: (result: ProvisioningResult) => void; onError?: (error: ProvisioningError) => void; onCancel?: () => void; } -/** - * Return type for usePushProvisioning hook - */ +/** Return type for usePushProvisioning hook */ export interface UsePushProvisioningReturn { - // Status status: ProvisioningStatus; error: ProvisioningError | null; - - // Actions initiateProvisioning: () => Promise; resetStatus: () => void; - isProvisioning: boolean; isSuccess: boolean; isError: boolean; - isLoading: boolean; canAddToWallet: boolean; } diff --git a/app/components/UI/Card/routes/index.test.tsx b/app/components/UI/Card/routes/index.test.tsx new file mode 100644 index 00000000000..ab23110edd3 --- /dev/null +++ b/app/components/UI/Card/routes/index.test.tsx @@ -0,0 +1,250 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +/* eslint-disable @typescript-eslint/no-var-requires */ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import { NavigationContainer } from '@react-navigation/native'; +import { Provider } from 'react-redux'; +import { configureStore } from '@reduxjs/toolkit'; +import CardRoutes from './index'; + +jest.mock('@react-navigation/stack', () => { + const { View, Text } = require('react-native'); + return { + createStackNavigator: () => ({ + Navigator: ({ + children, + screenOptions, + initialRouteName, + }: { + children: React.ReactNode; + screenOptions?: { headerShown?: boolean }; + initialRouteName?: string; + }) => ( + + {screenOptions?.headerShown === false && ( + headerShown: false + )} + {initialRouteName && ( + {initialRouteName} + )} + {children} + + ), + Screen: ({ + name, + options, + }: { + name: string; + options?: { + headerShown?: boolean; + }; + }) => ( + + {name} + {options?.headerShown === false && no-header} + + ), + }), + }; +}); + +jest.mock('../Views/CardHome/CardHome', () => { + const { View } = require('react-native'); + return () => ; +}); + +jest.mock('../Views/CardWelcome/CardWelcome', () => { + const { View } = require('react-native'); + return () => ; +}); + +jest.mock('../Views/CardAuthentication/CardAuthentication', () => { + const { View } = require('react-native'); + return () => ; +}); + +jest.mock('../Views/SpendingLimit/SpendingLimit', () => { + const { View } = require('react-native'); + return () => ; +}); + +jest.mock('../Views/ChooseYourCard/ChooseYourCard', () => { + const { View } = require('react-native'); + return () => ; +}); + +jest.mock('../Views/ReviewOrder/ReviewOrder', () => { + const { View } = require('react-native'); + return () => ; +}); + +jest.mock('./OnboardingNavigator', () => { + const { View } = require('react-native'); + return () => ; +}); + +jest.mock('../components/AddFundsBottomSheet/AddFundsBottomSheet', () => { + const { View } = require('react-native'); + return () => ; +}); + +jest.mock( + '../components/AssetSelectionBottomSheet/AssetSelectionBottomSheet', + () => { + const { View } = require('react-native'); + return () => ; + }, +); + +jest.mock('../components/PasswordBottomSheet', () => { + const { View } = require('react-native'); + return () => ; +}); + +jest.mock('../components/Onboarding/RegionSelectorModal', () => { + const { View } = require('react-native'); + return () => ; +}); + +jest.mock('../components/Onboarding/ConfirmModal', () => { + const { View } = require('react-native'); + return () => ; +}); + +jest.mock('../components/RecurringFeeModal/RecurringFeeModal', () => { + const { View } = require('react-native'); + return () => ; +}); + +jest.mock('../components/DaimoPayModal/DaimoPayModal', () => { + const { View } = require('react-native'); + return () => ; +}); + +jest.mock('../components/ViewPinBottomSheet', () => { + const { View } = require('react-native'); + return () => ; +}); + +jest.mock('../Views/OrderCompleted/OrderCompleted', () => { + const { View } = require('react-native'); + return () => ; +}); + +jest.mock('../Views/Cashback/Cashback', () => { + const { View } = require('react-native'); + return () => ; +}); + +jest.mock('../sdk', () => ({ + withCardSDK: (Component: React.ComponentType) => Component, +})); + +jest.mock('../../../../constants/navigation/Routes', () => ({ + CARD: { + HOME: 'CardHome', + WELCOME: 'CardWelcome', + CHOOSE_YOUR_CARD: 'ChooseYourCard', + REVIEW_ORDER: 'ReviewOrder', + ORDER_COMPLETED: 'OrderCompleted', + CASHBACK: 'Cashback', + AUTHENTICATION: 'CardAuthentication', + SPENDING_LIMIT: 'SpendingLimit', + ONBOARDING: { + ROOT: 'CardOnboarding', + }, + MODALS: { + ID: 'CardModals', + ADD_FUNDS: 'AddFunds', + ASSET_SELECTION: 'AssetSelection', + REGION_SELECTION: 'RegionSelection', + CONFIRM_MODAL: 'ConfirmModal', + PASSWORD: 'Password', + RECURRING_FEE: 'RecurringFee', + DAIMO_PAY: 'DaimoPay', + VIEW_PIN: 'ViewPin', + }, + }, +})); + +const createMockStore = (isAuthenticated = false, isCardholder = false) => + configureStore({ + reducer: { + card: () => ({ + isAuthenticatedCard: isAuthenticated, + isCardholder, + }), + }, + }); + +describe('CardRoutes', () => { + const renderWithProviders = ( + component: React.ReactElement, + store = createMockStore(), + ) => + render( + + {component} + , + ); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('CardRoutes component', () => { + it('renders successfully', () => { + const { getByTestId } = renderWithProviders(); + + expect(getByTestId('stack-navigator')).toBeTruthy(); + }); + + it('renders nested stack navigators', () => { + const { getAllByTestId } = renderWithProviders(); + + expect(getAllByTestId('stack-navigator').length).toBeGreaterThan(0); + }); + + it('includes CardHome screen', () => { + const { getByTestId } = renderWithProviders(); + + expect(getByTestId('screen-CardHome')).toBeTruthy(); + }); + + it('includes CardModals navigator', () => { + const { getByTestId } = renderWithProviders(); + + expect(getByTestId('screen-CardModals')).toBeTruthy(); + }); + }); + + describe('Initial route selection', () => { + it('navigates to Home when authenticated', () => { + const store = createMockStore(true, false); + const { getAllByTestId } = renderWithProviders(, store); + + const initialRoutes = getAllByTestId('initial-route'); + expect(initialRoutes.some((el) => el.children[0] === 'CardHome')).toBe( + true, + ); + }); + + it('navigates to Home when is cardholder', () => { + const store = createMockStore(false, true); + const { getAllByTestId } = renderWithProviders(, store); + + const initialRoutes = getAllByTestId('initial-route'); + expect(initialRoutes.some((el) => el.children[0] === 'CardHome')).toBe( + true, + ); + }); + }); + + describe('Navigator configuration', () => { + it('renders with header hidden configuration', () => { + const { getByText } = renderWithProviders(); + + expect(getByText('headerShown: false')).toBeTruthy(); + }); + }); +}); diff --git a/app/components/UI/Card/routes/index.tsx b/app/components/UI/Card/routes/index.tsx index e1b12fcb912..946905a052c 100644 --- a/app/components/UI/Card/routes/index.tsx +++ b/app/components/UI/Card/routes/index.tsx @@ -150,7 +150,7 @@ const MainRoutes = () => { ); return ( - + { const CardModalsRoutes = () => ( ( ); const CardRoutes = () => ( - + { const mockGoogleProvisioningResponse = { success: true, data: { - cardNetwork: 'MASTERCARD', - lastFourDigits: '1234', - cardholderName: 'John Doe', - cardDescription: 'MetaMask Card', opaquePaymentCard: 'encrypted-opc-data', }, }; @@ -4891,10 +4887,6 @@ describe('CardSDK', () => { const result = await cardSDK.createGoogleWalletProvisioningRequest(); expect(result).toEqual({ - cardNetwork: 'MASTERCARD', - lastFourDigits: '1234', - cardholderName: 'John Doe', - cardDescription: 'MetaMask Card', opaquePaymentCard: 'encrypted-opc-data', }); }); @@ -4934,51 +4926,6 @@ describe('CardSDK', () => { ); }); - it('handles response with panLast4 fallback', async () => { - const responseWithPanLast4 = { - success: true, - data: { - cardNetwork: 'MASTERCARD', - panLast4: '5678', - holderName: 'Jane Doe', - opaquePaymentCard: 'encrypted-opc-data', - }, - }; - - (global.fetch as jest.Mock).mockResolvedValueOnce({ - ok: true, - json: jest.fn().mockResolvedValue(responseWithPanLast4), - }); - - const result = await cardSDK.createGoogleWalletProvisioningRequest(); - - expect(result).toEqual({ - cardNetwork: 'MASTERCARD', - lastFourDigits: '5678', - cardholderName: 'Jane Doe', - cardDescription: undefined, - opaquePaymentCard: 'encrypted-opc-data', - }); - }); - - it('uses default cardNetwork when not provided', async () => { - const responseWithoutNetwork = { - success: true, - data: { - opaquePaymentCard: 'encrypted-opc-data', - }, - }; - - (global.fetch as jest.Mock).mockResolvedValueOnce({ - ok: true, - json: jest.fn().mockResolvedValue(responseWithoutNetwork), - }); - - const result = await cardSDK.createGoogleWalletProvisioningRequest(); - - expect(result.cardNetwork).toBe('MASTERCARD'); - }); - it('throws INVALID_CREDENTIALS error on 401 response', async () => { (global.fetch as jest.Mock).mockResolvedValueOnce({ ok: false, diff --git a/app/components/UI/Card/sdk/CardSDK.ts b/app/components/UI/Card/sdk/CardSDK.ts index 984d36aaca8..f64b6a01f39 100644 --- a/app/components/UI/Card/sdk/CardSDK.ts +++ b/app/components/UI/Card/sdk/CardSDK.ts @@ -2447,15 +2447,10 @@ export class CardSDK { * Google Wallet provisioning flow: * 1. Card provider returns opaquePaymentCard (OPC) * - * @param params - The Google Wallet provisioning request parameters - * @returns Promise resolving to the provisioning response with encrypted opaque payment card + * @returns Promise resolving to the opaque payment card string * @see https://dev.api.baanx.com/v1/card/wallet/provision/google */ createGoogleWalletProvisioningRequest = async (): Promise<{ - cardNetwork: string; - lastFourDigits: string; - cardholderName: string; - cardDescription?: string; opaquePaymentCard: string; }> => { const endpoint = 'card/wallet/provision/google'; @@ -2488,12 +2483,6 @@ export class CardSDK { const responseData = (await response.json()) as { success: boolean; data?: { - cardNetwork?: string; - lastFourDigits?: string; - panLast4?: string; - cardholderName?: string; - holderName?: string; - cardDescription?: string; opaquePaymentCard?: string; }; }; @@ -2507,14 +2496,8 @@ export class CardSDK { ); } - const data = responseData.data; - return { - cardNetwork: data.cardNetwork || 'MASTERCARD', - lastFourDigits: data.lastFourDigits || data.panLast4 || '', - cardholderName: data.cardholderName || data.holderName || '', - cardDescription: data.cardDescription, - opaquePaymentCard: data.opaquePaymentCard as string, + opaquePaymentCard: responseData.data.opaquePaymentCard, }; }; diff --git a/app/components/UI/Card/util/buildUserAddress.test.ts b/app/components/UI/Card/util/buildUserAddress.test.ts index 149b83775ec..42729c969d1 100644 --- a/app/components/UI/Card/util/buildUserAddress.test.ts +++ b/app/components/UI/Card/util/buildUserAddress.test.ts @@ -16,6 +16,7 @@ describe('buildUserAddress utilities', () => { usState: 'NY', zip: '10001', phoneNumber: '5551234567', + phoneCountryCode: '+1', mailingAddressLine1: '456 Mailing Ave', mailingAddressLine2: 'Suite 100', mailingCity: 'Mailing City', @@ -52,7 +53,7 @@ describe('buildUserAddress utilities', () => { ).toBeUndefined(); }); - it('builds UserAddress from physical address fields only', () => { + it('builds UserAddress with E.164 phone number', () => { const result = buildProvisioningUserAddress( mockFullUserDetails, 'John Doe', @@ -66,10 +67,35 @@ describe('buildUserAddress utilities', () => { administrativeArea: 'NY', postalCode: '10001', countryCode: 'US', - phoneNumber: '5551234567', + phoneNumber: '+15551234567', }); }); + it('formats phone number with + prefix when country code is missing', () => { + const result = buildProvisioningUserAddress( + { + ...mockFullUserDetails, + phoneCountryCode: undefined, + }, + 'John Doe', + ); + + expect(result?.phoneNumber).toBe('+5551234567'); + }); + + it('strips non-digit characters from phone number and country code', () => { + const result = buildProvisioningUserAddress( + { + ...mockFullUserDetails, + phoneCountryCode: '+1', + phoneNumber: '(555) 123-4567', + }, + 'John Doe', + ); + + expect(result?.phoneNumber).toBe('+15551234567'); + }); + it('handles missing optional fields with defaults', () => { const result = buildProvisioningUserAddress( { id: 'test', addressLine1: '123 St', city: 'NYC', zip: '10001' }, @@ -154,5 +180,45 @@ describe('buildUserAddress utilities', () => { buildCardholderName({ id: 'test', firstName: null, lastName: null }), ).toBe('Card Holder'); }); + + it('sanitizes special characters from names', () => { + expect( + buildCardholderName({ + id: 'test', + firstName: 'Josรฉ', + lastName: "O'Brien", + }), + ).toBe('Jose OBrien'); + }); + + it('sanitizes accented characters and keeps alphanumeric', () => { + expect( + buildCardholderName({ + id: 'test', + firstName: 'Mรผller', + lastName: 'StraรŸe', + }), + ).toBe('Muller Strae'); + }); + + it('trims whitespace before sanitizing', () => { + expect( + buildCardholderName({ + id: 'test', + firstName: ' John ', + lastName: ' Doe ', + }), + ).toBe('John Doe'); + }); + + it('returns fallback when names are only special characters', () => { + expect( + buildCardholderName({ + id: 'test', + firstName: '***', + lastName: '!!!', + }), + ).toBe('Card Holder'); + }); }); }); diff --git a/app/components/UI/Card/util/buildUserAddress.ts b/app/components/UI/Card/util/buildUserAddress.ts index fac7bfac56f..b2138054279 100644 --- a/app/components/UI/Card/util/buildUserAddress.ts +++ b/app/components/UI/Card/util/buildUserAddress.ts @@ -37,8 +37,15 @@ export function buildProvisioningUserAddress( return undefined; } - const { addressLine1, addressLine2, city, usState, zip, phoneNumber } = - userDetails; + const { + addressLine1, + addressLine2, + city, + usState, + zip, + phoneNumber, + phoneCountryCode, + } = userDetails; // Require at least address line 1, city, and zip if (!addressLine1 || !city || !zip) { @@ -53,10 +60,42 @@ export function buildProvisioningUserAddress( administrativeArea: usState ?? '', postalCode: zip, countryCode: 'US', - phoneNumber: phoneNumber ?? '', + phoneNumber: formatE164PhoneNumber(phoneCountryCode, phoneNumber), }; } +/** + * Format a phone number in E.164 format for the Google Tap and Pay SDK. + * + * The API returns phoneNumber and phoneCountryCode as separate fields + * (e.g. "2345678901" and "+1"), but Google's UserAddress.setPhoneNumber() + * requires E.164 format (e.g. "+12345678901"). + */ +function formatE164PhoneNumber( + countryCode: string | null | undefined, + phoneNumber: string | null | undefined, +): string { + if (!phoneNumber) { + return ''; + } + + const digits = phoneNumber.replace(/\D/g, ''); + if (!digits) { + return ''; + } + + if (!countryCode) { + return `+${digits}`; + } + + const codeDigits = countryCode.replace(/\D/g, ''); + if (!codeDigits) { + return `+${digits}`; + } + + return `+${codeDigits}${digits}`; +} + /** * Build a ShippingAddress object for metal card ordering * @@ -106,12 +145,35 @@ export function buildShippingAddress( return undefined; } +/** + * Sanitize a name part for card provisioning. + * + * Uses Unicode NFD normalization to decompose accented characters into + * base letter + combining mark, then strips the marks. This preserves + * base letters (e.g. "Josรฉ" โ†’ "Jose", "Mรผller" โ†’ "Muller") instead of + * dropping them entirely. The final ASCII filter ensures only characters + * accepted by Galileo remain. + * + * @param name - The name string to sanitize + * @returns Sanitized name containing only alphanumeric characters and spaces + */ +function sanitizeName(name: string): string { + return name + .trim() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/[^a-zA-Z0-9 ]/g, ''); +} + /** * Build cardholder full name from user details * + * Uses firstName and lastName from KYC details, sanitized through the same + * regex used for card ordering at Galileo: `name.trim().replace(/[^a-zA-Z0-9 ]/g, '')` + * * @param userDetails - User details from KYC status * @param fallback - Fallback name if user details are incomplete (default: 'Card Holder') - * @returns Full cardholder name + * @returns Sanitized full cardholder name */ export function buildCardholderName( userDetails: UserResponse | null | undefined, @@ -121,7 +183,11 @@ export function buildCardholderName( return fallback; } - return [userDetails.firstName, userDetails.lastName] + const result = [userDetails.firstName, userDetails.lastName] + .filter(Boolean) + .map((name) => sanitizeName(name as string)) .filter(Boolean) .join(' '); + + return result || fallback; } diff --git a/app/components/UI/Carousel/index.test.tsx b/app/components/UI/Carousel/index.test.tsx index f1d84fdfea4..3187ad3b61a 100644 --- a/app/components/UI/Carousel/index.test.tsx +++ b/app/components/UI/Carousel/index.test.tsx @@ -23,6 +23,8 @@ import Routes from '../../../constants/navigation/Routes'; import { WalletClientType } from '../../../core/SnapKeyring/MultichainWalletSnapClient'; import { SolScope } from '@metamask/keyring-api'; import { setContentPreviewToken } from '../../../actions/notification/helpers'; +import { createMockUseAnalyticsHook } from '../../../util/test/analyticsMock'; +import { useAnalytics } from '../../../components/hooks/useAnalytics/useAnalytics'; const makeMockState = () => ({ @@ -60,16 +62,7 @@ jest.mock('../../../core/Engine', () => ({ context: { PreferencesController: { state: {} } }, })); -const mockTrackEvent = jest.fn(); -const mockCreateEventBuilder = jest.fn(() => ({ - build: () => ({ category: 'Banner Display', properties: {} }), -})); -jest.mock('../../../components/hooks/useMetrics', () => ({ - useMetrics: () => ({ - trackEvent: mockTrackEvent, - createEventBuilder: mockCreateEventBuilder, - }), -})); +jest.mock('../../../components/hooks/useAnalytics/useAnalytics'); jest.mock('../../../core/DeeplinkManager/DeeplinkManager', () => { const mockParse = jest.fn().mockResolvedValue(true); @@ -119,6 +112,7 @@ const mockReduxHooks = (state?: RootState) => { beforeEach(() => { jest.clearAllMocks(); + jest.mocked(useAnalytics).mockReturnValue(createMockUseAnalyticsHook()); mockReduxHooks(); jest .spyOn(FeatureFlagSelectorsModule, 'selectContentfulCarouselEnabledFlag') diff --git a/app/components/UI/Carousel/index.tsx b/app/components/UI/Carousel/index.tsx index af76f168c6f..0261abb5418 100644 --- a/app/components/UI/Carousel/index.tsx +++ b/app/components/UI/Carousel/index.tsx @@ -21,7 +21,7 @@ import { TextColor, } from '@metamask/design-system-react-native'; import { useTailwind } from '@metamask/design-system-twrnc-preset'; -import { useMetrics } from '../../../components/hooks/useMetrics'; +import { useAnalytics } from '../../../components/hooks/useAnalytics/useAnalytics'; import { WalletViewSelectorsIDs } from '../../Views/Wallet/WalletView.testIds'; import { selectDismissedBanners } from '../../../selectors/banner'; ///: BEGIN:ONLY_INCLUDE_IF(solana) @@ -170,7 +170,7 @@ const CarouselComponent: FC = ({ style, onEmptyState }) => { carouselScaleY, }); - const { trackEvent, createEventBuilder } = useMetrics(); + const { trackEvent, createEventBuilder } = useAnalytics(); const hasBalance = useSelector(selectAddressHasTokenBalances); const dispatch = useDispatch(); const { navigate } = useNavigation(); diff --git a/app/components/UI/DeleteWalletModal/__snapshots__/index.test.tsx.snap b/app/components/UI/DeleteWalletModal/__snapshots__/index.test.tsx.snap index 54e3aa67777..0cae37871d8 100644 --- a/app/components/UI/DeleteWalletModal/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/DeleteWalletModal/__snapshots__/index.test.tsx.snap @@ -20,351 +20,331 @@ exports[`DeleteWalletModal bottom sheet renders matching snapshot 1`] = ` } > - - - + + /> + + - + - DeleteWalletModal - + + DeleteWalletModal + + + - - - - - + - - - - - - - + /> + + - - Forgot your password? - - - MetaMask canโ€™t recover your password for you. - + /> + + + Forgot your password? + + + MetaMask canโ€™t recover your password for you. + - - - If youโ€™re logged into MetaMask on a device with - + - biometrics turned on + If youโ€™re logged into MetaMask on a device with + + + biometrics turned on + + + (like Face ID), you can reset your password there. - - (like Face ID), you can reset your password there. - - - - + - - If you have your - + - Secret Recovery Phrase, + If you have your + + Secret Recovery Phrase, + + + you can reset your current wallet and reimport using Secret Recovery Phrase. - you can reset your current wallet and reimport using Secret Recovery Phrase. - + - - - - Reset wallet - - + + Reset wallet + + + @@ -649,9 +672,9 @@ exports[`DeleteWalletModal bottom sheet renders matching snapshot 1`] = ` - - - + + + `; diff --git a/app/components/UI/Earn/Views/EarnInputView/EarnInputView.test.tsx b/app/components/UI/Earn/Views/EarnInputView/EarnInputView.test.tsx index e5ab4bf2c60..811a6ea5511 100644 --- a/app/components/UI/Earn/Views/EarnInputView/EarnInputView.test.tsx +++ b/app/components/UI/Earn/Views/EarnInputView/EarnInputView.test.tsx @@ -129,7 +129,7 @@ jest.mock('@react-navigation/native', () => { setOptions: mockSetOptions, reset: mockReset, goBack: mockGoBack, - dangerouslyGetParent: () => ({ + getParent: () => ({ pop: mockPop, }), }), diff --git a/app/components/UI/Earn/Views/EarnInputView/__snapshots__/EarnInputView.test.tsx.snap b/app/components/UI/Earn/Views/EarnInputView/__snapshots__/EarnInputView.test.tsx.snap index 1e7a5a11197..c840dfdcc0a 100644 --- a/app/components/UI/Earn/Views/EarnInputView/__snapshots__/EarnInputView.test.tsx.snap +++ b/app/components/UI/Earn/Views/EarnInputView/__snapshots__/EarnInputView.test.tsx.snap @@ -20,928 +20,937 @@ exports[`EarnInputView render matches snapshot 1`] = ` } > - - - + + /> + + - + - Stake - + + Stake + + + - - - - - + - - - - - + + + - + testID="button-icon" + > + + - - - - Stake ETH - + + Stake ETH + + + + + - - - - - - - + } + > + - - 0 ETH available to withdraw - - - - + > + 0 ETH available to withdraw + + + + + + + Balance: 1.5 ETH + - - Balance: 1.5 ETH - - - - - + + + 0 + + + ETH + + + + + + - 0 + 0 USD - - ETH - - + width={16} + /> + - - - - 0 USD - - - - - - - - - - + + + - - 25% - - - - + 25% + + + - 50% - - - - + 50% + + + - 75% - - - - - + 75% + + + - Max - - - - + + + Max + + + @@ -950,736 +959,750 @@ exports[`EarnInputView render matches snapshot 1`] = ` [ { "display": "flex", - "flexBasis": "0%", - "flexGrow": 1, - "flexShrink": 1, + "flexDirection": "row", + "gap": 12, + "justifyContent": "space-between", }, undefined, ] } > - - - 1 - - - - - + 1 + + + + - - 2 - - - - - + 2 + + + + - - 3 - - - - - + + 3 + + + + - - - 4 - - - - - + 4 + + + + - - 5 - - - - - + 5 + + + + - - 6 - - + + 6 + + + - - - - - 7 - - - - - + 7 + + + + - - 8 - - - - - + 8 + + + + - - 9 - - + + 9 + + + - - - - - . - - - - - + . + + + + - - 0 - - - - - + 0 + + + + - - + testID="keypad-delete-button" + > + + + - - - - - Enter amount - - + + Enter amount + + + - - + + - - - + + + `; @@ -1704,928 +1727,937 @@ exports[`EarnInputView when values are entered in the keypad updates ETH and fia } > - - - + + /> + + - + - Stake - + + Stake + + + - - - - - - + + + - - + - - - - - + + + - + testID="button-icon" + > + + - - - - Stake ETH - + + Stake ETH + + + + + - - - - - - - + } + > + - + + 0 ETH available to withdraw + + + + + + + + Balance: 1.5 ETH + + + + - 0 ETH available to withdraw - - + - + 0 + + - + > + ETH + + - - Balance: 1.5 ETH - - - - - + - 0 + 0 USD - - ETH - - + width={16} + /> + - - - - 0 USD - - - - - - - - - - + + + - - 25% - - - - + 25% + + + - 50% - - - - + 50% + + + - 75% - - - - - + 75% + + + - Max - - - - + + + Max + + + @@ -2634,736 +2666,750 @@ exports[`EarnInputView when values are entered in the keypad updates ETH and fia [ { "display": "flex", - "flexBasis": "0%", - "flexGrow": 1, - "flexShrink": 1, + "flexDirection": "row", + "gap": 12, + "justifyContent": "space-between", }, undefined, ] } > - - - 1 - - - - - + 1 + + + + - - 2 - - - - - + 2 + + + + - - 3 - - + + 3 + + + - - - - - 4 - - - - - + 4 + + + + - - 5 - - - - - + 5 + + + + - - 6 - - + + 6 + + + - - - - - 7 - - - - - + + 7 + + + + - - 8 - - - - - + 8 + + + + - - 9 - - + + 9 + + + - - - - - . - - - - - + . + + + + - - 0 - - - - - + 0 + + + + - - + testID="keypad-delete-button" + > + + + - - - - - Enter amount - - + + Enter amount + + + - - + + - - - + + + `; diff --git a/app/components/UI/Earn/Views/EarnMusdConversionEducationView/index.test.tsx b/app/components/UI/Earn/Views/EarnMusdConversionEducationView/index.test.tsx index cc4e86cabf2..5ab09930aa1 100644 --- a/app/components/UI/Earn/Views/EarnMusdConversionEducationView/index.test.tsx +++ b/app/components/UI/Earn/Views/EarnMusdConversionEducationView/index.test.tsx @@ -180,7 +180,6 @@ describe('EarnMusdConversionEducationView', () => { jest.spyOn(Date, 'now').mockReturnValue(FIXED_NOW_MS); mockUseDispatch.mockReturnValue(mockDispatch); - // @ts-expect-error - partial mock of navigation is sufficient for testing mockUseNavigation.mockReturnValue(mockNavigation); mockUseFocusEffect.mockImplementation((callback) => { callback(); diff --git a/app/components/UI/Earn/Views/EarnWithdrawInputView/EarnWithdrawInputView.test.tsx b/app/components/UI/Earn/Views/EarnWithdrawInputView/EarnWithdrawInputView.test.tsx index 70e1899f397..d725f37c5a8 100644 --- a/app/components/UI/Earn/Views/EarnWithdrawInputView/EarnWithdrawInputView.test.tsx +++ b/app/components/UI/Earn/Views/EarnWithdrawInputView/EarnWithdrawInputView.test.tsx @@ -77,7 +77,7 @@ jest.mock('@react-navigation/native', () => { actualReactNavigation.useNavigation().setOptions, ), reset: mockReset, - dangerouslyGetParent: () => ({ + getParent: () => ({ pop: mockPop, }), }), diff --git a/app/components/UI/Earn/Views/EarnWithdrawInputView/__snapshots__/EarnWithdrawInputView.test.tsx.snap b/app/components/UI/Earn/Views/EarnWithdrawInputView/__snapshots__/EarnWithdrawInputView.test.tsx.snap index fc3557c05f3..76687e1d47f 100644 --- a/app/components/UI/Earn/Views/EarnWithdrawInputView/__snapshots__/EarnWithdrawInputView.test.tsx.snap +++ b/app/components/UI/Earn/Views/EarnWithdrawInputView/__snapshots__/EarnWithdrawInputView.test.tsx.snap @@ -20,775 +20,784 @@ exports[`EarnWithdrawInputView render matches snapshot 1`] = ` } > - - - + + /> + + - + - Unstake - + + Unstake + + + - - - - - + - - - - - - + } + handlerTag={1} + handlerType="NativeViewGestureHandler" + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + style={ + { + "flex": 1, + } + } + > + - - 0 ETH available to withdraw - - - + 0 ETH available to withdraw + + - + testID="LendingMaxSafeWithdrawalTooltipIcon" + > + + + + + Staked balance: 5.79133 ETH + - - Staked balance: 5.79133 ETH - - - - - + + + 0 + + + ETH + + + + + + - 0 + 0 USD - - ETH - - + width={16} + /> + - - - - 0 USD - - - - - - - - + - - 25% - - - - + 25% + + + - 50% - - - - + 50% + + + - 75% - - - - - + 75% + + + - Max - - - - + + + Max + + + @@ -797,737 +806,751 @@ exports[`EarnWithdrawInputView render matches snapshot 1`] = ` [ { "display": "flex", - "flexBasis": "0%", - "flexGrow": 1, - "flexShrink": 1, - }, - undefined, - ] - } - > - - - 1 - - - - - - - 2 - - - - - + 1 + + + + - - 3 - - - - - - - + 2 + + + + - - 4 - - + + 3 + + + - - - 5 - - - - - + 4 + + + + - - 6 - - - - - - - + 5 + + + + - - 7 - - + + 6 + + + - - - 8 - - - - - + 7 + + + + - - 9 - - - - - - - + 8 + + + + - - . - - + + 9 + + + - - + + . + + + + + - 0 - - - - - + 0 + + + + - - + testID="keypad-delete-button" + > + + + - - - - - Enter amount - - + + Enter amount + + + - - + + - - - + + + `; diff --git a/app/components/UI/Earn/components/MaxInputModal/__snapshots__/MaxInputModal.test.tsx.snap b/app/components/UI/Earn/components/MaxInputModal/__snapshots__/MaxInputModal.test.tsx.snap index d2d7fafaf24..529d6a11b6c 100644 --- a/app/components/UI/Earn/components/MaxInputModal/__snapshots__/MaxInputModal.test.tsx.snap +++ b/app/components/UI/Earn/components/MaxInputModal/__snapshots__/MaxInputModal.test.tsx.snap @@ -20,573 +20,539 @@ exports[`MaxInputModal render matches snapshot 1`] = ` } > - - - + + /> + + - + - MaxInput - + + 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. - - - - - - @@ -595,60 +561,117 @@ exports[`MaxInputModal render matches snapshot 1`] = ` style={ { "color": "#131416", - "fontFamily": "Geist-Medium", + "fontFamily": "Geist-Regular", "fontSize": 16, "letterSpacing": 0, "lineHeight": 24, } } > - Cancel + 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. - + - - - Use max - - + + Cancel + + + + + + + Use max + + + @@ -659,9 +682,9 @@ exports[`MaxInputModal render matches snapshot 1`] = ` - - - + + + `; diff --git a/app/components/UI/Earn/components/MerklRewards/hooks/useMerklBonusClaim.test.ts b/app/components/UI/Earn/components/MerklRewards/hooks/useMerklBonusClaim.test.ts index 9c5f2d07571..ada0bde5c55 100644 --- a/app/components/UI/Earn/components/MerklRewards/hooks/useMerklBonusClaim.test.ts +++ b/app/components/UI/Earn/components/MerklRewards/hooks/useMerklBonusClaim.test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react-hooks'; import { CHAIN_IDS } from '@metamask/transaction-controller'; import { useMerklBonusClaim } from './useMerklBonusClaim'; import { TokenI } from '../../../../Tokens/types'; @@ -9,6 +9,7 @@ const mockClaimRewards = jest.fn().mockResolvedValue(undefined); const mockUseMerklRewards = jest.fn((_opts?: unknown) => ({ claimableReward: null as string | null, hasClaimedBefore: false, + rewardsFetchVersion: 0, })); jest.mock('./useMerklRewards', () => ({ @@ -140,6 +141,7 @@ describe('useMerklBonusClaim', () => { mockUseMerklRewards.mockReturnValue({ claimableReward: null, hasClaimedBefore: false, + rewardsFetchVersion: 0, }); mockUsePendingMerklClaim.mockReturnValue({ hasPendingClaim: false }); mockUseMerklClaimTransaction.mockReturnValue({ @@ -211,10 +213,121 @@ describe('useMerklBonusClaim', () => { expect(mockUseMerklClaimTransaction).toHaveBeenCalledWith(eligibleAsset); }); + it('hides CTA after successful claim submission until remount', async () => { + const mockSuccessfulClaimRewards = jest.fn().mockResolvedValue({ + txHash: '0x123', + transactionMeta: {}, + }); + mockUseMerklRewards.mockReturnValue({ + claimableReward: '1.50', + hasClaimedBefore: false, + rewardsFetchVersion: 0, + }); + mockUseMerklClaimTransaction.mockReturnValue({ + claimRewards: mockSuccessfulClaimRewards, + isClaiming: false, + error: null, + }); + + const { result } = renderHook(() => + useMerklBonusClaim(eligibleAsset, 'test_location'), + ); + + expect(result.current.claimableReward).toBe('1.50'); + + await act(async () => { + await result.current.claimRewards(); + }); + + expect(result.current.claimableReward).toBeNull(); + }); + + it('unlocks CTA after rewards refetch version changes', async () => { + const mockSuccessfulClaimRewards = jest.fn().mockResolvedValue({ + txHash: '0x123', + transactionMeta: {}, + }); + mockUseMerklRewards.mockReturnValue({ + claimableReward: '1.50', + hasClaimedBefore: false, + rewardsFetchVersion: 0, + }); + mockUseMerklClaimTransaction.mockReturnValue({ + claimRewards: mockSuccessfulClaimRewards, + isClaiming: false, + error: null, + }); + + const { result, rerender } = renderHook(() => + useMerklBonusClaim(eligibleAsset, 'test_location'), + ); + + await act(async () => { + await result.current.claimRewards(); + }); + + expect(result.current.claimableReward).toBeNull(); + + mockUseMerklRewards.mockReturnValue({ + claimableReward: '1.50', + hasClaimedBefore: false, + rewardsFetchVersion: 1, + }); + rerender(); + + expect(result.current.claimableReward).toBe('1.50'); + }); + + it('locks CTA even when rewardsFetchVersion changes while claim is in flight', async () => { + let resolveClaim: + | ((value: { + txHash: string; + transactionMeta: Record; + }) => void) + | undefined; + const delayedClaim = new Promise<{ + txHash: string; + transactionMeta: Record; + }>((resolve) => { + resolveClaim = resolve; + }); + const mockDelayedClaimRewards = jest.fn().mockReturnValue(delayedClaim); + + let mockedRewardsFetchVersion = 0; + mockUseMerklRewards.mockImplementation(() => ({ + claimableReward: '1.50', + hasClaimedBefore: false, + rewardsFetchVersion: mockedRewardsFetchVersion, + })); + mockUseMerklClaimTransaction.mockReturnValue({ + claimRewards: mockDelayedClaimRewards, + isClaiming: false, + error: null, + }); + + const { result, rerender } = renderHook(() => + useMerklBonusClaim(eligibleAsset, 'test_location'), + ); + + const claimPromise = result.current.claimRewards(); + + mockedRewardsFetchVersion = 1; + rerender(); + expect(result.current.claimableReward).toBe('1.50'); + + await act(async () => { + resolveClaim?.({ txHash: '0x123', transactionMeta: {} }); + await claimPromise; + }); + + expect(result.current.claimableReward).toBeNull(); + }); + it('returns composed data from underlying hooks for eligible asset', () => { mockUseMerklRewards.mockReturnValue({ claimableReward: '1.50', hasClaimedBefore: false, + rewardsFetchVersion: 0, }); mockUsePendingMerklClaim.mockReturnValue({ hasPendingClaim: true }); mockUseMerklClaimTransaction.mockReturnValue({ @@ -230,7 +343,7 @@ describe('useMerklBonusClaim', () => { expect(result.current.claimableReward).toBe('1.50'); expect(result.current.hasPendingClaim).toBe(true); expect(result.current.isClaiming).toBe(true); - expect(result.current.claimRewards).toBe(mockClaimRewards); + expect(typeof result.current.claimRewards).toBe('function'); expect(result.current.error).toBeNull(); }); @@ -238,6 +351,7 @@ describe('useMerklBonusClaim', () => { mockUseMerklRewards.mockReturnValue({ claimableReward: '< 0.01', hasClaimedBefore: false, + rewardsFetchVersion: 0, }); const { result } = renderHook(() => @@ -252,6 +366,7 @@ describe('useMerklBonusClaim', () => { mockUseMerklRewards.mockReturnValue({ claimableReward: '0.005', hasClaimedBefore: false, + rewardsFetchVersion: 0, }); const { result } = renderHook(() => @@ -267,6 +382,7 @@ describe('useMerklBonusClaim', () => { mockUseMerklRewards.mockReturnValue({ claimableReward: '5.00', hasClaimedBefore: false, + rewardsFetchVersion: 0, }); renderHook(() => @@ -280,6 +396,7 @@ describe('useMerklBonusClaim', () => { mockUseMerklRewards.mockReturnValue({ claimableReward: '5.00', hasClaimedBefore: false, + rewardsFetchVersion: 0, }); renderHook(() => @@ -293,6 +410,7 @@ describe('useMerklBonusClaim', () => { mockUseMerklRewards.mockReturnValue({ claimableReward: '5.00', hasClaimedBefore: false, + rewardsFetchVersion: 0, }); mockUsePendingMerklClaim.mockReturnValue({ hasPendingClaim: true }); @@ -307,6 +425,7 @@ describe('useMerklBonusClaim', () => { mockUseMerklRewards.mockReturnValue({ claimableReward: null, hasClaimedBefore: false, + rewardsFetchVersion: 0, }); renderHook(() => @@ -320,6 +439,7 @@ describe('useMerklBonusClaim', () => { mockUseMerklRewards.mockReturnValue({ claimableReward: '< 0.01', hasClaimedBefore: false, + rewardsFetchVersion: 0, }); renderHook(() => @@ -333,6 +453,7 @@ describe('useMerklBonusClaim', () => { mockUseMerklRewards.mockReturnValue({ claimableReward: '0.005', hasClaimedBefore: false, + rewardsFetchVersion: 0, }); renderHook(() => @@ -346,6 +467,7 @@ describe('useMerklBonusClaim', () => { mockUseMerklRewards.mockReturnValue({ claimableReward: '5.00', hasClaimedBefore: false, + rewardsFetchVersion: 0, }); const { rerender } = renderHook(() => @@ -361,6 +483,7 @@ describe('useMerklBonusClaim', () => { mockUseMerklRewards.mockReturnValue({ claimableReward: '5.00', hasClaimedBefore: true, + rewardsFetchVersion: 0, }); renderHook(() => @@ -401,6 +524,7 @@ describe('useMerklBonusClaim', () => { mockUseMerklRewards.mockReturnValue({ claimableReward: bonusValue, hasClaimedBefore: false, + rewardsFetchVersion: 0, }); renderHook(() => diff --git a/app/components/UI/Earn/components/MerklRewards/hooks/useMerklBonusClaim.ts b/app/components/UI/Earn/components/MerklRewards/hooks/useMerklBonusClaim.ts index b456b778c69..4bcd440aee6 100644 --- a/app/components/UI/Earn/components/MerklRewards/hooks/useMerklBonusClaim.ts +++ b/app/components/UI/Earn/components/MerklRewards/hooks/useMerklBonusClaim.ts @@ -1,4 +1,4 @@ -import { useMemo, useRef, useEffect } from 'react'; +import { useMemo, useRef, useEffect, useState, useCallback } from 'react'; import { useSelector } from 'react-redux'; import { Hex } from '@metamask/utils'; import { TokenI } from '../../../../Tokens/types'; @@ -99,20 +99,41 @@ export const useMerklBonusClaim = ( const eligibleAsset = isEligible ? asset : undefined; - const { claimableReward, hasClaimedBefore } = useMerklRewards({ - asset: eligibleAsset, - }); + const { claimableReward, hasClaimedBefore, rewardsFetchVersion } = + useMerklRewards({ + asset: eligibleAsset, + }); const { hasPendingClaim } = usePendingMerklClaim(); const { claimRewards, isClaiming, error: claimError, } = useMerklClaimTransaction(eligibleAsset); + const [claimLockFetchVersion, setClaimLockFetchVersion] = useState< + number | null + >(null); + const latestRewardsFetchVersionRef = useRef(rewardsFetchVersion); + useEffect(() => { + latestRewardsFetchVersionRef.current = rewardsFetchVersion; + }, [rewardsFetchVersion]); + const isClaimLocked = + claimLockFetchVersion !== null && + claimLockFetchVersion === rewardsFetchVersion; + + const claimRewardsWithSessionLock = useCallback(async () => { + const claimResult = await claimRewards(); + // Keep CTA hidden until the next rewards refetch resolves. + if (claimResult) { + setClaimLockFetchVersion(latestRewardsFetchVersionRef.current); + } + return claimResult; + }, [claimRewards]); const hasClaimableBonus = isEligible && isClaimableBonusAboveThreshold(claimableReward) && - !hasPendingClaim; + !hasPendingClaim && + !isClaimLocked; const hasFiredCtaAvailableEvent = useRef(false); @@ -159,11 +180,12 @@ export const useMerklBonusClaim = ( } return { - claimableReward: isClaimableBonusAboveThreshold(claimableReward) - ? claimableReward - : null, + claimableReward: + !isClaimLocked && isClaimableBonusAboveThreshold(claimableReward) + ? claimableReward + : null, hasPendingClaim, - claimRewards, + claimRewards: claimRewardsWithSessionLock, isClaiming, error: claimError, }; @@ -171,8 +193,9 @@ export const useMerklBonusClaim = ( isEligible, claimableReward, hasPendingClaim, - claimRewards, + claimRewardsWithSessionLock, isClaiming, claimError, + isClaimLocked, ]); }; diff --git a/app/components/UI/Earn/components/MerklRewards/hooks/useMerklRewards.test.ts b/app/components/UI/Earn/components/MerklRewards/hooks/useMerklRewards.test.ts index 93450eca2cd..660390fa441 100644 --- a/app/components/UI/Earn/components/MerklRewards/hooks/useMerklRewards.test.ts +++ b/app/components/UI/Earn/components/MerklRewards/hooks/useMerklRewards.test.ts @@ -174,7 +174,7 @@ describe('useMerklRewards', () => { }); it('initializes with null claimableReward', () => { - const { result } = renderHook(() => useMerklRewards({ asset: mockAsset })); + const { result } = renderHook(() => useMerklRewards({ asset: undefined })); expect(result.current.claimableReward).toBe(null); }); @@ -996,4 +996,103 @@ describe('useMerklRewards', () => { expect(mockFetchMerklRewardsForAsset).toHaveBeenCalled(); expect(mockGetClaimedAmountFromContract).toHaveBeenCalled(); }); + + it('clears stale claimableReward when refetch returns no matching reward', async () => { + const mockRewardData = { + token: { + address: AGLAMERKL_ADDRESS_MAINNET, + chainId: 1, + symbol: 'aglaMerkl', + decimals: 18, + price: null, + }, + accumulated: '0', + unclaimed: '1500000000000000000', + pending: '0', + proofs: [], + amount: '1500000000000000000', + claimed: '0', + recipient: mockSelectedAddress, + }; + + mockFetchMerklRewardsForAsset.mockResolvedValueOnce(mockRewardData); + mockGetClaimedAmountFromContract.mockResolvedValueOnce('0'); + + const { result } = renderHook(() => useMerklRewards({ asset: mockAsset })); + + await waitFor(() => { + expect(result.current.claimableReward).toBe('1.50'); + }); + + mockFetchMerklRewardsForAsset.mockResolvedValueOnce(null); + + act(() => { + result.current.refetch(); + }); + + await waitFor(() => { + expect(result.current.claimableReward).toBe(null); + }); + }); + + it('starts auto-refresh interval and clears it on unmount', () => { + const intervalId = 123 as unknown as ReturnType; + const setIntervalSpy = jest + .spyOn(global, 'setInterval') + .mockReturnValue(intervalId); + const clearIntervalSpy = jest + .spyOn(global, 'clearInterval') + .mockImplementation(() => undefined); + + const { unmount } = renderHook(() => useMerklRewards({ asset: mockAsset })); + + expect(setIntervalSpy).toHaveBeenCalledWith(expect.any(Function), 60000); + + unmount(); + + expect(clearIntervalSpy).toHaveBeenCalledWith(intervalId); + + setIntervalSpy.mockRestore(); + clearIntervalSpy.mockRestore(); + }); + + it('increments rewardsFetchVersion after successful fetch and refetch', async () => { + const mockRewardData = { + token: { + address: AGLAMERKL_ADDRESS_MAINNET, + chainId: 1, + symbol: 'aglaMerkl', + decimals: 18, + price: null, + }, + accumulated: '0', + unclaimed: '1500000000000000000', + pending: '0', + proofs: [], + amount: '1500000000000000000', + claimed: '0', + recipient: mockSelectedAddress, + }; + + mockFetchMerklRewardsForAsset.mockResolvedValue(mockRewardData); + mockGetClaimedAmountFromContract.mockResolvedValue('0'); + + const { result } = renderHook(() => useMerklRewards({ asset: mockAsset })); + + await waitFor(() => { + expect(result.current.rewardsFetchVersion).toBeGreaterThan(0); + }); + + const versionAfterInitialFetch = result.current.rewardsFetchVersion; + + act(() => { + result.current.refetch(); + }); + + await waitFor(() => { + expect(result.current.rewardsFetchVersion).toBeGreaterThan( + versionAfterInitialFetch, + ); + }); + }); }); diff --git a/app/components/UI/Earn/components/MerklRewards/hooks/useMerklRewards.ts b/app/components/UI/Earn/components/MerklRewards/hooks/useMerklRewards.ts index e1dec172f61..79c401c7e27 100644 --- a/app/components/UI/Earn/components/MerklRewards/hooks/useMerklRewards.ts +++ b/app/components/UI/Earn/components/MerklRewards/hooks/useMerklRewards.ts @@ -17,6 +17,7 @@ import Logger from '../../../../../../util/Logger'; const MUSD_ADDRESS = MUSD_TOKEN_ADDRESS_BY_CHAIN[CHAIN_IDS.LINEA_MAINNET]; const MUSD_ADDRESS_MAINNET = MUSD_TOKEN_ADDRESS_BY_CHAIN[CHAIN_IDS.MAINNET]; +const MERKL_REWARDS_AUTO_REFRESH_INTERVAL_MS = 60_000; // Map of chains and eligible tokens // mUSD on mainnet is eligible because users earn rewards for holding it, @@ -59,6 +60,7 @@ interface UseMerklRewardsReturn { claimableReward: string | null; hasClaimedBefore: boolean; refetch: () => void; + rewardsFetchVersion: number; } /** @@ -69,6 +71,7 @@ export const useMerklRewards = ({ }: UseMerklRewardsOptions): UseMerklRewardsReturn => { const [claimableReward, setClaimableReward] = useState(null); const [hasClaimedBefore, setHasClaimedBefore] = useState(false); + const [rewardsFetchVersion, setRewardsFetchVersion] = useState(0); const selectedAddress = useSelector( selectSelectedInternalAccountFormattedAddress, @@ -111,6 +114,7 @@ export const useMerklRewards = ({ } if (!matchingReward) { + setClaimableReward(null); setHasClaimedBefore(false); return; } @@ -169,6 +173,10 @@ export const useMerklRewards = ({ error as Error, 'useMerklRewards: Error fetching claimable rewards', ); + } finally { + if (!controller.signal.aborted) { + setRewardsFetchVersion((version) => version + 1); + } } }, [asset, selectedAddress], @@ -191,9 +199,24 @@ export const useMerklRewards = ({ }; }, [fetchClaimableRewards]); + useEffect(() => { + if (!asset || !selectedAddress) { + return; + } + + const intervalId = setInterval(() => { + refetch(); + }, MERKL_REWARDS_AUTO_REFRESH_INTERVAL_MS); + + return () => { + clearInterval(intervalId); + }; + }, [asset, selectedAddress, refetch]); + return { claimableReward, hasClaimedBefore, refetch, + rewardsFetchVersion, }; }; diff --git a/app/components/UI/Earn/hooks/useMusdConversion.test.ts b/app/components/UI/Earn/hooks/useMusdConversion.test.ts index b3be0d3c9e8..c96fcdae686 100644 --- a/app/components/UI/Earn/hooks/useMusdConversion.test.ts +++ b/app/components/UI/Earn/hooks/useMusdConversion.test.ts @@ -67,8 +67,6 @@ const mockNavigation = { addListener: jest.fn(), removeListener: jest.fn(), getId: jest.fn(), - dangerouslyGetParent: jest.fn(), - dangerouslyGetState: jest.fn(), }; const mockNetworkController = { diff --git a/app/components/UI/Earn/routes/index.test.tsx b/app/components/UI/Earn/routes/index.test.tsx new file mode 100644 index 00000000000..4c0bc28b039 --- /dev/null +++ b/app/components/UI/Earn/routes/index.test.tsx @@ -0,0 +1,182 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import { NavigationContainer } from '@react-navigation/native'; +import { Provider } from 'react-redux'; +import configureMockStore from 'redux-mock-store'; +import { EarnScreenStack, EarnModalStack } from './index'; +import Routes from '../../../../constants/navigation/Routes'; +import { backgroundState } from '../../../../util/test/initial-root-state'; + +jest.mock('../../Earn/Views/EarnLendingDepositConfirmationView', () => { + const MockView = () => { + const { View, Text } = jest.requireActual('react-native'); + return ( + + Lending Deposit Confirmation + + ); + }; + MockView.displayName = 'MockEarnLendingDepositConfirmationView'; + return MockView; +}); + +jest.mock('../Views/EarnLendingWithdrawalConfirmationView', () => { + const MockView = () => { + const { View, Text } = jest.requireActual('react-native'); + return ( + + Lending Withdrawal Confirmation + + ); + }; + MockView.displayName = 'MockEarnLendingWithdrawalConfirmationView'; + return MockView; +}); + +jest.mock('../Views/EarnMusdConversionEducationView', () => { + const MockView = () => { + const { View, Text } = jest.requireActual('react-native'); + return ( + + MUSD Conversion Education + + ); + }; + MockView.displayName = 'MockEarnMusdConversionEducationView'; + return MockView; +}); + +jest.mock('../Views/MusdQuickConvertView', () => { + const MockView = () => { + const { View, Text } = jest.requireActual('react-native'); + return ( + + MUSD Quick Convert + + ); + }; + MockView.displayName = 'MockMusdQuickConvertView'; + return MockView; +}); + +jest.mock('../modals/LendingMaxWithdrawalModal', () => { + const MockModal = () => { + const { View, Text } = jest.requireActual('react-native'); + return ( + + Lending Max Withdrawal Modal + + ); + }; + MockModal.displayName = 'MockEarnLendingMaxWithdrawalModal'; + return MockModal; +}); + +jest.mock('../LendingLearnMoreModal', () => { + const MockModal = () => { + const { View, Text } = jest.requireActual('react-native'); + return ( + + Lending Learn More Modal + + ); + }; + MockModal.displayName = 'MockLendingLearnMoreModal'; + return MockModal; +}); + +jest.mock('../../../Views/confirmations/components/confirm', () => ({ + Confirm: () => { + const { View, Text } = jest.requireActual('react-native'); + return ( + + Confirm + + ); + }, +})); + +jest.mock( + '../../../Views/confirmations/hooks/ui/useEmptyNavHeaderForConfirmations', + () => ({ + useEmptyNavHeaderForConfirmations: () => ({ + headerShown: false, + }), + }), +); + +const mockStore = configureMockStore(); +const initialState = { + engine: { + backgroundState, + }, +}; + +const renderWithProviders = (component: React.ReactElement) => { + const store = mockStore(initialState); + return render( + + {component} + , + ); +}; + +describe('EarnScreenStack', () => { + it('renders correctly', () => { + const { toJSON } = renderWithProviders(); + expect(toJSON()).toBeTruthy(); + }); + + it('defines lending deposit confirmation route', () => { + expect(Routes.EARN.LENDING_DEPOSIT_CONFIRMATION).toBeDefined(); + }); + + it('defines lending withdrawal confirmation route', () => { + expect(Routes.EARN.LENDING_WITHDRAWAL_CONFIRMATION).toBeDefined(); + }); + + it('defines MUSD conversion education route', () => { + expect(Routes.EARN.MUSD.CONVERSION_EDUCATION).toBeDefined(); + }); + + it('defines MUSD quick convert route', () => { + expect(Routes.EARN.MUSD.QUICK_CONVERT).toBeDefined(); + }); + + it('defines full screen confirmations route', () => { + expect( + Routes.FULL_SCREEN_CONFIRMATIONS.REDESIGNED_CONFIRMATIONS, + ).toBeDefined(); + }); +}); + +describe('EarnModalStack', () => { + it('renders correctly', () => { + const { toJSON } = renderWithProviders(); + expect(toJSON()).toBeTruthy(); + }); + + it('defines lending max withdrawal modal route', () => { + expect(Routes.EARN.MODALS.LENDING_MAX_WITHDRAWAL).toBeDefined(); + }); + + it('defines lending learn more modal route', () => { + expect(Routes.EARN.MODALS.LENDING_LEARN_MORE).toBeDefined(); + }); + + it('defines full screen confirmations modal route', () => { + expect( + Routes.FULL_SCREEN_CONFIRMATIONS.REDESIGNED_CONFIRMATIONS, + ).toBeDefined(); + }); +}); + +describe('Route Constants', () => { + it('has earn root route defined', () => { + expect(Routes.EARN.ROOT).toBeDefined(); + }); + + it('has earn modals root route defined', () => { + expect(Routes.EARN.MODALS.ROOT).toBeDefined(); + }); +}); diff --git a/app/components/UI/Earn/routes/index.tsx b/app/components/UI/Earn/routes/index.tsx index d28eef44880..a4aa6166a92 100644 --- a/app/components/UI/Earn/routes/index.tsx +++ b/app/components/UI/Earn/routes/index.tsx @@ -25,7 +25,7 @@ const EarnScreenStack = () => { const emptyNavHeaderOptions = useEmptyNavHeaderForConfirmations(); return ( - + { }; const EarnModalStack = () => ( - + { }; export const handleTronStakingNavigationResult = ( - navigation: NavigationProp, + navigation: AppNavigationProp, result: TronStakingNavigationResult, action: TronStakingAction, accountId?: string, diff --git a/app/components/UI/FundActionMenu/FundActionMenu.test.tsx b/app/components/UI/FundActionMenu/FundActionMenu.test.tsx index ea3067610a0..75111645182 100644 --- a/app/components/UI/FundActionMenu/FundActionMenu.test.tsx +++ b/app/components/UI/FundActionMenu/FundActionMenu.test.tsx @@ -53,9 +53,6 @@ jest.mock( // Mock dependencies jest.mock('@react-navigation/native'); -jest.mock('@react-navigation/compat', () => ({ - withNavigation: jest.fn((component) => component), -})); jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), useSelector: jest.fn(), diff --git a/app/components/UI/Identity/ConfirmTurnOnBackupAndSyncModal/ConfirmTurnOnBackupAndSyncModal.test.tsx b/app/components/UI/Identity/ConfirmTurnOnBackupAndSyncModal/ConfirmTurnOnBackupAndSyncModal.test.tsx index d378eeae2ed..c1ed6c275ea 100644 --- a/app/components/UI/Identity/ConfirmTurnOnBackupAndSyncModal/ConfirmTurnOnBackupAndSyncModal.test.tsx +++ b/app/components/UI/Identity/ConfirmTurnOnBackupAndSyncModal/ConfirmTurnOnBackupAndSyncModal.test.tsx @@ -60,7 +60,7 @@ jest.mock('@react-navigation/native', () => { setOptions: jest.fn(), goBack: jest.fn(), reset: jest.fn(), - dangerouslyGetParent: () => ({ + getParent: () => ({ pop: jest.fn(), }), isFocused: jest.fn(() => true), diff --git a/app/components/UI/MarketInsights/Views/MarketInsightsView/MarketInsightsView.test.tsx b/app/components/UI/MarketInsights/Views/MarketInsightsView/MarketInsightsView.test.tsx index f5e87172a24..439a107e893 100644 --- a/app/components/UI/MarketInsights/Views/MarketInsightsView/MarketInsightsView.test.tsx +++ b/app/components/UI/MarketInsights/Views/MarketInsightsView/MarketInsightsView.test.tsx @@ -345,6 +345,24 @@ describe('MarketInsightsView', () => { expect(queryByTestId(MarketInsightsSelectorsIDs.VIEW_CONTAINER)).toBeNull(); }); + it('configures background video to mix with other audio', () => { + mockUseMarketInsights.mockReturnValue({ + report: buildMockReport(), + isLoading: false, + error: null, + timeAgo: '5m ago', + }); + + const { getByTestId } = renderWithProvider(); + + const backgroundVideo = getByTestId( + MarketInsightsSelectorsIDs.BACKGROUND_ANIMATION, + ); + + expect(backgroundVideo.props.ignoreSilentSwitch).toBe('obey'); + expect(backgroundVideo.props.mixWithOthers).toBe('mix'); + }); + it('renders report content and handles tweet/swap/buy actions', () => { mockUseMarketInsights.mockReturnValue({ report: { diff --git a/app/components/UI/MarketInsights/Views/MarketInsightsView/MarketInsightsView.tsx b/app/components/UI/MarketInsights/Views/MarketInsightsView/MarketInsightsView.tsx index 36c55a3265a..95ca778102d 100644 --- a/app/components/UI/MarketInsights/Views/MarketInsightsView/MarketInsightsView.tsx +++ b/app/components/UI/MarketInsights/Views/MarketInsightsView/MarketInsightsView.tsx @@ -551,17 +551,14 @@ const MarketInsightsView: React.FC = () => { } trackMarketInsightsInteraction('source_click', { source: url }); setSelectedTrend(null); - navigation.navigate( - Routes.BROWSER.HOME as never, - { - screen: Routes.BROWSER.VIEW, - params: { - newTabUrl: url, - timestamp: Date.now(), - fromTrending: true, - }, - } as never, - ); + navigation.navigate(Routes.BROWSER.HOME, { + screen: Routes.BROWSER.VIEW, + params: { + newTabUrl: url, + timestamp: Date.now(), + fromTrending: true, + }, + }); }, [trackMarketInsightsInteraction, navigation, setSelectedTrend], ); @@ -639,6 +636,8 @@ const MarketInsightsView: React.FC = () => { paused={false} controls={false} disableFocus + ignoreSilentSwitch="obey" + mixWithOthers="mix" onEnd={handleVideoEnd} testID={MarketInsightsSelectorsIDs.BACKGROUND_ANIMATION} /> diff --git a/app/components/UI/MultichainBridgeTransactionListItem/MultichainBridgeTransactionListItem.tsx b/app/components/UI/MultichainBridgeTransactionListItem/MultichainBridgeTransactionListItem.tsx index 5cb87b202fe..ab09e422929 100644 --- a/app/components/UI/MultichainBridgeTransactionListItem/MultichainBridgeTransactionListItem.tsx +++ b/app/components/UI/MultichainBridgeTransactionListItem/MultichainBridgeTransactionListItem.tsx @@ -1,4 +1,3 @@ -import { NavigationProp, ParamListBase } from '@react-navigation/native'; import React from 'react'; import { Image, @@ -6,6 +5,7 @@ import { TextStyle, useColorScheme, } from 'react-native'; +import type { AppNavigationProp } from '../../../core/NavigationService/types'; import { Transaction } from '@metamask/keyring-api'; import { BridgeHistoryItem } from '@metamask/bridge-status-controller'; import { useTheme } from '../../../util/theme'; @@ -46,7 +46,7 @@ const MultichainBridgeTransactionListItem = ({ }: { transaction: Transaction; bridgeHistoryItem: BridgeHistoryItem; - navigation: NavigationProp; + navigation: AppNavigationProp; index?: number; location?: TransactionDetailLocation; showDestinationPerspective?: boolean; diff --git a/app/components/UI/MultichainTransactionDetailsModal/MultichainTransactionDetailsSheet.tsx b/app/components/UI/MultichainTransactionDetailsModal/MultichainTransactionDetailsSheet.tsx index b28718d67ac..bff2e329ed9 100644 --- a/app/components/UI/MultichainTransactionDetailsModal/MultichainTransactionDetailsSheet.tsx +++ b/app/components/UI/MultichainTransactionDetailsModal/MultichainTransactionDetailsSheet.tsx @@ -96,13 +96,10 @@ const MultichainTransactionDetailsSheet: React.FC = () => { // Close the bottom sheet and navigate to webview sheetRef.current?.onCloseBottomSheet(() => { - navigation.navigate( - Routes.WEBVIEW.MAIN as never, - { - screen: Routes.WEBVIEW.SIMPLE, - params: { url }, - } as never, - ); + navigation.navigate(Routes.WEBVIEW.MAIN, { + screen: Routes.WEBVIEW.SIMPLE, + params: { url }, + }); }); }, [id, chain, from?.address, to?.address, navigation], diff --git a/app/components/UI/MultichainTransactionListItem/MultichainTransactionListItem.test.tsx b/app/components/UI/MultichainTransactionListItem/MultichainTransactionListItem.test.tsx index 22f32518211..64ceb5bc8c7 100644 --- a/app/components/UI/MultichainTransactionListItem/MultichainTransactionListItem.test.tsx +++ b/app/components/UI/MultichainTransactionListItem/MultichainTransactionListItem.test.tsx @@ -227,6 +227,7 @@ describe('MultichainTransactionListItem', () => { expect.objectContaining({ screen: Routes.SHEET.MULTICHAIN_TRANSACTION_DETAILS, params: expect.objectContaining({ + displayData: expect.any(Object), transaction: mockTransaction, }), }), diff --git a/app/components/UI/MultichainTransactionListItem/MultichainTransactionListItem.tsx b/app/components/UI/MultichainTransactionListItem/MultichainTransactionListItem.tsx index 10ac85793cb..24b416ef220 100644 --- a/app/components/UI/MultichainTransactionListItem/MultichainTransactionListItem.tsx +++ b/app/components/UI/MultichainTransactionListItem/MultichainTransactionListItem.tsx @@ -1,5 +1,5 @@ -import { NavigationProp, ParamListBase } from '@react-navigation/native'; import React, { useCallback } from 'react'; +import type { AppNavigationProp } from '../../../core/NavigationService/types'; import { Image, TouchableHighlight, @@ -38,7 +38,7 @@ const MultichainTransactionListItem = ({ }: { transaction: Transaction; chainId: SupportedCaipChainId; - navigation: NavigationProp; + navigation: AppNavigationProp; index?: number; location?: TransactionDetailLocation; }) => { @@ -63,13 +63,10 @@ const MultichainTransactionListItem = ({ .build(), ); - navigation.navigate( - Routes.MODAL.ROOT_MODAL_FLOW as never, - { - screen: Routes.SHEET.MULTICHAIN_TRANSACTION_DETAILS, - params: { displayData, transaction }, - } as never, - ); + navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, { + screen: Routes.SHEET.MULTICHAIN_TRANSACTION_DETAILS, + params: { displayData, transaction }, + }); }, [ navigation, displayData, diff --git a/app/components/UI/Navbar/index.js b/app/components/UI/Navbar/index.js index 403e06288b6..a426ba2e0e1 100644 --- a/app/components/UI/Navbar/index.js +++ b/app/components/UI/Navbar/index.js @@ -890,7 +890,7 @@ export function getPaymentSelectorMethodNavbar(navigation, onPop, themeColors) { // eslint-disable-next-line react/jsx-no-bind { - navigation.dangerouslyGetParent()?.pop(); + navigation.getParent()?.pop(); onPop?.(); }} style={styles.closeButton} @@ -935,7 +935,7 @@ export function getPaymentMethodApplePayNavbar( // eslint-disable-next-line react/jsx-no-bind { - navigation.dangerouslyGetParent()?.pop(); + navigation.getParent()?.pop(); onExit?.(); }} style={styles.closeButton} @@ -1064,7 +1064,7 @@ export function getSwapsAmountNavbar(navigation, route, themeColors) { headerRight: () => ( // eslint-disable-next-line react/jsx-no-bind navigation.dangerouslyGetParent()?.pop()} + onPress={() => navigation.getParent()?.pop()} style={styles.closeButton} > @@ -1129,7 +1129,7 @@ export function getSwapsQuotesNavbar(navigation, route, themeColors) { const rightAction = () => { trackQuotesCancelledIfNeeded(); - navigation.dangerouslyGetParent()?.pop(); + navigation.getParent()?.pop(); }; return { @@ -1177,7 +1177,7 @@ export function getBridgeNavbar(navigation, bridgeViewMode, themeColors) { return getHeaderCompactStandardNavbarOptions({ title, - onClose: () => navigation.dangerouslyGetParent()?.pop(), + onClose: () => navigation.getParent()?.pop(), includesTopInset: true, }); } diff --git a/app/components/UI/Navbar/index.test.js b/app/components/UI/Navbar/index.test.js new file mode 100644 index 00000000000..d86bc22d600 --- /dev/null +++ b/app/components/UI/Navbar/index.test.js @@ -0,0 +1,858 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react-native'; +import { + getTransactionsNavbarOptions, + getNavigationOptionsTitle, + getEditableOptions, + getTransactionOptionsTitle, + getApproveNavbar, + getModalNavbarOptions, + getOnboardingNavbarOptions, + getTransparentOnboardingNavbarOptions, + getTransparentBackOnboardingNavbarOptions, + getOptinMetricsNavbarOptions, + getClosableNavigationOptions, + getOfflineModalNavbar, + getDepositNavbarOptions, + getEditAccountNameNavBarOptions, + getNetworkNavbarOptions, + getBridgeNavbar, + getBridgeTransactionDetailsNavbar, + getStakingNavbar, + getDeFiProtocolPositionDetailsNavbarOptions, + getRampsOrderDetailsNavbarOptions, + getPaymentSelectorMethodNavbar, + getPaymentMethodApplePayNavbar, + getSwapsAmountNavbar, + getSwapsQuotesNavbar, +} from './index'; +import { BridgeViewMode } from '../Bridge/types'; + +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: () => ({ + navigate: jest.fn(), + goBack: jest.fn(), + setOptions: jest.fn(), + }), +})); + +/* eslint-disable @metamask/design-tokens/color-no-hex -- theme mock uses hex for test compatibility */ +const mockThemeColors = { + background: { + default: '#FFFFFF', + primary: '#F5F5F5', + }, + text: { + default: '#000000', + }, + primary: { + default: '#037DD6', + }, + icon: { + default: '#24272A', + }, + overlay: { + default: 'rgba(0,0,0,0.5)', + }, +}; +/* eslint-enable @metamask/design-tokens/color-no-hex */ + +const mockNavigation = { + goBack: jest.fn(), + pop: jest.fn(), + setOptions: jest.fn(), + navigate: jest.fn(), + getParent: jest.fn(() => ({ + pop: jest.fn(), + })), +}; + +const mockRoute = { + params: {}, + name: 'TestRoute', +}; + +describe('Navbar', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('getTransactionsNavbarOptions', () => { + it('returns correct navbar options with title', () => { + const options = getTransactionsNavbarOptions( + 'Transactions', + mockThemeColors, + jest.fn(), + '0x1234567890abcdef', + jest.fn(), + ); + + expect(options).toHaveProperty('headerTitle'); + expect(options).toHaveProperty('headerLeft'); + expect(options).toHaveProperty('headerRight'); + expect(options).toHaveProperty('headerStyle'); + expect(options.headerTintColor).toBe(mockThemeColors.primary.default); + }); + + it('returns headerTitle function', () => { + const options = getTransactionsNavbarOptions( + 'Transactions', + mockThemeColors, + jest.fn(), + '0x1234567890abcdef', + jest.fn(), + ); + + expect(typeof options.headerTitle).toBe('function'); + }); + + it('renders headerRight component', () => { + const handleRightPress = jest.fn(); + const options = getTransactionsNavbarOptions( + 'Transactions', + mockThemeColors, + jest.fn(), + '0x1234567890abcdef', + handleRightPress, + ); + + const HeaderRight = options.headerRight; + expect(HeaderRight).toBeDefined(); + }); + }); + + describe('getNavigationOptionsTitle', () => { + it('returns correct options with title', () => { + const options = getNavigationOptionsTitle( + 'Settings', + mockNavigation, + false, + mockThemeColors, + ); + + expect(options.title).toBe('Settings'); + expect(options.headerTitleAlign).toBe('center'); + expect(options).toHaveProperty('headerLeft'); + expect(options).toHaveProperty('headerRight'); + }); + + it('shows close button for full screen modal', () => { + const options = getNavigationOptionsTitle( + 'Settings', + mockNavigation, + true, + mockThemeColors, + ); + + const HeaderRight = options.headerRight; + expect(HeaderRight).toBeDefined(); + }); + + it('shows back button for non-full screen modal', () => { + const options = getNavigationOptionsTitle( + 'Settings', + mockNavigation, + false, + mockThemeColors, + ); + + const HeaderLeft = options.headerLeft; + expect(HeaderLeft).toBeDefined(); + }); + }); + + describe('getEditableOptions', () => { + it('returns correct options for edit mode', () => { + const routeWithEditMode = { + ...mockRoute, + params: { editMode: 'edit', dispatch: jest.fn() }, + }; + const options = getEditableOptions( + 'Contact', + mockNavigation, + routeWithEditMode, + mockThemeColors, + ); + + expect(options.title).toBe('Contact'); + expect(options).toHaveProperty('headerLeft'); + expect(options).toHaveProperty('headerRight'); + expect(options).toHaveProperty('headerStyle'); + }); + + it('returns correct options for add mode', () => { + const routeWithAddMode = { + ...mockRoute, + params: { mode: 'add' }, + }; + const options = getEditableOptions( + 'Contact', + mockNavigation, + routeWithAddMode, + mockThemeColors, + ); + + expect(options.title).toBe('Contact'); + }); + }); + + describe('getTransactionOptionsTitle', () => { + it('returns correct options', () => { + const options = getTransactionOptionsTitle( + 'transaction.confirm', + mockNavigation, + mockRoute, + mockThemeColors, + ); + + expect(options).toHaveProperty('headerTitle'); + expect(options).toHaveProperty('headerLeft'); + expect(options).toHaveProperty('headerRight'); + expect(options).toHaveProperty('headerStyle'); + }); + + it('returns edit title when mode is edit', () => { + const routeWithEditMode = { + ...mockRoute, + params: { mode: 'edit' }, + }; + const options = getTransactionOptionsTitle( + 'transaction.confirm', + mockNavigation, + routeWithEditMode, + mockThemeColors, + ); + + expect(options).toHaveProperty('headerTitle'); + }); + }); + + describe('getApproveNavbar', () => { + it('returns correct options', () => { + const options = getApproveNavbar('Approve'); + + expect(options).toHaveProperty('headerTitle'); + expect(options).toHaveProperty('headerLeft'); + expect(options).toHaveProperty('headerRight'); + }); + }); + + describe('getModalNavbarOptions', () => { + it('returns correct options with title', () => { + const options = getModalNavbarOptions('Modal Title'); + + expect(options).toHaveProperty('headerTitle'); + }); + }); + + describe('getOnboardingNavbarOptions', () => { + it('returns correct options', () => { + const options = getOnboardingNavbarOptions( + mockRoute, + {}, + mockThemeColors, + true, + ); + + expect(options).toHaveProperty('headerStyle'); + expect(options).toHaveProperty('headerTitle'); + expect(options).toHaveProperty('headerRight'); + expect(options).toHaveProperty('headerLeft'); + }); + + it('hides logo when showLogo is false', () => { + const options = getOnboardingNavbarOptions( + mockRoute, + {}, + mockThemeColors, + false, + ); + + expect(options.headerTitle).toBeNull(); + }); + }); + + describe('getTransparentOnboardingNavbarOptions', () => { + it('returns correct options', () => { + const options = getTransparentOnboardingNavbarOptions(mockThemeColors); + + expect(options).toHaveProperty('headerTitle'); + expect(options).toHaveProperty('headerLeft'); + expect(options).toHaveProperty('headerRight'); + expect(options).toHaveProperty('headerStyle'); + }); + + it('uses custom background color when provided', () => { + const testColor = 'rgb(255, 0, 0)'; + const options = getTransparentOnboardingNavbarOptions( + mockThemeColors, + testColor, + ); + + expect(options.headerStyle.backgroundColor).toBe(testColor); + }); + + it('hides logo when showLogo is false', () => { + const options = getTransparentOnboardingNavbarOptions( + mockThemeColors, + undefined, + false, + ); + + expect(options.headerTitle()).toBeNull(); + }); + }); + + describe('getTransparentBackOnboardingNavbarOptions', () => { + it('returns correct options', () => { + const options = + getTransparentBackOnboardingNavbarOptions(mockThemeColors); + + expect(options).toHaveProperty('headerTitle'); + expect(options).toHaveProperty('headerBackTitle'); + expect(options).toHaveProperty('headerRight'); + expect(options).toHaveProperty('headerStyle'); + }); + }); + + describe('getOptinMetricsNavbarOptions', () => { + it('returns correct options', () => { + const options = getOptinMetricsNavbarOptions(mockThemeColors, true); + + expect(options).toHaveProperty('headerTitle'); + expect(options).toHaveProperty('headerBackTitle'); + expect(options).toHaveProperty('headerRight'); + expect(options).toHaveProperty('headerLeft'); + expect(options).toHaveProperty('headerStyle'); + }); + + it('hides logo when showLogo is false', () => { + const options = getOptinMetricsNavbarOptions(mockThemeColors, false); + + expect(options.headerTitle()).toBeNull(); + }); + }); + + describe('getClosableNavigationOptions', () => { + it('returns correct options', () => { + const options = getClosableNavigationOptions( + 'Title', + 'Back', + mockNavigation, + mockThemeColors, + ); + + expect(options.title).toBe('Title'); + expect(options).toHaveProperty('headerTitleStyle'); + expect(options).toHaveProperty('headerLeft'); + expect(options).toHaveProperty('headerStyle'); + }); + }); + + describe('getOfflineModalNavbar', () => { + it('returns correct options', () => { + const options = getOfflineModalNavbar(); + + expect(options.headerShown).toBe(false); + }); + }); + + describe('getDepositNavbarOptions', () => { + it('returns correct options with title', () => { + const options = getDepositNavbarOptions( + mockNavigation, + { title: 'Deposit' }, + { colors: mockThemeColors }, + ); + + expect(options).toHaveProperty('header'); + }); + + it('shows back button when showBack is true', () => { + const options = getDepositNavbarOptions( + mockNavigation, + { title: 'Deposit', showBack: true }, + { colors: mockThemeColors }, + ); + + expect(options).toHaveProperty('header'); + }); + + it('shows configuration button when showConfiguration is true', () => { + const options = getDepositNavbarOptions( + mockNavigation, + { + title: 'Deposit', + showConfiguration: true, + onConfigurationPress: jest.fn(), + }, + { colors: mockThemeColors }, + ); + + expect(options).toHaveProperty('header'); + }); + + it('invokes onClose after pop when back is pressed and onClose is provided', () => { + const onClose = jest.fn(); + const options = getDepositNavbarOptions( + mockNavigation, + { title: 'Deposit', showBack: true }, + { colors: mockThemeColors }, + onClose, + ); + + const { getByTestId } = render(options.header()); + fireEvent.press(getByTestId('deposit-back-navbar-button')); + + expect(mockNavigation.pop).toHaveBeenCalled(); + expect(onClose).toHaveBeenCalled(); + }); + + it('omits start button when showBack and showClose are false', () => { + const options = getDepositNavbarOptions( + mockNavigation, + { + title: 'Deposit', + showBack: false, + showClose: false, + }, + { colors: mockThemeColors }, + ); + + const { queryByTestId } = render(options.header()); + expect(queryByTestId('deposit-back-navbar-button')).toBeNull(); + }); + }); + + describe('getEditAccountNameNavBarOptions', () => { + it('returns correct options', () => { + const goBack = jest.fn(); + const options = getEditAccountNameNavBarOptions(goBack, mockThemeColors); + + expect(options).toHaveProperty('headerTitle'); + expect(options.headerLeft).toBeNull(); + expect(options).toHaveProperty('headerRight'); + }); + }); + + describe('getNetworkNavbarOptions', () => { + it('returns correct options', () => { + const options = getNetworkNavbarOptions( + 'Network', + false, + mockNavigation, + mockThemeColors, + ); + + expect(options).toHaveProperty('header'); + }); + + it('shows right button when onRightPress is provided', () => { + const onRightPress = jest.fn(); + const options = getNetworkNavbarOptions( + 'Network', + false, + mockNavigation, + mockThemeColors, + onRightPress, + ); + + expect(options).toHaveProperty('header'); + }); + }); + + describe('getBridgeNavbar', () => { + it('returns correct options for Bridge mode', () => { + const options = getBridgeNavbar( + mockNavigation, + BridgeViewMode.Bridge, + mockThemeColors, + ); + + expect(options).toBeDefined(); + }); + + it('returns correct options for Swap mode', () => { + const options = getBridgeNavbar( + mockNavigation, + BridgeViewMode.Swap, + mockThemeColors, + ); + + expect(options).toBeDefined(); + }); + }); + + describe('getBridgeTransactionDetailsNavbar', () => { + it('returns correct options', () => { + const options = getBridgeTransactionDetailsNavbar(mockNavigation); + + expect(options).toHaveProperty('headerTitle'); + expect(options).toHaveProperty('headerLeft'); + }); + }); + + describe('getStakingNavbar', () => { + it('returns correct options', () => { + const options = getStakingNavbar( + 'Staking', + mockNavigation, + mockThemeColors, + ); + + expect(options).toHaveProperty('headerTitle'); + expect(options).toHaveProperty('headerStyle'); + expect(options).toHaveProperty('headerLeft'); + expect(options).toHaveProperty('headerRight'); + }); + + it('hides back button when hasBackButton is false', () => { + const options = getStakingNavbar( + 'Staking', + mockNavigation, + mockThemeColors, + { hasBackButton: false }, + ); + + expect(options).toHaveProperty('headerLeft'); + }); + + it('shows icon button when hasIconButton is true', () => { + const options = getStakingNavbar( + 'Staking', + mockNavigation, + mockThemeColors, + { + hasCancelButton: false, + hasIconButton: true, + handleIconPress: jest.fn(), + }, + ); + + expect(options).toHaveProperty('headerRight'); + }); + }); + + describe('getDeFiProtocolPositionDetailsNavbarOptions', () => { + it('returns correct options', () => { + const options = + getDeFiProtocolPositionDetailsNavbarOptions(mockNavigation); + + expect(options).toHaveProperty('headerTitle'); + expect(options).toHaveProperty('headerLeft'); + }); + }); + + describe('getRampsOrderDetailsNavbarOptions', () => { + it('returns correct options', () => { + const options = getRampsOrderDetailsNavbarOptions( + mockNavigation, + { title: 'Order Details' }, + { colors: mockThemeColors }, + ); + + expect(options).toBeDefined(); + }); + + it('shows back button when showBack is true', () => { + const options = getRampsOrderDetailsNavbarOptions( + mockNavigation, + { title: 'Order Details', showBack: true }, + { colors: mockThemeColors }, + jest.fn(), + ); + + expect(options).toBeDefined(); + }); + }); + + describe('getPaymentSelectorMethodNavbar', () => { + it('returns correct options', () => { + const onPop = jest.fn(); + const options = getPaymentSelectorMethodNavbar( + mockNavigation, + onPop, + mockThemeColors, + ); + + expect(options).toHaveProperty('headerTitle'); + expect(options).toHaveProperty('headerLeft'); + expect(options).toHaveProperty('headerRight'); + expect(options).toHaveProperty('headerStyle'); + }); + + it('calls navigation.getParent().pop() and onPop when headerRight is pressed', () => { + const onPop = jest.fn(); + const mockParentPop = jest.fn(); + const navigationWithParent = { + ...mockNavigation, + getParent: jest.fn(() => ({ + pop: mockParentPop, + })), + }; + const options = getPaymentSelectorMethodNavbar( + navigationWithParent, + onPop, + mockThemeColors, + ); + + const HeaderRight = options.headerRight; + const { getByText } = render(); + fireEvent.press(getByText('Cancel')); + + expect(navigationWithParent.getParent).toHaveBeenCalled(); + expect(mockParentPop).toHaveBeenCalled(); + expect(onPop).toHaveBeenCalled(); + }); + }); + + describe('getPaymentMethodApplePayNavbar', () => { + it('returns correct options', () => { + const onPop = jest.fn(); + const onExit = jest.fn(); + const options = getPaymentMethodApplePayNavbar( + mockNavigation, + onPop, + onExit, + mockThemeColors, + ); + + expect(options).toHaveProperty('title'); + expect(options).toHaveProperty('headerLeft'); + expect(options).toHaveProperty('headerRight'); + expect(options).toHaveProperty('headerStyle'); + }); + + it('calls navigation.getParent().pop() and onExit when headerRight is pressed', () => { + const onPop = jest.fn(); + const onExit = jest.fn(); + const mockParentPop = jest.fn(); + const navigationWithParent = { + ...mockNavigation, + getParent: jest.fn(() => ({ + pop: mockParentPop, + })), + }; + const options = getPaymentMethodApplePayNavbar( + navigationWithParent, + onPop, + onExit, + mockThemeColors, + ); + + const HeaderRight = options.headerRight; + const { getByText } = render(); + fireEvent.press(getByText('Cancel')); + + expect(navigationWithParent.getParent).toHaveBeenCalled(); + expect(mockParentPop).toHaveBeenCalled(); + expect(onExit).toHaveBeenCalled(); + }); + }); + + describe('getSwapsAmountNavbar', () => { + it('returns correct options', () => { + const options = getSwapsAmountNavbar( + mockNavigation, + mockRoute, + mockThemeColors, + ); + + expect(options).toHaveProperty('headerTitle'); + expect(options).toHaveProperty('headerLeft'); + expect(options).toHaveProperty('headerRight'); + expect(options).toHaveProperty('headerStyle'); + }); + + it('calls navigation.getParent().pop() when headerRight is pressed', () => { + const mockParentPop = jest.fn(); + const navigationWithParent = { + ...mockNavigation, + getParent: jest.fn(() => ({ + pop: mockParentPop, + })), + }; + const options = getSwapsAmountNavbar( + navigationWithParent, + mockRoute, + mockThemeColors, + ); + + const HeaderRight = options.headerRight; + const { getByText } = render(); + fireEvent.press(getByText('Cancel')); + + expect(navigationWithParent.getParent).toHaveBeenCalled(); + expect(mockParentPop).toHaveBeenCalled(); + }); + }); + + describe('getSwapsQuotesNavbar', () => { + const mockSwapsRoute = { + ...mockRoute, + params: { + requestedTrade: { + token_from: 'ETH', + token_to: 'DAI', + request_type: 'Order', + custom_slippage: false, + chain_id: '0x1', + token_from_amount: '1', + }, + selectedQuote: { id: 'quote-1' }, + quoteBegin: Date.now(), + }, + }; + + it('returns correct options', () => { + const options = getSwapsQuotesNavbar( + mockNavigation, + mockSwapsRoute, + mockThemeColors, + ); + + expect(options).toHaveProperty('headerTitle'); + expect(options).toHaveProperty('headerLeft'); + expect(options).toHaveProperty('headerRight'); + expect(options).toHaveProperty('headerStyle'); + }); + + it('calls navigation.getParent().pop() when headerRight is pressed', () => { + const mockParentPop = jest.fn(); + const navigationWithParent = { + ...mockNavigation, + getParent: jest.fn(() => ({ + pop: mockParentPop, + })), + }; + const options = getSwapsQuotesNavbar( + navigationWithParent, + mockSwapsRoute, + mockThemeColors, + ); + + const HeaderRight = options.headerRight; + const { getByText } = render(); + fireEvent.press(getByText('Cancel')); + + expect(navigationWithParent.getParent).toHaveBeenCalled(); + expect(mockParentPop).toHaveBeenCalled(); + }); + }); + + describe('getBridgeNavbar with getParent', () => { + it('calls navigation.getParent().pop() when close button is pressed', () => { + const mockParentPop = jest.fn(); + const navigationWithParent = { + ...mockNavigation, + getParent: jest.fn(() => ({ + pop: mockParentPop, + })), + }; + const options = getBridgeNavbar( + navigationWithParent, + BridgeViewMode.Bridge, + mockThemeColors, + ); + + expect(options).toBeDefined(); + const Header = options.header; + const { getByTestId } = render(
); + fireEvent.press(getByTestId('button-icon')); + expect(navigationWithParent.getParent).toHaveBeenCalled(); + expect(mockParentPop).toHaveBeenCalled(); + }); + }); + + describe('getEditableOptions back button behavior', () => { + it('calls navigation.pop when back button is pressed in edit mode', () => { + const routeWithEditMode = { + ...mockRoute, + params: { editMode: 'edit', dispatch: jest.fn() }, + }; + const options = getEditableOptions( + 'Contact', + mockNavigation, + routeWithEditMode, + mockThemeColors, + ); + + const HeaderLeft = options.headerLeft; + const { getByTestId } = render(); + fireEvent.press(getByTestId('edit-contact-back-button')); + + expect(mockNavigation.pop).toHaveBeenCalled(); + }); + }); + + describe('getNavigationOptionsTitle close button behavior', () => { + it('renders and handles close button in fullscreen modal mode', () => { + const options = getNavigationOptionsTitle( + 'Settings', + mockNavigation, + true, + mockThemeColors, + ); + + const HeaderRight = options.headerRight; + if (HeaderRight) { + const { getByTestId } = render(); + fireEvent.press(getByTestId('close-network-icon')); + expect(mockNavigation.goBack).toHaveBeenCalled(); + } + }); + }); + + describe('getStakingNavbar back button behavior', () => { + it('calls navigation.goBack when back button is pressed', () => { + const options = getStakingNavbar( + 'Staking', + mockNavigation, + mockThemeColors, + { hasBackButton: true }, + ); + + const HeaderLeft = options.headerLeft; + if (HeaderLeft) { + const rendered = render( + typeof HeaderLeft === 'function' ? : HeaderLeft, + ); + fireEvent.press(rendered.getByTestId('button-icon')); + expect(mockNavigation.goBack).toHaveBeenCalled(); + expect(rendered).toBeTruthy(); + } + }); + }); + + describe('getDepositNavbarOptions close button behavior', () => { + it('invokes onClose after pop when close button is pressed', () => { + const onClose = jest.fn(); + const options = getDepositNavbarOptions( + mockNavigation, + { title: 'Deposit', showClose: true }, + { colors: mockThemeColors }, + onClose, + ); + + const rendered = render(options.header()); + expect(rendered).toBeTruthy(); + const { getByTestId } = rendered; + fireEvent.press(getByTestId('deposit-back-navbar-button')); + expect(mockNavigation.pop).toHaveBeenCalled(); + expect(onClose).toHaveBeenCalled(); + }); + }); + + describe('getRampsOrderDetailsNavbarOptions close behavior', () => { + it('shows close button when showCloseButton is true', () => { + const options = getRampsOrderDetailsNavbarOptions( + mockNavigation, + { title: 'Order Details', showCloseButton: true }, + { colors: mockThemeColors }, + ); + + expect(options).toBeDefined(); + }); + }); +}); diff --git a/app/components/UI/Navbar/index.test.jsx b/app/components/UI/Navbar/index.test.jsx index 20e298a3898..e843edd43cd 100644 --- a/app/components/UI/Navbar/index.test.jsx +++ b/app/components/UI/Navbar/index.test.jsx @@ -280,7 +280,7 @@ describe('getOnboardingNavbarOptions', () => { describe('getBridgeNavbar', () => { const mockNavigation = { - dangerouslyGetParent: jest.fn(() => ({ + getParent: jest.fn(() => ({ pop: jest.fn(), })), }; @@ -304,7 +304,7 @@ describe('getBridgeNavbar', () => { const options = getBridgeNavbar( mockNavigation, BridgeViewMode.Swap, - mockThemeColors, + mockTheme.colors, ); expect(options.header).toBeDefined(); @@ -474,7 +474,7 @@ describe('getNavigationOptionsTitle', () => { describe('getSwapsQuotesNavbar', () => { const mockNavigation = { pop: jest.fn(), - dangerouslyGetParent: jest.fn(() => ({ + getParent: jest.fn(() => ({ pop: jest.fn(), })), }; @@ -516,4 +516,711 @@ describe('getSwapsQuotesNavbar', () => { expect(mockAnalyticsTrackEvent).toHaveBeenCalled(); expect(mockNavigation.pop).toHaveBeenCalled(); }); + + it('does not track event when quote is selected', () => { + const route = { + params: { + title: 'Swap', + requestedTrade: { + token_from: 'ETH', + token_to: 'DAI', + request_type: 'Order', + custom_slippage: false, + chain_id: '0x1', + token_from_amount: '1', + }, + selectedQuote: { id: 'quote-1' }, + quoteBegin: Date.now() - 1000, + }, + }; + + const options = getSwapsQuotesNavbar( + mockNavigation, + route, + mockTheme.colors, + ); + + Device.isAndroid.mockReturnValue(false); + const headerLeft = options.headerLeft(); + headerLeft.props.onPress(); + + expect(AnalyticsEventBuilder.createEventBuilder).not.toHaveBeenCalled(); + expect(mockNavigation.pop).toHaveBeenCalled(); + }); + + it('renders Android back button correctly', () => { + const route = { + params: { + title: 'Swap', + }, + }; + + Device.isAndroid.mockReturnValue(true); + const options = getSwapsQuotesNavbar( + mockNavigation, + route, + mockTheme.colors, + ); + + const headerLeft = options.headerLeft(); + expect(headerLeft.type).toBe(require('react-native').TouchableOpacity); + }); +}); + +describe('getTransactionsNavbarOptions', () => { + const { getTransactionsNavbarOptions } = require('.'); + + const mockHandleRightButtonPress = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns navbar options with title and right button', () => { + const options = getTransactionsNavbarOptions( + 'Transactions', + mockTheme.colors, + undefined, + '0x123', + mockHandleRightButtonPress, + ); + + expect(options.headerTitle).toBeDefined(); + expect(options.headerRight).toBeDefined(); + expect(options.headerLeft).toBeNull(); + }); +}); + +describe('getApproveNavbar', () => { + const { getApproveNavbar } = require('.'); + + it('returns navbar options with title and empty left/right components', () => { + const options = getApproveNavbar('Approve'); + + expect(options.headerTitle).toBeDefined(); + expect(options.headerLeft).toBeDefined(); + expect(options.headerRight).toBeDefined(); + + const HeaderLeft = options.headerLeft(); + const HeaderRight = options.headerRight(); + expect(HeaderLeft.type).toBe(View); + expect(HeaderRight.type).toBe(View); + }); +}); + +describe('getModalNavbarOptions', () => { + const { getModalNavbarOptions } = require('.'); + + it('returns navbar options with modal title', () => { + const options = getModalNavbarOptions('Modal Title'); + + expect(options.headerTitle).toBeDefined(); + }); +}); + +describe('getOfflineModalNavbar', () => { + const { getOfflineModalNavbar } = require('.'); + + it('returns navbar options with header hidden', () => { + const options = getOfflineModalNavbar(); + + expect(options.headerShown).toBe(false); + }); +}); + +describe('getOptinMetricsNavbarOptions', () => { + const { getOptinMetricsNavbarOptions } = require('.'); + + it('returns navbar options with logo when showLogo is true', () => { + const options = getOptinMetricsNavbarOptions(mockTheme.colors, true); + + expect(options.headerTitle).toBeDefined(); + expect(options.headerLeft).toBeDefined(); + expect(options.headerRight).toBeDefined(); + }); + + it('returns navbar options without logo when showLogo is false', () => { + const options = getOptinMetricsNavbarOptions(mockTheme.colors, false); + + const HeaderTitle = options.headerTitle(); + expect(HeaderTitle).toBeNull(); + }); +}); + +describe('getTransparentBackOnboardingNavbarOptions', () => { + const { getTransparentBackOnboardingNavbarOptions } = require('.'); + + it('returns navbar options with back button', () => { + const options = getTransparentBackOnboardingNavbarOptions(mockTheme.colors); + + expect(options.headerTitle).toBeDefined(); + expect(options.headerBackTitle).toBe(strings('navigation.back')); + expect(options.headerRight).toBeDefined(); + }); +}); + +describe('getEditAccountNameNavBarOptions', () => { + const { getEditAccountNameNavBarOptions } = require('.'); + + const mockGoBack = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns navbar options with close button', () => { + const options = getEditAccountNameNavBarOptions( + mockGoBack, + mockTheme.colors, + ); + + expect(options.headerTitle).toBeDefined(); + expect(options.headerLeft).toBeNull(); + expect(options.headerRight).toBeDefined(); + }); + + it('calls goBack when close button is pressed', () => { + const options = getEditAccountNameNavBarOptions( + mockGoBack, + mockTheme.colors, + ); + + const HeaderRight = options.headerRight(); + HeaderRight.props.onPress(); + + expect(mockGoBack).toHaveBeenCalledTimes(1); + }); +}); + +describe('getDeFiProtocolPositionDetailsNavbarOptions', () => { + const { getDeFiProtocolPositionDetailsNavbarOptions } = require('.'); + + const mockNavigation = { + pop: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns navbar options with back button', () => { + const options = getDeFiProtocolPositionDetailsNavbarOptions(mockNavigation); + + expect(options.headerTitle).toBeDefined(); + expect(options.headerLeft).toBeDefined(); + + const HeaderTitle = options.headerTitle(); + expect(HeaderTitle).toBeNull(); + }); + + it('calls navigation.pop when back button is pressed', () => { + const options = getDeFiProtocolPositionDetailsNavbarOptions(mockNavigation); + + const HeaderLeft = options.headerLeft(); + HeaderLeft.props.onPress(); + + expect(mockNavigation.pop).toHaveBeenCalledTimes(1); + }); +}); + +describe('getRampsOrderDetailsNavbarOptions', () => { + const { getRampsOrderDetailsNavbarOptions } = require('.'); + + const mockNavigation = { + pop: jest.fn(), + }; + + const mockOnClose = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns navbar options with back button when showBack is true', () => { + const options = getRampsOrderDetailsNavbarOptions( + mockNavigation, + { title: 'Order Details', showBack: true }, + mockTheme, + mockOnClose, + ); + + expect(options).toBeDefined(); + expect(options.header).toBeInstanceOf(Function); + }); + + it('pops navigation and calls onClose when back is pressed', () => { + const options = getRampsOrderDetailsNavbarOptions( + mockNavigation, + { title: 'Order Details', showBack: true }, + mockTheme, + mockOnClose, + ); + + const HeaderComponent = options.header(); + HeaderComponent.props.startButtonIconProps.onPress(); + + expect(mockNavigation.pop).toHaveBeenCalledTimes(1); + expect(mockOnClose).toHaveBeenCalledTimes(1); + }); +}); + +describe('getBridgeTransactionDetailsNavbar', () => { + const { getBridgeTransactionDetailsNavbar } = require('.'); + + const mockNavigation = { + pop: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns navbar options with back button', () => { + const options = getBridgeTransactionDetailsNavbar(mockNavigation); + + expect(options.headerTitle).toBeDefined(); + expect(options.headerLeft).toBeDefined(); + }); + + it('calls navigation.pop when back button is pressed', () => { + const options = getBridgeTransactionDetailsNavbar(mockNavigation); + + const HeaderLeft = options.headerLeft(); + HeaderLeft.props.onPress(); + + expect(mockNavigation.pop).toHaveBeenCalledTimes(1); + }); +}); + +describe('getEditableOptions', () => { + const { getEditableOptions } = require('.'); + + const mockNavigation = { + pop: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns navbar options with title and back button', () => { + const route = { params: { editMode: 'edit', dispatch: jest.fn() } }; + const options = getEditableOptions( + 'Edit Contact', + mockNavigation, + route, + mockTheme.colors, + ); + + expect(options.title).toBe('Edit Contact'); + expect(options.headerLeft).toBeDefined(); + expect(options.headerRight).toBeDefined(); + }); + + it('shows edit button when in view mode', () => { + const mockDispatch = jest.fn(); + const route = { params: { editMode: 'view', dispatch: mockDispatch } }; + const options = getEditableOptions( + 'View Contact', + mockNavigation, + route, + mockTheme.colors, + ); + + const HeaderRight = options.headerRight(); + expect(HeaderRight).toBeDefined(); + }); + + it('shows empty view when in add mode', () => { + const route = { params: { mode: 'add', dispatch: jest.fn() } }; + const options = getEditableOptions( + 'Add Contact', + mockNavigation, + route, + mockTheme.colors, + ); + + const HeaderRight = options.headerRight(); + expect(HeaderRight.type).toBe(View); + }); + + it('calls navigation.pop on back button press', () => { + const route = { params: { editMode: 'edit' } }; + const options = getEditableOptions( + 'Edit Contact', + mockNavigation, + route, + mockTheme.colors, + ); + + const HeaderLeft = options.headerLeft(); + HeaderLeft.props.onPress(); + + expect(mockNavigation.pop).toHaveBeenCalledTimes(1); + }); +}); + +describe('getClosableNavigationOptions', () => { + const { getClosableNavigationOptions } = require('.'); + + const mockNavigation = { + pop: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + Device.isIos.mockReset(); + }); + + it('returns navbar options with title', () => { + Device.isIos.mockReturnValue(true); + const options = getClosableNavigationOptions( + 'Test Title', + 'Back', + mockNavigation, + mockTheme.colors, + ); + + expect(options.title).toBe('Test Title'); + expect(options.headerLeft).toBeDefined(); + }); + + it('shows text button on iOS', () => { + Device.isIos.mockReturnValue(true); + const options = getClosableNavigationOptions( + 'Test Title', + 'Back', + mockNavigation, + mockTheme.colors, + ); + + const HeaderLeft = options.headerLeft(); + HeaderLeft.props.onPress(); + + expect(mockNavigation.pop).toHaveBeenCalledTimes(1); + }); + + it('shows icon button on Android', () => { + Device.isIos.mockReturnValue(false); + Device.isAndroid.mockReturnValue(true); + const options = getClosableNavigationOptions( + 'Test Title', + 'Back', + mockNavigation, + mockTheme.colors, + ); + + const HeaderLeft = options.headerLeft(); + HeaderLeft.props.onPress(); + + expect(mockNavigation.pop).toHaveBeenCalledTimes(1); + }); +}); + +describe('getImportTokenNavbarOptions', () => { + const { getImportTokenNavbarOptions } = require('.'); + + const mockNavigation = { + goBack: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns navbar options with header function', () => { + const options = getImportTokenNavbarOptions(mockNavigation, 'Import Token'); + + expect(options.header).toBeDefined(); + expect(typeof options.header).toBe('function'); + }); + + it('calls custom onPress when provided', () => { + const customOnPress = jest.fn(); + const options = getImportTokenNavbarOptions( + mockNavigation, + 'Import Token', + customOnPress, + ); + + expect(options.header).toBeDefined(); + }); +}); + +describe('getNftDetailsNavbarOptions', () => { + const { getNftDetailsNavbarOptions } = require('.'); + + const mockNavigation = { + pop: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns navbar options with back button', () => { + const options = getNftDetailsNavbarOptions( + mockNavigation, + mockTheme.colors, + ); + + expect(options.headerLeft).toBeDefined(); + expect(options.headerStyle).toBeDefined(); + }); + + it('calls navigation.pop when back button is pressed', () => { + const options = getNftDetailsNavbarOptions( + mockNavigation, + mockTheme.colors, + ); + + const HeaderLeft = options.headerLeft(); + HeaderLeft.props.onPress(); + + expect(mockNavigation.pop).toHaveBeenCalledTimes(1); + }); + + it('shows more options button when onRightPress is provided', () => { + const mockOnRightPress = jest.fn(); + const options = getNftDetailsNavbarOptions( + mockNavigation, + mockTheme.colors, + mockOnRightPress, + ); + + const HeaderRight = options.headerRight(); + HeaderRight.props.onPress(); + + expect(mockOnRightPress).toHaveBeenCalledTimes(1); + }); + + it('shows empty view when onRightPress is not provided', () => { + const options = getNftDetailsNavbarOptions( + mockNavigation, + mockTheme.colors, + ); + + const HeaderRight = options.headerRight(); + expect(HeaderRight.type).toBe(View); + }); + + it('applies shadow styles based on contentOffset', () => { + const options = getNftDetailsNavbarOptions( + mockNavigation, + mockTheme.colors, + undefined, + 25, + ); + + expect(options.headerStyle).toBeDefined(); + }); +}); + +describe('getNftFullImageNavbarOptions', () => { + const { getNftFullImageNavbarOptions } = require('.'); + + const mockNavigation = { + pop: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns navbar options with close button on right', () => { + const options = getNftFullImageNavbarOptions( + mockNavigation, + mockTheme.colors, + ); + + expect(options.headerRight).toBeDefined(); + expect(options.headerLeft).toBeDefined(); + }); + + it('calls navigation.pop when close button is pressed', () => { + const options = getNftFullImageNavbarOptions( + mockNavigation, + mockTheme.colors, + ); + + const HeaderRight = options.headerRight(); + HeaderRight.props.onPress(); + + expect(mockNavigation.pop).toHaveBeenCalledTimes(1); + }); + + it('shows empty view on left', () => { + const options = getNftFullImageNavbarOptions( + mockNavigation, + mockTheme.colors, + ); + + const HeaderLeft = options.headerLeft(); + expect(HeaderLeft.type).toBe(View); + }); +}); + +describe('getSwapsAmountNavbar', () => { + const { getSwapsAmountNavbar } = require('.'); + + const mockNavigation = { + getParent: jest.fn(() => ({ + pop: jest.fn(), + })), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns navbar options with title and cancel button', () => { + const route = { params: { title: 'Swap' } }; + const options = getSwapsAmountNavbar( + mockNavigation, + route, + mockTheme.colors, + ); + + expect(options.headerTitle).toBeDefined(); + expect(options.headerLeft).toBeDefined(); + expect(options.headerRight).toBeDefined(); + }); + + it('uses default title when not provided', () => { + const route = { params: {} }; + const options = getSwapsAmountNavbar( + mockNavigation, + route, + mockTheme.colors, + ); + + expect(options.headerTitle).toBeDefined(); + }); + + it('shows empty view on left', () => { + const route = { params: {} }; + const options = getSwapsAmountNavbar( + mockNavigation, + route, + mockTheme.colors, + ); + + const HeaderLeft = options.headerLeft(); + expect(HeaderLeft.type).toBe(View); + }); +}); + +describe('getPerpsTransactionsDetailsNavbar', () => { + const { getPerpsTransactionsDetailsNavbar } = require('.'); + + const mockNavigation = { + goBack: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns navbar options with title and back button', () => { + const options = getPerpsTransactionsDetailsNavbar( + mockNavigation, + 'Position Details', + ); + + expect(options.headerTitle).toBeDefined(); + expect(options.headerLeft).toBeDefined(); + expect(options.headerRight).toBeDefined(); + }); + + it('calls navigation.goBack when back button is pressed', () => { + const options = getPerpsTransactionsDetailsNavbar( + mockNavigation, + 'Position Details', + ); + + const HeaderLeft = options.headerLeft(); + HeaderLeft.props.onPress(); + + expect(mockNavigation.goBack).toHaveBeenCalledTimes(1); + }); +}); + +describe('getTransactionOptionsTitle', () => { + const { getTransactionOptionsTitle } = require('.'); + + const mockNavigation = { + pop: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns navbar options with title', () => { + const route = { + name: 'Send', + params: { mode: '', disableModeChange: false, dispatch: jest.fn() }, + }; + const options = getTransactionOptionsTitle( + 'transaction.title', + mockNavigation, + route, + mockTheme.colors, + ); + + expect(options.headerTitle).toBeDefined(); + expect(options.headerLeft).toBeDefined(); + expect(options.headerRight).toBeDefined(); + }); + + it('shows edit button when not in edit mode', () => { + const mockDispatch = jest.fn(); + const route = { + name: 'Confirm', + params: { mode: '', disableModeChange: false, dispatch: mockDispatch }, + }; + const options = getTransactionOptionsTitle( + 'transaction.title', + mockNavigation, + route, + mockTheme.colors, + ); + + const HeaderLeft = options.headerLeft(); + expect(HeaderLeft).toBeDefined(); + }); + + it('shows empty view on left when in edit mode', () => { + const route = { + name: 'Confirm', + params: { mode: 'edit', disableModeChange: false }, + }; + const options = getTransactionOptionsTitle( + 'transaction.title', + mockNavigation, + route, + mockTheme.colors, + ); + + const HeaderLeft = options.headerLeft(); + expect(HeaderLeft.type).toBe(View); + }); + + it('shows cancel button on right for Send screen', () => { + const route = { + name: 'Send', + params: { mode: '' }, + }; + const options = getTransactionOptionsTitle( + 'transaction.title', + mockNavigation, + route, + mockTheme.colors, + ); + + const HeaderRight = options.headerRight(); + HeaderRight.props.onPress(); + + expect(mockNavigation.pop).toHaveBeenCalledTimes(1); + }); }); diff --git a/app/components/UI/NavbarTitle/__snapshots__/index.test.js.snap b/app/components/UI/NavbarTitle/__snapshots__/index.test.js.snap index 8dd25f2d8cc..87dd79ae708 100644 --- a/app/components/UI/NavbarTitle/__snapshots__/index.test.js.snap +++ b/app/components/UI/NavbarTitle/__snapshots__/index.test.js.snap @@ -27,7 +27,7 @@ exports[`NavbarTitle should render correctly 1`] = ` } } > - diff --git a/app/components/UI/NavbarTitle/index.js b/app/components/UI/NavbarTitle/index.js index a07b88185bc..66fde0ac8d4 100644 --- a/app/components/UI/NavbarTitle/index.js +++ b/app/components/UI/NavbarTitle/index.js @@ -7,7 +7,7 @@ import { strings } from '../../../../locales/i18n'; import { ThemeContext, mockTheme } from '../../../util/theme'; import Routes from '../../../constants/navigation/Routes'; import { MetaMetricsEvents } from '../../../core/Analytics'; -import { withNavigation } from '@react-navigation/compat'; +import { useNavigation } from '@react-navigation/native'; import { selectChainId, selectProviderConfig, @@ -178,4 +178,11 @@ const mapStateToProps = (state) => ({ selectedNetworkName: selectNetworkName(state), }); -export default withNavigation(connect(mapStateToProps)(NavbarTitle)); +const ConnectedNavbarTitle = connect(mapStateToProps)(NavbarTitle); + +const NavbarTitleWrapper = (props) => { + const navigation = useNavigation(); + return ; +}; + +export default NavbarTitleWrapper; diff --git a/app/components/UI/NavbarTitle/index.test.js b/app/components/UI/NavbarTitle/index.test.js index 87fc5937295..4ec9935a84b 100644 --- a/app/components/UI/NavbarTitle/index.test.js +++ b/app/components/UI/NavbarTitle/index.test.js @@ -56,14 +56,12 @@ jest.mock('../../../core/Analytics', () => ({ })); const mockNavigate = jest.fn(); -jest.mock('@react-navigation/compat', () => ({ - withNavigation: (Component) => { - const WithNav = (props) => ( - - ); - WithNav.displayName = `withNavigation(${Component.displayName || Component.name || 'Component'})`; - return WithNav; - }, + +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: () => ({ + navigate: mockNavigate, + }), })); describe('NavbarTitle', () => { diff --git a/app/components/UI/OTAUpdatesModal/OTAUpdatesModal.test.tsx b/app/components/UI/OTAUpdatesModal/OTAUpdatesModal.test.tsx index 077f9133b7f..ad81f6d64f8 100644 --- a/app/components/UI/OTAUpdatesModal/OTAUpdatesModal.test.tsx +++ b/app/components/UI/OTAUpdatesModal/OTAUpdatesModal.test.tsx @@ -5,6 +5,9 @@ import { reloadAsync } from 'expo-updates'; import Logger from '../../../util/Logger'; import { MetaMetricsEvents } from '../../../core/Analytics'; import renderWithProvider from '../../../util/test/renderWithProvider'; +import { useAnalytics } from '../../hooks/useAnalytics/useAnalytics'; +import { createMockUseAnalyticsHook } from '../../../util/test/analyticsMock'; +import { AnalyticsEventBuilder } from '../../../util/analytics/AnalyticsEventBuilder'; // Mock theme utility jest.mock('../../../util/theme', () => ({ @@ -107,31 +110,9 @@ const mockLoggerError = Logger.error as jest.MockedFunction< typeof Logger.error >; -interface MockEventBuilder { - addProperties: jest.Mock; - build: jest.Mock; -} - -const mockCreateEventBuilder = jest.fn((event: string): MockEventBuilder => { - const builder: MockEventBuilder = { - addProperties: jest.fn(), - build: jest.fn(), - }; - - builder.addProperties.mockReturnValue(builder); - builder.build.mockReturnValue({ event }); - - return builder; -}); - const mockTrackEvent = jest.fn(); -jest.mock('../../hooks/useMetrics', () => ({ - useMetrics: () => ({ - trackEvent: mockTrackEvent, - createEventBuilder: mockCreateEventBuilder, - }), -})); +jest.mock('../../hooks/useAnalytics/useAnalytics'); // Import component AFTER all mocks are defined import OTAUpdatesModal from './OTAUpdatesModal'; @@ -139,6 +120,12 @@ import OTAUpdatesModal from './OTAUpdatesModal'; describe('OTAUpdatesModal', () => { beforeEach(() => { jest.clearAllMocks(); + jest.mocked(useAnalytics).mockReturnValue( + createMockUseAnalyticsHook({ + trackEvent: mockTrackEvent, + createEventBuilder: AnalyticsEventBuilder.createEventBuilder, + }), + ); (Platform as unknown as { OS: string }).OS = 'ios'; mockOnCloseBottomSheet.mockImplementation((callback?: () => void) => { if (callback) callback(); @@ -150,7 +137,7 @@ describe('OTAUpdatesModal', () => { expect(mockTrackEvent).toHaveBeenCalledWith( expect.objectContaining({ - event: MetaMetricsEvents.OTA_UPDATES_MODAL_VIEWED, + name: MetaMetricsEvents.OTA_UPDATES_MODAL_VIEWED.category, }), ); }); @@ -163,7 +150,8 @@ describe('OTAUpdatesModal', () => { await waitFor(() => { expect(mockTrackEvent).toHaveBeenCalledWith( expect.objectContaining({ - event: MetaMetricsEvents.OTA_UPDATES_MODAL_PRIMARY_ACTION_CLICKED, + name: MetaMetricsEvents.OTA_UPDATES_MODAL_PRIMARY_ACTION_CLICKED + .category, }), ); }); diff --git a/app/components/UI/OTAUpdatesModal/OTAUpdatesModal.tsx b/app/components/UI/OTAUpdatesModal/OTAUpdatesModal.tsx index fc872f5ab82..1c433f0187d 100644 --- a/app/components/UI/OTAUpdatesModal/OTAUpdatesModal.tsx +++ b/app/components/UI/OTAUpdatesModal/OTAUpdatesModal.tsx @@ -17,7 +17,7 @@ import Logger from '../../../util/Logger'; import { useAssetFromTheme } from '../../../util/theme'; import { MetaMetricsEvents } from '../../../core/Analytics'; import generateDeviceAnalyticsMetaData from '../../../util/metrics'; -import { useMetrics } from '../../hooks/useMetrics'; +import { useAnalytics } from '../../hooks/useAnalytics/useAnalytics'; import BottomSheet, { BottomSheetRef, } from '../../../component-library/components/BottomSheets/BottomSheet'; @@ -35,7 +35,7 @@ export const createOTAUpdatesModalNavDetails = createNavigationDetails( const OTAUpdatesModal = () => { const tw = useTailwind(); - const { trackEvent, createEventBuilder } = useMetrics(); + const { trackEvent, createEventBuilder } = useAnalytics(); const bottomSheetRef = useRef(null); const metamaskName = useAssetFromTheme( metamaskNameLightMode, diff --git a/app/components/UI/OptinMetrics/__snapshots__/index.test.tsx.snap b/app/components/UI/OptinMetrics/__snapshots__/index.test.tsx.snap index 187720aa95a..23d293c6837 100644 --- a/app/components/UI/OptinMetrics/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/OptinMetrics/__snapshots__/index.test.tsx.snap @@ -19,132 +19,126 @@ exports[`OptinMetrics Snapshots android render matches snapshot 1`] = ` ] } > - - - - + @@ -153,707 +147,758 @@ exports[`OptinMetrics Snapshots android render matches snapshot 1`] = ` pointerEvents="none" style={ { - "backgroundColor": "rgb(242, 242, 242)", - "bottom": 0, - "left": 0, - "position": "absolute", - "shadowColor": "#000", - "shadowOffset": { - "height": 1, - "width": -1, - }, - "shadowOpacity": 0.3, - "shadowRadius": 5, - "top": 0, - "width": 3, + "backgroundColor": "#000", + "flex": 1, + "opacity": 0.07, } } /> + + - - - - + } + testID="meta-metrics-container" + > + - - - - Improve MetaMask - - - Weโ€™d like to request these permissions. You can opt out or delete your usage data at any time. - - - + + + + Improve MetaMask + + + Weโ€™d like to request these permissions. You can opt out or delete your usage data at any time. + + - - Gather basic usage data - - - - + Gather basic usage data + + + - - - - - - We'll collect basic product usage data like general location, clicks, and views. No other information will be stored. + > + + + + - Learn more + We'll collect basic product usage data like general location, clicks, and views. No other information will be stored. + + Learn more + - - - + - - - Marketing updates - - - - + Marketing updates + + + - - - - We'll use this data to learn how you interact with our marketing communications. We may share relevant news (like product features). - - - - - - - - + + + + + We'll use this data to learn how you interact with our marketing communications. We may share relevant news (like product features). + + + + + + + - - Continue - + + Continue + + - - - - + + - - - + + /> + + - + - OptinMetrics - + + OptinMetrics + + + @@ -863,9 +908,23 @@ exports[`OptinMetrics Snapshots android render matches snapshot 1`] = ` - - - + + + + `; @@ -889,132 +948,126 @@ exports[`OptinMetrics Snapshots android render matches snapshot with status bar ] } > - - - - + @@ -1023,707 +1076,758 @@ exports[`OptinMetrics Snapshots android render matches snapshot with status bar pointerEvents="none" style={ { - "backgroundColor": "rgb(242, 242, 242)", - "bottom": 0, - "left": 0, - "position": "absolute", - "shadowColor": "#000", - "shadowOffset": { - "height": 1, - "width": -1, - }, - "shadowOpacity": 0.3, - "shadowRadius": 5, - "top": 0, - "width": 3, + "backgroundColor": "#000", + "flex": 1, + "opacity": 0.07, } } /> + + - - - - + } + testID="meta-metrics-container" + > + - + + + - - - Improve MetaMask - - - Weโ€™d like to request these permissions. You can opt out or delete your usage data at any time. - - - + Improve MetaMask + + + Weโ€™d like to request these permissions. You can opt out or delete your usage data at any time. + + - - Gather basic usage data - - - - + Gather basic usage data + + + - - - - - - We'll collect basic product usage data like general location, clicks, and views. No other information will be stored. + > + + + + - Learn more + We'll collect basic product usage data like general location, clicks, and views. No other information will be stored. + + Learn more + - - - + - - - Marketing updates - - - - + Marketing updates + + + - + > + + + + + We'll use this data to learn how you interact with our marketing communications. We may share relevant news (like product features). + - - We'll use this data to learn how you interact with our marketing communications. We may share relevant news (like product features). - - - - + - - Continue - + + Continue + + - - - - + + - - - + + /> + + - + - OptinMetrics - + + OptinMetrics + + + @@ -1733,9 +1837,23 @@ exports[`OptinMetrics Snapshots android render matches snapshot with status bar - - - + + + + `; @@ -1760,858 +1878,881 @@ exports[`OptinMetrics Snapshots iOS renders correctly 1`] = ` } > - - - + + /> + + - + - OptinMetrics - + + OptinMetrics + + + - - - - - + - - - - - + } + testID="meta-metrics-container" + > + - - - - Improve MetaMask - - - Weโ€™d like to request these permissions. You can opt out or delete your usage data at any time. - - - + + + + Improve MetaMask + + + Weโ€™d like to request these permissions. You can opt out or delete your usage data at any time. + + - - Gather basic usage data - - - - + Gather basic usage data + + + - - - - - - We'll collect basic product usage data like general location, clicks, and views. No other information will be stored. + > + + + + - Learn more + We'll collect basic product usage data like general location, clicks, and views. No other information will be stored. + + Learn more + - - - + - - - Marketing updates - - - - + Marketing updates + + + - + > + + + + + We'll use this data to learn how you interact with our marketing communications. We may share relevant news (like product features). + - - We'll use this data to learn how you interact with our marketing communications. We may share relevant news (like product features). - - - - + - - Continue - + + Continue + + - - + + - - - + + + `; diff --git a/app/components/UI/OptinMetrics/index.test.tsx b/app/components/UI/OptinMetrics/index.test.tsx index a8c5373eadc..a07027f7289 100644 --- a/app/components/UI/OptinMetrics/index.test.tsx +++ b/app/components/UI/OptinMetrics/index.test.tsx @@ -7,6 +7,9 @@ import { Platform } from 'react-native'; import Device from '../../../util/device'; import { MetaMetricsEvents } from '../../../core/Analytics'; import { AccountType } from '../../../constants/onboarding'; +import { createMockUseAnalyticsHook } from '../../../util/test/analyticsMock'; +import { useAnalytics } from '../../hooks/useAnalytics/useAnalytics'; +import { AnalyticsEventBuilder } from '../../../util/analytics/AnalyticsEventBuilder'; const { InteractionManager } = jest.requireActual('react-native'); @@ -28,6 +31,8 @@ jest.mock('../../../util/analytics/analytics', () => ({ }, })); +jest.mock('../../hooks/useAnalytics/useAnalytics'); + // Mock MetaMetrics for events and getInstance jest.mock('../../../core/Analytics/MetaMetrics', () => ({ MetaMetricsEvents: jest.requireActual('../../../core/Analytics/MetaMetrics') @@ -86,6 +91,24 @@ jest.doMock('react-native', () => { describe('OptinMetrics', () => { beforeEach(() => { jest.clearAllMocks(); + jest.mocked(useAnalytics).mockReturnValue( + createMockUseAnalyticsHook({ + trackEvent: (event) => mockAnalytics.trackEvent(event), + createEventBuilder: AnalyticsEventBuilder.createEventBuilder, + enable: async (enable) => { + if (enable === false) { + await mockAnalytics.optOut(); + } else { + await mockAnalytics.optIn(); + } + }, + identify: async (traits) => { + mockAnalytics.identify(traits); + }, + isEnabled: () => mockAnalytics.isEnabled(), + getAnalyticsId: () => mockAnalytics.getAnalyticsId(), + }), + ); (Device.isMediumDevice as jest.Mock).mockReturnValue(false); (Device.isAndroid as jest.Mock).mockReturnValue(false); (Device.isIos as jest.Mock).mockReturnValue(true); diff --git a/app/components/UI/OptinMetrics/index.tsx b/app/components/UI/OptinMetrics/index.tsx index 7485dff5b08..79de0bd4d2e 100644 --- a/app/components/UI/OptinMetrics/index.tsx +++ b/app/components/UI/OptinMetrics/index.tsx @@ -31,7 +31,9 @@ import { useDispatch, useSelector } from 'react-redux'; import { clearOnboardingEvents } from '../../../actions/onboarding'; import { selectOnboardingAccountType } from '../../../selectors/onboarding'; import { setDataCollectionForMarketing } from '../../../actions/security'; -import { MetaMetricsEvents, useMetrics } from '../../hooks/useMetrics'; +import { MetaMetricsEvents } from '../../../core/Analytics'; +import { AnalyticsEventBuilder } from '../../../util/analytics/AnalyticsEventBuilder'; +import { useAnalytics } from '../../hooks/useAnalytics/useAnalytics'; import { markMetricsOptInUISeen } from '../../../util/metrics/metricsOptInUIUtils'; import { MetaMetricsOptInSelectorsIDs } from './MetaMetricsOptIn.testIds'; import Checkbox from '../../../component-library/components/Checkbox'; @@ -73,7 +75,7 @@ const OptinMetrics = () => { > >(); const tw = useTailwind(); - const metrics = useMetrics(); + const metrics = useAnalytics(); // Redux state selectors const events = useSelector((state: RootState) => state.onboarding.events); @@ -198,7 +200,7 @@ const OptinMetrics = () => { .build(), ); - await metrics.addTraitsToUser({ + await metrics.identify({ ...generateDeviceAnalyticsMetaData(), ...generateUserSettingsAnalyticsMetaData(), [UserProfileProperty.CHAIN_IDS]: getConfiguredCaipChainIds(), @@ -217,7 +219,10 @@ const OptinMetrics = () => { // as precision is only to the milisecond // and loop seems to runs faster than that setTimeout(() => { - metrics.trackEvent(...eventArgs); + const event = AnalyticsEventBuilder.createEventBuilder( + eventArgs[0], + ).build(); + metrics.trackEvent(event); }, delay); delay += eventTrackingDelay; }); diff --git a/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.test.tsx b/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.test.tsx index 37baa25390a..e72a2cec84c 100644 --- a/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.test.tsx +++ b/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.test.tsx @@ -83,12 +83,6 @@ jest.mock('@react-navigation/native', () => { }; }); -// Mock @react-navigation/compat to prevent issues with createNavigatorFactory -jest.mock('@react-navigation/compat', () => ({ - withNavigation: jest.fn((component) => component), - withNavigationFocus: jest.fn((component) => component), -})); - // Mock i18n strings jest.mock('../../../../../../locales/i18n', () => ({ strings: jest.fn((key: string, params?: Record) => { diff --git a/app/components/UI/Perps/routes/index.tsx b/app/components/UI/Perps/routes/index.tsx index 1cfc3c83370..2257064f3d1 100644 --- a/app/components/UI/Perps/routes/index.tsx +++ b/app/components/UI/Perps/routes/index.tsx @@ -44,6 +44,9 @@ import { CONFIRMATION_HEADER_CONFIG } from '../constants/perpsConfig'; const Stack = createStackNavigator(); const ModalStack = createStackNavigator(); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ScreenComponent = React.ComponentType; + const styles = StyleSheet.create({ container: { flex: 1, @@ -96,9 +99,9 @@ const PerpsModalStack = () => { { { getRedesignedConfirmationsHeaderOptions(route.params) } diff --git a/app/components/UI/Predict/components/PredictAddFundsSheet/PredictAddFundsSheet.test.tsx b/app/components/UI/Predict/components/PredictAddFundsSheet/PredictAddFundsSheet.test.tsx index 67b261786ac..f0349d940d4 100644 --- a/app/components/UI/Predict/components/PredictAddFundsSheet/PredictAddFundsSheet.test.tsx +++ b/app/components/UI/Predict/components/PredictAddFundsSheet/PredictAddFundsSheet.test.tsx @@ -15,11 +15,6 @@ import { usePredictActionGuard } from '../../hooks/usePredictActionGuard'; jest.mock('../../hooks/usePredictDeposit'); jest.mock('../../hooks/usePredictActionGuard'); -jest.mock('@react-navigation/compat', () => ({ - withNavigation: (component: T): T => component, - withNavigationFocus: (component: T): T => component, -})); - jest.mock('../../../../../../locales/i18n', () => ({ strings: jest.fn((key: string) => { const translations: Record = { diff --git a/app/components/UI/Predict/components/PredictMarketMultiple/PredictMarketMultiple.test.tsx b/app/components/UI/Predict/components/PredictMarketMultiple/PredictMarketMultiple.test.tsx index 76d2285c8b5..ff5fa9659e6 100644 --- a/app/components/UI/Predict/components/PredictMarketMultiple/PredictMarketMultiple.test.tsx +++ b/app/components/UI/Predict/components/PredictMarketMultiple/PredictMarketMultiple.test.tsx @@ -115,6 +115,26 @@ describe('PredictMarketMultiple', () => { mockNavigate.mockClear(); }); + it('falls back to 0% label when percentage formatting throws', () => { + const formatModule = + jest.requireActual( + '../../utils/format', + ); + const spy = jest + .spyOn(formatModule, 'formatPercentage') + .mockImplementation(() => { + throw new Error('format failure'); + }); + + const { getByText } = renderWithProvider( + , + { state: initialState }, + ); + + expect(getByText('0')).toBeOnTheScreen(); + spy.mockRestore(); + }); + it('render market information correctly', () => { const { getByText } = renderWithProvider( , diff --git a/app/components/UI/Predict/hooks/usePredictDeposit.test.ts b/app/components/UI/Predict/hooks/usePredictDeposit.test.ts index b8b64ccf785..2b08c42b400 100644 --- a/app/components/UI/Predict/hooks/usePredictDeposit.test.ts +++ b/app/components/UI/Predict/hooks/usePredictDeposit.test.ts @@ -15,11 +15,6 @@ jest.mock('@react-navigation/native', () => ({ }), })); -jest.mock('@react-navigation/compat', () => ({ - withNavigation: (component: unknown) => component, - withNavigationFocus: (component: unknown) => component, -})); - jest.mock('../../../../core/Engine', () => ({ context: { PredictController: { diff --git a/app/components/UI/Predict/queries/priceHistory.test.ts b/app/components/UI/Predict/queries/priceHistory.test.ts new file mode 100644 index 00000000000..acbeb04326f --- /dev/null +++ b/app/components/UI/Predict/queries/priceHistory.test.ts @@ -0,0 +1,241 @@ +import { + predictPriceHistoryKeys, + predictPriceHistoryOptions, +} from './priceHistory'; +import { PredictPriceHistoryInterval } from '../types'; +import Engine from '../../../../core/Engine'; + +jest.mock('../../../../core/Engine', () => ({ + context: { + PredictController: { + getPriceHistory: jest.fn(), + }, + }, +})); + +describe('priceHistory queries', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('predictPriceHistoryKeys', () => { + it('returns base key for all price history', () => { + const keys = predictPriceHistoryKeys.all(); + + expect(keys).toEqual(['predict', 'priceHistory']); + }); + + it('returns detail key with market id and interval', () => { + const keys = predictPriceHistoryKeys.detail( + 'market-123', + PredictPriceHistoryInterval.ONE_DAY, + ); + + expect(keys).toEqual([ + 'predict', + 'priceHistory', + 'market-123', + PredictPriceHistoryInterval.ONE_DAY, + undefined, + undefined, + undefined, + ]); + }); + + it('returns detail key with all parameters', () => { + const keys = predictPriceHistoryKeys.detail( + 'market-456', + PredictPriceHistoryInterval.ONE_WEEK, + 100, + 1609459200, + 1609545600, + ); + + expect(keys).toEqual([ + 'predict', + 'priceHistory', + 'market-456', + PredictPriceHistoryInterval.ONE_WEEK, + 100, + 1609459200, + 1609545600, + ]); + }); + + it('returns detail key with fidelity only', () => { + const keys = predictPriceHistoryKeys.detail( + 'market-789', + PredictPriceHistoryInterval.ONE_HOUR, + 50, + ); + + expect(keys).toEqual([ + 'predict', + 'priceHistory', + 'market-789', + PredictPriceHistoryInterval.ONE_HOUR, + 50, + undefined, + undefined, + ]); + }); + }); + + describe('predictPriceHistoryOptions', () => { + it('returns query options with correct query key', () => { + const options = predictPriceHistoryOptions({ + marketId: 'market-123', + interval: PredictPriceHistoryInterval.ONE_DAY, + }); + + expect(options.queryKey).toEqual([ + 'predict', + 'priceHistory', + 'market-123', + PredictPriceHistoryInterval.ONE_DAY, + undefined, + undefined, + undefined, + ]); + }); + + it('uses default interval when not provided', () => { + const options = predictPriceHistoryOptions({ + marketId: 'market-123', + }); + + expect(options.queryKey).toEqual([ + 'predict', + 'priceHistory', + 'market-123', + PredictPriceHistoryInterval.ONE_DAY, + undefined, + undefined, + undefined, + ]); + }); + + it('includes all parameters in query key', () => { + const options = predictPriceHistoryOptions({ + marketId: 'market-456', + interval: PredictPriceHistoryInterval.ONE_WEEK, + fidelity: 100, + startTs: 1609459200, + endTs: 1609545600, + }); + + expect(options.queryKey).toEqual([ + 'predict', + 'priceHistory', + 'market-456', + PredictPriceHistoryInterval.ONE_WEEK, + 100, + 1609459200, + 1609545600, + ]); + }); + + it('has correct stale time', () => { + const options = predictPriceHistoryOptions({ + marketId: 'market-123', + }); + + expect(options.staleTime).toBe(5_000); + }); + + describe('queryFn', () => { + it('calls getPriceHistory with correct parameters', async () => { + const mockPriceHistory = [ + { timestamp: 1609459200, price: 0.5 }, + { timestamp: 1609545600, price: 0.55 }, + ]; + ( + Engine.context.PredictController.getPriceHistory as jest.Mock + ).mockResolvedValue(mockPriceHistory); + + const options = predictPriceHistoryOptions({ + marketId: 'market-123', + interval: PredictPriceHistoryInterval.ONE_DAY, + fidelity: 100, + startTs: 1609459200, + endTs: 1609545600, + }); + + expect(options.queryFn).toBeDefined(); + const result = await ( + options.queryFn as NonNullable + )({} as never); + + expect( + Engine.context.PredictController.getPriceHistory, + ).toHaveBeenCalledWith({ + marketId: 'market-123', + fidelity: 100, + interval: PredictPriceHistoryInterval.ONE_DAY, + startTs: 1609459200, + endTs: 1609545600, + }); + expect(result).toEqual(mockPriceHistory); + }); + + it('returns empty array when getPriceHistory returns null', async () => { + ( + Engine.context.PredictController.getPriceHistory as jest.Mock + ).mockResolvedValue(null); + + const options = predictPriceHistoryOptions({ + marketId: 'market-123', + }); + + expect(options.queryFn).toBeDefined(); + const result = await ( + options.queryFn as NonNullable + )({} as never); + + expect(result).toEqual([]); + }); + + it('returns empty array when getPriceHistory returns undefined', async () => { + ( + Engine.context.PredictController.getPriceHistory as jest.Mock + ).mockResolvedValue(undefined); + + const options = predictPriceHistoryOptions({ + marketId: 'market-123', + }); + + expect(options.queryFn).toBeDefined(); + const result = await ( + options.queryFn as NonNullable + )({} as never); + + expect(result).toEqual([]); + }); + + it('calls getPriceHistory with default interval', async () => { + ( + Engine.context.PredictController.getPriceHistory as jest.Mock + ).mockResolvedValue([]); + + const options = predictPriceHistoryOptions({ + marketId: 'market-789', + }); + + expect(options.queryFn).toBeDefined(); + await (options.queryFn as NonNullable)( + {} as never, + ); + + expect( + Engine.context.PredictController.getPriceHistory, + ).toHaveBeenCalledWith({ + marketId: 'market-789', + fidelity: undefined, + interval: PredictPriceHistoryInterval.ONE_DAY, + startTs: undefined, + endTs: undefined, + }); + }); + }); + }); +}); diff --git a/app/components/UI/Predict/routes/index.tsx b/app/components/UI/Predict/routes/index.tsx index 2d38dda1036..88cfeb777aa 100644 --- a/app/components/UI/Predict/routes/index.tsx +++ b/app/components/UI/Predict/routes/index.tsx @@ -56,9 +56,9 @@ const ModalStack = createStackNavigator(); const PredictModalStack = () => ( = { removeListener: jest.fn(), canGoBack: jest.fn(), isFocused: jest.fn(), - dangerouslyGetParent: jest.fn(), - dangerouslyGetState: jest.fn(), + getParent: jest.fn(), + getState: jest.fn(), + getId: jest.fn(), }; const initialState = { diff --git a/app/components/UI/Predict/views/PredictFeed/PredictFeed.tsx b/app/components/UI/Predict/views/PredictFeed/PredictFeed.tsx index 51615f6a308..462bf507497 100644 --- a/app/components/UI/Predict/views/PredictFeed/PredictFeed.tsx +++ b/app/components/UI/Predict/views/PredictFeed/PredictFeed.tsx @@ -631,15 +631,12 @@ const PredictFeed: React.FC = () => { if (navigation.canGoBack()) { navigation.goBack(); } else { - navigation.navigate( - Routes.WALLET.HOME as never, - { - screen: Routes.WALLET.TAB_STACK_FLOW, - params: { - screen: Routes.WALLET_VIEW, - }, - } as never, - ); + navigation.navigate(Routes.WALLET.HOME, { + screen: Routes.WALLET.TAB_STACK_FLOW, + params: { + screen: Routes.WALLET_VIEW, + }, + }); } }, [navigation]); diff --git a/app/components/UI/Predict/views/PredictSellPreview/PredictSellPreview.test.tsx b/app/components/UI/Predict/views/PredictSellPreview/PredictSellPreview.test.tsx index 2f584788953..9e327e7318d 100644 --- a/app/components/UI/Predict/views/PredictSellPreview/PredictSellPreview.test.tsx +++ b/app/components/UI/Predict/views/PredictSellPreview/PredictSellPreview.test.tsx @@ -225,8 +225,9 @@ const mockNavigation: NavigationProp = { removeListener: jest.fn(), canGoBack: jest.fn(), isFocused: jest.fn(), - dangerouslyGetParent: jest.fn(), - dangerouslyGetState: jest.fn(), + getParent: jest.fn(), + getState: jest.fn(), + getId: jest.fn(), }; const initialState = { diff --git a/app/components/UI/ProtectYourWalletModal/index.js b/app/components/UI/ProtectYourWalletModal/index.js index 9140fbc2f89..352cda15b83 100644 --- a/app/components/UI/ProtectYourWalletModal/index.js +++ b/app/components/UI/ProtectYourWalletModal/index.js @@ -25,7 +25,8 @@ import { strings } from '../../../../locales/i18n'; import scaling from '../../../util/scaling'; import { MetaMetricsEvents } from '../../../core/Analytics/MetaMetrics.events'; import { ProtectWalletModalSelectorsIDs } from './ProtectWalletModal.testIds'; -import { withAnalyticsAwareness } from '../../../components/hooks/useAnalytics/withAnalyticsAwareness'; +import { analytics } from '../../../util/analytics/analytics'; +import { AnalyticsEventBuilder } from '../../../util/analytics/AnalyticsEventBuilder'; import { selectSeedlessOnboardingLoginFlow } from '../../../selectors/seedlessOnboardingController'; import protectWalletImage from '../../../images/explain-backup-seedphrase.png'; @@ -36,7 +37,6 @@ class ProtectYourWalletModal extends PureComponent { protectWalletModalNotVisible: PropTypes.func, protectWalletModalVisible: PropTypes.bool, passwordSet: PropTypes.bool, - analytics: PropTypes.object, isSeedlessOnboardingLoginFlow: PropTypes.bool, }; @@ -46,9 +46,10 @@ class ProtectYourWalletModal extends PureComponent { 'SetPasswordFlow', this.props.passwordSet ? { screen: 'AccountBackupStep1' } : undefined, ); - this.props.analytics.trackEvent( - this.props.analytics - .createEventBuilder(MetaMetricsEvents.WALLET_SECURITY_PROTECT_ENGAGED) + analytics.trackEvent( + AnalyticsEventBuilder.createEventBuilder( + MetaMetricsEvents.WALLET_SECURITY_PROTECT_ENGAGED, + ) .addProperties({ wallet_protection_required: false, source: 'Modal', @@ -70,9 +71,10 @@ class ProtectYourWalletModal extends PureComponent { onDismiss = () => { this.props.protectWalletModalNotVisible(); - this.props.analytics.trackEvent( - this.props.analytics - .createEventBuilder(MetaMetricsEvents.WALLET_SECURITY_PROTECT_DISMISSED) + analytics.trackEvent( + AnalyticsEventBuilder.createEventBuilder( + MetaMetricsEvents.WALLET_SECURITY_PROTECT_DISMISSED, + ) .addProperties({ wallet_protection_required: false, source: 'Modal', @@ -174,4 +176,4 @@ const mapDispatchToProps = (dispatch) => ({ export default connect( mapStateToProps, mapDispatchToProps, -)(withAnalyticsAwareness(ProtectYourWalletModal)); +)(ProtectYourWalletModal); diff --git a/app/components/UI/ProtectYourWalletModal/index.test.tsx b/app/components/UI/ProtectYourWalletModal/index.test.tsx index b3ea730ed75..d623e877717 100644 --- a/app/components/UI/ProtectYourWalletModal/index.test.tsx +++ b/app/components/UI/ProtectYourWalletModal/index.test.tsx @@ -7,65 +7,15 @@ import { mockTheme, ThemeContext } from '../../../util/theme'; import renderWithProvider from '../../../util/test/renderWithProvider'; import { strings } from '../../../../locales/i18n'; import { ProtectWalletModalSelectorsIDs } from './ProtectWalletModal.testIds'; +import { analytics } from '../../../util/analytics/analytics'; -const mockMetricsIsEnabled = jest.fn().mockReturnValue(true); -const mockTrackEvent = jest.fn(); -const mockCreateEventBuilder = jest.fn().mockImplementation(() => ({ - addProperties: jest.fn().mockReturnThis(), - build: jest.fn().mockReturnValue({ - name: 'Wallet Security Reminder Engaged', - properties: { source: 'Modal', wallet_protection_required: false }, - saveDataRecording: true, - sensitiveProperties: {}, - }), -})); - -// Mock whenEngineReady to prevent Engine access after Jest teardown -jest.mock('../../../util/analytics/whenEngineReady', () => ({ - whenEngineReady: jest.fn().mockResolvedValue(undefined), -})); - -// Mock analytics module jest.mock('../../../util/analytics/analytics', () => ({ analytics: { - isEnabled: jest.fn(() => false), trackEvent: jest.fn(), - optIn: jest.fn().mockResolvedValue(undefined), - optOut: jest.fn().mockResolvedValue(undefined), - getAnalyticsId: jest.fn().mockResolvedValue('test-analytics-id'), - identify: jest.fn(), - trackView: jest.fn(), - isOptedIn: jest.fn().mockResolvedValue(false), }, })); -// Mock useAnalytics hook which is used by withAnalyticsAwareness HOC -jest.mock('../../../components/hooks/useAnalytics/useAnalytics', () => ({ - useAnalytics: () => ({ - trackEvent: mockTrackEvent, - createEventBuilder: mockCreateEventBuilder, - isEnabled: mockMetricsIsEnabled, - }), -})); - -jest.mock( - '../../../components/hooks/useAnalytics/withAnalyticsAwareness', - () => ({ - withAnalyticsAwareness: - (Component: React.ComponentType) => (props: Record) => ( - )} - /> - ), - }), -); +const mockTrackEvent = jest.mocked(analytics.trackEvent); const mockStore = configureMockStore(); @@ -194,7 +144,7 @@ describe('ProtectYourWalletModal', () => { expect.objectContaining({ name: 'Wallet Security Reminder Engaged', properties: { source: 'Modal', wallet_protection_required: false }, - saveDataRecording: true, + saveDataRecording: false, sensitiveProperties: {}, }), ); diff --git a/app/components/UI/Ramp/Aggregator/Views/BuildQuote/BuildQuote.test.tsx b/app/components/UI/Ramp/Aggregator/Views/BuildQuote/BuildQuote.test.tsx index 318dd947723..355268db5b1 100644 --- a/app/components/UI/Ramp/Aggregator/Views/BuildQuote/BuildQuote.test.tsx +++ b/app/components/UI/Ramp/Aggregator/Views/BuildQuote/BuildQuote.test.tsx @@ -102,7 +102,7 @@ jest.mock('@react-navigation/native', () => { goBack: mockGoBack, reset: mockReset, pop: mockPop, - dangerouslyGetParent: () => ({ + getParent: () => ({ pop: mockPop, }), }), diff --git a/app/components/UI/Ramp/Aggregator/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap b/app/components/UI/Ramp/Aggregator/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap index 4711ce40447..6625337434b 100644 --- a/app/components/UI/Ramp/Aggregator/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap +++ b/app/components/UI/Ramp/Aggregator/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap @@ -20,447 +20,199 @@ exports[`BuildQuote View Balance display displays balance from useBalance for no } > - - - - - - - - - - - - - Buy - - - - - - - - - - - - - - - - - + - - - - - + + + - - + + + - + + - - - - - - - ๐Ÿ‡จ๐Ÿ‡ฑ - - - - - - - You want to buy - - - + ๐Ÿ‡จ๐Ÿ‡ฑ + + + + + + - - + + + }, + undefined, + undefined, + { + "padding": 0, + }, + undefined, + ] + } + > + - - - + + + > + + - - - + > + + - - - - - Ethereum - - - - + testID="listitem-gap" + /> - ETH + Ethereum - + + + - + > + + ETH + + + + - - - - - Current balance - : - - 1.5 - โ‰ˆ $27.02 - - - - Amount - - + - - - - - - $ - 0 - - - - - - - - - USD - - - - - - - + Current balance + : + + 1.5 + โ‰ˆ $27.02 + - - - - Minimum deposit is - - $ - 2 - - - - Update payment method + Amount - - - ๎€ช - - - - - Credit or Debit Card - + + $ + 0 + + - - Change - - - - + > + + USD + + + + + + - + + + + } + testID="min-limit-error" + > + Minimum deposit is + + $ + 2 + + + + + Update payment method + + - + + + + ๎€ช + + + + + + Credit or Debit Card + + + + + + Change + + + + + + + > + + + + - - + + - - - + - - - Get quotes - - - - - - - - - - - - $100 - - - - - $500 - - + "paddingTop": 12, + }, + ] + } + > - $1000 + Get quotes - + - - + - + + $100 + + + + + $500 + + + - 1 - - - + + $1000 + + + + + + - - - 2 - - - - - + 1 + + + + - - 3 - - - - - - - + 2 + + + + - - 4 - - + + 3 + + + - - - 5 - - - - - + 4 + + + + - - 6 - - - - - - - + 5 + + + + - - 7 - - + + 6 + + + - - - 8 - - - - - + 7 + + + + - - 9 - - - - - - - + 8 + + + + - - . - - + + 9 + + + - - + + . + + + + + - 0 - - - - - + 0 + + + + - - + testID="keypad-delete-button" + > + + + - - - - - Done - - + + Done + + + - - - - - - - - - - - - -`; + + + + + + + + + + + + + + + + Buy + + + + + + + + + + + + + + + + + + + + + + + + +`; exports[`BuildQuote View Crypto Currency Data renders a special error page if crypto currencies are not available 1`] = ` - - - - - - - - - + - - Buy - - - - - - - - - - - - - - - - - - - - - - - - + + ๓ฐ‹ฝ + + + - ๓ฐ‹ฝ - - - + No tokens available + + + - + + There are currently no tokens available to purchase on this network with the selected payment method. + + + - No tokens available - + + + Change payment method + + + + + + + + + + + + + - - There are currently no tokens available to purchase on this network with the selected payment method. - + /> - + + + + + Buy + + + + + + - - + - Change payment method - - + "color": "#131416", + "height": 24, + "width": 24, + }, + undefined, + ] + } + /> - + - - - + + + + `; @@ -3133,378 +3141,199 @@ exports[`BuildQuote View Crypto Currency Data renders a special error page if cr } > - - - - - - - - - - - - - Sell - - - - - - - - - - - - - + - - - - + + ๓ฐ‹ฝ + + + - ๓ฐ‹ฝ - - - + No tokens available + + + - + + There are currently no tokens available to sell on this network with the selected cash destination. + + + - No tokens available - + + + Change cash destination + + + + + + + + + + + + + - - There are currently no tokens available to sell on this network with the selected cash destination. - + /> - + + + + - - - Change cash destination - - - + Sell + + + + - + - - - + + + + `; @@ -3723,447 +3735,199 @@ exports[`BuildQuote View Crypto Currency Data renders an error page when there i } > - - - - - - - - - - - - - Buy - - - - - - - - - - - - - - - - - + - - - - + - ๓ฐ…š - - - - + ๓ฐ…š + + + - Error - - - - + Error + + + - Test error - - - - + Test error + + + - - Try again - - + + Try again + + + - - - - - - - - - - - - -`; - -exports[`BuildQuote View Crypto Currency Data renders the loading page when cryptos are loading 1`] = ` - - - - - - - - - - - - - - - - Buy - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + Buy + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`BuildQuote View Crypto Currency Data renders the loading page when cryptos are loading 1`] = ` + + + + - - + - - - - - - - + + + - + - - - - + width={16} + /> + + + - - ๐Ÿ‡จ๐Ÿ‡ฑ - - - - - - - You want to buy - - - + ๐Ÿ‡จ๐Ÿ‡ฑ + + + + + + - - + + + }, + undefined, + undefined, + { + "padding": 0, + }, + undefined, + ] + } + > + + + + - - - + + + + testID="listitem-gap" + /> - - - - + testID="listitemcolumn" + > + + - - - + - - - Amount - - - + + + + Amount + + - - - $ - 0 - - - - - - - - USD + $ + 0 - - - - - - - - - - + + + + + + + USD + + + + + + + + + + - Minimum deposit is - - $ - 2 - - - - + Minimum deposit is + + $ + 2 + + + - Update payment method - - - + Update payment method + + - - - + testID="listitemcolumn" + > + + + + + - - - - - - - - - - - - - + + + + + + + + + + - - - Get quotes - - - - - - - - - - - - $100 - - - - - $500 - - + "paddingTop": 12, + }, + ] + } + > - $1000 + Get quotes - + - - + - + + $100 + + + - 1 - - - - - - - 2 - - - - - + $500 + + + - + - 3 - - - + "lineHeight": 24, + } + } + > + $1000 + + + + @@ -6098,566 +5690,994 @@ exports[`BuildQuote View Crypto Currency Data renders the loading page when cryp [ { "display": "flex", - "flexBasis": "0%", - "flexGrow": 1, - "flexShrink": 1, + "flexDirection": "row", + "gap": 12, + "justifyContent": "space-between", }, undefined, ] } > - - - 4 - - - - - + 1 + + + + - - 5 - - - - - + 2 + + + + - - 6 - - + + 3 + + + - - - - - 7 - - - - - + 4 + + + + - - 8 - - - - - + 5 + + + + - - 9 - - + + 6 + + + - - - + + + 7 + + + + - - . - - - - - + 8 + + + + - - 0 - - + + 9 + + + - - - - - - - - - + + . + + + + - Done - - - - + > + + + 0 + + + + + + + + + + + + + + Done + + + + + + + + + + + + + + + + + + + + + Buy + + + + + + + + + + + - + - - - + + + + `; @@ -6682,447 +6702,199 @@ exports[`BuildQuote View Fiat Currency Data renders an error page when there is } > - - - - - - - - - - - - - Buy - - - - - - - - - - - - - - - - - + - - - - + - ๓ฐ…š - - - - + ๓ฐ…š + + + - Error - - - - + Error + + + - Test error - - - - + Test error + + + - - Try again - - + + Try again + + + + + + + + + + + + + + + + + + + + + + + Buy + + + + + + + - + - - - + + + + `; @@ -7341,447 +7365,199 @@ exports[`BuildQuote View Fiat Currency Data renders the loading page when fiats } > - - - - - - - - - + - - - Buy - - - - - - - - - - - - - - - - - - - - - - - - - + + + - + - - - - + width={16} + /> + + + - - ๐Ÿ‡จ๐Ÿ‡ฑ - - + ๐Ÿ‡จ๐Ÿ‡ฑ + + + + + + + You want to buy + + + + + + > + + + + + + + + + + + + + + + - - - You want to buy - - + + + - + Amount + + + - + onPress={[Function]} + testID="amount-input" + > + + - - - - - - - + - - - Amount - - + > + + Minimum deposit is + + $ + 2 + + - + Update payment method + + - + + + - + + + + - + - + + + + + - - Minimum deposit is - - $ - 2 - - - - - Update payment method - - - - + + + + + + + + + + - - - - - - - - - - - + + + - + $500 + + + + - - - - - + $1000 + + + + - - - - - - - Get quotes - - - - - - - - - - - - $100 - - - - - $500 - - - + 1 + + + + - - $1000 - - - - - - - - - + 2 + + + + - - 1 - - + + 3 + + + - - - 2 - - - - - + 4 + + + + - - 3 - - - - - - - + 5 + + + + - - 4 - - + + 6 + + + - - - 5 - - - - - + 7 + + + + - - 6 - - - - - - - + 8 + + + + - - 7 - - + + 9 + + + - - - 8 - - - - - + . + + + + - - 9 - - - - - - - + 0 + + + + - - . - - + + + - + + - - - 0 - - - + Done + + + + + + + + + + + + - - - + /> @@ -9541,62 +9485,146 @@ exports[`BuildQuote View Fiat Currency Data renders the loading page when fiats style={ [ { - "padding": 15, - "paddingHorizontal": 16, + "alignItems": "center", + "display": "flex", + "flexBasis": "0%", + "flexGrow": 1, + "flexShrink": 1, }, undefined, - undefined, ] } > - - Done + Buy - + - - - + + + + + + + + + + - - - + + + + `; @@ -9621,447 +9649,199 @@ exports[`BuildQuote View Payment Method Data renders an error page when there is } > - - - - - - - - - - - - - Buy - - - - - - - - - - - - - - - - - + - - - - + - ๓ฐ…š - - - - + ๓ฐ…š + + + - Error - - - - + Error + + + - Test error - - - - + Test error + + + - - Try again - - + + Try again + + + - - - - - - - - - - - - -`; - -exports[`BuildQuote View Payment Method Data renders no icons if there are no payment methods 1`] = ` - - - - - - - - - - - - - - - - Buy - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + Buy + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`BuildQuote View Payment Method Data renders no icons if there are no payment methods 1`] = ` + + + + - - + - - - - - - - + + + - + - - - - + width={16} + /> + + + - - ๐Ÿ‡จ๐Ÿ‡ฑ - - - - - - - You want to buy - - - + ๐Ÿ‡จ๐Ÿ‡ฑ + + + + + + - - + + + }, + undefined, + undefined, + { + "padding": 0, + }, + undefined, + ] + } + > + - - - + + + > + + - - - + > + + - - - - - Ethereum - - - - + testID="listitem-gap" + /> - ETH + Ethereum - + + + + > + + ETH + + + - - - - + - Current balance - : - - 5.36385 ETH - โ‰ˆ $27.02 - - - - Amount - - - + Current balance + : + + 5.36385 ETH + โ‰ˆ $27.02 + + + + Amount + + - - - $ - 0 - - - - - - - - USD + $ + 0 - + + + + + - - + > + + USD + + + + + - - - - + - Minimum deposit is - - $ - 2 - - - - + Minimum deposit is + + $ + 2 + + + - Update payment method - - - + Update payment method + + - - ๎€ช - - - - - + ๎€ช + + + - Credit or Debit Card - - - - - + - Change - - + Credit or Debit Card + + + + + + Change + + + - - - - - + /> + + + + - - - + - - - Get quotes - - - - - - - - - - - - $100 - - - - - $500 - - + "paddingTop": 12, + }, + ] + } + > - $1000 + Get quotes - + - - + - + - 1 - - - - - + $100 + + + - + - 2 - - - - - + $500 + + + - + - 3 - - - + "lineHeight": 24, + } + } + > + $1000 + + + + @@ -12107,566 +11715,994 @@ exports[`BuildQuote View Payment Method Data renders no icons if there are no pa [ { "display": "flex", - "flexBasis": "0%", - "flexGrow": 1, - "flexShrink": 1, + "flexDirection": "row", + "gap": 12, + "justifyContent": "space-between", }, undefined, ] } > - - - 4 - - - - - + 1 + + + + - - 5 - - - - - + 2 + + + + - - 6 - - + + 3 + + + - - - - - 7 - - - - - + 4 + + + + - - 8 - - - - - + 5 + + + + - - 9 - - + + 6 + + + - - - + + + 7 + + + + - - . - - - - - + 8 + + + + - - 0 - - + + 9 + + + - - + + . + + + + + - + testID="keypad-key-0" + > + + 0 + + + + + + + + - - - + + + Done + + + + + + + + + + + + + + + + + + + + Buy + + + + + + - Done - - + + + - + - - - + + + + `; @@ -12691,447 +12727,199 @@ exports[`BuildQuote View Payment Method Data renders the loading page when payme } > - - - - - - - - - + - - Buy - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + - + + - - - + + ๐Ÿ‡จ๐Ÿ‡ฑ + + + + + + + > + You want to buy + - - ๐Ÿ‡จ๐Ÿ‡ฑ - - - - - - - You want to buy - - - - - + }, + undefined, + undefined, + { + "padding": 0, + }, + undefined, + ] + } + > + - - - + + + > + + - - - + > + + - - - - - Ethereum - - - - + testID="listitem-gap" + /> - ETH + Ethereum - + + + + > + + ETH + + + - - - - + - Current balance - : - - 5.36385 ETH - โ‰ˆ $27.02 - - - - Amount - - - - - - - - - $ - 0 - - - - - - - - - USD - - - - - - - + Current balance + : + + 5.36385 ETH + โ‰ˆ $27.02 + - - - - Minimum deposit is - - $ - 2 - - - - Update payment method + Amount - + onPress={[Function]} + testID="amount-input" + > + + $ + 0 + + - + onPress={[Function]} + testID="select-currency" + > + + + USD + + + + - - + + + + Minimum deposit is + + $ + 2 + + + + + Update payment method + + + + + + + + + + + + + + + + + - - + + - - - + - - - Get quotes - - - - - - - - - + > - $100 + Get quotes - + + + + + - + + - $500 - - - + $100 + + + - + + $500 + + + - $1000 - - - - - - + + $1000 + + + + + @@ -14338,676 +14123,847 @@ exports[`BuildQuote View Payment Method Data renders the loading page when payme [ { "display": "flex", - "flexBasis": "0%", - "flexGrow": 1, - "flexShrink": 1, + "flexDirection": "row", + "gap": 12, + "justifyContent": "space-between", }, undefined, ] } > - - - 1 - - - - - + 1 + + + + - - 2 - - + + 2 + + + + + + + 3 + + + - - - 3 - - - - - - - + 4 + + + + - - 4 - - - - - + 5 + + + + - - 5 - - + + 6 + + + - - - 6 - - - - - - - + 7 + + + + - - 7 - - - - - + 8 + + + + - - 8 - - + + 9 + + + - - - 9 - - - - - - - + . + + + + - - . - - - - - + 0 + + + + - - 0 - - + + + + + + + + Done + + + + + + + + + + + + - - - + /> @@ -15015,1186 +14971,363 @@ exports[`BuildQuote View Payment Method Data renders the loading page when payme style={ [ { - "padding": 15, - "paddingHorizontal": 16, + "alignItems": "center", + "display": "flex", + "flexBasis": "0%", + "flexGrow": 1, + "flexShrink": 1, }, undefined, - undefined, ] } > - - Done + Buy - + - - - - - - - - - - - - - -`; - -exports[`BuildQuote View Regions data renders an error page when there is a region error 1`] = ` - - - - - - - - - - - - - - - - Buy - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - ๓ฐ…š - - - - - Error - - - - - Test error - - - - - - Try again - - - - - - - - - - - - - - - - - - -`; - -exports[`BuildQuote View Regions data renders the loading page when regions are loading 1`] = ` - - - - - - - - - - - - - - - - Buy - - - - - - - + "opacity": 1, + "width": 32, + }, + undefined, + { + "transform": [ + { + "scale": 1, + }, + ], + }, + ] + } + testID="deposit-configuration-menu-button" + > + + + + + + + + + - - - - - + + + + + +`; + +exports[`BuildQuote View Regions data renders an error page when there is a region error 1`] = ` + + + + - - + - - - - - + ๓ฐ…š + + + + - + + + - - + "color": "#131416", + "fontFamily": "Geist-Regular", + "fontSize": 30, + "marginVertical": 2, + }, + { + "textAlign": "center", + }, + undefined, + undefined, + undefined, + undefined, + undefined, + { + "color": "#66676a", + }, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ] + } + > + Test error + + + + - - - - + Try again + + + + + + + + + + + + + + + - - - - You want to buy - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + Buy + + + + + + - - + + + + + + + + + + + + + + + +`; + +exports[`BuildQuote View Regions data renders the loading page when regions are loading 1`] = ` + + + + + + + + + + + + + + + + + - Amount - - - - - - + /> - + + + + + + + + You want to buy + + + + + + + + + "width": 16, + } + } + testID="listitem-gap" + /> + + + + + + + + - - - - + - Minimum deposit is - - $ - 2 - - - + + - Update payment method + Amount - + onPress={[Function]} + testID="amount-input" + > + + - + + + + } + testID="min-limit-error" + > + Minimum deposit is + + $ + 2 + + + + + Update payment method + + + + + + + + + + + + + - - - - - - - - - - - - - Get quotes - - + + + + + + + - - + - - - + - $100 + Get quotes - + + + + + - + + - $500 - - - + $100 + + + + + $500 + + + + + $1000 + + + + + + + + - + + 1 + + + + + - $1000 - - - - - - - - - + 2 + + + + - - 1 - - + + 3 + + + - - - 2 - - - - - + 4 + + + + - - 3 - - - - - - - + 5 + + + + - - 4 - - + + 6 + + + - - - 5 - - - - - + 7 + + + + + + + 8 + + + + - - 6 - - + + 9 + + + - - - - - 7 - - - - - + . + + + + - - 8 - - - - - + 0 + + + + - - 9 - - + + + - - - - . - - - + Done + + + + + + + + + + + + - - - 0 - - - - - - - + /> @@ -17930,527 +17894,363 @@ exports[`BuildQuote View Regions data renders the loading page when regions are style={ [ { - "padding": 15, - "paddingHorizontal": 16, + "alignItems": "center", + "display": "flex", + "flexBasis": "0%", + "flexGrow": 1, + "flexShrink": 1, }, undefined, - undefined, ] } > - - Done + Buy - + - - - - - - - - - - - - - -`; - -exports[`BuildQuote View renders correctly 1`] = ` - - - - - - - - - - - - - - - - Buy - - - - - - - + + + + + + + + + + + + - - - - - + + + + + +`; + +exports[`BuildQuote View renders correctly 1`] = ` + + + + - - + - - - - + + + + + + + + + + + + ๐Ÿ‡จ๐Ÿ‡ฑ + + + + + + + You want to buy + + + + + > + + + + + + + + + + + + + + + + + + + + + Ethereum + + + + + + + ETH + + + + + + - - - - - - ๐Ÿ‡จ๐Ÿ‡ฑ - - - - - - - You want to buy - - - + + - + Amount + + + - - - - - - - - - - - - - - + $ + 0 + + - - Ethereum - - - - - - - ETH - - - + > + USD + + + + - - - - + - Current balance - : - - 5.36385 ETH - โ‰ˆ $27.02 - - - - Amount - - + + Minimum deposit is + + $ + 2 + + - + Update payment method + + - + + + ๎€ช + + + - + - $ - 0 - - - - - - + Credit or Debit Card + + + + testID="listitem-gap" + /> - USD + Change - + - - - - - - - Minimum deposit is - - $ - 2 - - - - - Update payment method - - - - + /> - - ๎€ช - - - - - Credit or Debit Card - - - - - Change - - - - - - - - - - + /> + - - - - - - - - + + + + + + - - - Get quotes - - - - - - - - - + > - $100 + Get quotes - + + + + + - + + - $500 - - - + $100 + + + - + + $500 + + + - $1000 - - - - - - + + $1000 + + + + + @@ -19707,676 +19504,847 @@ exports[`BuildQuote View renders correctly 1`] = ` [ { "display": "flex", - "flexBasis": "0%", - "flexGrow": 1, - "flexShrink": 1, + "flexDirection": "row", + "gap": 12, + "justifyContent": "space-between", }, undefined, ] } > - + + + 1 + + + + - - 1 - - - - - + 2 + + + + - - 2 - - + + 3 + + + - - - 3 - - - - - - - + 4 + + + + - - 4 - - - - - + 5 + + + + - - 5 - - + + 6 + + + - - - 6 - - - - - - - + + 7 + + + + - - 7 - - - - - + 8 + + + + - - 8 - - + + 9 + + + - - - 9 - - - - - - - + . + + + + - - . - - - - - + 0 + + + + - - 0 - - + + + + + + + + Done + + + + + + + + + + + + - - - + /> @@ -20384,62 +20352,146 @@ exports[`BuildQuote View renders correctly 1`] = ` style={ [ { - "padding": 15, - "paddingHorizontal": 16, + "alignItems": "center", + "display": "flex", + "flexBasis": "0%", + "flexGrow": 1, + "flexShrink": 1, }, undefined, - undefined, ] } > - + Buy + + + + + + - Done - - + + + - + - - - + + + + `; @@ -20464,378 +20516,199 @@ exports[`BuildQuote View renders correctly 2`] = ` } > - - - - - - - - - - - - - Sell - - - - - - - - - - - - - + - - - - - - - + + + - + - - - - + width={16} + /> + + + - - ๐Ÿ‡จ๐Ÿ‡ฑ - - - - - - + > + ๐Ÿ‡จ๐Ÿ‡ฑ + + + + + - - USD - - - - - - - You want to sell - - - + USD + + + + + + - - + + + }, + undefined, + undefined, + { + "padding": 0, + }, + undefined, + ] + } + > + - - - + + + > + + - - - + > + + - - - - - Ethereum - - - - + testID="listitem-gap" + /> - ETH + Ethereum - + + + + > + + ETH + + + - - - - - Current balance - : - - 5.36385 ETH - โ‰ˆ $27.02 - - - - Amount - - + - - - - - - 0 ETH - - - - - - - - - - Enter a larger amount to continue - - - + + Current balance + : + + 5.36385 ETH + โ‰ˆ $27.02 + + - Send your cash to + Amount - - - - ๎€ช - - - - - - Credit or Debit Card - - - - - - Change - - - - - - - - - - - - - - - - - - - - - - - Get quotes - - - - - - - - - - + + + + 0 ETH + + + + + + + + - 25% + Enter a larger amount to continue - - + - 50% + Send your cash to - - + + + + + + ๎€ช + + + + + + Credit or Debit Card + + + + + + Change + + + + + + + + + + + + + + + + + + + + - - 75% - - + "marginVertical": 8, + }, + undefined, + undefined, + { + "paddingTop": 12, + }, + ] + } + > - MAX + Get quotes - + + + + + + + 25% + + + + + 50% + + + + + 75% + + + + + MAX + + + + + @@ -22107,676 +21977,847 @@ exports[`BuildQuote View renders correctly 2`] = ` [ { "display": "flex", - "flexBasis": "0%", - "flexGrow": 1, - "flexShrink": 1, + "flexDirection": "row", + "gap": 12, + "justifyContent": "space-between", }, undefined, ] } > - - - 1 - - - - - + 1 + + + + + + + 2 + + + + - - 2 - - + + 3 + + + - - - 3 - - - - - - - + 4 + + + + - - 4 - - - - - + 5 + + + + - - 5 - - + + 6 + + + - - - 6 - - - - - - - + 7 + + + + - - 7 - - - - - + 8 + + + + - - 8 - - + + 9 + + + - - - 9 - - - - - - - + . + + + + - - . - - - - - + 0 + + + + - - 0 - - + + + + + + + + Done + + + + + + + + + + + + - - - + /> @@ -22784,62 +22825,77 @@ exports[`BuildQuote View renders correctly 2`] = ` style={ [ { - "padding": 15, - "paddingHorizontal": 16, + "alignItems": "center", + "display": "flex", + "flexBasis": "0%", + "flexGrow": 1, + "flexShrink": 1, }, undefined, - undefined, ] } > - - Done + Sell - + + + + - + - - - + + + + `; @@ -22864,641 +22920,645 @@ exports[`BuildQuote View renders correctly when sdkError is present 1`] = ` } > - - - - - - - - - - - - - Buy - - - - - - - - - - - - - - - - - + - - + + + + + + + + ๓ฐ…š + + + + + Oops, something went wrong + + + + + sdkError + + + + + + Return to home screen + + + + + + + + + + - + - + - ๓ฐ…š - + /> - + + + - + - Oops, something went wrong - - + }, + undefined, + ] + } + > + Buy + + + + + - - sdkError - - - - - + - Return to home screen - - + "color": "#131416", + "height": 24, + "width": 24, + }, + undefined, + ] + } + /> - + - - - + + + + `; @@ -23523,572 +23583,576 @@ exports[`BuildQuote View renders correctly when sdkError is present 2`] = ` } > - - - - - - - - - - - - - Sell - - - - - - - - - - - - - + - - + + + + + + + + ๓ฐ…š + + + + + Oops, something went wrong + + + + + sdkError in sell + + + + + + Return to home screen + + + + + + + + + + - + - - ๓ฐ…š - - - - - Oops, something went wrong - - - - - sdkError in sell - + /> - + + + + - - - Return to home screen - - - + Sell + + + + - + - - - + + + + `; diff --git a/app/components/UI/Ramp/Aggregator/Views/Checkout/Checkout.test.tsx b/app/components/UI/Ramp/Aggregator/Views/Checkout/Checkout.test.tsx index caad2efe042..1590dd500aa 100644 --- a/app/components/UI/Ramp/Aggregator/Views/Checkout/Checkout.test.tsx +++ b/app/components/UI/Ramp/Aggregator/Views/Checkout/Checkout.test.tsx @@ -31,7 +31,7 @@ const mockPop = jest.fn(); const mockNavigation = { goBack: jest.fn(), setOptions: mockSetOptions, - dangerouslyGetParent: () => ({ pop: mockPop }), + getParent: () => ({ pop: mockPop }), isFocused: jest.fn(() => true), }; jest.mock('@react-navigation/native', () => ({ diff --git a/app/components/UI/Ramp/Aggregator/Views/Checkout/Checkout.tsx b/app/components/UI/Ramp/Aggregator/Views/Checkout/Checkout.tsx index d2bf3b17d08..3f0c8a86f58 100644 --- a/app/components/UI/Ramp/Aggregator/Views/Checkout/Checkout.tsx +++ b/app/components/UI/Ramp/Aggregator/Views/Checkout/Checkout.tsx @@ -137,7 +137,7 @@ const CheckoutWebView = () => { // There was no query params in the URL to parse // Most likely the user clicked the X in Wyre widget // @ts-expect-error navigation prop mismatch - navigation.dangerouslyGetParent()?.pop(); + navigation.getParent()?.pop(); return; } if (!selectedAddress) { diff --git a/app/components/UI/Ramp/Aggregator/Views/Checkout/__snapshots__/Checkout.test.tsx.snap b/app/components/UI/Ramp/Aggregator/Views/Checkout/__snapshots__/Checkout.test.tsx.snap index a475b6e2e66..735bfa37fb6 100644 --- a/app/components/UI/Ramp/Aggregator/Views/Checkout/__snapshots__/Checkout.test.tsx.snap +++ b/app/components/UI/Ramp/Aggregator/Views/Checkout/__snapshots__/Checkout.test.tsx.snap @@ -20,351 +20,331 @@ exports[`Checkout displays WebView when url is present and no errors 1`] = ` } > - - - + + /> + + - + - Checkout - + + Checkout + + + - - - - - + - - - - - - - + /> + + - + - + testID="header" + > + + + - + + - - + testID="checkout-close-button" + > + + + - - + testID="checkout-webview" + /> + @@ -535,9 +558,9 @@ exports[`Checkout displays WebView when url is present and no errors 1`] = ` - - - + + + `; @@ -562,351 +585,331 @@ exports[`Checkout displays and tracks error if no url or errors 1`] = ` } > - - - + + /> + + - + - Checkout - + + Checkout + + + - - - - - + - - - - - - - + /> + + - + - + testID="header" + > + + + - + + - - + testID="checkout-close-button" + > + + + - - - - + - ๓ฐ…š - - - - + ๓ฐ…š + + + - Error - - - - + Error + + + - No URL was provided to continue - + + No URL was provided to continue + + - - + + @@ -1229,9 +1275,9 @@ exports[`Checkout displays and tracks error if no url or errors 1`] = ` - - - + + + `; @@ -1256,351 +1302,331 @@ exports[`Checkout displays sdkError when present 1`] = ` } > - - - + + /> + + - + - Checkout - + + Checkout + + + - - - - - + - - - - - - - + /> + + - + - + testID="header" + > + + + - + + - - + testID="checkout-close-button" + > + + + - - - - + - ๓ฐ…š - - - - + ๓ฐ…š + + + - Oops, something went wrong - - - - + Oops, something went wrong + + + - SDK Error - - - - + SDK Error + + + - - Return to home screen - - + + Return to home screen + + + - - + + @@ -1967,9 +2036,9 @@ exports[`Checkout displays sdkError when present 1`] = ` - - - + + + `; @@ -1994,351 +2063,331 @@ exports[`Checkout displays sell WebView when url is present and no errors 1`] = } > - - - + + /> + + - + - Checkout - + + Checkout + + + - - - - - + - - - - + } + > - - - + /> + + - + - + testID="header" + > + + + - + + - - + testID="checkout-close-button" + > + + + - - + testID="checkout-webview" + /> + @@ -2509,9 +2601,9 @@ exports[`Checkout displays sell WebView when url is present and no errors 1`] = - - - + + + `; @@ -2536,351 +2628,331 @@ exports[`Checkout handles get order error gracefully 1`] = ` } > - - - + + /> + + - + - Checkout - + + Checkout + + + - - - - - + - - - + - - + } + > - - - + /> + + - + - + testID="header" + > + + + - + + - - + testID="checkout-close-button" + > + + + - - - - + - ๓ฐ…š - - - - + ๓ฐ…š + + + - Error - - - - + Error + + + - Get order error - - - - + Get order error + + + - - Try again - - + + Try again + + + - - + + @@ -3247,9 +3362,9 @@ exports[`Checkout handles get order error gracefully 1`] = ` - - - + + + `; @@ -3274,351 +3389,331 @@ exports[`Checkout handles undefined order gracefully 1`] = ` } > - - - + + /> + + + + - + + Checkout + + + - Checkout - + /> - - - - - + - - - - - - - + /> + + - + - + testID="header" + > + + + - + + - - + testID="checkout-close-button" + > + + + - - - - + - ๓ฐ…š - - - - + ๓ฐ…š + + + - Error - - - - + Error + + + - Order could not be retrieved. Callback was https://callback.test?success=true - - - - + Order could not be retrieved. Callback was https://callback.test?success=true + + + - - Try again - - + + Try again + + + - - + + @@ -3985,9 +4123,9 @@ exports[`Checkout handles undefined order gracefully 1`] = ` - - - + + + `; @@ -4012,351 +4150,331 @@ exports[`Checkout ignores irrelevant error on http error in WebView for callback } > - - - + + /> + + - + - Checkout - + + Checkout + + + - - - - - + - - - - - - - + /> + + - + - + testID="header" + > + + + - + + - - + testID="checkout-close-button" + > + + + - - + style={ + { + "backgroundColor": "#ffffff", + } + } + testID="checkout-webview" + /> + @@ -4527,9 +4688,9 @@ exports[`Checkout ignores irrelevant error on http error in WebView for callback - - - + + + `; @@ -4554,351 +4715,331 @@ exports[`Checkout sets and displays error on http error in WebView 1`] = ` } > - - - + + /> + + - + - Checkout - + + Checkout + + + - - - - - + - - - - - - - + /> + + - + - + testID="header" + > + + + - + + - - - - - - + + + + + + - - + - ๓ฐ…š - - - - + ๓ฐ…š + + + - Error - - - - + Error + + + - WebView received error status code: 500 - - - - + WebView received error status code: 500 + + + - - Try again - - + + Try again + + + - - + + @@ -5265,9 +5449,9 @@ exports[`Checkout sets and displays error on http error in WebView 1`] = ` - - - + + + `; @@ -5292,351 +5476,331 @@ exports[`Checkout sets and displays error on http error in WebView for callback } > - - - + + /> + + - + - Checkout - + + Checkout + + + - - - - - + - - - - + } + > - - - + /> + + - + - + testID="header" + > + + + - + + - - + testID="checkout-close-button" + > + + + - - - - + - ๓ฐ…š - - - - + ๓ฐ…š + + + - Error - - - - + Error + + + - WebView received error status code: 500 - - - - + WebView received error status code: 500 + + + - - Try again - - + + Try again + + + - - + + @@ -6003,9 +6210,9 @@ exports[`Checkout sets and displays error on http error in WebView for callback - - - + + + `; @@ -6030,351 +6237,331 @@ exports[`Checkout sets error when handling url navigation state change and selec } > - - - + + /> + + - + - Checkout - + + Checkout + + + - - - - - + - - - - - - - + /> + + - + - + testID="header" + > + + + - + + - - + testID="checkout-close-button" + > + + + - - - - + - ๓ฐ…š - - - - + ๓ฐ…š + + + - Error - - - - + Error + + + - No wallet address was provided to continue - - - - + No wallet address was provided to continue + + + - - Try again - - + + Try again + + + - - + + @@ -6741,9 +6971,9 @@ exports[`Checkout sets error when handling url navigation state change and selec - - - + + + `; diff --git a/app/components/UI/Ramp/Aggregator/Views/Modals/Settings/SettingsModal.test.tsx b/app/components/UI/Ramp/Aggregator/Views/Modals/Settings/SettingsModal.test.tsx index 4a3e0c25ebb..eb07496bcd9 100644 --- a/app/components/UI/Ramp/Aggregator/Views/Modals/Settings/SettingsModal.test.tsx +++ b/app/components/UI/Ramp/Aggregator/Views/Modals/Settings/SettingsModal.test.tsx @@ -37,7 +37,7 @@ jest.mock('@react-navigation/native', () => { ...actualReactNavigation.useNavigation(), navigate: mockNavigate, goBack: mockGoBack, - dangerouslyGetParent: mockDangerouslyGetParent, + getParent: mockDangerouslyGetParent, }), }; }); @@ -77,7 +77,7 @@ describe('SettingsModal', () => { beforeEach(() => { jest.clearAllMocks(); mockDangerouslyGetParent.mockReturnValue({ - dangerouslyGetParent: jest.fn().mockReturnValue({ + getParent: jest.fn().mockReturnValue({ goBack: jest.fn(), }), }); @@ -134,7 +134,7 @@ describe('SettingsModal', () => { it('navigates back through parent navigation when deposit is pressed', () => { const mockParentGoBack = jest.fn(); mockDangerouslyGetParent.mockReturnValue({ - dangerouslyGetParent: jest.fn().mockReturnValue({ + getParent: jest.fn().mockReturnValue({ goBack: mockParentGoBack, }), }); diff --git a/app/components/UI/Ramp/Aggregator/Views/Modals/Settings/SettingsModal.tsx b/app/components/UI/Ramp/Aggregator/Views/Modals/Settings/SettingsModal.tsx index d6f9d54ec9d..fe9c8a5e975 100644 --- a/app/components/UI/Ramp/Aggregator/Views/Modals/Settings/SettingsModal.tsx +++ b/app/components/UI/Ramp/Aggregator/Views/Modals/Settings/SettingsModal.tsx @@ -49,7 +49,7 @@ function SettingsModal() { order_count: buttonClickData.order_count, }); sheetRef.current?.onCloseBottomSheet(); - navigation.dangerouslyGetParent()?.dangerouslyGetParent()?.goBack(); + navigation.getParent()?.getParent()?.goBack(); goToDeposit(); }, [ navigation, diff --git a/app/components/UI/Ramp/Aggregator/Views/Modals/Settings/__snapshots__/SettingsModal.test.tsx.snap b/app/components/UI/Ramp/Aggregator/Views/Modals/Settings/__snapshots__/SettingsModal.test.tsx.snap index 56d468e0e5e..0e3919756bb 100644 --- a/app/components/UI/Ramp/Aggregator/Views/Modals/Settings/__snapshots__/SettingsModal.test.tsx.snap +++ b/app/components/UI/Ramp/Aggregator/Views/Modals/Settings/__snapshots__/SettingsModal.test.tsx.snap @@ -20,351 +20,331 @@ exports[`SettingsModal renders snapshot correctly 1`] = ` } > - - - + + /> + + - + - SettingsModal - + + SettingsModal + + + - - - - - + - - - - - - - + /> + + - + - + + + - Settings - - - - - + Settings + + + + - - + > + + + - - - - - - - + + + - + - View order history - + + View order history + + - - - - + - - - - - + + + - More ways to buy - - + - Switch to the new version - + + More ways to buy + + + Switch to the new version + + - - + + @@ -736,9 +759,9 @@ exports[`SettingsModal renders snapshot correctly 1`] = ` - - - + + + `; diff --git a/app/components/UI/Ramp/Aggregator/Views/OrderDetails/__snapshots__/OrderDetails.test.tsx.snap b/app/components/UI/Ramp/Aggregator/Views/OrderDetails/__snapshots__/OrderDetails.test.tsx.snap index 86d02eae154..3126e0806fb 100644 --- a/app/components/UI/Ramp/Aggregator/Views/OrderDetails/__snapshots__/OrderDetails.test.tsx.snap +++ b/app/components/UI/Ramp/Aggregator/Views/OrderDetails/__snapshots__/OrderDetails.test.tsx.snap @@ -19,419 +19,451 @@ exports[`OrderDetails renders a cancelled order 1`] = ` ] } > - - - - + - - - - - + + + - + testID="button-icon" + > + + - - - - Order details - + + Order details + + + + + - - - - - - } - > - - - + + } + > + + - - + "padding": 15, + "paddingHorizontal": 16, + }, + undefined, + undefined, + ] + } + > + - + + + + + Order canceled + + + Something went wrong, and Test Provider was unable to complete your order. Please try again or with another provider. + + + - Order canceled + 0.01231 + + ETH - Something went wrong, and Test Provider was unable to complete your order. Please try again or with another provider. + ... + USD @@ -478,430 +513,337 @@ exports[`OrderDetails renders a cancelled order 1`] = ` } } > - - 0.01231 - - ETH - - - ... - USD - - - - - - - - + > + + + > + + - - - Account 1 - ( - 0xC4955...4D272 + style={ + [ + { + "color": "#131416", + "fontFamily": "Geist-Regular", + "fontSize": 30, + "marginVertical": 2, + }, + { + "textAlign": "center", + }, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + { + "color": "#131416", + }, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + { + "flexShrink": 1, + "marginHorizontal": 5, + "marginVertical": 3, + }, + ] + } + > + Account 1 + ( + + 0xC4955...4D272 + + ) - ) - + - - - - Order ID - - - - - + Order ID + + + + + + - - - - Date and time - - - - - + Date and time + + + + - Oct 13 at 8:07 pm - + + Oct 13 at 8:07 pm + + - - - - - - Test Provider - - - - - - Token amount - - - - - - 0.01231 - - ETH + Test Provider - - - Exchange rate + Token amount - ... + 0.01231 + + ETH + + + + + + + Exchange rate + + + + + + ... + + + + + + + + + + + USD + + Amount + + + + + + ... + + + + + + - USD - - Amount + Total fees - - + /> - - Total fees - - - - - + Purchase amount total + + + - ... - - - - - - - - - - - + - Purchase amount total - - - - - - ... - + > + ... + + - - + + - - - - + - - Start a new order - - + + Start a new order + + + - - - - + + + + - - - - + + + /> + `; @@ -1437,437 +1431,469 @@ exports[`OrderDetails renders a completed order 1`] = ` ] } > - - - - + - - - - - + + + - + testID="button-icon" + > + + - - - - Order details - + + Order details + + + + + - - - - - - } - > - - - + + } + > + + - - + "padding": 15, + "paddingHorizontal": 16, + }, + undefined, + undefined, + ] + } + > + - - ๏„ฌ - + + ๏„ฌ + + + + Order successful! + + + Your ETH is now available in your account + + + - Order successful! + 0.01231 + + ETH - Your ETH is now available in your account + ... + USD @@ -1914,303 +1943,337 @@ exports[`OrderDetails renders a completed order 1`] = ` } } > - - 0.01231 - - ETH - - - ... - USD - - - - - - + > + + + + + + + Account 1 + ( + + 0xC4955...4D272 + + ) + + + + + - + testID="listitemcolumn" + > + + Order ID + + + + + + + - - Account 1 - ( - - 0xC4955...4D272 - - ) - - - - - Order ID - - - - - + Date and time + + + + + + Oct 13 at 8:07 pm + + - - - Date and time - - - - - - Oct 13 at 8:07 pm + Test Provider - - - - Test Provider - - - - - - - + + + Token amount + + + + + + 0.01231 + + ETH + + + + + + + - - Token amount - + + + Exchange rate + + + + + + ... + + + - + + } + > - - 0.01231 - - ETH - + + + USD + + Amount + + + + + + ... + + + - - - Exchange rate + Total fees + - USD - - Amount + Purchase amount total - - - - - - Total fees - - - - - - ... - - - - - - - - - - - - Purchase amount total - - - - - - ... - - - - - - - + + - - - - + - - Start a new order - - + + Start a new order + + + - - - - + + + + - - - - + + + /> + `; @@ -2873,464 +2861,481 @@ exports[`OrderDetails renders a created order 1`] = ` ] } > - - - - + - - - - - + + + - + testID="button-icon" + > + + - - - - Order details - - - - - - - - - } - > - - - + + Order details + + + + + + + + + } + > + + - - + "padding": 15, + "paddingHorizontal": 16, + }, + undefined, + undefined, + ] + } + > + + + + ๓ฐฒ + + + + - ๓ฐฒ + Submitted @@ -3347,14 +3352,32 @@ exports[`OrderDetails renders a created order 1`] = ` { "color": "#131416", "fontFamily": "Geist-Medium", - "fontSize": 20, + "fontSize": 24, + "letterSpacing": 0, + "lineHeight": 32, + "textAlign": "center", + } + } + > + 0.01231 + + ETH + + - Submitted + ... + USD @@ -3365,430 +3388,337 @@ exports[`OrderDetails renders a created order 1`] = ` } } > - - 0.01231 - - ETH - - - ... - USD - - - - - - - - + > + + + > + + - - - Account 1 - ( - 0xC4955...4D272 + style={ + [ + { + "color": "#131416", + "fontFamily": "Geist-Regular", + "fontSize": 30, + "marginVertical": 2, + }, + { + "textAlign": "center", + }, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + { + "color": "#131416", + }, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + { + "flexShrink": 1, + "marginHorizontal": 5, + "marginVertical": 3, + }, + ] + } + > + Account 1 + ( + + 0xC4955...4D272 + + ) - ) - + - - - - Order ID - - - - - + Order ID + + + + + + - - - - Date and time - - - - + Date and time + + + - + - Oct 13 at 8:07 pm - + + Oct 13 at 8:07 pm + + - - - - - - Test Provider - - - - - - Token quantity sold - - - - - - 0.01231 - - ETH + Test Provider - - - Exchange rate + Token quantity sold - ... + 0.01231 + + ETH @@ -3960,310 +3851,401 @@ exports[`OrderDetails renders a created order 1`] = ` - - USD - - Value - - - + Exchange rate + + + + + + ... + + + + + + + + } + > - - ... - + + USD + + Value + + + + + + ... + + - - - - Total fees - - - - + Total fees + + + - + - ... - + + ... + + - - - + /> - - Amount received total - - - - + Amount received total + + + - + - ... - + + ... + + - - + + + + + - - - - - - - + + + + - - - - + + + /> + `; @@ -4287,419 +4269,451 @@ exports[`OrderDetails renders a failed order 1`] = ` ] } > - - - - + - - - - - + + + - + testID="button-icon" + > + + - - - - Order details - + + Order details + + + + + - - - - - - } - > - - - + + } + > + + - - + "padding": 15, + "paddingHorizontal": 16, + }, + undefined, + undefined, + ] + } + > + - + + + + + Order failed + + + Something went wrong, and Test Provider was unable to complete your order. Please try again or with another provider. + + + - Order failed + 0.01231 + + ETH - Something went wrong, and Test Provider was unable to complete your order. Please try again or with another provider. + ... + USD @@ -4746,357 +4763,360 @@ exports[`OrderDetails renders a failed order 1`] = ` } } > - - 0.01231 - - ETH - - - ... - USD - - - - - - - - + > + + + > + + - - - Account 1 - ( - 0xC4955...4D272 + style={ + [ + { + "color": "#131416", + "fontFamily": "Geist-Regular", + "fontSize": 30, + "marginVertical": 2, + }, + { + "textAlign": "center", + }, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + { + "color": "#131416", + }, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + { + "flexShrink": 1, + "marginHorizontal": 5, + "marginVertical": 3, + }, + ] + } + > + Account 1 + ( + + 0xC4955...4D272 + + ) - ) - + - - - - Order ID - - - - - + Order ID + + + + + + - - - - Date and time - - - + Date and time + + + + + + Oct 13 at 8:07 pm + + + + + + + + } + > - Oct 13 at 8:07 pm + Test Provider - - - - Test Provider - + + + Token amount + + + + + + 0.01231 + + ETH + + + - - - - Token amount - + + + Exchange rate + + + + + + ... + + + - + + } + > - - 0.01231 - - ETH - + + + USD + + Amount + + + + + + ... + + + - - - Exchange rate + Total fees + - USD - - Amount + Purchase amount total - - - - - - Total fees - - - - - - ... - - - - - - - - - - - - Purchase amount total - - - - - - ... - - - - - - - + + - - - - + - - Start a new order - - + + Start a new order + + + - - - - + + + + - - - - + + + /> + `; @@ -5705,464 +5681,481 @@ exports[`OrderDetails renders a pending order 1`] = ` ] } > - - - - + - - - - - + + + - + testID="button-icon" + > + + - - - - Order details - + + Order details + + + + + - - - - - - } - > - - - + + } + > + + - - + "padding": 15, + "paddingHorizontal": 16, + }, + undefined, + undefined, + ] + } + > + + + + ๓ฐฒ + + + + - ๓ฐฒ + Processing order @@ -6179,14 +6172,32 @@ exports[`OrderDetails renders a pending order 1`] = ` { "color": "#131416", "fontFamily": "Geist-Medium", - "fontSize": 20, + "fontSize": 24, + "letterSpacing": 0, + "lineHeight": 32, + "textAlign": "center", + } + } + > + 0.01231 + + ETH + + - Processing order + ... + USD @@ -6197,303 +6208,337 @@ exports[`OrderDetails renders a pending order 1`] = ` } } > - - 0.01231 - - ETH - - - ... - USD - - - - - - - - + > + + + > + + - - - Account 1 - ( - 0xC4955...4D272 + style={ + [ + { + "color": "#131416", + "fontFamily": "Geist-Regular", + "fontSize": 30, + "marginVertical": 2, + }, + { + "textAlign": "center", + }, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + { + "color": "#131416", + }, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + { + "flexShrink": 1, + "marginHorizontal": 5, + "marginVertical": 3, + }, + ] + } + > + Account 1 + ( + + 0xC4955...4D272 + + ) - ) - + - - - - Order ID - - - + Order ID + + + + + + + + + + + + } + > - + + Date and time + + + + + + Oct 13 at 8:07 pm + + - - - Date and time + Test Provider + + + + - - - Oct 13 at 8:07 pm - - - - - - - - - + Token amount + + + - Test Provider - + testID="listitem-gap" + /> + + + 0.01231 + + ETH + + + - - - - Token amount - + + + Exchange rate + + + + + + ... + + + - + + } + > - - 0.01231 - - ETH - + + + USD + + Amount + + + + + + ... + + + - - - Exchange rate + Total fees + - USD - - Amount + Purchase amount total - - - - - - Total fees - - - - - - ... - - - - - - - - - - - - Purchase amount total - - - - - - ... - - - - - - - + + + + + - - - - - - - + + + + - - - - + + + /> + `; @@ -7119,382 +7089,376 @@ exports[`OrderDetails renders an empty screen layout if there is no order 1`] = ] } > - - - - + - - - - - + + + - + testID="button-icon" + > + + - - - - Order details - - - - - - - - - - - - - - - - - - + Order details + + + + + + + + + + + + + + + + + + + /> + `; @@ -7518,570 +7482,564 @@ exports[`OrderDetails renders an error screen if a CREATED order cannot be polle ] } > - - - - + - - - - - + + + - + testID="button-icon" + > + + - - - - Order details - + + Order details + + + + + - - - - - - + - ๓ฐ…š - - - - + ๓ฐ…š + + + - Error - - - - + Error + + + - An error occurred - - - - + An error occurred + + + - - Try again - - + + Try again + + + - - + + - - - - + + + /> + `; @@ -8105,464 +8063,496 @@ exports[`OrderDetails renders non-transacted orders 1`] = ` ] } > - - - - + - - - - - + + + - + testID="button-icon" + > + + - - - - Order details - + + Order details + + - - - - - - - } - > - - - + + + + + + } + > + + - - + "padding": 15, + "paddingHorizontal": 16, + }, + undefined, + undefined, + ] + } + > + + + + ๓ฐฒ + + + + + Order pending + + - ๓ฐฒ + To continue your order, you'll need to select the button at the bottom of this page. @@ -8579,14 +8569,16 @@ exports[`OrderDetails renders non-transacted orders 1`] = ` { "color": "#131416", "fontFamily": "Geist-Medium", - "fontSize": 20, + "fontSize": 24, "letterSpacing": 0, - "lineHeight": 24, + "lineHeight": 32, "textAlign": "center", } } > - Order pending + 0.01231 + + ETH - To continue your order, you'll need to select the button at the bottom of this page. + ... + USD @@ -8612,430 +8605,337 @@ exports[`OrderDetails renders non-transacted orders 1`] = ` } } > - - 0.01231 - - ETH - - - ... - USD - - - - - - - - + > + + + > + + - - - Account 1 - ( - 0xC4955...4D272 + style={ + [ + { + "color": "#131416", + "fontFamily": "Geist-Regular", + "fontSize": 30, + "marginVertical": 2, + }, + { + "textAlign": "center", + }, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + { + "color": "#131416", + }, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + { + "flexShrink": 1, + "marginHorizontal": 5, + "marginVertical": 3, + }, + ] + } + > + Account 1 + ( + + 0xC4955...4D272 + + ) - ) - + - - - - Order ID - - - - - + Order ID + + + + + + - - - - Date and time - - - - + Date and time + + + - + - Oct 13 at 8:07 pm - + + Oct 13 at 8:07 pm + + - - - - - - Test Provider - - - - - - Token quantity sold - - - - - - 0.01231 - - ETH + Test Provider - - - Exchange rate + Token quantity sold - ... + 0.01231 + + ETH @@ -9207,360 +9068,451 @@ exports[`OrderDetails renders non-transacted orders 1`] = ` - - USD - - Value - - - - + Exchange rate + + + + + + ... + + + + + + + + - - ... - + + USD + + Value + + + + + + ... + + - - - - Total fees - - - - + Total fees + + + - + - ... - + + ... + + - - - + /> - - Amount received total - - - - + Amount received total + + + - + - ... - + + ... + + - - + + - - - + - - - Continue this order - - + + Continue this order + + + - - - - + + + + - - - - + + + /> + `; @@ -9584,458 +9536,492 @@ exports[`OrderDetails renders the support links if the provider has them 1`] = ` ] } > - - - - + - - - - - + + + - + testID="button-icon" + > + + - - - - Order details - + + Order details + + + + + - - - - - - } - > - - - + + } + > + + - - + "padding": 15, + "paddingHorizontal": 16, + }, + undefined, + undefined, + ] + } + > + - - ๏„ฌ - + ๏„ฌ + + + + Order successful! + + + Your ETH is now available in your account + + + + + - Order successful! + 0.01231 + + ETH - Your ETH is now available in your account + ... + USD - - - - 0.01231 - - ETH - - - ... - USD - + + + View order status on Test Provider + + + - - View order status on Test Provider - - - - - - - - - + > + + + > + + - - - Account 1 - ( - 0xC4955...4D272 + style={ + [ + { + "color": "#131416", + "fontFamily": "Geist-Regular", + "fontSize": 30, + "marginVertical": 2, + }, + { + "textAlign": "center", + }, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + { + "color": "#131416", + }, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + { + "flexShrink": 1, + "marginHorizontal": 5, + "marginVertical": 3, + }, + ] + } + > + Account 1 + ( + + 0xC4955...4D272 + + ) - ) - + - - - - Order ID - - - - - + Order ID + + + + + + - - - - Date and time - - - - - + Date and time + + + - Oct 13 at 8:07 pm - - - - - - - - - - Test Provider - - โ€ข - - + - Contact support - - + + Oct 13 at 8:07 pm + + + - - - Token amount - - - - - - 0.01231 + Test Provider + + โ€ข - ETH + + Contact support + - - - Exchange rate + Token amount - ... + 0.01231 + + ETH @@ -10702,347 +10557,438 @@ exports[`OrderDetails renders the support links if the provider has them 1`] = ` - - USD - - Amount - - - + Exchange rate + + + + testID="listitem-gap" + /> + + + ... + + + + + + + - - ... - + + USD + + Amount + + + + + + ... + + - - - - Total fees - - - - + Total fees + + + - + - ... - + + ... + + - - - + /> - - Purchase amount total - - - - + Purchase amount total + + + - + - ... - + + ... + + - - + + - - - - + - - Start a new order - - + + Start a new order + + + - - - - + + + + - - - - + + + /> + `; @@ -11066,465 +11012,482 @@ exports[`OrderDetails renders transacted orders that do not have timeDescription ] } > - - - - + - - - - - + + + - + testID="button-icon" + > + + - - - - Order details - + + Order details + + + + + - - - - - - } - > - - - + + } + > + + - - + "padding": 15, + "paddingHorizontal": 16, + }, + undefined, + undefined, + ] + } + > + - - ๓ฐฒ - + + ๓ฐฒ + + + + + + Submitted + - Submitted + 0.01231 + + ETH + + + ... + USD @@ -11558,430 +11539,337 @@ exports[`OrderDetails renders transacted orders that do not have timeDescription } } > - - 0.01231 - - ETH - - - ... - USD - - - - - - - - + > + + + > + + - - - Account 1 - ( - 0xC4955...4D272 + style={ + [ + { + "color": "#131416", + "fontFamily": "Geist-Regular", + "fontSize": 30, + "marginVertical": 2, + }, + { + "textAlign": "center", + }, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + { + "color": "#131416", + }, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + { + "flexShrink": 1, + "marginHorizontal": 5, + "marginVertical": 3, + }, + ] + } + > + Account 1 + ( + + 0xC4955...4D272 + + ) - ) - + - - - - Order ID - - - - - + Order ID + + + + + + - - - - Date and time - - - - - + Date and time + + + - Oct 13 at 8:07 pm - - - - - - - - - + - Test Provider - + testID="listitemcolumn" + > + + Oct 13 at 8:07 pm + + + - - - Token quantity sold - - - - - - 0.01231 - - ETH + Test Provider - - - Exchange rate + Token quantity sold - ... + 0.01231 + + ETH + + + + + + + Exchange rate + + + + + + ... + + + + + + + + + + + USD + + Value + + + + + + ... + + + + + + - USD - - Value + Total fees - - + /> - - Total fees - - - - - - ... - - - - - - - - - - - + Amount received total + + + - Amount received total - - - - - + - ... - + + ... + + - - + + + + + - - - - - - - + + + + - - - - + + + /> + `; @@ -12480,464 +12420,496 @@ exports[`OrderDetails renders transacted orders that have timeDescriptionPending ] } > - - - - + - - - - - + + + - + testID="button-icon" + > + + - - - - Order details - - - - - - - - - } - > - - - + + Order details + + + + + + + + + } + > + + - - + "padding": 15, + "paddingHorizontal": 16, + }, + undefined, + undefined, + ] + } + > + + + + ๓ฐฒ + + + + + Submitted + + - ๓ฐฒ + test-time-description @@ -12954,14 +12926,16 @@ exports[`OrderDetails renders transacted orders that have timeDescriptionPending { "color": "#131416", "fontFamily": "Geist-Medium", - "fontSize": 20, + "fontSize": 24, "letterSpacing": 0, - "lineHeight": 24, + "lineHeight": 32, "textAlign": "center", } } > - Submitted + 0.01231 + + ETH - test-time-description + ... + USD @@ -12987,303 +12962,337 @@ exports[`OrderDetails renders transacted orders that have timeDescriptionPending } } > - - 0.01231 - - ETH - - - ... - USD - - - - - - + > + + + + + + + Account 1 + ( + + 0xC4955...4D272 + + ) + + + + + + + Order ID + + + - - - - Account 1 - ( - - 0xC4955...4D272 - - ) - + + + + + - - - - Order ID - - - - - + Date and time + + + + + + Oct 13 at 8:07 pm + + - - - Date and time - - - - - - Oct 13 at 8:07 pm + Test Provider - - - - Test Provider - + + + Token quantity sold + + + + + + 0.01231 + + ETH + + + - - - + + + Exchange rate + + + + - Token quantity sold - + testID="listitemcolumn" + > + + ... + + + - + + } + > - - 0.01231 - - ETH - + + + USD + + Value + + + + + + ... + + + - - - Exchange rate + Total fees + - USD - - Value + Amount received total - - - - - - Total fees - - - - - - ... - - - - - - - - - - - - Amount received total - - - - - - ... - - - - - - - + + + + + - - - - - - - + + + + - - - - + + + /> + `; diff --git a/app/components/UI/Ramp/Aggregator/Views/Quotes/Quotes.test.tsx b/app/components/UI/Ramp/Aggregator/Views/Quotes/Quotes.test.tsx index e05fc7a982e..4c8de4b9c5b 100644 --- a/app/components/UI/Ramp/Aggregator/Views/Quotes/Quotes.test.tsx +++ b/app/components/UI/Ramp/Aggregator/Views/Quotes/Quotes.test.tsx @@ -70,7 +70,7 @@ jest.mock('@react-navigation/native', () => { reset: mockReset, pop: mockPop, isFocused: () => true, - dangerouslyGetParent: () => ({ + getParent: () => ({ pop: mockPop, }), }), diff --git a/app/components/UI/Ramp/Aggregator/Views/Quotes/__snapshots__/Quotes.test.tsx.snap b/app/components/UI/Ramp/Aggregator/Views/Quotes/__snapshots__/Quotes.test.tsx.snap index 3f0cf712fa2..1c550c31941 100644 --- a/app/components/UI/Ramp/Aggregator/Views/Quotes/__snapshots__/Quotes.test.tsx.snap +++ b/app/components/UI/Ramp/Aggregator/Views/Quotes/__snapshots__/Quotes.test.tsx.snap @@ -541,401 +541,190 @@ exports[`Quotes custom action renders correctly after animation with the recomme } > - - - - - - - - - - - - - Select a quote - - - - - - - - - - - - - + - - - - - - - + /> + + - + + + + - - Recommended quote - + + Recommended quote + + - - - + + - + testID="button-icon" + > + + - - - - New quotes in - - 0:07 - - - - - - - - - + + + + + + + - - + - + - ๏†‡ - - - - - - + + ๏†‡ + + + + + + /> - - - Continue with Paypal (Staging) - - + + Continue with Paypal (Staging) + + + - - + + + + + + + + Explore more options + + + + + + + + + + + + + + + - + - - Explore more options + Select a quote - + + + + - @@ -1514,9 +1504,23 @@ exports[`Quotes custom action renders correctly after animation with the recomme - - - + + + + `; @@ -1541,401 +1545,190 @@ exports[`Quotes renders animation on first fetching 1`] = ` } > - - - - - - - - - - - - - Select a quote - - - - - - - - - - - - - + - - - - - - - + /> + + - - Fetching quotes - + /> + + - - - + > + Fetching quotes + - + + + - - - - - + - - + - - + - - + - - + - - + - - + - - - - - - - - - - - - - + + + + + + + + + + + + + + @@ -2546,14 +2382,186 @@ exports[`Quotes renders animation on first fetching 1`] = ` - - - + + + + + + + + + + + + + + Select a quote + + + + + + + + + + + + - - - + + + + `; @@ -2578,620 +2586,440 @@ exports[`Quotes renders correctly after animation with expanded quotes 2`] = ` } > - - - - - - - - - - - - - Select a quote - - - - - - - - - - - - - + - - - - + > + + - - - + + + - - Select a quote - + + Select a quote + + - - - + + - + testID="button-icon" + > + + - - - - - New quotes in - - 0:07 + New quotes in + + + 0:07 + - - - - + - Compare rates from these providers. Quotes are sorted by overall price. - - - - - - - - - + Compare rates from these providers. Quotes are sorted by overall price. + + + + + + + + - - + - - Best rate - + + Best rate + + + + + + + ๏†‡ + + + - - + > + 0.01714 + + ETH + + + + - ๏†‡ + โ‰ˆ + $ + + 46.97 AUD - - - + - - 0.01714 - - ETH - - - - - - โ‰ˆ - $ - - 46.97 AUD - + > + + + Continue with Banxa (Staging) + + + + + + + + + + + + - + + Most reliable + + + - Continue with Banxa (Staging) + Best rate - + + + + + + ๏†‡ + + + - - - - - - - - - - - - - Most reliable + 0.0162 + + ETH + - Best rate + โ‰ˆ + $ + + 44.39 AUD - - - ๏†‡ - + + + Continue with MoonPay (Staging) + + + - + + + + + + + + + - - 0.0162 - - ETH - - - - - - โ‰ˆ - $ - - 44.39 AUD - - - - - - - - + - Continue with MoonPay (Staging) + ๏†‡ - - + + - - - - - - - - - - - - - - - - ๏†‡ + 0.01591 + + ETH - - - - - - 0.01591 - - ETH - - - - - + - โ‰ˆ - $ - - 43.59 AUD - + + โ‰ˆ + $ + + 43.59 AUD + + - - - - - Continue with Transak (Staging) - - + + Continue with Transak (Staging) + + + - - + + - - + + - - + + - - - - - - - - - - -`; - -exports[`Quotes renders correctly after animation with the recommended quote 1`] = ` - - - - - - - - - + + + + + + + + + + + + + + Select a quote + + + + + + + + + + + - - - - Select a quote - - - - - - - - - - - + + + + + +`; + +exports[`Quotes renders correctly after animation with the recommended quote 1`] = ` + + + + - - + - - - - + } + > - - - + /> + + - + + + + - - Recommended quote - + + Recommended quote + + - - - + + - + testID="button-icon" + > + + - - - - New quotes in - - 0:07 - - - - - - - + 0:07 + + + + + + - - + - - Most reliable - + + Most reliable + + + + + Best rate + + + + + + + ๏†‡ + + + + + - Best rate + 0.0162 + + ETH - - - + + - - ๏†‡ + โ‰ˆ + $ + + 44.39 AUD - - - - - - 0.0162 - - ETH - - - - โ‰ˆ - $ - - 44.39 AUD - - - - - - - - - Continue with MoonPay (Staging) - - + + Continue with MoonPay (Staging) + + + - - + + + + + + + + Explore more options + + + + + + + + + + + + + + + - + - - Explore more options + Select a quote - + + + + - @@ -5459,9 +5461,23 @@ exports[`Quotes renders correctly after animation with the recommended quote 1`] - - - + + + + `; @@ -5486,642 +5502,632 @@ exports[`Quotes renders correctly after animation without quotes 1`] = ` } > - - - - - - - - - + - - - Select a quote - - - - - - - - - - - - - - - - - - + > + + - - + > + + - + - ๓ฐ…š - - - - + ๓ฐ…š + + + - No providers available - - - - + No providers available + + + - Try choosing a different payment method or try to increase or reduce the amount you want to buy! - - - - + Try choosing a different payment method or try to increase or reduce the amount you want to buy! + + + - - Try again - - + + Try again + + + + + + + + + + + + + + + + + + + + Select a quote + + + + + + @@ -6130,9 +6136,23 @@ exports[`Quotes renders correctly after animation without quotes 1`] = ` - - - + + + + `; @@ -6157,642 +6177,632 @@ exports[`Quotes renders correctly when fetching quotes errors 1`] = ` } > - - - + + - - - - - - - - Select a quote - - - - - - - - - - - - - - - - - - - - + > + + - - + > + + - + + ๓ฐ…š + + + - ๓ฐ…š - - - - + Error + + + - Error - + + Test Error + + + + + + Try again + + + + + + + + + + + + + - - Test Error - + /> - + + + + - - - Try again - - - + Select a quote + + + + @@ -6801,9 +6811,23 @@ exports[`Quotes renders correctly when fetching quotes errors 1`] = ` - - - + + + + `; @@ -6828,642 +6852,632 @@ exports[`Quotes renders correctly with sdkError 1`] = ` } > - - - - - - - - - - - - - Select a quote - - - - - - - - - - - - - + - - - - + > + + - - + > + + - + + ๓ฐ…š + + + - ๓ฐ…š - - - - + Oops, something went wrong + + + - Oops, something went wrong - + + Example SDK Error + + + + + + Return to home screen + + + + + + + + + + + + + - - Example SDK Error - + /> - + + + + - - - Return to home screen - - - + Select a quote + + + + @@ -7472,9 +7486,23 @@ exports[`Quotes renders correctly with sdkError 1`] = ` - - - + + + + `; @@ -7499,642 +7527,632 @@ exports[`Quotes renders quotes expired screen 1`] = ` } > - - - - - - - - - - - - - Select a quote - - - - - - - - - - - - - + - - - - + > + + - - + > + + - + + ๓ฐ… + + + - ๓ฐ… - - - + Quotes timeout + + + - + + Please request new quotes to get the latest best rate. + + + - Quotes timeout - + + + Get new quotes + + + + + + + + + + + + + - - Please request new quotes to get the latest best rate. - + /> - + + + + - - - Get new quotes - - - + Select a quote + + + + @@ -8143,9 +8161,23 @@ exports[`Quotes renders quotes expired screen 1`] = ` - - - + + + + `; diff --git a/app/components/UI/Ramp/Aggregator/Views/SendTransaction/SendTransaction.test.tsx b/app/components/UI/Ramp/Aggregator/Views/SendTransaction/SendTransaction.test.tsx index 18f22215d4d..20ddf5c4695 100644 --- a/app/components/UI/Ramp/Aggregator/Views/SendTransaction/SendTransaction.test.tsx +++ b/app/components/UI/Ramp/Aggregator/Views/SendTransaction/SendTransaction.test.tsx @@ -292,7 +292,7 @@ jest.mock('@react-navigation/native', () => { ), goBack: mockGoBack, reset: mockReset, - dangerouslyGetParent: () => ({ + getParent: () => ({ pop: mockPop, }), }), diff --git a/app/components/UI/Ramp/Aggregator/Views/SendTransaction/__snapshots__/SendTransaction.test.tsx.snap b/app/components/UI/Ramp/Aggregator/Views/SendTransaction/__snapshots__/SendTransaction.test.tsx.snap index 173b8ec31d5..6d11f54690a 100644 --- a/app/components/UI/Ramp/Aggregator/Views/SendTransaction/__snapshots__/SendTransaction.test.tsx.snap +++ b/app/components/UI/Ramp/Aggregator/Views/SendTransaction/__snapshots__/SendTransaction.test.tsx.snap @@ -20,378 +20,199 @@ exports[`SendTransaction View renders correctly 1`] = ` } > - - - - - - - - - - - - - Sell crypto - - - - - - - - - - - - - + - - - - - Send - - + Send + + + 0.012361263 + + + + + + ETH + + + + + - 0.012361263 - + width={24} + /> + + + + + + + + - + ๓ฐฐ + + + > + Instant Bank Transfer + - - ETH - + + + + - - - - - - - + > + You will send your ETH to Test (Staging), who then sends your cash to Instant Bank Transfer + - - - ๓ฐฐ - - Instant Bank Transfer + Next - + - + + + + + + + + + + + + @@ -669,94 +704,63 @@ exports[`SendTransaction View renders correctly 1`] = ` style={ [ { - "marginVertical": 8, - }, - { - "marginTop": 0, + "alignItems": "center", + "display": "flex", }, undefined, - undefined, ] } > - You will send your ETH to Test (Staging), who then sends your cash to Instant Bank Transfer + Sell crypto + + - - - Next - - - + onLayout={[Function]} + /> - + - - - + + + + `; @@ -781,378 +785,199 @@ exports[`SendTransaction View renders correctly for custom action payment method } > - - - - - - - - - - - - - Sell crypto - - - - - - - - - - - - - + /> + - - - - - Send - - - 0.0123456 - - + Send + + - + 0.0123456 + + + + + + > + USDC + - + + - USDC - + width={24} + /> + + + + + + + - + > + You will send your USDC to Test (Staging), who then sends your cash to Instant Bank Transfer + - + + Next + + + + + + + + + + + + + + + - - @@ -1348,94 +1387,63 @@ exports[`SendTransaction View renders correctly for custom action payment method style={ [ { - "marginVertical": 8, - }, - { - "marginTop": 0, + "alignItems": "center", + "display": "flex", }, undefined, - undefined, ] } > - You will send your USDC to Test (Staging), who then sends your cash to Instant Bank Transfer + Sell crypto + + - - - Next - - - + onLayout={[Function]} + /> - + - - - + + + + `; @@ -1460,378 +1468,199 @@ exports[`SendTransaction View renders correctly for token 1`] = ` } > - - - - - - - - - - - - - Sell crypto - - - - - - - - - - - - - + - - - - - Send - - - 0.0123456 - - + Send + + - + 0.0123456 + + + + + + > + USDC + - + + - USDC - + width={24} + /> - - - - - - + + + + + + + > + + + ๓ฐฐ + + + Instant Bank Transfer + + + + + + + - + > + You will send your USDC to Test (Staging), who then sends your cash to Instant Bank Transfer + - - - ๓ฐฐ - - Instant Bank Transfer + Next - + - + + + + + + + + + + + + @@ -2113,94 +2156,63 @@ exports[`SendTransaction View renders correctly for token 1`] = ` style={ [ { - "marginVertical": 8, - }, - { - "marginTop": 0, + "alignItems": "center", + "display": "flex", }, undefined, - undefined, ] } > - You will send your USDC to Test (Staging), who then sends your cash to Instant Bank Transfer + Sell crypto + + - - - Next - - - + onLayout={[Function]} + /> - + - - - + + + + `; diff --git a/app/components/UI/Ramp/Aggregator/Views/Settings/__snapshots__/ActivationKeyForm.test.tsx.snap b/app/components/UI/Ramp/Aggregator/Views/Settings/__snapshots__/ActivationKeyForm.test.tsx.snap index e62fc12c378..90ff8d53c61 100644 --- a/app/components/UI/Ramp/Aggregator/Views/Settings/__snapshots__/ActivationKeyForm.test.tsx.snap +++ b/app/components/UI/Ramp/Aggregator/Views/Settings/__snapshots__/ActivationKeyForm.test.tsx.snap @@ -20,485 +20,497 @@ exports[`AddActivationKey renders correctly 1`] = ` } > - - - + + /> + + - + - RampActivationKeyForm - + + RampActivationKeyForm + + + - - - - - + - - - - - + + + - + testID="activation-key-form-back-button" + > + + - - - - Add activation key - + + Add activation key + + + + + - - - - - - @@ -521,341 +531,354 @@ exports[`AddActivationKey renders correctly 1`] = ` style={ [ { - "marginVertical": 8, + "padding": 15, + "paddingHorizontal": 16, }, undefined, undefined, - undefined, ] } > - - Label - - + Label + + - - + > + + - - - - Key - - + Key + + - - + > + + - - - - - Cancel - - - - + Cancel + + + - Add - - + + Add + + + - + - + - - - + + + `; diff --git a/app/components/UI/Ramp/Aggregator/Views/Settings/__snapshots__/Settings.test.tsx.snap b/app/components/UI/Ramp/Aggregator/Views/Settings/__snapshots__/Settings.test.tsx.snap index 1a4b6771b55..edbd4e8d720 100644 --- a/app/components/UI/Ramp/Aggregator/Views/Settings/__snapshots__/Settings.test.tsx.snap +++ b/app/components/UI/Ramp/Aggregator/Views/Settings/__snapshots__/Settings.test.tsx.snap @@ -20,514 +20,524 @@ exports[`Settings Activation Keys renders correctly when is loading 1`] = ` } > - - - + + /> + + - + - RampSettings - + + RampSettings + + + - - - - - + - - - - - + + + + + + + + + - + > + Buy & sell crypto + + + + - - - Buy & sell crypto - - - - - - - - - - - - + @@ -535,197 +545,153 @@ exports[`Settings Activation Keys renders correctly when is loading 1`] = ` style={ [ { - "marginVertical": 8, - }, - { - "marginTop": 0, + "padding": 15, + "paddingHorizontal": 16, }, undefined, undefined, ] } > - - Current region - + + Current region + - - ๐Ÿ‡ช๐Ÿ‡บ - - - - + ๐Ÿ‡ช๐Ÿ‡บ + + + - + - Europe Union - + + Europe Union + + - - - - Change region - - - - - - - SDK activation keys - - - - - - + > + Change region + + + - Activation keys will enable specific features or providers. - - - - - - + + + - - + + + + > + Activation keys will enable specific features or providers. + + + - - test key 1 - - + + - testKey1 - - - - + - + + test key 1 + + + testKey1 + + + + - - - - - - + + + + + - - + > + + + - - - - - - - + + + - test key 2 - - + - testKey2 - - - - - + test key 2 + + + testKey2 + + + + - - - - - - + + + + + - - + > + + + - - - - - Add activation key - - + + Add activation key + + + - - - - - + + + + + - - - + + + `; @@ -1284,514 +1307,524 @@ exports[`Settings Activation Keys renders correctly when there are no keys 1`] = } > - - - + } + > + > + + + - + - RampSettings - + + RampSettings + + + - - - - - + - - - - - + + + + + + + + + - + > + Buy & sell crypto + + + + - - - Buy & sell crypto - - - - - - - - - - - - + @@ -1799,194 +1832,153 @@ exports[`Settings Activation Keys renders correctly when there are no keys 1`] = style={ [ { - "marginVertical": 8, - }, - { - "marginTop": 0, + "padding": 15, + "paddingHorizontal": 16, }, undefined, undefined, ] } > - - Current region - - + > + Current region + - - ๐Ÿ‡ช๐Ÿ‡บ - - - - + ๐Ÿ‡ช๐Ÿ‡บ + + + - + - Europe Union - + + Europe Union + + - - - - Change region - - - - - - - SDK activation keys - - - - - + > + Change region + + + - Activation keys will enable specific features or providers. + + SDK activation keys + + + + - - - - Add activation key + Activation keys will enable specific features or providers. - + + + + + Add activation key + + + - - - - - + + + + + - - - + + + `; @@ -2106,514 +2152,524 @@ exports[`Settings Region V2 disabled (Original) renders correctly when region is } > - - - + style={ + { + "zIndex": 1, + } + } + > + > + + + - + - RampSettings - + + RampSettings + + + - - - - - + - - - - - + + + + + + + + + - + > + Buy & sell crypto + + + + - - - Buy & sell crypto - - - - - - - - - - - - + @@ -2621,120 +2677,133 @@ exports[`Settings Region V2 disabled (Original) renders correctly when region is style={ [ { - "marginVertical": 8, - }, - { - "marginTop": 0, + "padding": 15, + "paddingHorizontal": 16, }, undefined, undefined, ] } > - - Current region - + + Current region + - - ๐Ÿณ๏ธ - - - - + ๐Ÿณ๏ธ + + + - + - No region selected - + + No region selected + + - - - - - + + + + + - - - + + + `; @@ -2759,514 +2828,524 @@ exports[`Settings Region V2 disabled (Original) renders correctly when region is } > - - - + + /> + + - + - RampSettings - + + RampSettings + + + - - - - - + - - - - + + + + + + + + - + > + Buy & sell crypto + + + + - - - Buy & sell crypto - - - - - - - - - - - - + @@ -3274,158 +3353,171 @@ exports[`Settings Region V2 disabled (Original) renders correctly when region is style={ [ { - "marginVertical": 8, - }, - { - "marginTop": 0, + "padding": 15, + "paddingHorizontal": 16, }, undefined, undefined, ] } > - - Current region - + + Current region + - - ๐Ÿ‡ช๐Ÿ‡บ - - - - + ๐Ÿ‡ช๐Ÿ‡บ + + + - + - Europe Union - + + Europe Union + + - - - - Reset region - - + + Reset region + + + - - - - - + + + + + - - - + + + `; @@ -3450,514 +3542,524 @@ exports[`Settings Region V2 enabled renders correctly when region has state 1`] } > - - - + + /> + + - + - RampSettings - + + RampSettings + + + - - - - - + - - - - - + + + + + + + + + - + > + Buy & sell crypto + + + + - - - Buy & sell crypto - - - - - - - - - - - - + @@ -3965,156 +4067,169 @@ exports[`Settings Region V2 enabled renders correctly when region has state 1`] style={ [ { - "marginVertical": 8, - }, - { - "marginTop": 0, + "padding": 15, + "paddingHorizontal": 16, }, undefined, undefined, ] } > - - Current region - + + Current region + - - ๐Ÿ‡ช๐Ÿ‡บ - - - - + ๐Ÿ‡ช๐Ÿ‡บ + + + - + - FR - + + FR + + - - - - Change region - - + + Change region + + + - - - - - + + + + + - - - + + + `; @@ -4139,514 +4254,524 @@ exports[`Settings Region V2 enabled renders correctly when region is country onl } > - - - + + /> + + - + - RampSettings - + + RampSettings + + + - - - - - + - - - - + + + + + + + + - + > + Buy & sell crypto + + + + - - - Buy & sell crypto - - - - - - - - - - - - + @@ -4654,156 +4779,169 @@ exports[`Settings Region V2 enabled renders correctly when region is country onl style={ [ { - "marginVertical": 8, - }, - { - "marginTop": 0, + "padding": 15, + "paddingHorizontal": 16, }, undefined, undefined, ] } > - - Current region - + + Current region + - - ๐Ÿ‡ช๐Ÿ‡บ - - - - + ๐Ÿ‡ช๐Ÿ‡บ + + + - + - Europe Union - + + Europe Union + + - - - - Change region - - + + Change region + + + - - - - - + + + + + - - - + + + `; @@ -4828,514 +4966,524 @@ exports[`Settings Region V2 enabled renders correctly when region is not set 1`] } > - - - + + /> + + + - + - RampSettings - + + RampSettings + + + - - - - - + - - - - - + + + + + + + + + - + > + Buy & sell crypto + + + + - - - Buy & sell crypto - - - - - - - - - - - - + @@ -5343,156 +5491,169 @@ exports[`Settings Region V2 enabled renders correctly when region is not set 1`] style={ [ { - "marginVertical": 8, - }, - { - "marginTop": 0, + "padding": 15, + "paddingHorizontal": 16, }, undefined, undefined, ] } > - - Current region - + + Current region + - - ๐Ÿณ๏ธ - - - - + ๐Ÿณ๏ธ + + + - + - No region selected - + + No region selected + + - - - - Change region - - + + Change region + + + - - - - - + + + + + - - - + + + `; @@ -5517,514 +5678,524 @@ exports[`Settings Region V2 enabled renders correctly when region is set 1`] = ` } > - - - + + /> + + - + - RampSettings - + + RampSettings + + + - - - - - + - - - - + + + + + + + + - + > + Buy & sell crypto + + + + - - - Buy & sell crypto - - - - - - - - - - - - + @@ -6032,156 +6203,169 @@ exports[`Settings Region V2 enabled renders correctly when region is set 1`] = ` style={ [ { - "marginVertical": 8, - }, - { - "marginTop": 0, + "padding": 15, + "paddingHorizontal": 16, }, undefined, undefined, ] } > - - Current region - + + Current region + - - ๐Ÿ‡ช๐Ÿ‡บ - - - - + ๐Ÿ‡ช๐Ÿ‡บ + + + - + - Europe Union - + + Europe Union + + - - - - Change region - - + + Change region + + + - - - - - + + + + + - - - + + + `; @@ -6206,514 +6390,524 @@ exports[`Settings renders correctly 1`] = ` } > - - - + + /> + + + - + - RampSettings - + + RampSettings + + + - - - - - + - - - - - + + + + + + + + + - + > + Buy & sell crypto + + + + - - - Buy & sell crypto - - - - - - - - - - - - + @@ -6721,156 +6915,169 @@ exports[`Settings renders correctly 1`] = ` style={ [ { - "marginVertical": 8, - }, - { - "marginTop": 0, + "padding": 15, + "paddingHorizontal": 16, }, undefined, undefined, ] } > - - Current region - + + Current region + - - ๐Ÿ‡ช๐Ÿ‡บ - - - - + ๐Ÿ‡ช๐Ÿ‡บ + + + - + - Europe Union - + + Europe Union + + - - - - Change region - - + + Change region + + + - - - - - + + + + + - - - + + + `; @@ -6895,514 +7102,524 @@ exports[`Settings renders correctly for internal builds 1`] = ` } > - - - + + /> + + - + - RampSettings - + + RampSettings + + + - - - - - + - - - - + + + + + + + + - + > + Buy & sell crypto + + + + - - - Buy & sell crypto - - - - - - - - - - - - + @@ -7410,194 +7627,153 @@ exports[`Settings renders correctly for internal builds 1`] = ` style={ [ { - "marginVertical": 8, - }, - { - "marginTop": 0, + "padding": 15, + "paddingHorizontal": 16, }, undefined, undefined, ] } > - - Current region - + + Current region + - - ๐Ÿ‡ช๐Ÿ‡บ - - - - + ๐Ÿ‡ช๐Ÿ‡บ + + + - + - Europe Union - + + Europe Union + + - - - - Change region - - - - - - - SDK activation keys - - - - - + > + Change region + + + - Activation keys will enable specific features or providers. + + SDK activation keys + + + + - - - - - - + + + } + > - - test key 1 - - + + - testKey1 - - - - + - + + test key 1 + + + testKey1 + + + + - - - - - - + + + + + - - + > + + + - - - - - - - + + + - test key 2 - - + - testKey2 - - - - - + test key 2 + + + testKey2 + + + + - - - - - - + + + + + - - + > + + + - - - - - Add activation key - - + + Add activation key + + + - - - - - + + + + + - - - + + + `; diff --git a/app/components/UI/Ramp/Aggregator/components/ErrorViewWithReporting.tsx b/app/components/UI/Ramp/Aggregator/components/ErrorViewWithReporting.tsx index f90104ee37a..a73554ec9c2 100644 --- a/app/components/UI/Ramp/Aggregator/components/ErrorViewWithReporting.tsx +++ b/app/components/UI/Ramp/Aggregator/components/ErrorViewWithReporting.tsx @@ -31,7 +31,7 @@ function ErrorViewWithReporting({ ctaOnPress={() => { //TODO: implement a mechanisim for user to submit a support ticket // @ts-expect-error navigation prop mismatch - navigation.dangerouslyGetParent()?.pop(); + navigation.getParent()?.pop(); }} location={location} asScreen={asScreen} diff --git a/app/components/UI/Ramp/Aggregator/components/FiatSelectorModal/__snapshots__/FiatSelectorModal.test.tsx.snap b/app/components/UI/Ramp/Aggregator/components/FiatSelectorModal/__snapshots__/FiatSelectorModal.test.tsx.snap index 3babdfca1f8..27c0235ed20 100644 --- a/app/components/UI/Ramp/Aggregator/components/FiatSelectorModal/__snapshots__/FiatSelectorModal.test.tsx.snap +++ b/app/components/UI/Ramp/Aggregator/components/FiatSelectorModal/__snapshots__/FiatSelectorModal.test.tsx.snap @@ -20,351 +20,331 @@ exports[`FiatSelectorModal renders the modal with currency list 1`] = ` } > - - - + + /> + + - + - RampFiatSelectorModal - + + RampFiatSelectorModal + + + - - - - - + - - - - - - - + /> + + - + + + + - - Select region currency - - - - - - + > + Select region currency + - - - + + + + + + + + - - - - - - - - - - - + + + - + + + + + + + - - US Dollar - - - USD - + > + US Dollar + + + USD + + - - - - - + + - - - Euro - - - EUR - + > + Euro + + + EUR + + - - - - - + + - - - British Pound - - - GBP - + > + British Pound + + + GBP + + - - + + - - + + @@ -982,9 +1005,9 @@ exports[`FiatSelectorModal renders the modal with currency list 1`] = ` - - - + + + `; @@ -1009,351 +1032,331 @@ exports[`FiatSelectorModal search displays filtered currencies when search strin } > - - - + + /> + + - + - RampFiatSelectorModal - + + RampFiatSelectorModal + + + - - - - - + - - - - - - - + /> + + - + + + + - - Select region currency - + + Select region currency + + - - - + + - + testID="button-icon" + > + + - - - - - - - - - - - - - + + + - + + + + + + + - - US Dollar - - - USD - + > + US Dollar + + + USD + + - - - - - + + - - - Euro - - - EUR - + > + Euro + + + EUR + + - - - - - + + - - - British Pound - - - GBP - + > + British Pound + + + GBP + + - - + + - - + + @@ -1971,9 +2017,9 @@ exports[`FiatSelectorModal search displays filtered currencies when search strin - - - + + + `; @@ -1998,351 +2044,331 @@ exports[`FiatSelectorModal search displays filtered currencies when search strin } > - - - + + /> + + - + + + RampFiatSelectorModal + + + - RampFiatSelectorModal - + /> - - - - - + - - - - - - - + /> + + - + + + + - - Select region currency - + + Select region currency + + - - - + + - + testID="button-icon" + > + + - - - - - - - - - - - - - + + + - + + + + + + + - - US Dollar - - - USD - + > + US Dollar + + + USD + + - - - - - + + - - - Euro - - - EUR - + > + Euro + + + EUR + + - - - - - + + - - - British Pound - - - GBP - + > + British Pound + + + GBP + + - - + + - - + + @@ -2960,9 +3029,9 @@ exports[`FiatSelectorModal search displays filtered currencies when search strin - - - + + + `; @@ -2987,351 +3056,331 @@ exports[`FiatSelectorModal search displays max 20 results 1`] = ` } > - - - + + /> + + - + - RampFiatSelectorModal - + + RampFiatSelectorModal + + + - - - - - + - - - - + } + > - - - + /> + + - + + + + - - Select region currency - + + Select region currency + + - - - + + - + testID="button-icon" + > + + - - - - - - - - - - - - - + + + - + + + + + + + - - US Dollar - - - USD - + > + US Dollar + + + USD + + - - - - - + + - - - Euro - - - EUR - + > + Euro + + + EUR + + - - - - - + + - - - British Pound - - - GBP - + > + British Pound + + + GBP + + - - + + - - + + @@ -3949,9 +4041,9 @@ exports[`FiatSelectorModal search displays max 20 results 1`] = ` - - - + + + `; diff --git a/app/components/UI/Ramp/Aggregator/components/IncompatibleAccountTokenModal/__snapshots__/IncompatibleAccountTokenModal.test.tsx.snap b/app/components/UI/Ramp/Aggregator/components/IncompatibleAccountTokenModal/__snapshots__/IncompatibleAccountTokenModal.test.tsx.snap index d5e5b5ebd87..5e3aaab99b8 100644 --- a/app/components/UI/Ramp/Aggregator/components/IncompatibleAccountTokenModal/__snapshots__/IncompatibleAccountTokenModal.test.tsx.snap +++ b/app/components/UI/Ramp/Aggregator/components/IncompatibleAccountTokenModal/__snapshots__/IncompatibleAccountTokenModal.test.tsx.snap @@ -20,351 +20,331 @@ exports[`IncompatibleAccountTokenModal renders the modal with the correct title } > - - - + + /> + + - + - RampIncompatibleAccountTokenModal - + + RampIncompatibleAccountTokenModal + + + - - - - - + - - - - - - - + /> + + - + - + + + - Incompatible account - - - - - - + + + + - + > + + + - - - - Your current account cannot support tokens on this network. Please switch to a compatible account to continue. - - - Got it + Your current account cannot support tokens on this network. Please switch to a compatible account to continue. - + + + Got it + + + @@ -587,9 +610,9 @@ exports[`IncompatibleAccountTokenModal renders the modal with the correct title - - - + + + `; diff --git a/app/components/UI/Ramp/Aggregator/components/PaymentMethodSelectorModal/__snapshots__/PaymentMethodSelectorModal.test.tsx.snap b/app/components/UI/Ramp/Aggregator/components/PaymentMethodSelectorModal/__snapshots__/PaymentMethodSelectorModal.test.tsx.snap index 28d9429380b..1b6fa7b4688 100644 --- a/app/components/UI/Ramp/Aggregator/components/PaymentMethodSelectorModal/__snapshots__/PaymentMethodSelectorModal.test.tsx.snap +++ b/app/components/UI/Ramp/Aggregator/components/PaymentMethodSelectorModal/__snapshots__/PaymentMethodSelectorModal.test.tsx.snap @@ -20,351 +20,331 @@ exports[`PaymentMethodSelectorModal renders correctly 1`] = ` } > - - - + + /> + + - + - RampPaymentMethodSelectorModal - + + RampPaymentMethodSelectorModal + + + - - - - - + - - - - - - - + /> + + - + + + + - - Select payment method - - - - - - + > + Select payment method + - - - - - + - - + + + + + + + + + - - ๎กฐ - + + ๎กฐ + + - - - - - Credit Card - - + - Test detail - - - - + + Credit Card + + + Test detail + + + + + - - - - - ๏„น - - - Instant - โ€ข - + /> - $ - - - $ - - + ๏„น + + + Instant + โ€ข + + - $ + > + $ + + + $ + + + $ + + + lowest buy limit - - lowest buy limit - + - - - - + + - - - - ๓ฐฐ - + + ๓ฐฐ + + - - - - - Bank Transfer - - + - Test detail - - - - + + Bank Transfer + + + Test detail + + + + + - - - - - ๏„น - - - 1 - 3 mins - โ€ข - + /> - $ - - - $ - - + ๏„น + + + 1 - 3 mins + โ€ข + + - $ + > + $ + + + $ + + + $ + + + lowest buy limit - - lowest buy limit - + - - - - - + + - Test disclaimer - + + Test disclaimer + + - - + + @@ -1191,9 +1214,9 @@ exports[`PaymentMethodSelectorModal renders correctly 1`] = ` - - - + + + `; @@ -1218,351 +1241,331 @@ exports[`PaymentMethodSelectorModal renders for sell flow 1`] = ` } > - - - + + /> + + - + - RampPaymentMethodSelectorModal - + + RampPaymentMethodSelectorModal + + + - - - - - + - - - - - - - + /> + + - + + + + - - Select where to send your cash - + + Select where to send your cash + + - - - + + - + testID="button-icon" + > + + - - - - + } + > + - - + - - ๎กฐ - + + ๎กฐ + + - - - - - Credit Card - - + - Test detail - - - - + + Credit Card + + + Test detail + + + + + - - - - - ๏„น - - - Instant - โ€ข - + /> - $ - - - $ - - + ๏„น + + + Instant + โ€ข + + - $ + > + $ + + + $ + + + $ + + + lowest sell limit - - lowest sell limit - + - - - - + + - - - - ๓ฐฐ - - - - - - + + ๓ฐฐ + + + + - Bank Transfer - - + - Test detail - - - - + + Bank Transfer + + + Test detail + + + + + - - - - - ๏„น - - - 1 - 3 mins - โ€ข - + /> - $ - - - $ - - + ๏„น + + + 1 - 3 mins + โ€ข + + - $ + > + $ + + + $ + + + $ + + + lowest sell limit - - lowest sell limit - + - - - - - + + - Test disclaimer - + + Test disclaimer + + - - + + @@ -2389,9 +2435,9 @@ exports[`PaymentMethodSelectorModal renders for sell flow 1`] = ` - - - + + + `; @@ -2416,351 +2462,331 @@ exports[`PaymentMethodSelectorModal renders without disclaimer when selected pay } > - - - + + /> + + - + - RampPaymentMethodSelectorModal - + + RampPaymentMethodSelectorModal + + + - - - - - + - - - - - - - + /> + + - + + + + - - Select payment method - - - - - - + > + Select payment method + - - - - - + - - + + + + + + + + + - - ๎กฐ - + + ๎กฐ + + - - - - - Credit Card - - + - Test detail - - - - + Credit Card + + + Test detail + + + + testID="listitem-gap" + /> + testID="listitemcolumn" + > + + - - - - - ๏„น - - - Instant - โ€ข - + /> - $ - - - $ - - + ๏„น + + + Instant + โ€ข + + - $ + > + $ + + + $ + + + $ + + + lowest buy limit - - lowest buy limit - + - - - - + + - - - - ๓ฐฐ - + + ๓ฐฐ + + - - - - - Bank Transfer - - + - Test detail - - - - + + Bank Transfer + + + Test detail + + + + + - - - - - ๏„น - - - 1 - 3 mins - โ€ข - + /> - $ - - - $ - - + ๏„น + + + 1 - 3 mins + โ€ข + + - $ + > + $ + + + $ + + + $ + + + lowest buy limit - - lowest buy limit - + - - + + - - + + @@ -3563,9 +3632,9 @@ exports[`PaymentMethodSelectorModal renders without disclaimer when selected pay - - - + + + `; diff --git a/app/components/UI/Ramp/Aggregator/components/TokenSelectModal/__snapshots__/TokenSelectModal.test.tsx.snap b/app/components/UI/Ramp/Aggregator/components/TokenSelectModal/__snapshots__/TokenSelectModal.test.tsx.snap index 7ea1718fdb8..76584ecb02e 100644 --- a/app/components/UI/Ramp/Aggregator/components/TokenSelectModal/__snapshots__/TokenSelectModal.test.tsx.snap +++ b/app/components/UI/Ramp/Aggregator/components/TokenSelectModal/__snapshots__/TokenSelectModal.test.tsx.snap @@ -20,351 +20,331 @@ exports[`TokenSelectModal renders the modal with token list 1`] = ` } > - - - + + /> + + - + - RampTokenSelectorModal - + + RampTokenSelectorModal + + + - - - - - + - - - - - - - + /> + + - + + + + - - Select a cryptocurrency - - - - - - - + > + Select a cryptocurrency + + + + + + + + + - - - - - + + - - - - + + + - - - + + + + > + + - - - All networks - - + All networks + + - - - - - + width={12} + /> + + + + - - - - + testID="textfield-startacccessory" + > + + + + + - - - - - + + - - - - + + + > + + - - - + > + + - - - - - ETH - - + - Ethereum - + + ETH + + + Ethereum + + - - - - - + + - - - - + + + > + + - - - + > + + - - - - - POL - - + - Polygon Token - + + POL + + + Polygon Token + + - - - - - + + - - - - + + + > + + - - - + > + + - - - - - SOL - - + - Solana - + + SOL + + + Solana + + - - - - - + + - - - - + + + > + + - - - + > + + - - - - - USDC - - + - USD Coin (Solana) - + + USDC + + + USD Coin (Solana) + + - - + + - - + + @@ -1844,9 +1867,9 @@ exports[`TokenSelectModal renders the modal with token list 1`] = ` - - - + + + `; diff --git a/app/components/UI/Ramp/Aggregator/components/UnsupportedRegionModal/__snapshots__/UnsupportedRegionModal.test.tsx.snap b/app/components/UI/Ramp/Aggregator/components/UnsupportedRegionModal/__snapshots__/UnsupportedRegionModal.test.tsx.snap index 39a7dd84f8a..ab71d071974 100644 --- a/app/components/UI/Ramp/Aggregator/components/UnsupportedRegionModal/__snapshots__/UnsupportedRegionModal.test.tsx.snap +++ b/app/components/UI/Ramp/Aggregator/components/UnsupportedRegionModal/__snapshots__/UnsupportedRegionModal.test.tsx.snap @@ -20,469 +20,468 @@ exports[`UnsupportedRegionModal renders correctly for buy flow 1`] = ` } > - - - + + /> + + - + - RampUnsupportedRegionModal - + + RampUnsupportedRegionModal + + + - - - - - + - - - - + > + + - - Region not supported - + + Region not supported + + - - - - We are working hard to expand coverage to your region as soon as we can. In the meantime, see our support article for other ways you may be able to buy crypto. - @@ -491,95 +490,119 @@ exports[`UnsupportedRegionModal renders correctly for buy flow 1`] = ` style={ { "color": "#131416", - "fontFamily": "Geist-Medium", - "fontSize": 20, + "fontFamily": "Geist-Regular", + "fontSize": 16, "letterSpacing": 0, "lineHeight": 24, } } > - ๐Ÿ‡ฆ๐Ÿ‡ซ + We are working hard to expand coverage to your region as soon as we can. In the meantime, see our support article for other ways you may be able to buy crypto. - - Afghanistan - - - - + ๐Ÿ‡ฆ๐Ÿ‡ซ + + + Afghanistan + + + - Visit support article - - - - - + Visit support article + + + + - - Select your region - - + + Select your region + + + @@ -589,9 +612,9 @@ exports[`UnsupportedRegionModal renders correctly for buy flow 1`] = ` - - - + + + `; @@ -616,469 +639,468 @@ exports[`UnsupportedRegionModal renders correctly for sell flow 1`] = ` } > - - - + + /> + + - + - RampUnsupportedRegionModal - + + RampUnsupportedRegionModal + + + - - - - - + - - - - + > + + - - Region not supported - + + Region not supported + + - - - - We are working hard to expand coverage to your region as soon as we can. In the meantime, see our support article for other ways you may be able to sell crypto. - @@ -1087,95 +1109,119 @@ exports[`UnsupportedRegionModal renders correctly for sell flow 1`] = ` style={ { "color": "#131416", - "fontFamily": "Geist-Medium", - "fontSize": 20, + "fontFamily": "Geist-Regular", + "fontSize": 16, "letterSpacing": 0, "lineHeight": 24, } } > - ๐Ÿ‡ฆ๐Ÿ‡ซ + We are working hard to expand coverage to your region as soon as we can. In the meantime, see our support article for other ways you may be able to sell crypto. - - Afghanistan - - - - + ๐Ÿ‡ฆ๐Ÿ‡ซ + + + Afghanistan + + + - Visit support article - - - - - + Visit support article + + + + - - Select your region - - + + Select your region + + + @@ -1185,9 +1231,9 @@ exports[`UnsupportedRegionModal renders correctly for sell flow 1`] = ` - - - + + + `; diff --git a/app/components/UI/Ramp/Aggregator/hooks/useHandleSuccessfulOrder.test.ts b/app/components/UI/Ramp/Aggregator/hooks/useHandleSuccessfulOrder.test.ts index 38f0919fcc2..de1b120c1d6 100644 --- a/app/components/UI/Ramp/Aggregator/hooks/useHandleSuccessfulOrder.test.ts +++ b/app/components/UI/Ramp/Aggregator/hooks/useHandleSuccessfulOrder.test.ts @@ -12,7 +12,7 @@ const mockTrackEvent = jest.fn(); jest.mock('@react-navigation/native', () => ({ useNavigation: () => ({ navigate: mockNavigate, - dangerouslyGetParent: () => ({ + getParent: () => ({ pop: mockPop, }), }), @@ -74,6 +74,7 @@ describe('useHandleSuccessfulOrder', () => { await result.current(sellOrder); + expect(mockPop).toHaveBeenCalled(); expect(mockTrackEvent).toHaveBeenCalledWith('OFFRAMP_PURCHASE_SUBMITTED', { payment_method_id: 'test-payment-method', order_type: OrderOrderTypeEnum.Sell, @@ -116,6 +117,7 @@ describe('useHandleSuccessfulOrder', () => { await result.current(buyOrder); + expect(mockPop).toHaveBeenCalled(); expect(mockTrackEvent).toHaveBeenCalledWith('ONRAMP_PURCHASE_SUBMITTED', { payment_method_id: 'test-payment-method', order_type: OrderOrderTypeEnum.Buy, diff --git a/app/components/UI/Ramp/Aggregator/hooks/useHandleSuccessfulOrder.ts b/app/components/UI/Ramp/Aggregator/hooks/useHandleSuccessfulOrder.ts index 9987083e9a6..4e6acd68891 100644 --- a/app/components/UI/Ramp/Aggregator/hooks/useHandleSuccessfulOrder.ts +++ b/app/components/UI/Ramp/Aggregator/hooks/useHandleSuccessfulOrder.ts @@ -38,7 +38,7 @@ function useHandleSuccessfulOrder() { ) => { handleDispatchUserWalletProtection(); // @ts-expect-error navigation prop mismatch - navigation.dangerouslyGetParent()?.pop(); + navigation.getParent()?.pop(); dispatchThunk((_dispatch, getState) => { const state = getState(); diff --git a/app/components/UI/Ramp/Aggregator/routes/index.test.tsx b/app/components/UI/Ramp/Aggregator/routes/index.test.tsx new file mode 100644 index 00000000000..7c53b360625 --- /dev/null +++ b/app/components/UI/Ramp/Aggregator/routes/index.test.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import { Provider } from 'react-redux'; +import configureMockStore from 'redux-mock-store'; +import { NavigationContainer } from '@react-navigation/native'; +import RampRoutes from './index'; +import { RampType } from '../types'; +import Routes from '../../../../../constants/navigation/Routes'; +import { backgroundState } from '../../../../../util/test/initial-root-state'; + +jest.mock('@react-navigation/stack', () => ({ + createStackNavigator: jest.fn().mockReturnValue({ + Navigator: ({ children }: { children: React.ReactNode }) => children, + Screen: ({ + name, + component: Component, + }: { + name: string; + component: React.ComponentType; + }) => , + }), +})); + +jest.mock('../sdk', () => ({ + RampSDKProvider: ({ children }: { children: React.ReactNode }) => children, +})); + +jest.mock('../Views/Quotes', () => 'Quotes'); +jest.mock('../Views/Checkout', () => 'CheckoutWebView'); +jest.mock('../Views/BuildQuote', () => 'BuildQuote'); +jest.mock( + '../components/TokenSelectModal/TokenSelectModal', + () => 'TokenSelectModal', +); +jest.mock( + '../components/PaymentMethodSelectorModal', + () => 'PaymentMethodSelectorModal', +); +jest.mock('../components/FiatSelectorModal', () => 'FiatSelectorModal'); +jest.mock( + '../components/IncompatibleAccountTokenModal', + () => 'IncompatibleAccountTokenModal', +); +jest.mock('../components/RegionSelectorModal', () => 'RegionSelectorModal'); +jest.mock( + '../components/UnsupportedRegionModal', + () => 'UnsupportedRegionModal', +); +jest.mock('../Views/Modals/Settings', () => 'SettingsModal'); + +const mockStore = configureMockStore(); + +const initialState = { + engine: { + backgroundState, + }, + fiatOrders: { + selectedRegion: null, + selectedPaymentMethodId: null, + getStartedToken: null, + getStartedChainId: null, + }, + settings: { + currentCurrency: 'USD', + }, +}; + +describe('RampRoutes', () => { + const renderWithProviders = (rampType: RampType) => { + const store = mockStore(initialState); + + return render( + + + + + , + ); + }; + + it('renders RampRoutes with BUY type', () => { + const { toJSON } = renderWithProviders(RampType.BUY); + expect(toJSON()).toBeTruthy(); + }); + + it('renders RampRoutes with SELL type', () => { + const { toJSON } = renderWithProviders(RampType.SELL); + expect(toJSON()).toBeTruthy(); + }); + + it('provides correct initial route name for BUY type', () => { + const { toJSON } = renderWithProviders(RampType.BUY); + expect(toJSON()).toBeTruthy(); + }); + + it('provides correct initial route name for SELL type', () => { + const { toJSON } = renderWithProviders(RampType.SELL); + expect(toJSON()).toBeTruthy(); + }); + + describe('route configuration', () => { + it('contains BUILD_QUOTE route', () => { + expect(Routes.RAMP.BUILD_QUOTE).toBeDefined(); + }); + + it('contains QUOTES route', () => { + expect(Routes.RAMP.QUOTES).toBeDefined(); + }); + + it('contains CHECKOUT route', () => { + expect(Routes.RAMP.CHECKOUT).toBeDefined(); + }); + + it('contains modal routes', () => { + expect(Routes.RAMP.MODALS.TOKEN_SELECTOR).toBeDefined(); + expect(Routes.RAMP.MODALS.PAYMENT_METHOD_SELECTOR).toBeDefined(); + expect(Routes.RAMP.MODALS.FIAT_SELECTOR).toBeDefined(); + expect(Routes.RAMP.MODALS.REGION_SELECTOR).toBeDefined(); + expect(Routes.RAMP.MODALS.UNSUPPORTED_REGION).toBeDefined(); + expect(Routes.RAMP.MODALS.SETTINGS).toBeDefined(); + }); + }); +}); diff --git a/app/components/UI/Ramp/Aggregator/routes/index.tsx b/app/components/UI/Ramp/Aggregator/routes/index.tsx index 2e2d5b62ed9..498dd7a62c8 100644 --- a/app/components/UI/Ramp/Aggregator/routes/index.tsx +++ b/app/components/UI/Ramp/Aggregator/routes/index.tsx @@ -26,10 +26,7 @@ const clearStackNavigatorOptions = { }; const MainRoutes = () => ( - + ( const RampModalsRoutes = () => ( ( const RampRoutes = ({ rampType }: { rampType: RampType }) => ( - + - - - - - - - - - - - - - Additional verification - - - - - - - - - - - - - + - - - - - - Additional verification - - - For larger deposits, youโ€™ll need a valid ID (like a driverโ€™s license) and a real-time selfie. - - + + Additional verification + + + For larger deposits, youโ€™ll need a valid ID (like a driverโ€™s license) and a real-time selfie. + + + In order to complete your verification, youโ€™ll need to enable access to your camera. + + + + + - In order to complete your verification, youโ€™ll need to enable access to your camera. - + + + Continue + + + + - + + + + + + + + + + + + - - Continue + Additional verification - - + + + - + - - - + + + + `; diff --git a/app/components/UI/Ramp/Deposit/Views/BankDetails/__snapshots__/BankDetails.test.tsx.snap b/app/components/UI/Ramp/Deposit/Views/BankDetails/__snapshots__/BankDetails.test.tsx.snap index e4edebb99d1..2e7c0772981 100644 --- a/app/components/UI/Ramp/Deposit/Views/BankDetails/__snapshots__/BankDetails.test.tsx.snap +++ b/app/components/UI/Ramp/Deposit/Views/BankDetails/__snapshots__/BankDetails.test.tsx.snap @@ -20,487 +20,257 @@ exports[`BankDetails Component render matches snapshot 1`] = ` } > - - - - - - - - - - - - - SEPA bank transfer - - - - - - - - - - - - - + - - - - + - - - + + } + testID="bank-details-refresh-control-scrollview" + > + + - - To complete your order - - - Initiate a transfer from your preferred bank using the information below. - - - Once youโ€™re finished, come back and click โ€œConfirm transferโ€ to confirm. - - - @@ -509,6 +279,34 @@ exports[`BankDetails Component render matches snapshot 1`] = ` style={ { "color": "#131416", + "fontFamily": "Geist-Bold", + "fontSize": 20, + "letterSpacing": 0, + "lineHeight": 24, + } + } + > + To complete your order + + + Initiate a transfer from your preferred bank using the information below. + + - Transfer amount + Once youโ€™re finished, come back and click โ€œConfirm transferโ€ to confirm. + + @@ -534,70 +339,70 @@ exports[`BankDetails Component render matches snapshot 1`] = ` accessibilityRole="text" style={ { - "color": "#66676a", - "flex": 1, - "flexWrap": "wrap", + "color": "#131416", "fontFamily": "Geist-Regular", "fontSize": 16, "letterSpacing": 0, "lineHeight": 24, - "textAlign": "right", } } > - $100.00 + Transfer amount - - - + > + $100.00 + + + + + - - - - Account holder name - @@ -605,70 +410,70 @@ exports[`BankDetails Component render matches snapshot 1`] = ` accessibilityRole="text" style={ { - "color": "#66676a", - "flex": 1, - "flexWrap": "wrap", + "color": "#131416", "fontFamily": "Geist-Regular", "fontSize": 16, "letterSpacing": 0, "lineHeight": 24, - "textAlign": "right", } } > - John Doe + Account holder name - - - + > + John Doe + + + + + - - - - Account number - @@ -676,283 +481,482 @@ exports[`BankDetails Component render matches snapshot 1`] = ` accessibilityRole="text" style={ { - "color": "#66676a", - "flex": 1, - "flexWrap": "wrap", + "color": "#131416", "fontFamily": "Geist-Regular", "fontSize": 16, "letterSpacing": 0, "lineHeight": 24, - "textAlign": "right", } } > - 1234567890 + Account number - - - + > + 1234567890 + + + + + - - - - Show bank information - - - + > + Show bank information + + + + - - - - + + - + + + + + + You should see 'John Doe' as the account holder. This helps your bank process the transfer. + + - + + - + + Cancel order + + + - You should see 'John Doe' as the account holder. This helps your bank process the transfer. + Confirm transfer - + + + + + + + + + + + - - + - Cancel order - - - - - Confirm transfer - - + /> + + + + SEPA bank transfer + + + + + + - + - - - + + + + `; @@ -977,487 +981,257 @@ exports[`BankDetails Component render matches snapshot with bank info shown 1`] } > - - - - - - - - - - - - - SEPA bank transfer - - - - - - - - - - - - - + - - - - + - - - + + } + testID="bank-details-refresh-control-scrollview" + > + + - - To complete your order - - - Initiate a transfer from your preferred bank using the information below. - - - Once youโ€™re finished, come back and click โ€œConfirm transferโ€ to confirm. - - - @@ -1466,6 +1240,34 @@ exports[`BankDetails Component render matches snapshot with bank info shown 1`] style={ { "color": "#131416", + "fontFamily": "Geist-Bold", + "fontSize": 20, + "letterSpacing": 0, + "lineHeight": 24, + } + } + > + To complete your order + + + Initiate a transfer from your preferred bank using the information below. + + - Transfer amount + Once youโ€™re finished, come back and click โ€œConfirm transferโ€ to confirm. + + @@ -1491,70 +1300,70 @@ exports[`BankDetails Component render matches snapshot with bank info shown 1`] accessibilityRole="text" style={ { - "color": "#66676a", - "flex": 1, - "flexWrap": "wrap", + "color": "#131416", "fontFamily": "Geist-Regular", "fontSize": 16, "letterSpacing": 0, "lineHeight": 24, - "textAlign": "right", } } > - $100.00 + Transfer amount - - - + > + $100.00 + + + + + - - - - Account holder name - @@ -1562,70 +1371,70 @@ exports[`BankDetails Component render matches snapshot with bank info shown 1`] accessibilityRole="text" style={ { - "color": "#66676a", - "flex": 1, - "flexWrap": "wrap", + "color": "#131416", "fontFamily": "Geist-Regular", "fontSize": 16, "letterSpacing": 0, "lineHeight": 24, - "textAlign": "right", } } > - John Doe + Account holder name - - - + > + John Doe + + + + + - - - - Account number - @@ -1633,70 +1442,141 @@ exports[`BankDetails Component render matches snapshot with bank info shown 1`] accessibilityRole="text" style={ { - "color": "#66676a", - "flex": 1, - "flexWrap": "wrap", + "color": "#131416", "fontFamily": "Geist-Regular", "fontSize": 16, "letterSpacing": 0, "lineHeight": 24, - "textAlign": "right", } } > - 1234567890 + Account number - - - + > + 1234567890 + + + + + - - - - Bank name - + + Bank name + + + + Test Bank + + + + + + @@ -1704,70 +1584,144 @@ exports[`BankDetails Component render matches snapshot with bank info shown 1`] accessibilityRole="text" style={ { - "color": "#66676a", - "flex": 1, - "flexWrap": "wrap", + "color": "#131416", "fontFamily": "Geist-Regular", "fontSize": 16, "letterSpacing": 0, "lineHeight": 24, - "textAlign": "right", } } > - Test Bank + Beneficiary address - - - + > + 456 Recipient Street + + + + + - - - - Beneficiary address - - + Bank address + + + + 123 Bank Street + + + + + + + @@ -1775,118 +1729,163 @@ exports[`BankDetails Component render matches snapshot with bank info shown 1`] accessibilityRole="text" style={ { - "color": "#66676a", - "flex": 1, - "flexWrap": "wrap", + "color": "#4459ff", "fontFamily": "Geist-Regular", "fontSize": 16, "letterSpacing": 0, "lineHeight": 24, - "textAlign": "right", } } > - 456 Recipient Street + Hide bank information - - - - + } + width={16} + /> + + + + + + + + + - - Bank address - + + + - 123 Bank Street + You should see 'John Doe' as the account holder. This helps your bank process the transfer. - - - + + @@ -1894,235 +1893,244 @@ exports[`BankDetails Component render matches snapshot with bank info shown 1`] accessibilityRole="text" style={ { - "color": "#4459ff", - "fontFamily": "Geist-Regular", + "color": "#131416", + "fontFamily": "Geist-Medium", "fontSize": 16, "letterSpacing": 0, "lineHeight": 24, } } > - Hide bank information + Cancel order - - - - - - - - - - - - - - - - - You should see 'John Doe' as the account holder. This helps your bank process the transfer. + Confirm transfer - + + + + + + + + + + + - - + - Cancel order - - - - - Confirm transfer - - + /> + + + + SEPA bank transfer + + + + + + - + - - - + + + + `; diff --git a/app/components/UI/Ramp/Deposit/Views/BasicInfo/__snapshots__/BasicInfo.test.tsx.snap b/app/components/UI/Ramp/Deposit/Views/BasicInfo/__snapshots__/BasicInfo.test.tsx.snap index 9f382714196..07bb029b4ca 100644 --- a/app/components/UI/Ramp/Deposit/Views/BasicInfo/__snapshots__/BasicInfo.test.tsx.snap +++ b/app/components/UI/Ramp/Deposit/Views/BasicInfo/__snapshots__/BasicInfo.test.tsx.snap @@ -20,558 +20,632 @@ exports[`BasicInfo Component navigates to address page when form is valid and co } > - - - - - - - - - - - - - Verify your identity - - - - - - - - - - - - - + - - - - - - + } + enableAutomaticScroll={true} + enableOnAndroid={false} + enableResetScrollToCoords={true} + extraHeight={75} + extraScrollHeight={0} + getScrollResponder={[Function]} + handleOnScroll={[Function]} + keyboardDismissMode="interactive" + keyboardOpeningTime={250} + keyboardShouldPersistTaps="handled" + keyboardSpace={0} + onScroll={[Function]} + resetKeyboardSpace={[Function]} + scrollEventThrottle={1} + scrollForExtraHeightOnAndroid={[Function]} + scrollIntoView={[Function]} + scrollToEnd={[Function]} + scrollToFocusedInput={[Function]} + scrollToPosition={[Function]} + showsVerticalScrollIndicator={false} + update={[Function]} + viewIsInsideTabBar={false} + > + + - + + + + + - + Enter your basic info + + + > + Next, we need some basic information about you. + + - - - Enter your basic info - - - Next, we need some basic information about you. - - + ] + } + > + + First name + + + + + + + + + + Last name + + + + + + + + @@ -603,7 +675,7 @@ exports[`BasicInfo Component navigates to address page when form is valid and co ] } > - First name + Phone number + + + + ๐Ÿ‡บ๐Ÿ‡ธ + + + +1 + + + @@ -700,9 +821,7 @@ exports[`BasicInfo Component navigates to address page when form is valid and co "flexDirection": "column", "marginBottom": 16, }, - { - "flex": 1, - }, + undefined, ] } > @@ -724,7 +843,7 @@ exports[`BasicInfo Component navigates to address page when form is valid and co ] } > - Last name + Date of birth - + + + + + - - - - Phone number - - - @@ -922,525 +989,462 @@ exports[`BasicInfo Component navigates to address page when form is valid and co } } > - ๐Ÿ‡บ๐Ÿ‡ธ + Social security number (SSN) - - +1 - - + + + - - - - - - - Date of birth - - - - - - - - + + + + + + + - - - Social security number (SSN) - - - - - + width={24} + /> + - - + - + } + > + MetaMask never receives or uses your data. + - - - - - - - - - - - - + + - - Transak encrypts and stores this information. - - - MetaMask never receives or uses your data. - - + } + /> + + + + + + + + + + + + - + + - Continue + Verify your identity - - + + + - + - - - + + + + `; @@ -1465,558 +1469,632 @@ exports[`BasicInfo Component passes regions to DepositPhoneField component 1`] = } > - - - - - - - - - - - - - Verify your identity - - - - - - - - - - - - - + - - - - - - + } + enableAutomaticScroll={true} + enableOnAndroid={false} + enableResetScrollToCoords={true} + extraHeight={75} + extraScrollHeight={0} + getScrollResponder={[Function]} + handleOnScroll={[Function]} + keyboardDismissMode="interactive" + keyboardOpeningTime={250} + keyboardShouldPersistTaps="handled" + keyboardSpace={0} + onScroll={[Function]} + resetKeyboardSpace={[Function]} + scrollEventThrottle={1} + scrollForExtraHeightOnAndroid={[Function]} + scrollIntoView={[Function]} + scrollToEnd={[Function]} + scrollToFocusedInput={[Function]} + scrollToPosition={[Function]} + showsVerticalScrollIndicator={false} + update={[Function]} + viewIsInsideTabBar={false} + > + + - + + + + + + > + Enter your basic info + + + Next, we need some basic information about you. + + - + + First name + + + + + + + + - - - Enter your basic info - - - Next, we need some basic information about you. - - + ] + } + > + + Last name + + + + + + + + @@ -2048,7 +2124,7 @@ exports[`BasicInfo Component passes regions to DepositPhoneField component 1`] = ] } > - First name + Phone number + + + + ๐Ÿ‡บ๐Ÿ‡ธ + + + +1 + + + @@ -2145,9 +2270,7 @@ exports[`BasicInfo Component passes regions to DepositPhoneField component 1`] = "flexDirection": "column", "marginBottom": 16, }, - { - "flex": 1, - }, + undefined, ] } > @@ -2169,7 +2292,7 @@ exports[`BasicInfo Component passes regions to DepositPhoneField component 1`] = ] } > - Last name + Date of birth - + + + + + - - - - Phone number - - - @@ -2367,526 +2438,463 @@ exports[`BasicInfo Component passes regions to DepositPhoneField component 1`] = } } > - ๐Ÿ‡บ๐Ÿ‡ธ + Social security number (SSN) - - +1 - - + + + - - - - - - - Date of birth - - - - - - - - + + + + + + + - - - Social security number (SSN) - - - - - + width={24} + /> + - - + - + } + > + MetaMask never receives or uses your data. + - - - - - - - - - + + + + + + + + + + + + + + - - - - - Transak encrypts and stores this information. - - - MetaMask never receives or uses your data. - - - + + - Continue + Verify your identity - - + + + - + - - - + + + + `; @@ -2911,568 +2919,640 @@ exports[`BasicInfo Component prefills form data when previousFormData is provide } > - - - - - - - - - + - - Verify your identity - - - - - - - - - - - - - - - - - - - - - + } + enableAutomaticScroll={true} + enableOnAndroid={false} + enableResetScrollToCoords={true} + extraHeight={75} + extraScrollHeight={0} + getScrollResponder={[Function]} + handleOnScroll={[Function]} + keyboardDismissMode="interactive" + keyboardOpeningTime={250} + keyboardShouldPersistTaps="handled" + keyboardSpace={0} + onScroll={[Function]} + resetKeyboardSpace={[Function]} + scrollEventThrottle={1} + scrollForExtraHeightOnAndroid={[Function]} + scrollIntoView={[Function]} + scrollToEnd={[Function]} + scrollToFocusedInput={[Function]} + scrollToPosition={[Function]} + showsVerticalScrollIndicator={false} + update={[Function]} + viewIsInsideTabBar={false} + > + + - + + + + + - + Enter your basic info + + + > + Next, we need some basic information about you. + + - - - Enter your basic info - - - Next, we need some basic information about you. - - - + + First name + + + + + + + + + + Last name + + + + + + + + + @@ -3494,7 +3574,7 @@ exports[`BasicInfo Component prefills form data when previousFormData is provide ] } > - First name + Phone number + + + + ๐Ÿ‡บ๐Ÿ‡ธ + + + +1 + + + @@ -3591,9 +3720,7 @@ exports[`BasicInfo Component prefills form data when previousFormData is provide "flexDirection": "column", "marginBottom": 16, }, - { - "flex": 1, - }, + undefined, ] } > @@ -3615,7 +3742,7 @@ exports[`BasicInfo Component prefills form data when previousFormData is provide ] } > - Last name + Date of birth - + + + + + - - - - Phone number - - - @@ -3813,526 +3888,463 @@ exports[`BasicInfo Component prefills form data when previousFormData is provide } } > - ๐Ÿ‡บ๐Ÿ‡ธ + Social security number (SSN) - - +1 - - + + + - - - - - - - Date of birth - - - - - - - - + + + + + + + - - - Social security number (SSN) - - - - - + width={24} + /> + - - + - + } + > + MetaMask never receives or uses your data. + - - - - - - - - - + + + + + + + + + + + + + + - - - - - Transak encrypts and stores this information. - - - MetaMask never receives or uses your data. - - - + + - Continue + Verify your identity - - + + + - + - - - + + + + `; @@ -4357,558 +4369,634 @@ exports[`BasicInfo Component render matches snapshot 1`] = ` } > - - - - - - - - - - - - - Verify your identity - - - - - - - - - - - - - + - - - - - - + } + enableAutomaticScroll={true} + enableOnAndroid={false} + enableResetScrollToCoords={true} + extraHeight={75} + extraScrollHeight={0} + getScrollResponder={[Function]} + handleOnScroll={[Function]} + keyboardDismissMode="interactive" + keyboardOpeningTime={250} + keyboardShouldPersistTaps="handled" + keyboardSpace={0} + onScroll={[Function]} + resetKeyboardSpace={[Function]} + scrollEventThrottle={1} + scrollForExtraHeightOnAndroid={[Function]} + scrollIntoView={[Function]} + scrollToEnd={[Function]} + scrollToFocusedInput={[Function]} + scrollToPosition={[Function]} + showsVerticalScrollIndicator={false} + update={[Function]} + viewIsInsideTabBar={false} + > + + - + + + + + + > + Enter your basic info + + + Next, we need some basic information about you. + + - + + First name + + + + + + + + - - - Enter your basic info - - - Next, we need some basic information about you. - - + ] + } + > + + Last name + + + + + + + + @@ -4940,7 +5026,7 @@ exports[`BasicInfo Component render matches snapshot 1`] = ` ] } > - First name + Phone number + + + + ๐Ÿ‡บ๐Ÿ‡ธ + + + +1 + + + @@ -5038,9 +5173,7 @@ exports[`BasicInfo Component render matches snapshot 1`] = ` "flexDirection": "column", "marginBottom": 16, }, - { - "flex": 1, - }, + undefined, ] } > @@ -5062,7 +5195,7 @@ exports[`BasicInfo Component render matches snapshot 1`] = ` ] } > - Last name + Date of birth - - - - - - + + + + + + + - Phone number - - - @@ -5261,527 +5341,463 @@ exports[`BasicInfo Component render matches snapshot 1`] = ` } } > - ๐Ÿ‡บ๐Ÿ‡ธ + Social security number (SSN) - - +1 - - + + + - - - - - - - Date of birth - - - - - - - - + + + + + + + - - - Social security number (SSN) - - - - - + width={24} + /> + - - + - + } + > + MetaMask never receives or uses your data. + - - - - - - - - - + + + + + + + + + + + + + + - - - - - Transak encrypts and stores this information. - - - MetaMask never receives or uses your data. - - - + + - Continue + Verify your identity - - + + + - + - - - + + + + `; @@ -5806,577 +5822,681 @@ exports[`BasicInfo Component snapshot matches validation errors when continue is } > - - - - - - - - - + - - Verify your identity - - - - - - - - - - - - - - - - - - - - - - - + } + enableAutomaticScroll={true} + enableOnAndroid={false} + enableResetScrollToCoords={true} + extraHeight={75} + extraScrollHeight={0} + getScrollResponder={[Function]} + handleOnScroll={[Function]} + keyboardDismissMode="interactive" + keyboardOpeningTime={250} + keyboardShouldPersistTaps="handled" + keyboardSpace={0} + onScroll={[Function]} + resetKeyboardSpace={[Function]} + scrollEventThrottle={1} + scrollForExtraHeightOnAndroid={[Function]} + scrollIntoView={[Function]} + scrollToEnd={[Function]} + scrollToFocusedInput={[Function]} + scrollToPosition={[Function]} + showsVerticalScrollIndicator={false} + update={[Function]} + viewIsInsideTabBar={false} + > + - + - + - + - - - Enter your basic info - - + + + - Next, we need some basic information about you. - - + Enter your basic info + + + > + Next, we need some basic information about you. + - + + First name + + + + + + + + First name is required + + + + + Last name + + + + + + + + Last name is required + + + + + - First name + Phone number + + + + ๐Ÿ‡บ๐Ÿ‡ธ + + + +1 + + + @@ -6492,7 +6661,7 @@ exports[`BasicInfo Component snapshot matches validation errors when continue is } } > - First name is required + Phone number is required @@ -6526,7 +6693,7 @@ exports[`BasicInfo Component snapshot matches validation errors when continue is ] } > - Last name + Date of birth - - - - Last name is required - + > + + + + + - - - - Phone number - - - @@ -6740,557 +6839,478 @@ exports[`BasicInfo Component snapshot matches validation errors when continue is } } > - ๐Ÿ‡บ๐Ÿ‡ธ + Social security number (SSN) - - +1 - - + + + - - - - - Phone number is required - - - - - Date of birth - - - - - - - - + + + Social security number is required + + + + + + + - - - Social security number (SSN) - - - - - + width={24} + /> + - - + - - - - Social security number is required - + > + MetaMask never receives or uses your data. + + - - - - - - - - - + + + + + + + + + + + + + + - - - - - Transak encrypts and stores this information. - - - MetaMask never receives or uses your data. - - - + + - Continue + Verify your identity - - + + + - + - - - + + + + `; @@ -7315,693 +7335,661 @@ exports[`BasicInfo Component snapshot matches validation errors when continue is } > - - - - - - - - - - - - - Verify your identity - - - - - - - - - - - - - + - - - - - - + } + enableAutomaticScroll={true} + enableOnAndroid={false} + enableResetScrollToCoords={true} + extraHeight={75} + extraScrollHeight={0} + getScrollResponder={[Function]} + handleOnScroll={[Function]} + keyboardDismissMode="interactive" + keyboardOpeningTime={250} + keyboardShouldPersistTaps="handled" + keyboardSpace={0} + onScroll={[Function]} + resetKeyboardSpace={[Function]} + scrollEventThrottle={1} + scrollForExtraHeightOnAndroid={[Function]} + scrollIntoView={[Function]} + scrollToEnd={[Function]} + scrollToFocusedInput={[Function]} + scrollToPosition={[Function]} + showsVerticalScrollIndicator={false} + update={[Function]} + viewIsInsideTabBar={false} + > + - + - + - + - - - Enter your basic info - - + + + - Next, we need some basic information about you. - - + Enter your basic info + + + > + Next, we need some basic information about you. + - - First name - - + First name + + - - + + + + + First name is required + + + + + Last name + + + + > + + - - - First name is required - + > + Please enter a valid last name + + @@ -8034,7 +8020,7 @@ exports[`BasicInfo Component snapshot matches validation errors when continue is ] } > - Last name + Phone number + + + + ๐Ÿ‡บ๐Ÿ‡ธ + + + +1 + + + - - Please enter a valid last name - - - - - Phone number - - + Date of birth + + - - - - ๐Ÿ‡บ๐Ÿ‡ธ - - + + + - +1 - - - - - + + + - - - - Date of birth - - - + > + + Social security number (SSN) + + + + + - - + + + Social security number is required + + + + + + + - - - Social security number (SSN) - - - - - + width={24} + /> + - - + - - - - Social security number is required - + > + MetaMask never receives or uses your data. + + - - - - - - - - - + + + + + + + + + + + + + + - - - - - Transak encrypts and stores this information. - - - MetaMask never receives or uses your data. - - - + + - Continue + Verify your identity - - + + + - + - - - + + + + `; @@ -8806,693 +8830,661 @@ exports[`BasicInfo Component snapshot matches validation errors when continue is } > - - - - - - - - - + - - Verify your identity - - - - - - - - - - - - - - - - - - - - - - - + } + enableAutomaticScroll={true} + enableOnAndroid={false} + enableResetScrollToCoords={true} + extraHeight={75} + extraScrollHeight={0} + getScrollResponder={[Function]} + handleOnScroll={[Function]} + keyboardDismissMode="interactive" + keyboardOpeningTime={250} + keyboardShouldPersistTaps="handled" + keyboardSpace={0} + onScroll={[Function]} + resetKeyboardSpace={[Function]} + scrollEventThrottle={1} + scrollForExtraHeightOnAndroid={[Function]} + scrollIntoView={[Function]} + scrollToEnd={[Function]} + scrollToFocusedInput={[Function]} + scrollToPosition={[Function]} + showsVerticalScrollIndicator={false} + update={[Function]} + viewIsInsideTabBar={false} + > + - + - + - + - - - Enter your basic info - - + + + - Next, we need some basic information about you. - - + Enter your basic info + + + > + Next, we need some basic information about you. + - - First name - - + First name + + + + + + + + First name is required + + + + + Last name + - + > + + - - - First name is required - + > + Please enter a valid last name + + @@ -9525,7 +9515,7 @@ exports[`BasicInfo Component snapshot matches validation errors when continue is ] } > - Last name + Phone number + + + + ๐Ÿ‡จ๐Ÿ‡ฆ + + + +1 + + + - - Please enter a valid last name - - - - - Phone number - - - - - - ๐Ÿ‡จ๐Ÿ‡ฆ - - - +1 - - - - - - - - - - - Date of birth - - + Date of birth + + - - - - - + + + - - + > + + + - - - - - + + + - - - - - + + + - Transak encrypts and stores this information. - - + - MetaMask never receives or uses your data. - + + Transak encrypts and stores this information. + + + MetaMask never receives or uses your data. + + + + + Continue + + + - + + + + + + + + + + + + + + + - Continue + Verify your identity - - + + + - + - - - + + + + `; diff --git a/app/components/UI/Ramp/Deposit/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap b/app/components/UI/Ramp/Deposit/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap index 5d139828610..01614e847b5 100644 --- a/app/components/UI/Ramp/Deposit/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap +++ b/app/components/UI/Ramp/Deposit/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap @@ -20,447 +20,199 @@ exports[`BuildQuote Component Continue button functionality displays error when } > - - - - - - - - - - - - - Buy - - - - - - - - - - - - - - - - - + - - - - - - Account is loading... - - - - - ๐Ÿ‡บ๐Ÿ‡ธ - - - US + Account is loading... - - - - - - - - $0 - - - - + - - - - - + } + > + ๐Ÿ‡บ๐Ÿ‡ธ + + + US + + + + + + + + + $0 + + + + + + + + + - + > + + - - - USDC - - + USDC + + - - - + width={16} + /> + + - - - - + + + - Failed to fetch quote - + + Failed to fetch quote + + - - - - - Pay with - - + Pay with + + + Debit or Credit + + + - Debit or Credit - - - - + testID="listitem-gap" + /> - - Instant - + + Instant + + - - - - + + + - - - + - - - 1 - - - - - + 1 + + + + - - 2 - - - - - + 2 + + + + - - 3 - - + + 3 + + + - - - - - 4 - - - - - + 4 + + + + - - 5 - - + + 5 + + + + + + + 6 + + + - - - 6 - - - - - - - + 7 + + + + - - 7 - - - - - + 8 + + + + - - 8 - - + + 9 + + + - - - 9 - - - - - - - - - - - - + + + + - - 0 - - - - - + 0 + + + + - - + testID="keypad-delete-button" + > + + + - - - - + - - Continue - - - - - - - - - - - - - - - - -`; + + Continue + + + + + + + + + + + + + + + + + + + + + Buy + + + + + + + + + + + + + + + + + + + + + + + + +`; exports[`BuildQuote Component Continue button functionality displays error when quote is falsy 1`] = ` - - - - - - - - - + - - Buy - - - - - - - - - - - - - - - - - - - - - - - - - - Account is loading... - - - - - ๐Ÿ‡บ๐Ÿ‡ธ + Account is loading... + + + + + ๐Ÿ‡บ๐Ÿ‡ธ + + + US + + + + + + + - US + $0 - - - - - - - $0 - - - - - - - - - - + + + + + - + > + + - - - USDC - - + USDC + + - - - + width={16} + /> + + - - - - + + + - Failed to fetch quote. - + + Failed to fetch quote. + + - - - - - Pay with - - + Pay with + + + Debit or Credit + + + - Debit or Credit - - - - + testID="listitem-gap" + /> - - Instant - + + Instant + + - - - - + + + - - - + - - - 1 - - - - - + 1 + + + + - - 2 - - - - - + 2 + + + + - - 3 - - + + 3 + + + - - - - - 4 - - - - - + 4 + + + + - - 5 - - - - - + 5 + + + + - - 6 - - + + 6 + + + - - - - - 7 - - - - - + 7 + + + + - - 8 - - - - - + 8 + + + + - - 9 - - + + 9 + + + - - - - - - - - + + + + - - 0 - - - - - + 0 + + + + - - + testID="keypad-delete-button" + > + + + + + + + + Continue + + + + - + + + + + + + + + + + + - - Continue + Buy - + - - - - - - - - - - - - - -`; - -exports[`BuildQuote Component Continue button functionality displays error when routeAfterAuthentication throws 1`] = ` - - - - - - - - - - - - - - - - Buy - - - - - - - + + + + + + + + + + + + - - - - - + + + + + +`; + +exports[`BuildQuote Component Continue button functionality displays error when routeAfterAuthentication throws 1`] = ` + + + + - - + - - - - - - Account is loading... - - - - - ๐Ÿ‡บ๐Ÿ‡ธ + Account is loading... + + + + + ๐Ÿ‡บ๐Ÿ‡ธ + + + US + + + + + + + - US + $0 - - - - - - - $0 - - - - - - - - - + + + + + - + > + + - - - USDC - - + USDC + + - - - + width={16} + /> + + - - - - + + + - Routing failed - - - - - - - - + Routing failed + + + + + + + - - Pay with - - + Pay with + + + Debit or Credit + + + - Debit or Credit - - - - + testID="listitem-gap" + /> - - Instant - + + Instant + + - - - - + + + - - - + - - - 1 - - - - - + 1 + + + + - - 2 - - - - - + 2 + + + + - - 3 - - + + 3 + + + - - - - - 4 - - - - - + 4 + + + + - - 5 - - - - - + 5 + + + + - - 6 - - + + 6 + + + - - - - - 7 - - - - - + 7 + + + + - - 8 - - - - - + 8 + + + + - - 9 - - + + 9 + + + - - - - - - - - + + + + - - 0 - - - - - + 0 + + + + - - - - - - - - + testID="keypad-delete-button" + > + + + + + + + + + + + + Continue + + + + + + + + + + + + + + + + + - + Buy + + + + + + - Continue - - + + + - + - - - + + + + `; @@ -5405,447 +5417,199 @@ exports[`BuildQuote Component Keypad Functionality displays converted token amou } > - - - - - - - - - + - - Buy - - - - - - - - - - - - - - - - - - - - - - - - - - - Account is loading... - - - - - ๐Ÿ‡บ๐Ÿ‡ธ - - - US + Account is loading... - - - - - - - - $0 - - - 1.5 - - USDC - - - - + - - - - - - - + + } + > + US + + + + + + + + + $0 + + + 1.5 + + USDC + + + + + + + + + + + + > + + - - - USDC - - + USDC + + - - - - - + + + + - - Pay with - - + Pay with + + + Debit or Credit + + + - Debit or Credit - - - - + testID="listitem-gap" + /> - - Instant - + + Instant + + - - - - - - - - - + + + + + + - - - 1 - - - - - + 1 + + + + - - 2 - - - - - + 2 + + + + - - 3 - - + + 3 + + + - - - - - 4 - - - - - + 4 + + + + - - 5 - - - - - + 5 + + + + - - 6 - - + + 6 + + + - - - - - 7 - - - - - + 7 + + + + - - 8 - - - - - + 8 + + + + - - 9 - - + + 9 + + + - - - - - - - - + + + + - - 0 - - - - - + 0 + + + + - - + testID="keypad-delete-button" + > + + + + + + + + Continue + + + + - + + + + + + + + + + + + - - Continue + Buy - + - - - - - - - - - - - - - -`; - -exports[`BuildQuote Component Keypad Functionality updates amount when keypad is used 1`] = ` + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`BuildQuote Component Keypad Functionality updates amount when keypad is used 1`] = ` - - - - - - - - - - - - - Buy - - - - - - - - - - - - - - - - - + - - - - - - Account is loading... - - - - - ๐Ÿ‡บ๐Ÿ‡ธ + Account is loading... - + + + + ๐Ÿ‡บ๐Ÿ‡ธ + + + US + + + + + + + + - US + $1 - + > + 0 + + USDC + - - - - - - $1 - - - 0 - - USDC - - - - - - - - - + + + + + - + > + + - - - USDC - - + USDC + + - - - - - + + + + - - Pay with - - - Debit or Credit - - - - + Pay with + + + Debit or Credit + + + + testID="listitem-gap" + /> - - Instant - + + Instant + + - - - - + + + - - - + - - - 1 - - - - - + 1 + + + + - - 2 - - - - - + 2 + + + + - - 3 - - + + 3 + + + - - - - - 4 - - - - - + 4 + + + + - - 5 - - - - - + 5 + + + + - - 6 - - + + 6 + + + - - - - - 7 - - - - - + 7 + + + + - - 8 - - - - - + 8 + + + + - - 9 - - + + 9 + + + - - - - - - - - + + + + - - 0 - - - - - + 0 + + + + - - + testID="keypad-delete-button" + > + + + - - - - + - - Continue + "alignItems": "center", + "alignSelf": "stretch", + "backgroundColor": "#131416", + "borderRadius": 12, + "flexDirection": "row", + "height": 48, + "justifyContent": "center", + "overflow": "hidden", + "paddingHorizontal": 16, + } + } + > + + Continue + + + + + + + + + + + + + + + + + + + + + Buy - + + + + + + + + - + - - - + + + + `; @@ -8872,447 +8892,199 @@ exports[`BuildQuote Component Payment Method Selection does not open payment met } > - - - - - - - - - + - - Buy - - - - - - - - - - - - - - - - - - - - - - - - - - Account is loading... - - - - - ๐Ÿ‡บ๐Ÿ‡ธ + Account is loading... + + + + + ๐Ÿ‡บ๐Ÿ‡ธ + + + US + + + + + + + - US + $0 - + > + 0 + + USDC + - - - - - - $0 - - - 0 - - USDC - - - - - - - + + + > + + - - - + > + + - - - USDC - - + USDC + + - - - - - + + + + - - Pay with - - + Pay with + + + Debit or Credit + + + - Debit or Credit - - - - + testID="listitem-gap" + /> - - Instant - + + Instant + + - - - - + + + - - - + - - - 1 - - - - - - + 1 + + + + + - 2 - - - - - + 2 + + + + - - 3 - - + + 3 + + + - - - - - 4 - - - - - + 4 + + + + - - 5 - - - - - + 5 + + + + - - 6 - - + + 6 + + + - - - - - 7 - - - - - + 7 + + + + - - 8 - - + + 8 + + + + + + + 9 + + + - - - 9 - - - - - - - + + + - - - - - + + 0 + + + + - - 0 - - + + + - + + + + + + + Continue + + + + + + + + + + + + + + - - - - + /> - - - - Continue + Buy - + - - - - - - - - - - - - - -`; - -exports[`BuildQuote Component Payment Method Selection does not open payment method modal when payment methods error occurs 1`] = ` - - - - - - - - - - - - - - - - Buy - - - - - - - + + + + + + + + + + + + - - - - - + + + + + +`; + +exports[`BuildQuote Component Payment Method Selection does not open payment method modal when payment methods error occurs 1`] = ` + + + + - - + - - - - - - Account is loading... - - - - - ๐Ÿ‡บ๐Ÿ‡ธ + Account is loading... + + + + + ๐Ÿ‡บ๐Ÿ‡ธ + + + US + + + + + + + - US + $0 - + > + 0 + + USDC + - - - - - - $0 - - - 0 - - USDC - - - - - - - - - + + + + + - + > + + - - - USDC - - + USDC + + - - - + width={16} + /> + + - - - - + + + - There was an issue fetching the payment methods. - - - Try again + There was an issue fetching the payment methods. - + + + Try again + + + - - - - - Pay with - - + Pay with + + + Debit or Credit + + + - Debit or Credit - - - - + testID="listitem-gap" + /> - - Instant - + + Instant + + - - - - + + + - - - + - - - 1 - - - - - + 1 + + + + - - 2 - - - - - + 2 + + + + - - 3 - - - - - + + 3 + + + + - - - 4 - - - - - + 4 + + + + - - 5 - - - - - + 5 + + + + - - 6 - - + + 6 + + + - - - - - 7 - - - - - + 7 + + + + - - 8 - - - - - + 8 + + + + - - 9 - - + + 9 + + + - - - - - - - - + + + + - - 0 - - - - - + 0 + + + + - - + testID="keypad-delete-button" + > + + + + + + + + Continue + + + + - + + + + + + + + + + + + - + Buy + + + + + + - Continue - - + + + - + - - - + + + + `; @@ -12433,447 +12461,199 @@ exports[`BuildQuote Component Payment Method Selection does not show the duratio } > - - - - - - - - - + - - Buy - - - - - - - - - - - - - - - - - - - - - - - - - Account is loading... - - - - - ๐Ÿ‡บ๐Ÿ‡ธ - - - US + Account is loading... - - - - - - - - $0 - - - 0 - - USDC - - - - + - - - - - - + ๐Ÿ‡บ๐Ÿ‡ธ + + - + + + + + + + + $0 + + + 0 + + USDC + + + + + + + + + + + - + > + + - - - USDC - - + USDC + + - - - - - + + + + - - Pay with - - + Pay with + + + + + + - - - - - - - + - + + + + - - - + - - - 1 - - - - - + 1 + + + + - - 2 - - - - - + 2 + + + + - - 3 - - + + 3 + + + - - - - - 4 - - - - - + 4 + + + + - - 5 - - - - - + 5 + + + + - - 6 - - + + 6 + + + - - - - - 7 - - - - - + 7 + + + + - - 8 - - - - - + 8 + + + + - - 9 - - - - - + + 9 + + + + - - - - - - + + + + - - 0 - - - - - + 0 + + + + - - + testID="keypad-delete-button" + > + + + + + + + + Continue + + + + - + + + + + + + + + + + + - + Buy + + + + + + - Continue - - + + + - + - - - - - + + + + + + `; exports[`BuildQuote Component Payment Method Selection shows the right duration for the selected payment method 1`] = ` @@ -14122,447 +14154,199 @@ exports[`BuildQuote Component Payment Method Selection shows the right duration } > - - - - - - - - - - - - - Buy - - - - - - - - - - - - - - - - - + - - - - - - Account is loading... - - - - - ๐Ÿ‡บ๐Ÿ‡ธ - - - US + Account is loading... - - - - - - - + - $0 - - - 0 - - USDC - + > + + ๐Ÿ‡บ๐Ÿ‡ธ + + + US + + + + - - + + $0 + + + 0 + + USDC + + + - - - - - + + + + + - + > + + - - - USDC - - - - - - - - - - Pay with + USDC - - SEPA Bank Transfer - + width={16} + /> - + + + + } + > - - - 1 to 2 days - - - - - + + SEPA Bank Transfer + + + - + - + + + + 1 to 2 days + + + + + + + + - - - + - - - 1 - - - - - + 1 + + + + - - 2 - - - - - + 2 + + + + - - 3 - - + + 3 + + + - - - - - 4 - - - - - + 4 + + + + - - 5 - - - - - + 5 + + + + - - 6 - - - - - + + 6 + + + + - - - 7 - - - - - + 7 + + + + - - 8 - - - - - + 8 + + + + - - 9 - - + + 9 + + + - - - - - - - - + + + + - - 0 - - - - - + 0 + + + + - - + testID="keypad-delete-button" + > + + + - - - - + - - Continue - - + + Continue + + + - - - - - - - - - - - - -`; - -exports[`BuildQuote Component Region Selection displays EUR currency when selectedRegion is EUR 1`] = ` - + + + + + + + + + + + + + + + Buy + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`BuildQuote Component Region Selection displays EUR currency when selectedRegion is EUR 1`] = ` + - - - - - - - - - + - - Buy - - - - - - - - - - - - - - - - - - - - - - - - - - Account is loading... - - - - - ๐Ÿ‡ฉ๐Ÿ‡ช + Account is loading... + + + + + ๐Ÿ‡ฉ๐Ÿ‡ช + + + DE + + + + + + + - DE + โ‚ฌ0 - + > + 0 + + USDC + - - - - - - โ‚ฌ0 - - - 0 - - USDC - - - - - - - + + + + style={ + { + "flex": 1, + "height": undefined, + "width": undefined, + } + } + testID="token-avatar-image" + /> + - - - + > + + - - - USDC - - + USDC + + - - - - - + + + + - - Pay with - - + Pay with + + + Debit or Credit + + + - Debit or Credit - - - - + testID="listitem-gap" + /> - - Instant - + + Instant + + - - - - + + + - - - + - - - 1 - - - - - + 1 + + + + - - 2 - - - - - + 2 + + + + - - 3 - - + + 3 + + + - - - - - 4 - - - - - + 4 + + + + - - 5 - - - - - + 5 + + + + - - 6 - - + + 6 + + + - - - - - 7 - - - - - + 7 + + + + - - 8 - - - - - + 8 + + + + - - 9 - - + + 9 + + + - - - - - - - - + + + + - - 0 - - - - - + 0 + + + + - - + testID="keypad-delete-button" + > + + + + + + + + Continue + + + + - + + + + + + + + + + + + - + Buy + + + + + + - Continue - - + + + - + - - - + + + + `; @@ -17590,447 +17630,199 @@ exports[`BuildQuote Component Region Selection displays default US region on ini } > - - - - - - - - - + - - - Buy - - - - - - - - - - - - - - - - - - - - - - - - Account is loading... - - - - - ๐Ÿ‡บ๐Ÿ‡ธ + Account is loading... + + + + + ๐Ÿ‡บ๐Ÿ‡ธ + + + US + + + + + + + + $0 + + - US + 0 + + USDC - + + - - - - - - - $0 - - - 0 - - USDC - - - - - - - - - - + + + + + - + > + + - - - USDC - - + USDC + + - - - - - + + + + - - Pay with - - + Pay with + + + Debit or Credit + + + - Debit or Credit - - - - + testID="listitem-gap" + /> - - Instant - + + Instant + + - - - - + + testID="listitemcolumn" + > + + - - - + - - - 1 - - - - - + 1 + + + + - - 2 - - - - - + 2 + + + + - - 3 - - + + 3 + + + - - - - - 4 - - - - - + 4 + + + + - - 5 - - - - - + 5 + + + + - - 6 - - + + 6 + + + - - - - - 7 - - - - - + 7 + + + + - - 8 - - - - - + 8 + + + + - - 9 - - + + 9 + + + - - - - - - - - + + + + - - 0 - - - - - + 0 + + + + - - + testID="keypad-delete-button" + > + + + + + + + + Continue + + + + - + + + + + + + + + + + + - - Continue + Buy - + - - - - - - - + + + + + + + + + + + + + + - - - + + + + `; @@ -19324,447 +19368,199 @@ exports[`BuildQuote Component Region Selection does not open region modal when r } > - - - - - - - - - - - - - Buy - - - - - - - - - - - - - - - - - + - - - - - - Account is loading... - - - - - ๐Ÿ‡บ๐Ÿ‡ธ + Account is loading... - - US - - - - - - - - + - $0 - - - 0 - - USDC - + > + + ๐Ÿ‡บ๐Ÿ‡ธ + + + US + + + + - - + + $0 + + + 0 + + USDC + + + - - - - - + + + + + - + > + + - - - USDC - - + USDC + + - - - - - + + + + - - Pay with - - + Pay with + + + Debit or Credit + + + - Debit or Credit - - - - + testID="listitem-gap" + /> - - Instant - + + Instant + + - - - - + + + - - - + - - - 1 - - - - - + 1 + + + + - - 2 - - - - - + 2 + + + + - - 3 - - + + 3 + + + - - - - - 4 - - - - - + 4 + + + + - - 5 - - - - - + 5 + + + + - - 6 - - + + 6 + + + - - - - - 7 - - - - - + 7 + + + + - - 8 - - - - - + 8 + + + + - - 9 - - + + 9 + + + - - - - - - - - + + + + - - 0 - - - - - + 0 + + + + - - + testID="keypad-delete-button" + > + + + - - - - + - - Continue + "alignItems": "center", + "alignSelf": "stretch", + "backgroundColor": "#131416", + "borderRadius": 12, + "flexDirection": "row", + "height": 48, + "justifyContent": "center", + "opacity": 0.5, + "overflow": "hidden", + "paddingHorizontal": 16, + } + } + > + + Continue + + + + + + + + + + + + + + + + + + + + + Buy - + + + + + + + + - + - - - + + + + `; @@ -21058,447 +21106,199 @@ exports[`BuildQuote Component Region Selection does not open region modal when r } > - - - - - - - - - + - - Buy - - - - - - - - - - - - - - - - - - - - - - - - - - Account is loading... - - - - - ๐Ÿ‡บ๐Ÿ‡ธ + Account is loading... + + + + + ๐Ÿ‡บ๐Ÿ‡ธ + + + US + + + + + + + - US + $0 - + > + 0 + + USDC + - - - - - - $0 - - - 0 - - USDC - - - - - - - + + + > + + - - - + > + + - - - USDC - - - - - - - + USDC + + - + width={16} + /> + + - - There was an issue fetching the regions. - - + + - Try again + There was an issue fetching the regions. - + + + Try again + + + - - - - - Pay with - - + Pay with + + + Debit or Credit + + + - Debit or Credit - - - - + testID="listitem-gap" + /> - - Instant - + + Instant + + - - - - + + + - - - + - - - 1 - - - - - + 1 + + + + - - 2 - - - - - + 2 + + + + - - 3 - - + + 3 + + + - - - - - 4 - - - - - + 4 + + + + - - 5 - - - - - + 5 + + + + - - 6 - - + + 6 + + + - - - - - 7 - - - - - + 7 + + + + - - 8 - - - - - + 8 + + + + - - 9 - - + + 9 + + + - - - - - - - - + + + + - - 0 - - - - - + 0 + + + + - - + testID="keypad-delete-button" + > + + + + + + + + Continue + + + + - + + + + + + + + + + + + - - Continue - - - - - - + [ + { + "color": "#131416", + "fontFamily": "Geist-Bold", + "fontSize": 16, + "fontWeight": 400, + "letterSpacing": 0, + "lineHeight": 24, + }, + undefined, + ] + } + > + Buy + + + + + + + + + + + + + - - - + + + + `; @@ -22885,447 +22937,199 @@ exports[`BuildQuote Component Token Selection does not open token modal when cry } > - - - - - - - - - - - - - Buy - - - - - - - - - - - - - - - - - + - - - - - - Account is loading... - - - - - ๐Ÿ‡บ๐Ÿ‡ธ + Account is loading... + + + + + ๐Ÿ‡บ๐Ÿ‡ธ + + + US + + + + + + + - US + $0 - + > + + - - - - - - $0 - - - - - - - - - - - - + + + + + - + > + + - - - USDC - - + USDC + + - - - - - + + + + - - Pay with - - + Pay with + + + Debit or Credit + + + - Debit or Credit - - - - + testID="listitem-gap" + /> - - Instant - + + Instant + + - - - - + + + - - - + - - - 1 - - - - - + 1 + + + + - - 2 - - - - - + 2 + + + + - - 3 - - + + 3 + + + - - - - - 4 - - - - - + 4 + + + + - - 5 - - - - - + 5 + + + + - - 6 - - + + 6 + + + - - - - - 7 - - - - - + 7 + + + + - - 8 - - - - - + 8 + + + + - - 9 - - + + 9 + + + - - - - - - - - + + + + - - 0 - - - - - + 0 + + + + - - + testID="keypad-delete-button" + > + + + + + + + + Continue + + + + - + + + + + + + + + + + + - - Continue + Buy - + - - - - - - - - - - - - - -`; - -exports[`BuildQuote Component Token Selection does not open token modal when crypto currencies error occurs 1`] = ` - - - - - - - - - - - - - - - - Buy - - - - - - - + + + + + + + + + + + + - - - - - + + + + + +`; + +exports[`BuildQuote Component Token Selection does not open token modal when crypto currencies error occurs 1`] = ` + + + + - - + - - - - - - Account is loading... - - - - - ๐Ÿ‡บ๐Ÿ‡ธ - - - US + Account is loading... - - - - - - - + - $0 - - - - + > + + ๐Ÿ‡บ๐Ÿ‡ธ + + + US + + + + - - + + + $0 + + + + + + - - - - - + + + + + - + > + + - - - USDC - - - - - - - + USDC + + - + width={16} + /> + + - - There was an issue fetching the tokens. - - + + - Try again + There was an issue fetching the tokens. - + + + Try again + + + - - - - - Pay with - - + Pay with + + + Debit or Credit + + + - Debit or Credit - - - - + testID="listitem-gap" + /> - - Instant - + + Instant + + - - - - + + + - - - + - - - 1 - - - - - + 1 + + + + - - 2 - - - - - + 2 + + + + - - 3 - - + + 3 + + + - - - - - 4 - - + + 4 + + + + + + + 5 + + + + + + + 6 + + + - - - 5 - - - - - + 7 + + + + - - 6 - - - - - - - + 8 + + + + - - 7 - - + + 9 + + + - - - 8 - - - - - + + + - - 9 - - - - - - - + 0 + + + + - - + testID="keypad-delete-button" + > + + + - + + + + + + + Continue + + + + + + + + + + + + + - - - 0 - - - - + - - - - + /> - - - + Buy + + + + + + - Continue - - + + + - + - - - + + + + `; @@ -26442,447 +26502,199 @@ exports[`BuildQuote Component User Details Error displays user details error ale } > - - - - - - - - - - - - - Buy - - - - - - - - - - - - - - - - - + - - - - - - - Account is loading... - - - - - - ๐Ÿ‡บ๐Ÿ‡ธ - - - US - - - - - - - - - $0 - - - 0 - - USDC - - - - - - - - - - - - - - - - - - - - USDC - - - - + style={ + [ + { + "flex": 1, + }, + undefined, + ] + } + > + - - - - + Account is loading... + + + + + + ๐Ÿ‡บ๐Ÿ‡ธ + + + US + + + + + + + - There was an issue fetching the user details. + $0 + 0 + + USDC + + + + + + + + + + + + + + + + + + - Try again + USDC - - - - - - + + + + + + - Pay with + There was an issue fetching the user details. - Debit or Credit + + Try again + - + + + + } + > + + Pay with + + + Debit or Credit + + + + - - Instant - + + Instant + + - - - - + + + - - - + - - - 1 - - - - - + 1 + + + + - - 2 - - - - - + 2 + + + + - - 3 - - + + 3 + + + - - - + + + 4 + + + + + + + 5 + + + + - - 4 - - + + 6 + + + - - - 5 - - - - - + 7 + + + + - - 6 - - - - - - - + 8 + + + + - - 7 - - + + 9 + + + - - - 8 - - - - - + + + - - 9 - - - - - - - + 0 + + + + - - + testID="keypad-delete-button" + > + + + - + + + + + + - + + + + + + + + + + + + - - 0 - - - - + - - - - + /> - - - + Buy + + + + + + - Continue - - + + + - + - - - + + + + `; @@ -28257,459 +28321,211 @@ exports[`BuildQuote Component render matches snapshot 1`] = ` } } > - - - - - - - - - - - - - - - Buy - - - - - - - - - - - - - - + - - - + - - - - - - Account is loading... - - - - - ๐Ÿ‡บ๐Ÿ‡ธ + Account is loading... + + + + + ๐Ÿ‡บ๐Ÿ‡ธ + + + US + + + + + + + - US + $0 - + > + 0 + + USDC + - - - - - - $0 - - - 0 - - USDC - - - - - - - - - + style={ + { + "alignItems": "center", + "backgroundColor": "#b4b4b528", + "borderRadius": 100, + "flexDirection": "row", + "gap": 8, + "paddingLeft": 8, + "paddingRight": 12, + "paddingVertical": 8, + } + } + > + + + + + - + > + + - - - USDC - - + USDC + + - - - - - + + + + - - Pay with - - + Pay with + + + Debit or Credit + + + - Debit or Credit - - - - + testID="listitem-gap" + /> - - Instant - + + Instant + + - - - - + + + - - - + - - - 1 - - - - - + + 1 + + + + - - 2 - - - - - + 2 + + + + - - 3 - - + + 3 + + + - - - - - 4 - - - - - + 4 + + + + - - 5 - - - - - + 5 + + + + - - 6 - - + + 6 + + + - - - - - 7 - - - - - + 7 + + + + - - 8 - - - - - + 8 + + + + - - 9 - - - - - + + 9 + + + + - - - - - - + + + + - - 0 - - - - - + 0 + + + + - - + testID="keypad-delete-button" + > + + + + + + + + Continue + + + + - + + + + + + + + + + + + - + Buy + + + + + + - Continue - - + + + - + - - - + + + + `; diff --git a/app/components/UI/Ramp/Deposit/Views/EnterAddress/__snapshots__/EnterAddress.test.tsx.snap b/app/components/UI/Ramp/Deposit/Views/EnterAddress/__snapshots__/EnterAddress.test.tsx.snap index 799e86494d3..3e0152a73ee 100644 --- a/app/components/UI/Ramp/Deposit/Views/EnterAddress/__snapshots__/EnterAddress.test.tsx.snap +++ b/app/components/UI/Ramp/Deposit/Views/EnterAddress/__snapshots__/EnterAddress.test.tsx.snap @@ -20,838 +20,403 @@ exports[`EnterAddress Component displays form validation errors when continue is } > - - - - - - - - - - - - - Verify your identity - - - - - - - - - - - - - + - - - - - - + } + enableAutomaticScroll={true} + enableOnAndroid={false} + enableResetScrollToCoords={true} + extraHeight={75} + extraScrollHeight={0} + getScrollResponder={[Function]} + handleOnScroll={[Function]} + keyboardDismissMode="interactive" + keyboardOpeningTime={250} + keyboardShouldPersistTaps="handled" + keyboardSpace={0} + onScroll={[Function]} + resetKeyboardSpace={[Function]} + scrollEventThrottle={1} + scrollForExtraHeightOnAndroid={[Function]} + scrollIntoView={[Function]} + scrollToEnd={[Function]} + scrollToFocusedInput={[Function]} + scrollToPosition={[Function]} + showsVerticalScrollIndicator={false} + update={[Function]} + viewIsInsideTabBar={false} + > + + - + + + + + + > + + Enter your address + + + Use your most recent permanent address. + + - - - - - Enter your address - - - Use your most recent permanent address. - - - - - Address line 1 - - - - - - - - Address line 1 is required - - - - - Address line 2 (optional) - - - - - - - - - - + - City + Address line 1 @@ -969,7 +535,7 @@ exports[`EnterAddress Component displays form validation errors when continue is } } > - City is required + Address line 1 is required @@ -1003,200 +567,75 @@ exports[`EnterAddress Component displays form validation errors when continue is ] } > - State/Region + Address line 2 (optional) - - - Select state - - - - - - State/Region is required - - - - - - - Postal code - - - - - - Postal code is required - - - Country - - + City + + - - - ๐Ÿ‡บ๐Ÿ‡ธ - + + - - + + + + State/Region + + + + + Select state + + + + + + State/Region is required + + + + + + + Postal code + + + + > + + + + + Postal code is required + + + + + Country + + + + + ๐Ÿ‡บ๐Ÿ‡ธ + + + + + - - - - + + - - - - - + + + - Transak encrypts and stores this information. - - + - MetaMask never receives or uses your data. - + + Transak encrypts and stores this information. + + + MetaMask never receives or uses your data. + + - - + + Continue + + + + + + + + + + + + + + + - + + + + + + - Continue - - - + > + Verify your identity + + + + - + - - - + + + + `; @@ -1569,805 +1573,389 @@ exports[`EnterAddress Component prefills form data when previousFormData is prov } > - - - - - - - - - - - - - Verify your identity - - - - - - - - - - - - - + - - - - - - + } + enableAutomaticScroll={true} + enableOnAndroid={false} + enableResetScrollToCoords={true} + extraHeight={75} + extraScrollHeight={0} + getScrollResponder={[Function]} + handleOnScroll={[Function]} + keyboardDismissMode="interactive" + keyboardOpeningTime={250} + keyboardShouldPersistTaps="handled" + keyboardSpace={0} + onScroll={[Function]} + resetKeyboardSpace={[Function]} + scrollEventThrottle={1} + scrollForExtraHeightOnAndroid={[Function]} + scrollIntoView={[Function]} + scrollToEnd={[Function]} + scrollToFocusedInput={[Function]} + scrollToPosition={[Function]} + showsVerticalScrollIndicator={false} + update={[Function]} + viewIsInsideTabBar={false} + > + - + - + - + - - - - Enter your address - - + - Use your most recent permanent address. - - - - - Address line 1 - + testID="deposit-progress-step-3" + /> + - - - - - - - + - Address line 2 (optional) - - - - - + Use your most recent permanent address. + - - @@ -2399,7 +1985,7 @@ exports[`EnterAddress Component prefills form data when previousFormData is prov ] } > - City + Address line 1 @@ -2495,9 +2082,7 @@ exports[`EnterAddress Component prefills form data when previousFormData is prov "flexDirection": "column", "marginBottom": 16, }, - { - "flex": 1, - }, + undefined, ] } > @@ -2519,112 +2104,10 @@ exports[`EnterAddress Component prefills form data when previousFormData is prov ] } > - State/Region + Address line 2 (optional) - - - New York - - - - - - - - - - Postal code - - - - Country - - + City + + - - + + + + + + - ๐Ÿ‡บ๐Ÿ‡ธ - - + }, + { + "marginBottom": 6, + }, + ] + } + > + State/Region + - + testID="state-input" + > + + New York + + + - - - - - - - - - - - + + Postal code + + + + + + + + + + Country + + + + + ๐Ÿ‡บ๐Ÿ‡ธ + + + + + + + + + + + + + + + } + > - - Transak encrypts and stores this information. - - + + + - MetaMask never receives or uses your data. - + + Transak encrypts and stores this information. + + + MetaMask never receives or uses your data. + + + + + Continue + + + - + + + + + + + + + + - + + + + + + - Continue - - - + > + Verify your identity + + + + - + - - - + + + + `; @@ -3046,807 +3054,389 @@ exports[`EnterAddress Component render matches snapshot 1`] = ` } > - - - - - - - - - - - - - Verify your identity - - - - - - - - - - - - - + - - - - - - + } + enableAutomaticScroll={true} + enableOnAndroid={false} + enableResetScrollToCoords={true} + extraHeight={75} + extraScrollHeight={0} + getScrollResponder={[Function]} + handleOnScroll={[Function]} + keyboardDismissMode="interactive" + keyboardOpeningTime={250} + keyboardShouldPersistTaps="handled" + keyboardSpace={0} + onScroll={[Function]} + resetKeyboardSpace={[Function]} + scrollEventThrottle={1} + scrollForExtraHeightOnAndroid={[Function]} + scrollIntoView={[Function]} + scrollToEnd={[Function]} + scrollToFocusedInput={[Function]} + scrollToPosition={[Function]} + showsVerticalScrollIndicator={false} + update={[Function]} + viewIsInsideTabBar={false} + > + - + - + - + - - - - Enter your address - - + - Use your most recent permanent address. - - - - - Address line 1 - + testID="deposit-progress-step-3" + /> + - - - - - - - + - Address line 2 (optional) - - - - - + Use your most recent permanent address. + - - @@ -3878,7 +3466,7 @@ exports[`EnterAddress Component render matches snapshot 1`] = ` ] } > - City + Address line 1 @@ -3975,119 +3564,7 @@ exports[`EnterAddress Component render matches snapshot 1`] = ` "flexDirection": "column", "marginBottom": 16, }, - { - "flex": 1, - }, - ] - } - > - - State/Region - - - - - Select state - - - - - - - - @@ -4109,7 +3586,7 @@ exports[`EnterAddress Component render matches snapshot 1`] = ` ] } > - Postal code + Address line 2 (optional) @@ -4202,315 +3679,850 @@ exports[`EnterAddress Component render matches snapshot 1`] = ` - - Country - - + City + + + + + + + + + + State/Region + + + + + Select state + + + + + + + + + + Postal code + - + + + + + + - ๐Ÿ‡บ๐Ÿ‡ธ - - + }, + { + "marginBottom": 6, + }, + ] + } + > + Country + - + testID="textfield-startacccessory" + > + + ๐Ÿ‡บ๐Ÿ‡ธ + + + + + - - - - + + - - - - - + + + - Transak encrypts and stores this information. - - + - MetaMask never receives or uses your data. - + + Transak encrypts and stores this information. + + + MetaMask never receives or uses your data. + + + + + Continue + + + - + + + + + + + + + + - + + + + + + - Continue - - - + > + Verify your identity + + + + - + - - - + + + + `; @@ -4535,927 +4547,388 @@ exports[`EnterAddress Component shows text input for state when region is not US } > - - - - - - - - - - - - - Verify your identity - - - - - - - - - - - - - + - - - - - - - - - - - - - - - Enter your address - - - Use your most recent permanent address. - - + } + enableAutomaticScroll={true} + enableOnAndroid={false} + enableResetScrollToCoords={true} + extraHeight={75} + extraScrollHeight={0} + getScrollResponder={[Function]} + handleOnScroll={[Function]} + keyboardDismissMode="interactive" + keyboardOpeningTime={250} + keyboardShouldPersistTaps="handled" + keyboardSpace={0} + onScroll={[Function]} + resetKeyboardSpace={[Function]} + scrollEventThrottle={1} + scrollForExtraHeightOnAndroid={[Function]} + scrollIntoView={[Function]} + scrollToEnd={[Function]} + scrollToFocusedInput={[Function]} + scrollToPosition={[Function]} + showsVerticalScrollIndicator={false} + update={[Function]} + viewIsInsideTabBar={false} + > + - - Address line 1 - - - - - - - - - - Address line 2 (optional) - - - - - - - - + + - City - + testID="deposit-progress-step-2" + /> - - - - + "borderRadius": 2, + "flex": 1, + "height": 4, + }, + { + "backgroundColor": "#2c3dc5", + }, + ], + undefined, + ] + } + testID="deposit-progress-step-3" + /> + + + + Enter your address + + + Use your most recent permanent address. + @@ -5488,7 +4959,7 @@ exports[`EnterAddress Component shows text input for state when region is not US ] } > - State/Region + Address line 1 - - @@ -5619,7 +5079,7 @@ exports[`EnterAddress Component shows text input for state when region is not US ] } > - Postal code + Address line 2 (optional) @@ -5712,315 +5172,871 @@ exports[`EnterAddress Component shows text input for state when region is not US - - Country - - + City + + + + + + + + + + State/Region + + + + + + + + + + + + Postal code + + + + + + + + - - - ๐Ÿ‡ซ๐Ÿ‡ท - - + }, + { + "marginBottom": 6, + }, + ] + } + > + Country + - + testID="textfield-startacccessory" + > + + ๐Ÿ‡ซ๐Ÿ‡ท + + + + + - - - - + + - - - - - + + + - Transak encrypts and stores this information. - - + - MetaMask never receives or uses your data. - + + Transak encrypts and stores this information. + + + MetaMask never receives or uses your data. + + + + + Continue + + + - + + + + + + + + + + - + + + + + + - Continue - - - + > + Verify your identity + + + + - + - - - + + + + `; diff --git a/app/components/UI/Ramp/Deposit/Views/EnterEmail/__snapshots__/EnterEmail.test.tsx.snap b/app/components/UI/Ramp/Deposit/Views/EnterEmail/__snapshots__/EnterEmail.test.tsx.snap index 21cb46ee5ff..83545594d79 100644 --- a/app/components/UI/Ramp/Deposit/Views/EnterEmail/__snapshots__/EnterEmail.test.tsx.snap +++ b/app/components/UI/Ramp/Deposit/Views/EnterEmail/__snapshots__/EnterEmail.test.tsx.snap @@ -20,378 +20,199 @@ exports[`EnterEmail Component render matches snapshot 1`] = ` } > - - - - - - - - - - - - - Verify your identity - - - - - - - - - - - - - + - - - - + - + + + ] + } + testID="deposit-progress-step-2" + /> + + + + Enter your email + + + We'll email you a six-digit verification code to securely log in or create an account with Transak. + + + + } + } + > + + + + + + - - Enter your email - - + Send email + + + - We'll email you a six-digit verification code to securely log in or create an account with Transak. - + /> + + + + + + + + + + - - - + "height": 24, + "width": 24, + }, + undefined, + ] + } + /> - - - - Send email + Verify your identity - - + + + - + - - - + + + + `; @@ -729,378 +733,199 @@ exports[`EnterEmail Component renders error message snapshot when API call fails } > - - - - - - - - - - - - - Verify your identity - - - - - - - - - - - - - + - - - - + - + + + ] + } + testID="deposit-progress-step-2" + /> + + + + Enter your email + + + We'll email you a six-digit verification code to securely log in or create an account with Transak. + + + + } + } + > + + + + + API Error + + + + - - Enter your email - - + Send email + + + - We'll email you a six-digit verification code to securely log in or create an account with Transak. - + /> + + + + + + + + + + - - - - - - API Error - + /> + - - - - Send email + Verify your identity - - + + + - + - - - + + + + `; @@ -1451,378 +1459,199 @@ exports[`EnterEmail Component renders loading state snapshot 1`] = ` } > - - - - - - - - - - - - - Verify your identity - - - - - - - - - - - - - + - - - - - + - + + + + + + > + + Enter your email + + + We'll email you a six-digit verification code to securely log in or create an account with Transak. + + + + + + + + + - - Enter your email - - + Send email + + + - We'll email you a six-digit verification code to securely log in or create an account with Transak. - + /> + + + + + + + + + + - - - + "height": 24, + "width": 24, + }, + undefined, + ] + } + /> - - - - Send email + Verify your identity - - + + + - + - - - + + + + `; @@ -2160,378 +2172,199 @@ exports[`EnterEmail Component renders validation error snapshot invalid email 1` } > - - - - - - - - - - - - - Verify your identity - - - - - - - - - - - - - + - - - - + - + + + ] + } + testID="deposit-progress-step-2" + /> + + + + Enter your email + + + We'll email you a six-digit verification code to securely log in or create an account with Transak. + + + + } + } + > + + + + + Please enter a valid email address + + + + - - Enter your email - - + Send email + + + - We'll email you a six-digit verification code to securely log in or create an account with Transak. - + /> + + + + + + + + + + - - - - - - Please enter a valid email address - + /> + - - - - Send email + Verify your identity - - + + + - + - - - + + + + `; diff --git a/app/components/UI/Ramp/Deposit/Views/KycProcessing/__snapshots__/KycProcessing.test.tsx.snap b/app/components/UI/Ramp/Deposit/Views/KycProcessing/__snapshots__/KycProcessing.test.tsx.snap index d82bfa5a1de..c99697fc2fe 100644 --- a/app/components/UI/Ramp/Deposit/Views/KycProcessing/__snapshots__/KycProcessing.test.tsx.snap +++ b/app/components/UI/Ramp/Deposit/Views/KycProcessing/__snapshots__/KycProcessing.test.tsx.snap @@ -20,378 +20,199 @@ exports[`KycProcessing Component render matches snapshot 1`] = ` } > - - - - - - - - - - - - - Verify your identity - - - - - - - - - - - - - + - - - - + - + + + ] + } + testID="deposit-progress-step-2" + /> + + + > + + + Hang tight... + + + This should only take about 2 minutes. + + + + - - - Hang tight... - - - This should only take about 2 minutes. - + /> - - - + + + + + + + + + + + + + + + Verify your identity + + + + + - + - - - + + + + `; @@ -615,378 +619,199 @@ exports[`KycProcessing Component renders approved state snapshot 1`] = ` } > - - - - - - - - - - - - - Verify your identity - - - - - - - - - - - - - + - - - - + - + + + ] + } + testID="deposit-progress-step-2" + /> + + + > + + + + + You're verified + + + Now you can complete your deposit. + + + + - - - - + Complete your order + + + - You're verified - - + + + + + + + + + + + - Now you can complete your deposit. - + + - - - - Complete your order + Verify your identity - - + + + - + - - - + + + + `; @@ -1268,378 +1276,199 @@ exports[`KycProcessing Component renders error state snapshot 1`] = ` } > - - - - - - - - - - - - - Verify your identity - - - - - - - - - - - - - + - - - - + - + + + ] + } + testID="deposit-progress-step-2" + /> + + + > + + + We were unable to verify your identity. + + + Network error + + + + + + + Retry verification + + - + + + + + + + + + + - We were unable to verify your identity. - - - Network error - + + - - - - Retry verification + Verify your identity - - + + + - + - - - + + + + `; @@ -1907,577 +1919,581 @@ exports[`KycProcessing Component renders loading state snapshot 1`] = ` } > - - - - - - - - - - - - - Verify your identity - - - - - - - - - - - - - + - - - + + + + + + + + + + + + Hang tight... + + + This should only take about 2 minutes. + + + + + + + + + + + + + + + - + - - - + - - - - - Hang tight... - - - This should only take about 2 minutes. - + /> + - - - + + Verify your identity + + + + + - + - - - + + + + `; @@ -2502,378 +2518,199 @@ exports[`KycProcessing Component renders pending forms state snapshot 1`] = ` } > - - - - - - - - - - - - - Verify your identity - - - - - - - - - - - - - + - - - - + - + + + ] + } + testID="deposit-progress-step-2" + /> + + + > + + + We were unable to verify your identity. + + + Try uploading your documents again and resubmit for verification. + + + + + + + Retry verification + + - + + + + + + + + + + - We were unable to verify your identity. - - - Try uploading your documents again and resubmit for verification. - + + - - - - Retry verification + Verify your identity - - + + + - + - - - + + + + `; @@ -3139,380 +3159,201 @@ exports[`KycProcessing Component renders rejected state snapshot 1`] = ` undefined, ] } - > - - - - - - - - - - - - - - Verify your identity - - - - - - - - - - + - - - + - - - - + - + + + ] + } + testID="deposit-progress-step-2" + /> + + + > + + + We were unable to verify your identity. + + + Try uploading your documents again and resubmit for verification. + + + + + + + Retry verification + + - + + + + + + + + + + - We were unable to verify your identity. - - - Try uploading your documents again and resubmit for verification. - + + - - - - Retry verification + Verify your identity - - + + + - + - - - + + + + `; diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/ConfigurationModal/ConfigurationModal.tsx b/app/components/UI/Ramp/Deposit/Views/Modals/ConfigurationModal/ConfigurationModal.tsx index 06de34cb0fb..98df694f8b0 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/ConfigurationModal/ConfigurationModal.tsx +++ b/app/components/UI/Ramp/Deposit/Views/Modals/ConfigurationModal/ConfigurationModal.tsx @@ -68,7 +68,7 @@ function ConfigurationModal() { preferred_provider: buttonClickData.preferred_provider, order_count: buttonClickData.order_count, }); - navigation.dangerouslyGetParent()?.dangerouslyGetParent()?.goBack(); + navigation.getParent()?.getParent()?.goBack(); goToAggregator(); }, [ navigation, diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/ConfigurationModal/__snapshots__/ConfigurationModal.test.tsx.snap b/app/components/UI/Ramp/Deposit/Views/Modals/ConfigurationModal/__snapshots__/ConfigurationModal.test.tsx.snap index 0616ccc2771..6e9a8504e14 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/ConfigurationModal/__snapshots__/ConfigurationModal.test.tsx.snap +++ b/app/components/UI/Ramp/Deposit/Views/Modals/ConfigurationModal/__snapshots__/ConfigurationModal.test.tsx.snap @@ -20,351 +20,331 @@ exports[`ConfigurationModal render matches snapshot 1`] = ` } > - - - + + /> + + - + - ConfigurationModal - + + ConfigurationModal + + + - - - - - + - - - - - - - + /> + + - + - + + + - Settings - - - - - + Settings + + + + - - + > + + + - - - - - - - + + + - + - View order history - + + View order history + + - - - - + - - - - + + + - + - Contact support - + + Contact support + + - - - - + - - - - - + + + - More ways to buy - - + - Switch to the classic version - + + More ways to buy + + + Switch to the classic version + + - - + + @@ -828,9 +851,9 @@ exports[`ConfigurationModal render matches snapshot 1`] = ` - - - + + + `; diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/ErrorDetailsModal/__snapshots__/ErrorDetailsModal.test.tsx.snap b/app/components/UI/Ramp/Deposit/Views/Modals/ErrorDetailsModal/__snapshots__/ErrorDetailsModal.test.tsx.snap index 03e87c0db7e..c5bc2a8d588 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/ErrorDetailsModal/__snapshots__/ErrorDetailsModal.test.tsx.snap +++ b/app/components/UI/Ramp/Deposit/Views/Modals/ErrorDetailsModal/__snapshots__/ErrorDetailsModal.test.tsx.snap @@ -20,351 +20,331 @@ exports[`ErrorDetailsModal renders correctly and matches snapshot 1`] = ` } > - - - + + /> + + - + - ErrorDetailsModal - + + ErrorDetailsModal + + + - - - - - + - - - - - - - + /> + + - + - - + - - Error details - - - - - + + Error details + + + + + + + + + - - - - - + + - This is a test error message. - + + This is a test error message. + + - - + + @@ -583,9 +606,9 @@ exports[`ErrorDetailsModal renders correctly and matches snapshot 1`] = ` - - - + + + `; diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/IncompatibleAccountTokenModal/__snapshots__/IncompatibleAccountTokenModal.test.tsx.snap b/app/components/UI/Ramp/Deposit/Views/Modals/IncompatibleAccountTokenModal/__snapshots__/IncompatibleAccountTokenModal.test.tsx.snap index a5b74ed560b..b72d9fe3681 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/IncompatibleAccountTokenModal/__snapshots__/IncompatibleAccountTokenModal.test.tsx.snap +++ b/app/components/UI/Ramp/Deposit/Views/Modals/IncompatibleAccountTokenModal/__snapshots__/IncompatibleAccountTokenModal.test.tsx.snap @@ -20,351 +20,331 @@ exports[`IncompatibleAccountTokenModal renders the modal with the correct title } > - - - + + /> + + - + - IncompatibleAccountTokenModal - + + IncompatibleAccountTokenModal + + + - - - - - + - - - - - - - + /> + + - + - + + + - Switch your account - - - - - - + + + + - + > + + + - - - - The Ethereum account youโ€™re on doesn't support the selected token. Switch your account or token to continue. - - - I understand + The Ethereum account youโ€™re on doesn't support the selected token. Switch your account or token to continue. - + + + I understand + + + @@ -587,9 +610,9 @@ exports[`IncompatibleAccountTokenModal renders the modal with the correct title - - - + + + `; diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/PaymentMethodSelectorModal/__snapshots__/PaymentMethodSelectorModal.test.tsx.snap b/app/components/UI/Ramp/Deposit/Views/Modals/PaymentMethodSelectorModal/__snapshots__/PaymentMethodSelectorModal.test.tsx.snap index ba2e23a2f9c..010d9e3fd56 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/PaymentMethodSelectorModal/__snapshots__/PaymentMethodSelectorModal.test.tsx.snap +++ b/app/components/UI/Ramp/Deposit/Views/Modals/PaymentMethodSelectorModal/__snapshots__/PaymentMethodSelectorModal.test.tsx.snap @@ -20,351 +20,331 @@ exports[`PaymentMethodSelectorModal Component renders correctly and matches snap } > - - - + + /> + + - + - PaymentMethodSelectorModal - + + PaymentMethodSelectorModal + + + - - - - - + - - - - - - - + /> + + - + + + + - - Select a payment method - + + Select a payment method + + - - - + + - + testID="button-icon" + > + + - - - - - + + - - - - - + + + - + - Debit or Credit - - - - + Debit or Credit + + + - + - Instant - + + Instant + + - - - - - - + + + - - - - - + + + - + - Apple Pay - - - - + Apple Pay + + + - + - Instant - + + Instant + + - - + + - - + + @@ -900,9 +923,9 @@ exports[`PaymentMethodSelectorModal Component renders correctly and matches snap - - - + + + `; diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/RegionSelectorModal/__snapshots__/RegionSelectorModal.test.tsx.snap b/app/components/UI/Ramp/Deposit/Views/Modals/RegionSelectorModal/__snapshots__/RegionSelectorModal.test.tsx.snap index ddfd97d4aab..488571b5d29 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/RegionSelectorModal/__snapshots__/RegionSelectorModal.test.tsx.snap +++ b/app/components/UI/Ramp/Deposit/Views/Modals/RegionSelectorModal/__snapshots__/RegionSelectorModal.test.tsx.snap @@ -20,351 +20,331 @@ exports[`RegionSelectorModal Component handles empty regions array from navigati } > - - - + + /> + + - + - RegionSelectorModal - + + RegionSelectorModal + + + - - - - - + - - - - - - - + /> + + - + + + + - - Select a region - - - - - - + > + Select a region + - - - + + + + + + + + - - - - + testID="textfield-startacccessory" + > + + + + + - - - - - + + - No region matches - + + No region matches + + - - + + @@ -751,9 +774,9 @@ exports[`RegionSelectorModal Component handles empty regions array from navigati - - - + + + `; @@ -778,351 +801,331 @@ exports[`RegionSelectorModal Component receives and uses regions from navigation } > - - - + + /> + + - + - RegionSelectorModal - + + RegionSelectorModal + + + - - - - - + - - - - - - - + /> + + - + + + + - - Select a region - + + Select a region + + - - - + + - + testID="button-icon" + > + + - - - - - - + + + + > + + - - - - - + + - - - ๐Ÿ‡ฉ๐Ÿ‡ช - - - - - Germany - + > + ๐Ÿ‡ฉ๐Ÿ‡ช + + + + + Germany + + - - - - - + + - - - ๐Ÿ‡จ๐Ÿ‡ฆ - - - - - Canada - + > + ๐Ÿ‡จ๐Ÿ‡ฆ + + + + + Canada + + - - + + - - + + @@ -1705,9 +1751,9 @@ exports[`RegionSelectorModal Component receives and uses regions from navigation - - - + + + `; @@ -1732,351 +1778,331 @@ exports[`RegionSelectorModal Component render matches snapshot 1`] = ` } > - - - + + /> + + - + - RegionSelectorModal - + + RegionSelectorModal + + + - - - - - + - - - - - + } + > - - - + /> + + - + + + + - - Select a region - + + Select a region + + - - - + + - + testID="button-icon" + > + + - - - - - - + testID="textfield-startacccessory" + > + + + + + - - - - - + + - - - ๐Ÿ‡บ๐Ÿ‡ธ - - - - - United States - + > + ๐Ÿ‡บ๐Ÿ‡ธ + + + + + United States + + - - - - - - + + + - - - ๐Ÿ‡ฉ๐Ÿ‡ช - - - - - Germany - + > + ๐Ÿ‡ฉ๐Ÿ‡ช + + + + + Germany + + - - - - - + + - - - ๐Ÿ‡จ๐Ÿ‡ฆ - - - - - Canada - + > + ๐Ÿ‡จ๐Ÿ‡ฆ + + + + + Canada + + - - - - - + + - - - ๐Ÿ‡ซ๐Ÿ‡ท - - - - - France - + > + ๐Ÿ‡ซ๐Ÿ‡ท + + + + + France + + - - + + - - + + @@ -2891,9 +2960,9 @@ exports[`RegionSelectorModal Component render matches snapshot 1`] = ` - - - + + + `; @@ -2918,351 +2987,331 @@ exports[`RegionSelectorModal Component render matches snapshot when search has n } > - - - + + /> + + - + - RegionSelectorModal - + + RegionSelectorModal + + + - - - - - + - - - - - - - + /> + + - + + + + - - Select a region - + + Select a region + + - - - + + - + testID="button-icon" + > + + - - - - - - - - - - + + + + + + + + + - - - - - + + - No region matches - + + No region matches + + - - + + @@ -3689,9 +3781,9 @@ exports[`RegionSelectorModal Component render matches snapshot when search has n - - - + + + `; @@ -3716,351 +3808,331 @@ exports[`RegionSelectorModal Component render matches snapshot when searching fo } > - - - + + /> + + - + - RegionSelectorModal - + + RegionSelectorModal + + + - - - - - + - - - - - - - + /> + + - + + + + - - Select a region - + + Select a region + + - - - + + - + testID="button-icon" + > + + - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + - - ๐Ÿ‡ฉ๐Ÿ‡ช - - - - - Germany - + > + ๐Ÿ‡ฉ๐Ÿ‡ช + + + + + Germany + + - - + + - - + + @@ -4575,9 +4690,9 @@ exports[`RegionSelectorModal Component render matches snapshot when searching fo - - - + + + `; @@ -4602,351 +4717,331 @@ exports[`RegionSelectorModal Component render matches snapshot with allRegionsSe } > - - - + + /> + + - + - RegionSelectorModal - + + RegionSelectorModal + + + - - - - - + - - - - + + - - + } + > - - - + /> + + - + + + + - - Select a region - + + Select a region + + - - - + + - + testID="button-icon" + > + + - - - - - - + testID="textfield-startacccessory" + > + + + + + - - - - - + + - - - ๐Ÿ‡บ๐Ÿ‡ธ - - - - - United States - - - - - - - + ๐Ÿ‡บ๐Ÿ‡ธ + + + + + United States + + + + + + + - - - - + + + - - - ๐Ÿ‡ฉ๐Ÿ‡ช - - - - - Germany - + > + ๐Ÿ‡ฉ๐Ÿ‡ช + + + + + Germany + + - - - - - + + - - - ๐Ÿ‡จ๐Ÿ‡ฆ - - - - - Canada - + > + ๐Ÿ‡จ๐Ÿ‡ฆ + + + + + Canada + + - - - - - + + - - - ๐Ÿ‡ซ๐Ÿ‡ท - - - - - France - + > + ๐Ÿ‡ซ๐Ÿ‡ท + + + + + France + + - - + + - - + + @@ -5761,9 +5899,9 @@ exports[`RegionSelectorModal Component render matches snapshot with allRegionsSe - - - + + + `; @@ -5788,435 +5926,435 @@ exports[`RegionSelectorModal Component render matches snapshot with custom selec } > - - - + + /> + + - + - RegionSelectorModal - + + RegionSelectorModal + + + - - - - - + - - - - + > + + - - - + > + + + - - Select a region - + + Select a region + + - - - + + - + testID="button-icon" + > + + - - - - - - + testID="textfield-startacccessory" + > + + + + + - - - - - + + - - - ๐Ÿ‡บ๐Ÿ‡ธ - - - - - United States - + > + ๐Ÿ‡บ๐Ÿ‡ธ + + + + + United States + + - - - - - + + - - - ๐Ÿ‡ฉ๐Ÿ‡ช - - - - - Germany - + > + ๐Ÿ‡ฉ๐Ÿ‡ช + + + + + Germany + + - - - - - - + + + - - - ๐Ÿ‡จ๐Ÿ‡ฆ - - - - - Canada - + > + ๐Ÿ‡จ๐Ÿ‡ฆ + + + + + Canada + + - - - - - + + - - - ๐Ÿ‡ซ๐Ÿ‡ท - - - - - France - + > + ๐Ÿ‡ซ๐Ÿ‡ท + + + + + France + + - - + + - - + + @@ -6947,9 +7108,9 @@ exports[`RegionSelectorModal Component render matches snapshot with custom selec - - - + + + `; @@ -6974,351 +7135,331 @@ exports[`RegionSelectorModal Component sorts recommended regions to the top when } > - - - + + /> + + - + - RegionSelectorModal - + + RegionSelectorModal + + + - - - - - + - - - - - - - + /> + + - + + + + - - Select a region - + + Select a region + + - - - + + - + testID="button-icon" + > + + - - - - - - + testID="textfield-startacccessory" + > + + + + + - - - - - + + - - - ๐Ÿ‡บ๐Ÿ‡ธ - - - - - United States - + > + ๐Ÿ‡บ๐Ÿ‡ธ + + + + + United States + + - - - - - - + + + - - - ๐Ÿ‡ฉ๐Ÿ‡ช - - - - - Germany - + > + ๐Ÿ‡ฉ๐Ÿ‡ช + + + + + Germany + + - - - - - + + - - - ๐Ÿ‡จ๐Ÿ‡ฆ - - - - - Canada - + > + ๐Ÿ‡จ๐Ÿ‡ฆ + + + + + Canada + + - - - - - + + - - - ๐Ÿ‡ซ๐Ÿ‡ท - - - - - France - + > + ๐Ÿ‡ซ๐Ÿ‡ท + + + + + France + + - - + + - - + + @@ -8133,9 +8317,9 @@ exports[`RegionSelectorModal Component sorts recommended regions to the top when - - - + + + `; diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/SsnInfoModal/__snapshots__/SsnInfoModal.test.tsx.snap b/app/components/UI/Ramp/Deposit/Views/Modals/SsnInfoModal/__snapshots__/SsnInfoModal.test.tsx.snap index 073c2728036..6564c814ca6 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/SsnInfoModal/__snapshots__/SsnInfoModal.test.tsx.snap +++ b/app/components/UI/Ramp/Deposit/Views/Modals/SsnInfoModal/__snapshots__/SsnInfoModal.test.tsx.snap @@ -20,351 +20,331 @@ exports[`SsnInfoModal Component renders correctly and matches snapshot 1`] = ` } > - - - + + /> + + - + - SsnInfoModal - + + SsnInfoModal + + + - - - - - + - - - - - - - + /> + + - + + + + - - Why we ask - + + Why we ask + + - - - + + - + testID="button-icon" + > + + - - - - U.S. laws require Transak to check your identity before changing dollars into crypto. This means U.S. customers need to provide a Social Security number. - + + U.S. laws require Transak to check your identity before changing dollars into crypto. This means U.S. customers need to provide a Social Security number. + + @@ -602,9 +625,9 @@ exports[`SsnInfoModal Component renders correctly and matches snapshot 1`] = ` - - - + + + `; diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/StateSelectorModal/__snapshots__/StateSelectorModal.test.tsx.snap b/app/components/UI/Ramp/Deposit/Views/Modals/StateSelectorModal/__snapshots__/StateSelectorModal.test.tsx.snap index 30be8b664c9..7269fd27772 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/StateSelectorModal/__snapshots__/StateSelectorModal.test.tsx.snap +++ b/app/components/UI/Ramp/Deposit/Views/Modals/StateSelectorModal/__snapshots__/StateSelectorModal.test.tsx.snap @@ -20,351 +20,331 @@ exports[`StateSelectorModal Component Snapshot Tests renders cleared search stat } > - - - + + /> + + - + - StateSelectorModal - + + StateSelectorModal + + + - - - - - + - - - - - - - + /> + + - + + + + - - Select a state - - - - - - + > + Select a state + - - - + + + + + + + + - - - - - - - - - - - - - - - + - + + + + + + + + + + + + - - California - + + California + + - - + + - - + + @@ -846,9 +869,9 @@ exports[`StateSelectorModal Component Snapshot Tests renders cleared search stat - - - + + + `; @@ -873,351 +896,331 @@ exports[`StateSelectorModal Component Snapshot Tests renders empty state when no } > - - - + + /> + + - + - StateSelectorModal - + + StateSelectorModal + + + - - - - - + - - - - - - - + /> + + - + + + + - - Select a state - + + Select a state + + - - - + + - + testID="state-selector-close-button" + > + + - - - - - - - - - - - - - - - - + - + + + - No states match "Nonexistent State" - + + + + - - + + + + + No states match "Nonexistent State" + + + + + + @@ -1643,9 +1689,9 @@ exports[`StateSelectorModal Component Snapshot Tests renders empty state when no - - - + + + `; @@ -1670,351 +1716,331 @@ exports[`StateSelectorModal Component Snapshot Tests renders filtered state when } > - - - + + /> + + - + - StateSelectorModal - + + StateSelectorModal + + + - - - - - + - - - - - - - + /> + + - + + + + - - Select a state - - - - - - + Select a state + + + + + + - + testID="state-selector-close-button" + > + + - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + - - California - + + California + + - - + + - - + + @@ -2496,9 +2565,9 @@ exports[`StateSelectorModal Component Snapshot Tests renders filtered state when - - - + + + `; @@ -2523,351 +2592,331 @@ exports[`StateSelectorModal Component Snapshot Tests renders filtered state when } > - - - + + /> + + - + - StateSelectorModal - + + StateSelectorModal + + + - - - - - + - - - - - - - + /> + + - + + + + - - Select a state - + + Select a state + + - - - + + - + testID="state-selector-close-button" + > + + - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + - - California - + + California + + - - + + - - + + @@ -3349,9 +3441,9 @@ exports[`StateSelectorModal Component Snapshot Tests renders filtered state when - - - + + + `; @@ -3376,351 +3468,331 @@ exports[`StateSelectorModal Component Snapshot Tests renders initial state corre } > - - - + + /> + + - + - StateSelectorModal - + + StateSelectorModal + + + - - - - - + - - - - - - - + /> + + - + + + + - - Select a state - + + Select a state + + - - - + + - + testID="state-selector-close-button" + > + + - - - - - - + + + + + testID="textfieldsearch" + value="" + /> + - - - - - + + - - - California - + + California + + - - - - - + + - - - Florida - + + Florida + + - - - - - + + - - - New York - + + New York + + - - - - - + + - - - Texas - + + Texas + + - - - - - + + - - - Washington - + + Washington + + - - + + - - + + @@ -4466,9 +4581,9 @@ exports[`StateSelectorModal Component Snapshot Tests renders initial state corre - - - + + + `; @@ -4493,351 +4608,331 @@ exports[`StateSelectorModal Component Snapshot Tests renders partial search resu } > - - - + + /> + + - + - StateSelectorModal - + + StateSelectorModal + + + - - - - - + - - - - - - - + /> + + - + + + + - - Select a state - + + Select a state + + - - - + + - + testID="state-selector-close-button" + > + + - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + - - California - + + California + + - - + + - - + + @@ -5319,9 +5457,9 @@ exports[`StateSelectorModal Component Snapshot Tests renders partial search resu - - - + + + `; diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/TokenSelectorModal/__snapshots__/TokenSelectorModal.test.tsx.snap b/app/components/UI/Ramp/Deposit/Views/Modals/TokenSelectorModal/__snapshots__/TokenSelectorModal.test.tsx.snap index 1f0b32370c8..d57e9e8a1fe 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/TokenSelectorModal/__snapshots__/TokenSelectorModal.test.tsx.snap +++ b/app/components/UI/Ramp/Deposit/Views/Modals/TokenSelectorModal/__snapshots__/TokenSelectorModal.test.tsx.snap @@ -20,351 +20,331 @@ exports[`TokenSelectorModal Component displays empty state when no tokens match } > - - - + + /> + + - + - TokenSelectorModal - + + TokenSelectorModal + + + - - - - - + - - - - - - - + /> + + - + + + + - - Select token - + + Select token + + - - - + + - + testID="button-icon" + > + + - - - - - + + - - - - + + + - - - + + + + > + + - - - All networks - - + All networks + + - - - - - + width={12} + /> + + + + - - - - - - - - - - - - - - + - - + + + + - No tokens match "Nonexistent Token" - - + width={20} + /> + - + - + + + + + + + No tokens match "Nonexistent Token" + + + + + + + @@ -999,9 +1022,9 @@ exports[`TokenSelectorModal Component displays empty state when no tokens match - - - + + + `; @@ -1026,351 +1049,331 @@ exports[`TokenSelectorModal Component displays network filter selector when pres } > - - - + + /> + + - + - TokenSelectorModal - + + TokenSelectorModal + + + - - - - - + - - - - - - - + /> + + - + + + + - - Select network - + + Select network + + - - - + + - + testID="button-icon" + > + + - - - - Deselect all - - - - - - + Deselect all + + + + + - - - - - - - - - + + + + + + testID="listitem-gap" + /> - + > + + - - - - + - Ethereum - - - - - - - - + Ethereum + + + + + + + - - - - - - - - - + + + + + + testID="listitem-gap" + /> - + > + + - - - - + - Bitcoin - + + Bitcoin + + - - - - - + + - - - - - - - - - + + + + + + testID="listitem-gap" + /> - + > + + - - - - + - Solana - + + Solana + + - - + + - - - - + - - Apply - - + + Apply + + + @@ -2205,9 +2251,9 @@ exports[`TokenSelectorModal Component displays network filter selector when pres - - - + + + `; @@ -2232,351 +2278,331 @@ exports[`TokenSelectorModal Component renders correctly and matches snapshot 1`] } > - - - + + /> + + - + - TokenSelectorModal - + + TokenSelectorModal + + + - - - - - + - - - - - - - + /> + + - + + + + - - Select token - + + Select token + + - - - + + - + testID="button-icon" + > + + - - - - - + + - - - - + + + - - - + + + + > + + - - - All networks - - + All networks + + - - - - - + width={12} + /> + + + + - - - - - - - - - - - + + + - + + + + + + + - - - - - + + + + + - - - - - - - - - + + + + + + + - USD Coin - - + - USDC - + + USD Coin + + + USDC + + - - - - - - + + + - - - - - - + + + + + - + > + + - - - - - Tether USD - - + - USDT - + + Tether USD + + + USDT + + - - - - - + + - - - - - - + + + + + - + > + + - - - - - Bitcoin - - + - BTC - + + Bitcoin + + + BTC + + - - - - - + + - - - - - - + + + + + - + > + + - - - - - Ethereum - - + - ETH - + + Ethereum + + + ETH + + - - - - - + + - - - - - - + + + + + - + > + + - - - - - USD Coin - - + - USDC - + + USD Coin + + + USDC + + - - + + - - + + @@ -4306,9 +4375,9 @@ exports[`TokenSelectorModal Component renders correctly and matches snapshot 1`] - - - + + + `; diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedRegionModal/UnsupportedRegionModal.test.tsx b/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedRegionModal/UnsupportedRegionModal.test.tsx index 66170791d09..690258983dd 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedRegionModal/UnsupportedRegionModal.test.tsx +++ b/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedRegionModal/UnsupportedRegionModal.test.tsx @@ -11,7 +11,7 @@ const mockGoToAggregator = jest.fn(); const mockUseDepositSDK = jest.fn(); const mockGoBack = jest.fn(); const mockPop = jest.fn(); -const mockDangerouslyGetParent = jest.fn(() => ({ +const mockGetParent = jest.fn(() => ({ pop: mockPop, })); @@ -22,7 +22,7 @@ jest.mock('@react-navigation/native', () => { useNavigation: () => ({ navigate: mockNavigate, goBack: mockGoBack, - dangerouslyGetParent: mockDangerouslyGetParent, + getParent: mockGetParent, isFocused: jest.fn(() => true), }), }; @@ -97,7 +97,7 @@ describe('UnsupportedRegionModal', () => { const buyCryptoButton = getByText('Buy crypto'); fireEvent.press(buyCryptoButton); - expect(mockDangerouslyGetParent).toHaveBeenCalled(); + expect(mockGetParent).toHaveBeenCalled(); expect(mockPop).toHaveBeenCalled(); expect(mockGoToAggregator).toHaveBeenCalledWith(); }); diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedRegionModal/UnsupportedRegionModal.tsx b/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedRegionModal/UnsupportedRegionModal.tsx index 6cc95eb5943..ea289fb142b 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedRegionModal/UnsupportedRegionModal.tsx +++ b/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedRegionModal/UnsupportedRegionModal.tsx @@ -51,7 +51,7 @@ function UnsupportedRegionModal() { const handleNavigateToBuy = useCallback(() => { sheetRef.current?.onCloseBottomSheet(() => { // @ts-expect-error navigation prop mismatch - navigation.dangerouslyGetParent()?.pop(); + navigation.getParent()?.pop(); goToAggregator(); }); }, [navigation, goToAggregator]); diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedRegionModal/__snapshots__/UnsupportedRegionModal.test.tsx.snap b/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedRegionModal/__snapshots__/UnsupportedRegionModal.test.tsx.snap index 9eb1fa59b86..a4b35b19fd8 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedRegionModal/__snapshots__/UnsupportedRegionModal.test.tsx.snap +++ b/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedRegionModal/__snapshots__/UnsupportedRegionModal.test.tsx.snap @@ -20,512 +20,511 @@ exports[`UnsupportedRegionModal handles missing region gracefully 1`] = ` } > - - - + + /> + + - + - DepositUnsupportedRegionModal - + + DepositUnsupportedRegionModal + + + - - - - - + - - - - + > + + - - - - + + + - Region not supported - - - - - - + + + + - + > + + + - - - - It looks like you're in: - @@ -540,120 +539,144 @@ exports[`UnsupportedRegionModal handles missing region gracefully 1`] = ` "lineHeight": 24, } } - /> + > + It looks like you're in: + + + + + + > + We're working hard to expand coverage to your region. In the meantime, there may be other ways for you to get crypto. + + - - We're working hard to expand coverage to your region. In the meantime, there may be other ways for you to get crypto. - - - - - - - Change region - - - - + Change region + + + - Buy crypto - - + + Buy crypto + + + @@ -663,9 +686,9 @@ exports[`UnsupportedRegionModal handles missing region gracefully 1`] = ` - - - + + + `; @@ -690,512 +713,511 @@ exports[`UnsupportedRegionModal render match snapshot 1`] = ` } > - - - + > + + + - + - DepositUnsupportedRegionModal - + + DepositUnsupportedRegionModal + + + - - - - - + - - - - + > + + - - - - + + + - Region not supported - - - - - - + + + + - + > + + + - - - - It looks like you're in: - @@ -1211,123 +1233,147 @@ exports[`UnsupportedRegionModal render match snapshot 1`] = ` } } > - ๐Ÿ‡ง๐Ÿ‡ท + It looks like you're in: + + + ๐Ÿ‡ง๐Ÿ‡ท + + + Brazil + + - Brazil + We're working hard to expand coverage to your region. In the meantime, there may be other ways for you to get crypto. + - - We're working hard to expand coverage to your region. In the meantime, there may be other ways for you to get crypto. - - - - - - - Change region - - - - + Change region + + + - Buy crypto - - + + Buy crypto + + + @@ -1337,9 +1383,9 @@ exports[`UnsupportedRegionModal render match snapshot 1`] = ` - - - + + + `; diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedStateModal/UnsupportedStateModal.test.tsx b/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedStateModal/UnsupportedStateModal.test.tsx index d0ee86dfc43..e1ee069b97b 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedStateModal/UnsupportedStateModal.test.tsx +++ b/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedStateModal/UnsupportedStateModal.test.tsx @@ -10,7 +10,7 @@ import Routes from '../../../../../../../constants/navigation/Routes'; const mockUseDepositSDK = jest.fn(); const mockNavigate = jest.fn(); const mockGoToAggregator = jest.fn(); -const mockDangerouslyGetParent = jest.fn(); +const mockGetParent = jest.fn(); const mockPop = jest.fn(); const mockGoBack = jest.fn(); const mockOnStateSelect = jest.fn(); @@ -39,7 +39,7 @@ jest.mock('@react-navigation/native', () => { useNavigation: () => ({ goBack: mockGoBack, navigate: mockNavigate, - dangerouslyGetParent: mockDangerouslyGetParent, + getParent: mockGetParent, isFocused: jest.fn(() => true), }), }; @@ -77,7 +77,7 @@ function render(Component: React.ComponentType) { describe('UnsupportedStateModal', () => { beforeEach(() => { jest.clearAllMocks(); - mockDangerouslyGetParent.mockReturnValue({ + mockGetParent.mockReturnValue({ pop: mockPop, }); }); @@ -102,7 +102,7 @@ describe('UnsupportedStateModal', () => { const tryAnotherOptionButton = getByText('Try another option'); fireEvent.press(tryAnotherOptionButton); - expect(mockDangerouslyGetParent).toHaveBeenCalled(); + expect(mockGetParent).toHaveBeenCalled(); expect(mockPop).toHaveBeenCalled(); expect(mockGoToAggregator).toHaveBeenCalledWith(); }); diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedStateModal/UnsupportedStateModal.tsx b/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedStateModal/UnsupportedStateModal.tsx index 198e45fb907..9d332e1ac0c 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedStateModal/UnsupportedStateModal.tsx +++ b/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedStateModal/UnsupportedStateModal.tsx @@ -72,7 +72,7 @@ function UnsupportedStateModal() { const handleTryAnotherOption = useCallback(() => { closeBottomSheetAndNavigate(() => { // @ts-expect-error navigation prop mismatch - navigation.dangerouslyGetParent()?.pop(); + navigation.getParent()?.pop(); goToAggregator(); }); }, [closeBottomSheetAndNavigate, navigation, goToAggregator]); diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedStateModal/__snapshots__/UnsupportedStateModal.test.tsx.snap b/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedStateModal/__snapshots__/UnsupportedStateModal.test.tsx.snap index a422fb5d9f6..d06e11de4af 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedStateModal/__snapshots__/UnsupportedStateModal.test.tsx.snap +++ b/app/components/UI/Ramp/Deposit/Views/Modals/UnsupportedStateModal/__snapshots__/UnsupportedStateModal.test.tsx.snap @@ -20,513 +20,511 @@ exports[`UnsupportedStateModal render match snapshot 1`] = ` } > - - - + + /> + + - + - DepositUnsupportedStateModal - + + DepositUnsupportedStateModal + + + - - - - - + - - - - + > + + - - - - + + + - Region not supported - - - - - - + + + + - + > + + + - - - - You've selected: - @@ -542,13 +540,52 @@ exports[`UnsupportedStateModal render match snapshot 1`] = ` } } > - ๐Ÿ‡บ๐Ÿ‡ธ + You've selected: + + + ๐Ÿ‡บ๐Ÿ‡ธ + + + New York + + - New York + We're working hard to expand coverage to your region. In the meantime, there are other ways for you to get crypto. + - - We're working hard to expand coverage to your region. In the meantime, there are other ways for you to get crypto. - - - - - - - Change region - - - - + Change region + + + - Try another option - - + + Try another option + + + @@ -667,9 +690,9 @@ exports[`UnsupportedStateModal render match snapshot 1`] = ` - - - + + + `; diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/WebviewModal/__snapshots__/KycWebviewModal.test.tsx.snap b/app/components/UI/Ramp/Deposit/Views/Modals/WebviewModal/__snapshots__/KycWebviewModal.test.tsx.snap index 45751092679..467db073490 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/WebviewModal/__snapshots__/KycWebviewModal.test.tsx.snap +++ b/app/components/UI/Ramp/Deposit/Views/Modals/WebviewModal/__snapshots__/KycWebviewModal.test.tsx.snap @@ -20,307 +20,330 @@ exports[`KycWebviewModal render matches snapshot 1`] = ` } > - - - + + /> + + - + - DepositKycWebviewModal - + + DepositKycWebviewModal + + + - - - - - + - - WebviewModal + + WebviewModal + - - - + + + `; diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/WebviewModal/__snapshots__/WebviewModal.test.tsx.snap b/app/components/UI/Ramp/Deposit/Views/Modals/WebviewModal/__snapshots__/WebviewModal.test.tsx.snap index 0610149010f..446ff044562 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/WebviewModal/__snapshots__/WebviewModal.test.tsx.snap +++ b/app/components/UI/Ramp/Deposit/Views/Modals/WebviewModal/__snapshots__/WebviewModal.test.tsx.snap @@ -20,351 +20,331 @@ exports[`WebviewModal Component renders correctly and matches snapshot 1`] = ` } > - - - + + /> + + - + - WebviewModal - + + WebviewModal + + + - - - - - + - - - - - - - + /> + + - + - - + testID="header" + > + + + + + + - + testID="button-icon" + > + + - - - - - + > + + + + @@ -587,9 +610,9 @@ exports[`WebviewModal Component renders correctly and matches snapshot 1`] = ` - - - + + + `; @@ -614,351 +637,331 @@ exports[`WebviewModal Component should display error view when webview HTTP erro } > - - - + + /> + + - + + + WebviewModal + + + - WebviewModal - + /> - - - - - + - - - - - - - + /> + + - + - - + testID="header" + > + + + + + + - + testID="button-icon" + > + + - - - - - - - - + + + - There was an error - - + There was an error + + - Webview received error: 404 - + > + Webview received error: 404 + + - - + + - - + + @@ -1284,9 +1330,9 @@ exports[`WebviewModal Component should display error view when webview HTTP erro - - - + + + `; diff --git a/app/components/UI/Ramp/Deposit/Views/OtpCode/__snapshots__/OtpCode.test.tsx.snap b/app/components/UI/Ramp/Deposit/Views/OtpCode/__snapshots__/OtpCode.test.tsx.snap index 95d7b617849..3aba7c31722 100644 --- a/app/components/UI/Ramp/Deposit/Views/OtpCode/__snapshots__/OtpCode.test.tsx.snap +++ b/app/components/UI/Ramp/Deposit/Views/OtpCode/__snapshots__/OtpCode.test.tsx.snap @@ -20,328 +20,340 @@ exports[`OtpCode Screen calls resendOtp when resend link is clicked and properly } > - - - + + /> + + - + - Enter six-digit code - + + Enter six-digit code + + + - - - - - + - - - + + + + + + + > + Enter six-digit code + + + Enter the code we sent to test@email.com. If you don't see it, check your spam folder. + + > + + Paste + + - + + + + - - - Enter six-digit code - - - Enter the code we sent to test@email.com. If you don't see it, check your spam folder. - - - - Paste - - - - - + - - - + + + + + - - - + + + + + + - - - + + + - - - @@ -713,159 +797,98 @@ exports[`OtpCode Screen calls resendOtp when resend link is clicked and properly accessibilityRole="text" style={ { - "color": "#131416", - "fontFamily": "Geist-Bold", - "fontSize": 24, - "fontWeight": "bold", + "color": "#66676a", + "fontFamily": "Geist-Regular", + "fontSize": 16, "letterSpacing": 0, - "lineHeight": 30, - "textAlign": "center", + "lineHeight": 24, + "marginRight": 4, } } - /> + > + Resend code in 30 seconds + - + + - - Resend code in 30 seconds - - - - - - - - + Submit + + + - Submit - - - + /> + - - + + - - - + + + `; @@ -890,328 +913,340 @@ exports[`OtpCode Screen render matches snapshot 1`] = ` } > - - - + + /> + + - + - Enter six-digit code - + + Enter six-digit code + + + - - - - - + - - - - + - + - + - - - Enter six-digit code - - - Enter the code we sent to test@email.com. If you don't see it, check your spam folder. - - + ] + } + testID="deposit-progress-step-2" + /> + + + + Enter six-digit code + - Paste + Enter the code we sent to test@email.com. If you don't see it, check your spam folder. - - + testID="otp-code-paste-button" + > + Paste + - + + + - - - + + + + + + - - - + + + + + + - - - + + + @@ -1583,179 +1690,118 @@ exports[`OtpCode Screen render matches snapshot 1`] = ` accessibilityRole="text" style={ { - "color": "#131416", - "fontFamily": "Geist-Bold", - "fontSize": 24, - "fontWeight": "bold", + "color": "#66676a", + "fontFamily": "Geist-Regular", + "fontSize": 16, "letterSpacing": 0, - "lineHeight": 30, - "textAlign": "center", + "lineHeight": 24, + "marginRight": 4, } } - /> + > + Didn't receive the code? + + + + Resend it + + - + + - - Didn't receive the code? - - - Resend it + Submit - - - - - - - - Submit - - - + /> + - - + + - - - + + + `; @@ -1780,328 +1826,340 @@ exports[`OtpCode Screen renders cooldown timer snapshot after resending OTP 1`] } > - - - + + /> + + + - + - Enter six-digit code - + + Enter six-digit code + + + - - - - - + - - - - + - + - + - - - Enter six-digit code - - - Enter the code we sent to test@email.com. If you don't see it, check your spam folder. - - + ] + } + testID="deposit-progress-step-2" + /> + + - Paste + Enter six-digit code - - - - - - - - - - - + Enter the code we sent to test@email.com. If you don't see it, check your spam folder. + + testID="otp-code-paste-button" + > + Paste + - + + + + + + + + + + + + + + + + + + @@ -2473,159 +2603,98 @@ exports[`OtpCode Screen renders cooldown timer snapshot after resending OTP 1`] accessibilityRole="text" style={ { - "color": "#131416", - "fontFamily": "Geist-Bold", - "fontSize": 24, - "fontWeight": "bold", + "color": "#66676a", + "fontFamily": "Geist-Regular", + "fontSize": 16, "letterSpacing": 0, - "lineHeight": 30, - "textAlign": "center", + "lineHeight": 24, + "marginRight": 4, } } - /> + > + Resend code in 30 seconds + - + + - - Resend code in 30 seconds - - - - - - - - + Submit + + + - Submit - - - + /> + - - + + - - - + + + `; @@ -2650,328 +2719,340 @@ exports[`OtpCode Screen renders error snapshot when API call fails 1`] = ` } > - - - + + /> + + - + - Enter six-digit code - + + Enter six-digit code + + + - - - - - + + /> - - - - + - + - + - - - Enter six-digit code - - - Enter the code we sent to test@email.com. If you don't see it, check your spam folder. - - + + + + "color": "#131416", + "fontFamily": "Geist-Bold", + "fontSize": 24, + "fontWeight": "bold", + "letterSpacing": 0, + "lineHeight": 32, + "marginTop": 24, + } + } + > + Enter six-digit code + - Paste + Enter the code we sent to test@email.com. If you don't see it, check your spam folder. - - - 1 + Paste - + + 1 + + + - 2 - - - - + 2 + + + + + 3 + + + - 3 - - - - + 4 + + + + + 5 + + + - 4 - - - - + 6 + + + - 5 - + testID="otp-code-input" + textContentType="oneTimeCode" + underlineColorAndroid="transparent" + value="123456" + /> + + API call failed + @@ -3353,194 +3522,117 @@ exports[`OtpCode Screen renders error snapshot when API call fails 1`] = ` accessibilityRole="text" style={ { - "color": "#131416", - "fontFamily": "Geist-Bold", - "fontSize": 24, - "fontWeight": "bold", + "color": "#66676a", + "fontFamily": "Geist-Regular", + "fontSize": 16, "letterSpacing": 0, - "lineHeight": 30, - "textAlign": "center", + "lineHeight": 24, + "marginRight": 4, } } > - 6 + Didn't receive the code? + + + Resend it + + - - - API call failed - + + - - Didn't receive the code? - - - Resend it + Submit - - - - - - - - Submit - - - + /> + - - + + - - - + + + `; @@ -3565,328 +3657,340 @@ exports[`OtpCode Screen renders resend error snapshot when resend fails 1`] = ` } > - - - + + /> + + - + - Enter six-digit code - + + Enter six-digit code + + + - - - - - + - - - - - + style={ + [ + { + "padding": 15, + "paddingHorizontal": 16, + }, + { + "flex": 1, + }, + undefined, + ] + } + > + - + + - - - Enter six-digit code - - - Enter the code we sent to test@email.com. If you don't see it, check your spam folder. - - + + + + "color": "#131416", + "fontFamily": "Geist-Bold", + "fontSize": 24, + "fontWeight": "bold", + "letterSpacing": 0, + "lineHeight": 32, + "marginTop": 24, + } + } + > + Enter six-digit code + - Paste + Enter the code we sent to test@email.com. If you don't see it, check your spam folder. - - + testID="otp-code-paste-button" + > + Paste + - + + + - - - + + + + + + - - - + + + + + + - - - + + + @@ -4258,179 +4434,118 @@ exports[`OtpCode Screen renders resend error snapshot when resend fails 1`] = ` accessibilityRole="text" style={ { - "color": "#131416", - "fontFamily": "Geist-Bold", - "fontSize": 24, - "fontWeight": "bold", + "color": "#66676a", + "fontFamily": "Geist-Regular", + "fontSize": 16, "letterSpacing": 0, - "lineHeight": 30, - "textAlign": "center", + "lineHeight": 24, + "marginRight": 4, } } - /> + > + Error resending code. + + + + Contact support + + - + + - - Error resending code. - - - Contact support + Submit - - - - - - - - Submit - - - + /> + - - + + - - - + + + `; diff --git a/app/components/UI/Ramp/Deposit/Views/Root/__snapshots__/Root.test.tsx.snap b/app/components/UI/Ramp/Deposit/Views/Root/__snapshots__/Root.test.tsx.snap index fa8775c5553..5d585e24670 100644 --- a/app/components/UI/Ramp/Deposit/Views/Root/__snapshots__/Root.test.tsx.snap +++ b/app/components/UI/Ramp/Deposit/Views/Root/__snapshots__/Root.test.tsx.snap @@ -20,326 +20,349 @@ exports[`Root Component render matches snapshot 1`] = ` } > - - - + + /> + + - + - DepositRoot - + + DepositRoot + + + - - - - - + - - + + + - - - + + + `; diff --git a/app/components/UI/Ramp/Deposit/Views/VerifyIdentity/__snapshots__/VerifyIdentity.test.tsx.snap b/app/components/UI/Ramp/Deposit/Views/VerifyIdentity/__snapshots__/VerifyIdentity.test.tsx.snap index 5dfdd7f8498..0a3337200b1 100644 --- a/app/components/UI/Ramp/Deposit/Views/VerifyIdentity/__snapshots__/VerifyIdentity.test.tsx.snap +++ b/app/components/UI/Ramp/Deposit/Views/VerifyIdentity/__snapshots__/VerifyIdentity.test.tsx.snap @@ -20,415 +20,408 @@ exports[`VerifyIdentity Component renders verify identity screen with all conten } > - - - + + /> + + - + - Verify your identity - + + Verify your identity + + + - - - - - + - - - - - - - - + + - Verify your identity - - - To deposit cash for the first time, your identity must be verified. - - + + > + Verify your identity + - Transak + To deposit cash for the first time, your identity must be verified. - will facilitate your deposit and your data is sent directly to Transak. We do not process the data you share. - - - Check out our + > + + Transak + + will facilitate your deposit and your data is sent directly to Transak. We do not process the data you share. + - privacy policy + Check out our + + privacy policy + + to learn more. - to learn more. - + - - - - - - + + + - By clicking the button below, you agree to - Transak's Terms of Use + By clicking the button below, you agree to + + Transak's Terms of Use + + and + + Privacy Policy + + . - and - - Privacy Policy - - . - - - + Agree and continue + + + - Agree and continue - - - + /> + - - + + - - - + + + `; diff --git a/app/components/UI/Ramp/Deposit/components/ErrorView/__snapshots__/ErrorView.test.tsx.snap b/app/components/UI/Ramp/Deposit/components/ErrorView/__snapshots__/ErrorView.test.tsx.snap index 15409e8d672..37130c6c9f9 100644 --- a/app/components/UI/Ramp/Deposit/components/ErrorView/__snapshots__/ErrorView.test.tsx.snap +++ b/app/components/UI/Ramp/Deposit/components/ErrorView/__snapshots__/ErrorView.test.tsx.snap @@ -20,444 +20,467 @@ exports[`ErrorView Component renders with all props and matches snapshot 1`] = ` } > - - - + + /> + + - + - ErrorView - + + ErrorView + + + - - - - - + - - - - - - - Custom Error Title - - - Custom error description - - + + + + > + Custom Error Title + - custom cta label + Custom error description - + + + custom cta label + + + - - + + - - - + + + `; @@ -482,408 +505,431 @@ exports[`ErrorView Component renders with default props and matches snapshot 1`] } > - - - + + /> + + - + - ErrorView - + + ErrorView + + + - - - - - + - - - - - - + + + - There was an error - - + There was an error + + - There was an error processing your deposit. Please contact support if the problem continues. - + > + There was an error processing your deposit. Please contact support if the problem continues. + + - - + + - - - + + + `; diff --git a/app/components/UI/Ramp/Deposit/hooks/useDepositRouting.test.ts b/app/components/UI/Ramp/Deposit/hooks/useDepositRouting.test.ts index 83ece7161cf..dacf2a21b84 100644 --- a/app/components/UI/Ramp/Deposit/hooks/useDepositRouting.test.ts +++ b/app/components/UI/Ramp/Deposit/hooks/useDepositRouting.test.ts @@ -7,10 +7,6 @@ import useHandleNewOrder from './useHandleNewOrder'; import { createEnterEmailNavDetails } from '../Views/EnterEmail/EnterEmail'; import { endTrace } from '../../../../../util/trace'; -jest.mock('@react-navigation/compat', () => ({ - withNavigation: jest.fn((component) => component), -})); - const mockUseDepositSdkMethodInitialState = { data: null, error: null as string | null, diff --git a/app/components/UI/Ramp/Deposit/routes/index.tsx b/app/components/UI/Ramp/Deposit/routes/index.tsx index 67924b58f87..6f8b7260395 100644 --- a/app/components/UI/Ramp/Deposit/routes/index.tsx +++ b/app/components/UI/Ramp/Deposit/routes/index.tsx @@ -3,7 +3,7 @@ import { createStackNavigator, StackNavigationOptions, } from '@react-navigation/stack'; -import { RouteProp } from '@react-navigation/native'; +import { RouteProp, useRoute } from '@react-navigation/native'; import { BuyQuote } from '@consensys/native-ramps-sdk'; import { DepositSDKProvider } from '../sdk'; import { DepositNavigationParams } from '../types'; @@ -52,6 +52,9 @@ const clearStackNavigatorOptions = { animationEnabled: false, }; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ScreenComponent = React.ComponentType; + const RootStack = createStackNavigator(); const Stack = createStackNavigator(); const ModalsStack = createStackNavigator(); @@ -69,15 +72,13 @@ const getAnimationOptions = ({ return { animationEnabled }; }; -interface MainRoutesProps { - route: RouteProp<{ params: DepositNavigationParams }, 'params'>; -} - -const MainRoutes = ({ route }: MainRoutesProps) => { +const MainRoutes = () => { + const route = + useRoute>(); const parentParams = route.params; return ( - + { const DepositModalsRoutes = () => ( ( - + { const actual = jest.requireActual('@react-navigation/native'); @@ -70,8 +72,19 @@ jest.mock('../../../../../util/theme', () => ({ }), })); +let capturedDepositNavbarOnClose: (() => void) | undefined; jest.mock('../../../Navbar', () => ({ - getDepositNavbarOptions: jest.fn(() => ({})), + getDepositNavbarOptions: jest.fn( + ( + _navigation: unknown, + _params: unknown, + _theme: unknown, + onClose?: () => void, + ) => { + capturedDepositNavbarOnClose = onClose; + return { header: () => null }; + }, + ), })); jest.mock('../../../../../util/Logger', () => ({ @@ -79,11 +92,13 @@ jest.mock('../../../../../util/Logger', () => ({ log: jest.fn(), })); -const mockCallbackBaseUrl = - 'https://on-ramp-content.uat-api.cx.metamask.io/regions/fake-callback'; +jest.mock('../../../../../util/browser', () => ({ + shouldStartLoadWithRequest: jest.fn(() => true), +})); jest.mock('../../Aggregator/sdk', () => ({ - callbackBaseUrl: mockCallbackBaseUrl, + callbackBaseUrl: + 'https://on-ramp-content.api.cx.metamask.io/regions/fake-callback', useRampSDK: jest.fn(() => null), })); @@ -94,10 +109,14 @@ let capturedOnNavigationStateChange: jest.mock('@metamask/react-native-webview', () => { // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires -- jest mock factory const { View, Button } = require('react-native'); + const getCallbackBaseUrl = () => + // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires -- resolve mocked sdk at press time (avoids jest hoist / TDZ with outer consts) + require('../../Aggregator/sdk').callbackBaseUrl as string; return { WebView: ({ onNavigationStateChange, onHttpError, + onShouldStartLoadWithRequest, testID, }: { onNavigationStateChange?: (state: { @@ -107,6 +126,7 @@ jest.mock('@metamask/react-native-webview', () => { onHttpError?: (e: { nativeEvent: { url: string; statusCode: number }; }) => void; + onShouldStartLoadWithRequest?: (req: { url: string }) => boolean; testID?: string; }) => { capturedOnNavigationStateChange = onNavigationStateChange; @@ -117,7 +137,7 @@ jest.mock('@metamask/react-native-webview', () => { title="TriggerCallback" onPress={() => onNavigationStateChange?.({ - url: `${mockCallbackBaseUrl}?orderId=123`, + url: `${getCallbackBaseUrl()}?orderId=123`, loading: false, }) } @@ -127,7 +147,7 @@ jest.mock('@metamask/react-native-webview', () => { title="TriggerCallbackEmptyQuery" onPress={() => onNavigationStateChange?.({ - url: mockCallbackBaseUrl, + url: getCallbackBaseUrl(), loading: false, }) } @@ -137,7 +157,7 @@ jest.mock('@metamask/react-native-webview', () => { title="TriggerCallbackLoading" onPress={() => onNavigationStateChange?.({ - url: `${mockCallbackBaseUrl}?orderId=123`, + url: `${getCallbackBaseUrl()}?orderId=123`, loading: true, }) } @@ -164,6 +184,27 @@ jest.mock('@metamask/react-native-webview', () => { }) } /> + )} - {isSrpWordSuggestionsEnabled && - currentStep === 0 && - isKeyboardVisible && ( - - { - srpInputGridRef.current?.handleSuggestionSelect(word); - }} - /> - - )} + {currentStep === 0 && isKeyboardVisible && ( + + { + srpInputGridRef.current?.handleSuggestionSelect(word); + }} + /> + + )} ); diff --git a/app/components/Views/ImportFromSecretRecoveryPhrase/index.test.tsx b/app/components/Views/ImportFromSecretRecoveryPhrase/index.test.tsx index 1f13179dbcc..11ee9b7443e 100644 --- a/app/components/Views/ImportFromSecretRecoveryPhrase/index.test.tsx +++ b/app/components/Views/ImportFromSecretRecoveryPhrase/index.test.tsx @@ -94,13 +94,6 @@ const initialState = { }, }; -jest.mock( - '../../../selectors/featureFlagController/importSrpWordSuggestion', - () => ({ - selectImportSrpWordSuggestionEnabledFlag: () => true, - }), -); - const mockIsEnabled = jest.fn().mockReturnValue(true); jest.mock('../../hooks/useAnalytics/useAnalytics', () => { diff --git a/app/components/Views/ImportNewSecretRecoveryPhrase/index.test.tsx b/app/components/Views/ImportNewSecretRecoveryPhrase/index.test.tsx index 9987408b472..7cbe3443317 100644 --- a/app/components/Views/ImportNewSecretRecoveryPhrase/index.test.tsx +++ b/app/components/Views/ImportNewSecretRecoveryPhrase/index.test.tsx @@ -141,14 +141,6 @@ const initialState = { }, }; -// Mock the feature flag selector to return true -jest.mock( - '../../../selectors/featureFlagController/importSrpWordSuggestion', - () => ({ - selectImportSrpWordSuggestionEnabledFlag: () => true, - }), -); - describe('ImportNewSecretRecoveryPhrase', () => { afterEach(() => { jest.clearAllMocks(); diff --git a/app/components/Views/ImportNewSecretRecoveryPhrase/index.tsx b/app/components/Views/ImportNewSecretRecoveryPhrase/index.tsx index 25483da44d7..44f964c3fc9 100644 --- a/app/components/Views/ImportNewSecretRecoveryPhrase/index.tsx +++ b/app/components/Views/ImportNewSecretRecoveryPhrase/index.tsx @@ -53,7 +53,6 @@ import Logger from '../../../util/Logger'; import { v4 as uuidv4 } from 'uuid'; import SrpInputGrid, { SrpInputGridRef } from '../../UI/SrpInputGrid'; import SrpWordSuggestions from '../../UI/SrpWordSuggestions'; -import { selectImportSrpWordSuggestionEnabledFlag } from '../../../selectors/featureFlagController/importSrpWordSuggestion'; import { isSRPLengthValid, SPACE_CHAR } from '../../../util/srp/srpInputUtils'; import { validateSRP, @@ -78,11 +77,6 @@ const ImportNewSecretRecoveryPhrase = () => { const [error, setError] = useState(''); const [currentInputWord, setCurrentInputWord] = useState(''); - // Feature flag for SRP word suggestions - const isSrpWordSuggestionsEnabled = useSelector( - selectImportSrpWordSuggestionEnabledFlag, - ); - const isKeyboardVisible = useKeyboardState((state) => state.isVisible); const hdKeyrings = useSelector(selectHDKeyrings); @@ -329,7 +323,7 @@ const ImportNewSecretRecoveryPhrase = () => { {strings('import_new_secret_recovery_phrase.cta_text')} - {isSrpWordSuggestionsEnabled && isKeyboardVisible && ( + {isKeyboardVisible && ( - - - + + /> + + - + - ImportPrivateKey - + + ImportPrivateKey + + + - - - - - + - - - - - + + - - + + + - + testID="close-button-on-account-screen" + > + + - - - - - - - - - Import account - + /> + + + - Imported private keys are backed up to your account and sync automatically when you sign in with the same Google or Apple login. + Import account - + + @@ -583,7 +578,7 @@ exports[`ImportPrivateKey render matches snapshot 1`] = ` accessibilityRole="text" style={ { - "color": "#4459ff", + "color": "#66676a", "fontFamily": "Geist-Regular", "fontSize": 16, "letterSpacing": 0, @@ -591,168 +586,196 @@ exports[`ImportPrivateKey render matches snapshot 1`] = ` } } > - Learn more - + Imported private keys are backed up to your account and sync automatically when you sign in with the same Google or Apple login. - about how imported keys work. - + + + Learn more + + + about how imported keys work. + + - - - - + - - or scan a QR code - - + + or scan a QR code + + + - - - - - Import - - + + Import + + + - - - - + + + + - - - + + + `; diff --git a/app/components/Views/ImportPrivateKeySuccess/__snapshots__/index.test.tsx.snap b/app/components/Views/ImportPrivateKeySuccess/__snapshots__/index.test.tsx.snap index 3f0040a1225..9810aed81fb 100644 --- a/app/components/Views/ImportPrivateKeySuccess/__snapshots__/index.test.tsx.snap +++ b/app/components/Views/ImportPrivateKeySuccess/__snapshots__/index.test.tsx.snap @@ -20,312 +20,321 @@ exports[`ImportPrivateKeySuccess should render correctly 1`] = ` } > - - - + + /> + + - + - ImportPrivateKeySuccess - + + ImportPrivateKeySuccess + + + - - - - - + - - - - - - - ๎— - - + } + style={ + { + "backgroundColor": "#ffffff", + "flex": 1, + } + } + > + - - ๎ฌ  - - - Account successfully imported! - + + ๎— + + + + ๎ฌ  + - You'll now be able to view your account in MetaMask. + Account successfully imported! + + + You'll now be able to view your account in MetaMask. + + - - - + + + - - - + + + `; diff --git a/app/components/Views/MultiRpcModal/__snapshots__/MultiRpcModal.test.tsx.snap b/app/components/Views/MultiRpcModal/__snapshots__/MultiRpcModal.test.tsx.snap index 14fcd9afc18..e5706d14c5b 100644 --- a/app/components/Views/MultiRpcModal/__snapshots__/MultiRpcModal.test.tsx.snap +++ b/app/components/Views/MultiRpcModal/__snapshots__/MultiRpcModal.test.tsx.snap @@ -20,351 +20,331 @@ exports[`MultiRpcModal render matches snapshot 1`] = ` } > - - - + + /> + + - + - MultiRPcMigrationModal - + + MultiRPcMigrationModal + + + - - - - - + - - - - - - - + /> + + - - Network RPCs Updated - + + - - - + + + Network RPCs Updated + + + + - - - + + - We now support multiple RPCs for a single network. Your most recent RPC has been selected as the default one to resolve conflicting information - - - - + We now support multiple RPCs for a single network. Your most recent RPC has been selected as the default one to resolve conflicting information + + + + > + + + - - - - - + - - Accept - - + + Accept + + + @@ -594,9 +617,9 @@ exports[`MultiRpcModal render matches snapshot 1`] = ` - - - + + + `; diff --git a/app/components/Views/MultichainTransactionsView/MultichainTransactionsView.tsx b/app/components/Views/MultichainTransactionsView/MultichainTransactionsView.tsx index 2701c30dabf..61c5a5d2ed0 100644 --- a/app/components/Views/MultichainTransactionsView/MultichainTransactionsView.tsx +++ b/app/components/Views/MultichainTransactionsView/MultichainTransactionsView.tsx @@ -6,8 +6,9 @@ import { NativeScrollEvent, } from 'react-native'; import { useSelector } from 'react-redux'; -import { useNavigation, NavigationProp } from '@react-navigation/native'; +import { useNavigation } from '@react-navigation/native'; import { FlashList } from '@shopify/flash-list'; +import type { AppNavigationProp } from '../../../core/NavigationService/types'; import { CaipChainId, Transaction } from '@metamask/keyring-api'; import { useTheme } from '../../../util/theme'; import { strings } from '../../../../locales/i18n'; @@ -41,7 +42,7 @@ interface MultichainTransactionsViewProps { /** * Override navigation instance */ - navigation?: NavigationProp>; + navigation?: AppNavigationProp; /** * Override selected address */ diff --git a/app/components/Views/NFTAutoDetectionModal/__snapshots__/NFTAutoDetectionModal.test.tsx.snap b/app/components/Views/NFTAutoDetectionModal/__snapshots__/NFTAutoDetectionModal.test.tsx.snap index c00220f6631..6fd33c1db1f 100644 --- a/app/components/Views/NFTAutoDetectionModal/__snapshots__/NFTAutoDetectionModal.test.tsx.snap +++ b/app/components/Views/NFTAutoDetectionModal/__snapshots__/NFTAutoDetectionModal.test.tsx.snap @@ -20,351 +20,331 @@ exports[`NFT Auto detection modal render matches snapshot 1`] = ` } > - - - + + /> + + - + - NFTAutoDetectionModal - + + NFTAutoDetectionModal + + + - - - - - + - - - - - - - + /> + + - - - Enable NFT autodetection - - - - - @@ -494,161 +454,224 @@ exports[`NFT Auto detection modal render matches snapshot 1`] = ` - - Allow MetaMask to detect and display your NFTs with autodetection. Youโ€™ll be able to: - + /> - โ€ข - Immediately access your NFTs + Enable NFT autodetection - - โ€ข - Effortlessly navigate your digital assets - - + + + - โ€ข - Dive straight into using your NFTs - - - - + + - Allow + Allow MetaMask to detect and display your NFTs with autodetection. Youโ€™ll be able to: - - - + โ€ข + Immediately access your NFTs + + + > + โ€ข + Effortlessly navigate your digital assets + - Not right now + โ€ข + Dive straight into using your NFTs - + + + + + Allow + + + + + + Not right now + + + @@ -659,9 +682,9 @@ exports[`NFT Auto detection modal render matches snapshot 1`] = ` - - - + + + `; diff --git a/app/components/Views/NavigationUnitTest/__snapshots__/TestScreen1.test.js.snap b/app/components/Views/NavigationUnitTest/__snapshots__/TestScreen1.test.js.snap index 8d4e9f5bd44..64401ad3002 100644 --- a/app/components/Views/NavigationUnitTest/__snapshots__/TestScreen1.test.js.snap +++ b/app/components/Views/NavigationUnitTest/__snapshots__/TestScreen1.test.js.snap @@ -20,310 +20,333 @@ exports[`NavigationUnitTest should render correctly 1`] = ` } > - - - + + /> + + - + - TestScreen1 - + + TestScreen1 + + + - - - - - + - - - TestScreen1 - THIS SHOULD NOT HAVE CHANGED, take a deeper look - + + + TestScreen1 + THIS SHOULD NOT HAVE CHANGED, take a deeper look + + - - - + + + `; diff --git a/app/components/Views/NavigationUnitTest/__snapshots__/TestScreen2.test.js.snap b/app/components/Views/NavigationUnitTest/__snapshots__/TestScreen2.test.js.snap index 8f3ce44db18..9330434ba0f 100644 --- a/app/components/Views/NavigationUnitTest/__snapshots__/TestScreen2.test.js.snap +++ b/app/components/Views/NavigationUnitTest/__snapshots__/TestScreen2.test.js.snap @@ -20,295 +20,310 @@ exports[`NavigationUnitTest should render correctly 1`] = ` } > - - - + + /> + + - + - TestStack - + + TestStack + + + - - - - - + - @@ -320,628 +335,704 @@ exports[`NavigationUnitTest should render correctly 1`] = ` } > - - - - - + + + - TestSubStack - + + + + + + TestSubStack + + + + + - - - - - - - - - - - - - - - - - - - TestScreen3 - - - - - - - - - - + + + + + + + + + + + TestScreen3 + + + + + + + + - + - - TestScreen3 - THIS SHOULD NOT HAVE CHANGED, take a deeper look - + + + + + + + TestScreen3 + THIS SHOULD NOT HAVE CHANGED, take a deeper look + + + + + + - - + + - - + + - - + + - - + + - - - + + + `; diff --git a/app/components/Views/NavigationUnitTest/__snapshots__/TestScreen3.test.js.snap b/app/components/Views/NavigationUnitTest/__snapshots__/TestScreen3.test.js.snap index 8f3ce44db18..9330434ba0f 100644 --- a/app/components/Views/NavigationUnitTest/__snapshots__/TestScreen3.test.js.snap +++ b/app/components/Views/NavigationUnitTest/__snapshots__/TestScreen3.test.js.snap @@ -20,295 +20,310 @@ exports[`NavigationUnitTest should render correctly 1`] = ` } > - - - + + /> + + - + - TestStack - + + TestStack + + + - - - - - + - @@ -320,628 +335,704 @@ exports[`NavigationUnitTest should render correctly 1`] = ` } > - - - - - + + + - TestSubStack - + + + + + + TestSubStack + + + + + - - - - - - - - - - - - - - - - - - - TestScreen3 - - - - - - - - - - + + + + + + + + + + + TestScreen3 + + + + + + + + - + - - TestScreen3 - THIS SHOULD NOT HAVE CHANGED, take a deeper look - + + + + + + + TestScreen3 + THIS SHOULD NOT HAVE CHANGED, take a deeper look + + + + + + - - + + - - + + - - + + - - + + - - - + + + `; diff --git a/app/components/Views/NetworkSelector/__snapshots__/NetworkSelector.test.tsx.snap b/app/components/Views/NetworkSelector/__snapshots__/NetworkSelector.test.tsx.snap index ba28aed9c8d..e06bb069241 100644 --- a/app/components/Views/NetworkSelector/__snapshots__/NetworkSelector.test.tsx.snap +++ b/app/components/Views/NetworkSelector/__snapshots__/NetworkSelector.test.tsx.snap @@ -20,313 +20,321 @@ exports[`Network Selector renders correctly 1`] = ` } > - - - + + /> + + - + - NETWORK_SELECTOR - + + NETWORK_SELECTOR + + + - - - - - + - - - - - + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + } + } + > + + - - Select a network - - + - + - ๎น  - - + + > + + ๎น  + + + - - - - - - + + @@ -557,107 +571,108 @@ exports[`Network Selector renders correctly 1`] = ` style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 32, - "justifyContent": "center", - "marginRight": 16, - "overflow": "hidden", - "width": 32, + "flexDirection": "row", } } - testID="cellbase-avatar" > - - - - + + + - Ethereum Mainnet - + + Ethereum Mainnet + + - - - - - + + - - - - + + + - Linea - + + Linea + + - - - - + @@ -762,87 +776,87 @@ exports[`Network Selector renders correctly 1`] = ` style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 32, - "justifyContent": "center", - "marginRight": 16, - "overflow": "hidden", - "width": 32, + "flexDirection": "row", } } - testID="cellbase-avatar" > - - - - + + + - Avalanche Mainnet C-Chain - + + Avalanche Mainnet C-Chain + + - - - - + @@ -858,87 +872,87 @@ exports[`Network Selector renders correctly 1`] = ` style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 32, - "justifyContent": "center", - "marginRight": 16, - "overflow": "hidden", - "width": 32, + "flexDirection": "row", } } - testID="cellbase-avatar" > - - - - + + + - Polygon Mainnet - - - - - - - - + Polygon Mainnet + + + + + + + @@ -954,87 +968,87 @@ exports[`Network Selector renders correctly 1`] = ` style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 32, - "justifyContent": "center", - "marginRight": 16, - "overflow": "hidden", - "width": 32, + "flexDirection": "row", } } - testID="cellbase-avatar" > - - - - + + + - Optimism - + + Optimism + + - - - - + @@ -1050,90 +1064,90 @@ exports[`Network Selector renders correctly 1`] = ` style={ { "alignItems": "center", - "backgroundColor": "#f3f3f4", - "borderRadius": 16, - "borderWidth": 1, - "height": 32, - "justifyContent": "center", - "marginRight": 16, - "overflow": "hidden", - "width": 32, + "flexDirection": "row", } } - testID="cellbase-avatar" > - - G - - - - + G + + + - Gnosis Chain - + + Gnosis Chain + + - - - - + @@ -1149,87 +1163,87 @@ exports[`Network Selector renders correctly 1`] = ` style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 32, - "justifyContent": "center", - "marginRight": 16, - "overflow": "hidden", - "width": 32, + "flexDirection": "row", } } - testID="cellbase-avatar" > - - - - + + + - Bitcoin - + + Bitcoin + + - - - - + @@ -1245,87 +1259,87 @@ exports[`Network Selector renders correctly 1`] = ` style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 32, - "justifyContent": "center", - "marginRight": 16, - "overflow": "hidden", - "width": 32, + "flexDirection": "row", } } - testID="cellbase-avatar" > - - - - + + + - Solana - + + Solana + + - - - - + @@ -1341,152 +1355,161 @@ exports[`Network Selector renders correctly 1`] = ` style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 32, - "justifyContent": "center", - "marginRight": 16, - "overflow": "hidden", - "width": 32, + "flexDirection": "row", } } - testID="cellbase-avatar" > - - - - + + + - Tron - + + Tron + + - - - - + - Show test networks - - + "color": "#66676a", + "fontFamily": "Geist-Medium", + "fontSize": 20, + "letterSpacing": 0, + "lineHeight": 24, + } + } + > + Show test networks + + + - - - - + - Add network - - + + Add network + + + @@ -1496,9 +1519,9 @@ exports[`Network Selector renders correctly 1`] = ` - - - + + + `; @@ -1523,313 +1546,321 @@ exports[`Network Selector renders correctly when network UI redesign is enabled } > - - - + + /> + + - + - NETWORK_SELECTOR - + + NETWORK_SELECTOR + + + - - - - - + - - - - - + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + } + } + > + + - - Select a network - - + - + - ๎น  - - + + > + + ๎น  + + + - - - - - - + + - Enabled networks - - - - + Enabled networks + + + - @@ -2108,80 +2145,43 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 24, - "justifyContent": "center", - "marginRight": 16, - "overflow": "hidden", - "width": 24, + "flexDirection": "row", } } - testID="cellbase-avatar" > - - - - - Ethereum Mainnet - - + + @@ -2190,127 +2190,164 @@ exports[`Network Selector renders correctly when network UI redesign is enabled numberOfLines={1} style={ { - "color": "#66676a", + "color": "#131416", "fontFamily": "Geist-Regular", "fontSize": 16, "letterSpacing": 0, "lineHeight": 24, } } + testID="cellbase-avatar-title" > - mainnet-rpc.publicnode.com + Ethereum Mainnet - - + > + + mainnet-rpc.publicnode.com + + + + - - - - + - - + testID="button-menu-select-test-id" + > + + + - - - - @@ -2326,80 +2363,43 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 24, - "justifyContent": "center", - "marginRight": 16, - "overflow": "hidden", - "width": 24, + "flexDirection": "row", } } - testID="cellbase-avatar" > - - - - - Linea - - + + @@ -2408,127 +2408,164 @@ exports[`Network Selector renders correctly when network UI redesign is enabled numberOfLines={1} style={ { - "color": "#66676a", + "color": "#131416", "fontFamily": "Geist-Regular", "fontSize": 16, "letterSpacing": 0, "lineHeight": 24, } } + testID="cellbase-avatar-title" > - linea-rpc.publicnode.com + Linea - - + > + + linea-rpc.publicnode.com + + + + - - - - + - - + testID="button-menu-select-test-id" + > + + + - - - - @@ -2544,56 +2581,110 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 24, - "justifyContent": "center", - "marginRight": 16, - "overflow": "hidden", - "width": 24, + "flexDirection": "row", } } - testID="cellbase-avatar" > - - - + + + - - + + + + Avalanche Mainnet C-Chain + + + + - Avalanche Mainnet C-Chain + api.avax.network/ext/bc/C - - - - - api.avax.network/ext/bc/C - - - + width={10} + /> + + - - - - + - - - - - - + + + + + - @@ -2775,56 +2812,110 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 24, - "justifyContent": "center", - "marginRight": 16, - "overflow": "hidden", - "width": 24, + "flexDirection": "row", } } - testID="cellbase-avatar" > - - - + + + - - + + + + Polygon Mainnet + + + + - Polygon Mainnet + polygon-mainnet.infura.io/v3 - - - - - polygon-mainnet.infura.io/v3 - - - + width={10} + /> + + - - - - + - - + testID="button-menu-select-test-id" + > + + + - - - - @@ -3006,56 +3043,110 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 24, - "justifyContent": "center", - "marginRight": 16, - "overflow": "hidden", - "width": 24, + "flexDirection": "row", } } - testID="cellbase-avatar" > - - - + + + - - + + + + Optimism + + + + - Optimism + optimism-mainnet.infura.io/v3 - - - - - optimism-mainnet.infura.io/v3 - - - + width={10} + /> + + - - - - + - - + testID="button-menu-select-test-id" + > + + + - - - - @@ -3237,206 +3274,206 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "backgroundColor": "#f3f3f4", - "borderRadius": 12, - "borderWidth": 1, - "height": 24, - "justifyContent": "center", - "marginRight": 16, - "overflow": "hidden", - "width": 24, + "flexDirection": "row", } } - testID="cellbase-avatar" > - - G - - - - - - - Gnosis Chain - - + G + - - + + + Gnosis Chain + + + + - rpc.gnosischain.com - - - + > + + rpc.gnosischain.com + + + + - - - - + - - + testID="button-menu-select-test-id" + > + + + - - - @@ -3452,87 +3489,87 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 24, - "justifyContent": "center", - "marginRight": 16, - "overflow": "hidden", - "width": 24, + "flexDirection": "row", } } - testID="cellbase-avatar" > - - - - + + + - Bitcoin - + + Bitcoin + + - - - - + @@ -3548,87 +3585,87 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 24, - "justifyContent": "center", - "marginRight": 16, - "overflow": "hidden", - "width": 24, + "flexDirection": "row", } } - testID="cellbase-avatar" > - - - - + + + - Solana - + + Solana + + - - - - + @@ -3644,149 +3681,149 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 24, - "justifyContent": "center", - "marginRight": 16, - "overflow": "hidden", - "width": 24, + "flexDirection": "row", } } - testID="cellbase-avatar" > - - - - + + + - Tron - + + Tron + + - - - - - Additional networks - - + - ๓ฐ‹ผ + Additional networks - - - - + + ๓ฐ‹ผ + + + + - @@ -3794,9 +3831,7 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "borderRadius": 10, - "justifyContent": "center", - "marginRight": 20, + "flexDirection": "row", } } > @@ -3804,131 +3839,133 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 32, + "borderRadius": 10, "justifyContent": "center", - "overflow": "hidden", - "width": 32, + "marginRight": 20, } } > - + > + + - - - - Arbitrum - + + Arbitrum + + - - - - - - - - + + + + @@ -3936,9 +3973,7 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "borderRadius": 10, - "justifyContent": "center", - "marginRight": 20, + "flexDirection": "row", } } > @@ -3946,165 +3981,167 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 32, + "borderRadius": 10, "justifyContent": "center", - "overflow": "hidden", - "width": 32, + "marginRight": 20, } } > - + > + + - - - - BNB Chain - - + BNB Chain + + - No network fee - + + No network fee + + - - - - - - - - + + + + @@ -4112,9 +4149,7 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "borderRadius": 10, - "justifyContent": "center", - "marginRight": 20, + "flexDirection": "row", } } > @@ -4122,131 +4157,133 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 32, + "borderRadius": 10, "justifyContent": "center", - "overflow": "hidden", - "width": 32, + "marginRight": 20, } } > - + > + + - - - - Base - + + Base + + - - - - - - - - + + + + @@ -4254,9 +4291,7 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "borderRadius": 10, - "justifyContent": "center", - "marginRight": 20, + "flexDirection": "row", } } > @@ -4264,131 +4299,133 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 32, + "borderRadius": 10, "justifyContent": "center", - "overflow": "hidden", - "width": 32, + "marginRight": 20, } } > - - - - + > + + + - - HyperEVM - + + HyperEVM + + - - - - - - - - + + + + @@ -4396,9 +4433,7 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "borderRadius": 10, - "justifyContent": "center", - "marginRight": 20, + "flexDirection": "row", } } > @@ -4406,131 +4441,133 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 32, + "borderRadius": 10, "justifyContent": "center", - "overflow": "hidden", - "width": 32, + "marginRight": 20, } } > - + > + + - - - - Palm - + + Palm + + - - - - - - - - + + + + @@ -4538,9 +4575,7 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "borderRadius": 10, - "justifyContent": "center", - "marginRight": 20, + "flexDirection": "row", } } > @@ -4548,131 +4583,133 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 32, + "borderRadius": 10, "justifyContent": "center", - "overflow": "hidden", - "width": 32, + "marginRight": 20, } } > - + > + + - - - - zkSync Era - + + zkSync Era + + - - - - - - - - + + + + @@ -4680,9 +4717,7 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "borderRadius": 10, - "justifyContent": "center", - "marginRight": 20, + "flexDirection": "row", } } > @@ -4690,131 +4725,133 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 32, + "borderRadius": 10, "justifyContent": "center", - "overflow": "hidden", - "width": 32, + "marginRight": 20, } } > - + > + + - - - - Sei - + + Sei + + - - - - - - - - + + + + + @@ -4822,9 +4859,7 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "borderRadius": 10, - "justifyContent": "center", - "marginRight": 20, + "flexDirection": "row", } } > @@ -4832,131 +4867,133 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 32, + "borderRadius": 10, "justifyContent": "center", - "overflow": "hidden", - "width": 32, + "marginRight": 20, } } > - + > + + - - - - Monad - + + Monad + + - - - - - - - - + + + + @@ -4964,9 +5001,7 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "borderRadius": 10, - "justifyContent": "center", - "marginRight": 20, + "flexDirection": "row", } } > @@ -4974,209 +5009,220 @@ exports[`Network Selector renders correctly when network UI redesign is enabled style={ { "alignItems": "center", - "backgroundColor": "#ffffff", - "borderRadius": 8, - "height": 32, + "borderRadius": 10, "justifyContent": "center", - "overflow": "hidden", - "width": 32, + "marginRight": 20, } } > - + > + + - - - - MegaETH - + + MegaETH + + - - - - - - - - - + + + + + - Show test networks - - + "color": "#66676a", + "fontFamily": "Geist-Medium", + "fontSize": 20, + "letterSpacing": 0, + "lineHeight": 24, + } + } + > + Show test networks + + + - - - - + - Add a custom network - - + + Add a custom network + + + @@ -5186,9 +5232,9 @@ exports[`Network Selector renders correctly when network UI redesign is enabled - - - + + + `; diff --git a/app/components/Views/NftDetails/__snapshots__/NftDetails.test.ts.snap b/app/components/Views/NftDetails/__snapshots__/NftDetails.test.ts.snap index 59830404c0f..870339e6d22 100644 --- a/app/components/Views/NftDetails/__snapshots__/NftDetails.test.ts.snap +++ b/app/components/Views/NftDetails/__snapshots__/NftDetails.test.ts.snap @@ -20,578 +20,512 @@ exports[`NftDetails renders correctly 1`] = ` } > - - - + + /> + + - + - NftDetails - + + NftDetails + + + - - - - - + - - - - - - + + - - - - - - - + + + + + + - + > + + - - - - - - - - - - Aura #44 - - - - - Aura is a collection of 100 high resolution AI-generated portraits, exploring the boundaries of realism versus imagination... how far a portrait image can be deconstructed so that a sense of humanity and emotion would still remain? Aura plays with our appreciation of realistic details in photography. - - - - + > + + + + + + @@ -599,570 +533,591 @@ exports[`NftDetails renders correctly 1`] = ` accessibilityRole="text" style={ { - "color": "#66676a", - "fontFamily": "Geist-Regular", - "fontSize": 10, - "fontWeight": "500", + "color": "#131416", + "fontFamily": "Geist-Bold", + "fontSize": 20, "letterSpacing": 0, - "lineHeight": 16, + "lineHeight": 24, + "paddingTop": 16, } } > - Bought for + Aura #44 + - - - data unavailable - - + Aura is a collection of 100 high resolution AI-generated portraits, exploring the boundaries of realism versus imagination... how far a portrait image can be deconstructed so that a sense of humanity and emotion would still remain? Aura plays with our appreciation of realistic details in photography. + - - Highest floor price - - - - - $1,206.43 - - - - - - - - - + Bought for + + + - Rank - - - - - #1 - + > + data unavailable + + - - - - Contract address - - - - - 0x7c3Ea...00B18 + Highest floor price - - - + + - + > + $1,206.43 + + + + + + + + + + Rank + + + + + #1 + + + + + + + Contract address + + + + + + 0x7c3Ea...00B18 + + + + + + - - - - Token ID - - - 23000044 - - - - + Token ID + + - Token symbol - - + 23000044 + + + - MOMENT-FLEX - - - - - Token standard - - + Token symbol + + - ERC721 - - - - + MOMENT-FLEX + + + - Date created - - - 2023-05-05 - - - - Collection - - - + Token standard + + + ERC721 + + + - Tokens in collection - - + Date created + + - 100 - - - - Price - - + > + 2023-05-05 + + - Highest current bid + Collection + + Tokens in collection + - 0.0626ETH + 100 - - - - - - Attributes - - + > + Price + - - - Title - - - + } + > + Highest current bid + + - You Came To See Me + 0.0626ETH + + + - - - Disclaimer: MetaMask pulls the media file from the source URL. This URL is sometimes changed by the marketplace the NFT was minted on. + Attributes + + + + + Title + + + + + You Came To See Me + + + + + + + Disclaimer: MetaMask pulls the media file from the source URL. This URL is sometimes changed by the marketplace the NFT was minted on. + + - - - + + - - Send - + + Send + + - - + + - - - + + + `; diff --git a/app/components/Views/Notifications/Details/index.test.tsx b/app/components/Views/Notifications/Details/index.test.tsx index 18b6fa5ad69..555b8262dc3 100644 --- a/app/components/Views/Notifications/Details/index.test.tsx +++ b/app/components/Views/Notifications/Details/index.test.tsx @@ -39,9 +39,6 @@ jest.mock('../../../../actions/alert', () => ({ })); jest.mock('@react-navigation/native'); -jest.mock('@react-navigation/compat', () => ({ - withNavigation: jest.fn((component) => component), -})); jest.mock('react-native-safe-area-context', () => { const inset = { top: 0, right: 0, bottom: 0, left: 0 }; const frame = { width: 0, height: 0, x: 0, y: 0 }; diff --git a/app/components/Views/Notifications/OptIn/OptIn.hooks.tsx b/app/components/Views/Notifications/OptIn/OptIn.hooks.tsx index 6adb7710b9e..9a30b2c6b7e 100644 --- a/app/components/Views/Notifications/OptIn/OptIn.hooks.tsx +++ b/app/components/Views/Notifications/OptIn/OptIn.hooks.tsx @@ -1,11 +1,11 @@ import { useCallback, useEffect, useState } from 'react'; -import { NavigationProp, ParamListBase } from '@react-navigation/native'; import { useSelector } from 'react-redux'; import { RootState } from '../../../../reducers'; import Routes from '../../../../constants/navigation/Routes'; import type { UseAnalyticsHook } from '../../../hooks/useAnalytics/useAnalytics.types'; import { MetaMetricsEvents } from '../../../../core/Analytics'; import { selectIsBackupAndSyncEnabled } from '../../../../selectors/identity'; +import type { AppNavigationProp } from '../../../../core/NavigationService/types'; /** * Creating wallet notifications can take time, so we will use optimistic loader @@ -14,7 +14,7 @@ import { selectIsBackupAndSyncEnabled } from '../../../../selectors/identity'; */ export function useOptimisticNavigationEffect(props: { isCreatingNotifications: boolean; - navigation: NavigationProp; + navigation: AppNavigationProp; }) { const { isCreatingNotifications, navigation } = props; const [optimisticLoading, setOptimisticLoading] = useState(false); @@ -47,7 +47,7 @@ export function useOptimisticNavigationEffect(props: { } export function useHandleOptInClick(props: { - navigation: NavigationProp; + navigation: AppNavigationProp; metrics: UseAnalyticsHook; enableNotifications: () => Promise; }) { @@ -97,7 +97,7 @@ export function useHandleOptInClick(props: { } export function useHandleOptInCancel(props: { - navigation: NavigationProp; + navigation: AppNavigationProp; metrics: UseAnalyticsHook; isCreatingNotifications: boolean; }) { diff --git a/app/components/Views/Onboarding/index.tsx b/app/components/Views/Onboarding/index.tsx index 3c105f2a087..9ae0f69158d 100644 --- a/app/components/Views/Onboarding/index.tsx +++ b/app/components/Views/Onboarding/index.tsx @@ -115,6 +115,7 @@ import { IconColor, IconName, } from '../../../component-library/components/Icons/Icon'; +import { AppNavigationProp } from '../../../core/NavigationService/types'; interface OnboardingState { warningModalVisible: boolean; loading: boolean; @@ -142,7 +143,7 @@ interface OnboardingRouteParams { } const Onboarding = () => { - const navigation = useNavigation(); + const navigation = useNavigation(); const onboardingVersion = useMemo( () => `${getVersion()} (${getBuildNumber()})`, [], diff --git a/app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/index.test.tsx b/app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/index.test.tsx index 354be5cb526..62ad7d0e836 100644 --- a/app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/index.test.tsx +++ b/app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/index.test.tsx @@ -13,7 +13,7 @@ jest.mock('@react-navigation/native', () => { setOptions: jest.fn(), goBack: jest.fn(), reset: jest.fn(), - dangerouslyGetParent: () => ({ + getParent: () => ({ pop: jest.fn(), }), }), diff --git a/app/components/Views/OnboardingSuccess/index.test.tsx b/app/components/Views/OnboardingSuccess/index.test.tsx index 93e4e119ff2..583c6850445 100644 --- a/app/components/Views/OnboardingSuccess/index.test.tsx +++ b/app/components/Views/OnboardingSuccess/index.test.tsx @@ -79,7 +79,7 @@ jest.mock('@react-navigation/native', () => { goBack: jest.fn(), reset: jest.fn(), dispatch: mockNavigationDispatch, - dangerouslyGetParent: () => ({ + getParent: () => ({ pop: jest.fn(), }), }), diff --git a/app/components/Views/ProtectWalletMandatoryModal/ProtectWalletMandatoryModal.test.tsx b/app/components/Views/ProtectWalletMandatoryModal/ProtectWalletMandatoryModal.test.tsx index 64f2525829f..1c62a2391f7 100644 --- a/app/components/Views/ProtectWalletMandatoryModal/ProtectWalletMandatoryModal.test.tsx +++ b/app/components/Views/ProtectWalletMandatoryModal/ProtectWalletMandatoryModal.test.tsx @@ -1,132 +1,534 @@ import React from 'react'; -import renderWithProvider from '../../../util/test/renderWithProvider'; -import ProtectWalletMandatoryModal from './ProtectWalletMandatoryModal'; -import { backgroundState } from '../../../util/test/initial-root-state'; +import { render, fireEvent, waitFor } from '@testing-library/react-native'; +import { Provider } from 'react-redux'; +import { configureStore } from '@reduxjs/toolkit'; import { InteractionManager } from 'react-native'; -import { fireEvent } from '@testing-library/react-native'; - -// Mock Device utility -const mockIsIphoneX = jest.fn(); -jest.mock('../../../util/device', () => ({ - isIphoneX: () => mockIsIphoneX(), - isAndroid: () => false, - isIos: () => true, - getDeviceWidth: () => 375, - getDeviceHeight: () => 812, -})); +import { useAnalytics } from '../../hooks/useAnalytics/useAnalytics'; +import { createMockUseAnalyticsHook } from '../../../util/test/analyticsMock'; + +type RunAfterInteractionsTask = NonNullable< + Parameters[0] +>; +import ProtectWalletMandatoryModal from './ProtectWalletMandatoryModal'; +import { ThemeContext, mockTheme } from '../../../util/theme'; -// Mock the navigation const mockNavigate = jest.fn(); -const mockDangerouslyGetState = jest.fn(); -jest.mock('@react-navigation/native', () => { - const actualNav = jest.requireActual('@react-navigation/native'); - return { - ...actualNav, - useNavigation: () => ({ - navigate: mockNavigate, - dangerouslyGetState: mockDangerouslyGetState, - }), - }; -}); -// Mock the metrics hook -jest.mock('../../hooks/useMetrics', () => ({ - MetaMetricsEvents: { - WALLET_SECURITY_PROTECT_VIEWED: 'WALLET_SECURITY_PROTECT_VIEWED', - WALLET_SECURITY_PROTECT_ENGAGED: 'WALLET_SECURITY_PROTECT_ENGAGED', - }, - useMetrics: () => ({ - trackEvent: jest.fn(), - createEventBuilder: jest.fn().mockReturnValue({ - addProperties: jest.fn().mockReturnValue({ - build: jest.fn(), - }), - }), +interface MockNavigationState { + routes: { name: string }[]; +} + +const mockGetState = jest.fn((): MockNavigationState | undefined => ({ + routes: [{ name: 'Home' }], +})); + +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: () => ({ + navigate: mockNavigate, + getState: mockGetState, }), })); -// Mock Engine +const mockTrackEvent = jest.fn(); +const mockCreateEventBuilder = jest.fn(() => ({ + addProperties: jest.fn().mockReturnThis(), + addSensitiveProperties: jest.fn().mockReturnThis(), + removeProperties: jest.fn().mockReturnThis(), + removeSensitiveProperties: jest.fn().mockReturnThis(), + setSaveDataRecording: jest.fn().mockReturnThis(), + build: jest.fn().mockReturnValue({}), +})); + +jest.mock('../../hooks/useAnalytics/useAnalytics'); + +const mockHasFunds = jest.fn(() => true); jest.mock('../../../core/Engine', () => ({ - hasFunds: jest.fn().mockReturnValue(true), + hasFunds: () => mockHasFunds(), })); -// Mock InteractionManager -jest.mock('react-native', () => { - const actualRN = jest.requireActual('react-native'); - return { - ...actualRN, - InteractionManager: { - runAfterInteractions: jest.fn((callback) => callback()), - }, +jest.mock('react-native-modal', () => { + const MockModal = ({ + children, + isVisible, + ...props + }: { + children: React.ReactNode; + isVisible: boolean; + }) => { + // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires + const { View } = require('react-native'); + return isVisible ? ( + + {children} + + ) : null; }; + MockModal.displayName = 'MockModal'; + return MockModal; }); -const initialState = { - engine: { - backgroundState: { - ...backgroundState, - SeedlessOnboardingController: { - vault: undefined, - }, +const createMockStore = ( + passwordSet = false, + seedphraseBackedUp = false, + isSeedlessOnboarding = false, +) => + configureStore({ + reducer: { + user: () => ({ + passwordSet, + seedphraseBackedUp, + }), + engine: () => ({ + backgroundState: { + TokenBalancesController: { + tokenBalances: {}, + }, + TokensController: { + allTokens: {}, + }, + NftController: { + allNfts: {}, + }, + AccountsController: { + internalAccounts: { + accounts: {}, + selectedAccount: '0x123', + }, + }, + SeedlessOnboardingController: { + vault: isSeedlessOnboarding ? 'mock-vault' : null, + }, + }, + }), }, - }, - user: { - passwordSet: false, - seedphraseBackedUp: false, - }, -}; + }); + +const renderWithTheme = ( + component: React.ReactElement, + store: ReturnType, +) => + render( + + {component} + , + ); describe('ProtectWalletMandatoryModal', () => { beforeEach(() => { - mockDangerouslyGetState.mockReturnValue({ + jest.clearAllMocks(); + jest.mocked(useAnalytics).mockReturnValue( + createMockUseAnalyticsHook({ + trackEvent: mockTrackEvent, + createEventBuilder: mockCreateEventBuilder, + }), + ); + mockGetState.mockReturnValue({ routes: [{ name: 'Home' }], }); - jest.clearAllMocks(); + mockHasFunds.mockReturnValue(true); + }); + + it('does not show modal when password is set and seedphrase is backed up', async () => { + const store = createMockStore(true, true); + + const { queryByTestId } = renderWithTheme( + , + store, + ); + + await waitFor(() => { + expect(queryByTestId('modal-container')).toBeNull(); + }); + }); + + it('does not show modal for seedless onboarding flow', async () => { + const store = createMockStore(false, false, true); + + const { queryByTestId } = renderWithTheme( + , + store, + ); + + await waitFor(() => { + expect(queryByTestId('modal-container')).toBeNull(); + }); + }); + + it('does not show modal when on SetPasswordFlow route', async () => { + mockGetState.mockReturnValue({ + routes: [{ name: 'SetPasswordFlow' }], + }); + + const store = createMockStore(false, false); + + const { queryByTestId } = renderWithTheme( + , + store, + ); + + await waitFor(() => { + expect(queryByTestId('modal-container')).toBeNull(); + }); + }); + + it('does not show modal when on ChoosePassword route', async () => { + mockGetState.mockReturnValue({ + routes: [{ name: 'ChoosePassword' }], + }); + + const store = createMockStore(false, false); + + const { queryByTestId } = renderWithTheme( + , + store, + ); + + await waitFor(() => { + expect(queryByTestId('modal-container')).toBeNull(); + }); }); - it('renders correctly', () => { - const { toJSON } = renderWithProvider(, { - state: initialState, + it('does not show modal when on ManualBackupStep routes', async () => { + mockGetState.mockReturnValue({ + routes: [{ name: 'ManualBackupStep1' }], + }); + + const store = createMockStore(true, false); + + const { queryByTestId } = renderWithTheme( + , + store, + ); + + await waitFor(() => { + expect(queryByTestId('modal-container')).toBeNull(); }); - expect(toJSON()).toMatchSnapshot(); }); - it('renders correctly on iPhoneX', () => { - mockIsIphoneX.mockReturnValue(true); + it('shows modal when password not set', async () => { + const store = createMockStore(false, false); - const { toJSON } = renderWithProvider(, { - state: initialState, + const { getByTestId } = renderWithTheme( + , + store, + ); + + await waitFor(() => { + expect(getByTestId('modal-container')).toBeTruthy(); }); - expect(toJSON()).toMatchSnapshot(); }); - it('tracks metrics event after interactions when securing wallet', () => { - const { getByText } = renderWithProvider(, { - state: initialState, + it('tracks WALLET_SECURITY_PROTECT_VIEWED event when modal is shown', async () => { + const store = createMockStore(false, false); + + renderWithTheme(, store); + + await waitFor(() => { + expect(mockTrackEvent).toHaveBeenCalled(); }); + }); - const secureButton = getByText('Protect wallet'); - fireEvent.press(secureButton); + it('navigates to SetPasswordFlow when Secure Wallet button is pressed', async () => { + const store = createMockStore(false, false); + + const { getByText } = renderWithTheme( + , + store, + ); + + await waitFor(() => { + const secureButton = getByText('Protect wallet'); + fireEvent.press(secureButton); + }); expect(mockNavigate).toHaveBeenCalledWith('SetPasswordFlow', undefined); - expect(InteractionManager.runAfterInteractions).toHaveBeenCalled(); }); - it('does not render when in seedless onboarding login flow', () => { - const { queryByText } = renderWithProvider( + it('tracks WALLET_SECURITY_PROTECT_ENGAGED after interactions when Secure Wallet is pressed', async () => { + const runAfterSpy = jest + .spyOn(InteractionManager, 'runAfterInteractions') + .mockImplementation((task?: RunAfterInteractionsTask) => { + if (typeof task === 'function') { + task(); + } else if (task !== undefined) { + Promise.resolve(task.gen()).catch(() => undefined); + } + return { + then: (onfulfilled?: () => unknown, onrejected?: () => unknown) => + Promise.resolve(undefined).then(onfulfilled, onrejected), + done: jest.fn(), + cancel: jest.fn(), + }; + }); + const store = createMockStore(false, false); + + const { getByText } = renderWithTheme( , - { - state: { - ...initialState, - engine: { - backgroundState: { - ...initialState.engine.backgroundState, - SeedlessOnboardingController: { vault: 'encrypted-vault-data' }, - }, - }, - }, - }, + store, + ); + + await waitFor(() => { + fireEvent.press(getByText('Protect wallet')); + }); + + await waitFor(() => { + expect(mockCreateEventBuilder).toHaveBeenCalledWith( + expect.objectContaining({ + category: 'Wallet Security Reminder Engaged', + }), + ); + expect(mockTrackEvent).toHaveBeenCalled(); + }); + + runAfterSpy.mockRestore(); + }); + + it('navigates to AccountBackupStep1 when password is already set', async () => { + const store = createMockStore(true, false); + + const { getByText } = renderWithTheme( + , + store, + ); + + await waitFor(() => { + const secureButton = getByText('Protect wallet'); + fireEvent.press(secureButton); + }); + + expect(mockNavigate).toHaveBeenCalledWith('SetPasswordFlow', { + screen: 'AccountBackupStep1', + }); + }); + + it('shows password-specific message when password not set', async () => { + const store = createMockStore(false, false); + + const { getByText } = renderWithTheme( + , + store, ); - expect(queryByText('Protect wallet')).toBeNull(); + + await waitFor(() => { + expect( + getByText(/Protect your wallet by setting a password/i), + ).toBeTruthy(); + }); + }); + + it('shows seedphrase-specific message when password is set but seedphrase not backed up', async () => { + const store = createMockStore(true, false); + + const { getByText } = renderWithTheme( + , + store, + ); + + await waitFor(() => { + expect( + getByText(/now that value was added to your wallet/i), + ).toBeTruthy(); + }); + }); + + it('does not show modal when on AccountBackupStep1B route', async () => { + mockGetState.mockReturnValue({ + routes: [{ name: 'AccountBackupStep1B' }], + }); + + const store = createMockStore(true, false); + + const { queryByTestId } = renderWithTheme( + , + store, + ); + + await waitFor(() => { + expect(queryByTestId('modal-container')).toBeNull(); + }); + }); + + it('does not show modal when on ManualBackupStep2 route', async () => { + mockGetState.mockReturnValue({ + routes: [{ name: 'ManualBackupStep2' }], + }); + + const store = createMockStore(true, false); + + const { queryByTestId } = renderWithTheme( + , + store, + ); + + await waitFor(() => { + expect(queryByTestId('modal-container')).toBeNull(); + }); + }); + + it('does not show modal when on ManualBackupStep3 route', async () => { + mockGetState.mockReturnValue({ + routes: [{ name: 'ManualBackupStep3' }], + }); + + const store = createMockStore(true, false); + + const { queryByTestId } = renderWithTheme( + , + store, + ); + + await waitFor(() => { + expect(queryByTestId('modal-container')).toBeNull(); + }); + }); + + it('does not show modal when on Webview route', async () => { + mockGetState.mockReturnValue({ + routes: [{ name: 'Webview' }], + }); + + const store = createMockStore(true, false); + + const { queryByTestId } = renderWithTheme( + , + store, + ); + + await waitFor(() => { + expect(queryByTestId('modal-container')).toBeNull(); + }); + }); + + it('does not show modal when user has no funds and password is set', async () => { + mockHasFunds.mockReturnValue(false); + const store = createMockStore(true, false); + + const { queryByTestId } = renderWithTheme( + , + store, + ); + + await waitFor(() => { + expect(queryByTestId('modal-container')).toBeNull(); + }); + }); + + it('shows modal when user has funds and seedphrase not backed up', async () => { + mockHasFunds.mockReturnValue(true); + const store = createMockStore(true, false); + + const { getByTestId } = renderWithTheme( + , + store, + ); + + await waitFor(() => { + expect(getByTestId('modal-container')).toBeTruthy(); + }); + }); + + it('shows modal when password not set and user has no funds', async () => { + mockHasFunds.mockReturnValue(false); + const store = createMockStore(false, false); + + const { getByTestId } = renderWithTheme( + , + store, + ); + + await waitFor(() => { + expect(getByTestId('modal-container')).toBeTruthy(); + }); + }); + + it('does not show modal when on AccountBackupStep1 route', async () => { + mockGetState.mockReturnValue({ + routes: [{ name: 'AccountBackupStep1' }], + }); + + const store = createMockStore(false, false); + + const { queryByTestId } = renderWithTheme( + , + store, + ); + + await waitFor(() => { + expect(queryByTestId('modal-container')).toBeNull(); + }); + }); + + it('does not show modal when on LockScreen route', async () => { + mockGetState.mockReturnValue({ + routes: [{ name: 'LockScreen' }], + }); + + const store = createMockStore(false, false); + + const { queryByTestId } = renderWithTheme( + , + store, + ); + + await waitFor(() => { + expect(queryByTestId('modal-container')).toBeNull(); + }); + }); + + it('displays correct title text', async () => { + const store = createMockStore(false, false); + + const { getByText } = renderWithTheme( + , + store, + ); + + await waitFor(() => { + expect(getByText('Protect your wallet')).toBeTruthy(); + }); + }); + + it('re-evaluates modal visibility when token balance changes', async () => { + mockHasFunds.mockReturnValue(false); + const store = createMockStore(true, false); + + const { queryByTestId, rerender } = renderWithTheme( + , + store, + ); + + await waitFor(() => { + expect(queryByTestId('modal-container')).toBeNull(); + }); + + mockHasFunds.mockReturnValue(true); + const storeWithFunds = createMockStore(true, false); + + rerender( + + + + + , + ); + + await waitFor(() => { + expect(queryByTestId('modal-container')).toBeTruthy(); + }); + }); + + it('does not crash when getState returns undefined', async () => { + mockGetState.mockReturnValue(undefined); + const store = createMockStore(false, false); + + const { getByTestId } = renderWithTheme( + , + store, + ); + + await waitFor(() => { + expect(getByTestId('modal-container')).toBeTruthy(); + }); }); }); diff --git a/app/components/Views/ProtectWalletMandatoryModal/ProtectWalletMandatoryModal.tsx b/app/components/Views/ProtectWalletMandatoryModal/ProtectWalletMandatoryModal.tsx index 82fa5ae5ed5..9ba85a9a45f 100644 --- a/app/components/Views/ProtectWalletMandatoryModal/ProtectWalletMandatoryModal.tsx +++ b/app/components/Views/ProtectWalletMandatoryModal/ProtectWalletMandatoryModal.tsx @@ -6,7 +6,8 @@ import { useTheme } from '../../../util/theme'; import StyledButton from '../../UI/StyledButton'; import { strings } from '../../../../locales/i18n'; import createStyles from './ProtectWalletMandatoryModal.styles'; -import { MetaMetricsEvents, useMetrics } from '../../hooks/useMetrics'; +import { MetaMetricsEvents } from '../../../core/Analytics'; +import { useAnalytics } from '../../hooks/useAnalytics/useAnalytics'; import { selectPasswordSet, selectSeedphraseBackedUp, @@ -29,7 +30,7 @@ const ProtectWalletMandatoryModal = () => { const styles = useMemo(() => createStyles(theme), [theme]); - const metrics = useMetrics(); + const metrics = useAnalytics(); const hasAnyTokenBalance = useSelector(selectHasAnyBalance); const allTokens = useSelector(selectAllTokens); @@ -39,12 +40,12 @@ const ProtectWalletMandatoryModal = () => { selectSeedlessOnboardingLoginFlow, ); - const { navigate, dangerouslyGetState } = useNavigation(); + const { navigate, getState } = useNavigation(); const passwordSet = useSelector(selectPasswordSet); const seedphraseBackedUp = useSelector(selectSeedphraseBackedUp); useEffect(() => { - const route = findRouteNameFromNavigatorState(dangerouslyGetState().routes); + const route = findRouteNameFromNavigatorState(getState()?.routes ?? []); if (isSeedlessOnboardingLoginFlow) { setShowProtectWalletModal(false); return; @@ -95,7 +96,7 @@ const ProtectWalletMandatoryModal = () => { metrics, passwordSet, seedphraseBackedUp, - dangerouslyGetState, + getState, hasAnyTokenBalance, allTokens, nfts, diff --git a/app/components/Views/ProtectWalletMandatoryModal/__snapshots__/ProtectWalletMandatoryModal.test.tsx.snap b/app/components/Views/ProtectWalletMandatoryModal/__snapshots__/ProtectWalletMandatoryModal.test.tsx.snap deleted file mode 100644 index 66dab1d2716..00000000000 --- a/app/components/Views/ProtectWalletMandatoryModal/__snapshots__/ProtectWalletMandatoryModal.test.tsx.snap +++ /dev/null @@ -1,491 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ProtectWalletMandatoryModal renders correctly 1`] = ` - - - - - - - - ๏„„ - - - - Protect your wallet - - - Protect your wallet by setting a password and saving your Secret Recovery Phrase (required). - - - - - Protect wallet - - - - - - - -`; - -exports[`ProtectWalletMandatoryModal renders correctly on iPhoneX 1`] = ` - - - - - - - - ๏„„ - - - - Protect your wallet - - - Protect your wallet by setting a password and saving your Secret Recovery Phrase (required). - - - - - Protect wallet - - - - - - - -`; diff --git a/app/components/Views/QRAccountDisplay/__snapshots__/QRAccountDisplay.test.tsx.snap b/app/components/Views/QRAccountDisplay/__snapshots__/QRAccountDisplay.test.tsx.snap index 565e2f13792..0d27af5e4ff 100644 --- a/app/components/Views/QRAccountDisplay/__snapshots__/QRAccountDisplay.test.tsx.snap +++ b/app/components/Views/QRAccountDisplay/__snapshots__/QRAccountDisplay.test.tsx.snap @@ -20,337 +20,327 @@ exports[`QRAccountDisplay render matches snapshot 1`] = ` } > - - - + + /> + + - + - QRAccountDisplay - + + QRAccountDisplay + + + - - - - - + - - - 0xd8dA6...96045 - - 0xd8dA - - 6BF26964aF9D7eEd9e03E53415D37aA - - 96045 + 0xd8dA6...96045 - Copy address + 0xd8dA + + 6BF26964aF9D7eEd9e03E53415D37aA + + 96045 - + testID="qr-account-display-copy-button" + > + + Copy address + + + @@ -505,9 +528,9 @@ exports[`QRAccountDisplay render matches snapshot 1`] = ` - - - + + + `; diff --git a/app/components/Views/QRTabSwitcher/QRTabSwitcher.test.tsx b/app/components/Views/QRTabSwitcher/QRTabSwitcher.test.tsx index bf1f71a4f82..2af764509f9 100644 --- a/app/components/Views/QRTabSwitcher/QRTabSwitcher.test.tsx +++ b/app/components/Views/QRTabSwitcher/QRTabSwitcher.test.tsx @@ -24,14 +24,6 @@ jest.mock('../../../util/test/configureStore', () => { return () => configureMockStore([])(); }); -jest.mock('@react-navigation/compat', () => { - const actualNav = jest.requireActual('@react-navigation/compat'); - return { - ...actualNav, - withNavigation: (obj: React.ReactNode) => obj, - }; -}); - jest.mock('../QRScanner', () => jest.fn(() => null)); describe('QRTabSwitcher', () => { diff --git a/app/components/Views/ResetPassword/index.test.tsx b/app/components/Views/ResetPassword/index.test.tsx index 69be8eee817..e6c08c2fcac 100644 --- a/app/components/Views/ResetPassword/index.test.tsx +++ b/app/components/Views/ResetPassword/index.test.tsx @@ -28,7 +28,10 @@ import ReduxService from '../../../core/redux/ReduxService'; import { ReduxStore } from '../../../core/redux/types'; import { recreateVaultsWithNewPassword } from '../../../core/Vault'; import { SeedlessOnboardingControllerErrorMessage } from '@metamask/seedless-onboarding-controller'; -import { NavigationContainerRef } from '@react-navigation/native'; +import { + NavigationContainerRef, + ParamListBase, +} from '@react-navigation/native'; jest.mock('../../../util/metrics/TrackOnboarding/trackOnboarding'); @@ -750,7 +753,7 @@ describe('ResetPassword', () => { .mockResolvedValueOnce(true); NavigationService.navigation = - mockNavigation as unknown as NavigationContainerRef; + mockNavigation as unknown as NavigationContainerRef; const component = await navigateToResetForm(null); await fillResetForm(component); @@ -875,7 +878,7 @@ describe('ResetPassword', () => { .mockResolvedValueOnce(false); NavigationService.navigation = - mockNavigation as unknown as NavigationContainerRef; + mockNavigation as unknown as NavigationContainerRef; const component = await navigateToResetForm(); await fillResetForm(component); diff --git a/app/components/Views/Settings/AdvancedSettings/index.js b/app/components/Views/Settings/AdvancedSettings/index.js index 3fd6bc453e4..d008f5f1970 100644 --- a/app/components/Views/Settings/AdvancedSettings/index.js +++ b/app/components/Views/Settings/AdvancedSettings/index.js @@ -39,7 +39,8 @@ import Button, { ButtonSize, ButtonWidthTypes, } from '../../../../component-library/components/Buttons/Button'; -import { withAnalyticsAwareness } from '../../../../components/hooks/useAnalytics/withAnalyticsAwareness'; +import { analytics } from '../../../../util/analytics/analytics'; +import { AnalyticsEventBuilder } from '../../../../util/analytics/AnalyticsEventBuilder'; import AppConstants from '../../../../../app/core/AppConstants'; import { downloadStateLogs } from '../../../../util/logs'; import AutoDetectTokensSettings from '../AutoDetectTokensSettings'; @@ -219,10 +220,6 @@ class AdvancedSettings extends PureComponent { * Object that represents the current route info like params passed to it */ route: PropTypes.object, - /** - * Analytics injected by withAnalyticsAwareness HOC - */ - analytics: PropTypes.object, /** * Boolean that checks if smart transactions is enabled */ @@ -281,9 +278,8 @@ class AdvancedSettings extends PureComponent { }; trackMetricsEvent = (event, properties) => { - this.props.analytics.trackEvent( - this.props.analytics - .createEventBuilder(event) + analytics.trackEvent( + AnalyticsEventBuilder.createEventBuilder(event) .addProperties({ location: 'Advanced Settings', ...properties, @@ -495,7 +491,4 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(setShowFiatOnTestnets(showFiatOnTestnets)), }); -export default connect( - mapStateToProps, - mapDispatchToProps, -)(withAnalyticsAwareness(AdvancedSettings)); +export default connect(mapStateToProps, mapDispatchToProps)(AdvancedSettings); diff --git a/app/components/Views/Settings/AdvancedSettings/index.test.tsx b/app/components/Views/Settings/AdvancedSettings/index.test.tsx index 3115be427c5..d991d5eb58b 100644 --- a/app/components/Views/Settings/AdvancedSettings/index.test.tsx +++ b/app/components/Views/Settings/AdvancedSettings/index.test.tsx @@ -68,30 +68,6 @@ jest.mock('../../../../core/Engine', () => { }; }); -// HOC mock must inject metrics; mock factory runs before imports so React is not in scope (hence require). -jest.mock( - '../../../../components/hooks/useAnalytics/withAnalyticsAwareness', - () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires -- hoisted mock factory - const ReactModule = require('react'); - return { - withAnalyticsAwareness: - (Component: unknown) => (props: Record) => - ReactModule.createElement(Component, { - ...props, - analytics: { - trackEvent: jest.fn(), - createEventBuilder: jest.fn(() => ({ - addProperties: jest.fn().mockReturnThis(), - build: jest.fn(), - })), - addTraitsToUser: jest.fn(), - }, - }), - }; - }, -); - describe('AdvancedSettings', () => { it('should render correctly', () => { const container = renderWithProvider( diff --git a/app/components/Views/Settings/AppInformation/__snapshots__/index.test.tsx.snap b/app/components/Views/Settings/AppInformation/__snapshots__/index.test.tsx.snap index 27881f7af5e..0a5a5487408 100644 --- a/app/components/Views/Settings/AppInformation/__snapshots__/index.test.tsx.snap +++ b/app/components/Views/Settings/AppInformation/__snapshots__/index.test.tsx.snap @@ -19,469 +19,397 @@ exports[`AppInformation renders correctly with snapshot 1`] = ` ] } > - - - - + - - - - - + + + - + testID="about-metamask-back-button" + > + + - - - - About MetaMask - + + About MetaMask + + + + + - - - - - - - - - - - - MetaMask v7.0.0 (1000) - - + + - Branch: undefined - - - - Links - - - - - Privacy Policy - - - + + - Terms of use + MetaMask v7.0.0 (1000) - - - Attributions + Branch: undefined - - - + - - - + + + + Privacy Policy + + + - Visit our support center - - - - + Terms of use + + + - Visit our website - - - - + Attributions + + + + + + - Contact us - - + + Visit our support center + + + + + Visit our website + + + + + Contact us + + + - - - + + + - - - - + + + /> + `; diff --git a/app/components/Views/Settings/Contacts/AmbiguousAddressSheet/__snapshots__/AmbiguousAddressSheet.test.tsx.snap b/app/components/Views/Settings/Contacts/AmbiguousAddressSheet/__snapshots__/AmbiguousAddressSheet.test.tsx.snap index ae6659ef275..62871afb6c2 100644 --- a/app/components/Views/Settings/Contacts/AmbiguousAddressSheet/__snapshots__/AmbiguousAddressSheet.test.tsx.snap +++ b/app/components/Views/Settings/Contacts/AmbiguousAddressSheet/__snapshots__/AmbiguousAddressSheet.test.tsx.snap @@ -20,511 +20,445 @@ exports[`AmbiguousAddressSheet should render correctly 1`] = ` } > - - - + + /> + + - + - AmbiguousAddress - + + AmbiguousAddress + + + - - - - - + - - - - + > + + - - - - This is a duplicate address - - + + - Your contact list shows this address on more than one chain. Be sure to select the correct contact before you send any funds. + This is a duplicate address - - - + + Your contact list shows this address on more than one chain. Be sure to select the correct contact before you send any funds. + + + - - Got it - - + + Got it + + + @@ -621,9 +644,9 @@ exports[`AmbiguousAddressSheet should render correctly 1`] = ` - - - + + + `; diff --git a/app/components/Views/Settings/Contacts/__snapshots__/index.test.tsx.snap b/app/components/Views/Settings/Contacts/__snapshots__/index.test.tsx.snap index e8f612d0b5a..e692f3ec2ea 100644 --- a/app/components/Views/Settings/Contacts/__snapshots__/index.test.tsx.snap +++ b/app/components/Views/Settings/Contacts/__snapshots__/index.test.tsx.snap @@ -19,473 +19,467 @@ exports[`Contacts renders correctly 1`] = ` ] } > - - - - + - - - - - + + + - + testID="back-arrow-button" + > + + - - - - Contacts - + + Contacts + + + + + - - - - - - - - - - - + + + + - Add contact - - - + + Add contact + + + + - - - - + + + /> + `; diff --git a/app/components/Views/Settings/DeveloperOptions/__snapshots__/index.test.tsx.snap b/app/components/Views/Settings/DeveloperOptions/__snapshots__/index.test.tsx.snap index dc6c317cdae..d086a420cb1 100644 --- a/app/components/Views/Settings/DeveloperOptions/__snapshots__/index.test.tsx.snap +++ b/app/components/Views/Settings/DeveloperOptions/__snapshots__/index.test.tsx.snap @@ -20,559 +20,476 @@ exports[`DeveloperOptions renders correctly 1`] = ` } > - - - + + /> + + + - + testID="back-arrow-button" + > + + - - - Developer options - + + - - - - - + - - - - - Sentry - - - Generate a developer test Sentry trace. - - + } + > + - Generate trace test + Sentry - - - Sample feature - - - A sample feature as a template for developers. - - - Navigate to sample feature + Generate a developer test Sentry trace. - - + > + + Generate trace test + + - Perps trading + Sample feature - - + + - Hyperliquid Network Toggle + Navigate to sample feature - + + } + > - Mainnet + Perps trading + + + Hyperliquid Network Toggle + + + + Mainnet + + - - - Predict Deposit - - - Trigger a Predict deposit confirmation. - - + } + } + > + Predict Deposit + + Trigger a Predict deposit confirmation. + + - Deposit + + Deposit + + + + Predict Claim - - - Predict Claim - - + Trigger a Predict claim confirmation. + + - Trigger a Predict claim confirmation. - - + + undefined, + ] + } + > + Claim + + - Claim + Predict Withdraw - - - Predict Withdraw - - - Trigger a Predict withdraw confirmation. - - - Withdraw + Trigger a Predict withdraw confirmation. - - - Perps Withdraw - - - Trigger a Perps withdraw confirmation. - - - - Withdraw - - - + + Withdraw + + - Card + Perps Withdraw - Reset Card onboarding state to start the onboarding flow from the beginning. + Trigger a Perps withdraw confirmation. - Reset Onboarding State + Withdraw + + + + + Card + + + Reset Card onboarding state to start the onboarding flow from the beginning. + + + Reset Onboarding State + + - - + + - - - + + + `; diff --git a/app/components/Views/Settings/GeneralSettings/index.js b/app/components/Views/Settings/GeneralSettings/index.js index c2880868666..273bb272cf4 100644 --- a/app/components/Views/Settings/GeneralSettings/index.js +++ b/app/components/Views/Settings/GeneralSettings/index.js @@ -31,7 +31,8 @@ import AvatarAccount, { } from '../../../../component-library/components/Avatars/Avatar/variants/AvatarAccount'; import { ThemeContext, mockTheme } from '../../../../util/theme'; import { selectCurrentCurrency } from '../../../../selectors/currencyRateController'; -import { withAnalyticsAwareness } from '../../../../components/hooks/useAnalytics/withAnalyticsAwareness'; +import { analytics } from '../../../../util/analytics/analytics'; +import { AnalyticsEventBuilder } from '../../../../util/analytics/AnalyticsEventBuilder'; import { selectSelectedInternalAccountFormattedAddress } from '../../../../selectors/accountsController'; import Text, { TextVariant, @@ -58,13 +59,12 @@ const infuraCurrencyOptions = sortedCurrencies.map( }), ); -export const updateUserTraitsWithCurrentCurrency = (currency, analytics) => { +export const updateUserTraitsWithCurrentCurrency = (currency) => { // track event and add selected currency to user profile for analytics const traits = { [UserProfileProperty.CURRENT_CURRENCY]: currency }; - analytics.addTraitsToUser(traits); + analytics.identify(traits); analytics.trackEvent( - analytics - .createEventBuilder(MetaMetricsEvents.CURRENCY_CHANGED) + AnalyticsEventBuilder.createEventBuilder(MetaMetricsEvents.CURRENCY_CHANGED) .addProperties({ ...traits, location: 'app_settings', @@ -73,13 +73,10 @@ export const updateUserTraitsWithCurrentCurrency = (currency, analytics) => { ); }; -export const updateUserTraitsWithCurrencyType = ( - primaryCurrency, - analytics, -) => { +export const updateUserTraitsWithCurrencyType = (primaryCurrency) => { // track event and add primary currency preference (fiat/crypto) to user profile for analytics const traits = { [UserProfileProperty.PRIMARY_CURRENCY]: primaryCurrency }; - analytics.addTraitsToUser(traits); + analytics.identify(traits); }; const createStyles = (colors) => @@ -210,10 +207,6 @@ class Settings extends PureComponent { * App theme */ // appTheme: PropTypes.string, - /** - * Analytics injected by withAnalyticsAwareness HOC - */ - analytics: PropTypes.object, }; state = { @@ -224,7 +217,7 @@ class Settings extends PureComponent { selectCurrency = async (currency) => { const { CurrencyRateController } = Engine.context; CurrencyRateController.setCurrentCurrency(currency); - updateUserTraitsWithCurrentCurrency(currency, this.props.analytics); + updateUserTraitsWithCurrentCurrency(currency); }; selectLanguage = (language) => { @@ -241,7 +234,7 @@ class Settings extends PureComponent { selectPrimaryCurrency = (primaryCurrency) => { this.props.setPrimaryCurrency(primaryCurrency); - updateUserTraitsWithCurrencyType(primaryCurrency, this.props.analytics); + updateUserTraitsWithCurrencyType(primaryCurrency); }; toggleHideZeroBalanceTokens = (toggleHideZeroBalanceTokens) => { @@ -567,7 +560,4 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(setHideZeroBalanceTokens(hideZeroBalanceTokens)), }); -export default connect( - mapStateToProps, - mapDispatchToProps, -)(withAnalyticsAwareness(Settings)); +export default connect(mapStateToProps, mapDispatchToProps)(Settings); diff --git a/app/components/Views/Settings/GeneralSettings/index.test.tsx b/app/components/Views/Settings/GeneralSettings/index.test.tsx index 426cb86a5ff..ff134026651 100644 --- a/app/components/Views/Settings/GeneralSettings/index.test.tsx +++ b/app/components/Views/Settings/GeneralSettings/index.test.tsx @@ -12,34 +12,29 @@ import { MetaMetricsEvents } from '../../../../core/Analytics'; import { UserProfileProperty } from '../../../../util/metrics/UserSettingsAnalyticsMetaData/UserProfileAnalyticsMetaData.types'; import { AvatarAccountType } from '../../../../component-library/components/Avatars/Avatar/variants/AvatarAccount'; import { ThemeContext, mockTheme } from '../../../../util/theme'; +import { analytics } from '../../../../util/analytics/analytics'; jest.mock('../../../../core/Analytics'); -const mockWithAnalyticsCreateEventBuilder = jest.fn(() => ({ - addProperties: jest.fn().mockReturnThis(), - build: jest.fn(), +jest.mock('../../../../util/analytics/analytics', () => ({ + analytics: { + identify: jest.fn(), + trackEvent: jest.fn(), + }, })); -jest.mock( - '../../../../components/hooks/useAnalytics/withAnalyticsAwareness', - () => ({ - withAnalyticsAwareness: - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (Component: React.ComponentType) => - (props: Record) => ( - - ), - }), -); +const mockAddProperties = jest.fn().mockReturnThis(); +const mockBuild = jest.fn().mockReturnValue({ name: 'CURRENCY_CHANGED' }); +const mockCreateEventBuilder = jest.fn().mockReturnValue({ + addProperties: mockAddProperties, + build: mockBuild, +}); + +jest.mock('../../../../util/analytics/AnalyticsEventBuilder', () => ({ + AnalyticsEventBuilder: { + createEventBuilder: (...args: unknown[]) => mockCreateEventBuilder(...args), + }, +})); jest.mock( '../../../../component-library/components/Avatars/Avatar/variants/AvatarAccount', () => ({ @@ -107,19 +102,6 @@ describe('GeneralSettings', () => { }); }); -const mockUpdateAddProperties = jest.fn().mockReturnThis(); -const mockUpdateBuild = jest.fn().mockReturnValue({ name: 'CURRENCY_CHANGED' }); -const mockUpdateCreateEventBuilder = jest.fn().mockReturnValue({ - addProperties: mockUpdateAddProperties, - build: mockUpdateBuild, -}); - -const mockAnalytics = { - addTraitsToUser: jest.fn(), - trackEvent: jest.fn(), - createEventBuilder: mockUpdateCreateEventBuilder, -}; - describe('updateUserTraitsWithCurrentCurrency', () => { afterEach(() => { jest.clearAllMocks(); @@ -128,9 +110,9 @@ describe('updateUserTraitsWithCurrentCurrency', () => { it('adds selected currency trait', () => { const mockCurrency = 'USD'; - updateUserTraitsWithCurrentCurrency(mockCurrency, mockAnalytics); + updateUserTraitsWithCurrentCurrency(mockCurrency); - expect(mockAnalytics.addTraitsToUser).toHaveBeenCalledWith({ + expect(analytics.identify).toHaveBeenCalledWith({ [UserProfileProperty.CURRENT_CURRENCY]: mockCurrency, }); }); @@ -138,23 +120,23 @@ describe('updateUserTraitsWithCurrentCurrency', () => { it('tracks currency changed event', () => { const mockCurrency = 'USD'; - updateUserTraitsWithCurrentCurrency(mockCurrency, mockAnalytics); + updateUserTraitsWithCurrentCurrency(mockCurrency); - expect(mockUpdateCreateEventBuilder).toHaveBeenCalledWith( + expect(mockCreateEventBuilder).toHaveBeenCalledWith( MetaMetricsEvents.CURRENCY_CHANGED, ); - expect(mockUpdateAddProperties).toHaveBeenCalledWith({ + expect(mockAddProperties).toHaveBeenCalledWith({ [UserProfileProperty.CURRENT_CURRENCY]: mockCurrency, location: 'app_settings', }); - expect(mockAnalytics.trackEvent).toHaveBeenCalledWith(mockUpdateBuild()); + expect(analytics.trackEvent).toHaveBeenCalledWith(mockBuild()); }); it('does not throw errors when a valid currency is passed', () => { const mockCurrency = 'USD'; expect(() => { - updateUserTraitsWithCurrentCurrency(mockCurrency, mockAnalytics); + updateUserTraitsWithCurrentCurrency(mockCurrency); }).not.toThrow(); }); }); @@ -167,18 +149,18 @@ describe('updateUserTraitsWithCurrencyType', () => { it('adds the primary currency preference', () => { const primaryCurrency = 'fiat'; - updateUserTraitsWithCurrencyType(primaryCurrency, mockAnalytics); + updateUserTraitsWithCurrencyType(primaryCurrency); - expect(mockAnalytics.addTraitsToUser).toHaveBeenCalledWith({ + expect(analytics.identify).toHaveBeenCalledWith({ [UserProfileProperty.PRIMARY_CURRENCY]: primaryCurrency, }); }); - it('does not throw errors if analytics object is properly passed', () => { + it('does not throw errors', () => { const primaryCurrency = 'fiat'; expect(() => { - updateUserTraitsWithCurrencyType(primaryCurrency, mockAnalytics); + updateUserTraitsWithCurrencyType(primaryCurrency); }).not.toThrow(); }); }); diff --git a/app/components/Views/Settings/SecuritySettings/Sections/MetaMetricsAndDataCollectionSection/__snapshots__/MetaMetricsAndDataCollectionSection.test.tsx.snap b/app/components/Views/Settings/SecuritySettings/Sections/MetaMetricsAndDataCollectionSection/__snapshots__/MetaMetricsAndDataCollectionSection.test.tsx.snap index c4c7908b577..440d00fb001 100644 --- a/app/components/Views/Settings/SecuritySettings/Sections/MetaMetricsAndDataCollectionSection/__snapshots__/MetaMetricsAndDataCollectionSection.test.tsx.snap +++ b/app/components/Views/Settings/SecuritySettings/Sections/MetaMetricsAndDataCollectionSection/__snapshots__/MetaMetricsAndDataCollectionSection.test.tsx.snap @@ -20,510 +20,533 @@ exports[`MetaMetricsAndDataCollectionSection render matches snapshot 1`] = ` } > - - - + + /> + + - + - MetaMetricsAndDataCollectionSection - + + MetaMetricsAndDataCollectionSection + + + - - - - - + - - - Participate in MetaMetrics - - + Participate in MetaMetrics + + + + [ + { + "alignSelf": "flex-start", + }, + { + "backgroundColor": "#b4b4b566", + "borderRadius": 16, + }, + ], + ] + } + testID="metametrics-switch" + thumbTintColor="#ffffff" + tintColor="#b4b4b566" + value={false} + /> + - - - Participate in MetaMetrics to help us make MetaMask better. - + Participate in MetaMetrics to help us make MetaMask better. + - Learn more. + + Learn more. + - - - + - - Data collection for marketing - - + Data collection for marketing + + + + [ + { + "alignSelf": "flex-start", + }, + { + "backgroundColor": "#b4b4b566", + "borderRadius": 16, + }, + ], + ] + } + testID="data-collection-switch" + thumbTintColor="#ffffff" + tintColor="#b4b4b566" + value={false} + /> + - - - Weโ€™ll use MetaMetrics to learn how you interact with our marketing communications. We may share relevant news (like product features and other materials). - + > + Weโ€™ll use MetaMetrics to learn how you interact with our marketing communications. We may share relevant news (like product features and other materials). + + - - - + + + `; diff --git a/app/components/Views/ShowDisplayMediaNFTSheet/__snapshots__/ShowDisplayNFTMediaSheet.test.tsx.snap b/app/components/Views/ShowDisplayMediaNFTSheet/__snapshots__/ShowDisplayNFTMediaSheet.test.tsx.snap index cdbc73c4bed..9d037d34287 100644 --- a/app/components/Views/ShowDisplayMediaNFTSheet/__snapshots__/ShowDisplayNFTMediaSheet.test.tsx.snap +++ b/app/components/Views/ShowDisplayMediaNFTSheet/__snapshots__/ShowDisplayNFTMediaSheet.test.tsx.snap @@ -20,351 +20,331 @@ exports[`ShowNftSheet render matches snapshot 1`] = ` } > - - - + + /> + + - + - ShowNftDisplayMedia - + + ShowNftDisplayMedia + + + - - - - - + - - - - - - - + /> + + - + + + + - - Display NFT media - + + Display NFT media + + - - - + + - + testID="button-icon" + > + + - - - To see an NFT, you need turn on - - - Display NFT media. - - - Displaying NFT media and data exposes your IP address to OpenSea or other third parties. NFT autodetection relies on this feature, and won't be available when this is turned off. - - - - - You can turn off Display NFT media in + To see an NFT, you need turn on - Settings > Security and privacy. + Display NFT media. - - - - + + Displaying NFT media and data exposes your IP address to OpenSea or other third parties. NFT autodetection relies on this feature, and won't be available when this is turned off. + + + + - Cancel + You can turn off Display NFT media in + + + Settings > Security and privacy. + - - + - - Confirm - - + + Cancel + + + + + Confirm + + + @@ -737,9 +760,9 @@ exports[`ShowNftSheet render matches snapshot 1`] = ` - - - + + + `; diff --git a/app/components/Views/ShowIpfsGatewaySheet/__snapshots__/ShowIpfsGatewaySheet.test.tsx.snap b/app/components/Views/ShowIpfsGatewaySheet/__snapshots__/ShowIpfsGatewaySheet.test.tsx.snap index f15d7a161ec..8ab682f3412 100644 --- a/app/components/Views/ShowIpfsGatewaySheet/__snapshots__/ShowIpfsGatewaySheet.test.tsx.snap +++ b/app/components/Views/ShowIpfsGatewaySheet/__snapshots__/ShowIpfsGatewaySheet.test.tsx.snap @@ -20,351 +20,331 @@ exports[`ShowIpfsGatewaySheet should render correctly 1`] = ` } > - - - + + /> + + - + - ShowIpfs - + + ShowIpfs + + + - - - - - + - - - - - - - + /> + + - - Show NFT - - - - - We use third-party services to show images of your NFTs stored on IPFS, display information related to ENS addresses entered in your browser's address bar, and fetch icons for different tokens. Your IP address may be exposed to these services when youโ€™re using them. - - - - - + + - Selecting - - - - - Confirm - - - turns on IPFS resolution. You can turn it off in - + /> + + Show NFT + + + - Settings > Security and privacy - - - at any time. - - - + We use third-party services to show images of your NFTs stored on IPFS, display information related to ENS addresses entered in your browser's address bar, and fetch icons for different tokens. Your IP address may be exposed to these services when youโ€™re using them. + + + + - Cancel + Selecting - - + + Confirm - + + turns on IPFS resolution. You can turn it off in + + + Settings > Security and privacy + + + at any time. + + + + + Cancel + + + + + Confirm + + + @@ -634,9 +657,9 @@ exports[`ShowIpfsGatewaySheet should render correctly 1`] = ` - - - + + + `; diff --git a/app/components/Views/ShowTokenIdSheet/__snapshots__/ShowTokenIdSheet.test.tsx.snap b/app/components/Views/ShowTokenIdSheet/__snapshots__/ShowTokenIdSheet.test.tsx.snap index 96b910338b7..5945928256f 100644 --- a/app/components/Views/ShowTokenIdSheet/__snapshots__/ShowTokenIdSheet.test.tsx.snap +++ b/app/components/Views/ShowTokenIdSheet/__snapshots__/ShowTokenIdSheet.test.tsx.snap @@ -20,351 +20,331 @@ exports[`ShowTokenId should render correctly 1`] = ` } > - - - + + /> + + - + - ShowTokenId - + + ShowTokenId + + + - - - - - + - - - - - - - + /> + + - + + + - Token ID - + + + Token ID + + + - - - + + /> + @@ -500,9 +523,9 @@ exports[`ShowTokenId should render correctly 1`] = ` - - - + + + `; diff --git a/app/components/Views/SuccessErrorSheet/utils.ts b/app/components/Views/SuccessErrorSheet/utils.ts index e9c655fa924..faf78b9a4bc 100644 --- a/app/components/Views/SuccessErrorSheet/utils.ts +++ b/app/components/Views/SuccessErrorSheet/utils.ts @@ -1,9 +1,9 @@ -import { NavigationProp, ParamListBase } from '@react-navigation/native'; import { SuccessErrorSheetParams } from './interface'; import Routes from '../../../constants/navigation/Routes'; +import { AppNavigationProp } from '../../../core/NavigationService/types'; export const navigateToSuccessErrorSheet = ( - navigation: NavigationProp, + navigation: AppNavigationProp, params: SuccessErrorSheetParams, ) => { navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, { @@ -15,7 +15,7 @@ export const navigateToSuccessErrorSheet = ( }; export const navigateToSuccessErrorSheetPromise = async ( - navigation: NavigationProp, + navigation: AppNavigationProp, params: SuccessErrorSheetParams, ) => new Promise((resolve) => { diff --git a/app/components/Views/TradeWalletActions/TradeWalletActions.test.tsx b/app/components/Views/TradeWalletActions/TradeWalletActions.test.tsx index 7d595c22677..ff3246b8d99 100644 --- a/app/components/Views/TradeWalletActions/TradeWalletActions.test.tsx +++ b/app/components/Views/TradeWalletActions/TradeWalletActions.test.tsx @@ -30,6 +30,16 @@ jest.mock('react-native-device-info', () => ({ getVersion: jest.fn().mockReturnValue('1.0.0'), })); +jest.mock('react-native-gesture-handler', () => { + const RN = jest.requireActual('react-native'); + const React = jest.requireActual('react'); + return { + ...jest.requireActual('react-native-gesture-handler'), + GestureHandlerRootView: RN.View, + GestureHandlerRootViewContext: React.createContext(true), + }; +}); + jest.mock('../../UI/Perps', () => ({ selectPerpsEnabledFlag: jest.fn(), })); @@ -277,8 +287,11 @@ jest.mock('../../../util/navigation/navUtils', () => ({ })); jest.mock('react-native-safe-area-context', () => { + const RN = jest.requireActual('react-native'); + const React = jest.requireActual('react'); const inset = { top: 0, right: 0, bottom: 0, left: 0 }; const frame = { width: 0, height: 0, x: 0, y: 0 }; + const SafeAreaInsetsContext = React.createContext(inset); return { SafeAreaProvider: jest.fn().mockImplementation(({ children }) => children), SafeAreaConsumer: jest @@ -286,6 +299,8 @@ jest.mock('react-native-safe-area-context', () => { .mockImplementation(({ children }) => children(inset)), useSafeAreaInsets: jest.fn().mockImplementation(() => inset), useSafeAreaFrame: jest.fn().mockImplementation(() => frame), + SafeAreaView: RN.View, + SafeAreaInsetsContext, }; }); diff --git a/app/components/Views/TransactionsView/__snapshots__/index.test.tsx.snap b/app/components/Views/TransactionsView/__snapshots__/index.test.tsx.snap index 8152a2ecec3..07e0e607ca8 100644 --- a/app/components/Views/TransactionsView/__snapshots__/index.test.tsx.snap +++ b/app/components/Views/TransactionsView/__snapshots__/index.test.tsx.snap @@ -20,295 +20,310 @@ exports[`TransactionsView Basic Rendering renders correctly and matches snapshot } > - - - + + /> + + - + - TransactionsView - + + TransactionsView + + + - - - - - + - @@ -318,15 +333,23 @@ exports[`TransactionsView Basic Rendering renders correctly and matches snapshot "flex": 1, } } - /> + > + + - - - + + + `; diff --git a/app/components/Views/TransactionsView/index.js b/app/components/Views/TransactionsView/index.js index 60ad329331d..01321865c94 100644 --- a/app/components/Views/TransactionsView/index.js +++ b/app/components/Views/TransactionsView/index.js @@ -3,7 +3,7 @@ import { StyleSheet, View } from 'react-native'; import PropTypes from 'prop-types'; import { connect, useSelector } from 'react-redux'; import { KnownCaipNamespace } from '@metamask/utils'; -import { withNavigation } from '@react-navigation/compat'; +import { useNavigation } from '@react-navigation/native'; import { showAlert } from '../../../actions/alert'; import Transactions from '../../UI/Transactions'; import { @@ -53,7 +53,6 @@ const styles = StyleSheet.create({ }); const TransactionsView = ({ - navigation, conversionRate, selectedInternalAccount, networkType, @@ -64,6 +63,7 @@ const TransactionsView = ({ addressBook, internalAccounts, }) => { + const navigation = useNavigation(); const [allTransactions, setAllTransactions] = useState([]); const [submittedTxs, setSubmittedTxs] = useState([]); const [confirmedTxs, setConfirmedTxs] = useState([]); @@ -244,10 +244,6 @@ TransactionsView.propTypes = { * InternalAccount object required to get account name, address and import time */ selectedInternalAccount: PropTypes.object, - /** - * navigation object required to push new views - */ - navigation: PropTypes.object, /** * An array that represents the user transactions */ @@ -313,7 +309,4 @@ const mapDispatchToProps = (dispatch) => ({ showAlert: (config) => dispatch(showAlert(config)), }); -export default connect( - mapStateToProps, - mapDispatchToProps, -)(withNavigation(TransactionsView)); +export default connect(mapStateToProps, mapDispatchToProps)(TransactionsView); diff --git a/app/components/Views/TransactionsView/index.test.js b/app/components/Views/TransactionsView/index.test.js new file mode 100644 index 00000000000..793d9c2d2ce --- /dev/null +++ b/app/components/Views/TransactionsView/index.test.js @@ -0,0 +1,206 @@ +/* eslint-disable react/prop-types */ +/* eslint-disable react/display-name */ +/* eslint-disable @typescript-eslint/no-require-imports */ +/* eslint-disable @typescript-eslint/no-var-requires */ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import { Provider } from 'react-redux'; +import { configureStore } from '@reduxjs/toolkit'; +import TransactionsView from './index'; +import { backgroundState } from '../../../util/test/initial-root-state'; + +const mockNavigation = { + navigate: jest.fn(), + setOptions: jest.fn(), + goBack: jest.fn(), +}; + +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: () => mockNavigation, +})); + +jest.mock('../../hooks/AssetPolling/useCurrencyRatePolling', () => + jest.fn(() => null), +); + +jest.mock('../../hooks/AssetPolling/useTokenRatesPolling', () => + jest.fn(() => null), +); + +jest.mock('../../UI/Transactions', () => { + const { View, Text } = require('react-native'); + const MockTransactions = ({ transactions, loading }) => ( + + {loading ? 'loading' : 'loaded'} + {transactions?.length || 0} + + ); + MockTransactions.displayName = 'MockTransactions'; + return MockTransactions; +}); + +const mockSelectedInternalAccount = { + address: '0x1234567890abcdef1234567890abcdef12345678', + metadata: { + importTime: Date.now() - 10000, + name: 'Account 1', + keyring: { + type: 'HD Key Tree', + }, + }, + type: 'eip155:eoa', +}; + +const mockTransaction = { + id: 'tx-1', + time: Date.now(), + status: 'confirmed', + chainId: '0x1', + txParams: { + from: '0x1234567890abcdef1234567890abcdef12345678', + to: '0xabcdef1234567890abcdef1234567890abcdef12', + value: '0x1', + nonce: '0x1', + }, +}; + +const createMockStore = (overrides = {}) => + configureStore({ + reducer: { + engine: () => ({ + backgroundState: { + ...backgroundState, + AccountsController: { + internalAccounts: { + accounts: { + '0x1234567890abcdef1234567890abcdef12345678': + mockSelectedInternalAccount, + }, + selectedAccount: '0x1234567890abcdef1234567890abcdef12345678', + }, + }, + AccountTrackerController: { + accountsByChainId: {}, + }, + CurrencyRateController: { + currentCurrency: 'USD', + currencyRates: { + ETH: { + conversionRate: 2000, + }, + }, + }, + TokensController: { + tokens: [], + allTokens: {}, + }, + TransactionController: { + transactions: [mockTransaction], + }, + NetworkController: { + selectedNetworkClientId: 'mainnet', + providerConfig: { + type: 'mainnet', + chainId: '0x1', + }, + networkConfigurations: {}, + }, + AddressBookController: { + addressBook: {}, + }, + NftController: { + allNfts: {}, + }, + TokenBalancesController: { + tokenBalances: {}, + }, + BridgeStatusController: { + bridgeHistory: {}, + }, + NetworkEnablementController: { + enabledNetworksByNamespace: { + eip155: { + '0x1': true, + }, + }, + enabledNetworks: { + '0x1': true, + }, + }, + ...overrides, + }, + }), + fiatOrders: () => ({ + orders: [], + }), + settings: () => ({ + showFiatOnTestnets: false, + }), + }, + }); + +describe('TransactionsView', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly', () => { + const store = createMockStore(); + const { getByTestId } = render( + + + , + ); + + expect(getByTestId('transactions-component')).toBeTruthy(); + }); + + it('passes transactions to Transactions component', () => { + const store = createMockStore(); + const { getByTestId } = render( + + + , + ); + + expect(getByTestId('transactions-count')).toBeTruthy(); + }); + + it('shows loading state initially', () => { + const store = createMockStore(); + const { getByTestId } = render( + + + , + ); + + expect(getByTestId('loading-state')).toBeTruthy(); + }); + + it('uses currency rate polling', () => { + const useCurrencyRatePolling = require('../../hooks/AssetPolling/useCurrencyRatePolling'); + const store = createMockStore(); + + render( + + + , + ); + + expect(useCurrencyRatePolling).toHaveBeenCalled(); + }); + + it('uses token rates polling', () => { + const useTokenRatesPolling = require('../../hooks/AssetPolling/useTokenRatesPolling'); + const store = createMockStore(); + + render( + + + , + ); + + expect(useTokenRatesPolling).toHaveBeenCalled(); + }); +}); diff --git a/app/components/Views/TransactionsView/index.test.tsx b/app/components/Views/TransactionsView/index.test.tsx index d35ec5b661c..76e73caf6fd 100644 --- a/app/components/Views/TransactionsView/index.test.tsx +++ b/app/components/Views/TransactionsView/index.test.tsx @@ -338,7 +338,6 @@ describe('TransactionsView', () => { diff --git a/app/components/Views/TrendingView/components/QuickActions/QuickActions.tsx b/app/components/Views/TrendingView/components/QuickActions/QuickActions.tsx index 841fa2576be..442b83e176b 100644 --- a/app/components/Views/TrendingView/components/QuickActions/QuickActions.tsx +++ b/app/components/Views/TrendingView/components/QuickActions/QuickActions.tsx @@ -20,6 +20,7 @@ import { type SectionIcon, } from '../../sections.config'; import { TrendingViewSelectorsIDs } from '../../TrendingView.testIds'; +import { AppNavigationProp } from '../../../../../core/NavigationService/types'; const SectionIconRenderer: React.FC<{ icon: SectionIcon; @@ -56,7 +57,7 @@ interface QuickActionsProps { * a corresponding button will automatically appear here. */ const QuickActions: React.FC = ({ emptySections }) => { - const navigation = useNavigation(); + const navigation = useNavigation(); const tw = useTailwind(); const sectionsArray = useSectionsArray(); diff --git a/app/components/Views/TrendingView/components/SectionHeader/SectionHeader.tsx b/app/components/Views/TrendingView/components/SectionHeader/SectionHeader.tsx index 30720ae0b30..c20a18237d1 100644 --- a/app/components/Views/TrendingView/components/SectionHeader/SectionHeader.tsx +++ b/app/components/Views/TrendingView/components/SectionHeader/SectionHeader.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { useNavigation } from '@react-navigation/native'; import { SectionId, SECTIONS_CONFIG } from '../../sections.config'; import SectionHeader from '../../../../../component-library/components-temp/SectionHeader'; +import { AppNavigationProp } from '../../../../../core/NavigationService/types'; export interface SectionHeaderProps { sectionId: SectionId; @@ -15,7 +16,7 @@ export interface SectionHeaderProps { * consistency between QuickActions buttons and section "View All" buttons. */ const TrendingSectionHeader: React.FC = ({ sectionId }) => { - const navigation = useNavigation(); + const navigation = useNavigation(); const sectionConfig = SECTIONS_CONFIG[sectionId]; return ( diff --git a/app/components/Views/TrendingView/sections.config.tsx b/app/components/Views/TrendingView/sections.config.tsx index e0f5af590b6..cf5149bf577 100644 --- a/app/components/Views/TrendingView/sections.config.tsx +++ b/app/components/Views/TrendingView/sections.config.tsx @@ -1,7 +1,8 @@ import React, { PropsWithChildren, useContext, useMemo } from 'react'; import Fuse, { type FuseOptions } from 'fuse.js'; -import type { NavigationProp, ParamListBase } from '@react-navigation/native'; +import type { NavigationProp } from '@react-navigation/native'; import type { TrendingAsset } from '@metamask/assets-controllers'; +import type { AppNavigationProp } from '../../../core/NavigationService/types'; import { useSelector } from 'react-redux'; import Routes from '../../../constants/navigation/Routes'; import { strings } from '../../../../locales/i18n'; @@ -54,18 +55,18 @@ export interface SectionConfig { id: SectionId; title: string; icon: SectionIcon; - viewAllAction: (navigation: NavigationProp) => void; + viewAllAction: (navigation: AppNavigationProp) => void; /** Returns a stable identifier for an item (e.g. assetId, symbol, url) used in analytics */ getItemIdentifier: (item: unknown) => string; RowItem: React.ComponentType<{ item: unknown; index: number; - navigation: NavigationProp; + navigation: AppNavigationProp; }>; OverrideRowItemSearch?: React.ComponentType<{ item: unknown; index?: number; - navigation: NavigationProp; + navigation: AppNavigationProp; }>; Skeleton: React.ComponentType; OverrideSkeletonSearch?: React.ComponentType; diff --git a/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap b/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap index 5ef9b666673..c7b8503acf5 100644 --- a/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap +++ b/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap @@ -20,483 +20,432 @@ exports[`Wallet Conditional Rendering should render banner when basic functional } > - - - + + /> + + - + - WalletView - + + WalletView + + + - - - - - + - - - - - - An error occurred - - - - - - Your information can't be shown. Donโ€™t worry, your wallet and funds are safe. + An error occurred - - - - - - + + + - If you keep getting this error, - - Save your Secret Recovery Phrase + Your information can't be shown. Donโ€™t worry, your wallet and funds are safe. - - and re-install the app. Remember: without your Secret Recovery Phrase, you can't restore your wallet. - + - - - Save your Secret Recovery Phrase - - - - Error message: - - + + - Copy - - - - - - + If you keep getting this error, + - View: Wallet -TypeError: Cannot read properties of undefined (reading 'key') + Save your Secret Recovery Phrase - - + + and re-install the app. Remember: without your Secret Recovery Phrase, you can't restore your wallet. + + - - - - + + - Describe what happened - - - + Error message: + + + + + Copy + + + + + + + + View: Wallet +TypeError: Cannot read properties of undefined (reading 'key') + + + + + + - - Contact support - - - + Describe what happened + + + - + + Contact support + + + - Try again - - + + Try again + + + - - + + - - - + + + `; @@ -779,483 +802,432 @@ exports[`Wallet Conditional Rendering should render loader when no selected acco } > - - - + + /> + + - + - WalletView - + + WalletView + + + - - - - - + - - - - - - An error occurred - - - - - - Your information can't be shown. Donโ€™t worry, your wallet and funds are safe. + An error occurred - - - - - - + + + - If you keep getting this error, - - Save your Secret Recovery Phrase + Your information can't be shown. Donโ€™t worry, your wallet and funds are safe. - - and re-install the app. Remember: without your Secret Recovery Phrase, you can't restore your wallet. - + - - - Save your Secret Recovery Phrase - - - - Error message: - - + + - Copy - - - - - - + If you keep getting this error, + - View: Wallet -TypeError: Cannot read properties of undefined (reading 'key') + Save your Secret Recovery Phrase - - + + and re-install the app. Remember: without your Secret Recovery Phrase, you can't restore your wallet. + + - - - - + + - Describe what happened - - - + Error message: + + + + + Copy + + + + + + + + View: Wallet +TypeError: Cannot read properties of undefined (reading 'key') + + + + + + - + + Describe what happened + + + + - Contact support - - - - + Contact support + + + - Try again - - + + Try again + + + - - + + - - - + + + `; @@ -1538,483 +1584,432 @@ exports[`Wallet should render correctly 1`] = ` } > - - - + + /> + + - + - WalletView - + + WalletView + + + - - - - - + - - - - - - - An error occurred - - - - - - - - - Your information can't be shown. Donโ€™t worry, your wallet and funds are safe. - - - + } + > + + An error occurred + - + + + - If you keep getting this error, - - Save your Secret Recovery Phrase + Your information can't be shown. Donโ€™t worry, your wallet and funds are safe. - - and re-install the app. Remember: without your Secret Recovery Phrase, you can't restore your wallet. - + - - - Save your Secret Recovery Phrase - - - - Error message: - - + + - Copy - - - - - - + If you keep getting this error, + - View: Wallet -TypeError: Cannot read properties of undefined (reading 'key') + Save your Secret Recovery Phrase - - + + and re-install the app. Remember: without your Secret Recovery Phrase, you can't restore your wallet. + + - - - - + + - Describe what happened - - - + Error message: + + + + + Copy + + + + + + + + View: Wallet +TypeError: Cannot read properties of undefined (reading 'key') + + + + + + - - Contact support - - - + Describe what happened + + + - + + Contact support + + + - Try again - - + + Try again + + + - - + + - - - + + + `; @@ -2297,483 +2366,432 @@ exports[`Wallet should render correctly when Solana support is enabled 1`] = ` } > - - - + > + + + - + - WalletView - + + WalletView + + + - - - - - + - - - - - - An error occurred - - - - - - Your information can't be shown. Donโ€™t worry, your wallet and funds are safe. + An error occurred - - - - - - + + + - If you keep getting this error, - - Save your Secret Recovery Phrase + Your information can't be shown. Donโ€™t worry, your wallet and funds are safe. - - and re-install the app. Remember: without your Secret Recovery Phrase, you can't restore your wallet. - + - - - Save your Secret Recovery Phrase - - - - Error message: - - + + - Copy + If you keep getting this error, + + + Save your Secret Recovery Phrase + + + and re-install the app. Remember: without your Secret Recovery Phrase, you can't restore your wallet. - + - - - + Save your Secret Recovery Phrase + + + + + Error message: + + + - View: Wallet -TypeError: Cannot read properties of undefined (reading 'key') + Copy - - - - - - - + + - Describe what happened - - - + + + View: Wallet +TypeError: Cannot read properties of undefined (reading 'key') + + + + + + - - Contact support - - - + Describe what happened + + + - + + Contact support + + + - Try again - - + + Try again + + + - - + + - - - + + + `; @@ -3056,483 +3148,432 @@ exports[`Wallet should render correctly when there are no detected tokens 1`] = } > - - - + + /> + + - + - WalletView - + + WalletView + + + - - - - - + - - - - - - An error occurred - - - - - - Your information can't be shown. Donโ€™t worry, your wallet and funds are safe. + An error occurred - - - - - - + + + - If you keep getting this error, - - Save your Secret Recovery Phrase + Your information can't be shown. Donโ€™t worry, your wallet and funds are safe. - - and re-install the app. Remember: without your Secret Recovery Phrase, you can't restore your wallet. - + - - - Save your Secret Recovery Phrase - - - - Error message: - - + + - Copy - - - - - - + If you keep getting this error, + - View: Wallet -TypeError: Cannot read properties of undefined (reading 'key') + Save your Secret Recovery Phrase - - + + and re-install the app. Remember: without your Secret Recovery Phrase, you can't restore your wallet. + + - - - - + + - Describe what happened - - - + Error message: + + + + + Copy + + + + + + + + View: Wallet +TypeError: Cannot read properties of undefined (reading 'key') + + + + + + - - Contact support - - - + Describe what happened + + + - + + Contact support + + + - Try again - - + + Try again + + + - - + + - - - + + + `; diff --git a/app/components/Views/WalletConnectSessions/__snapshots__/index.test.tsx.snap b/app/components/Views/WalletConnectSessions/__snapshots__/index.test.tsx.snap index 1a2088a1260..88fe439fd93 100644 --- a/app/components/Views/WalletConnectSessions/__snapshots__/index.test.tsx.snap +++ b/app/components/Views/WalletConnectSessions/__snapshots__/index.test.tsx.snap @@ -20,305 +20,328 @@ exports[`WalletConnectSessions does not render when not ready 1`] = ` } > - - - + + /> + + - + - WalletConnectSessionsView - + + WalletConnectSessionsView + + + - - - - - + - + > + + - - - + + + `; @@ -343,505 +366,528 @@ exports[`WalletConnectSessions renders empty component with no active sessions 1 } > - - - + + /> + + - + - WalletConnectSessionsView - + + WalletConnectSessionsView + + + - - - - - + - - - - - + + + - + testID="wallet-connect-sessions-back-button" + > + + - - - - WalletConnect sessions - + + WalletConnect sessions + + + + + - - - - - - - - + + - You have no active sessions - + + You have no active sessions + + - - - + + + - - - + + + `; @@ -866,712 +912,735 @@ exports[`WalletConnectSessions should render active sessions 1`] = ` } > - - - + + /> + + - + - WalletConnectSessionsView - - - - - - - - + WalletConnectSessionsView + + + + + + + + - - + - - - - - + + + - + testID="wallet-connect-sessions-back-button" + > + + - - - - WalletConnect sessions - + + WalletConnect sessions + + + + + - - - - - - - + + - + > + + + + + + + + - + Session 1 + + - + topic1 + + - - + https://example.com + - - + - - Session 1 - - - topic1 - - + - https://example.com - - - - - + useNativeDriver={true} + > + + + + + + - + Session 2 + + - + topic2 + + - - + https://example.org + - - - - Session 2 - - - topic2 - - - https://example.org - - - - - - + + + + + - - - + + + `; diff --git a/app/components/Views/confirmations/components/confirm/confirm-component.tsx b/app/components/Views/confirmations/components/confirm/confirm-component.tsx index 2315e4f1cfd..4541be03151 100755 --- a/app/components/Views/confirmations/components/confirm/confirm-component.tsx +++ b/app/components/Views/confirmations/components/confirm/confirm-component.tsx @@ -43,6 +43,7 @@ const TRANSACTION_TYPES_DISABLE_ALERT_BANNER = [ TransactionType.perpsDepositAndOrder, TransactionType.predictDeposit, TransactionType.predictWithdraw, + TransactionType.moneyAccountDeposit, ]; export enum ConfirmationLoader { diff --git a/app/components/Views/confirmations/components/developer/confirmations-developer-options/confirmations-developer-options.test.tsx b/app/components/Views/confirmations/components/developer/confirmations-developer-options/confirmations-developer-options.test.tsx index 19cacd58358..33ba2e778df 100644 --- a/app/components/Views/confirmations/components/developer/confirmations-developer-options/confirmations-developer-options.test.tsx +++ b/app/components/Views/confirmations/components/developer/confirmations-developer-options/confirmations-developer-options.test.tsx @@ -15,6 +15,10 @@ import { useConfirmNavigation } from '../../../hooks/useConfirmNavigation'; import { ConfirmationLoader } from '../../confirm/confirm-component'; import { ConfirmationsDeveloperOptions } from './confirmations-developer-options'; import { ConfirmationsDeveloperOptionsTestIds } from './confirmations-developer-options.testIds'; +import { + selectMoneyAccountDepositEnabledFlag, + selectMoneyAccountWithdrawEnabledFlag, +} from '../../../../../../selectors/featureFlagController/moneyAccount'; jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), @@ -52,11 +56,27 @@ jest.mock('../../../hooks/useConfirmNavigation', () => ({ useConfirmNavigation: jest.fn(), })); +jest.mock( + '../../../../../../selectors/featureFlagController/moneyAccount', + () => ({ + selectMoneyAccountDepositEnabledFlag: jest.fn(), + selectMoneyAccountWithdrawEnabledFlag: jest.fn(), + }), +); + const MOCK_ACCOUNT = '0x1234567890123456789012345678901234567890' as Hex; const MOCK_TRANSFER_DATA = '0xabcdef' as Hex; const MOCK_ARBITRUM_USDC = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' as Hex; +const MOCK_POLYGON_USDCE = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174' as Hex; +const MOCK_PROXY_ADDRESS = '0x13032833b30f3388208cda38971fdc839936b042' as Hex; const MOCK_NETWORK_CLIENT_ID = 'arbitrum-mainnet'; const mockNavigateToConfirmation = jest.fn(); +const mockSelectMoneyAccountDepositEnabledFlag = jest.mocked( + selectMoneyAccountDepositEnabledFlag, +); +const mockSelectMoneyAccountWithdrawEnabledFlag = jest.mocked( + selectMoneyAccountWithdrawEnabledFlag, +); describe('ConfirmationsDeveloperOptions', () => { const mockUseSelector = jest.mocked(useSelector); @@ -96,6 +116,8 @@ describe('ConfirmationsDeveloperOptions', () => { mockUseConfirmNavigation.mockReturnValue({ navigateToConfirmation: mockNavigateToConfirmation, } as never); + mockSelectMoneyAccountDepositEnabledFlag.mockReturnValue(false); + mockSelectMoneyAccountWithdrawEnabledFlag.mockReturnValue(false); mockUseSelector.mockImplementation((( selector: (state: object) => unknown, ) => selector({})) as typeof useSelector); @@ -157,4 +179,150 @@ describe('ConfirmationsDeveloperOptions', () => { ], }); }); + + describe('Money Account Deposit', () => { + it('renders when moneyAccountDepositEnabled flag is true', () => { + mockSelectMoneyAccountDepositEnabledFlag.mockReturnValue(true); + + const { getByText, getByTestId } = render( + , + ); + + expect(getByText('Money Account Deposit')).toBeOnTheScreen(); + expect( + getByText('Trigger a Money Account deposit confirmation.'), + ).toBeOnTheScreen(); + expect( + getByTestId( + ConfirmationsDeveloperOptionsTestIds.MONEY_ACCOUNT_DEPOSIT_BUTTON, + ), + ).toBeOnTheScreen(); + }); + + it('does not render when moneyAccountDepositEnabled flag is false', () => { + mockSelectMoneyAccountDepositEnabledFlag.mockReturnValue(false); + + const { queryByTestId } = render(); + + expect( + queryByTestId( + ConfirmationsDeveloperOptionsTestIds.MONEY_ACCOUNT_DEPOSIT_BUTTON, + ), + ).toBeNull(); + }); + + it('triggers money account deposit transaction batch on press', async () => { + mockSelectMoneyAccountDepositEnabledFlag.mockReturnValue(true); + + const { getByTestId } = render(); + + await act(async () => { + fireEvent.press( + getByTestId( + ConfirmationsDeveloperOptionsTestIds.MONEY_ACCOUNT_DEPOSIT_BUTTON, + ), + ); + }); + + expect(mockNavigateToConfirmation).toHaveBeenCalledWith({ + loader: ConfirmationLoader.CustomAmount, + stack: Routes.PREDICT.ROOT, + }); + expect(mockAddTransactionBatch).toHaveBeenCalledWith({ + from: MOCK_ACCOUNT, + origin: ORIGIN_METAMASK, + networkClientId: MOCK_NETWORK_CLIENT_ID, + disableHook: true, + disableSequential: true, + transactions: [ + { + params: { + to: MOCK_PROXY_ADDRESS, + value: '0x1', + }, + }, + { + params: { + to: MOCK_POLYGON_USDCE, + data: MOCK_TRANSFER_DATA, + }, + type: TransactionType.moneyAccountDeposit, + }, + ], + }); + }); + }); + + describe('Money Account Withdraw', () => { + it('renders when moneyAccountWithdrawEnabled flag is true', () => { + mockSelectMoneyAccountWithdrawEnabledFlag.mockReturnValue(true); + + const { getByText, getByTestId } = render( + , + ); + + expect(getByText('Money Account Withdraw')).toBeOnTheScreen(); + expect( + getByText('Trigger a Money Account withdraw confirmation.'), + ).toBeOnTheScreen(); + expect( + getByTestId( + ConfirmationsDeveloperOptionsTestIds.MONEY_ACCOUNT_WITHDRAW_BUTTON, + ), + ).toBeOnTheScreen(); + }); + + it('does not render when moneyAccountWithdrawEnabled flag is false', () => { + mockSelectMoneyAccountWithdrawEnabledFlag.mockReturnValue(false); + + const { queryByTestId } = render(); + + expect( + queryByTestId( + ConfirmationsDeveloperOptionsTestIds.MONEY_ACCOUNT_WITHDRAW_BUTTON, + ), + ).toBeNull(); + }); + + it('triggers money account withdraw transaction batch on press', async () => { + mockSelectMoneyAccountWithdrawEnabledFlag.mockReturnValue(true); + + const { getByTestId } = render(); + + await act(async () => { + fireEvent.press( + getByTestId( + ConfirmationsDeveloperOptionsTestIds.MONEY_ACCOUNT_WITHDRAW_BUTTON, + ), + ); + }); + + expect(mockNavigateToConfirmation).toHaveBeenCalledWith({ + loader: ConfirmationLoader.CustomAmount, + stack: Routes.PREDICT.ROOT, + }); + expect(mockAddTransactionBatch).toHaveBeenCalledWith({ + from: MOCK_ACCOUNT, + origin: ORIGIN_METAMASK, + networkClientId: MOCK_NETWORK_CLIENT_ID, + disableHook: true, + disableSequential: true, + transactions: [ + { + params: { + to: MOCK_PROXY_ADDRESS, + value: '0x1', + }, + }, + { + params: { + to: MOCK_POLYGON_USDCE, + data: MOCK_TRANSFER_DATA, + }, + type: TransactionType.moneyAccountWithdraw, + }, + ], + }); + }); + }); }); diff --git a/app/components/Views/confirmations/components/developer/confirmations-developer-options/confirmations-developer-options.testIds.ts b/app/components/Views/confirmations/components/developer/confirmations-developer-options/confirmations-developer-options.testIds.ts index 8c9bf3ec0fe..70b522c4ec5 100644 --- a/app/components/Views/confirmations/components/developer/confirmations-developer-options/confirmations-developer-options.testIds.ts +++ b/app/components/Views/confirmations/components/developer/confirmations-developer-options/confirmations-developer-options.testIds.ts @@ -1,4 +1,8 @@ export const ConfirmationsDeveloperOptionsTestIds = { PERPS_WITHDRAW_BUTTON: 'confirmations-developer-options-perps-withdraw-button', + MONEY_ACCOUNT_DEPOSIT_BUTTON: + 'confirmations-developer-options-money-account-deposit-button', + MONEY_ACCOUNT_WITHDRAW_BUTTON: + 'confirmations-developer-options-money-account-withdraw-button', } as const; diff --git a/app/components/Views/confirmations/components/developer/confirmations-developer-options/confirmations-developer-options.tsx b/app/components/Views/confirmations/components/developer/confirmations-developer-options/confirmations-developer-options.tsx index b72287a99d2..8b6be8237dd 100644 --- a/app/components/Views/confirmations/components/developer/confirmations-developer-options/confirmations-developer-options.tsx +++ b/app/components/Views/confirmations/components/developer/confirmations-developer-options/confirmations-developer-options.tsx @@ -25,6 +25,10 @@ import { selectSelectedInternalAccountAddress } from '../../../../../../selector import { RootState } from '../../../../../../reducers'; import { ConfirmationsDeveloperOptionsTestIds } from './confirmations-developer-options.testIds'; import { ARBITRUM_USDC } from '../../../constants/perps'; +import { + selectMoneyAccountDepositEnabledFlag, + selectMoneyAccountWithdrawEnabledFlag, +} from '../../../../../../selectors/featureFlagController/moneyAccount'; const POLYGON_USDCE_ADDRESS = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174' as Hex; @@ -33,12 +37,21 @@ const POLYGON_USDCE_ADDRESS = const PROXY_ADDRESS = '0x13032833b30f3388208cda38971fdc839936b042' as Hex; export function ConfirmationsDeveloperOptions() { + const isMoneyAccountDepositEnabled = useSelector( + selectMoneyAccountDepositEnabledFlag, + ); + const isMoneyAccountWithdrawEnabled = useSelector( + selectMoneyAccountWithdrawEnabledFlag, + ); + return ( <> + {isMoneyAccountDepositEnabled && } + {isMoneyAccountWithdrawEnabled && } ); } @@ -125,6 +138,50 @@ function PredictDeposit() { ); } +function MoneyAccountDeposit() { + const { addTransactionBatchAndNavigate } = useAddTransactionBatch(); + + const handleDeposit = useCallback(() => { + addTransactionBatchAndNavigate({ + loader: ConfirmationLoader.CustomAmount, + transactionType: TransactionType.moneyAccountDeposit, + }); + }, [addTransactionBatchAndNavigate]); + + return ( + + ); +} + +function MoneyAccountWithdraw() { + const { addTransactionBatchAndNavigate } = useAddTransactionBatch(); + + const handleWithdraw = useCallback(() => { + addTransactionBatchAndNavigate({ + loader: ConfirmationLoader.CustomAmount, + transactionType: TransactionType.moneyAccountWithdraw, + }); + }, [addTransactionBatchAndNavigate]); + + return ( + + ); +} + function useAddTransactionBatch() { const selectedAccount = useSelector(selectSelectedInternalAccountAddress); const { navigateToConfirmation } = useConfirmNavigation(); diff --git a/app/components/Views/confirmations/components/footer/footer.test.tsx b/app/components/Views/confirmations/components/footer/footer.test.tsx index f2809386d5f..89c6df8bf8c 100644 --- a/app/components/Views/confirmations/components/footer/footer.test.tsx +++ b/app/components/Views/confirmations/components/footer/footer.test.tsx @@ -5,9 +5,14 @@ import { ConfirmationFooterSelectorIDs } from '../../ConfirmationView.testIds'; import AppConstants from '../../../../../core/AppConstants'; import renderWithProvider from '../../../../../util/test/renderWithProvider'; import { + getAppStateForConfirmation, personalSignatureConfirmationState, stakingDepositConfirmationState, } from '../../../../../util/test/confirm-data-helpers'; +import { + TransactionMeta, + TransactionType, +} from '@metamask/transaction-controller'; // eslint-disable-next-line import-x/no-namespace import * as QRHardwareHook from '../../context/qr-hardware-context/qr-hardware-context'; import { Footer } from './footer'; @@ -243,6 +248,38 @@ describe('Footer', () => { ).toBe(true); }); + it('hides footer by default for moneyAccountDeposit transaction type', () => { + mockUseConfirmationContext.mockReturnValue({ + isFooterVisible: undefined, + isTransactionDataUpdating: false, + isTransactionValueUpdating: false, + setIsFooterVisible: jest.fn(), + setIsTransactionDataUpdating: jest.fn(), + setIsTransactionValueUpdating: jest.fn(), + }); + + const moneyAccountDepositConfirmation = { + chainId: '0x89', + id: 'money-account-deposit-id', + networkClientId: 'polygon', + origin: 'metamask', + txParams: { + from: '0x935e73edb9ff52e23bac7f7e043a1ecd06d05477', + to: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', + value: '0x0', + }, + type: TransactionType.moneyAccountDeposit, + } as unknown as TransactionMeta; + + const { queryByTestId } = renderWithProvider(