diff --git a/src/actions/ModalActions.js b/src/actions/ModalActions.js index 47f98776..fe2cd7af 100644 --- a/src/actions/ModalActions.js +++ b/src/actions/ModalActions.js @@ -59,6 +59,17 @@ export const onPassphraseSubmit = (passphrase: string): AsyncAction => async (di }); }; +export const onReceiveConfirmation = (confirmation: any): AsyncAction => async (dispatch: Dispatch): Promise => { + await TrezorConnect.uiResponse({ + type: UI.RECEIVE_CONFIRMATION, + payload: confirmation, + }); + + dispatch({ + type: MODAL.CLOSE, + }); +}; + export const onRememberDevice = (device: TrezorDevice): Action => ({ type: CONNECT.REMEMBER, device, @@ -173,6 +184,7 @@ export const onQrScan = (parsedUri: parsedURI, networkType: string): ThunkAction export default { onPinSubmit, onPassphraseSubmit, + onReceiveConfirmation, onRememberDevice, onForgetDevice, onForgetSingleDevice, diff --git a/src/actions/ReceiveActions.js b/src/actions/ReceiveActions.js index 116a5c15..0503fdf7 100644 --- a/src/actions/ReceiveActions.js +++ b/src/actions/ReceiveActions.js @@ -94,6 +94,10 @@ export const showAddress = (path: Array): AsyncAction => async (dispatch type: RECEIVE.HIDE_ADDRESS, }); + // special case: device no-backup permissions not granted + // $FlowIssue: remove this after trezor-connect@7.0.0 release + if (response.payload.code === 403) return; + dispatch({ type: NOTIFICATION.ADD, payload: { diff --git a/src/actions/RouterActions.js b/src/actions/RouterActions.js index 3d501714..7fdeb835 100644 --- a/src/actions/RouterActions.js +++ b/src/actions/RouterActions.js @@ -347,6 +347,17 @@ export const gotoFirmwareUpdate = (): ThunkAction => (dispatch: Dispatch, getSta dispatch(goto(`/device/${devUrl}/firmware-update`)); }; +/* +* Go to NoBackup page +*/ +export const gotoBackup = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + const { selectedDevice } = getState().wallet; + if (!selectedDevice || !selectedDevice.features) return; + const devUrl: string = `${selectedDevice.features.device_id}${selectedDevice.instance ? `:${selectedDevice.instance}` : ''}`; + dispatch(goto(`/device/${devUrl}/backup`)); +}; + + /* * Try to redirect to initial url */ diff --git a/src/actions/constants/modal.js b/src/actions/constants/modal.js index 3383e91a..d7ac2cb9 100644 --- a/src/actions/constants/modal.js +++ b/src/actions/constants/modal.js @@ -7,3 +7,4 @@ export const CONTEXT_DEVICE: 'modal_ctx_device' = 'modal_ctx_device'; export const CONTEXT_EXTERNAL_WALLET: 'modal_ctx_external-wallet' = 'modal_ctx_external-wallet'; export const OPEN_SCAN_QR: 'modal__open_scan_qr' = 'modal__open_scan_qr'; export const CONTEXT_SCAN_QR: 'modal__ctx_scan_qr' = 'modal__ctx_scan_qr'; +export const CONTEXT_CONFIRMATION: 'modal__ctx_confirmation' = 'modal__ctx_confirmation'; diff --git a/src/components/modals/confirm/NoBackup/index.js b/src/components/modals/confirm/NoBackup/index.js new file mode 100644 index 00000000..205a9947 --- /dev/null +++ b/src/components/modals/confirm/NoBackup/index.js @@ -0,0 +1,82 @@ +/* @flow */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; + +import icons from 'config/icons'; +import colors from 'config/colors'; + +import { H2 } from 'components/Heading'; +import P from 'components/Paragraph'; +import Icon from 'components/Icon'; +import Button from 'components/Button'; +import Link from 'components/Link'; + +import type { Props as BaseProps } from '../../Container'; + +type Props = { + onReceiveConfirmation: $ElementType<$ElementType, 'onReceiveConfirmation'>; +} + +const Wrapper = styled.div` + max-width: 370px; + padding: 30px 48px; +`; + +const StyledLink = styled(Link)` + position: absolute; + right: 15px; + top: 15px; +`; + +const BackupButton = styled(Button)` + width: 100%; + margin-bottom: 10px; +`; + +const ProceedButton = styled(Button)` + background: transparent; + border-color: ${colors.WARNING_PRIMARY}; + color: ${colors.WARNING_PRIMARY}; + + &:focus, + &:hover, + &:active { + color: ${colors.WHITE}; + background: ${colors.WARNING_PRIMARY}; + box-shadow: none; + } +`; + +const StyledP = styled(P)` + padding-bottom: 20px; +`; + +const Row = styled.div` + display: flex; + flex-direction: column; +`; + +const Confirmation = (props: Props) => ( + + props.onReceiveConfirmation(false)}> + + +

Your Trezor is not backed up

+ + If your device is ever lost or damaged, your funds will be lost. Backup your device first, to protect your coins against such events. + + + props.onReceiveConfirmation(false)}>Create a backup in 3 minutes + + props.onReceiveConfirmation(true)}>Show address, I will take the risk + +
+); + +Confirmation.propTypes = { + onReceiveConfirmation: PropTypes.func.isRequired, +}; + +export default Confirmation; \ No newline at end of file diff --git a/src/components/modals/confirm/UnverifiedAddress/index.js b/src/components/modals/confirm/UnverifiedAddress/index.js index e6f26592..dec2098f 100644 --- a/src/components/modals/confirm/UnverifiedAddress/index.js +++ b/src/components/modals/confirm/UnverifiedAddress/index.js @@ -31,11 +31,23 @@ const StyledLink = styled(Link)` const Wrapper = styled.div` max-width: 370px; - padding: 30px 48px; + padding: 30px 0px; + +`; + +const Content = styled.div` + padding: 0px 48px; `; const StyledP = styled(P)` - padding: 20px 0px; + padding-bottom: 20px; +`; + +const Divider = styled.div` + width: 100%; + height: 1px; + background: ${colors.DIVIDER}; + margin: 20px 0px; `; const Row = styled.div` @@ -47,6 +59,24 @@ const Row = styled.div` } `; +const BackupButton = styled(Button)` + width: 100%; +`; + +const WarnButton = styled(Button)` + background: transparent; + border-color: ${colors.WARNING_PRIMARY}; + color: ${colors.WARNING_PRIMARY}; + + &:focus, + &:hover, + &:active { + color: ${colors.WHITE}; + background: ${colors.WARNING_PRIMARY}; + box-shadow: none; + } +`; + class ConfirmUnverifiedAddress extends PureComponent { componentDidMount(): void { this.keyboardHandler = this.keyboardHandler.bind(this); @@ -86,26 +116,48 @@ class ConfirmUnverifiedAddress extends PureComponent { let claim: string; if (!device.connected) { - deviceStatus = `${device.label} is not connected`; + deviceStatus = `Device ${device.label} is not connected`; claim = 'Please connect your device'; } else { // corner-case where device is connected but it is unavailable because it was created with different "passphrase_protection" settings const enable: string = device.features && device.features.passphrase_protection ? 'enable' : 'disable'; - deviceStatus = `${device.label} is unavailable`; + deviceStatus = `Device ${device.label} is unavailable`; claim = `Please ${enable} passphrase settings`; } + const needsBackup = device.features && device.features.needs_backup; + return ( - - - -

{ deviceStatus }

- To prevent phishing attacks, you should verify the address on your Trezor first. { claim } to continue with the verification process. - - - - + + + + +

{ deviceStatus }

+ To prevent phishing attacks, you should verify the address on your Trezor first. { claim } to continue with the verification process. +
+ + + + this.showUnverifiedAddress()}>Show unverified address + + + {needsBackup && } + {needsBackup && ( + <> + +

Device {device.label} is not backed up

+ If your device is ever lost or damaged, your funds will be lost. Backup your device first, to protect your coins against such events. +
+ + + + Create a backup in 3 minutes + + + + + )}
); } diff --git a/src/components/modals/index.js b/src/components/modals/index.js index 4db54d9e..7fcb70db 100644 --- a/src/components/modals/index.js +++ b/src/components/modals/index.js @@ -19,6 +19,7 @@ import PassphraseType from 'components/modals/passphrase/Type'; import ConfirmSignTx from 'components/modals/confirm/SignTx'; import ConfirmAction from 'components/modals/confirm/Action'; import ConfirmUnverifiedAddress from 'components/modals/confirm/UnverifiedAddress'; +import ConfirmNoBackup from 'components/modals/confirm/NoBackup'; import ForgetDevice from 'components/modals/device/Forget'; import RememberDevice from 'components/modals/device/Remember'; import DuplicateDevice from 'components/modals/device/Duplicate'; @@ -189,6 +190,19 @@ const getQrModal = (props: Props) => { ); }; +const getConfirmationModal = (props: Props) => { + const { modal, modalActions } = props; + + if (modal.context !== MODAL.CONTEXT_CONFIRMATION) return null; + + switch (modal.windowType) { + case 'no-backup': + return (); + default: + return null; + } +}; + // modal container component const Modal = (props: Props) => { const { modal } = props; @@ -205,6 +219,9 @@ const Modal = (props: Props) => { case MODAL.CONTEXT_SCAN_QR: component = getQrModal(props); break; + case MODAL.CONTEXT_CONFIRMATION: + component = getConfirmationModal(props); + break; default: break; } diff --git a/src/components/notifications/App/components/NoBackup/index.js b/src/components/notifications/App/components/NoBackup/index.js new file mode 100644 index 00000000..5b7f2082 --- /dev/null +++ b/src/components/notifications/App/components/NoBackup/index.js @@ -0,0 +1,25 @@ +/* @flow */ +import * as React from 'react'; +import Notification from 'components/Notification'; + +import type { Props } from '../../index'; + +export default (props: Props) => { + const { selectedDevice } = props.wallet; + const needsBackup = selectedDevice && selectedDevice.features && selectedDevice.features.needs_backup; + if (!needsBackup) return null; + return ( + + ); +}; \ No newline at end of file diff --git a/src/components/notifications/App/index.js b/src/components/notifications/App/index.js index 6167cfcf..d7c91180 100644 --- a/src/components/notifications/App/index.js +++ b/src/components/notifications/App/index.js @@ -12,6 +12,7 @@ import * as RouterActions from 'actions/RouterActions'; import OnlineStatus from './components/OnlineStatus'; import UpdateBridge from './components/UpdateBridge'; import UpdateFirmware from './components/UpdateFirmware'; +import NoBackup from './components/NoBackup'; export type StateProps = { connect: $ElementType; @@ -33,6 +34,7 @@ const Notifications = (props: Props) => ( + ); diff --git a/src/reducers/ModalReducer.js b/src/reducers/ModalReducer.js index 266ced8b..70e534bf 100644 --- a/src/reducers/ModalReducer.js +++ b/src/reducers/ModalReducer.js @@ -20,7 +20,10 @@ export type State = { windowType?: string; } | { context: typeof MODAL.CONTEXT_SCAN_QR, -} +} | { + context: typeof MODAL.CONTEXT_CONFIRMATION, + windowType: string; +}; const initialState: State = { context: MODAL.CONTEXT_NONE, @@ -98,6 +101,12 @@ export default function modal(state: State = initialState, action: Action): Stat context: MODAL.CONTEXT_SCAN_QR, }; + case UI.REQUEST_CONFIRMATION: + return { + context: MODAL.CONTEXT_CONFIRMATION, + windowType: action.payload.view, + }; + default: return state; } diff --git a/src/support/routes.js b/src/support/routes.js index 717bed03..1ebe97c2 100644 --- a/src/support/routes.js +++ b/src/support/routes.js @@ -62,6 +62,11 @@ export const routes: Array = [ pattern: '/device/:device/firmware-update', fields: ['device', 'firmware-update'], }, + { + name: 'wallet-backup', + pattern: '/device/:device/backup', + fields: ['device', 'backup'], + }, { name: 'wallet-device-settings', pattern: '/device/:device/settings', diff --git a/src/views/Wallet/views/NoBackup/index.js b/src/views/Wallet/views/NoBackup/index.js new file mode 100644 index 00000000..62d2b982 --- /dev/null +++ b/src/views/Wallet/views/NoBackup/index.js @@ -0,0 +1,62 @@ +/* @flow */ + +import React from 'react'; +import styled from 'styled-components'; +import { H1 } from 'components/Heading'; +import P from 'components/Paragraph'; +import Link from 'components/Link'; +import Button from 'components/Button'; +import Icon from 'components/Icon'; + +import { FONT_SIZE } from 'config/variables'; +import colors from 'config/colors'; +import icons from 'config/icons'; + +const Wrapper = styled.section` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 90px 35px 40px 35px; +`; + +const StyledNavLink = styled(Link)` + color: ${colors.TEXT_SECONDARY}; + padding-top: 20px; + font-size: ${FONT_SIZE.BASE}; +`; + +const StyledH1 = styled(H1)` + text-align: center; +`; + +const StyledP = styled(P)` + max-width: 550px; + padding-bottom: 15px; +`; + +const Message = styled.div` + text-align: center; + padding: 0 0 15px 0; +`; + +const FirmwareUpdate = () => ( + + + Your Trezor is not backed up! + + If your device is ever lost or damaged, your funds will be lost. Backup your device first, to protect your coins against such events. +

Please use Bitcoin wallet interface to create a backup.

+
+ + + + I’ll do that later. +
+); + +export default FirmwareUpdate; \ No newline at end of file diff --git a/src/views/index.js b/src/views/index.js index 1b0a8963..184b4133 100644 --- a/src/views/index.js +++ b/src/views/index.js @@ -27,6 +27,7 @@ import WalletDeviceSettings from 'views/Wallet/views/DeviceSettings'; import WalletSettings from 'views/Wallet/views/WalletSettings'; import WalletBootloader from 'views/Wallet/views/Bootloader'; import WalletFirmwareUpdate from 'views/Wallet/views/FirmwareUpdate'; +import WalletNoBackup from 'views/Wallet/views/NoBackup'; import WalletInitialize from 'views/Wallet/views/Initialize'; import WalletSeedless from 'views/Wallet/views/Seedless'; import WalletAcquire from 'views/Wallet/views/Acquire'; @@ -54,6 +55,7 @@ const App = () => ( +