on master help sign in
invites,router: removing unused l1 invites, converting router to TS
commit7b122b6c90895c15a88d74d6df71f7cdd3eb9905 authorHunter Miller <hunter@hmiller.dev> dateTue 1 Mar 2022, 12:43 (5 years ago) parent81e5df5
8 files changed+303−348
src/lib/LocalRouter.js → src/lib/LocalRouter.ts+2−1 src/lib/routeNames.js−40 src/lib/routeNames.ts+41 src/lib/router.js → src/lib/router.ts+6−4 src/lib/useRouter.js → src/lib/useRouter.tsx+54−13 src/store/history.js → src/store/history.tsx+23−6 src/views/Invite/Cohort.tsx+10−284 src/views/Invite/GenerateInvites.tsx+167
src/lib/LocalRouter.js → src/lib/LocalRouter.tsrenamed+2−1
@@ -1,6 +1,7 @@
11import { createContext, useContext } from 'react';
2+import { Router } from './useRouter';
23
3−export const LocalRouterContext = createContext(null);
4+export const LocalRouterContext = createContext<Router>({} as Router);
45export const LocalRouterProvider = LocalRouterContext.Provider;
56
67// Hook version
src/lib/routeNames.jsdeleted−40
@@ -1,40 +0,0 @@
1−export const ROUTE_NAMES = {
2− DEFAULT: Symbol('DEFAULT'),
3− DISCLAIMER: Symbol('DISCLAIMER'),
4− //
5− ACTIVATE: Symbol('ACTIVATE'),
6− INVITE_TRANSACTIONS: Symbol('INVITE_TRANSACTIONS'),
7− INVITES_MANAGE: Symbol('INVITES_MANAGE'),
8− //
9− LOGIN: Symbol('LOGIN'),
10− LOGIN_OTHER: Symbol('OTHER'),
11− REDOWNLOAD: Symbol('REDOWNLOAD'),
12− RETICKET: Symbol('RETICKET'),
13− PERMISSIONS: Symbol('PERMISSIONS'),
14− //
15− ACCEPT_TRANSFER: Symbol('ACCEPT_TRANSFER'),
16− CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
17− POINTS: Symbol('POINTS'),
18− POINT: Symbol('POINT'),
19− SIGIL_GENERATOR: Symbol('SIGIL_GENERATOR'),
20− CREATE_GALAXY: Symbol('CREATE_GALAXY'),
21− SENATE: Symbol('SENATE'),
22− DOCUMENTS: Symbol('DOCUMENTS'),
23− UPGRADES: Symbol('UPGRADES'),
24− ISSUE_CHILD: Symbol('ISSUE_CHILD'),
25− PARTY_SET_POOL_SIZE: Symbol('PARTY_SET_POOL_SIZE'),
26− INVITE_COHORT: Symbol('INVITE_COHORT'),
27− STAR_RELEASE: Symbol('STAR_RELEASE'),
28− ACCEPT_LOCKUP: Symbol('ACCEPT_LOCKUP'),
29− URBIT_OS: Symbol('URBIT_OS'),
30− URBIT_ID: Symbol('URBIT_ID'),
31− RESIDENTS: Symbol('RESIDENTS'),
32− REQUESTS: Symbol('REQUESTS'),
33− ADOPT: Symbol('ADOPT'),
34− BITCOIN: Symbol('BITCOIN'),
35− BITCOIN_XPUB: Symbol('BITCOIN_XPUB'),
36− BITCOIN_SIGN_TRANSACTION: Symbol('BITCOIN_SIGN_TRANSACTION'),
37− TRANSACTION_HISTORY: Symbol('TRANSACTION_HISTORY'),
38− MIGRATE_L2: Symbol('MIGRATE_L2'),
39− OPS: Symbol('OPS'),
40−};
src/lib/routeNames.tsnew file+41
@@ -0,0 +1,41 @@
1+export const ROUTE_NAMES = {
2+ DEFAULT: 'DEFAULT',
3+ DISCLAIMER: 'DISCLAIMER',
4+ //
5+ ACTIVATE: 'ACTIVATE',
6+ INVITE_TRANSACTIONS: 'INVITE_TRANSACTIONS',
7+ INVITES_MANAGE: 'INVITES_MANAGE',
8+ //
9+ LOGIN: 'LOGIN',
10+ LOGIN_OTHER: 'OTHER',
11+ REDOWNLOAD: 'REDOWNLOAD',
12+ RETICKET: 'RETICKET',
13+ PERMISSIONS: 'PERMISSIONS',
14+ //
15+ ACCEPT_TRANSFER: 'ACCEPT_TRANSFER',
16+ CANCEL_TRANSFER: 'CANCEL_TRANSFER',
17+ POINTS: 'POINTS',
18+ POINT: 'POINT',
19+ SIGIL_GENERATOR: 'SIGIL_GENERATOR',
20+ CREATE_GALAXY: 'CREATE_GALAXY',
21+ SENATE: 'SENATE',
22+ DOCUMENTS: 'DOCUMENTS',
23+ UPGRADES: 'UPGRADES',
24+ ISSUE_CHILD: 'ISSUE_CHILD',
25+ PARTY_SET_POOL_SIZE: 'PARTY_SET_POOL_SIZE',
26+ INVITE_COHORT: 'INVITE_COHORT',
27+ GENERATE_INVITES: 'GENERATE_INVITES',
28+ STAR_RELEASE: 'STAR_RELEASE',
29+ ACCEPT_LOCKUP: 'ACCEPT_LOCKUP',
30+ URBIT_OS: 'URBIT_OS',
31+ URBIT_ID: 'URBIT_ID',
32+ RESIDENTS: 'RESIDENTS',
33+ REQUESTS: 'REQUESTS',
34+ ADOPT: 'ADOPT',
35+ BITCOIN: 'BITCOIN',
36+ BITCOIN_XPUB: 'BITCOIN_XPUB',
37+ BITCOIN_SIGN_TRANSACTION: 'BITCOIN_SIGN_TRANSACTION',
38+ TRANSACTION_HISTORY: 'TRANSACTION_HISTORY',
39+ MIGRATE_L2: 'MIGRATE_L2',
40+ OPS: 'OPS',
41+};
src/lib/router.js → src/lib/router.tsrenamed+6−4
@@ -15,19 +15,20 @@ import Point from 'views/Point/Point';
1515import UrbitOS from 'views/Point/UrbitOS';
1616import UrbitID from 'views/Point/UrbitID';
1717import { Residents } from 'views/Ops/Residents';
18−import MigrateL2 from 'views/Point/MigrateL2.tsx';
18+import MigrateL2 from 'views/Point/MigrateL2';
1919import IssueChild from 'views/Point/IssueChild';
2020
2121import PartySetPoolSize from 'views/Party/PartySetPoolSize';
2222import InviteCohort from 'views/Invite/Cohort';
23−import AcceptTransfer from 'views/AcceptTransfer.tsx';
24−import CancelTransfer from 'views/CancelTransfer.tsx';
25−import Bitcoin from 'views/Bitcoin/Bitcoin.tsx';
23+import AcceptTransfer from 'views/AcceptTransfer';
24+import CancelTransfer from 'views/CancelTransfer';
25+import Bitcoin from 'views/Bitcoin/Bitcoin';
2626import TransactionHistory from 'views/Transactions/TransactionHistory';
2727
2828import { ROUTE_NAMES } from './routeNames';
2929import { Ops } from 'views/Ops/Ops';
3030import { Requests } from 'views/Ops/Requests';
31+import { GenerateInvites } from 'views/Invite/GenerateInvites';
3132
3233export const ROUTES = {
3334 [ROUTE_NAMES.DISCLAIMER]: Disclaimer,
@@ -47,6 +48,7 @@ export const ROUTES = {
4748 // TODO: replace this with deep link to AdminCancelTransfer
4849 [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
4950 [ROUTE_NAMES.INVITE_COHORT]: InviteCohort,
51+ [ROUTE_NAMES.GENERATE_INVITES]: GenerateInvites,
5052 [ROUTE_NAMES.STAR_RELEASE]: StarRelease,
5153 [ROUTE_NAMES.ACCEPT_LOCKUP]: AcceptLockup,
5254 [ROUTE_NAMES.URBIT_OS]: UrbitOS,
src/lib/useRouter.js → src/lib/useRouter.tsxrenamed+54−13
@@ -1,21 +1,53 @@
11import { useCallback, useMemo, useState, useRef, useEffect } from 'react';
22import { last, includes as _includes, findIndex, get } from 'lodash';
3+import { ROUTES } from './router';
4+import { ROUTE_NAMES } from './routeNames';
35
46const NULL_DATA = {};
57
8+type RouteKey = keyof typeof ROUTES | string;
9+export interface Route {
10+ key: RouteKey;
11+ data?: any;
12+}
13+
14+interface UseRouterParams {
15+ names: Record<string, string>;
16+ views: any;
17+ initialRoutes: Route[];
18+}
19+
20+type OnPopStateHandler = (ev: PopStateEvent) => any;
21+
22+export interface Router {
23+ Route: any;
24+ names: typeof ROUTE_NAMES | Record<string, string>;
25+ routes: Route[];
26+ push: (key: RouteKey, data?: any) => void;
27+ popAndPush: (key: RouteKey, data?: any) => void;
28+ pop: () => void;
29+ popTo: (key: string) => void;
30+ replaceWith: (routes: Route[]) => void;
31+ reset: () => void;
32+ peek: () => Route | undefined;
33+ size: number;
34+ includes: (key: RouteKey) => boolean;
35+ data: any;
36+}
37+
638/**
739 * @param names map of string to view key, used for consumer references
840 * @param views map of view keys to react component views
941 * @param initialRoutes initial set of routes following the { key, data } format
1042 */
1143export default function useRouter({
12− names = {},
44+ names = {} as typeof ROUTE_NAMES,
1345 views = {},
1446 initialRoutes = [],
15−}) {
47+}: UseRouterParams): Router {
1648 const [routes, setRoutes] = useState(initialRoutes);
1749
18− const oldPopState = useRef();
50+ const oldPopState = useRef<OnPopStateHandler>();
1951
2052 const size = routes.length;
2153 const push = useCallback(
@@ -53,7 +85,7 @@ export default function useRouter({
5385 if (size <= 1) {
5486 // if we are at the root, pass this event to our parent
5587 if (oldPopState.current) {
56− oldPopState.current();
88+ oldPopState.current({} as PopStateEvent);
5789 }
5890 return;
5991 }
@@ -63,7 +95,7 @@ export default function useRouter({
6395 // TODO: allow the user to go forward by storing our data in history
6496 // and using the url for other state
6597 // (or just importing an actually good router lib)
66− window.history.pushState(null, null, null);
98+ window.history.pushState(null, '', null);
6799
68100 // pop as expected
69101 return setRoutes(routes => [...routes.slice(0, routes.length - count)]);
@@ -78,15 +110,16 @@ export default function useRouter({
78110 [pop, routes]
79111 );
80112 const peek = useCallback(() => last(routes), [routes]);
81− const replaceWith = useCallback(routes => setRoutes(() => routes), [
82− setRoutes,
83− ]);
113+ const replaceWith = useCallback(
114+ (routes: Route[]) => setRoutes(() => routes),
115+ [setRoutes]
116+ );
84117 const reset = useCallback(() => setRoutes(initialRoutes), [
85118 setRoutes,
86119 initialRoutes,
87120 ]);
88121 const includes = useCallback(
89− key =>
122+ (key: RouteKey) =>
90123 _includes(
91124 routes.map(r => r.key),
92125 key
@@ -96,7 +129,15 @@ export default function useRouter({
96129 const data = useMemo(() => {
97130 return get(last(routes), 'data', NULL_DATA);
98131 }, [routes]);
99− const Route = useMemo(() => views[peek().key], [views, peek]);
132+ const Route = useMemo(() => {
133+ const key = peek()?.key;
134+
135+ if (!key) {
136+ return null;
137+ }
138+
139+ return views[key];
140+ }, [views, peek]);
100141
101142 // Scroll to top of page with each route transition
102143 useEffect(() => {
@@ -105,13 +146,13 @@ export default function useRouter({
105146
106147 useEffect(() => {
107148 // on router mount, register new state with browser
108− window.history.pushState(null, null, null);
149+ window.history.pushState(null, '', null);
109150 }, []);
110151
111152 // capture browser pop in primary router
112153 useEffect(() => {
113154 // store the previous onpopstate handler
114− oldPopState.current = window.onpopstate;
155+ oldPopState.current = window.onpopstate?.bind(window) as OnPopStateHandler;
115156
116157 // construct new onpopstate handler
117158 window.onpopstate = e => {
@@ -121,7 +162,7 @@ export default function useRouter({
121162
122163 return () => {
123164 // reset the onpopstate to the previous version
124− window.onpopstate = oldPopState.current;
165+ window.onpopstate = oldPopState.current as typeof window.onpopstate;
125166 };
126167 }, [size, pop, oldPopState]);
127168
src/store/history.js → src/store/history.tsxrenamed+23−6
@@ -1,7 +1,13 @@
1−import React, { createContext, forwardRef, useContext } from 'react';
1+import React, {
2+ createContext,
3+ forwardRef,
4+ FunctionComponent,
5+ useContext,
6+} from 'react';
27
3−import useRouter from 'lib/useRouter';
8+import useRouter, { Route, Router } from 'lib/useRouter';
49import { LocalRouterProvider } from 'lib/LocalRouter';
10+import { ROUTE_NAMES } from 'lib/routeNames';
511
612/**
713 * `useHistory` is just a global `useRouter`
@@ -9,9 +15,20 @@ import { LocalRouterProvider } from 'lib/LocalRouter';
915 * (see Invite.js for an example)
1016 */
1117
12−export const HistoryContext = createContext(null);
18+export const HistoryContext = createContext<Router>({} as Router);
1319
14−export function HistoryProvider({ names, views, initialRoutes, children }) {
20+interface HistoryProviderProps {
21+ names: typeof ROUTE_NAMES;
22+ views: any;
23+ initialRoutes: Route[];
24+}
25+
26+export const HistoryProvider: FunctionComponent<HistoryProviderProps> = ({
27+ names,
28+ views,
29+ initialRoutes,
30+ children,
31+}) => {
1532 const router = useRouter({ names, views, initialRoutes });
1633
1734 return (
@@ -19,7 +36,7 @@ export function HistoryProvider({ names, views, initialRoutes, children }) {
1936 <LocalRouterProvider value={router}>{children}</LocalRouterProvider>
2037 </HistoryContext.Provider>
2138 );
22−}
39+};
2340
2441// Hook version
2542export function useHistory() {
@@ -27,7 +44,7 @@ export function useHistory() {
2744}
2845
2946// HOC version
30−export const withHistory = Component =>
47+export const withHistory = (Component: any) =>
3148 forwardRef((props, ref) => (
3249 <HistoryContext.Consumer>
3350 {router => <Component ref={ref} history={router} {...props} />}
src/views/Invite/Cohort.tsx+10−284
@@ -1,185 +1,51 @@
1−import React, { useEffect, useCallback, useMemo, useState } from 'react';
2−import { Grid, Button } from 'indigo-react';
3−import * as azimuth from 'azimuth-js';
4−import {
5− Icon,
6− Row,
7− StatelessTextInput,
8− Box,
9− LoadingSpinner,
10−} from '@tlon/indigo-react';
1+import { useCallback, useState } from 'react';
2+import { Button } from 'indigo-react';
3+import { Icon, Row, Box, LoadingSpinner } from '@tlon/indigo-react';
114
12−import { usePointCache } from 'store/pointCache';
13−import { useWallet } from 'store/wallet';
14−import { useNetwork } from 'store/network';
155import { useRollerStore } from 'store/rollerStore';
16−import { getStoredInvites } from 'store/storage/roller';
17−
186import { useLocalRouter } from 'lib/LocalRouter';
19−import * as need from 'lib/need';
20−import useRoller from 'lib/useRoller';
217import {
228 generateUrl,
239 generateUrlAbbreviation,
2410 generateCsvLine,
2511 generateCsvName,
2612} from 'lib/utils/invite';
27−import {
28− DEFAULT_NUM_INVITES,
29− ETH_ZERO_ADDR,
30− INVITES_PER_PAGE,
31− DEFAULT_CSV_NAME,
32−} from 'lib/constants';
33−import { isPlanet } from 'lib/utils/point';
34−import { generateInviteWallet } from 'lib/utils/roller';
13+import { INVITES_PER_PAGE, DEFAULT_CSV_NAME } from 'lib/constants';
3514import { useTimerStore } from 'store/timerStore';
3615import { Invite } from 'lib/types/Invite';
3716import { ddmmmYYYY } from 'lib/utils/date';
38−import { isExternalWallet } from 'lib/utils/wallet';
3917
40−import { useIssueChild } from 'views/Point/IssueChild';
4118import View from 'components/View';
4219import L2BackHeader from 'components/L2/Headers/L2BackHeader';
4320import Window from 'components/L2/Window/Window';
4421import HeaderPane from 'components/L2/Window/HeaderPane';
4522import BodyPane from 'components/L2/Window/BodyPane';
46−import LoadingOverlay from 'components/L2/LoadingOverlay';
4723import CopiableWithTooltip from 'components/copiable/CopiableWithTooltip';
4824import Paginator from 'components/L2/Paginator';
49−import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
5025
5126import './Cohort.scss';
5227import Modal from 'components/L2/Modal';
53−import {
54− InviteGeneratingStatus,
55− useInvites,
56− useInviteStore,
57−} from './useInvites';
58−import { GeneratingModal } from './GeneratingModal';
59−
60−interface L1Invite {
61− ticket: string;
62− planet: number;
63−}
28+import { useInvites, useInviteStore } from './useInvites';
6429
6530export default function InviteCohort() {
66− const { pop }: any = useLocalRouter();
67− const { authToken, walletType }: any = useWallet();
31+ const { names, pop, push } = useLocalRouter();
6832 const { point, pendingTransactions, nextQuotaTime } = useRollerStore();
6933
7034 const { nextRoll } = useTimerStore();
7135
72− const { ls, getPendingTransactions, getAndUpdatePoint } = useRoller();
73− const { contracts }: any = useNetwork();
74− const { syncControlledPoints }: any = usePointCache();
75−
76− const { invites, generatingNum, setInvites } = useInviteStore();
77− const { getInvites, generateInviteCodes } = useInvites();
36+ const { invites, generatingNum } = useInviteStore();
37+ const { getInvites } = useInvites();
7838 const currentL2 = Boolean(point.isL2Spawn);
7939
80− const _contracts = need.contracts(contracts);
81−
82− const [numInvites, setNumInvites] = useState(
83− currentL2 ? DEFAULT_NUM_INVITES : 1
84− );
85− const [showInviteForm, setShowInviteForm] = useState(false);
86− const [generatingStatus, setGeneratingStatus] = useState<
87− InviteGeneratingStatus
88− >('initial');
8940 const [loading, setLoading] = useState(false);
9041 const [showCsvModal, setShowCsvModal] = useState(false);
9142 const [csvElement, setCsvElement] = useState<HTMLAnchorElement | null>();
9243 // TODO: do we need to read this error? currently unused
9344 const [error, setError] = useState('');
94− const [l1Invite, setL1Invite] = useState<L1Invite | null>(null);
9545 const [page, setPage] = useState(0);
9646
97− const { construct, unconstruct, completed, bind } = useIssueChild();
9847 const invitePoints = invites[point.value];
9948
100− // Set up the invite spawn if on L1
101− useEffect(() => {
102− const _authToken = authToken.getOrElse(null);
103−
104− if (!_authToken) {
105− return;
106− }
107−
108− const setUpInvite = async () => {
109− setLoading(true);
110− const possiblePoints = await azimuth.azimuth.getUnspawnedChildren(
111− _contracts,
112− point
113− );
114−
115− let invitePoint;
116− for (let i = 0; i < possiblePoints.length; i++) {
117− const p = possiblePoints[i];
118− if (
119− !invitePoints.find(({ planet }) => planet === p) &&
120− isPlanet(p) &&
121− (await azimuth.azimuth.getOwner(_contracts, p)) === ETH_ZERO_ADDR
122− ) {
123− invitePoint = p;
124− break;
125− }
126− }
127−
128− if (invitePoint) {
129− useInviteStore.setState({ generatingNum: 1 });
130− const { ticket, inviteWallet } = await generateInviteWallet(
131− invitePoint,
132− _authToken
133− );
134−
135− construct(invitePoint, inviteWallet.ownership.keys.address);
136− setL1Invite({ ticket, planet: invitePoint });
137− } else {
138− setError('No available planets');
139− }
140−
141− setLoading(false);
142− };
143−
144− if (!currentL2 && _contracts && point && _authToken && showInviteForm) {
145− setUpInvite();
146− } else if (!currentL2 && !showInviteForm) {
147− unconstruct();
148− }
149− }, [
150− currentL2,
151− point,
152− _contracts,
153− authToken,
154− construct,
155− showInviteForm,
156− unconstruct,
157− invitePoints,
158− ]);
159−
160− useEffect(() => {
161− const storedInvites = getStoredInvites(ls);
162−
163− if (
164− !currentL2 ||
165− !completed ||
166− !l1Invite ||
167− storedInvites[l1Invite.planet]
168− ) {
169− return;
170− }
171−
172− syncControlledPoints();
173− setInvites(point.value, [
174− ...invitePoints,
175− { ...l1Invite, hash: '', owner: '' },
176− ]);
177− setShowInviteForm(false);
178− setL1Invite(null);
179− unconstruct();
180− pop();
181− }, [completed, currentL2, point]); // eslint-disable-line react-hooks/exhaustive-deps
182−
18349 const invitesToDisplay = invitePoints.slice(
18450 page * INVITES_PER_PAGE,
18551 (page + 1) * INVITES_PER_PAGE
@@ -188,32 +54,6 @@ export default function InviteCohort() {
18854 const hasPending = Boolean(pendingTransactions.length);
18955 const hasInvites = Boolean(invitePoints.length);
19056
191− const createInvites = useCallback(async () => {
192− setGeneratingStatus('generating');
193− try {
194− await generateInviteCodes(numInvites);
195− getPendingTransactions();
196− getAndUpdatePoint(point.value);
197− setGeneratingStatus('finished');
198− } catch (error) {
199− if (typeof error === 'object' && (error as Error)?.message) {
200− setError((error as Error).message);
201− } else if (typeof error === 'string') {
202− setError(error);
203− } else {
204− setError('Generating invites failed');
205− }
206− setGeneratingStatus('errored');
207− }
208− }, [
209− generateInviteCodes,
210− numInvites,
211− getPendingTransactions,
212− getAndUpdatePoint,
213− point,
214− setGeneratingStatus,
215− ]);
216−
21757 const generateCsv = useCallback(() => {
21858 const generateCsvElement = async () => {
21959 setLoading(true);
@@ -252,8 +92,6 @@ export default function InviteCohort() {
25292 setPage(page + 1);
25393 }, [page]);
25494
255− const overQuota = numInvites > point.l2Quota && numInvites > 0;
256−
25795 const getContent = () => {
25896 if (hasInvites) {
25997 return (
@@ -325,118 +163,6 @@ export default function InviteCohort() {
325163 </h5>
326164 );
327165
328− if (showInviteForm) {
329− const generateDisabled = loading || overQuota;
330− const showTxnNote = isExternalWallet(walletType);
331−
332− return (
333− <View
334− pop={() => setShowInviteForm(false)}
335− className="cohort show-invite-form"
336− hideBack
337− header={<L2BackHeader back={() => setShowInviteForm(false)} />}>
338− <Window>
339− <HeaderPane>
340− <h5>Generate Invite Codes</h5>
341− </HeaderPane>
342− <BodyPane>
343− <Box className="upper">
344− {currentL2 ? (
345− <Row className="points-input">
346− I want to generate
347− <StatelessTextInput
348− className={`input-box ${overQuota ? 'over-quota' : ''}`}
349− value={numInvites}
350− maxLength={3}
351− onChange={e => {
352− const target = e.target as HTMLInputElement;
353− setNumInvites(Number(target.value.replace(/\D/g, '')));
354− }}
355− />
356− planet invite code{numInvites > 1 ? 's' : ''}
357− </Row>
358− ) : (
359− <Box className="migration-prompt">
360− ETH fees are very high currently. You can spawn invites for
361− free after{' '}
362− <span className="migrate" onClick={() => null}>
363− {/* TODO: change this to navigate to migration */}
364− migrating this star to L2.
365− </span>
366− </Box>
367− )}
368− {currentL2 && (
369− <Box className="mb4" lineHeight="1.4em">
370− You can generate up to
371− <strong>{` ${point.l2Quota} `}</strong>
372− invites. You will be able to generate another
373− <strong>{` ${point.l2Allowance}`}</strong> invites on
374− <strong>{` ${ddmmmYYYY(nextQuotaTime)}`}</strong>.
375− </Box>
376− )}
377− {showTxnNote && (
378− <Box className="mb6" lineHeight="1.4em">
379− Note: you will have to sign <strong>4</strong> transactions
380− per invite, for a total of <strong>{4 * numInvites}</strong>{' '}
381− signatures.
382− </Box>
383− )}
384− </Box>
385− {/* <Inviter /> */}
386− <Box className="lower">
387− {currentL2 && (
388− <Row className="next-roll">
389− <span>Next Roll in</span>
390− <span className="timer">{nextRoll}</span>
391− </Row>
392− )}
393− {currentL2 ? (
394− // Ignoring for deprecated `indigo-react` component
395− //@ts-ignore
396− <Button
397− as={'button'}
398− className={`generate-codes ${
399− generateDisabled ? 'disabled' : ''
400− }`}
401− disabled={generateDisabled}
402− center
403− onClick={createInvites}>
404− {loading
405− ? `Generating ${numInvites} invites...`
406− : overQuota
407− ? `You can only generate ${point.l2Quota} codes`
408− : `Generate Invite Code${
409− currentL2 ? `s (${numInvites})` : ''
410− }`}
411− </Button>
412− ) : (
413− <Grid.Item
414− full
415− as={InlineEthereumTransaction}
416− label="Generate Invite Code"
417− {...bind}
418− onReturn={() => pop()}
419− />
420− )}
421− </Box>
422− </BodyPane>
423− </Window>
424− <GeneratingModal
425− status={generatingStatus}
426− current={generatingNum}
427− total={numInvites}
428− error={error}
429− hide={() => {
430− setGeneratingStatus('initial');
431− if (generatingStatus !== 'errored') {
432− pop();
433− }
434− }}
435− />
436− </View>
437− );
438− }
439−
440166 const showGenerateButton = !hasPending && !hasInvites && point.canSpawn;
441167 const showAddMoreButton =
442168 (hasInvites || hasPending) && point.canSpawn && point.l2Quota > 0;
@@ -473,7 +199,7 @@ export default function InviteCohort() {
473199 <Button
474200 className="generate-button ph4"
475201 center
476− onClick={() => setShowInviteForm(true)}>
202+ onClick={() => push(names.GENERATE_INVITES)}>
477203 Generate Codes
478204 </Button>
479205 )}
@@ -490,7 +216,7 @@ export default function InviteCohort() {
490216 </Window>
491217 {showAddMoreButton ? (
492218 <Button
493− onClick={() => setShowInviteForm(true)}
219+ onClick={() => push(names.GENERATE_INVITES)}
494220 className="add-more"
495221 //@ts-ignore
496222 accessory={<Icon icon="ChevronEast" />}>
src/views/Invite/GenerateInvites.tsxnew file+167
@@ -0,0 +1,167 @@
1+import { Button } from 'indigo-react';
2+import { Box, Row, StatelessTextInput } from '@tlon/indigo-react';
3+import L2BackHeader from 'components/L2/Headers/L2BackHeader';
4+import BodyPane from 'components/L2/Window/BodyPane';
5+import HeaderPane from 'components/L2/Window/HeaderPane';
6+import Window from 'components/L2/Window/Window';
7+import View from 'components/View';
8+import useRoller from 'lib/useRoller';
9+import { useLocalRouter } from 'lib/LocalRouter';
10+import { ddmmmYYYY } from 'lib/utils/date';
11+import { isExternalWallet } from 'lib/utils/wallet';
12+import { useCallback, useState } from 'react';
13+import { useRollerStore } from 'store/rollerStore';
14+import { GeneratingModal } from './GeneratingModal';
15+import {
16+ InviteGeneratingStatus,
17+ useInvites,
18+ useInviteStore,
19+} from './useInvites';
20+import { DEFAULT_NUM_INVITES } from 'lib/constants';
21+import { useTimerStore } from 'store/timerStore';
22+import { useWallet } from 'store/wallet';
23+
24+export const GenerateInvites = () => {
25+ const { pop } = useLocalRouter();
26+ const { walletType }: any = useWallet();
27+ const { point, nextQuotaTime } = useRollerStore();
28+ const { getPendingTransactions, getAndUpdatePoint } = useRoller();
29+ const { nextRoll } = useTimerStore();
30+ const { generatingNum } = useInviteStore();
31+ const { generateInviteCodes } = useInvites();
32+ const [generatingStatus, setGeneratingStatus] = useState<
33+ InviteGeneratingStatus
34+ >('initial');
35+ const [error, setError] = useState('');
36+ const currentL2 = Boolean(point.isL2Spawn);
37+ const [numInvites, setNumInvites] = useState(
38+ currentL2 ? DEFAULT_NUM_INVITES : 1
39+ );
40+
41+ const createInvites = useCallback(async () => {
42+ setGeneratingStatus('generating');
43+ try {
44+ await generateInviteCodes(numInvites);
45+ getPendingTransactions();
46+ getAndUpdatePoint(point.value);
47+ setGeneratingStatus('finished');
48+ } catch (error) {
49+ if (typeof error === 'object' && (error as Error)?.message) {
50+ setError((error as Error).message);
51+ } else if (typeof error === 'string') {
52+ setError(error);
53+ } else {
54+ setError('Generating invites failed');
55+ }
56+ setGeneratingStatus('errored');
57+ }
58+ }, [
59+ generateInviteCodes,
60+ numInvites,
61+ getPendingTransactions,
62+ getAndUpdatePoint,
63+ point,
64+ setGeneratingStatus,
65+ ]);
66+
67+ const overQuota = numInvites > point.l2Quota && numInvites > 0;
68+ const generateDisabled = overQuota; //loading?
69+ const showTxnNote = isExternalWallet(walletType);
70+
71+ return (
72+ <View
73+ pop={pop}
74+ className="cohort show-invite-form"
75+ hideBack
76+ header={<L2BackHeader back={pop} />}>
77+ <Window>
78+ <HeaderPane>
79+ <h5>Generate Invite Codes</h5>
80+ </HeaderPane>
81+ <BodyPane>
82+ <Box className="upper">
83+ {currentL2 ? (
84+ <Row className="points-input">
85+ I want to generate
86+ <StatelessTextInput
87+ className={`input-box ${overQuota ? 'over-quota' : ''}`}
88+ value={numInvites}
89+ maxLength={3}
90+ onChange={e => {
91+ const target = e.target as HTMLInputElement;
92+ setNumInvites(Number(target.value.replace(/\D/g, '')));
93+ }}
94+ />
95+ planet invite code{numInvites > 1 ? 's' : ''}
96+ </Row>
97+ ) : (
98+ <Box className="migration-prompt">
99+ ETH fees are very high currently. You can spawn invites for free
100+ after{' '}
101+ <span className="migrate" onClick={() => null}>
102+ {/* TODO: change this to navigate to migration */}
103+ migrating this star to L2.
104+ </span>
105+ </Box>
106+ )}
107+ {currentL2 && (
108+ <Box className="mb4" lineHeight="1.4em">
109+ You can generate up to
110+ <strong>{` ${point.l2Quota} `}</strong>
111+ invites. You will be able to generate another
112+ <strong>{` ${point.l2Allowance}`}</strong> invites on
113+ <strong>{` ${ddmmmYYYY(nextQuotaTime)}`}</strong>.
114+ </Box>
115+ )}
116+ {showTxnNote && (
117+ <Box className="mb6" lineHeight="1.4em">
118+ Note: you will have to sign <strong>4</strong> transactions per
119+ invite, for a total of <strong>{4 * numInvites}</strong>{' '}
120+ signatures.
121+ </Box>
122+ )}
123+ </Box>
124+ {/* <Inviter /> */}
125+ <Box className="lower">
126+ {currentL2 && (
127+ <Row className="next-roll">
128+ <span>Next Roll in</span>
129+ <span className="timer">{nextRoll}</span>
130+ </Row>
131+ )}
132+ {currentL2 ? (
133+ // Ignoring for deprecated `indigo-react` component
134+ //@ts-ignore
135+ <Button
136+ as={'button'}
137+ className={`generate-codes ${
138+ generateDisabled ? 'disabled' : ''
139+ }`}
140+ disabled={generateDisabled}
141+ center
142+ onClick={createInvites}>
143+ {overQuota
144+ ? `You can only generate ${point.l2Quota} codes`
145+ : `Generate Invite Code${
146+ currentL2 ? `s (${numInvites})` : ''
147+ }`}
148+ </Button>
149+ ) : null}
150+ </Box>
151+ </BodyPane>
152+ </Window>
153+ <GeneratingModal
154+ status={generatingStatus}
155+ current={generatingNum}
156+ total={numInvites}
157+ error={error}
158+ hide={() => {
159+ setGeneratingStatus('initial');
160+ if (generatingStatus !== 'errored') {
161+ pop();
162+ }
163+ }}
164+ />
165+ </View>
166+ );
167+};