From 7bffa131e199a734554b2aa606c1f2a3bd0c99b7 Mon Sep 17 00:00:00 2001 From: Willy Bruns Date: Wed, 26 Oct 2016 13:22:50 -0700 Subject: [PATCH] Add "Import recovery keys" button to Payments->Advanced->Recover (opens a file chooser dialog) fixes #4806 Includes tests for Ledger backup and recovery: - add advanced ledger panel tests file `test/components/ledgerPanelAdvancedPanelTest.js` - add tests for backup and recovery of wallet - add commands to Brave test client (`ipcOnce`, `ipcSendRendererSync`, and `translations`) client.translations: returns a map of all existing translations (current locale) to test client Import recovery keys success closes modals - successful import closes modals - and closing file chooser dialog does not trigger error screen fixes #6263 Import recovery keys shows error popover if keys are invalid or missing - error popover is displayed if paymentId / passphrase are missing or not UUIDs (ledger-client#recoverWallet should probably do validation too) - added tests for cases: one or both recovery keys missing from file a recovery key is not a UUID an empty recovery file --- .../locales/en-US/preferences.properties | 1 + app/filtering.js | 4 +- app/ledger.js | 102 +++++++- docs/appActions.md | 4 +- js/about/aboutActions.js | 7 + js/about/preferences.js | 27 ++- js/actions/appActions.js | 4 +- .../ledgerPanelAdvancedPanelTest.js | 225 ++++++++++++++++++ test/components/ledgerPanelTest.js | 2 +- test/lib/brave.js | 28 ++- test/lib/selectors.js | 11 +- 11 files changed, 396 insertions(+), 19 deletions(-) create mode 100644 test/components/ledgerPanelAdvancedPanelTest.js diff --git a/app/extensions/brave/locales/en-US/preferences.properties b/app/extensions/brave/locales/en-US/preferences.properties index ba0f439a469..c2d0e190796 100644 --- a/app/extensions/brave/locales/en-US/preferences.properties +++ b/app/extensions/brave/locales/en-US/preferences.properties @@ -160,6 +160,7 @@ backupLedger=Backup your wallet balanceRecovered={{balance}} BTC was recovered and transferred to your Brave wallet. recoverLedger=Recover your wallet recover=Recover +recoverFromFile=Import recovery keys printKeys=Print keys saveRecoveryFile=Save recovery file... advancedPrivacySettings=Advanced Privacy Settings: diff --git a/app/filtering.js b/app/filtering.js index ced17ccd81b..c40f398cd97 100644 --- a/app/filtering.js +++ b/app/filtering.js @@ -490,12 +490,14 @@ function registerForDownloadListener (session) { } const defaultPath = path.join(getSetting(settings.DEFAULT_DOWNLOAD_SAVE_PATH) || app.getPath('downloads'), itemFilename) - const savePath = dialog.showSaveDialog(win, { defaultPath }) + const savePath = (process.env.SPECTRON ? defaultPath : dialog.showSaveDialog(win, { defaultPath })) + // User cancelled out of save dialog prompt if (!savePath) { event.preventDefault() return } + item.setSavePath(savePath) appActions.changeSetting(settings.DEFAULT_DOWNLOAD_SAVE_PATH, path.dirname(savePath)) diff --git a/app/ledger.js b/app/ledger.js index 8d92f55f4ac..251cdbbebc4 100644 --- a/app/ledger.js +++ b/app/ledger.js @@ -255,6 +255,7 @@ var backupKeys = (appState, action) => { const date = moment().format('L') const paymentId = appState.getIn(['ledgerInfo', 'paymentId']) const passphrase = appState.getIn(['ledgerInfo', 'passphrase']) + const messageLines = [ locale.translation('ledgerBackupText1'), [locale.translation('ledgerBackupText2'), date].join(' '), @@ -264,6 +265,7 @@ var backupKeys = (appState, action) => { '', locale.translation('ledgerBackupText5') ] + const message = messageLines.join(os.EOL) const filePath = path.join(app.getPath('userData'), '/brave_wallet_recovery.txt') @@ -284,14 +286,83 @@ var backupKeys = (appState, action) => { return appState } +var loadKeysFromBackupFile = (filePath) => { + let keys = null + let data = fs.readFileSync(filePath) + + if (!data || !data.length || !(data.toString())) { + logError('No data in backup file', 'recoveryWallet') + } else { + try { + const recoveryFileContents = data.toString() + + let messageLines = recoveryFileContents.split(os.EOL) + + let paymentIdLine = '' || messageLines[3] + let passphraseLine = '' || messageLines[4] + + const paymentIdPattern = new RegExp([locale.translation('ledgerBackupText3'), '([^ ]+)'].join(' ')) + const paymentId = (paymentIdLine.match(paymentIdPattern) || [])[1] + + const passphrasePattern = new RegExp([locale.translation('ledgerBackupText4'), '(.+)$'].join(' ')) + const passphrase = (passphraseLine.match(passphrasePattern) || [])[1] + + keys = { + paymentId, + passphrase + } + } catch (exc) { + logError(exc, 'recoveryWallet') + } + } + + return keys +} + /* * Recover Ledger Keys */ var recoverKeys = (appState, action) => { - client.recoverWallet(action.firstRecoveryKey, action.secondRecoveryKey, (err, body) => { - if (logError(err, 'recoveryWallet')) appActions.updateLedgerInfo(underscore.omit(ledgerInfo, [ '_internal' ])) - if (err) { + let firstRecoveryKey, secondRecoveryKey + + if (action.useRecoveryKeyFile) { + let recoveryKeyFile = promptForRecoveryKeyFile() + if (!recoveryKeyFile) { + // user canceled from dialog, we abort without error + return appState + } + + if (recoveryKeyFile) { + let keys = loadKeysFromBackupFile(recoveryKeyFile) || {} + + if (keys) { + firstRecoveryKey = keys.paymentId + secondRecoveryKey = keys.passphrase + } + } + } + + if (!firstRecoveryKey || !secondRecoveryKey) { + firstRecoveryKey = action.firstRecoveryKey + secondRecoveryKey = action.secondRecoveryKey + } + + const UUID_REGEX = /^[0-9a-z]{8}\-[0-9a-z]{4}\-[0-9a-z]{4}\-[0-9a-z]{4}\-[0-9a-z]{12}$/ + if (typeof firstRecoveryKey !== 'string' || !firstRecoveryKey.match(UUID_REGEX) || typeof secondRecoveryKey !== 'string' || !secondRecoveryKey.match(UUID_REGEX)) { + setImmediate(() => appActions.ledgerRecoveryFailed()) + return appState + } + + client.recoverWallet(firstRecoveryKey, secondRecoveryKey, (err, body) => { + let existingLedgerError = ledgerInfo.error + + if (logError(err, 'recoveryWallet')) { + // we reset ledgerInfo.error to what it was before (likely null) + // if ledgerInfo.error is not null, the wallet info will not display in UI + // logError sets ledgerInfo.error, so we must we clear it or UI will show an error + ledgerInfo.error = existingLedgerError + appActions.updateLedgerInfo(underscore.omit(ledgerInfo, [ '_internal' ])) setImmediate(() => appActions.ledgerRecoveryFailed()) } else { setImmediate(() => appActions.ledgerRecoverySucceeded()) @@ -301,6 +372,31 @@ var recoverKeys = (appState, action) => { return appState } +const dialog = electron.dialog + +var promptForRecoveryKeyFile = () => { + const defaultRecoveryKeyFilePath = path.join(app.getPath('userData'), '/brave_wallet_recovery.txt') + + let files + + if (process.env.SPECTRON) { + // skip the dialog for tests + console.log(`for test, trying to recover keys from path: ${defaultRecoveryKeyFilePath}`) + files = [defaultRecoveryKeyFilePath] + } else { + files = dialog.showOpenDialog({ + properties: ['openFile'], + defaultPath: defaultRecoveryKeyFilePath, + filters: [{ + name: 'TXT files', + extensions: ['txt'] + }] + }) + } + + return (files && files.length ? files[0] : null) +} + /* * IPC entry point */ diff --git a/docs/appActions.md b/docs/appActions.md index f202b40ea5c..e12ab040a08 100644 --- a/docs/appActions.md +++ b/docs/appActions.md @@ -146,13 +146,13 @@ Dispatches a message to clear all completed downloads ### ledgerRecoverySucceeded() -Dispatches a message to clear all completed downloads +Dispatches a message indicating ledger recovery succeeded ### ledgerRecoveryFailed() -Dispatches a message to clear all completed downloads +Dispatches a message indicating ledger recovery failed diff --git a/js/about/aboutActions.js b/js/about/aboutActions.js index 05f537470ed..77eb70d4d02 100644 --- a/js/about/aboutActions.js +++ b/js/about/aboutActions.js @@ -108,6 +108,13 @@ const aboutActions = { }) }, + ledgerRecoverWalletFromFile: function () { + aboutActions.dispatchAction({ + actionType: appConstants.APP_RECOVER_WALLET, + useRecoveryKeyFile: true + }) + }, + /** * Clear wallet recovery status */ diff --git a/js/about/preferences.js b/js/about/preferences.js index f1d3d575bba..aa6b253bb3e 100644 --- a/js/about/preferences.js +++ b/js/about/preferences.js @@ -900,6 +900,10 @@ class PaymentsTab extends ImmutableComponent { aboutActions.ledgerRecoverWallet(this.state.FirstRecoveryKey, this.state.SecondRecoveryKey) } + recoverWalletFromFile () { + aboutActions.ledgerRecoverWalletFromFile() + } + copyToClipboard (text) { aboutActions.setClipboard(text) } @@ -910,6 +914,7 @@ class PaymentsTab extends ImmutableComponent { clearRecoveryStatus () { aboutActions.clearRecoveryStatus() + this.props.hideAdvancedOverlays() } printKeys () { @@ -1142,23 +1147,26 @@ class PaymentsTab extends ImmutableComponent { } get ledgerRecoveryContent () { + let balance = this.props.ledgerData.get('balance') + const l10nDataArgs = { - balance: this.props.ledgerData.get('balance') + balance: (!balance ? '0.00' : balance) } + return
{ this.props.ledgerData.get('recoverySucceeded') === true ?

Success!

-

: null } { this.props.ledgerData.get('recoverySucceeded') === false ?
-

Recovery failed

+

Recovery failed

Please re-enter keys or try different keys.

@@ -1183,6 +1191,7 @@ class PaymentsTab extends ImmutableComponent { return
@@ -1849,6 +1858,15 @@ class AboutPreferences extends React.Component { this.updateTabFromAnchor = this.updateTabFromAnchor.bind(this) } + hideAdvancedOverlays () { + this.setState({ + advancedSettingsOverlayVisible: false, + ledgerBackupOverlayVisible: false, + ledgerRecoveryOverlayVisible: false + }) + this.forceUpdate() + } + componentDidMount () { window.addEventListener('popstate', this.updateTabFromAnchor) } @@ -1974,7 +1992,8 @@ class AboutPreferences extends React.Component { ledgerRecoveryOverlayVisible={this.state.ledgerRecoveryOverlayVisible} addFundsOverlayVisible={this.state.addFundsOverlayVisible} showOverlay={this.setOverlayVisible.bind(this, true)} - hideOverlay={this.setOverlayVisible.bind(this, false)} /> + hideOverlay={this.setOverlayVisible.bind(this, false)} + hideAdvancedOverlays={this.hideAdvancedOverlays.bind(this)} /> break case preferenceTabs.SECURITY: tab = diff --git a/js/actions/appActions.js b/js/actions/appActions.js index 5c8e8045aef..61d346a24b9 100644 --- a/js/actions/appActions.js +++ b/js/actions/appActions.js @@ -192,7 +192,7 @@ const appActions = { }, /** - * Dispatches a message to clear all completed downloads + * Dispatches a message indicating ledger recovery succeeded */ ledgerRecoverySucceeded: function () { AppDispatcher.dispatch({ @@ -201,7 +201,7 @@ const appActions = { }, /** - * Dispatches a message to clear all completed downloads + * Dispatches a message indicating ledger recovery failed */ ledgerRecoveryFailed: function () { AppDispatcher.dispatch({ diff --git a/test/components/ledgerPanelAdvancedPanelTest.js b/test/components/ledgerPanelAdvancedPanelTest.js new file mode 100644 index 00000000000..9bcc7a88063 --- /dev/null +++ b/test/components/ledgerPanelAdvancedPanelTest.js @@ -0,0 +1,225 @@ +/* global describe, it, beforeEach */ + +const Brave = require('../lib/brave') +const {urlInput, paymentsWelcomePage, paymentsTab, walletSwitch, backupWallet, recoverWallet, saveWalletFile, advancedSettingsButton, recoverWalletFromFileButton, balanceRecovered, balanceNotRecovered, recoveryOverlayOkButton} = require('../lib/selectors') +const messages = require('../../js/constants/messages') + +const assert = require('assert') + +const prefsUrl = 'about:preferences' +const ledgerAPIWaitTimeout = 60000 + +const fs = require('fs') +const os = require('os') +const path = require('path') +const urlParse = require('url').parse +const uuid = require('uuid') + +const WALLET_RECOVERY_FILE_BASENAME = 'brave_wallet_recovery.txt' +const PAYMENT_ID_TRANSLATION_KEY = 'ledgerBackupText3' +const PASSPHRASE_TRANSLATION_KEY = 'ledgerBackupText4' + +const moment = require('moment') + +let translationsCache = null + +function setup (client) { + return client + .translations().then(function (translations) { + if (translations && translations.value) { + translationsCache = translations.value + } + return this + }) + .waitForBrowserWindow() + .waitForVisible(urlInput) +} + +function* setupPaymentsTabAndOpenAdvancedSettings (client, tabAlreadyLoaded) { + yield client + .tabByIndex(0) + + if (!tabAlreadyLoaded) { + yield client + .loadUrl(prefsUrl) + .waitForVisible(paymentsTab) + .click(paymentsTab) + .waitForVisible(paymentsWelcomePage) + .waitForVisible(walletSwitch) + .click(walletSwitch) + .waitForVisible(advancedSettingsButton, ledgerAPIWaitTimeout) + } + + yield client.click(advancedSettingsButton) +} + +function validateRecoveryFile (recoveryFileContents) { + const UUID_LENGTH = 36 + const RECOVERY_FILE_EXPECTED_NUM_LINES = 7 + + assert.equal(typeof recoveryFileContents, 'string', 'recovery file should contain a string') + + let messageLines = recoveryFileContents.split(os.EOL) + + assert.equal(messageLines.length, RECOVERY_FILE_EXPECTED_NUM_LINES, 'recovery file should have the expected number of lines') + + const paymentIdPrefixText = translationsCache[PAYMENT_ID_TRANSLATION_KEY] + assert.equal(typeof paymentIdPrefixText, 'string', `payment ID prefix text ("${PAYMENT_ID_TRANSLATION_KEY}") should exist in translation cache`) + + const passphrasePrefixText = translationsCache[PASSPHRASE_TRANSLATION_KEY] + assert.equal(typeof passphrasePrefixText, 'string', `passphrase prefix text ("${PASSPHRASE_TRANSLATION_KEY}") should exist in translation cache`) + + let paymentIdLine = '' || messageLines[3] + assert.equal(typeof paymentIdLine === 'string' && paymentIdLine.length >= paymentIdPrefixText.length + UUID_LENGTH, true) + + let passphraseLine = '' || messageLines[4] + assert.equal(typeof passphraseLine === 'string' && passphraseLine.length >= passphrasePrefixText.length + UUID_LENGTH, true) + + const paymentIdPattern = new RegExp([paymentIdPrefixText, '([^ ]+)'].join(' ')) + const paymentId = (paymentIdLine.match(paymentIdPattern) || [])[1] + assert.ok(paymentId) + assert.equal(typeof paymentId, 'string') + console.log(`recovered paymentId: ${paymentId}`) + + const passphrasePattern = new RegExp([passphrasePrefixText, '(.+)$'].join(' ')) + const passphrase = (passphraseLine.match(passphrasePattern) || [])[1] + assert.ok(passphrase) + assert.equal(typeof passphrase, 'string') + console.log(`recovered passphrase: ${passphrase}`) + + // validate that paymentId and passphrase are uuids here + const UUID_REGEX = /^[0-9a-z]{8}\-[0-9a-z]{4}\-[0-9a-z]{4}\-[0-9a-z]{4}\-[0-9a-z]{12}$/ + assert.ok(paymentId.match(UUID_REGEX), 'paymentId should be a valid UUID') + assert.ok(passphrase.match(UUID_REGEX), 'passphrase should be a valid UUID') + + return true +} + +let recoverWalletFromFile = function * (client) { + yield setupPaymentsTabAndOpenAdvancedSettings(client, true) + + // open "Recover your wallet" submodal and click "Import recovery keys" + yield client + .waitForVisible(recoverWallet, ledgerAPIWaitTimeout) + .click(recoverWallet) + .waitForVisible(recoverWalletFromFileButton, ledgerAPIWaitTimeout) + .click(recoverWalletFromFileButton) +} + +let generateAndSaveRecoveryFile = function (recoveryFilePath, paymentId, passphrase) { + let recoveryFileContents = '' + + if (typeof paymentId === 'string' || typeof passphrase === 'string') { + const date = moment().format('L') + + const messageLines = [ + translationsCache['ledgerBackupText1'], + [translationsCache['ledgerBackupText2'], date].join(' '), + '', + [translationsCache['ledgerBackupText3'], paymentId].join(' '), + [translationsCache['ledgerBackupText4'], passphrase].join(' '), + '', + translationsCache['ledgerBackupText5'] + ] + + recoveryFileContents = messageLines.join(os.EOL) + } + + fs.writeFileSync(recoveryFilePath, recoveryFileContents) + + return +} + +describe('Payments Panel -> Advanced Panel', function () { + let context = this + Brave.beforeEach(this) + + beforeEach(function * () { + yield setup(this.app.client) + }) + + it('can backup wallet to file', function * () { + context.cleanSessionStoreAfterEach = false + + yield setupPaymentsTabAndOpenAdvancedSettings(this.app.client) + + // open "Backup your wallet" sub-modal and click "Save recovery file..." + yield this.app.client + .waitForVisible(backupWallet, ledgerAPIWaitTimeout) + .click(backupWallet) + .waitForVisible(saveWalletFile, ledgerAPIWaitTimeout) + .click(saveWalletFile) + + const windowHandlesResponse = yield this.app.client.windowHandles() + const windowHandles = windowHandlesResponse.value + + // confirm the saved backup file is opened in a new tab + yield this.app.client.window(windowHandles[0]) + .ipcSend('shortcut-focus-url') + .waitForElementFocus(urlInput, ledgerAPIWaitTimeout) + .waitUntil(function () { + return this.getValue(urlInput) + .then(function (urlString) { + // VERIFY that the downloaded recovery file is opened in new tab + assert.equal(typeof urlString, 'string') + let urlObj = urlParse(urlString) + assert.ok(urlObj) + assert.equal(urlObj.protocol, 'file:') + assert.equal(path.basename(urlObj.pathname), WALLET_RECOVERY_FILE_BASENAME) + + // VERIFY contents of downloaded recovery file + let pathname = urlObj.pathname + // this is a test, so OK to throw exception in here + let recoveryFileContents = fs.readFileSync(pathname).toString() + + context.recoveryFilePathname = pathname + + return validateRecoveryFile(recoveryFileContents) + }) + }) + .pause(1000) + .ipcSend(messages.SHORTCUT_CLOSE_FRAME, 2) + .pause(1000) + .ipcSend('shortcut-focus-url') + }) + + it('can recover wallet from file', function * () { + yield recoverWalletFromFile(this.app.client) + + yield this.app.client + .waitForVisible(balanceRecovered, ledgerAPIWaitTimeout) + .waitForVisible(recoveryOverlayOkButton, ledgerAPIWaitTimeout) + .click(recoveryOverlayOkButton) + .pause(1000) + }) + + let randomPaymentId = uuid.v4().toLowerCase() + + it('shows an error popover if one recovery key is missing', function * () { + generateAndSaveRecoveryFile(context.recoveryFilePathname, randomPaymentId, '') + yield recoverWalletFromFile(this.app.client) + yield this.app.client + .waitForVisible(balanceNotRecovered, ledgerAPIWaitTimeout) + }) + + it('shows an error popover if a recovery key is not a UUID', function * () { + generateAndSaveRecoveryFile(context.recoveryFilePathname, randomPaymentId, 'not-a-uuid') + yield recoverWalletFromFile(this.app.client) + yield this.app.client + .waitForVisible(balanceNotRecovered, ledgerAPIWaitTimeout) + }) + + it('shows an error popover if both recovery keys are missing', function * () { + generateAndSaveRecoveryFile(context.recoveryFilePathname, '', '') + yield recoverWalletFromFile(this.app.client) + yield this.app.client + .waitForVisible(balanceNotRecovered, ledgerAPIWaitTimeout) + }) + + it('shows an error popover if the file is empty', function * () { + generateAndSaveRecoveryFile(context.recoveryFilePathname) + yield recoverWalletFromFile(this.app.client) + yield this.app.client + .waitForVisible(balanceNotRecovered, ledgerAPIWaitTimeout) + }) +}) diff --git a/test/components/ledgerPanelTest.js b/test/components/ledgerPanelTest.js index 05bac8cedb3..a2eaeda7e7e 100644 --- a/test/components/ledgerPanelTest.js +++ b/test/components/ledgerPanelTest.js @@ -5,7 +5,7 @@ const {urlInput, advancedSettings, addFundsButton, paymentsStatus, paymentsWelco const assert = require('assert') const prefsUrl = 'about:preferences' -const ledgerAPIWaitTimeout = 10000 +const ledgerAPIWaitTimeout = 20000 function * setup (client) { yield client diff --git a/test/lib/brave.js b/test/lib/brave.js index 57f59cc1f1e..bb041e65542 100644 --- a/test/lib/brave.js +++ b/test/lib/brave.js @@ -149,7 +149,7 @@ var exports = { }) context.afterEach(function () { - return exports.stopApp.call(this) + return exports.stopApp.call(this, context.cleanSessionStoreAfterEach) }) }, @@ -172,6 +172,12 @@ var exports = { }, message, ...param).then((response) => response.value) }) + this.app.client.addCommand('ipcSendRendererSync', function (message, ...param) { + return this.execute(function (message, ...param) { + return devTools('electron').ipcRenderer.sendSync(message, ...param) + }, message, ...param) + }) + var windowHandlesOrig = this.app.client.windowHandles Object.getPrototypeOf(this.app.client).windowHandles = function () { return windowHandlesOrig.apply(this) @@ -435,6 +441,12 @@ var exports = { }, message, fn).then((response) => response.value) }) + this.app.client.addCommand('ipcOnce', function (message, fn) { + return this.execute(function (message, fn) { + return devTools('electron').remote.getCurrentWindow().webContents.once(message, fn) + }, message, fn).then((response) => response.value) + }) + this.app.client.addCommand('newWindowAction', function (frameOpts, browserOpts) { return this.execute(function () { return devTools('appActions').newWindow() @@ -648,9 +660,9 @@ var exports = { }, frameKey, eventName, ...params).then((response) => response.value) }) - this.app.client.addCommand('waitForElementFocus', function (selector) { + this.app.client.addCommand('waitForElementFocus', function (selector, timeout) { let activeElement - return this.waitForVisible(selector) + return this.waitForVisible(selector, timeout) .element(selector) .then(function (el) { activeElement = el }) .waitUntil(function () { @@ -658,7 +670,7 @@ var exports = { .then(function (el) { return el.value.ELEMENT === activeElement.value.ELEMENT }) - }) + }, timeout) }) this.app.client.addCommand('waitForDataFile', function (dataFile) { @@ -670,6 +682,11 @@ var exports = { }) }, 10000) }) + + // retrieve a map of all the translations per existing IPC message 'translations' + this.app.client.addCommand('translations', function () { + return this.ipcSendRendererSync('translations') + }) }, startApp: function () { @@ -678,7 +695,8 @@ var exports = { } let env = { NODE_ENV: 'test', - BRAVE_USER_DATA_DIR: userDataDir + BRAVE_USER_DATA_DIR: userDataDir, + SPECTRON: true } this.app = new Application({ waitTimeout: exports.defaultTimeout, diff --git a/test/lib/selectors.js b/test/lib/selectors.js index 9069427fad9..d1cc7cdd0bb 100644 --- a/test/lib/selectors.js +++ b/test/lib/selectors.js @@ -65,6 +65,14 @@ module.exports = { siteSettingItem: '.siteSettingItem', ledgerTable: '.ledgerTable', bitcoinDashboard: '.bitcoinDashboard', + advancedSettingsButton: '[data-l10n-id="advancedSettings"]', + backupWallet: '[data-l10n-id="backupLedger"]', + recoverWallet: '[data-l10n-id="recoverLedger"]', + recoverWalletFromFileButton: '[data-l10n-id="recoverFromFile"]', + recoveryOverlayOkButton: '.recoveryOverlay [data-l10n-id="ok"]', + saveWalletFile: '[data-l10n-id="saveRecoveryFile"]', + balanceRecovered: '[data-l10n-id="balanceRecovered"]', + balanceNotRecovered: '.recoveryError', modalCloseButton: 'button.close', coinbaseBuyButton: '[data-l10n-id="add"]', paymentQRCode: '[title="Brave wallet QR code"]', @@ -79,5 +87,6 @@ module.exports = { dismissDenyRunInsecureContentButton: '.dismissDenyRunInsecureContentButton', tabsToolbar: '.tabsToolbar', hamburgerMenu: '.menuButton', - contextMenu: '.contextMenu' + contextMenu: '.contextMenu', + okButton: '[data-l10n-id="ok"]' }