on master help sign in
Merge pull request #174 from urbit/mino-login-functional

Functional-ify Login views

commitdf1910f30b4a7f4c150a314c0df5e0d234cbb418 authorFang <Fang-@users.noreply.github.com> dateTue 25 Jun 2019, 19:15 (7 years ago) parent563f97b8f40118
10 files changed+692−1101
src/components/Inputs.js+29−5 src/views/Login.js+63−80 src/views/Login/Keystore.js+87−116 src/views/Login/Ledger.js+142−186 src/views/Login/Mnemonic.js+74−157 src/views/Login/PrivateKey.js+40−69 src/views/Login/Shards.js+67−192 src/views/Login/Ticket.js+112−170 src/views/Login/Trezor.js+74−115 src/views/ViewPoint.js+4−11
src/components/Inputs.js+29−5
@@ -2,11 +2,14 @@ import React from 'react';
22import { Input } from 'indigo-react';
33import * as bip39 from 'bip39';
44
5+import InputSigil from 'components/InputSigil';
6+
57import { DEFAULT_HD_PATH } from 'lib/wallet';
68import {
79 validateNotEmpty,
810 validateMnemonic,
911 validatePoint,
12+ validateTicket,
1013} from 'lib/validators';
1114import { prependSig } from 'lib/transformers';
1215
@@ -82,13 +85,34 @@ export function PointInput(props) {
8285 validators={[validatePoint, validateNotEmpty]}
8386 transformers={[prependSig]}
8487 mono
88+ accessory={
89+ <InputSigil
90+ patp={props.initialValue || '~'}
91+ size={68}
92+ margin={8}
93+ pass={props.pass}
94+ onPass={props.setPass}
95+ focused={props.focused}
96+ onFocus={props.setFocused}
97+ error={props.error}
98+ onError={props.setError}
99+ />
100+ }
85101 {...props}
86102 />
87103 );
88104}
89105
90−// const TicketInput = advancedInput({
91−// WrappedComponent: Input,
92−// validators: [validateTicket, validateNotEmpty],
93−// transformers: [prependSig],
94−// });
106+//TODO needs to be fancier, displaying sig and dashes instead of •ing all
107+export function TicketInput(props) {
108+ return (
109+ <Input
110+ type="password"
111+ placeholder="~master-ticket"
112+ validators={[validateTicket, validateNotEmpty]}
113+ transformers={[prependSig]}
114+ mono
115+ {...props}
116+ />
117+ );
118+}
src/views/Login.js+63−80
@@ -1,22 +1,23 @@
11import { Nothing, Just } from 'folktale/maybe';
2−import React from 'react';
2+import React, { useState } from 'react';
33import * as azimuth from 'azimuth-js';
44
5−import { H1, Row, Col } from '../components/old/Base';
6−import Tabs from '../components/Tabs';
5+import { H1 } from 'indigo-react';
6+import View from 'components/View';
7+import Tabs from 'components/Tabs';
78
89import Ticket from './Login/Ticket';
910import Mnemonic from './Login/Mnemonic';
1011import Advanced from './Login/Advanced';
1112
12−import * as need from '../lib/need';
13−import { compose } from '../lib/lib';
14−import { ROUTE_NAMES } from '../lib/routeNames';
13+import useLifecycle from 'lib/useLifecycle';
14+import * as need from 'lib/need';
15+import { ROUTE_NAMES } from 'lib/routeNames';
1516
16−import { withHistory } from '../store/history';
17−import { withNetwork } from '../store/network';
18−import { withWallet } from '../store/wallet';
19−import { withPointCursor } from '../store/pointCursor';
17+import { useHistory } from 'store/history';
18+import { useNetwork } from 'store/network';
19+import { useWallet } from 'store/wallet';
20+import { usePointCursor } from 'store/pointCursor';
2021
2122const TABS = {
2223 TICKET: Symbol('TICKET'),
@@ -24,46 +25,51 @@ const TABS = {
2425 ADVANCED: Symbol('ADVANCED'),
2526};
2627
27−class Login extends React.Component {
28− constructor(props) {
29− super(props);
28+const tabViews = {
29+ [TABS.TICKET]: Ticket,
30+ [TABS.MNEMONIC]: Mnemonic,
31+ [TABS.ADVANCED]: Advanced,
32+};
3033
31− this.state = {
32− currentTab: TABS.TICKET,
33− };
34+const tabOptions = [
35+ { title: 'Ticket', value: TABS.TICKET },
36+ { title: 'Mnemonic', value: TABS.MNEMONIC },
37+ { title: 'Advanced', value: TABS.ADVANCED },
38+];
3439
35− this.handleTabChange = this.handleTabChange.bind(this);
36− this.continue = this.continue.bind(this);
37− }
40+export default function Login() {
41+ // globals
42+ const history = useHistory();
43+ const { contracts } = useNetwork();
44+ const { wallet, setUrbitWallet } = useWallet();
45+ const { pointCursor, setPointCursor } = usePointCursor();
3846
39− componentDidMount() {
40− // we expect wallet and pointCursor to not be set yet
41− this.props.setUrbitWallet(Nothing());
42− this.props.setWallet(Nothing());
43− this.props.setPointCursor(Nothing());
44− }
47+ // inputs
48+ const [currentTab, setCurrentTab] = useState(TABS.TICKET);
4549
46− handleTabChange(currentTab) {
47− this.setState({ currentTab });
48− }
50+ // on-mount
51+ useLifecycle(() => {
52+ setUrbitWallet(Nothing());
53+ setPointCursor(Nothing());
54+ });
4955
50− async continue() {
51− const wallet = need.wallet(this.props);
52− const contracts = need.contracts(this.props);
56+ const doContinue = async () => {
57+ const _wallet = need.wallet(wallet);
58+ const _contracts = need.contracts(contracts);
5359
5460 // if no point cursor set by login logic, try to deduce it
5561 let deduced = Nothing();
56− if (Nothing.hasInstance(this.props.pointCursor)) {
62+ if (Nothing.hasInstance(pointCursor)) {
5763 const owned = await azimuth.azimuth.getOwnedPoints(
58− contracts,
59− wallet.address
64+ _contracts,
65+ _wallet.address
6066 );
6167 if (owned.length === 1) {
6268 deduced = Just(owned[0]);
6369 } else if (owned.length === 0) {
6470 const canOwn = await azimuth.azimuth.getTransferringFor(
65− contracts,
66− wallet.address
71+ _contracts,
72+ _wallet.address
6773 );
6874 if (canOwn.length === 1) {
6975 deduced = Just(canOwn[0]);
@@ -74,50 +80,27 @@ class Login extends React.Component {
7480 // if we have a deduced point or one in the global context,
7581 // navigate to that specific point, otherwise navigate to list of points
7682 if (Just.hasInstance(deduced)) {
77− this.props.setPointCursor(deduced);
78− this.props.history.popAndPush(ROUTE_NAMES.POINT);
79− } else if (Just.hasInstance(this.props.pointCursor)) {
80− this.props.history.popAndPush(ROUTE_NAMES.POINT);
83+ setPointCursor(deduced);
84+ history.popAndPush(ROUTE_NAMES.POINT);
85+ } else if (Just.hasInstance(pointCursor)) {
86+ history.popAndPush(ROUTE_NAMES.POINT);
8187 } else {
82− this.props.history.popAndPush(ROUTE_NAMES.POINTS);
88+ history.popAndPush(ROUTE_NAMES.POINTS);
8389 }
84− }
85−
86− render() {
87− const tabOptions = [
88− { title: 'Ticket', value: TABS.TICKET },
89− { title: 'Mnemonic', value: TABS.MNEMONIC },
90− { title: 'Advanced', value: TABS.ADVANCED },
91− ];
92−
93− const tabViews = {
94− [TABS.TICKET]: Ticket,
95− [TABS.MNEMONIC]: Mnemonic,
96− [TABS.ADVANCED]: Advanced,
97− };
98−
99− return (
100− <Row>
101− <Col>
102− <H1>{'Login'}</H1>
103−
104− <Tabs
105− tabViews={tabViews}
106− tabOptions={tabOptions}
107− currentTab={this.state.currentTab}
108− onTabChange={this.handleTabChange}
109− //
110− loginCompleted={this.continue}
111− />
112− </Col>
113− </Row>
114− );
115− }
90+ };
91+
92+ return (
93+ <View>
94+ <H1>Login</H1>
95+
96+ <Tabs
97+ tabViews={tabViews}
98+ tabOptions={tabOptions}
99+ currentTab={currentTab}
100+ onTabChange={setCurrentTab}
101+ //
102+ loginCompleted={doContinue}
103+ />
104+ </View>
105+ );
116106}
117−
118−export default compose(
119− withHistory,
120− withNetwork,
121− withWallet,
122− withPointCursor
123−)(Login);
src/views/Login/Keystore.js+87−116
@@ -1,66 +1,56 @@
11import { Just, Nothing } from 'folktale/maybe';
22import { Ok, Error } from 'folktale/result';
3−import React from 'react';
3+import React, { useState } from 'react';
44import * as keythereum from 'keythereum';
5−import { Button, UploadButton } from '../../components/old/Base';
6−import { Input, InnerLabel, InputCaption } from '../../components/old/Base';
7−import { Row, Col, H1, H3, Warning } from '../../components/old/Base';
8−
9−import * as need from '../../lib/need';
10−import { EthereumWallet } from '../../lib/wallet';
11−import { compose } from '../../lib/lib';
12−import { withWallet } from '../../store/wallet';
13−
14−class Keystore extends React.Component {
15− constructor(props) {
16− super(props);
17−
18− this.state = {
19− keystore: Nothing(), // Maybe<Result<String, String>>
20− password: '',
21− decryptionProblem: false,
22− };
235
24− this.handleKeystoreUpload = this.handleKeystoreUpload.bind(this);
25− this.handlePasswordInput = this.handlePasswordInput.bind(this);
26− this.constructWallet = this.constructWallet.bind(this);
27− }
6+import View from 'components/View';
7+import { PassphraseInput } from 'components/Inputs';
8+import { ForwardButton } from 'components/Buttons';
9+import { H3 } from 'indigo-react';
10+import { InputCaption, UploadButton, Warning } from 'components/old/Base';
11+
12+import * as need from 'lib/need';
13+import { EthereumWallet } from 'lib/wallet';
14+import { useWallet } from 'store/wallet';
2815
29− handlePasswordInput(password) {
30− this.setState({ password });
31− }
16+export default function Keystore({ loginCompleted }) {
17+ // globals
18+ const { wallet, setWallet } = useWallet();
3219
33− constructWallet() {
34− const { setWallet } = this.props;
35− const { password } = this.state;
20+ // inputs
21+ // keystore: Maybe<Result<String, String>>
22+ const [keystore, setKeystore] = useState(Nothing());
23+ const [password, setPassword] = useState('');
24+ const [decryptionProblem, setDecryptionProblem] = useState(false);
3625
26+ const constructWallet = () => {
3727 try {
38− const text = need.keystore(this.state);
28+ const text = need.keystore(keystore);
3929
4030 const json = JSON.parse(text);
4131 const privateKey = keythereum.recover(password, json);
4232
43− const wallet = new EthereumWallet(privateKey);
44− this.setState({ decryptionProblem: false });
45− setWallet(Just(wallet));
33+ const newWallet = new EthereumWallet(privateKey);
34+ setDecryptionProblem(false);
35+ setWallet(Just(newWallet));
4636 } catch (err) {
47− this.setState({ decryptionProblem: true });
37+ setDecryptionProblem(true);
4838 setWallet(Nothing());
4939 }
50− }
40+ };
5141
52− handleKeystoreUpload = event => {
42+ const handleKeystoreUpload = event => {
5343 const file = event.files.item(0);
5444 const reader = new FileReader();
5545
5646 reader.onload = e => {
5747 const keystore = e.target.result;
58− this.setState({ keystore: Just(Ok(keystore)) });
48+ setKeystore(Just(Ok(keystore)));
5949 };
6050
6151 const failure = _ => {
6252 const message = 'There was a problem uploading your Keystore file';
63− this.setState({ keystore: Just(Error(message)) });
53+ setKeystore(Just(Error(message)));
6454 };
6555
6656 reader.onerror = failure;
@@ -69,83 +59,64 @@ class Keystore extends React.Component {
6959 reader.readAsText(file);
7060 };
7161
72− render() {
73− const { wallet } = this.props;
74− const { keystore, password, decryptionProblem } = this.state;
75−
76− const uploadButtonClass = keystore.matchWith({
77− Nothing: _ => 'bg-blue white',
78− Just: ks =>
79− ks.value.matchWith({
80− Ok: _ => 'bg-green white',
81− Error: _ => 'bg-yellow black',
82− }),
83− });
84−
85− const decryptMessage =
86− decryptionProblem === false ? (
87− <div />
88− ) : (
89− <Warning className={'mt-8'}>
90− <H3 style={{ marginTop: 0, paddingTop: 0 }}>
91− {"Couldn't decrypt wallet."}
92− </H3>
93− {'You may have entered an incorrect password.'}
94− </Warning>
95− );
96−
97− return (
98− <Row>
99− <Col className={'measure-md'}>
100− <H1 className={'mb-4'}>{'Upload Your Keystore File'}</H1>
101− <InputCaption>
102− {`Please upload your Ethereum keystore file. If your keystore
103− file is encrypted with a password, you'll also need to enter
104− that below.`}
105− </InputCaption>
106−
107− <UploadButton
108− className={`btn ${uploadButtonClass} mt-10`}
109− onChange={this.handleKeystoreUpload}>
110− <div className={'flex-center-all fs-4 h-11 pointer'}>
111− {'Upload Keystore file'}
112− </div>
113− </UploadButton>
114−
115− <Input
116− className="pt-8 mt-8"
117− prop-size="md"
118− prop-format="innerLabel"
119− type="password"
120− name="password"
121− onChange={this.handlePasswordInput}
122− value={password}
123− autocomplete="off"
124− autoFocus>
125− <InnerLabel>{'Password'}</InnerLabel>
126− </Input>
127−
128− <Button
129− className={` mt-10`}
130− prop-size={'wide lg'}
131− disabled={Nothing.hasInstance(keystore)}
132− onClick={this.constructWallet}>
133− {'Decrypt'}
134− </Button>
135−
136− {decryptMessage}
137−
138− <Button
139− className={'mt-10'}
140− prop-size={'wide lg'}
141− disabled={Nothing.hasInstance(wallet)}
142− onClick={this.props.loginCompleted}>
143− {'Continue →'}
144− </Button>
145− </Col>
146− </Row>
62+ const uploadButtonClass = keystore.matchWith({
63+ Nothing: _ => 'bg-blue white',
64+ Just: ks =>
65+ ks.value.matchWith({
66+ Ok: _ => 'bg-green white',
67+ Error: _ => 'bg-yellow black',
68+ }),
69+ });
70+
71+ const decryptMessage =
72+ decryptionProblem === false ? (
73+ <div />
74+ ) : (
75+ <Warning className="mt-8">
76+ <H3 style={{ marginTop: 0, paddingTop: 0 }}>
77+ Couldn't decrypt wallet.
78+ </H3>
79+ You may have entered an incorrect password.
80+ </Warning>
14781 );
148− }
149−}
15082
151−export default compose(withWallet)(Keystore);
83+ return (
84+ <View>
85+ <InputCaption>
86+ Please upload your Ethereum keystore file. If your keystore file is
87+ encrypted with a password, you'll also need to enter that below.
88+ </InputCaption>
89+
90+ <UploadButton
91+ className={`${uploadButtonClass} mt3`}
92+ onChange={handleKeystoreUpload}>
93+ <div className="flex-center-all fs-4 h-11 pointer">
94+ Upload Keystore file
95+ </div>
96+ </UploadButton>
97+
98+ <PassphraseInput
99+ name="password"
100+ label="Keystore password"
101+ initialValue={password}
102+ onValue={setPassword}
103+ autoFocus></PassphraseInput>
104+
105+ <ForwardButton
106+ className="mt3"
107+ disabled={Nothing.hasInstance(keystore)}
108+ onClick={constructWallet}>
109+ Decrypt
110+ </ForwardButton>
111+
112+ {decryptMessage}
113+
114+ <ForwardButton
115+ className="mt3"
116+ disabled={Nothing.hasInstance(wallet)}
117+ onClick={loginCompleted}>
118+ Continue
119+ </ForwardButton>
120+ </View>
121+ );
122+}
src/views/Login/Ledger.js+142−186
@@ -1,69 +1,56 @@
11import * as bip32 from 'bip32';
2−import React from 'react';
2+import React, { useState } from 'react';
3+import { times } from 'lodash';
34import Maybe from 'folktale/maybe';
4−import { Button } from '../../components/old/Base';
5−import { Row, Col, H1, P, H2 } from '../../components/old/Base';
6−import {
7− Input,
8− InnerLabel,
9− InnerLabelDropdown,
10−} from '../../components/old/Base';
115import Transport from '@ledgerhq/hw-transport-u2f';
126import Eth from '@ledgerhq/hw-app-eth';
137import * as secp256k1 from 'secp256k1';
148
15−import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from '../../lib/ledger';
16−import { compose } from '../../lib/lib';
17−import { withWallet } from '../../store/wallet';
9+import View from 'components/View';
10+import { ForwardButton } from 'components/Buttons';
11+import { H1, H2, P, Input } from 'indigo-react';
12+import { InnerLabel, InnerLabelDropdown } from 'components/old/Base';
13+
14+import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from 'lib/ledger';
15+import { useWallet } from 'store/wallet';
16+
17+const pathOptions = [
18+ { title: 'Ledger Live', value: LEDGER_LIVE_PATH },
19+ { title: 'Ledger Legacy', value: LEDGER_LEGACY_PATH },
20+ { title: 'Custom path', value: 'custom' },
21+];
1822
1923const chopHdPrefix = str => (str.slice(0, 2) === 'm/' ? str.slice(2) : str);
2024
2125const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
2226
23−class Ledger extends React.Component {
24− constructor(props) {
25− super(props);
26−
27− this.state = {
28− basePath: LEDGER_LIVE_PATH,
29− hdpath: LEDGER_LIVE_PATH.replace(/x/g, 0),
30− account: 0,
31− };
32−
33− this.pollDevice = this.pollDevice.bind(this);
34− this.handleHdPathInput = this.handleHdPathInput.bind(this);
35− this.handlePathSelection = this.handlePathSelection.bind(this);
36− this.handleAccountSelection = this.handleAccountSelection.bind(this);
37− this.updateHdPath = this.updateHdPath.bind(this);
38− }
39−
40− handlePathSelection(basePath) {
41− this.updateHdPath(basePath, this.state.account);
42− }
43−
44− handleAccountSelection(account) {
45− this.updateHdPath(this.state.basePath, account);
46− }
47−
48− updateHdPath(basePath, account) {
49− let hdpath = this.state.hdpath;
27+export default function Ledger({ loginCompleted }) {
28+ const { wallet, setWallet, setWalletHdPath } = useWallet();
29+
30+ const [basePath, setBasePath] = useState(LEDGER_LIVE_PATH);
31+ const [account, setAccount] = useState(0);
32+ const [hdPath, setHdPath] = useState(LEDGER_LIVE_PATH.replace(/x/g, 0));
33+
34+ const updateHdPath = (basePath, account) => {
5035 if (basePath !== 'custom') {
51− hdpath = basePath.replace(/x/g, account);
36+ setHdPath(basePath.replace(/x/g, account));
5237 }
53− this.setState({ basePath, account, hdpath });
54− }
38+ };
5539
56− handleHdPathInput(hdpath) {
57− this.setState({ hdpath });
58− }
40+ const handlePathSelection = basePath => {
41+ setBasePath(basePath);
42+ updateHdPath(basePath, account);
43+ };
5944
60− async pollDevice() {
61− const { setWallet, setWalletHdPath } = this.props;
62− const { hdpath } = this.state;
45+ const handleAccountSelection = account => {
46+ setAccount(account);
47+ updateHdPath(basePath, account);
48+ };
6349
50+ const pollDevice = async () => {
6451 const transport = await Transport.create();
6552 const eth = new Eth(transport);
66− const path = chopHdPrefix(hdpath);
53+ const path = chopHdPrefix(hdPath);
6754
6855 eth.getAddress(path, false, true).then(
6956 info => {
@@ -72,153 +59,122 @@ class Ledger extends React.Component {
7259 const pub = secp256k1.publicKeyConvert(publicKey, true);
7360 const hd = bip32.fromPublicKey(pub, chainCode);
7461 setWallet(Maybe.Just(hd));
75− setWalletHdPath(addHdPrefix(hdpath));
62+ setWalletHdPath(addHdPrefix(hdPath));
7663 },
7764 _ => {
7865 setWallet(Maybe.Nothing());
7966 }
8067 );
81− }
82−
83− render() {
84− const { wallet } = this.props;
85− const { basePath, hdpath, account } = this.state;
86− const { handlePathSelection, handleAccountSelection } = this;
87−
88− const pathOptions = [
89− { title: 'Ledger Live', value: LEDGER_LIVE_PATH },
90− { title: 'Ledger Legacy', value: LEDGER_LEGACY_PATH },
91− {
92− title: 'Custom path',
93− value: 'custom',
94− },
95− ];
96− let basePathTitle = pathOptions.find(o => o.value === basePath).title;
97−
98− let accountOptions = [];
99− for (let i = 0; i < 20; i++) {
100− accountOptions.push({
101− title: 'Account #' + (i + 1),
102− value: i,
103− });
104− }
105− let accountTitle = accountOptions.find(o => o.value === account).title;
106−
107− const basePathSelection = (
68+ };
69+
70+ const basePathTitle = pathOptions.find(o => o.value === basePath).title;
71+
72+ const accountOptions = times(20, i => ({
73+ title: `Account #${i + 1}`,
74+ value: i,
75+ }));
76+ const accountTitle = accountOptions.find(o => o.value === account).title;
77+
78+ const basePathSelection = (
79+ <InnerLabelDropdown
80+ className="mt-8"
81+ options={pathOptions}
82+ handleUpdate={handlePathSelection}
83+ title="Derivation path"
84+ currentSelectionTitle={basePathTitle}
85+ fullWidth={true}
86+ />
87+ );
88+
89+ const truePathSelection =
90+ basePath === 'custom' ? (
91+ <Input
92+ className="mt3"
93+ name="hdPath"
94+ label="HD path"
95+ autocomplete="off"
96+ initialValue={addHdPrefix(hdPath)}
97+ onValue={setHdPath}
98+ />
99+ ) : (
108100 <InnerLabelDropdown
109− className="mt-8"
110− options={pathOptions}
111− handleUpdate={handlePathSelection}
112− title={'Derivation path'}
113− currentSelectionTitle={basePathTitle}
101+ className="mt-4"
102+ prop-size="md"
103+ prop-format="innerLabel"
104+ options={accountOptions}
105+ handleUpdate={handleAccountSelection}
106+ title="Account"
107+ currentSelectionTitle={accountTitle}
114108 fullWidth={true}
115109 />
116110 );
117111
118− const truePathSelection =
119− basePath === 'custom' ? (
120− <Input
121− className="pt-8 mt-4 text-mono"
122− prop-size="md"
123− prop-format="innerLabel"
124− name="hdpath"
125− value={addHdPrefix(hdpath)}
126− autocomplete="off"
127− onChange={this.handleHdPathInput}>
128− <InnerLabel>{'HD Path'}</InnerLabel>
129− </Input>
130− ) : (
131− <InnerLabelDropdown
132− className="mt-4"
133− prop-size="md"
134− prop-format="innerLabel"
135− options={accountOptions}
136− handleUpdate={handleAccountSelection}
137− title={'Account'}
138− currentSelectionTitle={accountTitle}
139− fullWidth={true}
140− />
141− );
142−
143− return (
144− <Row>
145− <Col className={'measure-md'}>
146− <H1>{'Authenticate With Your Ledger'}</H1>
147−
148− <H2>{'Running on HTTPS?'}</H2>
149−
150− <P>
151− {`Connect and authenticate to your Ledger, and then open the
152− "Ethereum" application. If you're running on older firmware,
153− make sure the "browser support" option is turned on. To sign
154− transactions, you'll also need to enable the "contract data"
155− option.`}
156− </P>
157−
158− <P>
159− {`If you'd like to use a custom derivation path, you may enter
160− it below.`}
161− </P>
162−
163− <H2>{'Running on HTTP?'}</H2>
164−
165− <P>{`To authenticate and sign transactions with a Ledger, Bridge
166− must be serving over HTTPS on localhost. You can do this via the
167− following:`}</P>
168−
169− <ol className={'measure-md'}>
170− <li className={'mt-4'}>
171− {'Install'}{' '}
172− <a
173− target={'_blank'}
174− href={'https://github.com/FiloSottile/mkcert'}>
175− {'mkcert'}
176− </a>
177− </li>
178− <li className={'mt-4'}>
179− {'Install a local certificate authority via '}
180− <code>{'mkcert -install'}</code>
181− </li>
182− <li className={'mt-4'}>
183− {'In your '}
184− <code>{'bridge'}</code>
185− {
186− ' directory, generate a certificate valid for localhost via '
187− }{' '}
188− <code>{'mkcert localhost'}</code>
189− {'.'}
190− {'This will produce two files: '}
191− <code>{'localhost.pem'}</code> {', the local certificate, and '}
192− <code>{'localhost-key.pem'}</code>
193− {', its corresponding private key.'}
194− </li>
195− <li className={'mt-4'}>
196− {'Run '}
197− <code>{'python bridge-https.py'}</code>
198− </li>
199− </ol>
200−
201− {basePathSelection}
202− {truePathSelection}
203−
204− <Button
205− prop-size={'wide lg'}
206− className={'mt-8'}
207− onClick={this.pollDevice}>
208− {'Authenticate →'}
209− </Button>
210−
211− <Button
212− className={'mt-8'}
213− prop-size={'wide lg'}
214− disabled={Maybe.Nothing.hasInstance(wallet)}
215− onClick={this.props.loginCompleted}>
216− {'Continue →'}
217− </Button>
218− </Col>
219− </Row>
112+ // when not on https, tell user how to get there
113+ const body =
114+ document.location.protocol !== 'https:' ? (
115+ <>
116+ <H2>Running on HTTP?</H2>
117+
118+ <P>
119+ To authenticate and sign transactions with a Ledger, Bridge must be
120+ serving over HTTPS on localhost. You can do this via the following:
121+ </P>
122+
123+ <ol className="measure-md">
124+ <li className="mt-4">
125+ Install
126+ <a target="_blank" href="https://github.com/FiloSottile/mkcert">
127+ mkcert
128+ </a>
129+ </li>
130+ <li className="mt-4">
131+ Install a local certificate authority via{' '}
132+ <code>mkcert -install</code>
133+ </li>
134+ <li className="mt-4">
135+ In your <code>bridge</code> directory, generate a certificate valid
136+ for localhost via <code>mkcert localhost</code>. This will produce
137+ two files: <code>localhost.pem</code>, the local certificate, and
138+ <code>localhost-key.pem</code> , its corresponding private key.
139+ </li>
140+ <li className="mt-4">
141+ Run <code>python bridge-https.py</code>
142+ </li>
143+ </ol>
144+ </>
145+ ) : (
146+ <>
147+ <P>
148+ Connect and authenticate to your Ledger, and then open the "Ethereum"
149+ application. If you're running on older firmware, make sure the
150+ "browser support" option is turned on. To sign transactions, you'll
151+ also need to enable the "contract data" option.
152+ </P>
153+
154+ <P>
155+ If you'd like to use a custom derivation path, you may enter it below.
156+ </P>
157+
158+ {basePathSelection}
159+ {truePathSelection}
160+
161+ <ForwardButton className={'mt3'} onClick={pollDevice}>
162+ {'Authenticate'}
163+ </ForwardButton>
164+
165+ <ForwardButton
166+ className={'mt3'}
167+ disabled={Maybe.Nothing.hasInstance(wallet)}
168+ onClick={loginCompleted}>
169+ {'Continue'}
170+ </ForwardButton>
171+ </>
220172 );
221− }
222−}
223173
224−export default compose(withWallet)(Ledger);
174+ return (
175+ <View>
176+ <H1>Authenticate With Your Ledger</H1>
177+ {body}
178+ </View>
179+ );
180+}
src/views/Login/Mnemonic.js+74−157
@@ -1,160 +1,77 @@
1+import React, { useCallback, useEffect, useState } from 'react';
12import { Just, Nothing } from 'folktale/maybe';
2−import React from 'react';
3−import { Button } from '../../components/old/Base';
4−import {
5− Input,
6− MnemonicInput,
7− InnerLabel,
8− InputCaption,
9−} from '../../components/old/Base';
10−import { Row, Col } from '../../components/old/Base';
113
12−import { DEFAULT_HD_PATH, walletFromMnemonic } from '../../lib/wallet';
13−import { compose } from '../../lib/lib';
14−import { withWallet } from '../../store/wallet';
15−
16−class Mnemonic extends React.Component {
17− constructor(props) {
18− super(props);
19−
20− this.state = {
21− mnemonic: '',
22− passphrase: '',
23− hdpath: DEFAULT_HD_PATH,
24− };
25−
26− this.handleMnemonicInput = this.handleMnemonicInput.bind(this);
27− this.handlePassphraseInput = this.handlePassphraseInput.bind(this);
28− this.handleHdPathInput = this.handleHdPathInput.bind(this);
29− }
30−
31− componentDidMount() {
32− const { mnemonic, hdpath, passphrase } = this.state;
33− this.attemptWalletDerivation(mnemonic, hdpath, passphrase);
34− }
35−
36− handlePassphraseInput(passphrase) {
37− this.setState((state, _) => {
38− const mnemonic = state.mnemonic;
39− const hdpath = state.hdpath;
40− this.attemptWalletDerivation(
41− mnemonic,
42− hdpath === '' ? DEFAULT_HD_PATH : hdpath,
43− passphrase
44− );
45− return {
46− passphrase,
47− };
48− });
49− }
50−
51− handleMnemonicInput(mnemonic) {
52− this.setState((state, _) => {
53− const hdpath = state.hdpath;
54− const passphrase = state.passphrase;
55− this.attemptWalletDerivation(
56− mnemonic,
57− hdpath === '' ? DEFAULT_HD_PATH : hdpath,
58− passphrase
59− );
60− return {
61− mnemonic,
62− };
63− });
64− }
65−
66− handleHdPathInput(hdpath) {
67− this.setState((state, _) => {
68− const mnemonic = state.mnemonic;
69− const passphrase = state.passphrase;
70− this.attemptWalletDerivation(
71− mnemonic,
72− hdpath === '' ? DEFAULT_HD_PATH : hdpath,
73− passphrase
74− );
75− return {
76− hdpath,
77− };
78− });
79− }
80−
81− attemptWalletDerivation(mnemonic, hdpath, passphrase) {
82− const { setWallet, setAuthMnemonic, setWalletHdPath } = this.props;
83− const wallet = walletFromMnemonic(mnemonic, hdpath, passphrase);
84− setWallet(wallet);
85− setAuthMnemonic(Just(mnemonic));
86− setWalletHdPath(hdpath);
87− }
88−
89− render() {
90− const { wallet } = this.props;
91− const { mnemonic, hdpath, passphrase } = this.state;
92−
93− return (
94− <Row>
95− <Col>
96− <InputCaption>
97− {'Please enter your BIP39 mnemonic here.'}
98− </InputCaption>
99−
100− <MnemonicInput
101− className="pt-8"
102− prop-size="md"
103− prop-format="innerLabel"
104− type="text"
105− name="mnemonic"
106− onChange={this.handleMnemonicInput}
107− value={mnemonic}
108− autocomplete="off"
109− autoFocus>
110− <InnerLabel>{'Mnemonic'}</InnerLabel>
111− </MnemonicInput>
112−
113− {!this.props.advanced ? null : (
114− <>
115− <InputCaption>
116− {`If your wallet requires a passphrase, you may enter it below.`}
117− </InputCaption>
118−
119− <Input
120− className="pt-8"
121− prop-size="md"
122− prop-format="innerLabel"
123− name="passphrase"
124− type="password"
125− value={passphrase}
126− autocomplete="off"
127− onChange={this.handlePassphraseInput}>
128− <InnerLabel>{'Passphrase'}</InnerLabel>
129− </Input>
130−
131− <InputCaption>
132− {`If you'd like to use a custom derivation path, you may enter it below.`}
133− </InputCaption>
134−
135− <Input
136− className="pt-8 text-mono"
137− prop-size="md"
138− prop-format="innerLabel"
139− name="hdpath"
140− value={hdpath}
141− autocomplete="off"
142− onChange={this.handleHdPathInput}>
143− <InnerLabel>{'HD Path'}</InnerLabel>
144− </Input>
145− </>
146− )}
147−
148− <Button
149− className={'mt-10'}
150− disabled={Nothing.hasInstance(wallet)}
151− onClick={this.props.loginCompleted}>
152− {'Go →'}
153− </Button>
154− </Col>
155− </Row>
156− );
157− }
4+import View from 'components/View';
5+import { MnemonicInput, PassphraseInput, HdPathInput } from 'components/Inputs';
6+import { ForwardButton } from 'components/Buttons';
7+
8+import { walletFromMnemonic } from 'lib/wallet';
9+import { useWallet } from 'store/wallet';
10+
11+export default function Mnemonic({ advanced, loginCompleted }) {
12+ const {
13+ wallet,
14+ setWallet,
15+ authMnemonic,
16+ setAuthMnemonic,
17+ walletHdPath,
18+ setWalletHdPath,
19+ } = useWallet();
20+ const [passphrase, setPassphrase] = useState('');
21+ const mnemonic = authMnemonic.getOrElse('');
22+
23+ // TODO: move this into transformers?
24+ // transform the result of the mnemonic to Maybe<string>
25+ const _setAuthMnemonic = useCallback(
26+ mnemonic => setAuthMnemonic(mnemonic === '' ? Nothing() : Just(mnemonic)),
27+ [setAuthMnemonic]
28+ );
29+
30+ // when the properties change, re-derive wallet
31+ useEffect(() => {
32+ let mounted = true;
33+ (async () => {
34+ const wallet = walletFromMnemonic(mnemonic, walletHdPath, passphrase);
35+ mounted && setWallet(wallet);
36+ })();
37+ return () => (mounted = false);
38+ }, [mnemonic, passphrase, walletHdPath, setWallet]);
39+
40+ return (
41+ <View>
42+ <MnemonicInput
43+ name="mnemonic"
44+ label="BIP39 Mnemonic"
45+ initialValue={mnemonic}
46+ onValue={_setAuthMnemonic}
47+ autoFocus
48+ />
49+
50+ {!advanced ? null : (
51+ <>
52+ <PassphraseInput
53+ name="passphrase"
54+ label="(Optional) Wallet Passphrase"
55+ initialValue={passphrase}
56+ onValue={setPassphrase}
57+ />
58+
59+ <HdPathInput
60+ name="hdpath"
61+ label="HD Path"
62+ initialValue={walletHdPath}
63+ onValue={setWalletHdPath}
64+ />
65+ </>
66+ )}
67+
68+ <ForwardButton
69+ className="mt3"
70+ disabled={Nothing.hasInstance(wallet)}
71+ onClick={loginCompleted}
72+ solid>
73+ Continue
74+ </ForwardButton>
75+ </View>
76+ );
15877}
159−
160−export default compose(withWallet)(Mnemonic);
src/views/Login/PrivateKey.js+40−69
@@ -1,80 +1,51 @@
1−import Maybe from 'folktale/maybe';
2−import React from 'react';
3−import { Button } from '../../components/old/Base';
4−import {
5− RequiredInput,
6− InnerLabel,
7− InputCaption,
8−} from '../../components/old/Base';
9−import { Row, Col, H1 } from '../../components/old/Base';
1+import { Just, Nothing } from 'folktale/maybe';
2+import React, { useState } from 'react';
103
11−import { EthereumWallet } from '../../lib/wallet';
12−import { compose } from '../../lib/lib';
13−import { withWallet } from '../../store/wallet';
4+import View from 'components/View';
5+import { PassphraseInput } from 'components/Inputs';
6+import { ForwardButton } from 'components/Buttons';
147
15−class PrivateKey extends React.Component {
16− constructor(props) {
17− super(props);
8+import { EthereumWallet } from 'lib/wallet';
9+import { useWallet } from 'store/wallet';
1810
19− this.state = {
20− privateKey: '',
21− };
11+export default function PrivateKey({ loginCompleted }) {
12+ const { wallet, setWallet } = useWallet();
2213
23− this.handlePrivateKeyInput = this.handlePrivateKeyInput.bind(this);
24− }
14+ const [privateKey, setPrivateKey] = useState('');
2515
26− handlePrivateKeyInput(privateKey) {
27− this.setState({ privateKey });
28− this.constructWallet(privateKey);
29− }
16+ const handlePrivateKeyInput = privateKey => {
17+ setPrivateKey(privateKey);
18+ constructWallet(privateKey);
19+ };
3020
31− constructWallet(privateKey) {
32− const { setWallet } = this.props;
21+ const constructWallet = privateKey => {
3322 if (/^[0-9A-Fa-f]{64}$/g.test(privateKey) === true) {
3423 const sec = Buffer.from(privateKey, 'hex');
35− const wallet = new EthereumWallet(sec);
36− setWallet(Maybe.Just(wallet));
24+ const newWallet = new EthereumWallet(sec);
25+ setWallet(Just(newWallet));
3726 } else {
38− setWallet(Maybe.Nothing());
27+ setWallet(Nothing());
3928 }
40− }
41−
42− render() {
43− const { wallet } = this.props;
44− const { privateKey } = this.state;
45−
46− return (
47− <Row>
48− <Col className={'measure-md'}>
49− <H1 className={'mb-4'}>{'Enter Your Private Key'}</H1>
50− <InputCaption>
51− {`Please enter your raw Ethereum private key here.`}
52− </InputCaption>
53−
54− <RequiredInput
55− className="pt-8 mt-8"
56− prop-size="md"
57− prop-format="innerLabel"
58− type="password"
59− name="privateKey"
60− onChange={this.handlePrivateKeyInput}
61− value={privateKey}
62− autocomplete="off"
63− autoFocus>
64− <InnerLabel>{'Private Key'}</InnerLabel>
65− </RequiredInput>
66−
67− <Button
68− className={'mt-10'}
69− prop-size={'wide lg'}
70− disabled={Maybe.Nothing.hasInstance(wallet)}
71− onClick={this.props.loginCompleted}>
72− {'Continue →'}
73− </Button>
74− </Col>
75− </Row>
76− );
77− }
29+ };
30+
31+ return (
32+ <View>
33+ Please enter your raw Ethereum private key here.
34+ <PassphraseInput
35+ type="password"
36+ name="privateKey"
37+ label="Private key"
38+ initialValue={privateKey}
39+ onValue={handlePrivateKeyInput}
40+ autocomplete="off"
41+ autoFocus
42+ />
43+ <ForwardButton
44+ className="mt3"
45+ disabled={Nothing.hasInstance(wallet)}
46+ onClick={loginCompleted}>
47+ Continue
48+ </ForwardButton>
49+ </View>
50+ );
7851}
79−
80−export default compose(withWallet)(PrivateKey);
src/views/Login/Shards.js+67−192
@@ -1,78 +1,24 @@
11import { Just, Nothing } from 'folktale/maybe';
2−import React from 'react';
3−import { Button } from '../../components/old/Base';
4−import {
5− InnerLabel,
6− ValidatedSigil,
7− PointInput,
8− ShardInput,
9− InputCaption,
10− Input,
11−} from '../../components/old/Base';
12−import { Row, Col, H1, P } from '../../components/old/Base';
2+import React, { useState } from 'react';
133import * as kg from 'urbit-key-generation/dist/index';
14−import * as ob from 'urbit-ob';
154
16−import { urbitWalletFromTicket } from '../../lib/wallet';
17−import { compose } from '../../lib/lib';
18−import { withWallet } from '../../store/wallet';
5+import View from 'components/View';
6+import { PointInput, TicketInput, PassphraseInput } from 'components/Inputs';
7+import { ForwardButton } from 'components/Buttons';
198
20−const placeholder = len => {
21− let bytes = window.crypto.getRandomValues(new Uint8Array(len));
22− let hex = bytes.reduce(
23− (acc, byt) => acc + byt.toString(16).padStart(2, '0'),
24− ''
25− );
26− return ob.hex2patq(hex);
27−};
28−
29−const SHARDS = {
30− SHARD1: Symbol('SHARD1'),
31− SHARD2: Symbol('SHARD2'),
32− SHARD3: Symbol('SHARD3'),
33−};
34−
35−class Shards extends React.Component {
36− constructor(props) {
37− super(props);
38−
39− this.state = {
40− shard1: '',
41− shard2: '',
42− shard3: '',
43− passphrase: '',
44− pointName: '',
45− };
46−
47− this.pointPlaceholder = placeholder(4);
48− this.ticketPlaceholder = placeholder(16);
49−
50− this.handlePassphraseInput = this.handlePassphraseInput.bind(this);
51− this.handleShardInput = this.handleShardInput.bind(this);
52− this.handlePointNameInput = this.handlePointNameInput.bind(this);
53− }
9+import { urbitWalletFromTicket } from 'lib/wallet';
10+import { useWallet } from 'store/wallet';
5411
55− handleShardInput(shard, input) {
56− if (shard === SHARDS.SHARD1) {
57− this.setState({ shard1: input });
58− } else if (shard === SHARDS.SHARD2) {
59− this.setState({ shard2: input });
60− } else if (shard === SHARDS.SHARD3) {
61− this.setState({ shard3: input });
62− }
63− }
64−
65− handlePassphraseInput(passphrase) {
66− this.setState({ passphrase });
67− }
12+export default function Shards({ loginCompleted }) {
13+ const { wallet, setUrbitWallet } = useWallet();
6814
69− handlePointNameInput(pointName) {
70− if (pointName.length < 15) {
71− this.setState({ pointName });
72− }
73− }
15+ const [shard1, setShard1] = useState('');
16+ const [shard2, setShard2] = useState('');
17+ const [shard3, setShard3] = useState('');
18+ const [passphrase, setPassphrase] = useState('');
19+ const [pointName, setPointName] = useState('');
7420
75− async walletFromShards(shard1, shard2, shard3, pointName, passphrase) {
21+ const walletFromShards = async () => {
7622 const s1 = shard1 === '' ? undefined : shard1;
7723 const s2 = shard2 === '' ? undefined : shard2;
7824 const s3 = shard3 === '' ? undefined : shard3;
@@ -86,129 +32,58 @@ class Shards extends React.Component {
8632
8733 if (ticket !== undefined) {
8834 const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
89− this.props.setUrbitWallet(Just(uhdw));
35+ setUrbitWallet(Just(uhdw));
9036 }
91− }
92−
93− render() {
94− const { wallet } = this.props;
95− const { shard1, shard2, shard3, pointName, passphrase } = this.state;
96−
97− const phPoint = this.pointPlaceholder;
98− const phTick = this.ticketPlaceholder;
99−
100− const shards = [shard1, shard2, shard3];
101− const ready = shards.filter(x => x !== '').length > 1;
102−
103− return (
104− <Row>
105− <Col>
106− <H1>{'Authenticate'}</H1>
107−
108− <P>
109− {`Enter your point and at least two of your three Urbit master
110− ticket shards here. The index of the input field needs to
111− match the index of the shard.`}
112− </P>
113−
114− <PointInput
115− className="mono mt-8"
116− prop-size="lg"
117− prop-format="innerLabel"
118− type="text"
119− autoFocus
120− placeholder={`e.g. ${phPoint}`}
121− value={pointName}
122− onChange={this.handlePointNameInput}>
123− <InnerLabel>{'Point'}</InnerLabel>
124− <ValidatedSigil
125− className={'tr-0 mt-05 mr-0 abs'}
126− patp={pointName}
127− size={68}
128− margin={8}
129− />
130− </PointInput>
131−
132− <ShardInput
133− className="mono mt-8"
134− prop-size="md"
135− prop-format="innerLabel"
136− type="text"
137− name="shard1"
138− placeholder={`e.g. ${phTick}`}
139− value={shard1}
140− onChange={inp => this.handleShardInput(SHARDS.SHARD1, inp)}>
141− <InnerLabel>{'Shard 1'}</InnerLabel>
142− </ShardInput>
143−
144− <ShardInput
145− className="mono mt-8"
146− prop-size="md"
147− prop-format="innerLabel"
148− type="text"
149− name="shard2"
150− placeholder={`e.g. ${phTick}`}
151− value={shard2}
152− onChange={inp => this.handleShardInput(SHARDS.SHARD2, inp)}>
153− <InnerLabel>{'Shard 2'}</InnerLabel>
154− </ShardInput>
155−
156− <ShardInput
157− className="mono mt-8"
158− prop-size="md"
159− prop-format="innerLabel"
160− type="text"
161− name="shard3"
162− placeholder={`e.g. ${phTick}`}
163− value={shard3}
164− onChange={inp => this.handleShardInput(SHARDS.SHARD3, inp)}>
165− <InnerLabel>{'Shard 3'}</InnerLabel>
166− </ShardInput>
167−
168− <InputCaption>
169− {'If your wallet requires a passphrase, you may enter it below.'}
170− </InputCaption>
171−
172− <Input
173− className="pt-8"
174− prop-size="md"
175− prop-format="innerLabel"
176− name="passphrase"
177− type="password"
178− value={passphrase}
179− autocomplete="off"
180− onChange={this.handlePassphraseInput}>
181− <InnerLabel>{'Passphrase'}</InnerLabel>
182− </Input>
183−
184− <Button
185− disabled={!ready}
186− className={'mt-8'}
187− prop-size={'lg wide'}
188− // prop-color={this.buttonTriState(wallet)}
189− onClick={() =>
190− this.walletFromShards(
191− shard1,
192− shard2,
193− shard3,
194− pointName,
195− passphrase
196− )
197− }>
198− {'Unlock Wallet →'}
199− </Button>
200−
201− <Button
202− className={'mt-4'}
203− prop-size={'xl wide'}
204− disabled={Nothing.hasInstance(wallet)}
205− onClick={this.props.loginCompleted}>
206− {'Continue →'}
207− </Button>
208− </Col>
209− </Row>
210− );
211− }
37+ };
38+
39+ const shards = [shard1, shard2, shard3];
40+ const ready = shards.filter(x => x !== '').length > 1;
41+
42+ return (
43+ <View>
44+ Enter your point and at least two of your three Urbit master ticket shards
45+ here. The index of the input field needs to match the index of the shard.
46+ <PointInput
47+ name="point"
48+ label="Point"
49+ initialValue={pointName}
50+ onValue={setPointName}
51+ autoFocus
52+ />
53+ <TicketInput
54+ name="shard1"
55+ label="Shard 1"
56+ initialValue={shard1}
57+ onValue={setShard1}></TicketInput>
58+ <TicketInput
59+ name="shard2"
60+ label="Shard 2"
61+ initialValue={shard2}
62+ onValue={setShard2}></TicketInput>
63+ <TicketInput
64+ name="shard3"
65+ label="Shard 3"
66+ initialValue={shard3}
67+ onValue={setShard3}></TicketInput>
68+ If your wallet requires a passphrase, you may enter it below.
69+ <PassphraseInput
70+ name="passphrase"
71+ label="(Optional) Wallet Passphrase"
72+ initialValue={passphrase}
73+ onValue={setPassphrase}
74+ />
75+ <ForwardButton
76+ className="mt3"
77+ disabled={!ready}
78+ onClick={walletFromShards}>
79+ Unlock Wallet
80+ </ForwardButton>
81+ <ForwardButton
82+ className="mt3"
83+ disabled={Nothing.hasInstance(wallet)}
84+ onClick={loginCompleted}>
85+ Continue
86+ </ForwardButton>
87+ </View>
88+ );
21289}
213−
214−export default compose(withWallet)(Shards);
src/views/Login/Ticket.js+112−170
@@ -1,26 +1,18 @@
11import { Just, Nothing } from 'folktale/maybe';
2−import React from 'react';
2+import React, { useState, useEffect } from 'react';
33import * as azimuth from 'azimuth-js';
44import * as ob from 'urbit-ob';
55
6−import {
7− Button,
8− Row,
9− Col,
10− Input,
11− InnerLabel,
12− ValidatedSigil,
13− PointInput,
14− TicketInput,
15−} from '../../components/old/Base';
16−
17−import * as need from '../../lib/need';
18−import { compose } from '../../lib/lib';
19−import { WALLET_TYPES, urbitWalletFromTicket } from '../../lib/wallet';
20−
21−import { withWallet } from '../../store/wallet';
22−import { withNetwork } from '../../store/network';
23−import { withPointCursor } from '../../store/pointCursor';
6+import View from 'components/View';
7+import { PointInput, TicketInput, PassphraseInput } from 'components/Inputs';
8+import { ForwardButton } from 'components/Buttons';
9+
10+import * as need from 'lib/need';
11+import { WALLET_TYPES, urbitWalletFromTicket } from 'lib/wallet';
12+
13+import { useNetwork } from 'store/network';
14+import { useWallet } from 'store/wallet';
15+import { usePointCursor } from 'store/pointCursor';
2416
2517//TODO should be part of InputWithStatus component
2618const INPUT_STATUS = {
@@ -29,174 +21,124 @@ const INPUT_STATUS = {
2921 FAIL: Symbol('FAIL'),
3022};
3123
32−class Ticket extends React.Component {
33− constructor(props) {
34− super(props);
35−
36− this.state = {
37− pointName: '',
38− ticket: '',
39− ticketStatus: Nothing(),
40− passphrase: '',
41− };
42−
43− this.handlePointNameInput = this.handlePointNameInput.bind(this);
44− this.handleTicketInput = this.handleTicketInput.bind(this);
45− this.handlePassphraseInput = this.handlePassphraseInput.bind(this);
46− this.verifyTicket = this.verifyTicket.bind(this);
47− this.continue = this.continue.bind(this);
48− }
49−
50− componentDidMount() {
51− //TODO deduce point name from URL if we can, prefill input if we found it
52− }
53−
54− handlePointNameInput(pointName) {
55− if (pointName.length < 15) {
56− this.setState({ pointName });
57− this.verifyTicket(pointName, this.state.ticket, this.state.passphrase);
58− }
59− }
24+export default function Ticket({ advanced, loginCompleted }) {
25+ // globals
26+ const { contracts } = useNetwork();
27+ const { wallet, setWalletType, setUrbitWallet } = useWallet();
28+ const { setPointCursor } = usePointCursor();
6029
61− handleTicketInput(ticket) {
62− this.setState({ ticket });
63− this.verifyTicket(this.state.pointName, ticket, this.state.passphrase);
64− }
30+ // inputs
31+ //TODO deduce point name from URL if we can, prefill input if we found it
32+ const [pointName, setPointName] = useState('');
33+ const [ticket, setTicket] = useState('');
34+ const [passphrase, setPassphrase] = useState('');
6535
66− handlePassphraseInput(passphrase) {
67− this.setState({ passphrase });
68− this.verifyTicket(this.state.pointName, this.state.ticket, passphrase);
69− }
36+ // display state
37+ const [ticketStatus, setTicketStatus] = useState(Nothing());
38+
39+ useEffect(() => {
40+ verifyTicket(pointName, ticket, passphrase);
41+ return () => {};
42+ }, [pointName, ticket, passphrase]);
7043
7144 //TODO maybe want to do this only on-go, because wallet derivation is slow...
72− async verifyTicket(pointName, ticket, passphrase) {
45+ const verifyTicket = async (pointName, ticket, passphrase) => {
46+ setUrbitWallet(Nothing());
7347 if (!ob.isValidPatq(ticket) || !ob.isValidPatp(pointName)) {
74− this.props.setUrbitWallet(Nothing());
75− this.setState({ ticketStatus: Nothing() });
48+ setTicketStatus(Nothing());
7649 return;
7750 }
78− this.setState({ ticketStatus: Just(INPUT_STATUS.SPIN) });
79− const contracts = need.contracts(this.props);
51+ setTicketStatus(Just(INPUT_STATUS.SPIN));
52+ const _contracts = need.contracts(contracts);
8053 const pointNumber = ob.patp2dec(pointName);
8154 const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
8255 const isOwner = azimuth.azimuth.isOwner(
83− contracts,
56+ _contracts,
8457 pointNumber,
8558 uhdw.ownership.keys.address
8659 );
8760 const isTransferProxy = azimuth.azimuth.isTransferProxy(
88− contracts,
61+ _contracts,
8962 pointNumber,
9063 uhdw.ownership.keys.address
9164 );
92− this.props.setUrbitWallet(Just(uhdw));
93− this.setState({
94− ticketStatus:
95− (await isOwner) || (await isTransferProxy)
96− ? Just(INPUT_STATUS.GOOD)
97− : Just(INPUT_STATUS.FAIL),
98− });
99− }
100−
101− canContinue() {
65+ setUrbitWallet(Just(uhdw));
66+ const newStatus =
67+ (await isOwner) || (await isTransferProxy)
68+ ? INPUT_STATUS.GOOD
69+ : INPUT_STATUS.FAIL;
70+ setTicketStatus(Just(newStatus));
71+ };
72+
73+ const canContinue = () => {
10274 // this is our only requirement, since we still want people with
10375 // non-standard wallet setups to be able to log in
104− return Just.hasInstance(this.props.wallet);
105− }
106−
107− continue() {
108− //TODO maybe do on-mount?
109− this.props.setWalletType(WALLET_TYPES.TICKET);
110− this.props.setPointCursor(Just(ob.patp2dec(this.state.pointName)));
111− this.props.loginCompleted();
112− }
113−
114− render() {
115− const pointInput = (
116− <PointInput
117− className="mono mt-8"
118− prop-size="lg"
119− prop-format="innerLabel"
120− type="text"
121− autoFocus
122− value={this.state.pointName}
123− onChange={this.handlePointNameInput}>
124− <InnerLabel>{'Point name'}</InnerLabel>
125− <ValidatedSigil
126− className={'tr-0 mt-05 mr-0 abs'}
127− patp={this.state.pointName}
128− size={68}
129− margin={8}
130− />
131− </PointInput>
132− );
133−
134− const ticketStatus = this.state.ticketStatus.matchWith({
135− Nothing: () => <span />,
136− Just: status => {
137− switch (status.value) {
138− case INPUT_STATUS.SPIN:
139− return <span>⋯</span>;
140− case INPUT_STATUS.GOOD:
141− return <span>✓</span>;
142− case INPUT_STATUS.FAIL:
143− return <span>𐄂</span>;
144− default:
145− throw new Error('weird input status ' + status.value);
146− }
147− },
148− });
149−
150− const ticketInput = (
151− //TODO NewTicketInput component, that does dashes etc
152− <TicketInput
153− className="mono mt-8"
154− prop-size="lg"
155− prop-format="innerLabel"
156− type="password"
157− name="ticket"
158− value={this.state.ticket}
159− onChange={this.handleTicketInput}>
160− <InnerLabel>{'Master ticket'}</InnerLabel>
161− {ticketStatus}
162− </TicketInput>
163− );
164−
165− const passphraseInput = !this.props.advanced ? null : (
166− <Input
167− className="mt-8"
168− prop-size="md"
169− prop-format="innerLabel"
170− name="passphrase"
171− type="password"
172− value={this.state.passphrase}
173− autocomplete="off"
174− onChange={this.handlePassphraseInput}>
175− <InnerLabel>{'Passphrase'}</InnerLabel>
176− </Input>
177− );
178−
179− return (
180− <Row>
181− <Col>
182− {pointInput}
183− {ticketInput}
184− {passphraseInput}
185−
186− <Button
187− className={'mt-10'}
188− disabled={!this.canContinue()}
189− onClick={this.continue}>
190− {'Go →'}
191− </Button>
192− </Col>
193− </Row>
194− );
195− }
76+ return Just.hasInstance(wallet);
77+ };
78+
79+ const doContinue = () => {
80+ setWalletType(WALLET_TYPES.TICKET);
81+ setPointCursor(Just(ob.patp2dec(pointName)));
82+ loginCompleted();
83+ };
84+
85+ const pointInput = (
86+ <PointInput
87+ name="point"
88+ label="Point"
89+ initialValue={pointName}
90+ onValue={setPointName}
91+ autoFocus
92+ />
93+ );
94+
95+ //TODO integrate into TicketInput?
96+ const displayTicketStatus = ticketStatus.matchWith({
97+ Nothing: () => <span />,
98+ Just: status => {
99+ switch (status.value) {
100+ case INPUT_STATUS.SPIN:
101+ return <span>⋯</span>;
102+ case INPUT_STATUS.GOOD:
103+ return <span>✓</span>;
104+ case INPUT_STATUS.FAIL:
105+ return <span>𐄂</span>;
106+ default:
107+ throw new Error('weird input status ' + status.value);
108+ }
109+ },
110+ });
111+
112+ const ticketInput = (
113+ <TicketInput
114+ name="ticket"
115+ label="Master ticket"
116+ initialValue={ticket}
117+ onValue={setTicket}
118+ />
119+ );
120+
121+ const passphraseInput = !advanced ? null : (
122+ <PassphraseInput
123+ name="passphrase"
124+ label="(Optional) Wallet Passphrase"
125+ initialValue={passphrase}
126+ onValue={setPassphrase}
127+ />
128+ );
129+
130+ return (
131+ <View>
132+ {pointInput}
133+ {ticketInput}
134+ {passphraseInput}
135+
136+ <ForwardButton
137+ className={'mt3'}
138+ disabled={!canContinue()}
139+ onClick={doContinue}>
140+ Continue
141+ </ForwardButton>
142+ </View>
143+ );
196144}
197−
198−export default compose(
199− withWallet,
200− withNetwork,
201− withPointCursor
202−)(Ticket);
src/views/Login/Trezor.js+74−115
@@ -1,55 +1,42 @@
11import * as bip32 from 'bip32';
2−import React from 'react';
2+import React, { useState } from 'react';
33import Maybe from 'folktale/maybe';
4−import { Button } from '../../components/old/Base';
5−import { Row, Col, H1, P } from '../../components/old/Base';
6−import {
7− InnerLabel,
8− Input,
9− InnerLabelDropdown,
10−} from '../../components/old/Base';
4+import { times } from 'lodash';
115import TrezorConnect from 'trezor-connect';
126import * as secp256k1 from 'secp256k1';
137
14−import { TREZOR_PATH } from '../../lib/trezor';
15−import { compose } from '../../lib/lib';
16−import { withWallet } from '../../store/wallet';
8+import View from 'components/View';
9+import { ForwardButton } from 'components/Buttons';
10+import { H1, P, Input } from 'indigo-react';
11+import { InnerLabel, InnerLabelDropdown } from 'components/old/Base';
1712
18−class Trezor extends React.Component {
19− constructor(props) {
20− super(props);
13+import { TREZOR_PATH } from 'lib/trezor';
14+import { useWallet } from 'store/wallet';
2115
22− this.state = {
23− hdpath: TREZOR_PATH.replace(/x/g, 0),
24− account: 0,
25− };
16+const accountOptions = [
17+ { title: 'Custom path', value: 'custom' },
18+ ...times(20, i => ({ title: `Account #${i + 1}`, value: i })),
19+];
2620
27− this.handleAccountSelection = this.handleAccountSelection.bind(this);
28− this.handleHdPathInput = this.handleHdPathInput.bind(this);
29− this.pollDevice = this.pollDevice.bind(this);
30− }
21+export default function Trezor({ loginCompleted }) {
22+ const { wallet, setWallet, setWalletHdPath } = useWallet();
3123
32− handleAccountSelection(account) {
33− let hdpath = this.state.hdpath;
24+ const [hdPath, setHdPath] = useState(TREZOR_PATH.replace(/x/g, 0));
25+ const [account, setAccount] = useState(0);
26+
27+ const handleAccountSelection = account => {
28+ setAccount(account);
3429 if (account !== 'custom') {
35− hdpath = TREZOR_PATH.replace(/x/g, account);
30+ setHdPath(TREZOR_PATH.replace(/x/g, account));
3631 }
37− this.setState({ account, hdpath });
38− }
39−
40− handleHdPathInput(hdpath) {
41− this.setState({ hdpath });
42− }
43−
44− async pollDevice() {
45− const { setWallet, setWalletHdPath } = this.props;
46− const { hdpath } = this.state;
32+ };
4733
34+ const pollDevice = async () => {
4835 TrezorConnect.manifest({
4936 email: 'bridge-trezor@urbit.org',
5037 appUrl: 'https://github.com/urbit/bridge',
5138 });
52− TrezorConnect.getPublicKey({ path: hdpath }).then(info => {
39+ TrezorConnect.getPublicKey({ path: hdPath }).then(info => {
5340 if (info.success === true) {
5441 const payload = info.payload;
5542 const publicKey = Buffer.from(payload.publicKey, 'hex');
@@ -57,90 +44,62 @@ class Trezor extends React.Component {
5744 const pub = secp256k1.publicKeyConvert(publicKey, true);
5845 const hd = bip32.fromPublicKey(pub, chainCode);
5946 setWallet(Maybe.Just(hd));
60− setWalletHdPath(hdpath);
47+ setWalletHdPath(hdPath);
6148 } else {
6249 setWallet(Maybe.Nothing());
6350 }
6451 });
65− }
66−
67− render() {
68− const { wallet } = this.props;
69− const { hdpath, account } = this.state;
70− const { handleAccountSelection } = this;
71−
72− let accountOptions = [
73− {
74− title: 'Custom path',
75− value: 'custom',
76− },
77− ];
78− for (let i = 0; i < 20; i++) {
79− accountOptions.push({
80− title: 'Account #' + (i + 1),
81− value: i,
82− });
83− }
84− let accountTitle = accountOptions.find(o => o.value === account).title;
85−
86− const accountSelection = (
87− <InnerLabelDropdown
88− className="mt-8"
89− prop-size="md"
90− prop-format="innerLabel"
91− options={accountOptions}
92− handleUpdate={handleAccountSelection}
93− title={'Account'}
94− currentSelectionTitle={accountTitle}
95− fullWidth={true}
52+ };
53+
54+ let accountTitle = accountOptions.find(o => o.value === account).title;
55+
56+ const accountSelection = (
57+ <InnerLabelDropdown
58+ className="mt-8"
59+ prop-size="md"
60+ prop-format="innerLabel"
61+ options={accountOptions}
62+ handleUpdate={handleAccountSelection}
63+ title="Account"
64+ currentSelectionTitle={accountTitle}
65+ fullWidth={true}
66+ />
67+ );
68+
69+ const pathSelection =
70+ account !== 'custom' ? null : (
71+ <Input
72+ className="mt3"
73+ name="hdPath"
74+ label="HD path"
75+ autocomplete="off"
76+ initialValue={hdPath}
77+ onValue={setHdPath}
9678 />
9779 );
9880
99− const pathSelection =
100− account !== 'custom' ? null : (
101− <Input
102− className="mt-8 pt-8 text-mono"
103− prop-size="md"
104− prop-format="innerLabel"
105− name="hdpath"
106− value={hdpath}
107− autocomplete="off"
108− onChange={this.handleHdPathInput}>
109− <InnerLabel>{'HD Path'}</InnerLabel>
110− </Input>
111− );
112−
113− return (
114− <Row>
115− <Col className={'measure-md'}>
116− <H1>{'Authenticate With Your Trezor'}</H1>
117−
118− <P>
119− {`Connect and authenticate to your Trezor. If you'd like
120− to use a custom derivation path, you may enter it below.`}
121− </P>
122−
123− {accountSelection}
124− {pathSelection}
125−
126− <Button
127− className={'mt-8'}
128− prop-size={'wide lg'}
129− onClick={this.pollDevice}>
130− {'Authenticate →'}
131− </Button>
132−
133− <Button
134− className={'mt-8'}
135− prop-size={'wide lg'}
136− disabled={Maybe.Nothing.hasInstance(wallet)}
137− onClick={this.props.loginCompleted}>
138− {'Continue →'}
139− </Button>
140− </Col>
141− </Row>
142− );
143− }
81+ return (
82+ <View>
83+ <H1>Authenticate With Your Trezor</H1>
84+
85+ <P>
86+ Connect and authenticate to your Trezor. If you'd like to use a custom
87+ derivation path, you may enter it below.
88+ </P>
89+
90+ {accountSelection}
91+ {pathSelection}
92+
93+ <ForwardButton className="mt3" onClick={pollDevice}>
94+ Authenticate
95+ </ForwardButton>
96+
97+ <ForwardButton
98+ className="mt3"
99+ disabled={Maybe.Nothing.hasInstance(wallet)}
100+ onClick={loginCompleted}>
101+ Continue
102+ </ForwardButton>
103+ </View>
104+ );
144105}
145−
146−export default compose(withWallet)(Trezor);
src/views/ViewPoint.js+4−11
@@ -12,7 +12,6 @@ import { ROUTE_NAMES } from 'lib/routeNames';
1212import View from 'components/View';
1313import { ForwardButton } from 'components/Buttons';
1414import { PointInput } from 'components/Inputs';
15−import InputSigil from 'components/InputSigil';
1615
1716export default function ViewPoint() {
1817 const history = useHistory();
@@ -38,21 +37,15 @@ export default function ViewPoint() {
3837 <PointInput
3938 name="point"
4039 label="Point Name"
40+ initialValue={pointName}
4141 onValue={setPointName}
42+ pass={pass}
4243 onPass={setPass}
44+ error={error}
4345 onError={setError}
46+ focused={focused}
4447 onFocus={setFocused}
4548 onEnter={goForward}
46− accessory={
47− <InputSigil
48− patp={pointName}
49− size={68}
50− margin={8}
51− pass={pass}
52− focused={focused}
53− error={error}
54− />
55− }
5649 autoFocus
5750 />
5851