on master help sign in
checkpoint: using why-did-you-render
commite273c1c31932bf25992d1966d576b04ed9b7f84c authorMatt Condon (shrugs) <mattgcondon@gmail.com> dateTue 30 Jul 2019, 15:13 (7 years ago) parent87a9d76
11 files changed+319−137
package-lock.json+42 package.json+3 src/Bridge.js+1−5 src/form/Autosaver.js+20 src/form/BridgeForm.js+20 src/form/FormError.js+9 src/form/Inputs.js+56 src/form/SubmitButton.js+35 src/indigo-react/components/Input.js+37−35 src/views/Activate.js+7−1 src/views/Activate/ActivateCode.js+89−96
package-lock.json+42
@@ -6918,6 +6918,19 @@
69186918 }
69196919 }
69206920 },
6921+ "final-form": {
6922+ "version": "4.18.2",
6923+ "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.18.2.tgz",
6924+ "integrity": "sha512-VQx/5x9M4CiC8fG678Dm1IS3mXvBl7ZNIUx5tUZCk00lFImJzQix4KO0+eGtl49sha2bYOxuYn8jRJiq6sazXA==",
6925+ "requires": {
6926+ "@babel/runtime": "^7.3.1"
6927+ }
6928+ },
6929+ "final-form-set-field-data": {
6930+ "version": "1.0.2",
6931+ "resolved": "https://registry.npmjs.org/final-form-set-field-data/-/final-form-set-field-data-1.0.2.tgz",
6932+ "integrity": "sha512-gAnENimyQ5GW3OEGca5pbwm4lYshW2orzfBlPUYqzcm7ZxkQrVO8FqCAgEcCM+Rq9U1OU0q+D+UkqETvvDY6jw=="
6933+ },
69216934 "finalhandler": {
69226935 "version": "1.1.1",
69236936 "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
@@ -14511,6 +14524,25 @@
1451114524 "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-5.1.6.tgz",
1451214525 "integrity": "sha512-X1Y+0jR47ImDVr54Ab6V9eGk0Hnu7fVWGeHQSOXHf/C2pF9c6uy3gef8QUeuUiWlNb0i08InPSE5a/KJzNzw1Q=="
1451314526 },
14527+ "react-final-form": {
14528+ "version": "6.3.0",
14529+ "resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.3.0.tgz",
14530+ "integrity": "sha512-jijhXR1fFGUBQwNOSqF4MK8XJO7Ynl1p8vcFsnQS0INSkGI52+4IagjUgtHj3w8EviIHPFK/Eflji6FELUl07w==",
14531+ "requires": {
14532+ "@babel/runtime": "^7.4.5",
14533+ "ts-essentials": "^2.0.8"
14534+ },
14535+ "dependencies": {
14536+ "@babel/runtime": {
14537+ "version": "7.5.5",
14538+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.5.tgz",
14539+ "integrity": "sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==",
14540+ "requires": {
14541+ "regenerator-runtime": "^0.13.2"
14542+ }
14543+ }
14544+ }
14545+ },
1451414546 "react-hot-loader": {
1451514547 "version": "4.12.0",
1451614548 "resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.12.0.tgz",
@@ -14721,6 +14753,11 @@
1472114753 "regenerate": "^1.4.0"
1472214754 }
1472314755 },
14756+ "regenerator-runtime": {
14757+ "version": "0.13.3",
14758+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
14759+ "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw=="
14760+ },
1472414761 "regenerator-transform": {
1472514762 "version": "0.14.0",
1472614763 "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.0.tgz",
@@ -17199,6 +17236,11 @@
1719917236 "solc": "0.4.24"
1720017237 }
1720117238 },
17239+ "ts-essentials": {
17240+ "version": "2.0.12",
17241+ "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-2.0.12.tgz",
17242+ "integrity": "sha512-3IVX4nI6B5cc31/GFFE+i8ey/N2eA0CZDbo6n0yrz0zDX8ZJ8djmU1p+XRz7G3is0F3bB3pu2pAroFdAWQKU3w=="
17243+ },
1720217244 "ts-pnp": {
1720317245 "version": "1.1.2",
1720417246 "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.1.2.tgz",
package.json+3
@@ -28,6 +28,8 @@
2828 "ethereum-blockies-base64": "^1.0.2",
2929 "ethereumjs-tx": "^1.3.7",
3030 "file-saver": "^2.0.0",
31+ "final-form": "^4.18.2",
32+ "final-form-set-field-data": "^1.0.2",
3133 "folktale": "^2.3.1",
3234 "jszip": "^3.1.5",
3335 "keccak": "^1.4.0",
@@ -38,6 +40,7 @@
3840 "react": "^16.8.6",
3941 "react-app-rewire-hot-loader": "^2.0.1",
4042 "react-dom": "^16.8.6",
43+ "react-final-form": "^6.3.0",
4144 "react-hot-loader": "^4.12.0",
4245 "react-scripts": "3.0.1",
4346 "react-teleporter": "^1.1.0",
src/Bridge.js+1−5
@@ -41,11 +41,7 @@ function useInitialRoutes() {
4141 const hasImpliedTicket = !!useImpliedTicket();
4242
4343 if (IS_STUBBED) {
44− return [
45− { key: ROUTE_NAMES.LOGIN },
46− { key: ROUTE_NAMES.POINTS },
47− { key: ROUTE_NAMES.POINT },
48− ];
44+ return [{ key: ROUTE_NAMES.ACTIVATE }];
4945 }
5046
5147 return hasImpliedTicket
src/form/Autosaver.jsnew file+20
@@ -0,0 +1,20 @@
1+import { useEffect } from 'react';
2+import { useFormState } from 'react-final-form';
3+
4+export default function Autosaver({ onValues }) {
5+ const { valid, validating, values } = useFormState({
6+ subscription: {
7+ valid: true,
8+ validating: true,
9+ values: true,
10+ },
11+ });
12+
13+ useEffect(() => {
14+ if (valid && !validating) {
15+ onValues && onValues(values);
16+ }
17+ }, [onValues, valid, validating, values]);
18+
19+ return null;
20+}
src/form/BridgeForm.jsnew file+20
@@ -0,0 +1,20 @@
1+import React from 'react';
2+import { Form } from 'react-final-form';
3+import setFieldData from 'final-form-set-field-data';
4+
5+import FormError from './FormError';
6+import Autosaver from './Autosaver';
7+
8+export default function BridgeForm({ children, onValues, ...rest }) {
9+ return (
10+ <Form mutators={{ setFieldData }} {...rest}>
11+ {formProps => (
12+ <>
13+ {children(formProps)}
14+ {onValues && <Autosaver onValues={onValues} />}
15+ <FormError />
16+ </>
17+ )}
18+ </Form>
19+ );
20+}
src/form/FormError.jsnew file+9
@@ -0,0 +1,9 @@
1+import React from 'react';
2+import { useFormState } from 'react-final-form';
3+import { ErrorText } from 'indigo-react';
4+
5+export default function FormError() {
6+ const { submitError } = useFormState({ subscription: { submitError: true } });
7+
8+ return submitError ? <ErrorText>{submitError}</ErrorText> : null;
9+}
src/form/Inputs.jsnew file+56
@@ -0,0 +1,56 @@
1+import React, { useMemo } from 'react';
2+import { Input, AccessoryIcon } from 'indigo-react';
3+import { useField } from 'react-final-form';
4+import {
5+ validateNotEmpty,
6+ validateTicket,
7+ kDefaultValidator,
8+} from 'lib/validators';
9+import { compose } from 'lib/lib';
10+
11+const PLACEHOLDER_TICKET = '~sampel-ticlyt-migfun-falmel';
12+
13+const buildValidator = (
14+ validators = [],
15+ fn = x => undefined
16+) => async value => {
17+ console.log('validating', value);
18+ return (
19+ compose(
20+ ...validators,
21+ kDefaultValidator
22+ )(value).error || (await fn(value))
23+ );
24+};
25+
26+const kTicketValidators = [validateTicket, validateNotEmpty];
27+export function TicketInput({ name, validators = [], config = {}, ...rest }) {
28+ const { valid, error, validating } = useField(name, {
29+ subscription: { error: true, validating: true },
30+ });
31+
32+ const validate = useMemo(
33+ () =>
34+ buildValidator([...validators, ...kTicketValidators], config.validate),
35+ [config.validate, validators]
36+ );
37+
38+ return (
39+ <Input
40+ name={name}
41+ placeholder={PLACEHOLDER_TICKET}
42+ accessory={
43+ error ? (
44+ <AccessoryIcon.Failure />
45+ ) : validating ? (
46+ <AccessoryIcon.Pending />
47+ ) : valid ? (
48+ <AccessoryIcon.Success />
49+ ) : null
50+ }
51+ config={{ validate }}
52+ mono
53+ {...rest}
54+ />
55+ );
56+}
src/form/SubmitButton.jsnew file+35
@@ -0,0 +1,35 @@
1+import React from 'react';
2+import cn from 'classnames';
3+
4+import { ForwardButton } from 'components/Buttons';
5+
6+import { blinkIf } from 'components/Blinky';
7+import { useFormState } from 'react-final-form';
8+
9+export default function SubmitButton({
10+ as: As = ForwardButton,
11+ className,
12+ children,
13+ handleSubmit,
14+ ...rest
15+}) {
16+ const { valid, validating, submitting } = useFormState({
17+ subscription: {
18+ valid: true,
19+ validating: true,
20+ submitting: true,
21+ },
22+ });
23+
24+ return (
25+ <As
26+ className={cn('mt4', className)}
27+ disabled={!valid || validating || submitting}
28+ accessory={blinkIf(validating || submitting)}
29+ onClick={handleSubmit}
30+ solid
31+ {...rest}>
32+ {children}
33+ </As>
34+ );
35+}
src/indigo-react/components/Input.js+37−35
@@ -3,50 +3,53 @@ import cn from 'classnames';
33
44import Flex from './Flex';
55import { ErrorText } from './Typography';
6+import { useField } from 'react-final-form';
67
7−export default React.memo(function Input({
8+export default function Input({
89 // visuals
910 type,
1011 name,
1112 label,
1213 className,
1314 accessory,
15+ disabled = false,
1416 mono = false,
1517
1618 // callbacks
1719 onEnter,
1820
19− // state from hook
20− focused,
21− pass,
22− syncPass,
23− visiblyPassed,
24− error,
25− hintError,
26− data,
27− bind,
28− autoFocus,
29− disabled,
30− touched,
31−
32− // ignored
33− initialValue,
34− validators,
35− transformers,
21+ // state
22+ config,
3623
3724 // extra
38− textarea = false,
3925 ...rest
4026}) {
41− // NB(shrugs): we disable exhaustive deps because we don't want the callbacks
42− // (whose identity might change between renders) to trigger a re-notify of
43− // state that we already know
44− // this also happens to prevent render loops when rendering a map of inputs
27+ const {
28+ input,
29+ meta: {
30+ active,
31+ data,
32+ dirty,
33+ error,
34+ modified,
35+ pristine,
36+ submitError,
37+ submitFailed,
38+ submitSucceeded,
39+ submitting,
40+ touched,
41+ valid,
42+ validating,
43+ visited,
44+ },
45+ } = useField(name, config);
46+
47+ disabled = disabled || submitting;
4548
4649 // notify parent of enter keypress iff not disabled and passing
4750 const onKeyPress = useCallback(
48− e => !disabled && pass && e.key === 'Enter' && onEnter && onEnter(),
49− [disabled, pass] // eslint-disable-line react-hooks/exhaustive-deps
51+ e => !disabled && valid && e.key === 'Enter' && onEnter && onEnter(),
52+ [disabled, valid] // eslint-disable-line react-hooks/exhaustive-deps
5053 );
5154
5255 return (
@@ -83,21 +86,20 @@ export default React.memo(function Input({
8386 'bg-gray1': disabled,
8487 },
8588 {
86− gray4: !focused && !touched,
87− black: focused || touched,
89+ gray4: !active && !touched,
90+ black: active || touched,
8891 },
8992 {
90− 'b-green3': visiblyPassed,
91− 'b-black': focused && !visiblyPassed,
92− 'b-yellow3': !focused && hintError,
93− 'b-gray2': !focused && !hintError && !visiblyPassed,
93+ 'b-green3': valid,
94+ 'b-black': active && !valid,
95+ 'b-yellow3': !active && touched && error,
96+ 'b-gray2': !active && !error && !valid,
9497 }
95− // TODO: inputClassName ?
9698 )}
9799 id={name}
98100 name={name}
99101 onKeyPress={onKeyPress}
100− {...bind}
102+ {...input}
101103 flex
102104 />
103105 {accessory && (
@@ -114,11 +116,11 @@ export default React.memo(function Input({
114116 </div>
115117 )}
116118 </Flex.Item>
117− {error && (
119+ {touched && error && (
118120 <Flex.Item as={ErrorText} className="mv1">
119121 {error}
120122 </Flex.Item>
121123 )}
122124 </Flex>
123125 );
124−});
126+}
src/views/Activate.js+7−1
@@ -1,11 +1,12 @@
11import React from 'react';
22
33import useRouter from 'lib/useRouter';
4+import { LocalRouterProvider } from 'lib/LocalRouter';
5+import { useSyncKnownPoints } from 'lib/useSyncPoints';
46
57import { ActivateFlowProvider } from './Activate/ActivateFlow';
68import useActivateFlowState from './Activate/useActivateFlowState';
79import ActivateCode from './Activate/ActivateCode';
8−import { LocalRouterProvider } from 'lib/LocalRouter';
910import ActivatePassport from './Activate/ActivatePassport';
1011import Disclaimer from './Disclaimer';
1112
@@ -29,6 +30,11 @@ export default function Activate() {
2930 initialRoutes: [{ key: NAMES.CODE }],
3031 });
3132
33+ // when we know the derived point, ensure we have the data to display it
34+ useSyncKnownPoints(
35+ [state.derivedPoint.getOrElse(null)].filter(p => p !== null)
36+ );
37+
3238 return (
3339 <LocalRouterProvider value={router}>
3440 <ActivateFlowProvider value={state}>
src/views/Activate/ActivateCode.js+89−96
@@ -1,7 +1,7 @@
1−import React, { useCallback, useEffect, useState } from 'react';
1+import React, { useCallback, useRef } from 'react';
22import { Just, Nothing } from 'folktale/maybe';
33import * as azimuth from 'azimuth-js';
4−import { Grid, Input, H4, ErrorText } from 'indigo-react';
4+import { Grid, H4 } from 'indigo-react';
55
66import View from 'components/View';
77import { ForwardButton } from 'components/Buttons';
@@ -9,12 +9,9 @@ import Passport from 'components/Passport';
99
1010import { useHistory } from 'store/history';
1111
12−import { useTicketInput } from 'lib/useInputs';
1312import * as need from 'lib/need';
1413import { ROUTE_NAMES } from 'lib/routeNames';
15−import { useSyncKnownPoints } from 'lib/useSyncPoints';
1614import FooterButton from 'components/FooterButton';
17−import { blinkIf } from 'components/Blinky';
1815import { DEFAULT_HD_PATH, walletFromMnemonic } from 'lib/wallet';
1916import { useNetwork } from 'store/network';
2017import { generateWallet } from 'lib/invite';
@@ -22,111 +19,96 @@ import { generateTemporaryOwnershipWallet } from 'lib/walletgen';
2219import { useActivateFlow } from './ActivateFlow';
2320import { useLocalRouter } from 'lib/LocalRouter';
2421import useImpliedTicket from 'lib/useImpliedTicket';
22+import timeout from 'lib/timeout';
2523import useHasDisclaimed from 'lib/useHasDisclaimed';
2624
25+import BridgeForm from 'form/BridgeForm';
26+import SubmitButton from 'form/SubmitButton';
27+import { TicketInput } from 'form/Inputs';
28+
2729export default function ActivateCode() {
2830 const history = useHistory();
2931 const { names, push } = useLocalRouter();
3032 const { contracts } = useNetwork();
3133 const impliedTicket = useImpliedTicket();
3234 const [hasDisclaimed] = useHasDisclaimed();
33− const [generalError, setGeneralError] = useState();
34− const [deriving, setDeriving] = useState(false);
35+
36+ const cachedInviteWallet = useRef(Nothing());
37+ const cachedPoint = useRef();
38+
3539 const {
36− derivedWallet,
3740 setDerivedWallet,
3841 setInviteWallet,
3942 derivedPoint,
4043 setDerivedPoint,
4144 } = useActivateFlow();
4245
43− const [ticketInput, { pass: validTicket, data: ticket }] = useTicketInput({
44− name: 'ticket',
45− label: 'Activation Code',
46− initialValue: impliedTicket || '',
47− autoFocus: true,
48− });
49−
5046 const goToLogin = useCallback(() => history.popAndPush(ROUTE_NAMES.LOGIN), [
5147 history,
5248 ]);
5349 const goToPassport = useCallback(() => {
5450 push(names.PASSPORT);
51+
5552 if (!hasDisclaimed) {
5653 push(names.DISCLAIMER);
5754 }
5855 }, [names, push, hasDisclaimed]);
5956
60− const pass = derivedWallet.matchWith({
61− Nothing: () => false,
62− Just: () => true,
63− });
57+ const validate = useCallback(
58+ async ticket => {
59+ await timeout(100); // allow the ui changes to flush before we lag it out
6460
65− useEffect(() => {
66− if (validTicket) {
6761 const _contracts = need.contracts(contracts);
62+ const { seed } = await generateTemporaryOwnershipWallet(ticket);
63+
64+ // TODO(fang): isn't all this accessible in the ownership object?
65+ const inviteWallet = walletFromMnemonic(seed, DEFAULT_HD_PATH);
66+ cachedInviteWallet.current = inviteWallet;
67+
68+ const _inviteWallet = need.wallet(inviteWallet);
69+
70+ const owned = await azimuth.azimuth.getOwnedPoints(
71+ _contracts,
72+ _inviteWallet.address
73+ );
74+ const transferring = await azimuth.azimuth.getTransferringFor(
75+ _contracts,
76+ _inviteWallet.address
77+ );
78+ const incoming = [...owned, ...transferring];
79+
80+ if (incoming.length > 0) {
81+ if (incoming.length > 1) {
82+ // TODO: warnings
83+ // setGeneralError(
84+ // 'This invite code has multiple points available.\n' +
85+ // "Once you've activated this point, activate the next with the same process."
86+ // );
87+ }
6888
69− setDeriving(true);
70− // when the ticket becomes valid, derive the point
71− (async () => {
72− const { seed } = await generateTemporaryOwnershipWallet(ticket);
73−
74− //TODO isn't all this accessible in the ownership object?
75− const inviteWallet = walletFromMnemonic(seed, DEFAULT_HD_PATH);
76− const _inviteWallet = need.wallet(inviteWallet);
77−
78− const owned = await azimuth.azimuth.getOwnedPoints(
79− _contracts,
80− _inviteWallet.address
81− );
82− const transferring = await azimuth.azimuth.getTransferringFor(
83− _contracts,
84− _inviteWallet.address
89+ const point = parseInt(incoming[0], 10);
90+ // setDerivedPoint(Just(point));
91+ cachedPoint.current = point;
92+ } else {
93+ return (
94+ 'Invite code has no claimable point.\n' +
95+ 'Check your invite code and try again?'
8596 );
86− const incoming = [...owned, ...transferring];
87−
88− let realPoint = Nothing();
89− let wallet = Nothing();
90−
91− if (incoming.length > 0) {
92− const pointNum = parseInt(incoming[0], 10);
93− realPoint = Just(pointNum);
94− wallet = Just(await generateWallet(pointNum));
95−
96− if (incoming.length > 1) {
97− setGeneralError(
98− 'This invite code has multiple points available.\n' +
99− "Once you've activated this point, activate the next with the same process."
100− );
101− } else {
102− setGeneralError(false);
103− }
104− } else {
105− setGeneralError(
106− 'Invite code has no claimable point.\n' +
107− 'Check your invite code and try again?'
108− );
109− }
97+ }
98+ },
99+ [contracts]
100+ );
110101
111− setDerivedPoint(realPoint);
112− setDerivedWallet(wallet);
113− setInviteWallet(inviteWallet);
114− setDeriving(false);
115− })();
116− } else {
117− setGeneralError(false);
118− }
119− }, [
120− validTicket,
121− contracts,
122− ticket,
123− setDerivedPoint,
124− setDerivedWallet,
125− setInviteWallet,
126− ]);
102+ const onSubmit = useCallback(
103+ async values => {
104+ setInviteWallet(cachedInviteWallet.current);
105+ setDerivedPoint(Just(cachedPoint.current));
106+ setDerivedWallet(Just(await generateWallet(cachedPoint)));
127107
128− // when we know the derived point, ensure we have the data to display it
129− useSyncKnownPoints([derivedPoint.getOrElse(null)].filter(p => p !== null));
108+ goToPassport();
109+ },
110+ [goToPassport, setDerivedPoint, setDerivedWallet, setInviteWallet]
111+ );
130112
131113 return (
132114 <View inset>
@@ -135,23 +117,34 @@ export default function ActivateCode() {
135117 <Grid.Item as={H4} className="mt3 mb2" full>
136118 Activate
137119 </Grid.Item>
138− <Grid.Item as={Input} {...ticketInput} full />
139− {generalError && (
140− <Grid.Item full>
141− <ErrorText>{generalError}</ErrorText>{' '}
142− </Grid.Item>
143− )}
144− <Grid.Item
145− as={ForwardButton}
146− className="mt4"
147− disabled={!pass || deriving}
148− accessory={blinkIf(deriving)}
149− onClick={goToPassport}
150− solid
151− full>
152− {deriving && 'Deriving...'}
153− {!deriving && 'Go'}
154− </Grid.Item>
120+ <BridgeForm
121+ onSubmit={onSubmit}
122+ initialValues={{ ticket: impliedTicket || '' }}>
123+ {({ validating, submitting, handleSubmit }) => (
124+ <>
125+ <Grid.Item
126+ full
127+ as={TicketInput}
128+ name="ticket"
129+ label="Activation Code"
130+ config={{ validate }}
131+ autoFocus
132+ />
133+
134+ <Grid.Item
135+ full
136+ as={SubmitButton}
137+ className="mt4"
138+ handleSubmit={handleSubmit}>
139+ {validating
140+ ? 'Deriving...'
141+ : submitting
142+ ? 'Generating...'
143+ : 'Go'}
144+ </Grid.Item>
145+ </>
146+ )}
147+ </BridgeForm>
155148 </Grid>
156149 <FooterButton as={ForwardButton} onClick={goToLogin}>
157150 Login