on ee498fc help sign in
225 lines 6.0 kB history raw
1import React, { useState, useCallback, useMemo } from 'react';
2import { Grid } from 'indigo-react';
3import { FORM_ERROR } from 'final-form';
4import * as ob from 'urbit-ob';
5import { uniq, zip, compact, isEqual } from 'lodash';
7import { usePointCursor } from 'store/pointCursor';
9import * as need from 'lib/need';
10import useMailer from 'lib/useMailer';
11import { pluralize } from 'lib/pluralize';
12import useInviter from 'lib/useInviter';
14import {
15 composeValidator,
16 hasErrors,
17 buildEmailValidator,
18 buildArrayValidator,
19} from 'form/validators';
20import EmailChipInput from 'form/EmailChipInput';
21import FormError from 'form/FormError';
22import SubmitButton from 'form/SubmitButton';
23import BridgeForm from 'form/BridgeForm';
25import NeedFundsNotice from 'components/NeedFundsNotice';
26import ProgressButton from 'components/ProgressButton';
28const HAS_RECEIVED_TEXT = 'This email has already received an invite: ';
30const STATUS = {
31 INPUT: 'INPUT',
32 SENDING: 'SENDING',
33 SUCCESS: 'SUCCESS',
34 FAILURE: 'FAILURE',
35};
37const InviteMail = ({ setSubmitting }) => {
38 const {
39 progress,
40 txStatus,
41 needFunds,
42 generateInvites,
43 resetInvites,
44 } = useInviter();
46 const { sendMail, getHasReceived } = useMailer();
48 const [status, setStatus] = useState(STATUS.INPUT);
50 const [count, setCount] = useState(0);
51 const canInput = status === STATUS.INPUT;
52 const isSending = txStatus === STATUS.SENDING || status === STATUS.SENDING;
53 const isDone = status === STATUS.SUCCESS;
54 const isFailed = status === STATUS.FAILED;
56 const { pointCursor } = usePointCursor();
57 const point = need.point(pointCursor);
59 const sendInvites = useCallback(
60 async (emails, invites) => {
61 const mailingErrors = compact(
62 await Promise.all(
63 zip(emails, invites).map(([email, invite]) =>
64 sendMail(email, invite.ticket, ob.patp(point), '', invite.rawTx)
65 .then(() => null)
66 .catch(
67 () => `Sending ticket ${invite.ticket} to ${email} failed.`
68 )
69 )
70 )
71 ).join(', ');
72 if (mailingErrors.length) {
73 return { [FORM_ERROR]: mailingErrors };
74 }
75 },
76 [point, sendMail]
77 );
79 const onSubmit = useCallback(
80 async (values, form) => {
81 const emailCount = values.emails.length;
82 setCount(emailCount);
83 setStatus(STATUS.SENDING);
84 setSubmitting(true);
85 const { errors, invites } = await generateInvites(emailCount);
86 if (errors) {
87 setStatus(STATUS.FAILED);
88 setSubmitting(false);
89 return errors;
90 }
91 const mailErrors = await sendInvites(values.emails, invites);
92 if (mailErrors) {
93 console.log(mailErrors);
94 setStatus(STATUS.FAILED);
95 setSubmitting(false);
96 return mailErrors;
97 }
98 setSubmitting(false);
99 setStatus(STATUS.SUCCESS);
100 setTimeout(() => {
101 setStatus(STATUS.INPUT);
102 resetInvites();
103 form.reset();
104 }, 1500);
105 },
106 [setSubmitting, generateInvites, sendInvites, resetInvites]
107 );
109 const validateHasReceived = useCallback(
110 async email => {
111 const hasReceived = await getHasReceived(email);
112 if (hasReceived) {
113 return HAS_RECEIVED_TEXT + email;
114 }
115 },
116 [getHasReceived]
117 );
119 const buttonText = useCallback(() => {
120 switch (status) {
121 case STATUS.INPUT:
122 return 'Add to Invite Group';
123 case STATUS.SENDING:
124 return `Sending ${pluralize(count, 'Invite')}...`;
125 case STATUS.SUCCESS:
126 return `✓ ${pluralize(count, 'Invite')} sent`;
127 case STATUS.FAILURE:
128 default:
129 return 'Error';
130 }
131 }, [status, count]);
133 const renderButton = useCallback(
134 handleSubmit => {
135 if (canInput) {
136 return (
137 <Grid.Item full as={SubmitButton} handleSubmit={handleSubmit}>
138 {buttonText()}
139 </Grid.Item>
140 );
141 }
143 if (isSending) {
144 const percentProgress = progress / count;
145 return (
146 <Grid.Item
147 full
148 as={ProgressButton}
149 className="mt4"
150 solid
151 disabled
152 success
153 progress={percentProgress}>
154 {buttonText(count)}
155 </Grid.Item>
156 );
157 }
158 if (isDone) {
159 return (
160 <Grid.Item full className="mt4 p3 bg-green3 white f5">
161 {buttonText(count)}
162 </Grid.Item>
163 );
164 }
165 if (isFailed) {
166 return (
167 <Grid.Item full className="mt4 p3 bg-red3 white f5">
168 {buttonText(count)}
169 </Grid.Item>
170 );
171 }
173 return null;
174 },
175 [buttonText, isDone, isFailed, isSending, canInput, count, progress]
176 );
178 const validateForm = useCallback((values, errors) => {
179 if (hasErrors(errors)) {
180 return errors;
181 }
183 // check for email uniqenesss
184 const emails = values.emails.filter(d => !!d);
185 if (uniq(emails).length !== emails.length) {
186 return { [FORM_ERROR]: 'Duplicate email.' };
187 }
188 }, []);
190 const validate = useMemo(
191 () =>
192 composeValidator(
193 {
194 emails: buildArrayValidator(buildEmailValidator(validateHasReceived)),
195 },
196 validateForm
197 ),
198 [validateHasReceived, validateForm]
199 );
201 const initialValues = { emails: [] };
202 return (
203 <Grid.Item full as={Grid}>
204 <BridgeForm
205 validate={validate}
206 initialValues={initialValues}
207 initialValuesEqual={isEqual}
208 onSubmit={onSubmit}>
209 {({ handleSubmit, values }) => (
210 <>
211 <Grid.Item full as={EmailChipInput} name="emails" className="mt4" />
212 {renderButton(handleSubmit)}
214 <Grid.Item full as={FormError} />
215 {needFunds && (
216 <Grid.Item full as={NeedFundsNotice} {...needFunds} />
217 )}
218 </>
219 )}
220 </BridgeForm>
221 </Grid.Item>
222 );
223};
225export default InviteMail;