on feb602f help sign in
bridge/src/views/Activate/ActivateCodeForm.tsx
242 lines 7.3 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';
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 setInviteMasterTicketWallet,
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 // @ts-ignore
59 const activationAllowed = useBreakpoints([false, true, true]);
61 const validateForm = useCallback(
62 (values, errors) => {
63 didWarn.current = false;
65 if (hasErrors(errors)) {
66 return errors;
67 }
68 },
69 [didWarn]
70 );
72 const validate = useMemo(
73 () =>
74 composeValidator(
75 {
76 ticket: buildPatqValidator([validateActivationTicket]),
77 showTicket: buildCheckboxValidator(),
78 },
79 validateForm
80 ),
81 [validateForm]
82 );
84 const hasInitialValues = useMemo(
85 () => Boolean(impliedTicket && impliedPatp),
86 [impliedTicket, impliedPatp]
87 );
89 const getTicketAndPoint = useCallback(
90 (invite: string) => {
91 const segments = invite.split('-');
92 return {
93 ticket: segments.slice(0, 4).join('-'),
94 point: impliedPatp
95 ? ob.patp2dec(impliedPatp)
96 : ob.patp2dec(`~${segments.slice(4, segments.length).join('-')}`),
97 };
98 },
99 [impliedPatp]
100 );
102 const loadPoints = useCallback(
103 async (address: string): Promise<Ship[]> => {
104 const owned = await getPoints(POINT_PROXIES.OWN, address);
105 const transferring = await getPoints(POINT_PROXIES.TRANSFER, address);
106 const incoming = [...owned, ...transferring];
107 console.log('incoming points', incoming);
108 return incoming;
109 },
110 [getPoints]
111 );
113 // derive and set our state on submission
114 const onSubmit = useCallback(
115 async values => {
116 // Allow UI animation to start
117 await timeout(42);
119 // Derive wallet
120 const { ticket, point } = getTicketAndPoint(
121 impliedTicket || values.ticket
122 );
124 const inviteWallet = Just(await urbitWalletFromTicket(ticket, point));
125 setInviteWallet(inviteWallet);
126 const _inviteWallet: UrbitWallet = need.wallet(inviteWallet);
127 const inviteAddress = _inviteWallet.ownership.keys.address;
129 const sendWallet = await walletFromMnemonic(
130 _inviteWallet.ownership.seed,
131 DEFAULT_HD_PATH
132 );
133 setSendWallet(sendWallet);
135 let incoming = await loadPoints(inviteAddress);
137 // Set derived
138 if (incoming.length > 0) {
139 if (incoming.length > 1 && !didWarn.current) {
140 didWarn.current = true;
142 return {
143 [WARNING]:
144 'This invite code has multiple points available. ' +
145 "Once you've activated this point, " +
146 'activate the next with the same process.',
147 };
148 }
150 const point = convertToInt(incoming[0], 10);
151 const rollerPoint = await api.getPoint(point!);
153 setDerivedPatp(Just(ob.patp(point)));
154 setDerivedPoint(Just(point));
155 setDerivedPointDominion(Just(rollerPoint.dominion));
156 setInviteMasterTicketWallet(Just(await generateWallet(point, ticket, true)));
157 setIsIn(false);
158 await timeout(100);
159 } else {
160 return {
161 [FORM_ERROR]:
162 'Invite code has no claimable point.\n' +
163 'Check your invite code and try again?',
164 };
165 }
166 },
167 [
168 getTicketAndPoint,
169 impliedTicket,
170 setInviteWallet,
171 setSendWallet,
172 loadPoints,
173 api,
174 setDerivedPatp,
175 setDerivedPoint,
176 setDerivedPointDominion,
177 setInviteMasterTicketWallet,
178 setIsIn,
179 setSendWallet,
180 ]
181 );
183 const initialValues = useMemo(
184 () => ({
185 ticket: hasInitialValues
186 ? `${impliedTicket}-${impliedPatp?.slice(1)}`
187 : '',
188 showTicket: true,
189 }),
190 [impliedTicket, impliedPatp, hasInitialValues]
191 );
193 return (
194 <Box
195 className="w-full h-full flex-col"
196 flexWrap="nowrap"
197 justifyContent="flex-end">
198 <BridgeForm
199 validate={validate}
200 onSubmit={onSubmit}
201 afterSubmit={afterSubmit}
202 initialValues={initialValues}>
203 {({ values, valid, submitting, handleSubmit }: any) => (
204 <>
205 {!hasInitialValues && (
206 <Grid.Item
207 full
208 className="mb2"
209 as={TicketInput}
210 accessory={<TicketInputAccessory name="showTicket" />}
211 type={values.showTicket ? 'text' : 'password'}
212 name="ticket"
213 disabled={!activationAllowed}
214 placeholder={`${PLACEHOLDER_TICKET}-${PLACEHOLDER_PLANET.slice(
215 1
216 )}`}
217 />
218 )}
220 <Grid.Item full as={FormError} className="mb2" />
222 <ActivateButton
223 disabled={(!valid && !hasInitialValues) || submitting}
224 onClick={handleSubmit}>
225 {submitting ? 'Activating...' : 'Activate'}
226 </ActivateButton>
228 {!activationAllowed && (
229 <Grid.Item full as={WarningBox} className="mt4">
230 For your security, please access Bridge on a desktop device.
231 </Grid.Item>
232 )}
233 </>
234 )}
235 </BridgeForm>
236 </Box>
237 );
238};
240export default ActivateCodeForm;
242export const FadeableActivateCodeForm = withFadeable(ActivateCodeForm);