on master help sign in
fix dirtySinceLastSubmit for email chip input
c19b2a8Liam Fitzgerald
src/form/BridgeForm.js+1
@@ -25,6 +25,7 @@ export default function BridgeForm({
25 async (...args) => {
25 async (...args) => {
26 const errors = await onSubmit(...args);
26 const errors = await onSubmit(...args);
27 if (errors) {
27 if (errors) {
28+ console.log(errors);
28 return errors;
29 return errors;
29 }
30 }
30
31
src/form/EmailChipInput.js+19−36
@@ -1,19 +1,20 @@
1import React, { useState, useCallback } from 'react';
1import React, { useState, useCallback } from 'react';
2−import { isEqual } from 'lodash';
3import { Flex, ErrorText } from 'indigo-react';
2import { Flex, ErrorText } from 'indigo-react';
4import cn from 'classnames';
3import cn from 'classnames';
5import { useField } from 'react-final-form';
4import { useField } from 'react-final-form';
5+import { useFieldArray } from 'react-final-form-arrays';
6
6
7import { hasErrors } from 'form/validators';
7import { hasErrors } from 'form/validators';
8
8
9−const Chip = ({ value, onDelete, disabled }) => {
9+const Chip = ({ onDelete, disabled, name }) => {
10+ const { input } = useField(name, { subscription: { value: true } });
10 return (
11 return (
11 <Flex
12 <Flex
12 className={cn('r32 ph2 m1 flex-center h7', {
13 className={cn('r32 ph2 m1 flex-center h7', {
13 'bg-gray2': !disabled,
14 'bg-gray2': !disabled,
14 'bg-gray1 gray4': disabled,
15 'bg-gray1 gray4': disabled,
15 })}>
16 })}>
16− <Flex.Item>{value}</Flex.Item>
17+ <Flex.Item>{input.value}</Flex.Item>
17 <Flex.Item onClick={disabled ? onDelete : undefined} className="ml1 f6">
18 <Flex.Item onClick={disabled ? onDelete : undefined} className="ml1 f6">
18 ✗
19 ✗
19 </Flex.Item>
20 </Flex.Item>
@@ -22,42 +23,25 @@ const Chip = ({ value, onDelete, disabled }) => {
22};
23};
23
24
24const EmailChipInput = ({ className, name, label, disabled }) => {
25const EmailChipInput = ({ className, name, label, disabled }) => {
25− const [chips, _setChips] = useState([]);
26+ const {
27+ fields,
28+ meta: { error, active },
29+ } = useFieldArray(name, { subscription: { error: true, active: true } });
26
30
27 const [value, setValue] = useState('');
31 const [value, setValue] = useState('');
28
32
29 const handleChange = useCallback(
33 const handleChange = useCallback(
30 event => {
34 event => {
35+ event.preventDefault();
31 setValue(event.target.value);
36 setValue(event.target.value);
32 },
37 },
33 [setValue]
38 [setValue]
34 );
39 );
35
40
36− const {
37− input,
38− meta: { active, error },
39− } = useField(name, {
40− subscription: {
41− active: true,
42− error: true,
43− value: true,
44− },
45− isEqual,
46− });
47−
48− const setChips = useCallback(
49− chips => {
50− console.log(chips);
51− _setChips(chips);
52− input.onChange(chips);
53− },
54− [input, _setChips]
55− );
56−
57 const addToChips = useCallback(() => {
41 const addToChips = useCallback(() => {
58− setChips([...chips, value]);
42+ fields.push(value);
59 setValue('');
43 setValue('');
60− }, [chips, value, setChips]);
44+ }, [value, fields, setValue]);
61
45
62 const handleKeyDown = useCallback(
46 const handleKeyDown = useCallback(
63 event => {
47 event => {
@@ -66,29 +50,29 @@ const EmailChipInput = ({ className, name, label, disabled }) => {
66 addToChips();
50 addToChips();
67 } else if ('Backspace' === event.key && value === '') {
51 } else if ('Backspace' === event.key && value === '') {
68 event.preventDefault();
52 event.preventDefault();
69− setChips(chips.slice(0, -1));
53+ fields.pop();
70 } else if (disabled) {
54 } else if (disabled) {
71 event.preventDefault();
55 event.preventDefault();
72 }
56 }
73 },
57 },
74− [setChips, chips, value, addToChips, disabled]
58+ [fields, value, disabled, addToChips]
75 );
59 );
76
60
77 const handleDelete = useCallback(
61 const handleDelete = useCallback(
78 idx => () => {
62 idx => () => {
79− setChips([...chips.slice(0, idx), ...chips.slice(idx + 1)]);
63+ fields.remove(idx);
80 },
64 },
81− [setChips, chips]
65+ [fields]
82 );
66 );
83
67
84 const handleBlur = useCallback(
68 const handleBlur = useCallback(
85 e => {
69 e => {
70+ e.preventDefault();
86 if (value !== '') {
71 if (value !== '') {
87 addToChips();
72 addToChips();
88− input.onBlur();
89 }
73 }
90 },
74 },
91− [addToChips, value, input]
75+ [addToChips, value]
92 );
76 );
93
77
94 const hasError = hasErrors(error);
78 const hasError = hasErrors(error);
@@ -105,10 +89,10 @@ const EmailChipInput = ({ className, name, label, disabled }) => {
105 'b-green2': active && !disabled && !hasError,
89 'b-green2': active && !disabled && !hasError,
106 'b-red3': active && !disabled && hasError,
90 'b-red3': active && !disabled && hasError,
107 })}>
91 })}>
108− {chips.map((chip, idx) => (
92+ {fields.map((name, idx) => (
109 <Chip
93 <Chip
110 key={idx}
94 key={idx}
111− value={chip}
95+ name={name}
112 disabled={disabled}
96 disabled={disabled}
113 onDelete={handleDelete(idx)}
97 onDelete={handleDelete(idx)}
114 />
98 />
@@ -119,7 +103,6 @@ const EmailChipInput = ({ className, name, label, disabled }) => {
119 className="mv1 mh2 h7 b-none flex-grow"
103 className="mv1 mh2 h7 b-none flex-grow"
120 placeholder="Enter an email address"
104 placeholder="Enter an email address"
121 value={value}
105 value={value}
122− name={name}
123 onChange={handleChange}
106 onChange={handleChange}
124 onKeyDown={handleKeyDown}
107 onKeyDown={handleKeyDown}
125 onBlur={handleBlur}
108 onBlur={handleBlur}
src/form/FormError.js+1
@@ -25,6 +25,7 @@ export default function FormError(props) {
25
25
26 const warning = submitErrors && submitErrors[WARNING];
26 const warning = submitErrors && submitErrors[WARNING];
27 const showWarning = !!warning && !dirtySinceLastSubmit;
27 const showWarning = !!warning && !dirtySinceLastSubmit;
28+ console.log(dirtySinceLastSubmit);
28
29
29 return showSubmitError || showValidationError || showWarning ? (
30 return showSubmitError || showValidationError || showWarning ? (
30 <ErrorText {...props}>
31 <ErrorText {...props}>
src/lib/inviteMail.js+2
@@ -37,6 +37,8 @@ function sendMail(recipient, ticket, sender, message, tx) {
37 sender,
37 sender,
38 message,
38 message,
39 tx,
39 tx,
40+ }).then(() => {
41+ throw new Error('test');
40 });
42 });
41}
43}
42
44
src/views/Invite/Cohort.js+106−38
@@ -1,15 +1,14 @@
1import React, { useMemo, useCallback, useState, useEffect } from 'react';
1import React, { useMemo, useCallback, useState, useEffect } from 'react';
2import { Grid, ErrorText } from 'indigo-react';
2import { Grid, ErrorText } from 'indigo-react';
3−import { get } from 'lodash';
3+import { get, uniq, zip, compact, isEqual } from 'lodash';
4import * as azimuth from 'azimuth-js';
4import * as azimuth from 'azimuth-js';
5import { FORM_ERROR } from 'final-form';
5import { FORM_ERROR } from 'final-form';
6import { toWei, toBN } from 'web3-utils';
6import { toWei, toBN } from 'web3-utils';
7+import * as ob from 'urbit-ob';
7
8
8import * as need from 'lib/need';
9import * as need from 'lib/need';
9import * as wg from 'lib/walletgen';
10import * as wg from 'lib/walletgen';
10import * as tank from 'lib/tank';
11import * as tank from 'lib/tank';
11−// import useMailer from 'lib/useMailer';
12−import timeout from 'lib/timeout';
13import useCopiable from 'lib/useCopiable';
12import useCopiable from 'lib/useCopiable';
14
13
15import Tabs from 'components/Tabs';
14import Tabs from 'components/Tabs';
@@ -22,7 +21,6 @@ import { useWallet } from 'store/wallet';
22import { usePointCursor } from 'store/pointCursor';
21import { usePointCursor } from 'store/pointCursor';
23import { usePointCache } from 'store/pointCache';
22import { usePointCache } from 'store/pointCache';
24
23
25−// import { } from ''
26import {
24import {
27 signTransaction,
25 signTransaction,
28 sendSignedTransaction,
26 sendSignedTransaction,
@@ -38,11 +36,13 @@ import FormError from 'form/FormError';
38import SubmitButton from 'form/SubmitButton';
36import SubmitButton from 'form/SubmitButton';
39import BridgeForm from 'form/BridgeForm';
37import BridgeForm from 'form/BridgeForm';
40import {
38import {
41− buildEmailArrayValidator,
42 composeValidator,
39 composeValidator,
43 hasErrors,
40 hasErrors,
41+ buildEmailValidator,
42+ buildArrayValidator,
44} from 'form/validators';
43} from 'form/validators';
45import { WARNING } from 'form/helpers';
44import { WARNING } from 'form/helpers';
45+import useMailer from 'lib/useMailer';
46
46
47const GAS_LIMIT = GAS_LIMITS.GIFT_PLANET;
47const GAS_LIMIT = GAS_LIMITS.GIFT_PLANET;
48
48
@@ -53,6 +53,8 @@ const STATUS = {
53 FAILURE: 'FAILURE',
53 FAILURE: 'FAILURE',
54};
54};
55
55
56+const HAS_RECEIVED_TEXT = 'This email has already received an invite.';
57+
56const useInviter = sendInvites => {
58const useInviter = sendInvites => {
57 const { contracts, web3, networkType } = useNetwork();
59 const { contracts, web3, networkType } = useNetwork();
58 const { wallet, walletType, walletHdPath } = useWallet();
60 const { wallet, walletType, walletHdPath } = useWallet();
@@ -80,6 +82,7 @@ const useInviter = sendInvites => {
80 setInvites([]);
82 setInvites([]);
81 setTxStatus(STATUS.INPUT);
83 setTxStatus(STATUS.INPUT);
82 }, [setProgress, setInvites, setTxStatus]);
84 }, [setProgress, setInvites, setTxStatus]);
85+
83 const generateInvites = useCallback(
86 const generateInvites = useCallback(
84 async numInvites => {
87 async numInvites => {
85 const _contracts = contracts.getOrElse(null);
88 const _contracts = contracts.getOrElse(null);
@@ -102,7 +105,6 @@ const useInviter = sendInvites => {
102 setTxStatus(STATUS.SENDING);
105 setTxStatus(STATUS.SENDING);
103 // account for the race condition where invites got used up while we were
106 // account for the race condition where invites got used up while we were
104 // composing our target list
107 // composing our target list
105− console.log('a');
106 if (planets.length < numInvites) {
108 if (planets.length < numInvites) {
107 // resync invites to the cache, since they're out of date
109 // resync invites to the cache, since they're out of date
108 syncInvites(point);
110 syncInvites(point);
@@ -110,9 +112,11 @@ const useInviter = sendInvites => {
110 setTxStatus(STATUS.FAILURE);
112 setTxStatus(STATUS.FAILURE);
111
113
112 return {
114 return {
115+ errors: {
113− [FORM_ERROR]:
116+ [FORM_ERROR]:
114− `Can currently only send ${planets.length} invites. ` +
117+ `Can currently only send ${planets.length} invites. ` +
115− `Please remove invites until you are within the limit.`,
118+ `Please remove invites until you are within the limit.`,
119+ },
116 };
120 };
117 }
121 }
118
122
@@ -155,7 +159,9 @@ const useInviter = sendInvites => {
155 } catch (error) {
159 } catch (error) {
156 console.error(error);
160 console.error(error);
157 return {
161 return {
162+ errors: {
158− [WARNING]: `There was an error while generating wallets.`,
163+ [WARNING]: `There was an error while generating wallets.`,
164+ },
159 };
165 };
160 }
166 }
161 }
167 }
@@ -201,17 +207,24 @@ const useInviter = sendInvites => {
201 console.log(confirmedInvites);
207 console.log(confirmedInvites);
202
208
203 if (unsentInvites.length > 0) {
209 if (unsentInvites.length > 0) {
204− return { [FORM_ERROR]: unsentInvites };
210+ return { errors: { [FORM_ERROR]: unsentInvites } };
205 }
211 }
206
212
207 if (errorCount > 0) {
213 if (errorCount > 0) {
208 return {
214 return {
215+ errors: {
209− [WARNING]: `There ${pluralize(errorCount, 'was', 'were')} ${pluralize(
216+ [WARNING]: `There ${pluralize(
217+ errorCount,
218+ 'was',
219+ 'were'
220+ )} ${pluralize(
210− errorCount,
221+ errorCount,
211− 'error'
222+ 'error'
212− )} while generating wallets. You can still send the invites that generated correctly.`,
223+ )} while generating wallets. You can still send the invites that generated correctly.`,
224+ },
213 };
225 };
214 }
226 }
227+ return { invites: confirmedInvites };
215 },
228 },
216 [
229 [
217 contracts,
230 contracts,
@@ -238,14 +251,9 @@ const useInviter = sendInvites => {
238};
251};
239
252
240const InviteMail = () => {
253const InviteMail = () => {
241− const {
242− progress,
243− txStatus,
244− needFunds,
245− invites,
246− generateInvites,
254+ const { progress, txStatus, needFunds, generateInvites } = useInviter();
247− resetInvites,
255+
248− } = useInviter();
256+ const { sendMail, getHasReceived } = useMailer();
249
257
250 const [status, setStatus] = useState(STATUS.INPUT);
258 const [status, setStatus] = useState(STATUS.INPUT);
251
259
@@ -253,31 +261,62 @@ const InviteMail = () => {
253 const canInput = status === STATUS.INPUT;
261 const canInput = status === STATUS.INPUT;
254 const isSending = txStatus === STATUS.SENDING || status === STATUS.SENDING;
262 const isSending = txStatus === STATUS.SENDING || status === STATUS.SENDING;
255 const isDone = status === STATUS.SUCCESS;
263 const isDone = status === STATUS.SUCCESS;
264+ const isFailed = status === STATUS.FAILED;
265+
266+ const { pointCursor } = usePointCursor();
267+ const point = need.point(pointCursor);
268+
269+ const sendInvites = useCallback(
270+ async (emails, invites) => {
271+ const mailingErrors = compact(
272+ await Promise.all(
273+ zip(emails, invites).map(([email, invite]) =>
274+ sendMail(email, invite.ticket, ob.patp(point), '', invite.rawTx)
275+ .then(() => null)
276+ .catch(
277+ () => `Sending ticket ${invite.ticket} to ${email} failed.`
278+ )
279+ )
280+ )
281+ ).join(', ');
282+ if (mailingErrors.length) {
283+ return { [FORM_ERROR]: mailingErrors };
284+ }
285+ },
286+ [point, sendMail]
287+ );
288+
256 const onSubmit = useCallback(
289 const onSubmit = useCallback(
257 async values => {
290 async values => {
258 const emailCount = values.emails.length;
291 const emailCount = values.emails.length;
259 setCount(emailCount);
292 setCount(emailCount);
260 setStatus(STATUS.SENDING);
293 setStatus(STATUS.SENDING);
261− const errors = await generateInvites(emailCount);
294+ const { errors, invites } = await generateInvites(emailCount);
262 if (errors) {
295 if (errors) {
263 setStatus(STATUS.FAILED);
296 setStatus(STATUS.FAILED);
264 return errors;
297 return errors;
265 }
298 }
299+ const mailErrors = await sendInvites(values.emails, invites);
300+ console.log(mailErrors);
301+ if (mailErrors) {
302+ console.log(mailErrors);
303+ setStatus(STATUS.FAILED);
304+ return mailErrors;
305+ }
306+ setStatus(STATUS.SUCCESS);
266 },
307 },
267− [generateInvites, setCount, setStatus]
308+ [generateInvites, setCount, setStatus, sendInvites]
268 );
309 );
269
310
270− useEffect(() => {
311+ const validateHasReceived = useCallback(
271− async function processInvites() {
312+ async email => {
272− if (invites.length !== 0 && txStatus === STATUS.SUCCESS) {
313+ const hasReceived = await getHasReceived(email);
273− await timeout(500);
314+ if (hasReceived) {
274−
315+ return HAS_RECEIVED_TEXT;
275− resetInvites();
276− setStatus(STATUS.SUCCESS);
277 }
316 }
278− }
317+ },
279− processInvites();
318+ [getHasReceived]
280− }, [invites, setStatus, status, txStatus, resetInvites]);
319+ );
281
320
282 const buttonText = () => {
321 const buttonText = () => {
283 switch (status) {
322 switch (status) {
@@ -324,22 +363,51 @@ const InviteMail = () => {
324 </Grid.Item>
363 </Grid.Item>
325 );
364 );
326 }
365 }
366+ if (isFailed) {
367+ return (
368+ <Grid.Item full className="mt4 p3 bg-red3 white f5">
369+ {buttonText(count)}
370+ </Grid.Item>
371+ );
372+ }
327
373
328 return null;
374 return null;
329 };
375 };
330
376
377+ const validateForm = useCallback((values, errors) => {
378+ if (hasErrors(errors)) {
379+ return errors;
380+ }
381+
382+ // check for email uniqenesss
383+ const emails = values.emails.filter(d => !!d);
384+ if (uniq(emails).length !== emails.length) {
385+ return { [FORM_ERROR]: 'Duplicate email.' };
386+ }
387+ }, []);
388+
331 const validate = useMemo(
389 const validate = useMemo(
390+ () =>
332− () => composeValidator({ emails: buildEmailArrayValidator() }),
391+ composeValidator(
333− []
392+ {
393+ emails: buildArrayValidator(buildEmailValidator(validateHasReceived)),
394+ },
395+ validateForm
396+ ),
397+ [validateHasReceived, validateForm]
334 );
398 );
399+
400+ const initialValues = { emails: [] };
335 return (
401 return (
336 <Grid.Item full as={Grid}>
402 <Grid.Item full as={Grid}>
337 <BridgeForm
403 <BridgeForm
338 validate={validate}
404 validate={validate}
339− initialValues={{ emails: [] }}
405+ initialValues={initialValues}
406+ initialValuesEqual={isEqual}
340 onSubmit={onSubmit}>
407 onSubmit={onSubmit}>
341 {({ handleSubmit, values }) => (
408 {({ handleSubmit, values }) => (
342 <>
409 <>
410+ {console.log(values) && null}
343 <Grid.Item full as={EmailChipInput} name="emails" className="mt4" />
411 <Grid.Item full as={EmailChipInput} name="emails" className="mt4" />
344 {renderButton(handleSubmit)}
412 {renderButton(handleSubmit)}
345
413