on master help sign in
Merge pull request #180 from urbit/feat/indigo-login
a531c30Matt Condon
src/Bridge.js+1−1
@@ -23,7 +23,7 @@ const INITIAL_NETWORK_TYPE = isDevelopment
23const SHOULD_STUB_LOCAL = process.env.REACT_APP_STUB_LOCAL === 'true';
23const SHOULD_STUB_LOCAL = process.env.REACT_APP_STUB_LOCAL === 'true';
24const IS_STUBBED = isDevelopment && SHOULD_STUB_LOCAL;
24const IS_STUBBED = isDevelopment && SHOULD_STUB_LOCAL;
25const INITIAL_ROUTES = IS_STUBBED
25const INITIAL_ROUTES = IS_STUBBED
26− ? [{ key: ROUTE_NAMES.LANDING }, { key: ROUTE_NAMES.ACTIVATE }]
26+ ? [{ key: ROUTE_NAMES.LANDING }, { key: ROUTE_NAMES.LOGIN }]
27 : [{ key: ROUTE_NAMES.LANDING }];
27 : [{ key: ROUTE_NAMES.LANDING }];
28
28
29const INITIAL_WALLET = IS_STUBBED
29const INITIAL_WALLET = IS_STUBBED
src/components/Buttons.js+3−3
@@ -1,9 +1,9 @@
1import React from 'react';
1import React from 'react';
2import { Button } from 'indigo-react';
2import { Button } from 'indigo-react';
3
3
4−// TODO: use svg icons
4+// NOTE: the -> is correct because inter recognizes the pair
5−export const ForwardButton = props => <Button accessory="→" {...props} />;
5+export const ForwardButton = props => <Button accessory="->" {...props} />;
6−export const DownloadButton = props => <Button accessory="⬇" {...props} />;
6+export const DownloadButton = props => <Button accessory="↓" {...props} />;
7export const OutButton = props => (
7export const OutButton = props => (
8 <Button as="a" target="_blank" accessory="↗" {...props} />
8 <Button as="a" target="_blank" accessory="↗" {...props} />
9);
9);
src/components/Footer.js+2−1
@@ -1,8 +1,9 @@
1+import React from 'react';
1import { createTeleporter } from 'react-teleporter';
2import { createTeleporter } from 'react-teleporter';
2
3
3const FooterTeleporter = createTeleporter();
4const FooterTeleporter = createTeleporter();
4
5
5const Footer = FooterTeleporter.Source;
6const Footer = FooterTeleporter.Source;
6−Footer.Target = FooterTeleporter.Target;
7+Footer.Target = props => <FooterTeleporter.Target as="footer" {...props} />;
7
8
8export default Footer;
9export default Footer;
src/components/Inputs.jsdeleted−171
@@ -1,171 +0,0 @@
1−import React, { useState, useMemo } from 'react';
2−import * as bip39 from 'bip39';
3−
4−import InputSigil from 'components/InputSigil';
5−
6−import { DEFAULT_HD_PATH } from 'lib/wallet';
7−import {
8− validateNotEmpty,
9− validateMnemonic,
10− validatePoint,
11− validateTicket,
12− validateEmail,
13−} from 'lib/validators';
14−import { prependSig } from 'lib/transformers';
15−import useForm from 'indigo-react/lib/useForm';
16−import { AccessoryIcon } from 'indigo-react';
17−
18−// const RequiredInput = advancedInput({
19−// WrappedComponent: Input,
20−// validators: [validateNotEmpty],
21−// });
22−
23−// const NetworkKeyInput = advancedInput({
24−// WrappedComponent: Input,
25−// validators: [validateNotEmpty, validateNetworkKey],
26−// });
27−
28−// const NetworkSeedInput = advancedInput({
29−// WrappedComponent: Input,
30−// validators: [validateNotEmpty, validateNetworkSeed],
31−// });
32−
33−// const AddressInput = advancedInput({
34−// WrappedComponent: Input,
35−// validators: [validateEthereumAddress, validateNotEmpty],
36−// });
37−
38−const firstOf = state => state.inputs[0];
39−
40−export function usePassphraseInput(props) {
41− return firstOf(
42− useForm([
43− {
44− type: 'password',
45− autoComplete: 'off',
46− placeholder: 'Passphrase',
47− ...props,
48− },
49− ])
50− );
51−}
52−
53−const kExampleMnemonic = bip39.generateMnemonic();
54−const kMnemonicValidators = [validateMnemonic, validateNotEmpty];
55−export function useMnemonicInput(props) {
56− return firstOf(
57− useForm([
58− {
59− type: 'text',
60− autoComplete: 'off',
61− placeholder: `e.g. ${kExampleMnemonic}`,
62− validators: kMnemonicValidators,
63− ...props,
64− },
65− ])
66− );
67−}
68−
69−export function useHdPathInput(props) {
70− return firstOf(
71− useForm([
72− {
73− type: 'text',
74− autoComplete: 'off',
75− placeholder: `e.g. ${DEFAULT_HD_PATH}`,
76− ...props,
77− },
78− ])
79− );
80−}
81−
82−// const GalaxyInput = advancedInput({
83−// WrappedComponent: Input,
84−// validators: [validateGalaxy, validateNotEmpty],
85−// transformers: [prependSig],
86−// });
87−
88−const kTicketValidators = [validateTicket, validateNotEmpty];
89−const kTicketTransformers = [prependSig];
90−//TODO needs to be fancier, displaying sig and dashes instead of •ing all
91−export function useTicketInput({ validators = [], ...rest }) {
92− const input = firstOf(
93− useForm([
94− {
95− type: 'password',
96− label: 'Ticket',
97− placeholder: '~ragtyd-modwen-bostec-hinsep',
98− validators: useMemo(() => [...validators, ...kTicketValidators], [
99− validators,
100− ]),
101− transformers: kTicketTransformers,
102− mono: true,
103− ...rest,
104− },
105− ])
106− );
107−
108− return { ...input, accessory: input.pass ? <AccessoryIcon.Success /> : null };
109−}
110−
111−const kPointValidators = [validatePoint, validateNotEmpty];
112−const kPointTransformers = [prependSig];
113−export function usePointInput(rest) {
114− const [lastValidPoint, setLastValidPoint] = useState('');
115− const input = firstOf(
116− useForm([
117− {
118− type: 'text',
119− label: 'Point',
120− placeholder: 'e.g. ~zod',
121− validators: kPointValidators,
122− transformers: kPointTransformers,
123− mono: true,
124− onValue: setLastValidPoint,
125− ...rest,
126− },
127− ])
128− );
129−
130− const { pass, focused, error } = input;
131−
132− return {
133− ...input,
134− accessory: lastValidPoint && (
135− <InputSigil
136− patp={lastValidPoint}
137− size={68}
138− margin={8}
139− pass={pass}
140− focused={focused}
141− error={error}
142− />
143− ),
144− };
145−}
146−
147−export function useCheckboxInput({ initialValue, ...rest }) {
148− return firstOf(
149− useForm([
150− {
151− type: 'checkbox',
152− ...rest,
153− },
154− ])
155− );
156−}
157−
158−const kEmailValidators = [validateEmail, validateNotEmpty];
159−export const buildEmailInputConfig = extra => ({
160− type: 'email',
161− autoComplete: 'off',
162− validators: kEmailValidators,
163− initialValue: '',
164− ...extra,
165−});
166−
167−// const TicketInput = advancedInput({
168−// WrappedComponent: Input,
169−// validators: [validateTicket, validateNotEmpty],
170−// transformers: [prependSig],
171−// });
src/components/Passport.js+4−4
@@ -1,6 +1,6 @@
1import React from 'react';
1import React from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3−import Maybe from 'folktale/maybe';
3+import { Just, Nothing } from 'folktale/maybe';
4import * as ob from 'urbit-ob';
4import * as ob from 'urbit-ob';
5import { Grid, Flex, Text } from 'indigo-react';
5import { Grid, Flex, Text } from 'indigo-react';
6import { times } from 'lodash';
6import { times } from 'lodash';
@@ -36,7 +36,7 @@ export default function Passport({
36 point,
36 point,
37 className,
37 className,
38 ticket = null,
38 ticket = null,
39− address = Maybe.Nothing(),
39+ address = Nothing(),
40 mini = false,
40 mini = false,
41}) {
41}) {
42 const birthday = usePointBirthday(point.getOrElse(null));
42 const birthday = usePointBirthday(point.getOrElse(null));
@@ -46,8 +46,8 @@ export default function Passport({
46 });
46 });
47
47
48 const name = point.matchWith({
48 const name = point.matchWith({
49− Nothing: () => Maybe.Nothing(),
49+ Nothing: () => Nothing(),
50− Just: p => Maybe.Just(ob.patp(p.value)),
50+ Just: p => Just(ob.patp(p.value)),
51 });
51 });
52 const visualName = name.matchWith({
52 const visualName = name.matchWith({
53 Nothing: () => '...',
53 Nothing: () => '...',
src/components/Tabs.js+27−8
@@ -1,18 +1,37 @@
1import React from 'react';
1import React from 'react';
2−import HorizontalSelector from './old/HorizontalSelector';
2+import cn from 'classnames';
3+import { Grid, Flex } from 'indigo-react';
3
4
4export default function Tabs({
5export default function Tabs({
5− tabViews,
6+ views,
6− tabOptions,
7+ options,
7 currentTab,
8 currentTab,
8 onTabChange,
9 onTabChange,
10+ className,
9 ...rest
11 ...rest
10}) {
12}) {
11− const Component = tabViews[currentTab];
13+ const Tab = views[currentTab];
14+
12 return (
15 return (
13− <>
16+ <Grid className={className}>
14− <HorizontalSelector options={tabOptions} onChange={onTabChange} />
17+ <Grid.Item full as={Flex} wrap className="b-gray3 bb1">
15− <Component {...rest} />
18+ {options.map(option => {
16− </>
19+ const isActive = option.value === currentTab;
20+ return (
21+ <Flex.Item
22+ key={option.value}
23+ onClick={() => onTabChange(option.value)}
24+ className={cn('f5 pv3 mr6 clickable', {
25+ 'black b-black bb1': isActive,
26+ gray3: !isActive,
27+ })}
28+ style={{ marginBottom: '-1px' }}>
29+ {option.text}
30+ </Flex.Item>
31+ );
32+ })}
33+ </Grid.Item>
34+ <Grid.Item full as={Tab} className="mt6" {...rest} />
35+ </Grid>
17 );
36 );
18}
37}
src/components/UploadButton.jsnew file+19
@@ -0,0 +1,19 @@
1+import React, { useRef } from 'react';
2+import { Button } from 'indigo-react';
3+
4+export default function UploadButton({ children, onChange, ...rest }) {
5+ const button = useRef();
6+
7+ return (
8+ <label htmlFor="file" {...rest}>
9+ <Button accessory="↑">{children}</Button>
10+ <input
11+ id="file"
12+ ref={button}
13+ className="super-hidden"
14+ type="file"
15+ onChange={() => onChange(button.current)}
16+ />
17+ </label>
18+ );
19+}
src/components/View.js+8−2
@@ -5,7 +5,7 @@ import { Flex } from 'indigo-react';
5import Footer from './Footer';
5import Footer from './Footer';
6
6
7// View is a top-level component that all Views must render to inherit styling
7// View is a top-level component that all Views must render to inherit styling
8−function View({ className, children, full = false, ...rest }) {
8+function View({ className, children, inset = false, full = false, ...rest }) {
9 return (
9 return (
10 <Flex
10 <Flex
11 col
11 col
@@ -19,7 +19,13 @@ function View({ className, children, full = false, ...rest }) {
19 className
19 className
20 )}
20 )}
21 {...rest}>
21 {...rest}>
22− <Flex.Item className="pv5">{children}</Flex.Item>
22+ <Flex.Item
23+ className={cn('pb5', {
24+ pt10: inset,
25+ pt5: !inset,
26+ })}>
27+ {children}
28+ </Flex.Item>
23 <Footer.Target />
29 <Footer.Target />
24 </Flex>
30 </Flex>
25 );
31 );
src/components/old/RenderedPoint.js+4−4
@@ -1,5 +1,5 @@
1−import Maybe from 'folktale/maybe';
2import React from 'react';
1import React from 'react';
2+import { Just } from 'folktale/maybe';
3import { Col } from './Base';
3import { Col } from './Base';
4import { Button } from './Base';
4import { Button } from './Base';
5import { pour } from 'sigil-js';
5import { pour } from 'sigil-js';
@@ -24,7 +24,7 @@ const RenderedPoint = props => {
24 key={`rendered-sigil-${point}`}
24 key={`rendered-sigil-${point}`}
25 className="clickable"
25 className="clickable"
26 onClick={() => {
26 onClick={() => {
27− setPointCursor(Maybe.Just(point));
27+ setPointCursor(Just(point));
28 routeHandler(ROUTE_NAMES.POINT);
28 routeHandler(ROUTE_NAMES.POINT);
29 }}>
29 }}>
30 {sigil}
30 {sigil}
@@ -33,7 +33,7 @@ const RenderedPoint = props => {
33 key={`rendered-name-${point}`}
33 key={`rendered-name-${point}`}
34 className="clickable"
34 className="clickable"
35 onClick={() => {
35 onClick={() => {
36− setPointCursor(Maybe.Just(point));
36+ setPointCursor(Just(point));
37 routeHandler(ROUTE_NAMES.POINT);
37 routeHandler(ROUTE_NAMES.POINT);
38 }}>
38 }}>
39 <code>{name}</code>
39 <code>{name}</code>
@@ -44,7 +44,7 @@ const RenderedPoint = props => {
44 prop-type={'link'}
44 prop-type={'link'}
45 prop-size={'sm'}
45 prop-size={'sm'}
46 onClick={() => {
46 onClick={() => {
47− setPointCursor(Maybe.Just(point));
47+ setPointCursor(Just(point));
48 routeHandler(ROUTE_NAMES.POINT);
48 routeHandler(ROUTE_NAMES.POINT);
49 }}>
49 }}>
50 {'Details →'}
50 {'Details →'}
src/indigo-react/components/AccessoryIcon.js+1−1
@@ -4,7 +4,7 @@ import Flex from './Flex';
4
4
5const PENDING_ACCESSORY = '⋯';
5const PENDING_ACCESSORY = '⋯';
6const SUCCESS_ACCESSORY = '✓';
6const SUCCESS_ACCESSORY = '✓';
7−const FAILURE_ACCESSORY = '×';
7+const FAILURE_ACCESSORY = '✗';
8
8
9function AccessoryIcon({ ...props }) {
9function AccessoryIcon({ ...props }) {
10 return (
10 return (
src/indigo-react/components/Button.js+2−1
@@ -6,6 +6,7 @@ import Flex from './Flex';
6import { HelpText } from './Typography';
6import { HelpText } from './Typography';
7
7
8export default function Button({
8export default function Button({
9+ as: As = 'a',
9 solid = false,
10 solid = false,
10 success = false,
11 success = false,
11 disabled = false,
12 disabled = false,
@@ -23,7 +24,7 @@ export default function Button({
23 };
24 };
24 return (
25 return (
25 <Grid
26 <Grid
26− as="a"
27+ as={As}
27 gap={1}
28 gap={1}
28 className={cn(
29 className={cn(
29 'pointer pv4 truncate flex-row justify-between',
30 'pointer pv4 truncate flex-row justify-between',
src/indigo-react/components/CheckboxInput.js+21−24
@@ -12,9 +12,6 @@ export default function CheckboxInput({
12
12
13 // callbacks
13 // callbacks
14 onValue,
14 onValue,
15− onPass,
16− onError,
17− onFocus,
18 onEnter,
15 onEnter,
19
16
20 // state from hook
17 // state from hook
@@ -22,6 +19,7 @@ export default function CheckboxInput({
22 pass,
19 pass,
23 visiblyPassed,
20 visiblyPassed,
24 error,
21 error,
22+ hintError,
25 data,
23 data,
26 bind,
24 bind,
27 autoFocus,
25 autoFocus,
@@ -37,7 +35,7 @@ export default function CheckboxInput({
37}) {
35}) {
38 return (
36 return (
39 <Flex
37 <Flex
40− className={className}
38+ className={cn(className, 'mv2')}
41 row
39 row
42 align="center"
40 align="center"
43 style={{
41 style={{
@@ -46,35 +44,34 @@ export default function CheckboxInput({
46 cursor: 'not-allowed',
44 cursor: 'not-allowed',
47 }),
45 }),
48 }}>
46 }}>
47+ {/* we totally hide the checkbox itself */}
49 <Flex.Item
48 <Flex.Item
50 as="input"
49 as="input"
51 {...rest}
50 {...rest}
52− className={cn(
51+ className={cn('super-hidden')}
53− 'b b1 p3 outline-none',
54− {
55− 'bg-white': !disabled,
56− 'bg-gray1': disabled,
57− },
58− {
59− gray4: !focused,
60− black: focused,
61− },
62− {
63− 'b-green3': visiblyPassed,
64− 'b-black': focused && !visiblyPassed,
65− 'b-yellow3': !focused && error,
66− 'b-gray3': !focused && !error && !visiblyPassed,
67− }
68− )}
69 id={name}
52 id={name}
70 name={name}
53 name={name}
71 {...bind}
54 {...bind}
72 />
55 />
56+ {/* and then display a prettier one in its stead */}
73 <Flex.Item
57 <Flex.Item
58+ flex
74 as="label"
59 as="label"
75− className="f6 ph3 lh-tall us-none"
60+ className="f6 mr3 lh-tall us-none clickable flex-row align-center"
76− htmlFor={name}
61+ htmlFor={name}>
77− flex>
62+ <Flex
63+ justify="center"
64+ align="center"
65+ className={cn('b-black b1 p1 mr3', {
66+ 'bg-black white': data,
67+ 'bg-white black': !data,
68+ })}
69+ style={{
70+ height: '14px',
71+ width: '14px',
72+ }}>
73+ {data && '✓'}
74+ </Flex>
78 {label}
75 {label}
79 </Flex.Item>
76 </Flex.Item>
80 </Flex>
77 </Flex>
src/indigo-react/components/Flex.js+21−13
@@ -1,21 +1,25 @@
1import React from 'react';
1import React from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3
3
4−function Flex({
4+const Flex = React.forwardRef(function Flex(
5+ {
5− as: As = 'div',
6+ as: As = 'div',
6− row = false,
7+ row = false,
7− wrap = false,
8+ wrap = false,
8− col = false,
9+ col = false,
9− align,
10+ align,
10− justify,
11+ justify,
11− className,
12+ className,
12− ...rest
13+ ...rest
14+ },
15+ ref
13−}) {
16+) {
14 if (row && col) {
17 if (row && col) {
15 throw new Error('Only one of row or col must be true, not both.');
18 throw new Error('Only one of row or col must be true, not both.');
16 }
19 }
17 return (
20 return (
18 <As
21 <As
22+ ref={ref}
19 className={cn(
23 className={cn(
20 'flex',
24 'flex',
21 {
25 {
@@ -32,16 +36,20 @@ function Flex({
32 {...rest}
36 {...rest}
33 />
37 />
34 );
38 );
35−}
39+});
36
40
37// flex can be boolean {true} or integer flex
41// flex can be boolean {true} or integer flex
42+Flex.Item = React.forwardRef(function FlexItem(
38−Flex.Item = function FlexItem({ as: As = 'div', flex, className, ...rest }) {
43+ { as: As = 'div', flex, className, ...rest },
44+ ref
45+) {
39 if (flex === true) {
46 if (flex === true) {
40 flex = 1;
47 flex = 1;
41 }
48 }
42
49
43 return (
50 return (
44 <As
51 <As
52+ ref={ref}
45 className={cn(
53 className={cn(
46 {
54 {
47 [`flex${flex}`]: flex,
55 [`flex${flex}`]: flex,
@@ -51,6 +59,6 @@ Flex.Item = function FlexItem({ as: As = 'div', flex, className, ...rest }) {
51 {...rest}
59 {...rest}
52 />
60 />
53 );
61 );
54−};
62+});
55
63
56export default Flex;
64export default Flex;
src/indigo-react/components/Grid.js+24−16
@@ -1,9 +1,13 @@
1import React from 'react';
1import React from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3
3
4+const Grid = React.forwardRef(function Grid(
4−function Grid({ as: As = 'div', gap = 0, align, justify, className, ...rest }) {
5+ { as: As = 'div', gap = 0, align, justify, className, ...rest },
6+ ref
7+) {
5 return (
8 return (
6 <As
9 <As
10+ ref={ref}
7 className={cn(
11 className={cn(
8 'grid12',
12 'grid12',
9 gap && `gap${gap}`,
13 gap && `gap${gap}`,
@@ -16,23 +20,27 @@ function Grid({ as: As = 'div', gap = 0, align, justify, className, ...rest }) {
16 {...rest}
20 {...rest}
17 />
21 />
18 );
22 );
19−}
23+});
20
24
21−Grid.Item = function GridItem({
25+Grid.Item = React.forwardRef(function GridItem(
26+ {
22− as: As = 'div',
27+ as: As = 'div',
23− full = false,
28+ full = false,
24− half = 0,
29+ half = 0,
25− third = 0,
30+ third = 0,
26− fourth = 0,
31+ fourth = 0,
27− rows = [],
32+ rows = [],
28− cols = [],
33+ cols = [],
29− justifySelf,
34+ justifySelf,
30− alignSelf,
35+ alignSelf,
31− className,
36+ className,
32− ...rest
37+ ...rest
38+ },
39+ ref
33−}) {
40+) {
34 return (
41 return (
35 <As
42 <As
43+ ref={ref}
36 className={cn(
44 className={cn(
37 {
45 {
38 full,
46 full,
@@ -53,7 +61,7 @@ Grid.Item = function GridItem({
53 {...rest}
61 {...rest}
54 />
62 />
55 );
63 );
56−};
64+});
57
65
58Grid.Divider = function GridDivider({
66Grid.Divider = function GridDivider({
59 vertical = false,
67 vertical = false,
src/indigo-react/components/Input.js+7−21
@@ -14,9 +14,6 @@ export default function Input({
14
14
15 // callbacks
15 // callbacks
16 onValue,
16 onValue,
17− onPass,
18− onError,
19− onFocus,
20 onEnter,
17 onEnter,
21
18
22 // state from hook
19 // state from hook
@@ -24,6 +21,7 @@ export default function Input({
24 pass,
21 pass,
25 visiblyPassed,
22 visiblyPassed,
26 error,
23 error,
24+ hintError,
27 data,
25 data,
28 bind,
26 bind,
29 autoFocus,
27 autoFocus,
@@ -49,28 +47,16 @@ export default function Input({
49 );
47 );
50
48
51 // notify parent of value only when passing
49 // notify parent of value only when passing
50+ // TODO: deprecate/remove this
51+ // keeping it around for now to avoid refactoring InviteEmail.js, but this
52+ // feels like a hack
52 useEffect(() => {
53 useEffect(() => {
53 pass && onValue && onValue(data);
54 pass && onValue && onValue(data);
54 }, [pass, data]); // eslint-disable-line react-hooks/exhaustive-deps
55 }, [pass, data]); // eslint-disable-line react-hooks/exhaustive-deps
55
56
56− // notify parent of pass
57− useEffect(() => {
58− onPass && onPass(pass);
59− }, [pass]); // eslint-disable-line react-hooks/exhaustive-deps
60−
61− // notify parent of error whenever error changes
62− useEffect(() => {
63− onError && onError(error);
64− }, [error]); // eslint-disable-line react-hooks/exhaustive-deps
65−
66− // notify parent of focus
67− useEffect(() => {
68− onFocus && onFocus(focused);
69− }, [focused]); // eslint-disable-line react-hooks/exhaustive-deps
70−
71 return (
57 return (
72 <Flex
58 <Flex
73− className={className}
59+ className={cn(className, 'mb1')}
74 col
60 col
75 style={{
61 style={{
76 ...(disabled && {
62 ...(disabled && {
@@ -99,8 +85,8 @@ export default function Input({
99 {
85 {
100 'b-green3': visiblyPassed,
86 'b-green3': visiblyPassed,
101 'b-black': focused && !visiblyPassed,
87 'b-black': focused && !visiblyPassed,
102− 'b-yellow3': !focused && error,
88+ 'b-yellow3': !focused && hintError,
103− 'b-gray3': !focused && !error && !visiblyPassed,
89+ 'b-gray3': !focused && !hintError && !visiblyPassed,
104 }
90 }
105 // TODO: inputClassName ?
91 // TODO: inputClassName ?
106 )}
92 )}
src/indigo-react/components/SelectInput.jsnew file+151
@@ -0,0 +1,151 @@
1+import React, { useCallback, useRef, useState } from 'react';
2+import cn from 'classnames';
3+
4+import Flex from './Flex';
5+import { ErrorText } from './Typography';
6+import useOnClickOutside from 'indigo-react/lib/useOnClickOutside';
7+import AccessoryIcon from './AccessoryIcon';
8+
9+// NOTE: if we really care about accessibility, we should pull in a dependency
10+export default function SelectInput({
11+ // visuals
12+ name,
13+ label,
14+ className,
15+ accessory,
16+ mono = false,
17+ placeholder,
18+
19+ // callbacks
20+ onValue,
21+ onEnter,
22+
23+ // state from hook
24+ focused,
25+ pass,
26+ visiblyPassed,
27+ error,
28+ hintError,
29+ data,
30+ bind,
31+ autoFocus,
32+ disabled,
33+ options,
34+
35+ // ignored
36+ initialValue,
37+ validators,
38+ transformers,
39+
40+ // extra
41+ ...rest
42+}) {
43+ const [isOpen, setIsOpen] = useState(false);
44+ const ref = useRef();
45+
46+ // close select on outside clicks
47+ useOnClickOutside(ref, useCallback(() => setIsOpen(false), [setIsOpen]));
48+
49+ const toggleOpen = useCallback(() => setIsOpen(isOpen => !isOpen), [
50+ setIsOpen,
51+ ]);
52+
53+ const onChange = value => {
54+ // TODO: provide setValue here?
55+ // construct a pseudo event that sets the value correctly
56+ bind.onChange({ target: { value } });
57+ setIsOpen(false);
58+ };
59+
60+ // redefine accessory because we still want to ignore it from the ..rest above
61+ accessory = (
62+ <AccessoryIcon className="gray4">{isOpen ? '▲' : '▼'}</AccessoryIcon>
63+ );
64+
65+ const text = options.find(o => o.value === data).text;
66+
67+ return (
68+ <Flex
69+ ref={ref}
70+ className={cn(className, 'mb1 us-none')}
71+ col
72+ style={{
73+ ...(disabled && {
74+ pointerEvents: 'none',
75+ cursor: 'not-allowed',
76+ }),
77+ }}>
78+ <Flex.Item as="label" className="f6 lh-tall" htmlFor={name}>
79+ {label}
80+ </Flex.Item>
81+ <Flex.Item as={Flex} row className="rel clickable" onClick={toggleOpen}>
82+ <Flex.Item
83+ // as="select"
84+ className={cn(
85+ 'b b1 p3 outline-none',
86+ { mono },
87+ {
88+ 'bg-white': !disabled,
89+ 'bg-gray1': disabled,
90+ },
91+ {
92+ gray4: isOpen,
93+ black: !isOpen,
94+ },
95+ {
96+ 'b-green3': visiblyPassed,
97+ 'b-black': focused && !visiblyPassed,
98+ 'b-yellow3': !focused && hintError,
99+ 'b-gray3': !focused && !hintError && !visiblyPassed,
100+ }
101+ )}
102+ id={name}
103+ name={name}
104+ {...bind}
105+ flex>
106+ {isOpen ? placeholder : text}
107+ </Flex.Item>
108+ <div
109+ className="abs"
110+ style={{
111+ top: 0,
112+ right: 0,
113+ height: '100%',
114+ width: '44px',
115+ overflow: 'hidden',
116+ }}>
117+ {accessory}
118+ </div>
119+ {isOpen && (
120+ <Flex
121+ col
122+ className="abs bg-white b-black b1 z10"
123+ style={{
124+ top: 'calc(100% + 4px)',
125+ left: 0,
126+ right: 0,
127+ overflow: 'auto',
128+ maxHeight: '30vh',
129+ }}>
130+ {options.map(option => (
131+ <Flex.Item
132+ key={option.value}
133+ className="pv2 ph3 hover-bg-grey3"
134+ onClick={e => {
135+ e.stopPropagation();
136+ onChange(option.value);
137+ }}>
138+ {option.text}
139+ </Flex.Item>
140+ ))}
141+ </Flex>
142+ )}
143+ </Flex.Item>
144+ {error && (
145+ <Flex.Item as={ErrorText} className="mv1">
146+ {error}
147+ </Flex.Item>
148+ )}
149+ </Flex>
150+ );
151+}
src/indigo-react/index.js+2
@@ -8,3 +8,5 @@ export { default as Button } from './components/Button';
8export { default as IconButton } from './components/IconButton';
8export { default as IconButton } from './components/IconButton';
9
9
10export * from './components/Typography';
10export * from './components/Typography';
11+
12+export * from './lib';
src/indigo-react/lib/index.jsnew file+1
@@ -0,0 +1 @@
1+export { default as useForm } from './useForm';
src/indigo-react/lib/useForm.js+85−41
@@ -1,9 +1,10 @@
1import { useCallback, useMemo, useState } from 'react';
1import { useCallback, useMemo, useState } from 'react';
2−import { keyBy, get, every, some } from 'lodash';
2+import { isEqual, keyBy, get, every, some } from 'lodash';
3
3
4import { compose } from 'lib/lib';
4import { compose } from 'lib/lib';
5import { kDefaultValidator } from 'lib/validators';
5import { kDefaultValidator } from 'lib/validators';
6import useSetState from 'lib/useSetState';
6import useSetState from 'lib/useSetState';
7+import usePreviousValue from 'lib/usePreviousValue';
7
8
8// interface InputConfig {
9// interface InputConfig {
9// name: string;
10// name: string;
@@ -24,12 +25,22 @@ const defaultsFor = (configs, mapper) =>
24/**
25/**
25 * useForm manages a set of inputs for rendering them in a loop
26 * useForm manages a set of inputs for rendering them in a loop
26 */
27 */
27−export default function useForm(configs = []) {
28+export default function useForm(inputConfigs = []) {
29+ // track the old value of the config set
30+ const previousConfigs = usePreviousValue(inputConfigs);
31+ // then compare equality
32+ const configsAreEqual = isEqual(inputConfigs, previousConfigs);
33+ // if equality changes, give `configs` a new identity
34+ // eslint-disable-next-line react-hooks/exhaustive-deps
35+ const configs = useMemo(() => inputConfigs, [configsAreEqual]);
36+
28 const byName = useMemo(() => keyBy(configs, 'name'), [configs]);
37 const byName = useMemo(() => keyBy(configs, 'name'), [configs]);
29
38
30 // track values
39 // track values
31 const [values, _setValues] = useState(() =>
40 const [values, _setValues] = useState(() =>
32− defaultsFor(configs, config => config.initialValue || '')
41+ defaultsFor(configs, config =>
42+ config.initialValue === undefined ? '' : config.initialValue
43+ )
33 );
44 );
34 // ^ NB(shrugs): because we're not syncing the initialValue of _additional_
45 // ^ NB(shrugs): because we're not syncing the initialValue of _additional_
35 // configs that are added, we won't be able to set an initialValue for a
46 // configs that are added, we won't be able to set an initialValue for a
@@ -38,11 +49,6 @@ export default function useForm(configs = []) {
38 // but for dynamically added input configs, we won't get validation, etc
49 // but for dynamically added input configs, we won't get validation, etc
39 // until there's a traditional state update.
50 // until there's a traditional state update.
40
51
41− const setValue = useCallback(
42− (name, value) => _setValues(values => ({ ...values, [name]: value })),
43− [_setValues]
44− );
45−
46 // track focused states
52 // track focused states
47 const [focused, setFocused] = useSetState(() =>
53 const [focused, setFocused] = useSetState(() =>
48 defaultsFor(configs, config => config.autoFocus && !config.disabled)
54 defaultsFor(configs, config => config.autoFocus && !config.disabled)
@@ -55,12 +61,22 @@ export default function useForm(configs = []) {
55 // track whether or not input has been focused
61 // track whether or not input has been focused
56 const [hasBeenFocused, setHasBeenFocused] = useSetState({});
62 const [hasBeenFocused, setHasBeenFocused] = useSetState({});
57
63
64+ // track whether or not the input has been touched
65+ const [hasBeenTouched, setHasBeenTouched] = useSetState({});
66+
58 // build fn that transforms a value by input name
67 // build fn that transforms a value by input name
59 const transform = useCallback(
68 const transform = useCallback(
60 (name, value) => compose(...get(byName, [name, 'transformers'], []))(value),
69 (name, value) => compose(...get(byName, [name, 'transformers'], []))(value),
61 [byName]
70 [byName]
62 );
71 );
63
72
73+ // set a value (and transform it)
74+ const setValue = useCallback(
75+ (name, value) =>
76+ _setValues(values => ({ ...values, [name]: transform(name, value) })),
77+ [_setValues, transform]
78+ );
79+
64 // build fn that validates a value by input name
80 // build fn that validates a value by input name
65 const validate = useCallback(
81 const validate = useCallback(
66 (name, value) =>
82 (name, value) =>
@@ -84,8 +100,11 @@ export default function useForm(configs = []) {
84
100
85 // on change, transform and set value
101 // on change, transform and set value
86 const onChange = useCallback(
102 const onChange = useCallback(
103+ name => e => {
87− name => e => setValue(name, transform(name, getValue(name, e))), //
104+ setValue(name, getValue(name, e));
88− [setValue, transform, getValue]
105+ setHasBeenTouched({ [name]: true });
106+ }, //
107+ [setValue, getValue, setHasBeenTouched]
89 );
108 );
90
109
91 // on focus, update focus
110 // on focus, update focus
@@ -113,27 +132,45 @@ export default function useForm(configs = []) {
113 const datas = useMemo(() => validations.map(v => v.data), [validations]);
132 const datas = useMemo(() => validations.map(v => v.data), [validations]);
114
133
115 // the input has errored if it
134 // the input has errored if it
135+ // 1) did not pass validation and has an error text
136+ // 2) or has a specific error from parent state
137+ // and then we store that in errorTexts[i]
138+ // <string | falsy>[]
139+ const errorTexts = useMemo(
140+ () =>
141+ configs.map((config, i) => validations[i].error || config.error || false),
142+ [configs, validations]
143+ );
144+
145+ // we should hint at an error if it
116 // 1) has been focused before
146 // 1) has been focused before
117− // 2)
118− // a) did not pass validation and has an error text
147+ // 2) and has an error
119− // b) or has a specific error from parent state
148+ // boolean[]
120− // (and we implicitly return the error text at the end)
121− const errors = useMemo(
149+ const hintErrors = useMemo(
122 () =>
150 () =>
123 configs.map(
151 configs.map(
124− (config, i) =>
125− hasBeenFocused[config.name] && //
152+ (config, i) => hasBeenFocused[config.name] && !!errorTexts[i]
126− (validations[i].error || config.error)
127 ),
153 ),
128− [configs, validations, hasBeenFocused]
154+ [configs, errorTexts, hasBeenFocused]
155+ );
156+
157+ // we should block on error (with potential reflow) if it
158+ // 1) has been touched
159+ // 2) and has an error
160+ // consumers should use this value to show error text and block on operations
161+ // <string | falsy>[]
162+ const errors = useMemo(
163+ () =>
164+ configs.map((config, i) => hasBeenTouched[config.name] && errorTexts[i]),
165+ [configs, errorTexts, hasBeenTouched]
129 );
166 );
130
167
131 // this input has passed if
168 // this input has passed if
132 // 1) it has passed local validation
169 // 1) it has passed local validation
133 // 2) there are no errors
170 // 2) there are no errors
134 const passes = useMemo(
171 const passes = useMemo(
135− () => validations.map((v, i) => v.pass && !errors[i]),
172+ () => validations.map((v, i) => v.pass && !errorTexts[i]),
136− [validations, errors]
173+ [validations, errorTexts]
137 );
174 );
138
175
139 // visibly tell the user that their input has passed if
176 // visibly tell the user that their input has passed if
@@ -153,27 +190,32 @@ export default function useForm(configs = []) {
153 const inputs = useMemo(
190 const inputs = useMemo(
154 () =>
191 () =>
155 configs.map(
192 configs.map(
156− ({ name, error, autoFocus, disabled, initialValue, ...rest }, i) => ({
193+ ({ name, error, autoFocus, disabled, initialValue, ...rest }, i) => {
194+ const value =
195+ values[name] || (initialValue === undefined ? '' : initialValue);
196+ return {
157− // Input props
197+ // Input props
158− name,
198+ name,
159− data: datas[i],
199+ data: datas[i],
160− pass: passes[i],
200+ pass: passes[i],
161− visiblyPassed: visiblePasses[i],
201+ visiblyPassed: visiblePasses[i],
162− error: errors[i],
202+ error: errors[i],
203+ hintError: hintErrors[i],
163− focused: focuses[i],
204+ focused: focuses[i],
164− autoFocus: autoFocus && !disabled,
165− disabled,
166− ...rest,
167− // dom properties below:
168− bind: {
169− value: values[name] || initialValue || '',
170− checked: !!values[name],
171− onChange: onChange(name),
172− onFocus: onFocus(name),
173− onBlur: onBlur(name),
174 autoFocus: autoFocus && !disabled,
205 autoFocus: autoFocus && !disabled,
206+ disabled,
207+ ...rest,
208+ // dom properties below:
209+ bind: {
210+ value,
211+ checked: !!values[name],
212+ onChange: onChange(name),
213+ onFocus: onFocus(name),
214+ onBlur: onBlur(name),
215+ autoFocus: autoFocus && !disabled,
216+ },
175− },
217+ };
176− })
218+ }
177 ),
219 ),
178 [
220 [
179 configs,
221 configs,
@@ -181,6 +223,7 @@ export default function useForm(configs = []) {
181 visiblePasses,
223 visiblePasses,
182 passes,
224 passes,
183 errors,
225 errors,
226+ hintErrors,
184 focuses,
227 focuses,
185 values,
228 values,
186 onBlur,
229 onBlur,
@@ -198,5 +241,6 @@ export default function useForm(configs = []) {
198 inputs,
241 inputs,
199 pass,
242 pass,
200 error,
243 error,
244+ setValue,
201 };
245 };
202}
246}
src/indigo-react/lib/useOnClickOutside.jsnew file+24
@@ -0,0 +1,24 @@
1+import { useEffect } from 'react';
2+
3+// via https:// usehooks.com/useOnClickOutside/
4+
5+export default function useOnClickOutside(ref, handler) {
6+ useEffect(() => {
7+ const listener = event => {
8+ // Do nothing if clicking ref's element or descendent elements
9+ if (!ref.current || ref.current.contains(event.target)) {
10+ return;
11+ }
12+
13+ handler(event);
14+ };
15+
16+ document.addEventListener('mousedown', listener);
17+ document.addEventListener('touchstart', listener);
18+
19+ return () => {
20+ document.removeEventListener('mousedown', listener);
21+ document.removeEventListener('touchstart', listener);
22+ };
23+ }, [ref, handler]);
24+}
src/lib/keys.js+4−4
@@ -1,5 +1,5 @@
1import BN from 'bn.js';
1import BN from 'bn.js';
2−import Maybe from 'folktale/maybe';
2+import { Just, Nothing } from 'folktale/maybe';
3import * as need from './need';
3import * as need from './need';
4
4
5import * as noun from '../nockjs/noun';
5import * as noun from '../nockjs/noun';
@@ -119,13 +119,13 @@ const attemptNetworkSeedDerivation = async (next, args) => {
119 const seed = await kg.deriveNetworkSeed(managementSeed, '', revision);
119 const seed = await kg.deriveNetworkSeed(managementSeed, '', revision);
120
120
121 if (seed === '') {
121 if (seed === '') {
122− return Maybe.Nothing();
122+ return Nothing();
123 }
123 }
124
124
125− return Maybe.Just(seed);
125+ return Just(seed);
126 }
126 }
127
127
128− return Maybe.Nothing();
128+ return Nothing();
129};
129};
130
130
131export { genKey, attemptNetworkSeedDerivation };
131export { genKey, attemptNetworkSeedDerivation };
src/lib/ledger.js+6−6
@@ -1,12 +1,14 @@
1import Transport from '@ledgerhq/hw-transport-u2f';
1import Transport from '@ledgerhq/hw-transport-u2f';
2import Eth from '@ledgerhq/hw-app-eth';
2import Eth from '@ledgerhq/hw-app-eth';
3
3
4−const LEDGER_LEGACY_PATH = "m/44'/60'/0'/x";
4+export const LEDGER_LEGACY_PATH = "m/44'/60'/0'/x";
5−const LEDGER_LIVE_PATH = "m/44'/60'/x'/0/0";
5+export const LEDGER_LIVE_PATH = "m/44'/60'/x'/0/0";
6
6
7+export const chopHdPrefix = str =>
7−const chopHdPrefix = str => (str.slice(0, 2) === 'm/' ? str.slice(2) : str);
8+ str.slice(0, 2) === 'm/' ? str.slice(2) : str;
9+export const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
8
10
9−const ledgerSignTransaction = async (txn, hdpath) => {
11+export const ledgerSignTransaction = async (txn, hdpath) => {
10 const transport = await Transport.create();
12 const transport = await Transport.create();
11 const eth = new Eth(transport);
13 const eth = new Eth(transport);
12
14
@@ -20,5 +22,3 @@ const ledgerSignTransaction = async (txn, hdpath) => {
20
22
21 return txn;
23 return txn;
22};
24};
23−
24−export { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH, ledgerSignTransaction };
src/lib/lib.js+6
@@ -34,6 +34,11 @@ const randomPatq = len => {
34 return ob.hex2patq(hex);
34 return ob.hex2patq(hex);
35};
35};
36
36
37+// TODO: is there a function for this calculation in urbit-ob or similar?
38+// 3 characters per byte, plus a tilde and hypen prefix per pair of bytes
39+const patpStringLength = byteLength =>
40+ byteLength * 3 + Math.ceil(byteLength / 2);
41+
37export {
42export {
38 compose,
43 compose,
39 allFalse,
44 allFalse,
@@ -44,4 +49,5 @@ export {
44 fill,
49 fill,
45 isValidGalaxy,
50 isValidGalaxy,
46 randomPatq,
51 randomPatq,
52+ patpStringLength,
47};
53};
src/lib/useInputs.jsnew file+221
@@ -0,0 +1,221 @@
1+import React, { useCallback, useState, useMemo } from 'react';
2+import { AccessoryIcon, useForm } from 'indigo-react';
3+import { identity } from 'lodash';
4+import * as bip39 from 'bip39';
5+
6+import { DEFAULT_HD_PATH } from 'lib/wallet';
7+import {
8+ validateNotEmpty,
9+ validateMnemonic,
10+ validatePoint,
11+ validateTicket,
12+ validateEmail,
13+ validateLength,
14+ validateHexString,
15+ validateOneOf,
16+ validateMaximumPatpByteLength,
17+} from 'lib/validators';
18+import { prependSig } from 'lib/transformers';
19+
20+import InputSigil from 'components/InputSigil';
21+
22+const EXAMPLE_PRIVATE_KEY =
23+ '0xa44de2416ee6beb2f323fab48b432925c9785808d33a6ca6d7ba00b45e9370c3';
24+
25+// pulls out the first input from a useForm() call
26+function useFirstOf({ inputs, setValue, ...rest }, mapper = identity) {
27+ // ask the mapper function for any values to overwrite
28+ const input = {
29+ ...inputs[0],
30+ ...mapper(inputs[0]),
31+ };
32+ // memoize the setValue callback for manually setting the value
33+ // in response to some imperative event
34+ const _setValue = useCallback(value => setValue(input.name, value), [
35+ setValue,
36+ input.name,
37+ ]);
38+
39+ // provide the first input as the first element
40+ // _and_ as the second element to facilitate destructuring
41+ // and then provide the pass/error/setValue/etc properties at the end
42+ return [
43+ input,
44+ input,
45+ {
46+ ...rest,
47+ setValue: _setValue,
48+ },
49+ ];
50+}
51+
52+export function usePassphraseInput(props) {
53+ return useFirstOf(
54+ useForm([
55+ {
56+ type: 'password',
57+ autoComplete: 'off',
58+ placeholder: 'Passphrase',
59+ ...props,
60+ },
61+ ])
62+ );
63+}
64+
65+export function useHexInput({ length, ...rest }) {
66+ return useFirstOf(
67+ useForm([
68+ {
69+ type: 'text', // or password
70+ autoComplete: 'off',
71+ placeholder: EXAMPLE_PRIVATE_KEY,
72+ validators: useMemo(
73+ () => [
74+ validateHexString,
75+ validateLength(length + 2),
76+ validateNotEmpty,
77+ ],
78+ [length]
79+ ),
80+ ...rest,
81+ },
82+ ])
83+ );
84+}
85+
86+const kExampleMnemonic = bip39.generateMnemonic();
87+const kMnemonicValidators = [validateMnemonic, validateNotEmpty];
88+export function useMnemonicInput(props) {
89+ return useFirstOf(
90+ useForm([
91+ {
92+ type: 'text',
93+ autoComplete: 'off',
94+ placeholder: `e.g. ${kExampleMnemonic}`,
95+ validators: kMnemonicValidators,
96+ ...props,
97+ },
98+ ])
99+ );
100+}
101+
102+export function useHdPathInput(props) {
103+ return useFirstOf(
104+ useForm([
105+ {
106+ type: 'text',
107+ autoComplete: 'off',
108+ placeholder: `e.g. ${DEFAULT_HD_PATH}`,
109+ ...props,
110+ },
111+ ])
112+ );
113+}
114+
115+const kTicketValidators = [validateTicket, validateNotEmpty];
116+//TODO needs to be fancier, displaying sig and dashes instead of •ing all
117+const kTicketTransformers = [prependSig];
118+export function useTicketInput({ validators = [], deriving = false, ...rest }) {
119+ return useFirstOf(
120+ useForm([
121+ {
122+ type: 'password',
123+ label: 'Ticket',
124+ placeholder: 'e.g. ~ragtyd-modwen-bostec-hinsep',
125+ validators: useMemo(() => [...validators, ...kTicketValidators], [
126+ validators,
127+ ]),
128+ transformers: kTicketTransformers,
129+ mono: true,
130+ ...rest,
131+ },
132+ ]),
133+ ({ error, pass }) => ({
134+ accessory: error ? (
135+ <AccessoryIcon.Failure />
136+ ) : deriving ? (
137+ <AccessoryIcon.Pending />
138+ ) : pass ? (
139+ <AccessoryIcon.Success />
140+ ) : null,
141+ })
142+ );
143+}
144+
145+const kPointValidators = [
146+ validatePoint,
147+ validateMaximumPatpByteLength(4),
148+ validateNotEmpty,
149+];
150+const kPointTransformers = [prependSig];
151+export function usePointInput(rest) {
152+ const [lastValidPoint, setLastValidPoint] = useState('');
153+ return useFirstOf(
154+ useForm([
155+ {
156+ type: 'text',
157+ label: 'Point',
158+ placeholder: 'e.g. ~zod',
159+ validators: kPointValidators,
160+ transformers: kPointTransformers,
161+ mono: true,
162+ onValue: setLastValidPoint,
163+ ...rest,
164+ },
165+ ]),
166+ ({ error, pass, focused }) => ({
167+ accessory: lastValidPoint ? (
168+ <InputSigil
169+ patp={lastValidPoint}
170+ size={44}
171+ margin={8}
172+ pass={pass}
173+ focused={focused}
174+ error={error}
175+ />
176+ ) : null,
177+ })
178+ );
179+}
180+
181+export function useCheckboxInput({ initialValue, ...rest }) {
182+ return useFirstOf(
183+ useForm([
184+ {
185+ type: 'checkbox',
186+ ...rest,
187+ },
188+ ])
189+ );
190+}
191+
192+export function useSelectInput({ initialValue, options, ...rest }) {
193+ return useFirstOf(
194+ useForm([
195+ {
196+ type: 'select',
197+ validators: useMemo(
198+ () => [
199+ validateOneOf(
200+ options.map(option => option.value),
201+ validateNotEmpty
202+ ),
203+ ],
204+ [options]
205+ ),
206+ options,
207+ initialValue: initialValue || options[0].value,
208+ ...rest,
209+ },
210+ ])
211+ );
212+}
213+
214+const kEmailValidators = [validateEmail, validateNotEmpty];
215+export const buildEmailInputConfig = extra => ({
216+ type: 'email',
217+ autoComplete: 'off',
218+ validators: kEmailValidators,
219+ initialValue: '',
220+ ...extra,
221+});
src/lib/useLoginView.jsnew file+17
@@ -0,0 +1,17 @@
1+import { useEffect } from 'react';
2+import { Nothing } from 'folktale/maybe';
3+
4+import { useWallet } from 'store/wallet';
5+import { usePointCursor } from 'store/pointCursor';
6+
7+// All the things we want to do when we mount a login view
8+export default function useLoginView(walletType) {
9+ const { setWalletType, resetWallet } = useWallet();
10+ const { setPointCursor } = usePointCursor();
11+
12+ useEffect(() => {
13+ resetWallet();
14+ setWalletType(walletType);
15+ setPointCursor(Nothing());
16+ }, [setWalletType, walletType, setPointCursor, resetWallet]);
17+}
src/lib/useMailer.js+5−5
@@ -1,5 +1,5 @@
1import { useCallback } from 'react';
1import { useCallback } from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Just, Nothing } from 'folktale/maybe';
3
3
4import { hasReceived, sendMail } from './inviteMail';
4import { hasReceived, sendMail } from './inviteMail';
5import useSetState from './useSetState';
5import useSetState from './useSetState';
@@ -10,24 +10,24 @@ function useHasReceivedCache() {
10 const [cache, addToCache] = useSetState();
10 const [cache, addToCache] = useSetState();
11
11
12 const getHasRecieved = useCallback(
12 const getHasRecieved = useCallback(
13− email => cache[email] || Maybe.Nothing(), //
13+ email => cache[email] || Nothing(), //
14 [cache]
14 [cache]
15 );
15 );
16
16
17 const syncHasReceivedForEmail = useCallback(
17 const syncHasReceivedForEmail = useCallback(
18 async email => {
18 async email => {
19− if (Maybe.Just.hasInstance(getHasRecieved(email))) {
19+ if (Just.hasInstance(getHasRecieved(email))) {
20 // never update the cache after we know about it
20 // never update the cache after we know about it
21 return;
21 return;
22 }
22 }
23
23
24 if (STUB_MAILER) {
24 if (STUB_MAILER) {
25 // always allow sending emails when stubbing
25 // always allow sending emails when stubbing
26− return addToCache({ [email]: Maybe.Just(false) });
26+ return addToCache({ [email]: Just(false) });
27 }
27 }
28
28
29 const _hasReceived = await hasReceived(email);
29 const _hasReceived = await hasReceived(email);
30− addToCache({ [email]: Maybe.Just(_hasReceived) });
30+ addToCache({ [email]: Just(_hasReceived) });
31 },
31 },
32 [getHasRecieved, addToCache]
32 [getHasRecieved, addToCache]
33 );
33 );
src/lib/useResetPointCursor.jsdeleted−12
@@ -1,12 +0,0 @@
1−import { useEffect } from 'react';
2−import Maybe from 'folktale/maybe';
3−
4−import { usePointCursor } from 'store/pointCursor';
5−
6−export default function useResetPointCursor() {
7− const { setPointCursor } = usePointCursor();
8−
9− useEffect(() => {
10− setPointCursor(Maybe.Nothing());
11− }, [setPointCursor]);
12−}
src/lib/useWalletType.jsdeleted−11
@@ -1,11 +0,0 @@
1−import { useEffect } from 'react';
2−
3−import { useWallet } from 'store/wallet';
4−
5−export default function useWalletType(walletType) {
6− const { setWalletType } = useWallet();
7−
8− useEffect(() => {
9− setWalletType(walletType);
10− }, [setWalletType, walletType]);
11−}
src/lib/validators.js+58−13
@@ -1,15 +1,23 @@
1import * as bip39 from 'bip39';
1import * as bip39 from 'bip39';
2import * as ob from 'urbit-ob';
2import * as ob from 'urbit-ob';
3−import { identity } from 'lodash';
3+import { identity, includes } from 'lodash';
4
4
5import { isValidAddress } from './wallet';
5import { isValidAddress } from './wallet';
6import patp2dec from './patp2dec';
6import patp2dec from './patp2dec';
7+import { patpStringLength } from './lib';
8+
9+// NOTE: do not use the /g modifier for these regexes
10+// https://stackoverflow.com/a/21373261
11+// https://stackoverflow.com/a/1520853
7
12
8// via: https://emailregex.com/
13// via: https://emailregex.com/
9const 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,}))$/;
14const 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,}))$/;
10
15
11// is this 64 hex chars?
16// is this 64 hex chars?
12−const hexRegExp = /[0-9A-Fa-f]{64}/g;
17+const plain64CharHexValue = /[0-9A-Fa-f]{64}/;
18+
19+// is this only hex values with a 0x prefix?
20+const isHexString = /0x[0-9A-Fa-f]/;
13
21
14// Wraps single validation functions in a controlled and predictable way.
22// Wraps single validation functions in a controlled and predictable way.
15export const simpleValidatorWrapper = ({
23export const simpleValidatorWrapper = ({
@@ -129,30 +137,33 @@ export const validateShard = m =>
129 error: 'This is not a valid shard',
137 error: 'This is not a valid shard',
130 });
138 });
131
139
132−export const validateLength = (m, l) =>
140+export const validateOneOf = (options = []) => m =>
133 simpleValidatorWrapper({
141 simpleValidatorWrapper({
134 prevMessage: m,
142 prevMessage: m,
135− validator: d => {
143+ validator: d => includes(options, d),
136− try {
144+ error: `Is not a valid option.`,
137− return d.length === l;
138− } catch {
139− return false;
140− }
141− },
142− error: 'This is of an invalid length',
143 });
145 });
144
146
147+export const validateHexString = m =>
148+ simpleValidatorWrapper({
149+ prevMessage: m,
150+ validator: d => isHexString.test(d),
151+ error: 'This is not a valid hex string',
152+ });
153+
154+// @deprecate
145export const validateNetworkKey = m =>
155export const validateNetworkKey = m =>
146 simpleValidatorWrapper({
156 simpleValidatorWrapper({
147 prevMessage: m,
157 prevMessage: m,
148− validator: d => hexRegExp.test(d),
158+ validator: d => plain64CharHexValue.test(d),
149 error: 'This is not a valid network key',
159 error: 'This is not a valid network key',
150 });
160 });
151
161
162+// @deprecate
152export const validateNetworkSeed = m =>
163export const validateNetworkSeed = m =>
153 simpleValidatorWrapper({
164 simpleValidatorWrapper({
154 prevMessage: m,
165 prevMessage: m,
155− validator: d => hexRegExp.test(d),
166+ validator: d => plain64CharHexValue.test(d),
156 error: 'This is not a valid network seed',
167 error: 'This is not a valid network seed',
157 });
168 });
158
169
@@ -192,3 +203,37 @@ export const validateExactly = (value, error) => m =>
192 validator: d => d === value,
203 validator: d => d === value,
193 error,
204 error,
194 });
205 });
206+
207+export const validateLength = l => m =>
208+ simpleValidatorWrapper({
209+ prevMessage: m,
210+ validator: d => {
211+ try {
212+ return d.length === l;
213+ } catch {
214+ return false;
215+ }
216+ },
217+ error: `Must be exactly ${l} characters`,
218+ });
219+
220+export const validateMaximumLength = l => m =>
221+ simpleValidatorWrapper({
222+ prevMessage: m,
223+ validator: d => {
224+ try {
225+ return d.length <= l;
226+ } catch {
227+ return false;
228+ }
229+ },
230+ error: `Must be ${l} characters or fewer.`,
231+ });
232+
233+export const validatePatpByteLength = byteLength => {
234+ return validateLength(patpStringLength(byteLength));
235+};
236+
237+export const validateMaximumPatpByteLength = byteLength => {
238+ return validateMaximumLength(patpStringLength(byteLength));
239+};
src/lib/wallet.js+26−59
@@ -2,19 +2,18 @@ import * as bip32 from 'bip32';
2import * as bip39 from 'bip39';
2import * as bip39 from 'bip39';
3import keccak from 'keccak';
3import keccak from 'keccak';
4import { reduce } from 'lodash';
4import { reduce } from 'lodash';
5−import Maybe from 'folktale/maybe';
5+import { Just, Nothing } from 'folktale/maybe';
6import * as secp256k1 from 'secp256k1';
6import * as secp256k1 from 'secp256k1';
7import * as ob from 'urbit-ob';
7import * as ob from 'urbit-ob';
8import * as kg from 'urbit-key-generation/dist';
8import * as kg from 'urbit-key-generation/dist';
9import { isAddress } from 'web3-utils';
9import { isAddress } from 'web3-utils';
10
10
11−const DEFAULT_HD_PATH = "m/44'/60'/0'/0/0";
11+export const DEFAULT_HD_PATH = "m/44'/60'/0'/0/0";
12−
13−const ETH_ZERO_ADDR = '0x0000000000000000000000000000000000000000';
12+export const ETH_ZERO_ADDR = '0x0000000000000000000000000000000000000000';
14−const CURVE_ZERO_ADDR =
13+export const CURVE_ZERO_ADDR =
15 '0x0000000000000000000000000000000000000000000000000000000000000000';
14 '0x0000000000000000000000000000000000000000000000000000000000000000';
16
15
17−const WALLET_TYPES = {
16+export const WALLET_TYPES = {
18 MNEMONIC: Symbol('MNEMONIC'),
17 MNEMONIC: Symbol('MNEMONIC'),
19 TICKET: Symbol('TICKET'),
18 TICKET: Symbol('TICKET'),
20 SHARDS: Symbol('SHARDS'),
19 SHARDS: Symbol('SHARDS'),
@@ -24,31 +23,14 @@ const WALLET_TYPES = {
24 KEYSTORE: Symbol('KEYSTORE'),
23 KEYSTORE: Symbol('KEYSTORE'),
25};
24};
26
25
27−function EthereumWallet(privateKey) {
26+export function EthereumWallet(privateKey) {
28 this.privateKey = privateKey;
27 this.privateKey = privateKey;
29 this.publicKey = secp256k1.publicKeyCreate(this.privateKey);
28 this.publicKey = secp256k1.publicKeyCreate(this.privateKey);
30 const pub = this.publicKey.toString('hex');
29 const pub = this.publicKey.toString('hex');
31 this.address = addressFromSecp256k1Public(pub);
30 this.address = addressFromSecp256k1Public(pub);
32}
31}
33
32
34−const renderWalletType = wallet =>
35− wallet === WALLET_TYPES.MNEMONIC
36− ? 'Mnemonic'
37− : wallet === WALLET_TYPES.TICKET
38− ? 'Ticket'
39− : wallet === WALLET_TYPES.SHARDS
40− ? 'Ticket Shards'
41− : wallet === WALLET_TYPES.LEDGER
42− ? 'Ledger'
43− : wallet === WALLET_TYPES.TREZOR
44− ? 'Trezor'
45− : wallet === WALLET_TYPES.PRIVATE_KEY
46− ? 'Private Key'
47− : wallet === WALLET_TYPES.KEYSTORE
48− ? 'Keystore File'
49− : 'Wallet';
50−
51−const addressFromSecp256k1Public = pub => {
33+export const addressFromSecp256k1Public = pub => {
52 const compressed = false;
34 const compressed = false;
53 const uncompressed = secp256k1.publicKeyConvert(
35 const uncompressed = secp256k1.publicKeyConvert(
54 Buffer.from(pub, 'hex'),
36 Buffer.from(pub, 'hex'),
@@ -60,20 +42,22 @@ const addressFromSecp256k1Public = pub => {
60 return toChecksumAddress(addr);
42 return toChecksumAddress(addr);
61};
43};
62
44
45+export const addHexPrefix = hex =>
63−const addHexPrefix = hex => (hex.slice(0, 2) === '0x' ? hex : '0x' + hex);
46+ hex.slice(0, 2) === '0x' ? hex : '0x' + hex;
64
47
48+export const stripHexPrefix = hex =>
65−const stripHexPrefix = hex => (hex.slice(0, 2) === '0x' ? hex.slice(2) : hex);
49+ hex.slice(0, 2) === '0x' ? hex.slice(2) : hex;
66
50
67−const keccak256 = str =>
51+export const keccak256 = str =>
68 keccak('keccak256')
52 keccak('keccak256')
69 .update(str)
53 .update(str)
70 .digest();
54 .digest();
71
55
72−const isValidAddress = a => '0x0' === a || isAddress(a);
56+export const isValidAddress = a => '0x0' === a || isAddress(a);
73
57
74−const isZeroAddress = a => ETH_ZERO_ADDR === a;
58+export const isZeroAddress = a => ETH_ZERO_ADDR === a;
75
59
76−const toChecksumAddress = address => {
60+export const toChecksumAddress = address => {
77 const addr = stripHexPrefix(address).toLowerCase();
61 const addr = stripHexPrefix(address).toLowerCase();
78 const hash = keccak256(addr).toString('hex');
62 const hash = keccak256(addr).toString('hex');
79
63
@@ -85,27 +69,28 @@ const toChecksumAddress = address => {
85 );
69 );
86};
70};
87
71
88−const eqAddr = (addr0, addr1) =>
72+export const eqAddr = (addr0, addr1) =>
89 toChecksumAddress(addr0) === toChecksumAddress(addr1);
73 toChecksumAddress(addr0) === toChecksumAddress(addr1);
90
74
91−const urbitWalletFromTicket = async (ticket, point, passphrase) => {
75+export const urbitWalletFromTicket = async (ticket, point, passphrase) => {
92 if (typeof point === 'string') {
76 if (typeof point === 'string') {
93 //TODO why not in kg?
77 //TODO why not in kg?
94 point = ob.patp2dec(point);
78 point = ob.patp2dec(point);
95 }
79 }
80+
96− let uhdw = await kg.generateWallet({
81+ return await kg.generateWallet({
97 ticket: ticket,
82 ticket: ticket,
98 ship: point,
83 ship: point,
99 passphrase: passphrase,
84 passphrase: passphrase,
100 });
85 });
101− return uhdw;
102};
86};
103
87
104−const ownershipWalletFromTicket = async (ticket, point, passphrase) => {
88+export const ownershipWalletFromTicket = async (ticket, point, passphrase) => {
105 if (typeof point === 'string') {
89 if (typeof point === 'string') {
106 //TODO why not in kg?
90 //TODO why not in kg?
107 point = ob.patp2dec(point);
91 point = ob.patp2dec(point);
108 }
92 }
93+
109 return await kg.generateOwnershipWallet({
94 return await kg.generateOwnershipWallet({
110 ticket,
95 ticket,
111 ship: point,
96 ship: point,
@@ -113,10 +98,10 @@ const ownershipWalletFromTicket = async (ticket, point, passphrase) => {
113 });
98 });
114};
99};
115
100
116−const walletFromMnemonic = (mnemonic, hdpath, passphrase) => {
101+export const walletFromMnemonic = (mnemonic, hdpath, passphrase) => {
117 const seed = bip39.validateMnemonic(mnemonic)
102 const seed = bip39.validateMnemonic(mnemonic)
118− ? Maybe.Just(bip39.mnemonicToSeed(mnemonic, passphrase))
103+ ? Just(bip39.mnemonicToSeed(mnemonic, passphrase))
119− : Maybe.Nothing();
104+ : Nothing();
120
105
121 const toWallet = (sd, path) => {
106 const toWallet = (sd, path) => {
122 let wal;
107 let wal;
@@ -124,9 +109,9 @@ const walletFromMnemonic = (mnemonic, hdpath, passphrase) => {
124 const hd = bip32.fromSeed(sd);
109 const hd = bip32.fromSeed(sd);
125 wal = hd.derivePath(path);
110 wal = hd.derivePath(path);
126 wal.address = addressFromSecp256k1Public(wal.publicKey);
111 wal.address = addressFromSecp256k1Public(wal.publicKey);
127− wal = Maybe.Just(wal);
112+ wal = Just(wal);
128 } catch (_) {
113 } catch (_) {
129− wal = Maybe.Nothing();
114+ wal = Nothing();
130 }
115 }
131 return wal;
116 return wal;
132 };
117 };
@@ -134,21 +119,3 @@ const walletFromMnemonic = (mnemonic, hdpath, passphrase) => {
134 const wallet = seed.chain(sd => toWallet(sd, hdpath));
119 const wallet = seed.chain(sd => toWallet(sd, hdpath));
135 return wallet;
120 return wallet;
136};
121};
137−
138−export {
139− DEFAULT_HD_PATH,
140− WALLET_TYPES,
141− ETH_ZERO_ADDR,
142− CURVE_ZERO_ADDR,
143− renderWalletType,
144− isValidAddress,
145− isZeroAddress,
146− toChecksumAddress,
147− addressFromSecp256k1Public,
148− eqAddr,
149− urbitWalletFromTicket,
150− ownershipWalletFromTicket,
151− walletFromMnemonic,
152− addHexPrefix,
153− EthereumWallet,
154−};
src/store/lib/useBirthdaysStore.js+7−8
@@ -1,5 +1,5 @@
1import { useCallback, useState } from 'react';
1import { useCallback, useState } from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Just, Nothing } from 'folktale/maybe';
3import * as azimuth from 'azimuth-js';
3import * as azimuth from 'azimuth-js';
4
4
5import { useNetwork } from '../network';
5import { useNetwork } from '../network';
@@ -17,10 +17,9 @@ export default function useBirthdayStore() {
17 [_setBirthdayCache]
17 [_setBirthdayCache]
18 );
18 );
19
19
20− const getBirthday = useCallback(
21− point => birthdayCache[point] || Maybe.Nothing(),
20+ const getBirthday = useCallback(point => birthdayCache[point] || Nothing(), [
22− [birthdayCache]
21+ birthdayCache,
23− );
22+ ]);
24
23
25 const syncBirthday = useCallback(
24 const syncBirthday = useCallback(
26 async point => {
25 async point => {
@@ -31,7 +30,7 @@ export default function useBirthdayStore() {
31 }
30 }
32
31
33 // birthday will not change after being set, so bail if we already know
32 // birthday will not change after being set, so bail if we already know
34− if (Maybe.Just.hasInstance(getBirthday(point))) {
33+ if (Just.hasInstance(getBirthday(point))) {
35 return;
34 return;
36 }
35 }
37
36
@@ -43,13 +42,13 @@ export default function useBirthdayStore() {
43 if (birthBlock > 0) {
42 if (birthBlock > 0) {
44 const block = await _web3.eth.getBlock(birthBlock);
43 const block = await _web3.eth.getBlock(birthBlock);
45 addToBirthdayCache({
44 addToBirthdayCache({
46− [point]: Maybe.Just(new Date(block.timestamp * 1000)),
45+ [point]: Just(new Date(block.timestamp * 1000)),
47 });
46 });
48 } else {
47 } else {
49 // TODO: better encoding for "no birthday" state?
48 // TODO: better encoding for "no birthday" state?
50 // if there's no birthday, just use today
49 // if there's no birthday, just use today
51 addToBirthdayCache({
50 addToBirthdayCache({
52− [point]: Maybe.Just(new Date()),
51+ [point]: Just(new Date()),
53 });
52 });
54 }
53 }
55 },
54 },
src/store/lib/useControlledPointsStore.js+11−11
@@ -1,16 +1,16 @@
1import { useState, useCallback } from 'react';
1import { useState, useCallback } from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Just, Nothing } from 'folktale/maybe';
3import * as azimuth from 'azimuth-js';
3import * as azimuth from 'azimuth-js';
4
4
5import { useNetwork } from '../network';
5import { useNetwork } from '../network';
6import { useWallet } from 'store/wallet';
6import { useWallet } from 'store/wallet';
7
7
8const EMPTY_CONTROLLED_POINTS = {
8const EMPTY_CONTROLLED_POINTS = {
9− ownedPoints: Maybe.Nothing(),
9+ ownedPoints: Nothing(),
10− incomingPoints: Maybe.Nothing(),
10+ incomingPoints: Nothing(),
11− managingPoints: Maybe.Nothing(),
11+ managingPoints: Nothing(),
12− votingPoints: Maybe.Nothing(),
12+ votingPoints: Nothing(),
13− spawningPoints: Maybe.Nothing(),
13+ spawningPoints: Nothing(),
14};
14};
15
15
16export default function useControlledPointsStore() {
16export default function useControlledPointsStore() {
@@ -49,11 +49,11 @@ export default function useControlledPointsStore() {
49 ]);
49 ]);
50
50
51 _setControlledPointsCache({
51 _setControlledPointsCache({
52− ownedPoints: Maybe.Just(ownedPoints),
52+ ownedPoints: Just(ownedPoints),
53− incomingPoints: Maybe.Just(incomingPoints),
53+ incomingPoints: Just(incomingPoints),
54− managingPoints: Maybe.Just(managingPoints),
54+ managingPoints: Just(managingPoints),
55− votingPoints: Maybe.Just(votingPoints),
55+ votingPoints: Just(votingPoints),
56− spawningPoints: Maybe.Just(spawningPoints),
56+ spawningPoints: Just(spawningPoints),
57 });
57 });
58 }, [contracts, wallet, _setControlledPointsCache]);
58 }, [contracts, wallet, _setControlledPointsCache]);
59
59
src/store/lib/useDetailsStore.js+2−3
@@ -1,5 +1,5 @@
1import { useState, useCallback } from 'react';
1import { useState, useCallback } from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Just, Nothing } from 'folktale/maybe';
3import * as azimuth from 'azimuth-js';
3import * as azimuth from 'azimuth-js';
4
4
5import { useNetwork } from '../network';
5import { useNetwork } from '../network';
@@ -20,8 +20,7 @@ export default function useDetailsStore() {
20 // TODO: refactor detailsCache access to use accessor like bithday
20 // TODO: refactor detailsCache access to use accessor like bithday
21 // Maybe<{}>
21 // Maybe<{}>
22 const getDetails = useCallback(
22 const getDetails = useCallback(
23− point =>
24− point in detailsCache ? Maybe.Just(detailsCache[point]) : Maybe.Nothing(),
23+ point => (point in detailsCache ? Just(detailsCache[point]) : Nothing()),
25 [detailsCache]
24 [detailsCache]
26 );
25 );
27
26
src/store/lib/useInvitesStore.js+7−7
@@ -1,14 +1,14 @@
1import { useCallback, useState } from 'react';
1import { useCallback, useState } from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Just, Nothing } from 'folktale/maybe';
3import * as azimuth from 'azimuth-js';
3import * as azimuth from 'azimuth-js';
4
4
5import { useNetwork } from '../network';
5import { useNetwork } from '../network';
6
6
7// the default value of a point's invites
7// the default value of a point's invites
8const EMPTY_INVITES = {
8const EMPTY_INVITES = {
9− availableInvites: Maybe.Nothing(),
9+ availableInvites: Nothing(),
10− sentInvites: Maybe.Nothing(),
10+ sentInvites: Nothing(),
11− acceptedInvites: Maybe.Nothing(),
11+ acceptedInvites: Nothing(),
12};
12};
13
13
14export default function useInvitesStore() {
14export default function useInvitesStore() {
@@ -52,9 +52,9 @@ export default function useInvitesStore() {
52
52
53 addToInvitesCache({
53 addToInvitesCache({
54 [point]: {
54 [point]: {
55− availableInvites: Maybe.Just(availableInvites),
55+ availableInvites: Just(availableInvites),
56− sentInvites: Maybe.Just(sentInvites),
56+ sentInvites: Just(sentInvites),
57− acceptedInvites: Maybe.Just(acceptedInvites),
57+ acceptedInvites: Just(acceptedInvites),
58 },
58 },
59 });
59 });
60 },
60 },
src/store/lib/useRekeyDateStore.js+1−2
@@ -1,6 +1,5 @@
1import { useCallback, useState } from 'react';
1import { useCallback, useState } from 'react';
2import { Just, Nothing } from 'folktale/maybe';
2import { Just, Nothing } from 'folktale/maybe';
3−import * as azimuth from 'azimuth-js';
4
3
5import { useNetwork } from '../network';
4import { useNetwork } from '../network';
6
5
@@ -56,7 +55,7 @@ export default function useRekeyDateStore() {
56 });
55 });
57 }
56 }
58 },
57 },
59− [web3, addToRekeyDateCache]
58+ [web3, contracts, addToRekeyDateCache]
60 );
59 );
61
60
62 return { getRekeyDate, syncRekeyDate };
61 return { getRekeyDate, syncRekeyDate };
src/store/network.js+5−5
@@ -5,9 +5,9 @@ import React, {
5 useMemo,
5 useMemo,
6 useState,
6 useState,
7} from 'react';
7} from 'react';
8+import { Just, Nothing } from 'folktale/maybe';
8import * as azimuth from 'azimuth-js';
9import * as azimuth from 'azimuth-js';
9import Web3 from 'web3';
10import Web3 from 'web3';
10−import Maybe from 'folktale/maybe';
11import { includes } from 'lodash';
11import { includes } from 'lodash';
12
12
13import { CONTRACT_ADDRESSES } from '../lib/contracts';
13import { CONTRACT_ADDRESSES } from '../lib/contracts';
@@ -33,8 +33,8 @@ function _useNetwork(initialNetworkType = null) {
33 const contracts = azimuth.initContracts(web3, contractAddresses);
33 const contracts = azimuth.initContracts(web3, contractAddresses);
34
34
35 return {
35 return {
36− web3: Maybe.Just(web3),
36+ web3: Just(web3),
37− contracts: Maybe.Just(contracts),
37+ contracts: Just(contracts),
38 };
38 };
39 };
39 };
40
40
@@ -82,8 +82,8 @@ function _useNetwork(initialNetworkType = null) {
82 new Web3.providers.HttpProvider('http://example.com:3456'),
82 new Web3.providers.HttpProvider('http://example.com:3456'),
83 isDevelopment ? CONTRACT_ADDRESSES.DEV : CONTRACT_ADDRESSES.MAINNET
83 isDevelopment ? CONTRACT_ADDRESSES.DEV : CONTRACT_ADDRESSES.MAINNET
84 ),
84 ),
85− web3: Maybe.Nothing(),
85+ web3: Nothing(),
86− // ^ overwrite the web3 object from initWeb3 with a Maybe.Nothing
86+ // ^ overwrite the web3 object from initWeb3 with a Nothing
87 // to indicate that there is no valid web3 connection
87 // to indicate that there is no valid web3 connection
88 };
88 };
89 }
89 }
src/store/pointCursor.js+2−2
@@ -1,10 +1,10 @@
1import React, { createContext, forwardRef, useContext, useState } from 'react';
1import React, { createContext, forwardRef, useContext, useState } from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Nothing } from 'folktale/maybe';
3
3
4export const PointCursorContext = createContext(null);
4export const PointCursorContext = createContext(null);
5
5
6// pointCursor is Maybe<number>
6// pointCursor is Maybe<number>
7−function _usePointCursor(initialPointCursor = Maybe.Nothing()) {
7+function _usePointCursor(initialPointCursor = Nothing()) {
8 const [pointCursor, setPointCursor] = useState(initialPointCursor);
8 const [pointCursor, setPointCursor] = useState(initialPointCursor);
9
9
10 return {
10 return {
src/store/wallet.js+13−16
@@ -5,7 +5,7 @@ import React, {
5 useContext,
5 useContext,
6 useState,
6 useState,
7} from 'react';
7} from 'react';
8−import Maybe from 'folktale/maybe';
8+import { Just, Nothing } from 'folktale/maybe';
9import { includes } from 'lodash';
9import { includes } from 'lodash';
10
10
11import {
11import {
@@ -28,21 +28,18 @@ const DEFAULT_WALLET_TYPE = WALLET_TYPES.TICKET;
28// it doesn't necessarily help us much, since we branch on 'walletType'
28// it doesn't necessarily help us much, since we branch on 'walletType'
29// before setting 'wallet'.
29// before setting 'wallet'.
30//
30//
31−// Wallets are always wrapped in Maybe. For most authentication
31+// Wallets are always wrapped in For most authentication
32// mechanisms, Maybe wraps a BIP32 HD wallet; for Ethereum private keys,
32// mechanisms, Maybe wraps a BIP32 HD wallet; for Ethereum private keys,
33// JSON keystore files, and Metamask authentication, it wraps an
33// JSON keystore files, and Metamask authentication, it wraps an
34// 'EthereumWallet'.
34// 'EthereumWallet'.
35−function _useWallet(
36− initialWallet = Maybe.Nothing(),
37− initialMnemonic = Maybe.Nothing()
35+function _useWallet(initialWallet = Nothing(), initialMnemonic = Nothing()) {
38−) {
39 const [walletType, _setWalletType] = useState(DEFAULT_WALLET_TYPE);
36 const [walletType, _setWalletType] = useState(DEFAULT_WALLET_TYPE);
40 const [walletHdPath, setWalletHdPath] = useState(DEFAULT_HD_PATH);
37 const [walletHdPath, setWalletHdPath] = useState(DEFAULT_HD_PATH);
41 const [wallet, _setWallet] = useState(initialWallet);
38 const [wallet, _setWallet] = useState(initialWallet);
42− const [urbitWallet, _setUrbitWallet] = useState(Maybe.Nothing());
39+ const [urbitWallet, _setUrbitWallet] = useState(Nothing());
43 const [authMnemonic, setAuthMnemonic] = useState(initialMnemonic);
40 const [authMnemonic, setAuthMnemonic] = useState(initialMnemonic);
44− const [networkSeed, setNetworkSeed] = useState(Maybe.Nothing());
41+ const [networkSeed, setNetworkSeed] = useState(Nothing());
45− const [networkRevision, setNetworkRevision] = useState(Maybe.Nothing());
42+ const [networkRevision, setNetworkRevision] = useState(Nothing());
46
43
47 const setWalletType = useCallback(
44 const setWalletType = useCallback(
48 walletType => {
45 walletType => {
@@ -70,7 +67,7 @@ function _useWallet(
70
67
71 const setUrbitWallet = useCallback(
68 const setUrbitWallet = useCallback(
72 urbitWallet => {
69 urbitWallet => {
73− if (Maybe.Just.hasInstance(urbitWallet)) {
70+ if (Just.hasInstance(urbitWallet)) {
74 // when an urbit wallet is set, also derive
71 // when an urbit wallet is set, also derive
75 // a normal bip32 wallet using the ownership address
72 // a normal bip32 wallet using the ownership address
76 const wallet = walletFromMnemonic(
73 const wallet = walletFromMnemonic(
@@ -81,7 +78,7 @@ function _useWallet(
81
78
82 setWallet(wallet);
79 setWallet(wallet);
83 } else {
80 } else {
84− setWallet(Maybe.Nothing());
81+ setWallet(Nothing());
85 }
82 }
86
83
87 _setUrbitWallet(urbitWallet);
84 _setUrbitWallet(urbitWallet);
@@ -92,11 +89,11 @@ function _useWallet(
92 const resetWallet = useCallback(() => {
89 const resetWallet = useCallback(() => {
93 _setWalletType(DEFAULT_WALLET_TYPE);
90 _setWalletType(DEFAULT_WALLET_TYPE);
94 setWalletHdPath(DEFAULT_HD_PATH);
91 setWalletHdPath(DEFAULT_HD_PATH);
95− _setWallet(Maybe.Nothing());
92+ _setWallet(Nothing());
96− _setUrbitWallet(Maybe.Nothing());
93+ _setUrbitWallet(Nothing());
97− setAuthMnemonic(Maybe.Nothing());
94+ setAuthMnemonic(Nothing());
98− setNetworkSeed(Maybe.Nothing());
95+ setNetworkSeed(Nothing());
99− setNetworkRevision(Maybe.Nothing());
96+ setNetworkRevision(Nothing());
100 }, [
97 }, [
101 _setWalletType,
98 _setWalletType,
102 setWalletHdPath,
99 setWalletHdPath,
src/style/indigo-proposals.scss+4
@@ -39,3 +39,7 @@
39.auto-rows-min {
39.auto-rows-min {
40 grid-auto-rows: min-content;
40 grid-auto-rows: min-content;
41}
41}
42+
43+.hover-bg-grey3:hover {
44+ background-color: #e6e6e6;
45+}
src/views/Activate/ActivateCode.js+7−8
@@ -1,7 +1,6 @@
1import React, { useCallback, useEffect, useState } from 'react';
1import React, { useCallback, useEffect, useState } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
2import * as azimuth from 'azimuth-js';
3import * as azimuth from 'azimuth-js';
3−import Maybe from 'folktale/maybe';
4−import { useTicketInput } from 'components/Inputs';
5import { Grid, Input, H4, ErrorText } from 'indigo-react';
4import { Grid, Input, H4, ErrorText } from 'indigo-react';
6
5
7import View from 'components/View';
6import View from 'components/View';
@@ -10,6 +9,7 @@ import Passport from 'components/Passport';
10
9
11import { useHistory } from 'store/history';
10import { useHistory } from 'store/history';
12
11
12+import { useTicketInput } from 'lib/useInputs';
13import * as need from 'lib/need';
13import * as need from 'lib/need';
14import { ROUTE_NAMES } from 'lib/routeNames';
14import { ROUTE_NAMES } from 'lib/routeNames';
15import { useSyncKnownPoints } from 'lib/useSyncPoints';
15import { useSyncKnownPoints } from 'lib/useSyncPoints';
@@ -36,12 +36,11 @@ export default function ActivateCode() {
36 setDerivedPoint,
36 setDerivedPoint,
37 } = useActivateFlow();
37 } = useActivateFlow();
38
38
39− const ticketInput = useTicketInput({
39+ const [ticketInput, { pass: validTicket }] = useTicketInput({
40 name: 'ticket',
40 name: 'ticket',
41 label: 'Code',
41 label: 'Code',
42 autoFocus: true,
42 autoFocus: true,
43 });
43 });
44− const validTicket = ticketInput.pass;
45
44
46 const goToLogin = useCallback(() => history.popAndPush(ROUTE_NAMES.LOGIN), [
45 const goToLogin = useCallback(() => history.popAndPush(ROUTE_NAMES.LOGIN), [
47 history,
46 history,
@@ -81,13 +80,13 @@ export default function ActivateCode() {
81 );
80 );
82 const incoming = [...owned, ...transferring];
81 const incoming = [...owned, ...transferring];
83
82
84− let realPoint = Maybe.Nothing();
83+ let realPoint = Nothing();
85− let wallet = Maybe.Nothing();
84+ let wallet = Nothing();
86
85
87 if (incoming.length > 0) {
86 if (incoming.length > 0) {
88 const pointNum = parseInt(incoming[0], 10);
87 const pointNum = parseInt(incoming[0], 10);
89− realPoint = Maybe.Just(pointNum);
88+ realPoint = Just(pointNum);
90− wallet = Maybe.Just(await generateWallet(pointNum));
89+ wallet = Just(await generateWallet(pointNum));
91
90
92 if (incoming.length > 1) {
91 if (incoming.length > 1) {
93 setGeneralError(
92 setGeneralError(
src/views/Activate/ActivateDisclaimer.js+6−5
@@ -2,17 +2,18 @@ import React, { useCallback } from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3import { Grid, H3, B, Text, CheckboxInput } from 'indigo-react';
3import { Grid, H3, B, Text, CheckboxInput } from 'indigo-react';
4
4
5+import { useCheckboxInput } from 'lib/useInputs';
6+import { useLocalRouter } from 'lib/LocalRouter';
7+
5import View from 'components/View';
8import View from 'components/View';
6−import { useCheckboxInput } from 'components/Inputs';
7import { ForwardButton } from 'components/Buttons';
9import { ForwardButton } from 'components/Buttons';
8−import { useLocalRouter } from 'lib/LocalRouter';
9import WarningBox from 'components/WarningBox';
10import WarningBox from 'components/WarningBox';
10
11
11const TEXT_STYLE = 'f5';
12const TEXT_STYLE = 'f5';
12
13
13export default function ActivateDisclaimer() {
14export default function ActivateDisclaimer() {
14 const { push, names } = useLocalRouter();
15 const { push, names } = useLocalRouter();
15− const checkboxInput = useCheckboxInput({
16+ const [understoodInput, { data: isUnderstood }] = useCheckboxInput({
16 name: 'checkbox',
17 name: 'checkbox',
17 label: 'I acknowledge and understand these rights',
18 label: 'I acknowledge and understand these rights',
18 });
19 });
@@ -70,10 +71,10 @@ export default function ActivateDisclaimer() {
70 <Grid.Item full as={WarningBox}>
71 <Grid.Item full as={WarningBox}>
71 Warning: Nobody but you can restore or reset your Master Ticket
72 Warning: Nobody but you can restore or reset your Master Ticket
72 </Grid.Item>
73 </Grid.Item>
73− <Grid.Item as={CheckboxInput} {...checkboxInput} full />
74+ <Grid.Item as={CheckboxInput} {...understoodInput} full />
74 <Grid.Item
75 <Grid.Item
75 as={ForwardButton}
76 as={ForwardButton}
76− disabled={!checkboxInput.data}
77+ disabled={!isUnderstood}
77 onClick={goToPassport}
78 onClick={goToPassport}
78 solid
79 solid
79 full>
80 full>
src/views/Activate/ActivatePassport.js+3−4
@@ -46,8 +46,7 @@ export default function ActivatePassport() {
46 const gap = useBreakpoints([4, 4, 7]);
46 const gap = useBreakpoints([4, 4, 7]);
47 const marginTop = useBreakpoints([false, false, 8]);
47 const marginTop = useBreakpoints([false, false, 8]);
48 const full = useBreakpoints([true, true, false]);
48 const full = useBreakpoints([true, true, false]);
49− const leftHalf = useBreakpoints([false, false, 1]);
50− const rightHalf = useBreakpoints([false, false, 2]);
49+ const half = useBreakpoints([false, false, true]);
51
50
52 // addresses are not derived until we set[Urbit]Wallet(), so do that inline
51 // addresses are not derived until we set[Urbit]Wallet(), so do that inline
53 const address = useMemo(
52 const address = useMemo(
@@ -68,7 +67,7 @@ export default function ActivatePassport() {
68 <LocalRouterProvider value={router}>
67 <LocalRouterProvider value={router}>
69 <View full={fullView}>
68 <View full={fullView}>
70 <Grid gap={gap} className="mt8 mb10">
69 <Grid gap={gap} className="mt8 mb10">
71− <Grid.Item half={leftHalf} full={full}>
70+ <Grid.Item half={half && 1} full={full}>
72 <Passport
71 <Passport
73 className={cn({ [`mt${marginTop}`]: marginTop })}
72 className={cn({ [`mt${marginTop}`]: marginTop })}
74 point={derivedPoint}
73 point={derivedPoint}
@@ -77,7 +76,7 @@ export default function ActivatePassport() {
77 />
76 />
78 </Grid.Item>
77 </Grid.Item>
79 <Grid.Item
78 <Grid.Item
80− half={rightHalf}
79+ half={half && 2}
81 full={full}
80 full={full}
82 as={Route}
81 as={Route}
83 resetActivateRouter={resetActivateRouter}
82 resetActivateRouter={resetActivateRouter}
src/views/Activate/PassportDownload.js+3−3
@@ -1,6 +1,6 @@
1import React, { useState, useEffect, useCallback } from 'react';
1import React, { useState, useEffect, useCallback } from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3−import Maybe from 'folktale/maybe';
3+import { Just, Nothing } from 'folktale/maybe';
4import { Grid, H4, P } from 'indigo-react';
4import { Grid, H4, P } from 'indigo-react';
5
5
6import * as need from 'lib/need';
6import * as need from 'lib/need';
@@ -23,7 +23,7 @@ export default function PassportDownload({ className }) {
23 // const point = need.point(derivedPoint);
23 // const point = need.point(derivedPoint);
24 const wallet = need.wallet(derivedWallet);
24 const wallet = need.wallet(derivedWallet);
25
25
26− const [paper, setPaper] = useState(Maybe.Nothing());
26+ const [paper, setPaper] = useState(Nothing());
27 const [downloaded, setDownloaded] = useState(false);
27 const [downloaded, setDownloaded] = useState(false);
28
28
29 const pointAsString = derivedPoint.matchWith({
29 const pointAsString = derivedPoint.matchWith({
@@ -79,7 +79,7 @@ export default function PassportDownload({ className }) {
79 <PaperRenderer
79 <PaperRenderer
80 point={pointAsString}
80 point={pointAsString}
81 wallet={wallet}
81 wallet={wallet}
82− callback={data => setPaper(Maybe.Just(data))}
82+ callback={data => setPaper(Just(data))}
83 />
83 />
84 </Grid>
84 </Grid>
85 );
85 );
src/views/Activate/PassportVerify.js+3−4
@@ -4,13 +4,13 @@ import { Grid, H4, Input, P } from 'indigo-react';
4
4
5import * as need from 'lib/need';
5import * as need from 'lib/need';
6import { useLocalRouter } from 'lib/LocalRouter';
6import { useLocalRouter } from 'lib/LocalRouter';
7+import { useTicketInput } from 'lib/useInputs';
8+import { validateExactly } from 'lib/validators';
7
9
8import { ForwardButton } from 'components/Buttons';
10import { ForwardButton } from 'components/Buttons';
9import Steps from 'components/Steps';
11import Steps from 'components/Steps';
10−import { useTicketInput } from 'components/Inputs';
11
12
12import { useActivateFlow } from './ActivateFlow';
13import { useActivateFlow } from './ActivateFlow';
13−import { validateExactly } from 'lib/validators';
14
14
15const STUB_VERIFY_TICKET = process.env.NODE_ENV === 'development';
15const STUB_VERIFY_TICKET = process.env.NODE_ENV === 'development';
16
16
@@ -24,14 +24,13 @@ export default function PassportVerify({ className }) {
24 () => [validateExactly(ticket, 'Does not match expected master ticket.')],
24 () => [validateExactly(ticket, 'Does not match expected master ticket.')],
25 [ticket]
25 [ticket]
26 );
26 );
27− const ticketInput = useTicketInput({
27+ const [ticketInput, { pass }] = useTicketInput({
28 name: 'ticket',
28 name: 'ticket',
29 label: 'Master Ticket',
29 label: 'Master Ticket',
30 initialValue: STUB_VERIFY_TICKET ? ticket : undefined,
30 initialValue: STUB_VERIFY_TICKET ? ticket : undefined,
31 autoFocus: true,
31 autoFocus: true,
32 validators,
32 validators,
33 });
33 });
34− const { pass } = ticketInput;
35
34
36 return (
35 return (
37 <Grid className={cn(className, 'auto-rows-min')}>
36 <Grid className={cn(className, 'auto-rows-min')}>
src/views/Activate/useActivateFlowState.js+7−7
@@ -1,16 +1,16 @@
1import { useCallback, useState } from 'react';
1import { useCallback, useState } from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Nothing } from 'folktale/maybe';
3
3
4export default function useActivateFlowState() {
4export default function useActivateFlowState() {
5− const [derivedWallet, setDerivedWallet] = useState(Maybe.Nothing());
5+ const [derivedWallet, setDerivedWallet] = useState(Nothing());
6− const [inviteWallet, setInviteWallet] = useState(Maybe.Nothing());
6+ const [inviteWallet, setInviteWallet] = useState(Nothing());
7− const [derivedPoint, setDerivedPoint] = useState(Maybe.Nothing());
7+ const [derivedPoint, setDerivedPoint] = useState(Nothing());
8 const [generated, setGenerated] = useState(false);
8 const [generated, setGenerated] = useState(false);
9
9
10 const reset = useCallback(() => {
10 const reset = useCallback(() => {
11− setDerivedWallet(Maybe.Nothing());
11+ setDerivedWallet(Nothing());
12− setInviteWallet(Maybe.Nothing());
12+ setInviteWallet(Nothing());
13− setDerivedPoint(Maybe.Nothing());
13+ setDerivedPoint(Nothing());
14 setGenerated(false);
14 setGenerated(false);
15 }, [setDerivedWallet, setInviteWallet, setDerivedPoint, setGenerated]);
15 }, [setDerivedWallet, setInviteWallet, setDerivedPoint, setGenerated]);
16
16
src/views/Admin/Reticket/ReticketVerify.jsnew file+47
@@ -0,0 +1,47 @@
1+import React, { useCallback, useMemo } from 'react';
2+import { Input, Text, Grid } from 'indigo-react';
3+
4+import { ForwardButton } from 'components/Buttons';
5+
6+import { useTicketInput } from 'lib/useInputs';
7+import { useLocalRouter } from 'lib/LocalRouter';
8+import { validateExactly } from 'lib/validators';
9+import { isDevelopment } from 'lib/flags';
10+
11+const STUB_VERIFY_TICKET = isDevelopment;
12+
13+export default function ReticketVerify({ newWallet }) {
14+ const { push, names } = useLocalRouter();
15+
16+ const ticket = newWallet.value.wallet.ticket;
17+ const validators = useMemo(
18+ () => [validateExactly(ticket, 'Does not match expected master ticket.')],
19+ [ticket]
20+ );
21+ const [ticketInput, { pass }] = useTicketInput({
22+ name: 'ticket',
23+ label: 'New master ticket',
24+ initialValue: STUB_VERIFY_TICKET ? ticket : undefined,
25+ autoFocus: true,
26+ validators,
27+ });
28+
29+ const goExecute = useCallback(() => push(names.EXECUTE), [push, names]);
30+
31+ return (
32+ <Grid gap={4} className="mt4">
33+ <Grid.Item full as={Text}>
34+ Verify New Master Ticket
35+ </Grid.Item>
36+ <Grid.Item full as={Input} {...ticketInput} />
37+ <Grid.Item
38+ full
39+ as={ForwardButton}
40+ solid
41+ disabled={!pass}
42+ onClick={goExecute}>
43+ Verify & Reticket
44+ </Grid.Item>
45+ </Grid>
46+ );
47+}
src/views/CancelTransfer.js+5−4
@@ -1,8 +1,9 @@
1−import Maybe from 'folktale/maybe';
2import React from 'react';
1import React from 'react';
3−import { H1, P } from '../components/old/Base';
2+import { Just } from 'folktale/maybe';
4import * as azimuth from 'azimuth-js';
3import * as azimuth from 'azimuth-js';
5import * as ob from 'urbit-ob';
4import * as ob from 'urbit-ob';
5+
6+import { H1, P } from '../components/old/Base';
6import * as need from '../lib/need';
7import * as need from '../lib/need';
7
8
8import StatelessTransaction from '../components/old/StatelessTransaction';
9import StatelessTransaction from '../components/old/StatelessTransaction';
@@ -40,13 +41,13 @@ class CancelTransfer extends React.Component {
40 ETH_ZERO_ADDR
41 ETH_ZERO_ADDR
41 );
42 );
42
43
43− return Maybe.Just(txn);
44+ return Just(txn);
44 }
45 }
45
46
46 render() {
47 render() {
47 const { props, state } = this;
48 const { props, state } = this;
48
49
49− const online = Maybe.Just.hasInstance(props.web3);
50+ const online = Just.hasInstance(props.web3);
50
51
51 const proxy = online
52 const proxy = online
52 ? props.pointCache[state.pointInTransfer].transferProxy
53 ? props.pointCache[state.pointInTransfer].transferProxy
src/views/CreateGalaxy.js+6−6
@@ -1,5 +1,5 @@
1import React from 'react';
1import React from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Just, Nothing } from 'folktale/maybe';
3import * as azimuth from 'azimuth-js';
3import * as azimuth from 'azimuth-js';
4import * as ob from 'urbit-ob';
4import * as ob from 'urbit-ob';
5import * as need from '../lib/need';
5import * as need from '../lib/need';
@@ -68,10 +68,10 @@ class CreateGalaxy extends React.Component {
68
68
69 createUnsignedTxn = () => {
69 createUnsignedTxn = () => {
70 const { state, props } = this;
70 const { state, props } = this;
71− if (isValidAddress(state.galaxyOwner) === false) return Maybe.Nothing();
71+ if (isValidAddress(state.galaxyOwner) === false) return Nothing();
72− if (state.isAvailable === false) return Maybe.Nothing();
72+ if (state.isAvailable === false) return Nothing();
73− if (canDecodePatp(state.galaxyName) === false) return Maybe.Nothing();
73+ if (canDecodePatp(state.galaxyName) === false) return Nothing();
74− if (isValidGalaxy(state.galaxyName) === false) return Maybe.Nothing();
74+ if (isValidGalaxy(state.galaxyName) === false) return Nothing();
75
75
76 const validContracts = need.contracts(props.contracts);
76 const validContracts = need.contracts(props.contracts);
77
77
@@ -83,7 +83,7 @@ class CreateGalaxy extends React.Component {
83 state.galaxyOwner
83 state.galaxyOwner
84 );
84 );
85
85
86− return Maybe.Just(txn);
86+ return Just(txn);
87 };
87 };
88
88
89 confirmAvailability = async () => {
89 confirmAvailability = async () => {
src/views/Invite/InviteEmail.js+15−15
@@ -1,6 +1,6 @@
1import React, { useCallback, useEffect, useMemo, useState } from 'react';
1import React, { useCallback, useEffect, useMemo, useState } from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3−import Maybe from 'folktale/maybe';
3+import { Just } from 'folktale/maybe';
4import * as azimuth from 'azimuth-js';
4import * as azimuth from 'azimuth-js';
5import {
5import {
6 Grid,
6 Grid,
@@ -11,11 +11,14 @@ import {
11 Text,
11 Text,
12 ErrorText,
12 ErrorText,
13 AccessoryIcon,
13 AccessoryIcon,
14+ useForm,
14} from 'indigo-react';
15} from 'indigo-react';
15import { uniq } from 'lodash';
16import { uniq } from 'lodash';
16
17
17−import * as need from 'lib/need';
18+import { usePointCursor } from 'store/pointCursor';
18−import * as wg from 'lib/walletgen';
19+import { useNetwork } from 'store/network';
20+import { usePointCache } from 'store/pointCache';
21+import { useWallet } from 'store/wallet';
19
22
20// for transaction generation and signing
23// for transaction generation and signing
21import {
24import {
@@ -28,22 +31,19 @@ import {
28} from 'lib/txn';
31} from 'lib/txn';
29import * as tank from 'lib/tank';
32import * as tank from 'lib/tank';
30import { useLocalRouter } from 'lib/LocalRouter';
33import { useLocalRouter } from 'lib/LocalRouter';
31−
32−import MiniBackButton from 'components/MiniBackButton';
33−import { usePointCursor } from 'store/pointCursor';
34−import LoadableButton from 'components/LoadableButton';
35import useArray from 'lib/useArray';
34import useArray from 'lib/useArray';
36−import useForm from 'indigo-react/lib/useForm';
37−import { buildEmailInputConfig } from 'components/Inputs';
35+import { buildEmailInputConfig } from 'lib/useInputs';
38import { MIN_PLANET, GAS_LIMITS } from 'lib/constants';
36import { MIN_PLANET, GAS_LIMITS } from 'lib/constants';
39−import Highlighted from 'components/Highlighted';
37+import * as need from 'lib/need';
40−import { useNetwork } from 'store/network';
38+import * as wg from 'lib/walletgen';
41−import { usePointCache } from 'store/pointCache';
42−import { useWallet } from 'store/wallet';
43import useSetState from 'lib/useSetState';
39import useSetState from 'lib/useSetState';
44import pluralize from 'lib/pluralize';
40import pluralize from 'lib/pluralize';
45import useMailer from 'lib/useMailer';
41import useMailer from 'lib/useMailer';
46
42
43+import MiniBackButton from 'components/MiniBackButton';
44+import LoadableButton from 'components/LoadableButton';
45+import Highlighted from 'components/Highlighted';
46+
47const GAS_PRICE_GWEI = 20; // we pay the premium for faster ux
47const GAS_PRICE_GWEI = 20; // we pay the premium for faster ux
48const GAS_LIMIT = GAS_LIMITS.GIFT_PLANET;
48const GAS_LIMIT = GAS_LIMITS.GIFT_PLANET;
49const INVITE_COST = toWei((GAS_PRICE_GWEI * GAS_LIMIT).toString(), 'gwei');
49const INVITE_COST = toWei((GAS_PRICE_GWEI * GAS_LIMIT).toString(), 'gwei');
@@ -232,7 +232,7 @@ export default function InviteEmail() {
232 walletHdPath,
232 walletHdPath,
233 networkType,
233 networkType,
234 // TODO: ^ make a useTransactionSigner to encapsulate this logic
234 // TODO: ^ make a useTransactionSigner to encapsulate this logic
235− txn: Maybe.Just(inviteTx),
235+ txn: Just(inviteTx),
236 gasPrice: GAS_PRICE_GWEI.toString(),
236 gasPrice: GAS_PRICE_GWEI.toString(),
237 gasLimit: GAS_LIMIT.toString(),
237 gasLimit: GAS_LIMIT.toString(),
238 nonce: nonce + i,
238 nonce: nonce + i,
@@ -300,7 +300,7 @@ export default function InviteEmail() {
300 try {
300 try {
301 const txHash = await sendSignedTransaction(
301 const txHash = await sendSignedTransaction(
302 _web3,
302 _web3,
303− Maybe.Just(invite.signedTx),
303+ Just(invite.signedTx),
304 tankWasUsed,
304 tankWasUsed,
305 () => {}
305 () => {}
306 );
306 );
src/views/Login.js+83−42
@@ -1,58 +1,73 @@
1+import React, { useCallback, useState } from 'react';
1−import { Nothing, Just } from 'folktale/maybe';
2+import { Just, Nothing } from 'folktale/maybe';
2−import React, { useState } from 'react';
3import * as azimuth from 'azimuth-js';
3import * as azimuth from 'azimuth-js';
4
4
5−import { H1 } from 'indigo-react';
5+import { H4, Grid } from 'indigo-react';
6−import View from 'components/View';
7−import Tabs from 'components/Tabs';
8
6
9import Ticket from './Login/Ticket';
7import Ticket from './Login/Ticket';
10import Mnemonic from './Login/Mnemonic';
8import Mnemonic from './Login/Mnemonic';
11import Advanced from './Login/Advanced';
9import Advanced from './Login/Advanced';
10+import Hardware from './Login/Hardware';
12
11
13−import useLifecycle from 'lib/useLifecycle';
14import * as need from 'lib/need';
12import * as need from 'lib/need';
15−import { ROUTE_NAMES } from 'lib/routeNames';
16
13
17import { useHistory } from 'store/history';
14import { useHistory } from 'store/history';
18import { useNetwork } from 'store/network';
15import { useNetwork } from 'store/network';
19import { useWallet } from 'store/wallet';
16import { useWallet } from 'store/wallet';
20import { usePointCursor } from 'store/pointCursor';
17import { usePointCursor } from 'store/pointCursor';
21
18
19+import View from 'components/View';
20+import Tabs from 'components/Tabs';
21+import Crumbs from 'components/Crumbs';
22+import Footer from 'components/Footer';
23+import { ForwardButton, OfflineButton } from 'components/Buttons';
24+
22−const TABS = {
25+const NAMES = {
23− TICKET: Symbol('TICKET'),
26+ TICKET: 'TICKET',
24− MNEMONIC: Symbol('MNEMONIC'),
27+ MNEMONIC: 'MNEMONIC',
28+ HARDWARE: 'HARDWARE',
25− ADVANCED: Symbol('ADVANCED'),
29+ ADVANCED: 'ADVANCED',
26};
30};
27
31
28−const tabViews = {
32+const VIEWS = {
29− [TABS.TICKET]: Ticket,
33+ [NAMES.TICKET]: Ticket,
30− [TABS.MNEMONIC]: Mnemonic,
34+ [NAMES.MNEMONIC]: Mnemonic,
35+ [NAMES.HARDWARE]: Hardware,
31− [TABS.ADVANCED]: Advanced,
36+ [NAMES.ADVANCED]: Advanced,
32};
37};
33
38
34−const tabOptions = [
39+const OPTIONS = [
35− { title: 'Ticket', value: TABS.TICKET },
40+ { text: 'Master Ticket', value: NAMES.TICKET },
36− { title: 'Mnemonic', value: TABS.MNEMONIC },
41+ { text: 'Mnemonic', value: NAMES.MNEMONIC },
42+ { text: 'Hardware', value: NAMES.HARDWARE },
37− { title: 'Advanced', value: TABS.ADVANCED },
43+ { text: 'Advanced', value: NAMES.ADVANCED },
38];
44];
39
45
40export default function Login() {
46export default function Login() {
41 // globals
47 // globals
42− const history = useHistory();
48+ const { push, replaceWith, names } = useHistory();
43 const { contracts } = useNetwork();
49 const { contracts } = useNetwork();
44− const { wallet, resetWallet } = useWallet();
50+ const { wallet } = useWallet();
45 const { pointCursor, setPointCursor } = usePointCursor();
51 const { pointCursor, setPointCursor } = usePointCursor();
46
52
47 // inputs
53 // inputs
48− const [currentTab, setCurrentTab] = useState(TABS.TICKET);
54+ const [currentTab, setCurrentTab] = useState(NAMES.TICKET);
49
55
50− // on-mount
56+ const goToActivate = useCallback(
51− useLifecycle(() => {
57+ () => replaceWith([{ key: names.LANDING }, { key: names.ACTIVATE }]),
52− resetWallet();
58+ [replaceWith, names]
53− });
59+ );
54
60
61+ const goToPoints = useCallback(() => {
62+ push(names.POINTS);
63+ }, [push, names]);
64+
65+ const goToPoint = useCallback(() => {
66+ goToPoints();
67+ push(names.POINT);
68+ }, [goToPoints, push, names]);
69+
55− const doContinue = async () => {
70+ const doContinue = useCallback(async () => {
56 const _wallet = need.wallet(wallet);
71 const _wallet = need.wallet(wallet);
57 const _contracts = need.contracts(contracts);
72 const _contracts = need.contracts(contracts);
58
73
@@ -80,25 +95,51 @@ export default function Login() {
80 // navigate to that specific point, otherwise navigate to list of points
95 // navigate to that specific point, otherwise navigate to list of points
81 if (Just.hasInstance(deduced)) {
96 if (Just.hasInstance(deduced)) {
82 setPointCursor(deduced);
97 setPointCursor(deduced);
83− history.popAndPush(ROUTE_NAMES.POINTS);
98+ goToPoint();
84− history.push(ROUTE_NAMES.POINT);
85 } else {
99 } else {
86− history.popAndPush(ROUTE_NAMES.POINTS);
100+ goToPoints();
87 }
101 }
88− };
102+ }, [contracts, pointCursor, setPointCursor, wallet, goToPoint, goToPoints]);
89
103
90 return (
104 return (
91− <View>
105+ <View inset>
106+ <Grid>
107+ <Grid.Item full as={Crumbs} routes={[{ text: 'Multipass' }]} />
108+ <Grid.Item full as={H4} className="mt4">
92− <H1>Login</H1>
109+ Login
93−
110+ </Grid.Item>
111+
112+ <Grid.Item
113+ full
94− <Tabs
114+ as={Tabs}
95− tabViews={tabViews}
115+ className="mt1"
96− tabOptions={tabOptions}
116+ views={VIEWS}
117+ options={OPTIONS}
97− currentTab={currentTab}
118+ currentTab={currentTab}
98− onTabChange={setCurrentTab}
119+ onTabChange={setCurrentTab}
99− //
120+ />
121+
122+ <Grid.Item
123+ full
124+ as={ForwardButton}
125+ solid
126+ className="mt2"
127+ disabled={Nothing.hasInstance(wallet)}
100− loginCompleted={doContinue}
128+ onClick={doContinue}>
101− />
129+ Continue
130+ </Grid.Item>
131+ </Grid>
132+
133+ <Footer>
134+ <Grid>
135+ <Grid.Divider />
136+ <Grid.Item full as={ForwardButton} onClick={goToActivate}>
137+ Activate
138+ </Grid.Item>
139+ <Grid.Divider />
140+ <Grid.Item full as={OfflineButton} />
141+ </Grid>
142+ </Footer>
102 </View>
143 </View>
103 );
144 );
104}
145}
src/views/Login/Advanced.js+31−110
@@ -1,115 +1,36 @@
1−import React from 'react';
1+import React, { useState } from 'react';
2−import { P, InnerLabelDropdown } from '../../components/old/Base';
3−import { Row, Col } from '../../components/old/Base';
4
2
5−import Ticket from './Ticket';
3+import Tabs from 'components/Tabs';
6−import Shards from './Shards';
7−import Ledger from './Ledger';
8−import Trezor from './Trezor';
9−import Mnemonic from './Mnemonic';
10import PrivateKey from './PrivateKey';
4import PrivateKey from './PrivateKey';
11import Keystore from './Keystore';
5import Keystore from './Keystore';
12
6
13−import { WALLET_TYPES, renderWalletType } from '../../lib/wallet';
7+const NAMES = {
14−import { compose } from '../../lib/lib';
8+ PRIVATE_KEY: 'PRIVATE_KEY',
15−import { withWallet } from '../../store/wallet';
9+ KEYSTORE: 'KEYSTORE',
16−
17−class Advanced extends React.Component {
18− constructor(props) {
19− super(props);
20−
21− this.state = {
22− selected: WALLET_TYPES.TICKET,
23− };
24−
25− this.handleSelection = this.handleSelection.bind(this);
26− }
27−
28− getWalletOptions() {
29− return [
30− {
31− title: 'Urbit Master Ticket',
32− value: WALLET_TYPES.TICKET,
33− },
34− {
35− title: 'Urbit Master Ticket (Shards)',
36− value: WALLET_TYPES.SHARDS,
37− },
10+};
38− {
11+
39− type: 'divider',
12+const VIEWS = {
40− },
13+ [NAMES.PRIVATE_KEY]: PrivateKey,
41− {
14+ [NAMES.KEYSTORE]: Keystore,
42− title: 'Ledger',
43− value: WALLET_TYPES.LEDGER,
44− },
45− {
46− title: 'Trezor',
47− value: WALLET_TYPES.TREZOR,
48− },
49− {
50− type: 'divider',
51− },
15+};
52− {
16+
53− title: 'BIP39 Mnemonic',
17+const OPTIONS = [
54− value: WALLET_TYPES.MNEMONIC,
55− },
56− {
57− title: 'Ethereum Private Key',
18+ { text: 'Ethereum Private Key', value: NAMES.PRIVATE_KEY },
58− value: WALLET_TYPES.PRIVATE_KEY,
59− },
60− {
61− title: 'Ethereum Keystore File',
19+ { text: 'Ethereum Keystore', value: NAMES.KEYSTORE },
62− value: WALLET_TYPES.KEYSTORE,
63− },
64− ];
20+];
65− }
21+
66−
22+export default function Advanced({ loginCompleted, className }) {
67− handleSelection(selected) {
23+ const [currentTab, setCurrentTab] = useState(NAMES.PRIVATE_KEY);
68− this.setState({ selected });
24+
69− }
70−
71− render() {
72− const loginCompleted = this.props.loginCompleted;
73− const inputs = (() => {
74− switch (this.state.selected) {
75− case WALLET_TYPES.TICKET:
76− return <Ticket advanced={true} loginCompleted={loginCompleted} />;
77− case WALLET_TYPES.SHARDS:
78− return <Shards loginCompleted={loginCompleted} />;
79− //
80− case WALLET_TYPES.LEDGER:
81− return <Ledger loginCompleted={loginCompleted} />;
82− case WALLET_TYPES.TREZOR:
83− return <Trezor loginCompleted={loginCompleted} />;
84− //
85− case WALLET_TYPES.MNEMONIC:
86− return <Mnemonic advanced={true} loginCompleted={loginCompleted} />;
87− case WALLET_TYPES.PRIVATE_KEY:
88− return <PrivateKey loginCompleted={loginCompleted} />;
89− case WALLET_TYPES.KEYSTORE:
90− return <Keystore loginCompleted={loginCompleted} />;
91− //
92− default:
93− return <P>{'Coming soon.'}</P>;
94− }
95− })();
96−
97− return (
25+ return (
98− <Row>
26+ <Tabs
99− <Col>
100− <InnerLabelDropdown
101− className={'mb-10 mt-6'}
27+ className={className}
102− title={'Wallet Type:'}
28+ views={VIEWS}
103− options={this.getWalletOptions()}
29+ options={OPTIONS}
104− handleUpdate={this.handleSelection}
30+ currentTab={currentTab}
105− currentSelectionTitle={renderWalletType(this.state.selected)}
31+ onTabChange={setCurrentTab}
106− />
32+ //
107−
33+ loginCompleted={loginCompleted}
108− {inputs}
34+ />
109− </Col>
110− </Row>
111− );
35+ );
112− }
113}
36}
114−
115−export default compose(withWallet)(Advanced);
src/views/Login/Hardware.jsnew file+36
@@ -0,0 +1,36 @@
1+import React, { useState } from 'react';
2+
3+import Tabs from 'components/Tabs';
4+import Ledger from './Ledger';
5+import Trezor from './Trezor';
6+
7+const NAMES = {
8+ LEDGER: 'LEDGER',
9+ TREZOR: 'TREZOR',
10+};
11+
12+const VIEWS = {
13+ [NAMES.LEDGER]: Ledger,
14+ [NAMES.TREZOR]: Trezor,
15+};
16+
17+const OPTIONS = [
18+ { text: 'Ledger', value: NAMES.LEDGER },
19+ { text: 'Trezor', value: NAMES.TREZOR },
20+];
21+
22+export default function Hardware({ loginCompleted, className }) {
23+ const [currentTab, setCurrentTab] = useState(NAMES.LEDGER);
24+
25+ return (
26+ <Tabs
27+ className={className}
28+ views={VIEWS}
29+ options={OPTIONS}
30+ currentTab={currentTab}
31+ onTabChange={setCurrentTab}
32+ //
33+ loginCompleted={loginCompleted}
34+ />
35+ );
36+}
src/views/Login/Keystore.js+40−70
@@ -1,39 +1,33 @@
1−import { Just, Nothing } from 'folktale/maybe';
2−import { Ok, Error } from 'folktale/result';
3import React, { useState } from 'react';
1import React, { useState } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
3+import { P, Grid, Input, ErrorText } from 'indigo-react';
4import * as keythereum from 'keythereum';
4import * as keythereum from 'keythereum';
5−import { H3, Input } from 'indigo-react';
6−
7−import View from 'components/View';
8−import { usePassphraseInput } from 'components/Inputs';
9−import { ForwardButton } from 'components/Buttons';
10−import { InputCaption, UploadButton, Warning } from 'components/old/Base';
11
5
12import { useWallet } from 'store/wallet';
6import { useWallet } from 'store/wallet';
13
7
8+import { usePassphraseInput } from 'lib/useInputs';
14import * as need from 'lib/need';
9import * as need from 'lib/need';
15import { EthereumWallet, WALLET_TYPES } from 'lib/wallet';
10import { EthereumWallet, WALLET_TYPES } from 'lib/wallet';
16−import useWalletType from 'lib/useWalletType';
11+import useLoginView from 'lib/useLoginView';
17−import useResetPointCursor from 'lib/useResetPointCursor';
18
12
13+import { ForwardButton } from 'components/Buttons';
14+import UploadButton from 'components/UploadButton';
15+
19−export default function Keystore({ loginCompleted }) {
16+export default function Keystore({ className }) {
20− useResetPointCursor();
21− useWalletType(WALLET_TYPES.KEYSTORE);
17+ useLoginView(WALLET_TYPES.KEYSTORE);
22
18
23 // globals
19 // globals
24− const { wallet, setWallet } = useWallet();
20+ const { setWallet } = useWallet();
25
21
22+ const [error, setError] = useState();
26 // inputs
23 // inputs
27− // keystore: Maybe<Result<String, String>>
24+ // keystore: Maybe<String>
28 const [keystore, setKeystore] = useState(Nothing());
25 const [keystore, setKeystore] = useState(Nothing());
29− const passphraseInput = usePassphraseInput({
26+ const [passphraseInput, { data: passphrase }] = usePassphraseInput({
30 name: 'password',
27 name: 'password',
31 label: 'Keystore password',
28 label: 'Keystore password',
32 autoFocus: true,
29 autoFocus: true,
33 });
30 });
34− const passphrase = passphraseInput.data;
35−
36− const [decryptionProblem, setDecryptionProblem] = useState(false);
37
31
38 const constructWallet = () => {
32 const constructWallet = () => {
39 try {
33 try {
@@ -43,26 +37,27 @@ export default function Keystore({ loginCompleted }) {
43 const privateKey = keythereum.recover(passphrase, json);
37 const privateKey = keythereum.recover(passphrase, json);
44
38
45 const newWallet = new EthereumWallet(privateKey);
39 const newWallet = new EthereumWallet(privateKey);
46− setDecryptionProblem(false);
40+ setError();
47 setWallet(Just(newWallet));
41 setWallet(Just(newWallet));
48 } catch (err) {
42 } catch (err) {
49− setDecryptionProblem(true);
43+ setError(
44+ "Couldn't decrypt wallet. You may have entered an incorrect password."
45+ );
50 setWallet(Nothing());
46 setWallet(Nothing());
51 }
47 }
52 };
48 };
53
49
54− const handleKeystoreUpload = event => {
50+ const handleKeystoreUpload = element => {
55− const file = event.files.item(0);
51+ const file = element.files.item(0);
56 const reader = new FileReader();
52 const reader = new FileReader();
57
53
58 reader.onload = e => {
54 reader.onload = e => {
59 const keystore = e.target.result;
55 const keystore = e.target.result;
60− setKeystore(Just(Ok(keystore)));
56+ setKeystore(Just(keystore));
61 };
57 };
62
58
63 const failure = _ => {
59 const failure = _ => {
64− const message = 'There was a problem uploading your Keystore file';
60+ setError('There was a problem uploading your Keystore file');
65− setKeystore(Just(Error(message)));
66 };
61 };
67
62
68 reader.onerror = failure;
63 reader.onerror = failure;
@@ -71,59 +66,34 @@ export default function Keystore({ loginCompleted }) {
71 reader.readAsText(file);
66 reader.readAsText(file);
72 };
67 };
73
68
74− const uploadButtonClass = keystore.matchWith({
75− Nothing: _ => 'bg-blue white',
76− Just: ks =>
77− ks.value.matchWith({
78− Ok: _ => 'bg-green white',
79− Error: _ => 'bg-yellow black',
80− }),
81− });
82−
83− const decryptMessage =
84− decryptionProblem === false ? (
85− <div />
86− ) : (
87− <Warning className="mt-8">
88− <H3 style={{ marginTop: 0, paddingTop: 0 }}>
89− Couldn't decrypt wallet.
90− </H3>
91− You may have entered an incorrect password.
92− </Warning>
93− );
94−
95 return (
69 return (
96− <View>
70+ <Grid className={className}>
97− <InputCaption>
71+ <Grid.Item full as={P}>
98 Please upload your Ethereum keystore file. If your keystore file is
72 Please upload your Ethereum keystore file. If your keystore file is
99 encrypted with a password, you'll also need to enter that below.
73 encrypted with a password, you'll also need to enter that below.
100− </InputCaption>
74+ </Grid.Item>
75+
76+ <Grid.Item full as={UploadButton} onChange={handleKeystoreUpload}>
77+ Upload Keystore file
78+ </Grid.Item>
101
79
102− <UploadButton
80+ {error && (
103− className={`${uploadButtonClass} mt3`}
81+ <Grid.Item full as={ErrorText} className="mt1">
104− onChange={handleKeystoreUpload}>
82+ {error}
105− <div className="flex-center-all fs-4 h-11 pointer">
83+ </Grid.Item>
106− Upload Keystore file
84+ )}
107− </div>
108− </UploadButton>
109
85
110− <Input {...passphraseInput} />
86+ <Grid.Item full as={Input} className="mt3" {...passphraseInput} />
111
87
88+ <Grid.Item
89+ full
112− <ForwardButton
90+ as={ForwardButton}
91+ solid
113 className="mt3"
92 className="mt3"
114 disabled={Nothing.hasInstance(keystore)}
93 disabled={Nothing.hasInstance(keystore)}
115 onClick={constructWallet}>
94 onClick={constructWallet}>
116 Decrypt
95 Decrypt
117− </ForwardButton>
96+ </Grid.Item>
118−
97+ </Grid>
119− {decryptMessage}
120−
121− <ForwardButton
122− className="mt3"
123− disabled={Nothing.hasInstance(wallet)}
124− onClick={loginCompleted}>
125− Continue
126− </ForwardButton>
127− </View>
128 );
98 );
129}
99}
src/views/Login/Ledger.js+188−168
@@ -1,190 +1,210 @@
1−import * as bip32 from 'bip32';
1+import React, { useCallback, useEffect } from 'react';
2−import React, { useState } from 'react';
2+import cn from 'classnames';
3+import { Just, Nothing } from 'folktale/maybe';
4+import {
5+ P,
6+ Text,
7+ Input,
8+ Grid,
9+ H5,
10+ CheckboxInput,
11+ SelectInput,
12+} from 'indigo-react';
3import { times } from 'lodash';
13import { times } from 'lodash';
4−import Maybe from 'folktale/maybe';
14+import * as bip32 from 'bip32';
5import Transport from '@ledgerhq/hw-transport-u2f';
15import Transport from '@ledgerhq/hw-transport-u2f';
6import Eth from '@ledgerhq/hw-app-eth';
16import Eth from '@ledgerhq/hw-app-eth';
7import * as secp256k1 from 'secp256k1';
17import * as secp256k1 from 'secp256k1';
8−import { H1, H2, P, Input } from 'indigo-react';
9−
10−import View from 'components/View';
11−import { ForwardButton } from 'components/Buttons';
12−import { InnerLabelDropdown } from 'components/old/Base';
13
18
14import { useWallet } from 'store/wallet';
19import { useWallet } from 'store/wallet';
15
20
21+import { ForwardButton } from 'components/Buttons';
22+
23+import {
24+ useCheckboxInput,
25+ useHdPathInput,
26+ useSelectInput,
27+} from 'lib/useInputs';
28+
29+import {
30+ LEDGER_LIVE_PATH,
16−import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from 'lib/ledger';
31+ LEDGER_LEGACY_PATH,
17−import useWalletType from 'lib/useWalletType';
32+ chopHdPrefix,
33+ addHdPrefix,
34+} from 'lib/ledger';
18import { WALLET_TYPES } from 'lib/wallet';
35import { WALLET_TYPES } from 'lib/wallet';
19−import useResetPointCursor from 'lib/useResetPointCursor';
36+import useLoginView from 'lib/useLoginView';
37+import useBreakpoints from 'lib/useBreakpoints';
20
38
21−const pathOptions = [
39+const PATH_OPTIONS = [
22− { title: 'Ledger Live', value: LEDGER_LIVE_PATH },
40+ { text: 'Ledger Live', value: LEDGER_LIVE_PATH },
23− { title: 'Ledger Legacy', value: LEDGER_LEGACY_PATH },
41+ { text: 'Ledger Legacy', value: LEDGER_LEGACY_PATH },
24− { title: 'Custom path', value: 'custom' },
25];
42];
26
43
27−const chopHdPrefix = str => (str.slice(0, 2) === 'm/' ? str.slice(2) : str);
44+const ACCOUNT_OPTIONS = times(20, i => ({
28−
45+ text: `Account #${i + 1}`,
29−const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
46+ value: i,
30−
47+}));
48+
31−export default function Ledger({ loginCompleted }) {
49+export default function Ledger({ className }) {
32− useResetPointCursor();
33− useWalletType(WALLET_TYPES.LEDGER);
50+ useLoginView(WALLET_TYPES.LEDGER);
34−
51+
35− const { wallet, setWallet, setWalletHdPath } = useWallet();
52+ const { setWallet, setWalletHdPath } = useWallet();
36−
53+
37− const [basePath, setBasePath] = useState(LEDGER_LIVE_PATH);
54+ // derivation path input
38− const [account, setAccount] = useState(0);
55+ const [derivationPathInput, { data: basePathPattern }] = useSelectInput({
39− const [hdPath, setHdPath] = useState(LEDGER_LIVE_PATH.replace(/x/g, 0));
56+ name: 'derivationpath',
40−
57+ label: 'Derivation Path',
41− const updateHdPath = (basePath, account) => {
58+ placeholder: 'Choose path pattern...',
42− if (basePath !== 'custom') {
59+ options: PATH_OPTIONS,
43− setHdPath(basePath.replace(/x/g, account));
60+ });
44− }
61+
45− };
62+ // account input
46−
63+ const [accountInput, { data: accountIndex }] = useSelectInput({
47− const handlePathSelection = basePath => {
64+ name: 'account',
48− setBasePath(basePath);
65+ label: 'Account',
49− updateHdPath(basePath, account);
66+ placeholder: 'Choose account...',
67+ options: ACCOUNT_OPTIONS,
68+ });
69+
70+ // custom toggle
71+ const [customPathInput, { data: useCustomPath }] = useCheckboxInput({
72+ name: 'customPath',
73+ label: 'Custom HD Path',
74+ autoComplete: 'off',
75+ initialValue: false,
76+ });
77+
78+ // hd path input
79+ const [
80+ hdPathInput,
81+ { data: hdPath },
82+ { setValue: setHdPath },
83+ ] = useHdPathInput({
84+ name: 'hdpath',
85+ label: 'HD Path',
86+ initialValue: basePathPattern.replace(/x/g, 0),
50− };
87+ });
51−
88+
52− const handleAccountSelection = account => {
53− setAccount(account);
54− updateHdPath(basePath, account);
55− };
56−
57− const pollDevice = async () => {
89+ const pollDevice = useCallback(async () => {
58 const transport = await Transport.create();
90 const transport = await Transport.create();
59 const eth = new Eth(transport);
91 const eth = new Eth(transport);
60 const path = chopHdPrefix(hdPath);
92 const path = chopHdPrefix(hdPath);
61
93
94+ try {
62− eth.getAddress(path, false, true).then(
95+ const info = await eth.getAddress(path, false, true);
63− info => {
64− const publicKey = Buffer.from(info.publicKey, 'hex');
96+ const publicKey = Buffer.from(info.publicKey, 'hex');
65− const chainCode = Buffer.from(info.chainCode, 'hex');
97+ const chainCode = Buffer.from(info.chainCode, 'hex');
66− const pub = secp256k1.publicKeyConvert(publicKey, true);
98+ const pub = secp256k1.publicKeyConvert(publicKey, true);
67− const hd = bip32.fromPublicKey(pub, chainCode);
99+ const hd = bip32.fromPublicKey(pub, chainCode);
68− setWallet(Maybe.Just(hd));
100+ setWallet(Just(hd));
69− setWalletHdPath(addHdPrefix(hdPath));
101+ setWalletHdPath(addHdPrefix(hdPath));
70− },
102+ } catch (error) {
71− _ => {
103+ console.error(error);
72− setWallet(Maybe.Nothing());
104+ setWallet(Nothing());
73− }
74− );
75− };
105+ }
76−
106+ }, [hdPath, setWallet, setWalletHdPath]);
77− const basePathTitle = pathOptions.find(o => o.value === basePath).title;
107+
78−
108+ // when the base path pattern or the account index changes
79− const accountOptions = times(20, i => ({
109+ // update the hd path in our input
80− title: `Account #${i + 1}`,
110+ useEffect(() => {
81− value: i,
111+ if (useCustomPath) {
82− }));
112+ // updated by useForm
83− const accountTitle = accountOptions.find(o => o.value === account).title;
113+ } else {
84−
114+ setHdPath(basePathPattern.replace(/x/g, accountIndex));
85− const basePathSelection = (
115+ }
86− <InnerLabelDropdown
116+ }, [useCustomPath, setHdPath, basePathPattern, accountIndex]);
87− className="mt-8"
88− options={pathOptions}
89− handleUpdate={handlePathSelection}
90− title="Derivation path"
91− currentSelectionTitle={basePathTitle}
92− fullWidth={true}
93− />
94− );
95
117
96− const truePathSelection =
118+ const full = useBreakpoints([true, true, false]);
97− basePath === 'custom' ? (
119+ const half = useBreakpoints([false, false, true]);
98− <Input
120+ const isHTTPS = document.location.protocol === 'https:';
99− className="mt3"
100− name="hdPath"
101− label="HD path"
102− autocomplete="off"
103− initialValue={addHdPrefix(hdPath)}
104− onValue={setHdPath}
105− />
106− ) : (
107− <InnerLabelDropdown
108− className="mt-4"
109− prop-size="md"
110− prop-format="innerLabel"
111− options={accountOptions}
112− handleUpdate={handleAccountSelection}
113− title="Account"
114− currentSelectionTitle={accountTitle}
115− fullWidth={true}
116− />
117− );
118
121
119 // when not on https, tell user how to get there
122 // when not on https, tell user how to get there
120− const body =
123+ const renderHTTP = () => (
121− document.location.protocol !== 'https:' ? (
122− <>
124+ <>
125+ <Grid.Item full as={H5}>
123− <H2>Running on HTTP?</H2>
126+ Running on HTTP?
124−
127+ </Grid.Item>
125− <P>
128+
129+ <Grid.Item full as={P}>
126− To authenticate and sign transactions with a Ledger, Bridge must be
130+ To authenticate and sign transactions with a Ledger, Bridge must be
127− serving over HTTPS on localhost. You can do this via the following:
131+ serving over HTTPS on localhost. You can do this via the following:
128− </P>
132+ </Grid.Item>
129−
133+
130− <ol className="measure-md">
134+ <Grid.Item full as="ol">
131− <li className="mt-4">
135+ <li>
132− Install
136+ Install{' '}
133− <a
137+ <a
134− target="_blank"
138+ target="_blank"
135− href="https://github.com/FiloSottile/mkcert"
139+ href="https://github.com/FiloSottile/mkcert"
136− rel="noopener noreferrer">
140+ rel="noopener noreferrer">
137− mkcert
141+ mkcert
138− </a>
142+ </a>
139− </li>
143+ </li>
140− <li className="mt-4">
144+ <li className="mt3">
141− Install a local certificate authority via{' '}
145+ Install a local certificate authority via <code>mkcert -install</code>
142− <code>mkcert -install</code>
143− </li>
146+ </li>
144− <li className="mt-4">
147+ <li className="mt3">
145− In your <code>bridge</code> directory, generate a certificate valid
148+ In your <code>bridge</code> directory, generate a certificate valid
146− for localhost via <code>mkcert localhost</code>. This will produce
149+ for localhost via <code>mkcert localhost</code>. This will produce two
147− two files: <code>localhost.pem</code>, the local certificate, and
150+ files: <code>localhost.pem</code>, the local certificate, and
148− <code>localhost-key.pem</code> , its corresponding private key.
151+ <code>localhost-key.pem</code> , its corresponding private key.
149− </li>
152+ </li>
150− <li className="mt-4">
153+ <li className="mt3">
151− Run <code>python bridge-https.py</code>
154+ Run <code>python bridge-https.py</code>
152− </li>
155+ </li>
153− </ol>
156+ <li className="mt3">Return to this page.</li>
157+ </Grid.Item>
154− </>
158+ </>
155− ) : (
159+ );
160+
161+ const renderHTTPS = () => (
156− <>
162+ <>
157− <P>
163+ <Grid.Item full as={H5}>
164+ Authenticate With Your Ledger
165+ </Grid.Item>
166+ <Grid.Item full as={Text} className="f6 mb3">
158− Connect and authenticate to your Ledger, and then open the "Ethereum"
167+ Connect and authenticate to your Ledger, and then open the "Ethereum"
159− application. If you're running on older firmware, make sure the
168+ application. If you're running on older firmware, make sure the "browser
160− "browser support" option is turned on. To sign transactions, you'll
169+ support" option is turned on. To sign transactions, you'll also need to
161− also need to enable the "contract data" option.
170+ enable the "contract data" option.
162− </P>
171+ </Grid.Item>
163−
172+
164− <P>
173+ {useCustomPath && <Grid.Item full as={Input} {...hdPathInput} />}
165− If you'd like to use a custom derivation path, you may enter it below.
174+
166− </P>
175+ {!useCustomPath && (
167−
176+ <>
168− {basePathSelection}
177+ <Grid.Item
169− {truePathSelection}
178+ full={full}
170−
179+ half={half && 1}
180+ as={SelectInput}
181+ className={cn({ pr1: half })}
182+ {...derivationPathInput}
183+ />
184+ <Grid.Item
185+ full={full}
186+ half={half && 2}
187+ as={SelectInput}
188+ className={cn({ pl1: half })}
189+ {...accountInput}
190+ />
191+ </>
192+ )}
193+
194+ <Grid.Item full as={CheckboxInput} className="mv3" {...customPathInput} />
195+
196+ <Grid.Item
197+ full
198+ as={ForwardButton}
199+ solid
200+ className="mt2"
171− <ForwardButton className={'mt3'} onClick={pollDevice}>
201+ onClick={pollDevice}>
172− {'Authenticate'}
202+ Authenticate
173− </ForwardButton>
203+ </Grid.Item>
174−
175− <ForwardButton
176− className={'mt3'}
177− disabled={Maybe.Nothing.hasInstance(wallet)}
178− onClick={loginCompleted}>
179− {'Continue'}
180− </ForwardButton>
181− </>
204+ </>
182− );
205+ );
183
206
184 return (
207 return (
185− <View>
208+ <Grid className={className}>{isHTTPS ? renderHTTPS() : renderHTTP()}</Grid>
186− <H1>Authenticate With Your Ledger</H1>
187− {body}
188− </View>
189 );
209 );
190}
210}
src/views/Login/Mnemonic.js+43−52
@@ -1,92 +1,83 @@
1−import React, { useCallback, useEffect } from 'react';
1+import React, { useEffect } from 'react';
2import { Just, Nothing } from 'folktale/maybe';
2import { Just, Nothing } from 'folktale/maybe';
3−import { Input } from 'indigo-react';
3+import { Grid, Input, CheckboxInput } from 'indigo-react';
4+
5+import { useWallet } from 'store/wallet';
4
6
5−import View from 'components/View';
6import {
7import {
7 usePassphraseInput,
8 usePassphraseInput,
8 useMnemonicInput,
9 useMnemonicInput,
9 useHdPathInput,
10 useHdPathInput,
10−} from 'components/Inputs';
11+ useCheckboxInput,
11−import { ForwardButton } from 'components/Buttons';
12+} from 'lib/useInputs';
12−
13−import { useWallet } from 'store/wallet';
14−
15import { walletFromMnemonic, WALLET_TYPES } from 'lib/wallet';
13import { walletFromMnemonic, WALLET_TYPES } from 'lib/wallet';
16−import useWalletType from 'lib/useWalletType';
14+import useLoginView from 'lib/useLoginView';
17−import useResetPointCursor from 'lib/useResetPointCursor';
18
15
19−export default function Mnemonic({ advanced, loginCompleted }) {
16+export default function Mnemonic({ className }) {
20− useResetPointCursor();
21− useWalletType(WALLET_TYPES.MNEMONIC);
17+ useLoginView(WALLET_TYPES.MNEMONIC);
22
18
23 const {
19 const {
24− wallet,
25 setWallet,
20 setWallet,
26− authMnemonic,
27 setAuthMnemonic,
21 setAuthMnemonic,
28 walletHdPath,
22 walletHdPath,
29 setWalletHdPath,
23 setWalletHdPath,
30 } = useWallet();
24 } = useWallet();
31
25
32− const mnemonic = authMnemonic.getOrElse('');
26+ const [advancedInput, { data: useAdvanced }] = useCheckboxInput({
33− // TODO: move this into transformers?
27+ name: 'advanced',
34− // transform the result of the mnemonic to Maybe<string>
28+ label: 'Passphrase & HD Path',
35− const _setAuthMnemonic = useCallback(
29+ initialValue: false,
36− mnemonic => setAuthMnemonic(mnemonic === '' ? Nothing() : Just(mnemonic)),
37− [setAuthMnemonic]
38− );
30+ });
39
31
40− const mnemonicInput = useMnemonicInput({
32+ const [mnemonicInput, { pass, data: mnemonic }] = useMnemonicInput({
41 name: 'mnemonic',
33 name: 'mnemonic',
42 label: 'BIP39 Mnemonic',
34 label: 'BIP39 Mnemonic',
43− initialValue: mnemonic,
44− onValue: _setAuthMnemonic,
45 autoFocus: true,
35 autoFocus: true,
46 });
36 });
47
37
48− const passphraseInput = usePassphraseInput({
38+ const [passphraseInput, { data: passphrase }] = usePassphraseInput({
49 name: 'passphrase',
39 name: 'passphrase',
50− label: '(Optional) Wallet Passphrase',
40+ label: 'Wallet Passphrase',
51 });
41 });
52− const passphrase = passphraseInput.data;
53
42
54− const hdPathInput = useHdPathInput({
43+ const [hdPathInput, { data: hdPath }] = useHdPathInput({
55 name: 'hdpath',
44 name: 'hdpath',
56 label: 'HD Path',
45 label: 'HD Path',
57 initialValue: walletHdPath,
46 initialValue: walletHdPath,
58− onValue: setWalletHdPath,
59 });
47 });
60
48
61− // when the properties change, re-derive wallet
49+ // when the properties change, re-derive wallet and set global state
62 useEffect(() => {
50 useEffect(() => {
63− let mounted = true;
51+ if (pass) {
64− (async () => {
52+ setWalletHdPath(hdPath);
53+ setAuthMnemonic(Just(mnemonic));
65− const wallet = walletFromMnemonic(mnemonic, walletHdPath, passphrase);
54+ setWallet(walletFromMnemonic(mnemonic, hdPath, passphrase));
66− mounted && setWallet(wallet);
55+ } else {
67− })();
56+ setAuthMnemonic(Nothing());
68− return () => (mounted = false);
57+ setWallet(Nothing());
58+ }
59+ }, [
60+ pass,
61+ mnemonic,
69− }, [mnemonic, passphrase, walletHdPath, setWallet]);
62+ passphrase,
63+ hdPath,
64+ setWallet,
65+ setAuthMnemonic,
66+ setWalletHdPath,
67+ ]);
70
68
71 return (
69 return (
72− <View>
70+ <Grid className={className}>
73− <Input {...mnemonicInput} />
71+ <Grid.Item full as={Input} {...mnemonicInput} />
74
72
75− {!advanced ? null : (
73+ {useAdvanced && (
76 <>
74 <>
77− <Input {...passphraseInput} />
75+ <Grid.Item full as={Input} {...passphraseInput} />
78−
79− <Input {...hdPathInput} />
76+ <Grid.Item full as={Input} {...hdPathInput} />
80 </>
77 </>
81 )}
78 )}
82
79
83− <ForwardButton
80+ <Grid.Item as={CheckboxInput} {...advancedInput} full />
84− className="mt3"
81+ </Grid>
85− disabled={Nothing.hasInstance(wallet)}
86− onClick={loginCompleted}
87− solid>
88− Continue
89− </ForwardButton>
90− </View>
91 );
82 );
92}
83}
src/views/Login/PrivateKey.js+17−28
@@ -1,49 +1,38 @@
1−import { Just, Nothing } from 'folktale/maybe';
2import React, { useEffect } from 'react';
1import React, { useEffect } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
3−import { Input } from 'indigo-react';
3+import { Grid, Input } from 'indigo-react';
4−
5−import View from 'components/View';
6−import { usePassphraseInput } from 'components/Inputs';
7−import { ForwardButton } from 'components/Buttons';
8
4
9import { useWallet } from 'store/wallet';
5import { useWallet } from 'store/wallet';
10
6
7+import { useHexInput } from 'lib/useInputs';
11−import { EthereumWallet, WALLET_TYPES } from 'lib/wallet';
8+import { EthereumWallet, WALLET_TYPES, stripHexPrefix } from 'lib/wallet';
12−import useWalletType from 'lib/useWalletType';
9+import useLoginView from 'lib/useLoginView';
13−import useResetPointCursor from 'lib/useResetPointCursor';
10+
11+export default function PrivateKey({ className }) {
12+ useLoginView(WALLET_TYPES.PRIVATE_KEY);
14
13
15−export default function PrivateKey({ loginCompleted }) {
16− useResetPointCursor();
17− useWalletType(WALLET_TYPES.PRIVATE_KEY);
18− const { wallet, setWallet } = useWallet();
14+ const { setWallet } = useWallet();
19
15
20− const privateKeyInput = usePassphraseInput({
16+ const [privateKeyInput, { pass, data: privateKey }] = useHexInput({
17+ length: 64,
21 name: 'privateKey',
18 name: 'privateKey',
22 label: 'Private key',
19 label: 'Private key',
23 autoFocus: true,
20 autoFocus: true,
24 });
21 });
25− const privateKey = privateKeyInput.data;
26
22
27 useEffect(() => {
23 useEffect(() => {
28− if (/^[0-9A-Fa-f]{64}$/g.test(privateKey) === true) {
24+ if (pass) {
29− const sec = Buffer.from(privateKey, 'hex');
25+ const sec = Buffer.from(stripHexPrefix(privateKey), 'hex');
30 const newWallet = new EthereumWallet(sec);
26 const newWallet = new EthereumWallet(sec);
31 setWallet(Just(newWallet));
27 setWallet(Just(newWallet));
32 } else {
28 } else {
33 setWallet(Nothing());
29 setWallet(Nothing());
34 }
30 }
35− }, [privateKey, setWallet]);
31+ }, [pass, privateKey, setWallet]);
36
32
37 return (
33 return (
38− <View>
34+ <Grid className={className}>
39− Please enter your raw Ethereum private key here.
40− <Input {...privateKeyInput} />
35+ <Grid.Item full as={Input} {...privateKeyInput} />
41− <ForwardButton
36+ </Grid>
42− className="mt3"
43− disabled={Nothing.hasInstance(wallet)}
44− onClick={loginCompleted}>
45− Continue
46− </ForwardButton>
47− </View>
48 );
37 );
49}
38}
src/views/Login/Shards.jsdeleted−98
@@ -1,98 +0,0 @@
1−import { Just, Nothing } from 'folktale/maybe';
2−import React from 'react';
3−import * as kg from 'urbit-key-generation/dist/index';
4−import { Input } from 'indigo-react';
5−
6−import View from 'components/View';
7−import {
8− useTicketInput,
9− usePassphraseInput,
10− usePointInput,
11−} from 'components/Inputs';
12−import { ForwardButton } from 'components/Buttons';
13−
14−import { useWallet } from 'store/wallet';
15−
16−import { urbitWalletFromTicket, WALLET_TYPES } from 'lib/wallet';
17−import useWalletType from 'lib/useWalletType';
18−import useResetPointCursor from 'lib/useResetPointCursor';
19−
20−export default function Shards({ loginCompleted }) {
21− useResetPointCursor();
22− useWalletType(WALLET_TYPES.SHARDS);
23−
24− const { wallet, setUrbitWallet } = useWallet();
25−
26− const pointInput = usePointInput({
27− name: 'point',
28− autoFocus: true,
29− });
30− const pointName = pointInput.data;
31− const shard1Input = useTicketInput({
32− name: 'shard1',
33− label: 'Shard 1',
34− });
35− const shard1 = shard1Input.data;
36− const shard2Input = useTicketInput({
37− name: 'shard2',
38− label: 'Shard 2',
39− });
40− const shard2 = shard2Input.data;
41− const shard3Input = useTicketInput({
42− name: 'shard3',
43− label: 'Shard 3',
44− });
45− const shard3 = shard3Input.data;
46−
47− const passphraseInput = usePassphraseInput({
48− name: 'passphrase',
49− label: '(Optional) Wallet Passphrase',
50− });
51− const passphrase = passphraseInput.data;
52−
53− const walletFromShards = async () => {
54− const s1 = shard1 === '' ? undefined : shard1;
55− const s2 = shard2 === '' ? undefined : shard2;
56− const s3 = shard3 === '' ? undefined : shard3;
57−
58− let ticket = undefined;
59− try {
60− ticket = kg.combine([s1, s2, s3]);
61− } catch (_) {
62− // do nothing
63− }
64−
65− if (ticket !== undefined) {
66− const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
67− setUrbitWallet(Just(uhdw));
68− }
69− };
70−
71− const shards = [shard1, shard2, shard3];
72− const ready = shards.filter(x => x !== '').length > 1;
73−
74− return (
75− <View>
76− Enter your point and at least two of your three Urbit master ticket shards
77− here. The index of the input field needs to match the index of the shard.
78− <Input {...pointInput} />
79− <Input {...shard1Input} />
80− <Input {...shard2Input} />
81− <Input {...shard3Input} />
82− If your wallet requires a passphrase, you may enter it below.
83− <Input {...passphraseInput} />
84− <ForwardButton
85− className="mt3"
86− disabled={!ready}
87− onClick={walletFromShards}>
88− Unlock Wallet
89− </ForwardButton>
90− <ForwardButton
91− className="mt3"
92− disabled={Nothing.hasInstance(wallet)}
93− onClick={loginCompleted}>
94− Continue
95− </ForwardButton>
96− </View>
97− );
98−}
src/views/Login/Ticket.js+150−105
@@ -2,139 +2,184 @@ import { Just, Nothing } from 'folktale/maybe';
2import React, { useCallback, useState, useEffect } from 'react';
2import React, { useCallback, useState, useEffect } from 'react';
3import * as azimuth from 'azimuth-js';
3import * as azimuth from 'azimuth-js';
4import * as ob from 'urbit-ob';
4import * as ob from 'urbit-ob';
5+import * as kg from 'urbit-key-generation/dist/index';
5−import { Input, AccessoryIcon } from 'indigo-react';
6+import { Input, Grid, CheckboxInput } from 'indigo-react';
6−
7−import View from 'components/View';
8−import {
9− usePointInput,
10− useTicketInput,
11− usePassphraseInput,
12−} from 'components/Inputs';
13−import { ForwardButton } from 'components/Buttons';
14
7
15import { useNetwork } from 'store/network';
8import { useNetwork } from 'store/network';
16import { useWallet } from 'store/wallet';
9import { useWallet } from 'store/wallet';
17import { usePointCursor } from 'store/pointCursor';
10import { usePointCursor } from 'store/pointCursor';
18
11
12+import {
13+ usePointInput,
14+ useTicketInput,
15+ usePassphraseInput,
16+ useCheckboxInput,
17+} from 'lib/useInputs';
19import * as need from 'lib/need';
18import * as need from 'lib/need';
20import { WALLET_TYPES, urbitWalletFromTicket } from 'lib/wallet';
19import { WALLET_TYPES, urbitWalletFromTicket } from 'lib/wallet';
21−import useWalletType from 'lib/useWalletType';
20+import useImpliedPoint from 'lib/useImpliedPoint';
22−import useResetPointCursor from 'lib/useResetPointCursor';
21+import useLoginView from 'lib/useLoginView';
23−
22+
24−//TODO should be part of InputWithStatus component
25−const INPUT_STATUS = {
26− SPIN: Symbol('SPIN'),
27− GOOD: Symbol('GOOD'),
28− FAIL: Symbol('FAIL'),
29−};
30−
31−export default function Ticket({ advanced, loginCompleted }) {
23+export default function Ticket({ className }) {
32− useResetPointCursor();
33− useWalletType(WALLET_TYPES.TICKET);
24+ useLoginView(WALLET_TYPES.TICKET);
34− // globals
25+
35 const { contracts } = useNetwork();
26 const { contracts } = useNetwork();
36− const { wallet, setUrbitWallet } = useWallet();
27+ const { setUrbitWallet } = useWallet();
37 const { setPointCursor } = usePointCursor();
28 const { setPointCursor } = usePointCursor();
29+ const impliedPoint = useImpliedPoint();
38
30
39− // inputs
31+ // point
40− const pointInput = usePointInput({
32+ const [pointInput, { data: pointName }] = usePointInput({
41 name: 'point',
33 name: 'point',
42− //TODO deduce point name from URL if we can, prefill input if we found it
34+ initialValue: impliedPoint || '',
43 autoFocus: true,
35 autoFocus: true,
44 });
36 });
45− const pointName = pointInput.data;
46
37
38+ // passphrase
39+ const [passphraseInput, { data: passphrase }] = usePassphraseInput({
40+ name: 'passphrase',
41+ label: 'Wallet Passphrase',
42+ });
43+
44+ const [hasPassphraseInput] = useCheckboxInput({
45+ name: 'has-passphrase',
46+ label: 'Passphrase',
47+ initialValue: false,
48+ });
49+
50+ // ticket
51+ const [error, setError] = useState();
52+ const [deriving, setDeriving] = useState(false);
47− const ticketInput = useTicketInput({
53+ const [ticketInput, { data: ticket, pass: validTicket }] = useTicketInput({
48 name: 'ticket',
54 name: 'ticket',
49 label: 'Master Ticket',
55 label: 'Master Ticket',
56+ error,
57+ deriving,
50 });
58 });
51− const ticket = ticketInput.data;
52
59
53− const passphraseInput = usePassphraseInput({
60+ // shards
54− name: 'passphrase',
61+ const [shardsInput, { data: isUsingShards }] = useCheckboxInput({
55− label: '(Optional) Wallet Passphrase',
62+ name: 'shards',
63+ label: 'Shards',
64+ initialValue: false,
56 });
65 });
57− const passphrase = passphraseInput.data;
66+
58−
67+ const [shard1Input, { data: shard1, pass: shard1Pass }] = useTicketInput({
59− // display state
68+ name: 'shard1',
60− const [ticketStatus, setTicketStatus] = useState(Nothing());
69+ label: 'Shard 1',
61−
70+ });
71+
72+ const [shard2Input, { data: shard2, pass: shard2Pass }] = useTicketInput({
73+ name: 'shard2',
74+ label: 'Shard 2',
75+ });
76+
77+ const [shard3Input, { data: shard3, pass: shard3Pass }] = useTicketInput({
78+ name: 'shard3',
79+ label: 'Shard 3',
80+ });
81+
82+ const shardsReady = shard1Pass && shard2Pass && shard3Pass;
83+
62− //TODO maybe want to do this only on-go, because wallet derivation is slow...
84+ // TODO: maybe want to do this only on-go, because wallet derivation is slow...
63− const verifyTicket = useCallback(
85+ const deriveWalletFromTicket = useCallback(async () => {
64− async (pointName, ticket, passphrase) => {
86+ // clear states
87+ setError();
88+ setDeriving(true);
65− setUrbitWallet(Nothing());
89+ setUrbitWallet(Nothing());
90+
66− if (!ob.isValidPatq(ticket) || !ob.isValidPatp(pointName)) {
91+ if (!ob.isValidPatq(ticket) || !ob.isValidPatp(pointName)) {
67− setTicketStatus(Nothing());
92+ setDeriving(false);
68− return;
93+ return;
69− }
94+ }
70− setTicketStatus(Just(INPUT_STATUS.SPIN));
95+
71− const _contracts = need.contracts(contracts);
96+ const _contracts = need.contracts(contracts);
72− const pointNumber = ob.patp2dec(pointName);
97+ const pointNumber = ob.patp2dec(pointName);
73− const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
98+ const urbitWallet = await urbitWalletFromTicket(
99+ ticket,
100+ pointName,
101+ passphrase
102+ );
103+ const [isOwner, isTransferProxy] = await Promise.all([
74− const isOwner = azimuth.azimuth.isOwner(
104+ azimuth.azimuth.isOwner(
75 _contracts,
105 _contracts,
76 pointNumber,
106 pointNumber,
77− uhdw.ownership.keys.address
107+ urbitWallet.ownership.keys.address
78− );
108+ ),
79− const isTransferProxy = azimuth.azimuth.isTransferProxy(
109+ azimuth.azimuth.isTransferProxy(
80 _contracts,
110 _contracts,
81 pointNumber,
111 pointNumber,
82− uhdw.ownership.keys.address
112+ urbitWallet.ownership.keys.address
113+ ),
114+ ]);
115+
116+ if (!isOwner && !isTransferProxy) {
117+ // notify the user, but allow login regardless
118+ setError(
119+ 'This ticket is not the owner of or transfer proxy for this point.'
83 );
120 );
121+ }
122+
123+ setUrbitWallet(Just(urbitWallet));
124+ setPointCursor(Just(ob.patp2dec(pointName)));
125+ setDeriving(false);
126+ }, [
127+ pointName,
128+ ticket,
129+ passphrase,
130+ contracts,
131+ setUrbitWallet,
132+ setPointCursor,
133+ setDeriving,
134+ ]);
135+
136+ const deriveWalletFromShards = useCallback(async () => {
137+ const s1 = shard1 || undefined;
138+ const s2 = shard2 || undefined;
139+ const s3 = shard3 || undefined;
140+
141+ try {
142+ const ticket = kg.combine([s1, s2, s3]);
143+ const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
84 setUrbitWallet(Just(uhdw));
144 setUrbitWallet(Just(uhdw));
85− const newStatus =
145+ } catch (_) {
86− (await isOwner) || (await isTransferProxy)
146+ // do nothing
87− ? INPUT_STATUS.GOOD
147+ }
88− : INPUT_STATUS.FAIL;
148+ }, [passphrase, pointName, setUrbitWallet, shard1, shard2, shard3]);
89− setTicketStatus(Just(newStatus));
90− },
91− [contracts, setUrbitWallet, setTicketStatus]
92− );
93
149
150+ // derive wallet on change
94 useEffect(() => {
151 useEffect(() => {
95− verifyTicket(pointName, ticket, passphrase);
152+ if (isUsingShards && shardsReady) {
96− }, [verifyTicket, pointName, ticket, passphrase]);
153+ deriveWalletFromShards();
97−
154+ } else if (validTicket) {
98− const canContinue = () => {
155+ deriveWalletFromTicket();
99− // this is our only requirement, since we still want people with
156+ }
100− // non-standard wallet setups to be able to log in
157+ }, [
101− return Just.hasInstance(wallet);
158+ isUsingShards,
102− };
159+ validTicket,
103−
160+ shardsReady,
104− const doContinue = () => {
161+ deriveWalletFromShards,
105− setPointCursor(Just(ob.patp2dec(pointName)));
162+ deriveWalletFromTicket,
106− loginCompleted();
107− };
108−
109− //TODO integrate into TicketInput?
110− const displayTicketStatus = ticketStatus.matchWith({
111− Nothing: () => <span />,
112− Just: status => {
113− switch (status.value) {
114− case INPUT_STATUS.SPIN:
115− return <AccessoryIcon.Pending />;
116− case INPUT_STATUS.GOOD:
117− return <AccessoryIcon.Success />;
118− case INPUT_STATUS.FAIL:
119− return <AccessoryIcon.Failure />;
120− default:
121− return null;
122− }
123− },
124− });
163+ ]);
125
164
126 return (
165 return (
127− <View>
166+ <Grid className={className}>
128− <Input {...pointInput} />
167+ <Grid.Item full as={Input} {...pointInput} />
129− <Input {...ticketInput} accessory={displayTicketStatus} />
168+
169+ {!isUsingShards && <Grid.Item full as={Input} {...ticketInput} />}
170+ {isUsingShards && (
171+ <>
172+ <Grid.Item full as={Input} {...shard1Input} />
173+ <Grid.Item full as={Input} {...shard2Input} />
174+ <Grid.Item full as={Input} {...shard3Input} />
175+ </>
176+ )}
177+ {hasPassphraseInput.data && (
130− {advanced && <Input {...passphraseInput} />}
178+ <Grid.Item full as={Input} {...passphraseInput} />
131−
179+ )}
132− <ForwardButton
180+
133− className={'mt3'}
181+ <Grid.Item full as={CheckboxInput} {...hasPassphraseInput} />
134− disabled={!canContinue()}
182+ <Grid.Item full as={CheckboxInput} {...shardsInput} />
135− onClick={doContinue}>
183+ </Grid>
136− Continue
137− </ForwardButton>
138− </View>
139 );
184 );
140}
185}
src/views/Login/Trezor.js+96−83
@@ -1,112 +1,125 @@
1+import React, { useEffect } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
1import * as bip32 from 'bip32';
3import * as bip32 from 'bip32';
2−import React, { useState } from 'react';
3−import Maybe from 'folktale/maybe';
4import { times } from 'lodash';
4import { times } from 'lodash';
5import TrezorConnect from 'trezor-connect';
5import TrezorConnect from 'trezor-connect';
6import * as secp256k1 from 'secp256k1';
6import * as secp256k1 from 'secp256k1';
7+import {
8+ Text,
9+ Input,
10+ Grid,
11+ H5,
12+ CheckboxInput,
13+ SelectInput,
7−import { H1, P, Input } from 'indigo-react';
14+} from 'indigo-react';
8
15
9−import View from 'components/View';
16+import { useWallet } from 'store/wallet';
10−import { ForwardButton } from 'components/Buttons';
11−import { InnerLabelDropdown } from 'components/old/Base';
12
17
13import { TREZOR_PATH } from 'lib/trezor';
18import { TREZOR_PATH } from 'lib/trezor';
14−import useWalletType from 'lib/useWalletType';
15import { WALLET_TYPES } from 'lib/wallet';
19import { WALLET_TYPES } from 'lib/wallet';
16−import useResetPointCursor from 'lib/useResetPointCursor';
20+import useLoginView from 'lib/useLoginView';
17−
21+import {
18−import { useWallet } from 'store/wallet';
22+ useHdPathInput,
19−
23+ useCheckboxInput,
20−const accountOptions = [
24+ useSelectInput,
21− { title: 'Custom path', value: 'custom' },
25+} from 'lib/useInputs';
22− ...times(20, i => ({ title: `Account #${i + 1}`, value: i })),
23−];
24
26
25−export default function Trezor({ loginCompleted }) {
27+import { ForwardButton } from 'components/Buttons';
26− useResetPointCursor();
27− useWalletType(WALLET_TYPES.TREZOR);
28−
29− const { wallet, setWallet, setWalletHdPath } = useWallet();
30−
31− const [hdPath, setHdPath] = useState(TREZOR_PATH.replace(/x/g, 0));
32− const [account, setAccount] = useState(0);
33
28
34− const handleAccountSelection = account => {
29+const ACCOUNT_OPTIONS = times(20, i => ({
35− setAccount(account);
30+ text: `Account #${i + 1}`,
31+ value: i,
32+}));
33+
34+// see Ledger.js for context — Trezor is basicaly Ledger with less complexity
35+export default function Trezor({ className }) {
36+ useLoginView(WALLET_TYPES.TREZOR);
37+
38+ const { setWallet, setWalletHdPath } = useWallet();
39+
36− if (account !== 'custom') {
40+ // custom toggle
41+ const [customPathInput, { data: useCustomPath }] = useCheckboxInput({
42+ name: 'customPath',
43+ label: 'Custom HD Path',
44+ autoComplete: 'off',
45+ initialValue: false,
46+ });
47+
48+ // account input
49+ const [accountInput, { data: accountIndex }] = useSelectInput({
50+ name: 'account',
51+ label: 'Account',
52+ placeholder: 'Choose account...',
53+ options: ACCOUNT_OPTIONS,
54+ });
55+
56+ // hd path input
57+ const [
58+ hdPathInput,
59+ { data: hdPath },
60+ { setValue: setHdPath },
61+ ] = useHdPathInput({
62+ name: 'hdpath',
63+ label: 'HD Path',
37− setHdPath(TREZOR_PATH.replace(/x/g, account));
64+ initialValue: TREZOR_PATH.replace(/x/g, 0),
38− }
39− };
65+ });
40
66
41 const pollDevice = async () => {
67 const pollDevice = async () => {
42 TrezorConnect.manifest({
68 TrezorConnect.manifest({
43 email: 'bridge-trezor@urbit.org',
69 email: 'bridge-trezor@urbit.org',
44 appUrl: 'https://github.com/urbit/bridge',
70 appUrl: 'https://github.com/urbit/bridge',
45 });
71 });
72+
46− TrezorConnect.getPublicKey({ path: hdPath }).then(info => {
73+ const info = await TrezorConnect.getPublicKey({
47− if (info.success === true) {
74+ path: hdPath,
48− const payload = info.payload;
49− const publicKey = Buffer.from(payload.publicKey, 'hex');
50− const chainCode = Buffer.from(payload.chainCode, 'hex');
51− const pub = secp256k1.publicKeyConvert(publicKey, true);
52− const hd = bip32.fromPublicKey(pub, chainCode);
53− setWallet(Maybe.Just(hd));
54− setWalletHdPath(hdPath);
55− } else {
56− setWallet(Maybe.Nothing());
57− }
58 });
75 });
59− };
60
76
61− let accountTitle = accountOptions.find(o => o.value === account).title;
77+ if (info.success === true) {
62−
78+ const payload = info.payload;
63− const accountSelection = (
79+ const publicKey = Buffer.from(payload.publicKey, 'hex');
64− <InnerLabelDropdown
80+ const chainCode = Buffer.from(payload.chainCode, 'hex');
65− className="mt-8"
81+ const pub = secp256k1.publicKeyConvert(publicKey, true);
66− prop-size="md"
82+ const hd = bip32.fromPublicKey(pub, chainCode);
67− prop-format="innerLabel"
83+ setWallet(Just(hd));
68− options={accountOptions}
84+ setWalletHdPath(hdPath);
69− handleUpdate={handleAccountSelection}
85+ } else {
70− title="Account"
86+ setWallet(Nothing());
71− currentSelectionTitle={accountTitle}
87+ }
72− fullWidth={true}
73− />
74− );
88+ };
75
89
76− const pathSelection =
90+ useEffect(() => {
77− account !== 'custom' ? null : (
91+ if (useCustomPath) {
78− <Input
92+ // updated by useForm
79− className="mt3"
93+ } else {
80− name="hdPath"
94+ setHdPath(TREZOR_PATH.replace(/x/g, accountIndex));
81− label="HD path"
95+ }
82− autocomplete="off"
96+ }, [useCustomPath, setHdPath, accountIndex]);
83− initialValue={hdPath}
84− onValue={setHdPath}
85− />
86− );
87
97
88 return (
98 return (
89− <View>
99+ <Grid className={className}>
100+ <Grid.Item full as={H5}>
90− <H1>Authenticate With Your Trezor</H1>
101+ Authenticate With Your Trezor
102+ </Grid.Item>
91
103
92− <P>
104+ <Grid.Item full as={Text} className="f6 mb3">
93 Connect and authenticate to your Trezor. If you'd like to use a custom
105 Connect and authenticate to your Trezor. If you'd like to use a custom
94 derivation path, you may enter it below.
106 derivation path, you may enter it below.
95− </P>
107+ </Grid.Item>
108+
109+ {useCustomPath && <Grid.Item full as={Input} {...hdPathInput} />}
110+
111+ {!useCustomPath && <Grid.Item full as={SelectInput} {...accountInput} />}
96
112
97− {accountSelection}
113+ <Grid.Item full as={CheckboxInput} className="mv3" {...customPathInput} />
98− {pathSelection}
99
114
115+ <Grid.Item
116+ full
117+ as={ForwardButton}
118+ solid
119+ className="mt2"
100− <ForwardButton className="mt3" onClick={pollDevice}>
120+ onClick={pollDevice}>
101 Authenticate
121 Authenticate
102− </ForwardButton>
122+ </Grid.Item>
103−
123+ </Grid>
104− <ForwardButton
105− className="mt3"
106− disabled={Maybe.Nothing.hasInstance(wallet)}
107− onClick={loginCompleted}>
108− Continue
109− </ForwardButton>
110− </View>
111 );
124 );
112}
125}
src/views/Point.js+3−3
@@ -1,5 +1,5 @@
1import React, { useCallback } from 'react';
1import React, { useCallback } from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Just } from 'folktale/maybe';
3import * as need from 'lib/need';
3import * as need from 'lib/need';
4import { Grid } from 'indigo-react';
4import { Grid } from 'indigo-react';
5
5
@@ -48,7 +48,7 @@ export default function Point() {
48 const { availableInvites } = useInvites(point);
48 const { availableInvites } = useInvites(point);
49 const availableInvitesText = matchBlinky(availableInvites);
49 const availableInvitesText = matchBlinky(availableInvites);
50
50
51− const showActions = Maybe.Just.hasInstance(wallet);
51+ const showActions = Just.hasInstance(wallet);
52
52
53 const goAdmin = useCallback(() => history.push(ROUTE_NAMES.ADMIN), [history]);
53 const goAdmin = useCallback(() => history.push(ROUTE_NAMES.ADMIN), [history]);
54
54
@@ -61,7 +61,7 @@ export default function Point() {
61
61
62 return (
62 return (
63 <View>
63 <View>
64− <Passport point={Maybe.Just(point)} />
64+ <Passport point={Just(point)} />
65 <Grid className="pt2">
65 <Grid className="pt2">
66 <Grid.Item
66 <Grid.Item
67 full
67 full
src/views/Points.js+2−2
@@ -1,5 +1,5 @@
1import React from 'react';
1import React from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Nothing } from 'folktale/maybe';
3import { H1, H2, P, Grid } from 'indigo-react';
3import { H1, H2, P, Grid } from 'indigo-react';
4
4
5import { ROUTE_NAMES } from 'lib/routeNames';
5import { ROUTE_NAMES } from 'lib/routeNames';
@@ -59,7 +59,7 @@ export default function Points() {
59 ...spawning,
59 ...spawning,
60 ]);
60 ]);
61
61
62− const loading = Maybe.Nothing.hasInstance(ownedPoints);
62+ const loading = Nothing.hasInstance(ownedPoints);
63
63
64 const outgoing = ownedPoints.filter(point =>
64 const outgoing = ownedPoints.filter(point =>
65 getDetails(point).matchWith({
65 getDetails(point).matchWith({
src/views/Reticket/Verify.js+5−6
@@ -1,13 +1,13 @@
1import React, { useCallback, useMemo } from 'react';
1import React, { useCallback, useMemo } from 'react';
2import { Input } from 'indigo-react';
2import { Input } from 'indigo-react';
3
3
4−import View from 'components/View';
5−import { useTicketInput } from 'components/Inputs';
4+import { useTicketInput } from 'lib/useInputs';
6−import { ForwardButton } from 'components/Buttons';
7−
8import { useLocalRouter } from 'lib/LocalRouter';
5import { useLocalRouter } from 'lib/LocalRouter';
9import { validateExactly } from 'lib/validators';
6import { validateExactly } from 'lib/validators';
10
7
8+import View from 'components/View';
9+import { ForwardButton } from 'components/Buttons';
10+
11const STUB_VERIFY_TICKET = process.env.NODE_ENV === 'development';
11const STUB_VERIFY_TICKET = process.env.NODE_ENV === 'development';
12
12
13//TODO deduplicate with PassportVerify
13//TODO deduplicate with PassportVerify
@@ -19,14 +19,13 @@ export default function Verify({ newWallet }) {
19 () => [validateExactly(ticket, 'Does not match expected master ticket.')],
19 () => [validateExactly(ticket, 'Does not match expected master ticket.')],
20 [ticket]
20 [ticket]
21 );
21 );
22− const ticketInput = useTicketInput({
22+ const [ticketInput, { pass }] = useTicketInput({
23 name: 'ticket',
23 name: 'ticket',
24 label: 'New master ticket',
24 label: 'New master ticket',
25 initialValue: STUB_VERIFY_TICKET ? ticket : undefined,
25 initialValue: STUB_VERIFY_TICKET ? ticket : undefined,
26 autoFocus: true,
26 autoFocus: true,
27 validators,
27 validators,
28 });
28 });
29− const { pass } = ticketInput;
30
29
31 const next = useCallback(() => push(names.RETICKET), [push, names]);
30 const next = useCallback(() => push(names.RETICKET), [push, names]);
32
31
src/views/SetKeys.js+3−3
@@ -1,5 +1,5 @@
1import React from 'react';
1import React from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Just, Nothing } from 'folktale/maybe';
3import * as azimuth from 'azimuth-js';
3import * as azimuth from 'azimuth-js';
4import * as ob from 'urbit-ob';
4import * as ob from 'urbit-ob';
5import * as need from '../lib/need';
5import * as need from '../lib/need';
@@ -136,10 +136,10 @@ class SetKeys extends React.Component {
136 state.discontinuity
136 state.discontinuity
137 );
137 );
138
138
139− return Maybe.Just(txn);
139+ return Just(txn);
140 }
140 }
141
141
142− return Maybe.Nothing();
142+ return Nothing();
143 }
143 }
144
144
145 render() {
145 render() {
src/views/SetProxy.js+5−5
@@ -1,5 +1,5 @@
1−import Maybe from 'folktale/maybe';
2import React from 'react';
1import React from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
3import * as need from '../lib/need';
3import * as need from '../lib/need';
4import * as azimuth from 'azimuth-js';
4import * as azimuth from 'azimuth-js';
5import {
5import {
@@ -66,18 +66,18 @@ class _SetProxy extends React.Component {
66 const txArgs = [validContracts, validPoint, state.proxyAddress];
66 const txArgs = [validContracts, validPoint, state.proxyAddress];
67
67
68 if (props.proxyType === PROXY_TYPE.TRANSFER_PROXY) {
68 if (props.proxyType === PROXY_TYPE.TRANSFER_PROXY) {
69− return Maybe.Just(azimuth.ecliptic.setTransferProxy(...txArgs));
69+ return Just(azimuth.ecliptic.setTransferProxy(...txArgs));
70 }
70 }
71
71
72 if (props.proxyType === PROXY_TYPE.SPAWN_PROXY) {
72 if (props.proxyType === PROXY_TYPE.SPAWN_PROXY) {
73− return Maybe.Just(azimuth.ecliptic.setSpawnProxy(...txArgs));
73+ return Just(azimuth.ecliptic.setSpawnProxy(...txArgs));
74 }
74 }
75
75
76 if (props.proxyType === PROXY_TYPE.MANAGEMENT_PROXY) {
76 if (props.proxyType === PROXY_TYPE.MANAGEMENT_PROXY) {
77− return Maybe.Just(azimuth.ecliptic.setManagementProxy(...txArgs));
77+ return Just(azimuth.ecliptic.setManagementProxy(...txArgs));
78 }
78 }
79
79
80− return Maybe.Nothing();
80+ return Nothing();
81 }
81 }
82
82
83 render() {
83 render() {
src/views/Transfer.js+2−2
@@ -1,5 +1,5 @@
1−import Maybe from 'folktale/maybe';
2import React from 'react';
1import React from 'react';
2+import { Just } from 'folktale/maybe';
3import * as azimuth from 'azimuth-js';
3import * as azimuth from 'azimuth-js';
4import * as ob from 'urbit-ob';
4import * as ob from 'urbit-ob';
5import * as need from '../lib/need';
5import * as need from '../lib/need';
@@ -55,7 +55,7 @@ class Transfer extends React.Component {
55 state.receivingAddress
55 state.receivingAddress
56 );
56 );
57
57
58− return Maybe.Just(txn);
58+ return Just(txn);
59 }
59 }
60
60
61 render() {
61 render() {
src/views/ViewPoint.js+4−5
@@ -1,5 +1,5 @@
1import React, { useCallback } from 'react';
1import React, { useCallback } from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Just } from 'folktale/maybe';
3import { Input } from 'indigo-react';
3import { Input } from 'indigo-react';
4
4
5import { H1, P } from '../components/old/Base';
5import { H1, P } from '../components/old/Base';
@@ -9,23 +9,22 @@ import { usePointCursor } from 'store/pointCursor';
9
9
10import { ROUTE_NAMES } from 'lib/routeNames';
10import { ROUTE_NAMES } from 'lib/routeNames';
11import patp2dec from 'lib/patp2dec';
11import patp2dec from 'lib/patp2dec';
12+import { usePointInput } from 'lib/useInputs';
12
13
13import View from 'components/View';
14import View from 'components/View';
14import { ForwardButton } from 'components/Buttons';
15import { ForwardButton } from 'components/Buttons';
15−import { usePointInput } from 'components/Inputs';
16
16
17export default function ViewPoint() {
17export default function ViewPoint() {
18 const history = useHistory();
18 const history = useHistory();
19 const { setPointCursor } = usePointCursor();
19 const { setPointCursor } = usePointCursor();
20− const pointInput = usePointInput({
20+ const [pointInput, { data: pointName, pass }] = usePointInput({
21 name: 'point',
21 name: 'point',
22 autoFocus: true,
22 autoFocus: true,
23 });
23 });
24− const { data: pointName, pass } = pointInput;
25
24
26 const disabled = !pass;
25 const disabled = !pass;
27 const goForward = useCallback(() => {
26 const goForward = useCallback(() => {
28− setPointCursor(Maybe.Just(patp2dec(pointName)));
27+ setPointCursor(Just(patp2dec(pointName)));
29 history.popAndPush(ROUTE_NAMES.POINT);
28 history.popAndPush(ROUTE_NAMES.POINT);
30 }, [setPointCursor, history, pointName]);
29 }, [setPointCursor, history, pointName]);
31
30