on master help sign in
Merge pull request #178 from urbit/feat/invite-flow-ui

feat: indigo invite flow

commit93858a413f5a000006795db81e38fb591bb65ab7 authorMatt Condon <mattgcondon@gmail.com> dateMon 1 Jul 2019, 16:26 (7 years ago) parentc603ef92dd6efa
87 files changed+1602−922
config-overrides.js+6 package-lock.json+56 package.json+7−4 src/Bridge.js+13−19 src/components/AspectRatio.js+22 src/components/Blinky.js+8−7 src/components/Buttons.js+15 src/components/FooterButton.js+2−2 src/components/InputSigil.js+3−3 src/components/Inputs.js+24−8 src/components/LoadingBar.js+19 src/components/MaybeSigil.js+21 src/components/Passport.js+77−15 src/components/Sigil.js+12−7 src/components/Steps.js+13 src/components/WarningBox.js+11 src/components/old/Header.js−62 src/index.js+18−1 src/indigo-react/components/AccessoryIcon.js+1−1 src/indigo-react/components/Button.js+1−1 src/indigo-react/components/CheckboxInput.js+82 src/indigo-react/components/Flex.js+4−9 src/indigo-react/components/Grid.js+20−11 src/indigo-react/components/Input.js+11−10 src/indigo-react/components/Typography.js+4−6 src/indigo-react/index.js+1 src/indigo-react/indigo.css+39−37 src/indigo-react/lib/getComponent.js−1 src/indigo-react/lib/useForm.js+23−9 src/lib/MediaQuery.js+24 src/lib/constants.js+3 src/lib/invite.js+57−106 src/lib/keys.js+1−1 src/lib/need.js+27−12 src/lib/patp2dec.js+9 src/lib/routeNames.js+1−1 src/lib/router.js+2−50 src/lib/timeout.js+1 src/lib/useBreakpoints.js+5 src/lib/useImpliedPoint.js+14 src/lib/useMedia.js+31 src/lib/useRenderCount.js+2−2 src/lib/useResetPointCursor.js+12 src/lib/useRouter.js+10−6 src/lib/useSyncPoints.js+23−30 src/lib/validators.js+37−7 src/lib/walletgen.js+6−1 src/store/Provider.js+2 src/store/lib/useBirthdaysStore.js+6 src/store/lib/useControlledPointsStore.js+3−3 src/store/lib/useInvitesStore.js+2−2 src/store/lib/usePointStore.js+9−2 src/store/wallet.js+3−9 src/style/indigo-proposals.scss+8 src/views/AcceptTransfer.js+2−2 src/views/Activate.js+39 src/views/Activate/ActivateCode.js+150 src/views/Activate/ActivateDisclaimer.js+85 src/views/Activate/ActivateFlow.js+9 src/views/Activate/ActivatePassport.js+90 src/views/Activate/PassportDownload.js+87 src/views/Activate/PassportTransfer.js+191 src/views/Activate/PassportVerify.js+59 src/views/Activate/useActivateFlowState.js+28 src/views/CancelTransfer.js+2−2 src/views/CreateGalaxy.js+1−1 src/views/GenKeyfile.js+1−1 src/views/Invite.js+7−7 src/views/Invite/InviteEmail.js+5−5 src/views/Invite/InviteUrl.js+1−1 src/views/InviteTicket.js−434 src/views/InvitesManage.js+1−1 src/views/IssueChild.js+1−1 src/views/Landing.js+5−5 src/views/Login/Keystore.js+2 src/views/Login/Ledger.js+2 src/views/Login/Mnemonic.js+3 src/views/Login/PrivateKey.js+2 src/views/Login/Shards.js+2−1 src/views/Login/Ticket.js+3−3 src/views/Login/Trezor.js+2 src/views/Point.js+2−2 src/views/Point/Actions.js+1−1 src/views/SetKeys.js+2−2 src/views/SetProxy.js+2−2 src/views/Transfer.js+2−2 src/views/ViewPoint.js+2−4
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 @@
80178017 "minimalistic-crypto-utils": "^1.0.1"
80188018 }
80198019 },
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+ },
80208028 "home-or-tmp": {
80218029 "version": "2.0.0",
80228030 "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz",
@@ -14253,6 +14261,21 @@
1425314261 }
1425414262 }
1425514263 },
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+ },
1425614279 "react-dev-utils": {
1425714280 "version": "9.0.1",
1425814281 "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-9.0.1.tgz",
@@ -14416,11 +14439,39 @@
1441614439 "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-5.1.6.tgz",
1441714440 "integrity": "sha512-X1Y+0jR47ImDVr54Ab6V9eGk0Hnu7fVWGeHQSOXHf/C2pF9c6uy3gef8QUeuUiWlNb0i08InPSE5a/KJzNzw1Q=="
1441814441 },
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+ },
1441914465 "react-is": {
1442014466 "version": "16.8.6",
1442114467 "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
1442214468 "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA=="
1442314469 },
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+ },
1442414475 "react-scripts": {
1442514476 "version": "3.0.1",
1442614477 "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.0.1.tgz",
@@ -15545,6 +15596,11 @@
1554515596 }
1554615597 }
1554715598 },
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+ },
1554815604 "shebang-command": {
1554915605 "version": "1.2.0",
1555015606 "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
package.json+7−4
@@ -34,7 +34,10 @@
3434 "more-entropy": "^0.0.7",
3535 "prop-types": "^15.6.2",
3636 "react": "^16.8.6",
37+ "react-app-rewire-hot-loader": "^2.0.1",
38+ "react-app-rewired": "^2.1.3",
3739 "react-dom": "^16.8.6",
40+ "react-hot-loader": "^4.12.0",
3841 "react-scripts": "3.0.1",
3942 "react-teleporter": "^1.1.0",
4043 "secp256k1": "^3.5.2",
@@ -59,10 +62,10 @@
5962 "truffle": "4.1.11"
6063 },
6164 "scripts": {
62− "start": "react-scripts start",
63− "build": "react-scripts build",
64− "test": "react-scripts test --env=jsdom",
65− "eject": "react-scripts eject",
65+ "start": "react-app-rewired start",
66+ "build": "react-app-rewired build",
67+ "test": "react-app-rewired test --env=jsdom",
68+ "eject": "react-app-rewired eject",
6669 "checksum": "cd build && find . -exec shasum -a256 {} ';' > ../checksums.txt && cd ..",
6770 "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 ..",
6871 "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';
1414
1515import 'style/index.scss';
1616
17−const kInitialNetworkType = isDevelopment
17+const INITIAL_NETWORK_TYPE = isDevelopment
1818 ? NETWORK_TYPES.LOCAL
1919 : NETWORK_TYPES.MAINNET;
2020
2121// NB(shrugs): modify these variables to change the default local state.
22−const shouldStubLocal = process.env.REACT_APP_STUB_LOCAL === 'true';
23−const kIsStubbed = isDevelopment && shouldStubLocal;
24−const kInitialRoutes = kIsStubbed
25− ? [
26− { key: ROUTE_NAMES.LANDING },
27− { key: ROUTE_NAMES.LOGIN },
28− { key: ROUTE_NAMES.POINTS },
29− { key: ROUTE_NAMES.POINT },
30− { key: ROUTE_NAMES.INVITE },
31− ]
22+const SHOULD_STUB_LOCAL = process.env.REACT_APP_STUB_LOCAL === 'true';
23+const IS_STUBBED = isDevelopment && SHOULD_STUB_LOCAL;
24+const INITIAL_ROUTES = IS_STUBBED
25+ ? [{ key: ROUTE_NAMES.LANDING }, { key: ROUTE_NAMES.ACTIVATE }]
3226 : [{ key: ROUTE_NAMES.LANDING }];
3327
34−const kInitialWallet = kIsStubbed
28+const INITIAL_WALLET = IS_STUBBED
3529 ? walletFromMnemonic(
3630 process.env.REACT_APP_DEV_MNEMONIC,
3731 process.env.REACT_APP_HD_PATH
3832 )
3933 : undefined;
40−const kInitialMnemonic = kIsStubbed
34+const INITIAL_MNEMONIC = IS_STUBBED
4135 ? Just(process.env.REACT_APP_DEV_MNEMONIC)
4236 : Nothing();
43−const kInitialPointCursor = kIsStubbed ? Just(65792) : Nothing();
37+const INITIAL_POINT_CURSOR = IS_STUBBED ? Just(65792) : Nothing();
4438
4539export default function Bridge() {
4640 return (
4741 <Provider
4842 views={ROUTES}
4943 names={ROUTE_NAMES}
50− initialRoutes={kInitialRoutes}
51− initialNetworkType={kInitialNetworkType}
52− initialWallet={kInitialWallet}
53− initialMnemonic={kInitialMnemonic}
54− initialPointCursor={kInitialPointCursor}>
44+ initialRoutes={INITIAL_ROUTES}
45+ initialNetworkType={INITIAL_NETWORK_TYPE}
46+ initialWallet={INITIAL_WALLET}
47+ initialMnemonic={INITIAL_MNEMONIC}
48+ initialPointCursor={INITIAL_POINT_CURSOR}>
5549 <IndigoApp>
5650 <Router />
5751 </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';
33import useInterval from 'lib/useInterval';
44
55// TODO: make these characters display as the same width
6−export const kLoadingCharacter = '▓';
7−export const kInterstitialCharacter = '░';
6+export const LOADING_CHARACTER = '▓';
7+export const INTERSTITIAL_CHARACTER = '░';
88const BLINK_AFTER_MS = 2500; // ms
99
1010export const matchBlinky = obj =>
1111 obj.matchWith({
12− Nothing: () => <Blinky />,
12+ Nothing: () => <Blinky delayed />,
1313 Just: p => p.value,
1414 });
1515
1616export default function Blinky({
17− a = kLoadingCharacter,
18− b = kInterstitialCharacter,
17+ a = LOADING_CHARACTER,
18+ b = INTERSTITIAL_CHARACTER,
19+ delayed = false,
1920}) {
2021 const [value, setValue] = useState(true);
2122 const now = useRef(new Date());
2223
2324 useInterval(() => {
24− // only start blinking if we've elapsed enough time
25− if (new Date() - now.current > BLINK_AFTER_MS) {
25+ // only start blinking if we've elapsed enough time and want to delay
26+ if (!delayed || new Date() - now.current > BLINK_AFTER_MS) {
2627 setValue(val => !val);
2728 }
2829 }, 1000);
src/components/Buttons.js+15
@@ -4,3 +4,18 @@ import { Button } from 'indigo-react';
44// TODO: use svg icons
55export const ForwardButton = props => <Button accessory="→" {...props} />;
66export 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';
33
44import Footer from './Footer';
55
6−export default function FooterButton(props) {
6+export default function FooterButton({ as: As = Button, ...props }) {
77 return (
88 <Footer>
99 <Grid className="pt2">
1010 <Grid.Divider />
1111 <Grid.Item full>
12− <Button {...props} />
12+ <As {...props} />
1313 </Grid.Item>
1414 </Grid>
1515 </Footer>
src/components/InputSigil.js+3−3
@@ -1,8 +1,8 @@
11import React from 'react';
2−import cn from 'classnames';
32import * as ob from 'urbit-ob';
43
54import Sigil from './Sigil';
5+import AspectRatio from './AspectRatio';
66
77const selectColorway = (pass, fail, focused) => {
88 if (pass) {
@@ -20,7 +20,7 @@ const selectColorway = (pass, fail, focused) => {
2020 return ['#FFFFFF', '#7F7F7F'];
2121};
2222
23−export default function ValidatedSigil({
23+export default function InputSigil({
2424 className,
2525 patp,
2626 size,
@@ -35,6 +35,6 @@ export default function ValidatedSigil({
3535 return valid ? (
3636 <Sigil patp={patp} size={size} colorway={colorway} margin={0} {...rest} />
3737 ) : (
38− <div className={cn('bg-transparent', className)} />
38+ <AspectRatio aspectRatio={1} />
3939 );
4040}
src/components/Inputs.js+24−8
@@ -1,4 +1,4 @@
1−import React, { useState } from 'react';
1+import React, { useState, useMemo } from 'react';
22import * as bip39 from 'bip39';
33
44import InputSigil from 'components/InputSigil';
@@ -13,6 +13,7 @@ import {
1313} from 'lib/validators';
1414import { prependSig } from 'lib/transformers';
1515import useForm from 'indigo-react/lib/useForm';
16+import { AccessoryIcon } from 'indigo-react';
1617
1718// const RequiredInput = advancedInput({
1819// WrappedComponent: Input,
@@ -87,35 +88,39 @@ export function useHdPathInput(props) {
8788const kTicketValidators = [validateTicket, validateNotEmpty];
8889const kTicketTransformers = [prependSig];
8990//TODO needs to be fancier, displaying sig and dashes instead of •ing all
90−export function useTicketInput({ initialValue = '~', ...rest }) {
91− return firstOf(
91+export function useTicketInput({ validators = [], ...rest }) {
92+ const input = firstOf(
9293 useForm([
9394 {
9495 type: 'password',
95− placeholder: '~master-ticket',
96− validators: kTicketValidators,
96+ label: 'Ticket',
97+ placeholder: '~ragtyd-modwen-bostec-hinsep',
98+ validators: useMemo(() => [...validators, ...kTicketValidators], [
99+ validators,
100+ ]),
97101 transformers: kTicketTransformers,
98102 mono: true,
99− initialValue,
100103 ...rest,
101104 },
102105 ])
103106 );
107+
108+ return { ...input, accessory: input.pass ? <AccessoryIcon.Success /> : null };
104109}
105110
106111const kPointValidators = [validatePoint, validateNotEmpty];
107112const kPointTransformers = [prependSig];
108−export function usePointInput({ initialValue = '~', ...rest }) {
113+export function usePointInput(rest) {
109114 const [lastValidPoint, setLastValidPoint] = useState('');
110115 const input = firstOf(
111116 useForm([
112117 {
113118 type: 'text',
119+ label: 'Point',
114120 placeholder: 'e.g. ~zod',
115121 validators: kPointValidators,
116122 transformers: kPointTransformers,
117123 mono: true,
118− initialValue,
119124 onValue: setLastValidPoint,
120125 ...rest,
121126 },
@@ -139,6 +144,17 @@ export function usePointInput({ initialValue = '~', ...rest }) {
139144 };
140145}
141146
147+export function useCheckboxInput({ initialValue, ...rest }) {
148+ return firstOf(
149+ useForm([
150+ {
151+ type: 'checkbox',
152+ ...rest,
153+ },
154+ ])
155+ );
156+}
157+
142158const kEmailValidators = [validateEmail, validateNotEmpty];
143159export const buildEmailInputConfig = extra => ({
144160 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 @@
11import React from 'react';
2+import cn from 'classnames';
3+import Maybe from 'folktale/maybe';
24import * as ob from 'urbit-ob';
3−import { Grid, Flex } from 'indigo-react';
5+import { Grid, Flex, Text } from 'indigo-react';
46import { times } from 'lodash';
57
68import usePointBirthday from 'lib/usePointBirthday';
79import { formatDots } from 'lib/dateFormat';
810
9−import Sigil from './Sigil';
10−import Blinky, { kLoadingCharacter, kInterstitialCharacter } from './Blinky';
11+import Blinky, { LOADING_CHARACTER, INTERSTITIAL_CHARACTER } from './Blinky';
12+import MaybeSigil from './MaybeSigil';
1113
1214const buildDate = char =>
1315 [4, 2, 2].map(t => times(t, () => char).join('')).join('.');
14−const kDateA = buildDate(kLoadingCharacter);
15−const kDateB = buildDate(kInterstitialCharacter);
16+const DATE_A = buildDate(LOADING_CHARACTER);
17+const DATE_B = buildDate(INTERSTITIAL_CHARACTER);
1618
17−export default function Passport({ point }) {
18− const birthday = usePointBirthday(point);
19− const birthDate = birthday.matchWith({
20− Nothing: () => <Blinky a={kDateA} b={kDateB} />,
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+ */
35+export default function Passport({
36+ point,
37+ className,
38+ ticket = null,
39+ address = Maybe.Nothing(),
40+ mini = false,
41+}) {
42+ const birthday = usePointBirthday(point.getOrElse(null));
43+ const visualBirthday = birthday.matchWith({
44+ Nothing: () => <Blinky a={DATE_A} b={DATE_B} delayed />,
2145 Just: p => formatDots(p.value),
2246 });
23− const name = ob.patp(point);
47+
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}`;
2464
2565 return (
26− <Grid gap={16} className="bg-black r8 p7">
66+ <Grid gap={4} className={cn('bg-black r8 p7', className)}>
2767 <Grid.Item cols={[1, 4]} className="bg-green">
28− <Sigil patp={name} size={64} margin={0} />
68+ <MaybeSigil patp={name} size={64} margin={0} />
2969 </Grid.Item>
3070 <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>
3272 <Flex.Item as={Flex} col>
33− <Flex.Item className="mono gray4 f6 uppercase">Birth Time</Flex.Item>
34− <Flex.Item className="mono gray4 f6">{birthDate}</Flex.Item>
73+ <Flex.Item as={Text} className="mono f6 f5-ns gray4 uppercase">
74+ Birth Time
75+ </Flex.Item>
76+ <Flex.Item as={Text} className="mono f6 f5-ns gray4 ">
77+ {visualBirthday}
78+ </Flex.Item>
3579 </Flex.Item>
3680 </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+ )}
3799 </Grid>
38100 );
39101}
src/components/Sigil.js+12−7
@@ -3,11 +3,16 @@ import { pour } from 'sigil-js';
33import ReactSVGComponents from './ReactSVGComponents';
44
55export default function Sigil({ patp, size, colorway, ...rest }) {
6− return pour({
7− renderer: ReactSVGComponents,
8− patp,
9− size,
10− colorway,
11− ...rest,
12− });
6+ try {
7+ // https://github.com/urbit/sigil-js/issues/31
8+ return pour({
9+ renderer: ReactSVGComponents,
10+ patp,
11+ size,
12+ colorway,
13+ ...rest,
14+ });
15+ } catch {
16+ return null;
17+ }
1318}
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 @@
11import React from 'react';
22import ReactDOM from 'react-dom';
3+import { AppContainer } from 'react-hot-loader';
34
45import Bridge from './Bridge';
56
6−ReactDOM.render(<Bridge />, document.getElementById('root'));
7+const render = () =>
8+ ReactDOM.render(
9+ <AppContainer>
10+ <Bridge />
11+ </AppContainer>,
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';
33import Flex from './Flex';
44
55const PENDING_ACCESSORY = '⋯';
6−const SUCCESS_ACCESSORY = '✔';
6+const SUCCESS_ACCESSORY = '✓';
77const FAILURE_ACCESSORY = '×';
88
99function AccessoryIcon({ ...props }) {
src/indigo-react/components/Button.js+1−1
@@ -24,7 +24,7 @@ export default function Button({
2424 return (
2525 <Grid
2626 as="a"
27− gap={4}
27+ gap={1}
2828 className={cn(
2929 'pointer pv4 truncate flex-row justify-between',
3030 {
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 @@
11import React from 'react';
22import cn from 'classnames';
33
4−import getComponent from '../lib/getComponent';
5−
64function Flex({
7− as = 'div',
5+ as: As = 'div',
86 row = false,
97 wrap = false,
108 col = false,
@@ -16,10 +14,8 @@ function Flex({
1614 if (row && col) {
1715 throw new Error('Only one of row or col must be true, not both.');
1816 }
19−
20− const Component = getComponent(as);
2117 return (
22− <Component
18+ <As
2319 className={cn(
2420 'flex',
2521 {
@@ -39,14 +35,13 @@ function Flex({
3935}
4036
4137// 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 }) {
4339 if (flex === true) {
4440 flex = 1;
4541 }
4642
47− const Component = getComponent(as);
4843 return (
49− <Component
44+ <As
5045 className={cn(
5146 {
5247 [`flex${flex}`]: flex,
src/indigo-react/components/Grid.js+20−11
@@ -1,20 +1,25 @@
11import React from 'react';
22import cn from 'classnames';
33
4−import getComponent from '../lib/getComponent';
5−
6−function Grid({ as = 'div', gap = 0, className, ...rest }) {
7− const Component = getComponent(as);
4+function Grid({ as: As = 'div', gap = 0, align, justify, className, ...rest }) {
85 return (
9− <Component
10− className={cn('grid12', gap && `gap${gap}`, className)}
6+ <As
7+ className={cn(
8+ 'grid12',
9+ gap && `gap${gap}`,
10+ {
11+ [`justify-${justify}`]: justify,
12+ [`align-${align}`]: align,
13+ },
14+ className
15+ )}
1116 {...rest}
1217 />
1318 );
1419}
1520
1621Grid.Item = function GridItem({
17− as = 'div',
22+ as: As = 'div',
1823 full = false,
1924 half = 0,
2025 third = 0,
@@ -26,9 +31,8 @@ Grid.Item = function GridItem({
2631 className,
2732 ...rest
2833}) {
29− const Component = getComponent(as);
3034 return (
31− <Component
35+ <As
3236 className={cn(
3337 {
3438 full,
@@ -51,14 +55,19 @@ Grid.Item = function GridItem({
5155 );
5256};
5357
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+}) {
5564 return (
5665 <div
5766 style={{
5867 height: vertical ? 'auto' : '1px',
5968 width: vertical ? '1px' : 'auto',
6069 }}
61− className={cn('full', className || 'bg-gray2')}
70+ className={cn('full', `bg-${color}`, className)}
6271 {...rest}
6372 />
6473 );
src/indigo-react/components/Input.js+11−10
@@ -69,7 +69,15 @@ export default function Input({
6969 }, [focused]); // eslint-disable-line react-hooks/exhaustive-deps
7070
7171 return (
72− <Flex className={className} col>
72+ <Flex
73+ className={className}
74+ col
75+ style={{
76+ ...(disabled && {
77+ pointerEvents: 'none',
78+ cursor: 'not-allowed',
79+ }),
80+ }}>
7381 <Flex.Item as="label" className="f6 lh-tall" htmlFor={name}>
7482 {label}
7583 </Flex.Item>
@@ -77,9 +85,8 @@ export default function Input({
7785 <Flex.Item
7886 as="input"
7987 {...rest}
80− cols={[1, accessory ? 11 : 13]}
8188 className={cn(
82− 'rel b b1 p3 outline-none',
89+ 'b b1 p3 outline-none',
8390 { mono },
8491 {
8592 'bg-white': !disabled,
@@ -92,17 +99,11 @@ export default function Input({
9299 {
93100 'b-green3': visiblyPassed,
94101 'b-black': focused && !visiblyPassed,
95− 'b-yellow3': !focused && !visiblyPassed && error,
102+ 'b-yellow3': !focused && error,
96103 'b-gray3': !focused && !error && !visiblyPassed,
97104 }
98105 // TODO: inputClassName ?
99106 )}
100− style={{
101− ...(disabled && {
102− pointerEvents: 'none',
103− cursor: 'not-allowed',
104− }),
105− }}
106107 id={name}
107108 name={name}
108109 onKeyPress={onKeyPress}
src/indigo-react/components/Typography.js+4−6
@@ -1,12 +1,9 @@
11import React from 'react';
22import cn from 'classnames';
33
4−import getComponent from '../lib/getComponent';
5−
64const typeWith = (DefaultComponent, defaultClassName) =>
7− function({ as = DefaultComponent, className, ...rest }) {
8− const Component = getComponent(as);
9− return <Component className={cn(defaultClassName, className)} {...rest} />;
5+ function({ as: As = DefaultComponent, className, ...rest }) {
6+ return <As className={cn(defaultClassName, className)} {...rest} />;
107 };
118
129export const H1 = typeWith('h1');
@@ -17,8 +14,9 @@ export const H5 = typeWith('h5');
1714export const H6 = typeWith('h6');
1815export const Caption = typeWith('caption');
1916export const P = typeWith('p');
17+export const B = typeWith('b', 'fw-bold');
2018
2119export const Breadcrumb = typeWith('span', 'f6');
22−export const Text = typeWith('span', 'f5');
20+export const Text = typeWith('span');
2321export const HelpText = typeWith('span', 'f6 gray4');
2422export const ErrorText = typeWith('span', 'f6 red3');
src/indigo-react/index.js+1
@@ -2,6 +2,7 @@ export { default as IndigoApp } from './components/IndigoApp';
22export { default as Grid } from './components/Grid';
33export { default as Flex } from './components/Flex';
44export { default as Input } from './components/Input';
5+export { default as CheckboxInput } from './components/CheckboxInput';
56export { default as AccessoryIcon } from './components/AccessoryIcon';
67export { default as Button } from './components/Button';
78export { default as IconButton } from './components/IconButton';
src/indigo-react/indigo.css+39−37
@@ -13,7 +13,7 @@
1313 * git+https://github.com/urbit/Indigo.git
1414 * Bugs: https://github.com/urbit/Indigo/issues
1515 * 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)
1717 *******************************************************************************/
1818/**********************
1919Color variables
@@ -734,13 +734,18 @@ caption,
734734.pre {
735735 white-space: pre; }
736736
737+.wrap {
738+ word-break: break-word; }
739+
737740@media only screen and (min-width: 550px) {
738741 .ws-normal-ns {
739742 white-space: normal; }
740743 .nowrap-ns {
741744 white-space: nowrap; }
742745 .pre-ns {
743− white-space: pre; } }
746+ white-space: pre; }
747+ .wrap-ns {
748+ word-break: break-word; } }
744749
745750@media only screen and (min-width: 750px) and (max-width: 960px) {
746751 .ws-normal-md {
@@ -748,7 +753,9 @@ caption,
748753 .nowrap-md {
749754 white-space: nowrap; }
750755 .pre-md {
751− white-space: pre; } }
756+ white-space: pre; }
757+ .wrap-md {
758+ word-break: break-word; } }
752759
753760@media only screen and (min-width: 960px) {
754761 .ws-normal-lg {
@@ -756,7 +763,9 @@ caption,
756763 .nowrap-lg {
757764 white-space: nowrap; }
758765 .pre-lg {
759− white-space: pre; } }
766+ white-space: pre; }
767+ .wrap-lg {
768+ word-break: break-word; } }
760769
761770/*******************************************************************************
762771 Border Radius
@@ -1531,43 +1540,33 @@ caption,
15311540 flex-wrap: wrap; }
15321541
15331542.align-start {
1534− display: flex;
15351543 align-items: flex-start; }
15361544
15371545.align-end {
1538− display: flex;
15391546 align-items: flex-end; }
15401547
15411548.align-center {
1542− display: flex;
15431549 align-items: center; }
15441550
15451551.align-baseline {
1546− display: flex;
15471552 align-items: baseline; }
15481553
15491554.align-stretch {
1550− display: flex;
15511555 align-items: stretch; }
15521556
15531557.justify-start {
1554− display: flex;
15551558 justify-content: flex-start; }
15561559
15571560.justify-end {
1558− display: flex;
15591561 justify-content: flex-end; }
15601562
15611563.justify-center {
1562− display: flex;
15631564 justify-content: center; }
15641565
15651566.justify-between {
1566− display: flex;
15671567 justify-content: space-between; }
15681568
15691569.justify-around {
1570− display: flex;
15711570 justify-content: space-around; }
15721571
15731572.flex-center {
@@ -1627,16 +1626,12 @@ caption,
16271626 .flex-ns {
16281627 display: flex; }
16291628 .flex-row-ns {
1630− display: flex;
16311629 flex-direction: row; }
16321630 .flex-row-r-ns {
1633− display: flex;
16341631 flex-direction: row-reverse; }
16351632 .flex-col-ns {
1636− display: flex;
16371633 flex-direction: column; }
16381634 .flex-col-r-ns {
1639− display: flex;
16401635 flex-direction: column-reverse; }
16411636 .flex-wrap-ns {
16421637 flex-wrap: wrap; }
@@ -1700,16 +1695,12 @@ caption,
17001695 .flex-md {
17011696 display: flex; }
17021697 .flex-row-md {
1703− display: flex;
17041698 flex-direction: row; }
17051699 .flex-row-r-md {
1706− display: flex;
17071700 flex-direction: row-reverse; }
17081701 .flex-col-md {
1709− display: flex;
17101702 flex-direction: column; }
17111703 .flex-col-r-md {
1712− display: flex;
17131704 flex-direction: column-reverse; }
17141705 .flex-wrap-md {
17151706 flex-wrap: wrap; }
@@ -1773,16 +1764,12 @@ caption,
17731764 .flex-lg {
17741765 display: flex; }
17751766 .flex-row-lg {
1776− display: flex;
17771767 flex-direction: row; }
17781768 .flex-row-r-lg {
1779− display: flex;
17801769 flex-direction: row-reverse; }
17811770 .flex-col-lg {
1782− display: flex;
17831771 flex-direction: column; }
17841772 .flex-col-r-lg {
1785− display: flex;
17861773 flex-direction: column-reverse; }
17871774 .flex-wrap-lg {
17881775 flex-wrap: wrap; }
@@ -2032,30 +2019,45 @@ caption,
20322019 .fourth-4-lg {
20332020 grid-area: auto / 10 / auto / 13; } }
20342021
2035−.gap1 {
2036− gap: 1px; }
2037−
2038−.gap2 {
2039− gap: 2px; }
2022+.gap0 {
2023+ gap: 0px; }
20402024
2041−.gap4 {
2025+.gap1 {
20422026 gap: 4px; }
20432027
2044−.gap8 {
2028+.gap2 {
20452029 gap: 8px; }
20462030
2047−.gap12 {
2031+.gap3 {
20482032 gap: 12px; }
20492033
2050−.gap16 {
2034+.gap4 {
20512035 gap: 16px; }
20522036
2053−.gap24 {
2037+.gap5 {
2038+ gap: 20px; }
2039+
2040+.gap6 {
20542041 gap: 24px; }
20552042
2056−.gap32 {
2043+.gap7 {
20572044 gap: 32px; }
20582045
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+
20592061.c1-2 {
20602062 grid-column-start: 1;
20612063 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 = []) {
2929
3030 // track values
3131 const [values, _setValues] = useState(() =>
32− defaultsFor(configs, config => config.initialValue)
33− );
34− const setValue = useCallback(
35− (name, value) => _setValues(values => ({ ...values, [name]: value })),
36− [_setValues]
32+ defaultsFor(configs, config => config.initialValue || '')
3733 );
3834 // ^ NB(shrugs): because we're not syncing the initialValue of _additional_
3935 // configs that are added, we won't be able to set an initialValue for a
@@ -42,6 +38,11 @@ export default function useForm(configs = []) {
4238 // but for dynamically added input configs, we won't get validation, etc
4339 // until there's a traditional state update.
4440
41+ const setValue = useCallback(
42+ (name, value) => _setValues(values => ({ ...values, [name]: value })),
43+ [_setValues]
44+ );
45+
4546 // track focused states
4647 const [focused, setFocused] = useSetState(() =>
4748 defaultsFor(configs, config => config.autoFocus && !config.disabled)
@@ -70,11 +71,23 @@ export default function useForm(configs = []) {
7071 [byName]
7172 );
7273
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+
7385 // on change, transform and set value
7486 const onChange = useCallback(
75− name => e => setValue(name, transform(name, e.target.value)), //
76− [setValue, transform]
87+ name => e => setValue(name, transform(name, getValue(name, e))), //
88+ [setValue, transform, getValue]
7789 );
90+
7891 // on focus, update focus
7992 const onFocus = useCallback(
8093 name => e => setFocused({ [name]: true }), //
@@ -85,7 +98,7 @@ export default function useForm(configs = []) {
8598 const onBlur = useCallback(
8699 name => e => {
87100 setFocused({ [name]: false });
88− setHasBeenFocused({ [name]: false });
101+ setHasBeenFocused({ [name]: true });
89102 },
90103 [setFocused, setHasBeenFocused]
91104 );
@@ -153,7 +166,8 @@ export default function useForm(configs = []) {
153166 ...rest,
154167 // dom properties below:
155168 bind: {
156− value: values[name] || initialValue,
169+ value: values[name] || initialValue || '',
170+ checked: !!values[name],
157171 onChange: onChange(name),
158172 onFocus: onFocus(name),
159173 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;
55const MIN_PLANET = 65536;
66const MAX_PLANET = 4294967297;
77
8+const ZOD = MIN_GALAXY;
9+
810const PLANET_ENTROPY_BITS = 64;
911const STAR_ENTROPY_BITS = 128;
1012const GALAXY_ENTROPY_BITS = 384;
@@ -56,4 +58,5 @@ export {
5658 STAR_ENTROPY_BITS,
5759 GALAXY_ENTROPY_BITS,
5860 SEED_ENTROPY_BITS,
61+ ZOD,
5962};
src/lib/invite.js+57−106
@@ -1,4 +1,4 @@
1−import { Just, Nothing } from 'folktale/maybe';
1+import { Just } from 'folktale/maybe';
22import Tx from 'ethereumjs-tx';
33
44import * as azimuth from 'azimuth-js';
@@ -9,8 +9,7 @@ import * as tank from './tank';
99import JSZip from 'jszip';
1010import saveAs from 'file-saver';
1111
12−import { sendSignedTransaction } from './txn';
13−import { BRIDGE_ERROR } from './error';
12+import { sendSignedTransaction, hexify } from './txn';
1413import { attemptNetworkSeedDerivation } from './keys';
1514import { addHexPrefix, WALLET_TYPES } from './wallet';
1615
@@ -32,36 +31,36 @@ const WALLET_STATES = {
3231
3332const TRANSACTION_STATES = {
3433 GENERATING: {
35− label: 'Generating transactions',
36− pct: '0%',
34+ label: 'Generating Transactions...',
35+ progress: 0.0,
3736 },
3837 SIGNING: {
39− label: 'Signing transactions',
40− pct: '15%',
38+ label: 'Signing Transactions...',
39+ progress: 0.15,
4140 },
4241 FUNDING_INVITE: {
43− label: 'Funding invite wallet',
44− pct: '30%',
42+ label: 'Funding Invite Wallet...',
43+ progress: 0.3,
4544 },
4645 CLAIMING: {
47− label: 'Claiming invite',
48− pct: '55%',
46+ label: 'Claiming Invite...',
47+ progress: 0.55,
4948 },
5049 FUNDING_RECIPIENT: {
51− label: 'Funding recipient wallet',
52− pct: '65%',
50+ label: 'Funding Recipient Wallet...',
51+ progress: 0.65,
5352 },
5453 CONFIGURING: {
55− label: 'Configuring planet',
56− pct: '85%',
54+ label: 'Configuring Point...',
55+ progress: 0.85,
5756 },
5857 CLEANING: {
59− label: 'Cleaning up',
60− pct: '95%',
58+ label: 'Cleaning Up...',
59+ progress: 0.95,
6160 },
6261 DONE: {
6362 label: 'Done',
64− pct: '100%',
63+ progress: 1.0,
6564 },
6665};
6766
@@ -102,93 +101,61 @@ async function downloadWallet(paper) {
102101 });
103102}
104103
105−async function startTransactions(args) {
106− let {
107− realPointM,
108− web3: web3M,
109− contracts: contractsM,
110− inviteWalletM,
111− realWalletM,
112− setUrbitWallet,
113− updateProgress,
114− } = args;
115−
116− const askForFunding = (address, amount, current) => {
117− updateProgress({
118− type: 'notify',
119− value: `Please make sure ${address} has at least ${amount} wei, we'll continue once that's true. Current balance: ${current}. Waiting`,
104+async function claimPointFromInvite({
105+ inviteWallet,
106+ wallet,
107+ point,
108+ web3,
109+ contracts,
110+ onUpdate,
111+}) {
112+ const askForFunding = (address, minBalance, balance) =>
113+ onUpdate({
114+ type: 'askFunding',
115+ value: { address, minBalance, balance },
120116 });
121− };
122117
123− if (Nothing.hasInstance(web3M)) {
124− throw BRIDGE_ERROR.MISSING_WEB3;
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;
118+ const gotFunding = () => onUpdate({ type: 'gotFunding' });
119+ const progress = state => onUpdate({ type: 'progress', state });
149120
150121 const inviteAddress = inviteWallet.address;
151− console.log('working as', inviteAddress);
152122
153− // transfer from invite wallet to new wallet
123+ progress(TRANSACTION_STATES.GENERATING);
154124
155− let transferTx = azimuth.ecliptic.transferPoint(
125+ // transfer from invite wallet to new wallet
126+ const transferTx = azimuth.ecliptic.transferPoint(
156127 contracts,
157128 point,
158− realWallet.ownership.keys.address
129+ wallet.ownership.keys.address
159130 );
160131 transferTx.gas = 500000; //TODO can maybe be lower?
161132
162133 // ping gas tank with txs if needed
163−
164134 let inviteNonce = await web3.eth.getTransactionCount(inviteAddress);
165135 transferTx.nonce = inviteNonce++;
166136 transferTx.gasPrice = 20000000000; //NOTE we pay the premium for faster ux
167137 transferTx.from = inviteAddress;
168138
169− //NOTE using web3.eth.accounts.signTransaction is broken (1.0.0-beta51)
170− let transferStx = new Tx(transferTx);
139+ progress(TRANSACTION_STATES.SIGNING);
140+ // NOTE: using web3.eth.accounts.signTransaction is broken (1.0.0-beta51)
141+ const transferStx = new Tx(transferTx);
171142 transferStx.sign(inviteWallet.privateKey);
172− let rawTransferStx = '0x' + transferStx.serialize().toString('hex');
173−
143+ const rawTransferStx = hexify(transferStx.serialize());
174144 const transferCost = transferTx.gas * transferTx.gasPrice;
175− updateProgress({
176− type: 'progress',
177− value: TRANSACTION_STATES.FUNDING_INVITE,
178− });
145+
146+ progress(TRANSACTION_STATES.FUNDING_INVITE);
147+
179148 let usedTank = await tank.ensureFundsFor(
180149 web3,
181150 point,
182151 inviteAddress,
183152 transferCost,
184153 [rawTransferStx],
185− askForFunding
154+ askForFunding,
155+ gotFunding
186156 );
187157
188− updateProgress({
189− type: 'progress',
190− value: TRANSACTION_STATES.CLAIMING,
191− });
158+ progress(TRANSACTION_STATES.CLAIMING);
192159
193160 // send transaction
194161 await sendAndAwaitConfirm(web3, [Just(transferStx)], usedTank);
@@ -197,15 +164,13 @@ async function startTransactions(args) {
197164 // we're gonna be operating as the new wallet from here on out, so change
198165 // the relevant values
199166 //
200− setUrbitWallet(Just(realWallet));
201− const newAddress = realWallet.ownership.keys.address;
167+ const newAddress = wallet.ownership.keys.address;
202168
203169 // configure management proxy
204−
205− let managementTx = azimuth.ecliptic.setManagementProxy(
170+ const managementTx = azimuth.ecliptic.setManagementProxy(
206171 contracts,
207172 point,
208− realWallet.management.keys.address
173+ wallet.management.keys.address
209174 );
210175 managementTx.gas = 200000;
211176 managementTx.nonce = 0;
@@ -214,7 +179,7 @@ async function startTransactions(args) {
214179 //TODO feels like more of this logic should live in a lib?
215180 const seed = await attemptNetworkSeedDerivation(true, {
216181 walletType: WALLET_TYPES.TICKET,
217− urbitWallet: Just(realWallet),
182+ urbitWallet: Just(wallet),
218183 pointCursor: Just(point),
219184 pointCache: { [point]: { keyRevisionNumber: 0 } },
220185 });
@@ -251,17 +216,15 @@ async function startTransactions(args) {
251216
252217 let txPairs = txs.map(tx => {
253218 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'));
255220 return {
256221 raw: '0x' + stx.serialize().toString('hex'),
257222 signed: stx,
258223 };
259224 });
260225
261− updateProgress({
262− type: 'progress',
263− value: TRANSACTION_STATES.FUNDING_RECIPIENT,
264− });
226+ progress(TRANSACTION_STATES.FUNDING_RECIPIENT);
227+
265228 usedTank = await tank.ensureFundsFor(
266229 web3,
267230 point,
@@ -271,17 +234,11 @@ async function startTransactions(args) {
271234 askForFunding
272235 );
273236
274− updateProgress({
275− type: 'progress',
276− value: TRANSACTION_STATES.CONFIGURING,
277− });
237+ progress(TRANSACTION_STATES.CONFIGURING);
278238
279239 await sendAndAwaitConfirm(web3, txPairs.map(p => Just(p.signed)), usedTank);
280240
281− updateProgress({
282− type: 'progress',
283− value: TRANSACTION_STATES.CLEANING,
284− });
241+ progress(TRANSACTION_STATES.CLEANING);
285242
286243 // if non-trivial eth left in invite wallet, transfer to new ownership
287244 let balance = await web3.eth.getBalance(inviteAddress);
@@ -300,20 +257,14 @@ async function startTransactions(args) {
300257 };
301258 let stx = new Tx(tx);
302259 stx.sign(inviteWallet.privateKey);
303− const rawTx = '0x' + stx.serialize().toString('hex');
260+ const rawTx = hexify(stx.serialize());
304261 web3.eth.sendSignedTransaction(rawTx).catch(err => {
305262 console.log('error sending value tx, who cares', err);
306263 });
307264 console.log('sent old balance');
308265 }
309266
310− updateProgress({
311− type: 'progress',
312− value: TRANSACTION_STATES.DONE,
313− });
314−
315− // proceed without waiting for confirm
316− setUrbitWallet(Just(realWallet));
267+ progress(TRANSACTION_STATES.DONE);
317268}
318269
319270async function sendAndAwaitConfirm(web3, signedTxs, usedTank) {
@@ -329,7 +280,7 @@ async function sendAndAwaitConfirm(web3, signedTxs, usedTank) {
329280export {
330281 generateWallet,
331282 downloadWallet,
332− startTransactions,
283+ claimPointFromInvite,
333284 INVITE_STAGES,
334285 WALLET_STATES,
335286 TRANSACTION_STATES,
src/lib/keys.js+1−1
@@ -73,7 +73,7 @@ const attemptNetworkSeedDerivation = async (next, args) => {
7373 //
7474 // following code is intentionally verbose for sake of clarity
7575
76− const point = need.pointCursor(args.pointCursor);
76+ const point = need.point(args.pointCursor);
7777 const pointDetails = need.fromPointCache(args.pointCache, point);
7878
7979 const revision =
src/lib/need.js+27−12
@@ -3,34 +3,49 @@
33
44import { BRIDGE_ERROR } from './error';
55
6−const needBuilder = error => obj => {
6+const needBuilder = fn => obj => {
77 if (!obj) {
8− throw error;
8+ fn();
99 }
1010
1111 return obj.matchWith({
12− Nothing: () => {
13− throw error;
14− },
12+ Nothing: fn,
1513 Just: p => p.value,
1614 });
1715};
1816
19−export const web3 = needBuilder(BRIDGE_ERROR.MISSING_WEB3);
20−export const contracts = needBuilder(BRIDGE_ERROR.MISSING_CONTRACTS);
21−export const wallet = needBuilder(BRIDGE_ERROR.MISSING_WALLET);
17+export const web3 = needBuilder(() => {
18+ throw new Error(BRIDGE_ERROR.MISSING_WEB3.message);
19+});
20+
21+export const contracts = needBuilder(() => {
22+ throw new Error(BRIDGE_ERROR.MISSING_CONTRACTS.message);
23+});
24+
25+export const wallet = needBuilder(() => {
26+ throw new Error(BRIDGE_ERROR.MISSING_WALLET.message);
27+});
28+
2229export const addressFromWallet = obj => wallet(obj).address;
23−export const pointCursor = needBuilder(BRIDGE_ERROR.MISSING_POINT);
24−export const pointCache = needBuilder(BRIDGE_ERROR.MISSING_POINT);
30+
31+export const point = needBuilder(() => {
32+ throw new Error(BRIDGE_ERROR.MISSING_POINT.message);
33+});
34+
35+export const pointCache = needBuilder(() => {
36+ throw new Error(BRIDGE_ERROR.MISSING_POINT.message);
37+});
38+
2539export const fromPointCache = (cache, point) => {
2640 if (!(point in cache)) {
27− throw BRIDGE_ERROR.MISSING_POINT;
41+ throw new Error(BRIDGE_ERROR.MISSING_POINT.message);
2842 }
2943
3044 return cache[point];
3145};
46+
3247export const keystore = obj => {
33− const ks = needBuilder(BRIDGE_ERROR.MISSING_KEYSTORE)();
48+ const ks = needBuilder(BRIDGE_ERROR.MISSING_KEYSTORE)(obj);
3449 return ks.value.matchWith({
3550 Ok: result => result.value,
3651 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 @@
11export const ROUTE_NAMES = {
22 DEFAULT: Symbol('DEFAULT'),
33 //
4− INVITE_TICKET: Symbol('INVITE_TICKET'),
4+ ACTIVATE: Symbol('ACTIVATE'),
55 INVITE_TRANSACTIONS: Symbol('INVITE_TRANSACTIONS'),
66 INVITE: Symbol('INVITE'),
77 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−
41import Landing from '../views/Landing';
52import Login from '../views/Login.js';
6−import InviteTicket from '../views/InviteTicket';
73import InvitesManage from '../views/InvitesManage.js';
84import AcceptTransfer from '../views/AcceptTransfer';
95import CancelTransfer from '../views/CancelTransfer';
@@ -22,13 +18,13 @@ import Point from '../views/Point';
2218import Points from '../views/Points';
2319import Transfer from '../views/Transfer';
2420import Invite from 'views/Invite';
21+import Activate from 'views/Activate';
2522
2623import { ROUTE_NAMES } from './routeNames';
2724
2825export const ROUTES = {
29− [ROUTE_NAMES.DEFAULT]: Landing,
3026 [ROUTE_NAMES.LANDING]: Landing,
31− [ROUTE_NAMES.INVITE_TICKET]: InviteTicket,
27+ [ROUTE_NAMES.ACTIVATE]: Activate,
3228 [ROUTE_NAMES.INVITE]: Invite,
3329 [ROUTE_NAMES.INVITES_MANAGE]: InvitesManage,
3430 [ROUTE_NAMES.LOGIN]: Login,
@@ -47,47 +43,3 @@ export const ROUTES = {
4743 [ROUTE_NAMES.SENT_TRANSACTION]: SentTransaction,
4844 [ROUTE_NAMES.GEN_KEYFILE]: GenKeyfile,
4945};
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 @@
11import { useRef } from 'react';
22
3−export default function useRenderCount(debubgLabel = 'Component') {
3+export default function useRenderCount(debugLabel = 'Component') {
44 const count = useRef(0);
55 count.current++;
6− console.log(`${debubgLabel} — ${count.current}`);
6+ console.log(`${debugLabel} — ${count.current}`);
77}
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({
2424 )}`
2525 );
2626 }
27−
2827 setRoutes(routes => [...routes, { key, data }]);
2928 },
3029 [setRoutes, views]
@@ -55,6 +54,13 @@ export default function useRouter({
5554 [setRoutes]
5655 );
5756 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+ ]);
5864 const includes = useCallback(key => _includes(routes.map(r => r.key), key), [
5965 routes,
6066 ]);
@@ -62,12 +68,8 @@ export default function useRouter({
6268 const data = useMemo(() => last(routes).data, [routes]);
6369 const Route = useMemo(() => views[peek().key], [views, peek]);
6470
65− // Scroll to top of page with each route transition on primary navigation
71+ // Scroll to top of page with each route transition
6672 useEffect(() => {
67− if (!primary) {
68− return;
69− }
70−
7173 window.scrollTo(0, 0);
7274 }, [routes, primary]);
7375
@@ -94,6 +96,8 @@ export default function useRouter({
9496 push,
9597 popAndPush,
9698 pop,
99+ replaceWith,
100+ reset,
97101 peek,
98102 size,
99103 includes,
src/lib/useSyncPoints.js+23−30
@@ -1,39 +1,32 @@
11import { useEffect } from 'react';
2−import { isEqual } from 'lodash';
2+import { difference } from 'lodash';
33
44import { usePointCache } from 'store/pointCache';
55import usePreviousValue from 'lib/usePreviousValue';
66
7−// sync a known (i.e. mine) point to the cache (i.e display values)
8−export function useSyncKnownPoints(points = []) {
9− // TBD: right now we don't need anything more than the point itself
10− // but soon we'll need to know information around "key type"
11−}
7+const buildSyncHook = getFn =>
8+ function SyncPoint(points = []) {
9+ const fn = getFn();
10+ const prevPoints = usePreviousValue(points);
1211
13−// sync a foreign (not the user's) point to the cache
14−export function useSyncForeignPoints(points = []) {
15− const { syncForeignPoint } = usePointCache();
16− const prevPoints = usePreviousValue(points);
12+ useEffect(() => {
13+ // sync points that are new between renders
14+ const newPoints = difference(points, prevPoints);
15+ Promise.all(newPoints.map(fn));
16+ }, [fn, prevPoints, points]);
17+ };
1718
18− useEffect(() => {
19− if (!isEqual(prevPoints, points)) {
20− // if the actual set of points changes, sync the whole array
21− // TODO: we should get clever about caching
22− points.forEach(point => syncForeignPoint(point));
23− }
24− }, [syncForeignPoint, prevPoints, points]);
25−}
19+export const useSyncKnownPoints = buildSyncHook(function() {
20+ const { syncKnownPoint } = usePointCache();
21+ return syncKnownPoint;
22+});
2623
27−// sync an owned point to the cache
28−export function useSyncOwnedPoints(points = []) {
29− const { syncOwnedPoint } = usePointCache();
30− const prevPoints = usePreviousValue(points);
24+export const useSyncForeignPoints = buildSyncHook(function() {
25+ const { syncForeignPoint } = usePointCache();
26+ return syncForeignPoint;
27+});
3128
32− useEffect(() => {
33− if (!isEqual(prevPoints, points)) {
34− // if the actual set of points changes, sync the whole array
35− // TODO: we should get clever about caching
36− points.forEach(point => syncOwnedPoint(point));
37− }
38− }, [syncOwnedPoint, prevPoints, points]);
39−}
29+export const useSyncOwnedPoints = buildSyncHook(function() {
30+ const { syncOwnedPoint } = usePointCache();
31+ return syncOwnedPoint;
32+});
src/lib/validators.js+37−7
@@ -1,7 +1,9 @@
11import * as bip39 from 'bip39';
22import * as ob from 'urbit-ob';
3+import { identity } from 'lodash';
34
45import { isValidAddress } from './wallet';
6+import patp2dec from './patp2dec';
57
68// via: https://emailregex.com/
79const 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@"]+)*)|(".+"))
1012const hexRegExp = /[0-9A-Fa-f]{64}/g;
1113
1214// 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+}) => {
1421 // If a previous validation has already failed, skip this validation and
1522 // return the prev message to the next stage in the validation function chain.
1623 // Failed validations should drop all the way down the chain and drop out of
1724 // the output.
18− if (!config.prevMessage.pass) {
19− return config.prevMessage;
25+ if (!prevMessage.pass) {
26+ return prevMessage;
2027 }
2128
2229 // Run the validator and return the result.
23− return config.validator(config.prevMessage.data)
24− ? newMessage(config.prevMessage.data, true, null)
25− : newMessage(config.prevMessage.data, false, config.error);
30+ const pass = validator(prevMessage.data);
31+ const data = pass ? transform(prevMessage.data) : prevMessage.data;
32+
33+ return newMessage(data, pass, !pass && error);
2634};
2735
2836// Validation message
@@ -73,7 +81,7 @@ export const validateGalaxy = m =>
7381 validator: d => {
7482 let point;
7583 try {
76− point = parseInt(ob.patp2dec(d), 10);
84+ point = patp2dec(d);
7785 return point >= 0 && point < 256;
7886 } catch (e) {
7987 return false;
@@ -162,3 +170,25 @@ export const validateEmail = m =>
162170 validator: d => emailRegExp.test(d),
163171 error: 'This is not a valid email address',
164172 });
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 {
1010 GALAXY_ENTROPY_BITS,
1111 STAR_ENTROPY_BITS,
1212 PLANET_ENTROPY_BITS,
13+ ZOD,
1314} from './constants';
1415
1516const SEED_LENGTH_BYTES = SEED_ENTROPY_BITS / 8;
@@ -66,9 +67,13 @@ export const generateWallet = async (point, ticket, boot) => {
6667export const generateOwnershipWallet = (ship, ticket) =>
6768 kg.generateOwnershipWallet({ ship, ticket });
6869
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);
6974export const generateTemporaryTicketAndWallet = async point => {
7075 const ticket = await makeTicket(point);
71− const owner = await generateOwnershipWallet(0, ticket);
76+ const owner = await generateOwnershipWallet(ZOD, ticket);
7277
7378 return { ticket, owner };
7479};
src/store/Provider.js+2
@@ -8,6 +8,7 @@ import { WalletProvider } from './wallet';
88import { PointCursorProvider } from './pointCursor';
99import { PointCacheProvider } from './pointCache';
1010import { TxnCursorProvider } from './txnCursor';
11+import { MediaQueryProvider } from 'lib/MediaQuery';
1112
1213// nest all of the providers within each other to avoid hella depth
1314export default nest([
@@ -19,4 +20,5 @@ export default nest([
1920 PointCursorProvider,
2021 PointCacheProvider,
2122 TxnCursorProvider,
23+ MediaQueryProvider,
2224]);
src/store/lib/useBirthdaysStore.js+6
@@ -45,6 +45,12 @@ export default function useBirthdayStore() {
4545 addToBirthdayCache({
4646 [point]: Maybe.Just(new Date(block.timestamp * 1000)),
4747 });
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+ });
4854 }
4955 },
5056 [contracts, web3, addToBirthdayCache, getBirthday]
src/store/lib/useControlledPointsStore.js+3−3
@@ -5,7 +5,7 @@ import * as azimuth from 'azimuth-js';
55import { useNetwork } from '../network';
66import { useWallet } from 'store/wallet';
77
8−const kEmptyControlledPoints = {
8+const EMPTY_CONTROLLED_POINTS = {
99 ownedPoints: Maybe.Nothing(),
1010 incomingPoints: Maybe.Nothing(),
1111 managingPoints: Maybe.Nothing(),
@@ -17,11 +17,11 @@ export default function useControlledPointsStore() {
1717 const { contracts } = useNetwork();
1818 const { wallet } = useWallet();
1919 const [controlledPointsCache, _setControlledPointsCache] = useState(
20− kEmptyControlledPoints
20+ EMPTY_CONTROLLED_POINTS
2121 );
2222
2323 const getControlledPoints = useCallback(
24− () => controlledPointsCache || kEmptyControlledPoints,
24+ () => controlledPointsCache || EMPTY_CONTROLLED_POINTS,
2525 [controlledPointsCache]
2626 );
2727
src/store/lib/useInvitesStore.js+2−2
@@ -5,7 +5,7 @@ import * as azimuth from 'azimuth-js';
55import { useNetwork } from '../network';
66
77// the default value of a point's invites
8−const kEmptyInvites = {
8+const EMPTY_INVITES = {
99 availableInvites: Maybe.Nothing(),
1010 sentInvites: Maybe.Nothing(),
1111 acceptedInvites: Maybe.Nothing(),
@@ -25,7 +25,7 @@ export default function useInvitesStore() {
2525 );
2626
2727 const getInvites = useCallback(
28− point => invitesCache[point] || kEmptyInvites,
28+ point => invitesCache[point] || EMPTY_INVITES,
2929 [invitesCache]
3030 );
3131
src/store/lib/usePointStore.js+9−2
@@ -16,6 +16,12 @@ export default function usePointStore() {
1616 ...controlledPoints
1717 } = useControlledPointsStore();
1818
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+
1925 // sync all of the on-chain info required to display a foreign point
2026 const syncForeignPoint = useCallback(async point => Promise.all([]), []);
2127
@@ -23,11 +29,11 @@ export default function usePointStore() {
2329 const syncOwnedPoint = useCallback(
2430 async point =>
2531 Promise.all([
32+ syncKnownPoint(point),
2633 syncDetails(point),
27− syncBirthday(point),
2834 syncInvites(point),
2935 ]),
30− [syncDetails, syncBirthday, syncInvites]
36+ [syncKnownPoint, syncDetails, syncInvites]
3137 );
3238
3339 return {
@@ -38,6 +44,7 @@ export default function usePointStore() {
3844 ...ecliptic,
3945 syncInvites,
4046 syncControlledPoints,
47+ syncKnownPoint,
4148 syncOwnedPoint,
4249 syncForeignPoint,
4350 };
src/store/wallet.js+3−9
@@ -18,7 +18,7 @@ import { BRIDGE_ERROR } from 'lib/error';
1818
1919export const WalletContext = createContext(null);
2020
21−const kDefaultWalletType = WALLET_TYPES.TICKET;
21+const DEFAULT_WALLET_TYPE = WALLET_TYPES.TICKET;
2222
2323// NB (jtobin):
2424//
@@ -36,7 +36,7 @@ function _useWallet(
3636 initialWallet = Maybe.Nothing(),
3737 initialMnemonic = Maybe.Nothing()
3838) {
39− const [walletType, _setWalletType] = useState(kDefaultWalletType);
39+ const [walletType, _setWalletType] = useState(DEFAULT_WALLET_TYPE);
4040 const [walletHdPath, setWalletHdPath] = useState(DEFAULT_HD_PATH);
4141 const [wallet, _setWallet] = useState(initialWallet);
4242 const [urbitWallet, _setUrbitWallet] = useState(Maybe.Nothing());
@@ -79,12 +79,6 @@ function _useWallet(
7979 urbitWallet.value.meta.passphrase
8080 );
8181
82− // manually set the public address
83− wallet.map(wal => {
84− wal.address = urbitWallet.value.ownership.keys.address;
85− return wal;
86− });
87−
8882 setWallet(wallet);
8983 } else {
9084 setWallet(Maybe.Nothing());
@@ -96,7 +90,7 @@ function _useWallet(
9690 );
9791
9892 const resetWallet = useCallback(() => {
99− _setWalletType(kDefaultWalletType);
93+ _setWalletType(DEFAULT_WALLET_TYPE);
10094 setWalletHdPath(DEFAULT_HD_PATH);
10195 _setWallet(Maybe.Nothing());
10296 _setUrbitWallet(Maybe.Nothing());
src/style/indigo-proposals.scss+8
@@ -31,3 +31,11 @@
3131.outline-none {
3232 outline: none;
3333}
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 {
2525 super(props);
2626
2727 const receivingAddress = need.addressFromWallet(props.wallet);
28− const incomingPoint = need.pointCursor(props.pointCursor);
28+ const incomingPoint = need.point(props.pointCursor);
2929
3030 this.state = {
3131 receivingAddress: receivingAddress,
@@ -46,7 +46,7 @@ class AcceptTransfer extends React.Component {
4646 const { state, props } = this;
4747
4848 const validContracts = need.contracts(props.contracts);
49− const validPoint = need.pointCursor(props.pointCursor);
49+ const validPoint = need.point(props.pointCursor);
5050 //TODO state.incomingPoint ?
5151
5252 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 {
1717 constructor(props) {
1818 super(props);
1919
20− const pointInTransfer = need.pointCursor(props.pointCursor);
20+ const pointInTransfer = need.point(props.pointCursor);
2121
2222 this.state = {
2323 proxyAddress: '',
@@ -31,7 +31,7 @@ class CancelTransfer extends React.Component {
3131 const { props } = this;
3232
3333 const validContracts = need.contracts(props.contracts);
34− const validPoint = need.pointCursor(props.pointCursor);
34+ const validPoint = need.point(props.pointCursor);
3535 //TODO this.state.pointInTransfer ?
3636
3737 const txn = azimuth.ecliptic.setTransferProxy(
src/views/CreateGalaxy.js+1−1
@@ -75,7 +75,7 @@ class CreateGalaxy extends React.Component {
7575
7676 const validContracts = need.contracts(props.contracts);
7777
78− const galaxyDec = parseInt(ob.patp2dec(state.galaxyName), 10);
78+ const galaxyDec = ob.patp2dec(state.galaxyName);
7979
8080 const txn = azimuth.ecliptic.createGalaxy(
8181 validContracts,
src/views/GenKeyfile.js+1−1
@@ -27,7 +27,7 @@ class GenKeyfile extends React.Component {
2727 }
2828
2929 getPointDetails() {
30− const point = need.pointCursor(this.props.pointCursor);
30+ const point = need.point(this.props.pointCursor);
3131 const pointDetails = need.fromPointCache(this.props.pointCache, point);
3232
3333 // 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';
1919import InviteUrl from './Invite/InviteUrl';
2020import InviteEmail from './Invite/InviteEmail';
2121
22−const kInviteNames = {
22+const NAMES = {
2323 URL: 'URL',
2424 EMAIL: 'EMAIL',
2525};
2626
27−const kInviteViews = {
27+const VIEWS = {
2828 URL: InviteUrl,
2929 EMAIL: InviteEmail,
3030};
@@ -32,12 +32,12 @@ const kInviteViews = {
3232export default function Invite() {
3333 const history = useHistory();
3434 const { Route, ...router } = useRouter({
35− names: kInviteNames,
36− views: kInviteViews,
37− initialRoutes: [{ key: kInviteNames.URL }],
35+ names: NAMES,
36+ views: VIEWS,
37+ initialRoutes: [{ key: NAMES.URL }],
3838 });
3939 const { pointCursor } = usePointCursor();
40− const point = need.pointCursor(pointCursor);
40+ const point = need.point(pointCursor);
4141 const name = useCurrentPointName();
4242
4343 // sync the current point
@@ -71,7 +71,7 @@ export default function Invite() {
7171 <Grid.Item as={H5} className="mv4" full>
7272 Invite
7373 </Grid.Item>
74− <Grid.Item as={Text} full>
74+ <Grid.Item as={Text} className="f5" full>
7575 You currently have{' '}
7676 <Highlighted>{availableInvitesText} invitations</Highlighted> left.
7777 <br />
src/views/Invite/InviteEmail.js+5−5
@@ -100,7 +100,7 @@ export default function InviteEmail() {
100100 const { wallet, walletType, walletHdPath } = useWallet();
101101 const { syncInvites, getInvites } = usePointCache();
102102 const { pointCursor } = usePointCursor();
103− const point = need.pointCursor(pointCursor);
103+ const point = need.point(pointCursor);
104104 const { getHasRecieved, syncHasReceivedForEmail, sendMail } = useMailer();
105105
106106 const { availableInvites } = getInvites(point);
@@ -114,7 +114,7 @@ export default function InviteEmail() {
114114 inputConfigs,
115115 { append: appendInput, removeAt: removeInputAt },
116116 ] = useArray(
117− [buildInputConfig({ label: 'Email Address' })],
117+ [buildInputConfig({ label: 'Email Address', autoFocus: true })],
118118 buildInputConfig
119119 );
120120
@@ -391,7 +391,7 @@ export default function InviteEmail() {
391391 }, [isDone, syncInvites, point]);
392392
393393 return (
394− <Grid gap={12}>
394+ <Grid gap={3}>
395395 <Grid.Item as={Grid} full>
396396 <Grid.Item as={Flex} cols={[1, 11]} align="center">
397397 <MiniBackButton onClick={() => pop()} />
@@ -411,7 +411,7 @@ export default function InviteEmail() {
411411
412412 {isDone && (
413413 <>
414− <Grid.Item as={Text} full>
414+ <Grid.Item as={Text} className="f5" full>
415415 <Highlighted>{pluralize(inputs.length, 'invite')}</Highlighted>{' '}
416416 {pluralize(inputs.length, 'has', 'have')} been successfully sent
417417 </Grid.Item>
@@ -432,7 +432,7 @@ export default function InviteEmail() {
432432 <Grid.Item
433433 key={input.name}
434434 as={Grid}
435− gap={12}
435+ gap={3}
436436 onMouseOver={() => setHovered({ [input.name]: true })}
437437 onMouseLeave={() => setHovered({ [input.name]: false })}
438438 full>
src/views/Invite/InviteUrl.js+1−1
@@ -13,7 +13,7 @@ import FooterButton from 'components/FooterButton';
1313export default function InviteUrl() {
1414 const { names, push } = useLocalRouter();
1515 const { pointCursor } = usePointCursor();
16− const point = need.pointCursor(pointCursor);
16+ const point = need.point(pointCursor);
1717 const { availableInvites } = useInvites(point);
1818 const canInvite = availableInvites.matchWith({
1919 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 {
3939 }
4040
4141 componentDidMount() {
42− this.point = need.pointCursor(this.props.pointCursor);
42+ this.point = need.point(this.props.pointCursor);
4343 this.contracts = need.contracts(this.props.contracts);
4444
4545 azimuth.azimuth
src/views/IssueChild.js+1−1
@@ -40,7 +40,7 @@ class IssueChild extends React.Component {
4040 constructor(props) {
4141 super(props);
4242
43− const issuingPoint = need.pointCursor(props.pointCursor);
43+ const issuingPoint = need.point(props.pointCursor);
4444
4545 const getCandidate = () => ob.patp(getSpawnCandidate(issuingPoint));
4646
src/views/Landing.js+5−5
@@ -7,7 +7,7 @@ import { useHistory } from '../store/history';
77import View from 'components/View';
88import { ForwardButton } from 'components/Buttons';
99
10−function Landing(props) {
10+function Landing() {
1111 const history = useHistory();
1212
1313 return (
@@ -22,13 +22,13 @@ function Landing(props) {
2222 <ForwardButton
2323 className="mt3"
2424 detail="If you were sent an Azimuth invite code."
25− onClick={() => history.push(ROUTE_NAMES.INVITE_TICKET)}>
26− Claim an invite
25+ onClick={() => history.push(ROUTE_NAMES.ACTIVATE)}>
26+ Activate a Point
2727 </ForwardButton>
2828
2929 <ForwardButton
3030 className="mt3"
31− detail="Shiny new Login flow."
31+ detail="Login to an activated point."
3232 onClick={() => history.push(ROUTE_NAMES.LOGIN)}>
3333 Login
3434 </ForwardButton>
@@ -37,7 +37,7 @@ function Landing(props) {
3737 className="mt3"
3838 detail="View an Azimuth point without signing into a wallet."
3939 onClick={() => history.push(ROUTE_NAMES.VIEW_POINT)}>
40− View a point
40+ View a Point
4141 </ForwardButton>
4242 </View>
4343 );
src/views/Login/Keystore.js+2
@@ -14,8 +14,10 @@ import { useWallet } from 'store/wallet';
1414import * as need from 'lib/need';
1515import { EthereumWallet, WALLET_TYPES } from 'lib/wallet';
1616import useWalletType from 'lib/useWalletType';
17+import useResetPointCursor from 'lib/useResetPointCursor';
1718
1819export default function Keystore({ loginCompleted }) {
20+ useResetPointCursor();
1921 useWalletType(WALLET_TYPES.KEYSTORE);
2022
2123 // globals
src/views/Login/Ledger.js+2
@@ -16,6 +16,7 @@ import { useWallet } from 'store/wallet';
1616import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from 'lib/ledger';
1717import useWalletType from 'lib/useWalletType';
1818import { WALLET_TYPES } from 'lib/wallet';
19+import useResetPointCursor from 'lib/useResetPointCursor';
1920
2021const pathOptions = [
2122 { title: 'Ledger Live', value: LEDGER_LIVE_PATH },
@@ -28,6 +29,7 @@ const chopHdPrefix = str => (str.slice(0, 2) === 'm/' ? str.slice(2) : str);
2829const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
2930
3031export default function Ledger({ loginCompleted }) {
32+ useResetPointCursor();
3133 useWalletType(WALLET_TYPES.LEDGER);
3234
3335 const { wallet, setWallet, setWalletHdPath } = useWallet();
src/views/Login/Mnemonic.js+3
@@ -14,9 +14,12 @@ import { useWallet } from 'store/wallet';
1414
1515import { walletFromMnemonic, WALLET_TYPES } from 'lib/wallet';
1616import useWalletType from 'lib/useWalletType';
17+import useResetPointCursor from 'lib/useResetPointCursor';
1718
1819export default function Mnemonic({ advanced, loginCompleted }) {
20+ useResetPointCursor();
1921 useWalletType(WALLET_TYPES.MNEMONIC);
22+
2023 const {
2124 wallet,
2225 setWallet,
src/views/Login/PrivateKey.js+2
@@ -10,8 +10,10 @@ import { useWallet } from 'store/wallet';
1010
1111import { EthereumWallet, WALLET_TYPES } from 'lib/wallet';
1212import useWalletType from 'lib/useWalletType';
13+import useResetPointCursor from 'lib/useResetPointCursor';
1314
1415export default function PrivateKey({ loginCompleted }) {
16+ useResetPointCursor();
1517 useWalletType(WALLET_TYPES.PRIVATE_KEY);
1618 const { wallet, setWallet } = useWallet();
1719
src/views/Login/Shards.js+2−1
@@ -15,15 +15,16 @@ import { useWallet } from 'store/wallet';
1515
1616import { urbitWalletFromTicket, WALLET_TYPES } from 'lib/wallet';
1717import useWalletType from 'lib/useWalletType';
18+import useResetPointCursor from 'lib/useResetPointCursor';
1819
1920export default function Shards({ loginCompleted }) {
21+ useResetPointCursor();
2022 useWalletType(WALLET_TYPES.SHARDS);
2123
2224 const { wallet, setUrbitWallet } = useWallet();
2325
2426 const pointInput = usePointInput({
2527 name: 'point',
26− label: 'Point',
2728 autoFocus: true,
2829 });
2930 const pointName = pointInput.data;
src/views/Login/Ticket.js+3−3
@@ -19,6 +19,7 @@ import { usePointCursor } from 'store/pointCursor';
1919import * as need from 'lib/need';
2020import { WALLET_TYPES, urbitWalletFromTicket } from 'lib/wallet';
2121import useWalletType from 'lib/useWalletType';
22+import useResetPointCursor from 'lib/useResetPointCursor';
2223
2324//TODO should be part of InputWithStatus component
2425const INPUT_STATUS = {
@@ -28,6 +29,7 @@ const INPUT_STATUS = {
2829};
2930
3031export default function Ticket({ advanced, loginCompleted }) {
32+ useResetPointCursor();
3133 useWalletType(WALLET_TYPES.TICKET);
3234 // globals
3335 const { contracts } = useNetwork();
@@ -37,16 +39,14 @@ export default function Ticket({ advanced, loginCompleted }) {
3739 // inputs
3840 const pointInput = usePointInput({
3941 name: 'point',
40− label: 'Point',
4142 //TODO deduce point name from URL if we can, prefill input if we found it
42− // initialValue: '~',
4343 autoFocus: true,
4444 });
4545 const pointName = pointInput.data;
4646
4747 const ticketInput = useTicketInput({
4848 name: 'ticket',
49− label: 'Master ticket',
49+ label: 'Master Ticket',
5050 });
5151 const ticket = ticketInput.data;
5252
src/views/Login/Trezor.js+2
@@ -13,6 +13,7 @@ import { InnerLabelDropdown } from 'components/old/Base';
1313import { TREZOR_PATH } from 'lib/trezor';
1414import useWalletType from 'lib/useWalletType';
1515import { WALLET_TYPES } from 'lib/wallet';
16+import useResetPointCursor from 'lib/useResetPointCursor';
1617
1718import { useWallet } from 'store/wallet';
1819
@@ -22,6 +23,7 @@ const accountOptions = [
2223];
2324
2425export default function Trezor({ loginCompleted }) {
26+ useResetPointCursor();
2527 useWalletType(WALLET_TYPES.TREZOR);
2628
2729 const { wallet, setWallet, setWalletHdPath } = useWallet();
src/views/Point.js+2−2
@@ -24,7 +24,7 @@ export default function Point() {
2424 const { pointCursor } = usePointCursor();
2525 const { wallet } = useWallet();
2626
27− const point = need.pointCursor(pointCursor);
27+ const point = need.point(pointCursor);
2828
2929 // fetch the invites for the current cursor
3030 const { availableInvites } = useInvites(point);
@@ -41,7 +41,7 @@ export default function Point() {
4141
4242 return (
4343 <View>
44− <Passport point={point} />
44+ <Passport point={Maybe.Just(point)} />
4545 <Grid className="pt2">
4646 <Grid.Item full>
4747 <ForwardButton disabled>Admin</ForwardButton>
src/views/Point/Actions.js+1−1
@@ -19,7 +19,7 @@ function Actions() {
1919 const history = useHistory();
2020 const { wallet } = useWallet();
2121 const { pointCursor } = usePointCursor();
22− const point = need.pointCursor(pointCursor);
22+ const point = need.point(pointCursor);
2323 const { getDetails } = usePointCache();
2424 const pointDetails = getDetails(point);
2525
src/views/SetKeys.js+2−2
@@ -22,7 +22,7 @@ class SetKeys extends React.Component {
2222 constructor(props) {
2323 super(props);
2424
25− const point = need.pointCursor(props.pointCursor);
25+ const point = need.point(props.pointCursor);
2626
2727 this.state = {
2828 auth: '',
@@ -115,7 +115,7 @@ class SetKeys extends React.Component {
115115 const { state, props } = this;
116116
117117 const validContracts = need.contracts(props.contracts);
118− const validPoint = need.pointCursor(props.pointCursor);
118+ const validPoint = need.point(props.pointCursor);
119119
120120 // TODO: move this to a lib for validating things
121121 const hexRegExp = /[0-9A-Fa-f]{64}/g;
src/views/SetProxy.js+2−2
@@ -29,7 +29,7 @@ class _SetProxy extends React.Component {
2929 constructor(props) {
3030 super(props);
3131
32− const issuingPoint = need.pointCursor(props.pointCursor);
32+ const issuingPoint = need.point(props.pointCursor);
3333
3434 this.state = {
3535 proxyAddress: '',
@@ -61,7 +61,7 @@ class _SetProxy extends React.Component {
6161 const { state, props } = this;
6262
6363 const validContracts = need.contracts(props.contracts);
64− const validPoint = need.pointCursor(props.pointCursor);
64+ const validPoint = need.point(props.pointCursor);
6565
6666 const txArgs = [validContracts, validPoint, state.proxyAddress];
6767
src/views/Transfer.js+2−2
@@ -20,7 +20,7 @@ class Transfer extends React.Component {
2020 constructor(props) {
2121 super(props);
2222
23− const issuingPoint = need.pointCursor(props.pointCursor);
23+ const issuingPoint = need.point(props.pointCursor);
2424
2525 this.state = {
2626 receivingAddress: '',
@@ -47,7 +47,7 @@ class Transfer extends React.Component {
4747 const { state, props } = this;
4848
4949 const validContracts = need.contracts(props.contracts);
50− const validPoint = need.pointCursor(props.pointCursor);
50+ const validPoint = need.point(props.pointCursor);
5151
5252 const txn = azimuth.ecliptic.setTransferProxy(
5353 validContracts,
src/views/ViewPoint.js+2−4
@@ -1,6 +1,5 @@
11import React, { useCallback } from 'react';
22import Maybe from 'folktale/maybe';
3−import * as ob from 'urbit-ob';
43import { Input } from 'indigo-react';
54
65import { H1, P } from '../components/old/Base';
@@ -9,6 +8,7 @@ import { useHistory } from 'store/history';
98import { usePointCursor } from 'store/pointCursor';
109
1110import { ROUTE_NAMES } from 'lib/routeNames';
11+import patp2dec from 'lib/patp2dec';
1212
1313import View from 'components/View';
1414import { ForwardButton } from 'components/Buttons';
@@ -19,15 +19,13 @@ export default function ViewPoint() {
1919 const { setPointCursor } = usePointCursor();
2020 const pointInput = usePointInput({
2121 name: 'point',
22− label: 'Point Name',
2322 autoFocus: true,
2423 });
2524 const { data: pointName, pass } = pointInput;
2625
2726 const disabled = !pass;
2827 const goForward = useCallback(() => {
29− setPointCursor(Maybe.Just(parseInt(ob.patp2dec(pointName), 10)));
30− // ^ pointCursor expects native number type, not string
28+ setPointCursor(Maybe.Just(patp2dec(pointName)));
3129 history.popAndPush(ROUTE_NAMES.POINT);
3230 }, [setPointCursor, history, pointName]);
3331