@@ -0,0 +1,303 @@
1+import { useCallback, useEffect, useMemo, useRef } from 'react';
2+import { Just } from 'folktale/maybe';
3+import * as azimuth from 'azimuth-js';
4+import { Grid, H4, P, CheckboxInput } from 'indigo-react';
5+import { FORM_ERROR } from 'final-form';
6+
7+import View from 'components/View';
8+import { ForwardButton } from 'components/Buttons';
9+import WarningBox from 'components/WarningBox';
10+import FooterButton from 'components/FooterButton';
11+import ActivateView from './ActivateView';
12+
13+import { useNetwork } from 'store/network';
14+import { useHistory } from 'store/history';
15+
16+import * as need from 'lib/need';
17+import { ROUTE_NAMES } from 'lib/routeNames';
18+import { walletFromMnemonic } from 'lib/wallet';
19+import { DEFAULT_HD_PATH, POINT_DOMINIONS, POINT_PROXIES } from 'lib/constants';
20+import { generateWallet } from 'lib/invite';
21+import { generateTemporaryOwnershipWallet } from 'lib/walletgen';
22+import { useLocalRouter } from 'lib/LocalRouter';
23+import useImpliedTicket from 'lib/useImpliedTicket';
24+import { timeout } from 'lib/timeout';
25+import useHasDisclaimed from 'lib/useHasDisclaimed';
26+import useBreakpoints from 'lib/useBreakpoints';
27+
28+import BridgeForm from 'form/BridgeForm';
29+import SubmitButton from 'form/SubmitButton';
30+import { TicketInput } from 'form/Inputs';
31+import {
32+ composeValidator,
33+ buildPatqValidator,
34+ hasErrors,
35+ buildCheckboxValidator,
36+} from 'form/validators';
37+import FormError from 'form/FormError';
38+
39+import { useActivateFlow } from './ActivateFlow';
40+import { WARNING } from 'form/helpers';
41+import { convertToInt } from 'lib/convertToInt';
42+import PointPresenter from './PointPresenter';
43+import useRoller from 'lib/useRoller';
44+import { Ship } from '@urbit/roller-api';
45+import { Box, Text } from '@tlon/indigo-react';
46+import ActivateHeader from './ActivateHeader';
47+
48+export default function ActivateCode() {
49+ const history = useHistory();
50+ const { names, push } = useLocalRouter();
51+ const { contracts } = useNetwork();
52+ const {
53+ impliedAzimuthPoint,
54+ impliedPatp,
55+ impliedTicket,
56+ } = useImpliedTicket();
57+ const [hasDisclaimed] = useHasDisclaimed();
58+ const didWarn = useRef(false);
59+ const { api, getPoints } = useRoller();
60+
61+ const {
62+ setDerivedPoint,
63+ setDerivedWallet,
64+ setInviteWallet,
65+ } = useActivateFlow();
66+ // this is a pretty naive way to detect if we're on a mobile device
67+ // (i.e. we're checking the width of the screen)
68+ // but it will suffice for the 99% case and if someone wants to get around it
69+ // well by golly they're allowed to turn their phone into landscape mode
70+ // for this screen
71+ const activationAllowed = useBreakpoints([false, true, true]);
72+
73+ const goToLogin = useCallback(() => history.popAndPush(ROUTE_NAMES.LOGIN), [
74+ history,
75+ ]);
76+
77+ const goToPassport = useCallback(() => {
78+ if (!hasDisclaimed) {
79+ push(names.DISCLAIMER, { next: names.PASSPORT });
80+ } else {
81+ push(names.PASSPORT);
82+ }
83+ }, [hasDisclaimed, names.DISCLAIMER, names.PASSPORT, push]);
84+
85+ const validateForm = useCallback((values, errors) => {
86+ didWarn.current = false;
87+
88+ if (hasErrors(errors)) {
89+ return errors;
90+ }
91+ }, []);
92+
93+ const validate = useMemo(
94+ () =>
95+ composeValidator(
96+ { ticket: buildPatqValidator(), showTicket: buildCheckboxValidator() },
97+ validateForm
98+ ),
99+ [validateForm]
100+ );
101+
102+ const loadL1Points = useCallback(
103+ async (address: string): Promise<number[]> => {
104+ const _contracts = need.contracts(contracts);
105+ const owned = await azimuth.azimuth.getOwnedPoints(_contracts, address);
106+ const transferring = await azimuth.azimuth.getTransferringFor(
107+ _contracts,
108+ address
109+ );
110+ const incoming = [...owned, ...transferring];
111+ console.log('incoming L1 points', incoming);
112+ return incoming;
113+ },
114+ [contracts]
115+ );
116+
117+ const loadl2points = useCallback(
118+ async (address: string): Promise<Ship[]> => {
119+ const owned = await getPoints(POINT_PROXIES.OWN, address);
120+ const transferring = await getPoints(POINT_PROXIES.TRANSFER, address);
121+ const incoming = [...owned, ...transferring];
122+ console.log('incoming L2 points', incoming);
123+ return incoming;
124+ },
125+ [getPoints]
126+ );
127+
128+ // derive and set our state on submission
129+ const onSubmit = useCallback(
130+ async values => {
131+ await timeout(16); // allow the ui changes to flush before we lag it out
132+
133+ // Derive wallet
134+ // TODO: fallback logic to first use implied values if present, otherwise form values?
135+ const ticket = impliedTicket || values.ticket;
136+ const { seed } = await generateTemporaryOwnershipWallet(ticket);
137+ const inviteWallet = walletFromMnemonic(seed, DEFAULT_HD_PATH);
138+ setInviteWallet(inviteWallet);
139+ const _inviteWallet = need.wallet(inviteWallet);
140+ const inviteAddress = _inviteWallet.address;
141+
142+ // Query for points depending on point's dominion
143+ const rollerPoint = await api.getPoint(impliedAzimuthPoint!);
144+ let incoming: number[] | Ship[];
145+ if (rollerPoint.dominion === POINT_DOMINIONS.L2) {
146+ incoming = await loadl2points(inviteAddress);
147+ } else {
148+ incoming = await loadL1Points(inviteAddress);
149+ }
150+
151+ // Set derived
152+ if (incoming.length > 0) {
153+ if (incoming.length > 1 && !didWarn.current) {
154+ didWarn.current = true;
155+
156+ return {
157+ [WARNING]:
158+ 'This invite code has multiple points available. ' +
159+ "Once you've activated this point, " +
160+ 'activate the next with the same process.',
161+ };
162+ }
163+
164+ const point = convertToInt(incoming[0], 10);
165+ console.log('submitting point', point);
166+
167+ setDerivedPoint(Just(point));
168+ setDerivedWallet(Just(await generateWallet(point, true)));
169+ } else {
170+ return {
171+ [FORM_ERROR]:
172+ 'Invite code has no claimable point.\n' +
173+ 'Check your invite code and try again?',
174+ };
175+ }
176+ },
177+ [
178+ api,
179+ impliedAzimuthPoint,
180+ impliedTicket,
181+ loadL1Points,
182+ loadl2points,
183+ setDerivedPoint,
184+ setDerivedWallet,
185+ setInviteWallet,
186+ ]
187+ );
188+
189+ const initialValues = useMemo(
190+ () => ({ ticket: impliedTicket || '', showTicket: true }),
191+ [impliedTicket]
192+ );
193+
194+ useEffect(() => {
195+ const fetchRollerPoint = async () => {
196+ if (!impliedAzimuthPoint) {
197+ return;
198+ }
199+ const p = await api.getPoint(impliedAzimuthPoint!);
200+ console.log(p);
201+ };
202+
203+ fetchRollerPoint();
204+ }, [api, impliedAzimuthPoint]);
205+
206+ const footer = useMemo(() => {
207+ return (
208+ <Box
209+ display="flex"
210+ flexDirection="column"
211+ flexWrap="nowrap"
212+ justifyContent="flex-end">
213+ <Grid>
214+ <BridgeForm
215+ validate={validate}
216+ onSubmit={onSubmit}
217+ afterSubmit={goToPassport}
218+ initialValues={initialValues}>
219+ {({ validating, values, submitting, handleSubmit }) => (
220+ <>
221+ {!impliedTicket && (
222+ <>
223+ <Grid.Item
224+ full
225+ as={TicketInput}
226+ type={values.showTicket ? 'text' : 'password'}
227+ name="ticket"
228+ label="Activation Code"
229+ disabled={!activationAllowed}
230+ />
231+
232+ <Grid.Item
233+ full
234+ as={CheckboxInput}
235+ name="showTicket"
236+ label="Show"
237+ />
238+ </>
239+ )}
240+
241+ <Grid.Item full as={FormError} />
242+
243+ <Grid.Item
244+ full
245+ as={SubmitButton}
246+ className="mt4"
247+ handleSubmit={handleSubmit}>
248+ {isWarning =>
249+ validating
250+ ? 'Deriving...'
251+ : submitting
252+ ? 'Generating...'
253+ : isWarning
254+ ? 'Continue Activation'
255+ : 'Claim'
256+ }
257+ </Grid.Item>
258+
259+ {!activationAllowed && (
260+ <Grid.Item full as={WarningBox} className="mt4">
261+ For your security, please access Bridge on a desktop device.
262+ </Grid.Item>
263+ )}
264+ </>
265+ )}
266+ </BridgeForm>
267+ </Grid>
268+ </Box>
269+ );
270+ }, [
271+ activationAllowed,
272+ goToPassport,
273+ impliedTicket,
274+ initialValues,
275+ onSubmit,
276+ validate,
277+ ]);
278+
279+ return (
280+ <View inset>
281+ <ActivateView
282+ header={<ActivateHeader copy={'Welcome. This is your Urbit.'} />}
283+ footer={footer}>
284+ <Box
285+ alignItems={'center'}
286+ display={'flex'}
287+ flexDirection={'column'}
288+ flexWrap={'nowrap'}
289+ height={'100%'}
290+ justifyContent={'center'}>
291+ {!impliedTicket && (
292+ <Text className="mb2">Enter your activation code to continue.</Text>
293+ )}
294+ {impliedPatp && <PointPresenter patp={impliedPatp} />}
295+ </Box>
296+ </ActivateView>
297+
298+ <FooterButton as={ForwardButton} onClick={goToLogin}>
299+ Login
300+ </FooterButton>
301+ </View>
302+ );
303+}