on master help sign in
Update login and point transfer screens
9c28b5aWill Galebach
package-lock.json+3−3
@@ -32765,9 +32765,9 @@
32765 }
32765 }
32766 },
32766 },
32767 "urbit-paper-renderer": {
32767 "urbit-paper-renderer": {
32768− "version": "2.0.9",
32768+ "version": "2.0.10",
32769− "resolved": "https://registry.npmjs.org/urbit-paper-renderer/-/urbit-paper-renderer-2.0.9.tgz",
32769+ "resolved": "https://registry.npmjs.org/urbit-paper-renderer/-/urbit-paper-renderer-2.0.10.tgz",
32770− "integrity": "sha512-WETgzutzP6bQ/ghdp2TGetN8YcxegvKXvknGBL/9mBBblPwCJV2aFP7I4gslmL+IuboRJwP9u9HSE+SeSOTXLg==",
32770+ "integrity": "sha512-thQaV7EwtuyWR/Ti3chuqsIBdaqkiGYi+gH9KSEKKAyLUIhAX3ZDSA2RFCnQiLHB+4k5Rnq6kLmoZVAspc+/RA==",
32771 "requires": {
32771 "requires": {
32772 "lodash.get": "^4.4.2",
32772 "lodash.get": "^4.4.2",
32773 "prop-types": "^15.7.2",
32773 "prop-types": "^15.7.2",
package.json+1−1
@@ -81,7 +81,7 @@
81 "typescript": "^4.3.2",
81 "typescript": "^4.3.2",
82 "urbit-key-generation": "^0.19.0",
82 "urbit-key-generation": "^0.19.0",
83 "urbit-ob": "^4.1.4",
83 "urbit-ob": "^4.1.4",
84− "urbit-paper-renderer": "^2.0.9",
84+ "urbit-paper-renderer": "^2.0.10",
85 "web3": "^1.3.6",
85 "web3": "^1.3.6",
86 "web3-utils": "^1.5.1",
86 "web3-utils": "^1.5.1",
87 "zustand": "^3.5.9"
87 "zustand": "^3.5.9"
src/components/L2/Dropdowns/AccountsDropdown.scss+11
@@ -8,6 +8,17 @@
8 font-family: "Source Code Pro", "Roboto mono", "Courier New", monospace;
8 font-family: "Source Code Pro", "Roboto mono", "Courier New", monospace;
9 }
9 }
10
10
11+ .points {
12+ max-height: 160px;
13+ overflow-y: scroll;
14+ -ms-overflow-style: none;
15+ scrollbar-width: none;
16+
17+ &::-webkit-scrollbar {
18+ display: none;
19+ }
20+ }
21+
11 .content-border {
22 .content-border {
12 .dropdown-content {
23 .dropdown-content {
13 .entry, .current-address {
24 .entry, .current-address {
src/components/L2/Dropdowns/AccountsDropdown.tsx+16−10
@@ -14,7 +14,6 @@ import { clearInvitesStorage } from 'store/storage/roller';
14import { useRollerStore } from 'store/rollerStore';
14import { useRollerStore } from 'store/rollerStore';
15
15
16import { abbreviateAddress } from 'lib/utils/address';
16import { abbreviateAddress } from 'lib/utils/address';
17−import useCurrentPermissions from 'lib/useCurrentPermissions';
18
17
19import Dropdown from './Dropdown';
18import Dropdown from './Dropdown';
20import './AccountsDropdown.scss';
19import './AccountsDropdown.scss';
@@ -57,18 +56,19 @@ const AccountsDropdown = ({ showMigrate = false }: AccountsDropdownProps) => {
57 push,
56 push,
58 ]);
57 ]);
59
58
60− const goIssuePoint = useCallback(() => push(names.ISSUE_CHILD), [
59+ const goSpawn = useCallback(() => {
61− names.ISSUE_CHILD,
60+ if (point.isL2Spawn && point.isStar) {
61+ push(names.INVITE_COHORT);
62+ } else {
63+ push(names.ISSUE_CHILD);
64+ }
62− push,
65+ }, [names, push, point]);
63− ]);
64
66
65 const onLogout = () => {
67 const onLogout = () => {
66 clearInvitesStorage();
68 clearInvitesStorage();
67 reset();
69 reset();
68 };
70 };
69
71
70− const { canSpawn, isParent, isStar } = useCurrentPermissions();
71−
72 const hasL1Points = Boolean(pointList.find(({ layer }) => layer === 1));
72 const hasL1Points = Boolean(pointList.find(({ layer }) => layer === 1));
73 const showMigrateOption = useMemo(() => {
73 const showMigrateOption = useMemo(() => {
74 return showMigrate || hasL1Points;
74 return showMigrate || hasL1Points;
@@ -123,9 +123,15 @@ const AccountsDropdown = ({ showMigrate = false }: AccountsDropdownProps) => {
123 <Icon icon="Bitcoin" />
123 <Icon icon="Bitcoin" />
124 </Row>
124 </Row>
125 )}
125 )}
126− {isParent && canSpawn && (
126+ {point.isParent && point.canSpawn && (
127− <Row className="entry" onClick={goIssuePoint}>
127+ <Row className="entry" onClick={goSpawn}>
128+ <Box>
129+ {point.isStar
130+ ? point.isL2Spawn
128− <Box>{isStar ? 'Generate Invites' : 'Spawn Stars'}</Box>
131+ ? 'Generate Invites'
132+ : 'Spawn Planets'
133+ : 'Spawn Stars'}
134+ </Box>
129 <Icon icon="Plus" />
135 <Icon icon="Plus" />
130 </Row>
136 </Row>
131 )}
137 )}
src/components/L2/Dropdowns/Dropdown.scss+3−2
@@ -5,6 +5,7 @@
5 position: relative;
5 position: relative;
6
6
7 .close-background {
7 .close-background {
8+ z-index: 1;
8 position: fixed;
9 position: fixed;
9 top: 0;
10 top: 0;
10 bottom: 0;
11 bottom: 0;
@@ -39,14 +40,14 @@
39 top: 36px;
40 top: 36px;
40 left: -3px;
41 left: -3px;
41 border-radius: $medium-border-radius;
42 border-radius: $medium-border-radius;
42− z-index: 1;
43+ z-index: 2;
43
44
44 .dropdown-content {
45 .dropdown-content {
45 border-radius: $medium-border-radius;
46 border-radius: $medium-border-radius;
46 background-color: white;
47 background-color: white;
47 border: 1px solid $light-gray;
48 border: 1px solid $light-gray;
48 width: 240px;
49 width: 240px;
49− max-height: 200px;
50+ max-height: 400px;
50 overflow-y: scroll;
51 overflow-y: scroll;
51 -ms-overflow-style: none;
52 -ms-overflow-style: none;
52 scrollbar-width: none;
53 scrollbar-width: none;
src/components/L2/Headers/AdvancedOptions.scssnew file+54
@@ -0,0 +1,54 @@
1+@import 'style/shared/variables.scss';
2+
3+.advanced-options {
4+ position: relative;
5+
6+ .toggle {
7+ cursor: pointer;
8+ padding: 0px 0px 0px 8px;
9+ }
10+
11+ .options-border {
12+ border: 3px solid $washed-gray;
13+ position: absolute;
14+ top: 28px;
15+ right: 0px;
16+ border-radius: $medium-border-radius;
17+ z-index: 2;
18+
19+ .options {
20+ border-radius: $medium-border-radius;
21+ background-color: white;
22+ border: 1px solid $light-gray;
23+ padding: 8px 0px;
24+
25+ .option {
26+ padding: 8px 16px;
27+
28+ .checkbox {
29+ border-radius: $medium-border-radius;
30+ fill: green;
31+
32+ &.checked {
33+ border-color: $activate-blue;
34+ background-color: $activate-blue;
35+ }
36+ }
37+
38+ .text {
39+ margin-left: 8px;
40+ white-space: nowrap;
41+ }
42+ }
43+ }
44+ }
45+
46+ .close-background {
47+ z-index: 1;
48+ position: fixed;
49+ top: 0;
50+ bottom: 0;
51+ right: 0;
52+ left: 0;
53+ }
54+}
src/components/L2/Headers/AdvancedOptions.tsxnew file+52
@@ -0,0 +1,52 @@
1+import React, { useState } from 'react';
2+import { Box, Checkbox, Icon, Row } from '@tlon/indigo-react';
3+import './AdvancedOptions.scss';
4+
5+interface AdvancedOptionsProps {
6+ options: {
7+ selected: boolean;
8+ key: string;
9+ label: string;
10+ onClick: () => void;
11+ }[];
12+}
13+
14+export default function AdvancedOptions({ options }: AdvancedOptionsProps) {
15+ const showingAny = options.reduce(
16+ (anySelected, { selected }) => anySelected || selected,
17+ false
18+ );
19+ const [show, setShow] = useState(false);
20+ const toggleShow = () => setShow(!show);
21+
22+ return (
23+ <Box className="advanced-options">
24+ <Icon
25+ className="toggle"
26+ icon="Gear"
27+ onClick={toggleShow}
28+ color={show || showingAny ? 'rgba(33, 157, 255, 1)' : 'black'}
29+ />
30+ {show && (
31+ <>
32+ <Box className="options-border">
33+ <Box className="options">
34+ {options.map(option => (
35+ <Row className="option">
36+ <Checkbox
37+ className={`checkbox ${option.selected ? 'checked' : ''}`}
38+ {...option}
39+ height="16px"
40+ width="16px"
41+ />
42+ <Box className="text">{option.label}</Box>
43+ </Row>
44+ ))}
45+ </Box>
46+ </Box>
47+ <Box className="close-background" onClick={() => setShow(false)} />
48+ </>
49+ )}
50+ </Box>
51+ );
52+}
src/components/L2/Headers/L2BackHeader.tsx+1−1
@@ -24,7 +24,7 @@ const L2BackHeader = ({
24 className = '',
24 className = '',
25 hideBalance = false,
25 hideBalance = false,
26}: L2BackHeaderProps) => {
26}: L2BackHeaderProps) => {
27− const { point } = useRollerStore(store => store);
27+ const { point } = useRollerStore();
28 const { nextRoll } = useTimerStore();
28 const { nextRoll } = useTimerStore();
29 const { pop }: any = useHistory();
29 const { pop }: any = useHistory();
30 const { wallet }: any = useWallet();
30 const { wallet }: any = useWallet();
src/components/L2/Headers/L2PointHeader.tsx+16−12
@@ -1,7 +1,6 @@
1import React, { useCallback } from 'react';
1import React, { useCallback } from 'react';
2import { Box, LoadingSpinner, Row } from '@tlon/indigo-react';
2import { Box, LoadingSpinner, Row } from '@tlon/indigo-react';
3−
3+import { Nothing } from 'folktale/maybe';
4−import { isStar } from 'lib/utils/point';
5
4
6import { useHistory } from 'store/history';
5import { useHistory } from 'store/history';
7import { useRollerStore } from 'store/rollerStore';
6import { useRollerStore } from 'store/rollerStore';
@@ -13,6 +12,7 @@ import { ReactComponent as InviteIcon } from 'assets/invite.svg';
13import HeaderButton from './HeaderButton';
12import HeaderButton from './HeaderButton';
14import AccountsDropdown from '../Dropdowns/AccountsDropdown';
13import AccountsDropdown from '../Dropdowns/AccountsDropdown';
15import './L2PointHeader.scss';
14import './L2PointHeader.scss';
15+import LayerIndicator from '../LayerIndicator';
16
16
17export interface L2PointHeaderProps {
17export interface L2PointHeaderProps {
18 numInvites?: number;
18 numInvites?: number;
@@ -23,18 +23,22 @@ export interface L2PointHeaderProps {
23}
23}
24
24
25const L2PointHeader = ({
25const L2PointHeader = ({
26− numInvites,
26+ numInvites = 0,
27 hideHome = false,
27 hideHome = false,
28 hideTimer = false,
28 hideTimer = false,
29 hideInvites = false,
29 hideInvites = false,
30 showMigrate = false,
30 showMigrate = false,
31}: L2PointHeaderProps) => {
31}: L2PointHeaderProps) => {
32− const { point, pendingTransactions } = useRollerStore();
32+ const { point, pendingTransactions, invitesLoading } = useRollerStore();
33 const { nextRoll } = useTimerStore();
33 const { nextRoll } = useTimerStore();
34− const { pointCursor }: any = usePointCursor();
34+ const { pointCursor, setPointCursor }: any = usePointCursor();
35
35
36 const { popTo, push, names }: any = useHistory();
36 const { popTo, push, names }: any = useHistory();
37
37
38+ const goToHome = useCallback(() => {
39+ popTo(names.POINTS);
40+ setPointCursor(Nothing());
41+ }, [popTo, setPointCursor, names]);
38 const goToInvites = useCallback(() => push(names.INVITE_COHORT), [
42 const goToInvites = useCallback(() => push(names.INVITE_COHORT), [
39 push,
43 push,
40 names.INVITE_COHORT,
44 names.INVITE_COHORT,
@@ -45,25 +49,25 @@ const L2PointHeader = ({
45 );
49 );
46
50
47 const showInvites =
51 const showInvites =
52+ !hideInvites &&
48− !hideInvites && Boolean(pointCursor?.value && isStar(pointCursor.value));
53+ Boolean(pointCursor?.value && point.isStar && point.isL2Spawn);
49
54
50 return (
55 return (
51 <Row className="l2-point-header">
56 <Row className="l2-point-header">
52 <Row>
57 <Row>
53 {!hideHome && (
58 {!hideHome && (
54− <HeaderButton
55− className="home"
59+ <HeaderButton className="home" icon="Home" onClick={goToHome} />
56− icon="Home"
57− onClick={() => popTo(names.POINTS)}
58− />
59 )}
60 )}
60 <AccountsDropdown showMigrate={showMigrate} />
61 <AccountsDropdown showMigrate={showMigrate} />
62+ {point.value > -1 && (
63+ <LayerIndicator size="lg" layer={point.layer} className="ml2" />
64+ )}
61 </Row>
65 </Row>
62 <Row className="info">
66 <Row className="info">
63 {showInvites && point.showInvites && (
67 {showInvites && point.showInvites && (
64 <Row onClick={goToInvites} className="invites">
68 <Row onClick={goToInvites} className="invites">
65 <InviteIcon />
69 <InviteIcon />
66− {numInvites === undefined ? (
70+ {invitesLoading ? (
67 <LoadingSpinner foreground="rgba(0,0,0,0.3)" background="white" />
71 <LoadingSpinner foreground="rgba(0,0,0,0.3)" background="white" />
68 ) : (
72 ) : (
69 numInvites
73 numInvites
src/components/L2/LayerIndicator.scss+13−2
@@ -12,12 +12,15 @@
12 background-color: black;
12 background-color: black;
13 }
13 }
14
14
15+ &.sm, &.md, &.lg {
16+ text-align: center;
17+ }
18+
15 &.sm {
19 &.sm {
16 height: 16px;
20 height: 16px;
17 padding: 2px 4px 0;
21 padding: 2px 4px 0;
18 font-size: 12px;
22 font-size: 12px;
19 width: 14px;
23 width: 14px;
20− text-align: center;
21 }
24 }
22
25
23 &.md {
26 &.md {
@@ -26,6 +29,14 @@
26 font-size: 14px;
29 font-size: 14px;
27 margin-right: 4px;
30 margin-right: 4px;
28 width: 18px;
31 width: 18px;
29− text-align: center;
32+ }
33+
34+ &.lg {
35+ height: 24px;
36+ padding: 7px 7px 0;
37+ font-size: 14px;
38+ margin-right: 4px;
39+ width: 18px;
40+ border-radius: $medium-border-radius;
30 }
41 }
31}
42}
src/components/L2/LayerIndicator.tsx+12−3
@@ -5,11 +5,20 @@ import './LayerIndicator.scss';
5
5
6export interface LayerIndicatorProps {
6export interface LayerIndicatorProps {
7 layer: 1 | 2;
7 layer: 1 | 2;
8− size: 'sm' | 'md';
8+ size?: 'sm' | 'md' | 'lg';
9+ className?: string;
9}
10}
10
11
11−const LayerIndicator = ({ layer, size }: LayerIndicatorProps) => {
12+const LayerIndicator = ({
13+ layer,
14+ size = 'md',
15+ className = '',
16+}: LayerIndicatorProps) => {
17+ return (
12− return <Box className={`layer-indicator ${size} l${layer}`}>L{layer}</Box>;
18+ <Box className={`layer-indicator ${size} l${layer} ${className}`}>
19+ L{layer}
20+ </Box>
21+ );
13};
22};
14
23
15export default LayerIndicator;
24export default LayerIndicator;
src/components/L2/LoadingOverlay.tsx+2−2
@@ -3,7 +3,7 @@ import { Box, LoadingSpinner } from '@tlon/indigo-react';
3import './LoadingOverlay.scss';
3import './LoadingOverlay.scss';
4
4
5const LoadingOverlay = ({
5const LoadingOverlay = ({
6− loading = false,
6+ loading,
7 text,
7 text,
8}: {
8}: {
9 loading: boolean;
9 loading: boolean;
@@ -16,7 +16,7 @@ const LoadingOverlay = ({
16 return (
16 return (
17 <Box className="loading-overlay">
17 <Box className="loading-overlay">
18 <Box className={`${text ? 'solid' : ''}`}>
18 <Box className={`${text ? 'solid' : ''}`}>
19− {text && <Box className="loader-text">{text}</Box>}
19+ {!!text && <Box className="loader-text">{text}</Box>}
20 <LoadingSpinner foreground="white" background="rgba(0,0,0,0.3)" />
20 <LoadingSpinner foreground="white" background="rgba(0,0,0,0.3)" />
21 </Box>
21 </Box>
22 </Box>
22 </Box>
src/form/Inputs.tsx+2−1
@@ -33,7 +33,7 @@ const PLACEHOLDER_EMAIL = 'email@example.com';
33
33
34const formatPat = buildFormatter([downcase, ensurePatFormat]);
34const formatPat = buildFormatter([downcase, ensurePatFormat]);
35
35
36−export function TicketInput({ name, hidden, ...rest }) {
36+export function TicketInput({ name, hidden, className, ...rest }) {
37 const {
37 const {
38 meta: { valid, error, validating, touched, active },
38 meta: { valid, error, validating, touched, active },
39 } = useField(name, {
39 } = useField(name, {
@@ -48,6 +48,7 @@ export function TicketInput({ name, hidden, ...rest }) {
48
48
49 return (
49 return (
50 <Input
50 <Input
51+ className={className}
51 type={hidden ? 'password' : 'text'}
52 type={hidden ? 'password' : 'text'}
52 name={name}
53 name={name}
53 obscure={value => value.replace(/[^~-]+/g, '••••••')}
54 obscure={value => value.replace(/[^~-]+/g, '••••••')}
src/lib/constants.ts+8
@@ -167,6 +167,12 @@ const DUMMY_L2_ADDRESS = '0x1111111111111111111111111111111111111111';
167const ONE_SECOND = 1000;
167const ONE_SECOND = 1000;
168const TWO_SECONDS = 2000;
168const TWO_SECONDS = 2000;
169
169
170+const MASTER_TICKET_TOOLTIP =
171+ 'Your Master Ticket is your 4-word password for your Urbit';
172+
173+const PASSPORT_TOOLTIP =
174+ 'Your Passport contains all of the address information for your wallet';
175+
170export {
176export {
171 CHECK_BLOCK_EVERY_MS,
177 CHECK_BLOCK_EVERY_MS,
172 DEFAULT_GAS_PRICE_GWEI,
178 DEFAULT_GAS_PRICE_GWEI,
@@ -208,4 +214,6 @@ export {
208 DUMMY_L2_ADDRESS,
214 DUMMY_L2_ADDRESS,
209 ONE_SECOND,
215 ONE_SECOND,
210 TWO_SECONDS,
216 TWO_SECONDS,
217+ MASTER_TICKET_TOOLTIP,
218+ PASSPORT_TOOLTIP,
211};
219};
src/lib/useRoller.ts+32−5
@@ -55,6 +55,7 @@ import {
55import Point, { Points, Relationship } from './types/Point';
55import Point, { Points, Relationship } from './types/Point';
56import { useTimerStore } from 'store/timerStore';
56import { useTimerStore } from 'store/timerStore';
57import { ReticketParams, SendL2Params } from './types/L2Transaction';
57import { ReticketParams, SendL2Params } from './types/L2Transaction';
58+import { L1Point } from './types/L1Point';
58
59
59const ONE_SECOND = 1000;
60const ONE_SECOND = 1000;
60
61
@@ -89,10 +90,12 @@ export default function useRoller() {
89 const {
90 const {
90 nextBatchTime,
91 nextBatchTime,
91 point,
92 point,
93+ removeInvite,
92 setNextBatchTime,
94 setNextBatchTime,
93 setPendingTransactions,
95 setPendingTransactions,
94 setInvites,
96 setInvites,
95 setInviteGeneratingNum,
97 setInviteGeneratingNum,
98+ setInvitesLoading,
96 setPoints,
99 setPoints,
97 updateInvite,
100 updateInvite,
98 } = useRollerStore();
101 } = useRollerStore();
@@ -135,6 +138,7 @@ export default function useRoller() {
135 api
138 api
136 .getRollerConfig()
139 .getRollerConfig()
137 .then(response => {
140 .then(response => {
141+ // console.log('CONFIG', response)
138 setConfig(response);
142 setConfig(response);
139 setNextBatchTime(response.nextBatch);
143 setNextBatchTime(response.nextBatch);
140 })
144 })
@@ -318,6 +322,7 @@ export default function useRoller() {
318 const getInvites = useCallback(
322 const getInvites = useCallback(
319 async (isL2: boolean) => {
323 async (isL2: boolean) => {
320 try {
324 try {
325+ setInvitesLoading(true);
321 const curPoint: number = Number(need.point(pointCursor));
326 const curPoint: number = Number(need.point(pointCursor));
322 const _authToken = authToken.getOrElse(null);
327 const _authToken = authToken.getOrElse(null);
323 const _contracts = contracts.getOrElse(null);
328 const _contracts = contracts.getOrElse(null);
@@ -343,7 +348,13 @@ export default function useRoller() {
343 .concat(
348 .concat(
344 outgoingPoints.filter((p: number) => !spawnedPoints.includes(p))
349 outgoingPoints.filter((p: number) => !spawnedPoints.includes(p))
345 )
350 )
346− .filter((p: number) => !pendingSpawns.has(p));
351+ .filter((p: number) => !pendingSpawns.has(p))
352+ .sort();
353+
354+ setInvites(
355+ curPoint,
356+ invitePlanets.map(p => inviteTemplate(p))
357+ );
347
358
348 const newInvites: Invite[] = [];
359 const newInvites: Invite[] = [];
349 // Iterate over all of the stored invites, generating wallet info as necessary
360 // Iterate over all of the stored invites, generating wallet info as necessary
@@ -367,6 +378,8 @@ export default function useRoller() {
367 ) {
378 ) {
368 newInvites.push(invite);
379 newInvites.push(invite);
369 updateInvite(curPoint, invite);
380 updateInvite(curPoint, invite);
381+ } else {
382+ removeInvite(curPoint, invite.planet);
370 }
383 }
371
384
372 if (storedInvite) {
385 if (storedInvite) {
@@ -374,7 +387,7 @@ export default function useRoller() {
374 }
387 }
375 }
388 }
376 setInvites(curPoint, newInvites);
389 setInvites(curPoint, newInvites);
377−
390+ setInvitesLoading(false);
378 return newInvites;
391 return newInvites;
379 }
392 }
380 } catch (error) {
393 } catch (error) {
@@ -389,7 +402,9 @@ export default function useRoller() {
389 getDetails,
402 getDetails,
390 pointCursor,
403 pointCursor,
391 ls,
404 ls,
405+ removeInvite,
392 setInvites,
406 setInvites,
407+ setInvitesLoading,
393 setInviteGeneratingNum,
408 setInviteGeneratingNum,
394 setPendingTransactions,
409 setPendingTransactions,
395 updateInvite,
410 updateInvite,
@@ -739,17 +754,27 @@ export default function useRoller() {
739 if (!_wallet) {
754 if (!_wallet) {
740 return EMPTY_POINT;
755 return EMPTY_POINT;
741 }
756 }
757+
758+ const pointNum = Number(point);
742 try {
759 try {
743− const pointNum = Number(point);
744 const rawDetails = await api.getPoint(pointNum);
760 const rawDetails = await api.getPoint(pointNum);
745 const details = toL1Details(rawDetails);
761 const details = toL1Details(rawDetails);
746
762
747 return new Point(pointNum, relationship, details, _wallet.address);
763 return new Point(pointNum, relationship, details, _wallet.address);
748 } catch (e) {
764 } catch (e) {
749 console.warn(e);
765 console.warn(e);
766+
767+ // try getting the details from L1
768+ if (getDetails) {
769+ const details: L1Point = getDetails(point);
770+ return new Point(pointNum, relationship, details, _wallet.address);
771+ } else {
772+ const details: L1Point = toL1Details();
773+ return new Point(pointNum, relationship, details, _wallet.address);
774+ }
750 }
775 }
751 },
776 },
752− [api, wallet]
777+ [api, wallet, getDetails]
753 );
778 );
754
779
755 const getPointDetails = useCallback(
780 const getPointDetails = useCallback(
@@ -798,6 +823,8 @@ export default function useRoller() {
798 }, [config, fetchConfig]);
823 }, [config, fetchConfig]);
799
824
800 useEffect(() => {
825 useEffect(() => {
826+ const time = isDevelopment ? 10000 : ONE_SECOND;
827+
801 const interval = setInterval(() => {
828 const interval = setInterval(() => {
802 const nextRoll = getTimeToNextBatch(nextBatchTime, new Date().getTime());
829 const nextRoll = getTimeToNextBatch(nextBatchTime, new Date().getTime());
803 setNextRoll(nextRoll);
830 setNextRoll(nextRoll);
@@ -807,7 +834,7 @@ export default function useRoller() {
807 setNextBatchTime(response.nextBatch);
834 setNextBatchTime(response.nextBatch);
808 });
835 });
809 }
836 }
810− }, 10000000);
837+ }, time);
811
838
812 return () => clearInterval(interval);
839 return () => clearInterval(interval);
813 }, [nextBatchTime, getPendingTransactions, point]); // eslint-disable-line react-hooks/exhaustive-deps
840 }, [nextBatchTime, getPendingTransactions, point]); // eslint-disable-line react-hooks/exhaustive-deps
src/lib/utils/array.tsnew file+2
@@ -0,0 +1,2 @@
1+export const onlyUnique = <T>(value: T, index: number, self: T[]) =>
2+ self.indexOf(value) === index;
src/lib/utils/point.ts+2−2
@@ -11,10 +11,10 @@ export const isStar = (point: number) =>
11export const isPlanet = (point: number) =>
11export const isPlanet = (point: number) =>
12 azimuth.getPointSize(point) === azimuth.PointSize.Planet;
12 azimuth.getPointSize(point) === azimuth.PointSize.Planet;
13
13
14−export const toL1Details = (point: L2Point): L1Point => {
14+export const toL1Details = (point?: L2Point): L1Point => {
15 return {
15 return {
16 layer: point?.dominion === 'l2' ? 2 : 1,
16 layer: point?.dominion === 'l2' ? 2 : 1,
17− isL2Spawn: point.dominion === 'spawn' || point.dominion === 'l2',
17+ isL2Spawn: point?.dominion === 'spawn' || point?.dominion === 'l2',
18 active: true,
18 active: true,
19 authenticationKey: point?.network?.keys?.auth,
19 authenticationKey: point?.network?.keys?.auth,
20 continuityNumber: point?.network?.rift,
20 continuityNumber: point?.network?.rift,
src/store/lib/useControlledPointsStore.js+15−8
@@ -3,11 +3,10 @@ import { Just, Nothing } from 'folktale/maybe';
3import Result from 'folktale/result';
3import Result from 'folktale/result';
4import * as azimuth from 'azimuth-js';
4import * as azimuth from 'azimuth-js';
5
5
6−import { useNetwork } from '../network';
7import { useWallet } from 'store/wallet';
6import { useWallet } from 'store/wallet';
8import useRoller from 'lib/useRoller';
7import useRoller from 'lib/useRoller';
9−
8+import { onlyUnique } from 'lib/utils/array';
10−const onlyUnique = (value, index, self) => self.indexOf(value) === index;
9+import { useNetwork } from '../network';
11
10
12export default function useControlledPointsStore() {
11export default function useControlledPointsStore() {
13 const { contracts } = useNetwork();
12 const { contracts } = useNetwork();
@@ -69,11 +68,19 @@ export default function useControlledPointsStore() {
69 );
68 );
70 } else {
69 } else {
71 getPointDetails(
70 getPointDetails(
72− (ownedPointsL1 || []).concat(ownedPointsL2 || []),
71+ (ownedPointsL1 || []).concat(ownedPointsL2 || []).filter(onlyUnique),
72+ (incomingPointsL1 || [])
73− (incomingPointsL1 || []).concat(incomingPointsL2 || []),
73+ .concat(incomingPointsL2 || [])
74+ .filter(onlyUnique),
75+ (managingPointsL1 || [])
74− (managingPointsL1 || []).concat(managingPointsL2 || []),
76+ .concat(managingPointsL2 || [])
77+ .filter(onlyUnique),
78+ (votingPointsL1 || [])
75− (votingPointsL1 || []).concat(votingPointsL2 || []),
79+ .concat(votingPointsL2 || [])
80+ .filter(onlyUnique),
81+ (spawningPointsL1 || [])
76− (spawningPointsL1 || []).concat(spawningPointsL2 || [])
82+ .concat(spawningPointsL2 || [])
83+ .filter(onlyUnique)
77 );
84 );
78
85
79 _setControlledPoints(
86 _setControlledPoints(
src/store/rollerStore.ts+14
@@ -17,6 +17,7 @@ export interface RollerStore {
17 invites: Invites;
17 invites: Invites;
18 recentlyCompleted: number;
18 recentlyCompleted: number;
19 inviteGeneratingNum: number;
19 inviteGeneratingNum: number;
20+ invitesLoading: boolean;
20 setNextBatchTime: (nextBatchTime: number) => void;
21 setNextBatchTime: (nextBatchTime: number) => void;
21 setPendingTransactions: (pendingTransactions: PendingTransaction[]) => void;
22 setPendingTransactions: (pendingTransactions: PendingTransaction[]) => void;
22 setPoint: (point: number) => void;
23 setPoint: (point: number) => void;
@@ -24,6 +25,8 @@ export interface RollerStore {
24 setInvites: (points: number, invites: Invite[]) => void;
25 setInvites: (points: number, invites: Invite[]) => void;
25 setInviteGeneratingNum: (numGenerating: number) => void;
26 setInviteGeneratingNum: (numGenerating: number) => void;
26 updateInvite: (point: number, invite: Invite) => void;
27 updateInvite: (point: number, invite: Invite) => void;
28+ removeInvite: (point: number, planet: number) => void;
29+ setInvitesLoading: (invitesLoading: boolean) => void;
27}
30}
28
31
29export const useRollerStore = create<RollerStore>(set => ({
32export const useRollerStore = create<RollerStore>(set => ({
@@ -35,6 +38,7 @@ export const useRollerStore = create<RollerStore>(set => ({
35 invites: {},
38 invites: {},
36 recentlyCompleted: 0,
39 recentlyCompleted: 0,
37 inviteGeneratingNum: 0,
40 inviteGeneratingNum: 0,
41+ invitesLoading: false,
38 setNextBatchTime: (nextBatchTime: number) => set(() => ({ nextBatchTime })),
42 setNextBatchTime: (nextBatchTime: number) => set(() => ({ nextBatchTime })),
39 setPendingTransactions: (pendingTransactions: PendingTransaction[]) =>
43 setPendingTransactions: (pendingTransactions: PendingTransaction[]) =>
40 set(state => {
44 set(state => {
@@ -68,4 +72,14 @@ export const useRollerStore = create<RollerStore>(set => ({
68 );
72 );
69 return { invites: Object.assign(state.invites, newInvites) };
73 return { invites: Object.assign(state.invites, newInvites) };
70 }),
74 }),
75+ removeInvite: (point: number, planet: number) =>
76+ set(state => {
77+ const newInvites: Invites = {};
78+ newInvites[point] = state.invites[point]?.filter(
79+ i => i.planet !== planet
80+ );
81+ return { invites: Object.assign(state.invites, newInvites) };
82+ }),
83+ setInvitesLoading: (invitesLoading: boolean) =>
84+ set(() => ({ invitesLoading })),
71}));
85}));
src/style/shared/variables.scss−1
@@ -14,4 +14,3 @@ $warning-red: rgb(242, 33, 33);
14$light-gray-border: 1px solid $light-gray;
14$light-gray-border: 1px solid $light-gray;
15$activate-blue: rgba(33, 157, 255, 1);
15$activate-blue: rgba(33, 157, 255, 1);
16$activate-error-red: #ff4136;
16$activate-error-red: #ff4136;
17−
src/views/AcceptTransfer.js+87−40
@@ -1,6 +1,7 @@
1−import React, { useCallback, useMemo } from 'react';
1+import React, { useCallback, useMemo, useState } from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3+import { Nothing } from 'folktale/maybe';
3−import { Grid, Text, CheckboxInput } from 'indigo-react';
4+import { Grid, Text, CheckboxInput, Button } from 'indigo-react';
4import * as azimuth from 'azimuth-js';
5import * as azimuth from 'azimuth-js';
5
6
6import { useNetwork } from 'store/network';
7import { useNetwork } from 'store/network';
@@ -14,12 +15,18 @@ import useEthereumTransaction from 'lib/useEthereumTransaction';
14import { GAS_LIMITS } from 'lib/constants';
15import { GAS_LIMITS } from 'lib/constants';
15import { useLocalRouter } from 'lib/LocalRouter';
16import { useLocalRouter } from 'lib/LocalRouter';
16
17
17−import ViewHeader from 'components/ViewHeader';
18import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
18import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
19import View from 'components/View';
19import View from 'components/View';
20+import Window from 'components/L2/Window/Window';
21+import HeaderPane from 'components/L2/Window/HeaderPane';
22+import BodyPane from 'components/L2/Window/BodyPane';
23+import L2BackHeader from 'components/L2/Headers/L2BackHeader';
20
24
21import BridgeForm from 'form/BridgeForm';
25import BridgeForm from 'form/BridgeForm';
22import { composeValidator, buildCheckboxValidator } from 'form/validators';
26import { composeValidator, buildCheckboxValidator } from 'form/validators';
27+import { Row } from '@tlon/indigo-react';
28+import { useRollerStore } from 'store/rollerStore';
29+import useRoller from 'lib/useRoller';
23
30
24function useAcceptTransfer() {
31function useAcceptTransfer() {
25 const { contracts } = useNetwork();
32 const { contracts } = useNetwork();
@@ -51,9 +58,17 @@ function useAcceptTransfer() {
51
58
52export default function AcceptTransfer() {
59export default function AcceptTransfer() {
53 const { pop } = useLocalRouter();
60 const { pop } = useLocalRouter();
61+ const { setPointCursor } = usePointCursor();
62+ const { point } = useRollerStore();
63+ const { transferPoint } = useRoller();
64+
65+ const { wallet } = useWallet();
66+ const _address = need.addressFromWallet(wallet);
54
67
55 const name = useCurrentPointName();
68 const name = useCurrentPointName();
56
69
70+ const [reset, setReset] = useState(true);
71+
57 const { completed, bind, inputsLocked, construct } = useAcceptTransfer();
72 const { completed, bind, inputsLocked, construct } = useAcceptTransfer();
58
73
59 const initialValues = useMemo(() => ({ noReset: false }), []);
74 const initialValues = useMemo(() => ({ noReset: false }), []);
@@ -66,50 +81,82 @@ export default function AcceptTransfer() {
66 const onValues = useCallback(
81 const onValues = useCallback(
67 ({ valid, values, form }) => {
82 ({ valid, values, form }) => {
68 construct(!values.noReset);
83 construct(!values.noReset);
84+ setReset(!values.noReset);
69 },
85 },
70 [construct]
86 [construct]
71 );
87 );
88+ const goBack = useCallback(() => {
89+ setPointCursor(Nothing());
90+ pop();
91+ }, [pop, setPointCursor]);
92+
93+ const acceptTransfer = useCallback(async () => {
94+ await transferPoint(_address, reset);
95+ pop();
96+ }, [transferPoint, pop, _address, reset]);
72
97
73 return (
98 return (
99+ <View
74− <View pop={pop} inset>
100+ pop={pop}
75− <Grid>
101+ inset
76− <Grid.Item full as={ViewHeader}>
102+ className="cancel-transfer"
103+ hideBack
104+ header={<L2BackHeader hideBalance back={goBack} />}>
105+ <Window>
106+ <HeaderPane>
107+ <Row className="header-row">
77− Accept Transfer
108+ <h5>Accept Transfer</h5>
78− </Grid.Item>
109+ </Row>
79−
110+ </HeaderPane>
80− <Grid.Item
111+ <BodyPane>
81− full
112+ <Grid>
82− as={Text}
83− className={cn('f5 wrap', {
84− green3: completed,
85− })}>
86− {completed
87− ? `${name} has been accepted.`
88− : `Accept the incoming transfer of ${name}.`}
89− </Grid.Item>
90−
91− <BridgeForm
92− validate={validate}
93− initialValues={initialValues}
94− onValues={onValues}>
95− {() => (
96 <Grid.Item
113 <Grid.Item
97 full
114 full
115+ as={Text}
116+ className={cn('f5 wrap', {
117+ green3: completed,
118+ })}>
119+ {completed
120+ ? `${name} has been accepted.`
121+ : `Accept the incoming transfer of ${name}.`}
122+ </Grid.Item>
123+
124+ <BridgeForm
125+ validate={validate}
126+ initialValues={initialValues}
127+ onValues={onValues}>
128+ {() => (
129+ <Grid.Item
130+ full
98− as={CheckboxInput}
131+ as={CheckboxInput}
99− name="noReset"
132+ name="noReset"
100− label="Retain proxies and key configuration, in case of transferring to self"
133+ label="Retain proxies and key configuration, in case of transferring to self"
101− disabled={inputsLocked}
134+ disabled={inputsLocked}
102− />
135+ />
103− )}
136+ )}
104− </BridgeForm>
137+ </BridgeForm>
105−
138+
139+ {point.isL2 ? (
140+ <Grid.Item
141+ as={Button}
142+ full
143+ className="set-proxy mt4"
144+ center
145+ solid
146+ onClick={acceptTransfer}>
147+ {'Accept Transfer'}
148+ </Grid.Item>
149+ ) : (
106− <Grid.Item
150+ <Grid.Item
107− full
151+ full
108− as={InlineEthereumTransaction}
152+ as={InlineEthereumTransaction}
109− {...bind}
153+ {...bind}
110− onReturn={() => pop()}
154+ onReturn={() => pop()}
111− />
155+ />
112− </Grid>
156+ )}
157+ </Grid>
158+ </BodyPane>
159+ </Window>
113 </View>
160 </View>
114 );
161 );
115}
162}
src/views/CancelTransfer.js+66−27
@@ -1,11 +1,14 @@
1import React, { useCallback } from 'react';
1import React, { useCallback } from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3−import { Grid, Text } from 'indigo-react';
3+import { Grid, Text, Button } from 'indigo-react';
4import * as azimuth from 'azimuth-js';
4import * as azimuth from 'azimuth-js';
5+import { Nothing } from 'folktale/maybe';
6+import { Row } from '@tlon/indigo-react';
5
7
6import { useNetwork } from 'store/network';
8import { useNetwork } from 'store/network';
7import { usePointCursor } from 'store/pointCursor';
9import { usePointCursor } from 'store/pointCursor';
8import { usePointCache } from 'store/pointCache';
10import { usePointCache } from 'store/pointCache';
11+import { useRollerStore } from 'store/rollerStore';
9
12
10import * as need from 'lib/need';
13import * as need from 'lib/need';
11import { useLocalRouter } from 'lib/LocalRouter';
14import { useLocalRouter } from 'lib/LocalRouter';
@@ -13,10 +16,14 @@ import useCurrentPointName from 'lib/useCurrentPointName';
13import useEthereumTransaction from 'lib/useEthereumTransaction';
16import useEthereumTransaction from 'lib/useEthereumTransaction';
14import { ETH_ZERO_ADDR, GAS_LIMITS } from 'lib/constants';
17import { ETH_ZERO_ADDR, GAS_LIMITS } from 'lib/constants';
15import useLifecycle from 'lib/useLifecycle';
18import useLifecycle from 'lib/useLifecycle';
19+import useRoller from 'lib/useRoller';
16
20
17−import ViewHeader from 'components/ViewHeader';
18import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
21import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
19−import View from 'components/ViewHeader';
22+import View from 'components/View';
23+import Window from 'components/L2/Window/Window';
24+import HeaderPane from 'components/L2/Window/HeaderPane';
25+import BodyPane from 'components/L2/Window/BodyPane';
26+import L2BackHeader from 'components/L2/Headers/L2BackHeader';
20
27
21function useCancelTransfer() {
28function useCancelTransfer() {
22 const { contracts } = useNetwork();
29 const { contracts } = useNetwork();
@@ -48,6 +55,8 @@ function useCancelTransfer() {
48function AdminCancelTransfer() {
55function AdminCancelTransfer() {
49 const { pop } = useLocalRouter();
56 const { pop } = useLocalRouter();
50 const { getDetails } = usePointCache();
57 const { getDetails } = usePointCache();
58+ const { point } = useRollerStore();
59+ const { setProxyAddress } = useRoller();
51 const { pointCursor } = usePointCursor();
60 const { pointCursor } = usePointCursor();
52 const _point = need.point(pointCursor);
61 const _point = need.point(pointCursor);
53
62
@@ -56,38 +65,68 @@ function AdminCancelTransfer() {
56
65
57 const { completed, bind } = useCancelTransfer();
66 const { completed, bind } = useCancelTransfer();
58
67
68+ const cancelTransfer = useCallback(async () => {
69+ await setProxyAddress('transfer', ETH_ZERO_ADDR);
70+ pop();
71+ }, [setProxyAddress, pop]);
72+
59 return (
73 return (
60− <Grid>
74+ <Window>
61− <Grid.Item full as={ViewHeader}>
75+ <HeaderPane>
76+ <Row className="header-row">
62− Cancel Outgoing Transfer
77+ <h5>Cancel Outgoing Transfer</h5>
63− </Grid.Item>
78+ </Row>
64−
79+ </HeaderPane>
80+ <BodyPane>
65− <Grid.Item
81+ <Grid.Item
66− full
82+ full
67− as={Text}
83+ as={Text}
68− className={cn('f5 wrap', {
84+ className={cn('f5 wrap', {
69− green3: completed,
85+ green3: completed,
70− })}>
86+ })}>
71− {completed
87+ {completed
72− ? `The outgoing transfer of ${name} has been cancelled.`
88+ ? `The outgoing transfer of ${name} has been cancelled.`
73− : `Cancel the outgoing transfer of ${name} to ${_details.transferProxy}.`}
89+ : `Cancel the outgoing transfer of ${name} to ${_details.transferProxy}.`}
74− </Grid.Item>
90+ </Grid.Item>
75−
91+
92+ {point.isL2 ? (
76− <Grid.Item
93+ <Grid.Item
94+ as={Button}
95+ full
96+ className="set-proxy mt4"
97+ center
98+ solid
99+ onClick={cancelTransfer}>
100+ {'Cancel'}
101+ </Grid.Item>
102+ ) : (
103+ <Grid.Item
77− full
104+ full
78− as={InlineEthereumTransaction}
105+ as={InlineEthereumTransaction}
79− {...bind}
106+ {...bind}
80− onReturn={() => pop()}
107+ onReturn={() => pop()}
81− />
108+ />
82− </Grid>
109+ )}
110+ </BodyPane>
111+ </Window>
83 );
112 );
84}
113}
85
114
86export default function CancelTransfer() {
115export default function CancelTransfer() {
87 const { pop } = useLocalRouter();
116 const { pop } = useLocalRouter();
117+ const { setPointCursor } = usePointCursor();
118+
119+ const goBack = useCallback(() => {
120+ setPointCursor(Nothing());
121+ pop();
122+ }, [pop, setPointCursor]);
88
123
89 return (
124 return (
125+ <View
90− <View pop={pop} inset>
126+ pop={pop}
127+ className="cancel-transfer"
128+ hideBack
129+ header={<L2BackHeader hideBalance back={goBack} />}>
91 <AdminCancelTransfer />
130 <AdminCancelTransfer />
92 </View>
131 </View>
93 );
132 );
src/views/Invite/Cohort.tsx+9−10
@@ -46,6 +46,7 @@ import {
46import { isPlanet } from 'lib/utils/point';
46import { isPlanet } from 'lib/utils/point';
47import { generateInviteWallet } from 'lib/utils/roller';
47import { generateInviteWallet } from 'lib/utils/roller';
48import { useTimerStore } from 'store/timerStore';
48import { useTimerStore } from 'store/timerStore';
49+import { Invite } from 'lib/types/Invite';
49
50
50interface L1Invite {
51interface L1Invite {
51 ticket: string;
52 ticket: string;
@@ -83,13 +84,12 @@ export default function InviteCohort() {
83 );
84 );
84 const [showInviteForm, setShowInviteForm] = useState(false);
85 const [showInviteForm, setShowInviteForm] = useState(false);
85 const [loading, setLoading] = useState(false);
86 const [loading, setLoading] = useState(false);
86− const [downloadingCsv, setDownloadingCsv] = useState(false);
87 const [error, setError] = useState('');
87 const [error, setError] = useState('');
88 const [l1Invite, setL1Invite] = useState<L1Invite | null>(null);
88 const [l1Invite, setL1Invite] = useState<L1Invite | null>(null);
89 const [page, setPage] = useState(0);
89 const [page, setPage] = useState(0);
90
90
91 const { construct, unconstruct, completed, bind } = useIssueChild();
91 const { construct, unconstruct, completed, bind } = useIssueChild();
92− const invitePoints = invites[point.value] || [];
92+ const invitePoints: Invite[] = invites[point.value] || [];
93
93
94 // Set up the invite spawn if on L1
94 // Set up the invite spawn if on L1
95 useEffect(() => {
95 useEffect(() => {
@@ -100,7 +100,7 @@ export default function InviteCohort() {
100 }
100 }
101
101
102 const setUpInvite = async () => {
102 const setUpInvite = async () => {
103− setDownloadingCsv(true);
103+ setLoading(true);
104 const possiblePoints = await azimuth.azimuth.getUnspawnedChildren(
104 const possiblePoints = await azimuth.azimuth.getUnspawnedChildren(
105 _contracts,
105 _contracts,
106 point
106 point
@@ -120,6 +120,7 @@ export default function InviteCohort() {
120 }
120 }
121
121
122 if (invitePoint) {
122 if (invitePoint) {
123+ setInviteGeneratingNum(1);
123 const { ticket, inviteWallet } = await generateInviteWallet(
124 const { ticket, inviteWallet } = await generateInviteWallet(
124 invitePoint,
125 invitePoint,
125 _authToken
126 _authToken
@@ -131,7 +132,7 @@ export default function InviteCohort() {
131 setError('No available planets');
132 setError('No available planets');
132 }
133 }
133
134
134− setDownloadingCsv(false);
135+ setLoading(false);
135 };
136 };
136
137
137 if (!currentL2 && _contracts && point && _authToken && showInviteForm) {
138 if (!currentL2 && _contracts && point && _authToken && showInviteForm) {
@@ -148,6 +149,7 @@ export default function InviteCohort() {
148 showInviteForm,
149 showInviteForm,
149 unconstruct,
150 unconstruct,
150 invitePoints,
151 invitePoints,
152+ setInviteGeneratingNum,
151 ]);
153 ]);
152
154
153 useEffect(() => {
155 useEffect(() => {
@@ -256,7 +258,7 @@ export default function InviteCohort() {
256 Once a code has been claimed, the code will automatically disappear.
258 Once a code has been claimed, the code will automatically disappear.
257 </div>
259 </div>
258 <div className="invites">
260 <div className="invites">
259− {invitesToDisplay.map(invite => (
261+ {invitesToDisplay.map((invite: Invite) => (
260 <div className="invite" key={invite.planet}>
262 <div className="invite" key={invite.planet}>
261 <div
263 <div
262 className={`invite-url ${invite.ticket ? '' : 'shortened'}`}>
264 className={`invite-url ${invite.ticket ? '' : 'shortened'}`}>
@@ -393,7 +395,7 @@ export default function InviteCohort() {
393
395
394 const showGenerateButton = !hasPending && !hasInvites && point.canSpawn;
396 const showGenerateButton = !hasPending && !hasInvites && point.canSpawn;
395 const showAddMoreButton = (hasInvites || hasPending) && point.canSpawn;
397 const showAddMoreButton = (hasInvites || hasPending) && point.canSpawn;
396− const downloadingCsvText = `Generating invite code ${inviteGeneratingNum} of ${invitePoints.length}...`;
398+ const generatingCodesText = `Generating invite code ${inviteGeneratingNum} of ${invitePoints.length}...`;
397
399
398 return (
400 return (
399 <View
401 <View
@@ -447,10 +449,7 @@ export default function InviteCohort() {
447 Add More
449 Add More
448 </Button>
450 </Button>
449 )}
451 )}
450− <LoadingOverlay
452+ <LoadingOverlay loading={loading} text={generatingCodesText} />
451− loading={downloadingCsv || loading}
452− text={downloadingCsv ? downloadingCsvText : undefined}
453− />
454 </View>
453 </View>
455 );
454 );
456}
455}
src/views/Login/LoginSelector.scss+20−1
@@ -1,9 +1,14 @@
1@import 'style/shared/variables.scss';
1@import 'style/shared/variables.scss';
2
2
3.login-selector {
3.login-selector {
4− width: 100%;
4+ width: 482px;
5 min-height: 300px;
5 min-height: 300px;
6
6
7+ .icon {
8+ height: 14px;
9+ width: 14px;
10+ }
11+
7 .login-menu-item {
12 .login-menu-item {
8 padding: 16px 16px;
13 padding: 16px 16px;
9 font-size: 14px;
14 font-size: 14px;
@@ -69,6 +74,20 @@
69 padding: 24px;
74 padding: 24px;
70 font-size: 14px;
75 font-size: 14px;
71 line-height: 24px;
76 line-height: 24px;
77+ }
72
78
79+ .master-ticket {
80+ .body-pane {
81+ .input-form {
82+ width: 100%;
83+ min-height: 392px;
84+
85+ .ticket-input {
86+ input {
87+ height: 30px;
88+ }
89+ }
90+ }
91+ }
73 }
92 }
74}
93}
src/views/Login/LoginSelector.tsx+12−24
@@ -1,7 +1,7 @@
1import React, { useCallback, useState } from 'react';
1import React, { useCallback, useState } from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3import { Grid } from 'indigo-react';
3import { Grid } from 'indigo-react';
4−import { Box, Button, Icon, Row, Text } from '@tlon/indigo-react';
4+import { Box, Button, Icon, LoadingSpinner, Text } from '@tlon/indigo-react';
5import { useHistory } from 'store/history';
5import { useHistory } from 'store/history';
6import { Just, Nothing } from 'folktale/maybe';
6import { Just, Nothing } from 'folktale/maybe';
7import { FORM_ERROR } from 'final-form';
7import { FORM_ERROR } from 'final-form';
@@ -17,9 +17,6 @@ import * as need from 'lib/need';
17import { useWalletConnect } from 'lib/useWalletConnect';
17import { useWalletConnect } from 'lib/useWalletConnect';
18import useLoginView from 'lib/useLoginView';
18import useLoginView from 'lib/useLoginView';
19
19
20−import Window from 'components/L2/Window/Window';
21−import HeaderPane from 'components/L2/Window/HeaderPane';
22−import BodyPane from 'components/L2/Window/BodyPane';
23import Modal from 'components/L2/Modal';
20import Modal from 'components/L2/Modal';
24import HeaderButton from 'components/L2/Headers/HeaderButton';
21import HeaderButton from 'components/L2/Headers/HeaderButton';
25import './LoginSelector.scss';
22import './LoginSelector.scss';
@@ -53,6 +50,7 @@ export default function LoginSelector({
53
50
54 const { push, names }: any = useHistory();
51 const { push, names }: any = useHistory();
55 const [showModal, setShowModal] = useState(false);
52 const [showModal, setShowModal] = useState(false);
53+ const [metamaskSelected, setMetamaskSelected] = useState(false);
56
54
57 const {
55 const {
58 setWallet,
56 setWallet,
@@ -74,6 +72,7 @@ export default function LoginSelector({
74
72
75 const connectMetamask = useCallback(async () => {
73 const connectMetamask = useCallback(async () => {
76 try {
74 try {
75+ setMetamaskSelected(true);
77 resetWallet();
76 resetWallet();
78 setWalletType(WALLET_TYPES.METAMASK);
77 setWalletType(WALLET_TYPES.METAMASK);
79 setPointCursor(Nothing());
78 setPointCursor(Nothing());
@@ -94,6 +93,7 @@ export default function LoginSelector({
94 setWalletType(WALLET_TYPES.METAMASK);
93 setWalletType(WALLET_TYPES.METAMASK);
95 goHome();
94 goHome();
96 } catch (e) {
95 } catch (e) {
96+ setMetamaskSelected(false);
97 console.error(e);
97 console.error(e);
98 return { [FORM_ERROR]: (e as any).message };
98 return { [FORM_ERROR]: (e as any).message };
99 }
99 }
@@ -104,6 +104,7 @@ export default function LoginSelector({
104 setWalletType,
104 setWalletType,
105 resetWallet,
105 resetWallet,
106 setPointCursor,
106 setPointCursor,
107+ setMetamaskSelected,
107 goHome,
108 goHome,
108 ]);
109 ]);
109
110
@@ -118,6 +119,8 @@ export default function LoginSelector({
118 }
119 }
119 };
120 };
120
121
122+ const loader = <LoadingSpinner background="#bce2ff" foreground="#219DFF" />;
123+
121 if (!currentTab) {
124 if (!currentTab) {
122 return (
125 return (
123 <Grid className={className}>
126 <Grid className={className}>
@@ -128,7 +131,9 @@ export default function LoginSelector({
128 className={cn('f5 pv3 rel pointer login-menu-item')}
131 className={cn('f5 pv3 rel pointer login-menu-item')}
129 onClick={selectOption(option.value)}>
132 onClick={selectOption(option.value)}>
130 {option.text}
133 {option.text}
134+ {option.value === NAMES.METAMASK && metamaskSelected
135+ ? loader
131− {option.menuIcon}
136+ : option.menuIcon}
132 </Grid.Item>
137 </Grid.Item>
133 </React.Fragment>
138 </React.Fragment>
134 ))}
139 ))}
@@ -170,32 +175,15 @@ export default function LoginSelector({
170 }
175 }
171
176
172 const Tab = views[currentTab];
177 const Tab = views[currentTab];
173− const currentOption = options.find(({ value }) => value === currentTab);
174
178
175 return (
179 return (
176− <Box display="flex" flexDirection="column">
180+ <Box className="flex-col login-selector">
177 <HeaderButton
181 <HeaderButton
178 className="mb4"
182 className="mb4"
179 icon="ChevronWest"
183 icon="ChevronWest"
180 onClick={() => setCurrentTab(undefined)}
184 onClick={() => setCurrentTab(undefined)}
181 />
185 />
182− <Window>
183− <HeaderPane>
184− <Row className="header-row">
185− <h5>{currentOption?.text}</h5>
186− {currentOption?.headerIcon}
187− </Row>
188− </HeaderPane>
189− <BodyPane className="login-body-pane">
190− <Grid.Item
191− full
192− as={Tab}
193− {...rest}
194− goHome={goHome}
186+ <Grid.Item full as={Tab} {...rest} goHome={goHome} />
195− className="login-selector"
196− />
197− </BodyPane>
198− </Window>
199 </Box>
187 </Box>
200 );
188 );
201}
189}
src/views/Login/Mnemonic.jsdeleted−130
@@ -1,130 +0,0 @@
1−import React, { useCallback, useState, useMemo } from 'react';
2−import cn from 'classnames';
3−import { Just, Nothing } from 'folktale/maybe';
4−import { Grid, CheckboxInput } from 'indigo-react';
5−
6−import { useWallet } from 'store/wallet';
7−
8−import { walletFromMnemonic } from 'lib/wallet';
9−import { DEFAULT_HD_PATH, WALLET_TYPES } from 'lib/constants';
10−import useLoginView from 'lib/useLoginView';
11−import { MnemonicInput, HdPathInput, PassphraseInput } from 'form/Inputs';
12−import {
13− composeValidator,
14− buildAnyMnemonicValidator,
15− buildMnemonicValidator,
16− buildCheckboxValidator,
17− buildPassphraseValidator,
18− buildHdPathValidator,
19−} from 'form/validators';
20−import BridgeForm from 'form/BridgeForm';
21−import Condition from 'form/Condition';
22−import FormError from 'form/FormError';
23−import SubmitButton from 'form/SubmitButton';
24−
25−export default function Mnemonic({ className, goHome }) {
26− useLoginView(WALLET_TYPES.MNEMONIC);
27−
28− const { setWallet, setAuthMnemonic, setWalletHdPath } = useWallet();
29−
30− const [anyMnemonic, setAnyMnemonic] = useState(false);
31−
32− const validate = useMemo(() => {
33− const mnemonicValidator = anyMnemonic
34− ? buildAnyMnemonicValidator()
35− : buildMnemonicValidator();
36− return composeValidator({
37− useAdvanced: buildCheckboxValidator(),
38− mnemonic: mnemonicValidator,
39− passphrase: buildPassphraseValidator(),
40− hdpath: buildHdPathValidator(),
41− });
42− }, [anyMnemonic]);
43−
44− // when the properties change, re-derive wallet and set global state
45− const onValues = useCallback(
46− ({ valid, values }) => {
47− setAnyMnemonic(values.anyMnemonic);
48− if (valid) {
49− setWalletHdPath(values.hdpath);
50− setAuthMnemonic(Just(values.mnemonic));
51− setWallet(
52− walletFromMnemonic(
53− values.mnemonic,
54− values.hdpath,
55− values.passphrase,
56− values.anyMnemonic
57− )
58− );
59− } else {
60− setAuthMnemonic(Nothing());
61− setWallet(Nothing());
62− }
63− },
64− [setAuthMnemonic, setWallet, setWalletHdPath]
65− );
66−
67− const initialValues = {
68− hdpath: DEFAULT_HD_PATH,
69− useAdvanced: false,
70− anyMnemonic: false,
71− };
72−
73− return (
74− <Grid className={cn(className)}>
75− <BridgeForm
76− validate={validate}
77− onValues={onValues}
78− afterSubmit={goHome}
79− initialValues={initialValues}>
80− {({ handleSubmit }) => (
81− <Grid.Item full className="flex-col justify-between">
82− <Grid.Item full>
83− <Grid.Item full as={MnemonicInput} name="mnemonic" />
84−
85− <Grid.Item
86− full
87− as={CheckboxInput}
88− name="anyMnemonic"
89− label="Skip passphrase validation"
90− />
91−
92− <Condition when="useAdvanced" is={true}>
93− <Grid.Item
94− full
95− as={PassphraseInput}
96− name="passphrase"
97− label="Passphrase"
98− />
99−
100− <Grid.Item
101− full
102− as={HdPathInput}
103− name="hdpath"
104− label="HD Path"
105− />
106− </Condition>
107−
108− <Grid.Item
109− full
110− as={CheckboxInput}
111− name="useAdvanced"
112− label="Passphrase & HD Path"
113− />
114−
115− <Grid.Item full as={FormError} />
116− </Grid.Item>
117−
118− <Grid.Item
119− full
120− as={SubmitButton}
121− handleSubmit={handleSubmit}
122− center>
123− Log In
124− </Grid.Item>
125− </Grid.Item>
126− )}
127− </BridgeForm>
128− </Grid>
129− );
130−}
src/views/Login/Mnemonic.tsxnew file+153
@@ -0,0 +1,153 @@
1+import React, { useCallback, useState, useMemo } from 'react';
2+import cn from 'classnames';
3+import { Just, Nothing } from 'folktale/maybe';
4+import { Grid } from 'indigo-react';
5+
6+import { useWallet } from 'store/wallet';
7+
8+import { walletFromMnemonic } from 'lib/wallet';
9+import { DEFAULT_HD_PATH, WALLET_TYPES } from 'lib/constants';
10+import useLoginView from 'lib/useLoginView';
11+import { MnemonicInput, HdPathInput, PassphraseInput } from 'form/Inputs';
12+import {
13+ composeValidator,
14+ buildAnyMnemonicValidator,
15+ buildMnemonicValidator,
16+ buildCheckboxValidator,
17+ buildPassphraseValidator,
18+ buildHdPathValidator,
19+} from 'form/validators';
20+import BridgeForm from 'form/BridgeForm';
21+import FormError from 'form/FormError';
22+import SubmitButton from 'form/SubmitButton';
23+import Window from 'components/L2/Window/Window';
24+import HeaderPane from 'components/L2/Window/HeaderPane';
25+import BodyPane from 'components/L2/Window/BodyPane';
26+import AdvancedOptions from 'components/L2/Headers/AdvancedOptions';
27+import { Row } from '@tlon/indigo-react';
28+
29+interface MnemonicProps {
30+ className?: string;
31+ goHome: () => void;
32+}
33+
34+export default function Mnemonic({ className, goHome }: MnemonicProps) {
35+ useLoginView(WALLET_TYPES.MNEMONIC);
36+
37+ const { setWallet, setAuthMnemonic, setWalletHdPath }: any = useWallet();
38+
39+ const [anyMnemonic, setAnyMnemonic] = useState(false);
40+ const [skipValidation, setSkipValidation] = useState(false);
41+ const [phraseHdPath, setPhraseHdPath] = useState(false);
42+
43+ const validate = useMemo(() => {
44+ const mnemonicValidator = anyMnemonic
45+ ? buildAnyMnemonicValidator()
46+ : buildMnemonicValidator();
47+ return composeValidator({
48+ useAdvanced: buildCheckboxValidator(),
49+ mnemonic: mnemonicValidator,
50+ passphrase: buildPassphraseValidator(),
51+ hdpath: buildHdPathValidator(),
52+ });
53+ }, [anyMnemonic]);
54+
55+ // when the properties change, re-derive wallet and set global state
56+ const onValues = useCallback(
57+ ({ valid, values }) => {
58+ setAnyMnemonic(values.anyMnemonic);
59+ if (valid) {
60+ setWalletHdPath(values.hdpath);
61+ setAuthMnemonic(Just(values.mnemonic));
62+ setWallet(
63+ walletFromMnemonic(
64+ values.mnemonic,
65+ values.hdpath,
66+ values.passphrase,
67+ values.anyMnemonic
68+ )
69+ );
70+ } else {
71+ setAuthMnemonic(Nothing());
72+ setWallet(Nothing());
73+ }
74+ },
75+ [setAuthMnemonic, setWallet, setWalletHdPath]
76+ );
77+
78+ const initialValues = {
79+ hdpath: DEFAULT_HD_PATH,
80+ useAdvanced: false,
81+ anyMnemonic: false,
82+ };
83+ const advancedOptions = [
84+ {
85+ selected: skipValidation,
86+ key: 'skipValidation',
87+ label: 'Skip Passphrase Validation',
88+ onClick: () => setSkipValidation(!skipValidation),
89+ },
90+ {
91+ selected: phraseHdPath,
92+ key: 'phraseHdPath',
93+ label: 'Passphrase & HD Path',
94+ onClick: () => setPhraseHdPath(!phraseHdPath),
95+ },
96+ ];
97+
98+ return (
99+ <Window className="master-ticket">
100+ <HeaderPane>
101+ <Row className="header-row">
102+ <h5>Recovery Phrase</h5>
103+ <AdvancedOptions options={advancedOptions} />
104+ </Row>
105+ </HeaderPane>
106+ <BodyPane>
107+ <Grid className={cn(className, 'input-form')}>
108+ <BridgeForm
109+ validate={validate}
110+ onValues={onValues}
111+ afterSubmit={goHome}
112+ initialValues={initialValues}>
113+ {({ handleSubmit }) => (
114+ <Grid.Item full className="flex-col justify-between">
115+ <Grid.Item full>
116+ <Grid.Item full as={MnemonicInput} name="mnemonic" />
117+
118+ {skipValidation && (
119+ <Grid.Item
120+ full
121+ as={PassphraseInput}
122+ name="passphrase"
123+ label="Passphrase"
124+ />
125+ )}
126+
127+ {phraseHdPath && (
128+ <Grid.Item
129+ full
130+ as={HdPathInput}
131+ name="hdpath"
132+ label="HD Path"
133+ />
134+ )}
135+
136+ <Grid.Item full as={FormError} />
137+ </Grid.Item>
138+
139+ <Grid.Item
140+ full
141+ as={SubmitButton}
142+ handleSubmit={handleSubmit}
143+ center>
144+ Log In
145+ </Grid.Item>
146+ </Grid.Item>
147+ )}
148+ </BridgeForm>
149+ </Grid>
150+ </BodyPane>
151+ </Window>
152+ );
153+}
src/views/Login/Ticket.jsdeleted−294
@@ -1,294 +0,0 @@
1−import { useCallback, useMemo, useRef } from 'react';
2−import { Just } from 'folktale/maybe';
3−import * as kg from 'urbit-key-generation';
4−import {
5− Grid,
6− CheckboxInput,
7− Flex,
8− ToggleInput,
9− AccessoryIcon,
10−} from 'indigo-react';
11−import { FORM_ERROR } from 'final-form';
12−
13−import { useWallet } from 'store/wallet';
14−import { usePointCursor } from 'store/pointCursor';
15−
16−import { urbitWalletFromTicket } from 'lib/wallet';
17−import { WALLET_TYPES } from 'lib/constants';
18−import useImpliedPoint from 'lib/useImpliedPoint';
19−import useLoginView from 'lib/useLoginView';
20−import { patp2dec } from 'lib/patp2dec';
21−import { timeout } from 'lib/timeout';
22−import useRoller from 'lib/useRoller';
23−
24−import BridgeForm from 'form/BridgeForm';
25−import Condition from 'form/Condition';
26−import { TicketInput, PassphraseInput, PointInput } from 'form/Inputs';
27−import {
28− composeValidator,
29− buildCheckboxValidator,
30− buildPatqValidator,
31− buildShardValidator,
32− buildPassphraseValidator,
33− buildPointValidator,
34−} from 'form/validators';
35−import FormError from 'form/FormError';
36−import SubmitButton from 'form/SubmitButton';
37−import { WARNING } from 'form/helpers';
38−
39−import { ReactComponent as SecretShowIcon } from 'assets/secret-show.svg';
40−import { ReactComponent as SecretHideIcon } from 'assets/secret-hidden.svg';
41−
42−function AdvancedOptions() {
43− return (
44− <Flex.Item as={Flex}>
45− <Condition when="useAdvanced" is={true}>
46− <Flex.Item
47− as={CheckboxInput}
48− inline
49− white
50− className="mr4"
51− name="usePassphrase"
52− label="Passphrase"
53− style={{ boxSizing: 'border-box' }}
54− />
55− <Flex.Item
56− as={CheckboxInput}
57− inline
58− white
59− className="mr4"
60− name="useShards"
61− label="Shards"
62− style={{ boxSizing: 'border-box' }}
63− />
64− </Condition>
65− <Flex.Item
66− as={ToggleInput}
67− name="useAdvanced"
68− label="Settings"
69− small
70− inverseLabel="Close"
71− />
72− </Flex.Item>
73− );
74−}
75−
76−function TicketInputAccessory({ name }) {
77− return (
78− <AccessoryIcon>
79− <ToggleInput
80− name={name}
81− className="mt1"
82− inverseLabel={<SecretShowIcon />}
83− label={<SecretHideIcon />}
84− />
85− </AccessoryIcon>
86− );
87−}
88−
89−export default function Ticket({ className, goHome }) {
90− useLoginView(WALLET_TYPES.TICKET);
91−
92− const { setUrbitWallet } = useWallet();
93− const { setPointCursor } = usePointCursor();
94− const impliedPoint = useImpliedPoint();
95− const didWarn = useRef(false);
96− const { getPoints } = useRoller();
97−
98− const validateForm = useCallback((values, errors) => {
99− didWarn.current = false;
100−
101− if (errors.point) {
102− return errors;
103− }
104−
105− if (values.useShards) {
106− if (errors.shard1 || errors.shard2 || errors.shard3) {
107− return errors;
108− }
109− const empty = ['shard1', 'shard2', 'shard3'].filter(s => !values[s]);
110− if (empty.length > 1) {
111− let errors = {};
112− empty.forEach(s => {
113− errors[s] = 'Please provide at least two out of three shards.';
114− });
115− return errors;
116− }
117− } else {
118− if (errors.ticket) {
119− return errors;
120− }
121− }
122− }, []);
123−
124− const onSubmit = useCallback(
125− async values => {
126− const ticket = values.useShards
127− ? kg.combine(
128− [values.shard1, values.shard2, values.shard3].map(v =>
129− v === '' ? undefined : v
130− )
131− )
132− : values.ticket;
133−
134− try {
135− // const _contracts = need.contracts(contracts);
136− const point = patp2dec(values.point);
137−
138− await timeout(16); // allow ui events to flush
139− const urbitWallet = await urbitWalletFromTicket(
140− ticket,
141− point,
142− values.passphrase
143− );
144− const [ownedPoints, incomingPoints] = await Promise.all([
145− getPoints('own', urbitWallet.ownership.keys.address),
146− getPoints('transfer', urbitWallet.ownership.keys.address),
147− ]);
148− const isOwner = ownedPoints.length !== 0;
149− const isTransferProxy = incomingPoints.length !== 0;
150−
151− const noPermissions = !isOwner && !isTransferProxy;
152− // warn the user
153− if (noPermissions && !didWarn.current) {
154− didWarn.current = true;
155− return {
156− [WARNING]:
157− 'This wallet is not the owner or transfer proxy for this point.',
158− };
159− }
160−
161− setUrbitWallet(Just(urbitWallet));
162− setPointCursor(Just(patp2dec(values.point)));
163− } catch (error) {
164− console.error(error);
165− return {
166− [FORM_ERROR]: `Unable to derive wallet from ${
167− values.useShards ? 'shards' : 'ticket'
168− }.`,
169− };
170− }
171− },
172− [setPointCursor, setUrbitWallet, getPoints]
173− );
174−
175− const validate = useMemo(
176− () =>
177− composeValidator(
178− {
179− useAdvanced: buildCheckboxValidator(),
180− usePassphrase: buildCheckboxValidator(),
181− useShards: buildCheckboxValidator(),
182− point: buildPointValidator(4),
183− ticket: buildPatqValidator(),
184− shard1: buildShardValidator(),
185− shard2: buildShardValidator(),
186− shard3: buildShardValidator(),
187− ticketHidden: buildCheckboxValidator(),
188− passphrase: buildPassphraseValidator(),
189− },
190− validateForm
191− ),
192− [validateForm]
193− );
194−
195− const initialValues = useMemo(
196− () => ({
197− point: impliedPoint || '',
198− usePasshrase: false,
199− useShards: false,
200− ticketHidden: true,
201− useAdvanced: false,
202− }),
203− [impliedPoint]
204− );
205−
206− return (
207− <Grid className={className}>
208− <BridgeForm
209− validate={validate}
210− onSubmit={onSubmit}
211− afterSubmit={goHome}
212− className="flex-col justify-between"
213− initialValues={initialValues}>
214− {({ handleSubmit, values, submitting }) => (
215− <Grid.Item full className="flex-col justify-between">
216− <Grid.Item full>
217− <Grid.Item full as={PointInput} name="point" />
218−
219− <Condition when="useShards" is={false}>
220− <Grid.Item
221− full
222− as={TicketInput}
223− hidden={values.ticketHidden}
224− labelAccessory={<AdvancedOptions />}
225− accessory={<TicketInputAccessory name="ticketHidden" />}
226− className="mt3"
227− name="ticket"
228− label="Master Ticket"
229− />
230− </Condition>
231−
232− <Condition when="useShards" is={true}>
233− <Grid.Item
234− full
235− as={TicketInput}
236− accessory={<TicketInputAccessory name="ticketHidden" />}
237− hidden={values.ticketHidden}
238− labelAccessory={<AdvancedOptions />}
239− className="mt3"
240− name="shard1"
241− label="Shard 1"
242− />
243− <Grid.Item
244− full
245− as={TicketInput}
246− accessory={<TicketInputAccessory name="ticketHidden" />}
247− hidden={values.ticketHidden}
248− className="mt3"
249− name="shard2"
250− label="Shard 2"
251− />
252− <Grid.Item
253− full
254− as={TicketInput}
255− className="mt3"
256− accessory={<TicketInputAccessory name="ticketHidden" />}
257− hidden={values.ticketHidden}
258− name="shard3"
259− label="Shard 3"
260− />
261− </Condition>
262−
263− <Condition when="usePassphrase" is={true}>
264− <Grid.Item
265− full
266− as={PassphraseInput}
267− className="mt3"
268− name="passphrase"
269− label="Wallet Passphrase"
270− />
271− </Condition>
272−
273− <Grid.Item full as={FormError} />
274− </Grid.Item>
275−
276− <Grid.Item
277− full
278− as={SubmitButton}
279− center
280− handleSubmit={handleSubmit}>
281− {isWarning =>
282− submitting
283− ? 'Logging in...'
284− : isWarning
285− ? 'Login Anyway'
286− : 'Log In'
287− }
288− </Grid.Item>
289− </Grid.Item>
290− )}
291− </BridgeForm>
292− </Grid>
293− );
294−}
src/views/Login/Ticket.tsxnew file+298
@@ -0,0 +1,298 @@
1+import { useCallback, useMemo, useRef, useState } from 'react';
2+import { Just } from 'folktale/maybe';
3+import * as kg from 'urbit-key-generation';
4+import { Grid, ToggleInput, AccessoryIcon } from 'indigo-react';
5+import { FORM_ERROR } from 'final-form';
6+import { Icon, Row } from '@tlon/indigo-react';
7+
8+import { useWallet } from 'store/wallet';
9+import { usePointCursor } from 'store/pointCursor';
10+
11+import { urbitWalletFromTicket } from 'lib/wallet';
12+import { WALLET_TYPES } from 'lib/constants';
13+import useImpliedPoint from 'lib/useImpliedPoint';
14+import useLoginView from 'lib/useLoginView';
15+import { patp2dec } from 'lib/patp2dec';
16+import { timeout } from 'lib/timeout';
17+import useRoller from 'lib/useRoller';
18+
19+import BridgeForm from 'form/BridgeForm';
20+import Condition from 'form/Condition';
21+import { TicketInput, PassphraseInput, PointInput } from 'form/Inputs';
22+import {
23+ composeValidator,
24+ buildCheckboxValidator,
25+ buildPatqValidator,
26+ buildShardValidator,
27+ buildPassphraseValidator,
28+ buildPointValidator,
29+} from 'form/validators';
30+import FormError from 'form/FormError';
31+import SubmitButton from 'form/SubmitButton';
32+import { WARNING } from 'form/helpers';
33+import Window from 'components/L2/Window/Window';
34+import HeaderPane from 'components/L2/Window/HeaderPane';
35+import BodyPane from 'components/L2/Window/BodyPane';
36+import AdvancedOptions from 'components/L2/Headers/AdvancedOptions';
37+
38+function TicketInputAccessory({ name }: { name: any }) {
39+ return (
40+ <AccessoryIcon>
41+ <ToggleInput
42+ name={name}
43+ className="mt1"
44+ inverseLabel={<Icon className="icon" icon="Visible" />}
45+ label={<Icon className="icon" icon="Hidden" />}
46+ />
47+ </AccessoryIcon>
48+ );
49+}
50+
51+interface TicketProps {
52+ className?: string;
53+ goHome: () => void;
54+}
55+
56+export default function Ticket({ className, goHome }: TicketProps) {
57+ useLoginView(WALLET_TYPES.TICKET);
58+
59+ const { setUrbitWallet }: any = useWallet();
60+ const { setPointCursor }: any = usePointCursor();
61+ const impliedPoint = useImpliedPoint();
62+ const didWarn = useRef(false);
63+ const { getPoints } = useRoller();
64+
65+ const [usePassphrase, setUsePassphrase] = useState(false);
66+ const [useShards, setUseShards] = useState(false);
67+
68+ const validateForm = useCallback((values, errors) => {
69+ didWarn.current = false;
70+
71+ if (errors.point) {
72+ return errors;
73+ }
74+
75+ if (values.useShards) {
76+ if (errors.shard1 || errors.shard2 || errors.shard3) {
77+ return errors;
78+ }
79+ const empty = ['shard1', 'shard2', 'shard3'].filter(s => !values[s]);
80+ if (empty.length > 1) {
81+ let errors = {};
82+ empty.forEach(s => {
83+ errors[s] = 'Please provide at least two out of three shards.';
84+ });
85+ return errors;
86+ }
87+ } else {
88+ if (errors.ticket) {
89+ return errors;
90+ }
91+ }
92+ }, []);
93+
94+ const onSubmit = useCallback(
95+ async values => {
96+ const ticket = values.useShards
97+ ? kg.combine(
98+ [values.shard1, values.shard2, values.shard3].map(v =>
99+ v === '' ? undefined : v
100+ )
101+ )
102+ : values.ticket;
103+
104+ try {
105+ // const _contracts = need.contracts(contracts);
106+ const point = patp2dec(values.point);
107+
108+ await timeout(16); // allow ui events to flush
109+ const urbitWallet = await urbitWalletFromTicket(
110+ ticket,
111+ point,
112+ values.passphrase
113+ );
114+ const [ownedPoints, incomingPoints] = await Promise.all([
115+ getPoints('own', urbitWallet.ownership.keys.address),
116+ getPoints('transfer', urbitWallet.ownership.keys.address),
117+ ]);
118+ const isOwner = ownedPoints.length !== 0;
119+ const isTransferProxy = incomingPoints.length !== 0;
120+
121+ const noPermissions = !isOwner && !isTransferProxy;
122+ // warn the user
123+ if (noPermissions && !didWarn.current) {
124+ didWarn.current = true;
125+ return {
126+ [WARNING]:
127+ 'This wallet is not the owner or transfer proxy for this point.',
128+ };
129+ }
130+
131+ setUrbitWallet(Just(urbitWallet));
132+ setPointCursor(Just(patp2dec(values.point)));
133+ } catch (error) {
134+ console.error(error);
135+ return {
136+ [FORM_ERROR]: `Unable to derive wallet from ${
137+ values.useShards ? 'shards' : 'ticket'
138+ }.`,
139+ };
140+ }
141+ },
142+ [setPointCursor, setUrbitWallet, getPoints]
143+ );
144+
145+ const validate = useMemo(
146+ () =>
147+ composeValidator(
148+ {
149+ useAdvanced: buildCheckboxValidator(),
150+ usePassphrase: buildCheckboxValidator(),
151+ useShards: buildCheckboxValidator(),
152+ point: buildPointValidator(4),
153+ ticket: buildPatqValidator(),
154+ shard1: buildShardValidator(),
155+ shard2: buildShardValidator(),
156+ shard3: buildShardValidator(),
157+ ticketHidden: buildCheckboxValidator(),
158+ passphrase: buildPassphraseValidator(),
159+ },
160+ validateForm
161+ ),
162+ [validateForm]
163+ );
164+
165+ const initialValues = useMemo(
166+ () => ({
167+ point: impliedPoint || '',
168+ usePasshrase: false,
169+ useShards: false,
170+ ticketHidden: true,
171+ useAdvanced: false,
172+ }),
173+ [impliedPoint]
174+ );
175+
176+ const advancedOptions = [
177+ {
178+ selected: usePassphrase,
179+ key: 'usePassphrase',
180+ label: 'Passphrase',
181+ onClick: () => setUsePassphrase(!usePassphrase),
182+ },
183+ {
184+ selected: useShards,
185+ key: 'useShards',
186+ label: 'Shards',
187+ onClick: () => setUseShards(!useShards),
188+ },
189+ ];
190+
191+ const ticketInputClass = usePassphrase && useShards ? 'ticket-input' : '';
192+
193+ return (
194+ <Window className="master-ticket">
195+ <HeaderPane>
196+ <Row className="header-row">
197+ <h5>Master Ticket</h5>
198+ <AdvancedOptions options={advancedOptions} />
199+ </Row>
200+ </HeaderPane>
201+ <BodyPane className="login-body-pane">
202+ <Grid className="input-form" gap={1}>
203+ <BridgeForm
204+ validate={validate}
205+ onSubmit={onSubmit}
206+ afterSubmit={goHome}
207+ className="flex-col justify-between"
208+ onValues={() => null}
209+ initialValues={initialValues}>
210+ {({ handleSubmit, values, submitting }) => (
211+ <Grid.Item full className="flex-col justify-between">
212+ <Grid.Item full>
213+ <Grid.Item
214+ full
215+ as={PointInput}
216+ name="point"
217+ className={ticketInputClass}
218+ />
219+
220+ <Condition when="useShards" is={false}>
221+ <Grid.Item
222+ className={ticketInputClass}
223+ full
224+ as={TicketInput}
225+ hidden={values.ticketHidden}
226+ accessory={<TicketInputAccessory name="ticketHidden" />}
227+ name="ticket"
228+ label="Master Ticket"
229+ style={{ height: 40 }}
230+ />
231+ </Condition>
232+
233+ {useShards && (
234+ <>
235+ <Grid.Item
236+ className={ticketInputClass}
237+ full
238+ as={TicketInput}
239+ accessory={<TicketInputAccessory name="ticketHidden" />}
240+ hidden={values.ticketHidden}
241+ name="shard1"
242+ label="Shard 1"
243+ />
244+ <Grid.Item
245+ className={ticketInputClass}
246+ full
247+ as={TicketInput}
248+ accessory={<TicketInputAccessory name="ticketHidden" />}
249+ hidden={values.ticketHidden}
250+ name="shard2"
251+ label="Shard 2"
252+ />
253+ <Grid.Item
254+ className={ticketInputClass}
255+ full
256+ as={TicketInput}
257+ accessory={<TicketInputAccessory name="ticketHidden" />}
258+ hidden={values.ticketHidden}
259+ name="shard3"
260+ label="Shard 3"
261+ />
262+ </>
263+ )}
264+
265+ {usePassphrase && (
266+ <Grid.Item
267+ full
268+ as={PassphraseInput}
269+ name="passphrase"
270+ label="Wallet Passphrase"
271+ className={ticketInputClass}
272+ />
273+ )}
274+
275+ <Grid.Item full as={FormError} />
276+ </Grid.Item>
277+
278+ <Grid.Item
279+ full
280+ as={SubmitButton}
281+ center
282+ handleSubmit={handleSubmit}>
283+ {(isWarning: boolean) =>
284+ submitting
285+ ? 'Logging in...'
286+ : isWarning
287+ ? 'Login Anyway'
288+ : 'Log In'
289+ }
290+ </Grid.Item>
291+ </Grid.Item>
292+ )}
293+ </BridgeForm>
294+ </Grid>
295+ </BodyPane>
296+ </Window>
297+ );
298+}
src/views/Login/WalletConnect.tsx+81−58
@@ -1,4 +1,4 @@
1−import { Image } from '@tlon/indigo-react';
1+import { Image, Row } from '@tlon/indigo-react';
2import BridgeForm from 'form/BridgeForm';
2import BridgeForm from 'form/BridgeForm';
3import FormError from 'form/FormError';
3import FormError from 'form/FormError';
4import SubmitButton from 'form/SubmitButton';
4import SubmitButton from 'form/SubmitButton';
@@ -8,8 +8,16 @@ import useLoginView from 'lib/useLoginView';
8import { useWalletConnect } from 'lib/useWalletConnect';
8import { useWalletConnect } from 'lib/useWalletConnect';
9import { abbreviateAddress } from 'lib/utils/address';
9import { abbreviateAddress } from 'lib/utils/address';
10import { ForwardButton, RestartButton } from 'components/Buttons';
10import { ForwardButton, RestartButton } from 'components/Buttons';
11+import Window from 'components/L2/Window/Window';
12+import HeaderPane from 'components/L2/Window/HeaderPane';
13+import BodyPane from 'components/L2/Window/BodyPane';
11
14
15+interface WalletConnectLoginProps {
16+ className?: string;
17+ goHome: () => void;
18+}
19+
12−const WalletConnectLogin = ({ className, goHome }) => {
20+const WalletConnectLogin = ({ className, goHome }: WalletConnectLoginProps) => {
13 useLoginView(WALLET_TYPES.WALLET_CONNECT);
21 useLoginView(WALLET_TYPES.WALLET_CONNECT);
14
22
15 const {
23 const {
@@ -30,68 +38,83 @@ const WalletConnectLogin = ({ className, goHome }) => {
30
38
31 return (
39 return (
32 connector && (
40 connector && (
41+ <Window className="master-ticket">
42+ <HeaderPane>
43+ <Row className="header-row">
44+ <h5>WalletConnect</h5>
45+ </Row>
46+ </HeaderPane>
47+ <BodyPane className="login-body-pane">
33− <Grid className={className}>
48+ <Grid className={className}>
34− {!isConnected() && (
49+ {!isConnected() && (
35− <Grid.Item full as={LegacyText} className="f6 gray4 mb3">
50+ <Grid.Item full as={LegacyText} className="f6 gray4 mb3">
36− Note that WalletConnect is a young protocol, not all wallets may
51+ Note that WalletConnect is a young protocol, not all wallets may
37− work fully
52+ work fully
38− </Grid.Item>
39− )}
40−
41− {isConnected() && peerMeta && (
42− <Grid.Item full className="f6 gray5 mt3 mb3">
43− <Grid>
44− <Grid.Item style={{ gridArea: 'auto / 3 / auto / 5' }}>
45− {peerIcon ? (
46− <Image src={peerIcon} />
47− ) : (
48− <LegacyText>Icon Unavailable</LegacyText>
49− )}
50 </Grid.Item>
53 </Grid.Item>
51− <Grid.Item
52− className={'flex-col align-center justify-center'}
53− style={{ gridArea: 'auto / 6 / auto / 11' }}>
54− <LegacyText as="div">{peerMeta.name}</LegacyText>
55− <LegacyText as="div">
56− {address ? abbreviateAddress(address) : null}
57− </LegacyText>
58− <RestartButton as="a" className={'gray3'} onClick={disconnect}>
59− disconnect
60− </RestartButton>
61− </Grid.Item>
62− </Grid>
63− </Grid.Item>
64− )}
54+ )}
65−
66− <BridgeForm onSubmit={onSubmit} afterSubmit={goHome}>
67− {({ handleSubmit, submitting }) => (
68− <Grid.Item full className="flex-col justify-end">
69− {isConnected() ? (
70− <>
71− <Grid.Item full as={FormError} />
72
55
73− <Grid.Item full as={SubmitButton} handleSubmit={handleSubmit}>
56+ {isConnected() && peerMeta && (
74− {submitting
57+ <Grid.Item full className="f6 gray5 mt3 mb3">
75− ? 'Please check your WalletConnect wallet'
58+ <Grid>
76− : 'Authenticate'}
59+ <Grid.Item style={{ gridArea: 'auto / 3 / auto / 5' }}>
60+ {peerIcon ? (
61+ <Image src={peerIcon} />
62+ ) : (
63+ <LegacyText>Icon Unavailable</LegacyText>
64+ )}
77 </Grid.Item>
65 </Grid.Item>
78− </>
79− ) : (
80− <>
81 <Grid.Item
66 <Grid.Item
82− full
67+ className={'flex-col align-center justify-center'}
83− center
68+ style={{ gridArea: 'auto / 6 / auto / 11' }}>
84− as={ForwardButton}
69+ <LegacyText as="div">{peerMeta.name}</LegacyText>
85− solid
70+ <LegacyText as="div">
71+ {address ? abbreviateAddress(address) : null}
72+ </LegacyText>
73+ <RestartButton
74+ as="a"
75+ className={'gray3'}
86− onClick={connect}>
76+ onClick={disconnect}>
87− {'Connect'}
77+ disconnect
78+ </RestartButton>
88 </Grid.Item>
79 </Grid.Item>
80+ </Grid>
81+ </Grid.Item>
82+ )}
83+
84+ <BridgeForm onSubmit={onSubmit} afterSubmit={goHome}>
85+ {({ handleSubmit, submitting }) => (
86+ <Grid.Item full className="flex-col justify-end">
87+ {isConnected() ? (
88+ <>
89+ <Grid.Item full as={FormError} />
90+
91+ <Grid.Item
92+ full
93+ as={SubmitButton}
94+ handleSubmit={handleSubmit}>
95+ {submitting
96+ ? 'Please check your WalletConnect wallet'
97+ : 'Authenticate'}
98+ </Grid.Item>
99+ </>
100+ ) : (
101+ <>
102+ <Grid.Item
103+ full
104+ center
105+ as={ForwardButton}
106+ solid
107+ onClick={connect}>
108+ {'Connect'}
109+ </Grid.Item>
89− </>
110+ </>
111+ )}
112+ </Grid.Item>
90 )}
113 )}
91− </Grid.Item>
114+ </BridgeForm>
92− )}
115+ </Grid>
93− </BridgeForm>
116+ </BodyPane>
94− </Grid>
117+ </Window>
95 )
118 )
96 );
119 );
97};
120};
src/views/Point/IssueChild.tsx+1−1
@@ -202,7 +202,7 @@ export default function IssueChild() {
202 <Box className={'patp-badge-container'}>
202 <Box className={'patp-badge-container'}>
203 <Box className="badges">
203 <Box className="badges">
204 {candidates.map(p => (
204 {candidates.map(p => (
205− <PatpBadge className={'patp-badge'}>
205+ <PatpBadge className={'patp-badge'} key={p}>
206 {p}
206 {p}
207 </PatpBadge>
207 </PatpBadge>
208 ))}
208 ))}
src/views/Point/Point.tsx−2
@@ -44,8 +44,6 @@ export default function Point() {
44
44
45 const pointRef = useRef<number | null>(null);
45 const pointRef = useRef<number | null>(null);
46
46
47− const invitePoints = invites[point.value] || [];
48−
49 const hideModal = useCallback(() => {
47 const hideModal = useCallback(() => {
50 if (hideMessage) {
48 if (hideMessage) {
51 setSeenMissingKeys(true);
49 setSeenMissingKeys(true);
src/views/Points.js+4−1
@@ -27,6 +27,7 @@ import NavHeader from 'components/NavHeader';
27import L2PointHeader from 'components/L2/Headers/L2PointHeader';
27import L2PointHeader from 'components/L2/Headers/L2PointHeader';
28import IncomingPoint from 'components/L2/Points/IncomingPoint';
28import IncomingPoint from 'components/L2/Points/IncomingPoint';
29import LoadingOverlay from 'components/L2/LoadingOverlay';
29import LoadingOverlay from 'components/L2/LoadingOverlay';
30+import { onlyUnique } from 'lib/utils/array';
30
31
31export const maybeGetResult = (obj, key, defaultValue) =>
32export const maybeGetResult = (obj, key, defaultValue) =>
32 obj.matchWith({
33 obj.matchWith({
@@ -242,7 +243,9 @@ export default function Points() {
242 ...managingPoints,
243 ...managingPoints,
243 ...votingPoints,
244 ...votingPoints,
244 ...spawningPoints,
245 ...spawningPoints,
246+ ]
245− ].sort((a, b) => Number(a) - Number(b));
247+ .sort((a, b) => Number(a) - Number(b))
248+ .filter(onlyUnique);
246
249
247 const displayEmptyState =
250 const displayEmptyState =
248 !loading && incomingPoints.length === 0 && allPoints.length === 0;
251 !loading && incomingPoints.length === 0 && allPoints.length === 0;
src/views/Release/Active.js+1−1
@@ -77,7 +77,7 @@ export default function ActiveView({ className, goPoint }) {
77 .filter(p => 255 < p && p < 65536);
77 .filter(p => 255 < p && p < 65536);
78
78
79 return (
79 return (
80− <Grid full className={cn('f5', className)}>
80+ <Grid className={cn('f5', className)}>
81 <Grid.Item className="pv3 " cols={[1, 6]} third={1}>
81 <Grid.Item className="pv3 " cols={[1, 6]} third={1}>
82 Point
82 Point
83 </Grid.Item>
83 </Grid.Item>
src/views/Transactions/TransactionHistory.tsx+4−4
@@ -39,7 +39,7 @@ const TransactionHistory = () => {
39 const filterByPoint = useMemo(() => {
39 const filterByPoint = useMemo(() => {
40 return data?.filterByPoint.matchWith({
40 return data?.filterByPoint.matchWith({
41 Nothing: () => null,
41 Nothing: () => null,
42− Just: (p: number) => ob.patp(p.value).replace('~', ''),
42+ Just: (p: any) => ob.patp(p.value),
43 });
43 });
44 }, [data]);
44 }, [data]);
45
45
@@ -112,7 +112,7 @@ const TransactionHistory = () => {
112 toggleOpen={() => setDropdownOpen(!dropdownOpen)}>
112 toggleOpen={() => setDropdownOpen(!dropdownOpen)}>
113 <Box className="divider" />
113 <Box className="divider" />
114 <Box className="points">
114 <Box className="points">
115− <Row className="entry" onClick={() => selectPatp(null)}>
115+ <Row className="entry mono" onClick={() => selectPatp(null)}>
116 <Box>{shortAddress}</Box>
116 <Box>{shortAddress}</Box>
117 <Row>
117 <Row>
118 <Box
118 <Box
@@ -125,10 +125,10 @@ const TransactionHistory = () => {
125 {txPatps.map(patp => {
125 {txPatps.map(patp => {
126 return (
126 return (
127 <Row
127 <Row
128− className="entry"
128+ className="entry mono"
129 onClick={() => selectPatp(patp)}
129 onClick={() => selectPatp(patp)}
130 key={patp}>
130 key={patp}>
131− <Box>{patp}</Box>
131+ <Box>~{patp}</Box>
132 <Row>
132 <Row>
133 <Box className="sigil">
133 <Box className="sigil">
134 <Sigil
134 <Sigil
src/views/UrbitID/Home.tsx+35−24
@@ -7,7 +7,7 @@ import { PROXY_TYPE } from 'lib/proxy';
7import * as need from 'lib/need';
7import * as need from 'lib/need';
8import useCurrentPermissions from 'lib/useCurrentPermissions';
8import useCurrentPermissions from 'lib/useCurrentPermissions';
9import { convertToInt } from 'lib/convertToInt';
9import { convertToInt } from 'lib/convertToInt';
10−import { ETH_ZERO_ADDR } from 'lib/constants';
10+import { ETH_ZERO_ADDR, MASTER_TICKET_TOOLTIP } from 'lib/constants';
11import { abbreviateAddress } from 'lib/utils/address';
11import { abbreviateAddress } from 'lib/utils/address';
12import { downloadWallet } from 'lib/invite';
12import { downloadWallet } from 'lib/invite';
13import useKeyfileGenerator from 'lib/useKeyfileGenerator';
13import useKeyfileGenerator from 'lib/useKeyfileGenerator';
@@ -23,6 +23,8 @@ import CopiableWithTooltip from 'components/copiable/CopiableWithTooltip';
23import PaperBuilder from 'components/PaperBuilder';
23import PaperBuilder from 'components/PaperBuilder';
24
24
25import './UrbitID.scss';
25import './UrbitID.scss';
26+import LayerIndicator from 'components/L2/LayerIndicator';
27+import WithTooltip from 'components/WithTooltip';
26
28
27export default function UrbitIDHome() {
29export default function UrbitIDHome() {
28 const { push, names }: any = useLocalRouter();
30 const { push, names }: any = useLocalRouter();
@@ -59,7 +61,6 @@ export default function UrbitIDHome() {
59
61
60 const noManagement = details.managementProxy === ETH_ZERO_ADDR;
62 const noManagement = details.managementProxy === ETH_ZERO_ADDR;
61 const noSpawn = details.spawnProxy === ETH_ZERO_ADDR;
63 const noSpawn = details.spawnProxy === ETH_ZERO_ADDR;
62− const showSpawn = point.isL1 && !point.isL2Spawn;
63
64
64 return (
65 return (
65 <Window className="id-home">
66 <Window className="id-home">
@@ -125,40 +126,50 @@ export default function UrbitIDHome() {
125 </Row>
126 </Row>
126 )}
127 )}
127
128
128− {point.isParent && networkRevision !== 0 && showSpawn && (
129+ {point.isParent && networkRevision !== 0 && (
129 <Row className="between-row management">
130 <Row className="between-row management">
130 <Box>
131 <Box>
131 <Box>Spawn Proxy Address</Box>
132 <Box>Spawn Proxy Address</Box>
132 <div className="mt1 mono black subtitle">
133 <div className="mt1 mono black subtitle">
134+ {noSpawn
135+ ? point.isL2Spawn
136+ ? 'Layer 2'
137+ : 'Unset'
133− {noSpawn ? 'Unset' : abbreviateAddress(details.spawnProxy)}
138+ : abbreviateAddress(details.spawnProxy)}
134 </div>
139 </div>
135 </Box>
140 </Box>
141+ {point.isL2Spawn ? (
142+ <LayerIndicator size="lg" layer={2} />
143+ ) : (
136− <Row>
144+ <Row>
137− <Button
145+ <Button
138− className="secondary"
146+ className="secondary"
139− onClick={() => goSetProxy(PROXY_TYPE.SPAWN)}>
147+ onClick={() => goSetProxy(PROXY_TYPE.SPAWN)}>
140− {noSpawn ? 'Set' : 'Change'}
148+ {noSpawn ? 'Set' : 'Change'}
141− </Button>
149+ </Button>
142− {!noSpawn && (
150+ {!noSpawn && (
143− <CopiableWithTooltip
151+ <CopiableWithTooltip
144− text={details.spawnProxy}
152+ text={details.spawnProxy}
145− className="copy-button"
153+ className="copy-button"
146− />
154+ />
147− )}
155+ )}
148− </Row>
156+ </Row>
157+ )}
149 </Row>
158 </Row>
150 )}
159 )}
151
160
152 {isOwner && (
161 {isOwner && (
153 <Row className="between-row management">
162 <Row className="between-row management">
154− <Box>
163+ <WithTooltip content={MASTER_TICKET_TOOLTIP}>
164+ <Box>
155− <Box>Master Ticket</Box>
165+ <Box>Master Ticket</Box>
156− <Box className="subtitle">
166+ <Box className="subtitle">
157− {isMasterTicket
167+ {isMasterTicket
158− ? 'Reset Master Ticket and all other keys'
168+ ? 'Reset Master Ticket and all other keys'
159− : 'Transfer to Master Ticket, resetting all keys'}
169+ : 'Transfer to Master Ticket, resetting all keys'}
170+ </Box>
160 </Box>
171 </Box>
161− </Box>
172+ </WithTooltip>
162 <Button className="secondary" onClick={goResetKeys}>
173 <Button className="secondary" onClick={goResetKeys}>
163 {isMasterTicket ? 'Reset' : 'Transfer'}
174 {isMasterTicket ? 'Reset' : 'Transfer'}
164 </Button>
175 </Button>