@@ -29,7 +29,6 @@ import * as tank from 'lib/tank';
2929import { useLocalRouter } from 'lib/LocalRouter';
3030
3131import MiniBackButton from 'components/MiniBackButton';
32−import useInvites from 'lib/useInvites';
3332import { usePointCursor } from 'store/pointCursor';
3433import LoadableButton from 'components/LoadableButton';
3534import useArray from 'lib/useArray';
@@ -49,6 +48,8 @@ const GAS_PRICE_GWEI = 20; // we pay the premium for faster ux
4948const GAS_LIMIT = 400000;
5049const INVITE_COST = toWei((GAS_PRICE_GWEI * GAS_LIMIT).toString(), 'gwei');
5150
51+const kHasReceivedText = 'This email has already received an invite.';
52+
5253const STATUS = {
5354 INPUT: 'INPUT',
5455 GENERATING: 'GENERATING',
@@ -104,22 +105,23 @@ const buildAccessoryFor = (dones, errors) => name => (
104105);
105106
106107// TODO: test with tank, successful txs
107−// TODO: put accessory inside of input
108108export default function InviteEmail() {
109109 // TODO: resumption after error?
110110 const { pop } = useLocalRouter();
111111 const { contracts, web3, networkType } = useNetwork();
112112 const { wallet, walletType, walletHdPath } = useWallet();
113− const { syncInvites } = usePointCache();
113+ const { syncInvites, getInvites } = usePointCache();
114114 const { pointCursor } = usePointCursor();
115115 const point = need.pointCursor(pointCursor);
116+ const { getHasRecieved, syncHasReceivedForEmail, sendMail } = useMailer();
116117
117− const { availableInvites } = useInvites(point);
118+ const { availableInvites } = getInvites(point);
118119 const maxInvitesToSend = availableInvites.matchWith({
119120 Nothing: () => 0,
120121 Just: p => p.value,
121122 });
122123
124+ // manage the array of input configs
123125 const [
124126 inputConfigs,
125127 { append: appendInput, removeAt: removeInputAt },
@@ -128,16 +130,18 @@ export default function InviteEmail() {
128130 buildInputConfig
129131 );
130132
131− const { getHasRecieved, syncHasReceivedForEmail, sendMail } = useMailer();
133+ // manage per-input state
132134 const [hovered, setHovered] = useSetState();
133135 const [invites, addInvite, clearInvites] = useSetState();
134136 const [receipts, addReceipt, clearReceipts] = useSetState();
135137 const [errors, addError, clearErrors] = useSetState();
136138
139+ // manage general state that affects the whole form
137140 const [status, setStatus] = useState(STATUS.INPUT);
138141 const [needFunds, setNeedFunds] = useState(null);
139142 const [generalError, setGeneralError] = useState(null);
140143
144+ // derive booleans from status
141145 const canInput = status === STATUS.INPUT;
142146 const canSend = status === STATUS.CAN_SEND;
143147 const isGenerating = status === STATUS.GENERATING;
@@ -145,28 +149,32 @@ export default function InviteEmail() {
145149 const isFailed = status === STATUS.FAILURE;
146150 const isDone = status === STATUS.SUCCESS;
147151
152+ // add disabled, error info to input configs
148153 const dynamicConfigs = useMemo(
149154 () =>
150155 inputConfigs.map(config => {
151156 config.disabled = !canInput;
152157 const hasReceivedError = getHasRecieved(config.name).matchWith({
153158 Nothing: () => null, // loading
154− Just: p => p.value && 'This email has already received an invite.',
159+ Just: p => p.value && kHasReceivedText,
155160 });
156161 config.error = hasReceivedError || errors[config.name];
157162 return config;
158163 }),
159164 [inputConfigs, errors, canInput, getHasRecieved]
160165 );
161− const { inputs, pass } = useForm(dynamicConfigs);
162− const emails = useMemo(() => inputs.map(i => i.data).filter(d => !!d), [
163− inputs,
164− ]);
165166
166− const canAddInvite = canInput && inputs.length < maxInvitesToSend;
167+ // construct the state of the set of inputs we're rendering below
168+ const { inputs, pass } = useForm(dynamicConfigs);
169+ // did all of the inputs pass inspection (and there are no general errors)?
167170 const allPass = pass && !generalError;
168− const canGenerate = allPass && status === STATUS.INPUT;
171+ // the form is submittable iff passing and input is allowed
172+ const canGenerate = allPass && canInput;
173+ // additional inputs can be added iff input is allowed and we have enough
174+ // invites to send
175+ const canAddInvite = canInput && inputConfigs.length < maxInvitesToSend;
169176
177+ // progress is [0, .length] of invites or receipts, as we're generating them
170178 const progress = isGenerating
171179 ? Object.keys(invites).length
172180 : isSending
@@ -174,6 +182,7 @@ export default function InviteEmail() {
174182 : null;
175183 const visualProgress = progress === null ? '-' : `${progress + 1}`;
176184
185+ // build a builder for the accessory to the input, depending on status
177186 const accessoryFor = (() => {
178187 if (isGenerating || canSend) return buildAccessoryFor(invites, errors);
179188 if (isSending) return buildAccessoryFor(receipts, errors);
@@ -355,49 +364,38 @@ export default function InviteEmail() {
355364 sendMail,
356365 ]);
357366
358− const onClickGenerate = useCallback(async () => {
367+ const onClick = useCallback(async () => {
359368 setGeneralError(null);
360− setStatus(STATUS.GENERATING);
361369 try {
362− await generateInvites();
363− setStatus(STATUS.CAN_SEND);
364− } catch (error) {
365− console.error(error);
366− setGeneralError(error);
367− setStatus(STATUS.FAILURE);
368− }
369− }, [setStatus, setGeneralError, generateInvites]);
370−
371− const onClickSend = useCallback(async () => {
372− setGeneralError(null);
373− setStatus(STATUS.SENDING);
374− try {
375− await sendInvites();
376− setStatus(STATUS.SUCCESS);
370+ if (canGenerate) {
371+ setStatus(STATUS.GENERATING);
372+ await generateInvites();
373+ setStatus(STATUS.CAN_SEND);
374+ } else if (canSend) {
375+ setStatus(STATUS.SENDING);
376+ await sendInvites();
377+ setStatus(STATUS.SUCCESS);
378+ }
377379 } catch (error) {
378380 console.error(error);
379381 setGeneralError(error);
380382 setStatus(STATUS.FAILURE);
381383 }
382− }, [setStatus, sendInvites, setGeneralError]);
383−
384− const onClick = useCallback(() => {
385− if (canGenerate) {
386− onClickGenerate();
387− } else if (canSend) {
388− onClickSend();
389− }
390− }, [canGenerate, canSend, onClickGenerate, onClickSend]);
384+ }, [canGenerate, canSend, generateInvites, sendInvites]);
391385
386+ // when inputs update, check to see if any of them are duplicates
392387 useEffect(() => {
388+ // compute the list of valid emails
389+ const emails = inputs.map(i => i.data).filter(d => !!d);
393390 if (uniq(emails).length !== emails.length) {
394391 setGeneralError(new Error(`Duplicate email.`));
395392 } else {
396393 setGeneralError(null);
397394 }
398− }, [emails]);
395+ }, [inputs]);
399396
400397 // when we transition to done, sync invites because we just sent some
398+ // and therefore know that state has changed
401399 useEffect(() => {
402400 if (isDone) {
403401 syncInvites(point);