mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 09:57:02 +01:00
3box Replacement (#15243)
* Backup user data Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Tests for prependZero (utils.js) Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Fix advancedtab test Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> backup controller tests Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Lint fixes Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Backup controller don't have a store. Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Restore from file. Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Advanced Tab tests Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Lint fix Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> e2e tests for backup unit tests for restore. Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Fix comments on PR. Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> restore style Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Lint fixes Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> We should move the exportAsFile to a utility file in the shared/ directory Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Move export as file to shared folder Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Refactor create download folder methods Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Lint fixes. Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Move the backup/restore buttons closer to 3box Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Change descriptions Add to search Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> refactor code to use if instead of && Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Lint fixes Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Restore button should change cursor to pointer. Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Fix restore not uploading same file twice. Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> Do not backup these items in preferences identities lostIdentities selectedAddress Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> lint fixes. Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> * Only update what is needed. Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> * Fixed test for search Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> * remove txError as it currently does nothing. Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> * Remove dispatch, not needed since we're not dispatching any actions. Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> * Event should be title case. Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> * Make backup/restore normal async functions rename event as per product suggestion. Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> * Use success Actionable message for success message and danger for error message Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> * change event name to match with backup Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> * Lint fixes Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> * lint fixes Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com> * fix e2e Signed-off-by: Akintayo A. Olusegun <akintayo.segun@gmail.com>
This commit is contained in:
parent
d255fcdefb
commit
4f34e72085
21
app/_locales/en/messages.json
generated
21
app/_locales/en/messages.json
generated
@ -399,6 +399,9 @@
|
||||
"backToAll": {
|
||||
"message": "Back to all"
|
||||
},
|
||||
"backup": {
|
||||
"message": "Backup"
|
||||
},
|
||||
"backupApprovalInfo": {
|
||||
"message": "This secret code is required to recover your wallet in case you lose your device, forget your password, have to re-install MetaMask, or want to access your wallet on another device."
|
||||
},
|
||||
@ -408,6 +411,12 @@
|
||||
"backupNow": {
|
||||
"message": "Backup now"
|
||||
},
|
||||
"backupUserData": {
|
||||
"message": "Backup your data"
|
||||
},
|
||||
"backupUserDataDescription": {
|
||||
"message": "You can backup user settings containing preferences and account addresses into a JSON file."
|
||||
},
|
||||
"balance": {
|
||||
"message": "Balance"
|
||||
},
|
||||
@ -2768,6 +2777,18 @@
|
||||
"restore": {
|
||||
"message": "Restore"
|
||||
},
|
||||
"restoreFailed": {
|
||||
"message": "Can not restore your data from the file provided"
|
||||
},
|
||||
"restoreSuccessful": {
|
||||
"message": "Your data has been restored successfully"
|
||||
},
|
||||
"restoreUserData": {
|
||||
"message": "Restore user data"
|
||||
},
|
||||
"restoreUserDataDescription": {
|
||||
"message": "You can restore user settings containing preferences and account addresses from a previously backed up JSON file."
|
||||
},
|
||||
"restoreWalletPreferences": {
|
||||
"message": "A backup of your data from $1 has been found. Would you like to restore your wallet preferences?",
|
||||
"description": "$1 is the date at which the data was backed up"
|
||||
|
77
app/scripts/controllers/backup.js
Normal file
77
app/scripts/controllers/backup.js
Normal file
@ -0,0 +1,77 @@
|
||||
import { exportAsFile } from '../../../shared/modules/export-utils';
|
||||
import { prependZero } from '../../../shared/modules/string-utils';
|
||||
|
||||
export default class BackupController {
|
||||
constructor(opts = {}) {
|
||||
const {
|
||||
preferencesController,
|
||||
addressBookController,
|
||||
trackMetaMetricsEvent,
|
||||
} = opts;
|
||||
|
||||
this.preferencesController = preferencesController;
|
||||
this.addressBookController = addressBookController;
|
||||
this._trackMetaMetricsEvent = trackMetaMetricsEvent;
|
||||
}
|
||||
|
||||
async restoreUserData(jsonString) {
|
||||
const existingPreferences = this.preferencesController.store.getState();
|
||||
const { preferences, addressBook } = JSON.parse(jsonString);
|
||||
if (preferences) {
|
||||
preferences.identities = existingPreferences.identities;
|
||||
preferences.lostIdentities = existingPreferences.lostIdentities;
|
||||
preferences.selectedAddress = existingPreferences.selectedAddress;
|
||||
|
||||
this.preferencesController.store.updateState(preferences);
|
||||
}
|
||||
|
||||
if (addressBook) {
|
||||
this.addressBookController.update(addressBook, true);
|
||||
}
|
||||
|
||||
if (preferences && addressBook) {
|
||||
this._trackMetaMetricsEvent({
|
||||
event: 'User Data Imported',
|
||||
category: 'Backup',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async backupUserData() {
|
||||
const userData = {
|
||||
preferences: { ...this.preferencesController.store.getState() },
|
||||
addressBook: { ...this.addressBookController.state },
|
||||
};
|
||||
|
||||
/**
|
||||
* We can remove these properties since we will won't be restoring identities from backup
|
||||
*/
|
||||
delete userData.preferences.identities;
|
||||
delete userData.preferences.lostIdentities;
|
||||
delete userData.preferences.selectedAddress;
|
||||
|
||||
const result = JSON.stringify(userData);
|
||||
|
||||
const date = new Date();
|
||||
|
||||
const prefixZero = (num) => prependZero(num, 2);
|
||||
|
||||
/*
|
||||
* userData.YYYY_MM_DD_HH_mm_SS e.g userData.2022_01_13_13_45_56
|
||||
* */
|
||||
const userDataFileName = `MetaMaskUserData.${date.getFullYear()}_${prefixZero(
|
||||
date.getMonth() + 1,
|
||||
)}_${prefixZero(date.getDay())}_${prefixZero(date.getHours())}_${prefixZero(
|
||||
date.getMinutes(),
|
||||
)}_${prefixZero(date.getDay())}.json`;
|
||||
|
||||
exportAsFile(userDataFileName, result);
|
||||
|
||||
this._trackMetaMetricsEvent({
|
||||
event: 'User Data Exported',
|
||||
category: 'Backup',
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
118
app/scripts/controllers/backup.test.js
Normal file
118
app/scripts/controllers/backup.test.js
Normal file
@ -0,0 +1,118 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import sinon from 'sinon';
|
||||
import BackupController from './backup';
|
||||
|
||||
function getMockController() {
|
||||
const mcState = {
|
||||
getSelectedAddress: sinon.stub().returns('0x01'),
|
||||
selectedAddress: '0x01',
|
||||
identities: {
|
||||
'0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B': {
|
||||
address: '0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B',
|
||||
lastSelected: 1655380342907,
|
||||
name: 'Account 3',
|
||||
},
|
||||
},
|
||||
lostIdentities: {
|
||||
'0xfd59bbe569376e3d3e4430297c3c69ea93f77435': {
|
||||
address: '0xfd59bbe569376e3d3e4430297c3c69ea93f77435',
|
||||
lastSelected: 1655379648197,
|
||||
name: 'Ledger 1',
|
||||
},
|
||||
},
|
||||
update: (store) => (mcState.store = store),
|
||||
};
|
||||
|
||||
mcState.store = {
|
||||
getState: sinon.stub().returns(mcState),
|
||||
updateState: (store) => (mcState.store = store),
|
||||
};
|
||||
|
||||
return mcState;
|
||||
}
|
||||
|
||||
const jsonData = `{"preferences":{"frequentRpcListDetail":[{"chainId":"0x539","nickname":"Localhost 8545","rpcPrefs":{},"rpcUrl":"http://localhost:8545","ticker":"ETH"},{"chainId":"0x38","nickname":"Binance Smart Chain Mainnet","rpcPrefs":{"blockExplorerUrl":"https://bscscan.com"},"rpcUrl":"https://bsc-dataseed1.binance.org","ticker":"BNB"},{"chainId":"0x61","nickname":"Binance Smart Chain Testnet","rpcPrefs":{"blockExplorerUrl":"https://testnet.bscscan.com"},"rpcUrl":"https://data-seed-prebsc-1-s1.binance.org:8545","ticker":"tBNB"},{"chainId":"0x89","nickname":"Polygon Mainnet","rpcPrefs":{"blockExplorerUrl":"https://polygonscan.com"},"rpcUrl":"https://polygon-rpc.com","ticker":"MATIC"}],"useBlockie":false,"useNonceField":false,"usePhishDetect":true,"dismissSeedBackUpReminder":false,"useTokenDetection":false,"useCollectibleDetection":false,"openSeaEnabled":false,"advancedGasFee":null,"featureFlags":{"sendHexData":true,"showIncomingTransactions":true},"knownMethodData":{},"currentLocale":"en","forgottenPassword":false,"preferences":{"hideZeroBalanceTokens":false,"showFiatInTestnets":false,"showTestNetworks":true,"useNativeCurrencyAsPrimaryCurrency":true},"ipfsGateway":"dweb.link","infuraBlocked":false,"ledgerTransportType":"webhid","theme":"light","customNetworkListEnabled":false,"textDirection":"auto"},"addressBook":{"addressBook":{"0x61":{"0x42EB768f2244C8811C63729A21A3569731535f06":{"address":"0x42EB768f2244C8811C63729A21A3569731535f06","chainId":"0x61","isEns":false,"memo":"","name":""}}}}}`;
|
||||
|
||||
describe('BackupController', function () {
|
||||
const getBackupController = () => {
|
||||
return new BackupController({
|
||||
preferencesController: getMockController(),
|
||||
addressBookController: getMockController(),
|
||||
trackMetaMetricsEvent: sinon.stub(),
|
||||
});
|
||||
};
|
||||
|
||||
describe('constructor', function () {
|
||||
it('should setup correctly', async function () {
|
||||
const backupController = getBackupController();
|
||||
const selectedAddress =
|
||||
backupController.preferencesController.getSelectedAddress();
|
||||
assert.equal(selectedAddress, '0x01');
|
||||
});
|
||||
|
||||
it('should restore backup', async function () {
|
||||
const backupController = getBackupController();
|
||||
backupController.restoreUserData(jsonData);
|
||||
// check Preferences backup
|
||||
assert.equal(
|
||||
backupController.preferencesController.store.frequentRpcListDetail[0]
|
||||
.chainId,
|
||||
'0x539',
|
||||
);
|
||||
assert.equal(
|
||||
backupController.preferencesController.store.frequentRpcListDetail[1]
|
||||
.chainId,
|
||||
'0x38',
|
||||
);
|
||||
// make sure identities are not lost after restore
|
||||
assert.equal(
|
||||
backupController.preferencesController.store.identities[
|
||||
'0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B'
|
||||
].lastSelected,
|
||||
1655380342907,
|
||||
);
|
||||
assert.equal(
|
||||
backupController.preferencesController.store.identities[
|
||||
'0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B'
|
||||
].name,
|
||||
'Account 3',
|
||||
);
|
||||
assert.equal(
|
||||
backupController.preferencesController.store.lostIdentities[
|
||||
'0xfd59bbe569376e3d3e4430297c3c69ea93f77435'
|
||||
].lastSelected,
|
||||
1655379648197,
|
||||
);
|
||||
assert.equal(
|
||||
backupController.preferencesController.store.lostIdentities[
|
||||
'0xfd59bbe569376e3d3e4430297c3c69ea93f77435'
|
||||
].name,
|
||||
'Ledger 1',
|
||||
);
|
||||
// make sure selected address is not lost after restore
|
||||
assert.equal(
|
||||
backupController.preferencesController.store.selectedAddress,
|
||||
'0x01',
|
||||
);
|
||||
// check address book backup
|
||||
assert.equal(
|
||||
backupController.addressBookController.store.addressBook['0x61'][
|
||||
'0x42EB768f2244C8811C63729A21A3569731535f06'
|
||||
].chainId,
|
||||
'0x61',
|
||||
);
|
||||
assert.equal(
|
||||
backupController.addressBookController.store.addressBook['0x61'][
|
||||
'0x42EB768f2244C8811C63729A21A3569731535f06'
|
||||
].address,
|
||||
'0x42EB768f2244C8811C63729A21A3569731535f06',
|
||||
);
|
||||
assert.equal(
|
||||
backupController.addressBookController.store.addressBook['0x61'][
|
||||
'0x42EB768f2244C8811C63729A21A3569731535f06'
|
||||
].isEns,
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -122,6 +122,7 @@ import CachedBalancesController from './controllers/cached-balances';
|
||||
import AlertController from './controllers/alert';
|
||||
import OnboardingController from './controllers/onboarding';
|
||||
import ThreeBoxController from './controllers/threebox';
|
||||
import BackupController from './controllers/backup';
|
||||
import IncomingTransactionsController from './controllers/incoming-transactions';
|
||||
import MessageManager, { normalizeMsgData } from './lib/message-manager';
|
||||
import DecryptMessageManager from './lib/decrypt-message-manager';
|
||||
@ -797,6 +798,14 @@ export default class MetamaskController extends EventEmitter {
|
||||
),
|
||||
});
|
||||
|
||||
this.backupController = new BackupController({
|
||||
preferencesController: this.preferencesController,
|
||||
addressBookController: this.addressBookController,
|
||||
trackMetaMetricsEvent: this.metaMetricsController.trackEvent.bind(
|
||||
this.metaMetricsController,
|
||||
),
|
||||
});
|
||||
|
||||
this.txController = new TransactionController({
|
||||
initState:
|
||||
initState.TransactionController || initState.TransactionManager,
|
||||
@ -1047,6 +1056,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
PermissionLogController: this.permissionLogController.store,
|
||||
SubjectMetadataController: this.subjectMetadataController,
|
||||
ThreeBoxController: this.threeBoxController.store,
|
||||
BackupController: this.backupController,
|
||||
AnnouncementController: this.announcementController,
|
||||
GasFeeController: this.gasFeeController,
|
||||
TokenListController: this.tokenListController,
|
||||
@ -1085,6 +1095,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
PermissionLogController: this.permissionLogController.store,
|
||||
SubjectMetadataController: this.subjectMetadataController,
|
||||
ThreeBoxController: this.threeBoxController.store,
|
||||
BackupController: this.backupController,
|
||||
SwapsController: this.swapsController.store,
|
||||
EnsController: this.ensController.store,
|
||||
ApprovalController: this.approvalController,
|
||||
@ -1521,6 +1532,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
smartTransactionsController,
|
||||
txController,
|
||||
assetsContractController,
|
||||
backupController,
|
||||
} = this;
|
||||
|
||||
return {
|
||||
@ -1965,6 +1977,10 @@ export default class MetamaskController extends EventEmitter {
|
||||
removePollingTokenFromAppState:
|
||||
appStateController.removePollingToken.bind(appStateController),
|
||||
|
||||
// BackupController
|
||||
backupUserData: backupController.backupUserData.bind(backupController),
|
||||
restoreUserData: backupController.restoreUserData.bind(backupController),
|
||||
|
||||
// DetectTokenController
|
||||
detectNewTokens: detectTokensController.detectNewTokens.bind(
|
||||
detectTokensController,
|
||||
|
19
shared/modules/export-utils.js
Normal file
19
shared/modules/export-utils.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { getRandomFileName } from '../../ui/helpers/utils/util';
|
||||
|
||||
export function exportAsFile(filename, data, type = 'text/csv') {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
filename = filename || getRandomFileName();
|
||||
// source: https://stackoverflow.com/a/33542499 by Ludovic Feltz
|
||||
const blob = new window.Blob([data], { type });
|
||||
if (window.navigator.msSaveOrOpenBlob) {
|
||||
window.navigator.msSaveBlob(blob, filename);
|
||||
} else {
|
||||
const elem = window.document.createElement('a');
|
||||
elem.target = '_blank';
|
||||
elem.href = window.URL.createObjectURL(blob);
|
||||
elem.download = filename;
|
||||
document.body.appendChild(elem);
|
||||
elem.click();
|
||||
document.body.removeChild(elem);
|
||||
}
|
||||
}
|
@ -4,3 +4,7 @@ export function isEqualCaseInsensitive(value1, value2) {
|
||||
}
|
||||
return value1.toLowerCase() === value2.toLowerCase();
|
||||
}
|
||||
|
||||
export function prependZero(num, maxLength) {
|
||||
return num.toString().padStart(maxLength, '0');
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
const path = require('path');
|
||||
const { promises: fs } = require('fs');
|
||||
const BigNumber = require('bignumber.js');
|
||||
const mockttp = require('mockttp');
|
||||
const createStaticServer = require('../../development/create-static-server');
|
||||
@ -17,6 +18,11 @@ const largeDelayMs = regularDelayMs * 2;
|
||||
const veryLargeDelayMs = largeDelayMs * 2;
|
||||
const dappBasePort = 8080;
|
||||
|
||||
const createDownloadFolder = async (downloadsFolder) => {
|
||||
await fs.rm(downloadsFolder, { recursive: true, force: true });
|
||||
await fs.mkdir(downloadsFolder, { recursive: true });
|
||||
};
|
||||
|
||||
const convertToHexValue = (val) => `0x${new BigNumber(val, 10).toString(16)}`;
|
||||
|
||||
async function withFixtures(options, testSuite) {
|
||||
@ -330,4 +336,5 @@ module.exports = {
|
||||
connectDappWithExtensionPopup,
|
||||
completeImportSRPOnboardingFlow,
|
||||
completeImportSRPOnboardingFlowWordByWord,
|
||||
createDownloadFolder,
|
||||
};
|
||||
|
81
test/e2e/tests/backup.spec.js
Normal file
81
test/e2e/tests/backup.spec.js
Normal file
@ -0,0 +1,81 @@
|
||||
const { strict: assert } = require('assert');
|
||||
const { promises: fs } = require('fs');
|
||||
const {
|
||||
convertToHexValue,
|
||||
withFixtures,
|
||||
createDownloadFolder,
|
||||
} = require('../helpers');
|
||||
|
||||
const downloadsFolder = `${process.cwd()}/test-artifacts/downloads`;
|
||||
|
||||
const backupExists = async () => {
|
||||
const date = new Date();
|
||||
|
||||
const prependZero = (num, maxLength) => {
|
||||
return num.toString().padStart(maxLength, '0');
|
||||
};
|
||||
|
||||
const prefixZero = (num) => prependZero(num, 2);
|
||||
|
||||
/*
|
||||
* userData.YYYY_MM_DD_HH_mm_SS e.g userData.2022_01_13_13_45_56
|
||||
* */
|
||||
const userDataFileName = `MetaMaskUserData.${date.getFullYear()}_${prefixZero(
|
||||
date.getMonth() + 1,
|
||||
)}_${prefixZero(date.getDay())}_${prefixZero(date.getHours())}_${prefixZero(
|
||||
date.getMinutes(),
|
||||
)}_${prefixZero(date.getDay())}.json`;
|
||||
|
||||
try {
|
||||
const backup = `${downloadsFolder}/${userDataFileName}`;
|
||||
await fs.access(backup);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
describe('Backup', function () {
|
||||
const ganacheOptions = {
|
||||
accounts: [
|
||||
{
|
||||
secretKey:
|
||||
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC',
|
||||
balance: convertToHexValue(25000000000000000000),
|
||||
},
|
||||
],
|
||||
};
|
||||
it('should create backup for the account', async function () {
|
||||
await withFixtures(
|
||||
{
|
||||
fixtures: 'imported-account',
|
||||
ganacheOptions,
|
||||
title: this.test.title,
|
||||
failOnConsoleError: false,
|
||||
},
|
||||
async ({ driver }) => {
|
||||
await createDownloadFolder(downloadsFolder);
|
||||
await driver.navigate();
|
||||
await driver.fill('#password', 'correct horse battery staple');
|
||||
await driver.press('#password', driver.Key.ENTER);
|
||||
|
||||
// Download user settings
|
||||
await driver.clickElement('.account-menu__icon');
|
||||
await driver.clickElement({ text: 'Settings', tag: 'div' });
|
||||
await driver.clickElement({ text: 'Advanced', tag: 'div' });
|
||||
await driver.clickElement({
|
||||
text: 'Backup',
|
||||
tag: 'button',
|
||||
});
|
||||
|
||||
// Verify download
|
||||
let fileExists;
|
||||
await driver.wait(async () => {
|
||||
fileExists = await backupExists();
|
||||
return fileExists === true;
|
||||
}, 10000);
|
||||
assert.equal(fileExists, true);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
@ -1,14 +1,13 @@
|
||||
const { strict: assert } = require('assert');
|
||||
const { promises: fs } = require('fs');
|
||||
const { convertToHexValue, withFixtures } = require('../helpers');
|
||||
const {
|
||||
convertToHexValue,
|
||||
withFixtures,
|
||||
createDownloadFolder,
|
||||
} = require('../helpers');
|
||||
|
||||
const downloadsFolder = `${process.cwd()}/test-artifacts/downloads`;
|
||||
|
||||
const createDownloadFolder = async () => {
|
||||
await fs.rm(downloadsFolder, { recursive: true, force: true });
|
||||
await fs.mkdir(downloadsFolder, { recursive: true });
|
||||
};
|
||||
|
||||
const stateLogsExist = async () => {
|
||||
try {
|
||||
const stateLogs = `${downloadsFolder}/MetaMask state logs.json`;
|
||||
@ -38,7 +37,7 @@ describe('State logs', function () {
|
||||
failOnConsoleError: false,
|
||||
},
|
||||
async ({ driver }) => {
|
||||
await createDownloadFolder();
|
||||
await createDownloadFolder(downloadsFolder);
|
||||
await driver.navigate();
|
||||
await driver.fill('#password', 'correct horse battery staple');
|
||||
await driver.press('#password', driver.Key.ENTER);
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { exportAsFile } from '../../../helpers/utils/util';
|
||||
import Copy from '../icon/copy-icon.component';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
|
||||
import { exportAsFile } from '../../../../shared/modules/export-utils';
|
||||
|
||||
function ExportTextContainer({ text = '' }) {
|
||||
const t = useI18nContext();
|
||||
|
@ -353,4 +353,18 @@ export const SETTINGS_CONSTANTS = [
|
||||
route: `${EXPERIMENTAL_ROUTE}#show-custom-network`,
|
||||
icon: 'fa fa-flask',
|
||||
},
|
||||
{
|
||||
tabMessage: (t) => t('advanced'),
|
||||
sectionMessage: (t) => t('backupUserData'),
|
||||
descriptionMessage: (t) => t('backupUserDataDescription'),
|
||||
route: `${ADVANCED_ROUTE}#backup-userdata`,
|
||||
icon: 'fas fa-download',
|
||||
},
|
||||
{
|
||||
tabMessage: (t) => t('advanced'),
|
||||
sectionMessage: (t) => t('restoreUserData'),
|
||||
descriptionMessage: (t) => t('restoreUserDataDescription'),
|
||||
route: `${ADVANCED_ROUTE}#restore-userdata`,
|
||||
icon: 'fas fa-upload',
|
||||
},
|
||||
];
|
||||
|
@ -172,7 +172,7 @@ describe('Settings Search Utils', () => {
|
||||
});
|
||||
|
||||
it('should get good advanced section number', () => {
|
||||
expect(getNumberOfSettingsInSection(t, t('advanced'))).toStrictEqual(13);
|
||||
expect(getNumberOfSettingsInSection(t, t('advanced'))).toStrictEqual(15);
|
||||
});
|
||||
|
||||
it('should get good contact section number', () => {
|
||||
|
@ -193,24 +193,6 @@ export function getRandomFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
export function exportAsFile(filename, data, type = 'text/csv') {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
filename = filename || getRandomFileName();
|
||||
// source: https://stackoverflow.com/a/33542499 by Ludovic Feltz
|
||||
const blob = new window.Blob([data], { type });
|
||||
if (window.navigator.msSaveOrOpenBlob) {
|
||||
window.navigator.msSaveBlob(blob, filename);
|
||||
} else {
|
||||
const elem = window.document.createElement('a');
|
||||
elem.target = '_blank';
|
||||
elem.href = window.URL.createObjectURL(blob);
|
||||
elem.download = filename;
|
||||
document.body.appendChild(elem);
|
||||
elem.click();
|
||||
document.body.removeChild(elem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortens an Ethereum address for display, preserving the beginning and end.
|
||||
* Returns the given address if it is no longer than 10 characters.
|
||||
|
@ -6,8 +6,8 @@ import {
|
||||
INITIALIZE_END_OF_FLOW_ROUTE,
|
||||
INITIALIZE_SEED_PHRASE_ROUTE,
|
||||
} from '../../../../helpers/constants/routes';
|
||||
import { exportAsFile } from '../../../../helpers/utils/util';
|
||||
import { EVENT } from '../../../../../shared/constants/metametrics';
|
||||
import { exportAsFile } from '../../../../../shared/modules/export-utils';
|
||||
import DraggableSeed from './draggable-seed.component';
|
||||
|
||||
const EMPTY_SEEDS = Array(12).fill(null);
|
||||
|
@ -10,9 +10,9 @@ import {
|
||||
DEFAULT_ROUTE,
|
||||
INITIALIZE_SEED_PHRASE_INTRO_ROUTE,
|
||||
} from '../../../../helpers/constants/routes';
|
||||
import { exportAsFile } from '../../../../helpers/utils/util';
|
||||
import { EVENT } from '../../../../../shared/constants/metametrics';
|
||||
import { returnToOnboardingInitiatorTab } from '../../onboarding-initiator-util';
|
||||
import { exportAsFile } from '../../../../../shared/modules/export-utils';
|
||||
|
||||
export default class RevealSeedPhrase extends PureComponent {
|
||||
static contextTypes = {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { exportAsFile } from '../../../helpers/utils/util';
|
||||
import ToggleButton from '../../../components/ui/toggle-button';
|
||||
import TextField from '../../../components/ui/text-field';
|
||||
import Button from '../../../components/ui/button';
|
||||
@ -22,6 +21,8 @@ import {
|
||||
LEDGER_USB_VENDOR_ID,
|
||||
} from '../../../../shared/constants/hardware-wallets';
|
||||
import { EVENT } from '../../../../shared/constants/metametrics';
|
||||
import { exportAsFile } from '../../../../shared/modules/export-utils';
|
||||
import ActionableMessage from '../../../components/ui/actionable-message';
|
||||
|
||||
export default class AdvancedTab extends PureComponent {
|
||||
static contextTypes = {
|
||||
@ -58,6 +59,8 @@ export default class AdvancedTab extends PureComponent {
|
||||
userHasALedgerAccount: PropTypes.bool.isRequired,
|
||||
useTokenDetection: PropTypes.bool.isRequired,
|
||||
setUseTokenDetection: PropTypes.func.isRequired,
|
||||
backupUserData: PropTypes.func.isRequired,
|
||||
restoreUserData: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -66,6 +69,8 @@ export default class AdvancedTab extends PureComponent {
|
||||
ipfsGateway: this.props.ipfsGateway,
|
||||
ipfsGatewayError: '',
|
||||
showLedgerTransportWarning: false,
|
||||
showResultMessage: false,
|
||||
restoreSuccessful: true,
|
||||
};
|
||||
|
||||
settingsRefs = Array(
|
||||
@ -117,6 +122,123 @@ export default class AdvancedTab extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
async getTextFromFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new window.FileReader();
|
||||
reader.onload = (e) => {
|
||||
const text = e.target.result;
|
||||
resolve(text);
|
||||
};
|
||||
|
||||
reader.onerror = (e) => {
|
||||
reject(e);
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
async handleFileUpload(event) {
|
||||
/**
|
||||
* we need this to be able to access event.target after
|
||||
* the event handler has been called. [Synthetic Event Pooling, pre React 17]
|
||||
*
|
||||
* @see https://fb.me/react-event-pooling
|
||||
*/
|
||||
event.persist();
|
||||
const file = event.target.files[0];
|
||||
const jsonString = await this.getTextFromFile(file);
|
||||
/**
|
||||
* so that we can restore same file again if we want to.
|
||||
* chrome blocks uploading same file twice.
|
||||
*
|
||||
*/
|
||||
event.target.value = '';
|
||||
const result = await this.props.restoreUserData(jsonString);
|
||||
this.setState({
|
||||
showResultMessage: true,
|
||||
restoreSuccessful: result,
|
||||
});
|
||||
}
|
||||
|
||||
renderRestoreUserData() {
|
||||
const { t } = this.context;
|
||||
const { showResultMessage, restoreSuccessful } = this.state;
|
||||
|
||||
const settingsRefIndex = process.env.TOKEN_DETECTION_V2 ? 15 : 14;
|
||||
return (
|
||||
<div
|
||||
ref={this.settingsRefs[settingsRefIndex]}
|
||||
className="settings-page__content-row"
|
||||
data-testid="advanced-setting-data-restore"
|
||||
>
|
||||
<div className="settings-page__content-item">
|
||||
<span>{t('restoreUserData')}</span>
|
||||
<span className="settings-page__content-description">
|
||||
{t('restoreUserDataDescription')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="settings-page__content-item">
|
||||
<div className="settings-page__content-item-col">
|
||||
<label
|
||||
htmlFor="restore-file"
|
||||
className="button btn btn--rounded btn-secondary btn--large settings-page__button"
|
||||
>
|
||||
{t('restore')}
|
||||
</label>
|
||||
<input
|
||||
id="restore-file"
|
||||
style={{ visibility: 'hidden' }}
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={(e) => this.handleFileUpload(e)}
|
||||
/>
|
||||
</div>
|
||||
{showResultMessage && (
|
||||
<ActionableMessage
|
||||
type={restoreSuccessful ? 'success' : 'danger'}
|
||||
message={
|
||||
restoreSuccessful ? t('restoreSuccessful') : t('restoreFailed')
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderUserDataBackup() {
|
||||
const { t } = this.context;
|
||||
const settingsRefIndex = process.env.TOKEN_DETECTION_V2 ? 15 : 13;
|
||||
return (
|
||||
<div
|
||||
ref={this.settingsRefs[settingsRefIndex]}
|
||||
className="settings-page__content-row"
|
||||
data-testid="advanced-setting-data-backup"
|
||||
>
|
||||
<div className="settings-page__content-item">
|
||||
<span>{t('backupUserData')}</span>
|
||||
<span className="settings-page__content-description">
|
||||
{t('backupUserDataDescription')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="settings-page__content-item">
|
||||
<div className="settings-page__content-item-col">
|
||||
<Button
|
||||
type="secondary"
|
||||
large
|
||||
onClick={() => {
|
||||
this.props.backupUserData();
|
||||
}}
|
||||
>
|
||||
{t('backup')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderStateLogs() {
|
||||
const { t } = this.context;
|
||||
const { displayWarning } = this.props;
|
||||
@ -730,6 +852,8 @@ export default class AdvancedTab extends PureComponent {
|
||||
{this.renderToggleTestNetworks()}
|
||||
{this.renderUseNonceOptIn()}
|
||||
{this.renderAutoLockTimeLimit()}
|
||||
{this.renderUserDataBackup()}
|
||||
{this.renderRestoreUserData()}
|
||||
{this.renderThreeBoxControl()}
|
||||
{this.renderIpfsGatewayControl()}
|
||||
{notUsingFirefox ? this.renderLedgerLiveControl() : null}
|
||||
|
@ -31,6 +31,8 @@ describe('AdvancedTab Component', () => {
|
||||
useTokenDetection
|
||||
setUseTokenDetection={toggleTokenDetection}
|
||||
userHasALedgerAccount
|
||||
backupUserData={() => undefined}
|
||||
restoreUserData={() => undefined}
|
||||
/>,
|
||||
{
|
||||
context: {
|
||||
@ -41,7 +43,69 @@ describe('AdvancedTab Component', () => {
|
||||
});
|
||||
|
||||
it('should render correctly when threeBoxFeatureFlag', () => {
|
||||
expect(component.find('.settings-page__content-row')).toHaveLength(13);
|
||||
expect(component.find('.settings-page__content-row')).toHaveLength(15);
|
||||
});
|
||||
|
||||
it('should render backup button', () => {
|
||||
expect(component.find('.settings-page__content-row')).toHaveLength(15);
|
||||
|
||||
expect(
|
||||
component
|
||||
.find('.settings-page__content-row')
|
||||
.at(9)
|
||||
.find('.settings-page__content-item'),
|
||||
).toHaveLength(2);
|
||||
|
||||
expect(
|
||||
component
|
||||
.find('.settings-page__content-row')
|
||||
.at(9)
|
||||
.find('.settings-page__content-item')
|
||||
.at(0)
|
||||
.find('.settings-page__content-description')
|
||||
.props().children,
|
||||
).toStrictEqual('_backupUserDataDescription');
|
||||
|
||||
expect(
|
||||
component
|
||||
.find('.settings-page__content-row')
|
||||
.at(9)
|
||||
.find('.settings-page__content-item')
|
||||
.at(1)
|
||||
.find('Button')
|
||||
.props().children,
|
||||
).toStrictEqual('_backup');
|
||||
});
|
||||
|
||||
it('should render restore button', () => {
|
||||
expect(component.find('.settings-page__content-row')).toHaveLength(15);
|
||||
|
||||
expect(
|
||||
component
|
||||
.find('.settings-page__content-row')
|
||||
.at(10)
|
||||
.find('.settings-page__content-item'),
|
||||
).toHaveLength(2);
|
||||
|
||||
expect(
|
||||
component
|
||||
.find('.settings-page__content-row')
|
||||
.at(10)
|
||||
.find('.settings-page__content-item')
|
||||
.at(0)
|
||||
.find('.settings-page__content-description')
|
||||
.props().children,
|
||||
).toStrictEqual('_restoreUserDataDescription');
|
||||
|
||||
expect(
|
||||
component
|
||||
.find('.settings-page__content-row')
|
||||
.at(10)
|
||||
.find('.settings-page__content-item')
|
||||
.at(1)
|
||||
.find('label')
|
||||
.props().children,
|
||||
).toStrictEqual('_restore');
|
||||
});
|
||||
|
||||
it('should update autoLockTimeLimit', () => {
|
||||
@ -63,6 +127,8 @@ describe('AdvancedTab Component', () => {
|
||||
useTokenDetection
|
||||
setUseTokenDetection={toggleTokenDetection}
|
||||
userHasALedgerAccount
|
||||
backupUserData={() => undefined}
|
||||
restoreUserData={() => undefined}
|
||||
/>,
|
||||
{
|
||||
context: {
|
||||
@ -108,6 +174,8 @@ describe('AdvancedTab Component', () => {
|
||||
useTokenDetection
|
||||
setUseTokenDetection={toggleTokenDetection}
|
||||
userHasALedgerAccount
|
||||
backupUserData={() => undefined}
|
||||
restoreUserData={() => undefined}
|
||||
/>,
|
||||
{
|
||||
context: {
|
||||
@ -145,6 +213,8 @@ describe('AdvancedTab Component', () => {
|
||||
useTokenDetection
|
||||
setUseTokenDetection={toggleTokenDetection}
|
||||
userHasALedgerAccount
|
||||
backupUserData={() => undefined}
|
||||
restoreUserData={() => undefined}
|
||||
/>,
|
||||
{
|
||||
context: {
|
||||
|
@ -15,6 +15,8 @@ import {
|
||||
setLedgerTransportPreference,
|
||||
setDismissSeedBackUpReminder,
|
||||
setUseTokenDetection,
|
||||
backupUserData,
|
||||
restoreUserData,
|
||||
} from '../../../store/actions';
|
||||
import { getPreferences } from '../../../selectors';
|
||||
import { doesUserHaveALedgerAccount } from '../../../ducks/metamask/metamask';
|
||||
@ -63,6 +65,8 @@ export const mapStateToProps = (state) => {
|
||||
|
||||
export const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
backupUserData: () => backupUserData(),
|
||||
restoreUserData: (jsonString) => restoreUserData(jsonString),
|
||||
setHexDataFeatureFlag: (shouldShow) =>
|
||||
dispatch(setFeatureFlag('sendHexData', shouldShow)),
|
||||
displayWarning: (warning) => dispatch(displayWarning(warning)),
|
||||
|
@ -12,6 +12,12 @@
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
|
||||
&__error-text {
|
||||
@include H7;
|
||||
|
||||
color: var(--color-error-default);
|
||||
}
|
||||
|
||||
&__header {
|
||||
padding: 8px 24px 8px 24px;
|
||||
position: relative;
|
||||
@ -350,6 +356,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__copy-icon {
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
@ -778,6 +778,29 @@ export function updateTransactionSendFlowHistory(txId, sendFlowHistory) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function backupUserData() {
|
||||
let backedupData;
|
||||
try {
|
||||
backedupData = await promisifiedBackground.backupUserData();
|
||||
} catch (error) {
|
||||
log.error(error.message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return backedupData;
|
||||
}
|
||||
|
||||
export async function restoreUserData(jsonString) {
|
||||
try {
|
||||
await promisifiedBackground.restoreUserData(jsonString);
|
||||
} catch (error) {
|
||||
log.error(error.message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function updateTransactionGasFees(txId, txGasFees) {
|
||||
return async (dispatch) => {
|
||||
let updatedTransaction;
|
||||
|
Loading…
Reference in New Issue
Block a user