ソースを参照

refactored generate page

Abhinav 1 年間 前
コミット
9eb793d606

+ 9 - 122
apps/photos/src/pages/generate/index.tsx

@@ -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}
+        />
     );
 }

+ 6 - 0
packages/accounts/api/user.ts

@@ -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(),
+    });
+};

+ 83 - 0
packages/accounts/components/RecoveryKey/index.tsx

@@ -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;

+ 6 - 0
packages/accounts/components/RecoveryKey/styledComponents.tsx

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

+ 2 - 4
packages/accounts/components/SignUp.tsx

@@ -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';

+ 130 - 0
packages/accounts/pages/generate.tsx

@@ -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>
+            )}
+        </>
+    );
+}

+ 1 - 1
packages/accounts/pages/two-factor/recover.tsx

@@ -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';

+ 0 - 63
packages/accounts/utils/index.ts

@@ -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;

+ 63 - 0
packages/accounts/utils/srp.ts

@@ -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,
+    };
+}

+ 2 - 1
packages/shared/apps/types.ts

@@ -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;

+ 2 - 2
packages/shared/components/CodeBlock/index.tsx

@@ -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';

+ 18 - 0
packages/shared/components/DialogBox/DialogIcon.tsx

@@ -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>
+    );
+}

+ 54 - 0
packages/shared/components/DialogBox/TitleWithCloseButton.tsx

@@ -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();
+        }
+    };

+ 40 - 0
packages/shared/components/DialogBox/base.tsx

@@ -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;

+ 116 - 0
packages/shared/components/DialogBox/index.tsx

@@ -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>
+    );
+}

+ 30 - 0
packages/shared/components/DialogBox/types.ts

@@ -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>
+>;

+ 135 - 0
packages/shared/components/DialogBoxV2/index.tsx

@@ -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>
+    );
+}

+ 0 - 0
packages/shared/components/DialogBoxV2.tsx → packages/shared/components/DialogBoxV2/types.ts


+ 47 - 0
packages/shared/components/EnteButton.tsx

@@ -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>
+    );
+}

+ 1 - 1
packages/shared/components/Form/FormPaper/Footer.tsx

@@ -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 (

+ 2 - 2
packages/shared/components/SingleInputForm.tsx

@@ -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';
 

+ 19 - 0
packages/shared/utils/index.ts

@@ -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();
+}