on master help sign in
Merge pull request #178 from urbit/feat/invite-flow-ui
93858a4Matt Condon
config-overrides.jsnew file+6
@@ -0,0 +1,6 @@
1+const rewireReactHotLoader = require('react-app-rewire-hot-loader');
2+
3+module.exports = function override(config, env) {
4+ config = rewireReactHotLoader(config, env);
5+ return config;
6+};
package-lock.json+56
@@ -8017,6 +8017,14 @@
8017 "minimalistic-crypto-utils": "^1.0.1"
8017 "minimalistic-crypto-utils": "^1.0.1"
8018 }
8018 }
8019 },
8019 },
8020+ "hoist-non-react-statics": {
8021+ "version": "3.3.0",
8022+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz",
8023+ "integrity": "sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==",
8024+ "requires": {
8025+ "react-is": "^16.7.0"
8026+ }
8027+ },
8020 "home-or-tmp": {
8028 "home-or-tmp": {
8021 "version": "2.0.0",
8029 "version": "2.0.0",
8022 "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz",
8030 "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz",
@@ -14253,6 +14261,21 @@
14253 }
14261 }
14254 }
14262 }
14255 },
14263 },
14264+ "react-app-rewire-hot-loader": {
14265+ "version": "2.0.1",
14266+ "resolved": "https://registry.npmjs.org/react-app-rewire-hot-loader/-/react-app-rewire-hot-loader-2.0.1.tgz",
14267+ "integrity": "sha512-XTUPOv5q0zJeEtXECfilDPfqCiktSVbIyupiqigIHOO8EMZec2YfNpwZzqDWMu3yfaEHRs54qRCcCgRR/1w3EQ=="
14268+ },
14269+ "react-app-rewired": {
14270+ "version": "2.1.3",
14271+ "resolved": "https://registry.npmjs.org/react-app-rewired/-/react-app-rewired-2.1.3.tgz",
14272+ "integrity": "sha512-NXC2EsQrnEMV7xD70rHcBq0B4PSEzjY/K2m/e+GRgit2jZO/uZApnpCZSKvIX2leLRN69Sqf2id0VXZ1F62CDw==",
14273+ "requires": {
14274+ "cross-spawn": "^6.0.5",
14275+ "dotenv": "^6.2.0",
14276+ "semver": "^5.6.0"
14277+ }
14278+ },
14256 "react-dev-utils": {
14279 "react-dev-utils": {
14257 "version": "9.0.1",
14280 "version": "9.0.1",
14258 "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-9.0.1.tgz",
14281 "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-9.0.1.tgz",
@@ -14416,11 +14439,39 @@
14416 "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-5.1.6.tgz",
14439 "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-5.1.6.tgz",
14417 "integrity": "sha512-X1Y+0jR47ImDVr54Ab6V9eGk0Hnu7fVWGeHQSOXHf/C2pF9c6uy3gef8QUeuUiWlNb0i08InPSE5a/KJzNzw1Q=="
14440 "integrity": "sha512-X1Y+0jR47ImDVr54Ab6V9eGk0Hnu7fVWGeHQSOXHf/C2pF9c6uy3gef8QUeuUiWlNb0i08InPSE5a/KJzNzw1Q=="
14418 },
14441 },
14442+ "react-hot-loader": {
14443+ "version": "4.12.0",
14444+ "resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.12.0.tgz",
14445+ "integrity": "sha512-N+8ct1euiQnwqqDyX+SrxsgZ13ax4e8JiHbSAPf7xAshPxF3iTqVJQUxOLH90RXOFaT8LLynq0VPz3rCK7AW1A==",
14446+ "requires": {
14447+ "fast-levenshtein": "^2.0.6",
14448+ "global": "^4.3.0",
14449+ "hoist-non-react-statics": "^3.3.0",
14450+ "loader-utils": "^1.1.0",
14451+ "lodash": "^4.17.11",
14452+ "prop-types": "^15.6.1",
14453+ "react-lifecycles-compat": "^3.0.4",
14454+ "shallowequal": "^1.0.2",
14455+ "source-map": "^0.7.3"
14456+ },
14457+ "dependencies": {
14458+ "source-map": {
14459+ "version": "0.7.3",
14460+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
14461+ "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="
14462+ }
14463+ }
14464+ },
14419 "react-is": {
14465 "react-is": {
14420 "version": "16.8.6",
14466 "version": "16.8.6",
14421 "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
14467 "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
14422 "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA=="
14468 "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA=="
14423 },
14469 },
14470+ "react-lifecycles-compat": {
14471+ "version": "3.0.4",
14472+ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
14473+ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
14474+ },
14424 "react-scripts": {
14475 "react-scripts": {
14425 "version": "3.0.1",
14476 "version": "3.0.1",
14426 "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.0.1.tgz",
14477 "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.0.1.tgz",
@@ -15545,6 +15596,11 @@
15545 }
15596 }
15546 }
15597 }
15547 },
15598 },
15599+ "shallowequal": {
15600+ "version": "1.1.0",
15601+ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
15602+ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
15603+ },
15548 "shebang-command": {
15604 "shebang-command": {
15549 "version": "1.2.0",
15605 "version": "1.2.0",
15550 "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
15606 "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
package.json+7−4
@@ -34,7 +34,10 @@
34 "more-entropy": "^0.0.7",
34 "more-entropy": "^0.0.7",
35 "prop-types": "^15.6.2",
35 "prop-types": "^15.6.2",
36 "react": "^16.8.6",
36 "react": "^16.8.6",
37+ "react-app-rewire-hot-loader": "^2.0.1",
38+ "react-app-rewired": "^2.1.3",
37 "react-dom": "^16.8.6",
39 "react-dom": "^16.8.6",
40+ "react-hot-loader": "^4.12.0",
38 "react-scripts": "3.0.1",
41 "react-scripts": "3.0.1",
39 "react-teleporter": "^1.1.0",
42 "react-teleporter": "^1.1.0",
40 "secp256k1": "^3.5.2",
43 "secp256k1": "^3.5.2",
@@ -59,10 +62,10 @@
59 "truffle": "4.1.11"
62 "truffle": "4.1.11"
60 },
63 },
61 "scripts": {
64 "scripts": {
62− "start": "react-scripts start",
65+ "start": "react-app-rewired start",
63− "build": "react-scripts build",
66+ "build": "react-app-rewired build",
64− "test": "react-scripts test --env=jsdom",
67+ "test": "react-app-rewired test --env=jsdom",
65− "eject": "react-scripts eject",
68+ "eject": "react-app-rewired eject",
66 "checksum": "cd build && find . -exec shasum -a256 {} ';' > ../checksums.txt && cd ..",
69 "checksum": "cd build && find . -exec shasum -a256 {} ';' > ../checksums.txt && cd ..",
67 "zip": "zip -ur bridge-$npm_package_version.zip README.md bridge-https.py checksums.txt && cd build && zip -ur ../bridge-$npm_package_version.zip ./* && cd ..",
70 "zip": "zip -ur bridge-$npm_package_version.zip README.md bridge-https.py checksums.txt && cd build && zip -ur ../bridge-$npm_package_version.zip ./* && cd ..",
68 "release": "npm install && npm run build && npm run checksum && npm run zip",
71 "release": "npm install && npm run build && npm run checksum && npm run zip",
src/Bridge.js+13−19
@@ -14,44 +14,38 @@ import { isDevelopment } from 'lib/flags';
14
14
15import 'style/index.scss';
15import 'style/index.scss';
16
16
17−const kInitialNetworkType = isDevelopment
17+const INITIAL_NETWORK_TYPE = isDevelopment
18 ? NETWORK_TYPES.LOCAL
18 ? NETWORK_TYPES.LOCAL
19 : NETWORK_TYPES.MAINNET;
19 : NETWORK_TYPES.MAINNET;
20
20
21// NB(shrugs): modify these variables to change the default local state.
21// NB(shrugs): modify these variables to change the default local state.
22−const shouldStubLocal = process.env.REACT_APP_STUB_LOCAL === 'true';
22+const SHOULD_STUB_LOCAL = process.env.REACT_APP_STUB_LOCAL === 'true';
23−const kIsStubbed = isDevelopment && shouldStubLocal;
23+const IS_STUBBED = isDevelopment && SHOULD_STUB_LOCAL;
24−const kInitialRoutes = kIsStubbed
24+const INITIAL_ROUTES = IS_STUBBED
25− ? [
26− { key: ROUTE_NAMES.LANDING },
25+ ? [{ key: ROUTE_NAMES.LANDING }, { key: ROUTE_NAMES.ACTIVATE }]
27− { key: ROUTE_NAMES.LOGIN },
28− { key: ROUTE_NAMES.POINTS },
29− { key: ROUTE_NAMES.POINT },
30− { key: ROUTE_NAMES.INVITE },
31− ]
32 : [{ key: ROUTE_NAMES.LANDING }];
26 : [{ key: ROUTE_NAMES.LANDING }];
33
27
34−const kInitialWallet = kIsStubbed
28+const INITIAL_WALLET = IS_STUBBED
35 ? walletFromMnemonic(
29 ? walletFromMnemonic(
36 process.env.REACT_APP_DEV_MNEMONIC,
30 process.env.REACT_APP_DEV_MNEMONIC,
37 process.env.REACT_APP_HD_PATH
31 process.env.REACT_APP_HD_PATH
38 )
32 )
39 : undefined;
33 : undefined;
40−const kInitialMnemonic = kIsStubbed
34+const INITIAL_MNEMONIC = IS_STUBBED
41 ? Just(process.env.REACT_APP_DEV_MNEMONIC)
35 ? Just(process.env.REACT_APP_DEV_MNEMONIC)
42 : Nothing();
36 : Nothing();
43−const kInitialPointCursor = kIsStubbed ? Just(65792) : Nothing();
37+const INITIAL_POINT_CURSOR = IS_STUBBED ? Just(65792) : Nothing();
44
38
45export default function Bridge() {
39export default function Bridge() {
46 return (
40 return (
47 <Provider
41 <Provider
48 views={ROUTES}
42 views={ROUTES}
49 names={ROUTE_NAMES}
43 names={ROUTE_NAMES}
50− initialRoutes={kInitialRoutes}
44+ initialRoutes={INITIAL_ROUTES}
51− initialNetworkType={kInitialNetworkType}
45+ initialNetworkType={INITIAL_NETWORK_TYPE}
52− initialWallet={kInitialWallet}
46+ initialWallet={INITIAL_WALLET}
53− initialMnemonic={kInitialMnemonic}
47+ initialMnemonic={INITIAL_MNEMONIC}
54− initialPointCursor={kInitialPointCursor}>
48+ initialPointCursor={INITIAL_POINT_CURSOR}>
55 <IndigoApp>
49 <IndigoApp>
56 <Router />
50 <Router />
57 </IndigoApp>
51 </IndigoApp>
src/components/AspectRatio.jsnew file+22
@@ -0,0 +1,22 @@
1+import React from 'react';
2+
3+/**
4+ * A very useful Flutter widget reimplemented with
5+ * the world's dumbest css hack.
6+ *
7+ * Simply: container div with appropriate relative padding,
8+ * then absolutely pin the child.
9+ *
10+ * via: https://www.w3schools.com/howto/howto_css_aspect_ratio.asp
11+ */
12+export default function AspectRatio({ aspectRatio = 1, children }) {
13+ const percent = `${(1 / aspectRatio) * 100}%`;
14+
15+ return (
16+ <div className="rel" style={{ paddingTop: percent }}>
17+ <div className="abs" style={{ top: 0, bottom: 0, left: 0, right: 0 }}>
18+ {children}
19+ </div>
20+ </div>
21+ );
22+}
src/components/Blinky.js+8−7
@@ -3,26 +3,27 @@ import React, { useState, useRef } from 'react';
3import useInterval from 'lib/useInterval';
3import useInterval from 'lib/useInterval';
4
4
5// TODO: make these characters display as the same width
5// TODO: make these characters display as the same width
6−export const kLoadingCharacter = '▓';
6+export const LOADING_CHARACTER = '▓';
7−export const kInterstitialCharacter = '░';
7+export const INTERSTITIAL_CHARACTER = '░';
8const BLINK_AFTER_MS = 2500; // ms
8const BLINK_AFTER_MS = 2500; // ms
9
9
10export const matchBlinky = obj =>
10export const matchBlinky = obj =>
11 obj.matchWith({
11 obj.matchWith({
12− Nothing: () => <Blinky />,
12+ Nothing: () => <Blinky delayed />,
13 Just: p => p.value,
13 Just: p => p.value,
14 });
14 });
15
15
16export default function Blinky({
16export default function Blinky({
17− a = kLoadingCharacter,
17+ a = LOADING_CHARACTER,
18− b = kInterstitialCharacter,
18+ b = INTERSTITIAL_CHARACTER,
19+ delayed = false,
19}) {
20}) {
20 const [value, setValue] = useState(true);
21 const [value, setValue] = useState(true);
21 const now = useRef(new Date());
22 const now = useRef(new Date());
22
23
23 useInterval(() => {
24 useInterval(() => {
24− // only start blinking if we've elapsed enough time
25+ // only start blinking if we've elapsed enough time and want to delay
25− if (new Date() - now.current > BLINK_AFTER_MS) {
26+ if (!delayed || new Date() - now.current > BLINK_AFTER_MS) {
26 setValue(val => !val);
27 setValue(val => !val);
27 }
28 }
28 }, 1000);
29 }, 1000);
src/components/Buttons.js+15
@@ -4,3 +4,18 @@ import { Button } from 'indigo-react';
4// TODO: use svg icons
4// TODO: use svg icons
5export const ForwardButton = props => <Button accessory="→" {...props} />;
5export const ForwardButton = props => <Button accessory="→" {...props} />;
6export const DownloadButton = props => <Button accessory="⬇" {...props} />;
6export const DownloadButton = props => <Button accessory="⬇" {...props} />;
7+export const OutButton = props => (
8+ <Button as="a" target="_blank" accessory="↗" {...props} />
9+);
10+
11+export const OfflineButton = props => (
12+ <OutButton {...props} href="https://github.com/urbit/bridge/releases">
13+ Offline
14+ </OutButton>
15+);
16+
17+export const RestartButton = props => (
18+ <Button accessory="↺" solid {...props}>
19+ Restart
20+ </Button>
21+);
src/components/FooterButton.js+2−2
@@ -3,13 +3,13 @@ import { Grid, Button } from 'indigo-react';
3
3
4import Footer from './Footer';
4import Footer from './Footer';
5
5
6−export default function FooterButton(props) {
6+export default function FooterButton({ as: As = Button, ...props }) {
7 return (
7 return (
8 <Footer>
8 <Footer>
9 <Grid className="pt2">
9 <Grid className="pt2">
10 <Grid.Divider />
10 <Grid.Divider />
11 <Grid.Item full>
11 <Grid.Item full>
12− <Button {...props} />
12+ <As {...props} />
13 </Grid.Item>
13 </Grid.Item>
14 </Grid>
14 </Grid>
15 </Footer>
15 </Footer>
src/components/InputSigil.js+3−3
@@ -1,8 +1,8 @@
1import React from 'react';
1import React from 'react';
2−import cn from 'classnames';
3import * as ob from 'urbit-ob';
2import * as ob from 'urbit-ob';
4
3
5import Sigil from './Sigil';
4import Sigil from './Sigil';
5+import AspectRatio from './AspectRatio';
6
6
7const selectColorway = (pass, fail, focused) => {
7const selectColorway = (pass, fail, focused) => {
8 if (pass) {
8 if (pass) {
@@ -20,7 +20,7 @@ const selectColorway = (pass, fail, focused) => {
20 return ['#FFFFFF', '#7F7F7F'];
20 return ['#FFFFFF', '#7F7F7F'];
21};
21};
22
22
23−export default function ValidatedSigil({
23+export default function InputSigil({
24 className,
24 className,
25 patp,
25 patp,
26 size,
26 size,
@@ -35,6 +35,6 @@ export default function ValidatedSigil({
35 return valid ? (
35 return valid ? (
36 <Sigil patp={patp} size={size} colorway={colorway} margin={0} {...rest} />
36 <Sigil patp={patp} size={size} colorway={colorway} margin={0} {...rest} />
37 ) : (
37 ) : (
38− <div className={cn('bg-transparent', className)} />
38+ <AspectRatio aspectRatio={1} />
39 );
39 );
40}
40}
src/components/Inputs.js+24−8
@@ -1,4 +1,4 @@
1−import React, { useState } from 'react';
1+import React, { useState, useMemo } from 'react';
2import * as bip39 from 'bip39';
2import * as bip39 from 'bip39';
3
3
4import InputSigil from 'components/InputSigil';
4import InputSigil from 'components/InputSigil';
@@ -13,6 +13,7 @@ import {
13} from 'lib/validators';
13} from 'lib/validators';
14import { prependSig } from 'lib/transformers';
14import { prependSig } from 'lib/transformers';
15import useForm from 'indigo-react/lib/useForm';
15import useForm from 'indigo-react/lib/useForm';
16+import { AccessoryIcon } from 'indigo-react';
16
17
17// const RequiredInput = advancedInput({
18// const RequiredInput = advancedInput({
18// WrappedComponent: Input,
19// WrappedComponent: Input,
@@ -87,35 +88,39 @@ export function useHdPathInput(props) {
87const kTicketValidators = [validateTicket, validateNotEmpty];
88const kTicketValidators = [validateTicket, validateNotEmpty];
88const kTicketTransformers = [prependSig];
89const kTicketTransformers = [prependSig];
89//TODO needs to be fancier, displaying sig and dashes instead of •ing all
90//TODO needs to be fancier, displaying sig and dashes instead of •ing all
90−export function useTicketInput({ initialValue = '~', ...rest }) {
91+export function useTicketInput({ validators = [], ...rest }) {
91− return firstOf(
92+ const input = firstOf(
92 useForm([
93 useForm([
93 {
94 {
94 type: 'password',
95 type: 'password',
95− placeholder: '~master-ticket',
96+ label: 'Ticket',
97+ placeholder: '~ragtyd-modwen-bostec-hinsep',
96− validators: kTicketValidators,
98+ validators: useMemo(() => [...validators, ...kTicketValidators], [
99+ validators,
100+ ]),
97 transformers: kTicketTransformers,
101 transformers: kTicketTransformers,
98 mono: true,
102 mono: true,
99− initialValue,
100 ...rest,
103 ...rest,
101 },
104 },
102 ])
105 ])
103 );
106 );
107+
108+ return { ...input, accessory: input.pass ? <AccessoryIcon.Success /> : null };
104}
109}
105
110
106const kPointValidators = [validatePoint, validateNotEmpty];
111const kPointValidators = [validatePoint, validateNotEmpty];
107const kPointTransformers = [prependSig];
112const kPointTransformers = [prependSig];
108−export function usePointInput({ initialValue = '~', ...rest }) {
113+export function usePointInput(rest) {
109 const [lastValidPoint, setLastValidPoint] = useState('');
114 const [lastValidPoint, setLastValidPoint] = useState('');
110 const input = firstOf(
115 const input = firstOf(
111 useForm([
116 useForm([
112 {
117 {
113 type: 'text',
118 type: 'text',
119+ label: 'Point',
114 placeholder: 'e.g. ~zod',
120 placeholder: 'e.g. ~zod',
115 validators: kPointValidators,
121 validators: kPointValidators,
116 transformers: kPointTransformers,
122 transformers: kPointTransformers,
117 mono: true,
123 mono: true,
118− initialValue,
119 onValue: setLastValidPoint,
124 onValue: setLastValidPoint,
120 ...rest,
125 ...rest,
121 },
126 },
@@ -139,6 +144,17 @@ export function usePointInput({ initialValue = '~', ...rest }) {
139 };
144 };
140}
145}
141
146
147+export function useCheckboxInput({ initialValue, ...rest }) {
148+ return firstOf(
149+ useForm([
150+ {
151+ type: 'checkbox',
152+ ...rest,
153+ },
154+ ])
155+ );
156+}
157+
142const kEmailValidators = [validateEmail, validateNotEmpty];
158const kEmailValidators = [validateEmail, validateNotEmpty];
143export const buildEmailInputConfig = extra => ({
159export const buildEmailInputConfig = extra => ({
144 type: 'email',
160 type: 'email',
src/components/LoadingBar.jsnew file+19
@@ -0,0 +1,19 @@
1+import React from 'react';
2+import cn from 'classnames';
3+
4+// progrss is [0, 1]
5+export default function LoadingBar({ className, progress = 1 }) {
6+ return (
7+ <div className={cn('rel bg-gray2', className)} style={{ height: '4px' }}>
8+ <div
9+ className="abs bg-green3"
10+ style={{
11+ top: 0,
12+ bottom: 0,
13+ left: 0,
14+ width: `${progress * 100.0}%`,
15+ }}
16+ />
17+ </div>
18+ );
19+}
src/components/MaybeSigil.jsnew file+21
@@ -0,0 +1,21 @@
1+import React from 'react';
2+import * as ob from 'urbit-ob';
3+
4+import Sigil from './Sigil';
5+import AspectRatio from './AspectRatio';
6+
7+/**
8+ * patp is Maybe<string>
9+ */
10+export default function MaybeSigil({ className, patp, size, ...rest }) {
11+ const validPatp = patp.matchWith({
12+ Nothing: () => null,
13+ Just: p => (ob.isValidPatp(p.value) ? p.value : null),
14+ });
15+
16+ return validPatp ? (
17+ <Sigil patp={validPatp} size={size} margin={0} {...rest} />
18+ ) : (
19+ <AspectRatio aspectRatio={1} />
20+ );
21+}
src/components/Passport.js+77−15
@@ -1,39 +1,101 @@
1import React from 'react';
1import React from 'react';
2+import cn from 'classnames';
3+import Maybe from 'folktale/maybe';
2import * as ob from 'urbit-ob';
4import * as ob from 'urbit-ob';
3−import { Grid, Flex } from 'indigo-react';
5+import { Grid, Flex, Text } from 'indigo-react';
4import { times } from 'lodash';
6import { times } from 'lodash';
5
7
6import usePointBirthday from 'lib/usePointBirthday';
8import usePointBirthday from 'lib/usePointBirthday';
7import { formatDots } from 'lib/dateFormat';
9import { formatDots } from 'lib/dateFormat';
8
10
9−import Sigil from './Sigil';
11+import Blinky, { LOADING_CHARACTER, INTERSTITIAL_CHARACTER } from './Blinky';
10−import Blinky, { kLoadingCharacter, kInterstitialCharacter } from './Blinky';
12+import MaybeSigil from './MaybeSigil';
11
13
12const buildDate = char =>
14const buildDate = char =>
13 [4, 2, 2].map(t => times(t, () => char).join('')).join('.');
15 [4, 2, 2].map(t => times(t, () => char).join('')).join('.');
14−const kDateA = buildDate(kLoadingCharacter);
16+const DATE_A = buildDate(LOADING_CHARACTER);
15−const kDateB = buildDate(kInterstitialCharacter);
17+const DATE_B = buildDate(INTERSTITIAL_CHARACTER);
16
18
19+const TICKET_CHAR_LENGTH = 30;
20+const TICKET = times(TICKET_CHAR_LENGTH, i =>
21+ i % 2 === 0 ? LOADING_CHARACTER : INTERSTITIAL_CHARACTER
22+).join('');
23+const PENDING_TICKET = TICKET;
24+const VALID_TICKET = `${TICKET} ◆`;
25+
26+const VISIBLE_ADDRESS_CHAR_COUNT = 20;
27+const EMPTY_ADDRESS = times(VISIBLE_ADDRESS_CHAR_COUNT, () => '<').join('');
28+const ADDRESS_BUFFER_CHARS = times(40, () => '<').join('');
29+
30+/**
31+ * point is Maybe<number>
32+ * ticket is null | boolean
33+ * address is null | Maybe<string>
34+ */
17−export default function Passport({ point }) {
35+export default function Passport({
36+ point,
37+ className,
38+ ticket = null,
39+ address = Maybe.Nothing(),
40+ mini = false,
41+}) {
18− const birthday = usePointBirthday(point);
42+ const birthday = usePointBirthday(point.getOrElse(null));
19− const birthDate = birthday.matchWith({
43+ const visualBirthday = birthday.matchWith({
20− Nothing: () => <Blinky a={kDateA} b={kDateB} />,
44+ Nothing: () => <Blinky a={DATE_A} b={DATE_B} delayed />,
21 Just: p => formatDots(p.value),
45 Just: p => formatDots(p.value),
22 });
46 });
47+
23− const name = ob.patp(point);
48+ const name = point.matchWith({
49+ Nothing: () => Maybe.Nothing(),
50+ Just: p => Maybe.Just(ob.patp(p.value)),
51+ });
52+ const visualName = name.matchWith({
53+ Nothing: () => '...',
54+ Just: p => p.value,
55+ });
56+
57+ const showTicket = ticket !== null;
58+ const isValidTiket = !!ticket;
59+ const visibleTicket = isValidTiket ? VALID_TICKET : PENDING_TICKET;
60+ const visibleAddress = `${address.matchWith({
61+ Nothing: () => EMPTY_ADDRESS,
62+ Just: p => p.value.slice(0, VISIBLE_ADDRESS_CHAR_COUNT),
63+ })}${ADDRESS_BUFFER_CHARS}`;
24
64
25 return (
65 return (
26− <Grid gap={16} className="bg-black r8 p7">
66+ <Grid gap={4} className={cn('bg-black r8 p7', className)}>
27 <Grid.Item cols={[1, 4]} className="bg-green">
67 <Grid.Item cols={[1, 4]} className="bg-green">
28− <Sigil patp={name} size={64} margin={0} />
68+ <MaybeSigil patp={name} size={64} margin={0} />
29 </Grid.Item>
69 </Grid.Item>
30 <Grid.Item as={Flex} cols={[4, 13]} col justify="between">
70 <Grid.Item as={Flex} cols={[4, 13]} col justify="between">
31− <Flex.Item className="mono white f5">{name}</Flex.Item>
71+ <Flex.Item className="mono white f5">{visualName}</Flex.Item>
32 <Flex.Item as={Flex} col>
72 <Flex.Item as={Flex} col>
33− <Flex.Item className="mono gray4 f6 uppercase">Birth Time</Flex.Item>
73+ <Flex.Item as={Text} className="mono f6 f5-ns gray4 uppercase">
74+ Birth Time
75+ </Flex.Item>
34− <Flex.Item className="mono gray4 f6">{birthDate}</Flex.Item>
76+ <Flex.Item as={Text} className="mono f6 f5-ns gray4 ">
77+ {visualBirthday}
78+ </Flex.Item>
35 </Flex.Item>
79 </Flex.Item>
36 </Grid.Item>
80 </Grid.Item>
81+ {!mini && showTicket && (
82+ <Grid.Item full className="mv3" as={Flex} col>
83+ <Flex.Item as={Text} className="mono f6 f5-ns gray4 uppercase">
84+ Master Ticket
85+ </Flex.Item>
86+ <Flex.Item
87+ as={Text}
88+ className={cn('mono f6 f5-ns wrap', {
89+ gray4: !isValidTiket,
90+ green3: isValidTiket,
91+ })}>
92+ {visibleTicket}
93+ </Flex.Item>
94+ <Flex.Item as={Text} className="mono f6 f5-ns gray4 wrap">
95+ {visibleAddress}
96+ </Flex.Item>
97+ </Grid.Item>
98+ )}
37 </Grid>
99 </Grid>
38 );
100 );
39}
101}
src/components/Sigil.js+12−7
@@ -3,11 +3,16 @@ import { pour } from 'sigil-js';
3import ReactSVGComponents from './ReactSVGComponents';
3import ReactSVGComponents from './ReactSVGComponents';
4
4
5export default function Sigil({ patp, size, colorway, ...rest }) {
5export default function Sigil({ patp, size, colorway, ...rest }) {
6+ try {
7+ // https://github.com/urbit/sigil-js/issues/31
6− return pour({
8+ return pour({
7− renderer: ReactSVGComponents,
9+ renderer: ReactSVGComponents,
8− patp,
10+ patp,
9− size,
11+ size,
10− colorway,
12+ colorway,
11− ...rest,
13+ ...rest,
12− });
14+ });
15+ } catch {
16+ return null;
17+ }
13}
18}
src/components/Steps.jsnew file+13
@@ -0,0 +1,13 @@
1+import React from 'react';
2+import cn from 'classnames';
3+import { Text } from 'indigo-react';
4+
5+export default function Steps({ num = 1, total = 3, className }) {
6+ // NOTE: the 48px must match the `mt8` class on Passport for things to line
7+ // up nicely
8+ return (
9+ <Text className={cn('f5 gray3', className)} style={{ height: '48px' }}>
10+ <Text className="f5 black">Step {num}</Text> of {total}
11+ </Text>
12+ );
13+}
src/components/WarningBox.jsnew file+11
@@ -0,0 +1,11 @@
1+import React from 'react';
2+import cn from 'classnames';
3+import { Flex, Text } from 'indigo-react';
4+
5+export default function WarningBox({ className, children }) {
6+ return (
7+ <Flex className={cn('bg-red1 pv3 ph4', className)} align="center">
8+ <Text className="f6 fw-bold red3">{children}</Text>
9+ </Flex>
10+ );
11+}
src/components/old/Header.jsdeleted−62
@@ -1,62 +0,0 @@
1−import React from 'react';
2−import { Button } from './Base';
3−
4−import { getRouteBreadcrumb } from '../../lib/router';
5−import { ROUTE_NAMES } from '../../lib/routeNames';
6−import { useHistory } from '../../store/history';
7−import { isLast } from '../../lib/lib';
8−import { usePointCursor } from '../../store/pointCursor';
9−
10−// hook to create a breadcrumb builder function based on the current
11−// global states
12−function useRouteBreadcrumbBuilder() {
13− const { pointCursor } = usePointCursor();
14−
15− return getRouteBreadcrumb({ pointCursor });
16−}
17−
18−function Crumbs(props) {
19− const history = useHistory();
20− const breadcrumbBuilder = useRouteBreadcrumbBuilder();
21−
22− return (
23− <>
24− {history.routes.map((route, idx) => {
25− return (
26− <div key={`history-${idx}`} className={'flex items-center'}>
27− <Button
28− prop-type={'link'}
29− prop-size={'sm'}
30− onClick={() => history.pop(history.size - idx - 1)}>
31− {breadcrumbBuilder(route)}
32− </Button>
33−
34− {isLast(history.size, idx) ? <div /> : '/'}
35− </div>
36− );
37− })}
38− </>
39− );
40−}
41−
42−function Header(props) {
43− const history = useHistory();
44−
45− const showCrumbs = !history.includes(ROUTE_NAMES.INVITE_TICKET);
46−
47− if (showCrumbs) {
48− return (
49− <div className={'flex items-center h-10'}>
50− <Crumbs />
51− </div>
52− );
53− }
54−
55− return (
56− <h2 className="mt-9 clickable" onClick={() => history.pop()}>
57− Bridge
58− </h2>
59− );
60−}
61−
62−export default Header;
src/index.js+18−1
@@ -1,6 +1,23 @@
1import React from 'react';
1import React from 'react';
2import ReactDOM from 'react-dom';
2import ReactDOM from 'react-dom';
3+import { AppContainer } from 'react-hot-loader';
3
4
4import Bridge from './Bridge';
5import Bridge from './Bridge';
5
6
7+const render = () =>
8+ ReactDOM.render(
9+ <AppContainer>
10+ <Bridge />
11+ </AppContainer>,
6−ReactDOM.render(<Bridge />, document.getElementById('root'));
12+ document.getElementById('root')
13+ );
14+
15+// Render once
16+render();
17+
18+// Webpack Hot Module Replacement API
19+if (module.hot) {
20+ module.hot.accept('./Bridge', () => {
21+ render();
22+ });
23+}
src/indigo-react/components/AccessoryIcon.js+1−1
@@ -3,7 +3,7 @@ import React from 'react';
3import Flex from './Flex';
3import Flex from './Flex';
4
4
5const PENDING_ACCESSORY = '⋯';
5const PENDING_ACCESSORY = '⋯';
6−const SUCCESS_ACCESSORY = '✔';
6+const SUCCESS_ACCESSORY = '✓';
7const FAILURE_ACCESSORY = '×';
7const FAILURE_ACCESSORY = '×';
8
8
9function AccessoryIcon({ ...props }) {
9function AccessoryIcon({ ...props }) {
src/indigo-react/components/Button.js+1−1
@@ -24,7 +24,7 @@ export default function Button({
24 return (
24 return (
25 <Grid
25 <Grid
26 as="a"
26 as="a"
27− gap={4}
27+ gap={1}
28 className={cn(
28 className={cn(
29 'pointer pv4 truncate flex-row justify-between',
29 'pointer pv4 truncate flex-row justify-between',
30 {
30 {
src/indigo-react/components/CheckboxInput.jsnew file+82
@@ -0,0 +1,82 @@
1+import React from 'react';
2+import cn from 'classnames';
3+
4+import Flex from './Flex';
5+
6+// TODO: make this look like screens
7+export default function CheckboxInput({
8+ // visuals
9+ name,
10+ label,
11+ className,
12+
13+ // callbacks
14+ onValue,
15+ onPass,
16+ onError,
17+ onFocus,
18+ onEnter,
19+
20+ // state from hook
21+ focused,
22+ pass,
23+ visiblyPassed,
24+ error,
25+ data,
26+ bind,
27+ autoFocus,
28+ disabled,
29+
30+ // ignored
31+ initialValue,
32+ validators,
33+ transformers,
34+
35+ // extra
36+ ...rest
37+}) {
38+ return (
39+ <Flex
40+ className={className}
41+ row
42+ align="center"
43+ style={{
44+ ...(disabled && {
45+ pointerEvents: 'none',
46+ cursor: 'not-allowed',
47+ }),
48+ }}>
49+ <Flex.Item
50+ as="input"
51+ {...rest}
52+ className={cn(
53+ 'b b1 p3 outline-none',
54+ {
55+ 'bg-white': !disabled,
56+ 'bg-gray1': disabled,
57+ },
58+ {
59+ gray4: !focused,
60+ black: focused,
61+ },
62+ {
63+ 'b-green3': visiblyPassed,
64+ 'b-black': focused && !visiblyPassed,
65+ 'b-yellow3': !focused && error,
66+ 'b-gray3': !focused && !error && !visiblyPassed,
67+ }
68+ )}
69+ id={name}
70+ name={name}
71+ {...bind}
72+ />
73+ <Flex.Item
74+ as="label"
75+ className="f6 ph3 lh-tall us-none"
76+ htmlFor={name}
77+ flex>
78+ {label}
79+ </Flex.Item>
80+ </Flex>
81+ );
82+}
src/indigo-react/components/Flex.js+4−9
@@ -1,10 +1,8 @@
1import React from 'react';
1import React from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3
3
4−import getComponent from '../lib/getComponent';
5−
6function Flex({
4function Flex({
7− as = 'div',
5+ as: As = 'div',
8 row = false,
6 row = false,
9 wrap = false,
7 wrap = false,
10 col = false,
8 col = false,
@@ -16,10 +14,8 @@ function Flex({
16 if (row && col) {
14 if (row && col) {
17 throw new Error('Only one of row or col must be true, not both.');
15 throw new Error('Only one of row or col must be true, not both.');
18 }
16 }
19−
20− const Component = getComponent(as);
21 return (
17 return (
22− <Component
18+ <As
23 className={cn(
19 className={cn(
24 'flex',
20 'flex',
25 {
21 {
@@ -39,14 +35,13 @@ function Flex({
39}
35}
40
36
41// flex can be boolean {true} or integer flex
37// flex can be boolean {true} or integer flex
42−Flex.Item = function FlexItem({ as = 'div', flex, className, ...rest }) {
38+Flex.Item = function FlexItem({ as: As = 'div', flex, className, ...rest }) {
43 if (flex === true) {
39 if (flex === true) {
44 flex = 1;
40 flex = 1;
45 }
41 }
46
42
47− const Component = getComponent(as);
48 return (
43 return (
49− <Component
44+ <As
50 className={cn(
45 className={cn(
51 {
46 {
52 [`flex${flex}`]: flex,
47 [`flex${flex}`]: flex,
src/indigo-react/components/Grid.js+20−11
@@ -1,20 +1,25 @@
1import React from 'react';
1import React from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3
3
4−import getComponent from '../lib/getComponent';
5−
6−function Grid({ as = 'div', gap = 0, className, ...rest }) {
4+function Grid({ as: As = 'div', gap = 0, align, justify, className, ...rest }) {
7− const Component = getComponent(as);
8 return (
5 return (
9− <Component
6+ <As
10− className={cn('grid12', gap && `gap${gap}`, className)}
7+ className={cn(
8+ 'grid12',
9+ gap && `gap${gap}`,
10+ {
11+ [`justify-${justify}`]: justify,
12+ [`align-${align}`]: align,
13+ },
14+ className
15+ )}
11 {...rest}
16 {...rest}
12 />
17 />
13 );
18 );
14}
19}
15
20
16Grid.Item = function GridItem({
21Grid.Item = function GridItem({
17− as = 'div',
22+ as: As = 'div',
18 full = false,
23 full = false,
19 half = 0,
24 half = 0,
20 third = 0,
25 third = 0,
@@ -26,9 +31,8 @@ Grid.Item = function GridItem({
26 className,
31 className,
27 ...rest
32 ...rest
28}) {
33}) {
29− const Component = getComponent(as);
30 return (
34 return (
31− <Component
35+ <As
32 className={cn(
36 className={cn(
33 {
37 {
34 full,
38 full,
@@ -51,14 +55,19 @@ Grid.Item = function GridItem({
51 );
55 );
52};
56};
53
57
54−Grid.Divider = function GridDivider({ vertical = false, className, ...rest }) {
58+Grid.Divider = function GridDivider({
59+ vertical = false,
60+ color = 'gray2',
61+ className,
62+ ...rest
63+}) {
55 return (
64 return (
56 <div
65 <div
57 style={{
66 style={{
58 height: vertical ? 'auto' : '1px',
67 height: vertical ? 'auto' : '1px',
59 width: vertical ? '1px' : 'auto',
68 width: vertical ? '1px' : 'auto',
60 }}
69 }}
61− className={cn('full', className || 'bg-gray2')}
70+ className={cn('full', `bg-${color}`, className)}
62 {...rest}
71 {...rest}
63 />
72 />
64 );
73 );
src/indigo-react/components/Input.js+11−10
@@ -69,7 +69,15 @@ export default function Input({
69 }, [focused]); // eslint-disable-line react-hooks/exhaustive-deps
69 }, [focused]); // eslint-disable-line react-hooks/exhaustive-deps
70
70
71 return (
71 return (
72+ <Flex
72− <Flex className={className} col>
73+ className={className}
74+ col
75+ style={{
76+ ...(disabled && {
77+ pointerEvents: 'none',
78+ cursor: 'not-allowed',
79+ }),
80+ }}>
73 <Flex.Item as="label" className="f6 lh-tall" htmlFor={name}>
81 <Flex.Item as="label" className="f6 lh-tall" htmlFor={name}>
74 {label}
82 {label}
75 </Flex.Item>
83 </Flex.Item>
@@ -77,9 +85,8 @@ export default function Input({
77 <Flex.Item
85 <Flex.Item
78 as="input"
86 as="input"
79 {...rest}
87 {...rest}
80− cols={[1, accessory ? 11 : 13]}
81 className={cn(
88 className={cn(
82− 'rel b b1 p3 outline-none',
89+ 'b b1 p3 outline-none',
83 { mono },
90 { mono },
84 {
91 {
85 'bg-white': !disabled,
92 'bg-white': !disabled,
@@ -92,17 +99,11 @@ export default function Input({
92 {
99 {
93 'b-green3': visiblyPassed,
100 'b-green3': visiblyPassed,
94 'b-black': focused && !visiblyPassed,
101 'b-black': focused && !visiblyPassed,
95− 'b-yellow3': !focused && !visiblyPassed && error,
102+ 'b-yellow3': !focused && error,
96 'b-gray3': !focused && !error && !visiblyPassed,
103 'b-gray3': !focused && !error && !visiblyPassed,
97 }
104 }
98 // TODO: inputClassName ?
105 // TODO: inputClassName ?
99 )}
106 )}
100− style={{
101− ...(disabled && {
102− pointerEvents: 'none',
103− cursor: 'not-allowed',
104− }),
105− }}
106 id={name}
107 id={name}
107 name={name}
108 name={name}
108 onKeyPress={onKeyPress}
109 onKeyPress={onKeyPress}
src/indigo-react/components/Typography.js+4−6
@@ -1,12 +1,9 @@
1import React from 'react';
1import React from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3
3
4−import getComponent from '../lib/getComponent';
5−
6const typeWith = (DefaultComponent, defaultClassName) =>
4const typeWith = (DefaultComponent, defaultClassName) =>
7− function({ as = DefaultComponent, className, ...rest }) {
5+ function({ as: As = DefaultComponent, className, ...rest }) {
8− const Component = getComponent(as);
9− return <Component className={cn(defaultClassName, className)} {...rest} />;
6+ return <As className={cn(defaultClassName, className)} {...rest} />;
10 };
7 };
11
8
12export const H1 = typeWith('h1');
9export const H1 = typeWith('h1');
@@ -17,8 +14,9 @@ export const H5 = typeWith('h5');
17export const H6 = typeWith('h6');
14export const H6 = typeWith('h6');
18export const Caption = typeWith('caption');
15export const Caption = typeWith('caption');
19export const P = typeWith('p');
16export const P = typeWith('p');
17+export const B = typeWith('b', 'fw-bold');
20
18
21export const Breadcrumb = typeWith('span', 'f6');
19export const Breadcrumb = typeWith('span', 'f6');
22−export const Text = typeWith('span', 'f5');
20+export const Text = typeWith('span');
23export const HelpText = typeWith('span', 'f6 gray4');
21export const HelpText = typeWith('span', 'f6 gray4');
24export const ErrorText = typeWith('span', 'f6 red3');
22export const ErrorText = typeWith('span', 'f6 red3');
src/indigo-react/index.js+1
@@ -2,6 +2,7 @@ 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 CheckboxInput } from './components/CheckboxInput';
5export { default as AccessoryIcon } from './components/AccessoryIcon';
6export { default as AccessoryIcon } from './components/AccessoryIcon';
6export { default as Button } from './components/Button';
7export { default as Button } from './components/Button';
7export { default as IconButton } from './components/IconButton';
8export { default as IconButton } from './components/IconButton';
src/indigo-react/indigo.css+39−37
@@ -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: Mon Jun 24 2019 19:57:25 GMT-0700 (Pacific Daylight Time)
16+ * Timestamp: Mon Jul 01 2019 12:26:43 GMT-0700 (Pacific Daylight Time)
17 *******************************************************************************/
17 *******************************************************************************/
18/**********************
18/**********************
19Color variables
19Color variables
@@ -734,13 +734,18 @@ caption,
734.pre {
734.pre {
735 white-space: pre; }
735 white-space: pre; }
736
736
737+.wrap {
738+ word-break: break-word; }
739+
737@media only screen and (min-width: 550px) {
740@media only screen and (min-width: 550px) {
738 .ws-normal-ns {
741 .ws-normal-ns {
739 white-space: normal; }
742 white-space: normal; }
740 .nowrap-ns {
743 .nowrap-ns {
741 white-space: nowrap; }
744 white-space: nowrap; }
742 .pre-ns {
745 .pre-ns {
743− white-space: pre; } }
746+ white-space: pre; }
747+ .wrap-ns {
748+ word-break: break-word; } }
744
749
745@media only screen and (min-width: 750px) and (max-width: 960px) {
750@media only screen and (min-width: 750px) and (max-width: 960px) {
746 .ws-normal-md {
751 .ws-normal-md {
@@ -748,7 +753,9 @@ caption,
748 .nowrap-md {
753 .nowrap-md {
749 white-space: nowrap; }
754 white-space: nowrap; }
750 .pre-md {
755 .pre-md {
751− white-space: pre; } }
756+ white-space: pre; }
757+ .wrap-md {
758+ word-break: break-word; } }
752
759
753@media only screen and (min-width: 960px) {
760@media only screen and (min-width: 960px) {
754 .ws-normal-lg {
761 .ws-normal-lg {
@@ -756,7 +763,9 @@ caption,
756 .nowrap-lg {
763 .nowrap-lg {
757 white-space: nowrap; }
764 white-space: nowrap; }
758 .pre-lg {
765 .pre-lg {
759− white-space: pre; } }
766+ white-space: pre; }
767+ .wrap-lg {
768+ word-break: break-word; } }
760
769
761/*******************************************************************************
770/*******************************************************************************
762 Border Radius
771 Border Radius
@@ -1531,43 +1540,33 @@ caption,
1531 flex-wrap: wrap; }
1540 flex-wrap: wrap; }
1532
1541
1533.align-start {
1542.align-start {
1534− display: flex;
1535 align-items: flex-start; }
1543 align-items: flex-start; }
1536
1544
1537.align-end {
1545.align-end {
1538− display: flex;
1539 align-items: flex-end; }
1546 align-items: flex-end; }
1540
1547
1541.align-center {
1548.align-center {
1542− display: flex;
1543 align-items: center; }
1549 align-items: center; }
1544
1550
1545.align-baseline {
1551.align-baseline {
1546− display: flex;
1547 align-items: baseline; }
1552 align-items: baseline; }
1548
1553
1549.align-stretch {
1554.align-stretch {
1550− display: flex;
1551 align-items: stretch; }
1555 align-items: stretch; }
1552
1556
1553.justify-start {
1557.justify-start {
1554− display: flex;
1555 justify-content: flex-start; }
1558 justify-content: flex-start; }
1556
1559
1557.justify-end {
1560.justify-end {
1558− display: flex;
1559 justify-content: flex-end; }
1561 justify-content: flex-end; }
1560
1562
1561.justify-center {
1563.justify-center {
1562− display: flex;
1563 justify-content: center; }
1564 justify-content: center; }
1564
1565
1565.justify-between {
1566.justify-between {
1566− display: flex;
1567 justify-content: space-between; }
1567 justify-content: space-between; }
1568
1568
1569.justify-around {
1569.justify-around {
1570− display: flex;
1571 justify-content: space-around; }
1570 justify-content: space-around; }
1572
1571
1573.flex-center {
1572.flex-center {
@@ -1627,16 +1626,12 @@ caption,
1627 .flex-ns {
1626 .flex-ns {
1628 display: flex; }
1627 display: flex; }
1629 .flex-row-ns {
1628 .flex-row-ns {
1630− display: flex;
1631 flex-direction: row; }
1629 flex-direction: row; }
1632 .flex-row-r-ns {
1630 .flex-row-r-ns {
1633− display: flex;
1634 flex-direction: row-reverse; }
1631 flex-direction: row-reverse; }
1635 .flex-col-ns {
1632 .flex-col-ns {
1636− display: flex;
1637 flex-direction: column; }
1633 flex-direction: column; }
1638 .flex-col-r-ns {
1634 .flex-col-r-ns {
1639− display: flex;
1640 flex-direction: column-reverse; }
1635 flex-direction: column-reverse; }
1641 .flex-wrap-ns {
1636 .flex-wrap-ns {
1642 flex-wrap: wrap; }
1637 flex-wrap: wrap; }
@@ -1700,16 +1695,12 @@ caption,
1700 .flex-md {
1695 .flex-md {
1701 display: flex; }
1696 display: flex; }
1702 .flex-row-md {
1697 .flex-row-md {
1703− display: flex;
1704 flex-direction: row; }
1698 flex-direction: row; }
1705 .flex-row-r-md {
1699 .flex-row-r-md {
1706− display: flex;
1707 flex-direction: row-reverse; }
1700 flex-direction: row-reverse; }
1708 .flex-col-md {
1701 .flex-col-md {
1709− display: flex;
1710 flex-direction: column; }
1702 flex-direction: column; }
1711 .flex-col-r-md {
1703 .flex-col-r-md {
1712− display: flex;
1713 flex-direction: column-reverse; }
1704 flex-direction: column-reverse; }
1714 .flex-wrap-md {
1705 .flex-wrap-md {
1715 flex-wrap: wrap; }
1706 flex-wrap: wrap; }
@@ -1773,16 +1764,12 @@ caption,
1773 .flex-lg {
1764 .flex-lg {
1774 display: flex; }
1765 display: flex; }
1775 .flex-row-lg {
1766 .flex-row-lg {
1776− display: flex;
1777 flex-direction: row; }
1767 flex-direction: row; }
1778 .flex-row-r-lg {
1768 .flex-row-r-lg {
1779− display: flex;
1780 flex-direction: row-reverse; }
1769 flex-direction: row-reverse; }
1781 .flex-col-lg {
1770 .flex-col-lg {
1782− display: flex;
1783 flex-direction: column; }
1771 flex-direction: column; }
1784 .flex-col-r-lg {
1772 .flex-col-r-lg {
1785− display: flex;
1786 flex-direction: column-reverse; }
1773 flex-direction: column-reverse; }
1787 .flex-wrap-lg {
1774 .flex-wrap-lg {
1788 flex-wrap: wrap; }
1775 flex-wrap: wrap; }
@@ -2032,30 +2019,45 @@ caption,
2032 .fourth-4-lg {
2019 .fourth-4-lg {
2033 grid-area: auto / 10 / auto / 13; } }
2020 grid-area: auto / 10 / auto / 13; } }
2034
2021
2035−.gap1 {
2022+.gap0 {
2036− gap: 1px; }
2037−
2038−.gap2 {
2039− gap: 2px; }
2023+ gap: 0px; }
2040
2024
2041−.gap4 {
2025+.gap1 {
2042 gap: 4px; }
2026 gap: 4px; }
2043
2027
2044−.gap8 {
2028+.gap2 {
2045 gap: 8px; }
2029 gap: 8px; }
2046
2030
2047−.gap12 {
2031+.gap3 {
2048 gap: 12px; }
2032 gap: 12px; }
2049
2033
2050−.gap16 {
2034+.gap4 {
2051 gap: 16px; }
2035 gap: 16px; }
2052
2036
2053−.gap24 {
2037+.gap5 {
2038+ gap: 20px; }
2039+
2040+.gap6 {
2054 gap: 24px; }
2041 gap: 24px; }
2055
2042
2056−.gap32 {
2043+.gap7 {
2057 gap: 32px; }
2044 gap: 32px; }
2058
2045
2046+.gap8 {
2047+ gap: 48px; }
2048+
2049+.gap9 {
2050+ gap: 64px; }
2051+
2052+.gap10 {
2053+ gap: 96px; }
2054+
2055+.gap11 {
2056+ gap: 160px; }
2057+
2058+.gap12 {
2059+ gap: 288px; }
2060+
2059.c1-2 {
2061.c1-2 {
2060 grid-column-start: 1;
2062 grid-column-start: 1;
2061 grid-column-end: 2; }
2063 grid-column-end: 2; }
src/indigo-react/lib/getComponent.jsdeleted−1
@@ -1 +0,0 @@
1−export default as => as || 'div';
src/indigo-react/lib/useForm.js+23−9
@@ -29,11 +29,7 @@ export default function useForm(configs = []) {
29
29
30 // track values
30 // track values
31 const [values, _setValues] = useState(() =>
31 const [values, _setValues] = useState(() =>
32− defaultsFor(configs, config => config.initialValue)
32+ defaultsFor(configs, config => config.initialValue || '')
33− );
34− const setValue = useCallback(
35− (name, value) => _setValues(values => ({ ...values, [name]: value })),
36− [_setValues]
37 );
33 );
38 // ^ NB(shrugs): because we're not syncing the initialValue of _additional_
34 // ^ 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
35 // configs that are added, we won't be able to set an initialValue for a
@@ -42,6 +38,11 @@ export default function useForm(configs = []) {
42 // but for dynamically added input configs, we won't get validation, etc
38 // but for dynamically added input configs, we won't get validation, etc
43 // until there's a traditional state update.
39 // until there's a traditional state update.
44
40
41+ const setValue = useCallback(
42+ (name, value) => _setValues(values => ({ ...values, [name]: value })),
43+ [_setValues]
44+ );
45+
45 // track focused states
46 // track focused states
46 const [focused, setFocused] = useSetState(() =>
47 const [focused, setFocused] = useSetState(() =>
47 defaultsFor(configs, config => config.autoFocus && !config.disabled)
48 defaultsFor(configs, config => config.autoFocus && !config.disabled)
@@ -70,11 +71,23 @@ export default function useForm(configs = []) {
70 [byName]
71 [byName]
71 );
72 );
72
73
74+ const getValue = useCallback(
75+ (name, e) => {
76+ if (byName[name].type === 'checkbox') {
77+ return e.target.checked;
78+ }
79+
80+ return e.target.value;
81+ },
82+ [byName]
83+ );
84+
73 // on change, transform and set value
85 // on change, transform and set value
74 const onChange = useCallback(
86 const onChange = useCallback(
75− name => e => setValue(name, transform(name, e.target.value)), //
87+ name => e => setValue(name, transform(name, getValue(name, e))), //
76− [setValue, transform]
88+ [setValue, transform, getValue]
77 );
89 );
90+
78 // on focus, update focus
91 // on focus, update focus
79 const onFocus = useCallback(
92 const onFocus = useCallback(
80 name => e => setFocused({ [name]: true }), //
93 name => e => setFocused({ [name]: true }), //
@@ -85,7 +98,7 @@ export default function useForm(configs = []) {
85 const onBlur = useCallback(
98 const onBlur = useCallback(
86 name => e => {
99 name => e => {
87 setFocused({ [name]: false });
100 setFocused({ [name]: false });
88− setHasBeenFocused({ [name]: false });
101+ setHasBeenFocused({ [name]: true });
89 },
102 },
90 [setFocused, setHasBeenFocused]
103 [setFocused, setHasBeenFocused]
91 );
104 );
@@ -153,7 +166,8 @@ export default function useForm(configs = []) {
153 ...rest,
166 ...rest,
154 // dom properties below:
167 // dom properties below:
155 bind: {
168 bind: {
156− value: values[name] || initialValue,
169+ value: values[name] || initialValue || '',
170+ checked: !!values[name],
157 onChange: onChange(name),
171 onChange: onChange(name),
158 onFocus: onFocus(name),
172 onFocus: onFocus(name),
159 onBlur: onBlur(name),
173 onBlur: onBlur(name),
src/lib/MediaQuery.jsnew file+24
@@ -0,0 +1,24 @@
1+import React from 'react';
2+
3+import { createContext, useContext } from 'react';
4+import useMedia from './useMedia';
5+
6+// sync with //indigo/indigo/css/src/lib/_breakpoints.scss
7+const BREAKPOINTS = [
8+ '(min-width: 550px)', // ns
9+ '(min-width: 750px)', // md
10+ '(min-width: 960px)', // lg
11+];
12+
13+export const MediaQueryContext = createContext(null);
14+// provide the index to consumers so they can easily index an array
15+export const MediaQueryProvider = ({ children }) => (
16+ <MediaQueryContext.Provider value={useMedia(BREAKPOINTS, [0, 1, 2], 0)}>
17+ {children}
18+ </MediaQueryContext.Provider>
19+);
20+
21+// Hook version
22+export function useMediaQuery() {
23+ return useContext(MediaQueryContext);
24+}
src/lib/constants.js+3
@@ -5,6 +5,8 @@ const MAX_STAR = 65535;
5const MIN_PLANET = 65536;
5const MIN_PLANET = 65536;
6const MAX_PLANET = 4294967297;
6const MAX_PLANET = 4294967297;
7
7
8+const ZOD = MIN_GALAXY;
9+
8const PLANET_ENTROPY_BITS = 64;
10const PLANET_ENTROPY_BITS = 64;
9const STAR_ENTROPY_BITS = 128;
11const STAR_ENTROPY_BITS = 128;
10const GALAXY_ENTROPY_BITS = 384;
12const GALAXY_ENTROPY_BITS = 384;
@@ -56,4 +58,5 @@ export {
56 STAR_ENTROPY_BITS,
58 STAR_ENTROPY_BITS,
57 GALAXY_ENTROPY_BITS,
59 GALAXY_ENTROPY_BITS,
58 SEED_ENTROPY_BITS,
60 SEED_ENTROPY_BITS,
61+ ZOD,
59};
62};
src/lib/invite.js+57−106
@@ -1,4 +1,4 @@
1−import { Just, Nothing } from 'folktale/maybe';
1+import { Just } from 'folktale/maybe';
2import Tx from 'ethereumjs-tx';
2import Tx from 'ethereumjs-tx';
3
3
4import * as azimuth from 'azimuth-js';
4import * as azimuth from 'azimuth-js';
@@ -9,8 +9,7 @@ import * as tank from './tank';
9import JSZip from 'jszip';
9import JSZip from 'jszip';
10import saveAs from 'file-saver';
10import saveAs from 'file-saver';
11
11
12−import { sendSignedTransaction } from './txn';
12+import { sendSignedTransaction, hexify } from './txn';
13−import { BRIDGE_ERROR } from './error';
14import { attemptNetworkSeedDerivation } from './keys';
13import { attemptNetworkSeedDerivation } from './keys';
15import { addHexPrefix, WALLET_TYPES } from './wallet';
14import { addHexPrefix, WALLET_TYPES } from './wallet';
16
15
@@ -32,36 +31,36 @@ const WALLET_STATES = {
32
31
33const TRANSACTION_STATES = {
32const TRANSACTION_STATES = {
34 GENERATING: {
33 GENERATING: {
35− label: 'Generating transactions',
34+ label: 'Generating Transactions...',
36− pct: '0%',
35+ progress: 0.0,
37 },
36 },
38 SIGNING: {
37 SIGNING: {
39− label: 'Signing transactions',
38+ label: 'Signing Transactions...',
40− pct: '15%',
39+ progress: 0.15,
41 },
40 },
42 FUNDING_INVITE: {
41 FUNDING_INVITE: {
43− label: 'Funding invite wallet',
42+ label: 'Funding Invite Wallet...',
44− pct: '30%',
43+ progress: 0.3,
45 },
44 },
46 CLAIMING: {
45 CLAIMING: {
47− label: 'Claiming invite',
46+ label: 'Claiming Invite...',
48− pct: '55%',
47+ progress: 0.55,
49 },
48 },
50 FUNDING_RECIPIENT: {
49 FUNDING_RECIPIENT: {
51− label: 'Funding recipient wallet',
50+ label: 'Funding Recipient Wallet...',
52− pct: '65%',
51+ progress: 0.65,
53 },
52 },
54 CONFIGURING: {
53 CONFIGURING: {
55− label: 'Configuring planet',
54+ label: 'Configuring Point...',
56− pct: '85%',
55+ progress: 0.85,
57 },
56 },
58 CLEANING: {
57 CLEANING: {
59− label: 'Cleaning up',
58+ label: 'Cleaning Up...',
60− pct: '95%',
59+ progress: 0.95,
61 },
60 },
62 DONE: {
61 DONE: {
63 label: 'Done',
62 label: 'Done',
64− pct: '100%',
63+ progress: 1.0,
65 },
64 },
66};
65};
67
66
@@ -102,93 +101,61 @@ async function downloadWallet(paper) {
102 });
101 });
103}
102}
104
103
105−async function startTransactions(args) {
104+async function claimPointFromInvite({
106− let {
105+ inviteWallet,
107− realPointM,
106+ wallet,
107+ point,
108− web3: web3M,
108+ web3,
109− contracts: contractsM,
109+ contracts,
110− inviteWalletM,
110+ onUpdate,
111− realWalletM,
111+}) {
112− setUrbitWallet,
113− updateProgress,
114− } = args;
115−
116− const askForFunding = (address, amount, current) => {
112+ const askForFunding = (address, minBalance, balance) =>
117− updateProgress({
113+ onUpdate({
118− type: 'notify',
114+ type: 'askFunding',
119− value: `Please make sure ${address} has at least ${amount} wei, we'll continue once that's true. Current balance: ${current}. Waiting`,
115+ value: { address, minBalance, balance },
120 });
116 });
121− };
122
117
123− if (Nothing.hasInstance(web3M)) {
118+ const gotFunding = () => onUpdate({ type: 'gotFunding' });
124− throw BRIDGE_ERROR.MISSING_WEB3;
119+ const progress = state => onUpdate({ type: 'progress', state });
125− }
126− const web3 = web3M.value;
127−
128− if (Nothing.hasInstance(contractsM)) {
129− throw BRIDGE_ERROR.MISSING_CONTRACTS;
130− }
131− const contracts = contractsM.value;
132−
133− if (Nothing.hasInstance(inviteWalletM)) {
134− throw BRIDGE_ERROR.MISSING_WALLET;
135− }
136− const inviteWallet = inviteWalletM.value;
137−
138− if (Nothing.hasInstance(realWalletM)) {
139− throw BRIDGE_ERROR.MISSING_WALLET;
140− }
141− const realWallet = realWalletM.value;
142−
143− // const realWallet = await urbitWalletFromTicket(args.realTicket, point);
144−
145− if (Nothing.hasInstance(realPointM)) {
146− throw BRIDGE_ERROR.MISSING_POINT;
147− }
148− const point = realPointM.value;
149
120
150 const inviteAddress = inviteWallet.address;
121 const inviteAddress = inviteWallet.address;
151− console.log('working as', inviteAddress);
152
122
153− // transfer from invite wallet to new wallet
123+ progress(TRANSACTION_STATES.GENERATING);
154
124
125+ // transfer from invite wallet to new wallet
155− let transferTx = azimuth.ecliptic.transferPoint(
126+ const transferTx = azimuth.ecliptic.transferPoint(
156 contracts,
127 contracts,
157 point,
128 point,
158− realWallet.ownership.keys.address
129+ wallet.ownership.keys.address
159 );
130 );
160 transferTx.gas = 500000; //TODO can maybe be lower?
131 transferTx.gas = 500000; //TODO can maybe be lower?
161
132
162 // ping gas tank with txs if needed
133 // ping gas tank with txs if needed
163−
164 let inviteNonce = await web3.eth.getTransactionCount(inviteAddress);
134 let inviteNonce = await web3.eth.getTransactionCount(inviteAddress);
165 transferTx.nonce = inviteNonce++;
135 transferTx.nonce = inviteNonce++;
166 transferTx.gasPrice = 20000000000; //NOTE we pay the premium for faster ux
136 transferTx.gasPrice = 20000000000; //NOTE we pay the premium for faster ux
167 transferTx.from = inviteAddress;
137 transferTx.from = inviteAddress;
168
138
139+ progress(TRANSACTION_STATES.SIGNING);
169− //NOTE using web3.eth.accounts.signTransaction is broken (1.0.0-beta51)
140+ // NOTE: using web3.eth.accounts.signTransaction is broken (1.0.0-beta51)
170− let transferStx = new Tx(transferTx);
141+ const transferStx = new Tx(transferTx);
171 transferStx.sign(inviteWallet.privateKey);
142 transferStx.sign(inviteWallet.privateKey);
172− let rawTransferStx = '0x' + transferStx.serialize().toString('hex');
143+ const rawTransferStx = hexify(transferStx.serialize());
173−
174 const transferCost = transferTx.gas * transferTx.gasPrice;
144 const transferCost = transferTx.gas * transferTx.gasPrice;
175− updateProgress({
145+
176− type: 'progress',
177− value: TRANSACTION_STATES.FUNDING_INVITE,
146+ progress(TRANSACTION_STATES.FUNDING_INVITE);
178− });
147+
179 let usedTank = await tank.ensureFundsFor(
148 let usedTank = await tank.ensureFundsFor(
180 web3,
149 web3,
181 point,
150 point,
182 inviteAddress,
151 inviteAddress,
183 transferCost,
152 transferCost,
184 [rawTransferStx],
153 [rawTransferStx],
185− askForFunding
154+ askForFunding,
155+ gotFunding
186 );
156 );
187
157
188− updateProgress({
189− type: 'progress',
190− value: TRANSACTION_STATES.CLAIMING,
158+ progress(TRANSACTION_STATES.CLAIMING);
191− });
192
159
193 // send transaction
160 // send transaction
194 await sendAndAwaitConfirm(web3, [Just(transferStx)], usedTank);
161 await sendAndAwaitConfirm(web3, [Just(transferStx)], usedTank);
@@ -197,15 +164,13 @@ async function startTransactions(args) {
197 // we're gonna be operating as the new wallet from here on out, so change
164 // we're gonna be operating as the new wallet from here on out, so change
198 // the relevant values
165 // the relevant values
199 //
166 //
200− setUrbitWallet(Just(realWallet));
201− const newAddress = realWallet.ownership.keys.address;
167+ const newAddress = wallet.ownership.keys.address;
202
168
203 // configure management proxy
169 // configure management proxy
204−
205− let managementTx = azimuth.ecliptic.setManagementProxy(
170+ const managementTx = azimuth.ecliptic.setManagementProxy(
206 contracts,
171 contracts,
207 point,
172 point,
208− realWallet.management.keys.address
173+ wallet.management.keys.address
209 );
174 );
210 managementTx.gas = 200000;
175 managementTx.gas = 200000;
211 managementTx.nonce = 0;
176 managementTx.nonce = 0;
@@ -214,7 +179,7 @@ async function startTransactions(args) {
214 //TODO feels like more of this logic should live in a lib?
179 //TODO feels like more of this logic should live in a lib?
215 const seed = await attemptNetworkSeedDerivation(true, {
180 const seed = await attemptNetworkSeedDerivation(true, {
216 walletType: WALLET_TYPES.TICKET,
181 walletType: WALLET_TYPES.TICKET,
217− urbitWallet: Just(realWallet),
182+ urbitWallet: Just(wallet),
218 pointCursor: Just(point),
183 pointCursor: Just(point),
219 pointCache: { [point]: { keyRevisionNumber: 0 } },
184 pointCache: { [point]: { keyRevisionNumber: 0 } },
220 });
185 });
@@ -251,17 +216,15 @@ async function startTransactions(args) {
251
216
252 let txPairs = txs.map(tx => {
217 let txPairs = txs.map(tx => {
253 let stx = new Tx(tx);
218 let stx = new Tx(tx);
254− stx.sign(Buffer.from(realWallet.ownership.keys.private, 'hex'));
219+ stx.sign(Buffer.from(wallet.ownership.keys.private, 'hex'));
255 return {
220 return {
256 raw: '0x' + stx.serialize().toString('hex'),
221 raw: '0x' + stx.serialize().toString('hex'),
257 signed: stx,
222 signed: stx,
258 };
223 };
259 });
224 });
260
225
261− updateProgress({
262− type: 'progress',
263− value: TRANSACTION_STATES.FUNDING_RECIPIENT,
226+ progress(TRANSACTION_STATES.FUNDING_RECIPIENT);
264− });
227+
265 usedTank = await tank.ensureFundsFor(
228 usedTank = await tank.ensureFundsFor(
266 web3,
229 web3,
267 point,
230 point,
@@ -271,17 +234,11 @@ async function startTransactions(args) {
271 askForFunding
234 askForFunding
272 );
235 );
273
236
274− updateProgress({
275− type: 'progress',
276− value: TRANSACTION_STATES.CONFIGURING,
237+ progress(TRANSACTION_STATES.CONFIGURING);
277− });
278
238
279 await sendAndAwaitConfirm(web3, txPairs.map(p => Just(p.signed)), usedTank);
239 await sendAndAwaitConfirm(web3, txPairs.map(p => Just(p.signed)), usedTank);
280
240
281− updateProgress({
282− type: 'progress',
283− value: TRANSACTION_STATES.CLEANING,
241+ progress(TRANSACTION_STATES.CLEANING);
284− });
285
242
286 // if non-trivial eth left in invite wallet, transfer to new ownership
243 // if non-trivial eth left in invite wallet, transfer to new ownership
287 let balance = await web3.eth.getBalance(inviteAddress);
244 let balance = await web3.eth.getBalance(inviteAddress);
@@ -300,20 +257,14 @@ async function startTransactions(args) {
300 };
257 };
301 let stx = new Tx(tx);
258 let stx = new Tx(tx);
302 stx.sign(inviteWallet.privateKey);
259 stx.sign(inviteWallet.privateKey);
303− const rawTx = '0x' + stx.serialize().toString('hex');
260+ const rawTx = hexify(stx.serialize());
304 web3.eth.sendSignedTransaction(rawTx).catch(err => {
261 web3.eth.sendSignedTransaction(rawTx).catch(err => {
305 console.log('error sending value tx, who cares', err);
262 console.log('error sending value tx, who cares', err);
306 });
263 });
307 console.log('sent old balance');
264 console.log('sent old balance');
308 }
265 }
309
266
310− updateProgress({
311− type: 'progress',
312− value: TRANSACTION_STATES.DONE,
267+ progress(TRANSACTION_STATES.DONE);
313− });
314−
315− // proceed without waiting for confirm
316− setUrbitWallet(Just(realWallet));
317}
268}
318
269
319async function sendAndAwaitConfirm(web3, signedTxs, usedTank) {
270async function sendAndAwaitConfirm(web3, signedTxs, usedTank) {
@@ -329,7 +280,7 @@ async function sendAndAwaitConfirm(web3, signedTxs, usedTank) {
329export {
280export {
330 generateWallet,
281 generateWallet,
331 downloadWallet,
282 downloadWallet,
332− startTransactions,
283+ claimPointFromInvite,
333 INVITE_STAGES,
284 INVITE_STAGES,
334 WALLET_STATES,
285 WALLET_STATES,
335 TRANSACTION_STATES,
286 TRANSACTION_STATES,
src/lib/keys.js+1−1
@@ -73,7 +73,7 @@ const attemptNetworkSeedDerivation = async (next, args) => {
73 //
73 //
74 // following code is intentionally verbose for sake of clarity
74 // following code is intentionally verbose for sake of clarity
75
75
76− const point = need.pointCursor(args.pointCursor);
76+ const point = need.point(args.pointCursor);
77 const pointDetails = need.fromPointCache(args.pointCache, point);
77 const pointDetails = need.fromPointCache(args.pointCache, point);
78
78
79 const revision =
79 const revision =
src/lib/need.js+27−12
@@ -3,34 +3,49 @@
3
3
4import { BRIDGE_ERROR } from './error';
4import { BRIDGE_ERROR } from './error';
5
5
6−const needBuilder = error => obj => {
6+const needBuilder = fn => obj => {
7 if (!obj) {
7 if (!obj) {
8− throw error;
8+ fn();
9 }
9 }
10
10
11 return obj.matchWith({
11 return obj.matchWith({
12− Nothing: () => {
12+ Nothing: fn,
13− throw error;
14− },
15 Just: p => p.value,
13 Just: p => p.value,
16 });
14 });
17};
15};
18
16
19−export const web3 = needBuilder(BRIDGE_ERROR.MISSING_WEB3);
17+export const web3 = needBuilder(() => {
18+ throw new Error(BRIDGE_ERROR.MISSING_WEB3.message);
19+});
20+
20−export const contracts = needBuilder(BRIDGE_ERROR.MISSING_CONTRACTS);
21+export const contracts = needBuilder(() => {
22+ throw new Error(BRIDGE_ERROR.MISSING_CONTRACTS.message);
23+});
24+
21−export const wallet = needBuilder(BRIDGE_ERROR.MISSING_WALLET);
25+export const wallet = needBuilder(() => {
26+ throw new Error(BRIDGE_ERROR.MISSING_WALLET.message);
27+});
28+
22export const addressFromWallet = obj => wallet(obj).address;
29export const addressFromWallet = obj => wallet(obj).address;
30+
23−export const pointCursor = needBuilder(BRIDGE_ERROR.MISSING_POINT);
31+export const point = needBuilder(() => {
32+ throw new Error(BRIDGE_ERROR.MISSING_POINT.message);
33+});
34+
24−export const pointCache = needBuilder(BRIDGE_ERROR.MISSING_POINT);
35+export const pointCache = needBuilder(() => {
36+ throw new Error(BRIDGE_ERROR.MISSING_POINT.message);
37+});
38+
25export const fromPointCache = (cache, point) => {
39export const fromPointCache = (cache, point) => {
26 if (!(point in cache)) {
40 if (!(point in cache)) {
27− throw BRIDGE_ERROR.MISSING_POINT;
41+ throw new Error(BRIDGE_ERROR.MISSING_POINT.message);
28 }
42 }
29
43
30 return cache[point];
44 return cache[point];
31};
45};
46+
32export const keystore = obj => {
47export const keystore = obj => {
33− const ks = needBuilder(BRIDGE_ERROR.MISSING_KEYSTORE)();
48+ const ks = needBuilder(BRIDGE_ERROR.MISSING_KEYSTORE)(obj);
34 return ks.value.matchWith({
49 return ks.value.matchWith({
35 Ok: result => result.value,
50 Ok: result => result.value,
36 Error: _ => {
51 Error: _ => {
src/lib/patp2dec.jsnew file+9
@@ -0,0 +1,9 @@
1+import * as ob from 'urbit-ob';
2+
3+/**
4+ * Convert a patp string into a number, or throw.
5+ *
6+ * @throws
7+ * @return number
8+ */
9+export default patp => parseInt(ob.patp2dec(patp), 10);
src/lib/routeNames.js+1−1
@@ -1,7 +1,7 @@
1export const ROUTE_NAMES = {
1export const ROUTE_NAMES = {
2 DEFAULT: Symbol('DEFAULT'),
2 DEFAULT: Symbol('DEFAULT'),
3 //
3 //
4− INVITE_TICKET: Symbol('INVITE_TICKET'),
4+ ACTIVATE: Symbol('ACTIVATE'),
5 INVITE_TRANSACTIONS: Symbol('INVITE_TRANSACTIONS'),
5 INVITE_TRANSACTIONS: Symbol('INVITE_TRANSACTIONS'),
6 INVITE: Symbol('INVITE'),
6 INVITE: Symbol('INVITE'),
7 INVITES_MANAGE: Symbol('INVITES_MANAGE'),
7 INVITES_MANAGE: Symbol('INVITES_MANAGE'),
src/lib/router.js+2−50
@@ -1,9 +1,5 @@
1−import * as ob from 'urbit-ob';
2−import * as need from './need';
3−
4import Landing from '../views/Landing';
1import Landing from '../views/Landing';
5import Login from '../views/Login.js';
2import Login from '../views/Login.js';
6−import InviteTicket from '../views/InviteTicket';
7import InvitesManage from '../views/InvitesManage.js';
3import InvitesManage from '../views/InvitesManage.js';
8import AcceptTransfer from '../views/AcceptTransfer';
4import AcceptTransfer from '../views/AcceptTransfer';
9import CancelTransfer from '../views/CancelTransfer';
5import CancelTransfer from '../views/CancelTransfer';
@@ -22,13 +18,13 @@ import Point from '../views/Point';
22import Points from '../views/Points';
18import Points from '../views/Points';
23import Transfer from '../views/Transfer';
19import Transfer from '../views/Transfer';
24import Invite from 'views/Invite';
20import Invite from 'views/Invite';
21+import Activate from 'views/Activate';
25
22
26import { ROUTE_NAMES } from './routeNames';
23import { ROUTE_NAMES } from './routeNames';
27
24
28export const ROUTES = {
25export const ROUTES = {
29− [ROUTE_NAMES.DEFAULT]: Landing,
30 [ROUTE_NAMES.LANDING]: Landing,
26 [ROUTE_NAMES.LANDING]: Landing,
31− [ROUTE_NAMES.INVITE_TICKET]: InviteTicket,
27+ [ROUTE_NAMES.ACTIVATE]: Activate,
32 [ROUTE_NAMES.INVITE]: Invite,
28 [ROUTE_NAMES.INVITE]: Invite,
33 [ROUTE_NAMES.INVITES_MANAGE]: InvitesManage,
29 [ROUTE_NAMES.INVITES_MANAGE]: InvitesManage,
34 [ROUTE_NAMES.LOGIN]: Login,
30 [ROUTE_NAMES.LOGIN]: Login,
@@ -47,47 +43,3 @@ export const ROUTES = {
47 [ROUTE_NAMES.SENT_TRANSACTION]: SentTransaction,
43 [ROUTE_NAMES.SENT_TRANSACTION]: SentTransaction,
48 [ROUTE_NAMES.GEN_KEYFILE]: GenKeyfile,
44 [ROUTE_NAMES.GEN_KEYFILE]: GenKeyfile,
49};
45};
50−
51−export const getRouteBreadcrumb = ({ pointCursor }) => route => {
52− switch (route.name) {
53− case ROUTE_NAMES.INVITE_TICKET:
54− return 'Invite code';
55− case ROUTE_NAMES.INVITE_TRANSACTIONS:
56− return 'Setting up new wallet';
57− case ROUTE_NAMES.INVITE:
58− return 'Send invites';
59− case ROUTE_NAMES.INVITES_MANAGE:
60− return 'Manage invites';
61− case ROUTE_NAMES.LOGIN:
62− return 'Login to Bridge';
63− case ROUTE_NAMES.POINT:
64− return `Overview: ${ob.patp(need.pointCursor(pointCursor))}`;
65− case ROUTE_NAMES.POINTS:
66− return 'Points';
67− case ROUTE_NAMES.VIEW_POINT:
68− return 'View';
69− case ROUTE_NAMES.SET_TRANSFER_PROXY:
70− case ROUTE_NAMES.SET_MANAGEMENT_PROXY:
71− case ROUTE_NAMES.SET_SPAWN_PROXY:
72− return 'Set Proxy';
73− case ROUTE_NAMES.CREATE_GALAXY:
74− return 'Create Galaxy';
75− case ROUTE_NAMES.ISSUE_CHILD:
76− return 'Issue Child';
77− case ROUTE_NAMES.TRANSFER:
78− return 'Transfer';
79− case ROUTE_NAMES.ACCEPT_TRANSFER:
80− return 'Accept Transfer';
81− case ROUTE_NAMES.CANCEL_TRANSFER:
82− return 'Cancel Transfer';
83− case ROUTE_NAMES.GEN_KEYFILE:
84− return 'Keyfile';
85− case ROUTE_NAMES.SET_KEYS:
86− return 'Configure Keys';
87− case ROUTE_NAMES.SENT_TRANSACTION:
88− return 'Txn';
89− case ROUTE_NAMES.LANDING:
90− default:
91− return 'Bridge';
92− }
93−};
src/lib/timeout.jsnew file+1
@@ -0,0 +1 @@
1+export default ms => new Promise(resolve => setTimeout(resolve, ms));
src/lib/useBreakpoints.jsnew file+5
@@ -0,0 +1,5 @@
1+import { useMediaQuery } from './MediaQuery';
2+
3+export default function useBreakpoints(values = [null, null, null]) {
4+ return values[useMediaQuery()];
5+}
src/lib/useImpliedPoint.jsnew file+14
@@ -0,0 +1,14 @@
1+import * as ob from 'urbit-ob';
2+
3+import { prependSig } from './transformers';
4+
5+/**
6+ * pull the suggested point from the subdomain
7+ */
8+export default function useImpliedPoint() {
9+ const subdomain = window.location.host.split('.')[0];
10+ const patp = prependSig(subdomain);
11+ const isValid = ob.isValidPatp(patp);
12+
13+ return isValid ? patp : null;
14+}
src/lib/useMedia.jsnew file+31
@@ -0,0 +1,31 @@
1+import { useCallback, useMemo, useState, useEffect } from 'react';
2+
3+// via: https://usehooks.com/useMedia/
4+// NB(shrugs): we reverse the order because we want to be 'mobile first'
5+// and therefore want the breakpoint that is the most "specific"
6+export default function useMedia(queries, values, defaultValue) {
7+ const reverseValues = useMemo(() => values.reverse(), [values]);
8+ const mediaQueryLists = useMemo(
9+ () => queries.reverse().map(q => window.matchMedia(q)),
10+ [queries]
11+ );
12+
13+ const getValue = useCallback(() => {
14+ const index = mediaQueryLists.findIndex(mql => mql.matches);
15+
16+ return typeof reverseValues[index] !== 'undefined'
17+ ? reverseValues[index]
18+ : defaultValue;
19+ }, [mediaQueryLists, reverseValues, defaultValue]);
20+
21+ const [value, setValue] = useState(getValue);
22+
23+ useEffect(() => {
24+ const handler = () => setValue(getValue);
25+ mediaQueryLists.forEach(mql => mql.addListener(handler));
26+
27+ return () => mediaQueryLists.forEach(mql => mql.removeListener(handler));
28+ }, [mediaQueryLists, getValue]);
29+
30+ return value;
31+}
src/lib/useRenderCount.js+2−2
@@ -1,7 +1,7 @@
1import { useRef } from 'react';
1import { useRef } from 'react';
2
2
3−export default function useRenderCount(debubgLabel = 'Component') {
3+export default function useRenderCount(debugLabel = 'Component') {
4 const count = useRef(0);
4 const count = useRef(0);
5 count.current++;
5 count.current++;
6− console.log(`${debubgLabel} — ${count.current}`);
6+ console.log(`${debugLabel} — ${count.current}`);
7}
7}
src/lib/useResetPointCursor.jsnew file+12
@@ -0,0 +1,12 @@
1+import { useEffect } from 'react';
2+import Maybe from 'folktale/maybe';
3+
4+import { usePointCursor } from 'store/pointCursor';
5+
6+export default function useResetPointCursor() {
7+ const { setPointCursor } = usePointCursor();
8+
9+ useEffect(() => {
10+ setPointCursor(Maybe.Nothing());
11+ }, [setPointCursor]);
12+}
src/lib/useRouter.js+10−6
@@ -24,7 +24,6 @@ export default function useRouter({
24 )}`
24 )}`
25 );
25 );
26 }
26 }
27−
28 setRoutes(routes => [...routes, { key, data }]);
27 setRoutes(routes => [...routes, { key, data }]);
29 },
28 },
30 [setRoutes, views]
29 [setRoutes, views]
@@ -55,6 +54,13 @@ export default function useRouter({
55 [setRoutes]
54 [setRoutes]
56 );
55 );
57 const peek = useCallback(() => last(routes), [routes]);
56 const peek = useCallback(() => last(routes), [routes]);
57+ const replaceWith = useCallback(routes => setRoutes(() => routes), [
58+ setRoutes,
59+ ]);
60+ const reset = useCallback(() => setRoutes(initialRoutes), [
61+ setRoutes,
62+ initialRoutes,
63+ ]);
58 const includes = useCallback(key => _includes(routes.map(r => r.key), key), [
64 const includes = useCallback(key => _includes(routes.map(r => r.key), key), [
59 routes,
65 routes,
60 ]);
66 ]);
@@ -62,12 +68,8 @@ export default function useRouter({
62 const data = useMemo(() => last(routes).data, [routes]);
68 const data = useMemo(() => last(routes).data, [routes]);
63 const Route = useMemo(() => views[peek().key], [views, peek]);
69 const Route = useMemo(() => views[peek().key], [views, peek]);
64
70
65− // Scroll to top of page with each route transition on primary navigation
71+ // Scroll to top of page with each route transition
66 useEffect(() => {
72 useEffect(() => {
67− if (!primary) {
68− return;
69− }
70−
71 window.scrollTo(0, 0);
73 window.scrollTo(0, 0);
72 }, [routes, primary]);
74 }, [routes, primary]);
73
75
@@ -94,6 +96,8 @@ export default function useRouter({
94 push,
96 push,
95 popAndPush,
97 popAndPush,
96 pop,
98 pop,
99+ replaceWith,
100+ reset,
97 peek,
101 peek,
98 size,
102 size,
99 includes,
103 includes,
src/lib/useSyncPoints.js+23−30
@@ -1,39 +1,32 @@
1import { useEffect } from 'react';
1import { useEffect } from 'react';
2−import { isEqual } from 'lodash';
2+import { difference } from 'lodash';
3
3
4import { usePointCache } from 'store/pointCache';
4import { usePointCache } from 'store/pointCache';
5import usePreviousValue from 'lib/usePreviousValue';
5import usePreviousValue from 'lib/usePreviousValue';
6
6
7−// sync a known (i.e. mine) point to the cache (i.e display values)
7+const buildSyncHook = getFn =>
8−export function useSyncKnownPoints(points = []) {
8+ function SyncPoint(points = []) {
9− // TBD: right now we don't need anything more than the point itself
9+ const fn = getFn();
10− // but soon we'll need to know information around "key type"
10+ const prevPoints = usePreviousValue(points);
11−}
12
11
13−// sync a foreign (not the user's) point to the cache
12+ useEffect(() => {
14−export function useSyncForeignPoints(points = []) {
13+ // sync points that are new between renders
15− const { syncForeignPoint } = usePointCache();
14+ const newPoints = difference(points, prevPoints);
16− const prevPoints = usePreviousValue(points);
15+ Promise.all(newPoints.map(fn));
16+ }, [fn, prevPoints, points]);
17+ };
17
18
18− useEffect(() => {
19+export const useSyncKnownPoints = buildSyncHook(function() {
19− if (!isEqual(prevPoints, points)) {
20+ const { syncKnownPoint } = usePointCache();
20− // if the actual set of points changes, sync the whole array
21+ return syncKnownPoint;
21− // TODO: we should get clever about caching
22− points.forEach(point => syncForeignPoint(point));
23− }
24− }, [syncForeignPoint, prevPoints, points]);
25−}
22+});
26
23
27−// sync an owned point to the cache
24+export const useSyncForeignPoints = buildSyncHook(function() {
28−export function useSyncOwnedPoints(points = []) {
29− const { syncOwnedPoint } = usePointCache();
25+ const { syncForeignPoint } = usePointCache();
30− const prevPoints = usePreviousValue(points);
26+ return syncForeignPoint;
27+});
31
28
32− useEffect(() => {
29+export const useSyncOwnedPoints = buildSyncHook(function() {
33− if (!isEqual(prevPoints, points)) {
30+ const { syncOwnedPoint } = usePointCache();
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]);
31+ return syncOwnedPoint;
39−}
32+});
src/lib/validators.js+37−7
@@ -1,7 +1,9 @@
1import * as bip39 from 'bip39';
1import * as bip39 from 'bip39';
2import * as ob from 'urbit-ob';
2import * as ob from 'urbit-ob';
3+import { identity } from 'lodash';
3
4
4import { isValidAddress } from './wallet';
5import { isValidAddress } from './wallet';
6+import patp2dec from './patp2dec';
5
7
6// via: https://emailregex.com/
8// via: https://emailregex.com/
7const 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,}))$/;
9const 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,}))$/;
@@ -10,19 +12,25 @@ const emailRegExp = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))
10const hexRegExp = /[0-9A-Fa-f]{64}/g;
12const hexRegExp = /[0-9A-Fa-f]{64}/g;
11
13
12// Wraps single validation functions in a controlled and predictable way.
14// Wraps single validation functions in a controlled and predictable way.
13−export const simpleValidatorWrapper = config => {
15+export const simpleValidatorWrapper = ({
16+ prevMessage,
17+ transform = identity,
18+ validator,
19+ error,
20+}) => {
14 // If a previous validation has already failed, skip this validation and
21 // If a previous validation has already failed, skip this validation and
15 // return the prev message to the next stage in the validation function chain.
22 // return the prev message to the next stage in the validation function chain.
16 // Failed validations should drop all the way down the chain and drop out of
23 // Failed validations should drop all the way down the chain and drop out of
17 // the output.
24 // the output.
18− if (!config.prevMessage.pass) {
25+ if (!prevMessage.pass) {
19− return config.prevMessage;
26+ return prevMessage;
20 }
27 }
21
28
22 // Run the validator and return the result.
29 // Run the validator and return the result.
23− return config.validator(config.prevMessage.data)
30+ const pass = validator(prevMessage.data);
24− ? newMessage(config.prevMessage.data, true, null)
31+ const data = pass ? transform(prevMessage.data) : prevMessage.data;
32+
25− : newMessage(config.prevMessage.data, false, config.error);
33+ return newMessage(data, pass, !pass && error);
26};
34};
27
35
28// Validation message
36// Validation message
@@ -73,7 +81,7 @@ export const validateGalaxy = m =>
73 validator: d => {
81 validator: d => {
74 let point;
82 let point;
75 try {
83 try {
76− point = parseInt(ob.patp2dec(d), 10);
84+ point = patp2dec(d);
77 return point >= 0 && point < 256;
85 return point >= 0 && point < 256;
78 } catch (e) {
86 } catch (e) {
79 return false;
87 return false;
@@ -162,3 +170,25 @@ export const validateEmail = m =>
162 validator: d => emailRegExp.test(d),
170 validator: d => emailRegExp.test(d),
163 error: 'This is not a valid email address',
171 error: 'This is not a valid email address',
164 });
172 });
173+
174+export const validateInt = m =>
175+ simpleValidatorWrapper({
176+ prevMessage: m,
177+ transform: d => parseInt(d, 10),
178+ validator: d => {
179+ try {
180+ parseInt(d, 10);
181+ return;
182+ } catch {
183+ return false;
184+ }
185+ },
186+ error: 'This is not a valid number',
187+ });
188+
189+export const validateExactly = (value, error) => m =>
190+ simpleValidatorWrapper({
191+ prevMessage: m,
192+ validator: d => d === value,
193+ error,
194+ });
src/lib/walletgen.js+6−1
@@ -10,6 +10,7 @@ import {
10 GALAXY_ENTROPY_BITS,
10 GALAXY_ENTROPY_BITS,
11 STAR_ENTROPY_BITS,
11 STAR_ENTROPY_BITS,
12 PLANET_ENTROPY_BITS,
12 PLANET_ENTROPY_BITS,
13+ ZOD,
13} from './constants';
14} from './constants';
14
15
15const SEED_LENGTH_BYTES = SEED_ENTROPY_BITS / 8;
16const SEED_LENGTH_BYTES = SEED_ENTROPY_BITS / 8;
@@ -66,9 +67,13 @@ export const generateWallet = async (point, ticket, boot) => {
66export const generateOwnershipWallet = (ship, ticket) =>
67export const generateOwnershipWallet = (ship, ticket) =>
67 kg.generateOwnershipWallet({ ship, ticket });
68 kg.generateOwnershipWallet({ ship, ticket });
68
69
70+// temporary wallets need to be derivable from just the ticket,
71+// so we always use ~zod as the wallet point.
72+export const generateTemporaryOwnershipWallet = ticket =>
73+ generateOwnershipWallet(ZOD, ticket);
69export const generateTemporaryTicketAndWallet = async point => {
74export const generateTemporaryTicketAndWallet = async point => {
70 const ticket = await makeTicket(point);
75 const ticket = await makeTicket(point);
71− const owner = await generateOwnershipWallet(0, ticket);
76+ const owner = await generateOwnershipWallet(ZOD, ticket);
72
77
73 return { ticket, owner };
78 return { ticket, owner };
74};
79};
src/store/Provider.js+2
@@ -8,6 +8,7 @@ import { WalletProvider } from './wallet';
8import { PointCursorProvider } from './pointCursor';
8import { PointCursorProvider } from './pointCursor';
9import { PointCacheProvider } from './pointCache';
9import { PointCacheProvider } from './pointCache';
10import { TxnCursorProvider } from './txnCursor';
10import { TxnCursorProvider } from './txnCursor';
11+import { MediaQueryProvider } from 'lib/MediaQuery';
11
12
12// nest all of the providers within each other to avoid hella depth
13// nest all of the providers within each other to avoid hella depth
13export default nest([
14export default nest([
@@ -19,4 +20,5 @@ export default nest([
19 PointCursorProvider,
20 PointCursorProvider,
20 PointCacheProvider,
21 PointCacheProvider,
21 TxnCursorProvider,
22 TxnCursorProvider,
23+ MediaQueryProvider,
22]);
24]);
src/store/lib/useBirthdaysStore.js+6
@@ -45,6 +45,12 @@ export default function useBirthdayStore() {
45 addToBirthdayCache({
45 addToBirthdayCache({
46 [point]: Maybe.Just(new Date(block.timestamp * 1000)),
46 [point]: Maybe.Just(new Date(block.timestamp * 1000)),
47 });
47 });
48+ } else {
49+ // TODO: better encoding for "no birthday" state?
50+ // if there's no birthday, just use today
51+ addToBirthdayCache({
52+ [point]: Maybe.Just(new Date()),
53+ });
48 }
54 }
49 },
55 },
50 [contracts, web3, addToBirthdayCache, getBirthday]
56 [contracts, web3, addToBirthdayCache, getBirthday]
src/store/lib/useControlledPointsStore.js+3−3
@@ -5,7 +5,7 @@ import * as azimuth from 'azimuth-js';
5import { useNetwork } from '../network';
5import { useNetwork } from '../network';
6import { useWallet } from 'store/wallet';
6import { useWallet } from 'store/wallet';
7
7
8−const kEmptyControlledPoints = {
8+const EMPTY_CONTROLLED_POINTS = {
9 ownedPoints: Maybe.Nothing(),
9 ownedPoints: Maybe.Nothing(),
10 incomingPoints: Maybe.Nothing(),
10 incomingPoints: Maybe.Nothing(),
11 managingPoints: Maybe.Nothing(),
11 managingPoints: Maybe.Nothing(),
@@ -17,11 +17,11 @@ export default function useControlledPointsStore() {
17 const { contracts } = useNetwork();
17 const { contracts } = useNetwork();
18 const { wallet } = useWallet();
18 const { wallet } = useWallet();
19 const [controlledPointsCache, _setControlledPointsCache] = useState(
19 const [controlledPointsCache, _setControlledPointsCache] = useState(
20− kEmptyControlledPoints
20+ EMPTY_CONTROLLED_POINTS
21 );
21 );
22
22
23 const getControlledPoints = useCallback(
23 const getControlledPoints = useCallback(
24− () => controlledPointsCache || kEmptyControlledPoints,
24+ () => controlledPointsCache || EMPTY_CONTROLLED_POINTS,
25 [controlledPointsCache]
25 [controlledPointsCache]
26 );
26 );
27
27
src/store/lib/useInvitesStore.js+2−2
@@ -5,7 +5,7 @@ import * as azimuth from 'azimuth-js';
5import { useNetwork } from '../network';
5import { useNetwork } from '../network';
6
6
7// the default value of a point's invites
7// the default value of a point's invites
8−const kEmptyInvites = {
8+const EMPTY_INVITES = {
9 availableInvites: Maybe.Nothing(),
9 availableInvites: Maybe.Nothing(),
10 sentInvites: Maybe.Nothing(),
10 sentInvites: Maybe.Nothing(),
11 acceptedInvites: Maybe.Nothing(),
11 acceptedInvites: Maybe.Nothing(),
@@ -25,7 +25,7 @@ export default function useInvitesStore() {
25 );
25 );
26
26
27 const getInvites = useCallback(
27 const getInvites = useCallback(
28− point => invitesCache[point] || kEmptyInvites,
28+ point => invitesCache[point] || EMPTY_INVITES,
29 [invitesCache]
29 [invitesCache]
30 );
30 );
31
31
src/store/lib/usePointStore.js+9−2
@@ -16,6 +16,12 @@ export default function usePointStore() {
16 ...controlledPoints
16 ...controlledPoints
17 } = useControlledPointsStore();
17 } = useControlledPointsStore();
18
18
19+ // sync all of the on-chain info required to display a known point
20+ const syncKnownPoint = useCallback(
21+ async point => Promise.all([syncBirthday(point)]),
22+ [syncBirthday]
23+ );
24+
19 // sync all of the on-chain info required to display a foreign point
25 // sync all of the on-chain info required to display a foreign point
20 const syncForeignPoint = useCallback(async point => Promise.all([]), []);
26 const syncForeignPoint = useCallback(async point => Promise.all([]), []);
21
27
@@ -23,11 +29,11 @@ export default function usePointStore() {
23 const syncOwnedPoint = useCallback(
29 const syncOwnedPoint = useCallback(
24 async point =>
30 async point =>
25 Promise.all([
31 Promise.all([
32+ syncKnownPoint(point),
26 syncDetails(point),
33 syncDetails(point),
27− syncBirthday(point),
28 syncInvites(point),
34 syncInvites(point),
29 ]),
35 ]),
30− [syncDetails, syncBirthday, syncInvites]
36+ [syncKnownPoint, syncDetails, syncInvites]
31 );
37 );
32
38
33 return {
39 return {
@@ -38,6 +44,7 @@ export default function usePointStore() {
38 ...ecliptic,
44 ...ecliptic,
39 syncInvites,
45 syncInvites,
40 syncControlledPoints,
46 syncControlledPoints,
47+ syncKnownPoint,
41 syncOwnedPoint,
48 syncOwnedPoint,
42 syncForeignPoint,
49 syncForeignPoint,
43 };
50 };
src/store/wallet.js+3−9
@@ -18,7 +18,7 @@ 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;
21+const DEFAULT_WALLET_TYPE = WALLET_TYPES.TICKET;
22
22
23// NB (jtobin):
23// NB (jtobin):
24//
24//
@@ -36,7 +36,7 @@ function _useWallet(
36 initialWallet = Maybe.Nothing(),
36 initialWallet = Maybe.Nothing(),
37 initialMnemonic = Maybe.Nothing()
37 initialMnemonic = Maybe.Nothing()
38) {
38) {
39− const [walletType, _setWalletType] = useState(kDefaultWalletType);
39+ const [walletType, _setWalletType] = useState(DEFAULT_WALLET_TYPE);
40 const [walletHdPath, setWalletHdPath] = useState(DEFAULT_HD_PATH);
40 const [walletHdPath, setWalletHdPath] = useState(DEFAULT_HD_PATH);
41 const [wallet, _setWallet] = useState(initialWallet);
41 const [wallet, _setWallet] = useState(initialWallet);
42 const [urbitWallet, _setUrbitWallet] = useState(Maybe.Nothing());
42 const [urbitWallet, _setUrbitWallet] = useState(Maybe.Nothing());
@@ -79,12 +79,6 @@ function _useWallet(
79 urbitWallet.value.meta.passphrase
79 urbitWallet.value.meta.passphrase
80 );
80 );
81
81
82− // manually set the public address
83− wallet.map(wal => {
84− wal.address = urbitWallet.value.ownership.keys.address;
85− return wal;
86− });
87−
88 setWallet(wallet);
82 setWallet(wallet);
89 } else {
83 } else {
90 setWallet(Maybe.Nothing());
84 setWallet(Maybe.Nothing());
@@ -96,7 +90,7 @@ function _useWallet(
96 );
90 );
97
91
98 const resetWallet = useCallback(() => {
92 const resetWallet = useCallback(() => {
99− _setWalletType(kDefaultWalletType);
93+ _setWalletType(DEFAULT_WALLET_TYPE);
100 setWalletHdPath(DEFAULT_HD_PATH);
94 setWalletHdPath(DEFAULT_HD_PATH);
101 _setWallet(Maybe.Nothing());
95 _setWallet(Maybe.Nothing());
102 _setUrbitWallet(Maybe.Nothing());
96 _setUrbitWallet(Maybe.Nothing());
src/style/indigo-proposals.scss+8
@@ -31,3 +31,11 @@
31.outline-none {
31.outline-none {
32 outline: none;
32 outline: none;
33}
33}
34+
35+.us-none {
36+ user-select: none;
37+}
38+
39+.auto-rows-min {
40+ grid-auto-rows: min-content;
41+}
src/views/AcceptTransfer.js+2−2
@@ -25,7 +25,7 @@ class AcceptTransfer extends React.Component {
25 super(props);
25 super(props);
26
26
27 const receivingAddress = need.addressFromWallet(props.wallet);
27 const receivingAddress = need.addressFromWallet(props.wallet);
28− const incomingPoint = need.pointCursor(props.pointCursor);
28+ const incomingPoint = need.point(props.pointCursor);
29
29
30 this.state = {
30 this.state = {
31 receivingAddress: receivingAddress,
31 receivingAddress: receivingAddress,
@@ -46,7 +46,7 @@ class AcceptTransfer extends React.Component {
46 const { state, props } = this;
46 const { state, props } = this;
47
47
48 const validContracts = need.contracts(props.contracts);
48 const validContracts = need.contracts(props.contracts);
49− const validPoint = need.pointCursor(props.pointCursor);
49+ const validPoint = need.point(props.pointCursor);
50 //TODO state.incomingPoint ?
50 //TODO state.incomingPoint ?
51
51
52 return Just(
52 return Just(
src/views/Activate.jsnew file+39
@@ -0,0 +1,39 @@
1+import React from 'react';
2+
3+import useRouter from 'lib/useRouter';
4+
5+import { ActivateFlowProvider } from './Activate/ActivateFlow';
6+import useActivateFlowState from './Activate/useActivateFlowState';
7+import ActivateCode from './Activate/ActivateCode';
8+import ActivateDisclaimer from './Activate/ActivateDisclaimer';
9+import { LocalRouterProvider } from 'lib/LocalRouter';
10+import ActivatePassport from './Activate/ActivatePassport';
11+
12+const NAMES = {
13+ CODE: 'CODE',
14+ DISCLAIMER: 'DISCLAIMER',
15+ PASSPORT: 'PASSPORT',
16+};
17+
18+const VIEWS = {
19+ [NAMES.CODE]: ActivateCode,
20+ [NAMES.DISCLAIMER]: ActivateDisclaimer,
21+ [NAMES.PASSPORT]: ActivatePassport,
22+};
23+
24+export default function Activate() {
25+ const state = useActivateFlowState();
26+ const { Route, ...router } = useRouter({
27+ names: NAMES,
28+ views: VIEWS,
29+ initialRoutes: [{ key: NAMES.CODE }],
30+ });
31+
32+ return (
33+ <LocalRouterProvider value={router}>
34+ <ActivateFlowProvider value={state}>
35+ <Route />
36+ </ActivateFlowProvider>
37+ </LocalRouterProvider>
38+ );
39+}
src/views/Activate/ActivateCode.jsnew file+150
@@ -0,0 +1,150 @@
1+import React, { useCallback, useEffect, useState } from 'react';
2+import * as azimuth from 'azimuth-js';
3+import Maybe from 'folktale/maybe';
4+import { useTicketInput } from 'components/Inputs';
5+import { Grid, Input, H4, ErrorText } from 'indigo-react';
6+
7+import View from 'components/View';
8+import { ForwardButton } from 'components/Buttons';
9+import Passport from 'components/Passport';
10+
11+import { useHistory } from 'store/history';
12+
13+import * as need from 'lib/need';
14+import { ROUTE_NAMES } from 'lib/routeNames';
15+import { useSyncKnownPoints } from 'lib/useSyncPoints';
16+import FooterButton from 'components/FooterButton';
17+import Blinky from 'components/Blinky';
18+import { DEFAULT_HD_PATH, walletFromMnemonic } from 'lib/wallet';
19+import { useNetwork } from 'store/network';
20+import { generateWallet } from 'lib/invite';
21+import { generateTemporaryOwnershipWallet } from 'lib/walletgen';
22+import { useActivateFlow } from './ActivateFlow';
23+import { useLocalRouter } from 'lib/LocalRouter';
24+
25+export default function ActivateCode() {
26+ const history = useHistory();
27+ const { names, push } = useLocalRouter();
28+ const { contracts } = useNetwork();
29+ const [generalError, setGeneralError] = useState();
30+ const [deriving, setDeriving] = useState(false);
31+ const {
32+ derivedWallet,
33+ setDerivedWallet,
34+ setInviteWallet,
35+ derivedPoint,
36+ setDerivedPoint,
37+ } = useActivateFlow();
38+
39+ const ticketInput = useTicketInput({
40+ name: 'ticket',
41+ label: 'Code',
42+ autoFocus: true,
43+ });
44+ const validTicket = ticketInput.pass;
45+
46+ const goToLogin = useCallback(() => history.popAndPush(ROUTE_NAMES.LOGIN), [
47+ history,
48+ ]);
49+ const goToDisclaimer = useCallback(() => push(names.DISCLAIMER), [
50+ names,
51+ push,
52+ ]);
53+
54+ const pass = derivedWallet.matchWith({
55+ Nothing: () => false,
56+ Just: () => true,
57+ });
58+
59+ useEffect(() => {
60+ if (validTicket) {
61+ const _contracts = need.contracts(contracts);
62+
63+ setDeriving(true);
64+ // when the ticket becomes valid, derive the point
65+ (async () => {
66+ const { seed } = await generateTemporaryOwnershipWallet(
67+ ticketInput.data
68+ );
69+
70+ //TODO isn't all this accessible in the ownership object?
71+ const inviteWallet = walletFromMnemonic(seed, DEFAULT_HD_PATH);
72+ const _inviteWallet = need.wallet(inviteWallet);
73+
74+ const incoming = await azimuth.azimuth.getTransferringFor(
75+ _contracts,
76+ _inviteWallet.address
77+ );
78+
79+ let realPoint = Maybe.Nothing();
80+ let wallet = Maybe.Nothing();
81+
82+ if (incoming.length > 0) {
83+ const pointNum = parseInt(incoming[0], 10);
84+ realPoint = Maybe.Just(pointNum);
85+ wallet = Maybe.Just(await generateWallet(pointNum));
86+
87+ if (incoming.length > 1) {
88+ setGeneralError(
89+ 'This invite code has multiple points available.\n' +
90+ "Once you've activated this point, activate the next with the same process."
91+ );
92+ }
93+ } else {
94+ setGeneralError(
95+ 'Invite code has no claimable point.\n' +
96+ 'Check your invite code and try again?'
97+ );
98+ }
99+
100+ setDerivedPoint(realPoint);
101+ setDerivedWallet(wallet);
102+ setInviteWallet(inviteWallet);
103+ setDeriving(false);
104+ })();
105+ } else {
106+ setGeneralError(false);
107+ }
108+ }, [
109+ validTicket,
110+ contracts,
111+ ticketInput.data,
112+ setDerivedPoint,
113+ setDerivedWallet,
114+ setInviteWallet,
115+ ]);
116+
117+ // when we know the derived point, ensure we have the data to display it
118+ useSyncKnownPoints([derivedPoint.getOrElse(null)].filter(p => p !== null));
119+
120+ return (
121+ <View>
122+ <Grid>
123+ <Grid.Item as={Passport} point={derivedPoint} full />
124+ <Grid.Item as={H4} className="mt3 mb2" full>
125+ Activate
126+ </Grid.Item>
127+ <Grid.Item as={Input} {...ticketInput} full />
128+ {generalError && (
129+ <Grid.Item full>
130+ <ErrorText>{generalError}</ErrorText>{' '}
131+ </Grid.Item>
132+ )}
133+ <Grid.Item
134+ as={ForwardButton}
135+ className="mt4"
136+ disabled={!pass || deriving}
137+ accessory={deriving ? <Blinky /> : undefined}
138+ onClick={goToDisclaimer}
139+ solid
140+ full>
141+ {deriving && 'Deriving...'}
142+ {!deriving && 'Go'}
143+ </Grid.Item>
144+ </Grid>
145+ <FooterButton as={ForwardButton} onClick={goToLogin}>
146+ Login
147+ </FooterButton>
148+ </View>
149+ );
150+}
src/views/Activate/ActivateDisclaimer.jsnew file+85
@@ -0,0 +1,85 @@
1+import React, { useCallback } from 'react';
2+import cn from 'classnames';
3+import { Grid, H3, B, Text, CheckboxInput } from 'indigo-react';
4+
5+import View from 'components/View';
6+import { useCheckboxInput } from 'components/Inputs';
7+import { ForwardButton } from 'components/Buttons';
8+import { useLocalRouter } from 'lib/LocalRouter';
9+import WarningBox from 'components/WarningBox';
10+
11+const TEXT_STYLE = 'f5';
12+
13+export default function ActivateDisclaimer() {
14+ const { push, names } = useLocalRouter();
15+ const checkboxInput = useCheckboxInput({
16+ name: 'checkbox',
17+ label: 'I acknowledge and understand these rights',
18+ });
19+
20+ const goToPassport = useCallback(() => push(names.PASSPORT), [push, names]);
21+
22+ return (
23+ <View>
24+ <Grid gap={2} className="mt8 mb10">
25+ <Grid.Item as={H3} full>
26+ Disclaimer
27+ </Grid.Item>
28+ <Grid.Item full>
29+ <Text className={cn(TEXT_STYLE, 'block mb4')}>
30+ Welcome to Bridge. Please read this information. Your assets could
31+ be stolen if you do not pay attention to these warnings.
32+ </Text>
33+
34+ <Text className={cn(TEXT_STYLE, 'block mb4')}>
35+ <B>You own and control your assets. We do not.</B> If you send your
36+ assets to another address, we can’t get them back for you.
37+ </Text>
38+
39+ <B className={TEXT_STYLE}>Please understand that nobody can:</B>
40+ <Text className={cn(TEXT_STYLE, 'block mb4')}>
41+ 1. Access your assets for you <br />
42+ 2. Recover, reset, or modify ANY of your information <br />
43+ 3. Reverse, cancel, or refund transactions <br />
44+ 4. Hold, back-up, or access your keys
45+ </Text>
46+
47+ <B className={TEXT_STYLE}>
48+ You’re responsible for keeping your information safe. This includes:
49+ </B>
50+ <Text className={cn(TEXT_STYLE, 'block mb4')}>
51+ 1. Private Keys/Mnemonic Phrases and passwords <br />
52+ 2. JSON files <br />
53+ 3. Hardware wallet PINs; <br />
54+ 4. Downloading this software from a reliable site, and ensuring it
55+ has not been tampered with. [add something about using the checksum
56+ here]
57+ </Text>
58+
59+ <B className={TEXT_STYLE}>To be clear</B>
60+ <Text className={cn(TEXT_STYLE, 'block')}>
61+ This software is provided “as is” without warranties of any kind and
62+ our liability to you in connection with the same is limited;
63+ </Text>
64+ <Text className={cn(TEXT_STYLE, 'block')}>
65+ We have no liability for any security problems or incidents that you
66+ may experience, including any loss or theft of your keys or any
67+ problems that may arise in connection with your wallet.
68+ </Text>
69+ </Grid.Item>
70+ <Grid.Item full as={WarningBox}>
71+ Warning: Nobody but you can restore or reset your Master Ticket
72+ </Grid.Item>
73+ <Grid.Item as={CheckboxInput} {...checkboxInput} full />
74+ <Grid.Item
75+ as={ForwardButton}
76+ disabled={!checkboxInput.data}
77+ onClick={goToPassport}
78+ solid
79+ full>
80+ Continue
81+ </Grid.Item>
82+ </Grid>
83+ </View>
84+ );
85+}
src/views/Activate/ActivateFlow.jsnew file+9
@@ -0,0 +1,9 @@
1+import { createContext, useContext } from 'react';
2+
3+export const ActivateFlowContext = createContext(null);
4+export const ActivateFlowProvider = ActivateFlowContext.Provider;
5+
6+// Hook version
7+export function useActivateFlow() {
8+ return useContext(ActivateFlowContext);
9+}
src/views/Activate/ActivatePassport.jsnew file+90
@@ -0,0 +1,90 @@
1+import React, { useMemo } from 'react';
2+import cn from 'classnames';
3+import Maybe from 'folktale/maybe';
4+import { Grid } from 'indigo-react';
5+
6+import { LocalRouterProvider, useLocalRouter } from 'lib/LocalRouter';
7+
8+import View from 'components/View';
9+import Passport from 'components/Passport';
10+
11+import { useActivateFlow } from './ActivateFlow';
12+import useBreakpoints from 'lib/useBreakpoints';
13+import useRouter from 'lib/useRouter';
14+
15+import PassportDownload from './PassportDownload';
16+import PassportVerify from './PassportVerify';
17+import PassportTransfer from './PassportTransfer';
18+import {
19+ walletFromMnemonic,
20+ DEFAULT_HD_PATH,
21+ addressFromSecp256k1Public,
22+} from 'lib/wallet';
23+
24+const NAMES = {
25+ DOWNLOAD: 'DOWNLOAD',
26+ VERIFY: 'VERIFY',
27+ TRANSFER: 'TRANSFER',
28+};
29+
30+const VIEWS = {
31+ [NAMES.DOWNLOAD]: PassportDownload,
32+ [NAMES.VERIFY]: PassportVerify,
33+ [NAMES.TRANSFER]: PassportTransfer,
34+};
35+
36+export default function ActivatePassport() {
37+ const { derivedPoint, generated, derivedWallet } = useActivateFlow();
38+ // pull the reset function out of the 'activate' router
39+ const { reset: resetActivateRouter } = useLocalRouter();
40+ const { Route, ...router } = useRouter({
41+ names: NAMES,
42+ views: VIEWS,
43+ initialRoutes: [{ key: NAMES.DOWNLOAD }],
44+ });
45+
46+ const fullView = useBreakpoints([false, false, true]);
47+ const gap = useBreakpoints([4, 4, 7]);
48+ const marginTop = useBreakpoints([false, false, 8]);
49+ const full = useBreakpoints([true, true, false]);
50+ const leftHalf = useBreakpoints([false, false, 1]);
51+ const rightHalf = useBreakpoints([false, false, 2]);
52+
53+ // addresses are not derived until we set[Urbit]Wallet(), so do that inline
54+ const address = useMemo(
55+ () =>
56+ derivedWallet
57+ .chain(wallet =>
58+ walletFromMnemonic(
59+ wallet.ownership.seed,
60+ DEFAULT_HD_PATH,
61+ wallet.meta.passphrase
62+ )
63+ )
64+ .map(wallet => addressFromSecp256k1Public(wallet.publicKey)),
65+ [derivedWallet]
66+ );
67+
68+ return (
69+ <LocalRouterProvider value={router}>
70+ <View full={fullView}>
71+ <Grid gap={gap} className="mt8 mb10">
72+ <Grid.Item half={leftHalf} full={full}>
73+ <Passport
74+ className={cn({ [`mt${marginTop}`]: marginTop })}
75+ point={derivedPoint}
76+ ticket={generated}
77+ address={address}
78+ />
79+ </Grid.Item>
80+ <Grid.Item
81+ half={rightHalf}
82+ full={full}
83+ as={Route}
84+ resetActivateRouter={resetActivateRouter}
85+ />
86+ </Grid>
87+ </View>
88+ </LocalRouterProvider>
89+ );
90+}
src/views/Activate/PassportDownload.jsnew file+87
@@ -0,0 +1,87 @@
1+import React, { useState, useEffect, useCallback } from 'react';
2+import cn from 'classnames';
3+import Maybe from 'folktale/maybe';
4+import { Grid, H4, P } from 'indigo-react';
5+import PaperCollateralRenderer from 'PaperCollateralRenderer';
6+
7+import * as need from 'lib/need';
8+
9+import { useActivateFlow } from './ActivateFlow';
10+import { downloadWallet } from 'lib/invite';
11+import { DownloadButton, ForwardButton } from 'components/Buttons';
12+import { useLocalRouter } from 'lib/LocalRouter';
13+import Steps from 'components/Steps';
14+
15+export default function PassportDownload({ className }) {
16+ const {
17+ derivedPoint,
18+ derivedWallet,
19+ generated,
20+ setGenerated,
21+ } = useActivateFlow();
22+ const { push, names } = useLocalRouter();
23+ // const point = need.point(derivedPoint);
24+ const wallet = need.wallet(derivedWallet);
25+
26+ const [paper, setPaper] = useState(Maybe.Nothing());
27+ const [downloaded, setDownloaded] = useState(false);
28+
29+ const pointAsString = derivedPoint.matchWith({
30+ Nothing: () => '',
31+ Just: p => p.value.toFixed(),
32+ });
33+
34+ const download = useCallback(() => {
35+ downloadWallet(paper.getOrElse([]));
36+ setDownloaded(true);
37+ }, [paper, setDownloaded]);
38+
39+ // sync paper value to activation state
40+ useEffect(
41+ () =>
42+ setGenerated(
43+ paper.matchWith({
44+ Nothing: () => false,
45+ Just: () => true,
46+ })
47+ ),
48+ [paper, setGenerated]
49+ );
50+
51+ const goToVerify = useCallback(() => push(names.VERIFY), [push, names]);
52+
53+ return (
54+ <Grid className={cn(className, 'auto-rows-min')}>
55+ <Grid.Item full as={Steps} num={1} total={3} />
56+ <Grid.Item full as={H4}>
57+ Passport
58+ </Grid.Item>
59+ <Grid.Item full>
60+ <P>
61+ After you’ve downloaded your passport, back up the ticket manually or
62+ store on a trusted device.
63+ </P>
64+ <P>
65+ What is digital identity? A passport is your digital identity. You
66+ will use your passport to access your true computer, send payments,
67+ and administer your identity. So naturally, you must keep this secure.
68+ </P>
69+ </Grid.Item>
70+ <Grid.Item
71+ full
72+ as={!downloaded ? DownloadButton : ForwardButton}
73+ disabled={!generated}
74+ onClick={!downloaded ? download : goToVerify}
75+ success={downloaded}
76+ solid>
77+ {!downloaded ? 'Download Passport' : 'Continue'}
78+ </Grid.Item>
79+ <PaperCollateralRenderer
80+ className="abs super-hidden"
81+ wallet={{ [pointAsString]: wallet }}
82+ callback={data => setPaper(Maybe.Just(data))}
83+ mode="REGISTRATION"
84+ />
85+ </Grid>
86+ );
87+}
src/views/Activate/PassportTransfer.jsnew file+191
@@ -0,0 +1,191 @@
1+import React, { useState, useCallback } from 'react';
2+import cn from 'classnames';
3+import { Grid, H4, Text, ErrorText } from 'indigo-react';
4+
5+import { useNetwork } from 'store/network';
6+import { useWallet } from 'store/wallet';
7+import { useHistory } from 'store/history';
8+import { usePointCursor } from 'store/pointCursor';
9+
10+import * as need from 'lib/need';
11+import useLifecycle from 'lib/useLifecycle';
12+import { claimPointFromInvite } from 'lib/invite';
13+import timeout from 'lib/timeout';
14+import { fromWei } from 'lib/txn';
15+
16+import Steps from 'components/Steps';
17+import WarningBox from 'components/WarningBox';
18+import LoadingBar from 'components/LoadingBar';
19+import Highlighted from 'components/Highlighted';
20+
21+import { useActivateFlow } from './ActivateFlow';
22+import { RestartButton } from 'components/Buttons';
23+
24+export default function PassportTransfer({ className, resetActivateRouter }) {
25+ const { replaceWith, names } = useHistory();
26+ const { setUrbitWallet } = useWallet();
27+ const { setPointCursor } = usePointCursor();
28+ const { web3, contracts } = useNetwork();
29+ const {
30+ derivedWallet,
31+ derivedPoint,
32+ inviteWallet,
33+ reset: resetActivateFlow,
34+ } = useActivateFlow();
35+ const [generalError, setGeneralError] = useState();
36+ const [{ label, progress }, setState] = useState({
37+ label: 'Verify Passport',
38+ progress: 0,
39+ });
40+ const [needFunds, setNeedFunds] = useState();
41+
42+ const goToLogin = useCallback(
43+ () =>
44+ replaceWith([
45+ { key: names.LANDING },
46+ { key: names.LOGIN },
47+ { key: names.POINTS },
48+ { key: names.POINT },
49+ ]),
50+ [replaceWith, names]
51+ );
52+
53+ const goToRestart = useCallback(() => {
54+ // NOTE: because we're already on the ACTIVATE view in the history,
55+ // react (intelligently) doesn't trigger a re-render and that means the
56+ // current state of the two nested routers (activate & passport) stick
57+ // around unless we manually clear them.
58+ // we only need to clear activate because that will unmount the passport
59+ // router (which will be set to initialRoutes when mounted again)
60+
61+ // 1) replace history
62+ replaceWith([{ key: names.LANDING }, { key: names.ACTIVATE }]);
63+ // 2) reset local router
64+ resetActivateRouter();
65+ // 3) clear the state
66+ resetActivateFlow();
67+ }, [replaceWith, names, resetActivateRouter, resetActivateFlow]);
68+
69+ const handleUpdate = useCallback(
70+ ({ type, state, value }) => {
71+ switch (type) {
72+ case 'progress':
73+ return setState(state);
74+ case 'askFunding':
75+ return setNeedFunds(value);
76+ case 'gotFunding':
77+ return setNeedFunds(false);
78+ default:
79+ console.error(`Unknown update: ${type}`);
80+ }
81+ },
82+ [setState, setNeedFunds]
83+ );
84+
85+ const claimPoint = useCallback(async () => {
86+ setGeneralError(false);
87+
88+ try {
89+ const _inviteWallet = need.wallet(inviteWallet);
90+ const _wallet = need.wallet(derivedWallet);
91+ const _point = need.point(derivedPoint);
92+ const _web3 = need.web3(web3);
93+ const _contracts = need.contracts(contracts);
94+
95+ await claimPointFromInvite({
96+ inviteWallet: _inviteWallet,
97+ wallet: _wallet,
98+ point: _point,
99+ web3: _web3,
100+ contracts: _contracts,
101+ onUpdate: handleUpdate,
102+ });
103+
104+ // set the global wallet
105+ setUrbitWallet(derivedWallet);
106+ setPointCursor(derivedPoint);
107+
108+ // and redirect to login
109+ await timeout(3000);
110+ goToLogin();
111+ } catch (error) {
112+ // some generic error
113+ setGeneralError(error);
114+ }
115+ }, [
116+ inviteWallet,
117+ derivedWallet,
118+ derivedPoint,
119+ web3,
120+ contracts,
121+ setUrbitWallet,
122+ setPointCursor,
123+ handleUpdate,
124+ goToLogin,
125+ ]);
126+
127+ useLifecycle(() => {
128+ claimPoint();
129+ });
130+
131+ const renderAdditionalInfo = () => {
132+ if (generalError) {
133+ return (
134+ <>
135+ <Grid.Item full className="mt8">
136+ <ErrorText>{generalError.message.toString()}</ErrorText>
137+ </Grid.Item>
138+ <Grid.Item
139+ full
140+ className="mt3"
141+ as={RestartButton}
142+ onClick={goToRestart}
143+ />
144+ </>
145+ );
146+ }
147+
148+ if (needFunds) {
149+ return (
150+ <Grid.Item full className="mt8">
151+ <Highlighted>
152+ The address {needFunds.address} needs at least{' '}
153+ {fromWei(needFunds.minBalance)} ETH and currently has{' '}
154+ {fromWei(needFunds.balance)} ETH. Waiting until the account has
155+ enough funds.
156+ </Highlighted>
157+ </Grid.Item>
158+ );
159+ }
160+
161+ if (progress < 100) {
162+ return (
163+ <Grid.Item full as={WarningBox} className="mt8">
164+ Never give your Master Ticket to anyone
165+ </Grid.Item>
166+ );
167+ }
168+
169+ return null;
170+ };
171+
172+ return (
173+ <Grid className={cn(className, 'auto-rows-min')}>
174+ <Grid.Item full as={Steps} num={3} total={3} />
175+ <Grid.Item full as={H4}>
176+ {label}
177+ </Grid.Item>
178+ <Grid.Item full as={Grid} className="mt3" gap={3}>
179+ <Grid.Item full as={LoadingBar} progress={progress} />
180+ <Grid.Item full>
181+ <Text className="f5 green4">
182+ This process can take up to 5 minutes to complete. Don't leave this
183+ page until the process is complete.
184+ </Text>
185+ </Grid.Item>
186+ </Grid.Item>
187+
188+ {renderAdditionalInfo()}
189+ </Grid>
190+ );
191+}
src/views/Activate/PassportVerify.jsnew file+59
@@ -0,0 +1,59 @@
1+import React, { useCallback, useMemo } from 'react';
2+import cn from 'classnames';
3+import { Grid, H4, Input, P } from 'indigo-react';
4+
5+import * as need from 'lib/need';
6+import { useLocalRouter } from 'lib/LocalRouter';
7+
8+import { ForwardButton } from 'components/Buttons';
9+import Steps from 'components/Steps';
10+import { useTicketInput } from 'components/Inputs';
11+
12+import { useActivateFlow } from './ActivateFlow';
13+import { validateExactly } from 'lib/validators';
14+
15+const STUB_VERIFY_TICKET = process.env.NODE_ENV === 'development';
16+
17+export default function PassportVerify({ className }) {
18+ const { push, names } = useLocalRouter();
19+ const { derivedWallet } = useActivateFlow();
20+ const goToTransfer = useCallback(() => push(names.TRANSFER), [push, names]);
21+
22+ const { ticket } = need.wallet(derivedWallet);
23+ const validators = useMemo(
24+ () => [validateExactly(ticket, 'Does not match expected master ticket.')],
25+ [ticket]
26+ );
27+ const ticketInput = useTicketInput({
28+ name: 'ticket',
29+ label: 'Master Ticket',
30+ initialValue: STUB_VERIFY_TICKET ? ticket : undefined,
31+ autoFocus: true,
32+ validators,
33+ });
34+ const { pass } = ticketInput;
35+
36+ return (
37+ <Grid className={cn(className, 'auto-rows-min')}>
38+ <Grid.Item full as={Steps} num={2} total={3} />
39+ <Grid.Item full as={H4}>
40+ Verify Passport
41+ </Grid.Item>
42+ <Grid.Item full as={P}>
43+ After you download your passport, verify your custody. Your passport
44+ should be a folder of image files. One of them is your Master Ticket.
45+ Open it and enter the 4 word phrase below (with hyphens).
46+ </Grid.Item>
47+ <Grid.Item full as={Input} {...ticketInput} />
48+ <Grid.Item
49+ full
50+ className="mt3"
51+ as={ForwardButton}
52+ disabled={!pass}
53+ onClick={goToTransfer}
54+ solid>
55+ Verify
56+ </Grid.Item>
57+ </Grid>
58+ );
59+}
src/views/Activate/useActivateFlowState.jsnew file+28
@@ -0,0 +1,28 @@
1+import { useCallback, useState } from 'react';
2+import Maybe from 'folktale/maybe';
3+
4+export default function useActivateFlowState() {
5+ const [derivedWallet, setDerivedWallet] = useState(Maybe.Nothing());
6+ const [inviteWallet, setInviteWallet] = useState(Maybe.Nothing());
7+ const [derivedPoint, setDerivedPoint] = useState(Maybe.Nothing());
8+ const [generated, setGenerated] = useState(false);
9+
10+ const reset = useCallback(() => {
11+ setDerivedWallet(Maybe.Nothing());
12+ setInviteWallet(Maybe.Nothing());
13+ setDerivedPoint(Maybe.Nothing());
14+ setGenerated(false);
15+ }, [setDerivedWallet, setInviteWallet, setDerivedPoint, setGenerated]);
16+
17+ return {
18+ derivedWallet,
19+ setDerivedWallet,
20+ inviteWallet,
21+ setInviteWallet,
22+ derivedPoint,
23+ setDerivedPoint,
24+ generated,
25+ setGenerated,
26+ reset,
27+ };
28+}
src/views/CancelTransfer.js+2−2
@@ -17,7 +17,7 @@ class CancelTransfer extends React.Component {
17 constructor(props) {
17 constructor(props) {
18 super(props);
18 super(props);
19
19
20− const pointInTransfer = need.pointCursor(props.pointCursor);
20+ const pointInTransfer = need.point(props.pointCursor);
21
21
22 this.state = {
22 this.state = {
23 proxyAddress: '',
23 proxyAddress: '',
@@ -31,7 +31,7 @@ class CancelTransfer extends React.Component {
31 const { props } = this;
31 const { props } = this;
32
32
33 const validContracts = need.contracts(props.contracts);
33 const validContracts = need.contracts(props.contracts);
34− const validPoint = need.pointCursor(props.pointCursor);
34+ const validPoint = need.point(props.pointCursor);
35 //TODO this.state.pointInTransfer ?
35 //TODO this.state.pointInTransfer ?
36
36
37 const txn = azimuth.ecliptic.setTransferProxy(
37 const txn = azimuth.ecliptic.setTransferProxy(
src/views/CreateGalaxy.js+1−1
@@ -75,7 +75,7 @@ class CreateGalaxy extends React.Component {
75
75
76 const validContracts = need.contracts(props.contracts);
76 const validContracts = need.contracts(props.contracts);
77
77
78− const galaxyDec = parseInt(ob.patp2dec(state.galaxyName), 10);
78+ const galaxyDec = ob.patp2dec(state.galaxyName);
79
79
80 const txn = azimuth.ecliptic.createGalaxy(
80 const txn = azimuth.ecliptic.createGalaxy(
81 validContracts,
81 validContracts,
src/views/GenKeyfile.js+1−1
@@ -27,7 +27,7 @@ class GenKeyfile extends React.Component {
27 }
27 }
28
28
29 getPointDetails() {
29 getPointDetails() {
30− const point = need.pointCursor(this.props.pointCursor);
30+ const point = need.point(this.props.pointCursor);
31 const pointDetails = need.fromPointCache(this.props.pointCache, point);
31 const pointDetails = need.fromPointCache(this.props.pointCache, point);
32
32
33 // in case we did SetKeys earlier this session, make sure to generate the
33 // in case we did SetKeys earlier this session, make sure to generate the
src/views/Invite.js+7−7
@@ -19,12 +19,12 @@ import { LocalRouterProvider } from 'lib/LocalRouter';
19import InviteUrl from './Invite/InviteUrl';
19import InviteUrl from './Invite/InviteUrl';
20import InviteEmail from './Invite/InviteEmail';
20import InviteEmail from './Invite/InviteEmail';
21
21
22−const kInviteNames = {
22+const NAMES = {
23 URL: 'URL',
23 URL: 'URL',
24 EMAIL: 'EMAIL',
24 EMAIL: 'EMAIL',
25};
25};
26
26
27−const kInviteViews = {
27+const VIEWS = {
28 URL: InviteUrl,
28 URL: InviteUrl,
29 EMAIL: InviteEmail,
29 EMAIL: InviteEmail,
30};
30};
@@ -32,12 +32,12 @@ const kInviteViews = {
32export default function Invite() {
32export default function Invite() {
33 const history = useHistory();
33 const history = useHistory();
34 const { Route, ...router } = useRouter({
34 const { Route, ...router } = useRouter({
35− names: kInviteNames,
35+ names: NAMES,
36− views: kInviteViews,
36+ views: VIEWS,
37− initialRoutes: [{ key: kInviteNames.URL }],
37+ initialRoutes: [{ key: NAMES.URL }],
38 });
38 });
39 const { pointCursor } = usePointCursor();
39 const { pointCursor } = usePointCursor();
40− const point = need.pointCursor(pointCursor);
40+ const point = need.point(pointCursor);
41 const name = useCurrentPointName();
41 const name = useCurrentPointName();
42
42
43 // sync the current point
43 // sync the current point
@@ -71,7 +71,7 @@ export default function Invite() {
71 <Grid.Item as={H5} className="mv4" full>
71 <Grid.Item as={H5} className="mv4" full>
72 Invite
72 Invite
73 </Grid.Item>
73 </Grid.Item>
74− <Grid.Item as={Text} full>
74+ <Grid.Item as={Text} className="f5" full>
75 You currently have{' '}
75 You currently have{' '}
76 <Highlighted>{availableInvitesText} invitations</Highlighted> left.
76 <Highlighted>{availableInvitesText} invitations</Highlighted> left.
77 <br />
77 <br />
src/views/Invite/InviteEmail.js+5−5
@@ -100,7 +100,7 @@ export default function InviteEmail() {
100 const { wallet, walletType, walletHdPath } = useWallet();
100 const { wallet, walletType, walletHdPath } = useWallet();
101 const { syncInvites, getInvites } = usePointCache();
101 const { syncInvites, getInvites } = usePointCache();
102 const { pointCursor } = usePointCursor();
102 const { pointCursor } = usePointCursor();
103− const point = need.pointCursor(pointCursor);
103+ const point = need.point(pointCursor);
104 const { getHasRecieved, syncHasReceivedForEmail, sendMail } = useMailer();
104 const { getHasRecieved, syncHasReceivedForEmail, sendMail } = useMailer();
105
105
106 const { availableInvites } = getInvites(point);
106 const { availableInvites } = getInvites(point);
@@ -114,7 +114,7 @@ export default function InviteEmail() {
114 inputConfigs,
114 inputConfigs,
115 { append: appendInput, removeAt: removeInputAt },
115 { append: appendInput, removeAt: removeInputAt },
116 ] = useArray(
116 ] = useArray(
117− [buildInputConfig({ label: 'Email Address' })],
117+ [buildInputConfig({ label: 'Email Address', autoFocus: true })],
118 buildInputConfig
118 buildInputConfig
119 );
119 );
120
120
@@ -391,7 +391,7 @@ export default function InviteEmail() {
391 }, [isDone, syncInvites, point]);
391 }, [isDone, syncInvites, point]);
392
392
393 return (
393 return (
394− <Grid gap={12}>
394+ <Grid gap={3}>
395 <Grid.Item as={Grid} full>
395 <Grid.Item as={Grid} full>
396 <Grid.Item as={Flex} cols={[1, 11]} align="center">
396 <Grid.Item as={Flex} cols={[1, 11]} align="center">
397 <MiniBackButton onClick={() => pop()} />
397 <MiniBackButton onClick={() => pop()} />
@@ -411,7 +411,7 @@ export default function InviteEmail() {
411
411
412 {isDone && (
412 {isDone && (
413 <>
413 <>
414− <Grid.Item as={Text} full>
414+ <Grid.Item as={Text} className="f5" full>
415 <Highlighted>{pluralize(inputs.length, 'invite')}</Highlighted>{' '}
415 <Highlighted>{pluralize(inputs.length, 'invite')}</Highlighted>{' '}
416 {pluralize(inputs.length, 'has', 'have')} been successfully sent
416 {pluralize(inputs.length, 'has', 'have')} been successfully sent
417 </Grid.Item>
417 </Grid.Item>
@@ -432,7 +432,7 @@ export default function InviteEmail() {
432 <Grid.Item
432 <Grid.Item
433 key={input.name}
433 key={input.name}
434 as={Grid}
434 as={Grid}
435− gap={12}
435+ gap={3}
436 onMouseOver={() => setHovered({ [input.name]: true })}
436 onMouseOver={() => setHovered({ [input.name]: true })}
437 onMouseLeave={() => setHovered({ [input.name]: false })}
437 onMouseLeave={() => setHovered({ [input.name]: false })}
438 full>
438 full>
src/views/Invite/InviteUrl.js+1−1
@@ -13,7 +13,7 @@ import FooterButton from 'components/FooterButton';
13export default function InviteUrl() {
13export default function InviteUrl() {
14 const { names, push } = useLocalRouter();
14 const { names, push } = useLocalRouter();
15 const { pointCursor } = usePointCursor();
15 const { pointCursor } = usePointCursor();
16− const point = need.pointCursor(pointCursor);
16+ const point = need.point(pointCursor);
17 const { availableInvites } = useInvites(point);
17 const { availableInvites } = useInvites(point);
18 const canInvite = availableInvites.matchWith({
18 const canInvite = availableInvites.matchWith({
19 Nothing: () => false,
19 Nothing: () => false,
src/views/InviteTicket.jsdeleted−434
@@ -1,434 +0,0 @@
1−import { Just, Nothing } from 'folktale/maybe';
2−import React from 'react';
3−import * as azimuth from 'azimuth-js';
4−import * as need from 'lib/need';
5−
6−import View from 'components/View';
7−
8−import {
9− InnerLabel,
10− Warning,
11− TicketInput,
12− VerifyTicketInput,
13− Button,
14− Passport,
15−} from '../components/old/Base';
16−
17−import { randomPatq, compose } from '../lib/lib';
18−import { ROUTE_NAMES } from '../lib/routeNames';
19−import { withHistory } from '../store/history';
20−import {
21− DEFAULT_HD_PATH,
22− ownershipWalletFromTicket,
23− walletFromMnemonic,
24−} from '../lib/wallet';
25−import { BRIDGE_ERROR } from '../lib/error';
26−import {
27− INVITE_STAGES,
28− WALLET_STATES,
29− TRANSACTION_STATES,
30−} from '../lib/invite';
31−import { generateWallet, startTransactions } from '../lib/invite';
32−import { withNetwork } from '../store/network';
33−import { withWallet } from '../store/wallet';
34−import { Grid } from 'indigo-react';
35−
36−class InviteTicket extends React.Component {
37− constructor(props) {
38− super(props);
39−
40− this.state = {
41− inviteTicket: '',
42− inviteWallet: Nothing(),
43− verifyTicket: '',
44− realTicket: '',
45− realPoint: Nothing(),
46− realWallet: Nothing(),
47− walletReady: false,
48− walletStates: [],
49− errors: [],
50− stage: INVITE_STAGES.INVITE_LOGIN,
51− transactionProgress: TRANSACTION_STATES.GENERATING,
52− };
53−
54− this.pointPlaceholder = randomPatq(4);
55− this.ticketPlaceholder = randomPatq(12);
56−
57− this.handleInviteTicketInput = this.handleInviteTicketInput.bind(this);
58− this.handleVerifyTicketInput = this.handleVerifyTicketInput.bind(this);
59− this.updateProgress = this.updateProgress.bind(this);
60− this.pushWalletState = this.pushWalletState.bind(this);
61− this.navigateLogin = this.navigateLogin.bind(this);
62− }
63−
64− pushWalletState(walletState) {
65− this.setState({
66− walletStates: this.state.walletStates.concat(walletState),
67− });
68− }
69−
70− navigateLogin() {
71− this.props.history.popAndPush(ROUTE_NAMES.LOGIN);
72− }
73−
74− handleInviteTicketInput(inviteTicket) {
75− this.setState({ inviteTicket });
76− }
77−
78− handleVerifyTicketInput(verifyTicket) {
79− this.setState({ verifyTicket });
80− }
81−
82− updateTransactionProgress(transactionProgress) {
83− this.setState({ transactionProgress });
84− }
85−
86− async unlockInviteWallet(inviteTicket) {
87− this.setState({
88− walletStates: this.state.walletStates.concat(WALLET_STATES.UNLOCKING),
89− stage: INVITE_STAGES.INVITE_WALLET,
90− });
91−
92− //NOTE ~zod because tmp wallet
93− const ownership = await ownershipWalletFromTicket(inviteTicket, 0);
94− const mnemonic = ownership.seed;
95− //TODO isn't all this accessible in the ownership object?
96− const inviteWallet = walletFromMnemonic(
97− mnemonic,
98− DEFAULT_HD_PATH
99− ).matchWith({
100− Just: w => w.value,
101− Nothing: () => {
102− throw BRIDGE_ERROR.MISSING_WALLET;
103− },
104− });
105−
106− const addr = inviteWallet.address;
107−
108− const contracts = need.contracts(this.props.contracts);
109−
110− const incoming = await azimuth.azimuth.getTransferringFor(contracts, addr);
111−
112− this.setState({
113− walletStates: this.state.walletStates.concat(WALLET_STATES.GENERATING),
114− });
115−
116− let realPoint = Nothing();
117− let realWallet = Nothing();
118− let errors = [];
119−
120− if (incoming.length > 0) {
121− let pointNum = parseInt(incoming[0], 10);
122− realPoint = Just(pointNum);
123−
124− this.setState({
125− realPoint,
126− });
127−
128− let genWallet = await generateWallet(pointNum);
129−
130− realWallet = Just(genWallet);
131− if (incoming.length > 1) {
132− //TODO notify user "...and others / ticket reusable"
133− }
134− } else {
135− errors = [
136− 'Invite code has no claimable ship. Check your invite code and try again.',
137− ];
138− }
139−
140− this.setState({
141− realWallet,
142− inviteWallet: Just(inviteWallet),
143− // dev mode: uncomment this out to autofill verified ticket
144− verifyTicket: !Nothing.hasInstance(realWallet)
145− ? realWallet.value.ticket
146− : '',
147− walletStates: this.state.walletStates.concat(WALLET_STATES.RENDERING),
148− errors,
149− });
150− }
151−
152− getTicketElement() {
153− const phTick = this.ticketPlaceholder;
154−
155− let isLogin = this.state.stage === INVITE_STAGES.INVITE_LOGIN;
156− let isVerify = this.state.stage === INVITE_STAGES.INVITE_VERIFY;
157−
158− if (isLogin || isVerify) {
159− let value = isLogin ? this.state.inviteTicket : this.state.verifyTicket;
160− let handler = isLogin
161− ? this.handleInviteTicketInput
162− : this.handleVerifyTicketInput;
163− let label = isLogin ? 'Activation code' : 'ticket';
164−
165− // if realWallet is Just, then VerifyTicketInput appropriate contents; if Nothing, remain Nothing
166− const Verified = this.state.realWallet.map(realWallet =>
167− VerifyTicketInput(realWallet.ticket)
168− );
169−
170− // Verified.getOrElse(TicketInput) falls back to TicketInput if Verified is Nothing
171− const TicketElem = isVerify
172− ? Verified.getOrElse(TicketInput)
173− : TicketInput;
174−
175− return (
176− <TicketElem
177− className="mono mt-8"
178− prop-size="md"
179− prop-format="innerLabel"
180− type="text"
181− autoFocus
182− name="invite"
183− placeholder={`e.g. ${phTick}`}
184− value={value}
185− onChange={handler}>
186− <InnerLabel>{label}</InnerLabel>
187− </TicketElem>
188− );
189− } else {
190− return null;
191− }
192− }
193−
194− getStageText() {
195− const { stage } = this.state;
196−
197− let progressWidth = this.state.transactionProgress.pct;
198−
199− const stageText =
200− stage === INVITE_STAGES.INVITE_LOGIN ? (
201− <h1 className="text-700 mt-15">Welcome</h1>
202− ) : stage === INVITE_STAGES.INVITE_WALLET ? (
203− <div>
204− <h1 className="fs-6 lh-8 mb-3">Passport</h1>
205− <p>
206− A passport is your digital identity. You will use your passport to
207− access your true computer, send payments, and administer your
208− identity. So naturally, you must keep this secure.
209− </p>
210− <p>
211− After you’ve downloaded your passport, back up the ticket manually
212− or store on a trusted device.
213− </p>
214− </div>
215− ) : stage === INVITE_STAGES.INVITE_VERIFY ? (
216− <div>
217− <h1 className="fs-6 lh-8 mb-3">Verify Passport</h1>
218− <p>
219− Afer you download your passport, verify your custody. Your passport
220− should be a folder of image files. One of them is your Master
221− Ticket. Open it and enter the 4 word phrase below (with hyphens).
222− </p>
223− </div>
224− ) : INVITE_STAGES.INVITE_TRANSACTIONS ? (
225− this.state.transactionProgress.label ===
226− TRANSACTION_STATES.DONE.label ? (
227− <div>
228− <h1 className="fs-6 lh-8 mb-3">
229− <span>Success</span>
230− <span className="ml-4 green">✓</span>
231− </h1>
232− <p>Transferring to Bridge...</p>
233− </div>
234− ) : (
235− <div>
236− <h1 className="fs-6 lh-8 mb-3">Submitting</h1>
237− <p className="mt-4 mb-4">
238− This step can take up to five minutes. Please do not leave this
239− page until the transactions are complete.
240− </p>
241− <div className="passport-progress mb-2">
242− <div
243− className="passport-progress-filled"
244− style={{ width: progressWidth }}
245− />
246− </div>
247− <div className="flex justify-between">
248− <div className="text-mono text-sm lh-6 green-dark">
249− {this.state.transactionProgress.label}
250− </div>
251− </div>
252− </div>
253− )
254− ) : null;
255−
256− return stageText;
257− }
258−
259− updateProgress(notification) {
260− let newState =
261− notification.type === 'progress'
262− ? { transactionProgress: notification.value }
263− : notification.type === 'notify'
264− ? { errors: [notification.value] }
265− : {};
266−
267− this.setState(newState);
268− }
269−
270− getContinueButton() {
271− const {
272− realPoint,
273− realWallet,
274− inviteWallet,
275− stage,
276− walletStates,
277− verifyTicket,
278− inviteTicket,
279− } = this.state;
280− const { web3, contracts, setUrbitWallet } = this.props;
281−
282− const btnColor = this.state.walletStates.includes(WALLET_STATES.PAPER_READY)
283− ? 'green'
284− : 'black';
285−
286− const clickHandler =
287− stage === INVITE_STAGES.INVITE_LOGIN
288− ? () => {
289− this.unlockInviteWallet(inviteTicket);
290− }
291− : stage === INVITE_STAGES.INVITE_WALLET
292− ? () => {
293− this.setState({
294− stage: INVITE_STAGES.INVITE_VERIFY,
295− });
296− }
297− : stage === INVITE_STAGES.INVITE_VERIFY
298− ? () => {
299− this.setState({
300− stage: INVITE_STAGES.INVITE_TRANSACTIONS,
301− walletStates: this.state.walletStates.concat(
302− WALLET_STATES.TRANSACTIONS
303− ),
304− });
305−
306− startTransactions({
307− inviteWalletM: inviteWallet,
308− realWalletM: realWallet,
309− realPointM: realPoint,
310− realTicket: verifyTicket,
311− web3,
312− contracts,
313− setUrbitWallet,
314− updateProgress: this.updateProgress,
315− }).then(() => {
316− setTimeout(() => {
317− this.props.history.pop();
318− }, 5000);
319− });
320− }
321− : null;
322−
323− const ticketMatches = this.state.realWallet.matchWith({
324− Just: wal => wal.value.ticket === this.state.verifyTicket,
325− Nothing: () => false,
326− });
327−
328− const continueReady =
329− stage === INVITE_STAGES.INVITE_WALLET
330− ? walletStates.includes(WALLET_STATES.DOWNLOADED)
331− : stage === INVITE_STAGES.INVITE_VERIFY
332− ? ticketMatches
333− : true;
334−
335− const buttonElem =
336− stage === INVITE_STAGES.INVITE_TRANSACTIONS ? null : (
337− <Button
338− className={'mt-4'}
339− prop-size={'lg'}
340− prop-color={btnColor}
341− disabled={!continueReady}
342− onClick={clickHandler}>
343− {'Continue →'}
344− </Button>
345− );
346−
347− return buttonElem;
348− }
349−
350− getStageDisplay() {
351− const { stage } = this.state;
352−
353− if (stage === INVITE_STAGES.INVITE_LOGIN) return null;
354−
355− return (
356− <div className="mt-6 mb-8">
357− <span
358− className={`fs-35 mr-3 ${stage !== INVITE_STAGES.INVITE_WALLET &&
359− 'gray-20'}`}>
360− 1 Passport
361− </span>
362− <span
363− className={`fs-35 mr-3 ${stage !== INVITE_STAGES.INVITE_VERIFY &&
364− 'gray-20'}`}>
365− 2 Verify
366− </span>
367− <span
368− className={`fs-35 mr-3 ${stage !==
369− INVITE_STAGES.INVITE_TRANSACTIONS && 'gray-20'}`}>
370− 3 Invite
371− </span>
372− </div>
373− );
374− }
375−
376− getErrors() {
377− if (this.state.errors.length === 0) return null;
378−
379− let errorElems = this.state.errors.map(e => <span>{e}</span>);
380−
381− return (
382− <Warning>
383− <h3 className={'mb-2'}>{'Warning'}</h3>
384− {errorElems}
385− </Warning>
386− );
387− }
388−
389− render() {
390− let stageDisplay = this.getStageDisplay();
391− let stageText = this.getStageText();
392− let ticketElem = this.getTicketElement();
393− let continueButton = this.getContinueButton();
394− let errorDisplay = this.getErrors();
395−
396− return (
397− <View.Full>
398− <Grid>
399− <Grid.Item half={1}>
400− {stageDisplay}
401− {errorDisplay}
402− {stageText}
403− {ticketElem}
404− {continueButton}
405−
406− {this.state.stage === INVITE_STAGES.INVITE_LOGIN && (
407− <Button
408− prop-type={`link`}
409− prop-size={`sm`}
410− className={`block mt-4`}
411− onClick={this.navigateLogin}>
412− {`Log in`}
413− </Button>
414− )}
415− </Grid.Item>
416− <Grid.Item half={2}>
417− <Passport
418− point={this.state.realPoint}
419− wallet={this.state.realWallet}
420− walletStates={this.state.walletStates}
421− pushWalletState={this.pushWalletState}
422− />
423− </Grid.Item>
424− </Grid>
425− </View.Full>
426− );
427− }
428−}
429−
430−export default compose(
431− withNetwork,
432− withHistory,
433− withWallet
434−)(InviteTicket);
src/views/InvitesManage.js+1−1
@@ -39,7 +39,7 @@ class InvitesManage extends React.Component {
39 }
39 }
40
40
41 componentDidMount() {
41 componentDidMount() {
42− this.point = need.pointCursor(this.props.pointCursor);
42+ this.point = need.point(this.props.pointCursor);
43 this.contracts = need.contracts(this.props.contracts);
43 this.contracts = need.contracts(this.props.contracts);
44
44
45 azimuth.azimuth
45 azimuth.azimuth
src/views/IssueChild.js+1−1
@@ -40,7 +40,7 @@ class IssueChild extends React.Component {
40 constructor(props) {
40 constructor(props) {
41 super(props);
41 super(props);
42
42
43− const issuingPoint = need.pointCursor(props.pointCursor);
43+ const issuingPoint = need.point(props.pointCursor);
44
44
45 const getCandidate = () => ob.patp(getSpawnCandidate(issuingPoint));
45 const getCandidate = () => ob.patp(getSpawnCandidate(issuingPoint));
46
46
src/views/Landing.js+5−5
@@ -7,7 +7,7 @@ import { useHistory } from '../store/history';
7import View from 'components/View';
7import View from 'components/View';
8import { ForwardButton } from 'components/Buttons';
8import { ForwardButton } from 'components/Buttons';
9
9
10−function Landing(props) {
10+function Landing() {
11 const history = useHistory();
11 const history = useHistory();
12
12
13 return (
13 return (
@@ -22,13 +22,13 @@ function Landing(props) {
22 <ForwardButton
22 <ForwardButton
23 className="mt3"
23 className="mt3"
24 detail="If you were sent an Azimuth invite code."
24 detail="If you were sent an Azimuth invite code."
25− onClick={() => history.push(ROUTE_NAMES.INVITE_TICKET)}>
25+ onClick={() => history.push(ROUTE_NAMES.ACTIVATE)}>
26− Claim an invite
26+ Activate a Point
27 </ForwardButton>
27 </ForwardButton>
28
28
29 <ForwardButton
29 <ForwardButton
30 className="mt3"
30 className="mt3"
31− detail="Shiny new Login flow."
31+ detail="Login to an activated point."
32 onClick={() => history.push(ROUTE_NAMES.LOGIN)}>
32 onClick={() => history.push(ROUTE_NAMES.LOGIN)}>
33 Login
33 Login
34 </ForwardButton>
34 </ForwardButton>
@@ -37,7 +37,7 @@ function Landing(props) {
37 className="mt3"
37 className="mt3"
38 detail="View an Azimuth point without signing into a wallet."
38 detail="View an Azimuth point without signing into a wallet."
39 onClick={() => history.push(ROUTE_NAMES.VIEW_POINT)}>
39 onClick={() => history.push(ROUTE_NAMES.VIEW_POINT)}>
40− View a point
40+ View a Point
41 </ForwardButton>
41 </ForwardButton>
42 </View>
42 </View>
43 );
43 );
src/views/Login/Keystore.js+2
@@ -14,8 +14,10 @@ import { useWallet } from 'store/wallet';
14import * as need from 'lib/need';
14import * as need from 'lib/need';
15import { EthereumWallet, WALLET_TYPES } from 'lib/wallet';
15import { EthereumWallet, WALLET_TYPES } from 'lib/wallet';
16import useWalletType from 'lib/useWalletType';
16import useWalletType from 'lib/useWalletType';
17+import useResetPointCursor from 'lib/useResetPointCursor';
17
18
18export default function Keystore({ loginCompleted }) {
19export default function Keystore({ loginCompleted }) {
20+ useResetPointCursor();
19 useWalletType(WALLET_TYPES.KEYSTORE);
21 useWalletType(WALLET_TYPES.KEYSTORE);
20
22
21 // globals
23 // globals
src/views/Login/Ledger.js+2
@@ -16,6 +16,7 @@ import { useWallet } from 'store/wallet';
16import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from 'lib/ledger';
16import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from 'lib/ledger';
17import useWalletType from 'lib/useWalletType';
17import useWalletType from 'lib/useWalletType';
18import { WALLET_TYPES } from 'lib/wallet';
18import { WALLET_TYPES } from 'lib/wallet';
19+import useResetPointCursor from 'lib/useResetPointCursor';
19
20
20const pathOptions = [
21const pathOptions = [
21 { title: 'Ledger Live', value: LEDGER_LIVE_PATH },
22 { title: 'Ledger Live', value: LEDGER_LIVE_PATH },
@@ -28,6 +29,7 @@ const chopHdPrefix = str => (str.slice(0, 2) === 'm/' ? str.slice(2) : str);
28const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
29const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
29
30
30export default function Ledger({ loginCompleted }) {
31export default function Ledger({ loginCompleted }) {
32+ useResetPointCursor();
31 useWalletType(WALLET_TYPES.LEDGER);
33 useWalletType(WALLET_TYPES.LEDGER);
32
34
33 const { wallet, setWallet, setWalletHdPath } = useWallet();
35 const { wallet, setWallet, setWalletHdPath } = useWallet();
src/views/Login/Mnemonic.js+3
@@ -14,9 +14,12 @@ import { useWallet } from 'store/wallet';
14
14
15import { walletFromMnemonic, WALLET_TYPES } from 'lib/wallet';
15import { walletFromMnemonic, WALLET_TYPES } from 'lib/wallet';
16import useWalletType from 'lib/useWalletType';
16import useWalletType from 'lib/useWalletType';
17+import useResetPointCursor from 'lib/useResetPointCursor';
17
18
18export default function Mnemonic({ advanced, loginCompleted }) {
19export default function Mnemonic({ advanced, loginCompleted }) {
20+ useResetPointCursor();
19 useWalletType(WALLET_TYPES.MNEMONIC);
21 useWalletType(WALLET_TYPES.MNEMONIC);
22+
20 const {
23 const {
21 wallet,
24 wallet,
22 setWallet,
25 setWallet,
src/views/Login/PrivateKey.js+2
@@ -10,8 +10,10 @@ import { useWallet } from 'store/wallet';
10
10
11import { EthereumWallet, WALLET_TYPES } from 'lib/wallet';
11import { EthereumWallet, WALLET_TYPES } from 'lib/wallet';
12import useWalletType from 'lib/useWalletType';
12import useWalletType from 'lib/useWalletType';
13+import useResetPointCursor from 'lib/useResetPointCursor';
13
14
14export default function PrivateKey({ loginCompleted }) {
15export default function PrivateKey({ loginCompleted }) {
16+ useResetPointCursor();
15 useWalletType(WALLET_TYPES.PRIVATE_KEY);
17 useWalletType(WALLET_TYPES.PRIVATE_KEY);
16 const { wallet, setWallet } = useWallet();
18 const { wallet, setWallet } = useWallet();
17
19
src/views/Login/Shards.js+2−1
@@ -15,15 +15,16 @@ import { useWallet } from 'store/wallet';
15
15
16import { urbitWalletFromTicket, WALLET_TYPES } from 'lib/wallet';
16import { urbitWalletFromTicket, WALLET_TYPES } from 'lib/wallet';
17import useWalletType from 'lib/useWalletType';
17import useWalletType from 'lib/useWalletType';
18+import useResetPointCursor from 'lib/useResetPointCursor';
18
19
19export default function Shards({ loginCompleted }) {
20export default function Shards({ loginCompleted }) {
21+ useResetPointCursor();
20 useWalletType(WALLET_TYPES.SHARDS);
22 useWalletType(WALLET_TYPES.SHARDS);
21
23
22 const { wallet, setUrbitWallet } = useWallet();
24 const { wallet, setUrbitWallet } = useWallet();
23
25
24 const pointInput = usePointInput({
26 const pointInput = usePointInput({
25 name: 'point',
27 name: 'point',
26− label: 'Point',
27 autoFocus: true,
28 autoFocus: true,
28 });
29 });
29 const pointName = pointInput.data;
30 const pointName = pointInput.data;
src/views/Login/Ticket.js+3−3
@@ -19,6 +19,7 @@ import { usePointCursor } from 'store/pointCursor';
19import * as need from 'lib/need';
19import * as need from 'lib/need';
20import { WALLET_TYPES, urbitWalletFromTicket } from 'lib/wallet';
20import { WALLET_TYPES, urbitWalletFromTicket } from 'lib/wallet';
21import useWalletType from 'lib/useWalletType';
21import useWalletType from 'lib/useWalletType';
22+import useResetPointCursor from 'lib/useResetPointCursor';
22
23
23//TODO should be part of InputWithStatus component
24//TODO should be part of InputWithStatus component
24const INPUT_STATUS = {
25const INPUT_STATUS = {
@@ -28,6 +29,7 @@ const INPUT_STATUS = {
28};
29};
29
30
30export default function Ticket({ advanced, loginCompleted }) {
31export default function Ticket({ advanced, loginCompleted }) {
32+ useResetPointCursor();
31 useWalletType(WALLET_TYPES.TICKET);
33 useWalletType(WALLET_TYPES.TICKET);
32 // globals
34 // globals
33 const { contracts } = useNetwork();
35 const { contracts } = useNetwork();
@@ -37,16 +39,14 @@ export default function Ticket({ advanced, loginCompleted }) {
37 // inputs
39 // inputs
38 const pointInput = usePointInput({
40 const pointInput = usePointInput({
39 name: 'point',
41 name: 'point',
40− label: 'Point',
41 //TODO deduce point name from URL if we can, prefill input if we found it
42 //TODO deduce point name from URL if we can, prefill input if we found it
42− // initialValue: '~',
43 autoFocus: true,
43 autoFocus: true,
44 });
44 });
45 const pointName = pointInput.data;
45 const pointName = pointInput.data;
46
46
47 const ticketInput = useTicketInput({
47 const ticketInput = useTicketInput({
48 name: 'ticket',
48 name: 'ticket',
49− label: 'Master ticket',
49+ label: 'Master Ticket',
50 });
50 });
51 const ticket = ticketInput.data;
51 const ticket = ticketInput.data;
52
52
src/views/Login/Trezor.js+2
@@ -13,6 +13,7 @@ import { InnerLabelDropdown } from 'components/old/Base';
13import { TREZOR_PATH } from 'lib/trezor';
13import { TREZOR_PATH } from 'lib/trezor';
14import useWalletType from 'lib/useWalletType';
14import useWalletType from 'lib/useWalletType';
15import { WALLET_TYPES } from 'lib/wallet';
15import { WALLET_TYPES } from 'lib/wallet';
16+import useResetPointCursor from 'lib/useResetPointCursor';
16
17
17import { useWallet } from 'store/wallet';
18import { useWallet } from 'store/wallet';
18
19
@@ -22,6 +23,7 @@ const accountOptions = [
22];
23];
23
24
24export default function Trezor({ loginCompleted }) {
25export default function Trezor({ loginCompleted }) {
26+ useResetPointCursor();
25 useWalletType(WALLET_TYPES.TREZOR);
27 useWalletType(WALLET_TYPES.TREZOR);
26
28
27 const { wallet, setWallet, setWalletHdPath } = useWallet();
29 const { wallet, setWallet, setWalletHdPath } = useWallet();
src/views/Point.js+2−2
@@ -24,7 +24,7 @@ export default function Point() {
24 const { pointCursor } = usePointCursor();
24 const { pointCursor } = usePointCursor();
25 const { wallet } = useWallet();
25 const { wallet } = useWallet();
26
26
27− const point = need.pointCursor(pointCursor);
27+ const point = need.point(pointCursor);
28
28
29 // fetch the invites for the current cursor
29 // fetch the invites for the current cursor
30 const { availableInvites } = useInvites(point);
30 const { availableInvites } = useInvites(point);
@@ -41,7 +41,7 @@ export default function Point() {
41
41
42 return (
42 return (
43 <View>
43 <View>
44− <Passport point={point} />
44+ <Passport point={Maybe.Just(point)} />
45 <Grid className="pt2">
45 <Grid className="pt2">
46 <Grid.Item full>
46 <Grid.Item full>
47 <ForwardButton disabled>Admin</ForwardButton>
47 <ForwardButton disabled>Admin</ForwardButton>
src/views/Point/Actions.js+1−1
@@ -19,7 +19,7 @@ function Actions() {
19 const history = useHistory();
19 const history = useHistory();
20 const { wallet } = useWallet();
20 const { wallet } = useWallet();
21 const { pointCursor } = usePointCursor();
21 const { pointCursor } = usePointCursor();
22− const point = need.pointCursor(pointCursor);
22+ const point = need.point(pointCursor);
23 const { getDetails } = usePointCache();
23 const { getDetails } = usePointCache();
24 const pointDetails = getDetails(point);
24 const pointDetails = getDetails(point);
25
25
src/views/SetKeys.js+2−2
@@ -22,7 +22,7 @@ class SetKeys extends React.Component {
22 constructor(props) {
22 constructor(props) {
23 super(props);
23 super(props);
24
24
25− const point = need.pointCursor(props.pointCursor);
25+ const point = need.point(props.pointCursor);
26
26
27 this.state = {
27 this.state = {
28 auth: '',
28 auth: '',
@@ -115,7 +115,7 @@ class SetKeys extends React.Component {
115 const { state, props } = this;
115 const { state, props } = this;
116
116
117 const validContracts = need.contracts(props.contracts);
117 const validContracts = need.contracts(props.contracts);
118− const validPoint = need.pointCursor(props.pointCursor);
118+ const validPoint = need.point(props.pointCursor);
119
119
120 // TODO: move this to a lib for validating things
120 // TODO: move this to a lib for validating things
121 const hexRegExp = /[0-9A-Fa-f]{64}/g;
121 const hexRegExp = /[0-9A-Fa-f]{64}/g;
src/views/SetProxy.js+2−2
@@ -29,7 +29,7 @@ class _SetProxy extends React.Component {
29 constructor(props) {
29 constructor(props) {
30 super(props);
30 super(props);
31
31
32− const issuingPoint = need.pointCursor(props.pointCursor);
32+ const issuingPoint = need.point(props.pointCursor);
33
33
34 this.state = {
34 this.state = {
35 proxyAddress: '',
35 proxyAddress: '',
@@ -61,7 +61,7 @@ class _SetProxy extends React.Component {
61 const { state, props } = this;
61 const { state, props } = this;
62
62
63 const validContracts = need.contracts(props.contracts);
63 const validContracts = need.contracts(props.contracts);
64− const validPoint = need.pointCursor(props.pointCursor);
64+ const validPoint = need.point(props.pointCursor);
65
65
66 const txArgs = [validContracts, validPoint, state.proxyAddress];
66 const txArgs = [validContracts, validPoint, state.proxyAddress];
67
67
src/views/Transfer.js+2−2
@@ -20,7 +20,7 @@ class Transfer extends React.Component {
20 constructor(props) {
20 constructor(props) {
21 super(props);
21 super(props);
22
22
23− const issuingPoint = need.pointCursor(props.pointCursor);
23+ const issuingPoint = need.point(props.pointCursor);
24
24
25 this.state = {
25 this.state = {
26 receivingAddress: '',
26 receivingAddress: '',
@@ -47,7 +47,7 @@ class Transfer extends React.Component {
47 const { state, props } = this;
47 const { state, props } = this;
48
48
49 const validContracts = need.contracts(props.contracts);
49 const validContracts = need.contracts(props.contracts);
50− const validPoint = need.pointCursor(props.pointCursor);
50+ const validPoint = need.point(props.pointCursor);
51
51
52 const txn = azimuth.ecliptic.setTransferProxy(
52 const txn = azimuth.ecliptic.setTransferProxy(
53 validContracts,
53 validContracts,
src/views/ViewPoint.js+2−4
@@ -1,6 +1,5 @@
1import React, { useCallback } from 'react';
1import React, { useCallback } from 'react';
2import Maybe from 'folktale/maybe';
2import Maybe from 'folktale/maybe';
3−import * as ob from 'urbit-ob';
4import { Input } from 'indigo-react';
3import { Input } from 'indigo-react';
5
4
6import { H1, P } from '../components/old/Base';
5import { H1, P } from '../components/old/Base';
@@ -9,6 +8,7 @@ import { useHistory } from 'store/history';
9import { usePointCursor } from 'store/pointCursor';
8import { usePointCursor } from 'store/pointCursor';
10
9
11import { ROUTE_NAMES } from 'lib/routeNames';
10import { ROUTE_NAMES } from 'lib/routeNames';
11+import patp2dec from 'lib/patp2dec';
12
12
13import View from 'components/View';
13import View from 'components/View';
14import { ForwardButton } from 'components/Buttons';
14import { ForwardButton } from 'components/Buttons';
@@ -19,15 +19,13 @@ export default function ViewPoint() {
19 const { setPointCursor } = usePointCursor();
19 const { setPointCursor } = usePointCursor();
20 const pointInput = usePointInput({
20 const pointInput = usePointInput({
21 name: 'point',
21 name: 'point',
22− label: 'Point Name',
23 autoFocus: true,
22 autoFocus: true,
24 });
23 });
25 const { data: pointName, pass } = pointInput;
24 const { data: pointName, pass } = pointInput;
26
25
27 const disabled = !pass;
26 const disabled = !pass;
28 const goForward = useCallback(() => {
27 const goForward = useCallback(() => {
29− setPointCursor(Maybe.Just(parseInt(ob.patp2dec(pointName), 10)));
28+ setPointCursor(Maybe.Just(patp2dec(pointName)));
30− // ^ pointCursor expects native number type, not string
31 history.popAndPush(ROUTE_NAMES.POINT);
29 history.popAndPush(ROUTE_NAMES.POINT);
32 }, [setPointCursor, history, pointName]);
30 }, [setPointCursor, history, pointName]);
33
31