diff --git a/src/action/index-mobile.js b/src/action/index-mobile.js index cfc5f1678..9b9cc03fe 100644 --- a/src/action/index-mobile.js +++ b/src/action/index-mobile.js @@ -116,7 +116,6 @@ observe(store, 'lndReady', () => { // STUB DURING DEVELOPMENT sinon.stub(wallet, 'update'); -sinon.stub(wallet, 'checkSeed'); sinon.stub(wallet, 'getExchangeRate'); sinon.stub(transaction, 'update'); sinon.stub(invoice, 'generateUri'); @@ -199,6 +198,32 @@ store.selectedTransaction = (store.computedTransactions || []).find( tx => tx.type === 'bitcoin' ); store.selectedChannel = store.computedChannels && store.computedChannels[0]; +store.seedMnemonic = [ + 'empower', + 'neglect', + 'experience', + 'elevator', + 'entropy', + 'future', + 'trust', + 'swift', + 'pluck', + 'easy', + 'kite', + 'measure', + 'engage', + 'settle', + 'dog', + 'manager', + 'tool', + 'fan', + 'neglect', + 'conduct', + 'blouse', + 'stone', + 'quit', + 'cashew', +]; store.logs = [ '[14:00:24.995] [info] Using lnd in path lnd', 'Checking for update', diff --git a/src/action/wallet.js b/src/action/wallet.js index af2617957..151df4532 100644 --- a/src/action/wallet.js +++ b/src/action/wallet.js @@ -274,13 +274,42 @@ class WalletAction { } /** - * Initialize the seed flow by navigating to the proper next view. + * Initialize the seed flow by navigating to the proper next view and + * resetting the current seed index. * @return {undefined} */ initSeed() { + this._store.wallet.seedIndex = 0; this._nav.goSeedIntro ? this._nav.goSeedIntro() : this._nav.goSeed(); } + /** + * Initialize the next seed view by setting a new seedIndex or, if all seed + * words have been displayed, navigating to the mobile seed verify view. + * view. + * @return {undefined} + */ + initNextSeedPage() { + if (this._store.wallet.seedIndex < 16) { + this._store.wallet.seedIndex += 8; + } else { + this.initSeedVerify(); + } + } + + /** + * Initialize the previous seed view by setting a new seedIndex or, if on the + * first seed page, navigating to the select seed view. + * @return {undefined} + */ + initPrevSeedPage() { + if (this._store.wallet.seedIndex >= 8) { + this._store.wallet.seedIndex -= 8; + } else { + this._nav.goSelectSeed(); + } + } + /** * Initialize the restore wallet view by resetting input values and then * navigating to the view. diff --git a/src/component/header.js b/src/component/header.js index 9293cfe95..b2513aa76 100644 --- a/src/component/header.js +++ b/src/component/header.js @@ -76,6 +76,7 @@ const titleStyles = StyleSheet.create({ alignItems: 'center', }, title: { + textAlign: 'center', fontFamily: 'OpenSans Regular', fontSize: 15, letterSpacing: 2, @@ -86,7 +87,7 @@ const titleStyles = StyleSheet.create({ }, }); -export const Title = ({ title = '', style, children }) => ( +export const Title = ({ title = '', style, keepCase, children }) => ( {children} ( style, ]} > - {title.toUpperCase()} + {keepCase ? title : title.toUpperCase()} ); @@ -104,6 +105,7 @@ export const Title = ({ title = '', style, children }) => ( Title.propTypes = { title: PropTypes.string, style: View.propTypes.style, + keepCase: PropTypes.bool, children: PropTypes.node, }; diff --git a/src/component/input.js b/src/component/input.js index 8c336e2f9..dfeb26cf3 100644 --- a/src/component/input.js +++ b/src/component/input.js @@ -1,17 +1,14 @@ import React, { Component } from 'react'; -import { - TextInput as RNTextInput, - Text as RNText, - StyleSheet, -} from 'react-native'; +import { TextInput as RNTextInput, Text as RNText } from 'react-native'; import PropTypes from 'prop-types'; -import { color, font } from './style'; +import { createStyles, maxWidth } from '../component/media-query'; +import { color, font, breakWidth } from './style'; // // Base Text Input // -const baseStyles = StyleSheet.create({ +const baseStyles = { input: { fontFamily: 'OpenSans Regular', fontSize: font.sizeM, @@ -19,21 +16,43 @@ const baseStyles = StyleSheet.create({ height: font.lineHeightM + 2 * 12, color: color.blackText, }, -}); +}; + +const styles = createStyles( + baseStyles, -export const TextInput = ({ style, ...props }) => ( - + maxWidth(breakWidth, { + input: { + lineHeight: undefined, + }, + }) ); +export class TextInput extends Component { + componentDidUpdate(prevProps) { + const { autoFocus } = this.props; + prevProps.autoFocus === false && autoFocus && this._input.focus(); + } + + render() { + const { style, ...props } = this.props; + return ( + (this._input = component)} + {...props} + /> + ); + } +} + TextInput.propTypes = { style: RNText.propTypes.style, + autoFocus: PropTypes.bool, }; // diff --git a/src/store.js b/src/store.js index 4a838840f..498beebbd 100644 --- a/src/store.js +++ b/src/store.js @@ -47,6 +47,7 @@ export class Store { newPassword: '', passwordVerify: '', seedVerify: ['', '', ''], + seedIndex: 0, restoring: false, restoreIndex: 0, restoreSeed: Array(24).fill(''), diff --git a/src/view/main-mobile.js b/src/view/main-mobile.js index da8c213e3..63da0b3fc 100644 --- a/src/view/main-mobile.js +++ b/src/view/main-mobile.js @@ -10,6 +10,8 @@ import WelcomeView from './welcome'; import LoaderView from './loader'; import SelectSeedView from './select-seed'; import SeedIntroView from './seed-intro-mobile'; +import SeedView from './seed-mobile'; +import SeedVerifyView from './seed-verify-mobile'; import SetPinView from './set-pin-mobile'; import SetPinConfirmView from './set-pin-confirm-mobile'; import SeedSuccessView from './seed-success-mobile'; @@ -59,12 +61,16 @@ const Welcome = () => ; const Loader = () => ; -const SelectSeed = () => ( - -); +const SelectSeed = () => ; const SeedIntro = () => ; +const Seed = () => ; + +const SeedVerify = () => ( + +); + const SetPassword = () => ; const SetPasswordConfirm = () => ( @@ -167,6 +173,8 @@ const MainStack = createStackNavigator( Loader, SelectSeed, SeedIntro, + Seed, + SeedVerify, SetPassword, SetPasswordConfirm, SeedSuccess, diff --git a/src/view/seed-mobile.js b/src/view/seed-mobile.js new file mode 100644 index 000000000..170371835 --- /dev/null +++ b/src/view/seed-mobile.js @@ -0,0 +1,140 @@ +import React from 'react'; +import { View, StyleSheet } from 'react-native'; +import { observer } from 'mobx-react'; +import PropTypes from 'prop-types'; +import MainContent from '../component/main-content'; +import { Text } from '../component/text'; +import { Background } from '../component/background'; +import { Header, Title } from '../component/header'; +import { Button, BackButton, GlasButton } from '../component/button'; +import LightningBoltIcon from '../asset/icon/lightning-bolt'; +import { color, font } from '../component/style'; + +// +// Seed (Mobile) View +// + +const styles = StyleSheet.create({ + header: { + height: 150, + }, + title: { + width: 200, + }, + background: { + flex: 1, + justifyContent: 'space-between', + backgroundColor: color.blackDark, + }, + seed: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + pagination: { + paddingBottom: 10, + }, +}); + +const SeedView = ({ store, wallet }) => ( + +
+ wallet.initPrevSeedPage()} /> + + <LightningBoltIcon height={35} width={29} /> + +
+ + + + + {`${store.wallet.seedIndex + 8} of 24`} + + + wallet.initNextSeedPage()}>Next + +
+); + +SeedView.propTypes = { + store: PropTypes.object.isRequired, + wallet: PropTypes.object.isRequired, +}; + +// +// Word List +// + +const listStyles = StyleSheet.create({ + wrapper: { + justifyContent: 'center', + margin: 20, + }, + words: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'center', + maxWidth: 700, + }, +}); + +const WordList = ({ seedMnemonic, startingIndex }) => ( + + + {seedMnemonic.map((word, i) => ( + + ))} + + +); + +WordList.propTypes = { + seedMnemonic: PropTypes.array.isRequired, + startingIndex: PropTypes.number, +}; + +// +// Word +// + +const wordStyles = StyleSheet.create({ + wrapper: { + justifyContent: 'center', + height: 45, + width: 115, + margin: 10, + borderWidth: 1, + borderColor: color.seedBorder, + backgroundColor: color.seedBackground, + }, + word: { + fontSize: font.sizeS * 1.2, + paddingLeft: 10, + }, +}); + +const Word = ({ word, index }) => ( + + + {index}. {word} + + +); + +Word.propTypes = { + word: PropTypes.string.isRequired, + index: PropTypes.number.isRequired, +}; + +export default observer(SeedView); diff --git a/src/view/seed-verify-mobile.js b/src/view/seed-verify-mobile.js new file mode 100644 index 000000000..9100ca523 --- /dev/null +++ b/src/view/seed-verify-mobile.js @@ -0,0 +1,81 @@ +import React, { Component } from 'react'; +import { View, StyleSheet } from 'react-native'; +import { observer } from 'mobx-react'; +import PropTypes from 'prop-types'; +import SeedEntry from '../component/seed-entry'; +import { Button, BackButton, GlasButton } from '../component/button'; +import { FormSubText } from '../component/form'; +import Background from '../component/background'; +import MainContent from '../component/main-content'; +import { CopyOnboardText } from '../component/text'; +import { Header } from '../component/header'; +import Card from '../component/card'; + +// +// Seed Verify View +// + +const styles = StyleSheet.create({ + content: { + justifyContent: 'flex-end', + }, + title: { + textAlign: 'center', + marginBottom: 20, + }, +}); + +class SeedVerifyView extends Component { + constructor(props) { + super(props); + this.state = { + focusedInput: 0, + }; + } + + render() { + const { store, nav, wallet } = this.props; + return ( + +
+ nav.goSeed()} /> +
+ + + + {"Let's double check"} + + + + {store.seedVerifyCopy} + {store.seedVerifyIndexes.map((seedIndex, i) => ( + wallet.setSeedVerify({ word, index: i })} + key={i} + autoFocus={i === this.state.focusedInput} + onSubmitEditing={() => + i === 2 + ? wallet.checkSeed() + : this.setState({ focusedInput: i + 1 }) + } + onClick={() => this.setState({ focusedInput: i })} + /> + ))} + + wallet.checkSeed()}>Next + +
+ ); + } +} + +SeedVerifyView.propTypes = { + store: PropTypes.object.isRequired, + nav: PropTypes.object.isRequired, + wallet: PropTypes.object.isRequired, +}; + +export default observer(SeedVerifyView); diff --git a/stories/screen-story.js b/stories/screen-story.js index 823da873a..348f060d2 100644 --- a/stories/screen-story.js +++ b/stories/screen-story.js @@ -61,7 +61,9 @@ import SeedIntro from '../src/view/seed-intro-mobile'; import SeedSuccess from '../src/view/seed-success'; import SeedSuccessMobile from '../src/view/seed-success-mobile'; import Seed from '../src/view/seed'; +import SeedMobile from '../src/view/seed-mobile'; import SeedVerify from '../src/view/seed-verify'; +import SeedVerifyMobile from '../src/view/seed-verify-mobile'; import SetPassword from '../src/view/set-password'; import SetPinMobile from '../src/view/set-pin-mobile'; import SetPasswordConfirm from '../src/view/set-password-confirm'; @@ -116,14 +118,16 @@ sinon.stub(auth, '_generateWalletPassword'); storiesOf('Screens', module) .add('Welcome', () => ) .add('Loader - First Time', () => ) - .add('Select Seed', () => ( - - )) + .add('Select Seed', () => ) .add('Seed Intro (Mobile)', () => ) .add('Seed', () => ) + .add('Seed (Mobile)', () => ) .add('Seed Verify', () => ( )) + .add('Seed Verify (Mobile)', () => ( + + )) .add('Restore Wallet: Seed', () => ( )) diff --git a/test/unit/action/wallet.spec.js b/test/unit/action/wallet.spec.js index 2cda55e0b..44113abd5 100644 --- a/test/unit/action/wallet.spec.js +++ b/test/unit/action/wallet.spec.js @@ -310,8 +310,10 @@ describe('Action Wallet Unit Tests', () => { }); describe('initSeed()', () => { - it('should navigate to seed view', () => { + it('should clear attributes and navigate to view', () => { + store.wallet.seedIndex = 42; wallet.initSeed(); + expect(store.wallet.seedIndex, 'to equal', 0); expect(nav.goSeed, 'was called once'); }); @@ -323,6 +325,38 @@ describe('Action Wallet Unit Tests', () => { }); }); + describe('initPrevSeedPage()', () => { + it('should navigate to select seed if seedIndex < 8', () => { + store.wallet.seedIndex = 7; + wallet.initPrevSeedPage(); + expect(nav.goSelectSeed, 'was called once'); + expect(store.wallet.seedIndex, 'to equal', 7); + }); + + it('should decrement seedIndex if greater than 7', async () => { + store.wallet.seedIndex = 8; + wallet.initPrevSeedPage(); + expect(nav.goSelectSeed, 'was not called'); + expect(store.wallet.seedIndex, 'to equal', 0); + }); + }); + + describe('initNextSeedPage()', () => { + it('should init seed verify if seedIndex > 16', () => { + store.wallet.seedIndex = 16; + wallet.initNextSeedPage(); + expect(nav.goSeedVerify, 'was called'); + expect(store.wallet.seedIndex, 'to equal', 16); + }); + + it('should increment seedIndex if less than 16', async () => { + store.wallet.seedIndex = 8; + wallet.initNextSeedPage(); + expect(nav.goSeedVerify, 'was not called'); + expect(store.wallet.seedIndex, 'to equal', 16); + }); + }); + describe('initRestoreWallet()', () => { it('should clear attributes and navigate to view', () => { store.wallet.restoreIndex = 42;