on master help sign in
refactor: indigo-ifying login components
commit978150105528540d00a05d57acf8661204ca6028 authorMatt Condon (shrugs) <mattgcondon@gmail.com> dateMon 1 Jul 2019, 19:17 (7 years ago) parentc7bc9ab
20 files changed+613−568
src/Bridge.js+1−1 src/components/Inputs.js+13−2 src/components/Tabs.js+27−8 src/components/UploadButton.js+19 src/indigo-react/components/Button.js+2−1 src/indigo-react/components/Input.js+1−1 src/lib/useLoginView.js+17 src/lib/useResetPointCursor.js−12 src/lib/useWalletType.js−11 src/views/Activate/ActivatePassport.js−1 src/views/Login.js+86−45 src/views/Login/Advanced.js+31−110 src/views/Login/Hardware.js+36 src/views/Login/Keystore.js+40−69 src/views/Login/Ledger.js+106−114 src/views/Login/Mnemonic.js+17−23 src/views/Login/PrivateKey.js+9−19 src/views/Login/Shards.js+4−6 src/views/Login/Ticket.js+160−94 src/views/Login/Trezor.js+44−51
src/Bridge.js+1−1
@@ -23,7 +23,7 @@ const INITIAL_NETWORK_TYPE = isDevelopment
2323const SHOULD_STUB_LOCAL = process.env.REACT_APP_STUB_LOCAL === 'true';
2424const IS_STUBBED = isDevelopment && SHOULD_STUB_LOCAL;
2525const INITIAL_ROUTES = IS_STUBBED
26− ? [{ key: ROUTE_NAMES.LANDING }, { key: ROUTE_NAMES.ACTIVATE }]
26+ ? [{ key: ROUTE_NAMES.LANDING }, { key: ROUTE_NAMES.LOGIN }]
2727 : [{ key: ROUTE_NAMES.LANDING }];
2828
2929const INITIAL_WALLET = IS_STUBBED
src/components/Inputs.js+13−2
@@ -88,7 +88,7 @@ export function useHdPathInput(props) {
8888const kTicketValidators = [validateTicket, validateNotEmpty];
8989const kTicketTransformers = [prependSig];
9090//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 }) {
9292 const input = firstOf(
9393 useForm([
9494 {
@@ -105,7 +105,18 @@ export function useTicketInput({ validators = [], ...rest }) {
105105 ])
106106 );
107107
108− return { ...input, accessory: input.pass ? <AccessoryIcon.Success /> : null };
108+ const accessory = input.error ? (
109+ <AccessoryIcon.Failure />
110+ ) : deriving ? (
111+ <AccessoryIcon.Pending />
112+ ) : input.pass ? (
113+ <AccessoryIcon.Success />
114+ ) : null;
115+
116+ return {
117+ ...input,
118+ accessory,
119+ };
109120}
110121
111122const kPointValidators = [validatePoint, validateNotEmpty];
src/components/Tabs.js+27−8
@@ -1,18 +1,37 @@
11import React from 'react';
2−import HorizontalSelector from './old/HorizontalSelector';
2+import cn from 'classnames';
3+import { Grid, Flex } from 'indigo-react';
34
45export default function Tabs({
5− tabViews,
6− tabOptions,
6+ views,
7+ options,
78 currentTab,
89 onTabChange,
10+ className,
911 ...rest
1012}) {
11− const Component = tabViews[currentTab];
13+ const Tab = views[currentTab];
14+
1215 return (
13− <>
14− <HorizontalSelector options={tabOptions} onChange={onTabChange} />
15− <Component {...rest} />
16− </>
16+ <Grid className={className}>
17+ <Grid.Item full as={Flex} className="b-gray3 bb1">
18+ {options.map(option => {
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>
1736 );
1837}
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';
66import { HelpText } from './Typography';
77
88export default function Button({
9+ as: As = 'a',
910 solid = false,
1011 success = false,
1112 disabled = false,
@@ -23,7 +24,7 @@ export default function Button({
2324 };
2425 return (
2526 <Grid
26− as="a"
27+ as={As}
2728 gap={1}
2829 className={cn(
2930 'pointer pv4 truncate flex-row justify-between',
src/indigo-react/components/Input.js+1−1
@@ -70,7 +70,7 @@ export default function Input({
7070
7171 return (
7272 <Flex
73− className={className}
73+ className={cn(className, 'mb1')}
7474 col
7575 style={{
7676 ...(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 @@
11import React, { useMemo } from 'react';
22import cn from 'classnames';
3−import Maybe from 'folktale/maybe';
43import { Grid } from 'indigo-react';
54
65import { LocalRouterProvider, useLocalRouter } from 'lib/LocalRouter';
src/views/Login.js+86−45
@@ -1,104 +1,145 @@
1−import { Nothing, Just } from 'folktale/maybe';
2−import React, { useState } from 'react';
1+import React, { useCallback, useState } from 'react';
2+import Maybe from 'folktale/maybe';
33import * as azimuth from 'azimuth-js';
44
5−import { H1 } from 'indigo-react';
6−import View from 'components/View';
7−import Tabs from 'components/Tabs';
5+import { H4, Grid } from 'indigo-react';
86
97import Ticket from './Login/Ticket';
108import Mnemonic from './Login/Mnemonic';
119import Advanced from './Login/Advanced';
10+import Hardware from './Login/Hardware';
1211
13−import useLifecycle from 'lib/useLifecycle';
1412import * as need from 'lib/need';
15−import { ROUTE_NAMES } from 'lib/routeNames';
1613
1714import { useHistory } from 'store/history';
1815import { useNetwork } from 'store/network';
1916import { useWallet } from 'store/wallet';
2017import { usePointCursor } from 'store/pointCursor';
2118
22−const TABS = {
23− TICKET: Symbol('TICKET'),
24− MNEMONIC: Symbol('MNEMONIC'),
25− ADVANCED: Symbol('ADVANCED'),
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+
25+const NAMES = {
26+ TICKET: 'TICKET',
27+ MNEMONIC: 'MNEMONIC',
28+ HARDWARE: 'HARDWARE',
29+ ADVANCED: 'ADVANCED',
2630};
2731
28−const tabViews = {
29− [TABS.TICKET]: Ticket,
30− [TABS.MNEMONIC]: Mnemonic,
31− [TABS.ADVANCED]: Advanced,
32+const VIEWS = {
33+ [NAMES.TICKET]: Ticket,
34+ [NAMES.MNEMONIC]: Mnemonic,
35+ [NAMES.HARDWARE]: Hardware,
36+ [NAMES.ADVANCED]: Advanced,
3237};
3338
34−const tabOptions = [
35− { title: 'Ticket', value: TABS.TICKET },
36− { title: 'Mnemonic', value: TABS.MNEMONIC },
37− { title: 'Advanced', value: TABS.ADVANCED },
39+const OPTIONS = [
40+ { text: 'Master Ticket', value: NAMES.TICKET },
41+ { text: 'Mnemonic', value: NAMES.MNEMONIC },
42+ { text: 'Hardware', value: NAMES.HARDWARE },
43+ { text: 'Advanced', value: NAMES.ADVANCED },
3844];
3945
4046export default function Login() {
4147 // globals
42− const history = useHistory();
48+ const { push, replaceWith, names } = useHistory();
4349 const { contracts } = useNetwork();
44− const { wallet, resetWallet } = useWallet();
50+ const { wallet } = useWallet();
4551 const { pointCursor, setPointCursor } = usePointCursor();
4652
4753 // inputs
48− const [currentTab, setCurrentTab] = useState(TABS.TICKET);
54+ const [currentTab, setCurrentTab] = useState(NAMES.TICKET);
4955
50− // on-mount
51− useLifecycle(() => {
52− resetWallet();
53− });
56+ const goToActivate = useCallback(
57+ () => replaceWith([{ key: names.LANDING }, { key: names.ACTIVATE }]),
58+ [replaceWith, names]
59+ );
5460
55− const doContinue = async () => {
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+
70+ const doContinue = useCallback(async () => {
5671 const _wallet = need.wallet(wallet);
5772 const _contracts = need.contracts(contracts);
5873
5974 // if no point cursor set by login logic, try to deduce it
6075 let deduced = pointCursor;
61− if (Nothing.hasInstance(deduced)) {
76+ if (Maybe.Nothing.hasInstance(deduced)) {
6277 const owned = await azimuth.azimuth.getOwnedPoints(
6378 _contracts,
6479 _wallet.address
6580 );
6681 if (owned.length === 1) {
67− deduced = Just(owned[0]);
82+ deduced = Maybe.Just(owned[0]);
6883 } else if (owned.length === 0) {
6984 const canOwn = await azimuth.azimuth.getTransferringFor(
7085 _contracts,
7186 _wallet.address
7287 );
7388 if (canOwn.length === 1) {
74− deduced = Just(canOwn[0]);
89+ deduced = Maybe.Just(canOwn[0]);
7590 }
7691 }
7792 }
7893
7994 // if we have a deduced point or one in the global context,
8095 // navigate to that specific point, otherwise navigate to list of points
81− if (Just.hasInstance(deduced)) {
96+ if (Maybe.Just.hasInstance(deduced)) {
8297 setPointCursor(deduced);
83− history.popAndPush(ROUTE_NAMES.POINTS);
84− history.push(ROUTE_NAMES.POINT);
98+ goToPoint();
8599 } else {
86− history.popAndPush(ROUTE_NAMES.POINTS);
100+ goToPoints();
87101 }
88− };
102+ }, [contracts, pointCursor, setPointCursor, wallet, goToPoint, goToPoints]);
89103
90104 return (
91105 <View>
92− <H1>Login</H1>
93−
94− <Tabs
95− tabViews={tabViews}
96− tabOptions={tabOptions}
97− currentTab={currentTab}
98− onTabChange={setCurrentTab}
99− //
100− loginCompleted={doContinue}
101− />
106+ <Grid>
107+ <Grid.Item full as={Crumbs} routes={[{ text: 'Multipass' }]} />
108+ <Grid.Item full as={H4} className="mt4">
109+ Login
110+ </Grid.Item>
111+
112+ <Grid.Item
113+ full
114+ as={Tabs}
115+ className="mt1"
116+ views={VIEWS}
117+ options={OPTIONS}
118+ currentTab={currentTab}
119+ onTabChange={setCurrentTab}
120+ />
121+
122+ <Grid.Item
123+ full
124+ as={ForwardButton}
125+ solid
126+ className="mt3"
127+ disabled={Maybe.Nothing.hasInstance(wallet)}
128+ onClick={doContinue}>
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>
102143 </View>
103144 );
104145}
src/views/Login/Advanced.js+31−110
@@ -1,115 +1,36 @@
1−import React from 'react';
2−import { P, InnerLabelDropdown } from '../../components/old/Base';
3−import { Row, Col } from '../../components/old/Base';
1+import React, { useState } from 'react';
42
5−import Ticket from './Ticket';
6−import Shards from './Shards';
7−import Ledger from './Ledger';
8−import Trezor from './Trezor';
9−import Mnemonic from './Mnemonic';
3+import Tabs from 'components/Tabs';
104import PrivateKey from './PrivateKey';
115import Keystore from './Keystore';
126
13−import { WALLET_TYPES, renderWalletType } from '../../lib/wallet';
14−import { compose } from '../../lib/lib';
15−import { withWallet } from '../../store/wallet';
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− },
38− {
39− type: 'divider',
40− },
41− {
42− title: 'Ledger',
43− value: WALLET_TYPES.LEDGER,
44− },
45− {
46− title: 'Trezor',
47− value: WALLET_TYPES.TREZOR,
48− },
49− {
50− type: 'divider',
51− },
52− {
53− title: 'BIP39 Mnemonic',
54− value: WALLET_TYPES.MNEMONIC,
55− },
56− {
57− title: 'Ethereum Private Key',
58− value: WALLET_TYPES.PRIVATE_KEY,
59− },
60− {
61− title: 'Ethereum Keystore File',
62− value: WALLET_TYPES.KEYSTORE,
63− },
64− ];
65− }
66−
67− handleSelection(selected) {
68− this.setState({ selected });
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 (
98− <Row>
99− <Col>
100− <InnerLabelDropdown
101− className={'mb-10 mt-6'}
102− title={'Wallet Type:'}
103− options={this.getWalletOptions()}
104− handleUpdate={this.handleSelection}
105− currentSelectionTitle={renderWalletType(this.state.selected)}
106− />
107−
108− {inputs}
109− </Col>
110− </Row>
111− );
112− }
7+const NAMES = {
8+ PRIVATE_KEY: 'PRIVATE_KEY',
9+ KEYSTORE: 'KEYSTORE',
10+};
11+
12+const VIEWS = {
13+ [NAMES.PRIVATE_KEY]: PrivateKey,
14+ [NAMES.KEYSTORE]: Keystore,
15+};
16+
17+const OPTIONS = [
18+ { text: 'Ethereum Private Key', value: NAMES.PRIVATE_KEY },
19+ { text: 'Ethereum Keystore', value: NAMES.KEYSTORE },
20+];
21+
22+export default function Advanced({ loginCompleted, className }) {
23+ const [currentTab, setCurrentTab] = useState(NAMES.PRIVATE_KEY);
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+ );
11336}
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';
31import React, { useState } from 'react';
2+import Maybe from 'folktale/maybe';
43import * as keythereum from 'keythereum';
5−import { H3, Input } from 'indigo-react';
4+import { P, Grid, Input, ErrorText } from 'indigo-react';
65
7−import View from 'components/View';
86import { usePassphraseInput } from 'components/Inputs';
97import { ForwardButton } from 'components/Buttons';
10−import { InputCaption, UploadButton, Warning } from 'components/old/Base';
118
129import { useWallet } from 'store/wallet';
1310
1411import * as need from 'lib/need';
1512import { EthereumWallet, WALLET_TYPES } from 'lib/wallet';
16−import useWalletType from 'lib/useWalletType';
17−import useResetPointCursor from 'lib/useResetPointCursor';
13+import UploadButton from 'components/UploadButton';
14+import useLoginView from 'lib/useLoginView';
1815
19−export default function Keystore({ loginCompleted }) {
20− useResetPointCursor();
21− useWalletType(WALLET_TYPES.KEYSTORE);
16+export default function Keystore({ className }) {
17+ useLoginView(WALLET_TYPES.KEYSTORE);
2218
2319 // globals
24− const { wallet, setWallet } = useWallet();
20+ const { setWallet } = useWallet();
2521
22+ const [error, setError] = useState();
2623 // inputs
27− // keystore: Maybe<Result<String, String>>
28− const [keystore, setKeystore] = useState(Nothing());
24+ // keystore: Maybe<String>
25+ const [keystore, setKeystore] = useState(Maybe.Nothing());
2926 const passphraseInput = usePassphraseInput({
3027 name: 'password',
3128 label: 'Keystore password',
@@ -33,8 +30,6 @@ export default function Keystore({ loginCompleted }) {
3330 });
3431 const passphrase = passphraseInput.data;
3532
36− const [decryptionProblem, setDecryptionProblem] = useState(false);
37−
3833 const constructWallet = () => {
3934 try {
4035 const text = need.keystore(keystore);
@@ -43,26 +38,27 @@ export default function Keystore({ loginCompleted }) {
4338 const privateKey = keythereum.recover(passphrase, json);
4439
4540 const newWallet = new EthereumWallet(privateKey);
46− setDecryptionProblem(false);
47− setWallet(Just(newWallet));
41+ setError();
42+ setWallet(Maybe.Just(newWallet));
4843 } catch (err) {
49− setDecryptionProblem(true);
50− setWallet(Nothing());
44+ setError(
45+ "Couldn't decrypt wallet. You may have entered an incorrect password."
46+ );
47+ setWallet(Maybe.Nothing());
5148 }
5249 };
5350
54− const handleKeystoreUpload = event => {
55− const file = event.files.item(0);
51+ const handleKeystoreUpload = element => {
52+ const file = element.files.item(0);
5653 const reader = new FileReader();
5754
5855 reader.onload = e => {
5956 const keystore = e.target.result;
60− setKeystore(Just(Ok(keystore)));
57+ setKeystore(Maybe.Just(keystore));
6158 };
6259
6360 const failure = _ => {
64− const message = 'There was a problem uploading your Keystore file';
65− setKeystore(Just(Error(message)));
61+ setError('There was a problem uploading your Keystore file');
6662 };
6763
6864 reader.onerror = failure;
@@ -71,59 +67,34 @@ export default function Keystore({ loginCompleted }) {
7167 reader.readAsText(file);
7268 };
7369
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−
9570 return (
96− <View>
97− <InputCaption>
71+ <Grid className={className}>
72+ <Grid.Item full as={P}>
9873 Please upload your Ethereum keystore file. If your keystore file is
9974 encrypted with a password, you'll also need to enter that below.
100− </InputCaption>
75+ </Grid.Item>
10176
102− <UploadButton
103− className={`${uploadButtonClass} mt3`}
104− onChange={handleKeystoreUpload}>
105− <div className="flex-center-all fs-4 h-11 pointer">
106− Upload Keystore file
107− </div>
108− </UploadButton>
77+ <Grid.Item full as={UploadButton} onChange={handleKeystoreUpload}>
78+ Upload Keystore file
79+ </Grid.Item>
10980
110− <Input {...passphraseInput} />
81+ {error && (
82+ <Grid.Item full as={ErrorText} className="mt1">
83+ {error}
84+ </Grid.Item>
85+ )}
11186
112− <ForwardButton
87+ <Grid.Item full as={Input} className="mt3" {...passphraseInput} />
88+
89+ <Grid.Item
90+ full
91+ as={ForwardButton}
92+ solid
11393 className="mt3"
114− disabled={Nothing.hasInstance(keystore)}
94+ disabled={Maybe.Nothing.hasInstance(keystore)}
11595 onClick={constructWallet}>
11696 Decrypt
117− </ForwardButton>
118−
119− {decryptMessage}
120−
121− <ForwardButton
122− className="mt3"
123− disabled={Nothing.hasInstance(wallet)}
124− onClick={loginCompleted}>
125− Continue
126− </ForwardButton>
127− </View>
97+ </Grid.Item>
98+ </Grid>
12899 );
129100}
src/views/Login/Ledger.js+106−114
@@ -1,22 +1,20 @@
1−import * as bip32 from 'bip32';
21import React, { useState } from 'react';
3−import { times } from 'lodash';
42import 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';
56import Transport from '@ledgerhq/hw-transport-u2f';
67import Eth from '@ledgerhq/hw-app-eth';
78import * as secp256k1 from 'secp256k1';
8−import { H1, H2, P, Input } from 'indigo-react';
99
10−import View from 'components/View';
1110import { ForwardButton } from 'components/Buttons';
1211import { InnerLabelDropdown } from 'components/old/Base';
1312
1413import { useWallet } from 'store/wallet';
1514
1615import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from 'lib/ledger';
17−import useWalletType from 'lib/useWalletType';
1816import { WALLET_TYPES } from 'lib/wallet';
19−import useResetPointCursor from 'lib/useResetPointCursor';
17+import useLoginView from 'lib/useLoginView';
2018
2119const pathOptions = [
2220 { title: 'Ledger Live', value: LEDGER_LIVE_PATH },
@@ -24,15 +22,18 @@ const pathOptions = [
2422 { title: 'Custom path', value: 'custom' },
2523];
2624
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+}));
2829
30+const chopHdPrefix = str => (str.slice(0, 2) === 'm/' ? str.slice(2) : str);
2931const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
3032
31−export default function Ledger({ loginCompleted }) {
32− useResetPointCursor();
33− useWalletType(WALLET_TYPES.LEDGER);
33+export default function Ledger({ className }) {
34+ useLoginView(WALLET_TYPES.LEDGER);
3435
35− const { wallet, setWallet, setWalletHdPath } = useWallet();
36+ const { setWallet, setWalletHdPath } = useWallet();
3637
3738 const [basePath, setBasePath] = useState(LEDGER_LIVE_PATH);
3839 const [account, setAccount] = useState(0);
@@ -75,116 +76,107 @@ export default function Ledger({ loginCompleted }) {
7576 };
7677
7778 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− }));
8379 const accountTitle = accountOptions.find(o => o.value === account).title;
8480
85− const basePathSelection = (
86− <InnerLabelDropdown
87− className="mt-8"
88− options={pathOptions}
89− handleUpdate={handlePathSelection}
90− title="Derivation path"
91− currentSelectionTitle={basePathTitle}
92− fullWidth={true}
93− />
81+ const isHTTPS = document.location.protocol === 'https:';
82+
83+ // when not on https, tell user how to get there
84+ const renderHTTP = () => (
85+ <>
86+ <Grid.Item full as={H5}>
87+ Running on HTTP?
88+ </Grid.Item>
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+ </>
94119 );
95120
96− const truePathSelection =
97− basePath === 'custom' ? (
98− <Input
99− className="mt3"
100− name="hdPath"
101− label="HD path"
102− autocomplete="off"
103− initialValue={addHdPrefix(hdPath)}
104− onValue={setHdPath}
121+ const renderHTTPS = () => (
122+ <>
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}
137+ className="mv4"
138+ title="Derivation path"
139+ options={pathOptions}
140+ handleUpdate={handlePathSelection}
141+ currentSelectionTitle={basePathTitle}
105142 />
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− );
118143
119− // when not on https, tell user how to get there
120− const body =
121− document.location.protocol !== 'https:' ? (
122− <>
123− <H2>Running on HTTP?</H2>
124−
125− <P>
126− To authenticate and sign transactions with a Ledger, Bridge must be
127− serving over HTTPS on localhost. You can do this via the following:
128− </P>
129−
130− <ol className="measure-md">
131− <li className="mt-4">
132− Install
133− <a
134− target="_blank"
135− href="https://github.com/FiloSottile/mkcert"
136− rel="noopener noreferrer">
137− mkcert
138− </a>
139− </li>
140− <li className="mt-4">
141− Install a local certificate authority via{' '}
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}>
172− {'Authenticate'}
173− </ForwardButton>
174−
175− <ForwardButton
176− className={'mt3'}
177− disabled={Maybe.Nothing.hasInstance(wallet)}
178− onClick={loginCompleted}>
179− {'Continue'}
180− </ForwardButton>
181− </>
182− );
144+ {basePath === 'custom' ? (
145+ <Grid.Item
146+ full
147+ as={Input}
148+ className="mv3"
149+ name="hdPath"
150+ label="HD path"
151+ autoComplete="off"
152+ initialValue={addHdPrefix(hdPath)}
153+ onValue={setHdPath}
154+ />
155+ ) : (
156+ <Grid.Item
157+ full
158+ as={InnerLabelDropdown}
159+ className="mt4"
160+ title="Account"
161+ options={accountOptions}
162+ handleUpdate={handleAccountSelection}
163+ currentSelectionTitle={accountTitle}
164+ />
165+ )}
166+
167+ <ForwardButton className="mt3" onClick={pollDevice}>
168+ Authenticate
169+ </ForwardButton>
170+ </>
171+ );
183172
184173 return (
185− <View>
186− <H1>Authenticate With Your Ledger</H1>
187− {body}
188− </View>
174+ <Grid className={className}>
175+ <Grid.Item full as={H4}>
176+ Authenticate With Your Ledger
177+ </Grid.Item>
178+ {isHTTPS && renderHTTPS()}
179+ {!isHTTPS && renderHTTP()}
180+ </Grid>
189181 );
190182}
src/views/Login/Mnemonic.js+17−23
@@ -1,27 +1,23 @@
11import React, { useCallback, useEffect } from 'react';
22import { Just, Nothing } from 'folktale/maybe';
3−import { Input } from 'indigo-react';
3+import { Grid, Input, CheckboxInput } from 'indigo-react';
44
5−import View from 'components/View';
65import {
76 usePassphraseInput,
87 useMnemonicInput,
98 useHdPathInput,
9+ useCheckboxInput,
1010} from 'components/Inputs';
11−import { ForwardButton } from 'components/Buttons';
1211
1312import { useWallet } from 'store/wallet';
1413
1514import { walletFromMnemonic, WALLET_TYPES } from 'lib/wallet';
16−import useWalletType from 'lib/useWalletType';
17−import useResetPointCursor from 'lib/useResetPointCursor';
15+import useLoginView from 'lib/useLoginView';
1816
19−export default function Mnemonic({ advanced, loginCompleted }) {
20− useResetPointCursor();
21− useWalletType(WALLET_TYPES.MNEMONIC);
17+export default function Mnemonic({ className }) {
18+ useLoginView(WALLET_TYPES.MNEMONIC);
2219
2320 const {
24− wallet,
2521 setWallet,
2622 authMnemonic,
2723 setAuthMnemonic,
@@ -29,6 +25,11 @@ export default function Mnemonic({ advanced, loginCompleted }) {
2925 setWalletHdPath,
3026 } = useWallet();
3127
28+ const advancedInput = useCheckboxInput({
29+ name: 'advanced',
30+ label: 'Passphrase & HD Path',
31+ initialValue: false,
32+ });
3233 const mnemonic = authMnemonic.getOrElse('');
3334 // TODO: move this into transformers?
3435 // transform the result of the mnemonic to Maybe<string>
@@ -69,24 +70,17 @@ export default function Mnemonic({ advanced, loginCompleted }) {
6970 }, [mnemonic, passphrase, walletHdPath, setWallet]);
7071
7172 return (
72− <View>
73− <Input {...mnemonicInput} />
73+ <Grid className={className}>
74+ <Grid.Item full as={Input} {...mnemonicInput} />
7475
75− {!advanced ? null : (
76+ {advancedInput.data && (
7677 <>
77− <Input {...passphraseInput} />
78−
79− <Input {...hdPathInput} />
78+ <Grid.Item full as={Input} {...passphraseInput} />
79+ <Grid.Item full as={Input} {...hdPathInput} />
8080 </>
8181 )}
8282
83− <ForwardButton
84− className="mt3"
85− disabled={Nothing.hasInstance(wallet)}
86− onClick={loginCompleted}
87− solid>
88− Continue
89− </ForwardButton>
90− </View>
83+ <Grid.Item as={CheckboxInput} {...advancedInput} full />
84+ </Grid>
9185 );
9286}
src/views/Login/PrivateKey.js+9−19
@@ -1,21 +1,18 @@
11import { Just, Nothing } from 'folktale/maybe';
22import React, { useEffect } from 'react';
3−import { Input } from 'indigo-react';
3+import { Grid, Input } from 'indigo-react';
44
5−import View from 'components/View';
65import { usePassphraseInput } from 'components/Inputs';
7−import { ForwardButton } from 'components/Buttons';
86
97import { useWallet } from 'store/wallet';
108
119import { EthereumWallet, WALLET_TYPES } from 'lib/wallet';
12−import useWalletType from 'lib/useWalletType';
13−import useResetPointCursor from 'lib/useResetPointCursor';
10+import useLoginView from 'lib/useLoginView';
1411
15−export default function PrivateKey({ loginCompleted }) {
16− useResetPointCursor();
17− useWalletType(WALLET_TYPES.PRIVATE_KEY);
18− const { wallet, setWallet } = useWallet();
12+export default function PrivateKey({ className }) {
13+ useLoginView(WALLET_TYPES.PRIVATE_KEY);
14+
15+ const { setWallet } = useWallet();
1916
2017 const privateKeyInput = usePassphraseInput({
2118 name: 'privateKey',
@@ -35,15 +32,8 @@ export default function PrivateKey({ loginCompleted }) {
3532 }, [privateKey, setWallet]);
3633
3734 return (
38− <View>
39− Please enter your raw Ethereum private key here.
40− <Input {...privateKeyInput} />
41− <ForwardButton
42− className="mt3"
43− disabled={Nothing.hasInstance(wallet)}
44− onClick={loginCompleted}>
45− Continue
46− </ForwardButton>
47− </View>
35+ <Grid className={className}>
36+ <Grid.Item full as={Input} {...privateKeyInput} />
37+ </Grid>
4838 );
4939}
src/views/Login/Shards.js+4−6
@@ -14,12 +14,10 @@ import { ForwardButton } from 'components/Buttons';
1414import { useWallet } from 'store/wallet';
1515
1616import { urbitWalletFromTicket, WALLET_TYPES } from 'lib/wallet';
17−import useWalletType from 'lib/useWalletType';
18−import useResetPointCursor from 'lib/useResetPointCursor';
17+import useLoginView from 'lib/useLoginView';
1918
2019export default function Shards({ loginCompleted }) {
21− useResetPointCursor();
22− useWalletType(WALLET_TYPES.SHARDS);
20+ useLoginView(WALLET_TYPES.SHARDS);
2321
2422 const { wallet, setUrbitWallet } = useWallet();
2523
@@ -72,7 +70,7 @@ export default function Shards({ loginCompleted }) {
7270 const ready = shards.filter(x => x !== '').length > 1;
7371
7472 return (
75− <View>
73+ <>
7674 Enter your point and at least two of your three Urbit master ticket shards
7775 here. The index of the input field needs to match the index of the shard.
7876 <Input {...pointInput} />
@@ -93,6 +91,6 @@ export default function Shards({ loginCompleted }) {
9391 onClick={loginCompleted}>
9492 Continue
9593 </ForwardButton>
96− </View>
94+ </>
9795 );
9896}
src/views/Login/Ticket.js+160−94
@@ -1,16 +1,16 @@
1−import { Just, Nothing } from 'folktale/maybe';
1+import Maybe from 'folktale/maybe';
22import React, { useCallback, useState, useEffect } from 'react';
33import * as azimuth from 'azimuth-js';
44import * as ob from 'urbit-ob';
5−import { Input, AccessoryIcon } from 'indigo-react';
5+import * as kg from 'urbit-key-generation/dist/index';
6+import { Input, Grid, CheckboxInput } from 'indigo-react';
67
7−import View from 'components/View';
88import {
99 usePointInput,
1010 useTicketInput,
1111 usePassphraseInput,
12+ useCheckboxInput,
1213} from 'components/Inputs';
13−import { ForwardButton } from 'components/Buttons';
1414
1515import { useNetwork } from 'store/network';
1616import { useWallet } from 'store/wallet';
@@ -18,123 +18,189 @@ import { usePointCursor } from 'store/pointCursor';
1818
1919import * as need from 'lib/need';
2020import { WALLET_TYPES, urbitWalletFromTicket } from 'lib/wallet';
21−import useWalletType from 'lib/useWalletType';
22−import useResetPointCursor from 'lib/useResetPointCursor';
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 }) {
32− useResetPointCursor();
33− useWalletType(WALLET_TYPES.TICKET);
21+import useImpliedPoint from 'lib/useImpliedPoint';
22+import useLoginView from 'lib/useLoginView';
23+
24+export default function Ticket({ className }) {
25+ useLoginView(WALLET_TYPES.TICKET);
3426 // globals
3527 const { contracts } = useNetwork();
36− const { wallet, setUrbitWallet } = useWallet();
28+ const { setUrbitWallet } = useWallet();
3729 const { setPointCursor } = usePointCursor();
30+ const impliedPoint = useImpliedPoint();
3831
3932 // inputs
33+
34+ // point
4035 const pointInput = usePointInput({
4136 name: 'point',
42− //TODO deduce point name from URL if we can, prefill input if we found it
37+ initialValue: impliedPoint,
4338 autoFocus: true,
4439 });
4540 const pointName = pointInput.data;
4641
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);
4758 const ticketInput = useTicketInput({
4859 name: 'ticket',
4960 label: 'Master Ticket',
61+ error,
62+ deriving,
5063 });
5164 const ticket = ticketInput.data;
5265
53− const passphraseInput = usePassphraseInput({
54− name: 'passphrase',
55− label: '(Optional) Wallet Passphrase',
66+ // shards
67+ const shardsInput = useCheckboxInput({
68+ name: 'shards',
69+ label: 'Shards',
70+ initialValue: false,
5671 });
57− const passphrase = passphraseInput.data;
5872
59− // display state
60− const [ticketStatus, setTicketStatus] = useState(Nothing());
61−
62− //TODO maybe want to do this only on-go, because wallet derivation is slow...
63− const verifyTicket = useCallback(
64− async (pointName, ticket, passphrase) => {
65− setUrbitWallet(Nothing());
66− if (!ob.isValidPatq(ticket) || !ob.isValidPatp(pointName)) {
67− setTicketStatus(Nothing());
68− return;
69− }
70− setTicketStatus(Just(INPUT_STATUS.SPIN));
71− const _contracts = need.contracts(contracts);
72− const pointNumber = ob.patp2dec(pointName);
73− const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
74− const isOwner = azimuth.azimuth.isOwner(
73+ const shard1Input = useTicketInput({
74+ name: 'shard1',
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+
93+ // TODO: maybe want to do this only on-go, because wallet derivation is slow...
94+ const deriveWalletFromTicket = useCallback(async () => {
95+ // clear states
96+ setError();
97+ setDeriving(true);
98+ setUrbitWallet(Maybe.Nothing());
99+
100+ if (!ob.isValidPatq(ticket) || !ob.isValidPatp(pointName)) {
101+ setDeriving(false);
102+ return;
103+ }
104+
105+ const _contracts = need.contracts(contracts);
106+ const pointNumber = ob.patp2dec(pointName);
107+ const urbitWallet = await urbitWalletFromTicket(
108+ ticket,
109+ pointName,
110+ passphrase
111+ );
112+ const [isOwner, isTransferProxy] = await Promise.all([
113+ azimuth.azimuth.isOwner(
75114 _contracts,
76115 pointNumber,
77− uhdw.ownership.keys.address
78− );
79− const isTransferProxy = azimuth.azimuth.isTransferProxy(
116+ urbitWallet.ownership.keys.address
117+ ),
118+ azimuth.azimuth.isTransferProxy(
80119 _contracts,
81120 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.'
83128 );
84− setUrbitWallet(Just(uhdw));
85− const newStatus =
86− (await isOwner) || (await isTransferProxy)
87− ? INPUT_STATUS.GOOD
88− : INPUT_STATUS.FAIL;
89− setTicketStatus(Just(newStatus));
90− },
91− [contracts, setUrbitWallet, setTicketStatus]
92− );
129+ setDeriving(false);
130+ return;
131+ }
132+
133+ setUrbitWallet(Maybe.Just(urbitWallet));
134+ setPointCursor(Maybe.Just(ob.patp2dec(pointName)));
135+ setDeriving(false);
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);
158+ setUrbitWallet(Maybe.Just(uhdw));
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]);
93178
179+ // verify ticket if not using shards
94180 useEffect(() => {
95− verifyTicket(pointName, ticket, passphrase);
96− }, [verifyTicket, pointName, ticket, passphrase]);
97−
98− const canContinue = () => {
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− });
181+ if (!isUsingShards) {
182+ deriveWalletFromTicket();
183+ }
184+ }, [isUsingShards, deriveWalletFromTicket]);
125185
126186 return (
127− <View>
128− <Input {...pointInput} />
129− <Input {...ticketInput} accessory={displayTicketStatus} />
130− {advanced && <Input {...passphraseInput} />}
131−
132− <ForwardButton
133− className={'mt3'}
134− disabled={!canContinue()}
135− onClick={doContinue}>
136− Continue
137− </ForwardButton>
138− </View>
187+ <Grid className={className}>
188+ <Grid.Item full as={Input} {...pointInput} />
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 && (
199+ <Grid.Item full as={Input} {...passphraseInput} />
200+ )}
201+
202+ <Grid.Item full as={CheckboxInput} {...hasPassphraseInput} />
203+ <Grid.Item full as={CheckboxInput} {...shardsInput} />
204+ </Grid>
139205 );
140206}
src/views/Login/Trezor.js+44−51
@@ -4,29 +4,26 @@ import Maybe from 'folktale/maybe';
44import { times } from 'lodash';
55import TrezorConnect from 'trezor-connect';
66import * as secp256k1 from 'secp256k1';
7−import { H1, P, Input } from 'indigo-react';
7+import { P, Input, Grid, H5 } from 'indigo-react';
88
9−import View from 'components/View';
109import { ForwardButton } from 'components/Buttons';
1110import { InnerLabelDropdown } from 'components/old/Base';
1211
1312import { TREZOR_PATH } from 'lib/trezor';
14−import useWalletType from 'lib/useWalletType';
1513import { WALLET_TYPES } from 'lib/wallet';
16−import useResetPointCursor from 'lib/useResetPointCursor';
1714
1815import { useWallet } from 'store/wallet';
16+import useLoginView from 'lib/useLoginView';
1917
2018const accountOptions = [
2119 { title: 'Custom path', value: 'custom' },
2220 ...times(20, i => ({ title: `Account #${i + 1}`, value: i })),
2321];
2422
25−export default function Trezor({ loginCompleted }) {
26− useResetPointCursor();
27− useWalletType(WALLET_TYPES.TREZOR);
23+export default function Trezor({ className }) {
24+ useLoginView(WALLET_TYPES.TREZOR);
2825
29− const { wallet, setWallet, setWalletHdPath } = useWallet();
26+ const { setWallet, setWalletHdPath } = useWallet();
3027
3128 const [hdPath, setHdPath] = useState(TREZOR_PATH.replace(/x/g, 0));
3229 const [account, setAccount] = useState(0);
@@ -43,6 +40,7 @@ export default function Trezor({ loginCompleted }) {
4340 email: 'bridge-trezor@urbit.org',
4441 appUrl: 'https://github.com/urbit/bridge',
4542 });
43+
4644 TrezorConnect.getPublicKey({ path: hdPath }).then(info => {
4745 if (info.success === true) {
4846 const payload = info.payload;
@@ -58,55 +56,50 @@ export default function Trezor({ loginCompleted }) {
5856 });
5957 };
6058
61− let 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− );
59+ const accountTitle = accountOptions.find(o => o.value === account).title;
8760
8861 return (
89− <View>
90− <H1>Authenticate With Your Trezor</H1>
62+ <Grid className={className}>
63+ <Grid.Item full as={H5}>
64+ Authenticate With Your Trezor
65+ </Grid.Item>
9166
92− <P>
67+ <Grid.Item full as={P}>
9368 Connect and authenticate to your Trezor. If you'd like to use a custom
9469 derivation path, you may enter it below.
95− </P>
96−
97− {accountSelection}
98− {pathSelection}
99−
100− <ForwardButton className="mt3" onClick={pollDevice}>
101− Authenticate
102− </ForwardButton>
70+ </Grid.Item>
71+
72+ <Grid.Item
73+ full
74+ as={InnerLabelDropdown}
75+ className="mv4"
76+ title="Account"
77+ options={accountOptions}
78+ handleUpdate={handleAccountSelection}
79+ currentSelectionTitle={accountTitle}
80+ />
10381
104− <ForwardButton
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
97+ as={ForwardButton}
98+ solid
10599 className="mt3"
106− disabled={Maybe.Nothing.hasInstance(wallet)}
107− onClick={loginCompleted}>
108− Continue
109− </ForwardButton>
110− </View>
100+ onClick={pollDevice}>
101+ Authenticate
102+ </Grid.Item>
103+ </Grid>
111104 );
112105}