on master help sign in
checkpoint: initial refactor of email list, move all input state into useForm
commitf34e8d05b6a27b6e7a6a07f2c57bf388c2d2ba57 authorMatt Condon (shrugs) <mattgcondon@gmail.com> dateMon 24 Jun 2019, 20:04 (7 years ago) parentdf1910f
58 files changed+1052−1925
package-lock.json+5 package.json+1 src/Bridge.js+4−3 src/_walletgen/Walletgen.js+1−1 src/_walletgen/views/Generate.js+7−2 src/_walletgen/views/Upload.js+1−1 src/assets/back.svg+4 src/components/Buttons.js+2−2 src/components/Footer.js+4−13 src/components/Inputs.js+104−56 src/components/LoadableButton.js+6 src/components/MiniBackButton.js+8 src/components/View.js+1−1 src/components/old/Header.js+1−5 src/components/old/HorizontalSelector.js+1−1 src/indigo-react/components/Button.js+10−6 src/indigo-react/components/Flex.js+1−1 src/indigo-react/components/Grid.js+6 src/indigo-react/components/IconButton.js+48 src/indigo-react/components/Input.js+46−140 src/indigo-react/index.js+1 src/indigo-react/indigo.css+66−21 src/indigo-react/lib/useForm.js+171 src/lib/LocalRouter.js+9 src/_walletgen/lib/constants.js → src/lib/constants.js+1 src/lib/invite.js+1−1 src/lib/routeNames.js−9 src/lib/router.js+2−53 src/lib/useArray.js+36 src/lib/useRenderCount.js+7 src/lib/useSyncPoint.js+3−3 src/lib/validators.js+36−13 src/_walletgen/lib/lib.js → src/lib/walletgen.js+1 src/store/pointCache.js+80−27 src/style/indigo-proposals.scss+4 src/views/Invite.js+37−34 src/views/Invite/InviteEmail.js+99−69 src/views/Invite/InviteUrl.js+21−3 src/views/Keystore.js−166 src/views/Landing.js−7 src/views/Ledger.js−220 src/views/Login/Keystore.js+11−10 src/views/Login/Ledger.js+1−1 src/views/Login/Mnemonic.js+32−23 src/views/Login/PrivateKey.js+12−19 src/views/Login/Shards.js+39−35 src/views/Login/Ticket.js+63−68 src/views/Login/Trezor.js+1−1 src/views/Mnemonic.js−78 src/views/Network.js−62 src/views/Point.js+7−8 src/views/Point/Actions.js+25−48 src/views/PrivateKey.js−84 src/views/Shards.js−212 src/views/Ticket.js−154 src/views/Trezor.js−146 src/views/ViewPoint.js+25−17 src/views/Wallet.js−101
package-lock.json+5
@@ -14511,6 +14511,11 @@
1451114511 }
1451214512 }
1451314513 },
14514+ "react-teleporter": {
14515+ "version": "1.1.0",
14516+ "resolved": "https://registry.npmjs.org/react-teleporter/-/react-teleporter-1.1.0.tgz",
14517+ "integrity": "sha512-6j3I29y3sRcekOP1IfbIbrrtwZGKQhRU3WFZoWgMtXN0lTA2/i9Yp3zzAkVBFRtZIfhux4M7tSYVu4AQjCRDlQ=="
14518+ },
1451414519 "read-pkg": {
1451514520 "version": "1.1.0",
1451614521 "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
package.json+1
@@ -36,6 +36,7 @@
3636 "react": "^16.8.6",
3737 "react-dom": "^16.8.6",
3838 "react-scripts": "3.0.1",
39+ "react-teleporter": "^1.1.0",
3940 "secp256k1": "^3.5.2",
4041 "sigil-js": "github:urbit/sigil-js#f7f29c5",
4142 "trezor-connect": "^7.0.1",
src/Bridge.js+4−3
@@ -24,9 +24,10 @@ const kIsStubbed = isDevelopment && shouldStubLocal;
2424const kInitialRoutes = kIsStubbed
2525 ? [
2626 { key: ROUTE_NAMES.LANDING },
27− { key: ROUTE_NAMES.NETWORK },
28− { key: ROUTE_NAMES.WALLET },
27+ { key: ROUTE_NAMES.LOGIN },
2928 { key: ROUTE_NAMES.POINTS },
29+ { key: ROUTE_NAMES.POINT },
30+ { key: ROUTE_NAMES.INVITE },
3031 ]
3132 : [{ key: ROUTE_NAMES.LANDING }];
3233
@@ -39,7 +40,7 @@ const kInitialWallet = kIsStubbed
3940const kInitialMnemonic = kIsStubbed
4041 ? Just(process.env.REACT_APP_DEV_MNEMONIC)
4142 : Nothing();
42−const kInitialPointCursor = kIsStubbed ? Just(0) : Nothing();
43+const kInitialPointCursor = kIsStubbed ? Just(65792) : Nothing();
4344
4445export default function Bridge() {
4546 return (
src/_walletgen/Walletgen.js+1−1
@@ -4,7 +4,7 @@ import { find } from 'lodash';
44import Header from './components/Header';
55import Footer from './components/Footer';
66
7−import { PROFILE_STATES, GEN_STATES } from './lib/constants';
7+import { PROFILE_STATES, GEN_STATES } from '../lib/constants';
88
99import PrivacyPolicy from './views/PrivacyPolicy';
1010import Welcome from './views/Welcome';
src/_walletgen/views/Generate.js+7−2
@@ -2,13 +2,18 @@ import React from 'react';
22import * as sigil from 'sigil-js';
33import ob from 'urbit-ob';
44import classnames from 'classnames';
5−import * as wg from '../lib/lib';
5+import * as wg from '../../lib/walletgen';
66
77import Button from '../components/Button';
88import ReactSVGComponents from '../components/ReactSVGComponents';
99import PaperCollateralRenderer from 'PaperCollateralRenderer';
1010
11−import { MAX_GALAXY, MIN_STAR, MAX_STAR, GEN_STATES } from '../lib/constants';
11+import {
12+ MAX_GALAXY,
13+ MIN_STAR,
14+ MAX_STAR,
15+ GEN_STATES,
16+} from '../../lib/constants';
1217
1318const NEXT_STEP_NUM = 6;
1419
src/_walletgen/views/Upload.js+1−1
@@ -5,7 +5,7 @@ import UploadButton from '../components/UploadButton';
55
66import { compose } from '../../bridge/lib/lib';
77
8−import { PROFILE_STATES } from '../lib/constants';
8+import { PROFILE_STATES } from '../../lib/constants';
99
1010const NEXT_STEP_NUM = 3;
1111
src/assets/back.svgnew file+4
@@ -0,0 +1,4 @@
1+<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2+<path d="M13.8636 6.54545H4.56818V6.63636L8.06818 10.1364L7.20455 11L2.11364 5.90909L7.20455 0.818182L8.06818 1.68182L4.56818 5.18182V5.27273H13.8636V6.54545Z" fill="black"/>
3+<path d="M0 14.0227H16.0909V15.2727H0V14.0227Z" fill="black"/>
4+</svg>
src/components/Buttons.js+2−2
@@ -2,5 +2,5 @@ import React from 'react';
22import { Button } from 'indigo-react';
33
44// TODO: use svg icons
5−export const ForwardButton = props => <Button icon="→" {...props} />;
6−export const DownloadButton = props => <Button icon="⬇" {...props} />;
5+export const ForwardButton = props => <Button accessory="→" {...props} />;
6+export const DownloadButton = props => <Button accessory="⬇" {...props} />;
src/components/Footer.js+4−13
@@ -1,17 +1,8 @@
1−import React, { Children } from 'react';
2−import { createPortal } from 'react-dom';
1+import { createTeleporter } from 'react-teleporter';
32
4−const kFooterPortalId = 'footer-portal';
3+const FooterTeleporter = createTeleporter();
54
6−function Footer({ children }) {
7− return createPortal(
8− Children.only(children),
9− document.getElementById(kFooterPortalId)
10− );
11−}
12−
13−Footer.Portal = function FooterPortal(props) {
14− return <div {...props} id={kFooterPortalId} />;
15−};
5+const Footer = FooterTeleporter.Source;
6+Footer.Target = FooterTeleporter.Target;
167
178export default Footer;
src/components/Inputs.js+104−56
@@ -10,10 +10,10 @@ import {
1010 validateMnemonic,
1111 validatePoint,
1212 validateTicket,
13+ validateEmail,
1314} from 'lib/validators';
1415import { prependSig } from 'lib/transformers';
15−
16−const kExampleMnemonic = bip39.generateMnemonic();
16+import useForm from 'indigo-react/lib/useForm';
1717
1818// const RequiredInput = advancedInput({
1919// WrappedComponent: Input,
@@ -35,38 +35,47 @@ const kExampleMnemonic = bip39.generateMnemonic();
3535// validators: [validateEthereumAddress, validateNotEmpty],
3636// });
3737
38−export function PassphraseInput(props) {
39− return (
40− <Input
41− type="password"
42− autoComplete="off"
43− placeholder="Passphrase"
44− {...props}
45− />
38+const firstOf = state => state.inputs[0];
39+
40+export function usePassphraseInput(props) {
41+ return firstOf(
42+ useForm([
43+ {
44+ type: 'password',
45+ autoComplete: 'off',
46+ placeholder: 'Passphrase',
47+ ...props,
48+ },
49+ ])
4650 );
4751}
4852
49−export function MnemonicInput(props) {
50− return (
51− <Input
52− type="text"
53− autoComplete="off"
54− placeholder={`e.g. ${kExampleMnemonic}`}
55− validators={[validateMnemonic, validateNotEmpty]}
56− {...props}
57− />
53+const kExampleMnemonic = bip39.generateMnemonic();
54+const kMnemonicValidators = [validateMnemonic, validateNotEmpty];
55+export function useMnemonicInput(props) {
56+ return firstOf(
57+ useForm([
58+ {
59+ type: 'text',
60+ autoComplete: 'off',
61+ placeholder: `e.g. ${kExampleMnemonic}`,
62+ validators: kMnemonicValidators,
63+ ...props,
64+ },
65+ ])
5866 );
5967}
6068
61−export function HdPathInput(props) {
62− return (
63− <Input
64− type="text"
65− autoComplete="off"
66− placeholder={`${DEFAULT_HD_PATH}`}
67− validators={[validateNotEmpty]}
68− {...props}
69− />
69+export function useHdPathInput(props) {
70+ return firstOf(
71+ useForm([
72+ {
73+ type: 'text',
74+ autoComplete: 'off',
75+ placeholder: `e.g. ${DEFAULT_HD_PATH}`,
76+ ...props,
77+ },
78+ ])
7079 );
7180}
7281
@@ -77,42 +86,81 @@ export function HdPathInput(props) {
7786// });
7887
7988export function PointInput(props) {
89+ const input = usePointInput({
90+ name: 'point',
91+ label: 'Point Name',
92+ autoFocus: true,
93+ });
94+ const { data: validPointName, pass, focused, error } = input;
95+
8096 return (
8197 <Input
82− type="text"
83− placeholder="e.g. ~zod"
84− initialValue={props.initialValue || '~'}
85− validators={[validatePoint, validateNotEmpty]}
86− transformers={[prependSig]}
87− mono
98+ {...props}
99+ {...input}
88100 accessory={
89− <InputSigil
90− patp={props.initialValue || '~'}
91− size={68}
92− margin={8}
93− pass={props.pass}
94− onPass={props.setPass}
95− focused={props.focused}
96− onFocus={props.setFocused}
97− error={props.error}
98− onError={props.setError}
99− />
101+ validPointName && (
102+ <InputSigil
103+ patp={validPointName}
104+ size={68}
105+ margin={8}
106+ pass={pass}
107+ focused={focused}
108+ error={error}
109+ />
110+ )
100111 }
101− {...props}
102112 />
103113 );
104114}
105115
116+const kTicketValidators = [validateTicket, validateNotEmpty];
117+const kTicketTransformers = [prependSig];
106118//TODO needs to be fancier, displaying sig and dashes instead of •ing all
107−export function TicketInput(props) {
108− return (
109− <Input
110− type="password"
111− placeholder="~master-ticket"
112− validators={[validateTicket, validateNotEmpty]}
113− transformers={[prependSig]}
114− mono
115− {...props}
116− />
119+export function useTicketInput({ initialValue = '~', ...rest }) {
120+ return firstOf(
121+ useForm([
122+ {
123+ type: 'password',
124+ placeholder: '~master-ticket',
125+ validators: kTicketValidators,
126+ transformers: kTicketTransformers,
127+ mono: true,
128+ initialValue,
129+ ...rest,
130+ },
131+ ])
117132 );
118133}
134+
135+const kPointValidators = [validatePoint, validateNotEmpty];
136+const kPointTransformers = [prependSig];
137+export function usePointInput({ initialValue = '~', ...rest }) {
138+ return firstOf(
139+ useForm([
140+ {
141+ type: 'text',
142+ placeholder: 'e.g. ~zod',
143+ validators: kPointValidators,
144+ transformers: kPointTransformers,
145+ mono: true,
146+ initialValue,
147+ ...rest,
148+ },
149+ ])
150+ );
151+}
152+
153+const kEmailValidators = [validateEmail, validateNotEmpty];
154+export const buildEmailInputConfig = extra => ({
155+ type: 'email',
156+ autoComplete: 'off',
157+ validators: kEmailValidators,
158+ initialValue: '',
159+ ...extra,
160+});
161+
162+// const TicketInput = advancedInput({
163+// WrappedComponent: Input,
164+// validators: [validateTicket, validateNotEmpty],
165+// transformers: [prependSig],
166+// });
src/components/LoadableButton.jsnew file+6
@@ -0,0 +1,6 @@
1+import React from 'react';
2+import { Button } from 'indigo-react';
3+
4+export default function LoadableButton({ ...rest }) {
5+ return <Button {...rest}></Button>;
6+}
src/components/MiniBackButton.jsnew file+8
@@ -0,0 +1,8 @@
1+import React from 'react';
2+import cn from 'classnames';
3+
4+import { ReactComponent as Back } from 'assets/back.svg';
5+
6+export default ({ className, ...rest }) => (
7+ <Back className={cn('black pointer', className)} {...rest} />
8+);
src/components/View.js+1−1
@@ -20,7 +20,7 @@ function View({ className, children, full = false, ...rest }) {
2020 )}
2121 {...rest}>
2222 <Flex.Item className="pv5">{children}</Flex.Item>
23− <Footer.Portal />
23+ <Footer.Target />
2424 </Flex>
2525 );
2626}
src/components/old/Header.js+1−5
@@ -5,18 +5,14 @@ import { getRouteBreadcrumb } from '../../lib/router';
55import { ROUTE_NAMES } from '../../lib/routeNames';
66import { useHistory } from '../../store/history';
77import { isLast } from '../../lib/lib';
8−import { useNetwork } from '../../store/network';
9−import { useWallet } from '../../store/wallet';
108import { usePointCursor } from '../../store/pointCursor';
119
1210// hook to create a breadcrumb builder function based on the current
1311// global states
1412function useRouteBreadcrumbBuilder() {
15− const { networkType } = useNetwork();
16− const { wallet } = useWallet();
1713 const { pointCursor } = usePointCursor();
1814
19− return getRouteBreadcrumb({ pointCursor, networkType, wallet });
15+ return getRouteBreadcrumb({ pointCursor });
2016}
2117
2218function Crumbs(props) {
src/components/old/HorizontalSelector.js+1−1
@@ -22,8 +22,8 @@ class HorizontalSelector extends React.Component {
2222 let active = opt.value === this.state.option ? 'btn-primary active' : '';
2323 return (
2424 <button
25+ key={opt.value.toString()}
2526 className={`btn ${active} h-10 ph-4 flex sans ${this.props.className}`}
26− value={opt.value}
2727 onClick={() => this.selectOption(opt.value)}>
2828 {opt.title}
2929 </button>
src/indigo-react/components/Button.js+10−6
@@ -10,7 +10,8 @@ export default function Button({
1010 disabled = false,
1111 detail,
1212 className,
13− icon = '→',
13+ accessory = '→',
14+ onClick,
1415 children,
1516 ...rest
1617}) {
@@ -24,12 +25,14 @@ export default function Button({
2425 as="a"
2526 gap={4}
2627 className={cn(
27− 'pointer p4 truncate flex-row justify-between',
28+ 'pointer pv4 truncate flex-row justify-between',
2829 {
29− 'bg-black bg-gray6-hover': solid && !disabled,
30+ p4: solid,
31+ },
32+ {
33+ 'bg-black': solid && !disabled,
3034 'bg-gray3': solid && disabled,
31− 'bg-transparent bg-gray1-hover': !solid && !disabled,
32− 'bg-transparent': !solid && disabled,
35+ 'bg-transparent': !solid,
3336 },
3437 className
3538 )}
@@ -39,10 +42,11 @@ export default function Button({
3942 cursor: 'not-allowed',
4043 }),
4144 }}
45+ onClick={onClick ? () => !disabled && onClick() : undefined}
4246 {...rest}>
4347 <Grid.Item as={Flex} justify="between" full>
4448 <span className={cn(textColor)}>{children}</span>
45− <div className={cn('pl4', textColor)}>{icon}</div>
49+ <div className={cn('pl4', textColor)}>{accessory}</div>
4650 </Grid.Item>
4751 {detail && (
4852 <Grid.Item as={HelpText} full>
src/indigo-react/components/Flex.js+1−1
@@ -1,7 +1,7 @@
11import React from 'react';
22import cn from 'classnames';
33
4−import getComponent from 'indigo-react/lib/getComponent';
4+import getComponent from '../lib/getComponent';
55
66function Flex({
77 as = 'div',
src/indigo-react/components/Grid.js+6
@@ -21,6 +21,8 @@ Grid.Item = function GridItem({
2121 fourth = 0,
2222 rows = [],
2323 cols = [],
24+ justifySelf,
25+ alignSelf,
2426 className,
2527 ...rest
2628}) {
@@ -38,6 +40,10 @@ Grid.Item = function GridItem({
3840 [`r${rows[0]}-${rows[1]}`]: rows.length === 2,
3941 [`c${cols[0]}-${cols[1]}`]: cols.length === 2,
4042 },
43+ {
44+ [`justify-self-${justifySelf}`]: justifySelf,
45+ [`align-self-${alignSelf}`]: alignSelf,
46+ },
4147 className
4248 )}
4349 {...rest}
src/indigo-react/components/IconButton.jsnew file+48
@@ -0,0 +1,48 @@
1+import React from 'react';
2+import cn from 'classnames';
3+
4+import Flex from './Flex';
5+
6+export default function IconButton({
7+ children,
8+ className,
9+ onClick,
10+ disabled = false,
11+ solid = false,
12+ ...rest
13+}) {
14+ return (
15+ <Flex
16+ as="a"
17+ align="center"
18+ justify="center"
19+ className={cn(
20+ 'pointer p2',
21+ {
22+ 'bg-black': solid && !disabled,
23+ 'bg-gray3': !solid || disabled,
24+ },
25+ {
26+ white: solid,
27+ black: !solid && !disabled,
28+ gray4: !solid && disabled,
29+ },
30+ className
31+ )}
32+ style={{
33+ width: '1rem',
34+ height: '1rem',
35+ fontSize: '1rem',
36+ fontWeight: 'bold',
37+ // TODO: ^ replace with 1:1 svgs and remove explicit sizing
38+ ...(disabled && {
39+ pointerEvents: 'none',
40+ cursor: 'not-allowed',
41+ }),
42+ }}
43+ onClick={onClick ? () => !disabled && onClick() : undefined}
44+ {...rest}>
45+ {children}
46+ </Flex>
47+ );
48+}
src/indigo-react/components/Input.js+46−140
@@ -1,172 +1,74 @@
1−import React, { useCallback, useEffect, useMemo, useState } from 'react';
1+import React, { useCallback, useEffect } from 'react';
22import cn from 'classnames';
33
4−import { Flex } from 'indigo-react';
5−
6−import { compose } from 'lib/lib';
7−import { kDefaultValidator } from 'lib/validators';
8−
9−function useInput({
10− validators = [],
11− transformers = [],
12− initialValue = '',
13− autoFocus = false,
14−}) {
15− const [value, setValue] = useState(initialValue);
16− const [focused, setFocused] = useState(autoFocus);
17− const [hasBeenFocused, setHasBeenFocused] = useState(false);
18−
19− // NB(shrugs)
20− // TL;DR: This is what we want, the exhaustive deps rule is being too nosy.
21− //
22− // Here we memo-construct the `validate` and `transform` functions
23− // based on the prop arguments. We explicitly disable the exhausive deps rule
24− // because eslint wants an inline array specifying our dependencies.
25− //
26− // If we were to place the array of validators or transformers into
27− // the dependency array like `[validators]`, we would end up creating a new
28− // `validate` function every render, since deps are shallow-compared with
29− // `Object.is`. Because the array of validators are constructed as part of
30− // a component's render() function like:
31− //
32− // export function MnemonicInput(props) {
33− // return (
34− // <Input
35− // type="text"
36− // autoComplete="off"
37− // placeholder={`e.g. ${kExampleMnemonic}`}
38− // validators={[validateMnemonic, validateNotEmpty]}
39− // {...props}
40− // />
41− // );
42− // }
43− //
44− // we create a new array, which shallowly compares as not-equal
45− // between renders.
46− //
47− // So, in this specific case, we disable the rule and pass the
48− // array of validators (and transformers) themselves as dependencies.
49− // This means that validators and transformers can change between renders
50− // and we will re-create the composed `validate` and `transform` functions
51− // as expected.
52− //
53− // For context, an alternative solution I tried was defining the array of
54− // validators as a constant array outside of the render method, but:
55− // 1. It wasn't as clean or useful as specifying them inline,
56− // 2. It would mean that validators could not be updated between renders,
57− // 3. And the situation we have now feels naturally more 'correct'.
58− //
59− /* eslint-disable react-hooks/exhaustive-deps */
60− const transform = useMemo(() => compose(...transformers), transformers);
61− const validate = useMemo(
62− () =>
63− compose(
64− ...validators,
65− kDefaultValidator
66− ),
67− validators
68− );
69− /* eslint-enable react-hooks/exhaustive-deps */
70−
71− const onChange = useCallback(
72− e => setValue(transform(e.target.value)), //
73− [transform]
74− );
75− const onFocus = useCallback(
76− e => setFocused(true), //
77− [setFocused]
78− );
79− const onBlur = useCallback(
80− e => {
81− setFocused(false);
82− setHasBeenFocused(true);
83− },
84− [setFocused, setHasBeenFocused]
85− );
86−
87− // memo-compute whether or not the current value is valid
88− const { pass, error: _error, data } = useMemo(
89− () => validate(value), //
90− [validate, value]
91− );
92−
93− // only set error if it should be visible
94− const error = !pass && hasBeenFocused && !focused && _error;
95− // visibly tell the user that their input has passed if it has passed
96− // and they are or have interacted with the input before
97− const visiblyPassed = pass && (hasBeenFocused || focused);
98−
99− return {
100− pass,
101− visiblyPassed,
102− error,
103− data,
104− focused,
105− bind: {
106− value,
107− onChange,
108− focused: focused ? 'true' : 'false',
109− // ^ dom wants string value for focused
110− onFocus,
111− onBlur,
112− },
113− };
114−}
4+import Flex from './Flex';
1155
1166export default function Input({
7+ // visuals
1178 name,
1189 label,
119− initialValue = '',
120− className = '',
121− validators = [],
122− transformers = [],
10+ className,
11+ accessory,
12+ mono = false,
13+
14+ // callbacks
12315 onValue,
12416 onPass,
12517 onError,
12618 onFocus,
12719 onEnter,
128− accessory,
129− disabled = false,
130− mono = false,
131− autoFocus = false,
20+
21+ // state from hook
22+ focused,
23+ pass,
24+ visiblyPassed,
25+ error,
26+ data,
27+ bind,
28+ autoFocus,
29+ disabled,
30+
31+ // ignored
32+ initialValue,
33+ validators,
34+ transformers,
35+
36+ // extra
13237 ...rest
13338}) {
134− const actuallyAutoFocus = autoFocus && !disabled;
135− const { focused, pass, visiblyPassed, error, data, bind } = useInput({
136− validators,
137− transformers,
138− initialValue,
139− autoFocus: actuallyAutoFocus,
140− });
39+ // NB(shrugs): we disable exhaustive deps because we don't want the callbacks
40+ // (whose identity might change between renders) to trigger a re-notify of
41+ // state that we already know
42+ // this also happens to prevent render loops when rendering a map of inputs
14143
14244 // notify parent of enter keypress iff not disabled
14345 const onKeyPress = useCallback(
14446 e => !disabled && e.key === 'Enter' && onEnter && onEnter(),
145− [disabled, onEnter]
47+ [disabled] // eslint-disable-line react-hooks/exhaustive-deps
14648 );
14749
14850 // notify parent of value only when passing
14951 useEffect(() => {
15052 pass && onValue && onValue(data);
151− }, [pass, data, onValue]);
53+ }, [pass, data]); // eslint-disable-line react-hooks/exhaustive-deps
15254
15355 // notify parent of pass
15456 useEffect(() => {
15557 onPass && onPass(pass);
156− }, [pass, onPass]);
58+ }, [pass]); // eslint-disable-line react-hooks/exhaustive-deps
15759
15860 // notify parent of error whenever error changes
15961 useEffect(() => {
16062 onError && onError(error);
161− }, [error, onError]);
63+ }, [error]); // eslint-disable-line react-hooks/exhaustive-deps
16264
16365 // notify parent of focus
16466 useEffect(() => {
16567 onFocus && onFocus(focused);
166− }, [focused, onFocus]);
68+ }, [focused]); // eslint-disable-line react-hooks/exhaustive-deps
16769
16870 return (
169− <Flex col>
71+ <Flex className={className} col>
17072 <Flex.Item as="label" className="f6 lh-tall" htmlFor={name}>
17173 {label}
17274 </Flex.Item>
@@ -176,7 +78,7 @@ export default function Input({
17678 {...rest}
17779 cols={[1, accessory ? 11 : 13]}
17880 className={cn(
179− 'b b1 p3',
81+ 'b b1 p3 outline-none',
18082 { mono },
18183 {
18284 'bg-white': !disabled,
@@ -191,20 +93,24 @@ export default function Input({
19193 'b-black': focused && !visiblyPassed,
19294 'b-yellow3': !focused && !visiblyPassed && error,
19395 'b-gray3': !focused && !error && !visiblyPassed,
194− },
195− className
96+ }
97+ // TODO: inputClassName ?
19698 )}
19799 style={{
198− ...(disabled && { pointerEvents: 'none', cursor: 'not-allowed' }),
100+ ...(disabled && {
101+ pointerEvents: 'none',
102+ cursor: 'not-allowed',
103+ }),
199104 }}
200− onKeyPress={onKeyPress}
201105 id={name}
202− autoFocus={actuallyAutoFocus}
106+ name={name}
107+ onKeyPress={onKeyPress}
203108 {...bind}
204109 flex
205110 />
206111 {accessory && (
207− <Flex.Item style={{ height: '45px', overflow: 'hidden' }}>
112+ <Flex.Item
113+ style={{ height: '45px', width: '45px', overflow: 'hidden' }}>
208114 {accessory}
209115 </Flex.Item>
210116 )}
src/indigo-react/index.js+1
@@ -3,5 +3,6 @@ export { default as Grid } from './components/Grid';
33export { default as Flex } from './components/Flex';
44export { default as Input } from './components/Input';
55export { default as Button } from './components/Button';
6+export { default as IconButton } from './components/IconButton';
67
78export * from './components/Typography';
src/indigo-react/indigo.css+66−21
@@ -13,7 +13,7 @@
1313 * git+https://github.com/urbit/Indigo.git
1414 * Bugs: https://github.com/urbit/Indigo/issues
1515 * License: MIT
16− * Timestamp: Wed Jun 19 2019 17:17:14 GMT-0700 (Pacific Daylight Time)
16+ * Timestamp: Mon Jun 24 2019 19:57:25 GMT-0700 (Pacific Daylight Time)
1717 *******************************************************************************/
1818/**********************
1919Color variables
@@ -1575,19 +1575,34 @@ caption,
15751575 justify-content: center;
15761576 align-items: center; }
15771577
1578−.self-start {
1578+.align-self-start {
15791579 align-self: flex-start; }
15801580
1581−.self-end {
1581+.align-self-end {
15821582 align-self: flex-end; }
15831583
1584−.self-center {
1584+.align-self-center {
15851585 align-self: center; }
15861586
1587−.self-baseline {
1587+.align-self-baseline {
15881588 align-self: baseline; }
15891589
1590−.self-stretch {
1590+.align-self-stretch {
1591+ align-self: stretch; }
1592+
1593+.justify-self-start {
1594+ justify-self: flex-start; }
1595+
1596+.justify-self-end {
1597+ justify-self: flex-end; }
1598+
1599+.justify-self-center {
1600+ justify-self: center; }
1601+
1602+.justify-self-baseline {
1603+ justify-self: baseline; }
1604+
1605+.align-self-stretch {
15911606 align-self: stretch; }
15921607
15931608.content-start {
@@ -1648,16 +1663,26 @@ caption,
16481663 .flex-center-ns {
16491664 justify-content: center;
16501665 align-items: center; }
1651− .self-start-ns {
1666+ .align-self-start-ns {
16521667 align-self: flex-start; }
1653− .self-end-ns {
1668+ .align-self-end-ns {
16541669 align-self: flex-end; }
1655− .self-center-ns {
1670+ .align-self-center-ns {
16561671 align-self: center; }
1657− .self-baseline-ns {
1672+ .align-self-baseline-ns {
16581673 align-self: baseline; }
1659− .self-stretch-ns {
1674+ .align-self-stretch-ns {
16601675 align-self: stretch; }
1676+ .justify-self-start-ns {
1677+ justify-self: flex-start; }
1678+ .justify-self-end-ns {
1679+ justify-self: flex-end; }
1680+ .justify-self-center-ns {
1681+ justify-self: center; }
1682+ .justify-self-baseline-ns {
1683+ justify-self: baseline; }
1684+ .justify-self-stretch-ns {
1685+ justify-self: stretch; }
16611686 .content-start-ns {
16621687 align-content: flex-start; }
16631688 .content-end-ns {
@@ -1711,16 +1736,26 @@ caption,
17111736 .flex-center-md {
17121737 justify-content: center;
17131738 align-items: center; }
1714− .self-start-md {
1739+ .align-self-start-md {
17151740 align-self: flex-start; }
1716− .self-end-md {
1741+ .align-self-end-md {
17171742 align-self: flex-end; }
1718− .self-center-md {
1743+ .align-self-center-md {
17191744 align-self: center; }
1720− .self-baseline-md {
1745+ .align-self-baseline-md {
17211746 align-self: baseline; }
1722− .self-stretch-md {
1747+ .align-self-stretch-md {
17231748 align-self: stretch; }
1749+ .justify-self-start-md {
1750+ justify-self: flex-start; }
1751+ .justify-self-end-md {
1752+ justify-self: flex-end; }
1753+ .justify-self-center-md {
1754+ justify-self: center; }
1755+ .justify-self-baseline-md {
1756+ justify-self: baseline; }
1757+ .justify-self-stretch-md {
1758+ justify-self: stretch; }
17241759 .content-start-md {
17251760 align-content: flex-start; }
17261761 .content-end-md {
@@ -1774,16 +1809,26 @@ caption,
17741809 .flex-center-lg {
17751810 justify-content: center;
17761811 align-items: center; }
1777− .self-start-lg {
1812+ .align-self-start-lg {
17781813 align-self: flex-start; }
1779− .self-end-lg {
1814+ .align-self-end-lg {
17801815 align-self: flex-end; }
1781− .self-center-lg {
1816+ .align-self-center-lg {
17821817 align-self: center; }
1783− .self-baseline-lg {
1818+ .align-self-baseline-lg {
17841819 align-self: baseline; }
1785− .self-stretch-lg {
1820+ .align-self-stretch-lg {
17861821 align-self: stretch; }
1822+ .justify-self-start-lg {
1823+ justify-self: flex-start; }
1824+ .justify-self-end-lg {
1825+ justify-self: flex-end; }
1826+ .justify-self-center-lg {
1827+ justify-self: center; }
1828+ .justify-self-baseline-lg {
1829+ justify-self: baseline; }
1830+ .justify-self-stretch-lg {
1831+ justify-self: stretch; }
17871832 .content-start-lg {
17881833 align-content: flex-start; }
17891834 .content-end-lg {
src/indigo-react/lib/useForm.jsnew file+171
@@ -0,0 +1,171 @@
1+import { useCallback, useMemo, useState } from 'react';
2+import { keyBy, get, every, some } from 'lodash';
3+
4+import { compose } from 'lib/lib';
5+import { kDefaultValidator } from 'lib/validators';
6+
7+// interface InputConfig {
8+// name: string;
9+// validators: Function[];
10+// transformers: Function[];
11+// initialValue: string;
12+// autoFocus: boolean;
13+// disabled: boolean;
14+// ...extra,
15+// }
16+
17+const defaultsFor = (configs, mapper) =>
18+ configs.reduce(
19+ (memo, config) => ({ ...memo, [config.name]: mapper(config) }),
20+ {}
21+ );
22+
23+/**
24+ * useForm managest a set of inputs for rendering them in a loop
25+ */
26+export default function useForm(configs = []) {
27+ const byName = useMemo(() => keyBy(configs, 'name'), [configs]);
28+
29+ // track values
30+ const [values, _setValues] = useState(() =>
31+ defaultsFor(configs, config => config.initialValue)
32+ );
33+ const setValue = useCallback(
34+ (name, value) => _setValues(values => ({ ...values, [name]: value })),
35+ [_setValues]
36+ );
37+
38+ // track focused states
39+ const [focused, _setFocused] = useState(() =>
40+ defaultsFor(configs, config => config.autoFocus && !config.disabled)
41+ );
42+ const focuses = useMemo(
43+ () => configs.map(config => focused[config.name] || false), //
44+ [configs, focused]
45+ );
46+
47+ const setFocused = useCallback(
48+ (name, value) => _setFocused(values => ({ ...values, [name]: value })),
49+ [_setFocused]
50+ );
51+
52+ // track whether or not input has been focused
53+ const [hasBeenFocused, _setHasBeenFocused] = useState({});
54+ const setHasBeenFocused = useCallback(
55+ (name, value) =>
56+ _setHasBeenFocused(values => ({ ...values, [name]: value })),
57+ [_setHasBeenFocused]
58+ );
59+
60+ const transform = useCallback(
61+ (name, value) => compose(...get(byName, [name, 'transformers'], []))(value),
62+ [byName]
63+ );
64+ const validate = useCallback(
65+ (name, value) =>
66+ compose(
67+ ...get(byName, [name, 'validators'], []),
68+ kDefaultValidator
69+ )(value),
70+ [byName]
71+ );
72+
73+ const onChange = useCallback(
74+ name => e => setValue(name, transform(name, e.target.value)), //
75+ [setValue, transform]
76+ );
77+ const onFocus = useCallback(
78+ name => e => setFocused(name, true), //
79+ [setFocused]
80+ );
81+ const onBlur = useCallback(
82+ name => e => {
83+ setFocused(name, false);
84+ setHasBeenFocused(name, true);
85+ },
86+ [setFocused, setHasBeenFocused]
87+ );
88+
89+ // memo-compute whether or not the current value is valid
90+ const validations = useMemo(
91+ () => configs.map(config => validate(config.name, values[config.name])), //
92+ [configs, validate, values]
93+ );
94+
95+ const datas = useMemo(() => validations.map(v => v.data), [validations]);
96+ const passes = useMemo(() => validations.map(v => v.pass), [validations]);
97+
98+ // the input has errored if it
99+ // 1) did not pass validation
100+ // 2) has an error text
101+ // 3) has been focused before
102+ // 4) is not currently focused
103+ // returning the error text at the end
104+ const errors = useMemo(
105+ () =>
106+ configs.map(
107+ (config, i) =>
108+ !validations[i].pass &&
109+ hasBeenFocused[config.name] &&
110+ !focused[config.name] &&
111+ validations[i].error
112+ ),
113+ [configs, validations, hasBeenFocused, focused]
114+ );
115+
116+ // visibly tell the user that their input has passed if
117+ // 1) it has passed
118+ // 2) they are or have interacted with the input before
119+ const visiblePasses = useMemo(
120+ () =>
121+ configs.map(
122+ (config, i) =>
123+ validations[i].pass &&
124+ (hasBeenFocused[config.name] || focused[config.name])
125+ ),
126+ [configs, validations, hasBeenFocused, focused]
127+ );
128+
129+ const inputs = useMemo(
130+ () =>
131+ configs.map((config, i) => ({
132+ // Input props
133+ data: datas[i],
134+ pass: passes[i],
135+ visiblyPassed: visiblePasses[i],
136+ error: errors[i],
137+ focused: focuses[i],
138+ ...config,
139+ autoFocus: config.autoFocus && !config.disabled,
140+ // dom properties below:
141+ bind: {
142+ value: values[config.name],
143+ onChange: onChange(config.name),
144+ onFocus: onFocus(config.name),
145+ onBlur: onBlur(config.name),
146+ autoFocus: config.autoFocus && !config.disabled,
147+ },
148+ })),
149+ [
150+ configs,
151+ datas,
152+ visiblePasses,
153+ passes,
154+ errors,
155+ focuses,
156+ values,
157+ onBlur,
158+ onChange,
159+ onFocus,
160+ ]
161+ );
162+
163+ const pass = every(passes);
164+ const error = some(errors);
165+
166+ return {
167+ inputs,
168+ pass,
169+ error,
170+ };
171+}
src/lib/LocalRouter.jsnew file+9
@@ -0,0 +1,9 @@
1+import { createContext, useContext } from 'react';
2+
3+export const LocalRouterContext = createContext(null);
4+export const LocalRouterProvider = LocalRouterContext.Provider;
5+
6+// Hook version
7+export function useLocalRouter() {
8+ return useContext(LocalRouterContext);
9+}
src/_walletgen/lib/constants.js → src/lib/constants.jsrenamed+1
@@ -11,6 +11,7 @@ const GALAXY_ENTROPY_BITS = 384;
1111
1212const SEED_ENTROPY_BITS = 128;
1313
14+// TODO: this is walletgen-ui specific, move into a wallet router later
1415const GEN_STATES = {
1516 ENY_NOSTART: Symbol('ENY_NOSTART'),
1617 ENY_PENDING: Symbol('ENY_PENDING'),
src/lib/invite.js+1−1
@@ -3,7 +3,7 @@ import Tx from 'ethereumjs-tx';
33
44import * as azimuth from 'azimuth-js';
55import * as kg from 'urbit-key-generation/dist';
6−import * as wg from '../_walletgen/lib/lib.js';
6+import * as wg from './walletgen';
77import * as tank from './tank';
88
99import JSZip from 'jszip';
src/lib/routeNames.js−9
@@ -11,15 +11,6 @@ export const ROUTE_NAMES = {
1111 ACCEPT_TRANSFER: Symbol('ACCEPT_TRANSFER'),
1212 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
1313 LANDING: Symbol('LANDING'),
14− NETWORK: Symbol('NETWORK'),
15− WALLET: Symbol('WALLET'),
16− MNEMONIC: Symbol('MNEMONIC'),
17− TICKET: Symbol('TICKET'),
18− SHARDS: Symbol('SHARDS'),
19− LEDGER: Symbol('LEDGER'),
20− TREZOR: Symbol('TREZOR'),
21− PRIVATE_KEY: Symbol('PRIVATE_KEY'),
22− KEYSTORE: Symbol('KEYSTORE'),
2314 VIEW_POINT: Symbol('VIEW_POINT'),
2415 POINTS: Symbol('POINTS'),
2516 POINT: Symbol('POINT'),
src/lib/router.js+2−53
@@ -1,22 +1,15 @@
1−import React from 'react';
21import * as ob from 'urbit-ob';
32import * as need from './need';
43
54import Landing from '../views/Landing';
5+import Login from '../views/Login.js';
66import InviteTicket from '../views/InviteTicket';
77import InvitesManage from '../views/InvitesManage.js';
8−import Login from '../views/Login.js';
98import AcceptTransfer from '../views/AcceptTransfer';
109import CancelTransfer from '../views/CancelTransfer';
1110import CreateGalaxy from '../views/CreateGalaxy';
1211import GenKeyfile from '../views/GenKeyfile';
1312import IssueChild from '../views/IssueChild';
14−import Network from '../views/Network';
15−import Mnemonic from '../views/Mnemonic';
16−import Ledger from '../views/Ledger';
17−import Trezor from '../views/Trezor';
18−import PrivateKey from '../views/PrivateKey';
19−import Keystore from '../views/Keystore';
2013import ViewPoint from '../views/ViewPoint';
2114import SentTransaction from '../views/SentTransaction';
2215import SetKeys from '../views/SetKeys';
@@ -27,14 +20,9 @@ import {
2720} from '../views/SetProxy';
2821import Point from '../views/Point';
2922import Points from '../views/Points';
30−import Shards from '../views/Shards';
31−import Ticket from '../views/Ticket';
3223import Transfer from '../views/Transfer';
33−import Wallet from '../views/Wallet';
3424import Invite from 'views/Invite';
3525
36−import { EthereumWallet } from './wallet';
37−import { renderNetworkType } from './network';
3826import { ROUTE_NAMES } from './routeNames';
3927
4028export const ROUTES = {
@@ -46,16 +34,7 @@ export const ROUTES = {
4634 [ROUTE_NAMES.LOGIN]: Login,
4735 [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
4836 [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
49− [ROUTE_NAMES.NETWORK]: Network,
50− [ROUTE_NAMES.WALLET]: Wallet,
5137 [ROUTE_NAMES.VIEW_POINT]: ViewPoint,
52− [ROUTE_NAMES.MNEMONIC]: Mnemonic,
53− [ROUTE_NAMES.TICKET]: Ticket,
54− [ROUTE_NAMES.SHARDS]: Shards,
55− [ROUTE_NAMES.LEDGER]: Ledger,
56− [ROUTE_NAMES.TREZOR]: Trezor,
57− [ROUTE_NAMES.PRIVATE_KEY]: PrivateKey,
58− [ROUTE_NAMES.KEYSTORE]: Keystore,
5938 [ROUTE_NAMES.POINTS]: Points,
6039 [ROUTE_NAMES.POINT]: Point,
6140 [ROUTE_NAMES.SET_MANAGEMENT_PROXY]: SetManagementProxy,
@@ -69,11 +48,7 @@ export const ROUTES = {
6948 [ROUTE_NAMES.GEN_KEYFILE]: GenKeyfile,
7049};
7150
72−export const getRouteBreadcrumb = ({
73− pointCursor,
74− networkType,
75− wallet,
76−}) => route => {
51+export const getRouteBreadcrumb = ({ pointCursor }) => route => {
7752 switch (route.name) {
7853 case ROUTE_NAMES.INVITE_TICKET:
7954 return 'Invite code';
@@ -87,32 +62,6 @@ export const getRouteBreadcrumb = ({
8762 return 'Login to Bridge';
8863 case ROUTE_NAMES.POINT:
8964 return `Overview: ${ob.patp(need.pointCursor(pointCursor))}`;
90− case ROUTE_NAMES.NETWORK:
91− return renderNetworkType(networkType);
92− case ROUTE_NAMES.WALLET:
93− return wallet.matchWith({
94− Nothing: () => 'Wallet',
95− Just: wal =>
96− wal.value instanceof EthereumWallet ? (
97− <span className="text-mono">{wal.value.address}</span>
98− ) : (
99− <span className="text-mono">{wal.value.address}</span>
100− ),
101− });
102− case ROUTE_NAMES.MNEMONIC:
103− return 'Mnemonic';
104− case ROUTE_NAMES.TICKET:
105− return 'Urbit Ticket';
106− case ROUTE_NAMES.SHARDS:
107− return 'Urbit Ticket';
108− case ROUTE_NAMES.LEDGER:
109− return 'Ledger';
110− case ROUTE_NAMES.TREZOR:
111− return 'Trezor';
112− case ROUTE_NAMES.PRIVATE_KEY:
113− return 'Private Key';
114− case ROUTE_NAMES.KEYSTORE:
115− return 'Keystore File';
11665 case ROUTE_NAMES.POINTS:
11766 return 'Points';
11867 case ROUTE_NAMES.VIEW_POINT:
src/lib/useArray.jsnew file+36
@@ -0,0 +1,36 @@
1+import { useCallback, useState } from 'react';
2+
3+/**
4+ * Manages an immutable array of items.
5+ */
6+export default function useArray(initialItems = [], itemBuilder) {
7+ const [items, _setItems] = useState(initialItems);
8+
9+ const append = () => _setItems(items => [...items, itemBuilder()]);
10+
11+ const removeAt = i =>
12+ _setItems(items => {
13+ const newItems = [...items.slice(0, i), ...items.slice(i + 1)];
14+ console.log(items.length, newItems.length);
15+ return newItems;
16+ });
17+
18+ const updateAt = (i, update = {}) =>
19+ _setItems(items => [
20+ ...items.slice(0, i),
21+ {
22+ ...items[i],
23+ ...update,
24+ },
25+ ...items.slice(i + 1),
26+ ]);
27+
28+ return [
29+ items,
30+ {
31+ append,
32+ removeAt,
33+ updateAt,
34+ },
35+ ];
36+}
src/lib/useRenderCount.jsnew file+7
@@ -0,0 +1,7 @@
1+import { useRef } from 'react';
2+
3+export default function useRenderCount(debubgLabel = 'Component') {
4+ const count = useRef(0);
5+ count.current++;
6+ console.log(`${debubgLabel} — ${count.current}`);
7+}
src/lib/useSyncPoint.js+3−3
@@ -4,9 +4,9 @@ import { usePointCache } from 'store/pointCache';
44
55// sync a specified point to the cache
66export default function useSyncPoint(point) {
7− const { fetchPoint } = usePointCache();
7+ const { syncOwnedPoint } = usePointCache();
88
99 useEffect(() => {
10− fetchPoint(point);
11− }, [fetchPoint, point]);
10+ syncOwnedPoint(point);
11+ }, [syncOwnedPoint, point]);
1212}
src/lib/validators.js+36−13
@@ -3,6 +3,10 @@ import * as ob from 'urbit-ob';
33
44import { isValidAddress } from './wallet';
55
6+// via: https://emailregex.com/
7+const emailRegExp = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
8+
9+// is this hex?
610const hexRegExp = /[0-9A-Fa-f]{64}/g;
711
812// Wraps single validation functions in a controlled and predictable way.
@@ -18,7 +22,7 @@ export const simpleValidatorWrapper = config => {
1822 // Run the validator and return the result.
1923 return config.validator(config.prevMessage.data)
2024 ? newMessage(config.prevMessage.data, true, null)
21− : newMessage(config.prevMessage.data, false, config.errorMessage);
25+ : newMessage(config.prevMessage.data, false, config.error);
2226};
2327
2428// Validation message
@@ -45,15 +49,21 @@ export const validateMnemonic = m =>
4549 simpleValidatorWrapper({
4650 prevMessage: m,
4751 validator: d => bip39.validateMnemonic(d),
48− errorMessage: 'This is not a valid mnemonic.',
52+ error: 'This is not a valid mnemonic.',
4953 });
5054
5155// Checks an empty field
5256export const validateNotEmpty = m =>
5357 simpleValidatorWrapper({
5458 prevMessage: m,
55− validator: d => d.length > 1,
56− errorMessage: 'This field is required.',
59+ validator: d => {
60+ try {
61+ return d.length > 1;
62+ } catch {
63+ return false;
64+ }
65+ },
66+ error: 'This field is required.',
5767 });
5868
5969// Checks if a patp is a valid galaxy
@@ -69,7 +79,7 @@ export const validateGalaxy = m =>
6979 return false;
7080 }
7181 },
72− errorMessage: 'This is not a valid galaxy',
82+ error: 'This is not a valid galaxy',
7383 });
7484
7585export const validatePoint = m =>
@@ -82,7 +92,7 @@ export const validatePoint = m =>
8292 return false;
8393 }
8494 },
85− errorMessage: 'This is not a valid point',
95+ error: 'This is not a valid point',
8696 });
8797
8898export const validateTicket = m =>
@@ -95,7 +105,7 @@ export const validateTicket = m =>
95105 return false;
96106 }
97107 },
98− errorMessage: 'This is not a valid ticket',
108+ error: 'This is not a valid ticket',
99109 });
100110
101111export const validateShard = m =>
@@ -108,28 +118,34 @@ export const validateShard = m =>
108118 return false;
109119 }
110120 },
111− errorMessage: 'This is not a valid shard',
121+ error: 'This is not a valid shard',
112122 });
113123
114124export const validateLength = (m, l) =>
115125 simpleValidatorWrapper({
116126 prevMessage: m,
117− validator: d => d.length === l,
118− errorMessage: 'This is of an invalid length',
127+ validator: d => {
128+ try {
129+ return d.length === l;
130+ } catch {
131+ return false;
132+ }
133+ },
134+ error: 'This is of an invalid length',
119135 });
120136
121137export const validateNetworkKey = m =>
122138 simpleValidatorWrapper({
123139 prevMessage: m,
124140 validator: d => hexRegExp.test(d),
125− errorMessage: 'This is not a valid network key',
141+ error: 'This is not a valid network key',
126142 });
127143
128144export const validateNetworkSeed = m =>
129145 simpleValidatorWrapper({
130146 prevMessage: m,
131147 validator: d => hexRegExp.test(d),
132− errorMessage: 'This is not a valid network seed',
148+ error: 'This is not a valid network seed',
133149 });
134150
135151// Checks if a string is a valid ethereum address
@@ -137,5 +153,12 @@ export const validateEthereumAddress = m =>
137153 simpleValidatorWrapper({
138154 prevMessage: m,
139155 validator: d => isValidAddress(d),
140− errorMessage: 'This is not a valid Ethereum address',
156+ error: 'This is not a valid Ethereum address',
157+ });
158+
159+export const validateEmail = m =>
160+ simpleValidatorWrapper({
161+ prevMessage: m,
162+ validator: d => emailRegExp.test(d),
163+ error: 'This is not a valid email address',
141164 });
src/_walletgen/lib/lib.js → src/lib/walletgen.jsrenamed+1
@@ -2,6 +2,7 @@ import * as ob from 'urbit-ob';
22import * as kg from 'urbit-key-generation/dist';
33import * as more from 'more-entropy';
44import { chunk, flatMap, zipWith } from 'lodash';
5+
56import {
67 MIN_STAR,
78 MIN_PLANET,
src/store/pointCache.js+80−27
@@ -19,6 +19,8 @@ const kEmptyInvites = {
1919 acceptedInvites: Maybe.Nothing(),
2020};
2121
22+// TODO(shrugs): this hook is too big -> refactor into individual hooks for
23+// details, birthday, invites, etc
2224function _usePointCache() {
2325 const { contracts, web3 } = useNetwork();
2426 const [pointCache, _setPointCache] = useState({});
@@ -41,6 +43,10 @@ function _usePointCache() {
4143 );
4244
4345 // TODO: refactor pointCache access to use accessor like bithday
46+ const getDetails = useCallback(
47+ point => pointCache[point] || Maybe.Nothing(),
48+ [pointCache]
49+ );
4450 const getBirthday = useCallback(
4551 point => birthdayCache[point] || Maybe.Nothing(),
4652 [birthdayCache]
@@ -50,33 +56,31 @@ function _usePointCache() {
5056 [invitesCache]
5157 );
5258
53− const fetchPoint = useCallback(
59+ const syncDetails = useCallback(
5460 async point => {
5561 const _contracts = contracts.getOrElse(null);
56− const _web3 = web3.getOrElse(null);
57− if (!_contracts || !_web3) {
62+ if (!_contracts) {
5863 return;
5964 }
6065
6166 // fetch point details
6267 const details = await azimuth.azimuth.getPoint(_contracts, point);
63− addToPointCache({ [point]: details });
64−
65− // fetch invites
66− const count = await azimuth.delegatedSending.getTotalUsableInvites(
67− _contracts,
68− point
69− );
70− addToInvitesCache({
71− [point]: {
72− availableInvites: Maybe.Just(count),
73− // TODO: look up sent/accepted on-chain
74− sentInvites: Maybe.Just(6),
75− acceptedInvites: Maybe.Just(5),
76− },
68+ addToPointCache({
69+ [point]: details,
7770 });
71+ },
72+ [contracts, addToPointCache]
73+ );
7874
79− // fetch birthday (if not already known — will not change after being set)
75+ const syncBirthday = useCallback(
76+ async point => {
77+ const _contracts = contracts.getOrElse(null);
78+ const _web3 = web3.getOrElse(null);
79+ if (!_contracts || !web3) {
80+ return;
81+ }
82+
83+ // fetch birthday if not already known—will not change after being set
8084 if (Maybe.Nothing.hasInstance(getBirthday(point))) {
8185 const birthBlock = await azimuth.azimuth.getActivationBlock(
8286 _contracts,
@@ -91,22 +95,71 @@ function _usePointCache() {
9195 }
9296 }
9397 },
94− [
95− contracts,
96− web3,
97− addToPointCache,
98− addToBirthdayCache,
99− getBirthday,
100− addToInvitesCache,
101− ]
98+ [contracts, web3, addToBirthdayCache, getBirthday]
99+ );
100+
101+ const syncInvites = useCallback(
102+ async point => {
103+ const _contracts = contracts.getOrElse(null);
104+ if (!_contracts) {
105+ return;
106+ }
107+
108+ const availableInvites = await azimuth.delegatedSending.getTotalUsableInvites(
109+ _contracts,
110+ point
111+ );
112+
113+ const invitedPoints = await azimuth.delegatedSending.getInvited(
114+ _contracts,
115+ point
116+ );
117+ const invitedPointDetails = await Promise.all(
118+ invitedPoints.map(async invitedPoint => {
119+ console.log('invitedPoint', invitedPoint, typeof invitedPoint);
120+ const active = await azimuth.azimuth.isActive(
121+ _contracts,
122+ invitedPoint
123+ );
124+ return {
125+ point: Number(invitedPoint),
126+ active,
127+ };
128+ })
129+ );
130+ const sentInvites = invitedPointDetails.length;
131+ const acceptedInvites = invitedPointDetails.filter(i => i.active).length;
132+
133+ addToInvitesCache({
134+ [point]: {
135+ availableInvites: Maybe.Just(availableInvites),
136+ sentInvites: Maybe.Just(sentInvites),
137+ acceptedInvites: Maybe.Just(acceptedInvites),
138+ },
139+ });
140+ },
141+ [contracts, addToInvitesCache]
142+ );
143+
144+ // sync all of the on-chain info required for a point that the user owns
145+ const syncOwnedPoint = useCallback(
146+ async point => {
147+ await Promise.all([
148+ syncDetails(point),
149+ syncInvites(point),
150+ syncBirthday(point),
151+ ]);
152+ },
153+ [syncDetails, syncInvites, syncBirthday]
102154 );
103155
104156 return {
105157 pointCache,
158+ getDetails,
106159 getBirthday,
107160 getInvites,
108161 addToPointCache,
109− fetchPoint,
162+ syncOwnedPoint,
110163 };
111164}
112165
src/style/indigo-proposals.scss+4
@@ -27,3 +27,7 @@
2727.minh-100 {
2828 min-height: 100vh;
2929}
30+
31+.outline-none {
32+ outline: none;
33+}
src/views/Invite.js+37−34
@@ -4,7 +4,6 @@ import { Grid, H5, Text } from 'indigo-react';
44
55import { usePointCursor } from 'store/pointCursor';
66import { useHistory } from 'store/history';
7−// import { usePointCache } from 'store/pointCache';
87
98import View from 'components/View';
109import Crumbs from 'components/Crumbs';
@@ -14,6 +13,7 @@ import useSyncPoint from 'lib/useSyncPoint';
1413import useCurrentPointName from 'lib/useCurrentPointName';
1514import useRouter from 'lib/useRouter';
1615import loadingCharacter from 'lib/loadingCharacter';
16+import { LocalRouterProvider } from 'lib/LocalRouter';
1717
1818import InviteUrl from './Invite/InviteUrl';
1919import InviteEmail from './Invite/InviteEmail';
@@ -31,7 +31,7 @@ const kInviteViews = {
3131
3232export default function Invite() {
3333 const history = useHistory();
34− const { Route, ...routeProps } = useRouter({
34+ const { Route, ...router } = useRouter({
3535 names: kInviteNames,
3636 views: kInviteViews,
3737 initialRoutes: [{ key: kInviteNames.URL }],
@@ -40,45 +40,48 @@ export default function Invite() {
4040 const point = need.pointCursor(pointCursor);
4141 const name = useCurrentPointName();
4242
43− // sync the current cursor
43+ // sync the current point
4444 useSyncPoint(point);
4545
46− // fetch the invites for the current cursor
46+ // get the invites for the current point
4747 const { availableInvites, sentInvites, acceptedInvites } = useInvites(point);
48+
49+ // number or loading character
4850 const availableInvitesText = loadingCharacter(availableInvites);
4951 const sentInvitesText = loadingCharacter(sentInvites);
5052 const acceptedInvitesText = loadingCharacter(acceptedInvites);
5153
5254 return (
53− <View>
54− <Grid className="mb3">
55− <Grid.Item
56− as={Crumbs}
57− routes={[
58− {
59− text: name,
60− action: () => history.pop(),
61− },
62− {
63− text: 'Invite',
64− },
65− ]}
66− full
67− />
68− <Grid.Item as={H5} className="mv4" full>
69− Invite
70− </Grid.Item>
71− <Grid.Item as={Text} full>
72− You currently have{' '}
73− <Highlighted>{availableInvitesText} invitations</Highlighted> left.
74− <br />
75− Out of the <Highlighted>{sentInvitesText}</Highlighted> invites you
76− sent, <Highlighted>{acceptedInvitesText}</Highlighted> have been
77− accepted.
78− </Grid.Item>
79− </Grid>
80− {/* TODO(shrugs): use context isntead of this routeProps nonsense */}
81− <Route {...routeProps} />
82− </View>
55+ <LocalRouterProvider value={router}>
56+ <View>
57+ <Grid className="mb4">
58+ <Grid.Item
59+ as={Crumbs}
60+ routes={[
61+ {
62+ text: name,
63+ action: () => history.pop(),
64+ },
65+ {
66+ text: 'Invite',
67+ },
68+ ]}
69+ full
70+ />
71+ <Grid.Item as={H5} className="mv4" full>
72+ Invite
73+ </Grid.Item>
74+ <Grid.Item as={Text} full>
75+ You currently have{' '}
76+ <Highlighted>{availableInvitesText} invitations</Highlighted> left.
77+ <br />
78+ Out of the <Highlighted>{sentInvitesText}</Highlighted> invites you
79+ sent, <Highlighted>{acceptedInvitesText}</Highlighted> have been
80+ accepted.
81+ </Grid.Item>
82+ </Grid>
83+ <Route />
84+ </View>
85+ </LocalRouterProvider>
8386 );
8487}
src/views/Invite/InviteEmail.js+99−69
@@ -1,13 +1,13 @@
1−import { Just, Nothing } from 'folktale/maybe';
2−import React from 'react';
1+import Maybe, { Just, Nothing } from 'folktale/maybe';
2+import React, { useState } from 'react';
33import * as azimuth from 'azimuth-js';
4−import * as need from 'lib/need';
4+import { Grid, Flex, Button, Input, IconButton } from 'indigo-react';
55
6+import * as need from 'lib/need';
67import { hasReceived, sendMail } from 'lib/inviteMail';
7−import { Button, Input, H3, Warning } from 'components/old/Base';
8+import * as wg from 'lib/walletgen';
89
9−// for wallet generation
10−import * as wg from '_walletgen/lib/lib';
10+import { H3, Warning } from 'components/old/Base';
1111
1212// for transaction generation and signing
1313import {
@@ -19,11 +19,17 @@ import {
1919 toWei,
2020} from 'lib/txn';
2121import * as tank from 'lib/tank';
22−import { withNetwork } from 'store/network';
23−import { compose } from 'lib/lib';
24−import { withWallet } from 'store/wallet';
25−import { withPointCursor } from 'store/pointCursor';
22+import { useLocalRouter } from 'lib/LocalRouter';
23+
2624import View from 'components/View';
25+import MiniBackButton from 'components/MiniBackButton';
26+import useRenderCount from 'lib/useRenderCount';
27+import useInvites from 'lib/useInvites';
28+import { usePointCursor } from 'store/pointCursor';
29+import LoadableButton from 'components/LoadableButton';
30+import useArray from 'lib/useArray';
31+import useForm from 'indigo-react/lib/useForm';
32+import { buildEmailInputConfig } from 'components/Inputs';
2733
2834const GAS_PRICE_GWEI = 20; // we pay the premium for faster ux
2935const GAS_LIMIT = 350000;
@@ -45,13 +51,90 @@ const EMAIL_STATUS = {
4551 FAIL: '×',
4652};
4753
54+// world's simplest uid
55+let id = 0;
56+
57+const buildInputConfig = (extra = {}) =>
58+ buildEmailInputConfig({ name: (id++).toString(), ...extra });
59+
60+export default function NewInviteEmail() {
61+ const { pop } = useLocalRouter();
62+ const { pointCursor } = usePointCursor();
63+ const point = need.pointCursor(pointCursor);
64+
65+ const { availableInvites } = useInvites(point);
66+ const maxInvitesToSend = availableInvites.matchWith({
67+ Nothing: () => 0,
68+ Just: p => p.value,
69+ });
70+
71+ const [
72+ inputConfigs,
73+ { append: appendInput, removeAt: removeInputAt },
74+ ] = useArray(
75+ [buildInputConfig({ placeholder: 'Email Addresss' })],
76+ buildInputConfig
77+ );
78+
79+ const [hovered, _setHovered] = useState({});
80+ const setHovered = (name, value) => () =>
81+ _setHovered((value = { ...value, [name]: value }));
82+
83+ console.log(inputConfigs.length, inputConfigs.map(c => c.name));
84+
85+ const { inputs, pass } = useForm(inputConfigs);
86+
87+ const canAddInvite = inputs.length < maxInvitesToSend;
88+
89+ useRenderCount('InviteEmail');
90+
91+ return (
92+ <Grid gap={12}>
93+ <Grid.Item as={Grid} className="mb4" full>
94+ <Grid.Item as={Flex} cols={[1, 11]} align="center">
95+ <MiniBackButton onClick={() => pop()} />
96+ </Grid.Item>
97+ <Grid.Item cols={[11, 13]} justifySelf="end">
98+ <IconButton onClick={appendInput} disabled={!canAddInvite} solid>
99+ +
100+ </IconButton>
101+ </Grid.Item>
102+ </Grid.Item>
103+
104+ {/* email inputs */}
105+ {inputs.map((input, i) => {
106+ const isFirst = i === 0;
107+ return (
108+ <Grid.Item
109+ key={input.name}
110+ as={Grid}
111+ gap={12}
112+ onMouseEnter={setHovered(input.name, true)}
113+ onMouseLeave={setHovered(input.name, false)}
114+ full>
115+ <Grid.Item as={Input} cols={[1, 11]} {...input} />
116+ {!isFirst && (input.focused || hovered[input.name]) && (
117+ <Grid.Item cols={[11, 13]} justifySelf="end" alignSelf="center">
118+ <IconButton onClick={() => removeInputAt(i)}>-</IconButton>
119+ </Grid.Item>
120+ )}
121+ </Grid.Item>
122+ );
123+ })}
124+
125+ <Grid.Item as={LoadableButton} full disabled={!pass} solid>
126+ Generate Invites
127+ </Grid.Item>
128+ </Grid>
129+ );
130+}
131+
48132class InviteEmail extends React.Component {
49133 constructor(props) {
50134 super(props);
51135
52136 this.state = {
53137 invitesAvailable: Nothing(),
54− invited: Nothing(),
55138 targets: [{ email: '', hasReceived: Nothing(), status: Nothing() }],
56139 status: STATUS.INPUT,
57140 errors: Nothing(),
@@ -60,7 +143,6 @@ class InviteEmail extends React.Component {
60143 wipInvites: [],
61144 };
62145
63− this.findInvited = this.findInvited.bind(this);
64146 this.generateInvites = this.generateInvites.bind(this);
65147 this.sendInvites = this.sendInvites.bind(this);
66148 this.canSend = this.canSend.bind(this);
@@ -78,33 +160,11 @@ class InviteEmail extends React.Component {
78160 this.contracts = need.contracts(this.props.contracts);
79161 this.web3 = need.web3(this.props.web3);
80162
81− console.log(azimuth.delegatedSending);
82163 azimuth.delegatedSending
83164 .getTotalUsableInvites(this.contracts, this.point)
84165 .then(count => {
85166 this.setState({ invitesAvailable: Just(count) });
86167 });
87− this.findInvited();
88− }
89−
90− componentDidUpdate(prevProps) {
91− //
92− }
93−
94− async findInvited() {
95− let invited = await azimuth.delegatedSending.getInvited(
96− this.contracts,
97− this.point
98− );
99− invited = invited.map(async point => {
100− const active = await azimuth.azimuth.isActive(this.contracts, point);
101− const res = { point: Number(point), active: active };
102− return res;
103− });
104− invited = await Promise.all(invited);
105− const total = invited.length;
106− const accepted = invited.filter(i => i.active).length;
107− this.setState({ invited: Just({ total, accepted }) });
108168 }
109169
110170 async getTemporaryWallet() {
@@ -347,30 +407,6 @@ class InviteEmail extends React.Component {
347407 const dif =
348408 this.state.status === STATUS.DONE ? this.state.targets.length : null;
349409
350− const invitesAvailable = this.state.invitesAvailable.matchWith({
351− Nothing: () => 'Loading...',
352− Just: invites => {
353− const dia = dif ? `(-${dif})` : '';
354− return `You currently have ${invites.value}${dia} invitations left.`;
355− },
356− });
357−
358− const invitesSent = this.state.invited.matchWith({
359− Nothing: () => <>{'Loading...'}</>,
360− Just: invited => {
361− const dit = dif ? `(+${dif})` : '';
362− return (
363− <>
364− {'Out of the '}
365− {invited.value.total + dit}
366− {' invites you sent, '}
367− {invited.value.accepted}
368− {' have been accepted.'}
369− </>
370− );
371− },
372− });
373−
374410 const error = this.state.errors.matchWith({
375411 Nothing: () => null,
376412 Just: errors => (
@@ -478,12 +514,6 @@ class InviteEmail extends React.Component {
478514
479515 return (
480516 <View>
481− <p>{'send invites here, for planets'}</p>
482−
483− <p>{invitesAvailable}</p>
484−
485− <ul>{invitesSent}</ul>
486−
487517 {error}
488518
489519 {fundingMessage}
@@ -494,8 +524,8 @@ class InviteEmail extends React.Component {
494524 }
495525}
496526
497−export default compose(
498− withNetwork,
499− withWallet,
500− withPointCursor
501−)(InviteEmail);
527+// export default compose(
528+// withNetwork,
529+// withWallet,
530+// withPointCursor
531+// )(InviteEmail);
src/views/Invite/InviteUrl.js+21−3
@@ -1,10 +1,25 @@
11import React, { useCallback } from 'react';
22import { Grid, Button } from 'indigo-react';
33
4+import { usePointCursor } from 'store/pointCursor';
5+
6+import * as need from 'lib/need';
7+import { useLocalRouter } from 'lib/LocalRouter';
8+import useInvites from 'lib/useInvites';
9+
410import { ForwardButton } from 'components/Buttons';
511import FooterButton from 'components/FooterButton';
612
7−export default function InviteUrl({ names, push }) {
13+export default function InviteUrl() {
14+ const { names, push } = useLocalRouter();
15+ const { pointCursor } = usePointCursor();
16+ const point = need.pointCursor(pointCursor);
17+ const { availableInvites } = useInvites(point);
18+ const canInvite = availableInvites.matchWith({
19+ Nothing: () => false,
20+ Just: p => p.value > 0,
21+ });
22+
823 const pushEmail = useCallback(() => push(names.EMAIL), [push, names]);
924
1025 return (
@@ -15,7 +30,7 @@ export default function InviteUrl({ names, push }) {
1530 as={Button}
1631 detail="Generate a URL to send to one guest"
1732 full
18− disabled>
33+ disabled={!canInvite || true}>
1934 Generate Invite
2035 </Grid.Item>
2136 <Grid.Divider />
@@ -23,11 +38,14 @@ export default function InviteUrl({ names, push }) {
2338 as={ForwardButton}
2439 onClick={pushEmail}
2540 detail="Send invitation to your guest's email"
41+ disabled={!canInvite}
2642 full>
2743 Send Invite via Email
2844 </Grid.Item>
2945 </Grid>
30− <FooterButton detail="i.e. Ethereum Address, Text File" disabled>
46+ <FooterButton
47+ detail="i.e. Ethereum Address, Text File"
48+ disabled={!canInvite || true}>
3149 Advanced
3250 </FooterButton>
3351 </>
src/views/Keystore.jsdeleted−166
@@ -1,166 +0,0 @@
1−import { Just, Nothing } from 'folktale/maybe';
2−import { Ok, Error } from 'folktale/result';
3−import React from 'react';
4−import * as keythereum from 'keythereum';
5−import { Button, UploadButton } from '../components/old/Base';
6−import { Input, InnerLabel, InputCaption } from '../components/old/Base';
7−import { H1, H3, Warning } from '../components/old/Base';
8−
9−import { BRIDGE_ERROR } from '../lib/error';
10−import { ROUTE_NAMES } from '../lib/routeNames';
11−import { withHistory } from '../store/history';
12−import { EthereumWallet } from '../lib/wallet';
13−import { compose } from '../lib/lib';
14−import { withWallet } from '../store/wallet';
15−import View from 'components/View';
16−
17−class Keystore extends React.Component {
18− constructor(props) {
19− super(props);
20−
21− this.state = {
22− keystore: Nothing(), // Maybe<Result<String, String>>
23− password: '',
24− decryptionProblem: false,
25− };
26−
27− this.handleKeystoreUpload = this.handleKeystoreUpload.bind(this);
28− this.handlePasswordInput = this.handlePasswordInput.bind(this);
29− this.constructWallet = this.constructWallet.bind(this);
30− }
31−
32− handlePasswordInput(password) {
33− this.setState({ password });
34− }
35−
36− constructWallet() {
37− const { setWallet } = this.props;
38− const { keystore, password } = this.state;
39−
40− try {
41− const text = keystore.matchWith({
42− Nothing: _ => {
43− throw BRIDGE_ERROR.MISSING_KEYSTORE;
44− },
45− Just: ks =>
46− ks.value.matchWith({
47− Ok: result => result.value,
48− Error: _ => {
49− throw BRIDGE_ERROR.MISSING_KEYSTORE;
50− },
51− }),
52− });
53−
54− const json = JSON.parse(text);
55− const privateKey = keythereum.recover(password, json);
56−
57− const wallet = new EthereumWallet(privateKey);
58− this.setState({ decryptionProblem: false });
59− setWallet(Just(wallet));
60− } catch (err) {
61− this.setState({ decryptionProblem: true });
62− setWallet(Nothing());
63− }
64− }
65−
66− handleKeystoreUpload = event => {
67− const file = event.files.item(0);
68− const reader = new FileReader();
69−
70− reader.onload = e => {
71− const keystore = e.target.result;
72− this.setState({ keystore: Just(Ok(keystore)) });
73− };
74−
75− const failure = _ => {
76− const message = 'There was a problem uploading your Keystore file';
77− this.setState({ keystore: Just(Error(message)) });
78− };
79−
80− reader.onerror = failure;
81− reader.onabort = failure;
82−
83− reader.readAsText(file);
84− };
85−
86− render() {
87− const { history, wallet } = this.props;
88− const { keystore, password, decryptionProblem } = this.state;
89−
90− const uploadButtonClass = keystore.matchWith({
91− Nothing: _ => 'bg-blue white',
92− Just: ks =>
93− ks.value.matchWith({
94− Ok: _ => 'bg-green white',
95− Error: _ => 'bg-yellow black',
96− }),
97− });
98−
99− const decryptMessage =
100− decryptionProblem === false ? (
101− <div />
102− ) : (
103− <Warning className={'mt-8'}>
104− <H3 style={{ marginTop: 0, paddingTop: 0 }}>
105− {"Couldn't decrypt wallet."}
106− </H3>
107− {'You may have entered an incorrect password.'}
108− </Warning>
109− );
110−
111− return (
112− <View>
113− <H1 className={'mb-4'}>{'Upload Your Keystore File'}</H1>
114− <InputCaption>
115− {`Please upload your Ethereum keystore file. If your keystore
116− file is encrypted with a password, you'll also need to enter
117− that below.`}
118− </InputCaption>
119−
120− <UploadButton
121− className={`btn ${uploadButtonClass} mt-10`}
122− onChange={this.handleKeystoreUpload}>
123− <div className={'flex-center-all fs-4 h-11 pointer'}>
124− {'Upload Keystore file'}
125− </div>
126− </UploadButton>
127−
128− <Input
129− className="pt-8 mt-8"
130− prop-size="md"
131− prop-format="innerLabel"
132− type="password"
133− name="password"
134− onChange={this.handlePasswordInput}
135− value={password}
136− autocomplete="off"
137− autoFocus>
138− <InnerLabel>{'Password'}</InnerLabel>
139− </Input>
140−
141− <Button
142− className={` mt-10`}
143− prop-size={'wide lg'}
144− disabled={Nothing.hasInstance(keystore)}
145− onClick={this.constructWallet}>
146− {'Decrypt'}
147− </Button>
148−
149− {decryptMessage}
150−
151− <Button
152− className={'mt-10'}
153− prop-size={'wide lg'}
154− disabled={Nothing.hasInstance(wallet)}
155− onClick={() => history.popAndPush(ROUTE_NAMES.POINTS)}>
156− {'Continue →'}
157− </Button>
158− </View>
159− );
160− }
161−}
162−
163−export default compose(
164− withHistory,
165− withWallet
166−)(Keystore);
src/views/Landing.js−7
@@ -33,13 +33,6 @@ function Landing(props) {
3333 Login
3434 </ForwardButton>
3535
36− <ForwardButton
37− className="mt3"
38− detail="If you own Azimuth assets and want to manage them in some way."
39− onClick={() => history.push(ROUTE_NAMES.NETWORK)}>
40− Unlock a Wallet
41− </ForwardButton>
42−
4336 <ForwardButton
4437 className="mt3"
4538 detail="View an Azimuth point without signing into a wallet."
src/views/Ledger.jsdeleted−220
@@ -1,220 +0,0 @@
1−import * as bip32 from 'bip32';
2−import React from 'react';
3−import Maybe from 'folktale/maybe';
4−import { Button } from '../components/old/Base';
5−import { H1, P, H2 } from '../components/old/Base';
6−import { Input, InnerLabel, InnerLabelDropdown } from '../components/old/Base';
7−import Transport from '@ledgerhq/hw-transport-u2f';
8−import Eth from '@ledgerhq/hw-app-eth';
9−import * as secp256k1 from 'secp256k1';
10−
11−import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from '../lib/ledger';
12−import { ROUTE_NAMES } from '../lib/routeNames';
13−import { withHistory } from '../store/history';
14−import { compose } from '../lib/lib';
15−import { withWallet } from '../store/wallet';
16−import View from 'components/View';
17−
18−const chopHdPrefix = str => (str.slice(0, 2) === 'm/' ? str.slice(2) : str);
19−
20−const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
21−
22−class Ledger extends React.Component {
23− constructor(props) {
24− super(props);
25−
26− this.state = {
27− basePath: LEDGER_LIVE_PATH,
28− hdpath: LEDGER_LIVE_PATH.replace(/x/g, 0),
29− account: 0,
30− };
31−
32− this.pollDevice = this.pollDevice.bind(this);
33− this.handleHdPathInput = this.handleHdPathInput.bind(this);
34− this.handlePathSelection = this.handlePathSelection.bind(this);
35− this.handleAccountSelection = this.handleAccountSelection.bind(this);
36− this.updateHdPath = this.updateHdPath.bind(this);
37− }
38−
39− handlePathSelection(basePath) {
40− this.updateHdPath(basePath, this.state.account);
41− }
42−
43− handleAccountSelection(account) {
44− this.updateHdPath(this.state.basePath, account);
45− }
46−
47− updateHdPath(basePath, account) {
48− let hdpath = this.state.hdpath;
49− if (basePath !== 'custom') {
50− hdpath = basePath.replace(/x/g, account);
51− }
52− this.setState({ basePath, account, hdpath });
53− }
54−
55− handleHdPathInput(hdpath) {
56− this.setState({ hdpath });
57− }
58−
59− async pollDevice() {
60− const { setWallet, setWalletHdPath } = this.props;
61− const { hdpath } = this.state;
62−
63− const transport = await Transport.create();
64− const eth = new Eth(transport);
65− const path = chopHdPrefix(hdpath);
66−
67− eth.getAddress(path, false, true).then(
68− info => {
69− const publicKey = Buffer.from(info.publicKey, 'hex');
70− const chainCode = Buffer.from(info.chainCode, 'hex');
71− const pub = secp256k1.publicKeyConvert(publicKey, true);
72− const hd = bip32.fromPublicKey(pub, chainCode);
73− setWallet(Maybe.Just(hd));
74− setWalletHdPath(addHdPrefix(hdpath));
75− },
76− _ => {
77− setWallet(Maybe.Nothing());
78− }
79− );
80− }
81−
82− render() {
83− const { history, wallet } = this.props;
84− const { basePath, hdpath, account } = this.state;
85− const { handlePathSelection, handleAccountSelection } = this;
86−
87− const pathOptions = [
88− { title: 'Ledger Live', value: LEDGER_LIVE_PATH },
89− { title: 'Ledger Legacy', value: LEDGER_LEGACY_PATH },
90− {
91− title: 'Custom path',
92− value: 'custom',
93− },
94− ];
95− let basePathTitle = pathOptions.find(o => o.value === basePath).title;
96−
97− let accountOptions = [];
98− for (let i = 0; i < 20; i++) {
99− accountOptions.push({
100− title: 'Account #' + (i + 1),
101− value: i,
102− });
103− }
104− let accountTitle = accountOptions.find(o => o.value === account).title;
105−
106− const basePathSelection = (
107− <InnerLabelDropdown
108− className="mt-8"
109− options={pathOptions}
110− handleUpdate={handlePathSelection}
111− title={'Derivation path'}
112− currentSelectionTitle={basePathTitle}
113− fullWidth={true}
114− />
115− );
116−
117− const truePathSelection =
118− basePath === 'custom' ? (
119− <Input
120− className="pt-8 mt-4 text-mono"
121− prop-size="md"
122− prop-format="innerLabel"
123− name="hdpath"
124− value={addHdPrefix(hdpath)}
125− autocomplete="off"
126− onChange={this.handleHdPathInput}>
127− <InnerLabel>{'HD Path'}</InnerLabel>
128− </Input>
129− ) : (
130− <InnerLabelDropdown
131− className="mt-4"
132− prop-size="md"
133− prop-format="innerLabel"
134− options={accountOptions}
135− handleUpdate={handleAccountSelection}
136− title={'Account'}
137− currentSelectionTitle={accountTitle}
138− fullWidth={true}
139− />
140− );
141−
142− return (
143− <View>
144− <H1>{'Authenticate With Your Ledger'}</H1>
145−
146− <H2>{'Running on HTTPS?'}</H2>
147−
148− <P>
149− {`Connect and authenticate to your Ledger, and then open the
150− "Ethereum" application. If you're running on older firmware,
151− make sure the "browser support" option is turned on. To sign
152− transactions, you'll also need to enable the "contract data"
153− option.`}
154− </P>
155−
156− <P>
157− {`If you'd like to use a custom derivation path, you may enter
158− it below.`}
159− </P>
160−
161− <H2>{'Running on HTTP?'}</H2>
162−
163− <P>{`To authenticate and sign transactions with a Ledger, Bridge
164− must be serving over HTTPS on localhost. You can do this via the
165− following:`}</P>
166−
167− <ol className={'measure-md'}>
168− <li className={'mt-4'}>
169− {'Install'}{' '}
170− <a target={'_blank'} href={'https://github.com/FiloSottile/mkcert'}>
171− {'mkcert'}
172− </a>
173− </li>
174− <li className={'mt-4'}>
175− {'Install a local certificate authority via '}
176− <code>{'mkcert -install'}</code>
177− </li>
178− <li className={'mt-4'}>
179− {'In your '}
180− <code>{'bridge'}</code>
181− {' directory, generate a certificate valid for localhost via '}{' '}
182− <code>{'mkcert localhost'}</code>
183− {'.'}
184− {'This will produce two files: '}
185− <code>{'localhost.pem'}</code> {', the local certificate, and '}
186− <code>{'localhost-key.pem'}</code>
187− {', its corresponding private key.'}
188− </li>
189− <li className={'mt-4'}>
190− {'Run '}
191− <code>{'python bridge-https.py'}</code>
192− </li>
193− </ol>
194−
195− {basePathSelection}
196− {truePathSelection}
197−
198− <Button
199− prop-size={'wide lg'}
200− className={'mt-8'}
201− onClick={this.pollDevice}>
202− {'Authenticate →'}
203− </Button>
204−
205− <Button
206− className={'mt-8'}
207− prop-size={'wide lg'}
208− disabled={Maybe.Nothing.hasInstance(wallet)}
209− onClick={() => history.popAndPush(ROUTE_NAMES.POINTS)}>
210− {'Continue →'}
211− </Button>
212− </View>
213− );
214− }
215−}
216−
217−export default compose(
218− withHistory,
219− withWallet
220−)(Ledger);
src/views/Login/Keystore.js+11−10
@@ -4,9 +4,9 @@ import React, { useState } from 'react';
44import * as keythereum from 'keythereum';
55
66import View from 'components/View';
7−import { PassphraseInput } from 'components/Inputs';
7+import { usePassphraseInput } from 'components/Inputs';
88import { ForwardButton } from 'components/Buttons';
9−import { H3 } from 'indigo-react';
9+import { H3, Input } from 'indigo-react';
1010import { InputCaption, UploadButton, Warning } from 'components/old/Base';
1111
1212import * as need from 'lib/need';
@@ -20,7 +20,13 @@ export default function Keystore({ loginCompleted }) {
2020 // inputs
2121 // keystore: Maybe<Result<String, String>>
2222 const [keystore, setKeystore] = useState(Nothing());
23− const [password, setPassword] = useState('');
23+ const passphraseInput = usePassphraseInput({
24+ name: 'password',
25+ label: 'Keystore password',
26+ autoFocus: true,
27+ });
28+ const passphrase = passphraseInput.data;
29+
2430 const [decryptionProblem, setDecryptionProblem] = useState(false);
2531
2632 const constructWallet = () => {
@@ -28,7 +34,7 @@ export default function Keystore({ loginCompleted }) {
2834 const text = need.keystore(keystore);
2935
3036 const json = JSON.parse(text);
31− const privateKey = keythereum.recover(password, json);
37+ const privateKey = keythereum.recover(passphrase, json);
3238
3339 const newWallet = new EthereumWallet(privateKey);
3440 setDecryptionProblem(false);
@@ -95,12 +101,7 @@ export default function Keystore({ loginCompleted }) {
95101 </div>
96102 </UploadButton>
97103
98− <PassphraseInput
99− name="password"
100− label="Keystore password"
101− initialValue={password}
102− onValue={setPassword}
103− autoFocus></PassphraseInput>
104+ <Input {...passphraseInput} />
104105
105106 <ForwardButton
106107 className="mt3"
src/views/Login/Ledger.js+1−1
@@ -9,7 +9,7 @@ import * as secp256k1 from 'secp256k1';
99import View from 'components/View';
1010import { ForwardButton } from 'components/Buttons';
1111import { H1, H2, P, Input } from 'indigo-react';
12−import { InnerLabel, InnerLabelDropdown } from 'components/old/Base';
12+import { InnerLabelDropdown } from 'components/old/Base';
1313
1414import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from 'lib/ledger';
1515import { useWallet } from 'store/wallet';
src/views/Login/Mnemonic.js+32−23
@@ -1,12 +1,17 @@
1−import React, { useCallback, useEffect, useState } from 'react';
1+import React, { useCallback, useEffect } from 'react';
22import { Just, Nothing } from 'folktale/maybe';
33
44import View from 'components/View';
5−import { MnemonicInput, PassphraseInput, HdPathInput } from 'components/Inputs';
5+import {
6+ usePassphraseInput,
7+ useMnemonicInput,
8+ useHdPathInput,
9+} from 'components/Inputs';
610import { ForwardButton } from 'components/Buttons';
711
812import { walletFromMnemonic } from 'lib/wallet';
913import { useWallet } from 'store/wallet';
14+import { Input } from 'indigo-react';
1015
1116export default function Mnemonic({ advanced, loginCompleted }) {
1217 const {
@@ -17,9 +22,8 @@ export default function Mnemonic({ advanced, loginCompleted }) {
1722 walletHdPath,
1823 setWalletHdPath,
1924 } = useWallet();
20− const [passphrase, setPassphrase] = useState('');
21− const mnemonic = authMnemonic.getOrElse('');
2225
26+ const mnemonic = authMnemonic.getOrElse('');
2327 // TODO: move this into transformers?
2428 // transform the result of the mnemonic to Maybe<string>
2529 const _setAuthMnemonic = useCallback(
@@ -27,6 +31,27 @@ export default function Mnemonic({ advanced, loginCompleted }) {
2731 [setAuthMnemonic]
2832 );
2933
34+ const mnemonicInput = useMnemonicInput({
35+ name: 'mnemonic',
36+ label: 'BIP39 Mnemonic',
37+ initialValue: mnemonic,
38+ onValue: _setAuthMnemonic,
39+ autoFocus: true,
40+ });
41+
42+ const passphraseInput = usePassphraseInput({
43+ name: 'passphrase',
44+ label: '(Optional) Wallet Passphrase',
45+ });
46+ const passphrase = passphraseInput.data;
47+
48+ const hdPathInput = useHdPathInput({
49+ name: 'hdpath',
50+ label: 'HD Path',
51+ initialValue: walletHdPath,
52+ onValue: setWalletHdPath,
53+ });
54+
3055 // when the properties change, re-derive wallet
3156 useEffect(() => {
3257 let mounted = true;
@@ -39,29 +64,13 @@ export default function Mnemonic({ advanced, loginCompleted }) {
3964
4065 return (
4166 <View>
42− <MnemonicInput
43− name="mnemonic"
44− label="BIP39 Mnemonic"
45− initialValue={mnemonic}
46− onValue={_setAuthMnemonic}
47− autoFocus
48− />
67+ <Input {...mnemonicInput} />
4968
5069 {!advanced ? null : (
5170 <>
52− <PassphraseInput
53− name="passphrase"
54− label="(Optional) Wallet Passphrase"
55− initialValue={passphrase}
56− onValue={setPassphrase}
57− />
71+ <Input {...passphraseInput} />
5872
59− <HdPathInput
60− name="hdpath"
61− label="HD Path"
62− initialValue={walletHdPath}
63− onValue={setWalletHdPath}
64− />
73+ <Input {...hdPathInput} />
6574 </>
6675 )}
6776
src/views/Login/PrivateKey.js+12−19
@@ -1,24 +1,25 @@
11import { Just, Nothing } from 'folktale/maybe';
2−import React, { useState } from 'react';
2+import React, { useEffect } from 'react';
33
44import View from 'components/View';
5−import { PassphraseInput } from 'components/Inputs';
5+import { usePassphraseInput } from 'components/Inputs';
66import { ForwardButton } from 'components/Buttons';
77
88import { EthereumWallet } from 'lib/wallet';
99import { useWallet } from 'store/wallet';
10+import { Input } from 'indigo-react';
1011
1112export default function PrivateKey({ loginCompleted }) {
1213 const { wallet, setWallet } = useWallet();
1314
14− const [privateKey, setPrivateKey] = useState('');
15+ const privateKeyInput = usePassphraseInput({
16+ name: 'privateKey',
17+ label: 'Private key',
18+ autoFocus: true,
19+ });
20+ const privateKey = privateKeyInput.data;
1521
16− const handlePrivateKeyInput = privateKey => {
17− setPrivateKey(privateKey);
18− constructWallet(privateKey);
19− };
20−
21− const constructWallet = privateKey => {
22+ useEffect(() => {
2223 if (/^[0-9A-Fa-f]{64}$/g.test(privateKey) === true) {
2324 const sec = Buffer.from(privateKey, 'hex');
2425 const newWallet = new EthereumWallet(sec);
@@ -26,20 +27,12 @@ export default function PrivateKey({ loginCompleted }) {
2627 } else {
2728 setWallet(Nothing());
2829 }
29− };
30+ }, [privateKey, setWallet]);
3031
3132 return (
3233 <View>
3334 Please enter your raw Ethereum private key here.
34− <PassphraseInput
35− type="password"
36− name="privateKey"
37− label="Private key"
38− initialValue={privateKey}
39− onValue={handlePrivateKeyInput}
40− autocomplete="off"
41− autoFocus
42− />
35+ <Input {...privateKeyInput} />
4336 <ForwardButton
4437 className="mt3"
4538 disabled={Nothing.hasInstance(wallet)}
src/views/Login/Shards.js+39−35
@@ -1,22 +1,49 @@
11import { Just, Nothing } from 'folktale/maybe';
2−import React, { useState } from 'react';
2+import React from 'react';
33import * as kg from 'urbit-key-generation/dist/index';
44
55import View from 'components/View';
6−import { PointInput, TicketInput, PassphraseInput } from 'components/Inputs';
6+import {
7+ useTicketInput,
8+ usePassphraseInput,
9+ usePointInput,
10+} from 'components/Inputs';
711import { ForwardButton } from 'components/Buttons';
812
913import { urbitWalletFromTicket } from 'lib/wallet';
1014import { useWallet } from 'store/wallet';
15+import { Input } from 'indigo-react';
1116
1217export default function Shards({ loginCompleted }) {
1318 const { wallet, setUrbitWallet } = useWallet();
1419
15− const [shard1, setShard1] = useState('');
16− const [shard2, setShard2] = useState('');
17− const [shard3, setShard3] = useState('');
18− const [passphrase, setPassphrase] = useState('');
19− const [pointName, setPointName] = useState('');
20+ const pointInput = usePointInput({
21+ name: 'point',
22+ label: 'Point',
23+ autoFocus: true,
24+ });
25+ const pointName = pointInput.data;
26+ const shard1Input = useTicketInput({
27+ name: 'shard1',
28+ label: 'Shard 1',
29+ });
30+ const shard1 = shard1Input.data;
31+ const shard2Input = useTicketInput({
32+ name: 'shard2',
33+ label: 'Shard 2',
34+ });
35+ const shard2 = shard2Input.data;
36+ const shard3Input = useTicketInput({
37+ name: 'shard3',
38+ label: 'Shard 3',
39+ });
40+ const shard3 = shard3Input.data;
41+
42+ const passphraseInput = usePassphraseInput({
43+ name: 'passphrase',
44+ label: '(Optional) Wallet Passphrase',
45+ });
46+ const passphrase = passphraseInput.data;
2047
2148 const walletFromShards = async () => {
2249 const s1 = shard1 === '' ? undefined : shard1;
@@ -43,35 +70,12 @@ export default function Shards({ loginCompleted }) {
4370 <View>
4471 Enter your point and at least two of your three Urbit master ticket shards
4572 here. The index of the input field needs to match the index of the shard.
46− <PointInput
47− name="point"
48− label="Point"
49− initialValue={pointName}
50− onValue={setPointName}
51− autoFocus
52− />
53− <TicketInput
54− name="shard1"
55− label="Shard 1"
56− initialValue={shard1}
57− onValue={setShard1}></TicketInput>
58− <TicketInput
59− name="shard2"
60− label="Shard 2"
61− initialValue={shard2}
62− onValue={setShard2}></TicketInput>
63− <TicketInput
64− name="shard3"
65− label="Shard 3"
66− initialValue={shard3}
67− onValue={setShard3}></TicketInput>
73+ <Input {...pointInput} />
74+ <Input {...shard1Input} />
75+ <Input {...shard2Input} />
76+ <Input {...shard3Input} />
6877 If your wallet requires a passphrase, you may enter it below.
69− <PassphraseInput
70− name="passphrase"
71− label="(Optional) Wallet Passphrase"
72− initialValue={passphrase}
73− onValue={setPassphrase}
74− />
78+ <Input {...passphraseInput} />
7579 <ForwardButton
7680 className="mt3"
7781 disabled={!ready}
src/views/Login/Ticket.js+63−68
@@ -1,10 +1,14 @@
11import { Just, Nothing } from 'folktale/maybe';
2−import React, { useState, useEffect } from 'react';
2+import React, { useCallback, useState, useEffect } from 'react';
33import * as azimuth from 'azimuth-js';
44import * as ob from 'urbit-ob';
55
66import View from 'components/View';
7−import { PointInput, TicketInput, PassphraseInput } from 'components/Inputs';
7+import {
8+ usePointInput,
9+ useTicketInput,
10+ usePassphraseInput,
11+} from 'components/Inputs';
812import { ForwardButton } from 'components/Buttons';
913
1014import * as need from 'lib/need';
@@ -13,6 +17,7 @@ import { WALLET_TYPES, urbitWalletFromTicket } from 'lib/wallet';
1317import { useNetwork } from 'store/network';
1418import { useWallet } from 'store/wallet';
1519import { usePointCursor } from 'store/pointCursor';
20+import { Input } from 'indigo-react';
1621
1722//TODO should be part of InputWithStatus component
1823const INPUT_STATUS = {
@@ -28,47 +33,65 @@ export default function Ticket({ advanced, loginCompleted }) {
2833 const { setPointCursor } = usePointCursor();
2934
3035 // inputs
31− //TODO deduce point name from URL if we can, prefill input if we found it
32− const [pointName, setPointName] = useState('');
33− const [ticket, setTicket] = useState('');
34− const [passphrase, setPassphrase] = useState('');
36+ const pointInput = usePointInput({
37+ name: 'point',
38+ label: 'Point',
39+ //TODO deduce point name from URL if we can, prefill input if we found it
40+ // initialValue: '~',
41+ autoFocus: true,
42+ });
43+ const pointName = pointInput.data;
44+
45+ const ticketInput = useTicketInput({
46+ name: 'ticket',
47+ label: 'Master ticket',
48+ });
49+ const ticket = ticketInput.data;
50+
51+ const passphraseInput = usePassphraseInput({
52+ name: 'passphrase',
53+ label: '(Optional) Wallet Passphrase',
54+ });
55+ const passphrase = passphraseInput.data;
3556
3657 // display state
3758 const [ticketStatus, setTicketStatus] = useState(Nothing());
3859
60+ //TODO maybe want to do this only on-go, because wallet derivation is slow...
61+ const verifyTicket = useCallback(
62+ async (pointName, ticket, passphrase) => {
63+ setUrbitWallet(Nothing());
64+ if (!ob.isValidPatq(ticket) || !ob.isValidPatp(pointName)) {
65+ setTicketStatus(Nothing());
66+ return;
67+ }
68+ setTicketStatus(Just(INPUT_STATUS.SPIN));
69+ const _contracts = need.contracts(contracts);
70+ const pointNumber = ob.patp2dec(pointName);
71+ const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
72+ const isOwner = azimuth.azimuth.isOwner(
73+ _contracts,
74+ pointNumber,
75+ uhdw.ownership.keys.address
76+ );
77+ const isTransferProxy = azimuth.azimuth.isTransferProxy(
78+ _contracts,
79+ pointNumber,
80+ uhdw.ownership.keys.address
81+ );
82+ setUrbitWallet(Just(uhdw));
83+ const newStatus =
84+ (await isOwner) || (await isTransferProxy)
85+ ? INPUT_STATUS.GOOD
86+ : INPUT_STATUS.FAIL;
87+ setTicketStatus(Just(newStatus));
88+ },
89+ [contracts, setUrbitWallet, setTicketStatus]
90+ );
91+
3992 useEffect(() => {
4093 verifyTicket(pointName, ticket, passphrase);
41− return () => {};
42− }, [pointName, ticket, passphrase]);
43−
44− //TODO maybe want to do this only on-go, because wallet derivation is slow...
45− const verifyTicket = async (pointName, ticket, passphrase) => {
46− setUrbitWallet(Nothing());
47− if (!ob.isValidPatq(ticket) || !ob.isValidPatp(pointName)) {
48− setTicketStatus(Nothing());
49− return;
50− }
51− setTicketStatus(Just(INPUT_STATUS.SPIN));
52− const _contracts = need.contracts(contracts);
53− const pointNumber = ob.patp2dec(pointName);
54− const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
55− const isOwner = azimuth.azimuth.isOwner(
56− _contracts,
57− pointNumber,
58− uhdw.ownership.keys.address
59− );
60− const isTransferProxy = azimuth.azimuth.isTransferProxy(
61− _contracts,
62− pointNumber,
63− uhdw.ownership.keys.address
64− );
65− setUrbitWallet(Just(uhdw));
66− const newStatus =
67− (await isOwner) || (await isTransferProxy)
68− ? INPUT_STATUS.GOOD
69− : INPUT_STATUS.FAIL;
70− setTicketStatus(Just(newStatus));
71− };
94+ }, [verifyTicket, pointName, ticket, passphrase]);
7295
7396 const canContinue = () => {
7497 // this is our only requirement, since we still want people with
@@ -82,16 +105,6 @@ export default function Ticket({ advanced, loginCompleted }) {
82105 loginCompleted();
83106 };
84107
85− const pointInput = (
86− <PointInput
87− name="point"
88− label="Point"
89− initialValue={pointName}
90− onValue={setPointName}
91− autoFocus
92− />
93− );
94−
95108 //TODO integrate into TicketInput?
96109 const displayTicketStatus = ticketStatus.matchWith({
97110 Nothing: () => <span />,
@@ -109,29 +122,11 @@ export default function Ticket({ advanced, loginCompleted }) {
109122 },
110123 });
111124
112− const ticketInput = (
113− <TicketInput
114− name="ticket"
115− label="Master ticket"
116− initialValue={ticket}
117− onValue={setTicket}
118− />
119− );
120−
121− const passphraseInput = !advanced ? null : (
122− <PassphraseInput
123− name="passphrase"
124− label="(Optional) Wallet Passphrase"
125− initialValue={passphrase}
126− onValue={setPassphrase}
127− />
128− );
129−
130125 return (
131126 <View>
132− {pointInput}
133− {ticketInput}
134− {passphraseInput}
127+ <Input {...pointInput} />
128+ <Input {...ticketInput} accessory={displayTicketStatus} />
129+ {advanced && <Input {...passphraseInput} />}
135130
136131 <ForwardButton
137132 className={'mt3'}
src/views/Login/Trezor.js+1−1
@@ -8,7 +8,7 @@ import * as secp256k1 from 'secp256k1';
88import View from 'components/View';
99import { ForwardButton } from 'components/Buttons';
1010import { H1, P, Input } from 'indigo-react';
11−import { InnerLabel, InnerLabelDropdown } from 'components/old/Base';
11+import { InnerLabelDropdown } from 'components/old/Base';
1212
1313import { TREZOR_PATH } from 'lib/trezor';
1414import { useWallet } from 'store/wallet';
src/views/Mnemonic.jsdeleted−78
@@ -1,78 +0,0 @@
1−import React, { useCallback, useEffect, useState } from 'react';
2−import { Just, Nothing } from 'folktale/maybe';
3−
4−import View from 'components/View';
5−import { MnemonicInput, PassphraseInput, HdPathInput } from 'components/Inputs';
6−import { ForwardButton } from 'components/Buttons';
7−
8−import { ROUTE_NAMES } from 'lib/routeNames';
9−import { useHistory } from 'store/history';
10−import { walletFromMnemonic } from 'lib/wallet';
11−import { useWallet } from 'store/wallet';
12−
13−export default function Mnemonic() {
14− const history = useHistory();
15− const {
16− wallet,
17− setWallet,
18− authMnemonic,
19− setAuthMnemonic,
20− walletHdPath,
21− setWalletHdPath,
22− } = useWallet();
23− const [passphrase, setPassphrase] = useState('');
24− const mnemonic = authMnemonic.getOrElse('');
25−
26− // TODO: move this into transformers?
27− // transform the result of the mnemonic to Maybe<string>
28− const _setAuthMnemonic = useCallback(
29− mnemonic => setAuthMnemonic(mnemonic === '' ? Nothing() : Just(mnemonic)),
30− [setAuthMnemonic]
31− );
32−
33− // when the properties change, re-derive wallet
34− useEffect(() => {
35− let mounted = true;
36− // allow the browser to do something else while we derive this wallet
37− // on the main thread by making this an async function
38− (async () => {
39− const wallet = walletFromMnemonic(mnemonic, walletHdPath, passphrase);
40− mounted && setWallet(wallet);
41− })();
42− return () => (mounted = false);
43− }, [mnemonic, passphrase, walletHdPath, setWallet]);
44−
45− return (
46− <View>
47− <MnemonicInput
48− name="mnemonic"
49− label="BIP39 Mnemonic"
50− initialValue={mnemonic}
51− onValue={_setAuthMnemonic}
52− autoFocus
53− />
54−
55− <PassphraseInput
56− name="passphrase"
57− label="(Optional) Wallet Passphrase"
58− initialValue={passphrase}
59− onValue={setPassphrase}
60− />
61−
62− <HdPathInput
63− name="hdpath"
64− label="HD Path"
65− initialValue={walletHdPath}
66− onValue={setWalletHdPath}
67− />
68−
69− <ForwardButton
70− className="mt3"
71− disabled={Nothing.hasInstance(wallet)}
72− onClick={() => history.popAndPush(ROUTE_NAMES.POINTS)}
73− solid>
74− Continue
75− </ForwardButton>
76− </View>
77− );
78−}
src/views/Network.jsdeleted−62
@@ -1,62 +0,0 @@
1−import React from 'react';
2−import { Button } from 'indigo-react';
3−
4−import { InnerLabelDropdown } from '../components/old/Base';
5−import { H1, P } from '../components/old/Base';
6−import { NETWORK_TYPES, renderNetworkType } from '../lib/network';
7−import { ROUTE_NAMES } from '../lib/routeNames';
8−import { useHistory } from '../store/history';
9−import { useNetwork } from '../store/network';
10−import View from 'components/View';
11−
12−const kNetworkOptions = [
13− {
14− title: 'Main Network (default)',
15− value: NETWORK_TYPES.MAINNET,
16− },
17− {
18− title: 'Ropsten',
19− value: NETWORK_TYPES.ROPSTEN,
20− },
21− {
22− title: 'Local Node',
23− value: NETWORK_TYPES.LOCAL,
24− },
25− {
26− type: 'divider',
27− },
28− {
29− title: 'Offline',
30− value: NETWORK_TYPES.OFFLINE,
31− },
32−];
33−
34−function Network() {
35− const { networkType, setNetworkType } = useNetwork();
36− const history = useHistory();
37−
38− return (
39− <View>
40− <H1>{'Select Network'}</H1>
41−
42− <P>
43− {"Please select the Ethereum node you'd like to send " +
44− 'transactions to. For highly valuable keys, please select ' +
45− 'offline mode.'}
46− </P>
47−
48− <InnerLabelDropdown
49− title={'Node:'}
50− handleUpdate={setNetworkType}
51− options={kNetworkOptions}
52− currentSelectionTitle={renderNetworkType(networkType)}
53− />
54−
55− <Button icon="→" onClick={() => history.push(ROUTE_NAMES.WALLET)}>
56− Continue
57− </Button>
58− </View>
59− );
60−}
61−
62−export default Network;
src/views/Point.js+7−8
@@ -3,7 +3,6 @@ import * as need from 'lib/need';
33import { Grid } from 'indigo-react';
44
55import { usePointCursor } from 'store/pointCursor';
6−// import { usePointCache } from 'store/pointCache';
76
87import View from 'components/View';
98import Passport from 'components/Passport';
@@ -15,6 +14,8 @@ import { ROUTE_NAMES } from 'lib/routeNames';
1514
1615import { useHistory } from 'store/history';
1716import FooterButton from 'components/FooterButton';
17+import loadingCharacter from 'lib/loadingCharacter';
18+import Actions from './Point/Actions';
1819
1920export default function Point() {
2021 const history = useHistory();
@@ -25,6 +26,7 @@ export default function Point() {
2526
2627 // fetch the invites for the current cursor
2728 const { availableInvites } = useInvites(point);
29+ const availableInvitesText = loadingCharacter(availableInvites);
2830
2931 const goInvite = useCallback(() => history.push(ROUTE_NAMES.INVITE), [
3032 history,
@@ -33,8 +35,6 @@ export default function Point() {
3335 // sync the current cursor
3436 useSyncPoint(point);
3537
36− // const pointDetails = need.fromPointCache(pointCache, point);
37−
3838 return (
3939 <View>
4040 <Passport point={point} />
@@ -48,14 +48,13 @@ export default function Point() {
4848 Boot Arvo
4949 </ForwardButton>
5050 </Grid.Item>
51+ {/* <Grid.Item full>
52+ <Actions />
53+ </Grid.Item> */}
5154 </Grid>
5255
5356 <FooterButton onClick={goInvite}>
54− Invite
55− {availableInvites.matchWith({
56− Nothing: () => null,
57− Just: p => <sup className="ml1">{p.value}</sup>,
58− })}
57+ Invite <sup>{availableInvitesText}</sup>
5958 </FooterButton>
6059 </View>
6160 );
src/views/Point/Actions.js+25−48
@@ -1,4 +1,5 @@
11import React from 'react';
2+import Maybe from 'folktale/maybe';
23import { azimuth } from 'azimuth-js';
34import * as need from '../../lib/need';
45
@@ -9,20 +10,25 @@ import { ROUTE_NAMES } from '../../lib/routeNames';
910import { useHistory } from '../../store/history';
1011import { useWallet } from '../../store/wallet';
1112import { Grid } from 'indigo-react';
13+import { usePointCursor } from 'store/pointCursor';
14+import { usePointCache } from 'store/pointCache';
1215
1316const isPlanet = point =>
1417 azimuth.getPointSize(point) === azimuth.PointSize.Planet;
15−const isStar = point => azimuth.getPointSize(point) === azimuth.PointSize.Star;
1618
1719function Actions(props) {
1820 const history = useHistory();
1921 const { wallet } = useWallet();
20− const { online, point, pointDetails, invites } = props;
22+ const { pointCursor } = usePointCursor();
23+ const point = need.pointCursor(pointCursor);
24+ const { pointCache } = usePointCache();
25+ const pointDetails = Maybe.Just(
26+ need.fromPointCache(Maybe.Just(pointCache), point)
27+ );
2128
2229 const addr = need.addressFromWallet(wallet);
2330
2431 const planet = isPlanet(point);
25− const star = isStar(point);
2632
2733 const isOwner = pointDetails.matchWith({
2834 Nothing: _ => false,
@@ -57,11 +63,6 @@ function Actions(props) {
5763 },
5864 });
5965
60− const hasInvites = invites.matchWith({
61− Nothing: () => false,
62− Just: count => count.value > 0,
63− });
64−
6566 const canTransfer = pointDetails.matchWith({
6667 Nothing: () => false,
6768 Just: deets =>
@@ -104,37 +105,6 @@ function Actions(props) {
104105 },
105106 });
106107
107− let inviteAction = null;
108− if (planet) {
109− inviteAction = (
110− <Button
111− disabled={!isActiveOwner || !online || !hasInvites}
112− prop-size={'sm'}
113− prop-type={'link'}
114− onClick={() => history.push(ROUTE_NAMES.INVITE)}>
115− {'Send invites ('}
116− {invites.matchWith({
117− Nothing: () => '?',
118− Just: count => count.value,
119− })}
120− {')'}
121− </Button>
122− );
123− }
124− if (star) {
125− inviteAction = (
126− <Button
127− disabled={!(isActiveOwner && canIssueChild) || !online}
128− prop-size={'sm'}
129− prop-type={'link'}
130− onClick={() => {
131− history.push(ROUTE_NAMES.INVITES_MANAGE);
132− }}>
133− {'Manage invites'}
134− </Button>
135− );
136− }
137−
138108 return (
139109 <div>
140110 <H2>{'Actions'}</H2>
@@ -149,7 +119,7 @@ function Actions(props) {
149119 <Button
150120 prop-size={'sm'}
151121 prop-type={'link'}
152− disabled={(online || planet) && !canIssueChild}
122+ disabled={planet && !canIssueChild}
153123 onClick={() => {
154124 history.push(ROUTE_NAMES.ISSUE_CHILD);
155125 }}>
@@ -159,7 +129,7 @@ function Actions(props) {
159129 <Button
160130 prop-size={'sm'}
161131 prop-type={'link'}
162− disabled={online && !canAcceptTransfer}
132+ disabled={!canAcceptTransfer}
163133 onClick={() => {
164134 history.push(ROUTE_NAMES.ACCEPT_TRANSFER);
165135 }}>
@@ -169,7 +139,7 @@ function Actions(props) {
169139 <Button
170140 prop-size={'sm'}
171141 prop-type={'link'}
172− disabled={online && !canCancelTransfer}
142+ disabled={!canCancelTransfer}
173143 onClick={() => {
174144 history.push(ROUTE_NAMES.CANCEL_TRANSFER);
175145 }}>
@@ -179,7 +149,7 @@ function Actions(props) {
179149 <Button
180150 prop-size={'sm'}
181151 prop-type={'link'}
182− disabled={online && !canGenKeyfile}
152+ disabled={!canGenKeyfile}
183153 onClick={() => {
184154 history.push(ROUTE_NAMES.GEN_KEYFILE);
185155 }}>
@@ -188,7 +158,7 @@ function Actions(props) {
188158 </Grid.Item>
189159 <Grid.Item third={2}>
190160 <Button
191− disabled={online && !canSetSpawnProxy}
161+ disabled={!canSetSpawnProxy}
192162 prop-size={'sm'}
193163 prop-type={'link'}
194164 onClick={() => {
@@ -198,7 +168,7 @@ function Actions(props) {
198168 </Button>
199169
200170 <Button
201− disabled={online && !canSetManagementProxy}
171+ disabled={!canSetManagementProxy}
202172 prop-size={'sm'}
203173 prop-type={'link'}
204174 onClick={() => {
@@ -208,7 +178,7 @@ function Actions(props) {
208178 </Button>
209179
210180 <Button
211− disabled={online && !canConfigureKeys}
181+ disabled={!canConfigureKeys}
212182 prop-size={'sm'}
213183 prop-type={'link'}
214184 onClick={() => {
@@ -218,7 +188,7 @@ function Actions(props) {
218188 </Button>
219189
220190 <Button
221− disabled={online && !canTransfer}
191+ disabled={!canTransfer}
222192 prop-size={'sm'}
223193 prop-type={'link'}
224194 onClick={() => {
@@ -226,8 +196,15 @@ function Actions(props) {
226196 }}>
227197 {'Transfer'}
228198 </Button>
199+ <Button
200+ prop-size={'sm'}
201+ prop-type={'link'}
202+ onClick={() => {
203+ history.push(ROUTE_NAMES.INVITES_MANAGE);
204+ }}>
205+ {'Invite'}
206+ </Button>
229207 </Grid.Item>
230− <Grid.Item third={3}>{inviteAction}</Grid.Item>
231208 </Grid>
232209 </div>
233210 );
src/views/PrivateKey.jsdeleted−84
@@ -1,84 +0,0 @@
1−import Maybe from 'folktale/maybe';
2−import React from 'react';
3−import { Button } from '../components/old/Base';
4−import {
5− RequiredInput,
6− InnerLabel,
7− InputCaption,
8−} from '../components/old/Base';
9−import { H1 } from '../components/old/Base';
10−
11−import { ROUTE_NAMES } from '../lib/routeNames';
12−import { withHistory } from '../store/history';
13−import { EthereumWallet } from '../lib/wallet';
14−import { compose } from '../lib/lib';
15−import { withWallet } from '../store/wallet';
16−import View from 'components/View';
17−
18−class PrivateKey extends React.Component {
19− constructor(props) {
20− super(props);
21−
22− this.state = {
23− privateKey: '',
24− };
25−
26− this.handlePrivateKeyInput = this.handlePrivateKeyInput.bind(this);
27− }
28−
29− handlePrivateKeyInput(privateKey) {
30− this.setState({ privateKey });
31− this.constructWallet(privateKey);
32− }
33−
34− constructWallet(privateKey) {
35− const { setWallet } = this.props;
36− if (/^[0-9A-Fa-f]{64}$/g.test(privateKey) === true) {
37− const sec = Buffer.from(privateKey, 'hex');
38− const wallet = new EthereumWallet(sec);
39− setWallet(Maybe.Just(wallet));
40− } else {
41− setWallet(Maybe.Nothing());
42− }
43− }
44−
45− render() {
46− const { history, wallet } = this.props;
47− const { privateKey } = this.state;
48−
49− return (
50− <View>
51− <H1 className={'mb-4'}>{'Enter Your Private Key'}</H1>
52− <InputCaption>
53− {`Please enter your raw Ethereum private key here.`}
54− </InputCaption>
55−
56− <RequiredInput
57− className="pt-8 mt-8"
58− prop-size="md"
59− prop-format="innerLabel"
60− type="password"
61− name="privateKey"
62− onChange={this.handlePrivateKeyInput}
63− value={privateKey}
64− autocomplete="off"
65− autoFocus>
66− <InnerLabel>{'Private Key'}</InnerLabel>
67− </RequiredInput>
68−
69− <Button
70− className={'mt-10'}
71− prop-size={'wide lg'}
72− disabled={Maybe.Nothing.hasInstance(wallet)}
73− onClick={() => history.popAndPush(ROUTE_NAMES.POINTS)}>
74− {'Continue →'}
75− </Button>
76− </View>
77− );
78− }
79−}
80−
81−export default compose(
82− withHistory,
83− withWallet
84−)(PrivateKey);
src/views/Shards.jsdeleted−212
@@ -1,212 +0,0 @@
1−import { Just, Nothing } from 'folktale/maybe';
2−import React from 'react';
3−import { Button } from '../components/old/Base';
4−import {
5− InnerLabel,
6− ValidatedSigil,
7− PointInput,
8− ShardInput,
9− InputCaption,
10− Input,
11−} from '../components/old/Base';
12−import { H1, P } from '../components/old/Base';
13−import * as kg from 'urbit-key-generation/dist';
14−import * as ob from 'urbit-ob';
15−
16−import { ROUTE_NAMES } from '../lib/routeNames';
17−import { withHistory } from '../store/history';
18−import { urbitWalletFromTicket } from '../lib/wallet';
19−import { compose } from '../lib/lib';
20−import { withWallet } from '../store/wallet';
21−import View from 'components/View';
22−
23−const placeholder = len => {
24− let bytes = window.crypto.getRandomValues(new Uint8Array(len));
25− let hex = bytes.reduce(
26− (acc, byt) => acc + byt.toString(16).padStart(2, '0'),
27− ''
28− );
29− return ob.hex2patq(hex);
30−};
31−
32−const SHARDS = {
33− SHARD1: Symbol('SHARD1'),
34− SHARD2: Symbol('SHARD2'),
35− SHARD3: Symbol('SHARD3'),
36−};
37−
38−class Shards extends React.Component {
39− constructor(props) {
40− super(props);
41−
42− this.state = {
43− shard1: '',
44− shard2: '',
45− shard3: '',
46− passphrase: '',
47− pointName: '',
48− };
49−
50− this.pointPlaceholder = placeholder(4);
51− this.ticketPlaceholder = placeholder(16);
52−
53− this.handlePassphraseInput = this.handlePassphraseInput.bind(this);
54− this.handleShardInput = this.handleShardInput.bind(this);
55− this.handlePointNameInput = this.handlePointNameInput.bind(this);
56− }
57−
58− handleShardInput(shard, input) {
59− if (shard === SHARDS.SHARD1) {
60− this.setState({ shard1: input });
61− } else if (shard === SHARDS.SHARD2) {
62− this.setState({ shard2: input });
63− } else if (shard === SHARDS.SHARD3) {
64− this.setState({ shard3: input });
65− }
66− }
67−
68− handlePassphraseInput(passphrase) {
69− this.setState({ passphrase });
70− }
71−
72− handlePointNameInput(pointName) {
73− if (pointName.length < 15) {
74− this.setState({ pointName });
75− }
76− }
77−
78− async walletFromShards(shard1, shard2, shard3, pointName, passphrase) {
79− const s1 = shard1 === '' ? undefined : shard1;
80− const s2 = shard2 === '' ? undefined : shard2;
81− const s3 = shard3 === '' ? undefined : shard3;
82−
83− let ticket = undefined;
84− try {
85− ticket = kg.combine([s1, s2, s3]);
86− } catch (_) {
87− // do nothing
88− }
89−
90− if (ticket !== undefined) {
91− const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
92− this.props.setUrbitWallet(Just(uhdw));
93− }
94− }
95−
96− render() {
97− const { history, wallet } = this.props;
98− const { shard1, shard2, shard3, pointName, passphrase } = this.state;
99−
100− const phPoint = this.pointPlaceholder;
101− const phTick = this.ticketPlaceholder;
102−
103− const shards = [shard1, shard2, shard3];
104− const ready = shards.filter(x => x !== '').length > 1;
105−
106− return (
107− <View>
108− <H1>{'Authenticate'}</H1>
109−
110− <P>
111− {`Enter your point and at least two of your three Urbit master
112− ticket shards here. The index of the input field needs to
113− match the index of the shard.`}
114− </P>
115−
116− <PointInput
117− className="mono mt-8"
118− prop-size="lg"
119− prop-format="innerLabel"
120− type="text"
121− autoFocus
122− placeholder={`e.g. ${phPoint}`}
123− value={pointName}
124− onChange={this.handlePointNameInput}>
125− <InnerLabel>{'Point'}</InnerLabel>
126− <ValidatedSigil
127− className={'tr-0 mt-05 mr-0 abs'}
128− patp={pointName}
129− size={68}
130− margin={8}
131− />
132− </PointInput>
133−
134− <ShardInput
135− className="mono mt-8"
136− prop-size="md"
137− prop-format="innerLabel"
138− type="text"
139− name="shard1"
140− placeholder={`e.g. ${phTick}`}
141− value={shard1}
142− onChange={inp => this.handleShardInput(SHARDS.SHARD1, inp)}>
143− <InnerLabel>{'Shard 1'}</InnerLabel>
144− </ShardInput>
145−
146− <ShardInput
147− className="mono mt-8"
148− prop-size="md"
149− prop-format="innerLabel"
150− type="text"
151− name="shard2"
152− placeholder={`e.g. ${phTick}`}
153− value={shard2}
154− onChange={inp => this.handleShardInput(SHARDS.SHARD2, inp)}>
155− <InnerLabel>{'Shard 2'}</InnerLabel>
156− </ShardInput>
157−
158− <ShardInput
159− className="mono mt-8"
160− prop-size="md"
161− prop-format="innerLabel"
162− type="text"
163− name="shard3"
164− placeholder={`e.g. ${phTick}`}
165− value={shard3}
166− onChange={inp => this.handleShardInput(SHARDS.SHARD3, inp)}>
167− <InnerLabel>{'Shard 3'}</InnerLabel>
168− </ShardInput>
169−
170− <InputCaption>
171− {'If your wallet requires a passphrase, you may enter it below.'}
172− </InputCaption>
173−
174− <Input
175− className="pt-8"
176− prop-size="md"
177− prop-format="innerLabel"
178− name="passphrase"
179− type="password"
180− value={passphrase}
181− autocomplete="off"
182− onChange={this.handlePassphraseInput}>
183− <InnerLabel>{'Passphrase'}</InnerLabel>
184− </Input>
185−
186− <Button
187− disabled={!ready}
188− className={'mt-8'}
189− prop-size={'lg wide'}
190− // prop-color={this.buttonTriState(wallet)}
191− onClick={() =>
192− this.walletFromShards(shard1, shard2, shard3, pointName, passphrase)
193− }>
194− {'Unlock Wallet →'}
195− </Button>
196−
197− <Button
198− className={'mt-4'}
199− prop-size={'xl wide'}
200− disabled={Nothing.hasInstance(wallet)}
201− onClick={() => history.popAndPush(ROUTE_NAMES.POINTS)}>
202− {'Continue →'}
203− </Button>
204− </View>
205− );
206− }
207−}
208−
209−export default compose(
210− withHistory,
211− withWallet
212−)(Shards);
src/views/Ticket.jsdeleted−154
@@ -1,154 +0,0 @@
1−import { Just, Nothing } from 'folktale/maybe';
2−import React from 'react';
3−import { Button } from '../components/old/Base';
4−import {
5− InnerLabel,
6− ValidatedSigil,
7− PointInput,
8− TicketInput,
9− Input,
10− InputCaption,
11−} from '../components/old/Base';
12−import { H1, P } from '../components/old/Base';
13−
14−import { randomPatq, compose } from '../lib/lib';
15−import { ROUTE_NAMES } from '../lib/routeNames';
16−import { withHistory } from '../store/history';
17−import { urbitWalletFromTicket } from '../lib/wallet';
18−import { withWallet } from '../store/wallet';
19−import View from 'components/View';
20−
21−class Ticket extends React.Component {
22− constructor(props) {
23− super(props);
24−
25− this.state = {
26− ticket: '',
27− pointName: '',
28− passphrase: '',
29− isUnlocking: false,
30− };
31−
32− this.pointPlaceholder = randomPatq(4);
33− this.ticketPlaceholder = randomPatq(8);
34−
35− this.handleTicketInput = this.handleTicketInput.bind(this);
36− this.handlePassphraseInput = this.handlePassphraseInput.bind(this);
37− this.handlePointNameInput = this.handlePointNameInput.bind(this);
38− }
39−
40− handleTicketInput(ticket) {
41− this.setState({ ticket });
42− }
43−
44− handlePassphraseInput(passphrase) {
45− this.setState({ passphrase });
46− }
47−
48− handlePointNameInput(pointName) {
49− if (pointName.length < 15) {
50− this.setState({ pointName });
51− }
52− }
53−
54− async walletFromTicket(ticket, pointName, passphrase) {
55− this.setState({
56− isUnlocking: true,
57− });
58−
59− const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
60− this.props.setUrbitWallet(Just(uhdw));
61−
62− this.setState({
63− isUnlocking: false,
64− });
65− }
66−
67− render() {
68− const { history, wallet } = this.props;
69− const { ticket, pointName, passphrase } = this.state;
70−
71− const phPoint = this.pointPlaceholder;
72− const phTick = this.ticketPlaceholder;
73−
74− return (
75− <View>
76− <H1>{'Authenticate'}</H1>
77−
78− <P>
79− {`Please enter your point and Urbit master ticket here. This information is written on your Urbit HD paper wallets.`}
80− </P>
81−
82− <PointInput
83− className="mono mt-8"
84− prop-size="lg"
85− prop-format="innerLabel"
86− type="text"
87− autoFocus
88− placeholder={`e.g. ${phPoint}`}
89− value={pointName}
90− onChange={this.handlePointNameInput}>
91− <InnerLabel>{'Point'}</InnerLabel>
92− <ValidatedSigil
93− className={'tr-0 mt-05 mr-0 abs'}
94− patp={pointName}
95− size={68}
96− margin={8}
97− />
98− </PointInput>
99−
100− <TicketInput
101− className="mono mt-8"
102− prop-size="lg"
103− prop-format="innerLabel"
104− type="password"
105− name="ticket"
106− placeholder={`e.g. ${phTick}`}
107− value={ticket}
108− onChange={this.handleTicketInput}>
109− <InnerLabel>{'Ticket'}</InnerLabel>
110− </TicketInput>
111−
112− <InputCaption>
113− {'If your wallet requires a passphrase, you may enter it below.'}
114− </InputCaption>
115−
116− <Input
117− className="pt-8"
118− prop-size="md"
119− prop-format="innerLabel"
120− name="passphrase"
121− type="password"
122− value={passphrase}
123− autocomplete="off"
124− onChange={this.handlePassphraseInput}>
125− <InnerLabel>{'Passphrase'}</InnerLabel>
126− </Input>
127−
128− <Button
129− className={'mt-8'}
130− prop-size={'lg wide'}
131− disabled={this.state.isUnlocking || Just.hasInstance(wallet)}
132− onClick={() => this.walletFromTicket(ticket, pointName, passphrase)}>
133− <span className="relative">
134− {this.state.isUnlocking && <span className="btn-spinner" />}
135− {'Unlock Wallet →'}
136− </span>
137− </Button>
138−
139− <Button
140− className={'mt-4'}
141− prop-size={'xl wide'}
142− disabled={Nothing.hasInstance(wallet)}
143− onClick={() => history.popAndPush(ROUTE_NAMES.POINTS)}>
144− {'Continue →'}
145− </Button>
146− </View>
147− );
148− }
149−}
150−
151−export default compose(
152− withHistory,
153− withWallet
154−)(Ticket);
src/views/Trezor.jsdeleted−146
@@ -1,146 +0,0 @@
1−import * as bip32 from 'bip32';
2−import React from 'react';
3−import Maybe from 'folktale/maybe';
4−import { Button } from '../components/old/Base';
5−import { H1, P } from '../components/old/Base';
6−import { InnerLabel, Input, InnerLabelDropdown } from '../components/old/Base';
7−import TrezorConnect from 'trezor-connect';
8−import * as secp256k1 from 'secp256k1';
9−
10−import { TREZOR_PATH } from '../lib/trezor';
11−import { ROUTE_NAMES } from '../lib/routeNames';
12−import { withHistory } from '../store/history';
13−import { compose } from '../lib/lib';
14−import { withWallet } from '../store/wallet';
15−import View from 'components/View';
16−
17−class Trezor extends React.Component {
18− constructor(props) {
19− super(props);
20−
21− this.state = {
22− hdpath: TREZOR_PATH.replace(/x/g, 0),
23− account: 0,
24− };
25−
26− this.handleAccountSelection = this.handleAccountSelection.bind(this);
27− this.handleHdPathInput = this.handleHdPathInput.bind(this);
28− this.pollDevice = this.pollDevice.bind(this);
29− }
30−
31− handleAccountSelection(account) {
32− let hdpath = this.state.hdpath;
33− if (account !== 'custom') {
34− hdpath = TREZOR_PATH.replace(/x/g, account);
35− }
36− this.setState({ account, hdpath });
37− }
38−
39− handleHdPathInput(hdpath) {
40− this.setState({ hdpath });
41− }
42−
43− async pollDevice() {
44− const { setWallet, setWalletHdPath } = this.props;
45− const { hdpath } = this.state;
46−
47− TrezorConnect.manifest({
48− email: 'bridge-trezor@urbit.org',
49− appUrl: 'https://github.com/urbit/bridge',
50− });
51− TrezorConnect.getPublicKey({ path: hdpath }).then(info => {
52− if (info.success === true) {
53− const payload = info.payload;
54− const publicKey = Buffer.from(payload.publicKey, 'hex');
55− const chainCode = Buffer.from(payload.chainCode, 'hex');
56− const pub = secp256k1.publicKeyConvert(publicKey, true);
57− const hd = bip32.fromPublicKey(pub, chainCode);
58− setWallet(Maybe.Just(hd));
59− setWalletHdPath(hdpath);
60− } else {
61− setWallet(Maybe.Nothing());
62− }
63− });
64− }
65−
66− render() {
67− const { history, wallet } = this.props;
68− const { hdpath, account } = this.state;
69− const { handleAccountSelection } = this;
70−
71− let accountOptions = [
72− {
73− title: 'Custom path',
74− value: 'custom',
75− },
76− ];
77− for (let i = 0; i < 20; i++) {
78− accountOptions.push({
79− title: 'Account #' + (i + 1),
80− value: i,
81− });
82− }
83− let accountTitle = accountOptions.find(o => o.value === account).title;
84−
85− const accountSelection = (
86− <InnerLabelDropdown
87− className="mt-8"
88− prop-size="md"
89− prop-format="innerLabel"
90− options={accountOptions}
91− handleUpdate={handleAccountSelection}
92− title={'Account'}
93− currentSelectionTitle={accountTitle}
94− fullWidth={true}
95− />
96− );
97−
98− const pathSelection =
99− account !== 'custom' ? null : (
100− <Input
101− className="mt-8 pt-8 text-mono"
102− prop-size="md"
103− prop-format="innerLabel"
104− name="hdpath"
105− value={hdpath}
106− autocomplete="off"
107− onChange={this.handleHdPathInput}>
108− <InnerLabel>{'HD Path'}</InnerLabel>
109− </Input>
110− );
111−
112− return (
113− <View>
114− <H1>{'Authenticate With Your Trezor'}</H1>
115−
116− <P>
117− {`Connect and authenticate to your Trezor. If you'd like
118− to use a custom derivation path, you may enter it below.`}
119− </P>
120−
121− {accountSelection}
122− {pathSelection}
123−
124− <Button
125− className={'mt-8'}
126− prop-size={'wide lg'}
127− onClick={this.pollDevice}>
128− {'Authenticate →'}
129− </Button>
130−
131− <Button
132− className={'mt-8'}
133− prop-size={'wide lg'}
134− disabled={Maybe.Nothing.hasInstance(wallet)}
135− onClick={() => history.popAndPush(ROUTE_NAMES.POINTS)}>
136− {'Continue →'}
137− </Button>
138− </View>
139− );
140− }
141−}
142−
143−export default compose(
144− withHistory,
145− withWallet
146−)(Trezor);
src/views/ViewPoint.js+25−17
@@ -1,6 +1,7 @@
11import React, { useCallback, useState } from 'react';
22import Maybe from 'folktale/maybe';
33import * as ob from 'urbit-ob';
4+import { Input } from 'indigo-react';
45
56import { H1, P } from '../components/old/Base';
67
@@ -11,15 +12,20 @@ import { ROUTE_NAMES } from 'lib/routeNames';
1112
1213import View from 'components/View';
1314import { ForwardButton } from 'components/Buttons';
14−import { PointInput } from 'components/Inputs';
15+import { usePointInput } from 'components/Inputs';
16+import InputSigil from 'components/InputSigil';
1517
1618export default function ViewPoint() {
1719 const history = useHistory();
1820 const { setPointCursor } = usePointCursor();
19− const [pointName, setPointName] = useState('');
20− const [pass, setPass] = useState(false);
21− const [error, setError] = useState();
22− const [focused, setFocused] = useState(true);
21+ const [lastValidPoint, setLastValidPoint] = useState('');
22+ const pointInput = usePointInput({
23+ name: 'point',
24+ label: 'Point Name',
25+ autoFocus: true,
26+ onValue: setLastValidPoint,
27+ });
28+ const { data: pointName, pass, error, focused } = pointInput;
2329
2430 const disabled = !pass;
2531 const goForward = useCallback(() => {
@@ -34,19 +40,21 @@ export default function ViewPoint() {
3440
3541 <P>Enter a point name to view its public information.</P>
3642
37− <PointInput
38− name="point"
39− label="Point Name"
40− initialValue={pointName}
41− onValue={setPointName}
42− pass={pass}
43− onPass={setPass}
44− error={error}
45− onError={setError}
46− focused={focused}
47− onFocus={setFocused}
43+ <Input
44+ {...pointInput}
4845 onEnter={goForward}
49− autoFocus
46+ accessory={
47+ lastValidPoint && (
48+ <InputSigil
49+ patp={lastValidPoint}
50+ size={68}
51+ margin={8}
52+ pass={pass}
53+ focused={focused}
54+ error={error}
55+ />
56+ )
57+ }
5058 />
5159
5260 <ForwardButton
src/views/Wallet.jsdeleted−101
@@ -1,101 +0,0 @@
1−import React from 'react';
2−import Maybe from 'folktale/maybe';
3−
4−import { H1, P } from '../components/old/Base';
5−import { InnerLabelDropdown } from '../components/old/Base';
6−
7−import View from 'components/View';
8−import { ForwardButton } from 'components/Buttons';
9−
10−import { useHistory } from '../store/history';
11−import { useWallet } from '../store/wallet';
12−
13−import { ROUTE_NAMES } from '../lib/routeNames';
14−import { WALLET_TYPES, renderWalletType } from '../lib/wallet';
15−import useLifecycle from 'lib/useLifecycle';
16−
17−const kWalletOptions = [
18− {
19− title: 'Urbit Master Ticket',
20− value: WALLET_TYPES.TICKET,
21− },
22− {
23− title: 'Urbit Master Ticket (Shards)',
24− value: WALLET_TYPES.SHARDS,
25− },
26− {
27− type: 'divider',
28− },
29− {
30− title: 'Ledger',
31− value: WALLET_TYPES.LEDGER,
32− },
33− {
34− title: 'Trezor',
35− value: WALLET_TYPES.TREZOR,
36− },
37− {
38− type: 'divider',
39− },
40− {
41− title: 'BIP39 Mnemonic',
42− value: WALLET_TYPES.MNEMONIC,
43− },
44− {
45− title: 'Ethereum Private Key',
46− value: WALLET_TYPES.PRIVATE_KEY,
47− },
48− {
49− title: 'Ethereum Keystore File',
50− value: WALLET_TYPES.KEYSTORE,
51− },
52−];
53−
54−export default function Wallet() {
55− const history = useHistory();
56− const { setWallet, walletType, setWalletType } = useWallet();
57−
58− // reset wallet when this component is mounted
59− useLifecycle(() => setWallet(Maybe.Nothing()));
60−
61− return (
62− <View>
63− <H1>Unlock a Wallet</H1>
64−
65− <P>
66− To manage your assets, you need to unlock a wallet. Please select how
67− you would like to access your wallet.
68− </P>
69−
70− <InnerLabelDropdown
71− title="Wallet Type:"
72− options={kWalletOptions}
73− handleUpdate={setWalletType}
74− currentSelectionTitle={renderWalletType(walletType)}
75− />
76−
77− <ForwardButton
78− onClick={() =>
79− history.push(
80− walletType === WALLET_TYPES.MNEMONIC
81− ? ROUTE_NAMES.MNEMONIC
82− : walletType === WALLET_TYPES.TICKET
83− ? ROUTE_NAMES.TICKET
84− : walletType === WALLET_TYPES.SHARDS
85− ? ROUTE_NAMES.SHARDS
86− : walletType === WALLET_TYPES.LEDGER
87− ? ROUTE_NAMES.LEDGER
88− : walletType === WALLET_TYPES.TREZOR
89− ? ROUTE_NAMES.TREZOR
90− : walletType === WALLET_TYPES.PRIVATE_KEY
91− ? ROUTE_NAMES.PRIVATE_KEY
92− : walletType === WALLET_TYPES.KEYSTORE
93− ? ROUTE_NAMES.KEYSTORE
94− : ROUTE_NAMES.DEFAULT
95− )
96− }>
97− Continue
98− </ForwardButton>
99− </View>
100− );
101−}