@@ -56,6 +56,7 @@ import {
56import { FORM_ERROR } from 'final-form';
56import { FORM_ERROR } from 'final-form';
57import SubmitButton from 'form/SubmitButton';
57import SubmitButton from 'form/SubmitButton';
58import FormError from 'form/FormError';
58import FormError from 'form/FormError';
59+import { WARNING, onlyHasWarning } from 'form/helpers';
60const INITIAL_VALUES = { emails: [''] };
61const INITIAL_VALUES = { emails: [''] };
@@ -72,6 +73,8 @@ const STATUS = {
72 FAILURE: 'FAILURE',
73 FAILURE: 'FAILURE',
76+const nameForEmailField = i => `emails[${i}]`;
75const buttonText = (status, count) => {
78const buttonText = (status, count) => {
76 switch (status) {
79 switch (status) {
77 case STATUS.INPUT:
80 case STATUS.INPUT:
@@ -105,7 +108,7 @@ export default function InviteEmail() {
105 const { getHasReceived, sendMail } = useMailer();
108 const { getHasReceived, sendMail } = useMailer();
106 const { gasPrice } = useSuggestedGasPrice(networkType);
109 const { gasPrice } = useSuggestedGasPrice(networkType);
108− const cachedEmails = useRef([]);
111+ const cachedEmails = useRef({});
110 const { availableInvites } = getInvites(point);
113 const { availableInvites } = getInvites(point);
111 const maxInvitesToSend = availableInvites.matchWith({
114 const maxInvitesToSend = availableInvites.matchWith({
@@ -158,9 +161,7 @@ export default function InviteEmail() {
159 composeValidator(
162 composeValidator(
161− emails: buildArrayValidator(
162− buildEmailValidator([validateHasReceived])
164+ emails: buildArrayValidator(buildEmailValidator(validateHasReceived)),
165 validateForm
166 validateForm
@@ -218,6 +219,7 @@ export default function InviteEmail() {
218 // NB(shrugs) - must be processed in serial because main thread, etc
219 // NB(shrugs) - must be processed in serial because main thread, etc
219 let errorCount = 0;
220 let errorCount = 0;
220 for (let i = 0; i < values.emails.length; i++) {
221 for (let i = 0; i < values.emails.length; i++) {
222+ const name = nameForEmailField(i);
222 const email = values.emails[i];
224 const email = values.emails[i];
223 const planet = planets[i];
225 const planet = planets[i];
@@ -249,20 +251,20 @@ export default function InviteEmail() {
250 const rawTx = hexify(signedTx.serialize());
252 const rawTx = hexify(signedTx.serialize());
252− addInvite({ [email]: { email, ticket, signedTx, rawTx } });
254+ addInvite({ [name]: { email, ticket, signedTx, rawTx } });
253 } catch (error) {
255 } catch (error) {
254 console.error(error);
256 console.error(error);
255 errorCount++;
257 errorCount++;
258+ addError({ [name]: 'Error generating invite.' });
259 if (errorCount > 0) {
262 if (errorCount > 0) {
261− [FORM_ERROR]: `There ${pluralize(
264+ [WARNING]: `There ${pluralize(errorCount, 'was', 'were')} ${pluralize(
262 errorCount,
265 errorCount,
264− 'were'
267+ )} while generating wallets. You can still send the invites that generated correctly.`,
265− )} ${pluralize(errorCount, 'error')} while generating wallets.`,
@@ -278,11 +280,17 @@ export default function InviteEmail() {
278 networkType,
280 networkType,
279 gasPrice,
281 gasPrice,
280 addInvite,
282 addInvite,
284 const sendInvites = useCallback(async () => {
287 const sendInvites = useCallback(async () => {
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]);
285− const emails = cachedEmails.current;
292+ const emails = allFieldNames.map(name => cachedEmails.current[name]);
286 const _web3 = web3.getOrElse(null);
294 const _web3 = web3.getOrElse(null);
287 const _wallet = wallet.getOrElse(null);
295 const _wallet = wallet.getOrElse(null);
288 if (!_web3 || !_wallet) {
296 if (!_web3 || !_wallet) {
@@ -295,7 +303,7 @@ export default function InviteEmail() {
296 _wallet.address,
304 _wallet.address,
297 toWei((gasPrice * GAS_LIMIT * emails.length).toString(), 'gwei'),
305 toWei((gasPrice * GAS_LIMIT * emails.length).toString(), 'gwei'),
298− emails.map(email => invites[email].rawTx),
306+ names.map(name => invites[name].rawTx),
299 (address, minBalance, balance) =>
307 (address, minBalance, balance) =>
300 setNeedFunds({ address, minBalance, balance }),
308 setNeedFunds({ address, minBalance, balance }),
301 () => setNeedFunds(undefined)
309 () => setNeedFunds(undefined)
@@ -306,8 +314,8 @@ export default function InviteEmail() {
307 let unsentInvites = [];
315 let unsentInvites = [];
308 let orphanedInvites = [];
316 let orphanedInvites = [];
309− const txAndMailings = emails.map(async email => {
317+ const txAndMailings = names.map(async name => {
310− const invite = invites[email];
318+ const invite = invites[name];
312 const txHash = await sendSignedTransaction(
320 const txHash = await sendSignedTransaction(
@@ -334,7 +342,7 @@ export default function InviteEmail() {
334 orphanedInvites.push(invite);
342 orphanedInvites.push(invite);
337− addReceipt({ [email]: true });
345+ addReceipt({ [name]: true });
340 await Promise.all(txAndMailings);
348 await Promise.all(txAndMailings);
@@ -363,20 +371,28 @@ export default function InviteEmail() {
365 gasPrice,
373 gasPrice,
368 addReceipt,
376 addReceipt,
369 sendMail,
377 sendMail,
372 const onSubmit = useCallback(
380 const onSubmit = useCallback(
373 async values => {
381 async values => {
374− cachedEmails.current = values.emails;
382+ cachedEmails.current = values.emails.reduce((memo, email, i) => {
383+ memo[nameForEmailField(i)] = email;
375 setStatus(STATUS.GENERATING);
386 setStatus(STATUS.GENERATING);
376 const errors = await generateInvites(values);
387 const errors = await generateInvites(values);
388+ if (hasErrors(errors)) {
377− if (errors) {
389+ if (onlyHasWarning(errors)) {
390+ setStatus(STATUS.CAN_SEND);
378 return errors;
393 return errors;
380 setStatus(STATUS.CAN_SEND);
396 setStatus(STATUS.CAN_SEND);
382 [generateInvites]
398 [generateInvites]
@@ -409,130 +425,133 @@ export default function InviteEmail() {
409 validate={validate}
425 validate={validate}
410 onSubmit={onSubmit}
426 onSubmit={onSubmit}
411 initialValues={INITIAL_VALUES}>
427 initialValues={INITIAL_VALUES}>
412− {({ handleSubmit, valid, values }) => (
428+ {({ handleSubmit }) => (
413 <FieldArray name="emails">
429 <FieldArray name="emails">
414− {({ fields }) => (
430+ {({ fields }) => {
415− <>
431+ const sentInviteNames = Object.keys(receipts);
416− <Grid.Item full as={Flex} justify="end">
434+ <Grid.Item full as={Flex} justify="end">
417− {/* use hidden class instead of removing component from dom */}
435+ {/* use hidden class instead of removing component from dom */}
418− {/* in order to avoid janky reflow */}
436+ {/* in order to avoid janky reflow */}
419− <IconButton
437+ <IconButton
420− onClick={() => fields.push('')}
438+ onClick={() => fields.push('')}
421− disabled={!canInput || fields.length >= maxInvitesToSend}
439+ disabled={!canInput || fields.length >= maxInvitesToSend}
422− className={cn({ hidden: isDone })}
440+ className={cn({ hidden: isDone })}
425− </IconButton>
443+ </IconButton>
426− </Grid.Item>
444+ </Grid.Item>
428− {isDone ? (
446+ {isDone ? (
430− <Grid.Item as={Text} className="f5" full>
448+ <Grid.Item as={Text} className="f5" full>
431− <Highlighted>
449+ <Highlighted>
432− {pluralize(fields.length, 'invite')}
450+ {pluralize(sentInviteNames.length, 'invite')}
433− </Highlighted>{' '}
451+ </Highlighted>{' '}
434− {pluralize(fields.length, 'has', 'have')} been
452+ {pluralize(sentInviteNames.length, 'has', 'have')} been
435− successfully sent
453+ successfully sent
438− {fields.map(name => (
439− <Grid.Item as={HelpText} key={name} full>
441− {({ input: { value } }) => value}
443 </Grid.Item>
454 </Grid.Item>
445− </>
456+ {sentInviteNames.map(name => (
448− {fields.map((name, i) => {
449− const isFirst = i === 0;
451− <Grid.Item
457+ <Grid.Item as={HelpText} key={name} full>
456− onMouseOver={() => setHovered({ [name]: true })}
457− onMouseLeave={() => setHovered({ [name]: false })}>
462− label={isFirst ? 'Email Address' : undefined}
463− accessory={accessoryFor(name)}
465 <Field name={name}>
458 <Field name={name}>
466− {({ meta: { active } }) => {
459+ {({ input: { value } }) => value}
470− (active || hovered[name]) &&
477− onClick={() => fields.remove(i)}
488 </Grid.Item>
461 </Grid.Item>
466+ {fields.map((name, i) => {
467+ const isFirst = i === 0;
474+ onMouseOver={() => setHovered({ [name]: true })}
475+ onMouseLeave={() => setHovered({ [name]: false })}>
480+ label={isFirst ? 'Email Address' : undefined}
481+ accessory={accessoryFor(name)}
484+ {({ meta: { active } }) => {
488+ (active || hovered[name]) &&
495+ onClick={() => fields.remove(i)}
492− {canInput ? (
510+ {canInput ? (
493− <Grid.Item
511+ <Grid.Item
495− as={SubmitButton}
513+ as={SubmitButton}
496− handleSubmit={handleSubmit}
514+ handleSubmit={handleSubmit}
497− accessory={`${visualProgress}/${fields.length}`}>
515+ accessory={`${visualProgress}/${fields.length}`}>
498− {buttonText(status, fields.length)}
516+ {buttonText(status, fields.length)}
499− </Grid.Item>
517+ </Grid.Item>
501− <Grid.Item
519+ <Grid.Item
503− as={LoadableButton}
521+ as={LoadableButton}
504− className="mt4"
522+ className="mt4"
505− disabled={!canSend}
523+ disabled={!canSend}
506− accessory={`${visualProgress}/${fields.length}`}
524+ accessory={`${visualProgress}/${fields.length}`}
507− onClick={doSend}
525+ onClick={doSend}
508− success={canSend}
526+ success={canSend}
510− {buttonText(status, fields.length)}
528+ {buttonText(status, fields.length)}
511− </Grid.Item>
529+ </Grid.Item>
514− {needFunds && (
532+ {needFunds && (
515− <Grid.Item full>
533+ <Grid.Item full>
516− <Highlighted warning>
534+ <Highlighted warning>
517− Your ownership address {needFunds.address} needs at
535+ Your ownership address {needFunds.address} needs at
518− least {fromWei(needFunds.minBalance)} ETH and
536+ least {fromWei(needFunds.minBalance)} ETH and
519− currently has {fromWei(needFunds.balance)} ETH.
537+ currently has {fromWei(needFunds.balance)} ETH.
520− Waiting until the account has enough funds.
538+ Waiting until the account has enough funds.
521− </Highlighted>
539+ </Highlighted>
522− </Grid.Item>
540+ </Grid.Item>
525− <Grid.Item full as={FormError} />
543+ <Grid.Item full as={FormError} />
527− {generalError && (
545+ {generalError && (
528− <Grid.Item full as={ErrorText}>
546+ <Grid.Item full as={ErrorText}>
529− {generalError}
547+ {generalError}
530− </Grid.Item>
548+ </Grid.Item>
536 </FieldArray>
555 </FieldArray>
538 </BridgeForm>
557 </BridgeForm>