on master help sign in
Merge pull request #209 from urbit/feat/design-pass

feat: design pass updates

commit261ea40b5727c7fb5c0e1a63aba78ee2f509cf47 authorFang <Fang-@users.noreply.github.com> dateThu 25 Jul 2019, 16:16 (7 years ago) parent569e1bfb76a2ec
18 files changed+167−59
src/components/Accordion.js+47 src/components/Tabs.js+17−6 src/indigo-react/components/CheckboxInput.js+1 src/indigo-react/components/Input.js+9−3 src/indigo-react/components/SelectInput.js+1 src/indigo-react/components/ToggleInput.js+1 src/indigo-react/lib/useForm.js+2 src/lib/useInputs.js+15−11 src/lib/useLocalStorageState.js+15−10 src/views/Activate/ActivateCode.js+1−1 src/views/Admin/AdminNetworkingKeys.js+4−3 src/views/Disclaimer.js+2−2 src/views/Login/Advanced.js+4−3 src/views/Login/Hardware.js+4−3 src/views/Login/Mnemonic.js+2−1 src/views/Login/Ticket.js+8−2 src/views/Point.js+22−11 src/views/Points.js+12−3
src/components/Accordion.jsnew file+47
@@ -0,0 +1,47 @@
1+import React from 'react';
2+import { Grid, AccessoryIcon } from 'indigo-react';
3+
4+export default function Accordion({
5+ views,
6+ options,
7+ currentTab,
8+ onTabChange,
9+ className,
10+ ...rest
11+}) {
12+ const Tab = views[currentTab];
13+
14+ return (
15+ <Grid className={className}>
16+ {options.map((option, i) => {
17+ const isActive = option.value === currentTab;
18+
19+ return (
20+ <React.Fragment key={option.value}>
21+ <Grid.Item
22+ full
23+ className="f5 pv3 rel clickable"
24+ onClick={() => onTabChange(isActive ? undefined : option.value)}>
25+ {option.text}
26+ <div
27+ className="abs"
28+ style={{
29+ top: 0,
30+ right: 0,
31+ height: '100%',
32+ width: '44px',
33+ overflow: 'hidden',
34+ }}>
35+ <AccessoryIcon className="black">
36+ {isActive ? '▲' : '▼'}
37+ </AccessoryIcon>
38+ </div>
39+ </Grid.Item>
40+ {isActive && <Grid.Item full as={Tab} />}
41+ <Grid.Divider />
42+ </React.Fragment>
43+ );
44+ })}
45+ </Grid>
46+ );
47+}
src/components/Tabs.js+17−6
@@ -15,23 +15,34 @@ export default function Tabs({
1515 return (
1616 <Grid className={className}>
1717 <Grid.Item full as={Flex} wrap className="b-gray3 bb1">
18− {options.map(option => {
18+ {options.map((option, i) => {
1919 const isActive = option.value === currentTab;
20+ const isFirst = i === 0;
21+
2022 return (
2123 <Flex.Item
2224 key={option.value}
2325 onClick={() => onTabChange(option.value)}
24− className={cn('f5 pv3 mr6 clickable', {
25− 'black b-black bb1': isActive,
26− gray3: !isActive,
27− })}
26+ className={cn(
27+ 'f5 pv3 clickable',
28+ {
29+ 'black b-black bb1': isActive,
30+ gray3: !isActive,
31+ },
32+ {
33+ // all items have right margin/padding
34+ 'mr2 pr2': true,
35+ // the first one is flush to the left
36+ 'ml2 pl2': !isFirst,
37+ }
38+ )}
2839 style={{ marginBottom: '-1px' }}>
2940 {option.text}
3041 </Flex.Item>
3142 );
3243 })}
3344 </Grid.Item>
34− <Grid.Item full as={Tab} className="mt6" {...rest} />
45+ <Grid.Item full as={Tab} {...rest} />
3546 </Grid>
3647 );
3748}
src/indigo-react/components/CheckboxInput.js+1
@@ -23,6 +23,7 @@ export default function CheckboxInput({
2323 bind,
2424 autoFocus,
2525 disabled,
26+ touched,
2627
2728 // ignored
2829 initialValue,
src/indigo-react/components/Input.js+9−3
@@ -6,6 +6,7 @@ import { ErrorText } from './Typography';
66
77export default React.memo(function Input({
88 // visuals
9+ type,
910 name,
1011 label,
1112 className,
@@ -26,6 +27,7 @@ export default React.memo(function Input({
2627 bind,
2728 autoFocus,
2829 disabled,
30+ touched,
2931
3032 // ignored
3133 initialValue,
@@ -33,6 +35,7 @@ export default React.memo(function Input({
3335 transformers,
3436
3537 // extra
38+ textarea = false,
3639 ...rest
3740}) {
3841 // NB(shrugs): we disable exhaustive deps because we don't want the callbacks
@@ -67,8 +70,11 @@ export default React.memo(function Input({
6770 </Flex.Item>
6871 <Flex.Item as={Flex} className="rel" row>
6972 <Flex.Item
70− as="input"
73+ as={type === 'textarea' ? 'textarea' : 'input'}
74+ type={type === 'textarea' ? undefined : type}
7175 {...rest}
76+ // NOTE: 24px = 12px * 2 (from p3 styling)
77+ style={type === 'textarea' ? { minHeight: 'calc(1rem + 24px)' } : {}}
7278 className={cn(
7379 'b b1 p3 outline-none',
7480 { mono },
@@ -77,8 +83,8 @@ export default React.memo(function Input({
7783 'bg-gray1': disabled,
7884 },
7985 {
80− gray4: !focused,
81− black: focused,
86+ gray4: !focused && !touched,
87+ black: focused || touched,
8288 },
8389 {
8490 'b-green3': visiblyPassed,
src/indigo-react/components/SelectInput.js+1
@@ -31,6 +31,7 @@ export default function SelectInput({
3131 autoFocus,
3232 disabled,
3333 options,
34+ touched,
3435
3536 // ignored
3637 initialValue,
src/indigo-react/components/ToggleInput.js+1
@@ -25,6 +25,7 @@ export default function ToggleInput({
2525 bind,
2626 autoFocus,
2727 disabled,
28+ touched,
2829
2930 // ignored
3031 initialValue,
src/indigo-react/lib/useForm.js+2
@@ -217,6 +217,7 @@ export default function useForm(inputConfigs = []) {
217217 error: errors[i],
218218 hintError: hintErrors[i],
219219 focused: focuses[i],
220+ touched: hasBeenTouched[name],
220221 autoFocus: autoFocus && !disabled,
221222 disabled,
222223 ...rest,
@@ -241,6 +242,7 @@ export default function useForm(inputConfigs = []) {
241242 errors,
242243 hintErrors,
243244 focuses,
245+ hasBeenTouched,
244246 onChange,
245247 onFocus,
246248 onBlur,
src/lib/useInputs.js+15−11
@@ -1,7 +1,6 @@
11import React, { useCallback, useMemo } from 'react';
22import { AccessoryIcon, useForm } from 'indigo-react';
33import { identity } from 'lodash';
4−import * as bip39 from 'bip39';
54
65import { DEFAULT_HD_PATH } from 'lib/wallet';
76import {
@@ -22,8 +21,14 @@ import { prependSig, convertToNumber } from 'lib/transformers';
2221
2322import InputSigil from 'components/InputSigil';
2423
25−const EXAMPLE_PRIVATE_KEY =
26− '0xa44de2416ee6beb2f323fab48b432925c9785808d33a6ca6d7ba00b45e9370c3';
24+const PLACEHOLDER_POINT = '~sampel-ponnym';
25+const PLACEHOLDER_HD_PATH = DEFAULT_HD_PATH;
26+const PLACEHOLDER_MNEMONIC =
27+ 'example crew supreme gesture quantum web media hazard theory mercy wing kitten';
28+const PLACEHOLDER_TICKET = '~sampel-ticlyt-migfun-falmel';
29+const PLACEHOLDER_ADDRESS = '0x12345abcdeDB11D175F123F6891AA64F01c24F7d';
30+const PLACEHOLDER_PRIVATE_KEY =
31+ '0x12345abcdee6beb2f323fab48b432925c9785808d33a6ca6d7ba00b45e9370c3';
2732
2833// pulls out the first input from a useForm() call
2934function useFirstOf({ inputs, setValue, ...rest }, mapper = identity) {
@@ -71,7 +76,7 @@ export function useHexInput({ length, ...rest }) {
7176 {
7277 type: 'text', // or password
7378 autoComplete: 'off',
74− placeholder: `e.g. ${EXAMPLE_PRIVATE_KEY}`,
79+ placeholder: PLACEHOLDER_PRIVATE_KEY,
7580 validators: useMemo(
7681 () => [
7782 validateHexString,
@@ -86,15 +91,14 @@ export function useHexInput({ length, ...rest }) {
8691 );
8792}
8893
89−const kExampleMnemonic = bip39.generateMnemonic();
9094const kMnemonicValidators = [validateMnemonic, validateNotEmpty];
9195export function useMnemonicInput(props) {
9296 return useFirstOf(
9397 useForm([
9498 {
95− type: 'text',
99+ type: 'textarea',
96100 autoComplete: 'off',
97− placeholder: `e.g. ${kExampleMnemonic}`,
101+ placeholder: PLACEHOLDER_MNEMONIC,
98102 validators: kMnemonicValidators,
99103 ...props,
100104 },
@@ -108,7 +112,7 @@ export function useHdPathInput(props) {
108112 {
109113 type: 'text',
110114 autoComplete: 'off',
111− placeholder: `e.g. ${DEFAULT_HD_PATH}`,
115+ placeholder: PLACEHOLDER_HD_PATH,
112116 ...props,
113117 },
114118 ])
@@ -124,7 +128,7 @@ export function useTicketInput({ validators = [], deriving = false, ...rest }) {
124128 {
125129 type: 'password',
126130 label: 'Ticket',
127− placeholder: 'e.g. ~ragtyd-modwen-bostec-hinsep',
131+ placeholder: PLACEHOLDER_TICKET,
128132 validators: useMemo(() => [...validators, ...kTicketValidators], [
129133 validators,
130134 ]),
@@ -162,7 +166,7 @@ export function usePointInput({ size = 4, validators = [], ...rest }) {
162166 {
163167 type: 'text',
164168 label: 'Point',
165− placeholder: 'e.g. ~zod',
169+ placeholder: PLACEHOLDER_POINT,
166170 validators: _validators,
167171 transformers: kPointTransformers,
168172 mono: true,
@@ -236,7 +240,7 @@ export function useAddressInput({ ...rest }) {
236240 {
237241 type: 'string',
238242 label: 'Ethereum Address',
239− placeholder: 'e.g. 0x6DEfFb0caFDB11D175F123F6891AA64F01c24F7d',
243+ placeholder: PLACEHOLDER_ADDRESS,
240244 autoComplete: 'off',
241245 validators: kAddressValidators,
242246 mono: true,
src/lib/useLocalStorageState.js+15−10
@@ -1,4 +1,4 @@
1−import { useState, useEffect } from 'react';
1+import { useCallback, useState } from 'react';
22
33/**
44 * useLocalStorageState is useState but it persists to localStorage
@@ -8,7 +8,7 @@ import { useState, useEffect } from 'react';
88 * https://usehooks.com/useLocalStorage/
99 */
1010export default function useLocalStorageState(key, initialValue) {
11− const [state, setState] = useState(() => {
11+ const [state, _setState] = useState(() => {
1212 try {
1313 const value = localStorage.getItem(key);
1414 if (value === null) {
@@ -25,14 +25,19 @@ export default function useLocalStorageState(key, initialValue) {
2525 }
2626 });
2727
28− useEffect(() => {
29− try {
30− localStorage.setItem(key, JSON.stringify(state));
31− } catch {
32− // if user is in private mode or has storage restriction
33− // localStorage can throw. Also JSON.stringify can throw.
34− }
35− }, [key, state]);
28+ const setState = useCallback(
29+ value => {
30+ try {
31+ localStorage.setItem(key, JSON.stringify(value));
32+ } catch {
33+ // if user is in private mode or has storage restriction
34+ // localStorage can throw. Also JSON.stringify can throw.
35+ }
36+
37+ _setState(value);
38+ },
39+ [key]
40+ );
3641
3742 return [state, setState];
3843}
src/views/Activate/ActivateCode.js+1−1
@@ -40,7 +40,7 @@ export default function ActivateCode() {
4040
4141 const [ticketInput, { pass: validTicket, data: ticket }] = useTicketInput({
4242 name: 'ticket',
43− label: 'Code',
43+ label: 'Activation Code',
4444 initialValue: impliedTicket || '',
4545 autoFocus: true,
4646 });
src/views/Admin/AdminNetworkingKeys.js+4−3
@@ -30,6 +30,8 @@ import { formatDotsWithTime } from 'lib/dateFormat';
3030import useEthereumTransaction from 'lib/useEthereumTransaction';
3131import { GAS_LIMITS } from 'lib/constants';
3232import { addHexPrefix } from 'lib/wallet';
33+import { useHexInput, useCheckboxInput } from 'lib/useInputs';
34+import useKeyfileGenerator from 'lib/useKeyfileGenerator';
3335
3436import ViewHeader from 'components/ViewHeader';
3537import MiniBackButton from 'components/MiniBackButton';
@@ -38,8 +40,6 @@ import FooterButton from 'components/FooterButton';
3840import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
3941import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
4042import NoticeBox from 'components/NoticeBox';
41−import { useHexInput, useCheckboxInput } from 'lib/useInputs';
42−import useKeyfileGenerator from 'lib/useKeyfileGenerator';
4343
4444const chainKeyProp = name => d =>
4545 d[name] === CURVE_ZERO_ADDR ? Nothing() : Just(d[name]);
@@ -330,7 +330,8 @@ export default function AdminNetworkingKeys() {
330330 {completed && (
331331 <>
332332 <Grid.Item full as={NoticeBox} className="mb3">
333− You need this keyfile to authenticate with Arvo.
333+ You need this keyfile to authenticate with Arvo. If you have
334+ already booted Arvo, run <code>|rekey '0wkeyfile.contents'</code>
334335 </Grid.Item>
335336 <Grid.Item full as={DownloadKeyfileButton} solid {...keyfileBind} />
336337 </>
src/views/Disclaimer.js+2−2
@@ -20,7 +20,7 @@ export default function ActivateDisclaimer() {
2020 label: 'I acknowledge and understand these rights',
2121 });
2222
23− const goToPassport = useCallback(() => {
23+ const goBack = useCallback(async () => {
2424 setHasDisclaimed(true);
2525 pop();
2626 }, [pop, setHasDisclaimed]);
@@ -80,7 +80,7 @@ export default function ActivateDisclaimer() {
8080 <Grid.Item
8181 as={ForwardButton}
8282 disabled={!isUnderstood}
83− onClick={goToPassport}
83+ onClick={goBack}
8484 solid
8585 full>
8686 Continue
src/views/Login/Advanced.js+4−3
@@ -1,6 +1,7 @@
11import React, { useState } from 'react';
22
3−import Tabs from 'components/Tabs';
3+import Accordion from 'components/Accordion';
4+
45import PrivateKey from './PrivateKey';
56import Keystore from './Keystore';
67
@@ -20,10 +21,10 @@ const OPTIONS = [
2021];
2122
2223export default function Advanced({ loginCompleted, className }) {
23− const [currentTab, setCurrentTab] = useState(NAMES.PRIVATE_KEY);
24+ const [currentTab, setCurrentTab] = useState(undefined);
2425
2526 return (
26− <Tabs
27+ <Accordion
2728 className={className}
2829 views={VIEWS}
2930 options={OPTIONS}
src/views/Login/Hardware.js+4−3
@@ -1,6 +1,7 @@
11import React, { useState } from 'react';
22
3−import Tabs from 'components/Tabs';
3+import Accordion from 'components/Accordion';
4+
45import Ledger from './Ledger';
56import Trezor from './Trezor';
67
@@ -20,10 +21,10 @@ const OPTIONS = [
2021];
2122
2223export default function Hardware({ loginCompleted, className }) {
23− const [currentTab, setCurrentTab] = useState(NAMES.LEDGER);
24+ const [currentTab, setCurrentTab] = useState(undefined);
2425
2526 return (
26− <Tabs
27+ <Accordion
2728 className={className}
2829 views={VIEWS}
2930 options={OPTIONS}
src/views/Login/Mnemonic.js+2−1
@@ -1,4 +1,5 @@
11import React, { useEffect } from 'react';
2+import cn from 'classnames';
23import { Just, Nothing } from 'folktale/maybe';
34import { Grid, Input, CheckboxInput } from 'indigo-react';
45
@@ -67,7 +68,7 @@ export default function Mnemonic({ className }) {
6768 ]);
6869
6970 return (
70− <Grid className={className}>
71+ <Grid className={cn('mt4', className)}>
7172 <Grid.Item full as={Input} {...mnemonicInput} />
7273
7374 {useAdvanced && (
src/views/Login/Ticket.js+8−2
@@ -1,4 +1,5 @@
11import { Just, Nothing } from 'folktale/maybe';
2+import cn from 'classnames';
23import React, { useCallback, useState, useEffect } from 'react';
34import * as azimuth from 'azimuth-js';
45import * as ob from 'urbit-ob';
@@ -88,7 +89,12 @@ export default function Ticket({ className }) {
8889 setDeriving(true);
8990 setUrbitWallet(Nothing());
9091
91− if (!ob.isValidPatq(ticket) || !ob.isValidPatp(pointName)) {
92+ if (
93+ !ticket ||
94+ !pointName ||
95+ !ob.isValidPatq(ticket) ||
96+ !ob.isValidPatp(pointName)
97+ ) {
9298 setDeriving(false);
9399 return;
94100 }
@@ -160,7 +166,7 @@ export default function Ticket({ className }) {
160166 ]);
161167
162168 return (
163− <Grid className={className}>
169+ <Grid className={cn('mt4', className)}>
164170 <Grid.Item full as={Input} {...pointInput} />
165171
166172 {!isUsingShards && <Grid.Item full as={Input} {...ticketInput} />}
src/views/Point.js+22−11
@@ -10,7 +10,6 @@ import { useHistory } from 'store/history';
1010import View from 'components/View';
1111import Passport from 'components/Passport';
1212import { ForwardButton, BootArvoButton } from 'components/Buttons';
13−import FooterButton from 'components/FooterButton';
1413import { matchBlinky } from 'components/Blinky';
1514
1615import useInvites from 'lib/useInvites';
@@ -52,18 +51,30 @@ export default function Point() {
5251 case azimuth.PointSize.Planet:
5352 const availableInvitesText = matchBlinky(availableInvites);
5453 return (
55− <FooterButton disabled={!isActiveOwner} onClick={goInvite}>
56− Invite <sup>{availableInvitesText} available</sup>
57− </FooterButton>
54+ <>
55+ <Grid.Item
56+ full
57+ as={ForwardButton}
58+ disabled={!isActiveOwner}
59+ onClick={goInvite}>
60+ Invite <sup>{availableInvitesText}</sup>
61+ </Grid.Item>
62+ <Grid.Divider />
63+ </>
5864 );
5965 //
6066 case azimuth.PointSize.Star:
6167 return (
62− <FooterButton
63− disabled={!isActiveOwner}
64− onClick={goPartiesSetPoolSize}>
65− Manage Parties
66− </FooterButton>
68+ <>
69+ <Grid.Item
70+ full
71+ as={ForwardButton}
72+ disabled={!isActiveOwner}
73+ onClick={goPartiesSetPoolSize}>
74+ Manage Parties
75+ </Grid.Item>
76+ <Grid.Divider />
77+ </>
6778 );
6879 //
6980 default:
@@ -78,6 +89,7 @@ export default function Point() {
7889 <View>
7990 <Passport point={Just(point)} />
8091 <Grid className="pt2">
92+ {inviteButton}
8193 <Grid.Item
8294 full
8395 as={ForwardButton}
@@ -99,9 +111,8 @@ export default function Point() {
99111 </>
100112 )}
101113 <Grid.Item full as={BootArvoButton} disabled />
114+ <Grid.Divider />
102115 </Grid>
103−
104− {inviteButton}
105116 </View>
106117 );
107118}
src/views/Points.js+12−3
@@ -18,6 +18,7 @@ import FooterButton from 'components/FooterButton';
1818import Blinky from 'components/Blinky';
1919import Passport from 'components/Passport';
2020import useRejectedIncomingPointTransfers from 'lib/useRejectedIncomingPointTransfers';
21+import pluralize from 'lib/pluralize';
2122
2223const getFromMaybe = (obj, key, defaultValue) =>
2324 obj.matchWith({
@@ -139,7 +140,11 @@ export default function Points() {
139140 {incomingPoints.length > 0 && (
140141 <Grid.Item full as={Grid} gap={1} className="mb6">
141142 <Grid.Item full as={H5}>
142− Incoming Transfers
143+ {pluralize(
144+ incomingPoints.length,
145+ 'Incoming Transfer',
146+ 'Incoming Transfers'
147+ )}
143148 </Grid.Item>
144149 <Grid.Item
145150 full
@@ -172,7 +177,11 @@ export default function Points() {
172177 {outgoingPoints.length > 0 && (
173178 <Grid.Item full as={Grid} gap={1} className="mb6">
174179 <Grid.Item full as={H5}>
175− Outgoing Transfers
180+ {pluralize(
181+ outgoingPoints.length,
182+ 'Outgoing Transfer',
183+ 'Outgoing Transfers'
184+ )}
176185 </Grid.Item>
177186 <Grid.Item
178187 full
@@ -200,7 +209,7 @@ export default function Points() {
200209 {multipassPoints.length > 0 && (
201210 <Grid.Item full as={Grid} gap={1}>
202211 <Grid.Item full as={H5}>
203− Multipasses
212+ {pluralize(multipassPoints.length, 'Multipass', 'Multipasses')}
204213 </Grid.Item>
205214 <Grid.Item full as={PointList} points={multipassPoints} />
206215 </Grid.Item>