From ab8792974a4b48de5307af60818ce8b4e0073598 Mon Sep 17 00:00:00 2001 From: Danilo Date: Thu, 26 Sep 2019 15:03:20 +0200 Subject: [PATCH 1/3] [DDW-893] Implement wallet recovery phrase verification (#1565) * [DDW-893] UI Init * [DDW-893] CHANGELOG * [DDW-893] UI progress * [DDW-893] UI - progress * [DDW-893] Wallet Settings page UI and logic * [DDW-893] Lint * [DDW-893] Dialogs - init * [DDW-893] Dialogs - containers and buttons * [DDW-893] Dialogs - basic styling * [DDW-893] Dialogs - basic styling * [DDW-893] Dialogs - intl * [DDW-893] Dialogs - translations manager * [DDW-893] Dialogs - update Storybook * [DDW-893] Dialogs - Change 'safetyAgreement' logic * [DDW-893] Styling adjustments * [DDW-893] Correct button naming * [DDW-893] Remove unused dummy data * [DDW-893] Change local wallet data logic location * [DDW-893] updates mnemonicsConfirmationDate * [DDW-893] Move wallet status logic * [DDW-893] Sidebar notification * [DDW-893] Integrate new API endpoint * [DDW-893] Wallets list notification * [DDW-893] Implement actions and store logic * [DDW-893] Flow errors * [DDW-893] Flow errors * [DDW-893] Translation manager * [DDW-893] Introduce Recovery Phrase Autocomplete * [DDW-893] Wallet Recovery Phrase validation and error messages * [DDW-893] Prevent closing when entering mnemonics * [DDW-893] Handles ElectronStore not available * [DDW-893] Fixes broken Storybook story * [DDW-893] Removes local data when deleting a wallet * [DDW-893] Update get wallet id and balance API endpoint * [DDW-893] Wallet nav notification * [DDW-893] Replace isVerifying component inner state with request isExecuting flag * [DDW-893] Styling adjustment * [DDW-893] Support portal link * [DDW-893] Adjustment * [DDW-893] Styling adjustments * [DDW-893] CSS Variables * [DDW-893] Correct time until * [DDW-893] Translation manager * [DDW-893] Update CHANGELOG * [DDW-893] Japanese translation * [DDW-893] Japanese translation * [DDW-893] Japanese translation * [DDW-893] Japanese translation * [DDW-893] Japanese translation * [DDW-893] Bg color for OK status * [DDW-893] Checkbox labels * [DDW-893] Keeps button verifying state after request is finished * [DDW-893] Removes dummy data from store * [DDW-893] Removes unused styling * [DDW-893] Fixes wrong dropdown positioning * [DDW-893] Wallet LocalStorage as a class * [DDW-893] Flow errors * [DDW-893] Refactory - init * [DDW-893] Adjustments * [DDW-893] Refactory - progress * [DDW-893] Refactory - progress * [DDW-893] Restores nav notification * [DDW-893] Updates wallet local after verifying * [DDW-893] Removes local data when deleting a wallet * [DDW-893] Reverts removing logs * [DDW-893] Removes unfinished test file * [DDW-893] E2E tests - WIP * [DDW-893] E2E tests - WIP * [DDW-893] Working E2E tests * [DDW-893] Improves E2E tests * [DDW-893] Translation manager * [DDW-893] Fixes wrong font variable introduced in [DDW-757] * [DDW-893] Automatic update message improvement * [DDW-893] Code improvements * [DDW-893] Fix broken E2E tests * [DDW-893] Bump cardano-sl revision * [DDW-893] Fix failing acceptance test * [DDW-893] Replace notification threshold from 150 to 183 days * [DDW-893] Better intl variable naming * [DDW-893] English proofreading changes * [DDW-893] Japanese translation --- CHANGELOG.md | 1 + cardano-sl-src.json | 6 +- features/node-update-notification.feature | 6 +- features/tests/e2e/helpers/wallets-helpers.js | 24 +- .../steps/node-update-notification-steps.js | 2 - ...llet-recovery-phrase-verification-steps.js | 94 ++++ ...tings-recovery-phrase-verification.feature | 32 ++ .../app/actions/wallet-backup-actions.js | 7 +- .../renderer/app/actions/wallets-actions.js | 2 + source/renderer/app/api/api.js | 36 ++ source/renderer/app/api/utils/localStorage.js | 76 +++ .../wallets/requests/getWalletIdAndBalance.js | 23 + source/renderer/app/api/wallets/types.js | 19 +- ...hrase-verification-notification.inline.svg | 3 + ...recovery-phrase-verification-ok.inline.svg | 3 + ...ery-phrase-verification-warning.inline.svg | 3 + .../app/components/navigation/NavButton.js | 4 +- .../app/components/navigation/NavButton.scss | 17 + .../app/components/navigation/NavDropdown.js | 19 +- .../components/navigation/NavDropdown.scss | 18 +- .../app/components/navigation/Navigation.js | 6 +- .../AutomaticUpdateNotification.js | 2 +- .../sidebar/wallets/SidebarWalletMenuItem.js | 5 + .../wallets/SidebarWalletMenuItem.scss | 16 + .../sidebar/wallets/SidebarWalletsMenu.js | 3 + .../status/DaedalusDiagnostics.scss | 2 +- .../wallet/layouts/WalletWithNavigation.js | 3 + .../wallet/navigation/WalletNavigation.js | 10 +- .../wallet/settings/WalletRecoveryPhrase.js | 253 ++++++++++ .../wallet/settings/WalletRecoveryPhrase.scss | 56 +++ .../WalletRecoveryPhraseStep1Dialog.js | 101 ++++ .../WalletRecoveryPhraseStep2Dialog.js | 161 ++++++ .../WalletRecoveryPhraseStep3Dialog.js | 101 ++++ .../WalletRecoveryPhraseStep4Dialog.js | 113 +++++ .../WalletRecoveryPhraseStepDialogs.scss | 77 +++ .../wallet/settings/WalletSettings.js | 40 ++ source/renderer/app/config/walletsConfig.js | 4 + .../renderer/app/containers/wallet/Wallet.js | 19 +- .../containers/wallet/WalletSettingsPage.js | 43 +- .../WalletRecoveryPhraseStep1Container.js | 35 ++ .../WalletRecoveryPhraseStep2Container.js | 72 +++ .../WalletRecoveryPhraseStep3Container.js | 25 + .../WalletRecoveryPhraseStep4Container.js | 38 ++ source/renderer/app/i18n/locales/de-DE.json | 30 ++ .../app/i18n/locales/defaultMessages.json | 471 +++++++++++++++++- source/renderer/app/i18n/locales/en-US.json | 32 +- source/renderer/app/i18n/locales/hr-HR.json | 30 ++ source/renderer/app/i18n/locales/ja-JP.json | 30 ++ source/renderer/app/i18n/locales/ko-KR.json | 30 ++ source/renderer/app/i18n/locales/zh-CN.json | 30 ++ source/renderer/app/stores/SidebarStore.js | 26 +- .../renderer/app/stores/WalletBackupStore.js | 39 +- source/renderer/app/stores/WalletsStore.js | 236 ++++++++- .../renderer/app/themes/daedalus/cardano.js | 8 + .../renderer/app/themes/daedalus/dark-blue.js | 7 + .../app/themes/daedalus/dark-cardano.js | 9 + .../app/themes/daedalus/light-blue.js | 8 + source/renderer/app/themes/daedalus/white.js | 7 + source/renderer/app/themes/daedalus/yellow.js | 7 + .../renderer/app/themes/utils/createTheme.js | 9 + source/renderer/app/types/sidebarTypes.js | 1 + source/renderer/app/utils/asyncForEach.js | 5 + storybook/stories/Sidebar.stories.js | 7 + .../stories/SidebarWalletsMenu.stories.js | 28 ++ .../Staking-DelegationCenter.stories.js | 22 + .../stories/WalletScreens-Settings.stories.js | 359 +++++++++---- storybook/stories/support/StoryLayout.js | 3 + storybook/stories/support/StoryProvider.js | 8 + storybook/stories/support/utils.js | 12 +- 69 files changed, 2864 insertions(+), 170 deletions(-) create mode 100644 features/tests/e2e/steps/wallet-recovery-phrase-verification-steps.js create mode 100644 features/wallet-settings-recovery-phrase-verification.feature create mode 100644 source/renderer/app/api/wallets/requests/getWalletIdAndBalance.js create mode 100644 source/renderer/app/assets/images/recovery-phrase-verification-notification.inline.svg create mode 100644 source/renderer/app/assets/images/recovery-phrase-verification-ok.inline.svg create mode 100644 source/renderer/app/assets/images/recovery-phrase-verification-warning.inline.svg create mode 100644 source/renderer/app/components/wallet/settings/WalletRecoveryPhrase.js create mode 100644 source/renderer/app/components/wallet/settings/WalletRecoveryPhrase.scss create mode 100644 source/renderer/app/components/wallet/settings/WalletRecoveryPhraseStep1Dialog.js create mode 100644 source/renderer/app/components/wallet/settings/WalletRecoveryPhraseStep2Dialog.js create mode 100644 source/renderer/app/components/wallet/settings/WalletRecoveryPhraseStep3Dialog.js create mode 100644 source/renderer/app/components/wallet/settings/WalletRecoveryPhraseStep4Dialog.js create mode 100644 source/renderer/app/components/wallet/settings/WalletRecoveryPhraseStepDialogs.scss create mode 100644 source/renderer/app/containers/wallet/dialogs/settings/WalletRecoveryPhraseStep1Container.js create mode 100644 source/renderer/app/containers/wallet/dialogs/settings/WalletRecoveryPhraseStep2Container.js create mode 100644 source/renderer/app/containers/wallet/dialogs/settings/WalletRecoveryPhraseStep3Container.js create mode 100644 source/renderer/app/containers/wallet/dialogs/settings/WalletRecoveryPhraseStep4Container.js create mode 100644 source/renderer/app/utils/asyncForEach.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 44a384fd7a..eb74603c1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Changelog ### Features +- Implemented wallet recovery phrase verification ([PR 1565](https://github.com/input-output-hk/daedalus/pull/1565)) - Removed select dropdown arrow ([PR 1550](https://github.com/input-output-hk/daedalus/pull/1550)) - Implemented automated and manual update flows unification ([PR 1491](https://github.com/input-output-hk/daedalus/pull/1491)) - Updated behavior of system dialogs ([PR 1494](https://github.com/input-output-hk/daedalus/pull/1494)) diff --git a/cardano-sl-src.json b/cardano-sl-src.json index 2cf7a0252c..d534545dc0 100644 --- a/cardano-sl-src.json +++ b/cardano-sl-src.json @@ -1,7 +1,7 @@ { "url": "https://github.com/input-output-hk/cardano-sl", - "rev": "51ad7c0503b1c52a75a6eb36096c407934136468", - "date": "2019-08-10T03:35:47+00:00", - "sha256": "0cqaxk3k8i5z4q4b5na0pcln9fblglmn7900vp1xdzmw75pp1rrz", + "rev": "1a792d7cd0f0c93a0f0c28f66372bce3c3808dbd", + "date": "2019-09-25T010:53:54+00:00", + "sha256": "1vk71zn9bnkgkhgcyj59wzrp28crjwcd0lgnm013mhzpvxycgn61", "fetchSubmodules": false } diff --git a/features/node-update-notification.feature b/features/node-update-notification.feature index 09dabed758..aa685f69e6 100644 --- a/features/node-update-notification.feature +++ b/features/node-update-notification.feature @@ -8,8 +8,8 @@ Feature: Node Update Notification When I set next update version to "10" And I set next application version to "15" Then I should see the node update notification overlay - And Overlay should display "newer version" as available version and actions - + And Overlay should display "a newer version" as available version and actions + Scenario: Application version and next update version match When I set next application version to "15" And I set next update version to "15" @@ -31,4 +31,4 @@ Feature: Node Update Notification Then I should see the node update notification overlay And Overlay should display "0.14.0" as available version and actions When I click the accept update button - Then Daedalus should quit \ No newline at end of file + Then Daedalus should quit diff --git a/features/tests/e2e/helpers/wallets-helpers.js b/features/tests/e2e/helpers/wallets-helpers.js index ab9df8a927..4718d893bd 100644 --- a/features/tests/e2e/helpers/wallets-helpers.js +++ b/features/tests/e2e/helpers/wallets-helpers.js @@ -95,14 +95,17 @@ export const importWalletWithFunds = async ( const createWalletsAsync = async (table, context) => { const result = await context.client.executeAsync((wallets, done) => { + const mnemonics = {}; window.Promise.all( - wallets.map(wallet => - daedalus.api.ada.createWallet({ + wallets.map(wallet => { + const mnemonic = daedalus.utils.crypto.generateMnemonic(); + mnemonics[wallet.name] = mnemonic.split(' '); + return daedalus.api.ada.createWallet({ name: wallet.name, - mnemonic: daedalus.utils.crypto.generateMnemonic(), + mnemonic, spendingPassword: wallet.password || null, - }) - ) + }); + }) ) .then(() => daedalus.stores.wallets.walletsRequest @@ -110,7 +113,7 @@ const createWalletsAsync = async (table, context) => { .then(storeWallets => daedalus.stores.wallets .refreshWalletsData() - .then(() => done(storeWallets)) + .then(() => done({ storeWallets, mnemonics })) .catch(error => done(error)) ) .catch(error => done(error)) @@ -119,9 +122,14 @@ const createWalletsAsync = async (table, context) => { }, table); // Add or set the wallets for this scenario if (context.wallets != null) { - context.wallets.push(...result.value); + context.wallets.push(...result.value.storeWallets); } else { - context.wallets = result.value; + context.wallets = result.value.storeWallets; + } + if (context.mnemonics != null) { + context.mnemonics.push(...result.value.mnemonics); + } else { + context.mnemonics = result.value.mnemonics; } }; diff --git a/features/tests/e2e/steps/node-update-notification-steps.js b/features/tests/e2e/steps/node-update-notification-steps.js index 6bf8092028..46872cc3f8 100644 --- a/features/tests/e2e/steps/node-update-notification-steps.js +++ b/features/tests/e2e/steps/node-update-notification-steps.js @@ -23,12 +23,10 @@ When( this.client, SELECTORS.newAppVersionInfo ); - const [currentAppVersionInfo] = await getVisibleTextsForSelector( this.client, SELECTORS.currentAppVersionInfo ); - expect(newAppVersionInfo.replace('v ', '')).to.equal(nextVersion); expect(currentAppVersionInfo.replace('v ', '')).to.equal(currentAppVersion); this.client.waitForVisible('.AutomaticUpdateNotification_acceptButton'); diff --git a/features/tests/e2e/steps/wallet-recovery-phrase-verification-steps.js b/features/tests/e2e/steps/wallet-recovery-phrase-verification-steps.js new file mode 100644 index 0000000000..b155263adb --- /dev/null +++ b/features/tests/e2e/steps/wallet-recovery-phrase-verification-steps.js @@ -0,0 +1,94 @@ +import { Given, When, Then } from 'cucumber'; +import { expect } from 'chai'; +import { navigateTo } from '../helpers/route-helpers'; +import { + waitUntilWaletNamesEqual, + getNameOfActiveWalletInSidebar, +} from '../helpers/wallets-helpers'; + +const SETTINGS_PAGE_STATUS_SELECTOR = '.WalletRecoveryPhrase_validationStatus'; +const SETTINGS_PAGE_BUTTON_SELECTOR = `${SETTINGS_PAGE_STATUS_SELECTOR} .WalletRecoveryPhrase_validationStatusButton`; +const DIALOG_SELECTOR = '.Dialog_dialogWrapper'; +const DIALOG_CHECKBOX_SELECTOR = `${DIALOG_SELECTOR} .SimpleCheckbox_check`; +const DIALOG_CONTINUE_BUTTON_SELECTOR = `${DIALOG_SELECTOR} .SimpleButton_root`; +const DIALOG_SUCCESSFUL_SELECTOR = '.verification-successful'; +const DIALOG_UNSUCCESSFUL_SELECTOR = '.verification-unsuccessful'; +const DIALOG_VERIFY_AGAIN_BUTTON_SELECTOR = `${DIALOG_SELECTOR} button.attention`; +const DIALOG_CLOSE_BUTTON_SELECTOR = `${DIALOG_SELECTOR} .DialogCloseButton_component`; +const walletName = 'Wallet'; + +Given( + 'the last recovery phrase veryfication was done {int} days ago', + async function(daysAgo) { + await this.client.executeAsync((days, done) => { + const { id } = daedalus.stores.wallets.active; + const date = new Date(); + date.setDate(date.getDate() - days); + const recoveryPhraseVerificationDate = date.toISOString(); + const { updateWalletLocalData } = daedalus.actions.wallets; + updateWalletLocalData.once(done); + updateWalletLocalData.trigger({ + id, + recoveryPhraseVerificationDate, + }); + }, daysAgo); + } +); + +Then( + 'I should see a {string} recovery phrase veryfication feature', + async function(status) { + const statusClassname = `${SETTINGS_PAGE_STATUS_SELECTOR}${status}`; + return await this.client.waitForVisible(statusClassname); + } +); + +When(/^I click the recovery phrase veryfication button$/, function() { + return this.waitAndClick(SETTINGS_PAGE_BUTTON_SELECTOR); +}); + +When(/^I click the checkbox and Continue button$/, function() { + this.waitAndClick(DIALOG_CHECKBOX_SELECTOR); + return this.waitAndClick(DIALOG_CONTINUE_BUTTON_SELECTOR); +}); + +When(/^I enter the recovery phrase mnemonics correctly$/, async function() { + const recoveryPhrase = this.mnemonics[walletName].slice(); + await this.client.executeAsync((recoveryPhrase, done) => { + const { checkRecoveryPhrase } = daedalus.actions.walletBackup; + checkRecoveryPhrase.once(done); + checkRecoveryPhrase.trigger({ + recoveryPhrase, + }); + }, recoveryPhrase); +}); + +When(/^I enter the recovery phrase mnemonics incorrectly$/, async function() { + const incorrectRecoveryPhrase = [...this.mnemonics[walletName]]; + incorrectRecoveryPhrase[0] = 'wrong'; + await this.client.executeAsync((recoveryPhrase, done) => { + const { checkRecoveryPhrase } = daedalus.actions.walletBackup; + checkRecoveryPhrase.once(done); + checkRecoveryPhrase.trigger({ + recoveryPhrase, + }); + }, incorrectRecoveryPhrase); +}); + +When(/^I should see the confirmation dialog$/, async function() { + return this.client.waitForVisible(DIALOG_SUCCESSFUL_SELECTOR); +}); + +When(/^I should see the error dialog$/, async function() { + return this.client.waitForVisible(DIALOG_UNSUCCESSFUL_SELECTOR); +}); + +When(/^I should not see any dialog$/, async function() { + return this.client.waitForVisible(DIALOG_SELECTOR, null, true); +}); +When(/^I click the Verify again button$/, async function() { + return this.waitAndClick(DIALOG_VERIFY_AGAIN_BUTTON_SELECTOR); +}); +When(/^I click the close button$/, async function() { + return this.waitAndClick(DIALOG_CLOSE_BUTTON_SELECTOR); +}); diff --git a/features/wallet-settings-recovery-phrase-verification.feature b/features/wallet-settings-recovery-phrase-verification.feature new file mode 100644 index 0000000000..5d1b7f9816 --- /dev/null +++ b/features/wallet-settings-recovery-phrase-verification.feature @@ -0,0 +1,32 @@ +@e2e +Feature: Wallet Settings - Recovery Phrase Verification + + Background: + Given I have completed the basic setup + And I have the following wallets: + | name | + | Wallet | + + Scenario: Recovery phrase correctly verified + Given the last recovery phrase veryfication was done 400 days ago + And I am on the "Wallet" wallet "settings" screen + Then I should see a "Notification" recovery phrase veryfication feature + When I click the recovery phrase veryfication button + And I click the checkbox and Continue button + And I enter the recovery phrase mnemonics correctly + Then I should see the confirmation dialog + When I click the checkbox and Continue button + Then I should not see any dialog + And I should see a "Ok" recovery phrase veryfication feature + + Scenario: Recovery phrase incorrectly verified + Given the last recovery phrase veryfication was done 200 days ago + And I am on the "Wallet" wallet "settings" screen + Then I should see a "Warning" recovery phrase veryfication feature + When I click the recovery phrase veryfication button + And I click the checkbox and Continue button + And I enter the recovery phrase mnemonics incorrectly + Then I should see the error dialog + When I click the close button + Then I should not see any dialog + diff --git a/source/renderer/app/actions/wallet-backup-actions.js b/source/renderer/app/actions/wallet-backup-actions.js index 899ce0ff6d..49be9682f1 100644 --- a/source/renderer/app/actions/wallet-backup-actions.js +++ b/source/renderer/app/actions/wallet-backup-actions.js @@ -5,7 +5,9 @@ import Action from './lib/Action'; export default class WalletBackupActions { startWalletBackup: Action = new Action(); - initiateWalletBackup: Action<{ recoveryPhrase: string[] }> = new Action(); + initiateWalletBackup: Action<{ + recoveryPhrase: Array, + }> = new Action(); acceptPrivacyNoticeForWalletBackup: Action = new Action(); continueToRecoveryPhraseForWalletBackup: Action = new Action(); addWordToWalletBackupVerification: Action<{ @@ -18,4 +20,7 @@ export default class WalletBackupActions { restartWalletBackup: Action = new Action(); cancelWalletBackup: Action = new Action(); finishWalletBackup: Action = new Action(); + // Recovery phrase confirmation dialog actions + checkRecoveryPhrase: Action<{ recoveryPhrase: Array }> = new Action(); + resetRecoveryPhraseCheck: Action = new Action(); } diff --git a/source/renderer/app/actions/wallets-actions.js b/source/renderer/app/actions/wallets-actions.js index 00bcba2063..a549235c42 100644 --- a/source/renderer/app/actions/wallets-actions.js +++ b/source/renderer/app/actions/wallets-actions.js @@ -42,4 +42,6 @@ export default class WalletsActions { closeCertificateGeneration: Action = new Action(); setCertificateTemplate: Action<{ selectedTemplate: string }> = new Action(); finishCertificate: Action = new Action(); + updateWalletLocalData: Action = new Action(); + updateRecoveryPhraseVerificationDate: Action = new Action(); } diff --git a/source/renderer/app/api/api.js b/source/renderer/app/api/api.js index 7d1cad9708..06d34de5ba 100644 --- a/source/renderer/app/api/api.js +++ b/source/renderer/app/api/api.js @@ -45,6 +45,7 @@ import { createWallet } from './wallets/requests/createWallet'; import { restoreWallet } from './wallets/requests/restoreWallet'; import { updateWallet } from './wallets/requests/updateWallet'; import { getWalletUtxos } from './wallets/requests/getWalletUtxos'; +import { getWalletIdAndBalance } from './wallets/requests/getWalletIdAndBalance'; // utility functions import { @@ -118,6 +119,7 @@ import type { AdaWallet, AdaWallets, WalletUtxos, + WalletIdAndBalance, CreateWalletRequest, DeleteWalletRequest, RestoreWalletRequest, @@ -129,6 +131,8 @@ import type { ImportWalletFromFileRequest, UpdateWalletRequest, GetWalletUtxosRequest, + GetWalletIdAndBalanceRequest, + GetWalletIdAndBalanceResponse, } from './wallets/types'; // Common errors @@ -873,6 +877,36 @@ export default class AdaApi { } }; + getWalletIdAndBalance = async ( + request: GetWalletIdAndBalanceRequest + ): Promise => { + const { recoveryPhrase, getBalance } = request; + Logger.debug('AdaApi::getWalletIdAndBalance called', { + parameters: { getBalance }, + }); + try { + const response: GetWalletIdAndBalanceResponse = await getWalletIdAndBalance( + this.config, + { + recoveryPhrase, + getBalance, + } + ); + Logger.debug('AdaApi::getWalletIdAndBalance success', { response }); + const { walletId, balance } = response; + return { + walletId, + balance: + balance !== null // If balance is "null" it means we didn't fetch it - getBalance was false + ? new BigNumber(balance).dividedBy(LOVELACES_PER_ADA) + : null, + }; + } catch (error) { + Logger.error('AdaApi::getWalletIdAndBalance error', { error }); + throw new GenericApiError(); + } + }; + testReset = async (): Promise => { Logger.debug('AdaApi::testReset called'); try { @@ -1035,6 +1069,7 @@ const _createWalletFromServerData = action( hasSpendingPassword, spendingPasswordLastUpdate, syncState, + createdAt, } = data; return new Wallet({ @@ -1046,6 +1081,7 @@ const _createWalletFromServerData = action( passwordUpdateDate: new Date(`${spendingPasswordLastUpdate}Z`), syncState, isLegacy: false, + createdAt, }); } ); diff --git a/source/renderer/app/api/utils/localStorage.js b/source/renderer/app/api/utils/localStorage.js index 0f9a59b058..7687d22ed7 100644 --- a/source/renderer/app/api/utils/localStorage.js +++ b/source/renderer/app/api/utils/localStorage.js @@ -4,11 +4,22 @@ const store = global.electronStore; +export type WalletLocalData = { + id: string, + recoveryPhraseVerificationDate?: ?Date, + creationDate?: ?Date, +}; + +export type WalletsLocalData = { + [key: string]: WalletLocalData, +}; + type StorageKeys = { USER_LOCALE: string, TERMS_OF_USE_ACCEPTANCE: string, THEME: string, DATA_LAYER_MIGRATION_ACCEPTANCE: string, + WALLETS: string, }; /** @@ -25,6 +36,7 @@ export default class LocalStorageApi { TERMS_OF_USE_ACCEPTANCE: `${NETWORK}-TERMS-OF-USE-ACCEPTANCE`, THEME: `${NETWORK}-THEME`, DATA_LAYER_MIGRATION_ACCEPTANCE: `${NETWORK}-DATA-LAYER-MIGRATION-ACCEPTANCE`, + WALLETS: `${NETWORK}-WALLETS`, }; } @@ -146,6 +158,70 @@ export default class LocalStorageApi { } catch (error) {} // eslint-disable-line }); + getWalletsLocalData = (): Promise => + new Promise((resolve, reject) => { + try { + const walletsLocalData = store.get(this.storageKeys.WALLETS); + if (!walletsLocalData) return resolve({}); + return resolve(walletsLocalData); + } catch (error) { + return reject(error); + } + }); + + getWalletLocalData = (walletId: string): Promise => + new Promise((resolve, reject) => { + try { + const walletData = store.get(`${this.storageKeys.WALLETS}.${walletId}`); + if (!walletData) { + resolve({ + id: walletId, + }); + } + return resolve(walletData); + } catch (error) { + return reject(error); + } + }); + + setWalletLocalData = (walletData: WalletLocalData): Promise => + new Promise((resolve, reject) => { + try { + const walletId = walletData.id; + store.set(`${this.storageKeys.WALLETS}.${walletId}`, walletData); + return resolve(); + } catch (error) { + return reject(error); + } + }); + + updateWalletLocalData = (updatedWalletData: Object): Promise => + new Promise(async (resolve, reject) => { + const walletId = updatedWalletData.id; + const currentWalletData = await this.getWalletLocalData(walletId); + const walletData = Object.assign( + {}, + currentWalletData, + updatedWalletData + ); + try { + store.set(`${this.storageKeys.WALLETS}.${walletId}`, walletData); + return resolve(walletData); + } catch (error) { + return reject(error); + } + }); + + unsetWalletLocalData = (walletId: string): Promise => + new Promise((resolve, reject) => { + try { + store.delete(`${this.storageKeys.WALLETS}.${walletId}`); + return resolve(); + } catch (error) { + return reject(error); + } + }); + reset = async () => { await this.unsetUserLocale(); await this.unsetTermsOfUseAcceptance(); diff --git a/source/renderer/app/api/wallets/requests/getWalletIdAndBalance.js b/source/renderer/app/api/wallets/requests/getWalletIdAndBalance.js new file mode 100644 index 0000000000..c05dbcb9d0 --- /dev/null +++ b/source/renderer/app/api/wallets/requests/getWalletIdAndBalance.js @@ -0,0 +1,23 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { + GetWalletIdAndBalanceRequest, + GetWalletIdAndBalanceResponse, +} from '../types'; +import { request } from '../../utils/request'; + +export const getWalletIdAndBalance = ( + config: RequestConfig, + { recoveryPhrase, getBalance }: GetWalletIdAndBalanceRequest +): Promise => + request( + { + method: 'POST', + path: '/api/internal/calculate_mnemonic', + ...config, + }, + { + read_balance: getBalance, + }, + recoveryPhrase + ); diff --git a/source/renderer/app/api/wallets/types.js b/source/renderer/app/api/wallets/types.js index 30f82918be..5e3b25a078 100644 --- a/source/renderer/app/api/wallets/types.js +++ b/source/renderer/app/api/wallets/types.js @@ -1,6 +1,8 @@ // @flow +import BigNumber from 'bignumber.js'; + export type AdaWallet = { - createdAt: string, + createdAt: Date, syncState: WalletSyncState, balance: number, hasSpendingPassword: boolean, @@ -48,6 +50,11 @@ export type WalletUtxos = { }, }; +export type WalletIdAndBalance = { + walletId: string, + balance: ?BigNumber, +}; + // req/res Wallet types export type CreateWalletRequest = { name: string, @@ -69,6 +76,16 @@ export type GetWalletUtxosRequest = { walletId: string, }; +export type GetWalletIdAndBalanceRequest = { + recoveryPhrase: Array, + getBalance: boolean, +}; + +export type GetWalletIdAndBalanceResponse = { + walletId: string, + balance: ?number, +}; + export type RestoreWalletRequest = { recoveryPhrase: string, walletName: string, diff --git a/source/renderer/app/assets/images/recovery-phrase-verification-notification.inline.svg b/source/renderer/app/assets/images/recovery-phrase-verification-notification.inline.svg new file mode 100644 index 0000000000..d7d9b879df --- /dev/null +++ b/source/renderer/app/assets/images/recovery-phrase-verification-notification.inline.svg @@ -0,0 +1,3 @@ + + + diff --git a/source/renderer/app/assets/images/recovery-phrase-verification-ok.inline.svg b/source/renderer/app/assets/images/recovery-phrase-verification-ok.inline.svg new file mode 100644 index 0000000000..714fd3a335 --- /dev/null +++ b/source/renderer/app/assets/images/recovery-phrase-verification-ok.inline.svg @@ -0,0 +1,3 @@ + + + diff --git a/source/renderer/app/assets/images/recovery-phrase-verification-warning.inline.svg b/source/renderer/app/assets/images/recovery-phrase-verification-warning.inline.svg new file mode 100644 index 0000000000..3f5acb401d --- /dev/null +++ b/source/renderer/app/assets/images/recovery-phrase-verification-warning.inline.svg @@ -0,0 +1,3 @@ + + + diff --git a/source/renderer/app/components/navigation/NavButton.js b/source/renderer/app/components/navigation/NavButton.js index 2af1e15786..186daac2f7 100755 --- a/source/renderer/app/components/navigation/NavButton.js +++ b/source/renderer/app/components/navigation/NavButton.js @@ -11,16 +11,18 @@ type Props = { isActive: boolean, onClick: Function, className?: string, + hasNotification?: boolean, }; @observer export default class NavButton extends Component { render() { - const { isActive, icon, onClick, className } = this.props; + const { isActive, icon, onClick, className, hasNotification } = this.props; const componentClasses = classnames([ className, styles.component, isActive ? styles.active : styles.normal, + hasNotification ? styles.hasNotification : null, ]); const iconClasses = classnames([ styles.icon, diff --git a/source/renderer/app/components/navigation/NavButton.scss b/source/renderer/app/components/navigation/NavButton.scss index 0b31557e94..8307559de8 100755 --- a/source/renderer/app/components/navigation/NavButton.scss +++ b/source/renderer/app/components/navigation/NavButton.scss @@ -7,6 +7,23 @@ justify-content: center; text-align: center; width: 100%; + + &.hasNotification { + .container { + position: relative; + &:after { + background: var(--theme-button-attention-background-color); + border-radius: 50%; + content: ''; + height: 8px; + transform: translate(4px, -4px); + width: 8px; + } + .icon { + margin-left: 8px; + } + } + } } .container { diff --git a/source/renderer/app/components/navigation/NavDropdown.js b/source/renderer/app/components/navigation/NavDropdown.js index 54a81ff7cc..8650e7a8fb 100644 --- a/source/renderer/app/components/navigation/NavDropdown.js +++ b/source/renderer/app/components/navigation/NavDropdown.js @@ -1,6 +1,7 @@ // @flow import React, { Component } from 'react'; import { observer } from 'mobx-react'; +import classnames from 'classnames'; import { Select } from 'react-polymorph/lib/components/Select'; import { SelectSkin } from './NavSelectSkin'; @@ -16,14 +17,27 @@ type Props = { isActive: boolean, options?: Array<{ value: number | string, label: string }>, onChange: Function, + hasNotification?: boolean, }; @observer export default class NavDropdown extends Component { render() { - const { label, icon, isActive, onChange, options, activeItem } = this.props; + const { + label, + icon, + isActive, + onChange, + options, + activeItem, + hasNotification, + } = this.props; + const componentStyles = classnames([ + styles.component, + hasNotification ? styles.hasNotification : null, + ]); return ( -
+