diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index 7a66c76765f8..d39bca68dc68 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -34,12 +34,15 @@ "color": "^4.1.0", "date-fns": "2.27.0", "history": "^5.2.0", + "html-to-image": "^1.9.0", "i18next-browser-languagedetector": "^6.1.2", "json-stable-stringify": "^1.0.1", "react-avatar-editor": "^12.0.0", "react-hook-form": "^7.22.5", + "react-qrcode-logo": "^2.5.0", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", + "react-to-print": "^2.14.3", "react-use": "^17.3.1", "unstated-next": "^1.1.0", "urlcat": "^2.0.4", diff --git a/packages/dashboard/src/assets/images/MaskBlue.png b/packages/dashboard/src/assets/images/MaskBlue.png new file mode 100644 index 000000000000..ee2dbd5f0499 Binary files /dev/null and b/packages/dashboard/src/assets/images/MaskBlue.png differ diff --git a/packages/dashboard/src/assets/images/MaskWatermark.png b/packages/dashboard/src/assets/images/MaskWatermark.png new file mode 100644 index 000000000000..29b1025dab3e Binary files /dev/null and b/packages/dashboard/src/assets/images/MaskWatermark.png differ diff --git a/packages/dashboard/src/assets/index.ts b/packages/dashboard/src/assets/index.ts index 33f3af424100..a6bba9242542 100644 --- a/packages/dashboard/src/assets/index.ts +++ b/packages/dashboard/src/assets/index.ts @@ -4,3 +4,5 @@ export const ABOUT_DIALOG_BACKGROUND = new URL('./images/AboutDialogBackground.png', import.meta.url) export const PoolTogetherURL = new URL('./images/PoolTogether.png', import.meta.url) export const SetupTutorialURL = new URL('./images/SetupTutorial.svg', import.meta.url) +export const WatermarkURL = new URL('./images/MaskWatermark.png', import.meta.url) +export const MaskBlueURL = new URL('./images/MaskBlue.png', import.meta.url) diff --git a/packages/dashboard/src/components/Mnemonic/MnemonicReveal.tsx b/packages/dashboard/src/components/Mnemonic/MnemonicReveal.tsx index d96a658d7a2d..f105282e0ca3 100644 --- a/packages/dashboard/src/components/Mnemonic/MnemonicReveal.tsx +++ b/packages/dashboard/src/components/Mnemonic/MnemonicReveal.tsx @@ -16,15 +16,20 @@ const WordCard = styled(Typography)( export interface MnemonicRevealProps { words: string[] + indexed?: boolean + wordClass?: string } export function MnemonicReveal(props: MnemonicRevealProps) { - const { words } = props + const { words, indexed, wordClass } = props return ( {words.map((item, index) => ( - {item} + + {indexed ? `${index + 1}. ` : ''} + {item} + ))} diff --git a/packages/dashboard/src/components/RegisterFrame/ButtonContainer.tsx b/packages/dashboard/src/components/RegisterFrame/ButtonContainer.tsx index 78015e886a6d..e879b9fd64af 100644 --- a/packages/dashboard/src/components/RegisterFrame/ButtonContainer.tsx +++ b/packages/dashboard/src/components/RegisterFrame/ButtonContainer.tsx @@ -18,7 +18,7 @@ interface ButtonGroupProps extends PropsWithChildren<{}> {} export const ButtonContainer = ({ children }: ButtonGroupProps) => { return ( - + {children} ) diff --git a/packages/dashboard/src/components/Restore/RestoreFromMnemonic.tsx b/packages/dashboard/src/components/Restore/RestoreFromMnemonic.tsx index a25c83924901..0183a771b83f 100644 --- a/packages/dashboard/src/components/Restore/RestoreFromMnemonic.tsx +++ b/packages/dashboard/src/components/Restore/RestoreFromMnemonic.tsx @@ -8,7 +8,7 @@ import { MaskAlert } from '../MaskAlert' import { ButtonContainer } from '../RegisterFrame/ButtonContainer' import { Services } from '../../API' import { PersonaContext } from '../../pages/Personas/hooks/usePersonaContext' -import { DashboardRoutes } from '@masknet/shared-base' +import { DashboardRoutes, delay } from '@masknet/shared-base' import { useNavigate } from 'react-router-dom' import { useState } from 'react' import { SignUpRoutePath } from '../../pages/SignUp/routePath' @@ -36,9 +36,11 @@ export const RestoreFromMnemonic = () => { const persona = await Services.Identity.queryPersonaByMnemonic(values.join(' '), '') if (persona) { await changeCurrentPersona(persona.identifier) + // Waiting persona changed event notify + await delay(100) navigate(DashboardRoutes.Personas, { replace: true }) } else { - navigate(`${DashboardRoutes.SignUp}/${SignUpRoutePath.PersonaCreate}`, { + navigate(`${DashboardRoutes.SignUp}/${SignUpRoutePath.PersonaRecovery}`, { replace: false, state: { mnemonic: values }, }) diff --git a/packages/dashboard/src/components/Restore/RestoreFromPrivateKey.tsx b/packages/dashboard/src/components/Restore/RestoreFromPrivateKey.tsx index c8ad1cfb6b53..ff0738ddd69a 100644 --- a/packages/dashboard/src/components/Restore/RestoreFromPrivateKey.tsx +++ b/packages/dashboard/src/components/Restore/RestoreFromPrivateKey.tsx @@ -46,7 +46,7 @@ export const RestoreFromPrivateKey = memo(() => { await delay(100) navigate(DashboardRoutes.Personas) } else { - navigate(`${DashboardRoutes.SignUp}/${SignUpRoutePath.PersonaCreate}`, { + navigate(`${DashboardRoutes.SignUp}/${SignUpRoutePath.PersonaRecovery}`, { replace: false, state: { privateKey: data.privateKey }, }) diff --git a/packages/dashboard/src/hooks/useMnemonicWordsPuzzle.ts b/packages/dashboard/src/hooks/useMnemonicWordsPuzzle.ts index 37f9f08c5f57..c9770fb696e9 100644 --- a/packages/dashboard/src/hooks/useMnemonicWordsPuzzle.ts +++ b/packages/dashboard/src/hooks/useMnemonicWordsPuzzle.ts @@ -60,5 +60,5 @@ export function useMnemonicWordsPuzzle() { const refreshCallback = wordsRetry - return [words, puzzleWords, indexes, answerCallback, resetCallback, refreshCallback] as const + return { words, puzzleWords, indexes, answerCallback, resetCallback, refreshCallback } as const } diff --git a/packages/dashboard/src/locales/en-US.json b/packages/dashboard/src/locales/en-US.json index 4ee7cbf04ddf..119b69668b6b 100644 --- a/packages/dashboard/src/locales/en-US.json +++ b/packages/dashboard/src/locales/en-US.json @@ -21,6 +21,10 @@ "close": "Close", "send": "Send", "resend": "Resend", + "print": "Print", + "download": "Download", + "print_preview": "Print Preview", + "download_preview": "Download Preview", "confirm_password": "Confirm Password", "about_dialog_license": "License: ", "footer_bounty_list": "Bounty List", @@ -36,9 +40,15 @@ "setup_page_create_restore_title": "Restoring from Identity or Backups", "setup_page_create_restore_subtitle": "Restoring from identity & historical backups.", "setup_page_create_restore_button": "Recovery or Sign In", + "create_account_mask_id": "MASK ID", + "create_account_private_key": "Private Key", + "create_account_identity_id": "Identity ID", "create_account_identity_title": "Create an Identity for Mask Network", "create_account_sign_in_button": "Recovery", - "create_account_identity_warning": "Please don’t forget your identity code. Your identity code is the only proof of your user identity and the only basis for you to encrypt or decrypt social activities.", + "create_account_persona_exists": "Persona already exists.", + "create_account_identity_warning": "Download your idendity ID to backup safely.", + "create_account_mnemonic_download_or_print": "I’ve downloaded it or printed it ", + "create_account_preview_tip": "This QR saves your identity code, please keep it safely. You can use Mask APP to scan QR to login persona.", "create_account_mnemonic_confirm_failed": "Incorrect identity code", "create_account_connect_social_media_button": "Create", "create_account_connect_social_media": "Connect to {{type}}", diff --git a/packages/dashboard/src/pages/CreateMaskWallet/components/CreateMnemonic/index.tsx b/packages/dashboard/src/pages/CreateMaskWallet/components/CreateMnemonic/index.tsx index e0cbf00d6390..a5e5056087c8 100644 --- a/packages/dashboard/src/pages/CreateMaskWallet/components/CreateMnemonic/index.tsx +++ b/packages/dashboard/src/pages/CreateMaskWallet/components/CreateMnemonic/index.tsx @@ -79,7 +79,7 @@ const CreateMnemonic = memo(() => { const location = useLocation() as { search: Search; state: { password: string } } const navigate = useNavigate() const [open, setOpen] = useState(false) - const [words, puzzleWords, indexes, answerCallback, resetCallback, refreshCallback] = useMnemonicWordsPuzzle() + const { words, puzzleWords, indexes, answerCallback, resetCallback, refreshCallback } = useMnemonicWordsPuzzle() const [searchParams] = useSearchParams() const { value: hasPassword, loading, retry } = useAsyncRetry(PluginServices.Wallet.hasPassword, []) diff --git a/packages/dashboard/src/pages/SignUp/routePath.ts b/packages/dashboard/src/pages/SignUp/routePath.ts index 0a5b1f235311..54dc1174cc3d 100644 --- a/packages/dashboard/src/pages/SignUp/routePath.ts +++ b/packages/dashboard/src/pages/SignUp/routePath.ts @@ -1,5 +1,6 @@ export enum SignUpRoutePath { MnemonicReveal = 'mnemonic-reveal', PersonaCreate = 'persona-create', + PersonaRecovery = 'persona-recovery', ConnectSocialMedia = 'connect-social-media', } diff --git a/packages/dashboard/src/pages/SignUp/routes.tsx b/packages/dashboard/src/pages/SignUp/routes.tsx index f9e7cf77ecc4..88029644a340 100644 --- a/packages/dashboard/src/pages/SignUp/routes.tsx +++ b/packages/dashboard/src/pages/SignUp/routes.tsx @@ -1,5 +1,5 @@ import { Route, Routes, useParams } from 'react-router-dom' -import { ConnectSocialMedia, MnemonicRevealForm, PersonaCreate } from './steps' +import { ConnectSocialMedia, MnemonicRevealForm, PersonaCreate, PersonaRecovery } from './steps' import { SignUpRoutePath } from './routePath' const Actions = () => { @@ -10,6 +10,8 @@ const Actions = () => { return case SignUpRoutePath.PersonaCreate: return + case SignUpRoutePath.PersonaRecovery: + return case SignUpRoutePath.ConnectSocialMedia: return default: @@ -21,7 +23,7 @@ export const SignUpRoutes = () => { return ( } /> - } /> + } /> ) } diff --git a/packages/dashboard/src/pages/SignUp/steps/MnemonicRevealForm.tsx b/packages/dashboard/src/pages/SignUp/steps/MnemonicRevealForm.tsx index 8e7d62b744dc..b6b3bc72e741 100644 --- a/packages/dashboard/src/pages/SignUp/steps/MnemonicRevealForm.tsx +++ b/packages/dashboard/src/pages/SignUp/steps/MnemonicRevealForm.tsx @@ -1,10 +1,9 @@ -import { memo, useState } from 'react' -import { some } from 'lodash-unified' -import { useNavigate } from 'react-router-dom' -import { Button, Stack, Box } from '@mui/material' +import { memo, useEffect, useState } from 'react' +import { useLocation, useNavigate } from 'react-router-dom' +import { Button, Stack, Box, IconButton } from '@mui/material' import RefreshIcon from '@mui/icons-material/Refresh' -import { useCustomSnackbar } from '@masknet/theme' -import { DashboardRoutes } from '@masknet/shared-base' +import { MaskColorVar, useCustomSnackbar } from '@masknet/theme' +import { DashboardRoutes, ECKeyIdentifier } from '@masknet/shared-base' import { MaskAlert } from '../../../components/MaskAlert' import { Header } from '../../../components/RegisterFrame/ColumnContentHeader' import { @@ -14,52 +13,102 @@ import { SignUpAccountLogo, } from '../../../components/RegisterFrame/ColumnContentLayout' import { useDashboardI18N } from '../../../locales' -import { DesktopMnemonicConfirm, MnemonicReveal } from '../../../components/Mnemonic' +import { MnemonicReveal } from '../../../components/Mnemonic' import { SignUpRoutePath } from '../routePath' import { ButtonContainer } from '../../../components/RegisterFrame/ButtonContainer' import { useMnemonicWordsPuzzle } from '../../../hooks/useMnemonicWordsPuzzle' - -enum CreateWalletStep { - NameAndWords = 0, - Verify = 1, -} +import { useCreatePersonaV2 } from '../../../hooks/useCreatePersonaV2' +import { PersonaContext } from '../../Personas/hooks/usePersonaContext' +import { Services } from '../../../API' +import PrintIcon from '@mui/icons-material/Print' +import { PreviewDialog } from './PreviewDialog' +import { DownloadIcon } from '@masknet/icons' +import { useAsync } from 'react-use' export const MnemonicRevealForm = memo(() => { + const createPersona = useCreatePersonaV2() + const { changeCurrentPersona } = PersonaContext.useContainer() const t = useDashboardI18N() const navigate = useNavigate() + const { state } = useLocation() as { state: { personaName: string } } const { showSnackbar } = useCustomSnackbar() - const [words, puzzleWords, indexes, answerCallback, resetCallback, refreshCallback] = useMnemonicWordsPuzzle() + const { words, refreshCallback } = useMnemonicWordsPuzzle() + const [preview, setPreview] = useState<{ + open: boolean + type: 'print' | 'download' + }>({ + open: false, + type: 'print', + }) + const [id, setId] = useState(null) + const [privateKey, setPrivateKey] = useState('') - const [step, setStep] = useState(CreateWalletStep.NameAndWords) + const create = async () => { + try { + const identifier = await createPersona(words.join(' '), state.personaName) + setId(identifier) + const privateKey = await Services.Identity.exportPersonaPrivateKey(identifier) + setPrivateKey(privateKey) - const onSubmit = async () => { - if (words.join(' ') !== puzzleWords.join(' ')) { - showSnackbar(t.create_account_mnemonic_confirm_failed(), { variant: 'error' }) - } else { - navigate(SignUpRoutePath.PersonaCreate, { - replace: true, - state: { mnemonic: words }, - }) + await changeCurrentPersona(identifier) + } catch (error) { + showSnackbar((error as Error).message, { variant: 'error' }) } } - const onBack = () => { - setStep(CreateWalletStep.NameAndWords) - resetCallback() + const onConfirm = async () => { + if (!id) { + await create() + } + + navigate(`${DashboardRoutes.SignUp}/${SignUpRoutePath.ConnectSocialMedia}`) + } + + const onPreview = async (type: 'print' | 'download') => { + if (!id) { + await create() + } + + setPreview({ + open: true, + type, + }) } + useEffect(() => { + // handle refresh words + if (!id) return + + setId(null) + Services.Identity.deletePersona(id, 'delete even with private') + }, [words]) + + useAsync(async () => { + // handle refresh page after create + const personas = await Services.Identity.queryMyPersonas() + const persona = personas.find((p) => p.nickname === state.personaName) + + if (!persona) return + + Services.Identity.deletePersona(persona.identifier, 'delete even with private') + }, []) + return ( - -
navigate(DashboardRoutes.SignIn) }} - /> - - - - - {step === CreateWalletStep.NameAndWords && ( - <> + <> + +
navigate(DashboardRoutes.SignIn), + }} + /> + + + + + + { - - - - )} - {step === CreateWalletStep.Verify && ( - <> - - - - + onPreview('print')}> + + + onPreview('download')}> + + - - )} - - - - - + + + + + + + + setPreview({ ...preview, open: false })} + personaName={state.personaName} + id={id?.toText()} + words={words} + privateKey={privateKey} + /> + ) }) diff --git a/packages/dashboard/src/pages/SignUp/steps/PersonaCreate.tsx b/packages/dashboard/src/pages/SignUp/steps/PersonaCreate.tsx index 2cf50b31f80f..008ca624dd08 100644 --- a/packages/dashboard/src/pages/SignUp/steps/PersonaCreate.tsx +++ b/packages/dashboard/src/pages/SignUp/steps/PersonaCreate.tsx @@ -1,103 +1,34 @@ -import { useCallback, useEffect, useState } from 'react' -import { useLocation, useNavigate } from 'react-router-dom' -import { MaskTextField, useCustomSnackbar } from '@masknet/theme' -import { - Body, - ColumnContentLayout, - Footer, - PersonaLogoBox, - SignUpAccountLogo, -} from '../../../components/RegisterFrame/ColumnContentLayout' -import { DashboardRoutes, delay } from '@masknet/shared-base' -import { Header } from '../../../components/RegisterFrame/ColumnContentHeader' -import { Box, Typography } from '@mui/material' +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { DashboardRoutes } from '@masknet/shared-base' import { useDashboardI18N } from '../../../locales' import { SignUpRoutePath } from '../routePath' -import { useCreatePersonaByPrivateKey, useCreatePersonaV2 } from '../../../hooks/useCreatePersonaV2' import { Services } from '../../../API' -import { ButtonContainer } from '../../../components/RegisterFrame/ButtonContainer' -import { PersonaContext } from '../../Personas/hooks/usePersonaContext' -import { LoadingButton } from '../../../components/LoadingButton' - -const Label = ({ value }: { value: string }) => ( - theme.palette.primary.main }}> - {value} - -) +import { PersonaNameUI } from './PersonaNameUI' export const PersonaCreate = () => { const t = useDashboardI18N() const navigate = useNavigate() - const createPersona = useCreatePersonaV2() - const createPersonaByPrivateKey = useCreatePersonaByPrivateKey() - const { showSnackbar } = useCustomSnackbar() - const { changeCurrentPersona } = PersonaContext.useContainer() - const { state } = useLocation() as { state: { mnemonic: string[]; privateKey: string } } - const [personaName, setPersonaName] = useState('') const [error, setError] = useState('') - useEffect(() => { - if ( - (!state?.mnemonic || !Services.Identity.validateMnemonic(state?.mnemonic.join(' '))) && - !state?.privateKey - ) { - navigate(DashboardRoutes.SignUp, { replace: true }) - } - }, [state?.mnemonic, state?.privateKey]) - - const create = useCallback(async () => { - try { - const identifier = state?.mnemonic - ? await createPersona(state?.mnemonic.join(' '), personaName) - : await createPersonaByPrivateKey(state?.privateKey, personaName) + const onNext = async (personaName: string) => { + setError('') - await changeCurrentPersona(identifier) - showSnackbar(t.create_account_persona_successfully(), { variant: 'success' }) + const personas = await Services.Identity.queryMyPersonas() + const existing = personas.some((x) => x.nickname === personaName) - await delay(300) - navigate(`${DashboardRoutes.SignUp}/${SignUpRoutePath.ConnectSocialMedia}`) - } catch (error) { - setError((error as Error).message) + if (existing) { + return setError(t.create_account_persona_exists()) } - }, [state?.mnemonic, state?.privateKey, personaName]) - return ( - -
navigate(DashboardRoutes.SignIn) }} - /> - - - - - - } - InputProps={{ disableUnderline: true }} - onChange={(e) => setPersonaName(e.currentTarget.value)} - inputProps={{ maxLength: 24 }} - error={!!error} - helperText={error} - /> - - - {t.next()} - - - - -