@@ -1,15 +1,14 @@
11import React, { useMemo, useCallback, useState, useEffect } from 'react';
22import { Grid, ErrorText } from 'indigo-react';
3−import { get } from 'lodash';
3+import { get, uniq, zip, compact, isEqual } from 'lodash';
44import * as azimuth from 'azimuth-js';
55import { FORM_ERROR } from 'final-form';
66import { toWei, toBN } from 'web3-utils';
7+import * as ob from 'urbit-ob';
78
89import * as need from 'lib/need';
910import * as wg from 'lib/walletgen';
1011import * as tank from 'lib/tank';
11−// import useMailer from 'lib/useMailer';
12−import timeout from 'lib/timeout';
1312import useCopiable from 'lib/useCopiable';
1413
1514import Tabs from 'components/Tabs';
@@ -22,7 +21,6 @@ import { useWallet } from 'store/wallet';
2221import { usePointCursor } from 'store/pointCursor';
2322import { usePointCache } from 'store/pointCache';
2423
25−// import { } from ''
2624import {
2725 signTransaction,
2826 sendSignedTransaction,
@@ -38,11 +36,13 @@ import FormError from 'form/FormError';
3836import SubmitButton from 'form/SubmitButton';
3937import BridgeForm from 'form/BridgeForm';
4038import {
41− buildEmailArrayValidator,
4239 composeValidator,
4340 hasErrors,
41+ buildEmailValidator,
42+ buildArrayValidator,
4443} from 'form/validators';
4544import { WARNING } from 'form/helpers';
45+import useMailer from 'lib/useMailer';
4646
4747const GAS_LIMIT = GAS_LIMITS.GIFT_PLANET;
4848
@@ -53,6 +53,8 @@ const STATUS = {
5353 FAILURE: 'FAILURE',
5454};
5555
56+const HAS_RECEIVED_TEXT = 'This email has already received an invite.';
57+
5658const useInviter = sendInvites => {
5759 const { contracts, web3, networkType } = useNetwork();
5860 const { wallet, walletType, walletHdPath } = useWallet();
@@ -80,6 +82,7 @@ const useInviter = sendInvites => {
8082 setInvites([]);
8183 setTxStatus(STATUS.INPUT);
8284 }, [setProgress, setInvites, setTxStatus]);
85+
8386 const generateInvites = useCallback(
8487 async numInvites => {
8588 const _contracts = contracts.getOrElse(null);
@@ -102,7 +105,6 @@ const useInviter = sendInvites => {
102105 setTxStatus(STATUS.SENDING);
103106 // account for the race condition where invites got used up while we were
104107 // composing our target list
105− console.log('a');
106108 if (planets.length < numInvites) {
107109 // resync invites to the cache, since they're out of date
108110 syncInvites(point);
@@ -110,9 +112,11 @@ const useInviter = sendInvites => {
110112 setTxStatus(STATUS.FAILURE);
111113
112114 return {
113− [FORM_ERROR]:
114− `Can currently only send ${planets.length} invites. ` +
115− `Please remove invites until you are within the limit.`,
115+ errors: {
116+ [FORM_ERROR]:
117+ `Can currently only send ${planets.length} invites. ` +
118+ `Please remove invites until you are within the limit.`,
119+ },
116120 };
117121 }
118122
@@ -155,7 +159,9 @@ const useInviter = sendInvites => {
155159 } catch (error) {
156160 console.error(error);
157161 return {
158− [WARNING]: `There was an error while generating wallets.`,
162+ errors: {
163+ [WARNING]: `There was an error while generating wallets.`,
164+ },
159165 };
160166 }
161167 }
@@ -201,17 +207,24 @@ const useInviter = sendInvites => {
201207 console.log(confirmedInvites);
202208
203209 if (unsentInvites.length > 0) {
204− return { [FORM_ERROR]: unsentInvites };
210+ return { errors: { [FORM_ERROR]: unsentInvites } };
205211 }
206212
207213 if (errorCount > 0) {
208214 return {
209− [WARNING]: `There ${pluralize(errorCount, 'was', 'were')} ${pluralize(
210− errorCount,
211− 'error'
212− )} while generating wallets. You can still send the invites that generated correctly.`,
215+ errors: {
216+ [WARNING]: `There ${pluralize(
217+ errorCount,
218+ 'was',
219+ 'were'
220+ )} ${pluralize(
221+ errorCount,
222+ 'error'
223+ )} while generating wallets. You can still send the invites that generated correctly.`,
224+ },
213225 };
214226 }
227+ return { invites: confirmedInvites };
215228 },
216229 [
217230 contracts,
@@ -238,14 +251,9 @@ const useInviter = sendInvites => {
238251};
239252
240253const InviteMail = () => {
241− const {
242− progress,
243− txStatus,
244− needFunds,
245− invites,
246− generateInvites,
247− resetInvites,
248− } = useInviter();
254+ const { progress, txStatus, needFunds, generateInvites } = useInviter();
255+
256+ const { sendMail, getHasReceived } = useMailer();
249257
250258 const [status, setStatus] = useState(STATUS.INPUT);
251259
@@ -253,31 +261,62 @@ const InviteMail = () => {
253261 const canInput = status === STATUS.INPUT;
254262 const isSending = txStatus === STATUS.SENDING || status === STATUS.SENDING;
255263 const isDone = status === STATUS.SUCCESS;
264+ const isFailed = status === STATUS.FAILED;
265+
266+ const { pointCursor } = usePointCursor();
267+ const point = need.point(pointCursor);
268+
269+ const sendInvites = useCallback(
270+ async (emails, invites) => {
271+ const mailingErrors = compact(
272+ await Promise.all(
273+ zip(emails, invites).map(([email, invite]) =>
274+ sendMail(email, invite.ticket, ob.patp(point), '', invite.rawTx)
275+ .then(() => null)
276+ .catch(
277+ () => `Sending ticket ${invite.ticket} to ${email} failed.`
278+ )
279+ )
280+ )
281+ ).join(', ');
282+ if (mailingErrors.length) {
283+ return { [FORM_ERROR]: mailingErrors };
284+ }
285+ },
286+ [point, sendMail]
287+ );
288+
256289 const onSubmit = useCallback(
257290 async values => {
258291 const emailCount = values.emails.length;
259292 setCount(emailCount);
260293 setStatus(STATUS.SENDING);
261− const errors = await generateInvites(emailCount);
294+ const { errors, invites } = await generateInvites(emailCount);
262295 if (errors) {
263296 setStatus(STATUS.FAILED);
264297 return errors;
265298 }
299+ const mailErrors = await sendInvites(values.emails, invites);
300+ console.log(mailErrors);
301+ if (mailErrors) {
302+ console.log(mailErrors);
303+ setStatus(STATUS.FAILED);
304+ return mailErrors;
305+ }
306+ setStatus(STATUS.SUCCESS);
266307 },
267− [generateInvites, setCount, setStatus]
308+ [generateInvites, setCount, setStatus, sendInvites]
268309 );
269310
270− useEffect(() => {
271− async function processInvites() {
272− if (invites.length !== 0 && txStatus === STATUS.SUCCESS) {
273− await timeout(500);
274−
275− resetInvites();
276− setStatus(STATUS.SUCCESS);
311+ const validateHasReceived = useCallback(
312+ async email => {
313+ const hasReceived = await getHasReceived(email);
314+ if (hasReceived) {
315+ return HAS_RECEIVED_TEXT;
277316 }
278− }
279− processInvites();
280− }, [invites, setStatus, status, txStatus, resetInvites]);
317+ },
318+ [getHasReceived]
319+ );
281320
282321 const buttonText = () => {
283322 switch (status) {
@@ -324,22 +363,51 @@ const InviteMail = () => {
324363 </Grid.Item>
325364 );
326365 }
366+ if (isFailed) {
367+ return (
368+ <Grid.Item full className="mt4 p3 bg-red3 white f5">
369+ {buttonText(count)}
370+ </Grid.Item>
371+ );
372+ }
327373
328374 return null;
329375 };
330376
377+ const validateForm = useCallback((values, errors) => {
378+ if (hasErrors(errors)) {
379+ return errors;
380+ }
381+
382+ // check for email uniqenesss
383+ const emails = values.emails.filter(d => !!d);
384+ if (uniq(emails).length !== emails.length) {
385+ return { [FORM_ERROR]: 'Duplicate email.' };
386+ }
387+ }, []);
388+
331389 const validate = useMemo(
332− () => composeValidator({ emails: buildEmailArrayValidator() }),
333− []
390+ () =>
391+ composeValidator(
392+ {
393+ emails: buildArrayValidator(buildEmailValidator(validateHasReceived)),
394+ },
395+ validateForm
396+ ),
397+ [validateHasReceived, validateForm]
334398 );
399+
400+ const initialValues = { emails: [] };
335401 return (
336402 <Grid.Item full as={Grid}>
337403 <BridgeForm
338404 validate={validate}
339− initialValues={{ emails: [] }}
405+ initialValues={initialValues}
406+ initialValuesEqual={isEqual}
340407 onSubmit={onSubmit}>
341408 {({ handleSubmit, values }) => (
342409 <>
410+ {console.log(values) && null}
343411 <Grid.Item full as={EmailChipInput} name="emails" className="mt4" />
344412 {renderButton(handleSubmit)}
345413