on master help sign in
cleanup and integrate with real hosting backend
commit8d566eacf04b7f7a3d52b0415d114dc9e35cd7cd authorLiam Fitzgerald <liam@urizen.co> dateWed 4 Mar 2020, 14:09 (7 years ago) parent8227348
21 files changed+729−468
package.json+1 src/components/HostedShip.js−153 src/lib/hosting.js+24 src/lib/routeNames.js+1 src/lib/router.js+2 src/store/hosting.js+1−1 src/store/lib/useHostingStore.js+117−86 src/views/Boot.js−7 src/views/Hosting.js−63 src/views/Invite/Email.js+1−1 src/views/Point.js−27 src/views/Residents.js+34−44 src/views/UrbitID.js+17−11 src/views/UrbitID/Home.js+5−1 src/views/UrbitID/ResetKeys.js+64 src/views/UrbitID/ResetKeys/ResetConfirm.js+91 src/views/UrbitID/ResetKeys/ResetDownload.js+39 src/views/UrbitID/ResetKeys/ResetExecute.js+193 src/views/UrbitID/ResetKeys/ResetVerify.js+71 src/views/UrbitOS.js+10−10 src/views/UrbitOS/Home.js+58−64
package.json+1
@@ -13,6 +13,7 @@
1313 "engines": {
1414 "node": "11.0.0"
1515 },
16+ "proxy": "http://localhost:4000",
1617 "dependencies": {
1718 "@hot-loader/react-dom": "^16.8.6",
1819 "@ledgerhq/hw-app-eth": "^5.5.0",
src/components/HostedShip.jsdeleted−153
@@ -1,153 +0,0 @@
1−import React, { useEffect, useCallback } from 'react';
2−import { Grid, ErrorText } from 'indigo-react';
3−
4−import cn from 'classnames';
5−
6−import { useHosting } from 'store/hosting';
7−
8−import BridgeForm from 'form/BridgeForm';
9−
10−import { GenerateButton, ForwardButton } from './Buttons';
11−
12−import 'style/hosted-ship.scss';
13−import useKeyfileGenerator from 'lib/useKeyfileGenerator';
14−
15−export default function HostedShip({
16− // useHostedShip
17−
18− // additional from parent
19− className,
20−}) {
21− const ship = useHosting();
22− const { syncStatus } = ship;
23− console.log(ship);
24−
25− const { keyfile } = useKeyfileGenerator();
26−
27− const createShip = useCallback(() => ship.create(keyfile), [ship, keyfile]);
28−
29− const renderStatus = () => {
30− if (ship.missing || ship.booting) {
31− return (
32− <Grid.Item
33− full
34− as={GenerateButton}
35− solid
36− success
37− disabled={ship.booting}
38− loading={ship.booting}
39− onClick={createShip}>
40− Boot Urbit OS
41− </Grid.Item>
42− );
43− } else if (ship.running || ship.connecting) {
44− return (
45− <Grid.Item
46− full
47− as={ForwardButton}
48− solid
49− success
50− disabled={ship.connecting}
51− loading={ship.connecting}
52− onClick={ship.getEvents}>
53− Connect
54− </Grid.Item>
55− );
56− } else if (ship.connected) {
57− return (
58− <Grid.Item
59− full
60− as={GenerateButton}
61− solid
62− success
63− onClick={ship.getStatus}>
64− Disconnect
65− </Grid.Item>
66− );
67− } else if (ship.unknown || ship.querying) {
68− return (
69− <Grid.Item
70− full
71− as={GenerateButton}
72− accessory={ship.error ? '↺' : '->'}
73− solid
74− success
75− disabled={ship.querying}
76− loading={ship.querying}
77− onClick={syncStatus}>
78− Query
79− </Grid.Item>
80− );
81− }
82− };
83−
84− const renderEvents = (title, detail, events) => {
85− return (
86− <>
87− <Grid.Item
88− full
89− as={ForwardButton}
90− accessory=""
91− disabled
92− detail={detail}>
93− {title}
94− </Grid.Item>
95− <Grid.Item full>
96− <ul className="hosting-events">
97− {events.length === 0 && <li>&lt;Waiting&gt;</li>}
98− {events.map(event => (
99− <li>{event}</li>
100− ))}
101− </ul>
102− </Grid.Item>
103− </>
104− );
105− };
106−
107− return (
108− <>
109− <Grid className={cn(className, 'mt1')}>
110− <BridgeForm>
111− {() => (
112− <>
113− {ship.error && (
114− <Grid.Item full as={ErrorText} className="mv1">
115− {ship.error.message}
116− </Grid.Item>
117− )}
118− {renderStatus()}
119− {!ship.unknown && (
120− <Grid.Item
121− full
122− as={ForwardButton}
123− accessory=""
124− disabled
125− detail={ship.state}
126− detailClassName="mono">
127− Ship Status
128− </Grid.Item>
129− )}
130− </>
131− )}
132− </BridgeForm>
133−
134− {ship.connected && (
135− <>
136− <Grid.Divider />
137− {renderEvents('Run', 'Terminal events', ship.runEvents)}
138− {renderEvents(
139− 'New',
140− 'Pier creation and boot events',
141− ship.newEvents
142− )}
143− {renderEvents(
144− 'Sys',
145− 'Hosting platform events',
146− ship.sysEvents.map(e => e.object.message)
147− )}
148− </>
149− )}
150− </Grid>
151− </>
152− );
153−}
src/lib/hosting.jsnew file+24
@@ -0,0 +1,24 @@
1+export const getVaneName = code => {
2+ switch (code) {
3+ case 'a':
4+ return 'Ames';
5+ case 'b':
6+ return 'Behn';
7+ case 'c':
8+ return 'Clay';
9+ case 'd':
10+ return 'Dill';
11+ case 'e':
12+ return 'Eyre';
13+ case 'f':
14+ return 'Ford';
15+ case 'g':
16+ return 'Gall';
17+ case 'i':
18+ return 'Iris';
19+ case 'j':
20+ return 'Jael';
21+ }
22+};
23+
24+export const getVaneNumber = code => 1 + 'abcdefgij'.indexOf(code);
src/lib/routeNames.js+1
@@ -5,6 +5,7 @@ export const ROUTE_NAMES = {
55 ACTIVATE: Symbol('ACTIVATE'),
66 INVITE_TRANSACTIONS: Symbol('INVITE_TRANSACTIONS'),
77 INVITES_MANAGE: Symbol('INVITES_MANAGE'),
8+ SENATE: Symbol('SENATE'),
89 //
910 LOGIN: Symbol('LOGIN'),
1011 ADMIN: Symbol('ADMIN'),
src/lib/router.js+2
@@ -18,6 +18,8 @@ import UrbitOS from 'views/UrbitOS';
1818import UrbitID from 'views/UrbitID';
1919import Residents from 'views/Residents';
2020import Adopt from 'views/Adopt';
21+import Boot from 'views/Boot';
22+import Hosting from 'views/Hosting';
2123
2224import { ROUTE_NAMES } from './routeNames';
2325
src/store/hosting.js+1−1
@@ -5,7 +5,7 @@ import useHostingStore from './lib/useHostingStore';
55export const HostingContext = createContext(null);
66
77export function HostingProvider({ children }) {
8− const store = useHostingStore('http://localhost:4000');
8+ const store = useHostingStore('');
99 return (
1010 <HostingContext.Provider value={store}>{children}</HostingContext.Provider>
1111 );
src/store/lib/useHostingStore.js+117−86
@@ -1,9 +1,10 @@
1−import { useState, useEffect, useCallback } from 'react';
2−import { Nothing } from 'folktale/maybe';
1+import { useState, useCallback, useMemo } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
33import * as ob from 'urbit-ob';
44
5+import { getVaneName, getVaneNumber } from 'lib/hosting';
56import SolarisClient from 'lib/SolarisClient';
6−import { usePointCursor } from 'store/pointCursor';
7+// import { usePointCursor } from 'store/pointCursor';
78import useKeyfileGenerator from 'lib/useKeyfileGenerator';
89const STATE = {
910 // container states
@@ -11,32 +12,26 @@ const STATE = {
1112 MISSING: 'MISSING',
1213 RUNNING: 'RUNNING',
1314 PENDING: 'PENDING',
14− // api states
15− QUERYING: 'QUERYING',
16− BOOTING: 'BOOTING',
17− CONNECTING: 'CONNECTING',
18− // connection states
19− CONNECTED: 'CONNECTED',
2015};
2116
22−const mockFetch = status => ({
23− ok: true,
24−
25− json: () => Promise.resolve({ status }),
26−});
27−
2817function useHostingStore(url) {
2918 const client = new SolarisClient(url);
3019
31− const { pointCursor } = usePointCursor();
20+ // const { pointCursor } = usePointCursor();
21+ //
22+ const pointCursor = Just(ob.patp2dec('~wicdev-wisryt'));
23+
24+ const [bootProgress, setBootProgress] = useState(0);
25+ const [bootMessage, setBootMessage] = useState('Assembling Urbit');
26+ const [startTime] = useState(Date.now());
3227
33− const [state, _setState] = useState(STATE.UNKNOWN);
34− const setState = useCallback(
28+ const [status, _setStatus] = useState(STATE.UNKNOWN);
29+ const setStatus = useCallback(
3530 s => {
3631 console.log(s);
37− _setState(s);
32+ _setStatus(s);
3833 },
39− [_setState]
34+ [_setStatus]
4035 );
4136
4237 const [sysEvents, setSysEvents] = useState([]);
@@ -48,65 +43,25 @@ function useHostingStore(url) {
4843 error => {
4944 _setError(error);
5045 if (error) {
51− setState(STATE.UNKNOWN);
46+ setStatus(STATE.UNKNOWN);
5247 console.error(error);
5348 }
5449 },
55− [_setError, setState]
50+ [_setError, setStatus]
5651 );
5752
58− const unknown = state === STATE.UNKNOWN;
59− const missing = state === STATE.MISSING;
60− const running = state === STATE.RUNNING;
61− const pending = state === STATE.PENDING;
62− const querying = state === STATE.QUERYING;
63− const booting = state === STATE.BOOTING;
64− const connecting = state === STATE.CONNECTING;
65− const connected = state === STATE.CONNECTED;
53+ const unknown = status === STATE.UNKNOWN;
54+ const missing = status === STATE.MISSING;
55+ const running = status === STATE.RUNNING;
56+ const pending = status === STATE.PENDING;
57+
58+ let syncStatus;
6659
6760 const resetEvents = useCallback(() => {
6861 setSysEvents([]);
6962 setNewEvents([]);
7063 setRunEvents([]);
71− }, []);
72−
73− const syncStatus = useCallback(async () => {
74− try {
75− if (Nothing.hasInstance(pointCursor)) {
76− return;
77− }
78− const patp = ob.patp(pointCursor.value);
79− setError(undefined);
80−
81− resetEvents();
82−
83− setState(STATE.QUERYING);
84−
85− const response = await client.getShipsByPatp(patp);
86−
87− if (response.status === 404) {
88− setState(STATE.MISSING);
89− } else if (!response.ok) {
90− setError(response.statusText);
91− } else {
92− const ship = await response.json();
93−
94− if (ship.status === 'Pending') {
95− setState(STATE.PENDING);
96− } else if (ship.status === 'Running') {
97− setState(STATE.RUNNING);
98− } else {
99− setError(ship);
100− }
101− }
102− } catch (error) {
103− setError(error);
104− }
105− }, [client, pointCursor, resetEvents, setError, setState]);
106−
107− // useEffect(() => {
108− // syncStatus();
109− // }, [pointCursor, syncStatus]);
64+ }, [setSysEvents, setNewEvents, setRunEvents]);
11065
11166 const getEvents = useCallback(() => {
11267 try {
@@ -118,29 +73,53 @@ function useHostingStore(url) {
11873
11974 resetEvents();
12075
121− setState(STATE.CONNECTING);
122−
12376 const url = client.getShipsByPatpUrl(patp) + '/events';
12477 const source = new EventSource(url);
12578
126− source.onopen = () => setState(STATE.CONNECTED);
127− // source.onerror = () => setState(STATE.UNKNOWN);
79+ // source.onerror = () => setStatus(STATE.UNKNOWN);
12880 // source.onmessage = console.log;
12981
13082 source.addEventListener('sys', event => {
131− console.log(event);
132− setSysEvents(previous => [...previous, JSON.parse(event.data)]);
83+ // console.log(event);
84+ const sysEvent = JSON.parse(event.data);
85+ const time = new Date(sysEvent.object.firstTimestamp).getTime();
86+ if (time > startTime) {
87+ console.log(sysEvent);
88+ setSysEvents(previous => [...previous, sysEvent]);
89+ }
13390 });
13491
13592 source.addEventListener('new', event => {
136− console.log(event);
93+ // console.log(event);
94+ const elapsed = (Date.now() - startTime) / 1000;
13795 if (event.data !== '') {
13896 setNewEvents(previous => [...previous, event.data]);
13997 }
98+ if (event.data === '1-b') {
99+ setBootMessage('Compiling Hoon');
100+ setBootProgress(0.2);
101+ console.log(`${elapsed}: compiling Hoon`);
102+ } else if (event.data === '1-c') {
103+ setBootProgress(0.25);
104+ } else if (event.data === '%arvo-assembly') {
105+ setBootMessage('Compiling Urbit OS');
106+ setBootProgress(0.3);
107+ } else if (event.data.startsWith('[%vane %')) {
108+ const vane = getVaneName(event.data.slice(8, 9));
109+ setBootMessage(`Compiling vane: ${vane}`);
110+ setBootProgress(0.3 + 0.05 * getVaneNumber(event.data.slice(8, 9)));
111+ } else if (event.data === 'pier: boot complete') {
112+ setBootMessage('Ship starting');
113+ setBootProgress(0.85);
114+ }
140115 });
141116
142117 source.addEventListener('run', event => {
143− console.log(event);
118+ if (event.data === 'goad: recompiling all apps') {
119+ setBootProgress(1.0);
120+ setBootMessage('Ship Launched');
121+ syncStatus();
122+ }
144123 if (event.data !== '') {
145124 setRunEvents(previous => [...previous, event.data]);
146125 }
@@ -148,7 +127,54 @@ function useHostingStore(url) {
148127 } catch (error) {
149128 setError(error);
150129 }
151− }, [setError, resetEvents, client, pointCursor, setState]);
130+ }, [
131+ setError,
132+ resetEvents,
133+ client,
134+ pointCursor,
135+ setBootMessage,
136+ startTime,
137+ syncStatus,
138+ ]);
139+
140+ syncStatus = useCallback(async () => {
141+ try {
142+ if (Nothing.hasInstance(pointCursor)) {
143+ return;
144+ }
145+ const patp = ob.patp(pointCursor.value);
146+ setError(undefined);
147+
148+ resetEvents();
149+
150+ const response = await client.getShipsByPatp(patp);
151+
152+ if (response.status === 404) {
153+ setStatus(STATE.MISSING);
154+ } else if (!response.ok) {
155+ setError(response.statusText);
156+ } else {
157+ const ship = await response.json();
158+
159+ if (ship.status === 'Pending') {
160+ setStatus(STATE.PENDING);
161+ getEvents();
162+ } else if (ship.status === 'Running') {
163+ setStatus(STATE.RUNNING);
164+ } else {
165+ setError(ship);
166+ }
167+ }
168+ } catch (error) {
169+ setError(error);
170+ }
171+ }, [client, getEvents, pointCursor, resetEvents, setError, setStatus]);
172+
173+ const hostedShipUrl = useMemo(
174+ () =>
175+ `http://${ob.patp(pointCursor.getOrElse('')).slice(1)}.liam.tlon.network`,
176+ [pointCursor]
177+ );
152178
153179 const create = useCallback(
154180 async keyfile => {
@@ -156,10 +182,15 @@ function useHostingStore(url) {
156182 if (Nothing.hasInstance(pointCursor)) {
157183 return;
158184 }
159− const patp = ob.patp(pointCursor.value);
185+ keyfile =
186+ '0wnE.Z56pm.i2HLZ.6Pf1R.tAVZH.65R~C.Zgy6y.caI5Z.bZ2vV.09M5A.QP5-p.29UfR.35h2U.2k8-D.QP~~p.6bL62.~x3Em.t9h0x.0y01N.w80c1';
187+ const patp = ob.patp(pointCursor.value).slice(1);
160188 setError(undefined);
161189
162− setState(STATE.BOOTING);
190+ const start = Date.now();
191+ console.log(start);
192+ setStatus(STATE.PENDING);
193+ setBootProgress(0.1);
163194
164195 const response = await client.postShips({
165196 patp,
@@ -177,7 +208,7 @@ function useHostingStore(url) {
177208 setError(error);
178209 }
179210 },
180− [pointCursor, setError, setState, client, getEvents]
211+ [pointCursor, setError, setStatus, client, getEvents]
181212 );
182213
183214 return {
@@ -186,10 +217,7 @@ function useHostingStore(url) {
186217 missing,
187218 running,
188219 pending,
189− querying,
190− booting,
191− connecting,
192− connected,
220+ url: hostedShipUrl,
193221
194222 sysEvents,
195223 newEvents,
@@ -199,8 +227,11 @@ function useHostingStore(url) {
199227 getEvents,
200228 create,
201229
230+ bootProgress,
231+ bootMessage,
232+
202233 // debugging
203− state,
234+ status,
204235 };
205236}
206237
src/views/Boot.jsdeleted−7
@@ -1,7 +0,0 @@
1−import React from 'react';
2−
3−import View from 'components/View';
4−
5−export default function Boot() {
6− return <View />;
7−}
src/views/Hosting.jsdeleted−63
@@ -1,63 +0,0 @@
1−import React from 'react';
2−import { Grid } from 'indigo-react';
3−import { Just } from 'folktale/maybe';
4−import ob from 'urbit-ob';
5−
6−import { usePointCursor } from 'store/pointCursor';
7−import { useWallet } from 'store/wallet';
8−
9−import * as need from 'lib/need';
10−import { useLocalRouter } from 'lib/LocalRouter';
11−import { useHostedShip } from 'lib/useHostedShip';
12−import SolarisClient from 'lib/SolarisClient';
13−
14−import View from 'components/View';
15−import Passport from 'components/Passport';
16−import { OutButton, ForwardButton } from 'components/Buttons';
17−import HostedShip from 'components/HostedShip';
18−import Footer from 'components/Footer';
19−
20−export default function Hosting() {
21− const { pop } = useLocalRouter();
22−
23− const { pointCursor } = usePointCursor();
24− const point = need.point(pointCursor);
25− const patp = ob.patp(point);
26−
27− const { wallet } = useWallet();
28− const address = need.addressFromWallet(wallet);
29−
30− const client = new SolarisClient('https://localhost:3030');
31− const ship = useHostedShip(client, patp);
32−
33− return (
34− <View pop={pop} inset>
35− <Grid>
36− <Passport
37− point={Just(point)}
38− address={Just(address)}
39− animationMode={'slide'}
40− />
41− <Grid.Item full as={HostedShip} ship={ship} onReturn={() => pop()} />
42− <Grid.Divider />
43− </Grid>
44−
45− <Footer>
46− <Grid>
47− <Grid.Divider />
48− <Grid.Item full as={ForwardButton} disabled={!ship.running}>
49− Redirect to Urbit OS Dojo
50− </Grid.Item>
51− <Grid.Divider />
52− <Grid.Item
53− full
54− as={OutButton}
55− href="https://urbit.org/docs/getting-started"
56− detail="Run your own Urbit OS">
57− Hosting Instructions
58− </Grid.Item>
59− </Grid>
60− </Footer>
61− </View>
62− );
63−}
src/views/Invite/Email.js+1−1
@@ -103,7 +103,7 @@ const InviteMail = ({ setSubmitting }) => {
103103 form.reset();
104104 }, 1500);
105105 },
106− [generateInvites, setCount, setStatus, sendInvites, setSubmitting]
106+ [setSubmitting, generateInvites, sendInvites, resetInvites]
107107 );
108108
109109 const validateHasReceived = useCallback(
src/views/Point.js−27
@@ -10,14 +10,12 @@ import { useWallet } from 'store/wallet';
1010import View from 'components/View';
1111import Greeting from 'components/Greeting';
1212import Passport from 'components/Passport';
13−import { ForwardButton } from 'components/Buttons';
1413import Blinky, { matchBlinky } from 'components/Blinky';
1514import BarGraph from 'components/BarGraph';
1615import Chip from 'components/Chip';
1716import InviteSigilList from 'components/InviteSigilList';
1817import { ForwardButton, HostingButton } from 'components/Buttons';
1918import CopyButton from 'components/CopyButton';
20−import { matchBlinky } from 'components/Blinky';
2119import DownloadSigilButton from 'components/DownloadSigilButton';
2220
2321import * as need from 'lib/need';
@@ -72,8 +70,6 @@ export default function Point() {
7270 //
7371 // availableInvites.getOrElse(0) === 0
7472
75− const goAdmin = useCallback(() => push(names.ADMIN), [push, names]);
76−
7773 const goSenate = useCallback(() => push(names.SENATE), [push, names]);
7874
7975 const goSigil = useCallback(() => push(names.SIGIL_GENERATOR), [push, names]);
@@ -288,29 +284,6 @@ export default function Point() {
288284 </>
289285 )}
290286 {senateButton}
291− <Grid.Item
292− full
293− as={ForwardButton}
294− detail="Boot your computer"
295− onClick={goBoot}>
296− Boot Urbit OS
297− </Grid.Item>
298−
299− <Grid.Item full as={HostingButton} onClick={goHosting} />
300− <Grid.Divider />
301−
302− <Grid.Item
303− full
304− as={ForwardButton}
305− accessory={code && <CopyButton text={code} />}
306− detail={code || notice}
307− disabled={!code}
308− detailClassName="mono">
309− Login Code
310− </Grid.Item>
311− <Grid.Divider />
312− <Grid.Item full as={DownloadSigilButton} point={point} />
313− <Grid.Divider />
314287 </Grid>
315288 </View>
316289 );
src/views/Residents.js+34−44
@@ -1,6 +1,6 @@
1−import React, { useState, useEffect, useCallback } from 'react';
1+import React, { useState, useEffect, useCallback, useMemo } from 'react';
22import { Grid, H4, HelpText } from 'indigo-react';
3−import { Nothing } from 'folktale/maybe';
3+import { Just, Nothing } from 'folktale/maybe';
44import * as ob from 'urbit-ob';
55
66import View from 'components/View';
@@ -8,6 +8,7 @@ import Tabs from 'components/Tabs';
88import Crumbs from 'components/Crumbs';
99import Sigil from 'components/Sigil';
1010import Blinky from 'components/Blinky';
11+import NavHeader from 'components/NavHeader';
1112
1213import { useLocalRouter } from 'lib/LocalRouter';
1314import useCurrentPointName from 'lib/useCurrentPointName';
@@ -27,26 +28,32 @@ const OPTIONS = [
2728];
2829
2930const VIEWS = {
30− [NAMES.REQUESTS]: Requests,
31− [NAMES.ALL]: AllResidents,
31+ [NAMES.REQUESTS]: Tab,
32+ [NAMES.ALL]: Tab,
3233};
3334
34−function Requests({ className, requests, onAccept, onDecline }) {
35− if (Nothing.hasInstance(requests)) {
36− return (
37− <Grid className={className}>
38− <Grid.Item full as={HelpText} className="mt8 t-center">
39− <Blinky /> Loading...
40− </Grid.Item>
41− </Grid>
42− );
35+function Tab({ className, points, onAccept, onDecline }) {
36+ if (Nothing.hasInstance(points)) {
37+ return <Grid className={className}></Grid>;
4338 }
4439
45− const _requests = requests.getOrElse([]);
40+ const _points = points.getOrElse([]);
4641
4742 return (
4843 <Grid className={className}>
49− {_requests.map(point => (
44+ {_points.length === 0 && (
45+ <Grid.Item full as={HelpText} className="mt8 t-center">
46+ {Just.hasInstance(points) ? (
47+ 'No points to display'
48+ ) : (
49+ <>
50+ <Blinky /> Loading...
51+ </>
52+ )}
53+ </Grid.Item>
54+ )}
55+
56+ {_points.map(point => (
5057 <Resident
5158 key={point}
5259 point={point}
@@ -100,26 +107,6 @@ function Resident({ point, onAccept, onDecline }) {
100107 );
101108}
102109
103−function AllResidents({ className, residents }) {
104− if (Nothing.hasInstance(residents)) {
105− return (
106− <Grid className={className}>
107− <Grid.Item full>Loading...</Grid.Item>
108− </Grid>
109− );
110− }
111−
112− const _residents = residents.getOrElse([]);
113−
114− return (
115− <Grid className={className}>
116− {_residents.map(point => (
117− <Resident key={point} point={point} />
118− ))}
119− </Grid>
120− );
121−}
122−
123110export default function Residents() {
124111 const { pop, push, names } = useLocalRouter();
125112 const name = useCurrentPointName();
@@ -136,28 +123,32 @@ export default function Residents() {
136123
137124 const [currentTab, setCurrentTab] = useState(NAMES.ALL);
138125
139− const onAccept = useCallback(adoptee =>
126+ const isRequests = useMemo(() => currentTab === NAMES.REQUESTS, [currentTab]);
127+
128+ const _onAccept = useCallback(adoptee =>
140129 push(names.ADOPT, {
141130 adoptee,
142131 denied: false,
143132 })
144133 );
134+ const onAccept = isRequests && _onAccept;
145135
146− const onDecline = useCallback(adoptee =>
136+ const _onDecline = useCallback(adoptee =>
147137 push(names.ADOPT, {
148138 adoptee,
149139 denied: true,
150140 })
151141 );
142+ const onDecline = isRequests && _onDecline;
143+
144+ const points = isRequests ? requests : residents;
152145
153146 return (
154147 <View pop={pop} inset>
148+ <NavHeader>
149+ <Crumbs routes={[{ text: name, action: pop }, { text: 'Residents' }]} />
150+ </NavHeader>
155151 <Grid>
156− <Grid.Item
157− full
158− as={Crumbs}
159− routes={[{ text: name, action: pop }, { text: 'Residents' }]}
160− />
161152 <Grid.Item full as={H4} className="mt4">
162153 Residents
163154 </Grid.Item>
@@ -171,8 +162,7 @@ export default function Residents() {
171162 currentTab={currentTab}
172163 onTabChange={setCurrentTab}
173164 // Props for tab
174− residents={residents}
175− requests={requests}
165+ points={points}
176166 onAccept={onAccept}
177167 onDecline={onDecline}
178168 />
src/views/UrbitID.js+17−11
@@ -9,23 +9,27 @@ import { LocalRouterProvider } from 'lib/LocalRouter';
99
1010import View from 'components/View';
1111import Crumbs from 'components/Crumbs';
12+import NavHeader from 'components/NavHeader';
1213
1314import UrbitIDHome from './UrbitID/Home';
1415import SigilGenerator from './UrbitID/SigilGenerator';
1516import DownloadKeys from './UrbitID/DownloadKeys';
1617import SetProxy from './UrbitID/SetProxy';
18+import ResetKeys from './UrbitID/ResetKeys';
1719
1820const NAMES = {
1921 HOME: 'HOME',
2022 SIGIL_GENERATOR: 'SIGIL_GENERATOR',
2123 DOWNLOAD_KEYS: 'DOWNLOAD_KEYS',
2224 SET_PROXY: 'SET_PROXY',
25+ RESET_KEYS: 'RESET_KEYS',
2326};
2427
2528const VIEWS = {
2629 [NAMES.HOME]: UrbitIDHome,
2730 [NAMES.SIGIL_GENERATOR]: SigilGenerator,
2831 [NAMES.DOWNLOAD_KEYS]: DownloadKeys,
32+ [NAMES.RESET_KEYS]: ResetKeys,
2933 [NAMES.SET_PROXY]: SetProxy,
3034};
3135
@@ -37,6 +41,8 @@ const humanizeName = name => {
3741 return 'Download Keys';
3842 case NAMES.SET_PROXY:
3943 return 'Set Key';
44+ case NAMES.RESET_KEYS:
45+ return 'Reset Keys';
4046 default:
4147 return undefined;
4248 }
@@ -58,18 +64,18 @@ export default function UrbitOS() {
5864
5965 return (
6066 <View pop={router.pop} inset>
67+ <NavHeader>
68+ <Crumbs
69+ routes={[
70+ { text: name, action: () => history.pop() },
71+ { text: 'Urbit ID', action: homeAction },
72+ ...lastCrumb,
73+ ]}
74+ />
75+ </NavHeader>
76+
6177 <LocalRouterProvider value={router}>
62− <Grid className="mb4">
63− <Grid.Item
64− full
65− as={Crumbs}
66− routes={[
67− { text: name, action: () => history.pop() },
68− { text: 'Urbit ID', action: homeAction },
69− ...lastCrumb,
70− ]}
71− />
72− </Grid>
78+ <Grid className="mb4"></Grid>
7379 <Route />
7480 </LocalRouterProvider>
7581 </View>
src/views/UrbitID/Home.js+5−1
@@ -35,6 +35,8 @@ export default function UrbitIDHome() {
3535 names,
3636 ]);
3737
38+ const goResetKeys = useCallback(() => push(names.RESET_KEYS), [push, names]);
39+
3840 const isMasterTicket = Just.hasInstance(urbitWallet);
3941 const _contracts = need.contracts(contracts);
4042 const point = need.point(pointCursor);
@@ -96,7 +98,7 @@ export default function UrbitIDHome() {
9698 <Grid.Item
9799 full
98100 as={ForwardButton}
99− onClick={goDownloadKeys}
101+ onClick={goResetKeys}
100102 detail="Reset Master Key and all other keys">
101103 Reset Keys
102104 </Grid.Item>
@@ -122,6 +124,8 @@ export default function UrbitIDHome() {
122124
123125 {isParent && renderProxyAction(PROXY_TYPE.SPAWN, details.spawnProxy)}
124126
127+ {isParent && networkRevision !== 0 && <Grid.Divider />}
128+
125129 {isParent && networkRevision === 0 && (
126130 <>
127131 <Grid.Item
src/views/UrbitID/ResetKeys.jsnew file+64
@@ -0,0 +1,64 @@
1+import React, { useCallback, useState } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
3+import { Grid, Flex } from 'indigo-react';
4+
5+import { LocalRouterProvider } from 'lib/LocalRouter';
6+import useRouter from 'lib/useRouter';
7+
8+import Steps from 'components/Steps';
9+
10+import { usePointCursor } from 'store/pointCursor';
11+
12+import ResetConfirm from './ResetKeys/ResetConfirm';
13+import ResetDownload from './ResetKeys/ResetDownload';
14+import ResetVerify from './ResetKeys/ResetVerify';
15+import ResetExecute from './ResetKeys/ResetExecute';
16+
17+const NAMES = {
18+ CONFIRM: 'CONFIRM',
19+ DOWNLOAD: 'DOWNLOAD',
20+ VERIFY: 'VERIFY',
21+ EXECUTE: 'EXECUTE',
22+};
23+
24+const VIEWS = {
25+ CONFIRM: ResetConfirm,
26+ DOWNLOAD: ResetDownload,
27+ VERIFY: ResetVerify,
28+ EXECUTE: ResetExecute,
29+};
30+
31+const STEPS = [NAMES.CONFIRM, NAMES.DOWNLOAD, NAMES.VERIFY, NAMES.EXECUTE];
32+
33+export default function ResetKeys() {
34+ const { Route, ...router } = useRouter({
35+ names: NAMES,
36+ views: VIEWS,
37+ initialRoutes: [{ key: NAMES.CONFIRM }],
38+ });
39+
40+ const [newWallet, _setNewWallet] = useState(Nothing());
41+
42+ const setNewWallet = useCallback(
43+ (wallet, paper) => {
44+ _setNewWallet(
45+ Just({
46+ wallet,
47+ paper,
48+ })
49+ );
50+ },
51+ [_setNewWallet]
52+ );
53+
54+ return (
55+ <LocalRouterProvider value={router}>
56+ <Grid>
57+ <Grid.Item full as={Flex} row align="center">
58+ <Flex.Item as={Steps} num={router.size} total={STEPS.length} />
59+ </Grid.Item>
60+ </Grid>
61+ <Route newWallet={newWallet} setNewWallet={setNewWallet} />
62+ </LocalRouterProvider>
63+ );
64+}
src/views/UrbitID/ResetKeys/ResetConfirm.jsnew file+91
@@ -0,0 +1,91 @@
1+import React, { useState, useCallback } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
3+import { Grid, Text } from 'indigo-react';
4+import * as need from 'lib/need';
5+
6+import { usePointCursor } from 'store/pointCursor';
7+
8+import useLifecycle from 'lib/useLifecycle';
9+import { generateWallet } from 'lib/invite';
10+import { useLocalRouter } from 'lib/LocalRouter';
11+import { isDevelopment } from 'lib/flags';
12+
13+import { ForwardButton } from 'components/Buttons';
14+import PaperBuilder from 'components/PaperBuilder';
15+import WarningBox from 'components/WarningBox';
16+import { blinkIf } from 'components/Blinky';
17+import useCurrentPointName from 'lib/useCurrentPointName';
18+
19+export default function ResetConfirm({ newWallet, setNewWallet }) {
20+ const { push, names } = useLocalRouter();
21+ const { pointCursor } = usePointCursor();
22+ const point = need.point(pointCursor);
23+ const name = useCurrentPointName();
24+
25+ const [generatedWallet, setGeneratedWallet] = useState(Nothing());
26+
27+ // generating new wallet on mount, conveniently using it to 'force' users
28+ // to read the disclaimer text
29+ //TODO also check we can pay for the transactions?
30+ useLifecycle(() => {
31+ let mounted = true;
32+ (async () => {
33+ if (Just.hasInstance(newWallet)) {
34+ // skip if already computed (hitting the back button for example)
35+ return;
36+ }
37+
38+ const wal = await generateWallet(point);
39+ if (isDevelopment) {
40+ console.log(`The new ticket for ${name} is ${wal.ticket}`);
41+ }
42+ if (!mounted) {
43+ return;
44+ }
45+
46+ setGeneratedWallet(Just(wal));
47+ })();
48+
49+ return () => (mounted = false);
50+ });
51+
52+ const goDownload = useCallback(() => push(names.DOWNLOAD), [push, names]);
53+
54+ const paperRenderer = generatedWallet.matchWith({
55+ Nothing: () => null,
56+ Just: ({ value: wallet }) => (
57+ <PaperBuilder
58+ point={point}
59+ wallets={[wallet]}
60+ callback={paper => {
61+ setNewWallet(wallet, paper);
62+ }}
63+ />
64+ ),
65+ });
66+
67+ return (
68+ <Grid className="mt4">
69+ <Grid.Item full as={Text}>
70+ Reticketing is the process of generating a completely fresh wallet and
71+ transferring ownership of your point to that wallet.
72+ </Grid.Item>
73+ <Grid.Item full as={WarningBox} className="mt4">
74+ Beware, this resets your proxy addresses; if you're using smart
75+ contracts, this might break them! It will also change your networking
76+ keys!
77+ </Grid.Item>
78+ <Grid.Item
79+ full
80+ as={ForwardButton}
81+ solid
82+ className="mt4"
83+ accessory={blinkIf(Nothing.hasInstance(newWallet))}
84+ disabled={Nothing.hasInstance(newWallet)}
85+ onClick={goDownload}>
86+ I understand, continue
87+ </Grid.Item>
88+ {paperRenderer}
89+ </Grid>
90+ );
91+}
src/views/UrbitID/ResetKeys/ResetDownload.jsnew file+39
@@ -0,0 +1,39 @@
1+import React, { useState, useCallback } from 'react';
2+import { Nothing } from 'folktale/maybe';
3+import { Grid, Text } from 'indigo-react';
4+
5+import { useLocalRouter } from 'lib/LocalRouter';
6+import { downloadWallet } from 'lib/invite';
7+
8+import { ForwardButton, DownloadButton } from 'components/Buttons';
9+
10+export default function ReticketDownload({ newWallet }) {
11+ const { push, names } = useLocalRouter();
12+
13+ const [downloaded, setDownloaded] = useState(false);
14+
15+ const download = () => {
16+ downloadWallet(newWallet.value.paper);
17+ setDownloaded(true);
18+ };
19+
20+ const goVerify = useCallback(() => push(names.VERIFY), [push, names]);
21+
22+ return (
23+ <Grid className="mt4">
24+ <Grid.Item full as={Text}>
25+ Download the new passport, and keep it somewhere safe!
26+ </Grid.Item>
27+ <Grid.Item
28+ full
29+ as={!downloaded ? DownloadButton : ForwardButton}
30+ className="mt4"
31+ disabled={Nothing.hasInstance(newWallet)}
32+ onClick={!downloaded ? download : goVerify}
33+ success={downloaded}
34+ solid>
35+ {!downloaded ? 'Download Passport' : 'Continue'}
36+ </Grid.Item>
37+ </Grid>
38+ );
39+}
src/views/UrbitID/ResetKeys/ResetExecute.jsnew file+193
@@ -0,0 +1,193 @@
1+import React, { useState, useCallback } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
3+import { Grid, H4, Text, ErrorText } from 'indigo-react';
4+
5+import {
6+ reticketPointBetweenWallets,
7+ TRANSACTION_PROGRESS,
8+} from 'lib/reticket';
9+import * as need from 'lib/need';
10+import useLifecycle from 'lib/useLifecycle';
11+import { WALLET_TYPES } from 'lib/wallet';
12+import convertToInt from 'lib/convertToInt';
13+import useBlockWindowClose from 'lib/useBlockWindowClose';
14+
15+import { useNetwork } from 'store/network';
16+import { useWallet } from 'store/wallet';
17+import { usePointCursor } from 'store/pointCursor';
18+import { usePointCache } from 'store/pointCache';
19+import { useHistory } from 'store/history';
20+
21+import { RestartButton, ForwardButton } from 'components/Buttons';
22+import WarningBox from 'components/WarningBox';
23+import LoadingBar from 'components/LoadingBar';
24+import NeedFundsNotice from 'components/NeedFundsNotice';
25+
26+const labelForProgress = progress => {
27+ if (progress <= 0) {
28+ return 'Starting...';
29+ } else if (progress <= TRANSACTION_PROGRESS.GENERATING) {
30+ return 'Generating Ethereum Transactions...';
31+ } else if (progress <= TRANSACTION_PROGRESS.SIGNING) {
32+ return 'Signing Ethereum Transactions...';
33+ } else if (progress <= TRANSACTION_PROGRESS.FUNDING) {
34+ return 'Funding Ethereum Transactions...';
35+ } else if (progress <= TRANSACTION_PROGRESS.TRANSFERRING) {
36+ return 'Transferring ID...';
37+ } else if (progress <= TRANSACTION_PROGRESS.CLEANING) {
38+ return 'Cleaning Up...';
39+ } else if (progress <= TRANSACTION_PROGRESS.DONE) {
40+ return 'Done';
41+ }
42+};
43+
44+export default function ResetExecute({ newWallet, setNewWallet }) {
45+ const { popTo, names, reset } = useHistory();
46+ const { web3, contracts, networkType } = useNetwork();
47+ const {
48+ wallet,
49+ setWalletType,
50+ resetWallet,
51+ setUrbitWallet,
52+ walletType,
53+ walletHdPath,
54+ } = useWallet();
55+ const { pointCursor } = usePointCursor();
56+ const { getDetails } = usePointCache();
57+
58+ const [generalError, setGeneralError] = useState();
59+ const [progress, setProgress] = useState(0);
60+ const [needFunds, setNeedFunds] = useState();
61+ const isDone = progress >= 1.0;
62+
63+ useBlockWindowClose();
64+
65+ // start reticketing transactions on mount
66+ useLifecycle(() => {
67+ (async () => {
68+ const point = need.point(pointCursor);
69+ const details = need.details(getDetails(point));
70+ const networkRevision = convertToInt(details.keyRevisionNumber, 10);
71+ try {
72+ await reticketPointBetweenWallets({
73+ fromWallet: need.wallet(wallet),
74+ fromWalletType: walletType,
75+ fromWalletHdPath: walletHdPath,
76+ toWallet: newWallet.value.wallet,
77+ point: point,
78+ web3: need.web3(web3),
79+ contracts: need.contracts(contracts),
80+ networkType,
81+ onUpdate: handleUpdate,
82+ nextRevision: networkRevision + 1,
83+ });
84+ } catch (err) {
85+ console.error(err);
86+ setGeneralError(err);
87+ }
88+ })();
89+ });
90+
91+ const goToRestart = useCallback(() => {
92+ reset();
93+ setNewWallet(Nothing());
94+ }, [reset, setNewWallet]);
95+
96+ const loginAndGoHome = useCallback(() => {
97+ // set wallet state
98+ resetWallet();
99+ setWalletType(WALLET_TYPES.TICKET);
100+ setUrbitWallet(Just(newWallet.value.wallet));
101+ // (implicit) pointCursor stays the same
102+ // rediect to point
103+ popTo(names.POINT);
104+ }, [resetWallet, setWalletType, setUrbitWallet, newWallet, popTo, names]);
105+
106+ const handleUpdate = useCallback(
107+ ({ type, state, value }) => {
108+ switch (type) {
109+ case 'progress':
110+ return setProgress(state);
111+ case 'askFunding':
112+ return setNeedFunds(value);
113+ case 'gotFunding':
114+ return setNeedFunds(false);
115+ default:
116+ console.error(`Unknown update: ${type}`);
117+ }
118+ },
119+ [setProgress, setNeedFunds]
120+ );
121+
122+ const renderAdditionalInfo = () => {
123+ if (generalError) {
124+ console.log(generalError);
125+ generalError.message = generalError.message || 'Something went wrong!';
126+ return (
127+ <>
128+ <Grid.Item full className="mt4">
129+ <ErrorText>{generalError.message.toString()}</ErrorText>
130+ </Grid.Item>
131+ <Grid.Item
132+ full
133+ className="mt4"
134+ as={RestartButton}
135+ solid
136+ onClick={goToRestart}>
137+ Restart
138+ </Grid.Item>
139+ </>
140+ );
141+ }
142+
143+ if (needFunds) {
144+ return (
145+ <Grid.Item full as={NeedFundsNotice} className="mt4" {...needFunds} />
146+ );
147+ }
148+
149+ return (
150+ <Grid.Item full as={WarningBox} className="mt4">
151+ Never give your Master Ticket to anyone
152+ </Grid.Item>
153+ );
154+ };
155+
156+ return (
157+ <Grid className="mt4">
158+ <Grid.Item full as={H4}>
159+ {labelForProgress(progress)}
160+ </Grid.Item>
161+
162+ {isDone ? (
163+ <>
164+ <Grid.Item full as={Text} className="mt4">
165+ Your changes are now reflected on-chain and you can use the new
166+ ticket to manage your point.
167+ </Grid.Item>
168+ <Grid.Item
169+ full
170+ as={ForwardButton}
171+ solid
172+ className="mt4"
173+ accessory="↺"
174+ onClick={loginAndGoHome}>
175+ Login with New Master Ticket
176+ </Grid.Item>
177+ </>
178+ ) : (
179+ <>
180+ <Grid.Item full as={Grid} className="mt4" gap={3}>
181+ <Grid.Item full as={LoadingBar} progress={progress} />
182+ <Grid.Item full as={Text} className="f5 green4">
183+ This process can take up to 5 minutes to complete. Don't close
184+ this page until the process is complete.
185+ </Grid.Item>
186+ </Grid.Item>
187+
188+ {renderAdditionalInfo()}
189+ </>
190+ )}
191+ </Grid>
192+ );
193+}
src/views/UrbitID/ResetKeys/ResetVerify.jsnew file+71
@@ -0,0 +1,71 @@
1+import React, { useCallback, useMemo } from 'react';
2+import { Text, Grid, CheckboxInput } from 'indigo-react';
3+
4+import { useLocalRouter } from 'lib/LocalRouter';
5+import { validateExactly } from 'lib/validators';
6+import { isDevelopment } from 'lib/flags';
7+
8+import BridgeForm from 'form/BridgeForm';
9+import { TicketInput } from 'form/Inputs';
10+import { composeValidator, buildPatqValidator } from 'form/validators';
11+import SubmitButton from 'form/SubmitButton';
12+import FormError from 'form/FormError';
13+
14+const STUB_VERIFY_TICKET = isDevelopment;
15+
16+export default function ResetVerify({ newWallet }) {
17+ const { push, names } = useLocalRouter();
18+
19+ const ticket = newWallet.value.wallet.ticket;
20+ const validate = useMemo(
21+ () =>
22+ composeValidator({
23+ ticket: buildPatqValidator([
24+ validateExactly(ticket, 'Does not match expected master ticket.'),
25+ ]),
26+ }),
27+ [ticket]
28+ );
29+
30+ const initialValues = useMemo(
31+ () => ({
32+ ticket: STUB_VERIFY_TICKET ? ticket : undefined,
33+ }),
34+ [ticket]
35+ );
36+
37+ const goExecute = useCallback(() => push(names.EXECUTE), [push, names]);
38+
39+ return (
40+ <Grid className="mt4">
41+ <Grid.Item full as={Text}>
42+ Verify New Master Ticket
43+ </Grid.Item>
44+ <BridgeForm
45+ validate={validate}
46+ afterSubmit={goExecute}
47+ initialValues={initialValues}>
48+ {({ handleSubmit, values }) => (
49+ <>
50+ <Grid.Item
51+ full
52+ as={TicketInput}
53+ className="mt4"
54+ type={values.showTicket ? 'text' : 'password'}
55+ name="ticket"
56+ label="New master ticket"
57+ />
58+
59+ <Grid.Item full as={CheckboxInput} name="showTicket" label="Show" />
60+
61+ <Grid.Item full as={FormError} />
62+
63+ <Grid.Item full as={SubmitButton} handleSubmit={handleSubmit}>
64+ Verify & Reticket
65+ </Grid.Item>
66+ </>
67+ )}
68+ </BridgeForm>
69+ </Grid>
70+ );
71+}
src/views/UrbitOS.js+10−10
@@ -9,6 +9,7 @@ import { LocalRouterProvider } from 'lib/LocalRouter';
99
1010import View from 'components/View';
1111import Crumbs from 'components/Crumbs';
12+import NavHeader from 'components/NavHeader';
1213
1314import UrbitOSHome from './UrbitOS/Home';
1415import UrbitOSNetworkingKeys from './UrbitOS/NetworkingKeys';
@@ -38,17 +39,16 @@ export default function UrbitOS() {
3839
3940 return (
4041 <View pop={router.pop} inset>
42+ <NavHeader>
43+ <Crumbs
44+ routes={[
45+ { text: name, action: () => history.pop() },
46+ { text: 'Urbit OS' },
47+ ]}
48+ />
49+ </NavHeader>
4150 <LocalRouterProvider value={router}>
42− <Grid className="mb4">
43− <Grid.Item
44− full
45− as={Crumbs}
46− routes={[
47− { text: name, action: () => history.pop() },
48− { text: 'Urbit OS' },
49− ]}
50− />
51− </Grid>
51+ <Grid className="mb4"></Grid>
5252 <Route />
5353 </LocalRouterProvider>
5454 </View>
src/views/UrbitOS/Home.js+58−64
@@ -1,5 +1,6 @@
11import React, { useCallback, useState, useEffect } from 'react';
2−import { Grid, Button, SelectInput } from 'indigo-react';
2+import cn from 'classnames';
3+import { Grid, Button, SelectInput, Flex } from 'indigo-react';
34import * as ob from 'urbit-ob';
45import { azimuth } from 'azimuth-js';
56
@@ -10,14 +11,10 @@ import useCurrentPointName from 'lib/useCurrentPointName';
1011import useKeyfileGenerator from 'lib/useKeyfileGenerator';
1112import * as need from 'lib/need';
1213
13−import {
14− OutButton,
15− ForwardButton,
16− BootUrbitOSButton,
17− GenerateButton,
18−} from 'components/Buttons';
14+import { OutButton, ForwardButton } from 'components/Buttons';
1915import CopyButton from 'components/CopyButton';
2016import NetworkingKeys from 'components/NetworkingKeys';
17+import ProgressButton from 'components/ProgressButton';
2118
2219import BridgeForm from 'form/BridgeForm';
2320import { useLocalRouter } from 'lib/LocalRouter';
@@ -97,9 +94,27 @@ export default function UrbitOSHome() {
9794function Hosting() {
9895 const bind = useKeyfileGenerator();
9996 const { keyfile, code, notice } = bind;
100− const ship = { connected: true }; //useHosting();
101−
102− const { syncStatus } = ship;
97+ const ship = useHosting();
98+
99+ const {
100+ syncStatus,
101+ url,
102+ unknown,
103+ running,
104+ sysEvents,
105+ runEvents,
106+ newEvents,
107+ bootProgress,
108+ bootMessage,
109+ } = ship;
110+
111+ const [synced, setSynced] = useState(false);
112+ useEffect(() => {
113+ if (unknown && !synced) {
114+ syncStatus();
115+ setSynced(true);
116+ }
117+ }, [syncStatus, unknown, setSynced, synced]);
103118
104119 const createShip = useCallback(() => ship.create(keyfile), [keyfile, ship]);
105120
@@ -109,10 +124,10 @@ function Hosting() {
109124 const props = { name, options, ship };
110125
111126 const renderMain = useCallback(() => {
112− if (ship.connected) {
127+ if (ship.running) {
113128 return (
114129 <>
115− <Grid.Item cols={[1, 9]} as={OutButton} solid success>
130+ <Grid.Item cols={[1, 9]} as={OutButton} solid success href={url}>
116131 Open OS
117132 </Grid.Item>
118133 <Grid.Item cols={[9, 13]} as={Button} className="b-black b1" center>
@@ -121,47 +136,52 @@ function Hosting() {
121136 </>
122137 );
123138 }
124− if (ship.missing || ship.booting) {
139+ if (ship.missing) {
125140 return (
126141 <Grid.Item
127142 full
128− as={GenerateButton}
143+ as={ForwardButton}
129144 solid
130− success
131− disabled={ship.booting || !keyfile}
132− loading={ship.booting}
145+ disabled={!keyfile}
133146 onClick={createShip}>
134− Boot Urbit OS
147+ Connect
135148 </Grid.Item>
136149 );
137150 }
138− if (ship.running || ship.connecting) {
139− return <HostingDisconnected {...props} />;
151+ if (ship.pending) {
152+ return (
153+ <Grid.Item
154+ full
155+ as={ProgressButton}
156+ success
157+ disabled
158+ progress={bootProgress}>
159+ {bootMessage || 'Connecting'}
160+ </Grid.Item>
161+ );
140162 }
141163 }, [
142− ship.connected,
143164 ship.missing,
144− ship.booting,
145165 ship.running,
146− ship.connecting,
166+ ship.pending,
167+ bootProgress,
168+ bootMessage,
147169 keyfile,
148170 createShip,
149− props,
171+ url,
150172 ]);
151173
152174 const renderDetails = useCallback(() => {
153− if (ship.connected) {
175+ if (ship.running) {
154176 return (
155177 <>
156− <Grid.Item full>Status: Connected</Grid.Item>
157178 <Grid.Item cols={[1, 9]} className="gray4">
158− <span className="mono">{name}</span> is connected to Tlon and has
159− the IP address 127.0.0.1
179+ <span className="mono">{name}</span> is connected to Tlon
160180 </Grid.Item>
161181 </>
162182 );
163183 }
164− if (ship.running || ship.connected) {
184+ if (ship.missing) {
165185 return (
166186 <>
167187 <Grid.Item full className="gray4">
@@ -169,10 +189,6 @@ function Hosting() {
169189 <br />
170190 <span className="f6">Last connected: 4m ago</span>
171191 </Grid.Item>
172−
173− <Grid.Item full as={ForwardButton} solid>
174− Connect
175− </Grid.Item>
176192 </>
177193 );
178194 }
@@ -181,8 +197,15 @@ function Hosting() {
181197 return (
182198 <>
183199 <Grid gap={4}>
184− <Grid.Item full className="f5">
185− Urbit OS
200+ <Grid.Item full className="f5" as={Flex}>
201+ <Flex.Item>Urbit OS </Flex.Item>
202+ <Flex.Item
203+ className={cn(
204+ { green3: ship.running, grey4: !ship.running },
205+ 'ml3'
206+ )}>
207+ {ship.running ? 'Connected' : 'Disconnected'}
208+ </Flex.Item>
186209 </Grid.Item>
187210 <BridgeForm initialValues={{ provider: 'tlon' }}>
188211 {() => (
@@ -224,32 +247,3 @@ function Hosting() {
224247 </>
225248 );
226249}
227−
228−function HostingDisconnected({ options }) {
229− return (
230− <BridgeForm initialValues={{ provider: 'tlon' }}>
231− {() => (
232− <>
233− <Grid.Item full className="gray4">
234− <span className="gray3">Status:</span> Disconnected
235− <br />
236− <span className="f6">Last connected: 4m ago</span>
237− </Grid.Item>
238−
239− <Grid.Item full as={ForwardButton} solid>
240− Connect
241− </Grid.Item>
242−
243− <Grid.Item
244− full
245− as={SelectInput}
246− name="provider"
247− label="Host Provider"
248− options={options}
249− disabled
250− />
251− </>
252− )}
253− </BridgeForm>
254− );
255−}