on df0c310 help sign in
bridge/src/views/Points.tsx
375 lines 11 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';
5import { useHistory } from 'store/history';
6import { useWallet } from 'store/wallet';
7import { usePointCache } from 'store/pointCache';
8import { usePointCursor } from 'store/pointCursor';
9import { useStarReleaseCache } from 'store/starRelease';
10import { useNetwork } from 'store/network';
11import { useRollerStore } from 'store/rollerStore';
13import * as need from 'lib/need';
14import { 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 Point from 'lib/types/Point';
20import newGithubIssueUrl from 'new-github-issue-url';
22import View from 'components/View';
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 PointList from 'components/L2/PointList';
31import { Box, Icon, Text } from '@tlon/indigo-react';
33import './Points.scss';
34import { StarReleaseButton } from './Points/StarReleaseButton';
35import { maybeGetResult } from 'lib/maybeGetResult';
36import { isGalaxy, isStar } from 'lib/utils/point';
37import { L2VulnAlert } from 'components/alerts/L2VulnAlert';
39export const isLocked = (details: any, contracts: any) =>
40 details.owner === contracts.linearSR ||
41 details.owner === contracts.conditionalSR;
43interface ActionButtonProps {
44 actions?: {
45 text?: string;
46 onClick?: () => void;
47 }[];
50function ActionButtons({ actions = [] }: ActionButtonProps) {
51 return (
52 <Flex row>
53 {actions.map(action => (
54 <Flex.Item
55 key={action.text}
56 as={LinkButton}
57 className="mr3"
58 onClick={action.onClick}>
59 {action.text}
60 </Flex.Item>
61 ))}
62 </Flex>
63 );
66export default function Points() {
67 const { wallet }: any = useWallet();
68 const { pop, push, names }: any = useHistory();
69 const { setPointCursor }: any = usePointCursor();
70 const { controlledPoints, getDetails }: any = usePointCache();
71 const { contracts }: any = useNetwork();
72 const isEclipticOwner = useIsEclipticOwner();
73 const [
74 rejectedPoints,
75 addRejectedPoint,
76 ] = useRejectedIncomingPointTransfers();
77 const { pointList } = useRollerStore();
78 const {
79 syncStarReleaseDetails,
80 starReleaseDetails,
81 }: any = useStarReleaseCache();
83 const outgoingPoints = useMemo(
84 () => pointList.filter(({ isOutgoing }) => isOutgoing),
85 [pointList]
86 );
88 const incomingPoints = useMemo(
89 () =>
90 pointList.filter(
91 ({ isTransferProxy, value }) =>
92 isTransferProxy && !rejectedPoints.includes(value)
93 ),
94 [pointList, rejectedPoints]
95 );
97 const _contracts = need.contracts(contracts);
99 const maybeLockedPoints = useMemo(
100 () =>
101 controlledPoints.chain((points: any) =>
102 points.matchWith({
103 Error: () => Nothing(),
104 Ok: (c: any) => {
105 const points = c.value.ownedPoints.map((point: number) =>
106 getDetails(point).chain((details: any) =>
107 Just({ point, has: isLocked(details, _contracts) })
108 )
109 );
110 // if we have details for every point,
111 // return the array of pending transfers.
112 if (points.every((p: any) => Just.hasInstance(p))) {
113 const locked = points
114 .filter((p: any) => p.value.has)
115 .map((p: any) => p.value.point);
116 return Just(locked);
117 } else {
118 return Nothing();
119 }
120 },
121 })
122 ),
123 [getDetails, controlledPoints, _contracts]
124 );
126 const allPoints = useMemo(
127 () =>
128 pointList.filter(
129 ({ isTransferProxy, isTransferProxySet, shouldDisplay, value }) =>
130 shouldDisplay &&
131 !(isTransferProxy || isTransferProxySet) &&
132 !rejectedPoints.includes(value)
133 ),
134 [pointList, rejectedPoints]
135 );
137 // if we can only interact with a single point, jump to the point page.
138 // if there are any pending transfers, incoming or outgoing, stay on this
139 // page, because those can only be completed/cancelled here.
140 useEffect(() => {
141 if (Nothing.hasInstance(starReleaseDetails)) {
142 return;
143 }
144 if (
145 allPoints.length === 1 &&
146 incomingPoints.length === 0 &&
147 outgoingPoints.length === 0 &&
148 (starReleaseDetails.value === null ||
149 starReleaseDetails.value.total === 0)
150 ) {
151 setPointCursor(Just(allPoints[0].value));
152 push(names.POINT);
153 }
154 }, [
155 allPoints,
156 rejectedPoints,
157 outgoingPoints,
158 incomingPoints,
159 setPointCursor,
160 push,
161 names,
162 starReleaseDetails,
163 ]);
165 const address = need.addressFromWallet(wallet);
167 const loading = Nothing.hasInstance(controlledPoints);
169 const ownedPoints = maybeGetResult(controlledPoints, 'ownedPoints', []);
170 const lockedPoints = maybeLockedPoints
171 .getOrElse([])
172 .map((value: number) => ({ value }));
174 const displayEmptyState =
175 !loading && incomingPoints.length === 0 && allPoints.length === 0;
177 // If the user has any stars or galaxies, give a polite heads up that
178 // it can take some time for the roller be aware of L1 TXs
179 const showMigrateNotice =
180 !displayEmptyState &&
181 allPoints.some(p => isStar(p.value) || isGalaxy(p.value));
183 const starReleasing = starReleaseDetails
184 .map((s: any) => (s ? s.total > 0 : false))
185 .getOrElse(false);
187 useEffect(() => {
188 syncStarReleaseDetails();
189 }, [syncStarReleaseDetails]);
191 useEffect(() => {
192 if (
193 'Notification' in window &&
194 Notification.permission !== 'denied' &&
195 Notification.permission !== 'granted'
196 ) {
197 Notification.requestPermission();
198 }
199 }, []); // eslint-disable-line react-hooks/exhaustive-deps
201 // sync display details for known points
202 useSyncDetails(ownedPoints);
204 const goCreateGalaxy = useCallback(() => push(names.CREATE_GALAXY), [
205 names.CREATE_GALAXY,
206 push,
207 ]);
209 const goStarRelease = useCallback(() => push(names.STAR_RELEASE), [
210 names.STAR_RELEASE,
211 push,
212 ]);
214 // if we got an error result, we should display it, instead of showing
215 // a potentially inaccurately empty point list.
216 //
217 if (
218 Just.hasInstance(controlledPoints) &&
219 controlledPoints.value.matchWith({
220 Error: (e: any) => true,
221 Ok: (v: any) => false,
222 })
223 ) {
224 const url = newGithubIssueUrl({
225 user: 'urbit',
226 repo: 'bridge',
227 title: controlledPoints.value.value,
228 body: `<!-- Please provide some context. Do you have Metamask installed? What login method did you use? -->`,
229 });
230 return (
231 <View inset>
232 <Grid>
233 <Grid.Item full as={HelpText} className="mt8 t-center">
234 {controlledPoints.value.value}
235 <br />
236 Try reloading the page. If this problem persists, please{' '}
237 <a href={url}>file an issue on GitHub</a>.
238 </Grid.Item>
239 </Grid>
240 </View>
241 );
242 }
244 if (
245 loading ||
246 (allPoints.length === 1 &&
247 incomingPoints.length === 0 &&
248 outgoingPoints.length === 0 &&
249 !starReleasing)
250 ) {
251 return <LoadingOverlay loading />;
252 }
254 return (
255 <View
256 inset
257 pop={pop}
258 hideBack
259 header={
260 <L2PointHeader
261 hideHome
262 hideInvites
263 showMigrate={!displayEmptyState}
264 points={allPoints}
265 />
266 }>
267 <NavHeader>
268 <CopiableAddress
269 text={address}
270 className="f6 mono gray4 mb4 us-none pointer">
271 {abbreviateAddress(address)}
272 </CopiableAddress>
273 </NavHeader>
274 <L2VulnAlert />
275 {incomingPoints.map(({ value }) => (
276 <IncomingPoint
277 key={value}
278 point={value}
279 accept={() => {
280 setPointCursor(Just(value));
281 push(names.ACCEPT_TRANSFER);
282 }}
283 reject={() => addRejectedPoint(value)}
284 />
285 ))}
286 <Grid>
287 {displayEmptyState && (
288 <Grid.Item full as={Box} className="empty-points-container">
289 <Text className="empty-points-text">No Urbit IDs here yet</Text>
290 </Grid.Item>
291 )}
293 {outgoingPoints.length > 0 && (
294 <Grid.Item full as={Grid} gap={1} className="mb6">
295 <Grid.Item full as={H5}>
296 {pluralize(
297 outgoingPoints.length,
298 'Outgoing Transfer',
299 'Outgoing Transfers'
300 )}
301 </Grid.Item>
302 <Grid.Item
303 full
304 as={PointList}
305 points={outgoingPoints}
306 actions={(point: Point, i: number) => (
307 <ActionButtons
308 actions={[
309 {
310 text: 'Cancel',
311 onClick: () => {
312 setPointCursor(Just(point.value));
313 // TODO: deep linking to fix this duplicate route
314 push(names.CANCEL_TRANSFER);
315 },
316 },
317 ]}
318 />
319 )}
320 />
321 </Grid.Item>
322 )}
324 {allPoints.length > 0 && (
325 <Grid.Item full as={Grid} gap={1}>
326 <Grid.Item full as={PointList} points={allPoints} />
327 </Grid.Item>
328 )}
330 {lockedPoints.length > 0 && (
331 <Grid.Item full as={Grid} gap={1}>
332 <Grid.Item full as={PointList} locked points={lockedPoints} />
333 </Grid.Item>
334 )}
336 {showMigrateNotice && (
337 <Grid.Item full as={Box} className={'migrated-info-box'}>
338 <span className={'icon'}>
339 <Icon icon="Info" />
340 </span>
341 <span className={'message'}>
342 Not seeing a recently-migrated Layer 2 point? It may take a few
343 minutes for the public roller to be aware of the transaction.
344 </span>
345 </Grid.Item>
346 )}
348 <Footer>
349 <Grid>
350 {isEclipticOwner && (
351 <>
352 <Grid.Item
353 full
354 as={ForwardButton}
355 detail="You have the authority to create a new Galaxy."
356 onClick={goCreateGalaxy}>
357 Create a galaxy
358 </Grid.Item>
359 <Grid.Divider />
360 </>
361 )}
362 {starReleasing && (
363 <>
364 <StarReleaseButton
365 title={'Locked Stars'}
366 subtitle={`${starReleaseDetails.value.withdrawn} of ${starReleaseDetails.value.total} available`}
367 onClick={goStarRelease}></StarReleaseButton>
368 </>
369 )}
370 </Grid>
371 </Footer>
372 </Grid>
373 </View>
374 );