on master help sign in
feat: implement passport address, restart button, fix alignment for activate flow
8ca8b8aMatt Condon (shrugs)
src/components/Buttons.js+6
@@ -13,3 +13,9 @@ export const OfflineButton = props => (
13 Offline
13 Offline
14 </OutButton>
14 </OutButton>
15);
15);
16+
17+export const RestartButton = props => (
18+ <Button accessory="↺" solid {...props}>
19+ Restart
20+ </Button>
21+);
src/components/Passport.js+10−11
@@ -36,7 +36,7 @@ export default function Passport({
36 point,
36 point,
37 className,
37 className,
38 ticket = null,
38 ticket = null,
39− address = null,
39+ address = Maybe.Nothing(),
40 mini = false,
40 mini = false,
41}) {
41}) {
42 const birthday = usePointBirthday(point.getOrElse(null));
42 const birthday = usePointBirthday(point.getOrElse(null));
@@ -55,14 +55,12 @@ export default function Passport({
55 });
55 });
56
56
57 const showTicket = ticket !== null;
57 const showTicket = ticket !== null;
58− const showValidTicket = !!ticket;
58+ const isValidTiket = !!ticket;
59− const visibleTicket = showValidTicket ? kValidTicket : kPendingTicket;
59+ const visibleTicket = isValidTiket ? kValidTicket : kPendingTicket;
60− const visibleAddress =
60+ const visibleAddress = `${address.matchWith({
61− address &&
62− `${address.matchWith({
63− Nothing: () => kEmptyHalfAddress,
61+ Nothing: () => kEmptyHalfAddress,
64− Just: p => p.value.slice(0, kVisibleAddressCharacters),
62+ Just: p => p.value.slice(0, kVisibleAddressCharacters),
65− })}${kBufferAddressCharacters}`;
63+ })}${kBufferAddressCharacters}`;
66
64
67 return (
65 return (
68 <Grid gap={4} className={cn('bg-black r8 p7', className)}>
66 <Grid gap={4} className={cn('bg-black r8 p7', className)}>
@@ -87,8 +85,9 @@ export default function Passport({
87 </Flex.Item>
85 </Flex.Item>
88 <Flex.Item
86 <Flex.Item
89 as={Text}
87 as={Text}
90− className={cn('mono f6 f5-ns gray4 wrap', {
88+ className={cn('mono f6 f5-ns wrap', {
91− green3: showValidTicket,
89+ gray4: !isValidTiket,
90+ green3: isValidTiket,
92 })}>
91 })}>
93 {visibleTicket}
92 {visibleTicket}
94 </Flex.Item>
93 </Flex.Item>
src/components/Steps.js+4−2
@@ -3,9 +3,11 @@ import cn from 'classnames';
3import { Text } from 'indigo-react';
3import { Text } from 'indigo-react';
4
4
5export default function Steps({ num = 1, total = 3, className }) {
5export 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
6 return (
8 return (
7− <Text className={cn('f6 gray3', className)}>
9+ <Text className={cn('f5 gray3', className)} style={{ height: '48px' }}>
8− <Text className="f6 black">Step {num}</Text> of {total}
10+ <Text className="f5 black">Step {num}</Text> of {total}
9 </Text>
11 </Text>
10 );
12 );
11}
13}
src/lib/useRouter.js+8−2
@@ -24,7 +24,6 @@ export default function useRouter({
24 )}`
24 )}`
25 );
25 );
26 }
26 }
27−
28 setRoutes(routes => [...routes, { key, data }]);
27 setRoutes(routes => [...routes, { key, data }]);
29 },
28 },
30 [setRoutes, views]
29 [setRoutes, views]
@@ -55,7 +54,13 @@ export default function useRouter({
55 [setRoutes]
54 [setRoutes]
56 );
55 );
57 const peek = useCallback(() => last(routes), [routes]);
56 const peek = useCallback(() => last(routes), [routes]);
58− const replaceWith = useCallback(routes => setRoutes(routes), [setRoutes]);
57+ const replaceWith = useCallback(routes => setRoutes(() => routes), [
58+ setRoutes,
59+ ]);
60+ const reset = useCallback(() => setRoutes(initialRoutes), [
61+ setRoutes,
62+ initialRoutes,
63+ ]);
59 const includes = useCallback(key => _includes(routes.map(r => r.key), key), [
64 const includes = useCallback(key => _includes(routes.map(r => r.key), key), [
60 routes,
65 routes,
61 ]);
66 ]);
@@ -92,6 +97,7 @@ export default function useRouter({
92 popAndPush,
97 popAndPush,
93 pop,
98 pop,
94 replaceWith,
99 replaceWith,
100+ reset,
95 peek,
101 peek,
96 size,
102 size,
97 includes,
103 includes,
src/store/wallet.js−6
@@ -79,12 +79,6 @@ function _useWallet(
79 urbitWallet.value.meta.passphrase
79 urbitWallet.value.meta.passphrase
80 );
80 );
81
81
82− // manually set the public address
83− wallet.map(wal => {
84− wal.address = urbitWallet.value.ownership.keys.address;
85− return wal;
86− });
87−
88 setWallet(wallet);
82 setWallet(wallet);
89 } else {
83 } else {
90 setWallet(Maybe.Nothing());
84 setWallet(Maybe.Nothing());
src/style/indigo-proposals.scss+4
@@ -35,3 +35,7 @@
35.us-none {
35.us-none {
36 user-select: none;
36 user-select: none;
37}
37}
38+
39+.auto-rows-min {
40+ grid-auto-rows: min-content;
41+}
src/views/Activate/ActivateCode.js+1−1
@@ -5,7 +5,7 @@ import { useTicketInput } from 'components/Inputs';
5import { Grid, Input, H4, ErrorText } from 'indigo-react';
5import { Grid, Input, H4, ErrorText } from 'indigo-react';
6
6
7import View from 'components/View';
7import View from 'components/View';
8−import { ForwardButton, OfflineButton } from 'components/Buttons';
8+import { ForwardButton } from 'components/Buttons';
9import Passport from 'components/Passport';
9import Passport from 'components/Passport';
10
10
11import { useHistory } from 'store/history';
11import { useHistory } from 'store/history';
src/views/Activate/ActivatePassport.js+32−6
@@ -1,9 +1,9 @@
1−import React from 'react';
1+import React, { useMemo } from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3import Maybe from 'folktale/maybe';
3import Maybe from 'folktale/maybe';
4import { Grid } from 'indigo-react';
4import { Grid } from 'indigo-react';
5
5
6−import { LocalRouterProvider } from 'lib/LocalRouter';
6+import { LocalRouterProvider, useLocalRouter } from 'lib/LocalRouter';
7
7
8import View from 'components/View';
8import View from 'components/View';
9import Passport from 'components/Passport';
9import Passport from 'components/Passport';
@@ -15,6 +15,11 @@ import useRouter from 'lib/useRouter';
15import PassportDownload from './PassportDownload';
15import PassportDownload from './PassportDownload';
16import PassportVerify from './PassportVerify';
16import PassportVerify from './PassportVerify';
17import PassportTransfer from './PassportTransfer';
17import PassportTransfer from './PassportTransfer';
18+import {
19+ walletFromMnemonic,
20+ DEFAULT_HD_PATH,
21+ addressFromSecp256k1Public,
22+} from 'lib/wallet';
18
23
19const kPassportNames = {
24const kPassportNames = {
20 DOWNLOAD: 'DOWNLOAD',
25 DOWNLOAD: 'DOWNLOAD',
@@ -29,8 +34,9 @@ const kPassportViews = {
29};
34};
30
35
31export default function ActivatePassport() {
36export default function ActivatePassport() {
32− const { derivedPoint, generated } = useActivateFlow();
37+ const { derivedPoint, generated, derivedWallet } = useActivateFlow();
33−
38+ // pull the reset function out of the 'activate' router
39+ const { reset: resetActivateRouter } = useLocalRouter();
34 const { Route, ...router } = useRouter({
40 const { Route, ...router } = useRouter({
35 names: kPassportNames,
41 names: kPassportNames,
36 views: kPassportViews,
42 views: kPassportViews,
@@ -44,6 +50,21 @@ export default function ActivatePassport() {
44 const leftHalf = useBreakpoints([false, false, 1]);
50 const leftHalf = useBreakpoints([false, false, 1]);
45 const rightHalf = useBreakpoints([false, false, 2]);
51 const rightHalf = useBreakpoints([false, false, 2]);
46
52
53+ // addresses are not derived until we set[Urbit]Wallet(), so do that inline
54+ const address = useMemo(
55+ () =>
56+ derivedWallet
57+ .chain(wallet =>
58+ walletFromMnemonic(
59+ wallet.ownership.seed,
60+ DEFAULT_HD_PATH,
61+ wallet.meta.passphrase
62+ )
63+ )
64+ .map(wallet => addressFromSecp256k1Public(wallet.publicKey)),
65+ [derivedWallet]
66+ );
67+
47 return (
68 return (
48 <LocalRouterProvider value={router}>
69 <LocalRouterProvider value={router}>
49 <View full={fullView}>
70 <View full={fullView}>
@@ -53,10 +74,15 @@ export default function ActivatePassport() {
53 className={cn({ [`mt${marginTop}`]: marginTop })}
74 className={cn({ [`mt${marginTop}`]: marginTop })}
54 point={derivedPoint}
75 point={derivedPoint}
55 ticket={generated}
76 ticket={generated}
56− address={Maybe.Nothing()}
77+ address={address}
57 />
78 />
58 </Grid.Item>
79 </Grid.Item>
80+ <Grid.Item
59− <Grid.Item half={rightHalf} full={full} as={Route} />
81+ half={rightHalf}
82+ full={full}
83+ as={Route}
84+ resetActivateRouter={resetActivateRouter}
85+ />
60 </Grid>
86 </Grid>
61 </View>
87 </View>
62 </LocalRouterProvider>
88 </LocalRouterProvider>
src/views/Activate/PassportDownload.js+4−6
@@ -1,4 +1,5 @@
1import React, { useState, useEffect, useCallback } from 'react';
1import React, { useState, useEffect, useCallback } from 'react';
2+import cn from 'classnames';
2import Maybe from 'folktale/maybe';
3import Maybe from 'folktale/maybe';
3import { Grid, H4, P } from 'indigo-react';
4import { Grid, H4, P } from 'indigo-react';
4import PaperCollateralRenderer from 'PaperCollateralRenderer';
5import PaperCollateralRenderer from 'PaperCollateralRenderer';
@@ -50,7 +51,7 @@ export default function PassportDownload({ className }) {
50 const goToVerify = useCallback(() => push(names.VERIFY), [push, names]);
51 const goToVerify = useCallback(() => push(names.VERIFY), [push, names]);
51
52
52 return (
53 return (
53− <Grid gap={4} className={className}>
54+ <Grid className={cn(className, 'auto-rows-min')}>
54 <Grid.Item full as={Steps} num={1} total={3} />
55 <Grid.Item full as={Steps} num={1} total={3} />
55 <Grid.Item full as={H4}>
56 <Grid.Item full as={H4}>
56 Passport
57 Passport
@@ -76,12 +77,9 @@ export default function PassportDownload({ className }) {
76 {!downloaded ? 'Download Passport' : 'Continue'}
77 {!downloaded ? 'Download Passport' : 'Continue'}
77 </Grid.Item>
78 </Grid.Item>
78 <PaperCollateralRenderer
79 <PaperCollateralRenderer
79− className="super-hidden"
80+ className="abs super-hidden"
80 wallet={{ [pointAsString]: wallet }}
81 wallet={{ [pointAsString]: wallet }}
81− callback={data => {
82− console.log(data);
83− setPaper(Maybe.Just(data));
82+ callback={data => setPaper(Maybe.Just(data))}
84− }}
85 mode="REGISTRATION"
83 mode="REGISTRATION"
86 />
84 />
87 </Grid>
85 </Grid>
src/views/Activate/PassportTransfer.js+79−37
@@ -1,4 +1,5 @@
1import React, { useState, useCallback } from 'react';
1import React, { useState, useCallback } from 'react';
2+import cn from 'classnames';
2import { Grid, H4, Text, ErrorText } from 'indigo-react';
3import { Grid, H4, Text, ErrorText } from 'indigo-react';
3
4
4import { useNetwork } from 'store/network';
5import { useNetwork } from 'store/network';
@@ -18,13 +19,19 @@ import LoadingBar from 'components/LoadingBar';
18import Highlighted from 'components/Highlighted';
19import Highlighted from 'components/Highlighted';
19
20
20import { useActivateFlow } from './ActivateFlow';
21import { useActivateFlow } from './ActivateFlow';
22+import { RestartButton } from 'components/Buttons';
21
23
22−export default function PassportTransfer({ className }) {
24+export default function PassportTransfer({ className, resetActivateRouter }) {
23 const { replaceWith, names } = useHistory();
25 const { replaceWith, names } = useHistory();
24 const { setUrbitWallet } = useWallet();
26 const { setUrbitWallet } = useWallet();
25 const { setPointCursor } = usePointCursor();
27 const { setPointCursor } = usePointCursor();
26 const { web3, contracts } = useNetwork();
28 const { web3, contracts } = useNetwork();
29+ const {
30+ derivedWallet,
31+ derivedPoint,
32+ inviteWallet,
33+ reset: resetActivateFlow,
27− const { derivedWallet, derivedPoint, inviteWallet } = useActivateFlow();
34+ } = useActivateFlow();
28 const [generalError, setGeneralError] = useState();
35 const [generalError, setGeneralError] = useState();
29 const [{ label, progress }, setState] = useState({
36 const [{ label, progress }, setState] = useState({
30 label: 'Verify Passport',
37 label: 'Verify Passport',
@@ -43,10 +50,21 @@ export default function PassportTransfer({ className }) {
43 [replaceWith, names]
50 [replaceWith, names]
44 );
51 );
45
52
46− const goToRestart = useCallback(
53+ const goToRestart = useCallback(() => {
54+ // NOTE: because we're already on the ACTIVATE view in the history,
55+ // react (intelligently) doesn't trigger a re-render and that means the
56+ // current state of the two nested routers (activate & passport) stick
57+ // around unless we manually clear them.
58+ // we only need to clear activate because that will unmount the passport
59+ // router (which will be set to initialRoutes when mounted again)
60+
61+ // 1) replace history
47− () => replaceWith([{ key: names.LANDING }, { key: names.ACTIVATE }]),
62+ replaceWith([{ key: names.LANDING }, { key: names.ACTIVATE }]);
63+ // 2) reset local router
64+ resetActivateRouter();
65+ // 3) clear the state
66+ resetActivateFlow();
48− [replaceWith, names]
67+ }, [replaceWith, names, resetActivateRouter, resetActivateFlow]);
49− );
50
68
51 const handleUpdate = useCallback(
69 const handleUpdate = useCallback(
52 ({ type, state, value }) => {
70 ({ type, state, value }) => {
@@ -65,13 +83,15 @@ export default function PassportTransfer({ className }) {
65 );
83 );
66
84
67 const claimPoint = useCallback(async () => {
85 const claimPoint = useCallback(async () => {
68− const _inviteWallet = need.wallet(inviteWallet);
86+ setGeneralError(false);
69− const _wallet = need.wallet(derivedWallet);
70− const _point = need.point(derivedPoint);
71− const _web3 = need.web3(web3);
72− const _contracts = need.contracts(contracts);
73
87
74 try {
88 try {
89+ const _inviteWallet = need.wallet(inviteWallet);
90+ const _wallet = need.wallet(derivedWallet);
91+ const _point = need.point(derivedPoint);
92+ const _web3 = need.web3(web3);
93+ const _contracts = need.contracts(contracts);
94+
75 await claimPointFromInvite({
95 await claimPointFromInvite({
76 inviteWallet: _inviteWallet,
96 inviteWallet: _inviteWallet,
77 wallet: _wallet,
97 wallet: _wallet,
@@ -80,11 +100,13 @@ export default function PassportTransfer({ className }) {
80 contracts: _contracts,
100 contracts: _contracts,
81 onUpdate: handleUpdate,
101 onUpdate: handleUpdate,
82 });
102 });
103+
83 // set the global wallet
104 // set the global wallet
84 setUrbitWallet(derivedWallet);
105 setUrbitWallet(derivedWallet);
85 setPointCursor(derivedPoint);
106 setPointCursor(derivedPoint);
86− await timeout(3000);
107+
87 // and redirect to login
108 // and redirect to login
109+ await timeout(3000);
88 goToLogin();
110 goToLogin();
89 } catch (error) {
111 } catch (error) {
90 // some generic error
112 // some generic error
@@ -106,13 +128,54 @@ export default function PassportTransfer({ className }) {
106 claimPoint();
128 claimPoint();
107 });
129 });
108
130
131+ const renderAdditionalInfo = () => {
132+ if (generalError) {
133+ return (
134+ <>
135+ <Grid.Item full className="mt8">
136+ <ErrorText>{generalError.message.toString()}</ErrorText>
137+ </Grid.Item>
138+ <Grid.Item
139+ full
140+ className="mt3"
141+ as={RestartButton}
142+ onClick={goToRestart}
143+ />
144+ </>
145+ );
146+ }
147+
148+ if (needFunds) {
149+ return (
150+ <Grid.Item full className="mt8">
151+ <Highlighted>
152+ The address {needFunds.address} needs at least{' '}
153+ {fromWei(needFunds.minBalance)} ETH and currently has{' '}
154+ {fromWei(needFunds.balance)} ETH. Waiting until the account has
155+ enough funds.
156+ </Highlighted>
157+ </Grid.Item>
158+ );
159+ }
160+
161+ if (progress < 100) {
162+ return (
163+ <Grid.Item full as={WarningBox} className="mt8">
164+ Never give your Master Ticket to anyone
165+ </Grid.Item>
166+ );
167+ }
168+
169+ return null;
170+ };
171+
109 return (
172 return (
110− <Grid gap={4} className={className}>
173+ <Grid className={cn(className, 'auto-rows-min')}>
111− <Grid.Item full as={Steps} num={2} total={3} />
174+ <Grid.Item full as={Steps} num={3} total={3} />
112 <Grid.Item full as={H4}>
175 <Grid.Item full as={H4}>
113 {label}
176 {label}
114 </Grid.Item>
177 </Grid.Item>
115− <Grid.Item full as={Grid}>
178+ <Grid.Item full as={Grid} className="mt3" gap={3}>
116 <Grid.Item full as={LoadingBar} progress={progress} />
179 <Grid.Item full as={LoadingBar} progress={progress} />
117 <Grid.Item full>
180 <Grid.Item full>
118 <Text className="f5 green4">
181 <Text className="f5 green4">
@@ -122,28 +185,7 @@ export default function PassportTransfer({ className }) {
122 </Grid.Item>
185 </Grid.Item>
123 </Grid.Item>
186 </Grid.Item>
124
187
125− {progress < 0 && (
188+ {renderAdditionalInfo()}
126− <Grid.Item full as={WarningBox}>
127− Never give your Master Ticket to anyone
128− </Grid.Item>
129− )}
130−
131− {needFunds && (
132− <Grid.Item full>
133− <Highlighted>
134− The address {needFunds.address} needs at least{' '}
135− {fromWei(needFunds.minBalance)} ETH and currently has{' '}
136− {fromWei(needFunds.balance)} ETH. Waiting until the account has
137− enough funds.
138− </Highlighted>
139− </Grid.Item>
140− )}
141−
142− {generalError && (
143− <Grid.Item full>
144− <ErrorText>{generalError.message.toString()}</ErrorText>
145− </Grid.Item>
146− )}
147 </Grid>
189 </Grid>
148 );
190 );
149}
191}
src/views/Activate/PassportVerify.js+5−3
@@ -1,5 +1,6 @@
1import React, { useCallback, useMemo } from 'react';
1import React, { useCallback, useMemo } from 'react';
2+import cn from 'classnames';
2−import { Grid, H4, P, Input, Text } from 'indigo-react';
3+import { Grid, H4, Input, P } from 'indigo-react';
3
4
4import * as need from 'lib/need';
5import * as need from 'lib/need';
5import { useLocalRouter } from 'lib/LocalRouter';
6import { useLocalRouter } from 'lib/LocalRouter';
@@ -33,12 +34,12 @@ export default function PassportVerify({ className }) {
33 const { pass } = ticketInput;
34 const { pass } = ticketInput;
34
35
35 return (
36 return (
36− <Grid gap={4} className={className}>
37+ <Grid className={cn(className, 'auto-rows-min')}>
37 <Grid.Item full as={Steps} num={2} total={3} />
38 <Grid.Item full as={Steps} num={2} total={3} />
38 <Grid.Item full as={H4}>
39 <Grid.Item full as={H4}>
39 Verify Passport
40 Verify Passport
40 </Grid.Item>
41 </Grid.Item>
41− <Grid.Item full as={Text} className="f5">
42+ <Grid.Item full as={P}>
42 After you download your passport, verify your custody. Your passport
43 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 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 Open it and enter the 4 word phrase below (with hyphens).
@@ -46,6 +47,7 @@ export default function PassportVerify({ className }) {
46 <Grid.Item full as={Input} {...ticketInput} />
47 <Grid.Item full as={Input} {...ticketInput} />
47 <Grid.Item
48 <Grid.Item
48 full
49 full
50+ className="mt3"
49 as={ForwardButton}
51 as={ForwardButton}
50 disabled={!pass}
52 disabled={!pass}
51 onClick={goToTransfer}
53 onClick={goToTransfer}
src/views/Activate/useActivateFlowState.js+9−1
@@ -1,4 +1,4 @@
1−import { useState } from 'react';
1+import { useCallback, useState } from 'react';
2import Maybe from 'folktale/maybe';
2import Maybe from 'folktale/maybe';
3
3
4export default function useActivateFlowState() {
4export default function useActivateFlowState() {
@@ -7,6 +7,13 @@ export default function useActivateFlowState() {
7 const [derivedPoint, setDerivedPoint] = useState(Maybe.Nothing());
7 const [derivedPoint, setDerivedPoint] = useState(Maybe.Nothing());
8 const [generated, setGenerated] = useState(false);
8 const [generated, setGenerated] = useState(false);
9
9
10+ const reset = useCallback(() => {
11+ setDerivedWallet(Maybe.Nothing());
12+ setInviteWallet(Maybe.Nothing());
13+ setDerivedPoint(Maybe.Nothing());
14+ setGenerated(false);
15+ }, [setDerivedWallet, setInviteWallet, setDerivedPoint, setGenerated]);
16+
10 return {
17 return {
11 derivedWallet,
18 derivedWallet,
12 setDerivedWallet,
19 setDerivedWallet,
@@ -16,5 +23,6 @@ export default function useActivateFlowState() {
16 setDerivedPoint,
23 setDerivedPoint,
17 generated,
24 generated,
18 setGenerated,
25 setGenerated,
26+ reset,
19 };
27 };
20}
28}
src/views/Invite/InviteEmail.js+1−1
@@ -114,7 +114,7 @@ export default function InviteEmail() {
114 inputConfigs,
114 inputConfigs,
115 { append: appendInput, removeAt: removeInputAt },
115 { append: appendInput, removeAt: removeInputAt },
116 ] = useArray(
116 ] = useArray(
117− [buildInputConfig({ label: 'Email Address' })],
117+ [buildInputConfig({ label: 'Email Address', autoFocus: true })],
118 buildInputConfig
118 buildInputConfig
119 );
119 );
120
120