on master help sign in
WIP: Derive L1 & L2 points via Roller dominion

- also UI foundation: layout and components

commitdc733d561f4fa6ff4ff85bab2e5780e9b33aee9c authortomholford <tomholford@users.noreply.github.com> dateFri 3 Sep 2021, 00:26 (5 years ago) parent333d26f
14 files changed+487−263
DEVELOPMENT.md+5−6 src/lib/constants.ts+16 src/lib/useImpliedTicket.test.ts+28−10 src/lib/useImpliedTicket.ts+6−3 src/lib/useRoller.ts+1−1 src/lib/wallet.ts+4−4 src/lib/walletgen.js+5 src/style/index.scss+3 src/style/views/activate.scss+15 src/views/Activate/ActivateCode.js−239 src/views/Activate/ActivateCode.tsx+303 src/views/Activate/ActivateHeader.tsx+24 src/views/Activate/ActivateView.tsx+35 src/views/Activate/PointPresenter.tsx+42
DEVELOPMENT.md+5−6
@@ -158,14 +158,13 @@ To work with the L2 aggregator locally, set up the local environment like so:
158158
159159```sh
160160# Start ganache and apply the yu/naive-fallback branch from urbit/azimuth
161−ganache-cli --host '0.0.0.0' -m 'benefit crew supreme gesture quantum web media hazard theory mercy wing kitten'
162−truffle migrate with-state --reset --compile-all
161+npm run pilot-l2
163162
164163# Build urbit at naive/fake-txs branch, start a local fake ship, then run these in the dojo:
165−:azimuth|watch 'http://0.0.0.0:8545'
166−|start %aggregator
167−|start %aggregator-rpc
164+:azimuth|watch 'http://0.0.0.0:8545' %local
165+|start %roller
166+|start %roller-rpc
168167
169168# If having CORS issues with API requests, try this:
170−> |cors-approve ~~https~3a.~2f.~2f.localhost~3a.3000
169+> |cors-approve ~~http~3a.~2f.~2f.localhost~3a.3000
171170```
src/lib/constants.ts+16
@@ -106,6 +106,20 @@ const ROLLER_HOSTS = {
106106 MAINNET: 'roller.urbit.org',
107107};
108108
109+const POINT_DOMINIONS = {
110+ L1: 'l1',
111+ L2: 'l2',
112+ SPAWN: 'spawn',
113+};
114+
115+const POINT_PROXIES = {
116+ OWN: 'own',
117+ MANAGE: 'manage',
118+ TRANSFER: 'transfer',
119+ VOTE: 'vote',
120+ SPAWN: 'spawn',
121+}
122+
109123export {
110124 CHECK_BLOCK_EVERY_MS,
111125 DEFAULT_GAS_PRICE_GWEI,
@@ -134,4 +148,6 @@ export {
134148 ZOD,
135149 PROGRESS_ANIMATION_DELAY_MS,
136150 ROLLER_HOSTS,
151+ POINT_DOMINIONS,
152+ POINT_PROXIES,
137153};
src/lib/useImpliedTicket.test.ts+28−10
@@ -16,7 +16,13 @@ describe('#useImpliedTicket', () => {
1616 const input = '';
1717 window.location = { hash: input };
1818
19− const { impliedTicket } = useImpliedTicket();
19+ const {
20+ impliedAzimuthPoint,
21+ impliedPatp,
22+ impliedTicket,
23+ } = useImpliedTicket();
24+ expect(impliedAzimuthPoint).toEqual(null);
25+ expect(impliedPatp).toEqual(null);
2026 expect(impliedTicket).toEqual(null);
2127 });
2228 });
@@ -26,9 +32,13 @@ describe('#useImpliedTicket', () => {
2632 const input = '#widweb-tipfep-sabnux-nispec';
2733 window.location = { hash: input };
2834
29− const { impliedPoint, impliedTicket } = useImpliedTicket();
30−
31− expect(impliedPoint).toBe(null);
35+ const {
36+ impliedAzimuthPoint,
37+ impliedPatp,
38+ impliedTicket,
39+ } = useImpliedTicket();
40+ expect(impliedAzimuthPoint).toEqual(null);
41+ expect(impliedPatp).toEqual(null);
3242 expect(impliedTicket).toEqual('~widweb-tipfep-sabnux-nispec');
3343 });
3444 });
@@ -38,9 +48,13 @@ describe('#useImpliedTicket', () => {
3848 const input = '#widweb-tipfep-sabnux-nispec-sampel';
3949 window.location = { hash: input };
4050
41− const { impliedPoint, impliedTicket } = useImpliedTicket();
42−
43− expect(impliedPoint).toBe('~sampel');
51+ const {
52+ impliedAzimuthPoint,
53+ impliedPatp,
54+ impliedTicket,
55+ } = useImpliedTicket();
56+ expect(impliedAzimuthPoint).toEqual(1135);
57+ expect(impliedPatp).toBe('~sampel');
4458 expect(impliedTicket).toEqual('~widweb-tipfep-sabnux-nispec');
4559 });
4660 });
@@ -50,9 +64,13 @@ describe('#useImpliedTicket', () => {
5064 const input = '#widweb-tipfep-sabnux-nispec-sampel-palnet';
5165 window.location = { hash: input };
5266
53− const { impliedPoint, impliedTicket } = useImpliedTicket();
54−
55− expect(impliedPoint).toBe('~sampel-palnet');
67+ const {
68+ impliedAzimuthPoint,
69+ impliedPatp,
70+ impliedTicket,
71+ } = useImpliedTicket();
72+ expect(impliedAzimuthPoint).toEqual(1624961343);
73+ expect(impliedPatp).toBe('~sampel-palnet');
5674 expect(impliedTicket).toEqual('~widweb-tipfep-sabnux-nispec');
5775 });
5876 });
src/lib/useImpliedTicket.ts+6−3
@@ -1,5 +1,6 @@
11import ob from 'urbit-ob';
22import { ensurePatFormat, ensureSigPrefix } from 'form/formatters';
3+import { patp2dec } from './patp2dec';
34
45/**
56 * pull the implied ticket and point from the #hash in the url
@@ -24,13 +25,15 @@ export default function useImpliedTicket() {
2425 ? pat.match(CAPTURE_TICKET_REGEX)![0]
2526 : null;
2627
27− const impliedPoint = VALID_TICKET_AND_POINT_REGEX.test(pat)
28+ const impliedPatp = VALID_TICKET_AND_POINT_REGEX.test(pat)
2829 ? ensureSigPrefix(pat.match(CAPTURE_POINT_REGEX)![1])
2930 : null;
3031
3132 return {
32− impliedPoint:
33− impliedPoint && ob.isValidPatq(impliedPoint) ? impliedPoint : null,
33+ impliedAzimuthPoint:
34+ impliedPatp && ob.isValidPatq(impliedPatp) ? patp2dec(impliedPatp) : null,
35+ impliedPatp:
36+ impliedPatp && ob.isValidPatq(impliedPatp) ? impliedPatp : null,
3437 impliedTicket:
3538 impliedTicket && ob.isValidPatq(impliedTicket) ? impliedTicket : null,
3639 };
src/lib/useRoller.ts+1−1
@@ -314,7 +314,7 @@ export default function useRoller() {
314314 const points: Ship[] =
315315 proxy === 'own'
316316 ? await api.getOwnedPoints(address)
317− : proxy === 'mange'
317+ : proxy === 'manage'
318318 ? await api.getManagerFor(address)
319319 : proxy === 'vote'
320320 ? await api.getVotingFor(address)
src/lib/wallet.ts+4−4
@@ -21,10 +21,10 @@ export const urbitWalletFromTicket = async (ticket, point, passphrase) => {
2121};
2222
2323export const walletFromMnemonic = (
24− mnemonic,
25− hdpath,
26− passphrase,
27− skipMnemonicCheck
24+ mnemonic: string,
25+ hdpath: string,
26+ passphrase?: string,
27+ skipMnemonicCheck?: boolean
2828) => {
2929 const seed =
3030 skipMnemonicCheck || bip39.validateMnemonic(mnemonic)
src/lib/walletgen.js+5
@@ -87,6 +87,11 @@ export const generateOwnershipWallet = async (ship, ticket) => {
8787 return kg.generateOwnershipWallet({ ship, ticket });
8888};
8989
90+// temporary wallets need to be derivable from just the ticket,
91+// so we always use ~zod as the wallet point.
92+export const generateTemporaryOwnershipWallet = ticket =>
93+ generateOwnershipWallet(ZOD, ticket);
94+
9095export const generateTemporaryTicketAndWallet = async point => {
9196 const ticket = await makeTicket(point);
9297 const owner = await generateOwnershipWallet(ZOD, ticket);
src/style/index.scss+3
@@ -12,3 +12,6 @@
1212
1313/* Bridge Styles */
1414@import './bridge.scss';
15+
16+/* View Styles */
17+@import './views/activate.scss';
src/style/views/activate.scssnew file+15
@@ -0,0 +1,15 @@
1+div.activate-view {
2+ border: 1px solid rgb(204, 204, 204);
3+ border-radius: 5px;
4+ padding: 10px;
5+ min-width: 600px;
6+ min-height: 500px;
7+
8+ div.activate-view-inner {
9+ height: 100%;
10+ }
11+
12+ .mono {
13+ font-family: 'Source Code Pro';
14+ }
15+}
src/views/Activate/ActivateCode.jsdeleted−239
@@ -1,239 +0,0 @@
1−import { useCallback, useMemo, useRef } from 'react';
2−import { Just } from 'folktale/maybe';
3−import * as azimuth from 'azimuth-js';
4−import { Grid, H4, P, CheckboxInput } from 'indigo-react';
5−import { FORM_ERROR } from 'final-form';
6−
7−import View from 'components/View';
8−import { ForwardButton } from 'components/Buttons';
9−import WarningBox from 'components/WarningBox';
10−import FooterButton from 'components/FooterButton';
11−
12−import { useNetwork } from 'store/network';
13−import { useHistory } from 'store/history';
14−
15−import * as need from 'lib/need';
16−import { ROUTE_NAMES } from 'lib/routeNames';
17−import { walletFromMnemonic } from 'lib/wallet';
18−import { DEFAULT_HD_PATH } from 'lib/constants';
19−import { generateWallet } from 'lib/invite';
20−import { generateOwnershipWallet } from 'lib/walletgen';
21−import { useLocalRouter } from 'lib/LocalRouter';
22−import useImpliedTicket from 'lib/useImpliedTicket';
23−import { timeout } from 'lib/timeout';
24−import useHasDisclaimed from 'lib/useHasDisclaimed';
25−import useBreakpoints from 'lib/useBreakpoints';
26−
27−import BridgeForm from 'form/BridgeForm';
28−import SubmitButton from 'form/SubmitButton';
29−import { TicketInput } from 'form/Inputs';
30−import {
31− composeValidator,
32− buildPatqValidator,
33− hasErrors,
34− buildCheckboxValidator,
35−} from 'form/validators';
36−import FormError from 'form/FormError';
37−
38−import { useActivateFlow } from './ActivateFlow';
39−import { WARNING } from 'form/helpers';
40−import { convertToInt } from 'lib/convertToInt';
41−import { patp2dec } from 'lib/patp2dec';
42−
43−export default function ActivateCode() {
44− const history = useHistory();
45− const { names, push } = useLocalRouter();
46− const { contracts } = useNetwork();
47− const { impliedPoint, impliedTicket } = useImpliedTicket();
48− const [hasDisclaimed] = useHasDisclaimed();
49− const didWarn = useRef(false);
50−
51− const {
52− setDerivedPoint,
53− setDerivedWallet,
54− setInviteWallet,
55− } = useActivateFlow();
56− // this is a pretty naive way to detect if we're on a mobile device
57− // (i.e. we're checking the width of the screen)
58− // but it will suffice for the 99% case and if someone wants to get around it
59− // well by golly they're allowed to turn their phone into landscape mode
60− // for this screen
61− const activationAllowed = useBreakpoints([false, true, true]);
62−
63− const goToLogin = useCallback(() => history.popAndPush(ROUTE_NAMES.LOGIN), [
64− history,
65− ]);
66−
67− const goToPassport = useCallback(() => {
68− if (!hasDisclaimed) {
69− push(names.DISCLAIMER, { next: names.PASSPORT });
70− } else {
71− push(names.PASSPORT);
72− }
73− }, [hasDisclaimed, names.DISCLAIMER, names.PASSPORT, push]);
74−
75− const validateForm = useCallback((values, errors) => {
76− didWarn.current = false;
77−
78− if (hasErrors(errors)) {
79− return errors;
80− }
81− }, []);
82−
83− const validate = useMemo(
84− () =>
85− composeValidator(
86− { ticket: buildPatqValidator(), showTicket: buildCheckboxValidator() },
87− validateForm
88− ),
89− [validateForm]
90− );
91−
92− // derive and set our state on submission
93− const onSubmit = useCallback(
94− async _values => {
95− await timeout(16); // allow the ui changes to flush before we lag it out
96−
97− // TODO: fallback logic to first use implied values if present, otherwise form values?
98− // Derive wallet
99− const impliedAzimuthPoint = patp2dec(impliedPoint);
100− const _contracts = need.contracts(contracts);
101− const { seed } = await generateOwnershipWallet(
102− impliedAzimuthPoint,
103− impliedTicket
104− );
105− const inviteWallet = walletFromMnemonic(seed, DEFAULT_HD_PATH);
106− const _inviteWallet = need.wallet(inviteWallet);
107−
108− // Load associated points
109− const owned = await azimuth.azimuth.getOwnedPoints(
110− _contracts,
111− _inviteWallet.address
112− );
113− const transferring = await azimuth.azimuth.getTransferringFor(
114− _contracts,
115− _inviteWallet.address
116− );
117−
118− const incoming = [...owned, ...transferring];
119−
120− setInviteWallet(inviteWallet);
121−
122− // Set derived
123− if (incoming.length > 0) {
124− if (incoming.length > 1 && !didWarn.current) {
125− didWarn.current = true;
126−
127− return {
128− [WARNING]:
129− 'This invite code has multiple points available. ' +
130− "Once you've activated this point, " +
131− 'activate the next with the same process.',
132− };
133− }
134−
135− const point = convertToInt(incoming[0], 10);
136−
137− setDerivedPoint(Just(point));
138− setDerivedWallet(Just(await generateWallet(point, true)));
139− } else {
140− return {
141− [FORM_ERROR]:
142− 'Invite code has no claimable point.\n' +
143− 'Check your invite code and try again?',
144− };
145− }
146− },
147− [
148− contracts,
149− impliedPoint,
150− impliedTicket,
151− setDerivedPoint,
152− setDerivedWallet,
153− setInviteWallet,
154− ]
155− );
156−
157− const initialValues = useMemo(
158− () => ({ ticket: impliedTicket || '', showTicket: true }),
159− [impliedTicket]
160− );
161−
162− return (
163− <View inset>
164− <Grid>
165− <Grid.Item full as={H4} className="mt3">
166− Welcome. This is your Urbit.
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− )}
179− <BridgeForm
180− validate={validate}
181− onSubmit={onSubmit}
182− afterSubmit={goToPassport}
183− initialValues={initialValues}>
184− {({ validating, values, submitting, handleSubmit }) => (
185− <>
186− {!impliedTicket && (
187− <>
188− <Grid.Item
189− full
190− as={TicketInput}
191− type={values.showTicket ? 'text' : 'password'}
192− name="ticket"
193− label="Activation Code"
194− disabled={!activationAllowed}
195− />
196−
197− <Grid.Item
198− full
199− as={CheckboxInput}
200− name="showTicket"
201− label="Show"
202− />
203− </>
204− )}
205−
206− <Grid.Item full as={FormError} />
207−
208− <Grid.Item
209− full
210− as={SubmitButton}
211− className="mt4"
212− handleSubmit={handleSubmit}>
213− {isWarning =>
214− validating
215− ? 'Deriving...'
216− : submitting
217− ? 'Generating...'
218− : isWarning
219− ? 'Continue Activation'
220− : 'Claim'
221− }
222− </Grid.Item>
223−
224− {!activationAllowed && (
225− <Grid.Item full as={WarningBox} className="mt4">
226− For your security, please access Bridge on a desktop device.
227− </Grid.Item>
228− )}
229− </>
230− )}
231− </BridgeForm>
232− </Grid>
233−
234− <FooterButton as={ForwardButton} onClick={goToLogin}>
235− Login
236− </FooterButton>
237− </View>
238− );
239−}
src/views/Activate/ActivateCode.tsxnew file+303
@@ -0,0 +1,303 @@
1+import { useCallback, useEffect, useMemo, useRef } from 'react';
2+import { Just } from 'folktale/maybe';
3+import * as azimuth from 'azimuth-js';
4+import { Grid, H4, P, CheckboxInput } from 'indigo-react';
5+import { FORM_ERROR } from 'final-form';
6+
7+import View from 'components/View';
8+import { ForwardButton } from 'components/Buttons';
9+import WarningBox from 'components/WarningBox';
10+import FooterButton from 'components/FooterButton';
11+import ActivateView from './ActivateView';
12+
13+import { useNetwork } from 'store/network';
14+import { useHistory } from 'store/history';
15+
16+import * as need from 'lib/need';
17+import { ROUTE_NAMES } from 'lib/routeNames';
18+import { walletFromMnemonic } from 'lib/wallet';
19+import { DEFAULT_HD_PATH, POINT_DOMINIONS, POINT_PROXIES } from 'lib/constants';
20+import { generateWallet } from 'lib/invite';
21+import { generateTemporaryOwnershipWallet } from 'lib/walletgen';
22+import { useLocalRouter } from 'lib/LocalRouter';
23+import useImpliedTicket from 'lib/useImpliedTicket';
24+import { timeout } from 'lib/timeout';
25+import useHasDisclaimed from 'lib/useHasDisclaimed';
26+import useBreakpoints from 'lib/useBreakpoints';
27+
28+import BridgeForm from 'form/BridgeForm';
29+import SubmitButton from 'form/SubmitButton';
30+import { TicketInput } from 'form/Inputs';
31+import {
32+ composeValidator,
33+ buildPatqValidator,
34+ hasErrors,
35+ buildCheckboxValidator,
36+} from 'form/validators';
37+import FormError from 'form/FormError';
38+
39+import { useActivateFlow } from './ActivateFlow';
40+import { WARNING } from 'form/helpers';
41+import { convertToInt } from 'lib/convertToInt';
42+import PointPresenter from './PointPresenter';
43+import useRoller from 'lib/useRoller';
44+import { Ship } from '@urbit/roller-api';
45+import { Box, Text } from '@tlon/indigo-react';
46+import ActivateHeader from './ActivateHeader';
47+
48+export default function ActivateCode() {
49+ const history = useHistory();
50+ const { names, push } = useLocalRouter();
51+ const { contracts } = useNetwork();
52+ const {
53+ impliedAzimuthPoint,
54+ impliedPatp,
55+ impliedTicket,
56+ } = useImpliedTicket();
57+ const [hasDisclaimed] = useHasDisclaimed();
58+ const didWarn = useRef(false);
59+ const { api, getPoints } = useRoller();
60+
61+ const {
62+ setDerivedPoint,
63+ setDerivedWallet,
64+ setInviteWallet,
65+ } = useActivateFlow();
66+ // this is a pretty naive way to detect if we're on a mobile device
67+ // (i.e. we're checking the width of the screen)
68+ // but it will suffice for the 99% case and if someone wants to get around it
69+ // well by golly they're allowed to turn their phone into landscape mode
70+ // for this screen
71+ const activationAllowed = useBreakpoints([false, true, true]);
72+
73+ const goToLogin = useCallback(() => history.popAndPush(ROUTE_NAMES.LOGIN), [
74+ history,
75+ ]);
76+
77+ const goToPassport = useCallback(() => {
78+ if (!hasDisclaimed) {
79+ push(names.DISCLAIMER, { next: names.PASSPORT });
80+ } else {
81+ push(names.PASSPORT);
82+ }
83+ }, [hasDisclaimed, names.DISCLAIMER, names.PASSPORT, push]);
84+
85+ const validateForm = useCallback((values, errors) => {
86+ didWarn.current = false;
87+
88+ if (hasErrors(errors)) {
89+ return errors;
90+ }
91+ }, []);
92+
93+ const validate = useMemo(
94+ () =>
95+ composeValidator(
96+ { ticket: buildPatqValidator(), showTicket: buildCheckboxValidator() },
97+ validateForm
98+ ),
99+ [validateForm]
100+ );
101+
102+ const loadL1Points = useCallback(
103+ async (address: string): Promise<number[]> => {
104+ const _contracts = need.contracts(contracts);
105+ const owned = await azimuth.azimuth.getOwnedPoints(_contracts, address);
106+ const transferring = await azimuth.azimuth.getTransferringFor(
107+ _contracts,
108+ address
109+ );
110+ const incoming = [...owned, ...transferring];
111+ console.log('incoming L1 points', incoming);
112+ return incoming;
113+ },
114+ [contracts]
115+ );
116+
117+ const loadl2points = useCallback(
118+ async (address: string): Promise<Ship[]> => {
119+ const owned = await getPoints(POINT_PROXIES.OWN, address);
120+ const transferring = await getPoints(POINT_PROXIES.TRANSFER, address);
121+ const incoming = [...owned, ...transferring];
122+ console.log('incoming L2 points', incoming);
123+ return incoming;
124+ },
125+ [getPoints]
126+ );
127+
128+ // derive and set our state on submission
129+ const onSubmit = useCallback(
130+ async values => {
131+ await timeout(16); // allow the ui changes to flush before we lag it out
132+
133+ // Derive wallet
134+ // TODO: fallback logic to first use implied values if present, otherwise form values?
135+ const ticket = impliedTicket || values.ticket;
136+ const { seed } = await generateTemporaryOwnershipWallet(ticket);
137+ const inviteWallet = walletFromMnemonic(seed, DEFAULT_HD_PATH);
138+ setInviteWallet(inviteWallet);
139+ const _inviteWallet = need.wallet(inviteWallet);
140+ const inviteAddress = _inviteWallet.address;
141+
142+ // Query for points depending on point's dominion
143+ const rollerPoint = await api.getPoint(impliedAzimuthPoint!);
144+ let incoming: number[] | Ship[];
145+ if (rollerPoint.dominion === POINT_DOMINIONS.L2) {
146+ incoming = await loadl2points(inviteAddress);
147+ } else {
148+ incoming = await loadL1Points(inviteAddress);
149+ }
150+
151+ // Set derived
152+ if (incoming.length > 0) {
153+ if (incoming.length > 1 && !didWarn.current) {
154+ didWarn.current = true;
155+
156+ return {
157+ [WARNING]:
158+ 'This invite code has multiple points available. ' +
159+ "Once you've activated this point, " +
160+ 'activate the next with the same process.',
161+ };
162+ }
163+
164+ const point = convertToInt(incoming[0], 10);
165+ console.log('submitting point', point);
166+
167+ setDerivedPoint(Just(point));
168+ setDerivedWallet(Just(await generateWallet(point, true)));
169+ } else {
170+ return {
171+ [FORM_ERROR]:
172+ 'Invite code has no claimable point.\n' +
173+ 'Check your invite code and try again?',
174+ };
175+ }
176+ },
177+ [
178+ api,
179+ impliedAzimuthPoint,
180+ impliedTicket,
181+ loadL1Points,
182+ loadl2points,
183+ setDerivedPoint,
184+ setDerivedWallet,
185+ setInviteWallet,
186+ ]
187+ );
188+
189+ const initialValues = useMemo(
190+ () => ({ ticket: impliedTicket || '', showTicket: true }),
191+ [impliedTicket]
192+ );
193+
194+ useEffect(() => {
195+ const fetchRollerPoint = async () => {
196+ if (!impliedAzimuthPoint) {
197+ return;
198+ }
199+ const p = await api.getPoint(impliedAzimuthPoint!);
200+ console.log(p);
201+ };
202+
203+ fetchRollerPoint();
204+ }, [api, impliedAzimuthPoint]);
205+
206+ const footer = useMemo(() => {
207+ return (
208+ <Box
209+ display="flex"
210+ flexDirection="column"
211+ flexWrap="nowrap"
212+ justifyContent="flex-end">
213+ <Grid>
214+ <BridgeForm
215+ validate={validate}
216+ onSubmit={onSubmit}
217+ afterSubmit={goToPassport}
218+ initialValues={initialValues}>
219+ {({ validating, values, submitting, handleSubmit }) => (
220+ <>
221+ {!impliedTicket && (
222+ <>
223+ <Grid.Item
224+ full
225+ as={TicketInput}
226+ type={values.showTicket ? 'text' : 'password'}
227+ name="ticket"
228+ label="Activation Code"
229+ disabled={!activationAllowed}
230+ />
231+
232+ <Grid.Item
233+ full
234+ as={CheckboxInput}
235+ name="showTicket"
236+ label="Show"
237+ />
238+ </>
239+ )}
240+
241+ <Grid.Item full as={FormError} />
242+
243+ <Grid.Item
244+ full
245+ as={SubmitButton}
246+ className="mt4"
247+ handleSubmit={handleSubmit}>
248+ {isWarning =>
249+ validating
250+ ? 'Deriving...'
251+ : submitting
252+ ? 'Generating...'
253+ : isWarning
254+ ? 'Continue Activation'
255+ : 'Claim'
256+ }
257+ </Grid.Item>
258+
259+ {!activationAllowed && (
260+ <Grid.Item full as={WarningBox} className="mt4">
261+ For your security, please access Bridge on a desktop device.
262+ </Grid.Item>
263+ )}
264+ </>
265+ )}
266+ </BridgeForm>
267+ </Grid>
268+ </Box>
269+ );
270+ }, [
271+ activationAllowed,
272+ goToPassport,
273+ impliedTicket,
274+ initialValues,
275+ onSubmit,
276+ validate,
277+ ]);
278+
279+ return (
280+ <View inset>
281+ <ActivateView
282+ header={<ActivateHeader copy={'Welcome. This is your Urbit.'} />}
283+ footer={footer}>
284+ <Box
285+ alignItems={'center'}
286+ display={'flex'}
287+ flexDirection={'column'}
288+ flexWrap={'nowrap'}
289+ height={'100%'}
290+ justifyContent={'center'}>
291+ {!impliedTicket && (
292+ <Text className="mb2">Enter your activation code to continue.</Text>
293+ )}
294+ {impliedPatp && <PointPresenter patp={impliedPatp} />}
295+ </Box>
296+ </ActivateView>
297+
298+ <FooterButton as={ForwardButton} onClick={goToLogin}>
299+ Login
300+ </FooterButton>
301+ </View>
302+ );
303+}
src/views/Activate/ActivateHeader.tsxnew file+24
@@ -0,0 +1,24 @@
1+import { Box, Text } from '@tlon/indigo-react';
2+
3+type ActivateHeaderProps = {
4+ copy: string;
5+};
6+
7+const ActivateHeader = ({ copy }: ActivateHeaderProps) => {
8+ return (
9+ <Box>
10+ <Text
11+ display="block"
12+ fontFamily="Inter"
13+ fontSize={3}
14+ fontWeight="medium"
15+ mx={3}
16+ my={3}
17+ textAlign="center">
18+ {copy}
19+ </Text>
20+ </Box>
21+ );
22+};
23+
24+export default ActivateHeader;
src/views/Activate/ActivateView.tsxnew file+35
@@ -0,0 +1,35 @@
1+import { Box } from '@tlon/indigo-react';
2+import cn from 'classnames';
3+
4+// import Steps from 'components/Steps';
5+type ActivateViewProps = {
6+ className?: string;
7+ children?: React.ReactNode;
8+ header?: React.ReactNode | string;
9+ footer?: React.ReactNode;
10+ step?: number | null;
11+};
12+
13+export default function ActivateView({
14+ className,
15+ children,
16+ header,
17+ footer,
18+ step = null,
19+}: ActivateViewProps) {
20+ return (
21+ <>
22+ <Box
23+ display="grid"
24+ gridTemplateColumns="1fr"
25+ gridTemplateRows="20% 60% 20%"
26+ gridTemplateAreas={"'header' 'content' 'footer'"}
27+ className={cn(className, 'activate-view')}>
28+ <Box gridArea="header">{header}</Box>
29+ <Box gridArea="content">{children}</Box>
30+ <Box gridArea="footer">{footer ? footer : null}</Box>
31+ </Box>
32+ <Box>{step ? step : null}</Box>
33+ </>
34+ );
35+}
src/views/Activate/PointPresenter.tsxnew file+42
@@ -0,0 +1,42 @@
1+import { Box, Text } from '@tlon/indigo-react';
2+import Sigil from 'components/Sigil';
3+
4+type PointPresenterArgs = {
5+ patp: string;
6+ className?: string;
7+};
8+
9+const PointPresenter = ({ patp, className }: PointPresenterArgs) => {
10+ return patp ? (
11+ <>
12+ <Box
13+ width={128}
14+ height={128}
15+ display="flex"
16+ flexDirection="column"
17+ flexWrap="nowrap"
18+ justifyContent="center"
19+ alignItems="center"
20+ background="white"
21+ padding={32}
22+ className={className}>
23+ <Box background="black" borderRadius={5} padding={32}>
24+ <Sigil
25+ patp={patp}
26+ size={64}
27+ display="block"
28+ colors={['black', 'white']}
29+ icon
30+ padding={32}
31+ borderRadius={5}
32+ />
33+ </Box>
34+ <Text display="block" className={'mono'}>
35+ {patp}
36+ </Text>
37+ </Box>
38+ </>
39+ ) : null;
40+};
41+
42+export default PointPresenter;