on master help sign in
feat: implement basic Input component with hooks, convert mnemonic view to hooks
f130223Matt Condon (shrugs)
src/components/Inputs.jsnew file+81
@@ -0,0 +1,81 @@
1+import React from 'react';
2+import { Input } from 'indigo-react';
3+import * as bip39 from 'bip39';
4+
5+import { validateNotEmpty, validateMnemonic } from 'lib/validators';
6+import { DEFAULT_HD_PATH } from 'lib/wallet';
7+
8+const kExampleMnemonic = bip39.generateMnemonic();
9+
10+// const RequiredInput = advancedInput({
11+// WrappedComponent: Input,
12+// validators: [validateNotEmpty],
13+// });
14+
15+// const NetworkKeyInput = advancedInput({
16+// WrappedComponent: Input,
17+// validators: [validateNotEmpty, validateNetworkKey],
18+// });
19+
20+// const NetworkSeedInput = advancedInput({
21+// WrappedComponent: Input,
22+// validators: [validateNotEmpty, validateNetworkSeed],
23+// });
24+
25+// const AddressInput = advancedInput({
26+// WrappedComponent: Input,
27+// validators: [validateEthereumAddress, validateNotEmpty],
28+// });
29+
30+export function PassphraseInput(props) {
31+ return (
32+ <Input
33+ type="password"
34+ autoComplete="off"
35+ placeholder="Passphrase"
36+ {...props}
37+ />
38+ );
39+}
40+
41+export function MnemonicInput(props) {
42+ return (
43+ <Input
44+ type="text"
45+ autoComplete="off"
46+ placeholder={`e.g. ${kExampleMnemonic}`}
47+ validators={[validateMnemonic, validateNotEmpty]}
48+ {...props}
49+ />
50+ );
51+}
52+
53+export function HdPathInput(props) {
54+ return (
55+ <Input
56+ type="text"
57+ autoComplete="off"
58+ placeholder={`${DEFAULT_HD_PATH}`}
59+ validators={[validateNotEmpty]}
60+ {...props}
61+ />
62+ );
63+}
64+
65+// const GalaxyInput = advancedInput({
66+// WrappedComponent: Input,
67+// validators: [validateGalaxy, validateNotEmpty],
68+// transformers: [prependSig],
69+// });
70+
71+// const PointInput = advancedInput({
72+// WrappedComponent: Input,
73+// validators: [validatePoint, validateNotEmpty],
74+// transformers: [prependSig],
75+// });
76+
77+// const TicketInput = advancedInput({
78+// WrappedComponent: Input,
79+// validators: [validateTicket, validateNotEmpty],
80+// transformers: [prependSig],
81+// });
src/components/View.js+2−4
@@ -3,13 +3,11 @@ import cn from 'classnames';
3
3
4// View is a top-level component that all Views must render to inherit styling
4// View is a top-level component that all Views must render to inherit styling
5function View({ className, ...rest }) {
5function View({ className, ...rest }) {
6− return <div className={cn('mw1', className)} {...rest} />;
6+ return <div className={cn('ph5 mw1', className)} {...rest} />;
7}
7}
8
8
9View.Full = function FullView({ className, ...rest }) {
9View.Full = function FullView({ className, ...rest }) {
10− return (
11− <div className={cn('mw2 ph1 ph3-ns ph9-md ph10-lg', className)} {...rest} />
10+ return <div className={cn('mw2 ph5 ph9-md ph10-lg', className)} {...rest} />;
12− );
13};
11};
14
12
15export default View;
13export default View;
src/indigo-react/components/Input.jsnew file+176
@@ -0,0 +1,176 @@
1+import React, { useCallback, useEffect, useMemo, useState } from 'react';
2+import cn from 'classnames';
3+
4+import Grid from 'components/Grid';
5+
6+import { compose } from 'lib/lib';
7+import { kDefaultValidator } from 'lib/validators';
8+
9+function useInput({
10+ validators = [],
11+ transformers = [],
12+ initialValue = '',
13+ autofocus = false,
14+}) {
15+ const [value, setValue] = useState(initialValue);
16+ const [focused, setFocused] = useState(autofocus);
17+ const [hasBeenFocused, setHasBeenFocused] = useState(false);
18+
19+ // NB(shrugs)
20+ // TL;DR: This is what we want, the exhaustive deps rule is being too nosy.
21+ //
22+ // Here we memo-construct the `validate` and `transform` functions
23+ // based on the prop arguments. We explicitly disable the exhausive deps rule
24+ // because eslint wants an inline array specifying our dependencies.
25+ //
26+ // If we were to place the array of validators or transformers into
27+ // the dependency array like `[validators]`, we would end up creating a new
28+ // `validate` function every render, since deps are shallow-compared with
29+ // `Object.is`. Because the array of validators are constructed as part of
30+ // a component's render() function like:
31+ //
32+ // export function MnemonicInput(props) {
33+ // return (
34+ // <Input
35+ // type="text"
36+ // autoComplete="off"
37+ // placeholder={`e.g. ${kExampleMnemonic}`}
38+ // validators={[validateMnemonic, validateNotEmpty]}
39+ // {...props}
40+ // />
41+ // );
42+ // }
43+ //
44+ // we create a new array, which shallowly compares as not-equal
45+ // between renders.
46+ //
47+ // So, in this specific case, we disable the rule and pass the
48+ // array of validators (and transformers) themselves as dependencies.
49+ // This means that validators and transformers can change between renders
50+ // and we will re-create the composed `validate` and `transform` functions
51+ // as expected.
52+ //
53+ // For context, an alternative solution I tried was defining the array of
54+ // validators as a constant array outside of the render method, but:
55+ // 1. It wasn't as clean or useful as specifying them inline,
56+ // 2. It would mean that validators could not be updated between renders,
57+ // 3. And the situation we have now feels naturally more 'correct'.
58+ //
59+ /* eslint-disable react-hooks/exhaustive-deps */
60+ const transform = useMemo(() => compose(...transformers), transformers);
61+ const validate = useMemo(
62+ () =>
63+ compose(
64+ ...validators,
65+ kDefaultValidator
66+ ),
67+ validators
68+ );
69+ /* eslint-enable react-hooks/exhaustive-deps */
70+
71+ const onChange = useCallback(
72+ e => setValue(transform(e.target.value)), //
73+ [transform]
74+ );
75+ const onFocus = useCallback(
76+ e => setFocused(true), //
77+ [setFocused]
78+ );
79+ const onBlur = useCallback(
80+ e => {
81+ setFocused(false);
82+ setHasBeenFocused(true);
83+ },
84+ [setFocused, setHasBeenFocused]
85+ );
86+
87+ // memo-compute whether or not the current value is valid
88+ const { pass, error: _error, data } = useMemo(
89+ () => validate(value), //
90+ [validate, value]
91+ );
92+
93+ // only set error if it should be visible
94+ const error = !pass && hasBeenFocused && !focused && _error;
95+ // whether or not the 'pass' effect should be shown
96+ const visiblePass = pass && hasBeenFocused && !focused;
97+
98+ return {
99+ pass,
100+ // TODO: need a better name for `visiblePass`
101+ visiblePass,
102+ error,
103+ data,
104+ focused,
105+ bind: {
106+ value,
107+ onChange,
108+ focused: focused ? 'true' : 'false',
109+ // ^ dom wants string value for focused
110+ onFocus,
111+ onBlur,
112+ },
113+ };
114+}
115+
116+export default function Input({
117+ name,
118+ label,
119+ initialValue = '',
120+ className = '',
121+ validators = [],
122+ onSuccess,
123+ onFailure,
124+ disabled = false,
125+ mono = false,
126+ autofocus = false,
127+ ...rest
128+}) {
129+ const { focused, pass, visiblePass, error, data, bind } = useInput({
130+ validators,
131+ initialValue,
132+ autofocus,
133+ });
134+
135+ useEffect(() => {
136+ onSuccess && onSuccess(data);
137+ onFailure && onFailure(error);
138+ }, [pass, error, data, onSuccess, onFailure]);
139+
140+ return (
141+ <Grid>
142+ <Grid.Item full>
143+ <label className="f6 lh-tall" htmlFor={name}>
144+ {label}
145+ </label>
146+ </Grid.Item>
147+ <Grid.Item full>
148+ <input
149+ {...rest}
150+ className={cn(
151+ 'b b1 p3',
152+ { mono },
153+ {
154+ 'bg-white': !disabled,
155+ 'bg-gray1': disabled,
156+ },
157+ {
158+ gray4: !focused,
159+ black: focused,
160+ },
161+ {
162+ 'b-black': focused,
163+ 'b-gray3': !focused && !error && !visiblePass,
164+ 'b-yellow3': !focused && error && !visiblePass,
165+ 'b-green3': !focused && !error && visiblePass,
166+ },
167+ className
168+ )}
169+ id={name}
170+ name={name}
171+ {...bind}
172+ />
173+ </Grid.Item>
174+ </Grid>
175+ );
176+}
src/indigo-react/index.js+1
@@ -1 +1,2 @@
1export { default as IndigoApp } from './components/IndigoApp';
1export { default as IndigoApp } from './components/IndigoApp';
2+export { default as Input } from './components/Input';
src/indigo-react/indigo.css+33−46
@@ -13,7 +13,7 @@
13 * git+https://github.com/urbit/Indigo.git
13 * git+https://github.com/urbit/Indigo.git
14 * Bugs: https://github.com/urbit/Indigo/issues
14 * Bugs: https://github.com/urbit/Indigo/issues
15 * License: MIT
15 * License: MIT
16− * Timestamp: Tue Jun 18 2019 15:01:04 GMT-0700 (Pacific Daylight Time)
16+ * Timestamp: Tue Jun 18 2019 18:39:16 GMT-0700 (Pacific Daylight Time)
17 *******************************************************************************/
17 *******************************************************************************/
18/**********************
18/**********************
19Color variables
19Color variables
@@ -324,8 +324,8 @@ button {
324 -webkit-font-smoothing: antialiased; }
324 -webkit-font-smoothing: antialiased; }
325
325
326* {
326* {
327− font-family: "Inter";
327+ font-family: "Inter UI", -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji";
328− color: black; }
328+ color: #000000; }
329
329
330h1,
330h1,
331h2,
331h2,
@@ -338,7 +338,7 @@ h6 {
338 padding: 0; }
338 padding: 0; }
339
339
340a {
340a {
341− color: black;
341+ color: #000000;
342 text-decoration: none; }
342 text-decoration: none; }
343
343
344h1,
344h1,
@@ -646,12 +646,8 @@ caption,
646 .measure-wide-lg {
646 .measure-wide-lg {
647 max-width: 34rem; } }
647 max-width: 34rem; } }
648
648
649−.sans,
649+.sans {
650−.inter {
651− font-family: "Inter UI", sans-serif; }
652−
653−.sys {
654− font-family: -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji"; }
650+ font-family: "Inter UI", -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji"; }
655
651
656.mono {
652.mono {
657 font-family: "Source Code Pro", "Roboto mono", "Courier New", monospace; }
653 font-family: "Source Code Pro", "Roboto mono", "Courier New", monospace; }
@@ -1889,6 +1885,15 @@ caption,
1889.half-2 {
1885.half-2 {
1890 grid-area: auto / 7 / auto / 13; }
1886 grid-area: auto / 7 / auto / 13; }
1891
1887
1888+.third-1 {
1889+ grid-area: auto / 1 / auto / 4; }
1890+
1891+.third-2 {
1892+ grid-area: auto / 4 / auto / 8; }
1893+
1894+.third-3 {
1895+ grid-area: auto / 8 / auto / 13; }
1896+
1892.fourth-1 {
1897.fourth-1 {
1893 grid-area: auto / 1 / auto / 4; }
1898 grid-area: auto / 1 / auto / 4; }
1894
1899
@@ -1908,6 +1913,12 @@ caption,
1908 grid-area: auto / 1 / auto / 7; }
1913 grid-area: auto / 1 / auto / 7; }
1909 .half-2-ns {
1914 .half-2-ns {
1910 grid-area: auto / 7 / auto / 13; }
1915 grid-area: auto / 7 / auto / 13; }
1916+ .third-1-ns {
1917+ grid-area: auto / 1 / auto / 4; }
1918+ .third-2-ns {
1919+ grid-area: auto / 4 / auto / 8; }
1920+ .third-3-ns {
1921+ grid-area: auto / 8 / auto / 13; }
1911 .fourth-1-ns {
1922 .fourth-1-ns {
1912 grid-area: auto / 1 / auto / 4; }
1923 grid-area: auto / 1 / auto / 4; }
1913 .fourth-2-ns {
1924 .fourth-2-ns {
@@ -1924,6 +1935,12 @@ caption,
1924 grid-area: auto / 1 / auto / 7; }
1935 grid-area: auto / 1 / auto / 7; }
1925 .half-2-md {
1936 .half-2-md {
1926 grid-area: auto / 7 / auto / 13; }
1937 grid-area: auto / 7 / auto / 13; }
1938+ .third-1-md {
1939+ grid-area: auto / 1 / auto / 4; }
1940+ .third-2-md {
1941+ grid-area: auto / 4 / auto / 8; }
1942+ .third-3-md {
1943+ grid-area: auto / 8 / auto / 13; }
1927 .fourth-1-md {
1944 .fourth-1-md {
1928 grid-area: auto / 1 / auto / 4; }
1945 grid-area: auto / 1 / auto / 4; }
1929 .fourth-2-md {
1946 .fourth-2-md {
@@ -1940,6 +1957,12 @@ caption,
1940 grid-area: auto / 1 / auto / 7; }
1957 grid-area: auto / 1 / auto / 7; }
1941 .half-2-lg {
1958 .half-2-lg {
1942 grid-area: auto / 7 / auto / 13; }
1959 grid-area: auto / 7 / auto / 13; }
1960+ .third-1-lg {
1961+ grid-area: auto / 1 / auto / 4; }
1962+ .third-2-lg {
1963+ grid-area: auto / 4 / auto / 8; }
1964+ .third-3-lg {
1965+ grid-area: auto / 8 / auto / 13; }
1943 .fourth-1-lg {
1966 .fourth-1-lg {
1944 grid-area: auto / 1 / auto / 4; }
1967 grid-area: auto / 1 / auto / 4; }
1945 .fourth-2-lg {
1968 .fourth-2-lg {
@@ -1973,42 +1996,6 @@ caption,
1973.gap32 {
1996.gap32 {
1974 gap: 32px; }
1997 gap: 32px; }
1975
1998
1976−.fr1 {
1977− width: 1fr; }
1978−
1979−.fr2 {
1980− width: 2fr; }
1981−
1982−.fr3 {
1983− width: 3fr; }
1984−
1985−.fr4 {
1986− width: 4fr; }
1987−
1988−.fr5 {
1989− width: 5fr; }
1990−
1991−.fr6 {
1992− width: 6fr; }
1993−
1994−.fr7 {
1995− width: 7fr; }
1996−
1997−.fr8 {
1998− width: 8fr; }
1999−
2000−.fr9 {
2001− width: 9fr; }
2002−
2003−.fr10 {
2004− width: 10fr; }
2005−
2006−.fr11 {
2007− width: 11fr; }
2008−
2009−.fr12 {
2010− width: 12fr; }
2011−
2012.c1-2 {
1999.c1-2 {
2013 grid-column-start: 1;
2000 grid-column-start: 1;
2014 grid-column-end: 2; }
2001 grid-column-end: 2; }
src/lib/validators.js+44−79
@@ -5,32 +5,43 @@ import { isValidAddress } from './wallet';
5
5
6const hexRegExp = /[0-9A-Fa-f]{64}/g;
6const hexRegExp = /[0-9A-Fa-f]{64}/g;
7
7
8−// // Note that this validator also transforms data
8+// Wraps single validation functions in a controlled and predictable way.
9−// const validateBase64 = ({ data, pass, error }) => {
9+export const simpleValidatorWrapper = config => {
10−// if (pass !== true) return { data, pass, error }
10+ // If a previous validation has already failed, skip this validation and
11−//
11+ // return the prev message to the next stage in the validation function chain.
12−// try {
12+ // Failed validations should drop all the way down the chain and drop out of
13−// atob(data);
13+ // the output.
14−// return { data: atob(data), pass: true, error: '' }
14+ if (!config.prevMessage.pass) {
15−// } catch(e) {
15+ return config.prevMessage;
16−// return { data: data, pass: false, error: 'File is not valid base64.' }
16+ }
17−// }
17+
18+ // Run the validator and return the result.
19+ return config.validator(config.prevMessage.data)
20+ ? newMessage(config.prevMessage.data, true, '')
21+ : newMessage(config.prevMessage.data, false, config.errorMessage);
22+};
23+
24+// Validation message
25+// {
26+// data: ...
27+// pass: ...
28+// error: ...
18−// };
29+// }
19−//
30+// Creates a new validation message in a uniform way.
20−// // Note that this validator also transforms data
21−// const validateJSON = ({ data, pass, error }) => {
31+const newMessage = (data, pass, error) => ({
22−// if (pass !== true) return { data, pass, error }
32+ // The input data
23−//
33+ data,
24−// try {
34+ // Has the data passed validation?
25−// JSON.parse(data)
35+ pass,
26−// return { data: JSON.parse(data), pass: true, error: '' }
36+ // If data has failed a validator, the error message goes here.
27−// } catch(e) {
37+ error,
28−// return { data: data, pass: false, error: 'File does not decode into valid JSON.' }
38+});
29−// }
39+
30−// };
40+// A validator that always passes.
41+export const kDefaultValidator = data => newMessage(data, true, '');
31
42
32// Validates a bip39 mnemonic
43// Validates a bip39 mnemonic
33−const validateMnemonic = m =>
44+export const validateMnemonic = m =>
34 simpleValidatorWrapper({
45 simpleValidatorWrapper({
35 prevMessage: m,
46 prevMessage: m,
36 validator: d => bip39.validateMnemonic(d),
47 validator: d => bip39.validateMnemonic(d),
@@ -38,7 +49,7 @@ const validateMnemonic = m =>
38 });
49 });
39
50
40// Checks an empty field
51// Checks an empty field
41−const validateNotEmpty = m =>
52+export const validateNotEmpty = m =>
42 simpleValidatorWrapper({
53 simpleValidatorWrapper({
43 prevMessage: m,
54 prevMessage: m,
44 validator: d => d.length > 1,
55 validator: d => d.length > 1,
@@ -46,7 +57,7 @@ const validateNotEmpty = m =>
46 });
57 });
47
58
48// Checks if a patp is a valid galaxy
59// Checks if a patp is a valid galaxy
49−const validateGalaxy = m =>
60+export const validateGalaxy = m =>
50 simpleValidatorWrapper({
61 simpleValidatorWrapper({
51 prevMessage: m,
62 prevMessage: m,
52 validator: d => {
63 validator: d => {
@@ -61,7 +72,7 @@ const validateGalaxy = m =>
61 errorMessage: 'This is not a valid galaxy',
72 errorMessage: 'This is not a valid galaxy',
62 });
73 });
63
74
64−const validatePoint = m =>
75+export const validatePoint = m =>
65 simpleValidatorWrapper({
76 simpleValidatorWrapper({
66 prevMessage: m,
77 prevMessage: m,
67 validator: d => {
78 validator: d => {
@@ -74,7 +85,7 @@ const validatePoint = m =>
74 errorMessage: 'This is not a valid point',
85 errorMessage: 'This is not a valid point',
75 });
86 });
76
87
77−const validateTicket = m =>
88+export const validateTicket = m =>
78 simpleValidatorWrapper({
89 simpleValidatorWrapper({
79 prevMessage: m,
90 prevMessage: m,
80 validator: d => {
91 validator: d => {
@@ -87,7 +98,7 @@ const validateTicket = m =>
87 errorMessage: 'This is not a valid ticket',
98 errorMessage: 'This is not a valid ticket',
88 });
99 });
89
100
90−const validateShard = m =>
101+export const validateShard = m =>
91 simpleValidatorWrapper({
102 simpleValidatorWrapper({
92 prevMessage: m,
103 prevMessage: m,
93 validator: d => {
104 validator: d => {
@@ -100,21 +111,21 @@ const validateShard = m =>
100 errorMessage: 'This is not a valid shard',
111 errorMessage: 'This is not a valid shard',
101 });
112 });
102
113
103−const validateLength = (m, l) =>
114+export const validateLength = (m, l) =>
104 simpleValidatorWrapper({
115 simpleValidatorWrapper({
105 prevMessage: m,
116 prevMessage: m,
106 validator: d => d.length === l,
117 validator: d => d.length === l,
107 errorMessage: 'This is of an invalid length',
118 errorMessage: 'This is of an invalid length',
108 });
119 });
109
120
110−const validateNetworkKey = m =>
121+export const validateNetworkKey = m =>
111 simpleValidatorWrapper({
122 simpleValidatorWrapper({
112 prevMessage: m,
123 prevMessage: m,
113 validator: d => hexRegExp.test(d),
124 validator: d => hexRegExp.test(d),
114 errorMessage: 'This is not a valid network key',
125 errorMessage: 'This is not a valid network key',
115 });
126 });
116
127
117−const validateNetworkSeed = m =>
128+export const validateNetworkSeed = m =>
118 simpleValidatorWrapper({
129 simpleValidatorWrapper({
119 prevMessage: m,
130 prevMessage: m,
120 validator: d => hexRegExp.test(d),
131 validator: d => hexRegExp.test(d),
@@ -122,55 +133,9 @@ const validateNetworkSeed = m =>
122 });
133 });
123
134
124// Checks if a string is a valid ethereum address
135// Checks if a string is a valid ethereum address
125−const validateEthereumAddress = m =>
136+export const validateEthereumAddress = m =>
126 simpleValidatorWrapper({
137 simpleValidatorWrapper({
127 prevMessage: m,
138 prevMessage: m,
128 validator: d => isValidAddress(d),
139 validator: d => isValidAddress(d),
129 errorMessage: 'This is not a valid Ethereum address',
140 errorMessage: 'This is not a valid Ethereum address',
130 });
141 });
131−
132−// Wraps single validation functions in a controlled and predictable way.
133−const simpleValidatorWrapper = config => {
134− // If a previous validation has already failed, skip this validation and
135− // return the prev message to the next stage in the validation function chain.
136− // Failed validations should drop all the way down the chain and drop out of
137− // the output.
138− if (config.prevMessage.pass !== true) return config.prevMessage;
139−
140− // Run the validator and return the result.
141− return config.validator(config.prevMessage.data)
142− ? newMessage(config.prevMessage.data, true, '')
143− : newMessage(config.prevMessage.data, false, config.errorMessage);
144−};
145−
146−// Validation message
147−// {
148−// data: ...
149−// pass: ...
150−// error: ...
151−// }
152−// Creates a new validation message in a uniform way.
153−const newMessage = (data, pass, error) => ({
154− // The input data
155− data,
156− // Has the data passed validation?
157− pass,
158− // If data has failed a validator, the error message goes here.
159− error,
160−});
161−
162−export {
163− // validateBase64,
164− // validateJSON,
165− simpleValidatorWrapper,
166− validateMnemonic,
167− validateNotEmpty,
168− validateGalaxy,
169− validateEthereumAddress,
170− validatePoint,
171− validateTicket,
172− validateShard,
173− validateLength,
174− validateNetworkKey,
175− validateNetworkSeed,
176−};
src/store/wallet.js+36−27
@@ -1,4 +1,10 @@
1−import React, { createContext, forwardRef, useContext, useState } from 'react';
1+import React, {
2+ createContext,
3+ forwardRef,
4+ useCallback,
5+ useContext,
6+ useState,
7+} from 'react';
2import Maybe from 'folktale/maybe';
8import Maybe from 'folktale/maybe';
3import { includes } from 'lodash';
9import { includes } from 'lodash';
4
10
@@ -36,15 +42,15 @@ function _useWallet(
36 const [networkSeed, setNetworkSeed] = useState(Maybe.Nothing());
42 const [networkSeed, setNetworkSeed] = useState(Maybe.Nothing());
37 const [networkRevision, setNetworkRevision] = useState(Maybe.Nothing());
43 const [networkRevision, setNetworkRevision] = useState(Maybe.Nothing());
38
44
39− const setWalletType = walletType => {
45+ const setWalletType = useCallback(walletType => {
40 if (!includes(WALLET_TYPES, walletType)) {
46 if (!includes(WALLET_TYPES, walletType)) {
41 throw BRIDGE_ERROR.INVALID_WALLET_TYPE;
47 throw BRIDGE_ERROR.INVALID_WALLET_TYPE;
42 }
48 }
43
49
44 _setWalletType(walletType);
50 _setWalletType(walletType);
45− };
51+ }, []);
46
52
47− const setWallet = wallet => {
53+ const setWallet = useCallback(wallet => {
48 // force that public addresses are derived for each wallet
54 // force that public addresses are derived for each wallet
49 wallet.map(wal => {
55 wallet.map(wal => {
50 wal.address = wal.address || addressFromSecp256k1Public(wal.publicKey);
56 wal.address = wal.address || addressFromSecp256k1Public(wal.publicKey);
@@ -52,29 +58,32 @@ function _useWallet(
52 });
58 });
53
59
54 _setWallet(wallet);
60 _setWallet(wallet);
55− };
61+ }, []);
56−
62+
57− const setUrbitWallet = urbitWallet => {
63+ const setUrbitWallet = useCallback(
64+ urbitWallet => {
58− if (Maybe.Just.hasInstance(urbitWallet)) {
65+ if (Maybe.Just.hasInstance(urbitWallet)) {
59− // when an urbit wallet is set, also derive
66+ // when an urbit wallet is set, also derive
60− // a normal bip32 wallet using the ownership address
67+ // a normal bip32 wallet using the ownership address
61− const wallet = walletFromMnemonic(
68+ const wallet = walletFromMnemonic(
62− urbitWallet.value.ownership.seed,
69+ urbitWallet.value.ownership.seed,
63− DEFAULT_HD_PATH,
70+ DEFAULT_HD_PATH,
64− urbitWallet.value.meta.passphrase
71+ urbitWallet.value.meta.passphrase
65− );
72+ );
66−
73+
67− // manually set the public address
74+ // manually set the public address
68− wallet.map(wal => {
75+ wallet.map(wal => {
69− wal.address = urbitWallet.value.ownership.keys.address;
76+ wal.address = urbitWallet.value.ownership.keys.address;
70− return wal;
77+ return wal;
71− });
78+ });
72−
79+
73− setWallet(wallet);
80+ setWallet(wallet);
74− }
81+ }
75−
82+
76− _setUrbitWallet(urbitWallet);
83+ _setUrbitWallet(urbitWallet);
77− };
84+ },
85+ [setWallet]
86+ );
78
87
79 return {
88 return {
80 //
89 //
src/style/indigo-proposals.scss+4
@@ -7,3 +7,7 @@
7 margin: 0 auto;
7 margin: 0 auto;
8 max-width: 64rem;
8 max-width: 64rem;
9}
9}
10+
11+.lh-tall {
12+ line-height: 2;
13+}
src/views/Mnemonic.js+69−160
@@ -1,165 +1,74 @@
1−import * as bip39 from 'bip39';
1+import React, { useCallback, useEffect, useState } from 'react';
2import { Just, Nothing } from 'folktale/maybe';
2import { Just, Nothing } from 'folktale/maybe';
3−import React from 'react';
4−import { Button } from '../components/old/Base';
5−import {
6− Input,
7− MnemonicInput,
8− InnerLabel,
9− InputCaption,
10−} from '../components/old/Base';
11−import { H1 } from '../components/old/Base';
12−
13−import { ROUTE_NAMES } from '../lib/routeNames';
14−import { withHistory } from '../store/history';
15−import { DEFAULT_HD_PATH, walletFromMnemonic } from '../lib/wallet';
16−import { compose } from '../lib/lib';
17−import { withWallet } from '../store/wallet';
18−import View from 'components/View';
19−
20−class Mnemonic extends React.Component {
21− constructor(props) {
22− super(props);
23−
24− this.state = {
25− mnemonic: '',
26− passphrase: '',
27− hdpath: DEFAULT_HD_PATH,
28− };
29−
30− this.handleMnemonicInput = this.handleMnemonicInput.bind(this);
31− this.handlePassphraseInput = this.handlePassphraseInput.bind(this);
32− this.handleHdPathInput = this.handleHdPathInput.bind(this);
33− }
34−
35− componentDidMount() {
36− const { mnemonic, hdpath, passphrase } = this.state;
37− this.attemptWalletDerivation(mnemonic, hdpath, passphrase);
38−
39− this.setState({
40− exampleMnemonic: bip39.generateMnemonic(),
41− });
42− }
43−
44− handlePassphraseInput(passphrase) {
45− this.setState((state, _) => {
46− const mnemonic = state.mnemonic;
47− const hdpath = state.hdpath;
48− this.attemptWalletDerivation(
49− mnemonic,
50− hdpath === '' ? DEFAULT_HD_PATH : hdpath,
51− passphrase
52− );
53− return {
54− passphrase,
55− };
56− });
57− }
58−
59− handleMnemonicInput(mnemonic) {
60− this.setState((state, _) => {
61− const hdpath = state.hdpath;
62− const passphrase = state.passphrase;
63− this.attemptWalletDerivation(
64− mnemonic,
65− hdpath === '' ? DEFAULT_HD_PATH : hdpath,
66− passphrase
67− );
68− return {
69− mnemonic,
70− };
71− });
72− }
73
3
74− handleHdPathInput(hdpath) {
4+import { Button } from '../components/old/Base';
75− this.setState((state, _) => {
76− const mnemonic = state.mnemonic;
77− const passphrase = state.passphrase;
78− this.attemptWalletDerivation(
79− mnemonic,
80− hdpath === '' ? DEFAULT_HD_PATH : hdpath,
81− passphrase
82− );
83− return {
84− hdpath,
85− };
86− });
87− }
88−
89− attemptWalletDerivation(mnemonic, hdpath, passphrase) {
90− const { setWallet, setAuthMnemonic, setWalletHdPath } = this.props;
91− const wallet = walletFromMnemonic(mnemonic, hdpath, passphrase);
92− setWallet(wallet);
93− setAuthMnemonic(Just(mnemonic));
94− setWalletHdPath(hdpath);
95− }
96−
97− render() {
98− const { history, wallet } = this.props;
99− const { mnemonic, hdpath, exampleMnemonic, passphrase } = this.state;
100−
101− return (
102− <View>
103− <H1 className={'mb-4'}>{'Enter Your Mnemonic'}</H1>
104− <InputCaption>{'Please enter your BIP39 mnemonic here.'}</InputCaption>
105−
106− <MnemonicInput
107− className="pt-8"
108− prop-size="md"
109− prop-format="innerLabel"
110− type="text"
111− name="mnemonic"
112− placeholder={`e.g. ${exampleMnemonic}`}
113− onChange={this.handleMnemonicInput}
114− value={mnemonic}
115− autocomplete="off"
116− autoFocus>
117− <InnerLabel>{'Mnemonic'}</InnerLabel>
118− </MnemonicInput>
119−
120− <InputCaption>
121− {`If your wallet requires a passphrase, you may enter it below.`}
122− </InputCaption>
123−
124− <Input
125− className="pt-8"
126− prop-size="md"
127− prop-format="innerLabel"
128− name="passphrase"
129− type="password"
130− value={passphrase}
131− autocomplete="off"
132− onChange={this.handlePassphraseInput}>
133− <InnerLabel>{'Passphrase'}</InnerLabel>
134− </Input>
135−
136− <InputCaption>
137− {`If you'd like to use a custom derivation path, you may enter it below.`}
138− </InputCaption>
139
5
140− <Input
6+import { ROUTE_NAMES } from 'lib/routeNames';
141− className="pt-8 text-mono"
7+import { useHistory } from 'store/history';
142− prop-size="md"
8+import { walletFromMnemonic } from 'lib/wallet';
143− prop-format="innerLabel"
9+import { useWallet } from 'store/wallet';
144− name="hdpath"
145− value={hdpath}
146− autocomplete="off"
147− onChange={this.handleHdPathInput}>
148− <InnerLabel>{'HD Path'}</InnerLabel>
149− </Input>
150
10
151− <Button
11+import View from 'components/View';
152− className={'mt-10'}
12+import { MnemonicInput, PassphraseInput, HdPathInput } from 'components/Inputs';
13+
14+export default function Mnemonic() {
15+ const history = useHistory();
16+ const {
17+ wallet,
18+ setWallet,
19+ authMnemonic,
20+ setAuthMnemonic,
21+ walletHdPath,
22+ setWalletHdPath,
23+ } = useWallet();
24+ const [passphrase, setPassphrase] = useState('');
25+ const mnemonic = authMnemonic.getOrElse('');
26+
27+ // TODO: move this into transformers?
28+ // transform the result of the mnemonic to Maybe<string>
29+ const _setAuthMnemonic = useCallback(
30+ mnemonic => setAuthMnemonic(mnemonic === '' ? Nothing() : Just(mnemonic)),
31+ [setAuthMnemonic]
32+ );
33+
34+ // when the properties change, re-derive wallet
35+ useEffect(() => {
36+ let mounted = true;
37+ (async () => {
38+ const wallet = walletFromMnemonic(mnemonic, walletHdPath, passphrase);
39+ mounted && setWallet(wallet);
40+ })();
41+ return () => (mounted = false);
42+ }, [mnemonic, passphrase, walletHdPath, setWallet]);
43+
44+ return (
45+ <View>
46+ <MnemonicInput
47+ name="mnemonic"
48+ label="BIP39 Mnemonic"
49+ initialValue={mnemonic}
50+ onSuccess={_setAuthMnemonic}
51+ />
52+
53+ <PassphraseInput
54+ name="passphrase"
55+ label="(Optional) Wallet Passphrase"
56+ initialValue={passphrase}
57+ onSuccess={setPassphrase}
58+ />
59+
60+ <HdPathInput
61+ name="hdpath"
62+ label="HD Path"
63+ initialValue={walletHdPath}
64+ onSuccess={setWalletHdPath}
65+ />
66+
67+ <Button
153− disabled={Nothing.hasInstance(wallet)}
68+ disabled={Nothing.hasInstance(wallet)}
154− onClick={() => history.popAndPush(ROUTE_NAMES.SHIPS)}>
69+ onClick={() => history.popAndPush(ROUTE_NAMES.SHIPS)}>
155− {'Continue →'}
70+ {'Continue →'}
156− </Button>
71+ </Button>
157− </View>
72+ </View>
158− );
73+ );
159− }
160}
74}
161−
162−export default compose(
163− withHistory,
164− withWallet
165−)(Mnemonic);