@@ -1,4 +1,10 @@
1−import React, { useCallback, useEffect, useRef, useState } from 'react';
1+import React, {
2+ useCallback,
3+ useEffect,
4+ useRef,
5+ useMemo,
6+ useState,
7+} from 'react';
28import cn from 'classnames';
39import * as ob from 'urbit-ob';
410import * as azimuth from 'azimuth-js';
@@ -39,7 +45,12 @@ import LoadableButton from 'components/LoadableButton';
3945import Highlighted from 'components/Highlighted';
4046import BridgeForm from 'form/BridgeForm';
4147import { Field } from 'react-final-form';
42−import { EmailInput, buildEmailValidator } from 'form/Inputs';
48+import { EmailInput } from 'form/Inputs';
49+import {
50+ buildEmailValidator,
51+ composeValidator,
52+ hasErrors,
53+} from 'form/validators';
4354import { FORM_ERROR } from 'final-form';
4455import SubmitButton from 'form/SubmitButton';
4556import FormError from 'form/FormError';
@@ -89,9 +100,7 @@ const buildAccessoryFor = (dones, errors) => name => {
89100
90101const emailFormatValidator = buildEmailValidator();
91102
92−// TODO: test with tank, successful txs
93103export default function InviteEmail() {
94− // TODO: resumption after error?
95104 const { contracts, web3, networkType } = useNetwork();
96105 const { wallet, walletType, walletHdPath } = useWallet();
97106 const { syncInvites, getInvites } = usePointCache();
@@ -126,41 +135,44 @@ export default function InviteEmail() {
126135 const isFailed = status === STATUS.FAILURE;
127136 const isDone = status === STATUS.SUCCESS;
128137
129− const validate = useCallback(
130− async values => {
131− // compute the list of valid emails
132− const emails = values.emails.filter(d => !!d);
133− if (uniq(emails).length !== emails.length) {
134− return { [FORM_ERROR]: 'Duplicate email.' };
138+ const validateEmail = useCallback(
139+ async email => {
140+ // check individual email validity
141+ const error = await emailFormatValidator(email);
142+ if (error) {
143+ return error;
135144 }
136145
137− let errors = {};
138− for (let i = 0; i < values.emails.length; i++) {
139− const name = `emails[${i}]`;
140− const email = values.emails[i];
141−
142− // check individual email validity
143− // TODO: validate each email individually using similar pattern from form/Inputs
144− // and then reutrn that
145− const error = await emailFormatValidator(email);
146− if (error) {
147− errors[name] = error;
148− continue;
149− }
150−
151− const hasReceived = await getHasReceived(email);
152− if (hasReceived) {
153− errors[name] = HAS_RECEIVED_TEXT;
154− continue;
155− }
146+ const hasReceived = await getHasReceived(email);
147+ if (hasReceived) {
148+ return HAS_RECEIVED_TEXT;
156149 }
157−
158− console.log(errors);
159− return errors;
160150 },
161151 [getHasReceived]
162152 );
163153
154+ const validateEmails = useCallback(
155+ async emails => await Promise.all(emails.map(validateEmail)),
156+ [validateEmail]
157+ );
158+
159+ const validateForm = useCallback(async (values, errors) => {
160+ if (hasErrors(errors)) {
161+ return errors;
162+ }
163+
164+ // check for email uniqenesss
165+ const emails = values.emails.filter(d => !!d);
166+ if (uniq(emails).length !== emails.length) {
167+ return { [FORM_ERROR]: 'Duplicate email.' };
168+ }
169+ }, []);
170+
171+ const validate = useMemo(
172+ () => composeValidator({ emails: validateEmails }, validateForm),
173+ [validateEmails, validateForm]
174+ );
175+
164176 // progress is [0, .length] of invites or receipts, as we're generating them
165177 const progress = isGenerating
166178 ? Object.keys(invites).length
@@ -179,7 +191,6 @@ export default function InviteEmail() {
179191
180192 const generateInvites = useCallback(
181193 async values => {
182− debugger;
183194 const _contracts = contracts.getOrElse(null);
184195 const _web3 = web3.getOrElse(null);
185196 const _wallet = wallet.getOrElse(null);
@@ -215,6 +226,7 @@ export default function InviteEmail() {
215226 for (let i = 0; i < values.emails.length; i++) {
216227 try {
217228 const email = values.emails[i];
229+ const planet = planets[i];
218230
219231 const { ticket, owner } = await wg.generateTemporaryTicketAndWallet(
220232 MIN_PLANET
@@ -224,40 +236,40 @@ export default function InviteEmail() {
224236 const inviteTx = azimuth.delegatedSending.sendPoint(
225237 _contracts,
226238 point,
227− planets[i],
239+ planet,
228240 owner.keys.address
229241 );
242+
230243 const signedTx = await signTransaction({
231244 wallet: _wallet,
232245 walletType,
233246 walletHdPath,
234247 networkType,
248+ chainId,
249+ nonce: nonce + i,
235250 // TODO: ^ make a useTransactionSigner to encapsulate this logic
236251 txn: inviteTx,
237252 gasPrice: DEFAULT_GAS_PRICE_GWEI.toString(),
238253 gasLimit: GAS_LIMIT.toString(),
239− nonce: nonce + i,
240− chainId,
241254 });
255+
242256 const rawTx = hexify(signedTx.serialize());
243257
244258 addInvite({ [email]: { email, ticket, signedTx, rawTx } });
245259 } catch (error) {
246260 console.error(error);
247261 errorCount++;
248− return {
249− [FORM_ERROR]: `Wallet Error: unable to generate invite wallets.`,
250− };
251262 }
252263 }
253264
254265 if (errorCount > 0) {
255− throw new Error(
256− `There ${pluralize(errorCount, 'was', 'were')} ${pluralize(
266+ return {
267+ [FORM_ERROR]: `There ${pluralize(
257268 errorCount,
258− 'error'
259− )} while generating wallets.`
260− );
269+ 'was',
270+ 'were'
271+ )} ${pluralize(errorCount, 'error')} while generating wallets.`,
272+ };
261273 }
262274 },
263275 [
@@ -275,6 +287,7 @@ export default function InviteEmail() {
275287 );
276288
277289 const sendInvites = useCallback(async () => {
290+ const emails = cachedEmails.current;
278291 const _web3 = web3.getOrElse(null);
279292 const _wallet = wallet.getOrElse(null);
280293 if (!_web3 || !_wallet) {
@@ -286,8 +299,8 @@ export default function InviteEmail() {
286299 _web3,
287300 point,
288301 _wallet.address,
289− INVITE_COST * cachedEmails.current.length,
290− cachedEmails.current.map(email => invites[email].rawTx),
302+ INVITE_COST * emails.length,
303+ emails.map(email => invites[email].rawTx),
291304 (address, minBalance, balance) =>
292305 setNeedFunds({ address, minBalance, balance }),
293306 () => setNeedFunds(undefined)
@@ -298,7 +311,7 @@ export default function InviteEmail() {
298311
299312 let unsentInvites = [];
300313 let orphanedInvites = [];
301− const txAndMailings = cachedEmails.current.map(async email => {
314+ const txAndMailings = emails.map(async email => {
302315 const invite = invites[email];
303316 try {
304317 const txHash = await sendSignedTransaction(
@@ -356,9 +369,12 @@ export default function InviteEmail() {
356369
357370 const onSubmit = useCallback(
358371 async values => {
359− setStatus(STATUS.GENERATING);
360− await generateInvites(values);
361372 cachedEmails.current = values.emails;
373+ setStatus(STATUS.GENERATING);
374+ const errors = await generateInvites(values);
375+ if (errors) {
376+ return errors;
377+ }
362378 setStatus(STATUS.CAN_SEND);
363379 },
364380 [generateInvites]
@@ -372,7 +388,7 @@ export default function InviteEmail() {
372388 setStatus(STATUS.SUCCESS);
373389 } catch (error) {
374390 console.error(error);
375− setGeneralError(error);
391+ setGeneralError(error.message);
376392 setStatus(STATUS.FAILURE);
377393 }
378394 }, [sendInvites]);
@@ -407,7 +423,7 @@ export default function InviteEmail() {
407423 </IconButton>
408424 </Grid.Item>
409425
410− {isDone && (
426+ {isDone ? (
411427 <>
412428 <Grid.Item as={Text} className="f5" full>
413429 <Highlighted>
@@ -419,13 +435,13 @@ export default function InviteEmail() {
419435
420436 {fields.map(name => (
421437 <Grid.Item as={HelpText} key={name} full>
422− <Field name={name}>{({ value }) => value}</Field>
438+ <Field name={name}>
439+ {({ input: { value } }) => value}
440+ </Field>
423441 </Grid.Item>
424442 ))}
425443 </>
426− )}
427−
428− {!isDone && (
444+ ) : (
429445 <>
430446 {fields.map((name, i) => {
431447 const isFirst = i === 0;
@@ -471,26 +487,28 @@ export default function InviteEmail() {
471487 );
472488 })}
473489
474− {canInput && (
490+ {canInput ? (
475491 <Grid.Item
476492 full
477493 as={SubmitButton}
478− handleSubmit={handleSubmit}>
494+ handleSubmit={handleSubmit}
495+ accessory={`${visualProgress}/${fields.length}`}>
496+ {buttonText(status, fields.length)}
497+ </Grid.Item>
498+ ) : (
499+ <Grid.Item
500+ full
501+ as={LoadableButton}
502+ className="mt4"
503+ disabled={!canSend}
504+ accessory={`${visualProgress}/${fields.length}`}
505+ onClick={doSend}
506+ success={canSend}
507+ solid>
479508 {buttonText(status, fields.length)}
480509 </Grid.Item>
481510 )}
482511
483− <Grid.Item
484− full
485− as={LoadableButton}
486− disabled={!(canInput || canSend || valid)}
487− accessory={`${visualProgress}/${fields.length}`}
488− onClick={doSend}
489− success={canSend}
490− solid>
491− {buttonText(status, fields.length)}
492− </Grid.Item>
493−
494512 {needFunds && (
495513 <Grid.Item full>
496514 <Highlighted warning>
@@ -505,8 +523,8 @@ export default function InviteEmail() {
505523 <Grid.Item full as={FormError} />
506524
507525 {generalError && (
508− <Grid.Item full>
509− <ErrorText>{generalError.message.toString()}</ErrorText>
526+ <Grid.Item full as={ErrorText}>
527+ {generalError}
510528 </Grid.Item>
511529 )}
512530 </>