@@ -1,6 +1,6 @@
1−import React, { useCallback, useEffect, useState } from 'react';
1+import React, { useCallback, useEffect, useMemo, useState } from 'react';
22import cn from 'classnames';
3−import Maybe, { Just, Nothing } from 'folktale/maybe';
3+import Maybe from 'folktale/maybe';
44import * as azimuth from 'azimuth-js';
55import { Grid, Flex, Input, IconButton, HelpText, Text } from 'indigo-react';
66import { uniq } from 'lodash';
@@ -90,7 +90,6 @@ const buildAccessoryFor = (dones, errors) => name => (
9090);
9191
9292// TODO: refactor transactions + mailer into hooks
93−// TODO: include input-specific error info in form elements
9493// TODO: put accessory inside of input
9594// TODO: implement hasReceived cache with useHasReceived
9695export default function NewInviteEmail() {
@@ -116,13 +115,7 @@ export default function NewInviteEmail() {
116115 buildInputConfig
117116 );
118117
119− const [hovered, _setHovered] = useState({});
120− const setHovered = (name, value) => () =>
121− _setHovered((value = { ...value, [name]: value }));
122−
123− // TODO: pass disabled, error info into the useForm hook?
124− const { inputs, pass } = useForm(inputConfigs);
125−
118+ const [hovered, setHovered] = useSetState({});
126119 const [invites, addInvite, clearInvites] = useSetState({});
127120 const [receipts, addReceipt, clearReceipts] = useSetState({});
128121 const [errors, addError, clearError] = useSetState({});
@@ -131,17 +124,29 @@ export default function NewInviteEmail() {
131124 const [needFunds, setNeedFunds] = useState(null);
132125 const [generalError, setGeneralError] = useState(null);
133126
134− const allPass = pass && !generalError;
135−
136127 const canInput = status === STATUS.INPUT;
137− const canGenerate = allPass && status === STATUS.INPUT;
138128 const canSend = status === STATUS.CAN_SEND;
139129 const isGenerating = status === STATUS.GENERATING;
140130 const isSending = status === STATUS.SENDING;
141131 const isFunding = status === STATUS.FUNDING;
142132 const isFailed = status === STATUS.FAILURE;
143133 const isDone = status === STATUS.SUCCESS;
134+
135+ const dynamicConfigs = useMemo(
136+ () =>
137+ inputConfigs.map(config => {
138+ config.disabled = !canInput;
139+ console.log(config.name, errors[config.name]);
140+ config.error = errors[config.name];
141+ return config;
142+ }),
143+ [inputConfigs, errors, canInput]
144+ );
145+ const { inputs, pass } = useForm(dynamicConfigs);
146+
144147 const canAddInvite = canInput && inputs.length < maxInvitesToSend;
148+ const allPass = pass && !generalError;
149+ const canGenerate = allPass && status === STATUS.INPUT;
145150
146151 const progress = isGenerating
147152 ? Object.keys(invites).length
@@ -211,7 +216,7 @@ export default function NewInviteEmail() {
211216 walletHdPath,
212217 networkType,
213218 // TODO: ^ make a useTransactionSigner to encapsulate this logic
214− txn: Just(inviteTx),
219+ txn: Maybe.Just(inviteTx),
215220 gasPrice: GAS_PRICE_GWEI.toString(),
216221 gasLimit: GAS_LIMIT.toString(),
217222 nonce: nonce + i,
@@ -230,7 +235,10 @@ export default function NewInviteEmail() {
230235
231236 if (errorCount > 0) {
232237 throw new Error(
233− `There were ${errorCount} errors while generating wallets.`
238+ `There ${pluralize(errorCount, 'was', 'were')} ${pluralize(
239+ errorCount,
240+ 'error'
241+ )} while generating wallets.`
234242 );
235243 }
236244 }, [
@@ -276,7 +284,7 @@ export default function NewInviteEmail() {
276284 try {
277285 const txHash = await sendSignedTransaction(
278286 _web3,
279− Just(invite.signedTx),
287+ Maybe.Just(invite.signedTx),
280288 tankWasUsed,
281289 () => {}
282290 );
@@ -288,7 +296,7 @@ export default function NewInviteEmail() {
288296 console.error(error);
289297 errorCount++;
290298 addError({
291− [input.name]: `Transaction Failure for ${invite.email}}`,
299+ [input.name]: `Transaction Failure for ${invite.email}`,
292300 });
293301 }
294302
@@ -308,7 +316,7 @@ export default function NewInviteEmail() {
308316 console.error(error);
309317 errorCount++;
310318 addError({
311− [input.name]: `Mailing Failure for ${invite.email}}`,
319+ [input.name]: `Mailing Failure for ${invite.email}`,
312320 });
313321 }
314322
@@ -319,7 +327,10 @@ export default function NewInviteEmail() {
319327 // if there are any receipt errors, throw a general error
320328 if (errorCount > 0) {
321329 throw new Error(
322− `There were ${errorCount} errors while sending transactions.`
330+ `There ${pluralize(errorCount, 'was', 'were')} ${pluralize(
331+ errorCount,
332+ 'error'
333+ )} while sending transactions.`
323334 );
324335 }
325336 }, [
@@ -368,7 +379,8 @@ export default function NewInviteEmail() {
368379 }, [canGenerate, canSend, onClickGenerate, onClickSend]);
369380
370381 useEffect(() => {
371− if (uniq(inputs.map(i => i.data)).length !== inputs.length) {
382+ const emails = inputs.map(i => i.data).filter(d => !!d);
383+ if (uniq(emails).length !== emails.length) {
372384 setGeneralError(new Error(`Duplicate email.`));
373385 } else {
374386 setGeneralError(null);
@@ -418,15 +430,14 @@ export default function NewInviteEmail() {
418430 key={input.name}
419431 as={Grid}
420432 gap={12}
421− onMouseEnter={setHovered(input.name, true)}
422− onMouseLeave={setHovered(input.name, false)}
433+ onMouseOver={() => setHovered({ [input.name]: true })}
434+ onMouseLeave={() => setHovered({ [input.name]: false })}
423435 full>
424436 <Grid.Item
425437 as={Input}
426438 cols={[1, 11]}
427439 {...input}
428440 accessory={accessoryFor(input.name)}
429− disabled={!canInput}
430441 />
431442 {!isFirst &&
432443 (input.focused || hovered[input.name]) &&