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