on master help sign in
refactor: indigo-ifying login components
9781501Matt Condon (shrugs)
src/Bridge.js+1−1
@@ -23,7 +23,7 @@ const INITIAL_NETWORK_TYPE = isDevelopment
23const SHOULD_STUB_LOCAL = process.env.REACT_APP_STUB_LOCAL === 'true';
23const SHOULD_STUB_LOCAL = process.env.REACT_APP_STUB_LOCAL === 'true';
24const IS_STUBBED = isDevelopment && SHOULD_STUB_LOCAL;
24const IS_STUBBED = isDevelopment && SHOULD_STUB_LOCAL;
25const INITIAL_ROUTES = IS_STUBBED
25const INITIAL_ROUTES = IS_STUBBED
26− ? [{ key: ROUTE_NAMES.LANDING }, { key: ROUTE_NAMES.ACTIVATE }]
26+ ? [{ key: ROUTE_NAMES.LANDING }, { key: ROUTE_NAMES.LOGIN }]
27 : [{ key: ROUTE_NAMES.LANDING }];
27 : [{ key: ROUTE_NAMES.LANDING }];
28
28
29const INITIAL_WALLET = IS_STUBBED
29const INITIAL_WALLET = IS_STUBBED
src/components/Inputs.js+13−2
@@ -88,7 +88,7 @@ export function useHdPathInput(props) {
88const kTicketValidators = [validateTicket, validateNotEmpty];
88const kTicketValidators = [validateTicket, validateNotEmpty];
89const kTicketTransformers = [prependSig];
89const kTicketTransformers = [prependSig];
90//TODO needs to be fancier, displaying sig and dashes instead of •ing all
90//TODO needs to be fancier, displaying sig and dashes instead of •ing all
91−export function useTicketInput({ validators = [], ...rest }) {
91+export function useTicketInput({ validators = [], deriving = false, ...rest }) {
92 const input = firstOf(
92 const input = firstOf(
93 useForm([
93 useForm([
94 {
94 {
@@ -105,7 +105,18 @@ export function useTicketInput({ validators = [], ...rest }) {
105 ])
105 ])
106 );
106 );
107
107
108+ const accessory = input.error ? (
109+ <AccessoryIcon.Failure />
110+ ) : deriving ? (
111+ <AccessoryIcon.Pending />
112+ ) : input.pass ? (
108− return { ...input, accessory: input.pass ? <AccessoryIcon.Success /> : null };
113+ <AccessoryIcon.Success />
114+ ) : null;
115+
116+ return {
117+ ...input,
118+ accessory,
119+ };
109}
120}
110
121
111const kPointValidators = [validatePoint, validateNotEmpty];
122const kPointValidators = [validatePoint, validateNotEmpty];
src/components/Tabs.js+27−8
@@ -1,18 +1,37 @@
1import React from 'react';
1import React from 'react';
2−import HorizontalSelector from './old/HorizontalSelector';
2+import cn from 'classnames';
3+import { Grid, Flex } from 'indigo-react';
3
4
4export default function Tabs({
5export default function Tabs({
5− tabViews,
6+ views,
6− tabOptions,
7+ options,
7 currentTab,
8 currentTab,
8 onTabChange,
9 onTabChange,
10+ className,
9 ...rest
11 ...rest
10}) {
12}) {
11− const Component = tabViews[currentTab];
13+ const Tab = views[currentTab];
14+
12 return (
15 return (
13− <>
16+ <Grid className={className}>
14− <HorizontalSelector options={tabOptions} onChange={onTabChange} />
17+ <Grid.Item full as={Flex} className="b-gray3 bb1">
15− <Component {...rest} />
18+ {options.map(option => {
16− </>
19+ const isActive = option.value === currentTab;
20+ return (
21+ <Flex.Item
22+ key={option.value}
23+ onClick={() => onTabChange(option.value)}
24+ className={cn('f5 pv3 mr6 clickable', {
25+ 'black b-black bb1': isActive,
26+ gray3: !isActive,
27+ })}
28+ style={{ marginBottom: '-1px' }}>
29+ {option.text}
30+ </Flex.Item>
31+ );
32+ })}
33+ </Grid.Item>
34+ <Grid.Item full as={Tab} className="mt6" {...rest} />
35+ </Grid>
17 );
36 );
18}
37}
src/components/UploadButton.jsnew file+19
@@ -0,0 +1,19 @@
1+import React, { useRef } from 'react';
2+import { Button } from 'indigo-react';
3+
4+export default function UploadButton({ children, onChange, ...rest }) {
5+ const button = useRef();
6+
7+ return (
8+ <label htmlFor="file" {...rest}>
9+ <Button accessory="⬆">{children}</Button>
10+ <input
11+ id="file"
12+ ref={button}
13+ className="super-hidden"
14+ type="file"
15+ onChange={() => onChange(button.current)}
16+ />
17+ </label>
18+ );
19+}
src/indigo-react/components/Button.js+2−1
@@ -6,6 +6,7 @@ import Flex from './Flex';
6import { HelpText } from './Typography';
6import { HelpText } from './Typography';
7
7
8export default function Button({
8export default function Button({
9+ as: As = 'a',
9 solid = false,
10 solid = false,
10 success = false,
11 success = false,
11 disabled = false,
12 disabled = false,
@@ -23,7 +24,7 @@ export default function Button({
23 };
24 };
24 return (
25 return (
25 <Grid
26 <Grid
26− as="a"
27+ as={As}
27 gap={1}
28 gap={1}
28 className={cn(
29 className={cn(
29 'pointer pv4 truncate flex-row justify-between',
30 'pointer pv4 truncate flex-row justify-between',
src/indigo-react/components/Input.js+1−1
@@ -70,7 +70,7 @@ export default function Input({
70
70
71 return (
71 return (
72 <Flex
72 <Flex
73− className={className}
73+ className={cn(className, 'mb1')}
74 col
74 col
75 style={{
75 style={{
76 ...(disabled && {
76 ...(disabled && {
src/lib/useLoginView.jsnew file+17
@@ -0,0 +1,17 @@
1+import { useEffect } from 'react';
2+import Maybe from 'folktale/maybe';
3+
4+import { useWallet } from 'store/wallet';
5+import { usePointCursor } from 'store/pointCursor';
6+
7+// All the things we want to do when we mount a login view
8+export default function useLoginView(walletType) {
9+ const { setWalletType, resetWallet } = useWallet();
10+ const { setPointCursor } = usePointCursor();
11+
12+ useEffect(() => {
13+ resetWallet();
14+ setWalletType(walletType);
15+ setPointCursor(Maybe.Nothing());
16+ }, [setWalletType, walletType, setPointCursor, resetWallet]);
17+}
src/lib/useResetPointCursor.jsdeleted−12
@@ -1,12 +0,0 @@
1−import { useEffect } from 'react';
2−import Maybe from 'folktale/maybe';
3−
4−import { usePointCursor } from 'store/pointCursor';
5−
6−export default function useResetPointCursor() {
7− const { setPointCursor } = usePointCursor();
8−
9− useEffect(() => {
10− setPointCursor(Maybe.Nothing());
11− }, [setPointCursor]);
12−}
src/lib/useWalletType.jsdeleted−11
@@ -1,11 +0,0 @@
1−import { useEffect } from 'react';
2−
3−import { useWallet } from 'store/wallet';
4−
5−export default function useWalletType(walletType) {
6− const { setWalletType } = useWallet();
7−
8− useEffect(() => {
9− setWalletType(walletType);
10− }, [setWalletType, walletType]);
11−}
src/views/Activate/ActivatePassport.js−1
@@ -1,6 +1,5 @@
1import React, { useMemo } from 'react';
1import React, { useMemo } from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3−import Maybe from 'folktale/maybe';
4import { Grid } from 'indigo-react';
3import { Grid } from 'indigo-react';
5
4
6import { LocalRouterProvider, useLocalRouter } from 'lib/LocalRouter';
5import { LocalRouterProvider, useLocalRouter } from 'lib/LocalRouter';
src/views/Login.js+86−45
@@ -1,104 +1,145 @@
1+import React, { useCallback, useState } from 'react';
1−import { Nothing, Just } from 'folktale/maybe';
2+import Maybe from 'folktale/maybe';
2−import React, { useState } from 'react';
3import * as azimuth from 'azimuth-js';
3import * as azimuth from 'azimuth-js';
4
4
5−import { H1 } from 'indigo-react';
5+import { H4, Grid } from 'indigo-react';
6−import View from 'components/View';
7−import Tabs from 'components/Tabs';
8
6
9import Ticket from './Login/Ticket';
7import Ticket from './Login/Ticket';
10import Mnemonic from './Login/Mnemonic';
8import Mnemonic from './Login/Mnemonic';
11import Advanced from './Login/Advanced';
9import Advanced from './Login/Advanced';
10+import Hardware from './Login/Hardware';
12
11
13−import useLifecycle from 'lib/useLifecycle';
14import * as need from 'lib/need';
12import * as need from 'lib/need';
15−import { ROUTE_NAMES } from 'lib/routeNames';
16
13
17import { useHistory } from 'store/history';
14import { useHistory } from 'store/history';
18import { useNetwork } from 'store/network';
15import { useNetwork } from 'store/network';
19import { useWallet } from 'store/wallet';
16import { useWallet } from 'store/wallet';
20import { usePointCursor } from 'store/pointCursor';
17import { usePointCursor } from 'store/pointCursor';
21
18
19+import View from 'components/View';
20+import Tabs from 'components/Tabs';
21+import Crumbs from 'components/Crumbs';
22+import Footer from 'components/Footer';
23+import { ForwardButton, OfflineButton } from 'components/Buttons';
24+
22−const TABS = {
25+const NAMES = {
23− TICKET: Symbol('TICKET'),
26+ TICKET: 'TICKET',
24− MNEMONIC: Symbol('MNEMONIC'),
27+ MNEMONIC: 'MNEMONIC',
28+ HARDWARE: 'HARDWARE',
25− ADVANCED: Symbol('ADVANCED'),
29+ ADVANCED: 'ADVANCED',
26};
30};
27
31
28−const tabViews = {
32+const VIEWS = {
29− [TABS.TICKET]: Ticket,
33+ [NAMES.TICKET]: Ticket,
30− [TABS.MNEMONIC]: Mnemonic,
34+ [NAMES.MNEMONIC]: Mnemonic,
35+ [NAMES.HARDWARE]: Hardware,
31− [TABS.ADVANCED]: Advanced,
36+ [NAMES.ADVANCED]: Advanced,
32};
37};
33
38
34−const tabOptions = [
39+const OPTIONS = [
35− { title: 'Ticket', value: TABS.TICKET },
40+ { text: 'Master Ticket', value: NAMES.TICKET },
36− { title: 'Mnemonic', value: TABS.MNEMONIC },
41+ { text: 'Mnemonic', value: NAMES.MNEMONIC },
42+ { text: 'Hardware', value: NAMES.HARDWARE },
37− { title: 'Advanced', value: TABS.ADVANCED },
43+ { text: 'Advanced', value: NAMES.ADVANCED },
38];
44];
39
45
40export default function Login() {
46export default function Login() {
41 // globals
47 // globals
42− const history = useHistory();
48+ const { push, replaceWith, names } = useHistory();
43 const { contracts } = useNetwork();
49 const { contracts } = useNetwork();
44− const { wallet, resetWallet } = useWallet();
50+ const { wallet } = useWallet();
45 const { pointCursor, setPointCursor } = usePointCursor();
51 const { pointCursor, setPointCursor } = usePointCursor();
46
52
47 // inputs
53 // inputs
48− const [currentTab, setCurrentTab] = useState(TABS.TICKET);
54+ const [currentTab, setCurrentTab] = useState(NAMES.TICKET);
49
55
50− // on-mount
56+ const goToActivate = useCallback(
51− useLifecycle(() => {
57+ () => replaceWith([{ key: names.LANDING }, { key: names.ACTIVATE }]),
52− resetWallet();
58+ [replaceWith, names]
53− });
59+ );
54
60
61+ const goToPoints = useCallback(() => {
62+ push(names.POINTS);
63+ }, [push, names]);
64+
65+ const goToPoint = useCallback(() => {
66+ goToPoints();
67+ push(names.POINT);
68+ }, [goToPoints, push, names]);
69+
55− const doContinue = async () => {
70+ const doContinue = useCallback(async () => {
56 const _wallet = need.wallet(wallet);
71 const _wallet = need.wallet(wallet);
57 const _contracts = need.contracts(contracts);
72 const _contracts = need.contracts(contracts);
58
73
59 // if no point cursor set by login logic, try to deduce it
74 // if no point cursor set by login logic, try to deduce it
60 let deduced = pointCursor;
75 let deduced = pointCursor;
61− if (Nothing.hasInstance(deduced)) {
76+ if (Maybe.Nothing.hasInstance(deduced)) {
62 const owned = await azimuth.azimuth.getOwnedPoints(
77 const owned = await azimuth.azimuth.getOwnedPoints(
63 _contracts,
78 _contracts,
64 _wallet.address
79 _wallet.address
65 );
80 );
66 if (owned.length === 1) {
81 if (owned.length === 1) {
67− deduced = Just(owned[0]);
82+ deduced = Maybe.Just(owned[0]);
68 } else if (owned.length === 0) {
83 } else if (owned.length === 0) {
69 const canOwn = await azimuth.azimuth.getTransferringFor(
84 const canOwn = await azimuth.azimuth.getTransferringFor(
70 _contracts,
85 _contracts,
71 _wallet.address
86 _wallet.address
72 );
87 );
73 if (canOwn.length === 1) {
88 if (canOwn.length === 1) {
74− deduced = Just(canOwn[0]);
89+ deduced = Maybe.Just(canOwn[0]);
75 }
90 }
76 }
91 }
77 }
92 }
78
93
79 // if we have a deduced point or one in the global context,
94 // if we have a deduced point or one in the global context,
80 // navigate to that specific point, otherwise navigate to list of points
95 // navigate to that specific point, otherwise navigate to list of points
81− if (Just.hasInstance(deduced)) {
96+ if (Maybe.Just.hasInstance(deduced)) {
82 setPointCursor(deduced);
97 setPointCursor(deduced);
83− history.popAndPush(ROUTE_NAMES.POINTS);
98+ goToPoint();
84− history.push(ROUTE_NAMES.POINT);
85 } else {
99 } else {
86− history.popAndPush(ROUTE_NAMES.POINTS);
100+ goToPoints();
87 }
101 }
88− };
102+ }, [contracts, pointCursor, setPointCursor, wallet, goToPoint, goToPoints]);
89
103
90 return (
104 return (
91 <View>
105 <View>
106+ <Grid>
107+ <Grid.Item full as={Crumbs} routes={[{ text: 'Multipass' }]} />
108+ <Grid.Item full as={H4} className="mt4">
92− <H1>Login</H1>
109+ Login
93−
110+ </Grid.Item>
111+
112+ <Grid.Item
113+ full
94− <Tabs
114+ as={Tabs}
95− tabViews={tabViews}
115+ className="mt1"
96− tabOptions={tabOptions}
116+ views={VIEWS}
117+ options={OPTIONS}
97− currentTab={currentTab}
118+ currentTab={currentTab}
98− onTabChange={setCurrentTab}
119+ onTabChange={setCurrentTab}
99− //
120+ />
121+
122+ <Grid.Item
123+ full
124+ as={ForwardButton}
125+ solid
126+ className="mt3"
127+ disabled={Maybe.Nothing.hasInstance(wallet)}
100− loginCompleted={doContinue}
128+ onClick={doContinue}>
101− />
129+ Continue
130+ </Grid.Item>
131+ </Grid>
132+
133+ <Footer>
134+ <Grid>
135+ <Grid.Divider />
136+ <Grid.Item full as={ForwardButton} onClick={goToActivate}>
137+ Activate
138+ </Grid.Item>
139+ <Grid.Divider />
140+ <Grid.Item full as={OfflineButton} />
141+ </Grid>
142+ </Footer>
102 </View>
143 </View>
103 );
144 );
104}
145}
src/views/Login/Advanced.js+31−110
@@ -1,115 +1,36 @@
1−import React from 'react';
1+import React, { useState } from 'react';
2−import { P, InnerLabelDropdown } from '../../components/old/Base';
3−import { Row, Col } from '../../components/old/Base';
4
2
5−import Ticket from './Ticket';
3+import Tabs from 'components/Tabs';
6−import Shards from './Shards';
7−import Ledger from './Ledger';
8−import Trezor from './Trezor';
9−import Mnemonic from './Mnemonic';
10import PrivateKey from './PrivateKey';
4import PrivateKey from './PrivateKey';
11import Keystore from './Keystore';
5import Keystore from './Keystore';
12
6
13−import { WALLET_TYPES, renderWalletType } from '../../lib/wallet';
7+const NAMES = {
14−import { compose } from '../../lib/lib';
8+ PRIVATE_KEY: 'PRIVATE_KEY',
15−import { withWallet } from '../../store/wallet';
9+ KEYSTORE: 'KEYSTORE',
16−
17−class Advanced extends React.Component {
18− constructor(props) {
19− super(props);
20−
21− this.state = {
22− selected: WALLET_TYPES.TICKET,
23− };
24−
25− this.handleSelection = this.handleSelection.bind(this);
26− }
27−
28− getWalletOptions() {
29− return [
30− {
31− title: 'Urbit Master Ticket',
32− value: WALLET_TYPES.TICKET,
33− },
34− {
35− title: 'Urbit Master Ticket (Shards)',
36− value: WALLET_TYPES.SHARDS,
37− },
10+};
38− {
11+
39− type: 'divider',
12+const VIEWS = {
40− },
13+ [NAMES.PRIVATE_KEY]: PrivateKey,
41− {
14+ [NAMES.KEYSTORE]: Keystore,
42− title: 'Ledger',
43− value: WALLET_TYPES.LEDGER,
44− },
45− {
46− title: 'Trezor',
47− value: WALLET_TYPES.TREZOR,
48− },
49− {
50− type: 'divider',
51− },
15+};
52− {
16+
53− title: 'BIP39 Mnemonic',
17+const OPTIONS = [
54− value: WALLET_TYPES.MNEMONIC,
55− },
56− {
57− title: 'Ethereum Private Key',
18+ { text: 'Ethereum Private Key', value: NAMES.PRIVATE_KEY },
58− value: WALLET_TYPES.PRIVATE_KEY,
59− },
60− {
61− title: 'Ethereum Keystore File',
19+ { text: 'Ethereum Keystore', value: NAMES.KEYSTORE },
62− value: WALLET_TYPES.KEYSTORE,
63− },
64− ];
20+];
65− }
21+
66−
22+export default function Advanced({ loginCompleted, className }) {
67− handleSelection(selected) {
23+ const [currentTab, setCurrentTab] = useState(NAMES.PRIVATE_KEY);
68− this.setState({ selected });
24+
69− }
70−
71− render() {
72− const loginCompleted = this.props.loginCompleted;
73− const inputs = (() => {
74− switch (this.state.selected) {
75− case WALLET_TYPES.TICKET:
76− return <Ticket advanced={true} loginCompleted={loginCompleted} />;
77− case WALLET_TYPES.SHARDS:
78− return <Shards loginCompleted={loginCompleted} />;
79− //
80− case WALLET_TYPES.LEDGER:
81− return <Ledger loginCompleted={loginCompleted} />;
82− case WALLET_TYPES.TREZOR:
83− return <Trezor loginCompleted={loginCompleted} />;
84− //
85− case WALLET_TYPES.MNEMONIC:
86− return <Mnemonic advanced={true} loginCompleted={loginCompleted} />;
87− case WALLET_TYPES.PRIVATE_KEY:
88− return <PrivateKey loginCompleted={loginCompleted} />;
89− case WALLET_TYPES.KEYSTORE:
90− return <Keystore loginCompleted={loginCompleted} />;
91− //
92− default:
93− return <P>{'Coming soon.'}</P>;
94− }
95− })();
96−
97− return (
25+ return (
98− <Row>
26+ <Tabs
99− <Col>
100− <InnerLabelDropdown
101− className={'mb-10 mt-6'}
27+ className={className}
102− title={'Wallet Type:'}
28+ views={VIEWS}
103− options={this.getWalletOptions()}
29+ options={OPTIONS}
104− handleUpdate={this.handleSelection}
30+ currentTab={currentTab}
105− currentSelectionTitle={renderWalletType(this.state.selected)}
31+ onTabChange={setCurrentTab}
106− />
32+ //
107−
33+ loginCompleted={loginCompleted}
108− {inputs}
34+ />
109− </Col>
110− </Row>
111− );
35+ );
112− }
113}
36}
114−
115−export default compose(withWallet)(Advanced);
src/views/Login/Hardware.jsnew file+36
@@ -0,0 +1,36 @@
1+import React, { useState } from 'react';
2+
3+import Tabs from 'components/Tabs';
4+import Ledger from './Ledger';
5+import Trezor from './Trezor';
6+
7+const NAMES = {
8+ LEDGER: 'LEDGER',
9+ TREZOR: 'TREZOR',
10+};
11+
12+const VIEWS = {
13+ [NAMES.LEDGER]: Ledger,
14+ [NAMES.TREZOR]: Trezor,
15+};
16+
17+const OPTIONS = [
18+ { text: 'Ledger', value: NAMES.LEDGER },
19+ { text: 'Trezor', value: NAMES.TREZOR },
20+];
21+
22+export default function Hardware({ loginCompleted, className }) {
23+ const [currentTab, setCurrentTab] = useState(NAMES.LEDGER);
24+
25+ return (
26+ <Tabs
27+ className={className}
28+ views={VIEWS}
29+ options={OPTIONS}
30+ currentTab={currentTab}
31+ onTabChange={setCurrentTab}
32+ //
33+ loginCompleted={loginCompleted}
34+ />
35+ );
36+}
src/views/Login/Keystore.js+40−69
@@ -1,31 +1,28 @@
1−import { Just, Nothing } from 'folktale/maybe';
2−import { Ok, Error } from 'folktale/result';
3import React, { useState } from 'react';
1import React, { useState } from 'react';
2+import Maybe from 'folktale/maybe';
4import * as keythereum from 'keythereum';
3import * as keythereum from 'keythereum';
5−import { H3, Input } from 'indigo-react';
4+import { P, Grid, Input, ErrorText } from 'indigo-react';
6
5
7−import View from 'components/View';
8import { usePassphraseInput } from 'components/Inputs';
6import { usePassphraseInput } from 'components/Inputs';
9import { ForwardButton } from 'components/Buttons';
7import { ForwardButton } from 'components/Buttons';
10−import { InputCaption, UploadButton, Warning } from 'components/old/Base';
11
8
12import { useWallet } from 'store/wallet';
9import { useWallet } from 'store/wallet';
13
10
14import * as need from 'lib/need';
11import * as need from 'lib/need';
15import { EthereumWallet, WALLET_TYPES } from 'lib/wallet';
12import { EthereumWallet, WALLET_TYPES } from 'lib/wallet';
16−import useWalletType from 'lib/useWalletType';
13+import UploadButton from 'components/UploadButton';
17−import useResetPointCursor from 'lib/useResetPointCursor';
14+import useLoginView from 'lib/useLoginView';
18
15
19−export default function Keystore({ loginCompleted }) {
16+export default function Keystore({ className }) {
20− useResetPointCursor();
21− useWalletType(WALLET_TYPES.KEYSTORE);
17+ useLoginView(WALLET_TYPES.KEYSTORE);
22
18
23 // globals
19 // globals
24− const { wallet, setWallet } = useWallet();
20+ const { setWallet } = useWallet();
25
21
22+ const [error, setError] = useState();
26 // inputs
23 // inputs
27− // keystore: Maybe<Result<String, String>>
24+ // keystore: Maybe<String>
28− const [keystore, setKeystore] = useState(Nothing());
25+ const [keystore, setKeystore] = useState(Maybe.Nothing());
29 const passphraseInput = usePassphraseInput({
26 const passphraseInput = usePassphraseInput({
30 name: 'password',
27 name: 'password',
31 label: 'Keystore password',
28 label: 'Keystore password',
@@ -33,8 +30,6 @@ export default function Keystore({ loginCompleted }) {
33 });
30 });
34 const passphrase = passphraseInput.data;
31 const passphrase = passphraseInput.data;
35
32
36− const [decryptionProblem, setDecryptionProblem] = useState(false);
37−
38 const constructWallet = () => {
33 const constructWallet = () => {
39 try {
34 try {
40 const text = need.keystore(keystore);
35 const text = need.keystore(keystore);
@@ -43,26 +38,27 @@ export default function Keystore({ loginCompleted }) {
43 const privateKey = keythereum.recover(passphrase, json);
38 const privateKey = keythereum.recover(passphrase, json);
44
39
45 const newWallet = new EthereumWallet(privateKey);
40 const newWallet = new EthereumWallet(privateKey);
46− setDecryptionProblem(false);
41+ setError();
47− setWallet(Just(newWallet));
42+ setWallet(Maybe.Just(newWallet));
48 } catch (err) {
43 } catch (err) {
49− setDecryptionProblem(true);
44+ setError(
45+ "Couldn't decrypt wallet. You may have entered an incorrect password."
46+ );
50− setWallet(Nothing());
47+ setWallet(Maybe.Nothing());
51 }
48 }
52 };
49 };
53
50
54− const handleKeystoreUpload = event => {
51+ const handleKeystoreUpload = element => {
55− const file = event.files.item(0);
52+ const file = element.files.item(0);
56 const reader = new FileReader();
53 const reader = new FileReader();
57
54
58 reader.onload = e => {
55 reader.onload = e => {
59 const keystore = e.target.result;
56 const keystore = e.target.result;
60− setKeystore(Just(Ok(keystore)));
57+ setKeystore(Maybe.Just(keystore));
61 };
58 };
62
59
63 const failure = _ => {
60 const failure = _ => {
64− const message = 'There was a problem uploading your Keystore file';
61+ setError('There was a problem uploading your Keystore file');
65− setKeystore(Just(Error(message)));
66 };
62 };
67
63
68 reader.onerror = failure;
64 reader.onerror = failure;
@@ -71,59 +67,34 @@ export default function Keystore({ loginCompleted }) {
71 reader.readAsText(file);
67 reader.readAsText(file);
72 };
68 };
73
69
74− const uploadButtonClass = keystore.matchWith({
75− Nothing: _ => 'bg-blue white',
76− Just: ks =>
77− ks.value.matchWith({
78− Ok: _ => 'bg-green white',
79− Error: _ => 'bg-yellow black',
80− }),
81− });
82−
83− const decryptMessage =
84− decryptionProblem === false ? (
85− <div />
86− ) : (
87− <Warning className="mt-8">
88− <H3 style={{ marginTop: 0, paddingTop: 0 }}>
89− Couldn't decrypt wallet.
90− </H3>
91− You may have entered an incorrect password.
92− </Warning>
93− );
94−
95 return (
70 return (
96− <View>
71+ <Grid className={className}>
97− <InputCaption>
72+ <Grid.Item full as={P}>
98 Please upload your Ethereum keystore file. If your keystore file is
73 Please upload your Ethereum keystore file. If your keystore file is
99 encrypted with a password, you'll also need to enter that below.
74 encrypted with a password, you'll also need to enter that below.
100− </InputCaption>
75+ </Grid.Item>
101
76
102− <UploadButton
103− className={`${uploadButtonClass} mt3`}
104− onChange={handleKeystoreUpload}>
77+ <Grid.Item full as={UploadButton} onChange={handleKeystoreUpload}>
105− <div className="flex-center-all fs-4 h-11 pointer">
106− Upload Keystore file
78+ Upload Keystore file
107− </div>
79+ </Grid.Item>
108− </UploadButton>
109
80
110− <Input {...passphraseInput} />
81+ {error && (
82+ <Grid.Item full as={ErrorText} className="mt1">
83+ {error}
84+ </Grid.Item>
85+ )}
111
86
87+ <Grid.Item full as={Input} className="mt3" {...passphraseInput} />
88+
89+ <Grid.Item
90+ full
112− <ForwardButton
91+ as={ForwardButton}
92+ solid
113 className="mt3"
93 className="mt3"
114− disabled={Nothing.hasInstance(keystore)}
94+ disabled={Maybe.Nothing.hasInstance(keystore)}
115 onClick={constructWallet}>
95 onClick={constructWallet}>
116 Decrypt
96 Decrypt
117− </ForwardButton>
97+ </Grid.Item>
118−
98+ </Grid>
119− {decryptMessage}
120−
121− <ForwardButton
122− className="mt3"
123− disabled={Nothing.hasInstance(wallet)}
124− onClick={loginCompleted}>
125− Continue
126− </ForwardButton>
127− </View>
128 );
99 );
129}
100}
src/views/Login/Ledger.js+106−114
@@ -1,22 +1,20 @@
1−import * as bip32 from 'bip32';
2import React, { useState } from 'react';
1import React, { useState } from 'react';
3−import { times } from 'lodash';
4import Maybe from 'folktale/maybe';
2import Maybe from 'folktale/maybe';
3+import { P, Input, Grid, H4, H5 } from 'indigo-react';
4+import { times } from 'lodash';
5+import * as bip32 from 'bip32';
5import Transport from '@ledgerhq/hw-transport-u2f';
6import Transport from '@ledgerhq/hw-transport-u2f';
6import Eth from '@ledgerhq/hw-app-eth';
7import Eth from '@ledgerhq/hw-app-eth';
7import * as secp256k1 from 'secp256k1';
8import * as secp256k1 from 'secp256k1';
8−import { H1, H2, P, Input } from 'indigo-react';
9
9
10−import View from 'components/View';
11import { ForwardButton } from 'components/Buttons';
10import { ForwardButton } from 'components/Buttons';
12import { InnerLabelDropdown } from 'components/old/Base';
11import { InnerLabelDropdown } from 'components/old/Base';
13
12
14import { useWallet } from 'store/wallet';
13import { useWallet } from 'store/wallet';
15
14
16import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from 'lib/ledger';
15import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from 'lib/ledger';
17−import useWalletType from 'lib/useWalletType';
18import { WALLET_TYPES } from 'lib/wallet';
16import { WALLET_TYPES } from 'lib/wallet';
19−import useResetPointCursor from 'lib/useResetPointCursor';
17+import useLoginView from 'lib/useLoginView';
20
18
21const pathOptions = [
19const pathOptions = [
22 { title: 'Ledger Live', value: LEDGER_LIVE_PATH },
20 { title: 'Ledger Live', value: LEDGER_LIVE_PATH },
@@ -24,15 +22,18 @@ const pathOptions = [
24 { title: 'Custom path', value: 'custom' },
22 { title: 'Custom path', value: 'custom' },
25];
23];
26
24
27−const chopHdPrefix = str => (str.slice(0, 2) === 'm/' ? str.slice(2) : str);
25+const accountOptions = times(20, i => ({
26+ title: `Account #${i + 1}`,
27+ value: i,
28+}));
28
29
30+const chopHdPrefix = str => (str.slice(0, 2) === 'm/' ? str.slice(2) : str);
29const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
31const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
30
32
31−export default function Ledger({ loginCompleted }) {
33+export default function Ledger({ className }) {
32− useResetPointCursor();
33− useWalletType(WALLET_TYPES.LEDGER);
34+ useLoginView(WALLET_TYPES.LEDGER);
34
35
35− const { wallet, setWallet, setWalletHdPath } = useWallet();
36+ const { setWallet, setWalletHdPath } = useWallet();
36
37
37 const [basePath, setBasePath] = useState(LEDGER_LIVE_PATH);
38 const [basePath, setBasePath] = useState(LEDGER_LIVE_PATH);
38 const [account, setAccount] = useState(0);
39 const [account, setAccount] = useState(0);
@@ -75,116 +76,107 @@ export default function Ledger({ loginCompleted }) {
75 };
76 };
76
77
77 const basePathTitle = pathOptions.find(o => o.value === basePath).title;
78 const basePathTitle = pathOptions.find(o => o.value === basePath).title;
78−
79− const accountOptions = times(20, i => ({
80− title: `Account #${i + 1}`,
81− value: i,
82− }));
83 const accountTitle = accountOptions.find(o => o.value === account).title;
79 const accountTitle = accountOptions.find(o => o.value === account).title;
84
80
85− const basePathSelection = (
81+ const isHTTPS = document.location.protocol === 'https:';
86− <InnerLabelDropdown
82+
87− className="mt-8"
83+ // when not on https, tell user how to get there
88− options={pathOptions}
84+ const renderHTTP = () => (
89− handleUpdate={handlePathSelection}
85+ <>
90− title="Derivation path"
86+ <Grid.Item full as={H5}>
91− currentSelectionTitle={basePathTitle}
87+ Running on HTTP?
92− fullWidth={true}
88+ </Grid.Item>
93− />
89+
90+ <Grid.Item full as={P}>
91+ To authenticate and sign transactions with a Ledger, Bridge must be
92+ serving over HTTPS on localhost. You can do this via the following:
93+ </Grid.Item>
94+
95+ <Grid.Item full as="ol">
96+ <li>
97+ Install{' '}
98+ <a
99+ target="_blank"
100+ href="https://github.com/FiloSottile/mkcert"
101+ rel="noopener noreferrer">
102+ mkcert
103+ </a>
104+ </li>
105+ <li className="mt3">
106+ Install a local certificate authority via <code>mkcert -install</code>
107+ </li>
108+ <li className="mt3">
109+ In your <code>bridge</code> directory, generate a certificate valid
110+ for localhost via <code>mkcert localhost</code>. This will produce two
111+ files: <code>localhost.pem</code>, the local certificate, and
112+ <code>localhost-key.pem</code> , its corresponding private key.
113+ </li>
114+ <li className="mt3">
115+ Run <code>python bridge-https.py</code>
116+ </li>
117+ </Grid.Item>
118+ </>
94 );
119 );
95
120
96− const truePathSelection =
121+ const renderHTTPS = () => (
97− basePath === 'custom' ? (
122+ <>
98− <Input
123+ <Grid.Item full as={P}>
124+ Connect and authenticate to your Ledger, and then open the "Ethereum"
125+ application. If you're running on older firmware, make sure the "browser
126+ support" option is turned on. To sign transactions, you'll also need to
127+ enable the "contract data" option.
128+ </Grid.Item>
129+
130+ <Grid.Item full as={P}>
131+ If you'd like to use a custom derivation path, you may enter it below.
132+ </Grid.Item>
133+
134+ <Grid.Item
135+ full
136+ as={InnerLabelDropdown}
99− className="mt3"
137+ className="mv4"
100− name="hdPath"
138+ title="Derivation path"
101− label="HD path"
139+ options={pathOptions}
102− autocomplete="off"
140+ handleUpdate={handlePathSelection}
103− initialValue={addHdPrefix(hdPath)}
141+ currentSelectionTitle={basePathTitle}
104− onValue={setHdPath}
105 />
142 />
106− ) : (
107− <InnerLabelDropdown
108− className="mt-4"
109− prop-size="md"
110− prop-format="innerLabel"
111− options={accountOptions}
112− handleUpdate={handleAccountSelection}
113− title="Account"
114− currentSelectionTitle={accountTitle}
115− fullWidth={true}
116− />
117− );
118
143
119− // when not on https, tell user how to get there
144+ {basePath === 'custom' ? (
120− const body =
145+ <Grid.Item
121− document.location.protocol !== 'https:' ? (
146+ full
122− <>
147+ as={Input}
123− <H2>Running on HTTP?</H2>
148+ className="mv3"
124−
149+ name="hdPath"
125− <P>
150+ label="HD path"
126− To authenticate and sign transactions with a Ledger, Bridge must be
151+ autoComplete="off"
127− serving over HTTPS on localhost. You can do this via the following:
152+ initialValue={addHdPrefix(hdPath)}
128− </P>
153+ onValue={setHdPath}
129−
154+ />
130− <ol className="measure-md">
155+ ) : (
131− <li className="mt-4">
156+ <Grid.Item
132− Install
157+ full
133− <a
158+ as={InnerLabelDropdown}
134− target="_blank"
159+ className="mt4"
135− href="https://github.com/FiloSottile/mkcert"
160+ title="Account"
136− rel="noopener noreferrer">
161+ options={accountOptions}
137− mkcert
162+ handleUpdate={handleAccountSelection}
138− </a>
163+ currentSelectionTitle={accountTitle}
139− </li>
164+ />
140− <li className="mt-4">
165+ )}
141− Install a local certificate authority via{' '}
166+
142− <code>mkcert -install</code>
143− </li>
144− <li className="mt-4">
145− In your <code>bridge</code> directory, generate a certificate valid
146− for localhost via <code>mkcert localhost</code>. This will produce
147− two files: <code>localhost.pem</code>, the local certificate, and
148− <code>localhost-key.pem</code> , its corresponding private key.
149− </li>
150− <li className="mt-4">
151− Run <code>python bridge-https.py</code>
152− </li>
153− </ol>
154− </>
155− ) : (
156− <>
157− <P>
158− Connect and authenticate to your Ledger, and then open the "Ethereum"
159− application. If you're running on older firmware, make sure the
160− "browser support" option is turned on. To sign transactions, you'll
161− also need to enable the "contract data" option.
162− </P>
163−
164− <P>
165− If you'd like to use a custom derivation path, you may enter it below.
166− </P>
167−
168− {basePathSelection}
169− {truePathSelection}
170−
171− <ForwardButton className={'mt3'} onClick={pollDevice}>
167+ <ForwardButton className="mt3" onClick={pollDevice}>
172− {'Authenticate'}
168+ Authenticate
173− </ForwardButton>
169+ </ForwardButton>
174−
175− <ForwardButton
176− className={'mt3'}
177− disabled={Maybe.Nothing.hasInstance(wallet)}
178− onClick={loginCompleted}>
179− {'Continue'}
180− </ForwardButton>
181− </>
170+ </>
182− );
171+ );
183
172
184 return (
173 return (
185− <View>
174+ <Grid className={className}>
175+ <Grid.Item full as={H4}>
186− <H1>Authenticate With Your Ledger</H1>
176+ Authenticate With Your Ledger
187− {body}
177+ </Grid.Item>
188− </View>
178+ {isHTTPS && renderHTTPS()}
179+ {!isHTTPS && renderHTTP()}
180+ </Grid>
189 );
181 );
190}
182}
src/views/Login/Mnemonic.js+17−23
@@ -1,27 +1,23 @@
1import React, { useCallback, useEffect } from 'react';
1import React, { useCallback, useEffect } from 'react';
2import { Just, Nothing } from 'folktale/maybe';
2import { Just, Nothing } from 'folktale/maybe';
3−import { Input } from 'indigo-react';
3+import { Grid, Input, CheckboxInput } from 'indigo-react';
4
4
5−import View from 'components/View';
6import {
5import {
7 usePassphraseInput,
6 usePassphraseInput,
8 useMnemonicInput,
7 useMnemonicInput,
9 useHdPathInput,
8 useHdPathInput,
9+ useCheckboxInput,
10} from 'components/Inputs';
10} from 'components/Inputs';
11−import { ForwardButton } from 'components/Buttons';
12
11
13import { useWallet } from 'store/wallet';
12import { useWallet } from 'store/wallet';
14
13
15import { walletFromMnemonic, WALLET_TYPES } from 'lib/wallet';
14import { walletFromMnemonic, WALLET_TYPES } from 'lib/wallet';
16−import useWalletType from 'lib/useWalletType';
15+import useLoginView from 'lib/useLoginView';
17−import useResetPointCursor from 'lib/useResetPointCursor';
18
16
19−export default function Mnemonic({ advanced, loginCompleted }) {
17+export default function Mnemonic({ className }) {
20− useResetPointCursor();
21− useWalletType(WALLET_TYPES.MNEMONIC);
18+ useLoginView(WALLET_TYPES.MNEMONIC);
22
19
23 const {
20 const {
24− wallet,
25 setWallet,
21 setWallet,
26 authMnemonic,
22 authMnemonic,
27 setAuthMnemonic,
23 setAuthMnemonic,
@@ -29,6 +25,11 @@ export default function Mnemonic({ advanced, loginCompleted }) {
29 setWalletHdPath,
25 setWalletHdPath,
30 } = useWallet();
26 } = useWallet();
31
27
28+ const advancedInput = useCheckboxInput({
29+ name: 'advanced',
30+ label: 'Passphrase & HD Path',
31+ initialValue: false,
32+ });
32 const mnemonic = authMnemonic.getOrElse('');
33 const mnemonic = authMnemonic.getOrElse('');
33 // TODO: move this into transformers?
34 // TODO: move this into transformers?
34 // transform the result of the mnemonic to Maybe<string>
35 // transform the result of the mnemonic to Maybe<string>
@@ -69,24 +70,17 @@ export default function Mnemonic({ advanced, loginCompleted }) {
69 }, [mnemonic, passphrase, walletHdPath, setWallet]);
70 }, [mnemonic, passphrase, walletHdPath, setWallet]);
70
71
71 return (
72 return (
72− <View>
73+ <Grid className={className}>
73− <Input {...mnemonicInput} />
74+ <Grid.Item full as={Input} {...mnemonicInput} />
74
75
75− {!advanced ? null : (
76+ {advancedInput.data && (
76 <>
77 <>
77− <Input {...passphraseInput} />
78+ <Grid.Item full as={Input} {...passphraseInput} />
78−
79− <Input {...hdPathInput} />
79+ <Grid.Item full as={Input} {...hdPathInput} />
80 </>
80 </>
81 )}
81 )}
82
82
83− <ForwardButton
83+ <Grid.Item as={CheckboxInput} {...advancedInput} full />
84− className="mt3"
84+ </Grid>
85− disabled={Nothing.hasInstance(wallet)}
86− onClick={loginCompleted}
87− solid>
88− Continue
89− </ForwardButton>
90− </View>
91 );
85 );
92}
86}
src/views/Login/PrivateKey.js+9−19
@@ -1,21 +1,18 @@
1import { Just, Nothing } from 'folktale/maybe';
1import { Just, Nothing } from 'folktale/maybe';
2import React, { useEffect } from 'react';
2import React, { useEffect } from 'react';
3−import { Input } from 'indigo-react';
3+import { Grid, Input } from 'indigo-react';
4
4
5−import View from 'components/View';
6import { usePassphraseInput } from 'components/Inputs';
5import { usePassphraseInput } from 'components/Inputs';
7−import { ForwardButton } from 'components/Buttons';
8
6
9import { useWallet } from 'store/wallet';
7import { useWallet } from 'store/wallet';
10
8
11import { EthereumWallet, WALLET_TYPES } from 'lib/wallet';
9import { EthereumWallet, WALLET_TYPES } from 'lib/wallet';
12−import useWalletType from 'lib/useWalletType';
10+import useLoginView from 'lib/useLoginView';
13−import useResetPointCursor from 'lib/useResetPointCursor';
14
11
15−export default function PrivateKey({ loginCompleted }) {
12+export default function PrivateKey({ className }) {
16− useResetPointCursor();
17− useWalletType(WALLET_TYPES.PRIVATE_KEY);
13+ useLoginView(WALLET_TYPES.PRIVATE_KEY);
14+
18− const { wallet, setWallet } = useWallet();
15+ const { setWallet } = useWallet();
19
16
20 const privateKeyInput = usePassphraseInput({
17 const privateKeyInput = usePassphraseInput({
21 name: 'privateKey',
18 name: 'privateKey',
@@ -35,15 +32,8 @@ export default function PrivateKey({ loginCompleted }) {
35 }, [privateKey, setWallet]);
32 }, [privateKey, setWallet]);
36
33
37 return (
34 return (
38− <View>
35+ <Grid className={className}>
39− Please enter your raw Ethereum private key here.
40− <Input {...privateKeyInput} />
36+ <Grid.Item full as={Input} {...privateKeyInput} />
41− <ForwardButton
37+ </Grid>
42− className="mt3"
43− disabled={Nothing.hasInstance(wallet)}
44− onClick={loginCompleted}>
45− Continue
46− </ForwardButton>
47− </View>
48 );
38 );
49}
39}
src/views/Login/Shards.js+4−6
@@ -14,12 +14,10 @@ import { ForwardButton } from 'components/Buttons';
14import { useWallet } from 'store/wallet';
14import { useWallet } from 'store/wallet';
15
15
16import { urbitWalletFromTicket, WALLET_TYPES } from 'lib/wallet';
16import { urbitWalletFromTicket, WALLET_TYPES } from 'lib/wallet';
17−import useWalletType from 'lib/useWalletType';
17+import useLoginView from 'lib/useLoginView';
18−import useResetPointCursor from 'lib/useResetPointCursor';
19
18
20export default function Shards({ loginCompleted }) {
19export default function Shards({ loginCompleted }) {
21− useResetPointCursor();
22− useWalletType(WALLET_TYPES.SHARDS);
20+ useLoginView(WALLET_TYPES.SHARDS);
23
21
24 const { wallet, setUrbitWallet } = useWallet();
22 const { wallet, setUrbitWallet } = useWallet();
25
23
@@ -72,7 +70,7 @@ export default function Shards({ loginCompleted }) {
72 const ready = shards.filter(x => x !== '').length > 1;
70 const ready = shards.filter(x => x !== '').length > 1;
73
71
74 return (
72 return (
75− <View>
73+ <>
76 Enter your point and at least two of your three Urbit master ticket shards
74 Enter your point and at least two of your three Urbit master ticket shards
77 here. The index of the input field needs to match the index of the shard.
75 here. The index of the input field needs to match the index of the shard.
78 <Input {...pointInput} />
76 <Input {...pointInput} />
@@ -93,6 +91,6 @@ export default function Shards({ loginCompleted }) {
93 onClick={loginCompleted}>
91 onClick={loginCompleted}>
94 Continue
92 Continue
95 </ForwardButton>
93 </ForwardButton>
96− </View>
94+ </>
97 );
95 );
98}
96}
src/views/Login/Ticket.js+160−94
@@ -1,16 +1,16 @@
1−import { Just, Nothing } from 'folktale/maybe';
1+import Maybe from 'folktale/maybe';
2import React, { useCallback, useState, useEffect } from 'react';
2import React, { useCallback, useState, useEffect } from 'react';
3import * as azimuth from 'azimuth-js';
3import * as azimuth from 'azimuth-js';
4import * as ob from 'urbit-ob';
4import * as ob from 'urbit-ob';
5+import * as kg from 'urbit-key-generation/dist/index';
5−import { Input, AccessoryIcon } from 'indigo-react';
6+import { Input, Grid, CheckboxInput } from 'indigo-react';
6
7
7−import View from 'components/View';
8import {
8import {
9 usePointInput,
9 usePointInput,
10 useTicketInput,
10 useTicketInput,
11 usePassphraseInput,
11 usePassphraseInput,
12+ useCheckboxInput,
12} from 'components/Inputs';
13} from 'components/Inputs';
13−import { ForwardButton } from 'components/Buttons';
14
14
15import { useNetwork } from 'store/network';
15import { useNetwork } from 'store/network';
16import { useWallet } from 'store/wallet';
16import { useWallet } from 'store/wallet';
@@ -18,123 +18,189 @@ import { usePointCursor } from 'store/pointCursor';
18
18
19import * as need from 'lib/need';
19import * as need from 'lib/need';
20import { WALLET_TYPES, urbitWalletFromTicket } from 'lib/wallet';
20import { WALLET_TYPES, urbitWalletFromTicket } from 'lib/wallet';
21−import useWalletType from 'lib/useWalletType';
21+import useImpliedPoint from 'lib/useImpliedPoint';
22−import useResetPointCursor from 'lib/useResetPointCursor';
22+import useLoginView from 'lib/useLoginView';
23−
23+
24−//TODO should be part of InputWithStatus component
25−const INPUT_STATUS = {
26− SPIN: Symbol('SPIN'),
27− GOOD: Symbol('GOOD'),
28− FAIL: Symbol('FAIL'),
29−};
30−
31−export default function Ticket({ advanced, loginCompleted }) {
24+export default function Ticket({ className }) {
32− useResetPointCursor();
33− useWalletType(WALLET_TYPES.TICKET);
25+ useLoginView(WALLET_TYPES.TICKET);
34 // globals
26 // globals
35 const { contracts } = useNetwork();
27 const { contracts } = useNetwork();
36− const { wallet, setUrbitWallet } = useWallet();
28+ const { setUrbitWallet } = useWallet();
37 const { setPointCursor } = usePointCursor();
29 const { setPointCursor } = usePointCursor();
30+ const impliedPoint = useImpliedPoint();
38
31
39 // inputs
32 // inputs
33+
34+ // point
40 const pointInput = usePointInput({
35 const pointInput = usePointInput({
41 name: 'point',
36 name: 'point',
42− //TODO deduce point name from URL if we can, prefill input if we found it
37+ initialValue: impliedPoint,
43 autoFocus: true,
38 autoFocus: true,
44 });
39 });
45 const pointName = pointInput.data;
40 const pointName = pointInput.data;
46
41
42+ // passphrase
43+ const passphraseInput = usePassphraseInput({
44+ name: 'passphrase',
45+ label: '(Optional) Wallet Passphrase',
46+ });
47+ const passphrase = passphraseInput.data;
48+
49+ const hasPassphraseInput = useCheckboxInput({
50+ name: 'has-passphrase',
51+ label: 'Passphrase',
52+ initialValue: false,
53+ });
54+
55+ // ticket
56+ const [error, setError] = useState();
57+ const [deriving, setDeriving] = useState(false);
47 const ticketInput = useTicketInput({
58 const ticketInput = useTicketInput({
48 name: 'ticket',
59 name: 'ticket',
49 label: 'Master Ticket',
60 label: 'Master Ticket',
61+ error,
62+ deriving,
50 });
63 });
51 const ticket = ticketInput.data;
64 const ticket = ticketInput.data;
52
65
53− const passphraseInput = usePassphraseInput({
66+ // shards
54− name: 'passphrase',
67+ const shardsInput = useCheckboxInput({
55− label: '(Optional) Wallet Passphrase',
68+ name: 'shards',
69+ label: 'Shards',
70+ initialValue: false,
56 });
71 });
57− const passphrase = passphraseInput.data;
58
72
59− // display state
73+ const shard1Input = useTicketInput({
60− const [ticketStatus, setTicketStatus] = useState(Nothing());
74+ name: 'shard1',
61−
75+ label: 'Shard 1',
76+ });
77+ const shard1 = shard1Input.data;
78+ const shard2Input = useTicketInput({
79+ name: 'shard2',
80+ label: 'Shard 2',
81+ });
82+ const shard2 = shard2Input.data;
83+ const shard3Input = useTicketInput({
84+ name: 'shard3',
85+ label: 'Shard 3',
86+ });
87+ const shard3 = shard3Input.data;
88+
89+ const isUsingShards = !!shardsInput.data;
90+ const shards = [shard1, shard2, shard3];
91+ const shardsReady = shards.filter(x => x !== '').length > 1;
92+
62− //TODO maybe want to do this only on-go, because wallet derivation is slow...
93+ // TODO: maybe want to do this only on-go, because wallet derivation is slow...
63− const verifyTicket = useCallback(
94+ const deriveWalletFromTicket = useCallback(async () => {
64− async (pointName, ticket, passphrase) => {
95+ // clear states
96+ setError();
97+ setDeriving(true);
65− setUrbitWallet(Nothing());
98+ setUrbitWallet(Maybe.Nothing());
99+
66− if (!ob.isValidPatq(ticket) || !ob.isValidPatp(pointName)) {
100+ if (!ob.isValidPatq(ticket) || !ob.isValidPatp(pointName)) {
67− setTicketStatus(Nothing());
101+ setDeriving(false);
68− return;
102+ return;
69− }
103+ }
70− setTicketStatus(Just(INPUT_STATUS.SPIN));
104+
71− const _contracts = need.contracts(contracts);
105+ const _contracts = need.contracts(contracts);
72− const pointNumber = ob.patp2dec(pointName);
106+ const pointNumber = ob.patp2dec(pointName);
73− const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
107+ const urbitWallet = await urbitWalletFromTicket(
108+ ticket,
109+ pointName,
110+ passphrase
111+ );
112+ const [isOwner, isTransferProxy] = await Promise.all([
74− const isOwner = azimuth.azimuth.isOwner(
113+ azimuth.azimuth.isOwner(
75 _contracts,
114 _contracts,
76 pointNumber,
115 pointNumber,
77− uhdw.ownership.keys.address
116+ urbitWallet.ownership.keys.address
78− );
117+ ),
79− const isTransferProxy = azimuth.azimuth.isTransferProxy(
118+ azimuth.azimuth.isTransferProxy(
80 _contracts,
119 _contracts,
81 pointNumber,
120 pointNumber,
82− uhdw.ownership.keys.address
121+ urbitWallet.ownership.keys.address
122+ ),
123+ ]);
124+
125+ if (!isOwner && !isTransferProxy) {
126+ setError(
127+ 'This ticket is not the owner of or transfer proxy for this point.'
83 );
128 );
84− setUrbitWallet(Just(uhdw));
129+ setDeriving(false);
85− const newStatus =
130+ return;
86− (await isOwner) || (await isTransferProxy)
131+ }
87− ? INPUT_STATUS.GOOD
132+
88− : INPUT_STATUS.FAIL;
133+ setUrbitWallet(Maybe.Just(urbitWallet));
89− setTicketStatus(Just(newStatus));
134+ setPointCursor(Maybe.Just(ob.patp2dec(pointName)));
135+ setDeriving(false);
90− },
136+ }, [
137+ pointName,
138+ ticket,
139+ passphrase,
140+ contracts,
141+ setUrbitWallet,
142+ setPointCursor,
143+ setDeriving,
144+ ]);
145+
146+ const deriveWalletFromShards = useCallback(async () => {
147+ if (!shardsReady) {
148+ return;
149+ }
150+
151+ const s1 = shard1 || undefined;
152+ const s2 = shard2 || undefined;
153+ const s3 = shard3 || undefined;
154+
155+ try {
156+ const ticket = kg.combine([s1, s2, s3]);
157+ const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
91− [contracts, setUrbitWallet, setTicketStatus]
158+ setUrbitWallet(Maybe.Just(uhdw));
92− );
159+ } catch (_) {
160+ // do nothing
161+ }
162+ }, [
163+ passphrase,
164+ pointName,
165+ shardsReady,
166+ setUrbitWallet,
167+ shard1,
168+ shard2,
169+ shard3,
170+ ]);
171+
172+ // use shards if
173+ useEffect(() => {
174+ if (isUsingShards && shardsReady) {
175+ deriveWalletFromShards();
176+ }
177+ }, [isUsingShards, shardsReady, deriveWalletFromShards]);
93
178
179+ // verify ticket if not using shards
94 useEffect(() => {
180 useEffect(() => {
95− verifyTicket(pointName, ticket, passphrase);
181+ if (!isUsingShards) {
96− }, [verifyTicket, pointName, ticket, passphrase]);
182+ deriveWalletFromTicket();
97−
183+ }
98− const canContinue = () => {
184+ }, [isUsingShards, deriveWalletFromTicket]);
99− // this is our only requirement, since we still want people with
100− // non-standard wallet setups to be able to log in
101− return Just.hasInstance(wallet);
102− };
103−
104− const doContinue = () => {
105− setPointCursor(Just(ob.patp2dec(pointName)));
106− loginCompleted();
107− };
108−
109− //TODO integrate into TicketInput?
110− const displayTicketStatus = ticketStatus.matchWith({
111− Nothing: () => <span />,
112− Just: status => {
113− switch (status.value) {
114− case INPUT_STATUS.SPIN:
115− return <AccessoryIcon.Pending />;
116− case INPUT_STATUS.GOOD:
117− return <AccessoryIcon.Success />;
118− case INPUT_STATUS.FAIL:
119− return <AccessoryIcon.Failure />;
120− default:
121− return null;
122− }
123− },
124− });
125
185
126 return (
186 return (
127− <View>
187+ <Grid className={className}>
128− <Input {...pointInput} />
188+ <Grid.Item full as={Input} {...pointInput} />
129− <Input {...ticketInput} accessory={displayTicketStatus} />
189+
190+ {!isUsingShards && <Grid.Item full as={Input} {...ticketInput} />}
191+ {isUsingShards && (
192+ <>
193+ <Grid.Item full as={Input} {...shard1Input} />
194+ <Grid.Item full as={Input} {...shard2Input} />
195+ <Grid.Item full as={Input} {...shard3Input} />
196+ </>
197+ )}
198+ {hasPassphraseInput.data && (
130− {advanced && <Input {...passphraseInput} />}
199+ <Grid.Item full as={Input} {...passphraseInput} />
131−
200+ )}
132− <ForwardButton
201+
133− className={'mt3'}
202+ <Grid.Item full as={CheckboxInput} {...hasPassphraseInput} />
134− disabled={!canContinue()}
203+ <Grid.Item full as={CheckboxInput} {...shardsInput} />
135− onClick={doContinue}>
204+ </Grid>
136− Continue
137− </ForwardButton>
138− </View>
139 );
205 );
140}
206}
src/views/Login/Trezor.js+44−51
@@ -4,29 +4,26 @@ import Maybe from 'folktale/maybe';
4import { times } from 'lodash';
4import { times } from 'lodash';
5import TrezorConnect from 'trezor-connect';
5import TrezorConnect from 'trezor-connect';
6import * as secp256k1 from 'secp256k1';
6import * as secp256k1 from 'secp256k1';
7−import { H1, P, Input } from 'indigo-react';
7+import { P, Input, Grid, H5 } from 'indigo-react';
8
8
9−import View from 'components/View';
10import { ForwardButton } from 'components/Buttons';
9import { ForwardButton } from 'components/Buttons';
11import { InnerLabelDropdown } from 'components/old/Base';
10import { InnerLabelDropdown } from 'components/old/Base';
12
11
13import { TREZOR_PATH } from 'lib/trezor';
12import { TREZOR_PATH } from 'lib/trezor';
14−import useWalletType from 'lib/useWalletType';
15import { WALLET_TYPES } from 'lib/wallet';
13import { WALLET_TYPES } from 'lib/wallet';
16−import useResetPointCursor from 'lib/useResetPointCursor';
17
14
18import { useWallet } from 'store/wallet';
15import { useWallet } from 'store/wallet';
16+import useLoginView from 'lib/useLoginView';
19
17
20const accountOptions = [
18const accountOptions = [
21 { title: 'Custom path', value: 'custom' },
19 { title: 'Custom path', value: 'custom' },
22 ...times(20, i => ({ title: `Account #${i + 1}`, value: i })),
20 ...times(20, i => ({ title: `Account #${i + 1}`, value: i })),
23];
21];
24
22
25−export default function Trezor({ loginCompleted }) {
23+export default function Trezor({ className }) {
26− useResetPointCursor();
27− useWalletType(WALLET_TYPES.TREZOR);
24+ useLoginView(WALLET_TYPES.TREZOR);
28
25
29− const { wallet, setWallet, setWalletHdPath } = useWallet();
26+ const { setWallet, setWalletHdPath } = useWallet();
30
27
31 const [hdPath, setHdPath] = useState(TREZOR_PATH.replace(/x/g, 0));
28 const [hdPath, setHdPath] = useState(TREZOR_PATH.replace(/x/g, 0));
32 const [account, setAccount] = useState(0);
29 const [account, setAccount] = useState(0);
@@ -43,6 +40,7 @@ export default function Trezor({ loginCompleted }) {
43 email: 'bridge-trezor@urbit.org',
40 email: 'bridge-trezor@urbit.org',
44 appUrl: 'https://github.com/urbit/bridge',
41 appUrl: 'https://github.com/urbit/bridge',
45 });
42 });
43+
46 TrezorConnect.getPublicKey({ path: hdPath }).then(info => {
44 TrezorConnect.getPublicKey({ path: hdPath }).then(info => {
47 if (info.success === true) {
45 if (info.success === true) {
48 const payload = info.payload;
46 const payload = info.payload;
@@ -58,55 +56,50 @@ export default function Trezor({ loginCompleted }) {
58 });
56 });
59 };
57 };
60
58
61− let accountTitle = accountOptions.find(o => o.value === account).title;
59+ const accountTitle = accountOptions.find(o => o.value === account).title;
62−
63− const accountSelection = (
64− <InnerLabelDropdown
65− className="mt-8"
66− prop-size="md"
67− prop-format="innerLabel"
68− options={accountOptions}
69− handleUpdate={handleAccountSelection}
70− title="Account"
71− currentSelectionTitle={accountTitle}
72− fullWidth={true}
73− />
74− );
75−
76− const pathSelection =
77− account !== 'custom' ? null : (
78− <Input
79− className="mt3"
80− name="hdPath"
81− label="HD path"
82− autocomplete="off"
83− initialValue={hdPath}
84− onValue={setHdPath}
85− />
86− );
87
60
88 return (
61 return (
89− <View>
62+ <Grid className={className}>
63+ <Grid.Item full as={H5}>
90− <H1>Authenticate With Your Trezor</H1>
64+ Authenticate With Your Trezor
65+ </Grid.Item>
91
66
92− <P>
67+ <Grid.Item full as={P}>
93 Connect and authenticate to your Trezor. If you'd like to use a custom
68 Connect and authenticate to your Trezor. If you'd like to use a custom
94 derivation path, you may enter it below.
69 derivation path, you may enter it below.
95− </P>
70+ </Grid.Item>
96−
71+
97− {accountSelection}
72+ <Grid.Item
98− {pathSelection}
73+ full
99−
74+ as={InnerLabelDropdown}
100− <ForwardButton className="mt3" onClick={pollDevice}>
75+ className="mv4"
101− Authenticate
76+ title="Account"
102− </ForwardButton>
77+ options={accountOptions}
78+ handleUpdate={handleAccountSelection}
79+ currentSelectionTitle={accountTitle}
80+ />
103
81
82+ {account === 'custom' && (
83+ <Grid.Item
84+ full
85+ as={Input}
86+ className="mv4"
87+ name="hdPath"
88+ label="HD path"
89+ autoComplete="off"
90+ initialValue={hdPath}
91+ onValue={setHdPath}
92+ />
93+ )}
94+
95+ <Grid.Item
96+ full
104− <ForwardButton
97+ as={ForwardButton}
98+ solid
105 className="mt3"
99 className="mt3"
106− disabled={Maybe.Nothing.hasInstance(wallet)}
100+ onClick={pollDevice}>
107− onClick={loginCompleted}>
101+ Authenticate
108− Continue
102+ </Grid.Item>
109− </ForwardButton>
103+ </Grid>
110− </View>
111 );
104 );
112}
105}