1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-24 19:10:22 +01:00

Merge branch 'develop' of github.com:MetaMask/metamask-extension into minimal

This commit is contained in:
Matthias Kretschmann 2023-07-11 20:55:10 +01:00
commit 52e24c683c
Signed by: m
GPG Key ID: 606EEEF3C479A91F
131 changed files with 2673 additions and 1133 deletions

28
.github/workflows/stale-issues-pr.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '0 12 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@72afbce2b0dbd1d903bb142cebe2d15dc307ae57
with:
stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity in the last 90 days. It will be closed in 45 days. Thank you for your contributions.'
stale-issue-label: 'stale'
only-issue-labels: 'type-bug'
exempt-issue-labels: 'type-security, type-pinned, feature-request, awaiting-metamask'
stale-pr-message: 'This PR has been automatically marked as stale because it has not had recent activity in the last 60 days. It will be closed in 14 days. Thank you for your contributions.'
stale-pr-label: 'stale'
exempt-pr-labels: 'work-in-progress'
close-issue-message: 'This issue was closed because there has been no follow up activity in the last 7 days. If you feel this was closed in error, please reopen and provide evidence on the latest release of the extension. Thank you for your contributions.'
close-pr-message: 'This PR was closed because there has been no follow up activity in the last 7 days. Thank you for your contributions.'
days-before-issue-stale: 90
days-before-pr-stale: 60
days-before-issue-close: 45
days-before-pr-close: 14
operations-per-run: 1

View File

@ -223,3 +223,37 @@ jobs:
HAS_CHANGES: ${{ steps.policy-changes.outputs.HAS_CHANGES }} HAS_CHANGES: ${{ steps.policy-changes.outputs.HAS_CHANGES }}
GITHUB_TOKEN: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }} GITHUB_TOKEN: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }}
PR_NUMBER: ${{ github.event.issue.number }} PR_NUMBER: ${{ github.event.issue.number }}
check-status:
name: Check whether the policy update succeeded
runs-on: ubuntu-latest
needs:
- commit-updated-policies
outputs:
PASSED: ${{ steps.set-output.outputs.PASSED }}
steps:
- name: Set PASSED output
id: set-output
run: echo "PASSED=true" >> "$GITHUB_OUTPUT"
failure-comment:
name: Comment about the policy update failure
if: ${{ always() && needs.is-fork-pull-request.outputs.IS_FORK == 'false' }}
runs-on: ubuntu-latest
needs:
- is-fork-pull-request
- check-status
steps:
- uses: actions/checkout@v3
with:
token: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }}
- name: Post comment if the update failed
run: |
passed="${{ needs.check-status.outputs.PASSED }}"
if [[ $passed != "true" ]]; then
gh pr comment "${PR_NUMBER}" --body "Policy update failed. You can [review the logs or retry the policy update here](${ACTION_RUN_URL})"
fi
env:
GITHUB_TOKEN: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }}
PR_NUMBER: ${{ github.event.issue.number }}
ACTION_RUN_URL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"

9
.iyarc
View File

@ -3,6 +3,9 @@ GHSA-257v-vj4p-3w2h
# request library is subject to SSRF. # request library is subject to SSRF.
# addressed by temporary patch in .yarn/patches/request-npm-2.88.2-f4a57c72c4.patch # addressed by temporary patch in .yarn/patches/request-npm-2.88.2-f4a57c72c4.patch
# NOTE: Disabled for now since this library seems to have been removed from the GHSA-p8p7-x288-28g6
# dependency tree — we can re-enable this line later if it appears again
#GHSA-p8p7-x288-28g6 # Prototype pollution
# Not easily patched
# Minimal risk to us because we're using lockdown which also prevents this case of prototype pollution
GHSA-h755-8qp9-cq85

View File

@ -0,0 +1,17 @@
diff --git a/dist/SignatureController.js b/dist/SignatureController.js
index b58b27e84aa84393afb366d4585c084d0380d21d..0629bcf517db744ccfa40e4d7d8f2829fa95559e 100644
--- a/dist/SignatureController.js
+++ b/dist/SignatureController.js
@@ -237,8 +237,11 @@ _SignatureController_keyringController = new WeakMap(), _SignatureController_isE
yield __classPrivateFieldGet(this, _SignatureController_instances, "m", _SignatureController_requestApproval).call(this, messageParamsWithId, approvalType);
}
catch (error) {
+ signaturePromise.catch(() => {
+ // Expecting reject error but throwing manually rather than waiting
+ });
__classPrivateFieldGet(this, _SignatureController_instances, "m", _SignatureController_cancelAbstractMessage).call(this, messageManager, messageId);
- throw eth_rpc_errors_1.ethErrors.provider.userRejectedRequest('User rejected the request.');
+ throw eth_rpc_errors_1.ethErrors.provider.userRejectedRequest(`MetaMask ${messageName} Signature: User denied message signature.`);
}
yield signMessage(messageParamsWithId, version, signingOpts);
return signaturePromise;

View File

@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [10.33.1]
### Fixed
- Fix to bug causing users to see an infinite spinner when signing typed messages. ([#19894](https://github.com/MetaMask/metamask-extension/pull/19894))
## [10.33.0] ## [10.33.0]
### Added ### Added
- UI Upgrade ([#18903](https://github.com/MetaMask/metamask-extension/pull/18903)) - UI Upgrade ([#18903](https://github.com/MetaMask/metamask-extension/pull/18903))
@ -3825,7 +3829,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Uncategorized ### Uncategorized
- Added the ability to restore accounts from seed words. - Added the ability to restore accounts from seed words.
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.33.0...HEAD [Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.33.1...HEAD
[10.33.1]: https://github.com/MetaMask/metamask-extension/compare/v10.33.0...v10.33.1
[10.33.0]: https://github.com/MetaMask/metamask-extension/compare/v10.32.0...v10.33.0 [10.33.0]: https://github.com/MetaMask/metamask-extension/compare/v10.32.0...v10.33.0
[10.32.0]: https://github.com/MetaMask/metamask-extension/compare/v10.31.1...v10.32.0 [10.32.0]: https://github.com/MetaMask/metamask-extension/compare/v10.31.1...v10.32.0
[10.31.1]: https://github.com/MetaMask/metamask-extension/compare/v10.31.0...v10.31.1 [10.31.1]: https://github.com/MetaMask/metamask-extension/compare/v10.31.0...v10.31.1

View File

@ -2956,9 +2956,6 @@
"revealTheSeedPhrase": { "revealTheSeedPhrase": {
"message": "Seed-Phrase anzeigen" "message": "Seed-Phrase anzeigen"
}, },
"reviewSpendingCap": {
"message": "Überprüfen SIe Ihre Ausgabegrenze"
},
"revokeAllTokensTitle": { "revokeAllTokensTitle": {
"message": "Erlaubnis zum Zugriff auf alle Ihre $1 sowie deren Übertragung entziehen?", "message": "Erlaubnis zum Zugriff auf alle Ihre $1 sowie deren Übertragung entziehen?",
"description": "$1 is the symbol of the token for which the user is revoking approval" "description": "$1 is the symbol of the token for which the user is revoking approval"
@ -3140,10 +3137,6 @@
"message": "$1 ohne Ausgabenlimit genehmigen", "message": "$1 ohne Ausgabenlimit genehmigen",
"description": "The token symbol that is being approved" "description": "The token symbol that is being approved"
}, },
"setSpendingCap": {
"message": "Eine Ausgabegrenze für Ihr $1 einrichten",
"description": "$1 is a token symbol"
},
"settings": { "settings": {
"message": "Einstellungen" "message": "Einstellungen"
}, },
@ -4167,9 +4160,6 @@
"urlExistsErrorMsg": { "urlExistsErrorMsg": {
"message": "Diese URL wird derzeit vom $1-Netzwerk verwendet." "message": "Diese URL wird derzeit vom $1-Netzwerk verwendet."
}, },
"useDefault": {
"message": "Standardeinstellungen verwenden"
},
"useMultiAccountBalanceChecker": { "useMultiAccountBalanceChecker": {
"message": "Kontoguthaben-Anfragen sammeln" "message": "Kontoguthaben-Anfragen sammeln"
}, },

View File

@ -2956,9 +2956,6 @@
"revealTheSeedPhrase": { "revealTheSeedPhrase": {
"message": "Αποκάλυψη φράσης ανάκτησης" "message": "Αποκάλυψη φράσης ανάκτησης"
}, },
"reviewSpendingCap": {
"message": "Επανεξετάστε το όριο δαπανών σας"
},
"revokeAllTokensTitle": { "revokeAllTokensTitle": {
"message": "Ανάκληση άδειας πρόσβασης σε όλα σας τα $1;", "message": "Ανάκληση άδειας πρόσβασης σε όλα σας τα $1;",
"description": "$1 is the symbol of the token for which the user is revoking approval" "description": "$1 is the symbol of the token for which the user is revoking approval"
@ -3137,10 +3134,6 @@
"message": "Έγκριση $1 χωρίς όριο δαπανών", "message": "Έγκριση $1 χωρίς όριο δαπανών",
"description": "The token symbol that is being approved" "description": "The token symbol that is being approved"
}, },
"setSpendingCap": {
"message": "Ορίστε ένα ανώτατο όριο δαπανών για το $1",
"description": "$1 is a token symbol"
},
"settings": { "settings": {
"message": "Ρυθμίσεις" "message": "Ρυθμίσεις"
}, },
@ -4164,9 +4157,6 @@
"urlExistsErrorMsg": { "urlExistsErrorMsg": {
"message": "Αυτό το URL χρησιμοποιείται επί του παρόντος από το δίκτυο $1." "message": "Αυτό το URL χρησιμοποιείται επί του παρόντος από το δίκτυο $1."
}, },
"useDefault": {
"message": "Χρήση προκαθορισμένου"
},
"useMultiAccountBalanceChecker": { "useMultiAccountBalanceChecker": {
"message": "Μαζικά αιτήματα υπολοίπου λογαριασμού" "message": "Μαζικά αιτήματα υπολοίπου λογαριασμού"
}, },

View File

@ -366,6 +366,9 @@
"allowThisSiteTo": { "allowThisSiteTo": {
"message": "Allow this site to:" "message": "Allow this site to:"
}, },
"allowThisSnapTo": {
"message": "Allow this snap to:"
},
"allowWithdrawAndSpend": { "allowWithdrawAndSpend": {
"message": "Allow $1 to withdraw and spend up to the following amount:", "message": "Allow $1 to withdraw and spend up to the following amount:",
"description": "The url of the site that requested permission to 'withdraw and spend'" "description": "The url of the site that requested permission to 'withdraw and spend'"
@ -1052,6 +1055,9 @@
"customerSupport": { "customerSupport": {
"message": "customer support" "message": "customer support"
}, },
"dappRequestedSpendingCap": {
"message": "Site requested spending cap"
},
"dappSuggested": { "dappSuggested": {
"message": "Site suggested" "message": "Site suggested"
}, },
@ -3541,9 +3547,6 @@
"revealTheSeedPhrase": { "revealTheSeedPhrase": {
"message": "Reveal seed phrase" "message": "Reveal seed phrase"
}, },
"reviewSpendingCap": {
"message": "Review the spending cap for your"
},
"revokeAllTokensTitle": { "revokeAllTokensTitle": {
"message": "Revoke permission to access and transfer all of your $1?", "message": "Revoke permission to access and transfer all of your $1?",
"description": "$1 is the symbol of the token for which the user is revoking approval" "description": "$1 is the symbol of the token for which the user is revoking approval"
@ -3560,6 +3563,9 @@
"message": "This revokes the permission for a third party to access and transfer all of your NFTs from $1 without further notice.", "message": "This revokes the permission for a third party to access and transfer all of your NFTs from $1 without further notice.",
"description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name" "description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name"
}, },
"revokePermission": {
"message": "Revoke permission"
},
"revokeSpendingCap": { "revokeSpendingCap": {
"message": "Revoke spending cap for your $1", "message": "Revoke spending cap for your $1",
"description": "$1 is a token symbol" "description": "$1 is a token symbol"
@ -3681,6 +3687,9 @@
"selectAccounts": { "selectAccounts": {
"message": "Select the account(s) to use on this site" "message": "Select the account(s) to use on this site"
}, },
"selectAccountsForSnap": {
"message": "Select the account(s) to use with this snap"
},
"selectAll": { "selectAll": {
"message": "Select all" "message": "Select all"
}, },
@ -3757,10 +3766,6 @@
"message": "Approve $1 with no spend limit", "message": "Approve $1 with no spend limit",
"description": "The token symbol that is being approved" "description": "The token symbol that is being approved"
}, },
"setSpendingCap": {
"message": "Set a spending cap for your $1",
"description": "$1 is a token symbol"
},
"settingAddSnapAccount": { "settingAddSnapAccount": {
"message": "Add snap account" "message": "Add snap account"
}, },
@ -3770,10 +3775,6 @@
"settingsSearchMatchingNotFound": { "settingsSearchMatchingNotFound": {
"message": "No matching results found." "message": "No matching results found."
}, },
"shortVersion": {
"message": "v$1",
"description": "$1 is the version number to show"
},
"show": { "show": {
"message": "Show" "message": "Show"
}, },
@ -4087,6 +4088,9 @@
"message": "Only enter a number that you're comfortable with $1 accessing now or in the future. You can always increase the token limit later.", "message": "Only enter a number that you're comfortable with $1 accessing now or in the future. You can always increase the token limit later.",
"description": "$1 is origin of the site requesting the token limit" "description": "$1 is origin of the site requesting the token limit"
}, },
"spendingCapRequest": {
"message": "Spending cap request for your $1"
},
"srpInputNumberOfWords": { "srpInputNumberOfWords": {
"message": "I have a $1-word phrase", "message": "I have a $1-word phrase",
"description": "This is the text for each option in the dropdown where a user selects how many words their secret recovery phrase has during import. The $1 is the number of words (either 12, 15, 18, 21, or 24)." "description": "This is the text for each option in the dropdown where a user selects how many words their secret recovery phrase has during import. The $1 is the number of words (either 12, 15, 18, 21, or 24)."
@ -5136,9 +5140,6 @@
"urlExistsErrorMsg": { "urlExistsErrorMsg": {
"message": "This URL is currently used by the $1 network." "message": "This URL is currently used by the $1 network."
}, },
"useDefault": {
"message": "Use default"
},
"useMultiAccountBalanceChecker": { "useMultiAccountBalanceChecker": {
"message": "Batch account balance requests" "message": "Batch account balance requests"
}, },
@ -5169,6 +5170,9 @@
"usePhishingDetectionDescription": { "usePhishingDetectionDescription": {
"message": "Display a warning for phishing domains targeting Ethereum users" "message": "Display a warning for phishing domains targeting Ethereum users"
}, },
"useSiteSuggestion": {
"message": "Use site suggestion"
},
"useTokenDetectionPrivacyDesc": { "useTokenDetectionPrivacyDesc": {
"message": "Automatically displaying tokens sent to your account involves communication with third party servers to fetch tokens images. Those serves will have access to your IP address." "message": "Automatically displaying tokens sent to your account involves communication with third party servers to fetch tokens images. Those serves will have access to your IP address."
}, },

View File

@ -2956,9 +2956,6 @@
"revealTheSeedPhrase": { "revealTheSeedPhrase": {
"message": "Revelar frase semilla" "message": "Revelar frase semilla"
}, },
"reviewSpendingCap": {
"message": "Revise su límite de gasto"
},
"revokeAllTokensTitle": { "revokeAllTokensTitle": {
"message": "¿Revocar el permiso para acceder y transferir todos sus $1?", "message": "¿Revocar el permiso para acceder y transferir todos sus $1?",
"description": "$1 is the symbol of the token for which the user is revoking approval" "description": "$1 is the symbol of the token for which the user is revoking approval"
@ -3140,10 +3137,6 @@
"message": "Aprobar $1 sin límite preestablecido", "message": "Aprobar $1 sin límite preestablecido",
"description": "The token symbol that is being approved" "description": "The token symbol that is being approved"
}, },
"setSpendingCap": {
"message": "Establecer un límite de gasto para su $1",
"description": "$1 is a token symbol"
},
"settings": { "settings": {
"message": "Configuración" "message": "Configuración"
}, },
@ -4167,9 +4160,6 @@
"urlExistsErrorMsg": { "urlExistsErrorMsg": {
"message": "En este momento, la red $1 está utilizando esta dirección URL." "message": "En este momento, la red $1 está utilizando esta dirección URL."
}, },
"useDefault": {
"message": "Uso por defecto"
},
"useMultiAccountBalanceChecker": { "useMultiAccountBalanceChecker": {
"message": "Solicitudes de saldo de cuenta por lotes" "message": "Solicitudes de saldo de cuenta por lotes"
}, },

View File

@ -2956,9 +2956,6 @@
"revealTheSeedPhrase": { "revealTheSeedPhrase": {
"message": "Révéler la phrase mnémonique" "message": "Révéler la phrase mnémonique"
}, },
"reviewSpendingCap": {
"message": "Modifiez votre plafond de dépenses"
},
"revokeAllTokensTitle": { "revokeAllTokensTitle": {
"message": "Révoquer lautorisation daccès et de transfert de tous vos $1 ?", "message": "Révoquer lautorisation daccès et de transfert de tous vos $1 ?",
"description": "$1 is the symbol of the token for which the user is revoking approval" "description": "$1 is the symbol of the token for which the user is revoking approval"
@ -3140,10 +3137,6 @@
"message": "Approuver $1 sans limite de dépenses", "message": "Approuver $1 sans limite de dépenses",
"description": "The token symbol that is being approved" "description": "The token symbol that is being approved"
}, },
"setSpendingCap": {
"message": "Fixez un plafond de dépenses pour vos $1",
"description": "$1 is a token symbol"
},
"settings": { "settings": {
"message": "Paramètres" "message": "Paramètres"
}, },
@ -4167,9 +4160,6 @@
"urlExistsErrorMsg": { "urlExistsErrorMsg": {
"message": "Cette URL est actuellement utilisée par le réseau $1." "message": "Cette URL est actuellement utilisée par le réseau $1."
}, },
"useDefault": {
"message": "Utiliser par défaut"
},
"useMultiAccountBalanceChecker": { "useMultiAccountBalanceChecker": {
"message": "Demandes dinformations concernant le solde de plusieurs comptes" "message": "Demandes dinformations concernant le solde de plusieurs comptes"
}, },

View File

@ -2956,9 +2956,6 @@
"revealTheSeedPhrase": { "revealTheSeedPhrase": {
"message": "सीड फ़्रेज़ दिखाएं" "message": "सीड फ़्रेज़ दिखाएं"
}, },
"reviewSpendingCap": {
"message": "अपनी खर्च के सीमा की समीक्षा करें"
},
"revokeAllTokensTitle": { "revokeAllTokensTitle": {
"message": "आपके सभी $1 को एक्सेस और स्थानांतरित करने की अनुमति निरस्त करें?", "message": "आपके सभी $1 को एक्सेस और स्थानांतरित करने की अनुमति निरस्त करें?",
"description": "$1 is the symbol of the token for which the user is revoking approval" "description": "$1 is the symbol of the token for which the user is revoking approval"
@ -3140,10 +3137,6 @@
"message": "बिना किसी खर्च सीमा के $1 स्वीकृत करें", "message": "बिना किसी खर्च सीमा के $1 स्वीकृत करें",
"description": "The token symbol that is being approved" "description": "The token symbol that is being approved"
}, },
"setSpendingCap": {
"message": "अपने $1 के लिए खर्च की सीमा को निर्धारित करें",
"description": "$1 is a token symbol"
},
"settings": { "settings": {
"message": "सेटिंग" "message": "सेटिंग"
}, },
@ -4167,9 +4160,6 @@
"urlExistsErrorMsg": { "urlExistsErrorMsg": {
"message": "यह URL वर्तमान में $1 नेटवर्क द्वारा उपयोग किया जाता है।" "message": "यह URL वर्तमान में $1 नेटवर्क द्वारा उपयोग किया जाता है।"
}, },
"useDefault": {
"message": "डिफॉल्ट का उपयोग करें"
},
"useMultiAccountBalanceChecker": { "useMultiAccountBalanceChecker": {
"message": "खाता के शेष राशि के अनुरोधों को बैच करें" "message": "खाता के शेष राशि के अनुरोधों को बैच करें"
}, },

View File

@ -2956,9 +2956,6 @@
"revealTheSeedPhrase": { "revealTheSeedPhrase": {
"message": "Ungkap frasa seed" "message": "Ungkap frasa seed"
}, },
"reviewSpendingCap": {
"message": "Tinjau batas pengeluaran Anda"
},
"revokeAllTokensTitle": { "revokeAllTokensTitle": {
"message": "Cabut izin untuk mengakses dan mentransfer seluruh $1 Anda?", "message": "Cabut izin untuk mengakses dan mentransfer seluruh $1 Anda?",
"description": "$1 is the symbol of the token for which the user is revoking approval" "description": "$1 is the symbol of the token for which the user is revoking approval"
@ -3140,10 +3137,6 @@
"message": "Setujui $1 tanpa batas penggunaan", "message": "Setujui $1 tanpa batas penggunaan",
"description": "The token symbol that is being approved" "description": "The token symbol that is being approved"
}, },
"setSpendingCap": {
"message": "Tetapkan batas pengeluaran untuk $1",
"description": "$1 is a token symbol"
},
"settings": { "settings": {
"message": "Pengaturan" "message": "Pengaturan"
}, },
@ -4167,9 +4160,6 @@
"urlExistsErrorMsg": { "urlExistsErrorMsg": {
"message": "URL ini saat ini digunakan oleh jaringan $1." "message": "URL ini saat ini digunakan oleh jaringan $1."
}, },
"useDefault": {
"message": "Gunakan default"
},
"useMultiAccountBalanceChecker": { "useMultiAccountBalanceChecker": {
"message": "Kelompokkan permintaan saldo akun" "message": "Kelompokkan permintaan saldo akun"
}, },

View File

@ -2956,9 +2956,6 @@
"revealTheSeedPhrase": { "revealTheSeedPhrase": {
"message": "シードフレーズを表示" "message": "シードフレーズを表示"
}, },
"reviewSpendingCap": {
"message": "使用上限を確認してください"
},
"revokeAllTokensTitle": { "revokeAllTokensTitle": {
"message": "すべての $1 へのアクセスおよび転送許可を取り消しますか?", "message": "すべての $1 へのアクセスおよび転送許可を取り消しますか?",
"description": "$1 is the symbol of the token for which the user is revoking approval" "description": "$1 is the symbol of the token for which the user is revoking approval"
@ -3140,10 +3137,6 @@
"message": "使用限度額なしで $1 を承認", "message": "使用限度額なしで $1 を承認",
"description": "The token symbol that is being approved" "description": "The token symbol that is being approved"
}, },
"setSpendingCap": {
"message": "$1 の使用上限を設定",
"description": "$1 is a token symbol"
},
"settings": { "settings": {
"message": "設定" "message": "設定"
}, },
@ -4167,9 +4160,6 @@
"urlExistsErrorMsg": { "urlExistsErrorMsg": {
"message": "このURLは現在$1ネットワークで使用されています。" "message": "このURLは現在$1ネットワークで使用されています。"
}, },
"useDefault": {
"message": "既定値を使用"
},
"useMultiAccountBalanceChecker": { "useMultiAccountBalanceChecker": {
"message": "アカウント残高の一括リクエスト" "message": "アカウント残高の一括リクエスト"
}, },

View File

@ -2956,9 +2956,6 @@
"revealTheSeedPhrase": { "revealTheSeedPhrase": {
"message": "시드 구문 보기" "message": "시드 구문 보기"
}, },
"reviewSpendingCap": {
"message": "지출 한도 검토"
},
"revokeAllTokensTitle": { "revokeAllTokensTitle": {
"message": "모든 $1에 액세스할 수 있는 권한을 철회할까요?", "message": "모든 $1에 액세스할 수 있는 권한을 철회할까요?",
"description": "$1 is the symbol of the token for which the user is revoking approval" "description": "$1 is the symbol of the token for which the user is revoking approval"
@ -3140,10 +3137,6 @@
"message": "$1 무제한 지출 승인", "message": "$1 무제한 지출 승인",
"description": "The token symbol that is being approved" "description": "The token symbol that is being approved"
}, },
"setSpendingCap": {
"message": "$1에 대한 지출 한도 설정",
"description": "$1 is a token symbol"
},
"settings": { "settings": {
"message": "설정" "message": "설정"
}, },
@ -4167,9 +4160,6 @@
"urlExistsErrorMsg": { "urlExistsErrorMsg": {
"message": "이 URL은 현재 $1 네트워크에서 사용됩니다." "message": "이 URL은 현재 $1 네트워크에서 사용됩니다."
}, },
"useDefault": {
"message": "기본값 사용"
},
"useMultiAccountBalanceChecker": { "useMultiAccountBalanceChecker": {
"message": "일괄 계정 잔액 요청" "message": "일괄 계정 잔액 요청"
}, },

View File

@ -2956,9 +2956,6 @@
"revealTheSeedPhrase": { "revealTheSeedPhrase": {
"message": "Revelar a frase de recuperação" "message": "Revelar a frase de recuperação"
}, },
"reviewSpendingCap": {
"message": "Revise seu limite de gastos"
},
"revokeAllTokensTitle": { "revokeAllTokensTitle": {
"message": "Revogar permissão de acesso e transferência de todos os seus $1?", "message": "Revogar permissão de acesso e transferência de todos os seus $1?",
"description": "$1 is the symbol of the token for which the user is revoking approval" "description": "$1 is the symbol of the token for which the user is revoking approval"
@ -3140,10 +3137,6 @@
"message": "Aprovar $1 sem limite de gastos", "message": "Aprovar $1 sem limite de gastos",
"description": "The token symbol that is being approved" "description": "The token symbol that is being approved"
}, },
"setSpendingCap": {
"message": "Definir um limite de gastos para seu $1",
"description": "$1 is a token symbol"
},
"settings": { "settings": {
"message": "Definições" "message": "Definições"
}, },
@ -4167,9 +4160,6 @@
"urlExistsErrorMsg": { "urlExistsErrorMsg": {
"message": "O ID da cadeia está sendo usado pela rede $1." "message": "O ID da cadeia está sendo usado pela rede $1."
}, },
"useDefault": {
"message": "Usar padrão"
},
"useMultiAccountBalanceChecker": { "useMultiAccountBalanceChecker": {
"message": "Agrupar solicitações de saldo de contas" "message": "Agrupar solicitações de saldo de contas"
}, },

View File

@ -2956,9 +2956,6 @@
"revealTheSeedPhrase": { "revealTheSeedPhrase": {
"message": "Показать сид-фразу" "message": "Показать сид-фразу"
}, },
"reviewSpendingCap": {
"message": "Проверьте свой лимит расходов"
},
"revokeAllTokensTitle": { "revokeAllTokensTitle": {
"message": "Отозвать разрешение на доступ ко всем вашим $1 и их перевод?", "message": "Отозвать разрешение на доступ ко всем вашим $1 и их перевод?",
"description": "$1 is the symbol of the token for which the user is revoking approval" "description": "$1 is the symbol of the token for which the user is revoking approval"
@ -3140,10 +3137,6 @@
"message": "Одобрить $1 без ограничений по расходам", "message": "Одобрить $1 без ограничений по расходам",
"description": "The token symbol that is being approved" "description": "The token symbol that is being approved"
}, },
"setSpendingCap": {
"message": "Установить верхний лимит расходов для вашего $1",
"description": "$1 is a token symbol"
},
"settings": { "settings": {
"message": "Настройки" "message": "Настройки"
}, },
@ -4167,9 +4160,6 @@
"urlExistsErrorMsg": { "urlExistsErrorMsg": {
"message": "Это URL-адрес в настоящее время используется сетью $1." "message": "Это URL-адрес в настоящее время используется сетью $1."
}, },
"useDefault": {
"message": "Использовать значения по умолчанию"
},
"useMultiAccountBalanceChecker": { "useMultiAccountBalanceChecker": {
"message": "Пакетные запросы баланса счета" "message": "Пакетные запросы баланса счета"
}, },

View File

@ -2956,9 +2956,6 @@
"revealTheSeedPhrase": { "revealTheSeedPhrase": {
"message": "Ipakita ang seed phrase" "message": "Ipakita ang seed phrase"
}, },
"reviewSpendingCap": {
"message": "I-review ang iyong limitasyon sa paggastos"
},
"revokeAllTokensTitle": { "revokeAllTokensTitle": {
"message": "Bawiin ang pahintulot na i-access at ilipat ang lahat ng iyong $1?", "message": "Bawiin ang pahintulot na i-access at ilipat ang lahat ng iyong $1?",
"description": "$1 is the symbol of the token for which the user is revoking approval" "description": "$1 is the symbol of the token for which the user is revoking approval"
@ -3140,10 +3137,6 @@
"message": "Aprubahan ang $1 nang walang limitasyon sa paggastos", "message": "Aprubahan ang $1 nang walang limitasyon sa paggastos",
"description": "The token symbol that is being approved" "description": "The token symbol that is being approved"
}, },
"setSpendingCap": {
"message": "Magtakda ng limitasyon sa paggastos para sa iyong $1",
"description": "$1 is a token symbol"
},
"settings": { "settings": {
"message": "Mga Setting" "message": "Mga Setting"
}, },
@ -4167,9 +4160,6 @@
"urlExistsErrorMsg": { "urlExistsErrorMsg": {
"message": "Nasa kasalukuyang listahan ng mga network na ang URL." "message": "Nasa kasalukuyang listahan ng mga network na ang URL."
}, },
"useDefault": {
"message": "Gamitin ang default"
},
"useMultiAccountBalanceChecker": { "useMultiAccountBalanceChecker": {
"message": "Mga kahilingan sa balanse ng batch account" "message": "Mga kahilingan sa balanse ng batch account"
}, },

View File

@ -2956,9 +2956,6 @@
"revealTheSeedPhrase": { "revealTheSeedPhrase": {
"message": "Tohum ifadesini ortaya çıkar" "message": "Tohum ifadesini ortaya çıkar"
}, },
"reviewSpendingCap": {
"message": "Harcama üst limitinize göz atın"
},
"revokeAllTokensTitle": { "revokeAllTokensTitle": {
"message": "Tüm $1 için izin erişim ve transfer izni geri çekilsin mi?", "message": "Tüm $1 için izin erişim ve transfer izni geri çekilsin mi?",
"description": "$1 is the symbol of the token for which the user is revoking approval" "description": "$1 is the symbol of the token for which the user is revoking approval"
@ -3140,10 +3137,6 @@
"message": "$1 için harcama limiti olmadan onay ver", "message": "$1 için harcama limiti olmadan onay ver",
"description": "The token symbol that is being approved" "description": "The token symbol that is being approved"
}, },
"setSpendingCap": {
"message": "$1 için harcama üst limiti belirle",
"description": "$1 is a token symbol"
},
"settings": { "settings": {
"message": "Ayarlar" "message": "Ayarlar"
}, },
@ -4167,9 +4160,6 @@
"urlExistsErrorMsg": { "urlExistsErrorMsg": {
"message": "Bu URL şu anda $1 ağı tarafından kullanılıyor." "message": "Bu URL şu anda $1 ağı tarafından kullanılıyor."
}, },
"useDefault": {
"message": "Varsayılanı kullan"
},
"useMultiAccountBalanceChecker": { "useMultiAccountBalanceChecker": {
"message": "Toplu hesap bakiyesi talepleri" "message": "Toplu hesap bakiyesi talepleri"
}, },

View File

@ -2956,9 +2956,6 @@
"revealTheSeedPhrase": { "revealTheSeedPhrase": {
"message": "Hiện cụm từ khôi phục bí mật" "message": "Hiện cụm từ khôi phục bí mật"
}, },
"reviewSpendingCap": {
"message": "Xem lại hạn mức chi tiêu của bạn"
},
"revokeAllTokensTitle": { "revokeAllTokensTitle": {
"message": "Thu hồi quyền truy cập và chuyển tất cả $1 của bạn?", "message": "Thu hồi quyền truy cập và chuyển tất cả $1 của bạn?",
"description": "$1 is the symbol of the token for which the user is revoking approval" "description": "$1 is the symbol of the token for which the user is revoking approval"
@ -3140,10 +3137,6 @@
"message": "Phê duyệt $1 không có giới hạn chi tiêu", "message": "Phê duyệt $1 không có giới hạn chi tiêu",
"description": "The token symbol that is being approved" "description": "The token symbol that is being approved"
}, },
"setSpendingCap": {
"message": "Đặt hạn mức chi tiêu cho $1 của bạn",
"description": "$1 is a token symbol"
},
"settings": { "settings": {
"message": "Cài đặt" "message": "Cài đặt"
}, },
@ -4167,9 +4160,6 @@
"urlExistsErrorMsg": { "urlExistsErrorMsg": {
"message": "Mạng $1 hiện đang sử dụng URL này." "message": "Mạng $1 hiện đang sử dụng URL này."
}, },
"useDefault": {
"message": "Sử dụng mặc định"
},
"useMultiAccountBalanceChecker": { "useMultiAccountBalanceChecker": {
"message": "Xử lý hàng loạt yêu cầu số dư tài khoản" "message": "Xử lý hàng loạt yêu cầu số dư tài khoản"
}, },

View File

@ -2956,9 +2956,6 @@
"revealTheSeedPhrase": { "revealTheSeedPhrase": {
"message": "显示助记词" "message": "显示助记词"
}, },
"reviewSpendingCap": {
"message": "检查您的支出上限"
},
"revokeAllTokensTitle": { "revokeAllTokensTitle": {
"message": "撤销访问和转移您的所有 $1 的权限?", "message": "撤销访问和转移您的所有 $1 的权限?",
"description": "$1 is the symbol of the token for which the user is revoking approval" "description": "$1 is the symbol of the token for which the user is revoking approval"
@ -3140,10 +3137,6 @@
"message": "批准$1且无消费限制", "message": "批准$1且无消费限制",
"description": "The token symbol that is being approved" "description": "The token symbol that is being approved"
}, },
"setSpendingCap": {
"message": "为 $1 设置支出上限",
"description": "$1 is a token symbol"
},
"settings": { "settings": {
"message": "设置" "message": "设置"
}, },
@ -4167,9 +4160,6 @@
"urlExistsErrorMsg": { "urlExistsErrorMsg": {
"message": "此 URL 目前已被 $1 网络使用。" "message": "此 URL 目前已被 $1 网络使用。"
}, },
"useDefault": {
"message": "使用默认值"
},
"useMultiAccountBalanceChecker": { "useMultiAccountBalanceChecker": {
"message": "账户余额分批请求" "message": "账户余额分批请求"
}, },

View File

@ -526,7 +526,8 @@ export default class PreferencesController {
async updateSnapRegistry() { async updateSnapRegistry() {
let snapRegistry; let snapRegistry;
try { try {
snapRegistry = await fetch(KEYRING_SNAPS_REGISTRY_URL); const response = await fetch(KEYRING_SNAPS_REGISTRY_URL);
snapRegistry = await response.json();
} catch (error) { } catch (error) {
console.error(`Failed to fetch registry: `, error); console.error(`Failed to fetch registry: `, error);
snapRegistry = {}; snapRegistry = {};

View File

@ -1305,6 +1305,8 @@ export default class TransactionController extends EventEmitter {
txMeta.custodyId, txMeta.custodyId,
fromAddress, fromAddress,
); );
return null;
} }
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN

View File

@ -315,31 +315,42 @@ export default class MetamaskController extends EventEmitter {
], ],
}); });
let initialProviderConfig; let initialNetworkControllerState = {};
if (process.env.IN_TEST) { if (initState.NetworkController) {
initialProviderConfig = { initialNetworkControllerState = initState.NetworkController;
type: NETWORK_TYPES.RPC, } else if (process.env.IN_TEST) {
rpcUrl: 'http://localhost:8545', initialNetworkControllerState = {
chainId: '0x539', providerConfig: {
nickname: 'Localhost 8545', chainId: CHAIN_IDS.LOCALHOST,
ticker: 'ETH', nickname: 'Localhost 8545',
rpcPrefs: {},
rpcUrl: 'http://localhost:8545',
ticker: 'ETH',
type: 'rpc',
},
networkConfigurations: {
networkConfigurationId: {
chainId: CHAIN_IDS.LOCALHOST,
nickname: 'Localhost 8545',
rpcPrefs: {},
rpcUrl: 'http://localhost:8545',
ticker: 'ETH',
networkConfigurationId: 'networkConfigurationId',
},
},
}; };
} else if ( } else if (
process.env.METAMASK_DEBUG || process.env.METAMASK_DEBUG ||
process.env.METAMASK_ENVIRONMENT === 'test' process.env.METAMASK_ENVIRONMENT === 'test'
) { ) {
initialProviderConfig = { initialNetworkControllerState = {
type: NETWORK_TYPES.GOERLI, providerConfig: {
chainId: CHAIN_IDS.GOERLI, type: NETWORK_TYPES.GOERLI,
ticker: TEST_NETWORK_TICKER_MAP[NETWORK_TYPES.GOERLI], chainId: CHAIN_IDS.GOERLI,
ticker: TEST_NETWORK_TICKER_MAP[NETWORK_TYPES.GOERLI],
},
}; };
} }
const initialNetworkControllerState = initialProviderConfig
? {
providerConfig: initialProviderConfig,
...initState.NetworkController,
}
: initState.NetworkController;
this.networkController = new NetworkController({ this.networkController = new NetworkController({
messenger: networkControllerMessenger, messenger: networkControllerMessenger,
state: initialNetworkControllerState, state: initialNetworkControllerState,
@ -568,8 +579,12 @@ export default class MetamaskController extends EventEmitter {
), ),
getCurrentAccountEIP1559Compatibility: getCurrentAccountEIP1559Compatibility:
this.getCurrentAccountEIP1559Compatibility.bind(this), this.getCurrentAccountEIP1559Compatibility.bind(this),
legacyAPIEndpoint: `${gasApiBaseUrl}/networks/<chain_id>/gasPrices`,
EIP1559APIEndpoint: `${gasApiBaseUrl}/networks/<chain_id>/suggestedGasFees`, EIP1559APIEndpoint: `${gasApiBaseUrl}/networks/<chain_id>/suggestedGasFees`,
getCurrentNetworkLegacyGasAPICompatibility: () => false, getCurrentNetworkLegacyGasAPICompatibility: () => {
const { chainId } = this.networkController.state.providerConfig;
return chainId === CHAIN_IDS.BSC;
},
getChainId: () => this.networkController.state.providerConfig.chainId, getChainId: () => this.networkController.state.providerConfig.chainId,
}); });
@ -1467,6 +1482,18 @@ export default class MetamaskController extends EventEmitter {
this.signatureController.newUnsignedPersonalMessage.bind( this.signatureController.newUnsignedPersonalMessage.bind(
this.signatureController, this.signatureController,
), ),
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
setTypedMessageInProgress:
this.signatureController.setTypedMessageInProgress.bind(
this.signatureController,
),
setPersonalMessageInProgress:
this.signatureController.setPersonalMessageInProgress.bind(
this.signatureController,
),
///: END:ONLY_INCLUDE_IN
processEncryptionPublicKey: processEncryptionPublicKey:
this.encryptionPublicKeyController.newRequestEncryptionPublicKey.bind( this.encryptionPublicKeyController.newRequestEncryptionPublicKey.bind(
this.encryptionPublicKeyController, this.encryptionPublicKeyController,
@ -2468,6 +2495,10 @@ export default class MetamaskController extends EventEmitter {
this.controllerMessenger, this.controllerMessenger,
'SnapController:handleRequest', 'SnapController:handleRequest',
), ),
revokeDynamicSnapPermissions: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapController:revokeDynamicPermissions',
),
dismissNotifications: this.dismissNotifications.bind(this), dismissNotifications: this.dismissNotifications.bind(this),
markNotificationsAsRead: this.markNotificationsAsRead.bind(this), markNotificationsAsRead: this.markNotificationsAsRead.bind(this),
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN

View File

@ -52,7 +52,7 @@ buildTypes:
- SEGMENT_FLASK_WRITE_KEY - SEGMENT_FLASK_WRITE_KEY
- ALLOW_LOCAL_SNAPS: true - ALLOW_LOCAL_SNAPS: true
- REQUIRE_SNAPS_ALLOWLIST: false - REQUIRE_SNAPS_ALLOWLIST: false
- IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/0.35.2-flask.1/index.html - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/0.36.1-flask.1/index.html
- SUPPORT_LINK: https://metamask-flask.zendesk.com/hc - SUPPORT_LINK: https://metamask-flask.zendesk.com/hc
- SUPPORT_REQUEST_LINK: https://metamask-flask.zendesk.com/hc/en-us/requests/new - SUPPORT_REQUEST_LINK: https://metamask-flask.zendesk.com/hc/en-us/requests/new
- INFURA_ENV_KEY_REF: INFURA_FLASK_PROJECT_ID - INFURA_ENV_KEY_REF: INFURA_FLASK_PROJECT_ID
@ -71,7 +71,7 @@ buildTypes:
- SEGMENT_FLASK_WRITE_KEY - SEGMENT_FLASK_WRITE_KEY
- ALLOW_LOCAL_SNAPS: true - ALLOW_LOCAL_SNAPS: true
- REQUIRE_SNAPS_ALLOWLIST: false - REQUIRE_SNAPS_ALLOWLIST: false
- IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/0.35.2-flask.1/index.html - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/0.36.1-flask.1/index.html
- SUPPORT_LINK: https://metamask-flask.zendesk.com/hc - SUPPORT_LINK: https://metamask-flask.zendesk.com/hc
- SUPPORT_REQUEST_LINK: https://metamask-flask.zendesk.com/hc/en-us/requests/new - SUPPORT_REQUEST_LINK: https://metamask-flask.zendesk.com/hc/en-us/requests/new
- INFURA_ENV_KEY_REF: INFURA_FLASK_PROJECT_ID - INFURA_ENV_KEY_REF: INFURA_FLASK_PROJECT_ID
@ -89,7 +89,7 @@ buildTypes:
- SEGMENT_WRITE_KEY_REF: SEGMENT_MMI_WRITE_KEY - SEGMENT_WRITE_KEY_REF: SEGMENT_MMI_WRITE_KEY
- SUPPORT_LINK: https://mmi-support.zendesk.com/hc/en-us - SUPPORT_LINK: https://mmi-support.zendesk.com/hc/en-us
- SUPPORT_REQUEST_LINK: https://mmi-support.zendesk.com/hc/en-us/requests/new - SUPPORT_REQUEST_LINK: https://mmi-support.zendesk.com/hc/en-us/requests/new
- MMI_CONFIGURATION_SERVICE_URL: https://configuration.metamask-institutional.io/v1/configuration/default - MMI_CONFIGURATION_SERVICE_URL
# For some reason, MMI uses this type of versioning # For some reason, MMI uses this type of versioning
# Leaving it on for backwards compatibility # Leaving it on for backwards compatibility
isPrerelease: true isPrerelease: true

View File

@ -11,13 +11,19 @@
* @param {string} args.rejectionMessage - The message for the rejected promise * @param {string} args.rejectionMessage - The message for the rejected promise
* this function will return in the event of failure. (Default: "Retry limit * this function will return in the event of failure. (Default: "Retry limit
* reached") * reached")
* @param {string} args.retryUntilFailure - Retries until the function fails.
* @param {Function} functionToRetry - The function that is run and tested for * @param {Function} functionToRetry - The function that is run and tested for
* failure. * failure.
* @returns {Promise<null | Error>} a promise that either resolves to null if * @returns {Promise<null | Error>} a promise that either resolves to null if
* the function is successful or is rejected with rejectionMessage otherwise. * the function is successful or is rejected with rejectionMessage otherwise.
*/ */
async function retry( async function retry(
{ retries, delay = 0, rejectionMessage = 'Retry limit reached' }, {
retries,
delay = 0,
rejectionMessage = 'Retry limit reached',
retryUntilFailure = false,
},
functionToRetry, functionToRetry,
) { ) {
let attempts = 0; let attempts = 0;
@ -28,9 +34,14 @@ async function retry(
try { try {
await functionToRetry(); await functionToRetry();
return; if (!retryUntilFailure) {
return;
}
} catch (error) { } catch (error) {
console.error(error); console.error(error);
if (retryUntilFailure) {
return;
}
} finally { } finally {
attempts += 1; attempts += 1;
} }

View File

@ -1967,7 +1967,6 @@
"@metamask/rpc-methods-flask>@metamask/snaps-ui": true, "@metamask/rpc-methods-flask>@metamask/snaps-ui": true,
"@metamask/rpc-methods-flask>@metamask/snaps-utils": true, "@metamask/rpc-methods-flask>@metamask/snaps-utils": true,
"@metamask/rpc-methods-flask>@metamask/utils": true, "@metamask/rpc-methods-flask>@metamask/utils": true,
"@metamask/rpc-methods-flask>nanoid": true,
"eth-rpc-errors": true, "eth-rpc-errors": true,
"superstruct": true "superstruct": true
} }
@ -2173,7 +2172,6 @@
"@metamask/snaps-controllers-flask>@metamask/snaps-utils": true, "@metamask/snaps-controllers-flask>@metamask/snaps-utils": true,
"@metamask/snaps-controllers-flask>@metamask/snaps-utils>@metamask/snaps-ui": true, "@metamask/snaps-controllers-flask>@metamask/snaps-utils>@metamask/snaps-ui": true,
"@metamask/snaps-controllers-flask>@metamask/utils": true, "@metamask/snaps-controllers-flask>@metamask/utils": true,
"@metamask/snaps-controllers-flask>nanoid": true,
"eth-rpc-errors": true, "eth-rpc-errors": true,
"superstruct": true "superstruct": true
} }

View File

@ -1967,7 +1967,6 @@
"@metamask/rpc-methods-flask>@metamask/snaps-ui": true, "@metamask/rpc-methods-flask>@metamask/snaps-ui": true,
"@metamask/rpc-methods-flask>@metamask/snaps-utils": true, "@metamask/rpc-methods-flask>@metamask/snaps-utils": true,
"@metamask/rpc-methods-flask>@metamask/utils": true, "@metamask/rpc-methods-flask>@metamask/utils": true,
"@metamask/rpc-methods-flask>nanoid": true,
"eth-rpc-errors": true, "eth-rpc-errors": true,
"superstruct": true "superstruct": true
} }
@ -2173,7 +2172,6 @@
"@metamask/snaps-controllers-flask>@metamask/snaps-utils": true, "@metamask/snaps-controllers-flask>@metamask/snaps-utils": true,
"@metamask/snaps-controllers-flask>@metamask/snaps-utils>@metamask/snaps-ui": true, "@metamask/snaps-controllers-flask>@metamask/snaps-utils>@metamask/snaps-ui": true,
"@metamask/snaps-controllers-flask>@metamask/utils": true, "@metamask/snaps-controllers-flask>@metamask/utils": true,
"@metamask/snaps-controllers-flask>nanoid": true,
"eth-rpc-errors": true, "eth-rpc-errors": true,
"superstruct": true "superstruct": true
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "metamask-crx", "name": "metamask-crx",
"version": "10.33.0", "version": "10.33.1",
"private": true, "private": true,
"repository": { "repository": {
"type": "git", "type": "git",
@ -195,7 +195,8 @@
"fast-json-patch@^3.1.1": "patch:fast-json-patch@npm%3A3.1.1#./.yarn/patches/fast-json-patch-npm-3.1.1-7e8bb70a45.patch", "fast-json-patch@^3.1.1": "patch:fast-json-patch@npm%3A3.1.1#./.yarn/patches/fast-json-patch-npm-3.1.1-7e8bb70a45.patch",
"request@^2.83.0": "patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch", "request@^2.83.0": "patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch",
"request@^2.88.2": "patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch", "request@^2.88.2": "patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch",
"request@^2.85.0": "patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch" "request@^2.85.0": "patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch",
"@metamask/signature-controller@^4.0.1": "patch:@metamask/signature-controller@npm%3A4.0.1#./.yarn/patches/@metamask-signature-controller-npm-4.0.1-013e64c9fd.patch"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.18.9", "@babel/runtime": "^7.18.9",
@ -229,7 +230,7 @@
"@metamask/base-controller": "^3.0.0", "@metamask/base-controller": "^3.0.0",
"@metamask/browser-passworder": "^4.1.0", "@metamask/browser-passworder": "^4.1.0",
"@metamask/contract-metadata": "^2.3.1", "@metamask/contract-metadata": "^2.3.1",
"@metamask/controller-utils": "^4.0.1", "@metamask/controller-utils": "^4.1.0",
"@metamask/design-tokens": "^1.12.0", "@metamask/design-tokens": "^1.12.0",
"@metamask/desktop": "^0.3.0", "@metamask/desktop": "^0.3.0",
"@metamask/eth-json-rpc-middleware": "^11.0.0", "@metamask/eth-json-rpc-middleware": "^11.0.0",
@ -239,13 +240,13 @@
"@metamask/eth-token-tracker": "^4.0.0", "@metamask/eth-token-tracker": "^4.0.0",
"@metamask/eth-trezor-keyring": "^1.0.0", "@metamask/eth-trezor-keyring": "^1.0.0",
"@metamask/etherscan-link": "^2.2.0", "@metamask/etherscan-link": "^2.2.0",
"@metamask/gas-fee-controller": "^6.0.0", "@metamask/gas-fee-controller": "^6.0.1",
"@metamask/jazzicon": "^2.0.0", "@metamask/jazzicon": "^2.0.0",
"@metamask/key-tree": "^7.0.0", "@metamask/key-tree": "^7.0.0",
"@metamask/logo": "^3.1.1", "@metamask/logo": "^3.1.1",
"@metamask/message-manager": "^7.0.0", "@metamask/message-manager": "^7.0.2",
"@metamask/metamask-eth-abis": "^3.0.0", "@metamask/metamask-eth-abis": "^3.0.0",
"@metamask/network-controller": "^10.1.0", "@metamask/network-controller": "^10.3.0",
"@metamask/notification-controller": "^3.0.0", "@metamask/notification-controller": "^3.0.0",
"@metamask/obs-store": "^8.1.0", "@metamask/obs-store": "^8.1.0",
"@metamask/permission-controller": "^4.0.0", "@metamask/permission-controller": "^4.0.0",
@ -254,18 +255,18 @@
"@metamask/providers": "^11.1.0", "@metamask/providers": "^11.1.0",
"@metamask/rate-limit-controller": "^3.0.0", "@metamask/rate-limit-controller": "^3.0.0",
"@metamask/rpc-methods": "^1.0.0-prerelease.1", "@metamask/rpc-methods": "^1.0.0-prerelease.1",
"@metamask/rpc-methods-flask": "npm:@metamask/rpc-methods@0.35.2-flask.1", "@metamask/rpc-methods-flask": "npm:@metamask/rpc-methods@0.36.1-flask.1",
"@metamask/safe-event-emitter": "^2.0.0", "@metamask/safe-event-emitter": "^2.0.0",
"@metamask/scure-bip39": "^2.0.3", "@metamask/scure-bip39": "^2.0.3",
"@metamask/signature-controller": "^4.0.1", "@metamask/signature-controller": "^4.0.1",
"@metamask/slip44": "^3.0.0", "@metamask/slip44": "^3.0.0",
"@metamask/smart-transactions-controller": "^3.1.0", "@metamask/smart-transactions-controller": "^3.1.0",
"@metamask/snaps-controllers": "^1.0.0-prerelease.1", "@metamask/snaps-controllers": "^1.0.0-prerelease.1",
"@metamask/snaps-controllers-flask": "npm:@metamask/snaps-controllers@0.35.2-flask.1", "@metamask/snaps-controllers-flask": "npm:@metamask/snaps-controllers@0.36.1-flask.1",
"@metamask/snaps-ui": "^1.0.0-prerelease.1", "@metamask/snaps-ui": "^1.0.0-prerelease.1",
"@metamask/snaps-ui-flask": "npm:@metamask/snaps-ui@0.35.2-flask.1", "@metamask/snaps-ui-flask": "npm:@metamask/snaps-ui@0.36.1-flask.1",
"@metamask/snaps-utils": "^1.0.0-prerelease.1", "@metamask/snaps-utils": "^1.0.0-prerelease.1",
"@metamask/snaps-utils-flask": "npm:@metamask/snaps-utils@0.35.2-flask.1", "@metamask/snaps-utils-flask": "npm:@metamask/snaps-utils@0.36.1-flask.1",
"@metamask/subject-metadata-controller": "^2.0.0", "@metamask/subject-metadata-controller": "^2.0.0",
"@metamask/utils": "^5.0.0", "@metamask/utils": "^5.0.0",
"@ngraveio/bc-ur": "^1.1.6", "@ngraveio/bc-ur": "^1.1.6",

View File

@ -174,8 +174,7 @@ export const BSC_DISPLAY_NAME = 'Binance Smart Chain';
export const POLYGON_DISPLAY_NAME = 'Polygon'; export const POLYGON_DISPLAY_NAME = 'Polygon';
export const AVALANCHE_DISPLAY_NAME = 'Avalanche Network C-Chain'; export const AVALANCHE_DISPLAY_NAME = 'Avalanche Network C-Chain';
export const ARBITRUM_DISPLAY_NAME = 'Arbitrum One'; export const ARBITRUM_DISPLAY_NAME = 'Arbitrum One';
export const BNB_DISPLAY_NAME = export const BNB_DISPLAY_NAME = 'BNB Chain';
'BNB Smart Chain (previously Binance Smart Chain Mainnet)';
export const OPTIMISM_DISPLAY_NAME = 'Optimism'; export const OPTIMISM_DISPLAY_NAME = 'Optimism';
export const FANTOM_DISPLAY_NAME = 'Fantom Opera'; export const FANTOM_DISPLAY_NAME = 'Fantom Opera';
export const HARMONY_DISPLAY_NAME = 'Harmony Mainnet Shard 0'; export const HARMONY_DISPLAY_NAME = 'Harmony Mainnet Shard 0';
@ -549,7 +548,6 @@ export const BUYABLE_CHAINS_MAP: {
| typeof CHAIN_IDS.FANTOM_TESTNET | typeof CHAIN_IDS.FANTOM_TESTNET
| typeof CHAIN_IDS.MOONBEAM_TESTNET | typeof CHAIN_IDS.MOONBEAM_TESTNET
| typeof CHAIN_IDS.LINEA_GOERLI | typeof CHAIN_IDS.LINEA_GOERLI
| typeof CHAIN_IDS.LINEA_MAINNET
| typeof CHAIN_IDS.GOERLI | typeof CHAIN_IDS.GOERLI
>]: BuyableChainSettings; >]: BuyableChainSettings;
} = { } = {
@ -613,6 +611,10 @@ export const BUYABLE_CHAINS_MAP: {
nativeCurrency: CURRENCY_SYMBOLS.PALM, nativeCurrency: CURRENCY_SYMBOLS.PALM,
network: 'palm', network: 'palm',
}, },
[CHAIN_IDS.LINEA_MAINNET]: {
nativeCurrency: CURRENCY_SYMBOLS.ETH,
network: 'linea',
},
}; };
export const FEATURED_RPCS: RPCDefinition[] = [ export const FEATURED_RPCS: RPCDefinition[] = [

View File

@ -29,3 +29,5 @@ export const ExcludedSnapEndowments = Object.freeze({
'endowment:long-running is deprecated. For more information please see https://github.com/MetaMask/snaps-monorepo/issues/945.', 'endowment:long-running is deprecated. For more information please see https://github.com/MetaMask/snaps-monorepo/issues/945.',
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
}); });
export const DynamicSnapPermissions = Object.freeze(['eth_accounts']);

View File

@ -176,7 +176,7 @@ export async function determineTransactionType(txParams, query) {
contractCode = resultCode; contractCode = resultCode;
if (isContractAddress) { if (isContractAddress) {
const hasValue = txParams.value && txParams.value !== '0x0'; const hasValue = txParams.value && Number(txParams.value) !== 0;
const tokenMethodName = [ const tokenMethodName = [
TransactionType.tokenMethodApprove, TransactionType.tokenMethodApprove,

View File

@ -186,6 +186,20 @@ describe('Transaction.utils', function () {
getCodeResponse: '0xab', getCodeResponse: '0xab',
}); });
const resultWithEmptyValue2 = await determineTransactionType(
{
value: '0x0000',
to: '0x9e673399f795D01116e9A8B2dD2F156705131ee9',
data: '0xa9059cbb0000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C970000000000000000000000000000000000000000000000000000000000000000a',
},
new EthQuery(_provider),
);
expect(resultWithEmptyValue2).toMatchObject({
type: TransactionType.tokenMethodTransfer,
getCodeResponse: '0xab',
});
const resultWithValue = await determineTransactionType( const resultWithValue = await determineTransactionType(
{ {
value: '0x12345', value: '0x12345',

View File

@ -105,7 +105,9 @@ async function withFixtures(options, testSuite) {
}); });
} }
} }
const mockedEndpoint = await setupMocking(mockServer, testSpecificMock); const mockedEndpoint = await setupMocking(mockServer, testSpecificMock, {
chainId: ganacheOptions?.chainId || 1337,
});
await mockServer.start(8000); await mockServer.start(8000);
driver = (await buildWebDriver(driverOptions)).driver; driver = (await buildWebDriver(driverOptions)).driver;

View File

@ -291,13 +291,12 @@ describe('MetaMask', function () {
it('transitions to the confirm screen', async function () { it('transitions to the confirm screen', async function () {
// Continue to next screen // Continue to next screen
await driver.delay(largeDelayMs); await driver.waitForElementNotPresent('.loading-overlay');
await driver.clickElement({ text: 'Next', tag: 'button' }); await driver.clickElement({ text: 'Next', tag: 'button' });
await driver.delay(largeDelayMs);
}); });
it('displays the token transfer data', async function () { it('displays the token transfer data', async function () {
await driver.delay(largeDelayMs); await driver.waitForElementNotPresent('.loading-overlay');
await driver.clickElement({ text: 'Hex', tag: 'button' }); await driver.clickElement({ text: 'Hex', tag: 'button' });
await driver.delay(regularDelayMs); await driver.delay(regularDelayMs);
@ -481,7 +480,7 @@ describe('MetaMask', function () {
}); });
it('submits the transaction', async function () { it('submits the transaction', async function () {
await driver.delay(largeDelayMs * 2); await driver.waitForElementNotPresent('.loading-overlay');
await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.clickElement({ text: 'Confirm', tag: 'button' });
await driver.delay(largeDelayMs * 2); await driver.delay(largeDelayMs * 2);
}); });

View File

@ -0,0 +1,326 @@
/* eslint-disable no-useless-escape */
const { strict: assert } = require('assert');
const {
defaultGanacheOptions,
withFixtures,
unlockWallet,
sendTransaction,
getEventPayloads,
} = require('../helpers');
const FixtureBuilder = require('../fixture-builder');
/**
* 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
* 'Transaction Submitted' and 'Transaction Finalized'. In addition on the
* first event of each series we require a field that should only appear in the
* anonymized events so that we can guarantee order of seenRequests and can
* properly make assertions. 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 {import('mockttp').Mockttp} mockServer
* @returns {Promise<import('mockttp/dist/pluggable-admin').MockttpClientResponse>[]}
*/
async function mockSegment(mockServer) {
return [
await mockServer
.forPost('https://api.segment.io/v1/batch')
.withJsonBodyIncluding({
batch: [
{
type: 'track',
event: 'Transaction Submitted',
properties: { status: 'submitted' },
},
],
})
.thenCallback(() => {
return {
statusCode: 200,
};
}),
await mockServer
.forPost('https://api.segment.io/v1/batch')
.withJsonBodyIncluding({
batch: [
{
type: 'track',
event: 'Transaction Submitted',
},
],
})
.thenCallback(() => {
return {
statusCode: 200,
};
}),
await mockServer
.forPost('https://api.segment.io/v1/batch')
.withJsonBodyIncluding({
batch: [
{
type: 'track',
event: 'Transaction Finalized',
properties: { status: 'confirmed' },
},
],
})
.thenCallback(() => {
return {
statusCode: 200,
};
}),
await mockServer
.forPost('https://api.segment.io/v1/batch')
.withJsonBodyIncluding({
batch: [
{
type: 'track',
event: 'Transaction Finalized',
},
],
})
.thenCallback(() => {
return {
statusCode: 200,
};
}),
];
}
const RECIPIENT = '0x0Cc5261AB8cE458dc977078A3623E2BaDD27afD3';
describe('Transaction Finalized Event', function () {
it('Successfully tracked when sending a transaction', async function () {
await withFixtures(
{
fixtures: new FixtureBuilder()
.withMetaMetricsController({
metaMetricsId: 'fake-metrics-id',
participateInMetaMetrics: true,
})
.build(),
ganacheOptions: defaultGanacheOptions,
title: this.test.title,
testSpecificMock: mockSegment,
},
async ({ driver, mockedEndpoint: mockedEndpoints }) => {
await driver.navigate();
await unlockWallet(driver);
await sendTransaction(driver, RECIPIENT, '2.0');
const events = await getEventPayloads(driver, mockedEndpoints);
// The order of these events is ensured by the mockSegment function
// which requires that the first, and third (0, 2 indexes) have the
// status property, which will only appear on the anonymous events.
const transactionSubmittedNoMMId = events[0];
const transactionSubmittedWithMMId = events[1];
const transactionFinalizedNoMMId = events[2];
const transactionFinalizedWithMMId = events[3];
// Assert doesn't have generic matchers so we delete these timestamp related properties.
// If we switch to a different assertion library we can use generic matchers in the future.
delete transactionSubmittedNoMMId.properties.first_seen;
delete transactionFinalizedNoMMId.properties.first_seen;
delete transactionFinalizedNoMMId.properties.completion_time;
// Assert that the event names begin with the appropriate prefixes. Even finalized events begin with transaction-submitted
// because they start as event fragments created when the transaction is submitted.
assert.ok(
transactionSubmittedNoMMId.messageId.startsWith(
'transaction-submitted',
),
`Transaction Submitted event with sensitive properties has messageId \"${transactionSubmittedNoMMId.messageId}\" does not have a messageId beginning with \"transaction-submitted\"`,
);
assert.ok(
transactionFinalizedNoMMId.messageId.startsWith(
'transaction-submitted',
),
`Transaction Finalized event with sensitive properties has messageId \"${transactionFinalizedNoMMId.messageId}\" that does not begin with \"transaction-submitted\"`,
);
assert.ok(
transactionSubmittedWithMMId.messageId.startsWith(
'transaction-submitted',
),
`Transaction Submitted event has messageId \"${transactionSubmittedWithMMId.messageId}\" that does not begin with \"transaction-submitted\"`,
);
assert.ok(
transactionFinalizedWithMMId.messageId.startsWith(
'transaction-submitted',
),
`Transaction Finalized event has messageID \"${transactionFinalizedWithMMId.messageId}\" that does not begin with \"transaction-submitted\"`,
);
// Assert that the events with sensitive properties should have messageIds ending in 0x000
// This is important because otherwise the events are seen as duplicates in segment
assert.ok(
transactionSubmittedNoMMId.messageId.endsWith('0x000'),
`Transaction Submitted event with sensitive properties has messageId \"${transactionSubmittedNoMMId.messageId}\" that does not end in \"0x000\" to differentiate it from the event that does not include sensitive data.`,
);
assert.ok(
transactionFinalizedNoMMId.messageId.endsWith('0x000'),
`Transaction Finalized event with sensitive properties has messageID \"${transactionFinalizedNoMMId.messageId}\" that does not end in \"0x000\" to differentiate it from the event that does not include sensitive data.`,
);
// Assert that transaction finalized events contain '-success-' in their messageId
assert.ok(
transactionFinalizedWithMMId.messageId.includes('-success-'),
`Transaction Finalized event has messageId \"${transactionFinalizedWithMMId.messageId}\" that does not contain "-success-"`,
);
assert.ok(
transactionFinalizedNoMMId.messageId.includes('-success-'),
`Transaction Finalized event with sensitive properties has messageID \"${transactionFinalizedNoMMId.messageId}\" that does not contain "-success-"`,
);
// Assert that the events with sensitive data do not contain a userId (the random anonymous id generated when a user opts into metametrics)
assert.ok(
typeof transactionSubmittedNoMMId.userId === 'undefined',
'Transaction Submitted event with sensitive properties has a userId supplied when it should only have the anonymousId',
);
assert.ok(
typeof transactionFinalizedNoMMId.userId === 'undefined',
'Transaction Finalized event with sensitive properties has a userId supplied when it should only have the anonymousId',
);
// Assert that the events with sensitive data have anonymousId set to 0x0000000000000000 which is our universal anonymous record
assert.ok(
transactionSubmittedNoMMId.anonymousId === '0x0000000000000000',
'Transaction Submitted event with sensitive properties has an anonymousId that does not match our universal anonymous id of 0x0000000000000000',
);
assert.ok(
transactionFinalizedNoMMId.anonymousId === '0x0000000000000000',
'Transaction Finalized event with sensitive properties has an anonymousId that does not match our universal anonymous id of 0x0000000000000000',
);
// Assert that our events without sensitive data have a userId but no anonymousId
assert.ok(
typeof transactionSubmittedWithMMId.userId === 'string' &&
typeof transactionSubmittedWithMMId.anonymousId === 'undefined',
'Transaction Submitted event without sensitive properties should only have a userId specified, and no anonymousId',
);
assert.ok(
typeof transactionFinalizedWithMMId.userId === 'string' &&
typeof transactionFinalizedWithMMId.anonymousId === 'undefined',
'Transaction Finalized event without sensitive properties should only have a userId specified, and no anonymousId',
);
// Assert on the properties
assert.deepStrictEqual(
transactionSubmittedNoMMId.properties,
{
status: 'submitted',
transaction_envelope_type: 'legacy',
gas_limit: '0x5208',
gas_price: '2',
default_gas: '0.000021',
default_gas_price: '2',
chain_id: '0x539',
referrer: 'metamask',
source: 'user',
network: '1337',
eip_1559_version: '0',
gas_edit_type: 'none',
gas_edit_attempted: 'none',
account_type: 'MetaMask',
device_model: 'N/A',
asset_type: 'NATIVE',
token_standard: 'NONE',
transaction_type: 'simpleSend',
transaction_speed_up: false,
ui_customizations: null,
category: 'Transactions',
locale: 'en',
environment_type: 'background',
},
'Transaction Submitted event with sensitive properties does not match the expected payload',
);
assert.deepStrictEqual(
transactionSubmittedWithMMId.properties,
{
chain_id: '0x539',
referrer: 'metamask',
source: 'user',
network: '1337',
eip_1559_version: '0',
gas_edit_type: 'none',
gas_edit_attempted: 'none',
account_type: 'MetaMask',
device_model: 'N/A',
asset_type: 'NATIVE',
token_standard: 'NONE',
transaction_type: 'simpleSend',
transaction_speed_up: false,
ui_customizations: null,
category: 'Transactions',
locale: 'en',
environment_type: 'background',
},
'Transaction Submitted event without sensitive properties does not match the expected payload',
);
assert.deepStrictEqual(
transactionFinalizedNoMMId.properties,
{
status: 'confirmed',
transaction_envelope_type: 'legacy',
gas_limit: '0x5208',
gas_price: '2',
default_gas: '0.000021',
default_gas_price: '2',
chain_id: '0x539',
referrer: 'metamask',
source: 'user',
network: '1337',
eip_1559_version: '0',
gas_edit_type: 'none',
gas_edit_attempted: 'none',
account_type: 'MetaMask',
device_model: 'N/A',
asset_type: 'NATIVE',
token_standard: 'NONE',
transaction_type: 'simpleSend',
transaction_speed_up: false,
ui_customizations: null,
gas_used: '5208',
category: 'Transactions',
locale: 'en',
environment_type: 'background',
},
'Transaction Finalized event with sensitive properties does not match the expected payload',
);
assert.deepStrictEqual(
transactionFinalizedWithMMId.properties,
{
chain_id: '0x539',
referrer: 'metamask',
source: 'user',
network: '1337',
eip_1559_version: '0',
gas_edit_type: 'none',
gas_edit_attempted: 'none',
account_type: 'MetaMask',
device_model: 'N/A',
asset_type: 'NATIVE',
token_standard: 'NONE',
transaction_type: 'simpleSend',
transaction_speed_up: false,
ui_customizations: null,
category: 'Transactions',
locale: 'en',
environment_type: 'background',
},
'Transaction Finalized event without sensitive properties does not match the expected payload',
);
},
);
});
});

View File

@ -20,7 +20,16 @@ const emptyStalelist = {
lastUpdated: 0, lastUpdated: 0,
}; };
async function setupMocking(server, testSpecificMock) { /**
* Setup E2E network mocks.
*
* @param {object} server - The mock server used for network mocks.
* @param {Function} testSpecificMock - A function for setting up test-specific network mocks
* @param {object} options - Network mock options.
* @param {string} options.chainId - The chain ID used by the default configured network.
* @returns
*/
async function setupMocking(server, testSpecificMock, { chainId }) {
await server.forAnyRequest().thenPassThrough({ await server.forAnyRequest().thenPassThrough({
beforeRequest: (req) => { beforeRequest: (req) => {
const { host } = req.headers; const { host } = req.headers;
@ -99,7 +108,9 @@ async function setupMocking(server, testSpecificMock) {
}); });
await server await server
.forGet('https://gas-api.metaswap.codefi.network/networks/1337/gasPrices') .forGet(
`https://gas-api.metaswap.codefi.network/networks/${chainId}/gasPrices`,
)
.thenCallback(() => { .thenCallback(() => {
return { return {
statusCode: 200, statusCode: 200,
@ -130,7 +141,7 @@ async function setupMocking(server, testSpecificMock) {
await server await server
.forGet( .forGet(
'https://gas-api.metaswap.codefi.network/networks/1337/suggestedGasFees', `https://gas-api.metaswap.codefi.network/networks/${chainId}/suggestedGasFees`,
) )
.thenCallback(() => { .thenCallback(() => {
return { return {
@ -203,7 +214,7 @@ async function setupMocking(server, testSpecificMock) {
}); });
await server await server
.forGet('https://token-api.metaswap.codefi.network/tokens/1337') .forGet(`https://token-api.metaswap.codefi.network/tokens/${chainId}`)
.thenCallback(() => { .thenCallback(() => {
return { return {
statusCode: 200, statusCode: 200,
@ -343,7 +354,7 @@ async function setupMocking(server, testSpecificMock) {
}); });
await server await server
.forGet('https://token-api.metaswap.codefi.network/token/1337') .forGet(`https://token-api.metaswap.codefi.network/token/${chainId}`)
.thenCallback(() => { .thenCallback(() => {
return { return {
statusCode: 200, statusCode: 200,
@ -352,15 +363,15 @@ async function setupMocking(server, testSpecificMock) {
}); });
// It disables loading of token icons, e.g. this URL: https://static.metafi.codefi.network/api/v1/tokenIcons/1337/0x0000000000000000000000000000000000000000.png // It disables loading of token icons, e.g. this URL: https://static.metafi.codefi.network/api/v1/tokenIcons/1337/0x0000000000000000000000000000000000000000.png
await server const tokenIconRegex = new RegExp(
.forGet( `^https:\\/\\/static\\.metafi\\.codefi\\.network\\/api\\/vi\\/tokenIcons\\/${chainId}\\/.*\\.png`,
/^https:\/\/static\.metafi\.codefi\.network\/api\/v1\/tokenIcons\/1337\/.*\.png/u, 'u',
) );
.thenCallback(() => { await server.forGet(tokenIconRegex).thenCallback(() => {
return { return {
statusCode: 200, statusCode: 200,
}; };
}); });
await server await server
.forGet('https://min-api.cryptocompare.com/data/price') .forGet('https://min-api.cryptocompare.com/data/price')

View File

@ -31,6 +31,11 @@ async function main() {
'Set how many times the test should be retried upon failure.', 'Set how many times the test should be retried upon failure.',
type: 'number', type: 'number',
}) })
.option('retry-until-failure', {
default: false,
description: 'Retries until the test fails',
type: 'boolean',
})
.option('leave-running', { .option('leave-running', {
default: false, default: false,
description: description:
@ -46,7 +51,14 @@ async function main() {
.strict() .strict()
.help('help'); .help('help');
const { browser, debug, e2eTestPath, retries, leaveRunning } = argv; const {
browser,
debug,
e2eTestPath,
retries,
retryUntilFailure,
leaveRunning,
} = argv;
if (!browser) { if (!browser) {
exitWithError( exitWithError(
@ -97,7 +109,7 @@ async function main() {
const dir = 'test/test-results/e2e'; const dir = 'test/test-results/e2e';
fs.mkdir(dir, { recursive: true }); fs.mkdir(dir, { recursive: true });
await retry({ retries }, async () => { await retry({ retries, retryUntilFailure }, async () => {
await runInShell( await runInShell(
'yarn', 'yarn',
[ [

View File

@ -3,6 +3,9 @@ const {
withFixtures, withFixtures,
openDapp, openDapp,
locateAccountBalanceDOM, locateAccountBalanceDOM,
unlockWallet,
largeDelayMs,
WINDOW_TITLES,
} = require('../helpers'); } = require('../helpers');
const { SMART_CONTRACTS } = require('../seeder/smart-contracts'); const { SMART_CONTRACTS } = require('../seeder/smart-contracts');
const FixtureBuilder = require('../fixture-builder'); const FixtureBuilder = require('../fixture-builder');
@ -34,34 +37,32 @@ describe('Deploy contract and call contract methods', function () {
smartContract, smartContract,
); );
await driver.navigate(); await driver.navigate();
await driver.fill('#password', 'correct horse battery staple'); await unlockWallet(driver);
await driver.press('#password', driver.Key.ENTER);
// deploy contract // deploy contract
await openDapp(driver, contractAddress); await openDapp(driver, contractAddress);
// wait for deployed contract, calls and confirms a contract method where ETH is sent // wait for deployed contract, calls and confirms a contract method where ETH is sent
await driver.findClickableElement('#deployButton'); await driver.delay(largeDelayMs);
await driver.clickElement('#depositButton'); await driver.clickElement('#depositButton');
await driver.waitUntilXWindowHandles(3);
let windowHandles = await driver.getAllWindowHandles();
const extension = windowHandles[0];
const dapp = await driver.switchToWindowWithTitle(
'E2E Test Dapp',
windowHandles,
);
await driver.switchToWindowWithTitle( await driver.waitForSelector({
'MetaMask Notification', css: 'span',
windowHandles, text: 'Deposit initiated',
); });
await driver.waitUntilXWindowHandles(3);
await driver.switchToWindowWithTitle(WINDOW_TITLES.Notification);
await driver.waitForSelector({ await driver.waitForSelector({
css: '.confirm-page-container-summary__action__name', css: '.confirm-page-container-summary__action__name',
text: 'Deposit', text: 'Deposit',
}); });
await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.clickElement({ text: 'Confirm', tag: 'button' });
await driver.waitUntilXWindowHandles(2); await driver.waitUntilXWindowHandles(2);
await driver.switchToWindow(extension); await driver.switchToWindowWithTitle(
WINDOW_TITLES.ExtensionInFullScreenView,
);
await driver.clickElement({ text: 'Activity', tag: 'button' }); await driver.clickElement({ text: 'Activity', tag: 'button' });
await driver.waitForSelector( await driver.waitForSelector(
'.transaction-list__completed-transactions .transaction-list-item:nth-of-type(1)', '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(1)',
@ -72,17 +73,16 @@ describe('Deploy contract and call contract methods', function () {
}); });
// calls and confirms a contract method where ETH is received // calls and confirms a contract method where ETH is received
await driver.switchToWindow(dapp); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp);
await driver.clickElement('#withdrawButton'); await driver.clickElement('#withdrawButton');
await driver.waitUntilXWindowHandles(3); await driver.waitUntilXWindowHandles(3);
windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle( await driver.switchToWindowWithTitle(WINDOW_TITLES.Notification);
'MetaMask Notification',
windowHandles,
);
await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.clickElement({ text: 'Confirm', tag: 'button' });
await driver.waitUntilXWindowHandles(2); await driver.waitUntilXWindowHandles(2);
await driver.switchToWindow(extension); await driver.switchToWindowWithTitle(
WINDOW_TITLES.ExtensionInFullScreenView,
);
await driver.waitForSelector( await driver.waitForSelector(
'.transaction-list__completed-transactions .transaction-list-item:nth-of-type(2)', '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(2)',
); );
@ -92,7 +92,9 @@ describe('Deploy contract and call contract methods', function () {
}); });
// renders the correct ETH balance // renders the correct ETH balance
await driver.switchToWindow(extension); await driver.switchToWindowWithTitle(
WINDOW_TITLES.ExtensionInFullScreenView,
);
await locateAccountBalanceDOM(driver, ganacheServer); await locateAccountBalanceDOM(driver, ganacheServer);
}, },
); );

View File

@ -129,10 +129,6 @@ describe('Create token, approve token and approve token without gas', function (
text: 'Got it', text: 'Got it',
tag: 'button', tag: 'button',
}); });
await driver.clickElement({
text: 'Use default',
css: '.mm-button-link',
});
await driver.clickElement({ await driver.clickElement({
text: 'View details', text: 'View details',
css: '.token-allowance-container__view-details', css: '.token-allowance-container__view-details',
@ -157,17 +153,17 @@ describe('Create token, approve token and approve token without gas', function (
await driver.clickElement({ text: 'Next', tag: 'button' }); await driver.clickElement({ text: 'Next', tag: 'button' });
await driver.findElement({ await driver.findElement({
text: 'Review the spending cap for your', text: 'Spending cap request for your ',
tag: 'div', css: '.box--flex-direction-row',
}); });
const defaultSpendingCup = await driver.findElement({ const defaultSpendingCap = await driver.findElement({
text: '7 TST', text: '7 TST',
css: '.box--flex-direction-row > h6', css: '.box--flex-direction-row > h6',
}); });
assert.equal( assert.equal(
await defaultSpendingCup.getText(), await defaultSpendingCap.getText(),
'7 TST', '7 TST',
'Default value is not correctly set', 'Default value is not correctly set',
); );
@ -253,13 +249,13 @@ describe('Create token, approve token and approve token without gas', function (
tag: 'button', tag: 'button',
}); });
let spendingCup = await driver.findElement({ let spendingCap = await driver.findElement({
text: '5 TST', text: '5 TST',
css: '.box--flex-direction-row > h6', css: '.box--flex-direction-row > h6',
}); });
assert.equal( assert.equal(
await spendingCup.getText(), await spendingCap.getText(),
'5 TST', '5 TST',
'Default value is not correctly set', 'Default value is not correctly set',
); );
@ -309,12 +305,12 @@ describe('Create token, approve token and approve token without gas', function (
tag: 'button', tag: 'button',
}); });
spendingCup = await driver.findElement({ spendingCap = await driver.findElement({
text: '9 TST', text: '9 TST',
css: '.box--flex-direction-row > h6', css: '.box--flex-direction-row > h6',
}); });
assert.equal( assert.equal(
await spendingCup.getText(), await spendingCap.getText(),
'9 TST', '9 TST',
'Default value is not correctly set', 'Default value is not correctly set',
); );
@ -429,7 +425,7 @@ describe('Create token, approve token and approve token without gas', function (
); );
}); });
it('approves token without gas, set default spending cap, submits the transaction and finds the transaction in the transactions list', async function () { it('approves token without gas, set site suggested spending cap, submits the transaction and finds the transaction in the transactions list', async function () {
await withFixtures( await withFixtures(
{ {
dapp: true, dapp: true,
@ -465,9 +461,16 @@ describe('Create token, approve token and approve token without gas', function (
const pendingTxes = await driver.findElements('.transaction-list-item'); const pendingTxes = await driver.findElements('.transaction-list-item');
pendingTxes[0].click(); pendingTxes[0].click();
// set spending cap
// set custom spending cap
const spendingCap = await driver.findElement(
'[data-testid="custom-spending-cap-input"]',
);
await spendingCap.fill('5');
// set site suggested spending cap
await driver.clickElement({ await driver.clickElement({
text: 'Use default', text: 'Use site suggestion',
css: '.mm-button-link', css: '.mm-button-link',
}); });
await driver.clickElement({ await driver.clickElement({

View File

@ -2,7 +2,7 @@ const { strict: assert } = require('assert');
const { convertToHexValue, withFixtures, openDapp } = require('../helpers'); const { convertToHexValue, withFixtures, openDapp } = require('../helpers');
const FixtureBuilder = require('../fixture-builder'); const FixtureBuilder = require('../fixture-builder');
async function getEncryptionKey(driver) { async function validateEncryptionKey(driver, encryptionKey) {
await driver.clickElement('#getEncryptionKeyButton'); await driver.clickElement('#getEncryptionKeyButton');
await driver.waitUntilXWindowHandles(3); await driver.waitUntilXWindowHandles(3);
let windowHandles = await driver.getAllWindowHandles(); let windowHandles = await driver.getAllWindowHandles();
@ -15,7 +15,10 @@ async function getEncryptionKey(driver) {
await driver.waitUntilXWindowHandles(2); await driver.waitUntilXWindowHandles(2);
windowHandles = await driver.getAllWindowHandles(); windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles);
return await driver.findElement('#encryptionKeyDisplay'); await driver.findElement({
css: '#encryptionKeyDisplay',
text: encryptionKey,
});
} }
async function encryptMessage(driver, message) { async function encryptMessage(driver, message) {
@ -51,8 +54,10 @@ async function verifyDecryptedMessageMM(driver, message) {
async function verifyDecryptedMessageDapp(driver, message) { async function verifyDecryptedMessageDapp(driver, message) {
const windowHandles = await driver.getAllWindowHandles(); const windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles);
const clearTextLabel = await driver.findElement('#cleartextDisplay'); await driver.findElement({
assert.equal(await clearTextLabel.getText(), message); css: '#cleartextDisplay',
text: message,
});
} }
describe('Encrypt Decrypt', function () { describe('Encrypt Decrypt', function () {
@ -67,6 +72,7 @@ describe('Encrypt Decrypt', function () {
}; };
const encryptionKey = 'fxYXfCbun026g5zcCQh7Ia+O0urAEVZWLG8H4Jzu7Xs='; const encryptionKey = 'fxYXfCbun026g5zcCQh7Ia+O0urAEVZWLG8H4Jzu7Xs=';
const message = 'Hello, Bob!'; const message = 'Hello, Bob!';
it('should decrypt an encrypted message', async function () { it('should decrypt an encrypted message', async function () {
await withFixtures( await withFixtures(
{ {
@ -84,8 +90,7 @@ describe('Encrypt Decrypt', function () {
await openDapp(driver); await openDapp(driver);
// ------ Get Encryption key ------ // ------ Get Encryption key ------
const encryptionKeyLabel = await getEncryptionKey(driver); await validateEncryptionKey(driver, encryptionKey);
assert.equal(await encryptionKeyLabel.getText(), encryptionKey);
// ------ Encrypt ------ // ------ Encrypt ------
await encryptMessage(driver, message); await encryptMessage(driver, message);
@ -126,8 +131,7 @@ describe('Encrypt Decrypt', function () {
await openDapp(driver); await openDapp(driver);
// ------ Get Encryption key ------ // ------ Get Encryption key ------
const encryptionKeyLabel = await getEncryptionKey(driver); await validateEncryptionKey(driver, encryptionKey);
assert.equal(await encryptionKeyLabel.getText(), encryptionKey);
// ------ Encrypt Message 1------ // ------ Encrypt Message 1------
await encryptMessage(driver, message); await encryptMessage(driver, message);

View File

@ -4,6 +4,7 @@ const {
logInWithBalanceValidation, logInWithBalanceValidation,
} = require('../helpers'); } = require('../helpers');
const FixtureBuilder = require('../fixture-builder'); const FixtureBuilder = require('../fixture-builder');
const { CHAIN_IDS } = require('../../../shared/constants/network');
describe('Gas estimates generated by MetaMask', function () { describe('Gas estimates generated by MetaMask', function () {
const baseGanacheOptions = { const baseGanacheOptions = {
@ -139,7 +140,85 @@ describe('Gas estimates generated by MetaMask', function () {
}); });
describe('Send on a network that is not EIP-1559 compatible', function () { describe('Send on a network that is not EIP-1559 compatible', function () {
it('show expected gas defaults', async function () { it('show expected gas defaults on a network supported by legacy gas API', async function () {
await withFixtures(
{
fixtures: new FixtureBuilder().build(),
ganacheOptions: {
...preLondonGanacheOptions,
chainId: parseInt(CHAIN_IDS.BSC, 16),
},
title: this.test.title,
},
async ({ driver, ganacheServer }) => {
await driver.navigate();
await logInWithBalanceValidation(driver, ganacheServer);
await driver.clickElement('[data-testid="eth-overview-send"]');
await driver.fill(
'input[placeholder="Enter public address (0x) or ENS name"]',
'0x2f318C334780961FB129D2a6c30D0763d9a5C970',
);
await driver.fill('.unit-input__input', '1');
// Check that the gas estimation is what we expect
await driver.findElement({
cass: '[data-testid="confirm-gas-display"]',
text: '0.000042',
});
},
);
});
it('show expected gas defaults on a network supported by legacy gas API when that API is down', async function () {
await withFixtures(
{
fixtures: new FixtureBuilder().build(),
ganacheOptions: {
...preLondonGanacheOptions,
chainId: parseInt(CHAIN_IDS.BSC, 16),
},
testSpecificMock: (mockServer) => {
mockServer
.forGet(
`https://gas-api.metaswap.codefi.network/networks/${parseInt(
CHAIN_IDS.BSC,
16,
)}/gasPrices`,
)
.thenCallback(() => {
return {
statusCode: 422,
};
});
},
title: this.test.title,
},
async ({ driver, ganacheServer }) => {
await driver.navigate();
await logInWithBalanceValidation(driver, ganacheServer);
await driver.clickElement('[data-testid="eth-overview-send"]');
await driver.fill(
'input[placeholder="Enter public address (0x) or ENS name"]',
'0x2f318C334780961FB129D2a6c30D0763d9a5C970',
);
await driver.fill('.unit-input__input', '1');
// Check that the gas estimation is what we expect
await driver.findElement({
cass: '[data-testid="confirm-gas-display"]',
text: '0.000042',
});
},
);
});
it('show expected gas defaults on a network not supported by legacy gas API', async function () {
await withFixtures( await withFixtures(
{ {
fixtures: new FixtureBuilder().build(), fixtures: new FixtureBuilder().build(),

View File

@ -51,14 +51,14 @@ describe('Import flow', function () {
await driver.findVisibleElement('.qr-code__wrapper'); await driver.findVisibleElement('.qr-code__wrapper');
// shows a QR code for the account // shows a QR code for the account
await driver.findVisibleElement('.popover-container'); await driver.findVisibleElement('.mm-modal');
// shows the correct account address // shows the correct account address
const address = await driver.findElement( const address = await driver.findElement(
'.multichain-address-copy-button', '.multichain-address-copy-button',
); );
assert.equal(await address.getText(), '0x0Cc...afD3'); assert.equal(await address.getText(), '0x0Cc...afD3');
await driver.clickElement('[data-testid="popover-close"]'); await driver.clickElement('.mm-modal button[aria-label="Close"]');
// logs out of the account // logs out of the account
await driver.clickElement( await driver.clickElement(

View File

@ -78,8 +78,8 @@ describe('Incremental Security', function () {
const publicAddress = await address.getText(); const publicAddress = await address.getText();
// wait for account modal to be visible // wait for account modal to be visible
const accountModal = await driver.findVisibleElement('.popover-bg'); const accountModal = await driver.findVisibleElement('.mm-modal');
await driver.clickElement('[data-testid="popover-close"]'); await driver.clickElement('.mm-modal button[aria-label="Close"]');
// wait for account modal to be removed from DOM // wait for account modal to be removed from DOM
await accountModal.waitForElementState('hidden'); await accountModal.waitForElementState('hidden');

View File

@ -161,16 +161,18 @@ class Driver {
// replacement for the implementation below. It takes an option options // replacement for the implementation below. It takes an option options
// bucket that can include the state attribute to wait for elements that // bucket that can include the state attribute to wait for elements that
// match the selector to be removed from the DOM. // match the selector to be removed from the DOM.
const selector = this.buildLocator(rawLocator);
let element; let element;
if (!['visible', 'detached'].includes(state)) { if (!['visible', 'detached'].includes(state)) {
throw new Error(`Provided state selector ${state} is not supported`); throw new Error(`Provided state selector ${state} is not supported`);
} }
if (state === 'visible') { if (state === 'visible') {
element = await this.driver.wait(until.elementLocated(selector), timeout); element = await this.driver.wait(
until.elementLocated(this.buildLocator(rawLocator)),
timeout,
);
} else if (state === 'detached') { } else if (state === 'detached') {
element = await this.driver.wait( element = await this.driver.wait(
until.stalenessOf(await this.findElement(selector)), until.stalenessOf(await this.findElement(rawLocator)),
timeout, timeout,
); );
} }
@ -205,15 +207,13 @@ class Driver {
} }
async findVisibleElement(rawLocator) { async findVisibleElement(rawLocator) {
const locator = this.buildLocator(rawLocator); const element = await this.findElement(rawLocator);
const element = await this.findElement(locator);
await this.driver.wait(until.elementIsVisible(element), this.timeout); await this.driver.wait(until.elementIsVisible(element), this.timeout);
return wrapElementWithAPI(element, this); return wrapElementWithAPI(element, this);
} }
async findClickableElement(rawLocator) { async findClickableElement(rawLocator) {
const locator = this.buildLocator(rawLocator); const element = await this.findElement(rawLocator);
const element = await this.findElement(locator);
await Promise.all([ await Promise.all([
this.driver.wait(until.elementIsVisible(element), this.timeout), this.driver.wait(until.elementIsVisible(element), this.timeout),
this.driver.wait(until.elementIsEnabled(element), this.timeout), this.driver.wait(until.elementIsEnabled(element), this.timeout),
@ -231,8 +231,7 @@ class Driver {
} }
async findClickableElements(rawLocator) { async findClickableElements(rawLocator) {
const locator = this.buildLocator(rawLocator); const elements = await this.findElements(rawLocator);
const elements = await this.findElements(locator);
await Promise.all( await Promise.all(
elements.reduce((acc, element) => { elements.reduce((acc, element) => {
acc.push( acc.push(
@ -246,14 +245,12 @@ class Driver {
} }
async clickElement(rawLocator) { async clickElement(rawLocator) {
const locator = this.buildLocator(rawLocator); const element = await this.findClickableElement(rawLocator);
const element = await this.findClickableElement(locator);
await element.click(); await element.click();
} }
async clickPoint(rawLocator, x, y) { async clickPoint(rawLocator, x, y) {
const locator = this.buildLocator(rawLocator); const element = await this.findElement(rawLocator);
const element = await this.findElement(locator);
await this.driver await this.driver
.actions() .actions()
.move({ origin: element, x, y }) .move({ origin: element, x, y })
@ -281,10 +278,9 @@ class Driver {
} }
async assertElementNotPresent(rawLocator) { async assertElementNotPresent(rawLocator) {
const locator = this.buildLocator(rawLocator);
let dataTab; let dataTab;
try { try {
dataTab = await this.findElement(locator); dataTab = await this.findElement(rawLocator);
} catch (err) { } catch (err) {
assert( assert(
err instanceof webdriverError.NoSuchElementError || err instanceof webdriverError.NoSuchElementError ||
@ -375,7 +371,7 @@ class Driver {
return await this.driver.getAllWindowHandles(); return await this.driver.getAllWindowHandles();
} }
async waitUntilXWindowHandles(x, delayStep = 1000, timeout = 5000) { async waitUntilXWindowHandles(x, delayStep = 1000, timeout = this.timeout) {
let timeElapsed = 0; let timeElapsed = 0;
let windowHandles = []; let windowHandles = [];
while (timeElapsed <= timeout) { while (timeElapsed <= timeout) {

View File

@ -1,4 +1,4 @@
process.env.METAMASK_ENVIRONMENT = 'test'; process.env.METAMASK_ENVIRONMENT = 'test';
process.env.SUPPORT_LINK = 'https://support.metamask.io'; process.env.SUPPORT_LINK = 'https://support.metamask.io';
process.env.IFRAME_EXECUTION_ENVIRONMENT_URL = process.env.IFRAME_EXECUTION_ENVIRONMENT_URL =
'https://execution.metamask.io/0.35.2-flask.1/index.html'; 'https://execution.metamask.io/0.36.1-flask.1/index.html';

View File

@ -127,3 +127,6 @@ expect.extend({
}; };
}, },
}); });
// Setup window.prompt
global.prompt = () => undefined;

View File

@ -51,15 +51,6 @@ exports[`CustomSpendingCap should match snapshot 1`] = `
</div> </div>
</div> </div>
</div> </div>
<div
class="mm-box form-field__heading-detail mm-box--margin-right-0 mm-box--margin-bottom-2 mm-box--text-align-end"
>
<button
class="box mm-text mm-button-base mm-button-link mm-button-link--size-auto mm-text--body-md box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-primary-default box--background-color-transparent"
>
Use default
</button>
</div>
</div> </div>
<input <input
class="form-field__input" class="form-field__input"
@ -67,7 +58,7 @@ exports[`CustomSpendingCap should match snapshot 1`] = `
id="custom-spending-cap" id="custom-spending-cap"
placeholder="Enter a number" placeholder="Enter a number"
type="text" type="text"
value="10" value="7"
/> />
</div> </div>
@ -85,7 +76,7 @@ exports[`CustomSpendingCap should match snapshot 1`] = `
class="box custom-spending-cap__description box--flex-direction-row" class="box custom-spending-cap__description box--flex-direction-row"
> >
<h6 <h6
class="box mm-text mm-text--body-sm box--padding-top-2 box--padding-bottom-2 box--flex-direction-row box--color-text-default" class="box mm-text mm-text--body-sm box--padding-top-2 box--flex-direction-row box--color-text-default"
> >
<span> <span>
@ -93,7 +84,7 @@ exports[`CustomSpendingCap should match snapshot 1`] = `
<h6 <h6
class="box mm-text custom-spending-cap__input-value-and-token-name mm-text--body-sm-bold box--flex-direction-row box--color-text-default" class="box mm-text custom-spending-cap__input-value-and-token-name mm-text--body-sm-bold box--flex-direction-row box--color-text-default"
> >
10 7
TST TST
</h6> </h6>
@ -101,6 +92,13 @@ exports[`CustomSpendingCap should match snapshot 1`] = `
</span> </span>
</h6> </h6>
<a
class="box mm-text mm-button-base mm-button-base--size-sm mm-button-link mm-text--body-md box--margin-bottom-2 box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-primary-default box--background-color-transparent"
href="https://support.metamask.io/hc/en-us/articles/6055177143579-How-to-customize-token-approvals-with-a-spending-cap"
target="_blank"
>
Learn more
</a>
</div> </div>
</label> </label>
</div> </div>

View File

@ -1,5 +1,5 @@
import React, { useState, useContext, useEffect, useRef } from 'react'; import React, { useState, useContext, useEffect, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
@ -21,7 +21,6 @@ import {
BackgroundColor, BackgroundColor,
TextColor, TextColor,
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import { getCustomTokenAmount } from '../../../selectors';
import { setCustomTokenAmount } from '../../../ducks/app/app'; import { setCustomTokenAmount } from '../../../ducks/app/app';
import { calcTokenAmount } from '../../../../shared/lib/transactions-controller-utils'; import { calcTokenAmount } from '../../../../shared/lib/transactions-controller-utils';
import { hexToDecimal } from '../../../../shared/modules/conversion.utils'; import { hexToDecimal } from '../../../../shared/modules/conversion.utils';
@ -34,6 +33,7 @@ import { Numeric } from '../../../../shared/modules/Numeric';
import { estimateGas } from '../../../store/actions'; import { estimateGas } from '../../../store/actions';
import { getCustomTxParamsData } from '../../../pages/confirm-approve/confirm-approve.util'; import { getCustomTxParamsData } from '../../../pages/confirm-approve/confirm-approve.util';
import { useGasFeeContext } from '../../../contexts/gasFee'; import { useGasFeeContext } from '../../../contexts/gasFee';
import ZENDESK_URLS from '../../../helpers/constants/zendesk-url';
import { CustomSpendingCapTooltip } from './custom-spending-cap-tooltip'; import { CustomSpendingCapTooltip } from './custom-spending-cap-tooltip';
export default function CustomSpendingCap({ export default function CustomSpendingCap({
@ -45,18 +45,17 @@ export default function CustomSpendingCap({
passTheErrorText, passTheErrorText,
decimals, decimals,
setInputChangeInProgress, setInputChangeInProgress,
customSpendingCap,
setCustomSpendingCap,
}) { }) {
const t = useContext(I18nContext); const t = useContext(I18nContext);
const dispatch = useDispatch(); const dispatch = useDispatch();
const { updateTransaction } = useGasFeeContext(); const { updateTransaction } = useGasFeeContext();
const inputRef = useRef(null); const inputRef = useRef(null);
const value = useSelector(getCustomTokenAmount);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [showUseDefaultButton, setShowUseDefaultButton] = useState( const [showUseSiteSuggestionButton, setShowUseSiteSuggestionButton] =
value !== String(dappProposedValue) && true, useState(customSpendingCap !== String(dappProposedValue) && true);
);
const inputLogicEmptyStateText = t('inputLogicEmptyState'); const inputLogicEmptyStateText = t('inputLogicEmptyState');
const replaceCommaToDot = (inputValue) => { const replaceCommaToDot = (inputValue) => {
@ -102,7 +101,7 @@ export default function CustomSpendingCap({
}; };
const [customSpendingCapText, setCustomSpendingCapText] = useState( const [customSpendingCapText, setCustomSpendingCapText] = useState(
getInputTextLogic(value).description, getInputTextLogic(customSpendingCap).description,
); );
const handleChange = async (valueInput) => { const handleChange = async (valueInput) => {
@ -139,37 +138,42 @@ export default function CustomSpendingCap({
setError(spendingCapError); setError(spendingCapError);
} }
} }
setCustomSpendingCap(String(valueInput));
dispatch(setCustomTokenAmount(String(valueInput))); dispatch(setCustomTokenAmount(String(valueInput)));
try { if (String(valueInput) !== '') {
const newData = getCustomTxParamsData(txParams.data, { try {
customPermissionAmount: valueInput, const newData = getCustomTxParamsData(txParams.data, {
decimals, customPermissionAmount: valueInput,
}); decimals,
const { from, to, value: txValue } = txParams;
const estimatedGasLimit = await estimateGas({
from,
to,
value: txValue,
data: newData,
});
if (estimatedGasLimit) {
await updateTransaction({
gasLimit: hexToDecimal(addHexPrefix(estimatedGasLimit)),
}); });
const { from, to, value: txValue } = txParams;
const estimatedGasLimit = await estimateGas({
from,
to,
value: txValue,
data: newData,
});
if (estimatedGasLimit) {
await updateTransaction({
gasLimit: hexToDecimal(addHexPrefix(estimatedGasLimit)),
});
}
} catch (exp) {
console.error('Error in trying to update gas limit', exp);
} }
} catch (exp) {
console.error('Error in trying to update gas limit', exp);
} }
setInputChangeInProgress(false); setInputChangeInProgress(false);
}; };
useEffect(() => { useEffect(() => {
if (value !== String(dappProposedValue)) { if (customSpendingCap === String(dappProposedValue)) {
setShowUseDefaultButton(true); setShowUseSiteSuggestionButton(false);
} else {
setShowUseSiteSuggestionButton(true);
} }
}, [value, dappProposedValue]); }, [customSpendingCap, dappProposedValue]);
useEffect(() => { useEffect(() => {
passTheErrorText(error); passTheErrorText(error);
@ -185,7 +189,7 @@ export default function CustomSpendingCap({
}, [inputRef.current]); }, [inputRef.current]);
const chooseTooltipContentText = decConversionGreaterThan( const chooseTooltipContentText = decConversionGreaterThan(
value, customSpendingCap,
currentTokenBalance, currentTokenBalance,
) )
? t('warningTooltipText', [ ? t('warningTooltipText', [
@ -221,7 +225,7 @@ export default function CustomSpendingCap({
> >
<label <label
htmlFor={ htmlFor={
decConversionGreaterThan(value, currentTokenBalance) decConversionGreaterThan(customSpendingCap, currentTokenBalance)
? 'custom-spending-cap-input-value' ? 'custom-spending-cap-input-value'
: 'custom-spending-cap' : 'custom-spending-cap'
} }
@ -231,18 +235,23 @@ export default function CustomSpendingCap({
dataTestId="custom-spending-cap-input" dataTestId="custom-spending-cap-input"
wrappingLabelProps={{ as: 'div' }} wrappingLabelProps={{ as: 'div' }}
id={ id={
decConversionGreaterThan(value, currentTokenBalance) decConversionGreaterThan(customSpendingCap, currentTokenBalance)
? 'custom-spending-cap-input-value' ? 'custom-spending-cap-input-value'
: 'custom-spending-cap' : 'custom-spending-cap'
} }
TooltipCustomComponent={ TooltipCustomComponent={
<CustomSpendingCapTooltip <CustomSpendingCapTooltip
tooltipContentText={ tooltipContentText={
replaceCommaToDot(value) ? chooseTooltipContentText : '' replaceCommaToDot(customSpendingCap)
? chooseTooltipContentText
: ''
} }
tooltipIcon={ tooltipIcon={
replaceCommaToDot(value) replaceCommaToDot(customSpendingCap)
? decConversionGreaterThan(value, currentTokenBalance) ? decConversionGreaterThan(
customSpendingCap,
currentTokenBalance,
)
: '' : ''
} }
/> />
@ -251,18 +260,18 @@ export default function CustomSpendingCap({
titleText={t('customSpendingCap')} titleText={t('customSpendingCap')}
placeholder={t('enterANumber')} placeholder={t('enterANumber')}
error={error} error={error}
value={value} value={customSpendingCap}
titleDetail={ titleDetail={
showUseDefaultButton && ( showUseSiteSuggestionButton && (
<ButtonLink <ButtonLink
size={Size.auto} size={Size.auto}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
setShowUseDefaultButton(false); setShowUseSiteSuggestionButton(false);
handleChange(dappProposedValue); handleChange(dappProposedValue);
}} }}
> >
{t('useDefault')} {t('useSiteSuggestion')}
</ButtonLink> </ButtonLink>
) )
} }
@ -298,12 +307,19 @@ export default function CustomSpendingCap({
variant={TextVariant.bodySm} variant={TextVariant.bodySm}
as="h6" as="h6"
paddingTop={2} paddingTop={2}
paddingBottom={2}
> >
{replaceCommaToDot(value) {replaceCommaToDot(customSpendingCap)
? customSpendingCapText ? customSpendingCapText
: inputLogicEmptyStateText} : inputLogicEmptyStateText}
</Text> </Text>
<ButtonLink
size={Size.SM}
href={ZENDESK_URLS.TOKEN_ALLOWANCE_WITH_SPENDING_CAP}
target="_blank"
marginBottom={2}
>
{t('learnMoreUpperCase')}
</ButtonLink>
</Box> </Box>
</label> </label>
</Box> </Box>
@ -345,4 +361,12 @@ CustomSpendingCap.propTypes = {
* Updating input state to changing * Updating input state to changing
*/ */
setInputChangeInProgress: PropTypes.func.isRequired, setInputChangeInProgress: PropTypes.func.isRequired,
/**
* Custom token amount or The dapp suggested amount
*/
customSpendingCap: PropTypes.string,
/**
* State method to update the custom token value
*/
setCustomSpendingCap: PropTypes.func.isRequired,
}; };

View File

@ -23,6 +23,9 @@ export default {
decimals: { decimals: {
control: 'text', control: 'text',
}, },
customSpendingCap: {
control: { type: 'text' },
},
}, },
args: { args: {
tokenName: 'DAI', tokenName: 'DAI',
@ -30,6 +33,7 @@ export default {
dappProposedValue: '7', dappProposedValue: '7',
siteOrigin: 'Uniswap.org', siteOrigin: 'Uniswap.org',
decimals: '4', decimals: '4',
customSpendingCap: '7',
}, },
}; };
@ -38,3 +42,13 @@ export const DefaultStory = (args) => {
}; };
DefaultStory.storyName = 'Default'; DefaultStory.storyName = 'Default';
export const CustomSpendingCapStory = (args) => {
return <CustomSpendingCap {...args} />;
};
CustomSpendingCapStory.storyName = 'CustomSpendingCapStory';
CustomSpendingCapStory.args = {
...DefaultStory.args,
customSpendingCap: '8',
};

View File

@ -26,6 +26,8 @@ const props = {
decimals: '4', decimals: '4',
passTheErrorText: () => undefined, passTheErrorText: () => undefined,
setInputChangeInProgress: () => undefined, setInputChangeInProgress: () => undefined,
customSpendingCap: '7',
setCustomSpendingCap: () => undefined,
}; };
describe('CustomSpendingCap', () => { describe('CustomSpendingCap', () => {

View File

@ -2,11 +2,9 @@ import React, { useCallback, useContext, useRef, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import { I18nContext } from '../../../contexts/i18n'; import { I18nContext } from '../../../contexts/i18n';
import { Button } from '../../component-library';
import Box from '../../ui/box';
import { import {
AlignItems, AlignItems,
DISPLAY, Display,
JustifyContent, JustifyContent,
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import { MetaMetricsContext } from '../../../contexts/metametrics'; import { MetaMetricsContext } from '../../../contexts/metametrics';
@ -15,6 +13,7 @@ import {
MetaMetricsEventKeyType, MetaMetricsEventKeyType,
MetaMetricsEventName, MetaMetricsEventName,
} from '../../../../shared/constants/metametrics'; } from '../../../../shared/constants/metametrics';
import { Button, Box } from '../../component-library';
const radius = 14; const radius = 14;
const strokeWidth = 2; const strokeWidth = 2;
@ -131,7 +130,7 @@ export default function HoldToRevealButton({ buttonText, onLongPressed }) {
</svg> </svg>
</Box> </Box>
<Box <Box
display={DISPLAY.FLEX} display={Display.Flex}
alignItems={AlignItems.center} alignItems={AlignItems.center}
justifyContent={JustifyContent.center} justifyContent={JustifyContent.center}
className="hold-to-reveal-button__lock-icon-container" className="hold-to-reveal-button__lock-icon-container"
@ -197,10 +196,10 @@ export default function HoldToRevealButton({ buttonText, onLongPressed }) {
return ( return (
<Button <Button
onMouseDown={onMouseDown} onPointerDown={onMouseDown} // allows for touch and mouse events
onMouseUp={onMouseUp} onPointerUp={onMouseUp} // allows for touch and mouse events
className="hold-to-reveal-button__button-hold" className="hold-to-reveal-button__button-hold"
textProps={{ display: DISPLAY.FLEX, alignItems: AlignItems.center }} textProps={{ display: Display.Flex, alignItems: AlignItems.center }}
> >
<Box className="hold-to-reveal-button__icon-container" marginRight={2}> <Box className="hold-to-reveal-button__icon-container" marginRight={2}>
{renderPreCompleteContent()} {renderPreCompleteContent()}

View File

@ -86,7 +86,7 @@ describe('HoldToRevealButton', () => {
const button = getByText('Hold to reveal SRP'); const button = getByText('Hold to reveal SRP');
fireEvent.mouseDown(button); fireEvent.pointerDown(button);
const circleLocked = getByLabelText('hold to reveal circle locked'); const circleLocked = getByLabelText('hold to reveal circle locked');
fireEvent.transitionEnd(circleLocked); fireEvent.transitionEnd(circleLocked);

View File

@ -44,7 +44,6 @@ export const safeComponentList = {
SnapDelineator, SnapDelineator,
Copyable, Copyable,
Spinner, Spinner,
hr: 'hr',
SnapUIMarkdown, SnapUIMarkdown,
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
}; };

View File

@ -114,7 +114,7 @@ describe('Export Private Key Modal', () => {
const holdButton = getByText('Hold to reveal Private Key'); const holdButton = getByText('Hold to reveal Private Key');
expect(holdButton).toBeInTheDocument(); expect(holdButton).toBeInTheDocument();
fireEvent.mouseDown(holdButton); fireEvent.pointerDown(holdButton);
const circle = getByLabelText('hold to reveal circle locked'); const circle = getByLabelText('hold to reveal circle locked');
fireEvent.transitionEnd(circle); fireEvent.transitionEnd(circle);

View File

@ -67,7 +67,7 @@ describe('Hold to Reveal Modal', () => {
expect(holdButton).toBeDefined(); expect(holdButton).toBeDefined();
fireEvent.mouseUp(holdButton); fireEvent.pointerUp(holdButton);
expect(holdButton).toBeDefined(); expect(holdButton).toBeDefined();
}); });
@ -84,7 +84,7 @@ describe('Hold to Reveal Modal', () => {
const holdButton = getByText('Hold to reveal SRP'); const holdButton = getByText('Hold to reveal SRP');
const circleLocked = queryByLabelText('hold to reveal circle locked'); const circleLocked = queryByLabelText('hold to reveal circle locked');
fireEvent.mouseDown(holdButton); fireEvent.pointerDown(holdButton);
fireEvent.transitionEnd(circleLocked); fireEvent.transitionEnd(circleLocked);
const circleUnlocked = queryByLabelText('hold to reveal circle unlocked'); const circleUnlocked = queryByLabelText('hold to reveal circle unlocked');
@ -92,7 +92,7 @@ describe('Hold to Reveal Modal', () => {
await waitFor(() => { await waitFor(() => {
expect(holdButton.firstChild).toHaveClass( expect(holdButton.firstChild).toHaveClass(
'box hold-to-reveal-button__icon-container box--flex-direction-row', 'hold-to-reveal-button__icon-container',
); );
expect(onLongPressStub).toHaveBeenCalled(); expect(onLongPressStub).toHaveBeenCalled();
}); });
@ -164,7 +164,7 @@ describe('Hold to Reveal Modal', () => {
const holdButton = getByText('Hold to reveal SRP'); const holdButton = getByText('Hold to reveal SRP');
const circleLocked = queryByLabelText('hold to reveal circle locked'); const circleLocked = queryByLabelText('hold to reveal circle locked');
fireEvent.mouseDown(holdButton); fireEvent.pointerDown(holdButton);
fireEvent.transitionEnd(circleLocked); fireEvent.transitionEnd(circleLocked);
const circleUnlocked = queryByLabelText('hold to reveal circle unlocked'); const circleUnlocked = queryByLabelText('hold to reveal circle unlocked');
@ -172,7 +172,7 @@ describe('Hold to Reveal Modal', () => {
await waitFor(() => { await waitFor(() => {
expect(holdButton.firstChild).toHaveClass( expect(holdButton.firstChild).toHaveClass(
'box hold-to-reveal-button__icon-container box--flex-direction-row', 'hold-to-reveal-button__icon-container',
); );
expect(onLongPressStub).toHaveBeenCalled(); expect(onLongPressStub).toHaveBeenCalled();
expect(hideModalStub).not.toHaveBeenCalled(); expect(hideModalStub).not.toHaveBeenCalled();

View File

@ -26,11 +26,11 @@ exports[`NFT Details should match minimal props and state snapshot 1`] = `
<div> <div>
<button <button
aria-label="NFT Options" aria-label="NFT Options"
class="box mm-button-icon mm-button-icon--size-lg nft-options__button box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-text-default box--background-color-transparent box--rounded-lg" class="box mm-button-icon mm-button-icon--size-sm nft-options__button box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-text-default box--background-color-transparent box--rounded-lg"
data-testid="nft-options__button" data-testid="nft-options__button"
> >
<span <span
class="box mm-icon mm-icon--size-lg box--display-inline-block box--flex-direction-row box--color-inherit" class="box mm-icon mm-icon--size-sm box--display-inline-block box--flex-direction-row box--color-inherit"
style="mask-image: url('./images/icons/more-vertical.svg');" style="mask-image: url('./images/icons/more-vertical.svg');"
/> />
</button> </button>
@ -46,7 +46,7 @@ exports[`NFT Details should match minimal props and state snapshot 1`] = `
class="box nft-details__nft-item box--flex-direction-row" class="box nft-details__nft-item box--flex-direction-row"
> >
<button <button
class="box nft-item__container box--flex-direction-row" class="mm-box nft-item__container"
data-testid="nft-item" data-testid="nft-item"
> >
<div <div
@ -54,7 +54,7 @@ exports[`NFT Details should match minimal props and state snapshot 1`] = `
> >
<img <img
alt="MUNK #1 1" alt="MUNK #1 1"
class="box nft-item__item nft-item__item-image box--display-block box--flex-direction-row box--justify-content-center" class="mm-box nft-item__item nft-item__item-image mm-box--display-block mm-box--justify-content-center"
data-testid="nft-image" data-testid="nft-image"
src="https://bafybeiclzx7zfjvuiuwobn5ip3ogc236bjqfjzoblumf4pau4ep6dqramu.ipfs.dweb.link" src="https://bafybeiclzx7zfjvuiuwobn5ip3ogc236bjqfjzoblumf4pau4ep6dqramu.ipfs.dweb.link"
/> />

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { I18nContext } from '../../../contexts/i18n'; import { I18nContext } from '../../../contexts/i18n';
import { Menu, MenuItem } from '../../ui/menu'; import { Menu, MenuItem } from '../../ui/menu';
import { ButtonIcon, IconName } from '../../component-library'; import { ButtonIcon, ButtonIconSize, IconName } from '../../component-library';
import { Color } from '../../../helpers/constants/design-system'; import { Color } from '../../../helpers/constants/design-system';
const NftOptions = ({ onRemove, onViewOnOpensea }) => { const NftOptions = ({ onRemove, onViewOnOpensea }) => {
@ -19,6 +19,7 @@ const NftOptions = ({ onRemove, onViewOnOpensea }) => {
data-testid="nft-options__button" data-testid="nft-options__button"
onClick={() => setNftOptionsOpen(true)} onClick={() => setNftOptionsOpen(true)}
color={Color.textDefault} color={Color.textDefault}
size={ButtonIconSize.Sm}
ariaLabel={t('nftOptions')} ariaLabel={t('nftOptions')}
/> />

View File

@ -0,0 +1,101 @@
import React, { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import { useDispatch } from 'react-redux';
import Box from '../../ui/box';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { IconName, ButtonIcon, Text } from '../../component-library';
import { Menu, MenuItem } from '../../ui/menu';
import {
TextColor,
TextVariant,
} from '../../../helpers/constants/design-system';
import Popover from '../../ui/popover/popover.component';
import { DynamicSnapPermissions } from '../../../../shared/constants/snaps/permissions';
import { revokeDynamicSnapPermissions } from '../../../store/actions';
export const PermissionCellOptions = ({
snapId,
permissionName,
description,
}) => {
const t = useI18nContext();
const dispatch = useDispatch();
const ref = useRef(false);
const [showOptions, setShowOptions] = useState(false);
const [showDetails, setShowDetails] = useState(false);
const isRevokable = DynamicSnapPermissions.includes(permissionName);
const handleOpen = () => {
setShowOptions(true);
};
const handleClose = () => {
setShowOptions(false);
};
const handleDetailsOpen = () => {
setShowOptions(false);
setShowDetails(true);
};
const handleDetailsClose = () => {
setShowOptions(false);
setShowDetails(false);
};
const handleRevokePermission = () => {
setShowOptions(false);
dispatch(revokeDynamicSnapPermissions(snapId, [permissionName]));
};
return (
<Box ref={ref}>
<ButtonIcon
iconName={IconName.MoreVertical}
ariaLabel={t('options')}
onClick={handleOpen}
/>
{showOptions && (
<Menu anchorElement={ref.current} onHide={handleClose}>
<MenuItem onClick={handleDetailsOpen}>
<Text
variant={TextVariant.bodySm}
style={{
whiteSpace: 'nowrap',
}}
>
{t('details')}
</Text>
</MenuItem>
{isRevokable && (
<MenuItem onClick={handleRevokePermission}>
<Text
variant={TextVariant.bodySm}
color={TextColor.errorDefault}
style={{
whiteSpace: 'nowrap',
}}
>
{t('revokePermission')}
</Text>
</MenuItem>
)}
</Menu>
)}
{showDetails && (
<Popover title={t('details')} onClose={handleDetailsClose}>
<Box marginLeft={4} marginRight={4} marginBottom={4}>
<Text>{description}</Text>
</Box>
</Popover>
)}
</Box>
);
};
PermissionCellOptions.propTypes = {
snapId: PropTypes.string.isRequired,
permissionName: PropTypes.string.isRequired,
description: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
};

View File

@ -21,14 +21,18 @@ import {
import { formatDate } from '../../../helpers/utils/util'; import { formatDate } from '../../../helpers/utils/util';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import Tooltip from '../../ui/tooltip'; import Tooltip from '../../ui/tooltip';
import { PermissionCellOptions } from './permission-cell-options';
const PermissionCell = ({ const PermissionCell = ({
snapId,
permissionName,
title, title,
description, description,
weight, weight,
avatarIcon, avatarIcon,
dateApproved, dateApproved,
revoked, revoked,
showOptions,
}) => { }) => {
const t = useI18nContext(); const t = useI18nContext();
@ -107,15 +111,25 @@ const PermissionCell = ({
</Text> </Text>
</Box> </Box>
<Box> <Box>
<Tooltip html={<div>{description}</div>} position="bottom"> {showOptions && snapId ? (
<Icon color={infoIconColor} name={infoIcon} size={IconSize.Sm} /> <PermissionCellOptions
</Tooltip> snapId={snapId}
permissionName={permissionName}
description={description}
/>
) : (
<Tooltip html={<div>{description}</div>} position="bottom">
<Icon color={infoIconColor} name={infoIcon} size={IconSize.Sm} />
</Tooltip>
)}
</Box> </Box>
</Box> </Box>
); );
}; };
PermissionCell.propTypes = { PermissionCell.propTypes = {
snapId: PropTypes.string,
permissionName: PropTypes.string.isRequired,
title: PropTypes.oneOfType([ title: PropTypes.oneOfType([
PropTypes.string.isRequired, PropTypes.string.isRequired,
PropTypes.object.isRequired, PropTypes.object.isRequired,
@ -125,6 +139,7 @@ PermissionCell.propTypes = {
avatarIcon: PropTypes.any.isRequired, avatarIcon: PropTypes.any.isRequired,
dateApproved: PropTypes.number, dateApproved: PropTypes.number,
revoked: PropTypes.bool, revoked: PropTypes.bool,
showOptions: PropTypes.bool,
}; };
export default PermissionCell; export default PermissionCell;

View File

@ -19,6 +19,7 @@ describe('Permission Cell', () => {
it('renders approved permission cell', () => { it('renders approved permission cell', () => {
renderWithProvider( renderWithProvider(
<PermissionCell <PermissionCell
permissionName={mockPermissionData.permissionName}
title={mockPermissionData.label} title={mockPermissionData.label}
description={mockPermissionData.description} description={mockPermissionData.description}
weight={mockPermissionData.weight} weight={mockPermissionData.weight}
@ -36,6 +37,7 @@ describe('Permission Cell', () => {
it('renders revoked permission cell', () => { it('renders revoked permission cell', () => {
renderWithProvider( renderWithProvider(
<PermissionCell <PermissionCell
permissionName={mockPermissionData.permissionName}
title={mockPermissionData.label} title={mockPermissionData.label}
description={mockPermissionData.description} description={mockPermissionData.description}
weight={mockPermissionData.weight} weight={mockPermissionData.weight}
@ -54,6 +56,7 @@ describe('Permission Cell', () => {
it('renders requested permission cell', () => { it('renders requested permission cell', () => {
renderWithProvider( renderWithProvider(
<PermissionCell <PermissionCell
permissionName={mockPermissionData.permissionName}
title={mockPermissionData.label} title={mockPermissionData.label}
description={mockPermissionData.description} description={mockPermissionData.description}
weight={mockPermissionData.weight} weight={mockPermissionData.weight}

View File

@ -10,16 +10,24 @@
justify-content: space-between; justify-content: space-between;
} }
@include screen-sm-min { &__footers {
width: 426px; width: 100%;
flex: 1; flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
&__footers { .page-container__footer {
display: flex; align-items: center;
flex-direction: column-reverse; margin-top: 12px;
flex: 1;
padding-bottom: 20px; @include screen-sm-min {
justify-content: space-between; border-top: none;
}
footer {
width: 100%;
}
} }
} }
@ -38,6 +46,7 @@
color: var(--color-text-default); color: var(--color-text-default);
padding-left: 24px; padding-left: 24px;
padding-right: 24px; padding-right: 24px;
margin-top: 2px;
a, a,
a:hover { a:hover {
@ -90,19 +99,6 @@
margin-top: 38px; margin-top: 38px;
} }
.page-container__footer {
align-items: center;
margin-top: 12px;
@include screen-sm-min {
border-top: none;
}
footer {
width: 100%;
}
}
@include screen-sm-max { @include screen-sm-max {
&__title { &__title {
position: initial; position: initial;

View File

@ -1,5 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
///: BEGIN:ONLY_INCLUDE_IN(snaps)
import { SubjectType } from '@metamask/permission-controller';
///: END:ONLY_INCLUDE_IN
import PermissionsConnectHeader from '../../permissions-connect-header'; import PermissionsConnectHeader from '../../permissions-connect-header';
import Tooltip from '../../../ui/tooltip'; import Tooltip from '../../../ui/tooltip';
import PermissionsConnectPermissionList from '../../permissions-connect-permission-list'; import PermissionsConnectPermissionList from '../../permissions-connect-permission-list';
@ -96,12 +99,28 @@ export default class PermissionPageContainerContent extends PureComponent {
return t('connectTo', [selectedIdentities[0]?.addressLabel]); return t('connectTo', [selectedIdentities[0]?.addressLabel]);
} }
render() { getHeaderText() {
const { subjectMetadata } = this.props; const { subjectMetadata } = this.props;
const { t } = this.context; const { t } = this.context;
///: BEGIN:ONLY_INCLUDE_IN(snaps)
if (subjectMetadata.subjectType === SubjectType.Snap) {
return t('allowThisSnapTo');
}
///: END:ONLY_INCLUDE_IN
return subjectMetadata.extensionId
? t('allowExternalExtensionTo', [subjectMetadata.extensionId])
: t('allowThisSiteTo');
}
render() {
const { subjectMetadata } = this.props;
const title = this.getTitle(); const title = this.getTitle();
const headerText = this.getHeaderText();
return ( return (
<div className="permission-approval-container__content"> <div className="permission-approval-container__content">
<div className="permission-approval-container__content-container"> <div className="permission-approval-container__content-container">
@ -109,12 +128,9 @@ export default class PermissionPageContainerContent extends PureComponent {
iconUrl={subjectMetadata.iconUrl} iconUrl={subjectMetadata.iconUrl}
iconName={subjectMetadata.name} iconName={subjectMetadata.name}
headerTitle={title} headerTitle={title}
headerText={ headerText={headerText}
subjectMetadata.extensionId
? t('allowExternalExtensionTo', [subjectMetadata.extensionId])
: t('allowThisSiteTo')
}
siteOrigin={subjectMetadata.origin} siteOrigin={subjectMetadata.origin}
subjectType={subjectMetadata.subjectType}
/> />
<section className="permission-approval-container__permissions-container"> <section className="permission-approval-container__permissions-container">
{this.renderRequestedPermissions()} {this.renderRequestedPermissions()}

View File

@ -7,6 +7,7 @@ import {
WALLET_SNAP_PERMISSION_KEY, WALLET_SNAP_PERMISSION_KEY,
} from '@metamask/rpc-methods'; } from '@metamask/rpc-methods';
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
import { SubjectType } from '@metamask/permission-controller';
import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics';
import { PageContainerFooter } from '../../ui/page-container'; import { PageContainerFooter } from '../../ui/page-container';
import PermissionsConnectFooter from '../permissions-connect-footer'; import PermissionsConnectFooter from '../permissions-connect-footer';
@ -189,7 +190,7 @@ export default class PermissionPageContainer extends Component {
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
return ( return (
<div className="page-container permission-approval-container"> <>
{ {
///: BEGIN:ONLY_INCLUDE_IN(snaps) ///: BEGIN:ONLY_INCLUDE_IN(snaps)
<> <>
@ -210,7 +211,9 @@ export default class PermissionPageContainer extends Component {
allIdentitiesSelected={allIdentitiesSelected} allIdentitiesSelected={allIdentitiesSelected}
/> />
<div className="permission-approval-container__footers"> <div className="permission-approval-container__footers">
<PermissionsConnectFooter /> {targetSubjectMetadata?.subjectType !== SubjectType.Snap && (
<PermissionsConnectFooter />
)}
<PageContainerFooter <PageContainerFooter
cancelButtonType="default" cancelButtonType="default"
onCancel={() => this.onCancel()} onCancel={() => this.onCancel()}
@ -220,7 +223,7 @@ export default class PermissionPageContainer extends Component {
buttonSizeLarge={false} buttonSizeLarge={false}
/> />
</div> </div>
</div> </>
); );
} }
} }

View File

@ -1,6 +1,9 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
///: BEGIN:ONLY_INCLUDE_IN(snaps)
import { SubjectType } from '@metamask/subject-metadata-controller';
///: END:ONLY_INCLUDE_IN
import SiteOrigin from '../../ui/site-origin'; import SiteOrigin from '../../ui/site-origin';
import Box from '../../ui/box'; import Box from '../../ui/box';
import { import {
@ -19,6 +22,7 @@ export default class PermissionsConnectHeader extends Component {
headerText: PropTypes.string, headerText: PropTypes.string,
leftIcon: PropTypes.node, leftIcon: PropTypes.node,
rightIcon: PropTypes.node, rightIcon: PropTypes.node,
subjectType: PropTypes.string,
}; };
static defaultProps = { static defaultProps = {
@ -29,7 +33,23 @@ export default class PermissionsConnectHeader extends Component {
}; };
renderHeaderIcon() { renderHeaderIcon() {
const { iconUrl, iconName, siteOrigin, leftIcon, rightIcon } = this.props; const {
iconUrl,
iconName,
siteOrigin,
leftIcon,
rightIcon,
///: BEGIN:ONLY_INCLUDE_IN(snaps)
subjectType,
///: END:ONLY_INCLUDE_IN
} = this.props;
///: BEGIN:ONLY_INCLUDE_IN(snaps)
if (subjectType === SubjectType.Snap) {
return null;
}
///: END:ONLY_INCLUDE_IN
return ( return (
<div className="permissions-connect-header__icon"> <div className="permissions-connect-header__icon">

View File

@ -64,7 +64,10 @@ export default class SignatureRequestOriginal extends Component {
resolvePendingApproval: PropTypes.func.isRequired, resolvePendingApproval: PropTypes.func.isRequired,
completedTx: PropTypes.func.isRequired, completedTx: PropTypes.func.isRequired,
///: BEGIN:ONLY_INCLUDE_IN(build-mmi) ///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
// Used to show a warning if the signing account is not the selected account
// Largely relevant for contract wallet custodians
selectedAccount: PropTypes.object, selectedAccount: PropTypes.object,
mmiOnSignCallback: PropTypes.func,
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
}; };
@ -262,7 +265,7 @@ export default class SignatureRequestOriginal extends Component {
clearConfirmTransaction, clearConfirmTransaction,
history, history,
mostRecentOverviewPage, mostRecentOverviewPage,
txData: { type, id }, txData,
hardwareWalletRequiresConnection, hardwareWalletRequiresConnection,
rejectPendingApproval, rejectPendingApproval,
resolvePendingApproval, resolvePendingApproval,
@ -275,22 +278,34 @@ export default class SignatureRequestOriginal extends Component {
submitText={t('sign')} submitText={t('sign')}
onCancel={async () => { onCancel={async () => {
await rejectPendingApproval( await rejectPendingApproval(
id, txData.id,
serializeError(ethErrors.provider.userRejectedRequest()), serializeError(ethErrors.provider.userRejectedRequest()),
); );
clearConfirmTransaction(); clearConfirmTransaction();
history.push(mostRecentOverviewPage); history.push(mostRecentOverviewPage);
}} }}
onSubmit={async () => { onSubmit={async () => {
if (type === MESSAGE_TYPE.ETH_SIGN) { if (txData.type === MESSAGE_TYPE.ETH_SIGN) {
this.setState({ showSignatureRequestWarning: true }); this.setState({ showSignatureRequestWarning: true });
} else { } else {
await resolvePendingApproval(id); ///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
if (this.props.mmiOnSignCallback) {
await this.props.mmiOnSignCallback(txData);
return;
}
///: END:ONLY_INCLUDE_IN
await resolvePendingApproval(txData.id);
clearConfirmTransaction(); clearConfirmTransaction();
history.push(mostRecentOverviewPage); history.push(mostRecentOverviewPage);
} }
}} }}
disabled={hardwareWalletRequiresConnection} disabled={
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
Boolean(txData?.custodyId) ||
///: END:ONLY_INCLUDE_IN
hardwareWalletRequiresConnection
}
/> />
); );
}; };

View File

@ -9,6 +9,17 @@ import {
rejectAllMessages, rejectAllMessages,
completedTx, completedTx,
} from '../../../store/actions'; } from '../../../store/actions';
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
// eslint-disable-next-line import/order
import { showCustodianDeepLink } from '@metamask-institutional/extension';
import {
mmiActionsFactory,
setPersonalMessageInProgress,
} from '../../../store/institutional/institution-background';
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { checkForUnapprovedMessages } from '../../../store/institutional/institution-actions';
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../shared/constants/app';
///: END:ONLY_INCLUDE_IN
import { import {
accountsWithSendEtherInfoSelector, accountsWithSendEtherInfoSelector,
getSubjectMetadata, getSubjectMetadata,
@ -16,6 +27,8 @@ import {
unconfirmedMessagesHashSelector, unconfirmedMessagesHashSelector,
getTotalUnapprovedMessagesCount, getTotalUnapprovedMessagesCount,
///: BEGIN:ONLY_INCLUDE_IN(build-mmi) ///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
unapprovedPersonalMsgsSelector,
getAccountType,
getSelectedAccount, getSelectedAccount,
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
} from '../../../selectors'; } from '../../../selectors';
@ -30,6 +43,10 @@ function mapStateToProps(state, ownProps) {
msgParams: { from }, msgParams: { from },
} = ownProps.txData; } = ownProps.txData;
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
const envType = getEnvironmentType();
///: END:ONLY_INCLUDE_IN
const hardwareWalletRequiresConnection = const hardwareWalletRequiresConnection =
doesAddressRequireLedgerHidConnection(state, from); doesAddressRequireLedgerHidConnection(state, from);
const isLedgerWallet = isAddressLedger(state, from); const isLedgerWallet = isAddressLedger(state, from);
@ -48,12 +65,63 @@ function mapStateToProps(state, ownProps) {
messagesList, messagesList,
messagesCount, messagesCount,
///: BEGIN:ONLY_INCLUDE_IN(build-mmi) ///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
accountType: getAccountType(state),
isNotification: envType === ENVIRONMENT_TYPE_NOTIFICATION,
selectedAccount: getSelectedAccount(state), selectedAccount: getSelectedAccount(state),
unapprovedPersonalMessages: unapprovedPersonalMsgsSelector(state),
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
}; };
} }
function mapDispatchToProps(dispatch) { let mapDispatchToProps = null;
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
function mmiMapDispatchToProps(dispatch) {
const mmiActions = mmiActionsFactory();
return {
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
setMsgInProgress: (msgId) => dispatch(setPersonalMessageInProgress(msgId)),
showCustodianDeepLink: ({
custodyId,
fromAddress,
closeNotification,
onDeepLinkFetched,
onDeepLinkShown,
}) =>
showCustodianDeepLink({
dispatch,
mmiActions,
txId: undefined,
fromAddress,
custodyId,
isSignature: true,
closeNotification,
onDeepLinkFetched,
onDeepLinkShown,
}),
showTransactionsFailedModal: ({
errorMessage,
closeNotification,
operationFailed,
}) =>
dispatch(
showModal({
name: 'TRANSACTION_FAILED',
errorMessage,
closeNotification,
operationFailed,
}),
),
setWaitForConfirmDeepLinkDialog: (wait) =>
dispatch(mmiActions.setWaitForConfirmDeepLinkDialog(wait)),
goHome: () => dispatch(goHome()),
};
}
mapDispatchToProps = mmiMapDispatchToProps;
///: END:ONLY_INCLUDE_IN
mapDispatchToProps = function (dispatch) {
return { return {
goHome: () => dispatch(goHome()), goHome: () => dispatch(goHome()),
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
@ -80,12 +148,21 @@ function mapDispatchToProps(dispatch) {
dispatch(rejectAllMessages(messagesList)); dispatch(rejectAllMessages(messagesList));
}, },
}; };
} };
function mergeProps(stateProps, dispatchProps, ownProps) { function mergeProps(stateProps, dispatchProps, ownProps) {
const { txData } = ownProps; const { txData } = ownProps;
const { allAccounts, messagesList, ...otherStateProps } = stateProps; const {
allAccounts,
messagesList,
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
accountType,
isNotification,
unapprovedPersonalMessages,
///: END:ONLY_INCLUDE_IN
...otherStateProps
} = stateProps;
const { const {
msgParams: { from }, msgParams: { from },
@ -95,6 +172,41 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
const { cancelAllApprovals: dispatchCancelAllApprovals } = dispatchProps; const { cancelAllApprovals: dispatchCancelAllApprovals } = dispatchProps;
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
const mmiOnSignCallback = async (_msgData) => {
if (accountType === 'custody') {
try {
let msgData = _msgData;
let id = _msgData.custodyId;
if (!_msgData.custodyId) {
msgData = checkForUnapprovedMessages(
_msgData,
unapprovedPersonalMessages,
);
id = msgData.custodyId;
}
dispatchProps.showCustodianDeepLink({
custodyId: id,
fromAddress: fromAccount.address,
closeNotification: isNotification,
onDeepLinkFetched: () => undefined,
onDeepLinkShown: () => undefined,
});
await dispatchProps.setMsgInProgress(msgData.metamaskId);
await dispatchProps.setWaitForConfirmDeepLinkDialog(true);
await goHome();
} catch (err) {
await dispatchProps.setWaitForConfirmDeepLinkDialog(true);
await dispatchProps.showTransactionsFailedModal({
errorMessage: err.message,
closeNotification: true,
operationFailed: true,
});
}
}
};
///: END:ONLY_INCLUDE_IN
return { return {
...ownProps, ...ownProps,
...otherStateProps, ...otherStateProps,
@ -103,6 +215,9 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
txData, txData,
cancelAllApprovals: () => cancelAllApprovals: () =>
dispatchCancelAllApprovals(valuesFor(messagesList)), dispatchCancelAllApprovals(valuesFor(messagesList)),
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
mmiOnSignCallback,
///: END:ONLY_INCLUDE_IN
}; };
} }

View File

@ -223,14 +223,16 @@ export default class SignatureRequest extends PureComponent {
.toString(); .toString();
const onSign = async () => { const onSign = async () => {
await resolvePendingApproval(id);
completedTx(id);
///: BEGIN:ONLY_INCLUDE_IN(build-mmi) ///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
if (this.props.mmiOnSignCallback) { if (this.props.mmiOnSignCallback) {
await this.props.mmiOnSignCallback(txData); await this.props.mmiOnSignCallback(txData);
return;
} }
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
await resolvePendingApproval(id);
completedTx(id);
trackEvent({ trackEvent({
category: MetaMetricsEventCategory.Transactions, category: MetaMetricsEventCategory.Transactions,
event: 'Confirm', event: 'Confirm',

View File

@ -30,7 +30,7 @@ import {
setTypedMessageInProgress, setTypedMessageInProgress,
} from '../../../store/institutional/institution-background'; } from '../../../store/institutional/institution-background';
import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { checkForUnapprovedTypedMessages } from '../../../store/institutional/institution-actions'; import { checkForUnapprovedMessages } from '../../../store/institutional/institution-actions';
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
import { import {
///: BEGIN:ONLY_INCLUDE_IN(build-mmi) ///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
@ -214,7 +214,7 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
let msgData = _msgData; let msgData = _msgData;
let id = _msgData.custodyId; let id = _msgData.custodyId;
if (!_msgData.custodyId) { if (!_msgData.custodyId) {
msgData = checkForUnapprovedTypedMessages( msgData = checkForUnapprovedMessages(
_msgData, _msgData,
unapprovedTypedMessages, unapprovedTypedMessages,
); );

View File

@ -24,7 +24,11 @@ import { getTargetSubjectMetadata } from '../../../../selectors';
import SnapAvatar from '../snap-avatar'; import SnapAvatar from '../snap-avatar';
import SnapVersion from '../snap-version/snap-version'; import SnapVersion from '../snap-version/snap-version';
const SnapAuthorshipHeader = ({ snapId, className }) => { const SnapAuthorshipHeader = ({
snapId,
className,
boxShadow = 'var(--shadow-size-lg) var(--color-shadow-default)',
}) => {
// We're using optional chaining with snapId, because with the current implementation // We're using optional chaining with snapId, because with the current implementation
// of snap update in the snap controller, we do not have reference to snapId when an // of snap update in the snap controller, we do not have reference to snapId when an
// update request is rejected because the reference comes from the request itself and not subject metadata // update request is rejected because the reference comes from the request itself and not subject metadata
@ -51,7 +55,7 @@ const SnapAuthorshipHeader = ({ snapId, className }) => {
display={DISPLAY.FLEX} display={DISPLAY.FLEX}
padding={4} padding={4}
style={{ style={{
boxShadow: 'var(--shadow-size-lg) var(--color-shadow-default)', boxShadow,
}} }}
> >
<Box> <Box>
@ -91,6 +95,7 @@ SnapAuthorshipHeader.propTypes = {
* The className of the SnapAuthorship * The className of the SnapAuthorship
*/ */
className: PropTypes.string, className: PropTypes.string,
boxShadow: PropTypes.string,
}; };
export default SnapAuthorshipHeader; export default SnapAuthorshipHeader;

View File

@ -6,8 +6,10 @@ import PermissionCell from '../../permission-cell';
import Box from '../../../ui/box'; import Box from '../../../ui/box';
export default function SnapPermissionsList({ export default function SnapPermissionsList({
snapId,
permissions, permissions,
targetSubjectMetadata, targetSubjectMetadata,
showOptions,
}) { }) {
const t = useI18nContext(); const t = useI18nContext();
@ -17,12 +19,15 @@ export default function SnapPermissionsList({
(permission, index) => { (permission, index) => {
return ( return (
<PermissionCell <PermissionCell
snapId={snapId}
permissionName={permission.permissionName}
title={permission.label} title={permission.label}
description={permission.description} description={permission.description}
weight={permission.weight} weight={permission.weight}
avatarIcon={permission.leftIcon} avatarIcon={permission.leftIcon}
dateApproved={permission?.permissionValue?.date} dateApproved={permission?.permissionValue?.date}
key={`${permission.permissionName}-${index}`} key={`${permission.permissionName}-${index}`}
showOptions={showOptions}
/> />
); );
}, },
@ -32,6 +37,8 @@ export default function SnapPermissionsList({
} }
SnapPermissionsList.propTypes = { SnapPermissionsList.propTypes = {
snapId: PropTypes.string.isRequired,
permissions: PropTypes.object.isRequired, permissions: PropTypes.object.isRequired,
targetSubjectMetadata: PropTypes.object.isRequired, targetSubjectMetadata: PropTypes.object.isRequired,
showOptions: PropTypes.bool,
}; };

View File

@ -9,6 +9,7 @@
&__divider { &__divider {
width: 100%; width: 100%;
height: 1px;
} }
&__panel { &__panel {

View File

@ -10,6 +10,7 @@ import {
OverflowWrap, OverflowWrap,
FontWeight, FontWeight,
TextVariant, TextVariant,
BorderColor,
} from '../../../../helpers/constants/design-system'; } from '../../../../helpers/constants/design-system';
import { SnapDelineator } from '../snap-delineator'; import { SnapDelineator } from '../snap-delineator';
import { useI18nContext } from '../../../../hooks/useI18nContext'; import { useI18nContext } from '../../../../hooks/useI18nContext';
@ -53,9 +54,12 @@ export const UI_MAPPING = {
}, },
}), }),
divider: () => ({ divider: () => ({
element: 'hr', element: 'Box',
props: { props: {
className: 'snap-ui-renderer__divider', className: 'snap-ui-renderer__divider',
backgroundColor: BorderColor.borderDefault,
marginTop: 2,
marginBottom: 2,
}, },
}), }),
copyable: (props) => ({ copyable: (props) => ({

View File

@ -18,10 +18,8 @@ import {
Text, Text,
} from '../../../component-library'; } from '../../../component-library';
import Preloader from '../../../ui/icon/preloader/preloader-icon.component'; import Preloader from '../../../ui/icon/preloader/preloader-icon.component';
import { useI18nContext } from '../../../../hooks/useI18nContext';
const SnapVersion = ({ version, url }) => { const SnapVersion = ({ version, url }) => {
const t = useI18nContext();
return ( return (
<Button <Button
variant={BUTTON_VARIANT.LINK} variant={BUTTON_VARIANT.LINK}
@ -42,7 +40,7 @@ const SnapVersion = ({ version, url }) => {
> >
{version ? ( {version ? (
<Text color={Color.textAlternative} variant={TextVariant.bodyMd}> <Text color={Color.textAlternative} variant={TextVariant.bodyMd}>
{t('shortVersion', [version])} {version}
</Text> </Text>
) : ( ) : (
<Preloader size={18} /> <Preloader size={18} />

View File

@ -12,7 +12,7 @@ describe('SnapVersion', () => {
const { getByText, container } = renderWithLocalization( const { getByText, container } = renderWithLocalization(
<SnapVersion {...args} />, <SnapVersion {...args} />,
); );
expect(getByText(`v${args.version}`)).toBeDefined(); expect(getByText(args.version)).toBeDefined();
expect(container.firstChild).toHaveAttribute('href', args.url); expect(container.firstChild).toHaveAttribute('href', args.url);
}); });

View File

@ -18,6 +18,7 @@ export default function UpdateSnapPermissionList({
{getWeightedPermissions(t, newPermissions, targetSubjectMetadata).map( {getWeightedPermissions(t, newPermissions, targetSubjectMetadata).map(
(permission, index) => ( (permission, index) => (
<PermissionCell <PermissionCell
permissionName={permission.permissionName}
title={permission.label} title={permission.label}
description={permission.description} description={permission.description}
weight={permission.weight} weight={permission.weight}
@ -30,6 +31,7 @@ export default function UpdateSnapPermissionList({
{getWeightedPermissions(t, revokedPermissions, targetSubjectMetadata).map( {getWeightedPermissions(t, revokedPermissions, targetSubjectMetadata).map(
(permission, index) => ( (permission, index) => (
<PermissionCell <PermissionCell
permissionName={permission.permissionName}
title={permission.label} title={permission.label}
description={permission.description} description={permission.description}
weight={permission.weight} weight={permission.weight}
@ -46,6 +48,7 @@ export default function UpdateSnapPermissionList({
targetSubjectMetadata, targetSubjectMetadata,
).map((permission, index) => ( ).map((permission, index) => (
<PermissionCell <PermissionCell
permissionName={permission.permissionName}
title={permission.label} title={permission.label}
description={permission.description} description={permission.description}
weight={permission.weight} weight={permission.weight}

View File

@ -44,6 +44,17 @@ export const ModalContent = forwardRef(
}; };
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: MouseEvent) => {
// Popover should be launched from within Modal but
// the Popover containing element is a sibling to modal,
// so this is required to ensure `onClose` isn't triggered
// when clicking on a popover item
if (
isClosedOnOutsideClick &&
(event.target as HTMLElement).closest('.mm-popover')
) {
return;
}
if ( if (
isClosedOnOutsideClick && isClosedOnOutsideClick &&
modalDialogRef?.current && modalDialogRef?.current &&

View File

@ -6,7 +6,7 @@ exports[`NoteToTrader should render the Note to trader component 1`] = `
class="box confirm-page-container-content__data box--flex-direction-row" class="box confirm-page-container-content__data box--flex-direction-row"
> >
<div <div
class="box box--display-flex box--flex-direction-row" class="box box--padding-4 box--display-flex box--flex-direction-column"
> >
<div <div
class="box note-header box--display-flex box--flex-direction-row box--justify-content-space-between" class="box note-header box--display-flex box--flex-direction-row box--justify-content-space-between"

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
DISPLAY, Display,
FLEX_DIRECTION, FlexDirection,
JustifyContent, JustifyContent,
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import { Label, Text } from '../../component-library'; import { Label, Text } from '../../component-library';
@ -13,10 +13,14 @@ const NoteToTrader = (props) => {
return ( return (
<Box className="confirm-page-container-content__data"> <Box className="confirm-page-container-content__data">
<Box display={DISPLAY.FLEX} flexDirection={FLEX_DIRECTION.ROW}> <Box
display={Display.Flex}
flexDirection={FlexDirection.Column}
padding={4}
>
<Box <Box
className="note-header" className="note-header"
display={DISPLAY.FLEX} display={Display.Flex}
justifyContent={JustifyContent.spaceBetween} justifyContent={JustifyContent.spaceBetween}
> >
<Label htmlFor="transaction-note">{labelText}</Label> <Label htmlFor="transaction-note">{labelText}</Label>
@ -25,8 +29,8 @@ const NoteToTrader = (props) => {
</Text> </Text>
</Box> </Box>
<Box <Box
display={DISPLAY.FLEX} display={Display.Flex}
flexDirection={FLEX_DIRECTION.COLUMN} flexDirection={FlexDirection.Column}
className="note-field" className="note-field"
> >
<textarea <textarea

View File

@ -13,8 +13,8 @@ import {
ButtonSecondary, ButtonSecondary,
FormTextField, FormTextField,
Text, Text,
Box,
} from '../../component-library'; } from '../../component-library';
import Box from '../../ui/box/box';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import { exportAccount, hideWarning } from '../../../store/actions'; import { exportAccount, hideWarning } from '../../../store/actions';

View File

@ -15,6 +15,7 @@ import { isAbleToExportAccount } from '../../../helpers/utils/util';
import { import {
BUTTON_SECONDARY_SIZES, BUTTON_SECONDARY_SIZES,
ButtonSecondary, ButtonSecondary,
Box,
} from '../../component-library'; } from '../../component-library';
import { import {
AlignItems, AlignItems,
@ -29,7 +30,6 @@ import {
MetaMetricsEventName, MetaMetricsEventName,
} from '../../../../shared/constants/metametrics'; } from '../../../../shared/constants/metametrics';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import Box from '../../ui/box/box';
export const AccountDetailsDisplay = ({ export const AccountDetailsDisplay = ({
accounts, accounts,

View File

@ -6,6 +6,7 @@ import {
ButtonPrimary, ButtonPrimary,
IconName, IconName,
Text, Text,
Box,
} from '../../component-library'; } from '../../component-library';
import { import {
AlignItems, AlignItems,
@ -17,7 +18,6 @@ import {
TextVariant, TextVariant,
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import Box from '../../ui/box/box';
import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard'; import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
export const AccountDetailsKey = ({ accountName, onClose, privateKey }) => { export const AccountDetailsKey = ({ accountName, onClose, privateKey }) => {

View File

@ -1,7 +1,6 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import Popover from '../../ui/popover/popover.component';
import { import {
setAccountDetailsAddress, setAccountDetailsAddress,
clearAccountDetails, clearAccountDetails,
@ -11,19 +10,18 @@ import {
AvatarAccount, AvatarAccount,
AvatarAccountSize, AvatarAccountSize,
AvatarAccountVariant, AvatarAccountVariant,
ButtonIcon, Modal,
IconName, ModalContent,
PopoverHeader, ModalHeader,
ModalOverlay,
Text, Text,
Box,
} from '../../component-library'; } from '../../component-library';
import Box from '../../ui/box/box';
import { getMetaMaskAccountsOrdered } from '../../../selectors'; import { getMetaMaskAccountsOrdered } from '../../../selectors';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import { import {
AlignItems, AlignItems,
JustifyContent,
TextVariant, TextVariant,
Size,
Display, Display,
FlexDirection, FlexDirection,
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
@ -61,85 +59,68 @@ export const AccountDetails = ({ address }) => {
} }
address={address} address={address}
size={AvatarAccountSize.Lg} size={AvatarAccountSize.Lg}
style={{ margin: '0 auto' }}
/> />
); );
return ( return (
<Popover <Modal isOpen onClose={onClose}>
headerProps={{ <ModalOverlay />
paddingBottom: 1, <ModalContent>
}} <ModalHeader
contentProps={{ onClose={onClose}
paddingLeft: 4, onBack={
paddingRight: 4, attemptingExport
paddingBottom: 4, ? () => {
}}
title={
attemptingExport ? (
<PopoverHeader
startAccessory={
<ButtonIcon
onClick={() => {
dispatch(hideWarning()); dispatch(hideWarning());
setAttemptingExport(false); setAttemptingExport(false);
}} }
iconName={IconName.ArrowLeft} : null
size={Size.SM} }
/> >
} {attemptingExport ? t('showPrivateKey') : avatar}
> </ModalHeader>
{t('showPrivateKey')} {attemptingExport ? (
</PopoverHeader> <>
) : ( <Box
<PopoverHeader display={Display.Flex}
childrenWrapperProps={{ alignItems={AlignItems.center}
display: Display.Text, flexDirection={FlexDirection.Column}
justifyContent: JustifyContent.center,
}}
>
<Box paddingLeft={6}>{avatar}</Box>
</PopoverHeader>
)
}
onClose={onClose}
>
{attemptingExport ? (
<>
<Box
display={Display.Flex}
alignItems={AlignItems.center}
flexDirection={FlexDirection.Column}
>
{avatar}
<Text
marginTop={2}
marginBottom={2}
variant={TextVariant.bodyLgMedium}
style={{ wordBreak: 'break-word' }}
> >
{name} {avatar}
</Text> <Text
<AddressCopyButton address={address} shorten /> marginTop={2}
</Box> marginBottom={2}
{privateKey ? ( variant={TextVariant.bodyLgMedium}
<AccountDetailsKey style={{ wordBreak: 'break-word' }}
accountName={name} >
onClose={onClose} {name}
privateKey={privateKey} </Text>
/> <AddressCopyButton address={address} shorten />
) : ( </Box>
<AccountDetailsAuthenticate address={address} onCancel={onClose} /> {privateKey ? (
)} <AccountDetailsKey
</> accountName={name}
) : ( onClose={onClose}
<AccountDetailsDisplay privateKey={privateKey}
accounts={accounts} />
accountName={name} ) : (
address={address} <AccountDetailsAuthenticate
onExportClick={() => setAttemptingExport(true)} address={address}
/> onCancel={onClose}
)} />
</Popover> )}
</>
) : (
<AccountDetailsDisplay
accounts={accounts}
accountName={name}
address={address}
onExportClick={() => setAttemptingExport(true)}
/>
)}
</ModalContent>
</Modal>
); );
}; };

View File

@ -1,4 +1,4 @@
import React, { useState, useContext, useRef, useEffect } from 'react'; import React, { useState, useContext } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
@ -9,6 +9,10 @@ import {
TextFieldSearch, TextFieldSearch,
Text, Text,
Box, Box,
Modal,
ModalContent,
ModalOverlay,
ModalHeader,
} from '../../component-library'; } from '../../component-library';
import { AccountListItem, CreateAccount, ImportAccount } from '..'; import { AccountListItem, CreateAccount, ImportAccount } from '..';
import { import {
@ -18,7 +22,6 @@ import {
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import { MetaMetricsContext } from '../../../contexts/metametrics'; import { MetaMetricsContext } from '../../../contexts/metametrics';
import Popover from '../../ui/popover';
import { import {
getSelectedAccount, getSelectedAccount,
getMetaMaskAccountsOrdered, getMetaMaskAccountsOrdered,
@ -52,7 +55,6 @@ export const AccountListMenu = ({ onClose }) => {
const currentTabOrigin = useSelector(getOriginOfCurrentTab); const currentTabOrigin = useSelector(getOriginOfCurrentTab);
const history = useHistory(); const history = useHistory();
const dispatch = useDispatch(); const dispatch = useDispatch();
const inputRef = useRef();
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [actionMode, setActionMode] = useState(''); const [actionMode, setActionMode] = useState('');
@ -71,13 +73,6 @@ export const AccountListMenu = ({ onClose }) => {
searchResults = fuse.search(searchQuery); searchResults = fuse.search(searchQuery);
} }
// Focus on the search box when the popover is opened
useEffect(() => {
if (inputRef.current) {
inputRef.current.rootNode.querySelector('input[type=search]')?.focus();
}
}, [inputRef]);
let title = t('selectAnAccount'); let title = t('selectAnAccount');
if (actionMode === 'add') { if (actionMode === 'add') {
title = t('addAccount'); title = t('addAccount');
@ -86,225 +81,244 @@ export const AccountListMenu = ({ onClose }) => {
} }
return ( return (
<Popover <Modal isOpen onClose={onClose}>
title={title} <ModalOverlay />
ref={inputRef} <ModalContent
centerTitle className="multichain-account-menu-popover"
onClose={onClose} modalDialogProps={{ padding: 0, marginBottom: 0 }}
onBack={actionMode === '' ? null : () => setActionMode('')} >
className="multichain-account-menu-popover" <ModalHeader
> paddingTop={4}
{actionMode === 'add' ? ( paddingRight={4}
<Box paddingLeft={4} paddingRight={4} paddingBottom={4} paddingTop={0}> paddingBottom={6}
<CreateAccount onClose={onClose}
onActionComplete={(confirmed) => { onBack={actionMode === '' ? null : () => setActionMode('')}
if (confirmed) { >
dispatch(toggleAccountMenu()); {title}
} else { </ModalHeader>
setActionMode(''); {actionMode === 'add' ? (
} <Box
}} paddingLeft={4}
/> paddingRight={4}
</Box> paddingBottom={4}
) : null} paddingTop={0}
{actionMode === 'import' ? ( >
<Box paddingLeft={4} paddingRight={4} paddingBottom={4} paddingTop={0}> <CreateAccount
<ImportAccount onActionComplete={(confirmed) => {
onActionComplete={(confirmed) => { if (confirmed) {
if (confirmed) { dispatch(toggleAccountMenu());
dispatch(toggleAccountMenu()); } else {
} else { setActionMode('');
setActionMode(''); }
} }}
}} />
/> </Box>
</Box> ) : null}
) : null} {actionMode === 'import' ? (
{actionMode === '' ? ( <Box
<Box> paddingLeft={4}
{/* Search box */} paddingRight={4}
{accounts.length > 100 ? ( paddingBottom={4}
<Box paddingTop={0}
paddingLeft={4} >
paddingRight={4} <ImportAccount
paddingBottom={4} onActionComplete={(confirmed) => {
paddingTop={0} if (confirmed) {
> dispatch(toggleAccountMenu());
<TextFieldSearch } else {
size={Size.SM} setActionMode('');
width={BlockSize.Full} }
placeholder={t('searchAccounts')} }}
value={searchQuery} />
onChange={(e) => setSearchQuery(e.target.value)} </Box>
clearButtonOnClick={() => setSearchQuery('')} ) : null}
clearButtonProps={{ {actionMode === '' ? (
size: Size.SM, <Box>
}} {/* Search box */}
/> {accounts.length > 100 ? (
</Box> <Box
) : null}
{/* Account list block */}
<Box className="multichain-account-menu-popover__list">
{searchResults.length === 0 && searchQuery !== '' ? (
<Text
paddingLeft={4} paddingLeft={4}
paddingRight={4} paddingRight={4}
color={TextColor.textMuted} paddingBottom={4}
data-testid="multichain-account-menu-popover-no-results" paddingTop={0}
> >
{t('noAccountsFound')} <TextFieldSearch
</Text> size={Size.SM}
) : null} width={BlockSize.Full}
{searchResults.map((account) => { placeholder={t('searchAccounts')}
const connectedSite = connectedSites[account.address]?.find( value={searchQuery}
({ origin }) => origin === currentTabOrigin, onChange={(e) => setSearchQuery(e.target.value)}
); clearButtonOnClick={() => setSearchQuery('')}
clearButtonProps={{
return ( size: Size.SM,
<AccountListItem
onClick={() => {
dispatch(toggleAccountMenu());
trackEvent({
category: MetaMetricsEventCategory.Navigation,
event: MetaMetricsEventName.NavAccountSwitched,
properties: {
location: 'Main Menu',
},
});
dispatch(setSelectedAccount(account.address));
}} }}
identity={account} inputProps={{ autoFocus: true }}
key={account.address}
selected={selectedAccount.address === account.address}
closeMenu={onClose}
connectedAvatar={connectedSite?.iconUrl}
connectedAvatarName={connectedSite?.name}
/> />
); </Box>
})} ) : null}
</Box> {/* Account list block */}
{/* Add / Import / Hardware */} <Box className="multichain-account-menu-popover__list">
<Box padding={4}> {searchResults.length === 0 && searchQuery !== '' ? (
<Box> <Text
<ButtonLink paddingLeft={4}
size={Size.SM} paddingRight={4}
startIconName={IconName.Add} color={TextColor.textMuted}
onClick={() => { data-testid="multichain-account-menu-popover-no-results"
trackEvent({ >
category: MetaMetricsEventCategory.Navigation, {t('noAccountsFound')}
event: MetaMetricsEventName.AccountAddSelected, </Text>
properties: { ) : null}
account_type: MetaMetricsEventAccountType.Default, {searchResults.map((account) => {
location: 'Main Menu', const connectedSite = connectedSites[account.address]?.find(
}, ({ origin }) => origin === currentTabOrigin,
}); );
setActionMode('add');
}} return (
data-testid="multichain-account-menu-popover-add-account" <AccountListItem
>
{t('addAccount')}
</ButtonLink>
</Box>
<Box>
<ButtonLink
size={Size.SM}
startIconName={IconName.Import}
onClick={() => {
trackEvent({
category: MetaMetricsEventCategory.Navigation,
event: MetaMetricsEventName.AccountAddSelected,
properties: {
account_type: MetaMetricsEventAccountType.Imported,
location: 'Main Menu',
},
});
setActionMode('import');
}}
>
{t('importAccount')}
</ButtonLink>
</Box>
<Box>
<ButtonLink
size={Size.SM}
startIconName={IconName.Hardware}
onClick={() => {
dispatch(toggleAccountMenu());
trackEvent({
category: MetaMetricsEventCategory.Navigation,
event: MetaMetricsEventName.AccountAddSelected,
properties: {
account_type: MetaMetricsEventAccountType.Hardware,
location: 'Main Menu',
},
});
if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) {
global.platform.openExtensionInBrowser(
CONNECT_HARDWARE_ROUTE,
);
} else {
history.push(CONNECT_HARDWARE_ROUTE);
}
}}
>
{t('hardwareWallet')}
</ButtonLink>
</Box>
{
///: BEGIN:ONLY_INCLUDE_IN(keyring-snaps)
<>
<Box>
<ButtonLink
size={Size.SM}
startIconName={IconName.Snaps}
onClick={() => { onClick={() => {
dispatch(toggleAccountMenu()); dispatch(toggleAccountMenu());
getEnvironmentType() === ENVIRONMENT_TYPE_POPUP trackEvent({
? global.platform.openExtensionInBrowser( category: MetaMetricsEventCategory.Navigation,
ADD_SNAP_ACCOUNT_ROUTE, event: MetaMetricsEventName.NavAccountSwitched,
null, properties: {
true, location: 'Main Menu',
) },
: history.push(ADD_SNAP_ACCOUNT_ROUTE); });
dispatch(setSelectedAccount(account.address));
}} }}
> identity={account}
{t('settingAddSnapAccount')} key={account.address}
</ButtonLink> selected={selectedAccount.address === account.address}
</Box> closeMenu={onClose}
</> connectedAvatar={connectedSite?.iconUrl}
///: END:ONLY_INCLUDE_IN connectedAvatarName={connectedSite?.name}
} />
{ );
///: BEGIN:ONLY_INCLUDE_IN(build-mmi) })}
</Box>
{/* Add / Import / Hardware */}
<Box padding={4}>
<Box> <Box>
<ButtonLink <ButtonLink
size={Size.SM} size={Size.SM}
startIconName={IconName.Custody} startIconName={IconName.Add}
onClick={() => {
trackEvent({
category: MetaMetricsEventCategory.Navigation,
event: MetaMetricsEventName.AccountAddSelected,
properties: {
account_type: MetaMetricsEventAccountType.Default,
location: 'Main Menu',
},
});
setActionMode('add');
}}
data-testid="multichain-account-menu-popover-add-account"
>
{t('addAccount')}
</ButtonLink>
</Box>
<Box>
<ButtonLink
size={Size.SM}
startIconName={IconName.Import}
onClick={() => {
trackEvent({
category: MetaMetricsEventCategory.Navigation,
event: MetaMetricsEventName.AccountAddSelected,
properties: {
account_type: MetaMetricsEventAccountType.Imported,
location: 'Main Menu',
},
});
setActionMode('import');
}}
>
{t('importAccount')}
</ButtonLink>
</Box>
<Box>
<ButtonLink
size={Size.SM}
startIconName={IconName.Hardware}
onClick={() => { onClick={() => {
dispatch(toggleAccountMenu()); dispatch(toggleAccountMenu());
trackEvent({ trackEvent({
category: MetaMetricsEventCategory.Navigation, category: MetaMetricsEventCategory.Navigation,
event: event: MetaMetricsEventName.AccountAddSelected,
MetaMetricsEventName.UserClickedConnectCustodialAccount, properties: {
account_type: MetaMetricsEventAccountType.Hardware,
location: 'Main Menu',
},
}); });
if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) { if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) {
global.platform.openExtensionInBrowser( global.platform.openExtensionInBrowser(
CUSTODY_ACCOUNT_ROUTE, CONNECT_HARDWARE_ROUTE,
); );
} else { } else {
history.push(CUSTODY_ACCOUNT_ROUTE); history.push(CONNECT_HARDWARE_ROUTE);
} }
}} }}
> >
{t('connectCustodialAccountMenu')} {t('hardwareWallet')}
</ButtonLink> </ButtonLink>
</Box> </Box>
///: END:ONLY_INCLUDE_IN {
} ///: BEGIN:ONLY_INCLUDE_IN(keyring-snaps)
<>
<Box marginTop={4}>
<ButtonLink
size={Size.SM}
startIconName={IconName.Snaps}
onClick={() => {
dispatch(toggleAccountMenu());
getEnvironmentType() === ENVIRONMENT_TYPE_POPUP
? global.platform.openExtensionInBrowser(
ADD_SNAP_ACCOUNT_ROUTE,
null,
true,
)
: history.push(ADD_SNAP_ACCOUNT_ROUTE);
}}
>
{t('settingAddSnapAccount')}
</ButtonLink>
</Box>
</>
///: END:ONLY_INCLUDE_IN
}
{
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
<Box>
<ButtonLink
size={Size.SM}
startIconName={IconName.Custody}
onClick={() => {
dispatch(toggleAccountMenu());
trackEvent({
category: MetaMetricsEventCategory.Navigation,
event:
MetaMetricsEventName.UserClickedConnectCustodialAccount,
});
if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) {
global.platform.openExtensionInBrowser(
CUSTODY_ACCOUNT_ROUTE,
);
} else {
history.push(CUSTODY_ACCOUNT_ROUTE);
}
}}
>
{t('connectCustodialAccountMenu')}
</ButtonLink>
</Box>
///: END:ONLY_INCLUDE_IN
}
</Box>
</Box> </Box>
</Box> ) : null}
) : null} </ModalContent>
</Popover> </Modal>
); );
}; };

View File

@ -3,7 +3,7 @@
exports[`App Header should match snapshot 1`] = ` exports[`App Header should match snapshot 1`] = `
<div> <div>
<div <div
class="box multichain-app-header-logo box--margin-2 box--display-none box--sm:display-flex box--flex-direction-row box--justify-content-center box--align-items-center" class="mm-box multichain-app-header-logo mm-box--margin-2 mm-box--display-none mm-box--sm:display-flex mm-box--justify-content-center mm-box--align-items-center"
data-testid="app-header-logo" data-testid="app-header-logo"
> >
<button <button
@ -200,10 +200,10 @@ exports[`App Header should match snapshot 1`] = `
</button> </button>
</div> </div>
<div <div
class="box multichain-app-header box--margin-bottom-4 box--display-flex box--flex-direction-row box--align-items-center box--width-full box--background-color-background-alternative" class="mm-box multichain-app-header mm-box--margin-bottom-4 mm-box--display-flex mm-box--align-items-center mm-box--width-full mm-box--background-color-background-alternative"
> >
<div <div
class="box multichain-app-header__contents multichain-app-header-shadow box--padding-2 box--padding-right-4 box--padding-left-4 box--gap-2 box--flex-direction-row box--align-items-center box--width-full box--background-color-background-default box--display-flex" class="mm-box multichain-app-header__contents multichain-app-header-shadow mm-box--padding-2 mm-box--padding-right-4 mm-box--padding-left-4 mm-box--gap-2 mm-box--align-items-center mm-box--width-full mm-box--background-color-background-default"
> >
<div> <div>
<button <button
@ -289,14 +289,14 @@ exports[`App Header should match snapshot 1`] = `
</span> </span>
</button> </button>
<div <div
class="box box--display-flex box--flex-direction-row box--justify-content-flex-end box--align-items-center" class="mm-box mm-box--display-flex mm-box--justify-content-flex-end mm-box--align-items-center"
> >
<div <div
class="box box--display-flex box--gap-4 box--flex-direction-row" class="mm-box mm-box--display-flex mm-box--gap-4"
> >
<div <div
class="box box--display-flex box--flex-direction-row box--justify-content-flex-end box--width-full" class="mm-box mm-box--display-flex mm-box--justify-content-flex-end mm-box--width-full"
> >
<button <button
aria-label="Account options" aria-label="Account options"

View File

@ -28,8 +28,8 @@ import {
ButtonIcon, ButtonIcon,
ButtonIconSize, ButtonIconSize,
IconName, IconName,
IconSize,
PickerNetwork, PickerNetwork,
Box,
} from '../../component-library'; } from '../../component-library';
///: BEGIN:ONLY_INCLUDE_IN(build-mmi) ///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
import { getCustodianIconForAddress } from '../../../selectors/institutional/selectors'; import { getCustodianIconForAddress } from '../../../selectors/institutional/selectors';
@ -47,7 +47,6 @@ import {
} from '../../../selectors'; } from '../../../selectors';
import { GlobalMenu, ProductTour, AccountPicker } from '..'; import { GlobalMenu, ProductTour, AccountPicker } from '..';
import Box from '../../ui/box/box';
import { import {
hideProductTour, hideProductTour,
toggleAccountMenu, toggleAccountMenu,
@ -360,7 +359,6 @@ export const AppHeader = ({ location }) => {
setAccountOptionsMenuOpen(true); setAccountOptionsMenuOpen(true);
}} }}
size={ButtonIconSize.Sm} size={ButtonIconSize.Sm}
iconProps={{ size: IconSize.Sm }}
/> />
</Box> </Box>
</Box> </Box>

View File

@ -5,7 +5,7 @@ import mockState from '../../../../test/data/mock-state.json';
import { SEND_STAGES } from '../../../ducks/send'; import { SEND_STAGES } from '../../../ducks/send';
import { AppHeader } from '.'; import { AppHeader } from '.';
const render = (stateChanges = {}, location = jest.fn()) => { const render = (stateChanges = {}, location = {}) => {
const store = configureStore({ const store = configureStore({
...mockState, ...mockState,
activeTab: { activeTab: {

View File

@ -3,7 +3,7 @@
exports[`Connected Site Menu should render the site menu in connected state 1`] = ` exports[`Connected Site Menu should render the site menu in connected state 1`] = `
<div> <div>
<button <button
class="box multichain-connected-site-menu box--display-flex box--flex-direction-row box--justify-content-center box--align-items-center box--background-color-background-default" class="mm-box multichain-connected-site-menu mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--background-color-background-default"
data-testid="connection-menu" data-testid="connection-menu"
> >
<div> <div>
@ -27,7 +27,7 @@ exports[`Connected Site Menu should render the site menu in connected state 1`]
style="bottom: 2px; right: -4px; z-index: 1;" style="bottom: 2px; right: -4px; z-index: 1;"
> >
<div <div
class="box multichain-connected-site-menu__badge box--flex-direction-row box--background-color-success-default box--rounded-full box--border-color-background-default box--border-width-3 box--border-style-solid" class="mm-box multichain-connected-site-menu__badge mm-box--background-color-success-default mm-box--rounded-full mm-box--border-color-background-default mm-box--border-width-3 box--border-style-solid"
/> />
</div> </div>
</div> </div>
@ -40,7 +40,7 @@ exports[`Connected Site Menu should render the site menu in connected state 1`]
exports[`Connected Site Menu should render the site menu in not connected state 1`] = ` exports[`Connected Site Menu should render the site menu in not connected state 1`] = `
<div> <div>
<button <button
class="box multichain-connected-site-menu box--display-flex box--flex-direction-row box--justify-content-center box--align-items-center box--background-color-background-default" class="mm-box multichain-connected-site-menu mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--background-color-background-default"
data-testid="connection-menu" data-testid="connection-menu"
> >
<div> <div>
@ -64,7 +64,7 @@ exports[`Connected Site Menu should render the site menu in not connected state
style="bottom: 2px; right: -4px; z-index: 1;" style="bottom: 2px; right: -4px; z-index: 1;"
> >
<div <div
class="box multichain-connected-site-menu__badge box--flex-direction-row box--background-color-icon-alternative box--rounded-full box--border-color-background-default box--border-width-3 box--border-style-solid" class="mm-box multichain-connected-site-menu__badge mm-box--background-color-icon-alternative mm-box--rounded-full mm-box--border-color-background-default mm-box--border-width-3 box--border-style-solid"
/> />
</div> </div>
</div> </div>
@ -77,7 +77,7 @@ exports[`Connected Site Menu should render the site menu in not connected state
exports[`Connected Site Menu should render the site menu in not connected to current account state 1`] = ` exports[`Connected Site Menu should render the site menu in not connected to current account state 1`] = `
<div> <div>
<button <button
class="box multichain-connected-site-menu box--display-flex box--flex-direction-row box--justify-content-center box--align-items-center box--background-color-background-default" class="mm-box multichain-connected-site-menu mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--background-color-background-default"
data-testid="connection-menu" data-testid="connection-menu"
> >
<div> <div>
@ -101,7 +101,7 @@ exports[`Connected Site Menu should render the site menu in not connected to cur
style="bottom: 4px; right: -1px; z-index: 1;" style="bottom: 4px; right: -1px; z-index: 1;"
> >
<div <div
class="box multichain-connected-site-menu__badge not-connected box--flex-direction-row box--background-color-background-default box--rounded-full box--border-color-success-default box--border-width-2 box--border-style-solid" class="mm-box multichain-connected-site-menu__badge not-connected mm-box--background-color-background-default mm-box--rounded-full mm-box--border-color-success-default mm-box--border-width-2 box--border-style-solid"
/> />
</div> </div>
</div> </div>

View File

@ -20,8 +20,8 @@ import {
Icon, Icon,
IconName, IconName,
IconSize, IconSize,
Box,
} from '../../component-library'; } from '../../component-library';
import Box from '../../ui/box';
import { getSelectedIdentity } from '../../../selectors'; import { getSelectedIdentity } from '../../../selectors';
import Tooltip from '../../ui/tooltip'; import Tooltip from '../../ui/tooltip';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';

View File

@ -6,6 +6,7 @@ import {
ButtonPrimary, ButtonPrimary,
ButtonSecondary, ButtonSecondary,
FormTextField, FormTextField,
Box,
} from '../../component-library'; } from '../../component-library';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import { getAccountNameErrorMessage } from '../../../helpers/utils/accounts'; import { getAccountNameErrorMessage } from '../../../helpers/utils/accounts';
@ -15,7 +16,6 @@ import {
} from '../../../selectors'; } from '../../../selectors';
import { addNewAccount, setAccountLabel } from '../../../store/actions'; import { addNewAccount, setAccountLabel } from '../../../store/actions';
import { getMostRecentOverviewPage } from '../../../ducks/history/history'; import { getMostRecentOverviewPage } from '../../../ducks/history/history';
import Box from '../../ui/box/box';
import { import {
MetaMetricsEventAccountType, MetaMetricsEventAccountType,
MetaMetricsEventCategory, MetaMetricsEventCategory,

View File

@ -40,7 +40,7 @@ exports[`Json should match snapshot 1`] = `
</div> </div>
</div> </div>
<div <div
class="box box--display-flex box--gap-4 box--flex-direction-row" class="mm-box mm-box--display-flex mm-box--gap-4"
> >
<button <button
class="box mm-text mm-button-base mm-button-base--size-lg mm-button-base--block mm-button-secondary mm-text--body-md box--padding-right-4 box--padding-left-4 box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-primary-default box--background-color-transparent box--rounded-pill box--border-color-primary-default box--border-style-solid box--border-width-1" class="box mm-text mm-button-base mm-button-base--size-lg mm-button-base--block mm-button-secondary mm-text--body-md box--padding-right-4 box--padding-left-4 box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-primary-default box--background-color-transparent box--rounded-pill box--border-color-primary-default box--border-style-solid box--border-width-1"

View File

@ -4,9 +4,9 @@ import { useDispatch } from 'react-redux';
import { import {
ButtonPrimary, ButtonPrimary,
ButtonSecondary, ButtonSecondary,
Box,
BUTTON_SECONDARY_SIZES, BUTTON_SECONDARY_SIZES,
} from '../../component-library'; } from '../../component-library';
import Box from '../../ui/box/box';
import { Display } from '../../../helpers/constants/design-system'; import { Display } from '../../../helpers/constants/design-system';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import * as actions from '../../../store/actions'; import * as actions from '../../../store/actions';

View File

@ -7,8 +7,7 @@ import {
MetaMetricsEventCategory, MetaMetricsEventCategory,
MetaMetricsEventName, MetaMetricsEventName,
} from '../../../../shared/constants/metametrics'; } from '../../../../shared/constants/metametrics';
import { ButtonLink, Label, Text } from '../../component-library'; import { ButtonLink, Label, Text, Box } from '../../component-library';
import Box from '../../ui/box';
import Dropdown from '../../ui/dropdown'; import Dropdown from '../../ui/dropdown';
import { MetaMetricsContext } from '../../../contexts/metametrics'; import { MetaMetricsContext } from '../../../contexts/metametrics';
import { import {

View File

@ -3,10 +3,10 @@
exports[`Import Token Link should match snapshot for goerli chainId 1`] = ` exports[`Import Token Link should match snapshot for goerli chainId 1`] = `
<div> <div>
<div <div
class="box multichain-import-token-link box--flex-direction-row" class="mm-box multichain-import-token-link"
> >
<div <div
class="box box--display-flex box--flex-direction-row box--align-items-center" class="mm-box mm-box--display-flex mm-box--align-items-center"
> >
<button <button
class="box mm-text mm-button-base mm-button-base--size-md mm-button-link mm-text--body-md box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-primary-default box--background-color-transparent" class="box mm-text mm-button-base mm-button-base--size-md mm-button-link mm-text--body-md box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-primary-default box--background-color-transparent"
@ -20,7 +20,7 @@ exports[`Import Token Link should match snapshot for goerli chainId 1`] = `
</button> </button>
</div> </div>
<div <div
class="box box--padding-top-2 box--display-flex box--flex-direction-row box--align-items-center" class="mm-box mm-box--padding-top-2 mm-box--display-flex mm-box--align-items-center"
> >
<button <button
class="box mm-text mm-button-base mm-button-base--size-md mm-button-link mm-text--body-md box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-primary-default box--background-color-transparent" class="box mm-text mm-button-base mm-button-base--size-md mm-button-link mm-text--body-md box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-primary-default box--background-color-transparent"
@ -40,10 +40,10 @@ exports[`Import Token Link should match snapshot for goerli chainId 1`] = `
exports[`Import Token Link should match snapshot for mainnet chainId 1`] = ` exports[`Import Token Link should match snapshot for mainnet chainId 1`] = `
<div> <div>
<div <div
class="box multichain-import-token-link box--flex-direction-row" class="mm-box multichain-import-token-link"
> >
<div <div
class="box box--display-flex box--flex-direction-row box--align-items-center" class="mm-box mm-box--display-flex mm-box--align-items-center"
> >
<button <button
class="box mm-text mm-button-base mm-button-base--size-md mm-button-link mm-text--body-md box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-primary-default box--background-color-transparent" class="box mm-text mm-button-base mm-button-base--size-md mm-button-link mm-text--body-md box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-primary-default box--background-color-transparent"
@ -57,7 +57,7 @@ exports[`Import Token Link should match snapshot for mainnet chainId 1`] = `
</button> </button>
</div> </div>
<div <div
class="box box--padding-top-2 box--display-flex box--flex-direction-row box--align-items-center" class="mm-box mm-box--padding-top-2 mm-box--display-flex mm-box--align-items-center"
> >
<button <button
class="box mm-text mm-button-base mm-button-base--size-md mm-button-link mm-text--body-md box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-primary-default box--background-color-transparent" class="box mm-text mm-button-base mm-button-base--size-md mm-button-link mm-text--body-md box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-primary-default box--background-color-transparent"

View File

@ -1,10 +1,9 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import Box from '../../ui/box/box'; import { ButtonLink, IconName, Box } from '../../component-library';
import { ButtonLink, IconName } from '../../component-library';
import { import {
AlignItems, AlignItems,
Display, Display,
@ -27,6 +26,7 @@ export const ImportTokenLink = ({ className, ...props }) => {
const trackEvent = useContext(MetaMetricsContext); const trackEvent = useContext(MetaMetricsContext);
const t = useI18nContext(); const t = useI18nContext();
const history = useHistory(); const history = useHistory();
const dispatch = useDispatch();
const isTokenDetectionSupported = useSelector(getIsTokenDetectionSupported); const isTokenDetectionSupported = useSelector(getIsTokenDetectionSupported);
const isTokenDetectionInactiveOnMainnet = useSelector( const isTokenDetectionInactiveOnMainnet = useSelector(
@ -69,7 +69,7 @@ export const ImportTokenLink = ({ className, ...props }) => {
size={Size.MD} size={Size.MD}
startIconName={IconName.Refresh} startIconName={IconName.Refresh}
data-testid="refresh-list-button" data-testid="refresh-list-button"
onClick={() => detectNewTokens()} onClick={() => dispatch(detectNewTokens())}
> >
{t('refreshList')} {t('refreshList')}
</ButtonLink> </ButtonLink>

View File

@ -19,7 +19,7 @@ jest.mock('react-router-dom', () => {
}); });
jest.mock('../../../store/actions.ts', () => ({ jest.mock('../../../store/actions.ts', () => ({
detectNewTokens: jest.fn(), detectNewTokens: jest.fn().mockReturnValue({ type: '' }),
})); }));
describe('Import Token Link', () => { describe('Import Token Link', () => {

View File

@ -3,7 +3,7 @@
exports[`NetworkListItem renders properly 1`] = ` exports[`NetworkListItem renders properly 1`] = `
<div> <div>
<div <div
class="box multichain-network-list-item box--padding-4 box--gap-2 box--flex-direction-row box--justify-content-space-between box--align-items-center box--width-full box--background-color-transparent box--display-flex" class="mm-box multichain-network-list-item mm-box--padding-4 mm-box--display-flex mm-box--gap-2 mm-box--justify-content-space-between mm-box--align-items-center mm-box--width-full mm-box--background-color-transparent"
> >
<div <div
class="box mm-text mm-avatar-base mm-avatar-base--size-md mm-avatar-network mm-text--body-sm mm-text--text-transform-uppercase box--display-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-text-default box--background-color-background-alternative box--rounded-full box--border-color-transparent box--border-style-solid box--border-width-1" class="box mm-text mm-avatar-base mm-avatar-base--size-md mm-avatar-network mm-text--body-sm mm-text--text-transform-uppercase box--display-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-text-default box--background-color-background-alternative box--rounded-full box--border-color-transparent box--border-style-solid box--border-width-1"
@ -15,7 +15,7 @@ exports[`NetworkListItem renders properly 1`] = `
/> />
</div> </div>
<div <div
class="box multichain-network-list-item__network-name box--flex-direction-row" class="mm-box multichain-network-list-item__network-name"
> >
<button <button
class="box mm-text mm-button-base mm-button-base--ellipsis mm-button-link mm-button-link--size-auto mm-text--body-md mm-text--ellipsis box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-text-default box--background-color-transparent" class="box mm-text mm-button-base mm-button-base--ellipsis mm-button-link mm-button-link--size-auto mm-text--body-md mm-text--ellipsis box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-text-default box--background-color-transparent"

View File

@ -1,7 +1,6 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Box from '../../ui/box/box';
import { import {
AlignItems, AlignItems,
IconColor, IconColor,
@ -12,12 +11,14 @@ import {
TextColor, TextColor,
BackgroundColor, BackgroundColor,
BlockSize, BlockSize,
Display,
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import { import {
AvatarNetwork, AvatarNetwork,
ButtonIcon, ButtonIcon,
ButtonLink, ButtonLink,
IconName, IconName,
Box,
} from '../../component-library'; } from '../../component-library';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import Tooltip from '../../ui/tooltip/tooltip'; import Tooltip from '../../ui/tooltip/tooltip';
@ -67,6 +68,7 @@ export const NetworkListItem = ({
className={classnames('multichain-network-list-item', { className={classnames('multichain-network-list-item', {
'multichain-network-list-item--selected': selected, 'multichain-network-list-item--selected': selected,
})} })}
display={Display.Flex}
alignItems={AlignItems.center} alignItems={AlignItems.center}
justifyContent={JustifyContent.spaceBetween} justifyContent={JustifyContent.spaceBetween}
width={BlockSize.Full} width={BlockSize.Full}

View File

@ -1,8 +1,3 @@
.multichain-network-list-menu { .multichain-network-list-menu {
overflow: auto; overflow: auto;
/* Overrides the popover's default behavior */
&-content-wrapper {
border-radius: 0;
}
} }

View File

@ -1,9 +1,8 @@
import React, { useContext, useEffect, useRef } from 'react'; import React, { useContext } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import Popover from '../../ui/popover/popover.component';
import { NetworkListItem } from '../network-list-item'; import { NetworkListItem } from '../network-list-item';
import { import {
setActiveNetwork, setActiveNetwork,
@ -15,17 +14,25 @@ import {
import { CHAIN_IDS, TEST_CHAINS } from '../../../../shared/constants/network'; import { CHAIN_IDS, TEST_CHAINS } from '../../../../shared/constants/network';
import { import {
getShowTestNetworks, getShowTestNetworks,
getAllEnabledNetworks,
getCurrentChainId, getCurrentChainId,
getNonTestNetworks,
getTestNetworks,
} from '../../../selectors'; } from '../../../selectors';
import Box from '../../ui/box/box';
import ToggleButton from '../../ui/toggle-button'; import ToggleButton from '../../ui/toggle-button';
import { import {
Display, Display,
JustifyContent, JustifyContent,
Size, Size,
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import { Text, ButtonLink, IconName } from '../../component-library'; import {
ButtonLink,
Modal,
ModalContent,
ModalHeader,
ModalOverlay,
Text,
Box,
} from '../../component-library';
import { ADD_POPULAR_CUSTOM_NETWORK } from '../../../helpers/constants/routes'; import { ADD_POPULAR_CUSTOM_NETWORK } from '../../../helpers/constants/routes';
import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app'; import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app';
@ -47,7 +54,10 @@ const UNREMOVABLE_CHAIN_IDS = [
export const NetworkListMenu = ({ onClose }) => { export const NetworkListMenu = ({ onClose }) => {
const t = useI18nContext(); const t = useI18nContext();
const networks = useSelector(getAllEnabledNetworks);
const nonTestNetworks = useSelector(getNonTestNetworks);
const testNetworks = useSelector(getTestNetworks);
const showTestNetworks = useSelector(getShowTestNetworks); const showTestNetworks = useSelector(getShowTestNetworks);
const currentChainId = useSelector(getCurrentChainId); const currentChainId = useSelector(getCurrentChainId);
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -57,132 +67,135 @@ export const NetworkListMenu = ({ onClose }) => {
const environmentType = getEnvironmentType(); const environmentType = getEnvironmentType();
const isFullScreen = environmentType === ENVIRONMENT_TYPE_FULLSCREEN; const isFullScreen = environmentType === ENVIRONMENT_TYPE_FULLSCREEN;
const showTestNetworksRef = useRef(showTestNetworks);
const networkListRef = useRef(null);
const completedOnboarding = useSelector(getCompletedOnboarding); const completedOnboarding = useSelector(getCompletedOnboarding);
const lineaMainnetReleased = useSelector(isLineaMainnetNetworkReleased); const lineaMainnetReleased = useSelector(isLineaMainnetNetworkReleased);
useEffect(() => { const generateMenuItems = (desiredNetworks) => {
if (showTestNetworks && !showTestNetworksRef.current) { return desiredNetworks.map((network, index) => {
// Scroll to the bottom of the list if (!lineaMainnetReleased && network.providerType === 'linea-mainnet') {
networkListRef.current.lastChild.scrollIntoView(); return null;
} }
showTestNetworksRef.current = showTestNetworks; const isCurrentNetwork = currentChainId === network.chainId;
}, [showTestNetworks, showTestNetworksRef]); const canDeleteNetwork =
!isCurrentNetwork && !UNREMOVABLE_CHAIN_IDS.includes(network.chainId);
return (
<NetworkListItem
name={network.nickname}
iconSrc={network?.rpcPrefs?.imageUrl}
key={`${network.id || network.chainId}-${index}`}
selected={isCurrentNetwork}
onClick={async () => {
dispatch(toggleNetworkMenu());
if (network.providerType) {
dispatch(setProviderType(network.providerType));
} else {
dispatch(setActiveNetwork(network.id));
}
trackEvent({
event: MetaMetricsEventName.NavNetworkSwitched,
category: MetaMetricsEventCategory.Network,
properties: {
location: 'Network Menu',
chain_id: currentChainId,
from_network: currentChainId,
to_network: network.id || network.chainId,
},
});
}}
onDeleteClick={
canDeleteNetwork
? () => {
dispatch(toggleNetworkMenu());
dispatch(
showModal({
name: 'CONFIRM_DELETE_NETWORK',
target: network.id || network.chainId,
onConfirm: () => undefined,
}),
);
}
: null
}
/>
);
});
};
return ( return (
<Popover <Modal isOpen onClose={onClose}>
contentClassName="multichain-network-list-menu-content-wrapper" <ModalOverlay />
onClose={onClose} <ModalContent
centerTitle className="multichain-network-list-menu-content-wrapper"
title={t('networkMenuHeading')} modalDialogProps={{ padding: 0 }}
> >
<> <ModalHeader
<Box className="multichain-network-list-menu" ref={networkListRef}> paddingTop={4}
{networks.map((network, index) => { paddingRight={4}
if ( paddingBottom={6}
!lineaMainnetReleased && onClose={onClose}
network.providerType === 'linea-mainnet'
) {
return null;
}
const isCurrentNetwork = currentChainId === network.chainId;
const canDeleteNetwork =
!isCurrentNetwork &&
!UNREMOVABLE_CHAIN_IDS.includes(network.chainId);
return (
<NetworkListItem
name={network.nickname}
iconSrc={network?.rpcPrefs?.imageUrl}
key={`${network.id || network.chainId}-${index}`}
selected={isCurrentNetwork}
onClick={async () => {
dispatch(toggleNetworkMenu());
if (network.providerType) {
dispatch(setProviderType(network.providerType));
} else {
dispatch(setActiveNetwork(network.id));
}
trackEvent({
event: MetaMetricsEventName.NavNetworkSwitched,
category: MetaMetricsEventCategory.Network,
properties: {
location: 'Network Menu',
chain_id: currentChainId,
from_network: currentChainId,
to_network: network.id || network.chainId,
},
});
}}
onDeleteClick={
canDeleteNetwork
? () => {
dispatch(toggleNetworkMenu());
dispatch(
showModal({
name: 'CONFIRM_DELETE_NETWORK',
target: network.id || network.chainId,
onConfirm: () => undefined,
}),
);
}
: null
}
/>
);
})}
</Box>
<Box
padding={4}
display={Display.Flex}
justifyContent={JustifyContent.spaceBetween}
> >
<Text>{t('showTestnetNetworks')}</Text> {t('networkMenuHeading')}
<ToggleButton </ModalHeader>
value={showTestNetworks} <>
onToggle={(value) => { <Box className="multichain-network-list-menu">
const shouldShowTestNetworks = !value; {generateMenuItems(nonTestNetworks)}
dispatch(setShowTestNetworks(shouldShowTestNetworks)); </Box>
if (shouldShowTestNetworks) { <Box
padding={4}
display={Display.Flex}
justifyContent={JustifyContent.spaceBetween}
>
<Text>{t('showTestnetNetworks')}</Text>
<ToggleButton
value={showTestNetworks}
onToggle={(value) => {
const shouldShowTestNetworks = !value;
dispatch(setShowTestNetworks(shouldShowTestNetworks));
if (shouldShowTestNetworks) {
trackEvent({
event: MetaMetricsEventName.TestNetworksDisplayed,
category: MetaMetricsEventCategory.Network,
});
}
}}
/>
</Box>
{showTestNetworks ? (
<Box className="multichain-network-list-menu">
{generateMenuItems(testNetworks)}
</Box>
) : null}
<Box padding={4}>
<ButtonLink
size={Size.SM}
block
onClick={() => {
if (isFullScreen) {
if (completedOnboarding) {
history.push(ADD_POPULAR_CUSTOM_NETWORK);
} else {
dispatch(showModal({ name: 'ONBOARDING_ADD_NETWORK' }));
}
} else {
global.platform.openExtensionInBrowser(
ADD_POPULAR_CUSTOM_NETWORK,
);
}
dispatch(toggleNetworkMenu());
trackEvent({ trackEvent({
event: MetaMetricsEventName.TestNetworksDisplayed, event: MetaMetricsEventName.AddNetworkButtonClick,
category: MetaMetricsEventCategory.Network, category: MetaMetricsEventCategory.Network,
}); });
} }}
}} >
/> {t('addNetwork')}
</Box> </ButtonLink>
<Box paddingLeft={4} paddingRight={4} paddingBottom={4}> </Box>
<ButtonLink </>
size={Size.SM} </ModalContent>
startIconName={IconName.Add} </Modal>
onClick={() => {
if (isFullScreen) {
if (completedOnboarding) {
history.push(ADD_POPULAR_CUSTOM_NETWORK);
} else {
dispatch(showModal({ name: 'ONBOARDING_ADD_NETWORK' }));
}
} else {
global.platform.openExtensionInBrowser(
ADD_POPULAR_CUSTOM_NETWORK,
);
}
dispatch(toggleNetworkMenu());
trackEvent({
event: MetaMetricsEventName.AddNetworkButtonClick,
category: MetaMetricsEventCategory.Network,
});
}}
>
{t('addNetwork')}
</ButtonLink>
</Box>
</>
</Popover>
); );
}; };

View File

@ -6,6 +6,7 @@ import {
AvatarNetwork, AvatarNetwork,
BadgeWrapper, BadgeWrapper,
BadgeWrapperAnchorElementShape, BadgeWrapperAnchorElementShape,
Box,
} from '../../component-library'; } from '../../component-library';
import { import {
BackgroundColor, BackgroundColor,
@ -13,7 +14,6 @@ import {
JustifyContent, JustifyContent,
Size, Size,
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import Box from '../../ui/box/box';
export const NftItem = ({ export const NftItem = ({
alt, alt,

Some files were not shown because too many files have changed in this diff Show More