From 196a396f9a7ea5764eaddc7a7beed844a66636e6 Mon Sep 17 00:00:00 2001 From: Wukong Sun Date: Mon, 24 Mar 2025 20:27:43 +0800 Subject: [PATCH 1/9] feat: mf-6641 integrate google drive --- .gitignore | 1 + packages/icons/brands/GoogleDrive.svg | 14 ++ packages/icons/icon-generated-as-jsx.js | 5 + packages/icons/icon-generated-as-url.js | 1 + packages/icons/plugins/GoPlus.svg | 4 +- packages/icons/plugins/ScamSniffer.dark.svg | 20 ++- packages/icons/plugins/ScamSniffer.light.svg | 20 ++- packages/mask/.webpack/config.ts | 2 +- .../tasks/NotCancellable/OnInstall.ts | 2 + .../CompositionDialog/Composition.tsx | 20 +-- .../components/BackupPreview/index.tsx | 16 +- .../dashboard/components/SetupFrame/index.tsx | 6 +- .../contexts/CloudBackupFormContext.tsx | 8 +- .../dashboard/hooks/useBackupFormState.ts | 8 +- .../dashboard/hooks/useBackupPreviewInfo.ts | 11 ++ .../BackupPreviewDialog.tsx | 36 ++--- .../Backup/CloudBackup/EmailForm.tsx | 109 ++++++++++++++ .../Backup/CloudBackup/GoogleDriveBackup.tsx | 39 +++++ .../Backup/CloudBackup/MaskNetworkBackup.tsx | 139 ++++++++++++++++++ .../Backup/CloudBackup/PhoneForm.tsx | 106 +++++++++++++ .../SetupPersona/Backup/CloudBackup/index.tsx | 84 +++++++++++ .../Backup/CloudBackup/useCloudBackupForm.ts | 62 ++++++++ .../Backup/CloudBackupFormContext.tsx | 73 +++++++++ .../pages/SetupPersona/Backup/LocalBackup.tsx | 131 +++++++++++++++++ .../pages/SetupPersona/Backup/index.tsx | 107 ++++++++++++++ .../pages/SetupPersona/Backup/types.ts | 3 + .../pages/SetupPersona/LocalBackup/index.tsx | 28 ++-- .../dashboard/pages/SetupPersona/index.tsx | 1 + .../mask/shared-ui/initialization/storage.ts | 2 + packages/shared-base/src/KVStorage/index.ts | 2 + packages/shared-base/src/types/Routes.ts | 1 + .../shared/src/UI/components/Alert/index.tsx | 23 ++- .../UI/components/CountryCodePicker/index.tsx | 6 +- .../UI/components/PhoneNumberField/index.tsx | 13 +- .../web3-shared/evm/src/libs/EtherscanURL.ts | 2 +- 35 files changed, 1010 insertions(+), 95 deletions(-) create mode 100644 packages/icons/brands/GoogleDrive.svg create mode 100644 packages/mask/dashboard/hooks/useBackupPreviewInfo.ts create mode 100644 packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/EmailForm.tsx create mode 100644 packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/GoogleDriveBackup.tsx create mode 100644 packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/MaskNetworkBackup.tsx create mode 100644 packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/PhoneForm.tsx create mode 100644 packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/index.tsx create mode 100644 packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/useCloudBackupForm.ts create mode 100644 packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackupFormContext.tsx create mode 100644 packages/mask/dashboard/pages/SetupPersona/Backup/LocalBackup.tsx create mode 100644 packages/mask/dashboard/pages/SetupPersona/Backup/index.tsx create mode 100644 packages/mask/dashboard/pages/SetupPersona/Backup/types.ts diff --git a/.gitignore b/.gitignore index 7c68bc4670a5..1429728761ce 100644 --- a/.gitignore +++ b/.gitignore @@ -122,3 +122,4 @@ common/autoinstallers/*/.npmrc /package-lock.json /yarn.lock quick-lint-js.config +.aider* diff --git a/packages/icons/brands/GoogleDrive.svg b/packages/icons/brands/GoogleDrive.svg new file mode 100644 index 000000000000..bef62df4b6ce --- /dev/null +++ b/packages/icons/brands/GoogleDrive.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/packages/icons/icon-generated-as-jsx.js b/packages/icons/icon-generated-as-jsx.js index 2045693a7a8b..e410ca933542 100644 --- a/packages/icons/icon-generated-as-jsx.js +++ b/packages/icons/icon-generated-as-jsx.js @@ -239,6 +239,11 @@ export const Gnosis = /*#__PURE__*/ __createIcon('Gnosis', [ u: () => new URL('./brands/Gnosis.svg', import.meta.url).href, }, ]) +export const GoogleDrive = /*#__PURE__*/ __createIcon('GoogleDrive', [ + { + u: () => new URL('./brands/GoogleDrive.svg', import.meta.url).href, + }, +]) export const Highlight = /*#__PURE__*/ __createIcon('Highlight', [ { u: () => new URL('./brands/Highlight.svg', import.meta.url).href, diff --git a/packages/icons/icon-generated-as-url.js b/packages/icons/icon-generated-as-url.js index a8498ee101db..e6553edb637a 100644 --- a/packages/icons/icon-generated-as-url.js +++ b/packages/icons/icon-generated-as-url.js @@ -39,6 +39,7 @@ export function git_hub_url() { return new URL("./brands/GitHub.svg", import.met export function github_dark_url() { return new URL("./brands/GithubDark.svg", import.meta.url).href } export function git_hub_gray_url() { return new URL("./brands/GitHubGray.svg", import.meta.url).href } export function gnosis_url() { return new URL("./brands/Gnosis.svg", import.meta.url).href } +export function google_drive_url() { return new URL("./brands/GoogleDrive.svg", import.meta.url).href } export function highlight_url() { return new URL("./brands/Highlight.svg", import.meta.url).href } export function instagram_url() { return new URL("./brands/Instagram.svg", import.meta.url).href } export function instagram_round_colored_url() { return new URL("./brands/InstagramRoundColored.svg", import.meta.url).href } diff --git a/packages/icons/plugins/GoPlus.svg b/packages/icons/plugins/GoPlus.svg index de636d7e89f9..770ce2c875e8 100644 --- a/packages/icons/plugins/GoPlus.svg +++ b/packages/icons/plugins/GoPlus.svg @@ -1,7 +1,7 @@ - + - + diff --git a/packages/icons/plugins/ScamSniffer.dark.svg b/packages/icons/plugins/ScamSniffer.dark.svg index fdca736c3267..ed1a2b941ff6 100644 --- a/packages/icons/plugins/ScamSniffer.dark.svg +++ b/packages/icons/plugins/ScamSniffer.dark.svg @@ -1,17 +1,15 @@ - - - - - - - - - + + + + + + + - - + + diff --git a/packages/icons/plugins/ScamSniffer.light.svg b/packages/icons/plugins/ScamSniffer.light.svg index 0664681e4498..6442efd8fb57 100644 --- a/packages/icons/plugins/ScamSniffer.light.svg +++ b/packages/icons/plugins/ScamSniffer.light.svg @@ -1,17 +1,15 @@ - - - - - - - - - + + + + + + + - - + + diff --git a/packages/mask/.webpack/config.ts b/packages/mask/.webpack/config.ts index 80ac17d8617c..12a01e089f0a 100644 --- a/packages/mask/.webpack/config.ts +++ b/packages/mask/.webpack/config.ts @@ -238,6 +238,7 @@ export async function createConfiguration( NEXT_PUBLIC_FIREFLY_API_URL: process.env.NEXT_PUBLIC_FIREFLY_API_URL || '', SOLANA_DEFAULT_RPC_URL: process.env.SOLANA_DEFAULT_RPC_URL || '', MASK_ENABLE_EXCHANGE: process.env.MASK_ENABLE_EXCHANGE || '', + GOOGLE_CLIENT_ID: JSON.stringify(process.env.GOOGLE_CLIENT_ID || ''), }), new (rspack?.DefinePlugin || webpack.default.DefinePlugin)({ 'process.browser': 'true', @@ -312,7 +313,6 @@ export async function createConfiguration( // Focus on performance optimization. Not for download size/cache stability optimization. optimization: { // we don't need deterministic, and we also don't have chunk request at init we don't need "size" - // @ts-expect-error chunkIds: productionLike ? rspack ? 'deterministic' diff --git a/packages/mask/background/tasks/NotCancellable/OnInstall.ts b/packages/mask/background/tasks/NotCancellable/OnInstall.ts index 290f8c7ca1b2..3eacb2caa727 100644 --- a/packages/mask/background/tasks/NotCancellable/OnInstall.ts +++ b/packages/mask/background/tasks/NotCancellable/OnInstall.ts @@ -28,6 +28,7 @@ browser.runtime.onInstalled.addListener(async (detail) => { const localStorage = (globalThis as any).localStorage if (localStorage) { const backupPassword = localStorage.getItem('backupPassword') + // Migration if (backupPassword) { const backupMethod = localStorage.getItem('backupMethod') base.PersistentStorages.Settings.storage.backupConfig.setValue({ @@ -37,6 +38,7 @@ browser.runtime.onInstalled.addListener(async (detail) => { cloudBackupAt: backupMethod && backupMethod === 'cloud' ? localStorage.getItem('backupAt') : null, localBackupAt: backupMethod && backupMethod === 'local' ? localStorage.getItem('backupAt') : null, cloudBackupMethod: null, + googleToken: null, }) } // remove old data after migrate diff --git a/packages/mask/content-script/components/CompositionDialog/Composition.tsx b/packages/mask/content-script/components/CompositionDialog/Composition.tsx index 81eeacc2ed86..a66d5a4d66e5 100644 --- a/packages/mask/content-script/components/CompositionDialog/Composition.tsx +++ b/packages/mask/content-script/components/CompositionDialog/Composition.tsx @@ -1,8 +1,7 @@ -import { useCallback, useEffect, useRef, useState } from 'react' -import { useAsync } from 'react-use' -import { DialogContent, alpha } from '@mui/material' -import { makeStyles } from '@masknet/theme' -import { useCurrentPersonaConnectStatus, InjectedDialog, PersonaAction } from '@masknet/shared' +import Services from '#services' +import { Trans } from '@lingui/react/macro' +import type { CompositionType } from '@masknet/plugin-infra/content-script' +import { InjectedDialog, PersonaAction, useCurrentPersonaConnectStatus } from '@masknet/shared' import { CrossIsolationMessages, EMPTY_OBJECT, @@ -12,19 +11,20 @@ import { currentPersonaIdentifier, } from '@masknet/shared-base' import { useValueRef } from '@masknet/shared-base-ui' +import { makeStyles } from '@masknet/theme' import { Telemetry } from '@masknet/web3-telemetry' import { EventID, EventType } from '@masknet/web3-telemetry/types' -import type { CompositionType } from '@masknet/plugin-infra/content-script' -import Services from '#services' +import { DialogContent, alpha } from '@mui/material' +import { useCallback, useEffect, useRef, useState } from 'react' +import { useAsync } from 'react-use' +import { useCurrentPersona, usePersonasFromDB } from '../../../shared-ui/hooks/index.js' import { activatedSiteAdaptorUI } from '../../site-adaptor-infra/index.js' import { useCurrentIdentity, useLastRecognizedIdentity } from '../DataSource/useActivatedUI.js' import { CompositionDialogUI, type CompositionRef, E2EUnavailableReason } from './CompositionUI.js' +import { EncryptionMethodType } from './EncryptionMethodSelector.js' import { useCompositionClipboardRequest } from './useCompositionClipboardRequest.js' import { useRecipientsList } from './useRecipientsList.js' import { useSubmit } from './useSubmit.js' -import { usePersonasFromDB, useCurrentPersona } from '../../../shared-ui/hooks/index.js' -import { EncryptionMethodType } from './EncryptionMethodSelector.js' -import { Trans } from '@lingui/react/macro' const useStyles = makeStyles()((theme) => ({ dialogRoot: { diff --git a/packages/mask/dashboard/components/BackupPreview/index.tsx b/packages/mask/dashboard/components/BackupPreview/index.tsx index 7a9db487d147..d07a33d04b9d 100644 --- a/packages/mask/dashboard/components/BackupPreview/index.tsx +++ b/packages/mask/dashboard/components/BackupPreview/index.tsx @@ -83,18 +83,25 @@ const useStyles = makeStyles()((theme) => ({ list: { padding: 0, }, + grid: { + display: 'grid', + gridTemplateColumns: 'repeat(2, 1fr)', + }, walletHeaderIcon: { backgroundColor: '#1C68F3', boxShadow: '0px 6px 12px rgba(28, 104, 243, 0.2)', }, wallets: { margin: 0, - display: 'grid', - gridTemplateColumns: 'repeat(3, 1fr)', + display: 'flex', + flexWrap: 'wrap', + gap: 8, }, wallet: { + gap: 10, width: 'auto', flexWrap: 'nowrap', + paddingRight: 0, }, listItemIcon: { marginRight: theme.spacing(1), @@ -103,12 +110,12 @@ const useStyles = makeStyles()((theme) => ({ minWidth: 'unset', }, walletIcon: { - marginRight: theme.spacing(1), color: theme.palette.maskColor.second, minWidth: 'unset', }, listText: { fontSize: 14, + color: theme.palette.maskColor.main, }, link: { color: theme.palette.maskColor.main, @@ -167,7 +174,7 @@ export const PersonasBackupPreview = memo(function P } /> - + {info.accounts}}> @@ -253,6 +260,7 @@ export const WalletsBackupPreview = memo(function Wal pluginID={NetworkPluginID.PLUGIN_EVM} address={wallet} mr="10px" + fontWeight={400} /> diff --git a/packages/mask/dashboard/components/SetupFrame/index.tsx b/packages/mask/dashboard/components/SetupFrame/index.tsx index b7e4cd3cc5da..4077a1e605c6 100644 --- a/packages/mask/dashboard/components/SetupFrame/index.tsx +++ b/packages/mask/dashboard/components/SetupFrame/index.tsx @@ -1,9 +1,9 @@ -import { lazy, memo, Suspense, useState, type PropsWithChildren } from 'react' -import { Box, Typography } from '@mui/material' import { Icons } from '@masknet/icons' -import { Welcome } from '../../assets/index.js' import { LoadingBase, makeStyles } from '@masknet/theme' +import { Box, Typography } from '@mui/material' +import { lazy, memo, Suspense, useState, type PropsWithChildren } from 'react' import { Outlet } from 'react-router-dom' +import { Welcome } from '../../assets/index.js' const Spline = lazy(() => import('./spline.js')) interface SetupFrameProps { diff --git a/packages/mask/dashboard/contexts/CloudBackupFormContext.tsx b/packages/mask/dashboard/contexts/CloudBackupFormContext.tsx index 6b7c686ab5b1..5841bafcbe9c 100644 --- a/packages/mask/dashboard/contexts/CloudBackupFormContext.tsx +++ b/packages/mask/dashboard/contexts/CloudBackupFormContext.tsx @@ -1,11 +1,11 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { useLingui } from '@lingui/react/macro' import { createContainer } from '@masknet/shared-base-ui' +import { useTabs } from '@masknet/theme' +import guessCallingCode from 'guess-calling-code' import { useForm } from 'react-hook-form' -import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' -import { useTabs } from '@masknet/theme' import { emailRegexp, phoneRegexp } from '../utils/regexp.js' -import guessCallingCode from 'guess-calling-code' -import { useLingui } from '@lingui/react/macro' export interface CloudBackupFormInputs { email: string diff --git a/packages/mask/dashboard/hooks/useBackupFormState.ts b/packages/mask/dashboard/hooks/useBackupFormState.ts index f5e08a412ccb..6b9e5aabc336 100644 --- a/packages/mask/dashboard/hooks/useBackupFormState.ts +++ b/packages/mask/dashboard/hooks/useBackupFormState.ts @@ -1,12 +1,12 @@ +import Services from '#services' import { zodResolver } from '@hookform/resolvers/zod' +import { useLingui } from '@lingui/react/macro' import { useState } from 'react' import { useForm } from 'react-hook-form' import { useAsync } from 'react-use' import { z } from 'zod' import { UserContext } from '../../shared-ui/index.js' -import Services from '#services' import { passwordRegexp } from '../utils/regexp.js' -import { useLingui } from '@lingui/react/macro' export type BackupFormInputs = { backupPassword: string @@ -16,7 +16,6 @@ export type BackupFormInputs = { export function useBackupFormState() { const { t } = useLingui() const { value: hasPassword } = useAsync(Services.Wallet.hasPassword, []) - const { value: previewInfo, loading } = useAsync(Services.Backup.generateBackupPreviewInfo, []) const { user } = UserContext.useContainer() const [backupWallets, setBackupWallets] = useState(false) @@ -24,7 +23,6 @@ export function useBackupFormState() { mode: 'onBlur', context: { user, - backupWallets, hasPassword, }, @@ -55,8 +53,6 @@ export function useBackupFormState() { return { hasPassword, - previewInfo, - loading, backupWallets, setBackupWallets, formState, diff --git a/packages/mask/dashboard/hooks/useBackupPreviewInfo.ts b/packages/mask/dashboard/hooks/useBackupPreviewInfo.ts new file mode 100644 index 000000000000..524ad737c5c3 --- /dev/null +++ b/packages/mask/dashboard/hooks/useBackupPreviewInfo.ts @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query' +import Services from '#services' + +export function useBackupPreviewInfo() { + return useQuery({ + queryKey: ['backup', 'preview-info'], + queryFn: () => Services.Backup.generateBackupPreviewInfo(), + refetchInterval: 60_0000, + refetchOnWindowFocus: true, + }) +} diff --git a/packages/mask/dashboard/modals/BackupPreviewModal/BackupPreviewDialog.tsx b/packages/mask/dashboard/modals/BackupPreviewModal/BackupPreviewDialog.tsx index 239c540a82da..eefefc29012a 100644 --- a/packages/mask/dashboard/modals/BackupPreviewModal/BackupPreviewDialog.tsx +++ b/packages/mask/dashboard/modals/BackupPreviewModal/BackupPreviewDialog.tsx @@ -1,25 +1,26 @@ -import { InjectedDialog, LoadingStatus } from '@masknet/shared' -import { memo, useCallback, useMemo, useRef } from 'react' -import { Box, DialogActions, DialogContent, Typography } from '@mui/material' -import { useBackupFormState, type BackupFormInputs } from '../../hooks/useBackupFormState.js' -import { ActionButton, makeStyles, useCustomSnackbar } from '@masknet/theme' -import { Icons } from '@masknet/icons' -import { useAsyncFn, useUpdateEffect } from 'react-use' import Services from '#services' -import type { BackupAccountType } from '@masknet/shared-base' -import { fetchDownloadLink, fetchUploadLink, uploadBackupValue } from '../../utils/api.js' +import { msg } from '@lingui/core/macro' +import { useLingui } from '@lingui/react' +import { Trans } from '@lingui/react/macro' import { encryptBackup } from '@masknet/backup-format' +import { Icons } from '@masknet/icons' +import { InjectedDialog, LoadingStatus } from '@masknet/shared' +import type { BackupAccountType } from '@masknet/shared-base' +import { DashboardRoutes } from '@masknet/shared-base' +import { ActionButton, makeStyles, useCustomSnackbar } from '@masknet/theme' import { encode } from '@msgpack/msgpack' +import { Box, DialogActions, DialogContent, Typography } from '@mui/material' +import { format as formatDateTime } from 'date-fns' +import { memo, useCallback, useMemo, useRef } from 'react' import { Controller } from 'react-hook-form' -import { PersonasBackupPreview, WalletsBackupPreview } from '../../components/BackupPreview/index.js' -import PasswordField from '../../components/PasswordField/index.js' import { useNavigate, useSearchParams } from 'react-router-dom' -import { DashboardRoutes } from '@masknet/shared-base' -import { format as formatDateTime } from 'date-fns' +import { useAsyncFn, useUpdateEffect } from 'react-use' import { UserContext } from '../../../shared-ui/index.js' -import { msg } from '@lingui/core/macro' -import { Trans } from '@lingui/react/macro' -import { useLingui } from '@lingui/react' +import { PersonasBackupPreview, WalletsBackupPreview } from '../../components/BackupPreview/index.js' +import PasswordField from '../../components/PasswordField/index.js' +import { useBackupFormState, type BackupFormInputs } from '../../hooks/useBackupFormState.js' +import { useBackupPreviewInfo } from '../../hooks/useBackupPreviewInfo.js' +import { fetchDownloadLink, fetchUploadLink, uploadBackupValue } from '../../utils/api.js' const useStyles = makeStyles()((theme) => ({ container: { @@ -83,8 +84,6 @@ export const BackupPreviewDialog = memo(function Backu const { updateUser } = UserContext.useContainer() const { hasPassword, - previewInfo, - loading, backupWallets, setBackupWallets, formState: { @@ -96,6 +95,7 @@ export const BackupPreviewDialog = memo(function Backu formState: { errors, isDirty, isValid }, }, } = useBackupFormState() + const { data: previewInfo, isLoading: loading } = useBackupPreviewInfo() const { showSnackbar } = useCustomSnackbar() const [{ loading: uploadLoading, value }, handleUploadBackup] = useAsyncFn( diff --git a/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/EmailForm.tsx b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/EmailForm.tsx new file mode 100644 index 000000000000..71b91722b985 --- /dev/null +++ b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/EmailForm.tsx @@ -0,0 +1,109 @@ +import { Trans, useLingui } from '@lingui/react/macro' +import { BackupAccountType } from '@masknet/shared-base' +import { CountdownButton, makeStyles, useCustomSnackbar } from '@masknet/theme' +import { Box, TextField } from '@mui/material' +import { memo, useCallback } from 'react' +import { Controller } from 'react-hook-form' +import { UserContext, useLanguage } from '../../../../../shared-ui/index.js' +import { CloudBackupFormContext } from '../../../../contexts/CloudBackupFormContext.js' +import { sendCode } from '../../../../utils/api.js' +import { Locale, Scenario } from '../../../../utils/type.js' + +const useStyles = makeStyles()((theme) => ({ + send: { + color: theme.palette.maskColor.main, + fontSize: 14, + }, +})) + +export const EmailForm = memo(function EmailForm() { + const { t } = useLingui() + const language = useLanguage() + const { classes } = useStyles() + const { user } = UserContext.useContainer() + const { showSnackbar } = useCustomSnackbar() + const { + formState: { + clearErrors, + control, + watch, + trigger, + formState: { errors }, + }, + } = CloudBackupFormContext.useContainer() + + const email = watch('email') + + const handleSendVerificationCode = useCallback(async () => { + const response = await sendCode({ + account: email, + type: BackupAccountType.Email, + scenario: user.email ? Scenario.change : Scenario.create, + locale: language.includes('zh') ? Locale.zh : Locale.en, + }).catch((error) => { + showSnackbar( + error.message.includes('SendTemplatedEmail') ? + Invalid email address format. + : error.message, + { variant: 'error' }, + ) + }) + + if (response) { + showSnackbar(Verification code sent, { variant: 'success' }) + } + }, [email, user, language]) + + return ( + + ( + clearErrors('email')} + onBlur={() => trigger('email')} + fullWidth + placeholder={t`Email`} + type="email" + error={!!errors.email?.message} + helperText={errors.email?.message} + /> + )} + /> + ( + clearErrors('code')} + fullWidth + placeholder={t`Email verification code`} + error={!!errors.code?.message} + helperText={errors.code?.message} + InputProps={{ + disableUnderline: true, + endAdornment: ( + Resend}> + Send + + ), + }} + /> + )} + /> + + ) +}) diff --git a/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/GoogleDriveBackup.tsx b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/GoogleDriveBackup.tsx new file mode 100644 index 000000000000..e144a65ce312 --- /dev/null +++ b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/GoogleDriveBackup.tsx @@ -0,0 +1,39 @@ +import { makeStyles } from '@masknet/theme' +import { Box, Button, Typography } from '@mui/material' +import { memo } from 'react' + +const useStyles = makeStyles()((theme) => ({ + tabContainer: { + border: `1px solid ${theme.palette.maskColor.line}`, + marginTop: theme.spacing(3), + borderRadius: theme.spacing(1, 1, 0, 0), + overflow: 'hidden', + marginBottom: 46, + }, + radios: { + flexDirection: 'row', + display: 'flex', + flexWrap: 'nowrap', + justifyContent: 'space-evenly', + }, + radioContainer: { + flexGrow: 1, + '& label': { + cursor: 'pointer', + }, + }, + panelContainer: { + padding: theme.spacing(2), + }, +})) + +export const GoogleDriveBackup = memo(function GoogleDriveBackup() { + return ( + + + test@gmail.com + + + + ) +}) diff --git a/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/MaskNetworkBackup.tsx b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/MaskNetworkBackup.tsx new file mode 100644 index 000000000000..b3bb36bde89a --- /dev/null +++ b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/MaskNetworkBackup.tsx @@ -0,0 +1,139 @@ +import { t } from '@lingui/core/macro' +import { Trans } from '@lingui/react/macro' +import { Alert } from '@masknet/shared' +import { BackupAccountType, DashboardRoutes } from '@masknet/shared-base' +import { makeStyles } from '@masknet/theme' +import { Box, Portal, Radio, RadioGroup, Typography, type BoxProps } from '@mui/material' +import { memo, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { useAsyncFn } from 'react-use' +import urlcat from 'urlcat' +import { UserContext } from '../../../../../shared-ui/index.js' +import { PrimaryButton } from '../../../../components/PrimaryButton/index.js' +import { fetchDownloadLink } from '../../../../utils/api.js' +import type { PortalContainerProps } from '../types.js' +import { EmailForm } from './EmailForm.js' +import { PhoneForm } from './PhoneForm.js' +import { useCloudBackupForm, type CloudBackupFormInputs } from './useCloudBackupForm.js' + +const useStyles = makeStyles()((theme) => ({ + container: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + }, + tabContainer: { + overflow: 'hidden', + }, + radios: { + flexDirection: 'row', + display: 'flex', + flexWrap: 'nowrap', + justifyContent: 'space-evenly', + }, + radioContainer: { + flexGrow: 1, + '& label': { + cursor: 'pointer', + }, + }, +})) + +interface Props extends PortalContainerProps, BoxProps {} + +export const MaskNetworkBackup = memo(function MaskNetworkBackup({ portalContainerRef, ...rest }) { + const { classes } = useStyles() + const { user, updateUser } = UserContext.useContainer() + const navigate = useNavigate() + const [showAlert, setShowAlert] = useState(true) + const [backupType, setBackupType] = useState(BackupAccountType.Email) + + const { formState } = useCloudBackupForm(backupType) + + const isEmail = backupType === BackupAccountType.Email + const incorrectCodeMsg = t`The code is incorrect.` + const [{ loading }, handleSubmit] = useAsyncFn( + async (data: CloudBackupFormInputs) => { + const response = await fetchDownloadLink({ + account: isEmail ? data.email : `+${data.countryCode} ${data.phone}`, + type: isEmail ? BackupAccountType.Email : BackupAccountType.Phone, + code: data.code, + }).catch((error) => { + if (error.status === 400) { + formState.setError('code', { + type: 'custom', + message: incorrectCodeMsg, + }) + } else if (error.status === 404) { + // No cloud backup file + navigate( + urlcat(DashboardRoutes.CloudBackupPreview, { + type: isEmail ? BackupAccountType.Email : BackupAccountType.Phone, + account: isEmail ? data.email : `+${data.countryCode} ${data.phone}`, + code: data.code, + }), + ) + } + }) + + if (!response) return + + updateUser({ + email: data.email || user.email, + phone: data.phone ? `${data.countryCode} ${data.phone}` : user.phone, + }) + navigate( + urlcat(DashboardRoutes.CloudBackupPreview, { + ...response, + type: isEmail ? BackupAccountType.Email : BackupAccountType.Phone, + account: isEmail ? data.email : `+${data.countryCode} ${data.phone}`, + code: data.code, + }), + ) + }, + [isEmail, formState, navigate, updateUser, user], + ) + + return ( + + { + setBackupType(isEmail ? BackupAccountType.Phone : BackupAccountType.Email) + }}> +
+ +
+
+ +
+ + {isEmail ? + + : } + setShowAlert(false)}> + + The Mask Network Cloud Backup feature will be deactivated on April 30, 2025. Please use alternative + cloud backup services or local backup solutions. + + + portalContainerRef.current}> + + Continue + + + + ) +}) diff --git a/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/PhoneForm.tsx b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/PhoneForm.tsx new file mode 100644 index 000000000000..438a9e5d1442 --- /dev/null +++ b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/PhoneForm.tsx @@ -0,0 +1,106 @@ +import { Trans, useLingui } from '@lingui/react/macro' +import { PhoneNumberField } from '@masknet/shared' +import { BackupAccountType } from '@masknet/shared-base' +import { CountdownButton, makeStyles, useCustomSnackbar } from '@masknet/theme' +import { Box, TextField } from '@mui/material' +import { memo, useCallback } from 'react' +import { Controller } from 'react-hook-form' +import { UserContext, useLanguage } from '../../../../../shared-ui/index.js' +import { CloudBackupFormContext } from '../../../../contexts/CloudBackupFormContext.js' +import { sendCode } from '../../../../utils/api.js' +import { Locale, Scenario } from '../../../../utils/type.js' + +const useStyles = makeStyles()((theme) => ({ + send: { + color: theme.palette.maskColor.main, + fontSize: 14, + }, +})) + +export const PhoneForm = memo(function PhoneForm() { + const { t } = useLingui() + const { classes } = useStyles() + const { user } = UserContext.useContainer() + const lang = useLanguage() + const { showSnackbar } = useCustomSnackbar() + + const { + formState: { + control, + clearErrors, + watch, + setValue, + formState: { errors }, + }, + } = CloudBackupFormContext.useContainer() + + const [countryCode, phone] = watch(['countryCode', 'phone']) + + const handleSendVerificationCode = useCallback(async () => { + const response = await sendCode({ + account: `+${countryCode}${phone}`, + type: BackupAccountType.Phone, + scenario: user.phone ? Scenario.change : Scenario.create, + locale: lang.includes('zh') ? Locale.zh : Locale.en, + }).catch((error) => { + showSnackbar(error.message, { variant: 'error' }) + }) + + if (response) { + showSnackbar(Verification code sent, { variant: 'success' }) + } + }, [phone, user, lang, countryCode]) + + return ( + + ( + setValue('countryCode', code)} + onFocus={() => clearErrors('phone')} + fullWidth + placeholder={t`Mobile number`} + error={!!errors.phone?.message} + helperText={errors.phone?.message} + /> + )} + /> + ( + clearErrors('code')} + fullWidth + placeholder={t`Phone verification code`} + error={!!errors.code?.message} + helperText={errors.code?.message} + InputProps={{ + disableUnderline: true, + endAdornment: ( + Resend}> + Send + + ), + }} + /> + )} + /> + + ) +}) diff --git a/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/index.tsx b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/index.tsx new file mode 100644 index 000000000000..12c9fde8774e --- /dev/null +++ b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/index.tsx @@ -0,0 +1,84 @@ +import { Trans } from '@lingui/react/macro' +import { Icons } from '@masknet/icons' +import { makeStyles } from '@masknet/theme' +import { Box, Typography, type BoxProps } from '@mui/material' +import { memo, useState } from 'react' +import { GoogleDriveBackup } from './GoogleDriveBackup.js' +import { MaskNetworkBackup } from './MaskNetworkBackup.js' +import type { PortalContainerProps } from '../types.js' + +const useStyles = makeStyles()((theme, _, refs) => ({ + container: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + }, + providers: { + display: 'flex', + gap: theme.spacing(1.5), + }, + providerName: { + fontWeight: 700, + fontSize: 14, + lineHeight: '18px', + height: 18, + }, + activeButton: { + backgroundColor: theme.palette.maskColor.input, + }, + toggleButton: { + backgroundColor: theme.palette.maskColor.bottom, + cursor: 'pointer', + boxSizing: 'border-box', + borderRadius: 18, + height: 34, + gap: theme.spacing(1), + border: `1px solid ${theme.palette.maskColor.line}`, + display: 'inline-flex', + padding: theme.spacing(1, 1.5), + [`&.${refs.activeButton}`]: { + backgroundColor: theme.palette.maskColor.input, + }, + }, +})) + +enum CloudProvider { + MaskNetwork = 'mask-network', + GoogleDrive = 'google-drive', +} + +interface Props extends BoxProps, PortalContainerProps {} +export const CloudBackup = memo(function CloudBackup({ portalContainerRef, ...rest }) { + const { classes, cx } = useStyles() + const [cloudProvider, setCloudProvider] = useState(CloudProvider.MaskNetwork) + + const isMaskNetwork = cloudProvider === CloudProvider.MaskNetwork + + return ( + + + + + + {isMaskNetwork ? + + : } + + ) +}) diff --git a/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/useCloudBackupForm.ts b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/useCloudBackupForm.ts new file mode 100644 index 000000000000..00db23cf68f7 --- /dev/null +++ b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/useCloudBackupForm.ts @@ -0,0 +1,62 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { useLingui } from '@lingui/react/macro' +import { BackupAccountType } from '@masknet/shared-base' +import guessCallingCode from 'guess-calling-code' +import { useForm } from 'react-hook-form' +import { z } from 'zod' +import { emailRegexp, phoneRegexp } from '../../../../utils/regexp.js' + +export interface CloudBackupFormInputs { + email: string + phone: string + code: string + countryCode: string +} + +export function useCloudBackupForm(backupType: BackupAccountType) { + const { t } = useLingui() + + const formState = useForm({ + mode: 'onSubmit', + context: { + backupType, + }, + defaultValues: { + email: '', + phone: '', + code: '', + countryCode: (guessCallingCode.default || guessCallingCode)(), + }, + resolver: zodResolver( + z + .object({ + email: + backupType === BackupAccountType.Email ? + z.string().regex(emailRegexp, t`Invalid email address format.`) + : z.string().optional(), + countryCode: backupType === BackupAccountType.Phone ? z.string() : z.string().optional(), + phone: + backupType === BackupAccountType.Phone ? z.string().regex(phoneRegexp) : z.string().optional(), + code: z + .string() + .min(1, t`The code is incorrect.`) + .max(6, t`The code is incorrect.`), + }) + .refine( + (data) => { + if (backupType !== BackupAccountType.Phone) return true + if (!data.countryCode || !data.phone) return false + return phoneRegexp.test(`+${data.countryCode} ${data.phone}`) + }, + { + message: t`The phone number is incorrect.`, + path: ['phone'], + }, + ), + ), + }) + + return { + formState, + } +} diff --git a/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackupFormContext.tsx b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackupFormContext.tsx new file mode 100644 index 000000000000..ab4615467794 --- /dev/null +++ b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackupFormContext.tsx @@ -0,0 +1,73 @@ +import { createContainer } from '@masknet/shared-base-ui' +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { z } from 'zod' +import { useTabs } from '@masknet/theme' +import guessCallingCode from 'guess-calling-code' +import { useLingui } from '@lingui/react/macro' +import { emailRegexp, phoneRegexp } from '../../../utils/regexp.js' + +export interface CloudBackupFormInputs { + email: string + phone: string + code: string + countryCode: string +} + +function useCloudBackupFormContext() { + const { t } = useLingui() + + const [currentTab, onChange, tabs] = useTabs('email', 'mobile') + + const formState = useForm({ + mode: 'onSubmit', + context: { + currentTab, + tabs, + }, + defaultValues: { + email: '', + phone: '', + code: '', + countryCode: (guessCallingCode.default || guessCallingCode)(), + }, + resolver: zodResolver( + z + .object({ + email: + currentTab === tabs.email ? + z.string().refine((email) => emailRegexp.test(email), t`Invalid email address format.`) + : z.string().optional(), + countryCode: currentTab === tabs.mobile ? z.string() : z.string().optional(), + phone: + currentTab === tabs.mobile ? + z.string().refine((mobile) => phoneRegexp.test(mobile)) + : z.string().optional(), + code: z + .string() + .min(1, t`The code is incorrect.`) + .max(6, t`The code is incorrect.`), + }) + .refine( + (data) => { + if (currentTab !== tabs.mobile) return true + if (!data.countryCode || !data.phone) return false + return phoneRegexp.test(`+${data.countryCode} ${data.phone}`) + }, + { + message: t`The phone number is incorrect.`, + path: ['phone'], + }, + ), + ), + }) + + return { + formState, + currentTab, + onChange, + tabs, + } +} + +export const CloudBackupFormContext = createContainer(useCloudBackupFormContext) diff --git a/packages/mask/dashboard/pages/SetupPersona/Backup/LocalBackup.tsx b/packages/mask/dashboard/pages/SetupPersona/Backup/LocalBackup.tsx new file mode 100644 index 000000000000..a2c4d895767b --- /dev/null +++ b/packages/mask/dashboard/pages/SetupPersona/Backup/LocalBackup.tsx @@ -0,0 +1,131 @@ +import Services from '#services' +import { Trans, useLingui } from '@lingui/react/macro' +import { encryptBackup } from '@masknet/backup-format' +import { Icons } from '@masknet/icons' +import { LoadingStatus } from '@masknet/shared' +import { MimeType } from '@masknet/shared-base' +import { encode } from '@msgpack/msgpack' +import { Box, Portal } from '@mui/material' +import { format as formatDateTime } from 'date-fns' +import { memo, type RefObject } from 'react' +import { Controller } from 'react-hook-form' +import { useAsyncFn } from 'react-use' +import { UserContext } from '../../../../shared-ui/index.js' +import { PersonasBackupPreview, WalletsBackupPreview } from '../../../components/BackupPreview/index.js' +import PasswordField from '../../../components/PasswordField/index.js' +import { PrimaryButton } from '../../../components/PrimaryButton/index.js' +import { useBackupFormState, type BackupFormInputs } from '../../../hooks/useBackupFormState.js' +import { useBackupPreviewInfo } from '../../../hooks/useBackupPreviewInfo.js' + +interface Props { + portalContainerRef: RefObject +} +export const LocalBackup = memo(function LocalBackup({ portalContainerRef }) { + const { t } = useLingui() + const { user, updateUser } = UserContext.useContainer() + const { + hasPassword, + backupWallets, + setBackupWallets, + formState: { + setError, + control, + handleSubmit, + clearErrors, + formState: { errors, isDirty, isValid }, + }, + } = useBackupFormState() + const { data: previewInfo, isLoading: loading } = useBackupPreviewInfo() + + const [{ loading: downloadLoading }, handleFormSubmit] = useAsyncFn( + async (data: BackupFormInputs) => { + if (backupWallets && hasPassword) { + const verified = await Services.Wallet.verifyPassword(data.paymentPassword || '') + if (!verified) { + setError('paymentPassword', { type: 'custom', message: t`Incorrect Password` }) + return + } + } + const { file } = await Services.Backup.createBackupFile({ + excludeBase: false, + excludeWallet: !backupWallets, + }) + + const encrypted = await encryptBackup(encode(data.backupPassword), encode(file)) + const now = formatDateTime(new Date(), 'yyyy-MM-dd HH:mm') + const blob = new Blob([encrypted], { type: MimeType.Binary }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `mask-network-keystore-backup-${now}.bin` + a.click() + + await updateUser({ + localBackupAt: now, + }) + + window.close() + }, + [backupWallets, hasPassword, setError, updateUser, user], + ) + + return ( +
+ {!loading && previewInfo ? + + + + ( + clearErrors()} + sx={{ mb: 2 }} + placeholder={t`Backup Password`} + error={!!errors.backupPassword?.message} + helperText={errors.backupPassword?.message} + /> + )} + name="backupPassword" + /> + + + + {backupWallets ? + ( + clearErrors()} + sx={{ mb: 2 }} + placeholder={t`Payment Password`} + error={!!errors.paymentPassword?.message} + helperText={errors.paymentPassword?.message} + /> + )} + name="paymentPassword" + /> + : null} + + : } + portalContainerRef.current}> + } + size="large" + color="primary" + loading={downloadLoading} + disabled={!isDirty || !isValid} + onClick={handleSubmit(handleFormSubmit)}> + Download Backup + + + + ) +}) diff --git a/packages/mask/dashboard/pages/SetupPersona/Backup/index.tsx b/packages/mask/dashboard/pages/SetupPersona/Backup/index.tsx new file mode 100644 index 000000000000..cfaa4dea262d --- /dev/null +++ b/packages/mask/dashboard/pages/SetupPersona/Backup/index.tsx @@ -0,0 +1,107 @@ +import { Trans } from '@lingui/react/macro' +import { MaskTabList, makeStyles, useTabs } from '@masknet/theme' +import { TabContext, TabPanel } from '@mui/lab' +import { Box, Tab, Typography } from '@mui/material' +import { memo, useMemo, useRef } from 'react' +import { CloudBackupFormContext } from '../../../contexts/CloudBackupFormContext.js' +import { CloudBackup } from './CloudBackup/index.js' +import { LocalBackup } from './LocalBackup.js' + +const useStyles = makeStyles()((theme) => ({ + title: { + fontSize: 36, + lineHeight: 1.2, + fontWeight: 700, + }, + description: { + color: theme.palette.maskColor.second, + fontSize: 14, + marginTop: theme.spacing(1.5), + }, + tabContainer: { + border: `1px solid ${theme.palette.maskColor.line}`, + marginTop: theme.spacing(3), + borderRadius: theme.spacing(1, 1, 0, 0), + overflow: 'hidden', + }, + tabList: { + background: + theme.palette.mode === 'light' ? + 'linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 100%), linear-gradient(90deg, rgba(98, 152, 234, 0.2) 1.03%, rgba(98, 152, 234, 0.2) 1.04%, rgba(98, 126, 234, 0.2) 100%)' + : 'linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.06) 100%)', + padding: theme.spacing('14px', 2, 0), + }, + tab: { + fontSize: 16, + fontWeight: 700, + }, + panels: { + display: 'flex', + flexDirection: 'column', + padding: 0, + width: '100%', + }, + panelContainer: { + padding: theme.spacing(2), + }, + exclaveActions: { + marginTop: 136, + }, +})) + +const Backup = memo(function Backup() { + const { classes } = useStyles() + const tabPanelClasses = useMemo(() => ({ root: classes.panels }), [classes.panels]) + + const [currentTab, onChange, tabs] = useTabs('cloud', 'local') + + const { formState } = CloudBackupFormContext.useContainer() + const portalContainerRef = useRef(null) + + return ( + + + Back Up Your Data Your Way + + + + Choose from multiple backup options, now including encrypted storage via your authorized Google + Drive for added security and flexibility. + + + + +
+ { + onChange(_, value) + formState.reset() + }} + aria-label="Cloud Backup Methods"> + Locale Backup} value={tabs.local} /> + Cloud Backup} value={tabs.cloud} /> + +
+
+ + + + + + +
+
+
+ +
+ ) +}) + +export const Component = memo(function BackupPage() { + return ( + + + + ) +}) diff --git a/packages/mask/dashboard/pages/SetupPersona/Backup/types.ts b/packages/mask/dashboard/pages/SetupPersona/Backup/types.ts new file mode 100644 index 000000000000..80a61eaf7f35 --- /dev/null +++ b/packages/mask/dashboard/pages/SetupPersona/Backup/types.ts @@ -0,0 +1,3 @@ +export interface PortalContainerProps { + portalContainerRef: React.RefObject +} diff --git a/packages/mask/dashboard/pages/SetupPersona/LocalBackup/index.tsx b/packages/mask/dashboard/pages/SetupPersona/LocalBackup/index.tsx index e8f46ca65270..c4fe949a69ba 100644 --- a/packages/mask/dashboard/pages/SetupPersona/LocalBackup/index.tsx +++ b/packages/mask/dashboard/pages/SetupPersona/LocalBackup/index.tsx @@ -1,22 +1,23 @@ +import Services from '#services' +import { Trans, useLingui } from '@lingui/react/macro' +import { encryptBackup } from '@masknet/backup-format' +import { Icons } from '@masknet/icons' +import { LoadingStatus } from '@masknet/shared' +import { MimeType } from '@masknet/shared-base' import { makeStyles } from '@masknet/theme' +import { encode } from '@msgpack/msgpack' import { Box, Typography } from '@mui/material' +import { format as formatDateTime } from 'date-fns' import { memo } from 'react' +import { Controller } from 'react-hook-form' import { useAsyncFn } from 'react-use' -import Services from '#services' -import { LoadingStatus } from '@masknet/shared' -import PasswordField from '../../../components/PasswordField/index.js' +import { UserContext } from '../../../../shared-ui/index.js' import { PersonasBackupPreview, WalletsBackupPreview } from '../../../components/BackupPreview/index.js' -import { Controller } from 'react-hook-form' -import { SetupFrameController } from '../../../components/SetupFrame/index.js' +import PasswordField from '../../../components/PasswordField/index.js' import { PrimaryButton } from '../../../components/PrimaryButton/index.js' -import { Icons } from '@masknet/icons' -import { encryptBackup } from '@masknet/backup-format' -import { encode } from '@msgpack/msgpack' -import { format as formatDateTime } from 'date-fns' -import { MimeType } from '@masknet/shared-base' +import { SetupFrameController } from '../../../components/SetupFrame/index.js' import { useBackupFormState, type BackupFormInputs } from '../../../hooks/useBackupFormState.js' -import { UserContext } from '../../../../shared-ui/index.js' -import { Trans, useLingui } from '@lingui/react/macro' +import { useBackupPreviewInfo } from '../../../hooks/useBackupPreviewInfo.js' const useStyles = makeStyles()((theme) => ({ title: { @@ -38,8 +39,6 @@ export const Component = memo(function LocalBackup() { const { user, updateUser } = UserContext.useContainer() const { hasPassword, - previewInfo, - loading, backupWallets, setBackupWallets, formState: { @@ -50,6 +49,7 @@ export const Component = memo(function LocalBackup() { formState: { errors, isDirty, isValid }, }, } = useBackupFormState() + const { data: previewInfo, isLoading: loading } = useBackupPreviewInfo() const [{ loading: downloadLoading }, handleFormSubmit] = useAsyncFn( async (data: BackupFormInputs) => { diff --git a/packages/mask/dashboard/pages/SetupPersona/index.tsx b/packages/mask/dashboard/pages/SetupPersona/index.tsx index 46a9612124ca..4b4d862ff8a3 100644 --- a/packages/mask/dashboard/pages/SetupPersona/index.tsx +++ b/packages/mask/dashboard/pages/SetupPersona/index.tsx @@ -11,6 +11,7 @@ export const personaRoutes: RouteObject[] = [ { path: r(DashboardRoutes.RecoveryPersona), lazy: () => import('./Recovery/index.js') }, { path: r(DashboardRoutes.SignUpPersonaMnemonic), lazy: () => import('./Mnemonic/index.js') }, { path: r(DashboardRoutes.SignUpPersonaOnboarding), lazy: () => import('./Onboarding/index.js') }, + { path: r(DashboardRoutes.Backup), lazy: () => import('./Backup/index.js') }, { path: r(DashboardRoutes.LocalBackup), lazy: () => import('./LocalBackup/index.js') }, { path: r(DashboardRoutes.CloudBackup), lazy: () => import('./CloudBackup/index.js') }, { path: r(DashboardRoutes.CloudBackupPreview), lazy: () => import('./CloudBackupPreview/index.js') }, diff --git a/packages/mask/shared-ui/initialization/storage.ts b/packages/mask/shared-ui/initialization/storage.ts index d3bc387f97a8..5b2563a0a605 100644 --- a/packages/mask/shared-ui/initialization/storage.ts +++ b/packages/mask/shared-ui/initialization/storage.ts @@ -25,3 +25,5 @@ const indexedDB: KVStorageBackend = { } setupMaskKVStorageBackend(indexedDB, memory) setupLegacySettingsAtNonBackground(Services.Settings.getLegacySettingsInitialValue) + +indexedDB diff --git a/packages/shared-base/src/KVStorage/index.ts b/packages/shared-base/src/KVStorage/index.ts index 3bbd3ef675e2..1b18b422d855 100644 --- a/packages/shared-base/src/KVStorage/index.ts +++ b/packages/shared-base/src/KVStorage/index.ts @@ -34,6 +34,7 @@ export interface BackupConfig { localBackupAt: string | null cloudBackupAt: string | null cloudBackupMethod: BackupAccountType | null + googleToken: string | null } export const PersistentStorages = { @@ -53,6 +54,7 @@ export const PersistentStorages = { localBackupAt: '', cloudBackupAt: '', cloudBackupMethod: null, + googleToken: '', }, }), /** diff --git a/packages/shared-base/src/types/Routes.ts b/packages/shared-base/src/types/Routes.ts index bfb4b23743ff..debcc055625f 100644 --- a/packages/shared-base/src/types/Routes.ts +++ b/packages/shared-base/src/types/Routes.ts @@ -7,6 +7,7 @@ export enum DashboardRoutes { RecoveryPersona = '/setup/recovery', SignUpPersonaMnemonic = '/setup/sign-up/mnemonic', SignUpPersonaOnboarding = '/setup/sign-up/onboarding', + Backup = '/setup/backup', LocalBackup = '/setup/local-backup', CloudBackup = '/setup/cloud-backup', CloudBackupPreview = '/setup/cloud-backup-preview', diff --git a/packages/shared/src/UI/components/Alert/index.tsx b/packages/shared/src/UI/components/Alert/index.tsx index 322c52a9753e..3a3dcb948528 100644 --- a/packages/shared/src/UI/components/Alert/index.tsx +++ b/packages/shared/src/UI/components/Alert/index.tsx @@ -1,10 +1,11 @@ import { Icons } from '@masknet/icons' import { makeStyles } from '@masknet/theme' -import { Box, Typography } from '@mui/material' +import { Box, Typography, type AlertProps } from '@mui/material' import type { BoxProps } from '@mui/system' import { memo } from 'react' -const useStyles = makeStyles()((theme) => ({ +const useStyles = makeStyles()((theme, _, refs) => ({ + warning: {}, alert: { display: 'flex', borderRadius: 4, @@ -15,21 +16,31 @@ const useStyles = makeStyles()((theme) => ({ color: theme.palette.maskColor.main, backdropFilter: 'blur(5px)', gap: 10, + [`&.${refs.warning}`]: { + backgroundColor: 'rgba(255, 177, 0, 0.1)', + color: '#FFB100', + }, }, })) -interface Props extends BoxProps { +interface Props extends BoxProps, Pick { open?: boolean onClose?: () => void } -export const Alert = memo(function Alert({ className, children, open, onClose, ...rest }: Props) { +export const Alert = memo(function Alert({ className, children, open, onClose, severity, ...rest }: Props) { const { classes, cx } = useStyles() if (!open) return null return ( - - + + {severity === 'warning' ? + + : } {children} diff --git a/packages/shared/src/UI/components/CountryCodePicker/index.tsx b/packages/shared/src/UI/components/CountryCodePicker/index.tsx index 64b4b8d95f32..a972988db6a4 100644 --- a/packages/shared/src/UI/components/CountryCodePicker/index.tsx +++ b/packages/shared/src/UI/components/CountryCodePicker/index.tsx @@ -12,7 +12,7 @@ import { Trans, useLingui } from '@lingui/react/macro' const useStyles = makeStyles()((theme) => ({ paper: { padding: theme.spacing(2), - borderRadius: 8, + borderRadius: 24, width: 320, height: 316, background: theme.palette.maskColor.bottom, @@ -98,10 +98,10 @@ export const CountryCodePicker = memo(({ open, anchorEl, onClose() }} classes={{ paper: classes.paper }} - anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} + anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }} transformOrigin={{ vertical: 'top', - horizontal: 'center', + horizontal: 'left', }}> ({ + button: { + padding: 0, + }, +})) export interface PhoneNumberFieldProps extends Omit { code: string @@ -12,6 +19,7 @@ export interface PhoneNumberFieldProps extends Omit(null) @@ -32,7 +40,10 @@ export function PhoneNumberField({ code, onCodeChange, ...rest }: PhoneNumberFie ...rest.InputProps, disableUnderline: true, startAdornment: ( - + + + ) + } return ( - + test@gmail.com diff --git a/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/index.tsx b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/index.tsx index 12c9fde8774e..483b3e479ac8 100644 --- a/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/index.tsx +++ b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/index.tsx @@ -2,10 +2,11 @@ import { Trans } from '@lingui/react/macro' import { Icons } from '@masknet/icons' import { makeStyles } from '@masknet/theme' import { Box, Typography, type BoxProps } from '@mui/material' +import { GoogleOAuthProvider } from '@react-oauth/google' import { memo, useState } from 'react' +import type { PortalContainerProps } from '../types.js' import { GoogleDriveBackup } from './GoogleDriveBackup.js' import { MaskNetworkBackup } from './MaskNetworkBackup.js' -import type { PortalContainerProps } from '../types.js' const useStyles = makeStyles()((theme, _, refs) => ({ container: { @@ -48,9 +49,13 @@ enum CloudProvider { } interface Props extends BoxProps, PortalContainerProps {} +// cspell:disable +const clientId = + process.env.GOOGLE_CLIENT_ID || '18954568633-c7has4fcrm5b7fop5si83fleb51oodji.apps.googleusercontent.com' +// cspell:enable export const CloudBackup = memo(function CloudBackup({ portalContainerRef, ...rest }) { const { classes, cx } = useStyles() - const [cloudProvider, setCloudProvider] = useState(CloudProvider.MaskNetwork) + const [cloudProvider, setCloudProvider] = useState(CloudProvider.GoogleDrive) const isMaskNetwork = cloudProvider === CloudProvider.MaskNetwork @@ -78,7 +83,10 @@ export const CloudBackup = memo(function CloudBackup({ portalContainerRef {isMaskNetwork ? - : } + : + + + } ) }) diff --git a/packages/mask/dashboard/pages/SetupPersona/Backup/helpers.ts b/packages/mask/dashboard/pages/SetupPersona/Backup/helpers.ts new file mode 100644 index 000000000000..946ff4894b53 --- /dev/null +++ b/packages/mask/dashboard/pages/SetupPersona/Backup/helpers.ts @@ -0,0 +1,25 @@ +import { fetchJSON } from '@masknet/web3-providers/helpers' + +interface Options { + code: string + clientId: string + redirectUri: string +} + +interface TokenResponse { + access_token: string + error_description: string +} + +export async function requestGoogleDriveAccessToken(options: Options) { + return fetchJSON('https://oauth2.googleapis.com/token', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + code: options.code, + client_id: options.clientId, + redirect_uri: options.redirectUri, + grant_type: 'authorization_code', + }), + }) +} diff --git a/packages/mask/package.json b/packages/mask/package.json index a04948af0d78..26687e72f0d2 100644 --- a/packages/mask/package.json +++ b/packages/mask/package.json @@ -93,6 +93,7 @@ "@masknet/web3-telemetry": "workspace:^", "@msgpack/msgpack": "^3.0.0-beta2", "@react-hookz/web": "^24.0.4", + "@react-oauth/google": "^0.12.1", "@servie/events": "^3.0.0", "@splinetool/runtime": "0.9.342", "@tanstack/query-async-storage-persister": "^5.48.0", @@ -117,6 +118,8 @@ "event-iterator": "^2.0.0", "file-loader": "^6.2.0", "fuse.js": "^7.0.0", + "google-auth-library": "^9.15.1", + "googleapis": "^148.0.0", "guess-calling-code": "^1.1.8", "html-to-image": "^1.11.11", "https-browserify": "^1.0.0", diff --git a/packages/mask/public/oauth2.html b/packages/mask/public/oauth2.html new file mode 100644 index 000000000000..92bd6db89492 --- /dev/null +++ b/packages/mask/public/oauth2.html @@ -0,0 +1,17 @@ + + + + Google Auth + + +

Google Auth, process...

+ + + diff --git a/packages/mask/shared-ui/hooks/useUserContext.ts b/packages/mask/shared-ui/hooks/useUserContext.ts index 47dd2217ff0a..0769c015d1cf 100644 --- a/packages/mask/shared-ui/hooks/useUserContext.ts +++ b/packages/mask/shared-ui/hooks/useUserContext.ts @@ -9,7 +9,8 @@ function useUserContext() { ) const updateUser = useCallback( - async (obj: Partial) => { + async (obj: Partial | ((user: BackupConfig) => BackupConfig)) => { + if (typeof obj === 'function') obj = obj(user) await PersistentStorages.Settings.storage.backupConfig.setValue({ ...user, ...obj, diff --git a/packages/polyfills/types/env.d.ts b/packages/polyfills/types/env.d.ts index 6c3d331fdbf0..154eabfc661c 100644 --- a/packages/polyfills/types/env.d.ts +++ b/packages/polyfills/types/env.d.ts @@ -4,6 +4,7 @@ declare module NodeJS { readonly MASK_SENTRY_DSN: string readonly MASK_SENTRY: 'enabled' | 'disabled' readonly MASK_MIXPANEL: 'enabled' | 'disabled' + readonly GOOGLE_CLIENT_ID: string /** * Run skip tests like * RUN_SKIP_TESTS=1 pnpm test diff --git a/packages/shared-base/src/KVStorage/index.ts b/packages/shared-base/src/KVStorage/index.ts index 1b18b422d855..fe9727a0217f 100644 --- a/packages/shared-base/src/KVStorage/index.ts +++ b/packages/shared-base/src/KVStorage/index.ts @@ -35,6 +35,7 @@ export interface BackupConfig { cloudBackupAt: string | null cloudBackupMethod: BackupAccountType | null googleToken: string | null + googleAccount: string | null } export const PersistentStorages = { @@ -55,6 +56,7 @@ export const PersistentStorages = { cloudBackupAt: '', cloudBackupMethod: null, googleToken: '', + googleAccount: '', }, }), /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 488d8e926921..c4fab6625cdf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -543,6 +543,9 @@ importers: '@react-hookz/web': specifier: ^24.0.4 version: 24.0.4(react-dom@0.0.0-experimental-58af67a8f8-20240628(react@0.0.0-experimental-58af67a8f8-20240628))(react@0.0.0-experimental-58af67a8f8-20240628) + '@react-oauth/google': + specifier: ^0.12.1 + version: 0.12.1(react-dom@0.0.0-experimental-58af67a8f8-20240628(react@0.0.0-experimental-58af67a8f8-20240628))(react@0.0.0-experimental-58af67a8f8-20240628) '@servie/events': specifier: ^3.0.0 version: 3.0.0 @@ -615,6 +618,12 @@ importers: fuse.js: specifier: ^7.0.0 version: 7.0.0 + google-auth-library: + specifier: ^9.15.1 + version: 9.15.1(encoding@0.1.13) + googleapis: + specifier: ^148.0.0 + version: 148.0.0(encoding@0.1.13) guess-calling-code: specifier: ^1.1.8 version: 1.1.8 @@ -5951,6 +5960,12 @@ packages: js-cookie: optional: true + '@react-oauth/google@0.12.1': + resolution: {integrity: sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==} + peerDependencies: + react: 0.0.0-experimental-58af67a8f8-20240628 + react-dom: 0.0.0-experimental-58af67a8f8-20240628 + '@remix-run/router@1.17.0': resolution: {integrity: sha512-2D6XaHEVvkCn682XBnipbJjgZUU7xjLtA4dGJRBVUKpEaDYOZMENZoZjAOSb7qirxt5RupjzZxz4fK2FO+EFPw==} engines: {node: '>=14.0.0'} @@ -8272,6 +8287,9 @@ packages: buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -8373,6 +8391,9 @@ packages: resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} engines: {node: '>=16'} + caniuse-lite@1.0.30001684: + resolution: {integrity: sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==} + caniuse-lite@1.0.30001702: resolution: {integrity: sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA==} @@ -9479,6 +9500,9 @@ packages: ecc-jsbn@0.1.2: resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -9493,6 +9517,9 @@ packages: electron-to-chromium@1.5.112: resolution: {integrity: sha512-oen93kVyqSb3l+ziUgzIOlWt/oOuy4zRmpwestMn4rhFWAoFJeFuCVte9F2fASjeZZo7l/Cif9TiyrdW4CwEMA==} + electron-to-chromium@1.5.65: + resolution: {integrity: sha512-PWVzBjghx7/wop6n22vS2MLU8tKGd4Q91aCEGhG/TYmW6PP5OcSXcdnxTe1NNt0T66N8D6jxh4kC8UsdzOGaIw==} + elliptic@6.6.1: resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} @@ -10383,6 +10410,14 @@ packages: resolution: {integrity: sha512-rci1g6U0rdTg6bAaBboP7XdRu01dzTAaKXxFf+PUqGuCv6Xu7o8NZdY1D5MvKGIjb6EdS1g3VlXOgksir1uGkg==} hasBin: true + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + + gcp-metadata@6.1.1: + resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} + engines: {node: '>=14'} + gensequence@7.0.0: resolution: {integrity: sha512-47Frx13aZh01afHJTB3zTtKIlFI6vWY+MYCN9Qpew6i52rfKjnhCF/l1YlC8UmEMvvntZZ6z4PiCcmyuedR2aQ==} engines: {node: '>=18'} @@ -10527,6 +10562,22 @@ packages: resolution: {integrity: sha512-eWv1ds/zAlz+M1ioHsyKJomfY7jbDDPpwSkv14KQj89bycx1nvK5/2Cj/T9g7kzJcX5Bc7Yv22FjfBZS/jl94A==} engines: {node: '>= 10.13.0'} + google-auth-library@9.15.1: + resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} + engines: {node: '>=14'} + + google-logging-utils@0.0.2: + resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + engines: {node: '>=14'} + + googleapis-common@7.2.0: + resolution: {integrity: sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==} + engines: {node: '>=14.0.0'} + + googleapis@148.0.0: + resolution: {integrity: sha512-8PDG5VItm6E1TdZWDqtRrUJSlBcNwz0/MwCa6AL81y/RxPGXJRUwKqGZfCoVX1ZBbfr3I4NkDxBmeTyOAZSWqw==} + engines: {node: '>=14.0.0'} + gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} @@ -10574,6 +10625,10 @@ packages: growly@1.3.0: resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==} + gtoken@7.1.0: + resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} + engines: {node: '>=14.0.0'} + guess-calling-code@1.1.8: resolution: {integrity: sha512-IJkVk+DO3cIE/AzNYVnJPDUnEiKMjT7y0OHjyBQw/LOF5oOxEM8N6+fOcHG2/oZI8Eh7sExP9nlkEsf1Ta8UiQ==} @@ -11721,6 +11776,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -11792,6 +11850,12 @@ packages: jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + jwa@2.0.0: + resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==} + + jws@4.0.0: + resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + jwt-decode@3.1.2: resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==} @@ -12656,6 +12720,9 @@ packages: node-notifier@10.0.1: resolution: {integrity: sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==} + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} @@ -15064,6 +15131,9 @@ packages: url-set-query@1.0.0: resolution: {integrity: sha512-3AChu4NiXquPfeckE5R5cGdiHCMWJx1dwCWOmWIL4KHAziJNOFIYJlpGFeKDvwLPHovZRCxK3cYlwzqI9Vp+Gg==} + url-template@2.0.8: + resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==} + url@0.11.4: resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} engines: {node: '>= 0.4'} @@ -20144,6 +20214,11 @@ snapshots: react: 0.0.0-experimental-58af67a8f8-20240628 react-dom: 0.0.0-experimental-58af67a8f8-20240628(react@0.0.0-experimental-58af67a8f8-20240628) + '@react-oauth/google@0.12.1(react-dom@0.0.0-experimental-58af67a8f8-20240628(react@0.0.0-experimental-58af67a8f8-20240628))(react@0.0.0-experimental-58af67a8f8-20240628)': + dependencies: + react: 0.0.0-experimental-58af67a8f8-20240628 + react-dom: 0.0.0-experimental-58af67a8f8-20240628(react@0.0.0-experimental-58af67a8f8-20240628) + '@remix-run/router@1.17.0': {} '@rgba-image/common@0.1.13': {} @@ -23527,9 +23602,9 @@ snapshots: browserslist@4.24.2: dependencies: - caniuse-lite: 1.0.30001702 - electron-to-chromium: 1.5.112 - node-releases: 2.0.19 + caniuse-lite: 1.0.30001684 + electron-to-chromium: 1.5.65 + node-releases: 2.0.18 update-browserslist-db: 1.1.1(browserslist@4.24.2) browserslist@4.24.4: @@ -23563,6 +23638,8 @@ snapshots: buffer-crc32@0.2.13: {} + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer-layout@1.2.2: {} @@ -23658,6 +23735,8 @@ snapshots: camelcase@8.0.0: {} + caniuse-lite@1.0.30001684: {} + caniuse-lite@1.0.30001702: {} canonicalize@1.0.8: {} @@ -24896,6 +24975,10 @@ snapshots: jsbn: 0.1.1 safer-buffer: 2.1.2 + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} eip-712@1.0.0: @@ -24910,6 +24993,8 @@ snapshots: electron-to-chromium@1.5.112: {} + electron-to-chromium@1.5.65: {} + elliptic@6.6.1: dependencies: bn.js: 4.12.1 @@ -26252,6 +26337,26 @@ snapshots: which: 1.2.4 winreg: 0.0.12 + gaxios@6.7.1(encoding@0.1.13): + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.5 + is-stream: 2.0.1 + node-fetch: 2.7.0(encoding@0.1.13) + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + gcp-metadata@6.1.1(encoding@0.1.13): + dependencies: + gaxios: 6.7.1(encoding@0.1.13) + google-logging-utils: 0.0.2 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + gensequence@7.0.0: {} gensync@1.0.0-beta.2: {} @@ -26423,6 +26528,40 @@ snapshots: dependencies: sparkles: 2.1.0 + google-auth-library@9.15.1(encoding@0.1.13): + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 6.7.1(encoding@0.1.13) + gcp-metadata: 6.1.1(encoding@0.1.13) + gtoken: 7.1.0(encoding@0.1.13) + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + google-logging-utils@0.0.2: {} + + googleapis-common@7.2.0(encoding@0.1.13): + dependencies: + extend: 3.0.2 + gaxios: 6.7.1(encoding@0.1.13) + google-auth-library: 9.15.1(encoding@0.1.13) + qs: 6.13.1 + url-template: 2.0.8 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + googleapis@148.0.0(encoding@0.1.13): + dependencies: + google-auth-library: 9.15.1(encoding@0.1.13) + googleapis-common: 7.2.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + - supports-color + gopd@1.0.1: dependencies: get-intrinsic: 1.2.4 @@ -26492,6 +26631,14 @@ snapshots: growly@1.3.0: {} + gtoken@7.1.0(encoding@0.1.13): + dependencies: + gaxios: 6.7.1(encoding@0.1.13) + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + guess-calling-code@1.1.8: {} gulp-cli@3.0.0: @@ -27938,6 +28085,10 @@ snapshots: jsesc@3.1.0: {} + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.1.2 + json-buffer@3.0.1: {} json-merge-patch@1.0.2: @@ -28014,6 +28165,17 @@ snapshots: readable-stream: 2.3.8 setimmediate: 1.0.5 + jwa@2.0.0: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.0: + dependencies: + jwa: 2.0.0 + safe-buffer: 5.2.1 + jwt-decode@3.1.2: {} keccak@3.0.3: @@ -29122,6 +29284,8 @@ snapshots: uuid: 8.3.2 which: 2.0.2 + node-releases@2.0.18: {} + node-releases@2.0.19: {} normalize-package-data@3.0.3: @@ -31819,6 +31983,8 @@ snapshots: url-set-query@1.0.0: {} + url-template@2.0.8: {} + url@0.11.4: dependencies: punycode: 1.4.1 From c9d6b19e55a679fc7d22b5c86d738f1883a13266 Mon Sep 17 00:00:00 2001 From: Wukong Sun Date: Wed, 26 Mar 2025 17:00:13 +0800 Subject: [PATCH 3/9] refactor: organize routes --- .../mask/.webpack/manifest/manifest-mv3.json | 2 +- packages/mask/.webpack/manifest/manifest.json | 8 +- .../services/backup/google_drive.ts | 15 ++ .../mask/background/services/backup/index.ts | 1 + .../tasks/NotCancellable/OnInstall.ts | 1 + .../contexts/CloudBackupFormContext.tsx | 1 + .../dashboard/hooks/useBackupFormState.ts | 2 + ...ackupForm.ts => CloudBackupFormContext.ts} | 29 ++- .../Backup/CloudBackup/EmailForm.tsx | 4 +- .../Backup/CloudBackup/GoogleDriveBackup.tsx | 29 ++- .../Backup/CloudBackup/MaskNetworkBackup.tsx | 41 ++-- .../Backup/CloudBackup/PhoneForm.tsx | 4 +- .../Backup/CloudBackup/Preview.tsx | 208 ++++++++++++++++++ .../SetupPersona/Backup/CloudBackup/index.tsx | 44 ++-- .../Backup/CloudBackupFormContext.tsx | 73 ------ .../pages/SetupPersona/Backup/LocalBackup.tsx | 11 +- .../pages/SetupPersona/Backup/constants.ts | 8 + .../pages/SetupPersona/Backup/index.tsx | 71 +++--- .../pages/SetupPersona/Backup/types.ts | 5 + .../SetupPersona/CloudBackupPreview/index.tsx | 7 +- .../dashboard/pages/SetupPersona/index.tsx | 62 +++++- packages/mask/dashboard/utils/regexp.ts | 3 + .../mask/shared-ui/hooks/useUserContext.ts | 8 +- .../src/helpers/relativeRouteOf.ts | 4 + packages/shared-base/src/types/Routes.ts | 6 + .../src/UI/modals/ConfirmDialog/index.tsx | 6 +- packages/shared/src/hooks/index.ts | 1 + packages/shared/src/hooks/useParamTab.ts | 9 +- packages/shared/src/hooks/usePathTab.ts | 21 ++ 29 files changed, 467 insertions(+), 217 deletions(-) create mode 100644 packages/mask/background/services/backup/google_drive.ts rename packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/{useCloudBackupForm.ts => CloudBackupFormContext.ts} (62%) create mode 100644 packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/Preview.tsx delete mode 100644 packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackupFormContext.tsx create mode 100644 packages/mask/dashboard/pages/SetupPersona/Backup/constants.ts create mode 100644 packages/shared/src/hooks/usePathTab.ts diff --git a/packages/mask/.webpack/manifest/manifest-mv3.json b/packages/mask/.webpack/manifest/manifest-mv3.json index af0c5a6a1f07..8e36cb33bb07 100644 --- a/packages/mask/.webpack/manifest/manifest-mv3.json +++ b/packages/mask/.webpack/manifest/manifest-mv3.json @@ -2,7 +2,7 @@ "name": "Mask Network", "version": "0", "manifest_version": 3, - "permissions": ["storage", "webNavigation", "activeTab", "scripting"], + "permissions": ["storage", "webNavigation", "activeTab", "scripting", "identity", "tabs"], "optional_permissions": ["notifications", "clipboardRead"], "optional_host_permissions": [""], "background": { "service_worker": "/manifest-v3.entry.js" }, diff --git a/packages/mask/.webpack/manifest/manifest.json b/packages/mask/.webpack/manifest/manifest.json index f1f076af3c47..4d6e1069c215 100644 --- a/packages/mask/.webpack/manifest/manifest.json +++ b/packages/mask/.webpack/manifest/manifest.json @@ -2,7 +2,7 @@ "name": "Mask Network", "version": "0", "manifest_version": 2, - "permissions": ["storage", "webNavigation", "activeTab"], + "permissions": ["storage", "webNavigation", "activeTab", "identity"], "optional_permissions": ["", "notifications", "clipboardRead", "tabs"], "background": { "page": "background.html" }, "icons": { @@ -14,5 +14,9 @@ "browser_action": { "default_popup": "popups.html" }, "homepage_url": "https://mask.io", "description": "The portal to the new & open Internet. Send encrypted message and decentralized Apps right on top of social networks.", - "web_accessible_resources": ["js/*", "bundled/*", "entry/*", "*.svg", "*.png", "*.jpg", "*.css", "build-info.json"] + "web_accessible_resources": ["js/*", "bundled/*", "entry/*", "*.svg", "*.png", "*.jpg", "*.css", "build-info.json"], + "oauth2": { + "client_id": "884348965548-npd1mffii0tbrr87eb2fm73ue022k3t3.apps.googleusercontent.com", + "scopes": ["https://www.googleapis.com/auth/drive.file", "https://www.googleapis.com/auth/userinfo.email"] + } } diff --git a/packages/mask/background/services/backup/google_drive.ts b/packages/mask/background/services/backup/google_drive.ts new file mode 100644 index 000000000000..e4f0b2c70b4b --- /dev/null +++ b/packages/mask/background/services/backup/google_drive.ts @@ -0,0 +1,15 @@ +// cspell:disable +const clientId = + process.env.GOOGLE_CLIENT_ID || '18954568633-c7has4fcrm5b7fop5si83fleb51oodji.apps.googleusercontent.com' +// cspell:enable +const SCOPES = ['https://www.googleapis.com/auth/drive.file', 'https://www.googleapis.com/auth/userinfo.email'] +const redirectUri = browser.runtime.getURL('oauth2.html') + +export async function getAccessToken() { + const url = browser.identity.getRedirectURL() + const token = await browser.identity.launchWebAuthFlow({ + url: `https://accounts.google.com/o/oauth2/v2/auth?client_id=${clientId}&redirect_uri=${encodeURIComponent(url)}&response_type=code&scope=${encodeURIComponent(SCOPES.join(' '))}&access_type=offline`, + interactive: true, + }) + return token +} diff --git a/packages/mask/background/services/backup/index.ts b/packages/mask/background/services/backup/index.ts index f76aa6aed5b6..9012814c58bb 100644 --- a/packages/mask/background/services/backup/index.ts +++ b/packages/mask/background/services/backup/index.ts @@ -1,3 +1,4 @@ export { generateBackupPreviewInfo, createBackupFile, type BackupOptions } from './create.js' export { generateBackupSummary, restoreBackup } from './restore.js' export { backupPersonaPrivateKey } from './persona.js' +export { getAccessToken } from './google_drive.js' diff --git a/packages/mask/background/tasks/NotCancellable/OnInstall.ts b/packages/mask/background/tasks/NotCancellable/OnInstall.ts index 3eacb2caa727..31acb91a330e 100644 --- a/packages/mask/background/tasks/NotCancellable/OnInstall.ts +++ b/packages/mask/background/tasks/NotCancellable/OnInstall.ts @@ -39,6 +39,7 @@ browser.runtime.onInstalled.addListener(async (detail) => { localBackupAt: backupMethod && backupMethod === 'local' ? localStorage.getItem('backupAt') : null, cloudBackupMethod: null, googleToken: null, + googleAccount: null, }) } // remove old data after migrate diff --git a/packages/mask/dashboard/contexts/CloudBackupFormContext.tsx b/packages/mask/dashboard/contexts/CloudBackupFormContext.tsx index 5841bafcbe9c..15d881b63dce 100644 --- a/packages/mask/dashboard/contexts/CloudBackupFormContext.tsx +++ b/packages/mask/dashboard/contexts/CloudBackupFormContext.tsx @@ -70,4 +70,5 @@ function useCloudBackupFormContext() { } } +/** @deprecated */ export const CloudBackupFormContext = createContainer(useCloudBackupFormContext) diff --git a/packages/mask/dashboard/hooks/useBackupFormState.ts b/packages/mask/dashboard/hooks/useBackupFormState.ts index 6b9e5aabc336..61ed0af71dbc 100644 --- a/packages/mask/dashboard/hooks/useBackupFormState.ts +++ b/packages/mask/dashboard/hooks/useBackupFormState.ts @@ -19,6 +19,8 @@ export function useBackupFormState() { const { user } = UserContext.useContainer() const [backupWallets, setBackupWallets] = useState(false) + console.log('user', user) + const formState = useForm({ mode: 'onBlur', context: { diff --git a/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/useCloudBackupForm.ts b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/CloudBackupFormContext.ts similarity index 62% rename from packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/useCloudBackupForm.ts rename to packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/CloudBackupFormContext.ts index 00db23cf68f7..607f0e6da89c 100644 --- a/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/useCloudBackupForm.ts +++ b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/CloudBackupFormContext.ts @@ -1,10 +1,12 @@ import { zodResolver } from '@hookform/resolvers/zod' +import { createContainer } from '@masknet/shared-base-ui' import { useLingui } from '@lingui/react/macro' import { BackupAccountType } from '@masknet/shared-base' import guessCallingCode from 'guess-calling-code' import { useForm } from 'react-hook-form' import { z } from 'zod' -import { emailRegexp, phoneRegexp } from '../../../../utils/regexp.js' +import { phoneRegexp } from '../../../../utils/regexp.js' +import { useState } from 'react' export interface CloudBackupFormInputs { email: string @@ -13,10 +15,13 @@ export interface CloudBackupFormInputs { countryCode: string } -export function useCloudBackupForm(backupType: BackupAccountType) { +export function useCloudBackupForm() { const { t } = useLingui() - const formState = useForm({ + const [backupType, setBackupType] = useState(BackupAccountType.Email) + const isEmail = backupType === BackupAccountType.Email + + const form = useForm({ mode: 'onSubmit', context: { backupType, @@ -30,13 +35,9 @@ export function useCloudBackupForm(backupType: BackupAccountType) { resolver: zodResolver( z .object({ - email: - backupType === BackupAccountType.Email ? - z.string().regex(emailRegexp, t`Invalid email address format.`) - : z.string().optional(), - countryCode: backupType === BackupAccountType.Phone ? z.string() : z.string().optional(), - phone: - backupType === BackupAccountType.Phone ? z.string().regex(phoneRegexp) : z.string().optional(), + email: isEmail ? z.string().email(t`Invalid email address format.`) : z.string().optional(), + countryCode: isEmail ? z.string().optional() : z.string(), + phone: isEmail ? z.string().optional() : z.string().regex(phoneRegexp), code: z .string() .min(1, t`The code is incorrect.`) @@ -44,7 +45,7 @@ export function useCloudBackupForm(backupType: BackupAccountType) { }) .refine( (data) => { - if (backupType !== BackupAccountType.Phone) return true + if (isEmail) return true if (!data.countryCode || !data.phone) return false return phoneRegexp.test(`+${data.countryCode} ${data.phone}`) }, @@ -57,6 +58,10 @@ export function useCloudBackupForm(backupType: BackupAccountType) { }) return { - formState, + form, + backupType, + setBackupType, } } + +export const CloudBackupFormContext = createContainer(useCloudBackupForm) diff --git a/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/EmailForm.tsx b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/EmailForm.tsx index 71b91722b985..768e02af6158 100644 --- a/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/EmailForm.tsx +++ b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/EmailForm.tsx @@ -5,9 +5,9 @@ import { Box, TextField } from '@mui/material' import { memo, useCallback } from 'react' import { Controller } from 'react-hook-form' import { UserContext, useLanguage } from '../../../../../shared-ui/index.js' -import { CloudBackupFormContext } from '../../../../contexts/CloudBackupFormContext.js' import { sendCode } from '../../../../utils/api.js' import { Locale, Scenario } from '../../../../utils/type.js' +import { CloudBackupFormContext } from './CloudBackupFormContext.js' const useStyles = makeStyles()((theme) => ({ send: { @@ -23,7 +23,7 @@ export const EmailForm = memo(function EmailForm() { const { user } = UserContext.useContainer() const { showSnackbar } = useCustomSnackbar() const { - formState: { + form: { clearErrors, control, watch, diff --git a/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/GoogleDriveBackup.tsx b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/GoogleDriveBackup.tsx index 88a90b6608d5..b83dd8260eef 100644 --- a/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/GoogleDriveBackup.tsx +++ b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/GoogleDriveBackup.tsx @@ -5,6 +5,7 @@ import { memo, useEffect } from 'react' import { UserContext } from '../../../../../shared-ui/index.js' import { requestGoogleDriveAccessToken } from '../helpers.js' import { t } from '@lingui/core/macro' +import Services from '#services' const useStyles = makeStyles()((theme) => ({ container: { @@ -30,15 +31,15 @@ interface Props extends BoxProps {} const clientId = process.env.GOOGLE_CLIENT_ID || '18954568633-c7has4fcrm5b7fop5si83fleb51oodji.apps.googleusercontent.com' // cspell:enable -const SCOPE = 'https://www.googleapis.com/auth/drive.file' +const SCOPES = ['https://www.googleapis.com/auth/drive.file', 'https://www.googleapis.com/auth/userinfo.email'] const redirectUri = browser.runtime.getURL('oauth2.html') const handleSignIn = () => { - const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(SCOPE)}&access_type=offline` + const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(SCOPES.join(' '))}&access_type=offline` window.location.assign(authUrl) } -export const GoogleDriveBackup = memo(function GoogleDriveBackup(props) { - const { classes, cx } = useStyles() +export const Component = memo(function GoogleDriveBackup() { + const { classes } = useStyles() const { user, updateUser } = UserContext.useContainer() const { showSnackbar } = useCustomSnackbar() @@ -54,7 +55,6 @@ export const GoogleDriveBackup = memo(function GoogleDriveBackup(props) { redirectUri, }) if (res.access_token) { - debugger // TODO account updateUser((user) => ({ ...user, @@ -70,9 +70,22 @@ export const GoogleDriveBackup = memo(function GoogleDriveBackup(props) { return () => controller.abort() }, []) + const login = async () => { + // const token = await browser.identity.launchWebAuthFlow({ + // url: `https://accounts.google.com/o/oauth2/v2/auth?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(SCOPES.join(' '))}&access_type=offline`, + // interactive: true, + // }) + const token = await Services.Backup.getAccessToken() + if (!token) return + updateUser((user) => ({ + ...user, + googleAccessToken: token, + })) + } + if (!user.googleAccount) { return ( - + Add google Drive @@ -82,7 +95,7 @@ export const GoogleDriveBackup = memo(function GoogleDriveBackup(props) { - @@ -90,7 +103,7 @@ export const GoogleDriveBackup = memo(function GoogleDriveBackup(props) { ) } return ( - + test@gmail.com diff --git a/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/MaskNetworkBackup.tsx b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/MaskNetworkBackup.tsx index b3bb36bde89a..1e226efb3583 100644 --- a/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/MaskNetworkBackup.tsx +++ b/packages/mask/dashboard/pages/SetupPersona/Backup/CloudBackup/MaskNetworkBackup.tsx @@ -1,11 +1,11 @@ import { t } from '@lingui/core/macro' import { Trans } from '@lingui/react/macro' -import { Alert } from '@masknet/shared' +import { Alert, useParamTab } from '@masknet/shared' import { BackupAccountType, DashboardRoutes } from '@masknet/shared-base' import { makeStyles } from '@masknet/theme' -import { Box, Portal, Radio, RadioGroup, Typography, type BoxProps } from '@mui/material' +import { Box, Portal, Radio, RadioGroup, Typography } from '@mui/material' import { memo, useState } from 'react' -import { useNavigate } from 'react-router-dom' +import { useNavigate, useOutletContext } from 'react-router-dom' import { useAsyncFn } from 'react-use' import urlcat from 'urlcat' import { UserContext } from '../../../../../shared-ui/index.js' @@ -14,7 +14,7 @@ import { fetchDownloadLink } from '../../../../utils/api.js' import type { PortalContainerProps } from '../types.js' import { EmailForm } from './EmailForm.js' import { PhoneForm } from './PhoneForm.js' -import { useCloudBackupForm, type CloudBackupFormInputs } from './useCloudBackupForm.js' +import { CloudBackupFormContext, type CloudBackupFormInputs } from './CloudBackupFormContext.js' const useStyles = makeStyles()((theme) => ({ container: { @@ -22,9 +22,6 @@ const useStyles = makeStyles()((theme) => ({ flexDirection: 'column', gap: theme.spacing(2), }, - tabContainer: { - overflow: 'hidden', - }, radios: { flexDirection: 'row', display: 'flex', @@ -39,18 +36,18 @@ const useStyles = makeStyles()((theme) => ({ }, })) -interface Props extends PortalContainerProps, BoxProps {} - -export const MaskNetworkBackup = memo(function MaskNetworkBackup({ portalContainerRef, ...rest }) { +export const Component = memo(function MaskNetworkBackup() { const { classes } = useStyles() + const { portalContainerRef } = useOutletContext() const { user, updateUser } = UserContext.useContainer() const navigate = useNavigate() const [showAlert, setShowAlert] = useState(true) - const [backupType, setBackupType] = useState(BackupAccountType.Email) - const { formState } = useCloudBackupForm(backupType) + const [accountType, setAccountType] = useParamTab(BackupAccountType.Email, 'account-type') + const { form } = CloudBackupFormContext.useContainer() + const { setError, formState } = form - const isEmail = backupType === BackupAccountType.Email + const isEmail = accountType === BackupAccountType.Email const incorrectCodeMsg = t`The code is incorrect.` const [{ loading }, handleSubmit] = useAsyncFn( async (data: CloudBackupFormInputs) => { @@ -60,14 +57,14 @@ export const MaskNetworkBackup = memo(function MaskNetworkBackup({ portal code: data.code, }).catch((error) => { if (error.status === 400) { - formState.setError('code', { + setError('code', { type: 'custom', message: incorrectCodeMsg, }) } else if (error.status === 404) { // No cloud backup file navigate( - urlcat(DashboardRoutes.CloudBackupPreview, { + urlcat(DashboardRoutes.BackupPreview, { type: isEmail ? BackupAccountType.Email : BackupAccountType.Phone, account: isEmail ? data.email : `+${data.countryCode} ${data.phone}`, code: data.code, @@ -83,7 +80,7 @@ export const MaskNetworkBackup = memo(function MaskNetworkBackup({ portal phone: data.phone ? `${data.countryCode} ${data.phone}` : user.phone, }) navigate( - urlcat(DashboardRoutes.CloudBackupPreview, { + urlcat(DashboardRoutes.BackupPreview, { ...response, type: isEmail ? BackupAccountType.Email : BackupAccountType.Phone, account: isEmail ? data.email : `+${data.countryCode} ${data.phone}`, @@ -91,16 +88,16 @@ export const MaskNetworkBackup = memo(function MaskNetworkBackup({ portal }), ) }, - [isEmail, formState, navigate, updateUser, user], + [isEmail, setError, navigate, updateUser, user], ) return ( - + { - setBackupType(isEmail ? BackupAccountType.Phone : BackupAccountType.Email) + setAccountType(undefined, isEmail ? BackupAccountType.Phone : BackupAccountType.Email) }}>