on master help sign in
Merge pull request #180 from urbit/feat/indigo-login

indigo-ify login components

commita531c3055d3d83bc7057a613209e9ee5361c99b3 authorMatt Condon <mattgcondon@gmail.com> dateTue 9 Jul 2019, 15:08 (7 years ago) parent6605d991dbc526
66 files changed+1612−1313
src/Bridge.js+1−1 src/components/Buttons.js+3−3 src/components/Footer.js+2−1 src/components/Inputs.js−171 src/components/Passport.js+4−4 src/components/Tabs.js+27−8 src/components/UploadButton.js+19 src/components/View.js+8−2 src/components/old/RenderedPoint.js+4−4 src/indigo-react/components/AccessoryIcon.js+1−1 src/indigo-react/components/Button.js+2−1 src/indigo-react/components/CheckboxInput.js+21−24 src/indigo-react/components/Flex.js+21−13 src/indigo-react/components/Grid.js+24−16 src/indigo-react/components/Input.js+7−21 src/indigo-react/components/SelectInput.js+151 src/indigo-react/index.js+2 src/indigo-react/lib/index.js+1 src/indigo-react/lib/useForm.js+85−41 src/indigo-react/lib/useOnClickOutside.js+24 src/lib/keys.js+4−4 src/lib/ledger.js+6−6 src/lib/lib.js+6 src/lib/useInputs.js+221 src/lib/useLoginView.js+17 src/lib/useMailer.js+5−5 src/lib/useResetPointCursor.js−12 src/lib/useWalletType.js−11 src/lib/validators.js+58−13 src/lib/wallet.js+26−59 src/store/lib/useBirthdaysStore.js+7−8 src/store/lib/useControlledPointsStore.js+11−11 src/store/lib/useDetailsStore.js+2−3 src/store/lib/useInvitesStore.js+7−7 src/store/lib/useRekeyDateStore.js+1−2 src/store/network.js+5−5 src/store/pointCursor.js+2−2 src/store/wallet.js+13−16 src/style/indigo-proposals.scss+4 src/views/Activate/ActivateCode.js+7−8 src/views/Activate/ActivateDisclaimer.js+6−5 src/views/Activate/ActivatePassport.js+3−4 src/views/Activate/PassportDownload.js+3−3 src/views/Activate/PassportVerify.js+3−4 src/views/Activate/useActivateFlowState.js+7−7 src/views/Admin/Reticket/ReticketVerify.js+47 src/views/CancelTransfer.js+5−4 src/views/CreateGalaxy.js+6−6 src/views/Invite/InviteEmail.js+15−15 src/views/Login.js+83−42 src/views/Login/Advanced.js+31−110 src/views/Login/Hardware.js+36 src/views/Login/Keystore.js+40−70 src/views/Login/Ledger.js+188−168 src/views/Login/Mnemonic.js+43−52 src/views/Login/PrivateKey.js+17−28 src/views/Login/Shards.js−98 src/views/Login/Ticket.js+150−105 src/views/Login/Trezor.js+96−83 src/views/Point.js+3−3 src/views/Points.js+2−2 src/views/Reticket/Verify.js+5−6 src/views/SetKeys.js+3−3 src/views/SetProxy.js+5−5 src/views/Transfer.js+2−2 src/views/ViewPoint.js+4−5
src/Bridge.js+1−1
@@ -23,7 +23,7 @@ const INITIAL_NETWORK_TYPE = isDevelopment
2323const SHOULD_STUB_LOCAL = process.env.REACT_APP_STUB_LOCAL === 'true';
2424const IS_STUBBED = isDevelopment && SHOULD_STUB_LOCAL;
2525const INITIAL_ROUTES = IS_STUBBED
26− ? [{ key: ROUTE_NAMES.LANDING }, { key: ROUTE_NAMES.ACTIVATE }]
26+ ? [{ key: ROUTE_NAMES.LANDING }, { key: ROUTE_NAMES.LOGIN }]
2727 : [{ key: ROUTE_NAMES.LANDING }];
2828
2929const INITIAL_WALLET = IS_STUBBED
src/components/Buttons.js+3−3
@@ -1,9 +1,9 @@
11import React from 'react';
22import { Button } from 'indigo-react';
33
4−// TODO: use svg icons
5−export const ForwardButton = props => <Button accessory="→" {...props} />;
6−export const DownloadButton = props => <Button accessory="⬇" {...props} />;
4+// NOTE: the -> is correct because inter recognizes the pair
5+export const ForwardButton = props => <Button accessory="->" {...props} />;
6+export const DownloadButton = props => <Button accessory="↓" {...props} />;
77export const OutButton = props => (
88 <Button as="a" target="_blank" accessory="↗" {...props} />
99);
src/components/Footer.js+2−1
@@ -1,8 +1,9 @@
1+import React from 'react';
12import { createTeleporter } from 'react-teleporter';
23
34const FooterTeleporter = createTeleporter();
45
56const Footer = FooterTeleporter.Source;
6−Footer.Target = FooterTeleporter.Target;
7+Footer.Target = props => <FooterTeleporter.Target as="footer" {...props} />;
78
89export 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 @@
11import React from 'react';
22import cn from 'classnames';
3−import Maybe from 'folktale/maybe';
3+import { Just, Nothing } from 'folktale/maybe';
44import * as ob from 'urbit-ob';
55import { Grid, Flex, Text } from 'indigo-react';
66import { times } from 'lodash';
@@ -36,7 +36,7 @@ export default function Passport({
3636 point,
3737 className,
3838 ticket = null,
39− address = Maybe.Nothing(),
39+ address = Nothing(),
4040 mini = false,
4141}) {
4242 const birthday = usePointBirthday(point.getOrElse(null));
@@ -46,8 +46,8 @@ export default function Passport({
4646 });
4747
4848 const name = point.matchWith({
49− Nothing: () => Maybe.Nothing(),
50− Just: p => Maybe.Just(ob.patp(p.value)),
49+ Nothing: () => Nothing(),
50+ Just: p => Just(ob.patp(p.value)),
5151 });
5252 const visualName = name.matchWith({
5353 Nothing: () => '...',
src/components/Tabs.js+27−8
@@ -1,18 +1,37 @@
11import React from 'react';
2−import HorizontalSelector from './old/HorizontalSelector';
2+import cn from 'classnames';
3+import { Grid, Flex } from 'indigo-react';
34
45export default function Tabs({
5− tabViews,
6− tabOptions,
6+ views,
7+ options,
78 currentTab,
89 onTabChange,
10+ className,
911 ...rest
1012}) {
11− const Component = tabViews[currentTab];
13+ const Tab = views[currentTab];
14+
1215 return (
13− <>
14− <HorizontalSelector options={tabOptions} onChange={onTabChange} />
15− <Component {...rest} />
16− </>
16+ <Grid className={className}>
17+ <Grid.Item full as={Flex} wrap className="b-gray3 bb1">
18+ {options.map(option => {
19+ const isActive = option.value === currentTab;
20+ return (
21+ <Flex.Item
22+ key={option.value}
23+ onClick={() => onTabChange(option.value)}
24+ className={cn('f5 pv3 mr6 clickable', {
25+ 'black b-black bb1': isActive,
26+ gray3: !isActive,
27+ })}
28+ style={{ marginBottom: '-1px' }}>
29+ {option.text}
30+ </Flex.Item>
31+ );
32+ })}
33+ </Grid.Item>
34+ <Grid.Item full as={Tab} className="mt6" {...rest} />
35+ </Grid>
1736 );
1837}
src/components/UploadButton.jsnew file+19
@@ -0,0 +1,19 @@
1+import React, { useRef } from 'react';
2+import { Button } from 'indigo-react';
3+
4+export default function UploadButton({ children, onChange, ...rest }) {
5+ const button = useRef();
6+
7+ return (
8+ <label htmlFor="file" {...rest}>
9+ <Button accessory="↑">{children}</Button>
10+ <input
11+ id="file"
12+ ref={button}
13+ className="super-hidden"
14+ type="file"
15+ onChange={() => onChange(button.current)}
16+ />
17+ </label>
18+ );
19+}
src/components/View.js+8−2
@@ -5,7 +5,7 @@ import { Flex } from 'indigo-react';
55import Footer from './Footer';
66
77// 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 }) {
99 return (
1010 <Flex
1111 col
@@ -19,7 +19,13 @@ function View({ className, children, full = false, ...rest }) {
1919 className
2020 )}
2121 {...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>
2329 <Footer.Target />
2430 </Flex>
2531 );
src/components/old/RenderedPoint.js+4−4
@@ -1,5 +1,5 @@
1−import Maybe from 'folktale/maybe';
21import React from 'react';
2+import { Just } from 'folktale/maybe';
33import { Col } from './Base';
44import { Button } from './Base';
55import { pour } from 'sigil-js';
@@ -24,7 +24,7 @@ const RenderedPoint = props => {
2424 key={`rendered-sigil-${point}`}
2525 className="clickable"
2626 onClick={() => {
27− setPointCursor(Maybe.Just(point));
27+ setPointCursor(Just(point));
2828 routeHandler(ROUTE_NAMES.POINT);
2929 }}>
3030 {sigil}
@@ -33,7 +33,7 @@ const RenderedPoint = props => {
3333 key={`rendered-name-${point}`}
3434 className="clickable"
3535 onClick={() => {
36− setPointCursor(Maybe.Just(point));
36+ setPointCursor(Just(point));
3737 routeHandler(ROUTE_NAMES.POINT);
3838 }}>
3939 <code>{name}</code>
@@ -44,7 +44,7 @@ const RenderedPoint = props => {
4444 prop-type={'link'}
4545 prop-size={'sm'}
4646 onClick={() => {
47− setPointCursor(Maybe.Just(point));
47+ setPointCursor(Just(point));
4848 routeHandler(ROUTE_NAMES.POINT);
4949 }}>
5050 {'Details →'}
src/indigo-react/components/AccessoryIcon.js+1−1
@@ -4,7 +4,7 @@ import Flex from './Flex';
44
55const PENDING_ACCESSORY = '⋯';
66const SUCCESS_ACCESSORY = '✓';
7−const FAILURE_ACCESSORY = '×';
7+const FAILURE_ACCESSORY = '✗';
88
99function AccessoryIcon({ ...props }) {
1010 return (
src/indigo-react/components/Button.js+2−1
@@ -6,6 +6,7 @@ import Flex from './Flex';
66import { HelpText } from './Typography';
77
88export default function Button({
9+ as: As = 'a',
910 solid = false,
1011 success = false,
1112 disabled = false,
@@ -23,7 +24,7 @@ export default function Button({
2324 };
2425 return (
2526 <Grid
26− as="a"
27+ as={As}
2728 gap={1}
2829 className={cn(
2930 'pointer pv4 truncate flex-row justify-between',
src/indigo-react/components/CheckboxInput.js+21−24
@@ -12,9 +12,6 @@ export default function CheckboxInput({
1212
1313 // callbacks
1414 onValue,
15− onPass,
16− onError,
17− onFocus,
1815 onEnter,
1916
2017 // state from hook
@@ -22,6 +19,7 @@ export default function CheckboxInput({
2219 pass,
2320 visiblyPassed,
2421 error,
22+ hintError,
2523 data,
2624 bind,
2725 autoFocus,
@@ -37,7 +35,7 @@ export default function CheckboxInput({
3735}) {
3836 return (
3937 <Flex
40− className={className}
38+ className={cn(className, 'mv2')}
4139 row
4240 align="center"
4341 style={{
@@ -46,35 +44,34 @@ export default function CheckboxInput({
4644 cursor: 'not-allowed',
4745 }),
4846 }}>
47+ {/* we totally hide the checkbox itself */}
4948 <Flex.Item
5049 as="input"
5150 {...rest}
52− className={cn(
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− )}
51+ className={cn('super-hidden')}
6952 id={name}
7053 name={name}
7154 {...bind}
7255 />
56+ {/* and then display a prettier one in its stead */}
7357 <Flex.Item
58+ flex
7459 as="label"
75− className="f6 ph3 lh-tall us-none"
76− htmlFor={name}
77− flex>
60+ className="f6 mr3 lh-tall us-none clickable flex-row align-center"
61+ htmlFor={name}>
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>
7875 {label}
7976 </Flex.Item>
8077 </Flex>
src/indigo-react/components/Flex.js+21−13
@@ -1,21 +1,25 @@
11import React from 'react';
22import cn from 'classnames';
33
4−function Flex({
5− as: As = 'div',
6− row = false,
7− wrap = false,
8− col = false,
9− align,
10− justify,
11− className,
12− ...rest
13−}) {
4+const Flex = React.forwardRef(function Flex(
5+ {
6+ as: As = 'div',
7+ row = false,
8+ wrap = false,
9+ col = false,
10+ align,
11+ justify,
12+ className,
13+ ...rest
14+ },
15+ ref
16+) {
1417 if (row && col) {
1518 throw new Error('Only one of row or col must be true, not both.');
1619 }
1720 return (
1821 <As
22+ ref={ref}
1923 className={cn(
2024 'flex',
2125 {
@@ -32,16 +36,20 @@ function Flex({
3236 {...rest}
3337 />
3438 );
35−}
39+});
3640
3741// flex can be boolean {true} or integer flex
38−Flex.Item = function FlexItem({ as: As = 'div', flex, className, ...rest }) {
42+Flex.Item = React.forwardRef(function FlexItem(
43+ { as: As = 'div', flex, className, ...rest },
44+ ref
45+) {
3946 if (flex === true) {
4047 flex = 1;
4148 }
4249
4350 return (
4451 <As
52+ ref={ref}
4553 className={cn(
4654 {
4755 [`flex${flex}`]: flex,
@@ -51,6 +59,6 @@ Flex.Item = function FlexItem({ as: As = 'div', flex, className, ...rest }) {
5159 {...rest}
5260 />
5361 );
54−};
62+});
5563
5664export default Flex;
src/indigo-react/components/Grid.js+24−16
@@ -1,9 +1,13 @@
11import React from 'react';
22import cn from 'classnames';
33
4−function Grid({ as: As = 'div', gap = 0, align, justify, className, ...rest }) {
4+const Grid = React.forwardRef(function Grid(
5+ { as: As = 'div', gap = 0, align, justify, className, ...rest },
6+ ref
7+) {
58 return (
69 <As
10+ ref={ref}
711 className={cn(
812 'grid12',
913 gap && `gap${gap}`,
@@ -16,23 +20,27 @@ function Grid({ as: As = 'div', gap = 0, align, justify, className, ...rest }) {
1620 {...rest}
1721 />
1822 );
19−}
23+});
2024
21−Grid.Item = function GridItem({
22− as: As = 'div',
23− full = false,
24− half = 0,
25− third = 0,
26− fourth = 0,
27− rows = [],
28− cols = [],
29− justifySelf,
30− alignSelf,
31− className,
32− ...rest
33−}) {
25+Grid.Item = React.forwardRef(function GridItem(
26+ {
27+ as: As = 'div',
28+ full = false,
29+ half = 0,
30+ third = 0,
31+ fourth = 0,
32+ rows = [],
33+ cols = [],
34+ justifySelf,
35+ alignSelf,
36+ className,
37+ ...rest
38+ },
39+ ref
40+) {
3441 return (
3542 <As
43+ ref={ref}
3644 className={cn(
3745 {
3846 full,
@@ -53,7 +61,7 @@ Grid.Item = function GridItem({
5361 {...rest}
5462 />
5563 );
56−};
64+});
5765
5866Grid.Divider = function GridDivider({
5967 vertical = false,
src/indigo-react/components/Input.js+7−21
@@ -14,9 +14,6 @@ export default function Input({
1414
1515 // callbacks
1616 onValue,
17− onPass,
18− onError,
19− onFocus,
2017 onEnter,
2118
2219 // state from hook
@@ -24,6 +21,7 @@ export default function Input({
2421 pass,
2522 visiblyPassed,
2623 error,
24+ hintError,
2725 data,
2826 bind,
2927 autoFocus,
@@ -49,28 +47,16 @@ export default function Input({
4947 );
5048
5149 // 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
5253 useEffect(() => {
5354 pass && onValue && onValue(data);
5455 }, [pass, data]); // eslint-disable-line react-hooks/exhaustive-deps
5556
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−
7157 return (
7258 <Flex
73− className={className}
59+ className={cn(className, 'mb1')}
7460 col
7561 style={{
7662 ...(disabled && {
@@ -99,8 +85,8 @@ export default function Input({
9985 {
10086 'b-green3': visiblyPassed,
10187 'b-black': focused && !visiblyPassed,
102− 'b-yellow3': !focused && error,
103− 'b-gray3': !focused && !error && !visiblyPassed,
88+ 'b-yellow3': !focused && hintError,
89+ 'b-gray3': !focused && !hintError && !visiblyPassed,
10490 }
10591 // TODO: inputClassName ?
10692 )}
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';
88export { default as IconButton } from './components/IconButton';
99
1010export * 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 @@
11import { useCallback, useMemo, useState } from 'react';
2−import { keyBy, get, every, some } from 'lodash';
2+import { isEqual, keyBy, get, every, some } from 'lodash';
33
44import { compose } from 'lib/lib';
55import { kDefaultValidator } from 'lib/validators';
66import useSetState from 'lib/useSetState';
7+import usePreviousValue from 'lib/usePreviousValue';
78
89// interface InputConfig {
910// name: string;
@@ -24,12 +25,22 @@ const defaultsFor = (configs, mapper) =>
2425/**
2526 * useForm manages a set of inputs for rendering them in a loop
2627 */
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+
2837 const byName = useMemo(() => keyBy(configs, 'name'), [configs]);
2938
3039 // track values
3140 const [values, _setValues] = useState(() =>
32− defaultsFor(configs, config => config.initialValue || '')
41+ defaultsFor(configs, config =>
42+ config.initialValue === undefined ? '' : config.initialValue
43+ )
3344 );
3445 // ^ NB(shrugs): because we're not syncing the initialValue of _additional_
3546 // configs that are added, we won't be able to set an initialValue for a
@@ -38,11 +49,6 @@ export default function useForm(configs = []) {
3849 // but for dynamically added input configs, we won't get validation, etc
3950 // until there's a traditional state update.
4051
41− const setValue = useCallback(
42− (name, value) => _setValues(values => ({ ...values, [name]: value })),
43− [_setValues]
44− );
45−
4652 // track focused states
4753 const [focused, setFocused] = useSetState(() =>
4854 defaultsFor(configs, config => config.autoFocus && !config.disabled)
@@ -55,12 +61,22 @@ export default function useForm(configs = []) {
5561 // track whether or not input has been focused
5662 const [hasBeenFocused, setHasBeenFocused] = useSetState({});
5763
64+ // track whether or not the input has been touched
65+ const [hasBeenTouched, setHasBeenTouched] = useSetState({});
66+
5867 // build fn that transforms a value by input name
5968 const transform = useCallback(
6069 (name, value) => compose(...get(byName, [name, 'transformers'], []))(value),
6170 [byName]
6271 );
6372
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+
6480 // build fn that validates a value by input name
6581 const validate = useCallback(
6682 (name, value) =>
@@ -84,8 +100,11 @@ export default function useForm(configs = []) {
84100
85101 // on change, transform and set value
86102 const onChange = useCallback(
87− name => e => setValue(name, transform(name, getValue(name, e))), //
88− [setValue, transform, getValue]
103+ name => e => {
104+ setValue(name, getValue(name, e));
105+ setHasBeenTouched({ [name]: true });
106+ }, //
107+ [setValue, getValue, setHasBeenTouched]
89108 );
90109
91110 // on focus, update focus
@@ -113,27 +132,45 @@ export default function useForm(configs = []) {
113132 const datas = useMemo(() => validations.map(v => v.data), [validations]);
114133
115134 // 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
116146 // 1) has been focused before
117− // 2)
118− // a) did not pass validation and has an error text
119− // b) or has a specific error from parent state
120− // (and we implicitly return the error text at the end)
121− const errors = useMemo(
147+ // 2) and has an error
148+ // boolean[]
149+ const hintErrors = useMemo(
122150 () =>
123151 configs.map(
124− (config, i) =>
125− hasBeenFocused[config.name] && //
126− (validations[i].error || config.error)
152+ (config, i) => hasBeenFocused[config.name] && !!errorTexts[i]
127153 ),
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]
129166 );
130167
131168 // this input has passed if
132169 // 1) it has passed local validation
133170 // 2) there are no errors
134171 const passes = useMemo(
135− () => validations.map((v, i) => v.pass && !errors[i]),
136− [validations, errors]
172+ () => validations.map((v, i) => v.pass && !errorTexts[i]),
173+ [validations, errorTexts]
137174 );
138175
139176 // visibly tell the user that their input has passed if
@@ -153,27 +190,32 @@ export default function useForm(configs = []) {
153190 const inputs = useMemo(
154191 () =>
155192 configs.map(
156− ({ name, error, autoFocus, disabled, initialValue, ...rest }, i) => ({
157− // Input props
158− name,
159− data: datas[i],
160− pass: passes[i],
161− visiblyPassed: visiblePasses[i],
162− error: errors[i],
163− 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),
193+ ({ name, error, autoFocus, disabled, initialValue, ...rest }, i) => {
194+ const value =
195+ values[name] || (initialValue === undefined ? '' : initialValue);
196+ return {
197+ // Input props
198+ name,
199+ data: datas[i],
200+ pass: passes[i],
201+ visiblyPassed: visiblePasses[i],
202+ error: errors[i],
203+ hintError: hintErrors[i],
204+ focused: focuses[i],
174205 autoFocus: autoFocus && !disabled,
175− },
176− })
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+ },
217+ };
218+ }
177219 ),
178220 [
179221 configs,
@@ -181,6 +223,7 @@ export default function useForm(configs = []) {
181223 visiblePasses,
182224 passes,
183225 errors,
226+ hintErrors,
184227 focuses,
185228 values,
186229 onBlur,
@@ -198,5 +241,6 @@ export default function useForm(configs = []) {
198241 inputs,
199242 pass,
200243 error,
244+ setValue,
201245 };
202246}
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 @@
11import BN from 'bn.js';
2−import Maybe from 'folktale/maybe';
2+import { Just, Nothing } from 'folktale/maybe';
33import * as need from './need';
44
55import * as noun from '../nockjs/noun';
@@ -119,13 +119,13 @@ const attemptNetworkSeedDerivation = async (next, args) => {
119119 const seed = await kg.deriveNetworkSeed(managementSeed, '', revision);
120120
121121 if (seed === '') {
122− return Maybe.Nothing();
122+ return Nothing();
123123 }
124124
125− return Maybe.Just(seed);
125+ return Just(seed);
126126 }
127127
128− return Maybe.Nothing();
128+ return Nothing();
129129};
130130
131131export { genKey, attemptNetworkSeedDerivation };
src/lib/ledger.js+6−6
@@ -1,12 +1,14 @@
11import Transport from '@ledgerhq/hw-transport-u2f';
22import Eth from '@ledgerhq/hw-app-eth';
33
4−const LEDGER_LEGACY_PATH = "m/44'/60'/0'/x";
5−const LEDGER_LIVE_PATH = "m/44'/60'/x'/0/0";
4+export const LEDGER_LEGACY_PATH = "m/44'/60'/0'/x";
5+export const LEDGER_LIVE_PATH = "m/44'/60'/x'/0/0";
66
7−const chopHdPrefix = str => (str.slice(0, 2) === 'm/' ? str.slice(2) : str);
7+export const chopHdPrefix = str =>
8+ str.slice(0, 2) === 'm/' ? str.slice(2) : str;
9+export const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
810
9−const ledgerSignTransaction = async (txn, hdpath) => {
11+export const ledgerSignTransaction = async (txn, hdpath) => {
1012 const transport = await Transport.create();
1113 const eth = new Eth(transport);
1214
@@ -20,5 +22,3 @@ const ledgerSignTransaction = async (txn, hdpath) => {
2022
2123 return txn;
2224};
23−
24−export { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH, ledgerSignTransaction };
src/lib/lib.js+6
@@ -34,6 +34,11 @@ const randomPatq = len => {
3434 return ob.hex2patq(hex);
3535};
3636
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+
3742export {
3843 compose,
3944 allFalse,
@@ -44,4 +49,5 @@ export {
4449 fill,
4550 isValidGalaxy,
4651 randomPatq,
52+ patpStringLength,
4753};
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 @@
11import { useCallback } from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Just, Nothing } from 'folktale/maybe';
33
44import { hasReceived, sendMail } from './inviteMail';
55import useSetState from './useSetState';
@@ -10,24 +10,24 @@ function useHasReceivedCache() {
1010 const [cache, addToCache] = useSetState();
1111
1212 const getHasRecieved = useCallback(
13− email => cache[email] || Maybe.Nothing(), //
13+ email => cache[email] || Nothing(), //
1414 [cache]
1515 );
1616
1717 const syncHasReceivedForEmail = useCallback(
1818 async email => {
19− if (Maybe.Just.hasInstance(getHasRecieved(email))) {
19+ if (Just.hasInstance(getHasRecieved(email))) {
2020 // never update the cache after we know about it
2121 return;
2222 }
2323
2424 if (STUB_MAILER) {
2525 // always allow sending emails when stubbing
26− return addToCache({ [email]: Maybe.Just(false) });
26+ return addToCache({ [email]: Just(false) });
2727 }
2828
2929 const _hasReceived = await hasReceived(email);
30− addToCache({ [email]: Maybe.Just(_hasReceived) });
30+ addToCache({ [email]: Just(_hasReceived) });
3131 },
3232 [getHasRecieved, addToCache]
3333 );
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 @@
11import * as bip39 from 'bip39';
22import * as ob from 'urbit-ob';
3−import { identity } from 'lodash';
3+import { identity, includes } from 'lodash';
44
55import { isValidAddress } from './wallet';
66import 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
712
813// via: https://emailregex.com/
914const 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,}))$/;
1015
1116// 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]/;
1321
1422// Wraps single validation functions in a controlled and predictable way.
1523export const simpleValidatorWrapper = ({
@@ -129,30 +137,33 @@ export const validateShard = m =>
129137 error: 'This is not a valid shard',
130138 });
131139
132−export const validateLength = (m, l) =>
140+export const validateOneOf = (options = []) => m =>
133141 simpleValidatorWrapper({
134142 prevMessage: m,
135− validator: d => {
136− try {
137− return d.length === l;
138− } catch {
139− return false;
140− }
141− },
142− error: 'This is of an invalid length',
143+ validator: d => includes(options, d),
144+ error: `Is not a valid option.`,
143145 });
144146
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
145155export const validateNetworkKey = m =>
146156 simpleValidatorWrapper({
147157 prevMessage: m,
148− validator: d => hexRegExp.test(d),
158+ validator: d => plain64CharHexValue.test(d),
149159 error: 'This is not a valid network key',
150160 });
151161
162+// @deprecate
152163export const validateNetworkSeed = m =>
153164 simpleValidatorWrapper({
154165 prevMessage: m,
155− validator: d => hexRegExp.test(d),
166+ validator: d => plain64CharHexValue.test(d),
156167 error: 'This is not a valid network seed',
157168 });
158169
@@ -192,3 +203,37 @@ export const validateExactly = (value, error) => m =>
192203 validator: d => d === value,
193204 error,
194205 });
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';
22import * as bip39 from 'bip39';
33import keccak from 'keccak';
44import { reduce } from 'lodash';
5−import Maybe from 'folktale/maybe';
5+import { Just, Nothing } from 'folktale/maybe';
66import * as secp256k1 from 'secp256k1';
77import * as ob from 'urbit-ob';
88import * as kg from 'urbit-key-generation/dist';
99import { isAddress } from 'web3-utils';
1010
11−const DEFAULT_HD_PATH = "m/44'/60'/0'/0/0";
12−
13−const ETH_ZERO_ADDR = '0x0000000000000000000000000000000000000000';
14−const CURVE_ZERO_ADDR =
11+export const DEFAULT_HD_PATH = "m/44'/60'/0'/0/0";
12+export const ETH_ZERO_ADDR = '0x0000000000000000000000000000000000000000';
13+export const CURVE_ZERO_ADDR =
1514 '0x0000000000000000000000000000000000000000000000000000000000000000';
1615
17−const WALLET_TYPES = {
16+export const WALLET_TYPES = {
1817 MNEMONIC: Symbol('MNEMONIC'),
1918 TICKET: Symbol('TICKET'),
2019 SHARDS: Symbol('SHARDS'),
@@ -24,31 +23,14 @@ const WALLET_TYPES = {
2423 KEYSTORE: Symbol('KEYSTORE'),
2524};
2625
27−function EthereumWallet(privateKey) {
26+export function EthereumWallet(privateKey) {
2827 this.privateKey = privateKey;
2928 this.publicKey = secp256k1.publicKeyCreate(this.privateKey);
3029 const pub = this.publicKey.toString('hex');
3130 this.address = addressFromSecp256k1Public(pub);
3231}
3332
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 => {
5234 const compressed = false;
5335 const uncompressed = secp256k1.publicKeyConvert(
5436 Buffer.from(pub, 'hex'),
@@ -60,20 +42,22 @@ const addressFromSecp256k1Public = pub => {
6042 return toChecksumAddress(addr);
6143};
6244
63−const addHexPrefix = hex => (hex.slice(0, 2) === '0x' ? hex : '0x' + hex);
45+export const addHexPrefix = hex =>
46+ hex.slice(0, 2) === '0x' ? hex : '0x' + hex;
6447
65−const stripHexPrefix = hex => (hex.slice(0, 2) === '0x' ? hex.slice(2) : hex);
48+export const stripHexPrefix = hex =>
49+ hex.slice(0, 2) === '0x' ? hex.slice(2) : hex;
6650
67−const keccak256 = str =>
51+export const keccak256 = str =>
6852 keccak('keccak256')
6953 .update(str)
7054 .digest();
7155
72−const isValidAddress = a => '0x0' === a || isAddress(a);
56+export const isValidAddress = a => '0x0' === a || isAddress(a);
7357
74−const isZeroAddress = a => ETH_ZERO_ADDR === a;
58+export const isZeroAddress = a => ETH_ZERO_ADDR === a;
7559
76−const toChecksumAddress = address => {
60+export const toChecksumAddress = address => {
7761 const addr = stripHexPrefix(address).toLowerCase();
7862 const hash = keccak256(addr).toString('hex');
7963
@@ -85,27 +69,28 @@ const toChecksumAddress = address => {
8569 );
8670};
8771
88−const eqAddr = (addr0, addr1) =>
72+export const eqAddr = (addr0, addr1) =>
8973 toChecksumAddress(addr0) === toChecksumAddress(addr1);
9074
91−const urbitWalletFromTicket = async (ticket, point, passphrase) => {
75+export const urbitWalletFromTicket = async (ticket, point, passphrase) => {
9276 if (typeof point === 'string') {
9377 //TODO why not in kg?
9478 point = ob.patp2dec(point);
9579 }
96− let uhdw = await kg.generateWallet({
80+
81+ return await kg.generateWallet({
9782 ticket: ticket,
9883 ship: point,
9984 passphrase: passphrase,
10085 });
101− return uhdw;
10286};
10387
104−const ownershipWalletFromTicket = async (ticket, point, passphrase) => {
88+export const ownershipWalletFromTicket = async (ticket, point, passphrase) => {
10589 if (typeof point === 'string') {
10690 //TODO why not in kg?
10791 point = ob.patp2dec(point);
10892 }
93+
10994 return await kg.generateOwnershipWallet({
11095 ticket,
11196 ship: point,
@@ -113,10 +98,10 @@ const ownershipWalletFromTicket = async (ticket, point, passphrase) => {
11398 });
11499};
115100
116−const walletFromMnemonic = (mnemonic, hdpath, passphrase) => {
101+export const walletFromMnemonic = (mnemonic, hdpath, passphrase) => {
117102 const seed = bip39.validateMnemonic(mnemonic)
118− ? Maybe.Just(bip39.mnemonicToSeed(mnemonic, passphrase))
119− : Maybe.Nothing();
103+ ? Just(bip39.mnemonicToSeed(mnemonic, passphrase))
104+ : Nothing();
120105
121106 const toWallet = (sd, path) => {
122107 let wal;
@@ -124,9 +109,9 @@ const walletFromMnemonic = (mnemonic, hdpath, passphrase) => {
124109 const hd = bip32.fromSeed(sd);
125110 wal = hd.derivePath(path);
126111 wal.address = addressFromSecp256k1Public(wal.publicKey);
127− wal = Maybe.Just(wal);
112+ wal = Just(wal);
128113 } catch (_) {
129− wal = Maybe.Nothing();
114+ wal = Nothing();
130115 }
131116 return wal;
132117 };
@@ -134,21 +119,3 @@ const walletFromMnemonic = (mnemonic, hdpath, passphrase) => {
134119 const wallet = seed.chain(sd => toWallet(sd, hdpath));
135120 return wallet;
136121};
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 @@
11import { useCallback, useState } from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Just, Nothing } from 'folktale/maybe';
33import * as azimuth from 'azimuth-js';
44
55import { useNetwork } from '../network';
@@ -17,10 +17,9 @@ export default function useBirthdayStore() {
1717 [_setBirthdayCache]
1818 );
1919
20− const getBirthday = useCallback(
21− point => birthdayCache[point] || Maybe.Nothing(),
22− [birthdayCache]
23− );
20+ const getBirthday = useCallback(point => birthdayCache[point] || Nothing(), [
21+ birthdayCache,
22+ ]);
2423
2524 const syncBirthday = useCallback(
2625 async point => {
@@ -31,7 +30,7 @@ export default function useBirthdayStore() {
3130 }
3231
3332 // 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))) {
3534 return;
3635 }
3736
@@ -43,13 +42,13 @@ export default function useBirthdayStore() {
4342 if (birthBlock > 0) {
4443 const block = await _web3.eth.getBlock(birthBlock);
4544 addToBirthdayCache({
46− [point]: Maybe.Just(new Date(block.timestamp * 1000)),
45+ [point]: Just(new Date(block.timestamp * 1000)),
4746 });
4847 } else {
4948 // TODO: better encoding for "no birthday" state?
5049 // if there's no birthday, just use today
5150 addToBirthdayCache({
52− [point]: Maybe.Just(new Date()),
51+ [point]: Just(new Date()),
5352 });
5453 }
5554 },
src/store/lib/useControlledPointsStore.js+11−11
@@ -1,16 +1,16 @@
11import { useState, useCallback } from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Just, Nothing } from 'folktale/maybe';
33import * as azimuth from 'azimuth-js';
44
55import { useNetwork } from '../network';
66import { useWallet } from 'store/wallet';
77
88const EMPTY_CONTROLLED_POINTS = {
9− ownedPoints: Maybe.Nothing(),
10− incomingPoints: Maybe.Nothing(),
11− managingPoints: Maybe.Nothing(),
12− votingPoints: Maybe.Nothing(),
13− spawningPoints: Maybe.Nothing(),
9+ ownedPoints: Nothing(),
10+ incomingPoints: Nothing(),
11+ managingPoints: Nothing(),
12+ votingPoints: Nothing(),
13+ spawningPoints: Nothing(),
1414};
1515
1616export default function useControlledPointsStore() {
@@ -49,11 +49,11 @@ export default function useControlledPointsStore() {
4949 ]);
5050
5151 _setControlledPointsCache({
52− ownedPoints: Maybe.Just(ownedPoints),
53− incomingPoints: Maybe.Just(incomingPoints),
54− managingPoints: Maybe.Just(managingPoints),
55− votingPoints: Maybe.Just(votingPoints),
56− spawningPoints: Maybe.Just(spawningPoints),
52+ ownedPoints: Just(ownedPoints),
53+ incomingPoints: Just(incomingPoints),
54+ managingPoints: Just(managingPoints),
55+ votingPoints: Just(votingPoints),
56+ spawningPoints: Just(spawningPoints),
5757 });
5858 }, [contracts, wallet, _setControlledPointsCache]);
5959
src/store/lib/useDetailsStore.js+2−3
@@ -1,5 +1,5 @@
11import { useState, useCallback } from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Just, Nothing } from 'folktale/maybe';
33import * as azimuth from 'azimuth-js';
44
55import { useNetwork } from '../network';
@@ -20,8 +20,7 @@ export default function useDetailsStore() {
2020 // TODO: refactor detailsCache access to use accessor like bithday
2121 // Maybe<{}>
2222 const getDetails = useCallback(
23− point =>
24− point in detailsCache ? Maybe.Just(detailsCache[point]) : Maybe.Nothing(),
23+ point => (point in detailsCache ? Just(detailsCache[point]) : Nothing()),
2524 [detailsCache]
2625 );
2726
src/store/lib/useInvitesStore.js+7−7
@@ -1,14 +1,14 @@
11import { useCallback, useState } from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Just, Nothing } from 'folktale/maybe';
33import * as azimuth from 'azimuth-js';
44
55import { useNetwork } from '../network';
66
77// the default value of a point's invites
88const EMPTY_INVITES = {
9− availableInvites: Maybe.Nothing(),
10− sentInvites: Maybe.Nothing(),
11− acceptedInvites: Maybe.Nothing(),
9+ availableInvites: Nothing(),
10+ sentInvites: Nothing(),
11+ acceptedInvites: Nothing(),
1212};
1313
1414export default function useInvitesStore() {
@@ -52,9 +52,9 @@ export default function useInvitesStore() {
5252
5353 addToInvitesCache({
5454 [point]: {
55− availableInvites: Maybe.Just(availableInvites),
56− sentInvites: Maybe.Just(sentInvites),
57− acceptedInvites: Maybe.Just(acceptedInvites),
55+ availableInvites: Just(availableInvites),
56+ sentInvites: Just(sentInvites),
57+ acceptedInvites: Just(acceptedInvites),
5858 },
5959 });
6060 },
src/store/lib/useRekeyDateStore.js+1−2
@@ -1,6 +1,5 @@
11import { useCallback, useState } from 'react';
22import { Just, Nothing } from 'folktale/maybe';
3−import * as azimuth from 'azimuth-js';
43
54import { useNetwork } from '../network';
65
@@ -56,7 +55,7 @@ export default function useRekeyDateStore() {
5655 });
5756 }
5857 },
59− [web3, addToRekeyDateCache]
58+ [web3, contracts, addToRekeyDateCache]
6059 );
6160
6261 return { getRekeyDate, syncRekeyDate };
src/store/network.js+5−5
@@ -5,9 +5,9 @@ import React, {
55 useMemo,
66 useState,
77} from 'react';
8+import { Just, Nothing } from 'folktale/maybe';
89import * as azimuth from 'azimuth-js';
910import Web3 from 'web3';
10−import Maybe from 'folktale/maybe';
1111import { includes } from 'lodash';
1212
1313import { CONTRACT_ADDRESSES } from '../lib/contracts';
@@ -33,8 +33,8 @@ function _useNetwork(initialNetworkType = null) {
3333 const contracts = azimuth.initContracts(web3, contractAddresses);
3434
3535 return {
36− web3: Maybe.Just(web3),
37− contracts: Maybe.Just(contracts),
36+ web3: Just(web3),
37+ contracts: Just(contracts),
3838 };
3939 };
4040
@@ -82,8 +82,8 @@ function _useNetwork(initialNetworkType = null) {
8282 new Web3.providers.HttpProvider('http://example.com:3456'),
8383 isDevelopment ? CONTRACT_ADDRESSES.DEV : CONTRACT_ADDRESSES.MAINNET
8484 ),
85− web3: Maybe.Nothing(),
86− // ^ overwrite the web3 object from initWeb3 with a Maybe.Nothing
85+ web3: Nothing(),
86+ // ^ overwrite the web3 object from initWeb3 with a Nothing
8787 // to indicate that there is no valid web3 connection
8888 };
8989 }
src/store/pointCursor.js+2−2
@@ -1,10 +1,10 @@
11import React, { createContext, forwardRef, useContext, useState } from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Nothing } from 'folktale/maybe';
33
44export const PointCursorContext = createContext(null);
55
66// pointCursor is Maybe<number>
7−function _usePointCursor(initialPointCursor = Maybe.Nothing()) {
7+function _usePointCursor(initialPointCursor = Nothing()) {
88 const [pointCursor, setPointCursor] = useState(initialPointCursor);
99
1010 return {
src/store/wallet.js+13−16
@@ -5,7 +5,7 @@ import React, {
55 useContext,
66 useState,
77} from 'react';
8−import Maybe from 'folktale/maybe';
8+import { Just, Nothing } from 'folktale/maybe';
99import { includes } from 'lodash';
1010
1111import {
@@ -28,21 +28,18 @@ const DEFAULT_WALLET_TYPE = WALLET_TYPES.TICKET;
2828// it doesn't necessarily help us much, since we branch on 'walletType'
2929// before setting 'wallet'.
3030//
31−// Wallets are always wrapped in Maybe. For most authentication
31+// Wallets are always wrapped in For most authentication
3232// mechanisms, Maybe wraps a BIP32 HD wallet; for Ethereum private keys,
3333// JSON keystore files, and Metamask authentication, it wraps an
3434// 'EthereumWallet'.
35−function _useWallet(
36− initialWallet = Maybe.Nothing(),
37− initialMnemonic = Maybe.Nothing()
38−) {
35+function _useWallet(initialWallet = Nothing(), initialMnemonic = Nothing()) {
3936 const [walletType, _setWalletType] = useState(DEFAULT_WALLET_TYPE);
4037 const [walletHdPath, setWalletHdPath] = useState(DEFAULT_HD_PATH);
4138 const [wallet, _setWallet] = useState(initialWallet);
42− const [urbitWallet, _setUrbitWallet] = useState(Maybe.Nothing());
39+ const [urbitWallet, _setUrbitWallet] = useState(Nothing());
4340 const [authMnemonic, setAuthMnemonic] = useState(initialMnemonic);
44− const [networkSeed, setNetworkSeed] = useState(Maybe.Nothing());
45− const [networkRevision, setNetworkRevision] = useState(Maybe.Nothing());
41+ const [networkSeed, setNetworkSeed] = useState(Nothing());
42+ const [networkRevision, setNetworkRevision] = useState(Nothing());
4643
4744 const setWalletType = useCallback(
4845 walletType => {
@@ -70,7 +67,7 @@ function _useWallet(
7067
7168 const setUrbitWallet = useCallback(
7269 urbitWallet => {
73− if (Maybe.Just.hasInstance(urbitWallet)) {
70+ if (Just.hasInstance(urbitWallet)) {
7471 // when an urbit wallet is set, also derive
7572 // a normal bip32 wallet using the ownership address
7673 const wallet = walletFromMnemonic(
@@ -81,7 +78,7 @@ function _useWallet(
8178
8279 setWallet(wallet);
8380 } else {
84− setWallet(Maybe.Nothing());
81+ setWallet(Nothing());
8582 }
8683
8784 _setUrbitWallet(urbitWallet);
@@ -92,11 +89,11 @@ function _useWallet(
9289 const resetWallet = useCallback(() => {
9390 _setWalletType(DEFAULT_WALLET_TYPE);
9491 setWalletHdPath(DEFAULT_HD_PATH);
95− _setWallet(Maybe.Nothing());
96− _setUrbitWallet(Maybe.Nothing());
97− setAuthMnemonic(Maybe.Nothing());
98− setNetworkSeed(Maybe.Nothing());
99− setNetworkRevision(Maybe.Nothing());
92+ _setWallet(Nothing());
93+ _setUrbitWallet(Nothing());
94+ setAuthMnemonic(Nothing());
95+ setNetworkSeed(Nothing());
96+ setNetworkRevision(Nothing());
10097 }, [
10198 _setWalletType,
10299 setWalletHdPath,
src/style/indigo-proposals.scss+4
@@ -39,3 +39,7 @@
3939.auto-rows-min {
4040 grid-auto-rows: min-content;
4141}
42+
43+.hover-bg-grey3:hover {
44+ background-color: #e6e6e6;
45+}
src/views/Activate/ActivateCode.js+7−8
@@ -1,7 +1,6 @@
11import React, { useCallback, useEffect, useState } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
23import * as azimuth from 'azimuth-js';
3−import Maybe from 'folktale/maybe';
4−import { useTicketInput } from 'components/Inputs';
54import { Grid, Input, H4, ErrorText } from 'indigo-react';
65
76import View from 'components/View';
@@ -10,6 +9,7 @@ import Passport from 'components/Passport';
109
1110import { useHistory } from 'store/history';
1211
12+import { useTicketInput } from 'lib/useInputs';
1313import * as need from 'lib/need';
1414import { ROUTE_NAMES } from 'lib/routeNames';
1515import { useSyncKnownPoints } from 'lib/useSyncPoints';
@@ -36,12 +36,11 @@ export default function ActivateCode() {
3636 setDerivedPoint,
3737 } = useActivateFlow();
3838
39− const ticketInput = useTicketInput({
39+ const [ticketInput, { pass: validTicket }] = useTicketInput({
4040 name: 'ticket',
4141 label: 'Code',
4242 autoFocus: true,
4343 });
44− const validTicket = ticketInput.pass;
4544
4645 const goToLogin = useCallback(() => history.popAndPush(ROUTE_NAMES.LOGIN), [
4746 history,
@@ -81,13 +80,13 @@ export default function ActivateCode() {
8180 );
8281 const incoming = [...owned, ...transferring];
8382
84− let realPoint = Maybe.Nothing();
85− let wallet = Maybe.Nothing();
83+ let realPoint = Nothing();
84+ let wallet = Nothing();
8685
8786 if (incoming.length > 0) {
8887 const pointNum = parseInt(incoming[0], 10);
89− realPoint = Maybe.Just(pointNum);
90− wallet = Maybe.Just(await generateWallet(pointNum));
88+ realPoint = Just(pointNum);
89+ wallet = Just(await generateWallet(pointNum));
9190
9291 if (incoming.length > 1) {
9392 setGeneralError(
src/views/Activate/ActivateDisclaimer.js+6−5
@@ -2,17 +2,18 @@ import React, { useCallback } from 'react';
22import cn from 'classnames';
33import { Grid, H3, B, Text, CheckboxInput } from 'indigo-react';
44
5+import { useCheckboxInput } from 'lib/useInputs';
6+import { useLocalRouter } from 'lib/LocalRouter';
7+
58import View from 'components/View';
6−import { useCheckboxInput } from 'components/Inputs';
79import { ForwardButton } from 'components/Buttons';
8−import { useLocalRouter } from 'lib/LocalRouter';
910import WarningBox from 'components/WarningBox';
1011
1112const TEXT_STYLE = 'f5';
1213
1314export default function ActivateDisclaimer() {
1415 const { push, names } = useLocalRouter();
15− const checkboxInput = useCheckboxInput({
16+ const [understoodInput, { data: isUnderstood }] = useCheckboxInput({
1617 name: 'checkbox',
1718 label: 'I acknowledge and understand these rights',
1819 });
@@ -70,10 +71,10 @@ export default function ActivateDisclaimer() {
7071 <Grid.Item full as={WarningBox}>
7172 Warning: Nobody but you can restore or reset your Master Ticket
7273 </Grid.Item>
73− <Grid.Item as={CheckboxInput} {...checkboxInput} full />
74+ <Grid.Item as={CheckboxInput} {...understoodInput} full />
7475 <Grid.Item
7576 as={ForwardButton}
76− disabled={!checkboxInput.data}
77+ disabled={!isUnderstood}
7778 onClick={goToPassport}
7879 solid
7980 full>
src/views/Activate/ActivatePassport.js+3−4
@@ -46,8 +46,7 @@ export default function ActivatePassport() {
4646 const gap = useBreakpoints([4, 4, 7]);
4747 const marginTop = useBreakpoints([false, false, 8]);
4848 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]);
5150
5251 // addresses are not derived until we set[Urbit]Wallet(), so do that inline
5352 const address = useMemo(
@@ -68,7 +67,7 @@ export default function ActivatePassport() {
6867 <LocalRouterProvider value={router}>
6968 <View full={fullView}>
7069 <Grid gap={gap} className="mt8 mb10">
71− <Grid.Item half={leftHalf} full={full}>
70+ <Grid.Item half={half && 1} full={full}>
7271 <Passport
7372 className={cn({ [`mt${marginTop}`]: marginTop })}
7473 point={derivedPoint}
@@ -77,7 +76,7 @@ export default function ActivatePassport() {
7776 />
7877 </Grid.Item>
7978 <Grid.Item
80− half={rightHalf}
79+ half={half && 2}
8180 full={full}
8281 as={Route}
8382 resetActivateRouter={resetActivateRouter}
src/views/Activate/PassportDownload.js+3−3
@@ -1,6 +1,6 @@
11import React, { useState, useEffect, useCallback } from 'react';
22import cn from 'classnames';
3−import Maybe from 'folktale/maybe';
3+import { Just, Nothing } from 'folktale/maybe';
44import { Grid, H4, P } from 'indigo-react';
55
66import * as need from 'lib/need';
@@ -23,7 +23,7 @@ export default function PassportDownload({ className }) {
2323 // const point = need.point(derivedPoint);
2424 const wallet = need.wallet(derivedWallet);
2525
26− const [paper, setPaper] = useState(Maybe.Nothing());
26+ const [paper, setPaper] = useState(Nothing());
2727 const [downloaded, setDownloaded] = useState(false);
2828
2929 const pointAsString = derivedPoint.matchWith({
@@ -79,7 +79,7 @@ export default function PassportDownload({ className }) {
7979 <PaperRenderer
8080 point={pointAsString}
8181 wallet={wallet}
82− callback={data => setPaper(Maybe.Just(data))}
82+ callback={data => setPaper(Just(data))}
8383 />
8484 </Grid>
8585 );
src/views/Activate/PassportVerify.js+3−4
@@ -4,13 +4,13 @@ import { Grid, H4, Input, P } from 'indigo-react';
44
55import * as need from 'lib/need';
66import { useLocalRouter } from 'lib/LocalRouter';
7+import { useTicketInput } from 'lib/useInputs';
8+import { validateExactly } from 'lib/validators';
79
810import { ForwardButton } from 'components/Buttons';
911import Steps from 'components/Steps';
10−import { useTicketInput } from 'components/Inputs';
1112
1213import { useActivateFlow } from './ActivateFlow';
13−import { validateExactly } from 'lib/validators';
1414
1515const STUB_VERIFY_TICKET = process.env.NODE_ENV === 'development';
1616
@@ -24,14 +24,13 @@ export default function PassportVerify({ className }) {
2424 () => [validateExactly(ticket, 'Does not match expected master ticket.')],
2525 [ticket]
2626 );
27− const ticketInput = useTicketInput({
27+ const [ticketInput, { pass }] = useTicketInput({
2828 name: 'ticket',
2929 label: 'Master Ticket',
3030 initialValue: STUB_VERIFY_TICKET ? ticket : undefined,
3131 autoFocus: true,
3232 validators,
3333 });
34− const { pass } = ticketInput;
3534
3635 return (
3736 <Grid className={cn(className, 'auto-rows-min')}>
src/views/Activate/useActivateFlowState.js+7−7
@@ -1,16 +1,16 @@
11import { useCallback, useState } from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Nothing } from 'folktale/maybe';
33
44export default function useActivateFlowState() {
5− const [derivedWallet, setDerivedWallet] = useState(Maybe.Nothing());
6− const [inviteWallet, setInviteWallet] = useState(Maybe.Nothing());
7− const [derivedPoint, setDerivedPoint] = useState(Maybe.Nothing());
5+ const [derivedWallet, setDerivedWallet] = useState(Nothing());
6+ const [inviteWallet, setInviteWallet] = useState(Nothing());
7+ const [derivedPoint, setDerivedPoint] = useState(Nothing());
88 const [generated, setGenerated] = useState(false);
99
1010 const reset = useCallback(() => {
11− setDerivedWallet(Maybe.Nothing());
12− setInviteWallet(Maybe.Nothing());
13− setDerivedPoint(Maybe.Nothing());
11+ setDerivedWallet(Nothing());
12+ setInviteWallet(Nothing());
13+ setDerivedPoint(Nothing());
1414 setGenerated(false);
1515 }, [setDerivedWallet, setInviteWallet, setDerivedPoint, setGenerated]);
1616
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';
21import React from 'react';
3−import { H1, P } from '../components/old/Base';
2+import { Just } from 'folktale/maybe';
43import * as azimuth from 'azimuth-js';
54import * as ob from 'urbit-ob';
5+
6+import { H1, P } from '../components/old/Base';
67import * as need from '../lib/need';
78
89import StatelessTransaction from '../components/old/StatelessTransaction';
@@ -40,13 +41,13 @@ class CancelTransfer extends React.Component {
4041 ETH_ZERO_ADDR
4142 );
4243
43− return Maybe.Just(txn);
44+ return Just(txn);
4445 }
4546
4647 render() {
4748 const { props, state } = this;
4849
49− const online = Maybe.Just.hasInstance(props.web3);
50+ const online = Just.hasInstance(props.web3);
5051
5152 const proxy = online
5253 ? props.pointCache[state.pointInTransfer].transferProxy
src/views/CreateGalaxy.js+6−6
@@ -1,5 +1,5 @@
11import React from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Just, Nothing } from 'folktale/maybe';
33import * as azimuth from 'azimuth-js';
44import * as ob from 'urbit-ob';
55import * as need from '../lib/need';
@@ -68,10 +68,10 @@ class CreateGalaxy extends React.Component {
6868
6969 createUnsignedTxn = () => {
7070 const { state, props } = this;
71− if (isValidAddress(state.galaxyOwner) === false) return Maybe.Nothing();
72− if (state.isAvailable === false) return Maybe.Nothing();
73− if (canDecodePatp(state.galaxyName) === false) return Maybe.Nothing();
74− if (isValidGalaxy(state.galaxyName) === false) return Maybe.Nothing();
71+ if (isValidAddress(state.galaxyOwner) === false) return Nothing();
72+ if (state.isAvailable === false) return Nothing();
73+ if (canDecodePatp(state.galaxyName) === false) return Nothing();
74+ if (isValidGalaxy(state.galaxyName) === false) return Nothing();
7575
7676 const validContracts = need.contracts(props.contracts);
7777
@@ -83,7 +83,7 @@ class CreateGalaxy extends React.Component {
8383 state.galaxyOwner
8484 );
8585
86− return Maybe.Just(txn);
86+ return Just(txn);
8787 };
8888
8989 confirmAvailability = async () => {
src/views/Invite/InviteEmail.js+15−15
@@ -1,6 +1,6 @@
11import React, { useCallback, useEffect, useMemo, useState } from 'react';
22import cn from 'classnames';
3−import Maybe from 'folktale/maybe';
3+import { Just } from 'folktale/maybe';
44import * as azimuth from 'azimuth-js';
55import {
66 Grid,
@@ -11,11 +11,14 @@ import {
1111 Text,
1212 ErrorText,
1313 AccessoryIcon,
14+ useForm,
1415} from 'indigo-react';
1516import { uniq } from 'lodash';
1617
17−import * as need from 'lib/need';
18−import * as wg from 'lib/walletgen';
18+import { usePointCursor } from 'store/pointCursor';
19+import { useNetwork } from 'store/network';
20+import { usePointCache } from 'store/pointCache';
21+import { useWallet } from 'store/wallet';
1922
2023// for transaction generation and signing
2124import {
@@ -28,22 +31,19 @@ import {
2831} from 'lib/txn';
2932import * as tank from 'lib/tank';
3033import { useLocalRouter } from 'lib/LocalRouter';
31−
32−import MiniBackButton from 'components/MiniBackButton';
33−import { usePointCursor } from 'store/pointCursor';
34−import LoadableButton from 'components/LoadableButton';
3534import useArray from 'lib/useArray';
36−import useForm from 'indigo-react/lib/useForm';
37−import { buildEmailInputConfig } from 'components/Inputs';
35+import { buildEmailInputConfig } from 'lib/useInputs';
3836import { MIN_PLANET, GAS_LIMITS } from 'lib/constants';
39−import Highlighted from 'components/Highlighted';
40−import { useNetwork } from 'store/network';
41−import { usePointCache } from 'store/pointCache';
42−import { useWallet } from 'store/wallet';
37+import * as need from 'lib/need';
38+import * as wg from 'lib/walletgen';
4339import useSetState from 'lib/useSetState';
4440import pluralize from 'lib/pluralize';
4541import useMailer from 'lib/useMailer';
4642
43+import MiniBackButton from 'components/MiniBackButton';
44+import LoadableButton from 'components/LoadableButton';
45+import Highlighted from 'components/Highlighted';
46+
4747const GAS_PRICE_GWEI = 20; // we pay the premium for faster ux
4848const GAS_LIMIT = GAS_LIMITS.GIFT_PLANET;
4949const INVITE_COST = toWei((GAS_PRICE_GWEI * GAS_LIMIT).toString(), 'gwei');
@@ -232,7 +232,7 @@ export default function InviteEmail() {
232232 walletHdPath,
233233 networkType,
234234 // TODO: ^ make a useTransactionSigner to encapsulate this logic
235− txn: Maybe.Just(inviteTx),
235+ txn: Just(inviteTx),
236236 gasPrice: GAS_PRICE_GWEI.toString(),
237237 gasLimit: GAS_LIMIT.toString(),
238238 nonce: nonce + i,
@@ -300,7 +300,7 @@ export default function InviteEmail() {
300300 try {
301301 const txHash = await sendSignedTransaction(
302302 _web3,
303− Maybe.Just(invite.signedTx),
303+ Just(invite.signedTx),
304304 tankWasUsed,
305305 () => {}
306306 );
src/views/Login.js+83−42
@@ -1,58 +1,73 @@
1−import { Nothing, Just } from 'folktale/maybe';
2−import React, { useState } from 'react';
1+import React, { useCallback, useState } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
33import * as azimuth from 'azimuth-js';
44
5−import { H1 } from 'indigo-react';
6−import View from 'components/View';
7−import Tabs from 'components/Tabs';
5+import { H4, Grid } from 'indigo-react';
86
97import Ticket from './Login/Ticket';
108import Mnemonic from './Login/Mnemonic';
119import Advanced from './Login/Advanced';
10+import Hardware from './Login/Hardware';
1211
13−import useLifecycle from 'lib/useLifecycle';
1412import * as need from 'lib/need';
15−import { ROUTE_NAMES } from 'lib/routeNames';
1613
1714import { useHistory } from 'store/history';
1815import { useNetwork } from 'store/network';
1916import { useWallet } from 'store/wallet';
2017import { usePointCursor } from 'store/pointCursor';
2118
22−const TABS = {
23− TICKET: Symbol('TICKET'),
24− MNEMONIC: Symbol('MNEMONIC'),
25− ADVANCED: Symbol('ADVANCED'),
19+import View from 'components/View';
20+import Tabs from 'components/Tabs';
21+import Crumbs from 'components/Crumbs';
22+import Footer from 'components/Footer';
23+import { ForwardButton, OfflineButton } from 'components/Buttons';
24+
25+const NAMES = {
26+ TICKET: 'TICKET',
27+ MNEMONIC: 'MNEMONIC',
28+ HARDWARE: 'HARDWARE',
29+ ADVANCED: 'ADVANCED',
2630};
2731
28−const tabViews = {
29− [TABS.TICKET]: Ticket,
30− [TABS.MNEMONIC]: Mnemonic,
31− [TABS.ADVANCED]: Advanced,
32+const VIEWS = {
33+ [NAMES.TICKET]: Ticket,
34+ [NAMES.MNEMONIC]: Mnemonic,
35+ [NAMES.HARDWARE]: Hardware,
36+ [NAMES.ADVANCED]: Advanced,
3237};
3338
34−const tabOptions = [
35− { title: 'Ticket', value: TABS.TICKET },
36− { title: 'Mnemonic', value: TABS.MNEMONIC },
37− { title: 'Advanced', value: TABS.ADVANCED },
39+const OPTIONS = [
40+ { text: 'Master Ticket', value: NAMES.TICKET },
41+ { text: 'Mnemonic', value: NAMES.MNEMONIC },
42+ { text: 'Hardware', value: NAMES.HARDWARE },
43+ { text: 'Advanced', value: NAMES.ADVANCED },
3844];
3945
4046export default function Login() {
4147 // globals
42− const history = useHistory();
48+ const { push, replaceWith, names } = useHistory();
4349 const { contracts } = useNetwork();
44− const { wallet, resetWallet } = useWallet();
50+ const { wallet } = useWallet();
4551 const { pointCursor, setPointCursor } = usePointCursor();
4652
4753 // inputs
48− const [currentTab, setCurrentTab] = useState(TABS.TICKET);
54+ const [currentTab, setCurrentTab] = useState(NAMES.TICKET);
4955
50− // on-mount
51− useLifecycle(() => {
52− resetWallet();
53− });
56+ const goToActivate = useCallback(
57+ () => replaceWith([{ key: names.LANDING }, { key: names.ACTIVATE }]),
58+ [replaceWith, names]
59+ );
5460
55− const doContinue = async () => {
61+ const goToPoints = useCallback(() => {
62+ push(names.POINTS);
63+ }, [push, names]);
64+
65+ const goToPoint = useCallback(() => {
66+ goToPoints();
67+ push(names.POINT);
68+ }, [goToPoints, push, names]);
69+
70+ const doContinue = useCallback(async () => {
5671 const _wallet = need.wallet(wallet);
5772 const _contracts = need.contracts(contracts);
5873
@@ -80,25 +95,51 @@ export default function Login() {
8095 // navigate to that specific point, otherwise navigate to list of points
8196 if (Just.hasInstance(deduced)) {
8297 setPointCursor(deduced);
83− history.popAndPush(ROUTE_NAMES.POINTS);
84− history.push(ROUTE_NAMES.POINT);
98+ goToPoint();
8599 } else {
86− history.popAndPush(ROUTE_NAMES.POINTS);
100+ goToPoints();
87101 }
88− };
102+ }, [contracts, pointCursor, setPointCursor, wallet, goToPoint, goToPoints]);
89103
90104 return (
91− <View>
92− <H1>Login</H1>
93−
94− <Tabs
95− tabViews={tabViews}
96− tabOptions={tabOptions}
97− currentTab={currentTab}
98− onTabChange={setCurrentTab}
99− //
100− loginCompleted={doContinue}
101− />
105+ <View inset>
106+ <Grid>
107+ <Grid.Item full as={Crumbs} routes={[{ text: 'Multipass' }]} />
108+ <Grid.Item full as={H4} className="mt4">
109+ Login
110+ </Grid.Item>
111+
112+ <Grid.Item
113+ full
114+ as={Tabs}
115+ className="mt1"
116+ views={VIEWS}
117+ options={OPTIONS}
118+ currentTab={currentTab}
119+ onTabChange={setCurrentTab}
120+ />
121+
122+ <Grid.Item
123+ full
124+ as={ForwardButton}
125+ solid
126+ className="mt2"
127+ disabled={Nothing.hasInstance(wallet)}
128+ onClick={doContinue}>
129+ Continue
130+ </Grid.Item>
131+ </Grid>
132+
133+ <Footer>
134+ <Grid>
135+ <Grid.Divider />
136+ <Grid.Item full as={ForwardButton} onClick={goToActivate}>
137+ Activate
138+ </Grid.Item>
139+ <Grid.Divider />
140+ <Grid.Item full as={OfflineButton} />
141+ </Grid>
142+ </Footer>
102143 </View>
103144 );
104145}
src/views/Login/Advanced.js+31−110
@@ -1,115 +1,36 @@
1−import React from 'react';
2−import { P, InnerLabelDropdown } from '../../components/old/Base';
3−import { Row, Col } from '../../components/old/Base';
1+import React, { useState } from 'react';
42
5−import Ticket from './Ticket';
6−import Shards from './Shards';
7−import Ledger from './Ledger';
8−import Trezor from './Trezor';
9−import Mnemonic from './Mnemonic';
3+import Tabs from 'components/Tabs';
104import PrivateKey from './PrivateKey';
115import Keystore from './Keystore';
126
13−import { WALLET_TYPES, renderWalletType } from '../../lib/wallet';
14−import { compose } from '../../lib/lib';
15−import { withWallet } from '../../store/wallet';
16−
17−class Advanced extends React.Component {
18− constructor(props) {
19− super(props);
20−
21− this.state = {
22− selected: WALLET_TYPES.TICKET,
23− };
24−
25− this.handleSelection = this.handleSelection.bind(this);
26− }
27−
28− getWalletOptions() {
29− return [
30− {
31− title: 'Urbit Master Ticket',
32− value: WALLET_TYPES.TICKET,
33− },
34− {
35− title: 'Urbit Master Ticket (Shards)',
36− value: WALLET_TYPES.SHARDS,
37− },
38− {
39− type: 'divider',
40− },
41− {
42− title: 'Ledger',
43− value: WALLET_TYPES.LEDGER,
44− },
45− {
46− title: 'Trezor',
47− value: WALLET_TYPES.TREZOR,
48− },
49− {
50− type: 'divider',
51− },
52− {
53− title: 'BIP39 Mnemonic',
54− value: WALLET_TYPES.MNEMONIC,
55− },
56− {
57− title: 'Ethereum Private Key',
58− value: WALLET_TYPES.PRIVATE_KEY,
59− },
60− {
61− title: 'Ethereum Keystore File',
62− value: WALLET_TYPES.KEYSTORE,
63− },
64− ];
65− }
66−
67− handleSelection(selected) {
68− this.setState({ selected });
69− }
70−
71− render() {
72− const loginCompleted = this.props.loginCompleted;
73− const inputs = (() => {
74− switch (this.state.selected) {
75− case WALLET_TYPES.TICKET:
76− return <Ticket advanced={true} loginCompleted={loginCompleted} />;
77− case WALLET_TYPES.SHARDS:
78− return <Shards loginCompleted={loginCompleted} />;
79− //
80− case WALLET_TYPES.LEDGER:
81− return <Ledger loginCompleted={loginCompleted} />;
82− case WALLET_TYPES.TREZOR:
83− return <Trezor loginCompleted={loginCompleted} />;
84− //
85− case WALLET_TYPES.MNEMONIC:
86− return <Mnemonic advanced={true} loginCompleted={loginCompleted} />;
87− case WALLET_TYPES.PRIVATE_KEY:
88− return <PrivateKey loginCompleted={loginCompleted} />;
89− case WALLET_TYPES.KEYSTORE:
90− return <Keystore loginCompleted={loginCompleted} />;
91− //
92− default:
93− return <P>{'Coming soon.'}</P>;
94− }
95− })();
96−
97− return (
98− <Row>
99− <Col>
100− <InnerLabelDropdown
101− className={'mb-10 mt-6'}
102− title={'Wallet Type:'}
103− options={this.getWalletOptions()}
104− handleUpdate={this.handleSelection}
105− currentSelectionTitle={renderWalletType(this.state.selected)}
106− />
107−
108− {inputs}
109− </Col>
110− </Row>
111− );
112− }
7+const NAMES = {
8+ PRIVATE_KEY: 'PRIVATE_KEY',
9+ KEYSTORE: 'KEYSTORE',
10+};
11+
12+const VIEWS = {
13+ [NAMES.PRIVATE_KEY]: PrivateKey,
14+ [NAMES.KEYSTORE]: Keystore,
15+};
16+
17+const OPTIONS = [
18+ { text: 'Ethereum Private Key', value: NAMES.PRIVATE_KEY },
19+ { text: 'Ethereum Keystore', value: NAMES.KEYSTORE },
20+];
21+
22+export default function Advanced({ loginCompleted, className }) {
23+ const [currentTab, setCurrentTab] = useState(NAMES.PRIVATE_KEY);
24+
25+ return (
26+ <Tabs
27+ className={className}
28+ views={VIEWS}
29+ options={OPTIONS}
30+ currentTab={currentTab}
31+ onTabChange={setCurrentTab}
32+ //
33+ loginCompleted={loginCompleted}
34+ />
35+ );
11336}
114−
115−export default compose(withWallet)(Advanced);
src/views/Login/Hardware.jsnew file+36
@@ -0,0 +1,36 @@
1+import React, { useState } from 'react';
2+
3+import Tabs from 'components/Tabs';
4+import Ledger from './Ledger';
5+import Trezor from './Trezor';
6+
7+const NAMES = {
8+ LEDGER: 'LEDGER',
9+ TREZOR: 'TREZOR',
10+};
11+
12+const VIEWS = {
13+ [NAMES.LEDGER]: Ledger,
14+ [NAMES.TREZOR]: Trezor,
15+};
16+
17+const OPTIONS = [
18+ { text: 'Ledger', value: NAMES.LEDGER },
19+ { text: 'Trezor', value: NAMES.TREZOR },
20+];
21+
22+export default function Hardware({ loginCompleted, className }) {
23+ const [currentTab, setCurrentTab] = useState(NAMES.LEDGER);
24+
25+ return (
26+ <Tabs
27+ className={className}
28+ views={VIEWS}
29+ options={OPTIONS}
30+ currentTab={currentTab}
31+ onTabChange={setCurrentTab}
32+ //
33+ loginCompleted={loginCompleted}
34+ />
35+ );
36+}
src/views/Login/Keystore.js+40−70
@@ -1,39 +1,33 @@
1−import { Just, Nothing } from 'folktale/maybe';
2−import { Ok, Error } from 'folktale/result';
31import React, { useState } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
3+import { P, Grid, Input, ErrorText } from 'indigo-react';
44import * 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';
115
126import { useWallet } from 'store/wallet';
137
8+import { usePassphraseInput } from 'lib/useInputs';
149import * as need from 'lib/need';
1510import { EthereumWallet, WALLET_TYPES } from 'lib/wallet';
16−import useWalletType from 'lib/useWalletType';
17−import useResetPointCursor from 'lib/useResetPointCursor';
11+import useLoginView from 'lib/useLoginView';
1812
19−export default function Keystore({ loginCompleted }) {
20− useResetPointCursor();
21− useWalletType(WALLET_TYPES.KEYSTORE);
13+import { ForwardButton } from 'components/Buttons';
14+import UploadButton from 'components/UploadButton';
15+
16+export default function Keystore({ className }) {
17+ useLoginView(WALLET_TYPES.KEYSTORE);
2218
2319 // globals
24− const { wallet, setWallet } = useWallet();
20+ const { setWallet } = useWallet();
2521
22+ const [error, setError] = useState();
2623 // inputs
27− // keystore: Maybe<Result<String, String>>
24+ // keystore: Maybe<String>
2825 const [keystore, setKeystore] = useState(Nothing());
29− const passphraseInput = usePassphraseInput({
26+ const [passphraseInput, { data: passphrase }] = usePassphraseInput({
3027 name: 'password',
3128 label: 'Keystore password',
3229 autoFocus: true,
3330 });
34− const passphrase = passphraseInput.data;
35−
36− const [decryptionProblem, setDecryptionProblem] = useState(false);
3731
3832 const constructWallet = () => {
3933 try {
@@ -43,26 +37,27 @@ export default function Keystore({ loginCompleted }) {
4337 const privateKey = keythereum.recover(passphrase, json);
4438
4539 const newWallet = new EthereumWallet(privateKey);
46− setDecryptionProblem(false);
40+ setError();
4741 setWallet(Just(newWallet));
4842 } catch (err) {
49− setDecryptionProblem(true);
43+ setError(
44+ "Couldn't decrypt wallet. You may have entered an incorrect password."
45+ );
5046 setWallet(Nothing());
5147 }
5248 };
5349
54− const handleKeystoreUpload = event => {
55− const file = event.files.item(0);
50+ const handleKeystoreUpload = element => {
51+ const file = element.files.item(0);
5652 const reader = new FileReader();
5753
5854 reader.onload = e => {
5955 const keystore = e.target.result;
60− setKeystore(Just(Ok(keystore)));
56+ setKeystore(Just(keystore));
6157 };
6258
6359 const failure = _ => {
64− const message = 'There was a problem uploading your Keystore file';
65− setKeystore(Just(Error(message)));
60+ setError('There was a problem uploading your Keystore file');
6661 };
6762
6863 reader.onerror = failure;
@@ -71,59 +66,34 @@ export default function Keystore({ loginCompleted }) {
7166 reader.readAsText(file);
7267 };
7368
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−
9569 return (
96− <View>
97− <InputCaption>
70+ <Grid className={className}>
71+ <Grid.Item full as={P}>
9872 Please upload your Ethereum keystore file. If your keystore file is
9973 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>
10179
102− <UploadButton
103− className={`${uploadButtonClass} mt3`}
104− onChange={handleKeystoreUpload}>
105− <div className="flex-center-all fs-4 h-11 pointer">
106− Upload Keystore file
107− </div>
108− </UploadButton>
80+ {error && (
81+ <Grid.Item full as={ErrorText} className="mt1">
82+ {error}
83+ </Grid.Item>
84+ )}
10985
110− <Input {...passphraseInput} />
86+ <Grid.Item full as={Input} className="mt3" {...passphraseInput} />
11187
112− <ForwardButton
88+ <Grid.Item
89+ full
90+ as={ForwardButton}
91+ solid
11392 className="mt3"
11493 disabled={Nothing.hasInstance(keystore)}
11594 onClick={constructWallet}>
11695 Decrypt
117− </ForwardButton>
118−
119− {decryptMessage}
120−
121− <ForwardButton
122− className="mt3"
123− disabled={Nothing.hasInstance(wallet)}
124− onClick={loginCompleted}>
125− Continue
126− </ForwardButton>
127− </View>
96+ </Grid.Item>
97+ </Grid>
12898 );
12999}
src/views/Login/Ledger.js+188−168
@@ -1,190 +1,210 @@
1−import * as bip32 from 'bip32';
2−import React, { useState } from 'react';
1+import React, { useCallback, useEffect } 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';
313import { times } from 'lodash';
4−import Maybe from 'folktale/maybe';
14+import * as bip32 from 'bip32';
515import Transport from '@ledgerhq/hw-transport-u2f';
616import Eth from '@ledgerhq/hw-app-eth';
717import * 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';
1318
1419import { useWallet } from 'store/wallet';
1520
16−import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from 'lib/ledger';
17−import useWalletType from 'lib/useWalletType';
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,
31+ LEDGER_LEGACY_PATH,
32+ chopHdPrefix,
33+ addHdPrefix,
34+} from 'lib/ledger';
1835import { WALLET_TYPES } from 'lib/wallet';
19−import useResetPointCursor from 'lib/useResetPointCursor';
36+import useLoginView from 'lib/useLoginView';
37+import useBreakpoints from 'lib/useBreakpoints';
2038
21−const pathOptions = [
22− { title: 'Ledger Live', value: LEDGER_LIVE_PATH },
23− { title: 'Ledger Legacy', value: LEDGER_LEGACY_PATH },
24− { title: 'Custom path', value: 'custom' },
39+const PATH_OPTIONS = [
40+ { text: 'Ledger Live', value: LEDGER_LIVE_PATH },
41+ { text: 'Ledger Legacy', value: LEDGER_LEGACY_PATH },
2542];
2643
27−const chopHdPrefix = str => (str.slice(0, 2) === 'm/' ? str.slice(2) : str);
28−
29−const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
30−
31−export default function Ledger({ loginCompleted }) {
32− useResetPointCursor();
33− useWalletType(WALLET_TYPES.LEDGER);
34−
35− const { wallet, setWallet, setWalletHdPath } = useWallet();
36−
37− const [basePath, setBasePath] = useState(LEDGER_LIVE_PATH);
38− const [account, setAccount] = useState(0);
39− const [hdPath, setHdPath] = useState(LEDGER_LIVE_PATH.replace(/x/g, 0));
40−
41− const updateHdPath = (basePath, account) => {
42− if (basePath !== 'custom') {
43− setHdPath(basePath.replace(/x/g, account));
44− }
45− };
46−
47− const handlePathSelection = basePath => {
48− setBasePath(basePath);
49− updateHdPath(basePath, account);
50− };
51−
52− const handleAccountSelection = account => {
53− setAccount(account);
54− updateHdPath(basePath, account);
55− };
56−
57− const pollDevice = async () => {
44+const ACCOUNT_OPTIONS = times(20, i => ({
45+ text: `Account #${i + 1}`,
46+ value: i,
47+}));
48+
49+export default function Ledger({ className }) {
50+ useLoginView(WALLET_TYPES.LEDGER);
51+
52+ const { setWallet, setWalletHdPath } = useWallet();
53+
54+ // derivation path input
55+ const [derivationPathInput, { data: basePathPattern }] = useSelectInput({
56+ name: 'derivationpath',
57+ label: 'Derivation Path',
58+ placeholder: 'Choose path pattern...',
59+ options: PATH_OPTIONS,
60+ });
61+
62+ // account input
63+ const [accountInput, { data: accountIndex }] = useSelectInput({
64+ name: 'account',
65+ label: '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),
87+ });
88+
89+ const pollDevice = useCallback(async () => {
5890 const transport = await Transport.create();
5991 const eth = new Eth(transport);
6092 const path = chopHdPrefix(hdPath);
6193
62− eth.getAddress(path, false, true).then(
63− info => {
64− const publicKey = Buffer.from(info.publicKey, 'hex');
65− const chainCode = Buffer.from(info.chainCode, 'hex');
66− const pub = secp256k1.publicKeyConvert(publicKey, true);
67− const hd = bip32.fromPublicKey(pub, chainCode);
68− setWallet(Maybe.Just(hd));
69− setWalletHdPath(addHdPrefix(hdPath));
70− },
71− _ => {
72− setWallet(Maybe.Nothing());
73− }
74− );
75− };
76−
77− const basePathTitle = pathOptions.find(o => o.value === basePath).title;
78−
79− const accountOptions = times(20, i => ({
80− title: `Account #${i + 1}`,
81− value: i,
82− }));
83− const accountTitle = accountOptions.find(o => o.value === account).title;
84−
85− const basePathSelection = (
86− <InnerLabelDropdown
87− className="mt-8"
88− options={pathOptions}
89− handleUpdate={handlePathSelection}
90− title="Derivation path"
91− currentSelectionTitle={basePathTitle}
92− fullWidth={true}
93− />
94− );
94+ try {
95+ const info = await eth.getAddress(path, false, true);
96+ const publicKey = Buffer.from(info.publicKey, 'hex');
97+ const chainCode = Buffer.from(info.chainCode, 'hex');
98+ const pub = secp256k1.publicKeyConvert(publicKey, true);
99+ const hd = bip32.fromPublicKey(pub, chainCode);
100+ setWallet(Just(hd));
101+ setWalletHdPath(addHdPrefix(hdPath));
102+ } catch (error) {
103+ console.error(error);
104+ setWallet(Nothing());
105+ }
106+ }, [hdPath, setWallet, setWalletHdPath]);
107+
108+ // when the base path pattern or the account index changes
109+ // update the hd path in our input
110+ useEffect(() => {
111+ if (useCustomPath) {
112+ // updated by useForm
113+ } else {
114+ setHdPath(basePathPattern.replace(/x/g, accountIndex));
115+ }
116+ }, [useCustomPath, setHdPath, basePathPattern, accountIndex]);
95117
96− const truePathSelection =
97− basePath === 'custom' ? (
98− <Input
99− className="mt3"
100− name="hdPath"
101− label="HD path"
102− autocomplete="off"
103− initialValue={addHdPrefix(hdPath)}
104− onValue={setHdPath}
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+ const full = useBreakpoints([true, true, false]);
119+ const half = useBreakpoints([false, false, true]);
120+ const isHTTPS = document.location.protocol === 'https:';
118121
119122 // when not on https, tell user how to get there
120− const body =
121− document.location.protocol !== 'https:' ? (
122− <>
123− <H2>Running on HTTP?</H2>
124−
125− <P>
126− To authenticate and sign transactions with a Ledger, Bridge must be
127− serving over HTTPS on localhost. You can do this via the following:
128− </P>
129−
130− <ol className="measure-md">
131− <li className="mt-4">
132− Install
133− <a
134− target="_blank"
135− href="https://github.com/FiloSottile/mkcert"
136− rel="noopener noreferrer">
137− mkcert
138− </a>
139− </li>
140− <li className="mt-4">
141− Install a local certificate authority via{' '}
142− <code>mkcert -install</code>
143− </li>
144− <li className="mt-4">
145− In your <code>bridge</code> directory, generate a certificate valid
146− for localhost via <code>mkcert localhost</code>. This will produce
147− two files: <code>localhost.pem</code>, the local certificate, and
148− <code>localhost-key.pem</code> , its corresponding private key.
149− </li>
150− <li className="mt-4">
151− Run <code>python bridge-https.py</code>
152− </li>
153− </ol>
154− </>
155− ) : (
156− <>
157− <P>
158− Connect and authenticate to your Ledger, and then open the "Ethereum"
159− application. If you're running on older firmware, make sure the
160− "browser support" option is turned on. To sign transactions, you'll
161− also need to enable the "contract data" option.
162− </P>
163−
164− <P>
165− If you'd like to use a custom derivation path, you may enter it below.
166− </P>
167−
168− {basePathSelection}
169− {truePathSelection}
170−
171− <ForwardButton className={'mt3'} onClick={pollDevice}>
172− {'Authenticate'}
173− </ForwardButton>
174−
175− <ForwardButton
176− className={'mt3'}
177− disabled={Maybe.Nothing.hasInstance(wallet)}
178− onClick={loginCompleted}>
179− {'Continue'}
180− </ForwardButton>
181− </>
182− );
123+ const renderHTTP = () => (
124+ <>
125+ <Grid.Item full as={H5}>
126+ Running on HTTP?
127+ </Grid.Item>
128+
129+ <Grid.Item full as={P}>
130+ To authenticate and sign transactions with a Ledger, Bridge must be
131+ serving over HTTPS on localhost. You can do this via the following:
132+ </Grid.Item>
133+
134+ <Grid.Item full as="ol">
135+ <li>
136+ Install{' '}
137+ <a
138+ target="_blank"
139+ href="https://github.com/FiloSottile/mkcert"
140+ rel="noopener noreferrer">
141+ mkcert
142+ </a>
143+ </li>
144+ <li className="mt3">
145+ Install a local certificate authority via <code>mkcert -install</code>
146+ </li>
147+ <li className="mt3">
148+ In your <code>bridge</code> directory, generate a certificate valid
149+ for localhost via <code>mkcert localhost</code>. This will produce two
150+ files: <code>localhost.pem</code>, the local certificate, and
151+ <code>localhost-key.pem</code> , its corresponding private key.
152+ </li>
153+ <li className="mt3">
154+ Run <code>python bridge-https.py</code>
155+ </li>
156+ <li className="mt3">Return to this page.</li>
157+ </Grid.Item>
158+ </>
159+ );
160+
161+ const renderHTTPS = () => (
162+ <>
163+ <Grid.Item full as={H5}>
164+ Authenticate With Your Ledger
165+ </Grid.Item>
166+ <Grid.Item full as={Text} className="f6 mb3">
167+ Connect and authenticate to your Ledger, and then open the "Ethereum"
168+ application. If you're running on older firmware, make sure the "browser
169+ support" option is turned on. To sign transactions, you'll also need to
170+ enable the "contract data" option.
171+ </Grid.Item>
172+
173+ {useCustomPath && <Grid.Item full as={Input} {...hdPathInput} />}
174+
175+ {!useCustomPath && (
176+ <>
177+ <Grid.Item
178+ full={full}
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"
201+ onClick={pollDevice}>
202+ Authenticate
203+ </Grid.Item>
204+ </>
205+ );
183206
184207 return (
185− <View>
186− <H1>Authenticate With Your Ledger</H1>
187− {body}
188− </View>
208+ <Grid className={className}>{isHTTPS ? renderHTTPS() : renderHTTP()}</Grid>
189209 );
190210}
src/views/Login/Mnemonic.js+43−52
@@ -1,92 +1,83 @@
1−import React, { useCallback, useEffect } from 'react';
1+import React, { useEffect } from 'react';
22import { 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';
46
5−import View from 'components/View';
67import {
78 usePassphraseInput,
89 useMnemonicInput,
910 useHdPathInput,
10−} from 'components/Inputs';
11−import { ForwardButton } from 'components/Buttons';
12−
13−import { useWallet } from 'store/wallet';
14−
11+ useCheckboxInput,
12+} from 'lib/useInputs';
1513import { walletFromMnemonic, WALLET_TYPES } from 'lib/wallet';
16−import useWalletType from 'lib/useWalletType';
17−import useResetPointCursor from 'lib/useResetPointCursor';
14+import useLoginView from 'lib/useLoginView';
1815
19−export default function Mnemonic({ advanced, loginCompleted }) {
20− useResetPointCursor();
21− useWalletType(WALLET_TYPES.MNEMONIC);
16+export default function Mnemonic({ className }) {
17+ useLoginView(WALLET_TYPES.MNEMONIC);
2218
2319 const {
24− wallet,
2520 setWallet,
26− authMnemonic,
2721 setAuthMnemonic,
2822 walletHdPath,
2923 setWalletHdPath,
3024 } = useWallet();
3125
32− const mnemonic = authMnemonic.getOrElse('');
33− // TODO: move this into transformers?
34− // transform the result of the mnemonic to Maybe<string>
35− const _setAuthMnemonic = useCallback(
36− mnemonic => setAuthMnemonic(mnemonic === '' ? Nothing() : Just(mnemonic)),
37− [setAuthMnemonic]
38− );
26+ const [advancedInput, { data: useAdvanced }] = useCheckboxInput({
27+ name: 'advanced',
28+ label: 'Passphrase & HD Path',
29+ initialValue: false,
30+ });
3931
40− const mnemonicInput = useMnemonicInput({
32+ const [mnemonicInput, { pass, data: mnemonic }] = useMnemonicInput({
4133 name: 'mnemonic',
4234 label: 'BIP39 Mnemonic',
43− initialValue: mnemonic,
44− onValue: _setAuthMnemonic,
4535 autoFocus: true,
4636 });
4737
48− const passphraseInput = usePassphraseInput({
38+ const [passphraseInput, { data: passphrase }] = usePassphraseInput({
4939 name: 'passphrase',
50− label: '(Optional) Wallet Passphrase',
40+ label: 'Wallet Passphrase',
5141 });
52− const passphrase = passphraseInput.data;
5342
54− const hdPathInput = useHdPathInput({
43+ const [hdPathInput, { data: hdPath }] = useHdPathInput({
5544 name: 'hdpath',
5645 label: 'HD Path',
5746 initialValue: walletHdPath,
58− onValue: setWalletHdPath,
5947 });
6048
61− // when the properties change, re-derive wallet
49+ // when the properties change, re-derive wallet and set global state
6250 useEffect(() => {
63− let mounted = true;
64− (async () => {
65− const wallet = walletFromMnemonic(mnemonic, walletHdPath, passphrase);
66− mounted && setWallet(wallet);
67− })();
68− return () => (mounted = false);
69− }, [mnemonic, passphrase, walletHdPath, setWallet]);
51+ if (pass) {
52+ setWalletHdPath(hdPath);
53+ setAuthMnemonic(Just(mnemonic));
54+ setWallet(walletFromMnemonic(mnemonic, hdPath, passphrase));
55+ } else {
56+ setAuthMnemonic(Nothing());
57+ setWallet(Nothing());
58+ }
59+ }, [
60+ pass,
61+ mnemonic,
62+ passphrase,
63+ hdPath,
64+ setWallet,
65+ setAuthMnemonic,
66+ setWalletHdPath,
67+ ]);
7068
7169 return (
72− <View>
73− <Input {...mnemonicInput} />
70+ <Grid className={className}>
71+ <Grid.Item full as={Input} {...mnemonicInput} />
7472
75− {!advanced ? null : (
73+ {useAdvanced && (
7674 <>
77− <Input {...passphraseInput} />
78−
79− <Input {...hdPathInput} />
75+ <Grid.Item full as={Input} {...passphraseInput} />
76+ <Grid.Item full as={Input} {...hdPathInput} />
8077 </>
8178 )}
8279
83− <ForwardButton
84− className="mt3"
85− disabled={Nothing.hasInstance(wallet)}
86− onClick={loginCompleted}
87− solid>
88− Continue
89− </ForwardButton>
90− </View>
80+ <Grid.Item as={CheckboxInput} {...advancedInput} full />
81+ </Grid>
9182 );
9283}
src/views/Login/PrivateKey.js+17−28
@@ -1,49 +1,38 @@
1−import { Just, Nothing } from 'folktale/maybe';
21import React, { useEffect } from 'react';
3−import { Input } from 'indigo-react';
4−
5−import View from 'components/View';
6−import { usePassphraseInput } from 'components/Inputs';
7−import { ForwardButton } from 'components/Buttons';
2+import { Just, Nothing } from 'folktale/maybe';
3+import { Grid, Input } from 'indigo-react';
84
95import { useWallet } from 'store/wallet';
106
11−import { EthereumWallet, WALLET_TYPES } from 'lib/wallet';
12−import useWalletType from 'lib/useWalletType';
13−import useResetPointCursor from 'lib/useResetPointCursor';
7+import { useHexInput } from 'lib/useInputs';
8+import { EthereumWallet, WALLET_TYPES, stripHexPrefix } from 'lib/wallet';
9+import useLoginView from 'lib/useLoginView';
10+
11+export default function PrivateKey({ className }) {
12+ useLoginView(WALLET_TYPES.PRIVATE_KEY);
1413
15−export default function PrivateKey({ loginCompleted }) {
16− useResetPointCursor();
17− useWalletType(WALLET_TYPES.PRIVATE_KEY);
18− const { wallet, setWallet } = useWallet();
14+ const { setWallet } = useWallet();
1915
20− const privateKeyInput = usePassphraseInput({
16+ const [privateKeyInput, { pass, data: privateKey }] = useHexInput({
17+ length: 64,
2118 name: 'privateKey',
2219 label: 'Private key',
2320 autoFocus: true,
2421 });
25− const privateKey = privateKeyInput.data;
2622
2723 useEffect(() => {
28− if (/^[0-9A-Fa-f]{64}$/g.test(privateKey) === true) {
29− const sec = Buffer.from(privateKey, 'hex');
24+ if (pass) {
25+ const sec = Buffer.from(stripHexPrefix(privateKey), 'hex');
3026 const newWallet = new EthereumWallet(sec);
3127 setWallet(Just(newWallet));
3228 } else {
3329 setWallet(Nothing());
3430 }
35− }, [privateKey, setWallet]);
31+ }, [pass, privateKey, setWallet]);
3632
3733 return (
38− <View>
39− Please enter your raw Ethereum private key here.
40− <Input {...privateKeyInput} />
41− <ForwardButton
42− className="mt3"
43− disabled={Nothing.hasInstance(wallet)}
44− onClick={loginCompleted}>
45− Continue
46− </ForwardButton>
47− </View>
34+ <Grid className={className}>
35+ <Grid.Item full as={Input} {...privateKeyInput} />
36+ </Grid>
4837 );
4938}
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';
22import React, { useCallback, useState, useEffect } from 'react';
33import * as azimuth from 'azimuth-js';
44import * as ob from 'urbit-ob';
5−import { Input, AccessoryIcon } from 'indigo-react';
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';
5+import * as kg from 'urbit-key-generation/dist/index';
6+import { Input, Grid, CheckboxInput } from 'indigo-react';
147
158import { useNetwork } from 'store/network';
169import { useWallet } from 'store/wallet';
1710import { usePointCursor } from 'store/pointCursor';
1811
12+import {
13+ usePointInput,
14+ useTicketInput,
15+ usePassphraseInput,
16+ useCheckboxInput,
17+} from 'lib/useInputs';
1918import * as need from 'lib/need';
2019import { WALLET_TYPES, urbitWalletFromTicket } from 'lib/wallet';
21−import useWalletType from 'lib/useWalletType';
22−import useResetPointCursor from 'lib/useResetPointCursor';
23−
24−//TODO should be part of InputWithStatus component
25−const INPUT_STATUS = {
26− SPIN: Symbol('SPIN'),
27− GOOD: Symbol('GOOD'),
28− FAIL: Symbol('FAIL'),
29−};
30−
31−export default function Ticket({ advanced, loginCompleted }) {
32− useResetPointCursor();
33− useWalletType(WALLET_TYPES.TICKET);
34− // globals
20+import useImpliedPoint from 'lib/useImpliedPoint';
21+import useLoginView from 'lib/useLoginView';
22+
23+export default function Ticket({ className }) {
24+ useLoginView(WALLET_TYPES.TICKET);
25+
3526 const { contracts } = useNetwork();
36− const { wallet, setUrbitWallet } = useWallet();
27+ const { setUrbitWallet } = useWallet();
3728 const { setPointCursor } = usePointCursor();
29+ const impliedPoint = useImpliedPoint();
3830
39− // inputs
40− const pointInput = usePointInput({
31+ // point
32+ const [pointInput, { data: pointName }] = usePointInput({
4133 name: 'point',
42− //TODO deduce point name from URL if we can, prefill input if we found it
34+ initialValue: impliedPoint || '',
4335 autoFocus: true,
4436 });
45− const pointName = pointInput.data;
4637
47− const ticketInput = useTicketInput({
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);
53+ const [ticketInput, { data: ticket, pass: validTicket }] = useTicketInput({
4854 name: 'ticket',
4955 label: 'Master Ticket',
56+ error,
57+ deriving,
5058 });
51− const ticket = ticketInput.data;
5259
53− const passphraseInput = usePassphraseInput({
54− name: 'passphrase',
55− label: '(Optional) Wallet Passphrase',
60+ // shards
61+ const [shardsInput, { data: isUsingShards }] = useCheckboxInput({
62+ name: 'shards',
63+ label: 'Shards',
64+ initialValue: false,
5665 });
57− const passphrase = passphraseInput.data;
58−
59− // display state
60− const [ticketStatus, setTicketStatus] = useState(Nothing());
61−
62− //TODO maybe want to do this only on-go, because wallet derivation is slow...
63− const verifyTicket = useCallback(
64− async (pointName, ticket, passphrase) => {
65− setUrbitWallet(Nothing());
66− if (!ob.isValidPatq(ticket) || !ob.isValidPatp(pointName)) {
67− setTicketStatus(Nothing());
68− return;
69− }
70− setTicketStatus(Just(INPUT_STATUS.SPIN));
71− const _contracts = need.contracts(contracts);
72− const pointNumber = ob.patp2dec(pointName);
73− const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
74− const isOwner = azimuth.azimuth.isOwner(
66+
67+ const [shard1Input, { data: shard1, pass: shard1Pass }] = useTicketInput({
68+ name: 'shard1',
69+ label: 'Shard 1',
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+
84+ // TODO: maybe want to do this only on-go, because wallet derivation is slow...
85+ const deriveWalletFromTicket = useCallback(async () => {
86+ // clear states
87+ setError();
88+ setDeriving(true);
89+ setUrbitWallet(Nothing());
90+
91+ if (!ob.isValidPatq(ticket) || !ob.isValidPatp(pointName)) {
92+ setDeriving(false);
93+ return;
94+ }
95+
96+ const _contracts = need.contracts(contracts);
97+ const pointNumber = ob.patp2dec(pointName);
98+ const urbitWallet = await urbitWalletFromTicket(
99+ ticket,
100+ pointName,
101+ passphrase
102+ );
103+ const [isOwner, isTransferProxy] = await Promise.all([
104+ azimuth.azimuth.isOwner(
75105 _contracts,
76106 pointNumber,
77− uhdw.ownership.keys.address
78− );
79− const isTransferProxy = azimuth.azimuth.isTransferProxy(
107+ urbitWallet.ownership.keys.address
108+ ),
109+ azimuth.azimuth.isTransferProxy(
80110 _contracts,
81111 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.'
83120 );
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);
84144 setUrbitWallet(Just(uhdw));
85− const newStatus =
86− (await isOwner) || (await isTransferProxy)
87− ? INPUT_STATUS.GOOD
88− : INPUT_STATUS.FAIL;
89− setTicketStatus(Just(newStatus));
90− },
91− [contracts, setUrbitWallet, setTicketStatus]
92− );
145+ } catch (_) {
146+ // do nothing
147+ }
148+ }, [passphrase, pointName, setUrbitWallet, shard1, shard2, shard3]);
93149
150+ // derive wallet on change
94151 useEffect(() => {
95− verifyTicket(pointName, ticket, passphrase);
96− }, [verifyTicket, pointName, ticket, passphrase]);
97−
98− const canContinue = () => {
99− // this is our only requirement, since we still want people with
100− // non-standard wallet setups to be able to log in
101− return Just.hasInstance(wallet);
102− };
103−
104− const doContinue = () => {
105− setPointCursor(Just(ob.patp2dec(pointName)));
106− loginCompleted();
107− };
108−
109− //TODO integrate into TicketInput?
110− const displayTicketStatus = ticketStatus.matchWith({
111− Nothing: () => <span />,
112− Just: status => {
113− switch (status.value) {
114− case INPUT_STATUS.SPIN:
115− return <AccessoryIcon.Pending />;
116− case INPUT_STATUS.GOOD:
117− return <AccessoryIcon.Success />;
118− case INPUT_STATUS.FAIL:
119− return <AccessoryIcon.Failure />;
120− default:
121− return null;
122− }
123− },
124− });
152+ if (isUsingShards && shardsReady) {
153+ deriveWalletFromShards();
154+ } else if (validTicket) {
155+ deriveWalletFromTicket();
156+ }
157+ }, [
158+ isUsingShards,
159+ validTicket,
160+ shardsReady,
161+ deriveWalletFromShards,
162+ deriveWalletFromTicket,
163+ ]);
125164
126165 return (
127− <View>
128− <Input {...pointInput} />
129− <Input {...ticketInput} accessory={displayTicketStatus} />
130− {advanced && <Input {...passphraseInput} />}
131−
132− <ForwardButton
133− className={'mt3'}
134− disabled={!canContinue()}
135− onClick={doContinue}>
136− Continue
137− </ForwardButton>
138− </View>
166+ <Grid className={className}>
167+ <Grid.Item full as={Input} {...pointInput} />
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 && (
178+ <Grid.Item full as={Input} {...passphraseInput} />
179+ )}
180+
181+ <Grid.Item full as={CheckboxInput} {...hasPassphraseInput} />
182+ <Grid.Item full as={CheckboxInput} {...shardsInput} />
183+ </Grid>
139184 );
140185}
src/views/Login/Trezor.js+96−83
@@ -1,112 +1,125 @@
1+import React, { useEffect } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
13import * as bip32 from 'bip32';
2−import React, { useState } from 'react';
3−import Maybe from 'folktale/maybe';
44import { times } from 'lodash';
55import TrezorConnect from 'trezor-connect';
66import * as secp256k1 from 'secp256k1';
7−import { H1, P, Input } from 'indigo-react';
7+import {
8+ Text,
9+ Input,
10+ Grid,
11+ H5,
12+ CheckboxInput,
13+ SelectInput,
14+} from 'indigo-react';
815
9−import View from 'components/View';
10−import { ForwardButton } from 'components/Buttons';
11−import { InnerLabelDropdown } from 'components/old/Base';
16+import { useWallet } from 'store/wallet';
1217
1318import { TREZOR_PATH } from 'lib/trezor';
14−import useWalletType from 'lib/useWalletType';
1519import { WALLET_TYPES } from 'lib/wallet';
16−import useResetPointCursor from 'lib/useResetPointCursor';
17−
18−import { useWallet } from 'store/wallet';
19−
20−const accountOptions = [
21− { title: 'Custom path', value: 'custom' },
22− ...times(20, i => ({ title: `Account #${i + 1}`, value: i })),
23−];
20+import useLoginView from 'lib/useLoginView';
21+import {
22+ useHdPathInput,
23+ useCheckboxInput,
24+ useSelectInput,
25+} from 'lib/useInputs';
2426
25−export default function Trezor({ loginCompleted }) {
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);
27+import { ForwardButton } from 'components/Buttons';
3328
34− const handleAccountSelection = account => {
35− setAccount(account);
36− if (account !== 'custom') {
37− setHdPath(TREZOR_PATH.replace(/x/g, account));
38− }
39− };
29+const ACCOUNT_OPTIONS = times(20, i => ({
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+
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',
64+ initialValue: TREZOR_PATH.replace(/x/g, 0),
65+ });
4066
4167 const pollDevice = async () => {
4268 TrezorConnect.manifest({
4369 email: 'bridge-trezor@urbit.org',
4470 appUrl: 'https://github.com/urbit/bridge',
4571 });
46− TrezorConnect.getPublicKey({ path: hdPath }).then(info => {
47− if (info.success === true) {
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− }
72+
73+ const info = await TrezorConnect.getPublicKey({
74+ path: hdPath,
5875 });
59− };
6076
61− let accountTitle = accountOptions.find(o => o.value === account).title;
62−
63− const accountSelection = (
64− <InnerLabelDropdown
65− className="mt-8"
66− prop-size="md"
67− prop-format="innerLabel"
68− options={accountOptions}
69− handleUpdate={handleAccountSelection}
70− title="Account"
71− currentSelectionTitle={accountTitle}
72− fullWidth={true}
73− />
74− );
77+ if (info.success === true) {
78+ const payload = info.payload;
79+ const publicKey = Buffer.from(payload.publicKey, 'hex');
80+ const chainCode = Buffer.from(payload.chainCode, 'hex');
81+ const pub = secp256k1.publicKeyConvert(publicKey, true);
82+ const hd = bip32.fromPublicKey(pub, chainCode);
83+ setWallet(Just(hd));
84+ setWalletHdPath(hdPath);
85+ } else {
86+ setWallet(Nothing());
87+ }
88+ };
7589
76− const pathSelection =
77− account !== 'custom' ? null : (
78− <Input
79− className="mt3"
80− name="hdPath"
81− label="HD path"
82− autocomplete="off"
83− initialValue={hdPath}
84− onValue={setHdPath}
85− />
86− );
90+ useEffect(() => {
91+ if (useCustomPath) {
92+ // updated by useForm
93+ } else {
94+ setHdPath(TREZOR_PATH.replace(/x/g, accountIndex));
95+ }
96+ }, [useCustomPath, setHdPath, accountIndex]);
8797
8898 return (
89− <View>
90− <H1>Authenticate With Your Trezor</H1>
99+ <Grid className={className}>
100+ <Grid.Item full as={H5}>
101+ Authenticate With Your Trezor
102+ </Grid.Item>
91103
92− <P>
104+ <Grid.Item full as={Text} className="f6 mb3">
93105 Connect and authenticate to your Trezor. If you'd like to use a custom
94106 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} />}
96112
97− {accountSelection}
98− {pathSelection}
113+ <Grid.Item full as={CheckboxInput} className="mv3" {...customPathInput} />
99114
100− <ForwardButton className="mt3" onClick={pollDevice}>
115+ <Grid.Item
116+ full
117+ as={ForwardButton}
118+ solid
119+ className="mt2"
120+ onClick={pollDevice}>
101121 Authenticate
102− </ForwardButton>
103−
104− <ForwardButton
105− className="mt3"
106− disabled={Maybe.Nothing.hasInstance(wallet)}
107− onClick={loginCompleted}>
108− Continue
109− </ForwardButton>
110− </View>
122+ </Grid.Item>
123+ </Grid>
111124 );
112125}
src/views/Point.js+3−3
@@ -1,5 +1,5 @@
11import React, { useCallback } from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Just } from 'folktale/maybe';
33import * as need from 'lib/need';
44import { Grid } from 'indigo-react';
55
@@ -48,7 +48,7 @@ export default function Point() {
4848 const { availableInvites } = useInvites(point);
4949 const availableInvitesText = matchBlinky(availableInvites);
5050
51− const showActions = Maybe.Just.hasInstance(wallet);
51+ const showActions = Just.hasInstance(wallet);
5252
5353 const goAdmin = useCallback(() => history.push(ROUTE_NAMES.ADMIN), [history]);
5454
@@ -61,7 +61,7 @@ export default function Point() {
6161
6262 return (
6363 <View>
64− <Passport point={Maybe.Just(point)} />
64+ <Passport point={Just(point)} />
6565 <Grid className="pt2">
6666 <Grid.Item
6767 full
src/views/Points.js+2−2
@@ -1,5 +1,5 @@
11import React from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Nothing } from 'folktale/maybe';
33import { H1, H2, P, Grid } from 'indigo-react';
44
55import { ROUTE_NAMES } from 'lib/routeNames';
@@ -59,7 +59,7 @@ export default function Points() {
5959 ...spawning,
6060 ]);
6161
62− const loading = Maybe.Nothing.hasInstance(ownedPoints);
62+ const loading = Nothing.hasInstance(ownedPoints);
6363
6464 const outgoing = ownedPoints.filter(point =>
6565 getDetails(point).matchWith({
src/views/Reticket/Verify.js+5−6
@@ -1,13 +1,13 @@
11import React, { useCallback, useMemo } from 'react';
22import { Input } from 'indigo-react';
33
4−import View from 'components/View';
5−import { useTicketInput } from 'components/Inputs';
6−import { ForwardButton } from 'components/Buttons';
7−
4+import { useTicketInput } from 'lib/useInputs';
85import { useLocalRouter } from 'lib/LocalRouter';
96import { validateExactly } from 'lib/validators';
107
8+import View from 'components/View';
9+import { ForwardButton } from 'components/Buttons';
10+
1111const STUB_VERIFY_TICKET = process.env.NODE_ENV === 'development';
1212
1313//TODO deduplicate with PassportVerify
@@ -19,14 +19,13 @@ export default function Verify({ newWallet }) {
1919 () => [validateExactly(ticket, 'Does not match expected master ticket.')],
2020 [ticket]
2121 );
22− const ticketInput = useTicketInput({
22+ const [ticketInput, { pass }] = useTicketInput({
2323 name: 'ticket',
2424 label: 'New master ticket',
2525 initialValue: STUB_VERIFY_TICKET ? ticket : undefined,
2626 autoFocus: true,
2727 validators,
2828 });
29− const { pass } = ticketInput;
3029
3130 const next = useCallback(() => push(names.RETICKET), [push, names]);
3231
src/views/SetKeys.js+3−3
@@ -1,5 +1,5 @@
11import React from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Just, Nothing } from 'folktale/maybe';
33import * as azimuth from 'azimuth-js';
44import * as ob from 'urbit-ob';
55import * as need from '../lib/need';
@@ -136,10 +136,10 @@ class SetKeys extends React.Component {
136136 state.discontinuity
137137 );
138138
139− return Maybe.Just(txn);
139+ return Just(txn);
140140 }
141141
142− return Maybe.Nothing();
142+ return Nothing();
143143 }
144144
145145 render() {
src/views/SetProxy.js+5−5
@@ -1,5 +1,5 @@
1−import Maybe from 'folktale/maybe';
21import React from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
33import * as need from '../lib/need';
44import * as azimuth from 'azimuth-js';
55import {
@@ -66,18 +66,18 @@ class _SetProxy extends React.Component {
6666 const txArgs = [validContracts, validPoint, state.proxyAddress];
6767
6868 if (props.proxyType === PROXY_TYPE.TRANSFER_PROXY) {
69− return Maybe.Just(azimuth.ecliptic.setTransferProxy(...txArgs));
69+ return Just(azimuth.ecliptic.setTransferProxy(...txArgs));
7070 }
7171
7272 if (props.proxyType === PROXY_TYPE.SPAWN_PROXY) {
73− return Maybe.Just(azimuth.ecliptic.setSpawnProxy(...txArgs));
73+ return Just(azimuth.ecliptic.setSpawnProxy(...txArgs));
7474 }
7575
7676 if (props.proxyType === PROXY_TYPE.MANAGEMENT_PROXY) {
77− return Maybe.Just(azimuth.ecliptic.setManagementProxy(...txArgs));
77+ return Just(azimuth.ecliptic.setManagementProxy(...txArgs));
7878 }
7979
80− return Maybe.Nothing();
80+ return Nothing();
8181 }
8282
8383 render() {
src/views/Transfer.js+2−2
@@ -1,5 +1,5 @@
1−import Maybe from 'folktale/maybe';
21import React from 'react';
2+import { Just } from 'folktale/maybe';
33import * as azimuth from 'azimuth-js';
44import * as ob from 'urbit-ob';
55import * as need from '../lib/need';
@@ -55,7 +55,7 @@ class Transfer extends React.Component {
5555 state.receivingAddress
5656 );
5757
58− return Maybe.Just(txn);
58+ return Just(txn);
5959 }
6060
6161 render() {
src/views/ViewPoint.js+4−5
@@ -1,5 +1,5 @@
11import React, { useCallback } from 'react';
2−import Maybe from 'folktale/maybe';
2+import { Just } from 'folktale/maybe';
33import { Input } from 'indigo-react';
44
55import { H1, P } from '../components/old/Base';
@@ -9,23 +9,22 @@ import { usePointCursor } from 'store/pointCursor';
99
1010import { ROUTE_NAMES } from 'lib/routeNames';
1111import patp2dec from 'lib/patp2dec';
12+import { usePointInput } from 'lib/useInputs';
1213
1314import View from 'components/View';
1415import { ForwardButton } from 'components/Buttons';
15−import { usePointInput } from 'components/Inputs';
1616
1717export default function ViewPoint() {
1818 const history = useHistory();
1919 const { setPointCursor } = usePointCursor();
20− const pointInput = usePointInput({
20+ const [pointInput, { data: pointName, pass }] = usePointInput({
2121 name: 'point',
2222 autoFocus: true,
2323 });
24− const { data: pointName, pass } = pointInput;
2524
2625 const disabled = !pass;
2726 const goForward = useCallback(() => {
28− setPointCursor(Maybe.Just(patp2dec(pointName)));
27+ setPointCursor(Just(patp2dec(pointName)));
2928 history.popAndPush(ROUTE_NAMES.POINT);
3029 }, [setPointCursor, history, pointName]);
3130