1import React, { useMemo, useCallback, useState, useEffect } from 'react';
2import { Grid, ErrorText } from 'indigo-react';
3import { get, uniq, zip, compact, isEqual } from 'lodash';
4import * as azimuth from 'azimuth-js';
5import { FORM_ERROR } from 'final-form';
6import { toWei, toBN } from 'web3-utils';
7import * as ob from 'urbit-ob';
9import * as need from 'lib/need';
10import * as wg from 'lib/walletgen';
11import * as tank from 'lib/tank';
12import useCopiable from 'lib/useCopiable';
14import Tabs from 'components/Tabs';
15import ProgressButton from 'components/ProgressButton';
16import { ForwardButton } from 'components/Buttons';
17import NeedFundsNotice from 'components/NeedFundsNotice';
19import { useNetwork } from 'store/network';
20import { useWallet } from 'store/wallet';
21import { usePointCursor } from 'store/pointCursor';
22import { usePointCache } from 'store/pointCache';
26 sendSignedTransaction,
27 waitForTransactionConfirm,
30import { MIN_PLANET, GAS_LIMITS } from 'lib/constants';
31import pluralize from 'lib/pluralize';
32import useGasPrice from 'lib/useGasPrice';
34import EmailChipInput from 'form/EmailChipInput';
35import FormError from 'form/FormError';
36import SubmitButton from 'form/SubmitButton';
37import BridgeForm from 'form/BridgeForm';
43} from 'form/validators';
44import { WARNING } from 'form/helpers';
45import useMailer from 'lib/useMailer';
47const GAS_LIMIT = GAS_LIMITS.GIFT_PLANET;
56const HAS_RECEIVED_TEXT = 'This email has already received an invite.';
58const useInviter = sendInvites => {
59 const { contracts, web3, networkType } = useNetwork();
60 const { wallet, walletType, walletHdPath } = useWallet();
61 const { pointCursor } = usePointCursor();
62 const { syncInvites } = usePointCache();
63 const { gasPrice } = useGasPrice();
64 const point = need.point(pointCursor);
66 const [needFunds, setNeedFunds] = useState(null);
67 const [invites, setInvites] = useState([]);
69 const [progress, setProgress] = useState(0);
70 const [txStatus, setTxStatus] = useState(STATUS.INPUT);
71 // const [errors, addError] = useSetState();
73 const isDone = txStatus === STATUS.SUCCESS;
78 }, [isDone, syncInvites, point]);
80 const resetInvites = useCallback(() => {
83 setTxStatus(STATUS.INPUT);
84 }, [setProgress, setInvites, setTxStatus]);
86 const generateInvites = useCallback(
88 const _contracts = contracts.getOrElse(null);
89 const _web3 = web3.getOrElse(null);
90 const _wallet = wallet.getOrElse(null);
91 if (!_contracts || !_web3 || !_wallet) {
92 // not using need because we want a custom error
93 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
96 const nonce = await _web3.eth.getTransactionCount(_wallet.address);
97 const chainId = await _web3.eth.net.getId();
99 const planets = await azimuth.delegatedSending.getPlanetsToSend(
105 setTxStatus(STATUS.SENDING);
106 // account for the race condition where invites got used up while we were
107 // composing our target list
108 if (planets.length < numInvites) {
109 // resync invites to the cache, since they're out of date
112 setTxStatus(STATUS.FAILURE);
117 `Can currently only send ${planets.length} invites. ` +
118 `Please remove invites until you are within the limit.`,
123 // NB(shrugs) - must be processed in serial because main thread, etc
124 let signedInvites = [];
126 for (let i = 0; i < numInvites; i++) {
127 setProgress(x => x + 1);
129 const planet = planets[i];
131 const { ticket, owner } = await wg.generateTemporaryTicketAndWallet(
133 // we're always giving planets, so generate a ticket of the correct size
136 const inviteTx = azimuth.delegatedSending.sendPoint(
143 const signedTx = await signTransaction({
150 // TODO: ^ make a useTransactionSigner to encapsulate this logic
152 gasPrice: gasPrice.toFixed(), // expects string gwei
153 gasLimit: GAS_LIMIT.toFixed(),
156 const rawTx = hexify(signedTx.serialize());
158 signedInvites.push({ ticket, signedTx, rawTx });
160 console.error(error);
163 [WARNING]: `There was an error while generating wallets.`,
169 const totalCost = toBN(GAS_LIMIT)
171 .mul(toBN(numInvites));
172 const tankWasUsed = await tank.ensureFundsFor(
176 toWei(totalCost.toString(), 'gwei'),
177 Object.keys(invites).map(name => invites[name].rawTx),
178 (address, minBalance, balance) =>
179 setNeedFunds({ address, minBalance, balance }),
180 () => setNeedFunds(undefined)
183 let unsentInvites = [];
184 let confirmedInvites = [];
185 const txAndMailings = signedInvites.map(async invite => {
187 const txHash = await sendSignedTransaction(
193 await waitForTransactionConfirm(_web3, txHash);
194 confirmedInvites.push(invite);
196 console.error(error);
197 unsentInvites.push(invite);
202 await Promise.all(txAndMailings);
204 setTxStatus(STATUS.SUCCESS);
205 setInvites(confirmedInvites);
206 console.log(unsentInvites);
207 console.log(confirmedInvites);
209 if (unsentInvites.length > 0) {
210 return { errors: { [FORM_ERROR]: unsentInvites } };
213 if (errorCount > 0) {
216 [WARNING]: `There ${pluralize(
223 )} while generating wallets. You can still send the invites that generated correctly.`,
227 return { invites: confirmedInvites };
253const InviteMail = () => {
254 const { progress, txStatus, needFunds, generateInvites } = useInviter();
256 const { sendMail, getHasReceived } = useMailer();
258 const [status, setStatus] = useState(STATUS.INPUT);
260 const [count, setCount] = useState(0);
261 const canInput = status === STATUS.INPUT;
262 const isSending = txStatus === STATUS.SENDING || status === STATUS.SENDING;
263 const isDone = status === STATUS.SUCCESS;
264 const isFailed = status === STATUS.FAILED;
266 const { pointCursor } = usePointCursor();
267 const point = need.point(pointCursor);
269 const sendInvites = useCallback(
270 async (emails, invites) => {
271 const mailingErrors = compact(
273 zip(emails, invites).map(([email, invite]) =>
274 sendMail(email, invite.ticket, ob.patp(point), '', invite.rawTx)
277 () => `Sending ticket ${invite.ticket} to ${email} failed.`
282 if (mailingErrors.length) {
283 return { [FORM_ERROR]: mailingErrors };
289 const onSubmit = useCallback(
291 const emailCount = values.emails.length;
292 setCount(emailCount);
293 setStatus(STATUS.SENDING);
294 const { errors, invites } = await generateInvites(emailCount);
296 setStatus(STATUS.FAILED);
299 const mailErrors = await sendInvites(values.emails, invites);
300 console.log(mailErrors);
302 console.log(mailErrors);
303 setStatus(STATUS.FAILED);
306 setStatus(STATUS.SUCCESS);
308 [generateInvites, setCount, setStatus, sendInvites]
311 const validateHasReceived = useCallback(
313 const hasReceived = await getHasReceived(email);
315 return HAS_RECEIVED_TEXT;
321 const buttonText = () => {
324 return 'Add to Invite Group';
326 return `Sending ${pluralize(count, 'Invite')}...`;
328 return `✓ ${pluralize(count, 'Invite')} sent`;
335 const renderButton = handleSubmit => {
338 <Grid.Item full as={SubmitButton} handleSubmit={handleSubmit}>
345 const percentProgress = progress / count;
354 progress={percentProgress}>
361 <Grid.Item full className="mt4 p3 bg-green3 white f5">
368 <Grid.Item full className="mt4 p3 bg-red3 white f5">
377 const validateForm = useCallback((values, errors) => {
378 if (hasErrors(errors)) {
382 // check for email uniqenesss
383 const emails = values.emails.filter(d => !!d);
384 if (uniq(emails).length !== emails.length) {
385 return { [FORM_ERROR]: 'Duplicate email.' };
389 const validate = useMemo(
393 emails: buildArrayValidator(buildEmailValidator(validateHasReceived)),
397 [validateHasReceived, validateForm]
400 const initialValues = { emails: [] };
402 <Grid.Item full as={Grid}>
405 initialValues={initialValues}
406 initialValuesEqual={isEqual}
408 {({ handleSubmit, values }) => (
410 {console.log(values) && null}
411 <Grid.Item full as={EmailChipInput} name="emails" className="mt4" />
412 {renderButton(handleSubmit)}
414 <Grid.Item full as={FormError} />
416 <Grid.Item full as={NeedFundsNotice} {...needFunds} />
425const InviteUrl = () => {
426 const { txStatus, needFunds, invites, generateInvites } = useInviter();
428 const [error, setError] = useState();
430 const generateInvite = useCallback(async () => {
431 const errors = await generateInvites(1);
432 if (hasErrors(errors)) {
435 }, [generateInvites]);
437 const renderGenerateButton = () => {
440 <Grid.Item full className="mt4 p3 bg-red3 white f5">
441 Error generating invite
453 onClick={() => generateInvite()}>
467 Generating Invite URL
475 const url = `https://bridge.urbit.org/#${get(invites, '[0].ticket', '')}`;
477 const [doCopy, didCopy] = useCopiable(url);
480 <Grid.Item full as={Grid}>
481 {invites.length > 0 && (
483 <Grid.Item full className="b-gray3 b1 mv4 p1 flex flex-center">
484 <div className="flex1 ml1">{url}</div>
486 className="pv3 ph4 bg-black white"
487 onClick={() => doCopy()}>
488 {didCopy ? 'Copied' : 'Copy'}
492 Your invite link expires after consumption.
496 {invites.length === 0 && renderGenerateButton()}
497 {needFunds && <Grid.Item full as={NeedFundsNotice} {...needFunds} />}
498 {error && <Grid.Item as={ErrorText}>{JSON.stringify(error)}</Grid.Item>}
503const InviteCohort = () => {
504 const [tab, setTab] = useState(NAMES.MAIL);
509 tabClassName="t-center flex1"
525 [NAMES.MAIL]: InviteMail,
526 [NAMES.URL]: InviteUrl,
530 { text: 'Email', value: NAMES.MAIL },
531 { text: 'URL', value: NAMES.URL },
534export default InviteCohort;
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