1import Maybe, { Just, Nothing } from 'folktale/maybe';
2import React, { useState } from 'react';
3import * as azimuth from 'azimuth-js';
4import { Grid, Flex, Button, Input, IconButton } from 'indigo-react';
6import * as need from 'lib/need';
7import { hasReceived, sendMail } from 'lib/inviteMail';
8import * as wg from 'lib/walletgen';
10import { H3, Warning } from 'components/old/Base';
12// for transaction generation and signing
15 sendSignedTransaction,
16 waitForTransactionConfirm,
21import * as tank from 'lib/tank';
22import { useLocalRouter } from 'lib/LocalRouter';
24import View from 'components/View';
25import MiniBackButton from 'components/MiniBackButton';
26import useRenderCount from 'lib/useRenderCount';
27import useInvites from 'lib/useInvites';
28import { usePointCursor } from 'store/pointCursor';
29import LoadableButton from 'components/LoadableButton';
30import useArray from 'lib/useArray';
31import useForm from 'indigo-react/lib/useForm';
32import { buildEmailInputConfig } from 'components/Inputs';
34const GAS_PRICE_GWEI = 20; // we pay the premium for faster ux
35const GAS_LIMIT = 350000;
36const INVITE_COST = toWei((GAS_PRICE_GWEI * GAS_LIMIT).toString(), 'gwei');
39 INPUT: 'Generate invites',
40 GENERATING: 'Generating invites',
41 CAN_SEND: 'Send invites',
42 FUNDING: 'Funding invites',
43 SENDING: 'Sending invites',
54// world's simplest uid
56const buildInputConfig = (extra = {}) =>
57 buildEmailInputConfig({ name: `email-${id++}`, ...extra });
59export default function NewInviteEmail() {
60 const { pop } = useLocalRouter();
61 const { pointCursor } = usePointCursor();
62 const point = need.pointCursor(pointCursor);
64 const { availableInvites } = useInvites(point);
65 const maxInvitesToSend = availableInvites.matchWith({
72 { append: appendInput, removeAt: removeInputAt },
74 [buildInputConfig({ placeholder: 'Email Addresss' })],
78 const [hovered, _setHovered] = useState({});
79 const setHovered = (name, value) => () =>
80 _setHovered((value = { ...value, [name]: value }));
82 console.log(inputConfigs.length, inputConfigs.map(c => c.name));
84 const { inputs, pass } = useForm(inputConfigs);
86 const canAddInvite = inputs.length < maxInvitesToSend;
88 useRenderCount('InviteEmail');
92 <Grid.Item as={Grid} className="mb4" full>
93 <Grid.Item as={Flex} cols={[1, 11]} align="center">
94 <MiniBackButton onClick={() => pop()} />
96 <Grid.Item cols={[11, 13]} justifySelf="end">
97 <IconButton onClick={appendInput} disabled={!canAddInvite} solid>
104 {inputs.map((input, i) => {
105 const isFirst = i === 0;
111 onMouseEnter={setHovered(input.name, true)}
112 onMouseLeave={setHovered(input.name, false)}
114 <Grid.Item as={Input} cols={[1, 11]} {...input} />
115 {!isFirst && (input.focused || hovered[input.name]) && (
116 <Grid.Item cols={[11, 13]} justifySelf="end" alignSelf="center">
117 <IconButton onClick={() => removeInputAt(i)}>-</IconButton>
124 <Grid.Item as={LoadableButton} full disabled={!pass} solid>
131class InviteEmail extends React.Component {
136 invitesAvailable: Nothing(),
137 targets: [{ email: '', hasReceived: Nothing(), status: Nothing() }],
138 status: STATUS.INPUT,
141 fundingMessage: Nothing(),
145 this.generateInvites = this.generateInvites.bind(this);
146 this.sendInvites = this.sendInvites.bind(this);
147 this.canSend = this.canSend.bind(this);
148 this.addError = this.addError.bind(this);
149 this.handleEmailInput = this.handleEmailInput.bind(this);
150 this.addTarget = this.addTarget.bind(this);
151 this.removeTarget = this.removeTarget.bind(this);
152 this.setEmailStatus = this.setEmailStatus.bind(this);
153 this.askForFunding = this.askForFunding.bind(this);
156 componentDidMount() {
157 this.point = need.pointCursor(this.props.pointCursor);
158 this.address = need.addressFromWallet(this.props.wallet);
159 this.contracts = need.contracts(this.props.contracts);
160 this.web3 = need.web3(this.props.web3);
162 azimuth.delegatedSending
163 .getTotalUsableInvites(this.contracts, this.point)
165 this.setState({ invitesAvailable: Just(count) });
169 async getTemporaryWallet() {
170 const ticket = await wg.makeTicket(0x10000); // planet-sized ticket
171 const owner = await wg.generateOwnershipWallet(0, ticket);
172 return { ticket, owner: owner.keys.address };
175 async generateInvites() {
176 this.setState({ status: STATUS.GENERATING });
178 const web3 = this.web3;
179 const targets = this.state.targets;
180 const planets = await azimuth.delegatedSending.getPlanetsToSend(
186 // account for the race condition where invites got used up while we were
187 // composing our target list
188 if (planets.length < targets.length) {
189 this.addError('Can currently only send ' + planets.length + ' invites!');
193 const nonce = await web3.eth.getTransactionCount(this.address);
194 const chainId = await web3.eth.net.getId();
196 // create invite wallets and transactions
197 let invites = []; // { email, ticket, signedTx, rawTx }
198 for (let i = 0; i < targets.length; i++) {
199 console.log('generating email', i, targets[i].email);
200 this.setEmailStatus(i, EMAIL_STATUS.BUSY);
202 const email = targets[i].email;
203 const wallet = await this.getTemporaryWallet();
205 let inviteTx = azimuth.delegatedSending.sendPoint(
211 const signedTx = await signTransaction({
214 gasPrice: GAS_PRICE_GWEI.toString(),
215 gasLimit: GAS_LIMIT.toString(),
220 const rawTx = hexify(signedTx.serialize());
222 invites.push({ email, ticket: wallet.ticket, signedTx, rawTx });
223 this.setEmailStatus(i, EMAIL_STATUS.DONE);
226 this.setState({ wipInvites: invites, status: STATUS.CAN_SEND });
229 async sendInvites() {
230 const invites = this.state.wipInvites;
231 const web3 = this.web3;
233 //TODO invite status isn't changing/re-rendering... why?
235 status: STATUS.FUNDING,
236 invites: invites.map(i => {
237 return { ...i, status: Nothing() };
241 const tankWasUsed = await tank.ensureFundsFor(
245 INVITE_COST * invites.length,
246 invites.map(i => i.rawTx),
249 this.setState({ fundingMessage: Nothing() });
253 this.setState({ status: STATUS.SENDING });
255 //TODO nasty loop, probably move into lib... but so many callbacks!
257 for (let i = 0; i < invites.length; i++) {
258 const invite = invites[i];
260 // send transaction. if it fails, inform the user and skip to next
263 txHash = await sendSignedTransaction(
265 Just(invite.signedTx),
270 console.error('invite tx sending failed', err);
271 this.addError('Invite transaction not sent for ' + invite.email);
272 this.setEmailStatus(i, EMAIL_STATUS.FAIL);
276 // ask the email sender to send this. we can do this prior to tx confirm,
277 // because the sender service waits for confirm & success for us.
278 // if this fails, inform the user and skip to next
280 const mailSuccess = await sendMail(
285 if (!mailSuccess) throw new Error();
286 this.setEmailStatus(i, EMAIL_STATUS.DONE);
288 console.log('email send failed');
289 //NOTE this assumes that the transaction succeeds, but we don't know
290 // that for a fact yet...
292 'Invite email failed to send for ' +
294 '. Please give them this ticket: ' +
297 this.setEmailStatus(i, EMAIL_STATUS.FAIL);
301 // update status on transaction confirm, but don't block on it
303 waitForTransactionConfirm(web3, txHash).then(success => {
305 this.setEmailStatus(i, EMAIL_STATUS.DONE);
307 console.log('invite tx rejected');
308 this.addError('Invite transaction failed for ' + invite.email);
309 this.setEmailStatus(i, EMAIL_STATUS.FAIL);
316 const txStatus = await Promise.all(promises);
317 if (txStatus.every(e => e)) {
318 this.setState({ status: STATUS.DONE });
320 this.setState({ status: STATUS.FAIL });
324 setEmailStatus(i, status) {
325 const targets = this.state.targets;
326 targets[i].status = Just(status);
327 this.setState({ targets });
331 const newError = this.state.errors.matchWith({
332 Nothing: () => [error],
334 errs.value.push(error);
338 this.setState({ errors: Just(newError) });
341 askForFunding(address, minBalance, balance) {
343 fundingMessage: Just(
344 `Please make sure ${address} has at least ${fromWei(minBalance)} ETH,` +
345 `we'll continue once that's true. Current balance: ${fromWei(
353 handleEmailInput(i, email) {
354 let targets = this.state.targets;
355 targets[i].email = email;
357 // if it's a valid address, check if it has received an invite before
358 if (email.match(/.*@.*\...+/)) {
359 hasReceived(email).then(res => {
360 targets[i].hasReceived = Just(res);
361 this.setState({ targets });
364 targets[i].hasReceived = Nothing();
367 this.setState({ targets });
371 const targets = this.state.targets;
373 // may not have duplicate targets
374 if (new Set(targets.map(t => t.email)).size !== targets.length)
377 // none may have received invites already
378 for (let i = 0; i < targets.length; i++) {
379 const target = targets[i];
380 const res = target.hasReceived.matchWith({
382 Just: has => has.value,
384 if (res) return false;
387 return this.state.invitesAvailable.matchWith({
388 Nothing: () => false,
389 Just: invites => invites.value > 0,
394 let targets = this.state.targets;
395 targets.push({ email: '', hasReceived: Nothing(), status: Nothing() });
396 this.setState({ targets });
400 let targets = this.state.targets;
401 targets.length = targets.length - 1;
402 this.setState({ targets });
407 this.state.status === STATUS.DONE ? this.state.targets.length : null;
409 const error = this.state.errors.matchWith({
412 <Warning className={'mt-8'}>
413 <H3 style={{ marginTop: 0, paddingTop: 0 }}>
414 {'Something went wrong!'}
416 {errors.value.map(err => (
423 const fundingMessage = this.state.fundingMessage.matchWith({
425 Just: msg => <Warning className={'mt-8'}>{msg.value}</Warning>,
429 if (this.state.status === STATUS.DONE) {
430 //TODO proper clickable router thing
431 inputs = '<- Back to Home';
433 const inputDisabled = this.state.status !== STATUS.INPUT;
437 prop-size={'s narrow'}
439 disabled={inputDisabled || this.state.targets.length === 1}
440 onClick={this.removeTarget}>
447 prop-size={'s narrow'}
451 this.state.invitesAvailable.matchWith({
453 Just: inv => inv.value <= this.state.targets.length,
456 onClick={this.addTarget}>
461 let targetInputs = [];
462 for (let i = 0; i < this.state.targets.length; i++) {
463 const target = this.state.targets[i];
464 let progress = target.status.matchWith({
466 Just: status => status.value,
473 this.handleEmailInput(i, value);
475 placeholder={'Email address'}
476 disabled={inputDisabled}
478 {target.hasReceived.matchWith({
481 has.value ? <span>{'×'}</span> : <span>{'°'}</span>,
488 const submitButton = (
490 prop-size={'xl wide'}
493 (inputDisabled && this.state.status !== STATUS.CAN_SEND) ||
497 if (this.state.status === STATUS.INPUT) this.generateInvites();
498 if (this.state.status === STATUS.CAN_SEND) this.sendInvites();
526// export default compose(
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list