@@ -1,504 +1,10 @@
1−import React, { useMemo, useCallback, useState, useEffect } from 'react';
1+import React, { useState } from 'react';
2−import { Grid, ErrorText } from 'indigo-react';
2+import { Grid } from 'indigo-react';
3−import { get, uniq, zip, compact, isEqual } from 'lodash';
4−import * as azimuth from 'azimuth-js';
5−import { FORM_ERROR } from 'final-form';
6−import { toWei, toBN } from 'web3-utils';
7−import * as ob from 'urbit-ob';
9−import * as need from 'lib/need';
10−import * as wg from 'lib/walletgen';
11−import * as tank from 'lib/tank';
12−import useCopiable from 'lib/useCopiable';
14import Tabs from 'components/Tabs';
4import Tabs from 'components/Tabs';
15−import ProgressButton from 'components/ProgressButton';
16−import { ForwardButton } from 'components/Buttons';
17−import NeedFundsNotice from 'components/NeedFundsNotice';
19−import { useNetwork } from 'store/network';
20−import { useWallet } from 'store/wallet';
21−import { usePointCursor } from 'store/pointCursor';
22−import { usePointCache } from 'store/pointCache';
26− sendSignedTransaction,
27− waitForTransactionConfirm,
30−import { MIN_PLANET, GAS_LIMITS } from 'lib/constants';
31−import pluralize from 'lib/pluralize';
32−import useGasPrice from 'lib/useGasPrice';
34−import EmailChipInput from 'form/EmailChipInput';
35−import FormError from 'form/FormError';
36−import SubmitButton from 'form/SubmitButton';
37−import BridgeForm from 'form/BridgeForm';
43−} from 'form/validators';
44−import { WARNING } from 'form/helpers';
45−import useMailer from 'lib/useMailer';
47−const GAS_LIMIT = GAS_LIMITS.GIFT_PLANET;
56−const HAS_RECEIVED_TEXT = 'This email has already received an invite.';
58−const 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(
87− async numInvites => {
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)
170− .mul(toBN(gasPrice))
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 };
253−const InviteMail = () => {
6+import InviteMail from 'views/Invite/Email';
254− const { progress, txStatus, needFunds, generateInvites } = useInviter();
7+import InviteUrl from 'views/Invite/Url';
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';
325− case STATUS.SENDING:
326− return `Sending ${pluralize(count, 'Invite')}...`;
327− case STATUS.SUCCESS:
328− return `✓ ${pluralize(count, 'Invite')} sent`;
329− case STATUS.FAILURE:
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}
407− onSubmit={onSubmit}>
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} />
425−const 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()}>
457− case STATUS.SENDING:
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 = () => {
9const InviteCohort = () => {
504 const [tab, setTab] = useState(NAMES.MAIL);
10 const [tab, setTab] = useState(NAMES.MAIL);