on master help sign in
Update login and point transfer screens
commit9c28b5a1585f241bb08ed018127928c6545ecb10 authorWill Galebach <willbach@gmail.com> dateSun 21 Nov 2021, 19:51 (5 years ago) parent8a36573
36 files changed+1030−670
package-lock.json+3−3 package.json+1−1 src/components/L2/Dropdowns/AccountsDropdown.scss+11 src/components/L2/Dropdowns/AccountsDropdown.tsx+16−10 src/components/L2/Dropdowns/Dropdown.scss+3−2 src/components/L2/Headers/AdvancedOptions.scss+54 src/components/L2/Headers/AdvancedOptions.tsx+52 src/components/L2/Headers/L2BackHeader.tsx+1−1 src/components/L2/Headers/L2PointHeader.tsx+16−12 src/components/L2/LayerIndicator.scss+13−2 src/components/L2/LayerIndicator.tsx+12−3 src/components/L2/LoadingOverlay.tsx+2−2 src/form/Inputs.tsx+2−1 src/lib/constants.ts+8 src/lib/useRoller.ts+32−5 src/lib/utils/array.ts+2 src/lib/utils/point.ts+2−2 src/store/lib/useControlledPointsStore.js+15−8 src/store/rollerStore.ts+14 src/style/shared/variables.scss−1 src/views/AcceptTransfer.js+87−40 src/views/CancelTransfer.js+66−27 src/views/Invite/Cohort.tsx+9−10 src/views/Login/LoginSelector.scss+20−1 src/views/Login/LoginSelector.tsx+12−24 src/views/Login/Mnemonic.js−130 src/views/Login/Mnemonic.tsx+153 src/views/Login/Ticket.js−294 src/views/Login/Ticket.tsx+298 src/views/Login/WalletConnect.tsx+81−58 src/views/Point/IssueChild.tsx+1−1 src/views/Point/Point.tsx−2 src/views/Points.js+4−1 src/views/Release/Active.js+1−1 src/views/Transactions/TransactionHistory.tsx+4−4 src/views/UrbitID/Home.tsx+35−24
package-lock.json+3−3
@@ -32765,9 +32765,9 @@
3276532765 }
3276632766 },
3276732767 "urbit-paper-renderer": {
32768− "version": "2.0.9",
32769− "resolved": "https://registry.npmjs.org/urbit-paper-renderer/-/urbit-paper-renderer-2.0.9.tgz",
32770− "integrity": "sha512-WETgzutzP6bQ/ghdp2TGetN8YcxegvKXvknGBL/9mBBblPwCJV2aFP7I4gslmL+IuboRJwP9u9HSE+SeSOTXLg==",
32768+ "version": "2.0.10",
32769+ "resolved": "https://registry.npmjs.org/urbit-paper-renderer/-/urbit-paper-renderer-2.0.10.tgz",
32770+ "integrity": "sha512-thQaV7EwtuyWR/Ti3chuqsIBdaqkiGYi+gH9KSEKKAyLUIhAX3ZDSA2RFCnQiLHB+4k5Rnq6kLmoZVAspc+/RA==",
3277132771 "requires": {
3277232772 "lodash.get": "^4.4.2",
3277332773 "prop-types": "^15.7.2",
package.json+1−1
@@ -81,7 +81,7 @@
8181 "typescript": "^4.3.2",
8282 "urbit-key-generation": "^0.19.0",
8383 "urbit-ob": "^4.1.4",
84− "urbit-paper-renderer": "^2.0.9",
84+ "urbit-paper-renderer": "^2.0.10",
8585 "web3": "^1.3.6",
8686 "web3-utils": "^1.5.1",
8787 "zustand": "^3.5.9"
src/components/L2/Dropdowns/AccountsDropdown.scss+11
@@ -8,6 +8,17 @@
88 font-family: "Source Code Pro", "Roboto mono", "Courier New", monospace;
99 }
1010
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+
1122 .content-border {
1223 .dropdown-content {
1324 .entry, .current-address {
src/components/L2/Dropdowns/AccountsDropdown.tsx+16−10
@@ -14,7 +14,6 @@ import { clearInvitesStorage } from 'store/storage/roller';
1414import { useRollerStore } from 'store/rollerStore';
1515
1616import { abbreviateAddress } from 'lib/utils/address';
17−import useCurrentPermissions from 'lib/useCurrentPermissions';
1817
1918import Dropdown from './Dropdown';
2019import './AccountsDropdown.scss';
@@ -57,18 +56,19 @@ const AccountsDropdown = ({ showMigrate = false }: AccountsDropdownProps) => {
5756 push,
5857 ]);
5958
60− const goIssuePoint = useCallback(() => push(names.ISSUE_CHILD), [
61− names.ISSUE_CHILD,
62− push,
63− ]);
59+ const goSpawn = useCallback(() => {
60+ if (point.isL2Spawn && point.isStar) {
61+ push(names.INVITE_COHORT);
62+ } else {
63+ push(names.ISSUE_CHILD);
64+ }
65+ }, [names, push, point]);
6466
6567 const onLogout = () => {
6668 clearInvitesStorage();
6769 reset();
6870 };
6971
70− const { canSpawn, isParent, isStar } = useCurrentPermissions();
71−
7272 const hasL1Points = Boolean(pointList.find(({ layer }) => layer === 1));
7373 const showMigrateOption = useMemo(() => {
7474 return showMigrate || hasL1Points;
@@ -123,9 +123,15 @@ const AccountsDropdown = ({ showMigrate = false }: AccountsDropdownProps) => {
123123 <Icon icon="Bitcoin" />
124124 </Row>
125125 )}
126− {isParent && canSpawn && (
127− <Row className="entry" onClick={goIssuePoint}>
128− <Box>{isStar ? 'Generate Invites' : 'Spawn Stars'}</Box>
126+ {point.isParent && point.canSpawn && (
127+ <Row className="entry" onClick={goSpawn}>
128+ <Box>
129+ {point.isStar
130+ ? point.isL2Spawn
131+ ? 'Generate Invites'
132+ : 'Spawn Planets'
133+ : 'Spawn Stars'}
134+ </Box>
129135 <Icon icon="Plus" />
130136 </Row>
131137 )}
src/components/L2/Dropdowns/Dropdown.scss+3−2
@@ -5,6 +5,7 @@
55 position: relative;
66
77 .close-background {
8+ z-index: 1;
89 position: fixed;
910 top: 0;
1011 bottom: 0;
@@ -39,14 +40,14 @@
3940 top: 36px;
4041 left: -3px;
4142 border-radius: $medium-border-radius;
42− z-index: 1;
43+ z-index: 2;
4344
4445 .dropdown-content {
4546 border-radius: $medium-border-radius;
4647 background-color: white;
4748 border: 1px solid $light-gray;
4849 width: 240px;
49− max-height: 200px;
50+ max-height: 400px;
5051 overflow-y: scroll;
5152 -ms-overflow-style: none;
5253 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 = ({
2424 className = '',
2525 hideBalance = false,
2626}: L2BackHeaderProps) => {
27− const { point } = useRollerStore(store => store);
27+ const { point } = useRollerStore();
2828 const { nextRoll } = useTimerStore();
2929 const { pop }: any = useHistory();
3030 const { wallet }: any = useWallet();
src/components/L2/Headers/L2PointHeader.tsx+16−12
@@ -1,7 +1,6 @@
11import React, { useCallback } from 'react';
22import { Box, LoadingSpinner, Row } from '@tlon/indigo-react';
3−
4−import { isStar } from 'lib/utils/point';
3+import { Nothing } from 'folktale/maybe';
54
65import { useHistory } from 'store/history';
76import { useRollerStore } from 'store/rollerStore';
@@ -13,6 +12,7 @@ import { ReactComponent as InviteIcon } from 'assets/invite.svg';
1312import HeaderButton from './HeaderButton';
1413import AccountsDropdown from '../Dropdowns/AccountsDropdown';
1514import './L2PointHeader.scss';
15+import LayerIndicator from '../LayerIndicator';
1616
1717export interface L2PointHeaderProps {
1818 numInvites?: number;
@@ -23,18 +23,22 @@ export interface L2PointHeaderProps {
2323}
2424
2525const L2PointHeader = ({
26− numInvites,
26+ numInvites = 0,
2727 hideHome = false,
2828 hideTimer = false,
2929 hideInvites = false,
3030 showMigrate = false,
3131}: L2PointHeaderProps) => {
32− const { point, pendingTransactions } = useRollerStore();
32+ const { point, pendingTransactions, invitesLoading } = useRollerStore();
3333 const { nextRoll } = useTimerStore();
34− const { pointCursor }: any = usePointCursor();
34+ const { pointCursor, setPointCursor }: any = usePointCursor();
3535
3636 const { popTo, push, names }: any = useHistory();
3737
38+ const goToHome = useCallback(() => {
39+ popTo(names.POINTS);
40+ setPointCursor(Nothing());
41+ }, [popTo, setPointCursor, names]);
3842 const goToInvites = useCallback(() => push(names.INVITE_COHORT), [
3943 push,
4044 names.INVITE_COHORT,
@@ -45,25 +49,25 @@ const L2PointHeader = ({
4549 );
4650
4751 const showInvites =
48− !hideInvites && Boolean(pointCursor?.value && isStar(pointCursor.value));
52+ !hideInvites &&
53+ Boolean(pointCursor?.value && point.isStar && point.isL2Spawn);
4954
5055 return (
5156 <Row className="l2-point-header">
5257 <Row>
5358 {!hideHome && (
54− <HeaderButton
55− className="home"
56− icon="Home"
57− onClick={() => popTo(names.POINTS)}
58− />
59+ <HeaderButton className="home" icon="Home" onClick={goToHome} />
5960 )}
6061 <AccountsDropdown showMigrate={showMigrate} />
62+ {point.value > -1 && (
63+ <LayerIndicator size="lg" layer={point.layer} className="ml2" />
64+ )}
6165 </Row>
6266 <Row className="info">
6367 {showInvites && point.showInvites && (
6468 <Row onClick={goToInvites} className="invites">
6569 <InviteIcon />
66− {numInvites === undefined ? (
70+ {invitesLoading ? (
6771 <LoadingSpinner foreground="rgba(0,0,0,0.3)" background="white" />
6872 ) : (
6973 numInvites
src/components/L2/LayerIndicator.scss+13−2
@@ -12,12 +12,15 @@
1212 background-color: black;
1313 }
1414
15+ &.sm, &.md, &.lg {
16+ text-align: center;
17+ }
18+
1519 &.sm {
1620 height: 16px;
1721 padding: 2px 4px 0;
1822 font-size: 12px;
1923 width: 14px;
20− text-align: center;
2124 }
2225
2326 &.md {
@@ -26,6 +29,14 @@
2629 font-size: 14px;
2730 margin-right: 4px;
2831 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;
3041 }
3142}
src/components/L2/LayerIndicator.tsx+12−3
@@ -5,11 +5,20 @@ import './LayerIndicator.scss';
55
66export interface LayerIndicatorProps {
77 layer: 1 | 2;
8− size: 'sm' | 'md';
8+ size?: 'sm' | 'md' | 'lg';
9+ className?: string;
910}
1011
11−const LayerIndicator = ({ layer, size }: LayerIndicatorProps) => {
12− return <Box className={`layer-indicator ${size} l${layer}`}>L{layer}</Box>;
12+const LayerIndicator = ({
13+ layer,
14+ size = 'md',
15+ className = '',
16+}: LayerIndicatorProps) => {
17+ return (
18+ <Box className={`layer-indicator ${size} l${layer} ${className}`}>
19+ L{layer}
20+ </Box>
21+ );
1322};
1423
1524export default LayerIndicator;
src/components/L2/LoadingOverlay.tsx+2−2
@@ -3,7 +3,7 @@ import { Box, LoadingSpinner } from '@tlon/indigo-react';
33import './LoadingOverlay.scss';
44
55const LoadingOverlay = ({
6− loading = false,
6+ loading,
77 text,
88}: {
99 loading: boolean;
@@ -16,7 +16,7 @@ const LoadingOverlay = ({
1616 return (
1717 <Box className="loading-overlay">
1818 <Box className={`${text ? 'solid' : ''}`}>
19− {text && <Box className="loader-text">{text}</Box>}
19+ {!!text && <Box className="loader-text">{text}</Box>}
2020 <LoadingSpinner foreground="white" background="rgba(0,0,0,0.3)" />
2121 </Box>
2222 </Box>
src/form/Inputs.tsx+2−1
@@ -33,7 +33,7 @@ const PLACEHOLDER_EMAIL = 'email@example.com';
3333
3434const formatPat = buildFormatter([downcase, ensurePatFormat]);
3535
36−export function TicketInput({ name, hidden, ...rest }) {
36+export function TicketInput({ name, hidden, className, ...rest }) {
3737 const {
3838 meta: { valid, error, validating, touched, active },
3939 } = useField(name, {
@@ -48,6 +48,7 @@ export function TicketInput({ name, hidden, ...rest }) {
4848
4949 return (
5050 <Input
51+ className={className}
5152 type={hidden ? 'password' : 'text'}
5253 name={name}
5354 obscure={value => value.replace(/[^~-]+/g, '••••••')}
src/lib/constants.ts+8
@@ -167,6 +167,12 @@ const DUMMY_L2_ADDRESS = '0x1111111111111111111111111111111111111111';
167167const ONE_SECOND = 1000;
168168const TWO_SECONDS = 2000;
169169
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+
170176export {
171177 CHECK_BLOCK_EVERY_MS,
172178 DEFAULT_GAS_PRICE_GWEI,
@@ -208,4 +214,6 @@ export {
208214 DUMMY_L2_ADDRESS,
209215 ONE_SECOND,
210216 TWO_SECONDS,
217+ MASTER_TICKET_TOOLTIP,
218+ PASSPORT_TOOLTIP,
211219};
src/lib/useRoller.ts+32−5
@@ -55,6 +55,7 @@ import {
5555import Point, { Points, Relationship } from './types/Point';
5656import { useTimerStore } from 'store/timerStore';
5757import { ReticketParams, SendL2Params } from './types/L2Transaction';
58+import { L1Point } from './types/L1Point';
5859
5960const ONE_SECOND = 1000;
6061
@@ -89,10 +90,12 @@ export default function useRoller() {
8990 const {
9091 nextBatchTime,
9192 point,
93+ removeInvite,
9294 setNextBatchTime,
9395 setPendingTransactions,
9496 setInvites,
9597 setInviteGeneratingNum,
98+ setInvitesLoading,
9699 setPoints,
97100 updateInvite,
98101 } = useRollerStore();
@@ -135,6 +138,7 @@ export default function useRoller() {
135138 api
136139 .getRollerConfig()
137140 .then(response => {
141+ // console.log('CONFIG', response)
138142 setConfig(response);
139143 setNextBatchTime(response.nextBatch);
140144 })
@@ -318,6 +322,7 @@ export default function useRoller() {
318322 const getInvites = useCallback(
319323 async (isL2: boolean) => {
320324 try {
325+ setInvitesLoading(true);
321326 const curPoint: number = Number(need.point(pointCursor));
322327 const _authToken = authToken.getOrElse(null);
323328 const _contracts = contracts.getOrElse(null);
@@ -343,7 +348,13 @@ export default function useRoller() {
343348 .concat(
344349 outgoingPoints.filter((p: number) => !spawnedPoints.includes(p))
345350 )
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+ );
347358
348359 const newInvites: Invite[] = [];
349360 // Iterate over all of the stored invites, generating wallet info as necessary
@@ -367,6 +378,8 @@ export default function useRoller() {
367378 ) {
368379 newInvites.push(invite);
369380 updateInvite(curPoint, invite);
381+ } else {
382+ removeInvite(curPoint, invite.planet);
370383 }
371384
372385 if (storedInvite) {
@@ -374,7 +387,7 @@ export default function useRoller() {
374387 }
375388 }
376389 setInvites(curPoint, newInvites);
377−
390+ setInvitesLoading(false);
378391 return newInvites;
379392 }
380393 } catch (error) {
@@ -389,7 +402,9 @@ export default function useRoller() {
389402 getDetails,
390403 pointCursor,
391404 ls,
405+ removeInvite,
392406 setInvites,
407+ setInvitesLoading,
393408 setInviteGeneratingNum,
394409 setPendingTransactions,
395410 updateInvite,
@@ -739,17 +754,27 @@ export default function useRoller() {
739754 if (!_wallet) {
740755 return EMPTY_POINT;
741756 }
757+
758+ const pointNum = Number(point);
742759 try {
743− const pointNum = Number(point);
744760 const rawDetails = await api.getPoint(pointNum);
745761 const details = toL1Details(rawDetails);
746762
747763 return new Point(pointNum, relationship, details, _wallet.address);
748764 } catch (e) {
749765 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+ }
750775 }
751776 },
752− [api, wallet]
777+ [api, wallet, getDetails]
753778 );
754779
755780 const getPointDetails = useCallback(
@@ -798,6 +823,8 @@ export default function useRoller() {
798823 }, [config, fetchConfig]);
799824
800825 useEffect(() => {
826+ const time = isDevelopment ? 10000 : ONE_SECOND;
827+
801828 const interval = setInterval(() => {
802829 const nextRoll = getTimeToNextBatch(nextBatchTime, new Date().getTime());
803830 setNextRoll(nextRoll);
@@ -807,7 +834,7 @@ export default function useRoller() {
807834 setNextBatchTime(response.nextBatch);
808835 });
809836 }
810− }, 10000000);
837+ }, time);
811838
812839 return () => clearInterval(interval);
813840 }, [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) =>
1111export const isPlanet = (point: number) =>
1212 azimuth.getPointSize(point) === azimuth.PointSize.Planet;
1313
14−export const toL1Details = (point: L2Point): L1Point => {
14+export const toL1Details = (point?: L2Point): L1Point => {
1515 return {
1616 layer: point?.dominion === 'l2' ? 2 : 1,
17− isL2Spawn: point.dominion === 'spawn' || point.dominion === 'l2',
17+ isL2Spawn: point?.dominion === 'spawn' || point?.dominion === 'l2',
1818 active: true,
1919 authenticationKey: point?.network?.keys?.auth,
2020 continuityNumber: point?.network?.rift,
src/store/lib/useControlledPointsStore.js+15−8
@@ -3,11 +3,10 @@ import { Just, Nothing } from 'folktale/maybe';
33import Result from 'folktale/result';
44import * as azimuth from 'azimuth-js';
55
6−import { useNetwork } from '../network';
76import { useWallet } from 'store/wallet';
87import useRoller from 'lib/useRoller';
9−
10−const onlyUnique = (value, index, self) => self.indexOf(value) === index;
8+import { onlyUnique } from 'lib/utils/array';
9+import { useNetwork } from '../network';
1110
1211export default function useControlledPointsStore() {
1312 const { contracts } = useNetwork();
@@ -69,11 +68,19 @@ export default function useControlledPointsStore() {
6968 );
7069 } else {
7170 getPointDetails(
72− (ownedPointsL1 || []).concat(ownedPointsL2 || []),
73− (incomingPointsL1 || []).concat(incomingPointsL2 || []),
74− (managingPointsL1 || []).concat(managingPointsL2 || []),
75− (votingPointsL1 || []).concat(votingPointsL2 || []),
76− (spawningPointsL1 || []).concat(spawningPointsL2 || [])
71+ (ownedPointsL1 || []).concat(ownedPointsL2 || []).filter(onlyUnique),
72+ (incomingPointsL1 || [])
73+ .concat(incomingPointsL2 || [])
74+ .filter(onlyUnique),
75+ (managingPointsL1 || [])
76+ .concat(managingPointsL2 || [])
77+ .filter(onlyUnique),
78+ (votingPointsL1 || [])
79+ .concat(votingPointsL2 || [])
80+ .filter(onlyUnique),
81+ (spawningPointsL1 || [])
82+ .concat(spawningPointsL2 || [])
83+ .filter(onlyUnique)
7784 );
7885
7986 _setControlledPoints(
src/store/rollerStore.ts+14
@@ -17,6 +17,7 @@ export interface RollerStore {
1717 invites: Invites;
1818 recentlyCompleted: number;
1919 inviteGeneratingNum: number;
20+ invitesLoading: boolean;
2021 setNextBatchTime: (nextBatchTime: number) => void;
2122 setPendingTransactions: (pendingTransactions: PendingTransaction[]) => void;
2223 setPoint: (point: number) => void;
@@ -24,6 +25,8 @@ export interface RollerStore {
2425 setInvites: (points: number, invites: Invite[]) => void;
2526 setInviteGeneratingNum: (numGenerating: number) => void;
2627 updateInvite: (point: number, invite: Invite) => void;
28+ removeInvite: (point: number, planet: number) => void;
29+ setInvitesLoading: (invitesLoading: boolean) => void;
2730}
2831
2932export const useRollerStore = create<RollerStore>(set => ({
@@ -35,6 +38,7 @@ export const useRollerStore = create<RollerStore>(set => ({
3538 invites: {},
3639 recentlyCompleted: 0,
3740 inviteGeneratingNum: 0,
41+ invitesLoading: false,
3842 setNextBatchTime: (nextBatchTime: number) => set(() => ({ nextBatchTime })),
3943 setPendingTransactions: (pendingTransactions: PendingTransaction[]) =>
4044 set(state => {
@@ -68,4 +72,14 @@ export const useRollerStore = create<RollerStore>(set => ({
6872 );
6973 return { invites: Object.assign(state.invites, newInvites) };
7074 }),
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 })),
7185}));
src/style/shared/variables.scss−1
@@ -14,4 +14,3 @@ $warning-red: rgb(242, 33, 33);
1414$light-gray-border: 1px solid $light-gray;
1515$activate-blue: rgba(33, 157, 255, 1);
1616$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';
22import cn from 'classnames';
3−import { Grid, Text, CheckboxInput } from 'indigo-react';
3+import { Nothing } from 'folktale/maybe';
4+import { Grid, Text, CheckboxInput, Button } from 'indigo-react';
45import * as azimuth from 'azimuth-js';
56
67import { useNetwork } from 'store/network';
@@ -14,12 +15,18 @@ import useEthereumTransaction from 'lib/useEthereumTransaction';
1415import { GAS_LIMITS } from 'lib/constants';
1516import { useLocalRouter } from 'lib/LocalRouter';
1617
17−import ViewHeader from 'components/ViewHeader';
1818import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
1919import 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';
2024
2125import BridgeForm from 'form/BridgeForm';
2226import { composeValidator, buildCheckboxValidator } from 'form/validators';
27+import { Row } from '@tlon/indigo-react';
28+import { useRollerStore } from 'store/rollerStore';
29+import useRoller from 'lib/useRoller';
2330
2431function useAcceptTransfer() {
2532 const { contracts } = useNetwork();
@@ -51,9 +58,17 @@ function useAcceptTransfer() {
5158
5259export default function AcceptTransfer() {
5360 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);
5467
5568 const name = useCurrentPointName();
5669
70+ const [reset, setReset] = useState(true);
71+
5772 const { completed, bind, inputsLocked, construct } = useAcceptTransfer();
5873
5974 const initialValues = useMemo(() => ({ noReset: false }), []);
@@ -66,50 +81,82 @@ export default function AcceptTransfer() {
6681 const onValues = useCallback(
6782 ({ valid, values, form }) => {
6883 construct(!values.noReset);
84+ setReset(!values.noReset);
6985 },
7086 [construct]
7187 );
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]);
7297
7398 return (
74− <View pop={pop} inset>
75− <Grid>
76− <Grid.Item full as={ViewHeader}>
77− Accept Transfer
78− </Grid.Item>
79−
80− <Grid.Item
81− full
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− {() => (
99+ <View
100+ pop={pop}
101+ inset
102+ className="cancel-transfer"
103+ hideBack
104+ header={<L2BackHeader hideBalance back={goBack} />}>
105+ <Window>
106+ <HeaderPane>
107+ <Row className="header-row">
108+ <h5>Accept Transfer</h5>
109+ </Row>
110+ </HeaderPane>
111+ <BodyPane>
112+ <Grid>
96113 <Grid.Item
97114 full
98− as={CheckboxInput}
99− name="noReset"
100− label="Retain proxies and key configuration, in case of transferring to self"
101− disabled={inputsLocked}
102− />
103− )}
104− </BridgeForm>
105−
106− <Grid.Item
107− full
108− as={InlineEthereumTransaction}
109− {...bind}
110− onReturn={() => pop()}
111− />
112− </Grid>
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
131+ as={CheckboxInput}
132+ name="noReset"
133+ label="Retain proxies and key configuration, in case of transferring to self"
134+ disabled={inputsLocked}
135+ />
136+ )}
137+ </BridgeForm>
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+ ) : (
150+ <Grid.Item
151+ full
152+ as={InlineEthereumTransaction}
153+ {...bind}
154+ onReturn={() => pop()}
155+ />
156+ )}
157+ </Grid>
158+ </BodyPane>
159+ </Window>
113160 </View>
114161 );
115162}
src/views/CancelTransfer.js+66−27
@@ -1,11 +1,14 @@
11import React, { useCallback } from 'react';
22import cn from 'classnames';
3−import { Grid, Text } from 'indigo-react';
3+import { Grid, Text, Button } from 'indigo-react';
44import * as azimuth from 'azimuth-js';
5+import { Nothing } from 'folktale/maybe';
6+import { Row } from '@tlon/indigo-react';
57
68import { useNetwork } from 'store/network';
79import { usePointCursor } from 'store/pointCursor';
810import { usePointCache } from 'store/pointCache';
11+import { useRollerStore } from 'store/rollerStore';
912
1013import * as need from 'lib/need';
1114import { useLocalRouter } from 'lib/LocalRouter';
@@ -13,10 +16,14 @@ import useCurrentPointName from 'lib/useCurrentPointName';
1316import useEthereumTransaction from 'lib/useEthereumTransaction';
1417import { ETH_ZERO_ADDR, GAS_LIMITS } from 'lib/constants';
1518import useLifecycle from 'lib/useLifecycle';
19+import useRoller from 'lib/useRoller';
1620
17−import ViewHeader from 'components/ViewHeader';
1821import 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';
2027
2128function useCancelTransfer() {
2229 const { contracts } = useNetwork();
@@ -48,6 +55,8 @@ function useCancelTransfer() {
4855function AdminCancelTransfer() {
4956 const { pop } = useLocalRouter();
5057 const { getDetails } = usePointCache();
58+ const { point } = useRollerStore();
59+ const { setProxyAddress } = useRoller();
5160 const { pointCursor } = usePointCursor();
5261 const _point = need.point(pointCursor);
5362
@@ -56,38 +65,68 @@ function AdminCancelTransfer() {
5665
5766 const { completed, bind } = useCancelTransfer();
5867
68+ const cancelTransfer = useCallback(async () => {
69+ await setProxyAddress('transfer', ETH_ZERO_ADDR);
70+ pop();
71+ }, [setProxyAddress, pop]);
72+
5973 return (
60− <Grid>
61− <Grid.Item full as={ViewHeader}>
62− Cancel Outgoing Transfer
63− </Grid.Item>
64−
65− <Grid.Item
66− full
67− as={Text}
68− className={cn('f5 wrap', {
69− green3: completed,
70− })}>
71− {completed
72− ? `The outgoing transfer of ${name} has been cancelled.`
73− : `Cancel the outgoing transfer of ${name} to ${_details.transferProxy}.`}
74− </Grid.Item>
75−
76− <Grid.Item
77− full
78− as={InlineEthereumTransaction}
79− {...bind}
80− onReturn={() => pop()}
81− />
82− </Grid>
74+ <Window>
75+ <HeaderPane>
76+ <Row className="header-row">
77+ <h5>Cancel Outgoing Transfer</h5>
78+ </Row>
79+ </HeaderPane>
80+ <BodyPane>
81+ <Grid.Item
82+ full
83+ as={Text}
84+ className={cn('f5 wrap', {
85+ green3: completed,
86+ })}>
87+ {completed
88+ ? `The outgoing transfer of ${name} has been cancelled.`
89+ : `Cancel the outgoing transfer of ${name} to ${_details.transferProxy}.`}
90+ </Grid.Item>
91+
92+ {point.isL2 ? (
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
104+ full
105+ as={InlineEthereumTransaction}
106+ {...bind}
107+ onReturn={() => pop()}
108+ />
109+ )}
110+ </BodyPane>
111+ </Window>
83112 );
84113}
85114
86115export default function CancelTransfer() {
87116 const { pop } = useLocalRouter();
117+ const { setPointCursor } = usePointCursor();
118+
119+ const goBack = useCallback(() => {
120+ setPointCursor(Nothing());
121+ pop();
122+ }, [pop, setPointCursor]);
88123
89124 return (
90− <View pop={pop} inset>
125+ <View
126+ pop={pop}
127+ className="cancel-transfer"
128+ hideBack
129+ header={<L2BackHeader hideBalance back={goBack} />}>
91130 <AdminCancelTransfer />
92131 </View>
93132 );
src/views/Invite/Cohort.tsx+9−10
@@ -46,6 +46,7 @@ import {
4646import { isPlanet } from 'lib/utils/point';
4747import { generateInviteWallet } from 'lib/utils/roller';
4848import { useTimerStore } from 'store/timerStore';
49+import { Invite } from 'lib/types/Invite';
4950
5051interface L1Invite {
5152 ticket: string;
@@ -83,13 +84,12 @@ export default function InviteCohort() {
8384 );
8485 const [showInviteForm, setShowInviteForm] = useState(false);
8586 const [loading, setLoading] = useState(false);
86− const [downloadingCsv, setDownloadingCsv] = useState(false);
8787 const [error, setError] = useState('');
8888 const [l1Invite, setL1Invite] = useState<L1Invite | null>(null);
8989 const [page, setPage] = useState(0);
9090
9191 const { construct, unconstruct, completed, bind } = useIssueChild();
92− const invitePoints = invites[point.value] || [];
92+ const invitePoints: Invite[] = invites[point.value] || [];
9393
9494 // Set up the invite spawn if on L1
9595 useEffect(() => {
@@ -100,7 +100,7 @@ export default function InviteCohort() {
100100 }
101101
102102 const setUpInvite = async () => {
103− setDownloadingCsv(true);
103+ setLoading(true);
104104 const possiblePoints = await azimuth.azimuth.getUnspawnedChildren(
105105 _contracts,
106106 point
@@ -120,6 +120,7 @@ export default function InviteCohort() {
120120 }
121121
122122 if (invitePoint) {
123+ setInviteGeneratingNum(1);
123124 const { ticket, inviteWallet } = await generateInviteWallet(
124125 invitePoint,
125126 _authToken
@@ -131,7 +132,7 @@ export default function InviteCohort() {
131132 setError('No available planets');
132133 }
133134
134− setDownloadingCsv(false);
135+ setLoading(false);
135136 };
136137
137138 if (!currentL2 && _contracts && point && _authToken && showInviteForm) {
@@ -148,6 +149,7 @@ export default function InviteCohort() {
148149 showInviteForm,
149150 unconstruct,
150151 invitePoints,
152+ setInviteGeneratingNum,
151153 ]);
152154
153155 useEffect(() => {
@@ -256,7 +258,7 @@ export default function InviteCohort() {
256258 Once a code has been claimed, the code will automatically disappear.
257259 </div>
258260 <div className="invites">
259− {invitesToDisplay.map(invite => (
261+ {invitesToDisplay.map((invite: Invite) => (
260262 <div className="invite" key={invite.planet}>
261263 <div
262264 className={`invite-url ${invite.ticket ? '' : 'shortened'}`}>
@@ -393,7 +395,7 @@ export default function InviteCohort() {
393395
394396 const showGenerateButton = !hasPending && !hasInvites && point.canSpawn;
395397 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}...`;
397399
398400 return (
399401 <View
@@ -447,10 +449,7 @@ export default function InviteCohort() {
447449 Add More
448450 </Button>
449451 )}
450− <LoadingOverlay
451− loading={downloadingCsv || loading}
452− text={downloadingCsv ? downloadingCsvText : undefined}
453− />
452+ <LoadingOverlay loading={loading} text={generatingCodesText} />
454453 </View>
455454 );
456455}
src/views/Login/LoginSelector.scss+20−1
@@ -1,9 +1,14 @@
11@import 'style/shared/variables.scss';
22
33.login-selector {
4− width: 100%;
4+ width: 482px;
55 min-height: 300px;
66
7+ .icon {
8+ height: 14px;
9+ width: 14px;
10+ }
11+
712 .login-menu-item {
813 padding: 16px 16px;
914 font-size: 14px;
@@ -69,6 +74,20 @@
6974 padding: 24px;
7075 font-size: 14px;
7176 line-height: 24px;
77+ }
7278
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+ }
7392 }
7493}
src/views/Login/LoginSelector.tsx+12−24
@@ -1,7 +1,7 @@
11import React, { useCallback, useState } from 'react';
22import cn from 'classnames';
33import { 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';
55import { useHistory } from 'store/history';
66import { Just, Nothing } from 'folktale/maybe';
77import { FORM_ERROR } from 'final-form';
@@ -17,9 +17,6 @@ import * as need from 'lib/need';
1717import { useWalletConnect } from 'lib/useWalletConnect';
1818import useLoginView from 'lib/useLoginView';
1919
20−import Window from 'components/L2/Window/Window';
21−import HeaderPane from 'components/L2/Window/HeaderPane';
22−import BodyPane from 'components/L2/Window/BodyPane';
2320import Modal from 'components/L2/Modal';
2421import HeaderButton from 'components/L2/Headers/HeaderButton';
2522import './LoginSelector.scss';
@@ -53,6 +50,7 @@ export default function LoginSelector({
5350
5451 const { push, names }: any = useHistory();
5552 const [showModal, setShowModal] = useState(false);
53+ const [metamaskSelected, setMetamaskSelected] = useState(false);
5654
5755 const {
5856 setWallet,
@@ -74,6 +72,7 @@ export default function LoginSelector({
7472
7573 const connectMetamask = useCallback(async () => {
7674 try {
75+ setMetamaskSelected(true);
7776 resetWallet();
7877 setWalletType(WALLET_TYPES.METAMASK);
7978 setPointCursor(Nothing());
@@ -94,6 +93,7 @@ export default function LoginSelector({
9493 setWalletType(WALLET_TYPES.METAMASK);
9594 goHome();
9695 } catch (e) {
96+ setMetamaskSelected(false);
9797 console.error(e);
9898 return { [FORM_ERROR]: (e as any).message };
9999 }
@@ -104,6 +104,7 @@ export default function LoginSelector({
104104 setWalletType,
105105 resetWallet,
106106 setPointCursor,
107+ setMetamaskSelected,
107108 goHome,
108109 ]);
109110
@@ -118,6 +119,8 @@ export default function LoginSelector({
118119 }
119120 };
120121
122+ const loader = <LoadingSpinner background="#bce2ff" foreground="#219DFF" />;
123+
121124 if (!currentTab) {
122125 return (
123126 <Grid className={className}>
@@ -128,7 +131,9 @@ export default function LoginSelector({
128131 className={cn('f5 pv3 rel pointer login-menu-item')}
129132 onClick={selectOption(option.value)}>
130133 {option.text}
131− {option.menuIcon}
134+ {option.value === NAMES.METAMASK && metamaskSelected
135+ ? loader
136+ : option.menuIcon}
132137 </Grid.Item>
133138 </React.Fragment>
134139 ))}
@@ -170,32 +175,15 @@ export default function LoginSelector({
170175 }
171176
172177 const Tab = views[currentTab];
173− const currentOption = options.find(({ value }) => value === currentTab);
174178
175179 return (
176− <Box display="flex" flexDirection="column">
180+ <Box className="flex-col login-selector">
177181 <HeaderButton
178182 className="mb4"
179183 icon="ChevronWest"
180184 onClick={() => setCurrentTab(undefined)}
181185 />
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}
195− className="login-selector"
196− />
197− </BodyPane>
198− </Window>
186+ <Grid.Item full as={Tab} {...rest} goHome={goHome} />
199187 </Box>
200188 );
201189}
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';
22import BridgeForm from 'form/BridgeForm';
33import FormError from 'form/FormError';
44import SubmitButton from 'form/SubmitButton';
@@ -8,8 +8,16 @@ import useLoginView from 'lib/useLoginView';
88import { useWalletConnect } from 'lib/useWalletConnect';
99import { abbreviateAddress } from 'lib/utils/address';
1010import { 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';
1114
12−const WalletConnectLogin = ({ className, goHome }) => {
15+interface WalletConnectLoginProps {
16+ className?: string;
17+ goHome: () => void;
18+}
19+
20+const WalletConnectLogin = ({ className, goHome }: WalletConnectLoginProps) => {
1321 useLoginView(WALLET_TYPES.WALLET_CONNECT);
1422
1523 const {
@@ -30,68 +38,83 @@ const WalletConnectLogin = ({ className, goHome }) => {
3038
3139 return (
3240 connector && (
33− <Grid className={className}>
34− {!isConnected() && (
35− <Grid.Item full as={LegacyText} className="f6 gray4 mb3">
36− Note that WalletConnect is a young protocol, not all wallets may
37− 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− )}
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">
48+ <Grid className={className}>
49+ {!isConnected() && (
50+ <Grid.Item full as={LegacyText} className="f6 gray4 mb3">
51+ Note that WalletConnect is a young protocol, not all wallets may
52+ work fully
5053 </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− )}
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} />
54+ )}
7255
73− <Grid.Item full as={SubmitButton} handleSubmit={handleSubmit}>
74− {submitting
75− ? 'Please check your WalletConnect wallet'
76− : 'Authenticate'}
56+ {isConnected() && peerMeta && (
57+ <Grid.Item full className="f6 gray5 mt3 mb3">
58+ <Grid>
59+ <Grid.Item style={{ gridArea: 'auto / 3 / auto / 5' }}>
60+ {peerIcon ? (
61+ <Image src={peerIcon} />
62+ ) : (
63+ <LegacyText>Icon Unavailable</LegacyText>
64+ )}
7765 </Grid.Item>
78− </>
79− ) : (
80− <>
8166 <Grid.Item
82− full
83− center
84− as={ForwardButton}
85− solid
86− onClick={connect}>
87− {'Connect'}
67+ className={'flex-col align-center justify-center'}
68+ style={{ gridArea: 'auto / 6 / auto / 11' }}>
69+ <LegacyText as="div">{peerMeta.name}</LegacyText>
70+ <LegacyText as="div">
71+ {address ? abbreviateAddress(address) : null}
72+ </LegacyText>
73+ <RestartButton
74+ as="a"
75+ className={'gray3'}
76+ onClick={disconnect}>
77+ disconnect
78+ </RestartButton>
8879 </Grid.Item>
89− </>
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>
110+ </>
111+ )}
112+ </Grid.Item>
90113 )}
91− </Grid.Item>
92− )}
93− </BridgeForm>
94− </Grid>
114+ </BridgeForm>
115+ </Grid>
116+ </BodyPane>
117+ </Window>
95118 )
96119 );
97120};
src/views/Point/IssueChild.tsx+1−1
@@ -202,7 +202,7 @@ export default function IssueChild() {
202202 <Box className={'patp-badge-container'}>
203203 <Box className="badges">
204204 {candidates.map(p => (
205− <PatpBadge className={'patp-badge'}>
205+ <PatpBadge className={'patp-badge'} key={p}>
206206 {p}
207207 </PatpBadge>
208208 ))}
src/views/Point/Point.tsx−2
@@ -44,8 +44,6 @@ export default function Point() {
4444
4545 const pointRef = useRef<number | null>(null);
4646
47− const invitePoints = invites[point.value] || [];
48−
4947 const hideModal = useCallback(() => {
5048 if (hideMessage) {
5149 setSeenMissingKeys(true);
src/views/Points.js+4−1
@@ -27,6 +27,7 @@ import NavHeader from 'components/NavHeader';
2727import L2PointHeader from 'components/L2/Headers/L2PointHeader';
2828import IncomingPoint from 'components/L2/Points/IncomingPoint';
2929import LoadingOverlay from 'components/L2/LoadingOverlay';
30+import { onlyUnique } from 'lib/utils/array';
3031
3132export const maybeGetResult = (obj, key, defaultValue) =>
3233 obj.matchWith({
@@ -242,7 +243,9 @@ export default function Points() {
242243 ...managingPoints,
243244 ...votingPoints,
244245 ...spawningPoints,
245− ].sort((a, b) => Number(a) - Number(b));
246+ ]
247+ .sort((a, b) => Number(a) - Number(b))
248+ .filter(onlyUnique);
246249
247250 const displayEmptyState =
248251 !loading && incomingPoints.length === 0 && allPoints.length === 0;
src/views/Release/Active.js+1−1
@@ -77,7 +77,7 @@ export default function ActiveView({ className, goPoint }) {
7777 .filter(p => 255 < p && p < 65536);
7878
7979 return (
80− <Grid full className={cn('f5', className)}>
80+ <Grid className={cn('f5', className)}>
8181 <Grid.Item className="pv3 " cols={[1, 6]} third={1}>
8282 Point
8383 </Grid.Item>
src/views/Transactions/TransactionHistory.tsx+4−4
@@ -39,7 +39,7 @@ const TransactionHistory = () => {
3939 const filterByPoint = useMemo(() => {
4040 return data?.filterByPoint.matchWith({
4141 Nothing: () => null,
42− Just: (p: number) => ob.patp(p.value).replace('~', ''),
42+ Just: (p: any) => ob.patp(p.value),
4343 });
4444 }, [data]);
4545
@@ -112,7 +112,7 @@ const TransactionHistory = () => {
112112 toggleOpen={() => setDropdownOpen(!dropdownOpen)}>
113113 <Box className="divider" />
114114 <Box className="points">
115− <Row className="entry" onClick={() => selectPatp(null)}>
115+ <Row className="entry mono" onClick={() => selectPatp(null)}>
116116 <Box>{shortAddress}</Box>
117117 <Row>
118118 <Box
@@ -125,10 +125,10 @@ const TransactionHistory = () => {
125125 {txPatps.map(patp => {
126126 return (
127127 <Row
128− className="entry"
128+ className="entry mono"
129129 onClick={() => selectPatp(patp)}
130130 key={patp}>
131− <Box>{patp}</Box>
131+ <Box>~{patp}</Box>
132132 <Row>
133133 <Box className="sigil">
134134 <Sigil
src/views/UrbitID/Home.tsx+35−24
@@ -7,7 +7,7 @@ import { PROXY_TYPE } from 'lib/proxy';
77import * as need from 'lib/need';
88import useCurrentPermissions from 'lib/useCurrentPermissions';
99import { convertToInt } from 'lib/convertToInt';
10−import { ETH_ZERO_ADDR } from 'lib/constants';
10+import { ETH_ZERO_ADDR, MASTER_TICKET_TOOLTIP } from 'lib/constants';
1111import { abbreviateAddress } from 'lib/utils/address';
1212import { downloadWallet } from 'lib/invite';
1313import useKeyfileGenerator from 'lib/useKeyfileGenerator';
@@ -23,6 +23,8 @@ import CopiableWithTooltip from 'components/copiable/CopiableWithTooltip';
2323import PaperBuilder from 'components/PaperBuilder';
2424
2525import './UrbitID.scss';
26+import LayerIndicator from 'components/L2/LayerIndicator';
27+import WithTooltip from 'components/WithTooltip';
2628
2729export default function UrbitIDHome() {
2830 const { push, names }: any = useLocalRouter();
@@ -59,7 +61,6 @@ export default function UrbitIDHome() {
5961
6062 const noManagement = details.managementProxy === ETH_ZERO_ADDR;
6163 const noSpawn = details.spawnProxy === ETH_ZERO_ADDR;
62− const showSpawn = point.isL1 && !point.isL2Spawn;
6364
6465 return (
6566 <Window className="id-home">
@@ -125,40 +126,50 @@ export default function UrbitIDHome() {
125126 </Row>
126127 )}
127128
128− {point.isParent && networkRevision !== 0 && showSpawn && (
129+ {point.isParent && networkRevision !== 0 && (
129130 <Row className="between-row management">
130131 <Box>
131132 <Box>Spawn Proxy Address</Box>
132133 <div className="mt1 mono black subtitle">
133− {noSpawn ? 'Unset' : abbreviateAddress(details.spawnProxy)}
134+ {noSpawn
135+ ? point.isL2Spawn
136+ ? 'Layer 2'
137+ : 'Unset'
138+ : abbreviateAddress(details.spawnProxy)}
134139 </div>
135140 </Box>
136− <Row>
137− <Button
138− className="secondary"
139− onClick={() => goSetProxy(PROXY_TYPE.SPAWN)}>
140− {noSpawn ? 'Set' : 'Change'}
141− </Button>
142− {!noSpawn && (
143− <CopiableWithTooltip
144− text={details.spawnProxy}
145− className="copy-button"
146− />
147− )}
148− </Row>
141+ {point.isL2Spawn ? (
142+ <LayerIndicator size="lg" layer={2} />
143+ ) : (
144+ <Row>
145+ <Button
146+ className="secondary"
147+ onClick={() => goSetProxy(PROXY_TYPE.SPAWN)}>
148+ {noSpawn ? 'Set' : 'Change'}
149+ </Button>
150+ {!noSpawn && (
151+ <CopiableWithTooltip
152+ text={details.spawnProxy}
153+ className="copy-button"
154+ />
155+ )}
156+ </Row>
157+ )}
149158 </Row>
150159 )}
151160
152161 {isOwner && (
153162 <Row className="between-row management">
154− <Box>
155− <Box>Master Ticket</Box>
156− <Box className="subtitle">
157− {isMasterTicket
158− ? 'Reset Master Ticket and all other keys'
159− : 'Transfer to Master Ticket, resetting all keys'}
163+ <WithTooltip content={MASTER_TICKET_TOOLTIP}>
164+ <Box>
165+ <Box>Master Ticket</Box>
166+ <Box className="subtitle">
167+ {isMasterTicket
168+ ? 'Reset Master Ticket and all other keys'
169+ : 'Transfer to Master Ticket, resetting all keys'}
170+ </Box>
160171 </Box>
161− </Box>
172+ </WithTooltip>
162173 <Button className="secondary" onClick={goResetKeys}>
163174 {isMasterTicket ? 'Reset' : 'Transfer'}
164175 </Button>