on master help sign in
Implement minimum viable reticketing
commita2a5eedcb775b5937aa6ca2a6e8d7718d415a815 authorFang <git@fang.io> dateFri 28 Jun 2019, 15:19 (7 years ago) parent1cfc674
7 files changed+347
src/lib/routeNames.js+1 src/lib/router.js+4 src/views/Reticket.js+101 src/views/Reticket/Confirm.js+66 src/views/Reticket/DoReticket.js+98 src/views/Reticket/Download.js+37 src/views/Reticket/Verify.js+40
src/lib/routeNames.js+1
@@ -8,6 +8,7 @@ export const ROUTE_NAMES = {
88 //
99 LOGIN: Symbol('LOGIN'),
1010 ADMIN: Symbol('ADMIN'),
11+ RETICKET: Symbol('RETICKET'),
1112 //
1213 ACCEPT_TRANSFER: Symbol('ACCEPT_TRANSFER'),
1314 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
src/lib/router.js+4
@@ -6,6 +6,7 @@ import Login from '../views/Login.js';
66import InviteTicket from '../views/InviteTicket';
77import InvitesManage from '../views/InvitesManage.js';
88import Admin from '../views/Admin';
9+import Reticket from '../views/Reticket';
910import AcceptTransfer from '../views/AcceptTransfer';
1011import CancelTransfer from '../views/CancelTransfer';
1112import CreateGalaxy from '../views/CreateGalaxy';
@@ -34,6 +35,7 @@ export const ROUTES = {
3435 [ROUTE_NAMES.INVITES_MANAGE]: InvitesManage,
3536 [ROUTE_NAMES.LOGIN]: Login,
3637 [ROUTE_NAMES.ADMIN]: Admin,
38+ [ROUTE_NAMES.RETICKET]: Reticket,
3739 [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
3840 [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
3941 [ROUTE_NAMES.VIEW_POINT]: ViewPoint,
@@ -64,6 +66,8 @@ export const getRouteBreadcrumb = ({ pointCursor }) => route => {
6466 return 'Login to Bridge';
6567 case ROUTE_NAMES.ADMIN:
6668 return 'Admin';
69+ case ROUTE_NAMES.RETICKET:
70+ return 'Reticket';
6771 case ROUTE_NAMES.POINT:
6872 return `Overview: ${ob.patp(need.pointCursor(pointCursor))}`;
6973 case ROUTE_NAMES.POINTS:
src/views/Reticket.jsnew file+101
@@ -0,0 +1,101 @@
1+import React, { useState } from 'react';
2+import { size } from 'lodash';
3+import * as need from 'lib/need';
4+import ob from 'urbit-ob';
5+import { Just, Nothing } from 'folktale/maybe';
6+import { Grid } from 'indigo-react';
7+
8+import { usePointCursor } from 'store/pointCursor';
9+// import { usePointCache } from 'store/pointCache';
10+
11+import View from 'components/View';
12+import MiniBackButton from 'components/MiniBackButton';
13+import { LocalRouterProvider } from 'lib/LocalRouter';
14+import Crumbs from 'components/Crumbs';
15+
16+import useRouter from 'lib/useRouter';
17+
18+import { useHistory } from 'store/history';
19+
20+import Confirm from './Reticket/Confirm';
21+import Download from './Reticket/Download';
22+import Verify from './Reticket/Verify';
23+import DoReticket from './Reticket/DoReticket';
24+
25+const STEP_NAMES = {
26+ CONFIRM: 'CONFIRM',
27+ DOWNLOAD: 'DOWNLOAD',
28+ VERIFY: 'VERIFY',
29+ RETICKET: 'RETICKET',
30+};
31+
32+const STEP_VIEWS = {
33+ CONFIRM: Confirm,
34+ DOWNLOAD: Download,
35+ VERIFY: Verify,
36+ RETICKET: DoReticket,
37+};
38+
39+const STEP_MAX = size(STEP_NAMES);
40+
41+export default function Reticket() {
42+ const history = useHistory();
43+ const { Route, ...innerRouter } = useRouter({
44+ names: STEP_NAMES,
45+ views: STEP_VIEWS,
46+ initialRoutes: [{ key: STEP_NAMES.CONFIRM }],
47+ });
48+ const { pointCursor } = usePointCursor();
49+ const point = need.pointCursor(pointCursor);
50+ const name = ob.patp(point);
51+
52+ const [newWallet, setNewWallet] = useState(Nothing());
53+
54+ //TODO step # from Route?
55+ const stepNumber = 1;
56+
57+ const storeNewWallet = (wallet, paper) => {
58+ setNewWallet(
59+ Just({
60+ wallet,
61+ paper,
62+ })
63+ );
64+ };
65+
66+ const completed = () => {
67+ history.pop();
68+ };
69+
70+ return (
71+ <LocalRouterProvider value={innerRouter}>
72+ <View>
73+ <Crumbs
74+ routes={[
75+ {
76+ text: name,
77+ action: () => {
78+ history.pop();
79+ return history.pop();
80+ },
81+ },
82+ {
83+ text: 'Admin',
84+ action: () => history.pop(),
85+ },
86+ ]}
87+ />
88+ <MiniBackButton onClick={innerRouter.pop} />
89+ Step {stepNumber} of {STEP_MAX}
90+ <Route
91+ STEP_NAMES={STEP_NAMES}
92+ newWallet={newWallet}
93+ //
94+ storeNewWallet={storeNewWallet}
95+ //
96+ completed={completed}
97+ />
98+ </View>
99+ </LocalRouterProvider>
100+ );
101+}
src/views/Reticket/Confirm.jsnew file+66
@@ -0,0 +1,66 @@
1+import React, { useState } from 'react';
2+import * as need from 'lib/need';
3+import { Just, Nothing } from 'folktale/maybe';
4+
5+import PaperCollateralRenderer from 'PaperCollateralRenderer';
6+
7+import { usePointCursor } from 'store/pointCursor';
8+
9+import View from 'components/View';
10+import Passport from 'components/Passport';
11+import { ForwardButton, DownloadButton } from 'components/Buttons';
12+
13+import { useWallet } from 'store/wallet';
14+import useLifecycle from 'lib/useLifecycle';
15+import { generateWallet } from 'lib/invite';
16+import { useLocalRouter } from 'lib/LocalRouter';
17+
18+import { isDevelopment } from 'lib/flags';
19+
20+export default function Confirm({ STEP_NAMES, newWallet, storeNewWallet }) {
21+ const { push } = useLocalRouter();
22+ const { pointCursor } = usePointCursor();
23+ const point = need.pointCursor(pointCursor);
24+
25+ const [generatedWallet, setGeneratedWallet] = useState(Nothing());
26+
27+ // start generating new wallet on mount
28+ //TODO also check we can pay for the transactions?
29+ useLifecycle(() => {
30+ generateWallet(point).then(wal => {
31+ if (isDevelopment) {
32+ console.log('ticket for', point, wal.ticket);
33+ }
34+ setGeneratedWallet(Just(wal));
35+ });
36+ });
37+
38+ const next = () => {
39+ push(STEP_NAMES.DOWNLOAD);
40+ };
41+
42+ const paperRenderer = Nothing.hasInstance(generatedWallet) ? null : (
43+ <PaperCollateralRenderer
44+ wallet={{ [point]: generatedWallet.value }}
45+ className={'extremely-hidden'}
46+ callback={paper => {
47+ console.log('done rendering');
48+ storeNewWallet(generatedWallet.value, paper);
49+ }}
50+ mode={'REGISTRATION'}
51+ />
52+ );
53+
54+ return (
55+ <View>
56+ Maybe we should just duplicate the Login component here? Not everyone who
57+ does this will be logged in with an existing ticket. Also, beware, this
58+ changes your proxy addresses. If you're using smart contracts, this might
59+ break them! It will also change your networking keys!
60+ <ForwardButton disabled={Nothing.hasInstance(newWallet)} onClick={next}>
61+ I understand, continue
62+ </ForwardButton>
63+ {paperRenderer}
64+ </View>
65+ );
66+}
src/views/Reticket/DoReticket.jsnew file+98
@@ -0,0 +1,98 @@
1+import React, { useState } from 'react';
2+import { Just } from 'folktale/maybe';
3+
4+import View from 'components/View';
5+import { ForwardButton } from 'components/Buttons';
6+import { Warning } from 'components/old/Base';
7+
8+import { TRANSACTION_STATES, startTransactions } from 'lib/invite';
9+
10+import { useNetwork } from 'store/network';
11+import { useWallet } from 'store/wallet';
12+import { usePointCursor } from 'store/pointCursor';
13+import useLifecycle from 'lib/useLifecycle';
14+
15+export default function DoReticket({ newWallet, completed }) {
16+ const { web3, contracts } = useNetwork();
17+ const { wallet, setUrbitWallet } = useWallet();
18+ const { pointCursor } = usePointCursor();
19+
20+ const [transactionProgress, setTransactionProgress] = useState(
21+ TRANSACTION_STATES.GENERATING
22+ );
23+ const [errors, setErrors] = useState([]);
24+
25+ // start reticketing transactions on mount
26+ useLifecycle(() => {
27+ startTransactions({
28+ realPointM: pointCursor,
29+ web3,
30+ contracts,
31+ inviteWalletM: wallet,
32+ realWalletM: Just(newWallet.value.wallet),
33+ setUrbitWallet,
34+ updateProgress,
35+ });
36+ });
37+
38+ const updateProgress = notification => {
39+ if (notification.type === 'progress') {
40+ setTransactionProgress(notification.value);
41+ } else if (notification.type === 'notify') {
42+ setErrors([notification.value]);
43+ }
44+ };
45+
46+ const next = () => {
47+ completed();
48+ };
49+
50+ const getErrors = () => {
51+ if (errors.length === 0) return null;
52+
53+ let errorElems = errors.map(e => <span>{e}</span>);
54+
55+ return (
56+ <Warning>
57+ <h3 className={'mb-2'}>{'Warning'}</h3>
58+ {errorElems}
59+ </Warning>
60+ );
61+ };
62+
63+ const errorDisplay = getErrors();
64+
65+ return (
66+ <View>
67+ {errorDisplay}
68+ {transactionProgress.label === TRANSACTION_STATES.DONE.label ? (
69+ <div>
70+ <h1 className="fs-6 lh-8 mb-3">
71+ <span>Success</span>
72+ <span className="ml-4 green">✓</span>
73+ </h1>
74+ <ForwardButton onClick={next}>Done!</ForwardButton>
75+ </div>
76+ ) : (
77+ <div>
78+ <h1 className="fs-6 lh-8 mb-3">Submitting</h1>
79+ <p className="mt-4 mb-4">
80+ This step can take up to five minutes. Please do not leave this page
81+ until the transactions are complete.
82+ </p>
83+ <div className="passport-progress mb-2">
84+ <div
85+ className="passport-progress-filled"
86+ style={{ width: transactionProgress.pct }}
87+ />
88+ </div>
89+ <div className="flex justify-between">
90+ <div className="text-mono text-sm lh-6 green-dark">
91+ {transactionProgress.label}
92+ </div>
93+ </div>
94+ </div>
95+ )}
96+ </View>
97+ );
98+}
src/views/Reticket/Download.jsnew file+37
@@ -0,0 +1,37 @@
1+import React, { useState } from 'react';
2+import { Nothing } from 'folktale/maybe';
3+
4+import View from 'components/View';
5+import { ForwardButton, DownloadButton } from 'components/Buttons';
6+
7+import { useLocalRouter } from 'lib/LocalRouter';
8+import { downloadWallet } from 'lib/invite';
9+
10+export default function Download({ STEP_NAMES, newWallet }) {
11+ const { push } = useLocalRouter();
12+
13+ const [hasDownloaded, setHasDownloaded] = useState(false);
14+
15+ const doDownload = () => {
16+ downloadWallet(newWallet.value.paper);
17+ setHasDownloaded(true);
18+ };
19+
20+ const next = () => {
21+ push(STEP_NAMES.VERIFY);
22+ };
23+
24+ return (
25+ <View>
26+ Download the thing, and keep it somewhere safe!
27+ <DownloadButton
28+ disabled={Nothing.hasInstance(newWallet)}
29+ onClick={doDownload}>
30+ Download
31+ </DownloadButton>
32+ <ForwardButton disabled={!hasDownloaded} onClick={next}>
33+ Continue
34+ </ForwardButton>
35+ </View>
36+ );
37+}
src/views/Reticket/Verify.jsnew file+40
@@ -0,0 +1,40 @@
1+import React from 'react';
2+import { Input, AccessoryIcon } from 'indigo-react';
3+
4+import View from 'components/View';
5+import { useTicketInput } from 'components/Inputs';
6+import { ForwardButton } from 'components/Buttons';
7+
8+import { useLocalRouter } from 'lib/LocalRouter';
9+
10+export default function Verify({ STEP_NAMES, newWallet }) {
11+ const { push } = useLocalRouter();
12+
13+ const ticketInput = useTicketInput({
14+ name: 'ticket',
15+ label: 'New master ticket',
16+ });
17+ const ticket = ticketInput.data;
18+
19+ const next = () => {
20+ push(STEP_NAMES.RETICKET);
21+ };
22+
23+ const matches = newWallet.value.wallet.ticket === ticket;
24+
25+ const displayTicketStatus = matches ? (
26+ <AccessoryIcon.Success />
27+ ) : (
28+ <AccessoryIcon.Failure />
29+ );
30+
31+ return (
32+ <View>
33+ Prove that you downloaded the new secrets!
34+ <Input {...ticketInput} accessory={displayTicketStatus} />
35+ <ForwardButton disabled={!matches} onClick={next}>
36+ Reticket
37+ </ForwardButton>
38+ </View>
39+ );
40+}