@@ -1,4 +1,10 @@
1−import React, { useCallback, useEffect, useRef, useState } from 'react';
1+import React, {
2import cn from 'classnames';
8import cn from 'classnames';
3import * as ob from 'urbit-ob';
9import * as ob from 'urbit-ob';
4import * as azimuth from 'azimuth-js';
10import * as azimuth from 'azimuth-js';
@@ -39,7 +45,12 @@ import LoadableButton from 'components/LoadableButton';
39import Highlighted from 'components/Highlighted';
45import Highlighted from 'components/Highlighted';
40import BridgeForm from 'form/BridgeForm';
46import BridgeForm from 'form/BridgeForm';
41import { Field } from 'react-final-form';
47import { Field } from 'react-final-form';
42−import { EmailInput, buildEmailValidator } from 'form/Inputs';
48+import { EmailInput } from 'form/Inputs';
53+} from 'form/validators';
43import { FORM_ERROR } from 'final-form';
54import { FORM_ERROR } from 'final-form';
44import SubmitButton from 'form/SubmitButton';
55import SubmitButton from 'form/SubmitButton';
45import FormError from 'form/FormError';
56import FormError from 'form/FormError';
@@ -89,9 +100,7 @@ const buildAccessoryFor = (dones, errors) => name => {
90const emailFormatValidator = buildEmailValidator();
101const emailFormatValidator = buildEmailValidator();
92−// TODO: test with tank, successful txs
93export default function InviteEmail() {
103export default function InviteEmail() {
94− // TODO: resumption after error?
95 const { contracts, web3, networkType } = useNetwork();
104 const { contracts, web3, networkType } = useNetwork();
96 const { wallet, walletType, walletHdPath } = useWallet();
105 const { wallet, walletType, walletHdPath } = useWallet();
97 const { syncInvites, getInvites } = usePointCache();
106 const { syncInvites, getInvites } = usePointCache();
@@ -126,41 +135,44 @@ export default function InviteEmail() {
126 const isFailed = status === STATUS.FAILURE;
135 const isFailed = status === STATUS.FAILURE;
127 const isDone = status === STATUS.SUCCESS;
136 const isDone = status === STATUS.SUCCESS;
129− const validate = useCallback(
138+ const validateEmail = useCallback(
130− async values => {
139+ async email => {
131− // compute the list of valid emails
140+ // check individual email validity
132− const emails = values.emails.filter(d => !!d);
141+ const error = await emailFormatValidator(email);
133− if (uniq(emails).length !== emails.length) {
142+ if (error) {
134− return { [FORM_ERROR]: 'Duplicate email.' };
143+ return error;
138− for (let i = 0; i < values.emails.length; i++) {
139− const name = `emails[${i}]`;
140− const email = values.emails[i];
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);
147− errors[name] = error;
151− const hasReceived = await getHasReceived(email);
146+ const hasReceived = await getHasReceived(email);
152− if (hasReceived) {
147+ if (hasReceived) {
153− errors[name] = HAS_RECEIVED_TEXT;
148+ return HAS_RECEIVED_TEXT;
158− console.log(errors);
161 [getHasReceived]
151 [getHasReceived]
154+ const validateEmails = useCallback(
155+ async emails => await Promise.all(emails.map(validateEmail)),
159+ const validateForm = useCallback(async (values, errors) => {
160+ if (hasErrors(errors)) {
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.' };
171+ const validate = useMemo(
172+ () => composeValidator({ emails: validateEmails }, validateForm),
173+ [validateEmails, validateForm]
164 // progress is [0, .length] of invites or receipts, as we're generating them
176 // progress is [0, .length] of invites or receipts, as we're generating them
165 const progress = isGenerating
177 const progress = isGenerating
166 ? Object.keys(invites).length
178 ? Object.keys(invites).length
@@ -179,7 +191,6 @@ export default function InviteEmail() {
180 const generateInvites = useCallback(
192 const generateInvites = useCallback(
181 async values => {
193 async values => {
183 const _contracts = contracts.getOrElse(null);
194 const _contracts = contracts.getOrElse(null);
184 const _web3 = web3.getOrElse(null);
195 const _web3 = web3.getOrElse(null);
185 const _wallet = wallet.getOrElse(null);
196 const _wallet = wallet.getOrElse(null);
@@ -215,6 +226,7 @@ export default function InviteEmail() {
215 for (let i = 0; i < values.emails.length; i++) {
226 for (let i = 0; i < values.emails.length; i++) {
217 const email = values.emails[i];
228 const email = values.emails[i];
229+ const planet = planets[i];
219 const { ticket, owner } = await wg.generateTemporaryTicketAndWallet(
231 const { ticket, owner } = await wg.generateTemporaryTicketAndWallet(
220 MIN_PLANET
232 MIN_PLANET
@@ -224,40 +236,40 @@ export default function InviteEmail() {
224 const inviteTx = azimuth.delegatedSending.sendPoint(
236 const inviteTx = azimuth.delegatedSending.sendPoint(
225 _contracts,
237 _contracts,
227− planets[i],
239+ planet,
228 owner.keys.address
240 owner.keys.address
230 const signedTx = await signTransaction({
243 const signedTx = await signTransaction({
231 wallet: _wallet,
244 wallet: _wallet,
232 walletType,
245 walletType,
233 walletHdPath,
246 walletHdPath,
234 networkType,
247 networkType,
235 // TODO: ^ make a useTransactionSigner to encapsulate this logic
250 // TODO: ^ make a useTransactionSigner to encapsulate this logic
236 txn: inviteTx,
251 txn: inviteTx,
237 gasPrice: DEFAULT_GAS_PRICE_GWEI.toString(),
252 gasPrice: DEFAULT_GAS_PRICE_GWEI.toString(),
238 gasLimit: GAS_LIMIT.toString(),
253 gasLimit: GAS_LIMIT.toString(),
242 const rawTx = hexify(signedTx.serialize());
256 const rawTx = hexify(signedTx.serialize());
244 addInvite({ [email]: { email, ticket, signedTx, rawTx } });
258 addInvite({ [email]: { email, ticket, signedTx, rawTx } });
245 } catch (error) {
259 } catch (error) {
246 console.error(error);
260 console.error(error);
247 errorCount++;
261 errorCount++;
249− [FORM_ERROR]: `Wallet Error: unable to generate invite wallets.`,
254 if (errorCount > 0) {
265 if (errorCount > 0) {
255− throw new Error(
266+ return {
256− `There ${pluralize(errorCount, 'was', 'were')} ${pluralize(
267+ [FORM_ERROR]: `There ${pluralize(
257 errorCount,
268 errorCount,
259− )} while generating wallets.`
271+ )} ${pluralize(errorCount, 'error')} while generating wallets.`,
@@ -275,6 +287,7 @@ export default function InviteEmail() {
277 const sendInvites = useCallback(async () => {
289 const sendInvites = useCallback(async () => {
290+ const emails = cachedEmails.current;
278 const _web3 = web3.getOrElse(null);
291 const _web3 = web3.getOrElse(null);
279 const _wallet = wallet.getOrElse(null);
292 const _wallet = wallet.getOrElse(null);
280 if (!_web3 || !_wallet) {
293 if (!_web3 || !_wallet) {
@@ -286,8 +299,8 @@ export default function InviteEmail() {
288 _wallet.address,
301 _wallet.address,
289− INVITE_COST * cachedEmails.current.length,
302+ INVITE_COST * emails.length,
290− cachedEmails.current.map(email => invites[email].rawTx),
303+ emails.map(email => invites[email].rawTx),
291 (address, minBalance, balance) =>
304 (address, minBalance, balance) =>
292 setNeedFunds({ address, minBalance, balance }),
305 setNeedFunds({ address, minBalance, balance }),
293 () => setNeedFunds(undefined)
306 () => setNeedFunds(undefined)
@@ -298,7 +311,7 @@ export default function InviteEmail() {
299 let unsentInvites = [];
312 let unsentInvites = [];
300 let orphanedInvites = [];
313 let orphanedInvites = [];
301− const txAndMailings = cachedEmails.current.map(async email => {
314+ const txAndMailings = emails.map(async email => {
302 const invite = invites[email];
315 const invite = invites[email];
304 const txHash = await sendSignedTransaction(
317 const txHash = await sendSignedTransaction(
@@ -356,9 +369,12 @@ export default function InviteEmail() {
357 const onSubmit = useCallback(
370 const onSubmit = useCallback(
358 async values => {
371 async values => {
359− setStatus(STATUS.GENERATING);
360− await generateInvites(values);
361 cachedEmails.current = values.emails;
372 cachedEmails.current = values.emails;
373+ setStatus(STATUS.GENERATING);
374+ const errors = await generateInvites(values);
362 setStatus(STATUS.CAN_SEND);
378 setStatus(STATUS.CAN_SEND);
364 [generateInvites]
380 [generateInvites]
@@ -372,7 +388,7 @@ export default function InviteEmail() {
372 setStatus(STATUS.SUCCESS);
388 setStatus(STATUS.SUCCESS);
373 } catch (error) {
389 } catch (error) {
374 console.error(error);
390 console.error(error);
375− setGeneralError(error);
391+ setGeneralError(error.message);
376 setStatus(STATUS.FAILURE);
392 setStatus(STATUS.FAILURE);
378 }, [sendInvites]);
394 }, [sendInvites]);
@@ -407,7 +423,7 @@ export default function InviteEmail() {
407 </IconButton>
423 </IconButton>
408 </Grid.Item>
424 </Grid.Item>
410− {isDone && (
426+ {isDone ? (
412 <Grid.Item as={Text} className="f5" full>
428 <Grid.Item as={Text} className="f5" full>
413 <Highlighted>
429 <Highlighted>
@@ -419,13 +435,13 @@ export default function InviteEmail() {
420 {fields.map(name => (
436 {fields.map(name => (
421 <Grid.Item as={HelpText} key={name} full>
437 <Grid.Item as={HelpText} key={name} full>
422− <Field name={name}>{({ value }) => value}</Field>
438+ <Field name={name}>
439+ {({ input: { value } }) => value}
423 </Grid.Item>
441 </Grid.Item>
430 {fields.map((name, i) => {
446 {fields.map((name, i) => {
431 const isFirst = i === 0;
447 const isFirst = i === 0;
@@ -471,26 +487,28 @@ export default function InviteEmail() {
474− {canInput && (
490+ {canInput ? (
475 <Grid.Item
491 <Grid.Item
477 as={SubmitButton}
493 as={SubmitButton}
478− handleSubmit={handleSubmit}>
494+ handleSubmit={handleSubmit}
495+ accessory={`${visualProgress}/${fields.length}`}>
496+ {buttonText(status, fields.length)}
504+ accessory={`${visualProgress}/${fields.length}`}
479 {buttonText(status, fields.length)}
508 {buttonText(status, fields.length)}
480 </Grid.Item>
509 </Grid.Item>
486− disabled={!(canInput || canSend || valid)}
487− accessory={`${visualProgress}/${fields.length}`}
491− {buttonText(status, fields.length)}
494 {needFunds && (
512 {needFunds && (
495 <Grid.Item full>
513 <Grid.Item full>
496 <Highlighted warning>
514 <Highlighted warning>
@@ -505,8 +523,8 @@ export default function InviteEmail() {
505 <Grid.Item full as={FormError} />
523 <Grid.Item full as={FormError} />
507 {generalError && (
525 {generalError && (
508− <Grid.Item full>
526+ <Grid.Item full as={ErrorText}>
509− <ErrorText>{generalError.message.toString()}</ErrorText>
527+ {generalError}
510 </Grid.Item>
528 </Grid.Item>