on 852cc05 help sign in
210 lines 5.7 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 = () => {
38 const { progress, txStatus, needFunds, generateInvites } = useInviter();
40 const { sendMail, getHasReceived } = useMailer();
42 const [status, setStatus] = useState(STATUS.INPUT);
44 const [count, setCount] = useState(0);
45 const canInput = status === STATUS.INPUT;
46 const isSending = txStatus === STATUS.SENDING || status === STATUS.SENDING;
47 const isDone = status === STATUS.SUCCESS;
48 const isFailed = status === STATUS.FAILED;
50 const { pointCursor } = usePointCursor();
51 const point = need.point(pointCursor);
53 const sendInvites = useCallback(
54 async (emails, invites) => {
55 const mailingErrors = compact(
56 await Promise.all(
57 zip(emails, invites).map(([email, invite]) =>
58 sendMail(email, invite.ticket, ob.patp(point), '', invite.rawTx)
59 .then(() => null)
60 .catch(
61 () => `Sending ticket ${invite.ticket} to ${email} failed.`
62 )
63 )
64 )
65 ).join(', ');
66 if (mailingErrors.length) {
67 return { [FORM_ERROR]: mailingErrors };
68 }
69 },
70 [point, sendMail]
71 );
73 const onSubmit = useCallback(
74 async values => {
75 const emailCount = values.emails.length;
76 setCount(emailCount);
77 setStatus(STATUS.SENDING);
78 const { errors, invites } = await generateInvites(emailCount);
79 if (errors) {
80 setStatus(STATUS.FAILED);
81 return errors;
82 }
83 const mailErrors = await sendInvites(values.emails, invites);
84 if (mailErrors) {
85 console.log(mailErrors);
86 setStatus(STATUS.FAILED);
87 return mailErrors;
88 }
89 setStatus(STATUS.SUCCESS);
90 },
91 [generateInvites, setCount, setStatus, sendInvites]
92 );
94 const validateHasReceived = useCallback(
95 async email => {
96 const hasReceived = await getHasReceived(email);
97 if (hasReceived) {
98 return HAS_RECEIVED_TEXT;
99 }
100 },
101 [getHasReceived]
102 );
104 const buttonText = useCallback(() => {
105 switch (status) {
106 case STATUS.INPUT:
107 return 'Add to Invite Group';
108 case STATUS.SENDING:
109 return `Sending ${pluralize(count, 'Invite')}...`;
110 case STATUS.SUCCESS:
111 return `✓ ${pluralize(count, 'Invite')} sent`;
112 case STATUS.FAILURE:
113 default:
114 return 'Error';
115 }
116 }, [status, count]);
118 const renderButton = useCallback(
119 handleSubmit => {
120 if (canInput) {
121 return (
122 <Grid.Item full as={SubmitButton} handleSubmit={handleSubmit}>
123 {buttonText()}
124 </Grid.Item>
125 );
126 }
128 if (isSending) {
129 const percentProgress = progress / count;
130 return (
131 <Grid.Item
132 full
133 as={ProgressButton}
134 className="mt4"
135 solid
136 disabled
137 success
138 progress={percentProgress}>
139 {buttonText(count)}
140 </Grid.Item>
141 );
142 }
143 if (isDone) {
144 return (
145 <Grid.Item full className="mt4 p3 bg-green3 white f5">
146 {buttonText(count)}
147 </Grid.Item>
148 );
149 }
150 if (isFailed) {
151 return (
152 <Grid.Item full className="mt4 p3 bg-red3 white f5">
153 {buttonText(count)}
154 </Grid.Item>
155 );
156 }
158 return null;
159 },
160 [buttonText, isDone, isFailed, isSending, canInput, count, progress]
161 );
163 const validateForm = useCallback((values, errors) => {
164 if (hasErrors(errors)) {
165 return errors;
166 }
168 // check for email uniqenesss
169 const emails = values.emails.filter(d => !!d);
170 if (uniq(emails).length !== emails.length) {
171 return { [FORM_ERROR]: 'Duplicate email.' };
172 }
173 }, []);
175 const validate = useMemo(
176 () =>
177 composeValidator(
178 {
179 emails: buildArrayValidator(buildEmailValidator(validateHasReceived)),
180 },
181 validateForm
182 ),
183 [validateHasReceived, validateForm]
184 );
186 const initialValues = { emails: [] };
187 return (
188 <Grid.Item full as={Grid}>
189 <BridgeForm
190 validate={validate}
191 initialValues={initialValues}
192 initialValuesEqual={isEqual}
193 onSubmit={onSubmit}>
194 {({ handleSubmit, values }) => (
195 <>
196 <Grid.Item full as={EmailChipInput} name="emails" className="mt4" />
197 {renderButton(handleSubmit)}
199 <Grid.Item full as={FormError} />
200 {needFunds && (
201 <Grid.Item full as={NeedFundsNotice} {...needFunds} />
202 )}
203 </>
204 )}
205 </BridgeForm>
206 </Grid.Item>
207 );
208};
210export default InviteMail;