on master help sign in
checkpoint: test final-form on mnemonic page
commitf231080d837f07268e95caadc0b98016c190b142 authorMatt Condon (shrugs) <mattgcondon@gmail.com> dateTue 30 Jul 2019, 16:21 (7 years ago) parente273c1c
14 files changed+251−120
package-lock.json+8 package.json+1 src/Bridge.js+6−2 src/form/Autosaver.js+2−2 src/form/Condition.js+11 src/form/Inputs.js+94−7 src/form/SubmitButton.js+3−2 src/index.js+4 src/indigo-react/components/CheckboxInput.js+16−28 src/lib/transformers.js+2−1 src/store/wallet.js+6−1 src/views/Activate/ActivateCode.js+4−5 src/views/Activate/PassportVerify.js+32−20 src/views/Login/Mnemonic.js+62−52
package-lock.json+8
@@ -2143,6 +2143,14 @@
21432143 "@xtuc/long": "4.2.2"
21442144 }
21452145 },
2146+ "@welldone-software/why-did-you-render": {
2147+ "version": "3.2.3",
2148+ "resolved": "https://registry.npmjs.org/@welldone-software/why-did-you-render/-/why-did-you-render-3.2.3.tgz",
2149+ "integrity": "sha512-dUMkjsVsCfIo+IEmiMyb/FGKVbXrzbGqLvDTCn0ad5drNmD+BGoU/7Z+Nc7ckkV0F1G4vzs1XQrAcIxyPa8Ssw==",
2150+ "requires": {
2151+ "lodash": "^4"
2152+ }
2153+ },
21462154 "@xtuc/ieee754": {
21472155 "version": "1.2.0",
21482156 "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
package.json+1
@@ -17,6 +17,7 @@
1717 "@hot-loader/react-dom": "^16.8.6",
1818 "@ledgerhq/hw-app-eth": "^4.61.0",
1919 "@ledgerhq/hw-transport-u2f": "^4.61.0",
20+ "@welldone-software/why-did-you-render": "^3.2.3",
2021 "PaperCollateralRenderer": "github:urbit/PaperCollateralRenderer#rc-immediate",
2122 "async-retry": "^1.2.3",
2223 "azimuth-js": "^0.15.1",
src/Bridge.js+6−2
@@ -10,7 +10,7 @@ import Provider from 'store/Provider';
1010import { ROUTE_NAMES } from 'lib/routeNames';
1111import { ROUTES } from 'lib/router';
1212import { NETWORK_TYPES } from 'lib/network';
13−import { walletFromMnemonic } from 'lib/wallet';
13+import { walletFromMnemonic, WALLET_TYPES } from 'lib/wallet';
1414import { isDevelopment } from 'lib/flags';
1515import useImpliedTicket from 'lib/useImpliedTicket';
1616import useHasDisclaimed from 'lib/useHasDisclaimed';
@@ -41,7 +41,11 @@ function useInitialRoutes() {
4141 const hasImpliedTicket = !!useImpliedTicket();
4242
4343 if (IS_STUBBED) {
44− return [{ key: ROUTE_NAMES.ACTIVATE }];
44+ return [
45+ { key: ROUTE_NAMES.LOGIN },
46+ // { key: ROUTE_NAMES.POINTS },
47+ // { key: ROUTE_NAMES.POINT },
48+ ];
4549 }
4650
4751 return hasImpliedTicket
src/form/Autosaver.js+2−2
@@ -11,8 +11,8 @@ export default function Autosaver({ onValues }) {
1111 });
1212
1313 useEffect(() => {
14− if (valid && !validating) {
15− onValues && onValues(values);
14+ if (!validating) {
15+ onValues && onValues({ valid: valid && !validating, values });
1616 }
1717 }, [onValues, valid, validating, values]);
1818
src/form/Condition.jsnew file+11
@@ -0,0 +1,11 @@
1+import { useField } from 'react-final-form';
2+
3+// inspired by: https://codesandbox.io/s/lm4p3m92q
4+// renders childen if the relevant field's value is the test value
5+export default function Condition({ when, is, children }) {
6+ const {
7+ input: { value },
8+ } = useField(when, { subscription: { value: true } });
9+
10+ return value === is ? children : null;
11+}
src/form/Inputs.js+94−7
@@ -1,20 +1,37 @@
11import React, { useMemo } from 'react';
22import { Input, AccessoryIcon } from 'indigo-react';
33import { useField } from 'react-final-form';
4+
45import {
56 validateNotEmpty,
67 validateTicket,
78 kDefaultValidator,
9+ validateMnemonic,
810} from 'lib/validators';
911import { compose } from 'lib/lib';
12+import { prependSig } from 'lib/transformers';
13+import { DEFAULT_HD_PATH } from 'lib/wallet';
14+
15+// TODO: why is `validate` called when routing away? new props?
16+// active not true: https://github.com/final-form/react-final-form/issues/558
1017
18+const kEmptyValidators = []; // necessary for referential stability
19+const kEmptyTransformers = []; // also for referential stability
20+
21+const PLACEHOLDER_POINT = '~sampel-ponnym';
22+const PLACEHOLDER_HD_PATH = DEFAULT_HD_PATH;
23+const PLACEHOLDER_MNEMONIC =
24+ 'example crew supreme gesture quantum web media hazard theory mercy wing kitten';
1125const PLACEHOLDER_TICKET = '~sampel-ticlyt-migfun-falmel';
26+const PLACEHOLDER_ADDRESS = '0x12345abcdeDB11D175F123F6891AA64F01c24F7d';
27+const PLACEHOLDER_PRIVATE_KEY =
28+ '0x12345abcdee6beb2f323fab48b432925c9785808d33a6ca6d7ba00b45e9370c3';
1229
1330const buildValidator = (
1431 validators = [],
1532 fn = x => undefined
1633) => async value => {
17− console.log('validating', value);
34+ // console.log('validating:', value);
1835 return (
1936 compose(
2037 ...validators,
@@ -24,19 +41,27 @@ const buildValidator = (
2441};
2542
2643const kTicketValidators = [validateTicket, validateNotEmpty];
27−export function TicketInput({ name, validators = [], config = {}, ...rest }) {
44+export function TicketInput({
45+ name,
46+ validators = kEmptyValidators,
47+ transformers = kEmptyTransformers,
48+ validate,
49+ ...rest
50+}) {
2851 const { valid, error, validating } = useField(name, {
2952 subscription: { error: true, validating: true },
3053 });
3154
32− const validate = useMemo(
33− () =>
34− buildValidator([...validators, ...kTicketValidators], config.validate),
35− [config.validate, validators]
55+ const _validate = useMemo(
56+ () => buildValidator([...validators, ...kTicketValidators], validate),
57+ [validate, validators]
3658 );
3759
60+ const _format = (value, name) => prependSig(value);
61+
3862 return (
3963 <Input
64+ type="text"
4065 name={name}
4166 placeholder={PLACEHOLDER_TICKET}
4267 accessory={
@@ -48,9 +73,71 @@ export function TicketInput({ name, validators = [], config = {}, ...rest }) {
4873 <AccessoryIcon.Success />
4974 ) : null
5075 }
51− config={{ validate }}
76+ config={{ validate: _validate, format: _format }}
77+ mono
78+ {...rest}
79+ />
80+ );
81+}
82+
83+const kMnemonicValidators = [validateMnemonic, validateNotEmpty];
84+export function MnemonicInput({
85+ name,
86+ validators = kEmptyValidators,
87+ transformers = kEmptyTransformers,
88+ validate,
89+ ...rest
90+}) {
91+ const _validate = useMemo(
92+ () => buildValidator([...validators, ...kMnemonicValidators], validate),
93+ [validate, validators]
94+ );
95+
96+ return (
97+ <Input
98+ type="textarea"
99+ name={name}
100+ placeholder={PLACEHOLDER_MNEMONIC}
101+ config={{ validate: _validate }}
102+ autoComplete="off"
52103 mono
53104 {...rest}
54105 />
55106 );
56107}
108+
109+const kHdPathValidators = [validateNotEmpty];
110+export function HdPathInput({
111+ name,
112+ validators = kEmptyValidators,
113+ transformers = kEmptyTransformers,
114+ validate,
115+ ...rest
116+}) {
117+ const _validate = useMemo(
118+ () => buildValidator([...validators, ...kHdPathValidators], validate),
119+ [validate, validators]
120+ );
121+
122+ return (
123+ <Input
124+ type="text"
125+ name={name}
126+ placeholder={PLACEHOLDER_HD_PATH}
127+ config={{ validate: _validate }}
128+ autoComplete="off"
129+ {...rest}
130+ />
131+ );
132+}
133+
134+export function PassphraseInput({ ...rest }) {
135+ return (
136+ <Input
137+ type="password"
138+ placeholder="Passphrase"
139+ autoComplete="off"
140+ {...rest}
141+ />
142+ );
143+}
src/form/SubmitButton.js+3−2
@@ -13,11 +13,12 @@ export default function SubmitButton({
1313 handleSubmit,
1414 ...rest
1515}) {
16− const { valid, validating, submitting } = useFormState({
16+ const { valid, validating, submitting, submitError } = useFormState({
1717 subscription: {
1818 valid: true,
1919 validating: true,
2020 submitting: true,
21+ submitError: true,
2122 },
2223 });
2324
@@ -29,7 +30,7 @@ export default function SubmitButton({
2930 onClick={handleSubmit}
3031 solid
3132 {...rest}>
32− {children}
33+ {submitError ? 'Error submitting' : children}
3334 </As>
3435 );
3536}
src/index.js+4
@@ -3,4 +3,8 @@ import ReactDOM from 'react-dom';
33
44import Bridge from './Bridge';
55
6+if (process.env.NODE_ENV !== 'production') {
7+ require('@welldone-software/why-did-you-render')(React);
8+}
9+
610ReactDOM.render(<Bridge />, document.getElementById('root'));
src/indigo-react/components/CheckboxInput.js+16−28
@@ -2,6 +2,7 @@ import React from 'react';
22import cn from 'classnames';
33
44import Flex from './Flex';
5+import { useField } from 'react-final-form';
56
67export default function CheckboxInput({
78 // visuals
@@ -9,30 +10,15 @@ export default function CheckboxInput({
910 label,
1011 className,
1112
12− // callbacks
13− onEnter,
14−
15− // state from hook
16− focused,
17− pass,
18− syncPass,
19− visiblyPassed,
20− error,
21− hintError,
22− data,
23− bind,
24− autoFocus,
2513 disabled,
26− touched,
14+}) {
15+ const {
16+ input,
17+ meta: { submitting },
18+ } = useField(name);
2719
28− // ignored
29− initialValue,
30− validators,
31− transformers,
20+ disabled = disabled || submitting;
3221
33− // extra
34− ...rest
35−}) {
3622 return (
3723 <Flex
3824 row
@@ -50,11 +36,13 @@ export default function CheckboxInput({
5036 {/* we totally hide the checkbox itself */}
5137 <Flex.Item
5238 as="input"
53− {...rest}
54− className={cn('super-hidden')}
39+ type="checkbox"
40+ className="super-hidden"
5541 id={name}
56− name={name}
57− {...bind}
42+ {...input}
43+ // TODO: this seems hacky... final-form should have a standard way to do this
44+ checked={input.value}
45+ onClick={e => input.onChange(!input.value)}
5846 />
5947 {/* and then display a prettier one in its stead */}
6048 <Flex.Item
@@ -67,14 +55,14 @@ export default function CheckboxInput({
6755 align="center"
6856 className={cn('b1 p1 mr3', {
6957 'bg-gray1': disabled,
70− 'bg-black white b-black': !disabled && data,
71− 'bg-white black b-black': !disabled && !data,
58+ 'bg-black white b-black': !disabled && input.value,
59+ 'bg-white black b-black': !disabled && !input.value,
7260 })}
7361 style={{
7462 height: '14px',
7563 width: '14px',
7664 }}>
77− {data && '✓'}
65+ {input.value && '✓'}
7866 </Flex>
7967 {label}
8068 </Flex.Item>
src/lib/transformers.js+2−1
@@ -1,6 +1,7 @@
11// import { fill } from './lib'
22
3−export const prependSig = s => (s.charAt(0) !== '~' ? `~${s}` : s);
3+export const prependSig = (s = '') =>
4+ s.length && s.charAt(0) !== '~' ? `~${s}` : s;
45
56export const convertToNumber = s => {
67 try {
src/store/wallet.js+6−1
@@ -33,7 +33,12 @@ const DEFAULT_WALLET_TYPE = WALLET_TYPES.TICKET;
3333// JSON keystore files, and Metamask authentication, it wraps an
3434// 'EthereumWallet'.
3535function _useWallet(initialWallet = Nothing(), initialMnemonic = Nothing()) {
36− const [walletType, _setWalletType] = useState(DEFAULT_WALLET_TYPE);
36+ const [walletType, _setWalletType] = useState(() =>
37+ initialMnemonic.matchWith({
38+ Nothing: () => DEFAULT_WALLET_TYPE,
39+ Just: () => WALLET_TYPES.MNEMONIC,
40+ })
41+ );
3742 const [walletHdPath, setWalletHdPath] = useState(DEFAULT_HD_PATH);
3843 const [wallet, _setWallet] = useState(initialWallet);
3944 const [urbitWallet, _setUrbitWallet] = useState(Nothing());
src/views/Activate/ActivateCode.js+4−5
@@ -87,7 +87,7 @@ export default function ActivateCode() {
8787 }
8888
8989 const point = parseInt(incoming[0], 10);
90− // setDerivedPoint(Just(point));
90+ setDerivedPoint(Just(point));
9191 cachedPoint.current = point;
9292 } else {
9393 return (
@@ -96,18 +96,17 @@ export default function ActivateCode() {
9696 );
9797 }
9898 },
99− [contracts]
99+ [contracts, setDerivedPoint]
100100 );
101101
102102 const onSubmit = useCallback(
103103 async values => {
104104 setInviteWallet(cachedInviteWallet.current);
105− setDerivedPoint(Just(cachedPoint.current));
106105 setDerivedWallet(Just(await generateWallet(cachedPoint)));
107106
108107 goToPassport();
109108 },
110− [goToPassport, setDerivedPoint, setDerivedWallet, setInviteWallet]
109+ [goToPassport, setDerivedWallet, setInviteWallet]
111110 );
112111
113112 return (
@@ -127,7 +126,7 @@ export default function ActivateCode() {
127126 as={TicketInput}
128127 name="ticket"
129128 label="Activation Code"
130− config={{ validate }}
129+ validate={validate}
131130 autoFocus
132131 />
133132
src/views/Activate/PassportVerify.js+32−20
@@ -1,13 +1,14 @@
11import React, { useCallback, useMemo } from 'react';
2−import { Grid, Input, P } from 'indigo-react';
2+import { Grid, P } from 'indigo-react';
33
44import * as need from 'lib/need';
55import { useLocalRouter } from 'lib/LocalRouter';
6−import { useTicketInput } from 'lib/useInputs';
76import { validateExactly } from 'lib/validators';
87import { isDevelopment } from 'lib/flags';
98
10−import { ForwardButton } from 'components/Buttons';
9+import SubmitButton from 'form/SubmitButton';
10+import { TicketInput } from 'form/Inputs';
11+import BridgeForm from 'form/BridgeForm';
1112
1213import { useActivateFlow } from './ActivateFlow';
1314import PassportView from './PassportView';
@@ -24,13 +25,8 @@ export default function PassportVerify({ className }) {
2425 () => [validateExactly(ticket, 'Does not match expected master ticket.')],
2526 [ticket]
2627 );
27− const [ticketInput, { pass }] = useTicketInput({
28− name: 'ticket',
29− label: 'Master Ticket',
30− initialValue: STUB_VERIFY_TICKET ? ticket : undefined,
31− autoFocus: true,
32− validators,
33− });
28+
29+ const onSubmit = useCallback(() => goToTransfer(), [goToTransfer]);
3430
3531 return (
3632 <PassportView header="Verify Passport" step={2} className={className}>
@@ -40,16 +36,32 @@ export default function PassportVerify({ className }) {
4036 should be a folder of image files. One of them is your Master Ticket.
4137 Open it and enter the 4 word phrase below (with hyphens).
4238 </Grid.Item>
43− <Grid.Item full as={Input} {...ticketInput} />
44− <Grid.Item
45− full
46− className="mt3"
47− as={ForwardButton}
48− disabled={!pass}
49− onClick={goToTransfer}
50− solid>
51− Verify
52− </Grid.Item>
39+ <BridgeForm
40+ onSubmit={onSubmit}
41+ initialValues={{
42+ ticket: STUB_VERIFY_TICKET ? ticket : undefined,
43+ }}>
44+ {({ handleSubmit }) => (
45+ <>
46+ <Grid.Item
47+ full
48+ as={TicketInput}
49+ name="ticket"
50+ label="Master Ticket"
51+ validators={validators}
52+ autoFocus
53+ />
54+
55+ <Grid.Item
56+ full
57+ className="mt3"
58+ as={SubmitButton}
59+ handleSubmit={handleSubmit}>
60+ Verify
61+ </Grid.Item>
62+ </>
63+ )}
64+ </BridgeForm>
5365 </Grid>
5466 </PassportView>
5567 );
src/views/Login/Mnemonic.js+62−52
@@ -1,18 +1,15 @@
1−import React, { useEffect } from 'react';
1+import React, { useCallback } from 'react';
22import cn from 'classnames';
33import { Just, Nothing } from 'folktale/maybe';
4−import { Grid, Input, CheckboxInput } from 'indigo-react';
4+import { Grid, CheckboxInput } from 'indigo-react';
55
66import { useWallet } from 'store/wallet';
77
8−import {
9− usePassphraseInput,
10− useMnemonicInput,
11− useHdPathInput,
12− useCheckboxInput,
13−} from 'lib/useInputs';
148import { walletFromMnemonic, WALLET_TYPES } from 'lib/wallet';
159import useLoginView from 'lib/useLoginView';
10+import { MnemonicInput, HdPathInput, PassphraseInput } from 'form/Inputs';
11+import BridgeForm from 'form/BridgeForm';
12+import Condition from 'form/Condition';
1613
1714export default function Mnemonic({ className }) {
1815 useLoginView(WALLET_TYPES.MNEMONIC);
@@ -24,61 +21,74 @@ export default function Mnemonic({ className }) {
2421 setWalletHdPath,
2522 } = useWallet();
2623
27− const [advancedInput, { data: useAdvanced }] = useCheckboxInput({
28− name: 'advanced',
29− label: 'Passphrase & HD Path',
30− initialValue: false,
31− });
32−
33− const [mnemonicInput, { pass, data: mnemonic }] = useMnemonicInput({
34− name: 'mnemonic',
35− label: 'BIP39 Mnemonic',
36− autoFocus: true,
37− });
38−
39− const [passphraseInput, { data: passphrase }] = usePassphraseInput({
40− name: 'passphrase',
41− label: 'Wallet Passphrase',
42− });
43−
44− const [hdPathInput, { data: hdPath }] = useHdPathInput({
45− name: 'hdpath',
46− label: 'HD Path',
47− initialValue: walletHdPath,
48− });
49−
5024 // when the properties change, re-derive wallet and set global state
51− useEffect(() => {
52− if (pass) {
53− setWalletHdPath(hdPath);
54− setAuthMnemonic(Just(mnemonic));
55− setWallet(walletFromMnemonic(mnemonic, hdPath, passphrase));
56− } else {
57− setAuthMnemonic(Nothing());
58− setWallet(Nothing());
59− }
60− }, [
61− pass,
62− mnemonic,
63− passphrase,
64− hdPath,
65− setWallet,
66− setAuthMnemonic,
67− setWalletHdPath,
68− ]);
25+ const onValues = useCallback(
26+ ({ valid, values }) => {
27+ console.log(valid, values);
28+ if (valid) {
29+ setWalletHdPath(values.hdpath);
30+ setAuthMnemonic(Just(values.mnemonic));
31+ setWallet(
32+ walletFromMnemonic(values.mnemonic, values.hdpath, values.passphrase)
33+ );
34+ } else {
35+ setAuthMnemonic(Nothing());
36+ setWallet(Nothing());
37+ }
38+ },
39+ [setAuthMnemonic, setWallet, setWalletHdPath]
40+ );
6941
7042 return (
7143 <Grid className={cn('mt4', className)}>
72− <Grid.Item full as={Input} {...mnemonicInput} />
44+ <BridgeForm
45+ onValues={onValues}
46+ onSubmit={() => undefined}
47+ initialValues={{ hdpath: walletHdPath, advanced: false }}>
48+ {() => (
49+ <>
50+ <Grid.Item
51+ full
52+ as={MnemonicInput}
53+ name="mnemonic"
54+ label="BIP39 Mnemonic"
55+ />
56+
57+ <Condition when="advanced" is={true}>
58+ <Grid.Item
59+ full
60+ as={PassphraseInput}
61+ name="passphrase"
62+ label="Passphrase"
63+ />
64+
65+ <Grid.Item
66+ full
67+ as={HdPathInput}
68+ name="hdpath"
69+ label="HD Path"
70+ autoFocus
71+ />
72+ </Condition>
73+
74+ <Grid.Item
75+ full
76+ as={CheckboxInput}
77+ name="advanced"
78+ label="Passphrase & HD Path"
79+ />
80+ </>
81+ )}
82+ </BridgeForm>
7383
74− {useAdvanced && (
84+ {/* {useAdvanced && (
7585 <>
7686 <Grid.Item full as={Input} {...passphraseInput} />
7787 <Grid.Item full as={Input} {...hdPathInput} />
7888 </>
7989 )}
8090
81− <Grid.Item as={CheckboxInput} {...advancedInput} full />
91+ <Grid.Item as={CheckboxInput} {...advancedInput} full /> */}
8292 </Grid>
8393 );
8494}