on master help sign in
feat: add error messages to input and include (stubbed) hasReceived for mailer
commita432d0be37f53be175e2922cee6b60e6d1031d14 authorMatt Condon (shrugs) <mattgcondon@gmail.com> dateTue 25 Jun 2019, 19:17 (7 years ago) parentb465a33
5 files changed+77−47
src/indigo-react/components/Input.js+8−2 src/indigo-react/components/Typography.js+1 src/indigo-react/lib/useForm.js+21−19 src/lib/useMailer.js+24−12 src/views/Invite/InviteEmail.js+23−14
src/indigo-react/components/Input.js+8−2
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect } from 'react';
22import cn from 'classnames';
33
44import Flex from './Flex';
5+import { ErrorText } from './Typography';
56
67export default function Input({
78 // visuals
@@ -72,7 +73,7 @@ export default function Input({
7273 <Flex.Item as="label" className="f6 lh-tall" htmlFor={name}>
7374 {label}
7475 </Flex.Item>
75− <Flex row>
76+ <Flex.Item as={Flex} row>
7677 <Flex.Item
7778 as="input"
7879 {...rest}
@@ -114,7 +115,12 @@ export default function Input({
114115 {accessory}
115116 </Flex.Item>
116117 )}
117− </Flex>
118+ </Flex.Item>
119+ {error && (
120+ <Flex.Item as={ErrorText} className="mv1">
121+ {error}
122+ </Flex.Item>
123+ )}
118124 </Flex>
119125 );
120126}
src/indigo-react/components/Typography.js+1
@@ -21,3 +21,4 @@ export const P = typeWith('p');
2121export const Breadcrumb = typeWith('span', 'f6');
2222export const Text = typeWith('span', 'f5');
2323export const HelpText = typeWith('span', 'f6 gray4');
24+export const ErrorText = typeWith('span', 'f6 red3');
src/indigo-react/lib/useForm.js+21−19
@@ -99,40 +99,42 @@ export default function useForm(configs = []) {
9999 );
100100
101101 const datas = useMemo(() => validations.map(v => v.data), [validations]);
102− const passes = useMemo(() => validations.map(v => v.pass), [validations]);
103102
104103 // the input has errored if it
105− // 1) did not pass validation
106− // 2) has an error text
107− // 3) has been focused before
108− // 4) is not currently focused
109− // returning the error text at the end
104+ // 1) has been focused before
105+ // 2)
106+ // a) did not pass validation and has an error text
107+ // b) or has a specific error from parent state
108+ // (and we implicitly return the error text at the end)
110109 const errors = useMemo(
111110 () =>
112111 configs.map(
113112 (config, i) =>
114− config.error ||
115− (!validations[i].pass &&
116− hasBeenFocused[config.name] &&
117− !focused[config.name] &&
118− validations[i].error)
113+ hasBeenFocused[config.name] && //
114+ (validations[i].error || config.error)
119115 ),
120− [configs, validations, hasBeenFocused, focused]
116+ [configs, validations, hasBeenFocused]
121117 );
122118
123− // visibly tell the user that their input has passed if
124− // 1) it has passed
119+ // this input has passed if
120+ // 1) it has passed local validation
125121 // 2) there are no errors
126− // 3) they are or have interacted with the input before
122+ const passes = useMemo(
123+ () => validations.map((v, i) => v.pass && !errors[i]),
124+ [validations, errors]
125+ );
126+
127+ // visibly tell the user that their input has passed if
128+ // 1) they are or have interacted with the input before
129+ // 2) and it has passed as defined above
127130 const visiblePasses = useMemo(
128131 () =>
129132 configs.map(
130133 (config, i) =>
131− validations[i].pass &&
132− !errors[i] &&
133− (hasBeenFocused[config.name] || focused[config.name])
134+ (focused[config.name] || hasBeenFocused[config.name]) && //
135+ passes[i]
134136 ),
135− [configs, validations, errors, hasBeenFocused, focused]
137+ [configs, hasBeenFocused, focused, passes]
136138 );
137139
138140 const inputs = useMemo(
src/lib/useMailer.js+24−12
@@ -1,10 +1,12 @@
1−import { useCallback, useEffect } from 'react';
1+import { useCallback } from 'react';
22import Maybe from 'folktale/maybe';
33
44import { hasReceived, sendMail } from './inviteMail';
55import useSetState from './useSetState';
66
7−function useHasReceivedCache(emails = []) {
7+const kStubMailer = process.env.REACT_APP_STUB_MAILER === 'true';
8+
9+function useHasReceivedCache() {
810 const [cache, addToCache] = useSetState();
911
1012 const getHasRecieved = useCallback(
@@ -12,27 +14,37 @@ function useHasReceivedCache(emails = []) {
1214 [cache]
1315 );
1416
15− useEffect(() => {
16− for (const email of emails) {
17+ const syncHasReceivedForEmail = useCallback(
18+ async email => {
1719 if (Maybe.Just.hasInstance(getHasRecieved(email))) {
1820 // never update the cache after we know about it
1921 return;
2022 }
2123
22− (async () => {
23− addToCache({ [email]: await hasReceived(email) });
24− })();
25− }
26− }, [emails, getHasRecieved, addToCache]);
24+ if (kStubMailer) {
25+ // always allow sending emails when stubbing
26+ return addToCache({ [email]: Maybe.Just(false) });
27+ }
2728
28− return { getHasRecieved };
29+ const _hasReceived = await hasReceived(email);
30+ addToCache({ [email]: Maybe.Just(_hasReceived) });
31+ },
32+ [getHasRecieved, addToCache]
33+ );
34+
35+ return { getHasRecieved, syncHasReceivedForEmail };
2936}
3037
3138export default function useMailer(emails) {
32− const { getHasRecieved } = useHasReceivedCache(emails);
39+ const hasReceivedCache = useHasReceivedCache(emails);
3340
3441 // prefix to avoid clobbering sendMail import
3542 const _sendMail = useCallback(async (email, ticket, rawTx) => {
43+ if (kStubMailer) {
44+ console.log(`${email} - ${ticket}`);
45+ return;
46+ }
47+
3648 const mailSuccess = await sendMail(email, ticket, rawTx);
3749
3850 if (!mailSuccess) {
@@ -40,5 +52,5 @@ export default function useMailer(emails) {
4052 }
4153 }, []);
4254
43− return { getHasRecieved, sendMail: _sendMail };
55+ return { ...hasReceivedCache, sendMail: _sendMail };
4456}
src/views/Invite/InviteEmail.js+23−14
@@ -2,7 +2,15 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
22import cn from 'classnames';
33import Maybe from 'folktale/maybe';
44import * as azimuth from 'azimuth-js';
5−import { Grid, Flex, Input, IconButton, HelpText, Text } from 'indigo-react';
5+import {
6+ Grid,
7+ Flex,
8+ Input,
9+ IconButton,
10+ HelpText,
11+ Text,
12+ ErrorText,
13+} from 'indigo-react';
614import { uniq } from 'lodash';
715
816import * as need from 'lib/need';
@@ -35,8 +43,7 @@ import { useWallet } from 'store/wallet';
3543import useSetState from 'lib/useSetState';
3644import pluralize from 'lib/pluralize';
3745import useMailer from 'lib/useMailer';
38−
39−const kStubMailer = process.env.REACT_APP_STUB_MAILER === 'true';
46+import useRenderCount from 'lib/useRenderCount';
4047
4148const GAS_PRICE_GWEI = 20; // we pay the premium for faster ux
4249const GAS_LIMIT = 350000;
@@ -89,7 +96,6 @@ const buildAccessoryFor = (dones, errors) => name => (
8996 </Flex>
9097);
9198
92−// TODO: how to handle dependency loop between inputConfigs and hasReceived?
9399// TODO: test with tank, successful txs
94100// TODO: put accessory inside of input
95101export default function InviteEmail() {
@@ -111,10 +117,11 @@ export default function InviteEmail() {
111117 inputConfigs,
112118 { append: appendInput, removeAt: removeInputAt },
113119 ] = useArray(
114− [buildInputConfig({ placeholder: 'Email Addresss' })],
120+ [buildInputConfig({ placeholder: 'Email Address' })],
115121 buildInputConfig
116122 );
117123
124+ const { getHasRecieved, syncHasReceivedForEmail, sendMail } = useMailer();
118125 const [hovered, setHovered] = useSetState();
119126 const [invites, addInvite, clearInvites] = useSetState();
120127 const [receipts, addReceipt, clearReceipts] = useSetState();
@@ -136,16 +143,19 @@ export default function InviteEmail() {
136143 () =>
137144 inputConfigs.map(config => {
138145 config.disabled = !canInput;
139− config.error = errors[config.name];
146+ const hasReceivedError = getHasRecieved(config.name).matchWith({
147+ Nothing: () => null, // loading
148+ Just: p => p.value && 'This email has already received an invite.',
149+ });
150+ config.error = hasReceivedError || errors[config.name];
140151 return config;
141152 }),
142− [inputConfigs, errors, canInput]
153+ [inputConfigs, errors, canInput, getHasRecieved]
143154 );
144155 const { inputs, pass } = useForm(dynamicConfigs);
145156 const emails = useMemo(() => inputs.map(i => i.data).filter(d => !!d), [
146157 inputs,
147158 ]);
148− const { getHasRecieved, sendMail } = useMailer(emails);
149159
150160 const canAddInvite = canInput && inputs.length < maxInvitesToSend;
151161 const allPass = pass && !generalError;
@@ -301,14 +311,10 @@ export default function InviteEmail() {
301311 addError({
302312 [input.name]: `Transaction Failure for ${invite.email}`,
303313 });
314+ return;
304315 }
305316
306317 try {
307− if (kStubMailer) {
308− console.log(`${invite.email} - ${invite.ticket}`);
309− return;
310− }
311−
312318 const mailSuccess = await sendMail(
313319 invite.email,
314320 invite.ticket,
@@ -345,6 +351,7 @@ export default function InviteEmail() {
345351 point,
346352 wallet,
347353 addError,
354+ sendMail,
348355 ]);
349356
350357 const onClickGenerate = useCallback(async () => {
@@ -439,6 +446,8 @@ export default function InviteEmail() {
439446 as={Input}
440447 cols={[1, 11]}
441448 {...input}
449+ onValue={email => syncHasReceivedForEmail(email)}
450+ // NB(shrugs): ^ this feels like a hack?
442451 accessory={accessoryFor(input.name)}
443452 />
444453 {!isFirst &&
@@ -483,7 +492,7 @@ export default function InviteEmail() {
483492
484493 {generalError && (
485494 <Grid.Item full>
486− <Highlighted>{generalError.message.toString()}</Highlighted>
495+ <ErrorText>{generalError.message.toString()}</ErrorText>
487496 </Grid.Item>
488497 )}
489498 </>