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