Merge pull request #508 from ente-io/recover-modal-redesign
Recovery key modal redesign
This commit is contained in:
commit
9658fb041f
|
@ -1,86 +0,0 @@
|
|||
import styled from 'styled-components';
|
||||
import { FreeFlowText, IconButton } from './Container';
|
||||
import CopyIcon from './icons/CopyIcon';
|
||||
import React, { useState } from 'react';
|
||||
import { Tooltip, OverlayTrigger } from 'react-bootstrap';
|
||||
import TickIcon from '@mui/icons-material/Done';
|
||||
import EnteSpinner from './EnteSpinner';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: relative;
|
||||
margin: 20px 0;
|
||||
`;
|
||||
const CopyButtonWrapper = styled(IconButton)`
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
background: none !important;
|
||||
margin: 10px;
|
||||
`;
|
||||
|
||||
export const CodeWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #1a1919;
|
||||
padding: 37px 40px 20px 20px;
|
||||
color: white;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type Iprops = React.PropsWithChildren<{
|
||||
code: string;
|
||||
wordBreak?: 'normal' | 'break-all' | 'keep-all' | 'break-word';
|
||||
}>;
|
||||
export const CodeBlock = (props: Iprops) => {
|
||||
const [copied, setCopied] = useState<boolean>(false);
|
||||
|
||||
const copyToClipboardHelper = (text: string) => () => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1000);
|
||||
};
|
||||
|
||||
const RenderCopiedMessage = (props) => {
|
||||
const { style, ...rest } = props;
|
||||
return (
|
||||
<Tooltip
|
||||
{...rest}
|
||||
style={{ ...style, zIndex: 2001 }}
|
||||
id="button-tooltip">
|
||||
copied
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<CodeWrapper>
|
||||
{props.code ? (
|
||||
<FreeFlowText style={{ wordBreak: props.wordBreak }}>
|
||||
{props.code}
|
||||
</FreeFlowText>
|
||||
) : (
|
||||
<EnteSpinner />
|
||||
)}
|
||||
</CodeWrapper>
|
||||
{props.code && (
|
||||
<OverlayTrigger
|
||||
show={copied}
|
||||
placement="bottom"
|
||||
trigger={'click'}
|
||||
overlay={RenderCopiedMessage}
|
||||
delay={{ show: 200, hide: 800 }}>
|
||||
<CopyButtonWrapper
|
||||
onClick={copyToClipboardHelper(props.code)}
|
||||
style={{
|
||||
background: 'none',
|
||||
...(copied ? { color: '#51cd7c' } : {}),
|
||||
}}>
|
||||
{copied ? <TickIcon /> : <CopyIcon />}
|
||||
</CopyButtonWrapper>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
20
src/components/CodeBlock/CopyButton.tsx
Normal file
20
src/components/CodeBlock/CopyButton.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { CopyButtonWrapper } from './styledComponents';
|
||||
import DoneIcon from '@mui/icons-material/Done';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import { Tooltip } from '@mui/material';
|
||||
|
||||
export default function CopyButton({ code, copied, copyToClipboardHelper }) {
|
||||
return (
|
||||
<Tooltip arrow open={copied} title={constants.COPIED}>
|
||||
<CopyButtonWrapper onClick={copyToClipboardHelper(code)}>
|
||||
{copied ? (
|
||||
<DoneIcon fontSize="small" />
|
||||
) : (
|
||||
<ContentCopyIcon fontSize="small" />
|
||||
)}
|
||||
</CopyButtonWrapper>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
43
src/components/CodeBlock/index.tsx
Normal file
43
src/components/CodeBlock/index.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { FreeFlowText } from '../Container';
|
||||
import React, { useState } from 'react';
|
||||
import EnteSpinner from '../EnteSpinner';
|
||||
import { Wrapper, CodeWrapper } from './styledComponents';
|
||||
import CopyButton from './CopyButton';
|
||||
|
||||
type Iprops = React.PropsWithChildren<{
|
||||
code: string;
|
||||
wordBreak?: 'normal' | 'break-all' | 'keep-all' | 'break-word';
|
||||
}>;
|
||||
|
||||
// comment
|
||||
export const CodeBlock = (props: Iprops) => {
|
||||
const [copied, setCopied] = useState<boolean>(false);
|
||||
|
||||
const copyToClipboardHelper = (text: string) => () => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1000);
|
||||
};
|
||||
|
||||
if (!props.code) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<EnteSpinner />
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Wrapper>
|
||||
<CodeWrapper>
|
||||
<FreeFlowText style={{ wordBreak: props.wordBreak }}>
|
||||
{props.code}
|
||||
</FreeFlowText>
|
||||
</CodeWrapper>
|
||||
<CopyButton
|
||||
code={props.code}
|
||||
copied={copied}
|
||||
copyToClipboardHelper={copyToClipboardHelper}
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
21
src/components/CodeBlock/styledComponents.tsx
Normal file
21
src/components/CodeBlock/styledComponents.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { IconButton } from '@mui/material';
|
||||
import { CenteredFlex } from 'components/Container';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled(CenteredFlex)`
|
||||
position: relative;
|
||||
background: ${({ theme }) => theme.palette.accent.dark};
|
||||
border-radius: ${({ theme }) => theme.shape.borderRadius}px;
|
||||
min-height: 80px;
|
||||
`;
|
||||
export const CopyButtonWrapper = styled(IconButton)`
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
margin: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const CodeWrapper = styled.div`
|
||||
padding: 36px 36px 16px 16px;
|
||||
border-radius: ${({ theme }) => theme.shape.borderRadius}px;
|
||||
`;
|
|
@ -175,12 +175,11 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
|
|||
disablePadding: true,
|
||||
'aria-labelledby': 'collection-options',
|
||||
}}>
|
||||
<Paper sx={{ borderRadius: '10px' }}>
|
||||
<Paper>
|
||||
<MenuList
|
||||
sx={{
|
||||
padding: 0,
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
}}>
|
||||
<MenuItem>
|
||||
<ListItem onClick={showRenameCollectionModal}>
|
||||
|
|
|
@ -1,20 +1,6 @@
|
|||
import React from 'react';
|
||||
import { Spinner } from 'react-bootstrap';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
|
||||
export default function EnteSpinner(props) {
|
||||
const { style, ...others } = props ?? {};
|
||||
return (
|
||||
<Spinner
|
||||
animation="border"
|
||||
style={{
|
||||
width: '36px',
|
||||
height: '36px',
|
||||
borderWidth: '0.20em',
|
||||
color: '#51cd7c',
|
||||
...(style && style),
|
||||
}}
|
||||
{...others}
|
||||
role="status"
|
||||
/>
|
||||
);
|
||||
return <CircularProgress color="accent" size={32} {...props} />;
|
||||
}
|
||||
|
|
74
src/components/RecoveryKey/index.tsx
Normal file
74
src/components/RecoveryKey/index.tsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { downloadAsFile } from 'utils/file';
|
||||
import { getRecoveryKey } from 'utils/crypto';
|
||||
import constants from 'utils/strings/constants';
|
||||
import MessageDialog from '../MessageDialog';
|
||||
import { CodeBlock } from '../CodeBlock';
|
||||
import { ButtonProps, Typography } from '@mui/material';
|
||||
import * as bip39 from 'bip39';
|
||||
import { DashedBorderWrapper } from './styledComponents';
|
||||
|
||||
// mobile client library only supports english.
|
||||
bip39.setDefaultWordlist('english');
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
onHide: () => void;
|
||||
somethingWentWrong: any;
|
||||
}
|
||||
|
||||
function RecoveryKey({ somethingWentWrong, ...props }: Props) {
|
||||
const [recoveryKey, setRecoveryKey] = useState(null);
|
||||
useEffect(() => {
|
||||
if (!props.show) {
|
||||
return;
|
||||
}
|
||||
const main = async () => {
|
||||
try {
|
||||
const recoveryKey = await getRecoveryKey();
|
||||
setRecoveryKey(bip39.entropyToMnemonic(recoveryKey));
|
||||
} catch (e) {
|
||||
somethingWentWrong();
|
||||
props.onHide();
|
||||
}
|
||||
};
|
||||
main();
|
||||
}, [props.show]);
|
||||
|
||||
function onSaveClick() {
|
||||
downloadAsFile(constants.RECOVERY_KEY_FILENAME, recoveryKey);
|
||||
props.onHide();
|
||||
}
|
||||
|
||||
const recoveryKeyDialogAttributes = {
|
||||
title: constants.RECOVERY_KEY,
|
||||
close: {
|
||||
text: constants.SAVE_LATER,
|
||||
variant: 'secondary' as ButtonProps['color'],
|
||||
},
|
||||
staticBackdrop: true,
|
||||
proceed: {
|
||||
text: constants.SAVE,
|
||||
action: onSaveClick,
|
||||
disabled: !recoveryKey,
|
||||
variant: 'accent' as ButtonProps['color'],
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<MessageDialog
|
||||
show={props.show}
|
||||
onHide={props.onHide}
|
||||
size="sm"
|
||||
attributes={recoveryKeyDialogAttributes}>
|
||||
<Typography mb={3}>{constants.RECOVERY_KEY_DESCRIPTION}</Typography>
|
||||
<DashedBorderWrapper>
|
||||
<CodeBlock code={recoveryKey} />
|
||||
<Typography m={2}>
|
||||
{constants.KEY_NOT_STORED_DISCLAIMER}
|
||||
</Typography>
|
||||
</DashedBorderWrapper>
|
||||
</MessageDialog>
|
||||
);
|
||||
}
|
||||
export default RecoveryKey;
|
6
src/components/RecoveryKey/styledComponents.tsx
Normal file
6
src/components/RecoveryKey/styledComponents.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { Box, styled } from '@mui/material';
|
||||
|
||||
export const DashedBorderWrapper = styled(Box)(({ theme }) => ({
|
||||
border: `1px dashed ${theme.palette.grey.A400}`,
|
||||
borderRadius: theme.spacing(1),
|
||||
}));
|
|
@ -1,66 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { downloadAsFile } from 'utils/file';
|
||||
import { getRecoveryKey } from 'utils/crypto';
|
||||
import constants from 'utils/strings/constants';
|
||||
import MessageDialog from './MessageDialog';
|
||||
import { CodeBlock } from './CodeBlock';
|
||||
const bip39 = require('bip39');
|
||||
// mobile client library only supports english.
|
||||
bip39.setDefaultWordlist('english');
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
onHide: () => void;
|
||||
somethingWentWrong: any;
|
||||
}
|
||||
function RecoveryKeyModal({ somethingWentWrong, ...props }: Props) {
|
||||
const [recoveryKey, setRecoveryKey] = useState(null);
|
||||
useEffect(() => {
|
||||
if (!props.show) {
|
||||
return;
|
||||
}
|
||||
const main = async () => {
|
||||
try {
|
||||
const recoveryKey = await getRecoveryKey();
|
||||
setRecoveryKey(bip39.entropyToMnemonic(recoveryKey));
|
||||
} catch (e) {
|
||||
somethingWentWrong();
|
||||
props.onHide();
|
||||
}
|
||||
};
|
||||
main();
|
||||
}, [props.show]);
|
||||
|
||||
function onSaveClick() {
|
||||
downloadAsFile(constants.RECOVERY_KEY_FILENAME, recoveryKey);
|
||||
onClose();
|
||||
}
|
||||
function onClose() {
|
||||
props.onHide();
|
||||
}
|
||||
return (
|
||||
<MessageDialog
|
||||
show={props.show}
|
||||
onHide={onClose}
|
||||
size="lg"
|
||||
attributes={{
|
||||
title: constants.RECOVERY_KEY,
|
||||
close: {
|
||||
text: constants.SAVE_LATER,
|
||||
variant: 'danger',
|
||||
},
|
||||
staticBackdrop: true,
|
||||
proceed: {
|
||||
text: constants.SAVE,
|
||||
action: onSaveClick,
|
||||
disabled: !recoveryKey,
|
||||
variant: 'success',
|
||||
},
|
||||
}}>
|
||||
<p>{constants.RECOVERY_KEY_DESCRIPTION}</p>
|
||||
<CodeBlock code={recoveryKey} />
|
||||
<p>{constants.KEY_NOT_STORED_DISCLAIMER}</p>
|
||||
</MessageDialog>
|
||||
);
|
||||
}
|
||||
export default RecoveryKeyModal;
|
|
@ -21,7 +21,6 @@ export default function SubscriptionDetails({ userDetails }: Iprops) {
|
|||
flexDirection={'column'}
|
||||
height={160}
|
||||
bgcolor="accent.main"
|
||||
borderRadius={'8px'}
|
||||
position={'relative'}>
|
||||
{userDetails ? (
|
||||
<>
|
||||
|
@ -61,7 +60,6 @@ export default function SubscriptionDetails({ userDetails }: Iprops) {
|
|||
position={'relative'}
|
||||
zIndex="100"
|
||||
height="64px"
|
||||
borderRadius={'0 0 8px 8px'}
|
||||
bgcolor="accent.dark"
|
||||
padding="16px">
|
||||
<LinearProgress
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { useContext, useState } from 'react';
|
|||
import SidebarButton from './Button';
|
||||
import constants from 'utils/strings/constants';
|
||||
import FixLargeThumbnails from 'components/FixLargeThumbnail';
|
||||
import RecoveryKeyModal from 'components/RecoveryKeyModal';
|
||||
import RecoveryKey from 'components/RecoveryKey';
|
||||
import TwoFactorModal from 'components/TwoFactorModal';
|
||||
import { PAGES } from 'constants/pages';
|
||||
import { useRouter } from 'next/router';
|
||||
|
@ -65,7 +65,7 @@ export default function UtilitySection({ closeSidebar }) {
|
|||
{constants.COMPRESS_THUMBNAILS}
|
||||
</SidebarButton>
|
||||
|
||||
<RecoveryKeyModal
|
||||
<RecoveryKey
|
||||
show={recoverModalView}
|
||||
onHide={closeRecoveryKeyModal}
|
||||
somethingWentWrong={somethingWentWrong}
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from 'utils/crypto';
|
||||
import SetPasswordForm from 'components/SetPasswordForm';
|
||||
import { justSignedUp, setJustSignedUp } from 'utils/storage';
|
||||
import RecoveryKeyModal from 'components/RecoveryKeyModal';
|
||||
import RecoveryKey from 'components/RecoveryKey';
|
||||
import { PAGES } from 'constants/pages';
|
||||
import Container from 'components/Container';
|
||||
import EnteSpinner from 'components/EnteSpinner';
|
||||
|
@ -84,7 +84,7 @@ export default function Generate() {
|
|||
</EnteSpinner>
|
||||
</Container>
|
||||
) : recoverModalView ? (
|
||||
<RecoveryKeyModal
|
||||
<RecoveryKey
|
||||
show={recoverModalView}
|
||||
onHide={() => {
|
||||
setRecoveryModalView(false);
|
||||
|
|
|
@ -17,12 +17,11 @@
|
|||
}
|
||||
html, body {
|
||||
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family:Inter, Arial, sans-serif !important;
|
||||
font-family:Inter, Arial, sans-serif;
|
||||
}
|
||||
|
||||
#__next {
|
||||
|
|
|
@ -61,6 +61,20 @@ const darkThemeOptions = createTheme({
|
|||
},
|
||||
},
|
||||
},
|
||||
MuiDialog: {
|
||||
styleOverrides: {
|
||||
paper: {
|
||||
'& .MuiDialogActions-root': {
|
||||
padding: '32px 24px',
|
||||
},
|
||||
},
|
||||
root: {
|
||||
'& .MuiDialogActions-root button': {
|
||||
marginLeft: '16px',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
palette: {
|
||||
|
@ -77,8 +91,9 @@ const darkThemeOptions = createTheme({
|
|||
secondary: '#808080',
|
||||
},
|
||||
accent: {
|
||||
main: '#43BA6C',
|
||||
dark: '#369556',
|
||||
main: '#1dba54',
|
||||
dark: '#248546',
|
||||
light: '#2cd366',
|
||||
},
|
||||
|
||||
danger: {
|
||||
|
@ -88,6 +103,7 @@ const darkThemeOptions = createTheme({
|
|||
grey: {
|
||||
A100: '#ccc',
|
||||
A200: 'rgba(256, 256, 256, 0.24)',
|
||||
A400: '#434343',
|
||||
},
|
||||
divider: 'rgba(255, 255, 255, 0.24)',
|
||||
},
|
||||
|
|
7
src/types/theme/styled.d.ts
vendored
Normal file
7
src/types/theme/styled.d.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
// styled.d.ts
|
||||
import 'styled-components';
|
||||
import { Theme } from '@mui/material';
|
||||
|
||||
declare module 'styled-components' {
|
||||
export interface DefaultTheme extends Theme {}
|
||||
}
|
|
@ -161,10 +161,10 @@ const englishConstants = {
|
|||
CHANGE_PASSWORD: 'Change password',
|
||||
GO_BACK: 'go back',
|
||||
RECOVERY_KEY: 'Recovery key',
|
||||
SAVE_LATER: 'save later',
|
||||
SAVE: 'save',
|
||||
SAVE_LATER: 'Do this later',
|
||||
SAVE: 'Save Key',
|
||||
RECOVERY_KEY_DESCRIPTION:
|
||||
'if you forget your password, the only way you can recover your data is with this key',
|
||||
'If you forget your password, the only way you can recover your data is with this key.',
|
||||
RECOVER_KEY_GENERATION_FAILED:
|
||||
'recovery code could not be generated, please try again',
|
||||
KEY_NOT_STORED_DISCLAIMER:
|
||||
|
@ -720,6 +720,7 @@ const englishConstants = {
|
|||
ALL_ALBUMS: 'All Albums',
|
||||
PHOTOS: 'Photos',
|
||||
ENDS: 'Ends',
|
||||
COPIED: 'copied',
|
||||
};
|
||||
|
||||
export default englishConstants;
|
||||
|
|
Loading…
Reference in a new issue