on master help sign in
ux: Confirm Master Key

- tweak ActivateView layout to support multiple views - add helpers to formatters , validators

commit5a30d537f6c294b65e6d2521274ec353fff45a89 authortomholford <tomholford@users.noreply.github.com> dateFri 10 Sep 2021, 07:58 (5 years ago) parentac86588
13 files changed+303−42
src/form/BridgeForm.js+3−1 src/form/Inputs.js+26−1 src/form/formatters.js → src/form/formatters.ts+8 src/form/validators.js+3 src/lib/constants.ts+1−1 src/lib/validators.js → src/lib/validators.ts+6 src/views/Activate/ActivateButton.tsx+15−20 src/views/Activate/ActivateMasterKey.tsx+3−3 src/views/Activate/ActivateView.tsx+20−9 src/views/Activate/MasterKeyConfirm.tsx+204 src/views/Activate/MasterKeyDownload.tsx+12−4 src/views/Activate/MasterKeyPresenter.tsx−1 src/views/Activate/withFadeable.tsx+2−2
src/form/BridgeForm.js+3−1
@@ -36,7 +36,9 @@ export default function BridgeForm({
3636 return (
3737 <Form mutators={{ ...arrayMutators }} onSubmit={_onSubmit} {...rest}>
3838 {formProps => (
39− <form style={{ display: 'contents' }} onSubmit={formProps.handleSubmit}>
39+ <form
40+ style={formProps?.style || { display: 'contents' }}
41+ onSubmit={formProps.handleSubmit}>
4042 <ValidationPauser />
4143 {children(formProps)}
4244 {onValues && <ValuesHandler {...formProps} onValues={onValues} />}
src/form/Inputs.js+26−1
@@ -12,12 +12,13 @@ import {
1212} from 'form/formatters';
1313import { DEFAULT_HD_PATH } from 'lib/constants';
1414import InputSigil from 'components/InputSigil';
15+import { StatelessTextInput } from '@tlon/indigo-react';
1516
1617const PLACEHOLDER_POINT = '~sampel-ponnym';
1718const PLACEHOLDER_HD_PATH = DEFAULT_HD_PATH;
1819const PLACEHOLDER_MNEMONIC =
1920 'example crew supreme gesture quantum web media hazard theory mercy wing kitten';
20−const PLACEHOLDER_TICKET = '~sampel-ticlyt-migfun-falmel';
21+export const PLACEHOLDER_TICKET = '~sampel-ticlyt-migfun-falmel';
2122const PLACEHOLDER_PASSWORD = '••••••-••••••-••••••-••••••';
2223const PLACEHOLDER_ADDRESS = '0x';
2324const PLACEHOLDER_HEX = '0x';
@@ -208,3 +209,27 @@ export function EmailInput({ ...rest }) {
208209 />
209210 );
210211}
212+
213+export function LegacyTicketSegmentInput({ ...rest }) {
214+ return (
215+ <Input autoCapitalize="none" autoCorrect="off" type="text" {...rest} />
216+ );
217+}
218+
219+export function TicketSegmentInput({ name, ...rest }) {
220+ const { input } = useField(name);
221+
222+ return (
223+ <StatelessTextInput
224+ autoCapitalize="none"
225+ autoCorrect="off"
226+ type="text"
227+ {...input}
228+ {...rest}
229+ />
230+ );
231+}
232+
233+export const HiddenInput = ({ ...rest }) => {
234+ return <StatelessTextInput type="hidden" {...rest} />;
235+};
src/form/formatters.js → src/form/formatters.tsrenamed+8
@@ -96,3 +96,11 @@ export const stripSigPrefix = s => {
9696 ? s.slice(SIG_PREFIX.length)
9797 : s;
9898};
99+
100+export const ticketToSegments = (ticket: string): string[] => {
101+ return stripSigPrefix(ticket).split('-');
102+};
103+
104+export const ticketFromSegments = (segments: string[]): string => {
105+ return ensureSigPrefix(segments.join('-'));
106+};
src/form/validators.js+3
@@ -20,6 +20,7 @@ import {
2020 validateLength,
2121 validateUnique,
2222 validatePsbt,
23+ validateSegmentedTicket,
2324} from 'lib/validators';
2425import { isPromise } from 'lib/isPromise';
2526
@@ -62,6 +63,8 @@ export const buildArrayValidator = validator => values => {
6263export const hasErrors = iter =>
6364 some(iter, v => (Array.isArray(v) ? hasErrors(v) : v !== undefined));
6465
66+export const buildSegmentedTicketValidator = (validators = []) =>
67+ buildValidator([validateNotEmpty, validateSegmentedTicket, ...validators]);
6568export const buildPatqValidator = (validators = []) =>
6669 buildValidator([validateNotEmpty, validatePatq, ...validators]);
6770export const buildShardValidator = (validators = []) =>
src/lib/constants.ts+1−1
@@ -122,7 +122,7 @@ const POINT_PROXIES = {
122122
123123// In ms
124124const DEFAULT_FADE_TIMEOUT = 300;
125−const MASTER_KEY_DURATION = 1800;
125+const MASTER_KEY_DURATION = 1900;
126126
127127export {
128128 CHECK_BLOCK_EVERY_MS,
src/lib/validators.js → src/lib/validators.tsrenamed+6
@@ -12,6 +12,7 @@ import {
1212 MIN_GALAXY,
1313 MAX_GALAXY,
1414} from './constants';
15+import { ticketFromSegments } from 'form/formatters';
1516
1617// NOTE: do not use the /g modifier for these regexes
1718// https://stackoverflow.com/a/21373261
@@ -146,3 +147,8 @@ export const validatePsbt = base64 => {
146147 return 'Invalid Partially Signed Bitcoin Transaction';
147148 }
148149};
150+
151+export const validateSegmentedTicket = (segments: string[]) => {
152+ const ticket = ticketFromSegments(segments);
153+ return validatePatq(ticket);
154+};
src/views/Activate/ActivateButton.tsx+15−20
@@ -5,33 +5,28 @@ import withFadeable from './withFadeable';
55interface ActivateButtonProps {
66 children: React.ReactNode | string;
77 onClick: MouseEventHandler;
8− success?: boolean;
8+ disabled?: boolean;
99}
1010
1111const ActivateButton = ({
1212 children,
1313 onClick,
14− success,
14+ disabled,
15+ ...rest
1516}: ActivateButtonProps & React.ComponentPropsWithoutRef<'button'>) => {
1617 return (
17− <Box
18− display="flex"
19− flexDirection="column"
20− flexWrap="nowrap"
21− height={'100%'}
22− justifyContent="flex-end">
23− <Button
24− onClick={onClick}
25− backgroundColor={success ? 'rgb(70,156,106)' : 'black'}
26− color={'white'}
27− padding={'16px'}
28− fontFamily="Inter"
29− height={'50px'}
30− fontWeight={'400'}
31− fontSize={'18px'}>
32− {children}
33− </Button>
34− </Box>
18+ <Button
19+ onClick={onClick}
20+ backgroundColor={disabled ? 'rgba(0,0,0,0.2)' : 'black'}
21+ color={'white'}
22+ padding={'16px'}
23+ fontFamily="Inter"
24+ height={'50px'}
25+ fontWeight={'400'}
26+ fontSize={'18px'}
27+ {...rest}>
28+ {children}
29+ </Button>
3530 );
3631};
3732
src/views/Activate/ActivateMasterKey.tsx+3−3
@@ -5,21 +5,21 @@ import View from 'components/View';
55import useRouter from 'lib/useRouter';
66
77import MasterKeyDownload from './MasterKeyDownload';
8−import PassportVerify from './PassportVerify';
98import PassportTransfer from './PassportTransfer';
109import MasterKeyReveal from './MasterKeyReveal';
10+import MasterKeyConfirm from './MasterKeyConfirm';
1111
1212const NAMES = {
1313 REVEAL: 'REVEAL',
1414 DOWNLOAD: 'DOWNLOAD',
15− VERIFY: 'VERIFY',
15+ CONFIRM: 'CONFIRM',
1616 TRANSFER: 'TRANSFER',
1717};
1818
1919const VIEWS = {
2020 [NAMES.REVEAL]: MasterKeyReveal,
2121 [NAMES.DOWNLOAD]: MasterKeyDownload,
22− [NAMES.VERIFY]: PassportVerify,
22+ [NAMES.CONFIRM]: MasterKeyConfirm,
2323 [NAMES.TRANSFER]: PassportTransfer,
2424};
2525
src/views/Activate/ActivateView.tsx+20−9
@@ -1,29 +1,30 @@
11import { Box } from '@tlon/indigo-react';
22import cn from 'classnames';
33
4−// import Steps from 'components/Steps';
5−type ActivateViewProps = {
4+interface ActivateViewProps {
65 className?: string;
76 children?: React.ReactNode;
87 header?: React.ReactNode | string;
98 footer?: React.ReactNode;
10− step?: number | null;
11−};
9+ gridRows?: string;
10+ gridAreas?: string;
11+}
1212
1313export default function ActivateView({
1414 className,
1515 children,
1616 header,
1717 footer,
18− step = null,
18+ gridRows,
19+ gridAreas,
1920}: ActivateViewProps) {
2021 return (
2122 <>
2223 <Box
2324 display="grid"
2425 gridTemplateColumns="1fr"
25− gridTemplateRows="20% 60% 20%"
26− gridTemplateAreas={"'header' 'content' 'footer'"}
26+ gridTemplateRows={gridRows || '20% 60% 20%'}
27+ gridTemplateAreas={gridAreas || "'header' 'content' 'footer'"}
2728 border={'1px solid rgb(204, 204, 204)'}
2829 border-radius={'5px'}
2930 padding={'10px'}
@@ -35,9 +36,19 @@ export default function ActivateView({
3536 mb={3}>
3637 <Box gridArea="header">{header}</Box>
3738 <Box gridArea="content">{children}</Box>
38− <Box gridArea="footer">{footer ? footer : null}</Box>
39+ <Box gridArea="footer">
40+ {footer ? (
41+ <Box
42+ display="flex"
43+ flexDirection="column"
44+ flexWrap="nowrap"
45+ height={'100%'}
46+ justifyContent="flex-end">
47+ {footer}
48+ </Box>
49+ ) : null}
50+ </Box>
3951 </Box>
40− <Box>{step ? step : null}</Box>
4152 </>
4253 );
4354}
src/views/Activate/MasterKeyConfirm.tsxnew file+204
@@ -0,0 +1,204 @@
1+import React, { useCallback, useMemo } from 'react';
2+import * as need from 'lib/need';
3+
4+import ActivateView from './ActivateView';
5+import { Box, Text } from '@tlon/indigo-react';
6+import { composeValidator, buildPatqValidator } from 'form/validators';
7+import { DEFAULT_FADE_TIMEOUT } from 'lib/constants';
8+import { FadeableActivateButton as ActivateButton } from './ActivateButton';
9+import { FadeableActivateHeader as ActivateHeader } from './ActivateHeader';
10+import { FadeableActivateParagraph as ActivateParagraph } from './ActivateParagraph';
11+import { FadeableActivateSteps as ActivateSteps } from './ActivateSteps';
12+import { isDevelopment } from 'lib/flags';
13+import { ticketFromSegments, ticketToSegments } from 'form/formatters';
14+import { timeout } from 'lib/timeout';
15+import { useActivateFlow } from './ActivateFlow';
16+import { useLocalRouter } from 'lib/LocalRouter';
17+import { validateExactly } from 'lib/validators';
18+import BridgeForm from 'form/BridgeForm';
19+import FormError from 'form/FormError';
20+import useFadeIn from './useFadeIn';
21+import {
22+ HiddenInput,
23+ TicketSegmentInput,
24+} from 'form/Inputs';
25+
26+const MasterKeyConfirm = () => {
27+ const { derivedWallet, setIsIn } = useActivateFlow();
28+ const { ticket } = need.wallet(derivedWallet);
29+ const { push, names } = useLocalRouter();
30+ const STUB_VERIFY_TICKET = isDevelopment;
31+ const ticketSegments = useMemo(() => ticketToSegments(ticket), [ticket]);
32+
33+ const onValues = useCallback(({ valid, values, form }) => {
34+ console.log('valid:', valid);
35+ console.log('values:', values);
36+
37+ const { ticket0, ticket1, ticket2, ticket3 } = values;
38+
39+ const updatedTicket = ticketFromSegments([
40+ ticket0,
41+ ticket1,
42+ ticket2,
43+ ticket3,
44+ ]);
45+
46+ console.log(updatedTicket);
47+
48+ form.change('ticket', updatedTicket);
49+ }, []);
50+
51+ const validate = useMemo(
52+ () =>
53+ composeValidator({
54+ ticket: buildPatqValidator([
55+ validateExactly(ticket, 'Does not match expected master ticket.'),
56+ ]),
57+ }),
58+ [ticket]
59+ );
60+
61+ const initialValues = useMemo(
62+ () => ({
63+ ticket: STUB_VERIFY_TICKET ? ticket : undefined,
64+ ticket0: STUB_VERIFY_TICKET ? ticketSegments[0] : undefined,
65+ ticket1: STUB_VERIFY_TICKET ? ticketSegments[1] : undefined,
66+ ticket2: STUB_VERIFY_TICKET ? ticketSegments[2] : undefined,
67+ ticket3: undefined,
68+ }),
69+ [STUB_VERIFY_TICKET, ticket, ticketSegments]
70+ );
71+
72+ const goToTransfer = useCallback(async () => {
73+ setIsIn(false);
74+ await timeout(DEFAULT_FADE_TIMEOUT); // Pause for UI fade animation
75+ push(names.TRANSFER);
76+ }, [names.TRANSFER, push, setIsIn]);
77+
78+ const header = useMemo(() => {
79+ return (
80+ <Box>
81+ <ActivateHeader copy={'Confirm Backup'} />
82+ <ActivateParagraph
83+ copy={
84+ 'Confirm that you have stored your Master Key somewhere safe by entering it below'
85+ }
86+ />
87+ </Box>
88+ );
89+ }, []);
90+
91+ useFadeIn();
92+
93+ return (
94+ <>
95+ <ActivateView
96+ header={header}
97+ gridRows={'20% 80%'}
98+ gridAreas={"'header' 'content'"}>
99+ <Box
100+ alignItems={'center'}
101+ display={'flex'}
102+ flexDirection={'column'}
103+ flexWrap={'nowrap'}
104+ height={'100%'}
105+ justifyContent={'center'}>
106+ <BridgeForm
107+ style={{
108+ display: 'flex',
109+ flexFlow: 'column nowrap',
110+ height: '100%',
111+ width: '100%',
112+ justifyContent: 'center',
113+ alignItems: 'center',
114+ }}
115+ validate={validate}
116+ afterSubmit={goToTransfer}
117+ onValues={onValues}
118+ initialValues={initialValues}>
119+ {({ handleSubmit, valid }) => (
120+ <Box
121+ display={'grid'}
122+ gridTemplateAreas={"'inputs' 'submit'"}
123+ gridTemplateColumns={'1fr'}
124+ gridTemplateRows={'80% 20%'}
125+ width={'100%'}
126+ height={'100%'}>
127+ <Box
128+ gridArea="inputs"
129+ display={'flex'}
130+ flexDirection={'column'}
131+ flexWrap={'nowrap'}
132+ height={'100%'}
133+ alignItems={'center'}
134+ justifyContent={'center'}>
135+ <Box
136+ display={'flex'}
137+ flexDirection={'row'}
138+ width={'80%'}
139+ height={'min-content'}
140+ flexWrap={'nowrap'}
141+ justifyContent={'center'}>
142+ {ticketSegments.map((segment, i) => {
143+ return (
144+ <Box
145+ key={i}
146+ display={'flex'}
147+ flexDirection={'row'}
148+ flexWrap={'nowrap'}>
149+ <TicketSegmentInput
150+ width={'75px'}
151+ paddingY={'18px'}
152+ fontSize={'16px'}
153+ height={'24px'}
154+ fontFamily={'Source Code Pro'}
155+ borderRadius={'5px'}
156+ border={'solid 1px rgba(0,0,0,0.25)'}
157+ name={`ticket${i}`}
158+ />
159+ {i < ticketSegments.length - 1 ? (
160+ <Text
161+ key={i}
162+ alignSelf="center"
163+ color="gray"
164+ mx={1}>
165+ —
166+ </Text>
167+ ) : null}
168+ </Box>
169+ );
170+ })}
171+ </Box>
172+ </Box>
173+
174+ <Box
175+ full
176+ style={{ display: 'none' }}
177+ as={HiddenInput}
178+ name="ticket"
179+ />
180+
181+ <Box as={FormError} />
182+
183+ <Box
184+ display="flex"
185+ flexDirection="column"
186+ flexWrap="nowrap"
187+ height={'100%'}
188+ justifyContent="flex-end"
189+ gridArea="submit">
190+ <ActivateButton disabled={!valid} onClick={handleSubmit}>
191+ Confirm Backup
192+ </ActivateButton>
193+ </Box>
194+ </Box>
195+ )}
196+ </BridgeForm>
197+ </Box>
198+ </ActivateView>
199+ <ActivateSteps currentStep={2} totalSteps={4} overrideFadeIn={true} />
200+ </>
201+ );
202+};
203+
204+export default MasterKeyConfirm;
src/views/Activate/MasterKeyDownload.tsx+12−4
@@ -17,6 +17,8 @@ import { useLocalRouter } from 'lib/LocalRouter';
1717import { compileNetworkingKey } from 'lib/keys';
1818import { downloadWallet } from 'lib/invite';
1919import PaperBuilder from 'components/PaperBuilder';
20+import { DEFAULT_FADE_TIMEOUT } from 'lib/constants';
21+import { timeout } from 'lib/timeout';
2022
2123const MasterKeyDownload = () => {
2224 const {
@@ -60,7 +62,11 @@ const MasterKeyDownload = () => {
6062 [paper, setGenerated]
6163 );
6264
63− const goToVerify = useCallback(() => push(names.VERIFY), [push, names]);
65+ const goToConfirm = useCallback(async () => {
66+ setIsIn(false);
67+ await timeout(DEFAULT_FADE_TIMEOUT); // Pause for UI fade animation
68+ push(names.CONFIRM);
69+ }, [setIsIn, push, names.CONFIRM]);
6470
6571 const header = useMemo(() => {
6672 return triggerAnimation ? (
@@ -79,12 +85,12 @@ const MasterKeyDownload = () => {
7985 return triggerAnimation ? (
8086 <ActivateButton
8187 disabled={!generated}
82− onClick={!downloaded ? download : goToVerify}
88+ onClick={!downloaded ? download : goToConfirm}
8389 success={downloaded}>
8490 {!downloaded ? 'Download Backup (Passport)' : 'Continue'}
8591 </ActivateButton>
8692 ) : null;
87− }, [download, downloaded, generated, goToVerify, triggerAnimation]);
93+ }, [download, downloaded, generated, goToConfirm, triggerAnimation]);
8894
8995 const delayedReveal = useCallback(async () => {
9096 setTimeout(() => {
@@ -114,7 +120,9 @@ const MasterKeyDownload = () => {
114120 {triggerAnimation && (
115121 <DangerBox>Do not share this with anyone else!</DangerBox>
116122 )}
117− {ticket && <MasterKeyPresenter fadeIn={true} ticket={ticket} />}
123+ {ticket && (
124+ <MasterKeyPresenter overrideFadeIn={true} ticket={ticket} />
125+ )}
118126 {triggerAnimation && <MasterKeyCopy text={ticket} />}
119127 </Box>
120128 </ActivateView>
src/views/Activate/MasterKeyPresenter.tsx−1
@@ -1,6 +1,5 @@
11import { Box, Text } from '@tlon/indigo-react';
22import { stripSigPrefix } from 'form/formatters';
3−import useImpliedTicket from 'lib/useImpliedTicket';
43import { useMemo } from 'react';
54import withFadeable from './withFadeable';
65
src/views/Activate/withFadeable.tsx+2−2
@@ -4,7 +4,7 @@ import { DEFAULT_FADE_TIMEOUT } from 'lib/constants';
44
55interface withFadeableProps {
66 fadeTimeout?: number;
7− fadeIn?: boolean;
7+ overrideFadeIn?: boolean;
88}
99
1010const withFadeable = <P extends object>(
@@ -13,7 +13,7 @@ const withFadeable = <P extends object>(
1313 const { isIn } = useActivateFlow();
1414
1515 // Allow one-off overrides, otherwise fallback to shared fade-in state
16− const _in = props?.fadeIn ? props.fadeIn : isIn;
16+ const _in = props?.overrideFadeIn ? props.overrideFadeIn : isIn;
1717
1818 return (
1919 <CSSTransition