diff --git a/CHANGELOG.md b/CHANGELOG.md index c4bbe40409..c4a4851acc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,13 @@ Changelog ## vNext +### Features + +- Implemented Hardware wallets connection screens ([PR 2016](https://github.com/input-output-hk/daedalus/pull/2016)) + ### Fixes -- Eenabled the Recovery Phrase Verification feature of Shelley wallets on ITN ([PR 2008](https://github.com/input-output-hk/daedalus/pull/2008)) +- Enabled the Recovery Phrase Verification feature of Shelley wallets on ITN ([PR 2008](https://github.com/input-output-hk/daedalus/pull/2008)) - Disabled button on forms when there is nothing to submit ([PR 1998](https://github.com/input-output-hk/daedalus/pull/1998), [PR 2010](https://github.com/input-output-hk/daedalus/pull/2010)) - Fixed system locale detection ([PR 2009](https://github.com/input-output-hk/daedalus/pull/2009)) diff --git a/source/renderer/app/Routes.js b/source/renderer/app/Routes.js index 264da22442..81b19e2a2b 100644 --- a/source/renderer/app/Routes.js +++ b/source/renderer/app/Routes.js @@ -21,6 +21,7 @@ import StakingInfoPage from './containers/staking/StakingInfoPage'; import StakingRewardsPage from './containers/staking/StakingRewardsPage'; import StakePoolsListPage from './containers/staking/StakePoolsListPage'; import StakingCountdownPage from './containers/staking/StakingCountdownPage'; +import HardwareWallet from './containers/hardware-wallet/HardwareWallet'; import Wallet from './containers/wallet/Wallet'; import WalletAddPage from './containers/wallet/WalletAddPage'; import WalletSummaryPage from './containers/wallet/WalletSummaryPage'; @@ -54,6 +55,12 @@ export const Routes = ( + + + diff --git a/source/renderer/app/actions/sidebar-actions.js b/source/renderer/app/actions/sidebar-actions.js index f0f489ce71..2ada9443ad 100644 --- a/source/renderer/app/actions/sidebar-actions.js +++ b/source/renderer/app/actions/sidebar-actions.js @@ -11,4 +11,5 @@ export default class SidebarActions { showSubMenu?: boolean, }> = new Action(); walletSelected: Action<{ walletId: string }> = new Action(); + hardwareWalletSelected: Action<{ walletId: string }> = new Action(); } diff --git a/source/renderer/app/assets/images/hardware-wallet/check.inline.svg b/source/renderer/app/assets/images/hardware-wallet/check.inline.svg new file mode 100644 index 0000000000..2d47dfcc8b --- /dev/null +++ b/source/renderer/app/assets/images/hardware-wallet/check.inline.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/source/renderer/app/assets/images/hardware-wallet/close-cross-red.inline.svg b/source/renderer/app/assets/images/hardware-wallet/close-cross-red.inline.svg new file mode 100644 index 0000000000..2b7652bd9b --- /dev/null +++ b/source/renderer/app/assets/images/hardware-wallet/close-cross-red.inline.svg @@ -0,0 +1 @@ + diff --git a/source/renderer/app/assets/images/hardware-wallet/disconnected.inline.svg b/source/renderer/app/assets/images/hardware-wallet/disconnected.inline.svg new file mode 100644 index 0000000000..7475a660d8 --- /dev/null +++ b/source/renderer/app/assets/images/hardware-wallet/disconnected.inline.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/source/renderer/app/assets/images/hardware-wallet/export.inline.svg b/source/renderer/app/assets/images/hardware-wallet/export.inline.svg new file mode 100644 index 0000000000..f797fb56f5 --- /dev/null +++ b/source/renderer/app/assets/images/hardware-wallet/export.inline.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/source/renderer/app/assets/images/hardware-wallet/ledger-bold-ic.inline.svg b/source/renderer/app/assets/images/hardware-wallet/ledger-bold-ic.inline.svg new file mode 100644 index 0000000000..0cfd2aa56b --- /dev/null +++ b/source/renderer/app/assets/images/hardware-wallet/ledger-bold-ic.inline.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/source/renderer/app/assets/images/hardware-wallet/ledger-cropped.inline.svg b/source/renderer/app/assets/images/hardware-wallet/ledger-cropped.inline.svg new file mode 100644 index 0000000000..27f2f20b92 --- /dev/null +++ b/source/renderer/app/assets/images/hardware-wallet/ledger-cropped.inline.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/source/renderer/app/assets/images/hardware-wallet/ledger-x-cropped-outlines.inline.svg b/source/renderer/app/assets/images/hardware-wallet/ledger-x-cropped-outlines.inline.svg new file mode 100644 index 0000000000..28b3e4d30f --- /dev/null +++ b/source/renderer/app/assets/images/hardware-wallet/ledger-x-cropped-outlines.inline.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/source/renderer/app/assets/images/hardware-wallet/trezor-ledger.inline.svg b/source/renderer/app/assets/images/hardware-wallet/trezor-ledger.inline.svg new file mode 100644 index 0000000000..fea7c25020 --- /dev/null +++ b/source/renderer/app/assets/images/hardware-wallet/trezor-ledger.inline.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/source/renderer/app/assets/images/sidebar/hardware-wallet-ic.inline.svg b/source/renderer/app/assets/images/sidebar/hardware-wallet-ic.inline.svg new file mode 100644 index 0000000000..6717a31eda --- /dev/null +++ b/source/renderer/app/assets/images/sidebar/hardware-wallet-ic.inline.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/source/renderer/app/components/hardware-wallet/layouts/HardwareWalletWithNavigation.js b/source/renderer/app/components/hardware-wallet/layouts/HardwareWalletWithNavigation.js new file mode 100644 index 0000000000..2c5b2e2d42 --- /dev/null +++ b/source/renderer/app/components/hardware-wallet/layouts/HardwareWalletWithNavigation.js @@ -0,0 +1,71 @@ +// @flow +import React, { Component } from 'react'; +import type { Node } from 'react'; +import { observer } from 'mobx-react'; +import HardwareWalletNavigation from '../navigation/HardwareWalletNavigation'; +import styles from './HardwareWalletWithNavigation.scss'; +import ConnectHardwareWallet from '../settings/ConnectHardwareWallet'; + +type Props = { + children?: Node, + activeItem: string, + hasNotification?: boolean, + walletNotConnected: boolean, + isActiveScreen: Function, + onOpenExternalLink: Function, + onWalletNavItemClick: Function, + isLedger: boolean, + isTrezor: boolean, + isDeviceConnected: boolean, + fetchingDevice: boolean, + exportingExtendedPublicKey: boolean, + isExportingPublicKeyAborted: boolean, +}; + +@observer +export default class HardwareWalletWithNavigation extends Component { + render() { + const { + children, + activeItem, + hasNotification, + walletNotConnected, + isActiveScreen, + onWalletNavItemClick, + onOpenExternalLink, + isLedger, + isTrezor, + isDeviceConnected, + fetchingDevice, + exportingExtendedPublicKey, + isExportingPublicKeyAborted, + } = this.props; + + return ( +
+ {walletNotConnected ? ( + + ) : ( +
+ +
+ )} + +
{children}
+
+ ); + } +} diff --git a/source/renderer/app/components/hardware-wallet/layouts/HardwareWalletWithNavigation.scss b/source/renderer/app/components/hardware-wallet/layouts/HardwareWalletWithNavigation.scss new file mode 100644 index 0000000000..8427fe9428 --- /dev/null +++ b/source/renderer/app/components/hardware-wallet/layouts/HardwareWalletWithNavigation.scss @@ -0,0 +1,29 @@ +.component { + display: flex; + flex: 1; + flex-direction: column; + overflow-y: overlay; +} + +.navigation { + flex-shrink: 0; + height: 50px; + position: relative; +} + +.page { + align-items: center; + display: flex; + height: calc(100% - 50px); + justify-content: center; + overflow: overlay; + position: relative; + + > div { + width: 100%; + } +} + +.settingsTabPage { + overflow: hidden; +} diff --git a/source/renderer/app/components/hardware-wallet/navigation/HardwareWalletNavigation.js b/source/renderer/app/components/hardware-wallet/navigation/HardwareWalletNavigation.js new file mode 100755 index 0000000000..bfbb90dccf --- /dev/null +++ b/source/renderer/app/components/hardware-wallet/navigation/HardwareWalletNavigation.js @@ -0,0 +1,67 @@ +// @flow +import React, { Component } from 'react'; +import { observer } from 'mobx-react'; +import { includes } from 'lodash'; +import { defineMessages, intlShape } from 'react-intl'; +import { + WALLET_NAV_IDS, + ITN_LEGACY_WALLET_EXCLUDED_NAV_ITEMS, +} from '../../../config/walletNavigationConfig'; +import Navigation from '../../navigation/Navigation'; +import summaryIcon from '../../../assets/images/wallet-nav/summary-ic.inline.svg'; +import type { + NavButtonProps, + NavDropdownProps, +} from '../../navigation/Navigation'; + +const messages = defineMessages({ + summary: { + id: 'wallet.navigation.summary', + defaultMessage: '!!!Summary', + description: 'Label for the "Summary" nav button in the wallet navigation.', + }, +}); + +type Props = { + activeItem: string, + isActiveNavItem: Function, + onNavItemClick: Function, +}; + +@observer +export default class WalletNavigation extends Component { + static contextTypes = { + intl: intlShape.isRequired, + }; + + render() { + const { isActiveNavItem, onNavItemClick, activeItem } = this.props; + const { intl } = this.context; + const { isIncentivizedTestnet } = global; + const items: Array = [ + { + id: WALLET_NAV_IDS.SUMMARY, + label: intl.formatMessage(messages.summary), + icon: summaryIcon, + }, + ].filter( + item => + !( + isIncentivizedTestnet && + includes(ITN_LEGACY_WALLET_EXCLUDED_NAV_ITEMS, item.id) + ) + ); + return ( + <> + {activeItem ? ( + + ) : null} + + ); + } +} diff --git a/source/renderer/app/components/hardware-wallet/settings/ConnectHardwareWallet.js b/source/renderer/app/components/hardware-wallet/settings/ConnectHardwareWallet.js new file mode 100644 index 0000000000..d3dd569eb5 --- /dev/null +++ b/source/renderer/app/components/hardware-wallet/settings/ConnectHardwareWallet.js @@ -0,0 +1,169 @@ +// @flow +import React, { Component } from 'react'; +import { observer } from 'mobx-react'; +import { defineMessages, FormattedHTMLMessage, intlShape } from 'react-intl'; +import SVGInline from 'react-svg-inline'; +import classnames from 'classnames'; +import ledgerIcon from '../../../assets/images/hardware-wallet/ledger-cropped.inline.svg'; +import ledgerXIcon from '../../../assets/images/hardware-wallet/ledger-x-cropped-outlines.inline.svg'; +import trezorIcon from '../../../assets/images/hardware-wallet/trezor-ledger.inline.svg'; +import exportIcon from '../../../assets/images/hardware-wallet/export.inline.svg'; +import checkIcon from '../../../assets/images/hardware-wallet/check.inline.svg'; +import clearIcon from '../../../assets/images/hardware-wallet/close-cross-red.inline.svg'; +import ledgerSmallIcon from '../../../assets/images/hardware-wallet/ledger-bold-ic.inline.svg'; +import styles from './ConnectHardwareWallet.scss'; +import LoadingSpinner from '../../widgets/LoadingSpinner'; + +const messages = defineMessages({ + hardwareWalletTitle: { + id: 'wallet.hardware.hardwareWalletTitle', + defaultMessage: '!!!Hardware wallet', + description: 'Hardware wallet title.', + }, + ledgerWalletTitle: { + id: 'wallet.hardware.ledgerWalletTitle', + defaultMessage: '!!!Ledger wallet', + description: 'Ledger wallet title.', + }, + hardwareWalletInstructions: { + id: 'wallet.hardware.hardwareWalletInstructions', + defaultMessage: '!!!Follow instructions to access your wallet', + description: 'Follow instructions label', + }, + hardwareWalletLedgerBegin: { + id: 'wallet.hardware.hardwareWalletLedgerBegin', + defaultMessage: + '!!!To begin, connect and unlock your Ledger Device', + description: 'Connect device label', + }, + hardwareWalletBegin: { + id: 'wallet.hardware.hardwareWalletBegin', + defaultMessage: + '!!!To begin, connect and unlock your Hardware wallet Device', + description: 'Connect device label', + }, + hardwareWalletExport: { + id: 'wallet.hardware.hardwareWalletExport', + defaultMessage: '!!!Export public key on your device', + description: 'Export wallet label', + }, + linkUrl: { + id: 'wallet.select.import.dialog.linkUrl', + defaultMessage: '!!!https://daedaluswallet.io/', + description: 'External link URL on the hardware wallet connect screen', + }, +}); + +type Props = { + onOpenExternalLink: Function, + isLedger: boolean, + isTrezor: boolean, + isDeviceConnected: boolean | null, + fetchingDevice: boolean, + exportingExtendedPublicKey: boolean | null, + isExportingPublicKeyAborted: boolean, +}; + +@observer +export default class ConnectHardwareWallet extends Component { + static contextTypes = { + intl: intlShape.isRequired, + }; + + render() { + const { intl } = this.context; + + const { + onOpenExternalLink, + isLedger, + isTrezor, + isDeviceConnected, + fetchingDevice, + exportingExtendedPublicKey, + isExportingPublicKeyAborted, + } = this.props; + + const hardwareTitle = isTrezor + ? intl.formatMessage(messages.hardwareWalletTitle) + : intl.formatMessage(messages.ledgerWalletTitle); + + const hardwareConnectLabel = isTrezor + ? messages.hardwareWalletBegin + : messages.hardwareWalletLedgerBegin; + + const firstStepClasses = classnames([ + styles.hardwareWalletStep, + fetchingDevice ? styles.isActiveFetchingDevice : null, + isDeviceConnected === null ? styles.isErrorDevice : null, + ]); + + const secondStepClasses = classnames([ + styles.hardwareWalletStep, + exportingExtendedPublicKey ? styles.isActiveExport : null, + isExportingPublicKeyAborted ? styles.isErrorExport : null, + ]); + + return ( +
+
+
+ {isTrezor && ( +
+ +
+ )} + {isLedger && ( +
+ + +
+ )} +

{hardwareTitle}

+

+ {intl.formatMessage(messages.hardwareWalletInstructions)} +

+
+
+
+ + +
+ {fetchingDevice && } + {isDeviceConnected && ( + + )} + {!fetchingDevice && isDeviceConnected === null && ( + + )} +
+
+
+ + onOpenExternalLink(intl.formatMessage(messages.linkUrl)) + } + /> + +
+ {exportingExtendedPublicKey && } + {!isExportingPublicKeyAborted && + exportingExtendedPublicKey !== true && + exportingExtendedPublicKey !== null && ( + + )} + {!exportingExtendedPublicKey && isExportingPublicKeyAborted && ( + + )} +
+
+
+
+
+ ); + } +} diff --git a/source/renderer/app/components/hardware-wallet/settings/ConnectHardwareWallet.scss b/source/renderer/app/components/hardware-wallet/settings/ConnectHardwareWallet.scss new file mode 100644 index 0000000000..ca80afa621 --- /dev/null +++ b/source/renderer/app/components/hardware-wallet/settings/ConnectHardwareWallet.scss @@ -0,0 +1,167 @@ +.component { + align-items: center; + display: flex; + flex-direction: column; + justify-content: center; + min-height: 100%; + width: 100%; + + .hardwareWalletContainer { + font-family: var(--font-regular); + height: 100%; + width: 100%; + + .hardwareWalletWrapper { + align-items: center; + display: flex; + flex-direction: column; + height: 100%; + justify-content: center; + + .hardwareWalletTrezor { + display: flex; + + .trezorIcon { + height: 138px; + object-fit: contain; + width: 325.5px; + } + } + + .hardwareWalletLedger { + display: flex; + flex-direction: column; + + .ledgerIcon { + height: 78px; + object-fit: contain; + width: 250px; + } + + .ledgerXIcon { + height: 75px; + object-fit: contain; + width: 250px; + } + } + + .hardwareWalletTitle { + color: var(--theme-hardware-wallet-title-color); + font-size: 20px; + font-weight: 500; + line-height: 1.4; + padding-top: 36px; + text-align: center; + } + + .hardwareWalletMessage { + color: var(--theme-hardware-wallet-message-color); + font-size: 16px; + font-weight: 300; + line-height: 1.38; + max-width: 670px; + opacity: 0.7; + padding-top: 11px; + text-align: center; + } + + .hardwareWalletStepsWrapper { + max-width: 640px; + padding-top: 30px; + width: 100%; + + .hardwareWalletStep { + align-items: center; + background-color: var(--theme-hardware-wallet-step-background-color); + display: flex; + height: 72px; + justify-content: space-between; + padding: 20px; + + &:first-child { + border-left: 1px solid var(--theme-input-border-color); + border-radius: 4px 4px 0 0; + border-right: 1px solid var(--theme-input-border-color); + border-top: 1px solid var(--theme-input-border-color); + } + + &:nth-child(2) { + border-bottom: 1px solid var(--theme-input-border-color); + border-left: 1px solid var(--theme-input-border-color); + border-radius: 0 0 4px 4px; + border-right: 1px solid var(--theme-input-border-color); + } + + .hardwareWalletInnerStep { + align-items: center; + display: flex; + + .ledgerSmallIcon { + height: 32px; + margin-right: 20px; + object-fit: contain; + width: 32px; + } + + .exportIcon { + cursor: pointer; + height: 24px; + margin-right: 29px; + object-fit: contain; + width: 24px; + } + } + + span { + color: var(--theme-hardware-wallet-step-color); + font-family: var(--font-light); + font-size: 16px; + font-weight: 300; + line-height: 1.38; + + span { + font-family: var(--font-medium); + font-weight: 500; + } + } + + .checkIcon { + height: 16px; + object-fit: contain; + width: 16px; + } + + .clearIcon { + height: 14px; + object-fit: contain; + width: 14px; + } + + &.isActiveFetchingDevice, + &.isActiveExport { + border: 1px solid + var(--theme-hardware-wallet-step-border-active-color); + } + + &.isErrorDevice, + &.isErrorExport { + border: 1px solid var(--rp-theme-color-error); + } + } + } + } + } + + :global { + .LoadingSpinner_component { + height: 24px; + margin: initial; + width: 24px; + + .LoadingSpinner_icon svg path { + fill: var(--theme-hardware-wallet-message-color) !important; + opacity: 0.5; + } + } + } +} diff --git a/source/renderer/app/components/sidebar/Sidebar.js b/source/renderer/app/components/sidebar/Sidebar.js index f82cdb3a95..120db08c37 100644 --- a/source/renderer/app/components/sidebar/Sidebar.js +++ b/source/renderer/app/components/sidebar/Sidebar.js @@ -10,7 +10,10 @@ import SidebarWalletsMenu from './wallets/SidebarWalletsMenu'; import InstructionsDialog from '../wallet/paper-wallet-certificate/InstructionsDialog'; import { CATEGORIES_BY_NAME } from '../../config/sidebarConfig.js'; import { ROUTES } from '../../routes-config'; -import type { SidebarWalletType } from '../../types/sidebarTypes'; +import type { + SidebarHardwareWalletType, + SidebarWalletType, +} from '../../types/sidebarTypes'; import type { networkType } from '../../types/networkTypes'; import type { SidebarCategoryInfo } from '../../config/sidebarConfig'; @@ -28,14 +31,21 @@ type Props = { isIncentivizedTestnet: boolean, }; -export type SidebarMenus = ?{ - wallets: { +export type SidebarMenus = { + wallets: ?{ items: Array, activeWalletId: ?string, actions: { onWalletItemClick: Function, }, }, + hardwareWallets: ?{ + items: Array, + activeWalletId: ?string, + actions: { + onHardwareWalletItemClick: Function, + }, + }, }; @observer @@ -62,13 +72,29 @@ export default class Sidebar extends Component { name: CATEGORIES_BY_NAME.WALLETS.name, }).route; - if (menus && activeSidebarCategory === walletsCategory) { + const hardwareWalletsCategory = find(categories, { + name: CATEGORIES_BY_NAME.HARDWARE_WALLETS.name, + }).route; + + if ( + menus && + menus.wallets && + menus.wallets.items && + activeSidebarCategory === walletsCategory + ) { subMenu = ( id === menus.wallets.activeWalletId} + onWalletItemClick={ + menus.wallets && menus.wallets.actions + ? menus.wallets.actions.onWalletItemClick + : null + } + isActiveWallet={id => + id === (menus.wallets ? menus.wallets.activeWalletId : null) + } + isHardwareWalletsMenu={!!menus.wallets.items} isAddWalletButtonActive={pathname === '/wallets/add'} isIncentivizedTestnet={isIncentivizedTestnet} visible={isShowingSubMenus} @@ -76,6 +102,35 @@ export default class Sidebar extends Component { ); } + if ( + menus && + menus.hardwareWallets && + menus.hardwareWallets.items && + activeSidebarCategory === hardwareWalletsCategory + ) { + subMenu = ( + + id === + (menus.hardwareWallets + ? menus.hardwareWallets.activeWalletId + : null) + } + isHardwareWalletsMenu={!!menus.hardwareWallets.items} + isAddWalletButtonActive={pathname === '/hardware-wallets/add'} + isIncentivizedTestnet={isIncentivizedTestnet} + visible={isShowingSubMenus} + /> + ); + } + const sidebarStyles = classNames([ styles.component, !isShowingSubMenus || subMenu == null ? styles.minimized : null, diff --git a/source/renderer/app/components/sidebar/SidebarCategory.scss b/source/renderer/app/components/sidebar/SidebarCategory.scss index ad99390c92..0ccfcefead 100644 --- a/source/renderer/app/components/sidebar/SidebarCategory.scss +++ b/source/renderer/app/components/sidebar/SidebarCategory.scss @@ -42,6 +42,13 @@ } } +.hardwareWalletsIcon { + & > svg { + height: 44px; + width: 44px; + } +} + .paperWalletCreateCertificateIcon { & > svg { height: 21px; diff --git a/source/renderer/app/components/sidebar/wallets/SidebarWalletMenuItem.js b/source/renderer/app/components/sidebar/wallets/SidebarWalletMenuItem.js index 07166c84aa..8b0b11bce5 100644 --- a/source/renderer/app/components/sidebar/wallets/SidebarWalletMenuItem.js +++ b/source/renderer/app/components/sidebar/wallets/SidebarWalletMenuItem.js @@ -2,11 +2,13 @@ import React, { Component } from 'react'; import { observer } from 'mobx-react'; import classNames from 'classnames'; +import SVGInline from 'react-svg-inline'; import LegacyBadge, { LEGACY_BADGE_MODES, } from '../../notifications/LegacyBadge'; import ProgressBar from '../../widgets/ProgressBar'; import styles from './SidebarWalletMenuItem.scss'; +import disconnectedIcon from '../../../assets/images/hardware-wallet/disconnected.inline.svg'; type Props = { title: string, @@ -20,6 +22,7 @@ type Props = { isLegacy: boolean, isNotResponding: boolean, hasNotification: boolean, + isHardwareWalletsMenu?: boolean, }; @observer @@ -37,6 +40,7 @@ export default class SidebarWalletMenuItem extends Component { isLegacy, isNotResponding, hasNotification, + isHardwareWalletsMenu, } = this.props; const componentStyles = classNames([ @@ -51,7 +55,15 @@ export default class SidebarWalletMenuItem extends Component { return (