on master help sign in
Merge pull request #204 from urbit/feat/multi-home

implement multi-home

commit729283fae1e921c52c794592bb65de169079fa68 authorMatt Condon <mattgcondon@gmail.com> dateTue 23 Jul 2019, 13:06 (7 years ago) parent56fd47259995bf
17 files changed+551−240
src/Bridge.js+22−12 src/components/InlineEthereumTransaction.js+1−1 src/components/Passport.js+95−12 src/lib/disclaimerCookie.js−9 src/lib/routeNames.js−6 src/lib/router.js+6−1 src/lib/useEthereumTransaction.js+9−4 src/lib/useHasDisclaimed.js+7 src/lib/useLocalStorageState.js+38 src/lib/usePermissionsForPoint.js+6−2 src/lib/useRejectedIncomingPointTransfers.js+19 src/lib/wallet.js+3 src/store/lib/useControlledPointsStore.js+40−39 src/views/AcceptTransfer.js+96 src/views/CancelTransfer.js+21 src/views/Disclaimer.js+4−3 src/views/Points.js+184−151
src/Bridge.js+22−12
@@ -12,8 +12,8 @@ import { ROUTES } from 'lib/router';
1212import { NETWORK_TYPES } from 'lib/network';
1313import { walletFromMnemonic } from 'lib/wallet';
1414import { isDevelopment } from 'lib/flags';
15−import { hasDisclaimed } from 'lib/disclaimerCookie';
1615import useImpliedTicket from 'lib/useImpliedTicket';
16+import useHasDisclaimed from 'lib/useHasDisclaimed';
1717
1818import 'style/index.scss';
1919
@@ -36,19 +36,29 @@ const INITIAL_MNEMONIC = IS_STUBBED
3636 : Nothing();
3737const INITIAL_POINT_CURSOR = IS_STUBBED ? Just(65792) : Nothing();
3838
39−function Bridge() {
40− const landingPage = !!useImpliedTicket()
39+function useInitialRoutes() {
40+ const [hasDisclaimed] = useHasDisclaimed();
41+ const hasImpliedTicket = !!useImpliedTicket();
42+
43+ if (IS_STUBBED) {
44+ return [
45+ { key: ROUTE_NAMES.LOGIN },
46+ { key: ROUTE_NAMES.POINTS },
47+ { key: ROUTE_NAMES.POINT },
48+ ];
49+ }
50+
51+ const landingPage = hasImpliedTicket
4152 ? ROUTE_NAMES.ACTIVATE
4253 : ROUTE_NAMES.LOGIN;
43− const initialRoutes = IS_STUBBED
44− ? [
45− { key: ROUTE_NAMES.LOGIN },
46− { key: ROUTE_NAMES.POINTS },
47− { key: ROUTE_NAMES.POINT },
48− ]
49− : hasDisclaimed()
50− ? [{ key: landingPage }]
51− : [{ key: landingPage }, { key: ROUTE_NAMES.DISCLAIMER }];
54+
55+ const extraRoutes = !hasDisclaimed ? [{ key: ROUTE_NAMES.DISCLAIMER }] : [];
56+
57+ return [{ key: landingPage }, ...extraRoutes];
58+}
59+
60+function Bridge() {
61+ const initialRoutes = useInitialRoutes();
5262
5363 return (
5464 <Provider
src/components/InlineEthereumTransaction.js+1−1
@@ -51,7 +51,7 @@ export default function InlineEthereumTransaction({
5151 const showConfigureInput = !(signed || broadcasted || confirmed || completed);
5252 // show the send/loading button while signed, broadcasting, or confirme
5353 const showBroadcastButton = signed || broadcasted || confirmed;
54− const canBroadcast = signed;
54+ const canBroadcast = signed && !needFunds;
5555 const isLoading = broadcasted || confirmed;
5656 // show signed tx only when signing (for offline usage)
5757 const showSignedTx = signed;
src/components/Passport.js+95−12
@@ -1,18 +1,23 @@
1−import React from 'react';
1+import React, { useMemo } from 'react';
22import cn from 'classnames';
33import { Just, Nothing } from 'folktale/maybe';
44import * as ob from 'urbit-ob';
55import { Grid, Flex, Text } from 'indigo-react';
66import { times } from 'lodash';
77
8+import * as need from 'lib/need';
89import usePointBirthday from 'lib/usePointBirthday';
910
10−import {
11+import Blinky, {
1112 LOADING_CHARACTER,
1213 INTERSTITIAL_CHARACTER,
1314 matchBlinkyDate,
1415} from './Blinky';
16+import Sigil from './Sigil';
1517import MaybeSigil from './MaybeSigil';
18+import usePermissionsForPoint from 'lib/usePermissionsForPoint';
19+import { useWallet } from 'store/wallet';
20+import { useSyncOwnedPoints } from 'lib/useSyncPoints';
1621
1722const TICKET_CHAR_LENGTH = 30;
1823const TICKET = times(TICKET_CHAR_LENGTH, i =>
@@ -25,18 +30,28 @@ const VISIBLE_ADDRESS_CHAR_COUNT = 20;
2530const EMPTY_ADDRESS = times(VISIBLE_ADDRESS_CHAR_COUNT, () => '<').join('');
2631const ADDRESS_BUFFER_CHARS = times(40, () => '<').join('');
2732
33+const INVERTED_COLORWAY = ['#000', '#fff', '#000'];
34+
35+const buildKeyType = permissions => {
36+ if (permissions.isOwner) {
37+ return 'Ownership';
38+ } else if (permissions.isManagementProxy) {
39+ return 'Management';
40+ } else if (permissions.isSpawnProxy) {
41+ return 'Spawn';
42+ } else if (permissions.isVotingProxy) {
43+ return 'Voting';
44+ } else if (permissions.isTransferProxy) {
45+ return 'Transfer';
46+ }
47+};
48+
2849/**
2950 * point is Maybe<number>
3051 * ticket is null | boolean
3152 * address is null | Maybe<string>
3253 */
33−export default function Passport({
34− point,
35− className,
36− ticket = null,
37− address = Nothing(),
38− mini = false,
39−}) {
54+function Passport({ point, className, ticket = null, address = Nothing() }) {
4055 const birthday = usePointBirthday(point.getOrElse(null));
4156 const visualBirthday = matchBlinkyDate(birthday);
4257
@@ -59,7 +74,7 @@ export default function Passport({
5974
6075 return (
6176 <Grid gap={4} className={cn('bg-black r8 p7', className)}>
62− <Grid.Item cols={[1, 4]} className="bg-green">
77+ <Grid.Item cols={[1, 4]}>
6378 <MaybeSigil patp={name} size={64} margin={0} />
6479 </Grid.Item>
6580 <Grid.Item as={Flex} cols={[4, 13]} col justify="between">
@@ -68,12 +83,12 @@ export default function Passport({
6883 <Flex.Item as={Text} className="mono f6 f5-ns gray4 uppercase">
6984 Birth Time
7085 </Flex.Item>
71− <Flex.Item as={Text} className="mono f6 f5-ns gray4 ">
86+ <Flex.Item as={Text} className="mono f6 f5-ns gray4 uppercase">
7287 {visualBirthday}
7388 </Flex.Item>
7489 </Flex.Item>
7590 </Grid.Item>
76− {!mini && showTicket && (
91+ {showTicket && (
7792 <Grid.Item full className="mv3" as={Flex} col>
7893 <Flex.Item as={Text} className="mono f6 f5-ns gray4 uppercase">
7994 Master Ticket
@@ -94,3 +109,71 @@ export default function Passport({
94109 </Grid>
95110 );
96111}
112+
113+// TODO: Passport.Mini should probably accept Maybe<> for point
114+// in order to display a pretty skeleton loading view
115+// but that requires making usePermissionsForPoint accept a Maybe<point>
116+// and I don't feel like that refactor is worth it right now, and the spec
117+// doesn't call for it, hence the TODO
118+Passport.Mini = function MiniPassport({ point, className, inverted, ...rest }) {
119+ useSyncOwnedPoints([point]);
120+
121+ const { wallet } = useWallet();
122+ const address = need.addressFromWallet(wallet);
123+
124+ const permissions = usePermissionsForPoint(address, point);
125+ const keyType = buildKeyType(permissions);
126+ const name = useMemo(() => ob.patp(point), [point]);
127+
128+ return (
129+ <Grid
130+ gap={4}
131+ className={cn(
132+ 'r8 p4',
133+ {
134+ 'bg-black': !inverted,
135+ 'bg-white b-gray4 b1': inverted,
136+ },
137+ className
138+ )}
139+ {...rest}>
140+ <Grid.Item cols={[1, 4]}>
141+ <Sigil
142+ patp={name}
143+ size={64}
144+ margin={0}
145+ colorway={inverted ? INVERTED_COLORWAY : undefined}
146+ />
147+ </Grid.Item>
148+ <Grid.Item as={Flex} cols={[4, 13]} col justify="between">
149+ <Flex.Item
150+ className={cn('mono f6 pb2', {
151+ white: !inverted,
152+ black: inverted,
153+ })}>
154+ {name}
155+ </Flex.Item>
156+ <Flex.Item as={Flex} col className={cn('mono f6 uppercase')}>
157+ <Flex.Item
158+ as={Text}
159+ className={cn({
160+ gray4: !inverted,
161+ black: inverted,
162+ })}>
163+ Key Type
164+ </Flex.Item>
165+ <Flex.Item
166+ as={Text}
167+ className={cn({
168+ white: !inverted,
169+ black: inverted,
170+ })}>
171+ {keyType || <Blinky />}
172+ </Flex.Item>
173+ </Flex.Item>
174+ </Grid.Item>
175+ </Grid>
176+ );
177+};
178+
179+export default Passport;
src/lib/disclaimerCookie.jsdeleted−9
@@ -1,9 +0,0 @@
1−const COOKIE_ID = 'bridge_disclaimed';
2−
3−export const hasDisclaimed = () => {
4− return document.cookie.includes(COOKIE_ID);
5−};
6−
7−export const setDisclaimerCookie = () => {
8− document.cookie = COOKIE_ID;
9−};
src/lib/routeNames.js−6
@@ -19,13 +19,7 @@ export const ROUTE_NAMES = {
1919 VIEW_POINT: Symbol('VIEW_POINT'),
2020 POINTS: Symbol('POINTS'),
2121 POINT: Symbol('POINT'),
22− SET_MANAGEMENT_PROXY: Symbol('SET_MANAGEMENT_PROXY'),
23− SET_SPAWN_PROXY: Symbol('SET_SPAWN_PROXY'),
24− SET_TRANSFER_PROXY: Symbol('SET_TRANSFER_PROXY'),
2522 CREATE_GALAXY: Symbol('CREATE_GALAXY'),
2623 ISSUE_CHILD: Symbol('ISSUE_CHILD'),
27− SET_KEYS: Symbol('SET_KEYS'),
28− TRANSFER: Symbol('TRANSFER'),
29− GEN_KEYFILE: Symbol('GEN_KEYFILE'),
3024 PARTY_SET_POOL_SIZE: Symbol('PARTY_SET_POOL_SIZE'),
3125};
src/lib/router.js+6−1
@@ -11,6 +11,8 @@ import Invite from 'views/Invite';
1111import Activate from 'views/Activate';
1212
1313import PartySetPoolSize from 'views/Party/PartySetPoolSize';
14+import AcceptTransfer from 'views/AcceptTransfer';
15+import CancelTransfer from 'views/CancelTransfer';
1416
1517import { ROUTE_NAMES } from './routeNames';
1618
@@ -23,8 +25,11 @@ export const ROUTES = {
2325 [ROUTE_NAMES.POINT]: Point,
2426 [ROUTE_NAMES.CREATE_GALAXY]: CreateGalaxy,
2527 [ROUTE_NAMES.ISSUE_CHILD]: IssueChild,
26− [ROUTE_NAMES.PARTY_SET_POOL_SIZE]: PartySetPoolSize,
2728 [ROUTE_NAMES.INVITE]: Invite,
2829 [ROUTE_NAMES.ADMIN]: Admin,
2930 // TODO: nest this route under the Party.js router
31+ [ROUTE_NAMES.PARTY_SET_POOL_SIZE]: PartySetPoolSize,
32+ [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
33+ // TODO: replace this with deep link to AdminCancelTransfer
34+ [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
3035};
src/lib/useEthereumTransaction.js+9−4
@@ -14,7 +14,6 @@ import {
1414 hexify,
1515} from './txn';
1616import * as need from './need';
17−import useLifecycle from './useLifecycle';
1817import { ensureFundsFor } from './tank';
1918import useDeepEqualReference from './useDeepEqualReference';
2019
@@ -174,9 +173,10 @@ export default function useEthereumTransaction(
174173 ]);
175174
176175 const reset = useCallback(() => {
177− setUnsignedTransaction(undefined);
178176 setTxHash(undefined);
179177 setSignedTransaction(undefined);
178+ setNonce(undefined);
179+ setChainId(undefined);
180180 setGasPrice(suggestedGasPrice);
181181 setState(STATE.NONE);
182182 setError(undefined);
@@ -188,7 +188,12 @@ export default function useEthereumTransaction(
188188 suggestedGasPrice,
189189 ]);
190190
191− useLifecycle(() => {
191+ useEffect(() => {
192+ // if nonce or chainId is undefined, re-fetch on-chain info
193+ if (!(nonce === undefined || chainId === undefined)) {
194+ return;
195+ }
196+
192197 (async () => {
193198 try {
194199 setError(undefined);
@@ -207,7 +212,7 @@ export default function useEthereumTransaction(
207212 setError(error);
208213 }
209214 })();
210− });
215+ }, [_wallet, _web3, setError, nonce, chainId]);
211216
212217 useEffect(() => {
213218 let cancelled = false;
src/lib/useHasDisclaimed.jsnew file+7
@@ -0,0 +1,7 @@
1+import useLocalStorageState from './useLocalStorageState';
2+
3+const HAS_DISCLAIMED_KEY = 'bridge:has-disclaimed';
4+
5+export default function useHasDisclaimed() {
6+ return useLocalStorageState(HAS_DISCLAIMED_KEY, false);
7+}
src/lib/useLocalStorageState.jsnew file+38
@@ -0,0 +1,38 @@
1+import { useState, useEffect } from 'react';
2+
3+/**
4+ * useLocalStorageState is useState but it persists to localStorage
5+ *
6+ * inspired by:
7+ * https://github.com/streamich/react-use/blob/master/src/useLocalStorage.ts
8+ * https://usehooks.com/useLocalStorage/
9+ */
10+export default function useLocalStorageState(key, initialValue) {
11+ const [state, setState] = useState(() => {
12+ try {
13+ const value = localStorage.getItem(key);
14+ if (value === null) {
15+ localStorage.setItem(key, JSON.stringify(initialValue));
16+ return initialValue;
17+ } else {
18+ return JSON.parse(value);
19+ }
20+ } catch {
21+ // if user is in private mode or has storage restriction
22+ // localStorage can throw. JSON.parse and JSON.stringify
23+ // can throw, too.
24+ return initialValue;
25+ }
26+ });
27+
28+ useEffect(() => {
29+ try {
30+ localStorage.setItem(key, JSON.stringify(state));
31+ } catch {
32+ // if user is in private mode or has storage restriction
33+ // localStorage can throw. Also JSON.stringify can throw.
34+ }
35+ }, [key, state]);
36+
37+ return [state, setState];
38+}
src/lib/usePermissionsForPoint.js+6−2
@@ -14,8 +14,10 @@ const NULL_PERMISSIONS = {
1414 //
1515 isOwner: false,
1616 isActiveOwner: false,
17− isTransferProxy: false,
1817 isManagementProxy: false,
18+ isSpawnProxy: false,
19+ isVotingProxy: false,
20+ isTransferProxy: false,
1921 //
2022 isManagementProxySet: false,
2123 isSpawnProxySet: false,
@@ -84,8 +86,10 @@ export default function usePermissionsForPoint(address, point) {
8486 //
8587 isOwner,
8688 isActiveOwner,
87− isTransferProxy,
8889 isManagementProxy,
90+ isSpawnProxy,
91+ isVotingProxy,
92+ isTransferProxy,
8993 //
9094 isManagementProxySet,
9195 isSpawnProxySet,
src/lib/useRejectedIncomingPointTransfers.jsnew file+19
@@ -0,0 +1,19 @@
1+import { useCallback } from 'react';
2+
3+import useLocalStorageState from './useLocalStorageState';
4+
5+const REJECTED_POINTS_KEY = 'bridge:rejected-points';
6+
7+export default function useRejectedIncomingPointTransfers() {
8+ const [rejectedPoints, setRejectedPoints] = useLocalStorageState(
9+ REJECTED_POINTS_KEY,
10+ []
11+ );
12+
13+ const addRejectedPoint = useCallback(
14+ point => setRejectedPoints([...rejectedPoints, point]),
15+ [rejectedPoints, setRejectedPoints]
16+ );
17+
18+ return [rejectedPoints, addRejectedPoint];
19+}
src/lib/wallet.js+3
@@ -102,3 +102,6 @@ export const walletFromMnemonic = (mnemonic, hdpath, passphrase) => {
102102 const wallet = seed.chain(sd => toWallet(sd, hdpath));
103103 return wallet;
104104};
105+
106+export const abbreviateAddress = address =>
107+ `${address.slice(0, 4)}…${address.slice(-4)}`;
src/store/lib/useControlledPointsStore.js+40−39
@@ -1,29 +1,15 @@
1−import { useState, useCallback } from 'react';
1+import { useState, useEffect, useCallback } from 'react';
22import { Just, Nothing } from 'folktale/maybe';
3+import Result from 'folktale/result';
34import * as azimuth from 'azimuth-js';
45
56import { useNetwork } from '../network';
67import { useWallet } from 'store/wallet';
78
8−const EMPTY_CONTROLLED_POINTS = {
9− ownedPoints: Nothing(),
10− incomingPoints: Nothing(),
11− managingPoints: Nothing(),
12− votingPoints: Nothing(),
13− spawningPoints: Nothing(),
14−};
15−
169export default function useControlledPointsStore() {
1710 const { contracts } = useNetwork();
1811 const { wallet } = useWallet();
19− const [controlledPointsCache, _setControlledPointsCache] = useState(
20− EMPTY_CONTROLLED_POINTS
21− );
22−
23− const getControlledPoints = useCallback(
24− () => controlledPointsCache || EMPTY_CONTROLLED_POINTS,
25− [controlledPointsCache]
26− );
12+ const [controlledPoints, _setControlledPoints] = useState(Nothing());
2713
2814 const syncControlledPoints = useCallback(async () => {
2915 const _contracts = contracts.getOrElse(null);
@@ -32,33 +18,48 @@ export default function useControlledPointsStore() {
3218 return;
3319 }
3420
21+ _setControlledPoints(Nothing());
22+
3523 const address = _wallet.address;
3624
37− const [
38− ownedPoints,
39− incomingPoints,
40− managingPoints,
41− votingPoints,
42− spawningPoints,
43− ] = await Promise.all([
44− azimuth.azimuth.getOwnedPoints(_contracts, address),
45− azimuth.azimuth.getTransferringFor(_contracts, address),
46− azimuth.azimuth.getManagerFor(_contracts, address),
47− azimuth.azimuth.getVotingFor(_contracts, address),
48− azimuth.azimuth.getSpawningFor(_contracts, address),
49− ]);
25+ try {
26+ const [
27+ ownedPoints,
28+ incomingPoints,
29+ managingPoints,
30+ votingPoints,
31+ spawningPoints,
32+ ] = await Promise.all([
33+ azimuth.azimuth.getOwnedPoints(_contracts, address),
34+ azimuth.azimuth.getTransferringFor(_contracts, address),
35+ azimuth.azimuth.getManagerFor(_contracts, address),
36+ azimuth.azimuth.getVotingFor(_contracts, address),
37+ azimuth.azimuth.getSpawningFor(_contracts, address),
38+ ]);
39+
40+ _setControlledPoints(
41+ Just(
42+ Result.Ok({
43+ ownedPoints,
44+ incomingPoints,
45+ managingPoints,
46+ votingPoints,
47+ spawningPoints,
48+ })
49+ )
50+ );
51+ } catch (error) {
52+ _setControlledPoints(Just(Result.Error(error)));
53+ }
54+ }, [contracts, wallet]);
5055
51− _setControlledPointsCache({
52− ownedPoints: Just(ownedPoints),
53− incomingPoints: Just(incomingPoints),
54− managingPoints: Just(managingPoints),
55− votingPoints: Just(votingPoints),
56− spawningPoints: Just(spawningPoints),
57− });
58− }, [contracts, wallet, _setControlledPointsCache]);
56+ // sync controlled points whenever wallet or contracts changes
57+ useEffect(() => {
58+ syncControlledPoints();
59+ }, [syncControlledPoints]);
5960
6061 return {
61− getControlledPoints,
62+ controlledPoints,
6263 syncControlledPoints,
6364 };
6465}
src/views/AcceptTransfer.jsnew file+96
@@ -0,0 +1,96 @@
1+import React, { useCallback } from 'react';
2+import cn from 'classnames';
3+import { Grid, Text } from 'indigo-react';
4+import * as azimuth from 'azimuth-js';
5+
6+import { useNetwork } from 'store/network';
7+import { usePointCursor } from 'store/pointCursor';
8+import { usePointCache } from 'store/pointCache';
9+import { useHistory } from 'store/history';
10+import { useWallet } from 'store/wallet';
11+
12+import * as need from 'lib/need';
13+import useCurrentPointName from 'lib/useCurrentPointName';
14+import useEthereumTransaction from 'lib/useEthereumTransaction';
15+import { GAS_LIMITS } from 'lib/constants';
16+import useLifecycle from 'lib/useLifecycle';
17+
18+import ViewHeader from 'components/ViewHeader';
19+import MiniBackButton from 'components/MiniBackButton';
20+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
21+import View from 'components/View';
22+
23+function useAcceptTransfer() {
24+ const { contracts } = useNetwork();
25+ const { pointCursor } = usePointCursor();
26+ const { syncOwnedPoint, syncControlledPoints } = usePointCache();
27+ const { wallet } = useWallet();
28+
29+ const _contracts = need.contracts(contracts);
30+ const _point = need.point(pointCursor);
31+ const _address = need.addressFromWallet(wallet);
32+
33+ const { construct, ...rest } = useEthereumTransaction(
34+ useCallback(
35+ () =>
36+ azimuth.ecliptic.transferPoint(
37+ _contracts,
38+ _point,
39+ _address,
40+ /* reset */ true
41+ ),
42+ [_contracts, _point, _address]
43+ ),
44+ useCallback(
45+ () => Promise.all([syncOwnedPoint(_point), syncControlledPoints()]),
46+ [_point, syncControlledPoints, syncOwnedPoint]
47+ ),
48+ GAS_LIMITS.TRANSFER
49+ );
50+
51+ useLifecycle(() => {
52+ construct();
53+ });
54+
55+ return {
56+ ...rest,
57+ };
58+}
59+
60+export default function AcceptTransfer() {
61+ const { pop } = useHistory();
62+
63+ const name = useCurrentPointName();
64+
65+ const { completed, bind } = useAcceptTransfer();
66+
67+ return (
68+ <View inset>
69+ <Grid>
70+ <Grid.Item full as={MiniBackButton} onClick={() => pop()} />
71+
72+ <Grid.Item full as={ViewHeader}>
73+ Accept Transfer
74+ </Grid.Item>
75+
76+ <Grid.Item
77+ full
78+ as={Text}
79+ className={cn('f5', {
80+ green3: completed,
81+ })}>
82+ {completed
83+ ? `${name} has been accepted.`
84+ : `Accept the incoming transfer of ${name}.`}
85+ </Grid.Item>
86+
87+ <Grid.Item
88+ full
89+ as={InlineEthereumTransaction}
90+ {...bind}
91+ onReturn={() => pop()}
92+ />
93+ </Grid>
94+ </View>
95+ );
96+}
src/views/CancelTransfer.jsnew file+21
@@ -0,0 +1,21 @@
1+import React from 'react';
2+
3+import View from 'components/View';
4+
5+import AdminCancelTransfer from './Admin/AdminCancelTransfer';
6+import { LocalRouterProvider } from 'lib/LocalRouter';
7+import { useHistory } from 'store/history';
8+
9+export default function CancelTransfer() {
10+ const history = useHistory();
11+
12+ return (
13+ // TODO: this is a hack so AdminCancelTransfer works unmodified
14+ // TODO: consolidate history & 'local' router providers
15+ <LocalRouterProvider value={history}>
16+ <View inset>
17+ <AdminCancelTransfer />
18+ </View>
19+ </LocalRouterProvider>
20+ );
21+}
src/views/Disclaimer.js+4−3
@@ -4,7 +4,7 @@ import { Grid, H3, B, Text, CheckboxInput } from 'indigo-react';
44
55import { useCheckboxInput } from 'lib/useInputs';
66import { useHistory } from 'store/history';
7−import { setDisclaimerCookie } from 'lib/disclaimerCookie';
7+import useHasDisclaimed from 'lib/useHasDisclaimed';
88
99import View from 'components/View';
1010import { ForwardButton } from 'components/Buttons';
@@ -14,15 +14,16 @@ const TEXT_STYLE = 'f5';
1414
1515export default function ActivateDisclaimer() {
1616 const { pop } = useHistory();
17+ const [, setHasDisclaimed] = useHasDisclaimed();
1718 const [understoodInput, { data: isUnderstood }] = useCheckboxInput({
1819 name: 'checkbox',
1920 label: 'I acknowledge and understand these rights',
2021 });
2122
2223 const goToPassport = useCallback(() => {
23− setDisclaimerCookie();
24+ setHasDisclaimed(true);
2425 pop();
25− }, [pop]);
26+ }, [pop, setHasDisclaimed]);
2627
2728 return (
2829 <View>
src/views/Points.js+184−151
@@ -1,186 +1,219 @@
11import React from 'react';
22import { Just, Nothing } from 'folktale/maybe';
3−import { H1, H2, P, Grid } from 'indigo-react';
4−
5−import { ROUTE_NAMES } from 'lib/routeNames';
6−import { isZeroAddress } from 'lib/wallet';
7−import useIsEclipticOwner from 'lib/useIsEclipticOwner';
8−import { useSyncKnownPoints } from 'lib/useSyncPoints';
3+import { Grid, H5, H1, HelpText, LinkButton, Flex } from 'indigo-react';
94
105import { useHistory } from 'store/history';
116import { useWallet } from 'store/wallet';
127import { usePointCache } from 'store/pointCache';
8+import { usePointCursor } from 'store/pointCursor';
9+
10+import * as need from 'lib/need';
11+import { ROUTE_NAMES } from 'lib/routeNames';
12+import { isZeroAddress, abbreviateAddress } from 'lib/wallet';
13+import useIsEclipticOwner from 'lib/useIsEclipticOwner';
14+import { useSyncKnownPoints } from 'lib/useSyncPoints';
1315
1416import View from 'components/View';
15−import { ForwardButton } from 'components/Buttons';
16−import useLifecycle from 'lib/useLifecycle';
17+import FooterButton from 'components/FooterButton';
18+import Blinky from 'components/Blinky';
19+import Passport from 'components/Passport';
20+import useRejectedIncomingPointTransfers from 'lib/useRejectedIncomingPointTransfers';
21+
22+const getFromMaybe = (obj, key, defaultValue) =>
23+ obj.matchWith({
24+ Nothing: () => defaultValue,
25+ Just: p =>
26+ p.value.matchWith({
27+ Ok: r => r.value[key],
28+ Error: e => defaultValue,
29+ }),
30+ });
1731
18−//TODO new component
19−import { usePointCursor } from 'store/pointCursor';
20−import * as ob from 'urbit-ob';
21−const PointList = function(props) {
32+const hasTransferProxy = (details, point) =>
33+ !isZeroAddress(details.transferProxy);
34+
35+const PointList = function({ points, className, actions, ...rest }) {
2236 const { setPointCursor } = usePointCursor();
2337 const history = useHistory();
24− const { points, loading } = props;
25−
26− return points.length === 0 ? (
27− <p>{loading ? 'Loading...' : 'No points to display'}</p>
28− ) : (
29− <>
30− {points.map(point => (
31− <P
32− key={point}
33− onClick={() => {
34− setPointCursor(Just(point));
35− history.push(ROUTE_NAMES.POINT);
36− }}>
37− {ob.patp(point)}
38− </P>
38+
39+ return (
40+ <Grid gap={3} className={className}>
41+ {points.map((point, i) => (
42+ <Grid.Item half={(i % 2) + 1} key={point}>
43+ <Flex col>
44+ <Passport.Mini
45+ point={point}
46+ className="clickable"
47+ onClick={() => {
48+ setPointCursor(Just(point));
49+ history.push(history.names.POINT);
50+ }}
51+ {...rest}
52+ />
53+ {actions && (
54+ <Flex.Item className="mt2">{actions(point, i)}</Flex.Item>
55+ )}
56+ </Flex>
57+ </Grid.Item>
3958 ))}
40− </>
59+ </Grid>
4160 );
4261};
4362
44−const matchArray = obj =>
45− obj.matchWith({ Nothing: () => [], Just: p => p.value });
46−
47−const hasTransferProxy = (details, point) =>
48− !isZeroAddress(details.transferProxy);
63+function ActionButtons({ actions = [] }) {
64+ return (
65+ <Flex row>
66+ {actions.map(action => (
67+ <Flex.Item
68+ key={action.text}
69+ as={LinkButton}
70+ className="mr3"
71+ onClick={action.onClick}>
72+ {action.text}
73+ </Flex.Item>
74+ ))}
75+ </Flex>
76+ );
77+}
4978
5079export default function Points() {
5180 const { wallet } = useWallet();
5281 const history = useHistory();
53− const {
54− getControlledPoints,
55− syncControlledPoints,
56− getDetails,
57− } = usePointCache();
58− const {
59− ownedPoints,
60− incomingPoints,
61− managingPoints,
62− votingPoints,
63− spawningPoints,
64− } = getControlledPoints(wallet);
82+ const { setPointCursor } = usePointCursor();
83+ const { controlledPoints, getDetails } = usePointCache();
6584 const isEclipticOwner = useIsEclipticOwner();
66−
67− const owned = matchArray(ownedPoints);
68− const incoming = matchArray(incomingPoints);
69− const managing = matchArray(managingPoints);
70− const voting = matchArray(votingPoints);
71− const spawning = matchArray(spawningPoints);
72−
73− // sync controlled points on mount
74− useLifecycle(() => {
75− syncControlledPoints();
76− });
77−
78− // sync display details for known points
79− useSyncKnownPoints([
80− ...owned,
81− ...incoming,
82− ...managing,
83− ...voting,
84− ...spawning,
85− ]);
86−
87− const loading = Nothing.hasInstance(ownedPoints);
88−
89− const outgoing = ownedPoints.filter(point =>
85+ const [
86+ rejectedPoints,
87+ addRejectedPoint,
88+ ] = useRejectedIncomingPointTransfers();
89+
90+ const address = need.addressFromWallet(wallet);
91+
92+ const loading = Nothing.hasInstance(controlledPoints);
93+
94+ const ownedPoints = getFromMaybe(controlledPoints, 'ownedPoints', []);
95+ const incomingPoints = getFromMaybe(
96+ controlledPoints,
97+ 'incomingPoints',
98+ []
99+ ).filter(point => !rejectedPoints.includes(point));
100+ const managingPoints = getFromMaybe(controlledPoints, 'managingPoints', []);
101+ const votingPoints = getFromMaybe(controlledPoints, 'votingPoints', []);
102+ const spawningPoints = getFromMaybe(controlledPoints, 'spawningPoints', []);
103+ const outgoingPoints = ownedPoints.filter(point =>
90104 getDetails(point).matchWith({
91105 Nothing: () => false,
92106 Just: p => hasTransferProxy(p.value),
93107 })
94108 );
95109
110+ const multipassPoints = [
111+ ...ownedPoints,
112+ ...managingPoints,
113+ ...votingPoints,
114+ ...spawningPoints,
115+ ];
116+
117+ // sync display details for known points
118+ useSyncKnownPoints([
119+ ...ownedPoints,
120+ ...incomingPoints,
121+ ...managingPoints,
122+ ...votingPoints,
123+ ...spawningPoints,
124+ ]);
125+
96126 return (
97127 <View>
98− <H1>Points</H1>
99−
100− <P>
101− A point is an identity on the Ethereum blockchain. Points declare keys
102− for ships on the Arvo network.
103− </P>
104−
105− <Grid>
106− <Grid.Divider />
107− <Grid.Item full>
108− <ForwardButton
109− detail="View a point on Azimuth"
110− onClick={() => history.push(ROUTE_NAMES.VIEW_POINT)}>
111− View a point
112− </ForwardButton>
128+ <Grid className="mt6">
129+ <Grid.Item full as={H1} className="f6 mono gray4 mb4">
130+ {abbreviateAddress(address)}
113131 </Grid.Item>
114− <Grid.Divider />
115− {isEclipticOwner && (
116− <>
117− <Grid.Item full>
118− <ForwardButton
119− detail="You have the authority to create a new Galaxy."
120− onClick={() => history.push(ROUTE_NAMES.CREATE_GALAXY)}>
121− Create a galaxy
122− </ForwardButton>
132+
133+ {loading && (
134+ <Grid.Item full as={HelpText} className="mt8 t-center">
135+ <Blinky /> Loading...
136+ </Grid.Item>
137+ )}
138+
139+ {incomingPoints.length > 0 && (
140+ <Grid.Item full as={Grid} gap={1} className="mb6">
141+ <Grid.Item full as={H5}>
142+ Incoming Transfers
123143 </Grid.Item>
124− <Grid.Divider />
125− </>
144+ <Grid.Item
145+ full
146+ as={PointList}
147+ points={incomingPoints}
148+ actions={(point, i) => (
149+ <ActionButtons
150+ actions={[
151+ {
152+ text: 'Accept',
153+ onClick: () => {
154+ setPointCursor(Just(point));
155+ history.push(history.names.ACCEPT_TRANSFER);
156+ },
157+ },
158+ {
159+ text: 'Reject',
160+ onClick: () => {
161+ addRejectedPoint(point);
162+ },
163+ },
164+ ]}
165+ />
166+ )}
167+ inverted
168+ />
169+ </Grid.Item>
170+ )}
171+
172+ {outgoingPoints.length > 0 && (
173+ <Grid.Item full as={Grid} gap={1} className="mb6">
174+ <Grid.Item full as={H5}>
175+ Outgoing Transfers
176+ </Grid.Item>
177+ <Grid.Item
178+ full
179+ as={PointList}
180+ points={outgoingPoints}
181+ actions={(point, i) => (
182+ <ActionButtons
183+ actions={[
184+ {
185+ text: 'Cancel',
186+ onClick: () => {
187+ setPointCursor(Just(point));
188+ // TODO: deep linking to fix this duplicate route
189+ history.push(history.names.CANCEL_TRANSFER);
190+ },
191+ },
192+ ]}
193+ />
194+ )}
195+ inverted
196+ />
197+ </Grid.Item>
126198 )}
127− </Grid>
128199
129− {incoming.length > 0 && (
130− <>
131− <H2>Incoming Transfers</H2>
132− <P>
133− You do not own these points until you accept the incoming transfer.
134− You may reject any incoming transfers.
135− </P>
136− <PointList routeHandler={history.push} points={incoming} />
137− </>
138− )}
139−
140− {outgoing.length > 0 && (
141− <>
142− <H2>Outgoing Transfers</H2>
143− <P>
144− You own these points until the recipient accepts the incoming
145− transfer. You may cancel the transfer until accepted.
146− </P>
147− <PointList routeHandler={history.push} points={outgoing} />
148− </>
149− )}
150−
151− <H2>Your Points</H2>
152−
153− <PointList routeHandler={history.push} points={owned} loading={loading} />
154−
155− {managing.length > 0 && (
156− <>
157− <H2>You Are a Management Proxy For</H2>
158− <P>
159− You can configure or set network keys and conduct sponsorship
160− related operations for these points.
161− </P>
162− <PointList routeHandler={history.push} points={managing} />
163− </>
164− )}
165−
166− {voting.length > 0 && (
167− <>
168− <H2>You Are a Voting Proxy For</H2>
169− <P>
170− Since you are part of the Galactic Senate, you can cast votes on new
171− Azimuth proposals on behalf of these points.
172− </P>
173− <PointList routeHandler={history.push} points={voting} />
174− </>
175− )}
176−
177− {spawning.length > 0 && (
178− <>
179− <H2>You Are a Spawn Proxy For</H2>
180− <P>You can create new child points under these points.</P>
181− <PointList routeHandler={history.push} points={spawning} />
182− </>
183− )}
200+ {multipassPoints.length > 0 && (
201+ <Grid.Item full as={Grid} gap={1}>
202+ <Grid.Item full as={H5}>
203+ Multipasses
204+ </Grid.Item>
205+ <Grid.Item full as={PointList} points={multipassPoints} />
206+ </Grid.Item>
207+ )}
208+
209+ {isEclipticOwner && (
210+ <FooterButton
211+ detail="You have the authority to create a new Galaxy."
212+ onClick={() => history.push(ROUTE_NAMES.CREATE_GALAXY)}>
213+ Create a galaxy
214+ </FooterButton>
215+ )}
216+ </Grid>
184217 </View>
185218 );
186219}