refactored generate page

This commit is contained in:
Abhinav 2023-11-02 22:23:14 +05:30
parent 87ae9f8d31
commit 9eb793d606
22 changed files with 766 additions and 196 deletions

View file

@ -1,130 +1,17 @@
import React, { useState, useEffect, useContext } from 'react';
import { t } from 'i18next';
import { configureSRP, logoutUser, putAttributes } from 'services/userService';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import GeneratePage from '@ente/accounts/pages/generate';
import { useRouter } from 'next/router';
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
import {
saveKeyInSessionStore,
generateAndSaveIntermediateKeyAttributes,
generateKeyAndSRPAttributes,
} from 'utils/crypto';
import SetPasswordForm from 'components/SetPasswordForm';
import { justSignedUp, setJustSignedUp } from 'utils/storage';
import RecoveryKey from 'components/RecoveryKey';
import { PAGES } from 'constants/pages';
import { VerticallyCentered } from 'components/Container';
import EnteSpinner from 'components/EnteSpinner';
import { AppContext } from 'pages/_app';
import { logError } from 'utils/sentry';
import { KeyAttributes, User } from 'types/user';
import FormPaper from 'components/Form/FormPaper';
import FormTitle from 'components/Form/FormPaper/Title';
import { APPS, getAppName } from 'constants/apps';
import FormPaperFooter from 'components/Form/FormPaper/Footer';
import LinkButton from 'components/pages/gallery/LinkButton';
import { useContext } from 'react';
import { APPS } from '@ente/shared/apps/constants';
export default function Generate() {
const [token, setToken] = useState<string>();
const [user, setUser] = useState<User>();
const router = useRouter();
const [recoverModalView, setRecoveryModalView] = useState(false);
const [loading, setLoading] = useState(true);
const appContext = useContext(AppContext);
useEffect(() => {
const main = async () => {
const key: string = getKey(SESSION_KEYS.ENCRYPTION_KEY);
const keyAttributes: KeyAttributes = getData(
LS_KEYS.ORIGINAL_KEY_ATTRIBUTES
);
router.prefetch(PAGES.GALLERY);
router.prefetch(PAGES.CREDENTIALS);
const user: User = getData(LS_KEYS.USER);
setUser(user);
if (!user?.token) {
router.push(PAGES.ROOT);
} else if (key) {
if (justSignedUp()) {
setRecoveryModalView(true);
setLoading(false);
} else {
const appName = getAppName();
if (appName === APPS.AUTH) {
router.push(PAGES.AUTH);
} else {
router.push(PAGES.GALLERY);
}
}
} else if (keyAttributes?.encryptedKey) {
router.push(PAGES.CREDENTIALS);
} else {
setToken(user.token);
setLoading(false);
}
};
main();
appContext.showNavBar(true);
}, []);
const onSubmit = async (passphrase, setFieldError) => {
try {
const { keyAttributes, masterKey, srpSetupAttributes } =
await generateKeyAndSRPAttributes(passphrase);
await putAttributes(token, keyAttributes);
await configureSRP(srpSetupAttributes);
await generateAndSaveIntermediateKeyAttributes(
passphrase,
keyAttributes,
masterKey
);
await saveKeyInSessionStore(SESSION_KEYS.ENCRYPTION_KEY, masterKey);
setJustSignedUp(true);
setRecoveryModalView(true);
} catch (e) {
logError(e, 'failed to generate password');
setFieldError('passphrase', t('PASSWORD_GENERATION_FAILED'));
}
};
const router = useRouter();
return (
<>
{loading ? (
<VerticallyCentered>
<EnteSpinner />
</VerticallyCentered>
) : recoverModalView ? (
<RecoveryKey
show={recoverModalView}
onHide={() => {
setRecoveryModalView(false);
const appName = getAppName();
if (appName === APPS.AUTH) {
router.push(PAGES.AUTH);
} else {
router.push(PAGES.GALLERY);
}
}}
somethingWentWrong={() => null}
/>
) : (
<VerticallyCentered>
<FormPaper>
<FormTitle>{t('SET_PASSPHRASE')}</FormTitle>
<SetPasswordForm
userEmail={user?.email}
callback={onSubmit}
buttonText={t('SET_PASSPHRASE')}
/>
<FormPaperFooter>
<LinkButton onClick={logoutUser}>
{t('GO_BACK')}
</LinkButton>
</FormPaperFooter>
</FormPaper>
</VerticallyCentered>
)}
</>
<GeneratePage
appContext={appContext}
router={router}
appName={APPS.PHOTOS}
/>
);
}

View file

@ -151,3 +151,9 @@ export const setRecoveryKey = (token: string, recoveryKey: RecoveryKey) =>
HTTPService.put(`${ENDPOINT}/users/recovery-key`, recoveryKey, null, {
'X-Auth-Token': token,
});
export const disableTwoFactor = async () => {
await HTTPService.post(`${ENDPOINT}/users/two-factor/disable`, null, null, {
'X-Auth-Token': getToken(),
});
};

View file

@ -0,0 +1,83 @@
import React, { useEffect, useState } from 'react';
import { downloadAsFile } from '@ente/shared/utils';
import { getRecoveryKey } from '@ente/shared/crypto/helpers';
import CodeBlock from '@ente/shared/components/CodeBlock';
import {
Button,
Dialog,
DialogActions,
DialogContent,
Typography,
} from '@mui/material';
import * as bip39 from 'bip39';
import { DashedBorderWrapper } from './styledComponents';
import DialogTitleWithCloseButton from '@ente/shared/components/DialogBox/TitleWithCloseButton';
import { t } from 'i18next';
import { PageProps } from '@ente/shared/apps/types';
// mobile client library only supports english.
bip39.setDefaultWordlist('english');
const RECOVERY_KEY_FILE_NAME = 'ente-recovery-key.txt';
interface Props {
appContext: PageProps['appContext'];
show: boolean;
onHide: () => void;
somethingWentWrong: any;
}
function RecoveryKey({ somethingWentWrong, appContext, ...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(RECOVERY_KEY_FILE_NAME, recoveryKey);
props.onHide();
}
return (
<Dialog
fullScreen={appContext.isMobile}
open={props.show}
onClose={props.onHide}
maxWidth="xs">
<DialogTitleWithCloseButton onClose={props.onHide}>
{t('RECOVERY_KEY')}
</DialogTitleWithCloseButton>
<DialogContent>
<Typography mb={3}>{t('RECOVERY_KEY_DESCRIPTION')}</Typography>
<DashedBorderWrapper>
<CodeBlock code={recoveryKey} />
<Typography m={2}>
{t('KEY_NOT_STORED_DISCLAIMER')}
</Typography>
</DashedBorderWrapper>
</DialogContent>
<DialogActions>
<Button color="secondary" size="large" onClick={props.onHide}>
{t('SAVE_LATER')}
</Button>
<Button color="accent" size="large" onClick={onSaveClick}>
{t('SAVE')}
</Button>
</DialogActions>
</Dialog>
);
}
export default RecoveryKey;

View 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),
}));

View file

@ -8,10 +8,8 @@ import {
generateAndSaveIntermediateKeyAttributes,
saveKeyInSessionStore,
} from '@ente/shared/crypto/helpers';
import {
generateKeyAndSRPAttributes,
isWeakPassword,
} from '@ente/accounts/utils';
import { isWeakPassword } from '@ente/accounts/utils';
import { generateKeyAndSRPAttributes } from '@ente/accounts/utils/srp';
import { setJustSignedUp } from '@ente/shared/storage/localStorage/helpers';
import { SESSION_KEYS } from '@ente/shared/storage/sessionStorage';

View file

@ -0,0 +1,130 @@
import React, { useState, useEffect } from 'react';
import { t } from 'i18next';
import { logoutUser } from '@ente/accounts/services/user';
import { putAttributes } from '@ente/accounts/api/user';
import { configureSRP } from '@ente/accounts/services/srp';
import { getData, LS_KEYS } from '@ente/shared/storage/localStorage';
import { getKey, SESSION_KEYS } from '@ente/shared/storage/sessionStorage';
import {
saveKeyInSessionStore,
generateAndSaveIntermediateKeyAttributes,
} from '@ente/shared/crypto/helpers';
import { generateKeyAndSRPAttributes } from '@ente/accounts/utils/srp';
import SetPasswordForm from '@ente/accounts/components/SetPasswordForm';
import {
justSignedUp,
setJustSignedUp,
} from '@ente/shared/storage/localStorage/helpers';
import RecoveryKey from '@ente/accounts/components/RecoveryKey';
import { PAGES } from '@ente/accounts/constants/pages';
import { VerticallyCentered } from '@ente/shared/components/Container';
import EnteSpinner from '@ente/shared/components/EnteSpinner';
import { logError } from '@ente/shared/sentry';
import { KeyAttributes, User } from '@ente/shared/user/types';
import FormPaper from '@ente/shared/components/Form/FormPaper';
import FormTitle from '@ente/shared/components/Form/FormPaper/Title';
import { APPS } from '@ente/shared/apps/constants';
import FormPaperFooter from '@ente/shared/components/Form/FormPaper/Footer';
import LinkButton from '@ente/shared/components/LinkButton';
import { PageProps } from '@ente/shared/apps/types';
export default function Generate({ router, appContext, appName }: PageProps) {
const [user, setUser] = useState<User>();
const [recoverModalView, setRecoveryModalView] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
const main = async () => {
const key: string = getKey(SESSION_KEYS.ENCRYPTION_KEY);
const keyAttributes: KeyAttributes = getData(
LS_KEYS.ORIGINAL_KEY_ATTRIBUTES
);
router.prefetch(PAGES.GALLERY);
router.prefetch(PAGES.CREDENTIALS);
const user: User = getData(LS_KEYS.USER);
setUser(user);
if (!user?.token) {
router.push(PAGES.ROOT);
} else if (key) {
if (justSignedUp()) {
setRecoveryModalView(true);
setLoading(false);
} else {
if (appName === APPS.AUTH) {
router.push(PAGES.AUTH);
} else {
router.push(PAGES.GALLERY);
}
}
} else if (keyAttributes?.encryptedKey) {
router.push(PAGES.CREDENTIALS);
} else {
setLoading(false);
}
};
main();
appContext.showNavBar(true);
}, []);
const onSubmit = async (passphrase, setFieldError) => {
try {
const { keyAttributes, masterKey, srpSetupAttributes } =
await generateKeyAndSRPAttributes(passphrase);
await putAttributes(keyAttributes);
await configureSRP(srpSetupAttributes);
await generateAndSaveIntermediateKeyAttributes(
passphrase,
keyAttributes,
masterKey
);
await saveKeyInSessionStore(SESSION_KEYS.ENCRYPTION_KEY, masterKey);
setJustSignedUp(true);
setRecoveryModalView(true);
} catch (e) {
logError(e, 'failed to generate password');
setFieldError('passphrase', t('PASSWORD_GENERATION_FAILED'));
}
};
return (
<>
{loading ? (
<VerticallyCentered>
<EnteSpinner />
</VerticallyCentered>
) : recoverModalView ? (
<RecoveryKey
appContext={appContext}
show={recoverModalView}
onHide={() => {
setRecoveryModalView(false);
if (appName === APPS.AUTH) {
router.push(PAGES.AUTH);
} else {
router.push(PAGES.GALLERY);
}
}}
somethingWentWrong={() => null}
/>
) : (
<VerticallyCentered>
<FormPaper>
<FormTitle>{t('SET_PASSPHRASE')}</FormTitle>
<SetPasswordForm
userEmail={user?.email}
callback={onSubmit}
buttonText={t('SET_PASSPHRASE')}
/>
<FormPaperFooter>
<LinkButton onClick={logoutUser}>
{t('GO_BACK')}
</LinkButton>
</FormPaperFooter>
</FormPaper>
</VerticallyCentered>
)}
</>
);
}

View file

@ -19,7 +19,7 @@ import { t } from 'i18next';
import { Trans } from 'react-i18next';
import { Link } from '@mui/material';
import { SUPPORT_EMAIL } from '@ente/shared/constants/urls';
import { DialogBoxAttributesV2 } from '@ente/shared/components/DialogBoxV2';
import { DialogBoxAttributesV2 } from '@ente/shared/components/DialogBoxV2/types';
import { ApiError } from '@ente/shared/error';
import { HttpStatusCode } from 'axios';
import { PageProps } from '@ente/shared/apps/types';

View file

@ -1,8 +1,3 @@
import { generateLoginSubKey } from '@ente/shared/crypto/helpers';
import { KeyAttributes } from '@ente/shared/user/types';
import { generateSRPSetupAttributes } from '../services/srp';
import { SRPSetupAttributes } from '../types/srp';
import ComlinkCryptoWorker from '@ente/shared/crypto';
import { PasswordStrength } from '@ente/accounts/constants';
import zxcvbn from 'zxcvbn';
@ -14,64 +9,6 @@ export const convertBase64ToBuffer = (base64: string) => {
return Buffer.from(base64, 'base64');
};
export async function generateKeyAndSRPAttributes(passphrase: string): Promise<{
keyAttributes: KeyAttributes;
masterKey: string;
srpSetupAttributes: SRPSetupAttributes;
}> {
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const masterKey = await cryptoWorker.generateEncryptionKey();
const recoveryKey = await cryptoWorker.generateEncryptionKey();
const kekSalt = await cryptoWorker.generateSaltToDeriveKey();
const kek = await cryptoWorker.deriveSensitiveKey(passphrase, kekSalt);
const masterKeyEncryptedWithKek = await cryptoWorker.encryptToB64(
masterKey,
kek.key
);
const masterKeyEncryptedWithRecoveryKey = await cryptoWorker.encryptToB64(
masterKey,
recoveryKey
);
const recoveryKeyEncryptedWithMasterKey = await cryptoWorker.encryptToB64(
recoveryKey,
masterKey
);
const keyPair = await cryptoWorker.generateKeyPair();
const encryptedKeyPairAttributes = await cryptoWorker.encryptToB64(
keyPair.privateKey,
masterKey
);
const loginSubKey = await generateLoginSubKey(kek.key);
const srpSetupAttributes = await generateSRPSetupAttributes(loginSubKey);
const keyAttributes: KeyAttributes = {
kekSalt,
encryptedKey: masterKeyEncryptedWithKek.encryptedData,
keyDecryptionNonce: masterKeyEncryptedWithKek.nonce,
publicKey: keyPair.publicKey,
encryptedSecretKey: encryptedKeyPairAttributes.encryptedData,
secretKeyDecryptionNonce: encryptedKeyPairAttributes.nonce,
opsLimit: kek.opsLimit,
memLimit: kek.memLimit,
masterKeyEncryptedWithRecoveryKey:
masterKeyEncryptedWithRecoveryKey.encryptedData,
masterKeyDecryptionNonce: masterKeyEncryptedWithRecoveryKey.nonce,
recoveryKeyEncryptedWithMasterKey:
recoveryKeyEncryptedWithMasterKey.encryptedData,
recoveryKeyDecryptionNonce: recoveryKeyEncryptedWithMasterKey.nonce,
};
return {
keyAttributes,
masterKey,
srpSetupAttributes,
};
}
export function estimatePasswordStrength(password: string): PasswordStrength {
if (!password) {
return PasswordStrength.WEAK;

View file

@ -0,0 +1,63 @@
import { generateLoginSubKey } from '@ente/shared/crypto/helpers';
import { KeyAttributes } from '@ente/shared/user/types';
import { generateSRPSetupAttributes } from '../services/srp';
import { SRPSetupAttributes } from '../types/srp';
import ComlinkCryptoWorker from '@ente/shared/crypto';
export async function generateKeyAndSRPAttributes(passphrase: string): Promise<{
keyAttributes: KeyAttributes;
masterKey: string;
srpSetupAttributes: SRPSetupAttributes;
}> {
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const masterKey = await cryptoWorker.generateEncryptionKey();
const recoveryKey = await cryptoWorker.generateEncryptionKey();
const kekSalt = await cryptoWorker.generateSaltToDeriveKey();
const kek = await cryptoWorker.deriveSensitiveKey(passphrase, kekSalt);
const masterKeyEncryptedWithKek = await cryptoWorker.encryptToB64(
masterKey,
kek.key
);
const masterKeyEncryptedWithRecoveryKey = await cryptoWorker.encryptToB64(
masterKey,
recoveryKey
);
const recoveryKeyEncryptedWithMasterKey = await cryptoWorker.encryptToB64(
recoveryKey,
masterKey
);
const keyPair = await cryptoWorker.generateKeyPair();
const encryptedKeyPairAttributes = await cryptoWorker.encryptToB64(
keyPair.privateKey,
masterKey
);
const loginSubKey = await generateLoginSubKey(kek.key);
const srpSetupAttributes = await generateSRPSetupAttributes(loginSubKey);
const keyAttributes: KeyAttributes = {
kekSalt,
encryptedKey: masterKeyEncryptedWithKek.encryptedData,
keyDecryptionNonce: masterKeyEncryptedWithKek.nonce,
publicKey: keyPair.publicKey,
encryptedSecretKey: encryptedKeyPairAttributes.encryptedData,
secretKeyDecryptionNonce: encryptedKeyPairAttributes.nonce,
opsLimit: kek.opsLimit,
memLimit: kek.memLimit,
masterKeyEncryptedWithRecoveryKey:
masterKeyEncryptedWithRecoveryKey.encryptedData,
masterKeyDecryptionNonce: masterKeyEncryptedWithRecoveryKey.nonce,
recoveryKeyEncryptedWithMasterKey:
recoveryKeyEncryptedWithMasterKey.encryptedData,
recoveryKeyDecryptionNonce: recoveryKeyEncryptedWithMasterKey.nonce,
};
return {
keyAttributes,
masterKey,
srpSetupAttributes,
};
}

View file

@ -1,10 +1,11 @@
import { NextRouter } from 'next/router';
import { APPS } from './constants';
import { SetDialogBoxAttributesV2 } from '@ente/shared/components/DialogBoxV2';
import { SetDialogBoxAttributesV2 } from '@ente/shared/components/DialogBoxV2/types';
export interface PageProps {
appContext: {
showNavBar: (show: boolean) => void;
isMobile: boolean;
setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
};
router: NextRouter;

View file

@ -1,6 +1,6 @@
import { FreeFlowText } from '../Container';
import { FreeFlowText } from '@ente/shared/components/Container';
import React from 'react';
import EnteSpinner from '../EnteSpinner';
import EnteSpinner from '@ente/shared/components/EnteSpinner';
import { Wrapper, CodeWrapper, CopyButtonWrapper } from './styledComponents';
import CopyButton from './CopyButton';
import { BoxProps } from '@mui/material';

View file

@ -0,0 +1,18 @@
import { Box } from '@mui/material';
import React from 'react';
export default function DialogIcon({ icon }: { icon: React.ReactNode }) {
return (
<Box
className="DialogIcon"
sx={{
svg: {
width: '48px',
height: '48px',
},
color: 'stroke.muted',
}}>
{icon}
</Box>
);
}

View file

@ -0,0 +1,54 @@
import React from 'react';
import {
DialogProps,
DialogTitle,
IconButton,
Typography,
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import { SpaceBetweenFlex } from '@ente/shared/components/Container';
const DialogTitleWithCloseButton = (props) => {
const { children, onClose, ...other } = props;
return (
<DialogTitle {...other}>
<SpaceBetweenFlex>
<Typography variant="h3" fontWeight={'bold'}>
{children}
</Typography>
{onClose && (
<IconButton
aria-label="close"
onClick={onClose}
sx={{ float: 'right' }}
color="secondary">
<CloseIcon />
</IconButton>
)}
</SpaceBetweenFlex>
</DialogTitle>
);
};
export default DialogTitleWithCloseButton;
export const dialogCloseHandler =
({
staticBackdrop,
nonClosable,
onClose,
}: {
staticBackdrop?: boolean;
nonClosable?: boolean;
onClose: () => void;
}): DialogProps['onClose'] =>
(_, reason) => {
if (nonClosable) {
// no-op
} else if (staticBackdrop && reason === 'backdropClick') {
// no-op
} else {
onClose();
}
};

View file

@ -0,0 +1,40 @@
import { Dialog, styled } from '@mui/material';
const DialogBoxBase = styled(Dialog)(({ theme }) => ({
'& .MuiDialog-paper': {
padding: theme.spacing(1, 1.5),
maxWidth: '346px',
},
'& .DialogIcon': {
padding: theme.spacing(2),
paddingBottom: theme.spacing(1),
},
'& .MuiDialogTitle-root': {
padding: theme.spacing(2),
paddingBottom: theme.spacing(1),
},
'& .MuiDialogContent-root': {
padding: theme.spacing(2),
},
'.DialogIcon + .MuiDialogTitle-root': {
paddingTop: 0,
},
'.MuiDialogTitle-root + .MuiDialogContent-root': {
paddingTop: 0,
},
'.MuiDialogTitle-root + .MuiDialogActions-root': {
paddingTop: theme.spacing(3),
},
'& .MuiDialogActions-root': {
flexWrap: 'wrap-reverse',
},
'& .MuiButton-root': {
margin: `${theme.spacing(0.5, 0)} !important`,
},
}));
export default DialogBoxBase;

View file

@ -0,0 +1,116 @@
import React from 'react';
import {
Breakpoint,
Button,
DialogActions,
DialogContent,
DialogProps,
Typography,
} from '@mui/material';
import DialogTitleWithCloseButton, {
dialogCloseHandler,
} from './TitleWithCloseButton';
import DialogBoxBase from './base';
import { DialogBoxAttributes } from './types';
import DialogIcon from './DialogIcon';
import { t } from 'i18next';
type IProps = React.PropsWithChildren<
Omit<DialogProps, 'onClose' | 'maxSize'> & {
onClose: () => void;
attributes: DialogBoxAttributes;
size?: Breakpoint;
titleCloseButton?: boolean;
}
>;
export default function DialogBox({
attributes,
children,
open,
size,
onClose,
titleCloseButton,
...props
}: IProps) {
if (!attributes) {
return <></>;
}
const handleClose = dialogCloseHandler({
staticBackdrop: attributes.staticBackdrop,
nonClosable: attributes.nonClosable,
onClose: onClose,
});
return (
<DialogBoxBase
open={open}
maxWidth={size}
onClose={handleClose}
{...props}>
{attributes.icon && <DialogIcon icon={attributes.icon} />}
{attributes.title && (
<DialogTitleWithCloseButton
onClose={
titleCloseButton &&
!attributes.nonClosable &&
handleClose
}>
{attributes.title}
</DialogTitleWithCloseButton>
)}
{(children || attributes?.content) && (
<DialogContent>
{children || (
<Typography color="text.muted">
{attributes.content}
</Typography>
)}
</DialogContent>
)}
{(attributes.close || attributes.proceed) && (
<DialogActions>
<>
{attributes.close && (
<Button
size="large"
color={attributes.close?.variant ?? 'secondary'}
onClick={() => {
attributes.close.action &&
attributes.close?.action();
onClose();
}}>
{attributes.close?.text ?? t('OK')}
</Button>
)}
{attributes.proceed && (
<Button
size="large"
color={attributes.proceed?.variant}
onClick={() => {
attributes.proceed.action();
onClose();
}}
disabled={attributes.proceed.disabled}>
{attributes.proceed.text}
</Button>
)}
{attributes.secondary && (
<Button
size="large"
color={attributes.secondary?.variant}
onClick={() => {
attributes.secondary.action();
onClose();
}}
disabled={attributes.secondary.disabled}>
{attributes.secondary.text}
</Button>
)}
</>
</DialogActions>
)}
</DialogBoxBase>
);
}

View file

@ -0,0 +1,30 @@
import { ButtonProps } from '@mui/material';
export interface DialogBoxAttributes {
icon?: React.ReactNode;
title?: string;
staticBackdrop?: boolean;
nonClosable?: boolean;
content?: any;
close?: {
text?: string;
variant?: ButtonProps['color'];
action?: () => void;
};
proceed?: {
text: string;
action: () => void;
variant?: ButtonProps['color'];
disabled?: boolean;
};
secondary?: {
text: string;
action: () => void;
variant: ButtonProps['color'];
disabled?: boolean;
};
}
export type SetDialogBoxAttributes = React.Dispatch<
React.SetStateAction<DialogBoxAttributes>
>;

View file

@ -0,0 +1,135 @@
import React, { useState } from 'react';
import {
Box,
Button,
Dialog,
DialogProps,
Stack,
Typography,
} from '@mui/material';
import { t } from 'i18next';
import { dialogCloseHandler } from '@ente/shared/components/DialogBox/TitleWithCloseButton';
import { DialogBoxAttributesV2 } from './types';
import EnteButton from '@ente/shared/components/EnteButton';
type IProps = React.PropsWithChildren<
Omit<DialogProps, 'onClose'> & {
onClose: () => void;
attributes: DialogBoxAttributesV2;
}
>;
export default function DialogBoxV2({
attributes,
children,
open,
onClose,
...props
}: IProps) {
const [loading, setLoading] = useState(false);
if (!attributes) {
return <></>;
}
const handleClose = dialogCloseHandler({
staticBackdrop: attributes.staticBackdrop,
nonClosable: attributes.nonClosable,
onClose: onClose,
});
const { PaperProps, ...rest } = props;
return (
<Dialog
open={open}
onClose={handleClose}
PaperProps={{
...PaperProps,
sx: {
padding: '8px 12px',
maxWidth: '360px',
...PaperProps?.sx,
},
}}
{...rest}>
<Stack spacing={'36px'} p={'16px'}>
<Stack spacing={'19px'}>
{attributes.icon && (
<Box
sx={{
'& > svg': {
fontSize: '32px',
},
}}>
{attributes.icon}
</Box>
)}
{attributes.title && (
<Typography variant="large" fontWeight={'bold'}>
{attributes.title}
</Typography>
)}
{children ||
(attributes?.content && (
<Typography color="text.muted">
{attributes.content}
</Typography>
))}
</Stack>
{(attributes.proceed ||
attributes.close ||
attributes.buttons?.length) && (
<Stack
spacing={'8px'}
direction={
attributes.buttonDirection === 'row'
? 'row-reverse'
: 'column'
}
flex={1}>
{attributes.proceed && (
<EnteButton
loading={loading}
size="large"
color={attributes.proceed?.variant}
onClick={async () => {
await attributes.proceed.action(setLoading);
onClose();
}}
disabled={attributes.proceed.disabled}>
{attributes.proceed.text}
</EnteButton>
)}
{attributes.close && (
<Button
size="large"
color={attributes.close?.variant ?? 'secondary'}
onClick={() => {
attributes.close.action &&
attributes.close?.action();
onClose();
}}>
{attributes.close?.text ?? t('OK')}
</Button>
)}
{attributes.buttons &&
attributes.buttons.map((b) => (
<Button
size="large"
key={b.text}
color={b.variant}
onClick={() => {
b.action();
onClose();
}}
disabled={b.disabled}>
{b.text}
</Button>
))}
</Stack>
)}
</Stack>
</Dialog>
);
}

View file

@ -0,0 +1,47 @@
import Done from '@mui/icons-material/Done';
import {
Button,
ButtonProps,
CircularProgress,
PaletteColor,
} from '@mui/material';
interface Iprops extends ButtonProps {
loading?: boolean;
success?: boolean;
}
export default function EnteButton({
children,
loading,
success,
disabled,
sx,
...props
}: Iprops) {
return (
<Button
disabled={disabled}
sx={{
...sx,
...((loading || success) && {
'&.Mui-disabled': (theme) => ({
backgroundColor: (
theme.palette[props.color] as PaletteColor
).main,
color: (theme.palette[props.color] as PaletteColor)
.contrastText,
}),
}),
}}
{...props}>
{loading ? (
<CircularProgress size={20} sx={{ color: 'inherit' }} />
) : success ? (
<Done sx={{ fontSize: 20 }} />
) : (
children
)}
</Button>
);
}

View file

@ -1,6 +1,6 @@
import { FC } from 'react';
import { BoxProps, Divider } from '@mui/material';
import { VerticallyCentered } from '../../Container';
import { VerticallyCentered } from '@ente/shared/components/Container';
const FormPaperFooter: FC<BoxProps> = ({ sx, style, ...props }) => {
return (

View file

@ -3,8 +3,8 @@ import { Formik, FormikHelpers, FormikState } from 'formik';
import * as Yup from 'yup';
import SubmitButton from './SubmitButton';
import TextField from '@mui/material/TextField';
import ShowHidePassword from './Form/ShowHidePassword';
import { FlexWrapper } from './Container';
import ShowHidePassword from '@ente/shared/components/Form/ShowHidePassword';
import { FlexWrapper } from '@ente/shared/components/Container';
import { Button, FormHelperText } from '@mui/material';
import { t } from 'i18next';

View file

@ -3,3 +3,22 @@ export async function sleep(time: number) {
setTimeout(() => resolve(null), time);
});
}
export function downloadAsFile(filename: string, content: string) {
const file = new Blob([content], {
type: 'text/plain',
});
const fileURL = URL.createObjectURL(file);
downloadUsingAnchor(fileURL, filename);
}
export function downloadUsingAnchor(link: string, name: string) {
const a = document.createElement('a');
a.style.display = 'none';
a.href = link;
a.download = name;
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(link);
a.remove();
}