on master help sign in
ux: prepare existing Activation flow for L2
333d26ftomholford
src/Bridge.js+2−2
@@ -42,7 +42,7 @@ const INITIAL_POINT_CURSOR = IS_STUBBED ? Just(65792) : Nothing();
42
42
43function useInitialRoutes() {
43function useInitialRoutes() {
44 const [hasDisclaimed] = useHasDisclaimed();
44 const [hasDisclaimed] = useHasDisclaimed();
45− const hasImpliedTicket = !!useImpliedTicket();
45+ const { impliedTicket } = useImpliedTicket();
46
46
47 const isActivateUrl = window.location.pathname === '/activate';
47 const isActivateUrl = window.location.pathname === '/activate';
48
48
@@ -54,7 +54,7 @@ function useInitialRoutes() {
54 ];
54 ];
55 }
55 }
56
56
57− if (hasImpliedTicket || isActivateUrl) {
57+ if (impliedTicket || isActivateUrl) {
58 return [{ key: ROUTE_NAMES.ACTIVATE }];
58 return [{ key: ROUTE_NAMES.ACTIVATE }];
59 }
59 }
60
60
src/form/formatters.js+9
@@ -2,6 +2,7 @@ import { compose, strSplice } from 'lib/lib';
2import { convertToInt } from 'lib/convertToInt';
2import { convertToInt } from 'lib/convertToInt';
3
3
4const HEX_PREFIX = '0x';
4const HEX_PREFIX = '0x';
5+const SIG_PREFIX = '~';
5const TICKET_MAX_BYTE_LEN = 32; // tickets can be as large as 32 bytes
6const TICKET_MAX_BYTE_LEN = 32; // tickets can be as large as 32 bytes
6const PAT_BLOCK_CHAR_LENGTH = 6; // pat{p,q} blocks are 6 characters long
7const PAT_BLOCK_CHAR_LENGTH = 6; // pat{p,q} blocks are 6 characters long
7
8
@@ -77,3 +78,11 @@ export const stripHexPrefix = s => {
77 ? s.slice(HEX_PREFIX.length)
78 ? s.slice(HEX_PREFIX.length)
78 : s;
79 : s;
79};
80};
81+
82+export const ensureSigPrefix = s => {
83+ if (!s || s.length < SIG_PREFIX.length) {
84+ return s || '';
85+ }
86+
87+ return s.indexOf(SIG_PREFIX) !== 0 ? `${SIG_PREFIX}${s}` : s;
88+};
src/form/formatters.test.tsnew file+45
@@ -0,0 +1,45 @@
1+import { ensurePatFormat } from './formatters';
2+
3+describe('#ensurePatFormat', () => {
4+ it('ensures a valid ticket', () => {
5+ const input = '~ticket-ticket-ticket-ticket';
6+ const expected = '~ticket-ticket-ticket-ticket';
7+
8+ expect(ensurePatFormat(input)).toEqual(expected);
9+ });
10+
11+ it('prepends a ~ when missing', () => {
12+ const input = 'ticket-ticket-ticket-ticket';
13+ const expected = '~ticket-ticket-ticket-ticket';
14+
15+ expect(ensurePatFormat(input)).toEqual(expected);
16+ });
17+
18+ it('inserts missing separators', () => {
19+ const input = 'ticketticketticketticket';
20+ const expected = '~ticket-ticket-ticket-ticket';
21+
22+ expect(ensurePatFormat(input)).toEqual(expected);
23+ });
24+
25+ it('handles uneven patp', () => {
26+ const input = 'ticket-ticket-tic';
27+ const expected = '~ticket-ticket-tic';
28+
29+ expect(ensurePatFormat(input)).toEqual(expected);
30+ });
31+
32+ it('supports master ticket + star combo for Activation flow', () => {
33+ const input = 'ticket-ticket-ticket-ticket-sampel';
34+ const expected = '~ticket-ticket-ticket-ticket-sampel';
35+
36+ expect(ensurePatFormat(input)).toEqual(expected);
37+ });
38+
39+ it('supports master ticket + planet combo for Activation flow', () => {
40+ const input = 'ticket-ticket-ticket-ticket-sampel-palnet';
41+ const expected = '~ticket-ticket-ticket-ticket-sampel-palnet';
42+
43+ expect(ensurePatFormat(input)).toEqual(expected);
44+ });
45+});
src/lib/useImpliedTicket.jsdeleted−14
@@ -1,14 +0,0 @@
1−import ob from 'urbit-ob';
2−
3−import { ensurePatFormat } from 'form/formatters';
4−
5−/**
6− * pull the suggested ticket from the #hash in the url
7− */
8−export default function useImpliedTicket() {
9− const hash = window.location.hash.slice(1);
10− const ticket = ensurePatFormat(hash);
11− const isValid = ob.isValidPatq(ticket);
12−
13− return isValid ? ticket : null;
14−}
src/lib/useImpliedTicket.test.tsnew file+59
@@ -0,0 +1,59 @@
1+import useImpliedTicket from './useImpliedTicket';
2+
3+describe('#useImpliedTicket', () => {
4+ const { location } = window;
5+
6+ beforeAll(() => {
7+ delete window.location;
8+ });
9+
10+ afterAll(() => {
11+ window.location = location;
12+ });
13+
14+ describe('when hash is not present in URL', () => {
15+ it('returns null', () => {
16+ const input = '';
17+ window.location = { hash: input };
18+
19+ const { impliedTicket } = useImpliedTicket();
20+ expect(impliedTicket).toEqual(null);
21+ });
22+ });
23+
24+ describe('when hash is just a ticket', () => {
25+ it('parses the ticket, returns null point', () => {
26+ const input = '#widweb-tipfep-sabnux-nispec';
27+ window.location = { hash: input };
28+
29+ const { impliedPoint, impliedTicket } = useImpliedTicket();
30+
31+ expect(impliedPoint).toBe(null);
32+ expect(impliedTicket).toEqual('~widweb-tipfep-sabnux-nispec');
33+ });
34+ });
35+
36+ describe('when hash has both ticket and star point', () => {
37+ it('parses the ticket, returns null point', () => {
38+ const input = '#widweb-tipfep-sabnux-nispec-sampel';
39+ window.location = { hash: input };
40+
41+ const { impliedPoint, impliedTicket } = useImpliedTicket();
42+
43+ expect(impliedPoint).toBe('~sampel');
44+ expect(impliedTicket).toEqual('~widweb-tipfep-sabnux-nispec');
45+ });
46+ });
47+
48+ describe('when hash has both ticket and planet point', () => {
49+ it('parses both ticket and point', () => {
50+ const input = '#widweb-tipfep-sabnux-nispec-sampel-palnet';
51+ window.location = { hash: input };
52+
53+ const { impliedPoint, impliedTicket } = useImpliedTicket();
54+
55+ expect(impliedPoint).toBe('~sampel-palnet');
56+ expect(impliedTicket).toEqual('~widweb-tipfep-sabnux-nispec');
57+ });
58+ });
59+});
src/lib/useImpliedTicket.tsnew file+37
@@ -0,0 +1,37 @@
1+import ob from 'urbit-ob';
2+import { ensurePatFormat, ensureSigPrefix } from 'form/formatters';
3+
4+/**
5+ * pull the implied ticket and point from the #hash in the url
6+ */
7+// e.g., https://bridge.urbit.org/#widweb-tipfep-sabnux-nispec-sampel-palnet
8+
9+// Example tickets:
10+// ~widweb-tipfep-sabnux-nispec
11+
12+// Example tickets + points (Activation Flow):
13+// ~widweb-tipfep-sabnux-nispec-sampel
14+// ~widweb-tipfep-sabnux-nispec-sampel-palnet
15+const VALID_TICKET_REGEX = /^~(?:[[a-z]{6}-){3}[a-z]{6}/;
16+const VALID_TICKET_AND_POINT_REGEX = /^~([a-z]{6}-){4}([a-z-]{6,13})/;
17+const CAPTURE_TICKET_REGEX = /^~((?:[[a-z]{6}-){3}[a-z]{6})/;
18+const CAPTURE_POINT_REGEX = /^~(?:(?:[[a-z]{6}-){3}[a-z]{6}-{1})([a-z-]{6,13})/;
19+
20+export default function useImpliedTicket() {
21+ const hash = window.location.hash.slice(1);
22+ const pat = ensurePatFormat(hash);
23+ const impliedTicket = VALID_TICKET_REGEX.test(pat)
24+ ? pat.match(CAPTURE_TICKET_REGEX)![0]
25+ : null;
26+
27+ const impliedPoint = VALID_TICKET_AND_POINT_REGEX.test(pat)
28+ ? ensureSigPrefix(pat.match(CAPTURE_POINT_REGEX)![1])
29+ : null;
30+
31+ return {
32+ impliedPoint:
33+ impliedPoint && ob.isValidPatq(impliedPoint) ? impliedPoint : null,
34+ impliedTicket:
35+ impliedTicket && ob.isValidPatq(impliedTicket) ? impliedTicket : null,
36+ };
37+}
src/lib/walletgen.js+3−7
@@ -83,13 +83,9 @@ export const generateWallet = async (point, ticket, boot) => {
83 return wallet;
83 return wallet;
84};
84};
85
85
86−export const generateOwnershipWallet = (ship, ticket) =>
86+export const generateOwnershipWallet = async (ship, ticket) => {
87− kg.generateOwnershipWallet({ ship, ticket });
87+ return kg.generateOwnershipWallet({ ship, ticket });
88−
88+};
89−// temporary wallets need to be derivable from just the ticket,
90−// so we always use ~zod as the wallet point.
91−export const generateTemporaryOwnershipWallet = ticket =>
92− generateOwnershipWallet(ZOD, ticket);
93
89
94export const generateTemporaryTicketAndWallet = async point => {
90export const generateTemporaryTicketAndWallet = async point => {
95 const ticket = await makeTicket(point);
91 const ticket = await makeTicket(point);
src/views/Activate/ActivateCode.js+42−26
@@ -1,12 +1,11 @@
1−import React, { useCallback, useMemo, useRef } from 'react';
1+import { useCallback, useMemo, useRef } from 'react';
2−import { Just, Nothing } from 'folktale/maybe';
2+import { Just } from 'folktale/maybe';
3import * as azimuth from 'azimuth-js';
3import * as azimuth from 'azimuth-js';
4import { Grid, H4, P, CheckboxInput } from 'indigo-react';
4import { Grid, H4, P, CheckboxInput } from 'indigo-react';
5import { FORM_ERROR } from 'final-form';
5import { FORM_ERROR } from 'final-form';
6
6
7import View from 'components/View';
7import View from 'components/View';
8import { ForwardButton } from 'components/Buttons';
8import { ForwardButton } from 'components/Buttons';
9−import Passport from 'components/Passport';
10import WarningBox from 'components/WarningBox';
9import WarningBox from 'components/WarningBox';
11import FooterButton from 'components/FooterButton';
10import FooterButton from 'components/FooterButton';
12
11
@@ -18,7 +17,7 @@ import { ROUTE_NAMES } from 'lib/routeNames';
18import { walletFromMnemonic } from 'lib/wallet';
17import { walletFromMnemonic } from 'lib/wallet';
19import { DEFAULT_HD_PATH } from 'lib/constants';
18import { DEFAULT_HD_PATH } from 'lib/constants';
20import { generateWallet } from 'lib/invite';
19import { generateWallet } from 'lib/invite';
21−import { generateTemporaryOwnershipWallet } from 'lib/walletgen';
20+import { generateOwnershipWallet } from 'lib/walletgen';
22import { useLocalRouter } from 'lib/LocalRouter';
21import { useLocalRouter } from 'lib/LocalRouter';
23import useImpliedTicket from 'lib/useImpliedTicket';
22import useImpliedTicket from 'lib/useImpliedTicket';
24import { timeout } from 'lib/timeout';
23import { timeout } from 'lib/timeout';
@@ -39,20 +38,20 @@ import FormError from 'form/FormError';
39import { useActivateFlow } from './ActivateFlow';
38import { useActivateFlow } from './ActivateFlow';
40import { WARNING } from 'form/helpers';
39import { WARNING } from 'form/helpers';
41import { convertToInt } from 'lib/convertToInt';
40import { convertToInt } from 'lib/convertToInt';
41+import { patp2dec } from 'lib/patp2dec';
42
42
43export default function ActivateCode() {
43export default function ActivateCode() {
44 const history = useHistory();
44 const history = useHistory();
45 const { names, push } = useLocalRouter();
45 const { names, push } = useLocalRouter();
46 const { contracts } = useNetwork();
46 const { contracts } = useNetwork();
47− const impliedTicket = useImpliedTicket();
47+ const { impliedPoint, impliedTicket } = useImpliedTicket();
48 const [hasDisclaimed] = useHasDisclaimed();
48 const [hasDisclaimed] = useHasDisclaimed();
49 const didWarn = useRef(false);
49 const didWarn = useRef(false);
50
50
51 const {
51 const {
52+ setDerivedPoint,
52 setDerivedWallet,
53 setDerivedWallet,
53 setInviteWallet,
54 setInviteWallet,
54− derivedPoint,
55− setDerivedPoint,
56 } = useActivateFlow();
55 } = useActivateFlow();
57 // this is a pretty naive way to detect if we're on a mobile device
56 // 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)
57 // (i.e. we're checking the width of the screen)
@@ -90,18 +89,23 @@ export default function ActivateCode() {
90 [validateForm]
89 [validateForm]
91 );
90 );
92
91
93− // set our state on submission
92+ // derive and set our state on submission
94 const onSubmit = useCallback(
93 const onSubmit = useCallback(
95− async values => {
94+ async _values => {
96 await timeout(16); // allow the ui changes to flush before we lag it out
95 await timeout(16); // allow the ui changes to flush before we lag it out
97
96
97+ // TODO: fallback logic to first use implied values if present, otherwise form values?
98+ // Derive wallet
99+ const impliedAzimuthPoint = patp2dec(impliedPoint);
98 const _contracts = need.contracts(contracts);
100 const _contracts = need.contracts(contracts);
99− const { seed } = await generateTemporaryOwnershipWallet(values.ticket);
101+ const { seed } = await generateOwnershipWallet(
100−
102+ impliedAzimuthPoint,
103+ impliedTicket
104+ );
101 const inviteWallet = walletFromMnemonic(seed, DEFAULT_HD_PATH);
105 const inviteWallet = walletFromMnemonic(seed, DEFAULT_HD_PATH);
102−
103 const _inviteWallet = need.wallet(inviteWallet);
106 const _inviteWallet = need.wallet(inviteWallet);
104
107
108+ // Load associated points
105 const owned = await azimuth.azimuth.getOwnedPoints(
109 const owned = await azimuth.azimuth.getOwnedPoints(
106 _contracts,
110 _contracts,
107 _inviteWallet.address
111 _inviteWallet.address
@@ -110,11 +114,16 @@ export default function ActivateCode() {
110 _contracts,
114 _contracts,
111 _inviteWallet.address
115 _inviteWallet.address
112 );
116 );
117+
113 const incoming = [...owned, ...transferring];
118 const incoming = [...owned, ...transferring];
114
119
120+ setInviteWallet(inviteWallet);
121+
122+ // Set derived
115 if (incoming.length > 0) {
123 if (incoming.length > 0) {
116 if (incoming.length > 1 && !didWarn.current) {
124 if (incoming.length > 1 && !didWarn.current) {
117 didWarn.current = true;
125 didWarn.current = true;
126+
118 return {
127 return {
119 [WARNING]:
128 [WARNING]:
120 'This invite code has multiple points available. ' +
129 'This invite code has multiple points available. ' +
@@ -126,7 +135,6 @@ export default function ActivateCode() {
126 const point = convertToInt(incoming[0], 10);
135 const point = convertToInt(incoming[0], 10);
127
136
128 setDerivedPoint(Just(point));
137 setDerivedPoint(Just(point));
129− setInviteWallet(inviteWallet);
130 setDerivedWallet(Just(await generateWallet(point, true)));
138 setDerivedWallet(Just(await generateWallet(point, true)));
131 } else {
139 } else {
132 return {
140 return {
@@ -136,7 +144,14 @@ export default function ActivateCode() {
136 };
144 };
137 }
145 }
138 },
146 },
147+ [
148+ contracts,
149+ impliedPoint,
150+ impliedTicket,
151+ setDerivedPoint,
139− [contracts, setDerivedPoint, setDerivedWallet, setInviteWallet]
152+ setDerivedWallet,
153+ setInviteWallet,
154+ ]
140 );
155 );
141
156
142 const initialValues = useMemo(
157 const initialValues = useMemo(
@@ -147,19 +162,20 @@ export default function ActivateCode() {
147 return (
162 return (
148 <View inset>
163 <View inset>
149 <Grid>
164 <Grid>
150− <Grid.Item
151− full
152− as={Passport}
153− point={derivedPoint}
154− address={Nothing()}
155− />
156 <Grid.Item full as={H4} className="mt3">
165 <Grid.Item full as={H4} className="mt3">
157− Activate
166+ Welcome. This is your Urbit.
158− </Grid.Item>
159− <Grid.Item full as={P} className="mb2">
160− Someone has invited you to claim your Urbit identity and join the
161− network. {!impliedTicket && 'Enter your activation code to continue.'}
162 </Grid.Item>
167 </Grid.Item>
168+ {!impliedTicket && (
169+ <Grid.Item full as={P} className="mb2">
170+ Enter your activation code to continue.
171+ </Grid.Item>
172+ )}
173+
174+ {impliedPoint && (
175+ <Grid.Item full as={P} className="mb2">
176+ {impliedPoint}
177+ </Grid.Item>
178+ )}
163 <BridgeForm
179 <BridgeForm
164 validate={validate}
180 validate={validate}
165 onSubmit={onSubmit}
181 onSubmit={onSubmit}
@@ -201,7 +217,7 @@ export default function ActivateCode() {
201 ? 'Generating...'
217 ? 'Generating...'
202 : isWarning
218 : isWarning
203 ? 'Continue Activation'
219 ? 'Continue Activation'
204− : 'Go'
220+ : 'Claim'
205 }
221 }
206 </Grid.Item>
222 </Grid.Item>
207
223
src/views/Activate/useActivateFlowState.js+11−1
@@ -6,13 +6,21 @@ export default function useActivateFlowState() {
6 const [inviteWallet, setInviteWallet] = useState(Nothing());
6 const [inviteWallet, setInviteWallet] = useState(Nothing());
7 const [derivedPoint, setDerivedPoint] = useState(Nothing());
7 const [derivedPoint, setDerivedPoint] = useState(Nothing());
8 const [generated, setGenerated] = useState(false);
8 const [generated, setGenerated] = useState(false);
9+ const [incomingPoints, setIncomingPoints] = useState(Nothing());
9
10
10 const reset = useCallback(() => {
11 const reset = useCallback(() => {
11 setDerivedWallet(Nothing());
12 setDerivedWallet(Nothing());
12 setInviteWallet(Nothing());
13 setInviteWallet(Nothing());
13 setDerivedPoint(Nothing());
14 setDerivedPoint(Nothing());
14 setGenerated(false);
15 setGenerated(false);
16+ setIncomingPoints(Nothing());
17+ }, [
15− }, [setDerivedWallet, setInviteWallet, setDerivedPoint, setGenerated]);
18+ setDerivedWallet,
19+ setInviteWallet,
20+ setDerivedPoint,
21+ setGenerated,
22+ setIncomingPoints,
23+ ]);
16
24
17 return {
25 return {
18 derivedWallet,
26 derivedWallet,
@@ -23,6 +31,8 @@ export default function useActivateFlowState() {
23 setDerivedPoint,
31 setDerivedPoint,
24 generated,
32 generated,
25 setGenerated,
33 setGenerated,
34+ incomingPoints,
35+ setIncomingPoints,
26 reset,
36 reset,
27 };
37 };
28}
38}