diff --git a/packages/icons/brands/Blocto.tsx b/packages/icons/brands/Blocto.tsx new file mode 100644 index 000000000000..475355bf36dd --- /dev/null +++ b/packages/icons/brands/Blocto.tsx @@ -0,0 +1,49 @@ +import { createIcon } from '../utils' +import type { SvgIcon } from '@mui/material' + +export const BloctoIcon: typeof SvgIcon = createIcon( + 'Blocto', + + + + + + + + + + + + + + + + + + + + + , + '0 0 43 50', +) diff --git a/packages/icons/brands/index.ts b/packages/icons/brands/index.ts index 107479bd226b..de5508807f7f 100644 --- a/packages/icons/brands/index.ts +++ b/packages/icons/brands/index.ts @@ -12,3 +12,4 @@ export * from './MaskGrey' export * from './Discord' export * from './MaskBanner' export * from './MaskPlaceholder' +export * from './Blocto' diff --git a/packages/mask/src/components/shared/VerifyWallet/CurrentWalletBox.tsx b/packages/mask/src/components/shared/VerifyWallet/CurrentWalletBox.tsx new file mode 100644 index 000000000000..6feb2f06b437 --- /dev/null +++ b/packages/mask/src/components/shared/VerifyWallet/CurrentWalletBox.tsx @@ -0,0 +1,216 @@ +import { useCallback } from 'react' +import { ExternalLink } from 'react-feather' +import classNames from 'classnames' +import { ProviderType } from '@masknet/web3-shared-evm' +import { Button, Link, Typography } from '@mui/material' +import { makeStyles } from '@masknet/theme' +import { useRemoteControlledDialog } from '@masknet/shared-base-ui' +import { + useAccount, + useWeb3State, + useChainId, + useNetworkDescriptor, + useProviderDescriptor, + useProviderType, + useReverseAddress, + useWallet, +} from '@masknet/plugin-infra/web3' +import { FormattedAddress, WalletIcon } from '@masknet/shared' +import { WalletMessages } from '../../../plugins/Wallet/messages' +import { useI18N } from '../../../utils' +import Services from '../../../extension/service' +import { ActionButtonPromise } from '../../../extension/options-page/DashboardComponents/ActionButton' + +const useStyles = makeStyles()((theme) => ({ + currentAccount: { + padding: '12px 8px', + width: '100%', + background: '#fff', + boxSizing: 'border-box', + marginBottom: theme.spacing(2), + display: 'flex', + borderRadius: 8, + alignItems: 'center', + }, + + accountInfo: { + flexGrow: 1, + marginLeft: theme.spacing(1), + }, + accountName: { + fontSize: 16, + marginRight: 4, + fontWeight: 'bold', + }, + infoRow: { + display: 'flex', + alignItems: 'center', + }, + actionButton: { + fontSize: 14, + marginLeft: theme.spacing(1), + padding: '8px 12px', + fontWeight: 'bold', + background: 'rgba(15, 20, 25, 1)', + color: '#fff', + minWidth: 70, + borderRadius: '6px', + }, + address: { + fontSize: 12, + marginRight: theme.spacing(1), + display: 'inline-block', + color: theme.palette.text.secondary, + }, + link: { + color: theme.palette.text.primary, + fontSize: 14, + display: 'flex', + alignItems: 'center', + }, + linkIcon: { + marginRight: theme.spacing(1), + }, + twitterProviderBorder: { + width: 14, + height: 14, + }, + connectButtonWrapper: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + margin: theme.spacing(2, 0), + }, + domain: { + fontSize: 16, + lineHeight: '18px', + marginLeft: 6, + padding: 4, + borderRadius: 8, + backgroundColor: '#ffffff', + color: theme.palette.common.black, + }, +})) +interface CurrentWalletBox { + isDashboard?: boolean + disableChange?: boolean +} +export function CurrentWalletBox(props: CurrentWalletBox) { + const { t } = useI18N() + const { classes } = useStyles() + const chainId = useChainId() + const account = useAccount() + const wallet = useWallet() + const providerType = useProviderType() + const providerDescriptor = useProviderDescriptor() + const networkDescriptor = useNetworkDescriptor() + const { Utils } = useWeb3State() ?? {} + + const { value: domain } = useReverseAddress(account) + + // #region change provider + const { openDialog: openSelectProviderDialog } = useRemoteControlledDialog( + WalletMessages.events.selectProviderDialogUpdated, + ) + // #endregion + + // #region walletconnect + const { setDialog: setWalletConnectDialog } = useRemoteControlledDialog( + WalletMessages.events.walletConnectQRCodeDialogUpdated, + ) + // #endregion + + const onDisconnect = useCallback(async () => { + switch (providerType) { + case ProviderType.WalletConnect: + setWalletConnectDialog({ + open: true, + uri: await Services.Ethereum.createConnectionURI(), + }) + break + case ProviderType.Fortmatic: + await Services.Ethereum.disconnectFortmatic(chainId) + break + } + }, [chainId, providerType, setWalletConnectDialog]) + + const onChange = useCallback(() => { + openSelectProviderDialog() + }, [openSelectProviderDialog]) + + return account ? ( +
+ +
+
+ {providerType !== ProviderType.MaskWallet ? ( + + {domain && Utils?.formatDomainName + ? Utils.formatDomainName(domain) + : providerDescriptor?.name} + + ) : ( + <> + + {wallet?.name ?? providerDescriptor?.name} + + {domain && Utils?.formatDomainName ? ( + {Utils.formatDomainName(domain)} + ) : null} + + )} +
+
+ + + + + + + +
+
+ {!props.disableChange && ( +
+ {providerType === ProviderType.WalletConnect || providerType === ProviderType.Fortmatic ? ( + } + failIcon={<>} + /> + ) : null} + +
+ )} +
+ ) : ( +
+ +
+ ) +} diff --git a/packages/mask/src/components/shared/VerifyWallet/Steps.tsx b/packages/mask/src/components/shared/VerifyWallet/Steps.tsx new file mode 100644 index 000000000000..566a677ebd47 --- /dev/null +++ b/packages/mask/src/components/shared/VerifyWallet/Steps.tsx @@ -0,0 +1,161 @@ +import { makeStyles } from '@masknet/theme' +import { CurrentWalletBox } from './CurrentWalletBox' +import { + step1ActiveIcon, + stepSuccessIcon, + dividerDisableIcon, + dividerSuccessIcon, + dividerActiveIcon, + step2DisableIcon, + step2ActiveIcon, +} from './constants' +import { ImageIcon } from '@masknet/shared' +import { Typography } from '@mui/material' +import ActionButton from '../../../extension/options-page/DashboardComponents/ActionButton' +import { useNavigate } from 'react-router-dom' + +const useStyles = makeStyles()((theme) => ({ + container: { + width: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }, + stepBox: { + marginTop: 12, + display: 'flex', + alignItems: 'flex-start', + flexGrow: 'grow', + }, + stepLine: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + marginRight: 12, + }, + stepRowBox: { + width: '100%', + display: 'flex', + flexDirection: 'column', + minHeight: 200, + }, + stepRow: { + color: theme.palette.text.secondary, + minHeight: 100, + }, + stepIntro: { + marginTop: 12, + fontSize: 14, + }, + divider: { + margin: '4px 0', + }, + actionBox: { + width: 'calc(100% - 32px)', + gap: 12, + position: 'fixed', + bottom: 16, + display: 'flex', + alignItems: 'center', + }, + roundBtn: { + borderRadius: 99, + }, +})) + +export enum SignSteps { + Ready = 0, + FirstStepDone = 1, + SecondStepDone = 2, +} + +interface StepsProps { + step: SignSteps + personaSign: () => void + walletSign: () => void +} + +export function Steps(props: StepsProps) { + const { classes } = useStyles() + const navigate = useNavigate() + const { step, personaSign, walletSign } = props + + const stepIconMap = { + [SignSteps.Ready]: { + step1: step1ActiveIcon, + divider: dividerDisableIcon, + step2: step2DisableIcon, + }, + [SignSteps.FirstStepDone]: { + step1: stepSuccessIcon, + divider: dividerActiveIcon, + step2: step2ActiveIcon, + }, + [SignSteps.SecondStepDone]: { + step1: stepSuccessIcon, + divider: dividerSuccessIcon, + step2: stepSuccessIcon, + }, + } + + const onConfirm = () => { + if (step === SignSteps.Ready) { + personaSign() + } else if (step === SignSteps.FirstStepDone) { + walletSign() + } else { + navigate(-1) + } + } + + const onCancel = () => { + navigate(-1) + } + + return ( +
+ +
+
+ + + +
+
+
+ Persona Name Sign + + Sign seamlessly with Persona, ensure the validity of data. + +
+
+ Wallet Name Sign + + After two steps, you will own, view, utilize all your cyber identities through Next.ID. You + can also disconnect them easily. + +
+
+
+
+ + Cancel + + + {step === 2 ? 'Done' : 'Confirm'} + +
+
+ ) +} diff --git a/packages/mask/src/components/shared/VerifyWallet/constants.ts b/packages/mask/src/components/shared/VerifyWallet/constants.ts new file mode 100644 index 000000000000..fe083043d000 --- /dev/null +++ b/packages/mask/src/components/shared/VerifyWallet/constants.ts @@ -0,0 +1,7 @@ +export const step1ActiveIcon = new URL('../assets/stepAssets/step1Active.png', import.meta.url) +export const step2DisableIcon = new URL('../assets/stepAssets/step2Disable.png', import.meta.url) +export const step2ActiveIcon = new URL('../assets/stepAssets/step2Active.png', import.meta.url) +export const stepSuccessIcon = new URL('../assets/stepAssets/stepSuccess.png', import.meta.url) +export const dividerDisableIcon = new URL('../assets/stepAssets/dividerDisable.png', import.meta.url) +export const dividerActiveIcon = new URL('../assets/stepAssets/dividerActive.png', import.meta.url) +export const dividerSuccessIcon = new URL('../assets/stepAssets/dividerDone.png', import.meta.url) diff --git a/packages/mask/src/components/shared/assets/stepAssets/dividerActive.png b/packages/mask/src/components/shared/assets/stepAssets/dividerActive.png new file mode 100644 index 000000000000..260dae99f2f0 Binary files /dev/null and b/packages/mask/src/components/shared/assets/stepAssets/dividerActive.png differ diff --git a/packages/mask/src/components/shared/assets/stepAssets/dividerDisable.png b/packages/mask/src/components/shared/assets/stepAssets/dividerDisable.png new file mode 100644 index 000000000000..3eed001bf264 Binary files /dev/null and b/packages/mask/src/components/shared/assets/stepAssets/dividerDisable.png differ diff --git a/packages/mask/src/components/shared/assets/stepAssets/dividerDone.png b/packages/mask/src/components/shared/assets/stepAssets/dividerDone.png new file mode 100644 index 000000000000..91f645ea7995 Binary files /dev/null and b/packages/mask/src/components/shared/assets/stepAssets/dividerDone.png differ diff --git a/packages/mask/src/components/shared/assets/stepAssets/step1Active.png b/packages/mask/src/components/shared/assets/stepAssets/step1Active.png new file mode 100644 index 000000000000..6ff3cf904013 Binary files /dev/null and b/packages/mask/src/components/shared/assets/stepAssets/step1Active.png differ diff --git a/packages/mask/src/components/shared/assets/stepAssets/step2Active.png b/packages/mask/src/components/shared/assets/stepAssets/step2Active.png new file mode 100644 index 000000000000..8fc56f653eb1 Binary files /dev/null and b/packages/mask/src/components/shared/assets/stepAssets/step2Active.png differ diff --git a/packages/mask/src/components/shared/assets/stepAssets/step2Disable.png b/packages/mask/src/components/shared/assets/stepAssets/step2Disable.png new file mode 100644 index 000000000000..98fd95280d71 Binary files /dev/null and b/packages/mask/src/components/shared/assets/stepAssets/step2Disable.png differ diff --git a/packages/mask/src/components/shared/assets/stepAssets/stepSuccess.png b/packages/mask/src/components/shared/assets/stepAssets/stepSuccess.png new file mode 100644 index 000000000000..032d8502b4a6 Binary files /dev/null and b/packages/mask/src/components/shared/assets/stepAssets/stepSuccess.png differ diff --git a/packages/mask/src/extension/popups/assets/blocto.svg b/packages/mask/src/extension/popups/assets/blocto.svg new file mode 100644 index 000000000000..646d48924816 --- /dev/null +++ b/packages/mask/src/extension/popups/assets/blocto.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/mask/src/extension/popups/assets/mask.svg b/packages/mask/src/extension/popups/assets/mask.svg new file mode 100644 index 000000000000..3666da4207ed --- /dev/null +++ b/packages/mask/src/extension/popups/assets/mask.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/packages/mask/src/extension/popups/assets/metamask.svg b/packages/mask/src/extension/popups/assets/metamask.svg new file mode 100644 index 000000000000..2a8c95af16a7 --- /dev/null +++ b/packages/mask/src/extension/popups/assets/metamask.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/mask/src/extension/popups/assets/walletconnect.svg b/packages/mask/src/extension/popups/assets/walletconnect.svg new file mode 100644 index 000000000000..764df3dbd43b --- /dev/null +++ b/packages/mask/src/extension/popups/assets/walletconnect.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/packages/mask/src/extension/popups/pages/Personas/index.tsx b/packages/mask/src/extension/popups/pages/Personas/index.tsx index e65200c4b638..a64919dcf9af 100644 --- a/packages/mask/src/extension/popups/pages/Personas/index.tsx +++ b/packages/mask/src/extension/popups/pages/Personas/index.tsx @@ -10,6 +10,7 @@ const Home = lazy(() => import('./Home')) const Logout = lazy(() => import('./Logout')) const PersonaRename = lazy(() => import('./Rename')) const PersonaSignRequest = lazy(() => import('./PersonaSignRequest')) +const VerifyWallet = lazy(() => import('../Wallet/VerifyWallet')) const SelectPersona = lazy(() => import('./SelectPersona')) const Accounts = lazy(() => import('./Accounts')) const AccountDetail = lazy(() => import('./AccountDetail')) @@ -29,6 +30,7 @@ const Persona = memo(() => { } /> } /> } /> + } /> } /> diff --git a/packages/mask/src/extension/popups/pages/Wallet/ConnectWallet/constants.ts b/packages/mask/src/extension/popups/pages/Wallet/ConnectWallet/constants.ts new file mode 100644 index 000000000000..ed1b39f91e7f --- /dev/null +++ b/packages/mask/src/extension/popups/pages/Wallet/ConnectWallet/constants.ts @@ -0,0 +1,8 @@ +const supportedWallets = [ + { title: 'MetaMask', icon: new URL('../../../assets/metamask.svg', import.meta.url).toString() }, + { title: 'Mask', icon: new URL('../../../assets/mask.svg', import.meta.url).toString() }, + { title: 'WalletConnect', icon: new URL('../../../assets/walletconnect.svg', import.meta.url).toString() }, + { title: 'Blocto', icon: new URL('../../../assets/blocto.svg', import.meta.url).toString() }, +] + +export { supportedWallets } diff --git a/packages/mask/src/extension/popups/pages/Wallet/ConnectWallet/index.tsx b/packages/mask/src/extension/popups/pages/Wallet/ConnectWallet/index.tsx new file mode 100644 index 000000000000..b9f1e4ebe884 --- /dev/null +++ b/packages/mask/src/extension/popups/pages/Wallet/ConnectWallet/index.tsx @@ -0,0 +1,60 @@ +import { makeStyles } from '@masknet/theme' +import { Typography } from '@mui/material' +import { memo } from 'react' +import { supportedWallets } from './constants' +import { useNavigate } from 'react-router-dom' +import { PopupRoutes } from '@masknet/shared-base' + +const useStyles = makeStyles()((theme) => ({ + container: { + width: '100%', + display: 'flex', + flexWrap: 'wrap', + columnGap: 12, + rowGap: 16, + padding: '16px', + boxSizing: 'border-box', + }, + walletItem: { + cursor: 'pointer', + background: '#fff', + width: 'calc( 50% - 6px )', + borderRadius: '8px', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: 4, + padding: '12px 0', + boxSizing: 'content-box', + fontSize: 12, + fontWeight: 700, + color: theme.palette.text.secondary, + fontFamily: 'Helvetica', + height: 'fit-content', + }, + walletIcon: { + width: '5rem', + height: '5rem', + }, +})) +const ConnectWalletPage = memo(() => { + const { classes } = useStyles() + const navigate = useNavigate() + const clickItem = () => { + navigate(PopupRoutes.VerifyWallet) + } + return ( +
+ {supportedWallets.map((item, idx) => { + return ( +
+ + {item.title} +
+ ) + })} +
+ ) +}) + +export default ConnectWalletPage diff --git a/packages/mask/src/extension/popups/pages/Wallet/VerifyWallet/index.tsx b/packages/mask/src/extension/popups/pages/Wallet/VerifyWallet/index.tsx new file mode 100644 index 000000000000..ceb359a0cf9f --- /dev/null +++ b/packages/mask/src/extension/popups/pages/Wallet/VerifyWallet/index.tsx @@ -0,0 +1,79 @@ +import { useWallet } from '@masknet/web3-shared-evm' +import { NextIDAction, NextIDPayload, NextIDPlatform } from '@masknet/shared-base' +import { makeStyles } from '@masknet/theme' +import { NextIDProof } from '@masknet/web3-providers' +import { memo, useState } from 'react' +import { SignSteps, Steps } from '../../../../../components/shared/VerifyWallet/Steps' +import Services from '../../../../service' +import { PersonaContext } from '../../Personas/hooks/usePersonaContext' + +const useStyles = makeStyles()((theme) => ({ + container: { + display: 'flex', + flexDirection: 'column', + flex: 1, + background: 'rgba(247, 249, 250, 1)', + padding: '8px 16px 16px 16px', + }, +})) +const VerifyWallet = memo(() => { + const { classes } = useStyles() + const { currentPersona } = PersonaContext.useContainer() + const wallet = useWallet() + + const [step, setStep] = useState(SignSteps.Ready) + const [signature, setSignature] = useState() + const [payload, setPayload] = useState() + if (!currentPersona || !wallet) return null + const personaSilentSign = async () => { + try { + const payload = await NextIDProof.createPersonaPayload( + currentPersona.publicHexKey as string, + NextIDAction.Create, + wallet.address, + NextIDPlatform.Ethereum, + 'default', + ) + if (!payload) throw new Error('Failed to create persona payload.') + setPayload(payload) + const signResult = await Services.Identity.generateSignResult( + currentPersona.identifier, + payload.signPayload, + ) + setSignature(signResult.signature.signature) + if (!signResult) throw new Error('Failed to sign persona.') + setStep(SignSteps.FirstStepDone) + } catch (error) { + console.error(error) + } + } + const walletSign = async () => { + if (!payload) throw new Error('payload error') + try { + const walletSig = await Services.Ethereum.personalSign(payload.signPayload, wallet.address) + if (!walletSig) throw new Error('Wallet sign failed') + await NextIDProof.bindProof( + payload.uuid, + currentPersona.publicHexKey as string, + NextIDAction.Create, + NextIDPlatform.Ethereum, + wallet.address, + payload.createdAt, + { + walletSignature: walletSig, + signature: signature, + }, + ) + setStep(SignSteps.SecondStepDone) + } catch (error) { + console.error(error) + } + } + return ( +
+ +
+ ) +}) + +export default VerifyWallet diff --git a/packages/mask/src/extension/popups/pages/Wallet/index.tsx b/packages/mask/src/extension/popups/pages/Wallet/index.tsx index 884165e68f49..ca90500df96f 100644 --- a/packages/mask/src/extension/popups/pages/Wallet/index.tsx +++ b/packages/mask/src/extension/popups/pages/Wallet/index.tsx @@ -33,6 +33,8 @@ const SetPaymentPassword = lazy(() => import('./SetPaymentPassword')) const WalletRecovery = lazy(() => import('./WalletRecovery')) const LegacyWalletRecovery = lazy(() => import('./LegacyWalletRecovery')) const ReplaceTransaction = lazy(() => import('./ReplaceTransaction')) +const VerifyWallet = lazy(() => import('./VerifyWallet')) +const ConnectWallet = lazy(() => import('./ConnectWallet')) const r = relativeRouteOf(PopupRoutes.Wallet) export default function Wallet() { @@ -118,6 +120,7 @@ export default function Wallet() { } /> } /> } /> + } /> )} diff --git a/packages/shared-base/src/Routes/PopupRoutes.ts b/packages/shared-base/src/Routes/PopupRoutes.ts index 2d91f177a4c7..880024ec71c0 100644 --- a/packages/shared-base/src/Routes/PopupRoutes.ts +++ b/packages/shared-base/src/Routes/PopupRoutes.ts @@ -34,4 +34,6 @@ export enum PopupRoutes { ThirdPartyRequestPermission = '/3rd-request-permission', SignRequest = '/sign-request', Swap = '/swap', + ConnectWallet = '/wallet/connect', + VerifyWallet = '/personas/verify', }