@@ -29,7 +29,6 @@ import * as tank from 'lib/tank';
29import { useLocalRouter } from 'lib/LocalRouter';
29import { useLocalRouter } from 'lib/LocalRouter';
31import MiniBackButton from 'components/MiniBackButton';
31import MiniBackButton from 'components/MiniBackButton';
32−import useInvites from 'lib/useInvites';
33import { usePointCursor } from 'store/pointCursor';
32import { usePointCursor } from 'store/pointCursor';
34import LoadableButton from 'components/LoadableButton';
33import LoadableButton from 'components/LoadableButton';
35import useArray from 'lib/useArray';
34import useArray from 'lib/useArray';
@@ -49,6 +48,8 @@ const GAS_PRICE_GWEI = 20; // we pay the premium for faster ux
49const GAS_LIMIT = 400000;
48const GAS_LIMIT = 400000;
50const INVITE_COST = toWei((GAS_PRICE_GWEI * GAS_LIMIT).toString(), 'gwei');
49const INVITE_COST = toWei((GAS_PRICE_GWEI * GAS_LIMIT).toString(), 'gwei');
51+const kHasReceivedText = 'This email has already received an invite.';
52const STATUS = {
53const STATUS = {
53 INPUT: 'INPUT',
54 INPUT: 'INPUT',
54 GENERATING: 'GENERATING',
55 GENERATING: 'GENERATING',
@@ -104,22 +105,23 @@ const buildAccessoryFor = (dones, errors) => name => (
106// TODO: test with tank, successful txs
107// TODO: test with tank, successful txs
107−// TODO: put accessory inside of input
108export default function InviteEmail() {
108export default function InviteEmail() {
109 // TODO: resumption after error?
109 // TODO: resumption after error?
110 const { pop } = useLocalRouter();
110 const { pop } = useLocalRouter();
111 const { contracts, web3, networkType } = useNetwork();
111 const { contracts, web3, networkType } = useNetwork();
112 const { wallet, walletType, walletHdPath } = useWallet();
112 const { wallet, walletType, walletHdPath } = useWallet();
113− const { syncInvites } = usePointCache();
113+ const { syncInvites, getInvites } = usePointCache();
114 const { pointCursor } = usePointCursor();
114 const { pointCursor } = usePointCursor();
115 const point = need.pointCursor(pointCursor);
115 const point = need.pointCursor(pointCursor);
116+ const { getHasRecieved, syncHasReceivedForEmail, sendMail } = useMailer();
117− const { availableInvites } = useInvites(point);
118+ const { availableInvites } = getInvites(point);
118 const maxInvitesToSend = availableInvites.matchWith({
119 const maxInvitesToSend = availableInvites.matchWith({
119 Nothing: () => 0,
120 Nothing: () => 0,
120 Just: p => p.value,
121 Just: p => p.value,
124+ // manage the array of input configs
124 inputConfigs,
126 inputConfigs,
125 { append: appendInput, removeAt: removeInputAt },
127 { append: appendInput, removeAt: removeInputAt },
@@ -128,16 +130,18 @@ export default function InviteEmail() {
128 buildInputConfig
130 buildInputConfig
131− const { getHasRecieved, syncHasReceivedForEmail, sendMail } = useMailer();
133+ // manage per-input state
132 const [hovered, setHovered] = useSetState();
134 const [hovered, setHovered] = useSetState();
133 const [invites, addInvite, clearInvites] = useSetState();
135 const [invites, addInvite, clearInvites] = useSetState();
134 const [receipts, addReceipt, clearReceipts] = useSetState();
136 const [receipts, addReceipt, clearReceipts] = useSetState();
135 const [errors, addError, clearErrors] = useSetState();
137 const [errors, addError, clearErrors] = useSetState();
139+ // manage general state that affects the whole form
137 const [status, setStatus] = useState(STATUS.INPUT);
140 const [status, setStatus] = useState(STATUS.INPUT);
138 const [needFunds, setNeedFunds] = useState(null);
141 const [needFunds, setNeedFunds] = useState(null);
139 const [generalError, setGeneralError] = useState(null);
142 const [generalError, setGeneralError] = useState(null);
144+ // derive booleans from status
141 const canInput = status === STATUS.INPUT;
145 const canInput = status === STATUS.INPUT;
142 const canSend = status === STATUS.CAN_SEND;
146 const canSend = status === STATUS.CAN_SEND;
143 const isGenerating = status === STATUS.GENERATING;
147 const isGenerating = status === STATUS.GENERATING;
@@ -145,28 +149,32 @@ export default function InviteEmail() {
145 const isFailed = status === STATUS.FAILURE;
149 const isFailed = status === STATUS.FAILURE;
146 const isDone = status === STATUS.SUCCESS;
150 const isDone = status === STATUS.SUCCESS;
152+ // add disabled, error info to input configs
148 const dynamicConfigs = useMemo(
153 const dynamicConfigs = useMemo(
150 inputConfigs.map(config => {
155 inputConfigs.map(config => {
151 config.disabled = !canInput;
156 config.disabled = !canInput;
152 const hasReceivedError = getHasRecieved(config.name).matchWith({
157 const hasReceivedError = getHasRecieved(config.name).matchWith({
153 Nothing: () => null, // loading
158 Nothing: () => null, // loading
154− Just: p => p.value && 'This email has already received an invite.',
159+ Just: p => p.value && kHasReceivedText,
156 config.error = hasReceivedError || errors[config.name];
161 config.error = hasReceivedError || errors[config.name];
157 return config;
162 return config;
159 [inputConfigs, errors, canInput, getHasRecieved]
164 [inputConfigs, errors, canInput, getHasRecieved]
161− const { inputs, pass } = useForm(dynamicConfigs);
162− const emails = useMemo(() => inputs.map(i => i.data).filter(d => !!d), [
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)?
167 const allPass = pass && !generalError;
170 const allPass = pass && !generalError;
171+ // the form is submittable iff passing and input is allowed
168− const canGenerate = allPass && status === STATUS.INPUT;
172+ const canGenerate = allPass && canInput;
173+ // additional inputs can be added iff input is allowed and we have enough
175+ const canAddInvite = canInput && inputConfigs.length < maxInvitesToSend;
177+ // progress is [0, .length] of invites or receipts, as we're generating them
170 const progress = isGenerating
178 const progress = isGenerating
171 ? Object.keys(invites).length
179 ? Object.keys(invites).length
172 : isSending
180 : isSending
@@ -174,6 +182,7 @@ export default function InviteEmail() {
175 const visualProgress = progress === null ? '-' : `${progress + 1}`;
183 const visualProgress = progress === null ? '-' : `${progress + 1}`;
185+ // build a builder for the accessory to the input, depending on status
177 const accessoryFor = (() => {
186 const accessoryFor = (() => {
178 if (isGenerating || canSend) return buildAccessoryFor(invites, errors);
187 if (isGenerating || canSend) return buildAccessoryFor(invites, errors);
179 if (isSending) return buildAccessoryFor(receipts, errors);
188 if (isSending) return buildAccessoryFor(receipts, errors);
@@ -355,49 +364,38 @@ export default function InviteEmail() {
355 sendMail,
364 sendMail,
358− const onClickGenerate = useCallback(async () => {
367+ const onClick = useCallback(async () => {
359 setGeneralError(null);
368 setGeneralError(null);
360− setStatus(STATUS.GENERATING);
371+ setStatus(STATUS.GENERATING);
362− await generateInvites();
372+ await generateInvites();
363− setStatus(STATUS.CAN_SEND);
373+ setStatus(STATUS.CAN_SEND);
364− } catch (error) {
374+ } else if (canSend) {
365− console.error(error);
366− setGeneralError(error);
367− setStatus(STATUS.FAILURE);
369− }, [setStatus, setGeneralError, generateInvites]);
371− const onClickSend = useCallback(async () => {
372− setGeneralError(null);
373− setStatus(STATUS.SENDING);
375+ setStatus(STATUS.SENDING);
375− await sendInvites();
376+ await sendInvites();
376− setStatus(STATUS.SUCCESS);
377+ setStatus(STATUS.SUCCESS);
377 } catch (error) {
379 } catch (error) {
378 console.error(error);
380 console.error(error);
379 setGeneralError(error);
381 setGeneralError(error);
380 setStatus(STATUS.FAILURE);
382 setStatus(STATUS.FAILURE);
382− }, [setStatus, sendInvites, setGeneralError]);
384+ }, [canGenerate, canSend, generateInvites, sendInvites]);
384− const onClick = useCallback(() => {
387− } else if (canSend) {
390− }, [canGenerate, canSend, onClickGenerate, onClickSend]);
386+ // when inputs update, check to see if any of them are duplicates
392 useEffect(() => {
387 useEffect(() => {
388+ // compute the list of valid emails
389+ const emails = inputs.map(i => i.data).filter(d => !!d);
393 if (uniq(emails).length !== emails.length) {
390 if (uniq(emails).length !== emails.length) {
394 setGeneralError(new Error(`Duplicate email.`));
391 setGeneralError(new Error(`Duplicate email.`));
396 setGeneralError(null);
393 setGeneralError(null);
398− }, [emails]);
395+ }, [inputs]);
400 // when we transition to done, sync invites because we just sent some
397 // when we transition to done, sync invites because we just sent some
398+ // and therefore know that state has changed
401 useEffect(() => {
399 useEffect(() => {
402 if (isDone) {
400 if (isDone) {
403 syncInvites(point);
401 syncInvites(point);