@@ -1,6 +1,6 @@
1−import React, { useCallback, useEffect, useState } from 'react';
1+import React, { useCallback, useEffect, useMemo, useState } from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3−import Maybe, { Just, Nothing } from 'folktale/maybe';
3+import Maybe from 'folktale/maybe';
4import * as azimuth from 'azimuth-js';
4import * as azimuth from 'azimuth-js';
5import { Grid, Flex, Input, IconButton, HelpText, Text } from 'indigo-react';
5import { Grid, Flex, Input, IconButton, HelpText, Text } from 'indigo-react';
6import { uniq } from 'lodash';
6import { uniq } from 'lodash';
@@ -90,7 +90,6 @@ const buildAccessoryFor = (dones, errors) => name => (
92// TODO: refactor transactions + mailer into hooks
92// TODO: refactor transactions + mailer into hooks
93−// TODO: include input-specific error info in form elements
94// TODO: put accessory inside of input
93// TODO: put accessory inside of input
95// TODO: implement hasReceived cache with useHasReceived
94// TODO: implement hasReceived cache with useHasReceived
96export default function NewInviteEmail() {
95export default function NewInviteEmail() {
@@ -116,13 +115,7 @@ export default function NewInviteEmail() {
116 buildInputConfig
115 buildInputConfig
119− const [hovered, _setHovered] = useState({});
118+ const [hovered, setHovered] = useSetState({});
120− const setHovered = (name, value) => () =>
121− _setHovered((value = { ...value, [name]: value }));
123− // TODO: pass disabled, error info into the useForm hook?
124− const { inputs, pass } = useForm(inputConfigs);
126 const [invites, addInvite, clearInvites] = useSetState({});
119 const [invites, addInvite, clearInvites] = useSetState({});
127 const [receipts, addReceipt, clearReceipts] = useSetState({});
120 const [receipts, addReceipt, clearReceipts] = useSetState({});
128 const [errors, addError, clearError] = useSetState({});
121 const [errors, addError, clearError] = useSetState({});
@@ -131,17 +124,29 @@ export default function NewInviteEmail() {
131 const [needFunds, setNeedFunds] = useState(null);
124 const [needFunds, setNeedFunds] = useState(null);
132 const [generalError, setGeneralError] = useState(null);
125 const [generalError, setGeneralError] = useState(null);
134− const allPass = pass && !generalError;
136 const canInput = status === STATUS.INPUT;
127 const canInput = status === STATUS.INPUT;
137− const canGenerate = allPass && status === STATUS.INPUT;
138 const canSend = status === STATUS.CAN_SEND;
128 const canSend = status === STATUS.CAN_SEND;
139 const isGenerating = status === STATUS.GENERATING;
129 const isGenerating = status === STATUS.GENERATING;
140 const isSending = status === STATUS.SENDING;
130 const isSending = status === STATUS.SENDING;
141 const isFunding = status === STATUS.FUNDING;
131 const isFunding = status === STATUS.FUNDING;
142 const isFailed = status === STATUS.FAILURE;
132 const isFailed = status === STATUS.FAILURE;
143 const isDone = status === STATUS.SUCCESS;
133 const isDone = status === STATUS.SUCCESS;
135+ const dynamicConfigs = useMemo(
137+ inputConfigs.map(config => {
138+ config.disabled = !canInput;
139+ console.log(config.name, errors[config.name]);
140+ config.error = errors[config.name];
143+ [inputConfigs, errors, canInput]
145+ const { inputs, pass } = useForm(dynamicConfigs);
144 const canAddInvite = canInput && inputs.length < maxInvitesToSend;
147 const canAddInvite = canInput && inputs.length < maxInvitesToSend;
148+ const allPass = pass && !generalError;
149+ const canGenerate = allPass && status === STATUS.INPUT;
146 const progress = isGenerating
151 const progress = isGenerating
147 ? Object.keys(invites).length
152 ? Object.keys(invites).length
@@ -211,7 +216,7 @@ export default function NewInviteEmail() {
211 walletHdPath,
216 walletHdPath,
212 networkType,
217 networkType,
213 // TODO: ^ make a useTransactionSigner to encapsulate this logic
218 // TODO: ^ make a useTransactionSigner to encapsulate this logic
214− txn: Just(inviteTx),
219+ txn: Maybe.Just(inviteTx),
215 gasPrice: GAS_PRICE_GWEI.toString(),
220 gasPrice: GAS_PRICE_GWEI.toString(),
216 gasLimit: GAS_LIMIT.toString(),
221 gasLimit: GAS_LIMIT.toString(),
217 nonce: nonce + i,
222 nonce: nonce + i,
@@ -230,7 +235,10 @@ export default function NewInviteEmail() {
231 if (errorCount > 0) {
236 if (errorCount > 0) {
232 throw new Error(
237 throw new Error(
238+ `There ${pluralize(errorCount, 'was', 'were')} ${pluralize(
233− `There were ${errorCount} errors while generating wallets.`
241+ )} while generating wallets.`
@@ -276,7 +284,7 @@ export default function NewInviteEmail() {
277 const txHash = await sendSignedTransaction(
285 const txHash = await sendSignedTransaction(
279− Just(invite.signedTx),
287+ Maybe.Just(invite.signedTx),
280 tankWasUsed,
288 tankWasUsed,
@@ -288,7 +296,7 @@ export default function NewInviteEmail() {
288 console.error(error);
296 console.error(error);
289 errorCount++;
297 errorCount++;
290 addError({
298 addError({
291− [input.name]: `Transaction Failure for ${invite.email}}`,
299+ [input.name]: `Transaction Failure for ${invite.email}`,
@@ -308,7 +316,7 @@ export default function NewInviteEmail() {
308 console.error(error);
316 console.error(error);
309 errorCount++;
317 errorCount++;
310 addError({
318 addError({
311− [input.name]: `Mailing Failure for ${invite.email}}`,
319+ [input.name]: `Mailing Failure for ${invite.email}`,
@@ -319,7 +327,10 @@ export default function NewInviteEmail() {
319 // if there are any receipt errors, throw a general error
327 // if there are any receipt errors, throw a general error
320 if (errorCount > 0) {
328 if (errorCount > 0) {
321 throw new Error(
329 throw new Error(
330+ `There ${pluralize(errorCount, 'was', 'were')} ${pluralize(
322− `There were ${errorCount} errors while sending transactions.`
333+ )} while sending transactions.`
@@ -368,7 +379,8 @@ export default function NewInviteEmail() {
368 }, [canGenerate, canSend, onClickGenerate, onClickSend]);
379 }, [canGenerate, canSend, onClickGenerate, onClickSend]);
370 useEffect(() => {
381 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) {
372 setGeneralError(new Error(`Duplicate email.`));
384 setGeneralError(new Error(`Duplicate email.`));
374 setGeneralError(null);
386 setGeneralError(null);
@@ -418,15 +430,14 @@ export default function NewInviteEmail() {
418 key={input.name}
430 key={input.name}
419 as={Grid}
431 as={Grid}
421− onMouseEnter={setHovered(input.name, true)}
433+ onMouseOver={() => setHovered({ [input.name]: true })}
422− onMouseLeave={setHovered(input.name, false)}
434+ onMouseLeave={() => setHovered({ [input.name]: false })}
424 <Grid.Item
436 <Grid.Item
425 as={Input}
437 as={Input}
426 cols={[1, 11]}
438 cols={[1, 11]}
427 {...input}
439 {...input}
428 accessory={accessoryFor(input.name)}
440 accessory={accessoryFor(input.name)}
429− disabled={!canInput}
431 {!isFirst &&
442 {!isFirst &&
432 (input.focused || hovered[input.name]) &&
443 (input.focused || hovered[input.name]) &&