diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d14fefe82717..5d1b4d73bdab 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,8 +2,15 @@ name: Main on: push: - branches: [develop, master] + branches: + - develop + - master pull_request: + types: + - opened + - reopened + - synchronize + merge_group: jobs: check-workflows: @@ -21,11 +28,25 @@ jobs: run: ${{ steps.download-actionlint.outputs.executable }} -color shell: bash + run-tests: + name: Run tests + uses: ./.github/workflows/run-tests.yml + + sonarcloud: + name: SonarCloud + uses: ./.github/workflows/sonarcloud.yml + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + needs: + - run-tests + all-jobs-completed: name: All jobs completed runs-on: ubuntu-latest needs: - check-workflows + - run-tests + - sonarcloud outputs: PASSED: ${{ steps.set-output.outputs.PASSED }} steps: @@ -37,7 +58,8 @@ jobs: name: All jobs pass if: ${{ always() }} runs-on: ubuntu-latest - needs: all-jobs-completed + needs: + - all-jobs-completed steps: - name: Check that all jobs have passed run: | diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 77958f69da2d..3cb7c50e573a 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,16 +1,14 @@ name: Run tests on: - push: - branches: - - develop - - master - pull_request: - types: - - opened - - reopened - - synchronize - merge_group: + workflow_call: + outputs: + current-coverage: + description: Current coverage + value: ${{ jobs.report-coverage.outputs.current-coverage }} + stored-coverage: + description: Stored coverage + value: ${{ jobs.report-coverage.outputs.stored-coverage }} jobs: test-unit: @@ -79,18 +77,19 @@ jobs: name: coverage-integration path: coverage/integration/coverage-integration.json - sonarcloud: - name: SonarCloud + report-coverage: + name: Report coverage runs-on: ubuntu-latest needs: - test-unit - test-webpack - test-integration + outputs: + current-coverage: ${{ steps.get-current-coverage.outputs.CURRENT_COVERAGE }} + stored-coverage: ${{ steps.get-stored-coverage.outputs.STORED_COVERAGE }} steps: - name: Checkout repository uses: actions/checkout@v4 - with: - fetch-depth: 0 # Shallow clones should be disabled for better relevancy of analysis - name: Setup environment uses: metamask/github-tools/.github/actions/setup-environment@main @@ -109,35 +108,28 @@ jobs: name: lcov.info path: coverage/lcov.info - - name: Get Sonar coverage - id: get-sonar-coverage - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + - name: Get current coverage + id: get-current-coverage + run: | + current_coverage=$(yarn nyc report --reporter=text-summary | grep 'Lines' | awk '{gsub(/%/, ""); print int($3)}') + echo "The current coverage is $current_coverage%." + echo 'CURRENT_COVERAGE='"$current_coverage" >> "$GITHUB_OUTPUT" + + - name: Get stored coverage + id: get-stored-coverage run: | - projectKey=$(grep 'sonar.projectKey=' sonar-project.properties | cut -d'=' -f2) - sonar_coverage=$(curl --silent --header "Authorization: Bearer $SONAR_TOKEN" "https://sonarcloud.io/api/measures/component?component=$projectKey&metricKeys=coverage" | jq -r '.component.measures[0].value // 0') - echo "The Sonar coverage of $projectKey is $sonar_coverage%." - echo 'SONAR_COVERAGE='"$sonar_coverage" >> "$GITHUB_OUTPUT" + stored_coverage=$(jq ".coverage" coverage.json) + echo "The stored coverage is $stored_coverage%." + echo 'STORED_COVERAGE='"$stored_coverage" >> "$GITHUB_OUTPUT" - name: Validate test coverage env: - SONAR_COVERAGE: ${{ steps.get-sonar-coverage.outputs.SONAR_COVERAGE }} + CURRENT_COVERAGE: ${{ steps.get-current-coverage.outputs.CURRENT_COVERAGE }} + STORED_COVERAGE: ${{ steps.get-stored-coverage.outputs.STORED_COVERAGE }} run: | - coverage=$(yarn nyc report --reporter=text-summary | grep 'Lines' | awk '{gsub(/%/, ""); print $3}') - if [ -z "$coverage" ]; then - echo "::error::Could not retrieve test coverage." - exit 1 - fi - if (( $(echo "$coverage < $SONAR_COVERAGE" | bc -l) )); then - echo "::error::Quality gate failed for test coverage. Current test coverage is $coverage%, please increase coverage to at least $SONAR_COVERAGE%." + if (( $(echo "$CURRENT_COVERAGE < $STORED_COVERAGE" | bc -l) )); then + echo "::error::Quality gate failed for test coverage. Current coverage is $CURRENT_COVERAGE%, please increase coverage to at least $STORED_COVERAGE%." exit 1 else - echo "Test coverage is $coverage%. Quality gate passed." + echo "The current coverage is $CURRENT_COVERAGE%, stored coverage is $STORED_COVERAGE%. Quality gate passed." fi - - - name: SonarCloud Scan - # This is SonarSource/sonarcloud-github-action@v2.0.0 - uses: SonarSource/sonarcloud-github-action@4b4d7634dab97dcee0b75763a54a6dc92a9e6bc1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml new file mode 100644 index 000000000000..460d5c140462 --- /dev/null +++ b/.github/workflows/sonarcloud.yml @@ -0,0 +1,30 @@ +name: SonarCloud + +on: + workflow_call: + secrets: + SONAR_TOKEN: + required: true + +jobs: + sonarcloud: + name: SonarCloud + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Shallow clones should be disabled for better relevancy of analysis + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: lcov.info + path: coverage + + - name: SonarCloud Scan + # This is SonarSource/sonarcloud-github-action@v2.0.0 + uses: SonarSource/sonarcloud-github-action@4b4d7634dab97dcee0b75763a54a6dc92a9e6bc1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/update-coverage.yml b/.github/workflows/update-coverage.yml new file mode 100644 index 000000000000..f246bde7eb32 --- /dev/null +++ b/.github/workflows/update-coverage.yml @@ -0,0 +1,48 @@ +name: Update coverage + +on: + schedule: + # Once per day at midnight UTC + - cron: 0 0 * * * + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + run-tests: + name: Run tests + uses: ./.github/workflows/run-tests.yml + + update-coverage: + if: ${{ needs.run-tests.outputs.current-coverage > needs.run-tests.outputs.stored-coverage }} + name: Update coverage + runs-on: ubuntu-latest + needs: + - run-tests + env: + CURRENT_COVERAGE: ${{ needs.run-tests.outputs.current-coverage }} + STORED_COVERAGE: ${{ needs.run-tests.outputs.stored-coverage }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Update coverage + run: | + echo "{ \"coverage\": $CURRENT_COVERAGE }" > coverage.json + + - name: Checkout/create branch, commit, and force push + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b metamaskbot/update-coverage + git add coverage.json + git commit -m "chore: Update coverage.json" + git push -f origin metamaskbot/update-coverage + + - name: Create/update pull request + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr create --title "chore: Update coverage.json" --body "This PR is automatically opened to update the coverage.json file when test coverage increases. Coverage increased from $STORED_COVERAGE% to $CURRENT_COVERAGE%." --base develop --head metamaskbot/update-coverage || gh pr edit --body "This PR is automatically opened to update the coverage.json file when test coverage increases. Coverage increased from $STORED_COVERAGE% to $CURRENT_COVERAGE%." diff --git a/.metamaskrc.dist b/.metamaskrc.dist index 601105e2af44..fc2a5a831a4b 100644 --- a/.metamaskrc.dist +++ b/.metamaskrc.dist @@ -45,3 +45,7 @@ BLOCKAID_PUBLIC_KEY= ; Enable/disable why did you render debug tool: https://github.com/welldone-software/why-did-you-render ; This should NEVER be enabled in production since it slows down react ; ENABLE_WHY_DID_YOU_RENDER=false + +; API key used in Etherscan requests to prevent rate limiting. +; Only applies to Mainnet and Sepolia. +; ETHERSCAN_API_KEY= diff --git a/.prettierignore b/.prettierignore index 9c4b3868464b..d8d8cfe4a15c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +1,8 @@ *.scss .nyc_output/**/* node_modules/**/* +# Exclude lottie json files +/app/images/animations/**/*.json /app/vendor/** /builds/**/* /coverage/**/* diff --git a/.storybook/test-data.js b/.storybook/test-data.js index 72a9bc3b78aa..de94b69f857e 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -676,7 +676,7 @@ const state = { welcomeScreenSeen: false, currentLocale: 'en', preferences: { - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, }, incomingTransactionsPreferences: { [CHAIN_IDS.MAINNET]: true, diff --git a/app/_locales/am/messages.json b/app/_locales/am/messages.json index 4ce44a388ac1..f118bc17df41 100644 --- a/app/_locales/am/messages.json +++ b/app/_locales/am/messages.json @@ -235,10 +235,6 @@ "fast": { "message": "ፈጣን" }, - "fiat": { - "message": "ፊያት", - "description": "Exchange type" - }, "fileImportFail": { "message": "ፋይል ማስመጣት እየሰራ አይደለም? እዚህ ላይ ጠቅ ያድርጉ!", "description": "Helps user import their account from a JSON file" @@ -493,12 +489,6 @@ "prev": { "message": "የቀደመ" }, - "primaryCurrencySetting": { - "message": "ተቀዳሚ የገንዘብ ዓይነት" - }, - "primaryCurrencySettingDescription": { - "message": "ዋጋዎች በራሳቸው የሰንሰለት ገንዘብ ዓይነት (ለምሳሌ ETH) በቅድሚያ እንዲታዪ ይምረጡ። ዋጋዎች በተመረጠ የፊያት ገንዘብ ዓይነት እንዲታዩ ደግሞ ፊያትን ይምረጡ።" - }, "privacyMsg": { "message": "የግለኝነት መጠበቂያ ህግ" }, @@ -750,9 +740,6 @@ "unlockMessage": { "message": "ያልተማከለ ድር ይጠባበቃል" }, - "updatedWithDate": { - "message": "የዘመነ $1" - }, "urlErrorMsg": { "message": "URIs አግባብነት ያለው የ HTTP/HTTPS ቅድመ ቅጥያ ይፈልጋል።" }, diff --git a/app/_locales/ar/messages.json b/app/_locales/ar/messages.json index 6cb79c56b136..d9717df6b190 100644 --- a/app/_locales/ar/messages.json +++ b/app/_locales/ar/messages.json @@ -505,12 +505,6 @@ "prev": { "message": "السابق" }, - "primaryCurrencySetting": { - "message": "العملة الأساسية" - }, - "primaryCurrencySettingDescription": { - "message": "حدد خيار \"المحلية\" لتحديد أولويات عرض القيم بالعملة المحلية للسلسلة (مثلاً ETH). حدد Fiat لتحديد أولويات عرض القيم بعملات fiat المحددة الخاصة بك." - }, "privacyMsg": { "message": "سياسة الخصوصية" }, @@ -762,9 +756,6 @@ "unlockMessage": { "message": "شبكة الويب اللامركزية بانتظارك" }, - "updatedWithDate": { - "message": "تم تحديث $1" - }, "urlErrorMsg": { "message": "تتطلب الروابط بادئة HTTP/HTTPS مناسبة." }, diff --git a/app/_locales/bg/messages.json b/app/_locales/bg/messages.json index 1fa7a14393d4..749b1561dafe 100644 --- a/app/_locales/bg/messages.json +++ b/app/_locales/bg/messages.json @@ -504,12 +504,6 @@ "prev": { "message": "Предишен" }, - "primaryCurrencySetting": { - "message": "Основна валута" - }, - "primaryCurrencySettingDescription": { - "message": "Изберете местна, за да приоритизирате показването на стойности в основната валута на веригата (например ETH). Изберете Fiat, за да поставите приоритет на показването на стойности в избраната от вас fiat валута." - }, "privacyMsg": { "message": "Политика за поверителност" }, @@ -761,9 +755,6 @@ "unlockMessage": { "message": "Децентрализираната мрежа очаква" }, - "updatedWithDate": { - "message": "Актуализирано $1 " - }, "urlErrorMsg": { "message": "URI изискват съответния HTTP / HTTPS префикс." }, diff --git a/app/_locales/bn/messages.json b/app/_locales/bn/messages.json index a9cc5aa0d845..15acaa2e6765 100644 --- a/app/_locales/bn/messages.json +++ b/app/_locales/bn/messages.json @@ -241,10 +241,6 @@ "fast": { "message": "দ্রুত" }, - "fiat": { - "message": "ফিয়াট", - "description": "Exchange type" - }, "fileImportFail": { "message": "ফাইল আমদানি কাজ করছে না? এখানে ক্লিক করুন!", "description": "Helps user import their account from a JSON file" @@ -502,12 +498,6 @@ "prev": { "message": "পূর্ববর্তী" }, - "primaryCurrencySetting": { - "message": "প্রাথমিক মুদ্রা" - }, - "primaryCurrencySettingDescription": { - "message": "চেনটিতে (যেমন ETH) দেশীয় মুদ্রায় মানগুলি প্রদর্শনকে অগ্রাধিকার দিতে দেশীয় নির্বাচন করুন। আপনার নির্দেশিত মুদ্রায় মানগুলির প্রদর্শনকে অগ্রাধিকার দিতে নির্দেশিত নির্বাচন করুন।" - }, "privacyMsg": { "message": "সম্মত হয়েছেন" }, @@ -759,9 +749,6 @@ "unlockMessage": { "message": "ছড়িয়ে ছিটিয়ে থাকা ওয়েব অপেক্ষা করছে" }, - "updatedWithDate": { - "message": "আপডেট করা $1" - }, "urlErrorMsg": { "message": "URI গুলির যথাযথ HTTP/HTTPS প্রেফিক্সের প্রয়োজন।" }, diff --git a/app/_locales/ca/messages.json b/app/_locales/ca/messages.json index 92f2b2771ff9..fc9e2afb41e6 100644 --- a/app/_locales/ca/messages.json +++ b/app/_locales/ca/messages.json @@ -489,12 +489,6 @@ "personalAddressDetected": { "message": "Adreça personal detectada. Introduir l'adreça del contracte de fitxa." }, - "primaryCurrencySetting": { - "message": "Divisa principal" - }, - "primaryCurrencySettingDescription": { - "message": "Selecciona Natiu per a prioritzar la mostra de valors en la divisa nadiua de la cadena (p. ex. ETH). Selecciona Fiat per prioritzar la mostra de valors en la divisa fiduciària seleccionada." - }, "privacyMsg": { "message": "Política de privadesa" }, @@ -740,9 +734,6 @@ "unlockMessage": { "message": "La web descentralitzada està esperant" }, - "updatedWithDate": { - "message": "Actualitzat $1" - }, "urlErrorMsg": { "message": "Els URIs requereixen el prefix HTTP/HTTPS apropiat." }, diff --git a/app/_locales/cs/messages.json b/app/_locales/cs/messages.json index 6e3bfa315303..4113f8c5cc42 100644 --- a/app/_locales/cs/messages.json +++ b/app/_locales/cs/messages.json @@ -105,10 +105,6 @@ "failed": { "message": "Neúspěšné" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "Import souboru nefunguje? Klikněte sem!", "description": "Helps user import their account from a JSON file" diff --git a/app/_locales/da/messages.json b/app/_locales/da/messages.json index 12eba292e0a4..37e4663523cf 100644 --- a/app/_locales/da/messages.json +++ b/app/_locales/da/messages.json @@ -489,12 +489,6 @@ "prev": { "message": "Forrige" }, - "primaryCurrencySetting": { - "message": "Primær Valuta" - }, - "primaryCurrencySettingDescription": { - "message": "Vælg lokal for fortrinsvis at vise værdier i kædens (f.eks. ETH) lokale valuta. Vælg Fiat for fortrinsvis at vise værdier i din valgte fiat valuta." - }, "privacyMsg": { "message": "Privatlivspolitik" }, @@ -734,9 +728,6 @@ "unlockMessage": { "message": "Det decentraliserede internet venter" }, - "updatedWithDate": { - "message": "Opdaterede $1" - }, "urlErrorMsg": { "message": "Links kræver det rette HTTP/HTTPS-præfix." }, diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 5d5f0e30923f..92cc0f25f1a7 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "Details zur Gebühr" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "Dateiimport fehlgeschlagen? Bitte hier klicken!", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "MetaMask ist nicht mit dieser Website verbunden" }, - "noConversionDateAvailable": { - "message": "Kein Umrechnungskursdaten verfügbar" - }, "noConversionRateAvailable": { "message": "Kein Umrechnungskurs verfügbar" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "Preis nicht verfügbar" }, - "primaryCurrencySetting": { - "message": "Hauptwährung" - }, - "primaryCurrencySettingDescription": { - "message": "Wählen Sie 'Nativ', um dem Anzeigen von Werten in der nativen Währung der Chain (z. B. ETH) Vorrang zu geben. Wählen Sie 'Fiat', um dem Anzeigen von Werten in Ihrer gewählten Fiat-Währung Vorrang zu geben." - }, "primaryType": { "message": "Primärer Typ" }, @@ -4747,13 +4734,6 @@ "smartTransactionSuccess": { "message": "Ihre Transaktion ist abgeschlossen" }, - "smartTransactionTakingTooLong": { - "message": "Entschuldigung für die Wartezeit" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Wenn Ihre Transaktion nicht innerhalb von $1 abgeschlossen wird, wird sie storniert und Ihnen wird kein Gas berechnet.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Smart Transactions" }, @@ -5170,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "Versuchen Sie Ihren Swap erneut. Wir werden hier sein, um Sie beim nächsten Mal vor ähnlichen Risiken zu schützen." }, - "stxEstimatedCompletion": { - "message": "Geschätzter Abschluss in < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Swap fehlgeschlagen" }, @@ -6085,9 +6061,6 @@ "updateRequest": { "message": "Aktualisierungsanfrage" }, - "updatedWithDate": { - "message": "$1 aktualisiert" - }, "uploadDropFile": { "message": "Legen Sie Ihre Datei hier ab" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 340670a11bc9..c7f7137665a4 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "Λεπτομέρειες χρεώσεων" }, - "fiat": { - "message": "Εντολή", - "description": "Exchange type" - }, "fileImportFail": { "message": "Η εισαγωγή αρχείων δεν λειτουργεί; Κάντε κλικ εδώ!", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "Το MetaMask δεν συνδέεται με αυτόν τον ιστότοπο" }, - "noConversionDateAvailable": { - "message": "Δεν υπάρχει διαθέσιμη ημερομηνία μετατροπής νομίσματος" - }, "noConversionRateAvailable": { "message": "Δεν υπάρχει διαθέσιμη ισοτιμία μετατροπής" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "μη διαθέσιμη τιμή" }, - "primaryCurrencySetting": { - "message": "Κύριο νόμισμα" - }, - "primaryCurrencySettingDescription": { - "message": "Επιλέξτε εγχώριο για να δώσετε προτεραιότητα στην εμφάνιση των τιμών στο νόμισμα της αλυσίδας (π.χ. ETH). Επιλέξτε Παραστατικό για να δώσετε προτεραιότητα στην εμφάνιση τιμών στο επιλεγμένο παραστατικό νόμισμα." - }, "primaryType": { "message": "Βασικός τύπος" }, @@ -4747,13 +4734,6 @@ "smartTransactionSuccess": { "message": "Η συναλλαγή σας ολοκληρώθηκε" }, - "smartTransactionTakingTooLong": { - "message": "Συγγνώμη για την αναμονή" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Εάν η συναλλαγή σας δεν ολοκληρωθεί εντός $1, θα ακυρωθεί και δεν θα χρεωθείτε με τέλη συναλλαγών.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Έξυπνες συναλλαγές" }, @@ -5170,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "Προσπαθήστε ξανά να κάνετε ανταλλαγή. Θα είμαστε εδώ για να σας προστατεύσουμε από παρόμοιους κινδύνους και την επόμενη φορά." }, - "stxEstimatedCompletion": { - "message": "Εκτιμώμενη ολοκλήρωση σε < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Η ανταλλαγή απέτυχε" }, @@ -6085,9 +6061,6 @@ "updateRequest": { "message": "Αίτημα ενημέρωσης" }, - "updatedWithDate": { - "message": "Ενημερώθηκε $1" - }, "uploadDropFile": { "message": "Αφήστε το αρχείο σας εδώ" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 42a5a108fdf1..de17cf4ea877 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -402,6 +402,10 @@ "advancedPriorityFeeToolTip": { "message": "Priority fee (aka “miner tip”) goes directly to miners and incentivizes them to prioritize your transaction." }, + "aggregatedBalancePopover": { + "message": "This reflects the value of all tokens you own on a given network. If you prefer seeing this value in ETH or other currencies, go to $1.", + "description": "$1 represents the settings page" + }, "agreeTermsOfUse": { "message": "I agree to MetaMask's $1", "description": "$1 is the `terms` link" @@ -1347,7 +1351,7 @@ "message": "CryptoCompare" }, "currencyConversion": { - "message": "Currency conversion" + "message": "Currency" }, "currencyRateCheckToggle": { "message": "Show balance and token price checker" @@ -1565,6 +1569,29 @@ "deleteContact": { "message": "Delete contact" }, + "deleteMetaMetricsData": { + "message": "Delete MetaMetrics data" + }, + "deleteMetaMetricsDataDescription": { + "message": "This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our $1.", + "description": "$1 will have text saying Privacy Policy " + }, + "deleteMetaMetricsDataErrorDesc": { + "message": "This request can't be completed right now due to an analytics system server issue, please try again later" + }, + "deleteMetaMetricsDataErrorTitle": { + "message": "We are unable to delete this data right now" + }, + "deleteMetaMetricsDataModalDesc": { + "message": "We are about to remove all your MetaMetrics data. Are you sure?" + }, + "deleteMetaMetricsDataModalTitle": { + "message": "Delete MetaMetrics data?" + }, + "deleteMetaMetricsDataRequestedDescription": { + "message": "You initiated this action on $1. This process can take up to 30 days. View the $2", + "description": "$1 will be the date on which teh deletion is requested and $2 will have text saying Privacy Policy " + }, "deleteNetworkIntro": { "message": "If you delete this network, you will need to add it again to view your assets in this network" }, @@ -2032,10 +2059,6 @@ "feeDetails": { "message": "Fee details" }, - "fiat": { - "message": "Fiat", - "description": "Exchange type" - }, "fileImportFail": { "message": "File import not working? Click here!", "description": "Helps user import their account from a JSON file" @@ -2873,6 +2896,9 @@ "metaMaskConnectStatusParagraphTwo": { "message": "The connection status button shows if the website you’re visiting is connected to your currently selected account." }, + "metaMetricsIdNotAvailableError": { + "message": "Since you've never opted into MetaMetrics, there's no data to delete here." + }, "metadataModalSourceTooltip": { "message": "$1 is hosted on npm and $2 is this Snap’s unique identifier.", "description": "$1 is the snap name and $2 is the snap NPM id." @@ -3302,9 +3328,6 @@ "noConnectedAccountTitle": { "message": "MetaMask isn’t connected to this site" }, - "noConversionDateAvailable": { - "message": "No currency conversion date available" - }, "noConversionRateAvailable": { "message": "No conversion rate available" }, @@ -4145,12 +4168,6 @@ "priceUnavailable": { "message": "price unavailable" }, - "primaryCurrencySetting": { - "message": "Primary currency" - }, - "primaryCurrencySettingDescription": { - "message": "Select native to prioritize displaying values in the native currency of the chain (e.g. ETH). Select Fiat to prioritize displaying values in your selected fiat currency." - }, "primaryType": { "message": "Primary type" }, @@ -4885,6 +4902,9 @@ "showMore": { "message": "Show more" }, + "showNativeTokenAsMainBalance": { + "message": "Show native token as main balance" + }, "showNft": { "message": "Show NFT" }, @@ -5025,18 +5045,11 @@ "message": "Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support." }, "smartTransactionPending": { - "message": "Submitting your transaction" + "message": "Your transaction was submitted" }, "smartTransactionSuccess": { "message": "Your transaction is complete" }, - "smartTransactionTakingTooLong": { - "message": "Sorry for the wait" - }, - "smartTransactionTakingTooLongDescription": { - "message": "If your transaction is not finalized within $1, it will be canceled and you will not be charged for gas.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Smart Transactions" }, @@ -5462,10 +5475,6 @@ "stxCancelledSubDescription": { "message": "Try your swap again. We’ll be here to protect you against similar risks next time." }, - "stxEstimatedCompletion": { - "message": "Estimated completion in < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Swap failed" }, @@ -6420,9 +6429,6 @@ "updatedRpcForNetworks": { "message": "Network RPCs Updated" }, - "updatedWithDate": { - "message": "Updated $1" - }, "uploadDropFile": { "message": "Drop your file here" }, @@ -6679,6 +6685,9 @@ "yourBalance": { "message": "Your balance" }, + "yourBalanceIsAggregated": { + "message": "Your balance is aggregated" + }, "yourNFTmayBeAtRisk": { "message": "Your NFT may be at risk" }, diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index 80a17b1c5e22..d02d9b8c1af5 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -1934,10 +1934,6 @@ "feeDetails": { "message": "Fee details" }, - "fiat": { - "message": "Fiat", - "description": "Exchange type" - }, "fileImportFail": { "message": "File import not working? Click here!", "description": "Helps user import their account from a JSON file" @@ -3166,9 +3162,6 @@ "noConnectedAccountTitle": { "message": "MetaMask isn’t connected to this site" }, - "noConversionDateAvailable": { - "message": "No currency conversion date available" - }, "noConversionRateAvailable": { "message": "No conversion rate available" }, @@ -4007,12 +4000,6 @@ "priceUnavailable": { "message": "price unavailable" }, - "primaryCurrencySetting": { - "message": "Primary currency" - }, - "primaryCurrencySettingDescription": { - "message": "Select native to prioritize displaying values in the native currency of the chain (e.g. ETH). Select Fiat to prioritize displaying values in your selected fiat currency." - }, "primaryType": { "message": "Primary type" }, @@ -4834,7 +4821,7 @@ "message": "Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support." }, "smartTransactionPending": { - "message": "Submitting your transaction" + "message": "Your transaction was submitted" }, "smartTransactionSuccess": { "message": "Your transaction is complete" @@ -6225,9 +6212,6 @@ "updateRequest": { "message": "Update request" }, - "updatedWithDate": { - "message": "Updated $1" - }, "uploadDropFile": { "message": "Drop your file here" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 599168440ee5..3430b44cad96 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -1870,10 +1870,6 @@ "feeDetails": { "message": "Detalles de la tarifa" }, - "fiat": { - "message": "Fiduciaria", - "description": "Exchange type" - }, "fileImportFail": { "message": "¿No funciona la importación del archivo? Haga clic aquí.", "description": "Helps user import their account from a JSON file" @@ -3089,9 +3085,6 @@ "noConnectedAccountTitle": { "message": "MetaMask no está conectado a este sitio" }, - "noConversionDateAvailable": { - "message": "No hay fecha de conversión de moneda disponible" - }, "noConversionRateAvailable": { "message": "No hay tasa de conversión disponible" }, @@ -3912,12 +3905,6 @@ "priceUnavailable": { "message": "precio no disponible" }, - "primaryCurrencySetting": { - "message": "Moneda principal" - }, - "primaryCurrencySettingDescription": { - "message": "Seleccione Nativa para dar prioridad a mostrar los valores en la moneda nativa de la cadena (p. ej., ETH). Seleccione Fiduciaria para dar prioridad a mostrar los valores en la moneda fiduciaria seleccionada." - }, "primaryType": { "message": "Tipo principal" }, @@ -4744,13 +4731,6 @@ "smartTransactionSuccess": { "message": "Su transacción está completa" }, - "smartTransactionTakingTooLong": { - "message": "Disculpe la espera" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Si su transacción no finaliza en $1, se cancelará y no se le cobrará el gas.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Transacciones inteligentes" }, @@ -5167,10 +5147,6 @@ "stxCancelledSubDescription": { "message": "Intente su swap nuevamente. Estaremos aquí para protegerlo contra riesgos similares la próxima vez." }, - "stxEstimatedCompletion": { - "message": "Finalización estimada en < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Error al intercambiar" }, @@ -6082,9 +6058,6 @@ "updateRequest": { "message": "Solicitud de actualización" }, - "updatedWithDate": { - "message": "$1 actualizado" - }, "uploadDropFile": { "message": "Ingrese su archivo aquí" }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 1b8bc945343b..0bba1bd69551 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -782,10 +782,6 @@ "feeAssociatedRequest": { "message": "Esta solicitud tiene asociada una tarifa." }, - "fiat": { - "message": "Fiduciaria", - "description": "Exchange type" - }, "fileImportFail": { "message": "¿No funciona la importación del archivo? ¡Haga clic aquí!", "description": "Helps user import their account from a JSON file" @@ -1321,9 +1317,6 @@ "noAccountsFound": { "message": "No se encuentran cuentas para la consulta de búsqueda determinada" }, - "noConversionDateAvailable": { - "message": "No hay fecha de conversión de moneda disponible" - }, "noConversionRateAvailable": { "message": "No hay tasa de conversión disponible" }, @@ -1489,12 +1482,6 @@ "prev": { "message": "Ant." }, - "primaryCurrencySetting": { - "message": "Moneda principal" - }, - "primaryCurrencySettingDescription": { - "message": "Seleccione Nativa para dar prioridad a mostrar los valores en la moneda nativa de la cadena (p. ej., ETH). Seleccione Fiduciaria para dar prioridad a mostrar los valores en la moneda fiduciaria seleccionada." - }, "priorityFee": { "message": "Tarifa de prioridad" }, @@ -2405,9 +2392,6 @@ "message": "El envío de tokens coleccionables (ERC-721) no se admite actualmente", "description": "This is an error message we show the user if they attempt to send an NFT asset type, for which currently don't support sending" }, - "updatedWithDate": { - "message": "$1 actualizado" - }, "urlErrorMsg": { "message": "Las direcciones URL requieren el prefijo HTTP/HTTPS adecuado." }, diff --git a/app/_locales/et/messages.json b/app/_locales/et/messages.json index acebcc9091da..38125572b8ec 100644 --- a/app/_locales/et/messages.json +++ b/app/_locales/et/messages.json @@ -498,12 +498,6 @@ "prev": { "message": "Eelm" }, - "primaryCurrencySetting": { - "message": "Põhivaluuta" - }, - "primaryCurrencySettingDescription": { - "message": "Valige omavääring, et prioriseerida vääringu kuvamist ahela omavääringus (nt ETH). Valige Fiat, et prioriseerida vääringu kuvamist valitud fiat-vääringus." - }, "privacyMsg": { "message": "privaatsuspoliitika" }, @@ -755,9 +749,6 @@ "unlockMessage": { "message": "Detsentraliseeritud veeb ootab" }, - "updatedWithDate": { - "message": "Värskendatud $1" - }, "urlErrorMsg": { "message": "URI-d nõuavad sobivat HTTP/HTTPS-i prefiksit." }, diff --git a/app/_locales/fa/messages.json b/app/_locales/fa/messages.json index c9c1bafdc7bf..c1a4deb11ce4 100644 --- a/app/_locales/fa/messages.json +++ b/app/_locales/fa/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "سریع" }, - "fiat": { - "message": "حکم قانونی", - "description": "Exchange type" - }, "fileImportFail": { "message": "وارد کردن فایل کار نمیکند؟ اینجا کلیک نمایید!", "description": "Helps user import their account from a JSON file" @@ -508,12 +504,6 @@ "prev": { "message": "قبلی" }, - "primaryCurrencySetting": { - "message": "واحد پول اصلی" - }, - "primaryCurrencySettingDescription": { - "message": "برای اولویت دهی نمایش قیمت ها در واحد پولی اصلی زنجیره (مثلًا ETH)، اصلی را انتخاب کنید. برای اولویت دهی نمایش قیمت ها در فیات واحد پولی شما، فیات را انتخاب کنید." - }, "privacyMsg": { "message": "خط‌مشی رازداری" }, @@ -765,9 +755,6 @@ "unlockMessage": { "message": "وب غیر متمرکز شده انتظار میکشد" }, - "updatedWithDate": { - "message": "بروزرسانی شد 1$1" - }, "urlErrorMsg": { "message": "URl ها نیازمند پیشوند مناسب HTTP/HTTPS اند." }, diff --git a/app/_locales/fi/messages.json b/app/_locales/fi/messages.json index 1c9cdb7c7a43..89e274dd4466 100644 --- a/app/_locales/fi/messages.json +++ b/app/_locales/fi/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "Nopea" }, - "fiat": { - "message": "Kiinteä", - "description": "Exchange type" - }, "fileImportFail": { "message": "Eikö tiedoston tuominen onnistu? Klikkaa tästä!", "description": "Helps user import their account from a JSON file" @@ -505,12 +501,6 @@ "prev": { "message": "Aiemp." }, - "primaryCurrencySetting": { - "message": "Ensisijainen valuutta" - }, - "primaryCurrencySettingDescription": { - "message": "Valitse natiivivaihtoehto näyttääksesi arvot ensisijaisesti ketjun natiivivaluutalla (esim. ETH). Valitse oletusmääräys asettaaksesi valitsemasi oletusvaluutan ensisijaiseksi." - }, "privacyMsg": { "message": "Tietosuojakäytäntö" }, @@ -762,9 +752,6 @@ "unlockMessage": { "message": "Hajautettu verkko odottaa" }, - "updatedWithDate": { - "message": "$1 päivitetty" - }, "urlErrorMsg": { "message": "URI:t vaativat asianmukaisen HTTP/HTTPS-etuliitteen." }, diff --git a/app/_locales/fil/messages.json b/app/_locales/fil/messages.json index e08c88bd7ffa..498c1878fd10 100644 --- a/app/_locales/fil/messages.json +++ b/app/_locales/fil/messages.json @@ -436,12 +436,6 @@ "prev": { "message": "Nakaraan" }, - "primaryCurrencySetting": { - "message": "Pangunahing Currency" - }, - "primaryCurrencySettingDescription": { - "message": "Piliin ang native para bigyang priyoridad ang pagpapakita ng mga halaga sa native currency ng chain (hal. ETH). Piliin ang Fiat para bigyang priyoridad ang pagpapakita ng mga halaga sa napili mong fiat currency." - }, "privacyMsg": { "message": "Patakaran sa Privacy" }, @@ -677,9 +671,6 @@ "unlockMessage": { "message": "Naghihintay ang decentralized web" }, - "updatedWithDate": { - "message": "Na-update ang $1" - }, "urlErrorMsg": { "message": "Kinakailangan ng mga URI ang naaangkop na HTTP/HTTPS prefix." }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 84124b8f1fff..b2429962bad3 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "Détails des frais" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "L’importation de fichier ne fonctionne pas ? Cliquez ici !", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "MetaMask n’est pas connecté à ce site" }, - "noConversionDateAvailable": { - "message": "Aucune date de conversion des devises n’est disponible" - }, "noConversionRateAvailable": { "message": "Aucun taux de conversion disponible" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "prix non disponible" }, - "primaryCurrencySetting": { - "message": "Devise principale" - }, - "primaryCurrencySettingDescription": { - "message": "Sélectionnez « natif » pour donner la priorité à l’affichage des valeurs dans la devise native de la chaîne (par ex. ETH). Sélectionnez « fiduciaire » pour donner la priorité à l’affichage des valeurs dans la devise de votre choix." - }, "primaryType": { "message": "Type principal" }, @@ -4747,13 +4734,6 @@ "smartTransactionSuccess": { "message": "Votre transaction est terminée" }, - "smartTransactionTakingTooLong": { - "message": "Désolé de vous avoir fait attendre" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Si votre transaction n’est pas finalisée dans un délai de $1, elle sera annulée et les frais de gaz ne vous seront pas facturés.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Transactions intelligentes" }, @@ -5170,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "Réessayez le swap. Nous serons là pour vous protéger contre des risques similaires la prochaine fois." }, - "stxEstimatedCompletion": { - "message": "Délai estimé < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Échec du swap" }, @@ -6085,9 +6061,6 @@ "updateRequest": { "message": "Demande de mise à jour" }, - "updatedWithDate": { - "message": "Mis à jour $1" - }, "uploadDropFile": { "message": "Déposez votre fichier ici" }, diff --git a/app/_locales/he/messages.json b/app/_locales/he/messages.json index 9d118e31c098..413bf21d586b 100644 --- a/app/_locales/he/messages.json +++ b/app/_locales/he/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "מהיר" }, - "fiat": { - "message": "פיאט", - "description": "Exchange type" - }, "fileImportFail": { "message": "ייבוא הקובץ לא עובד? לחצ/י כאן!", "description": "Helps user import their account from a JSON file" @@ -505,12 +501,6 @@ "prev": { "message": "הקודם" }, - "primaryCurrencySetting": { - "message": "מטבע ראשי" - }, - "primaryCurrencySettingDescription": { - "message": "בחר/י 'מקומי' כדי לתעדף הצגת ערכים במטבע המקומי של הצ'יין (למשל ETH). בחר/י פיאט כדי לתעדף הצגת ערכים במטבע הפיאט שבחרת." - }, "privacyMsg": { "message": "מדיניות הפרטיות" }, @@ -762,9 +752,6 @@ "unlockMessage": { "message": "הרשת המבוזרת מחכה" }, - "updatedWithDate": { - "message": "עודכן $1" - }, "urlErrorMsg": { "message": "כתובות URI דורשות את קידומת HTTP/HTTPS המתאימה." }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index fd957c6925df..91bcfebef973 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "फ़ीस का ब्यौरा" }, - "fiat": { - "message": "फिएट", - "description": "Exchange type" - }, "fileImportFail": { "message": "फाइल इम्पोर्ट काम नहीं कर रहा है? यहां क्लिक करें!", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "MetaMask इस साइट से कनेक्टेड नहीं है।" }, - "noConversionDateAvailable": { - "message": "कोई करेंसी कन्वर्शन तारीख उपलब्ध नहीं है" - }, "noConversionRateAvailable": { "message": "कोई भी कन्वर्शन दर उपलब्ध नहीं है" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "प्राइस अनुपलब्ध है" }, - "primaryCurrencySetting": { - "message": "प्राथमिक मुद्रा" - }, - "primaryCurrencySettingDescription": { - "message": "चेन की ओरिजिनल करेंसी (जैसे ETH) में प्रदर्शित वैल्यूज़ को प्राथमिकता देने के लिए ओरिजिनल को चुनें। अपनी चुना गया फिएट करेंसी में प्रदर्शित वैल्यूज़ को प्राथमिकता देने के लिए फिएट को चुनें।" - }, "primaryType": { "message": "प्राइमरी टाइप" }, @@ -4747,13 +4734,6 @@ "smartTransactionSuccess": { "message": "आपका ट्रांसेक्शन पूरा हो गया है" }, - "smartTransactionTakingTooLong": { - "message": "माफ़ी चाहते हैं कि आपको इंतज़ार करना पड़ा" - }, - "smartTransactionTakingTooLongDescription": { - "message": "यदि आपका ट्रांसेक्शन $1 के भीतर फाइनलाइज़ नहीं होता है, तो इसे कैंसिल कर दिया जाएगा और आपसे गैस फ़ीस नहीं ली जाएगी।", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "स्मार्ट ट्रांसेक्शन" }, @@ -5170,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "अपना स्वैप फिर से कोशिश करें। अगली बार भी इस तरह के जोखिमों से आपको बचाने के लिए हम यहां होंगे।" }, - "stxEstimatedCompletion": { - "message": "<$1 में पूरा होने का अनुमान", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "स्वैप नहीं हो पाया" }, @@ -6085,9 +6061,6 @@ "updateRequest": { "message": "अपडेट का अनुरोध" }, - "updatedWithDate": { - "message": "अपडेट किया गया $1" - }, "uploadDropFile": { "message": "अपनी फ़ाइल यहां छोड़ें" }, diff --git a/app/_locales/hn/messages.json b/app/_locales/hn/messages.json index 8e9091f911db..a4e2e37bde22 100644 --- a/app/_locales/hn/messages.json +++ b/app/_locales/hn/messages.json @@ -87,10 +87,6 @@ "failed": { "message": "विफल" }, - "fiat": { - "message": "FIAT एक्सचेंज टाइप", - "description": "Exchange type" - }, "fileImportFail": { "message": "फ़ाइल आयात काम नहीं कर रहा है? यहां क्लिक करें!", "description": "Helps user import their account from a JSON file" diff --git a/app/_locales/hr/messages.json b/app/_locales/hr/messages.json index 4463408d9435..7f9334f49f5c 100644 --- a/app/_locales/hr/messages.json +++ b/app/_locales/hr/messages.json @@ -501,12 +501,6 @@ "prev": { "message": "Prethodno" }, - "primaryCurrencySetting": { - "message": "Glavna valuta" - }, - "primaryCurrencySettingDescription": { - "message": "Odaberite da se prvo prikazuju valute u osnovnoj valuti bloka (npr. ETH). Odaberite mogućnost Fiat za prikazivanje valuta u odabranoj valuti Fiat." - }, "privacyMsg": { "message": "Pravilnik o zaštiti privatnosti" }, @@ -755,9 +749,6 @@ "unlockMessage": { "message": "Decentralizirani internet čeka" }, - "updatedWithDate": { - "message": "Ažurirano $1" - }, "urlErrorMsg": { "message": "URI-jevima se zahtijeva prikladan prefiks HTTP/HTTPS." }, diff --git a/app/_locales/ht/messages.json b/app/_locales/ht/messages.json index 700f6debed18..7309b04dbd05 100644 --- a/app/_locales/ht/messages.json +++ b/app/_locales/ht/messages.json @@ -153,10 +153,6 @@ "failed": { "message": "Tonbe" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "Enpòte dosye ki pa travay? Klike la a!", "description": "Helps user import their account from a JSON file" @@ -357,12 +353,6 @@ "prev": { "message": "Avan" }, - "primaryCurrencySetting": { - "message": "Lajan ou itilize pi plis la" - }, - "primaryCurrencySettingDescription": { - "message": "Chwazi ETH pou bay priyorite montre valè nan ETH. Chwazi Fiat priyorite montre valè nan lajan ou chwazi a." - }, "privacyMsg": { "message": "Règleman sou enfòmasyon prive" }, @@ -548,9 +538,6 @@ "unlockMessage": { "message": "Entènèt desantralize a ap tann" }, - "updatedWithDate": { - "message": "Mete ajou $1" - }, "urlErrorMsg": { "message": "URIs mande pou apwopriye prefiks HTTP / HTTPS a." }, diff --git a/app/_locales/hu/messages.json b/app/_locales/hu/messages.json index 4786cfb9703d..7b2b429ae5ed 100644 --- a/app/_locales/hu/messages.json +++ b/app/_locales/hu/messages.json @@ -501,12 +501,6 @@ "prev": { "message": "Előző" }, - "primaryCurrencySetting": { - "message": "Elsődleges pénznem" - }, - "primaryCurrencySettingDescription": { - "message": "Válaszd a helyit, hogy az értékek elsősorban a helyi pénznemben jelenjenek meg (pl. ETH). Válaszd a Fiatot, hogy az értékek elsősorban a választott fiat pénznemben jelenjenek meg." - }, "privacyMsg": { "message": "Adatvédelmi szabályzat" }, @@ -755,9 +749,6 @@ "unlockMessage": { "message": "A decentralizált hálózat csak önre vár" }, - "updatedWithDate": { - "message": "$1 frissítve" - }, "urlErrorMsg": { "message": "Az URI-hez szükség van a megfelelő HTTP/HTTPS előtagra." }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index a9a34d9b9a4e..82ab45bdfa99 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "Detail biaya" }, - "fiat": { - "message": "Fiat", - "description": "Exchange type" - }, "fileImportFail": { "message": "Impor file tidak bekerja? Klik di sini!", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "MetaMask tidak terhubung ke situs ini" }, - "noConversionDateAvailable": { - "message": "Tanggal konversi mata uang tidak tersedia" - }, "noConversionRateAvailable": { "message": "Nilai konversi tidak tersedia" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "harga tidak tersedia" }, - "primaryCurrencySetting": { - "message": "Mata uang primer" - }, - "primaryCurrencySettingDescription": { - "message": "Pilih asal untuk memprioritaskan nilai yang ditampilkan dalam mata uang asal chain (contoh, ETH). Pilih Fiat untuk memprioritaskan nilai yang ditampilkan dalam mata uang fiat yang Anda pilih." - }, "primaryType": { "message": "Tipe primer" }, @@ -4747,13 +4734,6 @@ "smartTransactionSuccess": { "message": "Transaksi Anda selesai" }, - "smartTransactionTakingTooLong": { - "message": "Maaf telah menunggu" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Jika transaksi tidak diselesaikan dalam $1, transaksi akan dibatalkan dan Anda tidak akan dikenakan biaya gas.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Transaksi Pintar" }, @@ -5170,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "Cobalah untuk menukar lagi. Kami akan selalu hadir untuk melindungi Anda dari risiko serupa di lain waktu." }, - "stxEstimatedCompletion": { - "message": "Estimasi penyelesaian dalam < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Pertukaran gagal" }, @@ -6085,9 +6061,6 @@ "updateRequest": { "message": "Permintaan pembaruan" }, - "updatedWithDate": { - "message": "Diperbarui $1" - }, "uploadDropFile": { "message": "Letakkan fail di sini" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 7c413941da92..71b07590d6d1 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -808,10 +808,6 @@ "feeAssociatedRequest": { "message": "Una tassa è associata a questa richiesta." }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "Importazione file non funziona? Clicca qui!", "description": "Helps user import their account from a JSON file" @@ -1165,12 +1161,6 @@ "prev": { "message": "Precedente" }, - "primaryCurrencySetting": { - "message": "Moneta Primaria" - }, - "primaryCurrencySettingDescription": { - "message": "Seleziona ETH per privilegiare la visualizzazione dei valori nella moneta nativa della blockhain. Seleziona Fiat per privilegiare la visualizzazione dei valori nella moneta selezionata." - }, "privacyMsg": { "message": "Politica sulla Privacy" }, @@ -1698,9 +1688,6 @@ "unlockMessage": { "message": "Il web decentralizzato ti attende" }, - "updatedWithDate": { - "message": "Aggiornata $1" - }, "urlErrorMsg": { "message": "Gli URI richiedono un prefisso HTTP/HTTPS." }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index f491ac9aa280..280889881f57 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "手数料の詳細" }, - "fiat": { - "message": "法定通貨", - "description": "Exchange type" - }, "fileImportFail": { "message": "ファイルのインポートが機能していない場合、ここをクリックしてください!", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "MetaMaskはこのサイトに接続されていません" }, - "noConversionDateAvailable": { - "message": "通貨換算日がありません" - }, "noConversionRateAvailable": { "message": "利用可能な換算レートがありません" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "価格が利用できません" }, - "primaryCurrencySetting": { - "message": "プライマリ通貨" - }, - "primaryCurrencySettingDescription": { - "message": "チェーンのネイティブ通貨 (ETHなど) による値の表示を優先するには、「ネイティブ」を選択します。選択した法定通貨による値の表示を優先するには、「法定通貨」を選択します。" - }, "primaryType": { "message": "基本型" }, @@ -4747,13 +4734,6 @@ "smartTransactionSuccess": { "message": "トランザクションが完了しました" }, - "smartTransactionTakingTooLong": { - "message": "お待たせして申し訳ございません" - }, - "smartTransactionTakingTooLongDescription": { - "message": "$1以内にトランザクションが完了しない場合はキャンセルされ、ガス代は請求されません。", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "スマートトランザクション" }, @@ -5170,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "もう一度スワップをお試しください。次回は同様のリスクを避けられるようサポートします。" }, - "stxEstimatedCompletion": { - "message": "$1未満で完了予定", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "スワップに失敗しました" }, @@ -6085,9 +6061,6 @@ "updateRequest": { "message": "更新リクエスト" }, - "updatedWithDate": { - "message": "$1が更新されました" - }, "uploadDropFile": { "message": "ここにファイルをドロップします" }, diff --git a/app/_locales/kn/messages.json b/app/_locales/kn/messages.json index 6471b738b5b9..120651f0b759 100644 --- a/app/_locales/kn/messages.json +++ b/app/_locales/kn/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "ವೇಗ" }, - "fiat": { - "message": "ಫಿಯೆಟ್", - "description": "Exchange type" - }, "fileImportFail": { "message": "ಫೈಲ್ ಆಮದು ಮಾಡುವಿಕೆ ಕಾರ್ಯನಿರ್ವಹಿಸುತ್ತಿಲ್ಲವೇ? ಇಲ್ಲಿ ಕ್ಲಿಕ್ ಮಾಡಿ!", "description": "Helps user import their account from a JSON file" @@ -508,12 +504,6 @@ "prev": { "message": "ಹಿಂದಿನ" }, - "primaryCurrencySetting": { - "message": "ಪ್ರಾಥಮಿಕ ಕರೆನ್ಸಿ" - }, - "primaryCurrencySettingDescription": { - "message": "ಸರಪಳಿಯ ಸ್ಥಳೀಯ ಕರೆನ್ಸಿಯಲ್ಲಿ ಮೌಲ್ಯಗಳನ್ನು ಪ್ರದರ್ಶಿಸಲು ಆದ್ಯತೆ ನೀಡಲು ಸ್ಥಳೀಯವನ್ನು ಆಯ್ಕೆಮಾಡಿ (ಉದಾ. ETH). ನಿಮ್ಮ ಆಯ್ಕೆಮಾಡಿದ ಫಿಯೆಟ್ ಕರೆನ್ಸಿಯಲ್ಲಿ ಮೌಲ್ಯಗಳನ್ನು ಪ್ರದರ್ಶಿಸಲು ಆದ್ಯತೆ ನೀಡಲು ಫಿಯೆಟ್ ಆಯ್ಕೆಮಾಡಿ." - }, "privacyMsg": { "message": "ಗೌಪ್ಯತೆ ನೀತಿ" }, @@ -765,9 +755,6 @@ "unlockMessage": { "message": "ವಿಕೇಂದ್ರೀಕೃತ ವೆಬ್ ನಿರೀಕ್ಷಿಸುತ್ತಿದೆ" }, - "updatedWithDate": { - "message": "$1 ನವೀಕರಿಸಲಾಗಿದೆ" - }, "urlErrorMsg": { "message": "URI ಗಳಿಗೆ ಸೂಕ್ತವಾದ HTTP/HTTPS ಪೂರ್ವಪ್ರತ್ಯಯದ ಅಗತ್ಯವಿದೆ." }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 615db337e09e..c1591b2fc28e 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "수수료 세부 정보" }, - "fiat": { - "message": "명목", - "description": "Exchange type" - }, "fileImportFail": { "message": "파일 가져오기가 작동하지 않나요? 여기를 클릭하세요.", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "MetaMask가 이 사이트와 연결되어 있지 않습니다" }, - "noConversionDateAvailable": { - "message": "사용 가능한 통화 변환 날짜 없음" - }, "noConversionRateAvailable": { "message": "사용 가능한 환율 없음" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "가격 사용 불가" }, - "primaryCurrencySetting": { - "message": "기본 통화" - }, - "primaryCurrencySettingDescription": { - "message": "체인의 고유 통화(예: ETH)로 값을 우선 표시하려면 고유를 선택합니다. 선택한 명목 통화로 값을 우선 표시하려면 명목을 선택합니다." - }, "primaryType": { "message": "기본 유형" }, @@ -4747,13 +4734,6 @@ "smartTransactionSuccess": { "message": "트랜잭션 완료" }, - "smartTransactionTakingTooLong": { - "message": "기다리게 해서 죄송합니다" - }, - "smartTransactionTakingTooLongDescription": { - "message": "$1 이내에 트랜잭션이 완료되지 않으면 트랜잭션이 취소되고 가스비가 부과되지 않습니다.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "스마트 트랜잭션" }, @@ -5170,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "스왑을 다시 진행하세요. 다음에도 유사한 위험이 발생한다면 보호해 드리겠습니다." }, - "stxEstimatedCompletion": { - "message": "예상 잔여 시간: <$1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "스왑 실패" }, @@ -6085,9 +6061,6 @@ "updateRequest": { "message": "업데이트 요청" }, - "updatedWithDate": { - "message": "$1에 업데이트됨" - }, "uploadDropFile": { "message": "여기에 파일을 드롭" }, diff --git a/app/_locales/lt/messages.json b/app/_locales/lt/messages.json index 0668166eb8fe..fe825ae6b798 100644 --- a/app/_locales/lt/messages.json +++ b/app/_locales/lt/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "Greitas" }, - "fiat": { - "message": "Standartinė valiuta", - "description": "Exchange type" - }, "fileImportFail": { "message": "Failo importavimas neveikia? Spustelėkite čia!", "description": "Helps user import their account from a JSON file" @@ -508,12 +504,6 @@ "prev": { "message": "Peržiūra" }, - "primaryCurrencySetting": { - "message": "Pagrindinė valiuta" - }, - "primaryCurrencySettingDescription": { - "message": "Rinkitės vietinę, kad vertės pirmiausia būtų rodomos vietine grandinės valiuta (pvz., ETH). Rinkitės standartinę, kad vertės pirmiausia būtų rodomos jūsų pasirinkta standartine valiuta." - }, "privacyMsg": { "message": "Privatumo politika" }, @@ -765,9 +755,6 @@ "unlockMessage": { "message": "Laukiančios decentralizuotos svetainės" }, - "updatedWithDate": { - "message": "Atnaujinta $1" - }, "urlErrorMsg": { "message": "URI reikia atitinkamo HTTP/HTTPS priešdėlio." }, diff --git a/app/_locales/lv/messages.json b/app/_locales/lv/messages.json index 3939c9145c12..697af7849327 100644 --- a/app/_locales/lv/messages.json +++ b/app/_locales/lv/messages.json @@ -504,12 +504,6 @@ "prev": { "message": "Iepr." }, - "primaryCurrencySetting": { - "message": "Primārā valūta" - }, - "primaryCurrencySettingDescription": { - "message": "Atlasīt vietējo, lai piešķirtu attēlotajām vērtībām prioritātes ķēdes vietējā vērtībā (piemēram, ETH). Atlasiet Fiat, lai piešķirtu augstāku prioritāti vērtībām jūsu atlasītajā fiat valūtā." - }, "privacyMsg": { "message": "Privātuma politika" }, @@ -761,9 +755,6 @@ "unlockMessage": { "message": "Decentralizētais tīkls jau gaida" }, - "updatedWithDate": { - "message": "Atjaunināts $1" - }, "urlErrorMsg": { "message": "URI jāsākas ar atbilstošo HTTP/HTTPS priedēkli." }, diff --git a/app/_locales/ms/messages.json b/app/_locales/ms/messages.json index cfad6a22d73d..dc42e639ff2a 100644 --- a/app/_locales/ms/messages.json +++ b/app/_locales/ms/messages.json @@ -488,12 +488,6 @@ "prev": { "message": "Sebelumnya" }, - "primaryCurrencySetting": { - "message": "Mata Wang Utama" - }, - "primaryCurrencySettingDescription": { - "message": "Pilih natif untuk mengutamakan nilai paparan dalam mata wang natif rantaian (cth. ETH). Pilih Fiat untuk mengutamakan nilai paparan dalam mata wang fiat yang anda pilih." - }, "privacyMsg": { "message": "Dasar Privasi" }, @@ -742,9 +736,6 @@ "unlockMessage": { "message": "Web ternyahpusat menanti" }, - "updatedWithDate": { - "message": "Dikemaskini $1" - }, "urlErrorMsg": { "message": "URI memerlukan awalan HTTP/HTTPS yang sesuai." }, diff --git a/app/_locales/nl/messages.json b/app/_locales/nl/messages.json index 946976a7a93e..cbebb9a14563 100644 --- a/app/_locales/nl/messages.json +++ b/app/_locales/nl/messages.json @@ -84,10 +84,6 @@ "failed": { "message": "mislukt" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "Bestandsimport werkt niet? Klik hier!", "description": "Helps user import their account from a JSON file" diff --git a/app/_locales/no/messages.json b/app/_locales/no/messages.json index 0d1fa5173a9f..45a101fc83a5 100644 --- a/app/_locales/no/messages.json +++ b/app/_locales/no/messages.json @@ -492,12 +492,6 @@ "prev": { "message": "Tidligere" }, - "primaryCurrencySetting": { - "message": "Hovedvaluta " - }, - "primaryCurrencySettingDescription": { - "message": "Velg nasjonal for å prioritere å vise verdier i nasjonal valuta i kjeden (f.eks. ETH). Velg Fiat for å prioritere visning av verdier i den valgte fiat-valutaen." - }, "privacyMsg": { "message": "Personvernerklæring" }, @@ -740,9 +734,6 @@ "unlockMessage": { "message": "Det desentraliserte internett venter deg" }, - "updatedWithDate": { - "message": "Oppdatert $1" - }, "urlErrorMsg": { "message": "URI-er krever det aktuelle HTTP/HTTPS-prefikset." }, diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index df15e05a0bb6..454facde8524 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -513,10 +513,6 @@ "feeAssociatedRequest": { "message": "May nauugnay na bayarin para sa request na ito." }, - "fiat": { - "message": "Fiat", - "description": "Exchange type" - }, "fileImportFail": { "message": "Hindi gumagana ang pag-import ng file? Mag-click dito!", "description": "Helps user import their account from a JSON file" @@ -955,12 +951,6 @@ "prev": { "message": "Nakaraan" }, - "primaryCurrencySetting": { - "message": "Pangunahing Currency" - }, - "primaryCurrencySettingDescription": { - "message": "Piliin ang native para maisapriyoridad ang pagpapakita ng mga value sa native na currency ng chain (hal. ETH). Piliin ang Fiat para maisapriyoridad ang pagpapakita ng mga value sa napili mong fiat currency." - }, "privacyMsg": { "message": "Patakaran sa Pagkapribado" }, @@ -1655,9 +1645,6 @@ "message": "Hindi kinikilala ang custom na network na ito. Inirerekomenda naming $1 ka bago magpatuloy", "description": "$1 is a clickable link with text defined by the 'unrecognizedChanLinkText' key. The link will open to instructions for users to validate custom network details." }, - "updatedWithDate": { - "message": "Na-update noong $1" - }, "urlErrorMsg": { "message": "Kinakailangan ng mga URL ang naaangkop na HTTP/HTTPS prefix." }, diff --git a/app/_locales/pl/messages.json b/app/_locales/pl/messages.json index cb82388a8634..d22673fa9f1f 100644 --- a/app/_locales/pl/messages.json +++ b/app/_locales/pl/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "Szybko" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "Importowanie pliku nie działa? Kliknij tutaj!", "description": "Helps user import their account from a JSON file" @@ -502,12 +498,6 @@ "prev": { "message": "Poprzednie" }, - "primaryCurrencySetting": { - "message": "Waluta podstawowa" - }, - "primaryCurrencySettingDescription": { - "message": "Wybierz walutę natywną, aby preferować wyświetlanie wartości w walucie natywnej łańcucha (np. ETH). Wybierz walutę fiat, aby preferować wyświetlanie wartości w wybranej przez siebie walucie fiat." - }, "privacyMsg": { "message": "Polityka prywatności" }, @@ -753,9 +743,6 @@ "unlockMessage": { "message": "Zdecentralizowana sieć oczekuje" }, - "updatedWithDate": { - "message": "Zaktualizowano $1" - }, "urlErrorMsg": { "message": "URI wymaga prawidłowego prefiksu HTTP/HTTPS." }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index d5039ee7e604..95637cb057f9 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "Detalhes da taxa" }, - "fiat": { - "message": "Fiduciária", - "description": "Exchange type" - }, "fileImportFail": { "message": "A importação de arquivo não está funcionando? Clique aqui!", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "A MetaMask não está conectada a este site" }, - "noConversionDateAvailable": { - "message": "Não há uma data de conversão de moeda disponível" - }, "noConversionRateAvailable": { "message": "Não há uma taxa de conversão disponível" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "preço não disponível" }, - "primaryCurrencySetting": { - "message": "Moeda principal" - }, - "primaryCurrencySettingDescription": { - "message": "Selecione Nativa para priorizar a exibição de valores na moeda nativa da cadeia (por ex., ETH). Selecione Fiduciária para priorizar a exibição de valores na moeda fiduciária selecionada." - }, "primaryType": { "message": "Tipo primário" }, @@ -4747,13 +4734,6 @@ "smartTransactionSuccess": { "message": "Sua transação foi concluída" }, - "smartTransactionTakingTooLong": { - "message": "Desculpe pela espera" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Se a sua transação não for finalizada em $1, ela será cancelada e você não pagará gás.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Transações inteligentes" }, @@ -5170,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "Tente trocar novamente. Estaremos aqui para proteger você contra riscos semelhantes no futuro." }, - "stxEstimatedCompletion": { - "message": "Conclusão estimada em até $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Falha na troca" }, @@ -6085,9 +6061,6 @@ "updateRequest": { "message": "Solicitação de atualização" }, - "updatedWithDate": { - "message": "Atualizado em $1" - }, "uploadDropFile": { "message": "Solte seu arquivo aqui" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 4c4b17d74f6e..0f6efb88d348 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -782,10 +782,6 @@ "feeAssociatedRequest": { "message": "Há uma taxa associada a essa solicitação." }, - "fiat": { - "message": "Fiduciária", - "description": "Exchange type" - }, "fileImportFail": { "message": "A importação de arquivo não está funcionando? Clique aqui!", "description": "Helps user import their account from a JSON file" @@ -1321,9 +1317,6 @@ "noAccountsFound": { "message": "Nenhuma conta encontrada para a busca efetuada" }, - "noConversionDateAvailable": { - "message": "Não há uma data de conversão de moeda disponível" - }, "noConversionRateAvailable": { "message": "Não há uma taxa de conversão disponível" }, @@ -1493,12 +1486,6 @@ "prev": { "message": "Anterior" }, - "primaryCurrencySetting": { - "message": "Moeda principal" - }, - "primaryCurrencySettingDescription": { - "message": "Selecione Nativa para priorizar a exibição de valores na moeda nativa da cadeia (por ex., ETH). Selecione Fiduciária para priorizar a exibição de valores na moeda fiduciária selecionada." - }, "priorityFee": { "message": "Taxa de prioridade" }, @@ -2409,9 +2396,6 @@ "message": "O envio de tokens colecionáveis (ERC-721) não é suportado no momento", "description": "This is an error message we show the user if they attempt to send an NFT asset type, for which currently don't support sending" }, - "updatedWithDate": { - "message": "Atualizado em $1" - }, "urlErrorMsg": { "message": "Os URLs precisam do prefixo HTTP/HTTPS adequado." }, diff --git a/app/_locales/ro/messages.json b/app/_locales/ro/messages.json index aad720151da6..912accba29be 100644 --- a/app/_locales/ro/messages.json +++ b/app/_locales/ro/messages.json @@ -495,12 +495,6 @@ "prev": { "message": "Ant" }, - "primaryCurrencySetting": { - "message": "Moneda principală" - }, - "primaryCurrencySettingDescription": { - "message": "Selectați nativ pentru a prioritiza valorile afișate în moneda nativă a lanțului (ex. ETH). Selectați Fiat pentru a prioritiza valorile afișate în moneda selectată fiat." - }, "privacyMsg": { "message": "Politica de Confidențialitate" }, @@ -746,9 +740,6 @@ "unlockMessage": { "message": "Web-ul descentralizat așteaptă" }, - "updatedWithDate": { - "message": "Actualizat $1" - }, "urlErrorMsg": { "message": "URL necesită prefixul potrivit HTTP/HTTPS." }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 794cd777afd1..6ce19f83b4ed 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "Сведения о комиссии" }, - "fiat": { - "message": "Фиатная", - "description": "Exchange type" - }, "fileImportFail": { "message": "Импорт файлов не работает? Нажмите здесь!", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "MetaMask не подключен к этому сайту" }, - "noConversionDateAvailable": { - "message": "Дата обмена валюты недоступна" - }, "noConversionRateAvailable": { "message": "Нет доступного обменного курса" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "цена недоступна" }, - "primaryCurrencySetting": { - "message": "Основная валюта" - }, - "primaryCurrencySettingDescription": { - "message": "Выберите «собственная», чтобы установить приоритет отображения значений в собственной валюте блокчейна (например, ETH). Выберите «Фиатная», чтобы установить приоритет отображения значений в выбранной фиатной валюте." - }, "primaryType": { "message": "Основной тип" }, @@ -4747,13 +4734,6 @@ "smartTransactionSuccess": { "message": "Ваша транзакция завершена" }, - "smartTransactionTakingTooLong": { - "message": "Извините за ожидание" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Если ваша транзакция не будет завершена в течение $1, она будет отменена и с вас не будет взиматься плата за газ.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Умные транзакции" }, @@ -5170,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "Попробуйте выполнить своп еще раз. Мы готовы защитить вас от подобных рисков в следующий раз." }, - "stxEstimatedCompletion": { - "message": "Предполагаемое завершение через < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Своп не удался" }, @@ -6085,9 +6061,6 @@ "updateRequest": { "message": "Запрос обновления" }, - "updatedWithDate": { - "message": "Обновлено $1" - }, "uploadDropFile": { "message": "Переместите свой файл сюда" }, diff --git a/app/_locales/sk/messages.json b/app/_locales/sk/messages.json index 564e7cb12d94..829435f28ff7 100644 --- a/app/_locales/sk/messages.json +++ b/app/_locales/sk/messages.json @@ -238,10 +238,6 @@ "fast": { "message": "Rýchle" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "Import souboru nefunguje? Klikněte sem!", "description": "Helps user import their account from a JSON file" @@ -480,12 +476,6 @@ "prev": { "message": "Predchádzajúce" }, - "primaryCurrencySetting": { - "message": "Primárna mena" - }, - "primaryCurrencySettingDescription": { - "message": "Vyberte natívne, ak chcete priorizovať zobrazovanie hodnôt v natívnej mene reťazca (napr. ETH). Ak chcete priorizovať zobrazovanie hodnôt vo svojej vybranej mene fiat, zvoľte možnosť Fiat." - }, "privacyMsg": { "message": "Zásady ochrany osobních údajů" }, @@ -731,9 +721,6 @@ "unlockMessage": { "message": "Decentralizovaný web čaká" }, - "updatedWithDate": { - "message": "Aktualizované $1" - }, "urlErrorMsg": { "message": "URI vyžadují korektní HTTP/HTTPS prefix." }, diff --git a/app/_locales/sl/messages.json b/app/_locales/sl/messages.json index 8d43d184c427..cb82e0358212 100644 --- a/app/_locales/sl/messages.json +++ b/app/_locales/sl/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "Hiter" }, - "fiat": { - "message": "Klasične", - "description": "Exchange type" - }, "fileImportFail": { "message": "Uvoz z datoteko ne deluje? Kliknite tukaj!", "description": "Helps user import their account from a JSON file" @@ -496,12 +492,6 @@ "prev": { "message": "Prej" }, - "primaryCurrencySetting": { - "message": "Glavna valuta" - }, - "primaryCurrencySettingDescription": { - "message": "Izberite Native za prikaz vrednosti v privzeti valuti verige (npr. ETH). Izberite Klasične za prikaz vrednosti v izbrani klasični valuti." - }, "privacyMsg": { "message": "Zasebnost" }, @@ -753,9 +743,6 @@ "unlockMessage": { "message": "Decentralizirana spletna denarnica" }, - "updatedWithDate": { - "message": "Posodobljeno $1" - }, "urlErrorMsg": { "message": "URI zahtevajo ustrezno HTTP/HTTPS predpono." }, diff --git a/app/_locales/sr/messages.json b/app/_locales/sr/messages.json index d3f5e27c6235..e15ae23086b3 100644 --- a/app/_locales/sr/messages.json +++ b/app/_locales/sr/messages.json @@ -241,10 +241,6 @@ "fast": { "message": "Брзо" }, - "fiat": { - "message": "Dekret", - "description": "Exchange type" - }, "fileImportFail": { "message": "Uvoz datoteke ne radi? Kliknite ovde!", "description": "Helps user import their account from a JSON file" @@ -499,12 +495,6 @@ "prev": { "message": "Prethodno" }, - "primaryCurrencySetting": { - "message": "Primarna valuta" - }, - "primaryCurrencySettingDescription": { - "message": "Izaberite primarnu da biste postavili prioritete u prikazivanju vrednosti u primarnoj valuti lanca (npr. ETH). Izaberite Fiat da biste postavili prioritete u prikazivanju vrednosti u vašoj izabranoj fiat valuti." - }, "privacyMsg": { "message": "Smernice za privatnost" }, @@ -753,9 +743,6 @@ "unlockMessage": { "message": "Decentralizovani veb čeka" }, - "updatedWithDate": { - "message": "Ažuriran $1" - }, "urlErrorMsg": { "message": "URI-ovi zahtevaju odgovarajući prefiks HTTP / HTTPS." }, diff --git a/app/_locales/sv/messages.json b/app/_locales/sv/messages.json index a98db5bea015..163cdebc426e 100644 --- a/app/_locales/sv/messages.json +++ b/app/_locales/sv/messages.json @@ -492,12 +492,6 @@ "prev": { "message": "Föregående" }, - "primaryCurrencySetting": { - "message": "Primär valuta" - }, - "primaryCurrencySettingDescription": { - "message": "Välj native för att prioritera visning av värden i den ursprungliga valutan i kedjan (t.ex. ETH). Välj Fiat för att prioritera visning av värden i din valda fiatvaluta." - }, "privacyMsg": { "message": "Integritetspolicy" }, @@ -740,9 +734,6 @@ "unlockMessage": { "message": "Den decentraliserade webben väntar" }, - "updatedWithDate": { - "message": "Uppdaterat $1" - }, "urlErrorMsg": { "message": "URI:er kräver lämpligt HTTP/HTTPS-prefix." }, diff --git a/app/_locales/sw/messages.json b/app/_locales/sw/messages.json index d8ad6258e8ba..c1535d76cdd8 100644 --- a/app/_locales/sw/messages.json +++ b/app/_locales/sw/messages.json @@ -486,12 +486,6 @@ "prev": { "message": "Hakiki" }, - "primaryCurrencySetting": { - "message": "Sarafu ya Msingi" - }, - "primaryCurrencySettingDescription": { - "message": "Chagua mzawa ili kuweka kipaumbele kuonyesha thamani kwenye sarafu mzawa ya mnyororo (k.m ETH). Chagua Fiat ili uwelke kipaumbale kuonyesha thamani kwenye sarafu yako ya fiat uliyoichagua." - }, "privacyMsg": { "message": "Sera ya Faragha" }, @@ -743,9 +737,6 @@ "unlockMessage": { "message": "Wavuti uliotenganishwa unasubiri" }, - "updatedWithDate": { - "message": "Imesasishwa $1" - }, "urlErrorMsg": { "message": "URI huhitaji kiambishi sahihi cha HTTP/HTTPS." }, diff --git a/app/_locales/ta/messages.json b/app/_locales/ta/messages.json index 8e38061bebb2..d5d1929a2dc4 100644 --- a/app/_locales/ta/messages.json +++ b/app/_locales/ta/messages.json @@ -129,10 +129,6 @@ "fast": { "message": "வேகமான" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "கோப்பு இறக்குமதி வேலை செய்யவில்லையா? இங்கே கிளிக் செய்யவும்!", "description": "Helps user import their account from a JSON file" diff --git a/app/_locales/th/messages.json b/app/_locales/th/messages.json index c3b4a8a6e3fa..e6c074fe1264 100644 --- a/app/_locales/th/messages.json +++ b/app/_locales/th/messages.json @@ -120,10 +120,6 @@ "fast": { "message": "เร็ว" }, - "fiat": { - "message": "เงินตรา", - "description": "Exchange type" - }, "fileImportFail": { "message": "นำเข้าไฟล์ไม่สำเหร็จ กดที่นี่!", "description": "Helps user import their account from a JSON file" @@ -371,9 +367,6 @@ "unlock": { "message": "ปลดล็อก" }, - "updatedWithDate": { - "message": "อัปเดต $1 แล้ว" - }, "urlErrorMsg": { "message": "URI ต้องมีคำนำหน้าเป็น HTTP หรือ HTTPS" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 958fa1345041..1246c2a085a1 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "Mga detalye ng singil" }, - "fiat": { - "message": "Fiat", - "description": "Exchange type" - }, "fileImportFail": { "message": "Hindi gumagana ang pag-import ng file? Mag-click dito!", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "Ang MetaMask ay hindi nakakonekta sa site na ito" }, - "noConversionDateAvailable": { - "message": "Walang available na petsa sa pagpapapalit ng currency" - }, "noConversionRateAvailable": { "message": "Hindi available ang rate ng palitan" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "hindi available ang presyo" }, - "primaryCurrencySetting": { - "message": "Pangunahing Currency" - }, - "primaryCurrencySettingDescription": { - "message": "Piliin ang native para maisapriyoridad ang pagpapakita ng mga value sa native na currency ng chain (hal. ETH). Piliin ang Fiat para maisapriyoridad ang pagpapakita ng mga value sa napili mong fiat na salapi." - }, "primaryType": { "message": "Pangunahing uri" }, @@ -4747,13 +4734,6 @@ "smartTransactionSuccess": { "message": "Nakumpleto ang transaksyon mo" }, - "smartTransactionTakingTooLong": { - "message": "Paumanhin sa paghihintay" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Kung ang transaksyon mo ay hindi natapos sa loob ng $1, ito ay kakanselahin at hindi ka sisingilin para sa gas.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Mga Smart Transaction" }, @@ -5170,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "Subukan muli ang pag-swap. Narito kami para protektahan ka sa mga katulad na panganib sa susunod." }, - "stxEstimatedCompletion": { - "message": "Tinatayang pagkumpleto sa loob ng < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Nabigo ang pag-swap" }, @@ -6085,9 +6061,6 @@ "updateRequest": { "message": "Hiling sa pag-update" }, - "updatedWithDate": { - "message": "Na-update noong $1" - }, "uploadDropFile": { "message": "I-drop ang file mo rito" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 3fbbc1bc51ff..eedc60659269 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "Ücret bilgileri" }, - "fiat": { - "message": "Fiat Para", - "description": "Exchange type" - }, "fileImportFail": { "message": "Dosya içe aktarma çalışmıyor mu? Buraya tıklayın!", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "MetaMask bu siteye bağlı değil" }, - "noConversionDateAvailable": { - "message": "Para birimi dönüşüm tarihi mevcut değil" - }, "noConversionRateAvailable": { "message": "Dönüşüm oranı mevcut değil" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "fiyat mevcut değil" }, - "primaryCurrencySetting": { - "message": "Öncelikli para birimi" - }, - "primaryCurrencySettingDescription": { - "message": "Değerlerin zincirin yerli para biriminde (ör. ETH) görüntülenmesini önceliklendirmek için yerli seçimi yapın. Seçtiğiniz fiat parada değerlerin gösterilmesini önceliklendirmek için Fiat Para seçin." - }, "primaryType": { "message": "Öncelikli tür" }, @@ -4747,13 +4734,6 @@ "smartTransactionSuccess": { "message": "İşleminiz tamamlandı" }, - "smartTransactionTakingTooLong": { - "message": "Beklettiğimiz için özür dileriz" - }, - "smartTransactionTakingTooLongDescription": { - "message": "İşleminiz $1 dahilinde sonuçlanmazsa iptal edilir ve sizden gaz ücreti alınmaz.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Akıllı İşlemler" }, @@ -5170,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "Swap işlemini tekrar deneyin. Bir dahaki sefere sizi benzer risklere karşı korumak için burada olacağız." }, - "stxEstimatedCompletion": { - "message": "Tamamlanmasına kalan tahmini süre $1 altında", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Swap başarısız oldu" }, @@ -6085,9 +6061,6 @@ "updateRequest": { "message": "Talebi güncelle" }, - "updatedWithDate": { - "message": "$1 güncellendi" - }, "uploadDropFile": { "message": "Dosyanızı buraya sürükleyin" }, diff --git a/app/_locales/uk/messages.json b/app/_locales/uk/messages.json index 37bab506a87d..b0c011690910 100644 --- a/app/_locales/uk/messages.json +++ b/app/_locales/uk/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "Швидка" }, - "fiat": { - "message": "Вказівка", - "description": "Exchange type" - }, "fileImportFail": { "message": "Не працює імпорт файлу? Натисніть тут!", "description": "Helps user import their account from a JSON file" @@ -508,12 +504,6 @@ "prev": { "message": "Попередній" }, - "primaryCurrencySetting": { - "message": "Первісна валюта" - }, - "primaryCurrencySettingDescription": { - "message": "Оберіть \"рідна\", щоб пріоритезувати показ сум у рідних валютах мережі (напр.ETH). \nОберіть \"фіатна\", щоб пріоритезувати показ сум у ваших обраних фіатних валютах." - }, "privacyMsg": { "message": "Політика конфіденційності" }, @@ -765,9 +755,6 @@ "unlockMessage": { "message": "Децентралізована мережа очікує" }, - "updatedWithDate": { - "message": "Оновлено $1" - }, "urlErrorMsg": { "message": "URIs вимагають відповідного префікса HTTP/HTTPS." }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 52e7a3a18792..955c302f19a8 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "Chi tiết phí" }, - "fiat": { - "message": "Pháp định", - "description": "Exchange type" - }, "fileImportFail": { "message": "Tính năng nhập tập tin không hoạt động? Nhấp vào đây!", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "MetaMask không được kết nối với trang web này" }, - "noConversionDateAvailable": { - "message": "Hiện không có ngày quy đổi tiền tệ nào" - }, "noConversionRateAvailable": { "message": "Không có sẵn tỷ lệ quy đổi nào" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "giá không khả dụng" }, - "primaryCurrencySetting": { - "message": "Tiền tệ chính" - }, - "primaryCurrencySettingDescription": { - "message": "Chọn Gốc để ưu tiên hiển thị giá trị bằng đơn vị tiền tệ gốc của chuỗi (ví dụ: ETH). Chọn Pháp định để ưu tiên hiển thị giá trị bằng đơn vị tiền pháp định mà bạn chọn." - }, "primaryType": { "message": "Loại chính" }, @@ -4747,13 +4734,6 @@ "smartTransactionSuccess": { "message": "Giao dịch của bạn đã hoàn tất" }, - "smartTransactionTakingTooLong": { - "message": "Xin lỗi đã để bạn đợi lâu" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Nếu giao dịch của bạn không được hoàn thành trong vòng $1, thì giao dịch sẽ bị hủy và bạn sẽ không bị tính phí gas.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Giao dịch thông minh" }, @@ -5170,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "Hãy thử hoán đổi lại. Chúng tôi ở đây để bảo vệ bạn trước những rủi ro tương tự trong lần tới." }, - "stxEstimatedCompletion": { - "message": "Dự kiến hoàn thành sau < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Hoán đổi không thành công" }, @@ -6085,9 +6061,6 @@ "updateRequest": { "message": "Yêu cầu cập nhật" }, - "updatedWithDate": { - "message": "Đã cập nhật $1" - }, "uploadDropFile": { "message": "Thả tập tin của bạn vào đây" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 302eb5648224..14395afca8b8 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -1873,10 +1873,6 @@ "feeDetails": { "message": "费用详情" }, - "fiat": { - "message": "法币", - "description": "Exchange type" - }, "fileImportFail": { "message": "文件导入失败?点击这里!", "description": "Helps user import their account from a JSON file" @@ -3092,9 +3088,6 @@ "noConnectedAccountTitle": { "message": "MetaMask 未连接到此站点" }, - "noConversionDateAvailable": { - "message": "没有可用的货币转换日期" - }, "noConversionRateAvailable": { "message": "无可用汇率" }, @@ -3915,12 +3908,6 @@ "priceUnavailable": { "message": "价格不可用" }, - "primaryCurrencySetting": { - "message": "主要货币" - }, - "primaryCurrencySettingDescription": { - "message": "选择原生以优先显示链的原生货币(例如 ETH)的值。选择法币以优先显示以您所选法币显示的值。" - }, "primaryType": { "message": "主要类型" }, @@ -4747,13 +4734,6 @@ "smartTransactionSuccess": { "message": "您的交易已完成" }, - "smartTransactionTakingTooLong": { - "message": "抱歉让您久等" - }, - "smartTransactionTakingTooLongDescription": { - "message": "如果您的交易在 $1 内未完成,则会取消,您无需支付燃料费。", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "智能交易" }, @@ -5170,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "再次尝试进行交换。下次我们会在这里保护您免受类似风险。 " }, - "stxEstimatedCompletion": { - "message": "预计将在 $1 内完成", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "交换失败" }, @@ -6085,9 +6061,6 @@ "updateRequest": { "message": "更新请求" }, - "updatedWithDate": { - "message": "已于 $1 更新" - }, "uploadDropFile": { "message": "将您的文件放在此处" }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index 0924d284b529..dee06a7aef16 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -512,10 +512,6 @@ "feeAssociatedRequest": { "message": "這個請求會附帶一筆手續費。" }, - "fiat": { - "message": "法定貨幣", - "description": "Exchange type" - }, "fileImportFail": { "message": "檔案匯入失敗?點擊這裡!", "description": "Helps user import their account from a JSON file" @@ -944,12 +940,6 @@ "prev": { "message": "前一頁" }, - "primaryCurrencySetting": { - "message": "主要貨幣" - }, - "primaryCurrencySettingDescription": { - "message": "選擇原生來優先使用鏈上原生貨幣 (例如 ETH) 顯示金額。選擇法定貨幣來優先使用您選擇的法定貨幣顯示金額。" - }, "privacyMsg": { "message": "隱私政策" }, @@ -1380,9 +1370,6 @@ "message": "無法辨識這個自訂網路。我們建議您先$1再繼續。", "description": "$1 is a clickable link with text defined by the 'unrecognizedChanLinkText' key. The link will open to instructions for users to validate custom network details." }, - "updatedWithDate": { - "message": "更新時間 $1" - }, "urlErrorMsg": { "message": "URL 需要以適當的 HTTP/HTTPS 作為開頭" }, diff --git a/app/images/animations/smart-transaction-status/confirmed.lottie.json b/app/images/animations/smart-transaction-status/confirmed.lottie.json new file mode 100644 index 000000000000..d5552d380b45 --- /dev/null +++ b/app/images/animations/smart-transaction-status/confirmed.lottie.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.7","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":175,"w":500,"h":500,"nm":"OC_MMSmartTransactions_Confirmation 3","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"plane","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.354,"y":0.88},"o":{"x":0.182,"y":1},"t":-79,"s":[189.265,312.957,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.136,"y":1},"o":{"x":0.854,"y":0.176},"t":-69,"s":[168,335,0],"to":[0,0,0],"ti":[0,0,0]},{"t":-26,"s":[250,250,0]}],"ix":2,"x":"var $bm_rt;\n$bm_rt = wiggle($bm_mul(time, 5), effect('Slider Control')('Slider'));"},"a":{"a":0,"k":[2331.957,1839.486,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0.909,0.909,0.333],"y":[0,0,0]},"t":0,"s":[80,80,100]},{"i":{"x":[1,1,0.667],"y":[1,1,1]},"o":{"x":[1,1,0.333],"y":[0,0,0]},"t":15,"s":[92,92,100]},{"t":36,"s":[20,20,100]}],"ix":6}},"ao":0,"ef":[{"ty":5,"nm":"Slider Control","np":3,"mn":"ADBE Slider Control","ix":1,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":-53,"s":[0]},{"t":-24,"s":[5]}],"ix":1}}]}],"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.281,-1.65],[0,0],[2.829,3.643],[0,0],[-0.44,2.042],[0,0],[-0.498,-2.312],[0,0]],"o":[[0,0],[-2.829,3.643],[0,0],[-1.281,-1.65],[0,0],[0.498,-2.312],[0,0],[0.44,2.042]],"v":[[2370.826,1917.848],[2337.538,1960.709],[2326.376,1960.709],[2293.088,1917.848],[2291.761,1912.024],[2329.809,1735.544],[2334.104,1735.544],[2372.152,1912.024]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.5,0],[0,0],[0.704,3.319],[0,0],[-1.207,-2.19],[0,0]],"o":[[0,0],[-3.393,0],[0,0],[-0.47,-2.456],[0,0],[2.65,4.81]],"v":[[2443.586,1915.245],[2389.666,1915.245],[2382.638,1909.553],[2336.347,1718.15],[2340.428,1716.677],[2449.917,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.469,-2.456],[0,0],[3.393,0],[0,0],[-2.65,4.81]],"o":[[1.207,-2.19],[0,0],[-0.704,3.319],[0,0],[-5.49,0],[0,0]],"v":[[2323.467,1716.695],[2327.548,1718.168],[2281.274,1909.552],[2274.247,1915.245],[2220.316,1915.245],[2213.997,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":36,"st":-108,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Confirmation","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[11.185,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-58.942,-0.221],[-19.5,39.221],[58.942,-39.221]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.201],"y":[1]},"o":{"x":[0.725],"y":[0]},"t":81,"s":[0]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.566],"y":[0]},"t":89,"s":[26.7]},{"t":102,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":81,"op":175,"st":81,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Stroke Contour","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[96,96,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-76.193],[76.193,0],[0,76.193],[-76.193,0]],"o":[[0,76.193],[-76.193,0],[0,-76.193],[76.193,0]],"v":[[137.959,0],[0,137.959],[-137.959,0],[0,-137.959]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.206],"y":[1]},"o":{"x":[0.905],"y":[0]},"t":19.207,"s":[0]},{"t":76,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.206],"y":[1]},"o":{"x":[0.905],"y":[0]},"t":14,"s":[0]},{"t":58.79296875,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":14,"op":175,"st":14,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.356,0.356,0.667],"y":[1,1,1]},"o":{"x":[0.015,0.015,0.333],"y":[1.1,1.1,0]},"t":49,"s":[30,30,100]},{"i":{"x":[0,0,0.833],"y":[1,1,1]},"o":{"x":[0.694,0.694,0.167],"y":[0,0,0]},"t":89,"s":[100,100,100]},{"t":135,"s":[89,89,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-76.193],[76.193,0],[0,76.193],[-76.193,0]],"o":[[0,76.193],[-76.193,0],[0,-76.193],[76.193,0]],"v":[[137.959,0],[0,137.959],[-137.959,0],[0,-137.959]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":49,"op":175,"st":49,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Xplosion 6","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[428,428,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[80,80,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Xplosion 8","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[364,376,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Xplosion 12","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[190,22,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Xplosion 11","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[351,462,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Xplosion 10","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[54,460,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Xplosion 5","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[427,66,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Xplosion 4","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[28,247,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[71,71,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Xplosion 9","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[243,409,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[50,50,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"Xplosion 7","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[250,92,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[50,50,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"Xplosion 3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[477,245,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"Xplosion 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[72,373,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"Xplosion","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[84,90,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"Circle 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.356,0.356,0.667],"y":[1,1,1]},"o":{"x":[0.015,0.015,0.333],"y":[1.1,1.1,0]},"t":37,"s":[30,30,100]},{"t":77,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-76.193],[76.193,0],[0,76.193],[-76.193,0]],"o":[[0,76.193],[-76.193,0],[0,-76.193],[76.193,0]],"v":[[137.959,0],[0,137.959],[-137.959,0],[0,-137.959]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.722089460784,0.722089460784,0.722089460784,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":37,"op":89,"st":37,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/images/animations/smart-transaction-status/failed.lottie.json b/app/images/animations/smart-transaction-status/failed.lottie.json new file mode 100644 index 000000000000..f2405f22c72d --- /dev/null +++ b/app/images/animations/smart-transaction-status/failed.lottie.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.7","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":175,"w":500,"h":500,"nm":"OC_MMSmartTransactions_Fail","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"CTRL","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.086,0.086,0.667],"y":[0.96,0.96,1]},"o":{"x":[0.015,0.015,0.333],"y":[0.599,0.599,0]},"t":41,"s":[40,40,100]},{"i":{"x":[0,0,0.833],"y":[1,1,1]},"o":{"x":[0.539,0.539,0.167],"y":[-0.194,-0.194,0]},"t":71,"s":[80,80,100]},{"t":103,"s":[75,75,100]}],"ix":6}},"ao":0,"ip":41,"op":179,"st":4,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"exlamation point","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.014,59.504,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[0,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":41,"op":180,"st":5,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"exclamation line","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.014,-0.031,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-26.571],[0,26.571]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":41,"op":180,"st":5,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"triangle","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.014,-0.031,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.306,-7.458],[0,0],[-8.611,0],[0,0],[4.306,7.458],[0,0]],"o":[[0,0],[-4.306,7.458],[0,0],[8.611,0],[0,0],[-4.306,-7.458]],"v":[[-9.688,-95.736],[-113.775,84.549],[-104.088,101.329],[104.088,101.329],[113.775,84.549],[9.688,-95.736]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.878431372549,0.392156862745,0.439215686275,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":41,"op":180,"st":5,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"plane","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.354,"y":0.88},"o":{"x":0.182,"y":1},"t":-79,"s":[189.265,312.957,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.136,"y":1},"o":{"x":0.854,"y":0.176},"t":-69,"s":[168,335,0],"to":[0,0,0],"ti":[0,0,0]},{"t":-26,"s":[250,250,0]}],"ix":2,"x":"var $bm_rt;\n$bm_rt = wiggle($bm_mul(time, 5), effect('Slider Control')('Slider'));"},"a":{"a":0,"k":[2331.957,1839.486,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0.909,0.909,0.333],"y":[0,0,0]},"t":0,"s":[80,80,100]},{"i":{"x":[1,1,0.667],"y":[1,1,1]},"o":{"x":[1,1,0.333],"y":[0,0,0]},"t":15,"s":[92,92,100]},{"t":36,"s":[20,20,100]}],"ix":6}},"ao":0,"ef":[{"ty":5,"nm":"Slider Control","np":3,"mn":"ADBE Slider Control","ix":1,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":-53,"s":[0]},{"t":-24,"s":[5]}],"ix":1}}]}],"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.281,-1.65],[0,0],[2.829,3.643],[0,0],[-0.44,2.042],[0,0],[-0.498,-2.312],[0,0]],"o":[[0,0],[-2.829,3.643],[0,0],[-1.281,-1.65],[0,0],[0.498,-2.312],[0,0],[0.44,2.042]],"v":[[2370.826,1917.848],[2337.538,1960.709],[2326.376,1960.709],[2293.088,1917.848],[2291.761,1912.024],[2329.809,1735.544],[2334.104,1735.544],[2372.152,1912.024]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.5,0],[0,0],[0.704,3.319],[0,0],[-1.207,-2.19],[0,0]],"o":[[0,0],[-3.393,0],[0,0],[-0.47,-2.456],[0,0],[2.65,4.81]],"v":[[2443.586,1915.245],[2389.666,1915.245],[2382.638,1909.553],[2336.347,1718.15],[2340.428,1716.677],[2449.917,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.469,-2.456],[0,0],[3.393,0],[0,0],[-2.65,4.81]],"o":[[1.207,-2.19],[0,0],[-0.704,3.319],[0,0],[-5.49,0],[0,0]],"v":[[2323.467,1716.695],[2327.548,1718.168],[2281.274,1909.552],[2274.247,1915.245],[2220.316,1915.245],[2213.997,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":36,"st":-108,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Stroke Contour","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[96,96,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-76.193],[76.193,0],[0,76.193],[-76.193,0]],"o":[[0,76.193],[-76.193,0],[0,-76.193],[76.193,0]],"v":[[137.959,0],[0,137.959],[-137.959,0],[0,-137.959]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.206],"y":[1]},"o":{"x":[0.905],"y":[0]},"t":19.207,"s":[0]},{"t":76,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.206],"y":[1]},"o":{"x":[0.905],"y":[0]},"t":14,"s":[0]},{"t":58.79296875,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.878431432387,0.392156892664,0.439215716194,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":14,"op":175,"st":14,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/images/animations/smart-transaction-status/processing.lottie.json b/app/images/animations/smart-transaction-status/processing.lottie.json new file mode 100644 index 000000000000..96a4356b24ce --- /dev/null +++ b/app/images/animations/smart-transaction-status/processing.lottie.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.7","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":117,"w":500,"h":500,"nm":"OC_MMSmartTransactions_Processing","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Right Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":1,"y":1},"o":{"x":0.797,"y":0.561},"t":0,"s":[371,284,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.039,"y":0.701},"o":{"x":0,"y":0},"t":9,"s":[370,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.204,"y":0},"o":{"x":1,"y":0.399},"t":23,"s":[370,180,0],"to":[0,0,0],"ti":[0.627,-21.241,0]},{"i":{"x":0.059,"y":0.016},"o":{"x":0.391,"y":0.209},"t":41,"s":[370,250,0],"to":[-2.5,84.75,0],"ti":[15.378,106.092,0]},{"i":{"x":0.116,"y":0.585},"o":{"x":0.756,"y":0.511},"t":70,"s":[88,232,0],"to":[-18.703,-129.031,0],"ti":[-80,-102,0]},{"i":{"x":0.223,"y":0.67},"o":{"x":0.727,"y":1},"t":92,"s":[399,141,0],"to":[44.438,56.659,0],"ti":[0,0,0]},{"t":117,"s":[371,284,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":117,"st":6,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Center Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":1,"y":1},"o":{"x":0.493,"y":0.345},"t":0,"s":[251,234,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.039,"y":0.904},"o":{"x":0.474,"y":0.527},"t":4,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.116,"y":0.334},"o":{"x":1,"y":0.126},"t":19,"s":[250,180,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.116,"y":0.769},"o":{"x":0.56,"y":0.224},"t":38,"s":[250,250,0],"to":[0,0,0],"ti":[78,-51,0]},{"i":{"x":0.116,"y":0.499},"o":{"x":0.701,"y":0.293},"t":60,"s":[216,140,0],"to":[-78,51,0],"ti":[-67,85,0]},{"i":{"x":0.667,"y":0.768},"o":{"x":0.803,"y":0.791},"t":91,"s":[277,328,0],"to":[67,-85,0],"ti":[5.375,-23.25,0]},{"t":119,"s":[251,234,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":117,"st":3,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Left Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.039,"y":0.637},"o":{"x":1,"y":0.718},"t":0,"s":[130,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":0},"o":{"x":1,"y":0.399},"t":17,"s":[130,180,0],"to":[0,0,0],"ti":[0.031,-37.625,0]},{"i":{"x":0.116,"y":0.616},"o":{"x":0.712,"y":0.351},"t":35,"s":[130,250,0],"to":[3.938,127.151,0],"ti":[-66.538,75.217,0]},{"i":{"x":0.116,"y":0.173},"o":{"x":0.701,"y":0.488},"t":60,"s":[367,353,0],"to":[46,-52,0],"ti":[68,26,0]},{"i":{"x":0.223,"y":0.626},"o":{"x":0.769,"y":1},"t":90,"s":[241,180,0],"to":[-41.772,-15.972,0],"ti":[1,-85,0]},{"t":117,"s":[130,250,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":117,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/images/animations/smart-transaction-status/submitting-intro.lottie.json b/app/images/animations/smart-transaction-status/submitting-intro.lottie.json new file mode 100644 index 000000000000..7ab9d476cdaf --- /dev/null +++ b/app/images/animations/smart-transaction-status/submitting-intro.lottie.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.7","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":112,"w":500,"h":500,"nm":"OC_MMSmartTransactions_SubmittingIntro","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"CTRL","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":112,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"plane","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.354,"y":0.88},"o":{"x":0.182,"y":1},"t":39,"s":[189.265,312.957,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.136,"y":1},"o":{"x":0.854,"y":0.176},"t":49,"s":[168,335,0],"to":[0,0,0],"ti":[0,0,0]},{"t":92,"s":[250,250,0]}],"ix":2,"x":"var $bm_rt;\n$bm_rt = wiggle($bm_mul(time, 5), effect('Slider Control')('Slider'));"},"a":{"a":0,"k":[2331.957,1839.486,0],"ix":1},"s":{"a":0,"k":[80,80,100],"ix":6}},"ao":0,"ef":[{"ty":5,"nm":"Slider Control","np":3,"mn":"ADBE Slider Control","ix":1,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":65,"s":[0]},{"t":94,"s":[5]}],"ix":1}}]}],"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.281,-1.65],[0,0],[2.829,3.643],[0,0],[-0.44,2.042],[0,0],[-0.498,-2.312],[0,0]],"o":[[0,0],[-2.829,3.643],[0,0],[-1.281,-1.65],[0,0],[0.498,-2.312],[0,0],[0.44,2.042]],"v":[[2370.826,1917.848],[2337.538,1960.709],[2326.376,1960.709],[2293.088,1917.848],[2291.761,1912.024],[2329.809,1735.544],[2334.104,1735.544],[2372.152,1912.024]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.5,0],[0,0],[0.704,3.319],[0,0],[-1.207,-2.19],[0,0]],"o":[[0,0],[-3.393,0],[0,0],[-0.47,-2.456],[0,0],[2.65,4.81]],"v":[[2443.586,1915.245],[2389.666,1915.245],[2382.638,1909.553],[2336.347,1718.15],[2340.428,1716.677],[2449.917,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.469,-2.456],[0,0],[3.393,0],[0,0],[-2.65,4.81]],"o":[[1.207,-2.19],[0,0],[-0.704,3.319],[0,0],[-5.49,0],[0,0]],"v":[[2323.467,1716.695],[2327.548,1718.168],[2281.274,1909.552],[2274.247,1915.245],[2220.316,1915.245],[2213.997,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":39,"op":112,"st":10,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Left Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.039,"y":0.637},"o":{"x":1,"y":0.718},"t":-117,"s":[130,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":0},"o":{"x":1,"y":0.399},"t":-100,"s":[130,180,0],"to":[0,0,0],"ti":[0.031,-37.625,0]},{"i":{"x":0.116,"y":0.616},"o":{"x":0.712,"y":0.351},"t":-82,"s":[130,250,0],"to":[3.938,127.151,0],"ti":[-66.538,75.217,0]},{"i":{"x":0.116,"y":0.173},"o":{"x":0.701,"y":0.488},"t":-57,"s":[367,353,0],"to":[46,-52,0],"ti":[68,26,0]},{"i":{"x":0.223,"y":0.626},"o":{"x":0.769,"y":1},"t":-27,"s":[241,180,0],"to":[-41.772,-15.972,0],"ti":[1,-85,0]},{"i":{"x":0.223,"y":0.829},"o":{"x":0.167,"y":0},"t":0,"s":[130,250,0],"to":[-1,85,0],"ti":[0,0,0]},{"t":12,"s":[251,238,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":12,"st":-117,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Center Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":1,"y":1},"o":{"x":0.493,"y":0.345},"t":-117,"s":[251,234,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.039,"y":0.904},"o":{"x":0.474,"y":0.527},"t":-113,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.116,"y":0.334},"o":{"x":1,"y":0.126},"t":-98,"s":[250,180,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.116,"y":0.769},"o":{"x":0.56,"y":0.224},"t":-79,"s":[250,250,0],"to":[0,0,0],"ti":[78,-51,0]},{"i":{"x":0.116,"y":0.499},"o":{"x":0.701,"y":0.293},"t":-57,"s":[216,140,0],"to":[-78,51,0],"ti":[-67,85,0]},{"i":{"x":0.667,"y":0.768},"o":{"x":0.803,"y":0.791},"t":-26,"s":[277,328,0],"to":[67,-85,0],"ti":[5.375,-23.25,0]},{"i":{"x":0.407,"y":0},"o":{"x":0.484,"y":0.816},"t":2,"s":[251,234,0],"to":[-5.375,23.25,0],"ti":[0,0,0]},{"i":{"x":0.258,"y":0.825},"o":{"x":1,"y":0.894},"t":12,"s":[251,234,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":1,"y":1},"o":{"x":1,"y":0.12},"t":17,"s":[251,214,0],"to":[0,0,0],"ti":[0,0,0]},{"t":39,"s":[167,319,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.118,0.118,0.667],"y":[1,1,1]},"o":{"x":[0.821,0.821,0.333],"y":[0,0,0]},"t":8,"s":[100,100,100]},{"t":17,"s":[146,146,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":39,"st":-114,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Right Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":1,"y":1},"o":{"x":0.797,"y":0.561},"t":-117,"s":[371,284,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.039,"y":0.701},"o":{"x":0,"y":0},"t":-108,"s":[370,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.204,"y":0},"o":{"x":1,"y":0.399},"t":-94,"s":[370,180,0],"to":[0,0,0],"ti":[0.627,-21.241,0]},{"i":{"x":0.059,"y":0.016},"o":{"x":0.391,"y":0.209},"t":-76,"s":[370,250,0],"to":[-2.5,84.75,0],"ti":[15.378,106.092,0]},{"i":{"x":0.116,"y":0.585},"o":{"x":0.756,"y":0.511},"t":-47,"s":[88,232,0],"to":[-18.703,-129.031,0],"ti":[-80,-102,0]},{"i":{"x":0.223,"y":0.67},"o":{"x":0.727,"y":1},"t":-25,"s":[399,141,0],"to":[44.438,56.659,0],"ti":[0,0,0]},{"i":{"x":0.223,"y":0.822},"o":{"x":0.167,"y":0},"t":0,"s":[371,284,0],"to":[0,0,0],"ti":[65,80.5,0]},{"t":12,"s":[251,235,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":12,"st":-111,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Dash Orb Small 4","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.428],"y":[1]},"o":{"x":[0.578],"y":[0]},"t":85,"s":[40.089]},{"t":92,"s":[-198.911]}],"ix":3},"y":{"a":0,"k":117.5,"ix":4}},"a":{"a":0,"k":[-49.911,-144,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-27,-144],[-71.5,-144]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":86.5,"s":[0]},{"t":93,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":80,"s":[0]},{"t":86.5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":20,"op":112,"st":20,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Dash Orb Small 3","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.428],"y":[1]},"o":{"x":[0.578],"y":[0]},"t":104,"s":[121.104]},{"t":111,"s":[-117.896]}],"ix":3},"y":{"a":0,"k":149.278,"ix":4}},"a":{"a":0,"k":[-49.911,-144,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-27,-144],[-71.5,-144]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":105.5,"s":[0]},{"t":112,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":99,"s":[0]},{"t":105.5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":39,"op":112,"st":39,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Dash Orb Small 2","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.428],"y":[1]},"o":{"x":[0.578],"y":[0]},"t":101,"s":[166.352]},{"t":108,"s":[-72.648]}],"ix":3},"y":{"a":0,"k":-137.592,"ix":4}},"a":{"a":0,"k":[-49.911,-144,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-27,-144],[-71.5,-144]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":102.5,"s":[0]},{"t":109,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":96,"s":[0]},{"t":102.5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":36,"op":112,"st":36,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/images/animations/smart-transaction-status/submitting-loop.lottie.json b/app/images/animations/smart-transaction-status/submitting-loop.lottie.json new file mode 100644 index 000000000000..caf5052ee85f --- /dev/null +++ b/app/images/animations/smart-transaction-status/submitting-loop.lottie.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.7","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":32,"w":500,"h":500,"nm":"OC_MMSmartTransactions_SubmittingLoop","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"CTRL","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":32,"st":-80,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"plane","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.354,"y":0.88},"o":{"x":0.182,"y":1},"t":-41,"s":[189.265,312.957,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.136,"y":1},"o":{"x":0.854,"y":0.176},"t":-31,"s":[168,335,0],"to":[0,0,0],"ti":[0,0,0]},{"t":12,"s":[250,250,0]}],"ix":2,"x":"var $bm_rt;\n$bm_rt = wiggle($bm_mul(time, 5), effect('Slider Control')('Slider'));"},"a":{"a":0,"k":[2331.957,1839.486,0],"ix":1},"s":{"a":0,"k":[80,80,100],"ix":6}},"ao":0,"ef":[{"ty":5,"nm":"Slider Control","np":3,"mn":"ADBE Slider Control","ix":1,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":-15,"s":[0]},{"t":14,"s":[5]}],"ix":1}}]}],"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.281,-1.65],[0,0],[2.829,3.643],[0,0],[-0.44,2.042],[0,0],[-0.498,-2.312],[0,0]],"o":[[0,0],[-2.829,3.643],[0,0],[-1.281,-1.65],[0,0],[0.498,-2.312],[0,0],[0.44,2.042]],"v":[[2370.826,1917.848],[2337.538,1960.709],[2326.376,1960.709],[2293.088,1917.848],[2291.761,1912.024],[2329.809,1735.544],[2334.104,1735.544],[2372.152,1912.024]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.5,0],[0,0],[0.704,3.319],[0,0],[-1.207,-2.19],[0,0]],"o":[[0,0],[-3.393,0],[0,0],[-0.47,-2.456],[0,0],[2.65,4.81]],"v":[[2443.586,1915.245],[2389.666,1915.245],[2382.638,1909.553],[2336.347,1718.15],[2340.428,1716.677],[2449.917,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.469,-2.456],[0,0],[3.393,0],[0,0],[-2.65,4.81]],"o":[[1.207,-2.19],[0,0],[-0.704,3.319],[0,0],[-5.49,0],[0,0]],"v":[[2323.467,1716.695],[2327.548,1718.168],[2281.274,1909.552],[2274.247,1915.245],[2220.316,1915.245],[2213.997,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":32,"st":-70,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Left Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.039,"y":0.637},"o":{"x":1,"y":0.718},"t":-197,"s":[130,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":0},"o":{"x":1,"y":0.399},"t":-180,"s":[130,180,0],"to":[0,0,0],"ti":[0.031,-37.625,0]},{"i":{"x":0.116,"y":0.616},"o":{"x":0.712,"y":0.351},"t":-162,"s":[130,250,0],"to":[3.938,127.151,0],"ti":[-66.538,75.217,0]},{"i":{"x":0.116,"y":0.173},"o":{"x":0.701,"y":0.488},"t":-137,"s":[367,353,0],"to":[46,-52,0],"ti":[68,26,0]},{"i":{"x":0.223,"y":0.626},"o":{"x":0.769,"y":1},"t":-107,"s":[241,180,0],"to":[-41.772,-15.972,0],"ti":[1,-85,0]},{"i":{"x":0.223,"y":0.829},"o":{"x":0.167,"y":0},"t":-80,"s":[130,250,0],"to":[-1,85,0],"ti":[0,0,0]},{"t":-68,"s":[251,238,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-68.6060606060606,"op":-68,"st":-197,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Center Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":1,"y":1},"o":{"x":0.493,"y":0.345},"t":-197,"s":[251,234,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.039,"y":0.904},"o":{"x":0.474,"y":0.527},"t":-193,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.116,"y":0.334},"o":{"x":1,"y":0.126},"t":-178,"s":[250,180,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.116,"y":0.769},"o":{"x":0.56,"y":0.224},"t":-159,"s":[250,250,0],"to":[0,0,0],"ti":[78,-51,0]},{"i":{"x":0.116,"y":0.499},"o":{"x":0.701,"y":0.293},"t":-137,"s":[216,140,0],"to":[-78,51,0],"ti":[-67,85,0]},{"i":{"x":0.667,"y":0.768},"o":{"x":0.803,"y":0.791},"t":-106,"s":[277,328,0],"to":[67,-85,0],"ti":[5.375,-23.25,0]},{"i":{"x":0.407,"y":0},"o":{"x":0.484,"y":0.816},"t":-78,"s":[251,234,0],"to":[-5.375,23.25,0],"ti":[0,0,0]},{"i":{"x":0.258,"y":0.825},"o":{"x":1,"y":0.894},"t":-68,"s":[251,234,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":1,"y":1},"o":{"x":1,"y":0.12},"t":-63,"s":[251,214,0],"to":[0,0,0],"ti":[0,0,0]},{"t":-41,"s":[167,319,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.118,0.118,0.667],"y":[1,1,1]},"o":{"x":[0.821,0.821,0.333],"y":[0,0,0]},"t":-72,"s":[100,100,100]},{"t":-63,"s":[146,146,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-41.6060606060606,"op":-41,"st":-194,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Right Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":1,"y":1},"o":{"x":0.797,"y":0.561},"t":-197,"s":[371,284,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.039,"y":0.701},"o":{"x":0,"y":0},"t":-188,"s":[370,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.204,"y":0},"o":{"x":1,"y":0.399},"t":-174,"s":[370,180,0],"to":[0,0,0],"ti":[0.627,-21.241,0]},{"i":{"x":0.059,"y":0.016},"o":{"x":0.391,"y":0.209},"t":-156,"s":[370,250,0],"to":[-2.5,84.75,0],"ti":[15.378,106.092,0]},{"i":{"x":0.116,"y":0.585},"o":{"x":0.756,"y":0.511},"t":-127,"s":[88,232,0],"to":[-18.703,-129.031,0],"ti":[-80,-102,0]},{"i":{"x":0.223,"y":0.67},"o":{"x":0.727,"y":1},"t":-105,"s":[399,141,0],"to":[44.438,56.659,0],"ti":[0,0,0]},{"i":{"x":0.223,"y":0.822},"o":{"x":0.167,"y":0},"t":-80,"s":[371,284,0],"to":[0,0,0],"ti":[65,80.5,0]},{"t":-68,"s":[251,235,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-68.6060606060606,"op":-68,"st":-191,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Dash Orb Small 4","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.428],"y":[1]},"o":{"x":[0.578],"y":[0]},"t":5,"s":[40.089]},{"t":12,"s":[-198.911]}],"ix":3},"y":{"a":0,"k":117.5,"ix":4}},"a":{"a":0,"k":[-49.911,-144,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-27,-144],[-71.5,-144]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":6.5,"s":[0]},{"t":13,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":0,"s":[0]},{"t":6.5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":32,"st":-60,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Dash Orb Small 3","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.428],"y":[1]},"o":{"x":[0.578],"y":[0]},"t":24,"s":[121.104]},{"t":31,"s":[-117.896]}],"ix":3},"y":{"a":0,"k":149.278,"ix":4}},"a":{"a":0,"k":[-49.911,-144,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-27,-144],[-71.5,-144]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":25.5,"s":[0]},{"t":32,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":19,"s":[0]},{"t":25.5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":32,"st":-41,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Dash Orb Small 2","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.428],"y":[1]},"o":{"x":[0.578],"y":[0]},"t":21,"s":[166.352]},{"t":28,"s":[-72.648]}],"ix":3},"y":{"a":0,"k":-137.592,"ix":4}},"a":{"a":0,"k":[-49.911,-144,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-27,-144],[-71.5,-144]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":22.5,"s":[0]},{"t":29,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":16,"s":[0]},{"t":22.5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":32,"st":-44,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts index 33bf9bac0f22..76fb2386f1f6 100644 --- a/app/scripts/constants/sentry-state.ts +++ b/app/scripts/constants/sentry-state.ts @@ -102,6 +102,10 @@ export const SENTRY_BACKGROUND_STATE = { destNetworkAllowlist: [], srcNetworkAllowlist: [], }, + destTokens: {}, + destTopAssets: [], + srcTokens: {}, + srcTopAssets: [], }, }, CronjobController: { @@ -226,7 +230,7 @@ export const SENTRY_BACKGROUND_STATE = { showFiatInTestnets: true, showTestNetworks: true, smartTransactionsOptInStatus: true, - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, petnamesEnabled: true, showConfirmationAdvancedDetails: true, }, diff --git a/app/scripts/lib/account-tracker.test.ts b/app/scripts/controllers/account-tracker-controller.test.ts similarity index 85% rename from app/scripts/lib/account-tracker.test.ts rename to app/scripts/controllers/account-tracker-controller.test.ts index 7cc0dcba14c7..dbabb927fa71 100644 --- a/app/scripts/lib/account-tracker.test.ts +++ b/app/scripts/controllers/account-tracker-controller.test.ts @@ -1,19 +1,19 @@ import EventEmitter from 'events'; import { ControllerMessenger } from '@metamask/base-controller'; import { InternalAccount } from '@metamask/keyring-api'; -import { Hex } from '@metamask/utils'; import { BlockTracker, Provider } from '@metamask/network-controller'; import { flushPromises } from '../../../test/lib/timer-helpers'; -import PreferencesController from '../controllers/preferences-controller'; -import OnboardingController from '../controllers/onboarding'; import { createTestProviderTools } from '../../../test/stub/provider'; -import AccountTracker, { - AccountTrackerOptions, +import PreferencesController from './preferences-controller'; +import type { + AccountTrackerControllerOptions, AllowedActions, AllowedEvents, - getDefaultAccountTrackerState, -} from './account-tracker'; +} from './account-tracker-controller'; +import AccountTrackerController, { + getDefaultAccountTrackerControllerState, +} from './account-tracker-controller'; const noop = () => true; const currentNetworkId = '5'; @@ -68,18 +68,18 @@ type WithControllerOptions = { useMultiAccountBalanceChecker?: boolean; getNetworkClientById?: jest.Mock; getSelectedAccount?: jest.Mock; -} & Partial; +} & Partial; type WithControllerCallback = ({ controller, blockTrackerFromHookStub, blockTrackerStub, - triggerOnAccountRemoved, + triggerAccountRemoved, }: { - controller: AccountTracker; + controller: AccountTrackerController; blockTrackerFromHookStub: MockBlockTracker; blockTrackerStub: MockBlockTracker; - triggerOnAccountRemoved: (address: string) => void; + triggerAccountRemoved: (address: string) => void; }) => ReturnValue; type WithControllerArgs = @@ -132,23 +132,37 @@ function withController( chainId: '0x1', }); - const blockTrackerFromHookStub = buildMockBlockTracker(); + const getNetworkStateStub = jest.fn().mockReturnValue({ + selectedNetworkClientId: 'selectedNetworkClientId', + }); + controllerMessenger.registerActionHandler( + 'NetworkController:getState', + getNetworkStateStub, + ); + const blockTrackerFromHookStub = buildMockBlockTracker(); const getNetworkClientByIdStub = jest.fn().mockReturnValue({ configuration: { - chainId: '0x1', + chainId: currentChainId, }, blockTracker: blockTrackerFromHookStub, provider: providerFromHook, }); - controllerMessenger.registerActionHandler( 'NetworkController:getNetworkClientById', getNetworkClientById || getNetworkClientByIdStub, ); - const controller = new AccountTracker({ - initState: getDefaultAccountTrackerState(), + const getOnboardingControllerState = jest.fn().mockReturnValue({ + completedOnboarding, + }); + controllerMessenger.registerActionHandler( + 'OnboardingController:getState', + getOnboardingControllerState, + ); + + const controller = new AccountTrackerController({ + state: getDefaultAccountTrackerControllerState(), provider: provider as Provider, blockTracker: blockTrackerStub as unknown as BlockTracker, getNetworkIdentifier: jest.fn(), @@ -159,13 +173,20 @@ function withController( }), }, } as PreferencesController, - onboardingController: { - state: { - completedOnboarding, - }, - } as OnboardingController, - controllerMessenger, - getCurrentChainId: () => currentChainId, + messenger: controllerMessenger.getRestricted({ + name: 'AccountTrackerController', + allowedActions: [ + 'AccountsController:getSelectedAccount', + 'NetworkController:getState', + 'NetworkController:getNetworkClientById', + 'OnboardingController:getState', + ], + allowedEvents: [ + 'AccountsController:selectedEvmAccountChange', + 'OnboardingController:stateChange', + 'KeyringController:accountRemoved', + ], + }), ...accountTrackerOptions, }); @@ -173,13 +194,13 @@ function withController( controller, blockTrackerFromHookStub, blockTrackerStub, - triggerOnAccountRemoved: (address: string) => { + triggerAccountRemoved: (address: string) => { controllerMessenger.publish('KeyringController:accountRemoved', address); }, }); } -describe('Account Tracker', () => { +describe('AccountTrackerController', () => { describe('start', () => { it('restarts the subscription to the block tracker and update accounts', async () => { withController(({ controller, blockTrackerStub }) => { @@ -456,9 +477,7 @@ describe('Account Tracker', () => { expect(updateAccountsSpy).toHaveBeenCalledWith(undefined); - const newState = controller.store.getState(); - - expect(newState).toStrictEqual({ + expect(controller.state).toStrictEqual({ accounts: {}, accountsByChainId: {}, currentBlockGasLimit: GAS_LIMIT, @@ -509,9 +528,7 @@ describe('Account Tracker', () => { expect(updateAccountsSpy).toHaveBeenCalledWith('mainnet'); - const newState = controller.store.getState(); - - expect(newState).toStrictEqual({ + expect(controller.state).toStrictEqual({ accounts: {}, accountsByChainId: {}, currentBlockGasLimit: '', @@ -567,8 +584,7 @@ describe('Account Tracker', () => { async ({ controller }) => { await controller.updateAccounts(); - const state = controller.store.getState(); - expect(state).toStrictEqual({ + expect(controller.state).toStrictEqual({ accounts: {}, currentBlockGasLimit: '', accountsByChainId: {}, @@ -579,7 +595,6 @@ describe('Account Tracker', () => { }); describe('chain does not have single call balance address', () => { - const getCurrentChainIdStub: () => Hex = () => '0x999'; // chain without single call balance address const mockAccountsWithSelectedAddress = { ...mockAccounts, [SELECTED_ADDRESS]: { @@ -600,11 +615,9 @@ describe('Account Tracker', () => { { completedOnboarding: true, useMultiAccountBalanceChecker: true, - getCurrentChainId: getCurrentChainIdStub, + state: mockInitialState, }, async ({ controller }) => { - controller.store.updateState(mockInitialState); - await controller.updateAccounts(); const accounts = { @@ -622,8 +635,7 @@ describe('Account Tracker', () => { }, }; - const newState = controller.store.getState(); - expect(newState).toStrictEqual({ + expect(controller.state).toStrictEqual({ accounts, accountsByChainId: { '0x999': accounts, @@ -642,11 +654,9 @@ describe('Account Tracker', () => { { completedOnboarding: true, useMultiAccountBalanceChecker: false, - getCurrentChainId: getCurrentChainIdStub, + state: mockInitialState, }, async ({ controller }) => { - controller.store.updateState(mockInitialState); - await controller.updateAccounts(); const accounts = { @@ -661,8 +671,7 @@ describe('Account Tracker', () => { }, }; - const newState = controller.store.getState(); - expect(newState).toStrictEqual({ + expect(controller.state).toStrictEqual({ accounts, accountsByChainId: { '0x999': accounts, @@ -686,20 +695,18 @@ describe('Account Tracker', () => { getNetworkIdentifier: jest .fn() .mockReturnValue('http://not-localhost:8545'), - getCurrentChainId: () => '0x1', // chain with single call balance address getSelectedAccount: jest.fn().mockReturnValue({ id: 'accountId', address: VALID_ADDRESS, } as InternalAccount), - }, - async ({ controller }) => { - controller.store.updateState({ + state: { accounts: { ...mockAccounts }, accountsByChainId: { '0x1': { ...mockAccounts }, }, - }); - + }, + }, + async ({ controller }) => { await controller.updateAccounts('mainnet'); const accounts = { @@ -713,8 +720,7 @@ describe('Account Tracker', () => { }, }; - const newState = controller.store.getState(); - expect(newState).toStrictEqual({ + expect(controller.state).toStrictEqual({ accounts, accountsByChainId: { '0x1': accounts, @@ -731,75 +737,77 @@ describe('Account Tracker', () => { describe('onAccountRemoved', () => { it('should remove an account from state', () => { - withController(({ controller, triggerOnAccountRemoved }) => { - controller.store.updateState({ - accounts: { ...mockAccounts }, - accountsByChainId: { - [currentChainId]: { - ...mockAccounts, - }, - '0x1': { - ...mockAccounts, - }, - '0x2': { - ...mockAccounts, + withController( + { + state: { + accounts: { ...mockAccounts }, + accountsByChainId: { + [currentChainId]: { + ...mockAccounts, + }, + '0x1': { + ...mockAccounts, + }, + '0x2': { + ...mockAccounts, + }, }, }, - }); - - triggerOnAccountRemoved(VALID_ADDRESS); - - const newState = controller.store.getState(); - - const accounts = { - [VALID_ADDRESS_TWO]: mockAccounts[VALID_ADDRESS_TWO], - }; - - expect(newState).toStrictEqual({ - accounts, - accountsByChainId: { - [currentChainId]: accounts, - '0x1': accounts, - '0x2': accounts, - }, - currentBlockGasLimit: '', - currentBlockGasLimitByChainId: {}, - }); - }); + }, + ({ controller, triggerAccountRemoved }) => { + triggerAccountRemoved(VALID_ADDRESS); + + const accounts = { + [VALID_ADDRESS_TWO]: mockAccounts[VALID_ADDRESS_TWO], + }; + + expect(controller.state).toStrictEqual({ + accounts, + accountsByChainId: { + [currentChainId]: accounts, + '0x1': accounts, + '0x2': accounts, + }, + currentBlockGasLimit: '', + currentBlockGasLimitByChainId: {}, + }); + }, + ); }); }); describe('clearAccounts', () => { it('should reset state', () => { - withController(({ controller }) => { - controller.store.updateState({ - accounts: { ...mockAccounts }, - accountsByChainId: { - [currentChainId]: { - ...mockAccounts, - }, - '0x1': { - ...mockAccounts, - }, - '0x2': { - ...mockAccounts, + withController( + { + state: { + accounts: { ...mockAccounts }, + accountsByChainId: { + [currentChainId]: { + ...mockAccounts, + }, + '0x1': { + ...mockAccounts, + }, + '0x2': { + ...mockAccounts, + }, }, }, - }); - - controller.clearAccounts(); - - const newState = controller.store.getState(); + }, + ({ controller }) => { + controller.clearAccounts(); - expect(newState).toStrictEqual({ - accounts: {}, - accountsByChainId: { - [currentChainId]: {}, - }, - currentBlockGasLimit: '', - currentBlockGasLimitByChainId: {}, - }); - }); + expect(controller.state).toStrictEqual({ + accounts: {}, + accountsByChainId: { + [currentChainId]: {}, + }, + currentBlockGasLimit: '', + currentBlockGasLimitByChainId: {}, + }); + }, + ); }); }); }); diff --git a/app/scripts/lib/account-tracker.ts b/app/scripts/controllers/account-tracker-controller.ts similarity index 68% rename from app/scripts/lib/account-tracker.ts rename to app/scripts/controllers/account-tracker-controller.ts index 8ca119ccf83f..e2c78ea3f3f9 100644 --- a/app/scripts/lib/account-tracker.ts +++ b/app/scripts/controllers/account-tracker-controller.ts @@ -10,7 +10,6 @@ import EthQuery from '@metamask/eth-query'; import { v4 as random } from 'uuid'; -import { ObservableStore } from '@metamask/obs-store'; import log from 'loglevel'; import pify from 'pify'; import { Web3Provider } from '@ethersproject/providers'; @@ -22,10 +21,16 @@ import { NetworkClientConfiguration, NetworkClientId, NetworkControllerGetNetworkClientByIdAction, + NetworkControllerGetStateAction, Provider, } from '@metamask/network-controller'; import { hasProperty, Hex } from '@metamask/utils'; -import { ControllerMessenger } from '@metamask/base-controller'; +import { + BaseController, + ControllerGetStateAction, + ControllerStateChangeEvent, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; import { AccountsControllerGetSelectedAccountAction, AccountsControllerSelectedEvmAccountChangeEvent, @@ -33,51 +38,139 @@ import { import { KeyringControllerAccountRemovedEvent } from '@metamask/keyring-controller'; import { InternalAccount } from '@metamask/keyring-api'; -import OnboardingController, { - OnboardingControllerStateChangeEvent, -} from '../controllers/onboarding'; -import PreferencesController from '../controllers/preferences-controller'; import { LOCALHOST_RPC_URL } from '../../../shared/constants/network'; import { SINGLE_CALL_BALANCES_ADDRESSES } from '../constants/contracts'; -import { previousValueComparator } from './util'; +import { previousValueComparator } from '../lib/util'; +import type { + OnboardingControllerGetStateAction, + OnboardingControllerStateChangeEvent, +} from './onboarding'; +import PreferencesController from './preferences-controller'; + +// Unique name for the controller +const controllerName = 'AccountTrackerController'; type Account = { address: string; balance: string | null; }; -export type AccountTrackerState = { +/** + * The state of the {@link AccountTrackerController} + * + * @property accounts - The accounts currently stored in this AccountTrackerController + * @property accountsByChainId - The accounts currently stored in this AccountTrackerController keyed by chain id + * @property currentBlockGasLimit - A hex string indicating the gas limit of the current block + * @property currentBlockGasLimitByChainId - A hex string indicating the gas limit of the current block keyed by chain id + */ +export type AccountTrackerControllerState = { accounts: Record>; currentBlockGasLimit: string; - accountsByChainId: Record; + accountsByChainId: Record; currentBlockGasLimitByChainId: Record; }; -export const getDefaultAccountTrackerState = (): AccountTrackerState => ({ - accounts: {}, - currentBlockGasLimit: '', - accountsByChainId: {}, - currentBlockGasLimitByChainId: {}, -}); +/** + * {@link AccountTrackerController}'s metadata. + * + * This allows us to choose if fields of the state should be persisted or not + * using the `persist` flag; and if they can be sent to Sentry or not, using + * the `anonymous` flag. + */ +const controllerMetadata = { + accounts: { + persist: true, + anonymous: false, + }, + currentBlockGasLimit: { + persist: true, + anonymous: true, + }, + accountsByChainId: { + persist: true, + anonymous: false, + }, + currentBlockGasLimitByChainId: { + persist: true, + anonymous: true, + }, +}; + +/** + * Function to get default state of the {@link AccountTrackerController}. + */ +export const getDefaultAccountTrackerControllerState = + (): AccountTrackerControllerState => ({ + accounts: {}, + currentBlockGasLimit: '', + accountsByChainId: {}, + currentBlockGasLimitByChainId: {}, + }); + +/** + * Returns the state of the {@link AccountTrackerController}. + */ +export type AccountTrackerControllerGetStateAction = ControllerGetStateAction< + typeof controllerName, + AccountTrackerControllerState +>; +/** + * Actions exposed by the {@link AccountTrackerController}. + */ +export type AccountTrackerControllerActions = + AccountTrackerControllerGetStateAction; + +/** + * Event emitted when the state of the {@link AccountTrackerController} changes. + */ +export type AccountTrackerControllerStateChangeEvent = + ControllerStateChangeEvent< + typeof controllerName, + AccountTrackerControllerState + >; + +/** + * Events emitted by {@link AccountTrackerController}. + */ +export type AccountTrackerControllerEvents = + AccountTrackerControllerStateChangeEvent; + +/** + * Actions that this controller is allowed to call. + */ export type AllowedActions = + | OnboardingControllerGetStateAction | AccountsControllerGetSelectedAccountAction + | NetworkControllerGetStateAction | NetworkControllerGetNetworkClientByIdAction; +/** + * Events that this controller is allowed to subscribe. + */ export type AllowedEvents = | AccountsControllerSelectedEvmAccountChangeEvent | KeyringControllerAccountRemovedEvent | OnboardingControllerStateChangeEvent; -export type AccountTrackerOptions = { - initState: Partial; +/** + * Messenger type for the {@link AccountTrackerController}. + */ +export type AccountTrackerControllerMessenger = RestrictedControllerMessenger< + typeof controllerName, + AccountTrackerControllerActions | AllowedActions, + AccountTrackerControllerEvents | AllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] +>; + +export type AccountTrackerControllerOptions = { + state: Partial; + messenger: AccountTrackerControllerMessenger; provider: Provider; blockTracker: BlockTracker; - getCurrentChainId: () => Hex; getNetworkIdentifier: (config?: NetworkClientConfiguration) => string; preferencesController: PreferencesController; - onboardingController: OnboardingController; - controllerMessenger: ControllerMessenger; }; /** @@ -86,22 +179,12 @@ export type AccountTrackerOptions = { * * It also tracks transaction hashes, and checks their inclusion status on each new block. * - * AccountTracker - * - * @property store The stored object containing all accounts to track, as well as the current block's gas limit. - * @property store.accounts The accounts currently stored in this AccountTracker - * @property store.accountsByChainId The accounts currently stored in this AccountTracker keyed by chain id - * @property store.currentBlockGasLimit A hex string indicating the gas limit of the current block - * @property store.currentBlockGasLimitByChainId A hex string indicating the gas limit of the current block keyed by chain id */ -export default class AccountTracker { - /** - * Observable store containing controller data. - */ - store: ObservableStore; - - resetState: () => void; - +export default class AccountTrackerController extends BaseController< + typeof controllerName, + AccountTrackerControllerState, + AccountTrackerControllerMessenger +> { #pollingTokenSets = new Map>(); #listeners: Record Promise> = @@ -113,52 +196,48 @@ export default class AccountTracker { #currentBlockNumberByChainId: Record = {}; - #getCurrentChainId: AccountTrackerOptions['getCurrentChainId']; - - #getNetworkIdentifier: AccountTrackerOptions['getNetworkIdentifier']; + #getNetworkIdentifier: AccountTrackerControllerOptions['getNetworkIdentifier']; - #preferencesController: AccountTrackerOptions['preferencesController']; - - #onboardingController: AccountTrackerOptions['onboardingController']; - - #controllerMessenger: AccountTrackerOptions['controllerMessenger']; + #preferencesController: AccountTrackerControllerOptions['preferencesController']; #selectedAccount: InternalAccount; /** - * @param opts - Options for initializing the controller - * @param opts.provider - An EIP-1193 provider instance that uses the current global network - * @param opts.blockTracker - A block tracker, which emits events for each new block - * @param opts.getCurrentChainId - A function that returns the `chainId` for the current global network - * @param opts.getNetworkIdentifier - A function that returns the current network or passed nework configuration + * @param options - Options for initializing the controller + * @param options.state - Initial controller state. + * @param options.messenger - Messenger used to communicate with BaseV2 controller. + * @param options.provider - An EIP-1193 provider instance that uses the current global network + * @param options.blockTracker - A block tracker, which emits events for each new block + * @param options.getNetworkIdentifier - A function that returns the current network or passed network configuration + * @param options.preferencesController - The preferences controller */ - constructor(opts: AccountTrackerOptions) { - const initState = getDefaultAccountTrackerState(); - this.store = new ObservableStore({ - ...initState, - ...opts.initState, + constructor(options: AccountTrackerControllerOptions) { + super({ + name: controllerName, + metadata: controllerMetadata, + state: { + ...getDefaultAccountTrackerControllerState(), + ...options.state, + }, + messenger: options.messenger, }); - this.resetState = () => { - this.store.updateState(initState); - }; + this.#provider = options.provider; + this.#blockTracker = options.blockTracker; - this.#provider = opts.provider; - this.#blockTracker = opts.blockTracker; - - this.#getCurrentChainId = opts.getCurrentChainId; - this.#getNetworkIdentifier = opts.getNetworkIdentifier; - this.#preferencesController = opts.preferencesController; - this.#onboardingController = opts.onboardingController; - this.#controllerMessenger = opts.controllerMessenger; + this.#getNetworkIdentifier = options.getNetworkIdentifier; + this.#preferencesController = options.preferencesController; // subscribe to account removal - this.#controllerMessenger.subscribe( + this.messagingSystem.subscribe( 'KeyringController:accountRemoved', (address) => this.removeAccounts([address]), ); - this.#controllerMessenger.subscribe( + const onboardingState = this.messagingSystem.call( + 'OnboardingController:getState', + ); + this.messagingSystem.subscribe( 'OnboardingController:stateChange', previousValueComparator((prevState, currState) => { const { completedOnboarding: prevCompletedOnboarding } = prevState; @@ -167,14 +246,14 @@ export default class AccountTracker { this.updateAccountsAllActiveNetworks(); } return true; - }, this.#onboardingController.state), + }, onboardingState), ); - this.#selectedAccount = this.#controllerMessenger.call( + this.#selectedAccount = this.messagingSystem.call( 'AccountsController:getSelectedAccount', ); - this.#controllerMessenger.subscribe( + this.messagingSystem.subscribe( 'AccountsController:selectedEvmAccountChange', (newAccount) => { const { useMultiAccountBalanceChecker } = @@ -191,6 +270,21 @@ export default class AccountTracker { ); } + resetState(): void { + const { + accounts, + accountsByChainId, + currentBlockGasLimit, + currentBlockGasLimitByChainId, + } = getDefaultAccountTrackerControllerState(); + this.update((state) => { + state.accounts = accounts; + state.accountsByChainId = accountsByChainId; + state.currentBlockGasLimit = currentBlockGasLimit; + state.currentBlockGasLimitByChainId = currentBlockGasLimitByChainId; + }); + } + /** * Starts polling with global selected network */ @@ -220,6 +314,22 @@ export default class AccountTracker { this.#blockTracker.removeListener('latest', this.#updateForBlock); } + /** + * Gets the current chain ID. + */ + #getCurrentChainId(): Hex { + const { selectedNetworkClientId } = this.messagingSystem.call( + 'NetworkController:getState', + ); + const { + configuration: { chainId }, + } = this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + selectedNetworkClientId, + ); + return chainId; + } + /** * Resolves a networkClientId to a network client config * or globally selected network config if not provided @@ -235,7 +345,7 @@ export default class AccountTracker { } { if (networkClientId) { const { configuration, provider, blockTracker } = - this.#controllerMessenger.call( + this.messagingSystem.call( 'NetworkController:getNetworkClientById', networkClientId, ); @@ -355,13 +465,15 @@ export default class AccountTracker { * * @param chainId - The chain ID */ - #getAccountsForChainId(chainId: Hex): AccountTrackerState['accounts'] { - const { accounts, accountsByChainId } = this.store.getState(); + #getAccountsForChainId( + chainId: Hex, + ): AccountTrackerControllerState['accounts'] { + const { accounts, accountsByChainId } = this.state; if (accountsByChainId[chainId]) { return cloneDeep(accountsByChainId[chainId]); } - const newAccounts: AccountTrackerState['accounts'] = {}; + const newAccounts: AccountTrackerControllerState['accounts'] = {}; Object.keys(accounts).forEach((address) => { newAccounts[address] = {}; }); @@ -370,16 +482,16 @@ export default class AccountTracker { /** * Ensures that the locally stored accounts are in sync with a set of accounts stored externally to this - * AccountTracker. + * AccountTrackerController. * - * Once this AccountTracker's accounts are up to date with those referenced by the passed addresses, each + * Once this AccountTrackerController accounts are up to date with those referenced by the passed addresses, each * of these accounts are given an updated balance via EthQuery. * - * @param addresses - The array of hex addresses for accounts with which this AccountTracker's accounts should be + * @param addresses - The array of hex addresses for accounts with which this AccountTrackerController accounts should be * in sync */ syncWithAddresses(addresses: string[]): void { - const { accounts } = this.store.getState(); + const { accounts } = this.state; const locals = Object.keys(accounts); const accountsToAdd: string[] = []; @@ -408,7 +520,7 @@ export default class AccountTracker { */ addAccounts(addresses: string[]): void { const { accounts: _accounts, accountsByChainId: _accountsByChainId } = - this.store.getState(); + this.state; const accounts = cloneDeep(_accounts); const accountsByChainId = cloneDeep(_accountsByChainId); @@ -422,7 +534,10 @@ export default class AccountTracker { }); }); // save accounts state - this.store.updateState({ accounts, accountsByChainId }); + this.update((state) => { + state.accounts = accounts; + state.accountsByChainId = accountsByChainId; + }); // fetch balances for the accounts if there is block number ready if (this.#currentBlockNumberByChainId[this.#getCurrentChainId()]) { @@ -443,7 +558,7 @@ export default class AccountTracker { */ removeAccounts(addresses: string[]): void { const { accounts: _accounts, accountsByChainId: _accountsByChainId } = - this.store.getState(); + this.state; const accounts = cloneDeep(_accounts); const accountsByChainId = cloneDeep(_accountsByChainId); @@ -457,23 +572,26 @@ export default class AccountTracker { }); }); // save accounts state - this.store.updateState({ accounts, accountsByChainId }); + this.update((state) => { + state.accounts = accounts; + state.accountsByChainId = accountsByChainId; + }); } /** * Removes all addresses and associated balances */ clearAccounts(): void { - this.store.updateState({ - accounts: {}, - accountsByChainId: { + this.update((state) => { + state.accounts = {}; + state.accountsByChainId = { [this.#getCurrentChainId()]: {}, - }, + }; }); } /** - * Given a block, updates this AccountTracker's currentBlockGasLimit and currentBlockGasLimitByChainId and then updates + * Given a block, updates this AccountTrackerController currentBlockGasLimit and currentBlockGasLimitByChainId and then updates * each local account's balance via EthQuery * * @private @@ -485,7 +603,7 @@ export default class AccountTracker { }; /** - * Given a block, updates this AccountTracker's currentBlockGasLimitByChainId, and then updates each local account's balance + * Given a block, updates this AccountTrackerController currentBlockGasLimitByChainId, and then updates each local account's balance * via EthQuery * * @private @@ -510,15 +628,11 @@ export default class AccountTracker { return; } const currentBlockGasLimit = currentBlock.gasLimit; - const { currentBlockGasLimitByChainId } = this.store.getState(); - this.store.updateState({ - ...(chainId === this.#getCurrentChainId() && { - currentBlockGasLimit, - }), - currentBlockGasLimitByChainId: { - ...currentBlockGasLimitByChainId, - [chainId]: currentBlockGasLimit, - }, + this.update((state) => { + if (chainId === this.#getCurrentChainId()) { + state.currentBlockGasLimit = currentBlockGasLimit; + } + state.currentBlockGasLimitByChainId[chainId] = currentBlockGasLimit; }); try { @@ -549,7 +663,9 @@ export default class AccountTracker { * @param networkClientId - optional network client ID to use instead of the globally selected network. */ async updateAccounts(networkClientId?: NetworkClientId): Promise { - const { completedOnboarding } = this.#onboardingController.state; + const { completedOnboarding } = this.messagingSystem.call( + 'OnboardingController:getState', + ); if (!completedOnboarding) { return; } @@ -561,11 +677,11 @@ export default class AccountTracker { let addresses = []; if (useMultiAccountBalanceChecker) { - const { accounts } = this.store.getState(); + const { accounts } = this.state; addresses = Object.keys(accounts); } else { - const selectedAddress = this.#controllerMessenger.call( + const selectedAddress = this.messagingSystem.call( 'AccountsController:getSelectedAccount', ).address; @@ -573,14 +689,11 @@ export default class AccountTracker { } const rpcUrl = 'http://127.0.0.1:8545'; - const singleCallBalancesAddress = - SINGLE_CALL_BALANCES_ADDRESSES[ - chainId as keyof typeof SINGLE_CALL_BALANCES_ADDRESSES - ]; if ( identifier === LOCALHOST_RPC_URL || identifier === rpcUrl || - !singleCallBalancesAddress + !((id): id is keyof typeof SINGLE_CALL_BALANCES_ADDRESSES => + id in SINGLE_CALL_BALANCES_ADDRESSES)(chainId) ) { await Promise.all( addresses.map((address) => @@ -590,7 +703,7 @@ export default class AccountTracker { } else { await this.#updateAccountsViaBalanceChecker( addresses, - singleCallBalancesAddress, + SINGLE_CALL_BALANCES_ADDRESSES[chainId], provider, chainId, ); @@ -657,15 +770,11 @@ export default class AccountTracker { newAccounts[address] = result; - const { accountsByChainId } = this.store.getState(); - this.store.updateState({ - ...(chainId === this.#getCurrentChainId() && { - accounts: newAccounts, - }), - accountsByChainId: { - ...accountsByChainId, - [chainId]: newAccounts, - }, + this.update((state) => { + if (chainId === this.#getCurrentChainId()) { + state.accounts = newAccounts; + } + state.accountsByChainId[chainId] = newAccounts; }); } @@ -695,7 +804,7 @@ export default class AccountTracker { const balances = await ethContract.balances(addresses, ethBalance); const accounts = this.#getAccountsForChainId(chainId); - const newAccounts: AccountTrackerState['accounts'] = {}; + const newAccounts: AccountTrackerControllerState['accounts'] = {}; Object.keys(accounts).forEach((address) => { if (!addresses.includes(address)) { newAccounts[address] = { address, balance: null }; @@ -706,15 +815,11 @@ export default class AccountTracker { newAccounts[address] = { address, balance }; }); - const { accountsByChainId } = this.store.getState(); - this.store.updateState({ - ...(chainId === this.#getCurrentChainId() && { - accounts: newAccounts, - }), - accountsByChainId: { - ...accountsByChainId, - [chainId]: newAccounts, - }, + this.update((state) => { + if (chainId === this.#getCurrentChainId()) { + state.accounts = newAccounts; + } + state.accountsByChainId[chainId] = newAccounts; }); } catch (error) { log.warn( diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js index 32498a1c8bce..3d8f9d176fb6 100644 --- a/app/scripts/controllers/app-state.js +++ b/app/scripts/controllers/app-state.js @@ -57,6 +57,7 @@ export default class AppStateController extends EventEmitter { trezorModel: null, currentPopupId: undefined, onboardingDate: null, + lastViewedUserSurvey: null, newPrivacyPolicyToastClickedOrClosed: null, newPrivacyPolicyToastShownDate: null, // This key is only used for checking if the user had set advancedGasFee @@ -198,6 +199,12 @@ export default class AppStateController extends EventEmitter { }); } + setLastViewedUserSurvey(id) { + this.store.updateState({ + lastViewedUserSurvey: id, + }); + } + setNewPrivacyPolicyToastClickedOrClosed() { this.store.updateState({ newPrivacyPolicyToastClickedOrClosed: true, diff --git a/app/scripts/controllers/bridge/bridge-controller.test.ts b/app/scripts/controllers/bridge/bridge-controller.test.ts index 9c9036b87f7b..25b6eae98c33 100644 --- a/app/scripts/controllers/bridge/bridge-controller.test.ts +++ b/app/scripts/controllers/bridge/bridge-controller.test.ts @@ -1,6 +1,7 @@ import nock from 'nock'; import { BRIDGE_API_BASE_URL } from '../../../../shared/constants/bridge'; import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { SWAPS_API_V2_BASE_URL } from '../../../../shared/constants/swaps'; import BridgeController from './bridge-controller'; import { BridgeControllerMessenger } from './types'; import { DEFAULT_BRIDGE_CONTROLLER_STATE } from './constants'; @@ -32,6 +33,28 @@ describe('BridgeController', function () { 'src-network-allowlist': [10, 534352], 'dest-network-allowlist': [137, 42161], }); + nock(BRIDGE_API_BASE_URL) + .get('/getTokens?chainId=10') + .reply(200, [ + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + decimals: 16, + }, + { + address: '0x1291478912', + symbol: 'DEF', + decimals: 16, + }, + ]); + nock(SWAPS_API_V2_BASE_URL) + .get('/networks/10/topAssets') + .reply(200, [ + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + }, + ]); }); it('constructor should setup correctly', function () { @@ -51,4 +74,49 @@ describe('BridgeController', function () { expectedFeatureFlagsResponse, ); }); + + it('selectDestNetwork should set the bridge dest tokens and top assets', async function () { + await bridgeController.selectDestNetwork('0xa'); + expect(bridgeController.state.bridgeState.destTokens).toStrictEqual({ + '0x0000000000000000000000000000000000000000': { + address: '0x0000000000000000000000000000000000000000', + decimals: 18, + iconUrl: './images/eth_logo.svg', + name: 'Ether', + symbol: 'ETH', + }, + '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + decimals: 16, + }, + }); + expect(bridgeController.state.bridgeState.destTopAssets).toStrictEqual([ + { address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', symbol: 'ABC' }, + ]); + }); + + it('selectSrcNetwork should set the bridge src tokens and top assets', async function () { + await bridgeController.selectSrcNetwork('0xa'); + expect(bridgeController.state.bridgeState.srcTokens).toStrictEqual({ + '0x0000000000000000000000000000000000000000': { + address: '0x0000000000000000000000000000000000000000', + decimals: 18, + iconUrl: './images/eth_logo.svg', + name: 'Ether', + symbol: 'ETH', + }, + '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + decimals: 16, + }, + }); + expect(bridgeController.state.bridgeState.srcTopAssets).toStrictEqual([ + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + }, + ]); + }); }); diff --git a/app/scripts/controllers/bridge/bridge-controller.ts b/app/scripts/controllers/bridge/bridge-controller.ts index 6ca076c2e060..841d735ac52c 100644 --- a/app/scripts/controllers/bridge/bridge-controller.ts +++ b/app/scripts/controllers/bridge/bridge-controller.ts @@ -1,7 +1,14 @@ import { BaseController, StateMetadata } from '@metamask/base-controller'; +import { Hex } from '@metamask/utils'; +import { + fetchBridgeFeatureFlags, + fetchBridgeTokens, + // TODO: Remove restricted import + // eslint-disable-next-line import/no-restricted-paths +} from '../../../../ui/pages/bridge/bridge.util'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths -import { fetchBridgeFeatureFlags } from '../../../../ui/pages/bridge/bridge.util'; +import { fetchTopAssetsList } from '../../../../ui/pages/swaps/swaps.util'; import { BRIDGE_CONTROLLER_NAME, DEFAULT_BRIDGE_CONTROLLER_STATE, @@ -32,6 +39,14 @@ export default class BridgeController extends BaseController< `${BRIDGE_CONTROLLER_NAME}:setBridgeFeatureFlags`, this.setBridgeFeatureFlags.bind(this), ); + this.messagingSystem.registerActionHandler( + `${BRIDGE_CONTROLLER_NAME}:selectSrcNetwork`, + this.selectSrcNetwork.bind(this), + ); + this.messagingSystem.registerActionHandler( + `${BRIDGE_CONTROLLER_NAME}:selectDestNetwork`, + this.selectDestNetwork.bind(this), + ); } resetState = () => { @@ -49,4 +64,33 @@ export default class BridgeController extends BaseController< _state.bridgeState = { ...bridgeState, bridgeFeatureFlags }; }); }; + + selectSrcNetwork = async (chainId: Hex) => { + await this.#setTopAssets(chainId, 'srcTopAssets'); + await this.#setTokens(chainId, 'srcTokens'); + }; + + selectDestNetwork = async (chainId: Hex) => { + await this.#setTopAssets(chainId, 'destTopAssets'); + await this.#setTokens(chainId, 'destTokens'); + }; + + #setTopAssets = async ( + chainId: Hex, + stateKey: 'srcTopAssets' | 'destTopAssets', + ) => { + const { bridgeState } = this.state; + const topAssets = await fetchTopAssetsList(chainId); + this.update((_state) => { + _state.bridgeState = { ...bridgeState, [stateKey]: topAssets }; + }); + }; + + #setTokens = async (chainId: Hex, stateKey: 'srcTokens' | 'destTokens') => { + const { bridgeState } = this.state; + const tokens = await fetchBridgeTokens(chainId); + this.update((_state) => { + _state.bridgeState = { ...bridgeState, [stateKey]: tokens }; + }); + }; } diff --git a/app/scripts/controllers/bridge/constants.ts b/app/scripts/controllers/bridge/constants.ts index f2932120f98d..58c7d015b7bb 100644 --- a/app/scripts/controllers/bridge/constants.ts +++ b/app/scripts/controllers/bridge/constants.ts @@ -8,4 +8,8 @@ export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = { [BridgeFeatureFlagsKey.NETWORK_SRC_ALLOWLIST]: [], [BridgeFeatureFlagsKey.NETWORK_DEST_ALLOWLIST]: [], }, + srcTokens: {}, + srcTopAssets: [], + destTokens: {}, + destTopAssets: [], }; diff --git a/app/scripts/controllers/bridge/types.ts b/app/scripts/controllers/bridge/types.ts index aa92a6597c69..2fb36e1e983e 100644 --- a/app/scripts/controllers/bridge/types.ts +++ b/app/scripts/controllers/bridge/types.ts @@ -3,6 +3,7 @@ import { RestrictedControllerMessenger, } from '@metamask/base-controller'; import { Hex } from '@metamask/utils'; +import { SwapsTokenObject } from '../../../../shared/constants/swaps'; import BridgeController from './bridge-controller'; import { BRIDGE_CONTROLLER_NAME } from './constants'; @@ -20,8 +21,16 @@ export type BridgeFeatureFlags = { export type BridgeControllerState = { bridgeFeatureFlags: BridgeFeatureFlags; + srcTokens: Record; + srcTopAssets: { address: string }[]; + destTokens: Record; + destTopAssets: { address: string }[]; }; +export enum BridgeUserAction { + SELECT_SRC_NETWORK = 'selectSrcNetwork', + SELECT_DEST_NETWORK = 'selectDestNetwork', +} export enum BridgeBackgroundAction { SET_FEATURE_FLAGS = 'setBridgeFeatureFlags', } @@ -33,7 +42,9 @@ type BridgeControllerAction = { // Maps to BridgeController function names type BridgeControllerActions = - BridgeControllerAction; + | BridgeControllerAction + | BridgeControllerAction + | BridgeControllerAction; type BridgeControllerEvents = ControllerStateChangeEvent< typeof BRIDGE_CONTROLLER_NAME, diff --git a/app/scripts/controllers/metametrics.js b/app/scripts/controllers/metametrics.js index bfe7f79d1ac4..ef1dbe02789a 100644 --- a/app/scripts/controllers/metametrics.js +++ b/app/scripts/controllers/metametrics.js @@ -846,8 +846,8 @@ export default class MetaMetricsController { [MetaMetricsUserTrait.Theme]: metamaskState.theme || 'default', [MetaMetricsUserTrait.TokenDetectionEnabled]: metamaskState.useTokenDetection, - [MetaMetricsUserTrait.UseNativeCurrencyAsPrimaryCurrency]: - metamaskState.useNativeCurrencyAsPrimaryCurrency, + [MetaMetricsUserTrait.ShowNativeTokenAsMainBalance]: + metamaskState.showNativeTokenAsMainBalance, [MetaMetricsUserTrait.CurrentCurrency]: metamaskState.currentCurrency, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) [MetaMetricsUserTrait.MmiExtensionId]: this.extension?.runtime?.id, diff --git a/app/scripts/controllers/metametrics.test.js b/app/scripts/controllers/metametrics.test.js index 2113efd1715b..3d4845e056d0 100644 --- a/app/scripts/controllers/metametrics.test.js +++ b/app/scripts/controllers/metametrics.test.js @@ -1088,7 +1088,7 @@ describe('MetaMetricsController', function () { securityAlertsEnabled: true, theme: 'default', useTokenDetection: true, - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, security_providers: [], names: { [NameType.ETHEREUM_ADDRESS]: { @@ -1143,7 +1143,7 @@ describe('MetaMetricsController', function () { [MetaMetricsUserTrait.ThreeBoxEnabled]: false, [MetaMetricsUserTrait.Theme]: 'default', [MetaMetricsUserTrait.TokenDetectionEnabled]: true, - [MetaMetricsUserTrait.UseNativeCurrencyAsPrimaryCurrency]: true, + [MetaMetricsUserTrait.ShowNativeTokenAsMainBalance]: true, [MetaMetricsUserTrait.SecurityProviders]: ['blockaid'], ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) [MetaMetricsUserTrait.MmiExtensionId]: 'testid', @@ -1181,7 +1181,7 @@ describe('MetaMetricsController', function () { useNftDetection: false, theme: 'default', useTokenDetection: true, - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, }); const updatedTraits = metaMetricsController._buildUserTraitsObject({ @@ -1208,7 +1208,7 @@ describe('MetaMetricsController', function () { useNftDetection: false, theme: 'default', useTokenDetection: true, - useNativeCurrencyAsPrimaryCurrency: false, + showNativeTokenAsMainBalance: false, }); expect(updatedTraits).toStrictEqual({ @@ -1216,7 +1216,7 @@ describe('MetaMetricsController', function () { [MetaMetricsUserTrait.NumberOfAccounts]: 3, [MetaMetricsUserTrait.NumberOfTokens]: 1, [MetaMetricsUserTrait.OpenseaApiEnabled]: false, - [MetaMetricsUserTrait.UseNativeCurrencyAsPrimaryCurrency]: false, + [MetaMetricsUserTrait.ShowNativeTokenAsMainBalance]: false, }); }); @@ -1245,7 +1245,7 @@ describe('MetaMetricsController', function () { useNftDetection: true, theme: 'default', useTokenDetection: true, - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, }); const updatedTraits = metaMetricsController._buildUserTraitsObject({ @@ -1267,7 +1267,7 @@ describe('MetaMetricsController', function () { useNftDetection: true, theme: 'default', useTokenDetection: true, - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, }); expect(updatedTraits).toStrictEqual(null); }); diff --git a/app/scripts/controllers/mmi-controller.test.ts b/app/scripts/controllers/mmi-controller.test.ts index 3a9e6cddba6a..348ccd40916b 100644 --- a/app/scripts/controllers/mmi-controller.test.ts +++ b/app/scripts/controllers/mmi-controller.test.ts @@ -99,7 +99,7 @@ describe('MMIController', function () { 'NetworkController:infuraIsUnblocked', ], }), - state: mockNetworkState({chainId: CHAIN_IDS.SEPOLIA}), + state: mockNetworkState({ chainId: CHAIN_IDS.SEPOLIA }), infuraProjectId: 'mock-infura-project-id', }); @@ -272,7 +272,7 @@ describe('MMIController', function () { mmiController.getState = jest.fn(); mmiController.captureException = jest.fn(); - mmiController.accountTracker = { syncWithAddresses: jest.fn() }; + mmiController.accountTrackerController = { syncWithAddresses: jest.fn() }; jest.spyOn(metaMetricsController.store, 'getState').mockReturnValue({ metaMetricsId: mockMetaMetricsId, @@ -385,7 +385,7 @@ describe('MMIController', function () { mmiController.keyringController.addNewAccountForKeyring = jest.fn(); mmiController.custodyController.setAccountDetails = jest.fn(); - mmiController.accountTracker.syncWithAddresses = jest.fn(); + mmiController.accountTrackerController.syncWithAddresses = jest.fn(); mmiController.storeCustodianSupportedChains = jest.fn(); mmiController.custodyController.storeCustodyStatusMap = jest.fn(); @@ -400,7 +400,9 @@ describe('MMIController', function () { expect( mmiController.custodyController.setAccountDetails, ).toHaveBeenCalled(); - expect(mmiController.accountTracker.syncWithAddresses).toHaveBeenCalled(); + expect( + mmiController.accountTrackerController.syncWithAddresses, + ).toHaveBeenCalled(); expect(mmiController.storeCustodianSupportedChains).toHaveBeenCalled(); expect( mmiController.custodyController.storeCustodyStatusMap, diff --git a/app/scripts/controllers/mmi-controller.ts b/app/scripts/controllers/mmi-controller.ts index 0c43684d7f58..d0e905d673d8 100644 --- a/app/scripts/controllers/mmi-controller.ts +++ b/app/scripts/controllers/mmi-controller.ts @@ -39,12 +39,12 @@ import { Signature, ConnectionRequest, } from '../../../shared/constants/mmi-controller'; -import AccountTracker from '../lib/account-tracker'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { getCurrentChainId } from '../../../ui/selectors'; import MetaMetricsController from './metametrics'; import { getPermissionBackgroundApiMethods } from './permissions'; +import AccountTrackerController from './account-tracker-controller'; import PreferencesController from './preferences-controller'; import { AppStateController } from './app-state'; @@ -86,7 +86,7 @@ export default class MMIController extends EventEmitter { // eslint-disable-next-line @typescript-eslint/no-explicit-any private getPendingNonce: (address: string) => Promise; - private accountTracker: AccountTracker; + private accountTrackerController: AccountTrackerController; private metaMetricsController: MetaMetricsController; @@ -148,7 +148,7 @@ export default class MMIController extends EventEmitter { this.custodyController = opts.custodyController; this.getState = opts.getState; this.getPendingNonce = opts.getPendingNonce; - this.accountTracker = opts.accountTracker; + this.accountTrackerController = opts.accountTrackerController; this.metaMetricsController = opts.metaMetricsController; this.networkController = opts.networkController; this.permissionController = opts.permissionController; @@ -504,7 +504,7 @@ export default class MMIController extends EventEmitter { } }); - this.accountTracker.syncWithAddresses(accountsToTrack); + this.accountTrackerController.syncWithAddresses(accountsToTrack); for (const address of newAccounts) { try { diff --git a/app/scripts/controllers/preferences-controller.ts b/app/scripts/controllers/preferences-controller.ts index ab6c3e959215..a158ac0024d4 100644 --- a/app/scripts/controllers/preferences-controller.ts +++ b/app/scripts/controllers/preferences-controller.ts @@ -96,6 +96,7 @@ export type Preferences = { showFiatInTestnets: boolean; showTestNetworks: boolean; smartTransactionsOptInStatus: boolean | null; + showNativeTokenAsMainBalance: boolean; useNativeCurrencyAsPrimaryCurrency: boolean; hideZeroBalanceTokens: boolean; petnamesEnabled: boolean; @@ -105,6 +106,7 @@ export type Preferences = { showMultiRpcModal: boolean; isRedesignedConfirmationsDeveloperEnabled: boolean; showConfirmationAdvancedDetails: boolean; + shouldShowAggregatedBalancePopover: boolean; }; export type PreferencesControllerState = { @@ -122,7 +124,9 @@ export type PreferencesControllerState = { useRequestQueue: boolean; openSeaEnabled: boolean; securityAlertsEnabled: boolean; + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) watchEthereumAccountEnabled: boolean; + ///: END:ONLY_INCLUDE_IF bitcoinSupportEnabled: boolean; bitcoinTestnetSupportEnabled: boolean; addSnapAccountEnabled: boolean; @@ -223,6 +227,7 @@ export default class PreferencesController { showFiatInTestnets: false, showTestNetworks: false, smartTransactionsOptInStatus: null, // null means we will show the Smart Transactions opt-in modal to a user if they are eligible + showNativeTokenAsMainBalance: false, useNativeCurrencyAsPrimaryCurrency: true, hideZeroBalanceTokens: false, petnamesEnabled: true, @@ -232,6 +237,7 @@ export default class PreferencesController { showMultiRpcModal: false, isRedesignedConfirmationsDeveloperEnabled: false, showConfirmationAdvancedDetails: false, + shouldShowAggregatedBalancePopover: true, // by default user should see popover; }, // ENS decentralized website resolution ipfsGateway: IPFS_DEFAULT_GATEWAY_URL, @@ -425,6 +431,7 @@ export default class PreferencesController { } ///: END:ONLY_INCLUDE_IF + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) /** * Setter for the `watchEthereumAccountEnabled` property. * @@ -436,6 +443,7 @@ export default class PreferencesController { watchEthereumAccountEnabled, }); } + ///: END:ONLY_INCLUDE_IF /** * Setter for the `bitcoinSupportEnabled` property. diff --git a/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.js b/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.js deleted file mode 100644 index 70dbb7b16cfa..000000000000 --- a/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.js +++ /dev/null @@ -1,49 +0,0 @@ -import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; - -/** - * This RPC method gets background state relevant to the provider. - * The background sends RPC notifications on state changes, but the provider - * first requests state on initialization. - */ - -const getProviderState = { - methodNames: [MESSAGE_TYPE.GET_PROVIDER_STATE], - implementation: getProviderStateHandler, - hookNames: { - getProviderState: true, - }, -}; -export default getProviderState; - -/** - * @typedef {object} ProviderStateHandlerResult - * @property {string} chainId - The current chain ID. - * @property {boolean} isUnlocked - Whether the extension is unlocked or not. - * @property {string} networkVersion - The current network ID. - */ - -/** - * @typedef {object} ProviderStateHandlerOptions - * @property {() => ProviderStateHandlerResult} getProviderState - A function that - * gets the current provider state. - */ - -/** - * @param {import('json-rpc-engine').JsonRpcRequest<[]>} req - The JSON-RPC request object. - * @param {import('json-rpc-engine').JsonRpcResponse} res - The JSON-RPC response object. - * @param {Function} _next - The json-rpc-engine 'next' callback. - * @param {Function} end - The json-rpc-engine 'end' callback. - * @param {ProviderStateHandlerOptions} options - */ -async function getProviderStateHandler( - req, - res, - _next, - end, - { getProviderState: _getProviderState }, -) { - res.result = { - ...(await _getProviderState(req.origin)), - }; - return end(); -} diff --git a/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.test.ts b/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.test.ts new file mode 100644 index 000000000000..f3d76a09f5fc --- /dev/null +++ b/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.test.ts @@ -0,0 +1,56 @@ +import { PendingJsonRpcResponse } from '@metamask/utils'; +import { JsonRpcEngineEndCallback } from 'json-rpc-engine'; +import getProviderState, { + GetProviderState, + ProviderStateHandlerResult, +} from './get-provider-state'; +import { HandlerRequestType } from './types'; + +describe('getProviderState', () => { + let mockEnd: JsonRpcEngineEndCallback; + let mockGetProviderState: GetProviderState; + + beforeEach(() => { + mockEnd = jest.fn(); + mockGetProviderState = jest.fn().mockResolvedValue({ + chainId: '0x539', + isUnlocked: true, + networkVersion: '', + accounts: [], + }); + }); + + it('should call getProviderState when the handler is invoked', async () => { + const req: HandlerRequestType = { + origin: 'testOrigin', + params: [], + id: '22', + jsonrpc: '2.0', + method: 'metamask_getProviderState', + }; + + const res: PendingJsonRpcResponse = { + id: '22', + jsonrpc: '2.0', + result: { + chainId: '0x539', + isUnlocked: true, + networkVersion: '', + accounts: [], + }, + }; + + await getProviderState.implementation(req, res, jest.fn(), mockEnd, { + getProviderState: mockGetProviderState, + }); + + expect(mockGetProviderState).toHaveBeenCalledWith(req.origin); + expect(res.result).toStrictEqual({ + chainId: '0x539', + isUnlocked: true, + networkVersion: '', + accounts: [], + }); + expect(mockEnd).toHaveBeenCalled(); + }); +}); diff --git a/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.ts b/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.ts new file mode 100644 index 000000000000..530b48b25164 --- /dev/null +++ b/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.ts @@ -0,0 +1,81 @@ +import type { + JsonRpcEngineNextCallback, + JsonRpcEngineEndCallback, +} from 'json-rpc-engine'; +import type { + PendingJsonRpcResponse, + JsonRpcParams, + Hex, +} from '@metamask/utils'; +import { OriginString } from '@metamask/permission-controller'; +import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; +import { + HandlerWrapper, + HandlerRequestType as ProviderStateHandlerRequest, +} from './types'; + +/** + * @property chainId - The current chain ID. + * @property isUnlocked - Whether the extension is unlocked or not. + * @property networkVersion - The current network ID. + * @property accounts - List of permitted accounts for the specified origin. + */ +export type ProviderStateHandlerResult = { + chainId: Hex; + isUnlocked: boolean; + networkVersion: string; + accounts: string[]; +}; + +export type GetProviderState = ( + origin: OriginString, +) => Promise; + +type GetProviderStateConstraint = + { + implementation: ( + _req: ProviderStateHandlerRequest, + res: PendingJsonRpcResponse, + _next: JsonRpcEngineNextCallback, + end: JsonRpcEngineEndCallback, + { _getProviderState }: Record, + ) => Promise; + } & HandlerWrapper; + +/** + * This RPC method gets background state relevant to the provider. + * The background sends RPC notifications on state changes, but the provider + * first requests state on initialization. + */ +const getProviderState = { + methodNames: [MESSAGE_TYPE.GET_PROVIDER_STATE], + implementation: getProviderStateHandler, + hookNames: { + getProviderState: true, + }, +} satisfies GetProviderStateConstraint; + +export default getProviderState; + +/** + * @param req - The JSON-RPC request object. + * @param res - The JSON-RPC response object. + * @param _next - The json-rpc-engine 'next' callback. + * @param end - The json-rpc-engine 'end' callback. + * @param options + * @param options.getProviderState - An async function that gets the current provider state. + */ +async function getProviderStateHandler< + Params extends JsonRpcParams = JsonRpcParams, +>( + req: ProviderStateHandlerRequest, + res: PendingJsonRpcResponse, + _next: JsonRpcEngineNextCallback, + end: JsonRpcEngineEndCallback, + { getProviderState: _getProviderState }: Record, +): Promise { + res.result = { + ...(await _getProviderState(req.origin)), + }; + return end(); +} diff --git a/app/scripts/lib/rpc-method-middleware/handlers/types.ts b/app/scripts/lib/rpc-method-middleware/handlers/types.ts index 5b7a2a7494d4..46ceef442ec2 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/types.ts +++ b/app/scripts/lib/rpc-method-middleware/handlers/types.ts @@ -1,6 +1,13 @@ +import { OriginString } from '@metamask/permission-controller'; +import { JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; import { MessageType } from '../../../../../shared/constants/app'; export type HandlerWrapper = { methodNames: [MessageType] | MessageType[]; hookNames: Record; }; + +export type HandlerRequestType = + Required> & { + origin: OriginString; + }; diff --git a/app/scripts/lib/snap-keyring/account-watcher-snap.ts b/app/scripts/lib/snap-keyring/account-watcher-snap.ts index 3775dcd28405..48ace4e595af 100644 --- a/app/scripts/lib/snap-keyring/account-watcher-snap.ts +++ b/app/scripts/lib/snap-keyring/account-watcher-snap.ts @@ -1,3 +1,4 @@ +// BEGIN:ONLY_INCLUDE_IF(build-flask) import { SnapId } from '@metamask/snaps-sdk'; import AccountWatcherSnap from '@metamask/account-watcher/dist/preinstalled-snap.json'; @@ -6,3 +7,4 @@ export const ACCOUNT_WATCHER_SNAP_ID: SnapId = export const ACCOUNT_WATCHER_NAME: string = AccountWatcherSnap.manifest.proposedName; +// END:ONLY_INCLUDE_IF diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index c03b8802e22f..31dc5fef3fcd 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -274,7 +274,7 @@ import MMIController from './controllers/mmi-controller'; import { mmiKeyringBuilderFactory } from './mmi-keyring-builder-factory'; ///: END:ONLY_INCLUDE_IF import ComposableObservableStore from './lib/ComposableObservableStore'; -import AccountTracker from './lib/account-tracker'; +import AccountTrackerController from './controllers/account-tracker-controller'; import createDupeReqFilterStream from './lib/createDupeReqFilterStream'; import createLoggerMiddleware from './lib/createLoggerMiddleware'; import { @@ -347,7 +347,10 @@ import { updateSecurityAlertResponse } from './lib/ppom/ppom-util'; import createEvmMethodsToNonEvmAccountReqFilterMiddleware from './lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware'; import { isEthAddress } from './lib/multichain/address'; import { decodeTransactionData } from './lib/transaction/decode/util'; -import { BridgeBackgroundAction } from './controllers/bridge/types'; +import { + BridgeUserAction, + BridgeBackgroundAction, +} from './controllers/bridge/types'; import BridgeController from './controllers/bridge/bridge-controller'; import { BRIDGE_CONTROLLER_NAME } from './controllers/bridge/constants'; import { @@ -1222,7 +1225,7 @@ export default class MetamaskController extends EventEmitter { const internalAccountCount = internalAccounts.length; const accountTrackerCount = Object.keys( - this.accountTracker.store.getState().accounts || {}, + this.accountTrackerController.state.accounts || {}, ).length; captureException( @@ -1655,11 +1658,24 @@ export default class MetamaskController extends EventEmitter { }); // account tracker watches balances, nonces, and any code at their address - this.accountTracker = new AccountTracker({ + this.accountTrackerController = new AccountTrackerController({ + state: { accounts: {} }, + messenger: this.controllerMessenger.getRestricted({ + name: 'AccountTrackerController', + allowedActions: [ + 'AccountsController:getSelectedAccount', + 'NetworkController:getState', + 'NetworkController:getNetworkClientById', + 'OnboardingController:getState', + ], + allowedEvents: [ + 'AccountsController:selectedEvmAccountChange', + 'OnboardingController:stateChange', + 'KeyringController:accountRemoved', + ], + }), provider: this.provider, blockTracker: this.blockTracker, - getCurrentChainId: () => - getCurrentChainId({ metamask: this.networkController.state }), getNetworkIdentifier: (providerConfig) => { const { type, rpcUrl } = providerConfig ?? @@ -1669,17 +1685,6 @@ export default class MetamaskController extends EventEmitter { return type === NETWORK_TYPES.RPC ? rpcUrl : type; }, preferencesController: this.preferencesController, - onboardingController: this.onboardingController, - controllerMessenger: this.controllerMessenger.getRestricted({ - name: 'AccountTracker', - allowedActions: ['AccountsController:getSelectedAccount'], - allowedEvents: [ - 'AccountsController:selectedEvmAccountChange', - 'OnboardingController:stateChange', - 'KeyringController:accountRemoved', - ], - }), - initState: { accounts: {} }, }); // start and stop polling for balances based on activeControllerConnections @@ -1849,6 +1854,10 @@ export default class MetamaskController extends EventEmitter { getCurrentChainId({ metamask: this.networkController.state }) ], incomingTransactions: { + etherscanApiKeysByChainId: { + [CHAIN_IDS.MAINNET]: process.env.ETHERSCAN_API_KEY, + [CHAIN_IDS.SEPOLIA]: process.env.ETHERSCAN_API_KEY, + }, includeTokenTransfers: false, isEnabled: () => Boolean( @@ -1998,7 +2007,7 @@ export default class MetamaskController extends EventEmitter { custodyController: this.custodyController, getState: this.getState.bind(this), getPendingNonce: this.getPendingNonce.bind(this), - accountTracker: this.accountTracker, + accountTrackerController: this.accountTrackerController, metaMetricsController: this.metaMetricsController, networkController: this.networkController, permissionController: this.permissionController, @@ -2207,11 +2216,11 @@ export default class MetamaskController extends EventEmitter { this._onUserOperationTransactionUpdated.bind(this), ); - // ensure accountTracker updates balances after network change + // ensure AccountTrackerController updates balances after network change networkControllerMessenger.subscribe( 'NetworkController:networkDidChange', () => { - this.accountTracker.updateAccounts(); + this.accountTrackerController.updateAccounts(); }, ); @@ -2323,7 +2332,7 @@ export default class MetamaskController extends EventEmitter { * On chrome profile re-start, they will be re-initialized. */ const resetOnRestartStore = { - AccountTracker: this.accountTracker.store, + AccountTracker: this.accountTrackerController, TokenRatesController: this.tokenRatesController, DecryptMessageController: this.decryptMessageController, EncryptionPublicKeyController: this.encryptionPublicKeyController, @@ -2448,7 +2457,9 @@ export default class MetamaskController extends EventEmitter { // if this is the first time, clear the state of by calling these methods const resetMethods = [ - this.accountTracker.resetState, + this.accountTrackerController.resetState.bind( + this.accountTrackerController, + ), this.decryptMessageController.resetState.bind( this.decryptMessageController, ), @@ -2548,7 +2559,7 @@ export default class MetamaskController extends EventEmitter { } triggerNetworkrequests() { - this.accountTracker.start(); + this.accountTrackerController.start(); this.txController.startIncomingTransactionPolling(); this.tokenDetectionController.enable(); @@ -2567,7 +2578,7 @@ export default class MetamaskController extends EventEmitter { } stopNetworkRequests() { - this.accountTracker.stop(); + this.accountTrackerController.stop(); this.txController.stopIncomingTransactionPolling(); this.tokenDetectionController.disable(); @@ -3275,10 +3286,12 @@ export default class MetamaskController extends EventEmitter { preferencesController, ), ///: END:ONLY_INCLUDE_IF + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) setWatchEthereumAccountEnabled: preferencesController.setWatchEthereumAccountEnabled.bind( preferencesController, ), + ///: END:ONLY_INCLUDE_IF setBitcoinSupportEnabled: preferencesController.setBitcoinSupportEnabled.bind( preferencesController, @@ -3526,6 +3539,8 @@ export default class MetamaskController extends EventEmitter { ), setOnboardingDate: appStateController.setOnboardingDate.bind(appStateController), + setLastViewedUserSurvey: + appStateController.setLastViewedUserSurvey.bind(appStateController), setNewPrivacyPolicyToastClickedOrClosed: appStateController.setNewPrivacyPolicyToastClickedOrClosed.bind( appStateController, @@ -3880,6 +3895,15 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger, `${BRIDGE_CONTROLLER_NAME}:${BridgeBackgroundAction.SET_FEATURE_FLAGS}`, ), + [BridgeUserAction.SELECT_SRC_NETWORK]: this.controllerMessenger.call.bind( + this.controllerMessenger, + `${BRIDGE_CONTROLLER_NAME}:${BridgeUserAction.SELECT_SRC_NETWORK}`, + ), + [BridgeUserAction.SELECT_DEST_NETWORK]: + this.controllerMessenger.call.bind( + this.controllerMessenger, + `${BRIDGE_CONTROLLER_NAME}:${BridgeUserAction.SELECT_DEST_NETWORK}`, + ), // Smart Transactions fetchSmartTransactionFees: smartTransactionsController.getFees.bind( @@ -4108,7 +4132,7 @@ export default class MetamaskController extends EventEmitter { const { tokens } = this.tokensController.state; const staticTokenListDetails = - STATIC_MAINNET_TOKEN_LIST[address.toLowerCase()] || {}; + STATIC_MAINNET_TOKEN_LIST[address?.toLowerCase()] || {}; const tokenListDetails = tokenList[address.toLowerCase()] || {}; const userDefinedTokenDetails = tokens.find(({ address: _address }) => @@ -4264,8 +4288,8 @@ export default class MetamaskController extends EventEmitter { // Clear notification state this.notificationController.clear(); - // clear accounts in accountTracker - this.accountTracker.clearAccounts(); + // clear accounts in AccountTrackerController + this.accountTrackerController.clearAccounts(); this.txController.clearUnapprovedTransactions(); @@ -4362,14 +4386,14 @@ export default class MetamaskController extends EventEmitter { } /** - * Get an account balance from the AccountTracker or request it directly from the network. + * Get an account balance from the AccountTrackerController or request it directly from the network. * * @param {string} address - The account address * @param {EthQuery} ethQuery - The EthQuery instance to use when asking the network */ getBalance(address, ethQuery) { return new Promise((resolve, reject) => { - const cached = this.accountTracker.store.getState().accounts[address]; + const cached = this.accountTrackerController.state.accounts[address]; if (cached && cached.balance) { resolve(cached.balance); @@ -4427,9 +4451,9 @@ export default class MetamaskController extends EventEmitter { // Automatic login via config password await this.submitPassword(password); - // Updating accounts in this.accountTracker before starting UI syncing ensure that + // Updating accounts in this.accountTrackerController before starting UI syncing ensure that // state has account balance before it is synced with UI - await this.accountTracker.updateAccountsAllActiveNetworks(); + await this.accountTrackerController.updateAccountsAllActiveNetworks(); } finally { this._startUISync(); } @@ -4606,7 +4630,7 @@ export default class MetamaskController extends EventEmitter { oldAccounts.concat(accounts.map((a) => a.address.toLowerCase())), ), ]; - this.accountTracker.syncWithAddresses(accountsToTrack); + this.accountTrackerController.syncWithAddresses(accountsToTrack); return accounts; } @@ -6153,7 +6177,7 @@ export default class MetamaskController extends EventEmitter { return; } - this.accountTracker.syncWithAddresses(addresses); + this.accountTrackerController.syncWithAddresses(addresses); } /** diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index 4121160a45af..d1da34c48e0e 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -738,19 +738,23 @@ describe('MetaMaskController', () => { }); describe('#getBalance', () => { - it('should return the balance known by accountTracker', async () => { + it('should return the balance known by accountTrackerController', async () => { const accounts = {}; const balance = '0x14ced5122ce0a000'; accounts[TEST_ADDRESS] = { balance }; - metamaskController.accountTracker.store.putState({ accounts }); + jest + .spyOn(metamaskController.accountTrackerController, 'state', 'get') + .mockReturnValue({ + accounts, + }); const gotten = await metamaskController.getBalance(TEST_ADDRESS); expect(balance).toStrictEqual(gotten); }); - it('should ask the network for a balance when not known by accountTracker', async () => { + it('should ask the network for a balance when not known by accountTrackerController', async () => { const accounts = {}; const balance = '0x14ced5122ce0a000'; const ethQuery = new EthQuery(); @@ -758,7 +762,11 @@ describe('MetaMaskController', () => { callback(undefined, balance); }); - metamaskController.accountTracker.store.putState({ accounts }); + jest + .spyOn(metamaskController.accountTrackerController, 'state', 'get') + .mockReturnValue({ + accounts, + }); const gotten = await metamaskController.getBalance( TEST_ADDRESS, @@ -1687,21 +1695,27 @@ describe('MetaMaskController', () => { it('should do nothing if there are no keyrings in state', async () => { jest - .spyOn(metamaskController.accountTracker, 'syncWithAddresses') + .spyOn( + metamaskController.accountTrackerController, + 'syncWithAddresses', + ) .mockReturnValue(); const oldState = metamaskController.getState(); await metamaskController._onKeyringControllerUpdate({ keyrings: [] }); expect( - metamaskController.accountTracker.syncWithAddresses, + metamaskController.accountTrackerController.syncWithAddresses, ).not.toHaveBeenCalled(); expect(metamaskController.getState()).toStrictEqual(oldState); }); it('should sync addresses if there are keyrings in state', async () => { jest - .spyOn(metamaskController.accountTracker, 'syncWithAddresses') + .spyOn( + metamaskController.accountTrackerController, + 'syncWithAddresses', + ) .mockReturnValue(); const oldState = metamaskController.getState(); @@ -1714,14 +1728,17 @@ describe('MetaMaskController', () => { }); expect( - metamaskController.accountTracker.syncWithAddresses, + metamaskController.accountTrackerController.syncWithAddresses, ).toHaveBeenCalledWith(accounts); expect(metamaskController.getState()).toStrictEqual(oldState); }); it('should NOT update selected address if already unlocked', async () => { jest - .spyOn(metamaskController.accountTracker, 'syncWithAddresses') + .spyOn( + metamaskController.accountTrackerController, + 'syncWithAddresses', + ) .mockReturnValue(); const oldState = metamaskController.getState(); @@ -1735,14 +1752,17 @@ describe('MetaMaskController', () => { }); expect( - metamaskController.accountTracker.syncWithAddresses, + metamaskController.accountTrackerController.syncWithAddresses, ).toHaveBeenCalledWith(accounts); expect(metamaskController.getState()).toStrictEqual(oldState); }); it('filter out non-EVM addresses prior to calling syncWithAddresses', async () => { jest - .spyOn(metamaskController.accountTracker, 'syncWithAddresses') + .spyOn( + metamaskController.accountTrackerController, + 'syncWithAddresses', + ) .mockReturnValue(); const oldState = metamaskController.getState(); @@ -1759,7 +1779,7 @@ describe('MetaMaskController', () => { }); expect( - metamaskController.accountTracker.syncWithAddresses, + metamaskController.accountTrackerController.syncWithAddresses, ).toHaveBeenCalledWith(accounts); expect(metamaskController.getState()).toStrictEqual(oldState); }); diff --git a/app/scripts/migrations/128.test.ts b/app/scripts/migrations/128.test.ts new file mode 100644 index 000000000000..f2658bfc6bd9 --- /dev/null +++ b/app/scripts/migrations/128.test.ts @@ -0,0 +1,39 @@ +import { migrate, version } from './128'; + +const oldVersion = 127; + +describe(`migration #${version}`, () => { + it('updates the version metadata', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: {}, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.meta).toStrictEqual({ version }); + }); + + it('Removes useNativeCurrencyAsPrimaryCurrency from the PreferencesController.preferences state', async () => { + const oldState = { + PreferencesController: { + preferences: { + hideZeroBalanceTokens: false, + showTestNetworks: true, + useNativeCurrencyAsPrimaryCurrency: true, + }, + }, + }; + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + delete ( + oldState.PreferencesController.preferences as { + useNativeCurrencyAsPrimaryCurrency?: boolean; + } + ).useNativeCurrencyAsPrimaryCurrency; + expect(transformedState.data).toStrictEqual(oldState); + }); +}); diff --git a/app/scripts/migrations/128.ts b/app/scripts/migrations/128.ts new file mode 100644 index 000000000000..89f14606af7f --- /dev/null +++ b/app/scripts/migrations/128.ts @@ -0,0 +1,42 @@ +import { hasProperty, isObject } from '@metamask/utils'; +import { cloneDeep } from 'lodash'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 128; + +/** + * This migration removes `useNativeCurrencyAsPrimaryCurrency` from preferences in PreferencesController. + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly what we persist to dist. + * @param originalVersionedData.meta - State metadata. + * @param originalVersionedData.meta.version - The current state version. + * @param originalVersionedData.data - The persisted MetaMask state, keyed by controller. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate( + originalVersionedData: VersionedData, +): Promise { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + transformState(versionedData.data); + return versionedData; +} + +function transformState( + state: Record, +): Record { + if ( + hasProperty(state, 'PreferencesController') && + isObject(state.PreferencesController) && + isObject(state.PreferencesController.preferences) + ) { + delete state.PreferencesController.preferences + .useNativeCurrencyAsPrimaryCurrency; + } + + return state; +} diff --git a/app/scripts/migrations/129.test.ts b/app/scripts/migrations/129.test.ts new file mode 100644 index 000000000000..740add0e7e4e --- /dev/null +++ b/app/scripts/migrations/129.test.ts @@ -0,0 +1,60 @@ +import { migrate, version } from './129'; + +const oldVersion = 128; + +describe(`migration #${version}`, () => { + it('updates the version metadata', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: {}, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.meta).toStrictEqual({ version }); + }); + + it('Adds shouldShowAggregatedBalancePopover to the PreferencesController.preferences state when its undefined', async () => { + const oldState = { + PreferencesController: { + preferences: { + hideZeroBalanceTokens: false, + showTestNetworks: true, + }, + }, + }; + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + expect(transformedState.data).toStrictEqual({ + ...oldState, + PreferencesController: { + ...oldState.PreferencesController, + preferences: { + ...oldState.PreferencesController.preferences, + shouldShowAggregatedBalancePopover: true, + }, + }, + }); + }); + + it('Does not add shouldShowAggregatedBalancePopover to the PreferencesController.preferences state when its defined', async () => { + const oldState = { + PreferencesController: { + preferences: { + hideZeroBalanceTokens: false, + showTestNetworks: true, + shouldShowAggregatedBalancePopover: false, + }, + }, + }; + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + expect(transformedState.data).toStrictEqual(oldState); + }); +}); diff --git a/app/scripts/migrations/129.ts b/app/scripts/migrations/129.ts new file mode 100644 index 000000000000..b4323798a006 --- /dev/null +++ b/app/scripts/migrations/129.ts @@ -0,0 +1,47 @@ +import { hasProperty, isObject } from '@metamask/utils'; +import { cloneDeep } from 'lodash'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 129; + +/** + * This migration adds `shouldShowAggregatedBalancePopover` to preferences in PreferencesController and set it to true when its undefined. + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly what we persist to dist. + * @param originalVersionedData.meta - State metadata. + * @param originalVersionedData.meta.version - The current state version. + * @param originalVersionedData.data - The persisted MetaMask state, keyed by controller. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate( + originalVersionedData: VersionedData, +): Promise { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + transformState(versionedData.data); + return versionedData; +} + +function transformState( + state: Record, +): Record { + if ( + hasProperty(state, 'PreferencesController') && + isObject(state.PreferencesController) && + isObject(state.PreferencesController.preferences) + ) { + if ( + state.PreferencesController.preferences + .shouldShowAggregatedBalancePopover === undefined + ) { + state.PreferencesController.preferences.shouldShowAggregatedBalancePopover = + true; + } + } + + return state; +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 80407ecf232e..296ff8077613 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -147,6 +147,8 @@ const migrations = [ require('./125.1'), require('./126'), require('./127'), + require('./128'), + require('./129'), ]; export default migrations; diff --git a/app/scripts/offscreen.js b/app/scripts/offscreen.js index f09fcbcf0f58..67dcb43fb38f 100644 --- a/app/scripts/offscreen.js +++ b/app/scripts/offscreen.js @@ -5,6 +5,27 @@ import { } from '../../shared/constants/offscreen-communication'; import { getSocketBackgroundToMocha } from '../../test/e2e/background-socket/socket-background-to-mocha'; +/** + * Returns whether the offscreen document already exists or not. + * + * See https://developer.chrome.com/docs/extensions/reference/api/offscreen#before_chrome_116_check_if_an_offscreen_document_is_open + * + * @returns True if the offscreen document already is has been opened, otherwise false. + */ +async function hasOffscreenDocument() { + const { chrome, clients } = globalThis; + // getContexts is only available in Chrome 116+ + if ('getContexts' in chrome.runtime) { + const contexts = await chrome.runtime.getContexts({ + contextTypes: ['OFFSCREEN_DOCUMENT'], + }); + return contexts.length > 0; + } + const matchedClients = await clients.matchAll(); + const url = chrome.runtime.getURL('offscreen.html'); + return matchedClients.some((client) => client.url === url); +} + /** * Creates an offscreen document that can be used to load additional scripts * and iframes that can communicate with the extension through the chrome @@ -41,6 +62,14 @@ export async function createOffscreen() { }); try { + const offscreenExists = await hasOffscreenDocument(); + + // In certain cases the offscreen document may already exist during boot, if it does, we close it and recreate it. + if (offscreenExists) { + console.debug('Found existing offscreen document, closing.'); + await chrome.offscreen.closeDocument(); + } + await chrome.offscreen.createDocument({ url: './offscreen.html', reasons: ['IFRAME_SCRIPTING'], @@ -51,18 +80,10 @@ export async function createOffscreen() { if (offscreenDocumentLoadedListener) { chrome.runtime.onMessage.removeListener(offscreenDocumentLoadedListener); } - if ( - error?.message?.startsWith( - 'Only a single offscreen document may be created', - ) - ) { - console.debug('Offscreen document already exists; skipping creation'); - } else { - // Report unrecongized errors without halting wallet initialization - // Failures to create the offscreen document does not compromise wallet data integrity or - // core functionality, it's just needed for specific features. - captureException(error); - } + // Report unrecongized errors without halting wallet initialization + // Failures to create the offscreen document does not compromise wallet data integrity or + // core functionality, it's just needed for specific features. + captureException(error); return; } diff --git a/app/scripts/snaps/preinstalled-snaps.ts b/app/scripts/snaps/preinstalled-snaps.ts index 81755e98da2b..f46681ddab57 100644 --- a/app/scripts/snaps/preinstalled-snaps.ts +++ b/app/scripts/snaps/preinstalled-snaps.ts @@ -1,8 +1,8 @@ import type { PreinstalledSnap } from '@metamask/snaps-controllers'; import MessageSigningSnap from '@metamask/message-signing-snap/dist/preinstalled-snap.json'; import EnsResolverSnap from '@metamask/ens-resolver-snap/dist/preinstalled-snap.json'; -import AccountWatcherSnap from '@metamask/account-watcher/dist/preinstalled-snap.json'; ///: BEGIN:ONLY_INCLUDE_IF(build-flask) +import AccountWatcherSnap from '@metamask/account-watcher/dist/preinstalled-snap.json'; import BitcoinWalletSnap from '@metamask/bitcoin-wallet-snap/dist/preinstalled-snap.json'; import PreinstalledExampleSnap from '@metamask/preinstalled-example-snap/dist/preinstalled-snap.json'; ///: END:ONLY_INCLUDE_IF @@ -11,8 +11,8 @@ import PreinstalledExampleSnap from '@metamask/preinstalled-example-snap/dist/pr const PREINSTALLED_SNAPS = Object.freeze([ MessageSigningSnap as PreinstalledSnap, EnsResolverSnap as PreinstalledSnap, - AccountWatcherSnap as PreinstalledSnap, ///: BEGIN:ONLY_INCLUDE_IF(build-flask) + AccountWatcherSnap as PreinstalledSnap, BitcoinWalletSnap as unknown as PreinstalledSnap, PreinstalledExampleSnap as unknown as PreinstalledSnap, ///: END:ONLY_INCLUDE_IF diff --git a/builds.yml b/builds.yml index acee49063822..a69bf611a322 100644 --- a/builds.yml +++ b/builds.yml @@ -137,6 +137,7 @@ features: # env object supports both declarations (- FOO), and definitions (- FOO: BAR). # Variables that were declared have to be defined somewhere in the load chain before usage env: + - ACCOUNTS_USE_DEV_APIS: false - BRIDGE_USE_DEV_APIS: false - SWAPS_USE_DEV_APIS: false - PORTFOLIO_URL: https://portfolio.metamask.io @@ -272,8 +273,10 @@ env: - SECURITY_ALERTS_API_ENABLED: '' # URL of security alerts API used to validate dApp requests - SECURITY_ALERTS_API_URL: 'http://localhost:3000' + # API key to authenticate Etherscan requests to avoid rate limiting + - ETHERSCAN_API_KEY: '' - # Enables the notifications feature within the build: + # Enables the notifications feature within the build: - NOTIFICATIONS: '' - METAMASK_RAMP_API_CONTENT_BASE_URL: https://on-ramp-content.api.cx.metamask.io @@ -290,6 +293,7 @@ env: ### - EIP_4337_ENTRYPOINT: null + ### # Enable/disable why did you render debug tool: https://github.com/welldone-software/why-did-you-render # This should NEVER be enabled in production since it slows down react diff --git a/coverage.json b/coverage.json new file mode 100644 index 000000000000..f65ea343e9b3 --- /dev/null +++ b/coverage.json @@ -0,0 +1 @@ +{ "coverage": 0 } diff --git a/development/ts-migration-dashboard/files-to-convert.json b/development/ts-migration-dashboard/files-to-convert.json index d5063250db16..ea3015d4c1ba 100644 --- a/development/ts-migration-dashboard/files-to-convert.json +++ b/development/ts-migration-dashboard/files-to-convert.json @@ -63,7 +63,6 @@ "app/scripts/inpage.js", "app/scripts/lib/ComposableObservableStore.js", "app/scripts/lib/ComposableObservableStore.test.js", - "app/scripts/lib/account-tracker.js", "app/scripts/lib/cleanErrorStack.js", "app/scripts/lib/cleanErrorStack.test.js", "app/scripts/lib/createLoggerMiddleware.js", diff --git a/jest.config.js b/jest.config.js index be304e027ace..f1d38ab4aea3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,12 +1,10 @@ module.exports = { collectCoverageFrom: [ - '/app/**/*.(js|ts|tsx)', - '/development/**/*.(js|ts|tsx)', - '/offscreen/**/*.(js|ts|tsx)', + '/app/scripts/**/*.(js|ts|tsx)', '/shared/**/*.(js|ts|tsx)', - '/test/**/*.(js|ts|tsx)', - '/types/**/*.(js|ts|tsx)', '/ui/**/*.(js|ts|tsx)', + '/development/build/transforms/**/*.js', + '/test/unit-global/**/*.test.(js|ts|tsx)', ], coverageDirectory: './coverage/unit', coveragePathIgnorePatterns: ['.stories.*', '.snap'], @@ -24,7 +22,11 @@ module.exports = { // TODO: enable resetMocks // resetMocks: true, restoreMocks: true, - setupFiles: ['/test/setup.js', '/test/env.js'], + setupFiles: [ + 'jest-canvas-mock', + '/test/setup.js', + '/test/env.js', + ], setupFilesAfterEnv: ['/test/jest/setup.js'], testMatch: [ '/app/scripts/**/*.test.(js|ts|tsx)', diff --git a/jest.integration.config.js b/jest.integration.config.js index e6635bd5b695..6f5d79484386 100644 --- a/jest.integration.config.js +++ b/jest.integration.config.js @@ -18,6 +18,7 @@ module.exports = { ], restoreMocks: true, setupFiles: [ + 'jest-canvas-mock', '/test/integration/config/setup.js', '/test/integration/config/env.js', ], diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 70552b0d32a7..c8c97ce1dd8a 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -2946,7 +2946,7 @@ }, "@sentry/browser": { "globals": { - "PerformanceObserver.supportedEntryTypes.includes": true, + "PerformanceObserver.supportedEntryTypes": true, "Request": true, "URL": true, "XMLHttpRequest.prototype": true, @@ -3043,7 +3043,8 @@ "innerWidth": true, "location.href": true, "location.origin": true, - "parent": true + "parent": true, + "setTimeout": true }, "packages": { "@sentry/browser>@sentry-internal/browser-utils": true, @@ -4509,6 +4510,14 @@ "define": true } }, + "history": { + "globals": { + "console": true, + "define": true, + "document.defaultView": true, + "document.querySelector": true + } + }, "https-browserify": { "packages": { "browserify>url": true, @@ -4599,6 +4608,33 @@ "navigator": true } }, + "lottie-web": { + "globals": { + "Blob": true, + "Howl": true, + "OffscreenCanvas": true, + "URL.createObjectURL": true, + "Worker": true, + "XMLHttpRequest": true, + "bodymovin": "write", + "clearInterval": true, + "console": true, + "define": true, + "document.body": true, + "document.createElement": true, + "document.createElementNS": true, + "document.getElementsByClassName": true, + "document.getElementsByTagName": true, + "document.querySelectorAll": true, + "document.readyState": true, + "location.origin": true, + "location.pathname": true, + "navigator": true, + "requestAnimationFrame": true, + "setInterval": true, + "setTimeout": true + } + }, "luxon": { "globals": { "Intl": true @@ -5086,37 +5122,59 @@ "react-router-dom>tiny-warning": true } }, - "react-router-dom>history": { + "react-router-dom-v5-compat": { "globals": { + "FormData": true, + "URL": true, + "URLSearchParams": true, + "__reactRouterVersion": "write", "addEventListener": true, "confirm": true, + "define": true, "document": true, - "history": true, - "location": true, - "navigator.userAgent": true, - "removeEventListener": true + "history.scrollRestoration": true, + "location.href": true, + "removeEventListener": true, + "scrollTo": true, + "scrollY": true, + "sessionStorage.getItem": true, + "sessionStorage.setItem": true, + "setTimeout": true }, "packages": { - "react-router-dom>history>resolve-pathname": true, - "react-router-dom>history>value-equal": true, - "react-router-dom>tiny-invariant": true, - "react-router-dom>tiny-warning": true + "history": true, + "react": true, + "react-dom": true, + "react-router-dom": true, + "react-router-dom-v5-compat>@remix-run/router": true, + "react-router-dom-v5-compat>react-router": true } }, - "react-router-dom>react-router": { + "react-router-dom-v5-compat>@remix-run/router": { + "globals": { + "AbortController": true, + "DOMException": true, + "FormData": true, + "Headers": true, + "Request": true, + "Response": true, + "URL": true, + "URLSearchParams": true, + "console": true, + "document.defaultView": true + } + }, + "react-router-dom-v5-compat>react-router": { + "globals": { + "console.error": true, + "define": true + }, "packages": { - "prop-types": true, - "prop-types>react-is": true, "react": true, - "react-redux>hoist-non-react-statics": true, - "react-router-dom>react-router>history": true, - "react-router-dom>react-router>mini-create-react-context": true, - "react-router-dom>tiny-invariant": true, - "react-router-dom>tiny-warning": true, - "serve-handler>path-to-regexp": true + "react-router-dom-v5-compat>@remix-run/router": true } }, - "react-router-dom>react-router>history": { + "react-router-dom>history": { "globals": { "addEventListener": true, "confirm": true, @@ -5133,13 +5191,16 @@ "react-router-dom>tiny-warning": true } }, - "react-router-dom>react-router>mini-create-react-context": { + "react-router-dom>react-router": { "packages": { - "@babel/runtime": true, "prop-types": true, + "prop-types>react-is": true, "react": true, - "react-router-dom>react-router>mini-create-react-context>gud": true, - "react-router-dom>tiny-warning": true + "react-redux>hoist-non-react-statics": true, + "react-router-dom>history": true, + "react-router-dom>tiny-invariant": true, + "react-router-dom>tiny-warning": true, + "serve-handler>path-to-regexp": true } }, "react-router-dom>tiny-warning": { diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 70552b0d32a7..c8c97ce1dd8a 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -2946,7 +2946,7 @@ }, "@sentry/browser": { "globals": { - "PerformanceObserver.supportedEntryTypes.includes": true, + "PerformanceObserver.supportedEntryTypes": true, "Request": true, "URL": true, "XMLHttpRequest.prototype": true, @@ -3043,7 +3043,8 @@ "innerWidth": true, "location.href": true, "location.origin": true, - "parent": true + "parent": true, + "setTimeout": true }, "packages": { "@sentry/browser>@sentry-internal/browser-utils": true, @@ -4509,6 +4510,14 @@ "define": true } }, + "history": { + "globals": { + "console": true, + "define": true, + "document.defaultView": true, + "document.querySelector": true + } + }, "https-browserify": { "packages": { "browserify>url": true, @@ -4599,6 +4608,33 @@ "navigator": true } }, + "lottie-web": { + "globals": { + "Blob": true, + "Howl": true, + "OffscreenCanvas": true, + "URL.createObjectURL": true, + "Worker": true, + "XMLHttpRequest": true, + "bodymovin": "write", + "clearInterval": true, + "console": true, + "define": true, + "document.body": true, + "document.createElement": true, + "document.createElementNS": true, + "document.getElementsByClassName": true, + "document.getElementsByTagName": true, + "document.querySelectorAll": true, + "document.readyState": true, + "location.origin": true, + "location.pathname": true, + "navigator": true, + "requestAnimationFrame": true, + "setInterval": true, + "setTimeout": true + } + }, "luxon": { "globals": { "Intl": true @@ -5086,37 +5122,59 @@ "react-router-dom>tiny-warning": true } }, - "react-router-dom>history": { + "react-router-dom-v5-compat": { "globals": { + "FormData": true, + "URL": true, + "URLSearchParams": true, + "__reactRouterVersion": "write", "addEventListener": true, "confirm": true, + "define": true, "document": true, - "history": true, - "location": true, - "navigator.userAgent": true, - "removeEventListener": true + "history.scrollRestoration": true, + "location.href": true, + "removeEventListener": true, + "scrollTo": true, + "scrollY": true, + "sessionStorage.getItem": true, + "sessionStorage.setItem": true, + "setTimeout": true }, "packages": { - "react-router-dom>history>resolve-pathname": true, - "react-router-dom>history>value-equal": true, - "react-router-dom>tiny-invariant": true, - "react-router-dom>tiny-warning": true + "history": true, + "react": true, + "react-dom": true, + "react-router-dom": true, + "react-router-dom-v5-compat>@remix-run/router": true, + "react-router-dom-v5-compat>react-router": true } }, - "react-router-dom>react-router": { + "react-router-dom-v5-compat>@remix-run/router": { + "globals": { + "AbortController": true, + "DOMException": true, + "FormData": true, + "Headers": true, + "Request": true, + "Response": true, + "URL": true, + "URLSearchParams": true, + "console": true, + "document.defaultView": true + } + }, + "react-router-dom-v5-compat>react-router": { + "globals": { + "console.error": true, + "define": true + }, "packages": { - "prop-types": true, - "prop-types>react-is": true, "react": true, - "react-redux>hoist-non-react-statics": true, - "react-router-dom>react-router>history": true, - "react-router-dom>react-router>mini-create-react-context": true, - "react-router-dom>tiny-invariant": true, - "react-router-dom>tiny-warning": true, - "serve-handler>path-to-regexp": true + "react-router-dom-v5-compat>@remix-run/router": true } }, - "react-router-dom>react-router>history": { + "react-router-dom>history": { "globals": { "addEventListener": true, "confirm": true, @@ -5133,13 +5191,16 @@ "react-router-dom>tiny-warning": true } }, - "react-router-dom>react-router>mini-create-react-context": { + "react-router-dom>react-router": { "packages": { - "@babel/runtime": true, "prop-types": true, + "prop-types>react-is": true, "react": true, - "react-router-dom>react-router>mini-create-react-context>gud": true, - "react-router-dom>tiny-warning": true + "react-redux>hoist-non-react-statics": true, + "react-router-dom>history": true, + "react-router-dom>tiny-invariant": true, + "react-router-dom>tiny-warning": true, + "serve-handler>path-to-regexp": true } }, "react-router-dom>tiny-warning": { diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 70552b0d32a7..c8c97ce1dd8a 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -2946,7 +2946,7 @@ }, "@sentry/browser": { "globals": { - "PerformanceObserver.supportedEntryTypes.includes": true, + "PerformanceObserver.supportedEntryTypes": true, "Request": true, "URL": true, "XMLHttpRequest.prototype": true, @@ -3043,7 +3043,8 @@ "innerWidth": true, "location.href": true, "location.origin": true, - "parent": true + "parent": true, + "setTimeout": true }, "packages": { "@sentry/browser>@sentry-internal/browser-utils": true, @@ -4509,6 +4510,14 @@ "define": true } }, + "history": { + "globals": { + "console": true, + "define": true, + "document.defaultView": true, + "document.querySelector": true + } + }, "https-browserify": { "packages": { "browserify>url": true, @@ -4599,6 +4608,33 @@ "navigator": true } }, + "lottie-web": { + "globals": { + "Blob": true, + "Howl": true, + "OffscreenCanvas": true, + "URL.createObjectURL": true, + "Worker": true, + "XMLHttpRequest": true, + "bodymovin": "write", + "clearInterval": true, + "console": true, + "define": true, + "document.body": true, + "document.createElement": true, + "document.createElementNS": true, + "document.getElementsByClassName": true, + "document.getElementsByTagName": true, + "document.querySelectorAll": true, + "document.readyState": true, + "location.origin": true, + "location.pathname": true, + "navigator": true, + "requestAnimationFrame": true, + "setInterval": true, + "setTimeout": true + } + }, "luxon": { "globals": { "Intl": true @@ -5086,37 +5122,59 @@ "react-router-dom>tiny-warning": true } }, - "react-router-dom>history": { + "react-router-dom-v5-compat": { "globals": { + "FormData": true, + "URL": true, + "URLSearchParams": true, + "__reactRouterVersion": "write", "addEventListener": true, "confirm": true, + "define": true, "document": true, - "history": true, - "location": true, - "navigator.userAgent": true, - "removeEventListener": true + "history.scrollRestoration": true, + "location.href": true, + "removeEventListener": true, + "scrollTo": true, + "scrollY": true, + "sessionStorage.getItem": true, + "sessionStorage.setItem": true, + "setTimeout": true }, "packages": { - "react-router-dom>history>resolve-pathname": true, - "react-router-dom>history>value-equal": true, - "react-router-dom>tiny-invariant": true, - "react-router-dom>tiny-warning": true + "history": true, + "react": true, + "react-dom": true, + "react-router-dom": true, + "react-router-dom-v5-compat>@remix-run/router": true, + "react-router-dom-v5-compat>react-router": true } }, - "react-router-dom>react-router": { + "react-router-dom-v5-compat>@remix-run/router": { + "globals": { + "AbortController": true, + "DOMException": true, + "FormData": true, + "Headers": true, + "Request": true, + "Response": true, + "URL": true, + "URLSearchParams": true, + "console": true, + "document.defaultView": true + } + }, + "react-router-dom-v5-compat>react-router": { + "globals": { + "console.error": true, + "define": true + }, "packages": { - "prop-types": true, - "prop-types>react-is": true, "react": true, - "react-redux>hoist-non-react-statics": true, - "react-router-dom>react-router>history": true, - "react-router-dom>react-router>mini-create-react-context": true, - "react-router-dom>tiny-invariant": true, - "react-router-dom>tiny-warning": true, - "serve-handler>path-to-regexp": true + "react-router-dom-v5-compat>@remix-run/router": true } }, - "react-router-dom>react-router>history": { + "react-router-dom>history": { "globals": { "addEventListener": true, "confirm": true, @@ -5133,13 +5191,16 @@ "react-router-dom>tiny-warning": true } }, - "react-router-dom>react-router>mini-create-react-context": { + "react-router-dom>react-router": { "packages": { - "@babel/runtime": true, "prop-types": true, + "prop-types>react-is": true, "react": true, - "react-router-dom>react-router>mini-create-react-context>gud": true, - "react-router-dom>tiny-warning": true + "react-redux>hoist-non-react-statics": true, + "react-router-dom>history": true, + "react-router-dom>tiny-invariant": true, + "react-router-dom>tiny-warning": true, + "serve-handler>path-to-regexp": true } }, "react-router-dom>tiny-warning": { diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 2116f0a1a7c8..7478c04ea3aa 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -3038,7 +3038,7 @@ }, "@sentry/browser": { "globals": { - "PerformanceObserver.supportedEntryTypes.includes": true, + "PerformanceObserver.supportedEntryTypes": true, "Request": true, "URL": true, "XMLHttpRequest.prototype": true, @@ -3135,7 +3135,8 @@ "innerWidth": true, "location.href": true, "location.origin": true, - "parent": true + "parent": true, + "setTimeout": true }, "packages": { "@sentry/browser>@sentry-internal/browser-utils": true, @@ -4601,6 +4602,14 @@ "define": true } }, + "history": { + "globals": { + "console": true, + "define": true, + "document.defaultView": true, + "document.querySelector": true + } + }, "https-browserify": { "packages": { "browserify>url": true, @@ -4691,6 +4700,33 @@ "navigator": true } }, + "lottie-web": { + "globals": { + "Blob": true, + "Howl": true, + "OffscreenCanvas": true, + "URL.createObjectURL": true, + "Worker": true, + "XMLHttpRequest": true, + "bodymovin": "write", + "clearInterval": true, + "console": true, + "define": true, + "document.body": true, + "document.createElement": true, + "document.createElementNS": true, + "document.getElementsByClassName": true, + "document.getElementsByTagName": true, + "document.querySelectorAll": true, + "document.readyState": true, + "location.origin": true, + "location.pathname": true, + "navigator": true, + "requestAnimationFrame": true, + "setInterval": true, + "setTimeout": true + } + }, "luxon": { "globals": { "Intl": true @@ -5154,37 +5190,59 @@ "react-router-dom>tiny-warning": true } }, - "react-router-dom>history": { + "react-router-dom-v5-compat": { "globals": { + "FormData": true, + "URL": true, + "URLSearchParams": true, + "__reactRouterVersion": "write", "addEventListener": true, "confirm": true, + "define": true, "document": true, - "history": true, - "location": true, - "navigator.userAgent": true, - "removeEventListener": true + "history.scrollRestoration": true, + "location.href": true, + "removeEventListener": true, + "scrollTo": true, + "scrollY": true, + "sessionStorage.getItem": true, + "sessionStorage.setItem": true, + "setTimeout": true }, "packages": { - "react-router-dom>history>resolve-pathname": true, - "react-router-dom>history>value-equal": true, - "react-router-dom>tiny-invariant": true, - "react-router-dom>tiny-warning": true + "history": true, + "react": true, + "react-dom": true, + "react-router-dom": true, + "react-router-dom-v5-compat>@remix-run/router": true, + "react-router-dom-v5-compat>react-router": true } }, - "react-router-dom>react-router": { + "react-router-dom-v5-compat>@remix-run/router": { + "globals": { + "AbortController": true, + "DOMException": true, + "FormData": true, + "Headers": true, + "Request": true, + "Response": true, + "URL": true, + "URLSearchParams": true, + "console": true, + "document.defaultView": true + } + }, + "react-router-dom-v5-compat>react-router": { + "globals": { + "console.error": true, + "define": true + }, "packages": { - "prop-types": true, - "prop-types>react-is": true, "react": true, - "react-redux>hoist-non-react-statics": true, - "react-router-dom>react-router>history": true, - "react-router-dom>react-router>mini-create-react-context": true, - "react-router-dom>tiny-invariant": true, - "react-router-dom>tiny-warning": true, - "serve-handler>path-to-regexp": true + "react-router-dom-v5-compat>@remix-run/router": true } }, - "react-router-dom>react-router>history": { + "react-router-dom>history": { "globals": { "addEventListener": true, "confirm": true, @@ -5201,13 +5259,16 @@ "react-router-dom>tiny-warning": true } }, - "react-router-dom>react-router>mini-create-react-context": { + "react-router-dom>react-router": { "packages": { - "@babel/runtime": true, "prop-types": true, + "prop-types>react-is": true, "react": true, - "react-router-dom>react-router>mini-create-react-context>gud": true, - "react-router-dom>tiny-warning": true + "react-redux>hoist-non-react-statics": true, + "react-router-dom>history": true, + "react-router-dom>tiny-invariant": true, + "react-router-dom>tiny-warning": true, + "serve-handler>path-to-regexp": true } }, "react-router-dom>tiny-warning": { diff --git a/package.json b/package.json index 238f1c6c0c95..cc317effc2ae 100644 --- a/package.json +++ b/package.json @@ -297,14 +297,14 @@ "@metamask-institutional/transaction-update": "^0.2.5", "@metamask-institutional/types": "^1.1.0", "@metamask/abi-utils": "^2.0.2", - "@metamask/account-watcher": "^4.1.0", - "@metamask/accounts-controller": "^18.2.1", + "@metamask/account-watcher": "^4.1.1", + "@metamask/accounts-controller": "^18.2.2", "@metamask/address-book-controller": "^6.0.0", "@metamask/announcement-controller": "^7.0.0", "@metamask/approval-controller": "^7.0.0", "@metamask/assets-controllers": "^37.0.0", "@metamask/base-controller": "^7.0.0", - "@metamask/bitcoin-wallet-snap": "^0.6.0", + "@metamask/bitcoin-wallet-snap": "^0.6.1", "@metamask/browser-passworder": "^4.3.0", "@metamask/contract-metadata": "^2.5.0", "@metamask/controller-utils": "^11.2.0", @@ -316,17 +316,17 @@ "@metamask/eth-ledger-bridge-keyring": "^3.0.1", "@metamask/eth-query": "^4.0.0", "@metamask/eth-sig-util": "^7.0.1", - "@metamask/eth-snap-keyring": "^4.3.3", + "@metamask/eth-snap-keyring": "^4.3.6", "@metamask/eth-token-tracker": "^8.0.0", - "@metamask/eth-trezor-keyring": "^3.1.0", + "@metamask/eth-trezor-keyring": "^3.1.3", "@metamask/etherscan-link": "^3.0.0", "@metamask/ethjs": "^0.6.0", "@metamask/ethjs-contract": "^0.4.1", "@metamask/ethjs-query": "^0.7.1", "@metamask/gas-fee-controller": "^18.0.0", "@metamask/jazzicon": "^2.0.0", - "@metamask/keyring-api": "^8.1.0", - "@metamask/keyring-controller": "^17.2.1", + "@metamask/keyring-api": "^8.1.3", + "@metamask/keyring-controller": "^17.2.2", "@metamask/logging-controller": "^6.0.0", "@metamask/logo": "^3.1.2", "@metamask/message-manager": "^10.1.0", @@ -359,7 +359,7 @@ "@metamask/snaps-rpc-methods": "^11.1.1", "@metamask/snaps-sdk": "^6.5.1", "@metamask/snaps-utils": "^8.1.1", - "@metamask/transaction-controller": "^37.0.0", + "@metamask/transaction-controller": "^37.2.0", "@metamask/user-operation-controller": "^13.0.0", "@metamask/utils": "^9.1.0", "@ngraveio/bc-ur": "^1.1.12", @@ -397,6 +397,7 @@ "fast-json-patch": "^3.1.1", "fuse.js": "^3.2.0", "he": "^1.2.0", + "history": "^5.3.0", "human-standard-token-abi": "^2.0.0", "immer": "^9.0.6", "is-retry-allowed": "^2.2.0", @@ -407,6 +408,7 @@ "localforage": "^1.9.0", "lodash": "^4.17.21", "loglevel": "^1.8.1", + "lottie-web": "^5.12.2", "luxon": "^3.2.1", "nanoid": "^2.1.6", "pify": "^5.0.0", @@ -426,7 +428,8 @@ "react-popper": "^2.2.3", "react-redux": "^7.2.9", "react-responsive-carousel": "^3.2.21", - "react-router-dom": "^5.1.2", + "react-router-dom": "^5.3.4", + "react-router-dom-v5-compat": "^6.26.2", "react-simple-file-input": "^2.0.0", "react-tippy": "^1.2.2", "react-toggle-button": "^2.2.0", @@ -603,7 +606,6 @@ "gulp-stylelint": "^13.0.0", "gulp-watch": "^5.0.1", "gulp-zip": "^5.1.0", - "history": "^5.0.0", "html-bundler-webpack-plugin": "^3.17.3", "https-browserify": "^1.0.0", "husky": "^8.0.3", diff --git a/privacy-snapshot.json b/privacy-snapshot.json index 48c96582cab0..2516654f1803 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -1,4 +1,5 @@ [ + "accounts.api.cx.metamask.io", "acl.execution.metamask.io", "api.blockchair.com", "api.lens.dev", @@ -6,6 +7,7 @@ "api.web3modal.com", "app.ens.domains", "arbitrum-mainnet.infura.io", + "authentication.api.cx.metamask.io", "bafkreifvhjdf6ve4jfv6qytqtux5nd4nwnelioeiqx5x2ez5yrgrzk7ypi.ipfs.dweb.link", "bafybeidxfmwycgzcp4v2togflpqh2gnibuexjy4m4qqwxp7nh3jx5zlh4y.ipfs.dweb.link", "bridge.api.cx.metamask.io", @@ -13,6 +15,7 @@ "cdn.segment.io", "cdnjs.cloudflare.com", "chainid.network", + "client-side-detection.api.cx.metamask.io", "configuration.dev.metamask-institutional.io", "configuration.metamask-institutional.io", "connect.trezor.io", @@ -24,19 +27,23 @@ "gas.api.cx.metamask.io", "github.com", "goerli.infura.io", + "lattice.gridplus.io", "localhost:8000", "localhost:8545", "mainnet.infura.io", "metamask.eth", "metamask.github.io", + "metametrics.metamask.test", "min-api.cryptocompare.com", "nft.api.cx.metamask.io", + "oidc.api.cx.metamask.io", + "on-ramp-content.api.cx.metamask.io", + "on-ramp-content.uat-api.cx.metamask.io", "phishing-detection.api.cx.metamask.io", "portfolio.metamask.io", "price.api.cx.metamask.io", - "on-ramp-content.api.cx.metamask.io", - "on-ramp-content.uat-api.cx.metamask.io", "proxy.api.cx.metamask.io", + "proxy.dev-api.cx.metamask.io", "raw.githubusercontent.com", "registry.npmjs.org", "responsive-rpc.test", @@ -50,14 +57,8 @@ "token.api.cx.metamask.io", "tokens.api.cx.metamask.io", "tx-sentinel-ethereum-mainnet.api.cx.metamask.io", - "unresponsive-rpc.url", - "www.4byte.directory", - "lattice.gridplus.io", "unresponsive-rpc.test", - "authentication.api.cx.metamask.io", - "oidc.api.cx.metamask.io", - "price.api.cx.metamask.io", - "token.api.cx.metamask.io", - "client-side-detection.api.cx.metamask.io", - "user-storage.api.cx.metamask.io" + "unresponsive-rpc.url", + "user-storage.api.cx.metamask.io", + "www.4byte.directory" ] diff --git a/shared/constants/accounts.ts b/shared/constants/accounts.ts new file mode 100644 index 000000000000..1c63c19e1442 --- /dev/null +++ b/shared/constants/accounts.ts @@ -0,0 +1,6 @@ +export const ACCOUNTS_DEV_API_BASE_URL = + 'https://accounts.dev-api.cx.metamask.io'; +export const ACCOUNTS_PROD_API_BASE_URL = 'https://accounts.api.cx.metamask.io'; +export const ACCOUNTS_API_BASE_URL = process.env.ACCOUNTS_USE_DEV_APIS + ? ACCOUNTS_DEV_API_BASE_URL + : ACCOUNTS_PROD_API_BASE_URL; diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index d9287da93a12..d0f1cfb87cbe 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -446,9 +446,9 @@ export enum MetaMetricsUserTrait { */ TokenDetectionEnabled = 'token_detection_enabled', /** - * Identified when the user enables native currency. + * Identified when show native token as main balance is toggled. */ - UseNativeCurrencyAsPrimaryCurrency = 'use_native_currency_as_primary_currency', + ShowNativeTokenAsMainBalance = 'show_native_token_as_main_balance', /** * Identified when the security provider feature is enabled. */ @@ -536,6 +536,7 @@ export enum MetaMetricsEventName { EncryptionPublicKeyApproved = 'Encryption Approved', EncryptionPublicKeyRejected = 'Encryption Rejected', EncryptionPublicKeyRequested = 'Encryption Requested', + ErrorOccured = 'Error occured', ExternalLinkClicked = 'External Link Clicked', KeyExportSelected = 'Key Export Selected', KeyExportRequested = 'Key Export Requested', @@ -552,6 +553,7 @@ export enum MetaMetricsEventName { MarkAllNotificationsRead = 'Notifications Marked All as Read', MetricsOptIn = 'Metrics Opt In', MetricsOptOut = 'Metrics Opt Out', + MetricsDataDeletionRequest = 'Delete MetaMetrics Data Request Submitted', NavAccountMenuOpened = 'Account Menu Opened', NavConnectedSitesOpened = 'Connected Sites Opened', NavMainMenuOpened = 'Main Menu Opened', @@ -620,6 +622,7 @@ export enum MetaMetricsEventName { SrpCopiedToClipboard = 'Copies SRP to clipboard', SrpToConfirmBackup = 'SRP Backup Confirm Displayed', StakingEntryPointClicked = 'Stake Button Clicked', + SurveyToast = 'Survey Toast', SupportLinkClicked = 'Support Link Clicked', TermsOfUseShown = 'Terms of Use Shown', TermsOfUseAccepted = 'Terms of Use Accepted', @@ -632,12 +635,14 @@ export enum MetaMetricsEventName { TokenHidden = 'Token Hidden', TokenImportCanceled = 'Token Import Canceled', TokenImportClicked = 'Token Import Clicked', - UseNativeCurrencyAsPrimaryCurrency = 'Use Native Currency as Primary Currency', + ShowNativeTokenAsMainBalance = 'Show native token as main balance', WalletSetupStarted = 'Wallet Setup Selected', WalletSetupCanceled = 'Wallet Setup Canceled', WalletSetupFailed = 'Wallet Setup Failed', WalletCreated = 'Wallet Created', + // BEGIN:ONLY_INCLUDE_IF(build-flask) WatchEthereumAccountsToggled = 'Watch Ethereum Accounts Toggled', + // END:ONLY_INCLUDE_IF ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) DeeplinkClicked = 'Deeplink Clicked', ConnectCustodialAccountClicked = 'Connect Custodial Account Clicked', @@ -777,6 +782,7 @@ export enum MetaMetricsEventCategory { Retention = 'Retention', Send = 'Send', Settings = 'Settings', + Feedback = 'Feedback', Snaps = 'Snaps', Swaps = 'Swaps', Tokens = 'Tokens', diff --git a/shared/constants/mmi-controller.ts b/shared/constants/mmi-controller.ts index 50cc26ef5541..e61d7ed807cd 100644 --- a/shared/constants/mmi-controller.ts +++ b/shared/constants/mmi-controller.ts @@ -12,7 +12,7 @@ import PreferencesController from '../../app/scripts/controllers/preferences-con import { AppStateController } from '../../app/scripts/controllers/app-state'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths -import AccountTracker from '../../app/scripts/lib/account-tracker'; +import AccountTrackerController from '../../app/scripts/controllers/account-tracker-controller'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import MetaMetricsController from '../../app/scripts/controllers/metametrics'; @@ -35,7 +35,7 @@ export type MMIControllerOptions = { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any getPendingNonce: (address: string) => Promise; - accountTracker: AccountTracker; + accountTrackerController: AccountTrackerController; metaMetricsController: MetaMetricsController; networkController: NetworkController; // TODO: Replace `any` with type diff --git a/shared/modules/currency-display.utils.test.ts b/shared/modules/currency-display.utils.test.ts deleted file mode 100644 index b2fdbc456593..000000000000 --- a/shared/modules/currency-display.utils.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - showPrimaryCurrency, - showSecondaryCurrency, -} from './currency-display.utils'; - -describe('showPrimaryCurrency', () => { - it('should return true when useNativeCurrencyAsPrimaryCurrency is true', () => { - const result = showPrimaryCurrency(true, true); - expect(result).toBe(true); - }); - - it('should return true when isOriginalNativeSymbol is true', () => { - const result = showPrimaryCurrency(true, false); - expect(result).toBe(true); - }); - - it('should return false when useNativeCurrencyAsPrimaryCurrency and isOriginalNativeSymbol are false', () => { - const result = showPrimaryCurrency(false, false); - expect(result).toBe(false); - }); -}); - -describe('showSecondaryCurrency', () => { - it('should return true when useNativeCurrencyAsPrimaryCurrency is false', () => { - const result = showSecondaryCurrency(true, false); - expect(result).toBe(true); - }); - - it('should return true when isOriginalNativeSymbol is true', () => { - const result = showSecondaryCurrency(true, true); - expect(result).toBe(true); - }); - - it('should return false when useNativeCurrencyAsPrimaryCurrency is true and isOriginalNativeSymbol is false', () => { - const result = showSecondaryCurrency(false, true); - expect(result).toBe(false); - }); -}); diff --git a/shared/modules/currency-display.utils.ts b/shared/modules/currency-display.utils.ts deleted file mode 100644 index 3f50a2364e6d..000000000000 --- a/shared/modules/currency-display.utils.ts +++ /dev/null @@ -1,31 +0,0 @@ -export const showPrimaryCurrency = ( - isOriginalNativeSymbol: boolean, - useNativeCurrencyAsPrimaryCurrency: boolean, -): boolean => { - // crypto is the primary currency in this case , so we have to display it always - if (useNativeCurrencyAsPrimaryCurrency) { - return true; - } - // if the primary currency corresponds to a fiat value, check that the symbol is correct. - if (isOriginalNativeSymbol) { - return true; - } - - return false; -}; - -export const showSecondaryCurrency = ( - isOriginalNativeSymbol: boolean, - useNativeCurrencyAsPrimaryCurrency: boolean, -): boolean => { - // crypto is the secondary currency in this case , so we have to display it always - if (!useNativeCurrencyAsPrimaryCurrency) { - return true; - } - // if the secondary currency corresponds to a fiat value, check that the symbol is correct. - if (isOriginalNativeSymbol) { - return true; - } - - return false; -}; diff --git a/shared/modules/metametrics.test.ts b/shared/modules/metametrics.test.ts index 93d423fb3d2d..9d6d17bc8040 100644 --- a/shared/modules/metametrics.test.ts +++ b/shared/modules/metametrics.test.ts @@ -72,6 +72,9 @@ const createTransactionMeta = () => { }, hash: txHash, error: null, + swapMetaData: { + gas_included: true, + }, }; }; @@ -107,6 +110,7 @@ describe('getSmartTransactionMetricsProperties', () => { ); expect(result).toStrictEqual({ + gas_included: true, is_smart_transaction: true, smart_transaction_duplicated: true, smart_transaction_proxied: true, @@ -132,7 +136,7 @@ describe('getSmartTransactionMetricsProperties', () => { }); }); - it('returns "is_smart_transaction: true" only if it is a smart transaction, but does not have statusMetadata', () => { + it('returns "is_smart_transaction" and "gas_included" params only if it is a smart transaction, but does not have statusMetadata', () => { const transactionMetricsRequest = createTransactionMetricsRequest({ getIsSmartTransaction: () => true, getSmartTransactionByMinedTxHash: () => { @@ -152,6 +156,7 @@ describe('getSmartTransactionMetricsProperties', () => { expect(result).toStrictEqual({ is_smart_transaction: true, + gas_included: true, }); }); }); diff --git a/shared/modules/metametrics.ts b/shared/modules/metametrics.ts index c60c7a0e44cd..b689891da1fb 100644 --- a/shared/modules/metametrics.ts +++ b/shared/modules/metametrics.ts @@ -5,6 +5,7 @@ import { TransactionMetricsRequest } from '../../app/scripts/lib/transaction/met type SmartTransactionMetricsProperties = { is_smart_transaction: boolean; + gas_included: boolean; smart_transaction_duplicated?: boolean; smart_transaction_timed_out?: boolean; smart_transaction_proxied?: boolean; @@ -21,6 +22,7 @@ export const getSmartTransactionMetricsProperties = ( if (!isSmartTransaction) { return properties; } + properties.gas_included = transactionMeta.swapMetaData?.gas_included; const smartTransaction = transactionMetricsRequest.getSmartTransactionByMinedTxHash( transactionMeta.hash, diff --git a/sonar-project.properties b/sonar-project.properties index a965dab30d7e..ad18a60d6fc7 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,7 +3,9 @@ sonar.organization=consensys # Source sonar.sources=app,development,offscreen,shared,types,ui -sonar.exclusions=**/*.test.**,**/*.spec.**,app/images,test/e2e/page-objects,test/data + +# Exclude tests and stories from all analysis (to avoid code coverage, duplicate code, security issues, etc.) +sonar.exclusions=**/*.test.**,**/*.spec.**,app/images,test/e2e/page-objects,test/data,**/*.stories.js,**/*.stories.tsx # Tests sonar.tests=app,development,offscreen,shared,test,types,ui diff --git a/test/data/confirmations/contract-interaction.ts b/test/data/confirmations/contract-interaction.ts index 507a27a48dc3..49a6e1aad1ab 100644 --- a/test/data/confirmations/contract-interaction.ts +++ b/test/data/confirmations/contract-interaction.ts @@ -1,4 +1,6 @@ import { + SimulationData, + TransactionMeta, TransactionStatus, TransactionType, } from '@metamask/transaction-controller'; @@ -22,12 +24,14 @@ export const genUnapprovedContractInteractionConfirmation = ({ address = CONTRACT_INTERACTION_SENDER_ADDRESS, txData = DEPOSIT_METHOD_DATA, chainId = CHAIN_ID, + simulationData, }: { address?: Hex; txData?: Hex; chainId?: string; -} = {}): Confirmation => - ({ + simulationData?: SimulationData; +} = {}): Confirmation => { + const confirmation: Confirmation = { actionId: String(400855682), chainId, dappSuggestedGasFees: { @@ -160,4 +164,12 @@ export const genUnapprovedContractInteractionConfirmation = ({ userEditedGasLimit: false, userFeeLevel: 'medium', verifiedOnBlockchain: false, - } as SignatureRequestType); + } as SignatureRequestType; + + // Overwrite simulation data if provided + if (simulationData) { + (confirmation as TransactionMeta).simulationData = simulationData; + } + + return confirmation; +}; diff --git a/test/data/mock-send-state.json b/test/data/mock-send-state.json index 6629d8c6ac67..96cd95cfbd84 100644 --- a/test/data/mock-send-state.json +++ b/test/data/mock-send-state.json @@ -131,8 +131,7 @@ "preferences": { "hideZeroBalanceTokens": false, "showFiatInTestnets": false, - "showTestNetworks": true, - "useNativeCurrencyAsPrimaryCurrency": true + "showTestNetworks": true }, "seedPhraseBackedUp": null, "ensResolutionsByAddress": {}, diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 4b6a2f506215..32a61c573500 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -367,12 +367,12 @@ "preferences": { "hideZeroBalanceTokens": false, "isRedesignedConfirmationsDeveloperEnabled": false, + "petnamesEnabled": false, "showExtensionInFullSizeView": false, "showFiatInTestnets": false, + "showNativeTokenAsMainBalance": true, "showTestNetworks": true, - "smartTransactionsOptInStatus": false, - "useNativeCurrencyAsPrimaryCurrency": true, - "petnamesEnabled": false + "smartTransactionsOptInStatus": false }, "ensResolutionsByAddress": {}, "isAccountMenuOpen": false, @@ -616,8 +616,8 @@ "developer": "Metamask", "website": "https://www.consensys.io/", "auditUrls": ["auditUrl1", "auditUrl2"], - "version": "1.0.0", - "lastUpdated": "April 20, 2023" + "version": "1.1.6", + "lastUpdated": "September 26, 2024" } }, "notifications": { diff --git a/test/e2e/accounts/remove-account-snap.spec.ts b/test/e2e/accounts/remove-account-snap.spec.ts deleted file mode 100644 index f4b8e025c62d..000000000000 --- a/test/e2e/accounts/remove-account-snap.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { strict as assert } from 'assert'; -import { Suite } from 'mocha'; -import FixtureBuilder from '../fixture-builder'; -import { WINDOW_TITLES, defaultGanacheOptions, withFixtures } from '../helpers'; -import { Driver } from '../webdriver/driver'; -import { installSnapSimpleKeyring, makeNewAccountAndSwitch } from './common'; - -describe('Remove Account Snap', function (this: Suite) { - it('disable a snap and remove it', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder().build(), - ganacheOptions: defaultGanacheOptions, - title: this.test?.fullTitle(), - }, - async ({ driver }: { driver: Driver }) => { - await installSnapSimpleKeyring(driver, false); - - await makeNewAccountAndSwitch(driver); - - // Check accounts after adding the snap account. - await driver.clickElement('[data-testid="account-menu-icon"]'); - const accountMenuItemsWithSnapAdded = await driver.findElements( - '.multichain-account-list-item', - ); - await driver.clickElement('.mm-box button[aria-label="Close"]'); - - // Navigate to settings. - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); - - await driver.clickElement({ text: 'Snaps', tag: 'div' }); - await driver.clickElement({ - text: 'MetaMask Simple Snap Keyring', - tag: 'p', - }); - - // Disable the snap. - await driver.clickElement('.toggle-button > div'); - - // Remove the snap. - const removeButton = await driver.findElement( - '[data-testid="remove-snap-button"]', - ); - await driver.scrollToElement(removeButton); - await driver.clickElement('[data-testid="remove-snap-button"]'); - - await driver.clickElement({ - text: 'Continue', - tag: 'button', - }); - - await driver.fill( - '[data-testid="remove-snap-confirmation-input"]', - 'MetaMask Simple Snap Keyring', - ); - - await driver.clickElement({ - text: 'Remove Snap', - tag: 'button', - }); - - // Checking result modal - await driver.findVisibleElement({ - text: 'MetaMask Simple Snap Keyring removed', - tag: 'p', - }); - - // Assert that the snap was removed. - await driver.findElement({ - css: '.mm-box', - text: "You don't have any snaps installed.", - tag: 'p', - }); - await driver.clickElement('.mm-box button[aria-label="Close"]'); - - // Assert that an account was removed. - await driver.clickElement('[data-testid="account-menu-icon"]'); - const accountMenuItemsAfterRemoval = await driver.findElements( - '.multichain-account-list-item', - ); - assert.equal( - accountMenuItemsAfterRemoval.length, - accountMenuItemsWithSnapAdded.length - 1, - ); - }, - ); - }); -}); diff --git a/test/e2e/constants.ts b/test/e2e/constants.ts index e7fef587f533..7e92a28cf463 100644 --- a/test/e2e/constants.ts +++ b/test/e2e/constants.ts @@ -20,7 +20,7 @@ export const BUNDLER_URL = 'http://localhost:3000/rpc'; /* URL of the 4337 account snap site. */ export const ERC_4337_ACCOUNT_SNAP_URL = - 'https://metamask.github.io/snap-account-abstraction-keyring/0.4.1/'; + 'https://metamask.github.io/snap-account-abstraction-keyring/0.4.2/'; /* Salt used to generate the 4337 account. */ export const ERC_4337_ACCOUNT_SALT = '0x1'; @@ -31,7 +31,7 @@ export const SIMPLE_ACCOUNT_FACTORY = /* URL of the Snap Simple Keyring site. */ export const TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL = - 'https://metamask.github.io/snap-simple-keyring/1.1.2/'; + 'https://metamask.github.io/snap-simple-keyring/1.1.6/'; /* Address of the VerifyingPaymaster smart contract deployed to Ganache. */ export const VERIFYING_PAYMASTER = '0xbdbDEc38ed168331b1F7004cc9e5392A2272C1D7'; diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index 470e260ff959..83b8b29a5e83 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -127,6 +127,10 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { srcNetworkAllowlist: ['0x1', '0xa', '0xe708'], destNetworkAllowlist: ['0x1', '0xa', '0xe708'], }, + destTokens: {}, + destTopAssets: [], + srcTokens: {}, + srcTopAssets: [], }, }, CurrencyController: { @@ -206,11 +210,12 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { showFiatInTestnets: false, showTestNetworks: false, smartTransactionsOptInStatus: false, - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, petnamesEnabled: true, showMultiRpcModal: false, isRedesignedConfirmationsDeveloperEnabled: false, showConfirmationAdvancedDetails: false, + shouldShowAggregatedBalancePopover: true, }, selectedAddress: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', theme: 'light', diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index cc30c261d22d..415af23071e7 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -72,11 +72,12 @@ function onboardingFixture() { showFiatInTestnets: false, showTestNetworks: false, smartTransactionsOptInStatus: false, - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, petnamesEnabled: true, showMultiRpcModal: false, isRedesignedConfirmationsDeveloperEnabled: false, showConfirmationAdvancedDetails: false, + shouldShowAggregatedBalancePopover: true, }, useExternalServices: true, theme: 'light', @@ -186,6 +187,14 @@ class FixtureBuilder { }); } + withShowFiatTestnetEnabled() { + return this.withPreferencesController({ + preferences: { + showFiatInTestnets: true, + }, + }); + } + withConversionRateEnabled() { return this.withPreferencesController({ useCurrencyRateCheck: true, @@ -391,6 +400,10 @@ class FixtureBuilder { extensionSupport: false, srcNetworkAllowlist: [], }, + destTokens: {}, + destTopAssets: [], + srcTokens: {}, + srcTopAssets: [], }, }; return this; @@ -594,6 +607,14 @@ class FixtureBuilder { }); } + withPreferencesControllerShowNativeTokenAsMainBalanceDisabled() { + return this.withPreferencesController({ + preferences: { + showNativeTokenAsMainBalance: false, + }, + }); + } + withPreferencesControllerTxSimulationsDisabled() { return this.withPreferencesController({ useTransactionSimulations: false, diff --git a/test/e2e/accounts/create-watch-account.spec.ts b/test/e2e/flask/create-watch-account.spec.ts similarity index 100% rename from test/e2e/accounts/create-watch-account.spec.ts rename to test/e2e/flask/create-watch-account.spec.ts diff --git a/test/e2e/mock-e2e.js b/test/e2e/mock-e2e.js index 929a424a36ef..abc536ef6059 100644 --- a/test/e2e/mock-e2e.js +++ b/test/e2e/mock-e2e.js @@ -4,6 +4,10 @@ const { BRIDGE_DEV_API_BASE_URL, BRIDGE_PROD_API_BASE_URL, } = require('../../shared/constants/bridge'); +const { + ACCOUNTS_DEV_API_BASE_URL, + ACCOUNTS_PROD_API_BASE_URL, +} = require('../../shared/constants/accounts'); const { GAS_API_BASE_URL, SWAPS_API_V2_BASE_URL, @@ -313,6 +317,52 @@ async function setupMocking( }), ); + [ + `${ACCOUNTS_DEV_API_BASE_URL}/v1/users/fake-metrics-id/surveys`, + `${ACCOUNTS_DEV_API_BASE_URL}/v1/users/fake-metrics-fd20/surveys`, + `${ACCOUNTS_DEV_API_BASE_URL}/v1/users/test-metrics-id/surveys`, + `${ACCOUNTS_DEV_API_BASE_URL}/v1/users/invalid-metrics-id/surveys`, + `${ACCOUNTS_PROD_API_BASE_URL}/v1/users/fake-metrics-id/surveys`, + `${ACCOUNTS_PROD_API_BASE_URL}/v1/users/fake-metrics-fd20/surveys`, + `${ACCOUNTS_PROD_API_BASE_URL}/v1/users/test-metrics-id/surveys`, + `${ACCOUNTS_PROD_API_BASE_URL}/v1/users/invalid-metrics-id/surveys`, + ].forEach( + async (url) => + await server.forGet(url).thenCallback(() => { + return { + statusCode: 200, + json: { + userId: '0x123', + surveys: {}, + }, + }; + }), + ); + + let surveyCallCount = 0; + [ + `${ACCOUNTS_DEV_API_BASE_URL}/v1/users/fake-metrics-id-power-user/surveys`, + `${ACCOUNTS_PROD_API_BASE_URL}/v1/users/fake-metrics-id-power-user/surveys`, + ].forEach( + async (url) => + await server.forGet(url).thenCallback(() => { + const surveyId = surveyCallCount > 2 ? 2 : surveyCallCount; + surveyCallCount += 1; + return { + statusCode: 200, + json: { + userId: '0x123', + surveys: { + url: 'https://example.com', + description: `Test survey ${surveyId}`, + cta: 'Take survey', + id: surveyId, + }, + }, + }; + }), + ); + await server .forGet(`https://token.api.cx.metamask.io/tokens/${chainId}`) .thenCallback(() => { diff --git a/test/e2e/page-objects/flows/snap-simple-keyring.flow.ts b/test/e2e/page-objects/flows/snap-simple-keyring.flow.ts new file mode 100644 index 000000000000..68febce34b6b --- /dev/null +++ b/test/e2e/page-objects/flows/snap-simple-keyring.flow.ts @@ -0,0 +1,23 @@ +import { Driver } from '../../webdriver/driver'; +import SnapSimpleKeyringPage from '../pages/snap-simple-keyring-page'; +import { TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL } from '../../constants'; + +/** + * Go to the Snap Simple Keyring page and install the snap. + * + * @param driver - The WebDriver instance used to interact with the browser. + * @param isSyncFlow - Indicates whether to toggle on the use synchronous approval option on the snap. Defaults to true. + */ +export async function installSnapSimpleKeyring( + driver: Driver, + isSyncFlow: boolean = true, +) { + await driver.openNewPage(TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL); + + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + await snapSimpleKeyringPage.check_pageIsLoaded(); + await snapSimpleKeyringPage.installSnap(); + if (isSyncFlow) { + await snapSimpleKeyringPage.toggleUseSyncApproval(); + } +} diff --git a/test/e2e/page-objects/pages/account-list-page.ts b/test/e2e/page-objects/pages/account-list-page.ts index 05c394444c36..03bdeef1579d 100644 --- a/test/e2e/page-objects/pages/account-list-page.ts +++ b/test/e2e/page-objects/pages/account-list-page.ts @@ -1,69 +1,58 @@ import { Driver } from '../../webdriver/driver'; class AccountListPage { - private driver: Driver; + private readonly driver: Driver; - private accountListItem: string; + private readonly accountListItem = + '.multichain-account-menu-popover__list--menu-item'; - private accountMenuButton: string; + private readonly accountMenuButton = + '[data-testid="account-list-menu-details"]'; - private accountNameInput: string; + private readonly accountNameInput = '#account-name'; - private accountOptionsMenuButton: string; + private readonly accountOptionsMenuButton = + '[data-testid="account-list-item-menu-button"]'; - private addAccountConfirmButton: string; + private readonly addAccountConfirmButton = + '[data-testid="submit-add-account-with-name"]'; - private addEthereumAccountButton: string; + private readonly addEthereumAccountButton = + '[data-testid="multichain-account-menu-popover-add-account"]'; - private addSnapAccountButton: object; + private readonly addSnapAccountButton = { + text: 'Add account Snap', + tag: 'button', + }; - private closeAccountModalButton: string; + private readonly closeAccountModalButton = 'button[aria-label="Close"]'; - private createAccountButton: string; + private readonly createAccountButton = + '[data-testid="multichain-account-menu-popover-action-button"]'; - private editableLabelButton: string; + private readonly editableLabelButton = + '[data-testid="editable-label-button"]'; - private editableLabelInput: string; + private readonly editableLabelInput = '[data-testid="editable-input"] input'; - private hideUnhideAccountButton: string; + private readonly hideUnhideAccountButton = + '[data-testid="account-list-menu-hide"]'; - private hiddenAccountOptionsMenuButton: string; + private readonly hiddenAccountOptionsMenuButton = + '.multichain-account-menu-popover__list--menu-item-hidden-account [data-testid="account-list-item-menu-button"]'; - private hiddenAccountsList: string; + private readonly hiddenAccountsList = '[data-testid="hidden-accounts-list"]'; - private pinUnpinAccountButton: string; + private readonly pinUnpinAccountButton = + '[data-testid="account-list-menu-pin"]'; - private pinnedIcon: string; + private readonly pinnedIcon = '[data-testid="account-pinned-icon"]'; - private saveAccountLabelButton: string; + private readonly saveAccountLabelButton = + '[data-testid="save-account-label-input"]'; constructor(driver: Driver) { this.driver = driver; - this.accountListItem = '.multichain-account-menu-popover__list--menu-item'; - this.accountMenuButton = '[data-testid="account-list-menu-details"]'; - this.accountNameInput = '#account-name'; - this.accountOptionsMenuButton = - '[data-testid="account-list-item-menu-button"]'; - this.addAccountConfirmButton = - '[data-testid="submit-add-account-with-name"]'; - this.addEthereumAccountButton = - '[data-testid="multichain-account-menu-popover-add-account"]'; - this.addSnapAccountButton = { - text: 'Add account Snap', - tag: 'button', - }; - this.closeAccountModalButton = 'button[aria-label="Close"]'; - this.createAccountButton = - '[data-testid="multichain-account-menu-popover-action-button"]'; - this.editableLabelButton = '[data-testid="editable-label-button"]'; - this.editableLabelInput = '[data-testid="editable-input"] input'; - this.hideUnhideAccountButton = '[data-testid="account-list-menu-hide"]'; - this.hiddenAccountOptionsMenuButton = - '.multichain-account-menu-popover__list--menu-item-hidden-account [data-testid="account-list-item-menu-button"]'; - this.hiddenAccountsList = '[data-testid="hidden-accounts-list"]'; - this.pinUnpinAccountButton = '[data-testid="account-list-menu-pin"]'; - this.pinnedIcon = '[data-testid="account-pinned-icon"]'; - this.saveAccountLabelButton = '[data-testid="save-account-label-input"]'; } async check_pageIsLoaded(): Promise { @@ -179,9 +168,21 @@ class AccountListPage { }); } - async check_accountIsDisplayed(): Promise { - console.log(`Check that account is displayed in account list`); - await this.driver.waitForSelector(this.accountListItem); + /** + * Checks that the account with the specified label is not displayed in the account list. + * + * @param expectedLabel - The label of the account that should not be displayed. + */ + async check_accountIsNotDisplayedInAccountList( + expectedLabel: string, + ): Promise { + console.log( + `Check that account label ${expectedLabel} is not displayed in account list`, + ); + await this.driver.assertElementNotPresent({ + css: this.accountListItem, + text: expectedLabel, + }); } async check_accountIsPinned(): Promise { diff --git a/test/e2e/page-objects/pages/header-navbar.ts b/test/e2e/page-objects/pages/header-navbar.ts index 742b37a5c48f..495f7ddbf3c8 100644 --- a/test/e2e/page-objects/pages/header-navbar.ts +++ b/test/e2e/page-objects/pages/header-navbar.ts @@ -9,18 +9,28 @@ class HeaderNavbar { private lockMetaMaskButton: string; + private mmiPortfolioButton: string; + private settingsButton: string; + private accountSnapButton: object; + constructor(driver: Driver) { this.driver = driver; this.accountMenuButton = '[data-testid="account-menu-icon"]'; this.accountOptionMenu = '[data-testid="account-options-menu-button"]'; this.lockMetaMaskButton = '[data-testid="global-menu-lock"]'; + this.mmiPortfolioButton = '[data-testid="global-menu-mmi-portfolio"]'; this.settingsButton = '[data-testid="global-menu-settings"]'; + this.accountSnapButton = { text: 'Snaps', tag: 'div' }; } async lockMetaMask(): Promise { await this.driver.clickElement(this.accountOptionMenu); + // fix race condition with mmi build + if (process.env.MMI) { + await this.driver.waitForSelector(this.mmiPortfolioButton); + } await this.driver.clickElement(this.lockMetaMaskButton); } @@ -28,9 +38,19 @@ class HeaderNavbar { await this.driver.clickElement(this.accountMenuButton); } + async openSnapListPage(): Promise { + console.log('Open account snap page'); + await this.driver.clickElement(this.accountOptionMenu); + await this.driver.clickElement(this.accountSnapButton); + } + async openSettingsPage(): Promise { console.log('Open settings page'); await this.driver.clickElement(this.accountOptionMenu); + // fix race condition with mmi build + if (process.env.MMI) { + await this.driver.waitForSelector(this.mmiPortfolioButton); + } await this.driver.clickElement(this.settingsButton); } diff --git a/test/e2e/page-objects/pages/settings-page.ts b/test/e2e/page-objects/pages/settings-page.ts index 89678c8712ac..547f9e43a34e 100644 --- a/test/e2e/page-objects/pages/settings-page.ts +++ b/test/e2e/page-objects/pages/settings-page.ts @@ -29,7 +29,7 @@ class SettingsPage { } async goToExperimentalSettings(): Promise { - console.log('Navigating to Experimental Settings'); + console.log('Navigating to Experimental Settings page'); await this.driver.clickElement(this.experimentalSettingsButton); } } diff --git a/test/e2e/page-objects/pages/snap-list-page.ts b/test/e2e/page-objects/pages/snap-list-page.ts new file mode 100644 index 000000000000..b293340ea637 --- /dev/null +++ b/test/e2e/page-objects/pages/snap-list-page.ts @@ -0,0 +1,80 @@ +import { Driver } from '../../webdriver/driver'; + +class SnapListPage { + private readonly driver: Driver; + + private readonly closeModalButton = 'button[aria-label="Close"]'; + + private readonly continueRemoveSnapButton = { + tag: 'button', + text: 'Continue', + }; + + private readonly continueRemoveSnapModalMessage = { + tag: 'p', + text: 'Removing this Snap removes these accounts from MetaMask', + }; + + private readonly noSnapInstalledMessage = { + tag: 'p', + text: "You don't have any snaps installed.", + }; + + private readonly removeSnapButton = '[data-testid="remove-snap-button"]'; + + private readonly removeSnapConfirmationInput = + '[data-testid="remove-snap-confirmation-input"]'; + + private readonly removeSnapConfirmButton = { + tag: 'button', + text: 'Remove Snap', + }; + + // this selector needs to be combined with snap name to be unique. + private readonly snapListItem = '.snap-list-item'; + + constructor(driver: Driver) { + this.driver = driver; + } + + /** + * Removes a snap by its name from the snap list. + * + * @param snapName - The name of the snap to be removed. + */ + async removeSnapByName(snapName: string): Promise { + console.log('Removing snap on snap list page with name: ', snapName); + await this.driver.clickElement({ text: snapName, css: this.snapListItem }); + + const removeButton = await this.driver.findElement(this.removeSnapButton); + // The need to scroll to the element before clicking it is due to a bug in the Snap test dapp page. + // This bug has been fixed in the Snap test dapp page (PR here: https://github.com/MetaMask/snaps/pull/2782), which should mitigate the flaky issue of scrolling and clicking elements in the Snap test dapp. + // TODO: Once the Snaps team releases the new version with the fix, we'll be able to remove these scrolling steps and just use clickElement (which already handles scrolling). + await this.driver.scrollToElement(removeButton); + await this.driver.clickElement(this.removeSnapButton); + + await this.driver.waitForSelector(this.continueRemoveSnapModalMessage); + await this.driver.clickElement(this.continueRemoveSnapButton); + + console.log(`Fill confirmation input to confirm snap removal`); + await this.driver.waitForSelector(this.removeSnapConfirmationInput); + await this.driver.fill(this.removeSnapConfirmationInput, snapName); + await this.driver.clickElementAndWaitToDisappear( + this.removeSnapConfirmButton, + ); + + console.log(`Check snap removal success message is displayed`); + await this.driver.waitForSelector({ + text: `${snapName} removed`, + tag: 'p', + }); + await this.driver.clickElementAndWaitToDisappear(this.closeModalButton); + } + + async check_noSnapInstalledMessageIsDisplayed(): Promise { + console.log('Verifying no snaps is installed for current account'); + await this.driver.waitForSelector(this.noSnapInstalledMessage); + } +} + +export default SnapListPage; diff --git a/test/e2e/page-objects/pages/snap-simple-keyring-page.ts b/test/e2e/page-objects/pages/snap-simple-keyring-page.ts new file mode 100644 index 000000000000..8b20f1bc3bc0 --- /dev/null +++ b/test/e2e/page-objects/pages/snap-simple-keyring-page.ts @@ -0,0 +1,167 @@ +import { Driver } from '../../webdriver/driver'; +import { WINDOW_TITLES } from '../../helpers'; + +class SnapSimpleKeyringPage { + private readonly driver: Driver; + + private readonly accountCreatedMessage = { + text: 'Account created', + tag: 'h3', + }; + + private readonly accountSupportedMethods = { + text: 'Account Supported Methods', + tag: 'p', + }; + + private readonly addtoMetamaskMessage = { + text: 'Add to MetaMask', + tag: 'h3', + }; + + private readonly confirmAddtoMetamask = { + text: 'Confirm', + tag: 'button', + }; + + private readonly confirmationSubmitButton = + '[data-testid="confirmation-submit-button"]'; + + private readonly confirmCompleteButton = { + text: 'OK', + tag: 'button', + }; + + private readonly confirmConnectionButton = { + text: 'Connect', + tag: 'button', + }; + + private readonly connectButton = '#connectButton'; + + private readonly createAccountButton = { + text: 'Create Account', + tag: 'button', + }; + + private readonly createAccountMessage = + '[data-testid="create-snap-account-content-title"]'; + + private readonly createAccountSection = { + text: 'Create account', + tag: 'div', + }; + + private readonly createSnapAccountName = '#account-name'; + + private readonly installationCompleteMessage = { + text: 'Installation complete', + tag: 'h2', + }; + + private readonly pageTitle = { + text: 'Snap Simple Keyring', + tag: 'p', + }; + + private readonly snapConnectedMessage = '#snapConnected'; + + private readonly snapInstallScrollButton = + '[data-testid="snap-install-scroll"]'; + + private readonly submitAddAccountWithNameButton = + '[data-testid="submit-add-account-with-name"]'; + + private readonly useSyncApprovalToggle = + '[data-testid="use-sync-flow-toggle"]'; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.pageTitle, + this.useSyncApprovalToggle, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for Snap Simple Keyring page to be loaded', + e, + ); + throw e; + } + console.log('Snap Simple Keyring page is loaded'); + } + + /** + * Creates a new account on the Snap Simple Keyring page and checks the account is created. + */ + async createNewAccount(): Promise { + console.log('Create new account on Snap Simple Keyring page'); + await this.driver.clickElement(this.createAccountSection); + await this.driver.clickElement(this.createAccountButton); + + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector(this.createAccountMessage); + await this.driver.clickElement(this.confirmationSubmitButton); + + await this.driver.waitForSelector(this.createSnapAccountName); + await this.driver.clickElement(this.submitAddAccountWithNameButton); + + await this.driver.waitForSelector(this.accountCreatedMessage); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmationSubmitButton, + ); + await this.driver.switchToWindowWithTitle( + WINDOW_TITLES.SnapSimpleKeyringDapp, + ); + await this.check_accountSupportedMethodsDisplayed(); + } + + /** + * Installs the Simple Keyring Snap and checks the snap is connected. + */ + async installSnap(): Promise { + console.log('Install Simple Keyring Snap'); + await this.driver.clickElement(this.connectButton); + + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.clickElement(this.confirmConnectionButton); + + await this.driver.waitForSelector(this.addtoMetamaskMessage); + await this.driver.clickElementSafe(this.snapInstallScrollButton, 200); + await this.driver.waitForSelector(this.confirmAddtoMetamask); + await this.driver.clickElement(this.confirmAddtoMetamask); + + await this.driver.waitForSelector(this.installationCompleteMessage); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmCompleteButton, + ); + + await this.driver.switchToWindowWithTitle( + WINDOW_TITLES.SnapSimpleKeyringDapp, + ); + await this.check_simpleKeyringSnapConnected(); + } + + async toggleUseSyncApproval() { + console.log('Toggle Use Synchronous Approval'); + await this.driver.clickElement(this.useSyncApprovalToggle); + } + + async check_accountSupportedMethodsDisplayed(): Promise { + console.log( + 'Check new created account supported methods are displayed on simple keyring snap page', + ); + await this.driver.waitForSelector(this.accountSupportedMethods); + } + + async check_simpleKeyringSnapConnected(): Promise { + console.log('Check simple keyring snap is connected'); + await this.driver.waitForSelector(this.snapConnectedMessage); + } +} + +export default SnapSimpleKeyringPage; diff --git a/test/e2e/restore/MetaMaskUserData.json b/test/e2e/restore/MetaMaskUserData.json index 0f400e8a34e7..846acc8164cd 100644 --- a/test/e2e/restore/MetaMaskUserData.json +++ b/test/e2e/restore/MetaMaskUserData.json @@ -36,8 +36,7 @@ "showExtensionInFullSizeView": false, "showFiatInTestnets": false, "showTestNetworks": false, - "smartTransactionsOptInStatus": false, - "useNativeCurrencyAsPrimaryCurrency": true + "smartTransactionsOptInStatus": false }, "theme": "light", "useBlockie": false, diff --git a/test/e2e/snaps/test-snap-installed.spec.js b/test/e2e/snaps/test-snap-installed.spec.js index e9697fff806e..5c7a3394966f 100644 --- a/test/e2e/snaps/test-snap-installed.spec.js +++ b/test/e2e/snaps/test-snap-installed.spec.js @@ -35,7 +35,7 @@ describe('Test Snap Installed', function () { const confirmButton = await driver.findElement('#connectdialogs'); await driver.scrollToElement(confirmButton); - await driver.delay(500); + await driver.delay(1000); await driver.clickElement('#connectdialogs'); // switch to metamask extension and click connect diff --git a/test/e2e/tests/account/account-details.spec.js b/test/e2e/tests/account/account-details.spec.js index a21b9fad2e7d..f40560cb27fd 100644 --- a/test/e2e/tests/account/account-details.spec.js +++ b/test/e2e/tests/account/account-details.spec.js @@ -60,8 +60,7 @@ describe('Show account details', function () { ); await driver.clickElement('[data-testid="account-list-menu-details"'); - const qrCode = await driver.findElement('.qr-code__wrapper'); - assert.equal(await qrCode.isDisplayed(), true); + await driver.waitForSelector('.qr-code__wrapper'); }, ); }); @@ -198,11 +197,10 @@ describe('Show account details', function () { await driver.press('#account-details-authenticate', driver.Key.ENTER); // Display error when password is incorrect - const passwordErrorIsDisplayed = await driver.isElementPresent({ + await driver.waitForSelector({ css: '.mm-help-text', text: 'Incorrect Password.', }); - assert.equal(passwordErrorIsDisplayed, true); }, ); }); diff --git a/test/e2e/tests/account/remove-account-snap.spec.ts b/test/e2e/tests/account/remove-account-snap.spec.ts new file mode 100644 index 000000000000..2f0e2ab96a33 --- /dev/null +++ b/test/e2e/tests/account/remove-account-snap.spec.ts @@ -0,0 +1,50 @@ +import { Suite } from 'mocha'; +import { Driver } from '../../webdriver/driver'; +import FixtureBuilder from '../../fixture-builder'; +import { withFixtures, WINDOW_TITLES } from '../../helpers'; +import AccountListPage from '../../page-objects/pages/account-list-page'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import SnapListPage from '../../page-objects/pages/snap-list-page'; +import SnapSimpleKeyringPage from '../../page-objects/pages/snap-simple-keyring-page'; +import { installSnapSimpleKeyring } from '../../page-objects/flows/snap-simple-keyring.flow'; +import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; + +describe('Remove Account Snap @no-mmi', function (this: Suite) { + it('disable a snap and remove it', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await loginWithBalanceValidation(driver); + await installSnapSimpleKeyring(driver); + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + await snapSimpleKeyringPage.createNewAccount(); + + // Check snap account is displayed after adding the snap account. + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel('SSK Account'); + + // Navigate to account snaps list page. + await headerNavbar.openSnapListPage(); + const snapListPage = new SnapListPage(driver); + + // Remove the snap and check snap is successfully removed + await snapListPage.removeSnapByName('MetaMask Simple Snap Keyring'); + await snapListPage.check_noSnapInstalledMessageIsDisplayed(); + + // Assert that the snap account is removed from the account list + await headerNavbar.openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_accountIsNotDisplayedInAccountList( + 'SSK Account', + ); + }, + ); + }); +}); diff --git a/test/e2e/tests/bridge/bridge-test-utils.ts b/test/e2e/tests/bridge/bridge-test-utils.ts index 596c7623208d..40bb8c6bd97f 100644 --- a/test/e2e/tests/bridge/bridge-test-utils.ts +++ b/test/e2e/tests/bridge/bridge-test-utils.ts @@ -120,7 +120,17 @@ const mockServer = }; }), ); - return Promise.all(featureFlagMocks); + const portfolioMock = async () => + await mockServer_ + .forGet('https://portfolio.metamask.io/bridge') + .always() + .thenCallback(() => { + return { + statusCode: 200, + json: {}, + }; + }); + return Promise.all([...featureFlagMocks, portfolioMock]); }; export const getBridgeFixtures = ( diff --git a/test/e2e/tests/confirmations/alerts/insufficient-funds.spec.ts b/test/e2e/tests/confirmations/alerts/insufficient-funds.spec.ts index b473dabe8478..59618596c344 100644 --- a/test/e2e/tests/confirmations/alerts/insufficient-funds.spec.ts +++ b/test/e2e/tests/confirmations/alerts/insufficient-funds.spec.ts @@ -1,4 +1,3 @@ -import { strict as assert } from 'assert'; import FixtureBuilder from '../../../fixture-builder'; import { PRIVATE_KEY, @@ -52,8 +51,10 @@ describe('Alert for insufficient funds @no-mmi', function () { }); async function verifyAlertForInsufficientBalance(driver: Driver) { - const alert = await driver.findElement('[data-testid="inline-alert"]'); - assert.equal(await alert.getText(), 'Alert'); + await driver.waitForSelector({ + css: '[data-testid="inline-alert"]', + text: 'Alert', + }); await driver.clickElementSafe('.confirm-scroll-to-bottom__button'); await driver.clickElement('[data-testid="inline-alert"]'); @@ -70,12 +71,8 @@ async function mintNft(driver: Driver) { } async function displayAlertForInsufficientBalance(driver: Driver) { - const alertDescription = await driver.findElement( - '[data-testid="alert-modal__selected-alert"]', - ); - const alertDescriptionText = await alertDescription.getText(); - assert.equal( - alertDescriptionText, - 'You do not have enough ETH in your account to pay for transaction fees.', - ); + await driver.waitForSelector({ + css: '[data-testid="alert-modal__selected-alert"]', + text: 'You do not have enough ETH in your account to pay for transaction fees.', + }); } diff --git a/test/e2e/tests/confirmations/navigation.spec.ts b/test/e2e/tests/confirmations/navigation.spec.ts index d6befdff0a26..747ba15872b3 100644 --- a/test/e2e/tests/confirmations/navigation.spec.ts +++ b/test/e2e/tests/confirmations/navigation.spec.ts @@ -1,4 +1,3 @@ -import { strict as assert } from 'assert'; import { TransactionEnvelopeType } from '@metamask/transaction-controller'; import { Suite } from 'mocha'; import { By } from 'selenium-webdriver'; @@ -68,7 +67,7 @@ describe('Navigation Signature - Different signature types', function (this: Sui ); // Verify Transaction Sending ETH is displayed - await verifyTransaction(driver, 'SENDING ETH'); + await verifyTransaction(driver, 'Sending ETH'); await driver.clickElement('[data-testid="next-page"]'); @@ -80,7 +79,7 @@ describe('Navigation Signature - Different signature types', function (this: Sui ); // Verify Sign Typed Data v3 confirmation is displayed - await verifyTransaction(driver, 'SENDING ETH'); + await verifyTransaction(driver, 'Sending ETH'); await driver.clickElement('[data-testid="previous-page"]'); @@ -99,9 +98,10 @@ describe('Navigation Signature - Different signature types', function (this: Sui await openDapp(driver); await queueSignatures(driver); - await driver.clickElement('[data-testid="confirm-nav__reject-all"]'); + await driver.clickElementAndWaitForWindowToClose( + '[data-testid="confirm-nav__reject-all"]', + ); - await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await verifyRejectionResults(driver, '#signTypedDataResult'); @@ -113,97 +113,79 @@ describe('Navigation Signature - Different signature types', function (this: Sui }); async function verifySignTypedData(driver: Driver) { - const origin = await driver.findElement({ text: DAPP_HOST_ADDRESS }); - const message = await driver.findElement({ text: 'Hi, Alice!' }); - - // Verify Sign Typed Data confirmation is displayed - assert.ok(origin, 'origin'); - assert.ok(message, 'message'); + await driver.waitForSelector({ text: DAPP_HOST_ADDRESS }); + await driver.waitForSelector({ text: 'Hi, Alice!' }); } async function verifyRejectionResults(driver: Driver, verifyResultId: string) { - const rejectionResult = await driver.findElement(verifyResultId); - assert.equal( - await rejectionResult.getText(), - 'Error: User rejected the request.', - ); + await driver.waitForSelector({ + css: verifyResultId, + text: 'Error: User rejected the request.', + }); } async function verifySignedTypeV3Confirmation(driver: Driver) { - const origin = await driver.findElement({ text: DAPP_HOST_ADDRESS }); - const fromAddress = driver.findElement({ + await driver.waitForSelector({ text: DAPP_HOST_ADDRESS }); + await driver.waitForSelector({ css: '.name__value', text: '0xCD2a3...DD826', }); - const toAddress = driver.findElement({ + await driver.waitForSelector({ css: '.name__value', text: '0xbBbBB...bBBbB', }); - const contents = driver.findElement({ text: 'Hello, Bob!' }); - - assert.ok(await origin, 'origin'); - assert.ok(await fromAddress, 'fromAddress'); - assert.ok(await toAddress, 'toAddress'); - assert.ok(await contents, 'contents'); + await driver.waitForSelector({ text: 'Hello, Bob!' }); } async function verifySignedTypeV4Confirmation(driver: Driver) { verifySignedTypeV3Confirmation(driver); - const attachment = driver.findElement({ text: '0x' }); - assert.ok(await attachment, 'attachment'); + await driver.waitForSelector({ text: '0x' }); } async function queueSignatures(driver: Driver) { // There is a race condition which changes the order in which signatures are displayed (#25251) // We fix it deterministically by waiting for an element in the screen for each signature await driver.clickElement('#signTypedData'); - await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findElement({ text: 'Hi, Alice!' }); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#signTypedDataV3'); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findElement({ text: 'Reject all' }); await driver.waitForSelector(By.xpath("//div[normalize-space(.)='1 of 2']")); - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#signTypedDataV4'); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector(By.xpath("//div[normalize-space(.)='1 of 3']")); } async function queueSignaturesAndTransactions(driver: Driver) { await driver.clickElement('#signTypedData'); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.delay(2000); // Delay needed due to a race condition - // To be fixed in https://github.com/MetaMask/metamask-extension/issues/25251 - - await driver.waitUntilXWindowHandles(3); + await driver.waitForSelector({ + tag: 'p', + text: 'Hi, Alice!', + }); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#sendButton'); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.delay(2000); + await driver.waitForSelector(By.xpath("//div[normalize-space(.)='1 of 2']")); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#signTypedDataV3'); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.delay(2000); + await driver.waitForSelector(By.xpath("//div[normalize-space(.)='1 of 3']")); } async function verifyTransaction( driver: Driver, expectedTransactionType: string, ) { - const transactionType = await driver.findElement( - '.confirm-page-container-summary__action__name', - ); - assert.equal(await transactionType.getText(), expectedTransactionType); + await driver.waitForSelector({ + tag: 'span', + text: expectedTransactionType, + }); } diff --git a/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts b/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts index 69d8cbbc4f84..053c9f40f8b7 100644 --- a/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts +++ b/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts @@ -152,8 +152,10 @@ async function acknowledgeAlert(driver: Driver) { async function verifyAlertIsDisplayed(driver: Driver) { await driver.clickElementSafe('.confirm-scroll-to-bottom__button'); - const alert = await driver.findElement('[data-testid="inline-alert"]'); - assert.equal(await alert.getText(), 'Alert'); + await driver.waitForSelector({ + css: '[data-testid="inline-alert"]', + text: 'Alert', + }); await driver.clickElement('[data-testid="inline-alert"]'); } @@ -161,6 +163,8 @@ async function assertVerifiedMessage(driver: Driver, message: string) { await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - const verifySigUtil = await driver.findElement('#siweResult'); - assert.equal(await verifySigUtil.getText(), message); + await driver.waitForSelector({ + css: '#siweResult', + text: message, + }); } diff --git a/test/e2e/tests/confirmations/signatures/permit.spec.ts b/test/e2e/tests/confirmations/signatures/permit.spec.ts index 2a87db442da5..c9c4ca9399f4 100644 --- a/test/e2e/tests/confirmations/signatures/permit.spec.ts +++ b/test/e2e/tests/confirmations/signatures/permit.spec.ts @@ -94,11 +94,10 @@ describe('Confirmation Signature - Permit @no-mmi', function (this: Suite) { await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - const rejectionResult = await driver.findElement('#signPermitResult'); - assert.equal( - await rejectionResult.getText(), - 'Error: User rejected the request.', - ); + await driver.waitForSelector({ + tag: 'span', + text: 'Error: User rejected the request.', + }); await assertSignatureRejectedMetrics({ driver, @@ -146,31 +145,32 @@ async function assertVerifiedResults(driver: Driver, publicAddress: string) { await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#signPermitVerify'); - const verifyResult = await driver.findElement('#signPermitResult'); - const verifyResultR = await driver.findElement('#signPermitResultR'); - const verifyResultS = await driver.findElement('#signPermitResultS'); - const verifyResultV = await driver.findElement('#signPermitResultV'); + await driver.waitForSelector({ + css: '#signPermitVerifyResult', + text: publicAddress, + }); + + await driver.waitForSelector({ + css: '#signPermitResult', + text: '0x0a396f89ee073214f7e055e700048abd7b4aba6ecca0352937d6a2ebb7176f2f43c63097ad7597632e34d6a801695702ba603d5872a33ee7d7562fcdb9e816ee1c', + }); + await driver.waitForSelector({ + css: '#signPermitResultR', + text: 'r: 0x0a396f89ee073214f7e055e700048abd7b4aba6ecca0352937d6a2ebb7176f2f', + }); + + await driver.waitForSelector({ + css: '#signPermitResultS', + text: 's: 0x43c63097ad7597632e34d6a801695702ba603d5872a33ee7d7562fcdb9e816ee', + }); + + await driver.waitForSelector({ + css: '#signPermitResultV', + text: 'v: 28', + }); await driver.waitForSelector({ css: '#signPermitVerifyResult', text: publicAddress, }); - const verifyRecoverAddress = await driver.findElement( - '#signPermitVerifyResult', - ); - - assert.equal( - await verifyResult.getText(), - '0x0a396f89ee073214f7e055e700048abd7b4aba6ecca0352937d6a2ebb7176f2f43c63097ad7597632e34d6a801695702ba603d5872a33ee7d7562fcdb9e816ee1c', - ); - assert.equal( - await verifyResultR.getText(), - 'r: 0x0a396f89ee073214f7e055e700048abd7b4aba6ecca0352937d6a2ebb7176f2f', - ); - assert.equal( - await verifyResultS.getText(), - 's: 0x43c63097ad7597632e34d6a801695702ba603d5872a33ee7d7562fcdb9e816ee', - ); - assert.equal(await verifyResultV.getText(), 'v: 28'); - assert.equal(await verifyRecoverAddress.getText(), publicAddress); } diff --git a/test/e2e/tests/confirmations/signatures/personal-sign.spec.ts b/test/e2e/tests/confirmations/signatures/personal-sign.spec.ts index 429881bf2f23..dca5e6ba27d5 100644 --- a/test/e2e/tests/confirmations/signatures/personal-sign.spec.ts +++ b/test/e2e/tests/confirmations/signatures/personal-sign.spec.ts @@ -116,17 +116,18 @@ async function assertVerifiedPersonalMessage( await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#personalSignVerify'); - const verifySigUtil = await driver.findElement( - '#personalSignVerifySigUtilResult', - ); await driver.waitForSelector({ css: '#personalSignVerifyECRecoverResult', text: publicAddress, }); - const verifyECRecover = await driver.findElement( - '#personalSignVerifyECRecoverResult', - ); - assert.equal(await verifySigUtil.getText(), publicAddress); - assert.equal(await verifyECRecover.getText(), publicAddress); + await driver.waitForSelector({ + css: '#personalSignVerifySigUtilResult', + text: publicAddress, + }); + + await driver.waitForSelector({ + css: '#personalSignVerifyECRecoverResult', + text: publicAddress, + }); } diff --git a/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts b/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts index 0eb1d2c698b1..f2c62e617899 100644 --- a/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts +++ b/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts @@ -96,13 +96,10 @@ describe('Confirmation Signature - Sign Typed Data V3 @no-mmi', function (this: await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - const rejectionResult = await driver.findElement( - '#signTypedDataV3Result', - ); - assert.equal( - await rejectionResult.getText(), - 'Error: User rejected the request.', - ); + await driver.waitForSelector({ + css: '#signTypedDataV3Result', + text: 'Error: User rejected the request.', + }); }, mockSignatureRejected, ); diff --git a/test/e2e/tests/confirmations/signatures/sign-typed-data-v4.spec.ts b/test/e2e/tests/confirmations/signatures/sign-typed-data-v4.spec.ts index d78acb511ce9..ca0dbb8f9bb6 100644 --- a/test/e2e/tests/confirmations/signatures/sign-typed-data-v4.spec.ts +++ b/test/e2e/tests/confirmations/signatures/sign-typed-data-v4.spec.ts @@ -154,18 +154,13 @@ async function assertVerifiedResults(driver: Driver, publicAddress: string) { await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#signTypedDataV4Verify'); - const verifyResult = await driver.findElement('#signTypedDataV4Result'); + await driver.waitForSelector({ + css: '#signTypedDataV4Result', + text: '0xcd2f9c55840f5e1bcf61812e93c1932485b524ca673b36355482a4fbdf52f692684f92b4f4ab6f6c8572dacce46bd107da154be1c06939b855ecce57a1616ba71b', + }); + await driver.waitForSelector({ css: '#signTypedDataV4VerifyResult', text: publicAddress, }); - const verifyRecoverAddress = await driver.findElement( - '#signTypedDataV4VerifyResult', - ); - - assert.equal( - await verifyResult.getText(), - '0xcd2f9c55840f5e1bcf61812e93c1932485b524ca673b36355482a4fbdf52f692684f92b4f4ab6f6c8572dacce46bd107da154be1c06939b855ecce57a1616ba71b', - ); - assert.equal(await verifyRecoverAddress.getText(), publicAddress); } diff --git a/test/e2e/tests/confirmations/signatures/sign-typed-data.spec.ts b/test/e2e/tests/confirmations/signatures/sign-typed-data.spec.ts index 358d6b112cfc..a9a9dfd52ae9 100644 --- a/test/e2e/tests/confirmations/signatures/sign-typed-data.spec.ts +++ b/test/e2e/tests/confirmations/signatures/sign-typed-data.spec.ts @@ -116,18 +116,13 @@ async function assertVerifiedResults(driver: Driver, publicAddress: string) { await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#signTypedDataVerify'); - const result = await driver.findElement('#signTypedDataResult'); + await driver.waitForSelector({ + css: '#signTypedDataResult', + text: '0x32791e3c41d40dd5bbfb42e66cf80ca354b0869ae503ad61cd19ba68e11d4f0d2e42a5835b0bfd633596b6a7834ef7d36033633a2479dacfdb96bda360d51f451b', + }); + await driver.waitForSelector({ css: '#signTypedDataVerifyResult', text: publicAddress, }); - const verifyRecoverAddress = await driver.findElement( - '#signTypedDataVerifyResult', - ); - - assert.equal( - await result.getText(), - '0x32791e3c41d40dd5bbfb42e66cf80ca354b0869ae503ad61cd19ba68e11d4f0d2e42a5835b0bfd633596b6a7834ef7d36033633a2479dacfdb96bda360d51f451b', - ); - assert.equal(await verifyRecoverAddress.getText(), publicAddress); } diff --git a/test/e2e/tests/dapp-interactions/block-explorer.spec.js b/test/e2e/tests/dapp-interactions/block-explorer.spec.js index d9d21e92ee87..0b01aa65aab3 100644 --- a/test/e2e/tests/dapp-interactions/block-explorer.spec.js +++ b/test/e2e/tests/dapp-interactions/block-explorer.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { mockNetworkStateOld } = require('../../../stub/networks'); const { @@ -38,19 +37,17 @@ describe('Block Explorer', function () { await driver.clickElement({ text: 'View on explorer', tag: 'p' }); // Switch to block explorer - await driver.waitUntilXWindowHandles(2); - const windowHandles = await driver.getAllWindowHandles(); - await driver.switchToWindowWithTitle('E2E Test Page', windowHandles); - const body = await driver.findElement( - '[data-testid="empty-page-body"]', - ); + await driver.switchToWindowWithTitle('E2E Test Page'); // Verify block explorer - assert.equal(await body.getText(), 'Empty page by MetaMask'); - assert.equal( - await driver.getCurrentUrl(), - 'https://etherscan.io/address/0x5CfE73b6021E818B776b421B1c4Db2474086a7e1', - ); + await driver.waitForUrl({ + url: 'https://etherscan.io/address/0x5CfE73b6021E818B776b421B1c4Db2474086a7e1', + }); + + await driver.waitForSelector({ + text: 'Empty page by MetaMask', + tag: 'body', + }); }, ); }); @@ -82,10 +79,12 @@ describe('Block Explorer', function () { await driver.clickElement( '[data-testid="account-overview__asset-tab"]', ); - const [, tst] = await driver.findElements( - '[data-testid="multichain-token-list-button"]', - ); - await tst.click(); + + await driver.clickElement({ + text: 'TST', + tag: 'span', + }); + await driver.clickElement('[data-testid="asset-options__button"]'); await driver.clickElement({ text: 'View Asset in explorer', @@ -93,19 +92,17 @@ describe('Block Explorer', function () { }); // Switch to block explorer - await driver.waitUntilXWindowHandles(2); - const windowHandles = await driver.getAllWindowHandles(); - await driver.switchToWindowWithTitle('E2E Test Page', windowHandles); - const body = await driver.findElement( - '[data-testid="empty-page-body"]', - ); + await driver.switchToWindowWithTitle('E2E Test Page'); // Verify block explorer - assert.equal(await body.getText(), 'Empty page by MetaMask'); - assert.equal( - await driver.getCurrentUrl(), - 'https://etherscan.io/token/0x581c3C1A2A4EBDE2A0Df29B5cf4c116E42945947', - ); + await driver.waitForUrl({ + url: 'https://etherscan.io/token/0x581c3C1A2A4EBDE2A0Df29B5cf4c116E42945947', + }); + + await driver.waitForSelector({ + text: 'Empty page by MetaMask', + tag: 'body', + }); }, ); }); @@ -143,19 +140,17 @@ describe('Block Explorer', function () { }); // Switch to block explorer - await driver.waitUntilXWindowHandles(2); - const windowHandles = await driver.getAllWindowHandles(); - await driver.switchToWindowWithTitle('E2E Test Page', windowHandles); - const body = await driver.findElement( - '[data-testid="empty-page-body"]', - ); + await driver.switchToWindowWithTitle('E2E Test Page'); // Verify block explorer - assert.equal(await body.getText(), 'Empty page by MetaMask'); - assert.equal( - await driver.getCurrentUrl(), - 'https://etherscan.io/tx/0xe5e7b95690f584b8f66b33e31acc6184fea553fa6722d42486a59990d13d5fa2', - ); + await driver.waitForUrl({ + url: 'https://etherscan.io/tx/0xe5e7b95690f584b8f66b33e31acc6184fea553fa6722d42486a59990d13d5fa2', + }); + + await driver.waitForSelector({ + text: 'Empty page by MetaMask', + tag: 'body', + }); }, ); }); diff --git a/test/e2e/tests/dapp-interactions/encrypt-decrypt.spec.js b/test/e2e/tests/dapp-interactions/encrypt-decrypt.spec.js index 2f595138d0cf..fbf11b16cd40 100644 --- a/test/e2e/tests/dapp-interactions/encrypt-decrypt.spec.js +++ b/test/e2e/tests/dapp-interactions/encrypt-decrypt.spec.js @@ -195,7 +195,9 @@ describe('Encrypt Decrypt', function () { ); }); - it('should show balance correctly as Fiat', async function () { + it('should show balance correctly in native tokens', async function () { + // In component ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.container.js, after removing useNativeCurrencyAsPrimaryCurrency; + // We will display native balance in the confirm-encryption-public-key.component.js await withFixtures( { dapp: true, @@ -203,7 +205,7 @@ describe('Encrypt Decrypt', function () { .withPermissionControllerConnectedToTestDapp() .withPreferencesController({ preferences: { - useNativeCurrencyAsPrimaryCurrency: false, + showNativeTokenAsMainBalance: false, }, }) .build(), @@ -231,7 +233,7 @@ describe('Encrypt Decrypt', function () { const accountBalanceLabel = await driver.findElement( '.request-encryption-public-key__balance-value', ); - assert.equal(await accountBalanceLabel.getText(), '$42,500.00 USD'); + assert.equal(await accountBalanceLabel.getText(), '25 ETH'); }, ); }); diff --git a/test/e2e/tests/metrics/dapp-viewed.spec.js b/test/e2e/tests/metrics/dapp-viewed.spec.js index b9b4b08ca73e..78214685777e 100644 --- a/test/e2e/tests/metrics/dapp-viewed.spec.js +++ b/test/e2e/tests/metrics/dapp-viewed.spec.js @@ -14,11 +14,40 @@ const { MetaMetricsEventName, } = require('../../../../shared/constants/metametrics'); -async function mockedDappViewedEndpoint(mockServer) { +async function mockedDappViewedEndpointFirstVisit(mockServer) { return await mockServer .forPost('https://api.segment.io/v1/batch') .withJsonBodyIncluding({ - batch: [{ type: 'track', event: MetaMetricsEventName.DappViewed }], + batch: [ + { + type: 'track', + event: MetaMetricsEventName.DappViewed, + properties: { + is_first_visit: true, + }, + }, + ], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }); +} + +async function mockedDappViewedEndpointReVisit(mockServer) { + return await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [ + { + type: 'track', + event: MetaMetricsEventName.DappViewed, + properties: { + is_first_visit: false, + }, + }, + ], }) .thenCallback(() => { return { @@ -67,7 +96,7 @@ describe('Dapp viewed Event @no-mmi', function () { const validFakeMetricsId = 'fake-metrics-fd20'; it('is not sent when metametrics ID is not valid', async function () { async function mockSegment(mockServer) { - return [await mockedDappViewedEndpoint(mockServer)]; + return [await mockedDappViewedEndpointFirstVisit(mockServer)]; } await withFixtures( @@ -93,7 +122,7 @@ describe('Dapp viewed Event @no-mmi', function () { it('is sent when navigating to dapp with no account connected', async function () { async function mockSegment(mockServer) { - return [await mockedDappViewedEndpoint(mockServer)]; + return [await mockedDappViewedEndpointFirstVisit(mockServer)]; } await withFixtures( @@ -125,8 +154,8 @@ describe('Dapp viewed Event @no-mmi', function () { it('is sent when opening the dapp in a new tab with one account connected', async function () { async function mockSegment(mockServer) { return [ - await mockedDappViewedEndpoint(mockServer), - await mockedDappViewedEndpoint(mockServer), + await mockedDappViewedEndpointFirstVisit(mockServer), + await mockedDappViewedEndpointReVisit(mockServer), await mockPermissionApprovedEndpoint(mockServer), ]; } @@ -163,8 +192,8 @@ describe('Dapp viewed Event @no-mmi', function () { it('is sent when refreshing dapp with one account connected', async function () { async function mockSegment(mockServer) { return [ - await mockedDappViewedEndpoint(mockServer), - await mockedDappViewedEndpoint(mockServer), + await mockedDappViewedEndpointFirstVisit(mockServer), + await mockedDappViewedEndpointReVisit(mockServer), await mockPermissionApprovedEndpoint(mockServer), ]; } @@ -189,10 +218,9 @@ describe('Dapp viewed Event @no-mmi', function () { // refresh dapp await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.refresh(); - const events = await getEventPayloads(driver, mockedEndpoints); - // events are original dapp viewed, new dapp viewed when refresh, and permission approved + // events are original dapp viewed, navigate to dapp, new dapp viewed when refresh, new dapp viewed when navigate and permission approved const dappViewedEventProperties = events[1].properties; assert.equal(dappViewedEventProperties.is_first_visit, false); assert.equal(dappViewedEventProperties.number_of_accounts, 1); @@ -204,10 +232,10 @@ describe('Dapp viewed Event @no-mmi', function () { it('is sent when navigating to a connected dapp', async function () { async function mockSegment(mockServer) { return [ - await mockedDappViewedEndpoint(mockServer), - await mockedDappViewedEndpoint(mockServer), - await mockedDappViewedEndpoint(mockServer), - await mockedDappViewedEndpoint(mockServer), + await mockedDappViewedEndpointFirstVisit(mockServer), + await mockedDappViewedEndpointReVisit(mockServer), + await mockedDappViewedEndpointFirstVisit(mockServer), + await mockedDappViewedEndpointReVisit(mockServer), await mockPermissionApprovedEndpoint(mockServer), ]; } @@ -247,7 +275,7 @@ describe('Dapp viewed Event @no-mmi', function () { it('is sent when connecting dapp with two accounts', async function () { async function mockSegment(mockServer) { - return [await mockedDappViewedEndpoint(mockServer)]; + return [await mockedDappViewedEndpointFirstVisit(mockServer)]; } await withFixtures( { @@ -299,8 +327,8 @@ describe('Dapp viewed Event @no-mmi', function () { it('is sent when reconnect to a dapp that has been connected before', async function () { async function mockSegment(mockServer) { return [ - await mockedDappViewedEndpoint(mockServer), - await mockedDappViewedEndpoint(mockServer), + await mockedDappViewedEndpointFirstVisit(mockServer), + await mockedDappViewedEndpointReVisit(mockServer), ]; } diff --git a/test/e2e/tests/metrics/delete-metametrics-data.spec.ts b/test/e2e/tests/metrics/delete-metametrics-data.spec.ts new file mode 100644 index 000000000000..308ff8508d0a --- /dev/null +++ b/test/e2e/tests/metrics/delete-metametrics-data.spec.ts @@ -0,0 +1,246 @@ +import { strict as assert } from 'assert'; +import { MockedEndpoint, Mockttp } from 'mockttp'; +import { Suite } from 'mocha'; +import { + defaultGanacheOptions, + withFixtures, + getEventPayloads, + unlockWallet, +} from '../../helpers'; +import FixtureBuilder from '../../fixture-builder'; +import { Driver } from '../../webdriver/driver'; +import { TestSuiteArguments } from '../confirmations/transactions/shared'; +import { WebElementWithWaitForElementState } from '../../webdriver/types'; + +const selectors = { + accountOptionsMenuButton: '[data-testid="account-options-menu-button"]', + globalMenuSettingsButton: '[data-testid="global-menu-settings"]', + securityAndPrivacySettings: { text: 'Security & privacy', tag: 'div' }, + experimentalSettings: { text: 'Experimental', tag: 'div' }, + deletMetaMetricsSettings: '[data-testid="delete-metametrics-data-button"]', + deleteMetaMetricsDataButton: { + text: 'Delete MetaMetrics data', + tag: 'button', + }, + clearButton: { text: 'Clear', tag: 'button' }, + backButton: '[data-testid="settings-back-button"]', +}; + +/** + * mocks the segment api multiple times for specific payloads that we expect to + * see when these tests are run. In this case we are looking for + * 'Permissions Requested' and 'Permissions Received'. Do not use the constants + * from the metrics constants files, because if these change we want a strong + * indicator to our data team that the shape of data will change. + * + * @param mockServer + * @returns + */ +const mockSegment = async (mockServer: Mockttp) => { + return [ + await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [ + { type: 'track', event: 'Delete MetaMetrics Data Request Submitted' }, + ], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }), + await mockServer + .forPost('https://metametrics.metamask.test/regulations/sources/test') + .withHeaders({ 'Content-Type': 'application/vnd.segment.v1+json' }) + .withBodyIncluding( + JSON.stringify({ + regulationType: 'DELETE_ONLY', + subjectType: 'USER_ID', + subjectIds: ['fake-metrics-id'], + }), + ) + .thenCallback(() => ({ + statusCode: 200, + json: { data: { regulateId: 'fake-delete-regulation-id' } }, + })), + await mockServer + .forGet( + 'https://metametrics.metamask.test/regulations/fake-delete-regulation-id', + ) + .withHeaders({ 'Content-Type': 'application/vnd.segment.v1+json' }) + .thenCallback(() => ({ + statusCode: 200, + json: { + data: { + regulation: { + overallStatus: 'FINISHED', + }, + }, + }, + })), + ]; +}; +/** + * Scenarios: + * 1. Deletion while Metrics is Opted in. + * 2. Deletion while Metrics is Opted out. + * 3. Deletion when user never opted for metrics. + */ +describe('Delete MetaMetrics Data @no-mmi', function (this: Suite) { + it('while user has opted in for metrics tracking', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-id', + participateInMetaMetrics: true, + }) + .build(), + defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: mockSegment, + }, + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + await unlockWallet(driver); + + await driver.clickElement(selectors.accountOptionsMenuButton); + await driver.clickElement(selectors.globalMenuSettingsButton); + await driver.clickElement(selectors.securityAndPrivacySettings); + + await driver.findElement(selectors.deletMetaMetricsSettings); + await driver.clickElement(selectors.deleteMetaMetricsDataButton); + + // there is a race condition, where we need to wait before clicking clear button otherwise an error is thrown in the background + // we cannot wait for a UI conditon, so we a delay to mitigate this until another solution is found + await driver.delay(3000); + await driver.clickElementAndWaitToDisappear(selectors.clearButton); + + const deleteMetaMetricsDataButton = await driver.findElement( + selectors.deleteMetaMetricsDataButton, + ); + await ( + deleteMetaMetricsDataButton as WebElementWithWaitForElementState + ).waitForElementState('disabled'); + + const events = await getEventPayloads( + driver, + mockedEndpoints as MockedEndpoint[], + ); + assert.equal(events.length, 3); + assert.deepStrictEqual(events[0].properties, { + category: 'Settings', + locale: 'en', + chain_id: '0x539', + environment_type: 'fullscreen', + }); + + await driver.clickElementAndWaitToDisappear( + '.mm-box button[aria-label="Close"]', + ); + await driver.clickElement(selectors.accountOptionsMenuButton); + await driver.clickElement(selectors.globalMenuSettingsButton); + await driver.clickElement(selectors.securityAndPrivacySettings); + + const deleteMetaMetricsDataButtonRefreshed = + await driver.findClickableElement( + selectors.deleteMetaMetricsDataButton, + ); + assert.equal( + await deleteMetaMetricsDataButtonRefreshed.isEnabled(), + true, + 'Delete MetaMetrics data button is enabled', + ); + }, + ); + }); + it('while user has opted out for metrics tracking', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-id', + }) + .build(), + defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: mockSegment, + }, + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + await unlockWallet(driver); + + await driver.clickElement(selectors.accountOptionsMenuButton); + await driver.clickElement(selectors.globalMenuSettingsButton); + await driver.clickElement(selectors.securityAndPrivacySettings); + + await driver.findElement(selectors.deletMetaMetricsSettings); + await driver.clickElement(selectors.deleteMetaMetricsDataButton); + + // there is a race condition, where we need to wait before clicking clear button otherwise an error is thrown in the background + // we cannot wait for a UI conditon, so we a delay to mitigate this until another solution is found + await driver.delay(3000); + await driver.clickElementAndWaitToDisappear(selectors.clearButton); + + const deleteMetaMetricsDataButton = await driver.findElement( + selectors.deleteMetaMetricsDataButton, + ); + await ( + deleteMetaMetricsDataButton as WebElementWithWaitForElementState + ).waitForElementState('disabled'); + + const events = await getEventPayloads( + driver, + mockedEndpoints as MockedEndpoint[], + ); + assert.equal(events.length, 2); + + await driver.clickElementAndWaitToDisappear( + '.mm-box button[aria-label="Close"]', + ); + await driver.clickElement(selectors.accountOptionsMenuButton); + await driver.clickElement(selectors.globalMenuSettingsButton); + await driver.clickElement(selectors.securityAndPrivacySettings); + + const deleteMetaMetricsDataButtonRefreshed = await driver.findElement( + selectors.deleteMetaMetricsDataButton, + ); + await ( + deleteMetaMetricsDataButtonRefreshed as WebElementWithWaitForElementState + ).waitForElementState('disabled'); + }, + ); + }); + it('when the user has never opted in for metrics', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: mockSegment, + }, + async ({ driver }: { driver: Driver }) => { + await unlockWallet(driver); + + await driver.clickElement(selectors.accountOptionsMenuButton); + await driver.clickElement(selectors.globalMenuSettingsButton); + await driver.clickElement(selectors.securityAndPrivacySettings); + await driver.findElement(selectors.deletMetaMetricsSettings); + + const deleteMetaMetricsDataButton = await driver.findElement( + selectors.deleteMetaMetricsDataButton, + ); + assert.equal( + await deleteMetaMetricsDataButton.isEnabled(), + false, + 'Delete MetaMetrics data button is disabled', + ); + }, + ); + }); +}); diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index c133de6128ca..8d8c8c1ae895 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -38,6 +38,7 @@ "showAccountBanner": true, "trezorModel": null, "onboardingDate": null, + "lastViewedUserSurvey": null, "newPrivacyPolicyToastClickedOrClosed": "boolean", "newPrivacyPolicyToastShownDate": "number", "hadAdvancedGasFeesSetPriorToMigration92_3": false, @@ -62,17 +63,13 @@ "bridgeState": { "bridgeFeatureFlags": { "extensionSupport": "boolean", - "srcNetworkAllowlist": { - "0": "string", - "1": "string", - "2": "string" - }, - "destNetworkAllowlist": { - "0": "string", - "1": "string", - "2": "string" - } - } + "srcNetworkAllowlist": { "0": "string", "1": "string", "2": "string" }, + "destNetworkAllowlist": { "0": "string", "1": "string", "2": "string" } + }, + "destTokens": {}, + "destTopAssets": {}, + "srcTokens": {}, + "srcTopAssets": {} } }, "CronjobController": { "jobs": "object" }, @@ -180,6 +177,7 @@ "permissionActivityLog": "object" }, "PreferencesController": { + "selectedAddress": "string", "useBlockie": false, "useNonceField": false, "usePhishDetect": true, @@ -211,12 +209,14 @@ "showFiatInTestnets": false, "showTestNetworks": false, "smartTransactionsOptInStatus": false, - "useNativeCurrencyAsPrimaryCurrency": true, + "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, + "showMultiRpcModal": "boolean", "isRedesignedConfirmationsDeveloperEnabled": "boolean", "redesignedConfirmationsEnabled": true, "redesignedTransactionsEnabled": "boolean", - "showMultiRpcModal": "boolean" + "showMultiRpcModal": "boolean", + "shouldShowAggregatedBalancePopover": "boolean" }, "ipfsGateway": "string", "isIpfsGatewayEnabled": "boolean", @@ -228,8 +228,7 @@ "useExternalNameSources": "boolean", "useTransactionSimulations": true, "enableMV3TimestampSave": true, - "useExternalServices": "boolean", - "selectedAddress": "string" + "useExternalServices": "boolean" }, "QueuedRequestController": { "queuedRequestCount": 0 }, "SelectedNetworkController": { "domains": "object" }, @@ -305,8 +304,10 @@ }, "TxController": { "methodData": "object", + "submitHistory": "object", "transactions": "object", - "lastFetchedBlockNumbers": "object" + "lastFetchedBlockNumbers": "object", + "submitHistory": "object" }, "UserOperationController": { "userOperations": "object" }, "UserStorageController": { diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index dfee54fbd6cb..b1131ec4e7a2 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -17,7 +17,6 @@ "internalAccounts": { "accounts": "object", "selectedAccount": "string" }, "transactions": "object", "networkConfigurations": "object", - "networkConfigurationsByChainId": "object", "addressBook": "object", "confirmationExchangeRates": {}, "pendingTokens": "object", @@ -32,12 +31,14 @@ "showFiatInTestnets": false, "showTestNetworks": false, "smartTransactionsOptInStatus": false, - "useNativeCurrencyAsPrimaryCurrency": true, + "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, + "showMultiRpcModal": "boolean", "isRedesignedConfirmationsDeveloperEnabled": "boolean", "redesignedConfirmationsEnabled": true, "redesignedTransactionsEnabled": "boolean", - "showMultiRpcModal": "boolean" + "showMultiRpcModal": "boolean", + "shouldShowAggregatedBalancePopover": "boolean" }, "firstTimeFlowType": "import", "completedOnboarding": true, @@ -70,6 +71,7 @@ "showAccountBanner": true, "trezorModel": null, "onboardingDate": null, + "lastViewedUserSurvey": null, "newPrivacyPolicyToastClickedOrClosed": "boolean", "newPrivacyPolicyToastShownDate": "number", "hadAdvancedGasFeesSetPriorToMigration92_3": false, @@ -95,7 +97,9 @@ "status": "available" } }, + "networkConfigurationsByChainId": "object", "keyrings": "object", + "selectedAddress": "string", "useNonceField": false, "usePhishDetect": true, "dismissSeedBackUpReminder": true, @@ -127,16 +131,15 @@ "useTransactionSimulations": true, "enableMV3TimestampSave": true, "useExternalServices": "boolean", - "selectedAddress": "string", "metaMetricsId": "fake-metrics-id", "marketingCampaignCookieId": null, - "metaMetricsDataDeletionId": null, - "metaMetricsDataDeletionTimestamp": 0, "eventsBeforeMetricsOptIn": "object", "traits": "object", "previousUserTraits": "object", "fragments": "object", "segmentApiCalls": "object", + "metaMetricsDataDeletionId": null, + "metaMetricsDataDeletionTimestamp": 0, "currentCurrency": "usd", "alertEnabledness": { "unconnectedAccount": true, "web3ShimUsage": true }, "unconnectedAccountAlertShownOrigins": "object", @@ -177,6 +180,7 @@ "logs": "object", "methodData": "object", "lastFetchedBlockNumbers": "object", + "submitHistory": "object", "fiatCurrency": "usd", "rates": { "btc": { "conversionDate": 0, "conversionRate": "0" } }, "cryptocurrencies": ["btc"], @@ -194,6 +198,7 @@ "isSignedIn": "boolean", "isProfileSyncingEnabled": null, "isProfileSyncingUpdateLoading": "boolean", + "submitHistory": "object", "subscriptionAccountsSeen": "object", "isMetamaskNotificationsFeatureSeen": "boolean", "isNotificationServicesEnabled": "boolean", @@ -248,17 +253,13 @@ "bridgeState": { "bridgeFeatureFlags": { "extensionSupport": "boolean", - "srcNetworkAllowlist": { - "0": "string", - "1": "string", - "2": "string" - }, - "destNetworkAllowlist": { - "0": "string", - "1": "string", - "2": "string" - } - } + "srcNetworkAllowlist": { "0": "string", "1": "string", "2": "string" }, + "destNetworkAllowlist": { "0": "string", "1": "string", "2": "string" } + }, + "destTokens": {}, + "destTopAssets": {}, + "srcTokens": {}, + "srcTopAssets": {} }, "ensEntries": "object", "ensResolutionsByAddress": "object", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index b6354922add0..f40b2687316b 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -112,11 +112,12 @@ "showFiatInTestnets": false, "showTestNetworks": false, "smartTransactionsOptInStatus": false, - "useNativeCurrencyAsPrimaryCurrency": true, + "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, "showConfirmationAdvancedDetails": false, "isRedesignedConfirmationsDeveloperEnabled": "boolean", - "showMultiRpcModal": "boolean" + "showMultiRpcModal": "boolean", + "shouldShowAggregatedBalancePopover": "boolean" }, "selectedAddress": "string", "theme": "light", @@ -152,7 +153,11 @@ "1": "string", "2": "string" } - } + }, + "destTokens": {}, + "destTopAssets": {}, + "srcTokens": {}, + "srcTopAssets": {} } }, "SubjectMetadataController": { "subjectMetadata": "object" }, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index 51910b2057a7..3c692fa59405 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -112,11 +112,12 @@ "showFiatInTestnets": false, "showTestNetworks": false, "smartTransactionsOptInStatus": false, - "useNativeCurrencyAsPrimaryCurrency": true, + "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, "showConfirmationAdvancedDetails": false, "isRedesignedConfirmationsDeveloperEnabled": "boolean", - "showMultiRpcModal": "boolean" + "showMultiRpcModal": "boolean", + "shouldShowAggregatedBalancePopover": "boolean" }, "selectedAddress": "string", "theme": "light", @@ -161,7 +162,11 @@ "1": "string", "2": "string" } - } + }, + "destTokens": {}, + "destTopAssets": {}, + "srcTokens": {}, + "srcTopAssets": {} } }, "TransactionController": { "transactions": "object" }, diff --git a/test/e2e/tests/phishing-controller/phishing-detection.spec.js b/test/e2e/tests/phishing-controller/phishing-detection.spec.js index 444c98900026..ac9a6d8461d2 100644 --- a/test/e2e/tests/phishing-controller/phishing-detection.spec.js +++ b/test/e2e/tests/phishing-controller/phishing-detection.spec.js @@ -208,10 +208,9 @@ describe('Phishing Detection', function () { await driver.findElement({ text: `Empty page by ${BlockProvider.MetaMask}`, }); - assert.equal( - await driver.getCurrentUrl(), - `https://github.com/MetaMask/eth-phishing-detect/issues/new?title=[Legitimate%20Site%20Blocked]%20127.0.0.1&body=http%3A%2F%2F127.0.0.1%2F`, - ); + await driver.waitForUrl({ + url: `https://github.com/MetaMask/eth-phishing-detect/issues/new?title=[Legitimate%20Site%20Blocked]%20127.0.0.1&body=http%3A%2F%2F127.0.0.1%2F`, + }); }, ); }); @@ -445,11 +444,12 @@ describe('Phishing Detection', function () { await driver.openNewURL(blockedUrl); // check that the redirect was ultimately _not_ followed and instead // went to our "MetaMask Phishing Detection" site - assert.equal( - await driver.getCurrentUrl(), - // http://localhost:9999 is the Phishing Warning page - `http://localhost:9999/#hostname=${blocked}&href=http%3A%2F%2F${blocked}%3A${port}%2F`, - ); + + await driver.waitForUrl({ + url: + // http://localhost:9999 is the Phishing Warning page + `http://localhost:9999/#hostname=${blocked}&href=http%3A%2F%2F${blocked}%3A${port}%2F`, + }); }); } }); diff --git a/test/e2e/tests/portfolio/portfolio-site.spec.js b/test/e2e/tests/portfolio/portfolio-site.spec.js index ff4c7c363a71..cba9c0452522 100644 --- a/test/e2e/tests/portfolio/portfolio-site.spec.js +++ b/test/e2e/tests/portfolio/portfolio-site.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { withFixtures, unlockWallet, @@ -42,10 +41,9 @@ describe('Portfolio site', function () { await driver.switchToWindowWithTitle('E2E Test Page', windowHandles); // Verify site - const currentUrl = await driver.getCurrentUrl(); - const expectedUrl = - 'https://portfolio.metamask.io/?metamaskEntry=ext_portfolio_button&metametricsId=null&metricsEnabled=false&marketingEnabled=false'; - assert.equal(currentUrl, expectedUrl); + await driver.waitForUrl({ + url: 'https://portfolio.metamask.io/?metamaskEntry=ext_portfolio_button&metametricsId=null&metricsEnabled=false&marketingEnabled=false', + }); }, ); }); diff --git a/test/e2e/tests/settings/4byte-directory.spec.js b/test/e2e/tests/settings/4byte-directory.spec.js index 2874118c3a28..483ff1e0149a 100644 --- a/test/e2e/tests/settings/4byte-directory.spec.js +++ b/test/e2e/tests/settings/4byte-directory.spec.js @@ -1,12 +1,10 @@ -const { strict: assert } = require('assert'); const FixtureBuilder = require('../../fixture-builder'); const { - withFixtures, + logInWithBalanceValidation, openDapp, - unlockWallet, openMenuSafe, - largeDelayMs, - veryLargeDelayMs, + unlockWallet, + withFixtures, WINDOW_TITLES, } = require('../../helpers'); const { SMART_CONTRACTS } = require('../../seeder/smart-contracts'); @@ -27,27 +25,23 @@ describe('4byte setting', function () { const contractAddress = await contractRegistry.getContractAddress( smartContract, ); - await unlockWallet(driver); + await logInWithBalanceValidation(driver); // deploy contract await openDapp(driver, contractAddress); // wait for deployed contract, calls and confirms a contract method where ETH is sent - await driver.delay(largeDelayMs); await driver.clickElement('#depositButton'); - await driver.waitForSelector({ - css: 'span', - text: 'Deposit initiated', - }); - - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - const actionElement = await driver.waitForSelector({ - css: '.confirm-page-container-summary__action__name', + await driver.waitForSelector({ + tag: 'span', text: 'Deposit', }); - assert.equal(await actionElement.getText(), 'DEPOSIT'); + await driver.assertElementNotPresent({ + tag: 'span', + text: 'Contract interaction', + }); }, ); }); @@ -83,28 +77,18 @@ describe('4byte setting', function () { await openDapp(driver, contractAddress); // wait for deployed contract, calls and confirms a contract method where ETH is sent - await driver.findClickableElement('#depositButton'); await driver.clickElement('#depositButton'); - await driver.waitForSelector({ - css: 'span', - text: 'Deposit initiated', - }); - - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - const contractInteraction = 'Contract interaction'; - const actionElement = await driver.waitForSelector({ - css: '.confirm-page-container-summary__action__name', - text: contractInteraction, + + await driver.assertElementNotPresent({ + tag: 'span', + text: 'Deposit', + }); + await driver.waitForSelector({ + tag: 'span', + text: 'Contract interaction', }); - // We add a delay here to wait for any potential UI changes - await driver.delay(veryLargeDelayMs); - // css text-transform: uppercase is applied to the text - assert.equal( - await actionElement.getText(), - contractInteraction.toUpperCase(), - ); }, ); }); diff --git a/test/e2e/tests/settings/account-token-list.spec.js b/test/e2e/tests/settings/account-token-list.spec.js index 0fae71ae1d85..9e4822d0dbbc 100644 --- a/test/e2e/tests/settings/account-token-list.spec.js +++ b/test/e2e/tests/settings/account-token-list.spec.js @@ -3,6 +3,7 @@ const { withFixtures, defaultGanacheOptions, logInWithBalanceValidation, + unlockWallet, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); @@ -41,26 +42,18 @@ describe('Settings', function () { it('Should match the value of token list item and account list item for fiat conversion', async function () { await withFixtures( { - fixtures: new FixtureBuilder().withConversionRateEnabled().build(), + fixtures: new FixtureBuilder() + .withConversionRateEnabled() + .withShowFiatTestnetEnabled() + .withPreferencesControllerShowNativeTokenAsMainBalanceDisabled() + .build(), ganacheOptions: defaultGanacheOptions, title: this.test.fullTitle(), }, - async ({ driver, ganacheServer }) => { - await logInWithBalanceValidation(driver, ganacheServer); - - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); - await driver.clickElement({ text: 'Settings', tag: 'div' }); - await driver.clickElement({ - text: 'General', - tag: 'div', - }); - await driver.clickElement({ text: 'Fiat', tag: 'label' }); + async ({ driver }) => { + await unlockWallet(driver); - await driver.clickElement( - '.settings-page__header__title-container__close-button', - ); + await driver.clickElement('[data-testid="popover-close"]'); await driver.clickElement( '[data-testid="account-overview__asset-tab"]', ); @@ -70,7 +63,6 @@ describe('Settings', function () { ); await driver.delay(1000); assert.equal(await tokenListAmount.getText(), '$42,500.00\nUSD'); - await driver.clickElement('[data-testid="account-menu-icon"]'); const accountTokenValue = await driver.waitForSelector( '.multichain-account-list-item .multichain-account-list-item__asset', diff --git a/test/e2e/tests/settings/address-book.spec.js b/test/e2e/tests/settings/address-book.spec.js index c784ce3daa3b..e81bd7c544aa 100644 --- a/test/e2e/tests/settings/address-book.spec.js +++ b/test/e2e/tests/settings/address-book.spec.js @@ -5,6 +5,7 @@ const { withFixtures, logInWithBalanceValidation, openActionMenuAndStartSendFlow, + openMenuSafe, unlockWallet, } = require('../../helpers'); const { shortenAddress } = require('../../../../ui/helpers/utils/util'); @@ -92,10 +93,8 @@ describe('Address Book', function () { }, async ({ driver }) => { await unlockWallet(driver); + await openMenuSafe(driver); - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); await driver.clickElement({ text: 'Settings', tag: 'div' }); await driver.clickElement({ text: 'Contacts', tag: 'div' }); await driver.clickElement({ @@ -159,9 +158,8 @@ describe('Address Book', function () { async ({ driver }) => { await unlockWallet(driver); - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); + await openMenuSafe(driver); + await driver.clickElement({ text: 'Settings', tag: 'div' }); await driver.clickElement({ text: 'Contacts', tag: 'div' }); diff --git a/test/e2e/tests/settings/auto-lock.spec.js b/test/e2e/tests/settings/auto-lock.spec.js index bded29fa5f39..7d7a159d4a1b 100644 --- a/test/e2e/tests/settings/auto-lock.spec.js +++ b/test/e2e/tests/settings/auto-lock.spec.js @@ -1,8 +1,9 @@ const { strict: assert } = require('assert'); const { defaultGanacheOptions, - withFixtures, + openMenuSafe, unlockWallet, + withFixtures, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); @@ -17,9 +18,7 @@ describe('Auto-Lock Timer', function () { async ({ driver }) => { await unlockWallet(driver); // Set Auto Lock Timer - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); + await openMenuSafe(driver); await driver.clickElement({ text: 'Settings', tag: 'div' }); await driver.clickElement({ text: 'Advanced', tag: 'div' }); const sixSecsInMins = '0.1'; diff --git a/test/e2e/tests/settings/backup-restore.spec.js b/test/e2e/tests/settings/backup-restore.spec.js index 02a36b638884..41186e70cd23 100644 --- a/test/e2e/tests/settings/backup-restore.spec.js +++ b/test/e2e/tests/settings/backup-restore.spec.js @@ -2,10 +2,11 @@ const { strict: assert } = require('assert'); const { promises: fs } = require('fs'); const { - defaultGanacheOptions, - withFixtures, createDownloadFolder, + defaultGanacheOptions, + openMenuSafe, unlockWallet, + withFixtures, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); @@ -31,10 +32,10 @@ const getBackupJson = async () => { try { const backup = `${downloadsFolder}/${userDataFileName}`; - await fs.access(backup); const contents = await fs.readFile(backup); return JSON.parse(contents.toString()); } catch (e) { + console.log('Error reading the backup file', e); return null; } }; @@ -56,9 +57,8 @@ describe('Backup and Restore', function () { await unlockWallet(driver); // Download user settings - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); + await openMenuSafe(driver); + await driver.clickElement({ text: 'Settings', tag: 'div' }); await driver.clickElement({ text: 'Advanced', tag: 'div' }); await driver.clickElement('[data-testid="export-data-button"]'); diff --git a/test/e2e/tests/settings/change-language.spec.ts b/test/e2e/tests/settings/change-language.spec.ts index aafb36059c9b..1bd9915a33da 100644 --- a/test/e2e/tests/settings/change-language.spec.ts +++ b/test/e2e/tests/settings/change-language.spec.ts @@ -16,8 +16,8 @@ const selectors = { ethOverviewSend: '[data-testid="eth-overview-send"]', ensInput: '[data-testid="ens-input"]', nftsTab: '[data-testid="account-overview__nfts-tab"]', - labelSpanish: { tag: 'span', text: 'Idioma actual' }, - currentLanguageLabel: { tag: 'span', text: 'Current language' }, + labelSpanish: { tag: 'p', text: 'Idioma actual' }, + currentLanguageLabel: { tag: 'p', text: 'Current language' }, advanceText: { text: 'Avanceret', tag: 'div' }, waterText: '[placeholder="Søg"]', headerTextDansk: { text: 'Indstillinger', tag: 'h3' }, diff --git a/test/e2e/tests/settings/clear-activity.spec.js b/test/e2e/tests/settings/clear-activity.spec.js index 34a23aecb173..781363bf42d3 100644 --- a/test/e2e/tests/settings/clear-activity.spec.js +++ b/test/e2e/tests/settings/clear-activity.spec.js @@ -1,8 +1,9 @@ const { strict: assert } = require('assert'); const { defaultGanacheOptions, - withFixtures, + openMenuSafe, unlockWallet, + withFixtures, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); @@ -38,9 +39,8 @@ describe('Clear account activity', function () { }); // Clear activity and nonce data - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); + await openMenuSafe(driver); + await driver.clickElement({ text: 'Settings', tag: 'div' }); await driver.clickElement({ text: 'Advanced', tag: 'div' }); await driver.clickElement({ diff --git a/test/e2e/tests/settings/ipfs-ens-resolution.spec.js b/test/e2e/tests/settings/ipfs-ens-resolution.spec.js index e706a3f41e31..450ec649d809 100644 --- a/test/e2e/tests/settings/ipfs-ens-resolution.spec.js +++ b/test/e2e/tests/settings/ipfs-ens-resolution.spec.js @@ -1,4 +1,9 @@ -const { withFixtures, tinyDelayMs, unlockWallet } = require('../../helpers'); +const { + openMenuSafe, + tinyDelayMs, + unlockWallet, + withFixtures, +} = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); describe('Settings', function () { @@ -64,9 +69,8 @@ describe('Settings', function () { await unlockWallet(driver); // goes to the settings screen - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); + await openMenuSafe(driver); + await driver.clickElement({ text: 'Settings', tag: 'div' }); await driver.clickElement({ text: 'Security & privacy', tag: 'div' }); diff --git a/test/e2e/tests/settings/ipfs-toggle.spec.js b/test/e2e/tests/settings/ipfs-toggle.spec.js index 045c51496853..cd078b587d54 100644 --- a/test/e2e/tests/settings/ipfs-toggle.spec.js +++ b/test/e2e/tests/settings/ipfs-toggle.spec.js @@ -1,8 +1,9 @@ const { strict: assert } = require('assert'); const { - withFixtures, defaultGanacheOptions, + openMenuSafe, unlockWallet, + withFixtures, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); const { SMART_CONTRACTS } = require('../../seeder/smart-contracts'); @@ -20,10 +21,8 @@ describe('Settings', function () { }, async ({ driver }) => { await unlockWallet(driver); + await openMenuSafe(driver); - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); await driver.clickElement({ text: 'Settings', tag: 'div' }); await driver.clickElement({ text: 'Security & privacy', tag: 'div' }); diff --git a/test/e2e/tests/settings/localization.spec.js b/test/e2e/tests/settings/localization.spec.js index 707cc120e578..57dbfd5f68cf 100644 --- a/test/e2e/tests/settings/localization.spec.js +++ b/test/e2e/tests/settings/localization.spec.js @@ -17,6 +17,7 @@ describe('Localization', function () { .withPreferencesController({ preferences: { showFiatInTestnets: true, + showNativeTokenAsMainBalance: false, }, }) .build(), @@ -26,15 +27,13 @@ describe('Localization', function () { async ({ driver }) => { await unlockWallet(driver); - const secondaryBalance = await driver.findElement( - '[data-testid="eth-overview__secondary-currency"]', + // After the removal of displaying secondary currency in coin-overview.tsx, we will test localization on main balance with showNativeTokenAsMainBalance = false + const primaryBalance = await driver.findElement( + '[data-testid="eth-overview__primary-currency"]', ); - const secondaryBalanceText = await secondaryBalance.getText(); - const [fiatAmount, fiatUnit] = secondaryBalanceText - .trim() - .split(/\s+/u); - assert.ok(fiatAmount.startsWith('₱')); - assert.equal(fiatUnit, 'PHP'); + const balanceText = await primaryBalance.getText(); + assert.ok(balanceText.startsWith('₱')); + assert.ok(balanceText.endsWith('PHP')); }, ); }); diff --git a/test/e2e/tests/settings/settings-general.spec.js b/test/e2e/tests/settings/settings-general.spec.js index dafe32ba9bea..5e75c857a7f8 100644 --- a/test/e2e/tests/settings/settings-general.spec.js +++ b/test/e2e/tests/settings/settings-general.spec.js @@ -1,8 +1,9 @@ const { strict: assert } = require('assert'); const { defaultGanacheOptions, - withFixtures, + openMenuSafe, unlockWallet, + withFixtures, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); @@ -18,9 +19,8 @@ describe('Settings', function () { await unlockWallet(driver); // goes to the settings screen - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); + await openMenuSafe(driver); + await driver.clickElement({ text: 'Settings', tag: 'div' }); // finds the jazzicon toggle turned on diff --git a/test/e2e/tests/settings/settings-search.spec.js b/test/e2e/tests/settings/settings-search.spec.js index 7a9207dcbf9f..fb67fbffd23b 100644 --- a/test/e2e/tests/settings/settings-search.spec.js +++ b/test/e2e/tests/settings/settings-search.spec.js @@ -9,7 +9,7 @@ const FixtureBuilder = require('../../fixture-builder'); describe('Settings Search', function () { const settingsSearch = { - general: 'Primary currency', + general: 'Show native token as main balance', advanced: 'State logs', contacts: 'Contacts', security: 'Reveal Secret', diff --git a/test/e2e/tests/settings/show-native-as-main-balance.spec.ts b/test/e2e/tests/settings/show-native-as-main-balance.spec.ts new file mode 100644 index 000000000000..d81e590cc5db --- /dev/null +++ b/test/e2e/tests/settings/show-native-as-main-balance.spec.ts @@ -0,0 +1,240 @@ +import { strict as assert } from 'assert'; +import { expect } from '@playwright/test'; +import { + withFixtures, + defaultGanacheOptions, + logInWithBalanceValidation, + unlockWallet, + getEventPayloads, +} from '../../helpers'; +import { MockedEndpoint, Mockttp } from '../../mock-e2e'; +import { Driver } from '../../webdriver/driver'; + +import FixtureBuilder from '../../fixture-builder'; + +async function mockSegment(mockServer: Mockttp) { + return [ + await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [{ type: 'track', event: 'Show native token as main balance' }], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }), + ]; +} + +describe('Settings: Show native token as main balance', function () { + it('Should show balance in crypto when toggle is on', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().withConversionRateDisabled().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer: unknown; + }) => { + await logInWithBalanceValidation(driver, ganacheServer); + + await driver.clickElement( + '[data-testid="account-overview__asset-tab"]', + ); + const tokenValue = '25 ETH'; + const tokenListAmount = await driver.findElement( + '[data-testid="multichain-token-list-item-value"]', + ); + await driver.waitForNonEmptyElement(tokenListAmount); + assert.equal(await tokenListAmount.getText(), tokenValue); + }, + ); + }); + + it('Should show balance in fiat when toggle is OFF', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withConversionRateEnabled() + .withPreferencesControllerShowNativeTokenAsMainBalanceDisabled() + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await unlockWallet(driver); + + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + + await driver.clickElement({ + text: 'Advanced', + tag: 'div', + }); + await driver.clickElement('.show-fiat-on-testnets-toggle'); + + await driver.delay(1000); + + await driver.clickElement( + '.settings-page__header__title-container__close-button', + ); + // close popover + await driver.clickElement('[data-testid="popover-close"]'); + + await driver.clickElement( + '[data-testid="account-overview__asset-tab"]', + ); + + const tokenListAmount = await driver.findElement( + '.eth-overview__primary-container', + ); + assert.equal(await tokenListAmount.getText(), '$42,500.00\nUSD'); + }, + ); + }); + + it('Should not show popover twice', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withConversionRateEnabled() + .withPreferencesControllerShowNativeTokenAsMainBalanceDisabled() + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await unlockWallet(driver); + + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + + await driver.clickElement({ + text: 'Advanced', + tag: 'div', + }); + await driver.clickElement('.show-fiat-on-testnets-toggle'); + + await driver.delay(1000); + + await driver.clickElement( + '.settings-page__header__title-container__close-button', + ); + // close popover for the first time + await driver.clickElement('[data-testid="popover-close"]'); + // go to setting and back to home page and make sure popover is not shown again + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + // close setting + await driver.clickElement( + '.settings-page__header__title-container__close-button', + ); + // assert popover does not exist + await driver.assertElementNotPresent('[data-testid="popover-close"]'); + }, + ); + }); + + it('Should Successfully track the event when toggle is turned off', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-fd20', + participateInMetaMetrics: true, + }) + .build(), + defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: mockSegment, + }, + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: { + driver: Driver; + mockedEndpoint: MockedEndpoint[]; + }) => { + await unlockWallet(driver); + + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + + await driver.clickElement({ text: 'Settings', tag: 'div' }); + await driver.clickElement({ + text: 'General', + tag: 'div', + }); + await driver.clickElement('.show-native-token-as-main-balance'); + + const events = await getEventPayloads(driver, mockedEndpoints); + expect(events[0].properties).toMatchObject({ + show_native_token_as_main_balance: false, + category: 'Settings', + locale: 'en', + chain_id: '0x539', + environment_type: 'fullscreen', + }); + }, + ); + }); + + it('Should Successfully track the event when toggle is turned on', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-fd20', + participateInMetaMetrics: true, + }) + .withPreferencesControllerShowNativeTokenAsMainBalanceDisabled() + .build(), + defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: mockSegment, + }, + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: { + driver: Driver; + mockedEndpoint: MockedEndpoint[]; + }) => { + await unlockWallet(driver); + + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + + await driver.clickElement({ text: 'Settings', tag: 'div' }); + await driver.clickElement({ + text: 'General', + tag: 'div', + }); + await driver.clickElement('.show-native-token-as-main-balance'); + + const events = await getEventPayloads(driver, mockedEndpoints); + expect(events[0].properties).toMatchObject({ + show_native_token_as_main_balance: true, + category: 'Settings', + locale: 'en', + chain_id: '0x539', + environment_type: 'fullscreen', + }); + }, + ); + }); +}); diff --git a/test/e2e/tests/settings/state-logs.spec.js b/test/e2e/tests/settings/state-logs.spec.js index 07e9b4744900..3b9e568ee6bc 100644 --- a/test/e2e/tests/settings/state-logs.spec.js +++ b/test/e2e/tests/settings/state-logs.spec.js @@ -1,10 +1,11 @@ const { strict: assert } = require('assert'); const { promises: fs } = require('fs'); const { - defaultGanacheOptions, - withFixtures, createDownloadFolder, + defaultGanacheOptions, + openMenuSafe, unlockWallet, + withFixtures, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); @@ -38,9 +39,8 @@ describe('State logs', function () { await unlockWallet(driver); // Download state logs - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); + await openMenuSafe(driver); + await driver.clickElement({ text: 'Settings', tag: 'div' }); await driver.clickElement({ text: 'Advanced', tag: 'div' }); await driver.clickElement({ diff --git a/test/e2e/tests/settings/terms-of-use.spec.js b/test/e2e/tests/settings/terms-of-use.spec.js index 87c2c2d0018d..ee314ee95600 100644 --- a/test/e2e/tests/settings/terms-of-use.spec.js +++ b/test/e2e/tests/settings/terms-of-use.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { defaultGanacheOptions, withFixtures, @@ -26,12 +25,7 @@ describe('Terms of use', function () { const acceptTerms = '[data-testid="terms-of-use-accept-button"]'; await driver.clickElement('[data-testid="popover-scroll-button"]'); await driver.clickElement('[data-testid="terms-of-use-checkbox"]'); - await driver.clickElement(acceptTerms); - - // check modal is no longer shown - await driver.assertElementNotPresent(acceptTerms); - const termsExists = await driver.isElementPresent(acceptTerms); - assert.equal(termsExists, false, 'terms of use should not be shown'); + await driver.clickElementAndWaitToDisappear(acceptTerms); }, ); }); diff --git a/test/e2e/tests/survey/survey.spec.js b/test/e2e/tests/survey/survey.spec.js new file mode 100644 index 000000000000..66a494297e43 --- /dev/null +++ b/test/e2e/tests/survey/survey.spec.js @@ -0,0 +1,62 @@ +const { strict: assert } = require('assert'); +const { + withFixtures, + unlockWallet, + defaultGanacheOptions, +} = require('../../helpers'); +const FixtureBuilder = require('../../fixture-builder'); + +describe('Test Survey', function () { + it('should show 2 surveys, and then none', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPreferencesController() + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-id-power-user', + participateInMetaMetrics: true, + }) + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test.fullTitle(), + }, + async ({ driver }) => { + async function checkForToast(surveyId) { + await driver.findElement('[data-testid="survey-toast"]'); + const surveyElement = await driver.findElement( + '[data-testid="survey-toast-banner-base"] p', + ); + const surveyText = await surveyElement.getText(); + assert.equal( + surveyText, + `Test survey ${surveyId}`, + `Survey text should be "Test survey ${surveyId}"`, + ); + await driver.clickElement( + '[data-testid="survey-toast-banner-base"] [aria-label="Close"]', + ); + } + + async function checkForNoToast() { + const surveyToastAfterRefresh = + await driver.isElementPresentAndVisible( + '[data-testid="survey-toast"]', + ); + assert.equal( + surveyToastAfterRefresh, + false, + 'Survey should not be visible after refresh', + ); + } + + await unlockWallet(driver); + await checkForToast(1); + await driver.refresh(); + await checkForToast(2); + await driver.refresh(); + await checkForNoToast(); + }, + ); + }); +}); diff --git a/test/e2e/tests/tokens/import-tokens.spec.js b/test/e2e/tests/tokens/import-tokens.spec.js index 890353236912..a1eb2782f9db 100644 --- a/test/e2e/tests/tokens/import-tokens.spec.js +++ b/test/e2e/tests/tokens/import-tokens.spec.js @@ -37,7 +37,7 @@ describe('Import flow', function () { it('allows importing multiple tokens from search', async function () { await withFixtures( { - fixtures: new FixtureBuilder().build(), + fixtures: new FixtureBuilder().withNetworkControllerOnMainnet().build(), ganacheOptions: defaultGanacheOptions, title: this.test.fullTitle(), testSpecificMock: mockPriceFetch, @@ -45,22 +45,8 @@ describe('Import flow', function () { async ({ driver }) => { await unlockWallet(driver); - // Token list is only on mainnet - await driver.clickElement('[data-testid="network-display"]'); - const networkSelectionModal = await driver.findVisibleElement( - '.mm-modal', - ); await driver.assertElementNotPresent('.loading-overlay'); - await driver.clickElement({ text: 'Ethereum Mainnet', tag: 'p' }); - - // Wait for network to change and token list to load from state - await networkSelectionModal.waitForElementState('hidden'); - await driver.findElement({ - css: '[data-testid="network-display"]', - text: 'Ethereum Mainnet', - }); - await driver.clickElement('[data-testid="import-token-button"]'); await driver.fill('input[placeholder="Search tokens"]', 'cha'); diff --git a/test/e2e/tests/tokens/nft/auto-detect-nft.spec.js b/test/e2e/tests/tokens/nft/auto-detect-nft.spec.js index 36b09723444b..ccd15ce0f71b 100644 --- a/test/e2e/tests/tokens/nft/auto-detect-nft.spec.js +++ b/test/e2e/tests/tokens/nft/auto-detect-nft.spec.js @@ -1,8 +1,9 @@ const { strict: assert } = require('assert'); const { - withFixtures, defaultGanacheOptions, + openMenuSafe, unlockWallet, + withFixtures, } = require('../../../helpers'); const FixtureBuilder = require('../../../fixture-builder'); const { setupAutoDetectMocking } = require('./mocks'); @@ -25,9 +26,8 @@ describe('NFT detection', function () { await unlockWallet(driver); // go to settings - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); + await openMenuSafe(driver); + await driver.clickElement({ text: 'Settings', tag: 'div' }); await driver.clickElement({ text: 'Security & privacy', tag: 'div' }); await driver.clickElement( diff --git a/test/e2e/tests/transaction/send-eth.spec.js b/test/e2e/tests/transaction/send-eth.spec.js index 36872115dcbe..5cbcb8309a18 100644 --- a/test/e2e/tests/transaction/send-eth.spec.js +++ b/test/e2e/tests/transaction/send-eth.spec.js @@ -189,7 +189,9 @@ describe('Send ETH', function () { const balance = await driver.findElement( '[data-testid="eth-overview__primary-currency"]', ); + assert.ok(/^[\d.]+\sETH$/u.test(await balance.getText())); + await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index 97c616d01f4b..a03a0d1cbd04 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -63,6 +63,8 @@ function wrapElementWithAPI(element, driver) { return await driver.wait(until.stalenessOf(element), timeout); case 'visible': return await driver.wait(until.elementIsVisible(element), timeout); + case 'disabled': + return await driver.wait(until.elementIsDisabled(element), timeout); default: throw new Error(`Provided state: '${state}' is not supported`); } diff --git a/test/e2e/webdriver/types.ts b/test/e2e/webdriver/types.ts new file mode 100644 index 000000000000..68cfa15dd600 --- /dev/null +++ b/test/e2e/webdriver/types.ts @@ -0,0 +1,5 @@ +import { WebElement, WebElementPromise } from 'selenium-webdriver'; + +export type WebElementWithWaitForElementState = WebElement & { + waitForElementState: (state: unknown, timeout?: unknown) => WebElementPromise; +}; diff --git a/test/integration/confirmations/signatures/permit.test.tsx b/test/integration/confirmations/signatures/permit.test.tsx index 809ac988962f..e11f206d1996 100644 --- a/test/integration/confirmations/signatures/permit.test.tsx +++ b/test/integration/confirmations/signatures/permit.test.tsx @@ -1,16 +1,16 @@ -import { act, fireEvent, waitFor, screen } from '@testing-library/react'; -import nock from 'nock'; import { ApprovalType } from '@metamask/controller-utils'; -import mockMetaMaskState from '../../data/integration-init-state.json'; -import { integrationTestRender } from '../../../lib/render-helpers'; -import { shortenAddress } from '../../../../ui/helpers/utils/util'; -import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { act, fireEvent, screen, waitFor } from '@testing-library/react'; +import nock from 'nock'; +import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import { MetaMetricsEventCategory, - MetaMetricsEventName, MetaMetricsEventLocation, + MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; -import { MESSAGE_TYPE } from '../../../../shared/constants/app'; +import { shortenAddress } from '../../../../ui/helpers/utils/util'; +import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { integrationTestRender } from '../../../lib/render-helpers'; +import mockMetaMaskState from '../../data/integration-init-state.json'; import { createMockImplementation } from '../../helpers'; jest.mock('../../../../ui/store/background-connection', () => ({ @@ -182,10 +182,12 @@ describe('Permit Confirmation', () => { }); }); - expect(screen.getByText('Spending cap request')).toBeInTheDocument(); - expect( - screen.getByText('This site wants permission to spend your tokens.'), - ).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText('Spending cap request')).toBeInTheDocument(); + expect( + screen.getByText('This site wants permission to spend your tokens.'), + ).toBeInTheDocument(); + }); }); it('displays the simulation section', async () => { diff --git a/test/integration/confirmations/signatures/personalSign.test.tsx b/test/integration/confirmations/signatures/personalSign.test.tsx index 5a965a3d6928..690446caa533 100644 --- a/test/integration/confirmations/signatures/personalSign.test.tsx +++ b/test/integration/confirmations/signatures/personalSign.test.tsx @@ -1,15 +1,15 @@ -import { fireEvent, waitFor } from '@testing-library/react'; import { ApprovalType } from '@metamask/controller-utils'; -import mockMetaMaskState from '../../data/integration-init-state.json'; -import { integrationTestRender } from '../../../lib/render-helpers'; -import { shortenAddress } from '../../../../ui/helpers/utils/util'; -import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { act, fireEvent, screen, waitFor } from '@testing-library/react'; +import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import { MetaMetricsEventCategory, - MetaMetricsEventName, MetaMetricsEventLocation, + MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; -import { MESSAGE_TYPE } from '../../../../shared/constants/app'; +import { shortenAddress } from '../../../../ui/helpers/utils/util'; +import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { integrationTestRender } from '../../../lib/render-helpers'; +import mockMetaMaskState from '../../data/integration-init-state.json'; jest.mock('../../../../ui/store/background-connection', () => ({ ...jest.requireActual('../../../../ui/store/background-connection'), @@ -156,14 +156,16 @@ describe('PersonalSign Confirmation', () => { account.address, ); - const { getByText } = await integrationTestRender({ - preloadedState: mockedMetaMaskState, - backgroundConnection: backgroundConnectionMocked, + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); }); - expect(getByText('Signature request')).toBeInTheDocument(); + expect(screen.getByText('Signature request')).toBeInTheDocument(); expect( - getByText('Review request details before you confirm.'), + screen.getByText('Review request details before you confirm.'), ).toBeInTheDocument(); }); diff --git a/test/integration/confirmations/transactions/contract-deployment.test.tsx b/test/integration/confirmations/transactions/contract-deployment.test.tsx new file mode 100644 index 000000000000..ecef04f30861 --- /dev/null +++ b/test/integration/confirmations/transactions/contract-deployment.test.tsx @@ -0,0 +1,408 @@ +import { ApprovalType } from '@metamask/controller-utils'; +import { TransactionType } from '@metamask/transaction-controller'; +import { + act, + fireEvent, + screen, + waitFor, + within, +} from '@testing-library/react'; +import nock from 'nock'; +import { + MetaMetricsEventCategory, + MetaMetricsEventLocation, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { tEn } from '../../../lib/i18n-helpers'; +import { integrationTestRender } from '../../../lib/render-helpers'; +import mockMetaMaskState from '../../data/integration-init-state.json'; +import { createMockImplementation, mock4byte } from '../../helpers'; +import { getUnapprovedContractDeploymentTransaction } from './transactionDataHelpers'; + +jest.mock('../../../../ui/store/background-connection', () => ({ + ...jest.requireActual('../../../../ui/store/background-connection'), + submitRequestToBackground: jest.fn(), + callBackgroundMethod: jest.fn(), +})); + +const mockedBackgroundConnection = jest.mocked(backgroundConnection); + +const backgroundConnectionMocked = { + onNotification: jest.fn(), +}; +export const pendingTransactionId = '48a75190-45ca-11ef-9001-f3886ec2397c'; +export const pendingTransactionTime = new Date().getTime(); + +const getMetaMaskStateWithUnapprovedContractDeployment = ({ + accountAddress, + showConfirmationAdvancedDetails = false, +}: { + accountAddress: string; + showConfirmationAdvancedDetails?: boolean; +}) => { + return { + ...mockMetaMaskState, + preferences: { + ...mockMetaMaskState.preferences, + redesignedConfirmationsEnabled: true, + showConfirmationAdvancedDetails, + }, + nextNonce: '8', + currencyRates: { + SepoliaETH: { + conversionDate: 1721392020.645, + conversionRate: 3404.13, + usdConversionRate: 3404.13, + }, + ETH: { + conversionDate: 1721393858.083, + conversionRate: 3414.67, + usdConversionRate: 3414.67, + }, + }, + currentCurrency: 'usd', + pendingApprovals: { + [pendingTransactionId]: { + id: pendingTransactionId, + origin: 'local:http://localhost:8086/', + time: pendingTransactionTime, + type: ApprovalType.Transaction, + requestData: { + txId: pendingTransactionId, + }, + requestState: null, + expectsResult: false, + }, + }, + pendingApprovalCount: 1, + knownMethodData: { + '0xd0e30db0': { + name: 'Deposit', + params: [ + { + type: 'uint256', + }, + ], + }, + }, + transactions: [ + getUnapprovedContractDeploymentTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ), + ], + }; +}; + +const advancedDetailsMockedRequests = { + getGasFeeTimeEstimate: { + lowerTimeBound: new Date().getTime(), + upperTimeBound: new Date().getTime(), + }, + getNextNonce: '9', + decodeTransactionData: { + data: [ + { + name: 'Deposit', + params: [ + { + name: 'numberOfTokens', + type: 'uint256', + value: 1, + }, + ], + }, + ], + source: 'Sourcify', + }, +}; + +const setupSubmitRequestToBackgroundMocks = ( + mockRequests?: Record, +) => { + mockedBackgroundConnection.submitRequestToBackground.mockImplementation( + createMockImplementation({ + ...advancedDetailsMockedRequests, + ...(mockRequests ?? {}), + }), + ); +}; + +describe('Contract Deployment Confirmation', () => { + beforeEach(() => { + jest.resetAllMocks(); + setupSubmitRequestToBackgroundMocks(); + const DEPOSIT_HEX_SIG = '0xd0e30db0'; + mock4byte(DEPOSIT_HEX_SIG); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + it('displays the header account modal with correct data', async () => { + const account = + mockMetaMaskState.internalAccounts.accounts[ + mockMetaMaskState.internalAccounts + .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts + ]; + + const accountName = account.metadata.name; + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedContractDeployment({ + accountAddress: account.address, + }); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + expect(screen.getByTestId('header-account-name')).toHaveTextContent( + accountName, + ); + expect(screen.getByTestId('header-network-display-name')).toHaveTextContent( + 'Sepolia', + ); + + fireEvent.click(screen.getByTestId('header-info__account-details-button')); + + expect( + await screen.findByTestId( + 'confirmation-account-details-modal__account-name', + ), + ).toHaveTextContent(accountName); + expect(screen.getByTestId('address-copy-button-text')).toHaveTextContent( + '0x0DCD5...3E7bc', + ); + expect( + screen.getByTestId('confirmation-account-details-modal__account-balance'), + ).toHaveTextContent('1.582717SepoliaETH'); + + let confirmAccountDetailsModalMetricsEvent; + + await waitFor(() => { + confirmAccountDetailsModalMetricsEvent = + mockedBackgroundConnection.submitRequestToBackground.mock.calls?.find( + (call) => + call[0] === 'trackMetaMetricsEvent' && + call[1]?.[0].category === MetaMetricsEventCategory.Confirmations, + ); + + expect(confirmAccountDetailsModalMetricsEvent?.[0]).toBe( + 'trackMetaMetricsEvent', + ); + }); + + expect(confirmAccountDetailsModalMetricsEvent?.[1]).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + category: MetaMetricsEventCategory.Confirmations, + event: MetaMetricsEventName.AccountDetailsOpened, + properties: { + action: 'Confirm Screen', + location: MetaMetricsEventLocation.Transaction, + transaction_type: TransactionType.deployContract, + }, + }), + ]), + ); + + fireEvent.click( + screen.getByTestId('confirmation-account-details-modal__close-button'), + ); + + await waitFor(() => { + expect( + screen.queryByTestId( + 'confirmation-account-details-modal__account-name', + ), + ).not.toBeInTheDocument(); + }); + }); + + it('displays the transaction details section', async () => { + const account = + mockMetaMaskState.internalAccounts.accounts[ + mockMetaMaskState.internalAccounts + .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts + ]; + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedContractDeployment({ + accountAddress: account.address, + }); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + expect( + screen.getByText(tEn('confirmTitleDeployContract') as string), + ).toBeInTheDocument(); + + const simulationSection = screen.getByTestId('simulation-details-layout'); + expect(simulationSection).toBeInTheDocument(); + expect(simulationSection).toHaveTextContent( + tEn('simulationDetailsTitle') as string, + ); + const simulationDetailsRow = await screen.findByTestId( + 'simulation-rows-incoming', + ); + expect(simulationSection).toContainElement(simulationDetailsRow); + expect(simulationDetailsRow).toHaveTextContent( + tEn('simulationDetailsIncomingHeading') as string, + ); + expect(simulationDetailsRow).toContainElement( + screen.getByTestId('simulation-details-amount-pill'), + ); + + const transactionDetailsSection = screen.getByTestId( + 'transaction-details-section', + ); + expect(transactionDetailsSection).toBeInTheDocument(); + expect(transactionDetailsSection).toHaveTextContent( + tEn('requestFrom') as string, + ); + expect(transactionDetailsSection).toHaveTextContent( + tEn('interactingWith') as string, + ); + + const gasFeesSection = screen.getByTestId('gas-fee-section'); + expect(gasFeesSection).toBeInTheDocument(); + + const editGasFeesRow = + within(gasFeesSection).getByTestId('edit-gas-fees-row'); + expect(editGasFeesRow).toHaveTextContent(tEn('networkFee') as string); + + const firstGasField = within(editGasFeesRow).getByTestId('first-gas-field'); + expect(firstGasField).toHaveTextContent('0.0001 ETH'); + const editGasFeeNativeCurrency = + within(editGasFeesRow).getByTestId('native-currency'); + expect(editGasFeeNativeCurrency).toHaveTextContent('$0.47'); + expect(editGasFeesRow).toContainElement( + screen.getByTestId('edit-gas-fee-icon'), + ); + + const gasFeeSpeed = within(gasFeesSection).getByTestId( + 'gas-fee-details-speed', + ); + expect(gasFeeSpeed).toHaveTextContent(tEn('speed') as string); + + const gasTimingTime = within(gasFeeSpeed).getByTestId('gas-timing-time'); + expect(gasTimingTime).toHaveTextContent('~0 sec'); + }); + + it('sets the preference showConfirmationAdvancedDetails to true when advanced details button is clicked', async () => { + mockedBackgroundConnection.callBackgroundMethod.mockImplementation( + createMockImplementation({ setPreference: {} }), + ); + + const account = + mockMetaMaskState.internalAccounts.accounts[ + mockMetaMaskState.internalAccounts + .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts + ]; + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedContractDeployment({ + accountAddress: account.address, + showConfirmationAdvancedDetails: false, + }); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + fireEvent.click(screen.getByTestId('header-advanced-details-button')); + + await waitFor(() => { + expect( + mockedBackgroundConnection.callBackgroundMethod, + ).toHaveBeenCalledWith( + 'setPreference', + ['showConfirmationAdvancedDetails', true], + expect.anything(), + ); + }); + }); + + it('displays the advanced transaction details section', async () => { + mockedBackgroundConnection.callBackgroundMethod.mockImplementation( + createMockImplementation({ setPreference: {} }), + ); + + const account = + mockMetaMaskState.internalAccounts.accounts[ + mockMetaMaskState.internalAccounts + .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts + ]; + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedContractDeployment({ + accountAddress: account.address, + showConfirmationAdvancedDetails: true, + }); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + await waitFor(() => { + expect( + mockedBackgroundConnection.submitRequestToBackground, + ).toHaveBeenCalledWith('getNextNonce', expect.anything()); + }); + + const gasFeesSection = screen.getByTestId('gas-fee-section'); + const maxFee = screen.getByTestId('gas-fee-details-max-fee'); + expect(gasFeesSection).toContainElement(maxFee); + expect(maxFee).toHaveTextContent(tEn('maxFee') as string); + expect(maxFee).toHaveTextContent('0.0023 ETH'); + expect(maxFee).toHaveTextContent('$7.72'); + + const nonceSection = screen.getByTestId('advanced-details-nonce-section'); + expect(nonceSection).toBeInTheDocument(); + expect(nonceSection).toHaveTextContent( + tEn('advancedDetailsNonceDesc') as string, + ); + expect(nonceSection).toContainElement( + screen.getByTestId('advanced-details-displayed-nonce'), + ); + expect( + screen.getByTestId('advanced-details-displayed-nonce'), + ).toHaveTextContent('9'); + + const dataSection = screen.getByTestId('advanced-details-data-section'); + expect(dataSection).toBeInTheDocument(); + + const dataSectionFunction = screen.getByTestId( + 'advanced-details-data-function', + ); + expect(dataSection).toContainElement(dataSectionFunction); + expect(dataSectionFunction).toHaveTextContent( + tEn('transactionDataFunction') as string, + ); + expect(dataSectionFunction).toHaveTextContent('Deposit'); + + const transactionDataParams = screen.getByTestId( + 'advanced-details-data-param-0', + ); + expect(dataSection).toContainElement(transactionDataParams); + expect(transactionDataParams).toHaveTextContent('Number Of Tokens'); + expect(transactionDataParams).toHaveTextContent('1'); + }); +}); diff --git a/test/integration/confirmations/transactions/contract-interaction.test.tsx b/test/integration/confirmations/transactions/contract-interaction.test.tsx index cd5953db50b8..1102cb21c67d 100644 --- a/test/integration/confirmations/transactions/contract-interaction.test.tsx +++ b/test/integration/confirmations/transactions/contract-interaction.test.tsx @@ -1,25 +1,26 @@ +import { ApprovalType } from '@metamask/controller-utils'; +import { TransactionType } from '@metamask/transaction-controller'; import { + act, fireEvent, + screen, waitFor, within, - screen, - act, } from '@testing-library/react'; -import { ApprovalType } from '@metamask/controller-utils'; import nock from 'nock'; -import { TransactionType } from '@metamask/transaction-controller'; -import mockMetaMaskState from '../../data/integration-init-state.json'; -import { integrationTestRender } from '../../../lib/render-helpers'; -import * as backgroundConnection from '../../../../ui/store/background-connection'; import { MetaMetricsEventCategory, - MetaMetricsEventName, MetaMetricsEventLocation, + MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; +import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { tEn } from '../../../lib/i18n-helpers'; +import { integrationTestRender } from '../../../lib/render-helpers'; +import mockMetaMaskState from '../../data/integration-init-state.json'; import { createMockImplementation, mock4byte } from '../../helpers'; import { getMaliciousUnapprovedTransaction, - getUnapprovedTransaction, + getUnapprovedContractInteractionTransaction, } from './transactionDataHelpers'; jest.mock('../../../../ui/store/background-connection', () => ({ @@ -89,7 +90,7 @@ const getMetaMaskStateWithUnapprovedContractInteraction = ({ }, }, transactions: [ - getUnapprovedTransaction( + getUnapprovedContractInteractionTransaction( accountAddress, pendingTransactionId, pendingTransactionTime, @@ -261,18 +262,21 @@ describe('Contract Interaction Confirmation', () => { }); }); - expect(screen.getByText('Transaction request')).toBeInTheDocument(); + expect( + screen.getByText(tEn('confirmTitleTransaction') as string), + ).toBeInTheDocument(); const simulationSection = screen.getByTestId('simulation-details-layout'); expect(simulationSection).toBeInTheDocument(); - expect(simulationSection).toHaveTextContent('Estimated changes'); + expect(simulationSection).toHaveTextContent( + tEn('simulationDetailsTitle') as string, + ); const simulationDetailsRow = await screen.findByTestId( 'simulation-rows-incoming', ); expect(simulationSection).toContainElement(simulationDetailsRow); - expect(simulationDetailsRow).toHaveTextContent('You receive'); - expect(simulationDetailsRow).toContainElement( - screen.getByTestId('simulation-details-asset-pill'), + expect(simulationDetailsRow).toHaveTextContent( + tEn('simulationDetailsIncomingHeading') as string, ); expect(simulationDetailsRow).toContainElement( screen.getByTestId('simulation-details-amount-pill'), @@ -282,15 +286,19 @@ describe('Contract Interaction Confirmation', () => { 'transaction-details-section', ); expect(transactionDetailsSection).toBeInTheDocument(); - expect(transactionDetailsSection).toHaveTextContent('Request from'); - expect(transactionDetailsSection).toHaveTextContent('Interacting with'); + expect(transactionDetailsSection).toHaveTextContent( + tEn('requestFrom') as string, + ); + expect(transactionDetailsSection).toHaveTextContent( + tEn('interactingWith') as string, + ); const gasFeesSection = screen.getByTestId('gas-fee-section'); expect(gasFeesSection).toBeInTheDocument(); const editGasFeesRow = within(gasFeesSection).getByTestId('edit-gas-fees-row'); - expect(editGasFeesRow).toHaveTextContent('Network fee'); + expect(editGasFeesRow).toHaveTextContent(tEn('networkFee') as string); const firstGasField = within(editGasFeesRow).getByTestId('first-gas-field'); expect(firstGasField).toHaveTextContent('0.0001 ETH'); @@ -304,7 +312,7 @@ describe('Contract Interaction Confirmation', () => { const gasFeeSpeed = within(gasFeesSection).getByTestId( 'gas-fee-details-speed', ); - expect(gasFeeSpeed).toHaveTextContent('Speed'); + expect(gasFeeSpeed).toHaveTextContent(tEn('speed') as string); const gasTimingTime = within(gasFeeSpeed).getByTestId('gas-timing-time'); expect(gasTimingTime).toHaveTextContent('~0 sec'); @@ -393,13 +401,15 @@ describe('Contract Interaction Confirmation', () => { const gasFeesSection = screen.getByTestId('gas-fee-section'); const maxFee = screen.getByTestId('gas-fee-details-max-fee'); expect(gasFeesSection).toContainElement(maxFee); - expect(maxFee).toHaveTextContent('Max fee'); + expect(maxFee).toHaveTextContent(tEn('maxFee') as string); expect(maxFee).toHaveTextContent('0.0023 ETH'); expect(maxFee).toHaveTextContent('$7.72'); const nonceSection = screen.getByTestId('advanced-details-nonce-section'); expect(nonceSection).toBeInTheDocument(); - expect(nonceSection).toHaveTextContent('Nonce'); + expect(nonceSection).toHaveTextContent( + tEn('advancedDetailsNonceDesc') as string, + ); expect(nonceSection).toContainElement( screen.getByTestId('advanced-details-displayed-nonce'), ); @@ -414,7 +424,9 @@ describe('Contract Interaction Confirmation', () => { 'advanced-details-data-function', ); expect(dataSection).toContainElement(dataSectionFunction); - expect(dataSectionFunction).toHaveTextContent('Function'); + expect(dataSectionFunction).toHaveTextContent( + tEn('transactionDataFunction') as string, + ); expect(dataSectionFunction).toHaveTextContent('mintNFTs'); const transactionDataParams = screen.getByTestId( @@ -444,9 +456,8 @@ describe('Contract Interaction Confirmation', () => { }); }); - const headingText = 'This is a deceptive request'; - const bodyText = - 'If you approve this request, a third party known for scams will take all your assets.'; + const headingText = tEn('blockaidTitleDeceptive') as string; + const bodyText = tEn('blockaidDescriptionTransferFarming') as string; expect(screen.getByText(headingText)).toBeInTheDocument(); expect(screen.getByText(bodyText)).toBeInTheDocument(); }); diff --git a/test/integration/confirmations/transactions/erc20-approve.test.tsx b/test/integration/confirmations/transactions/erc20-approve.test.tsx index b6ab98774cb4..a2404ba75b09 100644 --- a/test/integration/confirmations/transactions/erc20-approve.test.tsx +++ b/test/integration/confirmations/transactions/erc20-approve.test.tsx @@ -2,12 +2,13 @@ import { ApprovalType } from '@metamask/controller-utils'; import { act, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import nock from 'nock'; +import { TokenStandard } from '../../../../shared/constants/transaction'; import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { tEn } from '../../../lib/i18n-helpers'; import { integrationTestRender } from '../../../lib/render-helpers'; +import { createTestProviderTools } from '../../../stub/provider'; import mockMetaMaskState from '../../data/integration-init-state.json'; import { createMockImplementation, mock4byte } from '../../helpers'; -import { TokenStandard } from '../../../../shared/constants/transaction'; -import { createTestProviderTools } from '../../../stub/provider'; import { getUnapprovedApproveTransaction } from './transactionDataHelpers'; jest.mock('../../../../ui/store/background-connection', () => ({ @@ -161,9 +162,13 @@ describe('ERC20 Approve Confirmation', () => { }); }); - expect(screen.getByText('Spending cap request')).toBeInTheDocument(); expect( - screen.getByText('This site wants permission to withdraw your tokens'), + screen.getByText(tEn('confirmTitlePermitTokens') as string), + ).toBeInTheDocument(); + expect( + screen.getByText( + tEn('confirmTitleDescERC20ApproveTransaction') as string, + ), ).toBeInTheDocument(); }); @@ -184,9 +189,9 @@ describe('ERC20 Approve Confirmation', () => { expect(simulationSection).toBeInTheDocument(); expect(simulationSection).toHaveTextContent( - "You're giving someone else permission to spend this amount from your account.", + tEn('simulationDetailsERC20ApproveDesc') as string, ); - expect(simulationSection).toHaveTextContent('Spending cap'); + expect(simulationSection).toHaveTextContent(tEn('spendingCap') as string); const spendingCapValue = screen.getByTestId('simulation-token-value'); expect(simulationSection).toContainElement(spendingCapValue); expect(spendingCapValue).toHaveTextContent('1'); @@ -213,7 +218,7 @@ describe('ERC20 Approve Confirmation', () => { ); expect(approveDetails).toContainElement(approveDetailsSpender); - expect(approveDetailsSpender).toHaveTextContent('Spender'); + expect(approveDetailsSpender).toHaveTextContent(tEn('spender') as string); expect(approveDetailsSpender).toHaveTextContent('0x2e0D7...5d09B'); const spenderTooltip = screen.getByTestId( 'confirmation__approve-spender-tooltip', @@ -222,7 +227,7 @@ describe('ERC20 Approve Confirmation', () => { await testUser.hover(spenderTooltip); const spenderTooltipContent = await screen.findByText( - 'This is the address that will be able to spend your tokens on your behalf.', + tEn('spenderTooltipERC20ApproveDesc') as string, ); expect(spenderTooltipContent).toBeInTheDocument(); @@ -243,7 +248,7 @@ describe('ERC20 Approve Confirmation', () => { ); await testUser.hover(approveDetailsRequestFromTooltip); const requestFromTooltipContent = await screen.findByText( - 'This is the site asking for your confirmation.', + tEn('requestFromTransactionDescription') as string, ); expect(requestFromTooltipContent).toBeInTheDocument(); }); @@ -266,13 +271,15 @@ describe('ERC20 Approve Confirmation', () => { ); expect(spendingCapSection).toBeInTheDocument(); - expect(spendingCapSection).toHaveTextContent('Account balance'); + expect(spendingCapSection).toHaveTextContent( + tEn('accountBalance') as string, + ); expect(spendingCapSection).toHaveTextContent('0'); const spendingCapGroup = screen.getByTestId( 'confirmation__approve-spending-cap-group', ); expect(spendingCapSection).toContainElement(spendingCapGroup); - expect(spendingCapGroup).toHaveTextContent('Spending cap'); + expect(spendingCapGroup).toHaveTextContent(tEn('spendingCap') as string); expect(spendingCapGroup).toHaveTextContent('1'); const spendingCapGroupTooltip = screen.getByTestId( @@ -281,7 +288,7 @@ describe('ERC20 Approve Confirmation', () => { expect(spendingCapGroup).toContainElement(spendingCapGroupTooltip); await testUser.hover(spendingCapGroupTooltip); const requestFromTooltipContent = await screen.findByText( - 'This is the amount of tokens the spender will be able to access on your behalf.', + tEn('spendingCapTooltipDesc') as string, ); expect(requestFromTooltipContent).toBeInTheDocument(); }); @@ -308,7 +315,9 @@ describe('ERC20 Approve Confirmation', () => { 'transaction-details-recipient-row', ); expect(approveDetails).toContainElement(approveDetailsRecipient); - expect(approveDetailsRecipient).toHaveTextContent('Interacting with'); + expect(approveDetailsRecipient).toHaveTextContent( + tEn('interactingWith') as string, + ); expect(approveDetailsRecipient).toHaveTextContent('0x07614...3ad68'); const approveDetailsRecipientTooltip = screen.getByTestId( @@ -319,7 +328,7 @@ describe('ERC20 Approve Confirmation', () => { ); await testUser.hover(approveDetailsRecipientTooltip); const recipientTooltipContent = await screen.findByText( - "This is the contract you're interacting with. Protect yourself from scammers by verifying the details.", + tEn('interactingWithTransactionDescription') as string, ); expect(recipientTooltipContent).toBeInTheDocument(); @@ -327,7 +336,7 @@ describe('ERC20 Approve Confirmation', () => { 'transaction-details-method-data-row', ); expect(approveDetails).toContainElement(approveMethodData); - expect(approveMethodData).toHaveTextContent('Method'); + expect(approveMethodData).toHaveTextContent(tEn('methodData') as string); expect(approveMethodData).toHaveTextContent('Approve'); const approveMethodDataTooltip = screen.getByTestId( 'transaction-details-method-data-row-tooltip', @@ -335,7 +344,7 @@ describe('ERC20 Approve Confirmation', () => { expect(approveMethodData).toContainElement(approveMethodDataTooltip); await testUser.hover(approveMethodDataTooltip); const approveMethodDataTooltipContent = await screen.findByText( - 'Function executed based on decoded input data.', + tEn('methodDataTransactionDesc') as string, ); expect(approveMethodDataTooltipContent).toBeInTheDocument(); @@ -351,7 +360,9 @@ describe('ERC20 Approve Confirmation', () => { 'advanced-details-data-function', ); expect(dataSection).toContainElement(dataSectionFunction); - expect(dataSectionFunction).toHaveTextContent('Function'); + expect(dataSectionFunction).toHaveTextContent( + tEn('transactionDataFunction') as string, + ); expect(dataSectionFunction).toHaveTextContent('Approve'); const approveDataParams1 = screen.getByTestId( diff --git a/test/integration/confirmations/transactions/erc721-approve.test.tsx b/test/integration/confirmations/transactions/erc721-approve.test.tsx index 8a836dbd7568..c3948d150b1d 100644 --- a/test/integration/confirmations/transactions/erc721-approve.test.tsx +++ b/test/integration/confirmations/transactions/erc721-approve.test.tsx @@ -1,12 +1,14 @@ import { ApprovalType } from '@metamask/controller-utils'; -import { act, screen, waitFor } from '@testing-library/react'; +import { act, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import nock from 'nock'; +import { TokenStandard } from '../../../../shared/constants/transaction'; import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { tEn } from '../../../lib/i18n-helpers'; import { integrationTestRender } from '../../../lib/render-helpers'; +import { createTestProviderTools } from '../../../stub/provider'; import mockMetaMaskState from '../../data/integration-init-state.json'; import { createMockImplementation, mock4byte } from '../../helpers'; -import { TokenStandard } from '../../../../shared/constants/transaction'; -import { createTestProviderTools } from '../../../stub/provider'; import { getUnapprovedApproveTransaction } from './transactionDataHelpers'; jest.mock('../../../../ui/store/background-connection', () => ({ @@ -23,14 +25,21 @@ const backgroundConnectionMocked = { export const pendingTransactionId = '48a75190-45ca-11ef-9001-f3886ec2397c'; export const pendingTransactionTime = new Date().getTime(); -const getMetaMaskStateWithUnapprovedApproveTransaction = ( - accountAddress: string, -) => { +const getMetaMaskStateWithUnapprovedApproveTransaction = (opts?: { + showAdvanceDetails: boolean; +}) => { + const account = + mockMetaMaskState.internalAccounts.accounts[ + mockMetaMaskState.internalAccounts + .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts + ]; + return { ...mockMetaMaskState, preferences: { ...mockMetaMaskState.preferences, redesignedConfirmationsEnabled: true, + showConfirmationAdvancedDetails: opts?.showAdvanceDetails ?? false, }, pendingApprovals: { [pendingTransactionId]: { @@ -61,7 +70,7 @@ const getMetaMaskStateWithUnapprovedApproveTransaction = ( }, transactions: [ getUnapprovedApproveTransaction( - accountAddress, + account.address, pendingTransactionId, pendingTransactionTime, ), @@ -78,7 +87,7 @@ const advancedDetailsMockedRequests = { decodeTransactionData: { data: [ { - name: 'approve', + name: 'Approve', params: [ { type: 'address', @@ -129,7 +138,8 @@ describe('ERC721 Approve Confirmation', () => { }, }); const APPROVE_NFT_HEX_SIG = '0x095ea7b3'; - mock4byte(APPROVE_NFT_HEX_SIG); + const APPROVE_NFT_TEXT_SIG = 'approve(address,uint256)'; + mock4byte(APPROVE_NFT_HEX_SIG, APPROVE_NFT_TEXT_SIG); }); afterEach(() => { @@ -141,15 +151,28 @@ describe('ERC721 Approve Confirmation', () => { delete (global as any).ethereumProvider; }); - it('displays approve details with correct data', async () => { - const account = - mockMetaMaskState.internalAccounts.accounts[ - mockMetaMaskState.internalAccounts - .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts - ]; + it('displays spending cap request title', async () => { + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedApproveTransaction(); + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + expect( + screen.getByText(tEn('confirmTitleApproveTransaction') as string), + ).toBeInTheDocument(); + expect( + screen.getByText(tEn('confirmTitleDescApproveTransaction') as string), + ).toBeInTheDocument(); + }); + + it('displays approve simulation section', async () => { const mockedMetaMaskState = - getMetaMaskStateWithUnapprovedApproveTransaction(account.address); + getMetaMaskStateWithUnapprovedApproveTransaction(); await act(async () => { await integrationTestRender({ @@ -158,12 +181,163 @@ describe('ERC721 Approve Confirmation', () => { }); }); - await waitFor(() => { - expect(screen.getByText('Allowance request')).toBeInTheDocument(); + const simulationSection = screen.getByTestId( + 'confirmation__simulation_section', + ); + expect(simulationSection).toBeInTheDocument(); + + expect(simulationSection).toHaveTextContent( + tEn('simulationDetailsApproveDesc') as string, + ); + expect(simulationSection).toHaveTextContent( + tEn('simulationApproveHeading') as string, + ); + const spendingCapValue = screen.getByTestId('simulation-token-value'); + expect(simulationSection).toContainElement(spendingCapValue); + expect(spendingCapValue).toHaveTextContent('1'); + expect(simulationSection).toHaveTextContent('0x07614...3ad68'); + }); + + it('displays approve details with correct data', async () => { + const testUser = userEvent.setup(); + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedApproveTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); }); - await waitFor(() => { - expect(screen.getByText('Request from')).toBeInTheDocument(); + const approveDetails = screen.getByTestId('confirmation__approve-details'); + expect(approveDetails).toBeInTheDocument(); + const approveDetailsSpender = screen.getByTestId( + 'confirmation__approve-spender', + ); + + expect(approveDetails).toContainElement(approveDetailsSpender); + expect(approveDetailsSpender).toHaveTextContent(tEn('spender') as string); + expect(approveDetailsSpender).toHaveTextContent('0x2e0D7...5d09B'); + const spenderTooltip = screen.getByTestId( + 'confirmation__approve-spender-tooltip', + ); + expect(approveDetailsSpender).toContainElement(spenderTooltip); + await testUser.hover(spenderTooltip); + const spenderTooltipContent = await screen.findByText( + tEn('spenderTooltipDesc') as string, + ); + expect(spenderTooltipContent).toBeInTheDocument(); + + const approveDetailsRequestFrom = screen.getByTestId( + 'transaction-details-origin-row', + ); + expect(approveDetails).toContainElement(approveDetailsRequestFrom); + expect(approveDetailsRequestFrom).toHaveTextContent( + tEn('requestFrom') as string, + ); + expect(approveDetailsRequestFrom).toHaveTextContent( + 'http://localhost:8086/', + ); + + const approveDetailsRequestFromTooltip = screen.getByTestId( + 'transaction-details-origin-row-tooltip', + ); + expect(approveDetailsRequestFrom).toContainElement( + approveDetailsRequestFromTooltip, + ); + await testUser.hover(approveDetailsRequestFromTooltip); + const requestFromTooltipContent = await screen.findByText( + tEn('requestFromTransactionDescription') as string, + ); + expect(requestFromTooltipContent).toBeInTheDocument(); + }); + + it('displays the advanced transaction details section', async () => { + const testUser = userEvent.setup(); + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedApproveTransaction({ + showAdvanceDetails: true, + }); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); }); + + const approveDetails = screen.getByTestId('confirmation__approve-details'); + expect(approveDetails).toBeInTheDocument(); + + const approveDetailsRecipient = screen.getByTestId( + 'transaction-details-recipient-row', + ); + expect(approveDetails).toContainElement(approveDetailsRecipient); + expect(approveDetailsRecipient).toHaveTextContent( + tEn('interactingWith') as string, + ); + expect(approveDetailsRecipient).toHaveTextContent('0x07614...3ad68'); + + const approveDetailsRecipientTooltip = screen.getByTestId( + 'transaction-details-recipient-row-tooltip', + ); + expect(approveDetailsRecipient).toContainElement( + approveDetailsRecipientTooltip, + ); + await testUser.hover(approveDetailsRecipientTooltip); + const recipientTooltipContent = await screen.findByText( + tEn('interactingWithTransactionDescription') as string, + ); + expect(recipientTooltipContent).toBeInTheDocument(); + + const approveMethodData = await screen.findByTestId( + 'transaction-details-method-data-row', + ); + expect(approveDetails).toContainElement(approveMethodData); + expect(approveMethodData).toHaveTextContent(tEn('methodData') as string); + expect(approveMethodData).toHaveTextContent('Approve'); + const approveMethodDataTooltip = screen.getByTestId( + 'transaction-details-method-data-row-tooltip', + ); + expect(approveMethodData).toContainElement(approveMethodDataTooltip); + await testUser.hover(approveMethodDataTooltip); + const approveMethodDataTooltipContent = await screen.findByText( + tEn('methodDataTransactionDesc') as string, + ); + expect(approveMethodDataTooltipContent).toBeInTheDocument(); + + const approveDetailsNonce = screen.getByTestId( + 'advanced-details-nonce-section', + ); + expect(approveDetailsNonce).toBeInTheDocument(); + + const dataSection = screen.getByTestId('advanced-details-data-section'); + expect(dataSection).toBeInTheDocument(); + + const dataSectionFunction = screen.getByTestId( + 'advanced-details-data-function', + ); + expect(dataSection).toContainElement(dataSectionFunction); + expect(dataSectionFunction).toHaveTextContent( + tEn('transactionDataFunction') as string, + ); + expect(dataSectionFunction).toHaveTextContent('Approve'); + + const approveDataParams1 = screen.getByTestId( + 'advanced-details-data-param-0', + ); + expect(dataSection).toContainElement(approveDataParams1); + expect(approveDataParams1).toHaveTextContent('Param #1'); + expect(approveDataParams1).toHaveTextContent('0x2e0D7...5d09B'); + + const approveDataParams2 = screen.getByTestId( + 'advanced-details-data-param-1', + ); + expect(dataSection).toContainElement(approveDataParams2); + expect(approveDataParams2).toHaveTextContent('Param #2'); + expect(approveDataParams2).toHaveTextContent('1'); }); }); diff --git a/test/integration/confirmations/transactions/increase-allowance.test.tsx b/test/integration/confirmations/transactions/increase-allowance.test.tsx new file mode 100644 index 000000000000..c288a5cc4e6d --- /dev/null +++ b/test/integration/confirmations/transactions/increase-allowance.test.tsx @@ -0,0 +1,384 @@ +import { ApprovalType } from '@metamask/controller-utils'; +import { act, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import nock from 'nock'; +import { TokenStandard } from '../../../../shared/constants/transaction'; +import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { tEn } from '../../../lib/i18n-helpers'; +import { integrationTestRender } from '../../../lib/render-helpers'; +import { createTestProviderTools } from '../../../stub/provider'; +import mockMetaMaskState from '../../data/integration-init-state.json'; +import { createMockImplementation, mock4byte } from '../../helpers'; +import { getUnapprovedIncreaseAllowanceTransaction } from './transactionDataHelpers'; + +jest.mock('../../../../ui/store/background-connection', () => ({ + ...jest.requireActual('../../../../ui/store/background-connection'), + submitRequestToBackground: jest.fn(), + callBackgroundMethod: jest.fn(), +})); + +const mockedBackgroundConnection = jest.mocked(backgroundConnection); + +const backgroundConnectionMocked = { + onNotification: jest.fn(), +}; +export const pendingTransactionId = '48a75190-45ca-11ef-9001-f3886ec2397c'; +export const pendingTransactionTime = new Date().getTime(); + +const getMetaMaskStateWithUnapprovedIncreaseAllowanceTransaction = (opts?: { + showAdvanceDetails: boolean; +}) => { + const account = + mockMetaMaskState.internalAccounts.accounts[ + mockMetaMaskState.internalAccounts + .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts + ]; + + return { + ...mockMetaMaskState, + preferences: { + ...mockMetaMaskState.preferences, + redesignedConfirmationsEnabled: true, + showConfirmationAdvancedDetails: opts?.showAdvanceDetails ?? false, + }, + pendingApprovals: { + [pendingTransactionId]: { + id: pendingTransactionId, + origin: 'origin', + time: pendingTransactionTime, + type: ApprovalType.Transaction, + requestData: { + txId: pendingTransactionId, + }, + requestState: null, + expectsResult: false, + }, + }, + pendingApprovalCount: 1, + knownMethodData: { + '0x39509351': { + name: 'increaseAllowance', + params: [ + { + type: 'address', + }, + { + type: 'uint256', + }, + ], + }, + }, + transactions: [ + getUnapprovedIncreaseAllowanceTransaction( + account.address, + pendingTransactionId, + pendingTransactionTime, + ), + ], + }; +}; + +const advancedDetailsMockedRequests = { + getGasFeeTimeEstimate: { + lowerTimeBound: new Date().getTime(), + upperTimeBound: new Date().getTime(), + }, + getNextNonce: '9', + decodeTransactionData: { + data: [ + { + name: 'increaseAllowance', + params: [ + { + type: 'address', + value: '0x2e0D7E8c45221FcA00d74a3609A0f7097035d09B', + }, + { + type: 'uint256', + value: 1, + }, + ], + }, + ], + source: 'FourByte', + }, +}; + +const setupSubmitRequestToBackgroundMocks = ( + mockRequests?: Record, +) => { + mockedBackgroundConnection.submitRequestToBackground.mockImplementation( + createMockImplementation({ + ...advancedDetailsMockedRequests, + ...(mockRequests ?? {}), + }), + ); + + mockedBackgroundConnection.callBackgroundMethod.mockImplementation( + createMockImplementation({ addKnownMethodData: {} }), + ); +}; + +describe('ERC20 increaseAllowance Confirmation', () => { + beforeAll(() => { + const { provider } = createTestProviderTools({ + networkId: 'sepolia', + chainId: '0xaa36a7', + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + global.ethereumProvider = provider as any; + }); + + beforeEach(() => { + jest.resetAllMocks(); + setupSubmitRequestToBackgroundMocks({ + getTokenStandardAndDetails: { + standard: TokenStandard.ERC20, + }, + }); + const INCREASE_ALLOWANCE_ERC20_HEX_SIG = '0x39509351'; + const INCREASE_ALLOWANCE_ERC20_TEXT_SIG = + 'increaseAllowance(address,uint256)'; + mock4byte( + INCREASE_ALLOWANCE_ERC20_HEX_SIG, + INCREASE_ALLOWANCE_ERC20_TEXT_SIG, + ); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + afterAll(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (global as any).ethereumProvider; + }); + + it('displays spending cap request title', async () => { + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedIncreaseAllowanceTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + expect( + screen.getByText(tEn('confirmTitlePermitTokens') as string), + ).toBeInTheDocument(); + expect( + screen.getByText(tEn('confirmTitleDescPermitSignature') as string), + ).toBeInTheDocument(); + }); + + it('displays increase allowance simulation section', async () => { + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedIncreaseAllowanceTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + const simulationSection = screen.getByTestId( + 'confirmation__simulation_section', + ); + expect(simulationSection).toBeInTheDocument(); + + expect(simulationSection).toHaveTextContent( + tEn('simulationDetailsERC20ApproveDesc') as string, + ); + expect(simulationSection).toHaveTextContent(tEn('spendingCap') as string); + const spendingCapValue = screen.getByTestId('simulation-token-value'); + expect(simulationSection).toContainElement(spendingCapValue); + expect(spendingCapValue).toHaveTextContent('1'); + expect(simulationSection).toHaveTextContent('0x07614...3ad68'); + }); + + it('displays approve details with correct data', async () => { + const testUser = userEvent.setup(); + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedIncreaseAllowanceTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + const approveDetails = screen.getByTestId('confirmation__approve-details'); + expect(approveDetails).toBeInTheDocument(); + const approveDetailsSpender = screen.getByTestId( + 'confirmation__approve-spender', + ); + + expect(approveDetails).toContainElement(approveDetailsSpender); + expect(approveDetailsSpender).toHaveTextContent(tEn('spender') as string); + expect(approveDetailsSpender).toHaveTextContent('0x2e0D7...5d09B'); + const spenderTooltip = screen.getByTestId( + 'confirmation__approve-spender-tooltip', + ); + expect(approveDetailsSpender).toContainElement(spenderTooltip); + await testUser.hover(spenderTooltip); + + const spenderTooltipContent = await screen.findByText( + tEn('spenderTooltipERC20ApproveDesc') as string, + ); + expect(spenderTooltipContent).toBeInTheDocument(); + + const approveDetailsRequestFrom = screen.getByTestId( + 'transaction-details-origin-row', + ); + expect(approveDetails).toContainElement(approveDetailsRequestFrom); + expect(approveDetailsRequestFrom).toHaveTextContent('Request from'); + expect(approveDetailsRequestFrom).toHaveTextContent( + 'http://localhost:8086/', + ); + + const approveDetailsRequestFromTooltip = screen.getByTestId( + 'transaction-details-origin-row-tooltip', + ); + expect(approveDetailsRequestFrom).toContainElement( + approveDetailsRequestFromTooltip, + ); + await testUser.hover(approveDetailsRequestFromTooltip); + const requestFromTooltipContent = await screen.findByText( + tEn('requestFromTransactionDescription') as string, + ); + expect(requestFromTooltipContent).toBeInTheDocument(); + }); + + it('displays spending cap section with correct data', async () => { + const testUser = userEvent.setup(); + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedIncreaseAllowanceTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + const spendingCapSection = screen.getByTestId( + 'confirmation__approve-spending-cap-section', + ); + expect(spendingCapSection).toBeInTheDocument(); + + expect(spendingCapSection).toHaveTextContent( + tEn('accountBalance') as string, + ); + expect(spendingCapSection).toHaveTextContent('0'); + const spendingCapGroup = screen.getByTestId( + 'confirmation__approve-spending-cap-group', + ); + expect(spendingCapSection).toContainElement(spendingCapGroup); + expect(spendingCapGroup).toHaveTextContent(tEn('spendingCap') as string); + expect(spendingCapGroup).toHaveTextContent('1'); + + const spendingCapGroupTooltip = screen.getByTestId( + 'confirmation__approve-spending-cap-group-tooltip', + ); + expect(spendingCapGroup).toContainElement(spendingCapGroupTooltip); + await testUser.hover(spendingCapGroupTooltip); + const requestFromTooltipContent = await screen.findByText( + tEn('spendingCapTooltipDesc') as string, + ); + expect(requestFromTooltipContent).toBeInTheDocument(); + }); + + it('displays the advanced transaction details section', async () => { + const testUser = userEvent.setup(); + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedIncreaseAllowanceTransaction({ + showAdvanceDetails: true, + }); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + const approveDetails = screen.getByTestId('confirmation__approve-details'); + expect(approveDetails).toBeInTheDocument(); + + const approveDetailsRecipient = screen.getByTestId( + 'transaction-details-recipient-row', + ); + expect(approveDetails).toContainElement(approveDetailsRecipient); + expect(approveDetailsRecipient).toHaveTextContent( + tEn('interactingWith') as string, + ); + expect(approveDetailsRecipient).toHaveTextContent('0x07614...3ad68'); + + const approveDetailsRecipientTooltip = screen.getByTestId( + 'transaction-details-recipient-row-tooltip', + ); + expect(approveDetailsRecipient).toContainElement( + approveDetailsRecipientTooltip, + ); + await testUser.hover(approveDetailsRecipientTooltip); + const recipientTooltipContent = await screen.findByText( + tEn('interactingWithTransactionDescription') as string, + ); + expect(recipientTooltipContent).toBeInTheDocument(); + + const approveMethodData = await screen.findByTestId( + 'transaction-details-method-data-row', + ); + expect(approveDetails).toContainElement(approveMethodData); + expect(approveMethodData).toHaveTextContent(tEn('methodData') as string); + expect(approveMethodData).toHaveTextContent('increaseAllowance'); + const approveMethodDataTooltip = screen.getByTestId( + 'transaction-details-method-data-row-tooltip', + ); + expect(approveMethodData).toContainElement(approveMethodDataTooltip); + await testUser.hover(approveMethodDataTooltip); + const approveMethodDataTooltipContent = await screen.findByText( + tEn('methodDataTransactionDesc') as string, + ); + expect(approveMethodDataTooltipContent).toBeInTheDocument(); + + const approveDetailsNonce = screen.getByTestId( + 'advanced-details-nonce-section', + ); + expect(approveDetailsNonce).toBeInTheDocument(); + + const dataSection = screen.getByTestId('advanced-details-data-section'); + expect(dataSection).toBeInTheDocument(); + + const dataSectionFunction = screen.getByTestId( + 'advanced-details-data-function', + ); + expect(dataSection).toContainElement(dataSectionFunction); + expect(dataSectionFunction).toHaveTextContent( + tEn('transactionDataFunction') as string, + ); + expect(dataSectionFunction).toHaveTextContent('increaseAllowance'); + + const approveDataParams1 = screen.getByTestId( + 'advanced-details-data-param-0', + ); + expect(dataSection).toContainElement(approveDataParams1); + expect(approveDataParams1).toHaveTextContent('Param #1'); + expect(approveDataParams1).toHaveTextContent('0x2e0D7...5d09B'); + + const approveDataParams2 = screen.getByTestId( + 'advanced-details-data-param-1', + ); + expect(dataSection).toContainElement(approveDataParams2); + expect(approveDataParams2).toHaveTextContent('Param #2'); + expect(approveDataParams2).toHaveTextContent('1'); + }); +}); diff --git a/test/integration/confirmations/transactions/set-approval-for-all.test.tsx b/test/integration/confirmations/transactions/set-approval-for-all.test.tsx new file mode 100644 index 000000000000..a65688030e90 --- /dev/null +++ b/test/integration/confirmations/transactions/set-approval-for-all.test.tsx @@ -0,0 +1,348 @@ +import { ApprovalType } from '@metamask/controller-utils'; +import { act, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import nock from 'nock'; +import { TokenStandard } from '../../../../shared/constants/transaction'; +import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { tEn } from '../../../lib/i18n-helpers'; +import { integrationTestRender } from '../../../lib/render-helpers'; +import { createTestProviderTools } from '../../../stub/provider'; +import mockMetaMaskState from '../../data/integration-init-state.json'; +import { createMockImplementation, mock4byte } from '../../helpers'; +import { getUnapprovedSetApprovalForAllTransaction } from './transactionDataHelpers'; + +jest.mock('../../../../ui/store/background-connection', () => ({ + ...jest.requireActual('../../../../ui/store/background-connection'), + submitRequestToBackground: jest.fn(), + callBackgroundMethod: jest.fn(), +})); + +const mockedBackgroundConnection = jest.mocked(backgroundConnection); + +const backgroundConnectionMocked = { + onNotification: jest.fn(), +}; +export const pendingTransactionId = '48a75190-45ca-11ef-9001-f3886ec2397c'; +export const pendingTransactionTime = new Date().getTime(); + +const getMetaMaskStateWithUnapprovedSetApprovalForAllTransaction = (opts?: { + showAdvanceDetails: boolean; +}) => { + const account = + mockMetaMaskState.internalAccounts.accounts[ + mockMetaMaskState.internalAccounts + .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts + ]; + + return { + ...mockMetaMaskState, + preferences: { + ...mockMetaMaskState.preferences, + redesignedConfirmationsEnabled: true, + showConfirmationAdvancedDetails: opts?.showAdvanceDetails ?? false, + }, + pendingApprovals: { + [pendingTransactionId]: { + id: pendingTransactionId, + origin: 'origin', + time: pendingTransactionTime, + type: ApprovalType.Transaction, + requestData: { + txId: pendingTransactionId, + }, + requestState: null, + expectsResult: false, + }, + }, + pendingApprovalCount: 1, + knownMethodData: { + '0xa22cb465': { + name: 'setApprovalForAll', + params: [ + { + type: 'address', + }, + { + type: 'bool', + }, + ], + }, + }, + transactions: [ + getUnapprovedSetApprovalForAllTransaction( + account.address, + pendingTransactionId, + pendingTransactionTime, + ), + ], + }; +}; + +const advancedDetailsMockedRequests = { + getGasFeeTimeEstimate: { + lowerTimeBound: new Date().getTime(), + upperTimeBound: new Date().getTime(), + }, + getNextNonce: '9', + decodeTransactionData: { + data: [ + { + name: 'setApprovalForAll', + params: [ + { + type: 'address', + value: '0x2e0D7E8c45221FcA00d74a3609A0f7097035d09B', + }, + { + type: 'bool', + value: true, + }, + ], + }, + ], + source: 'FourByte', + }, +}; + +const setupSubmitRequestToBackgroundMocks = ( + mockRequests?: Record, +) => { + mockedBackgroundConnection.submitRequestToBackground.mockImplementation( + createMockImplementation({ + ...advancedDetailsMockedRequests, + ...(mockRequests ?? {}), + }), + ); + + mockedBackgroundConnection.callBackgroundMethod.mockImplementation( + createMockImplementation({ addKnownMethodData: {} }), + ); +}; + +describe('ERC721 setApprovalForAll Confirmation', () => { + beforeAll(() => { + const { provider } = createTestProviderTools({ + networkId: 'sepolia', + chainId: '0xaa36a7', + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + global.ethereumProvider = provider as any; + }); + + beforeEach(() => { + jest.resetAllMocks(); + setupSubmitRequestToBackgroundMocks({ + getTokenStandardAndDetails: { + standard: TokenStandard.ERC721, + }, + }); + const INCREASE_SET_APPROVAL_FOR_ALL_HEX_SIG = '0xa22cb465'; + const INCREASE_SET_APPROVAL_FOR_ALL_TEXT_SIG = + 'setApprovalForAll(address,bool)'; + mock4byte( + INCREASE_SET_APPROVAL_FOR_ALL_HEX_SIG, + INCREASE_SET_APPROVAL_FOR_ALL_TEXT_SIG, + ); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + afterAll(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (global as any).ethereumProvider; + }); + + it('displays set approval for all request title', async () => { + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedSetApprovalForAllTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + expect( + screen.getByText(tEn('setApprovalForAllRedesignedTitle') as string), + ).toBeInTheDocument(); + expect( + screen.getByText(tEn('confirmTitleDescApproveTransaction') as string), + ).toBeInTheDocument(); + }); + + it('displays set approval for all simulation section', async () => { + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedSetApprovalForAllTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + const simulationSection = screen.getByTestId( + 'confirmation__simulation_section', + ); + expect(simulationSection).toBeInTheDocument(); + + expect(simulationSection).toHaveTextContent( + tEn('simulationDetailsSetApprovalForAllDesc') as string, + ); + expect(simulationSection).toHaveTextContent(tEn('withdrawing') as string); + const spendingCapValue = screen.getByTestId('simulation-token-value'); + expect(simulationSection).toContainElement(spendingCapValue); + expect(spendingCapValue).toHaveTextContent(tEn('all') as string); + expect(simulationSection).toHaveTextContent('0x07614...3ad68'); + }); + + it('displays approve details with correct data', async () => { + const testUser = userEvent.setup(); + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedSetApprovalForAllTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + const approveDetails = screen.getByTestId('confirmation__approve-details'); + expect(approveDetails).toBeInTheDocument(); + const approveDetailsSpender = screen.getByTestId( + 'confirmation__approve-spender', + ); + + expect(approveDetails).toContainElement(approveDetailsSpender); + expect(approveDetailsSpender).toHaveTextContent( + tEn('permissionFor') as string, + ); + expect(approveDetailsSpender).toHaveTextContent('0x2e0D7...5d09B'); + const spenderTooltip = screen.getByTestId( + 'confirmation__approve-spender-tooltip', + ); + expect(approveDetailsSpender).toContainElement(spenderTooltip); + await testUser.hover(spenderTooltip); + + const spenderTooltipContent = await screen.findByText( + tEn('spenderTooltipDesc') as string, + ); + expect(spenderTooltipContent).toBeInTheDocument(); + + const approveDetailsRequestFrom = screen.getByTestId( + 'transaction-details-origin-row', + ); + expect(approveDetails).toContainElement(approveDetailsRequestFrom); + expect(approveDetailsRequestFrom).toHaveTextContent( + tEn('requestFrom') as string, + ); + expect(approveDetailsRequestFrom).toHaveTextContent( + 'http://localhost:8086/', + ); + + const approveDetailsRequestFromTooltip = screen.getByTestId( + 'transaction-details-origin-row-tooltip', + ); + expect(approveDetailsRequestFrom).toContainElement( + approveDetailsRequestFromTooltip, + ); + await testUser.hover(approveDetailsRequestFromTooltip); + const requestFromTooltipContent = await screen.findByText( + tEn('requestFromTransactionDescription') as string, + ); + expect(requestFromTooltipContent).toBeInTheDocument(); + }); + + it('displays the advanced transaction details section', async () => { + const testUser = userEvent.setup(); + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedSetApprovalForAllTransaction({ + showAdvanceDetails: true, + }); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + const approveDetails = screen.getByTestId('confirmation__approve-details'); + expect(approveDetails).toBeInTheDocument(); + + const approveDetailsRecipient = screen.getByTestId( + 'transaction-details-recipient-row', + ); + expect(approveDetails).toContainElement(approveDetailsRecipient); + expect(approveDetailsRecipient).toHaveTextContent( + tEn('interactingWith') as string, + ); + expect(approveDetailsRecipient).toHaveTextContent('0x07614...3ad68'); + + const approveDetailsRecipientTooltip = screen.getByTestId( + 'transaction-details-recipient-row-tooltip', + ); + expect(approveDetailsRecipient).toContainElement( + approveDetailsRecipientTooltip, + ); + await testUser.hover(approveDetailsRecipientTooltip); + const recipientTooltipContent = await screen.findByText( + tEn('interactingWithTransactionDescription') as string, + ); + expect(recipientTooltipContent).toBeInTheDocument(); + + const approveMethodData = await screen.findByTestId( + 'transaction-details-method-data-row', + ); + expect(approveDetails).toContainElement(approveMethodData); + expect(approveMethodData).toHaveTextContent(tEn('methodData') as string); + expect(approveMethodData).toHaveTextContent('setApprovalForAll'); + const approveMethodDataTooltip = screen.getByTestId( + 'transaction-details-method-data-row-tooltip', + ); + expect(approveMethodData).toContainElement(approveMethodDataTooltip); + await testUser.hover(approveMethodDataTooltip); + const approveMethodDataTooltipContent = await screen.findByText( + tEn('methodDataTransactionDesc') as string, + ); + expect(approveMethodDataTooltipContent).toBeInTheDocument(); + + const approveDetailsNonce = screen.getByTestId( + 'advanced-details-nonce-section', + ); + expect(approveDetailsNonce).toBeInTheDocument(); + + const dataSection = screen.getByTestId('advanced-details-data-section'); + expect(dataSection).toBeInTheDocument(); + + const dataSectionFunction = screen.getByTestId( + 'advanced-details-data-function', + ); + expect(dataSection).toContainElement(dataSectionFunction); + expect(dataSectionFunction).toHaveTextContent( + tEn('transactionDataFunction') as string, + ); + expect(dataSectionFunction).toHaveTextContent('setApprovalForAll'); + + const approveDataParams1 = screen.getByTestId( + 'advanced-details-data-param-0', + ); + expect(dataSection).toContainElement(approveDataParams1); + expect(approveDataParams1).toHaveTextContent('Param #1'); + expect(approveDataParams1).toHaveTextContent('0x2e0D7...5d09B'); + + const approveDataParams2 = screen.getByTestId( + 'advanced-details-data-param-1', + ); + expect(dataSection).toContainElement(approveDataParams2); + expect(approveDataParams2).toHaveTextContent('Param #2'); + expect(approveDataParams2).toHaveTextContent('true'); + }); +}); diff --git a/test/integration/confirmations/transactions/transactionDataHelpers.tsx b/test/integration/confirmations/transactions/transactionDataHelpers.tsx index 12550ea5e563..e9bcd7b818f2 100644 --- a/test/integration/confirmations/transactions/transactionDataHelpers.tsx +++ b/test/integration/confirmations/transactions/transactionDataHelpers.tsx @@ -1,6 +1,6 @@ import { TransactionType } from '@metamask/transaction-controller'; -export const getUnapprovedTransaction = ( +export const getUnapprovedContractInteractionTransaction = ( accountAddress: string, pendingTransactionId: string, pendingTransactionTime: number, @@ -70,37 +70,105 @@ export const getUnapprovedTransaction = ( }; }; +export const getUnapprovedContractDeploymentTransaction = ( + accountAddress: string, + pendingTransactionId: string, + pendingTransactionTime: number, +) => { + return { + ...getUnapprovedContractInteractionTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ), + txParams: { + ...getUnapprovedContractInteractionTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ).txParams, + data: '0xd0e30db0', + }, + type: TransactionType.deployContract, + }; +}; + export const getUnapprovedApproveTransaction = ( accountAddress: string, pendingTransactionId: string, pendingTransactionTime: number, ) => { return { - ...getUnapprovedTransaction( + ...getUnapprovedContractInteractionTransaction( accountAddress, pendingTransactionId, pendingTransactionTime, ), txParams: { - from: accountAddress, + ...getUnapprovedContractInteractionTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ).txParams, data: '0x095ea7b30000000000000000000000002e0d7e8c45221fca00d74a3609a0f7097035d09b0000000000000000000000000000000000000000000000000000000000000001', - gas: '0x16a92', - to: '0x076146c765189d51be3160a2140cf80bfc73ad68', - value: '0x0', - maxFeePerGas: '0x5b06b0c0d', - maxPriorityFeePerGas: '0x59682f00', }, type: TransactionType.tokenMethodApprove, }; }; +export const getUnapprovedIncreaseAllowanceTransaction = ( + accountAddress: string, + pendingTransactionId: string, + pendingTransactionTime: number, +) => { + return { + ...getUnapprovedContractInteractionTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ), + txParams: { + ...getUnapprovedContractInteractionTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ).txParams, + data: '0x395093510000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000007530', + }, + type: TransactionType.tokenMethodIncreaseAllowance, + }; +}; + +export const getUnapprovedSetApprovalForAllTransaction = ( + accountAddress: string, + pendingTransactionId: string, + pendingTransactionTime: number, +) => { + return { + ...getUnapprovedContractInteractionTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ), + txParams: { + ...getUnapprovedContractInteractionTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ).txParams, + data: '0xa22cb4650000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000000001', + }, + type: TransactionType.tokenMethodSetApprovalForAll, + }; +}; + export const getMaliciousUnapprovedTransaction = ( accountAddress: string, pendingTransactionId: string, pendingTransactionTime: number, ) => { return { - ...getUnapprovedTransaction( + ...getUnapprovedContractInteractionTransaction( accountAddress, pendingTransactionId, pendingTransactionTime, diff --git a/test/integration/data/integration-init-state.json b/test/integration/data/integration-init-state.json index b031611a06ea..82c55c9bd7e0 100644 --- a/test/integration/data/integration-init-state.json +++ b/test/integration/data/integration-init-state.json @@ -782,7 +782,6 @@ "showFiatInTestnets": false, "showTestNetworks": true, "smartTransactionsOptInStatus": false, - "useNativeCurrencyAsPrimaryCurrency": true, "petnamesEnabled": false, "showConfirmationAdvancedDetails": false }, diff --git a/test/integration/data/onboarding-completion-route.json b/test/integration/data/onboarding-completion-route.json index 06d85e298409..e651e9c2ce29 100644 --- a/test/integration/data/onboarding-completion-route.json +++ b/test/integration/data/onboarding-completion-route.json @@ -224,7 +224,6 @@ "showFiatInTestnets": false, "showTestNetworks": false, "smartTransactionsOptInStatus": null, - "useNativeCurrencyAsPrimaryCurrency": true, "hideZeroBalanceTokens": false, "petnamesEnabled": true, "redesignedConfirmationsEnabled": true, diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index 736b9c4eb325..a18f2e0b6944 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -705,16 +705,23 @@ export const createSwapsMockStore = () => { export const createBridgeMockStore = ( featureFlagOverrides = {}, bridgeSliceOverrides = {}, + bridgeStateOverrides = {}, + metamaskStateOverrides = {}, ) => { const swapsStore = createSwapsMockStore(); return { ...swapsStore, bridge: { - toChain: null, + toChainId: null, ...bridgeSliceOverrides, }, metamask: { ...swapsStore.metamask, + ...mockNetworkState( + { chainId: CHAIN_IDS.MAINNET }, + { chainId: CHAIN_IDS.LINEA_MAINNET }, + ), + ...metamaskStateOverrides, bridgeState: { ...(swapsStore.metamask.bridgeState ?? {}), bridgeFeatureFlags: { @@ -723,11 +730,8 @@ export const createBridgeMockStore = ( destNetworkAllowlist: [], ...featureFlagOverrides, }, + ...bridgeStateOverrides, }, - ...mockNetworkState( - { chainId: CHAIN_IDS.MAINNET }, - { chainId: CHAIN_IDS.LINEA_MAINNET }, - ), }, }; }; diff --git a/test/jest/setup.js b/test/jest/setup.js index 0ee19a4d61b8..77fbb92783bc 100644 --- a/test/jest/setup.js +++ b/test/jest/setup.js @@ -1,6 +1,14 @@ // This file is for Jest-specific setup only and runs before our Jest tests. import '../helpers/setup-after-helper'; +jest.mock('webextension-polyfill', () => { + return { + runtime: { + getManifest: () => ({ manifest_version: 2 }), + }, + }; +}); + jest.mock('../../ui/hooks/usePetnamesEnabled', () => ({ usePetnamesEnabled: () => false, })); diff --git a/ui/components/app/assets/asset-list/asset-list.tsx b/ui/components/app/assets/asset-list/asset-list.tsx index ebc78c3ab378..a84ec99037f9 100644 --- a/ui/components/app/assets/asset-list/asset-list.tsx +++ b/ui/components/app/assets/asset-list/asset-list.tsx @@ -39,10 +39,6 @@ import { import { useAccountTotalFiatBalance } from '../../../../hooks/useAccountTotalFiatBalance'; import { useIsOriginalNativeTokenSymbol } from '../../../../hooks/useIsOriginalNativeTokenSymbol'; import { useI18nContext } from '../../../../hooks/useI18nContext'; -import { - showPrimaryCurrency, - showSecondaryCurrency, -} from '../../../../../shared/modules/currency-display.utils'; import { roundToDecimalPlacesRemovingExtraZeroes } from '../../../../helpers/utils/util'; import { FundingMethodModal } from '../../../multichain/funding-method-modal/funding-method-modal'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) @@ -70,7 +66,7 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { const nativeCurrency = useSelector(getMultichainNativeCurrency); const showFiat = useSelector(getMultichainShouldShowFiat); const isMainnet = useSelector(getMultichainIsMainnet); - const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences); + const { showNativeTokenAsMainBalance } = useSelector(getPreferences); const { chainId, ticker, type, rpcUrl } = useSelector( getMultichainCurrentNetwork, ); @@ -92,11 +88,17 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { const { currency: primaryCurrency, numberOfDecimals: primaryNumberOfDecimals, - } = useUserPreferencedCurrency(PRIMARY, { ethNumberOfDecimals: 4 }); + } = useUserPreferencedCurrency(PRIMARY, { + ethNumberOfDecimals: 4, + shouldCheckShowNativeToken: true, + }); const { currency: secondaryCurrency, numberOfDecimals: secondaryNumberOfDecimals, - } = useUserPreferencedCurrency(SECONDARY, { ethNumberOfDecimals: 4 }); + } = useUserPreferencedCurrency(SECONDARY, { + ethNumberOfDecimals: 4, + shouldCheckShowNativeToken: true, + }); const [primaryCurrencyDisplay, primaryCurrencyProperties] = useCurrencyDisplay(balance, { @@ -195,25 +197,14 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { title={nativeCurrency} // The primary and secondary currencies are subject to change based on the user's settings // TODO: rename this primary/secondary concept here to be more intuitive, regardless of setting - primary={ - showSecondaryCurrency( - isOriginalNativeSymbol, - useNativeCurrencyAsPrimaryCurrency, - ) - ? secondaryCurrencyDisplay - : undefined - } + primary={isOriginalNativeSymbol ? secondaryCurrencyDisplay : undefined} tokenSymbol={ - useNativeCurrencyAsPrimaryCurrency + showNativeTokenAsMainBalance ? primaryCurrencyProperties.suffix : secondaryCurrencyProperties.suffix } secondary={ - showFiat && - showPrimaryCurrency( - isOriginalNativeSymbol, - useNativeCurrencyAsPrimaryCurrency, - ) + showFiat && isOriginalNativeSymbol ? primaryCurrencyDisplay : undefined } diff --git a/ui/components/app/assets/nfts/nft-details/nft-full-image.tsx b/ui/components/app/assets/nfts/nft-details/nft-full-image.tsx index 64e2a3191c0e..42b4ddacbf95 100644 --- a/ui/components/app/assets/nfts/nft-details/nft-full-image.tsx +++ b/ui/components/app/assets/nfts/nft-details/nft-full-image.tsx @@ -30,6 +30,7 @@ export default function NftFullImage() { const nfts = useSelector(getNfts); const nft = nfts.find( ({ address, tokenId }: { address: string; tokenId: string }) => + // @ts-expect-error TODO: Fix this type error by handling undefined parameters isEqualCaseInsensitive(address, asset) && id === tokenId.toString(), ); diff --git a/ui/components/app/assets/nfts/nfts-items/nfts-items.js b/ui/components/app/assets/nfts/nfts-items/nfts-items.js index ff5d7ea877bf..6e9e45e0b54a 100644 --- a/ui/components/app/assets/nfts/nfts-items/nfts-items.js +++ b/ui/components/app/assets/nfts/nfts-items/nfts-items.js @@ -157,7 +157,7 @@ export default function NftsItems({ const updateNftDropDownStateKey = (key, isExpanded) => { const newCurrentAccountState = { - ...nftsDropdownState[selectedAddress][chainId], + ...nftsDropdownState?.[selectedAddress]?.[chainId], [key]: !isExpanded, }; diff --git a/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.tsx.snap b/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.tsx.snap index 4eeeb5603d46..dfed6aeffa98 100644 --- a/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.tsx.snap +++ b/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.tsx.snap @@ -52,7 +52,7 @@ exports[`Token Cell should match snapshot 1`] = ` class="mm-box mm-box--display-flex" >

@@ -67,7 +67,7 @@ exports[`Token Cell should match snapshot 1`] = ` 5.00

5 diff --git a/ui/components/app/assets/token-cell/token-cell.test.tsx b/ui/components/app/assets/token-cell/token-cell.test.tsx index 70714e9975f8..882c80964d5b 100644 --- a/ui/components/app/assets/token-cell/token-cell.test.tsx +++ b/ui/components/app/assets/token-cell/token-cell.test.tsx @@ -6,9 +6,13 @@ import { useSelector } from 'react-redux'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; import { useTokenFiatAmount } from '../../../../hooks/useTokenFiatAmount'; import { getTokenList } from '../../../../selectors'; -import { getMultichainCurrentChainId } from '../../../../selectors/multichain'; +import { + getMultichainCurrentChainId, + getMultichainIsEvm, +} from '../../../../selectors/multichain'; import { useIsOriginalTokenSymbol } from '../../../../hooks/useIsOriginalTokenSymbol'; +import { getIntlLocale } from '../../../../ducks/locale/locale'; import TokenCell from '.'; jest.mock('react-redux', () => { @@ -100,6 +104,12 @@ describe('Token Cell', () => { if (selector === getMultichainCurrentChainId) { return '0x89'; } + if (selector === getMultichainIsEvm) { + return true; + } + if (selector === getIntlLocale) { + return 'en-US'; + } return undefined; }); (useTokenFiatAmount as jest.Mock).mockReturnValue('5.00'); diff --git a/ui/components/app/clear-metametrics-data/clear-metametrics-data.test.tsx b/ui/components/app/clear-metametrics-data/clear-metametrics-data.test.tsx new file mode 100644 index 000000000000..5ce4ac7573dc --- /dev/null +++ b/ui/components/app/clear-metametrics-data/clear-metametrics-data.test.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { fireEvent } from '@testing-library/react'; +import configureStore from '../../../store/store'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import * as Actions from '../../../store/actions'; +import { DELETE_METAMETRICS_DATA_MODAL_CLOSE } from '../../../store/actionConstants'; +import ClearMetaMetricsData from './clear-metametrics-data'; + +const mockCloseDeleteMetaMetricsDataModal = jest.fn().mockImplementation(() => { + return { + type: DELETE_METAMETRICS_DATA_MODAL_CLOSE, + }; +}); + +jest.mock('../../../store/actions', () => ({ + createMetaMetricsDataDeletionTask: jest.fn(), +})); + +jest.mock('../../../ducks/app/app.ts', () => { + return { + hideDeleteMetaMetricsDataModal: () => { + return mockCloseDeleteMetaMetricsDataModal(); + }, + }; +}); + +describe('ClearMetaMetricsData', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render the data deletion error modal', async () => { + const store = configureStore({}); + const { getByText } = renderWithProvider(, store); + + expect(getByText('Delete MetaMetrics data?')).toBeInTheDocument(); + expect( + getByText( + 'We are about to remove all your MetaMetrics data. Are you sure?', + ), + ).toBeInTheDocument(); + }); + + it('should call createMetaMetricsDataDeletionTask when Clear button is clicked', () => { + const store = configureStore({}); + const { getByText } = renderWithProvider(, store); + expect(getByText('Clear')).toBeEnabled(); + fireEvent.click(getByText('Clear')); + expect(Actions.createMetaMetricsDataDeletionTask).toHaveBeenCalledTimes(1); + }); + + it('should call hideDeleteMetaMetricsDataModal when Cancel button is clicked', () => { + const store = configureStore({}); + const { getByText } = renderWithProvider(, store); + expect(getByText('Cancel')).toBeEnabled(); + fireEvent.click(getByText('Cancel')); + expect(mockCloseDeleteMetaMetricsDataModal).toHaveBeenCalledTimes(1); + }); +}); diff --git a/ui/components/app/clear-metametrics-data/clear-metametrics-data.tsx b/ui/components/app/clear-metametrics-data/clear-metametrics-data.tsx new file mode 100644 index 000000000000..019c115eceac --- /dev/null +++ b/ui/components/app/clear-metametrics-data/clear-metametrics-data.tsx @@ -0,0 +1,130 @@ +import React, { useContext } from 'react'; +import { useDispatch } from 'react-redux'; +import { + hideDeleteMetaMetricsDataModal, + openDataDeletionErrorModal, +} from '../../../ducks/app/app'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { + Box, + Button, + ButtonSize, + ButtonVariant, + Modal, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Text, +} from '../../component-library'; +import { + AlignItems, + BlockSize, + Display, + FlexDirection, + JustifyContent, + TextVariant, +} from '../../../helpers/constants/design-system'; +import { createMetaMetricsDataDeletionTask } from '../../../store/actions'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; + +export default function ClearMetaMetricsData() { + const t = useI18nContext(); + const dispatch = useDispatch(); + const trackEvent = useContext(MetaMetricsContext); + + const closeModal = () => { + dispatch(hideDeleteMetaMetricsDataModal()); + }; + + const deleteMetaMetricsData = async () => { + try { + await createMetaMetricsDataDeletionTask(); + trackEvent( + { + category: MetaMetricsEventCategory.Settings, + event: MetaMetricsEventName.MetricsDataDeletionRequest, + }, + { + excludeMetaMetricsId: true, + }, + ); + } catch (error: unknown) { + dispatch(openDataDeletionErrorModal()); + trackEvent( + { + category: MetaMetricsEventCategory.Settings, + event: MetaMetricsEventName.ErrorOccured, + }, + { + excludeMetaMetricsId: true, + }, + ); + } finally { + dispatch(hideDeleteMetaMetricsDataModal()); + } + }; + + return ( + + + + + + + {t('deleteMetaMetricsDataModalTitle')} + + + + + + {t('deleteMetaMetricsDataModalDesc')} + + + + + + + + + + + ); +} diff --git a/ui/components/app/clear-metametrics-data/index.ts b/ui/components/app/clear-metametrics-data/index.ts new file mode 100644 index 000000000000..b29aee18d564 --- /dev/null +++ b/ui/components/app/clear-metametrics-data/index.ts @@ -0,0 +1 @@ +export { default } from './clear-metametrics-data'; diff --git a/ui/components/app/confirm/info/row/__snapshots__/address.test.tsx.snap b/ui/components/app/confirm/info/row/__snapshots__/address.test.tsx.snap index 292be0318ce6..ec2eacb0d44b 100644 --- a/ui/components/app/confirm/info/row/__snapshots__/address.test.tsx.snap +++ b/ui/components/app/confirm/info/row/__snapshots__/address.test.tsx.snap @@ -315,7 +315,9 @@ exports[`ConfirmInfoRowAddress renders appropriately with PetNames enabled 1`] =

-
+
diff --git a/ui/components/app/confirm/info/row/__snapshots__/expandable-row.test.tsx.snap b/ui/components/app/confirm/info/row/__snapshots__/expandable-row.test.tsx.snap index b11a8d89bd87..c3958d886710 100644 --- a/ui/components/app/confirm/info/row/__snapshots__/expandable-row.test.tsx.snap +++ b/ui/components/app/confirm/info/row/__snapshots__/expandable-row.test.tsx.snap @@ -3,7 +3,7 @@ exports[`ConfirmInfoExpandableRow should match snapshot 1`] = `
{story()}] + decorators: [(story: any) => {story()}], }; export const DefaultStory = ({ variant, value }) => ( diff --git a/ui/components/app/confirm/info/row/currency.tsx b/ui/components/app/confirm/info/row/currency.tsx index 82ce82c3a113..51ce1fceba28 100644 --- a/ui/components/app/confirm/info/row/currency.tsx +++ b/ui/components/app/confirm/info/row/currency.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { PRIMARY } from '../../../../../helpers/constants/common'; import { AlignItems, Display, @@ -38,7 +37,7 @@ export const ConfirmInfoRowCurrency = ({ {currency ? ( ) : ( - + )} ); diff --git a/ui/components/app/confirm/info/row/row.tsx b/ui/components/app/confirm/info/row/row.tsx index 5e74552e62e2..7616ccae5f21 100644 --- a/ui/components/app/confirm/info/row/row.tsx +++ b/ui/components/app/confirm/info/row/row.tsx @@ -90,6 +90,7 @@ export const ConfirmInfoRow: React.FC = ({ flexDirection={FlexDirection.Row} justifyContent={JustifyContent.spaceBetween} flexWrap={FlexWrap.Wrap} + alignItems={AlignItems.center} backgroundColor={BACKGROUND_COLORS[variant]} borderRadius={BorderRadius.LG} marginTop={2} diff --git a/ui/components/app/data-deletion-error-modal/data-deletion-error-modal.test.tsx b/ui/components/app/data-deletion-error-modal/data-deletion-error-modal.test.tsx new file mode 100644 index 000000000000..cbb541f5648e --- /dev/null +++ b/ui/components/app/data-deletion-error-modal/data-deletion-error-modal.test.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; +import { fireEvent } from '@testing-library/react'; +import configureStore from '../../../store/store'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import { DATA_DELETION_ERROR_MODAL_CLOSE } from '../../../store/actionConstants'; + +import DataDeletionErrorModal from './data-deletion-error-modal'; + +const mockCloseDeleteMetaMetricsErrorModal = jest + .fn() + .mockImplementation(() => { + return { + type: DATA_DELETION_ERROR_MODAL_CLOSE, + }; + }); + +jest.mock('../../../ducks/app/app.ts', () => { + return { + hideDataDeletionErrorModal: () => { + return mockCloseDeleteMetaMetricsErrorModal(); + }, + }; +}); + +describe('DataDeletionErrorModal', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render data deletion error modal', async () => { + const store = configureStore({}); + const { getByText } = renderWithProvider(, store); + + expect( + getByText('We are unable to delete this data right now'), + ).toBeInTheDocument(); + expect( + getByText( + "This request can't be completed right now due to an analytics system server issue, please try again later", + ), + ).toBeInTheDocument(); + }); + + it('should call hideDeleteMetaMetricsDataModal when Ok button is clicked', () => { + const store = configureStore({}); + const { getByText } = renderWithProvider(, store); + expect(getByText('Ok')).toBeEnabled(); + fireEvent.click(getByText('Ok')); + expect(mockCloseDeleteMetaMetricsErrorModal).toHaveBeenCalledTimes(1); + }); +}); diff --git a/ui/components/app/data-deletion-error-modal/data-deletion-error-modal.tsx b/ui/components/app/data-deletion-error-modal/data-deletion-error-modal.tsx new file mode 100644 index 000000000000..0b6be4fa782b --- /dev/null +++ b/ui/components/app/data-deletion-error-modal/data-deletion-error-modal.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { + Display, + FlexDirection, + AlignItems, + JustifyContent, + TextVariant, + BlockSize, + IconColor, + TextAlign, +} from '../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { + ModalOverlay, + ModalContent, + ModalHeader, + Modal, + Box, + Text, + ModalFooter, + Button, + IconName, + ButtonVariant, + Icon, + IconSize, + ButtonSize, +} from '../../component-library'; +import { hideDataDeletionErrorModal } from '../../../ducks/app/app'; + +export default function DataDeletionErrorModal() { + const t = useI18nContext(); + const dispatch = useDispatch(); + + function closeModal() { + dispatch(hideDataDeletionErrorModal()); + } + + return ( + + + + + + + + {t('deleteMetaMetricsDataErrorTitle')} + + + + + + + {t('deleteMetaMetricsDataErrorDesc')} + + + + + + + + + + + ); +} diff --git a/ui/components/app/data-deletion-error-modal/index.ts b/ui/components/app/data-deletion-error-modal/index.ts new file mode 100644 index 000000000000..383efd7029b5 --- /dev/null +++ b/ui/components/app/data-deletion-error-modal/index.ts @@ -0,0 +1 @@ +export { default } from './data-deletion-error-modal'; diff --git a/ui/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.test.js b/ui/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.test.js index 05bce9e841a0..8966fa01b749 100644 --- a/ui/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.test.js +++ b/ui/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.test.js @@ -11,9 +11,7 @@ describe('CancelTransactionGasFee Component', () => { metamask: { ...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }), currencyRates: {}, - preferences: { - useNativeCurrencyAsPrimaryCurrency: false, - }, + preferences: {}, completedOnboarding: true, internalAccounts: mockState.metamask.internalAccounts, }, diff --git a/ui/components/app/modals/customize-nonce/__snapshots__/customize-nonce.test.js.snap b/ui/components/app/modals/customize-nonce/__snapshots__/customize-nonce.test.js.snap index cab80e399a43..020adaa0c952 100644 --- a/ui/components/app/modals/customize-nonce/__snapshots__/customize-nonce.test.js.snap +++ b/ui/components/app/modals/customize-nonce/__snapshots__/customize-nonce.test.js.snap @@ -75,7 +75,7 @@ exports[`Customize Nonce should match snapshot 1`] = ` class="MuiFormControl-root MuiTextField-root MuiFormControl-marginDense MuiFormControl-fullWidth" >
-
+
@@ -24,7 +26,9 @@ exports[`Name renders address with image 1`] = ` exports[`Name renders address with no saved name 1`] = `
-
+
@@ -44,7 +48,9 @@ exports[`Name renders address with no saved name 1`] = ` exports[`Name renders address with saved name 1`] = `
-
+
@@ -104,7 +110,9 @@ exports[`Name renders address with saved name 1`] = ` exports[`Name renders when no address value is passed 1`] = `
-
+
diff --git a/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap b/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap index 6ee9430c0fde..a6d0df79843d 100644 --- a/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap +++ b/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap @@ -66,7 +66,9 @@ exports[`NameDetails renders proposed names 1`] = `
-
+
@@ -326,7 +328,9 @@ exports[`NameDetails renders when no address value is passed 1`] = `
-
+
@@ -509,7 +513,9 @@ exports[`NameDetails renders with no saved name 1`] = `
-
+
@@ -694,7 +700,9 @@ exports[`NameDetails renders with recognized name 1`] = `
-
+
@@ -884,7 +892,9 @@ exports[`NameDetails renders with saved name 1`] = `
-
+
diff --git a/ui/components/app/name/name.tsx b/ui/components/app/name/name.tsx index d2684c188838..5af2851c8885 100644 --- a/ui/components/app/name/name.tsx +++ b/ui/components/app/name/name.tsx @@ -8,14 +8,14 @@ import React, { import { NameType } from '@metamask/name-controller'; import classnames from 'classnames'; import { toChecksumAddress } from 'ethereumjs-util'; -import { Icon, IconName, IconSize, Text } from '../../component-library'; +import { Box, Icon, IconName, IconSize, Text } from '../../component-library'; import { shortenAddress } from '../../../helpers/utils/util'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { MetaMetricsEventCategory, MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; -import { TextVariant } from '../../../helpers/constants/design-system'; +import { Display, TextVariant } from '../../../helpers/constants/design-system'; import { useDisplayName } from '../../../hooks/useDisplayName'; import Identicon from '../../ui/identicon'; import NameDetails from './name-details/name-details'; @@ -98,7 +98,7 @@ const Name = memo( const hasDisplayName = Boolean(name); return ( -
+ {!disableEdit && modalOpen && ( )} @@ -130,7 +130,7 @@ const Name = memo( )}
-
+ ); }, ); diff --git a/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js b/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js index da15d384849c..e0bed04e5429 100644 --- a/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js +++ b/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js @@ -7,6 +7,7 @@ import { getSnapsMetadata } from '../../../selectors'; import { getSnapName } from '../../../helpers/utils/util'; import PermissionCell from '../permission-cell'; import { Box } from '../../component-library'; +import { CaveatTypes } from '../../../../shared/constants/permissions'; /** * Get one or more permission descriptions for a permission name. @@ -17,6 +18,10 @@ import { Box } from '../../component-library'; * @returns {JSX.Element} A permission description node. */ function getDescriptionNode(permission, index, accounts) { + const permissionValue = permission?.permissionValue?.caveats?.find( + (caveat) => caveat.type === CaveatTypes.restrictNetworkSwitching, + )?.value; + return ( ); } diff --git a/ui/components/app/snaps/copyable/index.scss b/ui/components/app/snaps/copyable/index.scss index 7cfed455aade..4fa96c5d7604 100644 --- a/ui/components/app/snaps/copyable/index.scss +++ b/ui/components/app/snaps/copyable/index.scss @@ -3,7 +3,7 @@ transition: background-color background 0.2s; & .show-more__button { - background: linear-gradient(90deg, transparent 0%, var(--color-background-primary-muted) 33%); + background: linear-gradient(90deg, transparent 0%, var(--color-primary-muted) 33%); } &:hover { @@ -11,7 +11,7 @@ color: var(--color-primary-default) !important; & .show-more__button { - background: linear-gradient(90deg, transparent 0%, var(--color-background-primary-muted) 33%); + background: linear-gradient(90deg, transparent 0%, var(--color-primary-muted) 33%); } p, @@ -31,14 +31,14 @@ opacity: 0.75; & .show-more__button { - background: linear-gradient(90deg, transparent 0%, var(--color-background-primary-muted) 33%); + background: linear-gradient(90deg, transparent 0%, var(--color-primary-muted) 33%); } &:hover { background-color: var(--color-primary-muted); & .show-more__button { - background: linear-gradient(90deg, transparent 0%, var(--color-background-primary-muted) 33%); + background: linear-gradient(90deg, transparent 0%, var(--color-primary-muted) 33%); } } } diff --git a/ui/components/app/snaps/show-more/show-more.js b/ui/components/app/snaps/show-more/show-more.js index d6939b9f546f..03bf0c0994fd 100644 --- a/ui/components/app/snaps/show-more/show-more.js +++ b/ui/components/app/snaps/show-more/show-more.js @@ -41,7 +41,7 @@ export const ShowMore = ({ children, className = '', ...props }) => { bottom: 0, right: 0, // Avoids see-through with muted colors - background: `linear-gradient(90deg, transparent 0%, var(--color-${BackgroundColor.backgroundDefault}) 33%)`, + background: `linear-gradient(90deg, transparent 0%, var(--color-${BackgroundColor.backgroundAlternative}) 33%)`, }} > +
+
+
+ +
+
+`; diff --git a/ui/components/ui/survey-toast/index.ts b/ui/components/ui/survey-toast/index.ts new file mode 100644 index 000000000000..8926c3e7dfd9 --- /dev/null +++ b/ui/components/ui/survey-toast/index.ts @@ -0,0 +1 @@ +export { SurveyToast } from './survey-toast'; diff --git a/ui/components/ui/survey-toast/survey-toast.test.tsx b/ui/components/ui/survey-toast/survey-toast.test.tsx new file mode 100644 index 000000000000..c4b90ec27a5e --- /dev/null +++ b/ui/components/ui/survey-toast/survey-toast.test.tsx @@ -0,0 +1,153 @@ +import React from 'react'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { act } from 'react-dom/test-utils'; +import fetchWithCache from '../../../../shared/lib/fetch-with-cache'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import { SurveyToast } from './survey-toast'; + +jest.mock('../../../../shared/lib/fetch-with-cache', () => ({ + __esModule: true, + default: jest.fn(), +})); + +const mockFetchWithCache = fetchWithCache as jest.Mock; +const mockTrackEvent = jest.fn(); +const mockStore = configureStore([thunk]); + +const surveyData = { + valid: { + url: 'https://example.com', + description: 'Test Survey', + cta: 'Take Survey', + id: 3, + }, + stale: { + url: 'https://example.com', + description: 'Test Survey', + cta: 'Take Survey', + id: 1, + }, +}; + +const createStore = (options = { metametricsEnabled: true }) => + mockStore({ + user: { basicFunctionality: true }, + metamask: { + lastViewedUserSurvey: 2, + useExternalServices: true, + participateInMetaMetrics: options.metametricsEnabled, + metaMetricsId: '0x123', + internalAccounts: { + selectedAccount: '0x123', + accounts: { '0x123': { address: '0x123' } }, + }, + }, + }); + +const renderComponent = (options = { metametricsEnabled: true }) => + renderWithProvider( + + + , + createStore(options), + ); + +describe('SurveyToast', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + + global.platform = { + openTab: jest.fn(), + closeCurrentWindow: jest.fn(), + }; + }); + + afterEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + + it('should match snapshot', async () => { + mockFetchWithCache.mockResolvedValue({ surveys: surveyData.valid }); + + let container; + await act(async () => { + const result = renderComponent(); + container = result.container; + }); + + expect(container).toMatchSnapshot(); + }); + + it('renders nothing if no survey is available', () => { + mockFetchWithCache.mockResolvedValue({ surveys: [] }); + renderComponent(); + expect(screen.queryByTestId('survey-toast')).toBeNull(); + }); + + it('renders nothing if the survey is stale', () => { + mockFetchWithCache.mockResolvedValue({ surveys: surveyData.stale }); + renderComponent(); + expect(screen.queryByTestId('survey-toast')).toBeNull(); + }); + + it('renders the survey toast when a valid survey is available', async () => { + mockFetchWithCache.mockResolvedValue({ surveys: surveyData.valid }); + + await act(async () => { + renderComponent(); + }); + + await waitFor(() => { + expect(screen.getByTestId('survey-toast')).toBeInTheDocument(); + expect( + screen.getByText(surveyData.valid.description), + ).toBeInTheDocument(); + expect(screen.getByText(surveyData.valid.cta)).toBeInTheDocument(); + }); + }); + + it('handles action click correctly when metametrics is enabled', async () => { + mockFetchWithCache.mockResolvedValue({ surveys: surveyData.valid }); + + renderComponent(); + + await waitFor(() => { + expect(screen.getByTestId('survey-toast')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText(surveyData.valid.cta)); + + expect(global.platform.openTab).toHaveBeenCalledWith({ + url: surveyData.valid.url, + }); + expect(mockTrackEvent).toHaveBeenCalledWith({ + event: MetaMetricsEventName.SurveyToast, + category: MetaMetricsEventCategory.Feedback, + properties: { + response: 'accept', + survey: surveyData.valid.id, + }, + }); + }); + + it('should not show the toast if metametrics is disabled', async () => { + mockFetchWithCache.mockResolvedValue({ surveys: surveyData.valid }); + + renderComponent({ + metametricsEnabled: false, + }); + + await waitFor(() => { + expect(screen.queryByTestId('survey-toast')).toBeNull(); + }); + }); +}); diff --git a/ui/components/ui/survey-toast/survey-toast.tsx b/ui/components/ui/survey-toast/survey-toast.tsx new file mode 100644 index 000000000000..7359b9ecd480 --- /dev/null +++ b/ui/components/ui/survey-toast/survey-toast.tsx @@ -0,0 +1,145 @@ +import React, { useEffect, useState, useContext, useMemo } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import fetchWithCache from '../../../../shared/lib/fetch-with-cache'; +import { DAY } from '../../../../shared/constants/time'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import { + getSelectedInternalAccount, + getLastViewedUserSurvey, + getUseExternalServices, + getMetaMetricsId, + getParticipateInMetaMetrics, +} from '../../../selectors'; +import { ACCOUNTS_API_BASE_URL } from '../../../../shared/constants/accounts'; +import { setLastViewedUserSurvey } from '../../../store/actions'; +import { Toast } from '../../multichain'; + +type Survey = { + url: string; + description: string; + cta: string; + id: number; +}; + +export function SurveyToast() { + const [survey, setSurvey] = useState(null); + const dispatch = useDispatch(); + const trackEvent = useContext(MetaMetricsContext); + const lastViewedUserSurvey = useSelector(getLastViewedUserSurvey); + const participateInMetaMetrics = useSelector(getParticipateInMetaMetrics); + const basicFunctionality = useSelector(getUseExternalServices); + const internalAccount = useSelector(getSelectedInternalAccount); + const metaMetricsId = useSelector(getMetaMetricsId); + + const surveyUrl = useMemo( + () => `${ACCOUNTS_API_BASE_URL}/v1/users/${metaMetricsId}/surveys`, + [metaMetricsId], + ); + + useEffect(() => { + if (!basicFunctionality || !metaMetricsId || !participateInMetaMetrics) { + return undefined; + } + + const controller = new AbortController(); + + const fetchSurvey = async () => { + try { + const response = await fetchWithCache({ + url: surveyUrl, + fetchOptions: { + method: 'GET', + headers: { + 'x-metamask-clientproduct': 'metamask-extension', + }, + signal: controller.signal, + }, + functionName: 'fetchSurveys', + cacheOptions: { cacheRefreshTime: process.env.IN_TEST ? 0 : DAY }, + }); + + const _survey: Survey = response?.surveys; + + if ( + !_survey || + Object.keys(_survey).length === 0 || + _survey.id <= lastViewedUserSurvey + ) { + return; + } + + setSurvey(_survey); + } catch (error: unknown) { + if (error instanceof Error && error.name !== 'AbortError') { + console.error('Failed to fetch survey:', metaMetricsId, error); + } + } + }; + + fetchSurvey(); + + return () => { + controller.abort(); + }; + }, [ + internalAccount?.address, + lastViewedUserSurvey, + basicFunctionality, + metaMetricsId, + dispatch, + ]); + + function handleActionClick() { + if (!survey) { + return; + } + global.platform.openTab({ + url: survey.url, + }); + dispatch(setLastViewedUserSurvey(survey.id)); + trackAction('accept'); + } + + function handleClose() { + if (!survey) { + return; + } + dispatch(setLastViewedUserSurvey(survey.id)); + trackAction('deny'); + } + + function trackAction(response: 'accept' | 'deny') { + if (!participateInMetaMetrics || !survey) { + return; + } + + trackEvent({ + event: MetaMetricsEventName.SurveyToast, + category: MetaMetricsEventCategory.Feedback, + properties: { + response, + survey: survey.id, + }, + }); + } + + if (!survey || survey.id <= lastViewedUserSurvey) { + return null; + } + + return ( + + ); +} diff --git a/ui/components/ui/text-field/text-field.component.js b/ui/components/ui/text-field/text-field.component.js index 6ed0a9bff6fb..0a74978973b3 100644 --- a/ui/components/ui/text-field/text-field.component.js +++ b/ui/components/ui/text-field/text-field.component.js @@ -86,13 +86,14 @@ const styles = { border: '1px solid var(--color-border-default)', color: 'var(--color-text-default)', height: '48px', - borderRadius: '6px', padding: '0 16px', display: 'flex', alignItems: 'center', '&$inputFocused': { border: '1px solid var(--color-primary-default)', }, + borderRadius: '8px', + fontSize: '0.875rem', }, largeInputLabel: { ...inputLabelBase, @@ -212,6 +213,7 @@ const getBorderedThemeInputProps = ({ max, autoComplete, }, + disableUnderline: 'true', }, }); diff --git a/ui/ducks/app/app.test.js b/ui/ducks/app/app.test.js index 0d6441454f90..9a7a93ea958b 100644 --- a/ui/ducks/app/app.test.js +++ b/ui/ducks/app/app.test.js @@ -301,4 +301,42 @@ describe('App State', () => { }); expect(state.smartTransactionsError).toStrictEqual('Server Side Error'); }); + it('shows delete metametrics modal', () => { + const state = reduceApp(metamaskState, { + type: actions.DELETE_METAMETRICS_DATA_MODAL_OPEN, + }); + + expect(state.showDeleteMetaMetricsDataModal).toStrictEqual(true); + }); + it('hides delete metametrics modal', () => { + const deleteMetaMetricsDataModalState = { + showDeleteMetaMetricsDataModal: true, + }; + const oldState = { ...metamaskState, ...deleteMetaMetricsDataModalState }; + + const state = reduceApp(oldState, { + type: actions.DELETE_METAMETRICS_DATA_MODAL_CLOSE, + }); + + expect(state.showDeleteMetaMetricsDataModal).toStrictEqual(false); + }); + it('shows delete metametrics error modal', () => { + const state = reduceApp(metamaskState, { + type: actions.DATA_DELETION_ERROR_MODAL_OPEN, + }); + + expect(state.showDataDeletionErrorModal).toStrictEqual(true); + }); + it('hides delete metametrics error modal', () => { + const deleteMetaMetricsErrorModalState = { + showDataDeletionErrorModal: true, + }; + const oldState = { ...metamaskState, ...deleteMetaMetricsErrorModalState }; + + const state = reduceApp(oldState, { + type: actions.DATA_DELETION_ERROR_MODAL_CLOSE, + }); + + expect(state.showDataDeletionErrorModal).toStrictEqual(false); + }); }); diff --git a/ui/ducks/app/app.ts b/ui/ducks/app/app.ts index a2e553f34012..e6a7855ce7a5 100644 --- a/ui/ducks/app/app.ts +++ b/ui/ducks/app/app.ts @@ -100,12 +100,14 @@ type AppState = { customTokenAmount: string; txId: string | null; accountDetailsAddress: string; + showDeleteMetaMetricsDataModal: boolean; + showDataDeletionErrorModal: boolean; snapsInstallPrivacyWarningShown: boolean; isAddingNewNetwork: boolean; isMultiRpcOnboarding: boolean; }; -type AppSliceState = { +export type AppSliceState = { appState: AppState; }; @@ -185,6 +187,8 @@ const initialState: AppState = { scrollToBottom: true, txId: null, accountDetailsAddress: '', + showDeleteMetaMetricsDataModal: false, + showDataDeletionErrorModal: false, snapsInstallPrivacyWarningShown: false, isAddingNewNetwork: false, isMultiRpcOnboarding: false, @@ -608,6 +612,26 @@ export default function reduceApp( isAddingNewNetwork: Boolean(action.payload?.isAddingNewNetwork), isMultiRpcOnboarding: Boolean(action.payload?.isMultiRpcOnboarding), }; + case actionConstants.DELETE_METAMETRICS_DATA_MODAL_OPEN: + return { + ...appState, + showDeleteMetaMetricsDataModal: true, + }; + case actionConstants.DELETE_METAMETRICS_DATA_MODAL_CLOSE: + return { + ...appState, + showDeleteMetaMetricsDataModal: false, + }; + case actionConstants.DATA_DELETION_ERROR_MODAL_OPEN: + return { + ...appState, + showDataDeletionErrorModal: true, + }; + case actionConstants.DATA_DELETION_ERROR_MODAL_CLOSE: + return { + ...appState, + showDataDeletionErrorModal: false, + }; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) case actionConstants.SHOW_KEYRING_SNAP_REMOVAL_RESULT: return { @@ -717,3 +741,27 @@ export function getLedgerWebHidConnectedStatus( export function getLedgerTransportStatus(state: AppSliceState): string | null { return state.appState.ledgerTransportStatus; } + +export function openDeleteMetaMetricsDataModal(): Action { + return { + type: actionConstants.DELETE_METAMETRICS_DATA_MODAL_OPEN, + }; +} + +export function hideDeleteMetaMetricsDataModal(): Action { + return { + type: actionConstants.DELETE_METAMETRICS_DATA_MODAL_CLOSE, + }; +} + +export function openDataDeletionErrorModal(): Action { + return { + type: actionConstants.DATA_DELETION_ERROR_MODAL_OPEN, + }; +} + +export function hideDataDeletionErrorModal(): Action { + return { + type: actionConstants.DATA_DELETION_ERROR_MODAL_CLOSE, + }; +} diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts index 24cd9728625f..5bfbda1e23cf 100644 --- a/ui/ducks/bridge/actions.ts +++ b/ui/ducks/bridge/actions.ts @@ -1,18 +1,29 @@ // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths -import { BridgeBackgroundAction } from '../../../app/scripts/controllers/bridge/types'; +import { Hex } from '@metamask/utils'; +import { + BridgeBackgroundAction, + BridgeUserAction, + // TODO: Remove restricted import + // eslint-disable-next-line import/no-restricted-paths +} from '../../../app/scripts/controllers/bridge/types'; + import { forceUpdateMetamaskState } from '../../store/actions'; import { submitRequestToBackground } from '../../store/background-connection'; import { MetaMaskReduxDispatch } from '../../store/store'; import { bridgeSlice } from './bridge'; -const { setToChain, setFromToken, setToToken, setFromTokenInputValue } = - bridgeSlice.actions; +const { + setToChainId: setToChainId_, + setFromToken, + setToToken, + setFromTokenInputValue, +} = bridgeSlice.actions; -export { setToChain, setFromToken, setToToken, setFromTokenInputValue }; +export { setFromToken, setToToken, setFromTokenInputValue }; const callBridgeControllerMethod = ( - bridgeAction: BridgeBackgroundAction, + bridgeAction: BridgeUserAction | BridgeBackgroundAction, args?: T[], ) => { return async (dispatch: MetaMaskReduxDispatch) => { @@ -21,8 +32,6 @@ const callBridgeControllerMethod = ( }; }; -// User actions - // Background actions export const setBridgeFeatureFlags = () => { return async (dispatch: MetaMaskReduxDispatch) => { @@ -31,3 +40,25 @@ export const setBridgeFeatureFlags = () => { ); }; }; + +// User actions +export const setFromChain = (chainId: Hex) => { + return async (dispatch: MetaMaskReduxDispatch) => { + dispatch( + callBridgeControllerMethod(BridgeUserAction.SELECT_SRC_NETWORK, [ + chainId, + ]), + ); + }; +}; + +export const setToChain = (chainId: Hex) => { + return async (dispatch: MetaMaskReduxDispatch) => { + dispatch(setToChainId_(chainId)); + dispatch( + callBridgeControllerMethod(BridgeUserAction.SELECT_DEST_NETWORK, [ + chainId, + ]), + ); + }; +}; diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts index 0bfdb47b35eb..b8d2e09eb0ea 100644 --- a/ui/ducks/bridge/bridge.test.ts +++ b/ui/ducks/bridge/bridge.test.ts @@ -3,9 +3,12 @@ import thunk from 'redux-thunk'; import { createBridgeMockStore } from '../../../test/jest/mock-store'; import { CHAIN_IDS } from '../../../shared/constants/network'; import { setBackgroundConnection } from '../../store/background-connection'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { BridgeBackgroundAction } from '../../../app/scripts/controllers/bridge/types'; +import { + BridgeBackgroundAction, + BridgeUserAction, + // TODO: Remove restricted import + // eslint-disable-next-line import/no-restricted-paths +} from '../../../app/scripts/controllers/bridge/types'; import bridgeReducer from './bridge'; import { setBridgeFeatureFlags, @@ -13,6 +16,7 @@ import { setFromTokenInputValue, setToChain, setToToken, + setFromChain, } from './actions'; const middleware = [thunk]; @@ -26,14 +30,28 @@ describe('Ducks - Bridge', () => { }); describe('setToChain', () => { - it('calls the "bridge/setToChain" action', () => { + it('calls the "bridge/setToChainId" action and the selectDestNetwork background action', () => { const state = store.getState().bridge; - const actionPayload = CHAIN_IDS.BSC; - store.dispatch(setToChain(actionPayload)); + const actionPayload = CHAIN_IDS.OPTIMISM; + + const mockSelectDestNetwork = jest.fn().mockReturnValue({}); + setBackgroundConnection({ + [BridgeUserAction.SELECT_DEST_NETWORK]: mockSelectDestNetwork, + } as never); + + store.dispatch(setToChain(actionPayload as never) as never); + + // Check redux state const actions = store.getActions(); - expect(actions[0].type).toBe('bridge/setToChain'); + expect(actions[0].type).toBe('bridge/setToChainId'); const newState = bridgeReducer(state, actions[0]); - expect(newState.toChain).toBe(actionPayload); + expect(newState.toChainId).toBe(actionPayload); + // Check background state + expect(mockSelectDestNetwork).toHaveBeenCalledTimes(1); + expect(mockSelectDestNetwork).toHaveBeenCalledWith( + '0xa', + expect.anything(), + ); }); }); @@ -83,4 +101,21 @@ describe('Ducks - Bridge', () => { expect(mockSetBridgeFeatureFlags).toHaveBeenCalledTimes(1); }); }); + + describe('setFromChain', () => { + it('calls the selectSrcNetwork background action', async () => { + const mockSelectSrcNetwork = jest.fn().mockReturnValue({}); + setBackgroundConnection({ + [BridgeUserAction.SELECT_SRC_NETWORK]: mockSelectSrcNetwork, + } as never); + + await store.dispatch(setFromChain(CHAIN_IDS.MAINNET) as never); + + expect(mockSelectSrcNetwork).toHaveBeenCalledTimes(1); + expect(mockSelectSrcNetwork).toHaveBeenCalledWith( + '0x1', + expect.anything(), + ); + }); + }); }); diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts index a35534381000..f2469d1025f3 100644 --- a/ui/ducks/bridge/bridge.ts +++ b/ui/ducks/bridge/bridge.ts @@ -1,19 +1,19 @@ import { createSlice } from '@reduxjs/toolkit'; +import { Hex } from '@metamask/utils'; import { swapsSlice } from '../swaps/swaps'; import { SwapsTokenObject } from '../../../shared/constants/swaps'; import { SwapsEthToken } from '../../selectors'; -import { MultichainProviderConfig } from '../../../shared/constants/multichain/networks'; export type BridgeState = { - toChain: MultichainProviderConfig | null; + toChainId: Hex | null; fromToken: SwapsTokenObject | SwapsEthToken | null; toToken: SwapsTokenObject | SwapsEthToken | null; fromTokenInputValue: string | null; }; const initialState: BridgeState = { - toChain: null, + toChainId: null, fromToken: null, toToken: null, fromTokenInputValue: null, @@ -24,8 +24,8 @@ const bridgeSlice = createSlice({ initialState: { ...initialState }, reducers: { ...swapsSlice.reducer, - setToChain: (state, action) => { - state.toChain = action.payload; + setToChainId: (state, action) => { + state.toChainId = action.payload; }, setFromToken: (state, action) => { state.fromToken = action.payload; diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts index 9a7c818fb20a..cf27790aa943 100644 --- a/ui/ducks/bridge/selectors.test.ts +++ b/ui/ducks/bridge/selectors.test.ts @@ -1,7 +1,10 @@ import { createBridgeMockStore } from '../../../test/jest/mock-store'; -import { CHAIN_IDS, FEATURED_RPCS } from '../../../shared/constants/network'; +import { + BUILT_IN_NETWORKS, + CHAIN_IDS, + FEATURED_RPCS, +} from '../../../shared/constants/network'; import { ALLOWED_BRIDGE_CHAIN_IDS } from '../../../shared/constants/bridge'; -import { getProviderConfig } from '../metamask/metamask'; import { mockNetworkState } from '../../../test/stub/networks'; import { getAllBridgeableNetworks, @@ -9,107 +12,111 @@ import { getFromChain, getFromChains, getFromToken, + getFromTokens, + getFromTopAssets, getIsBridgeTx, getToAmount, getToChain, getToChains, getToToken, + getToTokens, + getToTopAssets, } from './selectors'; describe('Bridge selectors', () => { describe('getFromChain', () => { it('returns the fromChain from the state', () => { - const state = { - metamask: { ...mockNetworkState({ chainId: '0x1' }) }, - }; + const state = createBridgeMockStore( + { srcNetworkAllowlist: [CHAIN_IDS.ARBITRUM] }, + { toChainId: '0xe708' }, + {}, + { ...mockNetworkState(FEATURED_RPCS[0]) }, + ); + const result = getFromChain(state as never); - expect(result).toStrictEqual(getProviderConfig(state)); + expect(result).toStrictEqual({ + blockExplorerUrls: ['https://localhost/blockExplorer/0xa4b1'], + chainId: '0xa4b1', + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + name: 'Arbitrum One', + nativeCurrency: 'ETH', + rpcEndpoints: [ + { + networkClientId: expect.anything(), + type: 'custom', + url: 'https://localhost/rpc/0xa4b1', + }, + ], + }); }); }); describe('getToChain', () => { it('returns the toChain from the state', () => { - const state = { - bridge: { - toChain: { chainId: '0x1' } as unknown, - }, - }; + const state = createBridgeMockStore( + { destNetworkAllowlist: ['0xe708'] }, + { toChainId: '0xe708' }, + ); const result = getToChain(state as never); - expect(result).toStrictEqual({ chainId: '0x1' }); + expect(result).toStrictEqual({ + blockExplorerUrls: ['https://localhost/blockExplorer/0xe708'], + chainId: '0xe708', + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + name: 'Linea Mainnet', + rpcEndpoints: [ + { + networkClientId: expect.anything(), + type: 'custom', + url: 'https://localhost/rpc/0xe708', + }, + ], + nativeCurrency: 'ETH', + }); }); }); describe('getAllBridgeableNetworks', () => { it('returns list of ALLOWED_BRIDGE_CHAIN_IDS networks', () => { - const state = createBridgeMockStore(); + const state = createBridgeMockStore( + {}, + {}, + {}, + mockNetworkState(...FEATURED_RPCS), + ); const result = getAllBridgeableNetworks(state as never); - expect(result).toHaveLength(9); + expect(result).toHaveLength(7); expect(result[0]).toStrictEqual( - expect.objectContaining({ chainId: CHAIN_IDS.MAINNET }), + expect.objectContaining({ chainId: FEATURED_RPCS[0].chainId }), ); expect(result[1]).toStrictEqual( - expect.objectContaining({ chainId: CHAIN_IDS.LINEA_MAINNET }), + expect.objectContaining({ chainId: FEATURED_RPCS[1].chainId }), ); - expect(result.slice(2)).toStrictEqual(FEATURED_RPCS); + FEATURED_RPCS.forEach((rpcDefinition, idx) => { + expect(result[idx]).toStrictEqual( + expect.objectContaining({ + ...rpcDefinition, + blockExplorerUrls: [ + `https://localhost/blockExplorer/${rpcDefinition.chainId}`, + ], + name: expect.anything(), + rpcEndpoints: [ + { + networkClientId: expect.anything(), + type: 'custom', + url: `https://localhost/rpc/${rpcDefinition.chainId}`, + }, + ], + }), + ); + }); result.forEach(({ chainId }) => { expect(ALLOWED_BRIDGE_CHAIN_IDS).toContain(chainId); }); - ALLOWED_BRIDGE_CHAIN_IDS.forEach((allowedChainId) => { - expect( - result.findIndex(({ chainId }) => chainId === allowedChainId), - ).toBeGreaterThan(-1); - }); - }); - - it('uses config from allNetworks if network is in both FEATURED_RPCS and allNetworks', () => { - const addedFeaturedNetwork = { - ...FEATURED_RPCS[FEATURED_RPCS.length - 1], - }; - - const state = { - ...createBridgeMockStore(), - metamask: { - networkConfigurations: [addedFeaturedNetwork], - ...mockNetworkState( - { chainId: CHAIN_IDS.MAINNET }, - { chainId: CHAIN_IDS.LINEA_MAINNET }, - { - ...FEATURED_RPCS[FEATURED_RPCS.length - 1], - id: 'testid', - blockExplorerUrl: 'https://basescan.org', - rpcUrl: 'https://mainnet.base.org', - }, - ), - }, - }; - const result = getAllBridgeableNetworks(state as never); - - expect(result).toHaveLength(9); - expect(result[0]).toStrictEqual( - expect.objectContaining({ chainId: CHAIN_IDS.MAINNET }), - ); - expect(result[1]).toStrictEqual( - expect.objectContaining({ chainId: CHAIN_IDS.LINEA_MAINNET }), - ); - expect(result[2]).toStrictEqual({ - blockExplorerUrls: addedFeaturedNetwork.blockExplorerUrls, - chainId: addedFeaturedNetwork.chainId, - defaultBlockExplorerUrlIndex: - addedFeaturedNetwork.defaultBlockExplorerUrlIndex, - defaultRpcEndpointIndex: addedFeaturedNetwork.defaultRpcEndpointIndex, - name: addedFeaturedNetwork.name, - nativeCurrency: addedFeaturedNetwork.nativeCurrency, - rpcEndpoints: [ - { - networkClientId: 'testid', - ...addedFeaturedNetwork.rpcEndpoints[0], - }, - ], - }); - expect(result.slice(3)).toStrictEqual(FEATURED_RPCS.slice(0, -1)); }); it('returns network if included in ALLOWED_BRIDGE_CHAIN_IDS', () => { @@ -119,45 +126,46 @@ describe('Bridge selectors', () => { ...mockNetworkState( { chainId: CHAIN_IDS.MAINNET }, { chainId: CHAIN_IDS.LINEA_MAINNET }, + { chainId: CHAIN_IDS.MOONBEAM }, ), }, }; const result = getAllBridgeableNetworks(state as never); - expect(result).toHaveLength(9); + expect(result).toHaveLength(2); expect(result[0]).toStrictEqual( expect.objectContaining({ chainId: CHAIN_IDS.MAINNET }), ); expect(result[1]).toStrictEqual( expect.objectContaining({ chainId: CHAIN_IDS.LINEA_MAINNET }), ); - expect(result.slice(2)).toStrictEqual(FEATURED_RPCS); + expect( + result.find(({ chainId }) => chainId === CHAIN_IDS.MOONBEAM), + ).toStrictEqual(undefined); }); }); describe('getFromChains', () => { - it('excludes selected toChain and disabled chains from options', () => { + it('excludes disabled chains from options', () => { const state = createBridgeMockStore( { srcNetworkAllowlist: [ CHAIN_IDS.MAINNET, + CHAIN_IDS.LINEA_MAINNET, CHAIN_IDS.OPTIMISM, CHAIN_IDS.POLYGON, ], }, - { toChain: { chainId: CHAIN_IDS.MAINNET } }, + { toChainId: CHAIN_IDS.LINEA_MAINNET }, ); const result = getFromChains(state as never); - expect(result).toHaveLength(3); + expect(result).toHaveLength(2); expect(result[0]).toStrictEqual( expect.objectContaining({ chainId: CHAIN_IDS.MAINNET }), ); expect(result[1]).toStrictEqual( - expect.objectContaining({ chainId: CHAIN_IDS.OPTIMISM }), - ); - expect(result[2]).toStrictEqual( - expect.objectContaining({ chainId: CHAIN_IDS.POLYGON }), + expect.objectContaining({ chainId: CHAIN_IDS.LINEA_MAINNET }), ); }); @@ -171,24 +179,32 @@ describe('Bridge selectors', () => { describe('getToChains', () => { it('excludes selected providerConfig and disabled chains from options', () => { - const state = createBridgeMockStore({ - destNetworkAllowlist: [ - CHAIN_IDS.MAINNET, - CHAIN_IDS.OPTIMISM, - CHAIN_IDS.POLYGON, - ], - }); + const state = createBridgeMockStore( + { + destNetworkAllowlist: [ + CHAIN_IDS.ARBITRUM, + CHAIN_IDS.LINEA_MAINNET, + CHAIN_IDS.OPTIMISM, + CHAIN_IDS.POLYGON, + ], + }, + {}, + {}, + mockNetworkState(...FEATURED_RPCS, { + chainId: CHAIN_IDS.LINEA_MAINNET, + }), + ); const result = getToChains(state as never); expect(result).toHaveLength(3); expect(result[0]).toStrictEqual( - expect.objectContaining({ chainId: CHAIN_IDS.MAINNET }), + expect.objectContaining({ chainId: CHAIN_IDS.OPTIMISM }), ); expect(result[1]).toStrictEqual( - expect.objectContaining({ chainId: CHAIN_IDS.OPTIMISM }), + expect.objectContaining({ chainId: CHAIN_IDS.POLYGON }), ); expect(result[2]).toStrictEqual( - expect.objectContaining({ chainId: CHAIN_IDS.POLYGON }), + expect.objectContaining({ chainId: CHAIN_IDS.LINEA_MAINNET }), ); }); @@ -202,44 +218,50 @@ describe('Bridge selectors', () => { describe('getIsBridgeTx', () => { it('returns false if bridge is not enabled', () => { - const state = { - metamask: { - ...mockNetworkState({ chainId: '0x1' }), - useExternalServices: true, - bridgeState: { bridgeFeatureFlags: { extensionSupport: false } }, + const state = createBridgeMockStore( + { + extensionSupport: false, + srcNetworkAllowlist: ['0x1'], + destNetworkAllowlist: ['0x38'], }, - bridge: { toChain: { chainId: '0x38' } as unknown }, - }; + { toChainId: '0x38' }, + {}, + { ...mockNetworkState({ chainId: '0x1' }), useExternalServices: true }, + ); const result = getIsBridgeTx(state as never); expect(result).toBe(false); }); - it('returns false if toChain is null', () => { - const state = { - metamask: { - ...mockNetworkState({ chainId: '0x1' }), - useExternalServices: true, - bridgeState: { bridgeFeatureFlags: { extensionSupport: true } }, + it('returns false if toChainId is null', () => { + const state = createBridgeMockStore( + { + extensionSupport: true, + srcNetworkAllowlist: ['0x1'], + destNetworkAllowlist: ['0x1'], }, - bridge: { toChain: null }, - }; + { toChainId: null }, + {}, + { ...mockNetworkState({ chainId: '0x1' }), useExternalServices: true }, + ); const result = getIsBridgeTx(state as never); expect(result).toBe(false); }); - it('returns false if fromChain and toChain have the same chainId', () => { - const state = { - metamask: { - ...mockNetworkState({ chainId: '0x1' }), - useExternalServices: true, - bridgeState: { bridgeFeatureFlags: { extensionSupport: true } }, + it('returns false if fromChain and toChainId have the same chainId', () => { + const state = createBridgeMockStore( + { + extensionSupport: true, + srcNetworkAllowlist: ['0x1'], + destNetworkAllowlist: ['0x1'], }, - bridge: { toChain: { chainId: '0x1' } }, - }; + { toChainId: '0x1' }, + {}, + { ...mockNetworkState({ chainId: '0x1' }), useExternalServices: true }, + ); const result = getIsBridgeTx(state as never); @@ -247,29 +269,39 @@ describe('Bridge selectors', () => { }); it('returns false if useExternalServices is not enabled', () => { - const state = { - metamask: { - ...mockNetworkState({ chainId: '0x1' }), - useExternalServices: false, - bridgeState: { bridgeFeatureFlags: { extensionSupport: true } }, + const state = createBridgeMockStore( + { + extensionSupport: true, + srcNetworkAllowlist: ['0x1'], + destNetworkAllowlist: ['0x38'], }, - bridge: { toChain: { chainId: '0x38' } }, - }; + { toChainId: '0x38' }, + {}, + { ...mockNetworkState({ chainId: '0x1' }), useExternalServices: false }, + ); const result = getIsBridgeTx(state as never); expect(result).toBe(false); }); - it('returns true if bridge is enabled and fromChain and toChain have different chainIds', () => { - const state = { - metamask: { - ...mockNetworkState({ chainId: '0x1' }), + it('returns true if bridge is enabled and fromChain and toChainId have different chainIds', () => { + const state = createBridgeMockStore( + { + extensionSupport: true, + srcNetworkAllowlist: ['0x1'], + destNetworkAllowlist: ['0x38'], + }, + { toChainId: '0x38' }, + {}, + { + ...mockNetworkState( + ...Object.values(BUILT_IN_NETWORKS), + ...FEATURED_RPCS, + ), useExternalServices: true, - bridgeState: { bridgeFeatureFlags: { extensionSupport: true } }, }, - bridge: { toChain: { chainId: '0x38' } }, - }; + ); const result = getIsBridgeTx(state as never); @@ -366,4 +398,97 @@ describe('Bridge selectors', () => { expect(result).toStrictEqual('0'); }); }); + + describe('getToTokens', () => { + it('returns dest tokens from controller state when toChainId is defined', () => { + const state = createBridgeMockStore( + {}, + { toChainId: '0x1' }, + { + destTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, + }, + ); + const result = getToTokens(state as never); + + expect(result).toStrictEqual({ + '0x00': { address: '0x00', symbol: 'TEST' }, + }); + }); + + it('returns empty dest tokens from controller state when toChainId is undefined', () => { + const state = createBridgeMockStore( + {}, + {}, + { + destTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, + }, + ); + const result = getToTokens(state as never); + + expect(result).toStrictEqual({}); + }); + }); + + describe('getToTopAssets', () => { + it('returns dest top assets from controller state when toChainId is defined', () => { + const state = createBridgeMockStore( + {}, + { toChainId: '0x1' }, + { + destTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, + destTopAssets: [{ address: '0x00', symbol: 'TEST' }], + }, + ); + const result = getToTopAssets(state as never); + + expect(result).toStrictEqual([{ address: '0x00', symbol: 'TEST' }]); + }); + + it('returns empty dest top assets from controller state when toChainId is undefined', () => { + const state = createBridgeMockStore( + {}, + {}, + { + destTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, + destTopAssets: [{ address: '0x00', symbol: 'TEST' }], + }, + ); + const result = getToTopAssets(state as never); + + expect(result).toStrictEqual([]); + }); + }); + + describe('getFromTokens', () => { + it('returns src tokens from controller state', () => { + const state = createBridgeMockStore( + {}, + { toChainId: '0x1' }, + { + srcTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, + }, + ); + const result = getFromTokens(state as never); + + expect(result).toStrictEqual({ + '0x00': { address: '0x00', symbol: 'TEST' }, + }); + }); + }); + + describe('getFromTopAssets', () => { + it('returns src top assets from controller state', () => { + const state = createBridgeMockStore( + {}, + { toChainId: '0x1' }, + { + srcTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, + srcTopAssets: [{ address: '0x00', symbol: 'TEST' }], + }, + ); + const result = getFromTopAssets(state as never); + + expect(result).toStrictEqual([{ address: '0x00', symbol: 'TEST' }]); + }); + }); }); diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index b688b2096e51..8cd56928fc66 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -1,4 +1,7 @@ -import { NetworkState } from '@metamask/network-controller'; +import { + NetworkConfiguration, + NetworkState, +} from '@metamask/network-controller'; import { uniqBy } from 'lodash'; import { getNetworkConfigurationsByChainId, @@ -13,7 +16,6 @@ import { // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths } from '../../../app/scripts/controllers/bridge/types'; -import { FEATURED_RPCS } from '../../../shared/constants/network'; import { createDeepEqualSelector } from '../../selectors/util'; import { getProviderConfig } from '../metamask/metamask'; import { SwapsTokenObject } from '../../../shared/constants/swaps'; @@ -26,14 +28,12 @@ type BridgeAppState = { bridge: BridgeState; }; -export const getFromChain = (state: BridgeAppState) => getProviderConfig(state); -export const getToChain = (state: BridgeAppState) => state.bridge.toChain; - +// only includes networks user has added export const getAllBridgeableNetworks = createDeepEqualSelector( getNetworkConfigurationsByChainId, (networkConfigurationsByChainId) => { return uniqBy( - [...Object.values(networkConfigurationsByChainId), ...FEATURED_RPCS], + Object.values(networkConfigurationsByChainId), 'chainId', ).filter(({ chainId }) => ALLOWED_BRIDGE_CHAIN_IDS.includes( @@ -42,6 +42,7 @@ export const getAllBridgeableNetworks = createDeepEqualSelector( ); }, ); + export const getFromChains = createDeepEqualSelector( getAllBridgeableNetworks, (state: BridgeAppState) => state.metamask.bridgeState?.bridgeFeatureFlags, @@ -52,17 +53,61 @@ export const getFromChains = createDeepEqualSelector( ), ), ); + +export const getFromChain = createDeepEqualSelector( + getNetworkConfigurationsByChainId, + getProviderConfig, + ( + networkConfigurationsByChainId, + providerConfig, + ): NetworkConfiguration | undefined => + providerConfig?.chainId + ? networkConfigurationsByChainId[providerConfig.chainId] + : undefined, +); + export const getToChains = createDeepEqualSelector( + getFromChain, getAllBridgeableNetworks, (state: BridgeAppState) => state.metamask.bridgeState?.bridgeFeatureFlags, - (allBridgeableNetworks, bridgeFeatureFlags) => - allBridgeableNetworks.filter(({ chainId }) => - bridgeFeatureFlags[BridgeFeatureFlagsKey.NETWORK_DEST_ALLOWLIST].includes( - chainId, - ), + ( + fromChain, + allBridgeableNetworks, + bridgeFeatureFlags, + ): NetworkConfiguration[] => + allBridgeableNetworks.filter( + ({ chainId }) => + fromChain?.chainId && + chainId !== fromChain.chainId && + bridgeFeatureFlags[ + BridgeFeatureFlagsKey.NETWORK_DEST_ALLOWLIST + ].includes(chainId), ), ); +export const getToChain = createDeepEqualSelector( + getToChains, + (state: BridgeAppState) => state.bridge.toChainId, + (toChains, toChainId): NetworkConfiguration | undefined => + toChains.find(({ chainId }) => chainId === toChainId), +); + +export const getFromTokens = (state: BridgeAppState) => { + return state.metamask.bridgeState.srcTokens ?? {}; +}; + +export const getFromTopAssets = (state: BridgeAppState) => { + return state.metamask.bridgeState.srcTopAssets ?? []; +}; + +export const getToTopAssets = (state: BridgeAppState) => { + return state.bridge.toChainId ? state.metamask.bridgeState.destTopAssets : []; +}; + +export const getToTokens = (state: BridgeAppState) => { + return state.bridge.toChainId ? state.metamask.bridgeState.destTokens : {}; +}; + export const getFromToken = ( state: BridgeAppState, ): SwapsTokenObject | SwapsEthToken => { @@ -88,8 +133,7 @@ export const getIsBridgeTx = createDeepEqualSelector( getToChain, (state: BridgeAppState) => getIsBridgeEnabled(state), (fromChain, toChain, isBridgeEnabled: boolean) => - isBridgeEnabled && - toChain !== null && - fromChain !== undefined && - fromChain.chainId !== toChain.chainId, + isBridgeEnabled && toChain && fromChain?.chainId + ? fromChain.chainId !== toChain.chainId + : false, ); diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js index 4765a87df61d..05cc6d46cb27 100644 --- a/ui/ducks/metamask/metamask.js +++ b/ui/ducks/metamask/metamask.js @@ -48,7 +48,6 @@ const initialState = { showFiatInTestnets: false, showTestNetworks: false, smartTransactionsOptInStatus: false, - useNativeCurrencyAsPrimaryCurrency: true, petnamesEnabled: true, featureNotificationsEnabled: false, showMultiRpcModal: false, diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js index e399dc663806..97daa88726d3 100644 --- a/ui/ducks/swaps/swaps.js +++ b/ui/ducks/swaps/swaps.js @@ -942,7 +942,7 @@ export const signAndSendSwapsSmartTransaction = ({ stx_enabled: smartTransactionsEnabled, current_stx_enabled: currentSmartTransactionsEnabled, stx_user_opt_in: smartTransactionsOptInStatus, - is_gas_included_trade: usedQuote.isGasIncludedTrade, + gas_included: usedQuote.isGasIncludedTrade, ...additionalTrackingParams, }; trackEvent({ diff --git a/ui/helpers/constants/settings.js b/ui/helpers/constants/settings.js index f4529ed1d6ad..89cca83f27cf 100644 --- a/ui/helpers/constants/settings.js +++ b/ui/helpers/constants/settings.js @@ -34,9 +34,9 @@ const SETTINGS_CONSTANTS = [ }, { tabMessage: (t) => t('general'), - sectionMessage: (t) => t('primaryCurrencySetting'), - descriptionMessage: (t) => t('primaryCurrencySettingDescription'), - route: `${GENERAL_ROUTE}#primary-currency`, + sectionMessage: (t) => t('showNativeTokenAsMainBalance'), + descriptionMessage: (t) => t('showNativeTokenAsMainBalance'), + route: `${GENERAL_ROUTE}#show-native-token-as-main-balance`, iconName: IconName.Setting, }, { @@ -324,6 +324,13 @@ const SETTINGS_CONSTANTS = [ route: `${SECURITY_ROUTE}#dataCollectionForMarketing`, icon: 'fa fa-lock', }, + { + tabMessage: (t) => t('securityAndPrivacy'), + sectionMessage: (t) => t('deleteMetaMetricsData'), + descriptionMessage: (t) => t('deleteMetaMetricsDataDescription'), + route: `${SECURITY_ROUTE}#delete-metametrics-data`, + icon: 'fa fa-lock', + }, { tabMessage: (t) => t('alerts'), sectionMessage: (t) => t('alertSettingsUnconnectedAccount'), diff --git a/ui/helpers/utils/settings-search.js b/ui/helpers/utils/settings-search.js index 447a39901059..07b4501c0208 100644 --- a/ui/helpers/utils/settings-search.js +++ b/ui/helpers/utils/settings-search.js @@ -25,6 +25,15 @@ function getFilteredSettingsRoutes(t, tabMessage) { }); } +export function getSpecificSettingsRoute(t, tabMessage, sectionMessage) { + return getSettingsRoutes().find((routeObject) => { + return ( + routeObject.tabMessage(t) === tabMessage && + routeObject.sectionMessage(t) === sectionMessage + ); + }); +} + /** * @param {Function} t - context.t function * @param {string} tabMessage @@ -59,7 +68,7 @@ export function handleSettingsRefs(t, tabMessage, settingsRefs) { } } -function colorText(menuElement, regex) { +export function colorText(menuElement, regex) { if (menuElement !== null) { let elemText = menuElement.innerHTML; elemText = elemText.replace('&', '&'); @@ -74,9 +83,20 @@ function colorText(menuElement, regex) { } } +/** + * Replaces any special characters in the input string that have a meaning in regular expressions + * (such as \, *, +, ?, etc.) with their escaped versions (e.g., \ becomes \\). + * + * @param input - The input string to be escaped for use in a regular expression. + * @returns The escaped string safe for use in a regular expression. + */ +export const escapeRegExp = (input) => { + return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Escapes special characters +}; + export function highlightSearchedText() { const searchElem = document.getElementById('search-settings'); - const searchRegex = new RegExp(searchElem.value, 'gi'); + const searchRegex = new RegExp(escapeRegExp(searchElem.value), 'gi'); const results = document.querySelectorAll( '.settings-page__header__search__list__item', ); diff --git a/ui/helpers/utils/settings-search.test.js b/ui/helpers/utils/settings-search.test.js index 709b0354a4e7..c3d07073a7d3 100644 --- a/ui/helpers/utils/settings-search.test.js +++ b/ui/helpers/utils/settings-search.test.js @@ -4,6 +4,10 @@ import { getSettingsRoutes, getNumberOfSettingRoutesInTab, handleSettingsRefs, + getSpecificSettingsRoute, + escapeRegExp, + colorText, + highlightSearchedText, } from './settings-search'; const t = (key) => { @@ -170,7 +174,7 @@ describe('Settings Search Utils', () => { it('returns "Security & privacy" section count', () => { expect( getNumberOfSettingRoutesInTab(t, t('securityAndPrivacy')), - ).toStrictEqual(20); + ).toStrictEqual(21); }); it('returns "Alerts" section count', () => { @@ -198,6 +202,42 @@ describe('Settings Search Utils', () => { }); }); + describe('escapeRegExp', () => { + it('should escape special characters for use in regular expressions', () => { + const input = '.*+?^${}()|[]\\'; + const expectedOutput = '\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\'; + expect(escapeRegExp(input)).toBe(expectedOutput); + }); + + it('should return the same string if no special characters are present', () => { + const input = 'hello'; + expect(escapeRegExp(input)).toBe(input); + }); + + it('should escape only the special characters in a mixed string', () => { + const input = 'hello.*world'; + const expectedOutput = 'hello\\.\\*world'; + expect(escapeRegExp(input)).toBe(expectedOutput); + }); + + it('should handle an empty string correctly', () => { + const input = ''; + expect(escapeRegExp(input)).toBe(''); + }); + + it('should escape backslashes properly', () => { + const input = '\\'; + const expectedOutput = '\\\\'; + expect(escapeRegExp(input)).toBe(expectedOutput); + }); + + it('should escape backslashes with content properly', () => { + const input = 'foobar\\'; + const expectedOutput = 'foobar\\\\'; + expect(escapeRegExp(input)).toBe(expectedOutput); + }); + }); + // Can't be tested without DOM element describe('handleSettingsRefs', () => { it('should handle general refs', () => { @@ -209,4 +249,167 @@ describe('Settings Search Utils', () => { expect(handleSettingsRefs(t, t('general'), settingsRefs)).toBeUndefined(); }); }); + + describe('getSpecificSettingsRoute', () => { + it('should return show native token as main balance route', () => { + const result = getSpecificSettingsRoute( + t, + t('general'), + t('showNativeTokenAsMainBalance'), + ); + expect(result.route).toBe( + '/settings/general#show-native-token-as-main-balance', + ); + }); + }); + + describe('colorText', () => { + let mockElement; + + beforeEach(() => { + mockElement = { + innerHTML: 'Test & string with multiple words', + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should highlight text that matches the regex', () => { + const regex = /string/giu; + colorText(mockElement, regex); + expect(mockElement.innerHTML).toBe( + 'Test & string with multiple words', + ); + }); + + it('should correctly decode & to &', () => { + const regex = /&/giu; + colorText(mockElement, regex); + expect(mockElement.innerHTML).toBe( + 'Test & string with multiple words', + ); + }); + + it('should remove any existing highlight spans before applying new highlights', () => { + mockElement.innerHTML = + 'Test & string with multiple words'; + const regex = /multiple/giu; + colorText(mockElement, regex); + expect(mockElement.innerHTML).toBe( + 'Test & string with multiple words', + ); + }); + + it('should not modify innerHTML if menuElement is null', () => { + const regex = /string/giu; + const nullElement = null; + colorText(nullElement, regex); + expect(nullElement).toBeNull(); + }); + + it('should not highlight anything if regex doesn’t match', () => { + const regex = /nomatch/giu; + colorText(mockElement, regex); + expect(mockElement.innerHTML).toBe('Test & string with multiple words'); + }); + }); + + describe('highlightSearchedText', () => { + let searchElem; + let mockResultItems; + let mockMenuTabElement; + let mockMenuSectionElement; + + beforeEach(() => { + searchElem = document.createElement('input'); + searchElem.id = 'search-settings'; + searchElem.value = 'test'; + document.body.appendChild(searchElem); + + // Mock result list items + mockResultItems = [...Array(2)].map(() => { + const item = document.createElement('div'); + item.classList.add('settings-page__header__search__list__item'); + + mockMenuTabElement = document.createElement('div'); + mockMenuTabElement.classList.add( + 'settings-page__header__search__list__item__tab', + ); + mockMenuTabElement.innerHTML = 'Test tab'; + + mockMenuSectionElement = document.createElement('div'); + mockMenuSectionElement.classList.add( + 'settings-page__header__search__list__item__section', + ); + mockMenuSectionElement.innerHTML = 'Test section'; + + item.appendChild(mockMenuTabElement); + item.appendChild(mockMenuSectionElement); + + return item; + }); + + mockResultItems.forEach((item) => document.body.appendChild(item)); + }); + + afterEach(() => { + document.body.innerHTML = ''; + }); + + it('should highlight the matching text in both menuTabElement and menuSectionElement', () => { + highlightSearchedText(); + mockResultItems.forEach((item) => { + const tabElement = item.querySelector( + '.settings-page__header__search__list__item__tab', + ); + const sectionElement = item.querySelector( + '.settings-page__header__search__list__item__section', + ); + expect(tabElement.innerHTML).toBe( + 'Test tab', + ); + expect(sectionElement.innerHTML).toBe( + 'Test section', + ); + }); + }); + + it('should not alter the innerHTML if no match is found', () => { + searchElem.value = 'nomatch'; + highlightSearchedText(); + mockResultItems.forEach((item) => { + const tabElement = item.querySelector( + '.settings-page__header__search__list__item__tab', + ); + const sectionElement = item.querySelector( + '.settings-page__header__search__list__item__section', + ); + + expect(tabElement.innerHTML).toBe('Test tab'); + expect(sectionElement.innerHTML).toBe('Test section'); + }); + }); + + it('should do nothing if the search input is empty', () => { + searchElem.value = ''; + highlightSearchedText(); + mockResultItems.forEach((item) => { + const tabElement = item.querySelector( + '.settings-page__header__search__list__item__tab', + ); + const sectionElement = item.querySelector( + '.settings-page__header__search__list__item__section', + ); + + expect(tabElement.innerHTML).toBe( + 'Test tab', + ); + expect(sectionElement.innerHTML).toBe( + 'Test section', + ); + }); + }); + }); }); diff --git a/ui/helpers/utils/util.js b/ui/helpers/utils/util.js index 01cffeea3cdc..fe01bd15f5b1 100644 --- a/ui/helpers/utils/util.js +++ b/ui/helpers/utils/util.js @@ -823,3 +823,18 @@ export const getFilteredSnapPermissions = ( return filteredPermissions; }; +/** + * Helper function to calculate the token amount 1dAgo using price percentage a day ago. + * + * @param {*} tokenFiatBalance - current token fiat balance + * @param {*} tokenPricePercentChange1dAgo - price percentage 1day ago + * @returns token amount 1day ago + */ +export const getCalculatedTokenAmount1dAgo = ( + tokenFiatBalance, + tokenPricePercentChange1dAgo, +) => { + return tokenPricePercentChange1dAgo !== undefined && tokenFiatBalance + ? tokenFiatBalance / (1 + tokenPricePercentChange1dAgo / 100) + : tokenFiatBalance ?? 0; +}; diff --git a/ui/helpers/utils/util.test.js b/ui/helpers/utils/util.test.js index e10c70630dba..dd2282efa531 100644 --- a/ui/helpers/utils/util.test.js +++ b/ui/helpers/utils/util.test.js @@ -1226,4 +1226,37 @@ describe('util', () => { ]); }); }); + + describe('getCalculatedTokenAmount1dAgo', () => { + it('should return successfully balance of token 1dago', () => { + const mockTokenFiatAmount = '10'; + const mockTokenPercent1dAgo = 1; + const expectedRes = 9.900990099009901; + const result = util.getCalculatedTokenAmount1dAgo( + mockTokenFiatAmount, + mockTokenPercent1dAgo, + ); + expect(result).toBe(expectedRes); + }); + + it('should return token balance if percentage is undefined', () => { + const mockTokenFiatAmount = '10'; + const mockTokenPercent1dAgo = undefined; + const result = util.getCalculatedTokenAmount1dAgo( + mockTokenFiatAmount, + mockTokenPercent1dAgo, + ); + expect(result).toBe(mockTokenFiatAmount); + }); + + it('should return zero if token amount is undefined', () => { + const mockTokenFiatAmount = undefined; + const mockTokenPercent1dAgo = 1; + const result = util.getCalculatedTokenAmount1dAgo( + mockTokenFiatAmount, + mockTokenPercent1dAgo, + ); + expect(result).toBe(0); + }); + }); }); diff --git a/ui/hooks/bridge/useBridging.test.ts b/ui/hooks/bridge/useBridging.test.ts index 9e2f205c28dc..df8bbb940f4e 100644 --- a/ui/hooks/bridge/useBridging.test.ts +++ b/ui/hooks/bridge/useBridging.test.ts @@ -15,9 +15,16 @@ jest.mock('react-router-dom', () => ({ }), })); +const mockDispatch = jest.fn().mockReturnValue(() => jest.fn()); jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), - useDispatch: jest.fn().mockReturnValue(() => jest.fn()), + useDispatch: () => mockDispatch, +})); + +const mockSetFromChain = jest.fn(); +jest.mock('../../ducks/bridge/actions', () => ({ + ...jest.requireActual('../../ducks/bridge/actions'), + setFromChain: () => mockSetFromChain(), })); const MOCK_METAMETRICS_ID = '0xtestMetaMetricsId'; @@ -94,6 +101,8 @@ describe('useBridging', () => { }, }); + expect(mockDispatch.mock.calls).toHaveLength(1); + expect(nock(BRIDGE_API_BASE_URL).isDone()).toBe(true); result.current.openBridgeExperience(location, token, urlSuffix); @@ -165,6 +174,7 @@ describe('useBridging', () => { result.current.openBridgeExperience(location, token, urlSuffix); + expect(mockDispatch.mock.calls).toHaveLength(3); expect(mockHistoryPush.mock.calls).toHaveLength(1); expect(mockHistoryPush).toHaveBeenCalledWith(expectedUrl); expect(openTabSpy).not.toHaveBeenCalled(); diff --git a/ui/hooks/bridge/useBridging.ts b/ui/hooks/bridge/useBridging.ts index d11aaeb821a9..ce4b8c48b89c 100644 --- a/ui/hooks/bridge/useBridging.ts +++ b/ui/hooks/bridge/useBridging.ts @@ -1,9 +1,11 @@ import { useCallback, useContext, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { setBridgeFeatureFlags } from '../../ducks/bridge/actions'; import { - getCurrentChainId, + setBridgeFeatureFlags, + setFromChain, +} from '../../ducks/bridge/actions'; +import { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) getCurrentKeyring, getDataCollectionForMarketing, @@ -31,6 +33,7 @@ import { isHardwareKeyring } from '../../helpers/utils/hardware'; import { getPortfolioUrl } from '../../helpers/utils/portfolio'; import { setSwapsFromToken } from '../../ducks/swaps/swaps'; import { SwapsTokenObject } from '../../../shared/constants/swaps'; +import { getProviderConfig } from '../../ducks/metamask/metamask'; ///: END:ONLY_INCLUDE_IF const useBridging = () => { @@ -41,7 +44,7 @@ const useBridging = () => { const metaMetricsId = useSelector(getMetaMetricsId); const isMetaMetricsEnabled = useSelector(getParticipateInMetaMetrics); const isMarketingEnabled = useSelector(getDataCollectionForMarketing); - const chainId = useSelector(getCurrentChainId); + const providerConfig = useSelector(getProviderConfig); const keyring = useSelector(getCurrentKeyring); const usingHardwareWallet = isHardwareKeyring(keyring.type); @@ -52,13 +55,20 @@ const useBridging = () => { dispatch(setBridgeFeatureFlags()); }, [dispatch, setBridgeFeatureFlags]); + useEffect(() => { + isBridgeChain && + isBridgeSupported && + providerConfig && + dispatch(setFromChain(providerConfig.chainId)); + }, []); + const openBridgeExperience = useCallback( ( location: string, token: SwapsTokenObject | SwapsEthToken, portfolioUrlSuffix?: string, ) => { - if (!isBridgeChain) { + if (!isBridgeChain || !providerConfig) { return; } @@ -70,7 +80,7 @@ const useBridging = () => { token_symbol: token.symbol, location, text: 'Bridge', - chain_id: chainId, + chain_id: providerConfig.chainId, }, }); dispatch( @@ -105,7 +115,7 @@ const useBridging = () => { location, text: 'Bridge', url: portfolioUrl, - chain_id: chainId, + chain_id: providerConfig.chainId, token_symbol: token.symbol, }, }); @@ -114,7 +124,6 @@ const useBridging = () => { [ isBridgeSupported, isBridgeChain, - chainId, setSwapsFromToken, dispatch, usingHardwareWallet, @@ -123,6 +132,7 @@ const useBridging = () => { trackEvent, isMetaMetricsEnabled, isMarketingEnabled, + providerConfig, ], ); diff --git a/ui/hooks/useCurrencyDisplay.js b/ui/hooks/useCurrencyDisplay.js index a7798d5ff10e..12b2cfc06ec3 100644 --- a/ui/hooks/useCurrencyDisplay.js +++ b/ui/hooks/useCurrencyDisplay.js @@ -135,6 +135,7 @@ export function useCurrencyDisplay( numberOfDecimals, denomination, currency, + isAggregatedFiatOverviewBalance, ...opts }, ) { @@ -151,6 +152,7 @@ export function useCurrencyDisplay( getMultichainConversionRate, account, ); + const isUserPreferredCurrency = currency === currentCurrency; const isNativeCurrency = currency === nativeCurrency; @@ -172,6 +174,10 @@ export function useCurrencyDisplay( }); } + if (isAggregatedFiatOverviewBalance) { + return formatCurrency(inputValue, currency); + } + return formatEthCurrencyDisplay({ isNativeCurrency, isUserPreferredCurrency, @@ -194,6 +200,7 @@ export function useCurrencyDisplay( denomination, numberOfDecimals, currentCurrency, + isAggregatedFiatOverviewBalance, ]); let suffix; diff --git a/ui/hooks/useTransactionDisplayData.test.js b/ui/hooks/useTransactionDisplayData.test.js index 0aa77675bd7c..68a4d829bcf8 100644 --- a/ui/hooks/useTransactionDisplayData.test.js +++ b/ui/hooks/useTransactionDisplayData.test.js @@ -211,7 +211,6 @@ const renderHookWithRouter = (cb, tokenAddress) => { currentCurrency: 'ETH', useCurrencyRateCheck: false, // to force getShouldShowFiat to return false preferences: { - useNativeCurrencyAsPrimaryCurrency: true, getShowFiatInTestnets: false, }, allNfts: [], diff --git a/ui/hooks/useUserPreferencedCurrency.js b/ui/hooks/useUserPreferencedCurrency.js index 732d4ec726bb..e9e8133cf9e2 100644 --- a/ui/hooks/useUserPreferencedCurrency.js +++ b/ui/hooks/useUserPreferencedCurrency.js @@ -6,7 +6,7 @@ import { getMultichainShouldShowFiat, } from '../selectors/multichain'; -import { PRIMARY, SECONDARY } from '../helpers/constants/common'; +import { PRIMARY } from '../helpers/constants/common'; import { EtherDenomination } from '../../shared/constants/common'; import { ETH_DEFAULT_DECIMALS } from '../constants'; import { useMultichainSelector } from './useMultichainSelector'; @@ -20,6 +20,8 @@ import { useMultichainSelector } from './useMultichainSelector'; * when using ETH * @property {number} [fiatNumberOfDecimals] - Number of significant decimals to display * when using fiat + * @property {boolean} [shouldCheckShowNativeToken] - Boolean to know if checking the setting + * show native token as main balance is needed */ /** @@ -34,9 +36,10 @@ import { useMultichainSelector } from './useMultichainSelector'; * useUserPreferencedCurrency * * returns an object that contains what currency to use for displaying values based - * on the user's preference settings, as well as the significant number of decimals + * on whether the user needs to check showNativeTokenAsMainBalance setting, as well as the significant number of decimals * to display based on the currency * + * * @param {"PRIMARY" | "SECONDARY"} type - what display type is being rendered * @param {UseUserPreferencedCurrencyOptions} opts - options to override default values * @returns {UserPreferredCurrency} @@ -49,7 +52,7 @@ export function useUserPreferencedCurrency(type, opts = {}) { account, ); - const { useNativeCurrencyAsPrimaryCurrency } = useSelector( + const { showNativeTokenAsMainBalance } = useSelector( getPreferences, shallowEqual, ); @@ -74,12 +77,13 @@ export function useUserPreferencedCurrency(type, opts = {}) { return nativeReturn; } else if (opts.showFiatOverride) { return fiatReturn; + } else if (!showFiat) { + return nativeReturn; } else if ( - !showFiat || - (type === PRIMARY && useNativeCurrencyAsPrimaryCurrency) || - (type === SECONDARY && !useNativeCurrencyAsPrimaryCurrency) + (opts.shouldCheckShowNativeToken && showNativeTokenAsMainBalance) || + !opts.shouldCheckShowNativeToken ) { - return nativeReturn; + return type === PRIMARY ? nativeReturn : fiatReturn; } - return fiatReturn; + return type === PRIMARY ? fiatReturn : nativeReturn; } diff --git a/ui/hooks/useUserPreferencedCurrency.test.js b/ui/hooks/useUserPreferencedCurrency.test.js index 12785d44b5ff..9421834b0e31 100644 --- a/ui/hooks/useUserPreferencedCurrency.test.js +++ b/ui/hooks/useUserPreferencedCurrency.test.js @@ -8,16 +8,43 @@ import { mockNetworkState } from '../../test/stub/networks'; import { CHAIN_IDS } from '../../shared/constants/network'; import { useUserPreferencedCurrency } from './useUserPreferencedCurrency'; +const renderUseUserPreferencedCurrency = (state, value, restProps) => { + const defaultState = { + ...mockState, + metamask: { + ...mockState.metamask, + completedOnboarding: true, + ...mockNetworkState({ + chainId: state.showFiat ? CHAIN_IDS.MAINNET : CHAIN_IDS.SEPOLIA, + ticker: state?.nativeCurrency, + }), + currentCurrency: state.currentCurrency, + currencyRates: { ETH: { conversionRate: 280.45 } }, + preferences: { + showFiatInTestnets: state.showFiat, + showNativeTokenAsMainBalance: state.showNativeTokenAsMainBalance, + }, + }, + }; + + const wrapper = ({ children }) => ( + {children} + ); + + return renderHook(() => useUserPreferencedCurrency(value, restProps), { + wrapper, + }); +}; const tests = [ { state: { - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, nativeCurrency: 'ETH', showFiat: true, currentCurrency: 'usd', }, params: { - type: 'PRIMARY', + showNativeOverride: true, }, result: { currency: 'ETH', @@ -26,13 +53,13 @@ const tests = [ }, { state: { - useNativeCurrencyAsPrimaryCurrency: false, + showNativeTokenAsMainBalance: true, nativeCurrency: 'ETH', showFiat: true, currentCurrency: 'usd', }, params: { - type: 'PRIMARY', + showFiatOverride: true, }, result: { currency: 'usd', @@ -41,45 +68,59 @@ const tests = [ }, { state: { - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, nativeCurrency: 'ETH', showFiat: true, + currentCurrency: 'usd', }, params: { - type: 'SECONDARY', - fiatNumberOfDecimals: 4, - fiatPrefix: '-', + type: 'PRIMARY', + shouldCheckShowNativeToken: true, }, result: { - currency: undefined, - numberOfDecimals: 4, + currency: 'ETH', + numberOfDecimals: 8, }, }, { state: { - useNativeCurrencyAsPrimaryCurrency: false, + showNativeTokenAsMainBalance: false, nativeCurrency: 'ETH', showFiat: true, + currentCurrency: 'usd', }, params: { - type: 'SECONDARY', - fiatNumberOfDecimals: 4, - numberOfDecimals: 3, - fiatPrefix: 'a', + type: 'PRIMARY', }, result: { currency: 'ETH', - numberOfDecimals: 3, + numberOfDecimals: 8, + }, + }, + { + state: { + showNativeTokenAsMainBalance: false, + nativeCurrency: 'ETH', + showFiat: true, + currentCurrency: 'usd', + }, + params: { + type: 'SECONDARY', + }, + result: { + currency: 'usd', + numberOfDecimals: 2, }, }, { state: { - useNativeCurrencyAsPrimaryCurrency: false, + showNativeTokenAsMainBalance: false, nativeCurrency: 'ETH', showFiat: false, + currentCurrency: 'usd', }, params: { - type: 'PRIMARY', + type: 'SECONDARY', }, result: { currency: 'ETH', @@ -88,66 +129,54 @@ const tests = [ }, { state: { - useNativeCurrencyAsPrimaryCurrency: false, + showNativeTokenAsMainBalance: true, nativeCurrency: 'ETH', showFiat: true, + currentCurrency: 'usd', }, params: { type: 'PRIMARY', }, result: { - currency: undefined, + currency: 'ETH', + numberOfDecimals: 8, + }, + }, + { + state: { + showNativeTokenAsMainBalance: true, + nativeCurrency: 'ETH', + showFiat: true, + currentCurrency: 'usd', + }, + params: { + type: 'SECONDARY', + }, + result: { + currency: 'usd', numberOfDecimals: 2, }, }, { state: { - useNativeCurrencyAsPrimaryCurrency: false, + showNativeTokenAsMainBalance: true, nativeCurrency: 'ETH', showFiat: true, + currentCurrency: 'usd', }, params: { - type: 'PRIMARY', + type: 'SECONDARY', + shouldCheckShowNativeToken: true, }, result: { - currency: undefined, + currency: 'usd', numberOfDecimals: 2, }, }, ]; - -const renderUseUserPreferencedCurrency = (state, value, restProps) => { - const defaultState = { - ...mockState, - metamask: { - ...mockState.metamask, - completedOnboarding: true, - ...mockNetworkState({ - chainId: state.showFiat ? CHAIN_IDS.MAINNET : CHAIN_IDS.SEPOLIA, - ticker: state?.nativeCurrency, - }), - currentCurrency: state.currentCurrency, - currencyRates: { ETH: { conversionRate: 280.45 } }, - preferences: { - useNativeCurrencyAsPrimaryCurrency: - state.useNativeCurrencyAsPrimaryCurrency, - showFiatInTestnets: state.showFiat, - }, - }, - }; - - const wrapper = ({ children }) => ( - {children} - ); - - return renderHook(() => useUserPreferencedCurrency(value, restProps), { - wrapper, - }); -}; - describe('useUserPreferencedCurrency', () => { tests.forEach(({ params: { type, ...otherParams }, state, result }) => { - describe(`when showFiat is ${state.showFiat}, useNativeCurrencyAsPrimary is ${state.useNativeCurrencyAsPrimaryCurrency} and type is ${type}`, () => { + describe(`when showFiat is ${state.showFiat}, shouldCheckShowNativeToken is ${otherParams.shouldCheckShowNativeToken}, showNativeTokenAsMainBalance is ${state.showNativeTokenAsMainBalance} and type is ${type}`, () => { const { result: hookResult } = renderUseUserPreferencedCurrency( state, type, diff --git a/ui/index.js b/ui/index.js index fec0321164dd..5cb576e488d6 100644 --- a/ui/index.js +++ b/ui/index.js @@ -290,9 +290,6 @@ function setupStateHooks(store) { // for more info) state.version = global.platform.getVersion(); state.browser = window.navigator.userAgent; - state.completeTxList = await actions.getTransactions({ - filterToCurrentNetwork: false, - }); return state; }; window.stateHooks.getSentryAppState = function () { diff --git a/ui/pages/asset/asset.tsx b/ui/pages/asset/asset.tsx index 975ff2e5cc25..933c39872265 100644 --- a/ui/pages/asset/asset.tsx +++ b/ui/pages/asset/asset.tsx @@ -21,11 +21,13 @@ const Asset = () => { const { asset, id } = useParams<{ asset: string; id: string }>(); const token = tokens.find(({ address }: { address: string }) => + // @ts-expect-error TODO: Fix this type error by handling undefined parameters isEqualCaseInsensitive(address, asset), ); const nft = nfts.find( ({ address, tokenId }: { address: string; tokenId: string }) => + // @ts-expect-error TODO: Fix this type error by handling undefined parameters isEqualCaseInsensitive(address, asset) && id === tokenId.toString(), ); diff --git a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap index ef6e6331d2ba..95828e3e250e 100644 --- a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap +++ b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap @@ -43,7 +43,7 @@ exports[`AssetPage should render a native asset 1`] = ` data-theme="light" >
@@ -61,7 +61,7 @@ exports[`AssetPage should render a native asset 1`] = ` data-theme="light" >
@@ -79,7 +79,7 @@ exports[`AssetPage should render a native asset 1`] = ` data-theme="light" >
@@ -99,7 +99,7 @@ exports[`AssetPage should render a native asset 1`] = ` data-theme="light" >
@@ -118,7 +118,7 @@ exports[`AssetPage should render a native asset 1`] = ` data-theme="light" >
@@ -137,7 +137,7 @@ exports[`AssetPage should render a native asset 1`] = ` data-theme="light" >
@@ -156,7 +156,7 @@ exports[`AssetPage should render a native asset 1`] = ` data-theme="light" >
@@ -236,7 +236,7 @@ exports[`AssetPage should render a native asset 1`] = ` data-testid="multichain-token-list-item-secondary-value" />

0 TEST @@ -352,7 +352,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = ` data-theme="light" >

@@ -371,7 +371,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = ` data-theme="light" >
@@ -390,7 +390,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = ` data-theme="light" >
@@ -409,7 +409,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = ` data-theme="light" >
@@ -427,7 +427,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = ` data-theme="light" >
@@ -446,7 +446,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = ` data-theme="light" >
@@ -515,7 +515,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = ` class="mm-box mm-box--display-flex" >

@@ -528,7 +528,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = ` data-testid="multichain-token-list-item-secondary-value" />

0 @@ -835,7 +835,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` data-theme="light" >

@@ -854,7 +854,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` data-theme="light" >
@@ -873,7 +873,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` data-theme="light" >
@@ -892,7 +892,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` data-theme="light" >
@@ -910,7 +910,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` data-theme="light" >
@@ -929,7 +929,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` data-theme="light" >
@@ -998,7 +998,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` class="mm-box mm-box--display-flex" >

@@ -1011,7 +1011,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` data-testid="multichain-token-list-item-secondary-value" />

0 diff --git a/ui/pages/asset/components/asset-page.test.tsx b/ui/pages/asset/components/asset-page.test.tsx index bf616d0aaac9..35721a30a1c2 100644 --- a/ui/pages/asset/components/asset-page.test.tsx +++ b/ui/pages/asset/components/asset-page.test.tsx @@ -49,9 +49,7 @@ describe('AssetPage', () => { }, }, useCurrencyRateCheck: true, - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, internalAccounts: { accounts: { 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { diff --git a/ui/pages/asset/components/token-buttons.tsx b/ui/pages/asset/components/token-buttons.tsx index a07cdaca2d48..bb3f129bade8 100644 --- a/ui/pages/asset/components/token-buttons.tsx +++ b/ui/pages/asset/components/token-buttons.tsx @@ -48,7 +48,12 @@ import { JustifyContent, } from '../../../helpers/constants/design-system'; import IconButton from '../../../components/ui/icon-button/icon-button'; -import { Box, Icon, IconName } from '../../../components/component-library'; +import { + Box, + Icon, + IconName, + IconSize, +} from '../../../components/component-library'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; ///: END:ONLY_INCLUDE_IF @@ -115,7 +120,11 @@ const TokenButtons = ({ + } label={t('buyAndSell')} data-testid="token-overview-buy" @@ -144,7 +153,11 @@ const TokenButtons = ({ + } label={t('stake')} data-testid="token-overview-mmi-stake" @@ -163,6 +176,7 @@ const TokenButtons = ({ } label={t('portfolio')} @@ -215,6 +229,7 @@ const TokenButtons = ({ } label={t('send')} @@ -229,6 +244,7 @@ const TokenButtons = ({ } onClick={() => { @@ -281,7 +297,11 @@ const TokenButtons = ({ className="token-overview__button" data-testid="token-overview-bridge" Icon={ - + } label={t('bridge')} onClick={() => { diff --git a/ui/pages/bridge/bridge.util.test.ts b/ui/pages/bridge/bridge.util.test.ts index da2637b1f5f4..07c35ae57749 100644 --- a/ui/pages/bridge/bridge.util.test.ts +++ b/ui/pages/bridge/bridge.util.test.ts @@ -1,6 +1,6 @@ import fetchWithCache from '../../../shared/lib/fetch-with-cache'; import { CHAIN_IDS } from '../../../shared/constants/network'; -import { fetchBridgeFeatureFlags } from './bridge.util'; +import { fetchBridgeFeatureFlags, fetchBridgeTokens } from './bridge.util'; jest.mock('../../../shared/lib/fetch-with-cache'); @@ -79,4 +79,66 @@ describe('Bridge utils', () => { await expect(fetchBridgeFeatureFlags()).rejects.toThrowError(mockError); }); }); + + describe('fetchBridgeTokens', () => { + it('should fetch bridge tokens successfully', async () => { + const mockResponse = [ + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + decimals: 16, + }, + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f985', + decimals: 16, + }, + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f986', + symbol: 'DEF', + }, + { + address: '0x124', + symbol: 'JKL', + decimals: 16, + }, + ]; + + (fetchWithCache as jest.Mock).mockResolvedValue(mockResponse); + + const result = await fetchBridgeTokens('0xa'); + + expect(fetchWithCache).toHaveBeenCalledWith({ + url: 'https://bridge.api.cx.metamask.io/getTokens?chainId=10', + fetchOptions: { + method: 'GET', + headers: { 'X-Client-Id': 'extension' }, + }, + cacheOptions: { cacheRefreshTime: 600000 }, + functionName: 'fetchBridgeTokens', + }); + + expect(result).toStrictEqual({ + '0x0000000000000000000000000000000000000000': { + address: '0x0000000000000000000000000000000000000000', + decimals: 18, + iconUrl: './images/eth_logo.svg', + name: 'Ether', + symbol: 'ETH', + }, + '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + decimals: 16, + symbol: 'ABC', + }, + }); + }); + + it('should handle fetch error', async () => { + const mockError = new Error('Failed to fetch'); + + (fetchWithCache as jest.Mock).mockRejectedValue(mockError); + + await expect(fetchBridgeTokens('0xa')).rejects.toThrowError(mockError); + }); + }); }); diff --git a/ui/pages/bridge/bridge.util.ts b/ui/pages/bridge/bridge.util.ts index 0f72b75a0787..915a933e7c02 100644 --- a/ui/pages/bridge/bridge.util.ts +++ b/ui/pages/bridge/bridge.util.ts @@ -1,4 +1,4 @@ -import { add0x } from '@metamask/utils'; +import { Hex, add0x } from '@metamask/utils'; import { BridgeFeatureFlagsKey, BridgeFeatureFlags, @@ -12,7 +12,19 @@ import { import { MINUTE } from '../../../shared/constants/time'; import fetchWithCache from '../../../shared/lib/fetch-with-cache'; import { validateData } from '../../../shared/lib/swaps-utils'; -import { decimalToHex } from '../../../shared/modules/conversion.utils'; +import { + decimalToHex, + hexToDecimal, +} from '../../../shared/modules/conversion.utils'; +import { + SWAPS_CHAINID_DEFAULT_TOKEN_MAP, + SwapsTokenObject, +} from '../../../shared/constants/swaps'; +import { TOKEN_VALIDATORS } from '../swaps/swaps.util'; +import { + isSwapsDefaultTokenAddress, + isSwapsDefaultTokenSymbol, +} from '../../../shared/modules/swaps.utils'; const CLIENT_ID_HEADER = { 'X-Client-Id': BRIDGE_CLIENT_ID }; const CACHE_REFRESH_TEN_MINUTES = 10 * MINUTE; @@ -31,17 +43,17 @@ export type FeatureFlagResponse = { }; // End of copied types -type Validator = { - property: keyof T; +type Validator = { + property: keyof ExpectedResponse | string; type: string; - validator: (value: unknown) => boolean; + validator: (value: DataToValidate) => boolean; }; -const validateResponse = ( - validators: Validator[], +const validateResponse = ( + validators: Validator[], data: unknown, urlUsed: string, -): data is T => { +): data is ExpectedResponse => { return validateData(validators, data, urlUsed); }; @@ -55,7 +67,7 @@ export async function fetchBridgeFeatureFlags(): Promise { }); if ( - validateResponse( + validateResponse( [ { property: BridgeFlag.EXTENSION_SUPPORT, @@ -104,3 +116,46 @@ export async function fetchBridgeFeatureFlags(): Promise { [BridgeFeatureFlagsKey.NETWORK_DEST_ALLOWLIST]: [], }; } + +// Returns a list of enabled (unblocked) tokens +export async function fetchBridgeTokens( + chainId: Hex, +): Promise> { + // TODO make token api v2 call + const url = `${BRIDGE_API_BASE_URL}/getTokens?chainId=${hexToDecimal( + chainId, + )}`; + const tokens = await fetchWithCache({ + url, + fetchOptions: { method: 'GET', headers: CLIENT_ID_HEADER }, + cacheOptions: { cacheRefreshTime: CACHE_REFRESH_TEN_MINUTES }, + functionName: 'fetchBridgeTokens', + }); + + const nativeToken = + SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ + chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP + ]; + + const transformedTokens: Record = {}; + if (nativeToken) { + transformedTokens[nativeToken.address] = nativeToken; + } + + tokens.forEach((token: SwapsTokenObject) => { + if ( + validateResponse( + TOKEN_VALIDATORS, + token, + url, + ) && + !( + isSwapsDefaultTokenSymbol(token.symbol, chainId) || + isSwapsDefaultTokenAddress(token.address, chainId) + ) + ) { + transformedTokens[token.address] = token; + } + }); + return transformedTokens; +} diff --git a/ui/pages/bridge/index.test.tsx b/ui/pages/bridge/index.test.tsx index 4352ff359742..a73cfa370681 100644 --- a/ui/pages/bridge/index.test.tsx +++ b/ui/pages/bridge/index.test.tsx @@ -22,6 +22,8 @@ setBackgroundConnection({ getNetworkConfigurationByNetworkClientId: jest .fn() .mockResolvedValue({ chainId: '0x1' }), + setBridgeFeatureFlags: jest.fn(), + selectSrcNetwork: jest.fn(), // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); diff --git a/ui/pages/bridge/index.tsx b/ui/pages/bridge/index.tsx index f216a52ec71d..e4b5c0b930d4 100644 --- a/ui/pages/bridge/index.tsx +++ b/ui/pages/bridge/index.tsx @@ -28,10 +28,14 @@ import { BlockSize, } from '../../helpers/constants/design-system'; import { getIsBridgeEnabled } from '../../selectors'; +import useBridging from '../../hooks/bridge/useBridging'; import { PrepareBridgePage } from './prepare/prepare-bridge-page'; const CrossChainSwap = () => { const t = useContext(I18nContext); + + useBridging(); + const history = useHistory(); const dispatch = useDispatch(); @@ -40,7 +44,6 @@ const CrossChainSwap = () => { const redirectToDefaultRoute = async () => { history.push({ pathname: DEFAULT_ROUTE, - // @ts-expect-error - property 'state' does not exist on type PartialPath. state: { stayOnHomePage: true }, }); dispatch(clearSwapsState()); diff --git a/ui/pages/confirm-decrypt-message/confirm-decrypt-message.container.js b/ui/pages/confirm-decrypt-message/confirm-decrypt-message.container.js index fd6585381a27..88d7c8deb40b 100644 --- a/ui/pages/confirm-decrypt-message/confirm-decrypt-message.container.js +++ b/ui/pages/confirm-decrypt-message/confirm-decrypt-message.container.js @@ -10,9 +10,7 @@ import { decryptMsgInline, } from '../../store/actions'; import { - conversionRateSelector, getCurrentCurrency, - getPreferences, getTargetAccountWithSendEtherInfo, unconfirmedTransactionsListSelector, } from '../../selectors'; @@ -21,13 +19,12 @@ import { getMostRecentOverviewPage } from '../../ducks/history/history'; import { getNativeCurrency } from '../../ducks/metamask/metamask'; import ConfirmDecryptMessage from './confirm-decrypt-message.component'; +// ConfirmDecryptMessage component is not used in codebase, removing usage of useNativeCurrencyAsPrimaryCurrency function mapStateToProps(state) { const { metamask: { subjectMetadata = {} }, } = state; - const { useNativeCurrencyAsPrimaryCurrency } = getPreferences(state); - const unconfirmedTransactions = unconfirmedTransactionsListSelector(state); const txData = cloneDeep(unconfirmedTransactions[0]); @@ -43,9 +40,7 @@ function mapStateToProps(state) { fromAccount, requester: null, requesterAddress: null, - conversionRate: useNativeCurrencyAsPrimaryCurrency - ? null - : conversionRateSelector(state), + conversionRate: null, mostRecentOverviewPage: getMostRecentOverviewPage(state), nativeCurrency: getNativeCurrency(state), currentCurrency: getCurrentCurrency(state), diff --git a/ui/pages/confirm-encryption-public-key/__snapshots__/confirm-encryption-public-key.component.test.js.snap b/ui/pages/confirm-encryption-public-key/__snapshots__/confirm-encryption-public-key.component.test.js.snap index 05cee4e7016f..b6ae6ed60c6f 100644 --- a/ui/pages/confirm-encryption-public-key/__snapshots__/confirm-encryption-public-key.component.test.js.snap +++ b/ui/pages/confirm-encryption-public-key/__snapshots__/confirm-encryption-public-key.component.test.js.snap @@ -208,252 +208,6 @@ exports[`ConfirmDecryptMessage Component should match snapshot when preference i -

- - test - -
- would like your public encryption key. By consenting, this site will be able to compose encrypted messages to you. - - -
- -
-
- -
-
-`; - -exports[`ConfirmDecryptMessage Component should match snapshot when preference is Fiat currency 1`] = ` -
-
-
-
-
- Request encryption public key -
-
-
-
-
-
- -
-
- - T - -
- - -
diff --git a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js index 1595576a4fac..dd84bea68360 100644 --- a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js +++ b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js @@ -10,8 +10,6 @@ import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics' import SiteOrigin from '../../components/ui/site-origin'; import { Numeric } from '../../../shared/modules/Numeric'; import { EtherDenomination } from '../../../shared/constants/common'; -import { formatCurrency } from '../../helpers/utils/confirm-tx.util'; -import { getValueFromWeiHex } from '../../../shared/modules/conversion.utils'; export default class ConfirmEncryptionPublicKey extends Component { static contextTypes = { @@ -34,8 +32,6 @@ export default class ConfirmEncryptionPublicKey extends Component { subjectMetadata: PropTypes.object, mostRecentOverviewPage: PropTypes.string.isRequired, nativeCurrency: PropTypes.string.isRequired, - currentCurrency: PropTypes.string.isRequired, - conversionRate: PropTypes.number, }; renderHeader = () => { @@ -73,30 +69,20 @@ export default class ConfirmEncryptionPublicKey extends Component { renderBalance = () => { const { - conversionRate, nativeCurrency, - currentCurrency, fromAccount: { balance }, } = this.props; const { t } = this.context; - const nativeCurrencyBalance = conversionRate - ? formatCurrency( - getValueFromWeiHex({ - value: balance, - fromCurrency: nativeCurrency, - toCurrency: currentCurrency, - conversionRate, - numberOfDecimals: 6, - toDenomination: EtherDenomination.ETH, - }), - currentCurrency, - ) - : new Numeric(balance, 16, EtherDenomination.WEI) - .toDenomination(EtherDenomination.ETH) - .round(6) - .toBase(10) - .toString(); + const nativeCurrencyBalance = new Numeric( + balance, + 16, + EtherDenomination.WEI, + ) + .toDenomination(EtherDenomination.ETH) + .round(6) + .toBase(10) + .toString(); return (
@@ -104,9 +90,7 @@ export default class ConfirmEncryptionPublicKey extends Component { {`${t('balance')}:`}
- {`${nativeCurrencyBalance} ${ - conversionRate ? currentCurrency?.toUpperCase() : nativeCurrency - }`} + {`${nativeCurrencyBalance} ${nativeCurrency}`}
); diff --git a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.test.js b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.test.js index 771a5a5f5ef7..3edc10e9d313 100644 --- a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.test.js +++ b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.test.js @@ -61,19 +61,6 @@ describe('ConfirmDecryptMessage Component', () => { ).toMatchInlineSnapshot(`"966.987986 ABC"`); }); - it('should match snapshot when preference is Fiat currency', () => { - const { container } = renderWithProvider( - , - store, - ); - - expect(container).toMatchSnapshot(); - expect( - container.querySelector('.request-encryption-public-key__balance-value') - .textContent, - ).toMatchInlineSnapshot(`"1520956.064158 DEF"`); - }); - it('should match snapshot when there is no txData', () => { const newProps = { ...baseProps, diff --git a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.container.js b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.container.js index 489d7d088033..554ba41fdaa4 100644 --- a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.container.js +++ b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.container.js @@ -9,11 +9,8 @@ import { } from '../../store/actions'; import { - conversionRateSelector, unconfirmedTransactionsListSelector, getTargetAccountWithSendEtherInfo, - getPreferences, - getCurrentCurrency, } from '../../selectors'; import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck'; @@ -26,8 +23,6 @@ function mapStateToProps(state) { metamask: { subjectMetadata = {} }, } = state; - const { useNativeCurrencyAsPrimaryCurrency } = getPreferences(state); - const unconfirmedTransactions = unconfirmedTransactionsListSelector(state); const txData = unconfirmedTransactions[0]; @@ -43,12 +38,8 @@ function mapStateToProps(state) { fromAccount, requester: null, requesterAddress: null, - conversionRate: useNativeCurrencyAsPrimaryCurrency - ? null - : conversionRateSelector(state), mostRecentOverviewPage: getMostRecentOverviewPage(state), nativeCurrency: getNativeCurrency(state), - currentCurrency: getCurrentCurrency(state), }; } diff --git a/ui/pages/confirmations/components/confirm-gas-display/confirm-gas-display.test.js b/ui/pages/confirmations/components/confirm-gas-display/confirm-gas-display.test.js index c80ef735222b..c1d4ae838301 100644 --- a/ui/pages/confirmations/components/confirm-gas-display/confirm-gas-display.test.js +++ b/ui/pages/confirmations/components/confirm-gas-display/confirm-gas-display.test.js @@ -39,9 +39,7 @@ const render = async ({ transactionProp = {}, contextProps = {} } = {}) => { balance: '0x1F4', }, }, - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, gasFeeEstimates: mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, gasFeeEstimatesByChainId: { diff --git a/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.js b/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.js index 34440be28693..70d5ed1070f4 100644 --- a/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.js +++ b/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.js @@ -5,7 +5,6 @@ import { useSelector } from 'react-redux'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; import { getIsMainnet, - getPreferences, getUnapprovedTransactions, getUseCurrencyRateCheck, transactionFeeSelector, @@ -34,7 +33,6 @@ const ConfirmLegacyGasDisplay = ({ 'data-testid': dataTestId } = {}) => { // state selectors const isMainnet = useSelector(getIsMainnet); const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); - const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences); const unapprovedTxs = useSelector(getUnapprovedTransactions); const transactionData = useDraftTransactionWithTxParams(); const txData = useSelector((state) => txDataSelector(state)); @@ -108,7 +106,7 @@ const ConfirmLegacyGasDisplay = ({ 'data-testid': dataTestId } = {}) => {
) @@ -119,7 +117,6 @@ const ConfirmLegacyGasDisplay = ({ 'data-testid': dataTestId } = {}) => { { key="editGasSubTextFeeAmount" type={PRIMARY} value={estimatedHexMaxFeeTotal} - hideLabel={!useNativeCurrencyAsPrimaryCurrency} />
diff --git a/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.test.js b/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.test.js index df5b9ea0e50f..4952fb87edca 100644 --- a/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.test.js +++ b/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.test.js @@ -21,9 +21,6 @@ const mmState = { balance: '0x1F4', }, }, - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, }, confirmTransaction: { txData: { diff --git a/ui/pages/confirmations/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.test.js b/ui/pages/confirmations/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.test.js index 5b1e505ddc14..5d3065e8a3d3 100644 --- a/ui/pages/confirmations/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.test.js +++ b/ui/pages/confirmations/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.test.js @@ -11,9 +11,7 @@ describe('Confirm Detail Row Component', () => { metamask: { currencyRates: {}, ...mockNetworkState({ chainId: CHAIN_IDS.GOERLI }), - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, internalAccounts: defaultMockState.metamask.internalAccounts, }, }; diff --git a/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.js b/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.js index e1219d299288..da99ade8210c 100644 --- a/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.js +++ b/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.js @@ -29,7 +29,6 @@ const ConfirmSubTitle = ({ if (subtitleComponent) { return subtitleComponent; } - return ( -
-
-
-
-

- Data -

-
-
-
- - - - - - - - - -
-
-
-
-
-
-

- Data -

-
-
-
- - - - - - - - - -
-
-
-
@@ -188,7 +56,7 @@ exports[`Info renders info section for approve request 1`] = ` data-testid="gas-fee-section" >
@@ -252,7 +120,7 @@ exports[`Info renders info section for approve request 1`] = `
@@ -384,7 +252,7 @@ exports[`Info renders info section for contract interaction request 1`] = ` data-testid="transaction-details-section" >
@@ -428,7 +296,7 @@ exports[`Info renders info section for contract interaction request 1`] = `
@@ -525,7 +393,7 @@ exports[`Info renders info section for contract interaction request 1`] = ` data-testid="gas-fee-section" >
@@ -589,7 +457,7 @@ exports[`Info renders info section for contract interaction request 1`] = `
@@ -640,7 +508,7 @@ exports[`Info renders info section for personal sign request 1`] = ` class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >
-
+
@@ -824,73 +694,7 @@ exports[`Info renders info section for setApprovalForAll request 1`] = ` data-testid="confirmation__approve-details" >
-
-
-
-

- Data -

-
-
-
- - - - - - - - - -
-
-
-
@@ -939,7 +743,7 @@ exports[`Info renders info section for setApprovalForAll request 1`] = ` data-testid="gas-fee-section" >
@@ -1003,7 +807,7 @@ exports[`Info renders info section for setApprovalForAll request 1`] = `
@@ -1054,7 +858,7 @@ exports[`Info renders info section for typed sign request 1`] = ` class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >
renders component for approve request 1`] = ` data-testid="confirmation__simulation_section" >
renders component for approve request 1`] = `
renders component for approve request 1`] = ` 1000

-
+
@@ -107,7 +109,7 @@ exports[` renders component for approve request 1`] = ` data-testid="confirmation__approve-details" >
@@ -202,7 +204,7 @@ exports[` renders component for approve request 1`] = ` style="height: 1px; margin-left: -8px; margin-right: -8px;" />
@@ -246,7 +248,7 @@ exports[` renders component for approve request 1`] = `
@@ -343,7 +345,7 @@ exports[` renders component for approve request 1`] = ` data-testid="confirmation__approve-spending-cap-section" >
renders component for approve request 1`] = ` style="height: 1px; margin-left: -8px; margin-right: -8px;" />
@@ -435,7 +437,7 @@ exports[` renders component for approve request 1`] = ` data-testid="gas-fee-section" >
@@ -499,7 +501,7 @@ exports[` renders component for approve request 1`] = `
@@ -541,7 +543,7 @@ exports[` renders component for approve request 1`] = `
@@ -595,7 +597,7 @@ exports[` renders component for approve request 1`] = ` data-testid="advanced-details-nonce-section" >
renders component for approve request 1`] = ` data-testid="advanced-details-data-section" >
renders component for approve request 1`] = ` />
@@ -699,7 +701,7 @@ exports[` renders component for approve request 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -791,7 +793,7 @@ exports[` renders component for approve request 1`] = `
diff --git a/ui/pages/confirmations/components/confirm/info/approve/approve-details/__snapshots__/approve-details.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/approve/approve-details/__snapshots__/approve-details.test.tsx.snap index a66499aab561..17d04e237fb2 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/approve-details/__snapshots__/approve-details.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/approve/approve-details/__snapshots__/approve-details.test.tsx.snap @@ -11,7 +11,7 @@ exports[` renders component for approve details 1`] = ` data-testid="advanced-details-data-section" >
renders component for approve details for setApprova data-testid="advanced-details-data-section" >
renders component 1`] = ` data-testid="confirmation__simulation_section" >
renders component 1`] = `
renders component 1`] = ` 1000

-
+
diff --git a/ui/pages/confirmations/components/confirm/info/approve/approve.tsx b/ui/pages/confirmations/components/confirm/info/approve/approve.tsx index a982c8af90bb..eabf8639ccfb 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/approve.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/approve.tsx @@ -8,6 +8,7 @@ import { useConfirmContext } from '../../../../context/confirm'; import { useAssetDetails } from '../../../../hooks/useAssetDetails'; import { selectConfirmationAdvancedDetailsOpen } from '../../../../selectors/preferences'; import { AdvancedDetails } from '../shared/advanced-details/advanced-details'; +import { ConfirmLoader } from '../shared/confirm-loader/confirm-loader'; import { GasFeesSection } from '../shared/gas-fees-section/gas-fees-section'; import { ApproveDetails } from './approve-details/approve-details'; import { ApproveStaticSimulation } from './approve-static-simulation/approve-static-simulation'; @@ -38,7 +39,7 @@ const ApproveInfo = () => { transactionMeta.txParams.data, ); - const { spendingCap } = useApproveTokenSimulation( + const { spendingCap, pending } = useApproveTokenSimulation( transactionMeta, decimals || '0', ); @@ -51,6 +52,10 @@ const ApproveInfo = () => { return null; } + if (pending) { + return ; + } + return ( <> {showRevokeVariant ? ( diff --git a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/__snapshots__/edit-spending-cap-modal.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/__snapshots__/edit-spending-cap-modal.test.tsx.snap index 7d64def9ff73..e56a87ed34d7 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/__snapshots__/edit-spending-cap-modal.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/__snapshots__/edit-spending-cap-modal.test.tsx.snap @@ -57,7 +57,7 @@ exports[` renders component 1`] = ` focused="true" placeholder="1000 TST" type="number" - value="" + value="1000" />

{ + if (formattedSpendingCap) { + setCustomSpendingCapInputValue(formattedSpendingCap.toString()); + } + }, [formattedSpendingCap]); const handleCancel = useCallback(() => { setIsOpenEditSpendingCapModal(false); - setCustomSpendingCapInputValue(''); - }, [setIsOpenEditSpendingCapModal, setCustomSpendingCapInputValue]); + setCustomSpendingCapInputValue(formattedSpendingCap.toString()); + }, [ + setIsOpenEditSpendingCapModal, + setCustomSpendingCapInputValue, + formattedSpendingCap, + ]); const [isModalSaving, setIsModalSaving] = useState(false); const handleSubmit = useCallback(async () => { setIsModalSaving(true); - const parsedValue = parseInt(customSpendingCapInputValue, 10); + const parsedValue = parseInt(String(customSpendingCapInputValue), 10); const customTxParamsData = getCustomTxParamsData( transactionMeta?.txParams?.data, @@ -103,8 +113,8 @@ export const EditSpendingCapModal = ({ setIsModalSaving(false); setIsOpenEditSpendingCapModal(false); - setCustomSpendingCapInputValue(''); - }, [customSpendingCapInputValue]); + setCustomSpendingCapInputValue(formattedSpendingCap.toString()); + }, [customSpendingCapInputValue, formattedSpendingCap]); return ( param.value !== undefined && !isHexString(param.value) && + param.value.length === undefined && !isBoolean(param.value), ); if (paramIndex === -1) { return 0; } - return new BigNumber(value.data[0].params[paramIndex].value) + return new BigNumber(value.data[0].params[paramIndex].value.toString()) .dividedBy(new BigNumber(10).pow(Number(decimals))) .toNumber(); }, [value, decimals]); diff --git a/ui/pages/confirmations/components/confirm/info/approve/spending-cap/__snapshots__/spending-cap.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/approve/spending-cap/__snapshots__/spending-cap.test.tsx.snap index 6e415415a452..3579fe496673 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/spending-cap/__snapshots__/spending-cap.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/approve/spending-cap/__snapshots__/spending-cap.test.tsx.snap @@ -7,7 +7,7 @@ exports[` renders component 1`] = ` data-testid="confirmation__approve-spending-cap-section" >

renders component 1`] = ` style="height: 1px; margin-left: -8px; margin-right: -8px;" />
diff --git a/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap index efc77a1b8eb1..5f0370343f00 100644 --- a/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap @@ -89,7 +89,7 @@ exports[` renders component for contract interaction requ data-testid="transaction-details-section" >
@@ -133,7 +133,7 @@ exports[` renders component for contract interaction requ
@@ -230,7 +230,7 @@ exports[` renders component for contract interaction requ data-testid="gas-fee-section" >
@@ -294,7 +294,7 @@ exports[` renders component for contract interaction requ
@@ -464,7 +464,7 @@ exports[` renders component for contract interaction requ data-testid="transaction-details-section" >
@@ -508,7 +508,7 @@ exports[` renders component for contract interaction requ
@@ -605,7 +605,7 @@ exports[` renders component for contract interaction requ data-testid="gas-fee-section" >
@@ -669,7 +669,7 @@ exports[` renders component for contract interaction requ
@@ -836,7 +836,7 @@ exports[` renders component for contract interaction requ data-testid="transaction-details-section" >
@@ -880,7 +880,7 @@ exports[` renders component for contract interaction requ
@@ -977,7 +977,7 @@ exports[` renders component for contract interaction requ data-testid="gas-fee-section" >
@@ -1041,7 +1041,7 @@ exports[` renders component for contract interaction requ
diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.test.ts b/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.test.ts index 35f2f42c4792..32a711abf754 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.test.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.test.ts @@ -59,6 +59,23 @@ describe('useDecodedTransactionData', () => { }, ); + it('returns undefined if no transaction to', async () => { + const result = await runHook( + getMockConfirmStateForTransaction({ + id: '123', + chainId: CHAIN_ID_MOCK, + type: TransactionType.contractInteraction, + status: TransactionStatus.unapproved, + txParams: { + data: TRANSACTION_DATA_UNISWAP, + to: undefined, + } as TransactionParams, + }), + ); + + expect(result).toStrictEqual({ pending: false, value: undefined }); + }); + it('returns the decoded data', async () => { decodeTransactionDataMock.mockResolvedValue(TRANSACTION_DECODE_SOURCIFY); diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.ts b/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.ts index b2d69df413d4..6934f893378d 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.ts @@ -18,9 +18,10 @@ export function useDecodedTransactionData(): AsyncResult< const chainId = currentConfirmation?.chainId as Hex; const contractAddress = currentConfirmation?.txParams?.to as Hex; const transactionData = currentConfirmation?.txParams?.data as Hex; + const transactionTo = currentConfirmation?.txParams?.to as Hex; return useAsyncResult(async () => { - if (!hasTransactionData(transactionData)) { + if (!hasTransactionData(transactionData) || !transactionTo) { return undefined; } @@ -29,5 +30,5 @@ export function useDecodedTransactionData(): AsyncResult< chainId, contractAddress, }); - }, [transactionData, chainId, contractAddress]); + }, [transactionData, transactionTo, chainId, contractAddress]); } diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useFourByte.test.ts b/ui/pages/confirmations/components/confirm/info/hooks/useFourByte.test.ts index 5d4a023c82bb..9f1a848a96cd 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useFourByte.test.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useFourByte.test.ts @@ -34,7 +34,7 @@ describe('useFourByte', () => { expect(result.current.params).toEqual([]); }); - it('returns empty object if resolution is turned off', () => { + it('returns null if resolution disabled', () => { const currentConfirmation = genUnapprovedContractInteractionConfirmation({ address: CONTRACT_INTERACTION_SENDER_ADDRESS, txData: depositHexData, @@ -57,7 +57,7 @@ describe('useFourByte', () => { expect(result.current).toBeNull(); }); - it("returns undefined if it's not known even if resolution is enabled", () => { + it('returns null if not known even if resolution enabled', () => { const currentConfirmation = genUnapprovedContractInteractionConfirmation({ address: CONTRACT_INTERACTION_SENDER_ADDRESS, txData: depositHexData, @@ -77,4 +77,29 @@ describe('useFourByte', () => { expect(result.current).toBeNull(); }); + + it('returns null if no transaction to', () => { + const currentConfirmation = genUnapprovedContractInteractionConfirmation({ + address: CONTRACT_INTERACTION_SENDER_ADDRESS, + txData: depositHexData, + }) as TransactionMeta; + + currentConfirmation.txParams.to = undefined; + + const { result } = renderHookWithProvider( + () => useFourByte(currentConfirmation), + { + ...mockState, + metamask: { + ...mockState.metamask, + use4ByteResolution: true, + knownMethodData: { + [depositHexData]: { name: 'Deposit', params: [] }, + }, + }, + }, + ); + + expect(result.current).toBeNull(); + }); }); diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useFourByte.ts b/ui/pages/confirmations/components/confirm/info/hooks/useFourByte.ts index 7e2b81b443bd..7fece13fb417 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useFourByte.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useFourByte.ts @@ -1,28 +1,41 @@ import { TransactionMeta } from '@metamask/transaction-controller'; import { useDispatch, useSelector } from 'react-redux'; import { useEffect } from 'react'; +import { Hex } from '@metamask/utils'; import { getKnownMethodData, use4ByteResolutionSelector, } from '../../../../../../selectors'; import { getContractMethodData } from '../../../../../../store/actions'; +import { hasTransactionData } from '../../../../../../../shared/modules/transaction.utils'; export const useFourByte = (currentConfirmation: TransactionMeta) => { const dispatch = useDispatch(); const isFourByteEnabled = useSelector(use4ByteResolutionSelector); - const transactionData = currentConfirmation?.txParams?.data; + const transactionTo = currentConfirmation?.txParams?.to; + const transactionData = currentConfirmation?.txParams?.data as + | Hex + | undefined; useEffect(() => { - if (!isFourByteEnabled || !transactionData) { + if ( + !isFourByteEnabled || + !hasTransactionData(transactionData) || + !transactionTo + ) { return; } dispatch(getContractMethodData(transactionData)); - }, [isFourByteEnabled, transactionData, dispatch]); + }, [isFourByteEnabled, transactionData, transactionTo, dispatch]); const methodData = useSelector((state) => getKnownMethodData(state, transactionData), ); + if (!transactionTo) { + return null; + } + return methodData; }; diff --git a/ui/pages/confirmations/components/confirm/info/info.test.tsx b/ui/pages/confirmations/components/confirm/info/info.test.tsx index 4931c2fbaa01..75d98d91a1bd 100644 --- a/ui/pages/confirmations/components/confirm/info/info.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/info.test.tsx @@ -1,6 +1,6 @@ +import { screen, waitFor } from '@testing-library/react'; import React from 'react'; import configureMockStore from 'redux-mock-store'; - import { getMockApproveConfirmState, getMockContractInteractionConfirmState, @@ -50,17 +50,27 @@ describe('Info', () => { expect(container).toMatchSnapshot(); }); - it('renders info section for approve request', () => { + it('renders info section for approve request', async () => { const state = getMockApproveConfirmState(); const mockStore = configureMockStore([])(state); const { container } = renderWithConfirmContextProvider(, mockStore); + + await waitFor(() => { + expect(screen.getByText('Speed')).toBeInTheDocument(); + }); + expect(container).toMatchSnapshot(); }); - it('renders info section for setApprovalForAll request', () => { + it('renders info section for setApprovalForAll request', async () => { const state = getMockSetApprovalForAllConfirmState(); const mockStore = configureMockStore([])(state); const { container } = renderWithConfirmContextProvider(, mockStore); + + await waitFor(() => { + expect(screen.getByText('Speed')).toBeInTheDocument(); + }); + expect(container).toMatchSnapshot(); }); }); diff --git a/ui/pages/confirmations/components/confirm/info/personal-sign/__snapshots__/personal-sign.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/personal-sign/__snapshots__/personal-sign.test.tsx.snap index 79290bb3b49b..2f46c283b830 100644 --- a/ui/pages/confirmations/components/confirm/info/personal-sign/__snapshots__/personal-sign.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/personal-sign/__snapshots__/personal-sign.test.tsx.snap @@ -6,7 +6,7 @@ exports[`PersonalSignInfo handle reverse string properly 1`] = ` class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >
renders component for approve request 1`] = ` data-testid="confirmation__simulation_section" >
renders component for approve request 1`] = `
renders component for approve request 1`] = ` All

-
+
@@ -109,73 +111,7 @@ exports[` renders component for approve request 1`] = ` data-testid="confirmation__approve-details" >
-
-
-
-

- Data -

-
-
-
- - - - - - - - - -
-
-
-
@@ -224,7 +160,7 @@ exports[` renders component for approve request 1`] = ` data-testid="gas-fee-section" >
@@ -288,7 +224,7 @@ exports[` renders component for approve request 1`] = `
diff --git a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/revoke-set-approval-for-all-static-simulation/__snapshots__/revoke-set-approval-for-all-static-simulation.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/revoke-set-approval-for-all-static-simulation/__snapshots__/revoke-set-approval-for-all-static-simulation.test.tsx.snap index 37e785040b0f..8c94061320fe 100644 --- a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/revoke-set-approval-for-all-static-simulation/__snapshots__/revoke-set-approval-for-all-static-simulation.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/revoke-set-approval-for-all-static-simulation/__snapshots__/revoke-set-approval-for-all-static-simulation.test.tsx.snap @@ -7,7 +7,7 @@ exports[` renders component for setAp data-testid="confirmation__simulation_section" >
renders component for setAp
renders component for setAp
-
+
@@ -92,7 +94,7 @@ exports[` renders component for setAp
renders component for setAp
-
+
diff --git a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.test.tsx b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.test.tsx index 3460b1d8e76e..77a840dcece5 100644 --- a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.test.tsx @@ -1,3 +1,4 @@ +import { screen, waitFor } from '@testing-library/react'; import React from 'react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; @@ -39,6 +40,10 @@ describe('', () => { mockStore, ); + await waitFor(() => { + expect(screen.getByText('Data')).toBeInTheDocument(); + }); + expect(container).toMatchSnapshot(); }); diff --git a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.tsx b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.tsx index 6a2c98f224e2..92df913783a1 100644 --- a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.tsx +++ b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.tsx @@ -6,6 +6,7 @@ import { selectConfirmationAdvancedDetailsOpen } from '../../../../selectors/pre import { ApproveDetails } from '../approve/approve-details/approve-details'; import { useDecodedTransactionData } from '../hooks/useDecodedTransactionData'; import { AdvancedDetails } from '../shared/advanced-details/advanced-details'; +import { ConfirmLoader } from '../shared/confirm-loader/confirm-loader'; import { GasFeesSection } from '../shared/gas-fees-section/gas-fees-section'; import { getIsRevokeSetApprovalForAll } from '../utils'; import { RevokeSetApprovalForAllStaticSimulation } from './revoke-set-approval-for-all-static-simulation/revoke-set-approval-for-all-static-simulation'; @@ -21,7 +22,7 @@ const SetApprovalForAllInfo = () => { const decodedResponse = useDecodedTransactionData(); - const { value } = decodedResponse; + const { value, pending } = decodedResponse; const isRevokeSetApprovalForAll = getIsRevokeSetApprovalForAll(value); @@ -31,6 +32,10 @@ const SetApprovalForAllInfo = () => { return null; } + if (pending) { + return ; + } + return ( <> {isRevokeSetApprovalForAll ? ( diff --git a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/__snapshots__/set-approval-for-all-static-simulation.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/__snapshots__/set-approval-for-all-static-simulation.test.tsx.snap index 9ab4107ff173..b2f289875f6b 100644 --- a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/__snapshots__/set-approval-for-all-static-simulation.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/__snapshots__/set-approval-for-all-static-simulation.test.tsx.snap @@ -7,7 +7,7 @@ exports[` renders component for approve req data-testid="confirmation__simulation_section" >
renders component for approve req
renders component for approve req All

-
+
diff --git a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/__snapshots__/advanced-details.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/__snapshots__/advanced-details.test.tsx.snap index a521ff23795a..f66db615defe 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/__snapshots__/advanced-details.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/__snapshots__/advanced-details.test.tsx.snap @@ -7,7 +7,7 @@ exports[` does not render component for advanced transaction data-testid="advanced-details-nonce-section" >
does not render component for advanced transaction data-testid="advanced-details-data-section" >
renders component for advanced transaction details data-testid="advanced-details-nonce-section" >
renders component for advanced transaction details data-testid="advanced-details-data-section" >
renders component 1`] = ` +
+
+ + + + + + + + + +
+
+`; diff --git a/ui/pages/confirmations/components/confirm/info/shared/confirm-loader/confirm-loader.test.tsx b/ui/pages/confirmations/components/confirm/info/shared/confirm-loader/confirm-loader.test.tsx new file mode 100644 index 000000000000..155f6a27850b --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/shared/confirm-loader/confirm-loader.test.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { getMockSetApprovalForAllConfirmState } from '../../../../../../../../test/data/confirmations/helper'; +import { renderWithConfirmContextProvider } from '../../../../../../../../test/lib/confirmations/render-helpers'; +import { ConfirmLoader } from './confirm-loader'; + +describe('', () => { + const middleware = [thunk]; + + it('renders component', async () => { + const state = getMockSetApprovalForAllConfirmState(); + + const mockStore = configureMockStore(middleware)(state); + + const { container } = renderWithConfirmContextProvider( + , + mockStore, + ); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/ui/pages/confirmations/components/confirm/info/shared/confirm-loader/confirm-loader.tsx b/ui/pages/confirmations/components/confirm/info/shared/confirm-loader/confirm-loader.tsx new file mode 100644 index 000000000000..2aa57f489dfe --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/shared/confirm-loader/confirm-loader.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Box } from '../../../../../../../components/component-library'; +import Preloader from '../../../../../../../components/ui/icon/preloader'; +import { + AlignItems, + Display, + JustifyContent, +} from '../../../../../../../helpers/constants/design-system'; + +export const ConfirmLoader = () => { + return ( + + + + ); +}; diff --git a/ui/pages/confirmations/components/confirm/info/shared/edit-gas-fees-row/__snapshots__/edit-gas-fees-row.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/edit-gas-fees-row/__snapshots__/edit-gas-fees-row.test.tsx.snap index 87a78c4928bc..3ad4343e8ee5 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/edit-gas-fees-row/__snapshots__/edit-gas-fees-row.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/shared/edit-gas-fees-row/__snapshots__/edit-gas-fees-row.test.tsx.snap @@ -3,7 +3,7 @@ exports[` renders component 1`] = `
diff --git a/ui/pages/confirmations/components/confirm/info/shared/edit-gas-fees-row/edit-gas-fees-row.tsx b/ui/pages/confirmations/components/confirm/info/shared/edit-gas-fees-row/edit-gas-fees-row.tsx index 049270fd5af2..7d4223f01204 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/edit-gas-fees-row/edit-gas-fees-row.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/edit-gas-fees-row/edit-gas-fees-row.tsx @@ -1,5 +1,4 @@ import React, { Dispatch, SetStateAction } from 'react'; -import { useSelector } from 'react-redux'; import { TransactionMeta } from '@metamask/transaction-controller'; import { Box, Text } from '../../../../../../../components/component-library'; import { @@ -11,7 +10,6 @@ import { TextColor, } from '../../../../../../../helpers/constants/design-system'; import { useI18nContext } from '../../../../../../../hooks/useI18nContext'; -import { getPreferences } from '../../../../../../../selectors'; import { useConfirmContext } from '../../../../../context/confirm'; import { EditGasIconButton } from '../edit-gas-icon/edit-gas-icon-button'; import { ConfirmInfoAlertRow } from '../../../../../../../components/app/confirm/info/row/alert-row/alert-row'; @@ -30,9 +28,6 @@ export const EditGasFeesRow = ({ }) => { const t = useI18nContext(); - const { useNativeCurrencyAsPrimaryCurrency: isNativeCurrencyUsed } = - useSelector(getPreferences); - const { currentConfirmation: transactionMeta } = useConfirmContext(); @@ -56,14 +51,14 @@ export const EditGasFeesRow = ({ color={TextColor.textDefault} data-testid="first-gas-field" > - {isNativeCurrencyUsed ? nativeFee : fiatFee} + {nativeFee} - {isNativeCurrencyUsed ? fiatFee : nativeFee} + {fiatFee} renders component for gas fees section 1`] = `
@@ -67,7 +67,7 @@ exports[` renders component for gas fees section 1`] = `
diff --git a/ui/pages/confirmations/components/confirm/info/shared/gas-fees-row/__snapshots__/gas-fees-row.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/gas-fees-row/__snapshots__/gas-fees-row.test.tsx.snap index 6643621734f6..5de2d2361b38 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/gas-fees-row/__snapshots__/gas-fees-row.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/shared/gas-fees-row/__snapshots__/gas-fees-row.test.tsx.snap @@ -3,7 +3,7 @@ exports[` renders component 1`] = `
{ - const { useNativeCurrencyAsPrimaryCurrency: isNativeCurrencyUsed } = - useSelector(getPreferences); - return ( - {isNativeCurrencyUsed ? nativeFee : fiatFee} - - - {isNativeCurrencyUsed ? fiatFee : nativeFee} + {nativeFee} + {fiatFee} ); diff --git a/ui/pages/confirmations/components/confirm/info/shared/gas-fees-section/__snapshots__/gas-fees-section.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/gas-fees-section/__snapshots__/gas-fees-section.test.tsx.snap index fcdefe6be7c1..5901dd2e8f2d 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/gas-fees-section/__snapshots__/gas-fees-section.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/shared/gas-fees-section/__snapshots__/gas-fees-section.test.tsx.snap @@ -9,7 +9,7 @@ exports[` renders component for gas fees section 1`] = ` data-testid="gas-fee-section" >
@@ -73,7 +73,7 @@ exports[` renders component for gas fees section 1`] = `
diff --git a/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap index b830449ca55c..c53805c877e1 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap @@ -7,7 +7,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] = data-testid="advanced-details-data-section" >
@@ -140,7 +140,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -191,7 +191,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] = style="height: 1px; margin-left: -8px; margin-right: -8px;" />
@@ -299,7 +299,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -344,7 +344,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -389,7 +389,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -511,7 +511,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -562,7 +562,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] = style="height: 1px; margin-left: -8px; margin-right: -8px;" />
@@ -670,7 +670,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -726,7 +726,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -777,7 +777,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] = style="height: 1px; margin-left: -8px; margin-right: -8px;" />
@@ -885,7 +885,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -941,7 +941,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -999,7 +999,7 @@ exports[`TransactionData renders decoded data with no names 1`] = ` data-testid="advanced-details-data-section" >
@@ -1056,7 +1056,7 @@ exports[`TransactionData renders decoded data with no names 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -1101,7 +1101,7 @@ exports[`TransactionData renders decoded data with no names 1`] = `
@@ -1157,7 +1157,7 @@ exports[`TransactionData renders decoded data with no names 1`] = `
@@ -1213,7 +1213,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = ` data-testid="advanced-details-data-section" >
@@ -1286,7 +1286,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -1342,7 +1342,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1380,7 +1380,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -1418,7 +1418,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -1456,7 +1456,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -1512,7 +1512,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1557,7 +1557,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1602,7 +1602,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1648,7 +1648,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1686,7 +1686,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -1742,7 +1742,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1787,7 +1787,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1832,7 +1832,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1878,7 +1878,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1916,7 +1916,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -1972,7 +1972,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -2017,7 +2017,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -2062,7 +2062,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -2109,7 +2109,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -2165,7 +2165,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -2211,7 +2211,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -2269,7 +2269,7 @@ exports[`TransactionData renders raw hexadecimal if no decoded data 1`] = ` data-testid="advanced-details-data-section" >
renders component for transaction details 1`] = data-testid="transaction-details-section" >
@@ -60,7 +60,7 @@ exports[` renders component for transaction details 1`] =
diff --git a/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.test.tsx b/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.test.tsx index 34da84540e9b..1263acf08397 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.test.tsx @@ -1,11 +1,15 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { SimulationErrorCode } from '@metamask/transaction-controller'; import { getMockConfirmState, + getMockConfirmStateForTransaction, getMockContractInteractionConfirmState, } from '../../../../../../../../test/data/confirmations/helper'; import { renderWithConfirmContextProvider } from '../../../../../../../../test/lib/confirmations/render-helpers'; +import { CHAIN_IDS } from '../../../../../../../../shared/constants/network'; +import { genUnapprovedContractInteractionConfirmation } from '../../../../../../../../test/data/confirmations/contract-interaction'; import { TransactionDetails } from './transaction-details'; jest.mock( @@ -39,4 +43,22 @@ describe('', () => { ); expect(container).toMatchSnapshot(); }); + + it('renders component for transaction details with amount', () => { + const simulationDataMock = { + error: { code: SimulationErrorCode.Disabled }, + tokenBalanceChanges: [], + }; + const contractInteraction = genUnapprovedContractInteractionConfirmation({ + simulationData: simulationDataMock, + chainId: CHAIN_IDS.GOERLI, + }); + const state = getMockConfirmStateForTransaction(contractInteraction); + const mockStore = configureMockStore(middleware)(state); + const { getByTestId } = renderWithConfirmContextProvider( + , + mockStore, + ); + expect(getByTestId('transaction-details-amount-row')).toBeInTheDocument(); + }); }); diff --git a/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.tsx b/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.tsx index e53387af325a..706729a8cc0a 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.tsx @@ -16,6 +16,10 @@ import { selectPaymasterAddress } from '../../../../../../../selectors/account-a import { selectConfirmationAdvancedDetailsOpen } from '../../../../../selectors/preferences'; import { useConfirmContext } from '../../../../../context/confirm'; import { useFourByte } from '../../hooks/useFourByte'; +import { ConfirmInfoRowCurrency } from '../../../../../../../components/app/confirm/info/row/currency'; +import { PRIMARY } from '../../../../../../../helpers/constants/common'; +import { useUserPreferencedCurrency } from '../../../../../../../hooks/useUserPreferencedCurrency'; +import { HEX_ZERO } from '../constants'; export const OriginRow = () => { const t = useI18nContext(); @@ -83,6 +87,30 @@ export const MethodDataRow = () => { ); }; +const AmountRow = () => { + const t = useI18nContext(); + const { currentConfirmation } = useConfirmContext(); + const { currency } = useUserPreferencedCurrency(PRIMARY); + + const value = currentConfirmation?.txParams?.value; + const simulationData = currentConfirmation?.simulationData; + + if (!value || value === HEX_ZERO || !simulationData?.error) { + return null; + } + + return ( + + + + + + ); +}; + const PaymasterRow = () => { const t = useI18nContext(); const { currentConfirmation } = useConfirmContext(); @@ -124,6 +152,7 @@ export const TransactionDetails = () => { {showAdvancedDetails && } + ); diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/__snapshots__/typed-sign-v1.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/__snapshots__/typed-sign-v1.test.tsx.snap index 57aa7e62fcb2..59a6065e6b9d 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/__snapshots__/typed-sign-v1.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/__snapshots__/typed-sign-v1.test.tsx.snap @@ -6,7 +6,7 @@ exports[`TypedSignInfo correctly renders typed sign data request 1`] = ` class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >
-
+
@@ -127,7 +129,7 @@ exports[`TypedSignInfo correctly renders permit sign type 1`] = ` class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >
-
+
@@ -762,7 +766,7 @@ exports[`TypedSignInfo correctly renders permit sign type with no deadline 1`] = class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >
-
+
diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap index 022ff8b6dbc2..26def806c6fa 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap @@ -32,7 +32,9 @@ exports[`PermitSimulationValueDisplay renders component correctly 1`] = `
-
+
diff --git a/ui/pages/confirmations/components/confirm/row/__snapshots__/dataTree.test.tsx.snap b/ui/pages/confirmations/components/confirm/row/__snapshots__/dataTree.test.tsx.snap index cc8f3451676a..68d8aab887be 100644 --- a/ui/pages/confirmations/components/confirm/row/__snapshots__/dataTree.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/row/__snapshots__/dataTree.test.tsx.snap @@ -6,7 +6,7 @@ exports[`DataTree correctly renders reverse strings 1`] = ` class="mm-box mm-box--width-full" >
{ ).toBeInTheDocument(); }); - it('should render the title and description for a setApprovalForAll transaction', () => { + it('should render the title and description for a setApprovalForAll transaction', async () => { const mockStore = configureMockStore([])( getMockSetApprovalForAllConfirmState(), ); @@ -144,12 +144,15 @@ describe('ConfirmTitle', () => { mockStore, ); - expect( - getByText(tEn('setApprovalForAllRedesignedTitle') as string), - ).toBeInTheDocument(); - expect( - getByText(tEn('confirmTitleDescApproveTransaction') as string), - ).toBeInTheDocument(); + await waitFor(() => { + expect( + getByText(tEn('setApprovalForAllRedesignedTitle') as string), + ).toBeInTheDocument(); + + expect( + getByText(tEn('confirmTitleDescApproveTransaction') as string), + ).toBeInTheDocument(); + }); }); describe('Alert banner', () => { diff --git a/ui/pages/confirmations/components/confirm/title/title.tsx b/ui/pages/confirmations/components/confirm/title/title.tsx index 34c66740c041..702c496b4e25 100644 --- a/ui/pages/confirmations/components/confirm/title/title.tsx +++ b/ui/pages/confirmations/components/confirm/title/title.tsx @@ -68,7 +68,12 @@ const getTitle = ( isNFT?: boolean, customSpendingCap?: string, isRevokeSetApprovalForAll?: boolean, + pending?: boolean, ) => { + if (pending) { + return ''; + } + switch (confirmation?.type) { case TransactionType.contractInteraction: return t('confirmTitleTransaction'); @@ -109,7 +114,12 @@ const getDescription = ( isNFT?: boolean, customSpendingCap?: string, isRevokeSetApprovalForAll?: boolean, + pending?: boolean, ) => { + if (pending) { + return ''; + } + switch (confirmation?.type) { case TransactionType.contractInteraction: return ''; @@ -151,9 +161,11 @@ const ConfirmTitle: React.FC = memo(() => { const { isNFT } = useIsNFT(currentConfirmation as TransactionMeta); - const { customSpendingCap } = useCurrentSpendingCap(currentConfirmation); + const { customSpendingCap, pending: spendingCapPending } = + useCurrentSpendingCap(currentConfirmation); let isRevokeSetApprovalForAll = false; + let revokePending = false; if ( currentConfirmation?.type === TransactionType.tokenMethodSetApprovalForAll ) { @@ -162,6 +174,7 @@ const ConfirmTitle: React.FC = memo(() => { isRevokeSetApprovalForAll = getIsRevokeSetApprovalForAll( decodedResponse.value, ); + revokePending = decodedResponse.pending; } const title = useMemo( @@ -172,8 +185,16 @@ const ConfirmTitle: React.FC = memo(() => { isNFT, customSpendingCap, isRevokeSetApprovalForAll, + spendingCapPending || revokePending, ), - [currentConfirmation, isNFT, customSpendingCap, isRevokeSetApprovalForAll], + [ + currentConfirmation, + isNFT, + customSpendingCap, + isRevokeSetApprovalForAll, + spendingCapPending, + revokePending, + ], ); const description = useMemo( @@ -184,9 +205,16 @@ const ConfirmTitle: React.FC = memo(() => { isNFT, customSpendingCap, isRevokeSetApprovalForAll, + spendingCapPending || revokePending, ), - - [currentConfirmation, isNFT, customSpendingCap, isRevokeSetApprovalForAll], + [ + currentConfirmation, + isNFT, + customSpendingCap, + isRevokeSetApprovalForAll, + spendingCapPending, + revokePending, + ], ); if (!currentConfirmation) { @@ -196,21 +224,25 @@ const ConfirmTitle: React.FC = memo(() => { return ( <> - - {title} - - - {description} - + {title !== '' && ( + + {title} + + )} + {description !== '' && ( + + {description} + + )} ); }); diff --git a/ui/pages/confirmations/components/fee-details-component/fee-details-component.js b/ui/pages/confirmations/components/fee-details-component/fee-details-component.js index 19bcc25e445f..84ea244ec8cd 100644 --- a/ui/pages/confirmations/components/fee-details-component/fee-details-component.js +++ b/ui/pages/confirmations/components/fee-details-component/fee-details-component.js @@ -1,6 +1,6 @@ import React, { useCallback, useMemo, useState } from 'react'; -import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; import { AlignItems, Display, @@ -19,7 +19,7 @@ import { Text, } from '../../../../components/component-library'; import TransactionDetailItem from '../transaction-detail-item/transaction-detail-item.component'; -import { getPreferences, getShouldShowFiat } from '../../../../selectors'; +import { getShouldShowFiat } from '../../../../selectors'; import { useI18nContext } from '../../../../hooks/useI18nContext'; import LoadingHeartBeat from '../../../../components/ui/loading-heartbeat'; import UserPreferencedCurrencyDisplay from '../../../../components/app/user-preferenced-currency-display/user-preferenced-currency-display.component'; @@ -36,8 +36,6 @@ export default function FeeDetailsComponent({ const [expandFeeDetails, setExpandFeeDetails] = useState(false); const shouldShowFiat = useSelector(getShouldShowFiat); - const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences); - const t = useI18nContext(); const { minimumCostInHexWei: hexMinimumTransactionFee } = useGasFeeContext(); @@ -64,13 +62,13 @@ export default function FeeDetailsComponent({ color: TextColor.textAlternative, variant: TextVariant.bodySmBold, }} - hideLabel={Boolean(useNativeCurrencyAsPrimaryCurrency)} + hideLabel /> )}
); }, - [txData, useNativeCurrencyAsPrimaryCurrency], + [txData], ); const renderTotalDetailValue = useCallback( @@ -91,13 +89,12 @@ export default function FeeDetailsComponent({ color: TextColor.textAlternative, variant: TextVariant.bodySm, }} - hideLabel={!useNativeCurrencyAsPrimaryCurrency} /> )} ); }, - [txData, useNativeCurrencyAsPrimaryCurrency], + [txData], ); const hasLayer1GasFee = layer1GasFee !== null; diff --git a/ui/pages/confirmations/components/gas-details-item/gas-details-item.js b/ui/pages/confirmations/components/gas-details-item/gas-details-item.js index 90ccbb09ce23..c861a9dce9d9 100644 --- a/ui/pages/confirmations/components/gas-details-item/gas-details-item.js +++ b/ui/pages/confirmations/components/gas-details-item/gas-details-item.js @@ -17,7 +17,6 @@ import { import { PRIMARY, SECONDARY } from '../../../../helpers/constants/common'; import { PriorityLevels } from '../../../../../shared/constants/gas'; import { - getPreferences, getShouldShowFiat, getTxData, transactionFeeSelector, @@ -68,7 +67,6 @@ const GasDetailsItem = ({ supportsEIP1559, } = useGasFeeContext(); - const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences); const getTransactionFeeTotal = useMemo(() => { if (layer1GasFee) { return sumHexes(hexMinimumTransactionFee, layer1GasFee); @@ -148,7 +146,7 @@ const GasDetailsItem = ({ }} type={SECONDARY} value={getTransactionFeeTotal} - hideLabel={Boolean(useNativeCurrencyAsPrimaryCurrency)} + hideLabel // Label not required here as it will always display fiat value. /> )}
@@ -168,7 +166,7 @@ const GasDetailsItem = ({ }} type={PRIMARY} value={getTransactionFeeTotal || draftHexMinimumTransactionFee} - hideLabel={!useNativeCurrencyAsPrimaryCurrency} + // Label required here as it will always display crypto value />
} @@ -216,7 +214,6 @@ const GasDetailsItem = ({ value={ getMaxTransactionFeeTotal || draftHexMaximumTransactionFee } - hideLabel={!useNativeCurrencyAsPrimaryCurrency} />
diff --git a/ui/pages/confirmations/components/gas-details-item/gas-details-item.test.js b/ui/pages/confirmations/components/gas-details-item/gas-details-item.test.js index ca85dd9abae9..3e45b4c87722 100644 --- a/ui/pages/confirmations/components/gas-details-item/gas-details-item.test.js +++ b/ui/pages/confirmations/components/gas-details-item/gas-details-item.test.js @@ -35,9 +35,7 @@ const render = async ({ contextProps } = {}) => { balance: '0x1F4', }, }, - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, gasFeeEstimates: mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, gasFeeEstimatesByChainId: { diff --git a/ui/pages/confirmations/components/signature-request-header/signature-request-header.js b/ui/pages/confirmations/components/signature-request-header/signature-request-header.js index 30f839b3f78c..9c91ca476ebb 100644 --- a/ui/pages/confirmations/components/signature-request-header/signature-request-header.js +++ b/ui/pages/confirmations/components/signature-request-header/signature-request-header.js @@ -9,10 +9,8 @@ import { } from '../../../../ducks/metamask/metamask'; import { accountsWithSendEtherInfoSelector, - conversionRateSelector, getCurrentChainId, getCurrentCurrency, - getPreferences, } from '../../../../selectors'; import { formatCurrency } from '../../../../helpers/utils/confirm-tx.util'; import { @@ -38,11 +36,8 @@ const SignatureRequestHeader = ({ txData }) => { const providerConfig = useSelector(getProviderConfig); const networkName = getNetworkNameFromProviderType(providerConfig.type); - const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences); - const conversionRateFromSelector = useSelector(conversionRateSelector); - const conversionRate = useNativeCurrencyAsPrimaryCurrency - ? null - : conversionRateFromSelector; + + const conversionRate = null; // setting conversion rate to null by default to display balance in native const currentNetwork = networkName === '' diff --git a/ui/pages/confirmations/components/signature-request/signature-request.test.js b/ui/pages/confirmations/components/signature-request/signature-request.test.js index 0d50f906e5ca..9851cdbef454 100644 --- a/ui/pages/confirmations/components/signature-request/signature-request.test.js +++ b/ui/pages/confirmations/components/signature-request/signature-request.test.js @@ -45,9 +45,7 @@ const mockStore = { rpcUrl: 'http://localhost:8545', ticker: 'ETH', }), - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, accounts: { '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5': { address: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', diff --git a/ui/pages/confirmations/components/simulation-details/simulation-details.stories.tsx b/ui/pages/confirmations/components/simulation-details/simulation-details.stories.tsx index 15bfab8a2428..7c4fdc6e0d22 100644 --- a/ui/pages/confirmations/components/simulation-details/simulation-details.stories.tsx +++ b/ui/pages/confirmations/components/simulation-details/simulation-details.stories.tsx @@ -29,7 +29,7 @@ const storeMock = configureStore({ ...mockState.metamask, preferences: { ...mockState.metamask.preferences, - useNativeCurrencyAsPrimaryCurrency: false, + showNativeTokenAsMainBalance: false, }, ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), useTokenDetection: true, diff --git a/ui/pages/confirmations/components/simulation-details/useSimulationMetrics.ts b/ui/pages/confirmations/components/simulation-details/useSimulationMetrics.ts index 178561d0f5d6..b64a83394898 100644 --- a/ui/pages/confirmations/components/simulation-details/useSimulationMetrics.ts +++ b/ui/pages/confirmations/components/simulation-details/useSimulationMetrics.ts @@ -67,13 +67,14 @@ export function useSimulationMetrics({ setLoadingComplete(); } - const displayNameRequests: UseDisplayNameRequest[] = balanceChanges.map( - ({ asset }) => ({ - value: asset.address ?? '', + const displayNameRequests: UseDisplayNameRequest[] = balanceChanges + // Filter out changes with no address (e.g. ETH) + .filter(({ asset }) => Boolean(asset.address)) + .map(({ asset }) => ({ + value: asset.address as string, type: NameType.ETHEREUM_ADDRESS, preferContractSymbol: true, - }), - ); + })); const displayNames = useDisplayNames(displayNameRequests); @@ -145,7 +146,9 @@ export function useSimulationMetrics({ function useIncompleteAssetEvent( balanceChanges: BalanceChange[], - displayNamesByAddress: { [address: string]: UseDisplayNameResponse }, + displayNamesByAddress: { + [address: string]: UseDisplayNameResponse | undefined; + }, ) { const trackEvent = useContext(MetaMetricsContext); const [processedAssets, setProcessedAssets] = useState([]); @@ -170,7 +173,7 @@ function useIncompleteAssetEvent( properties: { asset_address: change.asset.address, asset_petname: getPetnameType(change, displayName), - asset_symbol: displayName.contractDisplayName, + asset_symbol: displayName?.contractDisplayName, asset_type: getAssetType(change.asset.standard), fiat_conversion_available: change.fiatAmount ? FiatType.Available @@ -244,7 +247,7 @@ function getAssetType(standard: TokenStandard) { function getPetnameType( balanceChange: BalanceChange, - displayName: UseDisplayNameResponse, + displayName: UseDisplayNameResponse = { name: '', hasPetname: false }, ) { if (balanceChange.asset.standard === TokenStandard.none) { return PetnameType.Default; diff --git a/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js b/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js index cbe80f86fe8b..ebd57c35a141 100644 --- a/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js +++ b/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js @@ -86,7 +86,6 @@ export default class ConfirmApproveContent extends Component { setUserAcknowledgedGasMissing: PropTypes.func, renderSimulationFailureWarning: PropTypes.bool, useCurrencyRateCheck: PropTypes.bool, - useNativeCurrencyAsPrimaryCurrency: PropTypes.bool, }; state = { @@ -159,7 +158,6 @@ export default class ConfirmApproveContent extends Component { userAcknowledgedGasMissing, renderSimulationFailureWarning, useCurrencyRateCheck, - useNativeCurrencyAsPrimaryCurrency, } = this.props; if ( !hasLayer1GasFee && @@ -183,7 +181,6 @@ export default class ConfirmApproveContent extends Component { } noBold diff --git a/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js b/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js index 2abec9ef4c13..ba1c7c5a568c 100644 --- a/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js +++ b/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js @@ -11,9 +11,7 @@ const renderComponent = (props) => { const store = configureMockStore([])({ metamask: { ...mockNetworkState({ chainId: '0x0' }), - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, currencyRates: {}, }, }); diff --git a/ui/pages/confirmations/confirm-approve/confirm-approve.js b/ui/pages/confirmations/confirm-approve/confirm-approve.js index 0828c236a38f..a5dcaeb6202d 100644 --- a/ui/pages/confirmations/confirm-approve/confirm-approve.js +++ b/ui/pages/confirmations/confirm-approve/confirm-approve.js @@ -27,7 +27,6 @@ import { getRpcPrefsForCurrentProvider, checkNetworkAndAccountSupports1559, getUseCurrencyRateCheck, - getPreferences, } from '../../../selectors'; import { useApproveTransaction } from '../hooks/useApproveTransaction'; import { useSimulationFailureWarning } from '../hooks/useSimulationFailureWarning'; @@ -84,7 +83,6 @@ export default function ConfirmApprove({ isAddressLedgerByFromAddress(userAddress), ); const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); - const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences); const [customPermissionAmount, setCustomPermissionAmount] = useState(''); const [submitWarning, setSubmitWarning] = useState(''); const [isContract, setIsContract] = useState(false); @@ -298,9 +296,6 @@ export default function ConfirmApprove({ hasLayer1GasFee={layer1GasFee !== undefined} supportsEIP1559={supportsEIP1559} useCurrencyRateCheck={useCurrencyRateCheck} - useNativeCurrencyAsPrimaryCurrency={ - useNativeCurrencyAsPrimaryCurrency - } /> {showCustomizeGasPopover && !supportsEIP1559 && (
0.000021 + + ETH +
@@ -431,13 +436,18 @@ exports[`Confirm Transaction Base should match snapshot 1`] = `
0.000021 + + ETH +
diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js index 31149997a39c..96fd5315e317 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js @@ -146,7 +146,6 @@ export default class ConfirmTransactionBase extends Component { secondaryTotalTextOverride: PropTypes.string, gasIsLoading: PropTypes.bool, primaryTotalTextOverrideMaxAmount: PropTypes.string, - useNativeCurrencyAsPrimaryCurrency: PropTypes.bool, maxFeePerGas: PropTypes.string, maxPriorityFeePerGas: PropTypes.string, baseFeePerGas: PropTypes.string, @@ -399,7 +398,6 @@ export default class ConfirmTransactionBase extends Component { nextNonce, getNextNonce, txData, - useNativeCurrencyAsPrimaryCurrency, primaryTotalTextOverrideMaxAmount, showLedgerSteps, nativeCurrency, @@ -459,7 +457,6 @@ export default class ConfirmTransactionBase extends Component { type={PRIMARY} key="total-max-amount" value={getTotalAmount(useMaxFee)} - hideLabel={!useNativeCurrencyAsPrimaryCurrency} /> ); } @@ -468,9 +465,8 @@ export default class ConfirmTransactionBase extends Component { const primaryTotal = useMaxFee ? primaryTotalTextOverrideMaxAmount : primaryTotalTextOverride; - const totalMaxAmount = useNativeCurrencyAsPrimaryCurrency - ? primaryTotal - : secondaryTotalTextOverride; + + const totalMaxAmount = primaryTotal; return isBoldTextAndNotOverridden ? ( {totalMaxAmount} @@ -500,14 +496,12 @@ export default class ConfirmTransactionBase extends Component { color: TextColor.textDefault, variant: TextVariant.bodyMdBold, }} - hideLabel={Boolean(useNativeCurrencyAsPrimaryCurrency)} + hideLabel />
); } - return useNativeCurrencyAsPrimaryCurrency - ? secondaryTotalTextOverride - : primaryTotalTextOverride; + return secondaryTotalTextOverride; }; const nextNonceValue = diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js index ed012d07e5ce..5d92a1af9c56 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js @@ -45,7 +45,6 @@ import { getIsEthGasPriceFetched, getShouldShowFiat, checkNetworkAndAccountSupports1559, - getPreferences, doesAddressRequireLedgerHidConnection, getTokenList, getEnsResolutionByAddress, @@ -266,7 +265,6 @@ const mapStateToProps = (state, ownProps) => { customNonceValue = getCustomNonceValue(state); const isEthGasPriceFetched = getIsEthGasPriceFetched(state); const noGasPrice = !supportsEIP1559 && getNoGasPriceFetched(state); - const { useNativeCurrencyAsPrimaryCurrency } = getPreferences(state); const gasFeeIsCustom = fullTxData.userFeeLevel === CUSTOM_GAS_ESTIMATE || txParamsAreDappSuggested(fullTxData); @@ -347,7 +345,6 @@ const mapStateToProps = (state, ownProps) => { noGasPrice, supportsEIP1559, gasIsLoading: isGasEstimatesLoading || gasLoadingAnimationIsShowing, - useNativeCurrencyAsPrimaryCurrency, maxFeePerGas: gasEstimationObject.maxFeePerGas, maxPriorityFeePerGas: gasEstimationObject.maxPriorityFeePerGas, baseFeePerGas: gasEstimationObject.baseFeePerGas, diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js index f8a7b40430fb..bea6aef1d84d 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js @@ -108,9 +108,7 @@ const baseStore = { chainId: CHAIN_IDS.GOERLI, }), tokens: [], - preferences: { - useNativeCurrencyAsPrimaryCurrency: false, - }, + preferences: {}, currentCurrency: 'USD', currencyRates: {}, featureFlags: { diff --git a/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap b/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap index 30a1b6ad118c..1d27b332f7a5 100644 --- a/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap +++ b/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap @@ -24,15 +24,48 @@ exports[`Confirm matches snapshot for signature - personal sign type 1`] = `
+ > +
+ + + + + +
+
- Goerli logo + G

Signature request

@@ -105,7 +138,7 @@ exports[`Confirm matches snapshot for signature - personal sign type 1`] = ` class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >

Spending cap request

@@ -340,7 +373,7 @@ exports[`Confirm should match snapshot for signature - typed sign - V4 - PermitB data-testid="confirmation__simulation_section" >
-
+
@@ -486,7 +521,9 @@ exports[`Confirm should match snapshot for signature - typed sign - V4 - PermitB
-
+
@@ -514,7 +551,7 @@ exports[`Confirm should match snapshot for signature - typed sign - V4 - PermitB class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >

Spending cap request

@@ -1446,7 +1483,7 @@ exports[`Confirm should match snapshot for signature - typed sign - V4 - PermitS data-testid="confirmation__simulation_section" >
-
+
@@ -1570,7 +1609,7 @@ exports[`Confirm should match snapshot for signature - typed sign - V4 - PermitS class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >
+ > +
+ + + + + +
+
- Goerli logo + G

Signature request

@@ -2254,7 +2326,7 @@ exports[`Confirm should match snapshot for signature - typed sign - V4 1`] = ` class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >

Spending cap request

@@ -2987,7 +3311,7 @@ exports[`Confirm should match snapshot for signature - typed sign - permit 1`] = data-testid="confirmation__simulation_section" >
-
+
@@ -3107,7 +3433,7 @@ exports[`Confirm should match snapshot for signature - typed sign - permit 1`] = class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >

Signature request

@@ -3772,7 +4098,7 @@ exports[`Confirm should match snapshot signature - typed sign - order 1`] = ` class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >
{ const mockStatePersonalSign = getMockPersonalSignConfirmState(); const mockStore = configureMockStore(middleware)(mockStatePersonalSign); + let container; await act(async () => { - const { container } = await renderWithConfirmContextProvider( - , - mockStore, - ); - expect(container).toMatchSnapshot(); + const { container: renderContainer } = + await renderWithConfirmContextProvider(, mockStore); + + container = renderContainer; }); + + expect(container).toMatchSnapshot(); }); it('should match snapshot signature - typed sign - order', async () => { @@ -106,8 +107,8 @@ describe('Confirm', () => { }); const mockStore = configureMockStore(middleware)(mockStateTypedSign); - let container; + let container; await act(async () => { const { container: renderContainer } = renderWithConfirmContextProvider( , @@ -123,13 +124,15 @@ describe('Confirm', () => { const mockStateTypedSign = getMockTypedSignConfirmState(); const mockStore = configureMockStore(middleware)(mockStateTypedSign); + let container; await act(async () => { - const { container } = await renderWithConfirmContextProvider( - , - mockStore, - ); - expect(container).toMatchSnapshot(); + const { container: renderContainer } = + await renderWithConfirmContextProvider(, mockStore); + + container = renderContainer; }); + + expect(container).toMatchSnapshot(); }); it('should match snapshot for signature - typed sign - V4 - PermitSingle', async () => { diff --git a/ui/pages/confirmations/confirm/stories/utils.tsx b/ui/pages/confirmations/confirm/stories/utils.tsx index dd194d574109..9c68a392cbd7 100644 --- a/ui/pages/confirmations/confirm/stories/utils.tsx +++ b/ui/pages/confirmations/confirm/stories/utils.tsx @@ -46,7 +46,7 @@ export function ConfirmStoryTemplate( }`, ]} > - } /> + } /> ); diff --git a/ui/pages/confirmations/hooks/test-utils.js b/ui/pages/confirmations/hooks/test-utils.js index 8af6bcf3ba40..908f600564f8 100644 --- a/ui/pages/confirmations/hooks/test-utils.js +++ b/ui/pages/confirmations/hooks/test-utils.js @@ -10,10 +10,10 @@ import { import { getCurrentCurrency, getShouldShowFiat, - getPreferences, txDataSelector, getCurrentKeyring, getTokenExchangeRates, + getPreferences, } from '../../../selectors'; import { @@ -118,7 +118,7 @@ export const generateUseSelectorRouter = } if (selector === getPreferences) { return { - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, }; } if ( diff --git a/ui/pages/confirmations/send/gas-display/gas-display.js b/ui/pages/confirmations/send/gas-display/gas-display.js index 5fbad8445cd6..33a011c2966a 100644 --- a/ui/pages/confirmations/send/gas-display/gas-display.js +++ b/ui/pages/confirmations/send/gas-display/gas-display.js @@ -48,6 +48,7 @@ import { MetaMetricsContext } from '../../../../contexts/metametrics'; import useRamps from '../../../../hooks/ramps/useRamps/useRamps'; import { getIsNativeTokenBuyable } from '../../../../ducks/ramps'; +// This function is no longer used in codebase, to be deleted. export default function GasDisplay({ gasError }) { const t = useContext(I18nContext); const dispatch = useDispatch(); @@ -61,8 +62,7 @@ export default function GasDisplay({ gasError }) { const isBuyableChain = useSelector(getIsNativeTokenBuyable); const draftTransaction = useSelector(getCurrentDraftTransaction); const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); - const { showFiatInTestnets, useNativeCurrencyAsPrimaryCurrency } = - useSelector(getPreferences); + const { showFiatInTestnets } = useSelector(getPreferences); const unapprovedTxs = useSelector(getUnapprovedTransactions); const nativeCurrency = useSelector(getNativeCurrency); const { chainId } = providerConfig; @@ -132,7 +132,6 @@ export default function GasDisplay({ gasError }) { type={PRIMARY} key="total-detail-value" value={hexTransactionTotal} - hideLabel={!useNativeCurrencyAsPrimaryCurrency} /> ); @@ -144,10 +143,9 @@ export default function GasDisplay({ gasError }) { draftTransaction.amount.value, hexMaximumTransactionFee, )} - hideLabel={!useNativeCurrencyAsPrimaryCurrency} /> ); - } else if (useNativeCurrencyAsPrimaryCurrency) { + } else { detailTotal = primaryTotalTextOverrideMaxAmount; maxAmount = primaryTotalTextOverrideMaxAmount; } @@ -177,7 +175,7 @@ export default function GasDisplay({ gasError }) { type={SECONDARY} key="total-detail-text" value={hexTransactionTotal} - hideLabel={Boolean(useNativeCurrencyAsPrimaryCurrency)} + hideLabel /> ) diff --git a/ui/pages/home/index.scss b/ui/pages/home/index.scss index 03b4cd5d7cf9..5a85a3eb5d3c 100644 --- a/ui/pages/home/index.scss +++ b/ui/pages/home/index.scss @@ -20,6 +20,7 @@ min-width: 0; display: flex; flex-direction: column; + padding-top: 8px; } &__connect-status-text { diff --git a/ui/pages/index.js b/ui/pages/index.js index 208436c9127a..0b1cdcef78cd 100644 --- a/ui/pages/index.js +++ b/ui/pages/index.js @@ -2,6 +2,7 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { Provider } from 'react-redux'; import { HashRouter } from 'react-router-dom'; +import { CompatRouter } from 'react-router-dom-v5-compat'; import * as Sentry from '@sentry/browser'; import { I18nProvider, LegacyI18nProvider } from '../contexts/i18n'; import { @@ -43,19 +44,21 @@ class Index extends PureComponent { return ( - - - - - - - - - - - - - + + + + + + + + + + + + + + + ); diff --git a/ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.test.tsx b/ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.test.tsx index cdefb3986d1f..d7a474ad3b24 100644 --- a/ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.test.tsx +++ b/ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.test.tsx @@ -17,9 +17,7 @@ jest.mock('../../../store/institutional/institution-background', () => ({ describe('Confirm Add Custodian Token', () => { const mockStore = { metamask: { - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, institutionalFeatures: { connectRequests: [ { @@ -50,9 +48,7 @@ describe('Confirm Add Custodian Token', () => { it('tries to connect to custodian with empty token', async () => { const customMockedStore = { metamask: { - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, institutionalFeatures: { connectRequests: [ { diff --git a/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.test.tsx b/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.test.tsx index 5719fb38015f..5044d6085812 100644 --- a/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.test.tsx +++ b/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.test.tsx @@ -9,9 +9,7 @@ describe('Confirm Add Custodian Token', () => { const mockStore = { metamask: { - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, }, history: { push: '/', diff --git a/ui/pages/institutional/custody/custody.test.tsx b/ui/pages/institutional/custody/custody.test.tsx index 383e615492da..577e599397ba 100644 --- a/ui/pages/institutional/custody/custody.test.tsx +++ b/ui/pages/institutional/custody/custody.test.tsx @@ -99,9 +99,7 @@ describe('CustodyPage', function () { }, ], }, - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, appState: { isLoading: false, }, diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index 82361cb6b690..1fdbad27ed67 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -38,6 +38,7 @@ import { ToastContainer, Toast, } from '../../components/multichain'; +import { SurveyToast } from '../../components/ui/survey-toast'; import UnlockPage from '../unlock-page'; import Alerts from '../../components/app/alerts'; import Asset from '../asset'; @@ -676,6 +677,7 @@ export default class Routes extends Component { return ( + {showConnectAccountToast && !this.state.hideConnectAccountToast && isEvmAccount ? ( diff --git a/ui/pages/settings/advanced-tab/__snapshots__/advanced-tab.component.test.js.snap b/ui/pages/settings/advanced-tab/__snapshots__/advanced-tab.component.test.js.snap index fcc11ec8336b..6318abd37570 100644 --- a/ui/pages/settings/advanced-tab/__snapshots__/advanced-tab.component.test.js.snap +++ b/ui/pages/settings/advanced-tab/__snapshots__/advanced-tab.component.test.js.snap @@ -501,7 +501,7 @@ exports[`AdvancedTab Component should match snapshot 1`] = ` class="MuiFormControl-root MuiTextField-root MuiFormControl-marginDense MuiFormControl-fullWidth" >
}); } + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) renderWatchAccountToggle() { const { t, trackEvent } = this.context; const { watchAccountEnabled, setWatchAccountEnabled } = this.props; @@ -297,7 +298,6 @@ export default class ExperimentalTab extends PureComponent }); } - ///: BEGIN:ONLY_INCLUDE_IF(build-flask) // We're only setting the code fences here since // we should remove it for the feature release renderBitcoinSupport() { @@ -385,12 +385,15 @@ export default class ExperimentalTab extends PureComponent this.renderKeyringSnapsToggle() ///: END:ONLY_INCLUDE_IF } - {this.renderWatchAccountToggle()} + { + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) + this.renderWatchAccountToggle() + ///: END:ONLY_INCLUDE_IF + } { ///: BEGIN:ONLY_INCLUDE_IF(build-flask) // We're only setting the code fences here since // we should remove it for the feature release - /* Section: Bitcoin Accounts */ this.renderBitcoinSupport() ///: END:ONLY_INCLUDE_IF diff --git a/ui/pages/settings/index.scss b/ui/pages/settings/index.scss index 48e12e8adebc..b4cea4bef630 100644 --- a/ui/pages/settings/index.scss +++ b/ui/pages/settings/index.scss @@ -263,11 +263,11 @@ } &__body { - padding: 24px; + padding: 0 16px 16px 16px; } &__content-row { - padding: 10px 0 20px; + padding: 16px 0 0; @include design-system.screen-sm-max { flex-wrap: wrap; @@ -296,6 +296,12 @@ margin-top: 10px; } + &__title { + font-size: 14px; + font-weight: 500; + line-height: 22px; + } + &__identicon { display: flex; flex-direction: row; @@ -326,11 +332,7 @@ &__description { @include design-system.H6; - margin-top: 8px; - margin-bottom: 12px; - color: var(--color-text-default); - font-size: 14px; - font-weight: 400; + line-height: 22px; } } diff --git a/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap b/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap index a57775ed145b..343a7f05ecb4 100644 --- a/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap +++ b/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap @@ -1020,7 +1020,7 @@ exports[`Security Tab should match snapshot 1`] = ` class="MuiFormControl-root MuiTextField-root MuiFormControl-marginDense MuiFormControl-fullWidth" >
+
+
+ + Delete MetaMetrics data + +
+ + + This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our + + Privacy policy + + . + + +
+
+
+
+ +

+ Since you've never opted into MetaMetrics, there's no data to delete here. +

+
+ +
+
diff --git a/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.test.tsx b/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.test.tsx new file mode 100644 index 000000000000..27132fb82f5c --- /dev/null +++ b/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.test.tsx @@ -0,0 +1,212 @@ +import * as React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { fireEvent } from '@testing-library/react'; +import configureStore from '../../../../store/store'; +import { renderWithProvider } from '../../../../../test/lib/render-helpers'; + +import { + getMetaMetricsDataDeletionTimestamp, + getMetaMetricsDataDeletionStatus, + getMetaMetricsId, + getLatestMetricsEventTimestamp, +} from '../../../../selectors'; +import { openDeleteMetaMetricsDataModal } from '../../../../ducks/app/app'; +import DeleteMetaMetricsDataButton from './delete-metametrics-data-button'; + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: jest.fn(), + useDispatch: jest.fn(), +})); + +describe('DeleteMetaMetricsDataButton', () => { + const useSelectorMock = useSelector as jest.Mock; + const useDispatchMock = useDispatch as jest.Mock; + const mockDispatch = jest.fn(); + + beforeEach(() => { + useDispatchMock.mockReturnValue(mockDispatch); + useSelectorMock.mockImplementation((selector) => { + if (selector === getMetaMetricsId) { + return 'fake-metrics-id'; + } + if (selector === getMetaMetricsDataDeletionStatus) { + return undefined; + } + if (selector === getMetaMetricsDataDeletionTimestamp) { + return ''; + } + + return undefined; + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly', () => { + const store = configureStore({}); + const { getByTestId, getAllByText, container } = renderWithProvider( + , + store, + ); + expect(getByTestId('delete-metametrics-data-button')).toBeInTheDocument(); + expect(getAllByText('Delete MetaMetrics data')).toHaveLength(2); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our Privacy policy. "`, + ); + }); + it('should enable the data deletion button when metrics is opted in and metametrics id is available ', async () => { + const store = configureStore({}); + const { getByRole, container } = renderWithProvider( + , + store, + ); + expect( + getByRole('button', { name: 'Delete MetaMetrics data' }), + ).toBeEnabled(); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our Privacy policy. "`, + ); + }); + it('should enable the data deletion button when page mounts after a deletion task is performed and more data is recoded after the deletion', async () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getMetaMetricsDataDeletionStatus) { + return 'INITIALIZED'; + } + if (selector === getMetaMetricsId) { + return 'fake-metrics-id'; + } + return undefined; + }); + const store = configureStore({}); + const { getByRole, container } = renderWithProvider( + , + store, + ); + expect( + getByRole('button', { name: 'Delete MetaMetrics data' }), + ).toBeEnabled(); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our Privacy policy. "`, + ); + }); + + // if user does not opt in to participate in metrics or for profile sync, metametricsId will not be created. + it('should disable the data deletion button when there is metametrics id not available', async () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getMetaMetricsId) { + return null; + } + return undefined; + }); + const store = configureStore({}); + const { getByRole, container } = renderWithProvider( + , + store, + ); + expect( + getByRole('button', { name: 'Delete MetaMetrics data' }), + ).toBeDisabled(); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our Privacy policy. "`, + ); + expect( + container.querySelector('.settings-page__content-item-col')?.textContent, + ).toMatchInlineSnapshot( + `"Since you've never opted into MetaMetrics, there's no data to delete here.Delete MetaMetrics data"`, + ); + }); + + // particilapteInMetrics will be false before the deletion is performed, this way no further data will be recorded after deletion. + it('should disable the data deletion button after a deletion task is performed and no data is recoded after the deletion', async () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getMetaMetricsId) { + return 'fake-metrics-id'; + } + if (selector === getMetaMetricsDataDeletionStatus) { + return 'INITIALIZED'; + } + if (selector === getMetaMetricsDataDeletionTimestamp) { + return 1717779342113; + } + if (selector === getLatestMetricsEventTimestamp) { + return 1717779342110; + } + return undefined; + }); + const store = configureStore({}); + const { getByRole, container } = renderWithProvider( + , + store, + ); + expect( + getByRole('button', { name: 'Delete MetaMetrics data' }), + ).toBeDisabled(); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" You initiated this action on 7/06/2024. This process can take up to 30 days. View the Privacy policy "`, + ); + }); + + // particilapteInMetrics will be false before the deletion is performed, this way no further data will be recorded after deletion. + it('should disable the data deletion button after a deletion task is performed and no data is recoded after the deletion', async () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getMetaMetricsId) { + return 'fake-metrics-id'; + } + if (selector === getMetaMetricsDataDeletionStatus) { + return 'INITIALIZED'; + } + if (selector === getMetaMetricsDataDeletionTimestamp) { + return 1717779342113; + } + if (selector === getLatestMetricsEventTimestamp) { + return 1717779342110; + } + return undefined; + }); + const store = configureStore({}); + const { getByRole, container } = renderWithProvider( + , + store, + ); + expect( + getByRole('button', { name: 'Delete MetaMetrics data' }), + ).toBeDisabled(); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" You initiated this action on 7/06/2024. This process can take up to 30 days. View the Privacy policy "`, + ); + }); + + it('should open the modal on the button click', () => { + const store = configureStore({}); + const { getByRole } = renderWithProvider( + , + store, + ); + const deleteButton = getByRole('button', { + name: 'Delete MetaMetrics data', + }); + fireEvent.click(deleteButton); + expect(mockDispatch).toHaveBeenCalledWith(openDeleteMetaMetricsDataModal()); + }); +}); diff --git a/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.tsx b/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.tsx new file mode 100644 index 000000000000..34b61697ed95 --- /dev/null +++ b/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.tsx @@ -0,0 +1,147 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { CONSENSYS_PRIVACY_LINK } from '../../../../../shared/lib/ui-utils'; +import ClearMetametricsData from '../../../../components/app/clear-metametrics-data'; +import { + Box, + ButtonPrimary, + Icon, + IconName, + IconSize, + PolymorphicComponentPropWithRef, + PolymorphicRef, + Text, +} from '../../../../components/component-library'; +import { + Display, + FlexDirection, + TextVariant, +} from '../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { + getMetaMetricsDataDeletionTimestamp, + getMetaMetricsDataDeletionStatus, + getMetaMetricsId, + getShowDataDeletionErrorModal, + getShowDeleteMetaMetricsDataModal, + getLatestMetricsEventTimestamp, +} from '../../../../selectors'; +import { openDeleteMetaMetricsDataModal } from '../../../../ducks/app/app'; +import DataDeletionErrorModal from '../../../../components/app/data-deletion-error-modal'; +import { formatDate } from '../../../../helpers/utils/util'; +import { DeleteRegulationStatus } from '../../../../../shared/constants/metametrics'; + +type DeleteMetaMetricsDataButtonProps = + PolymorphicComponentPropWithRef; + +type DeleteMetaMetricsDataButtonComponent = < + C extends React.ElementType = 'div', +>( + props: DeleteMetaMetricsDataButtonProps, +) => React.ReactElement | null; + +const DeleteMetaMetricsDataButton: DeleteMetaMetricsDataButtonComponent = + React.forwardRef( + ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { ...props }: DeleteMetaMetricsDataButtonProps, + ref: PolymorphicRef, + ) => { + const t = useI18nContext(); + const dispatch = useDispatch(); + + const metaMetricsId = useSelector(getMetaMetricsId); + const metaMetricsDataDeletionStatus: DeleteRegulationStatus = useSelector( + getMetaMetricsDataDeletionStatus, + ); + const metaMetricsDataDeletionTimestamp = useSelector( + getMetaMetricsDataDeletionTimestamp, + ); + const formatedDate = formatDate( + metaMetricsDataDeletionTimestamp, + 'd/MM/y', + ); + + const showDeleteMetaMetricsDataModal = useSelector( + getShowDeleteMetaMetricsDataModal, + ); + const showDataDeletionErrorModal = useSelector( + getShowDataDeletionErrorModal, + ); + const latestMetricsEventTimestamp = useSelector( + getLatestMetricsEventTimestamp, + ); + + let dataDeletionButtonDisabled = Boolean(!metaMetricsId); + if (!dataDeletionButtonDisabled && metaMetricsDataDeletionStatus) { + dataDeletionButtonDisabled = + [ + DeleteRegulationStatus.Initialized, + DeleteRegulationStatus.Running, + DeleteRegulationStatus.Finished, + ].includes(metaMetricsDataDeletionStatus) && + metaMetricsDataDeletionTimestamp > latestMetricsEventTimestamp; + } + const privacyPolicyLink = ( + + {t('privacyMsg')} + + ); + return ( + <> + +
+ {t('deleteMetaMetricsData')} +
+ {dataDeletionButtonDisabled && Boolean(metaMetricsId) + ? t('deleteMetaMetricsDataRequestedDescription', [ + formatedDate, + privacyPolicyLink, + ]) + : t('deleteMetaMetricsDataDescription', [privacyPolicyLink])} +
+
+
+ {Boolean(!metaMetricsId) && ( + + + + {t('metaMetricsIdNotAvailableError')} + + + )} + { + dispatch(openDeleteMetaMetricsDataModal()); + }} + disabled={dataDeletionButtonDisabled} + > + {t('deleteMetaMetricsData')} + +
+
+ {showDeleteMetaMetricsDataModal && } + {showDataDeletionErrorModal && } + + ); + }, + ); + +export default DeleteMetaMetricsDataButton; diff --git a/ui/pages/settings/security-tab/delete-metametrics-data-button/index.ts b/ui/pages/settings/security-tab/delete-metametrics-data-button/index.ts new file mode 100644 index 000000000000..945f4d349ede --- /dev/null +++ b/ui/pages/settings/security-tab/delete-metametrics-data-button/index.ts @@ -0,0 +1 @@ +export { default } from './delete-metametrics-data-button'; diff --git a/ui/pages/settings/security-tab/security-tab.component.js b/ui/pages/settings/security-tab/security-tab.component.js index f6da9fe2367f..1fae729d3f31 100644 --- a/ui/pages/settings/security-tab/security-tab.component.js +++ b/ui/pages/settings/security-tab/security-tab.component.js @@ -52,8 +52,10 @@ import { } from '../../../helpers/utils/settings-search'; import IncomingTransactionToggle from '../../../components/app/incoming-trasaction-toggle/incoming-transaction-toggle'; -import ProfileSyncToggle from './profile-sync-toggle'; +import { updateDataDeletionTaskStatus } from '../../../store/actions'; import MetametricsToggle from './metametrics-toggle'; +import ProfileSyncToggle from './profile-sync-toggle'; +import DeleteMetametricsDataButton from './delete-metametrics-data-button'; export default class SecurityTab extends PureComponent { static contextTypes = { @@ -102,6 +104,7 @@ export default class SecurityTab extends PureComponent { useExternalServices: PropTypes.bool, toggleExternalServices: PropTypes.func.isRequired, setSecurityAlertsEnabled: PropTypes.func, + metaMetricsDataDeletionId: PropTypes.string, }; state = { @@ -138,9 +141,12 @@ export default class SecurityTab extends PureComponent { } } - componentDidMount() { + async componentDidMount() { const { t } = this.context; handleSettingsRefs(t, t('securityAndPrivacy'), this.settingsRefs); + if (this.props.metaMetricsDataDeletionId) { + await updateDataDeletionTaskStatus(); + } } toggleSetting(value, eventName, eventAction, toggleMethod) { @@ -961,7 +967,7 @@ export default class SecurityTab extends PureComponent { return ( {this.renderDataCollectionForMarketing()} +
); diff --git a/ui/pages/settings/security-tab/security-tab.container.js b/ui/pages/settings/security-tab/security-tab.container.js index 747e3738fe3f..224072ef2b10 100644 --- a/ui/pages/settings/security-tab/security-tab.container.js +++ b/ui/pages/settings/security-tab/security-tab.container.js @@ -20,10 +20,12 @@ import { setUseExternalNameSources, setUseTransactionSimulations, setSecurityAlertsEnabled, + updateDataDeletionTaskStatus, } from '../../../store/actions'; import { getIsSecurityAlertsEnabled, getNetworkConfigurationsByChainId, + getMetaMetricsDataDeletionId, getPetnamesEnabled, } from '../../../selectors'; import { openBasicFunctionalityModal } from '../../../ducks/app/app'; @@ -78,6 +80,7 @@ const mapStateToProps = (state) => { petnamesEnabled, securityAlertsEnabled: getIsSecurityAlertsEnabled(state), useTransactionSimulations: metamask.useTransactionSimulations, + metaMetricsDataDeletionId: getMetaMetricsDataDeletionId(state), }; }; @@ -116,6 +119,9 @@ const mapDispatchToProps = (dispatch) => { setUseTransactionSimulations: (value) => { return dispatch(setUseTransactionSimulations(value)); }, + updateDataDeletionTaskStatus: () => { + return updateDataDeletionTaskStatus(); + }, setSecurityAlertsEnabled: (value) => setSecurityAlertsEnabled(value), }; }; diff --git a/ui/pages/settings/security-tab/security-tab.test.js b/ui/pages/settings/security-tab/security-tab.test.js index 905fec684fd5..5e31cfb68c57 100644 --- a/ui/pages/settings/security-tab/security-tab.test.js +++ b/ui/pages/settings/security-tab/security-tab.test.js @@ -13,6 +13,8 @@ import { renderWithProvider } from '../../../../test/lib/render-helpers'; import { getIsSecurityAlertsEnabled } from '../../../selectors'; import SecurityTab from './security-tab.container'; +const mockOpenDeleteMetaMetricsDataModal = jest.fn(); + const mockSetSecurityAlertsEnabled = jest .fn() .mockImplementation(() => () => undefined); @@ -36,6 +38,14 @@ jest.mock('../../../store/actions', () => ({ setSecurityAlertsEnabled: (val) => mockSetSecurityAlertsEnabled(val), })); +jest.mock('../../../ducks/app/app.ts', () => { + return { + openDeleteMetaMetricsDataModal: () => { + return mockOpenDeleteMetaMetricsDataModal; + }, + }; +}); + describe('Security Tab', () => { mockState.appState.warning = 'warning'; // This tests an otherwise untested render branch @@ -214,7 +224,23 @@ describe('Security Tab', () => { await user.click(screen.getByText(tEn('addCustomNetwork'))); expect(global.platform.openExtensionInBrowser).toHaveBeenCalled(); }); + it('clicks "Delete MetaMetrics Data"', async () => { + mockState.metamask.participateInMetaMetrics = true; + mockState.metamask.metaMetricsId = 'fake-metametrics-id'; + const localMockStore = configureMockStore([thunk])(mockState); + renderWithProvider(, localMockStore); + + expect( + screen.queryByTestId(`delete-metametrics-data-button`), + ).toBeInTheDocument(); + + fireEvent.click( + screen.getByRole('button', { name: 'Delete MetaMetrics data' }), + ); + + expect(mockOpenDeleteMetaMetricsDataModal).toHaveBeenCalled(); + }); describe('Blockaid', () => { afterEach(() => { jest.clearAllMocks(); diff --git a/ui/pages/settings/settings-search/settings-search.js b/ui/pages/settings/settings-search/settings-search.js index 8703d4a96a2c..b105e09da698 100644 --- a/ui/pages/settings/settings-search/settings-search.js +++ b/ui/pages/settings/settings-search/settings-search.js @@ -4,9 +4,12 @@ import Fuse from 'fuse.js'; import InputAdornment from '@material-ui/core/InputAdornment'; import TextField from '../../../components/ui/text-field'; import { I18nContext } from '../../../contexts/i18n'; -import SearchIcon from '../../../components/ui/icon/search-icon'; import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils'; -import { Icon, IconName } from '../../../components/component-library'; +import { + Icon, + IconName, + IconSize, +} from '../../../components/component-library'; import { IconColor } from '../../../helpers/constants/design-system'; export default function SettingsSearch({ @@ -17,9 +20,8 @@ export default function SettingsSearch({ const t = useContext(I18nContext); const [searchQuery, setSearchQuery] = useState(''); - const [searchIconColor, setSearchIconColor] = useState( - 'var(--color-icon-muted)', - ); + + const [searchIconColor, setSearchIconColor] = useState(IconColor.iconMuted); const settingsRoutesListArray = Object.values(settingsRoutesList); const settingsSearchFuse = new Fuse(settingsRoutesListArray, { @@ -37,9 +39,9 @@ export default function SettingsSearch({ const sanitizedSearchQuery = _searchQuery.trimStart(); setSearchQuery(sanitizedSearchQuery); if (sanitizedSearchQuery === '') { - setSearchIconColor('var(--color-icon-muted)'); + setSearchIconColor(IconColor.iconMuted); } else { - setSearchIconColor('var(--color-icon-default)'); + setSearchIconColor(IconColor.iconDefault); } const fuseSearchResult = settingsSearchFuse.search(sanitizedSearchQuery); @@ -58,7 +60,11 @@ export default function SettingsSearch({ const renderStartAdornment = () => { return ( - + ); }; @@ -73,7 +79,11 @@ export default function SettingsSearch({ onClick={() => handleSearch('')} style={{ cursor: 'pointer' }} > - + )} @@ -93,6 +103,7 @@ export default function SettingsSearch({ autoComplete="off" startAdornment={renderStartAdornment()} endAdornment={renderEndAdornment()} + theme="bordered" /> ); } diff --git a/ui/pages/settings/settings-tab/settings-tab.component.js b/ui/pages/settings/settings-tab/settings-tab.component.js index b998cde80515..191bbbc78685 100644 --- a/ui/pages/settings/settings-tab/settings-tab.component.js +++ b/ui/pages/settings/settings-tab/settings-tab.component.js @@ -62,12 +62,10 @@ export default class SettingsTab extends PureComponent { currentLocale: PropTypes.string, useBlockie: PropTypes.bool, currentCurrency: PropTypes.string, - nativeCurrency: PropTypes.string, - useNativeCurrencyAsPrimaryCurrency: PropTypes.bool, - setUseNativeCurrencyAsPrimaryCurrencyPreference: PropTypes.func, + showNativeTokenAsMainBalance: PropTypes.bool, + setShowNativeTokenAsMainBalancePreference: PropTypes.func, hideZeroBalanceTokens: PropTypes.bool, setHideZeroBalanceTokens: PropTypes.func, - lastFetchedConversionDate: PropTypes.number, selectedAddress: PropTypes.string, tokenList: PropTypes.object, theme: PropTypes.string, @@ -94,8 +92,7 @@ export default class SettingsTab extends PureComponent { renderCurrentConversion() { const { t } = this.context; - const { currentCurrency, setCurrentCurrency, lastFetchedConversionDate } = - this.props; + const { currentCurrency, setCurrentCurrency } = this.props; return (
- {t('currencyConversion')} - - {lastFetchedConversionDate - ? t('updatedWithDate', [ - new Date(lastFetchedConversionDate * 1000).toString(), - ]) - : t('noConversionDateAvailable')} - + + {t('currencyConversion')} +
@@ -131,6 +127,7 @@ export default class SettingsTab extends PureComponent { }, }); }} + className="settings-page__content-item__dropdown" />
@@ -141,10 +138,6 @@ export default class SettingsTab extends PureComponent { renderCurrentLocale() { const { t } = this.context; const { updateCurrentLocale, currentLocale } = this.props; - const currentLocaleMeta = locales.find( - (locale) => locale.code === currentLocale, - ); - const currentLocaleName = currentLocaleMeta ? currentLocaleMeta.name : ''; return (
- + {t('currentLanguage')} - - - {currentLocaleName} - +
@@ -191,15 +185,20 @@ export default class SettingsTab extends PureComponent { id="toggle-zero-balance" >
- {t('hideZeroBalanceTokens')} + + {t('hideZeroBalanceTokens')} +
setHideZeroBalanceTokens(!value)} - offLabel={t('off')} - onLabel={t('on')} + data-testid="toggle-zero-balance-button" />
@@ -229,14 +228,19 @@ export default class SettingsTab extends PureComponent {
{t('accountIdenticon')} - + {t('jazzAndBlockies')} - +
-
-
-
-`; - -exports[`SmartTransactionStatusPage renders the "cancelled" STX status 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction was canceled -

-
-

- Your transaction couldn't be completed, so it was canceled to save you from paying unnecessary gas fees. -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "cancelled" STX status for a dapp transaction 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction was canceled -

-
-

- Your transaction couldn't be completed, so it was canceled to save you from paying unnecessary gas fees. -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "deadline_missed" STX status 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction was canceled -

-
-

- Your transaction couldn't be completed, so it was canceled to save you from paying unnecessary gas fees. -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "pending" STX status for a dapp transaction 1`] = ` -
-
-
-
-
-
- -
-

- Submitting your transaction -

-
-
-
-
-
-
-

- - - Estimated completion in < -

- 0:45 -

- - - -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "reverted" STX status 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction failed -

-
-

- Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support. -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "success" STX status 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction is complete -

-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "success" STX status for a dapp transaction 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction is complete -

-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "unknown" STX status 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction failed -

-
-

- Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support. -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the component with initial props 1`] = ` -
-
-
-
-
-
- -
-

- Submitting your transaction -

-
-
-
-
-
-
-

- - - Estimated completion in < -

- 0:45 -

- - - -

-
-
-
-
- -
-
-`; diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/__snapshots__/smart-transactions-status-page.test.tsx.snap b/ui/pages/smart-transactions/smart-transaction-status-page/__snapshots__/smart-transactions-status-page.test.tsx.snap new file mode 100644 index 000000000000..f3ff42c89116 --- /dev/null +++ b/ui/pages/smart-transactions/smart-transaction-status-page/__snapshots__/smart-transactions-status-page.test.tsx.snap @@ -0,0 +1,151 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SmartTransactionStatusPage renders the "failed" STX status: smart-transaction-status-failed 1`] = ` +
+
+
+
+
+

+ Your transaction failed +

+
+

+ Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support. +

+
+
+ +
+
+
+ +
+
+`; + +exports[`SmartTransactionStatusPage renders the "pending" STX status: smart-transaction-status-pending 1`] = ` +
+
+
+
+
+

+ Your transaction was submitted +

+
+ +
+
+
+ +
+
+`; + +exports[`SmartTransactionStatusPage renders the "success" STX status: smart-transaction-status-success 1`] = ` +
+
+
+
+
+

+ Your transaction is complete +

+
+ +
+
+
+ +
+
+`; diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/index.scss b/ui/pages/smart-transactions/smart-transaction-status-page/index.scss index 5e74ba9a8b3d..2227673029d8 100644 --- a/ui/pages/smart-transactions/smart-transaction-status-page/index.scss +++ b/ui/pages/smart-transactions/smart-transaction-status-page/index.scss @@ -1,10 +1,3 @@ - -@keyframes shift { - to { - background-position: 100% 0; - } -} - .smart-transaction-status-page { text-align: center; @@ -20,24 +13,6 @@ } } - &__loading-bar-container { - @media screen and (min-width: 768px) { - max-width: 260px; - } - - width: 100%; - height: 3px; - background: var(--color-background-alternative); - display: flex; - margin-top: 16px; - } - - &__loading-bar { - height: 3px; - background: var(--color-primary-default); - transition: width 0.5s linear; - } - &__footer { grid-area: footer; } @@ -45,35 +20,4 @@ &__countdown { width: 25px; } - - // Slightly overwrite the default SimulationDetails layout to look better on the Smart Transaction status page. - .simulation-details-layout { - margin-left: 0; - margin-right: 0; - width: 100%; - text-align: left; - } - - &__background-animation { - position: relative; - left: -88px; - background-repeat: repeat; - background-position: 0 0; - - &--top { - width: 1634px; - height: 54px; - background-size: 817px 54px; - background-image: url('/images/transaction-background-top.svg'); - animation: shift 19s linear infinite; - } - - &--bottom { - width: 1600px; - height: 62px; - background-size: 800px 62px; - background-image: url('/images/transaction-background-bottom.svg'); - animation: shift 22s linear infinite; - } - } } diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.test.tsx b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.test.tsx new file mode 100644 index 000000000000..fa4166af1461 --- /dev/null +++ b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.test.tsx @@ -0,0 +1,134 @@ +import React from 'react'; +import { render, screen, act } from '@testing-library/react'; +import { SmartTransactionStatuses } from '@metamask/smart-transactions-controller/dist/types'; +import { SmartTransactionStatusAnimation } from './smart-transaction-status-animation'; + +// Declare a variable to store the onComplete callback +let mockOnComplete: () => void; + +// Modify the existing jest.mock to capture the onComplete callback +jest.mock('../../../components/component-library/lottie-animation', () => ({ + LottieAnimation: ({ + path, + loop, + autoplay, + onComplete, + }: { + path: string; + loop: boolean; + autoplay: boolean; + onComplete: () => void; + }) => { + // Store the onComplete callback for later use in tests + mockOnComplete = onComplete; + return ( +
+ ); + }, +})); + +describe('SmartTransactionsStatusAnimation', () => { + it('renders correctly for PENDING status', () => { + const { getByTestId } = render( + , + ); + const lottieAnimation = getByTestId('mock-lottie-animation'); + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('submitting-intro'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'false'); + }); + + it('renders correctly for SUCCESS status', () => { + const { getByTestId } = render( + , + ); + const lottieAnimation = getByTestId('mock-lottie-animation'); + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('confirmed'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'false'); + }); + + it('renders correctly for REVERTED status', () => { + const { getByTestId } = render( + , + ); + const lottieAnimation = getByTestId('mock-lottie-animation'); + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('failed'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'false'); + }); + + it('renders correctly for UNKNOWN status', () => { + const { getByTestId } = render( + , + ); + const lottieAnimation = getByTestId('mock-lottie-animation'); + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('failed'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'false'); + }); + + it('renders correctly for other statuses', () => { + const { getByTestId } = render( + , + ); + const lottieAnimation = getByTestId('mock-lottie-animation'); + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('processing'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'true'); + }); + + it('transitions from submittingIntro to submittingLoop when onComplete is called', () => { + render( + , + ); + const lottieAnimation = screen.getByTestId('mock-lottie-animation'); + + // Initially, should render 'submitting-intro' + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('submitting-intro'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'false'); + + // Trigger the onComplete callback to simulate animation completion + expect(lottieAnimation.getAttribute('data-on-complete')).toBeDefined(); + act(() => { + mockOnComplete(); + }); + + // After onComplete is called, it should transition to 'submitting-loop' + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('submitting-loop'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'true'); + }); +}); diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.tsx b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.tsx new file mode 100644 index 000000000000..3dc739aefa1f --- /dev/null +++ b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.tsx @@ -0,0 +1,80 @@ +import React, { useState, useCallback } from 'react'; +import { SmartTransactionStatuses } from '@metamask/smart-transactions-controller/dist/types'; +import { Box } from '../../../components/component-library'; +import { Display } from '../../../helpers/constants/design-system'; +import { LottieAnimation } from '../../../components/component-library/lottie-animation'; + +const ANIMATIONS_FOLDER = 'images/animations/smart-transaction-status'; + +type AnimationInfo = { + path: string; + loop: boolean; +}; + +const Animations: Record = { + Failed: { + path: `${ANIMATIONS_FOLDER}/failed.lottie.json`, + loop: false, + }, + Confirmed: { + path: `${ANIMATIONS_FOLDER}/confirmed.lottie.json`, + loop: false, + }, + SubmittingIntro: { + path: `${ANIMATIONS_FOLDER}/submitting-intro.lottie.json`, + loop: false, + }, + SubmittingLoop: { + path: `${ANIMATIONS_FOLDER}/submitting-loop.lottie.json`, + loop: true, + }, + Processing: { + path: `${ANIMATIONS_FOLDER}/processing.lottie.json`, + loop: true, + }, +}; + +export const SmartTransactionStatusAnimation = ({ + status, +}: { + status: SmartTransactionStatuses; +}) => { + const [isIntro, setIsIntro] = useState(true); + + let animation: AnimationInfo; + + if (status === SmartTransactionStatuses.PENDING) { + animation = isIntro + ? Animations.SubmittingIntro + : Animations.SubmittingLoop; + } else { + switch (status) { + case SmartTransactionStatuses.SUCCESS: + animation = Animations.Confirmed; + break; + case SmartTransactionStatuses.REVERTED: + case SmartTransactionStatuses.UNKNOWN: + animation = Animations.Failed; + break; + default: + animation = Animations.Processing; + } + } + + const handleAnimationComplete = useCallback(() => { + if (status === SmartTransactionStatuses.PENDING && isIntro) { + setIsIntro(false); + } + }, [status, isIntro]); + + return ( + + + + ); +}; diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.stories.tsx b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.stories.tsx new file mode 100644 index 000000000000..12d356ce4cc4 --- /dev/null +++ b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.stories.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import configureStore from '../../../store/store'; +import SmartTransactionStatusPage from './smart-transaction-status-page'; +import { Meta, StoryObj } from '@storybook/react'; +import { SimulationData } from '@metamask/transaction-controller'; +import { mockNetworkState } from '../../../../test/stub/networks'; + +// Mock data +const CHAIN_ID_MOCK = '0x1'; + +const simulationData: SimulationData = { + nativeBalanceChange: { + previousBalance: '0x0', + newBalance: '0x0', + difference: '0x12345678912345678', + isDecrease: true, + }, + tokenBalanceChanges: [], +}; + +const TX_MOCK = { + id: 'txId', + simulationData, + chainId: CHAIN_ID_MOCK, +}; + +const storeMock = configureStore({ + metamask: { + preferences: { + useNativeCurrencyAsPrimaryCurrency: false, + }, + ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), + transactions: [TX_MOCK], + currentNetworkTxList: [TX_MOCK], + }, +}); + +const meta: Meta = { + title: 'Pages/SmartTransactions/SmartTransactionStatusPage', + component: SmartTransactionStatusPage, + decorators: [(story) => {story()}], +}; + +export default meta; +type Story = StoryObj; + +export const Pending: Story = { + args: { + requestState: { + smartTransaction: { + status: 'pending', + creationTime: Date.now(), + uuid: 'uuid', + chainId: '0x1', + }, + isDapp: false, + txId: 'txId', + }, + onCloseExtension: () => {}, + onViewActivity: () => {}, + }, +}; + +export const Success: Story = { + args: { + requestState: { + smartTransaction: { + status: 'success', + creationTime: Date.now() - 60000, // 1 minute ago + uuid: 'uuid-success', + chainId: '0x1', + }, + isDapp: false, + txId: 'txId-success', + }, + onCloseExtension: () => {}, + onViewActivity: () => {}, + }, +}; + +export const Failed: Story = { + args: { + requestState: { + smartTransaction: { + status: 'unknown', + creationTime: Date.now() - 180000, // 3 minutes ago + uuid: 'uuid-failed', + chainId: '0x1', + }, + isDapp: false, + txId: 'txId-failed', + }, + onCloseExtension: () => {}, + onViewActivity: () => {}, + }, +}; diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.tsx b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.tsx index 2eb29bfa4e4e..4492ed4e4844 100644 --- a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.tsx +++ b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useEffect, useCallback } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { SmartTransactionStatuses, @@ -8,9 +8,7 @@ import { import { Box, Text, - Icon, IconName, - IconSize, Button, ButtonVariant, ButtonSecondary, @@ -26,22 +24,18 @@ import { TextColor, FontWeight, IconColor, - TextAlign, } from '../../../helpers/constants/design-system'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { getCurrentChainId, getFullTxData } from '../../../selectors'; -import { getFeatureFlagsByChainId } from '../../../../shared/modules/selectors'; import { BaseUrl } from '../../../../shared/constants/urls'; -import { - FALLBACK_SMART_TRANSACTIONS_EXPECTED_DEADLINE, - FALLBACK_SMART_TRANSACTIONS_MAX_DEADLINE, -} from '../../../../shared/constants/smartTransactions'; import { hideLoadingIndication } from '../../../store/actions'; import { hexToDecimal } from '../../../../shared/modules/conversion.utils'; import { SimulationDetails } from '../../confirmations/components/simulation-details'; import { NOTIFICATION_WIDTH } from '../../../../shared/constants/notifications'; -type RequestState = { +import { SmartTransactionStatusAnimation } from './smart-transaction-status-animation'; + +export type RequestState = { smartTransaction?: SmartTransaction; isDapp: boolean; txId?: string; @@ -49,8 +43,8 @@ type RequestState = { export type SmartTransactionStatusPageProps = { requestState: RequestState; - onCloseExtension: () => void; - onViewActivity: () => void; + onCloseExtension?: () => void; + onViewActivity?: () => void; }; export const showRemainingTimeInMinAndSec = ( @@ -66,30 +60,18 @@ export const showRemainingTimeInMinAndSec = ( const getDisplayValues = ({ t, - countdown, isSmartTransactionPending, - isSmartTransactionTakingTooLong, isSmartTransactionSuccess, isSmartTransactionCancelled, }: { t: ReturnType; - countdown: JSX.Element | undefined; isSmartTransactionPending: boolean; - isSmartTransactionTakingTooLong: boolean; isSmartTransactionSuccess: boolean; isSmartTransactionCancelled: boolean; }) => { - if (isSmartTransactionPending && isSmartTransactionTakingTooLong) { - return { - title: t('smartTransactionTakingTooLong'), - description: t('smartTransactionTakingTooLongDescription', [countdown]), - iconName: IconName.Clock, - iconColor: IconColor.primaryDefault, - }; - } else if (isSmartTransactionPending) { + if (isSmartTransactionPending) { return { title: t('smartTransactionPending'), - description: t('stxEstimatedCompletion', [countdown]), iconName: IconName.Clock, iconColor: IconColor.primaryDefault, }; @@ -102,7 +84,7 @@ const getDisplayValues = ({ } else if (isSmartTransactionCancelled) { return { title: t('smartTransactionCancelled'), - description: t('smartTransactionCancelledDescription', [countdown]), + description: t('smartTransactionCancelledDescription'), iconName: IconName.Danger, iconColor: IconColor.errorDefault, }; @@ -116,98 +98,6 @@ const getDisplayValues = ({ }; }; -const useRemainingTime = ({ - isSmartTransactionPending, - smartTransaction, - stxMaxDeadline, - stxEstimatedDeadline, -}: { - isSmartTransactionPending: boolean; - smartTransaction?: SmartTransaction; - stxMaxDeadline: number; - stxEstimatedDeadline: number; -}) => { - const [timeLeftForPendingStxInSec, setTimeLeftForPendingStxInSec] = - useState(0); - const [isSmartTransactionTakingTooLong, setIsSmartTransactionTakingTooLong] = - useState(false); - const stxDeadline = isSmartTransactionTakingTooLong - ? stxMaxDeadline - : stxEstimatedDeadline; - - useEffect(() => { - if (!isSmartTransactionPending) { - return; - } - - const calculateRemainingTime = () => { - const secondsAfterStxSubmission = smartTransaction?.creationTime - ? Math.round((Date.now() - smartTransaction.creationTime) / 1000) - : 0; - - if (secondsAfterStxSubmission > stxDeadline) { - setTimeLeftForPendingStxInSec(0); - if (!isSmartTransactionTakingTooLong) { - setIsSmartTransactionTakingTooLong(true); - } - return; - } - - setTimeLeftForPendingStxInSec(stxDeadline - secondsAfterStxSubmission); - }; - - const intervalId = setInterval(calculateRemainingTime, 1000); - calculateRemainingTime(); - - // eslint-disable-next-line consistent-return - return () => clearInterval(intervalId); - }, [ - isSmartTransactionPending, - isSmartTransactionTakingTooLong, - smartTransaction?.creationTime, - stxDeadline, - ]); - - return { - timeLeftForPendingStxInSec, - isSmartTransactionTakingTooLong, - stxDeadline, - }; -}; - -const Deadline = ({ - isSmartTransactionPending, - stxDeadline, - timeLeftForPendingStxInSec, -}: { - isSmartTransactionPending: boolean; - stxDeadline: number; - timeLeftForPendingStxInSec: number; -}) => { - if (!isSmartTransactionPending) { - return null; - } - return ( - -
-
-
- - ); -}; - const Description = ({ description }: { description: string | undefined }) => { if (!description) { return null; @@ -388,29 +278,10 @@ const Title = ({ title }: { title: string }) => { ); }; -const SmartTransactionsStatusIcon = ({ - iconName, - iconColor, -}: { - iconName: IconName; - iconColor: IconColor; -}) => { - return ( - - - - ); -}; - export const SmartTransactionStatusPage = ({ requestState, - onCloseExtension, - onViewActivity, + onCloseExtension = () => null, + onViewActivity = () => null, }: SmartTransactionStatusPageProps) => { const t = useI18nContext(); const dispatch = useDispatch(); @@ -423,50 +294,15 @@ export const SmartTransactionStatusPage = ({ const isSmartTransactionCancelled = Boolean( smartTransaction?.status?.startsWith(SmartTransactionStatuses.CANCELLED), ); - const featureFlags: { - smartTransactions?: { - expectedDeadline?: number; - maxDeadline?: number; - }; - } | null = useSelector(getFeatureFlagsByChainId); - const stxEstimatedDeadline = - featureFlags?.smartTransactions?.expectedDeadline || - FALLBACK_SMART_TRANSACTIONS_EXPECTED_DEADLINE; - const stxMaxDeadline = - featureFlags?.smartTransactions?.maxDeadline || - FALLBACK_SMART_TRANSACTIONS_MAX_DEADLINE; - const { - timeLeftForPendingStxInSec, - isSmartTransactionTakingTooLong, - stxDeadline, - } = useRemainingTime({ - isSmartTransactionPending, - smartTransaction, - stxMaxDeadline, - stxEstimatedDeadline, - }); + const chainId: string = useSelector(getCurrentChainId); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: This same selector is used in the awaiting-swap component. const fullTxData = useSelector((state) => getFullTxData(state, txId)) || {}; - const countdown = isSmartTransactionPending ? ( - - {showRemainingTimeInMinAndSec(timeLeftForPendingStxInSec)} - - ) : undefined; - - const { title, description, iconName, iconColor } = getDisplayValues({ + const { title, description } = getDisplayValues({ t, - countdown, isSmartTransactionPending, - isSmartTransactionTakingTooLong, isSmartTransactionSuccess, isSmartTransactionCancelled, }); @@ -515,20 +351,10 @@ export const SmartTransactionStatusPage = ({ paddingRight={6} width={BlockSize.Full} > - - - <Deadline - isSmartTransactionPending={isSmartTransactionPending} - stxDeadline={stxDeadline} - timeLeftForPendingStxInSec={timeLeftForPendingStxInSec} - /> <Description description={description} /> <PortfolioSmartTransactionStatusUrl portfolioSmartTransactionStatusUrl={ @@ -539,15 +365,13 @@ export const SmartTransactionStatusPage = ({ /> </Box> {canShowSimulationDetails && ( - <SimulationDetails - simulationData={fullTxData.simulationData} - transactionId={fullTxData.id} - /> + <Box width={BlockSize.Full}> + <SimulationDetails + simulationData={fullTxData.simulationData} + transactionId={fullTxData.id} + /> + </Box> )} - <Box - marginTop={3} - className="smart-transaction-status-page__background-animation smart-transaction-status-page__background-animation--bottom" - /> </Box> <SmartTransactionsStatusPageFooter isDapp={isDapp} diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.js b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.js deleted file mode 100644 index d014c56373a4..000000000000 --- a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.js +++ /dev/null @@ -1,226 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import { SmartTransactionStatuses } from '@metamask/smart-transactions-controller/dist/types'; - -import { - renderWithProvider, - createSwapsMockStore, -} from '../../../../test/jest'; -import { CHAIN_IDS } from '../../../../shared/constants/network'; -import { SmartTransactionStatusPage } from '.'; - -const middleware = [thunk]; - -describe('SmartTransactionStatusPage', () => { - const requestState = { - smartTransaction: { - status: SmartTransactionStatuses.PENDING, - creationTime: Date.now(), - }, - }; - - it('renders the component with initial props', () => { - const store = configureMockStore(middleware)(createSwapsMockStore()); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Submitting your transaction')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "Sorry for the wait" pending status', () => { - const store = configureMockStore(middleware)(createSwapsMockStore()); - const newRequestState = { - ...requestState, - smartTransaction: { - ...requestState.smartTransaction, - creationTime: 1519211809934, - }, - }; - const { queryByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={newRequestState} />, - store, - ); - expect( - queryByText('You may close this window anytime.'), - ).not.toBeInTheDocument(); - expect(queryByText('Sorry for the wait')).toBeInTheDocument(); - expect(queryByText('View activity')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "success" STX status', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.SUCCESS; - requestState.smartTransaction = latestSmartTransaction; - const store = configureMockStore(middleware)(mockStore); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Your transaction is complete')).toBeInTheDocument(); - expect(getByText('View transaction')).toBeInTheDocument(); - expect(getByText('View activity')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "reverted" STX status', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.REVERTED; - requestState.smartTransaction = latestSmartTransaction; - const store = configureMockStore(middleware)(mockStore); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Your transaction failed')).toBeInTheDocument(); - expect(getByText('View transaction')).toBeInTheDocument(); - expect(getByText('View activity')).toBeInTheDocument(); - expect( - getByText( - 'Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support.', - ), - ).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "cancelled" STX status', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - requestState.smartTransaction = latestSmartTransaction; - latestSmartTransaction.status = SmartTransactionStatuses.CANCELLED; - const store = configureMockStore(middleware)(mockStore); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Your transaction was canceled')).toBeInTheDocument(); - expect( - getByText( - `Your transaction couldn't be completed, so it was canceled to save you from paying unnecessary gas fees.`, - ), - ).toBeInTheDocument(); - expect(getByText('View transaction')).toBeInTheDocument(); - expect(getByText('View activity')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "deadline_missed" STX status', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = - SmartTransactionStatuses.CANCELLED_DEADLINE_MISSED; - requestState.smartTransaction = latestSmartTransaction; - const store = configureMockStore(middleware)(mockStore); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Your transaction was canceled')).toBeInTheDocument(); - expect(getByText('View transaction')).toBeInTheDocument(); - expect(getByText('View activity')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "unknown" STX status', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.UNKNOWN; - requestState.smartTransaction = latestSmartTransaction; - const store = configureMockStore(middleware)(mockStore); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Your transaction failed')).toBeInTheDocument(); - expect(getByText('View transaction')).toBeInTheDocument(); - expect(getByText('View activity')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "pending" STX status for a dapp transaction', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.PENDING; - requestState.smartTransaction = latestSmartTransaction; - requestState.isDapp = true; - const store = configureMockStore(middleware)(mockStore); - const { queryByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect( - queryByText('You may close this window anytime.'), - ).toBeInTheDocument(); - expect(queryByText('View transaction')).toBeInTheDocument(); - expect(queryByText('Close extension')).toBeInTheDocument(); - expect(queryByText('View activity')).not.toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "success" STX status for a dapp transaction', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.SUCCESS; - requestState.smartTransaction = latestSmartTransaction; - requestState.isDapp = true; - const store = configureMockStore(middleware)(mockStore); - const { queryByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect( - queryByText('You may close this window anytime.'), - ).not.toBeInTheDocument(); - expect(queryByText('View transaction')).toBeInTheDocument(); - expect(queryByText('Close extension')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "cancelled" STX status for a dapp transaction', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.CANCELLED; - requestState.smartTransaction = latestSmartTransaction; - requestState.isDapp = true; - const store = configureMockStore(middleware)(mockStore); - const { queryByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect( - queryByText('You may close this window anytime.'), - ).not.toBeInTheDocument(); - expect(queryByText('View transaction')).toBeInTheDocument(); - expect(queryByText('Close extension')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); -}); diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.tsx b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.tsx new file mode 100644 index 000000000000..afd9b2872ce1 --- /dev/null +++ b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.tsx @@ -0,0 +1,145 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { + SmartTransaction, + SmartTransactionStatuses, +} from '@metamask/smart-transactions-controller/dist/types'; + +import { fireEvent } from '@testing-library/react'; +import { + renderWithProvider, + createSwapsMockStore, +} from '../../../../test/jest'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { + SmartTransactionStatusPage, + RequestState, +} from './smart-transaction-status-page'; + +// Mock the SmartTransactionStatusAnimation component and capture props +jest.mock('./smart-transaction-status-animation', () => ({ + SmartTransactionStatusAnimation: ({ + status, + }: { + status: SmartTransactionStatuses; + }) => <div data-testid="mock-animation" data-status={status} />, +})); + +const middleware = [thunk]; +const mockStore = configureMockStore(middleware); + +const defaultRequestState: RequestState = { + smartTransaction: { + status: SmartTransactionStatuses.PENDING, + creationTime: Date.now(), + uuid: 'uuid', + chainId: CHAIN_IDS.MAINNET, + }, + isDapp: false, + txId: 'txId', +}; + +describe('SmartTransactionStatusPage', () => { + const statusTestCases = [ + { + status: SmartTransactionStatuses.PENDING, + isDapp: false, + expectedTexts: ['Your transaction was submitted', 'View activity'], + snapshotName: 'pending', + }, + { + status: SmartTransactionStatuses.SUCCESS, + isDapp: false, + expectedTexts: [ + 'Your transaction is complete', + 'View transaction', + 'View activity', + ], + snapshotName: 'success', + }, + { + status: SmartTransactionStatuses.REVERTED, + isDapp: false, + expectedTexts: [ + 'Your transaction failed', + 'View transaction', + 'View activity', + 'Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support.', + ], + snapshotName: 'failed', + }, + ]; + + statusTestCases.forEach(({ status, isDapp, expectedTexts, snapshotName }) => { + it(`renders the "${snapshotName}" STX status${ + isDapp ? ' for a dapp transaction' : '' + }`, () => { + const state = createSwapsMockStore(); + const latestSmartTransaction = + state.metamask.smartTransactionsState.smartTransactions[ + CHAIN_IDS.MAINNET + ][1]; + latestSmartTransaction.status = status; + const requestState: RequestState = { + smartTransaction: latestSmartTransaction as SmartTransaction, + isDapp, + txId: 'txId', + }; + + const { getByText, getByTestId, container } = renderWithProvider( + <SmartTransactionStatusPage requestState={requestState} />, + mockStore(state), + ); + + expectedTexts.forEach((text) => { + expect(getByText(text)).toBeInTheDocument(); + }); + + expect(getByTestId('mock-animation')).toBeInTheDocument(); + expect(getByTestId('mock-animation')).toHaveAttribute( + 'data-status', + status, + ); + expect(container).toMatchSnapshot( + `smart-transaction-status-${snapshotName}`, + ); + }); + }); + + describe('Action Buttons', () => { + it('calls onCloseExtension when Close extension button is clicked', () => { + const onCloseExtension = jest.fn(); + const store = mockStore(createSwapsMockStore()); + + const { getByText } = renderWithProvider( + <SmartTransactionStatusPage + requestState={{ ...defaultRequestState, isDapp: true }} + onCloseExtension={onCloseExtension} + />, + store, + ); + + const closeButton = getByText('Close extension'); + fireEvent.click(closeButton); + expect(onCloseExtension).toHaveBeenCalled(); + }); + + it('calls onViewActivity when View activity button is clicked', () => { + const onViewActivity = jest.fn(); + const store = mockStore(createSwapsMockStore()); + + const { getByText } = renderWithProvider( + <SmartTransactionStatusPage + requestState={{ ...defaultRequestState, isDapp: false }} + onViewActivity={onViewActivity} + />, + store, + ); + + const viewActivityButton = getByText('View activity'); + fireEvent.click(viewActivityButton); + expect(onViewActivity).toHaveBeenCalled(); + }); + }); +}); diff --git a/ui/pages/snap-account-redirect/__snapshots__/create-snap-redirect.test.tsx.snap b/ui/pages/snap-account-redirect/__snapshots__/create-snap-redirect.test.tsx.snap index a971dd30faba..e6bb4ba7579c 100644 --- a/ui/pages/snap-account-redirect/__snapshots__/create-snap-redirect.test.tsx.snap +++ b/ui/pages/snap-account-redirect/__snapshots__/create-snap-redirect.test.tsx.snap @@ -107,7 +107,7 @@ exports[`<SnapAccountRedirect /> renders the url and message when provided and i class="mm-box mm-text mm-text--body-md mm-box--padding-2 mm-box--color-primary-default" data-testid="snap-account-redirect-url-display-box" > - https://metamask.github.io/snap-simple-keyring/1.1.2/ + https://metamask.github.io/snap-simple-keyring/1.1.6/ </p> <button aria-label="" diff --git a/ui/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap b/ui/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap index 01a3a89ad4c5..d58907ed2684 100644 --- a/ui/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap +++ b/ui/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap @@ -5,7 +5,7 @@ exports[`DropdownInputPair renders the component with initial props 1`] = ` class="MuiFormControl-root MuiTextField-root dropdown-input-pair__input MuiFormControl-marginDense MuiFormControl-fullWidth" > <div - class="MuiInputBase-root MuiInput-root TextField-inputRoot-12 MuiInput-underline MuiInputBase-fullWidth MuiInput-fullWidth MuiInputBase-formControl MuiInput-formControl MuiInputBase-marginDense MuiInput-marginDense" + class="MuiInputBase-root MuiInput-root TextField-inputRoot-12 MuiInputBase-fullWidth MuiInput-fullWidth MuiInputBase-formControl MuiInput-formControl MuiInputBase-marginDense MuiInput-marginDense" > <input aria-invalid="false" diff --git a/ui/pages/swaps/searchable-item-list/__snapshots__/searchable-item-list.test.js.snap b/ui/pages/swaps/searchable-item-list/__snapshots__/searchable-item-list.test.js.snap index 651c33786acc..f5e215c9fa5e 100644 --- a/ui/pages/swaps/searchable-item-list/__snapshots__/searchable-item-list.test.js.snap +++ b/ui/pages/swaps/searchable-item-list/__snapshots__/searchable-item-list.test.js.snap @@ -5,7 +5,7 @@ exports[`SearchableItemList renders the component with initial props 1`] = ` class="MuiFormControl-root MuiTextField-root searchable-item-list__search MuiFormControl-fullWidth" > <div - class="MuiInputBase-root MuiInput-root TextField-inputRoot-12 MuiInput-underline MuiInputBase-fullWidth MuiInput-fullWidth Mui-focused Mui-focused TextField-inputFocused-11 MuiInputBase-formControl MuiInput-formControl MuiInputBase-adornedStart" + class="MuiInputBase-root MuiInput-root TextField-inputRoot-12 MuiInputBase-fullWidth MuiInput-fullWidth Mui-focused Mui-focused TextField-inputFocused-11 MuiInputBase-formControl MuiInput-formControl MuiInputBase-adornedStart" > <div class="MuiInputAdornment-root MuiInputAdornment-positionStart" diff --git a/ui/pages/swaps/swaps.util.test.js b/ui/pages/swaps/swaps.util.test.js index d7ecace642ae..4b277ab56345 100644 --- a/ui/pages/swaps/swaps.util.test.js +++ b/ui/pages/swaps/swaps.util.test.js @@ -35,6 +35,7 @@ import { showRemainingTimeInMinAndSec, getFeeForSmartTransaction, formatSwapsValueForDisplay, + fetchTopAssetsList, } from './swaps.util'; jest.mock('../../../shared/lib/storage-helpers', () => ({ @@ -85,6 +86,25 @@ describe('Swaps Util', () => { }); }); + describe('fetchTopAssetsList', () => { + beforeEach(() => { + nock('https://swap.api.cx.metamask.io') + .persist() + .get('/networks/1/topAssets') + .reply(200, TOP_ASSETS); + }); + + it('should fetch top assets', async () => { + const result = await fetchTopAssetsList(CHAIN_IDS.MAINNET); + expect(result).toStrictEqual(TOP_ASSETS); + }); + + it('should fetch top assets on prod', async () => { + const result = await fetchTopAssetsList(CHAIN_IDS.MAINNET); + expect(result).toStrictEqual(TOP_ASSETS); + }); + }); + describe('fetchTopAssets', () => { beforeEach(() => { nock('https://swap.api.cx.metamask.io') diff --git a/ui/pages/swaps/swaps.util.ts b/ui/pages/swaps/swaps.util.ts index ce06d4ef37f8..21de45b5f349 100644 --- a/ui/pages/swaps/swaps.util.ts +++ b/ui/pages/swaps/swaps.util.ts @@ -56,7 +56,7 @@ type Validator = { validator: (a: string) => boolean; }; -const TOKEN_VALIDATORS: Validator[] = [ +export const TOKEN_VALIDATORS: Validator[] = [ { property: 'address', type: 'string', @@ -199,9 +199,9 @@ export async function fetchAggregatorMetadata(chainId: any): Promise<object> { return filteredAggregators; } -// TODO: Replace `any` with type -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export async function fetchTopAssets(chainId: any): Promise<object> { +export async function fetchTopAssetsList( + chainId: string, +): Promise<{ address: string }[]> { const topAssetsUrl = getBaseApi('topAssets', chainId); const response = (await fetchWithCache({ @@ -210,14 +210,19 @@ export async function fetchTopAssets(chainId: any): Promise<object> { fetchOptions: { method: 'GET', headers: clientIdHeader }, cacheOptions: { cacheRefreshTime: CACHE_REFRESH_FIVE_MINUTES }, })) || []; + const topAssetsList = response.filter((asset: { address: string }) => + validateData(TOP_ASSET_VALIDATORS, asset, topAssetsUrl), + ); + return topAssetsList; +} + +export async function fetchTopAssets( + chainId: string, +): Promise<Record<string, { index: string }>> { + const response = await fetchTopAssetsList(chainId); const topAssetsMap = response.reduce( - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (_topAssetsMap: any, asset: { address: string }, index: number) => { - if (validateData(TOP_ASSET_VALIDATORS, asset, topAssetsUrl)) { - return { ..._topAssetsMap, [asset.address]: { index: String(index) } }; - } - return _topAssetsMap; + (_topAssetsMap, asset: { address: string }, index: number) => { + return { ..._topAssetsMap, [asset.address]: { index: String(index) } }; }, {}, ); diff --git a/ui/selectors/metametrics.js b/ui/selectors/metametrics.js index c623e378c003..1b0a9dd603dd 100644 --- a/ui/selectors/metametrics.js +++ b/ui/selectors/metametrics.js @@ -8,6 +8,9 @@ export const getDataCollectionForMarketing = (state) => export const getParticipateInMetaMetrics = (state) => Boolean(state.metamask.participateInMetaMetrics); +export const getLatestMetricsEventTimestamp = (state) => + state.metamask.latestNonAnonymousEventTimestamp; + export const selectFragmentBySuccessEvent = createSelector( selectFragments, (_, fragmentOptions) => fragmentOptions, diff --git a/ui/selectors/metametrics.test.js b/ui/selectors/metametrics.test.js index 13185a47700b..454def7d92a4 100644 --- a/ui/selectors/metametrics.test.js +++ b/ui/selectors/metametrics.test.js @@ -2,6 +2,7 @@ const { selectFragmentBySuccessEvent, selectFragmentById, selectMatchingFragment, + getLatestMetricsEventTimestamp, } = require('.'); describe('selectFragmentBySuccessEvent', () => { @@ -68,4 +69,15 @@ describe('selectMatchingFragment', () => { }); expect(selected).toHaveProperty('id', 'randomid'); }); + describe('getLatestMetricsEventTimestamp', () => { + it('should find matching fragment in state by id', () => { + const state = { + metamask: { + latestNonAnonymousEventTimestamp: 12345, + }, + }; + const timestamp = getLatestMetricsEventTimestamp(state); + expect(timestamp).toBe(12345); + }); + }); }); diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 31c262df62c4..644924a41e3e 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -1478,6 +1478,11 @@ export const getConnectedSitesList = createDeepEqualSelector( }, ); +export function getShouldShowAggregatedBalancePopover(state) { + const { shouldShowAggregatedBalancePopover } = getPreferences(state); + return shouldShowAggregatedBalancePopover; +} + export const getConnectedSnapsList = createDeepEqualSelector( getSnapsList, (snapsData) => { @@ -1964,6 +1969,10 @@ export function getShowPrivacyPolicyToast(state) { ); } +export function getLastViewedUserSurvey(state) { + return state.metamask.lastViewedUserSurvey; +} + export function getShowOutdatedBrowserWarning(state) { const { outdatedBrowserWarningLastShown } = state.metamask; if (!outdatedBrowserWarningLastShown) { @@ -2552,6 +2561,26 @@ export function getNameSources(state) { return state.metamask.nameSources || {}; } +export function getShowDeleteMetaMetricsDataModal(state) { + return state.appState.showDeleteMetaMetricsDataModal; +} + +export function getShowDataDeletionErrorModal(state) { + return state.appState.showDataDeletionErrorModal; +} + +export function getMetaMetricsDataDeletionId(state) { + return state.metamask.metaMetricsDataDeletionId; +} + +export function getMetaMetricsDataDeletionTimestamp(state) { + return state.metamask.metaMetricsDataDeletionTimestamp; +} + +export function getMetaMetricsDataDeletionStatus(state) { + return state.metamask.metaMetricsDataDeletionStatus; +} + /** * To get all installed snaps with proper metadata * diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index 342e7d7187c8..24b2a2afe125 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -11,6 +11,7 @@ import { createMockInternalAccount } from '../../test/jest/mocks'; import { ETH_EOA_METHODS } from '../../shared/constants/eth-methods'; import { getProviderConfig } from '../ducks/metamask/metamask'; import { mockNetworkState } from '../../test/stub/networks'; +import { DeleteRegulationStatus } from '../../shared/constants/metametrics'; import * as selectors from './selectors'; jest.mock('../../app/scripts/lib/util', () => ({ @@ -2018,4 +2019,65 @@ describe('#getConnectedSitesList', () => { }, }); }); + describe('#getShowDeleteMetaMetricsDataModal', () => { + it('returns state of showDeleteMetaMetricsDataModal', () => { + expect( + selectors.getShowDeleteMetaMetricsDataModal({ + appState: { + showDeleteMetaMetricsDataModal: true, + }, + }), + ).toStrictEqual(true); + }); + }); + describe('#getShowDataDeletionErrorModal', () => { + it('returns state of showDataDeletionErrorModal', () => { + expect( + selectors.getShowDataDeletionErrorModal({ + appState: { + showDataDeletionErrorModal: true, + }, + }), + ).toStrictEqual(true); + }); + }); + describe('#getMetaMetricsDataDeletionId', () => { + it('returns metaMetricsDataDeletionId', () => { + expect( + selectors.getMetaMetricsDataDeletionId({ + metamask: { + metaMetricsDataDeletionId: '123', + metaMetricsDataDeletionTimestamp: '123345', + metaMetricsDataDeletionStatus: DeleteRegulationStatus.Initialized, + }, + }), + ).toStrictEqual('123'); + }); + }); + describe('#getMetaMetricsDataDeletionTimestamp', () => { + it('returns metaMetricsDataDeletionTimestamp', () => { + expect( + selectors.getMetaMetricsDataDeletionTimestamp({ + metamask: { + metaMetricsDataDeletionId: '123', + metaMetricsDataDeletionTimestamp: '123345', + metaMetricsDataDeletionStatus: DeleteRegulationStatus.Initialized, + }, + }), + ).toStrictEqual('123345'); + }); + }); + describe('#getMetaMetricsDataDeletionStatus', () => { + it('returns metaMetricsDataDeletionStatus', () => { + expect( + selectors.getMetaMetricsDataDeletionStatus({ + metamask: { + metaMetricsDataDeletionId: '123', + metaMetricsDataDeletionTimestamp: '123345', + metaMetricsDataDeletionStatus: DeleteRegulationStatus.Initialized, + }, + }), + ).toStrictEqual('INITIALIZED'); + }); + }); }); diff --git a/ui/store/actionConstants.ts b/ui/store/actionConstants.ts index 074568cfbf1d..6e1e33d9531f 100644 --- a/ui/store/actionConstants.ts +++ b/ui/store/actionConstants.ts @@ -99,6 +99,14 @@ export const UPDATE_CUSTOM_NONCE = 'UPDATE_CUSTOM_NONCE'; export const SET_PARTICIPATE_IN_METAMETRICS = 'SET_PARTICIPATE_IN_METAMETRICS'; export const SET_DATA_COLLECTION_FOR_MARKETING = 'SET_DATA_COLLECTION_FOR_MARKETING'; +export const DELETE_METAMETRICS_DATA_MODAL_OPEN = + 'DELETE_METAMETRICS_DATA_MODAL_OPEN'; +export const DELETE_METAMETRICS_DATA_MODAL_CLOSE = + 'DELETE_METAMETRICS_DATA_MODAL_CLOSE'; +export const DATA_DELETION_ERROR_MODAL_OPEN = + 'DELETE_METAMETRICS_DATA_ERROR_MODAL_OPEN'; +export const DATA_DELETION_ERROR_MODAL_CLOSE = + 'DELETE_METAMETRICS_DATA_ERROR_MODAL_CLOSE'; // locale export const SET_CURRENT_LOCALE = 'SET_CURRENT_LOCALE'; diff --git a/ui/store/actions.ts b/ui/store/actions.ts index dae0052c46f6..e64d366a7c74 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -3035,10 +3035,8 @@ export function setDefaultHomeActiveTabName( }; } -export function setUseNativeCurrencyAsPrimaryCurrencyPreference( - value: boolean, -) { - return setPreference('useNativeCurrencyAsPrimaryCurrency', value); +export function setShowNativeTokenAsMainBalancePreference(value: boolean) { + return setPreference('showNativeTokenAsMainBalance', value); } export function setHideZeroBalanceTokens(value: boolean) { @@ -3049,6 +3047,14 @@ export function setShowFiatConversionOnTestnetsPreference(value: boolean) { return setPreference('showFiatInTestnets', value); } +/** + * Sets shouldShowAggregatedBalancePopover to false once the user toggles + * the setting to show native token as main balance. + */ +export function setAggregatedBalancePopoverShown() { + return setPreference('shouldShowAggregatedBalancePopover', false); +} + export function setShowTestNetworks(value: boolean) { return setPreference('showTestNetworks', value); } @@ -4251,6 +4257,12 @@ export function setNewPrivacyPolicyToastClickedOrClosed() { }; } +export function setLastViewedUserSurvey(id: number) { + return async () => { + await submitRequestToBackground('setLastViewedUserSurvey', [id]); + }; +} + export function setOnboardingDate() { return async () => { await submitRequestToBackground('setOnboardingDate'); diff --git a/yarn.lock b/yarn.lock index 1434d86d4e62..4cae5223a04c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2429,7 +2429,7 @@ __metadata: languageName: node linkType: hard -"@ethereumjs/tx@npm:^4.0.0, @ethereumjs/tx@npm:^4.0.2, @ethereumjs/tx@npm:^4.1.1, @ethereumjs/tx@npm:^4.2.0": +"@ethereumjs/tx@npm:^4.0.2, @ethereumjs/tx@npm:^4.1.1, @ethereumjs/tx@npm:^4.2.0": version: 4.2.0 resolution: "@ethereumjs/tx@npm:4.2.0" dependencies: @@ -4762,18 +4762,18 @@ __metadata: languageName: node linkType: hard -"@metamask/account-watcher@npm:^4.1.0": - version: 4.1.0 - resolution: "@metamask/account-watcher@npm:4.1.0" +"@metamask/account-watcher@npm:^4.1.1": + version: 4.1.1 + resolution: "@metamask/account-watcher@npm:4.1.1" dependencies: "@ethereumjs/tx": "npm:^5.1.0" "@ethereumjs/util": "npm:^9.0.1" - "@metamask/keyring-api": "npm:^4.0.1" + "@metamask/keyring-api": "npm:^8.1.3" "@metamask/snaps-sdk": "npm:^6.2.1" "@metamask/utils": "npm:^8.3.0" ethers: "npm:^5.7.2" uuid: "npm:^9.0.0" - checksum: 10/51c150cc1a703c6726f7c11eb6b4906636a5c33cf25c2b60c7d120e67483fae37ac79ba46a5156518cb9666c2c64fea00f1d6ec23faa266b28a814c4fcefa561 + checksum: 10/a1b53cdcd3a5844c1edd2e91bf6d2e5a1f3914f795c928f9611c56bc4133c8338e4ae491cb2fda7273e59830a1d613ce17997a0639bb82ec5c71c2f0b260d88e languageName: node linkType: hard @@ -4800,14 +4800,14 @@ __metadata: languageName: node linkType: hard -"@metamask/accounts-controller@npm:^18.2.1": - version: 18.2.1 - resolution: "@metamask/accounts-controller@npm:18.2.1" +"@metamask/accounts-controller@npm:^18.2.2": + version: 18.2.2 + resolution: "@metamask/accounts-controller@npm:18.2.2" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@metamask/base-controller": "npm:^7.0.1" - "@metamask/eth-snap-keyring": "npm:^4.3.3" - "@metamask/keyring-api": "npm:^8.1.0" + "@metamask/eth-snap-keyring": "npm:^4.3.6" + "@metamask/keyring-api": "npm:^8.1.3" "@metamask/snaps-sdk": "npm:^6.5.0" "@metamask/snaps-utils": "npm:^8.1.1" "@metamask/utils": "npm:^9.1.0" @@ -4818,7 +4818,7 @@ __metadata: peerDependencies: "@metamask/keyring-controller": ^17.0.0 "@metamask/snaps-controllers": ^9.7.0 - checksum: 10/cad8d68e5c5d8b349fcf5bfd6bc900cccbb5ad54bdcf2678a4469f7b3118064ca26bedafaafa89bea6ddce6f0cfb22af8eb8b7958bbd6cfce916f19a91a8e770 + checksum: 10/095be37c94a577304425f80600d4ef847c83c702ccf3d6b1591602d1fe292bdd3273131e336d6108bd713bff38812dfc4d7b21d4075669cde24e12f117f2dd81 languageName: node linkType: hard @@ -4943,10 +4943,10 @@ __metadata: languageName: node linkType: hard -"@metamask/bitcoin-wallet-snap@npm:^0.6.0": - version: 0.6.0 - resolution: "@metamask/bitcoin-wallet-snap@npm:0.6.0" - checksum: 10/baf4d7a43ddb5f210437c722e90abc6a3b4056390cc1d075e1a09acb82e934db338fce36fb897560e7f9ecd8ff3fcbd4795b3076dc7243af7ac93ea5d47b63f5 +"@metamask/bitcoin-wallet-snap@npm:^0.6.1": + version: 0.6.1 + resolution: "@metamask/bitcoin-wallet-snap@npm:0.6.1" + checksum: 10/9c595e328cd63efe62cdda4194efe44ab3da4a54a89007f485280924aa9e8ee37042bda0a07751f3ce01c2c3e4740b16cd130f07558aa84cd57b20a8d5f1d3a7 languageName: node linkType: hard @@ -5344,21 +5344,22 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-snap-keyring@npm:^4.3.1, @metamask/eth-snap-keyring@npm:^4.3.3": - version: 4.3.3 - resolution: "@metamask/eth-snap-keyring@npm:4.3.3" +"@metamask/eth-snap-keyring@npm:^4.3.1, @metamask/eth-snap-keyring@npm:^4.3.6": + version: 4.3.6 + resolution: "@metamask/eth-snap-keyring@npm:4.3.6" dependencies: "@ethereumjs/tx": "npm:^4.2.0" "@metamask/eth-sig-util": "npm:^7.0.3" - "@metamask/keyring-api": "npm:^8.1.0" - "@metamask/snaps-controllers": "npm:^9.6.0" - "@metamask/snaps-sdk": "npm:^6.4.0" - "@metamask/snaps-utils": "npm:^7.8.0" + "@metamask/snaps-controllers": "npm:^9.7.0" + "@metamask/snaps-sdk": "npm:^6.5.1" + "@metamask/snaps-utils": "npm:^7.8.1" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" - "@types/uuid": "npm:^9.0.1" - uuid: "npm:^9.0.0" - checksum: 10/035c82afef82a4cee7bc63b5c4f152a132b683017ec90a4b614764a4bc7adcca8faccf78c25adcddca2d29eee2fed08706f07d72afb93640956b86e862d4f555 + "@types/uuid": "npm:^9.0.8" + uuid: "npm:^9.0.1" + peerDependencies: + "@metamask/keyring-api": ^8.1.3 + checksum: 10/378dce125ba9e38b9ba7d9b7124383b4fd8d2782207dc69e1ae9e262beb83f22044eae5200986d4c353de29e5283c289e56b3acb88c8971a63f9365bdde3d5b4 languageName: node linkType: hard @@ -5379,17 +5380,17 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-trezor-keyring@npm:^3.1.0": - version: 3.1.0 - resolution: "@metamask/eth-trezor-keyring@npm:3.1.0" +"@metamask/eth-trezor-keyring@npm:^3.1.3": + version: 3.1.3 + resolution: "@metamask/eth-trezor-keyring@npm:3.1.3" dependencies: - "@ethereumjs/tx": "npm:^4.0.0" - "@ethereumjs/util": "npm:^8.0.0" - "@metamask/eth-sig-util": "npm:^7.0.1" + "@ethereumjs/tx": "npm:^4.2.0" + "@ethereumjs/util": "npm:^8.1.0" + "@metamask/eth-sig-util": "npm:^7.0.3" "@trezor/connect-plugin-ethereum": "npm:^9.0.3" "@trezor/connect-web": "npm:^9.1.11" hdkey: "npm:^2.1.0" - checksum: 10/2e72ab89f757494f4e4ddf46a6ddd4b6ac7db15d051d6252cd883fff537df01235f56fe9c6d02e8da03cf735a6c67f9bcdf35e50895cab034f88e838b1100b81 + checksum: 10/d32a687bcaab4593e6208a1bb59cbdd2b111eff357fd30e707787454ef571abfb4e6162422504f730f3ab2fe576b555d68114de0406ae5cdad252dab1b635cce languageName: node linkType: hard @@ -5590,18 +5591,6 @@ __metadata: languageName: node linkType: hard -"@metamask/json-rpc-middleware-stream@npm:^6.0.2": - version: 6.0.2 - resolution: "@metamask/json-rpc-middleware-stream@npm:6.0.2" - dependencies: - "@metamask/json-rpc-engine": "npm:^7.3.2" - "@metamask/safe-event-emitter": "npm:^3.0.0" - "@metamask/utils": "npm:^8.3.0" - readable-stream: "npm:^3.6.2" - checksum: 10/eb6fc179959206abeba8b12118757d55cc0028681566008a4005b570d21a9369795452e1bdb672fc9858f46a4e9ed5c996cfff0e85b47cef8bf39a6edfee8f1e - languageName: node - linkType: hard - "@metamask/json-rpc-middleware-stream@npm:^8.0.1, @metamask/json-rpc-middleware-stream@npm:^8.0.2": version: 8.0.2 resolution: "@metamask/json-rpc-middleware-stream@npm:8.0.2" @@ -5627,21 +5616,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-api@npm:^4.0.1": - version: 4.0.2 - resolution: "@metamask/keyring-api@npm:4.0.2" - dependencies: - "@metamask/providers": "npm:^15.0.0" - "@metamask/snaps-sdk": "npm:^3.1.1" - "@metamask/utils": "npm:^8.3.0" - "@types/uuid": "npm:^9.0.1" - superstruct: "npm:^1.0.3" - uuid: "npm:^9.0.0" - checksum: 10/8f6dc3b4913803eba8e22228ac6307ca66247900d70755a6dd457c2037b9fb6d3979da472a08e24ccdd81c28c68db3ad41219d915e5e8442ef640a3c0c46b261 - languageName: node - linkType: hard - -"@metamask/keyring-api@npm:^8.0.0, @metamask/keyring-api@npm:^8.1.0, @metamask/keyring-api@npm:^8.1.3": +"@metamask/keyring-api@npm:^8.0.0, @metamask/keyring-api@npm:^8.1.3": version: 8.1.3 resolution: "@metamask/keyring-api@npm:8.1.3" dependencies: @@ -5657,7 +5632,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-controller@npm:^17.1.0, @metamask/keyring-controller@npm:^17.2.1, @metamask/keyring-controller@npm:^17.2.2": +"@metamask/keyring-controller@npm:^17.1.0, @metamask/keyring-controller@npm:^17.2.2": version: 17.2.2 resolution: "@metamask/keyring-controller@npm:17.2.2" dependencies: @@ -6095,26 +6070,6 @@ __metadata: languageName: node linkType: hard -"@metamask/providers@npm:^15.0.0": - version: 15.0.0 - resolution: "@metamask/providers@npm:15.0.0" - dependencies: - "@metamask/json-rpc-engine": "npm:^7.3.2" - "@metamask/json-rpc-middleware-stream": "npm:^6.0.2" - "@metamask/object-multiplex": "npm:^2.0.0" - "@metamask/rpc-errors": "npm:^6.2.1" - "@metamask/safe-event-emitter": "npm:^3.0.0" - "@metamask/utils": "npm:^8.3.0" - detect-browser: "npm:^5.2.0" - extension-port-stream: "npm:^3.0.0" - fast-deep-equal: "npm:^3.1.3" - is-stream: "npm:^2.0.0" - readable-stream: "npm:^3.6.2" - webextension-polyfill: "npm:^0.10.0" - checksum: 10/d022fe6d2db577fcd299477f19dd1a0ca88baeae542d8a80330694d004bffc289eecf7008c619408c819de8f43eb9fc989b27e266a5961ffd43cb9c2ec749dd5 - languageName: node - linkType: hard - "@metamask/providers@npm:^17.1.2": version: 17.2.0 resolution: "@metamask/providers@npm:17.2.0" @@ -6267,7 +6222,7 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-controllers@npm:^9.6.0, @metamask/snaps-controllers@npm:^9.7.0": +"@metamask/snaps-controllers@npm:^9.7.0": version: 9.7.0 resolution: "@metamask/snaps-controllers@npm:9.7.0" dependencies: @@ -6364,7 +6319,7 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-utils@npm:^7.4.0, @metamask/snaps-utils@npm:^7.8.0": +"@metamask/snaps-utils@npm:^7.4.0, @metamask/snaps-utils@npm:^7.8.1": version: 7.8.1 resolution: "@metamask/snaps-utils@npm:7.8.1" dependencies: @@ -6507,9 +6462,9 @@ __metadata: languageName: node linkType: hard -"@metamask/transaction-controller@npm:^37.0.0": - version: 37.0.0 - resolution: "@metamask/transaction-controller@npm:37.0.0" +"@metamask/transaction-controller@npm:^37.2.0": + version: 37.2.0 + resolution: "@metamask/transaction-controller@npm:37.2.0" dependencies: "@ethereumjs/common": "npm:^3.2.0" "@ethereumjs/tx": "npm:^4.2.0" @@ -6536,7 +6491,7 @@ __metadata: "@metamask/approval-controller": ^7.0.0 "@metamask/gas-fee-controller": ^20.0.0 "@metamask/network-controller": ^21.0.0 - checksum: 10/b4608260cb86ad1a867926b983a21050a2be899f17af909ad2403b5148eada348b0fbb3f7ecef9ebc7cf8d28c040ce4d6f5009709328cda00fab61e10fa94de6 + checksum: 10/0850797efb2157de41eaec153d31f8f63d194d2290fa41a3d439a28f95a35436f47d56546b0fa64427294280476d11ab4a7ed6161a13ad6f8215a3bc052a41e2 languageName: node linkType: hard @@ -7830,6 +7785,13 @@ __metadata: languageName: node linkType: hard +"@remix-run/router@npm:1.19.2": + version: 1.19.2 + resolution: "@remix-run/router@npm:1.19.2" + checksum: 10/31b62b66ea68bd62018189047de7b262700113438f62407df019f81a9856a08a705b2b77454be9293518e2f5f3bbf3f8b858ac19f48cb7d89f8ab56b7b630c19 + languageName: node + linkType: hard + "@scure/base@npm:^1.0.0, @scure/base@npm:^1.1.1, @scure/base@npm:^1.1.3, @scure/base@npm:~1.1.0, @scure/base@npm:~1.1.3, @scure/base@npm:~1.1.6": version: 1.1.7 resolution: "@scure/base@npm:1.1.7" @@ -7889,64 +7851,64 @@ __metadata: languageName: node linkType: hard -"@sentry-internal/browser-utils@npm:8.19.0": - version: 8.19.0 - resolution: "@sentry-internal/browser-utils@npm:8.19.0" +"@sentry-internal/browser-utils@npm:8.33.1": + version: 8.33.1 + resolution: "@sentry-internal/browser-utils@npm:8.33.1" dependencies: - "@sentry/core": "npm:8.19.0" - "@sentry/types": "npm:8.19.0" - "@sentry/utils": "npm:8.19.0" - checksum: 10/d6df6cb6edc6b2ddb7362daee39770a51b255d343b3dcb80dc98f77dc43a7cc66f29076e14d1a0ac162a51a4f620b876493a04c23a530f57170009364b6464ea + "@sentry/core": "npm:8.33.1" + "@sentry/types": "npm:8.33.1" + "@sentry/utils": "npm:8.33.1" + checksum: 10/aed6ec58a2dea3613011c24c1e1f14899eaba721d4523ca7da281cbf70e1d48e5ab2bd50da17de76e8cc8052b983840d937e167ea980c6a07e4d32f0e374903c languageName: node linkType: hard -"@sentry-internal/feedback@npm:8.19.0": - version: 8.19.0 - resolution: "@sentry-internal/feedback@npm:8.19.0" +"@sentry-internal/feedback@npm:8.33.1": + version: 8.33.1 + resolution: "@sentry-internal/feedback@npm:8.33.1" dependencies: - "@sentry/core": "npm:8.19.0" - "@sentry/types": "npm:8.19.0" - "@sentry/utils": "npm:8.19.0" - checksum: 10/e10cf1f63d49a41072aaa1b7b007241a273bd4bfa6d2c628e50d621c8cde836e6743bdefbf9ba7e96684b6dd18ad49e17841f4420fc33757e7c119ec88b4ac15 + "@sentry/core": "npm:8.33.1" + "@sentry/types": "npm:8.33.1" + "@sentry/utils": "npm:8.33.1" + checksum: 10/2cb3f4c4b71f8cdf8bcab9251216b15e0caaae257bbce49fffcf053716fab60d61793898c221457e518b109e6319faf8190c2d0e57fcea8b91f28e5815f4e643 languageName: node linkType: hard -"@sentry-internal/replay-canvas@npm:8.19.0": - version: 8.19.0 - resolution: "@sentry-internal/replay-canvas@npm:8.19.0" +"@sentry-internal/replay-canvas@npm:8.33.1": + version: 8.33.1 + resolution: "@sentry-internal/replay-canvas@npm:8.33.1" dependencies: - "@sentry-internal/replay": "npm:8.19.0" - "@sentry/core": "npm:8.19.0" - "@sentry/types": "npm:8.19.0" - "@sentry/utils": "npm:8.19.0" - checksum: 10/1f379c141884b448c56fcd663b8acc0ff1c12d50a2b9db37f9552eb2bc8c99a970114f80e58c8c4fcd61f933f9a15f58dc6cbe6f4297bb574d6772be8f41c5bf + "@sentry-internal/replay": "npm:8.33.1" + "@sentry/core": "npm:8.33.1" + "@sentry/types": "npm:8.33.1" + "@sentry/utils": "npm:8.33.1" + checksum: 10/75432f627a73bad2e09ad2a7b7200c1ea4fe9d9e797458615850689dd7b017f38c876f4435ea548da9ae7653f55be90d58fc115897febacc53b69e6593867afb languageName: node linkType: hard -"@sentry-internal/replay@npm:8.19.0": - version: 8.19.0 - resolution: "@sentry-internal/replay@npm:8.19.0" +"@sentry-internal/replay@npm:8.33.1": + version: 8.33.1 + resolution: "@sentry-internal/replay@npm:8.33.1" dependencies: - "@sentry-internal/browser-utils": "npm:8.19.0" - "@sentry/core": "npm:8.19.0" - "@sentry/types": "npm:8.19.0" - "@sentry/utils": "npm:8.19.0" - checksum: 10/dc9bef6997d1f40fb0402f52c9d14f72cf050ec140fda27e00057c59ddd1a6144e78e40aeb5e0223dd48651bf02f809db26cf6e866dd5c8ec5c6bbbf76c6f1aa + "@sentry-internal/browser-utils": "npm:8.33.1" + "@sentry/core": "npm:8.33.1" + "@sentry/types": "npm:8.33.1" + "@sentry/utils": "npm:8.33.1" + checksum: 10/05cdb361ccde5039c7353877a95eb15e4d630d5edbb874cd55ac190ee8256a1456e1c6cae37636df55bff10fcde6ff1232d8ca290467d43393bb18d9e4efe99f languageName: node linkType: hard "@sentry/browser@npm:^8.19.0": - version: 8.19.0 - resolution: "@sentry/browser@npm:8.19.0" + version: 8.33.1 + resolution: "@sentry/browser@npm:8.33.1" dependencies: - "@sentry-internal/browser-utils": "npm:8.19.0" - "@sentry-internal/feedback": "npm:8.19.0" - "@sentry-internal/replay": "npm:8.19.0" - "@sentry-internal/replay-canvas": "npm:8.19.0" - "@sentry/core": "npm:8.19.0" - "@sentry/types": "npm:8.19.0" - "@sentry/utils": "npm:8.19.0" - checksum: 10/2412e938454bd5cc505bbbe7092a17bf5fde4b222ecfedaf3d54fb963a6c875c78661921d8f6e998498c85a9a52e616db75fd706867f76d38bf3f95714775aa6 + "@sentry-internal/browser-utils": "npm:8.33.1" + "@sentry-internal/feedback": "npm:8.33.1" + "@sentry-internal/replay": "npm:8.33.1" + "@sentry-internal/replay-canvas": "npm:8.33.1" + "@sentry/core": "npm:8.33.1" + "@sentry/types": "npm:8.33.1" + "@sentry/utils": "npm:8.33.1" + checksum: 10/085717b19c89184fad0c9e17dee679401ff87616678f952d91afff574ebcc56114845c216bbbd7b81c93d54c2a42b3db4232af1c707843424cdd6800a99030a5 languageName: node linkType: hard @@ -7965,36 +7927,29 @@ __metadata: languageName: node linkType: hard -"@sentry/core@npm:8.19.0": - version: 8.19.0 - resolution: "@sentry/core@npm:8.19.0" +"@sentry/core@npm:8.33.1": + version: 8.33.1 + resolution: "@sentry/core@npm:8.33.1" dependencies: - "@sentry/types": "npm:8.19.0" - "@sentry/utils": "npm:8.19.0" - checksum: 10/708ef5abd81a9ab5288a4b258411e78591a7fec4854fc582c34f087fce62f5cd74e1086fbbc27a9f55da77d113dde137fbf9649f5b7df3d1a22886850702adbd + "@sentry/types": "npm:8.33.1" + "@sentry/utils": "npm:8.33.1" + checksum: 10/dbd781777f5dc003e21680919d37e308a64320776c54a5712163f72d4c0c4d5d25d7f07b83123e517c333fcdefb92ac5a0f15cb4dbbc79f3cc7309038cb0fcbb languageName: node linkType: hard -"@sentry/types@npm:8.19.0": - version: 8.19.0 - resolution: "@sentry/types@npm:8.19.0" - checksum: 10/8812f7394c6c031197abc04d80e5b5b3693742dc065b877c535a9ceb538aabd60ee27fc2b13824e2b8fc264819868109bbd4de3642fd1c7bf30d304fb0c21aa9 +"@sentry/types@npm:8.33.1, @sentry/types@npm:^8.19.0": + version: 8.33.1 + resolution: "@sentry/types@npm:8.33.1" + checksum: 10/bcd7f80e84a23cb810fa5819dc85f45bd62d52b01b1f64a1b31297df21e9d1f4de8f7ea91835c5d6a7010d7dbfc8b09cd708d057d345a6ff685b7f12db41ae57 languageName: node linkType: hard -"@sentry/types@npm:^8.19.0": - version: 8.20.0 - resolution: "@sentry/types@npm:8.20.0" - checksum: 10/c7d7ed17975f0fc0b4bf5aece58084953c2a76e8f417923a476fe1fd42a2c9339c548d701edbc4b938c9252cf680d3eff4c6c2a986bc7ac62649aebf656c5b64 - languageName: node - linkType: hard - -"@sentry/utils@npm:8.19.0, @sentry/utils@npm:^8.19.0": - version: 8.19.0 - resolution: "@sentry/utils@npm:8.19.0" +"@sentry/utils@npm:8.33.1, @sentry/utils@npm:^8.19.0": + version: 8.33.1 + resolution: "@sentry/utils@npm:8.33.1" dependencies: - "@sentry/types": "npm:8.19.0" - checksum: 10/abd507e5b37c7753534865f74a1a622fdbe2d71cfa61fd009703f4c9c90634fb6d26e3b2f8e09904631d4692e3735de451ed914c505c31700a6f5504a61e649e + "@sentry/types": "npm:8.33.1" + checksum: 10/79426deba11c043f0410b4b5d635367147d7e41bb90526168f180ae05598768348de39a82f89a92a4f0365f5ece5f62950ba6eab0b7300faefea7a9bb0889df3 languageName: node linkType: hard @@ -14385,9 +14340,9 @@ __metadata: linkType: hard "caniuse-lite@npm:^1.0.30001109, caniuse-lite@npm:^1.0.30001587, caniuse-lite@npm:^1.0.30001599": - version: 1.0.30001600 - resolution: "caniuse-lite@npm:1.0.30001600" - checksum: 10/4c52f83ed71bc5f6e443bd17923460f1c77915adc2c2aa79ddaedceccc690b5917054b0c41b79e9138cbbd9abcdc0db9e224e79e3e734e581dfec06505f3a2b4 + version: 1.0.30001660 + resolution: "caniuse-lite@npm:1.0.30001660" + checksum: 10/5d83f0b7e2075b7e31f114f739155dc6c21b0afe8cb61180f625a4903b0ccd3d7591a5f81c930f14efddfa57040203ba0890850b8a3738f6c7f17c7dd83b9de8 languageName: node linkType: hard @@ -20804,13 +20759,6 @@ __metadata: languageName: node linkType: hard -"gud@npm:^1.0.0": - version: 1.0.0 - resolution: "gud@npm:1.0.0" - checksum: 10/3e2eb37cf794364077c18f036d6aa259c821c7fd188f2b7935cb00d589d82a41e0ebb1be809e1a93679417f62f1ad0513e745c3cf5329596e489aef8c5e5feae - languageName: node - linkType: hard - "gulp-autoprefixer@npm:^8.0.0": version: 8.0.0 resolution: "gulp-autoprefixer@npm:8.0.0" @@ -21301,12 +21249,12 @@ __metadata: languageName: node linkType: hard -"history@npm:^5.0.0": - version: 5.0.0 - resolution: "history@npm:5.0.0" +"history@npm:^5.3.0": + version: 5.3.0 + resolution: "history@npm:5.3.0" dependencies: "@babel/runtime": "npm:^7.7.6" - checksum: 10/d0b744c2028a163aebcee8df89400d6ed7eadc5ea877b0324040d1127a88d6b39395ea5a5f28a1912c75473953e3782c6fb682d363efb98e87a0cc49de95a2c9 + checksum: 10/52ba685b842ca6438ff11ef459951eb13d413ae715866a8dc5f7c3b1ea0cdeb8db6aabf7254551b85f56abc205e6e2d7e1d5afb36b711b401cdaff4f2cf187e9 languageName: node linkType: hard @@ -25354,6 +25302,13 @@ __metadata: languageName: node linkType: hard +"lottie-web@npm:^5.12.2": + version: 5.12.2 + resolution: "lottie-web@npm:5.12.2" + checksum: 10/cd377d54a675b37ac9359306b84097ea402dff3d74a2f45e6e0dbcff1df94b3a978e92e48fd34765754bdbb94bd2d8d4da31954d95f156e77489596b235cac91 + languageName: node + linkType: hard + "lower-case@npm:^2.0.2": version: 2.0.2 resolution: "lower-case@npm:2.0.2" @@ -26080,8 +26035,8 @@ __metadata: "@metamask-institutional/transaction-update": "npm:^0.2.5" "@metamask-institutional/types": "npm:^1.1.0" "@metamask/abi-utils": "npm:^2.0.2" - "@metamask/account-watcher": "npm:^4.1.0" - "@metamask/accounts-controller": "npm:^18.2.1" + "@metamask/account-watcher": "npm:^4.1.1" + "@metamask/accounts-controller": "npm:^18.2.2" "@metamask/address-book-controller": "npm:^6.0.0" "@metamask/announcement-controller": "npm:^7.0.0" "@metamask/api-specs": "npm:^0.9.3" @@ -26089,7 +26044,7 @@ __metadata: "@metamask/assets-controllers": "npm:^37.0.0" "@metamask/auto-changelog": "npm:^2.1.0" "@metamask/base-controller": "npm:^7.0.0" - "@metamask/bitcoin-wallet-snap": "npm:^0.6.0" + "@metamask/bitcoin-wallet-snap": "npm:^0.6.1" "@metamask/browser-passworder": "npm:^4.3.0" "@metamask/build-utils": "npm:^3.0.0" "@metamask/contract-metadata": "npm:^2.5.0" @@ -26108,9 +26063,9 @@ __metadata: "@metamask/eth-ledger-bridge-keyring": "npm:^3.0.1" "@metamask/eth-query": "npm:^4.0.0" "@metamask/eth-sig-util": "npm:^7.0.1" - "@metamask/eth-snap-keyring": "npm:^4.3.3" + "@metamask/eth-snap-keyring": "npm:^4.3.6" "@metamask/eth-token-tracker": "npm:^8.0.0" - "@metamask/eth-trezor-keyring": "npm:^3.1.0" + "@metamask/eth-trezor-keyring": "npm:^3.1.3" "@metamask/etherscan-link": "npm:^3.0.0" "@metamask/ethjs": "npm:^0.6.0" "@metamask/ethjs-contract": "npm:^0.4.1" @@ -26118,8 +26073,8 @@ __metadata: "@metamask/forwarder": "npm:^1.1.0" "@metamask/gas-fee-controller": "npm:^18.0.0" "@metamask/jazzicon": "npm:^2.0.0" - "@metamask/keyring-api": "npm:^8.1.0" - "@metamask/keyring-controller": "npm:^17.2.1" + "@metamask/keyring-api": "npm:^8.1.3" + "@metamask/keyring-controller": "npm:^17.2.2" "@metamask/logging-controller": "npm:^6.0.0" "@metamask/logo": "npm:^3.1.2" "@metamask/message-manager": "npm:^10.1.0" @@ -26155,7 +26110,7 @@ __metadata: "@metamask/snaps-utils": "npm:^8.1.1" "@metamask/test-bundler": "npm:^1.0.0" "@metamask/test-dapp": "npm:^8.4.0" - "@metamask/transaction-controller": "npm:^37.0.0" + "@metamask/transaction-controller": "npm:^37.2.0" "@metamask/user-operation-controller": "npm:^13.0.0" "@metamask/utils": "npm:^9.1.0" "@ngraveio/bc-ur": "npm:^1.1.12" @@ -26320,7 +26275,7 @@ __metadata: gulp-watch: "npm:^5.0.1" gulp-zip: "npm:^5.1.0" he: "npm:^1.2.0" - history: "npm:^5.0.0" + history: "npm:^5.3.0" html-bundler-webpack-plugin: "npm:^3.17.3" https-browserify: "npm:^1.0.0" human-standard-token-abi: "npm:^2.0.0" @@ -26347,6 +26302,7 @@ __metadata: lodash: "npm:^4.17.21" loglevel: "npm:^1.8.1" loose-envify: "npm:^1.4.0" + lottie-web: "npm:^5.12.2" luxon: "npm:^3.2.1" mocha: "npm:^10.2.0" mocha-junit-reporter: "npm:^2.2.1" @@ -26383,7 +26339,8 @@ __metadata: react-popper: "npm:^2.2.3" react-redux: "npm:^7.2.9" react-responsive-carousel: "npm:^3.2.21" - react-router-dom: "npm:^5.1.2" + react-router-dom: "npm:^5.3.4" + react-router-dom-v5-compat: "npm:^6.26.2" react-simple-file-input: "npm:^2.0.0" react-syntax-highlighter: "npm:^15.5.0" react-tippy: "npm:^1.2.2" @@ -26961,20 +26918,6 @@ __metadata: languageName: node linkType: hard -"mini-create-react-context@npm:^0.3.0": - version: 0.3.2 - resolution: "mini-create-react-context@npm:0.3.2" - dependencies: - "@babel/runtime": "npm:^7.4.0" - gud: "npm:^1.0.0" - tiny-warning: "npm:^1.0.2" - peerDependencies: - prop-types: ^15.0.0 - react: ^0.14.0 || ^15.0.0 || ^16.0.0 - checksum: 10/507e36241965e2dad99ffe191809b0b9dc5e949df03b68000a91a845e12ea3bda8fd4cd35a1f033f3781a72942c7b0208fc1876f37656c7fc7be7d4472f45589 - languageName: node - linkType: hard - "minimalistic-assert@npm:^1.0.0, minimalistic-assert@npm:^1.0.1": version: 1.0.1 resolution: "minimalistic-assert@npm:1.0.1" @@ -30683,32 +30626,46 @@ __metadata: languageName: node linkType: hard -"react-router-dom@npm:^5.1.2": - version: 5.1.2 - resolution: "react-router-dom@npm:5.1.2" +"react-router-dom-v5-compat@npm:^6.26.2": + version: 6.26.2 + resolution: "react-router-dom-v5-compat@npm:6.26.2" dependencies: - "@babel/runtime": "npm:^7.1.2" + "@remix-run/router": "npm:1.19.2" + history: "npm:^5.3.0" + react-router: "npm:6.26.2" + peerDependencies: + react: ">=16.8" + react-dom: ">=16.8" + react-router-dom: 4 || 5 + checksum: 10/0662c16f8fbed2d89b79d7977c94961c331f576bf1c638ba9782656d72a57ab49f081940effc796913870f34a3ebac01287b1bdcb67750b2b04d35e6b59f8180 + languageName: node + linkType: hard + +"react-router-dom@npm:^5.3.4": + version: 5.3.4 + resolution: "react-router-dom@npm:5.3.4" + dependencies: + "@babel/runtime": "npm:^7.12.13" history: "npm:^4.9.0" loose-envify: "npm:^1.3.1" prop-types: "npm:^15.6.2" - react-router: "npm:5.1.2" + react-router: "npm:5.3.4" tiny-invariant: "npm:^1.0.2" tiny-warning: "npm:^1.0.0" peerDependencies: react: ">=15" - checksum: 10/a6225fc454780db6afa5da97ac862abe8514f373a6c81d59a8c4d15c6c42eac0ccce76a468ec0ca216d327e84640561fc12af9759de4e12be09ed7fe1db08bb2 + checksum: 10/5e0696ae2d86f466ff700944758a227e1dcd79b48797d567776506e4e3b4a08b81336155feb86a33be9f38c17c4d3d94212b5c60c8ee9a086022e4fd3961db29 languageName: node linkType: hard -"react-router@npm:5.1.2": - version: 5.1.2 - resolution: "react-router@npm:5.1.2" +"react-router@npm:5.3.4": + version: 5.3.4 + resolution: "react-router@npm:5.3.4" dependencies: - "@babel/runtime": "npm:^7.1.2" + "@babel/runtime": "npm:^7.12.13" history: "npm:^4.9.0" hoist-non-react-statics: "npm:^3.1.0" loose-envify: "npm:^1.3.1" - mini-create-react-context: "npm:^0.3.0" path-to-regexp: "npm:^1.7.0" prop-types: "npm:^15.6.2" react-is: "npm:^16.6.0" @@ -30716,7 +30673,18 @@ __metadata: tiny-warning: "npm:^1.0.0" peerDependencies: react: ">=15" - checksum: 10/bba4a23090fa02364e21e03ad7b2ff4136ff262871be197b3031e4a03180e36bc9f03fc91c060ebbca58e00d4b59d3a99281a6ef26b6dea37479a5097b8ca2e2 + checksum: 10/99d54a99af6bc6d7cad2e5ea7eee9485b62a8b8e16a1182b18daa7fad7dafa5e526850eaeebff629848b297ae055a9cb5b4aba8760e81af8b903efc049d48f5c + languageName: node + linkType: hard + +"react-router@npm:6.26.2": + version: 6.26.2 + resolution: "react-router@npm:6.26.2" + dependencies: + "@remix-run/router": "npm:1.19.2" + peerDependencies: + react: ">=16.8" + checksum: 10/496e855b53e61066c1791e354f5d79eab56a128d9722fdc6486c3ecd3b3a0bf9968e927028f429893b157f3cc10fc09e890a055847723ee242663e7995fedc9d languageName: node linkType: hard