on master help sign in
ux: Confirm Master Key
5a30d53tomholford
src/form/BridgeForm.js+3−1
@@ -36,7 +36,9 @@ export default function BridgeForm({
36 return (
36 return (
37 <Form mutators={{ ...arrayMutators }} onSubmit={_onSubmit} {...rest}>
37 <Form mutators={{ ...arrayMutators }} onSubmit={_onSubmit} {...rest}>
38 {formProps => (
38 {formProps => (
39+ <form
40+ style={formProps?.style || { display: 'contents' }}
39− <form style={{ display: 'contents' }} onSubmit={formProps.handleSubmit}>
41+ onSubmit={formProps.handleSubmit}>
40 <ValidationPauser />
42 <ValidationPauser />
41 {children(formProps)}
43 {children(formProps)}
42 {onValues && <ValuesHandler {...formProps} onValues={onValues} />}
44 {onValues && <ValuesHandler {...formProps} onValues={onValues} />}
src/form/Inputs.js+26−1
@@ -12,12 +12,13 @@ import {
12} from 'form/formatters';
12} from 'form/formatters';
13import { DEFAULT_HD_PATH } from 'lib/constants';
13import { DEFAULT_HD_PATH } from 'lib/constants';
14import InputSigil from 'components/InputSigil';
14import InputSigil from 'components/InputSigil';
15+import { StatelessTextInput } from '@tlon/indigo-react';
15
16
16const PLACEHOLDER_POINT = '~sampel-ponnym';
17const PLACEHOLDER_POINT = '~sampel-ponnym';
17const PLACEHOLDER_HD_PATH = DEFAULT_HD_PATH;
18const PLACEHOLDER_HD_PATH = DEFAULT_HD_PATH;
18const PLACEHOLDER_MNEMONIC =
19const PLACEHOLDER_MNEMONIC =
19 'example crew supreme gesture quantum web media hazard theory mercy wing kitten';
20 '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';
21const PLACEHOLDER_PASSWORD = '••••••-••••••-••••••-••••••';
22const PLACEHOLDER_PASSWORD = '••••••-••••••-••••••-••••••';
22const PLACEHOLDER_ADDRESS = '0x';
23const PLACEHOLDER_ADDRESS = '0x';
23const PLACEHOLDER_HEX = '0x';
24const PLACEHOLDER_HEX = '0x';
@@ -208,3 +209,27 @@ export function EmailInput({ ...rest }) {
208 />
209 />
209 );
210 );
210}
211}
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 => {
96 ? s.slice(SIG_PREFIX.length)
96 ? s.slice(SIG_PREFIX.length)
97 : s;
97 : s;
98};
98};
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 {
20 validateLength,
20 validateLength,
21 validateUnique,
21 validateUnique,
22 validatePsbt,
22 validatePsbt,
23+ validateSegmentedTicket,
23} from 'lib/validators';
24} from 'lib/validators';
24import { isPromise } from 'lib/isPromise';
25import { isPromise } from 'lib/isPromise';
25
26
@@ -62,6 +63,8 @@ export const buildArrayValidator = validator => values => {
62export const hasErrors = iter =>
63export const hasErrors = iter =>
63 some(iter, v => (Array.isArray(v) ? hasErrors(v) : v !== undefined));
64 some(iter, v => (Array.isArray(v) ? hasErrors(v) : v !== undefined));
64
65
66+export const buildSegmentedTicketValidator = (validators = []) =>
67+ buildValidator([validateNotEmpty, validateSegmentedTicket, ...validators]);
65export const buildPatqValidator = (validators = []) =>
68export const buildPatqValidator = (validators = []) =>
66 buildValidator([validateNotEmpty, validatePatq, ...validators]);
69 buildValidator([validateNotEmpty, validatePatq, ...validators]);
67export const buildShardValidator = (validators = []) =>
70export const buildShardValidator = (validators = []) =>
src/lib/constants.ts+1−1
@@ -122,7 +122,7 @@ const POINT_PROXIES = {
122
122
123// In ms
123// In ms
124const DEFAULT_FADE_TIMEOUT = 300;
124const DEFAULT_FADE_TIMEOUT = 300;
125−const MASTER_KEY_DURATION = 1800;
125+const MASTER_KEY_DURATION = 1900;
126
126
127export {
127export {
128 CHECK_BLOCK_EVERY_MS,
128 CHECK_BLOCK_EVERY_MS,
src/lib/validators.js → src/lib/validators.tsrenamed+6
@@ -12,6 +12,7 @@ import {
12 MIN_GALAXY,
12 MIN_GALAXY,
13 MAX_GALAXY,
13 MAX_GALAXY,
14} from './constants';
14} from './constants';
15+import { ticketFromSegments } from 'form/formatters';
15
16
16// NOTE: do not use the /g modifier for these regexes
17// NOTE: do not use the /g modifier for these regexes
17// https://stackoverflow.com/a/21373261
18// https://stackoverflow.com/a/21373261
@@ -146,3 +147,8 @@ export const validatePsbt = base64 => {
146 return 'Invalid Partially Signed Bitcoin Transaction';
147 return 'Invalid Partially Signed Bitcoin Transaction';
147 }
148 }
148};
149};
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';
5interface ActivateButtonProps {
5interface ActivateButtonProps {
6 children: React.ReactNode | string;
6 children: React.ReactNode | string;
7 onClick: MouseEventHandler;
7 onClick: MouseEventHandler;
8− success?: boolean;
8+ disabled?: boolean;
9}
9}
10
10
11const ActivateButton = ({
11const ActivateButton = ({
12 children,
12 children,
13 onClick,
13 onClick,
14− success,
14+ disabled,
15+ ...rest
15}: ActivateButtonProps & React.ComponentPropsWithoutRef<'button'>) => {
16}: ActivateButtonProps & React.ComponentPropsWithoutRef<'button'>) => {
16 return (
17 return (
17− <Box
18+ <Button
18− display="flex"
19− flexDirection="column"
20− flexWrap="nowrap"
21− height={'100%'}
22− justifyContent="flex-end">
23− <Button
24− onClick={onClick}
19+ onClick={onClick}
25− backgroundColor={success ? 'rgb(70,156,106)' : 'black'}
20+ backgroundColor={disabled ? 'rgba(0,0,0,0.2)' : 'black'}
26− color={'white'}
21+ color={'white'}
27− padding={'16px'}
22+ padding={'16px'}
28− fontFamily="Inter"
23+ fontFamily="Inter"
29− height={'50px'}
24+ height={'50px'}
30− fontWeight={'400'}
25+ fontWeight={'400'}
31− fontSize={'18px'}>
26+ fontSize={'18px'}
27+ {...rest}>
32− {children}
28+ {children}
33− </Button>
29+ </Button>
34− </Box>
35 );
30 );
36};
31};
37
32
src/views/Activate/ActivateMasterKey.tsx+3−3
@@ -5,21 +5,21 @@ import View from 'components/View';
5import useRouter from 'lib/useRouter';
5import useRouter from 'lib/useRouter';
6
6
7import MasterKeyDownload from './MasterKeyDownload';
7import MasterKeyDownload from './MasterKeyDownload';
8−import PassportVerify from './PassportVerify';
9import PassportTransfer from './PassportTransfer';
8import PassportTransfer from './PassportTransfer';
10import MasterKeyReveal from './MasterKeyReveal';
9import MasterKeyReveal from './MasterKeyReveal';
10+import MasterKeyConfirm from './MasterKeyConfirm';
11
11
12const NAMES = {
12const NAMES = {
13 REVEAL: 'REVEAL',
13 REVEAL: 'REVEAL',
14 DOWNLOAD: 'DOWNLOAD',
14 DOWNLOAD: 'DOWNLOAD',
15− VERIFY: 'VERIFY',
15+ CONFIRM: 'CONFIRM',
16 TRANSFER: 'TRANSFER',
16 TRANSFER: 'TRANSFER',
17};
17};
18
18
19const VIEWS = {
19const VIEWS = {
20 [NAMES.REVEAL]: MasterKeyReveal,
20 [NAMES.REVEAL]: MasterKeyReveal,
21 [NAMES.DOWNLOAD]: MasterKeyDownload,
21 [NAMES.DOWNLOAD]: MasterKeyDownload,
22− [NAMES.VERIFY]: PassportVerify,
22+ [NAMES.CONFIRM]: MasterKeyConfirm,
23 [NAMES.TRANSFER]: PassportTransfer,
23 [NAMES.TRANSFER]: PassportTransfer,
24};
24};
25
25
src/views/Activate/ActivateView.tsx+20−9
@@ -1,29 +1,30 @@
1import { Box } from '@tlon/indigo-react';
1import { Box } from '@tlon/indigo-react';
2import cn from 'classnames';
2import cn from 'classnames';
3
3
4−// import Steps from 'components/Steps';
5−type ActivateViewProps = {
4+interface ActivateViewProps {
6 className?: string;
5 className?: string;
7 children?: React.ReactNode;
6 children?: React.ReactNode;
8 header?: React.ReactNode | string;
7 header?: React.ReactNode | string;
9 footer?: React.ReactNode;
8 footer?: React.ReactNode;
10− step?: number | null;
9+ gridRows?: string;
10+ gridAreas?: string;
11−};
11+}
12
12
13export default function ActivateView({
13export default function ActivateView({
14 className,
14 className,
15 children,
15 children,
16 header,
16 header,
17 footer,
17 footer,
18− step = null,
18+ gridRows,
19+ gridAreas,
19}: ActivateViewProps) {
20}: ActivateViewProps) {
20 return (
21 return (
21 <>
22 <>
22 <Box
23 <Box
23 display="grid"
24 display="grid"
24 gridTemplateColumns="1fr"
25 gridTemplateColumns="1fr"
25− gridTemplateRows="20% 60% 20%"
26+ gridTemplateRows={gridRows || '20% 60% 20%'}
26− gridTemplateAreas={"'header' 'content' 'footer'"}
27+ gridTemplateAreas={gridAreas || "'header' 'content' 'footer'"}
27 border={'1px solid rgb(204, 204, 204)'}
28 border={'1px solid rgb(204, 204, 204)'}
28 border-radius={'5px'}
29 border-radius={'5px'}
29 padding={'10px'}
30 padding={'10px'}
@@ -35,9 +36,19 @@ export default function ActivateView({
35 mb={3}>
36 mb={3}>
36 <Box gridArea="header">{header}</Box>
37 <Box gridArea="header">{header}</Box>
37 <Box gridArea="content">{children}</Box>
38 <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>
39 </Box>
51 </Box>
40− <Box>{step ? step : null}</Box>
41 </>
52 </>
42 );
53 );
43}
54}
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';
17import { compileNetworkingKey } from 'lib/keys';
17import { compileNetworkingKey } from 'lib/keys';
18import { downloadWallet } from 'lib/invite';
18import { downloadWallet } from 'lib/invite';
19import PaperBuilder from 'components/PaperBuilder';
19import PaperBuilder from 'components/PaperBuilder';
20+import { DEFAULT_FADE_TIMEOUT } from 'lib/constants';
21+import { timeout } from 'lib/timeout';
20
22
21const MasterKeyDownload = () => {
23const MasterKeyDownload = () => {
22 const {
24 const {
@@ -60,7 +62,11 @@ const MasterKeyDownload = () => {
60 [paper, setGenerated]
62 [paper, setGenerated]
61 );
63 );
62
64
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]);
64
70
65 const header = useMemo(() => {
71 const header = useMemo(() => {
66 return triggerAnimation ? (
72 return triggerAnimation ? (
@@ -79,12 +85,12 @@ const MasterKeyDownload = () => {
79 return triggerAnimation ? (
85 return triggerAnimation ? (
80 <ActivateButton
86 <ActivateButton
81 disabled={!generated}
87 disabled={!generated}
82− onClick={!downloaded ? download : goToVerify}
88+ onClick={!downloaded ? download : goToConfirm}
83 success={downloaded}>
89 success={downloaded}>
84 {!downloaded ? 'Download Backup (Passport)' : 'Continue'}
90 {!downloaded ? 'Download Backup (Passport)' : 'Continue'}
85 </ActivateButton>
91 </ActivateButton>
86 ) : null;
92 ) : null;
87− }, [download, downloaded, generated, goToVerify, triggerAnimation]);
93+ }, [download, downloaded, generated, goToConfirm, triggerAnimation]);
88
94
89 const delayedReveal = useCallback(async () => {
95 const delayedReveal = useCallback(async () => {
90 setTimeout(() => {
96 setTimeout(() => {
@@ -114,7 +120,9 @@ const MasterKeyDownload = () => {
114 {triggerAnimation && (
120 {triggerAnimation && (
115 <DangerBox>Do not share this with anyone else!</DangerBox>
121 <DangerBox>Do not share this with anyone else!</DangerBox>
116 )}
122 )}
123+ {ticket && (
117− {ticket && <MasterKeyPresenter fadeIn={true} ticket={ticket} />}
124+ <MasterKeyPresenter overrideFadeIn={true} ticket={ticket} />
125+ )}
118 {triggerAnimation && <MasterKeyCopy text={ticket} />}
126 {triggerAnimation && <MasterKeyCopy text={ticket} />}
119 </Box>
127 </Box>
120 </ActivateView>
128 </ActivateView>
src/views/Activate/MasterKeyPresenter.tsx−1
@@ -1,6 +1,5 @@
1import { Box, Text } from '@tlon/indigo-react';
1import { Box, Text } from '@tlon/indigo-react';
2import { stripSigPrefix } from 'form/formatters';
2import { stripSigPrefix } from 'form/formatters';
3−import useImpliedTicket from 'lib/useImpliedTicket';
4import { useMemo } from 'react';
3import { useMemo } from 'react';
5import withFadeable from './withFadeable';
4import withFadeable from './withFadeable';
6
5
src/views/Activate/withFadeable.tsx+2−2
@@ -4,7 +4,7 @@ import { DEFAULT_FADE_TIMEOUT } from 'lib/constants';
4
4
5interface withFadeableProps {
5interface withFadeableProps {
6 fadeTimeout?: number;
6 fadeTimeout?: number;
7− fadeIn?: boolean;
7+ overrideFadeIn?: boolean;
8}
8}
9
9
10const withFadeable = <P extends object>(
10const withFadeable = <P extends object>(
@@ -13,7 +13,7 @@ const withFadeable = <P extends object>(
13 const { isIn } = useActivateFlow();
13 const { isIn } = useActivateFlow();
14
14
15 // Allow one-off overrides, otherwise fallback to shared fade-in state
15 // 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;
17
17
18 return (
18 return (
19 <CSSTransition
19 <CSSTransition