@@ -1,16 +1,16 @@
1−import { Just, Nothing } from 'folktale/maybe';
1+import Maybe from 'folktale/maybe';
22import React, { useCallback, useState, useEffect } from 'react';
33import * as azimuth from 'azimuth-js';
44import * as ob from 'urbit-ob';
5−import { Input, AccessoryIcon } from 'indigo-react';
5+import * as kg from 'urbit-key-generation/dist/index';
6+import { Input, Grid, CheckboxInput } from 'indigo-react';
67
7−import View from 'components/View';
88import {
99 usePointInput,
1010 useTicketInput,
1111 usePassphraseInput,
12+ useCheckboxInput,
1213} from 'components/Inputs';
13−import { ForwardButton } from 'components/Buttons';
1414
1515import { useNetwork } from 'store/network';
1616import { useWallet } from 'store/wallet';
@@ -18,123 +18,189 @@ import { usePointCursor } from 'store/pointCursor';
1818
1919import * as need from 'lib/need';
2020import { WALLET_TYPES, urbitWalletFromTicket } from 'lib/wallet';
21−import useWalletType from 'lib/useWalletType';
22−import useResetPointCursor from 'lib/useResetPointCursor';
23−
24−//TODO should be part of InputWithStatus component
25−const INPUT_STATUS = {
26− SPIN: Symbol('SPIN'),
27− GOOD: Symbol('GOOD'),
28− FAIL: Symbol('FAIL'),
29−};
30−
31−export default function Ticket({ advanced, loginCompleted }) {
32− useResetPointCursor();
33− useWalletType(WALLET_TYPES.TICKET);
21+import useImpliedPoint from 'lib/useImpliedPoint';
22+import useLoginView from 'lib/useLoginView';
23+
24+export default function Ticket({ className }) {
25+ useLoginView(WALLET_TYPES.TICKET);
3426 // globals
3527 const { contracts } = useNetwork();
36− const { wallet, setUrbitWallet } = useWallet();
28+ const { setUrbitWallet } = useWallet();
3729 const { setPointCursor } = usePointCursor();
30+ const impliedPoint = useImpliedPoint();
3831
3932 // inputs
33+
34+ // point
4035 const pointInput = usePointInput({
4136 name: 'point',
42− //TODO deduce point name from URL if we can, prefill input if we found it
37+ initialValue: impliedPoint,
4338 autoFocus: true,
4439 });
4540 const pointName = pointInput.data;
4641
42+ // passphrase
43+ const passphraseInput = usePassphraseInput({
44+ name: 'passphrase',
45+ label: '(Optional) Wallet Passphrase',
46+ });
47+ const passphrase = passphraseInput.data;
48+
49+ const hasPassphraseInput = useCheckboxInput({
50+ name: 'has-passphrase',
51+ label: 'Passphrase',
52+ initialValue: false,
53+ });
54+
55+ // ticket
56+ const [error, setError] = useState();
57+ const [deriving, setDeriving] = useState(false);
4758 const ticketInput = useTicketInput({
4859 name: 'ticket',
4960 label: 'Master Ticket',
61+ error,
62+ deriving,
5063 });
5164 const ticket = ticketInput.data;
5265
53− const passphraseInput = usePassphraseInput({
54− name: 'passphrase',
55− label: '(Optional) Wallet Passphrase',
66+ // shards
67+ const shardsInput = useCheckboxInput({
68+ name: 'shards',
69+ label: 'Shards',
70+ initialValue: false,
5671 });
57− const passphrase = passphraseInput.data;
5872
59− // display state
60− const [ticketStatus, setTicketStatus] = useState(Nothing());
61−
62− //TODO maybe want to do this only on-go, because wallet derivation is slow...
63− const verifyTicket = useCallback(
64− async (pointName, ticket, passphrase) => {
65− setUrbitWallet(Nothing());
66− if (!ob.isValidPatq(ticket) || !ob.isValidPatp(pointName)) {
67− setTicketStatus(Nothing());
68− return;
69− }
70− setTicketStatus(Just(INPUT_STATUS.SPIN));
71− const _contracts = need.contracts(contracts);
72− const pointNumber = ob.patp2dec(pointName);
73− const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
74− const isOwner = azimuth.azimuth.isOwner(
73+ const shard1Input = useTicketInput({
74+ name: 'shard1',
75+ label: 'Shard 1',
76+ });
77+ const shard1 = shard1Input.data;
78+ const shard2Input = useTicketInput({
79+ name: 'shard2',
80+ label: 'Shard 2',
81+ });
82+ const shard2 = shard2Input.data;
83+ const shard3Input = useTicketInput({
84+ name: 'shard3',
85+ label: 'Shard 3',
86+ });
87+ const shard3 = shard3Input.data;
88+
89+ const isUsingShards = !!shardsInput.data;
90+ const shards = [shard1, shard2, shard3];
91+ const shardsReady = shards.filter(x => x !== '').length > 1;
92+
93+ // TODO: maybe want to do this only on-go, because wallet derivation is slow...
94+ const deriveWalletFromTicket = useCallback(async () => {
95+ // clear states
96+ setError();
97+ setDeriving(true);
98+ setUrbitWallet(Maybe.Nothing());
99+
100+ if (!ob.isValidPatq(ticket) || !ob.isValidPatp(pointName)) {
101+ setDeriving(false);
102+ return;
103+ }
104+
105+ const _contracts = need.contracts(contracts);
106+ const pointNumber = ob.patp2dec(pointName);
107+ const urbitWallet = await urbitWalletFromTicket(
108+ ticket,
109+ pointName,
110+ passphrase
111+ );
112+ const [isOwner, isTransferProxy] = await Promise.all([
113+ azimuth.azimuth.isOwner(
75114 _contracts,
76115 pointNumber,
77− uhdw.ownership.keys.address
78− );
79− const isTransferProxy = azimuth.azimuth.isTransferProxy(
116+ urbitWallet.ownership.keys.address
117+ ),
118+ azimuth.azimuth.isTransferProxy(
80119 _contracts,
81120 pointNumber,
82− uhdw.ownership.keys.address
121+ urbitWallet.ownership.keys.address
122+ ),
123+ ]);
124+
125+ if (!isOwner && !isTransferProxy) {
126+ setError(
127+ 'This ticket is not the owner of or transfer proxy for this point.'
83128 );
84− setUrbitWallet(Just(uhdw));
85− const newStatus =
86− (await isOwner) || (await isTransferProxy)
87− ? INPUT_STATUS.GOOD
88− : INPUT_STATUS.FAIL;
89− setTicketStatus(Just(newStatus));
90− },
91− [contracts, setUrbitWallet, setTicketStatus]
92− );
129+ setDeriving(false);
130+ return;
131+ }
132+
133+ setUrbitWallet(Maybe.Just(urbitWallet));
134+ setPointCursor(Maybe.Just(ob.patp2dec(pointName)));
135+ setDeriving(false);
136+ }, [
137+ pointName,
138+ ticket,
139+ passphrase,
140+ contracts,
141+ setUrbitWallet,
142+ setPointCursor,
143+ setDeriving,
144+ ]);
145+
146+ const deriveWalletFromShards = useCallback(async () => {
147+ if (!shardsReady) {
148+ return;
149+ }
150+
151+ const s1 = shard1 || undefined;
152+ const s2 = shard2 || undefined;
153+ const s3 = shard3 || undefined;
154+
155+ try {
156+ const ticket = kg.combine([s1, s2, s3]);
157+ const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
158+ setUrbitWallet(Maybe.Just(uhdw));
159+ } catch (_) {
160+ // do nothing
161+ }
162+ }, [
163+ passphrase,
164+ pointName,
165+ shardsReady,
166+ setUrbitWallet,
167+ shard1,
168+ shard2,
169+ shard3,
170+ ]);
171+
172+ // use shards if
173+ useEffect(() => {
174+ if (isUsingShards && shardsReady) {
175+ deriveWalletFromShards();
176+ }
177+ }, [isUsingShards, shardsReady, deriveWalletFromShards]);
93178
179+ // verify ticket if not using shards
94180 useEffect(() => {
95− verifyTicket(pointName, ticket, passphrase);
96− }, [verifyTicket, pointName, ticket, passphrase]);
97−
98− const canContinue = () => {
99− // this is our only requirement, since we still want people with
100− // non-standard wallet setups to be able to log in
101− return Just.hasInstance(wallet);
102− };
103−
104− const doContinue = () => {
105− setPointCursor(Just(ob.patp2dec(pointName)));
106− loginCompleted();
107− };
108−
109− //TODO integrate into TicketInput?
110− const displayTicketStatus = ticketStatus.matchWith({
111− Nothing: () => <span />,
112− Just: status => {
113− switch (status.value) {
114− case INPUT_STATUS.SPIN:
115− return <AccessoryIcon.Pending />;
116− case INPUT_STATUS.GOOD:
117− return <AccessoryIcon.Success />;
118− case INPUT_STATUS.FAIL:
119− return <AccessoryIcon.Failure />;
120− default:
121− return null;
122− }
123− },
124− });
181+ if (!isUsingShards) {
182+ deriveWalletFromTicket();
183+ }
184+ }, [isUsingShards, deriveWalletFromTicket]);
125185
126186 return (
127− <View>
128− <Input {...pointInput} />
129− <Input {...ticketInput} accessory={displayTicketStatus} />
130− {advanced && <Input {...passphraseInput} />}
131−
132− <ForwardButton
133− className={'mt3'}
134− disabled={!canContinue()}
135− onClick={doContinue}>
136− Continue
137− </ForwardButton>
138− </View>
187+ <Grid className={className}>
188+ <Grid.Item full as={Input} {...pointInput} />
189+
190+ {!isUsingShards && <Grid.Item full as={Input} {...ticketInput} />}
191+ {isUsingShards && (
192+ <>
193+ <Grid.Item full as={Input} {...shard1Input} />
194+ <Grid.Item full as={Input} {...shard2Input} />
195+ <Grid.Item full as={Input} {...shard3Input} />
196+ </>
197+ )}
198+ {hasPassphraseInput.data && (
199+ <Grid.Item full as={Input} {...passphraseInput} />
200+ )}
201+
202+ <Grid.Item full as={CheckboxInput} {...hasPassphraseInput} />
203+ <Grid.Item full as={CheckboxInput} {...shardsInput} />
204+ </Grid>
139205 );
140206}