on df0c310 help sign in
bridge/src/form/Inputs.tsx
335 lines 7.7 kB history raw
1import React, { useCallback } from 'react';
2import * as ob from 'urbit-ob';
3import { Input, AccessoryIcon } from 'indigo-react';
4import { useField, useForm } from 'react-final-form';
6import {
7 convertToNumber,
8 buildFormatter,
9 downcase,
10 ensurePatFormat,
11 stripHexPrefix,
12 ensureHexPrefix,
13 ticketToSegments,
14} from 'form/formatters';
15import { DEFAULT_HD_PATH } from 'lib/constants';
16import InputSigil from 'components/InputSigil';
17import {
18 StatelessTextInput,
19 StatelessTextInputProps,
20} from '@tlon/indigo-react';
22const PLACEHOLDER_GALAXY = '~pel';
23const PLACEHOLDER_STAR = '~sampel';
24export const PLACEHOLDER_PLANET = '~sampel-ponnym';
25const PLACEHOLDER_HD_PATH = DEFAULT_HD_PATH;
26const PLACEHOLDER_MNEMONIC =
27 'example crew supreme gesture quantum web media hazard theory mercy wing kitten';
28export const PLACEHOLDER_TICKET = '~sampel-ticlyt-migfun-falmel';
29const PLACEHOLDER_PASSWORD = '••••••-••••••-••••••-••••••';
30const PLACEHOLDER_ADDRESS = '0x';
31const PLACEHOLDER_HEX = '0x';
32const PLACEHOLDER_PRIVATE_KEY =
33 'a44de2416ee6beb2f323fab48b432925c9785808d33a6ca6d7ba00b45e9370c3';
34const PLACEHOLDER_EMAIL = 'email@example.com';
36const formatPat = buildFormatter([downcase, ensurePatFormat]);
38type TicketInputProps = React.HTMLProps<HTMLInputElement> & {
39 name: string;
40 hidden: boolean;
41 className: string;
42};
44export function TicketInput({
45 name,
46 hidden,
47 className,
48 ...rest
49}: TicketInputProps) {
50 const {
51 meta: { valid, error, validating, touched, active },
52 } = useField(name, {
53 subscription: {
54 valid: true,
55 error: true,
56 validating: true,
57 touched: true,
58 active: true,
59 },
60 });
62 return (
63 // This Input component is from the legacy `indigo-react` library, and is no longer being maintained.
64 // Ignoring the tsc compiler warning regarding unrecognized attributes from spreading `rest`
65 //@ts-ignore
66 <Input
67 className={className}
68 type={hidden ? 'password' : 'text'}
69 name={name}
70 obscure={(value: string) => value.replace(/[^~-]+/g, '••••••')}
71 placeholder={hidden ? PLACEHOLDER_PASSWORD : PLACEHOLDER_TICKET}
72 autoCapitalize="none"
73 autoCorrect="off"
74 accessory={
75 touched && !active && error ? (
76 <AccessoryIcon.Failure />
77 ) : validating ? (
78 <AccessoryIcon.Pending />
79 ) : valid ? (
80 <AccessoryIcon.Success />
81 ) : null
82 }
83 config={{ parse: formatPat }}
84 mono
85 {...rest}
86 />
87 );
90export function MnemonicInput({ ...rest }) {
91 return (
92 // @ts-ignore
93 <Input
94 type="textarea"
95 placeholder={PLACEHOLDER_MNEMONIC}
96 autoCapitalize="none"
97 autoComplete="off"
98 mono
99 {...rest}
100 />
101 );
104export function HdPathInput({ ...rest }) {
105 return (
106 // @ts-ignore
107 <Input
108 type="text"
109 placeholder={PLACEHOLDER_HD_PATH}
110 autoCapitalize="none"
111 autoComplete="off"
112 autoCorrect="off"
113 {...rest}
114 />
115 );
118export function PassphraseInput({ ...rest }) {
119 return (
120 // @ts-ignore
121 <Input
122 type="password"
123 placeholder="Passphrase"
124 autoCapitalize="none"
125 autoCorrect="off"
126 {...rest}
127 />
128 );
131type PointInputProps = React.HTMLProps<HTMLInputElement> & {
132 name: string;
133 size?: number;
134};
136export function PointInput({ name, size = 4, ...rest }: PointInputProps) {
137 const {
138 input: { value },
139 meta: { active, valid, error },
140 } = useField(name, {
141 subscription: { value: true, active: true, valid: true, error: true },
142 });
144 return (
145 // @ts-ignore
146 <Input
147 type="text"
148 label="Urbit ID"
149 name={name}
150 placeholder={
151 size === 1
152 ? PLACEHOLDER_GALAXY
153 : size === 2
154 ? PLACEHOLDER_STAR
155 : PLACEHOLDER_PLANET
156 }
157 autoCapitalize="none"
158 autoComplete="off"
159 autoCorrect="off"
160 accessory={
161 value ? (
162 <InputSigil
163 patp={value}
164 size={44}
165 valid={valid}
166 error={error}
167 active={active}
168 />
169 ) : null
170 }
171 config={{ parse: formatPat }}
172 mono
173 {...rest}
174 />
175 );
178export function HexInput({ type = 'text', ...rest }) {
179 return (
180 // @ts-ignore
181 <Input
182 type={type}
183 placeholder={PLACEHOLDER_HEX}
184 autoCapitalize="none"
185 autoComplete="off"
186 autoCorrect="off"
187 mono
188 config={{ parse: ensureHexPrefix }}
189 {...rest}
190 />
191 );
194export function PrivateKeyInput({ ...rest }) {
195 return (
196 <HexInput
197 type="password"
198 placeholder={PLACEHOLDER_PRIVATE_KEY}
199 config={{ parse: stripHexPrefix }}
200 {...rest}
201 />
202 );
205export function PsbtInput({ ...rest }) {
206 return (
207 <HexInput
208 type="textarea"
209 placeholder=""
210 config={{ parse: stripHexPrefix }}
211 {...rest}
212 />
213 );
216export function AddressInput({ ...rest }) {
217 return (
218 // @ts-ignore
219 <Input
220 type="text"
221 placeholder={PLACEHOLDER_ADDRESS}
222 autoCapitalize="none"
223 autoCorrect="off"
224 mono
225 {...rest}
226 />
227 );
230export function NumberInput({ ...rest }) {
231 return (
232 // @ts-ignore
233 <Input
234 type="number"
235 label="Number"
236 config={{ format: convertToNumber }}
237 {...rest}
238 />
239 );
242export function EmailInput({ ...rest }) {
243 return (
244 // @ts-ignore
245 <Input
246 type="email"
247 placeholder={PLACEHOLDER_EMAIL}
248 autoCapitalize="none"
249 {...rest}
250 />
251 );
254type TicketSegmentInputProps = {
255 className?: string;
256 name: string;
257 autoFocus?: boolean;
258 rest?: StatelessTextInputProps;
259};
261export function TicketSegmentInput({
262 className,
263 name,
264 ...rest
265}: TicketSegmentInputProps) {
266 const { input } = useField(name);
267 const { change } = useForm();
269 const onPaste = useCallback(
270 (event: React.ClipboardEvent<HTMLInputElement>) => {
271 try {
272 const pasteBuffer = event.clipboardData.getData('text');
273 const patp = ensurePatFormat(pasteBuffer);
274 if (!ob.isValidPatp(patp)) {
275 throw new Error('invalid patp');
276 }
278 // prevent pasting the entire clipboard text into the field
279 event.preventDefault();
281 const segments = ticketToSegments(pasteBuffer);
282 segments.forEach((s, i) => {
283 change(`ticket${i}`, s);
284 });
285 } catch (error) {
286 console.log(`skipping ticket autofill: ${error}`);
287 }
288 },
289 [change]
290 );
292 // Advance the focused field after typing 6 characters
293 const onChange = useCallback(
294 (event: React.ChangeEvent<HTMLInputElement>) => {
295 const currentIndex = Number(name.replace('ticket', ''));
296 if (currentIndex < 3 && event.target.value.length === 6) {
297 const nextField: HTMLElement | null = document.querySelector(
298 `input[name=ticket${currentIndex + 1}]`
299 );
301 if (nextField !== null) {
302 nextField.focus();
303 }
304 }
305 },
306 [name]
307 );
309 // Run the react-final-form onChange handler,
310 // and our own custom one to advance the form cursor
311 const mergedInput = {
312 ...input,
313 onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
314 input.onChange(e);
315 onChange(e);
316 },
317 };
319 return (
320 <StatelessTextInput
321 className={className}
322 {...mergedInput}
323 autoCapitalize="none"
324 autoCorrect="off"
325 type="text"
326 maxLength={6}
327 onPaste={onPaste}
328 {...rest}
329 />
330 );
333export const HiddenInput = ({ ...rest }) => {
334 return <StatelessTextInput type="hidden" {...rest} />;
335};