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