on master help sign in
Merge pull request #176 from urbit/refactor/invite-email
01c95c6Matt Condon
package-lock.json+5
@@ -14511,6 +14511,11 @@
14511 }
14511 }
14512 }
14512 }
14513 },
14513 },
14514+ "react-teleporter": {
14515+ "version": "1.1.0",
14516+ "resolved": "https://registry.npmjs.org/react-teleporter/-/react-teleporter-1.1.0.tgz",
14517+ "integrity": "sha512-6j3I29y3sRcekOP1IfbIbrrtwZGKQhRU3WFZoWgMtXN0lTA2/i9Yp3zzAkVBFRtZIfhux4M7tSYVu4AQjCRDlQ=="
14518+ },
14514 "read-pkg": {
14519 "read-pkg": {
14515 "version": "1.1.0",
14520 "version": "1.1.0",
14516 "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
14521 "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
package.json+1
@@ -36,6 +36,7 @@
36 "react": "^16.8.6",
36 "react": "^16.8.6",
37 "react-dom": "^16.8.6",
37 "react-dom": "^16.8.6",
38 "react-scripts": "3.0.1",
38 "react-scripts": "3.0.1",
39+ "react-teleporter": "^1.1.0",
39 "secp256k1": "^3.5.2",
40 "secp256k1": "^3.5.2",
40 "sigil-js": "github:urbit/sigil-js#f7f29c5",
41 "sigil-js": "github:urbit/sigil-js#f7f29c5",
41 "trezor-connect": "^7.0.1",
42 "trezor-connect": "^7.0.1",
src/Bridge.js+4−3
@@ -24,9 +24,10 @@ const kIsStubbed = isDevelopment && shouldStubLocal;
24const kInitialRoutes = kIsStubbed
24const kInitialRoutes = kIsStubbed
25 ? [
25 ? [
26 { key: ROUTE_NAMES.LANDING },
26 { key: ROUTE_NAMES.LANDING },
27− { key: ROUTE_NAMES.NETWORK },
27+ { key: ROUTE_NAMES.LOGIN },
28− { key: ROUTE_NAMES.WALLET },
29 { key: ROUTE_NAMES.POINTS },
28 { key: ROUTE_NAMES.POINTS },
29+ { key: ROUTE_NAMES.POINT },
30+ { key: ROUTE_NAMES.INVITE },
30 ]
31 ]
31 : [{ key: ROUTE_NAMES.LANDING }];
32 : [{ key: ROUTE_NAMES.LANDING }];
32
33
@@ -39,7 +40,7 @@ const kInitialWallet = kIsStubbed
39const kInitialMnemonic = kIsStubbed
40const kInitialMnemonic = kIsStubbed
40 ? Just(process.env.REACT_APP_DEV_MNEMONIC)
41 ? Just(process.env.REACT_APP_DEV_MNEMONIC)
41 : Nothing();
42 : Nothing();
42−const kInitialPointCursor = kIsStubbed ? Just(0) : Nothing();
43+const kInitialPointCursor = kIsStubbed ? Just(65792) : Nothing();
43
44
44export default function Bridge() {
45export default function Bridge() {
45 return (
46 return (
src/_walletgen/Walletgen.js+1−1
@@ -4,7 +4,7 @@ import { find } from 'lodash';
4import Header from './components/Header';
4import Header from './components/Header';
5import Footer from './components/Footer';
5import Footer from './components/Footer';
6
6
7−import { PROFILE_STATES, GEN_STATES } from './lib/constants';
7+import { PROFILE_STATES, GEN_STATES } from '../lib/constants';
8
8
9import PrivacyPolicy from './views/PrivacyPolicy';
9import PrivacyPolicy from './views/PrivacyPolicy';
10import Welcome from './views/Welcome';
10import Welcome from './views/Welcome';
src/_walletgen/views/Generate.js+7−2
@@ -2,13 +2,18 @@ import React from 'react';
2import * as sigil from 'sigil-js';
2import * as sigil from 'sigil-js';
3import ob from 'urbit-ob';
3import ob from 'urbit-ob';
4import classnames from 'classnames';
4import classnames from 'classnames';
5−import * as wg from '../lib/lib';
5+import * as wg from '../../lib/walletgen';
6
6
7import Button from '../components/Button';
7import Button from '../components/Button';
8import ReactSVGComponents from '../components/ReactSVGComponents';
8import ReactSVGComponents from '../components/ReactSVGComponents';
9import PaperCollateralRenderer from 'PaperCollateralRenderer';
9import PaperCollateralRenderer from 'PaperCollateralRenderer';
10
10
11+import {
12+ MAX_GALAXY,
13+ MIN_STAR,
14+ MAX_STAR,
15+ GEN_STATES,
11−import { MAX_GALAXY, MIN_STAR, MAX_STAR, GEN_STATES } from '../lib/constants';
16+} from '../../lib/constants';
12
17
13const NEXT_STEP_NUM = 6;
18const NEXT_STEP_NUM = 6;
14
19
src/_walletgen/views/Upload.js+1−1
@@ -5,7 +5,7 @@ import UploadButton from '../components/UploadButton';
5
5
6import { compose } from '../../bridge/lib/lib';
6import { compose } from '../../bridge/lib/lib';
7
7
8−import { PROFILE_STATES } from '../lib/constants';
8+import { PROFILE_STATES } from '../../lib/constants';
9
9
10const NEXT_STEP_NUM = 3;
10const NEXT_STEP_NUM = 3;
11
11
src/assets/back.svgnew file+4
@@ -0,0 +1,4 @@
1+<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2+<path d="M13.8636 6.54545H4.56818V6.63636L8.06818 10.1364L7.20455 11L2.11364 5.90909L7.20455 0.818182L8.06818 1.68182L4.56818 5.18182V5.27273H13.8636V6.54545Z" fill="black"/>
3+<path d="M0 14.0227H16.0909V15.2727H0V14.0227Z" fill="black"/>
4+</svg>
src/components/Blinky.jsnew file+31
@@ -0,0 +1,31 @@
1+import React, { useState, useRef } from 'react';
2+
3+import useInterval from 'lib/useInterval';
4+
5+// TODO: make these characters display as the same width
6+export const kLoadingCharacter = '▓';
7+export const kInterstitialCharacter = '░';
8+const BLINK_AFTER_MS = 2500; // ms
9+
10+export const matchBlinky = obj =>
11+ obj.matchWith({
12+ Nothing: () => <Blinky />,
13+ Just: p => p.value,
14+ });
15+
16+export default function Blinky({
17+ a = kLoadingCharacter,
18+ b = kInterstitialCharacter,
19+}) {
20+ const [value, setValue] = useState(true);
21+ const now = useRef(new Date());
22+
23+ useInterval(() => {
24+ // only start blinking if we've elapsed enough time
25+ if (new Date() - now.current > BLINK_AFTER_MS) {
26+ setValue(val => !val);
27+ }
28+ }, 1000);
29+
30+ return value ? a : b;
31+}
src/components/Buttons.js+2−2
@@ -2,5 +2,5 @@ import React from 'react';
2import { Button } from 'indigo-react';
2import { Button } from 'indigo-react';
3
3
4// TODO: use svg icons
4// TODO: use svg icons
5−export const ForwardButton = props => <Button icon="→" {...props} />;
5+export const ForwardButton = props => <Button accessory="→" {...props} />;
6−export const DownloadButton = props => <Button icon="⬇" {...props} />;
6+export const DownloadButton = props => <Button accessory="⬇" {...props} />;
src/components/Footer.js+4−13
@@ -1,17 +1,8 @@
1−import React, { Children } from 'react';
1+import { createTeleporter } from 'react-teleporter';
2−import { createPortal } from 'react-dom';
3
2
4−const kFooterPortalId = 'footer-portal';
3+const FooterTeleporter = createTeleporter();
5
4
6−function Footer({ children }) {
5+const Footer = FooterTeleporter.Source;
7− return createPortal(
6+Footer.Target = FooterTeleporter.Target;
8− Children.only(children),
9− document.getElementById(kFooterPortalId)
10− );
11−}
12−
13−Footer.Portal = function FooterPortal(props) {
14− return <div {...props} id={kFooterPortalId} />;
15−};
16
7
17export default Footer;
8export default Footer;
src/components/Inputs.js+102−65
@@ -1,5 +1,4 @@
1−import React from 'react';
1+import React, { useState } from 'react';
2−import { Input } from 'indigo-react';
3import * as bip39 from 'bip39';
2import * as bip39 from 'bip39';
4
3
5import InputSigil from 'components/InputSigil';
4import InputSigil from 'components/InputSigil';
@@ -10,10 +9,10 @@ import {
10 validateMnemonic,
9 validateMnemonic,
11 validatePoint,
10 validatePoint,
12 validateTicket,
11 validateTicket,
12+ validateEmail,
13} from 'lib/validators';
13} from 'lib/validators';
14import { prependSig } from 'lib/transformers';
14import { prependSig } from 'lib/transformers';
15−
15+import useForm from 'indigo-react/lib/useForm';
16−const kExampleMnemonic = bip39.generateMnemonic();
17
16
18// const RequiredInput = advancedInput({
17// const RequiredInput = advancedInput({
19// WrappedComponent: Input,
18// WrappedComponent: Input,
@@ -35,38 +34,47 @@ const kExampleMnemonic = bip39.generateMnemonic();
35// validators: [validateEthereumAddress, validateNotEmpty],
34// validators: [validateEthereumAddress, validateNotEmpty],
36// });
35// });
37
36
37+const firstOf = state => state.inputs[0];
38+
38−export function PassphraseInput(props) {
39+export function usePassphraseInput(props) {
39− return (
40+ return firstOf(
40− <Input
41+ useForm([
42+ {
41− type="password"
43+ type: 'password',
42− autoComplete="off"
44+ autoComplete: 'off',
43− placeholder="Passphrase"
45+ placeholder: 'Passphrase',
44− {...props}
46+ ...props,
45− />
47+ },
48+ ])
46 );
49 );
47}
50}
48
51
52+const kExampleMnemonic = bip39.generateMnemonic();
53+const kMnemonicValidators = [validateMnemonic, validateNotEmpty];
49−export function MnemonicInput(props) {
54+export function useMnemonicInput(props) {
50− return (
55+ return firstOf(
51− <Input
56+ useForm([
57+ {
52− type="text"
58+ type: 'text',
53− autoComplete="off"
59+ autoComplete: 'off',
54− placeholder={`e.g. ${kExampleMnemonic}`}
60+ placeholder: `e.g. ${kExampleMnemonic}`,
55− validators={[validateMnemonic, validateNotEmpty]}
61+ validators: kMnemonicValidators,
56− {...props}
62+ ...props,
57− />
63+ },
64+ ])
58 );
65 );
59}
66}
60
67
61−export function HdPathInput(props) {
68+export function useHdPathInput(props) {
62− return (
69+ return firstOf(
63− <Input
70+ useForm([
71+ {
64− type="text"
72+ type: 'text',
65− autoComplete="off"
73+ autoComplete: 'off',
66− placeholder={`${DEFAULT_HD_PATH}`}
74+ placeholder: `e.g. ${DEFAULT_HD_PATH}`,
67− validators={[validateNotEmpty]}
75+ ...props,
68− {...props}
76+ },
69− />
77+ ])
70 );
78 );
71}
79}
72
80
@@ -76,43 +84,72 @@ export function HdPathInput(props) {
76// transformers: [prependSig],
84// transformers: [prependSig],
77// });
85// });
78
86
79−export function PointInput(props) {
87+const kTicketValidators = [validateTicket, validateNotEmpty];
88+const kTicketTransformers = [prependSig];
89+//TODO needs to be fancier, displaying sig and dashes instead of •ing all
90+export function useTicketInput({ initialValue = '~', ...rest }) {
80− return (
91+ return firstOf(
81− <Input
92+ useForm([
93+ {
82− type="text"
94+ type: 'password',
83− placeholder="e.g. ~zod"
95+ placeholder: '~master-ticket',
84− initialValue={props.initialValue || '~'}
96+ validators: kTicketValidators,
85− validators={[validatePoint, validateNotEmpty]}
86− transformers={[prependSig]}
97+ transformers: kTicketTransformers,
87− mono
98+ mono: true,
88− accessory={
89− <InputSigil
90− patp={props.initialValue || '~'}
99+ initialValue,
91− size={68}
100+ ...rest,
92− margin={8}
93− pass={props.pass}
94− onPass={props.setPass}
95− focused={props.focused}
96− onFocus={props.setFocused}
97− error={props.error}
98− onError={props.setError}
99− />
100− }
101+ },
101− {...props}
102+ ])
102− />
103 );
103 );
104}
104}
105
105
106−//TODO needs to be fancier, displaying sig and dashes instead of •ing all
106+const kPointValidators = [validatePoint, validateNotEmpty];
107−export function TicketInput(props) {
107+const kPointTransformers = [prependSig];
108− return (
108+export function usePointInput({ initialValue = '~', ...rest }) {
109− <Input
109+ const [lastValidPoint, setLastValidPoint] = useState('');
110+ const input = firstOf(
111+ useForm([
112+ {
110− type="password"
113+ type: 'text',
111− placeholder="~master-ticket"
114+ placeholder: 'e.g. ~zod',
112− validators={[validateTicket, validateNotEmpty]}
115+ validators: kPointValidators,
113− transformers={[prependSig]}
116+ transformers: kPointTransformers,
114− mono
117+ mono: true,
115− {...props}
118+ initialValue,
116− />
119+ onValue: setLastValidPoint,
120+ ...rest,
121+ },
122+ ])
117 );
123 );
124+
125+ const { pass, focused, error } = input;
126+
127+ return {
128+ ...input,
129+ accessory: lastValidPoint && (
130+ <InputSigil
131+ patp={lastValidPoint}
132+ size={68}
133+ margin={8}
134+ pass={pass}
135+ focused={focused}
136+ error={error}
137+ />
138+ ),
139+ };
118}
140}
141+
142+const kEmailValidators = [validateEmail, validateNotEmpty];
143+export const buildEmailInputConfig = extra => ({
144+ type: 'email',
145+ autoComplete: 'off',
146+ validators: kEmailValidators,
147+ initialValue: '',
148+ ...extra,
149+});
150+
151+// const TicketInput = advancedInput({
152+// WrappedComponent: Input,
153+// validators: [validateTicket, validateNotEmpty],
154+// transformers: [prependSig],
155+// });
src/components/LoadableButton.jsnew file+7
@@ -0,0 +1,7 @@
1+import React from 'react';
2+import { Button } from 'indigo-react';
3+
4+// TODO: implement loading bar behind text
5+export default function LoadableButton({ ...rest }) {
6+ return <Button {...rest}></Button>;
7+}
src/components/MiniBackButton.jsnew file+11
@@ -0,0 +1,11 @@
1+import React from 'react';
2+import cn from 'classnames';
3+import { IconButton } from 'indigo-react';
4+
5+import { ReactComponent as Back } from 'assets/back.svg';
6+
7+export default ({ className, ...rest }) => (
8+ <IconButton {...rest}>
9+ <Back className={cn('black', className)} />
10+ </IconButton>
11+);
src/components/Passport.js+8−6
@@ -3,19 +3,21 @@ import * as ob from 'urbit-ob';
3import { Grid, Flex } from 'indigo-react';
3import { Grid, Flex } from 'indigo-react';
4import { times } from 'lodash';
4import { times } from 'lodash';
5
5
6−import Sigil from './Sigil';
7import usePointBirthday from 'lib/usePointBirthday';
6import usePointBirthday from 'lib/usePointBirthday';
8import { formatDots } from 'lib/dateFormat';
7import { formatDots } from 'lib/dateFormat';
9−import { kLoadingCharacter } from 'lib/loadingCharacter';
10
8
11−const kLoadingDate = [4, 2, 2]
9+import Sigil from './Sigil';
10+import Blinky, { kLoadingCharacter, kInterstitialCharacter } from './Blinky';
11+
12+const buildDate = char =>
12− .map(t => times(t, () => kLoadingCharacter).join(''))
13+ [4, 2, 2].map(t => times(t, () => char).join('')).join('.');
13− .join('.');
14+const kDateA = buildDate(kLoadingCharacter);
15+const kDateB = buildDate(kInterstitialCharacter);
14
16
15export default function Passport({ point }) {
17export default function Passport({ point }) {
16 const birthday = usePointBirthday(point);
18 const birthday = usePointBirthday(point);
17 const birthDate = birthday.matchWith({
19 const birthDate = birthday.matchWith({
18− Nothing: () => kLoadingDate,
20+ Nothing: () => <Blinky a={kDateA} b={kDateB} />,
19 Just: p => formatDots(p.value),
21 Just: p => formatDots(p.value),
20 });
22 });
21 const name = ob.patp(point);
23 const name = ob.patp(point);
src/components/View.js+1−1
@@ -20,7 +20,7 @@ function View({ className, children, full = false, ...rest }) {
20 )}
20 )}
21 {...rest}>
21 {...rest}>
22 <Flex.Item className="pv5">{children}</Flex.Item>
22 <Flex.Item className="pv5">{children}</Flex.Item>
23− <Footer.Portal />
23+ <Footer.Target />
24 </Flex>
24 </Flex>
25 );
25 );
26}
26}
src/components/old/Header.js+1−5
@@ -5,18 +5,14 @@ import { getRouteBreadcrumb } from '../../lib/router';
5import { ROUTE_NAMES } from '../../lib/routeNames';
5import { ROUTE_NAMES } from '../../lib/routeNames';
6import { useHistory } from '../../store/history';
6import { useHistory } from '../../store/history';
7import { isLast } from '../../lib/lib';
7import { isLast } from '../../lib/lib';
8−import { useNetwork } from '../../store/network';
9−import { useWallet } from '../../store/wallet';
10import { usePointCursor } from '../../store/pointCursor';
8import { usePointCursor } from '../../store/pointCursor';
11
9
12// hook to create a breadcrumb builder function based on the current
10// hook to create a breadcrumb builder function based on the current
13// global states
11// global states
14function useRouteBreadcrumbBuilder() {
12function useRouteBreadcrumbBuilder() {
15− const { networkType } = useNetwork();
16− const { wallet } = useWallet();
17 const { pointCursor } = usePointCursor();
13 const { pointCursor } = usePointCursor();
18
14
19− return getRouteBreadcrumb({ pointCursor, networkType, wallet });
15+ return getRouteBreadcrumb({ pointCursor });
20}
16}
21
17
22function Crumbs(props) {
18function Crumbs(props) {
src/components/old/HorizontalSelector.js+1−1
@@ -22,8 +22,8 @@ class HorizontalSelector extends React.Component {
22 let active = opt.value === this.state.option ? 'btn-primary active' : '';
22 let active = opt.value === this.state.option ? 'btn-primary active' : '';
23 return (
23 return (
24 <button
24 <button
25+ key={opt.value.toString()}
25 className={`btn ${active} h-10 ph-4 flex sans ${this.props.className}`}
26 className={`btn ${active} h-10 ph-4 flex sans ${this.props.className}`}
26− value={opt.value}
27 onClick={() => this.selectOption(opt.value)}>
27 onClick={() => this.selectOption(opt.value)}>
28 {opt.title}
28 {opt.title}
29 </button>
29 </button>
src/indigo-react/components/AccessoryIcon.jsnew file+32
@@ -0,0 +1,32 @@
1+import React from 'react';
2+
3+import Flex from './Flex';
4+
5+const PENDING_ACCESSORY = '⋯';
6+const SUCCESS_ACCESSORY = '✔';
7+const FAILURE_ACCESSORY = '×';
8+
9+function AccessoryIcon({ ...props }) {
10+ return (
11+ <Flex
12+ justify="center"
13+ align="center"
14+ style={{ height: '100%', width: '100%' }}
15+ {...props}
16+ />
17+ );
18+}
19+
20+AccessoryIcon.Pending = () => (
21+ <AccessoryIcon>{PENDING_ACCESSORY}</AccessoryIcon>
22+);
23+
24+AccessoryIcon.Success = () => (
25+ <AccessoryIcon>{SUCCESS_ACCESSORY}</AccessoryIcon>
26+);
27+
28+AccessoryIcon.Failure = () => (
29+ <AccessoryIcon>{FAILURE_ACCESSORY}</AccessoryIcon>
30+);
31+
32+export default AccessoryIcon;
src/indigo-react/components/Button.js+14−7
@@ -7,10 +7,12 @@ import { HelpText } from './Typography';
7
7
8export default function Button({
8export default function Button({
9 solid = false,
9 solid = false,
10+ success = false,
10 disabled = false,
11 disabled = false,
11 detail,
12 detail,
12 className,
13 className,
13− icon = '→',
14+ accessory = '→',
15+ onClick,
14 children,
16 children,
15 ...rest
17 ...rest
16}) {
18}) {
@@ -24,12 +26,16 @@ export default function Button({
24 as="a"
26 as="a"
25 gap={4}
27 gap={4}
26 className={cn(
28 className={cn(
27− 'pointer p4 truncate flex-row justify-between',
29+ 'pointer pv4 truncate flex-row justify-between',
28 {
30 {
31+ p4: solid,
32+ },
33+ {
34+ 'bg-green3': success && !disabled,
35+ 'bg-green1': success && disabled,
29− 'bg-black bg-gray6-hover': solid && !disabled,
36+ 'bg-black': !success && solid && !disabled,
30− 'bg-gray3': solid && disabled,
31− 'bg-transparent bg-gray1-hover': !solid && !disabled,
37+ 'bg-gray3': !success && solid && disabled,
32− 'bg-transparent': !solid && disabled,
38+ 'bg-transparent': !success && !solid,
33 },
39 },
34 className
40 className
35 )}
41 )}
@@ -39,10 +45,11 @@ export default function Button({
39 cursor: 'not-allowed',
45 cursor: 'not-allowed',
40 }),
46 }),
41 }}
47 }}
48+ onClick={!disabled && onClick ? onClick : undefined}
42 {...rest}>
49 {...rest}>
43 <Grid.Item as={Flex} justify="between" full>
50 <Grid.Item as={Flex} justify="between" full>
44 <span className={cn(textColor)}>{children}</span>
51 <span className={cn(textColor)}>{children}</span>
45− <div className={cn('pl4', textColor)}>{icon}</div>
52+ <div className={cn('pl4', textColor)}>{accessory}</div>
46 </Grid.Item>
53 </Grid.Item>
47 {detail && (
54 {detail && (
48 <Grid.Item as={HelpText} full>
55 <Grid.Item as={HelpText} full>
src/indigo-react/components/Flex.js+1−1
@@ -1,7 +1,7 @@
1import React from 'react';
1import React from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3
3
4−import getComponent from 'indigo-react/lib/getComponent';
4+import getComponent from '../lib/getComponent';
5
5
6function Flex({
6function Flex({
7 as = 'div',
7 as = 'div',
src/indigo-react/components/Grid.js+6
@@ -21,6 +21,8 @@ Grid.Item = function GridItem({
21 fourth = 0,
21 fourth = 0,
22 rows = [],
22 rows = [],
23 cols = [],
23 cols = [],
24+ justifySelf,
25+ alignSelf,
24 className,
26 className,
25 ...rest
27 ...rest
26}) {
28}) {
@@ -38,6 +40,10 @@ Grid.Item = function GridItem({
38 [`r${rows[0]}-${rows[1]}`]: rows.length === 2,
40 [`r${rows[0]}-${rows[1]}`]: rows.length === 2,
39 [`c${cols[0]}-${cols[1]}`]: cols.length === 2,
41 [`c${cols[0]}-${cols[1]}`]: cols.length === 2,
40 },
42 },
43+ {
44+ [`justify-self-${justifySelf}`]: justifySelf,
45+ [`align-self-${alignSelf}`]: alignSelf,
46+ },
41 className
47 className
42 )}
48 )}
43 {...rest}
49 {...rest}
src/indigo-react/components/IconButton.jsnew file+51
@@ -0,0 +1,51 @@
1+import React from 'react';
2+import cn from 'classnames';
3+
4+import Flex from './Flex';
5+
6+export default function IconButton({
7+ children,
8+ className,
9+ onClick,
10+ disabled = false,
11+ secondary = false,
12+ solid = false,
13+ ...rest
14+}) {
15+ return (
16+ <Flex
17+ as="a"
18+ align="center"
19+ justify="center"
20+ className={cn(
21+ 'pointer p2',
22+ {
23+ 'bg-black': solid && !secondary && !disabled,
24+ 'bg-gray3': (solid && disabled) || (solid && secondary),
25+ 'bg-gray2': !solid && secondary && disabled,
26+ 'bg-transparent': !solid,
27+ },
28+ {
29+ white: solid,
30+ black: !solid && !disabled,
31+ gray4: !solid && disabled,
32+ },
33+ className
34+ )}
35+ style={{
36+ width: '1rem',
37+ height: '1rem',
38+ fontSize: '1rem',
39+ fontWeight: 'bold',
40+ // TODO: ^ replace with 1:1 svgs and remove explicit sizing
41+ ...(disabled && {
42+ pointerEvents: 'none',
43+ cursor: 'not-allowed',
44+ }),
45+ }}
46+ onClick={onClick ? () => !disabled && onClick() : undefined}
47+ {...rest}>
48+ {children}
49+ </Flex>
50+ );
51+}
src/indigo-react/components/Input.js+64−145
@@ -1,182 +1,85 @@
1−import React, { useCallback, useEffect, useMemo, useState } from 'react';
1+import React, { useCallback, useEffect } from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3
3
4−import { Flex } from 'indigo-react';
4+import Flex from './Flex';
5−
5+import { ErrorText } from './Typography';
6−import { compose } from 'lib/lib';
7−import { kDefaultValidator } from 'lib/validators';
8−
9−function useInput({
10− validators = [],
11− transformers = [],
12− initialValue = '',
13− autoFocus = false,
14−}) {
15− const [value, setValue] = useState(initialValue);
16− const [focused, setFocused] = useState(autoFocus);
17− const [hasBeenFocused, setHasBeenFocused] = useState(false);
18−
19− // NB(shrugs)
20− // TL;DR: This is what we want, the exhaustive deps rule is being too nosy.
21− //
22− // Here we memo-construct the `validate` and `transform` functions
23− // based on the prop arguments. We explicitly disable the exhausive deps rule
24− // because eslint wants an inline array specifying our dependencies.
25− //
26− // If we were to place the array of validators or transformers into
27− // the dependency array like `[validators]`, we would end up creating a new
28− // `validate` function every render, since deps are shallow-compared with
29− // `Object.is`. Because the array of validators are constructed as part of
30− // a component's render() function like:
31− //
32− // export function MnemonicInput(props) {
33− // return (
34− // <Input
35− // type="text"
36− // autoComplete="off"
37− // placeholder={`e.g. ${kExampleMnemonic}`}
38− // validators={[validateMnemonic, validateNotEmpty]}
39− // {...props}
40− // />
41− // );
42− // }
43− //
44− // we create a new array, which shallowly compares as not-equal
45− // between renders.
46− //
47− // So, in this specific case, we disable the rule and pass the
48− // array of validators (and transformers) themselves as dependencies.
49− // This means that validators and transformers can change between renders
50− // and we will re-create the composed `validate` and `transform` functions
51− // as expected.
52− //
53− // For context, an alternative solution I tried was defining the array of
54− // validators as a constant array outside of the render method, but:
55− // 1. It wasn't as clean or useful as specifying them inline,
56− // 2. It would mean that validators could not be updated between renders,
57− // 3. And the situation we have now feels naturally more 'correct'.
58− //
59− /* eslint-disable react-hooks/exhaustive-deps */
60− const transform = useMemo(() => compose(...transformers), transformers);
61− const validate = useMemo(
62− () =>
63− compose(
64− ...validators,
65− kDefaultValidator
66− ),
67− validators
68− );
69− /* eslint-enable react-hooks/exhaustive-deps */
70−
71− const onChange = useCallback(
72− e => setValue(transform(e.target.value)), //
73− [transform]
74− );
75− const onFocus = useCallback(
76− e => setFocused(true), //
77− [setFocused]
78− );
79− const onBlur = useCallback(
80− e => {
81− setFocused(false);
82− setHasBeenFocused(true);
83− },
84− [setFocused, setHasBeenFocused]
85− );
86−
87− // memo-compute whether or not the current value is valid
88− const { pass, error: _error, data } = useMemo(
89− () => validate(value), //
90− [validate, value]
91− );
92−
93− // only set error if it should be visible
94− const error = !pass && hasBeenFocused && !focused && _error;
95− // visibly tell the user that their input has passed if it has passed
96− // and they are or have interacted with the input before
97− const visiblyPassed = pass && (hasBeenFocused || focused);
98−
99− return {
100− pass,
101− visiblyPassed,
102− error,
103− data,
104− focused,
105− bind: {
106− value,
107− onChange,
108− focused: focused ? 'true' : 'false',
109− // ^ dom wants string value for focused
110− onFocus,
111− onBlur,
112− },
113− };
114−}
115
6
116export default function Input({
7export default function Input({
8+ // visuals
117 name,
9 name,
118 label,
10 label,
119− initialValue = '',
120− className = '',
11+ className,
121− validators = [],
12+ accessory,
122− transformers = [],
13+ mono = false,
14+
15+ // callbacks
123 onValue,
16 onValue,
124 onPass,
17 onPass,
125 onError,
18 onError,
126 onFocus,
19 onFocus,
127 onEnter,
20 onEnter,
128− accessory,
21+
129− disabled = false,
22+ // state from hook
130− mono = false,
23+ focused,
24+ pass,
25+ visiblyPassed,
26+ error,
27+ data,
28+ bind,
131− autoFocus = false,
29+ autoFocus,
30+ disabled,
31+
32+ // ignored
33+ initialValue,
34+ validators,
35+ transformers,
36+
37+ // extra
132 ...rest
38 ...rest
133}) {
39}) {
134− const actuallyAutoFocus = autoFocus && !disabled;
40+ // NB(shrugs): we disable exhaustive deps because we don't want the callbacks
135− const { focused, pass, visiblyPassed, error, data, bind } = useInput({
41+ // (whose identity might change between renders) to trigger a re-notify of
136− validators,
42+ // state that we already know
137− transformers,
43+ // this also happens to prevent render loops when rendering a map of inputs
138− initialValue,
139− autoFocus: actuallyAutoFocus,
140− });
141
44
142− // notify parent of enter keypress iff not disabled
45+ // notify parent of enter keypress iff not disabled and passing
143 const onKeyPress = useCallback(
46 const onKeyPress = useCallback(
144− e => !disabled && e.key === 'Enter' && onEnter && onEnter(),
47+ e => !disabled && pass && e.key === 'Enter' && onEnter && onEnter(),
145− [disabled, onEnter]
48+ [disabled, pass] // eslint-disable-line react-hooks/exhaustive-deps
146 );
49 );
147
50
148 // notify parent of value only when passing
51 // notify parent of value only when passing
149 useEffect(() => {
52 useEffect(() => {
150 pass && onValue && onValue(data);
53 pass && onValue && onValue(data);
151− }, [pass, data, onValue]);
54+ }, [pass, data]); // eslint-disable-line react-hooks/exhaustive-deps
152
55
153 // notify parent of pass
56 // notify parent of pass
154 useEffect(() => {
57 useEffect(() => {
155 onPass && onPass(pass);
58 onPass && onPass(pass);
156− }, [pass, onPass]);
59+ }, [pass]); // eslint-disable-line react-hooks/exhaustive-deps
157
60
158 // notify parent of error whenever error changes
61 // notify parent of error whenever error changes
159 useEffect(() => {
62 useEffect(() => {
160 onError && onError(error);
63 onError && onError(error);
161− }, [error, onError]);
64+ }, [error]); // eslint-disable-line react-hooks/exhaustive-deps
162
65
163 // notify parent of focus
66 // notify parent of focus
164 useEffect(() => {
67 useEffect(() => {
165 onFocus && onFocus(focused);
68 onFocus && onFocus(focused);
166− }, [focused, onFocus]);
69+ }, [focused]); // eslint-disable-line react-hooks/exhaustive-deps
167
70
168 return (
71 return (
169− <Flex col>
72+ <Flex className={className} col>
170 <Flex.Item as="label" className="f6 lh-tall" htmlFor={name}>
73 <Flex.Item as="label" className="f6 lh-tall" htmlFor={name}>
171 {label}
74 {label}
172 </Flex.Item>
75 </Flex.Item>
173− <Flex row>
76+ <Flex.Item as={Flex} className="rel" row>
174 <Flex.Item
77 <Flex.Item
175 as="input"
78 as="input"
176 {...rest}
79 {...rest}
177 cols={[1, accessory ? 11 : 13]}
80 cols={[1, accessory ? 11 : 13]}
178 className={cn(
81 className={cn(
179− 'b b1 p3',
82+ 'rel b b1 p3 outline-none',
180 { mono },
83 { mono },
181 {
84 {
182 'bg-white': !disabled,
85 'bg-white': !disabled,
@@ -191,24 +94,40 @@ export default function Input({
191 'b-black': focused && !visiblyPassed,
94 'b-black': focused && !visiblyPassed,
192 'b-yellow3': !focused && !visiblyPassed && error,
95 'b-yellow3': !focused && !visiblyPassed && error,
193 'b-gray3': !focused && !error && !visiblyPassed,
96 'b-gray3': !focused && !error && !visiblyPassed,
194− },
97+ }
195− className
98+ // TODO: inputClassName ?
196 )}
99 )}
197 style={{
100 style={{
101+ ...(disabled && {
198− ...(disabled && { pointerEvents: 'none', cursor: 'not-allowed' }),
102+ pointerEvents: 'none',
103+ cursor: 'not-allowed',
104+ }),
199 }}
105 }}
200− onKeyPress={onKeyPress}
201 id={name}
106 id={name}
202− autoFocus={actuallyAutoFocus}
107+ name={name}
108+ onKeyPress={onKeyPress}
203 {...bind}
109 {...bind}
204 flex
110 flex
205 />
111 />
206 {accessory && (
112 {accessory && (
113+ <div
114+ className="abs"
115+ style={{
116+ top: 0,
117+ right: 0,
118+ height: '100%',
119+ width: '44px',
207− <Flex.Item style={{ height: '45px', overflow: 'hidden' }}>
120+ overflow: 'hidden',
121+ }}>
208 {accessory}
122 {accessory}
209− </Flex.Item>
123+ </div>
210 )}
124 )}
211− </Flex>
125+ </Flex.Item>
126+ {error && (
127+ <Flex.Item as={ErrorText} className="mv1">
128+ {error}
129+ </Flex.Item>
130+ )}
212 </Flex>
131 </Flex>
213 );
132 );
214}
133}
src/indigo-react/components/Typography.js+1
@@ -21,3 +21,4 @@ export const P = typeWith('p');
21export const Breadcrumb = typeWith('span', 'f6');
21export const Breadcrumb = typeWith('span', 'f6');
22export const Text = typeWith('span', 'f5');
22export const Text = typeWith('span', 'f5');
23export const HelpText = typeWith('span', 'f6 gray4');
23export const HelpText = typeWith('span', 'f6 gray4');
24+export const ErrorText = typeWith('span', 'f6 red3');
src/indigo-react/index.js+2
@@ -2,6 +2,8 @@ export { default as IndigoApp } from './components/IndigoApp';
2export { default as Grid } from './components/Grid';
2export { default as Grid } from './components/Grid';
3export { default as Flex } from './components/Flex';
3export { default as Flex } from './components/Flex';
4export { default as Input } from './components/Input';
4export { default as Input } from './components/Input';
5+export { default as AccessoryIcon } from './components/AccessoryIcon';
5export { default as Button } from './components/Button';
6export { default as Button } from './components/Button';
7+export { default as IconButton } from './components/IconButton';
6
8
7export * from './components/Typography';
9export * from './components/Typography';
src/indigo-react/indigo.css+66−21
@@ -13,7 +13,7 @@
13 * git+https://github.com/urbit/Indigo.git
13 * git+https://github.com/urbit/Indigo.git
14 * Bugs: https://github.com/urbit/Indigo/issues
14 * Bugs: https://github.com/urbit/Indigo/issues
15 * License: MIT
15 * License: MIT
16− * Timestamp: Wed Jun 19 2019 17:17:14 GMT-0700 (Pacific Daylight Time)
16+ * Timestamp: Mon Jun 24 2019 19:57:25 GMT-0700 (Pacific Daylight Time)
17 *******************************************************************************/
17 *******************************************************************************/
18/**********************
18/**********************
19Color variables
19Color variables
@@ -1575,19 +1575,34 @@ caption,
1575 justify-content: center;
1575 justify-content: center;
1576 align-items: center; }
1576 align-items: center; }
1577
1577
1578−.self-start {
1578+.align-self-start {
1579 align-self: flex-start; }
1579 align-self: flex-start; }
1580
1580
1581−.self-end {
1581+.align-self-end {
1582 align-self: flex-end; }
1582 align-self: flex-end; }
1583
1583
1584−.self-center {
1584+.align-self-center {
1585 align-self: center; }
1585 align-self: center; }
1586
1586
1587−.self-baseline {
1587+.align-self-baseline {
1588 align-self: baseline; }
1588 align-self: baseline; }
1589
1589
1590+.align-self-stretch {
1591+ align-self: stretch; }
1592+
1593+.justify-self-start {
1594+ justify-self: flex-start; }
1595+
1596+.justify-self-end {
1597+ justify-self: flex-end; }
1598+
1599+.justify-self-center {
1600+ justify-self: center; }
1601+
1602+.justify-self-baseline {
1603+ justify-self: baseline; }
1604+
1590−.self-stretch {
1605+.align-self-stretch {
1591 align-self: stretch; }
1606 align-self: stretch; }
1592
1607
1593.content-start {
1608.content-start {
@@ -1648,16 +1663,26 @@ caption,
1648 .flex-center-ns {
1663 .flex-center-ns {
1649 justify-content: center;
1664 justify-content: center;
1650 align-items: center; }
1665 align-items: center; }
1651− .self-start-ns {
1666+ .align-self-start-ns {
1652 align-self: flex-start; }
1667 align-self: flex-start; }
1653− .self-end-ns {
1668+ .align-self-end-ns {
1654 align-self: flex-end; }
1669 align-self: flex-end; }
1655− .self-center-ns {
1670+ .align-self-center-ns {
1656 align-self: center; }
1671 align-self: center; }
1657− .self-baseline-ns {
1672+ .align-self-baseline-ns {
1658 align-self: baseline; }
1673 align-self: baseline; }
1659− .self-stretch-ns {
1674+ .align-self-stretch-ns {
1660 align-self: stretch; }
1675 align-self: stretch; }
1676+ .justify-self-start-ns {
1677+ justify-self: flex-start; }
1678+ .justify-self-end-ns {
1679+ justify-self: flex-end; }
1680+ .justify-self-center-ns {
1681+ justify-self: center; }
1682+ .justify-self-baseline-ns {
1683+ justify-self: baseline; }
1684+ .justify-self-stretch-ns {
1685+ justify-self: stretch; }
1661 .content-start-ns {
1686 .content-start-ns {
1662 align-content: flex-start; }
1687 align-content: flex-start; }
1663 .content-end-ns {
1688 .content-end-ns {
@@ -1711,16 +1736,26 @@ caption,
1711 .flex-center-md {
1736 .flex-center-md {
1712 justify-content: center;
1737 justify-content: center;
1713 align-items: center; }
1738 align-items: center; }
1714− .self-start-md {
1739+ .align-self-start-md {
1715 align-self: flex-start; }
1740 align-self: flex-start; }
1716− .self-end-md {
1741+ .align-self-end-md {
1717 align-self: flex-end; }
1742 align-self: flex-end; }
1718− .self-center-md {
1743+ .align-self-center-md {
1719 align-self: center; }
1744 align-self: center; }
1720− .self-baseline-md {
1745+ .align-self-baseline-md {
1721 align-self: baseline; }
1746 align-self: baseline; }
1722− .self-stretch-md {
1747+ .align-self-stretch-md {
1723 align-self: stretch; }
1748 align-self: stretch; }
1749+ .justify-self-start-md {
1750+ justify-self: flex-start; }
1751+ .justify-self-end-md {
1752+ justify-self: flex-end; }
1753+ .justify-self-center-md {
1754+ justify-self: center; }
1755+ .justify-self-baseline-md {
1756+ justify-self: baseline; }
1757+ .justify-self-stretch-md {
1758+ justify-self: stretch; }
1724 .content-start-md {
1759 .content-start-md {
1725 align-content: flex-start; }
1760 align-content: flex-start; }
1726 .content-end-md {
1761 .content-end-md {
@@ -1774,16 +1809,26 @@ caption,
1774 .flex-center-lg {
1809 .flex-center-lg {
1775 justify-content: center;
1810 justify-content: center;
1776 align-items: center; }
1811 align-items: center; }
1777− .self-start-lg {
1812+ .align-self-start-lg {
1778 align-self: flex-start; }
1813 align-self: flex-start; }
1779− .self-end-lg {
1814+ .align-self-end-lg {
1780 align-self: flex-end; }
1815 align-self: flex-end; }
1781− .self-center-lg {
1816+ .align-self-center-lg {
1782 align-self: center; }
1817 align-self: center; }
1783− .self-baseline-lg {
1818+ .align-self-baseline-lg {
1784 align-self: baseline; }
1819 align-self: baseline; }
1785− .self-stretch-lg {
1820+ .align-self-stretch-lg {
1786 align-self: stretch; }
1821 align-self: stretch; }
1822+ .justify-self-start-lg {
1823+ justify-self: flex-start; }
1824+ .justify-self-end-lg {
1825+ justify-self: flex-end; }
1826+ .justify-self-center-lg {
1827+ justify-self: center; }
1828+ .justify-self-baseline-lg {
1829+ justify-self: baseline; }
1830+ .justify-self-stretch-lg {
1831+ justify-self: stretch; }
1787 .content-start-lg {
1832 .content-start-lg {
1788 align-content: flex-start; }
1833 align-content: flex-start; }
1789 .content-end-lg {
1834 .content-end-lg {
src/indigo-react/lib/useForm.jsnew file+188
@@ -0,0 +1,188 @@
1+import { useCallback, useMemo, useState } from 'react';
2+import { keyBy, get, every, some } from 'lodash';
3+
4+import { compose } from 'lib/lib';
5+import { kDefaultValidator } from 'lib/validators';
6+import useSetState from 'lib/useSetState';
7+
8+// interface InputConfig {
9+// name: string;
10+// validators: Function[];
11+// transformers: Function[];
12+// initialValue: string;
13+// autoFocus: boolean;
14+// disabled: boolean;
15+// ...extra,
16+// }
17+
18+const defaultsFor = (configs, mapper) =>
19+ configs.reduce(
20+ (memo, config) => ({ ...memo, [config.name]: mapper(config) }),
21+ {}
22+ );
23+
24+/**
25+ * useForm manages a set of inputs for rendering them in a loop
26+ */
27+export default function useForm(configs = []) {
28+ const byName = useMemo(() => keyBy(configs, 'name'), [configs]);
29+
30+ // track values
31+ const [values, _setValues] = useState(() =>
32+ defaultsFor(configs, config => config.initialValue)
33+ );
34+ const setValue = useCallback(
35+ (name, value) => _setValues(values => ({ ...values, [name]: value })),
36+ [_setValues]
37+ );
38+ // ^ NB(shrugs): because we're not syncing the initialValue of _additional_
39+ // configs that are added, we won't be able to set an initialValue for a
40+ // dynamic form that then triggers validation. for initial configs,
41+ // initialValue can be a validated string and everything will be happy,
42+ // but for dynamically added input configs, we won't get validation, etc
43+ // until there's a traditional state update.
44+
45+ // track focused states
46+ const [focused, setFocused] = useSetState(() =>
47+ defaultsFor(configs, config => config.autoFocus && !config.disabled)
48+ );
49+ const focuses = useMemo(
50+ () => configs.map(config => focused[config.name] || false), //
51+ [configs, focused]
52+ );
53+
54+ // track whether or not input has been focused
55+ const [hasBeenFocused, setHasBeenFocused] = useSetState({});
56+
57+ // build fn that transforms a value by input name
58+ const transform = useCallback(
59+ (name, value) => compose(...get(byName, [name, 'transformers'], []))(value),
60+ [byName]
61+ );
62+
63+ // build fn that validates a value by input name
64+ const validate = useCallback(
65+ (name, value) =>
66+ compose(
67+ ...get(byName, [name, 'validators'], []),
68+ kDefaultValidator
69+ )(value),
70+ [byName]
71+ );
72+
73+ // on change, transform and set value
74+ const onChange = useCallback(
75+ name => e => setValue(name, transform(name, e.target.value)), //
76+ [setValue, transform]
77+ );
78+ // on focus, update focus
79+ const onFocus = useCallback(
80+ name => e => setFocused({ [name]: true }), //
81+ [setFocused]
82+ );
83+
84+ // on blur, defocus and set has been focused
85+ const onBlur = useCallback(
86+ name => e => {
87+ setFocused({ [name]: false });
88+ setHasBeenFocused({ [name]: false });
89+ },
90+ [setFocused, setHasBeenFocused]
91+ );
92+
93+ // memo-compute validations of curent values
94+ const validations = useMemo(
95+ () => configs.map(config => validate(config.name, values[config.name])), //
96+ [configs, validate, values]
97+ );
98+
99+ // memo-compute the set of (perhaps changed by validation) data
100+ const datas = useMemo(() => validations.map(v => v.data), [validations]);
101+
102+ // the input has errored if it
103+ // 1) has been focused before
104+ // 2)
105+ // a) did not pass validation and has an error text
106+ // b) or has a specific error from parent state
107+ // (and we implicitly return the error text at the end)
108+ const errors = useMemo(
109+ () =>
110+ configs.map(
111+ (config, i) =>
112+ hasBeenFocused[config.name] && //
113+ (validations[i].error || config.error)
114+ ),
115+ [configs, validations, hasBeenFocused]
116+ );
117+
118+ // this input has passed if
119+ // 1) it has passed local validation
120+ // 2) there are no errors
121+ const passes = useMemo(
122+ () => validations.map((v, i) => v.pass && !errors[i]),
123+ [validations, errors]
124+ );
125+
126+ // visibly tell the user that their input has passed if
127+ // 1) they are or have interacted with the input before
128+ // 2) and it has passed as defined above
129+ const visiblePasses = useMemo(
130+ () =>
131+ configs.map(
132+ (config, i) =>
133+ (focused[config.name] || hasBeenFocused[config.name]) && //
134+ passes[i]
135+ ),
136+ [configs, hasBeenFocused, focused, passes]
137+ );
138+
139+ // generate the list of input states
140+ const inputs = useMemo(
141+ () =>
142+ configs.map(
143+ ({ name, error, autoFocus, disabled, initialValue, ...rest }, i) => ({
144+ // Input props
145+ name,
146+ data: datas[i],
147+ pass: passes[i],
148+ visiblyPassed: visiblePasses[i],
149+ error: errors[i],
150+ focused: focuses[i],
151+ autoFocus: autoFocus && !disabled,
152+ disabled,
153+ ...rest,
154+ // dom properties below:
155+ bind: {
156+ value: values[name] || initialValue,
157+ onChange: onChange(name),
158+ onFocus: onFocus(name),
159+ onBlur: onBlur(name),
160+ autoFocus: autoFocus && !disabled,
161+ },
162+ })
163+ ),
164+ [
165+ configs,
166+ datas,
167+ visiblePasses,
168+ passes,
169+ errors,
170+ focuses,
171+ values,
172+ onBlur,
173+ onChange,
174+ onFocus,
175+ ]
176+ );
177+
178+ // did all of the inputs pass validation?
179+ const pass = useMemo(() => every(passes), [passes]);
180+ // did any of the inputs error?
181+ const error = useMemo(() => some(errors), [errors]);
182+
183+ return {
184+ inputs,
185+ pass,
186+ error,
187+ };
188+}
src/lib/LocalRouter.jsnew file+9
@@ -0,0 +1,9 @@
1+import { createContext, useContext } from 'react';
2+
3+export const LocalRouterContext = createContext(null);
4+export const LocalRouterProvider = LocalRouterContext.Provider;
5+
6+// Hook version
7+export function useLocalRouter() {
8+ return useContext(LocalRouterContext);
9+}
src/_walletgen/lib/constants.js → src/lib/constants.jsrenamed+1
@@ -11,6 +11,7 @@ const GALAXY_ENTROPY_BITS = 384;
11
11
12const SEED_ENTROPY_BITS = 128;
12const SEED_ENTROPY_BITS = 128;
13
13
14+// TODO: this is walletgen-ui specific, move into a wallet router later
14const GEN_STATES = {
15const GEN_STATES = {
15 ENY_NOSTART: Symbol('ENY_NOSTART'),
16 ENY_NOSTART: Symbol('ENY_NOSTART'),
16 ENY_PENDING: Symbol('ENY_PENDING'),
17 ENY_PENDING: Symbol('ENY_PENDING'),
src/lib/invite.js+9−6
@@ -3,7 +3,7 @@ import Tx from 'ethereumjs-tx';
3
3
4import * as azimuth from 'azimuth-js';
4import * as azimuth from 'azimuth-js';
5import * as kg from 'urbit-key-generation/dist';
5import * as kg from 'urbit-key-generation/dist';
6−import * as wg from '../_walletgen/lib/lib.js';
6+import * as wg from './walletgen';
7import * as tank from './tank';
7import * as tank from './tank';
8
8
9import JSZip from 'jszip';
9import JSZip from 'jszip';
@@ -191,7 +191,7 @@ async function startTransactions(args) {
191 });
191 });
192
192
193 // send transaction
193 // send transaction
194− await sendAndAwaitConfirm(web3, [rawTransferStx], usedTank);
194+ await sendAndAwaitConfirm(web3, [Just(transferStx)], usedTank);
195
195
196 //
196 //
197 // we're gonna be operating as the new wallet from here on out, so change
197 // we're gonna be operating as the new wallet from here on out, so change
@@ -249,10 +249,13 @@ async function startTransactions(args) {
249 return tx;
249 return tx;
250 });
250 });
251
251
252− let rawStxs = txs.map(tx => {
252+ let txPairs = txs.map(tx => {
253 let stx = new Tx(tx);
253 let stx = new Tx(tx);
254 stx.sign(Buffer.from(realWallet.ownership.keys.private, 'hex'));
254 stx.sign(Buffer.from(realWallet.ownership.keys.private, 'hex'));
255+ return {
255− return '0x' + stx.serialize().toString('hex');
256+ raw: '0x' + stx.serialize().toString('hex'),
257+ signed: stx,
258+ };
256 });
259 });
257
260
258 updateProgress({
261 updateProgress({
@@ -264,7 +267,7 @@ async function startTransactions(args) {
264 point,
267 point,
265 newAddress,
268 newAddress,
266 totalCost,
269 totalCost,
267− rawStxs,
270+ txPairs.map(p => p.raw),
268 askForFunding
271 askForFunding
269 );
272 );
270
273
@@ -273,7 +276,7 @@ async function startTransactions(args) {
273 value: TRANSACTION_STATES.CONFIGURING,
276 value: TRANSACTION_STATES.CONFIGURING,
274 });
277 });
275
278
276− await sendAndAwaitConfirm(web3, rawStxs, usedTank);
279+ await sendAndAwaitConfirm(web3, txPairs.map(p => Just(p.signed)), usedTank);
277
280
278 updateProgress({
281 updateProgress({
279 type: 'progress',
282 type: 'progress',
src/lib/loadingCharacter.jsdeleted−7
@@ -1,7 +0,0 @@
1−export const kLoadingCharacter = '▓';
2−
3−export default val =>
4− val.matchWith({
5− Nothing: () => kLoadingCharacter,
6− Just: p => p.value,
7− });
src/lib/need.js+1−2
@@ -22,8 +22,7 @@ export const wallet = needBuilder(BRIDGE_ERROR.MISSING_WALLET);
22export const addressFromWallet = obj => wallet(obj).address;
22export const addressFromWallet = obj => wallet(obj).address;
23export const pointCursor = needBuilder(BRIDGE_ERROR.MISSING_POINT);
23export const pointCursor = needBuilder(BRIDGE_ERROR.MISSING_POINT);
24export const pointCache = needBuilder(BRIDGE_ERROR.MISSING_POINT);
24export const pointCache = needBuilder(BRIDGE_ERROR.MISSING_POINT);
25−export const fromPointCache = (obj, point) => {
25+export const fromPointCache = (cache, point) => {
26− const cache = pointCache(obj);
27 if (!(point in cache)) {
26 if (!(point in cache)) {
28 throw BRIDGE_ERROR.MISSING_POINT;
27 throw BRIDGE_ERROR.MISSING_POINT;
29 }
28 }
src/lib/pluralize.jsnew file+8
@@ -0,0 +1,8 @@
1+export default (count, singular, plural) => {
2+ const isPlural = count > 1;
3+ if (!plural) {
4+ return `${count} ${singular}${isPlural ? 's' : ''}`;
5+ }
6+
7+ return isPlural ? plural : singular;
8+};
src/lib/routeNames.js−9
@@ -11,15 +11,6 @@ export const ROUTE_NAMES = {
11 ACCEPT_TRANSFER: Symbol('ACCEPT_TRANSFER'),
11 ACCEPT_TRANSFER: Symbol('ACCEPT_TRANSFER'),
12 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
12 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
13 LANDING: Symbol('LANDING'),
13 LANDING: Symbol('LANDING'),
14− NETWORK: Symbol('NETWORK'),
15− WALLET: Symbol('WALLET'),
16− MNEMONIC: Symbol('MNEMONIC'),
17− TICKET: Symbol('TICKET'),
18− SHARDS: Symbol('SHARDS'),
19− LEDGER: Symbol('LEDGER'),
20− TREZOR: Symbol('TREZOR'),
21− PRIVATE_KEY: Symbol('PRIVATE_KEY'),
22− KEYSTORE: Symbol('KEYSTORE'),
23 VIEW_POINT: Symbol('VIEW_POINT'),
14 VIEW_POINT: Symbol('VIEW_POINT'),
24 POINTS: Symbol('POINTS'),
15 POINTS: Symbol('POINTS'),
25 POINT: Symbol('POINT'),
16 POINT: Symbol('POINT'),
src/lib/router.js+2−53
@@ -1,22 +1,15 @@
1−import React from 'react';
2import * as ob from 'urbit-ob';
1import * as ob from 'urbit-ob';
3import * as need from './need';
2import * as need from './need';
4
3
5import Landing from '../views/Landing';
4import Landing from '../views/Landing';
5+import Login from '../views/Login.js';
6import InviteTicket from '../views/InviteTicket';
6import InviteTicket from '../views/InviteTicket';
7import InvitesManage from '../views/InvitesManage.js';
7import InvitesManage from '../views/InvitesManage.js';
8−import Login from '../views/Login.js';
9import AcceptTransfer from '../views/AcceptTransfer';
8import AcceptTransfer from '../views/AcceptTransfer';
10import CancelTransfer from '../views/CancelTransfer';
9import CancelTransfer from '../views/CancelTransfer';
11import CreateGalaxy from '../views/CreateGalaxy';
10import CreateGalaxy from '../views/CreateGalaxy';
12import GenKeyfile from '../views/GenKeyfile';
11import GenKeyfile from '../views/GenKeyfile';
13import IssueChild from '../views/IssueChild';
12import IssueChild from '../views/IssueChild';
14−import Network from '../views/Network';
15−import Mnemonic from '../views/Mnemonic';
16−import Ledger from '../views/Ledger';
17−import Trezor from '../views/Trezor';
18−import PrivateKey from '../views/PrivateKey';
19−import Keystore from '../views/Keystore';
20import ViewPoint from '../views/ViewPoint';
13import ViewPoint from '../views/ViewPoint';
21import SentTransaction from '../views/SentTransaction';
14import SentTransaction from '../views/SentTransaction';
22import SetKeys from '../views/SetKeys';
15import SetKeys from '../views/SetKeys';
@@ -27,14 +20,9 @@ import {
27} from '../views/SetProxy';
20} from '../views/SetProxy';
28import Point from '../views/Point';
21import Point from '../views/Point';
29import Points from '../views/Points';
22import Points from '../views/Points';
30−import Shards from '../views/Shards';
31−import Ticket from '../views/Ticket';
32import Transfer from '../views/Transfer';
23import Transfer from '../views/Transfer';
33−import Wallet from '../views/Wallet';
34import Invite from 'views/Invite';
24import Invite from 'views/Invite';
35
25
36−import { EthereumWallet } from './wallet';
37−import { renderNetworkType } from './network';
38import { ROUTE_NAMES } from './routeNames';
26import { ROUTE_NAMES } from './routeNames';
39
27
40export const ROUTES = {
28export const ROUTES = {
@@ -46,16 +34,7 @@ export const ROUTES = {
46 [ROUTE_NAMES.LOGIN]: Login,
34 [ROUTE_NAMES.LOGIN]: Login,
47 [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
35 [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
48 [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
36 [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
49− [ROUTE_NAMES.NETWORK]: Network,
50− [ROUTE_NAMES.WALLET]: Wallet,
51 [ROUTE_NAMES.VIEW_POINT]: ViewPoint,
37 [ROUTE_NAMES.VIEW_POINT]: ViewPoint,
52− [ROUTE_NAMES.MNEMONIC]: Mnemonic,
53− [ROUTE_NAMES.TICKET]: Ticket,
54− [ROUTE_NAMES.SHARDS]: Shards,
55− [ROUTE_NAMES.LEDGER]: Ledger,
56− [ROUTE_NAMES.TREZOR]: Trezor,
57− [ROUTE_NAMES.PRIVATE_KEY]: PrivateKey,
58− [ROUTE_NAMES.KEYSTORE]: Keystore,
59 [ROUTE_NAMES.POINTS]: Points,
38 [ROUTE_NAMES.POINTS]: Points,
60 [ROUTE_NAMES.POINT]: Point,
39 [ROUTE_NAMES.POINT]: Point,
61 [ROUTE_NAMES.SET_MANAGEMENT_PROXY]: SetManagementProxy,
40 [ROUTE_NAMES.SET_MANAGEMENT_PROXY]: SetManagementProxy,
@@ -69,11 +48,7 @@ export const ROUTES = {
69 [ROUTE_NAMES.GEN_KEYFILE]: GenKeyfile,
48 [ROUTE_NAMES.GEN_KEYFILE]: GenKeyfile,
70};
49};
71
50
72−export const getRouteBreadcrumb = ({
51+export const getRouteBreadcrumb = ({ pointCursor }) => route => {
73− pointCursor,
74− networkType,
75− wallet,
76−}) => route => {
77 switch (route.name) {
52 switch (route.name) {
78 case ROUTE_NAMES.INVITE_TICKET:
53 case ROUTE_NAMES.INVITE_TICKET:
79 return 'Invite code';
54 return 'Invite code';
@@ -87,32 +62,6 @@ export const getRouteBreadcrumb = ({
87 return 'Login to Bridge';
62 return 'Login to Bridge';
88 case ROUTE_NAMES.POINT:
63 case ROUTE_NAMES.POINT:
89 return `Overview: ${ob.patp(need.pointCursor(pointCursor))}`;
64 return `Overview: ${ob.patp(need.pointCursor(pointCursor))}`;
90− case ROUTE_NAMES.NETWORK:
91− return renderNetworkType(networkType);
92− case ROUTE_NAMES.WALLET:
93− return wallet.matchWith({
94− Nothing: () => 'Wallet',
95− Just: wal =>
96− wal.value instanceof EthereumWallet ? (
97− <span className="text-mono">{wal.value.address}</span>
98− ) : (
99− <span className="text-mono">{wal.value.address}</span>
100− ),
101− });
102− case ROUTE_NAMES.MNEMONIC:
103− return 'Mnemonic';
104− case ROUTE_NAMES.TICKET:
105− return 'Urbit Ticket';
106− case ROUTE_NAMES.SHARDS:
107− return 'Urbit Ticket';
108− case ROUTE_NAMES.LEDGER:
109− return 'Ledger';
110− case ROUTE_NAMES.TREZOR:
111− return 'Trezor';
112− case ROUTE_NAMES.PRIVATE_KEY:
113− return 'Private Key';
114− case ROUTE_NAMES.KEYSTORE:
115− return 'Keystore File';
116 case ROUTE_NAMES.POINTS:
65 case ROUTE_NAMES.POINTS:
117 return 'Points';
66 return 'Points';
118 case ROUTE_NAMES.VIEW_POINT:
67 case ROUTE_NAMES.VIEW_POINT:
src/lib/tank.js+6−1
@@ -115,7 +115,12 @@ async function waitForBalance(
115 resolve();
115 resolve();
116 } else {
116 } else {
117 if (balance !== oldBalance) {
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
118− askForFunding(address, minBalance, balance);
123+ askForFunding(address, minBalance.toFixed(), balance);
119 }
124 }
120 setTimeout(checkForBalance, 13000);
125 setTimeout(checkForBalance, 13000);
121 }
126 }
src/lib/txn.js+5−2
@@ -9,6 +9,9 @@ import { ledgerSignTransaction } from './ledger';
9import { trezorSignTransaction } from './trezor';
9import { trezorSignTransaction } from './trezor';
10import { WALLET_TYPES, addHexPrefix } from './wallet';
10import { WALLET_TYPES, addHexPrefix } from './wallet';
11
11
12+const CHECK_BLOCK_EVERY_MS =
13+ process.env.NODE_ENV === 'development' ? 1000 : 5000;
14+
12const TXN_PURPOSE = {
15const TXN_PURPOSE = {
13 SET_MANAGEMENT_PROXY: Symbol('SET_MANAGEMENT_PROXY'),
16 SET_MANAGEMENT_PROXY: Symbol('SET_MANAGEMENT_PROXY'),
14 SET_TRANSFER_PROXY: Symbol('SET_TRANSFER_PROXY'),
17 SET_TRANSFER_PROXY: Symbol('SET_TRANSFER_PROXY'),
@@ -161,7 +164,7 @@ const sendSignedTransaction = (web3, stx, doubtNonceError, confirmationCb) => {
161 // if there's a nonce error, but we used the gas tank, it's likely
164 // if there's a nonce error, but we used the gas tank, it's likely
162 // that it's because the tank already submitted our transaction.
165 // that it's because the tank already submitted our transaction.
163 // we just wait for first confirmation here.
166 // we just wait for first confirmation here.
164− console.log('sent and got error', err);
167+ console.error(err);
165 if (
168 if (
166 doubtNonceError &&
169 doubtNonceError &&
167 (err.message || '').includes("the tx doesn't have the correct nonce.")
170 (err.message || '').includes("the tx doesn't have the correct nonce.")
@@ -194,7 +197,7 @@ const waitForTransactionConfirm = (web3, txHash) => {
194 console.log('tried, got', receipt);
197 console.log('tried, got', receipt);
195 let confirmed = receipt !== null;
198 let confirmed = receipt !== null;
196 if (confirmed) resolve(receipt.status === true);
199 if (confirmed) resolve(receipt.status === true);
197− else setTimeout(checkForConfirm, 13000);
200+ else setTimeout(checkForConfirm, CHECK_BLOCK_EVERY_MS);
198 };
201 };
199 checkForConfirm();
202 checkForConfirm();
200 });
203 });
src/lib/useArray.jsnew file+48
@@ -0,0 +1,48 @@
1+import { useCallback, useState } from 'react';
2+import { identity } from 'lodash';
3+
4+/**
5+ * Manages an immutable array of items.
6+ */
7+export default function useArray(initialItems = [], itemBuilder = identity) {
8+ const [items, _setItems] = useState(initialItems);
9+
10+ const append = useCallback(
11+ item => _setItems(items => [...items, itemBuilder(item)]),
12+ [_setItems, itemBuilder]
13+ );
14+
15+ const removeAt = useCallback(
16+ i =>
17+ _setItems(items => {
18+ const newItems = [...items.slice(0, i), ...items.slice(i + 1)];
19+ return newItems;
20+ }),
21+ [_setItems]
22+ );
23+
24+ const updateAt = useCallback(
25+ (i, update = {}) =>
26+ _setItems(items => [
27+ ...items.slice(0, i),
28+ {
29+ ...items[i],
30+ ...update,
31+ },
32+ ...items.slice(i + 1),
33+ ]),
34+ [_setItems]
35+ );
36+
37+ const clear = useCallback(() => _setItems([]), [_setItems]);
38+
39+ return [
40+ items,
41+ {
42+ append,
43+ removeAt,
44+ updateAt,
45+ clear,
46+ },
47+ ];
48+}
src/lib/useInterval.jsnew file+22
@@ -0,0 +1,22 @@
1+import { useEffect, useRef } from 'react';
2+
3+// via: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
4+export default function useInterval(callback, delay) {
5+ const savedCallback = useRef();
6+
7+ // Remember the latest callback.
8+ useEffect(() => {
9+ savedCallback.current = callback;
10+ }, [callback]);
11+
12+ // Set up the interval.
13+ useEffect(() => {
14+ function tick() {
15+ savedCallback.current();
16+ }
17+ if (delay !== null) {
18+ let id = setInterval(tick, delay);
19+ return () => clearInterval(id);
20+ }
21+ }, [delay]);
22+}
src/lib/useIsEclipticOwner.jsnew file+20
@@ -0,0 +1,20 @@
1+import { useWallet } from 'store/wallet';
2+import { usePointCache } from 'store/pointCache';
3+
4+import { eqAddr } from 'lib/wallet';
5+
6+export default function useIsEclipticOwner() {
7+ const { wallet } = useWallet();
8+ const { eclipticOwner } = usePointCache();
9+
10+ return wallet.matchWith({
11+ Nothing: () => false,
12+ Just: p => {
13+ if (!eclipticOwner) {
14+ return false;
15+ }
16+
17+ return eqAddr(p.value.address, eclipticOwner);
18+ },
19+ });
20+}
src/lib/useMailer.jsnew file+57
@@ -0,0 +1,57 @@
1+import { useCallback } from 'react';
2+import Maybe from 'folktale/maybe';
3+
4+import { hasReceived, sendMail } from './inviteMail';
5+import useSetState from './useSetState';
6+
7+const STUB_MAILER = process.env.REACT_APP_STUB_MAILER === 'true';
8+
9+function useHasReceivedCache() {
10+ const [cache, addToCache] = useSetState();
11+
12+ const getHasRecieved = useCallback(
13+ email => cache[email] || Maybe.Nothing(), //
14+ [cache]
15+ );
16+
17+ const syncHasReceivedForEmail = useCallback(
18+ async email => {
19+ if (Maybe.Just.hasInstance(getHasRecieved(email))) {
20+ // never update the cache after we know about it
21+ return;
22+ }
23+
24+ if (STUB_MAILER) {
25+ // always allow sending emails when stubbing
26+ return addToCache({ [email]: Maybe.Just(false) });
27+ }
28+
29+ const _hasReceived = await hasReceived(email);
30+ addToCache({ [email]: Maybe.Just(_hasReceived) });
31+ },
32+ [getHasRecieved, addToCache]
33+ );
34+
35+ return { getHasRecieved, syncHasReceivedForEmail };
36+}
37+
38+export default function useMailer(emails) {
39+ const hasReceivedCache = useHasReceivedCache(emails);
40+
41+ // prefix to avoid clobbering sendMail import
42+ // also throws if return value is false
43+ const _sendMail = useCallback(async (email, ticket, rawTx) => {
44+ if (STUB_MAILER) {
45+ console.log(`${email} - ${ticket}`);
46+ return;
47+ }
48+
49+ const mailSuccess = await sendMail(email, ticket, rawTx);
50+
51+ if (!mailSuccess) {
52+ throw new Error(`Internal mailing error when mailing ${email}`);
53+ }
54+ }, []);
55+
56+ return { ...hasReceivedCache, sendMail: _sendMail };
57+}
src/lib/usePreviousValue.jsnew file+16
@@ -0,0 +1,16 @@
1+import { useRef } from 'react';
2+
3+export default function usePreviousValue(value) {
4+ const prev = useRef(null);
5+ const curr = useRef(null);
6+
7+ if (prev.current !== curr.current) {
8+ prev.current = curr.current;
9+ }
10+
11+ if (curr.current !== value) {
12+ curr.current = value;
13+ }
14+
15+ return prev.current;
16+}
src/lib/useRenderCount.jsnew file+7
@@ -0,0 +1,7 @@
1+import { useRef } from 'react';
2+
3+export default function useRenderCount(debubgLabel = 'Component') {
4+ const count = useRef(0);
5+ count.current++;
6+ console.log(`${debubgLabel} — ${count.current}`);
7+}
src/lib/useSetState.jsnew file+16
@@ -0,0 +1,16 @@
1+import { useCallback, useState } from 'react';
2+
3+export default function useSetState(initialState = {}) {
4+ const [state, _setState] = useState(initialState);
5+ const setState = useCallback(
6+ items => _setState(state => ({ ...state, ...items })),
7+ [_setState]
8+ );
9+
10+ const reset = useCallback(() => _setState(initialState), [
11+ _setState,
12+ initialState,
13+ ]);
14+
15+ return [state, setState, reset];
16+}
src/lib/useSyncPoint.jsdeleted−12
@@ -1,12 +0,0 @@
1−import { useEffect } from 'react';
2−
3−import { usePointCache } from 'store/pointCache';
4−
5−// sync a specified point to the cache
6−export default function useSyncPoint(point) {
7− const { fetchPoint } = usePointCache();
8−
9− useEffect(() => {
10− fetchPoint(point);
11− }, [fetchPoint, point]);
12−}
src/lib/useSyncPoints.jsnew file+39
@@ -0,0 +1,39 @@
1+import { useEffect } from 'react';
2+import { isEqual } from 'lodash';
3+
4+import { usePointCache } from 'store/pointCache';
5+import usePreviousValue from 'lib/usePreviousValue';
6+
7+// sync a known (i.e. mine) point to the cache (i.e display values)
8+export function useSyncKnownPoints(points = []) {
9+ // TBD: right now we don't need anything more than the point itself
10+ // but soon we'll need to know information around "key type"
11+}
12+
13+// sync a foreign (not the user's) point to the cache
14+export function useSyncForeignPoints(points = []) {
15+ const { syncForeignPoint } = usePointCache();
16+ const prevPoints = usePreviousValue(points);
17+
18+ useEffect(() => {
19+ if (!isEqual(prevPoints, points)) {
20+ // if the actual set of points changes, sync the whole array
21+ // TODO: we should get clever about caching
22+ points.forEach(point => syncForeignPoint(point));
23+ }
24+ }, [syncForeignPoint, prevPoints, points]);
25+}
26+
27+// sync an owned point to the cache
28+export function useSyncOwnedPoints(points = []) {
29+ const { syncOwnedPoint } = usePointCache();
30+ const prevPoints = usePreviousValue(points);
31+
32+ useEffect(() => {
33+ if (!isEqual(prevPoints, points)) {
34+ // if the actual set of points changes, sync the whole array
35+ // TODO: we should get clever about caching
36+ points.forEach(point => syncOwnedPoint(point));
37+ }
38+ }, [syncOwnedPoint, prevPoints, points]);
39+}
src/lib/useWalletType.jsnew file+11
@@ -0,0 +1,11 @@
1+import { useEffect } from 'react';
2+
3+import { useWallet } from 'store/wallet';
4+
5+export default function useWalletType(walletType) {
6+ const { setWalletType } = useWallet();
7+
8+ useEffect(() => {
9+ setWalletType(walletType);
10+ }, [setWalletType, walletType]);
11+}
src/lib/validators.js+36−13
@@ -3,6 +3,10 @@ import * as ob from 'urbit-ob';
3
3
4import { isValidAddress } from './wallet';
4import { isValidAddress } from './wallet';
5
5
6+// via: https://emailregex.com/
7+const emailRegExp = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
8+
9+// is this 64 hex chars?
6const hexRegExp = /[0-9A-Fa-f]{64}/g;
10const hexRegExp = /[0-9A-Fa-f]{64}/g;
7
11
8// Wraps single validation functions in a controlled and predictable way.
12// Wraps single validation functions in a controlled and predictable way.
@@ -18,7 +22,7 @@ export const simpleValidatorWrapper = config => {
18 // Run the validator and return the result.
22 // Run the validator and return the result.
19 return config.validator(config.prevMessage.data)
23 return config.validator(config.prevMessage.data)
20 ? newMessage(config.prevMessage.data, true, null)
24 ? newMessage(config.prevMessage.data, true, null)
21− : newMessage(config.prevMessage.data, false, config.errorMessage);
25+ : newMessage(config.prevMessage.data, false, config.error);
22};
26};
23
27
24// Validation message
28// Validation message
@@ -45,15 +49,21 @@ export const validateMnemonic = m =>
45 simpleValidatorWrapper({
49 simpleValidatorWrapper({
46 prevMessage: m,
50 prevMessage: m,
47 validator: d => bip39.validateMnemonic(d),
51 validator: d => bip39.validateMnemonic(d),
48− errorMessage: 'This is not a valid mnemonic.',
52+ error: 'This is not a valid mnemonic.',
49 });
53 });
50
54
51// Checks an empty field
55// Checks an empty field
52export const validateNotEmpty = m =>
56export const validateNotEmpty = m =>
53 simpleValidatorWrapper({
57 simpleValidatorWrapper({
54 prevMessage: m,
58 prevMessage: m,
55− validator: d => d.length > 1,
59+ validator: d => {
60+ try {
61+ return d.length > 1;
62+ } catch {
63+ return false;
64+ }
65+ },
56− errorMessage: 'This field is required.',
66+ error: 'This field is required.',
57 });
67 });
58
68
59// Checks if a patp is a valid galaxy
69// Checks if a patp is a valid galaxy
@@ -69,7 +79,7 @@ export const validateGalaxy = m =>
69 return false;
79 return false;
70 }
80 }
71 },
81 },
72− errorMessage: 'This is not a valid galaxy',
82+ error: 'This is not a valid galaxy',
73 });
83 });
74
84
75export const validatePoint = m =>
85export const validatePoint = m =>
@@ -82,7 +92,7 @@ export const validatePoint = m =>
82 return false;
92 return false;
83 }
93 }
84 },
94 },
85− errorMessage: 'This is not a valid point',
95+ error: 'This is not a valid point',
86 });
96 });
87
97
88export const validateTicket = m =>
98export const validateTicket = m =>
@@ -95,7 +105,7 @@ export const validateTicket = m =>
95 return false;
105 return false;
96 }
106 }
97 },
107 },
98− errorMessage: 'This is not a valid ticket',
108+ error: 'This is not a valid ticket',
99 });
109 });
100
110
101export const validateShard = m =>
111export const validateShard = m =>
@@ -108,28 +118,34 @@ export const validateShard = m =>
108 return false;
118 return false;
109 }
119 }
110 },
120 },
111− errorMessage: 'This is not a valid shard',
121+ error: 'This is not a valid shard',
112 });
122 });
113
123
114export const validateLength = (m, l) =>
124export const validateLength = (m, l) =>
115 simpleValidatorWrapper({
125 simpleValidatorWrapper({
116 prevMessage: m,
126 prevMessage: m,
117− validator: d => d.length === l,
127+ validator: d => {
128+ try {
129+ return d.length === l;
130+ } catch {
131+ return false;
132+ }
133+ },
118− errorMessage: 'This is of an invalid length',
134+ error: 'This is of an invalid length',
119 });
135 });
120
136
121export const validateNetworkKey = m =>
137export const validateNetworkKey = m =>
122 simpleValidatorWrapper({
138 simpleValidatorWrapper({
123 prevMessage: m,
139 prevMessage: m,
124 validator: d => hexRegExp.test(d),
140 validator: d => hexRegExp.test(d),
125− errorMessage: 'This is not a valid network key',
141+ error: 'This is not a valid network key',
126 });
142 });
127
143
128export const validateNetworkSeed = m =>
144export const validateNetworkSeed = m =>
129 simpleValidatorWrapper({
145 simpleValidatorWrapper({
130 prevMessage: m,
146 prevMessage: m,
131 validator: d => hexRegExp.test(d),
147 validator: d => hexRegExp.test(d),
132− errorMessage: 'This is not a valid network seed',
148+ error: 'This is not a valid network seed',
133 });
149 });
134
150
135// Checks if a string is a valid ethereum address
151// Checks if a string is a valid ethereum address
@@ -137,5 +153,12 @@ export const validateEthereumAddress = m =>
137 simpleValidatorWrapper({
153 simpleValidatorWrapper({
138 prevMessage: m,
154 prevMessage: m,
139 validator: d => isValidAddress(d),
155 validator: d => isValidAddress(d),
140− errorMessage: 'This is not a valid Ethereum address',
156+ error: 'This is not a valid Ethereum address',
157+ });
158+
159+export const validateEmail = m =>
160+ simpleValidatorWrapper({
161+ prevMessage: m,
162+ validator: d => emailRegExp.test(d),
163+ error: 'This is not a valid email address',
141 });
164 });
src/_walletgen/lib/lib.js → src/lib/walletgen.jsrenamed+10−5
@@ -2,6 +2,7 @@ import * as ob from 'urbit-ob';
2import * as kg from 'urbit-key-generation/dist';
2import * as kg from 'urbit-key-generation/dist';
3import * as more from 'more-entropy';
3import * as more from 'more-entropy';
4import { chunk, flatMap, zipWith } from 'lodash';
4import { chunk, flatMap, zipWith } from 'lodash';
5+
5import {
6import {
6 MIN_STAR,
7 MIN_STAR,
7 MIN_PLANET,
8 MIN_PLANET,
@@ -14,7 +15,7 @@ import {
14const SEED_LENGTH_BYTES = SEED_ENTROPY_BITS / 8;
15const SEED_LENGTH_BYTES = SEED_ENTROPY_BITS / 8;
15
16
16// returns a promise for a ticket string
17// returns a promise for a ticket string
17−const makeTicket = point => {
18+export const makeTicket = point => {
18 const bits =
19 const bits =
19 point < MIN_STAR
20 point < MIN_STAR
20 ? GALAXY_ENTROPY_BITS
21 ? GALAXY_ENTROPY_BITS
@@ -43,7 +44,7 @@ const makeTicket = point => {
43};
44};
44
45
45// return a wallet object
46// return a wallet object
46−const generateWallet = async (point, ticket, boot) => {
47+export const generateWallet = async (point, ticket, boot) => {
47 const config = {
48 const config = {
48 ticket: ticket,
49 ticket: ticket,
49 seedSize: SEED_LENGTH_BYTES,
50 seedSize: SEED_LENGTH_BYTES,
@@ -62,8 +63,12 @@ const generateWallet = async (point, ticket, boot) => {
62 return wallet;
63 return wallet;
63};
64};
64
65
65−// returns a promise
66−const generateOwnershipWallet = (ship, ticket) =>
66+export const generateOwnershipWallet = (ship, ticket) =>
67 kg.generateOwnershipWallet({ ship, ticket });
67 kg.generateOwnershipWallet({ ship, ticket });
68
68
69+export const generateTemporaryTicketAndWallet = async point => {
70+ const ticket = await makeTicket(point);
69−export { makeTicket, generateWallet, generateOwnershipWallet };
71+ const owner = await generateOwnershipWallet(0, ticket);
72+
73+ return { ticket, owner };
74+};
src/store/lib/useBirthdaysStore.jsnew file+54
@@ -0,0 +1,54 @@
1+import { useCallback, useState } from 'react';
2+import Maybe from 'folktale/maybe';
3+import * as azimuth from 'azimuth-js';
4+
5+import { useNetwork } from '../network';
6+
7+export default function useBirthdayStore() {
8+ const { contracts, web3 } = useNetwork();
9+ const [birthdayCache, _setBirthdayCache] = useState({});
10+
11+ const addToBirthdayCache = useCallback(
12+ entry =>
13+ _setBirthdayCache(cache => ({
14+ ...cache,
15+ ...entry,
16+ })),
17+ [_setBirthdayCache]
18+ );
19+
20+ const getBirthday = useCallback(
21+ point => birthdayCache[point] || Maybe.Nothing(),
22+ [birthdayCache]
23+ );
24+
25+ const syncBirthday = useCallback(
26+ async point => {
27+ const _contracts = contracts.getOrElse(null);
28+ const _web3 = web3.getOrElse(null);
29+ if (!_contracts || !web3) {
30+ return;
31+ }
32+
33+ // birthday will not change after being set, so bail if we already know
34+ if (Maybe.Just.hasInstance(getBirthday(point))) {
35+ return;
36+ }
37+
38+ const birthBlock = await azimuth.azimuth.getActivationBlock(
39+ _contracts,
40+ point
41+ );
42+
43+ if (birthBlock > 0) {
44+ const block = await _web3.eth.getBlock(birthBlock);
45+ addToBirthdayCache({
46+ [point]: Maybe.Just(new Date(block.timestamp * 1000)),
47+ });
48+ }
49+ },
50+ [contracts, web3, addToBirthdayCache, getBirthday]
51+ );
52+
53+ return { getBirthday, syncBirthday };
54+}
src/store/lib/useControlledPointsStore.jsnew file+64
@@ -0,0 +1,64 @@
1+import { useState, useCallback } from 'react';
2+import Maybe from 'folktale/maybe';
3+import * as azimuth from 'azimuth-js';
4+
5+import { useNetwork } from '../network';
6+import { useWallet } from 'store/wallet';
7+
8+const kEmptyControlledPoints = {
9+ ownedPoints: Maybe.Nothing(),
10+ incomingPoints: Maybe.Nothing(),
11+ managingPoints: Maybe.Nothing(),
12+ votingPoints: Maybe.Nothing(),
13+ spawningPoints: Maybe.Nothing(),
14+};
15+
16+export default function useControlledPointsStore() {
17+ const { contracts } = useNetwork();
18+ const { wallet } = useWallet();
19+ const [controlledPointsCache, _setControlledPointsCache] = useState(
20+ kEmptyControlledPoints
21+ );
22+
23+ const getControlledPoints = useCallback(
24+ () => controlledPointsCache || kEmptyControlledPoints,
25+ [controlledPointsCache]
26+ );
27+
28+ const syncControlledPoints = useCallback(async () => {
29+ const _contracts = contracts.getOrElse(null);
30+ const _wallet = wallet.getOrElse(null);
31+ if (!_contracts || !_wallet) {
32+ return;
33+ }
34+
35+ const address = _wallet.address;
36+
37+ const [
38+ ownedPoints,
39+ incomingPoints,
40+ managingPoints,
41+ votingPoints,
42+ spawningPoints,
43+ ] = await Promise.all([
44+ azimuth.azimuth.getOwnedPoints(_contracts, address),
45+ azimuth.azimuth.getTransferringFor(_contracts, address),
46+ azimuth.azimuth.getManagerFor(_contracts, address),
47+ azimuth.azimuth.getVotingFor(_contracts, address),
48+ azimuth.azimuth.getSpawningFor(_contracts, address),
49+ ]);
50+
51+ _setControlledPointsCache({
52+ ownedPoints: Maybe.Just(ownedPoints),
53+ incomingPoints: Maybe.Just(incomingPoints),
54+ managingPoints: Maybe.Just(managingPoints),
55+ votingPoints: Maybe.Just(votingPoints),
56+ spawningPoints: Maybe.Just(spawningPoints),
57+ });
58+ }, [contracts, wallet, _setControlledPointsCache]);
59+
60+ return {
61+ getControlledPoints,
62+ syncControlledPoints,
63+ };
64+}
src/store/lib/useDetailsStore.jsnew file+51
@@ -0,0 +1,51 @@
1+import { useState, useCallback } from 'react';
2+import Maybe from 'folktale/maybe';
3+import * as azimuth from 'azimuth-js';
4+
5+import { useNetwork } from '../network';
6+
7+export default function useDetailsStore() {
8+ const { contracts } = useNetwork();
9+ const [detailsCache, _setDetailsCache] = useState({});
10+
11+ const addToDetails = useCallback(
12+ entry =>
13+ _setDetailsCache(cache => ({
14+ ...cache,
15+ ...entry,
16+ })),
17+ [_setDetailsCache]
18+ );
19+
20+ // TODO: refactor detailsCache access to use accessor like bithday
21+ // Maybe<{}>
22+ const getDetails = useCallback(
23+ point =>
24+ point in detailsCache ? Maybe.Just(detailsCache[point]) : Maybe.Nothing(),
25+ [detailsCache]
26+ );
27+
28+ const syncDetails = useCallback(
29+ async point => {
30+ const _contracts = contracts.getOrElse(null);
31+ if (!_contracts) {
32+ return;
33+ }
34+
35+ // fetch point details
36+ const details = await azimuth.azimuth.getPoint(_contracts, point);
37+ addToDetails({
38+ [point]: details,
39+ });
40+ },
41+ [contracts, addToDetails]
42+ );
43+
44+ return {
45+ // TODO: refactor accessors to use getDetails instead of
46+ // touching the cache directly
47+ pointCache: detailsCache,
48+ getDetails,
49+ syncDetails,
50+ };
51+}
src/store/lib/useEclipticOwnerStore.jsnew file+26
@@ -0,0 +1,26 @@
1+import { useCallback, useState } from 'react';
2+import * as azimuth from 'azimuth-js';
3+
4+import { useNetwork } from 'store/network';
5+
6+export default function useEclipticOwnerStore() {
7+ const { contracts } = useNetwork();
8+ const [eclipticOwner, _setEclipticOwner] = useState(null);
9+
10+ const syncEclipticOwner = useCallback(async () => {
11+ // owner is generally immutable
12+ if (eclipticOwner) {
13+ return;
14+ }
15+
16+ const _contracts = contracts.getOrElse(null);
17+ if (!_contracts) {
18+ return;
19+ }
20+
21+ const owner = await azimuth.ecliptic.owner(_contracts);
22+ _setEclipticOwner(owner);
23+ }, [contracts, eclipticOwner]);
24+
25+ return { eclipticOwner, syncEclipticOwner };
26+}
src/store/lib/useInvitesStore.jsnew file+65
@@ -0,0 +1,65 @@
1+import { useCallback, useState } from 'react';
2+import Maybe from 'folktale/maybe';
3+import * as azimuth from 'azimuth-js';
4+
5+import { useNetwork } from '../network';
6+
7+// the default value of a point's invites
8+const kEmptyInvites = {
9+ availableInvites: Maybe.Nothing(),
10+ sentInvites: Maybe.Nothing(),
11+ acceptedInvites: Maybe.Nothing(),
12+};
13+
14+export default function useInvitesStore() {
15+ const { contracts } = useNetwork();
16+ const [invitesCache, _setInvitesCache] = useState({});
17+
18+ const addToInvitesCache = useCallback(
19+ entry =>
20+ _setInvitesCache(cache => ({
21+ ...cache,
22+ ...entry,
23+ })),
24+ [_setInvitesCache]
25+ );
26+
27+ const getInvites = useCallback(
28+ point => invitesCache[point] || kEmptyInvites,
29+ [invitesCache]
30+ );
31+
32+ const syncInvites = useCallback(
33+ async point => {
34+ const _contracts = contracts.getOrElse(null);
35+ if (!_contracts) {
36+ return;
37+ }
38+
39+ const [availableInvites, invitedPoints] = await Promise.all([
40+ azimuth.delegatedSending.getTotalUsableInvites(_contracts, point),
41+ azimuth.delegatedSending.getInvited(_contracts, point),
42+ ]);
43+
44+ const activity = await Promise.all(
45+ invitedPoints.map(invitedPoint =>
46+ azimuth.azimuth.isActive(_contracts, invitedPoint)
47+ )
48+ );
49+
50+ const sentInvites = invitedPoints.length;
51+ const acceptedInvites = activity.filter(i => i.active).length;
52+
53+ addToInvitesCache({
54+ [point]: {
55+ availableInvites: Maybe.Just(availableInvites),
56+ sentInvites: Maybe.Just(sentInvites),
57+ acceptedInvites: Maybe.Just(acceptedInvites),
58+ },
59+ });
60+ },
61+ [contracts, addToInvitesCache]
62+ );
63+
64+ return { getInvites, syncInvites };
65+}
src/store/lib/usePointStore.jsnew file+41
@@ -0,0 +1,41 @@
1+import { useCallback } from 'react';
2+
3+import useDetailsStore from './useDetailsStore';
4+import useBirthdaysStore from './useBirthdaysStore';
5+import useInvitesStore from './useInvitesStore';
6+import useControlledPointsStore from './useControlledPointsStore';
7+
8+export default function usePointStore() {
9+ const { syncDetails, ...details } = useDetailsStore();
10+ const { syncBirthday, ...birthdays } = useBirthdaysStore();
11+ const { syncInvites, ...invites } = useInvitesStore();
12+ const {
13+ syncControlledPoints,
14+ ...controlledPoints
15+ } = useControlledPointsStore();
16+
17+ // sync all of the on-chain info required to display a foreign point
18+ const syncForeignPoint = useCallback(async point => Promise.all([]), []);
19+
20+ // sync all of the on-chain info required for a point that the user owns
21+ const syncOwnedPoint = useCallback(
22+ async point =>
23+ Promise.all([
24+ syncDetails(point),
25+ syncBirthday(point),
26+ syncInvites(point),
27+ ]),
28+ [syncDetails, syncBirthday, syncInvites]
29+ );
30+
31+ return {
32+ ...details,
33+ ...birthdays,
34+ ...invites,
35+ ...controlledPoints,
36+ syncInvites,
37+ syncControlledPoints,
38+ syncOwnedPoint,
39+ syncForeignPoint,
40+ };
41+}
src/store/pointCache.js+5−111
@@ -1,120 +1,14 @@
1−import React, {
1+import React, { createContext, forwardRef, useContext } from 'react';
2− createContext,
3− forwardRef,
4− useCallback,
5− useContext,
6− useState,
7−} from 'react';
8−import Maybe from 'folktale/maybe';
9−import * as azimuth from 'azimuth-js';
10
2
11−import { useNetwork } from './network';
3+import usePointStore from './lib/usePointStore';
12
4
13export const PointCacheContext = createContext(null);
5export const PointCacheContext = createContext(null);
14
6
15−// the default value of a point's invites
16−const kEmptyInvites = {
17− availableInvites: Maybe.Nothing(),
18− sentInvites: Maybe.Nothing(),
19− acceptedInvites: Maybe.Nothing(),
20−};
21−
22−function _usePointCache() {
23− const { contracts, web3 } = useNetwork();
24− const [pointCache, _setPointCache] = useState({});
25− const [birthdayCache, _setBirthdayCache] = useState({});
26− const [invitesCache, _setInvitesCache] = useState({});
27−
28− const addToPointCache = useCallback(
29− entry => _setPointCache(cache => ({ ...cache, ...entry })),
30− [_setPointCache]
31− );
32−
33− const addToBirthdayCache = useCallback(
34− entry => _setBirthdayCache(cache => ({ ...cache, ...entry })),
35− [_setBirthdayCache]
36− );
37−
38− const addToInvitesCache = useCallback(
39− entry => _setInvitesCache(cache => ({ ...cache, ...entry })),
40− [_setInvitesCache]
41− );
42−
43− // TODO: refactor pointCache access to use accessor like bithday
44− const getBirthday = useCallback(
45− point => birthdayCache[point] || Maybe.Nothing(),
46− [birthdayCache]
47− );
48− const getInvites = useCallback(
49− point => invitesCache[point] || kEmptyInvites,
50− [invitesCache]
51− );
52−
53− const fetchPoint = useCallback(
54− async point => {
55− const _contracts = contracts.getOrElse(null);
56− const _web3 = web3.getOrElse(null);
57− if (!_contracts || !_web3) {
58− return;
59− }
60−
61− // fetch point details
62− const details = await azimuth.azimuth.getPoint(_contracts, point);
63− addToPointCache({ [point]: details });
64−
65− // fetch invites
66− const count = await azimuth.delegatedSending.getTotalUsableInvites(
67− _contracts,
68− point
69− );
70− addToInvitesCache({
71− [point]: {
72− availableInvites: Maybe.Just(count),
73− // TODO: look up sent/accepted on-chain
74− sentInvites: Maybe.Just(6),
75− acceptedInvites: Maybe.Just(5),
76− },
77− });
78−
79− // fetch birthday (if not already known — will not change after being set)
80− if (Maybe.Nothing.hasInstance(getBirthday(point))) {
81− const birthBlock = await azimuth.azimuth.getActivationBlock(
82− _contracts,
83− point
84− );
85−
86− if (birthBlock > 0) {
87− const block = await _web3.eth.getBlock(birthBlock);
88− addToBirthdayCache({
89− [point]: Maybe.Just(new Date(block.timestamp * 1000)),
90− });
91− }
92− }
93− },
94− [
95− contracts,
96− web3,
97− addToPointCache,
98− addToBirthdayCache,
99− getBirthday,
100− addToInvitesCache,
101− ]
102− );
103−
104− return {
105− pointCache,
106− getBirthday,
107− getInvites,
108− addToPointCache,
109− fetchPoint,
110− };
111−}
112−
113export function PointCacheProvider({ children }) {
7export function PointCacheProvider({ children }) {
114− const pointCache = _usePointCache();
8+ const cache = usePointStore();
115
9
116 return (
10 return (
117− <PointCacheContext.Provider value={pointCache}>
11+ <PointCacheContext.Provider value={cache}>
118 {children}
12 {children}
119 </PointCacheContext.Provider>
13 </PointCacheContext.Provider>
120 );
14 );
@@ -129,6 +23,6 @@ export function usePointCache() {
129export const withPointCache = Component =>
23export const withPointCache = Component =>
130 forwardRef((props, ref) => (
24 forwardRef((props, ref) => (
131 <PointCacheContext.Consumer>
25 <PointCacheContext.Consumer>
132− {pointCache => <Component ref={ref} {...pointCache} {...props} />}
26+ {cache => <Component ref={ref} {...cache} {...props} />}
133 </PointCacheContext.Consumer>
27 </PointCacheContext.Consumer>
134 ));
28 ));
src/store/wallet.js+25−3
@@ -13,11 +13,13 @@ import {
13 DEFAULT_HD_PATH,
13 DEFAULT_HD_PATH,
14 addressFromSecp256k1Public,
14 addressFromSecp256k1Public,
15 walletFromMnemonic,
15 walletFromMnemonic,
16−} from '../lib/wallet';
16+} from 'lib/wallet';
17−import { BRIDGE_ERROR } from '../lib/error';
17+import { BRIDGE_ERROR } from 'lib/error';
18
18
19export const WalletContext = createContext(null);
19export const WalletContext = createContext(null);
20
20
21+const kDefaultWalletType = WALLET_TYPES.TICKET;
22+
21// NB (jtobin):
23// NB (jtobin):
22//
24//
23// Note that the 'wallet' prop has type depending on the 'walletType' prop.
25// Note that the 'wallet' prop has type depending on the 'walletType' prop.
@@ -34,7 +36,7 @@ function _useWallet(
34 initialWallet = Maybe.Nothing(),
36 initialWallet = Maybe.Nothing(),
35 initialMnemonic = Maybe.Nothing()
37 initialMnemonic = Maybe.Nothing()
36) {
38) {
37− const [walletType, _setWalletType] = useState(WALLET_TYPES.MNEMONIC);
39+ const [walletType, _setWalletType] = useState(kDefaultWalletType);
38 const [walletHdPath, setWalletHdPath] = useState(DEFAULT_HD_PATH);
40 const [walletHdPath, setWalletHdPath] = useState(DEFAULT_HD_PATH);
39 const [wallet, _setWallet] = useState(initialWallet);
41 const [wallet, _setWallet] = useState(initialWallet);
40 const [urbitWallet, _setUrbitWallet] = useState(Maybe.Nothing());
42 const [urbitWallet, _setUrbitWallet] = useState(Maybe.Nothing());
@@ -93,6 +95,24 @@ function _useWallet(
93 [setWallet]
95 [setWallet]
94 );
96 );
95
97
98+ const resetWallet = useCallback(() => {
99+ _setWalletType(kDefaultWalletType);
100+ setWalletHdPath(DEFAULT_HD_PATH);
101+ _setWallet(Maybe.Nothing());
102+ _setUrbitWallet(Maybe.Nothing());
103+ setAuthMnemonic(Maybe.Nothing());
104+ setNetworkSeed(Maybe.Nothing());
105+ setNetworkRevision(Maybe.Nothing());
106+ }, [
107+ _setWalletType,
108+ setWalletHdPath,
109+ _setWallet,
110+ _setUrbitWallet,
111+ setAuthMnemonic,
112+ setNetworkSeed,
113+ setNetworkRevision,
114+ ]);
115+
96 return {
116 return {
97 //
117 //
98 walletType,
118 walletType,
@@ -113,6 +133,8 @@ function _useWallet(
113 setNetworkSeed,
133 setNetworkSeed,
114 networkRevision,
134 networkRevision,
115 setNetworkRevision,
135 setNetworkRevision,
136+ //
137+ resetWallet,
116 };
138 };
117}
139}
118
140
src/style/indigo-proposals.scss+4
@@ -27,3 +27,7 @@
27.minh-100 {
27.minh-100 {
28 min-height: 100vh;
28 min-height: 100vh;
29}
29}
30+
31+.outline-none {
32+ outline: none;
33+}
src/views/Invite.js+44−41
@@ -4,20 +4,20 @@ import { Grid, H5, Text } from 'indigo-react';
4
4
5import { usePointCursor } from 'store/pointCursor';
5import { usePointCursor } from 'store/pointCursor';
6import { useHistory } from 'store/history';
6import { useHistory } from 'store/history';
7−// import { usePointCache } from 'store/pointCache';
8
7
9import View from 'components/View';
8import View from 'components/View';
10import Crumbs from 'components/Crumbs';
9import Crumbs from 'components/Crumbs';
10+import Highlighted from 'components/Highlighted';
11+import { matchBlinky } from 'components/Blinky';
11
12
12import useInvites from 'lib/useInvites';
13import useInvites from 'lib/useInvites';
13−import useSyncPoint from 'lib/useSyncPoint';
14+import { useSyncOwnedPoints } from 'lib/useSyncPoints';
14import useCurrentPointName from 'lib/useCurrentPointName';
15import useCurrentPointName from 'lib/useCurrentPointName';
15import useRouter from 'lib/useRouter';
16import useRouter from 'lib/useRouter';
16−import loadingCharacter from 'lib/loadingCharacter';
17+import { LocalRouterProvider } from 'lib/LocalRouter';
17
18
18import InviteUrl from './Invite/InviteUrl';
19import InviteUrl from './Invite/InviteUrl';
19import InviteEmail from './Invite/InviteEmail';
20import InviteEmail from './Invite/InviteEmail';
20−import Highlighted from 'components/Highlighted';
21
21
22const kInviteNames = {
22const kInviteNames = {
23 URL: 'URL',
23 URL: 'URL',
@@ -31,7 +31,7 @@ const kInviteViews = {
31
31
32export default function Invite() {
32export default function Invite() {
33 const history = useHistory();
33 const history = useHistory();
34− const { Route, ...routeProps } = useRouter({
34+ const { Route, ...router } = useRouter({
35 names: kInviteNames,
35 names: kInviteNames,
36 views: kInviteViews,
36 views: kInviteViews,
37 initialRoutes: [{ key: kInviteNames.URL }],
37 initialRoutes: [{ key: kInviteNames.URL }],
@@ -40,45 +40,48 @@ export default function Invite() {
40 const point = need.pointCursor(pointCursor);
40 const point = need.pointCursor(pointCursor);
41 const name = useCurrentPointName();
41 const name = useCurrentPointName();
42
42
43− // sync the current cursor
43+ // sync the current point
44− useSyncPoint(point);
44+ useSyncOwnedPoints([point]);
45
45
46− // fetch the invites for the current cursor
46+ // get the invites for the current point
47 const { availableInvites, sentInvites, acceptedInvites } = useInvites(point);
47 const { availableInvites, sentInvites, acceptedInvites } = useInvites(point);
48+
49+ // number or loading character
48− const availableInvitesText = loadingCharacter(availableInvites);
50+ const availableInvitesText = matchBlinky(availableInvites);
49− const sentInvitesText = loadingCharacter(sentInvites);
51+ const sentInvitesText = matchBlinky(sentInvites);
50− const acceptedInvitesText = loadingCharacter(acceptedInvites);
52+ const acceptedInvitesText = matchBlinky(acceptedInvites);
51
53
52 return (
54 return (
53− <View>
55+ <LocalRouterProvider value={router}>
56+ <View>
54− <Grid className="mb3">
57+ <Grid className="mb4">
55− <Grid.Item
58+ <Grid.Item
56− as={Crumbs}
59+ as={Crumbs}
57− routes={[
60+ routes={[
58− {
61+ {
59− text: name,
62+ text: name,
60− action: () => history.pop(),
63+ action: () => history.pop(),
61− },
64+ },
62− {
65+ {
63− text: 'Invite',
66+ text: 'Invite',
64− },
67+ },
65− ]}
68+ ]}
66− full
69+ full
67− />
70+ />
68− <Grid.Item as={H5} className="mv4" full>
71+ <Grid.Item as={H5} className="mv4" full>
69− Invite
72+ Invite
70− </Grid.Item>
73+ </Grid.Item>
71− <Grid.Item as={Text} full>
74+ <Grid.Item as={Text} full>
72− You currently have{' '}
75+ You currently have{' '}
73− <Highlighted>{availableInvitesText} invitations</Highlighted> left.
76+ <Highlighted>{availableInvitesText} invitations</Highlighted> left.
74− <br />
77+ <br />
75− Out of the <Highlighted>{sentInvitesText}</Highlighted> invites you
78+ Out of the <Highlighted>{sentInvitesText}</Highlighted> invites you
76− sent, <Highlighted>{acceptedInvitesText}</Highlighted> have been
79+ sent, <Highlighted>{acceptedInvitesText}</Highlighted> have been
77− accepted.
80+ accepted.
78− </Grid.Item>
81+ </Grid.Item>
79− </Grid>
82+ </Grid>
80− {/* TODO(shrugs): use context isntead of this routeProps nonsense */}
81− <Route {...routeProps} />
83+ <Route />
82− </View>
84+ </View>
85+ </LocalRouterProvider>
83 );
86 );
84}
87}
src/views/Invite/InviteEmail.js+435−439
@@ -1,13 +1,21 @@
1+import React, { useCallback, useEffect, useMemo, useState } from 'react';
2+import cn from 'classnames';
1−import { Just, Nothing } from 'folktale/maybe';
3+import Maybe from 'folktale/maybe';
2−import React from 'react';
3import * as azimuth from 'azimuth-js';
4import * as azimuth from 'azimuth-js';
4−import * as need from 'lib/need';
5+import {
5−
6+ Grid,
6−import { hasReceived, sendMail } from 'lib/inviteMail';
7+ Flex,
7−import { Button, Input, H3, Warning } from 'components/old/Base';
8+ Input,
9+ IconButton,
10+ HelpText,
11+ Text,
12+ ErrorText,
13+ AccessoryIcon,
14+} from 'indigo-react';
15+import { uniq } from 'lodash';
8
16
9−// for wallet generation
17+import * as need from 'lib/need';
10−import * as wg from '_walletgen/lib/lib';
18+import * as wg from 'lib/walletgen';
11
19
12// for transaction generation and signing
20// for transaction generation and signing
13import {
21import {
@@ -19,483 +27,471 @@ import {
19 toWei,
27 toWei,
20} from 'lib/txn';
28} from 'lib/txn';
21import * as tank from 'lib/tank';
29import * as tank from 'lib/tank';
30+import { useLocalRouter } from 'lib/LocalRouter';
31+
32+import MiniBackButton from 'components/MiniBackButton';
33+import { usePointCursor } from 'store/pointCursor';
34+import LoadableButton from 'components/LoadableButton';
35+import useArray from 'lib/useArray';
36+import useForm from 'indigo-react/lib/useForm';
37+import { buildEmailInputConfig } from 'components/Inputs';
38+import { MIN_PLANET } from 'lib/constants';
39+import Highlighted from 'components/Highlighted';
22−import { withNetwork } from 'store/network';
40+import { useNetwork } from 'store/network';
23−import { compose } from 'lib/lib';
41+import { usePointCache } from 'store/pointCache';
24−import { withWallet } from 'store/wallet';
42+import { useWallet } from 'store/wallet';
25−import { withPointCursor } from 'store/pointCursor';
43+import useSetState from 'lib/useSetState';
26−import View from 'components/View';
44+import pluralize from 'lib/pluralize';
45+import useMailer from 'lib/useMailer';
27
46
28const GAS_PRICE_GWEI = 20; // we pay the premium for faster ux
47const GAS_PRICE_GWEI = 20; // we pay the premium for faster ux
29−const GAS_LIMIT = 350000;
48+const GAS_LIMIT = 400000;
30const INVITE_COST = toWei((GAS_PRICE_GWEI * GAS_LIMIT).toString(), 'gwei');
49const INVITE_COST = toWei((GAS_PRICE_GWEI * GAS_LIMIT).toString(), 'gwei');
50+const HAS_RECEIVED_TEXT = 'This email has already received an invite.';
31
51
32const STATUS = {
52const STATUS = {
33− INPUT: 'Generate invites',
53+ INPUT: 'INPUT',
34− GENERATING: 'Generating invites',
54+ GENERATING: 'GENERATING',
35− CAN_SEND: 'Send invites',
55+ CAN_SEND: 'CAN_SEND',
36− FUNDING: 'Funding invites',
56+ FUNDING: 'FUNDING',
37− SENDING: 'Sending invites',
57+ SENDING: 'SENDING',
38− DONE: 'Done',
58+ SUCCESS: 'SUCCESS',
39− FAIL: 'Some failed',
59+ FAILURE: 'FAILURE',
40};
60};
41
61
42−const EMAIL_STATUS = {
62+const buttonText = (status, count) => {
43− BUSY: '⋯',
63+ switch (status) {
44− DONE: '✔',
45− FAIL: '×',
46−};
47−
48−class InviteEmail extends React.Component {
49− constructor(props) {
50− super(props);
51−
52− this.state = {
53− invitesAvailable: Nothing(),
54− invited: Nothing(),
55− targets: [{ email: '', hasReceived: Nothing(), status: Nothing() }],
56− status: STATUS.INPUT,
64+ case STATUS.INPUT:
57− errors: Nothing(),
65+ return `Generate ${pluralize(count, 'Invite')}`;
58− //
66+ case STATUS.GENERATING:
59− fundingMessage: Nothing(),
67+ return `Generating ${pluralize(count, 'Invite')}...`;
60− wipInvites: [],
68+ case STATUS.CAN_SEND:
61− };
69+ return `Send ${pluralize(count, 'Invite')}`;
62−
70+ case STATUS.FUNDING:
63− this.findInvited = this.findInvited.bind(this);
71+ return `Waiting on Funds...`;
64− this.generateInvites = this.generateInvites.bind(this);
72+ case STATUS.SENDING:
65− this.sendInvites = this.sendInvites.bind(this);
73+ return `Sending ${pluralize(count, 'Invite')}...`;
66− this.canSend = this.canSend.bind(this);
74+ case STATUS.FAILURE:
67− this.addError = this.addError.bind(this);
75+ default:
68− this.handleEmailInput = this.handleEmailInput.bind(this);
76+ return 'Error';
69− this.addTarget = this.addTarget.bind(this);
70− this.removeTarget = this.removeTarget.bind(this);
71− this.setEmailStatus = this.setEmailStatus.bind(this);
72− this.askForFunding = this.askForFunding.bind(this);
73− }
74−
75− componentDidMount() {
76− this.point = need.pointCursor(this.props.pointCursor);
77− this.address = need.addressFromWallet(this.props.wallet);
78− this.contracts = need.contracts(this.props.contracts);
79− this.web3 = need.web3(this.props.web3);
80−
81− console.log(azimuth.delegatedSending);
82− azimuth.delegatedSending
83− .getTotalUsableInvites(this.contracts, this.point)
84− .then(count => {
85− this.setState({ invitesAvailable: Just(count) });
86− });
87− this.findInvited();
88− }
89−
90− componentDidUpdate(prevProps) {
91− //
92− }
93−
94− async findInvited() {
95− let invited = await azimuth.delegatedSending.getInvited(
96− this.contracts,
97− this.point
98− );
99− invited = invited.map(async point => {
100− const active = await azimuth.azimuth.isActive(this.contracts, point);
101− const res = { point: Number(point), active: active };
102− return res;
103− });
104− invited = await Promise.all(invited);
105− const total = invited.length;
106− const accepted = invited.filter(i => i.active).length;
107− this.setState({ invited: Just({ total, accepted }) });
108 }
77 }
78+};
109
79
110− async getTemporaryWallet() {
80+// world's simplest uid
111− const ticket = await wg.makeTicket(0x10000); // planet-sized ticket
81+let id = 0;
112− const owner = await wg.generateOwnershipWallet(0, ticket);
82+const buildInputConfig = (extra = {}) =>
113− return { ticket, owner: owner.keys.address };
83+ buildEmailInputConfig({
84+ name: `email-${id++}`,
85+ placeholder: 'Email Address',
86+ ...extra,
87+ });
88+
89+const buildAccessoryFor = (dones, errors) => name => {
90+ if (dones[name]) return <AccessoryIcon.Success />;
91+ if (errors[name]) return <AccessoryIcon.Failure />;
92+ return <AccessoryIcon.Pending />;
114− }
93+};
115
94
116− async generateInvites() {
95+// TODO: test with tank, successful txs
117− this.setState({ status: STATUS.GENERATING });
96+export default function InviteEmail() {
97+ // TODO: resumption after error?
98+ const { pop } = useLocalRouter();
99+ const { contracts, web3, networkType } = useNetwork();
100+ const { wallet, walletType, walletHdPath } = useWallet();
101+ const { syncInvites, getInvites } = usePointCache();
102+ const { pointCursor } = usePointCursor();
103+ const point = need.pointCursor(pointCursor);
104+ const { getHasRecieved, syncHasReceivedForEmail, sendMail } = useMailer();
105+
106+ const { availableInvites } = getInvites(point);
107+ const maxInvitesToSend = availableInvites.matchWith({
108+ Nothing: () => 0,
109+ Just: p => p.value,
110+ });
111+
112+ // manage the array of input configs
113+ const [
114+ inputConfigs,
115+ { append: appendInput, removeAt: removeInputAt },
116+ ] = useArray(
117+ [buildInputConfig({ label: 'Email Address' })],
118+ buildInputConfig
119+ );
120+
121+ // manage per-input state
122+ const [hovered, setHovered] = useSetState();
123+ const [invites, addInvite, clearInvites] = useSetState();
124+ const [receipts, addReceipt, clearReceipts] = useSetState();
125+ const [errors, addError, clearErrors] = useSetState();
126+
127+ // manage general state that affects the whole form
128+ const [status, setStatus] = useState(STATUS.INPUT);
129+ const [needFunds, setNeedFunds] = useState(null);
130+ const [generalError, setGeneralError] = useState(null);
131+
132+ // derive booleans from status
133+ const canInput = status === STATUS.INPUT;
134+ const canSend = status === STATUS.CAN_SEND;
135+ const isGenerating = status === STATUS.GENERATING;
136+ const isSending = status === STATUS.SENDING;
137+ const isFailed = status === STATUS.FAILURE;
138+ const isDone = status === STATUS.SUCCESS;
139+
140+ // add disabled, error info to input configs
141+ const dynamicConfigs = useMemo(
142+ () =>
143+ inputConfigs.map(config => {
144+ config.disabled = !canInput;
145+ const hasReceivedError = getHasRecieved(config.name).matchWith({
146+ Nothing: () => null, // loading
147+ Just: p => p.value && HAS_RECEIVED_TEXT,
148+ });
149+ config.error = hasReceivedError || errors[config.name];
150+ return config;
151+ }),
152+ [inputConfigs, errors, canInput, getHasRecieved]
153+ );
154+
155+ // construct the state of the set of inputs we're rendering below
156+ const { inputs, pass } = useForm(dynamicConfigs);
157+ // did all of the inputs pass inspection (and there are no general errors)?
158+ const allPass = pass && !generalError;
159+ // the form is submittable iff passing and input is allowed
160+ const canGenerate = allPass && canInput;
161+ // additional inputs can be added iff input is allowed and we have enough
162+ // invites to send
163+ const canAddInvite = canInput && inputConfigs.length < maxInvitesToSend;
164+
165+ // progress is [0, .length] of invites or receipts, as we're generating them
166+ const progress = isGenerating
167+ ? Object.keys(invites).length
168+ : isSending
169+ ? Object.keys(receipts).length
170+ : null;
171+ const visualProgress = progress === null ? '-' : `${progress + 1}`;
172+
173+ // build a builder for the accessory to the input, depending on status
174+ const accessoryFor = (() => {
175+ if (isGenerating || canSend) return buildAccessoryFor(invites, errors);
176+ if (isSending) return buildAccessoryFor(receipts, errors);
177+ if (isFailed) return buildAccessoryFor({}, errors);
178+ return () => null;
179+ })();
180+
181+ const generateInvites = useCallback(async () => {
182+ const _contracts = contracts.getOrElse(null);
183+ const _web3 = web3.getOrElse(null);
184+ const _wallet = wallet.getOrElse(null);
185+ if (!_contracts || !_web3 || !_wallet) {
186+ // not using need because we want a custom error
187+ throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
188+ }
118
189
119− const web3 = this.web3;
190+ const nonce = await _web3.eth.getTransactionCount(_wallet.address);
120− const targets = this.state.targets;
191+ const chainId = await _web3.eth.net.getId();
121 const planets = await azimuth.delegatedSending.getPlanetsToSend(
192 const planets = await azimuth.delegatedSending.getPlanetsToSend(
122− this.contracts,
193+ _contracts,
123− this.point,
194+ point,
124− targets.length
195+ inputs.length
125 );
196 );
126
197
127 // account for the race condition where invites got used up while we were
198 // account for the race condition where invites got used up while we were
128 // composing our target list
199 // composing our target list
129− if (planets.length < targets.length) {
200+ if (planets.length < inputs.length) {
201+ // resync invites to the cache, since they're out of date
202+ syncInvites(point);
203+
204+ throw new Error(
130− this.addError('Can currently only send ' + planets.length + ' invites!');
205+ `Can currently only send ${planets.length} invites. ` +
131− return;
206+ `Please remove invites until you are within the limit.`
207+ );
132 }
208 }
133
209
134− const nonce = await web3.eth.getTransactionCount(this.address);
210+ let errorCount = 0;
135− const chainId = await web3.eth.net.getId();
211+ clearInvites();
212+ // NB(shrugs) - must be processed in serial because main thread, etc
213+ for (let i = 0; i < inputs.length; i++) {
214+ const input = inputs[i];
215+ try {
216+ const { data: email, name } = inputs[i];
136
217
137− // create invite wallets and transactions
218+ const { ticket, owner } = await wg.generateTemporaryTicketAndWallet(
138− let invites = []; // { email, ticket, signedTx, rawTx }
219+ MIN_PLANET
139− for (let i = 0; i < targets.length; i++) {
220+ // we're always giving planets, so generate a ticket of the correct size
140− console.log('generating email', i, targets[i].email);
221+ );
141− this.setEmailStatus(i, EMAIL_STATUS.BUSY);
142
222
143− const email = targets[i].email;
223+ const inviteTx = azimuth.delegatedSending.sendPoint(
144− const wallet = await this.getTemporaryWallet();
224+ _contracts,
225+ point,
226+ planets[i],
227+ owner.keys.address
228+ );
229+ const signedTx = await signTransaction({
230+ wallet,
231+ walletType,
232+ walletHdPath,
233+ networkType,
234+ // TODO: ^ make a useTransactionSigner to encapsulate this logic
235+ txn: Maybe.Just(inviteTx),
236+ gasPrice: GAS_PRICE_GWEI.toString(),
237+ gasLimit: GAS_LIMIT.toString(),
238+ nonce: nonce + i,
239+ chainId,
240+ setStx: () => {},
241+ });
242+ const rawTx = hexify(signedTx.serialize());
145
243
146− let inviteTx = azimuth.delegatedSending.sendPoint(
244+ addInvite({ [name]: { email, ticket, signedTx, rawTx } });
147− this.contracts,
245+ } catch (error) {
148− this.point,
246+ console.error(error);
149− planets[i],
247+ errorCount++;
150− wallet.owner
248+ addError({ [input.name]: `Wallet Error: ${input.email}` });
151− );
249+ }
152− const signedTx = await signTransaction({
153− ...this.props,
154− txn: Just(inviteTx),
155− gasPrice: GAS_PRICE_GWEI.toString(),
156− gasLimit: GAS_LIMIT.toString(),
157− nonce: nonce + i,
158− chainId: chainId,
159− setStx: () => {},
160− });
161− const rawTx = hexify(signedTx.serialize());
162−
163− invites.push({ email, ticket: wallet.ticket, signedTx, rawTx });
164− this.setEmailStatus(i, EMAIL_STATUS.DONE);
165 }
250 }
166
251
167− this.setState({ wipInvites: invites, status: STATUS.CAN_SEND });
252+ if (errorCount > 0) {
253+ throw new Error(
254+ `There ${pluralize(errorCount, 'was', 'were')} ${pluralize(
255+ errorCount,
256+ 'error'
257+ )} while generating wallets.`
258+ );
259+ }
168− }
260+ }, [
169−
261+ point,
262+ contracts,
263+ web3,
264+ addInvite,
265+ clearInvites,
266+ syncInvites,
267+ inputs,
268+ networkType,
269+ wallet,
270+ walletType,
271+ walletHdPath,
272+ addError,
273+ ]);
274+
170− async sendInvites() {
275+ const sendInvites = useCallback(async () => {
171− const invites = this.state.wipInvites;
276+ const _web3 = web3.getOrElse(null);
172− const web3 = this.web3;
277+ const _wallet = wallet.getOrElse(null);
173−
278+ if (!_web3 || !_wallet) {
174− //TODO invite status isn't changing/re-rendering... why?
279+ throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
175− this.setState({
176− status: STATUS.FUNDING,
177− invites: invites.map(i => {
178− return { ...i, status: Nothing() };
179− }),
180− });
280+ }
181
281
282+ setStatus(STATUS.FUNDING);
182 const tankWasUsed = await tank.ensureFundsFor(
283 const tankWasUsed = await tank.ensureFundsFor(
183− web3,
284+ _web3,
184− this.point,
285+ point,
185− this.address,
286+ _wallet.address,
186− INVITE_COST * invites.length,
287+ INVITE_COST * inputs.length,
187− invites.map(i => i.rawTx),
288+ inputs.map(input => invites[input.name].rawTx),
188− this.askForFunding,
289+ (address, minBalance, balance) =>
189− () => {
290+ setNeedFunds({ address, minBalance, balance }),
190− this.setState({ fundingMessage: Nothing() });
291+ () => setNeedFunds(false)
191− }
192 );
292 );
193
293
194− this.setState({ status: STATUS.SENDING });
294+ setStatus(STATUS.SENDING);
195−
295+ clearReceipts();
196− //TODO nasty loop, probably move into lib... but so many callbacks!
197− let promises = [];
198− for (let i = 0; i < invites.length; i++) {
199− const invite = invites[i];
200
296
201− // send transaction. if it fails, inform the user and skip to next
297+ let errorCount = 0;
202− let txHash;
298+ const txAndMailings = inputs.map(async input => {
299+ const invite = invites[input.name];
203 try {
300 try {
204− txHash = await sendSignedTransaction(
301+ const txHash = await sendSignedTransaction(
205− web3,
302+ _web3,
206− Just(invite.signedTx),
303+ Maybe.Just(invite.signedTx),
207 tankWasUsed,
304 tankWasUsed,
208 () => {}
305 () => {}
209 );
306 );
307+
308+ // TODO: waitForTransactionConfirm never rejects
309+ const didConfirm = await waitForTransactionConfirm(_web3, txHash);
310+ if (!didConfirm) throw new Error();
210− } catch (err) {
311+ } catch (error) {
211− console.error('invite tx sending failed', err);
312+ console.error(error);
212− this.addError('Invite transaction not sent for ' + invite.email);
313+ errorCount++;
213− this.setEmailStatus(i, EMAIL_STATUS.FAIL);
314+ addError({
214− continue;
315+ [input.name]: `Transaction Failure for ${invite.email}`,
316+ });
317+ return;
215 }
318 }
216
319
217− // ask the email sender to send this. we can do this prior to tx confirm,
218− // because the sender service waits for confirm & success for us.
219− // if this fails, inform the user and skip to next
220 try {
320 try {
221− const mailSuccess = await sendMail(
222− invite.email,
223− invite.ticket,
321+ await sendMail(invite.email, invite.ticket, invite.rawTx);
224− invite.rawTx
225− );
226− if (!mailSuccess) throw new Error();
227− this.setEmailStatus(i, EMAIL_STATUS.DONE);
228− } catch (e) {
322+ } catch (error) {
229− console.log('email send failed');
323+ console.error(error);
230− //NOTE this assumes that the transaction succeeds, but we don't know
324+ errorCount++;
231− // that for a fact yet...
232− this.addError(
325+ addError({
233− 'Invite email failed to send for ' +
326+ [input.name]: `Mailing Failure for ${invite.email}`,
234− invite.email +
235− '. Please give them this ticket: ' +
236− invite.ticket
237− );
327+ });
238− this.setEmailStatus(i, EMAIL_STATUS.FAIL);
239− continue;
240 }
328 }
241
329
242− // update status on transaction confirm, but don't block on it
330+ addReceipt({ [input.name]: true });
243− promises.push(
331+ });
244− waitForTransactionConfirm(web3, txHash).then(success => {
332+
245− if (success) {
333+ await Promise.all(txAndMailings);
246− this.setEmailStatus(i, EMAIL_STATUS.DONE);
334+ // if there are any receipt errors, throw a general error
247− } else {
335+ if (errorCount > 0) {
248− console.log('invite tx rejected');
336+ throw new Error(
249− this.addError('Invite transaction failed for ' + invite.email);
337+ `There ${pluralize(errorCount, 'was', 'were')} ${pluralize(
250− this.setEmailStatus(i, EMAIL_STATUS.FAIL);
338+ errorCount,
251− }
339+ 'error'
252− return success;
340+ )} while sending transactions.`
253− })
254 );
341 );
255 }
342 }
256−
343+ }, [
257− const txStatus = await Promise.all(promises);
344+ web3,
258− if (txStatus.every(e => e)) {
345+ inputs,
259− this.setState({ status: STATUS.DONE });
346+ addReceipt,
347+ clearReceipts,
348+ invites,
349+ point,
350+ wallet,
351+ addError,
352+ sendMail,
353+ ]);
354+
355+ const onClick = useCallback(async () => {
356+ setGeneralError(null);
357+ try {
358+ if (canGenerate) {
359+ setStatus(STATUS.GENERATING);
360+ await generateInvites();
361+ setStatus(STATUS.CAN_SEND);
260− } else {
362+ } else if (canSend) {
261− this.setState({ status: STATUS.FAIL });
363+ setStatus(STATUS.SENDING);
364+ await sendInvites();
365+ setStatus(STATUS.SUCCESS);
366+ }
367+ } catch (error) {
368+ console.error(error);
369+ setGeneralError(error);
370+ setStatus(STATUS.FAILURE);
262 }
371 }
263− }
372+ }, [canGenerate, canSend, generateInvites, sendInvites]);
264−
373+
265− setEmailStatus(i, status) {
374+ // when inputs update, check to see if any of them are duplicates
266− const targets = this.state.targets;
375+ useEffect(() => {
267− targets[i].status = Just(status);
376+ // compute the list of valid emails
268− this.setState({ targets });
377+ const emails = inputs.map(i => i.data).filter(d => !!d);
269− }
378+ if (uniq(emails).length !== emails.length) {
270−
379+ setGeneralError(new Error(`Duplicate email.`));
271− addError(error) {
272− const newError = this.state.errors.matchWith({
273− Nothing: () => [error],
274− Just: errs => {
275− errs.value.push(error);
276− return errs.value;
277− },
278− });
279− this.setState({ errors: Just(newError) });
280− }
281−
282− askForFunding(address, minBalance, balance) {
283− this.setState({
284− fundingMessage: Just(
285− `Please make sure ${address} has at least ${fromWei(minBalance)} ETH,` +
286− `we'll continue once that's true. Current balance: ${fromWei(
287− balance
288− )}.` +
289− `Waiting...`
290− ),
291− });
292− }
293−
294− handleEmailInput(i, email) {
295− let targets = this.state.targets;
296− targets[i].email = email;
297−
298− // if it's a valid address, check if it has received an invite before
299− if (email.match(/.*@.*\...+/)) {
300− hasReceived(email).then(res => {
301− targets[i].hasReceived = Just(res);
302− this.setState({ targets });
303− });
304 } else {
380 } else {
305− targets[i].hasReceived = Nothing();
381+ setGeneralError(null);
306 }
382 }
383+ }, [inputs]);
307
384
308− this.setState({ targets });
385+ // when we transition to done, sync invites because we just sent some
309− }
386+ // and therefore know that state has changed
310−
387+ useEffect(() => {
311− canSend() {
388+ if (isDone) {
312− const targets = this.state.targets;
389+ syncInvites(point);
313−
314− // may not have duplicate targets
315− if (new Set(targets.map(t => t.email)).size !== targets.length)
316− return false;
317−
318− // none may have received invites already
319− for (let i = 0; i < targets.length; i++) {
320− const target = targets[i];
321− const res = target.hasReceived.matchWith({
322− Nothing: () => true,
323− Just: has => has.value,
324− });
325− if (res) return false;
326 }
390 }
327−
391+ }, [isDone, syncInvites, point]);
328− return this.state.invitesAvailable.matchWith({
392+
329− Nothing: () => false,
393+ return (
330− Just: invites => invites.value > 0,
394+ <Grid gap={12}>
331− });
395+ <Grid.Item as={Grid} full>
332− }
396+ <Grid.Item as={Flex} cols={[1, 11]} align="center">
333−
397+ <MiniBackButton onClick={() => pop()} />
334− addTarget() {
398+ </Grid.Item>
335− let targets = this.state.targets;
399+ <Grid.Item cols={[11, 13]} justifySelf="end">
336− targets.push({ email: '', hasReceived: Nothing(), status: Nothing() });
400+ {/* use hidden class instead of removing component from dom */}
337− this.setState({ targets });
401+ {/* in order to avoid janky reflow */}
338− }
402+ <IconButton
339−
403+ onClick={() => appendInput()}
340− removeTarget() {
404+ disabled={!canAddInvite}
341− let targets = this.state.targets;
405+ className={cn({ hidden: isDone })}
342− targets.length = targets.length - 1;
406+ solid>
343− this.setState({ targets });
407+ +
344− }
408+ </IconButton>
345−
409+ </Grid.Item>
346− render() {
410+ </Grid.Item>
347− const dif =
411+
348− this.state.status === STATUS.DONE ? this.state.targets.length : null;
412+ {isDone && (
349−
413+ <>
350− const invitesAvailable = this.state.invitesAvailable.matchWith({
414+ <Grid.Item as={Text} full>
351− Nothing: () => 'Loading...',
415+ <Highlighted>{pluralize(inputs.length, 'invite')}</Highlighted>{' '}
352− Just: invites => {
416+ {pluralize(inputs.length, 'has', 'have')} been successfully sent
353− const dia = dif ? `(-${dif})` : '';
417+ </Grid.Item>
354− return `You currently have ${invites.value}${dia} invitations left.`;
418+ {inputs.map(input => (
355− },
419+ <Grid.Item as={HelpText} key={input.name} full>
356− });
420+ {invites[input.name].email}
357−
421+ </Grid.Item>
358− const invitesSent = this.state.invited.matchWith({
359− Nothing: () => <>{'Loading...'}</>,
360− Just: invited => {
361− const dit = dif ? `(+${dif})` : '';
362− return (
363− <>
364− {'Out of the '}
365− {invited.value.total + dit}
366− {' invites you sent, '}
367− {invited.value.accepted}
368− {' have been accepted.'}
369− </>
370− );
371− },
372− });
373−
374− const error = this.state.errors.matchWith({
375− Nothing: () => null,
376− Just: errors => (
377− <Warning className={'mt-8'}>
378− <H3 style={{ marginTop: 0, paddingTop: 0 }}>
379− {'Something went wrong!'}
380− </H3>
381− {errors.value.map(err => (
382− <p>{err}</p>
383 ))}
422 ))}
384− </Warning>
385− ),
386− });
387−
388− const fundingMessage = this.state.fundingMessage.matchWith({
389− Nothing: () => null,
390− Just: msg => <Warning className={'mt-8'}>{msg.value}</Warning>,
391− });
392−
393− let inputs;
394− if (this.state.status === STATUS.DONE) {
395− //TODO proper clickable router thing
396− inputs = '<- Back to Home';
397− } else {
398− const inputDisabled = this.state.status !== STATUS.INPUT;
399−
400− const lessButton = (
401− <Button
402− prop-size={'s narrow'}
403− className={'mt-8'}
404− disabled={inputDisabled || this.state.targets.length === 1}
405− onClick={this.removeTarget}>
406− {'-'}
407− </Button>
408− );
409−
410− const moreButton = (
411− <Button
412− prop-size={'s narrow'}
413− className={'mt-8'}
414− disabled={
415− inputDisabled ||
416− this.state.invitesAvailable.matchWith({
417− Nothing: () => true,
418− Just: inv => inv.value <= this.state.targets.length,
419− })
420− }
421− onClick={this.addTarget}>
422− {'+'}
423− </Button>
424− );
425−
426− let targetInputs = [];
427− for (let i = 0; i < this.state.targets.length; i++) {
428− const target = this.state.targets[i];
429− let progress = target.status.matchWith({
430− Nothing: () => '',
431− Just: status => status.value,
432− });
433− targetInputs.push(
434− <>
435− <Input
436− value={target.email}
437− onChange={value => {
438− this.handleEmailInput(i, value);
439− }}
440− placeholder={'Email address'}
441− disabled={inputDisabled}
442− />
443− {target.hasReceived.matchWith({
444− Nothing: () => null,
445− Just: has =>
446− has.value ? <span>{'×'}</span> : <span>{'°'}</span>,
447− })}
448− {progress}
449− </>
423+ </>
450− );
451− }
452−
453− const submitButton = (
454− <Button
455− prop-size={'xl wide'}
456− className={'mt-8'}
457− disabled={
458− (inputDisabled && this.state.status !== STATUS.CAN_SEND) ||
459− !this.canSend()
460− }
461− onClick={() => {
462− if (this.state.status === STATUS.INPUT) this.generateInvites();
463− if (this.state.status === STATUS.CAN_SEND) this.sendInvites();
464− }}>
424+ )}
465− {this.state.status}
466− </Button>
467− );
468
425
469− inputs = (
426+ {!isDone && (
470 <>
427 <>
471− {lessButton}
428+ {/* email inputs */}
472− {moreButton}
429+ {inputs.map((input, i) => {
473− {targetInputs}
430+ const isFirst = i === 0;
474− {submitButton}
431+ return (
432+ <Grid.Item
433+ key={input.name}
434+ as={Grid}
435+ gap={12}
436+ onMouseOver={() => setHovered({ [input.name]: true })}
437+ onMouseLeave={() => setHovered({ [input.name]: false })}
438+ full>
439+ <Grid.Item
440+ as={Input}
441+ cols={[1, 11]}
442+ {...input}
443+ onValue={email => syncHasReceivedForEmail(email)}
444+ // NB(shrugs): ^ this feels like a hack?
445+ accessory={accessoryFor(input.name)}
446+ />
447+ {!isFirst &&
448+ (input.focused || hovered[input.name]) &&
449+ canInput && (
450+ <Grid.Item
451+ cols={[11, 13]}
452+ justifySelf="end"
453+ alignSelf="center">
454+ <IconButton
455+ onClick={() => removeInputAt(i)}
456+ solid
457+ secondary>
458+ -
459+ </IconButton>
460+ </Grid.Item>
461+ )}
462+ </Grid.Item>
463+ );
464+ })}
465+
466+ <Grid.Item
467+ full
468+ as={LoadableButton}
469+ disabled={!canGenerate && !canSend}
470+ accessory={`${visualProgress}/${inputs.length}`}
471+ onClick={onClick}
472+ success={canSend}
473+ solid>
474+ {buttonText(status, inputs.length)}
475+ </Grid.Item>
476+
477+ {needFunds && (
478+ <Grid.Item full>
479+ <Highlighted>
480+ Your ownership address {needFunds.address} needs at least{' '}
481+ {fromWei(needFunds.minBalance)} ETH and currently has{' '}
482+ {fromWei(needFunds.balance)} ETH. Waiting until the account has
483+ enough funds.
484+ </Highlighted>
485+ </Grid.Item>
486+ )}
487+
488+ {generalError && (
489+ <Grid.Item full>
490+ <ErrorText>{generalError.message.toString()}</ErrorText>
491+ </Grid.Item>
492+ )}
475 </>
493 </>
476− );
477− }
494+ )}
478−
495+ </Grid>
479− return (
496+ );
480− <View>
481− <p>{'send invites here, for planets'}</p>
482−
483− <p>{invitesAvailable}</p>
484−
485− <ul>{invitesSent}</ul>
486−
487− {error}
488−
489− {fundingMessage}
490−
491− {inputs}
492− </View>
493− );
494− }
495}
497}
496−
497−export default compose(
498− withNetwork,
499− withWallet,
500− withPointCursor
501−)(InviteEmail);
src/views/Invite/InviteUrl.js+21−3
@@ -1,10 +1,25 @@
1import React, { useCallback } from 'react';
1import React, { useCallback } from 'react';
2import { Grid, Button } from 'indigo-react';
2import { Grid, Button } from 'indigo-react';
3
3
4+import { usePointCursor } from 'store/pointCursor';
5+
6+import * as need from 'lib/need';
7+import { useLocalRouter } from 'lib/LocalRouter';
8+import useInvites from 'lib/useInvites';
9+
4import { ForwardButton } from 'components/Buttons';
10import { ForwardButton } from 'components/Buttons';
5import FooterButton from 'components/FooterButton';
11import FooterButton from 'components/FooterButton';
6
12
7−export default function InviteUrl({ names, push }) {
13+export default function InviteUrl() {
14+ const { names, push } = useLocalRouter();
15+ const { pointCursor } = usePointCursor();
16+ const point = need.pointCursor(pointCursor);
17+ const { availableInvites } = useInvites(point);
18+ const canInvite = availableInvites.matchWith({
19+ Nothing: () => false,
20+ Just: p => p.value > 0,
21+ });
22+
8 const pushEmail = useCallback(() => push(names.EMAIL), [push, names]);
23 const pushEmail = useCallback(() => push(names.EMAIL), [push, names]);
9
24
10 return (
25 return (
@@ -15,7 +30,7 @@ export default function InviteUrl({ names, push }) {
15 as={Button}
30 as={Button}
16 detail="Generate a URL to send to one guest"
31 detail="Generate a URL to send to one guest"
17 full
32 full
18− disabled>
33+ disabled={!canInvite || true}>
19 Generate Invite
34 Generate Invite
20 </Grid.Item>
35 </Grid.Item>
21 <Grid.Divider />
36 <Grid.Divider />
@@ -23,11 +38,14 @@ export default function InviteUrl({ names, push }) {
23 as={ForwardButton}
38 as={ForwardButton}
24 onClick={pushEmail}
39 onClick={pushEmail}
25 detail="Send invitation to your guest's email"
40 detail="Send invitation to your guest's email"
41+ disabled={!canInvite}
26 full>
42 full>
27 Send Invite via Email
43 Send Invite via Email
28 </Grid.Item>
44 </Grid.Item>
29 </Grid>
45 </Grid>
46+ <FooterButton
30− <FooterButton detail="i.e. Ethereum Address, Text File" disabled>
47+ detail="i.e. Ethereum Address, Text File"
48+ disabled={!canInvite || true}>
31 Advanced
49 Advanced
32 </FooterButton>
50 </FooterButton>
33 </>
51 </>
src/views/InviteTicket.js+1−2
@@ -68,8 +68,7 @@ class InviteTicket extends React.Component {
68 }
68 }
69
69
70 navigateLogin() {
70 navigateLogin() {
71− this.props.history.pop();
71+ this.props.history.popAndPush(ROUTE_NAMES.LOGIN);
72− this.props.history.push(ROUTE_NAMES.NETWORK);
73 }
72 }
74
73
75 handleInviteTicketInput(inviteTicket) {
74 handleInviteTicketInput(inviteTicket) {
src/views/Keystore.jsdeleted−166
@@ -1,166 +0,0 @@
1−import { Just, Nothing } from 'folktale/maybe';
2−import { Ok, Error } from 'folktale/result';
3−import React from 'react';
4−import * as keythereum from 'keythereum';
5−import { Button, UploadButton } from '../components/old/Base';
6−import { Input, InnerLabel, InputCaption } from '../components/old/Base';
7−import { H1, H3, Warning } from '../components/old/Base';
8−
9−import { BRIDGE_ERROR } from '../lib/error';
10−import { ROUTE_NAMES } from '../lib/routeNames';
11−import { withHistory } from '../store/history';
12−import { EthereumWallet } from '../lib/wallet';
13−import { compose } from '../lib/lib';
14−import { withWallet } from '../store/wallet';
15−import View from 'components/View';
16−
17−class Keystore extends React.Component {
18− constructor(props) {
19− super(props);
20−
21− this.state = {
22− keystore: Nothing(), // Maybe<Result<String, String>>
23− password: '',
24− decryptionProblem: false,
25− };
26−
27− this.handleKeystoreUpload = this.handleKeystoreUpload.bind(this);
28− this.handlePasswordInput = this.handlePasswordInput.bind(this);
29− this.constructWallet = this.constructWallet.bind(this);
30− }
31−
32− handlePasswordInput(password) {
33− this.setState({ password });
34− }
35−
36− constructWallet() {
37− const { setWallet } = this.props;
38− const { keystore, password } = this.state;
39−
40− try {
41− const text = keystore.matchWith({
42− Nothing: _ => {
43− throw BRIDGE_ERROR.MISSING_KEYSTORE;
44− },
45− Just: ks =>
46− ks.value.matchWith({
47− Ok: result => result.value,
48− Error: _ => {
49− throw BRIDGE_ERROR.MISSING_KEYSTORE;
50− },
51− }),
52− });
53−
54− const json = JSON.parse(text);
55− const privateKey = keythereum.recover(password, json);
56−
57− const wallet = new EthereumWallet(privateKey);
58− this.setState({ decryptionProblem: false });
59− setWallet(Just(wallet));
60− } catch (err) {
61− this.setState({ decryptionProblem: true });
62− setWallet(Nothing());
63− }
64− }
65−
66− handleKeystoreUpload = event => {
67− const file = event.files.item(0);
68− const reader = new FileReader();
69−
70− reader.onload = e => {
71− const keystore = e.target.result;
72− this.setState({ keystore: Just(Ok(keystore)) });
73− };
74−
75− const failure = _ => {
76− const message = 'There was a problem uploading your Keystore file';
77− this.setState({ keystore: Just(Error(message)) });
78− };
79−
80− reader.onerror = failure;
81− reader.onabort = failure;
82−
83− reader.readAsText(file);
84− };
85−
86− render() {
87− const { history, wallet } = this.props;
88− const { keystore, password, decryptionProblem } = this.state;
89−
90− const uploadButtonClass = keystore.matchWith({
91− Nothing: _ => 'bg-blue white',
92− Just: ks =>
93− ks.value.matchWith({
94− Ok: _ => 'bg-green white',
95− Error: _ => 'bg-yellow black',
96− }),
97− });
98−
99− const decryptMessage =
100− decryptionProblem === false ? (
101− <div />
102− ) : (
103− <Warning className={'mt-8'}>
104− <H3 style={{ marginTop: 0, paddingTop: 0 }}>
105− {"Couldn't decrypt wallet."}
106− </H3>
107− {'You may have entered an incorrect password.'}
108− </Warning>
109− );
110−
111− return (
112− <View>
113− <H1 className={'mb-4'}>{'Upload Your Keystore File'}</H1>
114− <InputCaption>
115− {`Please upload your Ethereum keystore file. If your keystore
116− file is encrypted with a password, you'll also need to enter
117− that below.`}
118− </InputCaption>
119−
120− <UploadButton
121− className={`btn ${uploadButtonClass} mt-10`}
122− onChange={this.handleKeystoreUpload}>
123− <div className={'flex-center-all fs-4 h-11 pointer'}>
124− {'Upload Keystore file'}
125− </div>
126− </UploadButton>
127−
128− <Input
129− className="pt-8 mt-8"
130− prop-size="md"
131− prop-format="innerLabel"
132− type="password"
133− name="password"
134− onChange={this.handlePasswordInput}
135− value={password}
136− autocomplete="off"
137− autoFocus>
138− <InnerLabel>{'Password'}</InnerLabel>
139− </Input>
140−
141− <Button
142− className={` mt-10`}
143− prop-size={'wide lg'}
144− disabled={Nothing.hasInstance(keystore)}
145− onClick={this.constructWallet}>
146− {'Decrypt'}
147− </Button>
148−
149− {decryptMessage}
150−
151− <Button
152− className={'mt-10'}
153− prop-size={'wide lg'}
154− disabled={Nothing.hasInstance(wallet)}
155− onClick={() => history.popAndPush(ROUTE_NAMES.POINTS)}>
156− {'Continue →'}
157− </Button>
158− </View>
159− );
160− }
161−}
162−
163−export default compose(
164− withHistory,
165− withWallet
166−)(Keystore);
src/views/Landing.js−7
@@ -33,13 +33,6 @@ function Landing(props) {
33 Login
33 Login
34 </ForwardButton>
34 </ForwardButton>
35
35
36− <ForwardButton
37− className="mt3"
38− detail="If you own Azimuth assets and want to manage them in some way."
39− onClick={() => history.push(ROUTE_NAMES.NETWORK)}>
40− Unlock a Wallet
41− </ForwardButton>
42−
43 <ForwardButton
36 <ForwardButton
44 className="mt3"
37 className="mt3"
45 detail="View an Azimuth point without signing into a wallet."
38 detail="View an Azimuth point without signing into a wallet."
src/views/Ledger.jsdeleted−220
@@ -1,220 +0,0 @@
1−import * as bip32 from 'bip32';
2−import React from 'react';
3−import Maybe from 'folktale/maybe';
4−import { Button } from '../components/old/Base';
5−import { H1, P, H2 } from '../components/old/Base';
6−import { Input, InnerLabel, InnerLabelDropdown } from '../components/old/Base';
7−import Transport from '@ledgerhq/hw-transport-u2f';
8−import Eth from '@ledgerhq/hw-app-eth';
9−import * as secp256k1 from 'secp256k1';
10−
11−import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from '../lib/ledger';
12−import { ROUTE_NAMES } from '../lib/routeNames';
13−import { withHistory } from '../store/history';
14−import { compose } from '../lib/lib';
15−import { withWallet } from '../store/wallet';
16−import View from 'components/View';
17−
18−const chopHdPrefix = str => (str.slice(0, 2) === 'm/' ? str.slice(2) : str);
19−
20−const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
21−
22−class Ledger extends React.Component {
23− constructor(props) {
24− super(props);
25−
26− this.state = {
27− basePath: LEDGER_LIVE_PATH,
28− hdpath: LEDGER_LIVE_PATH.replace(/x/g, 0),
29− account: 0,
30− };
31−
32− this.pollDevice = this.pollDevice.bind(this);
33− this.handleHdPathInput = this.handleHdPathInput.bind(this);
34− this.handlePathSelection = this.handlePathSelection.bind(this);
35− this.handleAccountSelection = this.handleAccountSelection.bind(this);
36− this.updateHdPath = this.updateHdPath.bind(this);
37− }
38−
39− handlePathSelection(basePath) {
40− this.updateHdPath(basePath, this.state.account);
41− }
42−
43− handleAccountSelection(account) {
44− this.updateHdPath(this.state.basePath, account);
45− }
46−
47− updateHdPath(basePath, account) {
48− let hdpath = this.state.hdpath;
49− if (basePath !== 'custom') {
50− hdpath = basePath.replace(/x/g, account);
51− }
52− this.setState({ basePath, account, hdpath });
53− }
54−
55− handleHdPathInput(hdpath) {
56− this.setState({ hdpath });
57− }
58−
59− async pollDevice() {
60− const { setWallet, setWalletHdPath } = this.props;
61− const { hdpath } = this.state;
62−
63− const transport = await Transport.create();
64− const eth = new Eth(transport);
65− const path = chopHdPrefix(hdpath);
66−
67− eth.getAddress(path, false, true).then(
68− info => {
69− const publicKey = Buffer.from(info.publicKey, 'hex');
70− const chainCode = Buffer.from(info.chainCode, 'hex');
71− const pub = secp256k1.publicKeyConvert(publicKey, true);
72− const hd = bip32.fromPublicKey(pub, chainCode);
73− setWallet(Maybe.Just(hd));
74− setWalletHdPath(addHdPrefix(hdpath));
75− },
76− _ => {
77− setWallet(Maybe.Nothing());
78− }
79− );
80− }
81−
82− render() {
83− const { history, wallet } = this.props;
84− const { basePath, hdpath, account } = this.state;
85− const { handlePathSelection, handleAccountSelection } = this;
86−
87− const pathOptions = [
88− { title: 'Ledger Live', value: LEDGER_LIVE_PATH },
89− { title: 'Ledger Legacy', value: LEDGER_LEGACY_PATH },
90− {
91− title: 'Custom path',
92− value: 'custom',
93− },
94− ];
95− let basePathTitle = pathOptions.find(o => o.value === basePath).title;
96−
97− let accountOptions = [];
98− for (let i = 0; i < 20; i++) {
99− accountOptions.push({
100− title: 'Account #' + (i + 1),
101− value: i,
102− });
103− }
104− let accountTitle = accountOptions.find(o => o.value === account).title;
105−
106− const basePathSelection = (
107− <InnerLabelDropdown
108− className="mt-8"
109− options={pathOptions}
110− handleUpdate={handlePathSelection}
111− title={'Derivation path'}
112− currentSelectionTitle={basePathTitle}
113− fullWidth={true}
114− />
115− );
116−
117− const truePathSelection =
118− basePath === 'custom' ? (
119− <Input
120− className="pt-8 mt-4 text-mono"
121− prop-size="md"
122− prop-format="innerLabel"
123− name="hdpath"
124− value={addHdPrefix(hdpath)}
125− autocomplete="off"
126− onChange={this.handleHdPathInput}>
127− <InnerLabel>{'HD Path'}</InnerLabel>
128− </Input>
129− ) : (
130− <InnerLabelDropdown
131− className="mt-4"
132− prop-size="md"
133− prop-format="innerLabel"
134− options={accountOptions}
135− handleUpdate={handleAccountSelection}
136− title={'Account'}
137− currentSelectionTitle={accountTitle}
138− fullWidth={true}
139− />
140− );
141−
142− return (
143− <View>
144− <H1>{'Authenticate With Your Ledger'}</H1>
145−
146− <H2>{'Running on HTTPS?'}</H2>
147−
148− <P>
149− {`Connect and authenticate to your Ledger, and then open the
150− "Ethereum" application. If you're running on older firmware,
151− make sure the "browser support" option is turned on. To sign
152− transactions, you'll also need to enable the "contract data"
153− option.`}
154− </P>
155−
156− <P>
157− {`If you'd like to use a custom derivation path, you may enter
158− it below.`}
159− </P>
160−
161− <H2>{'Running on HTTP?'}</H2>
162−
163− <P>{`To authenticate and sign transactions with a Ledger, Bridge
164− must be serving over HTTPS on localhost. You can do this via the
165− following:`}</P>
166−
167− <ol className={'measure-md'}>
168− <li className={'mt-4'}>
169− {'Install'}{' '}
170− <a target={'_blank'} href={'https://github.com/FiloSottile/mkcert'}>
171− {'mkcert'}
172− </a>
173− </li>
174− <li className={'mt-4'}>
175− {'Install a local certificate authority via '}
176− <code>{'mkcert -install'}</code>
177− </li>
178− <li className={'mt-4'}>
179− {'In your '}
180− <code>{'bridge'}</code>
181− {' directory, generate a certificate valid for localhost via '}{' '}
182− <code>{'mkcert localhost'}</code>
183− {'.'}
184− {'This will produce two files: '}
185− <code>{'localhost.pem'}</code> {', the local certificate, and '}
186− <code>{'localhost-key.pem'}</code>
187− {', its corresponding private key.'}
188− </li>
189− <li className={'mt-4'}>
190− {'Run '}
191− <code>{'python bridge-https.py'}</code>
192− </li>
193− </ol>
194−
195− {basePathSelection}
196− {truePathSelection}
197−
198− <Button
199− prop-size={'wide lg'}
200− className={'mt-8'}
201− onClick={this.pollDevice}>
202− {'Authenticate →'}
203− </Button>
204−
205− <Button
206− className={'mt-8'}
207− prop-size={'wide lg'}
208− disabled={Maybe.Nothing.hasInstance(wallet)}
209− onClick={() => history.popAndPush(ROUTE_NAMES.POINTS)}>
210− {'Continue →'}
211− </Button>
212− </View>
213− );
214− }
215−}
216−
217−export default compose(
218− withHistory,
219− withWallet
220−)(Ledger);
src/views/Login.js+6−8
@@ -41,7 +41,7 @@ export default function Login() {
41 // globals
41 // globals
42 const history = useHistory();
42 const history = useHistory();
43 const { contracts } = useNetwork();
43 const { contracts } = useNetwork();
44− const { wallet, setUrbitWallet } = useWallet();
44+ const { wallet, resetWallet } = useWallet();
45 const { pointCursor, setPointCursor } = usePointCursor();
45 const { pointCursor, setPointCursor } = usePointCursor();
46
46
47 // inputs
47 // inputs
@@ -49,8 +49,7 @@ export default function Login() {
49
49
50 // on-mount
50 // on-mount
51 useLifecycle(() => {
51 useLifecycle(() => {
52− setUrbitWallet(Nothing());
52+ resetWallet();
53− setPointCursor(Nothing());
54 });
53 });
55
54
56 const doContinue = async () => {
55 const doContinue = async () => {
@@ -58,8 +57,8 @@ export default function Login() {
58 const _contracts = need.contracts(contracts);
57 const _contracts = need.contracts(contracts);
59
58
60 // if no point cursor set by login logic, try to deduce it
59 // if no point cursor set by login logic, try to deduce it
61− let deduced = Nothing();
60+ let deduced = pointCursor;
62− if (Nothing.hasInstance(pointCursor)) {
61+ if (Nothing.hasInstance(deduced)) {
63 const owned = await azimuth.azimuth.getOwnedPoints(
62 const owned = await azimuth.azimuth.getOwnedPoints(
64 _contracts,
63 _contracts,
65 _wallet.address
64 _wallet.address
@@ -81,9 +80,8 @@ export default function Login() {
81 // navigate to that specific point, otherwise navigate to list of points
80 // navigate to that specific point, otherwise navigate to list of points
82 if (Just.hasInstance(deduced)) {
81 if (Just.hasInstance(deduced)) {
83 setPointCursor(deduced);
82 setPointCursor(deduced);
84− history.popAndPush(ROUTE_NAMES.POINT);
83+ history.popAndPush(ROUTE_NAMES.POINTS);
85− } else if (Just.hasInstance(pointCursor)) {
86− history.popAndPush(ROUTE_NAMES.POINT);
84+ history.push(ROUTE_NAMES.POINT);
87 } else {
85 } else {
88 history.popAndPush(ROUTE_NAMES.POINTS);
86 history.popAndPush(ROUTE_NAMES.POINTS);
89 }
87 }
src/views/Login/Keystore.js+17−12
@@ -2,25 +2,35 @@ import { Just, Nothing } from 'folktale/maybe';
2import { Ok, Error } from 'folktale/result';
2import { Ok, Error } from 'folktale/result';
3import React, { useState } from 'react';
3import React, { useState } from 'react';
4import * as keythereum from 'keythereum';
4import * as keythereum from 'keythereum';
5+import { H3, Input } from 'indigo-react';
5
6
6import View from 'components/View';
7import View from 'components/View';
7−import { PassphraseInput } from 'components/Inputs';
8+import { usePassphraseInput } from 'components/Inputs';
8import { ForwardButton } from 'components/Buttons';
9import { ForwardButton } from 'components/Buttons';
9−import { H3 } from 'indigo-react';
10import { InputCaption, UploadButton, Warning } from 'components/old/Base';
10import { InputCaption, UploadButton, Warning } from 'components/old/Base';
11
11
12−import * as need from 'lib/need';
13−import { EthereumWallet } from 'lib/wallet';
14import { useWallet } from 'store/wallet';
12import { useWallet } from 'store/wallet';
15
13
14+import * as need from 'lib/need';
15+import { EthereumWallet, WALLET_TYPES } from 'lib/wallet';
16+import useWalletType from 'lib/useWalletType';
17+
16export default function Keystore({ loginCompleted }) {
18export default function Keystore({ loginCompleted }) {
19+ useWalletType(WALLET_TYPES.KEYSTORE);
20+
17 // globals
21 // globals
18 const { wallet, setWallet } = useWallet();
22 const { wallet, setWallet } = useWallet();
19
23
20 // inputs
24 // inputs
21 // keystore: Maybe<Result<String, String>>
25 // keystore: Maybe<Result<String, String>>
22 const [keystore, setKeystore] = useState(Nothing());
26 const [keystore, setKeystore] = useState(Nothing());
23− const [password, setPassword] = useState('');
27+ const passphraseInput = usePassphraseInput({
28+ name: 'password',
29+ label: 'Keystore password',
30+ autoFocus: true,
31+ });
32+ const passphrase = passphraseInput.data;
33+
24 const [decryptionProblem, setDecryptionProblem] = useState(false);
34 const [decryptionProblem, setDecryptionProblem] = useState(false);
25
35
26 const constructWallet = () => {
36 const constructWallet = () => {
@@ -28,7 +38,7 @@ export default function Keystore({ loginCompleted }) {
28 const text = need.keystore(keystore);
38 const text = need.keystore(keystore);
29
39
30 const json = JSON.parse(text);
40 const json = JSON.parse(text);
31− const privateKey = keythereum.recover(password, json);
41+ const privateKey = keythereum.recover(passphrase, json);
32
42
33 const newWallet = new EthereumWallet(privateKey);
43 const newWallet = new EthereumWallet(privateKey);
34 setDecryptionProblem(false);
44 setDecryptionProblem(false);
@@ -95,12 +105,7 @@ export default function Keystore({ loginCompleted }) {
95 </div>
105 </div>
96 </UploadButton>
106 </UploadButton>
97
107
98− <PassphraseInput
108+ <Input {...passphraseInput} />
99− name="password"
100− label="Keystore password"
101− initialValue={password}
102− onValue={setPassword}
103− autoFocus></PassphraseInput>
104
109
105 <ForwardButton
110 <ForwardButton
106 className="mt3"
111 className="mt3"
src/views/Login/Ledger.js+12−4
@@ -5,15 +5,18 @@ import Maybe from 'folktale/maybe';
5import Transport from '@ledgerhq/hw-transport-u2f';
5import Transport from '@ledgerhq/hw-transport-u2f';
6import Eth from '@ledgerhq/hw-app-eth';
6import Eth from '@ledgerhq/hw-app-eth';
7import * as secp256k1 from 'secp256k1';
7import * as secp256k1 from 'secp256k1';
8+import { H1, H2, P, Input } from 'indigo-react';
8
9
9import View from 'components/View';
10import View from 'components/View';
10import { ForwardButton } from 'components/Buttons';
11import { ForwardButton } from 'components/Buttons';
11−import { H1, H2, P, Input } from 'indigo-react';
12−import { InnerLabel, InnerLabelDropdown } from 'components/old/Base';
12+import { InnerLabelDropdown } from 'components/old/Base';
13
13
14−import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from 'lib/ledger';
15import { useWallet } from 'store/wallet';
14import { useWallet } from 'store/wallet';
16
15
16+import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from 'lib/ledger';
17+import useWalletType from 'lib/useWalletType';
18+import { WALLET_TYPES } from 'lib/wallet';
19+
17const pathOptions = [
20const pathOptions = [
18 { title: 'Ledger Live', value: LEDGER_LIVE_PATH },
21 { title: 'Ledger Live', value: LEDGER_LIVE_PATH },
19 { title: 'Ledger Legacy', value: LEDGER_LEGACY_PATH },
22 { title: 'Ledger Legacy', value: LEDGER_LEGACY_PATH },
@@ -25,6 +28,8 @@ const chopHdPrefix = str => (str.slice(0, 2) === 'm/' ? str.slice(2) : str);
25const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
28const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
26
29
27export default function Ledger({ loginCompleted }) {
30export default function Ledger({ loginCompleted }) {
31+ useWalletType(WALLET_TYPES.LEDGER);
32+
28 const { wallet, setWallet, setWalletHdPath } = useWallet();
33 const { wallet, setWallet, setWalletHdPath } = useWallet();
29
34
30 const [basePath, setBasePath] = useState(LEDGER_LIVE_PATH);
35 const [basePath, setBasePath] = useState(LEDGER_LIVE_PATH);
@@ -123,7 +128,10 @@ export default function Ledger({ loginCompleted }) {
123 <ol className="measure-md">
128 <ol className="measure-md">
124 <li className="mt-4">
129 <li className="mt-4">
125 Install
130 Install
131+ <a
132+ target="_blank"
126− <a target="_blank" href="https://github.com/FiloSottile/mkcert">
133+ href="https://github.com/FiloSottile/mkcert"
134+ rel="noopener noreferrer">
127 mkcert
135 mkcert
128 </a>
136 </a>
129 </li>
137 </li>
src/views/Login/Mnemonic.js+36−24
@@ -1,14 +1,22 @@
1−import React, { useCallback, useEffect, useState } from 'react';
1+import React, { useCallback, useEffect } from 'react';
2import { Just, Nothing } from 'folktale/maybe';
2import { Just, Nothing } from 'folktale/maybe';
3+import { Input } from 'indigo-react';
3
4
4import View from 'components/View';
5import View from 'components/View';
6+import {
7+ usePassphraseInput,
8+ useMnemonicInput,
9+ useHdPathInput,
5−import { MnemonicInput, PassphraseInput, HdPathInput } from 'components/Inputs';
10+} from 'components/Inputs';
6import { ForwardButton } from 'components/Buttons';
11import { ForwardButton } from 'components/Buttons';
7
12
8−import { walletFromMnemonic } from 'lib/wallet';
9import { useWallet } from 'store/wallet';
13import { useWallet } from 'store/wallet';
10
14
15+import { walletFromMnemonic, WALLET_TYPES } from 'lib/wallet';
16+import useWalletType from 'lib/useWalletType';
17+
11export default function Mnemonic({ advanced, loginCompleted }) {
18export default function Mnemonic({ advanced, loginCompleted }) {
19+ useWalletType(WALLET_TYPES.MNEMONIC);
12 const {
20 const {
13 wallet,
21 wallet,
14 setWallet,
22 setWallet,
@@ -17,9 +25,8 @@ export default function Mnemonic({ advanced, loginCompleted }) {
17 walletHdPath,
25 walletHdPath,
18 setWalletHdPath,
26 setWalletHdPath,
19 } = useWallet();
27 } = useWallet();
20− const [passphrase, setPassphrase] = useState('');
21− const mnemonic = authMnemonic.getOrElse('');
22
28
29+ const mnemonic = authMnemonic.getOrElse('');
23 // TODO: move this into transformers?
30 // TODO: move this into transformers?
24 // transform the result of the mnemonic to Maybe<string>
31 // transform the result of the mnemonic to Maybe<string>
25 const _setAuthMnemonic = useCallback(
32 const _setAuthMnemonic = useCallback(
@@ -27,6 +34,27 @@ export default function Mnemonic({ advanced, loginCompleted }) {
27 [setAuthMnemonic]
34 [setAuthMnemonic]
28 );
35 );
29
36
37+ const mnemonicInput = useMnemonicInput({
38+ name: 'mnemonic',
39+ label: 'BIP39 Mnemonic',
40+ initialValue: mnemonic,
41+ onValue: _setAuthMnemonic,
42+ autoFocus: true,
43+ });
44+
45+ const passphraseInput = usePassphraseInput({
46+ name: 'passphrase',
47+ label: '(Optional) Wallet Passphrase',
48+ });
49+ const passphrase = passphraseInput.data;
50+
51+ const hdPathInput = useHdPathInput({
52+ name: 'hdpath',
53+ label: 'HD Path',
54+ initialValue: walletHdPath,
55+ onValue: setWalletHdPath,
56+ });
57+
30 // when the properties change, re-derive wallet
58 // when the properties change, re-derive wallet
31 useEffect(() => {
59 useEffect(() => {
32 let mounted = true;
60 let mounted = true;
@@ -39,29 +67,13 @@ export default function Mnemonic({ advanced, loginCompleted }) {
39
67
40 return (
68 return (
41 <View>
69 <View>
42− <MnemonicInput
70+ <Input {...mnemonicInput} />
43− name="mnemonic"
44− label="BIP39 Mnemonic"
45− initialValue={mnemonic}
46− onValue={_setAuthMnemonic}
47− autoFocus
48− />
49
71
50 {!advanced ? null : (
72 {!advanced ? null : (
51 <>
73 <>
52− <PassphraseInput
74+ <Input {...passphraseInput} />
53− name="passphrase"
54− label="(Optional) Wallet Passphrase"
55− initialValue={passphrase}
56− onValue={setPassphrase}
57− />
58
75
59− <HdPathInput
76+ <Input {...hdPathInput} />
60− name="hdpath"
61− label="HD Path"
62− initialValue={walletHdPath}
63− onValue={setWalletHdPath}
64− />
65 </>
77 </>
66 )}
78 )}
67
79
src/views/Login/PrivateKey.js+16−20
@@ -1,24 +1,28 @@
1import { Just, Nothing } from 'folktale/maybe';
1import { Just, Nothing } from 'folktale/maybe';
2−import React, { useState } from 'react';
2+import React, { useEffect } from 'react';
3+import { Input } from 'indigo-react';
3
4
4import View from 'components/View';
5import View from 'components/View';
5−import { PassphraseInput } from 'components/Inputs';
6+import { usePassphraseInput } from 'components/Inputs';
6import { ForwardButton } from 'components/Buttons';
7import { ForwardButton } from 'components/Buttons';
7
8
8−import { EthereumWallet } from 'lib/wallet';
9import { useWallet } from 'store/wallet';
9import { useWallet } from 'store/wallet';
10
10
11+import { EthereumWallet, WALLET_TYPES } from 'lib/wallet';
12+import useWalletType from 'lib/useWalletType';
13+
11export default function PrivateKey({ loginCompleted }) {
14export default function PrivateKey({ loginCompleted }) {
15+ useWalletType(WALLET_TYPES.PRIVATE_KEY);
12 const { wallet, setWallet } = useWallet();
16 const { wallet, setWallet } = useWallet();
13
17
14− const [privateKey, setPrivateKey] = useState('');
18+ const privateKeyInput = usePassphraseInput({
15−
16− const handlePrivateKeyInput = privateKey => {
17− setPrivateKey(privateKey);
19+ name: 'privateKey',
18− constructWallet(privateKey);
20+ label: 'Private key',
21+ autoFocus: true,
19− };
22+ });
23+ const privateKey = privateKeyInput.data;
20
24
21− const constructWallet = privateKey => {
25+ useEffect(() => {
22 if (/^[0-9A-Fa-f]{64}$/g.test(privateKey) === true) {
26 if (/^[0-9A-Fa-f]{64}$/g.test(privateKey) === true) {
23 const sec = Buffer.from(privateKey, 'hex');
27 const sec = Buffer.from(privateKey, 'hex');
24 const newWallet = new EthereumWallet(sec);
28 const newWallet = new EthereumWallet(sec);
@@ -26,20 +30,12 @@ export default function PrivateKey({ loginCompleted }) {
26 } else {
30 } else {
27 setWallet(Nothing());
31 setWallet(Nothing());
28 }
32 }
29− };
33+ }, [privateKey, setWallet]);
30
34
31 return (
35 return (
32 <View>
36 <View>
33 Please enter your raw Ethereum private key here.
37 Please enter your raw Ethereum private key here.
34− <PassphraseInput
38+ <Input {...privateKeyInput} />
35− type="password"
36− name="privateKey"
37− label="Private key"
38− initialValue={privateKey}
39− onValue={handlePrivateKeyInput}
40− autocomplete="off"
41− autoFocus
42− />
43 <ForwardButton
39 <ForwardButton
44 className="mt3"
40 className="mt3"
45 disabled={Nothing.hasInstance(wallet)}
41 disabled={Nothing.hasInstance(wallet)}
src/views/Login/Shards.js+44−36
@@ -1,22 +1,53 @@
1import { Just, Nothing } from 'folktale/maybe';
1import { Just, Nothing } from 'folktale/maybe';
2−import React, { useState } from 'react';
2+import React from 'react';
3import * as kg from 'urbit-key-generation/dist/index';
3import * as kg from 'urbit-key-generation/dist/index';
4+import { Input } from 'indigo-react';
4
5
5import View from 'components/View';
6import View from 'components/View';
7+import {
8+ useTicketInput,
9+ usePassphraseInput,
10+ usePointInput,
6−import { PointInput, TicketInput, PassphraseInput } from 'components/Inputs';
11+} from 'components/Inputs';
7import { ForwardButton } from 'components/Buttons';
12import { ForwardButton } from 'components/Buttons';
8
13
9−import { urbitWalletFromTicket } from 'lib/wallet';
10import { useWallet } from 'store/wallet';
14import { useWallet } from 'store/wallet';
11
15
16+import { urbitWalletFromTicket, WALLET_TYPES } from 'lib/wallet';
17+import useWalletType from 'lib/useWalletType';
18+
12export default function Shards({ loginCompleted }) {
19export default function Shards({ loginCompleted }) {
20+ useWalletType(WALLET_TYPES.SHARDS);
21+
13 const { wallet, setUrbitWallet } = useWallet();
22 const { wallet, setUrbitWallet } = useWallet();
14
23
15− const [shard1, setShard1] = useState('');
24+ const pointInput = usePointInput({
16− const [shard2, setShard2] = useState('');
25+ name: 'point',
17− const [shard3, setShard3] = useState('');
26+ label: 'Point',
18− const [passphrase, setPassphrase] = useState('');
27+ autoFocus: true,
28+ });
19− const [pointName, setPointName] = useState('');
29+ const pointName = pointInput.data;
30+ const shard1Input = useTicketInput({
31+ name: 'shard1',
32+ label: 'Shard 1',
33+ });
34+ const shard1 = shard1Input.data;
35+ const shard2Input = useTicketInput({
36+ name: 'shard2',
37+ label: 'Shard 2',
38+ });
39+ const shard2 = shard2Input.data;
40+ const shard3Input = useTicketInput({
41+ name: 'shard3',
42+ label: 'Shard 3',
43+ });
44+ const shard3 = shard3Input.data;
45+
46+ const passphraseInput = usePassphraseInput({
47+ name: 'passphrase',
48+ label: '(Optional) Wallet Passphrase',
49+ });
50+ const passphrase = passphraseInput.data;
20
51
21 const walletFromShards = async () => {
52 const walletFromShards = async () => {
22 const s1 = shard1 === '' ? undefined : shard1;
53 const s1 = shard1 === '' ? undefined : shard1;
@@ -43,35 +74,12 @@ export default function Shards({ loginCompleted }) {
43 <View>
74 <View>
44 Enter your point and at least two of your three Urbit master ticket shards
75 Enter your point and at least two of your three Urbit master ticket shards
45 here. The index of the input field needs to match the index of the shard.
76 here. The index of the input field needs to match the index of the shard.
46− <PointInput
77+ <Input {...pointInput} />
47− name="point"
78+ <Input {...shard1Input} />
48− label="Point"
79+ <Input {...shard2Input} />
49− initialValue={pointName}
80+ <Input {...shard3Input} />
50− onValue={setPointName}
51− autoFocus
52− />
53− <TicketInput
54− name="shard1"
55− label="Shard 1"
56− initialValue={shard1}
57− onValue={setShard1}></TicketInput>
58− <TicketInput
59− name="shard2"
60− label="Shard 2"
61− initialValue={shard2}
62− onValue={setShard2}></TicketInput>
63− <TicketInput
64− name="shard3"
65− label="Shard 3"
66− initialValue={shard3}
67− onValue={setShard3}></TicketInput>
68 If your wallet requires a passphrase, you may enter it below.
81 If your wallet requires a passphrase, you may enter it below.
69− <PassphraseInput
82+ <Input {...passphraseInput} />
70− name="passphrase"
71− label="(Optional) Wallet Passphrase"
72− initialValue={passphrase}
73− onValue={setPassphrase}
74− />
75 <ForwardButton
83 <ForwardButton
76 className="mt3"
84 className="mt3"
77 disabled={!ready}
85 disabled={!ready}
src/views/Login/Ticket.js+73−77
@@ -1,19 +1,25 @@
1import { Just, Nothing } from 'folktale/maybe';
1import { Just, Nothing } from 'folktale/maybe';
2−import React, { useState, useEffect } from 'react';
2+import React, { useCallback, useState, useEffect } from 'react';
3import * as azimuth from 'azimuth-js';
3import * as azimuth from 'azimuth-js';
4import * as ob from 'urbit-ob';
4import * as ob from 'urbit-ob';
5+import { Input, AccessoryIcon } from 'indigo-react';
5
6
6import View from 'components/View';
7import View from 'components/View';
8+import {
9+ usePointInput,
10+ useTicketInput,
11+ usePassphraseInput,
7−import { PointInput, TicketInput, PassphraseInput } from 'components/Inputs';
12+} from 'components/Inputs';
8import { ForwardButton } from 'components/Buttons';
13import { ForwardButton } from 'components/Buttons';
9
14
10−import * as need from 'lib/need';
11−import { WALLET_TYPES, urbitWalletFromTicket } from 'lib/wallet';
12−
13import { useNetwork } from 'store/network';
15import { useNetwork } from 'store/network';
14import { useWallet } from 'store/wallet';
16import { useWallet } from 'store/wallet';
15import { usePointCursor } from 'store/pointCursor';
17import { usePointCursor } from 'store/pointCursor';
16
18
19+import * as need from 'lib/need';
20+import { WALLET_TYPES, urbitWalletFromTicket } from 'lib/wallet';
21+import useWalletType from 'lib/useWalletType';
22+
17//TODO should be part of InputWithStatus component
23//TODO should be part of InputWithStatus component
18const INPUT_STATUS = {
24const INPUT_STATUS = {
19 SPIN: Symbol('SPIN'),
25 SPIN: Symbol('SPIN'),
@@ -22,53 +28,72 @@ const INPUT_STATUS = {
22};
28};
23
29
24export default function Ticket({ advanced, loginCompleted }) {
30export default function Ticket({ advanced, loginCompleted }) {
31+ useWalletType(WALLET_TYPES.TICKET);
25 // globals
32 // globals
26 const { contracts } = useNetwork();
33 const { contracts } = useNetwork();
27− const { wallet, setWalletType, setUrbitWallet } = useWallet();
34+ const { wallet, setUrbitWallet } = useWallet();
28 const { setPointCursor } = usePointCursor();
35 const { setPointCursor } = usePointCursor();
29
36
30 // inputs
37 // inputs
38+ const pointInput = usePointInput({
39+ name: 'point',
40+ label: 'Point',
31− //TODO deduce point name from URL if we can, prefill input if we found it
41+ //TODO deduce point name from URL if we can, prefill input if we found it
42+ // initialValue: '~',
43+ autoFocus: true,
44+ });
32− const [pointName, setPointName] = useState('');
45+ const pointName = pointInput.data;
46+
47+ const ticketInput = useTicketInput({
48+ name: 'ticket',
49+ label: 'Master ticket',
50+ });
33− const [ticket, setTicket] = useState('');
51+ const ticket = ticketInput.data;
52+
53+ const passphraseInput = usePassphraseInput({
54+ name: 'passphrase',
55+ label: '(Optional) Wallet Passphrase',
56+ });
34− const [passphrase, setPassphrase] = useState('');
57+ const passphrase = passphraseInput.data;
35
58
36 // display state
59 // display state
37 const [ticketStatus, setTicketStatus] = useState(Nothing());
60 const [ticketStatus, setTicketStatus] = useState(Nothing());
38
61
62+ //TODO maybe want to do this only on-go, because wallet derivation is slow...
63+ const verifyTicket = useCallback(
64+ async (pointName, ticket, passphrase) => {
65+ setUrbitWallet(Nothing());
66+ if (!ob.isValidPatq(ticket) || !ob.isValidPatp(pointName)) {
67+ setTicketStatus(Nothing());
68+ return;
69+ }
70+ setTicketStatus(Just(INPUT_STATUS.SPIN));
71+ const _contracts = need.contracts(contracts);
72+ const pointNumber = ob.patp2dec(pointName);
73+ const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
74+ const isOwner = azimuth.azimuth.isOwner(
75+ _contracts,
76+ pointNumber,
77+ uhdw.ownership.keys.address
78+ );
79+ const isTransferProxy = azimuth.azimuth.isTransferProxy(
80+ _contracts,
81+ pointNumber,
82+ uhdw.ownership.keys.address
83+ );
84+ setUrbitWallet(Just(uhdw));
85+ const newStatus =
86+ (await isOwner) || (await isTransferProxy)
87+ ? INPUT_STATUS.GOOD
88+ : INPUT_STATUS.FAIL;
89+ setTicketStatus(Just(newStatus));
90+ },
91+ [contracts, setUrbitWallet, setTicketStatus]
92+ );
93+
39 useEffect(() => {
94 useEffect(() => {
40 verifyTicket(pointName, ticket, passphrase);
95 verifyTicket(pointName, ticket, passphrase);
41− return () => {};
96+ }, [verifyTicket, pointName, ticket, passphrase]);
42− }, [pointName, ticket, passphrase]);
43−
44− //TODO maybe want to do this only on-go, because wallet derivation is slow...
45− const verifyTicket = async (pointName, ticket, passphrase) => {
46− setUrbitWallet(Nothing());
47− if (!ob.isValidPatq(ticket) || !ob.isValidPatp(pointName)) {
48− setTicketStatus(Nothing());
49− return;
50− }
51− setTicketStatus(Just(INPUT_STATUS.SPIN));
52− const _contracts = need.contracts(contracts);
53− const pointNumber = ob.patp2dec(pointName);
54− const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
55− const isOwner = azimuth.azimuth.isOwner(
56− _contracts,
57− pointNumber,
58− uhdw.ownership.keys.address
59− );
60− const isTransferProxy = azimuth.azimuth.isTransferProxy(
61− _contracts,
62− pointNumber,
63− uhdw.ownership.keys.address
64− );
65− setUrbitWallet(Just(uhdw));
66− const newStatus =
67− (await isOwner) || (await isTransferProxy)
68− ? INPUT_STATUS.GOOD
69− : INPUT_STATUS.FAIL;
70− setTicketStatus(Just(newStatus));
71− };
72
97
73 const canContinue = () => {
98 const canContinue = () => {
74 // this is our only requirement, since we still want people with
99 // this is our only requirement, since we still want people with
@@ -77,61 +102,32 @@ export default function Ticket({ advanced, loginCompleted }) {
77 };
102 };
78
103
79 const doContinue = () => {
104 const doContinue = () => {
80− setWalletType(WALLET_TYPES.TICKET);
81 setPointCursor(Just(ob.patp2dec(pointName)));
105 setPointCursor(Just(ob.patp2dec(pointName)));
82 loginCompleted();
106 loginCompleted();
83 };
107 };
84
108
85− const pointInput = (
86− <PointInput
87− name="point"
88− label="Point"
89− initialValue={pointName}
90− onValue={setPointName}
91− autoFocus
92− />
93− );
94−
95 //TODO integrate into TicketInput?
109 //TODO integrate into TicketInput?
96 const displayTicketStatus = ticketStatus.matchWith({
110 const displayTicketStatus = ticketStatus.matchWith({
97 Nothing: () => <span />,
111 Nothing: () => <span />,
98 Just: status => {
112 Just: status => {
99 switch (status.value) {
113 switch (status.value) {
100 case INPUT_STATUS.SPIN:
114 case INPUT_STATUS.SPIN:
101− return <span>⋯</span>;
115+ return <AccessoryIcon.Pending />;
102 case INPUT_STATUS.GOOD:
116 case INPUT_STATUS.GOOD:
103− return <span>✓</span>;
117+ return <AccessoryIcon.Success />;
104 case INPUT_STATUS.FAIL:
118 case INPUT_STATUS.FAIL:
105− return <span>𐄂</span>;
119+ return <AccessoryIcon.Failure />;
106 default:
120 default:
107− throw new Error('weird input status ' + status.value);
121+ return null;
108 }
122 }
109 },
123 },
110 });
124 });
111
125
112− const ticketInput = (
113− <TicketInput
114− name="ticket"
115− label="Master ticket"
116− initialValue={ticket}
117− onValue={setTicket}
118− />
119− );
120−
121− const passphraseInput = !advanced ? null : (
122− <PassphraseInput
123− name="passphrase"
124− label="(Optional) Wallet Passphrase"
125− initialValue={passphrase}
126− onValue={setPassphrase}
127− />
128− );
129−
130 return (
126 return (
131 <View>
127 <View>
132− {pointInput}
128+ <Input {...pointInput} />
133− {ticketInput}
129+ <Input {...ticketInput} accessory={displayTicketStatus} />
134− {passphraseInput}
130+ {advanced && <Input {...passphraseInput} />}
135
131
136 <ForwardButton
132 <ForwardButton
137 className={'mt3'}
133 className={'mt3'}
src/views/Login/Trezor.js+7−2
@@ -4,13 +4,16 @@ import Maybe from 'folktale/maybe';
4import { times } from 'lodash';
4import { times } from 'lodash';
5import TrezorConnect from 'trezor-connect';
5import TrezorConnect from 'trezor-connect';
6import * as secp256k1 from 'secp256k1';
6import * as secp256k1 from 'secp256k1';
7+import { H1, P, Input } from 'indigo-react';
7
8
8import View from 'components/View';
9import View from 'components/View';
9import { ForwardButton } from 'components/Buttons';
10import { ForwardButton } from 'components/Buttons';
10−import { H1, P, Input } from 'indigo-react';
11−import { InnerLabel, InnerLabelDropdown } from 'components/old/Base';
11+import { InnerLabelDropdown } from 'components/old/Base';
12
12
13import { TREZOR_PATH } from 'lib/trezor';
13import { TREZOR_PATH } from 'lib/trezor';
14+import useWalletType from 'lib/useWalletType';
15+import { WALLET_TYPES } from 'lib/wallet';
16+
14import { useWallet } from 'store/wallet';
17import { useWallet } from 'store/wallet';
15
18
16const accountOptions = [
19const accountOptions = [
@@ -19,6 +22,8 @@ const accountOptions = [
19];
22];
20
23
21export default function Trezor({ loginCompleted }) {
24export default function Trezor({ loginCompleted }) {
25+ useWalletType(WALLET_TYPES.TREZOR);
26+
22 const { wallet, setWallet, setWalletHdPath } = useWallet();
27 const { wallet, setWallet, setWalletHdPath } = useWallet();
23
28
24 const [hdPath, setHdPath] = useState(TREZOR_PATH.replace(/x/g, 0));
29 const [hdPath, setHdPath] = useState(TREZOR_PATH.replace(/x/g, 0));
src/views/Mnemonic.jsdeleted−78
@@ -1,78 +0,0 @@
1−import React, { useCallback, useEffect, useState } from 'react';
2−import { Just, Nothing } from 'folktale/maybe';
3−
4−import View from 'components/View';
5−import { MnemonicInput, PassphraseInput, HdPathInput } from 'components/Inputs';
6−import { ForwardButton } from 'components/Buttons';
7−
8−import { ROUTE_NAMES } from 'lib/routeNames';
9−import { useHistory } from 'store/history';
10−import { walletFromMnemonic } from 'lib/wallet';
11−import { useWallet } from 'store/wallet';
12−
13−export default function Mnemonic() {
14− const history = useHistory();
15− const {
16− wallet,
17− setWallet,
18− authMnemonic,
19− setAuthMnemonic,
20− walletHdPath,
21− setWalletHdPath,
22− } = useWallet();
23− const [passphrase, setPassphrase] = useState('');
24− const mnemonic = authMnemonic.getOrElse('');
25−
26− // TODO: move this into transformers?
27− // transform the result of the mnemonic to Maybe<string>
28− const _setAuthMnemonic = useCallback(
29− mnemonic => setAuthMnemonic(mnemonic === '' ? Nothing() : Just(mnemonic)),
30− [setAuthMnemonic]
31− );
32−
33− // when the properties change, re-derive wallet
34− useEffect(() => {
35− let mounted = true;
36− // allow the browser to do something else while we derive this wallet
37− // on the main thread by making this an async function
38− (async () => {
39− const wallet = walletFromMnemonic(mnemonic, walletHdPath, passphrase);
40− mounted && setWallet(wallet);
41− })();
42− return () => (mounted = false);
43− }, [mnemonic, passphrase, walletHdPath, setWallet]);
44−
45− return (
46− <View>
47− <MnemonicInput
48− name="mnemonic"
49− label="BIP39 Mnemonic"
50− initialValue={mnemonic}
51− onValue={_setAuthMnemonic}
52− autoFocus
53− />
54−
55− <PassphraseInput
56− name="passphrase"
57− label="(Optional) Wallet Passphrase"
58− initialValue={passphrase}
59− onValue={setPassphrase}
60− />
61−
62− <HdPathInput
63− name="hdpath"
64− label="HD Path"
65− initialValue={walletHdPath}
66− onValue={setWalletHdPath}
67− />
68−
69− <ForwardButton
70− className="mt3"
71− disabled={Nothing.hasInstance(wallet)}
72− onClick={() => history.popAndPush(ROUTE_NAMES.POINTS)}
73− solid>
74− Continue
75− </ForwardButton>
76− </View>
77− );
78−}
src/views/Network.jsdeleted−62
@@ -1,62 +0,0 @@
1−import React from 'react';
2−import { Button } from 'indigo-react';
3−
4−import { InnerLabelDropdown } from '../components/old/Base';
5−import { H1, P } from '../components/old/Base';
6−import { NETWORK_TYPES, renderNetworkType } from '../lib/network';
7−import { ROUTE_NAMES } from '../lib/routeNames';
8−import { useHistory } from '../store/history';
9−import { useNetwork } from '../store/network';
10−import View from 'components/View';
11−
12−const kNetworkOptions = [
13− {
14− title: 'Main Network (default)',
15− value: NETWORK_TYPES.MAINNET,
16− },
17− {
18− title: 'Ropsten',
19− value: NETWORK_TYPES.ROPSTEN,
20− },
21− {
22− title: 'Local Node',
23− value: NETWORK_TYPES.LOCAL,
24− },
25− {
26− type: 'divider',
27− },
28− {
29− title: 'Offline',
30− value: NETWORK_TYPES.OFFLINE,
31− },
32−];
33−
34−function Network() {
35− const { networkType, setNetworkType } = useNetwork();
36− const history = useHistory();
37−
38− return (
39− <View>
40− <H1>{'Select Network'}</H1>
41−
42− <P>
43− {"Please select the Ethereum node you'd like to send " +
44− 'transactions to. For highly valuable keys, please select ' +
45− 'offline mode.'}
46− </P>
47−
48− <InnerLabelDropdown
49− title={'Node:'}
50− handleUpdate={setNetworkType}
51− options={kNetworkOptions}
52− currentSelectionTitle={renderNetworkType(networkType)}
53− />
54−
55− <Button icon="→" onClick={() => history.push(ROUTE_NAMES.WALLET)}>
56− Continue
57− </Button>
58− </View>
59− );
60−}
61−
62−export default Network;
src/views/Point.js+18−13
@@ -1,39 +1,43 @@
1import React, { useCallback } from 'react';
1import React, { useCallback } from 'react';
2+import Maybe from 'folktale/maybe';
2import * as need from 'lib/need';
3import * as need from 'lib/need';
3import { Grid } from 'indigo-react';
4import { Grid } from 'indigo-react';
4
5
5import { usePointCursor } from 'store/pointCursor';
6import { usePointCursor } from 'store/pointCursor';
6−// import { usePointCache } from 'store/pointCache';
7+import { useHistory } from 'store/history';
7
8
8import View from 'components/View';
9import View from 'components/View';
9import Passport from 'components/Passport';
10import Passport from 'components/Passport';
10import { ForwardButton } from 'components/Buttons';
11import { ForwardButton } from 'components/Buttons';
12+import FooterButton from 'components/FooterButton';
13+import { matchBlinky } from 'components/Blinky';
11
14
12import useInvites from 'lib/useInvites';
15import useInvites from 'lib/useInvites';
13−import useSyncPoint from 'lib/useSyncPoint';
16+import { useSyncOwnedPoints } from 'lib/useSyncPoints';
14import { ROUTE_NAMES } from 'lib/routeNames';
17import { ROUTE_NAMES } from 'lib/routeNames';
15
18
16−import { useHistory } from 'store/history';
19+import Actions from './Point/Actions';
17−import FooterButton from 'components/FooterButton';
20+import { useWallet } from 'store/wallet';
18
21
19export default function Point() {
22export default function Point() {
20 const history = useHistory();
23 const history = useHistory();
21 const { pointCursor } = usePointCursor();
24 const { pointCursor } = usePointCursor();
22− // const { pointCache } = usePointCache();
25+ const { wallet } = useWallet();
23
26
24 const point = need.pointCursor(pointCursor);
27 const point = need.pointCursor(pointCursor);
25
28
26 // fetch the invites for the current cursor
29 // fetch the invites for the current cursor
27 const { availableInvites } = useInvites(point);
30 const { availableInvites } = useInvites(point);
31+ const availableInvitesText = matchBlinky(availableInvites);
32+
33+ const showActions = Maybe.Just.hasInstance(wallet);
28
34
29 const goInvite = useCallback(() => history.push(ROUTE_NAMES.INVITE), [
35 const goInvite = useCallback(() => history.push(ROUTE_NAMES.INVITE), [
30 history,
36 history,
31 ]);
37 ]);
32
38
33 // sync the current cursor
39 // sync the current cursor
34− useSyncPoint(point);
40+ useSyncOwnedPoints([point]);
35−
36− // const pointDetails = need.fromPointCache(pointCache, point);
37
41
38 return (
42 return (
39 <View>
43 <View>
@@ -48,14 +52,15 @@ export default function Point() {
48 Boot Arvo
52 Boot Arvo
49 </ForwardButton>
53 </ForwardButton>
50 </Grid.Item>
54 </Grid.Item>
55+ {showActions && (
56+ <Grid.Item full>
57+ <Actions />
58+ </Grid.Item>
59+ )}
51 </Grid>
60 </Grid>
52
61
53 <FooterButton onClick={goInvite}>
62 <FooterButton onClick={goInvite}>
54− Invite
63+ Invite <sup>{availableInvitesText}</sup>
55− {availableInvites.matchWith({
56− Nothing: () => null,
57− Just: p => <sup className="ml1">{p.value}</sup>,
58− })}
59 </FooterButton>
64 </FooterButton>
60 </View>
65 </View>
61 );
66 );
src/views/Point/Actions.js+23−49
@@ -9,20 +9,23 @@ import { ROUTE_NAMES } from '../../lib/routeNames';
9import { useHistory } from '../../store/history';
9import { useHistory } from '../../store/history';
10import { useWallet } from '../../store/wallet';
10import { useWallet } from '../../store/wallet';
11import { Grid } from 'indigo-react';
11import { Grid } from 'indigo-react';
12+import { usePointCursor } from 'store/pointCursor';
13+import { usePointCache } from 'store/pointCache';
12
14
13const isPlanet = point =>
15const isPlanet = point =>
14 azimuth.getPointSize(point) === azimuth.PointSize.Planet;
16 azimuth.getPointSize(point) === azimuth.PointSize.Planet;
15−const isStar = point => azimuth.getPointSize(point) === azimuth.PointSize.Star;
16
17
17−function Actions(props) {
18+function Actions() {
18 const history = useHistory();
19 const history = useHistory();
19 const { wallet } = useWallet();
20 const { wallet } = useWallet();
20− const { online, point, pointDetails, invites } = props;
21+ const { pointCursor } = usePointCursor();
22+ const point = need.pointCursor(pointCursor);
23+ const { getDetails } = usePointCache();
24+ const pointDetails = getDetails(point);
21
25
22 const addr = need.addressFromWallet(wallet);
26 const addr = need.addressFromWallet(wallet);
23
27
24 const planet = isPlanet(point);
28 const planet = isPlanet(point);
25− const star = isStar(point);
26
29
27 const isOwner = pointDetails.matchWith({
30 const isOwner = pointDetails.matchWith({
28 Nothing: _ => false,
31 Nothing: _ => false,
@@ -57,11 +60,6 @@ function Actions(props) {
57 },
60 },
58 });
61 });
59
62
60− const hasInvites = invites.matchWith({
61− Nothing: () => false,
62− Just: count => count.value > 0,
63− });
64−
65 const canTransfer = pointDetails.matchWith({
63 const canTransfer = pointDetails.matchWith({
66 Nothing: () => false,
64 Nothing: () => false,
67 Just: deets =>
65 Just: deets =>
@@ -104,37 +102,6 @@ function Actions(props) {
104 },
102 },
105 });
103 });
106
104
107− let inviteAction = null;
108− if (planet) {
109− inviteAction = (
110− <Button
111− disabled={!isActiveOwner || !online || !hasInvites}
112− prop-size={'sm'}
113− prop-type={'link'}
114− onClick={() => history.push(ROUTE_NAMES.INVITE)}>
115− {'Send invites ('}
116− {invites.matchWith({
117− Nothing: () => '?',
118− Just: count => count.value,
119− })}
120− {')'}
121− </Button>
122− );
123− }
124− if (star) {
125− inviteAction = (
126− <Button
127− disabled={!(isActiveOwner && canIssueChild) || !online}
128− prop-size={'sm'}
129− prop-type={'link'}
130− onClick={() => {
131− history.push(ROUTE_NAMES.INVITES_MANAGE);
132− }}>
133− {'Manage invites'}
134− </Button>
135− );
136− }
137−
138 return (
105 return (
139 <div>
106 <div>
140 <H2>{'Actions'}</H2>
107 <H2>{'Actions'}</H2>
@@ -149,7 +116,7 @@ function Actions(props) {
149 <Button
116 <Button
150 prop-size={'sm'}
117 prop-size={'sm'}
151 prop-type={'link'}
118 prop-type={'link'}
152− disabled={(online || planet) && !canIssueChild}
119+ disabled={planet && !canIssueChild}
153 onClick={() => {
120 onClick={() => {
154 history.push(ROUTE_NAMES.ISSUE_CHILD);
121 history.push(ROUTE_NAMES.ISSUE_CHILD);
155 }}>
122 }}>
@@ -159,7 +126,7 @@ function Actions(props) {
159 <Button
126 <Button
160 prop-size={'sm'}
127 prop-size={'sm'}
161 prop-type={'link'}
128 prop-type={'link'}
162− disabled={online && !canAcceptTransfer}
129+ disabled={!canAcceptTransfer}
163 onClick={() => {
130 onClick={() => {
164 history.push(ROUTE_NAMES.ACCEPT_TRANSFER);
131 history.push(ROUTE_NAMES.ACCEPT_TRANSFER);
165 }}>
132 }}>
@@ -169,7 +136,7 @@ function Actions(props) {
169 <Button
136 <Button
170 prop-size={'sm'}
137 prop-size={'sm'}
171 prop-type={'link'}
138 prop-type={'link'}
172− disabled={online && !canCancelTransfer}
139+ disabled={!canCancelTransfer}
173 onClick={() => {
140 onClick={() => {
174 history.push(ROUTE_NAMES.CANCEL_TRANSFER);
141 history.push(ROUTE_NAMES.CANCEL_TRANSFER);
175 }}>
142 }}>
@@ -179,7 +146,7 @@ function Actions(props) {
179 <Button
146 <Button
180 prop-size={'sm'}
147 prop-size={'sm'}
181 prop-type={'link'}
148 prop-type={'link'}
182− disabled={online && !canGenKeyfile}
149+ disabled={!canGenKeyfile}
183 onClick={() => {
150 onClick={() => {
184 history.push(ROUTE_NAMES.GEN_KEYFILE);
151 history.push(ROUTE_NAMES.GEN_KEYFILE);
185 }}>
152 }}>
@@ -188,7 +155,7 @@ function Actions(props) {
188 </Grid.Item>
155 </Grid.Item>
189 <Grid.Item third={2}>
156 <Grid.Item third={2}>
190 <Button
157 <Button
191− disabled={online && !canSetSpawnProxy}
158+ disabled={!canSetSpawnProxy}
192 prop-size={'sm'}
159 prop-size={'sm'}
193 prop-type={'link'}
160 prop-type={'link'}
194 onClick={() => {
161 onClick={() => {
@@ -198,7 +165,7 @@ function Actions(props) {
198 </Button>
165 </Button>
199
166
200 <Button
167 <Button
201− disabled={online && !canSetManagementProxy}
168+ disabled={!canSetManagementProxy}
202 prop-size={'sm'}
169 prop-size={'sm'}
203 prop-type={'link'}
170 prop-type={'link'}
204 onClick={() => {
171 onClick={() => {
@@ -208,7 +175,7 @@ function Actions(props) {
208 </Button>
175 </Button>
209
176
210 <Button
177 <Button
211− disabled={online && !canConfigureKeys}
178+ disabled={!canConfigureKeys}
212 prop-size={'sm'}
179 prop-size={'sm'}
213 prop-type={'link'}
180 prop-type={'link'}
214 onClick={() => {
181 onClick={() => {
@@ -218,7 +185,7 @@ function Actions(props) {
218 </Button>
185 </Button>
219
186
220 <Button
187 <Button
221− disabled={online && !canTransfer}
188+ disabled={!canTransfer}
222 prop-size={'sm'}
189 prop-size={'sm'}
223 prop-type={'link'}
190 prop-type={'link'}
224 onClick={() => {
191 onClick={() => {
@@ -226,8 +193,15 @@ function Actions(props) {
226 }}>
193 }}>
227 {'Transfer'}
194 {'Transfer'}
228 </Button>
195 </Button>
196+ <Button
197+ prop-size={'sm'}
198+ prop-type={'link'}
199+ onClick={() => {
200+ history.push(ROUTE_NAMES.INVITES_MANAGE);
201+ }}>
202+ {'Invite'}
203+ </Button>
229 </Grid.Item>
204 </Grid.Item>
230− <Grid.Item third={3}>{inviteAction}</Grid.Item>
231 </Grid>
205 </Grid>
232 </div>
206 </div>
233 );
207 );
src/views/Points.js+133−219
@@ -1,245 +1,159 @@
1import React from 'react';
1import React from 'react';
2−import { H1, H2, P } from '../components/old/Base';
2+import Maybe from 'folktale/maybe';
3−import { Button } from '../components/old/Base';
3+import { H1, H2, P, Grid } from 'indigo-react';
4−import * as azimuth from 'azimuth-js';
4+
5−
6−import PointList from '../components/old/PointList';
7−import { NETWORK_TYPES } from '../lib/network';
8−import { ROUTE_NAMES } from '../lib/routeNames';
5+import { ROUTE_NAMES } from 'lib/routeNames';
9−import { withHistory } from '../store/history';
10−import { ETH_ZERO_ADDR, eqAddr } from '../lib/wallet';
6+import { ETH_ZERO_ADDR, eqAddr } from 'lib/wallet';
11−import { withNetwork } from '../store/network';
7+import useIsEclipticOwner from 'lib/useIsEclipticOwner';
12−import { compose } from '../lib/lib';
8+import { useSyncKnownPoints } from 'lib/useSyncPoints';
9+
10+import { useHistory } from 'store/history';
13−import { withWallet } from '../store/wallet';
11+import { useWallet } from 'store/wallet';
14−import { withPointCache } from '../store/pointCache';
12+import { usePointCache } from 'store/pointCache';
13+
14+import PointList from 'components/old/PointList';
15import View from 'components/View';
15import View from 'components/View';
16−import { Grid } from 'indigo-react';
16+import { ForwardButton } from 'components/Buttons';
17−
17+import useLifecycle from 'lib/useLifecycle';
18+
19+const matchArray = obj =>
20+ obj.matchWith({ Nothing: () => [], Just: p => p.value });
21+
18−const hasTransferProxy = (cache, point) =>
22+const hasTransferProxy = (details, point) =>
19− point in cache ? !eqAddr(cache[point].transferProxy, ETH_ZERO_ADDR) : false;
23+ !eqAddr(details.transferProxy, ETH_ZERO_ADDR);
20−
24+
21−class Points extends React.Component {
25+export default function Points() {
22− constructor(props) {
26+ const { wallet } = useWallet();
23− super(props);
24−
25− const { networkType } = props;
26− const loading = networkType !== NETWORK_TYPES.OFFLINE;
27−
28− this.state = {
29− points: [],
30− incoming: [],
31− managing: [],
32− voting: [],
33− spawning: [],
34− eclipticOwner: false,
35− loading: loading,
36− };
37− }
38−
39− componentDidMount() {
40− const { web3, wallet, contracts, addToPointCache } = this.props;
41−
42− web3.chain(_ =>
43− contracts.chain(ctrcs =>
44− wallet.chain(async wal => {
45− const addr = wal.address;
46−
47− const points = await azimuth.azimuth.getOwnedPoints(ctrcs, addr);
48−
49− const incoming = await azimuth.azimuth.getTransferringFor(
50− ctrcs,
51− addr
52− );
53−
54− const managing = await azimuth.azimuth.getManagerFor(ctrcs, addr);
55−
56− const voting = await azimuth.azimuth.getVotingFor(ctrcs, addr);
57−
58− const spawning = await azimuth.azimuth.getSpawningFor(ctrcs, addr);
59−
60− const owner = await azimuth.ecliptic.owner(ctrcs);
61−
62− const eclipticOwner = eqAddr(addr, owner);
63−
64− this.setState({
65− points,
66− incoming,
67− managing,
68− voting,
69− spawning,
70− eclipticOwner,
71− loading: false,
72− });
73−
74− this.cachePoints(ctrcs, addToPointCache, points);
75− })
76− )
77− );
78− }
79−
80− cachePoints(contracts, addToPointCache, points) {
81− points.forEach(point => {
82− azimuth.azimuth
83− .getPoint(contracts, point)
84− .then(details => addToPointCache({ [point]: details }));
85− });
86− }
87−
88− render() {
89− const { history, pointCache } = this.props;
27+ const history = useHistory();
90−
28+ const {
91− const {
29+ getControlledPoints,
92− points,
30+ syncControlledPoints,
31+ getDetails,
32+ } = usePointCache();
33+ const {
34+ ownedPoints,
35+ incomingPoints,
36+ managingPoints,
37+ votingPoints,
38+ spawningPoints,
39+ } = getControlledPoints(wallet);
40+ const isEclipticOwner = useIsEclipticOwner();
41+
42+ const owned = matchArray(ownedPoints);
43+ const incoming = matchArray(incomingPoints);
44+ const managing = matchArray(managingPoints);
45+ const voting = matchArray(votingPoints);
46+ const spawning = matchArray(spawningPoints);
47+
48+ // sync controlled points on mount
49+ useLifecycle(() => {
50+ syncControlledPoints();
51+ });
52+
53+ // sync display details for known points
54+ useSyncKnownPoints([
55+ ...owned,
93− incoming,
56+ ...incoming,
94− managing,
57+ ...managing,
95− voting,
58+ ...voting,
96− spawning,
59+ ...spawning,
97− loading,
60+ ]);
98− eclipticOwner,
61+
99− } = this.state;
62+ const loading = Maybe.Nothing.hasInstance(ownedPoints);
100−
63+
101− const lookupPointButton = (
64+ const outgoing = ownedPoints.filter(point =>
65+ getDetails(point).matchWith({
66+ Nothing: () => false,
67+ Just: p => hasTransferProxy(p.value),
68+ })
69+ );
70+
71+ return (
72+ <View>
73+ <H1>Points</H1>
74+
75+ <P>
76+ A point is an identity on the Ethereum blockchain. Points declare keys
77+ for ships on the Arvo network.
78+ </P>
79+
102 <Grid>
80 <Grid>
103 <Grid.Item full>
81 <Grid.Item full>
104− <Button
82+ <ForwardButton
105− prop-type={'link'}
83+ detail="View a point on Azimuth"
106− prop-size={'md'}
107 onClick={() => history.push(ROUTE_NAMES.VIEW_POINT)}>
84 onClick={() => history.push(ROUTE_NAMES.VIEW_POINT)}>
108− {'View a point →'}
85+ View a point
109− </Button>
86+ </ForwardButton>
110− <P>{'View a point on Azimuth.'}</P>
111 </Grid.Item>
87 </Grid.Item>
112 </Grid>
88 </Grid>
113− );
114
89
115− const createGalaxyButton =
90+ {isEclipticOwner && (
116− eclipticOwner === false ? (
117− <div />
118− ) : (
119 <Grid>
91 <Grid>
120 <Grid.Item full>
92 <Grid.Item full>
121− <Button
93+ <ForwardButton
122− prop-type={'link'}
94+ detail="You have the authority to create a new Galaxy."
123− prop-size={'md'}
124 onClick={() => history.push(ROUTE_NAMES.CREATE_GALAXY)}>
95 onClick={() => history.push(ROUTE_NAMES.CREATE_GALAXY)}>
125− {'Create a galaxy →'}
96+ Create a galaxy
126− </Button>
97+ </ForwardButton>
127− <P>
128− {
129− 'You have the authority to create a new Galaxy and you can do so here.'
130− }
131− </P>
132 </Grid.Item>
98 </Grid.Item>
133 </Grid>
99 </Grid>
134− );
100+ )}
135
101
136− const outgoing = points.filter(point =>
137− hasTransferProxy(pointCache, point)
138− );
139−
140− const outgoingPoints =
141− outgoing.length !== 0 ? (
102+ {incoming.length > 0 && (
142− <React.Fragment>
103+ <>
143− <H2>{'Outgoing Transfers'}</H2>
104+ <H2>Incoming Transfers</H2>
144 <P>
105 <P>
145− {`You own these points until the recipient accepts the incoming
106+ You do not own these points until you accept the incoming transfer.
146− transfer. You may cancel the transfer until accepted.`}
107+ You may reject any incoming transfers.
147 </P>
108 </P>
148− <PointList routeHandler={history.push} points={outgoing} />
109+ <PointList routeHandler={history.push} points={incoming} />
149− </React.Fragment>
110+ </>
150− ) : (
111+ )}
151− <div />
112+
152− );
153−
154− const incomingPoints =
155− incoming.length !== 0 ? (
113+ {outgoing.length > 0 && (
156− <React.Fragment>
114+ <>
157− <H2>{'Incoming Transfers'}</H2>
115+ <H2>Outgoing Transfers</H2>
158 <P>
116 <P>
159− {`You do not own these points until you accept the incoming transfer.
117+ You own these points until the recipient accepts the incoming
160− You may reject any incoming transfers.`}
118+ transfer. You may cancel the transfer until accepted.
161 </P>
119 </P>
162− <PointList routeHandler={history.push} points={incoming} />
120+ <PointList routeHandler={history.push} points={outgoing} />
163− </React.Fragment>
121+ </>
164− ) : (
122+ )}
165− <div />
123+
166− );
124+ <H2>Your Points</H2>
167−
125+
168− const managingPoints =
126+ <PointList routeHandler={history.push} points={owned} loading={loading} />
127+
169− managing.length !== 0 ? (
128+ {managing.length > 0 && (
170− <React.Fragment>
129+ <>
171− <H2>{'You Are a Management Proxy For'}</H2>
130+ <H2>You Are a Management Proxy For</H2>
172 <P>
131 <P>
173− {`You can configure or set network keys and conduct sponsorship
132+ You can configure or set network keys and conduct sponsorship
174− related operations for these points.`}
133+ related operations for these points.
175 </P>
134 </P>
176 <PointList routeHandler={history.push} points={managing} />
135 <PointList routeHandler={history.push} points={managing} />
177− </React.Fragment>
136+ </>
178− ) : (
179− <div />
180− );
137+ )}
181−
138+
182− const votingPoints =
183− voting.length !== 0 ? (
139+ {voting.length > 0 && (
184− <React.Fragment>
140+ <>
185− <H2>{'You Are a Voting Proxy For'}</H2>
141+ <H2>You Are a Voting Proxy For</H2>
186 <P>
142 <P>
187− {`Since you are part of the Galactic Senate, you can cast votes on
143+ Since you are part of the Galactic Senate, you can cast votes on new
188− new Azimuth proposals on behalf of these points.`}
144+ Azimuth proposals on behalf of these points.
189 </P>
145 </P>
190 <PointList routeHandler={history.push} points={voting} />
146 <PointList routeHandler={history.push} points={voting} />
191− </React.Fragment>
147+ </>
192− ) : (
193− <div />
194− );
195−
196− const spawningPoints =
197− spawning.length !== 0 ? (
198− <React.Fragment>
199− <H2>{'You Are a Spawn Proxy For'}</H2>
200− <P>{`You can create new child ships under these points.`}</P>
201− <PointList routeHandler={history.push} points={spawning} />
202− </React.Fragment>
203− ) : (
204− <div />
205− );
148+ )}
206−
207− return (
208− <View>
209− <H1>{'Points'}</H1>
210−
211− <P>{`A point is an identity on the Ethereum blockchain.
212− Points declare keys for ships on the Arvo network.`}</P>
213−
214− {lookupPointButton}
215−
216− {createGalaxyButton}
217−
218− {incomingPoints}
219−
220− {outgoingPoints}
221
149
222− <H2>{'Your Points'}</H2>
150+ {spawning.length > 0 && (
223−
151+ <>
224− <PointList
152+ <H2>You Are a Spawn Proxy For</H2>
153+ <P>You can create new child points under these points.</P>
225− routeHandler={history.push}
154+ <PointList routeHandler={history.push} points={spawning} />
226− points={points}
155+ </>
227− loading={loading}
156+ )}
228− />
157+ </View>
229−
158+ );
230− {managingPoints}
231−
232− {votingPoints}
233−
234− {spawningPoints}
235− </View>
236− );
237− }
238}
159}
239−
240−export default compose(
241− withNetwork,
242− withHistory,
243− withWallet,
244− withPointCache
245−)(Points);
src/views/PrivateKey.jsdeleted−84
@@ -1,84 +0,0 @@
1−import Maybe from 'folktale/maybe';
2−import React from 'react';
3−import { Button } from '../components/old/Base';
4−import {
5− RequiredInput,
6− InnerLabel,
7− InputCaption,
8−} from '../components/old/Base';
9−import { H1 } from '../components/old/Base';
10−
11−import { ROUTE_NAMES } from '../lib/routeNames';
12−import { withHistory } from '../store/history';
13−import { EthereumWallet } from '../lib/wallet';
14−import { compose } from '../lib/lib';
15−import { withWallet } from '../store/wallet';
16−import View from 'components/View';
17−
18−class PrivateKey extends React.Component {
19− constructor(props) {
20− super(props);
21−
22− this.state = {
23− privateKey: '',
24− };
25−
26− this.handlePrivateKeyInput = this.handlePrivateKeyInput.bind(this);
27− }
28−
29− handlePrivateKeyInput(privateKey) {
30− this.setState({ privateKey });
31− this.constructWallet(privateKey);
32− }
33−
34− constructWallet(privateKey) {
35− const { setWallet } = this.props;
36− if (/^[0-9A-Fa-f]{64}$/g.test(privateKey) === true) {
37− const sec = Buffer.from(privateKey, 'hex');
38− const wallet = new EthereumWallet(sec);
39− setWallet(Maybe.Just(wallet));
40− } else {
41− setWallet(Maybe.Nothing());
42− }
43− }
44−
45− render() {
46− const { history, wallet } = this.props;
47− const { privateKey } = this.state;
48−
49− return (
50− <View>
51− <H1 className={'mb-4'}>{'Enter Your Private Key'}</H1>
52− <InputCaption>
53− {`Please enter your raw Ethereum private key here.`}
54− </InputCaption>
55−
56− <RequiredInput
57− className="pt-8 mt-8"
58− prop-size="md"
59− prop-format="innerLabel"
60− type="password"
61− name="privateKey"
62− onChange={this.handlePrivateKeyInput}
63− value={privateKey}
64− autocomplete="off"
65− autoFocus>
66− <InnerLabel>{'Private Key'}</InnerLabel>
67− </RequiredInput>
68−
69− <Button
70− className={'mt-10'}
71− prop-size={'wide lg'}
72− disabled={Maybe.Nothing.hasInstance(wallet)}
73− onClick={() => history.popAndPush(ROUTE_NAMES.POINTS)}>
74− {'Continue →'}
75− </Button>
76− </View>
77− );
78− }
79−}
80−
81−export default compose(
82− withHistory,
83− withWallet
84−)(PrivateKey);
src/views/Shards.jsdeleted−212
@@ -1,212 +0,0 @@
1−import { Just, Nothing } from 'folktale/maybe';
2−import React from 'react';
3−import { Button } from '../components/old/Base';
4−import {
5− InnerLabel,
6− ValidatedSigil,
7− PointInput,
8− ShardInput,
9− InputCaption,
10− Input,
11−} from '../components/old/Base';
12−import { H1, P } from '../components/old/Base';
13−import * as kg from 'urbit-key-generation/dist';
14−import * as ob from 'urbit-ob';
15−
16−import { ROUTE_NAMES } from '../lib/routeNames';
17−import { withHistory } from '../store/history';
18−import { urbitWalletFromTicket } from '../lib/wallet';
19−import { compose } from '../lib/lib';
20−import { withWallet } from '../store/wallet';
21−import View from 'components/View';
22−
23−const placeholder = len => {
24− let bytes = window.crypto.getRandomValues(new Uint8Array(len));
25− let hex = bytes.reduce(
26− (acc, byt) => acc + byt.toString(16).padStart(2, '0'),
27− ''
28− );
29− return ob.hex2patq(hex);
30−};
31−
32−const SHARDS = {
33− SHARD1: Symbol('SHARD1'),
34− SHARD2: Symbol('SHARD2'),
35− SHARD3: Symbol('SHARD3'),
36−};
37−
38−class Shards extends React.Component {
39− constructor(props) {
40− super(props);
41−
42− this.state = {
43− shard1: '',
44− shard2: '',
45− shard3: '',
46− passphrase: '',
47− pointName: '',
48− };
49−
50− this.pointPlaceholder = placeholder(4);
51− this.ticketPlaceholder = placeholder(16);
52−
53− this.handlePassphraseInput = this.handlePassphraseInput.bind(this);
54− this.handleShardInput = this.handleShardInput.bind(this);
55− this.handlePointNameInput = this.handlePointNameInput.bind(this);
56− }
57−
58− handleShardInput(shard, input) {
59− if (shard === SHARDS.SHARD1) {
60− this.setState({ shard1: input });
61− } else if (shard === SHARDS.SHARD2) {
62− this.setState({ shard2: input });
63− } else if (shard === SHARDS.SHARD3) {
64− this.setState({ shard3: input });
65− }
66− }
67−
68− handlePassphraseInput(passphrase) {
69− this.setState({ passphrase });
70− }
71−
72− handlePointNameInput(pointName) {
73− if (pointName.length < 15) {
74− this.setState({ pointName });
75− }
76− }
77−
78− async walletFromShards(shard1, shard2, shard3, pointName, passphrase) {
79− const s1 = shard1 === '' ? undefined : shard1;
80− const s2 = shard2 === '' ? undefined : shard2;
81− const s3 = shard3 === '' ? undefined : shard3;
82−
83− let ticket = undefined;
84− try {
85− ticket = kg.combine([s1, s2, s3]);
86− } catch (_) {
87− // do nothing
88− }
89−
90− if (ticket !== undefined) {
91− const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
92− this.props.setUrbitWallet(Just(uhdw));
93− }
94− }
95−
96− render() {
97− const { history, wallet } = this.props;
98− const { shard1, shard2, shard3, pointName, passphrase } = this.state;
99−
100− const phPoint = this.pointPlaceholder;
101− const phTick = this.ticketPlaceholder;
102−
103− const shards = [shard1, shard2, shard3];
104− const ready = shards.filter(x => x !== '').length > 1;
105−
106− return (
107− <View>
108− <H1>{'Authenticate'}</H1>
109−
110− <P>
111− {`Enter your point and at least two of your three Urbit master
112− ticket shards here. The index of the input field needs to
113− match the index of the shard.`}
114− </P>
115−
116− <PointInput
117− className="mono mt-8"
118− prop-size="lg"
119− prop-format="innerLabel"
120− type="text"
121− autoFocus
122− placeholder={`e.g. ${phPoint}`}
123− value={pointName}
124− onChange={this.handlePointNameInput}>
125− <InnerLabel>{'Point'}</InnerLabel>
126− <ValidatedSigil
127− className={'tr-0 mt-05 mr-0 abs'}
128− patp={pointName}
129− size={68}
130− margin={8}
131− />
132− </PointInput>
133−
134− <ShardInput
135− className="mono mt-8"
136− prop-size="md"
137− prop-format="innerLabel"
138− type="text"
139− name="shard1"
140− placeholder={`e.g. ${phTick}`}
141− value={shard1}
142− onChange={inp => this.handleShardInput(SHARDS.SHARD1, inp)}>
143− <InnerLabel>{'Shard 1'}</InnerLabel>
144− </ShardInput>
145−
146− <ShardInput
147− className="mono mt-8"
148− prop-size="md"
149− prop-format="innerLabel"
150− type="text"
151− name="shard2"
152− placeholder={`e.g. ${phTick}`}
153− value={shard2}
154− onChange={inp => this.handleShardInput(SHARDS.SHARD2, inp)}>
155− <InnerLabel>{'Shard 2'}</InnerLabel>
156− </ShardInput>
157−
158− <ShardInput
159− className="mono mt-8"
160− prop-size="md"
161− prop-format="innerLabel"
162− type="text"
163− name="shard3"
164− placeholder={`e.g. ${phTick}`}
165− value={shard3}
166− onChange={inp => this.handleShardInput(SHARDS.SHARD3, inp)}>
167− <InnerLabel>{'Shard 3'}</InnerLabel>
168− </ShardInput>
169−
170− <InputCaption>
171− {'If your wallet requires a passphrase, you may enter it below.'}
172− </InputCaption>
173−
174− <Input
175− className="pt-8"
176− prop-size="md"
177− prop-format="innerLabel"
178− name="passphrase"
179− type="password"
180− value={passphrase}
181− autocomplete="off"
182− onChange={this.handlePassphraseInput}>
183− <InnerLabel>{'Passphrase'}</InnerLabel>
184− </Input>
185−
186− <Button
187− disabled={!ready}
188− className={'mt-8'}
189− prop-size={'lg wide'}
190− // prop-color={this.buttonTriState(wallet)}
191− onClick={() =>
192− this.walletFromShards(shard1, shard2, shard3, pointName, passphrase)
193− }>
194− {'Unlock Wallet →'}
195− </Button>
196−
197− <Button
198− className={'mt-4'}
199− prop-size={'xl wide'}
200− disabled={Nothing.hasInstance(wallet)}
201− onClick={() => history.popAndPush(ROUTE_NAMES.POINTS)}>
202− {'Continue →'}
203− </Button>
204− </View>
205− );
206− }
207−}
208−
209−export default compose(
210− withHistory,
211− withWallet
212−)(Shards);
src/views/Ticket.jsdeleted−154
@@ -1,154 +0,0 @@
1−import { Just, Nothing } from 'folktale/maybe';
2−import React from 'react';
3−import { Button } from '../components/old/Base';
4−import {
5− InnerLabel,
6− ValidatedSigil,
7− PointInput,
8− TicketInput,
9− Input,
10− InputCaption,
11−} from '../components/old/Base';
12−import { H1, P } from '../components/old/Base';
13−
14−import { randomPatq, compose } from '../lib/lib';
15−import { ROUTE_NAMES } from '../lib/routeNames';
16−import { withHistory } from '../store/history';
17−import { urbitWalletFromTicket } from '../lib/wallet';
18−import { withWallet } from '../store/wallet';
19−import View from 'components/View';
20−
21−class Ticket extends React.Component {
22− constructor(props) {
23− super(props);
24−
25− this.state = {
26− ticket: '',
27− pointName: '',
28− passphrase: '',
29− isUnlocking: false,
30− };
31−
32− this.pointPlaceholder = randomPatq(4);
33− this.ticketPlaceholder = randomPatq(8);
34−
35− this.handleTicketInput = this.handleTicketInput.bind(this);
36− this.handlePassphraseInput = this.handlePassphraseInput.bind(this);
37− this.handlePointNameInput = this.handlePointNameInput.bind(this);
38− }
39−
40− handleTicketInput(ticket) {
41− this.setState({ ticket });
42− }
43−
44− handlePassphraseInput(passphrase) {
45− this.setState({ passphrase });
46− }
47−
48− handlePointNameInput(pointName) {
49− if (pointName.length < 15) {
50− this.setState({ pointName });
51− }
52− }
53−
54− async walletFromTicket(ticket, pointName, passphrase) {
55− this.setState({
56− isUnlocking: true,
57− });
58−
59− const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
60− this.props.setUrbitWallet(Just(uhdw));
61−
62− this.setState({
63− isUnlocking: false,
64− });
65− }
66−
67− render() {
68− const { history, wallet } = this.props;
69− const { ticket, pointName, passphrase } = this.state;
70−
71− const phPoint = this.pointPlaceholder;
72− const phTick = this.ticketPlaceholder;
73−
74− return (
75− <View>
76− <H1>{'Authenticate'}</H1>
77−
78− <P>
79− {`Please enter your point and Urbit master ticket here. This information is written on your Urbit HD paper wallets.`}
80− </P>
81−
82− <PointInput
83− className="mono mt-8"
84− prop-size="lg"
85− prop-format="innerLabel"
86− type="text"
87− autoFocus
88− placeholder={`e.g. ${phPoint}`}
89− value={pointName}
90− onChange={this.handlePointNameInput}>
91− <InnerLabel>{'Point'}</InnerLabel>
92− <ValidatedSigil
93− className={'tr-0 mt-05 mr-0 abs'}
94− patp={pointName}
95− size={68}
96− margin={8}
97− />
98− </PointInput>
99−
100− <TicketInput
101− className="mono mt-8"
102− prop-size="lg"
103− prop-format="innerLabel"
104− type="password"
105− name="ticket"
106− placeholder={`e.g. ${phTick}`}
107− value={ticket}
108− onChange={this.handleTicketInput}>
109− <InnerLabel>{'Ticket'}</InnerLabel>
110− </TicketInput>
111−
112− <InputCaption>
113− {'If your wallet requires a passphrase, you may enter it below.'}
114− </InputCaption>
115−
116− <Input
117− className="pt-8"
118− prop-size="md"
119− prop-format="innerLabel"
120− name="passphrase"
121− type="password"
122− value={passphrase}
123− autocomplete="off"
124− onChange={this.handlePassphraseInput}>
125− <InnerLabel>{'Passphrase'}</InnerLabel>
126− </Input>
127−
128− <Button
129− className={'mt-8'}
130− prop-size={'lg wide'}
131− disabled={this.state.isUnlocking || Just.hasInstance(wallet)}
132− onClick={() => this.walletFromTicket(ticket, pointName, passphrase)}>
133− <span className="relative">
134− {this.state.isUnlocking && <span className="btn-spinner" />}
135− {'Unlock Wallet →'}
136− </span>
137− </Button>
138−
139− <Button
140− className={'mt-4'}
141− prop-size={'xl wide'}
142− disabled={Nothing.hasInstance(wallet)}
143− onClick={() => history.popAndPush(ROUTE_NAMES.POINTS)}>
144− {'Continue →'}
145− </Button>
146− </View>
147− );
148− }
149−}
150−
151−export default compose(
152− withHistory,
153− withWallet
154−)(Ticket);
src/views/Trezor.jsdeleted−146
@@ -1,146 +0,0 @@
1−import * as bip32 from 'bip32';
2−import React from 'react';
3−import Maybe from 'folktale/maybe';
4−import { Button } from '../components/old/Base';
5−import { H1, P } from '../components/old/Base';
6−import { InnerLabel, Input, InnerLabelDropdown } from '../components/old/Base';
7−import TrezorConnect from 'trezor-connect';
8−import * as secp256k1 from 'secp256k1';
9−
10−import { TREZOR_PATH } from '../lib/trezor';
11−import { ROUTE_NAMES } from '../lib/routeNames';
12−import { withHistory } from '../store/history';
13−import { compose } from '../lib/lib';
14−import { withWallet } from '../store/wallet';
15−import View from 'components/View';
16−
17−class Trezor extends React.Component {
18− constructor(props) {
19− super(props);
20−
21− this.state = {
22− hdpath: TREZOR_PATH.replace(/x/g, 0),
23− account: 0,
24− };
25−
26− this.handleAccountSelection = this.handleAccountSelection.bind(this);
27− this.handleHdPathInput = this.handleHdPathInput.bind(this);
28− this.pollDevice = this.pollDevice.bind(this);
29− }
30−
31− handleAccountSelection(account) {
32− let hdpath = this.state.hdpath;
33− if (account !== 'custom') {
34− hdpath = TREZOR_PATH.replace(/x/g, account);
35− }
36− this.setState({ account, hdpath });
37− }
38−
39− handleHdPathInput(hdpath) {
40− this.setState({ hdpath });
41− }
42−
43− async pollDevice() {
44− const { setWallet, setWalletHdPath } = this.props;
45− const { hdpath } = this.state;
46−
47− TrezorConnect.manifest({
48− email: 'bridge-trezor@urbit.org',
49− appUrl: 'https://github.com/urbit/bridge',
50− });
51− TrezorConnect.getPublicKey({ path: hdpath }).then(info => {
52− if (info.success === true) {
53− const payload = info.payload;
54− const publicKey = Buffer.from(payload.publicKey, 'hex');
55− const chainCode = Buffer.from(payload.chainCode, 'hex');
56− const pub = secp256k1.publicKeyConvert(publicKey, true);
57− const hd = bip32.fromPublicKey(pub, chainCode);
58− setWallet(Maybe.Just(hd));
59− setWalletHdPath(hdpath);
60− } else {
61− setWallet(Maybe.Nothing());
62− }
63− });
64− }
65−
66− render() {
67− const { history, wallet } = this.props;
68− const { hdpath, account } = this.state;
69− const { handleAccountSelection } = this;
70−
71− let accountOptions = [
72− {
73− title: 'Custom path',
74− value: 'custom',
75− },
76− ];
77− for (let i = 0; i < 20; i++) {
78− accountOptions.push({
79− title: 'Account #' + (i + 1),
80− value: i,
81− });
82− }
83− let accountTitle = accountOptions.find(o => o.value === account).title;
84−
85− const accountSelection = (
86− <InnerLabelDropdown
87− className="mt-8"
88− prop-size="md"
89− prop-format="innerLabel"
90− options={accountOptions}
91− handleUpdate={handleAccountSelection}
92− title={'Account'}
93− currentSelectionTitle={accountTitle}
94− fullWidth={true}
95− />
96− );
97−
98− const pathSelection =
99− account !== 'custom' ? null : (
100− <Input
101− className="mt-8 pt-8 text-mono"
102− prop-size="md"
103− prop-format="innerLabel"
104− name="hdpath"
105− value={hdpath}
106− autocomplete="off"
107− onChange={this.handleHdPathInput}>
108− <InnerLabel>{'HD Path'}</InnerLabel>
109− </Input>
110− );
111−
112− return (
113− <View>
114− <H1>{'Authenticate With Your Trezor'}</H1>
115−
116− <P>
117− {`Connect and authenticate to your Trezor. If you'd like
118− to use a custom derivation path, you may enter it below.`}
119− </P>
120−
121− {accountSelection}
122− {pathSelection}
123−
124− <Button
125− className={'mt-8'}
126− prop-size={'wide lg'}
127− onClick={this.pollDevice}>
128− {'Authenticate →'}
129− </Button>
130−
131− <Button
132− className={'mt-8'}
133− prop-size={'wide lg'}
134− disabled={Maybe.Nothing.hasInstance(wallet)}
135− onClick={() => history.popAndPush(ROUTE_NAMES.POINTS)}>
136− {'Continue →'}
137− </Button>
138− </View>
139− );
140− }
141−}
142−
143−export default compose(
144− withHistory,
145− withWallet
146−)(Trezor);
src/views/ViewPoint.js+10−20
@@ -1,6 +1,7 @@
1−import React, { useCallback, useState } from 'react';
1+import React, { useCallback } from 'react';
2import Maybe from 'folktale/maybe';
2import Maybe from 'folktale/maybe';
3import * as ob from 'urbit-ob';
3import * as ob from 'urbit-ob';
4+import { Input } from 'indigo-react';
4
5
5import { H1, P } from '../components/old/Base';
6import { H1, P } from '../components/old/Base';
6
7
@@ -11,15 +12,17 @@ import { ROUTE_NAMES } from 'lib/routeNames';
11
12
12import View from 'components/View';
13import View from 'components/View';
13import { ForwardButton } from 'components/Buttons';
14import { ForwardButton } from 'components/Buttons';
14−import { PointInput } from 'components/Inputs';
15+import { usePointInput } from 'components/Inputs';
15
16
16export default function ViewPoint() {
17export default function ViewPoint() {
17 const history = useHistory();
18 const history = useHistory();
18 const { setPointCursor } = usePointCursor();
19 const { setPointCursor } = usePointCursor();
19− const [pointName, setPointName] = useState('');
20+ const pointInput = usePointInput({
20− const [pass, setPass] = useState(false);
21+ name: 'point',
21− const [error, setError] = useState();
22+ label: 'Point Name',
22− const [focused, setFocused] = useState(true);
23+ autoFocus: true,
24+ });
25+ const { data: pointName, pass } = pointInput;
23
26
24 const disabled = !pass;
27 const disabled = !pass;
25 const goForward = useCallback(() => {
28 const goForward = useCallback(() => {
@@ -34,20 +37,7 @@ export default function ViewPoint() {
34
37
35 <P>Enter a point name to view its public information.</P>
38 <P>Enter a point name to view its public information.</P>
36
39
37− <PointInput
38− name="point"
39− label="Point Name"
40− initialValue={pointName}
41− onValue={setPointName}
42− pass={pass}
43− onPass={setPass}
44− error={error}
45− onError={setError}
46− focused={focused}
47− onFocus={setFocused}
48− onEnter={goForward}
40+ <Input {...pointInput} onEnter={goForward} />
49− autoFocus
50− />
51
41
52 <ForwardButton
42 <ForwardButton
53 className="mt3"
43 className="mt3"
src/views/Wallet.jsdeleted−101
@@ -1,101 +0,0 @@
1−import React from 'react';
2−import Maybe from 'folktale/maybe';
3−
4−import { H1, P } from '../components/old/Base';
5−import { InnerLabelDropdown } from '../components/old/Base';
6−
7−import View from 'components/View';
8−import { ForwardButton } from 'components/Buttons';
9−
10−import { useHistory } from '../store/history';
11−import { useWallet } from '../store/wallet';
12−
13−import { ROUTE_NAMES } from '../lib/routeNames';
14−import { WALLET_TYPES, renderWalletType } from '../lib/wallet';
15−import useLifecycle from 'lib/useLifecycle';
16−
17−const kWalletOptions = [
18− {
19− title: 'Urbit Master Ticket',
20− value: WALLET_TYPES.TICKET,
21− },
22− {
23− title: 'Urbit Master Ticket (Shards)',
24− value: WALLET_TYPES.SHARDS,
25− },
26− {
27− type: 'divider',
28− },
29− {
30− title: 'Ledger',
31− value: WALLET_TYPES.LEDGER,
32− },
33− {
34− title: 'Trezor',
35− value: WALLET_TYPES.TREZOR,
36− },
37− {
38− type: 'divider',
39− },
40− {
41− title: 'BIP39 Mnemonic',
42− value: WALLET_TYPES.MNEMONIC,
43− },
44− {
45− title: 'Ethereum Private Key',
46− value: WALLET_TYPES.PRIVATE_KEY,
47− },
48− {
49− title: 'Ethereum Keystore File',
50− value: WALLET_TYPES.KEYSTORE,
51− },
52−];
53−
54−export default function Wallet() {
55− const history = useHistory();
56− const { setWallet, walletType, setWalletType } = useWallet();
57−
58− // reset wallet when this component is mounted
59− useLifecycle(() => setWallet(Maybe.Nothing()));
60−
61− return (
62− <View>
63− <H1>Unlock a Wallet</H1>
64−
65− <P>
66− To manage your assets, you need to unlock a wallet. Please select how
67− you would like to access your wallet.
68− </P>
69−
70− <InnerLabelDropdown
71− title="Wallet Type:"
72− options={kWalletOptions}
73− handleUpdate={setWalletType}
74− currentSelectionTitle={renderWalletType(walletType)}
75− />
76−
77− <ForwardButton
78− onClick={() =>
79− history.push(
80− walletType === WALLET_TYPES.MNEMONIC
81− ? ROUTE_NAMES.MNEMONIC
82− : walletType === WALLET_TYPES.TICKET
83− ? ROUTE_NAMES.TICKET
84− : walletType === WALLET_TYPES.SHARDS
85− ? ROUTE_NAMES.SHARDS
86− : walletType === WALLET_TYPES.LEDGER
87− ? ROUTE_NAMES.LEDGER
88− : walletType === WALLET_TYPES.TREZOR
89− ? ROUTE_NAMES.TREZOR
90− : walletType === WALLET_TYPES.PRIVATE_KEY
91− ? ROUTE_NAMES.PRIVATE_KEY
92− : walletType === WALLET_TYPES.KEYSTORE
93− ? ROUTE_NAMES.KEYSTORE
94− : ROUTE_NAMES.DEFAULT
95− )
96− }>
97− Continue
98− </ForwardButton>
99− </View>
100− );
101−}