@@ -56,6 +56,7 @@ import {
5656import { FORM_ERROR } from 'final-form';
5757import SubmitButton from 'form/SubmitButton';
5858import FormError from 'form/FormError';
59+import { WARNING, onlyHasWarning } from 'form/helpers';
5960
6061const INITIAL_VALUES = { emails: [''] };
6162
@@ -72,6 +73,8 @@ const STATUS = {
7273 FAILURE: 'FAILURE',
7374};
7475
76+const nameForEmailField = i => `emails[${i}]`;
77+
7578const buttonText = (status, count) => {
7679 switch (status) {
7780 case STATUS.INPUT:
@@ -105,7 +108,7 @@ export default function InviteEmail() {
105108 const { getHasReceived, sendMail } = useMailer();
106109 const { gasPrice } = useSuggestedGasPrice(networkType);
107110
108โ const cachedEmails = useRef([]);
111+ const cachedEmails = useRef({});
109112
110113 const { availableInvites } = getInvites(point);
111114 const maxInvitesToSend = availableInvites.matchWith({
@@ -158,9 +161,7 @@ export default function InviteEmail() {
158161 () =>
159162 composeValidator(
160163 {
161โ emails: buildArrayValidator(
162โ buildEmailValidator([validateHasReceived])
163โ ),
164+ emails: buildArrayValidator(buildEmailValidator(validateHasReceived)),
164165 },
165166 validateForm
166167 ),
@@ -218,6 +219,7 @@ export default function InviteEmail() {
218219 // NB(shrugs) - must be processed in serial because main thread, etc
219220 let errorCount = 0;
220221 for (let i = 0; i < values.emails.length; i++) {
222+ const name = nameForEmailField(i);
221223 try {
222224 const email = values.emails[i];
223225 const planet = planets[i];
@@ -249,20 +251,20 @@ export default function InviteEmail() {
249251
250252 const rawTx = hexify(signedTx.serialize());
251253
252โ addInvite({ [email]: { email, ticket, signedTx, rawTx } });
254+ addInvite({ [name]: { email, ticket, signedTx, rawTx } });
253255 } catch (error) {
254256 console.error(error);
255257 errorCount++;
258+ addError({ [name]: 'Error generating invite.' });
256259 }
257260 }
258261
259262 if (errorCount > 0) {
260263 return {
261โ [FORM_ERROR]: `There ${pluralize(
264+ [WARNING]: `There ${pluralize(errorCount, 'was', 'were')} ${pluralize(
262265 errorCount,
263โ 'was',
264โ 'were'
265โ )} ${pluralize(errorCount, 'error')} while generating wallets.`,
266+ 'error'
267+ )} while generating wallets. You can still send the invites that generated correctly.`,
266268 };
267269 }
268270 },
@@ -278,11 +280,17 @@ export default function InviteEmail() {
278280 networkType,
279281 gasPrice,
280282 addInvite,
283+ addError,
281284 ]
282285 );
283286
284287 const sendInvites = useCallback(async () => {
285โ const emails = cachedEmails.current;
288+ // compute all of the names of the fields
289+ const allFieldNames = Object.keys(cachedEmails.current);
290+ // only emails we're sending are the valid ones with invites
291+ const names = allFieldNames.filter(name => !!invites[name]);
292+ const emails = allFieldNames.map(name => cachedEmails.current[name]);
293+
286294 const _web3 = web3.getOrElse(null);
287295 const _wallet = wallet.getOrElse(null);
288296 if (!_web3 || !_wallet) {
@@ -295,7 +303,7 @@ export default function InviteEmail() {
295303 point,
296304 _wallet.address,
297305 toWei((gasPrice * GAS_LIMIT * emails.length).toString(), 'gwei'),
298โ emails.map(email => invites[email].rawTx),
306+ names.map(name => invites[name].rawTx),
299307 (address, minBalance, balance) =>
300308 setNeedFunds({ address, minBalance, balance }),
301309 () => setNeedFunds(undefined)
@@ -306,8 +314,8 @@ export default function InviteEmail() {
306314
307315 let unsentInvites = [];
308316 let orphanedInvites = [];
309โ const txAndMailings = emails.map(async email => {
310โ const invite = invites[email];
317+ const txAndMailings = names.map(async name => {
318+ const invite = invites[name];
311319 try {
312320 const txHash = await sendSignedTransaction(
313321 _web3,
@@ -334,7 +342,7 @@ export default function InviteEmail() {
334342 orphanedInvites.push(invite);
335343 }
336344
337โ addReceipt({ [email]: true });
345+ addReceipt({ [name]: true });
338346 });
339347
340348 await Promise.all(txAndMailings);
@@ -363,20 +371,28 @@ export default function InviteEmail() {
363371 wallet,
364372 point,
365373 gasPrice,
366โ clearReceipts,
367374 invites,
375+ clearReceipts,
368376 addReceipt,
369377 sendMail,
370378 ]);
371379
372380 const onSubmit = useCallback(
373381 async values => {
374โ cachedEmails.current = values.emails;
382+ cachedEmails.current = values.emails.reduce((memo, email, i) => {
383+ memo[nameForEmailField(i)] = email;
384+ return memo;
385+ }, {});
375386 setStatus(STATUS.GENERATING);
376387 const errors = await generateInvites(values);
377โ if (errors) {
388+ if (hasErrors(errors)) {
389+ if (onlyHasWarning(errors)) {
390+ setStatus(STATUS.CAN_SEND);
391+ }
392+
378393 return errors;
379394 }
395+
380396 setStatus(STATUS.CAN_SEND);
381397 },
382398 [generateInvites]
@@ -409,130 +425,133 @@ export default function InviteEmail() {
409425 validate={validate}
410426 onSubmit={onSubmit}
411427 initialValues={INITIAL_VALUES}>
412โ {({ handleSubmit, valid, values }) => (
428+ {({ handleSubmit }) => (
413429 <FieldArray name="emails">
414โ {({ fields }) => (
415โ <>
416โ <Grid.Item full as={Flex} justify="end">
417โ {/* use hidden class instead of removing component from dom */}
418โ {/* in order to avoid janky reflow */}
419โ <IconButton
420โ onClick={() => fields.push('')}
421โ disabled={!canInput || fields.length >= maxInvitesToSend}
422โ className={cn({ hidden: isDone })}
423โ solid>
424โ +
425โ </IconButton>
426โ </Grid.Item>
427โ
428โ {isDone ? (
429โ <>
430โ <Grid.Item as={Text} className="f5" full>
431โ <Highlighted>
432โ {pluralize(fields.length, 'invite')}
433โ </Highlighted>{' '}
434โ {pluralize(fields.length, 'has', 'have')} been
435โ successfully sent
436โ </Grid.Item>
437โ
438โ {fields.map(name => (
439โ <Grid.Item as={HelpText} key={name} full>
440โ <Field name={name}>
441โ {({ input: { value } }) => value}
442โ </Field>
430+ {({ fields }) => {
431+ const sentInviteNames = Object.keys(receipts);
432+ return (
433+ <>
434+ <Grid.Item full as={Flex} justify="end">
435+ {/* use hidden class instead of removing component from dom */}
436+ {/* in order to avoid janky reflow */}
437+ <IconButton
438+ onClick={() => fields.push('')}
439+ disabled={!canInput || fields.length >= maxInvitesToSend}
440+ className={cn({ hidden: isDone })}
441+ solid>
442+ +
443+ </IconButton>
444+ </Grid.Item>
445+
446+ {isDone ? (
447+ <>
448+ <Grid.Item as={Text} className="f5" full>
449+ <Highlighted>
450+ {pluralize(sentInviteNames.length, 'invite')}
451+ </Highlighted>{' '}
452+ {pluralize(sentInviteNames.length, 'has', 'have')} been
453+ successfully sent
443454 </Grid.Item>
444โ ))}
445โ </>
446โ ) : (
447โ <>
448โ {fields.map((name, i) => {
449โ const isFirst = i === 0;
450โ return (
451โ <Grid.Item
452โ key={name}
453โ full
454โ as={Grid}
455โ gap={3}
456โ onMouseOver={() => setHovered({ [name]: true })}
457โ onMouseLeave={() => setHovered({ [name]: false })}>
458โ <Grid.Item
459โ cols={[1, 11]}
460โ as={EmailInput}
461โ name={name}
462โ label={isFirst ? 'Email Address' : undefined}
463โ accessory={accessoryFor(name)}
464โ />
455+
456+ {sentInviteNames.map(name => (
457+ <Grid.Item as={HelpText} key={name} full>
465458 <Field name={name}>
466โ {({ meta: { active } }) => {
467โ return (
468โ <>
469โ {!isFirst &&
470โ (active || hovered[name]) &&
471โ canInput && (
472โ <Grid.Item
473โ cols={[11, 13]}
474โ justifySelf="end"
475โ alignSelf="center">
476โ <IconButton
477โ onClick={() => fields.remove(i)}
478โ solid
479โ secondary>
480โ -
481โ </IconButton>
482โ </Grid.Item>
483โ )}
484โ </>
485โ );
486โ }}
459+ {({ input: { value } }) => value}
487460 </Field>
488461 </Grid.Item>
489โ );
490โ })}
491โ
492โ {canInput ? (
493โ <Grid.Item
494โ full
495โ as={SubmitButton}
496โ handleSubmit={handleSubmit}
497โ accessory={`${visualProgress}/${fields.length}`}>
498โ {buttonText(status, fields.length)}
499โ </Grid.Item>
500โ ) : (
501โ <Grid.Item
502โ full
503โ as={LoadableButton}
504โ className="mt4"
505โ disabled={!canSend}
506โ accessory={`${visualProgress}/${fields.length}`}
507โ onClick={doSend}
508โ success={canSend}
509โ solid>
510โ {buttonText(status, fields.length)}
511โ </Grid.Item>
512โ )}
513โ
514โ {needFunds && (
515โ <Grid.Item full>
516โ <Highlighted warning>
517โ Your ownership address {needFunds.address} needs at
518โ least {fromWei(needFunds.minBalance)} ETH and
519โ currently has {fromWei(needFunds.balance)} ETH.
520โ Waiting until the account has enough funds.
521โ </Highlighted>
522โ </Grid.Item>
523โ )}
462+ ))}
463+ </>
464+ ) : (
465+ <>
466+ {fields.map((name, i) => {
467+ const isFirst = i === 0;
468+ return (
469+ <Grid.Item
470+ key={name}
471+ full
472+ as={Grid}
473+ gap={3}
474+ onMouseOver={() => setHovered({ [name]: true })}
475+ onMouseLeave={() => setHovered({ [name]: false })}>
476+ <Grid.Item
477+ cols={[1, 11]}
478+ as={EmailInput}
479+ name={name}
480+ label={isFirst ? 'Email Address' : undefined}
481+ accessory={accessoryFor(name)}
482+ />
483+ <Field name={name}>
484+ {({ meta: { active } }) => {
485+ return (
486+ <>
487+ {!isFirst &&
488+ (active || hovered[name]) &&
489+ canInput && (
490+ <Grid.Item
491+ cols={[11, 13]}
492+ justifySelf="end"
493+ alignSelf="center">
494+ <IconButton
495+ onClick={() => fields.remove(i)}
496+ solid
497+ secondary>
498+ -
499+ </IconButton>
500+ </Grid.Item>
501+ )}
502+ </>
503+ );
504+ }}
505+ </Field>
506+ </Grid.Item>
507+ );
508+ })}
509+
510+ {canInput ? (
511+ <Grid.Item
512+ full
513+ as={SubmitButton}
514+ handleSubmit={handleSubmit}
515+ accessory={`${visualProgress}/${fields.length}`}>
516+ {buttonText(status, fields.length)}
517+ </Grid.Item>
518+ ) : (
519+ <Grid.Item
520+ full
521+ as={LoadableButton}
522+ className="mt4"
523+ disabled={!canSend}
524+ accessory={`${visualProgress}/${fields.length}`}
525+ onClick={doSend}
526+ success={canSend}
527+ solid>
528+ {buttonText(status, fields.length)}
529+ </Grid.Item>
530+ )}
531+
532+ {needFunds && (
533+ <Grid.Item full>
534+ <Highlighted warning>
535+ Your ownership address {needFunds.address} needs at
536+ least {fromWei(needFunds.minBalance)} ETH and
537+ currently has {fromWei(needFunds.balance)} ETH.
538+ Waiting until the account has enough funds.
539+ </Highlighted>
540+ </Grid.Item>
541+ )}
524542
525โ <Grid.Item full as={FormError} />
543+ <Grid.Item full as={FormError} />
526544
527โ {generalError && (
528โ <Grid.Item full as={ErrorText}>
529โ {generalError}
530โ </Grid.Item>
531โ )}
532โ </>
533โ )}
534โ </>
535โ )}
545+ {generalError && (
546+ <Grid.Item full as={ErrorText}>
547+ {generalError}
548+ </Grid.Item>
549+ )}
550+ </>
551+ )}
552+ </>
553+ );
554+ }}
536555 </FieldArray>
537556 )}
538557 </BridgeForm>