mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 18:00:18 +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": {
|
"backToAll": {
|
||||||
"message": "Back to all"
|
"message": "Back to all"
|
||||||
},
|
},
|
||||||
|
"backup": {
|
||||||
|
"message": "Backup"
|
||||||
|
},
|
||||||
"backupApprovalInfo": {
|
"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."
|
"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": {
|
"backupNow": {
|
||||||
"message": "Backup now"
|
"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": {
|
"balance": {
|
||||||
"message": "Balance"
|
"message": "Balance"
|
||||||
},
|
},
|
||||||
@ -2768,6 +2777,18 @@
|
|||||||
"restore": {
|
"restore": {
|
||||||
"message": "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": {
|
"restoreWalletPreferences": {
|
||||||
"message": "A backup of your data from $1 has been found. Would you like to restore your wallet preferences?",
|
"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"
|
"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 AlertController from './controllers/alert';
|
||||||
import OnboardingController from './controllers/onboarding';
|
import OnboardingController from './controllers/onboarding';
|
||||||
import ThreeBoxController from './controllers/threebox';
|
import ThreeBoxController from './controllers/threebox';
|
||||||
|
import BackupController from './controllers/backup';
|
||||||
import IncomingTransactionsController from './controllers/incoming-transactions';
|
import IncomingTransactionsController from './controllers/incoming-transactions';
|
||||||
import MessageManager, { normalizeMsgData } from './lib/message-manager';
|
import MessageManager, { normalizeMsgData } from './lib/message-manager';
|
||||||
import DecryptMessageManager from './lib/decrypt-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({
|
this.txController = new TransactionController({
|
||||||
initState:
|
initState:
|
||||||
initState.TransactionController || initState.TransactionManager,
|
initState.TransactionController || initState.TransactionManager,
|
||||||
@ -1047,6 +1056,7 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
PermissionLogController: this.permissionLogController.store,
|
PermissionLogController: this.permissionLogController.store,
|
||||||
SubjectMetadataController: this.subjectMetadataController,
|
SubjectMetadataController: this.subjectMetadataController,
|
||||||
ThreeBoxController: this.threeBoxController.store,
|
ThreeBoxController: this.threeBoxController.store,
|
||||||
|
BackupController: this.backupController,
|
||||||
AnnouncementController: this.announcementController,
|
AnnouncementController: this.announcementController,
|
||||||
GasFeeController: this.gasFeeController,
|
GasFeeController: this.gasFeeController,
|
||||||
TokenListController: this.tokenListController,
|
TokenListController: this.tokenListController,
|
||||||
@ -1085,6 +1095,7 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
PermissionLogController: this.permissionLogController.store,
|
PermissionLogController: this.permissionLogController.store,
|
||||||
SubjectMetadataController: this.subjectMetadataController,
|
SubjectMetadataController: this.subjectMetadataController,
|
||||||
ThreeBoxController: this.threeBoxController.store,
|
ThreeBoxController: this.threeBoxController.store,
|
||||||
|
BackupController: this.backupController,
|
||||||
SwapsController: this.swapsController.store,
|
SwapsController: this.swapsController.store,
|
||||||
EnsController: this.ensController.store,
|
EnsController: this.ensController.store,
|
||||||
ApprovalController: this.approvalController,
|
ApprovalController: this.approvalController,
|
||||||
@ -1521,6 +1532,7 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
smartTransactionsController,
|
smartTransactionsController,
|
||||||
txController,
|
txController,
|
||||||
assetsContractController,
|
assetsContractController,
|
||||||
|
backupController,
|
||||||
} = this;
|
} = this;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -1965,6 +1977,10 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
removePollingTokenFromAppState:
|
removePollingTokenFromAppState:
|
||||||
appStateController.removePollingToken.bind(appStateController),
|
appStateController.removePollingToken.bind(appStateController),
|
||||||
|
|
||||||
|
// BackupController
|
||||||
|
backupUserData: backupController.backupUserData.bind(backupController),
|
||||||
|
restoreUserData: backupController.restoreUserData.bind(backupController),
|
||||||
|
|
||||||
// DetectTokenController
|
// DetectTokenController
|
||||||
detectNewTokens: detectTokensController.detectNewTokens.bind(
|
detectNewTokens: detectTokensController.detectNewTokens.bind(
|
||||||
detectTokensController,
|
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();
|
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 path = require('path');
|
||||||
|
const { promises: fs } = require('fs');
|
||||||
const BigNumber = require('bignumber.js');
|
const BigNumber = require('bignumber.js');
|
||||||
const mockttp = require('mockttp');
|
const mockttp = require('mockttp');
|
||||||
const createStaticServer = require('../../development/create-static-server');
|
const createStaticServer = require('../../development/create-static-server');
|
||||||
@ -17,6 +18,11 @@ const largeDelayMs = regularDelayMs * 2;
|
|||||||
const veryLargeDelayMs = largeDelayMs * 2;
|
const veryLargeDelayMs = largeDelayMs * 2;
|
||||||
const dappBasePort = 8080;
|
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)}`;
|
const convertToHexValue = (val) => `0x${new BigNumber(val, 10).toString(16)}`;
|
||||||
|
|
||||||
async function withFixtures(options, testSuite) {
|
async function withFixtures(options, testSuite) {
|
||||||
@ -330,4 +336,5 @@ module.exports = {
|
|||||||
connectDappWithExtensionPopup,
|
connectDappWithExtensionPopup,
|
||||||
completeImportSRPOnboardingFlow,
|
completeImportSRPOnboardingFlow,
|
||||||
completeImportSRPOnboardingFlowWordByWord,
|
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 { strict: assert } = require('assert');
|
||||||
const { promises: fs } = require('fs');
|
const { promises: fs } = require('fs');
|
||||||
const { convertToHexValue, withFixtures } = require('../helpers');
|
const {
|
||||||
|
convertToHexValue,
|
||||||
|
withFixtures,
|
||||||
|
createDownloadFolder,
|
||||||
|
} = require('../helpers');
|
||||||
|
|
||||||
const downloadsFolder = `${process.cwd()}/test-artifacts/downloads`;
|
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 () => {
|
const stateLogsExist = async () => {
|
||||||
try {
|
try {
|
||||||
const stateLogs = `${downloadsFolder}/MetaMask state logs.json`;
|
const stateLogs = `${downloadsFolder}/MetaMask state logs.json`;
|
||||||
@ -38,7 +37,7 @@ describe('State logs', function () {
|
|||||||
failOnConsoleError: false,
|
failOnConsoleError: false,
|
||||||
},
|
},
|
||||||
async ({ driver }) => {
|
async ({ driver }) => {
|
||||||
await createDownloadFolder();
|
await createDownloadFolder(downloadsFolder);
|
||||||
await driver.navigate();
|
await driver.navigate();
|
||||||
await driver.fill('#password', 'correct horse battery staple');
|
await driver.fill('#password', 'correct horse battery staple');
|
||||||
await driver.press('#password', driver.Key.ENTER);
|
await driver.press('#password', driver.Key.ENTER);
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { exportAsFile } from '../../../helpers/utils/util';
|
|
||||||
import Copy from '../icon/copy-icon.component';
|
import Copy from '../icon/copy-icon.component';
|
||||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||||
import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
|
import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
|
||||||
|
import { exportAsFile } from '../../../../shared/modules/export-utils';
|
||||||
|
|
||||||
function ExportTextContainer({ text = '' }) {
|
function ExportTextContainer({ text = '' }) {
|
||||||
const t = useI18nContext();
|
const t = useI18nContext();
|
||||||
|
@ -353,4 +353,18 @@ export const SETTINGS_CONSTANTS = [
|
|||||||
route: `${EXPERIMENTAL_ROUTE}#show-custom-network`,
|
route: `${EXPERIMENTAL_ROUTE}#show-custom-network`,
|
||||||
icon: 'fa fa-flask',
|
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', () => {
|
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', () => {
|
it('should get good contact section number', () => {
|
||||||
|
@ -193,24 +193,6 @@ export function getRandomFileName() {
|
|||||||
return fileName;
|
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.
|
* Shortens an Ethereum address for display, preserving the beginning and end.
|
||||||
* Returns the given address if it is no longer than 10 characters.
|
* Returns the given address if it is no longer than 10 characters.
|
||||||
|
@ -6,8 +6,8 @@ import {
|
|||||||
INITIALIZE_END_OF_FLOW_ROUTE,
|
INITIALIZE_END_OF_FLOW_ROUTE,
|
||||||
INITIALIZE_SEED_PHRASE_ROUTE,
|
INITIALIZE_SEED_PHRASE_ROUTE,
|
||||||
} from '../../../../helpers/constants/routes';
|
} from '../../../../helpers/constants/routes';
|
||||||
import { exportAsFile } from '../../../../helpers/utils/util';
|
|
||||||
import { EVENT } from '../../../../../shared/constants/metametrics';
|
import { EVENT } from '../../../../../shared/constants/metametrics';
|
||||||
|
import { exportAsFile } from '../../../../../shared/modules/export-utils';
|
||||||
import DraggableSeed from './draggable-seed.component';
|
import DraggableSeed from './draggable-seed.component';
|
||||||
|
|
||||||
const EMPTY_SEEDS = Array(12).fill(null);
|
const EMPTY_SEEDS = Array(12).fill(null);
|
||||||
|
@ -10,9 +10,9 @@ import {
|
|||||||
DEFAULT_ROUTE,
|
DEFAULT_ROUTE,
|
||||||
INITIALIZE_SEED_PHRASE_INTRO_ROUTE,
|
INITIALIZE_SEED_PHRASE_INTRO_ROUTE,
|
||||||
} from '../../../../helpers/constants/routes';
|
} from '../../../../helpers/constants/routes';
|
||||||
import { exportAsFile } from '../../../../helpers/utils/util';
|
|
||||||
import { EVENT } from '../../../../../shared/constants/metametrics';
|
import { EVENT } from '../../../../../shared/constants/metametrics';
|
||||||
import { returnToOnboardingInitiatorTab } from '../../onboarding-initiator-util';
|
import { returnToOnboardingInitiatorTab } from '../../onboarding-initiator-util';
|
||||||
|
import { exportAsFile } from '../../../../../shared/modules/export-utils';
|
||||||
|
|
||||||
export default class RevealSeedPhrase extends PureComponent {
|
export default class RevealSeedPhrase extends PureComponent {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { exportAsFile } from '../../../helpers/utils/util';
|
|
||||||
import ToggleButton from '../../../components/ui/toggle-button';
|
import ToggleButton from '../../../components/ui/toggle-button';
|
||||||
import TextField from '../../../components/ui/text-field';
|
import TextField from '../../../components/ui/text-field';
|
||||||
import Button from '../../../components/ui/button';
|
import Button from '../../../components/ui/button';
|
||||||
@ -22,6 +21,8 @@ import {
|
|||||||
LEDGER_USB_VENDOR_ID,
|
LEDGER_USB_VENDOR_ID,
|
||||||
} from '../../../../shared/constants/hardware-wallets';
|
} from '../../../../shared/constants/hardware-wallets';
|
||||||
import { EVENT } from '../../../../shared/constants/metametrics';
|
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 {
|
export default class AdvancedTab extends PureComponent {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
@ -58,6 +59,8 @@ export default class AdvancedTab extends PureComponent {
|
|||||||
userHasALedgerAccount: PropTypes.bool.isRequired,
|
userHasALedgerAccount: PropTypes.bool.isRequired,
|
||||||
useTokenDetection: PropTypes.bool.isRequired,
|
useTokenDetection: PropTypes.bool.isRequired,
|
||||||
setUseTokenDetection: PropTypes.func.isRequired,
|
setUseTokenDetection: PropTypes.func.isRequired,
|
||||||
|
backupUserData: PropTypes.func.isRequired,
|
||||||
|
restoreUserData: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -66,6 +69,8 @@ export default class AdvancedTab extends PureComponent {
|
|||||||
ipfsGateway: this.props.ipfsGateway,
|
ipfsGateway: this.props.ipfsGateway,
|
||||||
ipfsGatewayError: '',
|
ipfsGatewayError: '',
|
||||||
showLedgerTransportWarning: false,
|
showLedgerTransportWarning: false,
|
||||||
|
showResultMessage: false,
|
||||||
|
restoreSuccessful: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
settingsRefs = Array(
|
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() {
|
renderStateLogs() {
|
||||||
const { t } = this.context;
|
const { t } = this.context;
|
||||||
const { displayWarning } = this.props;
|
const { displayWarning } = this.props;
|
||||||
@ -730,6 +852,8 @@ export default class AdvancedTab extends PureComponent {
|
|||||||
{this.renderToggleTestNetworks()}
|
{this.renderToggleTestNetworks()}
|
||||||
{this.renderUseNonceOptIn()}
|
{this.renderUseNonceOptIn()}
|
||||||
{this.renderAutoLockTimeLimit()}
|
{this.renderAutoLockTimeLimit()}
|
||||||
|
{this.renderUserDataBackup()}
|
||||||
|
{this.renderRestoreUserData()}
|
||||||
{this.renderThreeBoxControl()}
|
{this.renderThreeBoxControl()}
|
||||||
{this.renderIpfsGatewayControl()}
|
{this.renderIpfsGatewayControl()}
|
||||||
{notUsingFirefox ? this.renderLedgerLiveControl() : null}
|
{notUsingFirefox ? this.renderLedgerLiveControl() : null}
|
||||||
|
@ -31,6 +31,8 @@ describe('AdvancedTab Component', () => {
|
|||||||
useTokenDetection
|
useTokenDetection
|
||||||
setUseTokenDetection={toggleTokenDetection}
|
setUseTokenDetection={toggleTokenDetection}
|
||||||
userHasALedgerAccount
|
userHasALedgerAccount
|
||||||
|
backupUserData={() => undefined}
|
||||||
|
restoreUserData={() => undefined}
|
||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
context: {
|
context: {
|
||||||
@ -41,7 +43,69 @@ describe('AdvancedTab Component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should render correctly when threeBoxFeatureFlag', () => {
|
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', () => {
|
it('should update autoLockTimeLimit', () => {
|
||||||
@ -63,6 +127,8 @@ describe('AdvancedTab Component', () => {
|
|||||||
useTokenDetection
|
useTokenDetection
|
||||||
setUseTokenDetection={toggleTokenDetection}
|
setUseTokenDetection={toggleTokenDetection}
|
||||||
userHasALedgerAccount
|
userHasALedgerAccount
|
||||||
|
backupUserData={() => undefined}
|
||||||
|
restoreUserData={() => undefined}
|
||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
context: {
|
context: {
|
||||||
@ -108,6 +174,8 @@ describe('AdvancedTab Component', () => {
|
|||||||
useTokenDetection
|
useTokenDetection
|
||||||
setUseTokenDetection={toggleTokenDetection}
|
setUseTokenDetection={toggleTokenDetection}
|
||||||
userHasALedgerAccount
|
userHasALedgerAccount
|
||||||
|
backupUserData={() => undefined}
|
||||||
|
restoreUserData={() => undefined}
|
||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
context: {
|
context: {
|
||||||
@ -145,6 +213,8 @@ describe('AdvancedTab Component', () => {
|
|||||||
useTokenDetection
|
useTokenDetection
|
||||||
setUseTokenDetection={toggleTokenDetection}
|
setUseTokenDetection={toggleTokenDetection}
|
||||||
userHasALedgerAccount
|
userHasALedgerAccount
|
||||||
|
backupUserData={() => undefined}
|
||||||
|
restoreUserData={() => undefined}
|
||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
context: {
|
context: {
|
||||||
|
@ -15,6 +15,8 @@ import {
|
|||||||
setLedgerTransportPreference,
|
setLedgerTransportPreference,
|
||||||
setDismissSeedBackUpReminder,
|
setDismissSeedBackUpReminder,
|
||||||
setUseTokenDetection,
|
setUseTokenDetection,
|
||||||
|
backupUserData,
|
||||||
|
restoreUserData,
|
||||||
} from '../../../store/actions';
|
} from '../../../store/actions';
|
||||||
import { getPreferences } from '../../../selectors';
|
import { getPreferences } from '../../../selectors';
|
||||||
import { doesUserHaveALedgerAccount } from '../../../ducks/metamask/metamask';
|
import { doesUserHaveALedgerAccount } from '../../../ducks/metamask/metamask';
|
||||||
@ -63,6 +65,8 @@ export const mapStateToProps = (state) => {
|
|||||||
|
|
||||||
export const mapDispatchToProps = (dispatch) => {
|
export const mapDispatchToProps = (dispatch) => {
|
||||||
return {
|
return {
|
||||||
|
backupUserData: () => backupUserData(),
|
||||||
|
restoreUserData: (jsonString) => restoreUserData(jsonString),
|
||||||
setHexDataFeatureFlag: (shouldShow) =>
|
setHexDataFeatureFlag: (shouldShow) =>
|
||||||
dispatch(setFeatureFlag('sendHexData', shouldShow)),
|
dispatch(setFeatureFlag('sendHexData', shouldShow)),
|
||||||
displayWarning: (warning) => dispatch(displayWarning(warning)),
|
displayWarning: (warning) => dispatch(displayWarning(warning)),
|
||||||
|
@ -12,6 +12,12 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column nowrap;
|
flex-flow: column nowrap;
|
||||||
|
|
||||||
|
&__error-text {
|
||||||
|
@include H7;
|
||||||
|
|
||||||
|
color: var(--color-error-default);
|
||||||
|
}
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
padding: 8px 24px 8px 24px;
|
padding: 8px 24px 8px 24px;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -350,6 +356,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
&__copy-icon {
|
&__copy-icon {
|
||||||
padding-left: 4px;
|
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) {
|
export function updateTransactionGasFees(txId, txGasFees) {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
let updatedTransaction;
|
let updatedTransaction;
|
||||||
|
Loading…
Reference in New Issue
Block a user