@@ -1,348 +1,146 @@
1−import React, { useMemo, useEffect, useCallback, useState } from 'react';
2−import { Grid, Flex, Button } from 'indigo-react';
3−import * as ob from 'urbit-ob';
4−import { Just, Nothing } from 'folktale/maybe';
1+import React, { useEffect, useCallback, useState } from 'react';
2+import { Grid, Button } from 'indigo-react';
53import cn from 'classnames';
4+import * as azimuth from 'azimuth-js';
5+import { Icon, Row, StatelessTextInput } from '@tlon/indigo-react';
66
77import { usePointCursor } from 'store/pointCursor';
88import { usePointCache } from 'store/pointCache';
99import { useWallet } from 'store/wallet';
10−
11−import View from 'components/View';
12−import MaybeSigil from 'components/MaybeSigil';
13−import { matchBlinky } from 'components/Blinky';
10+import { useNetwork } from 'store/network';
11+import { useRollerStore } from 'store/roller';
1412
1513import { useLocalRouter } from 'lib/LocalRouter';
16−import useInvites from 'lib/useInvites';
1714import * as need from 'lib/need';
1815import * as wg from 'lib/walletgen';
16+import useRoller from 'lib/useRoller';
17+import { convertToInt } from 'lib/convertToInt';
18+import { generateUrl } from 'lib/utils/invite';
1919
20−import { ReactComponent as SearchIcon } from 'assets/search.svg';
21−import CopyButton from 'components/copiable/CopyButton';
22−import { useNetwork } from 'store/network';
20+import { useIssueChild } from 'views/IssueChild';
21+import View from 'components/View';
2322import L2BackHeader from 'components/L2/Headers/L2BackHeader';
2423import Window from 'components/L2/Window/Window';
2524import HeaderPane from 'components/L2/Window/HeaderPane';
26−import { Icon, Row, StatelessTextInput } from '@tlon/indigo-react';
2725import BodyPane from 'components/L2/Window/BodyPane';
28−import { useRollerStore } from 'store/roller';
29−import useRoller from 'lib/useRoller';
30−import FeeDropdown from 'components/L2/Dropdowns/FeeDropdown';
3126import LoadingOverlay from 'components/L2/LoadingOverlay';
32−import { generateUrl } from 'lib/utils/invite';
3327import CopiableWithTooltip from 'components/copiable/CopiableWithTooltip';
3428import Paginator from 'components/L2/Paginator';
29+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
3530
3631import './Cohort.scss';
37−import useInviter from 'lib/useInviter';
32+import { getStoredInvites, setStoredInvites } from 'store/storage/roller';
3833
3934const INVITES_PER_PAGE = 7;
4035const DEFAULT_NUM_INVITES = 5;
4136
42−function SearchInput({ className, value, onChange }) {
43− return (
44− <Flex align="center" full className={cn('b b-gray2 b1', className)}>
45− <Flex.Item className="p2">
46− <SearchIcon />
47− </Flex.Item>
48−
49− <Flex.Item
50− flex={1}
51− as={'input'}
52− value={value}
53− onChange={onChange}
54− className="b-none h7"
55− />
56− </Flex>
57− );
58−}
37+export default function InviteCohort() {
38+ const { pop, push, names } = useLocalRouter();
39+ const { authToken } = useWallet();
40+ const {
41+ nextRoll,
42+ currentL2,
43+ pendingTransactions,
44+ invites,
45+ } = useRollerStore();
46+ const { generateInviteCodes, getPendingTransactions } = useRoller();
47+ const { pointCursor } = usePointCursor();
48+ const { contracts } = useNetwork();
49+ const { getDetails } = usePointCache();
5950
60−function CohortMember({ point, pending = false, className, ...rest }) {
61− const { sentInvites } = useInvites(point);
62− const patp = useMemo(() => ob.patp(point), [point]);
63− const colors = pending ? ['#ee892b', '#FFFFFF'] : ['#000000', '#FFFFFF'];
51+ const point = need.point(pointCursor);
52+ const _contracts = need.contracts(contracts);
6453
65− const DetailText = useCallback(
66− () =>
67− pending ? 'Pending' : <> {matchBlinky(sentInvites)} points invited </>,
68− [pending, sentInvites]
69− );
54+ const pointSize = azimuth.azimuth.getPointSize(point);
55+ const isParent = pointSize !== azimuth.azimuth.PointSize.Planet;
56+ const details = need.details(getDetails(point));
57+ const networkRevision = convertToInt(details.keyRevisionNumber, 10);
58+ const networkKeysNotSet = !currentL2 && isParent && networkRevision === 0;
7059
71− return (
72− <Flex justify="between" className={cn('b1 b-gray2', className)} {...rest}>
73− <Flex.Item className="w9 h9">
74− <MaybeSigil patp={Just(patp)} size={64} colors={colors} />
75− </Flex.Item>
60+ const goUrbitOS = useCallback(() => push(names.URBIT_OS), [push, names]);
7661
77− <Flex.Item
78− flex={1}
79− justify="evenly"
80− as={Flex}
81− col
82− className="mono f6 ph4">
83− <Flex.Item>{patp}</Flex.Item>
84− <Flex.Item className="gray4">
85− <DetailText />
86− </Flex.Item>
87− </Flex.Item>
88− </Flex>
62+ const [numInvites, setNumInvites] = useState(
63+ currentL2 ? DEFAULT_NUM_INVITES : 1
8964 );
90−}
91−function CohortList({
92− acceptedPoints,
93− pendingPoints,
94− className,
95− onlyPending,
96− onSelectInvite,
97−}) {
98− const { syncInvites } = usePointCache();
65+ const [showInviteForm, setShowInviteForm] = useState(false);
66+ const [loading, setLoading] = useState(false);
67+ const [error, setError] = useState(null);
68+ const [l1Invite, setL1Invite] = useState(null);
9969
100− const [query, setQuery] = useState('');
70+ const { construct, unconstruct, completed, bind } = useIssueChild();
10171
72+ // Set up the invite spawn if on L1
10273 useEffect(() => {
103− acceptedPoints.map(syncInvites);
104− }, [acceptedPoints, syncInvites]);
105−
106− const handleChange = useCallback(
107− e => {
108− setQuery(e.target.value);
109− e.preventDefault();
110− },
111− [setQuery]
112− );
113−
114− const filterPoints = useCallback(
115− points =>
116− points.filter(p => {
117− const patp = ob.patp(p).slice(1);
118− return patp.startsWith(query);
119− }),
120− [query]
121− );
122− const selectInvite = useCallback(
123− point => () => {
124− onSelectInvite(point);
125− },
126− [onSelectInvite]
127− );
128− const _pendingPoints = filterPoints(pendingPoints);
129− const _acceptedPoints = filterPoints(acceptedPoints);
130−
131− const offset = onlyPending ? 0 : _acceptedPoints.length;
132−
133− return (
134− <Grid gap={3} className={cn('mt4', className)}>
135− <Grid.Item full as={SearchInput} value={query} onChange={handleChange} />
136− <>
137− {!onlyPending &&
138− _acceptedPoints.map((p, idx) => (
139− <Grid.Item
140− key={p}
141− half={(idx % 2) + 1}
142− as={CohortMember}
143− point={p}
144− onClick={selectInvite(p)}
145− />
146− ))}
147− {_pendingPoints.map((p, idx) => (
148− <Grid.Item
149− key={p}
150− half={((offset + idx) % 2) + 1}
151− as={CohortMember}
152− point={p}
153− pending
154− onClick={selectInvite(p)}
155− />
156− ))}
157− </>
158−
159− {_pendingPoints.length === 0 &&
160− (_acceptedPoints.length === 0 || onlyPending) && (
161− <Grid.Item full className="p4 t-center">
162− {' '}
163− No invites accepted yet.
164− </Grid.Item>
165− )}
166− </Grid>
167− );
168−}
169−
170−function hideIf(hider, hidden) {
171− return hider ? '●●●●●●●' : hidden;
172−}
74+ const _authToken = authToken.getOrElse(null);
17375
174−function CohortMemberExpanded({ point, className, ...rest }) {
175− const { sentInvites, acceptedInvites } = useInvites(point);
176− const { getDetails } = usePointCache();
177− const { authToken } = useWallet();
178− const { contracts } = useNetwork();
179− const details = getDetails(point);
180− const { active } = details.getOrElse({});
181− const patp = useMemo(() => ob.patp(point), [point]);
182− const colors = useMemo(
183− () => (!active ? ['#ee892b', '#FFFFFF'] : ['#000000', '#FFFFFF']),
184− [active]
185− );
186−
187− const [code, setCode] = useState(Nothing());
188− const [codeVisible, setCodeVisible] = useState(false);
76+ const setUpInvite = async () => {
77+ setLoading(true);
78+ const [invitePoint] = await azimuth.azimuth.getUnspawnedChildren(
79+ _contracts,
80+ point
81+ );
18982
190− useEffect(() => {
191− const fetchWallet = async () => {
192− const _authToken = authToken.getOrElse(null);
193− const _details = details.getOrElse(null);
194− const _contracts = contracts.getOrElse(null);
195− if (
196− !_authToken ||
197− !_details ||
198− _details.active ||
199− !_contracts ||
200− !codeVisible
201− ) {
202− setCode(Nothing());
203− return;
204− }
83+ // TODO: figure out how to store the ticket and invite info
20584 const { ticket, owner } = await wg.generateTemporaryDeterministicWallet(
20685 point,
20786 _authToken
20887 );
20988
210− if (owner.keys.address !== _details.transferProxy) {
211− setCode(Just(null));
212− } else {
213− setCode(Just(ticket));
214− }
89+ construct(invitePoint, owner.keys.address);
90+ setL1Invite({ ticket, planet: invitePoint });
91+ setLoading(false);
21592 };
216− fetchWallet();
217− }, [authToken, codeVisible, contracts, details, point]);
218−
219− useEffect(() => {
220− setCodeVisible(false);
221− }, [point]);
222−
223− return (
224− <Grid justify="between" className={cn('b1 b-gray2', className)} {...rest}>
225− <Grid.Item rows={[1, 4]} cols={[1, 4]} className="h10 w10">
226− <MaybeSigil patp={Just(patp)} size={64} colors={colors} />
227− </Grid.Item>
228−
229− <Grid.Item cols={[4, 13]} className="mono mt-auto ">
230− {patp}
231− </Grid.Item>
232−
233− {!active && (
234− <>
235− <Grid.Item
236− cols={[4, 13]}
237− className="mt-auto mb1 f6 gray4"
238− as={Flex}
239− col>
240− Invite Code
241− </Grid.Item>
242− <Grid.Item cols={[4, 10]} className="mb-auto mt1 f6 gray4">
243− {(code.getOrElse(true) === null &&
244− 'This invite code cannot be recovered') ||
245− hideIf(!codeVisible, matchBlinky(code))}
246− </Grid.Item>
247− {!codeVisible && (
248− <Grid.Item
249− cols={[10, 13]}
250− className="mh-auto f6 underline pointer"
251− onClick={() => setCodeVisible(true)}>
252− Show
253− </Grid.Item>
254− )}
255− {codeVisible && code.getOrElse('') !== null && (
256− <Grid.Item
257− className="mh-auto f6 t-right"
258− cols={[10, 13]}
259− as={CopyButton}
260− text={code.getOrElse('')}
261− />
262− )}
263− </>
264− )}
265−
266− {active && (
267− <>
268− <Grid.Item
269− cols={[4, 13]}
270− className="mt-auto mb1 f6 gray4"
271− as={Flex}
272− col>
273− {matchBlinky(sentInvites)} invites sent
274− </Grid.Item>
275− <Grid.Item
276− cols={[4, 13]}
277− className="mb-auto mt1 f6 gray4"
278− as={Flex}
279− col>
280− {matchBlinky(acceptedInvites)} invites accepted
281− </Grid.Item>
282− </>
283− )}
284− </Grid>
285− );
286−}
28793
288−export default function InviteCohort() {
289− const { pop } = useLocalRouter();
290− const {
291− nextRoll,
94+ if (!currentL2 && _contracts && point && _authToken && showInviteForm) {
95+ setUpInvite();
96+ } else {
97+ unconstruct();
98+ }
99+ }, [
292100 currentL2,
293− pendingTransactions,
294− invites,
295− } = useRollerStore();
296− const { generateInvites } = useInviter();
297− const { generateInviteCodes, getPendingTransactions } = useRoller();
298−
299− const { pointCursor } = usePointCursor();
300− const point = need.point(pointCursor);
101+ point,
102+ _contracts,
103+ authToken,
104+ construct,
105+ showInviteForm,
106+ unconstruct,
107+ ]);
301108
302− const [page, setPage] = useState(0);
109+ const l1Complete = useCallback(() => {
110+ const { available, pending, claimed } = getStoredInvites(point);
111+ setStoredInvites(point, {
112+ available: [...available, l1Invite],
113+ pending,
114+ claimed,
115+ });
116+ setShowInviteForm(false);
117+ }, [l1Invite, point]);
303118
304− const { pendingPoints } = useInvites(point);
119+ useEffect(() => (completed ? l1Complete() : null), [completed, l1Complete]);
305120
306− const displayedInvites = currentL2 ? invites : pendingPoints.getOrElse([]);
121+ const [page, setPage] = useState(0);
307122
308− const invitesToDisplay = displayedInvites.slice(
123+ const invitesToDisplay = invites.slice(
309124 page * INVITES_PER_PAGE,
310125 (page + 1) * INVITES_PER_PAGE
311126 );
312127
313128 const hasPending = Boolean(pendingTransactions.length);
314− const hasInvites = Boolean(displayedInvites.length);
315−
316− const [numInvites, setNumInvites] = useState(DEFAULT_NUM_INVITES);
317− const [showInviteForm, setShowInviteForm] = useState(false);
318− const [loading, setLoading] = useState(false);
319− const [error, setError] = useState(null);
129+ const hasInvites = Boolean(invites.length);
320130
321131 const createInvites = useCallback(async () => {
322132 setLoading(true);
323133 try {
324− if (currentL2) {
325− const invites = await generateInviteCodes(numInvites);
326− console.log('INVITES', invites);
327− getPendingTransactions(point);
328− } else {
329− const invites = await generateInvites(numInvites);
330− console.log('INVITES', invites);
331− }
332− setShowInviteForm(false);
134+ const invites = await generateInviteCodes(numInvites);
135+ console.log('L2 INVITES', invites);
136+ getPendingTransactions(point);
137+ pop();
333138 } catch (error) {
334139 setError(error);
335140 } finally {
336141 setLoading(false);
337142 }
338− }, [
339− generateInviteCodes,
340− numInvites,
341− getPendingTransactions,
342− point,
343− generateInvites,
344− currentL2,
345− ]);
143+ }, [generateInviteCodes, numInvites, getPendingTransactions, point, pop]);
346144
347145 const getContent = () => {
348146 if (hasInvites) {
@@ -378,6 +176,18 @@ export default function InviteCohort() {
378176 Check back in <span className="timer"> {nextRoll}</span>
379177 </>
380178 );
179+ } else if (networkKeysNotSet) {
180+ return (
181+ <>
182+ This star's network keys are not set.
183+ <br />
184+ Either set the network keys or{' '}
185+ <span className="migrate" onClick={() => null}>
186+ {/* TODO: change this to navigate to migration */}
187+ migrate this star to L2.
188+ </span>
189+ </>
190+ );
381191 }
382192
383193 return (
@@ -398,8 +208,8 @@ export default function InviteCohort() {
398208 const header = (
399209 <h5>
400210 You have
401− <span className="number-emphasis"> {displayedInvites.length} </span>
402− Planet Code{displayedInvites.length === 1 ? '' : 's'}
211+ <span className="number-emphasis"> {invites.length} </span>
212+ Planet Code{invites.length === 1 ? '' : 's'}
403213 </h5>
404214 );
405215
@@ -415,18 +225,29 @@ export default function InviteCohort() {
415225 </HeaderPane>
416226 <BodyPane>
417227 <div className="upper">
418− <Row className="points-input">
419− I want to generate
420− <StatelessTextInput
421− className="input-box"
422− value={numInvites}
423− maxLength="3"
424− onChange={e =>
425− setNumInvites(Number(e.target.value.replace(/\D/g, '')))
426− }
427− />
428− planet invite code{numInvites > 1 ? 's' : ''}
429− </Row>
228+ {currentL2 ? (
229+ <Row className="points-input">
230+ I want to generate
231+ <StatelessTextInput
232+ className="input-box"
233+ value={numInvites}
234+ maxLength="3"
235+ onChange={e =>
236+ setNumInvites(Number(e.target.value.replace(/\D/g, '')))
237+ }
238+ />
239+ planet invite code{numInvites > 1 ? 's' : ''}
240+ </Row>
241+ ) : (
242+ <div className="migration-prompt">
243+ ETH fees are very high currently. You can spawn invites for
244+ free after{' '}
245+ <span className="migrate" onClick={() => null}>
246+ {/* TODO: change this to navigate to migration */}
247+ migrating this star to L2.
248+ </span>
249+ </div>
250+ )}
430251 {/* <div>Destination</div>
431252 <div className="ship-or-address">Ship or ethereum address</div>
432253 <Dropdown value={dropdownValue}>
@@ -439,22 +260,30 @@ export default function InviteCohort() {
439260 </div>
440261 {/* <Inviter /> */}
441262 <div className="lower">
442− <Row className="next-roll">
443− <span>{currentL2 ? 'Next Roll in' : 'Transaction Fee'}</span>
444− {currentL2 ? (
263+ {currentL2 && (
264+ <Row className="next-roll">
265+ <span>Next Roll in</span>
445266 <span className="timer">{nextRoll}</span>
446− ) : (
447− <FeeDropdown />
448− )}
449− </Row>
450− <Button
451− as={'button'}
452− className="generate-codes"
453− center
454− solid
455− onClick={createInvites}>
456− Generate Planet Codes ({numInvites})
457− </Button>
267+ </Row>
268+ )}
269+ {currentL2 ? (
270+ <Button
271+ as={'button'}
272+ className="generate-codes"
273+ center
274+ solid
275+ onClick={createInvites}>
276+ Generate Planet Code{currentL2 ? `s (${numInvites})` : ''}
277+ </Button>
278+ ) : (
279+ <Grid.Item
280+ full
281+ as={InlineEthereumTransaction}
282+ label="Generate Planet Code"
283+ {...bind}
284+ onReturn={() => pop()}
285+ />
286+ )}
458287 </div>
459288 </BodyPane>
460289 </Window>
@@ -463,8 +292,15 @@ export default function InviteCohort() {
463292 );
464293 }
465294
295+ const showGenerateButton = !hasPending && !hasInvites && !networkKeysNotSet;
296+
466297 return (
467− <View pop={pop} inset className="cohort" hideBack header={<L2BackHeader />}>
298+ <View
299+ pop={pop}
300+ inset
301+ className="cohort"
302+ hideBack
303+ header={<L2BackHeader hideBalance />}>
468304 <Window>
469305 <HeaderPane>
470306 {!hasInvites ? (
@@ -483,7 +319,7 @@ export default function InviteCohort() {
483319 <div className={`content ${!hasInvites ? 'center' : ''}`}>
484320 {getContent()}
485321 </div>
486− {!hasPending && !hasInvites && (
322+ {showGenerateButton && (
487323 <Button
488324 as={'button'}
489325 className="generate-button"
@@ -493,11 +329,21 @@ export default function InviteCohort() {
493329 Generate Codes
494330 </Button>
495331 )}
332+ {networkKeysNotSet && (
333+ <Button
334+ as={'button'}
335+ className="generate-button"
336+ center
337+ solid
338+ onClick={goUrbitOS}>
339+ Set Network Keys
340+ </Button>
341+ )}
496342 {hasInvites && (
497343 <Paginator
498344 page={page}
499345 numPerPage={INVITES_PER_PAGE}
500− numElements={displayedInvites.length}
346+ numElements={invites.length}
501347 goPrevious={() => setPage(page - 1)}
502348 goNext={() => setPage(page + 1)}
503349 />