@@ -1,15 +1,14 @@
1import React, { useMemo, useCallback, useState, useEffect } from 'react';
1import React, { useMemo, useCallback, useState, useEffect } from 'react';
2import { Grid, ErrorText } from 'indigo-react';
2import { Grid, ErrorText } from 'indigo-react';
3−import { get } from 'lodash';
3+import { get, uniq, zip, compact, isEqual } from 'lodash';
4import * as azimuth from 'azimuth-js';
4import * as azimuth from 'azimuth-js';
5import { FORM_ERROR } from 'final-form';
5import { FORM_ERROR } from 'final-form';
6import { toWei, toBN } from 'web3-utils';
6import { toWei, toBN } from 'web3-utils';
7+import * as ob from 'urbit-ob';
8import * as need from 'lib/need';
9import * as need from 'lib/need';
9import * as wg from 'lib/walletgen';
10import * as wg from 'lib/walletgen';
10import * as tank from 'lib/tank';
11import * as tank from 'lib/tank';
11−// import useMailer from 'lib/useMailer';
12−import timeout from 'lib/timeout';
13import useCopiable from 'lib/useCopiable';
12import useCopiable from 'lib/useCopiable';
15import Tabs from 'components/Tabs';
14import Tabs from 'components/Tabs';
@@ -22,7 +21,6 @@ import { useWallet } from 'store/wallet';
22import { usePointCursor } from 'store/pointCursor';
21import { usePointCursor } from 'store/pointCursor';
23import { usePointCache } from 'store/pointCache';
22import { usePointCache } from 'store/pointCache';
27 signTransaction,
25 signTransaction,
28 sendSignedTransaction,
26 sendSignedTransaction,
@@ -38,11 +36,13 @@ import FormError from 'form/FormError';
38import SubmitButton from 'form/SubmitButton';
36import SubmitButton from 'form/SubmitButton';
39import BridgeForm from 'form/BridgeForm';
37import BridgeForm from 'form/BridgeForm';
41− buildEmailArrayValidator,
42 composeValidator,
39 composeValidator,
43 hasErrors,
40 hasErrors,
44} from 'form/validators';
43} from 'form/validators';
45import { WARNING } from 'form/helpers';
44import { WARNING } from 'form/helpers';
45+import useMailer from 'lib/useMailer';
47const GAS_LIMIT = GAS_LIMITS.GIFT_PLANET;
47const GAS_LIMIT = GAS_LIMITS.GIFT_PLANET;
@@ -53,6 +53,8 @@ const STATUS = {
53 FAILURE: 'FAILURE',
53 FAILURE: 'FAILURE',
56+const HAS_RECEIVED_TEXT = 'This email has already received an invite.';
56const useInviter = sendInvites => {
58const useInviter = sendInvites => {
57 const { contracts, web3, networkType } = useNetwork();
59 const { contracts, web3, networkType } = useNetwork();
58 const { wallet, walletType, walletHdPath } = useWallet();
60 const { wallet, walletType, walletHdPath } = useWallet();
@@ -80,6 +82,7 @@ const useInviter = sendInvites => {
80 setInvites([]);
82 setInvites([]);
81 setTxStatus(STATUS.INPUT);
83 setTxStatus(STATUS.INPUT);
82 }, [setProgress, setInvites, setTxStatus]);
84 }, [setProgress, setInvites, setTxStatus]);
83 const generateInvites = useCallback(
86 const generateInvites = useCallback(
84 async numInvites => {
87 async numInvites => {
85 const _contracts = contracts.getOrElse(null);
88 const _contracts = contracts.getOrElse(null);
@@ -102,7 +105,6 @@ const useInviter = sendInvites => {
102 setTxStatus(STATUS.SENDING);
105 setTxStatus(STATUS.SENDING);
103 // account for the race condition where invites got used up while we were
106 // account for the race condition where invites got used up while we were
104 // composing our target list
107 // composing our target list
106 if (planets.length < numInvites) {
108 if (planets.length < numInvites) {
107 // resync invites to the cache, since they're out of date
109 // resync invites to the cache, since they're out of date
108 syncInvites(point);
110 syncInvites(point);
@@ -110,9 +112,11 @@ const useInviter = sendInvites => {
110 setTxStatus(STATUS.FAILURE);
112 setTxStatus(STATUS.FAILURE);
113− [FORM_ERROR]:
116+ [FORM_ERROR]:
114− `Can currently only send ${planets.length} invites. ` +
117+ `Can currently only send ${planets.length} invites. ` +
115− `Please remove invites until you are within the limit.`,
118+ `Please remove invites until you are within the limit.`,
@@ -155,7 +159,9 @@ const useInviter = sendInvites => {
155 } catch (error) {
159 } catch (error) {
156 console.error(error);
160 console.error(error);
158− [WARNING]: `There was an error while generating wallets.`,
163+ [WARNING]: `There was an error while generating wallets.`,
@@ -201,17 +207,24 @@ const useInviter = sendInvites => {
201 console.log(confirmedInvites);
207 console.log(confirmedInvites);
203 if (unsentInvites.length > 0) {
209 if (unsentInvites.length > 0) {
204− return { [FORM_ERROR]: unsentInvites };
210+ return { errors: { [FORM_ERROR]: unsentInvites } };
207 if (errorCount > 0) {
213 if (errorCount > 0) {
209− [WARNING]: `There ${pluralize(errorCount, 'was', 'were')} ${pluralize(
216+ [WARNING]: `There ${pluralize(
210− errorCount,
221+ errorCount,
212− )} while generating wallets. You can still send the invites that generated correctly.`,
223+ )} while generating wallets. You can still send the invites that generated correctly.`,
227+ return { invites: confirmedInvites };
217 contracts,
230 contracts,
@@ -238,14 +251,9 @@ const useInviter = sendInvites => {
240const InviteMail = () => {
253const InviteMail = () => {
246− generateInvites,
254+ const { progress, txStatus, needFunds, generateInvites } = useInviter();
248− } = useInviter();
256+ const { sendMail, getHasReceived } = useMailer();
250 const [status, setStatus] = useState(STATUS.INPUT);
258 const [status, setStatus] = useState(STATUS.INPUT);
@@ -253,31 +261,62 @@ const InviteMail = () => {
253 const canInput = status === STATUS.INPUT;
261 const canInput = status === STATUS.INPUT;
254 const isSending = txStatus === STATUS.SENDING || status === STATUS.SENDING;
262 const isSending = txStatus === STATUS.SENDING || status === STATUS.SENDING;
255 const isDone = status === STATUS.SUCCESS;
263 const isDone = status === STATUS.SUCCESS;
264+ const isFailed = status === STATUS.FAILED;
266+ const { pointCursor } = usePointCursor();
267+ const point = need.point(pointCursor);
269+ const sendInvites = useCallback(
270+ async (emails, invites) => {
271+ const mailingErrors = compact(
273+ zip(emails, invites).map(([email, invite]) =>
274+ sendMail(email, invite.ticket, ob.patp(point), '', invite.rawTx)
277+ () => `Sending ticket ${invite.ticket} to ${email} failed.`
282+ if (mailingErrors.length) {
283+ return { [FORM_ERROR]: mailingErrors };
256 const onSubmit = useCallback(
289 const onSubmit = useCallback(
257 async values => {
290 async values => {
258 const emailCount = values.emails.length;
291 const emailCount = values.emails.length;
259 setCount(emailCount);
292 setCount(emailCount);
260 setStatus(STATUS.SENDING);
293 setStatus(STATUS.SENDING);
261− const errors = await generateInvites(emailCount);
294+ const { errors, invites } = await generateInvites(emailCount);
262 if (errors) {
295 if (errors) {
263 setStatus(STATUS.FAILED);
296 setStatus(STATUS.FAILED);
264 return errors;
297 return errors;
299+ const mailErrors = await sendInvites(values.emails, invites);
300+ console.log(mailErrors);
302+ console.log(mailErrors);
303+ setStatus(STATUS.FAILED);
306+ setStatus(STATUS.SUCCESS);
267− [generateInvites, setCount, setStatus]
308+ [generateInvites, setCount, setStatus, sendInvites]
270− useEffect(() => {
311+ const validateHasReceived = useCallback(
271− async function processInvites() {
312+ async email => {
272− if (invites.length !== 0 && txStatus === STATUS.SUCCESS) {
313+ const hasReceived = await getHasReceived(email);
273− await timeout(500);
314+ if (hasReceived) {
274−
315+ return HAS_RECEIVED_TEXT;
276− setStatus(STATUS.SUCCESS);
279− processInvites();
318+ [getHasReceived]
280− }, [invites, setStatus, status, txStatus, resetInvites]);
319+ );
282 const buttonText = () => {
321 const buttonText = () => {
283 switch (status) {
322 switch (status) {
@@ -324,22 +363,51 @@ const InviteMail = () => {
324 </Grid.Item>
363 </Grid.Item>
368+ <Grid.Item full className="mt4 p3 bg-red3 white f5">
328 return null;
374 return null;
377+ const validateForm = useCallback((values, errors) => {
378+ if (hasErrors(errors)) {
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.' };
331 const validate = useMemo(
389 const validate = useMemo(
332− () => composeValidator({ emails: buildEmailArrayValidator() }),
391+ composeValidator(
393+ emails: buildArrayValidator(buildEmailValidator(validateHasReceived)),
397+ [validateHasReceived, validateForm]
400+ const initialValues = { emails: [] };
336 <Grid.Item full as={Grid}>
402 <Grid.Item full as={Grid}>
337 <BridgeForm
403 <BridgeForm
338 validate={validate}
404 validate={validate}
339− initialValues={{ emails: [] }}
405+ initialValues={initialValues}
406+ initialValuesEqual={isEqual}
340 onSubmit={onSubmit}>
407 onSubmit={onSubmit}>
341 {({ handleSubmit, values }) => (
408 {({ handleSubmit, values }) => (
410+ {console.log(values) && null}
343 <Grid.Item full as={EmailChipInput} name="emails" className="mt4" />
411 <Grid.Item full as={EmailChipInput} name="emails" className="mt4" />
344 {renderButton(handleSubmit)}
412 {renderButton(handleSubmit)}