diff --git a/apps/photos/src/pages/generate/index.tsx b/apps/photos/src/pages/generate/index.tsx index 165b1c559..e18500a7a 100644 --- a/apps/photos/src/pages/generate/index.tsx +++ b/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(); - const [user, setUser] = useState(); - 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 ? ( - - - - ) : recoverModalView ? ( - { - setRecoveryModalView(false); - const appName = getAppName(); - if (appName === APPS.AUTH) { - router.push(PAGES.AUTH); - } else { - router.push(PAGES.GALLERY); - } - }} - somethingWentWrong={() => null} - /> - ) : ( - - - {t('SET_PASSPHRASE')} - - - - {t('GO_BACK')} - - - - - )} - + ); } diff --git a/packages/accounts/api/user.ts b/packages/accounts/api/user.ts index 33c28ba86..61fc2ddb8 100644 --- a/packages/accounts/api/user.ts +++ b/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(), + }); +}; diff --git a/packages/accounts/components/RecoveryKey/index.tsx b/packages/accounts/components/RecoveryKey/index.tsx new file mode 100644 index 000000000..ab8073d04 --- /dev/null +++ b/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 ( + + + {t('RECOVERY_KEY')} + + + {t('RECOVERY_KEY_DESCRIPTION')} + + + + {t('KEY_NOT_STORED_DISCLAIMER')} + + + + + + + + + ); +} +export default RecoveryKey; diff --git a/packages/accounts/components/RecoveryKey/styledComponents.tsx b/packages/accounts/components/RecoveryKey/styledComponents.tsx new file mode 100644 index 000000000..291965edd --- /dev/null +++ b/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), +})); diff --git a/packages/accounts/components/SignUp.tsx b/packages/accounts/components/SignUp.tsx index a15db078d..7705d4e84 100644 --- a/packages/accounts/components/SignUp.tsx +++ b/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'; diff --git a/packages/accounts/pages/generate.tsx b/packages/accounts/pages/generate.tsx new file mode 100644 index 000000000..1f885d337 --- /dev/null +++ b/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(); + 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 ? ( + + + + ) : recoverModalView ? ( + { + setRecoveryModalView(false); + if (appName === APPS.AUTH) { + router.push(PAGES.AUTH); + } else { + router.push(PAGES.GALLERY); + } + }} + somethingWentWrong={() => null} + /> + ) : ( + + + {t('SET_PASSPHRASE')} + + + + {t('GO_BACK')} + + + + + )} + + ); +} diff --git a/packages/accounts/pages/two-factor/recover.tsx b/packages/accounts/pages/two-factor/recover.tsx index a80d40ddc..2682733b7 100644 --- a/packages/accounts/pages/two-factor/recover.tsx +++ b/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'; diff --git a/packages/accounts/utils/index.ts b/packages/accounts/utils/index.ts index 57ddf84b0..4c2bb2545 100644 --- a/packages/accounts/utils/index.ts +++ b/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; diff --git a/packages/accounts/utils/srp.ts b/packages/accounts/utils/srp.ts new file mode 100644 index 000000000..195b7d769 --- /dev/null +++ b/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, + }; +} diff --git a/packages/shared/apps/types.ts b/packages/shared/apps/types.ts index a02926e71..ea2084944 100644 --- a/packages/shared/apps/types.ts +++ b/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; diff --git a/packages/shared/components/CodeBlock/index.tsx b/packages/shared/components/CodeBlock/index.tsx index a05c913b2..fc8c5b68b 100644 --- a/packages/shared/components/CodeBlock/index.tsx +++ b/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'; diff --git a/packages/shared/components/DialogBox/DialogIcon.tsx b/packages/shared/components/DialogBox/DialogIcon.tsx new file mode 100644 index 000000000..1a9c275e6 --- /dev/null +++ b/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 ( + + {icon} + + ); +} diff --git a/packages/shared/components/DialogBox/TitleWithCloseButton.tsx b/packages/shared/components/DialogBox/TitleWithCloseButton.tsx new file mode 100644 index 000000000..c3457ebc6 --- /dev/null +++ b/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 ( + + + + {children} + + {onClose && ( + + + + )} + + + ); +}; + +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(); + } + }; diff --git a/packages/shared/components/DialogBox/base.tsx b/packages/shared/components/DialogBox/base.tsx new file mode 100644 index 000000000..159dad961 --- /dev/null +++ b/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; diff --git a/packages/shared/components/DialogBox/index.tsx b/packages/shared/components/DialogBox/index.tsx new file mode 100644 index 000000000..23a04992c --- /dev/null +++ b/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 & { + 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 ( + + {attributes.icon && } + {attributes.title && ( + + {attributes.title} + + )} + {(children || attributes?.content) && ( + + {children || ( + + {attributes.content} + + )} + + )} + {(attributes.close || attributes.proceed) && ( + + <> + {attributes.close && ( + + )} + {attributes.proceed && ( + + )} + {attributes.secondary && ( + + )} + + + )} + + ); +} diff --git a/packages/shared/components/DialogBox/types.ts b/packages/shared/components/DialogBox/types.ts new file mode 100644 index 000000000..363757420 --- /dev/null +++ b/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 +>; diff --git a/packages/shared/components/DialogBoxV2/index.tsx b/packages/shared/components/DialogBoxV2/index.tsx new file mode 100644 index 000000000..18bcea87d --- /dev/null +++ b/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 & { + 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 ( + + + + {attributes.icon && ( + svg': { + fontSize: '32px', + }, + }}> + {attributes.icon} + + )} + {attributes.title && ( + + {attributes.title} + + )} + {children || + (attributes?.content && ( + + {attributes.content} + + ))} + + {(attributes.proceed || + attributes.close || + attributes.buttons?.length) && ( + + {attributes.proceed && ( + { + await attributes.proceed.action(setLoading); + + onClose(); + }} + disabled={attributes.proceed.disabled}> + {attributes.proceed.text} + + )} + {attributes.close && ( + + )} + {attributes.buttons && + attributes.buttons.map((b) => ( + + ))} + + )} + + + ); +} diff --git a/packages/shared/components/DialogBoxV2.tsx b/packages/shared/components/DialogBoxV2/types.ts similarity index 100% rename from packages/shared/components/DialogBoxV2.tsx rename to packages/shared/components/DialogBoxV2/types.ts diff --git a/packages/shared/components/EnteButton.tsx b/packages/shared/components/EnteButton.tsx new file mode 100644 index 000000000..b684198fc --- /dev/null +++ b/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 ( + + ); +} diff --git a/packages/shared/components/Form/FormPaper/Footer.tsx b/packages/shared/components/Form/FormPaper/Footer.tsx index 368acdd48..90226b99b 100644 --- a/packages/shared/components/Form/FormPaper/Footer.tsx +++ b/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 = ({ sx, style, ...props }) => { return ( diff --git a/packages/shared/components/SingleInputForm.tsx b/packages/shared/components/SingleInputForm.tsx index 03be563c6..88a2b2dc3 100644 --- a/packages/shared/components/SingleInputForm.tsx +++ b/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'; diff --git a/packages/shared/utils/index.ts b/packages/shared/utils/index.ts index 39e64cb5f..f83937702 100644 --- a/packages/shared/utils/index.ts +++ b/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(); +}