1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 09:23:21 +01:00

Merge branch 'develop' into feature/17670-cancel-speedup-popover

This commit is contained in:
George Marshall 2023-06-21 18:19:09 -07:00 committed by GitHub
commit bdd384460e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
124 changed files with 7992 additions and 4969 deletions

View File

@ -0,0 +1,328 @@
import * as core from '@actions/core';
import { context, getOctokit } from '@actions/github';
import { GitHub } from '@actions/github/lib/utils';
// A labelable object can be a pull request or an issue
interface Labelable {
id: string;
number: number;
repoOwner: string;
repoName: string;
createdAt: string;
}
main().catch((error: Error): void => {
console.error(error);
process.exit(1);
});
async function main(): Promise<void> {
// "GITHUB_TOKEN" is an automatically generated, repository-specific access token provided by GitHub Actions.
// We can't use "GITHUB_TOKEN" here, as its permissions are scoped to the repository where the action is running.
// "GITHUB_TOKEN" does not have access to other repositories, even when they belong to the same organization.
// As we want to update linked issues which are not necessarily located in the same repository,
// we need to create our own "RELEASE_LABEL_TOKEN" with "repo" permissions.
// Such a token allows to access other repositories of the MetaMask organisation.
const personalAccessToken = process.env.RELEASE_LABEL_TOKEN;
if (!personalAccessToken) {
core.setFailed('RELEASE_LABEL_TOKEN not found');
process.exit(1);
}
const nextReleaseVersionNumber = process.env.NEXT_SEMVER_VERSION;
if (!nextReleaseVersionNumber) {
// NEXT_SEMVER_VERSION is automatically deduced as minor version bump on top of the latest version
// found, either in repo's list of branches, or in repo's list of tags or in repo's "package.json" version.
// For edge cases (e.g. major version bumps, etc.), where the value can not be deduced automatically,
// NEXT_SEMVER_VERSION can be defined manually set by defining FORCE_NEXT_SEMVER_VERSION variable in
// section "Secrets and variables">"Actions">"Variables">"New repository variable" in the settings of this repo.
// Example value: 6.5.0
core.setFailed('NEXT_SEMVER_VERSION not found');
process.exit(1);
}
if (!isValidVersionFormat(nextReleaseVersionNumber)) {
core.setFailed(`NEXT_SEMVER_VERSION (${nextReleaseVersionNumber}) is not a valid version format. The expected format is "x.y.z", where "x", "y" and "z" are numbers.`);
process.exit(1);
}
// Release label indicates the next release version number
// Example release label: "release-6.5.0"
const releaseLabelName = `release-${nextReleaseVersionNumber}`;
const releaseLabelColor = "ededed";
const releaseLabelDescription = `Issue or pull request that will be included in release ${nextReleaseVersionNumber}`;
// Initialise octokit, required to call Github GraphQL API
const octokit: InstanceType<typeof GitHub> = getOctokit(
personalAccessToken,
{
previews: ["bane"], // The "bane" preview is required for adding, updating, creating and deleting labels.
},
);
// Retrieve pull request info from context
const prRepoOwner = context.repo.owner;
const prRepoName = context.repo.repo;
const prNumber = context.payload.pull_request?.number;
if (!prNumber) {
core.setFailed('Pull request number not found');
process.exit(1);
}
// Retrieve pull request
const pullRequest: Labelable = await retrievePullRequest(octokit, prRepoOwner, prRepoName, prNumber);
// Add the release label to the pull request
await addLabelToLabelable(octokit, pullRequest, releaseLabelName, releaseLabelColor, releaseLabelDescription);
// Retrieve linked issues for the pull request
const linkedIssues: Labelable[] = await retrieveLinkedIssues(octokit, prRepoOwner, prRepoName, prNumber);
// Add the release label to the linked issues
for (const linkedIssue of linkedIssues) {
await addLabelToLabelable(octokit, linkedIssue, releaseLabelName, releaseLabelColor, releaseLabelDescription);
}
}
// This helper function checks if version has the correct format: "x.y.z" where "x", "y" and "z" are numbers.
function isValidVersionFormat(str: string): boolean {
const regex = /^\d+\.\d+\.\d+$/;
return regex.test(str);
}
// This function retrieves the repo
async function retrieveRepo(octokit: InstanceType<typeof GitHub>, repoOwner: string, repoName: string): Promise<string> {
const retrieveRepoQuery = `
query RetrieveRepo($repoOwner: String!, $repoName: String!) {
repository(owner: $repoOwner, name: $repoName) {
id
}
}
`;
const retrieveRepoResult: {
repository: {
id: string;
};
} = await octokit.graphql(retrieveRepoQuery, {
repoOwner,
repoName,
});
const repoId = retrieveRepoResult?.repository?.id;
return repoId;
}
// This function retrieves the label on a specific repo
async function retrieveLabel(octokit: InstanceType<typeof GitHub>, repoOwner: string, repoName: string, labelName: string): Promise<string> {
const retrieveLabelQuery = `
query RetrieveLabel($repoOwner: String!, $repoName: String!, $labelName: String!) {
repository(owner: $repoOwner, name: $repoName) {
label(name: $labelName) {
id
}
}
}
`;
const retrieveLabelResult: {
repository: {
label: {
id: string;
};
};
} = await octokit.graphql(retrieveLabelQuery, {
repoOwner,
repoName,
labelName,
});
const labelId = retrieveLabelResult?.repository?.label?.id;
return labelId;
}
// This function creates the label on a specific repo
async function createLabel(octokit: InstanceType<typeof GitHub>, repoId: string, labelName: string, labelColor: string, labelDescription: string): Promise<string> {
const createLabelMutation = `
mutation CreateLabel($repoId: ID!, $labelName: String!, $labelColor: String!, $labelDescription: String) {
createLabel(input: {repositoryId: $repoId, name: $labelName, color: $labelColor, description: $labelDescription}) {
label {
id
}
}
}
`;
const createLabelResult: {
createLabel: {
label: {
id: string;
};
};
} = await octokit.graphql(createLabelMutation, {
repoId,
labelName,
labelColor,
labelDescription,
});
const labelId = createLabelResult?.createLabel?.label?.id;
return labelId;
}
// This function creates or retrieves the label on a specific repo
async function createOrRetrieveLabel(octokit: InstanceType<typeof GitHub>, repoOwner: string, repoName: string, labelName: string, labelColor: string, labelDescription: string): Promise<string> {
// Check if label already exists on the repo
let labelId = await retrieveLabel(octokit, repoOwner, repoName, labelName);
// If label doesn't exist on the repo, create it
if (!labelId) {
// Retrieve PR's repo
const repoId = await retrieveRepo(octokit, repoOwner, repoName);
// Create label on repo
labelId = await createLabel(octokit, repoId, labelName, labelColor, labelDescription);
}
return labelId;
}
// This function retrieves the pull request on a specific repo
async function retrievePullRequest(octokit: InstanceType<typeof GitHub>, repoOwner: string, repoName: string, prNumber: number): Promise<Labelable> {
const retrievePullRequestQuery = `
query GetPullRequest($repoOwner: String!, $repoName: String!, $prNumber: Int!) {
repository(owner: $repoOwner, name: $repoName) {
pullRequest(number: $prNumber) {
id
createdAt
}
}
}
`;
const retrievePullRequestResult: {
repository: {
pullRequest: {
id: string;
createdAt: string;
};
};
} = await octokit.graphql(retrievePullRequestQuery, {
repoOwner,
repoName,
prNumber,
});
const pullRequest: Labelable = {
id: retrievePullRequestResult?.repository?.pullRequest?.id,
number: prNumber,
repoOwner: repoOwner,
repoName: repoName,
createdAt: retrievePullRequestResult?.repository?.pullRequest?.createdAt,
}
return pullRequest;
}
// This function retrieves the list of linked issues for a pull request
async function retrieveLinkedIssues(octokit: InstanceType<typeof GitHub>, repoOwner: string, repoName: string, prNumber: number): Promise<Labelable[]> {
// We assume there won't be more than 100 linked issues
const retrieveLinkedIssuesQuery = `
query ($repoOwner: String!, $repoName: String!, $prNumber: Int!) {
repository(owner: $repoOwner, name: $repoName) {
pullRequest(number: $prNumber) {
closingIssuesReferences(first: 100) {
nodes {
id
number
createdAt
repository {
name
owner {
login
}
}
}
}
}
}
}
`;
const retrieveLinkedIssuesResult: {
repository: {
pullRequest: {
closingIssuesReferences: {
nodes: Array<{
id: string;
number: number;
createdAt: string;
repository: {
name: string;
owner: {
login: string;
};
};
}>;
};
};
};
} = await octokit.graphql(retrieveLinkedIssuesQuery, {
repoOwner,
repoName,
prNumber
});
const linkedIssues = retrieveLinkedIssuesResult?.repository?.pullRequest?.closingIssuesReferences?.nodes?.map((issue: {
id: string;
number: number;
createdAt: string;
repository: {
name: string;
owner: {
login: string;
};
};
}) => {
return {
id: issue?.id,
number: issue?.number,
repoOwner: issue?.repository?.owner?.login,
repoName: issue?.repository?.name,
createdAt: issue?.createdAt
};
}) || [];
return linkedIssues;
}
// This function adds label to a labelable object (i.e. a pull request or an issue)
async function addLabelToLabelable(octokit: InstanceType<typeof GitHub>, labelable: Labelable, labelName: string, labelColor: string, labelDescription: string): Promise<void> {
// Retrieve label from the labelable's repo, or create label if required
const labelId = await createOrRetrieveLabel(octokit, labelable?.repoOwner, labelable?.repoName, labelName, labelColor, labelDescription);
const addLabelsToLabelableMutation = `
mutation AddLabelsToLabelable($labelableId: ID!, $labelIds: [ID!]!) {
addLabelsToLabelable(input: {labelableId: $labelableId, labelIds: $labelIds}) {
clientMutationId
}
}
`;
await octokit.graphql(addLabelsToLabelableMutation, {
labelableId: labelable?.id,
labelIds: [labelId],
});
}

39
.github/workflows/add-release-label.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: Add release label to PR and linked issues when PR gets merged
on:
pull_request:
branches:
- develop
types:
- closed
jobs:
add-release-label:
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0 # This is needed to checkout all branches
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
- name: Install dependencies
run: yarn --immutable
- name: Get the next semver version
id: get-next-semver-version
env:
FORCE_NEXT_SEMVER_VERSION: ${{ vars.FORCE_NEXT_SEMVER_VERSION }}
run: ./development/get-next-semver-version.sh "$FORCE_NEXT_SEMVER_VERSION"
- name: Add release label to PR and linked issues
id: add-release-label-to-pr-and-linked-issues
env:
RELEASE_LABEL_TOKEN: ${{ secrets.RELEASE_LABEL_TOKEN }}
NEXT_SEMVER_VERSION: ${{ env.NEXT_SEMVER_VERSION }}
run: npm run add-release-label-to-pr-and-linked-issues

View File

@ -17,10 +17,10 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '16'
node-version-file: '.nvmrc'
- name: Install dependencies
run: yarn
run: yarn --immutable
- name: Run fitness functions
env:
@ -31,4 +31,4 @@ jobs:
# files in the current directory and its subdirectories. The output is
# then saved to a file called "diff".
git diff "$(git merge-base "origin/$BASE_REF" HEAD)" HEAD -- . > ./diff
npm run fitness-functions -- "ci" "./diff"
npm run fitness-functions -- "ci" "./diff"

45
.github/workflows/main.yml vendored Normal file
View File

@ -0,0 +1,45 @@
name: Main
on:
push:
branches: [develop, master]
pull_request:
jobs:
check-workflows:
name: Check workflows
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Download actionlint
id: download-actionlint
run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/7fdc9630cc360ea1a469eed64ac6d78caeda1234/scripts/download-actionlint.bash) 1.6.23
shell: bash
- name: Check workflow files
run: ${{ steps.download-actionlint.outputs.executable }} -color
shell: bash
all-jobs-completed:
name: All jobs completed
runs-on: ubuntu-latest
needs:
- check-workflows
outputs:
PASSED: ${{ steps.set-output.outputs.PASSED }}
steps:
- name: Set PASSED output
id: set-output
run: echo "PASSED=true" >> "$GITHUB_OUTPUT"
all-jobs-pass:
name: All jobs pass
if: ${{ always() }}
runs-on: ubuntu-latest
needs: all-jobs-completed
steps:
- name: Check that all jobs have passed
run: |
passed="${{ needs.all-jobs-completed.outputs.PASSED }}"
if [[ $passed != "true" ]]; then
exit 1
fi

View File

@ -0,0 +1,40 @@
name: Remove labels after issue (or PR) closed
on:
issues:
types: [closed]
pull_request:
types: [closed]
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Remove labels
env:
REPO: ${{ github.repository }}
ISSUE_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
LABELS=(
"product-backlog"
"needs-design"
"design-in-progress"
"ready-for-dev"
"sprint-backlog"
"in-progress"
"blocked"
"needs-dev-review"
"needs-qa"
"issues-found"
"ready-for-release"
)
for LABEL in "${LABELS[@]}"; do
curl \
-X DELETE \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/$REPO/issues/$ISSUE_NUMBER/labels/$LABEL"
done

22
.github/workflows/sonar.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: Sonar
on:
workflow_call:
secrets:
SONAR_TOKEN:
required: true
jobs:
sonarcloud:
name: SonarCloud
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Shallow clones should be disabled for better relevancy of analysis
- name: SonarCloud Scan
# v1.9.1
uses: SonarSource/sonarcloud-github-action@5875562561d22a34be0c657405578705a169af6c
with:
args: >
-Dsonar.javascript.lcov.reportPaths=tests/coverage/lcov.info
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

View File

@ -0,0 +1,172 @@
name: Update LavaMoat policies
on:
issue_comment:
types: created
jobs:
is-fork-pull-request:
name: Determine whether this issue comment was on a pull request from a fork
if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '@metamaskbot update-policies') }}
runs-on: ubuntu-latest
outputs:
IS_FORK: ${{ steps.is-fork.outputs.IS_FORK }}
steps:
- uses: actions/checkout@v3
- name: Determine whether this PR is from a fork
id: is-fork
run: echo "IS_FORK=$(gh pr view --json isCrossRepository --jq '.isCrossRepository' "${PR_NUMBER}" )" >> "$GITHUB_OUTPUT"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.issue.number }}
prepare:
name: Prepare dependencies
runs-on: ubuntu-latest
needs: is-fork-pull-request
# Early exit if this is a fork, since later steps are skipped for forks
if: ${{ needs.is-fork-pull-request.outputs.IS_FORK == 'false' }}
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Install Yarn dependencies
run: yarn --immutable
update-lavamoat-build-policy:
name: Update LavaMoat build policy
runs-on: ubuntu-latest
needs:
- prepare
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Install dependencies from cache
run: yarn --immutable --immutable-cache
- name: Update LavaMoat build policy
run: yarn lavamoat:build:auto
- name: Cache build policy
uses: actions/cache/save@v3
with:
path: lavamoat/build-system
key: cache-build-${{ github.run_id }}-${{ github.run_attempt }}
update-lavamoat-webapp-policy:
strategy:
matrix:
# Ensure this is synchronized with the list below in the "commit-updated-policies" job
# and with the build type list in `builds.yml`
build-type: [main, beta, flask, mmi, desktop]
name: Update LavaMoat ${{ matrix.build-type }} application policy
runs-on: ubuntu-latest
needs:
- prepare
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Install dependencies from cache
run: yarn --immutable --immutable-cache
- name: Update LavaMoat ${{ matrix.build-type }} policy
run: yarn lavamoat:webapp:auto:ci '--build-types=${{ matrix.build-type }}'
env:
INFURA_PROJECT_ID: 00000000000
- name: Cache ${{ matrix.build-type }} application policy
uses: actions/cache/save@v3
with:
path: lavamoat/browserify/${{ matrix.build-type }}
key: cache-${{ matrix.build-type }}-${{ github.run_id }}-${{ github.run_attempt }}
commit-updated-policies:
name: Commit the updated LavaMoat policies
runs-on: ubuntu-latest
needs:
- is-fork-pull-request
- update-lavamoat-build-policy
- update-lavamoat-webapp-policy
# Ensure forks don't get access to the LavaMoat update token
if: ${{ needs.is-fork-pull-request.outputs.IS_FORK == 'false' }}
steps:
- uses: actions/checkout@v3
with:
# Use PAT to ensure that the commit later can trigger status check workflows
token: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }}
- name: Checkout pull request
run: gh pr checkout "${PR_NUMBER}"
env:
GITHUB_TOKEN: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }}
PR_NUMBER: ${{ github.event.issue.number }}
- name: Restore build policy
uses: actions/cache/restore@v3
with:
path: lavamoat/build-system
key: cache-build-${{ github.run_id }}-${{ github.run_attempt }}
# One restore step per build type: [main, beta, flask, mmi, desktop]
# Ensure this is synchronized with the list above in the "update-lavamoat-webapp-policy" job
# and with the build type list in `builds.yml`
- name: Restore main application policy
uses: actions/cache/restore@v3
with:
path: lavamoat/browserify/main
key: cache-main-${{ github.run_id }}-${{ github.run_attempt }}
- name: Restore beta application policy
uses: actions/cache/restore@v3
with:
path: lavamoat/browserify/beta
key: cache-beta-${{ github.run_id }}-${{ github.run_attempt }}
- name: Restore flask application policy
uses: actions/cache/restore@v3
with:
path: lavamoat/browserify/flask
key: cache-flask-${{ github.run_id }}-${{ github.run_attempt }}
- name: Restore mmi application policy
uses: actions/cache/restore@v3
with:
path: lavamoat/browserify/mmi
key: cache-mmi-${{ github.run_id }}-${{ github.run_attempt }}
- name: Restore desktop application policy
uses: actions/cache/restore@v3
with:
path: lavamoat/browserify/desktop
key: cache-desktop-${{ github.run_id }}-${{ github.run_attempt }}
- name: Check whether there are policy changes
id: policy-changes
run: |
if git diff --exit-code
then
echo "HAS_CHANGES=false" >> "$GITHUB_OUTPUT"
else
echo "HAS_CHANGES=true" >> "$GITHUB_OUTPUT"
fi
- name: Commit the updated policies
if: steps.policy-changes.outputs.HAS_CHANGES == 'true'
run: |
git config --global user.name 'MetaMask Bot'
git config --global user.email 'metamaskbot@users.noreply.github.com'
git commit -am "Update LavaMoat policies"
git push
- name: Post comment
run: |
if [[ $HAS_CHANGES == 'true' ]]
then
gh pr comment "${PR_NUMBER}" --body 'Policies updated'
else
gh pr comment "${PR_NUMBER}" --body 'No policy changes'
fi
env:
HAS_CHANGES: ${{ steps.policy-changes.outputs.HAS_CHANGES }}
GITHUB_TOKEN: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }}
PR_NUMBER: ${{ github.event.issue.number }}

View File

@ -1,6 +1,6 @@
import React, { Component, createContext, useMemo } from 'react';
import PropTypes from 'prop-types';
import { getMessage } from '../ui/helpers/utils/i18n-helper';
import { getMessage } from '../shared/modules/i18n';
import { I18nContext } from '../ui/contexts/i18n';
export { I18nContext };

View File

@ -1,18 +0,0 @@
diff --git a/dist/SignatureController.js b/dist/SignatureController.js
index b39d274f4547ab4e8b647293199ec21c4a9e38ca..288e55c97c3e4a234874dd8b8986ba77576b0dc4 100644
--- a/dist/SignatureController.js
+++ b/dist/SignatureController.js
@@ -308,12 +308,12 @@ _SignatureController_keyringController = new WeakMap(), _SignatureController_isE
const messageId = msgParams.metamaskId;
try {
const cleanMessageParams = yield messageManager.approveMessage(msgParams);
+ __classPrivateFieldGet(this, _SignatureController_instances, "m", _SignatureController_acceptApproval).call(this, messageId);
const signature = yield getSignature(cleanMessageParams);
this.hub.emit(`${methodName}:signed`, { signature, messageId });
if (!cleanMessageParams.deferSetAsSigned) {
messageManager.setMessageStatusSigned(messageId, signature);
}
- __classPrivateFieldGet(this, _SignatureController_instances, "m", _SignatureController_acceptApproval).call(this, messageId);
return __classPrivateFieldGet(this, _SignatureController_getAllState, "f").call(this);
}
catch (error) {

View File

@ -112,19 +112,22 @@ Whenever you change dependencies (adding, removing, or updating, either in `pack
- The `allow-scripts` configuration in `package.json`
- Run `yarn allow-scripts auto` to update the `allow-scripts` configuration automatically. This config determines whether the package's install/postinstall scripts are allowed to run. Review each new package to determine whether the install script needs to run or not, testing if necessary.
- Unfortunately, `yarn allow-scripts auto` will behave inconsistently on different platforms. macOS and Windows users may see extraneous changes relating to optional dependencies.
- The LavaMoat policy files. The _tl;dr_ is to run `yarn lavamoat:auto` to update these files, but there can be devils in the details:
- There are two sets of LavaMoat policy files:
- The production LavaMoat policy files (`lavamoat/browserify/*/policy.json`), which are re-generated using `yarn lavamoat:background:auto`. Add `--help` for usage.
- These should be regenerated whenever the production dependencies for the background change.
- The build system LavaMoat policy file (`lavamoat/build-system/policy.json`), which is re-generated using `yarn lavamoat:build:auto`.
- This should be regenerated whenever the dependencies used by the build system itself change.
- Whenever you regenerate a policy file, review the changes to determine whether the access granted to each package seems appropriate.
- Unfortunately, `yarn lavamoat:auto` will behave inconsistently on different platforms.
macOS and Windows users may see extraneous changes relating to optional dependencies.
- If you keep getting policy failures even after regenerating the policy files, try regenerating the policies after a clean install by doing:
- `rm -rf node_modules/ && yarn && yarn lavamoat:auto`
- Keep in mind that any kind of dynamic import or dynamic use of globals may elude LavaMoat's static analysis.
Refer to the LavaMoat documentation or ask for help if you run into any issues.
- The LavaMoat policy files
- If you are a MetaMask team member and your PR is on a repository branch, you can use the bot command `@metamaskbot update-policies` to ask the MetaMask bot to automatically update the policies for you.
- If your PR is from a fork, you can ask a MetaMask team member to help with updating the policy files.
- Manual update instructions: The _tl;dr_ is to run `yarn lavamoat:auto` to update these files, but there can be devils in the details:
- There are two sets of LavaMoat policy files:
- The production LavaMoat policy files (`lavamoat/browserify/*/policy.json`), which are re-generated using `yarn lavamoat:background:auto`. Add `--help` for usage.
- These should be regenerated whenever the production dependencies for the background change.
- The build system LavaMoat policy file (`lavamoat/build-system/policy.json`), which is re-generated using `yarn lavamoat:build:auto`.
- This should be regenerated whenever the dependencies used by the build system itself change.
- Whenever you regenerate a policy file, review the changes to determine whether the access granted to each package seems appropriate.
- Unfortunately, `yarn lavamoat:auto` will behave inconsistently on different platforms.
macOS and Windows users may see extraneous changes relating to optional dependencies.
- If you keep getting policy failures even after regenerating the policy files, try regenerating the policies after a clean install by doing:
- `rm -rf node_modules/ && yarn && yarn lavamoat:auto`
- Keep in mind that any kind of dynamic import or dynamic use of globals may elude LavaMoat's static analysis.
Refer to the LavaMoat documentation or ask for help if you run into any issues.
## Architecture

View File

@ -1562,6 +1562,12 @@
"message": "Betrüger aber schon.",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealSRP": {
"message": "Halten, um GWP anzuzeigen"
},
"holdToRevealSRPTitle": {
"message": "Bewahren Sie Ihre GWP sicher auf"
},
"ignoreAll": {
"message": "Alle ignorieren"
},
@ -3333,12 +3339,63 @@
},
"srpPasteFailedTooManyWords": {
"message": "Das Einfügen schlug fehl, weil sie mehr als 24 Wörter enthielt. Eine geheime Wiederherstellungsphrase darf maximal 24 Wörter enthalten.",
"description": "Description of SRP paste erorr when the pasted content has too many words"
"description": "Description of SRP paste error when the pasted content has too many words"
},
"srpPasteTip": {
"message": "Sie können Ihre gesamte geheime Wiederherstellungsphrase in ein beliebiges Feld einfügen",
"description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly."
},
"srpSecurityQuizGetStarted": {
"message": "Anfangen"
},
"srpSecurityQuizIntroduction": {
"message": "Zur Enthüllung Ihrer geheimen Wiederherstellungsphrase, müssen Sie zwei Fragen beantworten"
},
"srpSecurityQuizQuestionOneQuestion": {
"message": "Wenn Sie Ihre geheime Wiederherstellungsphrase verlieren, kann MetaMask ..."
},
"srpSecurityQuizQuestionOneRightAnswer": {
"message": "Ihnen nicht helfen"
},
"srpSecurityQuizQuestionOneRightAnswerDescription": {
"message": "Schreiben Sie sie auf, gravieren Sie sie in Metall ein oder bewahren Sie sie an mehreren geheimen Orten auf, damit Sie sie niemals verlieren. Sollten Sie sie verlieren, ist sie für immer weg."
},
"srpSecurityQuizQuestionOneRightAnswerTitle": {
"message": "Richtig! Niemand kann Ihnen dabei helfen, Ihre geheime Wiederherstellungsphrase zurückzubekommen"
},
"srpSecurityQuizQuestionOneWrongAnswer": {
"message": "diese für Sie zurückzubekommen"
},
"srpSecurityQuizQuestionOneWrongAnswerDescription": {
"message": "Wenn Sie Ihre geheime Wiederherstellungsphrase verlieren, ist diese für immer verloren. Niemand kann Ihnen dabei helfen, sie zurückzubekommen, egal, was behauptet wird."
},
"srpSecurityQuizQuestionOneWrongAnswerTitle": {
"message": "Falsch! Niemand kann Ihnen dabei helfen, Ihre geheime Wiederherstellungsphrase zurückzubekommen"
},
"srpSecurityQuizQuestionTwoQuestion": {
"message": "Sollte jemand, selbst ein Support-Mitarbeiter, nach Ihrer geheimen Wiederherstellungsphrase fragen ..."
},
"srpSecurityQuizQuestionTwoRightAnswer": {
"message": "Werden Sie betrogen"
},
"srpSecurityQuizQuestionTwoRightAnswerDescription": {
"message": "Jeder, der behauptet, Ihre gemeine Wiederherstellungsphrase zu benötigen, lügt Sie an. Wenn Sie diese mit solchen Personen teilen, werden diese Ihre Vermögenswerte stehlen."
},
"srpSecurityQuizQuestionTwoRightAnswerTitle": {
"message": "Richtig! Es ist nie eine gute Idee, Ihre geheime Wiederherstellungsphrase mit anderen zu teilen"
},
"srpSecurityQuizQuestionTwoWrongAnswer": {
"message": "Sie sollten sie dieser Person geben"
},
"srpSecurityQuizQuestionTwoWrongAnswerDescription": {
"message": "Jeder, der behauptet, Ihre gemeine Wiederherstellungsphrase zu benötigen, lügt Sie an. Wenn Sie diese mit einer solchen Person teilen, wird sie Ihre Vermögenswerte stehlen."
},
"srpSecurityQuizQuestionTwoWrongAnswerTitle": {
"message": "Nein! Teilen Sie Ihre geheime Wiederherstellungsphrase mit niemandem, niemals"
},
"srpSecurityQuizTitle": {
"message": "Sicherheits-Quiz"
},
"srpToggleShow": {
"message": "Dieses Wort der geheimen Wiederherstellungsphrase anzeigen/ausblenden",
"description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase"

View File

@ -1562,6 +1562,12 @@
"message": "αλλά οι απατεώνες μπορεί να το κάνουν.",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealSRP": {
"message": "Κρατήστε το πατημένο για να αποκαλυφθεί το ΜΦΑ"
},
"holdToRevealSRPTitle": {
"message": "Κρατήστε το ΜΦΑ σας ασφαλές"
},
"ignoreAll": {
"message": "Αγνόηση όλων"
},
@ -3330,12 +3336,63 @@
},
"srpPasteFailedTooManyWords": {
"message": "Η επικόλληση απέτυχε επειδή περιείχε περισσότερες από 24 λέξεις. Μια μυστική φράση ανάκτησης μπορεί να αποτελείται από το πολύ 24 λέξεις.",
"description": "Description of SRP paste erorr when the pasted content has too many words"
"description": "Description of SRP paste error when the pasted content has too many words"
},
"srpPasteTip": {
"message": "Μπορείτε να επικολλήσετε ολόκληρη τη μυστική φράση ανάκτησής σας σε οποιοδήποτε πεδίο",
"description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly."
},
"srpSecurityQuizGetStarted": {
"message": "Ξεκινήστε"
},
"srpSecurityQuizIntroduction": {
"message": "Για να σας αποκαλύψουμε τη Μυστική Φράση Ανάκτησης, πρέπει να απαντήσετε σωστά σε δύο ερωτήσεις"
},
"srpSecurityQuizQuestionOneQuestion": {
"message": "Αν χάσετε τη Μυστική Φράση Ανάκτησης, το MetaMask..."
},
"srpSecurityQuizQuestionOneRightAnswer": {
"message": "Δεν μπορεί να σας βοηθήσει"
},
"srpSecurityQuizQuestionOneRightAnswerDescription": {
"message": "Γράψτε την κάπου, χαράξτε την πάνω σε μέταλλο ή κρατήστε την σε πολλά μυστικά σημεία για να μην την χάσετε ποτέ. Αν την χάσετε, θα χαθεί για πάντα."
},
"srpSecurityQuizQuestionOneRightAnswerTitle": {
"message": "Σωστά! Κανείς δεν μπορεί να σας βοηθήσει να επαναφέρετε τη Μυστική Φράση Ανάκτησης"
},
"srpSecurityQuizQuestionOneWrongAnswer": {
"message": "Μπορεί να την επαναφέρει για εσάς"
},
"srpSecurityQuizQuestionOneWrongAnswerDescription": {
"message": "Αν χάσετε τη Μυστική Φράση Ανάκτησης, θα την χάσετε για πάντα. Κανείς δεν μπορεί να σας βοηθήσει να την επαναφέρετε, ό,τι κι αν σας πει."
},
"srpSecurityQuizQuestionOneWrongAnswerTitle": {
"message": "Λάθος! Κανείς δεν μπορεί να σας βοηθήσει να επαναφέρετε τη Μυστική Φράση Ανάκτησης"
},
"srpSecurityQuizQuestionTwoQuestion": {
"message": "Αν κάποιος, ακόμα και ένας τεχνικός υποστήριξης, σας ζητήσει τη Μυστική Φράση Ανάκτησης..."
},
"srpSecurityQuizQuestionTwoRightAnswer": {
"message": "Σας έχουν εξαπατήσει"
},
"srpSecurityQuizQuestionTwoRightAnswerDescription": {
"message": "Όποιος ισχυριστεί ότι χρειάζεται τη Μυστική Φράση Ανάκτησης, σας λέει ψέματα. Αν την μοιραστείτε μαζί του, θα κλέψει τα περιουσιακά σας στοιχεία."
},
"srpSecurityQuizQuestionTwoRightAnswerTitle": {
"message": "Σωστά! Το να μοιράζεστε τη Μυστική Φράση Ανάκτησης δεν είναι καλή ιδέα"
},
"srpSecurityQuizQuestionTwoWrongAnswer": {
"message": "Πρέπει να τους την δώσετε"
},
"srpSecurityQuizQuestionTwoWrongAnswerDescription": {
"message": "Όποιος ισχυριστεί ότι χρειάζεται τη Μυστική Φράση Ανάκτησης, σας λέει ψέματα. Αν την μοιραστείτε μαζί του, θα κλέψει τα περιουσιακά σας στοιχεία."
},
"srpSecurityQuizQuestionTwoWrongAnswerTitle": {
"message": "Όχι! Ποτέ μα ποτέ μην μοιραστείτε με κανέναν τη Μυστική Φράση Ανάκτησης"
},
"srpSecurityQuizTitle": {
"message": "Κουίζ Ασφαλείας"
},
"srpToggleShow": {
"message": "Εμφάνιση/Απόκρυψη αυτής της λέξης από τη μυστική φράση ανάκτησης",
"description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase"

View File

@ -2559,6 +2559,30 @@
"notePlaceholder": {
"message": "The approver will see this note when approving the transaction at the custodian."
},
"notificationTransactionFailedMessage": {
"message": "Transaction $1 failed! $2",
"description": "Content of the browser notification that appears when a transaction fails"
},
"notificationTransactionFailedMessageMMI": {
"message": "Transaction failed! $1",
"description": "Content of the browser notification that appears when a transaction fails in MMI"
},
"notificationTransactionFailedTitle": {
"message": "Failed transaction",
"description": "Title of the browser notification that appears when a transaction fails"
},
"notificationTransactionSuccessMessage": {
"message": "Transaction $1 confirmed!",
"description": "Content of the browser notification that appears when a transaction is confirmed"
},
"notificationTransactionSuccessTitle": {
"message": "Confirmed transaction",
"description": "Title of the browser notification that appears when a transaction is confirmed"
},
"notificationTransactionSuccessView": {
"message": "View on $1",
"description": "Additional content in browser notification that appears when a transaction is confirmed and has a block explorer URL"
},
"notifications": {
"message": "Notifications"
},
@ -4000,12 +4024,66 @@
},
"srpPasteFailedTooManyWords": {
"message": "Paste failed because it contained over 24 words. A secret recovery phrase can have a maximum of 24 words.",
"description": "Description of SRP paste erorr when the pasted content has too many words"
"description": "Description of SRP paste error when the pasted content has too many words"
},
"srpPasteTip": {
"message": "You can paste your entire secret recovery phrase into any field",
"description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly."
},
"srpSecurityQuizGetStarted": {
"message": "Get started"
},
"srpSecurityQuizImgAlt": {
"message": "An eye with a keyhole in the center, and three floating password fields"
},
"srpSecurityQuizIntroduction": {
"message": "To reveal your Secret Recovery Phrase, you need to correctly answer two questions"
},
"srpSecurityQuizQuestionOneQuestion": {
"message": "If you lose your Secret Recovery Phrase, MetaMask..."
},
"srpSecurityQuizQuestionOneRightAnswer": {
"message": "Cant help you"
},
"srpSecurityQuizQuestionOneRightAnswerDescription": {
"message": "Write it down, engrave it on metal, or keep it in multiple secret spots so you never lose it. If you lose it, its gone forever."
},
"srpSecurityQuizQuestionOneRightAnswerTitle": {
"message": "Right! No one can help get your Secret Recovery Phrase back"
},
"srpSecurityQuizQuestionOneWrongAnswer": {
"message": "Can get it back for you"
},
"srpSecurityQuizQuestionOneWrongAnswerDescription": {
"message": "If you lose your Secret Recovery Phrase, its gone forever. No one can help you get it back, no matter what they might say."
},
"srpSecurityQuizQuestionOneWrongAnswerTitle": {
"message": "Wrong! No one can help get your Secret Recovery Phrase back"
},
"srpSecurityQuizQuestionTwoQuestion": {
"message": "If anyone, even a support agent, asks for your Secret Recovery Phrase..."
},
"srpSecurityQuizQuestionTwoRightAnswer": {
"message": "Youre being scammed"
},
"srpSecurityQuizQuestionTwoRightAnswerDescription": {
"message": "Anyone claiming to need your Secret Recovery Phrase is lying to you. If you share it with them, they will steal your assets."
},
"srpSecurityQuizQuestionTwoRightAnswerTitle": {
"message": "Correct! Sharing your Secret Recovery Phrase is never a good idea"
},
"srpSecurityQuizQuestionTwoWrongAnswer": {
"message": "You should give it to them"
},
"srpSecurityQuizQuestionTwoWrongAnswerDescription": {
"message": "Anyone claiming to need your Secret Recovery Phrase is lying to you. If you share it with them, they will steal your assets."
},
"srpSecurityQuizQuestionTwoWrongAnswerTitle": {
"message": "Nope! Never share your Secret Recovery Phrase with anyone, ever"
},
"srpSecurityQuizTitle": {
"message": "Security quiz"
},
"srpToggleShow": {
"message": "Show/Hide this word of the secret recovery phrase",
"description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase"

View File

@ -1562,6 +1562,12 @@
"message": "pero los defraudadores sí.",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealSRP": {
"message": "Mantén presionado para revelar su SRP"
},
"holdToRevealSRPTitle": {
"message": "Proteja su SRP"
},
"ignoreAll": {
"message": "Ignorar todo"
},
@ -3333,12 +3339,63 @@
},
"srpPasteFailedTooManyWords": {
"message": "Pegar falló porque contenía más de 24 palabras. Una frase de recuperación secreta puede tener un máximo de 24 palabras.",
"description": "Description of SRP paste erorr when the pasted content has too many words"
"description": "Description of SRP paste error when the pasted content has too many words"
},
"srpPasteTip": {
"message": "Puede pegar toda su frase secreta de recuperación en cualquier campo",
"description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly."
},
"srpSecurityQuizGetStarted": {
"message": "Iniciar"
},
"srpSecurityQuizIntroduction": {
"message": "Para revelar su frase secreta de recuperación, debe responder correctamente dos preguntas"
},
"srpSecurityQuizQuestionOneQuestion": {
"message": "Si extravía su frase secreta de recuperación, MetaMask..."
},
"srpSecurityQuizQuestionOneRightAnswer": {
"message": "No puede ayudarlo"
},
"srpSecurityQuizQuestionOneRightAnswerDescription": {
"message": "Anótela, grábela en metal o guárdela en múltiples lugares secretos para que nunca la pierda. Si la extravía, se ha ido para siempre."
},
"srpSecurityQuizQuestionOneRightAnswerTitle": {
"message": "¡Correcto! Nadie puede ayudarlo a recuperar su frase secreta de recuperación"
},
"srpSecurityQuizQuestionOneWrongAnswer": {
"message": "Puede recuperarla para usted"
},
"srpSecurityQuizQuestionOneWrongAnswerDescription": {
"message": "Si pierde su frase secreta de recuperación, ésta desaparecerá para siempre. Nadie puede ayudarle a recuperarla, sin importar lo que digan."
},
"srpSecurityQuizQuestionOneWrongAnswerTitle": {
"message": "¡Incorrecto! Nadie puede ayudarlo a recuperar su frase secreta de recuperación"
},
"srpSecurityQuizQuestionTwoQuestion": {
"message": "Si alguien, incluso un agente de soporte, le pide su frase secreta de recuperación..."
},
"srpSecurityQuizQuestionTwoRightAnswer": {
"message": "Lo están estafando"
},
"srpSecurityQuizQuestionTwoRightAnswerDescription": {
"message": "Cualquiera que afirme necesitar su frase secreta de recuperación le está mintiendo. Si la comparte, le robarán sus activos."
},
"srpSecurityQuizQuestionTwoRightAnswerTitle": {
"message": "¡Correcto! Compartir su frase secreta de recuperación nunca es una buena idea"
},
"srpSecurityQuizQuestionTwoWrongAnswer": {
"message": "Debiera brindársela"
},
"srpSecurityQuizQuestionTwoWrongAnswerDescription": {
"message": "Cualquiera que afirme necesitar su frase secreta de recuperación le está mintiendo. Si la comparte, le robarán sus activos."
},
"srpSecurityQuizQuestionTwoWrongAnswerTitle": {
"message": "¡No! Nunca comparta su frase secreta de recuperación con nadie, nunca"
},
"srpSecurityQuizTitle": {
"message": "Cuestionario de seguridad"
},
"srpToggleShow": {
"message": "Mostrar/Ocultar esta palabra de la frase secreta de recuperación",
"description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase"

View File

@ -1562,6 +1562,12 @@
"message": "mais les hameçonneurs pourraient le faire.",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealSRP": {
"message": "Appuyez longuement pour révéler PSR"
},
"holdToRevealSRPTitle": {
"message": "Protégez votre PSR"
},
"ignoreAll": {
"message": "Ignorer tout"
},
@ -3333,12 +3339,63 @@
},
"srpPasteFailedTooManyWords": {
"message": "Le collage a échoué parce que la phrase contenait plus de 24 mots. Une phrase secrète de récupération peut contenir un maximum de 24 mots.",
"description": "Description of SRP paste erorr when the pasted content has too many words"
"description": "Description of SRP paste error when the pasted content has too many words"
},
"srpPasteTip": {
"message": "Vous pouvez coller toute votre phrase de récupération secrète dans nimporte quel champ",
"description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly."
},
"srpSecurityQuizGetStarted": {
"message": "Commencer"
},
"srpSecurityQuizIntroduction": {
"message": "Pour révéler votre Phrase secrète de récupération, vous devez répondre correctement à deux questions"
},
"srpSecurityQuizQuestionOneQuestion": {
"message": "Si vous perdez votre Phrase secrète de récupération, MetaMask..."
},
"srpSecurityQuizQuestionOneRightAnswer": {
"message": "Ne pourra pas vous aider"
},
"srpSecurityQuizQuestionOneRightAnswerDescription": {
"message": "Gravez-la sur une plaque en métal ou inscrivez-la sur plusieurs bouts de papier et cachez-les dans différents endroits secrets pour ne jamais la perdre. Si vous la perdez, il n'y a aucun moyen de la récupérer."
},
"srpSecurityQuizQuestionOneRightAnswerTitle": {
"message": "En effet ! Personne ne peut vous aider à récupérer votre Phrase secrète de récupération."
},
"srpSecurityQuizQuestionOneWrongAnswer": {
"message": "Pourra la récupérer pour vous"
},
"srpSecurityQuizQuestionOneWrongAnswerDescription": {
"message": "Personne ne peut vous aider à récupérer votre phrase secrète de récupération si jamais vous la perdez."
},
"srpSecurityQuizQuestionOneWrongAnswerTitle": {
"message": "C'est faux ! Personne ne peut vous aider à récupérer votre Phrase secrète de récupération."
},
"srpSecurityQuizQuestionTwoQuestion": {
"message": "Si un membre du service d'assistance ou toute autre personne vous demande votre Phrase secrète de récupération..."
},
"srpSecurityQuizQuestionTwoRightAnswer": {
"message": "Ne la lui fournissez pas, car cette personne essaie de vous arnaquer."
},
"srpSecurityQuizQuestionTwoRightAnswerDescription": {
"message": "Toute personne qui vous demande votre phrase secrète de récupération, que ce soit pour des raisons de sécurité ou autre, essaie de vous arnaquer."
},
"srpSecurityQuizQuestionTwoRightAnswerTitle": {
"message": "C'est exact ! Vous ne devez jamais partager votre Phrase secrète de récupération avec qui que ce soit."
},
"srpSecurityQuizQuestionTwoWrongAnswer": {
"message": "Vous devez la lui fournir"
},
"srpSecurityQuizQuestionTwoWrongAnswerDescription": {
"message": "Toute personne qui vous demande votre phrase secrète de récupération, que ce soit pour des raisons de sécurité ou autre, essaie de vous arnaquer."
},
"srpSecurityQuizQuestionTwoWrongAnswerTitle": {
"message": "C'est faux ! Vous ne devez jamais partager votre Phrase secrète de récupération avec qui que ce soit."
},
"srpSecurityQuizTitle": {
"message": "Quiz sur la sécurité"
},
"srpToggleShow": {
"message": "Afficher / Masquer ce mot de la phrase de récupération secrète",
"description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase"

View File

@ -1562,6 +1562,12 @@
"message": "लेकिन फिशर कर सकते हैं।",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealSRP": {
"message": "SRP दिखाने के लिए होल्ड करें"
},
"holdToRevealSRPTitle": {
"message": "अपना SRP सुरक्षित रखें"
},
"ignoreAll": {
"message": "सभी को अनदेखा करें"
},
@ -3333,12 +3339,63 @@
},
"srpPasteFailedTooManyWords": {
"message": "पेस्ट विफल हुआ क्योंकि उसमें 24 से ज़्यादा शब्द हैं। सीक्रेट रिकवरी फ़्रेज़ में अधिकतम 24 शब्द हो सकते हैं।",
"description": "Description of SRP paste erorr when the pasted content has too many words"
"description": "Description of SRP paste error when the pasted content has too many words"
},
"srpPasteTip": {
"message": "आप अपना पूरा सीक्रेट रिकवरी फ़्रेज किसी भी फ़ील्ड में पेस्ट कर सकते हैं",
"description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly."
},
"srpSecurityQuizGetStarted": {
"message": "शुरू करें"
},
"srpSecurityQuizIntroduction": {
"message": "अपना सीक्रेट रिकवरी फ्रेज़ प्रकट करने के लिए, आपको दो प्रश्नों का सही उत्तर देना होगा"
},
"srpSecurityQuizQuestionOneQuestion": {
"message": "यदि आप अपना सीक्रेट रिकवरी फ्रेज़ खो देते हैं, तो MetaMask..."
},
"srpSecurityQuizQuestionOneRightAnswer": {
"message": "आपकी मदद नहीं कर सकता"
},
"srpSecurityQuizQuestionOneRightAnswerDescription": {
"message": "इसे लिख लें, इसे किसी धातु पर उकेर दें, या इसे कई गुप्त स्थानों पर रखें ताकि आप इसे कभी न खोएं। यदि आप इसे खो देते हैं, तो यह हमेशा के लिए चला जाता है।"
},
"srpSecurityQuizQuestionOneRightAnswerTitle": {
"message": "सही! आपके सीक्रेट रिकवरी फ्रेज़ को वापस पाने में कोई भी सहायता नहीं कर सकता"
},
"srpSecurityQuizQuestionOneWrongAnswer": {
"message": "आपके लिए इसे वापस ला सकते हैं"
},
"srpSecurityQuizQuestionOneWrongAnswerDescription": {
"message": "यदि आप अपना सीक्रेट रिकवरी फ्रेज़ खो देते हैं, तो यह हमेशा के लिए चला जाता है। इसे वापस पाने में कोई भी आपकी मदद नहीं कर सकता, चाहे वे कुछ भी कहें।"
},
"srpSecurityQuizQuestionOneWrongAnswerTitle": {
"message": "गलत! आपके सीक्रेट रिकवरी फ्रेज़ को वापस पाने में कोई भी सहायता नहीं कर सकता"
},
"srpSecurityQuizQuestionTwoQuestion": {
"message": "यदि कोई, यहां तक कि एक सहायक एजेंट भी, आपका सीक्रेट रिकवरी फ्रेज़ मांगता है..."
},
"srpSecurityQuizQuestionTwoRightAnswer": {
"message": "तो आपके साथ धोखा किया जा रहा है"
},
"srpSecurityQuizQuestionTwoRightAnswerDescription": {
"message": "आपके सीक्रेट रिकवरी फ्रेज़ की आवश्यकता का दावा करने वाला कोई भी व्यक्ति आपसे झूठ बोल रहा है। यदि आप इसे उनके साथ साझा करते हैं, तो वे आपकी संपत्ति चुरा लेंगे।"
},
"srpSecurityQuizQuestionTwoRightAnswerTitle": {
"message": "सही! अपना सीक्रेट रिकवरी फ्रेज़ साझा करना कभी भी अच्छा विचार नहीं है"
},
"srpSecurityQuizQuestionTwoWrongAnswer": {
"message": "आपको उन्हें यह देना चाहिए"
},
"srpSecurityQuizQuestionTwoWrongAnswerDescription": {
"message": "आपके सीक्रेट रिकवरी फ्रेज़ की आवश्यकता का दावा करने वाला कोई भी व्यक्ति आपसे झूठ बोल रहा है। यदि आप इसे उनके साथ साझा करते हैं, तो वे आपकी संपत्तियां चुरा लेंगे।"
},
"srpSecurityQuizQuestionTwoWrongAnswerTitle": {
"message": "नहीं! अपने सीक्रेट रिकवरी फ्रेज़ को कभी भी किसी के साथ साझा न करें"
},
"srpSecurityQuizTitle": {
"message": "सुरक्षा प्रश्नोत्तरी"
},
"srpToggleShow": {
"message": "सीक्रेट रिकवरी फ़्रेज का ये शब्द दिखाएं/छुपाएं",
"description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase"

View File

@ -1562,6 +1562,12 @@
"message": "tetapi penipu akan mencoba memintanya.",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealSRP": {
"message": "Tahan untuk mengungkap FPR"
},
"holdToRevealSRPTitle": {
"message": "Jaga keamanan FPR Anda"
},
"ignoreAll": {
"message": "Abaikan semua"
},
@ -3333,12 +3339,63 @@
},
"srpPasteFailedTooManyWords": {
"message": "Gagal ditempel karena memuat lebih dari 24 kata. Frasa pemulihan rahasia dapat memuat maksimum 24 kata.",
"description": "Description of SRP paste erorr when the pasted content has too many words"
"description": "Description of SRP paste error when the pasted content has too many words"
},
"srpPasteTip": {
"message": "Anda bisa menempelkan seluruh frasa pemulihan rahasia ke bagian mana pun",
"description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly."
},
"srpSecurityQuizGetStarted": {
"message": "Mulai"
},
"srpSecurityQuizIntroduction": {
"message": "Untuk mengungkapkan Frasa Pemulihan Rahasia, Anda perlu menjawab dua pertanyaan dengan benar"
},
"srpSecurityQuizQuestionOneQuestion": {
"message": "Jika Anda kehilangan Frasa Pemulihan Rahasia, MetaMask..."
},
"srpSecurityQuizQuestionOneRightAnswer": {
"message": "Tidak dapat membantu Anda"
},
"srpSecurityQuizQuestionOneRightAnswerDescription": {
"message": "Catat, ukir pada logam, atau simpan di beberapa tempat rahasia agar Anda tidak pernah kehilangan. Jika Anda kehilangan, maka akan hilang selamanya."
},
"srpSecurityQuizQuestionOneRightAnswerTitle": {
"message": "Benar! Tidak ada yang dapat membantu mengembalikan Frasa Pemulihan Rahasia Anda"
},
"srpSecurityQuizQuestionOneWrongAnswer": {
"message": "Dapat mengembalikannya untuk Anda"
},
"srpSecurityQuizQuestionOneWrongAnswerDescription": {
"message": "Jika Anda kehilangan Frasa Pemulihan Rahasia, maka akan hilang selamanya. Tidak ada yang dapat membantu Anda mengembalikannya, apa pun yang mereka katakan."
},
"srpSecurityQuizQuestionOneWrongAnswerTitle": {
"message": "Salah! Tidak ada yang dapat membantu mengembalikan Frasa Pemulihan Rahasia Anda"
},
"srpSecurityQuizQuestionTwoQuestion": {
"message": "Jika ada yang menanyakan Frasa Pemulihan Rahasia Anda, bahkan agen pendukung,..."
},
"srpSecurityQuizQuestionTwoRightAnswer": {
"message": "Anda ditipu"
},
"srpSecurityQuizQuestionTwoRightAnswerDescription": {
"message": "Siapa pun yang mengaku membutuhkan Frasa Pemulihan Rahasia, mereka berbohong kepada Anda. Jika membaginya, mereka akan mencuri aset Anda."
},
"srpSecurityQuizQuestionTwoRightAnswerTitle": {
"message": "Benar! Membagikan Frasa Pemulihan Rahasia bukanlah ide yang baik"
},
"srpSecurityQuizQuestionTwoWrongAnswer": {
"message": "Anda harus memberikannya kepada mereka"
},
"srpSecurityQuizQuestionTwoWrongAnswerDescription": {
"message": "Siapa pun yang mengaku membutuhkan Frasa Pemulihan Rahasia, mereka berbohong kepada Anda. Jika membaginya, mereka akan mencuri aset Anda."
},
"srpSecurityQuizQuestionTwoWrongAnswerTitle": {
"message": "Tidak! Jangan pernah membagikan Frasa Pemulihan Rahasia kepada siapa pun"
},
"srpSecurityQuizTitle": {
"message": "Kuis keamanan"
},
"srpToggleShow": {
"message": "Tampilkan/Sembunyikan kata dari frasa pemulihan rahasia ini",
"description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase"

View File

@ -1562,6 +1562,12 @@
"message": "もし尋ねられた場合はフィッシング詐欺の可能性があります。",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealSRP": {
"message": "長押ししてSRPを表示"
},
"holdToRevealSRPTitle": {
"message": "SRPは安全に保管してください"
},
"ignoreAll": {
"message": "すべて無視"
},
@ -3333,12 +3339,63 @@
},
"srpPasteFailedTooManyWords": {
"message": "24 を超える単語が含まれていたため、貼り付けに失敗しました。秘密のリカバリーフレーズは 24 語までです。",
"description": "Description of SRP paste erorr when the pasted content has too many words"
"description": "Description of SRP paste error when the pasted content has too many words"
},
"srpPasteTip": {
"message": "秘密のリカバリーフレーズ全体をいずれかのフィールドに張り付けできます。",
"description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly."
},
"srpSecurityQuizGetStarted": {
"message": "開始"
},
"srpSecurityQuizIntroduction": {
"message": "秘密のリカバリーフレーズを表示するには、2 つの質問に正しく答える必要があります。"
},
"srpSecurityQuizQuestionOneQuestion": {
"message": "秘密のリカバリーフレーズをなくした場合、MetaMask は..."
},
"srpSecurityQuizQuestionOneRightAnswer": {
"message": "どうすることもできません"
},
"srpSecurityQuizQuestionOneRightAnswerDescription": {
"message": "書き留めたり金属に掘ったり、いくつかの秘密の場所に保管したりして、絶対になくさないようにしてください。なくした場合、一生戻ってきません。"
},
"srpSecurityQuizQuestionOneRightAnswerTitle": {
"message": "正解です!秘密のリカバリーフレーズは誰にも取り戻すことができません"
},
"srpSecurityQuizQuestionOneWrongAnswer": {
"message": "それを取り戻すことができます"
},
"srpSecurityQuizQuestionOneWrongAnswerDescription": {
"message": "秘密のリカバリーフレーズをなくした場合、一生戻ってきません。誰が何と言おうと、誰にも取り戻すことはできません。"
},
"srpSecurityQuizQuestionOneWrongAnswerTitle": {
"message": "不正解!秘密のリカバリーフレーズは誰にも取り戻せません"
},
"srpSecurityQuizQuestionTwoQuestion": {
"message": "誰かに秘密のリカバリーフレーズを尋ねられたら、それがサポート担当者であっても..."
},
"srpSecurityQuizQuestionTwoRightAnswer": {
"message": "あなたは騙されようとしています"
},
"srpSecurityQuizQuestionTwoRightAnswerDescription": {
"message": "秘密のリカバリーフレーズが必要だと言われたら、それは嘘です。教えてしまったら資産を盗まれます。"
},
"srpSecurityQuizQuestionTwoRightAnswerTitle": {
"message": "正解です!秘密のリカバリーフレーズは決して誰にも教えてはいけません"
},
"srpSecurityQuizQuestionTwoWrongAnswer": {
"message": "教えるべきです"
},
"srpSecurityQuizQuestionTwoWrongAnswerDescription": {
"message": "秘密のリカバリーフレーズが必要だと言われたら、それは嘘です。教えてしまったら資産を盗まれます。"
},
"srpSecurityQuizQuestionTwoWrongAnswerTitle": {
"message": "不正解!秘密のリカバリーフレーズは決して誰にも教えないでください"
},
"srpSecurityQuizTitle": {
"message": "セキュリティの質問"
},
"srpToggleShow": {
"message": "秘密のリカバリーフレーズのこの単語を表示・非表示",
"description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase"

View File

@ -1562,6 +1562,12 @@
"message": "오히려 피싱 사기꾼들이 요구할 수 있으니 주의가 필요합니다.",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealSRP": {
"message": "눌러서 SRP 정보를 확인하세요"
},
"holdToRevealSRPTitle": {
"message": "SRP 정보를 안전하게 보관하세요"
},
"ignoreAll": {
"message": "모두 무시"
},
@ -3333,12 +3339,63 @@
},
"srpPasteFailedTooManyWords": {
"message": "단어가 24개를 초과하여 붙여넣기에 실패했습니다. 비밀 복구 구문은 24개 이하의 단어로 이루어집니다.",
"description": "Description of SRP paste erorr when the pasted content has too many words"
"description": "Description of SRP paste error when the pasted content has too many words"
},
"srpPasteTip": {
"message": "비밀 복구 구문 전체를 아무 입력란에 붙여넣을 수 있습니다",
"description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly."
},
"srpSecurityQuizGetStarted": {
"message": "시작하기"
},
"srpSecurityQuizIntroduction": {
"message": "비밀 복구 구문을 찾으려면 두 가지 질문에 올바르게 답해야 합니다"
},
"srpSecurityQuizQuestionOneQuestion": {
"message": "비밀 복구 구문을 분실하시면 MetaMask가..."
},
"srpSecurityQuizQuestionOneRightAnswer": {
"message": "도와드릴 수 없습니다"
},
"srpSecurityQuizQuestionOneRightAnswerDescription": {
"message": "따라서 이를 적거나, 금속 등에 새기거나, 여러 비밀 장소에 보관하여 절대로 잃어버리지 않도록 하세요. 한 번 잃어버리면 영원히 찾을 수 없습니다."
},
"srpSecurityQuizQuestionOneRightAnswerTitle": {
"message": "맞습니다! 아무도 본인의 비밀 복구 구문을 복구할 수 없습니다"
},
"srpSecurityQuizQuestionOneWrongAnswer": {
"message": "찾아드릴 수 있습니다"
},
"srpSecurityQuizQuestionOneWrongAnswerDescription": {
"message": "비밀 복구 구문은 한 번 잃어버리면 영원히 찾을 수 없습니다. 누가 뭐라고 해도 아무도 이를 찾아드리지 못합니다."
},
"srpSecurityQuizQuestionOneWrongAnswerTitle": {
"message": "아닙니다! 아무도 본인의 비밀 복구 구문을 복구할 수 없습니다"
},
"srpSecurityQuizQuestionTwoQuestion": {
"message": "누군가, 심지어 고객 센터 직원이라고 해도 여러분의 비밀 복구 구문을 물어본다면..."
},
"srpSecurityQuizQuestionTwoRightAnswer": {
"message": "이는 반드시 사기입니다"
},
"srpSecurityQuizQuestionTwoRightAnswerDescription": {
"message": "비밀 복구 구문이 필요하다고 하는 사람은 모두 거짓말쟁이입니다. 그런 자들과 비밀 복구 구문을 공유하면 자산을 도둑맞게 됩니다."
},
"srpSecurityQuizQuestionTwoRightAnswerTitle": {
"message": "맞습니다! 비밀 복구 구문은 아무와도 공유하면 안 됩니다"
},
"srpSecurityQuizQuestionTwoWrongAnswer": {
"message": "주어야 합니다"
},
"srpSecurityQuizQuestionTwoWrongAnswerDescription": {
"message": "비밀 복구 구문이 필요하다고 하는 사람은 모두 거짓말쟁이입니다. 그런 자들과 비밀 복구 구문을 공유하면 자산을 도둑맞게 됩니다."
},
"srpSecurityQuizQuestionTwoWrongAnswerTitle": {
"message": "맞습니다! 비밀 복구 구문은 절대로 아무와도 공유하면 안 됩니다"
},
"srpSecurityQuizTitle": {
"message": "보안 퀴즈"
},
"srpToggleShow": {
"message": "비밀 복구 구문 중에서 이 단어 공개하기/숨기기",
"description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase"

View File

@ -1562,6 +1562,12 @@
"message": "mas os phishers talvez solicitem.",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealSRP": {
"message": "Mantenha pressionado para revelar FRS"
},
"holdToRevealSRPTitle": {
"message": "Mantenha sua FRS protegida"
},
"ignoreAll": {
"message": "Ignorar tudo"
},
@ -3333,12 +3339,63 @@
},
"srpPasteFailedTooManyWords": {
"message": "A função colar falhou porque continha mais de 24 palavras. Uma frase secreta de recuperação pode ter no máximo 24 palavras.",
"description": "Description of SRP paste erorr when the pasted content has too many words"
"description": "Description of SRP paste error when the pasted content has too many words"
},
"srpPasteTip": {
"message": "Você pode colar a sua frase secreta de recuperação inteira em qualquer campo",
"description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly."
},
"srpSecurityQuizGetStarted": {
"message": "Começar"
},
"srpSecurityQuizIntroduction": {
"message": "Para revelar sua Frase de Recuperação Secreta, você precisa responder corretamente duas perguntas"
},
"srpSecurityQuizQuestionOneQuestion": {
"message": "Se você perder sua Frase de Recuperação Secreta, a MetaMask..."
},
"srpSecurityQuizQuestionOneRightAnswer": {
"message": "Não poderá ajudar"
},
"srpSecurityQuizQuestionOneRightAnswerDescription": {
"message": "Anote-a, grave em metal ou guarde-a em diversos lugares secretos para que nunca a perca. Se perdê-la, é para sempre."
},
"srpSecurityQuizQuestionOneRightAnswerTitle": {
"message": "Certo! Ninguém pode ajudar a recuperar sua Frase de Recuperação Secreta"
},
"srpSecurityQuizQuestionOneWrongAnswer": {
"message": "Poderá recuperá-la para você"
},
"srpSecurityQuizQuestionOneWrongAnswerDescription": {
"message": "Se você perder sua Frase de Recuperação Secreta, é para sempre. Ninguém consegue ajudar a recuperá-la, não importa o que digam."
},
"srpSecurityQuizQuestionOneWrongAnswerTitle": {
"message": "Errado! Ninguém consegue recuperar sua Frase de Recuperação Secreta"
},
"srpSecurityQuizQuestionTwoQuestion": {
"message": "Se alguém, até mesmo um atendente do suporte, pedir sua Frase de Recuperação Secreta..."
},
"srpSecurityQuizQuestionTwoRightAnswer": {
"message": "Você estará sendo vítima de um golpe"
},
"srpSecurityQuizQuestionTwoRightAnswerDescription": {
"message": "Qualquer pessoa que afirme precisar da sua Frase de Recuperação Secreta está mentindo. Se você compartilhar com ela, seus ativos serão roubados."
},
"srpSecurityQuizQuestionTwoRightAnswerTitle": {
"message": "Correto! Compartilhar sua Frase de Recuperação Secreta nunca é uma boa ideia"
},
"srpSecurityQuizQuestionTwoWrongAnswer": {
"message": "Você deverá revelar"
},
"srpSecurityQuizQuestionTwoWrongAnswerDescription": {
"message": "Qualquer pessoa que afirme precisar da sua Frase de Recuperação Secreta está mentindo. Se você compartilhar com ela, seus ativos serão roubados."
},
"srpSecurityQuizQuestionTwoWrongAnswerTitle": {
"message": "Não! Não compartilhe sua Frase de Recuperação Secreta com ninguém, nunca"
},
"srpSecurityQuizTitle": {
"message": "Quiz de segurança"
},
"srpToggleShow": {
"message": "Mostrar/Ocultar esta palavra da frase secreta de recuperação",
"description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase"

View File

@ -1562,6 +1562,12 @@
"message": "но злоумышленники-фишеры могут.",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealSRP": {
"message": "Удерживайте, чтобы показать СФВ"
},
"holdToRevealSRPTitle": {
"message": "Храните СФВ в безопасности"
},
"ignoreAll": {
"message": "Игнорировать все"
},
@ -3333,12 +3339,63 @@
},
"srpPasteFailedTooManyWords": {
"message": "Не удалось вставить, так как он содержит более 24 слов. Секретная фраза для восстановления может содержать не более 24 слов.",
"description": "Description of SRP paste erorr when the pasted content has too many words"
"description": "Description of SRP paste error when the pasted content has too many words"
},
"srpPasteTip": {
"message": "Вы можете вставить всю свою секретную фразу для восстановления в любое поле",
"description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly."
},
"srpSecurityQuizGetStarted": {
"message": "Начать"
},
"srpSecurityQuizIntroduction": {
"message": "Чтобы увидеть свою секретную фразу для восстановления, вам нужно правильно ответить на два вопроса"
},
"srpSecurityQuizQuestionOneQuestion": {
"message": "Если вы потеряете свою секретную фразу для восстановления, MetaMask..."
},
"srpSecurityQuizQuestionOneRightAnswer": {
"message": "Не сможет вам помочь"
},
"srpSecurityQuizQuestionOneRightAnswerDescription": {
"message": "Запишите ее, выгравируйте ее на металле или храните в нескольких потайных местах, чтобы никогда не потерять. Если вы потеряете ее, она пропадет навсегда."
},
"srpSecurityQuizQuestionOneRightAnswerTitle": {
"message": "Правильно! Никто не может помочь вернуть вашу секретную фразу для восстановления"
},
"srpSecurityQuizQuestionOneWrongAnswer": {
"message": "Не сможет вернуть ее вам"
},
"srpSecurityQuizQuestionOneWrongAnswerDescription": {
"message": "Если вы потеряете свою секретную фразу для восстановления, она пропадет навсегда. Никто не может помочь вам вернуть ее, что бы кто ни говорил."
},
"srpSecurityQuizQuestionOneWrongAnswerTitle": {
"message": "Неправильно! Никто не может помочь вернуть вашу секретную фразу для восстановления"
},
"srpSecurityQuizQuestionTwoQuestion": {
"message": "Если кто-нибудь, даже представитель службы поддержки, попросит вашу секретную фразу для восстановления..."
},
"srpSecurityQuizQuestionTwoRightAnswer": {
"message": "Вас обманывают"
},
"srpSecurityQuizQuestionTwoRightAnswerDescription": {
"message": "Любой, кто утверждает, что ему нужна ваша секретная фраза для восстановления, лжет вам. Если вы сообщите эту фразу ему (ей), он(-а) украдет ваши активы."
},
"srpSecurityQuizQuestionTwoRightAnswerTitle": {
"message": "Правильно! Сообщать кому-либо своей секретную фразу для восстановления — это всегда плохая идея"
},
"srpSecurityQuizQuestionTwoWrongAnswer": {
"message": "Вы должны сообщите фразу ему (ей)"
},
"srpSecurityQuizQuestionTwoWrongAnswerDescription": {
"message": "Любой, кто утверждает, что ему нужна ваша секретная фраза для восстановления, лжет вам. Если вы сообщите эту фразу ему (ей), он(-а) украдет ваши активы."
},
"srpSecurityQuizQuestionTwoWrongAnswerTitle": {
"message": "Нет! Никогда никому не сообщайте никому свою секретную фразу для восстановления"
},
"srpSecurityQuizTitle": {
"message": "Тест по безопасности"
},
"srpToggleShow": {
"message": "Показать/скрыть это слово секретной фразы для восстановления",
"description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase"

View File

@ -1562,6 +1562,12 @@
"message": "ngunit maaring hingin ng mga phisher.",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealSRP": {
"message": "I-hold para ipakita ang SRP"
},
"holdToRevealSRPTitle": {
"message": "Panatilihing ligtas ang iyong SRP"
},
"ignoreAll": {
"message": "Huwag pansinin ang lahat"
},
@ -3333,12 +3339,63 @@
},
"srpPasteFailedTooManyWords": {
"message": "Nabigong i-paste dahil naglalaman ito ng higit sa 24 na salita. Ang secret recovery phrase ay mayroong hanggang 24 na salita lamang.",
"description": "Description of SRP paste erorr when the pasted content has too many words"
"description": "Description of SRP paste error when the pasted content has too many words"
},
"srpPasteTip": {
"message": "Maaari mong i-paste ang iyong buong secret recovery phrase sa alinmang patlang",
"description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly."
},
"srpSecurityQuizGetStarted": {
"message": "Magsimula"
},
"srpSecurityQuizIntroduction": {
"message": "Upang ipakita ang iyong Secret Recovery Phrase, kailangan mong sagutin nang tama ang dalawang tanong"
},
"srpSecurityQuizQuestionOneQuestion": {
"message": "Kung mawala mo ang iyong Secret Recovery Phrase, ang MetaMask ay..."
},
"srpSecurityQuizQuestionOneRightAnswer": {
"message": "Hindi ka matutulungan"
},
"srpSecurityQuizQuestionOneRightAnswerDescription": {
"message": "Isulat ito, iukit sa metal, o itago ito sa maraming lihim na lugar upang hindi ito mawala. Kung nawala mo ito, wala na ito ng tuluyan."
},
"srpSecurityQuizQuestionOneRightAnswerTitle": {
"message": "Tama! Walang makakatulong na maibalik ang iyong Secret Recovery Phrase"
},
"srpSecurityQuizQuestionOneWrongAnswer": {
"message": "Maaari itong ibalik para sa iyo"
},
"srpSecurityQuizQuestionOneWrongAnswerDescription": {
"message": "Kung nawala mo ang iyong Secret Recovery Phrase mawawala na ito nang tuluyan. Walang makakatulong sa iyo na maibalik ito, anuman ang maaaring sabihin nila."
},
"srpSecurityQuizQuestionOneWrongAnswerTitle": {
"message": "Mali! Walang makakatulong na maibalik ang iyong Secret Recovery Phrase"
},
"srpSecurityQuizQuestionTwoQuestion": {
"message": "Kung sinuman, kahit isang ahente ng suporta, ay humingi ng iyong Secret Recovery Phrase..."
},
"srpSecurityQuizQuestionTwoRightAnswer": {
"message": "Niloloko ka"
},
"srpSecurityQuizQuestionTwoRightAnswerDescription": {
"message": "Sinumang nagsasabing nangangailangan ng iyong Secret Recovery Phrase ay nagsisinungaling sa iyo. Kung ibabahagi mo ito sa kanila, nanakawin nila ng iyong mga ari-arian."
},
"srpSecurityQuizQuestionTwoRightAnswerTitle": {
"message": "Tama! Ang pagbabahagi ng iyong Secret Recovery Phrase ay hindi kailanman isang magandang ideya"
},
"srpSecurityQuizQuestionTwoWrongAnswer": {
"message": "Dapat mong ibigay sa kanila"
},
"srpSecurityQuizQuestionTwoWrongAnswerDescription": {
"message": "Sinumang nagsasabing nangangailangan ng iyong Secret Recovery Phrase ay nagsisinungaling sa iyo. Kung ibabahagi mo ito sa kanila, nanakawin nila ng iyong mga ari-arian."
},
"srpSecurityQuizQuestionTwoWrongAnswerTitle": {
"message": "Hindi! Huwag kailanman ibahagi ang iyong Secret Recovery Phrase sa sinuman, kailanman"
},
"srpSecurityQuizTitle": {
"message": "Pagsusulit sa seguridad"
},
"srpToggleShow": {
"message": "Ipakita/Itago ang salitang ito ng secret recovery phrase",
"description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase"

View File

@ -1562,6 +1562,12 @@
"message": "ancak dolandırıcılar talep edilebilir.",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealSRP": {
"message": "GKİ bilgisinin gösterilmesi için tut"
},
"holdToRevealSRPTitle": {
"message": "GKİ bilgini güvende tut"
},
"ignoreAll": {
"message": "Tümünü yoksay"
},
@ -3333,12 +3339,63 @@
},
"srpPasteFailedTooManyWords": {
"message": "24'ten fazla sözcük içerdiği için yapıştırma başarısız oldu. Gizli bir kurtarma ifadesi en fazla 24 sözcükten oluşabilir.",
"description": "Description of SRP paste erorr when the pasted content has too many words"
"description": "Description of SRP paste error when the pasted content has too many words"
},
"srpPasteTip": {
"message": "Gizli kurtarma ifadenin tamamını herhangi bir alana yapıştırabilirsin",
"description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly."
},
"srpSecurityQuizGetStarted": {
"message": "Başla"
},
"srpSecurityQuizIntroduction": {
"message": "Gizli Kurtarma İfadenizi görmek için iki soruyu doğru cevaplamanız gerekmektedir"
},
"srpSecurityQuizQuestionOneQuestion": {
"message": "Gizli Kurtarma İfadenizi kaybederseniz MetaMask..."
},
"srpSecurityQuizQuestionOneRightAnswer": {
"message": "Size yardımcı olamaz"
},
"srpSecurityQuizQuestionOneRightAnswerDescription": {
"message": "Bir yere yazın, bir metalin üzerine kazıyın veya asla kaybetmemeniz için birden fazla noktada saklayın. Kaybederseniz sonsuza dek kaybolur."
},
"srpSecurityQuizQuestionOneRightAnswerTitle": {
"message": "Doğru! Hiç kimse Gizli Kurtarma İfadenizi geri almanıza yardımcı olamaz"
},
"srpSecurityQuizQuestionOneWrongAnswer": {
"message": "Size onu tekrar verebilir"
},
"srpSecurityQuizQuestionOneWrongAnswerDescription": {
"message": "Gizli Kurtarma İfadenizi kaybederseniz sonsuza dek kaybolur. Söylediklerinin ne olduğuna bakılmaksızın hiç kimse onu geri almanıza yardımcı olamaz."
},
"srpSecurityQuizQuestionOneWrongAnswerTitle": {
"message": "Yanlış! Hiç kimse Gizli Kurtarma İfadenizi geri almanıza yardımcı olamaz"
},
"srpSecurityQuizQuestionTwoQuestion": {
"message": "Herhangi birisi, bir destek temsilcisi bile sizden Gizli Kurtarma İfadenizi isterse..."
},
"srpSecurityQuizQuestionTwoRightAnswer": {
"message": "Dolandırılıyorsunuzdur"
},
"srpSecurityQuizQuestionTwoRightAnswerDescription": {
"message": "Gizli Kurtarma İfadenizi isteyen kişi size yalan söylüyordur. Kendisi ile paylaşırsanız varlıklarınızı çalacaktır."
},
"srpSecurityQuizQuestionTwoRightAnswerTitle": {
"message": "Doğru! Gizli Kurtarma İfadenizi paylaşmak asla iyi bir fikir değildir"
},
"srpSecurityQuizQuestionTwoWrongAnswer": {
"message": "Kendisine vermelisiniz"
},
"srpSecurityQuizQuestionTwoWrongAnswerDescription": {
"message": "Gizli Kurtarma İfadenizi isteyen kişi size yalan söylüyordur. Kendisi ile paylaşırsanız varlıklarınızı çalacaktır."
},
"srpSecurityQuizQuestionTwoWrongAnswerTitle": {
"message": "Hayır! Gizli Kurtarma İfadenizi asla hiç kimse ile paylaşmayın, asla"
},
"srpSecurityQuizTitle": {
"message": "Güvenlik testi"
},
"srpToggleShow": {
"message": "Gizli kurtarma ifadesinin bu sözcüğünü göster/gizle",
"description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase"

View File

@ -1562,6 +1562,12 @@
"message": "nhưng những kẻ lừa đảo qua mạng thì có.",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealSRP": {
"message": "Giữ để hiển thị SRP"
},
"holdToRevealSRPTitle": {
"message": "Đảm bảo an toàn cho SRP của bạn"
},
"ignoreAll": {
"message": "Bỏ qua tất cả"
},
@ -3333,12 +3339,63 @@
},
"srpPasteFailedTooManyWords": {
"message": "Dán không thành công vì cụm từ có nhiều hơn 24 từ. Cụm từ khôi phục bí mật chỉ có tối đa 24 từ.",
"description": "Description of SRP paste erorr when the pasted content has too many words"
"description": "Description of SRP paste error when the pasted content has too many words"
},
"srpPasteTip": {
"message": "Bạn có thể dán toàn bộ cụm từ khôi phục bí mật vào bất kỳ trường nào",
"description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly."
},
"srpSecurityQuizGetStarted": {
"message": "Bắt đầu"
},
"srpSecurityQuizIntroduction": {
"message": "Để hiển thị Cụm từ khôi phục bí mật, bạn cần trả lời đúng hai câu hỏi"
},
"srpSecurityQuizQuestionOneQuestion": {
"message": "Nếu bạn làm mất Cụm từ khôi phục bí mật, MetaMask..."
},
"srpSecurityQuizQuestionOneRightAnswer": {
"message": "Không thể giúp bạn"
},
"srpSecurityQuizQuestionOneRightAnswerDescription": {
"message": "Hãy viết ra, khắc lên kim loại hoặc cất giữ ở nhiều nơi bí mật để bạn không bao giờ làm mất nó. Nếu bạn làm mất, nó sẽ bị mất vĩnh viễn."
},
"srpSecurityQuizQuestionOneRightAnswerTitle": {
"message": "Đúng! Không ai có thể giúp bạn lấy lại Cụm từ khôi phục bí mật"
},
"srpSecurityQuizQuestionOneWrongAnswer": {
"message": "Có thể lấy lại cho bạn"
},
"srpSecurityQuizQuestionOneWrongAnswerDescription": {
"message": "Nếu bạn làm mất Cụm từ khôi phục bí mật, nó sẽ bị mất vĩnh viễn. Dù mọi người có nói gì, thì cũng không ai có thể giúp bạn lấy lại."
},
"srpSecurityQuizQuestionOneWrongAnswerTitle": {
"message": "Sai! Không ai có thể giúp bạn lấy lại Cụm từ khôi phục bí mật"
},
"srpSecurityQuizQuestionTwoQuestion": {
"message": "Nếu có bất kỳ ai, kể cả nhân viên hỗ trợ, hỏi về Cụm từ khôi phục bí mật của bạn..."
},
"srpSecurityQuizQuestionTwoRightAnswer": {
"message": "Bạn đang bị lừa đảo"
},
"srpSecurityQuizQuestionTwoRightAnswerDescription": {
"message": "Bất kỳ ai nói rằng họ cần Cụm từ khôi phục bí mật của bạn thì đều đang nói dối bạn. Nếu bạn chia sẻ với họ thì họ sẽ đánh cắp tài sản của bạn."
},
"srpSecurityQuizQuestionTwoRightAnswerTitle": {
"message": "Chính xác! Chia sẻ Cụm từ khôi phục bí mật chưa bao giờ là một ý hay"
},
"srpSecurityQuizQuestionTwoWrongAnswer": {
"message": "Bạn nên đưa nó cho họ"
},
"srpSecurityQuizQuestionTwoWrongAnswerDescription": {
"message": "Bất kỳ ai nói rằng họ cần Cụm từ khôi phục bí mật của bạn thì đều đang nói dối bạn. Nếu bạn chia sẻ với họ thì họ sẽ đánh cắp tài sản của bạn."
},
"srpSecurityQuizQuestionTwoWrongAnswerTitle": {
"message": "Không! Tuyệt đối không bao giờ chia sẻ Cụm từ khôi phục bí mật của bạn với bất kỳ ai"
},
"srpSecurityQuizTitle": {
"message": "Câu hỏi bảo mật"
},
"srpToggleShow": {
"message": "Hiện/Ẩn từ này của cụm từ khôi phục bí mật",
"description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase"

View File

@ -1562,6 +1562,12 @@
"message": "但网络钓鱼者可能会。",
"description": "The text link in 'holdToRevealContent3'"
},
"holdToRevealSRP": {
"message": "按住以显示 助记词"
},
"holdToRevealSRPTitle": {
"message": "保护您的 助记词 安全"
},
"ignoreAll": {
"message": "忽略所有"
},
@ -3333,12 +3339,63 @@
},
"srpPasteFailedTooManyWords": {
"message": "粘贴失败因为它包含超过24个单词。一个助记词最多可包含24个单词。",
"description": "Description of SRP paste erorr when the pasted content has too many words"
"description": "Description of SRP paste error when the pasted content has too many words"
},
"srpPasteTip": {
"message": "您可以将整个助记词粘贴到任何字段中",
"description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly."
},
"srpSecurityQuizGetStarted": {
"message": "开始"
},
"srpSecurityQuizIntroduction": {
"message": "要查看助记词,您需要答对两个问题"
},
"srpSecurityQuizQuestionOneQuestion": {
"message": "如果您丢失了助记词MetaMask......"
},
"srpSecurityQuizQuestionOneRightAnswer": {
"message": "无法帮助您"
},
"srpSecurityQuizQuestionOneRightAnswerDescription": {
"message": "将它写下来、刻在金属上,或保存在多个秘密位置,这样您就不会丢失它。如果丢失了,它就会永远消失。"
},
"srpSecurityQuizQuestionOneRightAnswerTitle": {
"message": "答对了!没有人能够帮您找回您的助记词"
},
"srpSecurityQuizQuestionOneWrongAnswer": {
"message": "可以为您找回来"
},
"srpSecurityQuizQuestionOneWrongAnswerDescription": {
"message": "一旦遗失助记词,它将永远消失。无论他人如何保证,无人能够帮您找回。"
},
"srpSecurityQuizQuestionOneWrongAnswerTitle": {
"message": "答错了!没有人能够帮您找回您的助记词"
},
"srpSecurityQuizQuestionTwoQuestion": {
"message": "如果有人(即使是技术支持人员)查问您的助记词......"
},
"srpSecurityQuizQuestionTwoRightAnswer": {
"message": "就是在对您进行诈骗"
},
"srpSecurityQuizQuestionTwoRightAnswerDescription": {
"message": "任何声称需要您的助记词的人都在对您进行欺诈。如果您与他们分享助记词,他们就会偷窃您的资产。"
},
"srpSecurityQuizQuestionTwoRightAnswerTitle": {
"message": "答对了!分享您的助记词绝对不是个好主意"
},
"srpSecurityQuizQuestionTwoWrongAnswer": {
"message": "您应该交给他们"
},
"srpSecurityQuizQuestionTwoWrongAnswerDescription": {
"message": "任何声称需要您的助记词的人都在对您进行欺诈。如果您与他们分享助记词,他们就会偷窃您的资产。"
},
"srpSecurityQuizQuestionTwoWrongAnswerTitle": {
"message": "不!永远不要与任何人分享您的助记词"
},
"srpSecurityQuizTitle": {
"message": "安全问答"
},
"srpToggleShow": {
"message": "显示/隐藏助记词中的这个单词",
"description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase"

BIN
app/images/reveal-srp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -45,7 +45,7 @@ export type CoreMessage = AbstractMessage & {
};
export type StateMessage = Required<
Omit<AbstractMessage, 'securityProviderResponse' | 'metadata'>
Omit<AbstractMessage, 'securityProviderResponse' | 'metadata' | 'error'>
>;
export type DecryptMessageControllerState = {

View File

@ -45,7 +45,7 @@ export type CoreMessage = AbstractMessage & {
};
export type StateMessage = Required<
Omit<AbstractMessage, 'securityProviderResponse' | 'metadata'>
Omit<AbstractMessage, 'securityProviderResponse' | 'metadata' | 'error'>
> & {
msgParams: string;
};

File diff suppressed because it is too large Load Diff

View File

@ -205,6 +205,7 @@ import {
} from './controllers/permissions';
import createRPCMethodTrackingMiddleware from './lib/createRPCMethodTrackingMiddleware';
import { securityProviderCheck } from './lib/security-provider-helpers';
import { updateCurrentLocale } from './translate';
export const METAMASK_CONTROLLER_EVENTS = {
// Fired after state changes that impact the extension badge (unapproved msg count)
@ -360,6 +361,10 @@ export default class MetamaskController extends EventEmitter {
///: END:ONLY_INCLUDE_IN
});
this.preferencesController.store.subscribe(async ({ currentLocale }) => {
await updateCurrentLocale(currentLocale);
});
this.tokensController = new TokensController({
chainId: this.networkController.store.getState().providerConfig.chainId,
onPreferencesStateChange: this.preferencesController.store.subscribe.bind(
@ -1263,11 +1268,7 @@ export default class MetamaskController extends EventEmitter {
this.signatureController = new SignatureController({
messenger: this.controllerMessenger.getRestricted({
name: 'SignatureController',
allowedActions: [
`${this.approvalController.name}:addRequest`,
`${this.approvalController.name}:acceptRequest`,
`${this.approvalController.name}:rejectRequest`,
],
allowedActions: [`${this.approvalController.name}:addRequest`],
}),
keyringController: this.keyringController,
isEthSignEnabled: () =>
@ -2253,27 +2254,6 @@ export default class MetamaskController extends EventEmitter {
updatePreviousGasParams:
txController.updatePreviousGasParams.bind(txController),
// signatureController
signMessage: this.signatureController.signMessage.bind(
this.signatureController,
),
cancelMessage: this.signatureController.cancelMessage.bind(
this.signatureController,
),
signPersonalMessage: this.signatureController.signPersonalMessage.bind(
this.signatureController,
),
cancelPersonalMessage:
this.signatureController.cancelPersonalMessage.bind(
this.signatureController,
),
signTypedMessage: this.signatureController.signTypedMessage.bind(
this.signatureController,
),
cancelTypedMessage: this.signatureController.cancelTypedMessage.bind(
this.signatureController,
),
// decryptMessageController
decryptMessage: this.decryptMessageController.decryptMessage.bind(
this.decryptMessageController,

View File

@ -6,6 +6,7 @@ import { getEnvironmentType } from '../lib/util';
import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../shared/constants/app';
import { TransactionStatus } from '../../../shared/constants/transaction';
import { getURLHostName } from '../../../ui/helpers/utils/util';
import { t } from '../translate';
export default class ExtensionPlatform {
//
@ -181,22 +182,30 @@ export default class ExtensionPlatform {
toLower(getURLHostName(url).replace(/([.]\w+)$/u, '')),
);
const title = 'Confirmed transaction';
const message = `Transaction ${nonce} confirmed! ${
url.length ? `View on ${view}` : ''
}`;
const title = t('notificationTransactionSuccessTitle');
let message = t('notificationTransactionSuccessMessage', nonce);
if (url.length) {
message += ` ${t('notificationTransactionSuccessView', view)}`;
}
await this._showNotification(title, message, url);
}
async _showFailedTransaction(txMeta, errorMessage) {
const nonce = parseInt(txMeta.txParams.nonce, 16);
const title = 'Failed transaction';
let message = `Transaction ${nonce} failed! ${
errorMessage || txMeta.err.message
}`;
const title = t('notificationTransactionFailedTitle');
let message = t(
'notificationTransactionFailedMessage',
nonce,
errorMessage || txMeta.err.message,
);
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
if (isNaN(nonce)) {
message = `Transaction failed! ${errorMessage || txMeta.err.message}`;
message = t(
'notificationTransactionFailedMessageMMI',
errorMessage || txMeta.err.message,
);
}
///: END:ONLY_INCLUDE_IN
await this._showNotification(title, message);

View File

@ -0,0 +1,125 @@
import {
getMessage,
fetchLocale,
FALLBACK_LOCALE,
} from '../../shared/modules/i18n';
import { t, updateCurrentLocale } from './translate';
const localeCodeMock = 'te';
const keyMock = 'testKey';
const substitutionsMock = ['a1', 'b2'];
const messageMock = 'testMessage';
const messageMock2 = 'testMessage2';
const alternateLocaleDataMock = { [keyMock]: { message: messageMock2 } };
jest.mock('../../shared/modules/i18n');
jest.mock('../_locales/en/messages.json', () => ({
[keyMock]: { message: messageMock },
}));
describe('Translate', () => {
const getMessageMock = getMessage as jest.MockedFunction<typeof getMessage>;
const fetchLocaleMock = fetchLocale as jest.MockedFunction<
typeof fetchLocale
>;
beforeEach(async () => {
jest.resetAllMocks();
await updateCurrentLocale(FALLBACK_LOCALE);
});
describe('updateCurrentLocale', () => {
it('retrieves locale data from shared module', async () => {
await updateCurrentLocale(localeCodeMock);
expect(fetchLocale).toHaveBeenCalledTimes(1);
expect(fetchLocale).toHaveBeenCalledWith(localeCodeMock);
});
it('does not retrieve locale data if same locale already set', async () => {
await updateCurrentLocale(localeCodeMock);
await updateCurrentLocale(localeCodeMock);
expect(fetchLocale).toHaveBeenCalledTimes(1);
expect(fetchLocale).toHaveBeenCalledWith(localeCodeMock);
});
it('does not retrieve locale data if fallback locale set', async () => {
await updateCurrentLocale(localeCodeMock);
await updateCurrentLocale(FALLBACK_LOCALE);
expect(fetchLocale).toHaveBeenCalledTimes(1);
expect(fetchLocale).toHaveBeenCalledWith(localeCodeMock);
});
});
describe('t', () => {
it('returns value from shared module', () => {
getMessageMock.mockReturnValue(messageMock);
expect(t(keyMock, ...substitutionsMock)).toStrictEqual(messageMock);
});
it('uses en locale by default', () => {
getMessageMock.mockReturnValue(messageMock);
t(keyMock, ...substitutionsMock);
expect(getMessage).toHaveBeenCalledTimes(1);
expect(getMessage).toHaveBeenCalledWith(
FALLBACK_LOCALE,
{ [keyMock]: { message: messageMock } },
keyMock,
substitutionsMock,
);
});
it('uses locale passed to updateCurrentLocale if called', async () => {
(getMessage as jest.MockedFunction<typeof getMessage>).mockReturnValue(
messageMock,
);
fetchLocaleMock.mockResolvedValueOnce(alternateLocaleDataMock);
await updateCurrentLocale(localeCodeMock);
t(keyMock, ...substitutionsMock);
expect(getMessage).toHaveBeenCalledTimes(1);
expect(getMessage).toHaveBeenCalledWith(
localeCodeMock,
alternateLocaleDataMock,
keyMock,
substitutionsMock,
);
});
it('returns value from en locale as fallback if current locale returns null', async () => {
(
getMessage as jest.MockedFunction<typeof getMessage>
).mockReturnValueOnce(null);
(
getMessage as jest.MockedFunction<typeof getMessage>
).mockReturnValueOnce(messageMock2);
fetchLocaleMock.mockResolvedValueOnce(alternateLocaleDataMock);
await updateCurrentLocale(localeCodeMock);
expect(t(keyMock, ...substitutionsMock)).toStrictEqual(messageMock2);
expect(getMessage).toHaveBeenCalledTimes(2);
expect(getMessage).toHaveBeenCalledWith(
FALLBACK_LOCALE,
{ [keyMock]: { message: messageMock } },
keyMock,
substitutionsMock,
);
expect(getMessage).toHaveBeenCalledWith(
localeCodeMock,
alternateLocaleDataMock,
keyMock,
substitutionsMock,
);
});
});
});

31
app/scripts/translate.ts Normal file
View File

@ -0,0 +1,31 @@
import enTranslations from '../_locales/en/messages.json';
import {
FALLBACK_LOCALE,
I18NMessageDict,
fetchLocale,
getMessage,
} from '../../shared/modules/i18n';
let currentLocale: string = FALLBACK_LOCALE;
let translations: I18NMessageDict = enTranslations;
export async function updateCurrentLocale(locale: string): Promise<void> {
if (currentLocale === locale) {
return;
}
if (locale === FALLBACK_LOCALE) {
translations = enTranslations;
} else {
translations = await fetchLocale(locale);
}
currentLocale = locale;
}
export function t(key: string, ...substitutions: string[]): string | null {
return (
getMessage(currentLocale, translations, key, substitutions) ||
getMessage(FALLBACK_LOCALE, enTranslations, key, substitutions)
);
}

View File

@ -11,6 +11,7 @@ default: &default main
# Declaration of build types
# Each build type is composed of features, env variables and assets.
# Also known as productFlavors in Android lingo
# Note: These build types should be kept in sync with the list in `.github/workflows/update-lavamoat-policies.yml`
buildTypes:
main:
features:

View File

@ -126,6 +126,7 @@ async function defineAndRunBuildTasks() {
'Promise',
'JSON',
'Date',
'Proxy',
// globals sentry needs to function
'__SENTRY__',
'appState',

View File

@ -0,0 +1,27 @@
#!/bin/bash
FORCE_NEXT_SEMVER_VERSION=$1
# If FORCE_NEXT_SEMVER_VERSION is defined and not empty, use its value and skip the next operations
if [ -n "$FORCE_NEXT_SEMVER_VERSION" ]
then
echo "NEXT_SEMVER_VERSION=${FORCE_NEXT_SEMVER_VERSION}" >> "$GITHUB_ENV"
exit 0
fi
# Get the highest version from release branches
VERSION_BRANCHES=$(git branch -r | grep -o 'Version-v[0-9]*\.[0-9]*\.[0-9]*' | grep -o '[0-9]*\.[0-9]*\.[0-9]*' | sort --version-sort | tail -n 1)
# Get the highest version from tags
VERSION_TAGS=$(git tag | grep -o 'v[0-9]*\.[0-9]*\.[0-9]*' | grep -o '[0-9]*\.[0-9]*\.[0-9]*' | sort --version-sort | tail -n 1)
# Get the version from package.json
VERSION_PACKAGE=$(node -p "require('./package.json').version")
# Compare versions and keep the highest one
HIGHEST_VERSION=$(printf "%s\n%s\n%s" "$VERSION_BRANCHES" "$VERSION_TAGS" "$VERSION_PACKAGE" | sort --version-sort | tail -n 1)
# Increment the minor version of the highest version found
NEXT_VERSION=$(echo "$HIGHEST_VERSION" | awk -F. -v OFS=. '{$2++; print}')
echo "NEXT_SEMVER_VERSION=${NEXT_VERSION}" >> "$GITHUB_ENV"

View File

@ -1,317 +0,0 @@
{
"appState": {
"menuOpen": false,
"modal": {
"open": false,
"modalState": {
"name": null,
"props": {}
},
"previousModalState": {
"name": null
}
},
"sidebar": {
"isOpen": false,
"transitionName": "",
"type": ""
},
"alertOpen": false,
"alertMessage": null,
"qrCodeData": null,
"networkDropdownOpen": false,
"currentView": {
"name": "confTx",
"context": 0
},
"accountDetail": {},
"transForward": false,
"isLoading": false,
"warning": null,
"buyView": {},
"isMouseUser": true,
"gasIsLoading": false,
"networkNonce": "0x92",
"defaultHdPaths": {
"trezor": "m/44'/60'/0'/0",
"ledger": "m/44'/60'/0'/0/0",
"lattice": "m/44'/60'/0'/0"
}
},
"confirmTransaction": {
"txData": {
"estimatedGas": "0x38f53",
"gasLimitSpecified": true,
"gasPriceSpecified": false,
"history": [],
"id": 2389644572638774,
"loadingDefaults": false,
"metamaskNetworkId": "4",
"origin": "remix.ethereum.org",
"status": "unapproved",
"time": 1538844223352,
"txParams": {
"data": "0x608060405234801561001057600080fd5b506102a7806100206000396000f30060806040526004361061004b5763ffffffff7c0100000000000000000000000000000000000000000000000000000000600035041663d13319c48114610050578063dfb29935146100da575b600080fd5b34801561005c57600080fd5b50610065610135565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561009f578181015183820152602001610087565b50505050905090810190601f1680156100cc5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b3480156100e657600080fd5b506040805160206004803580820135601f81018490048402850184019095528484526101339436949293602493928401919081908401838280828437509497506101cc9650505050505050565b005b60008054604080516020601f60026000196101006001881615020190951694909404938401819004810282018101909252828152606093909290918301828280156101c15780601f10610196576101008083540402835291602001916101c1565b820191906000526020600020905b8154815290600101906020018083116101a457829003601f168201915b505050505090505b90565b80516101df9060009060208401906101e3565b5050565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061022457805160ff1916838001178555610251565b82800160010185558215610251579182015b82811115610251578251825591602001919060010190610236565b5061025d929150610261565b5090565b6101c991905b8082111561025d57600081556001016102675600a165627a7a72305820cf4282c534b8f2faad947d592afa109b907e4e6b2f52335b361b69c24fedb9580029",
"from": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2",
"gas": "0x38f53",
"gasPrice": "0x3b9aca00",
"value": "0x0"
},
"type": "standard"
},
"tokenData": {},
"methodData": {},
"tokenProps": {
"tokenDecimals": "",
"tokenSymbol": ""
},
"fiatTransactionAmount": "0",
"fiatTransactionFee": "0.05",
"fiatTransactionTotal": "0.05",
"ethTransactionAmount": "0",
"ethTransactionFee": "0.000233",
"ethTransactionTotal": "0.000233",
"hexGasTotal": "0xd42f28057e00",
"nonce": "",
"toSmartContract": false,
"fetchingData": false
},
"localeMessages": {},
"metamask": {
"isInitialized": true,
"isUnlocked": true,
"isAccountMenuOpen": false,
"isPopup": false,
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {
"0x8cf82b5aa41ff2282427be151dd328568684007a": {
"address": "0x8cf82b5aa41ff2282427be151dd328568684007a",
"name": "Account 3"
},
"0xbe1a00e10ec68b154adb84e8119167146a71c9a2": {
"address": "0xbe1a00e10ec68b154adb84e8119167146a71c9a2",
"name": "Account 2"
},
"0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2": {
"address": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2",
"name": "Account 1"
}
},
"unapprovedTxs": {
"2389644572638771": {
"estimatedGas": "0x8544",
"gasLimitSpecified": true,
"gasPriceSpecified": true,
"history": [],
"id": 2389644572638771,
"loadingDefaults": false,
"metamaskNetworkId": "4",
"origin": "MetaMask",
"status": "unapproved",
"time": 1538844175144,
"txParams": {
"data": "0xa9059cbb000000000000000000000000be1a00e10ec68b154adb84e8119167146a71c9a20000000000000000000000000000000000000000000000000000000000000000",
"from": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2",
"gas": "0x8544",
"gasPrice": "0x3b9aca00",
"to": "0xe0b7927c4af23765cb51314a0e0521a9645f0e2a",
"value": "0x0"
},
"type": "standard"
},
"2389644572638772": {
"estimatedGas": "0x5208",
"gasLimitSpecified": true,
"gasPriceSpecified": true,
"history": [],
"id": 2389644572638772,
"loadingDefaults": false,
"metamaskNetworkId": "4",
"origin": "MetaMask",
"status": "unapproved",
"time": 1538844178492,
"txParams": {
"from": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2",
"gas": "0x5208",
"gasPrice": "0x3b9aca00",
"to": "0xbe1a00e10ec68b154adb84e8119167146a71c9a2",
"value": "0x0"
},
"type": "standard"
},
"2389644572638773": {
"estimatedGas": {
"length": 1,
"negative": 0,
"red": null,
"words": [34061, null]
},
"gasLimitSpecified": false,
"gasPriceSpecified": true,
"history": [],
"id": 2389644572638773,
"loadingDefaults": false,
"metamaskNetworkId": "4",
"origin": "localhost",
"status": "unapproved",
"time": 1538844204724,
"txParams": {
"data": "0xdfb29935000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000155468697320697320746865206970667320686173680000000000000000000000",
"from": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2",
"gas": "0xc793",
"gasPrice": "0x3b9aca00",
"to": "0xb7ec370c889b3b48ec537e0b2c887faedceb254a",
"value": "0x0"
},
"type": "standard"
},
"2389644572638774": {
"estimatedGas": "0x38f53",
"gasLimitSpecified": true,
"gasPriceSpecified": false,
"history": [],
"id": 2389644572638774,
"loadingDefaults": false,
"metamaskNetworkId": "4",
"origin": "remix.ethereum.org",
"status": "unapproved",
"time": 1538844223352,
"txParams": {
"data": "0x608060405234801561001057600080fd5b506102a7806100206000396000f30060806040526004361061004b5763ffffffff7c0100000000000000000000000000000000000000000000000000000000600035041663d13319c48114610050578063dfb29935146100da575b600080fd5b34801561005c57600080fd5b50610065610135565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561009f578181015183820152602001610087565b50505050905090810190601f1680156100cc5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b3480156100e657600080fd5b506040805160206004803580820135601f81018490048402850184019095528484526101339436949293602493928401919081908401838280828437509497506101cc9650505050505050565b005b60008054604080516020601f60026000196101006001881615020190951694909404938401819004810282018101909252828152606093909290918301828280156101c15780601f10610196576101008083540402835291602001916101c1565b820191906000526020600020905b8154815290600101906020018083116101a457829003601f168201915b505050505090505b90565b80516101df9060009060208401906101e3565b5050565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061022457805160ff1916838001178555610251565b82800160010185558215610251579182015b82811115610251578251825591602001919060010190610236565b5061025d929150610261565b5090565b6101c991905b8082111561025d57600081556001016102675600a165627a7a72305820cf4282c534b8f2faad947d592afa109b907e4e6b2f52335b361b69c24fedb9580029",
"from": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2",
"gas": "0x38f53",
"gasPrice": "0x3b9aca00",
"value": "0x0"
},
"type": "standard"
}
},
"noActiveNotices": true,
"frequentRpcList": [],
"addressBook": [],
"selectedTokenAddress": null,
"contractExchangeRates": {},
"tokenExchangeRates": {},
"tokens": [
{
"address": "0xe0b7927c4af23765cb51314a0e0521a9645f0e2a",
"decimals": 9,
"symbol": "DGD"
}
],
"pendingTokens": {},
"send": {
"gasLimit": null,
"gasPrice": null,
"gasTotal": null,
"tokenBalance": null,
"from": "",
"to": "",
"amount": "0x0",
"memo": "",
"errors": {},
"editingTransactionId": null,
"forceGasMin": null
},
"coinOptions": {},
"useBlockie": false,
"featureFlags": {
"betaUI": true,
"skipAnnounceBetaUI": true
},
"isRevealingSeedWords": false,
"welcomeScreenSeen": false,
"currentLocale": "en",
"preferences": {
"useETHAsPrimaryCurrency": true
},
"providerConfig": {
"type": "goerli"
},
"network": "4",
"accounts": {
"0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2": {
"address": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2",
"balance": "0x36aabfb2a0190c00"
},
"0xbe1a00e10ec68b154adb84e8119167146a71c9a2": {
"address": "0xbe1a00e10ec68b154adb84e8119167146a71c9a2",
"balance": "0x7b3ef08c294a000"
},
"0x8cf82b5aa41ff2282427be151dd328568684007a": {
"address": "0x8cf82b5aa41ff2282427be151dd328568684007a",
"balance": "0x0"
}
},
"currentBlockGasLimit": "0x731e25",
"selectedAddressTxList": [],
"unapprovedMsgs": {},
"unapprovedMsgCount": 0,
"unapprovedPersonalMsgs": {},
"unapprovedPersonalMsgCount": 0,
"unapprovedTypedMessages": {},
"unapprovedTypedMessagesCount": 0,
"keyringTypes": [
"Simple Key Pair",
"HD Key Tree",
"Trezor Hardware",
"Ledger Hardware",
"Lattice Hardware"
],
"keyrings": [
{
"type": "HD Key Tree",
"accounts": [
"0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2",
"0xbe1a00e10ec68b154adb84e8119167146a71c9a2",
"0x8cf82b5aa41ff2282427be151dd328568684007a"
]
}
],
"currentAccountTab": "history",
"accountTokens": {
"0x8cf82b5aa41ff2282427be151dd328568684007a": {},
"0xbe1a00e10ec68b154adb84e8119167146a71c9a2": {},
"0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2": {
"goerli": [
{
"address": "0xe0b7927c4af23765cb51314a0e0521a9645f0e2a",
"decimals": 9,
"symbol": "DGD"
},
{
"address": "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359",
"decimals": 18,
"symbol": "DAI"
}
]
}
},
"assetImages": {
"0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359": null,
"0xe0b7927c4af23765cb51314a0e0521a9645f0e2a": null
},
"suggestedTokens": {},
"lostIdentities": {},
"seedWords": null,
"forgottenPassword": false,
"selectedAddress": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2",
"recentBlocks": [],
"currentCurrency": "usd",
"conversionRate": 225.23,
"conversionDate": 1538859376,
"shapeShiftTxList": [],
"infuraNetworkStatus": {
"mainnet": "ok",
"goerli": "ok",
"sepolia": "ok",
"lineaGoerli": "ok",
"lineaMainnet": "ok"
}
},
"send": {
"toDropdownOpen": false,
"errors": {},
"warnings": {}
}
}

View File

@ -183,10 +183,13 @@ async function verifyEnglishLocale() {
[
'ui/**/*.js',
'ui/**/*.ts',
'ui/**/*.tsx',
'shared/**/*.js',
'shared/**/*.ts',
'shared/**/*.tsx',
'app/scripts/constants/**/*.js',
'app/scripts/constants/**/*.ts',
'app/scripts/platforms/**/*.js',
],
{
ignore: [...globsToStrictSearch, testGlob],

View File

@ -51,6 +51,7 @@ module.exports = {
'<rootDir>/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js',
'<rootDir>/app/scripts/migrations/*.test.(js|ts)',
'<rootDir>/app/scripts/platforms/*.test.js',
'<rootDir>/app/scripts/translate.test.ts',
'<rootDir>/shared/**/*.test.(js|ts)',
'<rootDir>/ui/**/*.test.(js|ts|tsx)',
'<rootDir>/development/fitness-functions/**/*.test.(js|ts|tsx)',

View File

@ -1718,7 +1718,8 @@
"browserify>buffer": true,
"browserify>events": true,
"eth-rpc-errors": true,
"ethereumjs-util": true
"ethereumjs-util": true,
"lodash": true
}
},
"@metamask/smart-transactions-controller": {

View File

@ -1908,7 +1908,8 @@
"browserify>buffer": true,
"browserify>events": true,
"eth-rpc-errors": true,
"ethereumjs-util": true
"ethereumjs-util": true,
"lodash": true
}
},
"@metamask/smart-transactions-controller": {

View File

@ -1908,7 +1908,8 @@
"browserify>buffer": true,
"browserify>events": true,
"eth-rpc-errors": true,
"ethereumjs-util": true
"ethereumjs-util": true,
"lodash": true
}
},
"@metamask/smart-transactions-controller": {

View File

@ -1718,7 +1718,8 @@
"browserify>buffer": true,
"browserify>events": true,
"eth-rpc-errors": true,
"ethereumjs-util": true
"ethereumjs-util": true,
"lodash": true
}
},
"@metamask/smart-transactions-controller": {

View File

@ -1939,7 +1939,8 @@
"browserify>buffer": true,
"browserify>events": true,
"eth-rpc-errors": true,
"ethereumjs-util": true
"ethereumjs-util": true,
"lodash": true
}
},
"@metamask/smart-transactions-controller": {

View File

@ -980,7 +980,6 @@
"packages": {
"@lavamoat/allow-scripts>@npmcli/run-script>node-gyp>npmlog>are-we-there-yet": true,
"@lavamoat/allow-scripts>@npmcli/run-script>node-gyp>npmlog>gauge": true,
"@storybook/addon-mdx-gfm>@storybook/node-logger>npmlog>console-control-strings": true,
"@storybook/react>@storybook/node-logger>npmlog>console-control-strings": true,
"nyc>yargs>set-blocking": true
}
@ -1009,9 +1008,6 @@
"@lavamoat/allow-scripts>@npmcli/run-script>node-gyp>npmlog>gauge>aproba": true,
"@lavamoat/allow-scripts>@npmcli/run-script>node-gyp>npmlog>gauge>string-width": true,
"@lavamoat/allow-scripts>@npmcli/run-script>node-gyp>npmlog>gauge>strip-ansi": true,
"@storybook/addon-mdx-gfm>@storybook/node-logger>npmlog>console-control-strings": true,
"@storybook/addon-mdx-gfm>@storybook/node-logger>npmlog>gauge>has-unicode": true,
"@storybook/addon-mdx-gfm>@storybook/node-logger>npmlog>gauge>wide-align": true,
"@storybook/react>@storybook/node-logger>npmlog>console-control-strings": true,
"@storybook/react>@storybook/node-logger>npmlog>gauge>has-unicode": true,
"@storybook/react>@storybook/node-logger>npmlog>gauge>wide-align": true,
@ -1137,33 +1133,11 @@
"@metamask/jazzicon>color>color-convert>color-name": true
}
},
"@sentry/cli>mkdirp": {
"builtin": {
"fs": true,
"path.dirname": true,
"path.resolve": true
}
},
"@storybook/addon-knobs>qs": {
"packages": {
"string.prototype.matchall>side-channel": true
}
},
"@storybook/addon-mdx-gfm>@storybook/node-logger>npmlog>gauge>has-unicode": {
"builtin": {
"os.type": true
},
"globals": {
"process.env.LANG": true,
"process.env.LC_ALL": true,
"process.env.LC_CTYPE": true
}
},
"@storybook/addon-mdx-gfm>@storybook/node-logger>npmlog>gauge>wide-align": {
"packages": {
"yargs>string-width": true
}
},
"@storybook/core>@storybook/core-server>x-default-browser>default-browser-id>untildify>os-homedir": {
"builtin": {
"os.homedir": true
@ -4895,20 +4869,9 @@
},
"packages": {
"@storybook/core>@storybook/core-server>x-default-browser>default-browser-id>untildify>os-homedir": true,
"gulp-watch>chokidar>fsevents>node-pre-gyp>nopt>osenv>os-homedir": true,
"gulp-watch>chokidar>fsevents>node-pre-gyp>nopt>osenv>os-tmpdir": true
}
},
"gulp-watch>chokidar>fsevents>node-pre-gyp>nopt>osenv>os-homedir": {
"builtin": {
"os.homedir": true
},
"globals": {
"process.env": true,
"process.getuid": true,
"process.platform": true
}
},
"gulp-watch>chokidar>fsevents>node-pre-gyp>nopt>osenv>os-tmpdir": {
"globals": {
"process.env.SystemRoot": true,
@ -4930,34 +4893,9 @@
"setTimeout": true
},
"packages": {
"gulp-watch>chokidar>fsevents>node-pre-gyp>rimraf>glob": true,
"nyc>glob": true
}
},
"gulp-watch>chokidar>fsevents>node-pre-gyp>rimraf>glob": {
"builtin": {
"assert": true,
"events.EventEmitter": true,
"fs": true,
"path.join": true,
"path.resolve": true,
"util": true
},
"globals": {
"console.error": true,
"process.cwd": true,
"process.nextTick": true,
"process.platform": true
},
"packages": {
"eslint>minimatch": true,
"gulp-watch>path-is-absolute": true,
"nyc>glob>fs.realpath": true,
"nyc>glob>inflight": true,
"pump>once": true,
"pumpify>inherits": true
}
},
"gulp-watch>chokidar>fsevents>node-pre-gyp>semver": {
"globals": {
"console": true,
@ -8246,7 +8184,14 @@
"path.dirname": true
},
"packages": {
"@sentry/cli>mkdirp": true
"stylelint>file-entry-cache>flat-cache>write>mkdirp": true
}
},
"stylelint>file-entry-cache>flat-cache>write>mkdirp": {
"builtin": {
"fs": true,
"path.dirname": true,
"path.resolve": true
}
},
"stylelint>global-modules": {

View File

@ -96,7 +96,8 @@
"fitness-functions": "ts-node development/fitness-functions/index.ts",
"generate-beta-commit": "node ./development/generate-beta-commit.js",
"validate-branch-name": "validate-branch-name",
"label-prs": "ts-node ./.github/scripts/label-prs.ts"
"label-prs": "ts-node ./.github/scripts/label-prs.ts",
"add-release-label-to-pr-and-linked-issues": "ts-node ./.github/scripts/add-release-label-to-pr-and-linked-issues.ts"
},
"resolutions": {
"@babel/core": "patch:@babel/core@npm%3A7.21.5#./.yarn/patches/@babel-core-npm-7.21.5-c72c337956.patch",
@ -196,12 +197,9 @@
"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.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",
"@metamask/signature-controller@^3.0.0": "patch:@metamask/signature-controller@npm%3A3.0.0#./.yarn/patches/@metamask-signature-controller-npm-3.0.0-8771b6885e.patch"
"request@^2.85.0": "patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch"
},
"dependencies": {
"@actions/core": "^1.10.0",
"@actions/github": "^5.1.1",
"@babel/runtime": "^7.18.9",
"@download/blockies": "^1.0.3",
"@ensdomains/content-hash": "^2.5.6",
@ -229,7 +227,7 @@
"@metamask/address-book-controller": "^3.0.0",
"@metamask/announcement-controller": "^4.0.0",
"@metamask/approval-controller": "^3.1.0",
"@metamask/assets-controllers": "^9.0.0",
"@metamask/assets-controllers": "^9.1.0",
"@metamask/base-controller": "^3.0.0",
"@metamask/browser-passworder": "^4.1.0",
"@metamask/contract-metadata": "^2.3.1",
@ -248,7 +246,7 @@
"@metamask/jazzicon": "^2.0.0",
"@metamask/key-tree": "^7.0.0",
"@metamask/logo": "^3.1.1",
"@metamask/message-manager": "^6.0.0",
"@metamask/message-manager": "^7.0.0",
"@metamask/metamask-eth-abis": "^3.0.0",
"@metamask/notification-controller": "^3.0.0",
"@metamask/obs-store": "^8.1.0",
@ -261,7 +259,7 @@
"@metamask/rpc-methods-flask": "npm:@metamask/rpc-methods@0.34.0-flask.1",
"@metamask/safe-event-emitter": "^2.0.0",
"@metamask/scure-bip39": "^2.0.3",
"@metamask/signature-controller": "^3.0.0",
"@metamask/signature-controller": "^4.0.1",
"@metamask/slip44": "^3.0.0",
"@metamask/smart-transactions-controller": "^3.1.0",
"@metamask/snaps-controllers": "^0.32.2",
@ -317,7 +315,6 @@
"fuse.js": "^3.2.0",
"globalthis": "^1.0.1",
"human-standard-token-abi": "^2.0.0",
"husky": "^8.0.3",
"immer": "^9.0.6",
"is-retry-allowed": "^2.2.0",
"jest-junit": "^14.0.1",
@ -366,6 +363,8 @@
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@actions/core": "^1.10.0",
"@actions/github": "^5.1.1",
"@babel/code-frame": "^7.12.13",
"@babel/core": "^7.21.5",
"@babel/eslint-parser": "^7.13.14",
@ -426,6 +425,7 @@
"@types/react": "^16.9.53",
"@types/react-dom": "^17.0.11",
"@types/react-redux": "^7.1.25",
"@types/react-router-dom": "^5.3.3",
"@types/remote-redux-devtools": "^0.5.5",
"@types/sass": "^1.43.1",
"@types/sinon": "^10.0.13",

View File

@ -3,10 +3,7 @@ import browser from 'webextension-polyfill';
///: END:ONLY_INCLUDE_IN
import { memoize } from 'lodash';
import getFirstPreferredLangCode from '../../app/scripts/lib/get-first-preferred-lang-code';
import {
fetchLocale,
loadRelativeTimeFormatLocaleData,
} from '../../ui/helpers/utils/i18n-helper';
import { fetchLocale, loadRelativeTimeFormatLocaleData } from '../modules/i18n';
///: BEGIN:ONLY_INCLUDE_IN(desktop)
import { renderDesktopError } from '../../ui/pages/desktop-error/render-desktop-error';
import { EXTENSION_ERROR_PAGE_TYPES } from '../constants/desktop';

View File

@ -1,5 +1,5 @@
import browser from 'webextension-polyfill';
import { fetchLocale } from '../../ui/helpers/utils/i18n-helper';
import { fetchLocale } from '../modules/i18n';
import { SUPPORT_LINK } from './ui-utils';
import {
downloadDesktopApp,
@ -12,7 +12,7 @@ import {
} from './error-utils';
import { openCustomProtocol } from './deep-linking';
jest.mock('../../ui/helpers/utils/i18n-helper', () => ({
jest.mock('../modules/i18n', () => ({
fetchLocale: jest.fn(),
loadRelativeTimeFormatLocaleData: jest.fn(),
}));

335
shared/modules/i18n.test.ts Normal file
View File

@ -0,0 +1,335 @@
import log from 'loglevel';
import {
FALLBACK_LOCALE,
I18NMessageDict,
clearCaches,
fetchLocale,
getMessage,
loadRelativeTimeFormatLocaleData,
} from './i18n';
const localeCodeMock = 'te';
const keyMock = 'testKey';
const errorLocaleMock = 'testLocaleError';
const errorMock = 'TestError';
jest.mock('loglevel');
jest.mock('./fetch-with-timeout', () =>
jest.fn(() => (url: string) => {
return Promise.resolve({
json: () => {
if (url.includes(errorLocaleMock)) {
throw new Error(errorMock);
}
return { url };
},
});
}),
);
describe('I18N Module', () => {
beforeEach(() => {
jest.resetAllMocks();
clearCaches();
process.env.IN_TEST = 'true';
});
describe('getMessage', () => {
describe('on error', () => {
it('returns null if no messages', () => {
expect(
getMessage(
localeCodeMock,
null as unknown as I18NMessageDict,
keyMock,
),
).toBeNull();
});
describe('if missing key', () => {
describe('if not using fallback locale', () => {
it('logs warning', () => {
expect(
getMessage(
localeCodeMock,
{} as unknown as I18NMessageDict,
keyMock,
),
).toBeNull();
expect(log.warn).toHaveBeenCalledTimes(1);
expect(log.warn).toHaveBeenCalledWith(
`Translator - Unable to find value of key "${keyMock}" for locale "${localeCodeMock}"`,
);
});
it('does not log warning if warning already created', () => {
expect(
getMessage(
localeCodeMock,
{} as unknown as I18NMessageDict,
keyMock,
),
).toBeNull();
expect(
getMessage(
localeCodeMock,
{} as unknown as I18NMessageDict,
keyMock,
),
).toBeNull();
expect(log.warn).toHaveBeenCalledTimes(1);
expect(log.warn).toHaveBeenCalledWith(
`Translator - Unable to find value of key "${keyMock}" for locale "${localeCodeMock}"`,
);
});
});
describe('if using fallback locale', () => {
it('logs error', () => {
delete process.env.IN_TEST;
expect(
getMessage(
FALLBACK_LOCALE,
{} as unknown as I18NMessageDict,
keyMock,
),
).toBeNull();
expect(log.error).toHaveBeenCalledTimes(1);
expect(log.error).toHaveBeenCalledWith(
new Error(
`Unable to find value of key "${keyMock}" for locale "${FALLBACK_LOCALE}"`,
),
);
});
it('throws if test env set', () => {
expect(() =>
getMessage(
FALLBACK_LOCALE,
{} as unknown as I18NMessageDict,
keyMock,
),
).toThrow(
`Unable to find value of key "${keyMock}" for locale "${FALLBACK_LOCALE}"`,
);
});
it('calls onError callback', () => {
const onErrorMock = jest.fn();
try {
getMessage(
FALLBACK_LOCALE,
{} as unknown as I18NMessageDict,
keyMock,
[],
onErrorMock,
);
} catch {
// Expected
}
expect(onErrorMock).toHaveBeenCalledTimes(1);
expect(onErrorMock).toHaveBeenCalledWith(
new Error(
`Unable to find value of key "${keyMock}" for locale "${FALLBACK_LOCALE}"`,
),
);
});
it('does nothing if error already created', () => {
const onErrorMock = jest.fn();
try {
getMessage(
FALLBACK_LOCALE,
{} as unknown as I18NMessageDict,
keyMock,
[],
onErrorMock,
);
} catch {
// Expected
}
getMessage(
FALLBACK_LOCALE,
{} as unknown as I18NMessageDict,
keyMock,
[],
onErrorMock,
);
expect(log.error).toHaveBeenCalledTimes(1);
expect(onErrorMock).toHaveBeenCalledTimes(1);
});
});
});
describe('if missing substitution', () => {
it('logs error', () => {
expect(
getMessage(
localeCodeMock,
{ [keyMock]: { message: 'test1 $1 test2 $2 test3' } },
keyMock,
['a1'],
),
).toStrictEqual('test1 a1 test2 test3');
expect(log.error).toHaveBeenCalledTimes(1);
expect(log.error).toHaveBeenCalledWith(
new Error(
`Insufficient number of substitutions for key "${keyMock}" with locale "${localeCodeMock}"`,
),
);
});
it('calls onError callback', () => {
const onErrorMock = jest.fn();
expect(
getMessage(
localeCodeMock,
{ [keyMock]: { message: 'test1 $1 test2 $2 test3' } },
keyMock,
['a1'],
onErrorMock,
),
).toStrictEqual('test1 a1 test2 test3');
expect(onErrorMock).toHaveBeenCalledTimes(1);
expect(onErrorMock).toHaveBeenCalledWith(
new Error(
`Insufficient number of substitutions for key "${keyMock}" with locale "${localeCodeMock}"`,
),
);
});
it('does nothing if error already created', () => {
const onErrorMock = jest.fn();
expect(
getMessage(
localeCodeMock,
{ [keyMock]: { message: 'test1 $1 test2 $2 test3' } },
keyMock,
['a1'],
onErrorMock,
),
).toStrictEqual('test1 a1 test2 test3');
expect(
getMessage(
localeCodeMock,
{ [keyMock]: { message: 'test1 $1 test2 $2 test3' } },
keyMock,
['a1'],
onErrorMock,
),
).toStrictEqual('test1 a1 test2 test3');
expect(log.error).toHaveBeenCalledTimes(1);
expect(onErrorMock).toHaveBeenCalledTimes(1);
});
});
});
it('returns text only if no substitutions', () => {
expect(
getMessage(
localeCodeMock,
{ [keyMock]: { message: 'testValue' } },
keyMock,
),
).toStrictEqual('testValue');
});
it('returns text including substitutions', () => {
expect(
getMessage(
localeCodeMock,
{ [keyMock]: { message: 'test1 $1 test2 $2 test3' } },
keyMock,
['a1', 'b2'],
),
).toStrictEqual('test1 a1 test2 b2 test3');
});
it('returns text including substitutions using custom join', () => {
expect(
getMessage(
localeCodeMock,
{ [keyMock]: { message: 'test1 $1 test2 $2 test3' } },
keyMock,
['a1', 'b2'],
undefined,
(substitutions) => substitutions.join(','),
),
).toStrictEqual('test1 ,a1, test2 ,b2, test3');
});
});
describe('fetchLocale', () => {
it('returns json from locale file', async () => {
const result = await fetchLocale(localeCodeMock);
expect(result).toStrictEqual({
url: `./_locales/${localeCodeMock}/messages.json`,
});
});
it('logs if fetch fails', async () => {
await fetchLocale(errorLocaleMock);
expect(log.error).toHaveBeenCalledTimes(1);
expect(log.error).toHaveBeenCalledWith(
`failed to fetch testLocaleError locale because of Error: ${errorMock}`,
);
});
it('returns empty object if fetch fails', async () => {
expect(await fetchLocale(errorLocaleMock)).toStrictEqual({});
});
});
describe('loadRelativeTimeFormatLocaleData', () => {
it('adds locale data if function exists', async () => {
const addMock = jest.fn();
global.Intl = {
RelativeTimeFormat: {
__addLocaleData: addMock,
},
} as any;
await loadRelativeTimeFormatLocaleData(`${localeCodeMock}_test`);
expect(addMock).toHaveBeenCalledTimes(1);
expect(addMock).toHaveBeenCalledWith({
url: `./intl/${localeCodeMock}/relative-time-format-data.json`,
});
});
it('does not add locale data if language tag already processed', async () => {
const addMock = jest.fn();
global.Intl = {
RelativeTimeFormat: {
__addLocaleData: addMock,
},
} as any;
await loadRelativeTimeFormatLocaleData(`${localeCodeMock}_test`);
await loadRelativeTimeFormatLocaleData(`${localeCodeMock}_test`);
expect(addMock).toHaveBeenCalledTimes(1);
});
});
});

223
shared/modules/i18n.ts Normal file
View File

@ -0,0 +1,223 @@
import log from 'loglevel';
import { Json } from '@metamask/utils';
import getFetchWithTimeout from './fetch-with-timeout';
const fetchWithTimeout = getFetchWithTimeout();
// From app/_locales folders there is a messages.json file such as app/_locales/en, comes with key and translated results
// and we use as t('reject') to get the translated message in the codebase
// and in i18n lib, the translated message is an object (I18NMessage) with message & description -
// message is the string that will replace the translationKey, and that message may contain replacement variables such as $1, $2, etc.
// Description is key describing the usage of the message.
export interface I18NMessage {
message: string;
description?: string;
}
// The overall translation file is made of same entries
// translationKey (string) and the I18NMessage as the value.
export interface I18NMessageDict {
[translationKey: string]: I18NMessage;
}
export type I18NSubstitution = string | (() => any) | object;
// A parameterized type (or generic type) of maps that use the same structure (translationKey) key
interface I18NMessageDictMap<R> {
[translationKey: string]: R;
}
export const FALLBACK_LOCALE = 'en';
const warned: { [localeCode: string]: I18NMessageDictMap<boolean> } = {};
const missingMessageErrors: I18NMessageDictMap<Error> = {};
const missingSubstitutionErrors: {
[localeCode: string]: I18NMessageDictMap<boolean>;
} = {};
const relativeTimeFormatLocaleData = new Set();
/**
* Returns a localized message for the given key
*
* @param localeCode - The code for the current locale
* @param localeMessages - The map of messages for the current locale
* @param key - The message key
* @param substitutions - A list of message substitution replacements can replace $n in given message
* @param onError - An optional callback to provide additional processing on any errors
* @param join - An optional callback to join the substituted parts using custom logic
* @returns The localized message
*/
export const getMessage = <T>(
localeCode: string,
localeMessages: I18NMessageDict,
key: string,
substitutions?: I18NSubstitution[],
onError?: (error: Error) => void,
join?: (substitutedParts: I18NSubstitution[]) => T,
): T | string | null => {
if (!localeMessages) {
return null;
}
const message = localeMessages[key];
if (!message) {
missingKeyError(key, localeCode, onError);
return null;
}
const text = message.message;
const parts = hasSubstitutions(substitutions)
? applySubstitutions(
text,
substitutions as I18NSubstitution[],
key,
localeCode,
onError,
)
: [text];
return join ? join(parts) : parts.join('');
};
export async function fetchLocale(
localeCode: string,
): Promise<I18NMessageDict> {
try {
const response = await fetchWithTimeout(
`./_locales/${localeCode}/messages.json`,
);
return await response.json();
} catch (error) {
log.error(`failed to fetch ${localeCode} locale because of ${error}`);
return {};
}
}
export async function loadRelativeTimeFormatLocaleData(
localeCode: string,
): Promise<void> {
const languageTag = localeCode.split('_')[0];
if (
Intl.RelativeTimeFormat &&
typeof (Intl.RelativeTimeFormat as any).__addLocaleData === 'function' &&
!relativeTimeFormatLocaleData.has(languageTag)
) {
const localeData = await fetchRelativeTimeFormatData(languageTag);
(Intl.RelativeTimeFormat as any).__addLocaleData(localeData);
relativeTimeFormatLocaleData.add(languageTag);
}
}
export function clearCaches() {
Object.keys(warned).forEach((key) => {
delete warned[key];
});
Object.keys(missingMessageErrors).forEach((key) => {
delete missingMessageErrors[key];
});
Object.keys(missingSubstitutionErrors).forEach((key) => {
delete missingSubstitutionErrors[key];
});
relativeTimeFormatLocaleData.clear();
}
function applySubstitutions(
message: string,
substitutions: I18NSubstitution[],
key: string,
localeCode: string,
onError?: (error: Error) => void,
): I18NSubstitution[] {
const parts = message.split(/(\$\d)/gu);
return parts.map((part: string) => {
const subMatch = part.match(/\$(\d)/u);
if (!subMatch) {
return part;
}
const substituteIndex = Number(subMatch[1]) - 1;
const substitution = substitutions[substituteIndex];
if (substitution === null || substitution === undefined) {
missingSubstitutionError(key, localeCode, onError);
}
return substitutions?.[substituteIndex];
});
}
function missingKeyError(
key: string,
localeCode: string,
onError?: (error: Error) => void,
) {
if (localeCode === FALLBACK_LOCALE && !missingMessageErrors[key]) {
const error = new Error(
`Unable to find value of key "${key}" for locale "${localeCode}"`,
);
missingMessageErrors[key] = error;
onError?.(error);
log.error(error);
if (process.env.IN_TEST) {
throw error;
}
}
if (localeCode === FALLBACK_LOCALE || warned[localeCode]?.[key]) {
return;
}
warned[localeCode] = warned[localeCode] ?? {};
warned[localeCode][key] = true;
log.warn(
`Translator - Unable to find value of key "${key}" for locale "${localeCode}"`,
);
}
function missingSubstitutionError(
key: string,
localeCode: string,
onError?: (error: Error) => void,
) {
if (missingSubstitutionErrors[localeCode]?.[key]) {
return;
}
missingSubstitutionErrors[localeCode] =
missingSubstitutionErrors[localeCode] ?? {};
missingSubstitutionErrors[localeCode][key] = true;
const error = new Error(
`Insufficient number of substitutions for key "${key}" with locale "${localeCode}"`,
);
log.error(error);
onError?.(error);
}
function hasSubstitutions(substitutions?: I18NSubstitution[]) {
return (substitutions?.length ?? 0) > 0;
}
async function fetchRelativeTimeFormatData(languageTag: string): Promise<Json> {
const response = await fetchWithTimeout(
`./intl/${languageTag}/relative-time-format-data.json`,
);
return await response.json();
}

18
sonar-project.properties Normal file
View File

@ -0,0 +1,18 @@
sonar.projectKey=metamask-extension
sonar.organization=consensys
# This is the name and version displayed in the SonarCloud UI.
sonar.projectName=MetaMask Extension
#sonar.projectVersion=1.0
# Root for sonar analysis.
sonar.sources=app/
# Excluded project files from analysis.
#sonar.exclusions=
# Inclusions for test files.
sonar.test.inclusions=**.test.**
# Encoding of the source code. Default is default system encoding
sonar.sourceEncoding=UTF-8

View File

@ -4,6 +4,7 @@ const { promises: fs } = require('fs');
const BigNumber = require('bignumber.js');
const mockttp = require('mockttp');
const createStaticServer = require('../../development/create-static-server');
const { tEn } = require('../lib/i18n-helpers');
const { setupMocking } = require('./mock-e2e');
const Ganache = require('./ganache');
const FixtureServer = require('./fixture-server');
@ -384,6 +385,56 @@ const testSRPDropdownIterations = async (options, driver, iterations) => {
}
};
const passwordUnlockOpenSRPRevealQuiz = async (driver) => {
await driver.navigate();
await driver.fill('#password', 'correct horse battery staple');
await driver.press('#password', driver.Key.ENTER);
// navigate settings to reveal SRP
await driver.clickElement('[data-testid="account-options-menu-button"]');
await driver.clickElement({ text: 'Settings', tag: 'div' });
await driver.clickElement({ text: 'Security & privacy', tag: 'div' });
await driver.clickElement('[data-testid="reveal-seed-words"]');
};
const completeSRPRevealQuiz = async (driver) => {
// start quiz
await driver.clickElement('[data-testid="srp-quiz-get-started"]');
// tap correct answer 1
await driver.clickElement('[data-testid="srp-quiz-right-answer"]');
// tap Continue 1
await driver.clickElement('[data-testid="srp-quiz-continue"]');
// tap correct answer 2
await driver.clickElement('[data-testid="srp-quiz-right-answer"]');
// tap Continue 2
await driver.clickElement('[data-testid="srp-quiz-continue"]');
};
const tapAndHoldToRevealSRP = async (driver) => {
await driver.holdMouseDownOnElement(
{
text: tEn('holdToRevealSRP'),
tag: 'span',
},
2000,
);
};
const closeSRPReveal = async (driver) => {
await driver.clickElement({
text: tEn('close'),
tag: 'button',
});
await driver.findVisibleElement({
text: tEn('tokens'),
tag: 'button',
});
};
const DAPP_URL = 'http://127.0.0.1:8080';
const DAPP_ONE_URL = 'http://127.0.0.1:8081';
@ -500,10 +551,12 @@ const findAnotherAccountFromAccountList = async (
) => {
await driver.clickElement('[data-testid="account-menu-icon"]');
const accountMenuItemSelector = `.multichain-account-list-item:nth-child(${itemNumber})`;
const acctName = await driver.findElement(
`${accountMenuItemSelector} .multichain-account-list-item__account-name__button`,
);
assert.equal(await acctName.getText(), accountName);
await driver.findElement({
css: `${accountMenuItemSelector} .multichain-account-list-item__account-name__button`,
text: accountName,
});
return accountMenuItemSelector;
};
@ -530,32 +583,97 @@ const locateAccountBalanceDOM = async (driver, ganacheServer) => {
text: `${balance} ETH`,
});
};
const DEFAULT_PRIVATE_KEY =
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC';
const WALLET_PASSWORD = 'correct horse battery staple';
const restartServiceWorker = async (driver) => {
const serviceWorkerElements = await driver.findElements({
text: 'terminate',
tag: 'span',
});
// 1st one is app-init.js; while 2nd one is service-worker.js
await serviceWorkerElements[1].click();
const DEFAULT_GANACHE_OPTIONS = {
accounts: [
{
secretKey: DEFAULT_PRIVATE_KEY,
balance: generateETHBalance(25),
},
],
};
const generateGanacheOptions = (overrides) => ({
...DEFAULT_GANACHE_OPTIONS,
...overrides,
});
async function waitForAccountRendered(driver) {
await driver.waitForSelector(
'[data-testid="eth-overview__primary-currency"]',
);
}
const WINDOW_TITLES = Object.freeze({
ExtensionInFullScreenView: 'MetaMask',
TestDApp: 'E2E Test Dapp',
Notification: 'MetaMask Notification',
ServiceWorkerSettings: 'Inspect with Chrome Developer Tools',
InstalledExtensions: 'Extensions',
});
const login = async (driver) => {
const unlockWallet = async (driver) => {
await driver.fill('#password', 'correct horse battery staple');
await driver.press('#password', driver.Key.ENTER);
};
const logInWithBalanceValidation = async (driver, ganacheServer) => {
await login(driver);
await unlockWallet(driver);
await assertAccountBalanceForDOM(driver, ganacheServer);
};
function roundToXDecimalPlaces(number, decimalPlaces) {
return Math.round(number * 10 ** decimalPlaces) / 10 ** decimalPlaces;
}
function generateRandNumBetween(x, y) {
const min = Math.min(x, y);
const max = Math.max(x, y);
const randomNumber = Math.random() * (max - min) + min;
return randomNumber;
}
async function switchToWindow(driver, windowTitle) {
const windowHandles = await driver.getAllWindowHandles();
return await driver.switchToWindowWithTitle(windowTitle, windowHandles);
}
async function sleepSeconds(sec) {
return new Promise((resolve) => setTimeout(resolve, sec * 1000));
}
async function terminateServiceWorker(driver) {
await driver.openNewPage(SERVICE_WORKER_URL);
await driver.waitForSelector({
text: 'Service workers',
tag: 'button',
});
await driver.clickElement({
text: 'Service workers',
tag: 'button',
});
const serviceWorkerElements = await driver.findElements({
text: 'terminate',
tag: 'span',
});
// 1st one is app-init.js; while 2nd one is service-worker.js
await serviceWorkerElements[serviceWorkerElements.length - 1].click();
const serviceWorkerTab = await switchToWindow(
driver,
WINDOW_TITLES.ServiceWorkerSettings,
);
await driver.closeWindowHandle(serviceWorkerTab);
}
module.exports = {
DAPP_URL,
DAPP_ONE_URL,
@ -574,6 +692,10 @@ module.exports = {
completeImportSRPOnboardingFlow,
completeImportSRPOnboardingFlowWordByWord,
completeCreateNewWalletOnboardingFlow,
passwordUnlockOpenSRPRevealQuiz,
completeSRPRevealQuiz,
closeSRPReveal,
tapAndHoldToRevealSRP,
createDownloadFolder,
importWrongSRPOnboardingFlow,
testSRPDropdownIterations,
@ -583,10 +705,19 @@ module.exports = {
defaultGanacheOptions,
sendTransaction,
findAnotherAccountFromAccountList,
login,
unlockWallet,
logInWithBalanceValidation,
assertAccountBalanceForDOM,
locateAccountBalanceDOM,
restartServiceWorker,
waitForAccountRendered,
generateGanacheOptions,
WALLET_PASSWORD,
WINDOW_TITLES,
DEFAULT_GANACHE_OPTIONS,
generateETHBalance,
roundToXDecimalPlaces,
generateRandNumBetween,
switchToWindow,
sleepSeconds,
terminateServiceWorker,
};

View File

@ -0,0 +1,427 @@
const { strict: assert } = require('assert');
const {
withFixtures,
openDapp,
generateGanacheOptions,
WALLET_PASSWORD,
WINDOW_TITLES,
DEFAULT_GANACHE_OPTIONS,
generateETHBalance,
roundToXDecimalPlaces,
generateRandNumBetween,
switchToWindow,
sleepSeconds,
terminateServiceWorker,
unlockWallet,
} = require('../helpers');
const FixtureBuilder = require('../fixture-builder');
describe('MV3 - Restart service worker multiple times', function () {
it('Simple simple send flow within full screen view should still be usable', async function () {
const initialBalance = roundToXDecimalPlaces(
generateRandNumBetween(10, 100),
4,
);
await withFixtures(
{
fixtures: new FixtureBuilder().build(),
ganacheOptions: generateGanacheOptions({
accounts: [
{
secretKey: DEFAULT_GANACHE_OPTIONS.accounts[0].secretKey,
balance: generateETHBalance(initialBalance),
},
],
}),
title: this.test.title,
driverOptions: { openDevToolsForTabs: true },
},
async ({ driver }) => {
await driver.navigate();
await unlockWallet(driver, WALLET_PASSWORD);
await assertETHBalance(driver, initialBalance);
// first send ETH and then terminate SW
const RECIPIENT_ADDRESS = '0x985c30949c92df7a0bd42e0f3e3d539ece98db24';
const amountFirstTx = roundToXDecimalPlaces(
generateRandNumBetween(0.5, 2),
4,
);
const gasFeesFirstTx = await simpleSendETH(
driver,
amountFirstTx,
RECIPIENT_ADDRESS,
);
const totalAfterFirstTx = roundToXDecimalPlaces(
initialBalance - amountFirstTx - gasFeesFirstTx,
4,
);
await terminateServiceWorker(driver);
await assertETHBalance(driver, totalAfterFirstTx);
// first send ETH #2 and then terminate SW
const amountSecondTx = roundToXDecimalPlaces(
generateRandNumBetween(0.5, 2),
4,
);
const gasFeesSecondTx = await simpleSendETH(
driver,
amountSecondTx,
RECIPIENT_ADDRESS,
);
const totalAfterSecondTx = roundToXDecimalPlaces(
initialBalance -
amountFirstTx -
gasFeesFirstTx -
amountSecondTx -
gasFeesSecondTx,
4,
);
await terminateServiceWorker(driver);
await assertETHBalance(driver, totalAfterSecondTx);
// first terminate SW and then send ETH
const amountThirdTx = roundToXDecimalPlaces(
generateRandNumBetween(0.5, 2),
4,
);
const gasFeesThirdTx = await simpleSendETH(
driver,
amountThirdTx,
RECIPIENT_ADDRESS,
);
const totalAfterThirdTx = roundToXDecimalPlaces(
initialBalance -
amountFirstTx -
gasFeesFirstTx -
amountSecondTx -
gasFeesSecondTx -
amountThirdTx -
gasFeesThirdTx,
4,
);
await assertETHBalance(driver, totalAfterThirdTx);
},
);
async function simpleSendETH(driver, value, recipient) {
await switchToWindow(driver, WINDOW_TITLES.ExtensionInFullScreenView);
await driver.clickElement('[data-testid="eth-overview-send"]');
await driver.fill('[data-testid="ens-input"]', recipient);
const formattedValue = `${value}`.replace('.', ',');
await driver.fill('.unit-input__input', formattedValue);
await driver.clickElement('[data-testid="page-container-footer-next"]');
const gasFeesEl = await driver.findElement(
'.transaction-detail-item__detail-values .currency-display-component',
);
const gasFees = await gasFeesEl.getText();
await driver.clickElement('[data-testid="page-container-footer-next"]');
await driver.clickElement('[data-testid="home__activity-tab"]');
await driver.findElement('.transaction-list-item');
// reset view to assets tab
await driver.clickElement('[data-testid="home__asset-tab"]');
return gasFees;
}
async function assertETHBalance(driver, expectedBalance) {
await switchToWindow(driver, WINDOW_TITLES.ExtensionInFullScreenView);
const isETHBalanceOverviewPresentAndVisible =
await driver.isElementPresentAndVisible({
css: '[data-testid="eth-overview__primary-currency"]',
text: `${expectedBalance} ETH`,
});
assert.equal(
isETHBalanceOverviewPresentAndVisible,
true,
`Balance DOM element should be visible and match ${expectedBalance} ETH.`,
);
}
});
it('Should continue to support add network dApp interactions after service worker re-starts multiple times', async function () {
await withFixtures(
{
dapp: true,
fixtures: new FixtureBuilder()
.withPermissionControllerConnectedToTestDapp()
.build(),
ganacheOptions: generateGanacheOptions({
concurrent: { port: 8546, chainId: 1338 },
}),
title: this.test.title,
driverOptions: { openDevToolsForTabs: true },
},
async ({ driver }) => {
await driver.navigate();
await unlockWallet(driver, WALLET_PASSWORD);
await openDapp(driver);
// Click add Ethereum chain
await switchToWindow(driver, WINDOW_TITLES.TestDApp);
await driver.clickElement('#addEthereumChain');
await driver.waitUntilXWindowHandles(2);
// Notification pop up opens
await switchToWindow(driver, WINDOW_TITLES.Notification);
let notification = await driver.isElementPresent({
text: 'Allow this site to add a network?',
tag: 'h3',
});
assert.ok(notification, 'Dapp action does not appear in Metamask');
// Cancel Notification
await driver.clickElement({ text: 'Cancel', tag: 'button' });
await driver.waitUntilXWindowHandles(2);
// Terminate Service Worker
await switchToWindow(driver, WINDOW_TITLES.TestDApp);
await terminateServiceWorker(driver);
// Click add Ethereum chain #2
await switchToWindow(driver, WINDOW_TITLES.TestDApp);
await driver.clickElement('#addEthereumChain');
await driver.waitUntilXWindowHandles(2);
// Notification pop up opens
await switchToWindow(driver, WINDOW_TITLES.Notification);
notification = await driver.isElementPresent({
text: 'Allow this site to add a network?',
tag: 'h3',
});
assert.ok(notification, 'Dapp action does not appear in Metamask');
// Cancel Notification
await driver.clickElement({ text: 'Cancel', tag: 'button' });
await driver.waitUntilXWindowHandles(2);
// Terminate Service Worker
await switchToWindow(driver, WINDOW_TITLES.TestDApp);
await terminateServiceWorker(driver);
// Click add Ethereum chain #3
await switchToWindow(driver, WINDOW_TITLES.TestDApp);
await driver.clickElement('#addEthereumChain');
await driver.waitUntilXWindowHandles(2);
// Notification pop up opens
await switchToWindow(driver, WINDOW_TITLES.Notification);
notification = await driver.isElementPresent({
text: 'Allow this site to add a network?',
tag: 'h3',
});
assert.ok(notification, 'Dapp action does not appear in Metamask');
// Accept Notification
await driver.clickElement({ text: 'Approve', tag: 'button' });
await driver.clickElement({ text: 'Switch network', tag: 'button' });
await driver.waitUntilXWindowHandles(2);
},
);
});
it('Should continue to support send ETH dApp interactions after service worker re-starts multiple times', async function () {
await withFixtures(
{
dapp: true,
fixtures: new FixtureBuilder()
.withPermissionControllerConnectedToTestDapp()
.build(),
ganacheOptions: generateGanacheOptions({
concurrent: { port: 8546, chainId: 1338 },
}),
title: this.test.title,
driverOptions: { openDevToolsForTabs: true },
},
async ({ driver }) => {
await driver.navigate();
await unlockWallet(driver, WALLET_PASSWORD);
await openDapp(driver);
await clickSendButton(driver);
await driver.waitUntilXWindowHandles(2);
await switchToWindow(driver, WINDOW_TITLES.TestDApp);
await terminateServiceWorker(driver);
await driver.waitUntilXWindowHandles(2);
await clickSendButton(driver);
await driver.waitUntilXWindowHandles(2);
await switchToWindow(driver, WINDOW_TITLES.TestDApp);
await terminateServiceWorker(driver);
await clickSendButton(driver);
await driver.waitUntilXWindowHandles(2);
await assertNumberOfTransactionsInPopUp(driver, 3);
await confirmETHSendNotification(driver, 1);
await assertNumberOfTransactionsInPopUp(driver, 2);
await confirmETHSendNotification(driver, 1);
await confirmETHSendNotification(driver, 1);
},
);
async function clickSendButton(driver) {
// Click send button
await switchToWindow(driver, WINDOW_TITLES.TestDApp);
await driver.waitForSelector({
css: '#sendButton',
text: 'Send',
});
await driver.clickElement('#sendButton');
}
async function confirmETHSendNotification(driver, amount) {
await switchToWindow(driver, WINDOW_TITLES.Notification);
await driver.clickElement({
text: 'Edit',
tag: 'span',
});
await driver.fill('[data-testid="currency-input"]', amount);
await driver.clickElement({
text: 'Next',
tag: 'button',
});
await driver.clickElement({
text: 'Confirm',
tag: 'button',
});
}
async function assertNumberOfTransactionsInPopUp(driver, number) {
await switchToWindow(driver, WINDOW_TITLES.Notification);
const navEl = await driver.findElement(
'.confirm-page-container-navigation__navtext',
);
const notificationProgress = await navEl.getText();
assert.ok(notificationProgress, `1 of ${number}`);
}
});
it('Should lock wallet when a browser session ends (after turning off the extension)', async function () {
await withFixtures(
{
dapp: true,
fixtures: new FixtureBuilder()
.withPermissionControllerConnectedToTestDapp()
.build(),
ganacheOptions: generateGanacheOptions({
concurrent: { port: 8546, chainId: 1338 },
}),
title: this.test.title,
},
async ({ driver }) => {
const { extensionUrl } = driver;
const extensionId = extensionUrl.split('//')[1];
await driver.navigate();
await unlockWallet(driver, WALLET_PASSWORD);
await reloadExtension(driver, extensionId);
// ensure extension finishes reloading before reopening full screen extension
await sleepSeconds(0.1);
await driver.openNewPage(`${extensionUrl}/home.html`);
const passwordField = await driver.isElementPresent('#password');
assert.ok(
passwordField,
'Password screen is not visible. Wallet should have been locked.',
);
},
);
async function reloadExtension(driver, extensionId) {
await switchToWindow(driver, WINDOW_TITLES.ExtensionInFullScreenView);
await driver.openNewPage('chrome://extensions/');
// extensions-manager
const extensionsManager = await driver.findElement('extensions-manager');
// shadowRoot
const extensionsManagerShadowRoot = await driver.executeScript(
'return arguments[0][0].shadowRoot',
extensionsManager,
);
// cr-view-manager
const viewManager = await extensionsManagerShadowRoot.findElement({
css: '#viewManager',
});
// extensions-item-list
const itemList = await viewManager.findElement({
css: '#items-list',
});
// shadowRoot
const itemListShadowRoot = await driver.executeScript(
'return arguments[0][0].shadowRoot',
itemList,
);
// extension-item
const extensionItem = await await itemListShadowRoot.findElement({
css: `#${extensionId}`,
});
// shadowRoot
const extensionItemShadowRoot = await driver.executeScript(
'return arguments[0][0].shadowRoot',
extensionItem,
);
// cr-icon-button
const devReloadButton = await extensionItemShadowRoot.findElement({
css: '#dev-reload-button',
});
// shadowRoot
const devReloadButtonShadowRoot = await driver.executeScript(
'return arguments[0][0].shadowRoot',
devReloadButton,
);
// cr-icon-button
const reloadBtn = await devReloadButtonShadowRoot.findElement({
css: '#maskedImage',
});
await reloadBtn.click();
}
});
});

View File

@ -5,9 +5,11 @@ const {
openDapp,
defaultGanacheOptions,
assertAccountBalanceForDOM,
restartServiceWorker,
SERVICE_WORKER_URL,
regularDelayMs,
WALLET_PASSWORD,
unlockWallet,
terminateServiceWorker,
} = require('../helpers');
const FixtureBuilder = require('../fixture-builder');
@ -28,12 +30,12 @@ describe('Phishing warning page', function () {
},
async ({ driver, ganacheServer }) => {
await driver.navigate();
// log in wallet
await driver.fill('#password', 'correct horse battery staple');
await driver.press('#password', driver.Key.ENTER);
await unlockWallet(driver, WALLET_PASSWORD);
// DAPP is detected as phishing page
await openDapp(driver);
const phishingPageHeader = await driver.findElements({
text: 'Deceptive site ahead',
tag: 'h1',
@ -42,7 +44,7 @@ describe('Phishing warning page', function () {
// Restart service worker
await driver.openNewPage(SERVICE_WORKER_URL);
await restartServiceWorker(driver);
await terminateServiceWorker(driver);
await driver.delay(regularDelayMs);
// wait until extension is reloaded
@ -55,6 +57,7 @@ describe('Phishing warning page', function () {
await openDapp(driver);
// - extension, dapp, service worker and new dapp
await driver.waitUntilXWindowHandles(4);
const newPhishingPageHeader = await driver.findElements({
text: 'Deceptive site ahead',
tag: 'h1',

View File

@ -8,6 +8,7 @@ const {
waitForAccountRendered,
convertToHexValue,
regularDelayMs,
unlockWallet,
} = require('../helpers');
const FixtureBuilder = require('../fixture-builder');
@ -37,21 +38,19 @@ describe('Add account', function () {
},
async ({ driver }) => {
await driver.navigate();
await driver.fill('#password', 'correct horse battery staple');
await driver.press('#password', driver.Key.ENTER);
await unlockWallet(driver);
await driver.clickElement('[data-testid="account-menu-icon"]');
await driver.clickElement(
'[data-testid="multichain-account-menu-add-account"]',
'[data-testid="multichain-account-menu-popover-add-account"]',
);
await driver.fill('[placeholder="Account 2"]', '2nd account');
await driver.clickElement({ text: 'Create', tag: 'button' });
const accountName = await driver.waitForSelector({
await driver.findElement({
css: '[data-testid="account-menu-icon"]',
text: '2nd',
text: '2nd account',
});
assert.equal(await accountName.getText(), '2nd account');
},
);
});
@ -76,28 +75,26 @@ describe('Add account', function () {
// Check address of 1st account
await waitForAccountRendered(driver);
const firstAccountPublicAddress = await retrieveShortenAccountAddress(
driver,
);
assert.equal(firstAccountPublicAddress, shortenAddress(firstAccount));
await driver.findElement({
css: '.multichain-address-copy-button',
text: shortenAddress(firstAccount),
});
// Create 2nd account
await driver.clickElement('[data-testid="account-menu-icon"]');
await driver.clickElement(
'[data-testid="multichain-account-menu-add-account"]',
'[data-testid="multichain-account-menu-popover-add-account"]',
);
await driver.fill('[placeholder="Account 2"]', '2nd account');
await driver.clickElement({ text: 'Create', tag: 'button' });
await waitForAccountRendered(driver);
// Check address of 2nd account
const secondAccountPublicAddress = await retrieveShortenAccountAddress(
driver,
);
assert.strictEqual(
secondAccountPublicAddress,
shortenAddress(secondAccount),
);
await waitForAccountRendered(driver);
await driver.findElement({
css: '.multichain-address-copy-button',
text: shortenAddress(secondAccount),
});
// Log into the account with balance(account 1)
// and transfer some balance to 2nd account
@ -143,12 +140,10 @@ describe('Add account', function () {
await waitForAccountRendered(driver);
// Check address of 1st account
const restoredFirstAccountPublicAddress =
await retrieveShortenAccountAddress(driver);
assert.equal(
restoredFirstAccountPublicAddress,
shortenAddress(firstAccount),
);
await driver.findElement({
css: '.multichain-address-copy-button',
text: shortenAddress(firstAccount),
});
// Check address of 2nd account
const accountTwoSelector = await findAnotherAccountFromAccountList(
@ -158,17 +153,15 @@ describe('Add account', function () {
);
await driver.clickElement(accountTwoSelector);
const restoredSecondAccountPublicAddress =
await retrieveShortenAccountAddress(driver);
assert.equal(
restoredSecondAccountPublicAddress,
shortenAddress(secondAccount),
);
await driver.findElement({
css: '.multichain-address-copy-button',
text: shortenAddress(secondAccount),
});
},
);
});
it('It should be possible to remove an account imported with a private key, but should not be possible to remove an account generated from the SRP imported in onboarding', async function () {
it('should be possible to remove an account imported with a private key, but should not be possible to remove an account generated from the SRP imported in onboarding', async function () {
const testPrivateKey =
'14abe6f4aab7f9f626fe981c864d0adeb5685f289ac9270c27b8fd790b4235d6';
@ -180,25 +173,22 @@ describe('Add account', function () {
},
async ({ driver }) => {
await driver.navigate();
await driver.fill('#password', 'correct horse battery staple');
await driver.press('#password', driver.Key.ENTER);
await waitForAccountRendered(driver);
await unlockWallet(driver);
await driver.clickElement('[data-testid="account-menu-icon"]');
await driver.clickElement(
'[data-testid="multichain-account-menu-add-account"]',
'[data-testid="multichain-account-menu-popover-add-account"]',
);
await driver.fill('[placeholder="Account 2"]', '2nd account');
await driver.clickElement({ text: 'Create', tag: 'button' });
// Wait for 2nd account to be created
await waitForAccountRendered(driver);
const secondAccountCreated = await driver.findElement(
'[data-testid="account-menu-icon"]',
);
assert.equal(await secondAccountCreated.getText(), '2nd account');
await driver.findElement({
css: '[data-testid="account-menu-icon"]',
text: '2nd account',
});
await driver.clickElement('[data-testid="account-menu-icon"]');
@ -226,10 +216,10 @@ describe('Add account', function () {
// Wait for 3rd account to be created
await waitForAccountRendered(driver);
const thirdAccountCreated = await driver.findElement(
'[data-testid="account-menu-icon"]',
);
assert.equal(await thirdAccountCreated.getText(), 'Account 3');
await driver.findElement({
css: '[data-testid="account-menu-icon"]',
text: 'Account 3',
});
// User can delete 3rd account imported with a private key
await driver.clickElement('[data-testid="account-menu-icon"]');
@ -245,11 +235,3 @@ describe('Add account', function () {
);
});
});
async function retrieveShortenAccountAddress(driver) {
// get the shorten public address for account
const accountDOM = await driver.waitForSelector(
'.multichain-address-copy-button',
);
return await accountDOM.getText();
}

View File

@ -3,8 +3,8 @@ const {
withFixtures,
openDapp,
DAPP_URL,
login,
defaultGanacheOptions,
unlockWallet,
} = require('../helpers');
const FixtureBuilder = require('../fixture-builder');
@ -21,7 +21,7 @@ describe('Eth sign', function () {
},
async ({ driver }) => {
await driver.navigate();
await login(driver);
await unlockWallet(driver);
await openDapp(driver);
await driver.clickElement('#ethSign');
@ -56,7 +56,7 @@ describe('Eth sign', function () {
},
async ({ driver }) => {
await driver.navigate();
await login(driver);
await unlockWallet(driver);
await openDapp(driver);
await driver.clickElement('#ethSign');

View File

@ -412,8 +412,7 @@ describe('Send ETH from inside MetaMask to a Multisig Address', function () {
smartContract,
);
await driver.navigate();
await driver.fill('#password', 'correct horse battery staple');
await driver.press('#password', driver.Key.ENTER);
await logInWithBalanceValidation(driver, ganacheServer);
await driver.clickElement('[data-testid="eth-overview-send"]');
@ -426,10 +425,7 @@ describe('Send ETH from inside MetaMask to a Multisig Address', function () {
await inputAmount.fill('1');
// Continue to next screen
await driver.findClickableElement({ text: 'Next', tag: 'button' });
await driver.clickElement({ text: 'Next', tag: 'button' });
await driver.findClickableElement({ text: 'Confirm', tag: 'button' });
await driver.clickElement({ text: 'Confirm', tag: 'button' });
// Go back to home screen to check txn

View File

@ -0,0 +1,135 @@
const { strict: assert } = require('assert');
const {
withFixtures,
passwordUnlockOpenSRPRevealQuiz,
completeSRPRevealQuiz,
tapAndHoldToRevealSRP,
closeSRPReveal,
} = require('../helpers');
const FixtureBuilder = require('../fixture-builder');
const { tEn } = require('../../lib/i18n-helpers');
describe('Reveal SRP through settings', function () {
const testPassword = 'correct horse battery staple';
const wrongTestPassword = 'test test test test';
const seedPhraseWords =
'spread raise short crane omit tent fringe mandate neglect detail suspect cradle';
it('should not reveal SRP text with incorrect password', async function () {
await withFixtures(
{
fixtures: new FixtureBuilder().build(),
title: this.test.title,
failOnConsoleError: false,
},
async ({ driver }) => {
await passwordUnlockOpenSRPRevealQuiz(driver);
await completeSRPRevealQuiz(driver);
await driver.fill('#password-box', wrongTestPassword);
await driver.press('#password-box', driver.Key.ENTER);
await driver.isElementPresent(
{
css: '.mm-help-text',
text: 'Incorrect password',
},
true,
);
},
);
});
it('completes quiz and reveals SRP text', async function () {
await withFixtures(
{
fixtures: new FixtureBuilder().build(),
title: this.test.title,
},
async ({ driver }) => {
await passwordUnlockOpenSRPRevealQuiz(driver);
await completeSRPRevealQuiz(driver);
// enter password
await driver.fill('#password-box', testPassword);
await driver.press('#password-box', driver.Key.ENTER);
await tapAndHoldToRevealSRP(driver);
// confirm SRP text matches expected
const displayedSRP = await driver.findVisibleElement(
'[data-testid="srp_text"]',
);
assert.equal(await displayedSRP.getText(), seedPhraseWords);
// copy SRP text to clipboard
await driver.clickElement({
text: tEn('copyToClipboard'),
tag: 'button',
});
await driver.findVisibleElement({
text: tEn('copiedExclamation'),
tag: 'button',
});
// confirm that CTA returns user to wallet view
await closeSRPReveal(driver);
},
);
});
it('completes quiz and reveals SRP QR after wrong answers', async function () {
await withFixtures(
{
fixtures: new FixtureBuilder().build(),
title: this.test.title,
},
async ({ driver }) => {
await passwordUnlockOpenSRPRevealQuiz(driver);
// start quiz
await driver.clickElement('[data-testid="srp-quiz-get-started"]');
// tap incorrect answer 1
await driver.clickElement('[data-testid="srp-quiz-wrong-answer"]');
// try again
await driver.clickElement('[data-testid="srp-quiz-try-again"]');
// tap correct answer 1
await driver.clickElement('[data-testid="srp-quiz-right-answer"]');
// tap Continue 1
await driver.clickElement('[data-testid="srp-quiz-continue"]');
// tap incorrect answer 2
await driver.clickElement('[data-testid="srp-quiz-wrong-answer"]');
// try again
await driver.clickElement('[data-testid="srp-quiz-try-again"]');
// tap correct answer 1
await driver.clickElement('[data-testid="srp-quiz-right-answer"]');
// tap Continue 2
await driver.clickElement('[data-testid="srp-quiz-continue"]');
// enter password
await driver.fill('#password-box', testPassword);
await driver.press('#password-box', driver.Key.ENTER);
// tap and hold to reveal
await tapAndHoldToRevealSRP(driver);
// confirm SRP QR is displayed
await driver.clickElement({
text: 'QR',
tag: 'button',
});
const qrCode = await driver.findElement('[data-testid="qr-srp"]');
assert.equal(await qrCode.isDisplayed(), true);
// confirm that CTA returns user to wallet view
await closeSRPReveal(driver);
},
);
});
});

View File

@ -36,7 +36,7 @@ async function loadNewAccount() {
await driver.clickElement('[data-testid="account-menu-icon"]');
const timestampBeforeAction = new Date();
await driver.clickElement(
'[data-testid="multichain-account-menu-add-account"]',
'[data-testid="multichain-account-menu-popover-add-account"]',
);
await driver.fill('[placeholder="Account 2"]', '2nd account');
await driver.clickElement({ text: 'Create', tag: 'button' });

View File

@ -261,6 +261,18 @@ class Driver {
.perform();
}
async holdMouseDownOnElement(rawLocator, ms) {
const locator = this.buildLocator(rawLocator);
const element = await this.findClickableElement(locator);
await this.driver
.actions()
.move({ origin: element, x: 1, y: 1 })
.press()
.pause(ms)
.release()
.perform();
}
async scrollToElement(element) {
await this.driver.executeScript(
'arguments[0].scrollIntoView(true)',
@ -407,6 +419,11 @@ class Driver {
await this.driver.close();
}
async closeWindowHandle(windowHandle) {
await this.driver.switchTo().window(windowHandle);
await this.driver.close();
}
// Close Alert Popup
async closeAlertPopup() {
return await this.driver.switchTo().alert().accept();

6
test/lib/i18n-helpers.js Normal file
View File

@ -0,0 +1,6 @@
import { getMessage } from '../../ui/helpers/utils/i18n-helper';
import * as en from '../../app/_locales/en/messages.json';
export function tEn(key) {
return getMessage('en', en, key);
}

View File

@ -14,7 +14,8 @@
"outDir": "tsout",
"rootDir": ".",
"sourceMap": true,
"strict": true
"strict": true,
"resolveJsonModule": true
},
"exclude": [
"**/jest-coverage/**/*",

View File

@ -6,8 +6,8 @@ import { EditGasModes } from '../../../../../shared/constants/gas';
import Box from '../../../ui/box';
import CheckBox from '../../../ui/check-box';
import {
DISPLAY,
FLEX_DIRECTION,
Display,
FlexDirection,
TextColor,
TextVariant,
} from '../../../../helpers/constants/design-system';
@ -72,8 +72,8 @@ const AdvancedGasFeeDefaults = () => {
return (
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.ROW}
display={Display.Flex}
flexDirection={FlexDirection.Row}
marginTop={4}
marginLeft={2}
marginRight={2}
@ -90,7 +90,6 @@ const AdvancedGasFeeDefaults = () => {
variant={TextVariant.bodySm}
as="h6"
color={TextColor.textAlternative}
margin={0}
>
{isDefaultSettingsSelected
? t('advancedGasFeeDefaultOptOut')

View File

@ -0,0 +1,29 @@
import React from 'react';
import CancelButton from './cancel-button';
export default {
title: 'Components/App/CancelButton',
component: CancelButton,
argTypes: {
transaction: {
control: 'object',
},
cancelTransaction: {
control: 'cancelTransaction',
},
detailsModal: {
control: 'boolean',
},
},
args: {
detailsModal: true,
transaction: {
id: '12345',
status: 'pending',
},
},
};
export const DefaultStory = (args) => <CancelButton {...args} />;
DefaultStory.storyName = 'Default';

View File

@ -89,7 +89,7 @@ export default function HoldToRevealButton({ buttonText, onLongPressed }) {
setHasTriggeredUnlock(true);
preventPropogation(e);
},
[onLongPressed],
[onLongPressed, trackEvent],
);
/**

View File

@ -12,7 +12,7 @@ import Popover from '../../../ui/popover';
import {
FontWeight,
TextVariant,
DISPLAY,
Display,
Size,
BorderStyle,
BorderColor,
@ -65,17 +65,16 @@ export default function ContractDetailsModal({
fontWeight={FontWeight.Bold}
variant={TextVariant.bodyMd}
as="h5"
display={DISPLAY.FLEX}
boxProps={{ marginTop: 0, marginBottom: 0 }}
display={Display.Flex}
>
{t('contractTitle')}
</Text>
<Text
variant={TextVariant.bodySm}
as="h6"
display={DISPLAY.FLEX}
display={Display.Flex}
color={TextColor.textAlternative}
boxProps={{ marginTop: 2, marginBottom: 0 }}
marginTop={2}
>
{t('contractDescription')}
</Text>
@ -84,14 +83,14 @@ export default function ContractDetailsModal({
<Text
variant={TextVariant.bodySm}
as="h6"
display={DISPLAY.FLEX}
display={Display.Flex}
marginTop={4}
marginBottom={2}
>
{nft ? t('contractNFT') : t('contractToken')}
</Text>
<Box
display={DISPLAY.FLEX}
display={Display.Flex}
borderRadius={Size.SM}
borderStyle={BorderStyle.solid}
borderColor={BorderColor.borderDefault}
@ -124,9 +123,8 @@ export default function ContractDetailsModal({
<Text
variant={TextVariant.bodySm}
as="h6"
display={DISPLAY.FLEX}
display={Display.Flex}
color={TextColor.textAlternative}
marginTop={0}
marginBottom={4}
>
{ellipsify(tokenAddress)}
@ -148,7 +146,7 @@ export default function ContractDetailsModal({
}
>
<ButtonIcon
display={DISPLAY.FLEX}
display={Display.Flex}
iconName={
copiedTokenAddress ? IconName.CopySuccess : IconName.Copy
}
@ -163,7 +161,7 @@ export default function ContractDetailsModal({
</Tooltip>
<Tooltip position="top" title={t('openInBlockExplorer')}>
<ButtonIcon
display={DISPLAY.FLEX}
display={Display.Flex}
iconName={IconName.Export}
color={Color.iconMuted}
onClick={() => {
@ -189,7 +187,7 @@ export default function ContractDetailsModal({
<Text
variant={TextVariant.bodySm}
as="h6"
display={DISPLAY.FLEX}
display={Display.Flex}
marginTop={4}
marginBottom={2}
>
@ -200,7 +198,7 @@ export default function ContractDetailsModal({
t('contractRequestingSpendingCap')}
</Text>
<Box
display={DISPLAY.FLEX}
display={Display.Flex}
borderRadius={Size.SM}
borderStyle={BorderStyle.solid}
borderColor={BorderColor.borderDefault}
@ -224,9 +222,8 @@ export default function ContractDetailsModal({
<Text
variant={TextVariant.bodySm}
as="h6"
display={DISPLAY.FLEX}
display={Display.Flex}
color={TextColor.textAlternative}
marginTop={0}
marginBottom={4}
>
{ellipsify(toAddress)}
@ -246,7 +243,7 @@ export default function ContractDetailsModal({
}
>
<ButtonIcon
display={DISPLAY.FLEX}
display={Display.Flex}
iconName={
copiedToAddress ? IconName.CopySuccess : IconName.Copy
}
@ -261,7 +258,7 @@ export default function ContractDetailsModal({
</Tooltip>
<Tooltip position="top" title={t('openInBlockExplorer')}>
<ButtonIcon
display={DISPLAY.FLEX}
display={Display.Flex}
iconName={IconName.Export}
color={Color.iconMuted}
onClick={() => {
@ -284,7 +281,7 @@ export default function ContractDetailsModal({
</Box>
</Box>
<Box
display={DISPLAY.FLEX}
display={Display.Flex}
paddingTop={6}
paddingRight={4}
paddingBottom={6}

View File

@ -2,10 +2,10 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from '../../../store/actions';
import isMobileView from '../../../helpers/utils/is-mobile-view';
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
import isMobileView from '../../../helpers/utils/is-mobile-view';
import * as actions from '../../../store/actions';
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
import { mmiActionsFactory } from '../../../store/institutional/institution-background';
///: END:ONLY_INCLUDE_IN
@ -13,31 +13,31 @@ import { mmiActionsFactory } from '../../../store/institutional/institution-back
// Modal Components
import AddNetworkModal from '../../../pages/onboarding-flow/add-network-modal';
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
import ComplianceDetailsModal from '../../institutional/compliance-details';
import ComplianceModal from '../../institutional/compliance-modal';
import ConfirmRemoveJWT from '../../institutional/confirm-remove-jwt-modal';
import TransactionFailed from '../../institutional/transaction-failed-modal';
import CustodyConfirmLink from '../../institutional/custody-confirm-link-modal';
import InteractiveReplacementTokenModal from '../../institutional/interactive-replacement-token-modal';
import ComplianceModal from '../../institutional/compliance-modal';
import ComplianceDetailsModal from '../../institutional/compliance-details';
import TransactionFailed from '../../institutional/transaction-failed-modal';
///: END:ONLY_INCLUDE_IN
import AccountDetailsModal from './account-details-modal';
import ExportPrivateKeyModal from './export-private-key-modal';
import HideTokenConfirmationModal from './hide-token-confirmation-modal';
import QRScanner from './qr-scanner';
import HoldToRevealModal from './hold-to-reveal-modal';
import ConfirmRemoveAccount from './confirm-remove-account';
import ConfirmResetAccount from './confirm-reset-account';
import HoldToRevealModal from './hold-to-reveal-modal';
import TransactionConfirmed from './transaction-confirmed';
import FadeModal from './fade-modal';
import RejectTransactions from './reject-transactions';
import ConfirmDeleteNetwork from './confirm-delete-network';
import EditApprovalPermission from './edit-approval-permission';
import NewAccountModal from './new-account-modal';
import CustomizeNonceModal from './customize-nonce';
import ConvertTokenToNftModal from './convert-token-to-nft-modal/convert-token-to-nft-modal';
import CustomizeNonceModal from './customize-nonce';
import EditApprovalPermission from './edit-approval-permission';
import EthSignModal from './eth-sign-modal/eth-sign-modal';
import FadeModal from './fade-modal';
import NewAccountModal from './new-account-modal';
import RejectTransactions from './reject-transactions';
const modalContainerBaseStyle = {
transform: 'translate3d(-50%, 0, 0px)',

View File

@ -0,0 +1,35 @@
import React from 'react';
import MultilayerFeeMessage from './multi-layer-fee-message';
export default {
title: 'Components/App/MultilayerFeeMessage',
component: MultilayerFeeMessage,
argTypes: {
transaction: {
control: 'object',
},
layer2fee: {
control: 'text',
},
nativeCurrency: {
control: 'text',
},
plainStyle: {
control: 'boolean',
},
},
args: {
transaction: {
txParams: {
value: '0x123456789',
},
},
layer2fee: '0x987654321',
nativeCurrency: 'ETH',
plainStyle: true,
},
};
export const DefaultStory = (args) => <MultilayerFeeMessage {...args} />;
DefaultStory.storyName = 'Default';

View File

@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
import IconWithFallback from '../../ui/icon-with-fallback';
import Identicon from '../../ui/identicon';
import {
DISPLAY,
FLEX_DIRECTION,
Display,
FlexDirection,
TextVariant,
FontWeight,
AlignItems,
@ -33,22 +33,22 @@ export default function NetworkAccountBalanceHeader({
return (
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.ROW}
display={Display.Flex}
flexDirection={FlexDirection.Row}
padding={4}
className="network-account-balance-header"
alignItems={AlignItems.center}
justifyContent={JustifyContent.spaceBetween}
>
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.ROW}
display={Display.Flex}
flexDirection={FlexDirection.Row}
alignItems={AlignItems.center}
gap={2}
>
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.ROW}
display={Display.Flex}
flexDirection={FlexDirection.Row}
alignItems={AlignItems.center}
>
<Identicon address={accountAddress} diameter={32} />
@ -60,15 +60,14 @@ export default function NetworkAccountBalanceHeader({
/>
</Box>
<Box
display={DISPLAY.FLEX}
display={Display.Flex}
alignItems={AlignItems.flexStart}
flexDirection={FLEX_DIRECTION.COLUMN}
flexDirection={FlexDirection.Column}
>
<Text
variant={TextVariant.bodySm}
as="h6"
color={TextColor.textAlternative}
marginBottom={0}
>
{networkName}
</Text>
@ -78,22 +77,20 @@ export default function NetworkAccountBalanceHeader({
as="h6"
color={TextColor.textDefault}
fontWeight={FontWeight.Bold}
marginTop={0}
>
{accountName}
</Text>
</Box>
</Box>
<Box
display={DISPLAY.FLEX}
display={Display.Flex}
alignItems={AlignItems.flexEnd}
flexDirection={FLEX_DIRECTION.COLUMN}
flexDirection={FlexDirection.Column}
>
<Text
variant={TextVariant.bodySm}
as="h6"
color={TextColor.textAlternative}
marginBottom={0}
>
{t('balance')}
</Text>
@ -103,7 +100,6 @@ export default function NetworkAccountBalanceHeader({
as="h6"
color={TextColor.textDefault}
fontWeight={FontWeight.Bold}
marginTop={0}
align={TextAlign.End}
>
{accountBalance} {tokenName}

View File

@ -0,0 +1,19 @@
import React from 'react';
import NftOptions from './nft-options';
export default {
title: 'Components/App/NftOptions',
component: NftOptions,
argTypes: {
onRemove: {
action: 'onRemove',
},
onViewOnOpensea: {
action: 'onViewOnOpensea',
},
},
};
export const DefaultStory = (args) => <NftOptions {...args} />;
DefaultStory.storyName = 'Default';

View File

@ -1,5 +1,6 @@
import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { ethErrors, serializeError } from 'eth-rpc-errors';
import { getCurrentQRHardwareState } from '../../../selectors';
import Popover from '../../ui/popover';
import { useI18nContext } from '../../../hooks/useI18nContext';
@ -7,11 +8,8 @@ import {
cancelSyncQRHardware as cancelSyncQRHardwareAction,
cancelQRHardwareSignRequest as cancelQRHardwareSignRequestAction,
cancelTx,
cancelPersonalMsg,
cancelMsg,
cancelTypedMsg,
rejectPendingApproval,
} from '../../../store/actions';
import { MESSAGE_TYPE } from '../../../../shared/constants/app';
import QRHardwareWalletImporter from './qr-hardware-wallet-importer';
import QRHardwareSignRequest from './qr-hardware-sign-request';
@ -43,25 +41,13 @@ const QRHardwarePopover = () => {
);
const signRequestCancel = useCallback(() => {
let action = cancelTx;
switch (_txData.type) {
case MESSAGE_TYPE.PERSONAL_SIGN: {
action = cancelPersonalMsg;
break;
}
case MESSAGE_TYPE.ETH_SIGN: {
action = cancelMsg;
break;
}
case MESSAGE_TYPE.ETH_SIGN_TYPED_DATA: {
action = cancelTypedMsg;
break;
}
default: {
action = cancelTx;
}
}
dispatch(action(_txData));
dispatch(
rejectPendingApproval(
_txData.id,
serializeError(ethErrors.provider.userRejectedRequest()),
),
);
dispatch(cancelTx(_txData));
dispatch(cancelQRHardwareSignRequestAction());
}, [dispatch, _txData]);

View File

@ -96,6 +96,7 @@ const SignatureRequestOriginalWarning = ({
<Button
className="signature-request-warning__footer__sign-button"
type="danger-primary"
data-testid="signature-warning-sign-button"
onClick={onSubmit}
>
{t('sign')}

View File

@ -2,6 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { ObjectInspector } from 'react-inspector';
import { ethErrors, serializeError } from 'eth-rpc-errors';
import LedgerInstructionField from '../ledger-instruction-field';
import { MESSAGE_TYPE } from '../../../../shared/constants/app';
import {
@ -49,18 +50,19 @@ export default class SignatureRequestOriginal extends Component {
address: PropTypes.string.isRequired,
name: PropTypes.string,
}).isRequired,
cancel: PropTypes.func.isRequired,
clearConfirmTransaction: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
mostRecentOverviewPage: PropTypes.string.isRequired,
sign: PropTypes.func.isRequired,
txData: PropTypes.object.isRequired,
subjectMetadata: PropTypes.object,
hardwareWalletRequiresConnection: PropTypes.bool,
isLedgerWallet: PropTypes.bool,
messagesCount: PropTypes.number,
showRejectTransactionsConfirmationModal: PropTypes.func.isRequired,
cancelAll: PropTypes.func.isRequired,
cancelAllApprovals: PropTypes.func.isRequired,
rejectPendingApproval: PropTypes.func.isRequired,
resolvePendingApproval: PropTypes.func.isRequired,
completedTx: PropTypes.func.isRequired,
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
selectedAccount: PropTypes.object,
///: END:ONLY_INCLUDE_IN
@ -230,33 +232,48 @@ export default class SignatureRequestOriginal extends Component {
);
};
onSubmit = async (event) => {
const { clearConfirmTransaction, history, mostRecentOverviewPage, sign } =
this.props;
onSubmit = async () => {
const {
clearConfirmTransaction,
history,
mostRecentOverviewPage,
resolvePendingApproval,
completedTx,
txData: { id },
} = this.props;
await sign(event);
await resolvePendingApproval(id);
completedTx(id);
clearConfirmTransaction();
history.push(mostRecentOverviewPage);
};
onCancel = async (event) => {
const { clearConfirmTransaction, history, mostRecentOverviewPage, cancel } =
this.props;
onCancel = async () => {
const {
clearConfirmTransaction,
history,
mostRecentOverviewPage,
rejectPendingApproval,
txData: { id },
} = this.props;
await cancel(event);
await rejectPendingApproval(
id,
serializeError(ethErrors.provider.userRejectedRequest()),
);
clearConfirmTransaction();
history.push(mostRecentOverviewPage);
};
renderFooter = () => {
const {
cancel,
sign,
clearConfirmTransaction,
history,
mostRecentOverviewPage,
txData: { type },
txData: { type, id },
hardwareWalletRequiresConnection,
rejectPendingApproval,
resolvePendingApproval,
} = this.props;
const { t } = this.context;
@ -264,16 +281,19 @@ export default class SignatureRequestOriginal extends Component {
<PageContainerFooter
cancelText={t('reject')}
submitText={t('sign')}
onCancel={async (event) => {
await cancel(event);
onCancel={async () => {
await rejectPendingApproval(
id,
serializeError(ethErrors.provider.userRejectedRequest()),
);
clearConfirmTransaction();
history.push(mostRecentOverviewPage);
}}
onSubmit={async (event) => {
onSubmit={async () => {
if (type === MESSAGE_TYPE.ETH_SIGN) {
this.setState({ showSignatureRequestWarning: true });
} else {
await sign(event);
await resolvePendingApproval(id);
clearConfirmTransaction();
history.push(mostRecentOverviewPage);
}
@ -285,19 +305,19 @@ export default class SignatureRequestOriginal extends Component {
handleCancelAll = () => {
const {
cancelAll,
clearConfirmTransaction,
history,
mostRecentOverviewPage,
showRejectTransactionsConfirmationModal,
messagesCount,
cancelAllApprovals,
} = this.props;
const unapprovedTxCount = messagesCount;
showRejectTransactionsConfirmationModal({
unapprovedTxCount,
onSubmit: async () => {
await cancelAll();
await cancelAllApprovals();
clearConfirmTransaction();
history.push(mostRecentOverviewPage);
},

View File

@ -1,9 +1,14 @@
import { connect } from 'react-redux';
import { compose } from 'redux';
import { withRouter } from 'react-router-dom';
import { MESSAGE_TYPE } from '../../../../shared/constants/app';
import { goHome, cancelMsgs, showModal } from '../../../store/actions';
import {
goHome,
showModal,
resolvePendingApproval,
rejectPendingApproval,
rejectAllMessages,
completedTx,
} from '../../../store/actions';
import {
accountsWithSendEtherInfoSelector,
getSubjectMetadata,
@ -65,44 +70,30 @@ function mapDispatchToProps(dispatch) {
}),
);
},
cancelAll: (messagesList) => dispatch(cancelMsgs(messagesList)),
completedTx: (txId) => dispatch(completedTx(txId)),
resolvePendingApproval: (id) => {
dispatch(resolvePendingApproval(id));
},
rejectPendingApproval: (id, error) =>
dispatch(rejectPendingApproval(id, error)),
cancelAllApprovals: (messagesList) => {
dispatch(rejectAllMessages(messagesList));
},
};
}
function mergeProps(stateProps, dispatchProps, ownProps) {
const {
signPersonalMessage,
signTypedMessage,
cancelPersonalMessage,
cancelTypedMessage,
signMessage,
cancelMessage,
txData,
} = ownProps;
const { txData } = ownProps;
const { allAccounts, messagesList, ...otherStateProps } = stateProps;
const {
type,
msgParams: { from },
} = txData;
const fromAccount = getAccountByAddress(allAccounts, from);
const { cancelAll: dispatchCancelAll } = dispatchProps;
let cancel;
let sign;
if (type === MESSAGE_TYPE.PERSONAL_SIGN) {
cancel = cancelPersonalMessage;
sign = signPersonalMessage;
} else if (type === MESSAGE_TYPE.ETH_SIGN_TYPED_DATA) {
cancel = cancelTypedMessage;
sign = signTypedMessage;
} else if (type === MESSAGE_TYPE.ETH_SIGN) {
cancel = cancelMessage;
sign = signMessage;
}
const { cancelAllApprovals: dispatchCancelAllApprovals } = dispatchProps;
return {
...ownProps,
@ -110,9 +101,8 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
...dispatchProps,
fromAccount,
txData,
cancel,
sign,
cancelAll: () => dispatchCancelAll(valuesFor(messagesList)),
cancelAllApprovals: () =>
dispatchCancelAllApprovals(valuesFor(messagesList)),
};
}

View File

@ -1,13 +1,25 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import { fireEvent, screen } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { MESSAGE_TYPE } from '../../../../shared/constants/app';
import mockState from '../../../../test/data/mock-state.json';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import configureStore from '../../../store/store';
import { SECURITY_PROVIDER_MESSAGE_SEVERITIES } from '../security-provider-banner-message/security-provider-banner-message.constants';
import {
resolvePendingApproval,
rejectPendingApproval,
completedTx,
} from '../../../store/actions';
import SignatureRequestOriginal from '.';
jest.mock('../../../store/actions', () => ({
resolvePendingApproval: jest.fn().mockReturnValue({ type: 'test' }),
rejectPendingApproval: jest.fn().mockReturnValue({ type: 'test' }),
completedTx: jest.fn().mockReturnValue({ type: 'test' }),
}));
const MOCK_SIGN_DATA = JSON.stringify({
domain: {
name: 'happydapp.website',
@ -92,12 +104,30 @@ describe('SignatureRequestOriginal', () => {
expect(screen.getByText('Signature request')).toBeInTheDocument();
});
it('should render warning for eth sign when sign button clicked', () => {
it('should render warning for eth sign when sign button clicked', async () => {
render();
const signButton = screen.getByTestId('page-container-footer-next');
fireEvent.click(signButton);
expect(screen.getByText('Your funds may be at risk')).toBeInTheDocument();
const secondSignButton = screen.getByTestId(
'signature-warning-sign-button',
);
await act(async () => {
fireEvent.click(secondSignButton);
});
expect(resolvePendingApproval).toHaveBeenCalledTimes(1);
expect(completedTx).toHaveBeenCalledTimes(1);
});
it('should cancel approval when user reject signing', async () => {
render();
const rejectButton = screen.getByTestId('page-container-footer-cancel');
await act(async () => {
fireEvent.click(rejectButton);
});
expect(rejectPendingApproval).toHaveBeenCalledTimes(1);
});
it('should escape RTL character in label or value', () => {

View File

@ -4,6 +4,7 @@ import { useSelector, useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
import log from 'loglevel';
import { isValidSIWEOrigin } from '@metamask/controller-utils';
import { ethErrors, serializeError } from 'eth-rpc-errors';
import { BannerAlert, Text } from '../../component-library';
import Popover from '../../ui/popover';
import Checkbox from '../../ui/check-box';
@ -25,27 +26,28 @@ import {
SEVERITIES,
TextVariant,
} from '../../../helpers/constants/design-system';
import {
resolvePendingApproval,
rejectPendingApproval,
rejectAllMessages,
completedTx,
showModal,
} from '../../../store/actions';
import SecurityProviderBannerMessage from '../security-provider-banner-message/security-provider-banner-message';
import { SECURITY_PROVIDER_MESSAGE_SEVERITIES } from '../security-provider-banner-message/security-provider-banner-message.constants';
import ConfirmPageContainerNavigation from '../confirm-page-container/confirm-page-container-navigation';
import { getMostRecentOverviewPage } from '../../../ducks/history/history';
import { showModal, cancelMsgs } from '../../../store/actions';
import LedgerInstructionField from '../ledger-instruction-field';
import SignatureRequestHeader from '../signature-request-header';
import Header from './signature-request-siwe-header';
import Message from './signature-request-siwe-message';
export default function SignatureRequestSIWE({
txData,
cancelPersonalMessage,
signPersonalMessage,
}) {
export default function SignatureRequestSIWE({ txData }) {
const dispatch = useDispatch();
const history = useHistory();
const t = useContext(I18nContext);
const allAccounts = useSelector(accountsWithSendEtherInfoSelector);
const subjectMetadata = useSelector(getSubjectMetadata);
@ -59,6 +61,7 @@ export default function SignatureRequestSIWE({
origin,
siwe: { parsedMessage },
},
id,
} = txData;
const isLedgerWallet = useSelector((state) => isAddressLedger(state, from));
@ -82,27 +85,27 @@ export default function SignatureRequestSIWE({
(txData?.securityProviderResponse &&
Object.keys(txData.securityProviderResponse).length === 0);
const onSign = useCallback(
async (event) => {
try {
await signPersonalMessage(event);
} catch (e) {
log.error(e);
}
},
[signPersonalMessage],
);
const onSign = useCallback(async () => {
try {
await dispatch(resolvePendingApproval(id, null));
dispatch(completedTx(id));
} catch (e) {
log.error(e);
}
}, [id, dispatch]);
const onCancel = useCallback(
async (event) => {
try {
await cancelPersonalMessage(event);
} catch (e) {
log.error(e);
}
},
[cancelPersonalMessage],
);
const onCancel = useCallback(async () => {
try {
await dispatch(
rejectPendingApproval(
id,
serializeError(ethErrors.provider.userRejectedRequest()),
),
);
} catch (e) {
log.error(e);
}
}, []);
const handleCancelAll = () => {
const unapprovedTxCount = messagesCount;
@ -112,7 +115,7 @@ export default function SignatureRequestSIWE({
name: 'REJECT_TRANSACTIONS',
unapprovedTxCount,
onSubmit: async () => {
await dispatch(cancelMsgs(valuesFor(messagesList)));
await dispatch(rejectAllMessages(valuesFor(messagesList)));
dispatch(clearConfirmTransaction());
history.push(mostRecentOverviewPage);
},
@ -242,12 +245,4 @@ SignatureRequestSIWE.propTypes = {
* The display content of transaction data
*/
txData: PropTypes.object.isRequired,
/**
* Handler for cancel button
*/
cancelPersonalMessage: PropTypes.func.isRequired,
/**
* Handler for sign button
*/
signPersonalMessage: PropTypes.func.isRequired,
};

View File

@ -1,6 +1,7 @@
import React, { PureComponent } from 'react';
import { memoize } from 'lodash';
import PropTypes from 'prop-types';
import { ethErrors, serializeError } from 'eth-rpc-errors';
import LedgerInstructionField from '../ledger-instruction-field';
import {
sanitizeMessage,
@ -62,14 +63,7 @@ export default class SignatureRequest extends PureComponent {
* Check if the wallet is ledget wallet or not
*/
isLedgerWallet: PropTypes.bool,
/**
* Handler for cancel button
*/
cancel: PropTypes.func.isRequired,
/**
* Handler for sign button
*/
sign: PropTypes.func.isRequired,
/**
* Whether the hardware wallet requires a connection disables the sign button if true.
*/
@ -92,10 +86,14 @@ export default class SignatureRequest extends PureComponent {
history: PropTypes.object,
mostRecentOverviewPage: PropTypes.string,
showRejectTransactionsConfirmationModal: PropTypes.func.isRequired,
cancelAll: PropTypes.func.isRequired,
cancelAllApprovals: PropTypes.func.isRequired,
resolvePendingApproval: PropTypes.func.isRequired,
rejectPendingApproval: PropTypes.func.isRequired,
completedTx: PropTypes.func.isRequired,
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
showCustodianDeepLink: PropTypes.func,
isNotification: PropTypes.bool,
mmiOnSignCallback: PropTypes.func,
// Used to show a warning if the signing account is not the selected account
// Largely relevant for contract wallet custodians
selectedAccount: PropTypes.object,
@ -150,18 +148,18 @@ export default class SignatureRequest extends PureComponent {
handleCancelAll = () => {
const {
cancelAll,
clearConfirmTransaction,
history,
mostRecentOverviewPage,
showRejectTransactionsConfirmationModal,
unapprovedMessagesCount,
cancelAllApprovals,
} = this.props;
showRejectTransactionsConfirmationModal({
unapprovedTxCount: unapprovedMessagesCount,
onSubmit: async () => {
await cancelAll();
await cancelAllApprovals();
clearConfirmTransaction();
history.push(mostRecentOverviewPage);
},
@ -174,10 +172,9 @@ export default class SignatureRequest extends PureComponent {
txData: {
msgParams: { data, origin, version },
type,
id,
},
fromAccount: { address, balance, name },
cancel,
sign,
isLedgerWallet,
hardwareWalletRequiresConnection,
chainId,
@ -188,6 +185,9 @@ export default class SignatureRequest extends PureComponent {
currentCurrency,
conversionRate,
unapprovedMessagesCount,
resolvePendingApproval,
rejectPendingApproval,
completedTx,
} = this.props;
const { t, trackEvent } = this.context;
@ -221,8 +221,15 @@ export default class SignatureRequest extends PureComponent {
.toBase(10)
.toString();
const onSign = (event) => {
sign(event);
const onSign = async () => {
await resolvePendingApproval(id);
completedTx(id);
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
if (this.props.mmiOnSignCallback) {
await this.props.mmiOnSignCallback(txData);
}
///: END:ONLY_INCLUDE_IN
trackEvent({
category: MetaMetricsEventCategory.Transactions,
event: 'Confirm',
@ -235,8 +242,11 @@ export default class SignatureRequest extends PureComponent {
});
};
const onCancel = (event) => {
cancel(event);
const onCancel = async () => {
await rejectPendingApproval(
id,
serializeError(ethErrors.provider.userRejectedRequest()),
);
trackEvent({
category: MetaMetricsEventCategory.Transactions,
event: 'Cancel',

View File

@ -13,6 +13,7 @@ import {
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
getAccountType,
getSelectedAccount,
unapprovedTypedMessagesSelector,
///: END:ONLY_INCLUDE_IN
} from '../../../selectors';
import {
@ -29,16 +30,20 @@ import {
setTypedMessageInProgress,
} from '../../../store/institutional/institution-background';
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { checkForUnapprovedTypedMessages } from '../../../store/institutional/institution-actions';
///: END:ONLY_INCLUDE_IN
import {
MESSAGE_TYPE,
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
ENVIRONMENT_TYPE_NOTIFICATION,
///: END:ONLY_INCLUDE_IN
} from '../../../../shared/constants/app';
import {
cancelMsgs,
showModal,
resolvePendingApproval,
rejectPendingApproval,
rejectAllMessages,
completedTx,
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
goHome,
///: END:ONLY_INCLUDE_IN
@ -89,6 +94,7 @@ function mapStateToProps(state, ownProps) {
accountType: getAccountType(state),
isNotification: envType === ENVIRONMENT_TYPE_NOTIFICATION,
selectedAccount: getSelectedAccount(state),
unapprovedTypedMessages: unapprovedTypedMessagesSelector(state),
///: END:ONLY_INCLUDE_IN
};
}
@ -143,6 +149,10 @@ mapDispatchToProps = mmiMapDispatchToProps;
mapDispatchToProps = function (dispatch) {
return {
resolvePendingApproval: (id) => dispatch(resolvePendingApproval(id)),
completedTx: (id) => dispatch(completedTx(id)),
rejectPendingApproval: (id, error) =>
dispatch(rejectPendingApproval(id, error)),
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
showRejectTransactionsConfirmationModal: ({
onSubmit,
@ -157,8 +167,9 @@ mapDispatchToProps = function (dispatch) {
}),
);
},
cancelAll: (unconfirmedMessagesList) =>
dispatch(cancelMsgs(unconfirmedMessagesList)),
cancelAllApprovals: (unconfirmedMessagesList) => {
dispatch(rejectAllMessages(unconfirmedMessagesList));
},
};
};
@ -180,49 +191,33 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
accountType,
isNotification,
unapprovedTypedMessages,
///: END:ONLY_INCLUDE_IN
} = stateProps;
const {
signPersonalMessage,
signTypedMessage,
cancelPersonalMessage,
cancelTypedMessage,
signMessage,
cancelMessage,
txData,
} = ownProps;
const { cancelAll: dispatchCancelAll } = dispatchProps;
const { txData } = ownProps;
const {
cancelAll: dispatchCancelAll,
cancelAllApprovals: dispatchCancelAllApprovals,
} = dispatchProps;
const {
type,
msgParams: { from },
} = txData;
const fromAccount = getAccountByAddress(allAccounts, from);
let cancel;
let sign;
if (type === MESSAGE_TYPE.PERSONAL_SIGN) {
cancel = cancelPersonalMessage;
sign = signPersonalMessage;
} else if (type === MESSAGE_TYPE.ETH_SIGN_TYPED_DATA) {
cancel = cancelTypedMessage;
sign = signTypedMessage;
} else if (type === MESSAGE_TYPE.ETH_SIGN) {
cancel = cancelMessage;
sign = signMessage;
}
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
const signFn = async (...opts) => {
const mmiOnSignCallback = async (_msgData) => {
if (accountType === 'custody') {
try {
let msgData = opts;
let id = opts.custodyId;
if (!opts.custodyId) {
msgData = await sign(opts);
let msgData = _msgData;
let id = _msgData.custodyId;
if (!_msgData.custodyId) {
msgData = checkForUnapprovedTypedMessages(
_msgData,
unapprovedTypedMessages,
);
id = msgData.custodyId;
}
dispatchProps.showCustodianDeepLink({
@ -235,7 +230,6 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
await dispatchProps.setMsgInProgress(msgData.metamaskId);
await dispatchProps.setWaitForConfirmDeepLinkDialog(true);
await goHome();
return msgData;
} catch (err) {
await dispatchProps.setWaitForConfirmDeepLinkDialog(true);
await dispatchProps.showTransactionsFailedModal({
@ -243,11 +237,8 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
closeNotification: true,
operationFailed: true,
});
return null;
}
}
return sign(opts);
};
///: END:ONLY_INCLUDE_IN
@ -256,14 +247,6 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
...dispatchProps,
fromAccount,
txData,
cancel,
///: BEGIN:ONLY_INCLUDE_IN(build-main,build-beta,build-flask)
sign,
///: END:ONLY_INCLUDE_IN
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
// eslint-disable-next-line no-dupe-keys
sign: signFn,
///: END:ONLY_INCLUDE_IN
isLedgerWallet,
hardwareWalletRequiresConnection,
chainId,
@ -276,6 +259,11 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
unapprovedMessagesCount,
mostRecentOverviewPage,
cancelAll: () => dispatchCancelAll(valuesFor(unconfirmedMessagesList)),
cancelAllApprovals: () =>
dispatchCancelAllApprovals(valuesFor(unconfirmedMessagesList)),
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
mmiOnSignCallback,
///: END:ONLY_INCLUDE_IN
};
}

View File

@ -1,6 +1,6 @@
import React from 'react';
import sinon from 'sinon';
import { fireEvent, screen } from '@testing-library/react';
import { fireEvent, screen, act } from '@testing-library/react';
import configureMockStore from 'redux-mock-store';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import SignatureRequest from './signature-request.container';
@ -90,6 +90,7 @@ describe('Signature Request', () => {
clearConfirmTransaction: sinon.spy(),
cancelMessage: sinon.spy(),
cancel: sinon.stub().resolves(),
rejectPendingApproval: sinon.stub().resolves(),
showRejectTransactionsConfirmationModal: sinon.stub().resolves(),
cancelAll: sinon.stub().resolves(),
providerConfig: {
@ -97,6 +98,9 @@ describe('Signature Request', () => {
},
unapprovedMessagesCount: 2,
sign: sinon.stub().resolves(),
cancelAllApprovals: sinon.stub().resolves(),
resolvePendingApproval: sinon.stub().resolves(),
completedTx: sinon.stub().resolves(),
txData: {
msgParams: {
id: 1,
@ -162,22 +166,24 @@ describe('Signature Request', () => {
propsWithFiat.clearConfirmTransaction.resetHistory();
});
it('cancel', () => {
it('cancel', async () => {
const cancelButton = screen.getByTestId('page-container-footer-cancel');
fireEvent.click(cancelButton);
expect(propsWithFiat.cancel.calledOnce).toStrictEqual(true);
await act(() => {
fireEvent.click(cancelButton);
});
expect(propsWithFiat.rejectPendingApproval.calledOnce).toStrictEqual(
true,
);
});
it('sign', () => {
it('sign', async () => {
const signButton = screen.getByTestId('page-container-footer-next');
fireEvent.click(signButton);
expect(propsWithFiat.sign.calledOnce).toStrictEqual(true);
});
it('cancelAll', () => {
const cancelAll = screen.getByTestId('signature-request-reject-all');
fireEvent.click(cancelAll);
expect(propsWithFiat.cancelAll.calledOnce).toStrictEqual(false);
await act(() => {
fireEvent.click(signButton);
});
expect(propsWithFiat.resolvePendingApproval.calledOnce).toStrictEqual(
true,
);
});
it('have user warning', () => {

View File

@ -0,0 +1,73 @@
import React from 'react';
import {
AlignItems,
BlockSize,
Display,
FlexDirection,
JustifyContent,
TextAlign,
TextVariant,
} from '../../../../helpers/constants/design-system';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import { Button, Text, Box } from '../../../component-library';
import { IQuizInformationProps } from '../types';
export default function QuizContent({
icon,
image,
content,
moreContent,
buttons,
}: IQuizInformationProps) {
const t = useI18nContext();
return (
<>
{icon && (
<Box
display={Display.Flex}
flexDirection={FlexDirection.Row}
alignItems={AlignItems.center}
justifyContent={JustifyContent.center}
>
{icon}
</Box>
)}
{image && (
<Box display={Display.Flex} margin="auto" textAlign={TextAlign.Center}>
<img
src={image}
alt={t('srpSecurityQuizImgAlt')}
width="300"
style={{ maxWidth: '100%' }} // should probably be in a className instead
/>
</Box>
)}
<Text
variant={TextVariant.bodyLgMedium}
textAlign={TextAlign.Center}
color={icon?.props.color} // Inherit this text color from the icon's color
>
{content}
</Text>
{moreContent && (
<Text variant={TextVariant.bodyMd} textAlign={TextAlign.Center}>
{moreContent}
</Text>
)}
{buttons.map((btn, idx) => (
<Button
key={idx}
size={btn.size}
onClick={btn.onClick}
label={btn.label}
variant={btn.variant}
width={BlockSize.Full}
data-testid={btn['data-testid']}
>
{btn.label}
</Button>
))}
</>
);
}

View File

@ -0,0 +1 @@
export { default } from './QuizContent';

View File

@ -0,0 +1,35 @@
import React from 'react';
import { StoryFn, Meta } from '@storybook/react';
import { useArgs } from '@storybook/client-api';
import { Button } from '../../../component-library';
import SRPQuiz from '.';
export default {
title: 'Components/App/SRPQuizModal',
component: SRPQuiz,
argTypes: {
isShowingModal: {
control: 'boolean',
},
},
} as Meta<typeof SRPQuiz>;
export const DefaultStory: StoryFn<typeof SRPQuiz> = () => {
const [{ isShowingModal }, updateArgs] = useArgs();
return (
<>
<Button onClick={() => updateArgs({ isShowingModal: true })}>
Open modal
</Button>
{isShowingModal && (
<SRPQuiz
isOpen={isShowingModal}
onClose={() => updateArgs({ isShowingModal: false })}
/>
)}
</>
);
};
DefaultStory.storyName = 'Default';

View File

@ -0,0 +1,95 @@
import { fireEvent, screen, waitFor } from '@testing-library/react';
import React from 'react';
import mockState from '../../../../../test/data/mock-state.json';
import { renderWithProvider } from '../../../../../test/jest';
import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url';
import configureStore from '../../../../store/store';
import { QuizStage } from '../types';
import SRPQuiz from './SRPQuiz';
const store = configureStore({
metamask: {
...mockState.metamask,
},
});
let openTabSpy;
jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');
return {
...original,
useHistory: () => ({
push: jest.fn(),
}),
};
});
async function waitForStage(stage) {
return await waitFor(() => {
expect(screen.getByTestId(`srp_stage_${stage}`)).toBeInTheDocument();
});
}
function clickButton(id) {
fireEvent.click(screen.getByTestId(id));
}
describe('srp-reveal-quiz', () => {
beforeAll(() => {
global.platform = { openTab: jest.fn() };
openTabSpy = jest.spyOn(global.platform, 'openTab');
});
it('should go through the full sequence of steps', async () => {
renderWithProvider(<SRPQuiz isOpen />, store);
expect(screen.queryByTestId('srp-quiz-get-started')).toBeInTheDocument();
expect(
screen.queryByTestId('srp-quiz-right-answer'),
).not.toBeInTheDocument();
clickButton('srp-quiz-learn-more');
await waitFor(() =>
expect(openTabSpy).toHaveBeenCalledWith({
url: expect.stringMatching(ZENDESK_URLS.PASSWORD_AND_SRP_ARTICLE),
}),
);
clickButton('srp-quiz-get-started');
await waitForStage(QuizStage.questionOne);
clickButton('srp-quiz-wrong-answer');
await waitForStage(QuizStage.wrongAnswerQuestionOne);
clickButton('srp-quiz-try-again');
await waitForStage(QuizStage.questionOne);
clickButton('srp-quiz-right-answer');
await waitForStage(QuizStage.rightAnswerQuestionOne);
clickButton('srp-quiz-continue');
await waitForStage(QuizStage.questionTwo);
clickButton('srp-quiz-wrong-answer');
await waitForStage(QuizStage.wrongAnswerQuestionTwo);
clickButton('srp-quiz-try-again');
await waitForStage(QuizStage.questionTwo);
clickButton('srp-quiz-right-answer');
await waitForStage(QuizStage.rightAnswerQuestionTwo);
clickButton('srp-quiz-continue');
});
});

View File

@ -0,0 +1,298 @@
/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports, import/no-commonjs */
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import {
MetaMetricsEventCategory,
MetaMetricsEventKeyType,
MetaMetricsEventName,
} from '../../../../../shared/constants/metametrics';
import { MetaMetricsContext } from '../../../../contexts/metametrics';
import {
BlockSize,
Display,
FlexDirection,
IconColor,
TextAlign,
} from '../../../../helpers/constants/design-system';
import { REVEAL_SEED_ROUTE } from '../../../../helpers/constants/routes';
import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import {
BUTTON_SIZES,
BUTTON_VARIANT,
Icon,
IconName,
IconSize,
Modal,
ModalContent,
ModalHeader,
ModalOverlay,
} from '../../../component-library';
import QuizContent from '../QuizContent';
import { JSXDict, QuizStage } from '../types';
const wrongAnswerIcon = (
<Icon
size={IconSize.Xl}
name={IconName.Warning}
color={IconColor.errorDefault}
textAlign={TextAlign.Center}
width={BlockSize.OneTwelfth}
/>
);
const rightAnswerIcon = (
<Icon
size={IconSize.Xl}
name={IconName.Confirmation}
color={IconColor.successDefault}
textAlign={TextAlign.Center}
width={BlockSize.OneTwelfth}
/>
);
const openSupportArticle = (): void => {
global.platform.openTab({
url: ZENDESK_URLS.PASSWORD_AND_SRP_ARTICLE,
});
};
export default function SRPQuiz(props: any) {
const [stage, setStage] = useState<QuizStage>(QuizStage.introduction);
const trackEvent = useContext(MetaMetricsContext);
const history = useHistory();
const t = useI18nContext();
// This should not be a state variable, because it's derivable from the state variable `stage`
// (Making it a state variable forces the component to render twice)
let title = '';
// Using a dictionary of JSX elements eliminates the need for a switch statement
const stages: JSXDict = {};
stages[QuizStage.introduction] = () => {
title = t('srpSecurityQuizTitle');
return (
<QuizContent
image={'images/reveal-srp.png'}
content={t('srpSecurityQuizIntroduction')}
buttons={[
{
label: t('srpSecurityQuizGetStarted'),
onClick: () => setStage(QuizStage.questionOne),
variant: BUTTON_VARIANT.PRIMARY,
size: BUTTON_SIZES.LG,
'data-testid': 'srp-quiz-get-started',
},
{
label: t('learnMoreUpperCase'),
onClick: openSupportArticle,
variant: BUTTON_VARIANT.LINK,
'data-testid': 'srp-quiz-learn-more',
},
]}
/>
);
};
stages[QuizStage.questionOne] = () => {
title = `1 ${t('ofTextNofM')} 2`;
return (
<QuizContent
content={t('srpSecurityQuizQuestionOneQuestion')}
buttons={[
{
label: t('srpSecurityQuizQuestionOneWrongAnswer'),
onClick: () => setStage(QuizStage.wrongAnswerQuestionOne),
variant: BUTTON_VARIANT.SECONDARY,
size: BUTTON_SIZES.LG,
'data-testid': 'srp-quiz-wrong-answer',
},
{
label: t('srpSecurityQuizQuestionOneRightAnswer'),
onClick: () => setStage(QuizStage.rightAnswerQuestionOne),
variant: BUTTON_VARIANT.SECONDARY,
size: BUTTON_SIZES.LG,
'data-testid': 'srp-quiz-right-answer',
},
{
label: t('learnMoreUpperCase'),
onClick: openSupportArticle,
variant: BUTTON_VARIANT.LINK,
},
]}
/>
);
};
stages[QuizStage.rightAnswerQuestionOne] = () => {
title = `1 ${t('ofTextNofM')} 2`;
return (
<QuizContent
icon={rightAnswerIcon}
content={t('srpSecurityQuizQuestionOneRightAnswerTitle')}
moreContent={t('srpSecurityQuizQuestionOneRightAnswerDescription')}
buttons={[
{
label: t('continue'),
onClick: () => setStage(QuizStage.questionTwo),
variant: BUTTON_VARIANT.PRIMARY,
size: BUTTON_SIZES.LG,
'data-testid': 'srp-quiz-continue',
},
{
label: t('learnMoreUpperCase'),
onClick: openSupportArticle,
variant: BUTTON_VARIANT.LINK,
},
]}
/>
);
};
stages[QuizStage.wrongAnswerQuestionOne] = () => {
title = `1 ${t('ofTextNofM')} 2`;
return (
<QuizContent
icon={wrongAnswerIcon}
content={t('srpSecurityQuizQuestionOneWrongAnswerTitle')}
moreContent={t('srpSecurityQuizQuestionOneWrongAnswerDescription')}
buttons={[
{
label: t('tryAgain'),
onClick: () => setStage(QuizStage.questionOne),
variant: BUTTON_VARIANT.PRIMARY,
size: BUTTON_SIZES.LG,
'data-testid': 'srp-quiz-try-again',
},
{
label: t('learnMoreUpperCase'),
onClick: openSupportArticle,
variant: BUTTON_VARIANT.LINK,
},
]}
/>
);
};
stages[QuizStage.questionTwo] = () => {
title = `2 ${t('ofTextNofM')} 2`;
return (
<QuizContent
content={t('srpSecurityQuizQuestionTwoQuestion')}
buttons={[
{
label: t('srpSecurityQuizQuestionTwoRightAnswer'),
onClick: () => setStage(QuizStage.rightAnswerQuestionTwo),
variant: BUTTON_VARIANT.SECONDARY,
size: BUTTON_SIZES.LG,
'data-testid': 'srp-quiz-right-answer',
},
{
label: t('srpSecurityQuizQuestionTwoWrongAnswer'),
onClick: () => setStage(QuizStage.wrongAnswerQuestionTwo),
variant: BUTTON_VARIANT.SECONDARY,
size: BUTTON_SIZES.LG,
'data-testid': 'srp-quiz-wrong-answer',
},
{
label: t('learnMoreUpperCase'),
onClick: openSupportArticle,
variant: BUTTON_VARIANT.LINK,
},
]}
/>
);
};
stages[QuizStage.rightAnswerQuestionTwo] = () => {
title = `2 ${t('ofTextNofM')} 2`;
return (
<QuizContent
icon={rightAnswerIcon}
content={t('srpSecurityQuizQuestionTwoRightAnswerTitle')}
moreContent={t('srpSecurityQuizQuestionTwoRightAnswerDescription')}
buttons={[
{
label: t('continue'),
onClick: () => history.push(REVEAL_SEED_ROUTE),
variant: BUTTON_VARIANT.PRIMARY,
size: BUTTON_SIZES.LG,
'data-testid': 'srp-quiz-continue',
},
{
label: t('learnMoreUpperCase'),
onClick: openSupportArticle,
variant: BUTTON_VARIANT.LINK,
},
]}
/>
);
};
stages[QuizStage.wrongAnswerQuestionTwo] = () => {
title = `2 ${t('ofTextNofM')} 2`;
return (
<QuizContent
icon={wrongAnswerIcon}
content={t('srpSecurityQuizQuestionTwoWrongAnswerTitle')}
moreContent={t('srpSecurityQuizQuestionTwoWrongAnswerDescription')}
buttons={[
{
label: t('tryAgain'),
onClick: () => setStage(QuizStage.questionTwo),
variant: BUTTON_VARIANT.PRIMARY,
size: BUTTON_SIZES.LG,
'data-testid': 'srp-quiz-try-again',
},
{
label: t('learnMoreUpperCase'),
onClick: openSupportArticle,
variant: BUTTON_VARIANT.LINK,
},
]}
/>
);
};
// trackEvent shortcut specific to the SRP quiz
const trackEventSrp = useCallback((location) => {
trackEvent(
{
category: MetaMetricsEventCategory.Keys,
event: MetaMetricsEventName.KeyExportSelected,
properties: {
key_type: MetaMetricsEventKeyType.Srp,
location,
},
},
{},
);
}, []);
useEffect(() => {
trackEventSrp(`stage_${stage}`); // Call MetaMetrics based on the current stage
}, [stage]); // Only call this when the stage changes
const quizContent = stages[stage](); // Pick the content using the right stage from the JSXDict
return (
<Modal isOpen={props.isOpen} onClose={props.onClose}>
<ModalOverlay />
<ModalContent
modalDialogProps={{
display: Display.Flex,
flexDirection: FlexDirection.Column,
gap: 4,
}}
>
<ModalHeader onClose={props.onClose} data-testid="srp-quiz-header">
{title}
</ModalHeader>
<span data-testid={`srp_stage_${stage}`} />
{quizContent}
</ModalContent>
</Modal>
);
}

View File

@ -0,0 +1 @@
export { default } from './SRPQuiz';

View File

@ -0,0 +1 @@
export { default } from './SRPQuiz';

View File

@ -0,0 +1,40 @@
export enum QuizStage {
introduction = 'introduction',
questionOne = 'question_one',
wrongAnswerQuestionOne = 'wrong_answer_question_one',
rightAnswerQuestionOne = 'right_answer_question_one',
questionTwo = 'question_two',
wrongAnswerQuestionTwo = 'wrong_answer_question_two',
rightAnswerQuestionTwo = 'right_answer_question_two',
}
export interface IQuizInformationProps {
/**
* The icon to display in the modal should use <Icon /> component
*/
icon?: any;
/**
* The image to display in the modal
*/
image?: string;
/**
* The text content to go inside of the <Text /> component
*/
content: string;
/**
* More text content to go inside of the <Text /> component
*/
moreContent?: string;
/**
* Array of <Button /> component props
*/
buttons: {
onClick: () => void;
label: string;
variant: string;
size?: string;
'data-testid'?: string;
}[];
}
export type JSXDict = { [key: string]: () => JSX.Element };

View File

@ -101,6 +101,7 @@ export const AccountListMenu = ({ onClose }) => {
centerTitle
onClose={onClose}
onBack={actionMode === '' ? null : () => setActionMode('')}
className="multichain-account-menu-popover"
>
{actionMode === 'add' ? (
<Box paddingLeft={4} paddingRight={4} paddingBottom={4} paddingTop={0}>
@ -129,7 +130,7 @@ export const AccountListMenu = ({ onClose }) => {
</Box>
) : null}
{actionMode === '' ? (
<Box className="multichain-account-menu">
<Box>
{/* Search box */}
{accounts.length > 1 ? (
<Box
@ -152,13 +153,13 @@ export const AccountListMenu = ({ onClose }) => {
</Box>
) : null}
{/* Account list block */}
<Box className="multichain-account-menu__list">
<Box className="multichain-account-menu-popover__list">
{searchResults.length === 0 && searchQuery !== '' ? (
<Text
paddingLeft={4}
paddingRight={4}
color={TextColor.textMuted}
data-testid="multichain-account-menu-no-results"
data-testid="multichain-account-menu-popover-no-results"
>
{t('noAccountsFound')}
</Text>
@ -208,7 +209,7 @@ export const AccountListMenu = ({ onClose }) => {
});
setActionMode('add');
}}
data-testid="multichain-account-menu-add-account"
data-testid="multichain-account-menu-popover-add-account"
>
{t('addAccount')}
</ButtonLink>

View File

@ -101,7 +101,7 @@ describe('AccountListMenu', () => {
);
expect(filteredListItems).toHaveLength(0);
expect(
getByTestId('multichain-account-menu-no-results'),
getByTestId('multichain-account-menu-popover-no-results'),
).toBeInTheDocument();
});

View File

@ -1,4 +1,8 @@
.multichain-account-menu {
.multichain-account-menu-popover {
.popover-content {
border-radius: 0;
}
&__list {
max-height: 200px;
overflow: auto;

View File

@ -35,6 +35,7 @@ function ExportTextContainer({ text = '', onClickCopy = null }) {
justifyContent={JustifyContent.CENTER}
className="notranslate"
variant={TextVariant.bodyLgMedium}
data-testid="srp_text"
>
{text}
</Text>

View File

@ -0,0 +1,28 @@
import React from 'react';
import LoadingHeartBeat from '.';
export default {
title: 'Components/UI/LoadingHeartBeat',
component: LoadingHeartBeat,
argTypes: {
backgroundColor: {
control: 'text',
},
estimateUsed: {
control: 'text',
},
},
args: {
backgroundColor: 'var(--color-background-default)',
estimateUsed: 'low',
},
};
export const DefaultStory = (args) => (
<div>
<LoadingHeartBeat {...args} />
Text underneath LoadingHeartBeat
</div>
);
DefaultStory.storyName = 'Default';

View File

@ -0,0 +1,31 @@
import React from 'react';
import LoadingIndicator from './loading-indicator';
export default {
title: 'Components/UI/LoadingIndicator',
component: LoadingIndicator,
argTypes: {
isLoading: {
control: 'boolean',
},
alt: {
control: 'text',
},
title: {
control: 'text',
},
children: {
control: 'text',
},
},
args: {
isLoading: true,
alt: '',
title: '',
children: '',
},
};
export const DefaultStory = (args) => <LoadingIndicator {...args} />;
DefaultStory.storyName = 'Default';

View File

@ -1,12 +1,12 @@
import React, { Component, createContext, useMemo } from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { getMessage } from '../helpers/utils/i18n-helper';
import {
getCurrentLocale,
getCurrentLocaleMessages,
getEnLocaleMessages,
} from '../ducks/locale/locale';
import { getMessage } from '../helpers/utils/i18n-helper';
export const I18nContext = createContext((key) => `[${key}]`);

View File

@ -607,6 +607,10 @@
align-items: center;
padding: 0 8px;
cursor: pointer;
/* Prevents the contents of the asset from going outside the dropdown area */
max-width: 100%;
overflow: hidden;
}
&__asset-icon {
@ -620,6 +624,9 @@
flex-flow: column nowrap;
margin-left: 8px;
flex-grow: 1;
/* Keeps the down arrow on screen */
overflow: hidden;
}
@include screen-sm-max {
@ -640,8 +647,11 @@
&__name {
@include H7;
/* Prevents the token balance or NFT title from wrapping or silently truncating */
display: flex;
flex-flow: row nowrap;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&__label {
margin-right: 0.25rem;

View File

@ -20,12 +20,10 @@ const ZENDESK_URLS = {
LEGACY_WEB3: 'https://metamask.zendesk.com/hc/en-us/articles/360053147012',
NFT_TOKENS:
'https://metamask.zendesk.com/hc/en-us/articles/360058238591-NFT-tokens-in-MetaMask-wallet',
PASSWORD_ARTICLE:
PASSWORD_AND_SRP_ARTICLE:
'https://metamask.zendesk.com/hc/en-us/articles/4404722782107',
SECRET_RECOVERY_PHRASE:
'https://metamask.zendesk.com/hc/en-us/articles/360060826432-What-is-a-Secret-Recovery-Phrase-and-how-to-keep-your-crypto-wallet-secure',
SECRET_RECOVERY_PHRASE_USER_GUIDE:
'https://metamask.zendesk.com/hc/en-us/articles/4404722782107-User-guide-Secret-Recovery-Phrase-password-and-private-keys',
NON_CUSTODIAL_WALLET:
'https://metamask.zendesk.com/hc/en-us/articles/360059952212-MetaMask-is-a-non-custodial-wallet',
SPEEDUP_CANCEL:

View File

@ -1,64 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`i18n helper getMessage should return the correct message when a single react substitution is made 1`] = `
exports[`I18N Helper getMessage renders substitutions inside span if substitutions include React components 1`] = `
<div>
<span>
Testing a react substitution
<div
style="color: red;"
>
TEST_SUBSTITUTION_1
</div>
.
</span>
</div>
`;
exports[`i18n helper getMessage should return the correct message when substituting a mix of react elements and strings 1`] = `
<div>
<span>
Testing a mix
TEST_SUBSTITUTION_1
of react substitutions
<div
style="color: orange;"
>
TEST_SUBSTITUTION_3
a1
</div>
and string substitutions
TEST_SUBSTITUTION_2
+
<div
style="color: pink;"
>
TEST_SUBSTITUTION_4
b2
</div>
.
</span>
</div>
`;
exports[`i18n helper getMessage should return the correct message when two react substitutions are made 1`] = `
<div>
<span>
Testing a react substitution
<div
style="color: red;"
>
TEST_SUBSTITUTION_1
</div>
and another
<div
style="color: blue;"
>
TEST_SUBSTITUTION_2
</div>
.
c3
</span>
</div>

View File

@ -1,169 +1,93 @@
import React from 'react';
import * as Sentry from '@sentry/browser';
import { getMessage as getMessageShared } from '../../../shared/modules/i18n';
import { renderWithProvider } from '../../../test/lib/render-helpers';
import { getMessage } from './i18n-helper';
describe('i18n helper', () => {
const TEST_LOCALE_CODE = 'TEST_LOCALE_CODE';
jest.mock('../../../shared/modules/i18n');
jest.mock('@sentry/browser');
const TEST_KEY_1 = 'TEST_KEY_1';
const TEST_KEY_2 = 'TEST_KEY_2';
const TEST_KEY_3 = 'TEST_KEY_3';
const TEST_KEY_4 = 'TEST_KEY_4';
const TEST_KEY_5 = 'TEST_KEY_5';
const TEST_KEY_6 = 'TEST_KEY_6';
const TEST_KEY_6_HELPER = 'TEST_KEY_6_HELPER';
const TEST_KEY_7 = 'TEST_KEY_7';
const TEST_KEY_7_HELPER_1 = 'TEST_KEY_7_HELPER_1';
const TEST_KEY_7_HELPER_2 = 'TEST_KEY_7_HELPER_2';
const TEST_KEY_8 = 'TEST_KEY_8';
const TEST_KEY_8_HELPER_1 = 'TEST_KEY_8_HELPER_1';
const TEST_KEY_8_HELPER_2 = 'TEST_KEY_8_HELPER_2';
const localeCodeMock = 'te';
const keyMock = 'testKey';
const localeMessagesMock = { [keyMock]: { message: 'testMessage' } };
const errorMock = new Error('testError');
const messageMock = 'testMessage';
const TEST_SUBSTITUTION_1 = 'TEST_SUBSTITUTION_1';
const TEST_SUBSTITUTION_2 = 'TEST_SUBSTITUTION_2';
const TEST_SUBSTITUTION_3 = 'TEST_SUBSTITUTION_3';
const TEST_SUBSTITUTION_4 = 'TEST_SUBSTITUTION_4';
const TEST_SUBSTITUTION_5 = 'TEST_SUBSTITUTION_5';
const testLocaleMessages = {
[TEST_KEY_1]: {
message: 'This is a simple message.',
expectedResult: 'This is a simple message.',
},
[TEST_KEY_2]: {
message: 'This is a message with a single non-react substitution $1.',
},
[TEST_KEY_3]: {
message: 'This is a message with two non-react substitutions $1 and $2.',
},
[TEST_KEY_4]: {
message: '$1 - $2 - $3 - $4 - $5',
},
[TEST_KEY_5]: {
message: '$1 - $2 - $3',
},
[TEST_KEY_6]: {
message: 'Testing a react substitution $1.',
},
[TEST_KEY_6_HELPER]: {
message: TEST_SUBSTITUTION_1,
},
[TEST_KEY_7]: {
message: 'Testing a react substitution $1 and another $2.',
},
[TEST_KEY_7_HELPER_1]: {
message: TEST_SUBSTITUTION_1,
},
[TEST_KEY_7_HELPER_2]: {
message: TEST_SUBSTITUTION_2,
},
[TEST_KEY_8]: {
message:
'Testing a mix $1 of react substitutions $2 and string substitutions $3 + $4.',
},
[TEST_KEY_8_HELPER_1]: {
message: TEST_SUBSTITUTION_3,
},
[TEST_KEY_8_HELPER_2]: {
message: TEST_SUBSTITUTION_4,
},
};
const t = getMessage.bind(null, TEST_LOCALE_CODE, testLocaleMessages);
const TEST_SUBSTITUTION_6 = (
<div style={{ color: 'red' }} key="test-react-substitutions-1">
{t(TEST_KEY_6_HELPER)}
</div>
);
const TEST_SUBSTITUTION_7_1 = (
<div style={{ color: 'red' }} key="test-react-substitutions-7-1">
{t(TEST_KEY_7_HELPER_1)}
</div>
);
const TEST_SUBSTITUTION_7_2 = (
<div style={{ color: 'blue' }} key="test-react-substitutions-7-2">
{t(TEST_KEY_7_HELPER_2)}
</div>
);
const TEST_SUBSTITUTION_8_1 = (
<div style={{ color: 'orange' }} key="test-react-substitutions-8-1">
{t(TEST_KEY_8_HELPER_1)}
</div>
);
const TEST_SUBSTITUTION_8_2 = (
<div style={{ color: 'pink' }} key="test-react-substitutions-1">
{t(TEST_KEY_8_HELPER_2)}
</div>
);
describe('I18N Helper', () => {
beforeEach(() => {
jest.resetAllMocks();
});
describe('getMessage', () => {
it('should return the exact message paired with key if there are no substitutions', () => {
const result = t(TEST_KEY_1);
expect(result).toStrictEqual('This is a simple message.');
});
it('returns value from getMessage in shared module', () => {
getMessageShared.mockReturnValue(messageMock);
it('should return the correct message when a single non-react substitution is made', () => {
const result = t(TEST_KEY_2, [TEST_SUBSTITUTION_1]);
expect(result).toStrictEqual(
`This is a message with a single non-react substitution ${TEST_SUBSTITUTION_1}.`,
expect(
getMessage(localeCodeMock, localeMessagesMock, keyMock),
).toStrictEqual(messageMock);
expect(getMessageShared).toHaveBeenCalledTimes(1);
expect(getMessageShared).toHaveBeenCalledWith(
localeCodeMock,
localeMessagesMock,
keyMock,
undefined,
expect.any(Function),
undefined,
);
});
it('should return the correct message when two non-react substitutions are made', () => {
const result = t(TEST_KEY_3, [TEST_SUBSTITUTION_1, TEST_SUBSTITUTION_2]);
expect(result).toStrictEqual(
`This is a message with two non-react substitutions ${TEST_SUBSTITUTION_1} and ${TEST_SUBSTITUTION_2}.`,
it('invokes getMessage from shared module with onError callback that logs Sentry exception', () => {
getMessage(localeCodeMock, localeMessagesMock, keyMock);
const onErrorCallback = getMessageShared.mock.calls[0][4];
onErrorCallback(errorMock);
expect(Sentry.captureException).toHaveBeenCalledTimes(1);
expect(Sentry.captureException).toHaveBeenCalledWith(errorMock);
});
it('does not provide custom join logic if only strings in substitutions', () => {
getMessage(localeCodeMock, localeMessagesMock, keyMock, ['a1', 'a2']);
expect(getMessageShared).toHaveBeenCalledTimes(1);
expect(getMessageShared).toHaveBeenCalledWith(
localeCodeMock,
localeMessagesMock,
keyMock,
['a1', 'a2'],
expect.any(Function),
undefined,
);
});
it('should return the correct message when multiple non-react substitutions are made', () => {
const result = t(TEST_KEY_4, [
TEST_SUBSTITUTION_1,
TEST_SUBSTITUTION_2,
TEST_SUBSTITUTION_3,
TEST_SUBSTITUTION_4,
TEST_SUBSTITUTION_5,
]);
expect(result).toStrictEqual(
`${TEST_SUBSTITUTION_1} - ${TEST_SUBSTITUTION_2} - ${TEST_SUBSTITUTION_3} - ${TEST_SUBSTITUTION_4} - ${TEST_SUBSTITUTION_5}`,
it('renders substitutions inside span if substitutions include React components', () => {
const substitution1 = (
<div style={{ color: 'orange' }} key="substitution-1">
a1
</div>
);
});
it('should correctly render falsy substitutions', () => {
const result = t(TEST_KEY_4, [0, -0, '', false, NaN]);
expect(result).toStrictEqual('0 - 0 - - false - NaN');
});
const substitution2 = (
<div style={{ color: 'pink' }} key="substitution-2">
b2
</div>
);
it('should render nothing for "null" and "undefined" substitutions', () => {
const result = t(TEST_KEY_5, [null, TEST_SUBSTITUTION_2]);
expect(result).toStrictEqual(` - ${TEST_SUBSTITUTION_2} - `);
});
const substitution3 = 'c3';
it('should return the correct message when a single react substitution is made', () => {
const result = t(TEST_KEY_6, [TEST_SUBSTITUTION_6]);
const { container } = renderWithProvider(result);
expect(container).toMatchSnapshot();
});
it('should return the correct message when two react substitutions are made', () => {
const result = t(TEST_KEY_7, [
TEST_SUBSTITUTION_7_1,
TEST_SUBSTITUTION_7_2,
getMessage(localeCodeMock, localeMessagesMock, keyMock, [
substitution1,
substitution2,
substitution3,
]);
const { container } = renderWithProvider(result);
const joinCallback = getMessageShared.mock.calls[0][5];
expect(container).toMatchSnapshot();
});
it('should return the correct message when substituting a mix of react elements and strings', () => {
const result = t(TEST_KEY_8, [
TEST_SUBSTITUTION_1,
TEST_SUBSTITUTION_8_1,
TEST_SUBSTITUTION_2,
TEST_SUBSTITUTION_8_2,
const result = joinCallback([
substitution1,
substitution2,
substitution3,
]);
const { container } = renderWithProvider(result);

View File

@ -1,46 +1,12 @@
// cross-browser connection to extension i18n API
import React from 'react';
import log from 'loglevel';
import { Json } from '@metamask/utils';
import * as Sentry from '@sentry/browser';
import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout';
import {
I18NMessageDict,
I18NSubstitution,
getMessage as getMessageShared,
} from '../../../shared/modules/i18n';
import { NETWORK_TYPES } from '../../../shared/constants/network';
const fetchWithTimeout = getFetchWithTimeout();
// From app/_locales folders there is a messages.json file such as app/_locales/en, comes with key and translated results
// and we use as t('reject') to get the translated message in the codebase
// and in i18n lib, the translated message is an object (I18NMessage) with message & description -
// message is the string that will replace the translationKey, and that message may contain replacement variables such as $1, $2, etc.
// Description is key describing the usage of the message.
interface I18NMessage {
message: string;
description?: string;
}
// The overall translation file is made of same entries
// translationKey (string) and the I18NMessage as the value.
interface I18NMessageDict {
[translationKey: string]: I18NMessage;
}
// A parameterized type (or generic type) of maps that use the same structure (translationKey) key
interface I18NMessageDictMap<R> {
[translationKey: string]: R;
}
const warned: { [localeCode: string]: I18NMessageDictMap<boolean> } = {};
const missingMessageErrors: I18NMessageDictMap<Error> = {};
const missingSubstitutionErrors: {
[localeCode: string]: I18NMessageDictMap<boolean>;
} = {};
function getHasSubstitutions(
substitutions?: string[],
): substitutions is string[] {
return (substitutions?.length ?? 0) > 0;
}
/**
* Returns a localized message for the given key
*
@ -56,116 +22,29 @@ export const getMessage = (
key: string,
substitutions?: string[],
): JSX.Element | string | null => {
if (!localeMessages) {
return null;
}
if (!localeMessages[key]) {
if (localeCode === 'en') {
if (!missingMessageErrors[key]) {
missingMessageErrors[key] = new Error(
`Unable to find value of key "${key}" for locale "${localeCode}"`,
);
Sentry.captureException(missingMessageErrors[key]);
log.error(missingMessageErrors[key]);
if (process.env.IN_TEST) {
throw missingMessageErrors[key];
}
}
} else if (!warned[localeCode] || !warned[localeCode][key]) {
if (!warned[localeCode]) {
warned[localeCode] = {};
}
warned[localeCode][key] = true;
log.warn(
`Translator - Unable to find value of key "${key}" for locale "${localeCode}"`,
);
}
return null;
}
const hasSubstitutions = getHasSubstitutions(substitutions);
const hasReactSubstitutions =
hasSubstitutions &&
substitutions?.some(
(element) =>
element !== null &&
(typeof element === 'function' || typeof element === 'object'),
);
const entry = localeMessages[key];
const phrase = entry.message;
// perform substitutions
if (hasSubstitutions) {
const parts = phrase.split(/(\$\d)/gu);
const substitutedParts = parts.map((part: string) => {
const subMatch = part.match(/\$(\d)/u);
if (!subMatch) {
return part;
}
const substituteIndex = Number(subMatch[1]) - 1;
if (
(substitutions[substituteIndex] === null ||
substitutions[substituteIndex] === undefined) &&
!missingSubstitutionErrors[localeCode]?.[key]
) {
if (!missingSubstitutionErrors[localeCode]) {
missingSubstitutionErrors[localeCode] = {};
}
missingSubstitutionErrors[localeCode][key] = true;
const error = new Error(
`Insufficient number of substitutions for key "${key}" with locale "${localeCode}"`,
);
log.error(error);
Sentry.captureException(error);
}
return substitutions?.[substituteIndex];
});
return hasReactSubstitutions ? (
<span> {substitutedParts} </span>
) : (
substitutedParts.join('')
);
}
return phrase;
};
export async function fetchLocale(
localeCode: string,
): Promise<I18NMessageDict> {
try {
const response = await fetchWithTimeout(
`./_locales/${localeCode}/messages.json`,
);
return await response.json();
} catch (error) {
log.error(`failed to fetch ${localeCode} locale because of ${error}`);
return {};
}
}
const relativeTimeFormatLocaleData = new Set();
export async function loadRelativeTimeFormatLocaleData(
localeCode: string,
): Promise<void> {
const languageTag = localeCode.split('_')[0];
if (
Intl.RelativeTimeFormat &&
typeof (Intl.RelativeTimeFormat as any).__addLocaleData === 'function' &&
!relativeTimeFormatLocaleData.has(languageTag)
) {
const localeData = await fetchRelativeTimeFormatData(languageTag);
(Intl.RelativeTimeFormat as any).__addLocaleData(localeData);
}
}
async function fetchRelativeTimeFormatData(languageTag: string): Promise<Json> {
const response = await fetchWithTimeout(
`./intl/${languageTag}/relative-time-format-data.json`,
const hasReactSubstitutions = substitutions?.some(
(element) =>
element !== null &&
(typeof element === 'function' || typeof element === 'object'),
);
return await response.json();
}
const join = hasReactSubstitutions
? (parts: I18NSubstitution[]) => <span> {parts} </span>
: undefined;
const onError = (error: Error) => {
Sentry.captureException(error);
};
return getMessageShared(
localeCode,
localeMessages,
key,
substitutions,
onError,
join,
);
};
export function getNetworkLabelKey(network: string): string {
if (network === NETWORK_TYPES.LINEA_GOERLI) {

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