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 && (
- <>
-
-
-
-
- >
- )}
-
-
-
-
-
+
+
+
+
+
+
+
+ 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()}
-
-
-
-
-
-
- )
+ navigate(`${DashboardRoutes.SignUp}/${SignUpRoutePath.MnemonicReveal}`, {
+ replace: true,
+ state: {
+ personaName,
+ },
+ })
+ }
+
+ return
}
diff --git a/packages/dashboard/src/pages/SignUp/steps/PersonaNameUI.tsx b/packages/dashboard/src/pages/SignUp/steps/PersonaNameUI.tsx
new file mode 100644
index 000000000000..2582ef43ac41
--- /dev/null
+++ b/packages/dashboard/src/pages/SignUp/steps/PersonaNameUI.tsx
@@ -0,0 +1,81 @@
+import { useEffect, useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { MaskTextField } from '@masknet/theme'
+import {
+ Body,
+ ColumnContentLayout,
+ Footer,
+ PersonaLogoBox,
+ SignUpAccountLogo,
+} from '../../../components/RegisterFrame/ColumnContentLayout'
+import { DashboardRoutes } from '@masknet/shared-base'
+import { Header } from '../../../components/RegisterFrame/ColumnContentHeader'
+import { Box, Button, Typography } from '@mui/material'
+import { useDashboardI18N } from '../../../locales'
+import { ButtonContainer } from '../../../components/RegisterFrame/ButtonContainer'
+
+const Label = ({ value }: { value: string }) => (
+ theme.palette.primary.main }}>
+ {value}
+
+)
+
+export interface PersonaNameUIProps {
+ error?: string
+ onNext(personaName: string): void
+}
+
+export const PersonaNameUI = ({ onNext, error }: PersonaNameUIProps) => {
+ const t = useDashboardI18N()
+ const navigate = useNavigate()
+
+ const [personaName, setPersonaName] = useState('')
+ const [helper, setHelper] = useState('')
+
+ useEffect(() => {
+ setHelper('')
+ }, [personaName])
+
+ useEffect(() => {
+ if (error) setHelper(error)
+ }, [error])
+
+ return (
+
+ navigate(DashboardRoutes.SignIn) }}
+ />
+
+
+
+
+
+ }
+ InputProps={{ disableUnderline: true }}
+ onChange={(e) => setPersonaName(e.currentTarget.value)}
+ inputProps={{ maxLength: 24 }}
+ error={!!helper}
+ helperText={helper}
+ />
+
+ onNext(personaName)}
+ disabled={!personaName}>
+ {t.next()}
+
+
+
+
+
+
+ )
+}
diff --git a/packages/dashboard/src/pages/SignUp/steps/PersonaRecovery.tsx b/packages/dashboard/src/pages/SignUp/steps/PersonaRecovery.tsx
new file mode 100644
index 000000000000..85fa12b3845a
--- /dev/null
+++ b/packages/dashboard/src/pages/SignUp/steps/PersonaRecovery.tsx
@@ -0,0 +1,56 @@
+import { useCallback, useState } from 'react'
+import { useLocation, useNavigate } from 'react-router-dom'
+import { useCustomSnackbar } from '@masknet/theme'
+import { DashboardRoutes, delay } from '@masknet/shared-base'
+import { useDashboardI18N } from '../../../locales'
+import { SignUpRoutePath } from '../routePath'
+import { Services } from '../../../API'
+import { PersonaNameUI } from './PersonaNameUI'
+import { useCreatePersonaByPrivateKey, useCreatePersonaV2 } from '../../../hooks/useCreatePersonaV2'
+import { PersonaContext } from '../../Personas/hooks/usePersonaContext'
+import { useAsync } from 'react-use'
+
+export const PersonaRecovery = () => {
+ 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 [error, setError] = useState('')
+
+ useAsync(async () => {
+ if (
+ // eslint-disable-next-line @typescript-eslint/await-thenable
+ (!state?.mnemonic || (await !Services.Identity.validateMnemonic(state?.mnemonic.join(' ')))) &&
+ !state?.privateKey
+ ) {
+ navigate(DashboardRoutes.SignUp, { replace: true })
+ }
+ }, [state?.mnemonic, state?.privateKey])
+
+ const onNext = useCallback(
+ async (personaName: string) => {
+ setError('')
+ try {
+ const identifier = state?.mnemonic
+ ? await createPersona(state?.mnemonic.join(' '), personaName)
+ : await createPersonaByPrivateKey(state?.privateKey, personaName)
+
+ await changeCurrentPersona(identifier)
+ showSnackbar(t.create_account_persona_successfully(), { variant: 'success' })
+
+ await delay(300)
+ navigate(`${DashboardRoutes.SignUp}/${SignUpRoutePath.ConnectSocialMedia}`)
+ } catch (error) {
+ setError((error as Error).message)
+ }
+ },
+ [state?.mnemonic, state?.privateKey],
+ )
+
+ return
+}
diff --git a/packages/dashboard/src/pages/SignUp/steps/PreviewDialog.tsx b/packages/dashboard/src/pages/SignUp/steps/PreviewDialog.tsx
new file mode 100644
index 000000000000..556f93108cd5
--- /dev/null
+++ b/packages/dashboard/src/pages/SignUp/steps/PreviewDialog.tsx
@@ -0,0 +1,162 @@
+import { QRCode } from 'react-qrcode-logo'
+import { makeStyles, MaskDialog, MaskColorVar, MaskLightTheme } from '@masknet/theme'
+import { Box, Button, DialogContent, ThemeProvider, Typography } from '@mui/material'
+import { MnemonicReveal } from '../../../components/Mnemonic'
+import { MiniMaskIcon, InfoIcon } from '@masknet/icons'
+import { ForwardedRef, forwardRef, useRef } from 'react'
+import { useReactToPrint } from 'react-to-print'
+import { toJpeg } from 'html-to-image'
+import { WatermarkURL } from '../../../assets'
+import { useDashboardI18N } from '../../../locales'
+
+const useStyles = makeStyles()((theme) => ({
+ preview: {
+ position: 'relative',
+ background: `url(${WatermarkURL}) repeat`,
+ backgroundSize: '141px',
+ },
+ wordClass: {
+ background: 'rgba(28, 104, 243, 0.1)',
+ color: '#1C68F3',
+ },
+ card: {
+ background: 'linear-gradient(180deg, #003EAF 0%, #1C68F3 100%)',
+ borderRadius: theme.spacing(1),
+ padding: theme.spacing(2),
+ color: '#fff',
+ },
+ name: {
+ maxWidth: 350,
+ paddingLeft: 10,
+ overflow: 'hidden',
+ whiteSpace: 'nowrap',
+ textOverflow: 'ellipsis',
+ },
+ infoIcon: {
+ color: MaskColorVar.secondaryInfoText,
+ fontSize: 24,
+ marginRight: 12,
+ },
+}))
+
+interface PreviewDialogProps {
+ open: boolean
+ type: 'print' | 'download'
+ personaName: string
+ id?: string
+ privateKey: string
+ words: string[]
+ onClose(): void
+}
+
+export function PreviewDialog(props: PreviewDialogProps) {
+ const { personaName, open, type, onClose } = props
+ const t = useDashboardI18N()
+ const ref = useRef(null)
+
+ const onPrint = useReactToPrint({
+ content: () => ref.current,
+ })
+
+ const onDownload = async () => {
+ if (!ref.current) return
+
+ const dataUrl = await toJpeg(ref.current, { quality: 0.95 })
+ const link = document.createElement('a')
+ link.download = `mask-persona-${personaName}.jpeg`
+ link.href = dataUrl
+ link.click()
+ }
+
+ const onClick = async () => {
+ type === 'print' ? onPrint() : await onDownload()
+ onClose()
+ }
+
+ return (
+
+
+
+
+
+
+
+ {type === 'print' ? t.print() : t.download()}
+
+
+
+
+
+ )
+}
+
+const ComponentToPrint = forwardRef((props: PreviewDialogProps, ref: ForwardedRef) => {
+ const { personaName, id, privateKey, words } = props
+ const { classes } = useStyles()
+ const t = useDashboardI18N()
+
+ return (
+
+
+
+
+
+
+
+ {t.persona()}: {personaName}
+
+
+
+
+
+ {t.create_account_mask_id()}
+
+
+ {id?.replace('ec_key:secp256k1/', '')}
+
+
+
+
+
+ {t.create_account_private_key()}
+
+
+ {privateKey}
+
+
+
+
+
+
+ {t.create_account_identity_id()}
+
+
+
+
+
+ {t.create_account_preview_tip()}
+
+
+
+
+ )
+})
diff --git a/packages/dashboard/src/pages/SignUp/steps/index.ts b/packages/dashboard/src/pages/SignUp/steps/index.ts
index 152a1605c6c4..41cfe2ded6c0 100644
--- a/packages/dashboard/src/pages/SignUp/steps/index.ts
+++ b/packages/dashboard/src/pages/SignUp/steps/index.ts
@@ -1,3 +1,4 @@
export * from './MnemonicRevealForm'
export * from './PersonaCreate'
export * from './ConnectSocialMedia'
+export * from './PersonaRecovery'
diff --git a/packages/mask/src/utils/native-rpc/Web.ts b/packages/mask/src/utils/native-rpc/Web.ts
index fe4325d365e6..d1279b0a8511 100644
--- a/packages/mask/src/utils/native-rpc/Web.ts
+++ b/packages/mask/src/utils/native-rpc/Web.ts
@@ -237,6 +237,10 @@ export const MaskNetworkAPI: MaskNetworkAPIs = {
const privateKey = await Services.Identity.exportPersonaPrivateKey(stringToPersonaIdentifier(identifier))
return privateKey
},
+ persona_queryPersonaByPrivateKey: async ({ privateKey }) => {
+ const persona = await Services.Identity.queryPersonaByPrivateKey(privateKey)
+ return persona ? personaFormatter(persona) : undefined
+ },
persona_getCurrentPersonaIdentifier: async () => {
const identifier = await Services.Settings.getCurrentPersonaIdentifier()
return identifier?.toText()
diff --git a/packages/public-api/src/web.ts b/packages/public-api/src/web.ts
index 80fcfc1e5fd3..575f3eb8bdf2 100644
--- a/packages/public-api/src/web.ts
+++ b/packages/public-api/src/web.ts
@@ -57,6 +57,7 @@ export interface MaskNetworkAPIs {
persona_backupBase64(params: { identifier: PersonaIdentifier_string }): Promise
persona_backupJson(params: { identifier: PersonaIdentifier_string }): Promise
persona_backupPrivateKey(params: { identifier: PersonaIdentifier_string }): Promise
+ persona_queryPersonaByPrivateKey(params: { privateKey: string }): Promise
persona_getCurrentPersonaIdentifier(): Promise
persona_setCurrentPersonaIdentifier(params: { identifier: PersonaIdentifier_string }): Promise
persona_getOwnedPersonaInformation(params: { identifier: PersonaIdentifier_string }): Promise
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e2dfaa52d509..fd8dbe29e178 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -159,13 +159,16 @@ importers:
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
path-browserify: ^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
stream-browserify: ^3.0.0
unstated-next: ^1.1.0
@@ -202,12 +205,15 @@ importers:
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_2a5ce53653c2861b1c74a6612fe40887
react-hook-form: 7.22.5_react@18.0.0-rc.0
+ react-qrcode-logo: 2.5.0_757a802188413a36d4f24237d13b8e90
react-router: 6.0.0-beta.0_history@5.2.0+react@18.0.0-rc.0
react-router-dom: 6.0.0-beta.0_154c53788755629e2430b224cbac559e
+ react-to-print: 2.14.3_757a802188413a36d4f24237d13b8e90
react-use: 17.3.1_757a802188413a36d4f24237d13b8e90
unstated-next: 1.1.0
urlcat: 2.0.4
@@ -1306,7 +1312,6 @@ packages:
/@babel/compat-data/7.16.4:
resolution: {integrity: sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q==}
engines: {node: '>=6.9.0'}
- dev: true
/@babel/core/7.12.9:
resolution: {integrity: sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ==}
@@ -3456,6 +3461,7 @@ packages:
/@dimensiondev/eslint-plugin/0.0.1-20220117062517-fd3cb01_eslint@8.5.0:
resolution: {integrity: sha512-WtUberb1MC1s554Lm3C3mno3Nf/TdVJP80htgpNWiGOcIk+WYwQi+gWqi5IlnqGd71bYRZPYpYs2MH+GS+dbuw==, tarball: download/@dimensiondev/eslint-plugin/0.0.1-20220117062517-fd3cb01/8298f20d707432bcb777cd5084173fc894362a3496e8f8bc6a5c85070adb77cf}
+ engines: {node: '>= 14'}
peerDependencies:
'@typescript-eslint/experimental-utils': '*'
eslint: '>= 8'
@@ -6302,7 +6308,10 @@ packages:
react-textarea-autosize: 8.3.3_2fa291bfae6e56080648438396754a97
regenerator-runtime: 0.13.9
ts-dedent: 2.2.0
+ typescript: 4.6.0-dev.20211202
+ unfetch: 4.2.0
util-deprecate: 1.0.2
+ webpack: 4.46.0
transitivePeerDependencies:
- '@types/react'
dev: true
@@ -10012,7 +10021,7 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/compat-data': 7.16.0
+ '@babel/compat-data': 7.16.4
'@babel/core': 7.16.5
'@babel/helper-define-polyfill-provider': 0.2.4_@babel+core@7.16.5
semver: 6.3.0
@@ -15756,6 +15765,10 @@ packages:
engines: {node: '>=8'}
dev: true
+ /html-to-image/1.9.0:
+ resolution: {integrity: sha512-9gaDCIYg62Ek07F2pBk76AHgYZ2gxq2YALU7rK3gNCqXuhu6cWzsOQqM7qGbjZiOzxGzrU1deDqZpAod2NEwbA==}
+ dev: false
+
/html-tokenize/2.0.1:
resolution: {integrity: sha512-QY6S+hZ0f5m1WT8WffYN+Hg+xm/w5I8XeUcAq/ZYP5wVC8xbKi4Whhru3FtrAebD5EhBW8rmFzkDI6eCAuFe2w==}
hasBin: true
@@ -20191,6 +20204,10 @@ packages:
engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
dev: true
+ /qrcode-generator/1.4.4:
+ resolution: {integrity: sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==}
+ dev: false
+
/qrcode/1.5.0:
resolution: {integrity: sha512-9MgRpgVc+/+47dFvQeD6U2s0Z92EsKzcHogtum4QB+UNd025WOJSHvn/hjk9xmzj7Stj95CyUAs31mrjxliEsQ==}
engines: {node: '>=10.13.0'}
@@ -20556,6 +20573,18 @@ packages:
warning: 4.0.3
dev: true
+ /react-qrcode-logo/2.5.0_757a802188413a36d4f24237d13b8e90:
+ resolution: {integrity: sha512-Z+Ywb2Cw3EK5csfkZHuyNpxZl5BiHxrBPAeRE3bB1qERAUMc/tMzBKpfMEb/Olti/Ic04j/DYt6ncYRLa/wFMA==}
+ peerDependencies:
+ react: '>=16.4.1'
+ react-dom: '>=16.4.1'
+ dependencies:
+ lodash.isequal: 4.5.0
+ qrcode-generator: 1.4.4
+ react: 18.0.0-rc.0
+ react-dom: 18.0.0-rc.0_react@18.0.0-rc.0
+ dev: false
+
/react-refresh/0.10.0:
resolution: {integrity: sha512-PgidR3wST3dDYKr6b4pJoqQFpPGNKDSCDx4cZoshjXipw3LzO7mG1My2pwEzz2JVkF+inx3xRpDeQLFQGH/hsQ==}
engines: {node: '>=0.10.0'}
@@ -20681,6 +20710,17 @@ packages:
- '@types/react'
dev: true
+ /react-to-print/2.14.3_757a802188413a36d4f24237d13b8e90:
+ resolution: {integrity: sha512-j0EAplQGcw4ujSLfUQoIhf1PgrSLrWRh4NSAiKZdWm6YNXMioHW56cGpbKEJkCpYN1E5g3tvFVHRADn0mTKGzg==}
+ peerDependencies:
+ react: ^15.0.0 || ^16.0.0 || ^17.0.0
+ react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0
+ dependencies:
+ prop-types: 15.8.0
+ react: 18.0.0-rc.0
+ react-dom: 18.0.0-rc.0_react@18.0.0-rc.0
+ dev: false
+
/react-transition-group/4.4.2_757a802188413a36d4f24237d13b8e90:
resolution: {integrity: sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==}
peerDependencies: