diff --git a/packages/mask/background/services/helper/index.ts b/packages/mask/background/services/helper/index.ts index 6f4810cfab58..f9d6d1327ca6 100644 --- a/packages/mask/background/services/helper/index.ts +++ b/packages/mask/background/services/helper/index.ts @@ -3,4 +3,4 @@ export { resolveTCOLink } from './short-link-resolver' export { openPopupWindow, removePopupWindow } from './popup-opener' export { __deprecated__getStorage, __deprecated__setStorage } from './deprecated-storage' export { queryExtensionPermission, requestExtensionPermission } from './request-permission' -export { createPersonaPayload, queryExistedBinding, bindProof } from './nextID' +export { createPersonaPayload, queryExistedBinding, bindProof, queryPersonaHexPublicKey } from './nextID' diff --git a/packages/mask/background/services/helper/nextID.ts b/packages/mask/background/services/helper/nextID.ts index c9dba1b0f509..11eb94379d9f 100644 --- a/packages/mask/background/services/helper/nextID.ts +++ b/packages/mask/background/services/helper/nextID.ts @@ -72,7 +72,7 @@ export async function bindProof( }) } -async function queryPersonaHexPublicKey(persona: PersonaIdentifier) { +export async function queryPersonaHexPublicKey(persona: PersonaIdentifier) { const key256 = decompressSecp256k1Key(persona.compressedPoint.replace(/\|/g, '/')) if (!key256.x || !key256.y) return null const arr = compressSecp256k1Point(key256.x, key256.y) diff --git a/packages/mask/shared-ui/locales/en-US.json b/packages/mask/shared-ui/locales/en-US.json index 3064c07e26be..7bdcff909a1f 100644 --- a/packages/mask/shared-ui/locales/en-US.json +++ b/packages/mask/shared-ui/locales/en-US.json @@ -994,7 +994,12 @@ "popups_wallet_no_transactions": "You have no transactions", "popups_missing_parameter_caption": "Please close this page.", "popups_persona_connect_to": "Connect to {{type}}", + "popups_persona_to_be_verified": "To be verified", "popups_persona_disconnect": "Disconnect", + "popups_persona_disconnect_confirmation": "Disconnect confirmation?", + "popups_persona_disconnect_confirmation_description": "This persona verification record will no longer show up in your verification profile page. Your\n Mask friends can no longer send encrypted message to you by this persona or check your Web 3\n products", + "popups_persona": "Persona", + "popups_twitter_id": "Twitter ID", "popups_persona_logout": "Log out", "popups_persona_disconnect_tip": "After logging out, your associated social accounts can no longer decrypt past encrypted messages. If you need to reuse your account, you can recover your account with your identity, private key, local or cloud backup.", "popups_persona_persona_name_exists": "The persona name already exists", diff --git a/packages/mask/src/components/InjectedComponents/SetupGuide.tsx b/packages/mask/src/components/InjectedComponents/SetupGuide.tsx index 8af270978eff..ba5944d1c4ee 100644 --- a/packages/mask/src/components/InjectedComponents/SetupGuide.tsx +++ b/packages/mask/src/components/InjectedComponents/SetupGuide.tsx @@ -140,15 +140,10 @@ function SetupGuideUI(props: SetupGuideUIProps) { const post = collectVerificationPost?.(postContent) if (post && persona_.publicHexKey) { clearInterval(verifyPostCollectTimer.current!) - await bindProof( - persona_.publicHexKey, - NextIDAction.Create, - platform, - username, - undefined, + await bindProof(persona_.publicHexKey, NextIDAction.Create, platform, username, { signature, - post.postId, - ) + proofLocation: post.postId, + }) resolve() } }, 1000) diff --git a/packages/mask/src/extension/background-script/IdentityService.ts b/packages/mask/src/extension/background-script/IdentityService.ts index 6c4ba9387f59..1586d9a3bba9 100644 --- a/packages/mask/src/extension/background-script/IdentityService.ts +++ b/packages/mask/src/extension/background-script/IdentityService.ts @@ -183,6 +183,7 @@ export async function queryOwnedPersonaInformation(): Promise { const { currentPersona, setDeletingPersona, personas } = PersonaContext.useContainer() const history = useHistory() - console.log(personas) return ( <>
diff --git a/packages/mask/src/extension/popups/pages/Personas/PersonaSignRequest/index.tsx b/packages/mask/src/extension/popups/pages/Personas/PersonaSignRequest/index.tsx index c0f6833c56d3..dfd5fd569d25 100644 --- a/packages/mask/src/extension/popups/pages/Personas/PersonaSignRequest/index.tsx +++ b/packages/mask/src/extension/popups/pages/Personas/PersonaSignRequest/index.tsx @@ -135,7 +135,10 @@ const PersonaSignRequest = memo(() => { {message}
- + + + ) + }, +) diff --git a/packages/mask/src/extension/popups/pages/Personas/components/ProfileList/index.tsx b/packages/mask/src/extension/popups/pages/Personas/components/ProfileList/index.tsx index 5c3c50d9f984..ca6fd4dc39d5 100644 --- a/packages/mask/src/extension/popups/pages/Personas/components/ProfileList/index.tsx +++ b/packages/mask/src/extension/popups/pages/Personas/components/ProfileList/index.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react' +import { memo, useCallback, useState } from 'react' import { Avatar, Link, List, ListItem, ListItemText, Typography } from '@mui/material' import { definedSocialNetworkUIs } from '../../../../../../social-network' import { SOCIAL_MEDIA_ICON_MAPPING } from '@masknet/shared' @@ -7,9 +7,14 @@ import { compact } from 'lodash-unified' import { makeStyles } from '@masknet/theme' import { useI18N } from '../../../../../../utils' import { PersonaContext } from '../../hooks/usePersonaContext' -import { useAsyncFn } from 'react-use' +import { useAsyncFn, useAsyncRetry } from 'react-use' import Services from '../../../../../service' import { GrayMasks } from '@masknet/icons' +import { DisconnectDialog } from '../DisconnectDialog' +import { bindProof, createPersonaPayload, queryExistedBindingByPersona } from '@masknet/web3-providers' +import { NextIDAction, NextIDPlatform } from '@masknet/shared-base' +import { delay } from '@dimensiondev/kit' +import classNames from 'classnames' const useStyles = makeStyles()((theme) => ({ list: { @@ -29,6 +34,10 @@ const useStyles = makeStyles()((theme) => ({ text: { fontWeight: 600, margin: 0, + '& > span': { + display: 'flex', + alignItems: 'center', + }, }, link: { cursor: 'pointer', @@ -42,6 +51,13 @@ const useStyles = makeStyles()((theme) => ({ avatar: { width: 20, height: 20, + borderRadius: '50%', + }, + verified_avatar: { + border: '1px solid #60DFAB', + }, + unverified_avatar: { + border: '1px solid #FFB915', }, circle: { backgroundColor: '#ffffff', @@ -60,6 +76,15 @@ const useStyles = makeStyles()((theme) => ({ height: 9, }, }, + tag: { + background: 'linear-gradient(0deg, rgba(255, 177, 0, 0.2), rgba(255, 177, 0, 0.2)), #FFFFFF', + padding: 4, + color: '#FFB100', + fontSize: 10, + lineHeight: 1, + borderRadius: 4, + marginLeft: 6, + }, })) export interface ProfileListProps {} @@ -67,6 +92,12 @@ export interface ProfileListProps {} export const ProfileList = memo(() => { const { currentPersona } = PersonaContext.useContainer() + const [unbind, setUnbind] = useState<{ + identifier: ProfileIdentifier + identity?: string + platform?: NextIDPlatform + } | null>(null) + const definedSocialNetworks = compact( [...definedSocialNetworkUIs.values()].map(({ networkIdentifier }) => { if (networkIdentifier === 'localhost') return null @@ -88,26 +119,112 @@ export const ProfileList = memo(() => { [], ) - const [, onDisconnect] = useAsyncFn(async (identifier: ProfileIdentifier) => - Services.Identity.detachProfile(identifier), + const onDisconnect = useCallback( + (identifier: ProfileIdentifier, is_valid?: boolean, platform?: NextIDPlatform, identity?: string) => { + if (is_valid) { + setUnbind({ + identifier, + platform, + identity, + }) + return + } + Services.Identity.detachProfile(identifier) + }, + [], ) + const { value: mergedProfiles, retry: refreshProfileList } = useAsyncRetry(async () => { + if (!currentPersona) return [] + if (!currentPersona.publicHexKey) return currentPersona.linkedProfiles + const response = await queryExistedBindingByPersona(currentPersona.publicHexKey) + if (!response) return currentPersona.linkedProfiles + + return currentPersona.linkedProfiles.map((profile) => { + const target = response.proofs.find( + (x) => + profile.identifier.userId.toLowerCase() === x.identity.toLowerCase() && + profile.identifier.network.replace('.com', '') === x.platform, + ) + + return { + ...profile, + platform: target?.platform, + identity: target?.identity, + is_valid: target?.is_valid, + } + }) + }, [currentPersona]) + + const [confirmState, onConfirmDisconnect] = useAsyncFn(async () => { + // fetch signature payload + try { + if (!currentPersona) return + const publicHexKey = await Services.Helper.queryPersonaHexPublicKey(currentPersona.identifier) + if (!publicHexKey || !unbind || !unbind.identity || !unbind.platform) return + const result = await createPersonaPayload( + publicHexKey, + NextIDAction.Delete, + unbind.identity, + unbind.platform, + ) + if (!result) return + const signatureResult = await Services.Identity.signWithPersona({ + method: 'eth', + message: result.signPayload, + identifier: currentPersona.identifier.toText(), + }) + + if (!signatureResult) return + + await bindProof(publicHexKey, NextIDAction.Delete, unbind.platform, unbind.identity, { + signature: signatureResult.signature.signature, + }) + + await delay(2000) + setUnbind(null) + refreshProfileList() + } catch { + console.log('Disconnect failed') + } + }, [unbind, currentPersona?.identifier, refreshProfileList]) + return ( - + <> + + setUnbind(null)} + onConfirmDisconnect={onConfirmDisconnect} + confirmLoading={confirmState.loading} + /> + ) }) +interface MergedProfileInformation extends ProfileInformation { + is_valid?: boolean + identity?: string + platform?: NextIDPlatform +} + export interface ProfileListUIProps { onConnect: (networkIdentifier: string) => void - onDisconnect: (identifier: ProfileIdentifier) => void + onDisconnect: ( + identifier: ProfileIdentifier, + is_valid?: boolean, + platform?: NextIDPlatform, + identity?: string, + ) => void openProfilePage: (network: string, userId: string) => void - profiles: ProfileInformation[] + profiles: MergedProfileInformation[] networks: string[] } @@ -118,7 +235,7 @@ export const ProfileListUI = memo( return ( - {profiles.map(({ nickname, identifier, avatar }) => { + {profiles.map(({ identifier, avatar, is_valid, platform, identity }) => { return ( ( onDisconnect(identifier)}> + onClick={() => onDisconnect(identifier, is_valid, platform, identity)}> {t('popups_persona_disconnect')} }>
{avatar ? ( - + ) : (
- +
)}
{SOCIAL_MEDIA_ICON_MAPPING[identifier.network]}
@@ -149,6 +277,11 @@ export const ProfileListUI = memo( @{identifier.userId} + {!is_valid && identifier.network === 'twitter.com' ? ( + + {t('popups_persona_to_be_verified')} + + ) : null} ) diff --git a/packages/mask/src/extension/popups/pages/Personas/components/SwitchSNSDialog/index.tsx b/packages/mask/src/extension/popups/pages/Personas/components/SwitchSNSDialog/index.tsx new file mode 100644 index 000000000000..2201dbf12b2c --- /dev/null +++ b/packages/mask/src/extension/popups/pages/Personas/components/SwitchSNSDialog/index.tsx @@ -0,0 +1,49 @@ +import { memo } from 'react' +import { Button, Dialog, DialogActions, DialogContent, Typography } from '@mui/material' +import { makeStyles } from '@masknet/theme' +import { useI18N } from '../../../../../../utils' + +const useStyles = makeStyles()(() => ({ + title: { + fontSize: 16, + lineHeight: '22px', + color: '#0F1419', + }, + content: { + marginTop: 24, + fontSize: 14, + lineHeight: '20px', + color: '#536471', + }, + button: { + padding: '8px 0', + width: '100%', + borderRadius: 9999, + fontSize: 14, + fontWeight: 600, + lineHeight: '20px', + backgroundColor: '#1C68F3', + color: '#ffffff', + '&:hover': { + backgroundColor: '#1a5edd', + }, + }, +})) + +export const SwitchSNSDialog = memo(() => { + const { t } = useI18N() + const { classes } = useStyles() + return ( + + + Switch Twitter Account + + You are not connected to @Vitalik.eth, please log in and try again. + + + + + + + ) +}) diff --git a/packages/shared-base/src/NextID/type.ts b/packages/shared-base/src/NextID/type.ts index c80d44f9528d..9454675a4c51 100644 --- a/packages/shared-base/src/NextID/type.ts +++ b/packages/shared-base/src/NextID/type.ts @@ -19,8 +19,9 @@ export interface NextIDPayload { export interface NextIDPersonaBindings { persona: string proofs: { - platform: string + platform: NextIDPlatform identity: string + is_valid: boolean }[] } diff --git a/packages/shared-base/src/Persona/type.ts b/packages/shared-base/src/Persona/type.ts index 27e12b9c995b..fe5ccf8c9cb3 100644 --- a/packages/shared-base/src/Persona/type.ts +++ b/packages/shared-base/src/Persona/type.ts @@ -6,6 +6,7 @@ import type { PersonaIdentifier, ProfileIdentifier } from '../Identifier/type' export interface PersonaInformation { /** The nickname of the persona. Should use profile.nickname if it presents. */ nickname?: string + publicHexKey?: string identifier: PersonaIdentifier linkedProfiles: ProfileInformation[] } diff --git a/packages/web3-providers/src/NextID/index.ts b/packages/web3-providers/src/NextID/index.ts index 34a890ca439d..ae1365ca5030 100644 --- a/packages/web3-providers/src/NextID/index.ts +++ b/packages/web3-providers/src/NextID/index.ts @@ -19,19 +19,21 @@ export async function bindProof( action: NextIDAction, platform: string, identity: string, - walletSignature?: string, - signature?: string, - proofLocation?: string, + options?: { + walletSignature?: string + signature?: string + proofLocation?: string + }, ) { const requestBody = { action, platform, identity, public_key: personaPublicKey, - ...(proofLocation ? { proof_location: proofLocation } : {}), + proof_location: options?.proofLocation, extra: { - ...(walletSignature ? { wallet_signature: toBase64(fromHex(walletSignature)) } : {}), - ...(signature ? { signature: toBase64(fromHex(signature)) } : {}), + wallet_signature: options?.walletSignature ? toBase64(fromHex(options.walletSignature)) : undefined, + signature: options?.signature ? toBase64(fromHex(options.signature)) : undefined, }, }