@@ -0,0 +1,298 @@
1+import { useCallback, useMemo, useRef, useState } from 'react';
2+import { Just } from 'folktale/maybe';
3+import * as kg from 'urbit-key-generation';
4+import { Grid, ToggleInput, AccessoryIcon } from 'indigo-react';
5+import { FORM_ERROR } from 'final-form';
6+import { Icon, Row } from '@tlon/indigo-react';
7+
8+import { useWallet } from 'store/wallet';
9+import { usePointCursor } from 'store/pointCursor';
10+
11+import { urbitWalletFromTicket } from 'lib/wallet';
12+import { WALLET_TYPES } from 'lib/constants';
13+import useImpliedPoint from 'lib/useImpliedPoint';
14+import useLoginView from 'lib/useLoginView';
15+import { patp2dec } from 'lib/patp2dec';
16+import { timeout } from 'lib/timeout';
17+import useRoller from 'lib/useRoller';
18+
19+import BridgeForm from 'form/BridgeForm';
20+import Condition from 'form/Condition';
21+import { TicketInput, PassphraseInput, PointInput } from 'form/Inputs';
22+import {
23+ composeValidator,
24+ buildCheckboxValidator,
25+ buildPatqValidator,
26+ buildShardValidator,
27+ buildPassphraseValidator,
28+ buildPointValidator,
29+} from 'form/validators';
30+import FormError from 'form/FormError';
31+import SubmitButton from 'form/SubmitButton';
32+import { WARNING } from 'form/helpers';
33+import Window from 'components/L2/Window/Window';
34+import HeaderPane from 'components/L2/Window/HeaderPane';
35+import BodyPane from 'components/L2/Window/BodyPane';
36+import AdvancedOptions from 'components/L2/Headers/AdvancedOptions';
37+
38+function TicketInputAccessory({ name }: { name: any }) {
39+ return (
40+ <AccessoryIcon>
41+ <ToggleInput
42+ name={name}
43+ className="mt1"
44+ inverseLabel={<Icon className="icon" icon="Visible" />}
45+ label={<Icon className="icon" icon="Hidden" />}
46+ />
47+ </AccessoryIcon>
48+ );
49+}
50+
51+interface TicketProps {
52+ className?: string;
53+ goHome: () => void;
54+}
55+
56+export default function Ticket({ className, goHome }: TicketProps) {
57+ useLoginView(WALLET_TYPES.TICKET);
58+
59+ const { setUrbitWallet }: any = useWallet();
60+ const { setPointCursor }: any = usePointCursor();
61+ const impliedPoint = useImpliedPoint();
62+ const didWarn = useRef(false);
63+ const { getPoints } = useRoller();
64+
65+ const [usePassphrase, setUsePassphrase] = useState(false);
66+ const [useShards, setUseShards] = useState(false);
67+
68+ const validateForm = useCallback((values, errors) => {
69+ didWarn.current = false;
70+
71+ if (errors.point) {
72+ return errors;
73+ }
74+
75+ if (values.useShards) {
76+ if (errors.shard1 || errors.shard2 || errors.shard3) {
77+ return errors;
78+ }
79+ const empty = ['shard1', 'shard2', 'shard3'].filter(s => !values[s]);
80+ if (empty.length > 1) {
81+ let errors = {};
82+ empty.forEach(s => {
83+ errors[s] = 'Please provide at least two out of three shards.';
84+ });
85+ return errors;
86+ }
87+ } else {
88+ if (errors.ticket) {
89+ return errors;
90+ }
91+ }
92+ }, []);
93+
94+ const onSubmit = useCallback(
95+ async values => {
96+ const ticket = values.useShards
97+ ? kg.combine(
98+ [values.shard1, values.shard2, values.shard3].map(v =>
99+ v === '' ? undefined : v
100+ )
101+ )
102+ : values.ticket;
103+
104+ try {
105+ // const _contracts = need.contracts(contracts);
106+ const point = patp2dec(values.point);
107+
108+ await timeout(16); // allow ui events to flush
109+ const urbitWallet = await urbitWalletFromTicket(
110+ ticket,
111+ point,
112+ values.passphrase
113+ );
114+ const [ownedPoints, incomingPoints] = await Promise.all([
115+ getPoints('own', urbitWallet.ownership.keys.address),
116+ getPoints('transfer', urbitWallet.ownership.keys.address),
117+ ]);
118+ const isOwner = ownedPoints.length !== 0;
119+ const isTransferProxy = incomingPoints.length !== 0;
120+
121+ const noPermissions = !isOwner && !isTransferProxy;
122+ // warn the user
123+ if (noPermissions && !didWarn.current) {
124+ didWarn.current = true;
125+ return {
126+ [WARNING]:
127+ 'This wallet is not the owner or transfer proxy for this point.',
128+ };
129+ }
130+
131+ setUrbitWallet(Just(urbitWallet));
132+ setPointCursor(Just(patp2dec(values.point)));
133+ } catch (error) {
134+ console.error(error);
135+ return {
136+ [FORM_ERROR]: `Unable to derive wallet from ${
137+ values.useShards ? 'shards' : 'ticket'
138+ }.`,
139+ };
140+ }
141+ },
142+ [setPointCursor, setUrbitWallet, getPoints]
143+ );
144+
145+ const validate = useMemo(
146+ () =>
147+ composeValidator(
148+ {
149+ useAdvanced: buildCheckboxValidator(),
150+ usePassphrase: buildCheckboxValidator(),
151+ useShards: buildCheckboxValidator(),
152+ point: buildPointValidator(4),
153+ ticket: buildPatqValidator(),
154+ shard1: buildShardValidator(),
155+ shard2: buildShardValidator(),
156+ shard3: buildShardValidator(),
157+ ticketHidden: buildCheckboxValidator(),
158+ passphrase: buildPassphraseValidator(),
159+ },
160+ validateForm
161+ ),
162+ [validateForm]
163+ );
164+
165+ const initialValues = useMemo(
166+ () => ({
167+ point: impliedPoint || '',
168+ usePasshrase: false,
169+ useShards: false,
170+ ticketHidden: true,
171+ useAdvanced: false,
172+ }),
173+ [impliedPoint]
174+ );
175+
176+ const advancedOptions = [
177+ {
178+ selected: usePassphrase,
179+ key: 'usePassphrase',
180+ label: 'Passphrase',
181+ onClick: () => setUsePassphrase(!usePassphrase),
182+ },
183+ {
184+ selected: useShards,
185+ key: 'useShards',
186+ label: 'Shards',
187+ onClick: () => setUseShards(!useShards),
188+ },
189+ ];
190+
191+ const ticketInputClass = usePassphrase && useShards ? 'ticket-input' : '';
192+
193+ return (
194+ <Window className="master-ticket">
195+ <HeaderPane>
196+ <Row className="header-row">
197+ <h5>Master Ticket</h5>
198+ <AdvancedOptions options={advancedOptions} />
199+ </Row>
200+ </HeaderPane>
201+ <BodyPane className="login-body-pane">
202+ <Grid className="input-form" gap={1}>
203+ <BridgeForm
204+ validate={validate}
205+ onSubmit={onSubmit}
206+ afterSubmit={goHome}
207+ className="flex-col justify-between"
208+ onValues={() => null}
209+ initialValues={initialValues}>
210+ {({ handleSubmit, values, submitting }) => (
211+ <Grid.Item full className="flex-col justify-between">
212+ <Grid.Item full>
213+ <Grid.Item
214+ full
215+ as={PointInput}
216+ name="point"
217+ className={ticketInputClass}
218+ />
219+
220+ <Condition when="useShards" is={false}>
221+ <Grid.Item
222+ className={ticketInputClass}
223+ full
224+ as={TicketInput}
225+ hidden={values.ticketHidden}
226+ accessory={<TicketInputAccessory name="ticketHidden" />}
227+ name="ticket"
228+ label="Master Ticket"
229+ style={{ height: 40 }}
230+ />
231+ </Condition>
232+
233+ {useShards && (
234+ <>
235+ <Grid.Item
236+ className={ticketInputClass}
237+ full
238+ as={TicketInput}
239+ accessory={<TicketInputAccessory name="ticketHidden" />}
240+ hidden={values.ticketHidden}
241+ name="shard1"
242+ label="Shard 1"
243+ />
244+ <Grid.Item
245+ className={ticketInputClass}
246+ full
247+ as={TicketInput}
248+ accessory={<TicketInputAccessory name="ticketHidden" />}
249+ hidden={values.ticketHidden}
250+ name="shard2"
251+ label="Shard 2"
252+ />
253+ <Grid.Item
254+ className={ticketInputClass}
255+ full
256+ as={TicketInput}
257+ accessory={<TicketInputAccessory name="ticketHidden" />}
258+ hidden={values.ticketHidden}
259+ name="shard3"
260+ label="Shard 3"
261+ />
262+ </>
263+ )}
264+
265+ {usePassphrase && (
266+ <Grid.Item
267+ full
268+ as={PassphraseInput}
269+ name="passphrase"
270+ label="Wallet Passphrase"
271+ className={ticketInputClass}
272+ />
273+ )}
274+
275+ <Grid.Item full as={FormError} />
276+ </Grid.Item>
277+
278+ <Grid.Item
279+ full
280+ as={SubmitButton}
281+ center
282+ handleSubmit={handleSubmit}>
283+ {(isWarning: boolean) =>
284+ submitting
285+ ? 'Logging in...'
286+ : isWarning
287+ ? 'Login Anyway'
288+ : 'Log In'
289+ }
290+ </Grid.Item>
291+ </Grid.Item>
292+ )}
293+ </BridgeForm>
294+ </Grid>
295+ </BodyPane>
296+ </Window>
297+ );
298+}