on master help sign in
Redesign L1 transactions, OS, ID, and invites
commit1a8e5f00d56ff8a298cf61255c844ecce24ad37d authorWill Galebach <willbach@gmail.com> dateThu 30 Sep 2021, 21:20 (5 years ago) parent715f8b0
30 files changed+761−483
src/assets/keyfile.svg+5 src/components/InlineEthereumTransaction.js+1−1 src/components/L2/Card.tsx+4−2 src/components/L2/Dropdowns/AccountsDropdown.tsx+2−1 src/components/L2/Headers/L2BackHeader.tsx+5−2 src/components/L2/Headers/L2PointHeader.tsx+3−3 src/components/L2/Window/BodyPane.scss+56−1 src/components/L2/Window/HeaderPane.scss+22 src/components/L2/Window/Window.tsx+3−2 src/components/Passport.js+2−7 src/components/copiable/CopiableWithTooltip.js+3−3 src/indigo-react/components/Grid.d.ts+2−1 src/lib/constants.ts+7 src/lib/types/Invite.ts−1 src/lib/useRoller.ts+110−93 src/lib/utils/invite.ts+8 src/lib/utils/roller.ts+5−3 src/store/lib/useDetailsStore.ts+15−8 src/store/roller.ts+9 src/store/storage/roller.ts+3−14 src/views/Invite/Cohort.scss+11−6 src/views/Invite/Cohort.js → src/views/Invite/Cohort.tsx+122−42 src/views/Point/Point.tsx+19−14 src/views/Point/UrbitID.js+12−39 src/views/Point/UrbitOS.js+13−17 src/views/UrbitID/Home.js−167 src/views/UrbitID/Home.tsx+212 src/views/UrbitID/UrbitID.scss+17 src/views/UrbitOS/Home.tsx+85−56 src/views/UrbitOS/UrbitOS.scss+5
src/assets/keyfile.svgnew file+5
@@ -0,0 +1,5 @@
1+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2+<path fill-rule="evenodd" clip-rule="evenodd" d="M4.05556 5.11111C4.91467 5.11111 5.61111 4.41467 5.61111 3.55556C5.61111 2.69645 4.91467 2 4.05556 2C3.19645 2 2.5 2.69645 2.5 3.55556C2.5 4.41467 3.19645 5.11111 4.05556 5.11111ZM4.05556 6.11111C5.46695 6.11111 6.61111 4.96695 6.61111 3.55556C6.61111 2.14416 5.46695 1 4.05556 1C2.64416 1 1.5 2.14416 1.5 3.55556C1.5 4.96695 2.64416 6.11111 4.05556 6.11111Z" fill="white"/>
3+<path fill-rule="evenodd" clip-rule="evenodd" d="M3.55554 5.47217H4.55554V8.8055H5.99988V9.8055H4.55554V10.7222H6.6111V11.7222H4.55554V12.4999H3.55554V5.47217Z" fill="white"/>
4+<path fill-rule="evenodd" clip-rule="evenodd" d="M9.5 2.5H7V1.5H10.2071L13.5 4.79289V14.5H3.5V13.5H12.5V5.5H9.5V2.5ZM10.5 3.2071L11.7929 4.5H10.5V3.2071Z" fill="white"/>
5+</svg>
src/components/InlineEthereumTransaction.js+1−1
@@ -1,6 +1,6 @@
11import React, { useMemo, useCallback, useState } from 'react';
22import cn from 'classnames';
3−import { Grid, ErrorText, Flex, LinkButton, H5, Text } from 'indigo-react';
3+import { Grid, ErrorText, Flex, LinkButton, H5 } from 'indigo-react';
44import { toBN } from 'web3-utils';
55
66import { useExploreTxUrls } from 'lib/explorer';
src/components/L2/Card.tsx+4−2
@@ -8,7 +8,8 @@ export interface CardProps {
88 title?: string;
99 subtitle?: string;
1010 onClick?: () => void;
11− children: React.ReactNode;
11+ disabled?: boolean;
12+ children?: React.ReactNode;
1213}
1314
1415export default function Card({
@@ -17,9 +18,10 @@ export default function Card({
1718 subtitle,
1819 onClick,
1920 children,
21+ disabled = false,
2022}: CardProps) {
2123 return (
22− <Box className="card" onClick={onClick}>
24+ <Box className="card" onClick={disabled ? undefined : onClick}>
2325 {(!!icon || !!title) && (
2426 <Row>
2527 {!!icon && icon}
src/components/L2/Dropdowns/AccountsDropdown.tsx+2−1
@@ -81,7 +81,8 @@ const AccountsDropdown = ({ showMigrate = false }: AccountsDropdownProps) => {
8181 toggleOpen={() => setOpen(!open)}>
8282 <CopiableAddressWrap
8383 className="current-address"
84− position={TooltipPosition.Left}>
84+ position={TooltipPosition.Left}
85+ text={currentAddress}>
8586 {displayAddress}
8687 </CopiableAddressWrap>
8788 <Box className="divider" />
src/components/L2/Headers/L2BackHeader.tsx+5−2
@@ -59,16 +59,19 @@ const L2BackHeader = ({ back, hideBalance = false }: L2BackHeaderProps) => {
5959 }
6060 }, [currentL2, setEthBalance, wallet, web3]);
6161
62+ const hasBalance = Number(ethBalance.toString()) !== 0;
63+
6264 return (
6365 <Row className="l2-back-header">
6466 <Icon className="back-button" icon="ChevronWest" onClick={goBack} />
65− {currentL2 ? (
67+ {currentL2 && !hideBalance ? (
6668 <Row className="rollup-timer">
6769 <Icon icon="Clock" />
6870 {nextRoll}
6971 </Row>
7072 ) : (
71− !hideBalance && (
73+ !hideBalance &&
74+ hasBalance && (
7275 <Row className="eth-balance">
7376 <Wallet className="wallet-icon" />
7477 <Box>{ethBalance.toString()} ETH</Box>
src/components/L2/Headers/L2PointHeader.tsx+3−3
@@ -15,9 +15,9 @@ import { isStar } from 'lib/utils/point';
1515
1616export interface L2PointHeaderProps {
1717 numInvites: number;
18− hideTimer: boolean;
19− hideInvites: boolean;
20− showMigrate: boolean;
18+ hideTimer?: boolean;
19+ hideInvites?: boolean;
20+ showMigrate?: boolean;
2121}
2222
2323const L2PointHeader = ({
src/components/L2/Window/BodyPane.scss+56−1
@@ -10,4 +10,59 @@
1010 &.spaced {
1111 justify-content: space-between;
1212 }
13−}
13+
14+ .between-row {
15+ display: flex;
16+ flex-direction: row;
17+ justify-content: space-between;
18+ align-items: center;
19+ margin: 16px 0;
20+ width: 100%;
21+ }
22+
23+ .subtitle {
24+ margin-top: 4px;
25+ color: $mid-gray;
26+ }
27+
28+ .arrow-button {
29+ height: 16px;
30+ width: 16px;
31+ padding: 0 4px;
32+ }
33+
34+ button.secondary {
35+ background-color: $washed-gray;
36+ border-radius: $medium-border-radius;
37+ border: none;
38+ height: 32px;
39+ padding: 8px 16px;
40+ font-family: $default-font;
41+
42+ &:nth-child(2) {
43+ margin-left: 8px;
44+ }
45+
46+ &:hover {
47+ opacity: 0.8;
48+ }
49+ }
50+
51+ .copy-button {
52+ background-color: $washed-gray;
53+ border-radius: $medium-border-radius;
54+ width: 32px;
55+ height: 32px;
56+ display: flex;
57+ flex-direction: column;
58+ justify-content: center;
59+ align-items: center;
60+ margin-left: 8px;
61+
62+ svg {
63+ font-size: 16px;
64+ margin-top: 6px;
65+ margin-right: 4px;
66+ }
67+ }
68+}
src/components/L2/Window/HeaderPane.scss+22
@@ -11,4 +11,26 @@
1111 h5 {
1212 font-weight: 600;
1313 }
14+
15+ .header-row {
16+ width: 100%;
17+ display: flex;
18+ flex-direction: row;
19+ justify-content: space-between;
20+ align-items: center;
21+
22+ .header-button {
23+ background-color: black;
24+ color: white;
25+ border-radius: $medium-border-radius;
26+ border: none;
27+ height: 32px;
28+ padding: 8px 16px;
29+ font-family: $default-font;
30+
31+ &:hover {
32+ opacity: 0.8;
33+ }
34+ }
35+ }
1436}
src/components/L2/Window/Window.tsx+3−2
@@ -4,10 +4,11 @@ import './Window.scss';
44
55export interface WindowProps {
66 children: React.ReactNode;
7+ className?: string;
78}
89
9−const Window = ({ children }: WindowProps) => {
10− return <Box className="window">{children}</Box>;
10+const Window = ({ children, className }: WindowProps) => {
11+ return <Box className={`window ${className}`}>{children}</Box>;
1112};
1213
1314export default Window;
src/components/Passport.js+2−7
@@ -6,13 +6,8 @@ import 'style/anim.css';
66import { sigil, reactRenderer } from 'urbit-sigil-js';
77import { Icon } from '@tlon/indigo-react';
88
9−import * as need from 'lib/need';
109import { chunkStr, Matrix, walk, rand } from 'lib/card';
11−import usePermissionsForPoint from 'lib/usePermissionsForPoint';
1210import { useSyncDetails } from 'lib/useSyncPoints';
13−import { buildKeyType } from 'lib/point';
14−
15−import { useWallet } from 'store/wallet';
1611
1712import './SigilMini.scss';
1813
@@ -40,9 +35,9 @@ const symbols = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
4035function Passport({
4136 address,
4237 point,
43− inverted,
38+ inverted = false,
4439 animationMode = 'none',
45− keyType,
40+ keyType = '',
4641}) {
4742 const [cols, rows, tile] = [35, 12, 12];
4843
src/components/copiable/CopiableWithTooltip.js+3−3
@@ -1,15 +1,15 @@
11import React from 'react';
22
33import useCopiable from 'lib/useCopiable';
4−import WithTooltip from 'components/WithTooltip';
4+import WithTooltip, { TooltipPosition } from 'components/WithTooltip';
55import { Icon } from '@tlon/indigo-react';
66
77export default function CopiableWithTooltip({
88 as: As = 'span',
99 text,
10− children,
10+ children = null,
1111 className,
12− position,
12+ position = TooltipPosition.Top,
1313 ...rest
1414}) {
1515 const [doCopy, didCopy] = useCopiable(text || children);
src/indigo-react/components/Grid.d.ts+2−1
@@ -2,6 +2,7 @@ import React from 'react';
22
33const Grid = {
44 Item: React.ReactNode,
5−}
5+ Divider: React.ReactNode,
6+};
67
78export default Grid;
src/lib/constants.ts+7
@@ -124,6 +124,10 @@ const POINT_PROXIES = {
124124const DEFAULT_FADE_TIMEOUT = 300;
125125const MASTER_KEY_DURATION = 850;
126126
127+const INVITES_PER_PAGE = 7;
128+const DEFAULT_NUM_INVITES = 5;
129+const DEFAULT_CSV_NAME = 'urbit_invites.csv';
130+
127131export {
128132 CHECK_BLOCK_EVERY_MS,
129133 DEFAULT_GAS_PRICE_GWEI,
@@ -156,4 +160,7 @@ export {
156160 POINT_PROXIES,
157161 DEFAULT_FADE_TIMEOUT,
158162 MASTER_KEY_DURATION,
163+ INVITES_PER_PAGE,
164+ DEFAULT_NUM_INVITES,
165+ DEFAULT_CSV_NAME,
159166};
src/lib/types/Invite.ts−1
@@ -3,5 +3,4 @@ export interface Invite {
33 planet: number;
44 hash: string;
55 owner: string;
6− status: 'pending' | 'sending' | 'claimed' | 'available';
76}
src/lib/useRoller.ts+110−93
@@ -2,24 +2,19 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
22import _ from 'lodash';
33import * as azimuth from 'azimuth-js';
44import * as need from 'lib/need';
5−import * as ob from 'urbit-ob';
65import { Just } from 'folktale/maybe';
76
87import { Invite } from 'lib/types/Invite';
98import { convertToInt } from './convertToInt';
109import { isDevelopment, isRopsten } from './flags';
11−import { hasPoint, generateInviteWallet } from './utils/roller';
12−import { ROLLER_HOSTS } from './constants';
10+import { hasInvite, generateInviteWallet } from './utils/roller';
11+import { INVITES_PER_PAGE, ROLLER_HOSTS } from './constants';
1312
1413import { useNetwork } from 'store/network';
1514import { usePointCursor } from 'store/pointCursor';
1615import { useRollerStore } from 'store/roller';
1716import { useWallet } from 'store/wallet';
18−import {
19− getStoredInvites,
20− setPendingInvites,
21− setStoredInvites,
22−} from 'store/storage/roller';
17+import { getStoredInvites, setStoredInvites } from 'store/storage/roller';
2318import { usePointCache } from 'store/pointCache';
2419import { getOutgoingPoints } from 'views/Points';
2520import {
@@ -63,6 +58,18 @@ function isShipNumber(ship: number | string | undefined): ship is number {
6358 return (ship as number) !== undefined;
6459}
6560
61+const inviteTemplate = (
62+ planet: number,
63+ ticket = '',
64+ owner = '',
65+ hash = ''
66+): Invite => ({
67+ planet,
68+ ticket,
69+ owner,
70+ hash,
71+});
72+
6673export default function useRoller() {
6774 const { wallet, authToken, authMnemonic, urbitWallet }: any = useWallet();
6875 const { pointCursor }: any = usePointCursor();
@@ -73,11 +80,14 @@ export default function useRoller() {
7380
7481 const {
7582 nextBatchTime,
83+ currentL2,
84+ invitePoints,
7685 setNextBatchTime,
7786 setNextRoll,
7887 setPendingTransactions,
7988 setInvites,
80− currentL2,
89+ setInvitePoints,
90+ setInviteGeneratingNum,
8191 } = useRollerStore();
8292 const [config, setConfig] = useState<Config | null>(null);
8393
@@ -176,6 +186,7 @@ export default function useRoller() {
176186 });
177187
178188 for (let i = 0; i < numInvites && planets[i]; i++) {
189+ setInviteGeneratingNum(i + 1);
179190 const planet = planets[i];
180191 const nonceInc = i + nonce;
181192
@@ -237,16 +248,6 @@ export default function useRoller() {
237248 owner: inviteWallet.ownership.keys.address,
238249 });
239250 }
240−
241− const hashes = await Promise.all(requests);
242−
243− const pendingInvites: Invite[] = tickets.map((ticket, ind) => ({
244− ...ticket,
245− hash: hashes[ind * 3 + 2],
246− status: 'pending',
247− }));
248−
249− setPendingInvites(_point, pendingInvites);
250251 },
251252 [
252253 api,
@@ -257,6 +258,7 @@ export default function useRoller() {
257258 web3,
258259 getDetails,
259260 authMnemonic,
261+ setInviteGeneratingNum,
260262 ]
261263 );
262264
@@ -284,6 +286,7 @@ export default function useRoller() {
284286 try {
285287 const curPoint = Number(need.point(pointCursor));
286288 const newPending = await api.getPendingByShip(curPoint);
289+
287290 setPendingTransactions(newPending);
288291
289292 // const allTransactions = await api.getHistory()
@@ -293,102 +296,75 @@ export default function useRoller() {
293296 }, [api, setPendingTransactions, pointCursor]);
294297
295298 const getInvites = useCallback(
296− async (isL2: boolean) => {
299+ async (isL2: boolean, page: number, getAll = false) => {
297300 try {
298301 const curPoint: number = Number(need.point(pointCursor));
299− const invites = getStoredInvites(curPoint);
300− const availableInvites = invites.available;
301− const pendingTransactions = await api.getPendingByShip(curPoint);
302− if (isDevelopment) {
303− console.log('PENDING', pendingTransactions);
302+ const storedInvites = getStoredInvites(curPoint);
303+ if (storedInvites.length) {
304+ setInvites(storedInvites);
305+ } else {
306+ setInvites(invitePoints.map(p => inviteTemplate(p)));
304307 }
305− setPendingTransactions(pendingTransactions);
306−
307− const stillPending = invites.pending.filter(invite => {
308− const completed = !pendingTransactions.find(
309− p => `~${p?.rawTx?.tx?.data?.ship}` === ob.patp(invite.planet)
310− );
311−
312− if (
313− completed &&
314− !availableInvites.find(({ planet }) => invite.planet === planet)
315− ) {
316− availableInvites.push({ ...invite, status: 'available' });
317− }
318−
319− return !completed;
320− });
321−
322− setStoredInvites(curPoint, {
323− available: availableInvites,
324− pending: stillPending,
325− });
326− setInvites(availableInvites);
327308
328309 const _authToken = authToken.getOrElse(null);
329310 const _contracts = contracts.getOrElse(null);
330311
331312 if (_authToken && _contracts) {
332− let possibleMissingInvites: number[] = [];
333− let availablePoints: number[] = [];
334− if (isL2) {
335− possibleMissingInvites = await api.getSpawned(curPoint);
336− availablePoints = await api.getUnspawned(curPoint);
337− } else {
338− // TODO: should it be removed?
339− availablePoints = await azimuth.azimuth.getUnspawnedChildren(
340− _contracts,
341− curPoint
342− );
343− }
313+ const spawnedPoints = isL2 ? await api.getSpawned(curPoint) : [];
314+
315+ const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
316+ _contracts,
317+ curPoint
318+ );
344319
345320 const outgoingPoints = getOutgoingPoints(
346321 controlledPoints,
347322 getDetails
348− );
323+ ).filter((p: number) => isPlanet(p) && availablePoints.includes(p));
349324
350− possibleMissingInvites = possibleMissingInvites.concat(
351− outgoingPoints.filter(
352− (p: number) => isPlanet(p) && availablePoints.includes(p)
353− )
354− );
325+ const invitePlanets = spawnedPoints.concat(outgoingPoints);
355326
356− // Iterate over all spawned and controlled planets
357− // If the planet is not in available invites, generate the ticket and add it
358327 if (isDevelopment) {
359− console.log('POSSIBLE MISSING', possibleMissingInvites);
328+ console.log('INVITED PLANETS', invitePlanets);
360329 }
361− // TODO: this can be verym slow, so best to allow the user to manually
362− // reveal the ticket, after checking if the invite is still "active"
363− // (i.e. if the address inferred from the derived inviteWallet corresponds to
364− // to the address the point has as owner)
365− //
366− for (let i = 0; i < possibleMissingInvites.length; i++) {
367− const planet = possibleMissingInvites[i];
368− if (!availableInvites.find(hasPoint(planet))) {
369− console.log('MISSING IN AVAILABLE', planet);
330+
331+ const newInvites: Invite[] = [];
332+ // Iterate over all of the stored invites, generating wallet info as necessary
333+ for (let i = 0; i < invitePlanets.length; i++) {
334+ setInviteGeneratingNum(i + 1);
335+ const planet = invitePlanets[i];
336+ const invite = storedInvites.find(hasInvite(planet));
337+
338+ const start = page * INVITES_PER_PAGE;
339+ const end = start + INVITES_PER_PAGE;
340+
341+ if (invite) {
342+ newInvites.push(invite);
343+ } else if (getAll || (i >= start && i < end)) {
344+ if (isDevelopment) {
345+ console.log('MISSING INVITE INFO', planet);
346+ }
370347 const { ticket, inviteWallet } = await generateInviteWallet(
371348 planet,
372349 _authToken
373350 );
374351
375− availableInvites.push({
376− ticket,
377− status: 'available',
378− planet,
379− hash: '',
380− owner: inviteWallet.ownership.keys.address,
381− });
352+ newInvites.push(
353+ inviteTemplate(
354+ planet,
355+ ticket,
356+ inviteWallet.ownership.keys.address
357+ )
358+ );
359+ } else {
360+ newInvites.push(inviteTemplate(planet));
382361 }
383362 }
384363
385− setStoredInvites(curPoint, {
386− available: availableInvites.filter(({ planet }) =>
387− possibleMissingInvites.includes(planet)
388− ),
389− pending: stillPending,
390− });
391− setInvites(availableInvites);
364+ setStoredInvites(curPoint, newInvites);
365+ setInvites(newInvites);
366+
367+ return newInvites;
392368 }
393369 } catch (error) {
394370 console.warn('ERROR GETTING INVITES', error);
@@ -401,8 +377,48 @@ export default function useRoller() {
401377 controlledPoints,
402378 getDetails,
403379 pointCursor,
380+ invitePoints,
404381 setInvites,
405− setPendingTransactions,
382+ setInviteGeneratingNum,
383+ ]
384+ );
385+
386+ const getNumInvites = useCallback(
387+ async (isL2: boolean) => {
388+ try {
389+ const curPoint: number = Number(need.point(pointCursor));
390+ const _authToken = authToken.getOrElse(null);
391+ const _contracts = contracts.getOrElse(null);
392+
393+ if (_authToken && _contracts) {
394+ let invitePoints = isL2 ? await api.getSpawned(curPoint) : [];
395+
396+ const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
397+ _contracts,
398+ curPoint
399+ );
400+
401+ const outgoingPoints = getOutgoingPoints(
402+ controlledPoints,
403+ getDetails
404+ ).filter((p: number) => isPlanet(p) && availablePoints.includes(p));
405+
406+ setInvitePoints(invitePoints.concat(outgoingPoints));
407+ }
408+ } catch (e) {
409+ if (isDevelopment) {
410+ console.warn(e);
411+ }
412+ }
413+ },
414+ [
415+ api,
416+ pointCursor,
417+ setInvitePoints,
418+ authToken,
419+ contracts,
420+ controlledPoints,
421+ getDetails,
406422 ]
407423 );
408424
@@ -632,7 +648,7 @@ export default function useRoller() {
632648 setNextBatchTime(response.nextBatch);
633649 });
634650
635− getInvites(currentL2);
651+ getInvites(currentL2, 0);
636652 }
637653 }, 1000);
638654
@@ -650,5 +666,6 @@ export default function useRoller() {
650666 transferPoint,
651667 setProxyAddress,
652668 configureNetworkingKeys,
669+ getNumInvites,
653670 };
654671}
src/lib/utils/invite.ts+8
@@ -3,7 +3,15 @@ import * as ob from 'urbit-ob';
33export const generateUrl = (ticket: string, planet: number) => {
44 if (ticket && planet) {
55 return `urb.it/#${ticket.slice(1)}-${ob.patp(planet).slice(1)}`;
6+ } else if (planet) {
7+ return ob.patp(planet);
68 }
79
810 return 'No Ticket or Ship';
911};
12+
13+export const generateCsvLine = (ticket: string, planet: number) =>
14+ `${ob.patp(planet)},${generateUrl(ticket, planet)}\n`;
15+
16+export const generateCsvName = (defaultName: string, point: number) =>
17+ `${ob.patp(point).slice(1)}_${defaultName}`;
src/lib/utils/roller.ts+5−3
@@ -112,15 +112,14 @@ export const configureKeys = async (
112112export const transferPointRequest = async (
113113 api: RollerRPCAPI,
114114 _wallet: any,
115− _point: Ship,
116− planet: number,
115+ pointToTransfer: Ship,
117116 proxy: string,
118117 nonce: number,
119118 address: string,
120119 reset?: boolean
121120) => {
122121 const from = {
123− ship: planet, //planet to transfer
122+ ship: pointToTransfer,
124123 proxy,
125124 };
126125
@@ -198,3 +197,6 @@ export const registerProxyAddress = async (
198197};
199198
200199export const reticketL2Point = async () => {};
200+
201+export const hasInvite = (point: number) => (invite: Invite) =>
202+ invite.planet === point && invite.ticket.length > 0;
src/store/lib/useDetailsStore.ts+15−8
@@ -6,6 +6,7 @@ import useRoller from '../../lib/useRoller';
66import { useNetwork } from '../network';
77import { L1Point } from 'lib/types/L1Point';
88import { L2Point } from '@urbit/roller-api';
9+import { isDevelopment } from 'lib/flags';
910
1011const toL1Details = (point: L2Point): L1Point => {
1112 return {
@@ -57,14 +58,20 @@ export default function useDetailsStore() {
5758 }
5859
5960 // fetch point details
60− const l2Point = await api.getPoint(point);
61− const details =
62− l2Point && (l2Point.dominion === 'l2' || l2Point.dominion === 'spawn')
63− ? toL1Details(l2Point)
64− : await azimuth.azimuth.getPoint(_contracts, point);
65− addToDetails({
66− [point]: details,
67− });
61+ try {
62+ const l2Point = await api.getPoint(point);
63+ const details =
64+ l2Point && (l2Point.dominion === 'l2' || l2Point.dominion === 'spawn')
65+ ? toL1Details(l2Point)
66+ : await azimuth.azimuth.getPoint(_contracts, point);
67+ addToDetails({
68+ [point]: details,
69+ });
70+ } catch (e) {
71+ if (isDevelopment) {
72+ console.warn(e);
73+ }
74+ }
6875 },
6976 [api, contracts, addToDetails]
7077 );
src/store/roller.ts+9
@@ -11,13 +11,17 @@ export interface RollerStore {
1111 currentPoint: L2Point | null;
1212 currentL2: boolean;
1313 currentL2Spawn: boolean;
14+ invitePoints: number[];
1415 invites: Invite[];
1516 recentlyCompleted: number;
17+ inviteGeneratingNum: number;
1618 setNextBatchTime: (nextBatchTime: number) => void;
1719 setNextRoll: (nextRoll: string) => void;
1820 setPendingTransactions: (pendingTransactions: PendingTransaction[]) => void;
1921 setCurrentPoint: (currentPoint: L2Point) => void;
22+ setInvitePoints: (points: number[]) => void;
2023 setInvites: (invites: Invite[]) => void;
24+ setInviteGeneratingNum: (numGenerating: number) => void;
2125}
2226
2327export const useRollerStore = create<RollerStore>(set => ({
@@ -27,8 +31,10 @@ export const useRollerStore = create<RollerStore>(set => ({
2731 currentPoint: null,
2832 currentL2: false,
2933 currentL2Spawn: false,
34+ invitePoints: [],
3035 invites: [],
3136 recentlyCompleted: 0,
37+ inviteGeneratingNum: 0,
3238 setNextBatchTime: (nextBatchTime: number) => set(() => ({ nextBatchTime })),
3339 setNextRoll: (nextRoll: string) => set(() => ({ nextRoll })),
3440 setPendingTransactions: (pendingTransactions: PendingTransaction[]) =>
@@ -49,5 +55,8 @@ export const useRollerStore = create<RollerStore>(set => ({
4955 currentL2: isL2(currentPoint?.dominion),
5056 currentL2Spawn: isL2Spawn(currentPoint?.dominion),
5157 })),
58+ setInvitePoints: (invitePoints: number[]) => set(() => ({ invitePoints })),
5259 setInvites: (invites: Invite[]) => set(() => ({ invites })),
60+ setInviteGeneratingNum: (inviteGeneratingNum: number) =>
61+ set(() => ({ inviteGeneratingNum })),
5362}));
src/store/storage/roller.ts+3−14
@@ -3,12 +3,7 @@ import { Invite } from 'lib/types/Invite';
33const INVITES_KEY = 'invites';
44const HIDE_MIGRATION_MESSAGE_KEY = 'hide_migration_message';
55
6−export interface StoredInvites {
7− available: Invite[];
8− pending: Invite[];
9−}
10−
11−export const getStoredInvites = (point: number): StoredInvites => {
6+export const getStoredInvites = (point: number): Invite[] => {
127 const invitesString = localStorage.getItem(INVITES_KEY) || '{}';
138 const invites = JSON.parse(invitesString);
149
@@ -16,22 +11,16 @@ export const getStoredInvites = (point: number): StoredInvites => {
1611 return invites[String(point)];
1712 }
1813
19− return { available: [], pending: [] };
14+ return [];
2015};
2116
22−export const setStoredInvites = (point: number, newInvites: StoredInvites) => {
17+export const setStoredInvites = (point: number, newInvites: Invite[]) => {
2318 const invitesString = localStorage.getItem(INVITES_KEY) || '{}';
2419 const invites = JSON.parse(invitesString);
2520 invites[Number(point)] = newInvites;
2621 localStorage.setItem(INVITES_KEY, JSON.stringify(invites));
2722};
2823
29−export const setPendingInvites = (point: number, pendingInvites: Invite[]) => {
30− const invites = getStoredInvites(point);
31− invites.pending.push(...pendingInvites);
32− setStoredInvites(point, invites);
33−};
34−
3524export const clearInvitesStorage = () => localStorage.removeItem(INVITES_KEY);
3625
3726export const getHideMigrationMessage = () =>
src/views/Invite/Cohort.scss+11−6
@@ -79,7 +79,7 @@
7979 padding: 3px 8px;
8080 }
8181
82− .copy-invite {
82+ .copy-invite, .ticket-loader {
8383 background-color: $washed-gray;
8484 border-radius: $medium-border-radius;
8585 width: 32px;
@@ -88,12 +88,17 @@
8888 flex-direction: column;
8989 justify-content: center;
9090 align-items: center;
91+ }
92+
93+ .copy-invite svg {
94+ font-size: 16px;
95+ margin-top: 6px;
96+ margin-right: 4px;
97+ }
9198
92− svg {
93− font-size: 16px;
94− margin-top: 6px;
95− margin-right: 4px;
96− }
99+ .ticket-loader > svg {
100+ width: 16px;
101+ height: 16px;
97102 }
98103 }
99104
src/views/Invite/Cohort.js → src/views/Invite/Cohort.tsxrenamed+122−42
@@ -1,8 +1,12 @@
11import React, { useEffect, useCallback, useState } from 'react';
22import { Grid, Button } from 'indigo-react';
3−import cn from 'classnames';
43import * as azimuth from 'azimuth-js';
5−import { Icon, Row, StatelessTextInput } from '@tlon/indigo-react';
4+import {
5+ Icon,
6+ LoadingSpinner,
7+ Row,
8+ StatelessTextInput,
9+} from '@tlon/indigo-react';
610
711import { usePointCursor } from 'store/pointCursor';
812import { usePointCache } from 'store/pointCache';
@@ -14,7 +18,11 @@ import { useLocalRouter } from 'lib/LocalRouter';
1418import * as need from 'lib/need';
1519import * as wg from 'lib/walletgen';
1620import useRoller from 'lib/useRoller';
17−import { generateUrl } from 'lib/utils/invite';
21+import {
22+ generateUrl,
23+ generateCsvLine,
24+ generateCsvName,
25+} from 'lib/utils/invite';
1826
1927import { useIssueChild } from 'views/Point/IssueChild';
2028import View from 'components/View';
@@ -29,26 +37,40 @@ import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
2937
3038import './Cohort.scss';
3139import { getStoredInvites } from 'store/storage/roller';
32−import { ETH_ZERO_ADDR } from 'lib/constants';
40+import {
41+ DEFAULT_NUM_INVITES,
42+ ETH_ZERO_ADDR,
43+ INVITES_PER_PAGE,
44+ DEFAULT_CSV_NAME,
45+} from 'lib/constants';
3346import { isPlanet } from 'lib/utils/point';
3447
35−const INVITES_PER_PAGE = 7;
36−const DEFAULT_NUM_INVITES = 5;
48+interface L1Invite {
49+ ticket: string;
50+ planet: number;
51+}
3752
3853export default function InviteCohort() {
39− const { pop } = useLocalRouter();
40− const { authToken } = useWallet();
54+ const { pop }: any = useLocalRouter();
55+ const { authToken }: any = useWallet();
4156 const {
4257 nextRoll,
4358 currentL2,
4459 pendingTransactions,
4560 invites,
61+ invitePoints,
62+ setInviteGeneratingNum,
4663 setInvites,
64+ // inviteGeneratingNum,
4765 } = useRollerStore();
48− const { generateInviteCodes, getPendingTransactions } = useRoller();
49− const { pointCursor } = usePointCursor();
50− const { contracts } = useNetwork();
51− const { syncControlledPoints } = usePointCache();
66+ const {
67+ generateInviteCodes,
68+ getPendingTransactions,
69+ getInvites,
70+ } = useRoller();
71+ const { pointCursor }: any = usePointCursor();
72+ const { contracts }: any = useNetwork();
73+ const { syncControlledPoints }: any = usePointCache();
5274
5375 const point = need.point(pointCursor);
5476 const _contracts = need.contracts(contracts);
@@ -58,11 +80,19 @@ export default function InviteCohort() {
5880 );
5981 const [showInviteForm, setShowInviteForm] = useState(false);
6082 const [loading, setLoading] = useState(false);
61− const [error, setError] = useState(null);
62− const [l1Invite, setL1Invite] = useState(null);
83+ const [error, setError] = useState('');
84+ const [downloadingCsv, setDownloadingCsv] = useState(false);
85+ const [l1Invite, setL1Invite] = useState<L1Invite | null>(null);
86+ const [page, setPage] = useState(0);
6387
6488 const { construct, unconstruct, completed, bind } = useIssueChild();
6589
90+ useEffect(() => {
91+ if (invitePoints.length > 0) {
92+ getInvites(currentL2, page);
93+ }
94+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
95+
6696 // Set up the invite spawn if on L1
6797 useEffect(() => {
6898 const _authToken = authToken.getOrElse(null);
@@ -119,26 +149,24 @@ export default function InviteCohort() {
119149 ]);
120150
121151 useEffect(() => {
122− const { available } = getStoredInvites(point);
152+ const storedInvites = getStoredInvites(point);
123153
124154 if (
125155 !completed ||
126156 !l1Invite ||
127− available.find(({ planet }) => planet === l1Invite.planet)
157+ storedInvites.find(({ planet }) => planet === l1Invite.planet)
128158 ) {
129159 return;
130160 }
131161
132162 syncControlledPoints();
133− setInvites([...invites, l1Invite]);
163+ setInvites([...invites, { ...l1Invite, hash: '', owner: '' }]);
134164 setShowInviteForm(false);
135165 setL1Invite(null);
136166 unconstruct();
137167 pop();
138168 }, [completed]); // eslint-disable-line react-hooks/exhaustive-deps
139169
140− const [page, setPage] = useState(0);
141−
142170 const invitesToDisplay = invites.slice(
143171 page * INVITES_PER_PAGE,
144172 (page + 1) * INVITES_PER_PAGE
@@ -151,16 +179,64 @@ export default function InviteCohort() {
151179 setLoading(true);
152180 try {
153181 await generateInviteCodes(numInvites);
154− getPendingTransactions(point);
182+ getPendingTransactions();
155183 pop();
156184 } catch (error) {
157185 console.log(error);
158186 // TODO: error is not used anywhere on the UI
159− setError(error);
187+ setError(error as string);
160188 } finally {
161189 setLoading(false);
190+ setInviteGeneratingNum(0);
162191 }
163− }, [generateInviteCodes, numInvites, getPendingTransactions, point, pop]);
192+ }, [
193+ generateInviteCodes,
194+ numInvites,
195+ getPendingTransactions,
196+ pop,
197+ setInviteGeneratingNum,
198+ ]);
199+
200+ const downloadCsv = useCallback(() => {
201+ const generateAndDownload = async () => {
202+ setDownloadingCsv(true);
203+ const invites = await getInvites(currentL2, page, true);
204+ if (invites) {
205+ const csv = invites.reduce(
206+ (csvData, { ticket, planet }) =>
207+ (csvData += generateCsvLine(ticket, planet)),
208+ 'Planet,Invite URL\n'
209+ );
210+ const hiddenElement = document.createElement('a');
211+ hiddenElement.href = `data:text/csv;charset=utf-8,${encodeURIComponent(
212+ csv
213+ )}`;
214+ hiddenElement.target = '_blank';
215+
216+ //provide the name for the CSV file to be downloaded
217+ hiddenElement.download = generateCsvName(DEFAULT_CSV_NAME, point);
218+ hiddenElement.click();
219+ } else {
220+ setError('There was an error creating the CSV');
221+ }
222+ setDownloadingCsv(false);
223+ setInviteGeneratingNum(0);
224+ };
225+ generateAndDownload();
226+ }, [
227+ getInvites,
228+ setError,
229+ setDownloadingCsv,
230+ setInviteGeneratingNum,
231+ currentL2,
232+ page,
233+ point,
234+ ]);
235+
236+ const goNextPage = useCallback(() => {
237+ setPage(page + 1);
238+ getInvites(currentL2, page + 1);
239+ }, [currentL2, getInvites, page]);
164240
165241 const getContent = () => {
166242 if (hasInvites) {
@@ -175,14 +251,20 @@ export default function InviteCohort() {
175251 </div>
176252 <div className="invites">
177253 {invitesToDisplay.map(invite => (
178− <div className="invite" key={invite.ticket}>
254+ <div className="invite" key={invite.planet}>
179255 <div className="invite-url">
180256 {generateUrl(invite.ticket, invite.planet)}
181257 </div>
182− <CopiableWithTooltip
183− text={generateUrl(invite.ticket, invite.planet)}
184− className={cn('copy-invite')}
185− />
258+ {invite.ticket ? (
259+ <CopiableWithTooltip
260+ text={generateUrl(invite.ticket, invite.planet)}
261+ className="copy-invite"
262+ />
263+ ) : (
264+ <div className="ticket-loader">
265+ <LoadingSpinner foreground="black" background="white" />
266+ </div>
267+ )}
186268 </div>
187269 ))}
188270 </div>
@@ -226,6 +308,7 @@ export default function InviteCohort() {
226308 if (showInviteForm) {
227309 return (
228310 <View
311+ pop={() => setShowInviteForm(false)}
229312 className="cohort show-invite-form"
230313 hideBack
231314 header={<L2BackHeader back={() => setShowInviteForm(false)} />}>
@@ -258,15 +341,6 @@ export default function InviteCohort() {
258341 </span>
259342 </div>
260343 )}
261− {/* <div>Destination</div>
262− <div className="ship-or-address">Ship or ethereum address</div>
263− <Dropdown value={dropdownValue}>
264− {points.map(point => (
265− <div onClick={selectPoint(point)} className="address">
266− {ob.patp(point)}
267− </div>
268− ))}
269− </Dropdown> */}
270344 </div>
271345 {/* <Inviter /> */}
272346 <div className="lower">
@@ -285,7 +359,7 @@ export default function InviteCohort() {
285359 disabled={loading}
286360 onClick={createInvites}>
287361 {loading
288− ? 'Generating...'
362+ ? `Generating ${numInvites} invites...`
289363 : `Generate Planet Code${
290364 currentL2 ? `s (${numInvites})` : ''
291365 }`}
@@ -323,10 +397,16 @@ export default function InviteCohort() {
323397 ) : (
324398 <Row className="has-invites-header">
325399 {header}
326− <div className="download-csv" onClick={() => null}>
327− <Icon icon="Download" />
328− <div>CSV</div>
329− </div>
400+ {downloadingCsv ? (
401+ <Row className="download-csv">
402+ Generating {invitePoints.length} codes...
403+ </Row>
404+ ) : (
405+ <Row className="download-csv" onClick={downloadCsv}>
406+ <Icon icon="Download" />
407+ <div>CSV</div>
408+ </Row>
409+ )}
330410 </Row>
331411 )}
332412 </HeaderPane>
@@ -350,7 +430,7 @@ export default function InviteCohort() {
350430 numPerPage={INVITES_PER_PAGE}
351431 numElements={invites.length}
352432 goPrevious={() => setPage(page - 1)}
353− goNext={() => setPage(page + 1)}
433+ goNext={goNextPage}
354434 />
355435 )}
356436 </BodyPane>
src/views/Point/Point.tsx+19−14
@@ -1,4 +1,4 @@
1−import { useCallback, useEffect, useMemo, useState } from 'react';
1+import { useCallback, useEffect, useState } from 'react';
22import { azimuth } from 'azimuth-js';
33import { Just } from 'folktale/maybe';
44import { Grid, Flex } from 'indigo-react';
@@ -34,17 +34,18 @@ import { useHasNetworkKeysSet } from 'lib/useHasNetworkKeysSet';
3434import { InviteForm } from './InviteForm';
3535
3636export default function Point() {
37− const { pop, push, names } = useLocalRouter();
38− const { wallet } = useWallet();
39− const { pointCursor } = usePointCursor();
37+ const { pop, push, names }: any = useLocalRouter();
38+ const { wallet }: any = useWallet();
39+ const { pointCursor }: any = usePointCursor();
4040 const point = need.point(pointCursor);
41− const { syncExtras } = usePointCache();
42− const { api, getInvites } = useRoller();
41+ const { syncExtras }: any = usePointCache();
42+ const { api, getNumInvites, getPendingTransactions } = useRoller();
4343 const {
4444 pendingTransactions,
4545 nextRoll,
46− invites,
46+ invitePoints,
4747 setCurrentPoint,
48+ setInvites,
4849 } = useRollerStore();
4950 const networkKeysSet = useHasNetworkKeysSet();
5051 const [showModal, setShowModal] = useState(false);
@@ -66,18 +67,22 @@ export default function Point() {
6667 if (isDevelopment) {
6768 console.log('POINT INFO', pointInfo);
6869 }
70+ getPendingTransactions();
6971 setCurrentPoint(pointInfo);
70− await getInvites(isL2(pointInfo.dominion));
72+ getNumInvites(isL2(pointInfo.dominion));
7173 };
7274
7375 await getTransactions();
74− }, [api, point, getInvites, setCurrentPoint]);
76+ }, [api, point, getNumInvites, setCurrentPoint, getPendingTransactions]);
7577
7678 useEffect(() => {
7779 loadL1Info();
7880 loadL2Info();
79− // eslint-disable-next-line react-hooks/exhaustive-deps
80− }, [point]);
81+ }, [point]); // eslint-disable-line react-hooks/exhaustive-deps
82+
83+ useEffect(() => {
84+ setInvites([]);
85+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
8186
8287 const { isParent, canManage, canSpawn, canVote } = useCurrentPermissions();
8388
@@ -156,7 +161,7 @@ export default function Point() {
156161 header={
157162 <L2PointHeader
158163 hideTimer={!!spawnedPending}
159− numInvites={invites.length}
164+ numInvites={invitePoints.length}
160165 hideInvites={!networkKeysSet}
161166 />
162167 }>
@@ -244,7 +249,7 @@ export default function Point() {
244249 <Card
245250 title="ID"
246251 subtitle="Identity and security settings"
247− icon={<Icon icon="User" />}
252+ icon={(<Icon icon="User" />) as any}
248253 onClick={goUrbitID}
249254 disabled={!canManage}
250255 />
@@ -253,7 +258,7 @@ export default function Point() {
253258 <Card
254259 title="OS"
255260 subtitle="Urbit OS Settings"
256− icon={<Icon icon="Server" />}
261+ icon={(<Icon icon="Server" />) as any}
257262 onClick={goUrbitOS}
258263 disabled={!canManage}
259264 />
src/views/Point/UrbitID.js+12−39
@@ -1,15 +1,12 @@
11import React from 'react';
2−import { Grid } from 'indigo-react';
32
43import { useHistory } from 'store/history';
54
6−import useCurrentPointName from 'lib/useCurrentPointName';
75import useRouter from 'lib/useRouter';
86import { LocalRouterProvider } from 'lib/LocalRouter';
97
108import View from 'components/View';
11−import Crumbs from 'components/Crumbs';
12−import NavHeader from 'components/NavHeader';
9+import L2BackHeader from 'components/L2/Headers/L2BackHeader';
1310
1411import UrbitIDHome from '../UrbitID/Home';
1512import SigilGenerator from '../UrbitID/SigilGenerator';
@@ -36,26 +33,8 @@ const VIEWS = {
3633 [NAMES.TRANSFER]: Transfer,
3734};
3835
39−const humanizeName = name => {
40− switch (name) {
41− case NAMES.SIGIL_GENERATOR:
42− return 'Sigil';
43− case NAMES.DOWNLOAD_KEYS:
44− return 'Download Keys';
45− case NAMES.SET_PROXY:
46− return 'Set Key';
47− case NAMES.RESET_KEYS:
48− return 'Reset Keys';
49− case NAMES.TRANSFER:
50− return 'Transfer';
51− default:
52− return undefined;
53− }
54−};
55−
5636export default function UrbitID() {
5737 const history = useHistory();
58− const name = useCurrentPointName();
5938
6039 const { Route, ...router } = useRouter({
6140 names: NAMES,
@@ -63,28 +42,22 @@ export default function UrbitID() {
6342 initialRoutes: [{ key: NAMES.HOME }],
6443 });
6544
66− const last = router.peek().key;
67− const homeAction = last === NAMES.HOME ? undefined : () => history.pop();
68− const lastCrumb = homeAction ? [{ text: humanizeName(last) }] : [];
45+ const onBack = () =>
46+ router.peek().key === NAMES.HOME ? history.pop() : router.pop();
6947
7048 return (
7149 <View
72− pop={() =>
73− router.peek().key === NAMES.HOME ? history.pop() : router.pop()
74− }
75− inset>
76− <NavHeader>
77− <Crumbs
78− routes={[
79− { text: name, action: () => history.pop() },
80− { text: 'Urbit ID', action: homeAction },
81− ...lastCrumb,
82− ]}
50+ pop={onBack}
51+ inset
52+ hideBack
53+ header={
54+ <L2BackHeader
55+ hideBalance={router.peek().key === NAMES.HOME}
56+ back={onBack}
8357 />
84− </NavHeader>
85−
58+ }
59+ className="urbit-id">
8660 <LocalRouterProvider value={router}>
87− <Grid className="mb4"></Grid>
8861 <Route />
8962 </LocalRouterProvider>
9063 </View>
src/views/Point/UrbitOS.js+13−17
@@ -1,15 +1,12 @@
11import React, { useState } from 'react';
2−import { Grid } from 'indigo-react';
32
43import { useHistory } from 'store/history';
54
6−import useCurrentPointName from 'lib/useCurrentPointName';
75import useRouter from 'lib/useRouter';
86import { LocalRouterProvider } from 'lib/LocalRouter';
97
108import View from 'components/View';
11−import Crumbs from 'components/Crumbs';
12−import NavHeader from 'components/NavHeader';
9+import L2BackHeader from 'components/L2/Headers/L2BackHeader';
1310
1411import UrbitOSHome from '../UrbitOS/Home';
1512import UrbitOSNetworkingKeys from '../UrbitOS/NetworkingKeys';
@@ -29,7 +26,6 @@ const VIEWS = {
2926
3027export default function UrbitOS() {
3128 const history = useHistory();
32− const name = useCurrentPointName();
3329
3430 const { Route, ...router } = useRouter({
3531 names: NAMES,
@@ -39,22 +35,22 @@ export default function UrbitOS() {
3935
4036 const [manualNetworkSeed, setManualNetworkSeed] = useState();
4137
38+ const onBack = () =>
39+ router.peek().key === NAMES.HOME ? history.pop() : router.pop();
40+
4241 return (
4342 <View
44− pop={() =>
45− router.peek().key === NAMES.HOME ? history.pop() : router.pop()
46− }
47− inset>
48− <NavHeader>
49− <Crumbs
50− routes={[
51− { text: name, action: () => history.pop() },
52− { text: 'Urbit OS' },
53− ]}
43+ pop={onBack}
44+ inset
45+ hideBack
46+ header={
47+ <L2BackHeader
48+ hideBalance={router.peek().key === NAMES.HOME}
49+ back={onBack}
5450 />
55− </NavHeader>
51+ }
52+ className="urbit-id">
5653 <LocalRouterProvider value={router}>
57− <Grid className="mb4"></Grid>
5854 <Route
5955 manualNetworkSeed={manualNetworkSeed}
6056 setManualNetworkSeed={setManualNetworkSeed}
src/views/UrbitID/Home.jsdeleted−167
@@ -1,167 +0,0 @@
1−import React, { useCallback } from 'react';
2−import { Grid, B } from 'indigo-react';
3−import { azimuth } from 'azimuth-js';
4−import { Just } from 'folktale/maybe';
5−
6−import { ForwardButton } from 'components/Buttons';
7−import CopyButton from 'components/copiable/CopyButton';
8−
9−import { useLocalRouter } from 'lib/LocalRouter';
10−import { PROXY_TYPE, proxyTypeToHuman } from 'lib/proxy';
11−import * as need from 'lib/need';
12−import { capitalize } from 'lib/capitalize';
13−import useCurrentPermissions from 'lib/useCurrentPermissions';
14−import { convertToInt } from 'lib/convertToInt';
15−
16−import { usePointCursor } from 'store/pointCursor';
17−import { usePointCache } from 'store/pointCache';
18−import { useWallet } from 'store/wallet';
19−
20−export default function UrbitIDHome() {
21− const { push, names } = useLocalRouter();
22− const { pointCursor } = usePointCursor();
23− const { getDetails } = usePointCache();
24− const { urbitWallet } = useWallet();
25−
26− const goSigil = useCallback(() => push(names.SIGIL_GENERATOR), [push, names]);
27−
28− const goDownloadKeys = useCallback(() => push(names.DOWNLOAD_KEYS), [
29− push,
30− names,
31− ]);
32−
33− const goUrbitOS = useCallback(() => push(names.URBIT_OS), [push, names]);
34−
35− const goResetKeys = useCallback(() => push(names.RESET_KEYS), [push, names]);
36−
37− const isMasterTicket = Just.hasInstance(urbitWallet);
38− const point = need.point(pointCursor);
39− const details = need.details(getDetails(point));
40− const { isOwner } = useCurrentPermissions();
41− const networkRevision = convertToInt(details.keyRevisionNumber, 10);
42−
43− const pointSize = azimuth.getPointSize(point);
44− const isParent = pointSize !== azimuth.PointSize.Planet;
45− const isSenate = pointSize === azimuth.PointSize.Galaxy;
46−
47− const goSetProxy = useCallback(
48− proxyType => push(names.SET_PROXY, { proxyType }),
49− [push, names]
50− );
51−
52− const goTransfer = useCallback(() => push(names.TRANSFER), [names, push]);
53−
54− const renderProxyAction = useCallback(
55− (proxyType, address) => {
56− const disabled =
57− !isOwner ||
58− (proxyType === PROXY_TYPE.SPAWN ? networkRevision === 0 : false);
59− return (
60− <>
61− <Grid.Item
62− full
63− as={ForwardButton}
64− disabled={disabled}
65− onClick={() => goSetProxy(proxyType)}>
66− Edit {capitalize(proxyTypeToHuman(proxyType))} Key
67− </Grid.Item>
68− </>
69− );
70− },
71− [goSetProxy, isOwner, networkRevision]
72− );
73− return (
74− <Grid>
75− <Grid.Item full className="mb7 f5">
76− Urbit ID
77− </Grid.Item>
78− <Grid.Divider />
79− <Grid.Item full as={ForwardButton} onClick={goSigil}>
80− Sigil
81− </Grid.Item>
82− <Grid.Divider />
83− <Grid.Item
84− full
85− as={ForwardButton}
86− onClick={goDownloadKeys}
87− disabled={!isMasterTicket}
88− disabledDetail={
89− <B className="wrap ws-normal">
90− · Master Key Authentication required.
91− </B>
92− }>
93− Download Keys
94− </Grid.Item>
95− <Grid.Divider />
96− <Grid.Item
97− full
98− as={ForwardButton}
99− onClick={goResetKeys}
100− disabled={!isOwner}
101− disabledDetail={
102− <B className="wrap ws-normal"> · Ownership key required</B>
103− }
104− detail={
105− isMasterTicket
106− ? 'Reset Master Key and all other keys'
107− : 'Transfer to Master Ticket, resetting all keys'
108− }>
109− {isMasterTicket ? 'Reset Keys' : 'Transfer to Master Ticket'}
110− </Grid.Item>
111−
112− <Grid.Divider />
113−
114− <Grid.Item full className="mv7 f5 gray4">
115− Advanced
116− </Grid.Item>
117− <Grid.Divider />
118− <Grid.Item
119− full
120− as={ForwardButton}
121− accessory={<CopyButton text={details.owner} />}
122− detail={<div className="mt1 mono black">{details.owner}</div>}
123− disabled={!details.owner}>
124− Ownership Address
125− </Grid.Item>
126− <Grid.Divider />
127−
128− {renderProxyAction(PROXY_TYPE.MANAGEMENT, details.managementProxy)}
129− <Grid.Divider />
130−
131− {isParent && renderProxyAction(PROXY_TYPE.SPAWN, details.spawnProxy)}
132−
133− {isParent && networkRevision !== 0 && <Grid.Divider />}
134−
135− {isParent && networkRevision === 0 && (
136− <>
137− <Grid.Item
138− full
139− as={ForwardButton}
140− onClick={goUrbitOS}
141− className="f6"
142− accessory={<span className="underline pointer">Set Keys</span>}>
143− Network Keys Required
144− </Grid.Item>
145− <Grid.Divider />
146− </>
147− )}
148−
149− {isSenate && (
150− <>
151− {renderProxyAction(PROXY_TYPE.VOTING, details.votingProxy)}{' '}
152− <Grid.Divider />
153− </>
154− )}
155− <Grid.Item
156− full
157− as={ForwardButton}
158− onClick={goTransfer}
159− disabled={!isOwner}
160− disabledDetail={
161− <B className="wrap ws-normal"> · Ownership key required</B>
162− }>
163− Transfer this point
164− </Grid.Item>
165− </Grid>
166− );
167−}
src/views/UrbitID/Home.tsxnew file+212
@@ -0,0 +1,212 @@
1+import React, { useCallback } from 'react';
2+import { azimuth } from 'azimuth-js';
3+import { Just } from 'folktale/maybe';
4+import { Box, Button, Icon, Row } from '@tlon/indigo-react';
5+
6+import { useLocalRouter } from 'lib/LocalRouter';
7+import { PROXY_TYPE } from 'lib/proxy';
8+import * as need from 'lib/need';
9+import useCurrentPermissions from 'lib/useCurrentPermissions';
10+import { convertToInt } from 'lib/convertToInt';
11+import { ETH_ZERO_ADDR } from 'lib/constants';
12+import { abbreviateAddress } from 'lib/utils/address';
13+
14+import { usePointCursor } from 'store/pointCursor';
15+import { usePointCache } from 'store/pointCache';
16+import { useWallet } from 'store/wallet';
17+import { useRollerStore } from 'store/roller';
18+
19+import Window from 'components/L2/Window/Window';
20+import HeaderPane from 'components/L2/Window/HeaderPane';
21+import BodyPane from 'components/L2/Window/BodyPane';
22+import CopiableWithTooltip from 'components/copiable/CopiableWithTooltip';
23+
24+import './UrbitID.scss';
25+
26+export default function UrbitIDHome() {
27+ const { push, names }: any = useLocalRouter();
28+ const { pointCursor }: any = usePointCursor();
29+ const { getDetails }: any = usePointCache();
30+ const { urbitWallet }: any = useWallet();
31+ const { currentPoint } = useRollerStore();
32+
33+ const goSigil = useCallback(() => push(names.SIGIL_GENERATOR), [push, names]);
34+
35+ const goDownloadKeys = useCallback(() => push(names.DOWNLOAD_KEYS), [
36+ push,
37+ names,
38+ ]);
39+
40+ const goUrbitOS = useCallback(() => push(names.URBIT_OS), [push, names]);
41+
42+ const goResetKeys = useCallback(() => push(names.RESET_KEYS), [push, names]);
43+
44+ const isMasterTicket = Just.hasInstance(urbitWallet);
45+ const point = need.point(pointCursor);
46+ const details = need.details(getDetails(point));
47+ const { isOwner } = useCurrentPermissions();
48+ const networkRevision = convertToInt(details.keyRevisionNumber, 10);
49+
50+ const pointSize = azimuth.getPointSize(point);
51+ const isParent = pointSize !== azimuth.PointSize.Planet;
52+ const isSenate = pointSize === azimuth.PointSize.Galaxy;
53+
54+ const goSetProxy = useCallback(
55+ proxyType => push(names.SET_PROXY, { proxyType }),
56+ [push, names]
57+ );
58+
59+ const goTransfer = useCallback(() => push(names.TRANSFER), [names, push]);
60+
61+ const noManagement = details.managementProxy === ETH_ZERO_ADDR;
62+ const noSpawn = details.spawnProxy === ETH_ZERO_ADDR;
63+ const showSpawn = currentPoint?.dominion === 'l1';
64+ const showManage = showSpawn || currentPoint?.dominion === 'spawn';
65+
66+ return (
67+ <Window className="id-home">
68+ <HeaderPane>
69+ <Row className="header-row">
70+ <h5>ID</h5>
71+ <Button className="header-button">Download Passport</Button>
72+ </Row>
73+ </HeaderPane>
74+ <BodyPane>
75+ {isOwner && (
76+ <Row className="between-row owner">
77+ <Box>
78+ <Box>Ownership Address</Box>
79+ <div className="mt1 mono black subtitle">
80+ {abbreviateAddress(details.owner)}
81+ </div>
82+ </Box>
83+ <Row>
84+ <Button className="secondary" onClick={goTransfer}>
85+ Transfer
86+ </Button>
87+ <CopiableWithTooltip
88+ text={details.owner}
89+ className="copy-button"
90+ />
91+ </Row>
92+ </Row>
93+ )}
94+
95+ {isOwner && showManage && (
96+ <Row className="between-row management">
97+ <Box>
98+ <Box>Management Address</Box>
99+ <div className="mt1 mono black subtitle">
100+ {noManagement
101+ ? 'Unset'
102+ : abbreviateAddress(details.managementProxy)}
103+ </div>
104+ </Box>
105+ <Row>
106+ <Button
107+ className="secondary"
108+ onClick={() => goSetProxy(PROXY_TYPE.MANAGEMENT)}>
109+ {noManagement ? 'Set' : 'Change'}
110+ </Button>
111+ {!noManagement && (
112+ <CopiableWithTooltip
113+ text={details.managementProxy}
114+ className="copy-button"
115+ />
116+ )}
117+ </Row>
118+ </Row>
119+ )}
120+
121+ {isParent && networkRevision !== 0 && showSpawn && (
122+ <Row className="between-row management">
123+ <Box>
124+ <Box>Spawn Proxy Address</Box>
125+ <div className="mt1 mono black subtitle">
126+ {noSpawn ? 'Unset' : abbreviateAddress(details.spawnProxy)}
127+ </div>
128+ </Box>
129+ <Row>
130+ <Button
131+ className="secondary"
132+ onClick={() => goSetProxy(PROXY_TYPE.SPAWN)}>
133+ {noSpawn ? 'Set' : 'Change'}
134+ </Button>
135+ {!noSpawn && (
136+ <CopiableWithTooltip
137+ text={details.spawnProxy}
138+ className="copy-button"
139+ />
140+ )}
141+ </Row>
142+ </Row>
143+ )}
144+
145+ {isOwner && (
146+ <Row className="between-row management">
147+ <Box>
148+ <Box>Master Ticket</Box>
149+ <Box className="subtitle">
150+ {isMasterTicket
151+ ? 'Reset Master Key and all other keys'
152+ : 'Transfer to Master Ticket, resetting all keys'}
153+ </Box>
154+ </Box>
155+ <Button className="secondary" onClick={goResetKeys}>
156+ {isMasterTicket ? 'Reset' : 'Transfer'}
157+ </Button>
158+ </Row>
159+ )}
160+
161+ {isParent && networkRevision === 0 && (
162+ <Row className="between-row management">
163+ <Box>
164+ <Box>Network Keys Required</Box>
165+ <Box className="subtitle">
166+ Set your network keys to spawn points
167+ </Box>
168+ </Box>
169+ <Button className="secondary" onClick={goUrbitOS}>
170+ Set Keys
171+ </Button>
172+ </Row>
173+ )}
174+
175+ {isMasterTicket && (
176+ <Row className="between-row management">
177+ <Box>
178+ <Box>Download Keys</Box>
179+ <Box className="subtitle">
180+ Download the keys to your master ticket.
181+ </Box>
182+ </Box>
183+ <Button className="secondary" onClick={goDownloadKeys}>
184+ Download
185+ </Button>
186+ </Row>
187+ )}
188+
189+ <Row className="between-row sigil" onClick={goSigil}>
190+ <Box>
191+ <Box>Sigil</Box>
192+ <Box className="subtitle">Modify and download your sigil</Box>
193+ </Box>
194+ <Icon className="arrow-button" icon="ArrowEast" />
195+ </Row>
196+
197+ {isSenate && (
198+ <Row className="between-row management">
199+ <Box>
200+ <Box>Edit Voting Key</Box>
201+ </Box>
202+ <Button
203+ className="secondary"
204+ onClick={() => goSetProxy(PROXY_TYPE.VOTING)}>
205+ Edit
206+ </Button>
207+ </Row>
208+ )}
209+ </BodyPane>
210+ </Window>
211+ );
212+}
src/views/UrbitID/UrbitID.scssnew file+17
@@ -0,0 +1,17 @@
1+@import 'style/shared/variables.scss';
2+
3+.urbit-id {
4+ font-size: 14px;
5+
6+ .body-pane {
7+ .between-row {
8+ &.sigil {
9+ align-items: flex-start;
10+ &:hover {
11+ cursor: pointer;
12+ opacity: 0.8;
13+ }
14+ }
15+ }
16+ }
17+}
src/views/UrbitOS/Home.tsx+85−56
@@ -8,21 +8,26 @@ import { usePointCache } from 'store/pointCache';
88
99import * as need from 'lib/need';
1010
11−import { ForwardButton } from 'components/Buttons';
1211import NetworkingKeys from 'components/NetworkingKeys';
12+import Window from 'components/L2/Window/Window';
13+import HeaderPane from 'components/L2/Window/HeaderPane';
14+import BodyPane from 'components/L2/Window/BodyPane';
15+import { ReactComponent as KeyfileIcon } from 'assets/keyfile.svg';
1316
1417import { useLocalRouter } from 'lib/LocalRouter';
1518import { L1Point } from 'lib/types/L1Point';
1619import AlertBox from 'components/AlertBox';
17−import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
1820import useKeyfileGenerator from 'lib/useKeyfileGenerator';
19−import AccessCode from './AccessCode';
21+import { Box, Button, Row } from '@tlon/indigo-react';
22+import CopiableWithTooltip from 'components/copiable/CopiableWithTooltip';
2023
21−export default function UrbitOSHome({ manualNetworkSeed }) {
22− const { pointCursor } = usePointCursor();
23− const { getDetails } = usePointCache();
24+import './UrbitOS.scss';
2425
25− const { push, names } = useLocalRouter();
26+export default function UrbitOSHome() {
27+ const { pointCursor }: any = usePointCursor();
28+ const { getDetails }: any = usePointCache();
29+
30+ const { push, names }: any = useLocalRouter();
2631
2732 const point = need.point(pointCursor);
2833 const details: L1Point = need.details(getDetails(point));
@@ -36,7 +41,6 @@ export default function UrbitOSHome({ manualNetworkSeed }) {
3641
3742 // The initial key revision number is 0, and increments when set
3843 const hasSetNetworkingKeys = details.keyRevisionNumber !== '0';
39− const networkKeysAction = hasSetNetworkingKeys ? 'Reset' : 'Initialize';
4044
4145 const goNetworkingKeys = useCallback(() => push(names.NETWORKING_KEYS), [
4246 names,
@@ -51,54 +55,67 @@ export default function UrbitOSHome({ manualNetworkSeed }) {
5155 const { bind: keyBind, code } = useKeyfileGenerator();
5256
5357 return (
54− <>
55− <Grid>
56− <Grid.Item full className="mv7 f5">
57− Urbit OS
58− </Grid.Item>
58+ <Window className="os-home">
59+ <HeaderPane>
60+ <Row className="header-row">
61+ <h5>OS</h5>
62+ <Button
63+ className="header-button keyfile"
64+ disabled={!hasSetNetworkingKeys}
65+ onClick={keyBind.download}>
66+ <KeyfileIcon />
67+ Download Keyfile
68+ </Button>
69+ {/* <Grid.Item
70+ full
71+ detail="A keyfile is used to boot your Urbit OS"
72+ as={DownloadKeyfileButton}
73+ {...keyBind}
74+ /> */}
75+ </Row>
76+ </HeaderPane>
77+ <BodyPane>
5978 {showSponsor && (
60− <>
61− <Grid.Divider />
62− <Grid.Item
63− full
64− as={ForwardButton}
65− detail="A sponsor finds new peers in your network"
66− accessory="Change"
67− onClick={goChangeSponsor}>
68− <span className="mono">{sponsor}</span>
69− <span className="f7 bg-black white p1 mb2 ml2 r4">SPONSOR</span>
70− </Grid.Item>
71− </>
72− )}
73− {hasSetNetworkingKeys && (
74− <>
75− <Grid.Divider />
76− <Grid.Item
77− full
78− detail="A keyfile is used to boot your Urbit OS"
79− as={DownloadKeyfileButton}
80− {...keyBind}
81− />
82− </>
83− )}
84− <AccessCode code={code} />
85− <Grid.Divider />
86− <Grid.Item full as={ForwardButton} onClick={goNetworkingKeys}>
87− {networkKeysAction} Networking Keys
88− </Grid.Item>
89− {hasSetNetworkingKeys && (
90− <>
91− <Grid.Divider />
92− <Grid.Item
93− full
94− as={ForwardButton}
95− accessory={showKeys ? '▲' : '▼'}
96− onClick={toggleShowKeys}>
97− View Networking Keys
98− </Grid.Item>
99− {showKeys && <Grid.Item full as={NetworkingKeys} point={point} />}
100− </>
79+ <Row className="between-row management">
80+ <Box>
81+ <Row>
82+ <span className="mono">{sponsor}</span>
83+ <span className="f7 bg-black white p1 mb2 ml2 r4">SPONSOR</span>
84+ </Row>
85+ <Box className="subtitle">
86+ Your sponsor finds new peers in your network
87+ </Box>
88+ </Box>
89+ <Row>
90+ <Button className="secondary" onClick={goChangeSponsor}>
91+ Change
92+ </Button>
93+ </Row>
94+ </Row>
10195 )}
96+ <Row className="between-row management">
97+ <Box>
98+ <Box>Network Keys</Box>
99+ <Box className="subtitle">
100+ {hasSetNetworkingKeys
101+ ? `Revision: ${details.keyRevisionNumber}`
102+ : 'Unset'}
103+ </Box>
104+ </Box>
105+ <Row>
106+ <Button className="secondary" onClick={goNetworkingKeys}>
107+ {hasSetNetworkingKeys ? 'Reset' : 'Initialize'}
108+ </Button>
109+ {hasSetNetworkingKeys && (
110+ <Button className="secondary" onClick={toggleShowKeys}>
111+ {showKeys ? 'Hide' : 'View'}
112+ </Button>
113+ )}
114+ </Row>
115+ </Row>
116+
117+ {showKeys && <Grid.Item full as={NetworkingKeys} point={point} />}
118+
102119 {!hasSetNetworkingKeys && (
103120 <>
104121 <Grid.Item full as={AlertBox} className="mt4">
@@ -106,7 +123,19 @@ export default function UrbitOSHome({ manualNetworkSeed }) {
106123 </Grid.Item>
107124 </>
108125 )}
109− </Grid>
110− </>
126+
127+ {!!code && (
128+ <Row className="between-row management">
129+ <Box>
130+ <Box>Access Key</Box>
131+ <Box className="subtitle">Your passkey to login to Landscape</Box>
132+ </Box>
133+ <Row>
134+ <CopiableWithTooltip text={code} className="copy-button" />
135+ </Row>
136+ </Row>
137+ )}
138+ </BodyPane>
139+ </Window>
111140 );
112141}
src/views/UrbitOS/UrbitOS.scssnew file+5
@@ -0,0 +1,5 @@
1+.os-home {
2+ .keyfile > svg {
3+ margin-right: 4px;
4+ }
5+}