Browse Source

Merge branch 'master' into i18n-eslint

master
Thibaut Boustany 7 years ago
parent
commit
053f270bfc
No known key found for this signature in database GPG Key ID: 32475B11A2B13EEC
  1. 76
      src/commands/libcoreSignAndBroadcast.js
  2. 8
      src/components/AccountPage/index.js
  3. 8
      src/components/Breadcrumb/Step.js
  4. 9
      src/components/ManagerPage/AppSearchBar.js
  5. 26
      src/components/ManagerPage/AppsList.js
  6. 4
      src/components/ManagerPage/Dashboard.js
  7. 2
      src/components/ManagerPage/FirmwareUpdate.js
  8. 12
      src/components/ManagerPage/ManagerApp.js
  9. 26
      src/components/ManagerPage/Workflow.js
  10. 28
      src/components/ManagerPage/WorkflowDefault.js
  11. 12
      src/components/ManagerPage/index.js
  12. 17
      src/components/Onboarding/OnboardingFooter.js
  13. 11
      src/components/Onboarding/helperComponents.js
  14. 2
      src/components/Onboarding/index.js
  15. 25
      src/components/Onboarding/steps/Analytics.js
  16. 4
      src/components/Onboarding/steps/Finish.js
  17. 112
      src/components/Onboarding/steps/GenuineCheck.js
  18. 24
      src/components/Onboarding/steps/Init.js
  19. 71
      src/components/Onboarding/steps/NoDevice.js
  20. 15
      src/components/Onboarding/steps/SelectDevice.js
  21. 8
      src/components/Onboarding/steps/SelectPIN/index.js
  22. 34
      src/components/Onboarding/steps/SetPassword.js
  23. 8
      src/components/Onboarding/steps/WriteSeed/index.js
  24. 3
      src/components/base/InputPassword/index.js
  25. 38
      src/helpers/libcore.js
  26. 16
      src/icons/Cart.js
  27. 16
      src/icons/EyeOff.js
  28. 16
      src/icons/Truck.js
  29. 9
      src/reducers/onboarding.js
  30. 16
      static/i18n/en/onboarding.yml

76
src/commands/libcoreSignAndBroadcast.js

@ -10,7 +10,7 @@ import { isSegwitAccount } from 'helpers/bip32'
import withLibcore from 'helpers/withLibcore'
import { createCommand, Command } from 'helpers/ipc'
import { withDevice } from 'helpers/deviceAccess'
import { getWalletIdentifier } from 'helpers/libcore'
import * as accountIdHelper from 'helpers/accountId'
type BitcoinLikeTransaction = {
amount: number,
@ -152,56 +152,46 @@ export async function doSignAndBroadcast({
onSigned: () => void,
onOperationBroadcasted: (optimisticOp: $Exact<OperationRaw>) => void,
}): Promise<void> {
let njsAccount
const { walletName } = accountIdHelper.decode(account.id)
const njsWallet = await core.getWallet(walletName)
if (isCancelled()) return
const njsAccount = await njsWallet.getAccount(account.index)
if (isCancelled()) return
const bitcoinLikeAccount = njsAccount.asBitcoinLikeAccount()
const njsWalletCurrency = njsWallet.getCurrency()
const amount = core.createAmount(njsWalletCurrency, transaction.amount)
const fees = core.createAmount(njsWalletCurrency, transaction.feePerByte)
const transactionBuilder = bitcoinLikeAccount.buildTransaction()
// TODO: check if is valid address. if not, it will fail silently on invalid
transactionBuilder.sendToAddress(amount, transaction.recipient)
// TODO: don't use hardcoded value for sequence (and first also maybe)
transactionBuilder.pickInputs(0, 0xffffff)
transactionBuilder.setFeesPerByte(fees)
const builded = await transactionBuilder.build()
if (isCancelled()) return
const sigHashType = Buffer.from(njsWalletCurrency.bitcoinLikeNetworkParameters.SigHash).toString(
'hex',
)
const signedTransaction = await withDevice(deviceId)(async transport => {
const hwApp = new Btc(transport)
const hasTimestamp = !!njsWalletCurrency.bitcoinLikeNetworkParameters.UsesTimestampedTransaction
// TODO: const timestampDelay = njsWalletCurrency.bitcoinLikeNetworkParameters.TimestampDelay
const WALLET_IDENTIFIER = await getWalletIdentifier({
hwApp,
isSegwit: isSegwitAccount(account),
currencyId: account.currencyId,
devicePath: deviceId,
})
const njsWallet = await core.getWallet(WALLET_IDENTIFIER)
if (isCancelled()) return null
njsAccount = await njsWallet.getAccount(account.index)
if (isCancelled()) return null
const bitcoinLikeAccount = njsAccount.asBitcoinLikeAccount()
const njsWalletCurrency = njsWallet.getCurrency()
const amount = core.createAmount(njsWalletCurrency, transaction.amount)
const fees = core.createAmount(njsWalletCurrency, transaction.feePerByte)
const transactionBuilder = bitcoinLikeAccount.buildTransaction()
// TODO: check if is valid address. if not, it will fail silently on invalid
transactionBuilder.sendToAddress(amount, transaction.recipient)
// TODO: don't use hardcoded value for sequence (and first also maybe)
transactionBuilder.pickInputs(0, 0xffffff)
transactionBuilder.setFeesPerByte(fees)
const builded = await transactionBuilder.build()
if (isCancelled()) return null
const sigHashType = Buffer.from(
njsWalletCurrency.bitcoinLikeNetworkParameters.SigHash,
).toString('hex')
const hasTimestamp = !!njsWalletCurrency.bitcoinLikeNetworkParameters.UsesTimestampedTransaction
// TODO: const timestampDelay = njsWalletCurrency.bitcoinLikeNetworkParameters.TimestampDelay
const currency = getCryptoCurrencyById(account.currencyId)
return signTransaction({
hwApp,
const currency = getCryptoCurrencyById(account.currencyId)
const signedTransaction = await withDevice(deviceId)(async transport =>
signTransaction({
hwApp: new Btc(transport),
currencyId: account.currencyId,
transaction: builded,
sigHashType: parseInt(sigHashType, 16),
supportsSegwit: !!currency.supportsSegwit,
isSegwit: isSegwitAccount(account),
hasTimestamp,
})
})
}),
)
if (!signedTransaction || isCancelled() || !njsAccount) return
onSigned()

8
src/components/AccountPage/index.js

@ -130,9 +130,11 @@ class AccountPage extends PureComponent<Props, State> {
)}
<Tooltip render={() => t('app:account.settings.title')}>
<ButtonSettings onClick={() => openModal(MODAL_SETTINGS_ACCOUNT, { account })}>
<Box align="center">
<IconAccountSettings size={16} />
</Box>
<Button small outlineGrey>
<Box justifyContent="center">
<IconAccountSettings size={16} />
</Box>
</Button>
</ButtonSettings>
</Tooltip>
</Box>

8
src/components/Breadcrumb/Step.js

@ -35,7 +35,13 @@ const StepNumber = styled(Box).attrs({
ff: 'Rubik|Regular',
})`
border-radius: 50%;
border: 1px solid ${p => (['active', 'valid'].includes(p.status) ? colors.wallet : colors.fog)};
border: 1px solid
${p =>
['active', 'valid'].includes(p.status)
? colors.wallet
: p.status === 'error'
? colors.alertRed
: colors.fog};
font-size: 10px;
height: ${RADIUS}px;
line-height: 10px;

9
src/components/ManagerPage/AppSearchBar.js

@ -42,6 +42,13 @@ const SearchBarWrapper = styled(Box).attrs({
margin: 0 0 20px 0;
background-color: white;
padding: 0 13px;
${p =>
p.focused
? `
border: 1px solid #6490f1;
`
: 'border: 1px solid white;'};
`
const Input = styled.input.attrs({
@ -91,7 +98,7 @@ class AppSearchBar extends PureComponent<Props, State> {
return (
<Box>
<SearchBarWrapper align="center">
<SearchBarWrapper align="center" focused={focused}>
<SearchIcon size={16} style={{ color }} />
<Input
innerRef={c => (this.input = c)}

26
src/components/ManagerPage/AppsList.js

@ -15,6 +15,8 @@ import Modal, { ModalBody } from 'components/base/Modal'
import Tooltip from 'components/base/Tooltip'
import Text from 'components/base/Text'
import Progress from 'components/base/Progress'
import Spinner from 'components/base/Spinner'
import Button from 'components/base/Button'
import ExclamationCircle from 'icons/ExclamationCircle'
import Update from 'icons/Update'
@ -38,7 +40,7 @@ const ICONS_FALLBACK = {
}
type Status = 'loading' | 'idle' | 'busy' | 'success' | 'error'
type Mode = '' | 'installing' | 'uninstalling'
type Mode = 'home' | 'installing' | 'uninstalling'
type LedgerApp = {
name: string,
@ -71,7 +73,7 @@ class AppsList extends PureComponent<Props, State> {
error: null,
appsList: [],
app: '',
mode: '',
mode: 'home',
}
componentDidMount() {
@ -108,7 +110,7 @@ class AppsList extends PureComponent<Props, State> {
await installApp.send(data).toPromise()
this.setState({ status: 'success', app: '' })
} catch (err) {
this.setState({ status: 'error', error: err.message, app: '', mode: '' })
this.setState({ status: 'error', error: err.message, app: '', mode: 'home' })
}
}
@ -123,11 +125,11 @@ class AppsList extends PureComponent<Props, State> {
await uninstallApp.send(data).toPromise()
this.setState({ status: 'success', app: '' })
} catch (err) {
this.setState({ status: 'error', error: err.message, app: '', mode: '' })
this.setState({ status: 'error', error: err.message, app: '', mode: 'home' })
}
}
handleCloseModal = () => this.setState({ status: 'idle', mode: '' })
handleCloseModal = () => this.setState({ status: 'idle', mode: 'home' })
renderModal = () => {
const { t } = this.props
@ -152,7 +154,9 @@ class AppsList extends PureComponent<Props, State> {
<Box align="center" justify="center" flow={3}>
<div>{'error happened'}</div>
{error}
<button onClick={this.handleCloseModal}>close</button>
<Button primary onClick={this.handleCloseModal}>
close
</Button>
</Box>
) : status === 'success' ? (
<Box align="center" justify="center" flow={3}>
@ -167,7 +171,9 @@ class AppsList extends PureComponent<Props, State> {
{ app },
)}
</Text>
<button onClick={this.handleCloseModal}>close</button>
<Button primary onClick={this.handleCloseModal}>
close
</Button>
</Box>
) : null}
</ModalBody>
@ -178,7 +184,7 @@ class AppsList extends PureComponent<Props, State> {
renderList() {
const { appsList } = this.state
return (
return appsList.length > 0 ? (
<Box>
<AppSearchBar list={appsList}>
{items => (
@ -198,6 +204,10 @@ class AppsList extends PureComponent<Props, State> {
</AppSearchBar>
{this.renderModal()}
</Box>
) : (
<Box align="center" justify="center">
<Spinner size={50} />
</Box>
)
}

4
src/components/ManagerPage/Dashboard.js

@ -33,7 +33,7 @@ const Dashboard = ({ device, deviceInfo, t }: Props) => (
{t('app:manager.subtitle')}
</Text>
</Box>
<Box mt={7}>
<Box mt={5}>
<FirmwareUpdate
infos={{
targetId: deviceInfo.targetId,
@ -42,7 +42,7 @@ const Dashboard = ({ device, deviceInfo, t }: Props) => (
device={device}
/>
</Box>
<Box>
<Box mt={5}>
<AppsList device={device} targetId={deviceInfo.targetId} />
</Box>
</Box>

2
src/components/ManagerPage/FirmwareUpdate.js

@ -102,7 +102,7 @@ class FirmwareUpdate extends PureComponent<Props, State> {
const { latestFirmware } = this.state
return (
<Card px={4} py={25}>
<Card p={4}>
<Box horizontal align="center" flow={2}>
<Box color="dark">
<NanoS size={30} />

12
src/components/ManagerPage/ManagerApp.js

@ -14,13 +14,14 @@ import Button from 'components/base/Button'
const Container = styled(Box).attrs({
horizontal: true,
m: 3,
my: 2,
mx: 3,
p: 4,
boxShadow: 0,
borderRadius: 4,
flow: 3,
flow: 2,
})`
width: 342px;
width: calc(50% - 30px);
background: white;
line-height: normal;
`
@ -37,7 +38,6 @@ const AppName = styled(Box).attrs({
color: 'dark',
})`
display: block;
width: 115px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@ -57,8 +57,8 @@ function ManagerApp({ name, version, icon, onInstall, onUninstall, t }: Props) {
return (
<Container>
<AppIcon src={iconUrl} />
<Box flex="1">
<AppName>{name}</AppName>
<Box flex="1" ml={3}>
<AppName flex={1}>{name}</AppName>
<Text ff="Open Sans|Regular" fontSize={3} color="grey">
{version}
</Text>

26
src/components/ManagerPage/Workflow.js

@ -25,13 +25,16 @@ type Props = {
renderDefault: (
device: ?Device,
deviceInfo: ?DeviceInfo,
dashboardError: ?Error,
isGenuine: ?boolean,
error: {
dashboardError: ?Error,
genuineError: ?Error,
},
) => Node,
renderMcuUpdate: (deviceInfo: DeviceInfo) => Node,
renderFinalUpdate: (deviceInfo: DeviceInfo) => Node,
renderDashboard: (device: Device, deviceInfo: DeviceInfo) => Node,
renderError: (dashboardError: ?Error, genuineError: ?Error) => Node,
renderError?: (dashboardError: ?Error, genuineError: ?Error) => Node,
}
type State = {}
@ -52,14 +55,12 @@ class Workflow extends PureComponent<Props, State> {
<EnsureGenuine device={device}>
{(isGenuine: ?boolean, genuineError: ?Error) => {
if (dashboardError || genuineError) {
return renderError ? (
renderError(dashboardError, genuineError)
) : (
<div>
{dashboardError && <span>{dashboardError.message}</span>}
{genuineError && <span>{genuineError.message}</span>}
</div>
)
return renderError
? renderError(dashboardError, genuineError)
: renderDefault(device, deviceInfo, isGenuine, {
genuineError,
dashboardError,
})
}
if (deviceInfo && deviceInfo.mcu) {
@ -74,7 +75,10 @@ class Workflow extends PureComponent<Props, State> {
return renderDashboard(device, deviceInfo)
}
return renderDefault(device, deviceInfo, dashboardError, isGenuine)
return renderDefault(device, deviceInfo, isGenuine, {
genuineError,
dashboardError,
})
}}
</EnsureGenuine>
)}

28
src/components/ManagerPage/WorkflowDefault.js

@ -8,7 +8,6 @@ import type { Device, T } from 'types/common'
import { i } from 'helpers/staticPath'
import Box from 'components/base/Box'
import Space from 'components/base/Space'
import Text from 'components/base/Text'
import Spinner from 'components/base/Spinner'
@ -97,13 +96,15 @@ type Props = {
t: T,
device: ?Device,
deviceInfo: ?DeviceInfo,
dashboardError: ?Error,
errors: {
dashboardError: ?Error,
genuineError: ?Error,
},
isGenuine: boolean,
}
const WorkflowDefault = ({ device, deviceInfo, dashboardError, isGenuine, t }: Props) => (
<Box align="center">
<Space of={152} />
const WorkflowDefault = ({ device, deviceInfo, errors, isGenuine, t }: Props) => (
<Box align="center" justify="center" sticky>
<Box align="center" style={{ maxWidth: 460, padding: '0 10px' }}>
<img
src={i('logos/connectDevice.png')}
@ -117,7 +118,7 @@ const WorkflowDefault = ({ device, deviceInfo, dashboardError, isGenuine, t }: P
{t('app:manager.device.desc')}
</Text>
</Box>
<Box flow={4} style={{ maxWidth: 460, padding: '60px 10px 0' }}>
<Box flow={4} style={{ maxWidth: 460, padding: '60px 10px 0' }} ff="Open Sans|Regular">
{/* DEVICE CHECK */}
<Step validated={!!device}>
<StepContent>
@ -138,7 +139,7 @@ const WorkflowDefault = ({ device, deviceInfo, dashboardError, isGenuine, t }: P
</Step>
{/* DASHBOARD CHECK */}
<Step validated={!!device && !!deviceInfo} hasErrors={!!device && !!dashboardError}>
<Step validated={!!device && !!deviceInfo} hasErrors={!!device && !!errors.dashboardError}>
<StepContent>
<StepIcon>
<WrapperIconCurrency>
@ -152,14 +153,21 @@ const WorkflowDefault = ({ device, deviceInfo, dashboardError, isGenuine, t }: P
{' on your device'}
</Trans>
</Box>
<StepCheck checked={!!device && !!deviceInfo} hasErrors={!!device && !!dashboardError} />
<StepCheck
checked={!!device && !!deviceInfo}
hasErrors={!!device && !!errors.dashboardError}
/>
</StepContent>
</Step>
{/* GENUINE CHECK */}
<Step
validated={(!!device && !isNull(isGenuine) && isGenuine) || undefined}
hasErrors={(!!device && !isNull(isGenuine) && !isGenuine) || undefined}
validated={
(!!device && !isNull(isGenuine) && isGenuine && !errors.genuineError) || undefined
}
hasErrors={
(!!device && !isNull(isGenuine) && !isGenuine) || errors.genuineError || undefined
}
>
<StepContent>
<StepIcon>

12
src/components/ManagerPage/index.js

@ -24,11 +24,6 @@ type Error = {
function ManagerPage(): Node {
return (
<Workflow
renderError={(dashboardError: ?Error, genuineError: ?Error) => {
if (dashboardError) return <span>Dashboard Error: {dashboardError.message}</span>
if (genuineError) return <span>Genuine Error: {genuineError.message}</span>
return <span>Error</span>
}}
renderFinalUpdate={(deviceInfo: DeviceInfo) => (
<p>UPDATE FINAL FIRMARE (TEMPLATE + ACTION WIP) {deviceInfo.final}</p>
)}
@ -41,13 +36,16 @@ function ManagerPage(): Node {
renderDefault={(
device: ?Device,
deviceInfo: ?DeviceInfo,
dashboardError: ?Error,
isGenuine: ?boolean,
errors: {
dashboardError: ?Error,
genuineError: ?Error,
},
) => (
<WorkflowDefault
device={device}
deviceInfo={deviceInfo}
dashboardError={dashboardError}
errors={errors}
isGenuine={isGenuine}
/>
)}

17
src/components/Onboarding/OnboardingFooter.js

@ -1,22 +1,11 @@
// @flow
import React from 'react'
import styled from 'styled-components'
import { radii } from 'styles/theme'
import type { T } from 'types/common'
import Button from 'components/base/Button'
import Box from 'components/base/Box'
const Wrapper = styled(Box).attrs({
px: 5,
py: 3,
})`
border-top: 1px solid ${p => p.theme.colors.lightFog};
border-bottom-left-radius: ${radii[1]}px;
border-bottom-right-radius: ${radii[1]}px;
`
import { OnboardingFooterWrapper } from './helperComponents'
type Props = {
t: T,
@ -33,13 +22,13 @@ const OnboardingFooter = ({
...props
}: Props) => (
<Wrapper {...props}>
<OnboardingFooterWrapper {...props}>
<Button padded outlineGrey onClick={() => prevStep()}>
{t('app:common.back')}
</Button>
<Button padded disabled={isContinueDisabled} primary onClick={() => nextStep()} ml="auto">
{t('app:common.continue')}
</Button>
</Wrapper>
</OnboardingFooterWrapper>
)
export default OnboardingFooter

11
src/components/Onboarding/helperComponents.js

@ -32,12 +32,19 @@ export const Inner = styled(Box).attrs({
flow: 4,
})``
export const FixedTopContainer = styled(Box).attrs({
sticky: true,
mt: 170,
backgroundColor: 'red',
})``
// FOOTER
export const OnboardingFooter = styled(Box).attrs({
export const OnboardingFooterWrapper = styled(Box).attrs({
px: 5,
py: 3,
horizontal: true,
})`
border-top: 2px solid ${p => p.theme.colors.lightGrey};
border-top: 2px solid ${p => p.theme.colors.lightFog};
border-bottom-left-radius: ${radii[1]}px;
border-bottom-right-radius: ${radii[1]}px;
`

2
src/components/Onboarding/index.js

@ -27,6 +27,7 @@ import Box from 'components/base/Box'
import Start from './steps/Start'
import InitStep from './steps/Init'
import NoDeviceStep from './steps/NoDevice'
import OnboardingBreadcrumb from './OnboardingBreadcrumb'
import SelectDevice from './steps/SelectDevice'
import SelectPIN from './steps/SelectPIN/index'
@ -46,6 +47,7 @@ const STEPS = {
analytics: Analytics,
finish: Finish,
start: Start,
noDevice: NoDeviceStep,
}
const mapStateToProps = state => ({

25
src/components/Onboarding/steps/Analytics.js

@ -6,7 +6,7 @@ import { connect } from 'react-redux'
import { saveSettings } from 'actions/settings'
import Box from 'components/base/Box'
import CheckBox from 'components/base/CheckBox'
import { Title, Description } from '../helperComponents'
import { Title, Description, FixedTopContainer } from '../helperComponents'
import OnboardingFooter from '../OnboardingFooter'
import type { StepProps } from '..'
@ -15,13 +15,11 @@ const mapDispatchToProps = { saveSettings }
type State = {
analyticsToggle: boolean,
termsConditionsToggle: boolean,
sentryLogsToggle: boolean,
}
const INITIAL_STATE = {
analyticsToggle: true,
termsConditionsToggle: false,
sentryLogsToggle: true,
}
@ -40,9 +38,6 @@ class Analytics extends PureComponent<StepProps, State> {
shareAnalytics: isChecked,
})
}
handleTermsToggle = () => {
this.setState({ termsConditionsToggle: !this.state.termsConditionsToggle })
}
handleNavBack = () => {
const { savePassword, prevStep } = this.props
@ -52,11 +47,11 @@ class Analytics extends PureComponent<StepProps, State> {
render() {
const { nextStep, t } = this.props
const { analyticsToggle, termsConditionsToggle, sentryLogsToggle } = this.state
const { analyticsToggle, sentryLogsToggle } = this.state
return (
<Box sticky pt={50}>
<Box grow alignItems="center" justifyContent="center">
<FixedTopContainer>
<Box grow alignItems="center">
<Title>{t('onboarding:analytics.title')}</Title>
<Description>{t('onboarding:analytics.desc')}</Description>
<Box mt={5}>
@ -78,15 +73,6 @@ class Analytics extends PureComponent<StepProps, State> {
<CheckBox isChecked={analyticsToggle} onChange={this.handleAnalyticsToggle} />
</Box>
</Container>
<Container>
<Box>
<AnalyticsTitle>{t('onboarding:analytics.termsConditions.title')}</AnalyticsTitle>
<AnalyticsText>{t('onboarding:analytics.termsConditions.desc')}</AnalyticsText>
</Box>
<Box justifyContent="center">
<CheckBox isChecked={termsConditionsToggle} onChange={this.handleTermsToggle} />
</Box>
</Container>
</Box>
</Box>
<OnboardingFooter
@ -96,9 +82,8 @@ class Analytics extends PureComponent<StepProps, State> {
t={t}
nextStep={nextStep}
prevStep={this.handleNavBack}
isContinueDisabled={!termsConditionsToggle}
/>
</Box>
</FixedTopContainer>
)
}
}

4
src/components/Onboarding/steps/Finish.js

@ -39,8 +39,8 @@ const socialMedia = [
export default (props: StepProps) => {
const { finish, t } = props
return (
<Box sticky pt={200}>
<Box grow alignItems="center">
<Box sticky justifyContent="center">
<Box alignItems="center">
<Box color="positiveGreen">
<IconCheckCircle size={44} />
</Box>

112
src/components/Onboarding/steps/GenuineCheck.js

@ -4,13 +4,13 @@ import React, { PureComponent, Fragment } from 'react'
import { shell } from 'electron'
import { connect } from 'react-redux'
import styled from 'styled-components'
import { radii, colors } from 'styles/theme'
import { colors } from 'styles/theme'
import type { T } from 'types/common'
import { updateGenuineCheck } from 'reducers/onboarding'
import Box, { Card } from 'components/base/Box'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import RadioGroup from 'components/base/RadioGroup'
import GenuineCheckModal from 'components/GenuineCheckModal'
@ -19,7 +19,13 @@ import IconLedgerNanoError from 'icons/illustrations/LedgerNanoError'
import IconLedgerBlueError from 'icons/illustrations/LedgerBlueError'
import IconCheck from 'icons/Check'
import { Title, Description, IconOptionRow } from '../helperComponents'
import {
Title,
Description,
IconOptionRow,
FixedTopContainer,
OnboardingFooterWrapper,
} from '../helperComponents'
import type { StepProps } from '..'
import OnboardingFooter from '../OnboardingFooter'
@ -120,8 +126,8 @@ class GenuineCheck extends PureComponent<StepProps, State> {
}
return (
<Box sticky pt={50}>
<Box grow alignItems="center" justifyContent="center">
<FixedTopContainer>
<Box grow alignItems="center">
<Title>{t('onboarding:genuineCheck.title')}</Title>
<Description>{t('onboarding:genuineCheck.desc')}</Description>
<Box mt={5}>
@ -145,16 +151,18 @@ class GenuineCheck extends PureComponent<StepProps, State> {
<CardWrapper isDisabled={!genuine.pinStepPass}>
<Box justify="center">
<Box horizontal>
<IconOptionRow>{'2.'}</IconOptionRow>
<IconOptionRow color={!genuine.pinStepPass ? 'grey' : 'wallet'}>{'2.'}</IconOptionRow>
<CardTitle>{t('onboarding:genuineCheck.steps.step2.title')}</CardTitle>
</Box>
</Box>
<Box justify="center">
<RadioGroup
items={this.getButtonLabel()}
activeKey={cachedRecoveryStepButton}
onChange={item => this.handleButtonPass(item, 'recoveryStepPass')}
/>
{genuine.pinStepPass && (
<RadioGroup
items={this.getButtonLabel()}
activeKey={cachedRecoveryStepButton}
onChange={item => this.handleButtonPass(item, 'recoveryStepPass')}
/>
)}
</Box>
</CardWrapper>
</Box>
@ -162,28 +170,32 @@ class GenuineCheck extends PureComponent<StepProps, State> {
<CardWrapper isDisabled={!genuine.recoveryStepPass}>
<Box justify="center">
<Box horizontal>
<IconOptionRow>{'3.'}</IconOptionRow>
<IconOptionRow color={!genuine.recoveryStepPass ? 'grey' : 'wallet'}>
{'3.'}
</IconOptionRow>
<CardTitle>{t('onboarding:genuineCheck.steps.step3.title')}</CardTitle>
</Box>
</Box>
<Box justify="center">
{genuine.isDeviceGenuine ? (
<Box horizontal align="center" flow={1} color={colors.wallet}>
<IconCheck size={16} />
<GenuineSuccessText>
{t('onboarding:genuineCheck.isGenuinePassed')}
</GenuineSuccessText>
</Box>
) : (
<Button
primary
disabled={!genuine.recoveryStepPass}
onClick={this.handleOpenGenuineCheckModal}
>
{t('onboarding:genuineCheck.buttons.genuineCheck')}
</Button>
)}
</Box>
{genuine.recoveryStepPass && (
<Box justify="center">
{genuine.isDeviceGenuine ? (
<Box horizontal align="center" flow={1} color={colors.wallet}>
<IconCheck size={16} />
<GenuineSuccessText>
{t('onboarding:genuineCheck.isGenuinePassed')}
</GenuineSuccessText>
</Box>
) : (
<Button
primary
disabled={!genuine.recoveryStepPass}
onClick={this.handleOpenGenuineCheckModal}
>
{t('onboarding:genuineCheck.buttons.genuineCheck')}
</Button>
)}
</Box>
)}
</CardWrapper>
</Box>
</Box>
@ -201,7 +213,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
onClose={this.handleCloseGenuineCheckModal}
onGenuineCheck={this.handleGenuineCheck}
/>
</Box>
</FixedTopContainer>
)
}
}
@ -246,27 +258,14 @@ export function GenuineCheckFail({
</Fragment>
)}
</Box>
<Wrapper horizontal>
<Button
padded
outline
onClick={() => {
redoGenuineCheck()
}}
>
<OnboardingFooterWrapper>
<Button padded outlineGrey onClick={() => redoGenuineCheck()}>
{t('app:common.back')}
</Button>
<Button
padded
danger
onClick={() => {
contactSupport()
}}
ml="auto"
>
<Button padded danger onClick={() => contactSupport()} ml="auto">
{t('onboarding:genuineCheck.buttons.contactSupport')}
</Button>
</Wrapper>
</OnboardingFooterWrapper>
</Box>
)
}
@ -281,23 +280,22 @@ export const CardTitle = styled(Box).attrs({
pl: 2,
})``
const Wrapper = styled(Box).attrs({
px: 5,
py: 3,
})`
border-top: 2px solid ${p => p.theme.colors.lightGrey};
border-bottom-left-radius: ${radii[1]}px;
border-bottom-right-radius: ${radii[1]}px;
`
const CardWrapper = styled(Card).attrs({
const CardWrapper = styled(Box).attrs({
horizontal: true,
p: 5,
borderRadius: '4px',
justify: 'space-between',
})`
width: 580px;
height: 74px;
transition: all ease-in-out 0.2s;
color: ${p => (p.isDisabled ? p.theme.colors.grey : p.theme.colors.black)};
border: ${p => `1px ${p.isDisabled ? 'dashed' : 'solid'} ${p.theme.colors.fog}`};
pointer-events: ${p => (p.isDisabled ? 'none' : 'auto')};
background-color: ${p => (p.isDisabled ? p.theme.colors.lightGrey : p.theme.colors.white)};
opacity: ${p => (p.isDisabled ? 0.7 : 1)};
&:hover {
cursor: pointer;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.05);
}
`

24
src/components/Onboarding/steps/Init.js

@ -1,7 +1,6 @@
// @flow
import React, { PureComponent } from 'react'
import { shell } from 'electron'
import { connect } from 'react-redux'
import { colors } from 'styles/theme'
@ -22,7 +21,7 @@ const mapDispatchToProps = { flowType }
class Init extends PureComponent<StepProps, *> {
render() {
const { nextStep, t } = this.props
const { t, flowType, jumpStep } = this.props
const optionCards = [
{
@ -30,8 +29,8 @@ class Init extends PureComponent<StepProps, *> {
icon: <IconPlus size={20} />,
title: t('onboarding:init.newDevice.title'),
onClick: () => {
nextStep()
this.props.flowType('newDevice')
jumpStep('selectDevice')
flowType('newDevice')
},
},
{
@ -39,8 +38,8 @@ class Init extends PureComponent<StepProps, *> {
icon: <IconRecover size={20} />,
title: t('onboarding:init.restoreDevice.title'),
onClick: () => {
nextStep()
this.props.flowType('restoreDevice')
jumpStep('selectDevice')
flowType('restoreDevice')
},
},
{
@ -48,8 +47,8 @@ class Init extends PureComponent<StepProps, *> {
icon: <IconCheck size={20} />,
title: t('onboarding:init.initializedDevice.title'),
onClick: () => {
nextStep()
this.props.flowType('initializedDevice')
jumpStep('selectDevice')
flowType('initializedDevice')
},
},
{
@ -57,15 +56,15 @@ class Init extends PureComponent<StepProps, *> {
icon: <IconExternalLink size={20} />,
title: t('onboarding:init.noDevice.title'),
onClick: () => {
shell.openExternal('https://www.ledger.fr/')
this.props.flowType('noDevice')
jumpStep('noDevice')
flowType('noDevice')
},
},
]
return (
<Box sticky pt={130}>
<Box align="center" justifyContent="center">
<Box sticky justifyContent="center">
<Box align="center">
<Box color="wallet">
<IconUser size={36} />
</Box>
@ -117,6 +116,7 @@ const InitCardContainer = styled(Box).attrs({
border: 1px solid ${p => p.theme.colors.fog};
width: 530px;
height: 70px;
transition: all ease-in-out 0.2s;
&:hover {
cursor: pointer;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.05);

71
src/components/Onboarding/steps/NoDevice.js

@ -0,0 +1,71 @@
// @flow
import React, { PureComponent } from 'react'
import { shell } from 'electron'
import Box from 'components/base/Box'
import IconUser from 'icons/User'
import IconCart from 'icons/Cart'
import IconTruck from 'icons/Truck'
import IconInfoCircle from 'icons/InfoCircle'
import Button from '../../base/Button/index'
import { Title, OnboardingFooterWrapper } from '../helperComponents'
import { OptionFlowCard } from './Init'
import type { StepProps } from '..'
class NoDevice extends PureComponent<StepProps, *> {
render() {
const { t, prevStep } = this.props
const optionCards = [
{
key: 'buyNew',
icon: <IconCart size={20} />,
title: t('onboarding:noDevice.buyNew.title'),
onClick: () => {
shell.openExternal('https://www.ledgerwallet.com/')
},
},
{
key: 'trackOrder',
icon: <IconTruck size={20} />,
title: t('onboarding:noDevice.trackOrder.title'),
onClick: () => {
shell.openExternal('http://order.ledgerwallet.com/')
},
},
{
key: 'learnMore',
icon: <IconInfoCircle size={20} />,
title: t('onboarding:noDevice.learnMore.title'),
onClick: () => {
shell.openExternal('https://www.ledgerwallet.com/')
},
},
]
return (
<Box sticky pt={130}>
<Box grow alignItems="center">
<Box color="wallet">
<IconUser size={36} />
</Box>
<Box m={5} style={{ maxWidth: 480 }}>
<Title>{t('onboarding:noDevice.title')}</Title>
</Box>
<Box pt={4} flow={4}>
{optionCards.map(card => <OptionFlowCard key={card.key} card={card} />)}
</Box>
</Box>
<OnboardingFooterWrapper>
<Button padded outlineGrey onClick={() => prevStep()} mr="auto">
{t('app:common.back')}
</Button>
</OnboardingFooterWrapper>
</Box>
)
}
}
export default NoDevice

15
src/components/Onboarding/steps/SelectDevice.js

@ -11,7 +11,7 @@ import Box from 'components/base/Box'
import IconCheckCirle from 'icons/Check'
import IconLedgerNano from 'icons/illustrations/LedgerNano'
import IconLedgerBlue from 'icons/illustrations/LedgerBlue'
import { Title, Inner } from '../helperComponents'
import { Title, Inner, FixedTopContainer } from '../helperComponents'
import OnboardingFooter from '../OnboardingFooter'
import type { StepProps } from '..'
@ -32,11 +32,11 @@ class SelectDevice extends PureComponent<StepProps, {}> {
}
}
render() {
const { t, onboarding, prevStep } = this.props
const { t, onboarding, jumpStep } = this.props
return (
<Box sticky>
<Box grow alignItems="center" justifyContent="center">
<Box m={5}>
<FixedTopContainer>
<Box grow alignItems="center">
<Box mb={5}>
<Title>{t('onboarding:selectDevice.title')}</Title>
</Box>
<Box pt={4}>
@ -66,13 +66,12 @@ class SelectDevice extends PureComponent<StepProps, {}> {
</Box>
<OnboardingFooter
horizontal
flow={2}
t={t}
nextStep={this.handleContinue}
prevStep={prevStep}
prevStep={() => jumpStep('init')}
isContinueDisabled={onboarding.isLedgerNano === null}
/>
</Box>
</FixedTopContainer>
)
}
}

8
src/components/Onboarding/steps/SelectPIN/index.js

@ -4,7 +4,7 @@ import React from 'react'
import Box from 'components/base/Box'
import { Title } from '../../helperComponents'
import { Title, FixedTopContainer } from '../../helperComponents'
import OnboardingFooter from '../../OnboardingFooter'
import SelectPINnano from './SelectPINnano'
import SelectPINblue from './SelectPINblue'
@ -15,14 +15,14 @@ export default (props: StepProps) => {
const { nextStep, prevStep, t, onboarding } = props
return (
<Box sticky pt={50}>
<Box grow alignItems="center" justifyContent="center">
<FixedTopContainer>
<Box grow alignItems="center">
<Title>{t('onboarding:selectPIN.title')}</Title>
<Box align="center" mt={7}>
{onboarding.isLedgerNano ? <SelectPINnano /> : <SelectPINblue />}
</Box>
</Box>
<OnboardingFooter horizontal flow={2} t={t} nextStep={nextStep} prevStep={prevStep} />
</Box>
</FixedTopContainer>
)
}

34
src/components/Onboarding/steps/SetPassword.js

@ -2,8 +2,7 @@
import React, { PureComponent, Fragment } from 'react'
import bcrypt from 'bcryptjs'
import { colors, radii } from 'styles/theme'
import styled from 'styled-components'
import { colors } from 'styles/theme'
import { setEncryptionKey } from 'helpers/db'
@ -15,7 +14,13 @@ import IconChevronRight from 'icons/ChevronRight'
import PasswordForm from '../../SettingsPage/PasswordForm'
import type { StepProps } from '..'
import { Title, Description, DisclaimerBox } from '../helperComponents'
import {
Title,
Description,
DisclaimerBox,
FixedTopContainer,
OnboardingFooterWrapper,
} from '../helperComponents'
type State = {
currentPassword: string,
@ -90,10 +95,10 @@ class SetPassword extends PureComponent<StepProps, State> {
]
return (
<Box sticky pt={50}>
<Box grow alignItems="center" justify="center">
<FixedTopContainer>
<Box grow alignItems="center">
<Fragment>
<Box mb={3} alignItems="center">
<Box alignItems="center">
<Title>{t('onboarding:setPassword.title')}</Title>
<Description style={{ maxWidth: 620 }}>
{t('onboarding:setPassword.desc')}
@ -117,7 +122,7 @@ class SetPassword extends PureComponent<StepProps, State> {
</Fragment>
</Box>
<CustomFooter>
<OnboardingFooterWrapper>
<Button padded outlineGrey onClick={() => prevStep()}>
{t('app:common.back')}
</Button>
@ -134,21 +139,10 @@ class SetPassword extends PureComponent<StepProps, State> {
{t('app:common.continue')}
</Button>
</Box>
</CustomFooter>
</Box>
</OnboardingFooterWrapper>
</FixedTopContainer>
)
}
}
export default SetPassword
const CustomFooter = styled(Box).attrs({
px: 5,
py: 3,
horizontal: true,
align: 'center',
})`
border-top: 1px solid ${p => p.theme.colors.lightFog};
border-bottom-left-radius: ${radii[1]}px;
border-bottom-right-radius: ${radii[1]}px;
`

8
src/components/Onboarding/steps/WriteSeed/index.js

@ -9,15 +9,15 @@ import OnboardingFooter from '../../OnboardingFooter'
import WriteSeedNano from './WriteSeedNano'
import WriteSeedBlue from './WriteSeedBlue'
import WriteSeedRestore from './WriteSeedRestore'
import { FixedTopContainer } from '../../helperComponents'
import type { StepProps } from '../..'
export default (props: StepProps) => {
const { nextStep, prevStep, t, onboarding } = props
return (
<Box sticky pt={50}>
<Box grow alignItems="center" justifyContent="center">
<FixedTopContainer>
<Box grow alignItems="center">
{onboarding.flowType === 'restoreDevice' ? (
<WriteSeedRestore />
) : onboarding.isLedgerNano ? (
@ -34,6 +34,6 @@ export default (props: StepProps) => {
nextStep={nextStep}
prevStep={prevStep}
/>
</Box>
</FixedTopContainer>
)
}

3
src/components/base/InputPassword/index.js

@ -14,6 +14,7 @@ import Box from 'components/base/Box'
import Input from 'components/base/Input'
import IconEye from 'icons/Eye'
import IconEyeOff from 'icons/EyeOff'
const InputRight = styled(Box).attrs({
color: 'grey',
@ -99,7 +100,7 @@ class InputPassword extends PureComponent<Props, State> {
onChange={this.handleChange}
renderRight={
<InputRight onClick={this.toggleInputType}>
<IconEye size={16} />
{inputType === 'password' ? <IconEye size={16} /> : <IconEyeOff size={16} />}
</InputRight>
}
/>

38
src/helpers/libcore.js

@ -34,10 +34,9 @@ export function scanAccountsOnDevice(props: Props): Promise<AccountRaw[]> {
const commonParams = {
core,
hwApp,
currencyId,
onAccountScanned,
devicePath,
hwApp,
}
let allAccounts = []
@ -62,22 +61,16 @@ export function scanAccountsOnDevice(props: Props): Promise<AccountRaw[]> {
})
}
export async function getWalletIdentifier({
hwApp,
isSegwit,
function encodeWalletName({
publicKey,
currencyId,
devicePath,
isSegwit,
}: {
hwApp: Object,
isSegwit: boolean,
publicKey: string,
currencyId: string,
devicePath: string,
}): Promise<string> {
const isVerify = false
const deviceIdentifiers = await hwApp.getWalletPublicKey(devicePath, isVerify, isSegwit)
const { publicKey } = deviceIdentifiers
const WALLET_IDENTIFIER = `${publicKey}__${currencyId}${isSegwit ? '_segwit' : ''}`
return WALLET_IDENTIFIER
isSegwit: boolean,
}) {
return `${publicKey}__${currencyId}${isSegwit ? '_segwit' : ''}`
}
async function scanAccountsOnDeviceBySegwit({
@ -85,7 +78,6 @@ async function scanAccountsOnDeviceBySegwit({
hwApp,
currencyId,
onAccountScanned,
devicePath,
isSegwit,
showNewAccount,
}: {
@ -93,15 +85,19 @@ async function scanAccountsOnDeviceBySegwit({
hwApp: Object,
currencyId: string,
onAccountScanned: AccountRaw => void,
devicePath: string,
isSegwit: boolean,
isSegwit: boolean, // FIXME all segwit to change to 'purpose'
showNewAccount: boolean,
}): Promise<AccountRaw[]> {
// compute wallet identifier
const WALLET_IDENTIFIER = await getWalletIdentifier({ hwApp, isSegwit, currencyId, devicePath })
const { coinType } = getCryptoCurrencyById(currencyId)
const { publicKey } = await hwApp.getWalletPublicKey(
`${isSegwit ? '49' : '44'}'/${coinType}'`,
false,
isSegwit,
)
const walletName = encodeWalletName({ publicKey, currencyId, isSegwit })
// retrieve or create the wallet
const wallet = await getOrCreateWallet(core, WALLET_IDENTIFIER, currencyId, isSegwit)
const wallet = await getOrCreateWallet(core, walletName, currencyId, isSegwit)
const accountsCount = await wallet.getAccountCount()
// recursively scan all accounts on device on the given app

16
src/icons/Cart.js

@ -0,0 +1,16 @@
// @flow
import React from 'react'
const path = (
<path
fill="currentColor"
d="M15.333 2.375H4.008l-.243-1.307A.677.677 0 0 0 3.111.5H.333A.343.343 0 0 0 0 .852v.703c0 .194.15.351.333.351h2.23l1.932 10.421A1.923 1.923 0 0 0 4 13.625c0 1.036.796 1.875 1.778 1.875s1.778-.84 1.778-1.875c0-.33-.082-.653-.239-.938h4.033a1.945 1.945 0 0 0-.239.938c0 1.036.796 1.875 1.778 1.875s1.778-.84 1.778-1.875c0-.531-.21-1.01-.547-1.352l.029-.14c.09-.437-.226-.852-.652-.852H5.66L5.4 9.875h8.677c.314 0 .585-.23.652-.554l1.256-6.094c.09-.438-.227-.852-.652-.852zM5.778 14.328c-.368 0-.667-.315-.667-.703 0-.388.3-.703.667-.703.367 0 .666.315.666.703 0 .388-.299.703-.666.703zm7.11 0c-.367 0-.666-.315-.666-.703 0-.388.3-.703.667-.703.368 0 .667.315.667.703 0 .388-.3.703-.667.703zm.652-5.86H5.138l-.87-4.687h10.238L13.54 8.47z"
/>
)
export default ({ size, ...p }: { size: number }) => (
<svg viewBox="0 0 16 16" height={size} width={size} {...p}>
{path}
</svg>
)

16
src/icons/EyeOff.js

@ -0,0 +1,16 @@
// @flow
import React from 'react'
const path = (
<path
fill="currentColor"
d="M2.502 8.393c.335.494.731.99 1.184 1.45 1.267 1.286 2.713 2.046 4.304 2.046a5.462 5.462 0 0 0 3.121-1.03c.257-.187.512-.072.708.172.195.244.256.525 0 .71A6.674 6.674 0 0 1 8 13c-1.972 0-3.698-.907-5.165-2.398a11.477 11.477 0 0 1-1.313-1.606 8.35 8.35 0 0 1-.46-.748.532.532 0 0 1 .007-.51 10.965 10.965 0 0 1 3.112-3.48.603.603 0 0 1 .818.105.538.538 0 0 1-.11.779 9.859 9.859 0 0 0-2.64 2.86c.074.12.158.251.253.391zm10.996-.786c-.335-.494-.731-.99-1.184-1.45-1.267-1.286-2.713-2.046-4.315-2.046-.368 0-.734.04-1.091.119a.585.585 0 0 1-.701-.414.555.555 0 0 1 .435-.668c.446-.1.902-.149 1.358-.148 1.972 0 3.698.907 5.165 2.398.504.512.942 1.059 1.313 1.606.225.33.378.591.46.748a.532.532 0 0 1-.007.51 10.821 10.821 0 0 1-1.328 1.868.604.604 0 0 1-.822.067.538.538 0 0 1-.07-.782c.39-.442.738-.916 1.04-1.416a9.37 9.37 0 0 0-.253-.392zm-4.426 2.29a2.225 2.225 0 0 1-3.38-1.306 2.221 2.221 0 0 1 .3-1.775L1.17 1.996a.583.583 0 1 1 .825-.825l12.833 12.833a.583.583 0 1 1-.825.825L9.072 9.896zm-2.26-2.26a1.11 1.11 0 0 0 1.44 1.44l-1.44-1.44z"
/>
)
export default ({ size, ...p }: { size: number }) => (
<svg viewBox="0 0 16 16" height={size} width={size} {...p}>
{path}
</svg>
)

16
src/icons/Truck.js

@ -0,0 +1,16 @@
// @flow
import React from 'react'
const path = (
<path
fill="currentColor"
d="M14.8 1.5h-8c-.663 0-1.2.546-1.2 1.219v1.219H4.497a1.19 1.19 0 0 0-.848.356L1.15 6.831a1.228 1.228 0 0 0-.351.861v3.152H.5c-.166 0-.3.136-.3.304v.61c0 .168.134.305.3.305h1.1C1.6 13.409 2.675 14.5 4 14.5s2.4-1.091 2.4-2.438h3.2c0 1.347 1.075 2.438 2.4 2.438s2.4-1.091 2.4-2.438h.4c.663 0 1.2-.545 1.2-1.218V2.719A1.21 1.21 0 0 0 14.8 1.5zM4 13.281a1.21 1.21 0 0 1-1.2-1.219A1.21 1.21 0 0 1 4 10.845c.662 0 1.2.546 1.2 1.219A1.21 1.21 0 0 1 4 13.28zm1.6-3.035A2.37 2.37 0 0 0 4 9.625c-.835 0-1.57.433-2 1.09V7.692l2.497-2.536H5.6v5.09zM12 13.28a1.21 1.21 0 0 1-1.2-1.219 1.21 1.21 0 0 1 1.2-1.218c.662 0 1.2.546 1.2 1.219A1.21 1.21 0 0 1 12 13.28zm2.8-2.437h-.721A2.393 2.393 0 0 0 12 9.625c-.888 0-1.664.49-2.079 1.219H6.8V2.719h8v8.125zM2.8 8l2-2.031V8h-2z"
/>
)
export default ({ size, ...p }: { size: number }) => (
<svg viewBox="0 0 16 16" height={size} width={size} {...p}>
{path}
</svg>
)

9
src/reducers/onboarding.js

@ -57,6 +57,15 @@ const state: OnboardingState = {
showBreadcrumb: false,
},
},
{
name: 'noDevice',
external: true,
options: {
showFooter: false,
showBackground: true,
showBreadcrumb: false,
},
},
{
name: 'selectDevice',
label: 'Select Device',

16
static/i18n/en/onboarding.yml

@ -15,6 +15,14 @@ init:
noDevice:
title: Do not have a Ledger device yet?
desc: Please replace it with the final wording once it’s done.
noDevice:
title: Do not have a Ledger device yet?
buyNew:
title: Buy a Ledger device
trackOrder:
title: Track your order
learnMore:
title: Learn about Ledger Live
selectDevice:
title: To get started, select your device
ledgerNanoCard:
@ -44,14 +52,14 @@ writeSeed:
title: Save your recovery phrase
desc: Your recovery phrase is formed by 24 words. They will be displayed only once.
step1: Press the right button to select the length of your recovery phrase. Press both buttons to confirm.
step2: Select the first letters of Word \#1 by pressing the right or left button. Press both buttons to confirm each letter.
step3: Select Word \#1 from the suggested words. Press both buttons to continue.
step2: 'Select the first letters of Word #1 by pressing the right or left button. Press both buttons to confirm each letter.'
step3: 'Select Word #1 from the suggested words. Press both buttons to continue.'
step4: Repeat the process until the last word.
nano:
title: Save your recovery phrase
desc: Your recovery phrase is formed by 24 words. They will be displayed only once.
step1: Copy the first word (Word \#1) in position 1 on the blank Recovery sheet.
step2: Press the right button to display Word \#2 and repeat the process until all 24 words are copied on the Recovery sheet.
step1: 'Copy the first word (Word #1) in position 1 on the blank Recovery sheet.'
step2: 'Press the right button to display Word #2 and repeat the process until all 24 words are copied on the Recovery sheet.'
step3: Confirm your recovery phrase press both buttons to validate each word displayed on the screen.
blue:
title: Save your recovery phrase

Loading…
Cancel
Save