on ca31bdf 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';
12import * as need from 'lib/need';
13import { isZeroAddress, abbreviateAddress } from 'lib/utils/address';
14import useIsEclipticOwner from 'lib/useIsEclipticOwner';
15import { useSyncDetails } from 'lib/useSyncPoints';
16import useRejectedIncomingPointTransfers from 'lib/useRejectedIncomingPointTransfers';
17import { pluralize } from 'lib/pluralize';
18import newGithubIssueUrl from 'new-github-issue-url';
20import View from 'components/View';
21import Blinky from 'components/Blinky';
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 { useNetwork } from 'store/network';
31export const maybeGetResult = (obj, key, defaultValue) =>
32 obj.matchWith({
33 Nothing: () => defaultValue,
34 Just: p =>
35 p.value.matchWith({
36 Ok: r => get(r.value, key, defaultValue),
37 Error: e => defaultValue,
38 }),
39 });
41export const hasTransferProxy = details =>
42 !isZeroAddress(details.transferProxy);
44export const getOutgoingPoints = (controlledPoints, getDetails) => {
45 return controlledPoints
46 .chain(points =>
47 points.matchWith({
48 Error: () => Nothing(),
49 Ok: c => {
50 const points = c.value.ownedPoints.map(point =>
51 getDetails(point).chain(details =>
52 Just({ point: point, has: hasTransferProxy(details) })
53 )
54 );
55 if (points.every(p => Just.hasInstance(p))) {
56 const outgoing = points
57 .filter(p => p.value.has)
58 .map(p => p.value.point);
59 return Just(outgoing);
60 } else {
61 return Nothing();
62 }
63 },
64 })
65 )
66 .getOrElse([]);
67};
69export const isLocked = (details, contracts) =>
70 details.owner === contracts.linearSR ||
71 details.owner === contracts.conditionalSR;
73const PointList = function({
74 points,
75 className,
76 actions,
77 locked = false,
78 ...rest
79}) {
80 const { setPointCursor } = usePointCursor();
81 const { push, names } = useHistory();
83 return (
84 <Grid gap={4} className={className}>
85 {points.map((point, i) => (
86 <Grid.Item
87 key={point}
88 className={`full fourth-${(i % 4) + 1}-md fourth-${(i % 4) + 1}-lg`}>
89 <Flex col>
90 <Passport.Mini
91 locked={locked}
92 point={point}
93 onClick={
94 locked
95 ? undefined
96 : () => {
97 setPointCursor(Just(point));
98 push(names.POINT);
99 }
100 }
101 {...rest}
102 />
103 {actions && (
104 <Flex.Item className="mt2">{actions(point, i)}</Flex.Item>
105 )}
106 </Flex>
107 </Grid.Item>
108 ))}
109 </Grid>
110 );
111};
113function ActionButtons({ actions = [] }) {
114 return (
115 <Flex row>
116 {actions.map(action => (
117 <Flex.Item
118 key={action.text}
119 as={LinkButton}
120 className="mr3"
121 onClick={action.onClick}>
122 {action.text}
123 </Flex.Item>
124 ))}
125 </Flex>
126 );
129export default function Points() {
130 const { wallet } = useWallet();
131 const { pop, push, popAndPush, names } = useHistory();
132 const { setPointCursor } = usePointCursor();
133 const { controlledPoints, getDetails } = usePointCache();
134 const { contracts } = useNetwork();
135 const isEclipticOwner = useIsEclipticOwner();
136 const [
137 rejectedPoints,
138 addRejectedPoint,
139 ] = useRejectedIncomingPointTransfers();
140 const { syncStarReleaseDetails, starReleaseDetails } = useStarReleaseCache();
142 const outgoingPoints = useMemo(
143 () => getOutgoingPoints(controlledPoints, getDetails),
144 [getDetails, controlledPoints]
145 );
147 const _contracts = need.contracts(contracts);
149 const maybeLockedPoints = useMemo(
150 () =>
151 controlledPoints.chain(points =>
152 points.matchWith({
153 Error: () => Nothing(),
154 Ok: c => {
155 const points = c.value.ownedPoints.map(point =>
156 getDetails(point).chain(details =>
157 Just({ point, has: isLocked(details, _contracts) })
158 )
159 );
160 // if we have details for every point,
161 // return the array of pending transfers.
162 if (points.every(p => Just.hasInstance(p))) {
163 const locked = points
164 .filter(p => p.value.has)
165 .map(p => p.value.point);
166 return Just(locked);
167 } else {
168 return Nothing();
169 }
170 },
171 })
172 ),
173 [getDetails, controlledPoints, _contracts]
174 );
175 // if we can only interact with a single point, forget about the existence
176 // of this page and jump to the point page.
177 // if there are any pending transfers, incoming or outgoing, stay on this
178 // page, because those can only be completed/cancelled here.
179 useEffect(() => {
180 if (
181 Nothing.hasInstance(outgoingPoints) ||
182 Nothing.hasInstance(starReleaseDetails)
183 ) {
184 return;
185 }
186 controlledPoints.matchWith({
187 Nothing: () => null,
188 Just: r => {
189 r.value.matchWith({
190 Error: () => null,
191 Ok: c => {
192 let all = [
193 ...c.value.ownedPoints,
194 ...c.value.votingPoints,
195 ...c.value.managingPoints,
196 ...c.value.spawningPoints,
197 ];
198 const incoming = c.value.incomingPoints.filter(
199 p => !rejectedPoints.includes(p)
200 );
201 if (
202 all.length === 1 &&
203 incoming.length === 0 &&
204 outgoingPoints.length === 0 &&
205 (starReleaseDetails.value === null ||
206 starReleaseDetails.value.total === 0)
207 ) {
208 setPointCursor(Just(all[0]));
209 popAndPush(names.POINT);
210 }
211 },
212 });
213 },
214 });
215 }, [
216 controlledPoints,
217 rejectedPoints,
218 outgoingPoints,
219 setPointCursor,
220 popAndPush,
221 names,
222 starReleaseDetails,
223 ]);
225 const address = need.addressFromWallet(wallet);
227 const loading = Nothing.hasInstance(controlledPoints);
229 const ownedPoints = maybeGetResult(controlledPoints, 'ownedPoints', []);
230 const incomingPoints = maybeGetResult(
231 controlledPoints,
232 'incomingPoints',
233 []
234 ).filter(point => !rejectedPoints.includes(point));
235 const managingPoints = maybeGetResult(controlledPoints, 'managingPoints', []);
236 const votingPoints = maybeGetResult(controlledPoints, 'votingPoints', []);
237 const spawningPoints = maybeGetResult(controlledPoints, 'spawningPoints', []);
238 const lockedPoints = maybeLockedPoints.getOrElse([]);
240 const allPoints = [
241 ...ownedPoints.filter(p => !outgoingPoints.includes(p)),
242 ...managingPoints,
243 ...votingPoints,
244 ...spawningPoints,
245 ].sort((a, b) => Number(a) - Number(b));
247 const displayEmptyState =
248 !loading && incomingPoints.length === 0 && allPoints.length === 0;
250 const starReleasing = starReleaseDetails
251 .map(s => (s ? s.total > 0 : false))
252 .getOrElse(false);
254 useEffect(() => {
255 syncStarReleaseDetails();
256 }, [syncStarReleaseDetails]);
258 // sync display details for known points
259 useSyncDetails(ownedPoints);
261 const goCreateGalaxy = useCallback(() => push(names.CREATE_GALAXY), [
262 names.CREATE_GALAXY,
263 push,
264 ]);
266 const goStarRelease = useCallback(() => push(names.STAR_RELEASE), [
267 names.STAR_RELEASE,
268 push,
269 ]);
271 // if we got an error result, we should display it, instead of showing
272 // a potentially inaccurately empty point list.
273 //
274 if (
275 Just.hasInstance(controlledPoints) &&
276 controlledPoints.value.matchWith({
277 Error: e => true,
278 Ok: v => false,
279 })
280 ) {
281 const url = newGithubIssueUrl({
282 user: 'urbit',
283 repo: 'bridge',
284 title: controlledPoints.value.value,
285 body: `<!-- Please provide some context. Do you have Metamask installed? What login method did you use? -->`,
286 });
287 return (
288 <View inset>
289 <Grid>
290 <Grid.Item full as={HelpText} className="mt8 t-center">
291 {controlledPoints.value.value}
292 <br />
293 Try reloading the page. If this problem persists, please{' '}
294 <a href={url}>file an issue on GitHub</a>.
295 </Grid.Item>
296 </Grid>
297 </View>
298 );
299 }
301 if (
302 loading ||
303 (allPoints.length === 1 &&
304 incomingPoints.length === 0 &&
305 outgoingPoints.length === 0 &&
306 !starReleasing)
307 ) {
308 return (
309 <View inset>
310 <Grid>
311 <Grid.Item full as={HelpText} className="mt8 t-center">
312 <Blinky /> Loading...
313 </Grid.Item>
314 </Grid>
315 </View>
316 );
317 }
319 return (
320 <View
321 inset
322 pop={pop}
323 hideBack
324 header={<L2PointHeader hideTimer hideInvites showMigrate />}>
325 <NavHeader>
326 <CopiableAddress
327 text={address}
328 className="f6 mono gray4 mb4 us-none pointer">
329 {abbreviateAddress(address)}
330 </CopiableAddress>
331 </NavHeader>
332 {incomingPoints.map(point => (
333 <IncomingPoint
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 );