on 42e34eb help sign in
bridge/src/views/Activate/ActivateCodeForm.tsx
240 lines 7.2 kB history raw
1import * as need from 'lib/need';
2import * as ob from 'urbit-ob';
3import { WARNING } from 'form/helpers';
4import { FORM_ERROR } from 'final-form';
5import { Just } from 'folktale/maybe';
6import BridgeForm from 'form/BridgeForm';
7import { PLACEHOLDER_PLANET, PLACEHOLDER_TICKET, TicketInput } from 'form/Inputs';
8import {
9 composeValidator,
10 buildPatqValidator,
11 hasErrors,
12 buildCheckboxValidator,
13} from 'form/validators';
14import FormError from 'form/FormError';
15import { Grid } from 'indigo-react';
16import { useCallback, useMemo, useRef } from 'react';
17import { useActivateFlow } from './ActivateFlow';
18import WarningBox from 'components/WarningBox';
19import useRoller from 'lib/useRoller';
20import useImpliedTicket from 'lib/useImpliedTicket';
21import { generateWallet } from 'lib/walletgen';
22import { urbitWalletFromTicket, walletFromMnemonic } from 'lib/wallet';
23import { DEFAULT_HD_PATH, POINT_PROXIES } from 'lib/constants';
24import useBreakpoints from 'lib/useBreakpoints.tsx';
25import { Ship } from '@urbit/roller-api';
26import { convertToInt } from 'lib/convertToInt';
27import { Box } from '@tlon/indigo-react';
28import { timeout } from 'lib/timeout';
29import withFadeable from './withFadeable';
30import ActivateButton from './ActivateButton';
31import TicketInputAccessory from 'components/form/TicketInputAccessory';
32import { validateActivationTicket } from 'lib/validators';
34interface ActivateCodeFormProps {
35 afterSubmit: VoidFunction;
38const ActivateCodeForm = ({ afterSubmit }: ActivateCodeFormProps) => {
39 const { api, getPoints } = useRoller();
40 const { impliedTicket, impliedPatp } = useImpliedTicket();
41 const didWarn = useRef<boolean>(false);
43 const {
44 setDerivedPatp,
45 setDerivedPoint,
46 setDerivedPointDominion,
47 setDerivedWallet,
48 setInviteWallet,
49 setIsIn,
50 setSendWallet,
51 }: any = useActivateFlow();
53 // this is a pretty naive way to detect if we're on a mobile device
54 // (i.e. we're checking the width of the screen)
55 // but it will suffice for the 99% case and if someone wants to get around it
56 // well by golly they're allowed to turn their phone into landscape mode
57 // for this screen
58 const activationAllowed = useBreakpoints([false, true, true]);
60 const validateForm = useCallback(
61 (values, errors) => {
62 didWarn.current = false;
64 if (hasErrors(errors)) {
65 return errors;
66 }
67 },
68 [didWarn]
69 );
71 const validate = useMemo(
72 () =>
73 composeValidator(
74 {
75 ticket: buildPatqValidator([validateActivationTicket]),
76 showTicket: buildCheckboxValidator(),
77 },
78 validateForm
79 ),
80 [validateForm]
81 );
83 const hasInitialValues = useMemo(
84 () => Boolean(impliedTicket && impliedPatp),
85 [impliedTicket, impliedPatp]
86 );
88 const getTicketAndPoint = useCallback(
89 (invite: string) => {
90 const segments = invite.split('-');
91 return {
92 ticket: segments.slice(0, 4).join('-'),
93 point: impliedPatp
94 ? ob.patp2dec(impliedPatp)
95 : ob.patp2dec(`~${segments.slice(4, segments.length).join('-')}`),
96 };
97 },
98 [impliedPatp]
99 );
101 const loadPoints = useCallback(
102 async (address: string): Promise<Ship[]> => {
103 const owned = await getPoints(POINT_PROXIES.OWN, address);
104 const transferring = await getPoints(POINT_PROXIES.TRANSFER, address);
105 const incoming = [...owned, ...transferring];
106 console.log('incoming points', incoming);
107 return incoming;
108 },
109 [getPoints]
110 );
112 // derive and set our state on submission
113 const onSubmit = useCallback(
114 async values => {
115 // Allow UI animation to start
116 await timeout(42);
118 // Derive wallet
119 const { ticket, point } = getTicketAndPoint(
120 impliedTicket || values.ticket
121 );
123 const inviteWallet = Just(await urbitWalletFromTicket(ticket, point));
124 setInviteWallet(inviteWallet);
125 const _inviteWallet: UrbitWallet = need.wallet(inviteWallet);
126 const inviteAddress = _inviteWallet.ownership.keys.address;
128 const sendWallet = await walletFromMnemonic(
129 _inviteWallet.ownership.seed,
130 DEFAULT_HD_PATH
131 );
132 setSendWallet(sendWallet);
134 let incoming = await loadPoints(inviteAddress);
136 // Set derived
137 if (incoming.length > 0) {
138 if (incoming.length > 1 && !didWarn.current) {
139 didWarn.current = true;
141 return {
142 [WARNING]:
143 'This invite code has multiple points available. ' +
144 "Once you've activated this point, " +
145 'activate the next with the same process.',
146 };
147 }
149 const point = convertToInt(incoming[0], 10);
150 const rollerPoint = await api.getPoint(point!);
152 setDerivedPatp(Just(ob.patp(point)));
153 setDerivedPoint(Just(point));
154 setDerivedPointDominion(Just(rollerPoint.dominion));
155 setDerivedWallet(Just(await generateWallet(point, ticket, true)));
156 setIsIn(false);
157 await timeout(100);
158 } else {
159 return {
160 [FORM_ERROR]:
161 'Invite code has no claimable point.\n' +
162 'Check your invite code and try again?',
163 };
164 }
165 },
166 [
167 api,
168 impliedTicket,
169 getTicketAndPoint,
170 loadPoints,
171 setDerivedPatp,
172 setDerivedPoint,
173 setDerivedPointDominion,
174 setDerivedWallet,
175 setInviteWallet,
176 setIsIn,
177 setSendWallet,
178 ]
179 );
181 const initialValues = useMemo(
182 () => ({
183 ticket: hasInitialValues
184 ? `${impliedTicket}-${impliedPatp?.slice(1)}`
185 : '',
186 showTicket: true,
187 }),
188 [impliedTicket, impliedPatp, hasInitialValues]
189 );
191 return (
192 <Box
193 className="w-full h-full flex-col"
194 flexWrap="nowrap"
195 justifyContent="flex-end">
196 <BridgeForm
197 validate={validate}
198 onSubmit={onSubmit}
199 afterSubmit={afterSubmit}
200 initialValues={initialValues}>
201 {({ valid, validating, values, submitting, handleSubmit }) => (
202 <>
203 {!hasInitialValues && (
204 <Grid.Item
205 full
206 className="mb2"
207 as={TicketInput}
208 accessory={<TicketInputAccessory name="showTicket" />}
209 type={values.showTicket ? 'text' : 'password'}
210 name="ticket"
211 disabled={!activationAllowed}
212 placeholder={`${PLACEHOLDER_TICKET}-${PLACEHOLDER_PLANET.slice(
213 1
214 )}`}
215 />
216 )}
218 <Grid.Item full as={FormError} className="mb2" />
220 <ActivateButton
221 disabled={(!valid && !hasInitialValues) || submitting}
222 onClick={handleSubmit}>
223 {submitting ? 'Activating...' : 'Activate'}
224 </ActivateButton>
226 {!activationAllowed && (
227 <Grid.Item full as={WarningBox} className="mt4">
228 For your security, please access Bridge on a desktop device.
229 </Grid.Item>
230 )}
231 </>
232 )}
233 </BridgeForm>
234 </Box>
235 );
236};
238export default ActivateCodeForm;
240export const FadeableActivateCodeForm = withFadeable(ActivateCodeForm);