on ade464d help sign in
bridge/src/views/Points.js
425 lines 13 kB history raw
1import React, { useCallback, useEffect, useMemo } from 'react';
2import { Just, Nothing } from 'folktale/maybe';
3import { Grid, H5, HelpText, LinkButton, Flex } from 'indigo-react';
4import { get } from 'lodash';
6import { useHistory } from 'store/history';
7import { useWallet } from 'store/wallet';
8import { usePointCache } from 'store/pointCache';
9import { usePointCursor } from 'store/pointCursor';
10import { useStarReleaseCache } from 'store/starRelease';
11import { useNetwork } from 'store/network';
13import * as need from 'lib/need';
14import { isZeroAddress, abbreviateAddress } from 'lib/utils/address';
15import useIsEclipticOwner from 'lib/useIsEclipticOwner';
16import { useSyncDetails } from 'lib/useSyncPoints';
17import useRejectedIncomingPointTransfers from 'lib/useRejectedIncomingPointTransfers';
18import { pluralize } from 'lib/pluralize';
19import newGithubIssueUrl from 'new-github-issue-url';
21import View from 'components/View';
22import Passport from 'components/Passport';
23import Footer from 'components/Footer';
24import { ForwardButton } from 'components/Buttons';
25import CopiableAddress from 'components/copiable/CopiableAddress';
26import NavHeader from 'components/NavHeader';
27import L2PointHeader from 'components/L2/Headers/L2PointHeader';
28import IncomingPoint from 'components/L2/Points/IncomingPoint';
29import LoadingOverlay from 'components/L2/LoadingOverlay';
30import { onlyUnique } from 'lib/utils/array';
32export const maybeGetResult = (obj, key, defaultValue) =>
33 obj.matchWith({
34 Nothing: () => defaultValue,
35 Just: p =>
36 p.value.matchWith({
37 Ok: r => get(r.value, key, defaultValue),
38 Error: e => defaultValue,
39 }),
40 });
42export const hasTransferProxy = details =>
43 !isZeroAddress(details.transferProxy);
45export const getOutgoingPoints = (controlledPoints, getDetails) => {
46 return controlledPoints
47 .chain(points =>
48 points.matchWith({
49 Error: () => Nothing(),
50 Ok: c => {
51 const points = c.value.ownedPoints.map(point =>
52 getDetails(point).chain(details =>
53 Just({ point: point, has: hasTransferProxy(details) })
54 )
55 );
56 if (points.every(p => Just.hasInstance(p))) {
57 const outgoing = points
58 .filter(p => p.value.has)
59 .map(p => p.value.point);
60 return Just(outgoing);
61 } else {
62 return Nothing();
63 }
64 },
65 })
66 )
67 .getOrElse([]);
68};
70export const isLocked = (details, contracts) =>
71 details.owner === contracts.linearSR ||
72 details.owner === contracts.conditionalSR;
74const PointList = function({
75 points,
76 className,
77 actions,
78 locked = false,
79 ...rest
80}) {
81 const { setPointCursor } = usePointCursor();
82 const { push, names } = useHistory();
84 return (
85 <Grid gap={4} className={className}>
86 {points.map((point, i) => (
87 <Grid.Item
88 key={point}
89 className={`full fourth-${(i % 4) + 1}-md fourth-${(i % 4) + 1}-lg`}>
90 <Flex col>
91 <Passport.Mini
92 locked={locked}
93 point={point}
94 onClick={
95 locked
96 ? undefined
97 : () => {
98 setPointCursor(Just(point));
99 push(names.POINT);
100 }
101 }
102 {...rest}
103 />
104 {actions && (
105 <Flex.Item className="mt2">{actions(point, i)}</Flex.Item>
106 )}
107 </Flex>
108 </Grid.Item>
109 ))}
110 </Grid>
111 );
112};
114function ActionButtons({ actions = [] }) {
115 return (
116 <Flex row>
117 {actions.map(action => (
118 <Flex.Item
119 key={action.text}
120 as={LinkButton}
121 className="mr3"
122 onClick={action.onClick}>
123 {action.text}
124 </Flex.Item>
125 ))}
126 </Flex>
127 );
130export default function Points() {
131 const { wallet } = useWallet();
132 const { pop, push, popAndPush, names } = useHistory();
133 const { setPointCursor } = usePointCursor();
134 const { controlledPoints, getDetails } = usePointCache();
135 const { contracts } = useNetwork();
136 const isEclipticOwner = useIsEclipticOwner();
137 const [
138 rejectedPoints,
139 addRejectedPoint,
140 ] = useRejectedIncomingPointTransfers();
141 const { syncStarReleaseDetails, starReleaseDetails } = useStarReleaseCache();
143 const outgoingPoints = useMemo(
144 () => getOutgoingPoints(controlledPoints, getDetails),
145 [getDetails, controlledPoints]
146 );
148 const _contracts = need.contracts(contracts);
150 const maybeLockedPoints = useMemo(
151 () =>
152 controlledPoints.chain(points =>
153 points.matchWith({
154 Error: () => Nothing(),
155 Ok: c => {
156 const points = c.value.ownedPoints.map(point =>
157 getDetails(point).chain(details =>
158 Just({ point, has: isLocked(details, _contracts) })
159 )
160 );
161 // if we have details for every point,
162 // return the array of pending transfers.
163 if (points.every(p => Just.hasInstance(p))) {
164 const locked = points
165 .filter(p => p.value.has)
166 .map(p => p.value.point);
167 return Just(locked);
168 } else {
169 return Nothing();
170 }
171 },
172 })
173 ),
174 [getDetails, controlledPoints, _contracts]
175 );
176 // if we can only interact with a single point, forget about the existence
177 // of this page and jump to the point page.
178 // if there are any pending transfers, incoming or outgoing, stay on this
179 // page, because those can only be completed/cancelled here.
180 useEffect(() => {
181 if (
182 Nothing.hasInstance(outgoingPoints) ||
183 Nothing.hasInstance(starReleaseDetails)
184 ) {
185 return;
186 }
187 controlledPoints.matchWith({
188 Nothing: () => null,
189 Just: r => {
190 r.value.matchWith({
191 Error: () => null,
192 Ok: c => {
193 let all = [
194 ...c.value.ownedPoints,
195 ...c.value.votingPoints,
196 ...c.value.managingPoints,
197 ...c.value.spawningPoints,
198 ];
199 const incoming = c.value.incomingPoints.filter(
200 p => !rejectedPoints.includes(p)
201 );
202 if (
203 all.length === 1 &&
204 incoming.length === 0 &&
205 outgoingPoints.length === 0 &&
206 (starReleaseDetails.value === null ||
207 starReleaseDetails.value.total === 0)
208 ) {
209 setPointCursor(Just(all[0]));
210 popAndPush(names.POINT);
211 }
212 },
213 });
214 },
215 });
216 }, [
217 controlledPoints,
218 rejectedPoints,
219 outgoingPoints,
220 setPointCursor,
221 popAndPush,
222 names,
223 starReleaseDetails,
224 ]);
226 const address = need.addressFromWallet(wallet);
228 const loading = Nothing.hasInstance(controlledPoints);
230 const ownedPoints = maybeGetResult(controlledPoints, 'ownedPoints', []);
231 const incomingPoints = maybeGetResult(
232 controlledPoints,
233 'incomingPoints',
234 []
235 ).filter(point => !rejectedPoints.includes(point));
236 const managingPoints = maybeGetResult(controlledPoints, 'managingPoints', []);
237 const votingPoints = maybeGetResult(controlledPoints, 'votingPoints', []);
238 const spawningPoints = maybeGetResult(controlledPoints, 'spawningPoints', []);
239 const lockedPoints = maybeLockedPoints.getOrElse([]);
241 const allPoints = [
242 ...ownedPoints.filter(p => !outgoingPoints.includes(p)),
243 ...managingPoints,
244 ...votingPoints,
245 ...spawningPoints,
246 ]
247 .sort((a, b) => Number(a) - Number(b))
248 .filter(onlyUnique);
250 const displayEmptyState =
251 !loading && incomingPoints.length === 0 && allPoints.length === 0;
253 const starReleasing = starReleaseDetails
254 .map(s => (s ? s.total > 0 : false))
255 .getOrElse(false);
257 useEffect(() => {
258 syncStarReleaseDetails();
259 }, [syncStarReleaseDetails]);
261 // sync display details for known points
262 useSyncDetails(ownedPoints);
264 const goCreateGalaxy = useCallback(() => push(names.CREATE_GALAXY), [
265 names.CREATE_GALAXY,
266 push,
267 ]);
269 const goStarRelease = useCallback(() => push(names.STAR_RELEASE), [
270 names.STAR_RELEASE,
271 push,
272 ]);
274 // if we got an error result, we should display it, instead of showing
275 // a potentially inaccurately empty point list.
276 //
277 if (
278 Just.hasInstance(controlledPoints) &&
279 controlledPoints.value.matchWith({
280 Error: e => true,
281 Ok: v => false,
282 })
283 ) {
284 const url = newGithubIssueUrl({
285 user: 'urbit',
286 repo: 'bridge',
287 title: controlledPoints.value.value,
288 body: `<!-- Please provide some context. Do you have Metamask installed? What login method did you use? -->`,
289 });
290 return (
291 <View inset>
292 <Grid>
293 <Grid.Item full as={HelpText} className="mt8 t-center">
294 {controlledPoints.value.value}
295 <br />
296 Try reloading the page. If this problem persists, please{' '}
297 <a href={url}>file an issue on GitHub</a>.
298 </Grid.Item>
299 </Grid>
300 </View>
301 );
302 }
304 if (
305 loading ||
306 (allPoints.length === 1 &&
307 incomingPoints.length === 0 &&
308 outgoingPoints.length === 0 &&
309 !starReleasing)
310 ) {
311 return (
312 <View inset>
313 <LoadingOverlay loading />
314 </View>
315 );
316 }
318 return (
319 <View
320 inset
321 pop={pop}
322 hideBack
323 header={<L2PointHeader hideHome hideTimer hideInvites showMigrate />}>
324 <NavHeader>
325 <CopiableAddress
326 text={address}
327 className="f6 mono gray4 mb4 us-none pointer">
328 {abbreviateAddress(address)}
329 </CopiableAddress>
330 </NavHeader>
331 {incomingPoints.map(point => (
332 <IncomingPoint
333 key={point}
334 point={point}
335 accept={() => {
336 setPointCursor(Just(point));
337 push(names.ACCEPT_TRANSFER);
338 }}
339 reject={() => addRejectedPoint(point)}
340 />
341 ))}
342 <Grid>
343 {displayEmptyState && (
344 <Grid.Item full as={HelpText} className="mt8 t-center">
345 No points to display. This wallet is not the owner or proxy for any
346 points.
347 </Grid.Item>
348 )}
350 {outgoingPoints.length > 0 && (
351 <Grid.Item full as={Grid} gap={1} className="mb6">
352 <Grid.Item full as={H5}>
353 {pluralize(
354 outgoingPoints.length,
355 'Outgoing Transfer',
356 'Outgoing Transfers'
357 )}
358 </Grid.Item>
359 <Grid.Item
360 full
361 as={PointList}
362 points={outgoingPoints}
363 actions={(point, i) => (
364 <ActionButtons
365 actions={[
366 {
367 text: 'Cancel',
368 onClick: () => {
369 setPointCursor(Just(point));
370 // TODO: deep linking to fix this duplicate route
371 push(names.CANCEL_TRANSFER);
372 },
373 },
374 ]}
375 />
376 )}
377 />
378 </Grid.Item>
379 )}
381 {allPoints.length > 0 && (
382 <Grid.Item full as={Grid} gap={1}>
383 <Grid.Item full as={PointList} points={allPoints} />
384 </Grid.Item>
385 )}
387 {lockedPoints.length > 0 && (
388 <Grid.Item full as={Grid} gap={1}>
389 <Grid.Item full as={PointList} locked points={allPoints} />
390 </Grid.Item>
391 )}
393 <Footer>
394 <Grid>
395 <Grid.Divider />
396 {isEclipticOwner && (
397 <>
398 <Grid.Item
399 full
400 as={ForwardButton}
401 detail="You have the authority to create a new Galaxy."
402 onClick={goCreateGalaxy}>
403 Create a galaxy
404 </Grid.Item>
405 <Grid.Divider />
406 </>
407 )}
408 {starReleasing && (
409 <>
410 <Grid.Item
411 full
412 as={ForwardButton}
413 detail="You have points being released"
414 onClick={goStarRelease}>
415 View Star Release
416 </Grid.Item>
417 <Grid.Divider />
418 </>
419 )}
420 </Grid>
421 </Footer>
422 </Grid>
423 </View>
424 );