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
57const buildInputConfig = (extra = {}) =>
58 buildEmailInputConfig({ name: (id++).toString(), ...extra });
60export default function NewInviteEmail() {
61 const { pop } = useLocalRouter();
62 const { pointCursor } = usePointCursor();
63 const point = need.pointCursor(pointCursor);
65 const { availableInvites } = useInvites(point);
66 const maxInvitesToSend = availableInvites.matchWith({
73 { append: appendInput, removeAt: removeInputAt },
75 [buildInputConfig({ placeholder: 'Email Addresss' })],
79 const [hovered, _setHovered] = useState({});
80 const setHovered = (name, value) => () =>
81 _setHovered((value = { ...value, [name]: value }));
83 console.log(inputConfigs.length, inputConfigs.map(c => c.name));
85 const { inputs, pass } = useForm(inputConfigs);
87 const canAddInvite = inputs.length < maxInvitesToSend;
89 useRenderCount('InviteEmail');
93 <Grid.Item as={Grid} className="mb4" full>
94 <Grid.Item as={Flex} cols={[1, 11]} align="center">
95 <MiniBackButton onClick={() => pop()} />
97 <Grid.Item cols={[11, 13]} justifySelf="end">
98 <IconButton onClick={appendInput} disabled={!canAddInvite} solid>
105 {inputs.map((input, i) => {
106 const isFirst = i === 0;
112 onMouseEnter={setHovered(input.name, true)}
113 onMouseLeave={setHovered(input.name, false)}
115 <Grid.Item as={Input} cols={[1, 11]} {...input} />
116 {!isFirst && (input.focused || hovered[input.name]) && (
117 <Grid.Item cols={[11, 13]} justifySelf="end" alignSelf="center">
118 <IconButton onClick={() => removeInputAt(i)}>-</IconButton>
125 <Grid.Item as={LoadableButton} full disabled={!pass} solid>
132class InviteEmail extends React.Component {
137 invitesAvailable: Nothing(),
138 targets: [{ email: '', hasReceived: Nothing(), status: Nothing() }],
139 status: STATUS.INPUT,
142 fundingMessage: Nothing(),
146 this.generateInvites = this.generateInvites.bind(this);
147 this.sendInvites = this.sendInvites.bind(this);
148 this.canSend = this.canSend.bind(this);
149 this.addError = this.addError.bind(this);
150 this.handleEmailInput = this.handleEmailInput.bind(this);
151 this.addTarget = this.addTarget.bind(this);
152 this.removeTarget = this.removeTarget.bind(this);
153 this.setEmailStatus = this.setEmailStatus.bind(this);
154 this.askForFunding = this.askForFunding.bind(this);
157 componentDidMount() {
158 this.point = need.pointCursor(this.props.pointCursor);
159 this.address = need.addressFromWallet(this.props.wallet);
160 this.contracts = need.contracts(this.props.contracts);
161 this.web3 = need.web3(this.props.web3);
163 azimuth.delegatedSending
164 .getTotalUsableInvites(this.contracts, this.point)
166 this.setState({ invitesAvailable: Just(count) });
170 async getTemporaryWallet() {
171 const ticket = await wg.makeTicket(0x10000); // planet-sized ticket
172 const owner = await wg.generateOwnershipWallet(0, ticket);
173 return { ticket, owner: owner.keys.address };
176 async generateInvites() {
177 this.setState({ status: STATUS.GENERATING });
179 const web3 = this.web3;
180 const targets = this.state.targets;
181 const planets = await azimuth.delegatedSending.getPlanetsToSend(
187 // account for the race condition where invites got used up while we were
188 // composing our target list
189 if (planets.length < targets.length) {
190 this.addError('Can currently only send ' + planets.length + ' invites!');
194 const nonce = await web3.eth.getTransactionCount(this.address);
195 const chainId = await web3.eth.net.getId();
197 // create invite wallets and transactions
198 let invites = []; // { email, ticket, signedTx, rawTx }
199 for (let i = 0; i < targets.length; i++) {
200 console.log('generating email', i, targets[i].email);
201 this.setEmailStatus(i, EMAIL_STATUS.BUSY);
203 const email = targets[i].email;
204 const wallet = await this.getTemporaryWallet();
206 let inviteTx = azimuth.delegatedSending.sendPoint(
212 const signedTx = await signTransaction({
215 gasPrice: GAS_PRICE_GWEI.toString(),
216 gasLimit: GAS_LIMIT.toString(),
221 const rawTx = hexify(signedTx.serialize());
223 invites.push({ email, ticket: wallet.ticket, signedTx, rawTx });
224 this.setEmailStatus(i, EMAIL_STATUS.DONE);
227 this.setState({ wipInvites: invites, status: STATUS.CAN_SEND });
230 async sendInvites() {
231 const invites = this.state.wipInvites;
232 const web3 = this.web3;
234 //TODO invite status isn't changing/re-rendering... why?
236 status: STATUS.FUNDING,
237 invites: invites.map(i => {
238 return { ...i, status: Nothing() };
242 const tankWasUsed = await tank.ensureFundsFor(
246 INVITE_COST * invites.length,
247 invites.map(i => i.rawTx),
250 this.setState({ fundingMessage: Nothing() });
254 this.setState({ status: STATUS.SENDING });
256 //TODO nasty loop, probably move into lib... but so many callbacks!
258 for (let i = 0; i < invites.length; i++) {
259 const invite = invites[i];
261 // send transaction. if it fails, inform the user and skip to next
264 txHash = await sendSignedTransaction(
266 Just(invite.signedTx),
271 console.error('invite tx sending failed', err);
272 this.addError('Invite transaction not sent for ' + invite.email);
273 this.setEmailStatus(i, EMAIL_STATUS.FAIL);
277 // ask the email sender to send this. we can do this prior to tx confirm,
278 // because the sender service waits for confirm & success for us.
279 // if this fails, inform the user and skip to next
281 const mailSuccess = await sendMail(
286 if (!mailSuccess) throw new Error();
287 this.setEmailStatus(i, EMAIL_STATUS.DONE);
289 console.log('email send failed');
290 //NOTE this assumes that the transaction succeeds, but we don't know
291 // that for a fact yet...
293 'Invite email failed to send for ' +
295 '. Please give them this ticket: ' +
298 this.setEmailStatus(i, EMAIL_STATUS.FAIL);
302 // update status on transaction confirm, but don't block on it
304 waitForTransactionConfirm(web3, txHash).then(success => {
306 this.setEmailStatus(i, EMAIL_STATUS.DONE);
308 console.log('invite tx rejected');
309 this.addError('Invite transaction failed for ' + invite.email);
310 this.setEmailStatus(i, EMAIL_STATUS.FAIL);
317 const txStatus = await Promise.all(promises);
318 if (txStatus.every(e => e)) {
319 this.setState({ status: STATUS.DONE });
321 this.setState({ status: STATUS.FAIL });
325 setEmailStatus(i, status) {
326 const targets = this.state.targets;
327 targets[i].status = Just(status);
328 this.setState({ targets });
332 const newError = this.state.errors.matchWith({
333 Nothing: () => [error],
335 errs.value.push(error);
339 this.setState({ errors: Just(newError) });
342 askForFunding(address, minBalance, balance) {
344 fundingMessage: Just(
345 `Please make sure ${address} has at least ${fromWei(minBalance)} ETH,` +
346 `we'll continue once that's true. Current balance: ${fromWei(
354 handleEmailInput(i, email) {
355 let targets = this.state.targets;
356 targets[i].email = email;
358 // if it's a valid address, check if it has received an invite before
359 if (email.match(/.*@.*\...+/)) {
360 hasReceived(email).then(res => {
361 targets[i].hasReceived = Just(res);
362 this.setState({ targets });
365 targets[i].hasReceived = Nothing();
368 this.setState({ targets });
372 const targets = this.state.targets;
374 // may not have duplicate targets
375 if (new Set(targets.map(t => t.email)).size !== targets.length)
378 // none may have received invites already
379 for (let i = 0; i < targets.length; i++) {
380 const target = targets[i];
381 const res = target.hasReceived.matchWith({
383 Just: has => has.value,
385 if (res) return false;
388 return this.state.invitesAvailable.matchWith({
389 Nothing: () => false,
390 Just: invites => invites.value > 0,
395 let targets = this.state.targets;
396 targets.push({ email: '', hasReceived: Nothing(), status: Nothing() });
397 this.setState({ targets });
401 let targets = this.state.targets;
402 targets.length = targets.length - 1;
403 this.setState({ targets });
408 this.state.status === STATUS.DONE ? this.state.targets.length : null;
410 const error = this.state.errors.matchWith({
413 <Warning className={'mt-8'}>
414 <H3 style={{ marginTop: 0, paddingTop: 0 }}>
415 {'Something went wrong!'}
417 {errors.value.map(err => (
424 const fundingMessage = this.state.fundingMessage.matchWith({
426 Just: msg => <Warning className={'mt-8'}>{msg.value}</Warning>,
430 if (this.state.status === STATUS.DONE) {
431 //TODO proper clickable router thing
432 inputs = '<- Back to Home';
434 const inputDisabled = this.state.status !== STATUS.INPUT;
438 prop-size={'s narrow'}
440 disabled={inputDisabled || this.state.targets.length === 1}
441 onClick={this.removeTarget}>
448 prop-size={'s narrow'}
452 this.state.invitesAvailable.matchWith({
454 Just: inv => inv.value <= this.state.targets.length,
457 onClick={this.addTarget}>
462 let targetInputs = [];
463 for (let i = 0; i < this.state.targets.length; i++) {
464 const target = this.state.targets[i];
465 let progress = target.status.matchWith({
467 Just: status => status.value,
474 this.handleEmailInput(i, value);
476 placeholder={'Email address'}
477 disabled={inputDisabled}
479 {target.hasReceived.matchWith({
482 has.value ? <span>{'×'}</span> : <span>{'°'}</span>,
489 const submitButton = (
491 prop-size={'xl wide'}
494 (inputDisabled && this.state.status !== STATUS.CAN_SEND) ||
498 if (this.state.status === STATUS.INPUT) this.generateInvites();
499 if (this.state.status === STATUS.CAN_SEND) this.sendInvites();
527// 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