on master help sign in
Merge pull request #592 from urbit/m/urbit-os-view-enhancements

ux: Urbit OS view and flow enhancements

commit72f9834b292ffb434341ccf5f4169ffcbf1bf0f2 authorfang <github@fang.io> dateThu 22 Jul 2021, 15:50 (5 years ago) parentc7d32983f50754
10 files changed+186−560
src/components/AlertBox.tsx+20 src/indigo-react/indigo.css+16 src/store/Provider.js−2 src/store/hosting.js−37 src/store/lib/useHostingStore.js−244 src/style/bridge.scss+8 src/types/L1Point.ts+18 src/views/UrbitOS/Home.js−270 src/views/UrbitOS/Home.tsx+110 src/views/UrbitOS/NetworkingKeys.js+14−7
src/components/AlertBox.tsxnew file+20
@@ -0,0 +1,20 @@
1+import React from 'react';
2+import cn from 'classnames';
3+import { Flex, Text } from 'indigo-react';
4+import { Icon } from '@tlon/indigo-react';
5+
6+export default function AlertBox({ className, children }) {
7+ return (
8+ <Flex className={cn('bg-red5 pv3 ph4', className)} align="center">
9+ <span className="circular-icon-bg bg-red6">
10+ <Icon
11+ display="inline-block"
12+ icon="ExclaimationMark"
13+ size="18px"
14+ color={'white'}
15+ />
16+ </span>
17+ <Text className="f6 fw-bolder red5">{children}</Text>
18+ </Flex>
19+ );
20+}
src/indigo-react/indigo.css+16
@@ -512,6 +512,10 @@ caption,
512512.fw-bold {
513513 font-weight: 600; }
514514
515+.fw-bolder {
516+ font-weight: 700;
517+}
518+
515519@media only screen and (min-width: 550px) {
516520 .fw-thin-ns {
517521 font-weight: 200; }
@@ -1015,6 +1019,14 @@ caption,
10151019.bg-blue1 {
10161020 background-color: #ecf6ff; }
10171021
1022+.bg-red6 {
1023+ background-color: #FF4136;
1024+}
1025+
1026+.bg-red5 {
1027+ background-color: #FFC6C3;
1028+}
1029+
10181030.bg-red4 {
10191031 background-color: #c10d30; }
10201032
@@ -1090,6 +1102,10 @@ caption,
10901102.blue1 {
10911103 color: #ecf6ff; }
10921104
1105+.red5 {
1106+ color: #FF4136;
1107+}
1108+
10931109.red4 {
10941110 color: #c10d30; }
10951111
src/store/Provider.js−2
@@ -9,7 +9,6 @@ import { StarReleaseCacheProvider } from './starRelease';
99import { PointCacheProvider } from './pointCache';
1010import { TxnCursorProvider } from './txnCursor';
1111import { MediaQueryProvider } from 'lib/MediaQuery';
12−import { HostingProvider } from './hosting';
1312
1413// nest all of the providers within each other to avoid hella depth
1514export default nest([
@@ -22,5 +21,4 @@ export default nest([
2221 TxnCursorProvider,
2322 MediaQueryProvider,
2423 StarReleaseCacheProvider,
25− HostingProvider,
2624]);
src/store/hosting.jsdeleted−37
@@ -1,37 +0,0 @@
1−import React, { createContext, useContext, forwardRef } from 'react';
2−
3−import useHostingStore from './lib/useHostingStore';
4−
5−import { NETWORK_TYPES } from '../lib/network';
6−
7−import { useNetwork } from 'store/network';
8−
9−export const HostingContext = createContext(null);
10−
11−const HOSTING_DETAILS = {
12− DEV: { url: '', domain: 'liam.tlon.network', disabled: true },
13− MAINNET: { url: '', domain: '', disabled: true },
14−};
15−
16−export function HostingProvider({ children }) {
17− const { networkType } = useNetwork();
18− const { url, domain, disabled } =
19− networkType === NETWORK_TYPES.MAINNET
20− ? HOSTING_DETAILS.MAINNET
21− : HOSTING_DETAILS.DEV;
22− const store = useHostingStore(url, domain, disabled);
23− return (
24− <HostingContext.Provider value={store}>{children}</HostingContext.Provider>
25− );
26−}
27−
28−export function useHosting() {
29− return useContext(HostingContext);
30−}
31−
32−export const withHosting = Component =>
33− forwardRef((props, ref) => (
34− <HostingContext.Consumer>
35− {store => <Component ref={ref} {...store} {...props} />}
36− </HostingContext.Consumer>
37− ));
src/store/lib/useHostingStore.jsdeleted−244
@@ -1,244 +0,0 @@
1−import { useState, useCallback, useMemo } from 'react';
2−import { Nothing } from 'folktale/maybe';
3−import * as ob from 'urbit-ob';
4−
5−import { getVaneName, getVaneNumber } from 'lib/hosting';
6−import SolarisClient from 'lib/SolarisClient';
7−
8−import { usePointCursor } from 'store/pointCursor';
9−
10−const STATE = {
11− UNKNOWN: 'UNKNOWN',
12− MISSING: 'MISSING',
13− RUNNING: 'RUNNING',
14− PENDING: 'PENDING',
15−};
16−
17−function useHostingStore(url, domain, disabled) {
18− const client = new SolarisClient(url);
19−
20− const { pointCursor } = usePointCursor();
21−
22− const [bootProgress, setBootProgress] = useState(0);
23− const [bootMessage, setBootMessage] = useState('Assembling Urbit');
24− const [startTime] = useState(Date.now());
25−
26− const [status, _setStatus] = useState(STATE.UNKNOWN);
27− const setStatus = useCallback(
28− s => {
29− _setStatus(s);
30− },
31− [_setStatus]
32− );
33−
34− const [sysEvents, setSysEvents] = useState([]);
35− const [newEvents, setNewEvents] = useState([]);
36− const [runEvents, setRunEvents] = useState([]);
37−
38− const [error, _setError] = useState();
39− const setError = useCallback(
40− error => {
41− _setError(error);
42− if (error) {
43− setStatus(STATE.UNKNOWN);
44− console.error(error);
45− }
46− },
47− [_setError, setStatus]
48− );
49−
50− const unknown = status === STATE.UNKNOWN;
51− const missing = status === STATE.MISSING;
52− const running = status === STATE.RUNNING;
53− const pending = status === STATE.PENDING;
54−
55− let syncStatus;
56−
57− const resetEvents = useCallback(() => {
58− setSysEvents([]);
59− setNewEvents([]);
60− setRunEvents([]);
61− }, [setSysEvents, setNewEvents, setRunEvents]);
62−
63− const newSource = useCallback(
64− (url, retry = 0) => {
65− let source = new EventSource(url);
66−
67− source.addEventListener('sys', event => {
68− const sysEvent = JSON.parse(event.data);
69− const time = new Date(sysEvent.object.firstTimestamp).getTime();
70− if (time > startTime) {
71− setSysEvents(previous => [...previous, sysEvent]);
72− }
73− });
74−
75− source.addEventListener('new', event => {
76− if (event.data !== '') {
77− setNewEvents(previous => [...previous, event.data]);
78− }
79− if (event.data === '1-b') {
80− setBootMessage('Compiling Hoon');
81− setBootProgress(0.2);
82− } else if (event.data === '1-c') {
83− setBootProgress(0.25);
84− } else if (event.data === '%arvo-assembly') {
85− setBootMessage('Compiling Urbit OS');
86− setBootProgress(0.3);
87− } else if (event.data.startsWith('[%vane %')) {
88− const vane = getVaneName(event.data.slice(8, 9));
89− setBootMessage(`Compiling vane: ${vane}`);
90− setBootProgress(0.3 + 0.05 * getVaneNumber(event.data.slice(8, 9)));
91− } else if (event.data === 'pier: boot complete') {
92− setBootMessage('Ship starting');
93− setBootProgress(0.85);
94− }
95− });
96−
97− source.addEventListener('run', event => {
98− if (event.data === 'goad: recompiling all apps') {
99− setBootProgress(1.0);
100− setBootMessage('Ship Launched');
101− syncStatus();
102− }
103− if (event.data !== '') {
104− setRunEvents(previous => [...previous, event.data]);
105− }
106− });
107−
108− source.addEventListener(
109− 'error',
110− error => {
111− if (source.readyState === EventSource.CLOSED) {
112− if (retry < 5) {
113− newSource(url, retry + 1);
114− return;
115− }
116− setError(error);
117− }
118− },
119− false
120− );
121− },
122− [setError, startTime, syncStatus]
123− );
124−
125− const getEvents = useCallback(() => {
126− try {
127− if (Nothing.hasInstance(pointCursor) || disabled) {
128− return;
129− }
130− const patp = ob.patp(pointCursor.value);
131− setError(undefined);
132−
133− resetEvents();
134−
135− const url = client.getShipsByPatpUrl(patp) + '/events';
136− newSource(url);
137− } catch (error) {
138− setError(error);
139− }
140− }, [setError, disabled, resetEvents, client, pointCursor, newSource]);
141−
142− syncStatus = useCallback(async () => {
143− try {
144− if (Nothing.hasInstance(pointCursor) || disabled) {
145− return;
146− }
147− const patp = ob.patp(pointCursor.value);
148− setError(undefined);
149−
150− resetEvents();
151−
152− const response = await client.getShipsByPatp(patp);
153−
154− if (response.status === 404) {
155− setStatus(STATE.MISSING);
156− } else if (!response.ok) {
157− setError(response.statusText);
158− } else {
159− const ship = await response.json();
160−
161− if (ship.status === 'Pending') {
162− setStatus(STATE.PENDING);
163− getEvents();
164− } else if (ship.status === 'Running') {
165− setStatus(STATE.RUNNING);
166− } else {
167− setError(ship);
168− }
169− }
170− } catch (error) {
171− setError(error);
172− }
173− }, [
174− client,
175− getEvents,
176− pointCursor,
177− resetEvents,
178− setError,
179− setStatus,
180− disabled,
181− ]);
182−
183− const hostedShipUrl = useMemo(
184− () => `http://${ob.patp(pointCursor.getOrElse('')).slice(1)}.${domain}`,
185− [pointCursor, domain]
186− );
187−
188− const create = useCallback(
189− async keyfile => {
190− try {
191− if (Nothing.hasInstance(pointCursor) || disabled) {
192− return;
193− }
194− const patp = ob.patp(pointCursor.value).slice(1);
195− setError(undefined);
196−
197− setStatus(STATE.PENDING);
198− setBootProgress(0.1);
199−
200− const response = await client.postShips({
201− patp,
202− key: keyfile,
203− debug: true,
204− fake: false,
205− });
206−
207− if (!response.ok) {
208− setError(response.statusText);
209− } else {
210− getEvents();
211− }
212− } catch (error) {
213− setError(error);
214− }
215− },
216− [pointCursor, setError, setStatus, client, getEvents, disabled]
217− );
218−
219− return {
220− error,
221− unknown,
222− missing,
223− running,
224− pending,
225− url: hostedShipUrl,
226−
227− sysEvents,
228− newEvents,
229− runEvents,
230−
231− syncStatus,
232− getEvents,
233− create,
234−
235− bootProgress,
236− bootMessage,
237−
238− disabled,
239− // debugging
240− status,
241− };
242−}
243−
244−export default useHostingStore;
src/style/bridge.scss+8
@@ -14,3 +14,11 @@ body {
1414.animated-width {
1515 transition: width 500ms ease-out;
1616}
17+
18+// Wrap a @tlon/indigo-react Icon with a circular background
19+span.circular-icon-bg {
20+ height: 18px;
21+ width: 18px;
22+ margin-right: 8px;
23+ border-radius: 50%;
24+}
src/types/L1Point.tsnew file+18
@@ -0,0 +1,18 @@
1+// Response returned by L1's azimuth#getPoint
2+export type L1Point = {
3+ active: boolean;
4+ authenticationKey: string;
5+ continuityNumber: string;
6+ cryptoSuiteVersion: string;
7+ encryptionKey: string;
8+ escapeRequested: boolean;
9+ escapeRequestedTo: string;
10+ hasSponsor: boolean;
11+ keyRevisionNumber: string;
12+ managementProxy: string;
13+ owner: string;
14+ spawnProxy: string;
15+ sponsor: string;
16+ transferProxy: string;
17+ votingProxy: string;
18+};
src/views/UrbitOS/Home.jsdeleted−270
@@ -1,270 +0,0 @@
1−import React, { useCallback, useState, useEffect, useMemo } from 'react';
2−import cn from 'classnames';
3−import { Grid, Button, SelectInput, Flex } from 'indigo-react';
4−import * as ob from 'urbit-ob';
5−import { azimuth } from 'azimuth-js';
6−
7−import { usePointCursor } from 'store/pointCursor';
8−import { usePointCache } from 'store/pointCache';
9−
10−import useCurrentPointName from 'lib/useCurrentPointName';
11−import useKeyfileGenerator from 'lib/useKeyfileGenerator';
12−import * as need from 'lib/need';
13−
14−import { OutButton, ForwardButton } from 'components/Buttons';
15−import { CopyButtonWide } from 'components/CopyButton';
16−import NetworkingKeys from 'components/NetworkingKeys';
17−import ProgressButton from 'components/ProgressButton';
18−import LoginButton from 'components/LoginButton';
19−
20−import BridgeForm from 'form/BridgeForm';
21−import { useLocalRouter } from 'lib/LocalRouter';
22−import { useHosting } from 'store/hosting';
23−import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
24−import useLifecycle from 'lib/useLifecycle';
25−import useLocalHosting from 'lib/useLocalHosting';
26−
27−export default function UrbitOSHome({ manualNetworkSeed }) {
28− const { pointCursor } = usePointCursor();
29− const { getDetails } = usePointCache();
30−
31− const { push, names } = useLocalRouter();
32−
33− const point = need.point(pointCursor);
34− const details = need.details(getDetails(point));
35−
36− const sponsor = ob.patp(details.sponsor);
37−
38− const [showKeys, setShowKeys] = useState(false);
39−
40− const showSponsor = azimuth.getPointSize(point) !== azimuth.PointSize.Galaxy;
41− const toggleShowKeys = useCallback(() => setShowKeys(s => !s), [setShowKeys]);
42−
43− const goNetworkingKeys = useCallback(() => push(names.NETWORKING_KEYS), [
44− names,
45− push,
46− ]);
47−
48− const goChangeSponsor = useCallback(() => push(names.CHANGE_SPONSOR), [
49− push,
50− names,
51− ]);
52− return (
53− <>
54− <Hosting manualNetworkSeed={manualNetworkSeed} />
55−
56− <Grid>
57− <Grid.Item full className="mv7 f5">
58− Network
59− </Grid.Item>
60− {showSponsor && (
61− <>
62− <Grid.Divider />
63− <Grid.Item
64− full
65− as={ForwardButton}
66− detail="A sponsor finds new peers in your network"
67− accessory={<u>Change</u>}
68− onClick={goChangeSponsor}>
69− <span className="mono">{sponsor}</span>
70− <span className="f7 bg-black white p1 ml2 r4">SPONSOR</span>
71− </Grid.Item>
72− </>
73− )}
74−
75− <Grid.Divider />
76− <Grid.Item full as={ForwardButton} onClick={goNetworkingKeys}>
77− Reset Networking Keys
78− </Grid.Item>
79− <Grid.Divider />
80− <Grid.Item
81− full
82− as={ForwardButton}
83− accessory={showKeys ? '▼' : '▲'}
84− onClick={toggleShowKeys}>
85− View Networking Keys
86− </Grid.Item>
87− {showKeys && <Grid.Item full as={NetworkingKeys} point={point} />}
88− </Grid>
89− </>
90− );
91−}
92−
93−// eslint-disable-next-line
94−function Hosting({ manualNetworkSeed }) {
95− const bind = useKeyfileGenerator(manualNetworkSeed);
96− const { keyfile, code } = bind;
97− const ship = useHosting();
98−
99− const {
100− syncStatus,
101− url,
102− unknown,
103− bootProgress,
104− bootMessage,
105− disabled,
106− } = ship;
107−
108− const { running: localRunning, url: localUrl } = useLocalHosting();
109−
110− let running = useMemo(() => localRunning || ship.running, [
111− ship,
112− localRunning,
113− ]);
114−
115− useLifecycle(() => {
116− syncStatus();
117− });
118−
119− const createShip = useCallback(() => ship.create(keyfile), [keyfile, ship]);
120−
121− const name = useCurrentPointName();
122− const options = [{ text: 'Tlon', value: 'tlon' }];
123−
124− const renderMain = useCallback(() => {
125− if (localRunning) {
126− return (
127− <Grid.Item
128− full
129− as={LoginButton}
130− solid
131− success
132− url={localUrl}
133− code={code}>
134− Open OS
135− </Grid.Item>
136− );
137− }
138− if (ship.running) {
139− return (
140− <>
141− <Grid.Item
142− cols={[1, 9]}
143− as={LoginButton}
144− solid
145− success
146− url={url}
147− code={code}>
148− Open OS
149− </Grid.Item>
150− {/* Unsupported for now */}
151− {/* <Grid.Item cols={[9, 13]} as={Button} className="b-black b1" center> */}
152− {/* Disconnect */}
153− {/* </Grid.Item> */}
154− </>
155− );
156− }
157− if (ship.missing) {
158− return (
159− <Grid.Item
160− full
161− as={ForwardButton}
162− solid
163− disabled={!keyfile}
164− onClick={createShip}>
165− Connect
166− </Grid.Item>
167− );
168− }
169− if (ship.pending) {
170− return (
171− <Grid.Item
172− full
173− as={ProgressButton}
174− success
175− disabled
176− progress={bootProgress}>
177− {bootMessage || 'Connecting'}
178− </Grid.Item>
179− );
180− }
181− }, [
182− localRunning,
183− ship.running,
184− ship.missing,
185− ship.pending,
186− localUrl,
187− code,
188− url,
189− keyfile,
190− createShip,
191− bootProgress,
192− bootMessage,
193− ]);
194−
195− const renderDetails = useCallback(() => {
196− if (ship.running) {
197− return (
198− <>
199− <Grid.Item cols={[1, 9]} className="gray4">
200− <span className="mono">{name}</span> is connected to Tlon
201− </Grid.Item>
202− </>
203− );
204− }
205− if (!keyfile && ship.missing) {
206− return (
207− <Grid.Item full className="gray4">
208− Please reset your networking keys in order to use hosting
209− </Grid.Item>
210− );
211− }
212− }, [name, ship, keyfile]);
213−
214− return (
215− <>
216− {disabled && (
217− <Grid gap={4}>
218− <Grid.Item full className="f5">
219− Urbit OS
220− </Grid.Item>
221− <Grid.Divider />
222− </Grid>
223− )}
224− {!disabled && (
225− <Grid gap={4}>
226− <Grid.Item full className="f5" as={Flex}>
227− <Flex.Item>Urbit OS </Flex.Item>
228− <Flex.Item
229− className={cn({ green3: running, gray4: !running }, 'ml3')}>
230− {running ? 'Connected' : 'Disconnected'}
231− </Flex.Item>
232− </Grid.Item>
233− <BridgeForm initialValues={{ provider: 'tlon' }}>
234− {() => (
235− <>
236− {renderDetails()}
237− {renderMain()}
238− {!localRunning && (
239− <Grid.Item
240− full
241− as={SelectInput}
242− name="provider"
243− label="Host Provider"
244− options={options}
245− disabled
246− />
247− )}
248− </>
249− )}
250− </BridgeForm>
251− </Grid>
252− )}
253− <Grid>
254− <Grid.Item
255− full
256− as={DownloadKeyfileButton}
257− {...bind}
258− className="mt2"
259− detail="A keyfile authenticates your Urbit ID to Urbit OS"
260− />
261− <Grid.Divider />
262− {code && (
263− <Grid.Item full className="mt2" as={CopyButtonWide} text={code}>
264− Login Code
265− </Grid.Item>
266− )}
267− </Grid>
268− </>
269− );
270−}
src/views/UrbitOS/Home.tsxnew file+110
@@ -0,0 +1,110 @@
1+import React, { useCallback, useState } from 'react';
2+import { Grid } from 'indigo-react';
3+import * as ob from 'urbit-ob';
4+import { azimuth } from 'azimuth-js';
5+
6+import { usePointCursor } from 'store/pointCursor';
7+import { usePointCache } from 'store/pointCache';
8+
9+import * as need from 'lib/need';
10+
11+import { ForwardButton } from 'components/Buttons';
12+import NetworkingKeys from 'components/NetworkingKeys';
13+
14+import { useLocalRouter } from 'lib/LocalRouter';
15+import { L1Point } from 'types/L1Point';
16+import AlertBox from 'components/AlertBox';
17+import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
18+import useKeyfileGenerator from 'lib/useKeyfileGenerator';
19+
20+export default function UrbitOSHome({ manualNetworkSeed }) {
21+ const { pointCursor } = usePointCursor();
22+ const { getDetails } = usePointCache();
23+
24+ const { push, names } = useLocalRouter();
25+
26+ const point = need.point(pointCursor);
27+ const details: L1Point = need.details(getDetails(point));
28+
29+ const sponsor = ob.patp(details.sponsor);
30+
31+ const [showKeys, setShowKeys] = useState(false);
32+
33+ const showSponsor = azimuth.getPointSize(point) !== azimuth.PointSize.Galaxy;
34+ const toggleShowKeys = useCallback(() => setShowKeys(s => !s), [setShowKeys]);
35+
36+ // The initial key revision number is 0, and increments when set
37+ const hasSetNetworkingKeys = details.keyRevisionNumber !== '0';
38+ const networkKeysAction = hasSetNetworkingKeys ? 'Reset' : 'Initialize';
39+
40+ const goNetworkingKeys = useCallback(() => push(names.NETWORKING_KEYS), [
41+ names,
42+ push,
43+ ]);
44+
45+ const goChangeSponsor = useCallback(() => push(names.CHANGE_SPONSOR), [
46+ push,
47+ names,
48+ ]);
49+
50+ const { bind: keyBind } = useKeyfileGenerator();
51+
52+ return (
53+ <>
54+ <Grid>
55+ <Grid.Item full className="mv7 f5">
56+ Urbit OS
57+ </Grid.Item>
58+ {showSponsor && (
59+ <>
60+ <Grid.Divider />
61+ <Grid.Item
62+ full
63+ as={ForwardButton}
64+ detail="A sponsor finds new peers in your network"
65+ accessory="Change"
66+ onClick={goChangeSponsor}>
67+ <span className="mono">{sponsor}</span>
68+ <span className="f7 bg-black white p1 mb2 ml2 r4">SPONSOR</span>
69+ </Grid.Item>
70+ </>
71+ )}
72+ {hasSetNetworkingKeys && (
73+ <>
74+ <Grid.Divider />
75+ <Grid.Item
76+ full
77+ detail="A keyfile is used to boot your Urbit OS"
78+ as={DownloadKeyfileButton}
79+ {...keyBind}
80+ />
81+ </>
82+ )}
83+ <Grid.Divider />
84+ <Grid.Item full as={ForwardButton} onClick={goNetworkingKeys}>
85+ {networkKeysAction} Networking Keys
86+ </Grid.Item>
87+ {hasSetNetworkingKeys && (
88+ <>
89+ <Grid.Divider />
90+ <Grid.Item
91+ full
92+ as={ForwardButton}
93+ accessory={showKeys ? '▲' : '▼'}
94+ onClick={toggleShowKeys}>
95+ View Networking Keys
96+ </Grid.Item>
97+ {showKeys && <Grid.Item full as={NetworkingKeys} point={point} />}
98+ </>
99+ )}
100+ {!hasSetNetworkingKeys && (
101+ <>
102+ <Grid.Item full as={AlertBox} className="mt4">
103+ Networking Keys are required to generate a keyfile
104+ </Grid.Item>
105+ </>
106+ )}
107+ </Grid>
108+ </>
109+ );
110+}
src/views/UrbitOS/NetworkingKeys.js+14−7
@@ -25,6 +25,7 @@ import ViewHeader from 'components/ViewHeader';
2525import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
2626import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
2727import NoticeBox from 'components/NoticeBox';
28+import AlertBox from 'components/AlertBox';
2829
2930import { HexInput } from 'form/Inputs';
3031import {
@@ -205,28 +206,34 @@ export default function UrbitOSNetworkingKeys({
205206 []
206207 );
207208
209+ const viewTitle = completed
210+ ? 'Network Keys are now set'
211+ : 'Set Networking Keys';
212+
208213 const usageMessage =
209− 'You need this to authenticate with Arvo. Please download';
214+ 'You need this keyfile to authenticate with your OS. Please Download.';
210215
211216 return (
212217 <>
213218 <Grid>
214219 <Grid.Item full as={ViewHeader}>
215− Set Networking Keys
216− </Grid.Item>
217− <Grid.Item className="gray4 f6" full>
218− Network Keys solidify the handshake between ID and OS
220+ {viewTitle}
219221 </Grid.Item>
222+ {!completed && (
223+ <Grid.Item className="gray4 f5" full>
224+ Network Keys authenticate Urbit ID with Urbit OS.
225+ </Grid.Item>
226+ )}
220227 {completed && (
221228 <>
222229 <Grid.Item
223230 full
224− className="mv3"
231+ className="mb3"
225232 as={DownloadKeyfileButton}
226233 solid
227234 {...keyfileBind}
228235 />
229− <Grid.Item full className="mb3 bg-red3 white t-center p3">
236+ <Grid.Item full as={AlertBox} className="mb8">
230237 {usageMessage}
231238 </Grid.Item>
232239 </>