on master help sign in
checkpoint: initial refactor of email list, move all input state into useForm
f34e8d0Matt Condon (shrugs)
package-lock.json+5
@@ -14511,6 +14511,11 @@
14511 }
14511 }
14512 }
14512 }
14513 },
14513 },
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+ },
14514 "read-pkg": {
14519 "read-pkg": {
14515 "version": "1.1.0",
14520 "version": "1.1.0",
14516 "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
14521 "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
package.json+1
@@ -36,6 +36,7 @@
36 "react": "^16.8.6",
36 "react": "^16.8.6",
37 "react-dom": "^16.8.6",
37 "react-dom": "^16.8.6",
38 "react-scripts": "3.0.1",
38 "react-scripts": "3.0.1",
39+ "react-teleporter": "^1.1.0",
39 "secp256k1": "^3.5.2",
40 "secp256k1": "^3.5.2",
40 "sigil-js": "github:urbit/sigil-js#f7f29c5",
41 "sigil-js": "github:urbit/sigil-js#f7f29c5",
41 "trezor-connect": "^7.0.1",
42 "trezor-connect": "^7.0.1",
src/Bridge.js+4−3
@@ -24,9 +24,10 @@ const kIsStubbed = isDevelopment && shouldStubLocal;
24const kInitialRoutes = kIsStubbed
24const kInitialRoutes = kIsStubbed
25 ? [
25 ? [
26 { key: ROUTE_NAMES.LANDING },
26 { key: ROUTE_NAMES.LANDING },
27− { key: ROUTE_NAMES.NETWORK },
27+ { key: ROUTE_NAMES.LOGIN },
28− { key: ROUTE_NAMES.WALLET },
29 { key: ROUTE_NAMES.POINTS },
28 { key: ROUTE_NAMES.POINTS },
29+ { key: ROUTE_NAMES.POINT },
30+ { key: ROUTE_NAMES.INVITE },
30 ]
31 ]
31 : [{ key: ROUTE_NAMES.LANDING }];
32 : [{ key: ROUTE_NAMES.LANDING }];
32
33
@@ -39,7 +40,7 @@ const kInitialWallet = kIsStubbed
39const kInitialMnemonic = kIsStubbed
40const kInitialMnemonic = kIsStubbed
40 ? Just(process.env.REACT_APP_DEV_MNEMONIC)
41 ? Just(process.env.REACT_APP_DEV_MNEMONIC)
41 : Nothing();
42 : Nothing();
42−const kInitialPointCursor = kIsStubbed ? Just(0) : Nothing();
43+const kInitialPointCursor = kIsStubbed ? Just(65792) : Nothing();
43
44
44export default function Bridge() {
45export default function Bridge() {
45 return (
46 return (
src/_walletgen/Walletgen.js+1−1
@@ -4,7 +4,7 @@ import { find } from 'lodash';
4import Header from './components/Header';
4import Header from './components/Header';
5import Footer from './components/Footer';
5import Footer from './components/Footer';
6
6
7−import { PROFILE_STATES, GEN_STATES } from './lib/constants';
7+import { PROFILE_STATES, GEN_STATES } from '../lib/constants';
8
8
9import PrivacyPolicy from './views/PrivacyPolicy';
9import PrivacyPolicy from './views/PrivacyPolicy';
10import Welcome from './views/Welcome';
10import Welcome from './views/Welcome';
src/_walletgen/views/Generate.js+7−2
@@ -2,13 +2,18 @@ import React from 'react';
2import * as sigil from 'sigil-js';
2import * as sigil from 'sigil-js';
3import ob from 'urbit-ob';
3import ob from 'urbit-ob';
4import classnames from 'classnames';
4import classnames from 'classnames';
5−import * as wg from '../lib/lib';
5+import * as wg from '../../lib/walletgen';
6
6
7import Button from '../components/Button';
7import Button from '../components/Button';
8import ReactSVGComponents from '../components/ReactSVGComponents';
8import ReactSVGComponents from '../components/ReactSVGComponents';
9import PaperCollateralRenderer from 'PaperCollateralRenderer';
9import PaperCollateralRenderer from 'PaperCollateralRenderer';
10
10
11+import {
12+ MAX_GALAXY,
13+ MIN_STAR,
14+ MAX_STAR,
15+ GEN_STATES,
11−import { MAX_GALAXY, MIN_STAR, MAX_STAR, GEN_STATES } from '../lib/constants';
16+} from '../../lib/constants';
12
17
13const NEXT_STEP_NUM = 6;
18const NEXT_STEP_NUM = 6;
14
19
src/_walletgen/views/Upload.js+1−1
@@ -5,7 +5,7 @@ import UploadButton from '../components/UploadButton';
5
5
6import { compose } from '../../bridge/lib/lib';
6import { compose } from '../../bridge/lib/lib';
7
7
8−import { PROFILE_STATES } from '../lib/constants';
8+import { PROFILE_STATES } from '../../lib/constants';
9
9
10const NEXT_STEP_NUM = 3;
10const NEXT_STEP_NUM = 3;
11
11
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';
2import { Button } from 'indigo-react';
2import { Button } from 'indigo-react';
3
3
4// TODO: use svg icons
4// TODO: use svg icons
5−export const ForwardButton = props => <Button icon="→" {...props} />;
5+export const ForwardButton = props => <Button accessory="→" {...props} />;
6−export const DownloadButton = props => <Button icon="⬇" {...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';
1+import { createTeleporter } from 'react-teleporter';
2−import { createPortal } from 'react-dom';
3
2
4−const kFooterPortalId = 'footer-portal';
3+const FooterTeleporter = createTeleporter();
5
4
6−function Footer({ children }) {
5+const Footer = FooterTeleporter.Source;
7− return createPortal(
6+Footer.Target = FooterTeleporter.Target;
8− Children.only(children),
9− document.getElementById(kFooterPortalId)
10− );
11−}
12−
13−Footer.Portal = function FooterPortal(props) {
14− return <div {...props} id={kFooterPortalId} />;
15−};
16
7
17export default Footer;
8export default Footer;
src/components/Inputs.js+104−56
@@ -10,10 +10,10 @@ import {
10 validateMnemonic,
10 validateMnemonic,
11 validatePoint,
11 validatePoint,
12 validateTicket,
12 validateTicket,
13+ validateEmail,
13} from 'lib/validators';
14} from 'lib/validators';
14import { prependSig } from 'lib/transformers';
15import { prependSig } from 'lib/transformers';
15−
16+import useForm from 'indigo-react/lib/useForm';
16−const kExampleMnemonic = bip39.generateMnemonic();
17
17
18// const RequiredInput = advancedInput({
18// const RequiredInput = advancedInput({
19// WrappedComponent: Input,
19// WrappedComponent: Input,
@@ -35,38 +35,47 @@ const kExampleMnemonic = bip39.generateMnemonic();
35// validators: [validateEthereumAddress, validateNotEmpty],
35// validators: [validateEthereumAddress, validateNotEmpty],
36// });
36// });
37
37
38+const firstOf = state => state.inputs[0];
39+
38−export function PassphraseInput(props) {
40+export function usePassphraseInput(props) {
39− return (
41+ return firstOf(
40− <Input
42+ useForm([
43+ {
41− type="password"
44+ type: 'password',
42− autoComplete="off"
45+ autoComplete: 'off',
43− placeholder="Passphrase"
46+ placeholder: 'Passphrase',
44− {...props}
47+ ...props,
45− />
48+ },
49+ ])
46 );
50 );
47}
51}
48
52
53+const kExampleMnemonic = bip39.generateMnemonic();
54+const kMnemonicValidators = [validateMnemonic, validateNotEmpty];
49−export function MnemonicInput(props) {
55+export function useMnemonicInput(props) {
50− return (
56+ return firstOf(
51− <Input
57+ useForm([
58+ {
52− type="text"
59+ type: 'text',
53− autoComplete="off"
60+ autoComplete: 'off',
54− placeholder={`e.g. ${kExampleMnemonic}`}
61+ placeholder: `e.g. ${kExampleMnemonic}`,
55− validators={[validateMnemonic, validateNotEmpty]}
62+ validators: kMnemonicValidators,
56− {...props}
63+ ...props,
57− />
64+ },
65+ ])
58 );
66 );
59}
67}
60
68
61−export function HdPathInput(props) {
69+export function useHdPathInput(props) {
62− return (
70+ return firstOf(
63− <Input
71+ useForm([
72+ {
64− type="text"
73+ type: 'text',
65− autoComplete="off"
74+ autoComplete: 'off',
66− placeholder={`${DEFAULT_HD_PATH}`}
75+ placeholder: `e.g. ${DEFAULT_HD_PATH}`,
67− validators={[validateNotEmpty]}
76+ ...props,
68− {...props}
77+ },
69− />
78+ ])
70 );
79 );
71}
80}
72
81
@@ -77,42 +86,81 @@ export function HdPathInput(props) {
77// });
86// });
78
87
79export function PointInput(props) {
88export 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+
80 return (
96 return (
81 <Input
97 <Input
82− type="text"
98+ {...props}
83− placeholder="e.g. ~zod"
99+ {...input}
84− initialValue={props.initialValue || '~'}
85− validators={[validatePoint, validateNotEmpty]}
86− transformers={[prependSig]}
87− mono
88 accessory={
100 accessory={
101+ validPointName && (
89− <InputSigil
102+ <InputSigil
90− patp={props.initialValue || '~'}
103+ patp={validPointName}
91− size={68}
104+ size={68}
92− margin={8}
105+ margin={8}
93− pass={props.pass}
106+ pass={pass}
94− onPass={props.setPass}
95− focused={props.focused}
107+ focused={focused}
96− onFocus={props.setFocused}
97− error={props.error}
108+ error={error}
98− onError={props.setError}
99− />
109+ />
110+ )
100 }
111 }
101− {...props}
102 />
112 />
103 );
113 );
104}
114}
105
115
116+const kTicketValidators = [validateTicket, validateNotEmpty];
117+const kTicketTransformers = [prependSig];
106//TODO needs to be fancier, displaying sig and dashes instead of •ing all
118//TODO needs to be fancier, displaying sig and dashes instead of •ing all
107−export function TicketInput(props) {
119+export function useTicketInput({ initialValue = '~', ...rest }) {
108− return (
120+ return firstOf(
109− <Input
121+ useForm([
122+ {
110− type="password"
123+ type: 'password',
111− placeholder="~master-ticket"
124+ placeholder: '~master-ticket',
112− validators={[validateTicket, validateNotEmpty]}
125+ validators: kTicketValidators,
113− transformers={[prependSig]}
126+ transformers: kTicketTransformers,
114− mono
127+ mono: true,
115− {...props}
128+ initialValue,
116− />
129+ ...rest,
130+ },
131+ ])
117 );
132 );
118}
133}
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 }) {
20 )}
20 )}
21 {...rest}>
21 {...rest}>
22 <Flex.Item className="pv5">{children}</Flex.Item>
22 <Flex.Item className="pv5">{children}</Flex.Item>
23− <Footer.Portal />
23+ <Footer.Target />
24 </Flex>
24 </Flex>
25 );
25 );
26}
26}
src/components/old/Header.js+1−5
@@ -5,18 +5,14 @@ import { getRouteBreadcrumb } from '../../lib/router';
5import { ROUTE_NAMES } from '../../lib/routeNames';
5import { ROUTE_NAMES } from '../../lib/routeNames';
6import { useHistory } from '../../store/history';
6import { useHistory } from '../../store/history';
7import { isLast } from '../../lib/lib';
7import { isLast } from '../../lib/lib';
8−import { useNetwork } from '../../store/network';
9−import { useWallet } from '../../store/wallet';
10import { usePointCursor } from '../../store/pointCursor';
8import { usePointCursor } from '../../store/pointCursor';
11
9
12// hook to create a breadcrumb builder function based on the current
10// hook to create a breadcrumb builder function based on the current
13// global states
11// global states
14function useRouteBreadcrumbBuilder() {
12function useRouteBreadcrumbBuilder() {
15− const { networkType } = useNetwork();
16− const { wallet } = useWallet();
17 const { pointCursor } = usePointCursor();
13 const { pointCursor } = usePointCursor();
18
14
19− return getRouteBreadcrumb({ pointCursor, networkType, wallet });
15+ return getRouteBreadcrumb({ pointCursor });
20}
16}
21
17
22function Crumbs(props) {
18function Crumbs(props) {
src/components/old/HorizontalSelector.js+1−1
@@ -22,8 +22,8 @@ class HorizontalSelector extends React.Component {
22 let active = opt.value === this.state.option ? 'btn-primary active' : '';
22 let active = opt.value === this.state.option ? 'btn-primary active' : '';
23 return (
23 return (
24 <button
24 <button
25+ key={opt.value.toString()}
25 className={`btn ${active} h-10 ph-4 flex sans ${this.props.className}`}
26 className={`btn ${active} h-10 ph-4 flex sans ${this.props.className}`}
26− value={opt.value}
27 onClick={() => this.selectOption(opt.value)}>
27 onClick={() => this.selectOption(opt.value)}>
28 {opt.title}
28 {opt.title}
29 </button>
29 </button>
src/indigo-react/components/Button.js+10−6
@@ -10,7 +10,8 @@ export default function Button({
10 disabled = false,
10 disabled = false,
11 detail,
11 detail,
12 className,
12 className,
13− icon = '→',
13+ accessory = '→',
14+ onClick,
14 children,
15 children,
15 ...rest
16 ...rest
16}) {
17}) {
@@ -24,12 +25,14 @@ export default function Button({
24 as="a"
25 as="a"
25 gap={4}
26 gap={4}
26 className={cn(
27 className={cn(
27− 'pointer p4 truncate flex-row justify-between',
28+ 'pointer pv4 truncate flex-row justify-between',
28 {
29 {
30+ p4: solid,
31+ },
32+ {
29− 'bg-black bg-gray6-hover': solid && !disabled,
33+ 'bg-black': solid && !disabled,
30 'bg-gray3': solid && disabled,
34 'bg-gray3': solid && disabled,
31− 'bg-transparent bg-gray1-hover': !solid && !disabled,
35+ 'bg-transparent': !solid,
32− 'bg-transparent': !solid && disabled,
33 },
36 },
34 className
37 className
35 )}
38 )}
@@ -39,10 +42,11 @@ export default function Button({
39 cursor: 'not-allowed',
42 cursor: 'not-allowed',
40 }),
43 }),
41 }}
44 }}
45+ onClick={onClick ? () => !disabled && onClick() : undefined}
42 {...rest}>
46 {...rest}>
43 <Grid.Item as={Flex} justify="between" full>
47 <Grid.Item as={Flex} justify="between" full>
44 <span className={cn(textColor)}>{children}</span>
48 <span className={cn(textColor)}>{children}</span>
45− <div className={cn('pl4', textColor)}>{icon}</div>
49+ <div className={cn('pl4', textColor)}>{accessory}</div>
46 </Grid.Item>
50 </Grid.Item>
47 {detail && (
51 {detail && (
48 <Grid.Item as={HelpText} full>
52 <Grid.Item as={HelpText} full>
src/indigo-react/components/Flex.js+1−1
@@ -1,7 +1,7 @@
1import React from 'react';
1import React from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3
3
4−import getComponent from 'indigo-react/lib/getComponent';
4+import getComponent from '../lib/getComponent';
5
5
6function Flex({
6function Flex({
7 as = 'div',
7 as = 'div',
src/indigo-react/components/Grid.js+6
@@ -21,6 +21,8 @@ Grid.Item = function GridItem({
21 fourth = 0,
21 fourth = 0,
22 rows = [],
22 rows = [],
23 cols = [],
23 cols = [],
24+ justifySelf,
25+ alignSelf,
24 className,
26 className,
25 ...rest
27 ...rest
26}) {
28}) {
@@ -38,6 +40,10 @@ Grid.Item = function GridItem({
38 [`r${rows[0]}-${rows[1]}`]: rows.length === 2,
40 [`r${rows[0]}-${rows[1]}`]: rows.length === 2,
39 [`c${cols[0]}-${cols[1]}`]: cols.length === 2,
41 [`c${cols[0]}-${cols[1]}`]: cols.length === 2,
40 },
42 },
43+ {
44+ [`justify-self-${justifySelf}`]: justifySelf,
45+ [`align-self-${alignSelf}`]: alignSelf,
46+ },
41 className
47 className
42 )}
48 )}
43 {...rest}
49 {...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';
2import cn from 'classnames';
2import cn from 'classnames';
3
3
4−import { Flex } from 'indigo-react';
4+import Flex from './Flex';
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−}
115
5
116export default function Input({
6export default function Input({
7+ // visuals
117 name,
8 name,
118 label,
9 label,
119− initialValue = '',
120− className = '',
10+ className,
121− validators = [],
11+ accessory,
122− transformers = [],
12+ mono = false,
13+
14+ // callbacks
123 onValue,
15 onValue,
124 onPass,
16 onPass,
125 onError,
17 onError,
126 onFocus,
18 onFocus,
127 onEnter,
19 onEnter,
128− accessory,
20+
129− disabled = false,
21+ // state from hook
130− mono = false,
22+ focused,
23+ pass,
24+ visiblyPassed,
25+ error,
26+ data,
27+ bind,
131− autoFocus = false,
28+ autoFocus,
29+ disabled,
30+
31+ // ignored
32+ initialValue,
33+ validators,
34+ transformers,
35+
36+ // extra
132 ...rest
37 ...rest
133}) {
38}) {
134− const actuallyAutoFocus = autoFocus && !disabled;
39+ // NB(shrugs): we disable exhaustive deps because we don't want the callbacks
135− const { focused, pass, visiblyPassed, error, data, bind } = useInput({
40+ // (whose identity might change between renders) to trigger a re-notify of
136− validators,
41+ // state that we already know
137− transformers,
42+ // this also happens to prevent render loops when rendering a map of inputs
138− initialValue,
139− autoFocus: actuallyAutoFocus,
140− });
141
43
142 // notify parent of enter keypress iff not disabled
44 // notify parent of enter keypress iff not disabled
143 const onKeyPress = useCallback(
45 const onKeyPress = useCallback(
144 e => !disabled && e.key === 'Enter' && onEnter && onEnter(),
46 e => !disabled && e.key === 'Enter' && onEnter && onEnter(),
145− [disabled, onEnter]
47+ [disabled] // eslint-disable-line react-hooks/exhaustive-deps
146 );
48 );
147
49
148 // notify parent of value only when passing
50 // notify parent of value only when passing
149 useEffect(() => {
51 useEffect(() => {
150 pass && onValue && onValue(data);
52 pass && onValue && onValue(data);
151− }, [pass, data, onValue]);
53+ }, [pass, data]); // eslint-disable-line react-hooks/exhaustive-deps
152
54
153 // notify parent of pass
55 // notify parent of pass
154 useEffect(() => {
56 useEffect(() => {
155 onPass && onPass(pass);
57 onPass && onPass(pass);
156− }, [pass, onPass]);
58+ }, [pass]); // eslint-disable-line react-hooks/exhaustive-deps
157
59
158 // notify parent of error whenever error changes
60 // notify parent of error whenever error changes
159 useEffect(() => {
61 useEffect(() => {
160 onError && onError(error);
62 onError && onError(error);
161− }, [error, onError]);
63+ }, [error]); // eslint-disable-line react-hooks/exhaustive-deps
162
64
163 // notify parent of focus
65 // notify parent of focus
164 useEffect(() => {
66 useEffect(() => {
165 onFocus && onFocus(focused);
67 onFocus && onFocus(focused);
166− }, [focused, onFocus]);
68+ }, [focused]); // eslint-disable-line react-hooks/exhaustive-deps
167
69
168 return (
70 return (
169− <Flex col>
71+ <Flex className={className} col>
170 <Flex.Item as="label" className="f6 lh-tall" htmlFor={name}>
72 <Flex.Item as="label" className="f6 lh-tall" htmlFor={name}>
171 {label}
73 {label}
172 </Flex.Item>
74 </Flex.Item>
@@ -176,7 +78,7 @@ export default function Input({
176 {...rest}
78 {...rest}
177 cols={[1, accessory ? 11 : 13]}
79 cols={[1, accessory ? 11 : 13]}
178 className={cn(
80 className={cn(
179− 'b b1 p3',
81+ 'b b1 p3 outline-none',
180 { mono },
82 { mono },
181 {
83 {
182 'bg-white': !disabled,
84 'bg-white': !disabled,
@@ -191,20 +93,24 @@ export default function Input({
191 'b-black': focused && !visiblyPassed,
93 'b-black': focused && !visiblyPassed,
192 'b-yellow3': !focused && !visiblyPassed && error,
94 'b-yellow3': !focused && !visiblyPassed && error,
193 'b-gray3': !focused && !error && !visiblyPassed,
95 'b-gray3': !focused && !error && !visiblyPassed,
194− },
96+ }
195− className
97+ // TODO: inputClassName ?
196 )}
98 )}
197 style={{
99 style={{
100+ ...(disabled && {
198− ...(disabled && { pointerEvents: 'none', cursor: 'not-allowed' }),
101+ pointerEvents: 'none',
102+ cursor: 'not-allowed',
103+ }),
199 }}
104 }}
200− onKeyPress={onKeyPress}
201 id={name}
105 id={name}
202− autoFocus={actuallyAutoFocus}
106+ name={name}
107+ onKeyPress={onKeyPress}
203 {...bind}
108 {...bind}
204 flex
109 flex
205 />
110 />
206 {accessory && (
111 {accessory && (
112+ <Flex.Item
207− <Flex.Item style={{ height: '45px', overflow: 'hidden' }}>
113+ style={{ height: '45px', width: '45px', overflow: 'hidden' }}>
208 {accessory}
114 {accessory}
209 </Flex.Item>
115 </Flex.Item>
210 )}
116 )}
src/indigo-react/index.js+1
@@ -3,5 +3,6 @@ export { default as Grid } from './components/Grid';
3export { default as Flex } from './components/Flex';
3export { default as Flex } from './components/Flex';
4export { default as Input } from './components/Input';
4export { default as Input } from './components/Input';
5export { default as Button } from './components/Button';
5export { default as Button } from './components/Button';
6+export { default as IconButton } from './components/IconButton';
6
7
7export * from './components/Typography';
8export * from './components/Typography';
src/indigo-react/indigo.css+66−21
@@ -13,7 +13,7 @@
13 * git+https://github.com/urbit/Indigo.git
13 * git+https://github.com/urbit/Indigo.git
14 * Bugs: https://github.com/urbit/Indigo/issues
14 * Bugs: https://github.com/urbit/Indigo/issues
15 * License: MIT
15 * 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)
17 *******************************************************************************/
17 *******************************************************************************/
18/**********************
18/**********************
19Color variables
19Color variables
@@ -1575,19 +1575,34 @@ caption,
1575 justify-content: center;
1575 justify-content: center;
1576 align-items: center; }
1576 align-items: center; }
1577
1577
1578−.self-start {
1578+.align-self-start {
1579 align-self: flex-start; }
1579 align-self: flex-start; }
1580
1580
1581−.self-end {
1581+.align-self-end {
1582 align-self: flex-end; }
1582 align-self: flex-end; }
1583
1583
1584−.self-center {
1584+.align-self-center {
1585 align-self: center; }
1585 align-self: center; }
1586
1586
1587−.self-baseline {
1587+.align-self-baseline {
1588 align-self: baseline; }
1588 align-self: baseline; }
1589
1589
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+
1590−.self-stretch {
1605+.align-self-stretch {
1591 align-self: stretch; }
1606 align-self: stretch; }
1592
1607
1593.content-start {
1608.content-start {
@@ -1648,16 +1663,26 @@ caption,
1648 .flex-center-ns {
1663 .flex-center-ns {
1649 justify-content: center;
1664 justify-content: center;
1650 align-items: center; }
1665 align-items: center; }
1651− .self-start-ns {
1666+ .align-self-start-ns {
1652 align-self: flex-start; }
1667 align-self: flex-start; }
1653− .self-end-ns {
1668+ .align-self-end-ns {
1654 align-self: flex-end; }
1669 align-self: flex-end; }
1655− .self-center-ns {
1670+ .align-self-center-ns {
1656 align-self: center; }
1671 align-self: center; }
1657− .self-baseline-ns {
1672+ .align-self-baseline-ns {
1658 align-self: baseline; }
1673 align-self: baseline; }
1659− .self-stretch-ns {
1674+ .align-self-stretch-ns {
1660 align-self: stretch; }
1675 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; }
1661 .content-start-ns {
1686 .content-start-ns {
1662 align-content: flex-start; }
1687 align-content: flex-start; }
1663 .content-end-ns {
1688 .content-end-ns {
@@ -1711,16 +1736,26 @@ caption,
1711 .flex-center-md {
1736 .flex-center-md {
1712 justify-content: center;
1737 justify-content: center;
1713 align-items: center; }
1738 align-items: center; }
1714− .self-start-md {
1739+ .align-self-start-md {
1715 align-self: flex-start; }
1740 align-self: flex-start; }
1716− .self-end-md {
1741+ .align-self-end-md {
1717 align-self: flex-end; }
1742 align-self: flex-end; }
1718− .self-center-md {
1743+ .align-self-center-md {
1719 align-self: center; }
1744 align-self: center; }
1720− .self-baseline-md {
1745+ .align-self-baseline-md {
1721 align-self: baseline; }
1746 align-self: baseline; }
1722− .self-stretch-md {
1747+ .align-self-stretch-md {
1723 align-self: stretch; }
1748 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; }
1724 .content-start-md {
1759 .content-start-md {
1725 align-content: flex-start; }
1760 align-content: flex-start; }
1726 .content-end-md {
1761 .content-end-md {
@@ -1774,16 +1809,26 @@ caption,
1774 .flex-center-lg {
1809 .flex-center-lg {
1775 justify-content: center;
1810 justify-content: center;
1776 align-items: center; }
1811 align-items: center; }
1777− .self-start-lg {
1812+ .align-self-start-lg {
1778 align-self: flex-start; }
1813 align-self: flex-start; }
1779− .self-end-lg {
1814+ .align-self-end-lg {
1780 align-self: flex-end; }
1815 align-self: flex-end; }
1781− .self-center-lg {
1816+ .align-self-center-lg {
1782 align-self: center; }
1817 align-self: center; }
1783− .self-baseline-lg {
1818+ .align-self-baseline-lg {
1784 align-self: baseline; }
1819 align-self: baseline; }
1785− .self-stretch-lg {
1820+ .align-self-stretch-lg {
1786 align-self: stretch; }
1821 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; }
1787 .content-start-lg {
1832 .content-start-lg {
1788 align-content: flex-start; }
1833 align-content: flex-start; }
1789 .content-end-lg {
1834 .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;
11
11
12const SEED_ENTROPY_BITS = 128;
12const SEED_ENTROPY_BITS = 128;
13
13
14+// TODO: this is walletgen-ui specific, move into a wallet router later
14const GEN_STATES = {
15const GEN_STATES = {
15 ENY_NOSTART: Symbol('ENY_NOSTART'),
16 ENY_NOSTART: Symbol('ENY_NOSTART'),
16 ENY_PENDING: Symbol('ENY_PENDING'),
17 ENY_PENDING: Symbol('ENY_PENDING'),
src/lib/invite.js+1−1
@@ -3,7 +3,7 @@ import Tx from 'ethereumjs-tx';
3
3
4import * as azimuth from 'azimuth-js';
4import * as azimuth from 'azimuth-js';
5import * as kg from 'urbit-key-generation/dist';
5import * as kg from 'urbit-key-generation/dist';
6−import * as wg from '../_walletgen/lib/lib.js';
6+import * as wg from './walletgen';
7import * as tank from './tank';
7import * as tank from './tank';
8
8
9import JSZip from 'jszip';
9import JSZip from 'jszip';
src/lib/routeNames.js−9
@@ -11,15 +11,6 @@ export const ROUTE_NAMES = {
11 ACCEPT_TRANSFER: Symbol('ACCEPT_TRANSFER'),
11 ACCEPT_TRANSFER: Symbol('ACCEPT_TRANSFER'),
12 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
12 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
13 LANDING: Symbol('LANDING'),
13 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'),
23 VIEW_POINT: Symbol('VIEW_POINT'),
14 VIEW_POINT: Symbol('VIEW_POINT'),
24 POINTS: Symbol('POINTS'),
15 POINTS: Symbol('POINTS'),
25 POINT: Symbol('POINT'),
16 POINT: Symbol('POINT'),
src/lib/router.js+2−53
@@ -1,22 +1,15 @@
1−import React from 'react';
2import * as ob from 'urbit-ob';
1import * as ob from 'urbit-ob';
3import * as need from './need';
2import * as need from './need';
4
3
5import Landing from '../views/Landing';
4import Landing from '../views/Landing';
5+import Login from '../views/Login.js';
6import InviteTicket from '../views/InviteTicket';
6import InviteTicket from '../views/InviteTicket';
7import InvitesManage from '../views/InvitesManage.js';
7import InvitesManage from '../views/InvitesManage.js';
8−import Login from '../views/Login.js';
9import AcceptTransfer from '../views/AcceptTransfer';
8import AcceptTransfer from '../views/AcceptTransfer';
10import CancelTransfer from '../views/CancelTransfer';
9import CancelTransfer from '../views/CancelTransfer';
11import CreateGalaxy from '../views/CreateGalaxy';
10import CreateGalaxy from '../views/CreateGalaxy';
12import GenKeyfile from '../views/GenKeyfile';
11import GenKeyfile from '../views/GenKeyfile';
13import IssueChild from '../views/IssueChild';
12import 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';
20import ViewPoint from '../views/ViewPoint';
13import ViewPoint from '../views/ViewPoint';
21import SentTransaction from '../views/SentTransaction';
14import SentTransaction from '../views/SentTransaction';
22import SetKeys from '../views/SetKeys';
15import SetKeys from '../views/SetKeys';
@@ -27,14 +20,9 @@ import {
27} from '../views/SetProxy';
20} from '../views/SetProxy';
28import Point from '../views/Point';
21import Point from '../views/Point';
29import Points from '../views/Points';
22import Points from '../views/Points';
30−import Shards from '../views/Shards';
31−import Ticket from '../views/Ticket';
32import Transfer from '../views/Transfer';
23import Transfer from '../views/Transfer';
33−import Wallet from '../views/Wallet';
34import Invite from 'views/Invite';
24import Invite from 'views/Invite';
35
25
36−import { EthereumWallet } from './wallet';
37−import { renderNetworkType } from './network';
38import { ROUTE_NAMES } from './routeNames';
26import { ROUTE_NAMES } from './routeNames';
39
27
40export const ROUTES = {
28export const ROUTES = {
@@ -46,16 +34,7 @@ export const ROUTES = {
46 [ROUTE_NAMES.LOGIN]: Login,
34 [ROUTE_NAMES.LOGIN]: Login,
47 [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
35 [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
48 [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
36 [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
49− [ROUTE_NAMES.NETWORK]: Network,
50− [ROUTE_NAMES.WALLET]: Wallet,
51 [ROUTE_NAMES.VIEW_POINT]: ViewPoint,
37 [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,
59 [ROUTE_NAMES.POINTS]: Points,
38 [ROUTE_NAMES.POINTS]: Points,
60 [ROUTE_NAMES.POINT]: Point,
39 [ROUTE_NAMES.POINT]: Point,
61 [ROUTE_NAMES.SET_MANAGEMENT_PROXY]: SetManagementProxy,
40 [ROUTE_NAMES.SET_MANAGEMENT_PROXY]: SetManagementProxy,
@@ -69,11 +48,7 @@ export const ROUTES = {
69 [ROUTE_NAMES.GEN_KEYFILE]: GenKeyfile,
48 [ROUTE_NAMES.GEN_KEYFILE]: GenKeyfile,
70};
49};
71
50
72−export const getRouteBreadcrumb = ({
51+export const getRouteBreadcrumb = ({ pointCursor }) => route => {
73− pointCursor,
74− networkType,
75− wallet,
76−}) => route => {
77 switch (route.name) {
52 switch (route.name) {
78 case ROUTE_NAMES.INVITE_TICKET:
53 case ROUTE_NAMES.INVITE_TICKET:
79 return 'Invite code';
54 return 'Invite code';
@@ -87,32 +62,6 @@ export const getRouteBreadcrumb = ({
87 return 'Login to Bridge';
62 return 'Login to Bridge';
88 case ROUTE_NAMES.POINT:
63 case ROUTE_NAMES.POINT:
89 return `Overview: ${ob.patp(need.pointCursor(pointCursor))}`;
64 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';
116 case ROUTE_NAMES.POINTS:
65 case ROUTE_NAMES.POINTS:
117 return 'Points';
66 return 'Points';
118 case ROUTE_NAMES.VIEW_POINT:
67 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';
4
4
5// sync a specified point to the cache
5// sync a specified point to the cache
6export default function useSyncPoint(point) {
6export default function useSyncPoint(point) {
7− const { fetchPoint } = usePointCache();
7+ const { syncOwnedPoint } = usePointCache();
8
8
9 useEffect(() => {
9 useEffect(() => {
10− fetchPoint(point);
10+ syncOwnedPoint(point);
11− }, [fetchPoint, point]);
11+ }, [syncOwnedPoint, point]);
12}
12}
src/lib/validators.js+36−13
@@ -3,6 +3,10 @@ import * as ob from 'urbit-ob';
3
3
4import { isValidAddress } from './wallet';
4import { isValidAddress } from './wallet';
5
5
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?
6const hexRegExp = /[0-9A-Fa-f]{64}/g;
10const hexRegExp = /[0-9A-Fa-f]{64}/g;
7
11
8// Wraps single validation functions in a controlled and predictable way.
12// Wraps single validation functions in a controlled and predictable way.
@@ -18,7 +22,7 @@ export const simpleValidatorWrapper = config => {
18 // Run the validator and return the result.
22 // Run the validator and return the result.
19 return config.validator(config.prevMessage.data)
23 return config.validator(config.prevMessage.data)
20 ? newMessage(config.prevMessage.data, true, null)
24 ? newMessage(config.prevMessage.data, true, null)
21− : newMessage(config.prevMessage.data, false, config.errorMessage);
25+ : newMessage(config.prevMessage.data, false, config.error);
22};
26};
23
27
24// Validation message
28// Validation message
@@ -45,15 +49,21 @@ export const validateMnemonic = m =>
45 simpleValidatorWrapper({
49 simpleValidatorWrapper({
46 prevMessage: m,
50 prevMessage: m,
47 validator: d => bip39.validateMnemonic(d),
51 validator: d => bip39.validateMnemonic(d),
48− errorMessage: 'This is not a valid mnemonic.',
52+ error: 'This is not a valid mnemonic.',
49 });
53 });
50
54
51// Checks an empty field
55// Checks an empty field
52export const validateNotEmpty = m =>
56export const validateNotEmpty = m =>
53 simpleValidatorWrapper({
57 simpleValidatorWrapper({
54 prevMessage: m,
58 prevMessage: m,
55− validator: d => d.length > 1,
59+ validator: d => {
60+ try {
61+ return d.length > 1;
62+ } catch {
63+ return false;
64+ }
65+ },
56− errorMessage: 'This field is required.',
66+ error: 'This field is required.',
57 });
67 });
58
68
59// Checks if a patp is a valid galaxy
69// Checks if a patp is a valid galaxy
@@ -69,7 +79,7 @@ export const validateGalaxy = m =>
69 return false;
79 return false;
70 }
80 }
71 },
81 },
72− errorMessage: 'This is not a valid galaxy',
82+ error: 'This is not a valid galaxy',
73 });
83 });
74
84
75export const validatePoint = m =>
85export const validatePoint = m =>
@@ -82,7 +92,7 @@ export const validatePoint = m =>
82 return false;
92 return false;
83 }
93 }
84 },
94 },
85− errorMessage: 'This is not a valid point',
95+ error: 'This is not a valid point',
86 });
96 });
87
97
88export const validateTicket = m =>
98export const validateTicket = m =>
@@ -95,7 +105,7 @@ export const validateTicket = m =>
95 return false;
105 return false;
96 }
106 }
97 },
107 },
98− errorMessage: 'This is not a valid ticket',
108+ error: 'This is not a valid ticket',
99 });
109 });
100
110
101export const validateShard = m =>
111export const validateShard = m =>
@@ -108,28 +118,34 @@ export const validateShard = m =>
108 return false;
118 return false;
109 }
119 }
110 },
120 },
111− errorMessage: 'This is not a valid shard',
121+ error: 'This is not a valid shard',
112 });
122 });
113
123
114export const validateLength = (m, l) =>
124export const validateLength = (m, l) =>
115 simpleValidatorWrapper({
125 simpleValidatorWrapper({
116 prevMessage: m,
126 prevMessage: m,
117− validator: d => d.length === l,
127+ validator: d => {
128+ try {
129+ return d.length === l;
130+ } catch {
131+ return false;
132+ }
133+ },
118− errorMessage: 'This is of an invalid length',
134+ error: 'This is of an invalid length',
119 });
135 });
120
136
121export const validateNetworkKey = m =>
137export const validateNetworkKey = m =>
122 simpleValidatorWrapper({
138 simpleValidatorWrapper({
123 prevMessage: m,
139 prevMessage: m,
124 validator: d => hexRegExp.test(d),
140 validator: d => hexRegExp.test(d),
125− errorMessage: 'This is not a valid network key',
141+ error: 'This is not a valid network key',
126 });
142 });
127
143
128export const validateNetworkSeed = m =>
144export const validateNetworkSeed = m =>
129 simpleValidatorWrapper({
145 simpleValidatorWrapper({
130 prevMessage: m,
146 prevMessage: m,
131 validator: d => hexRegExp.test(d),
147 validator: d => hexRegExp.test(d),
132− errorMessage: 'This is not a valid network seed',
148+ error: 'This is not a valid network seed',
133 });
149 });
134
150
135// Checks if a string is a valid ethereum address
151// Checks if a string is a valid ethereum address
@@ -137,5 +153,12 @@ export const validateEthereumAddress = m =>
137 simpleValidatorWrapper({
153 simpleValidatorWrapper({
138 prevMessage: m,
154 prevMessage: m,
139 validator: d => isValidAddress(d),
155 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',
141 });
164 });
src/_walletgen/lib/lib.js → src/lib/walletgen.jsrenamed+1
@@ -2,6 +2,7 @@ import * as ob from 'urbit-ob';
2import * as kg from 'urbit-key-generation/dist';
2import * as kg from 'urbit-key-generation/dist';
3import * as more from 'more-entropy';
3import * as more from 'more-entropy';
4import { chunk, flatMap, zipWith } from 'lodash';
4import { chunk, flatMap, zipWith } from 'lodash';
5+
5import {
6import {
6 MIN_STAR,
7 MIN_STAR,
7 MIN_PLANET,
8 MIN_PLANET,
src/store/pointCache.js+80−27
@@ -19,6 +19,8 @@ const kEmptyInvites = {
19 acceptedInvites: Maybe.Nothing(),
19 acceptedInvites: Maybe.Nothing(),
20};
20};
21
21
22+// TODO(shrugs): this hook is too big -> refactor into individual hooks for
23+// details, birthday, invites, etc
22function _usePointCache() {
24function _usePointCache() {
23 const { contracts, web3 } = useNetwork();
25 const { contracts, web3 } = useNetwork();
24 const [pointCache, _setPointCache] = useState({});
26 const [pointCache, _setPointCache] = useState({});
@@ -41,6 +43,10 @@ function _usePointCache() {
41 );
43 );
42
44
43 // TODO: refactor pointCache access to use accessor like bithday
45 // TODO: refactor pointCache access to use accessor like bithday
46+ const getDetails = useCallback(
47+ point => pointCache[point] || Maybe.Nothing(),
48+ [pointCache]
49+ );
44 const getBirthday = useCallback(
50 const getBirthday = useCallback(
45 point => birthdayCache[point] || Maybe.Nothing(),
51 point => birthdayCache[point] || Maybe.Nothing(),
46 [birthdayCache]
52 [birthdayCache]
@@ -50,33 +56,31 @@ function _usePointCache() {
50 [invitesCache]
56 [invitesCache]
51 );
57 );
52
58
53− const fetchPoint = useCallback(
59+ const syncDetails = useCallback(
54 async point => {
60 async point => {
55 const _contracts = contracts.getOrElse(null);
61 const _contracts = contracts.getOrElse(null);
56− const _web3 = web3.getOrElse(null);
57− if (!_contracts || !_web3) {
62+ if (!_contracts) {
58 return;
63 return;
59 }
64 }
60
65
61 // fetch point details
66 // fetch point details
62 const details = await azimuth.azimuth.getPoint(_contracts, point);
67 const details = await azimuth.azimuth.getPoint(_contracts, point);
63− addToPointCache({ [point]: details });
68+ addToPointCache({
64−
69+ [point]: details,
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− },
77 });
70 });
71+ },
72+ [contracts, addToPointCache]
73+ );
78
74
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+
79− // fetch birthday (if not already known — will not change after being set)
83+ // fetch birthday if not already known—will not change after being set
80 if (Maybe.Nothing.hasInstance(getBirthday(point))) {
84 if (Maybe.Nothing.hasInstance(getBirthday(point))) {
81 const birthBlock = await azimuth.azimuth.getActivationBlock(
85 const birthBlock = await azimuth.azimuth.getActivationBlock(
82 _contracts,
86 _contracts,
@@ -91,22 +95,71 @@ function _usePointCache() {
91 }
95 }
92 }
96 }
93 },
97 },
94− [
95− contracts,
96− web3,
97− addToPointCache,
98− addToBirthdayCache,
98+ [contracts, web3, addToBirthdayCache, getBirthday]
99− 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+ },
100− addToInvitesCache,
141+ [contracts, addToInvitesCache]
101− ]
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]
102 );
154 );
103
155
104 return {
156 return {
105 pointCache,
157 pointCache,
158+ getDetails,
106 getBirthday,
159 getBirthday,
107 getInvites,
160 getInvites,
108 addToPointCache,
161 addToPointCache,
109− fetchPoint,
162+ syncOwnedPoint,
110 };
163 };
111}
164}
112
165
src/style/indigo-proposals.scss+4
@@ -27,3 +27,7 @@
27.minh-100 {
27.minh-100 {
28 min-height: 100vh;
28 min-height: 100vh;
29}
29}
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';
4
4
5import { usePointCursor } from 'store/pointCursor';
5import { usePointCursor } from 'store/pointCursor';
6import { useHistory } from 'store/history';
6import { useHistory } from 'store/history';
7−// import { usePointCache } from 'store/pointCache';
8
7
9import View from 'components/View';
8import View from 'components/View';
10import Crumbs from 'components/Crumbs';
9import Crumbs from 'components/Crumbs';
@@ -14,6 +13,7 @@ import useSyncPoint from 'lib/useSyncPoint';
14import useCurrentPointName from 'lib/useCurrentPointName';
13import useCurrentPointName from 'lib/useCurrentPointName';
15import useRouter from 'lib/useRouter';
14import useRouter from 'lib/useRouter';
16import loadingCharacter from 'lib/loadingCharacter';
15import loadingCharacter from 'lib/loadingCharacter';
16+import { LocalRouterProvider } from 'lib/LocalRouter';
17
17
18import InviteUrl from './Invite/InviteUrl';
18import InviteUrl from './Invite/InviteUrl';
19import InviteEmail from './Invite/InviteEmail';
19import InviteEmail from './Invite/InviteEmail';
@@ -31,7 +31,7 @@ const kInviteViews = {
31
31
32export default function Invite() {
32export default function Invite() {
33 const history = useHistory();
33 const history = useHistory();
34− const { Route, ...routeProps } = useRouter({
34+ const { Route, ...router } = useRouter({
35 names: kInviteNames,
35 names: kInviteNames,
36 views: kInviteViews,
36 views: kInviteViews,
37 initialRoutes: [{ key: kInviteNames.URL }],
37 initialRoutes: [{ key: kInviteNames.URL }],
@@ -40,45 +40,48 @@ export default function Invite() {
40 const point = need.pointCursor(pointCursor);
40 const point = need.pointCursor(pointCursor);
41 const name = useCurrentPointName();
41 const name = useCurrentPointName();
42
42
43− // sync the current cursor
43+ // sync the current point
44 useSyncPoint(point);
44 useSyncPoint(point);
45
45
46− // fetch the invites for the current cursor
46+ // get the invites for the current point
47 const { availableInvites, sentInvites, acceptedInvites } = useInvites(point);
47 const { availableInvites, sentInvites, acceptedInvites } = useInvites(point);
48+
49+ // number or loading character
48 const availableInvitesText = loadingCharacter(availableInvites);
50 const availableInvitesText = loadingCharacter(availableInvites);
49 const sentInvitesText = loadingCharacter(sentInvites);
51 const sentInvitesText = loadingCharacter(sentInvites);
50 const acceptedInvitesText = loadingCharacter(acceptedInvites);
52 const acceptedInvitesText = loadingCharacter(acceptedInvites);
51
53
52 return (
54 return (
53− <View>
55+ <LocalRouterProvider value={router}>
56+ <View>
54− <Grid className="mb3">
57+ <Grid className="mb4">
55− <Grid.Item
58+ <Grid.Item
56− as={Crumbs}
59+ as={Crumbs}
57− routes={[
60+ routes={[
58− {
61+ {
59− text: name,
62+ text: name,
60− action: () => history.pop(),
63+ action: () => history.pop(),
61− },
64+ },
62− {
65+ {
63− text: 'Invite',
66+ text: 'Invite',
64− },
67+ },
65− ]}
68+ ]}
66− full
69+ full
67− />
70+ />
68− <Grid.Item as={H5} className="mv4" full>
71+ <Grid.Item as={H5} className="mv4" full>
69− Invite
72+ Invite
70− </Grid.Item>
73+ </Grid.Item>
71− <Grid.Item as={Text} full>
74+ <Grid.Item as={Text} full>
72− You currently have{' '}
75+ You currently have{' '}
73− <Highlighted>{availableInvitesText} invitations</Highlighted> left.
76+ <Highlighted>{availableInvitesText} invitations</Highlighted> left.
74− <br />
77+ <br />
75− Out of the <Highlighted>{sentInvitesText}</Highlighted> invites you
78+ Out of the <Highlighted>{sentInvitesText}</Highlighted> invites you
76− sent, <Highlighted>{acceptedInvitesText}</Highlighted> have been
79+ sent, <Highlighted>{acceptedInvitesText}</Highlighted> have been
77− accepted.
80+ accepted.
78− </Grid.Item>
81+ </Grid.Item>
79− </Grid>
82+ </Grid>
80− {/* TODO(shrugs): use context isntead of this routeProps nonsense */}
81− <Route {...routeProps} />
83+ <Route />
82− </View>
84+ </View>
85+ </LocalRouterProvider>
83 );
86 );
84}
87}
src/views/Invite/InviteEmail.js+99−69
@@ -1,13 +1,13 @@
1−import { Just, Nothing } from 'folktale/maybe';
1+import Maybe, { Just, Nothing } from 'folktale/maybe';
2−import React from 'react';
2+import React, { useState } from 'react';
3import * as azimuth from 'azimuth-js';
3import * as azimuth from 'azimuth-js';
4−import * as need from 'lib/need';
4+import { Grid, Flex, Button, Input, IconButton } from 'indigo-react';
5
5
6+import * as need from 'lib/need';
6import { hasReceived, sendMail } from 'lib/inviteMail';
7import { hasReceived, sendMail } from 'lib/inviteMail';
7−import { Button, Input, H3, Warning } from 'components/old/Base';
8+import * as wg from 'lib/walletgen';
8
9
9−// for wallet generation
10+import { H3, Warning } from 'components/old/Base';
10−import * as wg from '_walletgen/lib/lib';
11
11
12// for transaction generation and signing
12// for transaction generation and signing
13import {
13import {
@@ -19,11 +19,17 @@ import {
19 toWei,
19 toWei,
20} from 'lib/txn';
20} from 'lib/txn';
21import * as tank from 'lib/tank';
21import * as tank from 'lib/tank';
22−import { withNetwork } from 'store/network';
22+import { useLocalRouter } from 'lib/LocalRouter';
23−import { compose } from 'lib/lib';
23+
24−import { withWallet } from 'store/wallet';
25−import { withPointCursor } from 'store/pointCursor';
26import View from 'components/View';
24import 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';
27
33
28const GAS_PRICE_GWEI = 20; // we pay the premium for faster ux
34const GAS_PRICE_GWEI = 20; // we pay the premium for faster ux
29const GAS_LIMIT = 350000;
35const GAS_LIMIT = 350000;
@@ -45,13 +51,90 @@ const EMAIL_STATUS = {
45 FAIL: '×',
51 FAIL: '×',
46};
52};
47
53
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+
48class InviteEmail extends React.Component {
132class InviteEmail extends React.Component {
49 constructor(props) {
133 constructor(props) {
50 super(props);
134 super(props);
51
135
52 this.state = {
136 this.state = {
53 invitesAvailable: Nothing(),
137 invitesAvailable: Nothing(),
54− invited: Nothing(),
55 targets: [{ email: '', hasReceived: Nothing(), status: Nothing() }],
138 targets: [{ email: '', hasReceived: Nothing(), status: Nothing() }],
56 status: STATUS.INPUT,
139 status: STATUS.INPUT,
57 errors: Nothing(),
140 errors: Nothing(),
@@ -60,7 +143,6 @@ class InviteEmail extends React.Component {
60 wipInvites: [],
143 wipInvites: [],
61 };
144 };
62
145
63− this.findInvited = this.findInvited.bind(this);
64 this.generateInvites = this.generateInvites.bind(this);
146 this.generateInvites = this.generateInvites.bind(this);
65 this.sendInvites = this.sendInvites.bind(this);
147 this.sendInvites = this.sendInvites.bind(this);
66 this.canSend = this.canSend.bind(this);
148 this.canSend = this.canSend.bind(this);
@@ -78,33 +160,11 @@ class InviteEmail extends React.Component {
78 this.contracts = need.contracts(this.props.contracts);
160 this.contracts = need.contracts(this.props.contracts);
79 this.web3 = need.web3(this.props.web3);
161 this.web3 = need.web3(this.props.web3);
80
162
81− console.log(azimuth.delegatedSending);
82 azimuth.delegatedSending
163 azimuth.delegatedSending
83 .getTotalUsableInvites(this.contracts, this.point)
164 .getTotalUsableInvites(this.contracts, this.point)
84 .then(count => {
165 .then(count => {
85 this.setState({ invitesAvailable: Just(count) });
166 this.setState({ invitesAvailable: Just(count) });
86 });
167 });
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 }) });
108 }
168 }
109
169
110 async getTemporaryWallet() {
170 async getTemporaryWallet() {
@@ -347,30 +407,6 @@ class InviteEmail extends React.Component {
347 const dif =
407 const dif =
348 this.state.status === STATUS.DONE ? this.state.targets.length : null;
408 this.state.status === STATUS.DONE ? this.state.targets.length : null;
349
409
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−
374 const error = this.state.errors.matchWith({
410 const error = this.state.errors.matchWith({
375 Nothing: () => null,
411 Nothing: () => null,
376 Just: errors => (
412 Just: errors => (
@@ -478,12 +514,6 @@ class InviteEmail extends React.Component {
478
514
479 return (
515 return (
480 <View>
516 <View>
481− <p>{'send invites here, for planets'}</p>
482−
483− <p>{invitesAvailable}</p>
484−
485− <ul>{invitesSent}</ul>
486−
487 {error}
517 {error}
488
518
489 {fundingMessage}
519 {fundingMessage}
@@ -494,8 +524,8 @@ class InviteEmail extends React.Component {
494 }
524 }
495}
525}
496
526
497−export default compose(
527+// export default compose(
498− withNetwork,
528+// withNetwork,
499− withWallet,
529+// withWallet,
500− withPointCursor
530+// withPointCursor
501−)(InviteEmail);
531+// )(InviteEmail);
src/views/Invite/InviteUrl.js+21−3
@@ -1,10 +1,25 @@
1import React, { useCallback } from 'react';
1import React, { useCallback } from 'react';
2import { Grid, Button } from 'indigo-react';
2import { Grid, Button } from 'indigo-react';
3
3
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+
4import { ForwardButton } from 'components/Buttons';
10import { ForwardButton } from 'components/Buttons';
5import FooterButton from 'components/FooterButton';
11import FooterButton from 'components/FooterButton';
6
12
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+
8 const pushEmail = useCallback(() => push(names.EMAIL), [push, names]);
23 const pushEmail = useCallback(() => push(names.EMAIL), [push, names]);
9
24
10 return (
25 return (
@@ -15,7 +30,7 @@ export default function InviteUrl({ names, push }) {
15 as={Button}
30 as={Button}
16 detail="Generate a URL to send to one guest"
31 detail="Generate a URL to send to one guest"
17 full
32 full
18− disabled>
33+ disabled={!canInvite || true}>
19 Generate Invite
34 Generate Invite
20 </Grid.Item>
35 </Grid.Item>
21 <Grid.Divider />
36 <Grid.Divider />
@@ -23,11 +38,14 @@ export default function InviteUrl({ names, push }) {
23 as={ForwardButton}
38 as={ForwardButton}
24 onClick={pushEmail}
39 onClick={pushEmail}
25 detail="Send invitation to your guest's email"
40 detail="Send invitation to your guest's email"
41+ disabled={!canInvite}
26 full>
42 full>
27 Send Invite via Email
43 Send Invite via Email
28 </Grid.Item>
44 </Grid.Item>
29 </Grid>
45 </Grid>
46+ <FooterButton
30− <FooterButton detail="i.e. Ethereum Address, Text File" disabled>
47+ detail="i.e. Ethereum Address, Text File"
48+ disabled={!canInvite || true}>
31 Advanced
49 Advanced
32 </FooterButton>
50 </FooterButton>
33 </>
51 </>
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) {
33 Login
33 Login
34 </ForwardButton>
34 </ForwardButton>
35
35
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−
43 <ForwardButton
36 <ForwardButton
44 className="mt3"
37 className="mt3"
45 detail="View an Azimuth point without signing into a wallet."
38 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';
4import * as keythereum from 'keythereum';
4import * as keythereum from 'keythereum';
5
5
6import View from 'components/View';
6import View from 'components/View';
7−import { PassphraseInput } from 'components/Inputs';
7+import { usePassphraseInput } from 'components/Inputs';
8import { ForwardButton } from 'components/Buttons';
8import { ForwardButton } from 'components/Buttons';
9−import { H3 } from 'indigo-react';
9+import { H3, Input } from 'indigo-react';
10import { InputCaption, UploadButton, Warning } from 'components/old/Base';
10import { InputCaption, UploadButton, Warning } from 'components/old/Base';
11
11
12import * as need from 'lib/need';
12import * as need from 'lib/need';
@@ -20,7 +20,13 @@ export default function Keystore({ loginCompleted }) {
20 // inputs
20 // inputs
21 // keystore: Maybe<Result<String, String>>
21 // keystore: Maybe<Result<String, String>>
22 const [keystore, setKeystore] = useState(Nothing());
22 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+
24 const [decryptionProblem, setDecryptionProblem] = useState(false);
30 const [decryptionProblem, setDecryptionProblem] = useState(false);
25
31
26 const constructWallet = () => {
32 const constructWallet = () => {
@@ -28,7 +34,7 @@ export default function Keystore({ loginCompleted }) {
28 const text = need.keystore(keystore);
34 const text = need.keystore(keystore);
29
35
30 const json = JSON.parse(text);
36 const json = JSON.parse(text);
31− const privateKey = keythereum.recover(password, json);
37+ const privateKey = keythereum.recover(passphrase, json);
32
38
33 const newWallet = new EthereumWallet(privateKey);
39 const newWallet = new EthereumWallet(privateKey);
34 setDecryptionProblem(false);
40 setDecryptionProblem(false);
@@ -95,12 +101,7 @@ export default function Keystore({ loginCompleted }) {
95 </div>
101 </div>
96 </UploadButton>
102 </UploadButton>
97
103
98− <PassphraseInput
104+ <Input {...passphraseInput} />
99− name="password"
100− label="Keystore password"
101− initialValue={password}
102− onValue={setPassword}
103− autoFocus></PassphraseInput>
104
105
105 <ForwardButton
106 <ForwardButton
106 className="mt3"
107 className="mt3"
src/views/Login/Ledger.js+1−1
@@ -9,7 +9,7 @@ import * as secp256k1 from 'secp256k1';
9import View from 'components/View';
9import View from 'components/View';
10import { ForwardButton } from 'components/Buttons';
10import { ForwardButton } from 'components/Buttons';
11import { H1, H2, P, Input } from 'indigo-react';
11import { H1, H2, P, Input } from 'indigo-react';
12−import { InnerLabel, InnerLabelDropdown } from 'components/old/Base';
12+import { InnerLabelDropdown } from 'components/old/Base';
13
13
14import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from 'lib/ledger';
14import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from 'lib/ledger';
15import { useWallet } from 'store/wallet';
15import { 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';
2import { Just, Nothing } from 'folktale/maybe';
2import { Just, Nothing } from 'folktale/maybe';
3
3
4import View from 'components/View';
4import View from 'components/View';
5+import {
6+ usePassphraseInput,
7+ useMnemonicInput,
8+ useHdPathInput,
5−import { MnemonicInput, PassphraseInput, HdPathInput } from 'components/Inputs';
9+} from 'components/Inputs';
6import { ForwardButton } from 'components/Buttons';
10import { ForwardButton } from 'components/Buttons';
7
11
8import { walletFromMnemonic } from 'lib/wallet';
12import { walletFromMnemonic } from 'lib/wallet';
9import { useWallet } from 'store/wallet';
13import { useWallet } from 'store/wallet';
14+import { Input } from 'indigo-react';
10
15
11export default function Mnemonic({ advanced, loginCompleted }) {
16export default function Mnemonic({ advanced, loginCompleted }) {
12 const {
17 const {
@@ -17,9 +22,8 @@ export default function Mnemonic({ advanced, loginCompleted }) {
17 walletHdPath,
22 walletHdPath,
18 setWalletHdPath,
23 setWalletHdPath,
19 } = useWallet();
24 } = useWallet();
20− const [passphrase, setPassphrase] = useState('');
21− const mnemonic = authMnemonic.getOrElse('');
22
25
26+ const mnemonic = authMnemonic.getOrElse('');
23 // TODO: move this into transformers?
27 // TODO: move this into transformers?
24 // transform the result of the mnemonic to Maybe<string>
28 // transform the result of the mnemonic to Maybe<string>
25 const _setAuthMnemonic = useCallback(
29 const _setAuthMnemonic = useCallback(
@@ -27,6 +31,27 @@ export default function Mnemonic({ advanced, loginCompleted }) {
27 [setAuthMnemonic]
31 [setAuthMnemonic]
28 );
32 );
29
33
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+
30 // when the properties change, re-derive wallet
55 // when the properties change, re-derive wallet
31 useEffect(() => {
56 useEffect(() => {
32 let mounted = true;
57 let mounted = true;
@@ -39,29 +64,13 @@ export default function Mnemonic({ advanced, loginCompleted }) {
39
64
40 return (
65 return (
41 <View>
66 <View>
42− <MnemonicInput
67+ <Input {...mnemonicInput} />
43− name="mnemonic"
44− label="BIP39 Mnemonic"
45− initialValue={mnemonic}
46− onValue={_setAuthMnemonic}
47− autoFocus
48− />
49
68
50 {!advanced ? null : (
69 {!advanced ? null : (
51 <>
70 <>
52− <PassphraseInput
71+ <Input {...passphraseInput} />
53− name="passphrase"
54− label="(Optional) Wallet Passphrase"
55− initialValue={passphrase}
56− onValue={setPassphrase}
57− />
58
72
59− <HdPathInput
73+ <Input {...hdPathInput} />
60− name="hdpath"
61− label="HD Path"
62− initialValue={walletHdPath}
63− onValue={setWalletHdPath}
64− />
65 </>
74 </>
66 )}
75 )}
67
76
src/views/Login/PrivateKey.js+12−19
@@ -1,24 +1,25 @@
1import { Just, Nothing } from 'folktale/maybe';
1import { Just, Nothing } from 'folktale/maybe';
2−import React, { useState } from 'react';
2+import React, { useEffect } from 'react';
3
3
4import View from 'components/View';
4import View from 'components/View';
5−import { PassphraseInput } from 'components/Inputs';
5+import { usePassphraseInput } from 'components/Inputs';
6import { ForwardButton } from 'components/Buttons';
6import { ForwardButton } from 'components/Buttons';
7
7
8import { EthereumWallet } from 'lib/wallet';
8import { EthereumWallet } from 'lib/wallet';
9import { useWallet } from 'store/wallet';
9import { useWallet } from 'store/wallet';
10+import { Input } from 'indigo-react';
10
11
11export default function PrivateKey({ loginCompleted }) {
12export default function PrivateKey({ loginCompleted }) {
12 const { wallet, setWallet } = useWallet();
13 const { wallet, setWallet } = useWallet();
13
14
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;
15
21
16− const handlePrivateKeyInput = privateKey => {
22+ useEffect(() => {
17− setPrivateKey(privateKey);
18− constructWallet(privateKey);
19− };
20−
21− const constructWallet = privateKey => {
22 if (/^[0-9A-Fa-f]{64}$/g.test(privateKey) === true) {
23 if (/^[0-9A-Fa-f]{64}$/g.test(privateKey) === true) {
23 const sec = Buffer.from(privateKey, 'hex');
24 const sec = Buffer.from(privateKey, 'hex');
24 const newWallet = new EthereumWallet(sec);
25 const newWallet = new EthereumWallet(sec);
@@ -26,20 +27,12 @@ export default function PrivateKey({ loginCompleted }) {
26 } else {
27 } else {
27 setWallet(Nothing());
28 setWallet(Nothing());
28 }
29 }
29− };
30+ }, [privateKey, setWallet]);
30
31
31 return (
32 return (
32 <View>
33 <View>
33 Please enter your raw Ethereum private key here.
34 Please enter your raw Ethereum private key here.
34− <PassphraseInput
35+ <Input {...privateKeyInput} />
35− type="password"
36− name="privateKey"
37− label="Private key"
38− initialValue={privateKey}
39− onValue={handlePrivateKeyInput}
40− autocomplete="off"
41− autoFocus
42− />
43 <ForwardButton
36 <ForwardButton
44 className="mt3"
37 className="mt3"
45 disabled={Nothing.hasInstance(wallet)}
38 disabled={Nothing.hasInstance(wallet)}
src/views/Login/Shards.js+39−35
@@ -1,22 +1,49 @@
1import { Just, Nothing } from 'folktale/maybe';
1import { Just, Nothing } from 'folktale/maybe';
2−import React, { useState } from 'react';
2+import React from 'react';
3import * as kg from 'urbit-key-generation/dist/index';
3import * as kg from 'urbit-key-generation/dist/index';
4
4
5import View from 'components/View';
5import View from 'components/View';
6+import {
7+ useTicketInput,
8+ usePassphraseInput,
9+ usePointInput,
6−import { PointInput, TicketInput, PassphraseInput } from 'components/Inputs';
10+} from 'components/Inputs';
7import { ForwardButton } from 'components/Buttons';
11import { ForwardButton } from 'components/Buttons';
8
12
9import { urbitWalletFromTicket } from 'lib/wallet';
13import { urbitWalletFromTicket } from 'lib/wallet';
10import { useWallet } from 'store/wallet';
14import { useWallet } from 'store/wallet';
15+import { Input } from 'indigo-react';
11
16
12export default function Shards({ loginCompleted }) {
17export default function Shards({ loginCompleted }) {
13 const { wallet, setUrbitWallet } = useWallet();
18 const { wallet, setUrbitWallet } = useWallet();
14
19
15− const [shard1, setShard1] = useState('');
20+ const pointInput = usePointInput({
16− const [shard2, setShard2] = useState('');
21+ name: 'point',
17− const [shard3, setShard3] = useState('');
22+ label: 'Point',
18− const [passphrase, setPassphrase] = useState('');
23+ autoFocus: true,
24+ });
19− const [pointName, setPointName] = useState('');
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;
20
47
21 const walletFromShards = async () => {
48 const walletFromShards = async () => {
22 const s1 = shard1 === '' ? undefined : shard1;
49 const s1 = shard1 === '' ? undefined : shard1;
@@ -43,35 +70,12 @@ export default function Shards({ loginCompleted }) {
43 <View>
70 <View>
44 Enter your point and at least two of your three Urbit master ticket shards
71 Enter your point and at least two of your three Urbit master ticket shards
45 here. The index of the input field needs to match the index of the shard.
72 here. The index of the input field needs to match the index of the shard.
46− <PointInput
73+ <Input {...pointInput} />
47− name="point"
74+ <Input {...shard1Input} />
48− label="Point"
75+ <Input {...shard2Input} />
49− initialValue={pointName}
76+ <Input {...shard3Input} />
50− onValue={setPointName}
51− autoFocus
52− />
53− <TicketInput
54− name="shard1"
55− label="Shard 1"
56− initialValue={shard1}
57− onValue={setShard1}></TicketInput>
58− <TicketInput
59− name="shard2"
60− label="Shard 2"
61− initialValue={shard2}
62− onValue={setShard2}></TicketInput>
63− <TicketInput
64− name="shard3"
65− label="Shard 3"
66− initialValue={shard3}
67− onValue={setShard3}></TicketInput>
68 If your wallet requires a passphrase, you may enter it below.
77 If your wallet requires a passphrase, you may enter it below.
69− <PassphraseInput
78+ <Input {...passphraseInput} />
70− name="passphrase"
71− label="(Optional) Wallet Passphrase"
72− initialValue={passphrase}
73− onValue={setPassphrase}
74− />
75 <ForwardButton
79 <ForwardButton
76 className="mt3"
80 className="mt3"
77 disabled={!ready}
81 disabled={!ready}
src/views/Login/Ticket.js+63−68
@@ -1,10 +1,14 @@
1import { Just, Nothing } from 'folktale/maybe';
1import { Just, Nothing } from 'folktale/maybe';
2−import React, { useState, useEffect } from 'react';
2+import React, { useCallback, useState, useEffect } from 'react';
3import * as azimuth from 'azimuth-js';
3import * as azimuth from 'azimuth-js';
4import * as ob from 'urbit-ob';
4import * as ob from 'urbit-ob';
5
5
6import View from 'components/View';
6import View from 'components/View';
7+import {
8+ usePointInput,
9+ useTicketInput,
10+ usePassphraseInput,
7−import { PointInput, TicketInput, PassphraseInput } from 'components/Inputs';
11+} from 'components/Inputs';
8import { ForwardButton } from 'components/Buttons';
12import { ForwardButton } from 'components/Buttons';
9
13
10import * as need from 'lib/need';
14import * as need from 'lib/need';
@@ -13,6 +17,7 @@ import { WALLET_TYPES, urbitWalletFromTicket } from 'lib/wallet';
13import { useNetwork } from 'store/network';
17import { useNetwork } from 'store/network';
14import { useWallet } from 'store/wallet';
18import { useWallet } from 'store/wallet';
15import { usePointCursor } from 'store/pointCursor';
19import { usePointCursor } from 'store/pointCursor';
20+import { Input } from 'indigo-react';
16
21
17//TODO should be part of InputWithStatus component
22//TODO should be part of InputWithStatus component
18const INPUT_STATUS = {
23const INPUT_STATUS = {
@@ -28,47 +33,65 @@ export default function Ticket({ advanced, loginCompleted }) {
28 const { setPointCursor } = usePointCursor();
33 const { setPointCursor } = usePointCursor();
29
34
30 // inputs
35 // inputs
36+ const pointInput = usePointInput({
37+ name: 'point',
38+ label: 'Point',
31− //TODO deduce point name from URL if we can, prefill input if we found it
39+ //TODO deduce point name from URL if we can, prefill input if we found it
40+ // initialValue: '~',
41+ autoFocus: true,
42+ });
32− const [pointName, setPointName] = useState('');
43+ const pointName = pointInput.data;
44+
45+ const ticketInput = useTicketInput({
46+ name: 'ticket',
47+ label: 'Master ticket',
48+ });
33− const [ticket, setTicket] = useState('');
49+ const ticket = ticketInput.data;
50+
51+ const passphraseInput = usePassphraseInput({
52+ name: 'passphrase',
53+ label: '(Optional) Wallet Passphrase',
54+ });
34− const [passphrase, setPassphrase] = useState('');
55+ const passphrase = passphraseInput.data;
35
56
36 // display state
57 // display state
37 const [ticketStatus, setTicketStatus] = useState(Nothing());
58 const [ticketStatus, setTicketStatus] = useState(Nothing());
38
59
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+
39 useEffect(() => {
92 useEffect(() => {
40 verifyTicket(pointName, ticket, passphrase);
93 verifyTicket(pointName, ticket, passphrase);
41− return () => {};
94+ }, [verifyTicket, pointName, ticket, passphrase]);
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− };
72
95
73 const canContinue = () => {
96 const canContinue = () => {
74 // this is our only requirement, since we still want people with
97 // this is our only requirement, since we still want people with
@@ -82,16 +105,6 @@ export default function Ticket({ advanced, loginCompleted }) {
82 loginCompleted();
105 loginCompleted();
83 };
106 };
84
107
85− const pointInput = (
86− <PointInput
87− name="point"
88− label="Point"
89− initialValue={pointName}
90− onValue={setPointName}
91− autoFocus
92− />
93− );
94−
95 //TODO integrate into TicketInput?
108 //TODO integrate into TicketInput?
96 const displayTicketStatus = ticketStatus.matchWith({
109 const displayTicketStatus = ticketStatus.matchWith({
97 Nothing: () => <span />,
110 Nothing: () => <span />,
@@ -109,29 +122,11 @@ export default function Ticket({ advanced, loginCompleted }) {
109 },
122 },
110 });
123 });
111
124
112− const ticketInput = (
113− <TicketInput
114− name="ticket"
115− label="Master ticket"
116− initialValue={ticket}
117− onValue={setTicket}
118− />
119− );
120−
121− const passphraseInput = !advanced ? null : (
122− <PassphraseInput
123− name="passphrase"
124− label="(Optional) Wallet Passphrase"
125− initialValue={passphrase}
126− onValue={setPassphrase}
127− />
128− );
129−
130 return (
125 return (
131 <View>
126 <View>
132− {pointInput}
127+ <Input {...pointInput} />
133− {ticketInput}
128+ <Input {...ticketInput} accessory={displayTicketStatus} />
134− {passphraseInput}
129+ {advanced && <Input {...passphraseInput} />}
135
130
136 <ForwardButton
131 <ForwardButton
137 className={'mt3'}
132 className={'mt3'}
src/views/Login/Trezor.js+1−1
@@ -8,7 +8,7 @@ import * as secp256k1 from 'secp256k1';
8import View from 'components/View';
8import View from 'components/View';
9import { ForwardButton } from 'components/Buttons';
9import { ForwardButton } from 'components/Buttons';
10import { H1, P, Input } from 'indigo-react';
10import { H1, P, Input } from 'indigo-react';
11−import { InnerLabel, InnerLabelDropdown } from 'components/old/Base';
11+import { InnerLabelDropdown } from 'components/old/Base';
12
12
13import { TREZOR_PATH } from 'lib/trezor';
13import { TREZOR_PATH } from 'lib/trezor';
14import { useWallet } from 'store/wallet';
14import { 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';
3import { Grid } from 'indigo-react';
3import { Grid } from 'indigo-react';
4
4
5import { usePointCursor } from 'store/pointCursor';
5import { usePointCursor } from 'store/pointCursor';
6−// import { usePointCache } from 'store/pointCache';
7
6
8import View from 'components/View';
7import View from 'components/View';
9import Passport from 'components/Passport';
8import Passport from 'components/Passport';
@@ -15,6 +14,8 @@ import { ROUTE_NAMES } from 'lib/routeNames';
15
14
16import { useHistory } from 'store/history';
15import { useHistory } from 'store/history';
17import FooterButton from 'components/FooterButton';
16import FooterButton from 'components/FooterButton';
17+import loadingCharacter from 'lib/loadingCharacter';
18+import Actions from './Point/Actions';
18
19
19export default function Point() {
20export default function Point() {
20 const history = useHistory();
21 const history = useHistory();
@@ -25,6 +26,7 @@ export default function Point() {
25
26
26 // fetch the invites for the current cursor
27 // fetch the invites for the current cursor
27 const { availableInvites } = useInvites(point);
28 const { availableInvites } = useInvites(point);
29+ const availableInvitesText = loadingCharacter(availableInvites);
28
30
29 const goInvite = useCallback(() => history.push(ROUTE_NAMES.INVITE), [
31 const goInvite = useCallback(() => history.push(ROUTE_NAMES.INVITE), [
30 history,
32 history,
@@ -33,8 +35,6 @@ export default function Point() {
33 // sync the current cursor
35 // sync the current cursor
34 useSyncPoint(point);
36 useSyncPoint(point);
35
37
36− // const pointDetails = need.fromPointCache(pointCache, point);
37−
38 return (
38 return (
39 <View>
39 <View>
40 <Passport point={point} />
40 <Passport point={point} />
@@ -48,14 +48,13 @@ export default function Point() {
48 Boot Arvo
48 Boot Arvo
49 </ForwardButton>
49 </ForwardButton>
50 </Grid.Item>
50 </Grid.Item>
51+ {/* <Grid.Item full>
52+ <Actions />
53+ </Grid.Item> */}
51 </Grid>
54 </Grid>
52
55
53 <FooterButton onClick={goInvite}>
56 <FooterButton onClick={goInvite}>
54− Invite
57+ Invite <sup>{availableInvitesText}</sup>
55− {availableInvites.matchWith({
56− Nothing: () => null,
57− Just: p => <sup className="ml1">{p.value}</sup>,
58− })}
59 </FooterButton>
58 </FooterButton>
60 </View>
59 </View>
61 );
60 );
src/views/Point/Actions.js+25−48
@@ -1,4 +1,5 @@
1import React from 'react';
1import React from 'react';
2+import Maybe from 'folktale/maybe';
2import { azimuth } from 'azimuth-js';
3import { azimuth } from 'azimuth-js';
3import * as need from '../../lib/need';
4import * as need from '../../lib/need';
4
5
@@ -9,20 +10,25 @@ import { ROUTE_NAMES } from '../../lib/routeNames';
9import { useHistory } from '../../store/history';
10import { useHistory } from '../../store/history';
10import { useWallet } from '../../store/wallet';
11import { useWallet } from '../../store/wallet';
11import { Grid } from 'indigo-react';
12import { Grid } from 'indigo-react';
13+import { usePointCursor } from 'store/pointCursor';
14+import { usePointCache } from 'store/pointCache';
12
15
13const isPlanet = point =>
16const isPlanet = point =>
14 azimuth.getPointSize(point) === azimuth.PointSize.Planet;
17 azimuth.getPointSize(point) === azimuth.PointSize.Planet;
15−const isStar = point => azimuth.getPointSize(point) === azimuth.PointSize.Star;
16
18
17function Actions(props) {
19function Actions(props) {
18 const history = useHistory();
20 const history = useHistory();
19 const { wallet } = useWallet();
21 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+ );
21
28
22 const addr = need.addressFromWallet(wallet);
29 const addr = need.addressFromWallet(wallet);
23
30
24 const planet = isPlanet(point);
31 const planet = isPlanet(point);
25− const star = isStar(point);
26
32
27 const isOwner = pointDetails.matchWith({
33 const isOwner = pointDetails.matchWith({
28 Nothing: _ => false,
34 Nothing: _ => false,
@@ -57,11 +63,6 @@ function Actions(props) {
57 },
63 },
58 });
64 });
59
65
60− const hasInvites = invites.matchWith({
61− Nothing: () => false,
62− Just: count => count.value > 0,
63− });
64−
65 const canTransfer = pointDetails.matchWith({
66 const canTransfer = pointDetails.matchWith({
66 Nothing: () => false,
67 Nothing: () => false,
67 Just: deets =>
68 Just: deets =>
@@ -104,37 +105,6 @@ function Actions(props) {
104 },
105 },
105 });
106 });
106
107
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−
138 return (
108 return (
139 <div>
109 <div>
140 <H2>{'Actions'}</H2>
110 <H2>{'Actions'}</H2>
@@ -149,7 +119,7 @@ function Actions(props) {
149 <Button
119 <Button
150 prop-size={'sm'}
120 prop-size={'sm'}
151 prop-type={'link'}
121 prop-type={'link'}
152− disabled={(online || planet) && !canIssueChild}
122+ disabled={planet && !canIssueChild}
153 onClick={() => {
123 onClick={() => {
154 history.push(ROUTE_NAMES.ISSUE_CHILD);
124 history.push(ROUTE_NAMES.ISSUE_CHILD);
155 }}>
125 }}>
@@ -159,7 +129,7 @@ function Actions(props) {
159 <Button
129 <Button
160 prop-size={'sm'}
130 prop-size={'sm'}
161 prop-type={'link'}
131 prop-type={'link'}
162− disabled={online && !canAcceptTransfer}
132+ disabled={!canAcceptTransfer}
163 onClick={() => {
133 onClick={() => {
164 history.push(ROUTE_NAMES.ACCEPT_TRANSFER);
134 history.push(ROUTE_NAMES.ACCEPT_TRANSFER);
165 }}>
135 }}>
@@ -169,7 +139,7 @@ function Actions(props) {
169 <Button
139 <Button
170 prop-size={'sm'}
140 prop-size={'sm'}
171 prop-type={'link'}
141 prop-type={'link'}
172− disabled={online && !canCancelTransfer}
142+ disabled={!canCancelTransfer}
173 onClick={() => {
143 onClick={() => {
174 history.push(ROUTE_NAMES.CANCEL_TRANSFER);
144 history.push(ROUTE_NAMES.CANCEL_TRANSFER);
175 }}>
145 }}>
@@ -179,7 +149,7 @@ function Actions(props) {
179 <Button
149 <Button
180 prop-size={'sm'}
150 prop-size={'sm'}
181 prop-type={'link'}
151 prop-type={'link'}
182− disabled={online && !canGenKeyfile}
152+ disabled={!canGenKeyfile}
183 onClick={() => {
153 onClick={() => {
184 history.push(ROUTE_NAMES.GEN_KEYFILE);
154 history.push(ROUTE_NAMES.GEN_KEYFILE);
185 }}>
155 }}>
@@ -188,7 +158,7 @@ function Actions(props) {
188 </Grid.Item>
158 </Grid.Item>
189 <Grid.Item third={2}>
159 <Grid.Item third={2}>
190 <Button
160 <Button
191− disabled={online && !canSetSpawnProxy}
161+ disabled={!canSetSpawnProxy}
192 prop-size={'sm'}
162 prop-size={'sm'}
193 prop-type={'link'}
163 prop-type={'link'}
194 onClick={() => {
164 onClick={() => {
@@ -198,7 +168,7 @@ function Actions(props) {
198 </Button>
168 </Button>
199
169
200 <Button
170 <Button
201− disabled={online && !canSetManagementProxy}
171+ disabled={!canSetManagementProxy}
202 prop-size={'sm'}
172 prop-size={'sm'}
203 prop-type={'link'}
173 prop-type={'link'}
204 onClick={() => {
174 onClick={() => {
@@ -208,7 +178,7 @@ function Actions(props) {
208 </Button>
178 </Button>
209
179
210 <Button
180 <Button
211− disabled={online && !canConfigureKeys}
181+ disabled={!canConfigureKeys}
212 prop-size={'sm'}
182 prop-size={'sm'}
213 prop-type={'link'}
183 prop-type={'link'}
214 onClick={() => {
184 onClick={() => {
@@ -218,7 +188,7 @@ function Actions(props) {
218 </Button>
188 </Button>
219
189
220 <Button
190 <Button
221− disabled={online && !canTransfer}
191+ disabled={!canTransfer}
222 prop-size={'sm'}
192 prop-size={'sm'}
223 prop-type={'link'}
193 prop-type={'link'}
224 onClick={() => {
194 onClick={() => {
@@ -226,8 +196,15 @@ function Actions(props) {
226 }}>
196 }}>
227 {'Transfer'}
197 {'Transfer'}
228 </Button>
198 </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>
229 </Grid.Item>
207 </Grid.Item>
230− <Grid.Item third={3}>{inviteAction}</Grid.Item>
231 </Grid>
208 </Grid>
232 </div>
209 </div>
233 );
210 );
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 @@
1import React, { useCallback, useState } from 'react';
1import React, { useCallback, useState } from 'react';
2import Maybe from 'folktale/maybe';
2import Maybe from 'folktale/maybe';
3import * as ob from 'urbit-ob';
3import * as ob from 'urbit-ob';
4+import { Input } from 'indigo-react';
4
5
5import { H1, P } from '../components/old/Base';
6import { H1, P } from '../components/old/Base';
6
7
@@ -11,15 +12,20 @@ import { ROUTE_NAMES } from 'lib/routeNames';
11
12
12import View from 'components/View';
13import View from 'components/View';
13import { ForwardButton } from 'components/Buttons';
14import { ForwardButton } from 'components/Buttons';
14−import { PointInput } from 'components/Inputs';
15+import { usePointInput } from 'components/Inputs';
16+import InputSigil from 'components/InputSigil';
15
17
16export default function ViewPoint() {
18export default function ViewPoint() {
17 const history = useHistory();
19 const history = useHistory();
18 const { setPointCursor } = usePointCursor();
20 const { setPointCursor } = usePointCursor();
19− const [pointName, setPointName] = useState('');
21+ const [lastValidPoint, setLastValidPoint] = useState('');
20− const [pass, setPass] = useState(false);
22+ const pointInput = usePointInput({
21− const [error, setError] = useState();
23+ name: 'point',
22− const [focused, setFocused] = useState(true);
24+ label: 'Point Name',
25+ autoFocus: true,
26+ onValue: setLastValidPoint,
27+ });
28+ const { data: pointName, pass, error, focused } = pointInput;
23
29
24 const disabled = !pass;
30 const disabled = !pass;
25 const goForward = useCallback(() => {
31 const goForward = useCallback(() => {
@@ -34,19 +40,21 @@ export default function ViewPoint() {
34
40
35 <P>Enter a point name to view its public information.</P>
41 <P>Enter a point name to view its public information.</P>
36
42
37− <PointInput
43+ <Input
38− name="point"
44+ {...pointInput}
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}
48 onEnter={goForward}
45 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+ }
50 />
58 />
51
59
52 <ForwardButton
60 <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−}