mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 01:47:00 +01:00
Add friendly error handling when background throws an error before listening for connection (#14461)
* When background port closes, UI should display a user friendly error. Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Remove console.log Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> A couple of fixes 1. Use timeout in metaRPCClientFactory to check if UI can't communicate with bg 2. Refactor locale setup 3. Fixed wording/capitalization 4. Fix locales usage so that linting works 5. Refactor CSS Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> do not simulate errorwq Refactor loading css Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Remove the onDisconnect event handler in ui as this is handled in metarpcclientfactory Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Do not throw in bg Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Fix PR comments Remove unused message 'failedToLoadMessage' Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Move usage of locales to shared/** so that linter can see it. Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Do not simulate error. Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> metarpc can handle multiple requests, responseHandled should be a map. Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> reload metamask button on critical error Use metamask state (if available) to the locale, else read locale files manually. Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> use constant and numeric separator Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> refactor error utils remove error simulation Memoize setupLocale function Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> test cases Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Do not simulate error Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> 1. store should be metamask state 2. code refactorings. Tests: mock setupLocale Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Mock fetchLocale instead Test setup locale Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> UI/CSS changes. Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Do not simulate failure Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> * spell MetaMask correctly Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> * Rename state to mockStore Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> * we should clean up this.responseHandled[id] in the error case. Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> * Fixed PR comments. Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> * clean up response handled. Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com>
This commit is contained in:
parent
2e1a37b47d
commit
4fa4930c8a
12
app/_locales/en/messages.json
generated
12
app/_locales/en/messages.json
generated
@ -2652,6 +2652,9 @@
|
|||||||
"resetWalletWarning": {
|
"resetWalletWarning": {
|
||||||
"message": "Make sure you’re using the correct Secret Recovery Phrase before proceeding. You will not be able to undo this."
|
"message": "Make sure you’re using the correct Secret Recovery Phrase before proceeding. You will not be able to undo this."
|
||||||
},
|
},
|
||||||
|
"restartMetamask": {
|
||||||
|
"message": "Restart MetaMask"
|
||||||
|
},
|
||||||
"restore": {
|
"restore": {
|
||||||
"message": "Restore"
|
"message": "Restore"
|
||||||
},
|
},
|
||||||
@ -2836,6 +2839,9 @@
|
|||||||
"sendAmount": {
|
"sendAmount": {
|
||||||
"message": "Send Amount"
|
"message": "Send Amount"
|
||||||
},
|
},
|
||||||
|
"sendBugReport": {
|
||||||
|
"message": "Send us a bug report."
|
||||||
|
},
|
||||||
"sendSpecifiedTokens": {
|
"sendSpecifiedTokens": {
|
||||||
"message": "Send $1",
|
"message": "Send $1",
|
||||||
"description": "Symbol of the specified token"
|
"description": "Symbol of the specified token"
|
||||||
@ -3109,6 +3115,9 @@
|
|||||||
"message": "Connect your wallet directly to your computer. Unlock your Ledger and open the Ethereum app. For more on using your hardware wallet device, $1.",
|
"message": "Connect your wallet directly to your computer. Unlock your Ledger and open the Ethereum app. For more on using your hardware wallet device, $1.",
|
||||||
"description": "$1 represents the `hardwareWalletSupportLinkConversion` localization key"
|
"description": "$1 represents the `hardwareWalletSupportLinkConversion` localization key"
|
||||||
},
|
},
|
||||||
|
"stillGettingMessage": {
|
||||||
|
"message": "Still getting this message?"
|
||||||
|
},
|
||||||
"storePhrase": {
|
"storePhrase": {
|
||||||
"message": "Store this phrase in a password manager like 1Password."
|
"message": "Store this phrase in a password manager like 1Password."
|
||||||
},
|
},
|
||||||
@ -3802,6 +3811,9 @@
|
|||||||
"message": "We had trouble connecting to your $1, try reviewing $2 and try again.",
|
"message": "We had trouble connecting to your $1, try reviewing $2 and try again.",
|
||||||
"description": "$1 is the wallet device name; $2 is a link to wallet connection guide"
|
"description": "$1 is the wallet device name; $2 is a link to wallet connection guide"
|
||||||
},
|
},
|
||||||
|
"troubleStarting": {
|
||||||
|
"message": "MetaMask had trouble starting. This error could be intermittent, so try restarting the extension."
|
||||||
|
},
|
||||||
"troubleTokenBalances": {
|
"troubleTokenBalances": {
|
||||||
"message": "We had trouble loading your token balances. You can view them ",
|
"message": "We had trouble loading your token balances. You can view them ",
|
||||||
"description": "Followed by a link (here) to view token balances"
|
"description": "Followed by a link (here) to view token balances"
|
||||||
|
@ -8,7 +8,10 @@
|
|||||||
<link rel="stylesheet" type="text/css" href="./index-rtl.css" title="rtl" disabled>
|
<link rel="stylesheet" type="text/css" href="./index-rtl.css" title="rtl" disabled>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app-content"><div id="app-loader">Loading...</div></div>
|
<div id="app-content">
|
||||||
|
<img class="loading-logo" src="./images/logo/metamask-fox.svg" alt="" />
|
||||||
|
<img class="loading-spinner" src="./images/spinner.gif" alt="" />
|
||||||
|
</div>
|
||||||
<div id="popover-content"></div>
|
<div id="popover-content"></div>
|
||||||
<script src="./globalthis.js" type="text/javascript" charset="utf-8"></script>
|
<script src="./globalthis.js" type="text/javascript" charset="utf-8"></script>
|
||||||
<script src="./sentry-install.js" type="text/javascript" charset="utf-8"></script>
|
<script src="./sentry-install.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
@ -8,7 +8,10 @@
|
|||||||
<link rel="stylesheet" type="text/css" href="./index-rtl.css" title="rtl" disabled>
|
<link rel="stylesheet" type="text/css" href="./index-rtl.css" title="rtl" disabled>
|
||||||
</head>
|
</head>
|
||||||
<body style="width:357px; height:600px;">
|
<body style="width:357px; height:600px;">
|
||||||
<div id="app-content"></div>
|
<div id="app-content">
|
||||||
|
<img class="loading-logo" src="./images/logo/metamask-fox.svg" alt="" />
|
||||||
|
<img class="loading-spinner" src="./images/spinner.gif" alt="" />
|
||||||
|
</div>
|
||||||
<div id="popover-content"></div>
|
<div id="popover-content"></div>
|
||||||
<script src="./globalthis.js" type="text/javascript" charset="utf-8"></script>
|
<script src="./globalthis.js" type="text/javascript" charset="utf-8"></script>
|
||||||
<script src="./sentry-install.js" type="text/javascript" charset="utf-8"></script>
|
<script src="./sentry-install.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { EthereumRpcError } from 'eth-rpc-errors';
|
import { EthereumRpcError } from 'eth-rpc-errors';
|
||||||
import SafeEventEmitter from 'safe-event-emitter';
|
import SafeEventEmitter from 'safe-event-emitter';
|
||||||
import createRandomId from '../../../shared/modules/random-id';
|
import createRandomId from '../../../shared/modules/random-id';
|
||||||
|
import { TEN_SECONDS_IN_MILLISECONDS } from '../../../ui/helpers/constants/critical-error';
|
||||||
|
|
||||||
class MetaRPCClient {
|
class MetaRPCClient {
|
||||||
constructor(connectionStream) {
|
constructor(connectionStream) {
|
||||||
@ -10,6 +11,23 @@ class MetaRPCClient {
|
|||||||
this.requests = new Map();
|
this.requests = new Map();
|
||||||
this.connectionStream.on('data', this.handleResponse.bind(this));
|
this.connectionStream.on('data', this.handleResponse.bind(this));
|
||||||
this.connectionStream.on('end', this.close.bind(this));
|
this.connectionStream.on('end', this.close.bind(this));
|
||||||
|
this.responseHandled = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
send(id, payload, cb) {
|
||||||
|
this.requests.set(id, cb);
|
||||||
|
this.connectionStream.write(payload);
|
||||||
|
this.responseHandled[id] = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.responseHandled[id] && cb) {
|
||||||
|
delete this.responseHandled[id];
|
||||||
|
return cb(new Error('No response from RPC'), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.responseHandled[id];
|
||||||
|
// needed for linter to pass
|
||||||
|
return true;
|
||||||
|
}, TEN_SECONDS_IN_MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
onNotification(handler) {
|
onNotification(handler) {
|
||||||
@ -34,6 +52,8 @@ class MetaRPCClient {
|
|||||||
const isNotification = id === undefined && error === undefined;
|
const isNotification = id === undefined && error === undefined;
|
||||||
const cb = this.requests.get(id);
|
const cb = this.requests.get(id);
|
||||||
|
|
||||||
|
this.responseHandled[id] = true;
|
||||||
|
|
||||||
if (method && params && !isNotification) {
|
if (method && params && !isNotification) {
|
||||||
// dont handle server-side to client-side requests
|
// dont handle server-side to client-side requests
|
||||||
return;
|
return;
|
||||||
@ -79,14 +99,13 @@ const metaRPCClientFactory = (connectionStream) => {
|
|||||||
const cb = p[p.length - 1];
|
const cb = p[p.length - 1];
|
||||||
const params = p.slice(0, -1);
|
const params = p.slice(0, -1);
|
||||||
const id = createRandomId();
|
const id = createRandomId();
|
||||||
|
const payload = {
|
||||||
object.requests.set(id, cb);
|
|
||||||
object.connectionStream.write({
|
|
||||||
jsonrpc: '2.0',
|
jsonrpc: '2.0',
|
||||||
method: property,
|
method: property,
|
||||||
params,
|
params,
|
||||||
id,
|
id,
|
||||||
});
|
};
|
||||||
|
object.send(id, payload, cb);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -131,4 +131,21 @@ describe('metaRPCClientFactory', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should be able to handle no message within TIMEOUT secs', async () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
const streamTest = createThoughStream();
|
||||||
|
const metaRPCClient = metaRPCClientFactory(streamTest);
|
||||||
|
|
||||||
|
const errorPromise = new Promise((_resolve, reject) =>
|
||||||
|
metaRPCClient.foo('bad', (error, _) => {
|
||||||
|
reject(error);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
await expect(errorPromise).rejects.toThrow('No response from RPC');
|
||||||
|
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -17,6 +17,8 @@ import {
|
|||||||
ENVIRONMENT_TYPE_POPUP,
|
ENVIRONMENT_TYPE_POPUP,
|
||||||
} from '../../shared/constants/app';
|
} from '../../shared/constants/app';
|
||||||
import { isManifestV3 } from '../../shared/modules/mv3.utils';
|
import { isManifestV3 } from '../../shared/modules/mv3.utils';
|
||||||
|
import { SUPPORT_LINK } from '../../ui/helpers/constants/common';
|
||||||
|
import { getErrorHtml } from '../../ui/helpers/utils/error-utils';
|
||||||
import ExtensionPlatform from './platforms/extension';
|
import ExtensionPlatform from './platforms/extension';
|
||||||
import { setupMultiplex } from './lib/stream-utils';
|
import { setupMultiplex } from './lib/stream-utils';
|
||||||
import { getEnvironmentType } from './lib/util';
|
import { getEnvironmentType } from './lib/util';
|
||||||
@ -25,6 +27,21 @@ import metaRPCClientFactory from './lib/metaRPCClientFactory';
|
|||||||
start().catch(log.error);
|
start().catch(log.error);
|
||||||
|
|
||||||
async function start() {
|
async function start() {
|
||||||
|
async function displayCriticalError(container, err, metamaskState) {
|
||||||
|
const html = await getErrorHtml(SUPPORT_LINK, metamaskState);
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
|
||||||
|
const button = document.getElementById('critical-error-button');
|
||||||
|
|
||||||
|
button.addEventListener('click', (_) => {
|
||||||
|
browser.runtime.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
log.error(err.stack);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
// create platform global
|
// create platform global
|
||||||
global.platform = new ExtensionPlatform();
|
global.platform = new ExtensionPlatform();
|
||||||
|
|
||||||
@ -33,6 +50,7 @@ async function start() {
|
|||||||
|
|
||||||
// setup stream to background
|
// setup stream to background
|
||||||
const extensionPort = browser.runtime.connect({ name: windowType });
|
const extensionPort = browser.runtime.connect({ name: windowType });
|
||||||
|
|
||||||
const connectionStream = new PortStream(extensionPort);
|
const connectionStream = new PortStream(extensionPort);
|
||||||
|
|
||||||
const activeTab = await queryCurrentActiveTab(windowType);
|
const activeTab = await queryCurrentActiveTab(windowType);
|
||||||
@ -51,19 +69,12 @@ async function start() {
|
|||||||
initializeUiWithTab(activeTab);
|
initializeUiWithTab(activeTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayCriticalError(container, err) {
|
|
||||||
container.innerHTML =
|
|
||||||
'<div class="critical-error">The MetaMask app failed to load: please open and close MetaMask again to restart.</div>';
|
|
||||||
container.style.height = '80px';
|
|
||||||
log.error(err.stack);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeUiWithTab(tab) {
|
function initializeUiWithTab(tab) {
|
||||||
const container = document.getElementById('app-content');
|
const container = document.getElementById('app-content');
|
||||||
initializeUi(tab, container, connectionStream, (err, store) => {
|
initializeUi(tab, container, connectionStream, (err, store) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
displayCriticalError(container, err);
|
// if there's an error, store will be = metamaskState
|
||||||
|
displayCriticalError(container, err, store);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +115,7 @@ async function queryCurrentActiveTab(windowType) {
|
|||||||
function initializeUi(activeTab, container, connectionStream, cb) {
|
function initializeUi(activeTab, container, connectionStream, cb) {
|
||||||
connectToAccountManager(connectionStream, (err, backgroundConnection) => {
|
connectToAccountManager(connectionStream, (err, backgroundConnection) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
cb(err);
|
cb(err, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
44
ui/css/errors.scss
Normal file
44
ui/css/errors.scss
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
.critical-error {
|
||||||
|
padding: 16px;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
&__alert {
|
||||||
|
color: var(--color-text-default);
|
||||||
|
background-color: var(--color-error-muted);
|
||||||
|
border: 1px solid var(--color-error-default);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
&__message {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
background-color: var(--color-primary-default);
|
||||||
|
color: var(--color-primary-inverse);
|
||||||
|
border: 1px solid var(--color-primary-default);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__paragraph {
|
||||||
|
color: var(--color-text-default);
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&__link {
|
||||||
|
color: var(--color-primary-default);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-primary-alternative);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,8 @@
|
|||||||
@import '../components/app/app-components';
|
@import '../components/app/app-components';
|
||||||
@import '../components/ui/ui-components';
|
@import '../components/ui/ui-components';
|
||||||
@import '../pages/pages';
|
@import '../pages/pages';
|
||||||
|
@import './errors.scss';
|
||||||
|
@import './loading.scss';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
ITCSS
|
ITCSS
|
||||||
|
13
ui/css/loading.scss
Normal file
13
ui/css/loading.scss
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.loading-logo {
|
||||||
|
width: 10rem;
|
||||||
|
height: 10rem;
|
||||||
|
align-self: center;
|
||||||
|
margin: 10rem 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
align-self: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
1
ui/helpers/constants/critical-error.js
Normal file
1
ui/helpers/constants/critical-error.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const TEN_SECONDS_IN_MILLISECONDS = 10_000;
|
55
ui/helpers/utils/error-utils.js
Normal file
55
ui/helpers/utils/error-utils.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import getFirstPreferredLangCode from '../../../app/scripts/lib/get-first-preferred-lang-code';
|
||||||
|
import { setupLocale } from '../..';
|
||||||
|
import switchDirection from './switch-direction';
|
||||||
|
|
||||||
|
const getLocaleContext = (currentLocaleMessages, enLocaleMessages) => {
|
||||||
|
return (key) => {
|
||||||
|
let message = currentLocaleMessages[key]?.message;
|
||||||
|
if (!message && enLocaleMessages[key]) {
|
||||||
|
message = enLocaleMessages[key].message;
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getErrorHtml(supportLink, metamaskState) {
|
||||||
|
let response, preferredLocale;
|
||||||
|
if (metamaskState?.currentLocale) {
|
||||||
|
preferredLocale = metamaskState.currentLocale;
|
||||||
|
response = await setupLocale(metamaskState.currentLocale);
|
||||||
|
} else {
|
||||||
|
preferredLocale = await getFirstPreferredLangCode();
|
||||||
|
response = await setupLocale(preferredLocale);
|
||||||
|
}
|
||||||
|
|
||||||
|
const textDirection = ['ar', 'dv', 'fa', 'he', 'ku'].includes(preferredLocale)
|
||||||
|
? 'rtl'
|
||||||
|
: 'auto';
|
||||||
|
|
||||||
|
switchDirection(textDirection);
|
||||||
|
const { currentLocaleMessages, enLocaleMessages } = response;
|
||||||
|
const t = getLocaleContext(currentLocaleMessages, enLocaleMessages);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="critical-error">
|
||||||
|
<div class="critical-error__alert">
|
||||||
|
<p class="critical-error__alert__message">
|
||||||
|
${t('troubleStarting')}
|
||||||
|
</p>
|
||||||
|
<button id='critical-error-button' class="critical-error__alert__button">
|
||||||
|
${t('restartMetamask')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="critical-error__paragraph">
|
||||||
|
${t('stillGettingMessage')}
|
||||||
|
<a
|
||||||
|
href=${supportLink}
|
||||||
|
class="critical-error__paragraph__link"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
|
${t('sendBugReport')}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
49
ui/helpers/utils/error-utils.test.js
Normal file
49
ui/helpers/utils/error-utils.test.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { SUPPORT_LINK } from '../constants/common';
|
||||||
|
import { getErrorHtml } from './error-utils';
|
||||||
|
import { fetchLocale } from './i18n-helper';
|
||||||
|
|
||||||
|
jest.mock('./i18n-helper', () => ({
|
||||||
|
fetchLocale: jest.fn(),
|
||||||
|
loadRelativeTimeFormatLocaleData: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Error utils Tests', () => {
|
||||||
|
it('should get error html', async () => {
|
||||||
|
const mockStore = {
|
||||||
|
localeMessages: {
|
||||||
|
current: {
|
||||||
|
troubleStarting: {
|
||||||
|
message:
|
||||||
|
'MetaMask had trouble starting. This error could be intermittent, so try restarting the extension.',
|
||||||
|
},
|
||||||
|
restartMetamask: {
|
||||||
|
message: 'Restart MetaMask',
|
||||||
|
},
|
||||||
|
stillGettingMessage: {
|
||||||
|
message: 'Still getting this message?',
|
||||||
|
},
|
||||||
|
sendBugReport: {
|
||||||
|
message: 'Send us a bug report.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
metamask: {
|
||||||
|
currentLocale: 'en',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchLocale.mockReturnValue(mockStore.localeMessages.current);
|
||||||
|
const errorHtml = await getErrorHtml(SUPPORT_LINK, mockStore.metamask);
|
||||||
|
const currentLocale = mockStore.localeMessages.current;
|
||||||
|
const troubleStartingMessage = currentLocale.troubleStarting.message;
|
||||||
|
const restartMetamaskMessage = currentLocale.restartMetamask.message;
|
||||||
|
const stillGettingMessageMessage =
|
||||||
|
currentLocale.stillGettingMessage.message;
|
||||||
|
const sendBugReportMessage = currentLocale.sendBugReport.message;
|
||||||
|
|
||||||
|
expect(errorHtml).toContain(troubleStartingMessage);
|
||||||
|
expect(errorHtml).toContain(restartMetamaskMessage);
|
||||||
|
expect(errorHtml).toContain(stillGettingMessageMessage);
|
||||||
|
expect(errorHtml).toContain(sendBugReportMessage);
|
||||||
|
});
|
||||||
|
});
|
32
ui/index.js
32
ui/index.js
@ -1,6 +1,6 @@
|
|||||||
import copyToClipboard from 'copy-to-clipboard';
|
import copyToClipboard from 'copy-to-clipboard';
|
||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
import { clone } from 'lodash';
|
import { clone, memoize } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render } from 'react-dom';
|
import { render } from 'react-dom';
|
||||||
import browser from 'webextension-polyfill';
|
import browser from 'webextension-polyfill';
|
||||||
@ -36,7 +36,7 @@ export default function launchMetamaskUi(opts, cb) {
|
|||||||
// check if we are unlocked first
|
// check if we are unlocked first
|
||||||
backgroundConnection.getState(function (err, metamaskState) {
|
backgroundConnection.getState(function (err, metamaskState) {
|
||||||
if (err) {
|
if (err) {
|
||||||
cb(err);
|
cb(err, metamaskState);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
startApp(metamaskState, backgroundConnection, opts).then((store) => {
|
startApp(metamaskState, backgroundConnection, opts).then((store) => {
|
||||||
@ -46,21 +46,31 @@ export default function launchMetamaskUi(opts, cb) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _setupLocale = async (currentLocale) => {
|
||||||
|
const currentLocaleMessages = currentLocale
|
||||||
|
? await fetchLocale(currentLocale)
|
||||||
|
: {};
|
||||||
|
const enLocaleMessages = await fetchLocale('en');
|
||||||
|
|
||||||
|
await loadRelativeTimeFormatLocaleData('en');
|
||||||
|
if (currentLocale) {
|
||||||
|
await loadRelativeTimeFormatLocaleData(currentLocale);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { currentLocaleMessages, enLocaleMessages };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setupLocale = memoize(_setupLocale);
|
||||||
|
|
||||||
async function startApp(metamaskState, backgroundConnection, opts) {
|
async function startApp(metamaskState, backgroundConnection, opts) {
|
||||||
// parse opts
|
// parse opts
|
||||||
if (!metamaskState.featureFlags) {
|
if (!metamaskState.featureFlags) {
|
||||||
metamaskState.featureFlags = {};
|
metamaskState.featureFlags = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentLocaleMessages = metamaskState.currentLocale
|
const { currentLocaleMessages, enLocaleMessages } = await setupLocale(
|
||||||
? await fetchLocale(metamaskState.currentLocale)
|
metamaskState.currentLocale,
|
||||||
: {};
|
);
|
||||||
const enLocaleMessages = await fetchLocale('en');
|
|
||||||
|
|
||||||
await loadRelativeTimeFormatLocaleData('en');
|
|
||||||
if (metamaskState.currentLocale) {
|
|
||||||
await loadRelativeTimeFormatLocaleData(metamaskState.currentLocale);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metamaskState.textDirection === 'rtl') {
|
if (metamaskState.textDirection === 'rtl') {
|
||||||
await switchDirection('rtl');
|
await switchDirection('rtl');
|
||||||
|
70
ui/index.test.js
Normal file
70
ui/index.test.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { setupLocale } from '.';
|
||||||
|
|
||||||
|
const enMessages = {
|
||||||
|
troubleStarting: {
|
||||||
|
message:
|
||||||
|
'MetaMask had trouble starting. This error could be intermittent, so try restarting the extension.',
|
||||||
|
},
|
||||||
|
restartMetamask: {
|
||||||
|
message: 'Restart MetaMask',
|
||||||
|
},
|
||||||
|
stillGettingMessage: {
|
||||||
|
message: 'Still getting this message?',
|
||||||
|
},
|
||||||
|
sendBugReport: {
|
||||||
|
message: 'Send us a bug report.',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const esMessages = {
|
||||||
|
troubleStarting: {
|
||||||
|
message:
|
||||||
|
'MetaMask tuvo problemas para iniciarse. Este error podría ser intermitente, así que intente reiniciar la extensión.',
|
||||||
|
},
|
||||||
|
restartMetamask: {
|
||||||
|
message: 'Reiniciar metamáscara',
|
||||||
|
},
|
||||||
|
sendBugReport: {
|
||||||
|
message: 'Envíenos un informe de errores.',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock('./helpers/utils/i18n-helper', () => ({
|
||||||
|
fetchLocale: jest.fn((locale) => (locale === 'en' ? enMessages : esMessages)),
|
||||||
|
loadRelativeTimeFormatLocaleData: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Index Tests', () => {
|
||||||
|
it('should get locale messages by calling setupLocale', async () => {
|
||||||
|
let result = await setupLocale('en');
|
||||||
|
const { currentLocaleMessages: clm, enLocaleMessages: elm } = result;
|
||||||
|
expect(clm).toBeDefined();
|
||||||
|
expect(elm).toBeDefined();
|
||||||
|
expect(clm.troubleStarting).toStrictEqual(enMessages.troubleStarting);
|
||||||
|
|
||||||
|
expect(clm.restartMetamask).toStrictEqual(enMessages.restartMetamask);
|
||||||
|
|
||||||
|
expect(clm.stillGettingMessage).toStrictEqual(
|
||||||
|
enMessages.stillGettingMessage,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(clm.sendBugReport).toStrictEqual(enMessages.sendBugReport);
|
||||||
|
|
||||||
|
result = await setupLocale('es');
|
||||||
|
|
||||||
|
const { currentLocaleMessages: clm2, enLocaleMessages: elm2 } = result;
|
||||||
|
expect(clm2).toBeDefined();
|
||||||
|
expect(elm2).toBeDefined();
|
||||||
|
|
||||||
|
expect(clm2.troubleStarting).toStrictEqual(esMessages.troubleStarting);
|
||||||
|
|
||||||
|
expect(clm2.restartMetamask).toStrictEqual(esMessages.restartMetamask);
|
||||||
|
|
||||||
|
expect(clm2.stillGettingMessage).toBeUndefined();
|
||||||
|
expect(elm2.stillGettingMessage).toStrictEqual(
|
||||||
|
enMessages.stillGettingMessage,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(clm2.sendBugReport).toStrictEqual(esMessages.sendBugReport);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user