on master help sign in
feat: indigo-ify reticket component
commitd1230bb5a4deba1aad6628c3061da63b6989f10e authorMatt Condon (shrugs) <mattgcondon@gmail.com> dateTue 9 Jul 2019, 13:34 (7 years ago) parentd760cbf
21 files changed+420−404
src/components/Steps.js+2−4 src/indigo-react/components/Button.js+4−4 src/indigo-react/components/IconButton.js−1 src/lib/constants.js+5 src/lib/invite.js+15−15 src/lib/tank.js+12−12 src/lib/txn.js+1−3 src/lib/useRouter.js+39−17 src/views/Activate/PassportDownload.js+33−32 src/views/Activate/PassportTransfer.js+21−24 src/views/Activate/PassportVerify.js+23−25 src/views/Activate/PassportView.js+27 src/views/Admin/AdminReticket.js+35−68 src/views/Admin/Reticket/ReticketConfirm.js+83 src/views/Admin/Reticket/ReticketDownload.js+38 src/views/Reticket/DoReticket.js → src/views/Admin/Reticket/ReticketExecute.js+68−39 src/views/Admin/Reticket/ReticketVerify.js+2−2 src/views/Point.js+12−19 src/views/Reticket/Confirm.js−63 src/views/Reticket/Download.js−35 src/views/Reticket/Verify.js−41
src/components/Steps.js+2−4
@@ -2,11 +2,9 @@ import React from 'react';
22import cn from 'classnames';
33import { Text } from 'indigo-react';
44
5−export default function Steps({ num = 1, total = 3, className }) {
6− // NOTE: the 48px must match the `mt8` class on Passport for things to line
7− // up nicely
5+export default function Steps({ num = 1, total = 3, className, ...rest }) {
86 return (
9− <Text className={cn('f5 gray3', className)} style={{ height: '48px' }}>
7+ <Text className={cn('f5 gray3', className)} {...rest}>
108 <Text className="f5 black">Step {num}</Text> of {total}
119 </Text>
1210 );
src/indigo-react/components/Button.js+4−4
@@ -48,12 +48,12 @@ export default function Button({
4848 }}
4949 onClick={!disabled && onClick ? onClick : undefined}
5050 {...rest}>
51− <Grid.Item as={Flex} justify="between" full>
52− <span className={cn(textColor)}>{children}</span>
53− <div className={cn('pl4', textColor)}>{accessory}</div>
51+ <Grid.Item full as={Flex} justify="between" className={cn(textColor)}>
52+ <span>{children}</span>
53+ <div className={cn('pl4')}>{accessory}</div>
5454 </Grid.Item>
5555 {detail && (
56− <Grid.Item as={HelpText} full>
56+ <Grid.Item full as={HelpText}>
5757 {detail}
5858 </Grid.Item>
5959 )}
src/indigo-react/components/IconButton.js−1
@@ -37,7 +37,6 @@ export default function IconButton({
3737 height: '1rem',
3838 fontSize: '1rem',
3939 fontWeight: 'bold',
40− // TODO: ^ replace with 1:1 svgs and remove explicit sizing
4140 ...(disabled && {
4241 pointerEvents: 'none',
4342 cursor: 'not-allowed',
src/lib/constants.js+5
@@ -1,3 +1,7 @@
1+import { isDevelopment } from './flags';
2+
3+const CHECK_BLOCK_EVERY_MS = isDevelopment ? 1000 : 5000;
4+
15const MIN_GALAXY = 0;
26const MAX_GALAXY = 255;
37const MIN_STAR = 256;
@@ -63,6 +67,7 @@ const PROFILE_STATES = {
6367};
6468
6569export {
70+ CHECK_BLOCK_EVERY_MS,
6671 GAS_LIMITS,
6772 GEN_STATES,
6873 BUTTON_STATES,
src/lib/invite.js+15−15
@@ -30,8 +30,8 @@ const TRANSACTION_STATES = {
3030 label: 'Funding Transactions...',
3131 progress: 0.3,
3232 },
33− CLAIMING: {
34− label: 'Claiming Invite...',
33+ TRANSFERRING: {
34+ label: 'Transferring Point...',
3535 progress: 0.55,
3636 },
3737 CLEANING: {
@@ -81,9 +81,9 @@ export async function downloadWallet(paper) {
8181 });
8282}
8383
84−export async function claimPointFromInvite({
85− inviteWallet,
86− wallet,
84+export async function reticketPointBetweenWallets({
85+ fromWallet,
86+ toWallet,
8787 point,
8888 web3,
8989 contracts,
@@ -99,7 +99,7 @@ export async function claimPointFromInvite({
9999 const gotFunding = () => onUpdate({ type: 'gotFunding' });
100100 const progress = state => onUpdate({ type: 'progress', state });
101101
102− const inviteAddress = inviteWallet.address;
102+ const inviteAddress = fromWallet.address;
103103
104104 //
105105 // generate transactions
@@ -124,7 +124,7 @@ export async function claimPointFromInvite({
124124
125125 // configure networking public keys
126126 const seed = await deriveNetworkSeedFromUrbitWallet(
127− Just(wallet),
127+ Just(toWallet),
128128 INITIAL_NETWORK_KEY_REVISION
129129 );
130130
@@ -152,7 +152,7 @@ export async function claimPointFromInvite({
152152 const managementTx = azimuth.ecliptic.setManagementProxy(
153153 contracts,
154154 point,
155− wallet.management.keys.address
155+ toWallet.management.keys.address
156156 );
157157 managementTx.gas = GAS_LIMITS.SET_PROXY;
158158
@@ -165,7 +165,7 @@ export async function claimPointFromInvite({
165165 const spawnTx = azimuth.ecliptic.setSpawnProxy(
166166 contracts,
167167 point,
168− wallet.spawn.keys.address
168+ toWallet.spawn.keys.address
169169 );
170170 spawnTx.gas = GAS_LIMITS.SET_PROXY;
171171 txs.push(spawnTx);
@@ -176,7 +176,7 @@ export async function claimPointFromInvite({
176176 const votingTx = azimuth.ecliptic.setVotingProxy(
177177 contracts,
178178 point,
179− wallet.voting.keys.address
179+ toWallet.voting.keys.address
180180 );
181181 votingTx.gas = GAS_LIMITS.SET_PROXY;
182182 txs.push(votingTx);
@@ -188,7 +188,7 @@ export async function claimPointFromInvite({
188188 const transferFinalTx = azimuth.ecliptic.transferPoint(
189189 contracts,
190190 point,
191− wallet.ownership.keys.address,
191+ toWallet.ownership.keys.address,
192192 false
193193 );
194194 transferFinalTx.gas = GAS_LIMITS.TRANSFER;
@@ -212,7 +212,7 @@ export async function claimPointFromInvite({
212212
213213 let txPairs = txs.map(tx => {
214214 let stx = new Tx(tx);
215− stx.sign(Buffer.from(inviteWallet.privateKey, 'hex'));
215+ stx.sign(Buffer.from(fromWallet.privateKey, 'hex'));
216216 return {
217217 raw: hexify(stx.serialize()),
218218 signed: stx,
@@ -239,7 +239,7 @@ export async function claimPointFromInvite({
239239 // sending and awaiting transactions
240240 //
241241
242− progress(TRANSACTION_STATES.CLAIMING);
242+ progress(TRANSACTION_STATES.TRANSFERRING);
243243
244244 await sendAndAwaitConfirm(web3, txPairs.map(p => Just(p.signed)), usedTank);
245245
@@ -257,14 +257,14 @@ export async function claimPointFromInvite({
257257 if (transferEth && balance > sendEthCost) {
258258 const value = balance - sendEthCost;
259259 const tx = {
260− to: wallet.ownership.keys.address,
260+ to: toWallet.ownership.keys.address,
261261 value: value,
262262 gasPrice: gasPrice,
263263 gas: gasLimit,
264264 nonce: inviteNonce++,
265265 };
266266 let stx = new Tx(tx);
267− stx.sign(inviteWallet.privateKey);
267+ stx.sign(fromWallet.privateKey);
268268 const rawTx = hexify(stx.serialize());
269269 web3.eth.sendSignedTransaction(rawTx).catch(err => {
270270 console.log('error sending value tx, who cares', err);
src/lib/tank.js+12−12
@@ -1,6 +1,7 @@
11// /lib/tank: functions for funding transactions
22
33import { waitForTransactionConfirm } from './txn';
4+import { CHECK_BLOCK_EVERY_MS } from './constants';
45
56//NOTE if accessing this in a localhost configuration fails with "CORS request
67// did not succeed", you might need to visit localhost:3001 or whatever
@@ -104,27 +105,26 @@ async function waitForBalance(
104105) {
105106 console.log('tank: awaiting balance', address, minBalance);
106107 return new Promise((resolve, reject) => {
107− let oldBalance = null;
108108 const checkForBalance = async () => {
109109 const balance = await web3.eth.getBalance(address);
110+
110111 if (balance >= minBalance) {
111− // if we ever asked for funding, retract that request now
112− if (gotFunding && oldBalance !== null) {
112+ if (gotFunding) {
113113 gotFunding();
114114 }
115+
115116 resolve();
116117 } else {
117− if (balance !== oldBalance) {
118− // TODO: minBalance is a `number` type
119− // but we want to display ETH, which will never accept a number
120− // but instead wants string or BN/BigNumber.
121− // this will be heavily refactored to correctly do BN math, but until
122− // then we'll manually convert this number to an integer string
123− askForFunding(address, minBalance.toFixed(), balance);
124− }
125− setTimeout(checkForBalance, 13000);
118+ // TODO: minBalance is a `number` type
119+ // but we want to display ETH, which will never accept a number
120+ // but instead wants string or BN/BigNumber.
121+ // this will be heavily refactored to correctly do BN math, but until
122+ // then we'll manually convert this number to an integer string
123+ askForFunding(address, minBalance.toFixed(), balance);
124+ setTimeout(checkForBalance, CHECK_BLOCK_EVERY_MS);
126125 }
127126 };
127+
128128 checkForBalance();
129129 });
130130}
src/lib/txn.js+1−3
@@ -8,9 +8,7 @@ import { NETWORK_TYPES } from './network';
88import { ledgerSignTransaction } from './ledger';
99import { trezorSignTransaction } from './trezor';
1010import { WALLET_TYPES, addHexPrefix } from './wallet';
11−
12−const CHECK_BLOCK_EVERY_MS =
13− process.env.NODE_ENV === 'development' ? 1000 : 5000;
11+import { CHECK_BLOCK_EVERY_MS } from './constants';
1412
1513const signTransaction = async config => {
1614 let {
src/lib/useRouter.js+39−17
@@ -1,5 +1,5 @@
1−import { useCallback, useMemo, useState, useEffect } from 'react';
2−import { last, includes as _includes } from 'lodash';
1+import { useCallback, useMemo, useState, useRef, useEffect } from 'react';
2+import { last, includes as _includes, findIndex } from 'lodash';
33
44/**
55 * @param primary whether or not this is the top-level router
@@ -15,6 +15,9 @@ export default function useRouter({
1515}) {
1616 const [routes, setRoutes] = useState(initialRoutes);
1717
18+ const oldPopState = useRef();
19+
20+ const size = routes.length;
1821 const push = useCallback(
1922 (key, data) => {
2023 if (!views[key]) {
@@ -46,12 +49,25 @@ export default function useRouter({
4649 [setRoutes, views]
4750 );
4851 const pop = useCallback(
49− (count = 1) =>
50− setRoutes(routes =>
51− routes.length > 1 ? [...routes.slice(0, routes.length - count)] : routes
52− ),
53− // ^ can't pop root route
54− [setRoutes]
52+ (count = 1) => {
53+ if (size > 1) {
54+ // pop as expected
55+ return setRoutes(routes => [...routes.slice(0, routes.length - count)]);
56+ }
57+
58+ // if we are at the root, pass this event to our parent
59+ if (oldPopState.current) {
60+ window.history.back();
61+ }
62+ },
63+ [size, setRoutes, oldPopState]
64+ );
65+ const popTo = useCallback(
66+ name => {
67+ const index = findIndex(routes, route => route.key === name);
68+ return pop(routes.length - 1 - index);
69+ },
70+ [pop, routes]
5571 );
5672 const peek = useCallback(() => last(routes), [routes]);
5773 const replaceWith = useCallback(routes => setRoutes(() => routes), [
@@ -64,7 +80,6 @@ export default function useRouter({
6480 const includes = useCallback(key => _includes(routes.map(r => r.key), key), [
6581 routes,
6682 ]);
67− const size = routes.length;
6883 const data = useMemo(() => last(routes).data, [routes]);
6984 const Route = useMemo(() => views[peek().key], [views, peek]);
7085
@@ -75,24 +90,30 @@ export default function useRouter({
7590
7691 // capture browser pop in primary router
7792 useEffect(() => {
78− const oldPopState = window.onpopstate;
79−
80− window.history.pushState(null, null, null);
93+ // store the previous onpopstate handler
94+ oldPopState.current = window.onpopstate;
8195
96+ // construct new onpopstate handler
8297 window.onpopstate = e => {
83− if (size <= 1) {
84− // if this is the root route, give the event to parent router
85− return oldPopState(e);
98+ if (size <= 1 && oldPopState.current) {
99+ // if this is the root route and there's a parent handler,
100+ // give the event to the handler
101+ return oldPopState.current(e);
86102 }
87103
104+ // on pop, tell the browser of a new state to avoid giving the user
105+ // the ability to go forward
88106 window.history.pushState(null, null, null);
107+
108+ // then update our local state for rendering
89109 pop();
90110 };
91111
92112 return () => {
93− window.onpopstate = oldPopState;
113+ // reset the onpopstate to the previous version
114+ window.onpopstate = oldPopState.current;
94115 };
95− }, [size, pop]);
116+ }, [size, pop, oldPopState]);
96117
97118 return {
98119 Route,
@@ -101,6 +122,7 @@ export default function useRouter({
101122 push,
102123 popAndPush,
103124 pop,
125+ popTo,
104126 replaceWith,
105127 reset,
106128 peek,
src/views/Activate/PassportDownload.js+33−32
@@ -1,16 +1,16 @@
11import React, { useState, useEffect, useCallback } from 'react';
2−import cn from 'classnames';
32import { Just, Nothing } from 'folktale/maybe';
4−import { Grid, H4, P } from 'indigo-react';
3+import { Grid, P } from 'indigo-react';
54
65import * as need from 'lib/need';
7−
8−import { useActivateFlow } from './ActivateFlow';
96import { downloadWallet } from 'lib/invite';
7+import { useLocalRouter } from 'lib/LocalRouter';
8+
109import { DownloadButton, ForwardButton } from 'components/Buttons';
1110import PaperRenderer from 'components/PaperRenderer';
12−import { useLocalRouter } from 'lib/LocalRouter';
13−import Steps from 'components/Steps';
11+
12+import { useActivateFlow } from './ActivateFlow';
13+import PassportView from './PassportView';
1414
1515export default function PassportDownload({ className }) {
1616 const {
@@ -51,36 +51,37 @@ export default function PassportDownload({ className }) {
5151 const goToVerify = useCallback(() => push(names.VERIFY), [push, names]);
5252
5353 return (
54− <Grid className={cn(className, 'auto-rows-min')}>
55− <Grid.Item full as={Steps} num={1} total={3} />
56− <Grid.Item full as={H4}>
57− Passport
58− </Grid.Item>
59− <Grid.Item full>
60− <P>
61− After you’ve downloaded your passport, back up the ticket manually or
62− store on a trusted device.
63− </P>
64− <P>
65− What is digital identity? A passport is your digital identity. You
66− will use your passport to access your true computer, send payments,
67− and administer your identity. So naturally, you must keep this secure.
68− </P>
69− </Grid.Item>
70− <Grid.Item
71− full
72− as={!downloaded ? DownloadButton : ForwardButton}
73− disabled={!generated}
74− onClick={!downloaded ? download : goToVerify}
75− success={downloaded}
76− solid>
77− {!downloaded ? 'Download Passport' : 'Continue'}
78− </Grid.Item>
54+ <>
55+ <PassportView className={className} header="Passport" step={1}>
56+ <Grid>
57+ <Grid.Item full>
58+ <P>
59+ After you’ve downloaded your passport, back up the ticket manually
60+ or store on a trusted device.
61+ </P>
62+ <P>
63+ What is digital identity? A passport is your digital identity. You
64+ will use your passport to access your true computer, send
65+ payments, and administer your identity. So naturally, you must
66+ keep this secure.
67+ </P>
68+ </Grid.Item>
69+ <Grid.Item
70+ full
71+ as={!downloaded ? DownloadButton : ForwardButton}
72+ disabled={!generated}
73+ onClick={!downloaded ? download : goToVerify}
74+ success={downloaded}
75+ solid>
76+ {!downloaded ? 'Download Passport' : 'Continue'}
77+ </Grid.Item>
78+ </Grid>
79+ </PassportView>
7980 <PaperRenderer
8081 point={pointAsString}
8182 wallet={wallet}
8283 callback={data => setPaper(Just(data))}
8384 />
84− </Grid>
85+ </>
8586 );
8687}
src/views/Activate/PassportTransfer.js+21−24
@@ -1,6 +1,5 @@
11import React, { useState, useCallback } from 'react';
2−import cn from 'classnames';
3−import { Grid, H4, Text, ErrorText } from 'indigo-react';
2+import { Grid, Text, ErrorText } from 'indigo-react';
43
54import { useNetwork } from 'store/network';
65import { useWallet } from 'store/wallet';
@@ -9,17 +8,17 @@ import { usePointCursor } from 'store/pointCursor';
98
109import * as need from 'lib/need';
1110import useLifecycle from 'lib/useLifecycle';
12−import { claimPointFromInvite } from 'lib/invite';
11+import { reticketPointBetweenWallets } from 'lib/invite';
1312import timeout from 'lib/timeout';
1413import { fromWei } from 'lib/txn';
1514
16−import Steps from 'components/Steps';
1715import WarningBox from 'components/WarningBox';
1816import LoadingBar from 'components/LoadingBar';
1917import Highlighted from 'components/Highlighted';
18+import { RestartButton } from 'components/Buttons';
2019
2120import { useActivateFlow } from './ActivateFlow';
22−import { RestartButton } from 'components/Buttons';
21+import PassportView from './PassportView';
2322
2423export default function PassportTransfer({ className, resetActivateRouter }) {
2524 const { replaceWith, names } = useHistory();
@@ -92,9 +91,9 @@ export default function PassportTransfer({ className, resetActivateRouter }) {
9291 const _web3 = need.web3(web3);
9392 const _contracts = need.contracts(contracts);
9493
95− await claimPointFromInvite({
96− inviteWallet: _inviteWallet,
97− wallet: _wallet,
94+ await reticketPointBetweenWallets({
95+ fromWallet: _inviteWallet,
96+ toWallet: _wallet,
9897 point: _point,
9998 web3: _web3,
10099 contracts: _contracts,
@@ -159,7 +158,7 @@ export default function PassportTransfer({ className, resetActivateRouter }) {
159158 );
160159 }
161160
162− if (progress < 100) {
161+ if (progress < 1.0) {
163162 return (
164163 <Grid.Item full as={WarningBox} className="mt8">
165164 Never give your Master Ticket to anyone
@@ -171,22 +170,20 @@ export default function PassportTransfer({ className, resetActivateRouter }) {
171170 };
172171
173172 return (
174− <Grid className={cn(className, 'auto-rows-min')}>
175− <Grid.Item full as={Steps} num={3} total={3} />
176− <Grid.Item full as={H4}>
177− {label}
178− </Grid.Item>
179− <Grid.Item full as={Grid} className="mt3" gap={3}>
180− <Grid.Item full as={LoadingBar} progress={progress} />
181− <Grid.Item full>
182− <Text className="f5 green4">
183− This process can take up to 5 minutes to complete. Don't leave this
184− page until the process is complete.
185− </Text>
173+ <PassportView className={className} header={label} step={3}>
174+ <Grid>
175+ <Grid.Item full as={Grid} className="mt3" gap={3}>
176+ <Grid.Item full as={LoadingBar} progress={progress} />
177+ <Grid.Item full>
178+ <Text className="f5 green4">
179+ This process can take up to 5 minutes to complete. Don't leave
180+ this page until the process is complete.
181+ </Text>
182+ </Grid.Item>
186183 </Grid.Item>
187− </Grid.Item>
188184
189− {renderAdditionalInfo()}
190− </Grid>
185+ {renderAdditionalInfo()}
186+ </Grid>
187+ </PassportView>
191188 );
192189}
src/views/Activate/PassportVerify.js+23−25
@@ -1,18 +1,18 @@
11import React, { useCallback, useMemo } from 'react';
2−import cn from 'classnames';
3−import { Grid, H4, Input, P } from 'indigo-react';
2+import { Grid, Input, P } from 'indigo-react';
43
54import * as need from 'lib/need';
65import { useLocalRouter } from 'lib/LocalRouter';
76import { useTicketInput } from 'lib/useInputs';
87import { validateExactly } from 'lib/validators';
8+import { isDevelopment } from 'lib/flags';
99
1010import { ForwardButton } from 'components/Buttons';
11−import Steps from 'components/Steps';
1211
1312import { useActivateFlow } from './ActivateFlow';
13+import PassportView from './PassportView';
1414
15−const STUB_VERIFY_TICKET = process.env.NODE_ENV === 'development';
15+const STUB_VERIFY_TICKET = isDevelopment;
1616
1717export default function PassportVerify({ className }) {
1818 const { push, names } = useLocalRouter();
@@ -33,26 +33,24 @@ export default function PassportVerify({ className }) {
3333 });
3434
3535 return (
36− <Grid className={cn(className, 'auto-rows-min')}>
37− <Grid.Item full as={Steps} num={2} total={3} />
38− <Grid.Item full as={H4}>
39− Verify Passport
40− </Grid.Item>
41− <Grid.Item full as={P}>
42− After you download your passport, verify your custody. Your passport
43− should be a folder of image files. One of them is your Master Ticket.
44− Open it and enter the 4 word phrase below (with hyphens).
45− </Grid.Item>
46− <Grid.Item full as={Input} {...ticketInput} />
47− <Grid.Item
48− full
49− className="mt3"
50− as={ForwardButton}
51− disabled={!pass}
52− onClick={goToTransfer}
53− solid>
54− Verify
55− </Grid.Item>
56− </Grid>
36+ <PassportView header="Verify Passport" step={2} className={className}>
37+ <Grid>
38+ <Grid.Item full as={P}>
39+ After you download your passport, verify your custody. Your passport
40+ should be a folder of image files. One of them is your Master Ticket.
41+ Open it and enter the 4 word phrase below (with hyphens).
42+ </Grid.Item>
43+ <Grid.Item full as={Input} {...ticketInput} />
44+ <Grid.Item
45+ full
46+ className="mt3"
47+ as={ForwardButton}
48+ disabled={!pass}
49+ onClick={goToTransfer}
50+ solid>
51+ Verify
52+ </Grid.Item>
53+ </Grid>
54+ </PassportView>
5755 );
5856}
src/views/Activate/PassportView.jsnew file+27
@@ -0,0 +1,27 @@
1+import React from 'react';
2+import cn from 'classnames';
3+import { Grid, H4 } from 'indigo-react';
4+
5+import Steps from 'components/Steps';
6+
7+// NOTE: the 48px matches the `mt8` class on Passport so things line up nicely
8+// TODO: there's probably a clever grid/flex way to do this instead
9+export default function PassportView({ className, children, header, step }) {
10+ return (
11+ <>
12+ <Grid className={cn(className, 'auto-rows-min')}>
13+ <Grid.Item
14+ full
15+ as={Steps}
16+ num={step}
17+ total={3}
18+ style={{ height: '48px' }}
19+ />
20+ <Grid.Item full as={H4}>
21+ {header}
22+ </Grid.Item>
23+ <Grid.Item full>{children}</Grid.Item>
24+ </Grid>
25+ </>
26+ );
27+}
src/views/Admin/AdminReticket.js+35−68
@@ -1,97 +1,64 @@
1−import React, { useState } from 'react';
2−import { size } from 'lodash';
3−import * as need from 'lib/need';
4−import ob from 'urbit-ob';
1+import React, { useCallback, useState } from 'react';
52import { Just, Nothing } from 'folktale/maybe';
3+import { Grid, Flex } from 'indigo-react';
64
7−import { usePointCursor } from 'store/pointCursor';
8−
9−import View from 'components/View';
10−import MiniBackButton from 'components/MiniBackButton';
115import { LocalRouterProvider } from 'lib/LocalRouter';
12−import Crumbs from 'components/Crumbs';
13−
146import useRouter from 'lib/useRouter';
157
16−import { useHistory } from 'store/history';
8+import MiniBackButton from 'components/MiniBackButton';
9+import Steps from 'components/Steps';
1710
18−import Confirm from '../Reticket/Confirm';
19−import Download from '../Reticket/Download';
20−import Verify from '../Reticket/Verify';
21−import DoReticket from '../Reticket/DoReticket';
11+import ReticketConfirm from './Reticket/ReticketConfirm';
12+import ReticketDownload from './Reticket/ReticketDownload';
13+import ReticketVerify from './Reticket/ReticketVerify';
14+import ReticketExecute from './Reticket/ReticketExecute';
2215
2316const NAMES = {
2417 CONFIRM: 'CONFIRM',
2518 DOWNLOAD: 'DOWNLOAD',
2619 VERIFY: 'VERIFY',
27− RETICKET: 'RETICKET',
20+ EXECUTE: 'EXECUTE',
2821};
2922
3023const VIEWS = {
31− CONFIRM: Confirm,
32− DOWNLOAD: Download,
33− VERIFY: Verify,
34− RETICKET: DoReticket,
24+ CONFIRM: ReticketConfirm,
25+ DOWNLOAD: ReticketDownload,
26+ VERIFY: ReticketVerify,
27+ EXECUTE: ReticketExecute,
3528};
3629
37−const STEP_MAX = size(NAMES);
30+const STEPS = [NAMES.CONFIRM, NAMES.DOWNLOAD, NAMES.VERIFY, NAMES.RETICKET];
3831
3932export default function AdminReticket() {
40− const history = useHistory();
41− const { Route, ...innerRouter } = useRouter({
33+ const { Route, ...router } = useRouter({
4234 names: NAMES,
4335 views: VIEWS,
4436 initialRoutes: [{ key: NAMES.CONFIRM }],
4537 });
46− const { pointCursor } = usePointCursor();
47− const point = need.point(pointCursor);
48− const name = ob.patp(point);
4938
50− const [newWallet, setNewWallet] = useState(Nothing());
39+ const [newWallet, _setNewWallet] = useState(Nothing());
5140
52− //TODO step # from Route?
53− const stepNumber = 1;
54−
55− const storeNewWallet = (wallet, paper) => {
56− setNewWallet(
57− Just({
58− wallet,
59− paper,
60− })
61− );
62− };
63−
64− const completed = () => {
65− history.pop();
66− };
41+ const setNewWallet = useCallback(
42+ (wallet, paper) => {
43+ _setNewWallet(
44+ Just({
45+ wallet,
46+ paper,
47+ })
48+ );
49+ },
50+ [_setNewWallet]
51+ );
6752
6853 return (
69− <LocalRouterProvider value={innerRouter}>
70− <View>
71− <Crumbs
72− routes={[
73− {
74− text: name,
75− action: () => {
76− return history.pop(2);
77− },
78− },
79− {
80− text: 'Admin',
81− action: () => history.pop(),
82− },
83− ]}
84− />
85− <MiniBackButton onClick={innerRouter.pop} />
86− Step {stepNumber} of {STEP_MAX}
87− <Route
88− newWallet={newWallet}
89− //
90− storeNewWallet={storeNewWallet}
91− //
92− completed={completed}
93− />
94− </View>
54+ <LocalRouterProvider value={router}>
55+ <Grid>
56+ <Grid.Item full as={Flex} row justify="between" align="center">
57+ <Flex.Item as={MiniBackButton} onClick={() => router.pop()} />
58+ <Flex.Item as={Steps} num={router.size} total={STEPS.length} />
59+ </Grid.Item>
60+ </Grid>
61+ <Route newWallet={newWallet} setNewWallet={setNewWallet} />
9562 </LocalRouterProvider>
9663 );
9764}
src/views/Admin/Reticket/ReticketConfirm.jsnew file+83
@@ -0,0 +1,83 @@
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 PaperRenderer from 'components/PaperRenderer';
15+import WarningBox from 'components/WarningBox';
16+import Blinky from 'components/Blinky';
17+import useCurrentPointName from 'lib/useCurrentPointName';
18+
19+export default function ReticketConfirm({ 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+ (async () => {
32+ if (Just.hasInstance(newWallet)) {
33+ // skip if already computed (hitting the back button for example)
34+ return;
35+ }
36+
37+ const wal = await generateWallet(point);
38+ if (isDevelopment) {
39+ console.log(`The new ticket for ${name} is ${wal.ticket}`);
40+ }
41+ setGeneratedWallet(Just(wal));
42+ })();
43+ });
44+
45+ const goDownload = useCallback(() => push(names.DOWNLOAD), [push, names]);
46+
47+ const paperRenderer = generatedWallet.matchWith({
48+ Nothing: () => null,
49+ Just: ({ value: wallet }) => (
50+ <PaperRenderer
51+ point={point}
52+ wallet={wallet}
53+ callback={paper => {
54+ setNewWallet(wallet, paper);
55+ }}
56+ />
57+ ),
58+ });
59+
60+ return (
61+ <Grid gap={4} className="mt4">
62+ <Grid.Item full as={Text}>
63+ Reticketing is the process of generating a completely fresh wallet and
64+ transferring ownership of your point to that wallet.
65+ </Grid.Item>
66+ <Grid.Item full as={WarningBox}>
67+ Beware, this resets your proxy addresses; if you're using smart
68+ contracts, this might break them! It will also change your networking
69+ keys!
70+ </Grid.Item>
71+ <Grid.Item
72+ full
73+ as={ForwardButton}
74+ solid
75+ accessory={Nothing.hasInstance(newWallet) ? <Blinky /> : undefined}
76+ disabled={Nothing.hasInstance(newWallet)}
77+ onClick={goDownload}>
78+ I understand, continue
79+ </Grid.Item>
80+ {paperRenderer}
81+ </Grid>
82+ );
83+}
src/views/Admin/Reticket/ReticketDownload.jsnew file+38
@@ -0,0 +1,38 @@
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 gap={4} className="mt4">
24+ <Grid.Item full as={Text}>
25+ Download the thing, and keep it somewhere safe!
26+ </Grid.Item>
27+ <Grid.Item
28+ full
29+ as={!downloaded ? DownloadButton : ForwardButton}
30+ disabled={Nothing.hasInstance(newWallet)}
31+ onClick={!downloaded ? download : goVerify}
32+ success={downloaded}
33+ solid>
34+ {!downloaded ? 'Download Passport' : 'Continue'}
35+ </Grid.Item>
36+ </Grid>
37+ );
38+}
src/views/Reticket/DoReticket.js → src/views/Admin/Reticket/ReticketExecute.jsrenamed+68−39
@@ -1,25 +1,28 @@
11import React, { useState, useCallback } from 'react';
22import { Just } from 'folktale/maybe';
3−
43import { Grid, H4, Text, ErrorText } from 'indigo-react';
5−import { RestartButton } from 'components/Buttons';
6−import WarningBox from 'components/WarningBox';
7−import LoadingBar from 'components/LoadingBar';
8−import Highlighted from 'components/Highlighted';
94
10−import { claimPointFromInvite } from 'lib/invite';
5+import { reticketPointBetweenWallets } from 'lib/invite';
116import { fromWei } from 'lib/txn';
12−
137import * as need from 'lib/need';
8+import useLifecycle from 'lib/useLifecycle';
9+
1410import { useNetwork } from 'store/network';
1511import { useWallet } from 'store/wallet';
1612import { usePointCursor } from 'store/pointCursor';
17−import useLifecycle from 'lib/useLifecycle';
13+import { useHistory } from 'store/history';
1814
19−//TODO maybe we want to drop in PassportTransfer here?
20−export default function DoReticket({ newWallet, completed }) {
15+import { RestartButton, ForwardButton } from 'components/Buttons';
16+import WarningBox from 'components/WarningBox';
17+import LoadingBar from 'components/LoadingBar';
18+import Highlighted from 'components/Highlighted';
19+import { WALLET_TYPES } from 'lib/wallet';
20+
21+// TODO: maybe we can merge PassportTransfer here?
22+export default function ReticketExecute({ newWallet }) {
23+ const { popTo, names } = useHistory();
2124 const { web3, contracts } = useNetwork();
22− const { wallet, setUrbitWallet } = useWallet();
25+ const { wallet, setWalletType, resetWallet, setUrbitWallet } = useWallet();
2326 const { pointCursor } = usePointCursor();
2427
2528 const [generalError, setGeneralError] = useState();
@@ -28,28 +31,36 @@ export default function DoReticket({ newWallet, completed }) {
2831 progress: 0,
2932 });
3033 const [needFunds, setNeedFunds] = useState();
34+ const isDone = progress >= 1.0;
3135
3236 // start reticketing transactions on mount
3337 useLifecycle(() => {
3438 (async () => {
3539 try {
36− await claimPointFromInvite({
37− inviteWallet: need.wallet(wallet),
38− wallet: newWallet.value.wallet,
40+ await reticketPointBetweenWallets({
41+ fromWallet: need.wallet(wallet),
42+ toWallet: newWallet.value.wallet,
3943 point: need.point(pointCursor),
4044 web3: need.web3(web3),
4145 contracts: need.contracts(contracts),
4246 onUpdate: handleUpdate,
4347 });
44− //TODO would it be better for this to happen in completed()?
45− setUrbitWallet(Just(newWallet.value.wallet));
46− completed(); //TODO don't auto-redirect
4748 } catch (err) {
4849 setGeneralError(err);
4950 }
5051 })();
5152 });
5253
54+ const loginAndGoHome = useCallback(() => {
55+ // set wallet state
56+ resetWallet();
57+ setWalletType(WALLET_TYPES.TICKET);
58+ setUrbitWallet(Just(newWallet.value.wallet));
59+ // (implicit) pointCursor stays the same
60+ // rediect to point
61+ popTo(names.POINT);
62+ }, [resetWallet, setWalletType, setUrbitWallet, newWallet, popTo, names]);
63+
5364 const handleUpdate = useCallback(
5465 ({ type, state, value }) => {
5566 switch (type) {
@@ -70,7 +81,7 @@ export default function DoReticket({ newWallet, completed }) {
7081 if (generalError) {
7182 return (
7283 <>
73− <Grid.Item full className="mt8">
84+ <Grid.Item full>
7485 <ErrorText>{generalError.message.toString()}</ErrorText>
7586 </Grid.Item>
7687 <Grid.Item
@@ -85,7 +96,7 @@ export default function DoReticket({ newWallet, completed }) {
8596
8697 if (needFunds) {
8798 return (
88− <Grid.Item full className="mt8">
99+ <Grid.Item full>
89100 <Highlighted>
90101 The address {needFunds.address} needs at least{' '}
91102 {fromWei(needFunds.minBalance)} ETH and currently has{' '}
@@ -96,33 +107,51 @@ export default function DoReticket({ newWallet, completed }) {
96107 );
97108 }
98109
99− if (progress < 100) {
100− return (
101− <Grid.Item full as={WarningBox} className="mt8">
102− Never give your Master Ticket to anyone
103− </Grid.Item>
104− );
105− }
106−
107− return null;
110+ return (
111+ <Grid.Item full as={WarningBox}>
112+ Never give your Master Ticket to anyone
113+ </Grid.Item>
114+ );
108115 };
109116
110117 return (
111− <Grid className={'auto-rows-min'}>
118+ <Grid gap={4} className="mt4">
112119 <Grid.Item full as={H4}>
113120 {label}
114121 </Grid.Item>
115− <Grid.Item full as={Grid} className="mt3" gap={3}>
116− <Grid.Item full as={LoadingBar} progress={progress} />
117− <Grid.Item full>
118− <Text className="f5 green4">
119− This process can take up to 5 minutes to complete. Don't leave this
120− page until the process is complete.
121− </Text>
122− </Grid.Item>
123− </Grid.Item>
124122
125− {renderAdditionalInfo()}
123+ {!isDone && (
124+ <>
125+ <Grid.Item full as={Grid} className="mt3" gap={3}>
126+ <Grid.Item full as={LoadingBar} progress={progress} />
127+ <Grid.Item full>
128+ <Text className="f5 green4">
129+ This process can take up to 5 minutes to complete. Don't leave
130+ this page until the process is complete.
131+ </Text>
132+ </Grid.Item>
133+ </Grid.Item>
134+
135+ {renderAdditionalInfo()}
136+ </>
137+ )}
138+
139+ {isDone && (
140+ <>
141+ <Grid.Item full as={Text}>
142+ Your changes are now reflected on-chain and you can use the new
143+ ticket to manage your point.
144+ </Grid.Item>
145+ <Grid.Item
146+ full
147+ as={ForwardButton}
148+ solid
149+ accessory="↺"
150+ onClick={loginAndGoHome}>
151+ Login with New Master Ticket
152+ </Grid.Item>
153+ </>
154+ )}
126155 </Grid>
127156 );
128157}
src/views/Admin/Reticket/ReticketVerify.js+2−2
@@ -1,13 +1,13 @@
11import React, { useCallback, useMemo } from 'react';
22import { Input, Text, Grid } from 'indigo-react';
33
4−import { ForwardButton } from 'components/Buttons';
5−
64import { useTicketInput } from 'lib/useInputs';
75import { useLocalRouter } from 'lib/LocalRouter';
86import { validateExactly } from 'lib/validators';
97import { isDevelopment } from 'lib/flags';
108
9+import { ForwardButton } from 'components/Buttons';
10+
1111const STUB_VERIFY_TICKET = isDevelopment;
1212
1313export default function ReticketVerify({ newWallet }) {
src/views/Point.js+12−19
@@ -16,33 +16,26 @@ import { matchBlinky } from 'components/Blinky';
1616import useInvites from 'lib/useInvites';
1717import { useSyncOwnedPoints } from 'lib/useSyncPoints';
1818import { ROUTE_NAMES } from 'lib/routeNames';
19−import { eqAddr } from 'lib/wallet';
2019
2120import Actions from './Point/Actions';
2221import { useWallet } from 'store/wallet';
22+import usePermissionsForPoint from 'lib/usePermissionsForPoint';
2323
2424export default function Point() {
2525 const history = useHistory();
2626 const { pointCursor } = usePointCursor();
27− const { pointCache } = usePointCache();
2827 const { wallet } = useWallet();
2928
3029 const point = need.point(pointCursor);
3130
32− let canAdmin = false;
33− if (point in pointCache) {
34− canAdmin = wallet.matchWith({
35− Nothing: () => false,
36− Just: wal => {
37− const pointDetails = need.fromPointCache(pointCache, point);
38− const address = need.addressFromWallet(wallet);
39− return (
40− eqAddr(address, pointDetails.owner) ||
41− eqAddr(address, pointDetails.managementProxy)
42− );
43− },
44− });
45− }
31+ const { canManage } = usePermissionsForPoint(
32+ // using empty string should be ok here
33+ wallet.matchWith({
34+ Nothing: () => '',
35+ Just: p => p.value.address,
36+ }),
37+ point
38+ );
4639
4740 // fetch the invites for the current cursor
4841 const { availableInvites } = useInvites(point);
@@ -66,7 +59,7 @@ export default function Point() {
6659 <Grid.Item
6760 full
6861 as={ForwardButton}
69− disabled={!canAdmin}
62+ disabled={!canManage}
7063 onClick={goAdmin}>
7164 Admin
7265 </Grid.Item>
@@ -81,8 +74,8 @@ export default function Point() {
8174 )}
8275 </Grid>
8376
84− <FooterButton onClick={goInvite}>
85− Invite <sup>{availableInvitesText}</sup>
77+ <FooterButton disabled={!canManage} onClick={goInvite}>
78+ Invite <sup>{availableInvitesText} available</sup>
8679 </FooterButton>
8780 </View>
8881 );
src/views/Reticket/Confirm.jsdeleted−63
@@ -1,63 +0,0 @@
1−import React, { useState, useCallback } from 'react';
2−import * as need from 'lib/need';
3−import { Just, Nothing } from 'folktale/maybe';
4−
5−import { usePointCursor } from 'store/pointCursor';
6−
7−import View from 'components/View';
8−import { ForwardButton } from 'components/Buttons';
9−import PaperRenderer from 'components/PaperRenderer';
10−
11−import useLifecycle from 'lib/useLifecycle';
12−import { generateWallet } from 'lib/invite';
13−import { useLocalRouter } from 'lib/LocalRouter';
14−
15−import { isDevelopment } from 'lib/flags';
16−
17−export default function Confirm({ newWallet, storeNewWallet }) {
18− const { push, names } = useLocalRouter();
19− const { pointCursor } = usePointCursor();
20− const point = need.point(pointCursor);
21−
22− const [generatedWallet, setGeneratedWallet] = useState(Nothing());
23−
24− // start generating new wallet on mount
25− //TODO also check we can pay for the transactions?
26− useLifecycle(() => {
27− (async () => {
28− const wal = await generateWallet(point);
29− if (isDevelopment) {
30− console.log('ticket for', point, wal.ticket);
31− }
32− setGeneratedWallet(Just(wal));
33− })();
34− });
35−
36− const next = useCallback(() => push(names.DOWNLOAD), [push, names]);
37−
38− const paperRenderer = generatedWallet.matchWith({
39− Nothing: () => null,
40− Just: gw => (
41− <PaperRenderer
42− point={point}
43− wallet={gw.value}
44− callback={paper => {
45− storeNewWallet(gw.value, paper);
46− }}
47− />
48− ),
49− });
50−
51− return (
52− <View>
53− Maybe we should just duplicate the Login component here? Not everyone who
54− does this will be logged in with an existing ticket. Also, beware, this
55− changes your proxy addresses. If you're using smart contracts, this might
56− break them! It will also change your networking keys!
57− <ForwardButton disabled={Nothing.hasInstance(newWallet)} onClick={next}>
58− I understand, continue
59− </ForwardButton>
60− {paperRenderer}
61− </View>
62− );
63−}
src/views/Reticket/Download.jsdeleted−35
@@ -1,35 +0,0 @@
1−import React, { useState, useCallback } 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({ newWallet }) {
11− const { push, names } = 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 = useCallback(() => push(names.VERIFY), [push, names]);
21−
22− return (
23− <View>
24− Download the thing, and keep it somewhere safe!
25− <DownloadButton
26− disabled={Nothing.hasInstance(newWallet)}
27− onClick={doDownload}>
28− Download
29− </DownloadButton>
30− <ForwardButton disabled={!hasDownloaded} onClick={next}>
31− Continue
32− </ForwardButton>
33− </View>
34− );
35−}
src/views/Reticket/Verify.jsdeleted−41
@@ -1,41 +0,0 @@
1−import React, { useCallback, useMemo } from 'react';
2−import { Input } from 'indigo-react';
3−
4−import { useTicketInput } from 'lib/useInputs';
5−import { useLocalRouter } from 'lib/LocalRouter';
6−import { validateExactly } from 'lib/validators';
7−
8−import View from 'components/View';
9−import { ForwardButton } from 'components/Buttons';
10−
11−const STUB_VERIFY_TICKET = process.env.NODE_ENV === 'development';
12−
13−//TODO deduplicate with PassportVerify
14−export default function Verify({ newWallet }) {
15− const { push, names } = useLocalRouter();
16−
17− const ticket = newWallet.value.wallet.ticket;
18− const validators = useMemo(
19− () => [validateExactly(ticket, 'Does not match expected master ticket.')],
20− [ticket]
21− );
22− const [ticketInput, { pass }] = useTicketInput({
23− name: 'ticket',
24− label: 'New master ticket',
25− initialValue: STUB_VERIFY_TICKET ? ticket : undefined,
26− autoFocus: true,
27− validators,
28− });
29−
30− const next = useCallback(() => push(names.RETICKET), [push, names]);
31−
32− return (
33− <View>
34− Prove that you downloaded the new secrets!
35− <Input {...ticketInput} />
36− <ForwardButton disabled={!pass} onClick={next}>
37− Reticket
38− </ForwardButton>
39− </View>
40− );
41−}