From 426228f9793f818d7d161a1a55a57d39ba085123 Mon Sep 17 00:00:00 2001 From: Wukong Sun Date: Mon, 1 Sep 2025 12:40:30 +0800 Subject: [PATCH 1/7] feat: add support for farcaster social network integration --- cspell.json | 24 ++++++++++-- .../services/site-adaptors/connect.ts | 6 +-- packages/mask/popups/constants.ts | 4 +- .../ConnectSocialAccountModal/index.tsx | 18 ++++++--- .../modals/SupportedSitesModal/index.tsx | 12 +++--- .../pages/Personas/ConnectFirefly/index.tsx | 35 +++++++++++++++++ .../pages/Personas/ConnectWallet/index.tsx | 38 +++++++++---------- packages/mask/popups/pages/Personas/index.tsx | 1 + packages/mask/shared/definitions/event.ts | 1 + .../mask/shared/site-adaptors/definitions.ts | 2 + .../implementations/farcaster.xyz.ts | 13 +++++++ .../src/LegacySettings/settings.ts | 1 + packages/shared-base/src/NextID/index.ts | 9 +---- packages/shared-base/src/Site/index.ts | 1 + packages/shared-base/src/Site/types.ts | 1 + packages/shared-base/src/constants.ts | 4 +- packages/shared-base/src/types/Routes.ts | 1 + packages/shared/src/constants.tsx | 4 +- .../src/AvatarStore/helpers/getAvatar.ts | 1 + 19 files changed, 126 insertions(+), 50 deletions(-) create mode 100644 packages/mask/popups/pages/Personas/ConnectFirefly/index.tsx create mode 100644 packages/mask/shared/site-adaptors/implementations/farcaster.xyz.ts diff --git a/cspell.json b/cspell.json index 3467b4e789e8..6f34244e0dc8 100644 --- a/cspell.json +++ b/cspell.json @@ -1,7 +1,14 @@ { "version": "0.2", "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", - "dictionaries": ["typescript", "node", "npm", "html", "css", "fonts"], + "dictionaries": [ + "typescript", + "node", + "npm", + "html", + "css", + "fonts" + ], "ignorePaths": [ "./packages/gun-utils/gun.js", "./packages/icons/**", @@ -27,7 +34,11 @@ "pnpm-workspace.yaml", "qya-aa.json" ], - "TODO: fix those words": ["bridgable", "clonable", "sniffings"], + "TODO: fix those words": [ + "bridgable", + "clonable", + "sniffings" + ], "ignoreWords": [ "aeth", "algr", @@ -241,7 +252,11 @@ "zksync", "zora" ], - "ignoreRegExpList": ["/[A-Za-z0-9]{44}/", "/[A-Za-z0-9]{46}/", "/[A-Za-z0-9]{59}/"], + "ignoreRegExpList": [ + "/[A-Za-z0-9]{44}/", + "/[A-Za-z0-9]{46}/", + "/[A-Za-z0-9]{59}/" + ], "overrides": [], "words": [ "arbitrum", @@ -249,6 +264,7 @@ "cashtags", "celo", "endregion", + "firelfy", "linkedin", "luma", "muln", @@ -262,4 +278,4 @@ "waitlist", "youtube" ] -} +} \ No newline at end of file diff --git a/packages/mask/background/services/site-adaptors/connect.ts b/packages/mask/background/services/site-adaptors/connect.ts index f283040a2264..7a6143eb5031 100644 --- a/packages/mask/background/services/site-adaptors/connect.ts +++ b/packages/mask/background/services/site-adaptors/connect.ts @@ -1,5 +1,3 @@ -import { compact, first, sortBy } from 'lodash-es' -import stringify from 'json-stable-stringify' import { delay } from '@masknet/kit' import { type PersonaIdentifier, @@ -7,9 +5,11 @@ import { currentSetupGuideStatus, SetupGuideStep, } from '@masknet/shared-base' +import stringify from 'json-stable-stringify' +import { compact, first, sortBy } from 'lodash-es' +import type { Tabs } from 'webextension-polyfill' import { definedSiteAdaptors } from '../../../shared/site-adaptors/definitions.js' import type { SiteAdaptor } from '../../../shared/site-adaptors/types.js' -import type { Tabs } from 'webextension-polyfill' async function hasPermission(origin: string): Promise { return browser.permissions.contains({ diff --git a/packages/mask/popups/constants.ts b/packages/mask/popups/constants.ts index 10996ada2a2a..f93c614b5c92 100644 --- a/packages/mask/popups/constants.ts +++ b/packages/mask/popups/constants.ts @@ -3,7 +3,7 @@ import { EnhanceableSite } from '@masknet/shared-base' export const MATCH_PASSWORD_RE = /^(?=.{8,20}$)(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[^\dA-Za-z]).*/u export const MAX_FILE_SIZE = 10 * 1024 * 1024 -export const SOCIAL_MEDIA_ICON_FILTER_COLOR: Record = { +export const SOCIAL_MEDIA_ICON_FILTER_COLOR: Record = { [EnhanceableSite.Twitter]: 'drop-shadow(0px 6px 12px rgba(29, 161, 242, 0.20))', [EnhanceableSite.Facebook]: 'drop-shadow(0px 6px 12px rgba(60, 89, 155, 0.20))', [EnhanceableSite.Minds]: 'drop-shadow(0px 6px 12px rgba(33, 37, 42, 0.20))', @@ -11,4 +11,6 @@ export const SOCIAL_MEDIA_ICON_FILTER_COLOR: Record(function ConnectSocialAccountModal(props) { const { data: definedSocialNetworks = EMPTY_LIST } = useSupportSocialNetworks() const { currentPersona } = PersonaContext.useContainer() + const navigate = useNavigate() const handleConnect = useCallback( async (networkIdentifier: EnhanceableSite) => { + if (networkIdentifier === EnhanceableSite.Farcaster) { + navigate(PopupRoutes.ConnectFirefly) + return + } if (!currentPersona) return if (!(await requestPermissionFromExtensionPage(networkIdentifier))) return await Services.SiteAdaptor.connectSite(currentPersona.identifier, networkIdentifier, undefined) diff --git a/packages/mask/popups/modals/SupportedSitesModal/index.tsx b/packages/mask/popups/modals/SupportedSitesModal/index.tsx index 34e8d50696b0..a6e2ea13e9c9 100644 --- a/packages/mask/popups/modals/SupportedSitesModal/index.tsx +++ b/packages/mask/popups/modals/SupportedSitesModal/index.tsx @@ -75,21 +75,19 @@ export const SupportedSitesModal = memo(function Supported {!isPending && data ? data.map((x) => { - const Icon = SOCIAL_MEDIA_ROUND_ICON_MAPPING[x.networkIdentifier] - + const networkIdentifier = x.networkIdentifier as EnhanceableSite + const Icon = SOCIAL_MEDIA_ROUND_ICON_MAPPING[networkIdentifier] return ( - handleSwitch({ ...x, networkIdentifier: x.networkIdentifier as EnhanceableSite }) - }> + onClick={() => handleSwitch({ ...x, networkIdentifier })}> {Icon ? (function Supported : null} diff --git a/packages/mask/popups/pages/Personas/ConnectFirefly/index.tsx b/packages/mask/popups/pages/Personas/ConnectFirefly/index.tsx new file mode 100644 index 000000000000..0468a445c709 --- /dev/null +++ b/packages/mask/popups/pages/Personas/ConnectFirefly/index.tsx @@ -0,0 +1,35 @@ +import { useLingui } from '@lingui/react/macro' +import { PopupHomeTabType } from '@masknet/shared' +import { QRCode } from 'react-qrcode-logo' +import { PopupRoutes } from '@masknet/shared-base' +import { makeStyles } from '@masknet/theme' +import { Box } from '@mui/material' +import { memo, useCallback } from 'react' +import { useNavigate } from 'react-router-dom' +import urlcat from 'urlcat' +import { useTitle } from '../../../hooks/index.js' + +const useStyles = makeStyles()({ + container: {}, +}) + +export const Component = memo(function ConnectFireflyPage() { + const { t } = useLingui() + const { classes } = useStyles() + + const navigate = useNavigate() + + const handleBack = useCallback(() => { + navigate(urlcat(PopupRoutes.Personas, { tab: PopupHomeTabType.ConnectedWallets }), { + replace: true, + }) + }, []) + + useTitle(t`Connect Firefly`, handleBack) + + return ( + + + + ) +}) diff --git a/packages/mask/popups/pages/Personas/ConnectWallet/index.tsx b/packages/mask/popups/pages/Personas/ConnectWallet/index.tsx index 29a35cf0108e..7a80e319974d 100644 --- a/packages/mask/popups/pages/Personas/ConnectWallet/index.tsx +++ b/packages/mask/popups/pages/Personas/ConnectWallet/index.tsx @@ -1,22 +1,16 @@ -import { memo, useCallback } from 'react' -import urlcat from 'urlcat' -import { useAsync, useAsyncFn } from 'react-use' -import { useNavigate } from 'react-router-dom' -import { Avatar, Box, Button, Link, Typography } from '@mui/material' -import { ActionButton, makeStyles, usePopupCustomSnackbar } from '@masknet/theme' +import { Icons } from '@masknet/icons' +import { FormattedAddress, PersonaContext, PopupHomeTabType, WalletIcon } from '@masknet/shared' import { - NextIDPlatform, - type NetworkPluginID, + MaskMessages, NextIDAction, + NextIDPlatform, + PopupModalRoutes, + PopupRoutes, SignType, + type NetworkPluginID, type NextIDPayload, - PopupRoutes, - PopupModalRoutes, - MaskMessages, } from '@masknet/shared-base' -import { formatDomainName, formatEthereumAddress, ProviderType } from '@masknet/web3-shared-evm' -import { FormattedAddress, PersonaContext, PopupHomeTabType, WalletIcon } from '@masknet/shared' -import { EVMExplorerResolver, NextIDProof, EVMProviderResolver, EVMWeb3 } from '@masknet/web3-providers' +import { ActionButton, makeStyles, usePopupCustomSnackbar } from '@masknet/theme' import { useChainContext, useNetworkContext, @@ -24,15 +18,21 @@ import { useReverseAddress, useWallets, } from '@masknet/web3-hooks-base' +import { EVMExplorerResolver, EVMProviderResolver, EVMWeb3, NextIDProof } from '@masknet/web3-providers' import { isSameAddress } from '@masknet/web3-shared-base' -import { Icons } from '@masknet/icons' +import { formatDomainName, formatEthereumAddress, ProviderType } from '@masknet/web3-shared-evm' +import { Avatar, Box, Button, Link, Typography } from '@mui/material' +import { memo, useCallback } from 'react' +import { useNavigate } from 'react-router-dom' +import { useAsync, useAsyncFn } from 'react-use' +import urlcat from 'urlcat' -import { useTitle } from '../../../hooks/index.js' -import { BottomController } from '../../../components/BottomController/index.js' -import { LoadingMask } from '../../../components/LoadingMask/index.js' import Services from '#services' -import { useModalNavigate } from '../../../components/index.js' import { Trans, useLingui } from '@lingui/react/macro' +import { BottomController } from '../../../components/BottomController/index.js' +import { useModalNavigate } from '../../../components/index.js' +import { LoadingMask } from '../../../components/LoadingMask/index.js' +import { useTitle } from '../../../hooks/index.js' const useStyles = makeStyles()((theme) => ({ provider: { diff --git a/packages/mask/popups/pages/Personas/index.tsx b/packages/mask/popups/pages/Personas/index.tsx index d4564687f63d..0ba08f44a61e 100644 --- a/packages/mask/popups/pages/Personas/index.tsx +++ b/packages/mask/popups/pages/Personas/index.tsx @@ -17,6 +17,7 @@ export const personaRoute: RouteObject[] = [ { path: r(PopupRoutes.WalletConnect), lazy: () => import('./WalletConnect/index.js') }, { path: r(PopupRoutes.ExportPrivateKey), lazy: () => import('./ExportPrivateKey/index.js') }, { path: r(PopupRoutes.PersonaAvatarSetting), lazy: () => import('./PersonaAvatarSetting/index.js') }, + { path: r(PopupRoutes.ConnectFirefly), lazy: () => import('./ConnectFirefly/index.js') }, { path: '*', element: }, ] diff --git a/packages/mask/shared/definitions/event.ts b/packages/mask/shared/definitions/event.ts index 1492d75693d7..167aec78276e 100644 --- a/packages/mask/shared/definitions/event.ts +++ b/packages/mask/shared/definitions/event.ts @@ -10,6 +10,7 @@ export const EventMap: Record = { [EnhanceableSite.OpenSea]: EventID.Debug, [EnhanceableSite.Mirror]: EventID.Debug, [EnhanceableSite.Firefly]: EventID.Debug, + [EnhanceableSite.Farcaster]: EventID.Debug, } export const DisconnectEventMap: Record = { diff --git a/packages/mask/shared/site-adaptors/definitions.ts b/packages/mask/shared/site-adaptors/definitions.ts index 9c3a447abff0..0b6f17f0a0a8 100644 --- a/packages/mask/shared/site-adaptors/definitions.ts +++ b/packages/mask/shared/site-adaptors/definitions.ts @@ -4,6 +4,7 @@ import { MindsAdaptor } from './implementations/minds.com.js' import { MirrorAdaptor } from './implementations/mirror.xyz.js' import { TwitterAdaptor } from './implementations/twitter.com.js' import type { SiteAdaptor } from './types.js' +import { FarcasterAdaptor } from './implementations/farcaster.xyz.js' const defined = new Map() export const definedSiteAdaptors: ReadonlyMap = defined @@ -16,6 +17,7 @@ defineSiteAdaptor(InstagramAdaptor) defineSiteAdaptor(MindsAdaptor) defineSiteAdaptor(MirrorAdaptor) defineSiteAdaptor(TwitterAdaptor) +defineSiteAdaptor(FarcasterAdaptor) function matches(url: string, pattern: string) { const l = new URL(pattern) diff --git a/packages/mask/shared/site-adaptors/implementations/farcaster.xyz.ts b/packages/mask/shared/site-adaptors/implementations/farcaster.xyz.ts new file mode 100644 index 000000000000..a1e1e982a89a --- /dev/null +++ b/packages/mask/shared/site-adaptors/implementations/farcaster.xyz.ts @@ -0,0 +1,13 @@ +import { EnhanceableSite } from '@masknet/shared-base' +import type { SiteAdaptor } from '../types.js' + +const origins = ['https://farcaster.xyz/*'] + +export const FarcasterAdaptor: SiteAdaptor.Definition = { + name: 'Farcaster', + networkIdentifier: EnhanceableSite.Farcaster, + declarativePermissions: { origins }, + homepage: 'https://farcaster.xyz', + isSocialNetwork: true, + sortIndex: 1, +} diff --git a/packages/shared-base/src/LegacySettings/settings.ts b/packages/shared-base/src/LegacySettings/settings.ts index 0ff2292e2799..d6c110da6bfe 100644 --- a/packages/shared-base/src/LegacySettings/settings.ts +++ b/packages/shared-base/src/LegacySettings/settings.ts @@ -23,6 +23,7 @@ export const pluginIDsSettings = createGlobalSettings> = { [EnhanceableSite.Twitter]: NextIDPlatform.Twitter, } export function resolveNetworkToNextIDPlatform(key: EnhanceableSite): NextIDPlatform | undefined { diff --git a/packages/shared-base/src/Site/index.ts b/packages/shared-base/src/Site/index.ts index 9a9338e985f7..3745609e536d 100644 --- a/packages/shared-base/src/Site/index.ts +++ b/packages/shared-base/src/Site/index.ts @@ -14,6 +14,7 @@ const matchEnhanceableSiteHost: Record = { process.env.NODE_ENV === 'production' ? /(?:^(?:firefly\.|firefly-staging\.|firefly-canary\.)?mask\.social|[\w-]+\.vercel\.app)$/iu : /^localhost:\d+$/u, + [EnhanceableSite.Farcaster]: /(^|\.)farcaster\.xyz$/iu, } const matchExtensionSitePathname: Record = { diff --git a/packages/shared-base/src/Site/types.ts b/packages/shared-base/src/Site/types.ts index db3956523127..588328dcc262 100644 --- a/packages/shared-base/src/Site/types.ts +++ b/packages/shared-base/src/Site/types.ts @@ -2,6 +2,7 @@ export enum EnhanceableSite { Localhost = 'localhost', Twitter = 'twitter.com', Facebook = 'facebook.com', + Farcaster = 'farcaster.xyz', Minds = 'minds.com', Instagram = 'instagram.com', OpenSea = 'opensea.io', diff --git a/packages/shared-base/src/constants.ts b/packages/shared-base/src/constants.ts index ce75f9d4f687..3ab4f7e2c478 100644 --- a/packages/shared-base/src/constants.ts +++ b/packages/shared-base/src/constants.ts @@ -2,7 +2,7 @@ import { NextIDPlatform } from './NextID/types.js' import { EnhanceableSite } from './Site/types.js' import { PluginID } from './types/PluginID.js' -export const SOCIAL_MEDIA_NAME: Record = { +export const SOCIAL_MEDIA_NAME: Record = { [EnhanceableSite.Twitter]: 'X', [EnhanceableSite.Facebook]: 'Facebook', [EnhanceableSite.Minds]: 'Minds', @@ -10,6 +10,8 @@ export const SOCIAL_MEDIA_NAME: Record = { [EnhanceableSite.OpenSea]: 'OpenSea', [EnhanceableSite.Localhost]: 'Localhost', [EnhanceableSite.Mirror]: 'Mirror', + [EnhanceableSite.Farcaster]: 'Farcaster', + [EnhanceableSite.Firefly]: 'Firefly', } export const NEXT_ID_PLATFORM_SOCIAL_MEDIA_MAP: Record = { diff --git a/packages/shared-base/src/types/Routes.ts b/packages/shared-base/src/types/Routes.ts index 378fe8fc000d..ee1c25426d77 100644 --- a/packages/shared-base/src/types/Routes.ts +++ b/packages/shared-base/src/types/Routes.ts @@ -88,6 +88,7 @@ export enum PopupRoutes { WalletConnect = '/personas/wallet-connect', ExportPrivateKey = '/personas/export-private-key', PersonaAvatarSetting = '/personas/avatar-setting', + ConnectFirefly = '/personas/connect-firefly', Trader = '/trader', } export interface PopupRoutesParamsMap { diff --git a/packages/shared/src/constants.tsx b/packages/shared/src/constants.tsx index 6659721ddf60..f2add927ae60 100644 --- a/packages/shared/src/constants.tsx +++ b/packages/shared/src/constants.tsx @@ -1,7 +1,7 @@ import { Icons, type GeneratedIcon } from '@masknet/icons' import { EnhanceableSite } from '@masknet/shared-base' -export const SOCIAL_MEDIA_ROUND_ICON_MAPPING: Record = { +export const SOCIAL_MEDIA_ROUND_ICON_MAPPING: Record = { [EnhanceableSite.Twitter]: Icons.TwitterXRound, [EnhanceableSite.Facebook]: Icons.FacebookRound, [EnhanceableSite.Minds]: Icons.MindsRound, @@ -9,6 +9,8 @@ export const SOCIAL_MEDIA_ROUND_ICON_MAPPING: Record Date: Tue, 2 Sep 2025 17:31:51 +0800 Subject: [PATCH 2/7] feat: add firefly session management and farcaster integration --- .node-version | 2 +- cspell.json | 2 + .../bindOrRestoreFireflySession.ts | 0 .../createAccountByRelayService.ts | 64 +++++++++++ .../src/Firefly/FarcasterSession.ts | 108 ++++++++++++++++++ .../web3-providers/src/Firefly/Session.ts | 82 +++++++++++++ .../src/Firefly/SessionHolder.ts | 34 ++++++ packages/web3-providers/src/Firefly/index.ts | 2 + .../web3-providers/src/Session/Session.ts | 27 +++++ .../src/Session/SessionHolder.ts | 70 ++++++++++++ packages/web3-providers/src/entry.ts | 9 +- .../src/helpers/encodeSessionPayload.ts | 17 +++ packages/web3-providers/src/types/Session.ts | 67 +++++++++++ 13 files changed, 482 insertions(+), 2 deletions(-) create mode 100644 packages/mask/popups/pages/Personas/ConnectFirefly/bindOrRestoreFireflySession.ts create mode 100644 packages/mask/popups/pages/Personas/ConnectFirefly/createAccountByRelayService.ts create mode 100644 packages/web3-providers/src/Firefly/FarcasterSession.ts create mode 100644 packages/web3-providers/src/Firefly/Session.ts create mode 100644 packages/web3-providers/src/Firefly/SessionHolder.ts create mode 100644 packages/web3-providers/src/Session/Session.ts create mode 100644 packages/web3-providers/src/Session/SessionHolder.ts create mode 100644 packages/web3-providers/src/helpers/encodeSessionPayload.ts create mode 100644 packages/web3-providers/src/types/Session.ts diff --git a/.node-version b/.node-version index c9758a53fae1..4c8f24d74e13 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -v23.6.0 +v24.7.0 diff --git a/cspell.json b/cspell.json index 6f34244e0dc8..544c81f53015 100644 --- a/cspell.json +++ b/cspell.json @@ -263,6 +263,7 @@ "boba", "cashtags", "celo", + "deeplink", "endregion", "firelfy", "linkedin", @@ -276,6 +277,7 @@ "tweetnacl", "txid", "waitlist", + "WARPCAST", "youtube" ] } \ No newline at end of file diff --git a/packages/mask/popups/pages/Personas/ConnectFirefly/bindOrRestoreFireflySession.ts b/packages/mask/popups/pages/Personas/ConnectFirefly/bindOrRestoreFireflySession.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/mask/popups/pages/Personas/ConnectFirefly/createAccountByRelayService.ts b/packages/mask/popups/pages/Personas/ConnectFirefly/createAccountByRelayService.ts new file mode 100644 index 000000000000..69ffee122391 --- /dev/null +++ b/packages/mask/popups/pages/Personas/ConnectFirefly/createAccountByRelayService.ts @@ -0,0 +1,64 @@ +import type { Account } from '@masknet/shared-base' +import { fetchJSON } from '@masknet/web3-providers/helpers' +import { FarcasterSession } from '@masknet/web3-providers' +import urlcat from 'urlcat' + +const FARCASTER_REPLY_URL = 'https://relay.farcaster.xyz' +const NOT_DEPEND_SECRET = '[TO_BE_REPLACED_LATER]' + +interface FarcasterReplyResponse { + channelToken: string + url: string + // the same as url + connectUri: string + // cspell: disable-next-line + /** @example dpO7VRkrPcwyLhyFZ */ + nonce: string +} + +async function createSession(signal?: AbortSignal) { + const url = urlcat(FARCASTER_REPLY_URL, '/v1/channel') + const response = await fetchJSON(url, { + method: 'POST', + body: JSON.stringify({ + siteUri: 'https://www.mask.io', + domain: 'www.mask.io', + }), + signal, + }) + + const now = Date.now() + const farcasterSession = new FarcasterSession( + NOT_DEPEND_SECRET, + NOT_DEPEND_SECRET, + now, + now, + '', + response.channelToken, + ) + + return { + deeplink: response.connectUri, + session: farcasterSession, + } +} + +export async function createAccountByRelayService(callback?: (url: string) => void, signal?: AbortSignal) { + const { deeplink, session } = await createSession(signal) + + // present QR code to the user or open the link in a new tab + callback?.(deeplink) + + // polling for the session to be ready + const fireflySession = await bindOrRestoreFireflySession(session, signal) + + // profile id is available after the session is ready + const profile = await getFarcasterProfileById(session.profileId) + + return { + origin: 'sync', + session, + profile, + fireflySession, + } satisfies Account +} diff --git a/packages/web3-providers/src/Firefly/FarcasterSession.ts b/packages/web3-providers/src/Firefly/FarcasterSession.ts new file mode 100644 index 000000000000..d329322d674f --- /dev/null +++ b/packages/web3-providers/src/Firefly/FarcasterSession.ts @@ -0,0 +1,108 @@ +import urlcat from 'urlcat' + +import { SessionType, type Session } from '../types/Session.js' +import { fetchJSON } from '../helpers/fetchJSON.js' +import { BaseSession } from '../Session/Session.js' + +export const WARPCAST_ROOT_URL_V2 = 'https://api.warpcast.com/v2' +export const FAKE_SIGNER_REQUEST_TOKEN = 'fake_signer_request_token' + +export enum FarcasterSponsorship { + Firefly = 'firefly', +} + +export class FarcasterSession extends BaseSession implements Session { + constructor( + /** + * Fid + */ + profileId: string, + /** + * the private key of the signer + */ + token: string, + createdAt: number, + expiresAt: number, + public signerRequestToken?: string, + public channelToken?: string, + public sponsorshipSignature?: string, + public walletAddress?: string, + ) { + super(SessionType.Farcaster, profileId, token, createdAt, expiresAt) + } + + override serialize(): `${SessionType}:${string}:${string}:${string}` { + return [ + super.serialize(), + this.signerRequestToken ?? '', + this.channelToken ?? '', + this.sponsorshipSignature ?? '', + ].join(':') as `${SessionType}:${string}:${string}:${string}` + } + + refresh(): Promise { + throw new Error('Not allowed') + } + + async destroy(): Promise { + const url = urlcat(WARPCAST_ROOT_URL_V2, '/auth') + const response = await fetchJSON<{ + result: { + success: boolean + } + }>(url, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${this.token}`, + }, + body: JSON.stringify({ + method: 'revokeToken', + params: { + timestamp: this.createdAt, + }, + }), + }) + + // indicate the session is destroyed + this.expiresAt = 0 + + if (!response.result.success) throw new Error('Failed to destroy the session.') + return + } + + static isGrantByPermission( + session: Session | null, + // if strict is true, the session must have a valid signer request token + strict = false, + ): session is FarcasterSession & { signerRequestToken: string } { + if (!session) return false + const token = (session as FarcasterSession).signerRequestToken + return ( + session.type === SessionType.Farcaster && + !!token && + // strict mode + (strict ? token !== FAKE_SIGNER_REQUEST_TOKEN : true) + ) + } + + static isSponsorship( + session: Session | null, + strict = false, + ): session is FarcasterSession & { signerRequestToken: string; sponsorshipSignature: string } { + if (!session) return false + return ( + FarcasterSession.isGrantByPermission(session, strict) && + !!(session as FarcasterSession).sponsorshipSignature + ) + } + + static isRelayService(session: Session | null): session is FarcasterSession & { channelToken: string } { + if (!session) return false + return session.type === 'Farcaster' && !!(session as FarcasterSession).channelToken + } + + static isLoginByWallet(session: Session | null): session is FarcasterSession & { walletAddress: string } { + if (!session) return false + return session.type === 'Farcaster' && !!(session as FarcasterSession).walletAddress + } +} diff --git a/packages/web3-providers/src/Firefly/Session.ts b/packages/web3-providers/src/Firefly/Session.ts new file mode 100644 index 000000000000..cd42b8f36006 --- /dev/null +++ b/packages/web3-providers/src/Firefly/Session.ts @@ -0,0 +1,82 @@ +import { BaseSession } from '../Session/Session' +import { SessionType, type Session } from '../types/Session' + +export const WARPCAST_ROOT_URL_V2 = 'https://api.warpcast.com/v2' +export const FAKE_SIGNER_REQUEST_TOKEN = 'fake_signer_request_token' + +export enum FarcasterSponsorship { + Firefly = 'firefly', +} + +export type FireflySessionSignature = { + address: string + message: string + signature: string +} + +export type FireflySessionPayload = { + /** + * indicate a new firefly binding when it was created + */ + isNew?: boolean + + /** + * numeric user ID + */ + uid?: string + /** + * UUID of the user + */ + accountId?: string + avatar?: string | null + displayName?: string | null +} + +export class FireflySession extends BaseSession implements Session { + constructor( + accountId: string, + accessToken: string, + public parent: Session | null, + public signature: FireflySessionSignature | null, + /** + * @deprecated + * This field always false. Use `payload.isNew` instead + */ + public isNew?: boolean, + public payload?: FireflySessionPayload, + ) { + super(SessionType.Firefly, accountId, accessToken, 0, 0) + } + + /** + * For users after this patch use accountId in UUID format for events. + * For legacy users use profileId in numeric format for events. + */ + get accountIdForEvent() { + return this.payload?.accountId ?? this.profileId + } + + override serialize(): `${SessionType}:${string}:${string}:${string}` { + return [ + super.serialize(), + // parent session + this.parent ? btoa(this.parent.serialize()) : '', + // signature if session created by signing a message + this.signature ? encodeAsciiPayload(this.signature) : '', + // isNew flag + this.isNew ? '1' : '0', + // extra data payload + this.payload ? encodeNoAsciiPayload(this.payload) : '', + ].join(':') as `${SessionType}:${string}:${string}:${string}` + } + + override async refresh(): Promise { + // throw new NotAllowedError() + throw new Error('Not allowed') + } + + override async destroy(): Promise { + // throw new NotAllowedError() + throw new Error('Not allowed') + } +} diff --git a/packages/web3-providers/src/Firefly/SessionHolder.ts b/packages/web3-providers/src/Firefly/SessionHolder.ts new file mode 100644 index 000000000000..8702def1ddb9 --- /dev/null +++ b/packages/web3-providers/src/Firefly/SessionHolder.ts @@ -0,0 +1,34 @@ +import type { NextFetchersOptions } from '@/helpers/getNextFetchers.js' +import type { FireflySession } from './Session.js' +import { fetchJSON } from '../helpers/fetchJSON.js' +import { SessionHolder } from '../Session/SessionHolder.js' + +class FireflySessionHolder extends SessionHolder { + fetchWithSessionGiven(session: FireflySession) { + return (url: string, init?: RequestInit) => { + return fetchJSON(url, { + ...init, + headers: { ...init?.headers, Authorization: `Bearer ${session.token}` }, + }) + } + } + + override async fetchWithSession(url: string, init?: RequestInit, options?: NextFetchersOptions) { + const authToken = this.sessionRequired.token + + return fetchJSON( + url, + { + ...init, + headers: { ...init?.headers, Authorization: `Bearer ${authToken}` }, + }, + options, + ) + } + + override fetchWithoutSession(url: string, init?: RequestInit, options?: NextFetchersOptions) { + return fetchJSON(url, init, options) + } +} + +export const fireflySessionHolder = new FireflySessionHolder() diff --git a/packages/web3-providers/src/Firefly/index.ts b/packages/web3-providers/src/Firefly/index.ts index 5245497e9246..23b2ff74a379 100644 --- a/packages/web3-providers/src/Firefly/index.ts +++ b/packages/web3-providers/src/Firefly/index.ts @@ -2,4 +2,6 @@ export * from './Config.js' export * from './RedPacket.js' export * from './Twitter.js' export * from './Farcaster.js' +export * from './Session.js' +export * from './FarcasterSession.js' export { FIREFLY_SITE_URL } from './constants.js' diff --git a/packages/web3-providers/src/Session/Session.ts b/packages/web3-providers/src/Session/Session.ts new file mode 100644 index 000000000000..53b9eaa7f9ae --- /dev/null +++ b/packages/web3-providers/src/Session/Session.ts @@ -0,0 +1,27 @@ +import { type SessionType, type Session } from '../types/Session.js' + +export abstract class BaseSession implements Session { + constructor( + public type: SessionType, + public profileId: string, + public token: string, + public createdAt: number, + public expiresAt: number, + ) {} + + serialize(): `${SessionType}:${string}` { + const body = JSON.stringify({ + type: this.type, + token: this.token, + profileId: this.profileId, + createdAt: this.createdAt, + expiresAt: this.expiresAt, + }) + + return `${this.type}:${btoa(body)}` + } + + abstract refresh(): Promise + + abstract destroy(): Promise +} diff --git a/packages/web3-providers/src/Session/SessionHolder.ts b/packages/web3-providers/src/Session/SessionHolder.ts new file mode 100644 index 000000000000..b4e8b0ecd215 --- /dev/null +++ b/packages/web3-providers/src/Session/SessionHolder.ts @@ -0,0 +1,70 @@ +import type { Session } from '../types/Session.js' + +export class SessionHolder { + protected internalSession: T | null = null + + private removeQueries() { + if (!this.session) return + } + + get session() { + return this.internalSession + } + + get sessionRequired() { + if (!this.internalSession) throw new Error('No session found.') + return this.internalSession + } + + assertSession(message?: string) { + try { + return this.sessionRequired + } catch (error: unknown) { + if (typeof message === 'string') throw new Error(message) + throw error + } + } + + refreshSession(): Promise { + throw new Error('Not implemented') + } + + resumeSession(session: T) { + this.removeQueries() + this.internalSession = session + } + + removeSession() { + this.removeQueries() + this.internalSession = null + } + + withSession unknown>(callback: K, required = false) { + return callback(required ? this.sessionRequired : this.session) as ReturnType + } + + fetchWithSession(url: string, init?: RequestInit, options?: { withSession?: boolean }): Promise { + throw new Error('Not implemented') + } + + fetchWithoutSession(url: string, init?: RequestInit, options?: { withSession?: boolean }): Promise { + throw new Error('Not implemented') + } + + /** + * Fetch w/ or w/o session. + * + * withSession = true: fetch with session + * withSession = false: fetch without session + * withSession = undefined: fetch with session if session exists + * @param url + * @param init + * @param withSession + */ + fetch(url: string, init?: RequestInit, options?: { withSession?: boolean }): Promise { + if (options?.withSession === true) return this.fetchWithSession(url, init, options) + if (options?.withSession === false) return this.fetchWithoutSession(url, init, options) + if (this.session) return this.fetchWithSession(url, init, options) + return this.fetchWithoutSession(url, init, options) + } +} diff --git a/packages/web3-providers/src/entry.ts b/packages/web3-providers/src/entry.ts index fb1e13e0ad0d..80ad8b7d2453 100644 --- a/packages/web3-providers/src/entry.ts +++ b/packages/web3-providers/src/entry.ts @@ -108,7 +108,14 @@ export { Airdrop } from './Airdrop/index.js' // Firefly -export { FireflyConfig, FireflyRedPacket, FireflyTwitter, FireflyFarcaster, FIREFLY_SITE_URL } from './Firefly/index.js' +export { + FireflyConfig, + FireflyRedPacket, + FireflyTwitter, + FireflyFarcaster, + FIREFLY_SITE_URL, + FarcasterSession, +} from './Firefly/index.js' // FiatCurrencyRate export { FiatCurrencyRate } from './FiatCurrencyRate/index.js' diff --git a/packages/web3-providers/src/helpers/encodeSessionPayload.ts b/packages/web3-providers/src/helpers/encodeSessionPayload.ts new file mode 100644 index 000000000000..d6d6be83fc73 --- /dev/null +++ b/packages/web3-providers/src/helpers/encodeSessionPayload.ts @@ -0,0 +1,17 @@ +import { parseJSON } from './parseJSON' + +export function encodeAsciiPayload(payload: unknown) { + return btoa(JSON.stringify(payload)) +} + +export function decodeAsciiPayload(payload: string) { + return parseJSON(atob(payload)) +} + +export function encodeNoAsciiPayload(payload: unknown) { + return btoa(unescape(encodeURIComponent(JSON.stringify(payload)))) +} + +export function decodeNoAsciiPayload(payload: string) { + return parseJSON(decodeURIComponent(escape(atob(payload)))) +} diff --git a/packages/web3-providers/src/types/Session.ts b/packages/web3-providers/src/types/Session.ts new file mode 100644 index 000000000000..dfb47e13e2e0 --- /dev/null +++ b/packages/web3-providers/src/types/Session.ts @@ -0,0 +1,67 @@ +export enum SessionType { + Apple = 'Apple', + Email = 'Email', + Google = 'Google', + Telegram = 'Telegram', + Twitter = 'Twitter', + Lens = 'Lens', + Farcaster = 'Farcaster', + Firefly = 'Firefly', + Bsky = 'Bsky', +} + +export interface Session { + profileId: string | number + + /** + * The type of social platform that the session is associated with. + */ + type: SessionType + + /** + * The secret associated with the authenticated account. + * It's typically used to validate the authenticity of the user in subsequent + * requests to a server or API. + */ + token: string + + /** + * Represents the time at which the session was established or last updated. + * It's represented as a UNIX timestamp, counting the number of seconds since + * January 1, 1970 (the UNIX epoch). + */ + createdAt: number + + /** + * Specifies when the authentication or session will expire. + * It's represented as a UNIX timestamp, which counts the number of seconds + * since January 1, 1970 (known as the UNIX epoch). + * A value of 0 indicates that the session has no expiration. + */ + expiresAt: number + + /** + * Serializes the session data into a string format. + * This can be useful for storing or transmitting the session data. + * + * @returns A string representation of the session. + */ + serialize(): string + + /** + * Refreshes the session, typically by acquiring a new token or extending the + * expiration time. This method might make an asynchronous call to a backend + * server to perform the refresh operation. + * + * @returns A promise that resolves when the session is successfully refreshed. + */ + refresh(): Promise + + /** + * Destroys the session, ending the authenticated state. This might involve + * invalidating the token on a backend server or performing other cleanup operations. + * + * @returns A promise that resolves when the session is successfully destroyed. + */ + destroy(): Promise +} From 358bb73a17dc3637b91e9004590b0b6e890849a4 Mon Sep 17 00:00:00 2001 From: Wukong Sun Date: Tue, 2 Sep 2025 17:55:30 +0800 Subject: [PATCH 3/7] feat(firefly): add firefly session bind and restore with farcaster integration --- cspell.json | 27 ++-- .../SetupGuide/AccountConnectStatus.tsx | 2 +- .../ConnectSocialAccountModal/index.tsx | 2 +- .../ConnectFirefly/bindFireflySession.ts | 65 ++++++++++ .../bindOrRestoreFireflySession.ts | 22 ++++ .../createAccountByRelayService.ts | 15 ++- .../pages/Personas/ConnectFirefly/index.tsx | 116 ++++++++++++++++-- .../ConnectFirefly/restoreFireflySession.ts | 55 +++++++++ .../SiteAdaptor/SocialFeeds/SocialFeed.tsx | 1 + packages/public-api/src/types/index.ts | 1 + packages/shared-base/src/errors.ts | 41 +++++++ packages/shared-base/src/index.ts | 1 + .../src/Farcaster/getFarcasterFriendship.ts | 18 +++ .../src/Farcaster/getFarcasterProfileById.ts | 21 ++++ .../web3-providers/src/Farcaster/index.ts | 2 + .../web3-providers/src/Firefly/RedPacket.ts | 4 +- .../web3-providers/src/Firefly/Session.ts | 12 +- .../src/Firefly/SessionHolder.ts | 2 +- .../web3-providers/src/Firefly/constants.ts | 6 + packages/web3-providers/src/Firefly/index.ts | 4 + .../Firefly/patchFarcasterSessionRequired.ts | 15 +++ .../src/FireflyAccount/index.ts | 32 +++++ .../src/Session/SessionHolder.ts | 15 ++- packages/web3-providers/src/entry-types.ts | 1 + packages/web3-providers/src/entry.ts | 11 +- packages/web3-providers/src/helpers/social.ts | 3 + packages/web3-providers/src/types/Firefly.ts | 57 +++++++++ packages/web3-providers/src/types/Social.ts | 4 +- 28 files changed, 496 insertions(+), 59 deletions(-) create mode 100644 packages/mask/popups/pages/Personas/ConnectFirefly/bindFireflySession.ts create mode 100644 packages/mask/popups/pages/Personas/ConnectFirefly/restoreFireflySession.ts create mode 100644 packages/shared-base/src/errors.ts create mode 100644 packages/web3-providers/src/Farcaster/getFarcasterFriendship.ts create mode 100644 packages/web3-providers/src/Farcaster/getFarcasterProfileById.ts create mode 100644 packages/web3-providers/src/Farcaster/index.ts create mode 100644 packages/web3-providers/src/Firefly/patchFarcasterSessionRequired.ts create mode 100644 packages/web3-providers/src/FireflyAccount/index.ts diff --git a/cspell.json b/cspell.json index 544c81f53015..d7f627ae9b19 100644 --- a/cspell.json +++ b/cspell.json @@ -1,14 +1,7 @@ { "version": "0.2", "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", - "dictionaries": [ - "typescript", - "node", - "npm", - "html", - "css", - "fonts" - ], + "dictionaries": ["typescript", "node", "npm", "html", "css", "fonts"], "ignorePaths": [ "./packages/gun-utils/gun.js", "./packages/icons/**", @@ -34,11 +27,7 @@ "pnpm-workspace.yaml", "qya-aa.json" ], - "TODO: fix those words": [ - "bridgable", - "clonable", - "sniffings" - ], + "TODO: fix those words": ["bridgable", "clonable", "sniffings"], "ignoreWords": [ "aeth", "algr", @@ -252,15 +241,12 @@ "zksync", "zora" ], - "ignoreRegExpList": [ - "/[A-Za-z0-9]{44}/", - "/[A-Za-z0-9]{46}/", - "/[A-Za-z0-9]{59}/" - ], + "ignoreRegExpList": ["/[A-Za-z0-9]{44}/", "/[A-Za-z0-9]{46}/", "/[A-Za-z0-9]{59}/"], "overrides": [], "words": [ "arbitrum", "boba", + "Bsky", "cashtags", "celo", "deeplink", @@ -269,6 +255,7 @@ "linkedin", "luma", "muln", + "pathnames", "reposted", "reposts", "sepolia", @@ -278,6 +265,8 @@ "txid", "waitlist", "WARPCAST", + "webm", "youtube" ] -} \ No newline at end of file +} + diff --git a/packages/mask/content-script/components/InjectedComponents/SetupGuide/AccountConnectStatus.tsx b/packages/mask/content-script/components/InjectedComponents/SetupGuide/AccountConnectStatus.tsx index 77099aab16ec..da570f1cb74e 100644 --- a/packages/mask/content-script/components/InjectedComponents/SetupGuide/AccountConnectStatus.tsx +++ b/packages/mask/content-script/components/InjectedComponents/SetupGuide/AccountConnectStatus.tsx @@ -1,3 +1,4 @@ +import { Trans } from '@lingui/react/macro' import { Icons } from '@masknet/icons' import { BindingDialog, LoadingStatus, SOCIAL_MEDIA_ROUND_ICON_MAPPING, type BindingDialogProps } from '@masknet/shared' import { Sniffings, SOCIAL_MEDIA_NAME } from '@masknet/shared-base' @@ -6,7 +7,6 @@ import { Box, Button, Typography } from '@mui/material' import { memo } from 'react' import { activatedSiteAdaptorUI } from '../../../site-adaptor-infra/ui.js' import { SetupGuideContext } from './SetupGuideContext.js' -import { Trans } from '@lingui/react/macro' const useStyles = makeStyles()((theme) => { return { diff --git a/packages/mask/popups/modals/ConnectSocialAccountModal/index.tsx b/packages/mask/popups/modals/ConnectSocialAccountModal/index.tsx index babf014bdb6c..2015bd7e351c 100644 --- a/packages/mask/popups/modals/ConnectSocialAccountModal/index.tsx +++ b/packages/mask/popups/modals/ConnectSocialAccountModal/index.tsx @@ -5,12 +5,12 @@ import { EMPTY_LIST, EnhanceableSite, PopupRoutes } from '@masknet/shared-base' import { Telemetry } from '@masknet/web3-telemetry' import { EventType } from '@masknet/web3-telemetry/types' import { memo, useCallback } from 'react' +import { useNavigate } from 'react-router-dom' import { requestPermissionFromExtensionPage } from '../../../shared-ui/index.js' import { EventMap } from '../../../shared/definitions/event.js' import { ConnectSocialAccounts } from '../../components/ConnectSocialAccounts/index.js' import { ActionModal, type ActionModalBaseProps } from '../../components/index.js' import { useSupportSocialNetworks } from '../../hooks/index.js' -import { useNavigate } from 'react-router-dom' export const ConnectSocialAccountModal = memo(function ConnectSocialAccountModal(props) { const { data: definedSocialNetworks = EMPTY_LIST } = useSupportSocialNetworks() diff --git a/packages/mask/popups/pages/Personas/ConnectFirefly/bindFireflySession.ts b/packages/mask/popups/pages/Personas/ConnectFirefly/bindFireflySession.ts new file mode 100644 index 000000000000..8c67083f8b0e --- /dev/null +++ b/packages/mask/popups/pages/Personas/ConnectFirefly/bindFireflySession.ts @@ -0,0 +1,65 @@ +import { + FarcasterSession, + FIREFLY_ROOT_URL, + fireflySessionHolder, + patchFarcasterSessionRequired, + resolveFireflyResponseData, +} from '@masknet/web3-providers' +import { SessionType, type FireflyConfigAPI, type Session } from '@masknet/web3-providers/types' +import urlcat from 'urlcat' + +async function bindFarcasterSessionToFirefly(session: FarcasterSession, signal?: AbortSignal) { + const isGrantByPermission = FarcasterSession.isGrantByPermission(session, true) + const isRelayService = FarcasterSession.isRelayService(session) + + if (!isGrantByPermission && !isRelayService) + throw new Error( + '[bindFarcasterSessionToFirefly] Only grant-by-permission or relay service sessions are allowed.', + ) + + const response = await fireflySessionHolder.fetch( + urlcat(FIREFLY_ROOT_URL, '/v3/user/bindFarcaster'), + { + method: 'POST', + body: JSON.stringify({ + token: isGrantByPermission ? session.signerRequestToken : undefined, + channelToken: isRelayService ? session.channelToken : undefined, + isForce: false, + }), + signal, + }, + ) + + if (response.error?.some((x) => x.includes('Farcaster binding timed out'))) { + throw new Error('Bind Farcaster account to Firefly timeout.') + } + + // If the farcaster is already bound to another account, throw an error. + if ( + isRelayService && + response.error?.some((x) => x.includes('This farcaster already bound to the other account')) + ) { + throw new Error('This Farcaster account has already bound to another Firefly account.') + } + + const data = resolveFireflyResponseData(response) + patchFarcasterSessionRequired(session, data.fid, data.farcaster_signer_private_key) + return data +} + +/** + * Bind a lens or farcaster session to the currently logged-in Firefly session. + * @param session + * @param signal + * @returns + */ +export async function bindFireflySession(session: Session, signal?: AbortSignal) { + // Ensure that the Firefly session is resumed before calling this function. + fireflySessionHolder.assertSession() + if (session.type === SessionType.Farcaster) { + return bindFarcasterSessionToFirefly(session as FarcasterSession, signal) + } else if (session.type === SessionType.Firefly) { + throw new Error('Not allowed') + } + throw new Error(`Unknown session type: ${session.type}`) +} diff --git a/packages/mask/popups/pages/Personas/ConnectFirefly/bindOrRestoreFireflySession.ts b/packages/mask/popups/pages/Personas/ConnectFirefly/bindOrRestoreFireflySession.ts index e69de29bb2d1..62acaad4f268 100644 --- a/packages/mask/popups/pages/Personas/ConnectFirefly/bindOrRestoreFireflySession.ts +++ b/packages/mask/popups/pages/Personas/ConnectFirefly/bindOrRestoreFireflySession.ts @@ -0,0 +1,22 @@ +import { fireflySessionHolder } from '@masknet/web3-providers' +import type { Session } from '@masknet/web3-providers/types' +import { bindFireflySession } from './bindFireflySession' +import { restoreFireflySession } from './restoreFireflySession' + +export async function bindOrRestoreFireflySession(session: Session, signal?: AbortSignal) { + try { + if (fireflySessionHolder.session) { + await bindFireflySession(session, signal) + + // this will return the existing session + return fireflySessionHolder.assertSession( + '[bindOrRestoreFireflySession] Failed to bind farcaster session with firefly.', + ) + } else { + throw new Error('[bindOrRestoreFireflySession] Firefly session is not available.') + } + } catch (error) { + // this will create a new session + return restoreFireflySession(session, signal) + } +} diff --git a/packages/mask/popups/pages/Personas/ConnectFirefly/createAccountByRelayService.ts b/packages/mask/popups/pages/Personas/ConnectFirefly/createAccountByRelayService.ts index 69ffee122391..75eabc338889 100644 --- a/packages/mask/popups/pages/Personas/ConnectFirefly/createAccountByRelayService.ts +++ b/packages/mask/popups/pages/Personas/ConnectFirefly/createAccountByRelayService.ts @@ -1,7 +1,7 @@ -import type { Account } from '@masknet/shared-base' import { fetchJSON } from '@masknet/web3-providers/helpers' -import { FarcasterSession } from '@masknet/web3-providers' +import { FarcasterSession, getFarcasterProfileById, type FireflyAccount } from '@masknet/web3-providers' import urlcat from 'urlcat' +import { bindOrRestoreFireflySession } from './bindOrRestoreFireflySession' const FARCASTER_REPLY_URL = 'https://relay.farcaster.xyz' const NOT_DEPEND_SECRET = '[TO_BE_REPLACED_LATER]' @@ -20,9 +20,13 @@ async function createSession(signal?: AbortSignal) { const url = urlcat(FARCASTER_REPLY_URL, '/v1/channel') const response = await fetchJSON(url, { method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, body: JSON.stringify({ - siteUri: 'https://www.mask.io', - domain: 'www.mask.io', + // cspell: disable-next-line + siweUri: 'https://firefly.social', + domain: 'firefly.social', }), signal, }) @@ -51,6 +55,7 @@ export async function createAccountByRelayService(callback?: (url: string) => vo // polling for the session to be ready const fireflySession = await bindOrRestoreFireflySession(session, signal) + console.log('fireflySession', fireflySession) // profile id is available after the session is ready const profile = await getFarcasterProfileById(session.profileId) @@ -60,5 +65,5 @@ export async function createAccountByRelayService(callback?: (url: string) => vo session, profile, fireflySession, - } satisfies Account + } satisfies FireflyAccount } diff --git a/packages/mask/popups/pages/Personas/ConnectFirefly/index.tsx b/packages/mask/popups/pages/Personas/ConnectFirefly/index.tsx index 0468a445c709..946c7abdd300 100644 --- a/packages/mask/popups/pages/Personas/ConnectFirefly/index.tsx +++ b/packages/mask/popups/pages/Personas/ConnectFirefly/index.tsx @@ -1,23 +1,118 @@ -import { useLingui } from '@lingui/react/macro' -import { PopupHomeTabType } from '@masknet/shared' -import { QRCode } from 'react-qrcode-logo' -import { PopupRoutes } from '@masknet/shared-base' -import { makeStyles } from '@masknet/theme' +import { Trans, useLingui } from '@lingui/react/macro' +import { attachProfile } from '@masknet/plugin-infra/dom/context' +import { PersonaContext, PopupHomeTabType } from '@masknet/shared' +import { + AbortError, + EnhanceableSite, + FarcasterPatchSignerError, + FireflyAlreadyBoundError, + FireflyBindTimeoutError, + PopupRoutes, + ProfileIdentifier, + TimeoutError, +} from '@masknet/shared-base' +import { LoadingBase, makeStyles, usePopupCustomSnackbar } from '@masknet/theme' +import { addAccount, type AccountOptions, type FireflyAccount } from '@masknet/web3-providers' +import { Social } from '@masknet/web3-providers/types' import { Box } from '@mui/material' -import { memo, useCallback } from 'react' +import { memo, useCallback, useState } from 'react' +import { QRCode } from 'react-qrcode-logo' import { useNavigate } from 'react-router-dom' +import { useMount } from 'react-use' import urlcat from 'urlcat' import { useTitle } from '../../../hooks/index.js' +import { createAccountByRelayService } from './createAccountByRelayService.js' const useStyles = makeStyles()({ - container: {}, + container: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + position: 'relative', + }, + loading: { + backgroundColor: 'rgba(255,255,255,0.5)', + position: 'absolute', + inset: 0, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, }) +function useLogin() { + const { showSnackbar } = usePopupCustomSnackbar() + return useCallback( + async function login(createAccount: () => Promise, options?: Omit) { + try { + const account = await createAccount() + + const done = await addAccount(account, options) + console.log('created account', account) + if (done) showSnackbar(Your {Social.Source.Farcaster} account is now connected.) + } catch (error) { + // skip if the error is abort error + if (AbortError.is(error)) return + + // if login timed out, let the user refresh the QR code + if (error instanceof TimeoutError || error instanceof FireflyBindTimeoutError) { + showSnackbar(This QR code is longer valid. Please scan a new one to continue.) + return + } + + // failed to patch the signer + if (error instanceof FarcasterPatchSignerError) throw error + + // if any error occurs, close the modal + // by this we don't need to do error handling in UI part. + // if the account is already bound to another account, show a warning message + if (error instanceof FireflyAlreadyBoundError) { + showSnackbar( + + The account you are trying to log in with is already linked to a different Firefly account. + , + ) + return + } + + throw error + } + }, + [showSnackbar], + ) +} + export const Component = memo(function ConnectFireflyPage() { const { t } = useLingui() const { classes } = useStyles() + const [url, setUrl] = useState('') const navigate = useNavigate() + const login = useLogin() + const { currentPersona } = PersonaContext.useContainer() + + useMount(async () => { + login(async () => { + try { + const account = await createAccountByRelayService((url) => { + setUrl(url) + }) + console.log('account', account) + if (attachProfile && currentPersona) { + await attachProfile( + ProfileIdentifier.of(EnhanceableSite.Farcaster, account.session.profileId).unwrap(), + currentPersona.identifier, + { connectionConfirmState: 'pending', token: account.session.token }, + ) + } + console.log('account', account) + return account + } catch (err) { + console.log('error', err) + throw err + } + }) + }) const handleBack = useCallback(() => { navigate(urlcat(PopupRoutes.Personas, { tab: PopupHomeTabType.ConnectedWallets }), { @@ -29,7 +124,12 @@ export const Component = memo(function ConnectFireflyPage() { return ( - + + {!url ? +
+ +
+ : null}
) }) diff --git a/packages/mask/popups/pages/Personas/ConnectFirefly/restoreFireflySession.ts b/packages/mask/popups/pages/Personas/ConnectFirefly/restoreFireflySession.ts new file mode 100644 index 000000000000..941c253243ce --- /dev/null +++ b/packages/mask/popups/pages/Personas/ConnectFirefly/restoreFireflySession.ts @@ -0,0 +1,55 @@ +import { + FarcasterSession, + FIREFLY_ROOT_URL, + FireflySession, + patchFarcasterSessionRequired, + resolveFireflyResponseData, +} from '@masknet/web3-providers' +import type { FireflyConfigAPI, Session } from '@masknet/web3-providers/types' +import urlcat from 'urlcat' + +export async function restoreFireflySessionFromFarcaster(session: FarcasterSession, signal?: AbortSignal) { + const isGrantByPermission = FarcasterSession.isGrantByPermission(session, true) + const isRelayService = FarcasterSession.isRelayService(session) + if (!isGrantByPermission && !isRelayService) + throw new Error('[restoreFireflySession] Only grant-by-permission or relay service sessions are allowed.') + + const url = urlcat(FIREFLY_ROOT_URL, '/v3/auth/farcaster/login') + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + token: isGrantByPermission ? session.signerRequestToken : undefined, + channelToken: isRelayService ? session.channelToken : undefined, + }), + signal, + }) + + const json: FireflyConfigAPI.LoginResponse = await response.json() + if (!response.ok && json.error?.includes('Farcaster login timed out')) + throw new Error('[restoreFireflySession] Farcaster login timed out.') + + const data = resolveFireflyResponseData(json) + if (data.fid && data.accountId && data.accessToken) { + patchFarcasterSessionRequired(session as FarcasterSession, data.fid, data.farcaster_signer_private_key) + return new FireflySession(data.uid ?? data.accountId, data.accessToken, session, null, false, data) + } + throw new Error('[restoreFireflySession] Failed to restore firefly session.') +} + +/** + * Restore firefly session from a lens or farcaster session. + * @param session + * @param signal + * @returns + */ +export function restoreFireflySession(session: Session, signal?: AbortSignal) { + if (session.type === 'Farcaster') { + return restoreFireflySessionFromFarcaster(session as FarcasterSession, signal) + } else if (session.type === 'Firefly') { + throw new Error('[restoreFireflySession] Firefly session is not allowed.') + } + throw new Error(`[restoreFireflySession] Unknown session type: ${session.type}`) +} diff --git a/packages/plugins/RSS3/src/SiteAdaptor/SocialFeeds/SocialFeed.tsx b/packages/plugins/RSS3/src/SiteAdaptor/SocialFeeds/SocialFeed.tsx index 1bbbfe33c20c..ff6f2d543939 100644 --- a/packages/plugins/RSS3/src/SiteAdaptor/SocialFeeds/SocialFeed.tsx +++ b/packages/plugins/RSS3/src/SiteAdaptor/SocialFeeds/SocialFeed.tsx @@ -183,6 +183,7 @@ const useStyles = makeStyles { diff --git a/packages/public-api/src/types/index.ts b/packages/public-api/src/types/index.ts index f6e8f566fd76..e76ea705589b 100644 --- a/packages/public-api/src/types/index.ts +++ b/packages/public-api/src/types/index.ts @@ -14,6 +14,7 @@ export interface JsonRpcResponse { export interface LinkedProfileDetails { connectionConfirmState: 'confirmed' | 'pending' + token?: string } // These type MUST be sync with packages/shared-base/src/crypto/JWKType diff --git a/packages/shared-base/src/errors.ts b/packages/shared-base/src/errors.ts new file mode 100644 index 000000000000..88b895dc2dbf --- /dev/null +++ b/packages/shared-base/src/errors.ts @@ -0,0 +1,41 @@ +export class AbortError extends Error { + override name = 'AbortError' + + constructor(message = 'Aborted') { + super(message) + } + + static is(error: unknown) { + return error instanceof AbortError || (error instanceof DOMException && error.name === 'AbortError') + } +} + +export class FarcasterPatchSignerError extends Error { + override name = 'FarcasterPatchSignerError' + + constructor(public fid: number) { + super(`Failed to patch signer key to Farcaster session: ${fid}`) + } +} + +export class TimeoutError extends Error { + override name = 'TimeoutError' + + constructor(message?: string) { + super(message ?? 'Timeout.') + } +} + +export class FireflyBindTimeoutError extends Error { + override name = 'FireflyBindTimeoutError' + constructor(public source: string) { + super(`Bind ${source} account to Firefly timeout.`) + } +} +export class FireflyAlreadyBoundError extends Error { + override name = 'FireflyAlreadyBoundError' + + constructor(public source: string) { + super(`This ${source} account has already bound to another Firefly account.`) + } +} diff --git a/packages/shared-base/src/index.ts b/packages/shared-base/src/index.ts index f51a56eaa1b0..18e21038d2ba 100644 --- a/packages/shared-base/src/index.ts +++ b/packages/shared-base/src/index.ts @@ -4,6 +4,7 @@ export * from './constants.js' export * from './types.js' export * from './types/index.js' export * from './helpers/index.js' +export * from './errors.js' export * from './Messages/Events.js' export * from './Messages/CrossIsolationEvents.js' diff --git a/packages/web3-providers/src/Farcaster/getFarcasterFriendship.ts b/packages/web3-providers/src/Farcaster/getFarcasterFriendship.ts new file mode 100644 index 000000000000..5bc6fd93c90c --- /dev/null +++ b/packages/web3-providers/src/Farcaster/getFarcasterFriendship.ts @@ -0,0 +1,18 @@ +import urlcat from 'urlcat' +import { FIREFLY_ROOT_URL } from '../Firefly/constants' +import { resolveFireflyResponseData } from '../Firefly/helpers' +import { fetchJSON } from '../helpers/fetchJSON' +import type { FireflyConfigAPI } from '../types/Firefly' + +export async function getFarcasterFriendship(sourceFid: string, destFid: string) { + const response = await fetchJSON( + urlcat(FIREFLY_ROOT_URL, '/v2/farcaster-hub/user/friendship', { + sourceFid, + destFid, + }), + { + method: 'GET', + }, + ) + return resolveFireflyResponseData(response) +} diff --git a/packages/web3-providers/src/Farcaster/getFarcasterProfileById.ts b/packages/web3-providers/src/Farcaster/getFarcasterProfileById.ts new file mode 100644 index 000000000000..1d5469bf689e --- /dev/null +++ b/packages/web3-providers/src/Farcaster/getFarcasterProfileById.ts @@ -0,0 +1,21 @@ +import urlcat from 'urlcat' +import { fireflySessionHolder } from '../Firefly/SessionHolder' +import { FIREFLY_ROOT_URL } from '../Firefly/constants' +import { formatFarcasterProfileFromFirefly, resolveFireflyResponseData } from '../Firefly/helpers' +import { getFarcasterFriendship } from './getFarcasterFriendship' +import type { FireflyConfigAPI } from '../types/Firefly' + +export async function getFarcasterProfileById(profileId: string, viewerFid?: string) { + const response = await fireflySessionHolder.fetch( + urlcat(FIREFLY_ROOT_URL, '/v2/farcaster-hub/user/profile', { + fid: profileId, + sourceFid: viewerFid, + }), + { + method: 'GET', + }, + ) + const user = resolveFireflyResponseData(response) + const friendship = viewerFid ? await getFarcasterFriendship(viewerFid, profileId) : null + return formatFarcasterProfileFromFirefly({ ...user, ...friendship }) +} diff --git a/packages/web3-providers/src/Farcaster/index.ts b/packages/web3-providers/src/Farcaster/index.ts new file mode 100644 index 000000000000..dc5ae2d6f49f --- /dev/null +++ b/packages/web3-providers/src/Farcaster/index.ts @@ -0,0 +1,2 @@ +export * from './getFarcasterProfileById.js' +export * from './getFarcasterFriendship.js' diff --git a/packages/web3-providers/src/Firefly/RedPacket.ts b/packages/web3-providers/src/Firefly/RedPacket.ts index 2b215c8f40d0..207502397d38 100644 --- a/packages/web3-providers/src/Firefly/RedPacket.ts +++ b/packages/web3-providers/src/Firefly/RedPacket.ts @@ -10,12 +10,10 @@ import { import urlcat from 'urlcat' import { fetchJSON } from '../entry-helpers.js' import { FireflyRedPacketAPI, type FireflyResponse } from '../entry-types.js' +import { FIREFLY_ROOT_URL } from './constants.js' const siteType = getSiteType() const SITE_URL = siteType === EnhanceableSite.Firefly ? location.origin : 'https://firefly.social' -const FIREFLY_ROOT_URL = - process.env.NEXT_PUBLIC_FIREFLY_API_URL || - (process.env.NODE_ENV === 'development' ? 'https://api-dev.firefly.land' : 'https://api.firefly.land') function fetchFireflyJSON(url: string, init?: RequestInit): Promise { return fetchJSON(url, { diff --git a/packages/web3-providers/src/Firefly/Session.ts b/packages/web3-providers/src/Firefly/Session.ts index cd42b8f36006..42e8fda70b27 100644 --- a/packages/web3-providers/src/Firefly/Session.ts +++ b/packages/web3-providers/src/Firefly/Session.ts @@ -1,12 +1,6 @@ -import { BaseSession } from '../Session/Session' -import { SessionType, type Session } from '../types/Session' - -export const WARPCAST_ROOT_URL_V2 = 'https://api.warpcast.com/v2' -export const FAKE_SIGNER_REQUEST_TOKEN = 'fake_signer_request_token' - -export enum FarcasterSponsorship { - Firefly = 'firefly', -} +import { encodeAsciiPayload, encodeNoAsciiPayload } from '../helpers/encodeSessionPayload.js' +import { BaseSession } from '../Session/Session.js' +import { SessionType, type Session } from '../types/Session.js' export type FireflySessionSignature = { address: string diff --git a/packages/web3-providers/src/Firefly/SessionHolder.ts b/packages/web3-providers/src/Firefly/SessionHolder.ts index 8702def1ddb9..ac73425653eb 100644 --- a/packages/web3-providers/src/Firefly/SessionHolder.ts +++ b/packages/web3-providers/src/Firefly/SessionHolder.ts @@ -1,7 +1,7 @@ -import type { NextFetchersOptions } from '@/helpers/getNextFetchers.js' import type { FireflySession } from './Session.js' import { fetchJSON } from '../helpers/fetchJSON.js' import { SessionHolder } from '../Session/SessionHolder.js' +import type { NextFetchersOptions } from '../helpers/getNextFetchers.js' class FireflySessionHolder extends SessionHolder { fetchWithSessionGiven(session: FireflySession) { diff --git a/packages/web3-providers/src/Firefly/constants.ts b/packages/web3-providers/src/Firefly/constants.ts index 91938fb1a189..b80c88e7ab31 100644 --- a/packages/web3-providers/src/Firefly/constants.ts +++ b/packages/web3-providers/src/Firefly/constants.ts @@ -5,3 +5,9 @@ export const EMAIL_REGEX = /(([^\s"(),./:;<>@[\\\]]+(\.[^\s"(),./:;<>@[\\\]]+)*)|(".+"))@((\[(?:\d{1,3}\.){3}\d{1,3}\])|(([\dA-Za-z-]+\.)+[A-Za-z]{2,}))$/u export const URL_REGEX = /((https?:\/\/)?[\da-z]+([.-][\da-z]+)*\.[a-z]{2,}(:\d{1,5})?(\/[^\n ),>]*)?)/giu + +export const FIREFLY_ROOT_URL = + process.env.NEXT_PUBLIC_FIREFLY_API_URL || + (process.env.NODE_ENV === 'development' ? 'https://api-dev.firefly.land' : 'https://api.firefly.land') + +export const NOT_DEPEND_SECRET = '[TO_BE_REPLACED_LATER]' diff --git a/packages/web3-providers/src/Firefly/index.ts b/packages/web3-providers/src/Firefly/index.ts index 23b2ff74a379..f3242bc9bc2f 100644 --- a/packages/web3-providers/src/Firefly/index.ts +++ b/packages/web3-providers/src/Firefly/index.ts @@ -4,4 +4,8 @@ export * from './Twitter.js' export * from './Farcaster.js' export * from './Session.js' export * from './FarcasterSession.js' +export * from './SessionHolder.js' +export * from './helpers.js' +export * from './constants.js' +export * from './patchFarcasterSessionRequired.js' export { FIREFLY_SITE_URL } from './constants.js' diff --git a/packages/web3-providers/src/Firefly/patchFarcasterSessionRequired.ts b/packages/web3-providers/src/Firefly/patchFarcasterSessionRequired.ts new file mode 100644 index 000000000000..06e741d718b0 --- /dev/null +++ b/packages/web3-providers/src/Firefly/patchFarcasterSessionRequired.ts @@ -0,0 +1,15 @@ +import { NOT_DEPEND_SECRET } from './constants' +import { FAKE_SIGNER_REQUEST_TOKEN, type FarcasterSession } from './FarcasterSession' + +export function patchFarcasterSessionRequired(session: FarcasterSession, fid: number, token: string | undefined) { + if (session.profileId === NOT_DEPEND_SECRET) { + session.profileId = fid.toString() + } + if (session.token === NOT_DEPEND_SECRET) { + if (!token) throw new Error(`Failed to patch signer key to Farcaster session: ${fid}`) + + session.token = token + session.signerRequestToken = FAKE_SIGNER_REQUEST_TOKEN + } + return session +} diff --git a/packages/web3-providers/src/FireflyAccount/index.ts b/packages/web3-providers/src/FireflyAccount/index.ts new file mode 100644 index 000000000000..f09dbf7e422e --- /dev/null +++ b/packages/web3-providers/src/FireflyAccount/index.ts @@ -0,0 +1,32 @@ +import type { FireflySession } from '../Firefly/Session' +import type { Session } from '../types/Session' +import type { Social } from '../types/Social' + +type AccountOrigin = 'inherent' | 'sync' + +export interface FireflyAccount { + origin?: AccountOrigin + profile: Social.Profile + session: Session + fireflySession?: FireflySession +} +export interface AccountOptions { + // set the account as the current account, default: true + setAsCurrent?: boolean | ((account: FireflyAccount) => Promise) + // skip the belongs to check, default: false + skipBelongsToCheck?: boolean + // resume accounts from firefly, default: false + skipResumeFireflyAccounts?: boolean + // resume the firefly session, default: false + skipResumeFireflySession?: boolean + // skip reporting farcaster signer, default: true + skipReportFarcasterSigner?: boolean + // skip syncing accounts, default: false + skipSyncAccounts?: boolean + // early return signal + signal?: AbortSignal +} + +export async function addAccount(_account: FireflyAccount, _options?: AccountOptions) { + return true +} diff --git a/packages/web3-providers/src/Session/SessionHolder.ts b/packages/web3-providers/src/Session/SessionHolder.ts index b4e8b0ecd215..319a60c8fa64 100644 --- a/packages/web3-providers/src/Session/SessionHolder.ts +++ b/packages/web3-providers/src/Session/SessionHolder.ts @@ -1,3 +1,4 @@ +import type { NextFetchersOptions } from '../helpers/getNextFetchers.js' import type { Session } from '../types/Session.js' export class SessionHolder { @@ -43,11 +44,19 @@ export class SessionHolder { return callback(required ? this.sessionRequired : this.session) as ReturnType } - fetchWithSession(url: string, init?: RequestInit, options?: { withSession?: boolean }): Promise { + fetchWithSession( + url: string, + init?: RequestInit, + options?: NextFetchersOptions & { withSession?: boolean }, + ): Promise { throw new Error('Not implemented') } - fetchWithoutSession(url: string, init?: RequestInit, options?: { withSession?: boolean }): Promise { + fetchWithoutSession( + url: string, + init?: RequestInit, + options?: NextFetchersOptions & { withSession?: boolean }, + ): Promise { throw new Error('Not implemented') } @@ -61,7 +70,7 @@ export class SessionHolder { * @param init * @param withSession */ - fetch(url: string, init?: RequestInit, options?: { withSession?: boolean }): Promise { + fetch(url: string, init?: RequestInit, options?: NextFetchersOptions & { withSession?: boolean }): Promise { if (options?.withSession === true) return this.fetchWithSession(url, init, options) if (options?.withSession === false) return this.fetchWithoutSession(url, init, options) if (this.session) return this.fetchWithSession(url, init, options) diff --git a/packages/web3-providers/src/entry-types.ts b/packages/web3-providers/src/entry-types.ts index 6d0b646c0a9c..1a806754037d 100644 --- a/packages/web3-providers/src/entry-types.ts +++ b/packages/web3-providers/src/entry-types.ts @@ -35,6 +35,7 @@ export * from './types/LensV3.js' export type * from './types/Storage.js' export type * from './types/Snapshot.js' export type * from './types/Store.js' +export * from './types/Session.js' // Provider Implementations export * from './DeBank/types.js' diff --git a/packages/web3-providers/src/entry.ts b/packages/web3-providers/src/entry.ts index 80ad8b7d2453..4312906dbdc0 100644 --- a/packages/web3-providers/src/entry.ts +++ b/packages/web3-providers/src/entry.ts @@ -108,14 +108,9 @@ export { Airdrop } from './Airdrop/index.js' // Firefly -export { - FireflyConfig, - FireflyRedPacket, - FireflyTwitter, - FireflyFarcaster, - FIREFLY_SITE_URL, - FarcasterSession, -} from './Firefly/index.js' +export * from './Firefly/index.js' +export * from './Farcaster/index.js' +export * from './FireflyAccount/index.js' // FiatCurrencyRate export { FiatCurrencyRate } from './FiatCurrencyRate/index.js' diff --git a/packages/web3-providers/src/helpers/social.ts b/packages/web3-providers/src/helpers/social.ts index 97166a77c9d7..8ea34702bda1 100644 --- a/packages/web3-providers/src/helpers/social.ts +++ b/packages/web3-providers/src/helpers/social.ts @@ -143,6 +143,7 @@ export function getProfileUrl(profile: Social.Profile) { if (!profile.handle) return '' return resolveProfileUrl(profile.source, profile.handle) case Social.Source.Farcaster: + case Social.Source.Firefly: if (!profile.profileId) return '' return resolveProfileUrl(profile.source, profile.profileId) default: @@ -167,6 +168,7 @@ export const resolveSourceInUrl = createLookupTableResolver { throw new Error(`Unreachable source = ${source}.`) @@ -177,6 +179,7 @@ export const resolveSocialSourceInUrl = createLookupTableResolver { throw new Error(`Unreachable source = ${source}.`) diff --git a/packages/web3-providers/src/types/Firefly.ts b/packages/web3-providers/src/types/Firefly.ts index 471b6d9fa898..2755b40bbdc2 100644 --- a/packages/web3-providers/src/types/Firefly.ts +++ b/packages/web3-providers/src/types/Firefly.ts @@ -6,6 +6,7 @@ type WithNumberChainId = WithoutChainId & { chain_id: number } export interface FireflyResponse { code: number data: T + error?: string[] } export namespace FireflyConfigAPI { @@ -133,6 +134,62 @@ export namespace FireflyConfigAPI { secretAccessKey: string sessionToken: string }> + export type LoginResponse = FireflyResponse<{ + accessToken: string + /** uuid */ + accountId: string + farcaster_signer_public_key?: string + farcaster_signer_private_key?: string + isNew: boolean + fid?: number + uid?: string + avatar?: string + displayName?: string + telegram_username?: string + telegram_user_id?: string + }> + export type BindResponse = FireflyResponse<{ + fid: number + farcaster_signer_public_key?: string + farcaster_signer_private_key?: string + account_id: string + account_raw_id: number + twitters: Array<{ + id: string + handle: string + }> + wallets: Array<{ + _id: number + id: string // the wallet address as id + createdAt: string + connectedAt: string + updatedAt: string + address: string + chain: string + ens: unknown + }> + }> + export interface User { + pfp: string + username: string + display_name: string + bio?: string + following: number + followers: number + addresses: string[] + solanaAddresses: string[] + fid: string + isFollowing?: boolean + /** if followed by the user, no relation to whether you follow the user or not */ + isFollowedBack?: boolean + isPowerUser?: boolean + isProUser?: boolean + } + export type UserResponse = FireflyResponse + export type FriendshipResponse = FireflyResponse<{ + isFollowing: boolean + isFollowedBack: boolean + }> } export namespace FireflyRedPacketAPI { diff --git a/packages/web3-providers/src/types/Social.ts b/packages/web3-providers/src/types/Social.ts index 43168d5c8042..ae60992f31ad 100644 --- a/packages/web3-providers/src/types/Social.ts +++ b/packages/web3-providers/src/types/Social.ts @@ -15,12 +15,14 @@ export namespace Social { export enum Source { Farcaster = 'Farcaster', Lens = 'Lens', + Firefly = 'Firefly', } export enum SourceInURL { Farcaster = 'farcaster', Lens = 'lens', + Firefly = '', } - export type SocialSource = Source.Farcaster | Source.Lens + export type SocialSource = Source.Farcaster | Source.Lens | Source.Firefly /** Normalized Channel, different from Farcaster's */ export interface Channel { From f0a39233618eaccd9e101f6b556475c68715f33e Mon Sep 17 00:00:00 2001 From: swkatmask Date: Wed, 3 Sep 2025 11:30:26 +0000 Subject: [PATCH 4/7] fix: linter --- cspell.json | 1 - packages/mask/shared-ui/locale/en-US.json | 4 ++++ packages/mask/shared-ui/locale/en-US.po | 17 +++++++++++++++++ packages/mask/shared-ui/locale/ja-JP.json | 4 ++++ packages/mask/shared-ui/locale/ja-JP.po | 17 +++++++++++++++++ packages/mask/shared-ui/locale/ko-KR.json | 4 ++++ packages/mask/shared-ui/locale/ko-KR.po | 17 +++++++++++++++++ packages/mask/shared-ui/locale/zh-CN.json | 4 ++++ packages/mask/shared-ui/locale/zh-CN.po | 17 +++++++++++++++++ packages/mask/shared-ui/locale/zh-TW.json | 4 ++++ packages/mask/shared-ui/locale/zh-TW.po | 17 +++++++++++++++++ 11 files changed, 105 insertions(+), 1 deletion(-) diff --git a/cspell.json b/cspell.json index d7f627ae9b19..75b4729ba36e 100644 --- a/cspell.json +++ b/cspell.json @@ -269,4 +269,3 @@ "youtube" ] } - diff --git a/packages/mask/shared-ui/locale/en-US.json b/packages/mask/shared-ui/locale/en-US.json index 0da143543324..fcd4c6fc9d8b 100644 --- a/packages/mask/shared-ui/locale/en-US.json +++ b/packages/mask/shared-ui/locale/en-US.json @@ -359,6 +359,7 @@ "LWJCIn": ["Connected sites"], "LcET2C": ["Privacy Policy"], "LcZh+r": ["Unsupported data backup"], + "Lh3tjm": ["Connect Firefly"], "LkYKvc": ["Please select the correct words in the correct order."], "LqWHk1": ["Hide ", ["0"]], "Lt0Hf0": [ @@ -429,6 +430,7 @@ "QXDgyQ": [ "Please write down the following words in correct order. Keep it safe and do not share with anyone!" ], + "QbIq7s": ["This QR code is longer valid. Please scan a new one to continue."], "Qbo7Ev": ["Write down mnemonic words"], "QcVZFH": ["Step ", ["step"], "/", ["totalSteps"]], "QrHM/A": ["Backup downloaded and merged to local successfully."], @@ -708,6 +710,7 @@ "nUWEsV": ["Max fee is higher than necessary"], "nUeoRs": ["Signing Message (Text)"], "nVT2pJ": ["realMaskNetwork"], + "nWd3Q/": ["The account you are trying to log in with is already linked to a different Firefly account."], "ndRqFD": ["Chain ID"], "ngWO+e": ["No local key"], "npsHM5": ["Back Up to Google Drive"], @@ -732,6 +735,7 @@ "ou+PEI": ["Profile Photo"], "p2/GCq": ["Confirm Password"], "p2xE4C": ["Overwrite current backup"], + "p70+lb": ["Your ", ["0"], " account is now connected."], "p8Aea2": ["The persona name already exists."], "pGElS5": ["Mnemonic"], "pHqZUU": ["You used <0>", ["0"], " for the last cloud backup."], diff --git a/packages/mask/shared-ui/locale/en-US.po b/packages/mask/shared-ui/locale/en-US.po index f5f821c3ca03..798908244ad9 100644 --- a/packages/mask/shared-ui/locale/en-US.po +++ b/packages/mask/shared-ui/locale/en-US.po @@ -666,6 +666,10 @@ msgstr "" msgid "Connect and switch between your wallets." msgstr "" +#: popups/pages/Personas/ConnectFirefly/index.tsx +msgid "Connect Firefly" +msgstr "" + #: popups/modals/SelectProviderModal/index.tsx msgid "Connect Mask Network Account using your wallet." msgstr "" @@ -2786,6 +2790,10 @@ msgstr "" msgid "Text copied!" msgstr "" +#: popups/pages/Personas/ConnectFirefly/index.tsx +msgid "The account you are trying to log in with is already linked to a different Firefly account." +msgstr "" + #: popups/components/UnlockERC20Token/index.tsx msgid "The approval for this contract will be revoked in case of the amount is 0." msgstr "" @@ -2946,6 +2954,10 @@ msgstr "" msgid "This network name already exists" msgstr "" +#: popups/pages/Personas/ConnectFirefly/index.tsx +msgid "This QR code is longer valid. Please scan a new one to continue." +msgstr "" + #: dashboard/pages/SetupPersona/Mnemonic/ComponentToPrint.tsx msgid "This QR includes your identity, please keep it safely." msgstr "" @@ -3306,6 +3318,11 @@ msgstr "" #~ msgid "You used <0>{0} for the last cloud backup." #~ msgstr "" +#. placeholder {0}: Social.Source.Farcaster +#: popups/pages/Personas/ConnectFirefly/index.tsx +msgid "Your {0} account is now connected." +msgstr "" + #: popups/components/SignRequestInfo/index.tsx msgid "Your connection to this site is not encrypted which can be modified by a hostile third party, we strongly suggest you reject this request." msgstr "" diff --git a/packages/mask/shared-ui/locale/ja-JP.json b/packages/mask/shared-ui/locale/ja-JP.json index 48a0b7ec6b9d..5b01513ba643 100644 --- a/packages/mask/shared-ui/locale/ja-JP.json +++ b/packages/mask/shared-ui/locale/ja-JP.json @@ -361,6 +361,7 @@ "LWJCIn": ["接続されたサイト"], "LcET2C": ["プライバシー・ポリシー(個人情報に関する方針)"], "LcZh+r": ["サポートされていないバックアップ"], + "Lh3tjm": ["Connect Firefly"], "LkYKvc": ["Please select the correct words in the correct order."], "LqWHk1": ["Hide ", ["0"]], "Lt0Hf0": [ @@ -429,6 +430,7 @@ "QU9aqK": ["You have signed with your wallet."], "QWxok/": ["マスクウォレットと接続"], "QXDgyQ": ["以下の単語を正しい順序で書き留めてください。安全に保管し、誰とも共有しないでください!"], + "QbIq7s": ["This QR code is longer valid. Please scan a new one to continue."], "Qbo7Ev": ["ニーモニックワードを書き留めてください"], "QcVZFH": ["Step ", ["step"], "/", ["totalSteps"]], "QrHM/A": ["Backup downloaded and merged to local successfully."], @@ -708,6 +710,7 @@ "nUWEsV": ["最大手数料が必要以上です"], "nUeoRs": ["署名メッセージ"], "nVT2pJ": ["realMaskNetwork"], + "nWd3Q/": ["The account you are trying to log in with is already linked to a different Firefly account."], "ndRqFD": ["チェーン ID"], "ngWO+e": ["ローカル・キーはありません"], "npsHM5": ["Back Up to Google Drive"], @@ -732,6 +735,7 @@ "ou+PEI": ["プロフィール写真"], "p2/GCq": ["パスワードの確認"], "p2xE4C": ["Overwrite current backup"], + "p70+lb": ["Your ", ["0"], " account is now connected."], "p8Aea2": ["このペルソナ名は既に存在しています."], "pGElS5": ["ニーモニック"], "pHqZUU": ["You used <0>", ["0"], " for the last cloud backup."], diff --git a/packages/mask/shared-ui/locale/ja-JP.po b/packages/mask/shared-ui/locale/ja-JP.po index cf986132ed2e..c05d7fc3ee75 100644 --- a/packages/mask/shared-ui/locale/ja-JP.po +++ b/packages/mask/shared-ui/locale/ja-JP.po @@ -671,6 +671,10 @@ msgstr "接続" msgid "Connect and switch between your wallets." msgstr "ウォレットを接続して切り替えます。" +#: popups/pages/Personas/ConnectFirefly/index.tsx +msgid "Connect Firefly" +msgstr "" + #: popups/modals/SelectProviderModal/index.tsx msgid "Connect Mask Network Account using your wallet." msgstr "ウォレットを使用してマスクネットワークアカウントを接続します。" @@ -2791,6 +2795,10 @@ msgstr "テキスト" msgid "Text copied!" msgstr "" +#: popups/pages/Personas/ConnectFirefly/index.tsx +msgid "The account you are trying to log in with is already linked to a different Firefly account." +msgstr "" + #: popups/components/UnlockERC20Token/index.tsx msgid "The approval for this contract will be revoked in case of the amount is 0." msgstr "" @@ -2951,6 +2959,10 @@ msgstr "このメッセージは無効なEIP-4361メッセージを含んでい msgid "This network name already exists" msgstr "このネットワーク名は既に存在します" +#: popups/pages/Personas/ConnectFirefly/index.tsx +msgid "This QR code is longer valid. Please scan a new one to continue." +msgstr "" + #: dashboard/pages/SetupPersona/Mnemonic/ComponentToPrint.tsx msgid "This QR includes your identity, please keep it safely." msgstr "" @@ -3311,6 +3323,11 @@ msgstr "" #~ msgid "You used <0>{0} for the last cloud backup." #~ msgstr "" +#. placeholder {0}: Social.Source.Farcaster +#: popups/pages/Personas/ConnectFirefly/index.tsx +msgid "Your {0} account is now connected." +msgstr "" + #: popups/components/SignRequestInfo/index.tsx msgid "Your connection to this site is not encrypted which can be modified by a hostile third party, we strongly suggest you reject this request." msgstr "このサイトへの接続は暗号化されていないため、敵対的な第三者によって変更されます。このリクエストを拒否することを強くお勧めします。" diff --git a/packages/mask/shared-ui/locale/ko-KR.json b/packages/mask/shared-ui/locale/ko-KR.json index b3bd9ddbf862..2f150ec420c9 100644 --- a/packages/mask/shared-ui/locale/ko-KR.json +++ b/packages/mask/shared-ui/locale/ko-KR.json @@ -357,6 +357,7 @@ "LWJCIn": ["연결된 사이트"], "LcET2C": ["개인정보처리방침"], "LcZh+r": ["지원하지 않는 데이터 백업"], + "Lh3tjm": ["Connect Firefly"], "LkYKvc": ["Please select the correct words in the correct order."], "LqWHk1": ["Hide ", ["0"]], "Lt0Hf0": [ @@ -425,6 +426,7 @@ "QU9aqK": ["You have signed with your wallet."], "QWxok/": ["Connect with Mask Wallet"], "QXDgyQ": ["다음 단어를 정확한 순서로 적어주세요. 안전하게 보관하고 다른 사람과 공유하지 마세요!"], + "QbIq7s": ["This QR code is longer valid. Please scan a new one to continue."], "Qbo7Ev": ["니모닉 단어를 적어두세요"], "QcVZFH": ["Step ", ["step"], "/", ["totalSteps"]], "QrHM/A": ["Backup downloaded and merged to local successfully."], @@ -704,6 +706,7 @@ "nUWEsV": ["최대 가스비는 필요한 것보다 높습니다."], "nUeoRs": ["사인 메시지"], "nVT2pJ": ["realMaskNetwork"], + "nWd3Q/": ["The account you are trying to log in with is already linked to a different Firefly account."], "ndRqFD": ["체인 ID"], "ngWO+e": ["로컬 키 없음"], "npsHM5": ["Back Up to Google Drive"], @@ -728,6 +731,7 @@ "ou+PEI": ["프로필 사진"], "p2/GCq": ["비밀번호 확인"], "p2xE4C": ["Overwrite current backup"], + "p70+lb": ["Your ", ["0"], " account is now connected."], "p8Aea2": ["이미 존재된 페르소나입니다"], "pGElS5": ["니모닉"], "pHqZUU": ["You used <0>", ["0"], " for the last cloud backup."], diff --git a/packages/mask/shared-ui/locale/ko-KR.po b/packages/mask/shared-ui/locale/ko-KR.po index 2829c2580a9e..0bff0ddcdbcc 100644 --- a/packages/mask/shared-ui/locale/ko-KR.po +++ b/packages/mask/shared-ui/locale/ko-KR.po @@ -671,6 +671,10 @@ msgstr "연결" msgid "Connect and switch between your wallets." msgstr "여기서 월렛을 연결하세요. 여기서 네트워크나 월렛을 바꿀 수 있습니다." +#: popups/pages/Personas/ConnectFirefly/index.tsx +msgid "Connect Firefly" +msgstr "" + #: popups/modals/SelectProviderModal/index.tsx msgid "Connect Mask Network Account using your wallet." msgstr "Mask Network 계정을 연결하여 월렛을 사용합니다." @@ -2791,6 +2795,10 @@ msgstr "텍스트" msgid "Text copied!" msgstr "" +#: popups/pages/Personas/ConnectFirefly/index.tsx +msgid "The account you are trying to log in with is already linked to a different Firefly account." +msgstr "" + #: popups/components/UnlockERC20Token/index.tsx msgid "The approval for this contract will be revoked in case of the amount is 0." msgstr "" @@ -2951,6 +2959,10 @@ msgstr "" msgid "This network name already exists" msgstr "이미 존재된 네트워크입니다" +#: popups/pages/Personas/ConnectFirefly/index.tsx +msgid "This QR code is longer valid. Please scan a new one to continue." +msgstr "" + #: dashboard/pages/SetupPersona/Mnemonic/ComponentToPrint.tsx msgid "This QR includes your identity, please keep it safely." msgstr "" @@ -3311,6 +3323,11 @@ msgstr "" #~ msgid "You used <0>{0} for the last cloud backup." #~ msgstr "" +#. placeholder {0}: Social.Source.Farcaster +#: popups/pages/Personas/ConnectFirefly/index.tsx +msgid "Your {0} account is now connected." +msgstr "" + #: popups/components/SignRequestInfo/index.tsx msgid "Your connection to this site is not encrypted which can be modified by a hostile third party, we strongly suggest you reject this request." msgstr "" diff --git a/packages/mask/shared-ui/locale/zh-CN.json b/packages/mask/shared-ui/locale/zh-CN.json index 16667cbea4fc..f03bca97729a 100644 --- a/packages/mask/shared-ui/locale/zh-CN.json +++ b/packages/mask/shared-ui/locale/zh-CN.json @@ -353,6 +353,7 @@ "LWJCIn": ["已连接的网站"], "LcET2C": ["隐私政策"], "LcZh+r": ["不支持的数据备份格式"], + "Lh3tjm": ["Connect Firefly"], "LkYKvc": ["Please select the correct words in the correct order."], "LqWHk1": ["隐藏 ", ["0"]], "Lt0Hf0": [ @@ -421,6 +422,7 @@ "QU9aqK": ["You have signed with your wallet."], "QWxok/": ["Connect with Mask Wallet"], "QXDgyQ": ["请按正确顺序写下以下单词。保证安全,不与任何人分享!"], + "QbIq7s": ["This QR code is longer valid. Please scan a new one to continue."], "Qbo7Ev": ["写下助记词"], "QcVZFH": ["Step ", ["step"], "/", ["totalSteps"]], "QrHM/A": ["Backup downloaded and merged to local successfully."], @@ -698,6 +700,7 @@ "nUWEsV": ["Max fee 高于必要值"], "nUeoRs": ["消息签名"], "nVT2pJ": ["realMaskNetwork"], + "nWd3Q/": ["The account you are trying to log in with is already linked to a different Firefly account."], "ndRqFD": ["Chain ID"], "ngWO+e": ["缺失本地密钥"], "npsHM5": ["Back Up to Google Drive"], @@ -722,6 +725,7 @@ "ou+PEI": ["个人头像"], "p2/GCq": ["确认密码"], "p2xE4C": ["Overwrite current backup"], + "p70+lb": ["Your ", ["0"], " account is now connected."], "p8Aea2": ["此身份名称已存在"], "pGElS5": ["助记词"], "pHqZUU": ["You used <0>", ["0"], " for the last cloud backup."], diff --git a/packages/mask/shared-ui/locale/zh-CN.po b/packages/mask/shared-ui/locale/zh-CN.po index 4e5c323fc355..99e5d56f9026 100644 --- a/packages/mask/shared-ui/locale/zh-CN.po +++ b/packages/mask/shared-ui/locale/zh-CN.po @@ -671,6 +671,10 @@ msgstr "连接" msgid "Connect and switch between your wallets." msgstr "点击这里连接您的钱包。您可以在此选择网络或更改您的钱包。" +#: popups/pages/Personas/ConnectFirefly/index.tsx +msgid "Connect Firefly" +msgstr "" + #: popups/modals/SelectProviderModal/index.tsx msgid "Connect Mask Network Account using your wallet." msgstr "使用您的钱包连接Mask Network账户。" @@ -2791,6 +2795,10 @@ msgstr "文本" msgid "Text copied!" msgstr "" +#: popups/pages/Personas/ConnectFirefly/index.tsx +msgid "The account you are trying to log in with is already linked to a different Firefly account." +msgstr "" + #: popups/components/UnlockERC20Token/index.tsx msgid "The approval for this contract will be revoked in case of the amount is 0." msgstr "" @@ -2951,6 +2959,10 @@ msgstr "" msgid "This network name already exists" msgstr "此网络名称已存在" +#: popups/pages/Personas/ConnectFirefly/index.tsx +msgid "This QR code is longer valid. Please scan a new one to continue." +msgstr "" + #: dashboard/pages/SetupPersona/Mnemonic/ComponentToPrint.tsx msgid "This QR includes your identity, please keep it safely." msgstr "" @@ -3311,6 +3323,11 @@ msgstr "" #~ msgid "You used <0>{0} for the last cloud backup." #~ msgstr "" +#. placeholder {0}: Social.Source.Farcaster +#: popups/pages/Personas/ConnectFirefly/index.tsx +msgid "Your {0} account is now connected." +msgstr "" + #: popups/components/SignRequestInfo/index.tsx msgid "Your connection to this site is not encrypted which can be modified by a hostile third party, we strongly suggest you reject this request." msgstr "" diff --git a/packages/mask/shared-ui/locale/zh-TW.json b/packages/mask/shared-ui/locale/zh-TW.json index 26c07dea7b35..0778d124fcfe 100644 --- a/packages/mask/shared-ui/locale/zh-TW.json +++ b/packages/mask/shared-ui/locale/zh-TW.json @@ -353,6 +353,7 @@ "LWJCIn": ["已连接的网站"], "LcET2C": ["隐私政策"], "LcZh+r": ["不支持數據備份格式"], + "Lh3tjm": ["Connect Firefly"], "LkYKvc": ["Please select the correct words in the correct order."], "LqWHk1": ["隐藏 ", ["0"]], "Lt0Hf0": [ @@ -421,6 +422,7 @@ "QU9aqK": ["You have signed with your wallet."], "QWxok/": ["Connect with Mask Wallet"], "QXDgyQ": ["请按正确顺序写下以下单词。保证安全,不与任何人分享!"], + "QbIq7s": ["This QR code is longer valid. Please scan a new one to continue."], "Qbo7Ev": ["写下助记词"], "QcVZFH": ["Step ", ["step"], "/", ["totalSteps"]], "QrHM/A": ["Backup downloaded and merged to local successfully."], @@ -698,6 +700,7 @@ "nUWEsV": ["Max fee 高于必要值"], "nUeoRs": ["消息签名"], "nVT2pJ": ["realMaskNetwork"], + "nWd3Q/": ["The account you are trying to log in with is already linked to a different Firefly account."], "ndRqFD": ["Chain ID"], "ngWO+e": ["缺失本地密钥"], "npsHM5": ["Back Up to Google Drive"], @@ -722,6 +725,7 @@ "ou+PEI": ["个人头像"], "p2/GCq": ["确认密码"], "p2xE4C": ["Overwrite current backup"], + "p70+lb": ["Your ", ["0"], " account is now connected."], "p8Aea2": ["此身份名称已存在"], "pGElS5": ["助记词"], "pHqZUU": ["You used <0>", ["0"], " for the last cloud backup."], diff --git a/packages/mask/shared-ui/locale/zh-TW.po b/packages/mask/shared-ui/locale/zh-TW.po index fc4524e27b09..a55cc7d63594 100644 --- a/packages/mask/shared-ui/locale/zh-TW.po +++ b/packages/mask/shared-ui/locale/zh-TW.po @@ -671,6 +671,10 @@ msgstr "" msgid "Connect and switch between your wallets." msgstr "" +#: popups/pages/Personas/ConnectFirefly/index.tsx +msgid "Connect Firefly" +msgstr "" + #: popups/modals/SelectProviderModal/index.tsx msgid "Connect Mask Network Account using your wallet." msgstr "" @@ -2791,6 +2795,10 @@ msgstr "" msgid "Text copied!" msgstr "" +#: popups/pages/Personas/ConnectFirefly/index.tsx +msgid "The account you are trying to log in with is already linked to a different Firefly account." +msgstr "" + #: popups/components/UnlockERC20Token/index.tsx msgid "The approval for this contract will be revoked in case of the amount is 0." msgstr "" @@ -2951,6 +2959,10 @@ msgstr "" msgid "This network name already exists" msgstr "" +#: popups/pages/Personas/ConnectFirefly/index.tsx +msgid "This QR code is longer valid. Please scan a new one to continue." +msgstr "" + #: dashboard/pages/SetupPersona/Mnemonic/ComponentToPrint.tsx msgid "This QR includes your identity, please keep it safely." msgstr "" @@ -3311,6 +3323,11 @@ msgstr "" #~ msgid "You used <0>{0} for the last cloud backup." #~ msgstr "" +#. placeholder {0}: Social.Source.Farcaster +#: popups/pages/Personas/ConnectFirefly/index.tsx +msgid "Your {0} account is now connected." +msgstr "" + #: popups/components/SignRequestInfo/index.tsx msgid "Your connection to this site is not encrypted which can be modified by a hostile third party, we strongly suggest you reject this request." msgstr "" From e6855a931b10f0dbfec013671ef0f8d736ab3038 Mon Sep 17 00:00:00 2001 From: Jack Works <5390719+Jack-Works@users.noreply.github.com> Date: Wed, 3 Sep 2025 20:06:17 +0800 Subject: [PATCH 5/7] refactor: store token in profile record --- .../background/database/persona/helper.ts | 19 ++++++++++--------- .../mask/background/database/persona/type.ts | 1 + .../mask/background/database/persona/web.ts | 7 +++++++ .../services/identity/profile/update.ts | 8 +++++--- .../pages/Personas/ConnectFirefly/index.tsx | 9 +++++---- packages/public-api/src/types/index.ts | 1 - 6 files changed, 28 insertions(+), 17 deletions(-) diff --git a/packages/mask/background/database/persona/helper.ts b/packages/mask/background/database/persona/helper.ts index f048c8acbf48..ea890ff06283 100644 --- a/packages/mask/background/database/persona/helper.ts +++ b/packages/mask/background/database/persona/helper.ts @@ -180,31 +180,32 @@ export async function createPersonaByJsonWebKey(options: { export async function createProfileWithPersona( profileID: ProfileIdentifier, - data: LinkedProfileDetails, - keys: { + linkMeta: LinkedProfileDetails, + persona: { nickname?: string publicKey: EC_Public_JsonWebKey privateKey?: EC_Private_JsonWebKey localKey?: AESJsonWebKey mnemonic?: PersonaRecord['mnemonic'] }, + token?: string | null, ): Promise { - const ec_id = (await ECKeyIdentifier.fromJsonWebKey(keys.publicKey)).unwrap() + const ec_id = (await ECKeyIdentifier.fromJsonWebKey(persona.publicKey)).unwrap() const rec: PersonaRecord = { createdAt: new Date(), updatedAt: new Date(), identifier: ec_id, linkedProfiles: new Map(), - nickname: keys.nickname, - publicKey: keys.publicKey, - privateKey: keys.privateKey, - localKey: keys.localKey, - mnemonic: keys.mnemonic, + nickname: persona.nickname, + publicKey: persona.publicKey, + privateKey: persona.privateKey, + localKey: persona.localKey, + mnemonic: persona.mnemonic, hasLogout: false, } await consistentPersonaDBWriteAccess(async (t) => { await createOrUpdatePersonaDB(rec, { explicitUndefinedField: 'ignore', linkedProfiles: 'merge' }, t) - await attachProfileDB(profileID, ec_id, data, t) + await attachProfileDB(profileID, ec_id, linkMeta, { token }, t) }) } // #endregion diff --git a/packages/mask/background/database/persona/type.ts b/packages/mask/background/database/persona/type.ts index 0ff1cb99b50f..86bbe0fa538c 100644 --- a/packages/mask/background/database/persona/type.ts +++ b/packages/mask/background/database/persona/type.ts @@ -112,6 +112,7 @@ export interface ProfileRecord { nickname?: string localKey?: AESJsonWebKey linkedPersona?: PersonaIdentifier + token?: string createdAt: Date updatedAt: Date } diff --git a/packages/mask/background/database/persona/web.ts b/packages/mask/background/database/persona/web.ts index 58e32b7c2a58..af6c45981254 100644 --- a/packages/mask/background/database/persona/web.ts +++ b/packages/mask/background/database/persona/web.ts @@ -522,6 +522,7 @@ export async function attachProfileDB( identifier: ProfileIdentifier, attachTo: PersonaIdentifier, data: LinkedProfileDetails, + profileExtra?: { token?: string | null }, t?: FullPersonaDBTransaction<'readwrite'>, ): Promise { t = t || createTransaction(await db(), 'readwrite')('personas', 'profiles', 'relations') @@ -536,6 +537,12 @@ export async function attachProfileDB( await detachProfileDB(identifier, t) } + if (profileExtra?.token) { + profile.token = profileExtra.token + } else if (profileExtra && 'token' in profileExtra && !profileExtra.token) { + delete profile.token + } + profile.linkedPersona = attachTo persona.linkedProfiles.set(identifier, data) diff --git a/packages/mask/background/services/identity/profile/update.ts b/packages/mask/background/services/identity/profile/update.ts index c6c1a2416e80..0697a3c3e229 100644 --- a/packages/mask/background/services/identity/profile/update.ts +++ b/packages/mask/background/services/identity/profile/update.ts @@ -86,14 +86,15 @@ export async function resolveUnknownLegacyIdentity(identifier: ProfileIdentifier export async function attachProfile( source: ProfileIdentifier, target: ProfileIdentifier | PersonaIdentifier, - data: LinkedProfileDetails, + linkMeta: LinkedProfileDetails, + profileExtra?: { token?: string | null }, ): Promise { if (target instanceof ProfileIdentifier) { const profile = await queryProfileDB(target) if (!profile?.linkedPersona) throw new Error('target not found') target = profile.linkedPersona } - return attachProfileDB(source, target, data) + return attachProfileDB(source, target, linkMeta, profileExtra) } export function detachProfile(identifier: ProfileIdentifier): Promise { return detachProfileDB(identifier) @@ -101,7 +102,7 @@ export function detachProfile(identifier: ProfileIdentifier): Promise { /** * Set NextID profile to profileDB - * */ + */ export async function attachNextIDPersonaToProfile(item: ProfileInformationFromNextID, whoAmI: ECKeyIdentifier) { if (!item.linkedPersona) throw new Error('LinkedPersona Not Found') @@ -137,6 +138,7 @@ export async function attachNextIDPersonaToProfile(item: ProfileInformationFromN profileRecord.identifier, item.linkedPersona!, { connectionConfirmState: 'confirmed' }, + undefined, t, ) await createOrUpdateRelationDB( diff --git a/packages/mask/popups/pages/Personas/ConnectFirefly/index.tsx b/packages/mask/popups/pages/Personas/ConnectFirefly/index.tsx index 946c7abdd300..8f1c1cad483c 100644 --- a/packages/mask/popups/pages/Personas/ConnectFirefly/index.tsx +++ b/packages/mask/popups/pages/Personas/ConnectFirefly/index.tsx @@ -1,5 +1,4 @@ import { Trans, useLingui } from '@lingui/react/macro' -import { attachProfile } from '@masknet/plugin-infra/dom/context' import { PersonaContext, PopupHomeTabType } from '@masknet/shared' import { AbortError, @@ -22,6 +21,7 @@ import { useMount } from 'react-use' import urlcat from 'urlcat' import { useTitle } from '../../../hooks/index.js' import { createAccountByRelayService } from './createAccountByRelayService.js' +import Services from '#services' const useStyles = makeStyles()({ container: { @@ -98,11 +98,12 @@ export const Component = memo(function ConnectFireflyPage() { setUrl(url) }) console.log('account', account) - if (attachProfile && currentPersona) { - await attachProfile( + if (currentPersona) { + await Services.Identity.attachProfile( ProfileIdentifier.of(EnhanceableSite.Farcaster, account.session.profileId).unwrap(), currentPersona.identifier, - { connectionConfirmState: 'pending', token: account.session.token }, + { connectionConfirmState: 'pending' }, + { token: account.session.token }, ) } console.log('account', account) diff --git a/packages/public-api/src/types/index.ts b/packages/public-api/src/types/index.ts index e76ea705589b..f6e8f566fd76 100644 --- a/packages/public-api/src/types/index.ts +++ b/packages/public-api/src/types/index.ts @@ -14,7 +14,6 @@ export interface JsonRpcResponse { export interface LinkedProfileDetails { connectionConfirmState: 'confirmed' | 'pending' - token?: string } // These type MUST be sync with packages/shared-base/src/crypto/JWKType From 12d73223e4da49c987a25e3bde0b013c546784d3 Mon Sep 17 00:00:00 2001 From: Wukong Sun Date: Thu, 4 Sep 2025 13:34:39 +0800 Subject: [PATCH 6/7] feat: add support for lens social network integration --- .node-version | 2 +- packages/mask/package.json | 1 + packages/mask/popups/Popup.tsx | 47 ++-- .../components/SocialAccounts/index.tsx | 2 +- packages/mask/popups/constants.ts | 1 + .../popups/hooks/useSupportSocialNetworks.ts | 2 +- .../ConnectSocialAccountModal/index.tsx | 5 +- .../pages/Personas/AccountDetail/UI.tsx | 4 +- .../ConnectFirefly/bindFireflySession.ts | 28 ++- .../pages/Personas/ConnectFirefly/index.tsx | 32 ++- .../Personas/ConnectLens/createLensSession.ts | 27 +++ .../pages/Personas/ConnectLens/index.tsx | 204 ++++++++++++++++++ .../components/AccountAvatar/index.tsx | 2 +- packages/mask/popups/pages/Personas/index.tsx | 20 +- .../Wallet/Interaction/InteractionContext.ts | 2 +- .../popups/pages/Wallet/WalletGuard/index.tsx | 26 ++- .../Wallet/components/WalletHeader/UI.tsx | 6 +- .../Wallet/components/WalletHeader/index.tsx | 7 +- packages/mask/shared-ui/locale/en-US.po | 1 + packages/mask/shared-ui/locale/ja-JP.po | 1 + packages/mask/shared-ui/locale/ko-KR.po | 1 + packages/mask/shared-ui/locale/zh-CN.po | 1 + packages/mask/shared-ui/locale/zh-TW.po | 1 + packages/mask/shared/definitions/event.ts | 1 + .../mask/shared/site-adaptors/definitions.ts | 26 +-- .../site-adaptors/implementations/hey.xyz.ts | 13 ++ packages/mask/shared/site-adaptors/types.d.ts | 4 +- .../plugins/RedPacket/src/locale/en-US.po | 2 +- .../plugins/RedPacket/src/locale/ja-JP.po | 2 +- .../plugins/RedPacket/src/locale/ko-KR.po | 2 +- .../plugins/RedPacket/src/locale/zh-CN.po | 2 +- .../plugins/RedPacket/src/locale/zh-TW.po | 2 +- .../src/LegacySettings/settings.ts | 1 + packages/shared-base/src/Site/index.ts | 1 + packages/shared-base/src/Site/types.ts | 1 + packages/shared-base/src/constants.ts | 1 + packages/shared-base/src/errors.ts | 8 + packages/shared-base/src/types/Routes.ts | 1 + packages/shared/src/constants.tsx | 5 +- packages/web3-hooks/base/src/useContext.tsx | 6 +- .../src/AvatarStore/helpers/getAvatar.ts | 1 + .../web3-providers/src/Firefly/LensSession.ts | 29 +++ packages/web3-providers/src/Firefly/index.ts | 1 + .../src/FireflyAccount/index.ts | 1 + pnpm-lock.yaml | 3 + 45 files changed, 437 insertions(+), 99 deletions(-) create mode 100644 packages/mask/popups/pages/Personas/ConnectLens/createLensSession.ts create mode 100644 packages/mask/popups/pages/Personas/ConnectLens/index.tsx create mode 100644 packages/mask/shared/site-adaptors/implementations/hey.xyz.ts create mode 100644 packages/web3-providers/src/Firefly/LensSession.ts diff --git a/.node-version b/.node-version index 4c8f24d74e13..13bbaed213cc 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -v24.7.0 +v24.5.0 diff --git a/packages/mask/package.json b/packages/mask/package.json index f0d5c50bad44..2554b15da68e 100644 --- a/packages/mask/package.json +++ b/packages/mask/package.json @@ -36,6 +36,7 @@ "@dimensiondev/mask-wallet-core": "0.1.0-20211013082857-eb62e5f", "@ethereumjs/util": "^9.0.3", "@hookform/resolvers": "^3.6.0", + "@lens-protocol/client": "0.0.0-canary-20250408064617", "@masknet/backup-format": "workspace:^", "@masknet/encryption": "workspace:^", "@masknet/flags": "workspace:^", diff --git a/packages/mask/popups/Popup.tsx b/packages/mask/popups/Popup.tsx index 62c4f4fb95cb..f274bc3047a2 100644 --- a/packages/mask/popups/Popup.tsx +++ b/packages/mask/popups/Popup.tsx @@ -1,10 +1,10 @@ import { PageUIProvider, PersonaContext } from '@masknet/shared' -import { jsxCompose, MaskMessages, PopupRoutes } from '@masknet/shared-base' +import { MaskMessages, PopupRoutes } from '@masknet/shared-base' import { PopupSnackbarProvider } from '@masknet/theme' import { EVMWeb3ContextProvider } from '@masknet/web3-hooks-base' import { ProviderType } from '@masknet/web3-shared-evm' import { Box } from '@mui/material' -import { Suspense, cloneElement, lazy, memo, useEffect, useMemo, useState, type ReactNode } from 'react' +import { Suspense, lazy, memo, useEffect, useMemo, useState, type ReactNode } from 'react' import { useIdleTimer } from 'react-idle-timer' import { createHashRouter, @@ -30,6 +30,7 @@ import { WalletFrame, walletRoutes } from './pages/Wallet/index.js' import { ContactsFrame, contactsRoutes } from './pages/Friends/index.js' import { ErrorBoundaryUIOfError } from '../../shared-base-ui/src/components/ErrorBoundary/ErrorBoundary.js' import { TraderFrame, traderRoutes } from './pages/Trader/index.js' +import { InteractionWalletContext } from './pages/Wallet/Interaction/InteractionContext.js' const personaInitialState = { queryOwnedPersonaInformation: Services.Identity.queryOwnedPersonaInformation, @@ -108,23 +109,31 @@ export default function Popups() { throttle: 10000, }) - return jsxCompose( - , - // eslint-disable-next-line react-compiler/react-compiler - , - , - , - , - , - )( - cloneElement, - <> - {/* https://github.com/TanStack/query/issues/5417 */} - {process.env.NODE_ENV === 'development' ? - - : null} - - , + return ( + + {/* eslint-disable-next-line react-compiler/react-compiler */} + + + + + + + {/* https://github.com/TanStack/query/issues/5417 */} + {process.env.NODE_ENV === 'development' ? + + : null} + + + + + + + + ) } diff --git a/packages/mask/popups/components/SocialAccounts/index.tsx b/packages/mask/popups/components/SocialAccounts/index.tsx index 6cd9b81a1698..75ac963bd17a 100644 --- a/packages/mask/popups/components/SocialAccounts/index.tsx +++ b/packages/mask/popups/components/SocialAccounts/index.tsx @@ -91,7 +91,7 @@ export const SocialAccounts = memo(function SocialAccounts( onAccountClick(account)}> diff --git a/packages/mask/popups/constants.ts b/packages/mask/popups/constants.ts index f93c614b5c92..53e301ff97dc 100644 --- a/packages/mask/popups/constants.ts +++ b/packages/mask/popups/constants.ts @@ -13,4 +13,5 @@ export const SOCIAL_MEDIA_ICON_FILTER_COLOR: Record = { [EnhanceableSite.Localhost]: '', [EnhanceableSite.Firefly]: '', [EnhanceableSite.Farcaster]: '', + [EnhanceableSite.Lens]: '', } diff --git a/packages/mask/popups/hooks/useSupportSocialNetworks.ts b/packages/mask/popups/hooks/useSupportSocialNetworks.ts index 7f5ff5522523..be0cc3eb54c2 100644 --- a/packages/mask/popups/hooks/useSupportSocialNetworks.ts +++ b/packages/mask/popups/hooks/useSupportSocialNetworks.ts @@ -4,7 +4,7 @@ import { useQuery } from '@tanstack/react-query' export function useSupportSocialNetworks() { return useQuery({ - queryKey: ['@@Service.SiteAdaptor.getSupportedSites({ isSocialNetwork: true })'], + queryKey: ['Service.SiteAdaptor.getSupportedSites({ isSocialNetwork: true })'], queryFn: async () => { const sites = await Service.SiteAdaptor.getSupportedSites({ isSocialNetwork: true }) return sites.map((x) => x.networkIdentifier as EnhanceableSite) diff --git a/packages/mask/popups/modals/ConnectSocialAccountModal/index.tsx b/packages/mask/popups/modals/ConnectSocialAccountModal/index.tsx index 2015bd7e351c..d8dc024aba07 100644 --- a/packages/mask/popups/modals/ConnectSocialAccountModal/index.tsx +++ b/packages/mask/popups/modals/ConnectSocialAccountModal/index.tsx @@ -21,8 +21,9 @@ export const ConnectSocialAccountModal = memo(function Con const handleConnect = useCallback( async (networkIdentifier: EnhanceableSite) => { if (networkIdentifier === EnhanceableSite.Farcaster) { - navigate(PopupRoutes.ConnectFirefly) - return + return navigate(PopupRoutes.ConnectFirefly) + } else if (networkIdentifier === EnhanceableSite.Lens) { + return navigate(PopupRoutes.ConnectLens) } if (!currentPersona) return if (!(await requestPermissionFromExtensionPage(networkIdentifier))) return diff --git a/packages/mask/popups/pages/Personas/AccountDetail/UI.tsx b/packages/mask/popups/pages/Personas/AccountDetail/UI.tsx index 4b4f0ca7c9cd..ff1c3df324da 100644 --- a/packages/mask/popups/pages/Personas/AccountDetail/UI.tsx +++ b/packages/mask/popups/pages/Personas/AccountDetail/UI.tsx @@ -1,5 +1,5 @@ import { Trans } from '@lingui/react/macro' -import type { BindingProof, ProfileAccount } from '@masknet/shared-base' +import type { BindingProof, EnhanceableSite, ProfileAccount } from '@masknet/shared-base' import { makeStyles } from '@masknet/theme' import { Box, Button, Typography } from '@mui/material' import { memo, useCallback } from 'react' @@ -52,7 +52,7 @@ export const AccountDetailUI = memo(function AccountDetail diff --git a/packages/mask/popups/pages/Personas/ConnectFirefly/bindFireflySession.ts b/packages/mask/popups/pages/Personas/ConnectFirefly/bindFireflySession.ts index 8c67083f8b0e..4dc6b2f6d5b8 100644 --- a/packages/mask/popups/pages/Personas/ConnectFirefly/bindFireflySession.ts +++ b/packages/mask/popups/pages/Personas/ConnectFirefly/bindFireflySession.ts @@ -1,9 +1,11 @@ +import { FireflyAlreadyBoundError } from '@masknet/shared-base' import { FarcasterSession, FIREFLY_ROOT_URL, fireflySessionHolder, patchFarcasterSessionRequired, resolveFireflyResponseData, + type LensSession, } from '@masknet/web3-providers' import { SessionType, type FireflyConfigAPI, type Session } from '@masknet/web3-providers/types' import urlcat from 'urlcat' @@ -39,7 +41,7 @@ async function bindFarcasterSessionToFirefly(session: FarcasterSession, signal?: isRelayService && response.error?.some((x) => x.includes('This farcaster already bound to the other account')) ) { - throw new Error('This Farcaster account has already bound to another Firefly account.') + throw new FireflyAlreadyBoundError('Farcaster') } const data = resolveFireflyResponseData(response) @@ -47,6 +49,28 @@ async function bindFarcasterSessionToFirefly(session: FarcasterSession, signal?: return data } +async function bindLensToFirefly(session: LensSession, signal?: AbortSignal) { + const response = await fireflySessionHolder.fetch( + urlcat(FIREFLY_ROOT_URL, '/v3/user/bindLens'), + { + method: 'POST', + body: JSON.stringify({ + accessToken: session.token, + isForce: false, + version: 'v3', + }), + signal, + }, + ) + + if (response.error?.some((x) => x.includes('This wallet already bound to the other account'))) { + throw new FireflyAlreadyBoundError('Lens') + } + + const data = resolveFireflyResponseData(response) + return data +} + /** * Bind a lens or farcaster session to the currently logged-in Firefly session. * @param session @@ -58,6 +82,8 @@ export async function bindFireflySession(session: Session, signal?: AbortSignal) fireflySessionHolder.assertSession() if (session.type === SessionType.Farcaster) { return bindFarcasterSessionToFirefly(session as FarcasterSession, signal) + } else if (session.type === SessionType.Lens) { + return bindLensToFirefly(session as LensSession, signal) } else if (session.type === SessionType.Firefly) { throw new Error('Not allowed') } diff --git a/packages/mask/popups/pages/Personas/ConnectFirefly/index.tsx b/packages/mask/popups/pages/Personas/ConnectFirefly/index.tsx index 8f1c1cad483c..74bffebe4055 100644 --- a/packages/mask/popups/pages/Personas/ConnectFirefly/index.tsx +++ b/packages/mask/popups/pages/Personas/ConnectFirefly/index.tsx @@ -48,7 +48,6 @@ function useLogin() { const account = await createAccount() const done = await addAccount(account, options) - console.log('created account', account) if (done) showSnackbar(Your {Social.Source.Farcaster} account is now connected.) } catch (error) { // skip if the error is abort error @@ -92,26 +91,19 @@ export const Component = memo(function ConnectFireflyPage() { const { currentPersona } = PersonaContext.useContainer() useMount(async () => { - login(async () => { - try { - const account = await createAccountByRelayService((url) => { - setUrl(url) - }) - console.log('account', account) - if (currentPersona) { - await Services.Identity.attachProfile( - ProfileIdentifier.of(EnhanceableSite.Farcaster, account.session.profileId).unwrap(), - currentPersona.identifier, - { connectionConfirmState: 'pending' }, - { token: account.session.token }, - ) - } - console.log('account', account) - return account - } catch (err) { - console.log('error', err) - throw err + await login(async () => { + const account = await createAccountByRelayService((url) => { + setUrl(url) + }) + if (currentPersona) { + await Services.Identity.attachProfile( + ProfileIdentifier.of(EnhanceableSite.Farcaster, account.session.profileId).unwrap(), + currentPersona.identifier, + { connectionConfirmState: 'pending' }, + { token: account.session.token }, + ) } + return account }) }) diff --git a/packages/mask/popups/pages/Personas/ConnectLens/createLensSession.ts b/packages/mask/popups/pages/Personas/ConnectLens/createLensSession.ts new file mode 100644 index 000000000000..c0d90c094cc9 --- /dev/null +++ b/packages/mask/popups/pages/Personas/ConnectLens/createLensSession.ts @@ -0,0 +1,27 @@ +import type { SessionClient } from '@lens-protocol/client' +import { LensSession } from '@masknet/web3-providers' +import { ZERO_ADDRESS } from '@masknet/web3-shared-evm' + +const SEVEN_DAYS = 1000 * 60 * 60 * 24 * 7 + +export function createLensSession(profileId: string, sessionClient: SessionClient) { + const now = Date.now() + const credentialsRe = sessionClient.getCredentials() + if (credentialsRe.isErr()) { + throw new Error(credentialsRe.error.message ?? 'Failed to get lens credentials') + } + const credentials = credentialsRe.value + if (!credentials) throw new Error('Failed to get lens credentials') + + const authenticatedRes = sessionClient.getAuthenticatedUser() + if (!authenticatedRes.isOk()) { + throw new Error(authenticatedRes.error.message) + } + const authenticated = authenticatedRes.value + + const address = authenticated.address + + const { accessToken, refreshToken } = credentials + + return new LensSession(profileId, accessToken, now, now + SEVEN_DAYS, refreshToken, address ?? ZERO_ADDRESS) +} diff --git a/packages/mask/popups/pages/Personas/ConnectLens/index.tsx b/packages/mask/popups/pages/Personas/ConnectLens/index.tsx new file mode 100644 index 000000000000..49c63ad05b3d --- /dev/null +++ b/packages/mask/popups/pages/Personas/ConnectLens/index.tsx @@ -0,0 +1,204 @@ +import type { AccountAvailable, EvmAddress } from '@lens-protocol/client' +import { Image, PersonaContext, useAvailableLensAccounts, useLensClient, useMyLensAccount } from '@masknet/shared' +import { EMPTY_LIST, EnhanceableSite, ProfileIdentifier } from '@masknet/shared-base' +import { LoadingBase, makeStyles } from '@masknet/theme' +import { LensV3 } from '@masknet/web3-providers' +import { isSameAddress } from '@masknet/web3-shared-base' +import { + List, + ListItemButton, + ListItemIcon, + ListItemSecondaryAction, + ListItemText, + Radio, + Typography, +} from '@mui/material' +import { first } from 'lodash-es' +import { memo, useState } from 'react' +import { Icons } from '@masknet/icons' +import { formatEthereumAddress } from '@masknet/web3-shared-evm' +import { LoadingButton } from '@mui/lab' +import { useAsyncFn } from 'react-use' +import { Trans } from '@lingui/react/macro' +import { createLensSession } from './createLensSession' +import Services from '#services' + +const useStyles = makeStyles()((theme) => ({ + container: { + minHeight: 0, + display: 'flex', + flexDirection: 'column', + }, + loading: { + height: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, + list: { + minHeight: 0, + overflow: 'auto', + marginBottom: theme.spacing(1.5), + scrollbarWidth: 'none', + '::-webkit-scrollbar': { + backgroundColor: 'transparent', + width: 18, + }, + '::-webkit-scrollbar-thumb': { + borderRadius: '20px', + width: 5, + border: '7px solid rgba(0, 0, 0, 0)', + backgroundColor: theme.palette.maskColor.secondaryLine, + backgroundClip: 'padding-box', + }, + }, + avatar: { + borderRadius: 99, + overflow: 'hidden', + }, + primary: { + color: theme.palette.maskColor.main, + fontWeight: 700, + lineHeight: '18px', + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + paddingRight: 50, + }, + second: { + display: 'flex', + columnGap: 4, + alignItems: 'center', + }, + address: { + fontWeight: 700, + fontSize: 14, + lineHeight: '18px', + color: theme.palette.maskColor.second, + }, + managedTag: { + background: theme.palette.maskColor.third, + color: theme.palette.maskColor.bottom, + fontSize: 12, + padding: theme.spacing(0.5), + borderRadius: 4, + lineHeight: '12px', + }, + item: { + padding: theme.spacing(1.5), + borderRadius: 8, + }, + disabled: { + opacity: 0.5, + cursor: 'not-allowed', + }, + listItemText: { + margin: 0, + }, + buttonWrap: { + padding: theme.spacing(1.5), + }, +})) + +export const Component = memo(function ConnectLensView() { + const { classes, cx } = useStyles() + const { data: accounts = EMPTY_LIST, isLoading } = useAvailableLensAccounts() + const myLensAccount = useMyLensAccount() + const myLensAddress = myLensAccount?.account.address + const currentAccount = accounts?.find((p) => isSameAddress(p.account.address, myLensAddress)) || first(accounts) + const lensClient = useLensClient() + const [selected = currentAccount, setSelected] = useState() + const selectedAccountId = selected?.account.username?.id + const { currentPersona } = PersonaContext.useContainer() + + const [{ loading }, connect] = useAsyncFn(async (account: AccountAvailable) => { + const client = await lensClient.login(account as AccountAvailable) + const profileId = account.account.address + const session = createLensSession(profileId, client) + if (currentPersona) { + await Services.Identity.attachProfile( + ProfileIdentifier.of(EnhanceableSite.Lens, profileId).unwrap(), + currentPersona.identifier, + { connectionConfirmState: 'pending' }, + { token: session.token }, + ) + } + }, []) + + if (isLoading) { + return ( +
+ +
+ ) + } + + return ( +
+ + {accounts?.map((accountItem) => { + const { account, __typename: accountType } = accountItem + const avatar = LensV3.getAccountAvatar(account) + const name = account.metadata?.name || account.username?.localName + const ownerAddress: EvmAddress = account.username?.ownedBy as EvmAddress + const accountId = account.username?.id + const disabled = accountId === selectedAccountId + return ( + { + if (disabled) return + setSelected(accountItem) + }}> + + {avatar ? + } + /> + : } + + + + {formatEthereumAddress(ownerAddress, 4)} + + {accountType === 'AccountManaged' ? + + Managed + + : null} +
+ } + /> + + + + + ) + })} +
+
+ { + if (!selected) return + connect(selected) + }}> + Connect + +
+ + ) +}) diff --git a/packages/mask/popups/pages/Personas/components/AccountAvatar/index.tsx b/packages/mask/popups/pages/Personas/components/AccountAvatar/index.tsx index 2f7c314496c7..5e9d8b881708 100644 --- a/packages/mask/popups/pages/Personas/components/AccountAvatar/index.tsx +++ b/packages/mask/popups/pages/Personas/components/AccountAvatar/index.tsx @@ -42,7 +42,7 @@ const useStyles = makeStyles()((theme) => ({ export interface AccountAvatar extends withClasses<'avatar' | 'container'> { avatar?: string | null - network?: string + network?: EnhanceableSite isValid?: boolean size?: number } diff --git a/packages/mask/popups/pages/Personas/index.tsx b/packages/mask/popups/pages/Personas/index.tsx index 0ba08f44a61e..72c9170c8efe 100644 --- a/packages/mask/popups/pages/Personas/index.tsx +++ b/packages/mask/popups/pages/Personas/index.tsx @@ -1,11 +1,12 @@ -import { memo, useEffect } from 'react' -import { useMount, useAsync } from 'react-use' -import { Navigate, Outlet, useNavigate, useSearchParams, type RouteObject } from 'react-router-dom' +import Services from '#services' import { CrossIsolationMessages, PopupModalRoutes, PopupRoutes, relativeRouteOf } from '@masknet/shared-base' -import { PersonaHeader } from './components/PersonaHeader/index.js' import { EVMWeb3ContextProvider } from '@masknet/web3-hooks-base' +import { memo, useEffect } from 'react' +import { Navigate, Outlet, useNavigate, useSearchParams, type RouteObject } from 'react-router-dom' +import { useAsync, useMount } from 'react-use' import { useModalNavigate } from '../../components/index.js' -import Services from '#services' +import { WalletGuard } from '../Wallet/WalletGuard/index.js' +import { PersonaHeader } from './components/PersonaHeader/index.js' const r = relativeRouteOf(PopupRoutes.Personas) export const personaRoute: RouteObject[] = [ @@ -18,6 +19,15 @@ export const personaRoute: RouteObject[] = [ { path: r(PopupRoutes.ExportPrivateKey), lazy: () => import('./ExportPrivateKey/index.js') }, { path: r(PopupRoutes.PersonaAvatarSetting), lazy: () => import('./PersonaAvatarSetting/index.js') }, { path: r(PopupRoutes.ConnectFirefly), lazy: () => import('./ConnectFirefly/index.js') }, + { + element: , + children: [ + { + path: r(PopupRoutes.ConnectLens), + lazy: () => import('./ConnectLens/index.js'), + }, + ], + }, { path: '*', element: }, ] diff --git a/packages/mask/popups/pages/Wallet/Interaction/InteractionContext.ts b/packages/mask/popups/pages/Wallet/Interaction/InteractionContext.ts index acb61fed71db..32d165b4b81d 100644 --- a/packages/mask/popups/pages/Wallet/Interaction/InteractionContext.ts +++ b/packages/mask/popups/pages/Wallet/Interaction/InteractionContext.ts @@ -10,7 +10,7 @@ import { createContainer, useRenderPhraseCallbackOnDepsChange } from '@masknet/s export const { Provider: InteractionWalletContext, useContainer: useInteractionWalletContext } = createContainer( function () { const wallet = useWallet() - const [interactionWallet, setInteractionWallet] = useState() + const [interactionWallet, setInteractionWallet] = useState() function useInteractionWallet(currentInteractingWallet: string | undefined) { useRenderPhraseCallbackOnDepsChange(() => { diff --git a/packages/mask/popups/pages/Wallet/WalletGuard/index.tsx b/packages/mask/popups/pages/Wallet/WalletGuard/index.tsx index 91b5f3acb8e4..9ce58a7cef5d 100644 --- a/packages/mask/popups/pages/Wallet/WalletGuard/index.tsx +++ b/packages/mask/popups/pages/Wallet/WalletGuard/index.tsx @@ -9,9 +9,13 @@ import { WalletHeader } from '../components/WalletHeader/index.js' import { useWalletLockStatus } from '../hooks/index.js' import { useMessageGuard } from './useMessageGuard.js' import { usePaymentPasswordGuard } from './usePaymentPasswordGuard.js' -import { InteractionWalletContext, useInteractionWalletContext } from '../Interaction/InteractionContext.js' +import { useInteractionWalletContext } from '../Interaction/InteractionContext.js' -export const WalletGuard = memo(function WalletGuard() { +interface Props { + disableHeader?: boolean +} + +export const WalletGuard = memo(function WalletGuard({ disableHeader }: Props) { const wallets = useWallets() const location = useLocation() const [params] = useSearchParams() @@ -19,6 +23,7 @@ export const WalletGuard = memo(function WalletGuard() { const hitPaymentPasswordGuard = usePaymentPasswordGuard() const hitMessageGuard = useMessageGuard() + const { interactionWallet } = useInteractionWalletContext() if (!wallets.length) { return ( @@ -44,18 +49,11 @@ export const WalletGuard = memo(function WalletGuard() { if (hitMessageGuard) return return ( - - - - ) -}) - -function WalletGuardContent() { - const { interactionWallet } = useInteractionWalletContext() - return ( - - + + {!disableHeader ? + + : null} ) -} +}) diff --git a/packages/mask/popups/pages/Wallet/components/WalletHeader/UI.tsx b/packages/mask/popups/pages/Wallet/components/WalletHeader/UI.tsx index 30fa0fe75c64..75695766587e 100644 --- a/packages/mask/popups/pages/Wallet/components/WalletHeader/UI.tsx +++ b/packages/mask/popups/pages/Wallet/components/WalletHeader/UI.tsx @@ -169,11 +169,7 @@ export const WalletHeaderUI = memo(function WalletHeaderUI( {!disabled && !wallet.owner ? - + : null} {isPending ? null : ( diff --git a/packages/mask/popups/pages/Wallet/components/WalletHeader/index.tsx b/packages/mask/popups/pages/Wallet/components/WalletHeader/index.tsx index ae00fc764565..2ee67a9e2370 100644 --- a/packages/mask/popups/pages/Wallet/components/WalletHeader/index.tsx +++ b/packages/mask/popups/pages/Wallet/components/WalletHeader/index.tsx @@ -14,7 +14,10 @@ const CUSTOM_HEADER_PATTERNS = [ PopupRoutes.ExportWalletPrivateKey, ] -export const WalletHeader = memo(function WalletHeader() { +interface Props { + isWallet?: boolean +} +export const WalletHeader = memo(function WalletHeader({ isWallet }: Props) { const modalNavigate = useModalNavigate() const { chainId } = useChainContext() const location = useLocation() @@ -29,7 +32,7 @@ export const WalletHeader = memo(function WalletHeader() { const currentNetwork = useNetwork(NetworkPluginID.PLUGIN_EVM, chainId) const matchResetWallet = useMatch(PopupRoutes.ResetWallet) - const matchWallet = PopupRoutes.Wallet === location.pathname + const matchWallet = isWallet || PopupRoutes.Wallet === location.pathname const customHeader = CUSTOM_HEADER_PATTERNS.some((pattern) => matchPath(pattern, location.pathname)) const matchContractInteraction = useMatch(PopupRoutes.ContractInteraction) diff --git a/packages/mask/shared-ui/locale/en-US.po b/packages/mask/shared-ui/locale/en-US.po index 798908244ad9..3a882311b9f9 100644 --- a/packages/mask/shared-ui/locale/en-US.po +++ b/packages/mask/shared-ui/locale/en-US.po @@ -659,6 +659,7 @@ msgstr "" #: popups/components/ConnectedWallet/index.tsx #: popups/components/SocialAccounts/index.tsx #: popups/modals/SelectProviderModal/index.tsx +#: popups/pages/Personas/ConnectLens/index.tsx msgid "Connect" msgstr "" diff --git a/packages/mask/shared-ui/locale/ja-JP.po b/packages/mask/shared-ui/locale/ja-JP.po index c05d7fc3ee75..67dc21100d96 100644 --- a/packages/mask/shared-ui/locale/ja-JP.po +++ b/packages/mask/shared-ui/locale/ja-JP.po @@ -664,6 +664,7 @@ msgstr "おめでとうございます!" #: popups/components/ConnectedWallet/index.tsx #: popups/components/SocialAccounts/index.tsx #: popups/modals/SelectProviderModal/index.tsx +#: popups/pages/Personas/ConnectLens/index.tsx msgid "Connect" msgstr "接続" diff --git a/packages/mask/shared-ui/locale/ko-KR.po b/packages/mask/shared-ui/locale/ko-KR.po index 0bff0ddcdbcc..71f409dc178d 100644 --- a/packages/mask/shared-ui/locale/ko-KR.po +++ b/packages/mask/shared-ui/locale/ko-KR.po @@ -664,6 +664,7 @@ msgstr "축하합니다" #: popups/components/ConnectedWallet/index.tsx #: popups/components/SocialAccounts/index.tsx #: popups/modals/SelectProviderModal/index.tsx +#: popups/pages/Personas/ConnectLens/index.tsx msgid "Connect" msgstr "연결" diff --git a/packages/mask/shared-ui/locale/zh-CN.po b/packages/mask/shared-ui/locale/zh-CN.po index 99e5d56f9026..6b8a6a488e40 100644 --- a/packages/mask/shared-ui/locale/zh-CN.po +++ b/packages/mask/shared-ui/locale/zh-CN.po @@ -664,6 +664,7 @@ msgstr "恭喜您!" #: popups/components/ConnectedWallet/index.tsx #: popups/components/SocialAccounts/index.tsx #: popups/modals/SelectProviderModal/index.tsx +#: popups/pages/Personas/ConnectLens/index.tsx msgid "Connect" msgstr "连接" diff --git a/packages/mask/shared-ui/locale/zh-TW.po b/packages/mask/shared-ui/locale/zh-TW.po index a55cc7d63594..275ced22dfa1 100644 --- a/packages/mask/shared-ui/locale/zh-TW.po +++ b/packages/mask/shared-ui/locale/zh-TW.po @@ -664,6 +664,7 @@ msgstr "" #: popups/components/ConnectedWallet/index.tsx #: popups/components/SocialAccounts/index.tsx #: popups/modals/SelectProviderModal/index.tsx +#: popups/pages/Personas/ConnectLens/index.tsx msgid "Connect" msgstr "" diff --git a/packages/mask/shared/definitions/event.ts b/packages/mask/shared/definitions/event.ts index 167aec78276e..ae921f6109f7 100644 --- a/packages/mask/shared/definitions/event.ts +++ b/packages/mask/shared/definitions/event.ts @@ -11,6 +11,7 @@ export const EventMap: Record = { [EnhanceableSite.Mirror]: EventID.Debug, [EnhanceableSite.Firefly]: EventID.Debug, [EnhanceableSite.Farcaster]: EventID.Debug, + [EnhanceableSite.Lens]: EventID.Debug, } export const DisconnectEventMap: Record = { diff --git a/packages/mask/shared/site-adaptors/definitions.ts b/packages/mask/shared/site-adaptors/definitions.ts index 0b6f17f0a0a8..8f154ff702de 100644 --- a/packages/mask/shared/site-adaptors/definitions.ts +++ b/packages/mask/shared/site-adaptors/definitions.ts @@ -1,23 +1,23 @@ import { FacebookAdaptor } from './implementations/facebook.com.js' +import { FarcasterAdaptor } from './implementations/farcaster.xyz.js' +import { LensAdaptor } from './implementations/hey.xyz.js' import { InstagramAdaptor } from './implementations/instagram.com.js' import { MindsAdaptor } from './implementations/minds.com.js' import { MirrorAdaptor } from './implementations/mirror.xyz.js' import { TwitterAdaptor } from './implementations/twitter.com.js' import type { SiteAdaptor } from './types.js' -import { FarcasterAdaptor } from './implementations/farcaster.xyz.js' -const defined = new Map() -export const definedSiteAdaptors: ReadonlyMap = defined - -function defineSiteAdaptor(UI: SiteAdaptor.Definition) { - defined.set(UI.networkIdentifier, UI) -} -defineSiteAdaptor(FacebookAdaptor) -defineSiteAdaptor(InstagramAdaptor) -defineSiteAdaptor(MindsAdaptor) -defineSiteAdaptor(MirrorAdaptor) -defineSiteAdaptor(TwitterAdaptor) -defineSiteAdaptor(FarcasterAdaptor) +export const definedSiteAdaptors: ReadonlyMap = new Map( + [ + [FacebookAdaptor.networkIdentifier, FacebookAdaptor], + [InstagramAdaptor.networkIdentifier, InstagramAdaptor], + [MindsAdaptor.networkIdentifier, MindsAdaptor], + [MirrorAdaptor.networkIdentifier, MirrorAdaptor], + [TwitterAdaptor.networkIdentifier, TwitterAdaptor], + [FarcasterAdaptor.networkIdentifier, FarcasterAdaptor], + [LensAdaptor.networkIdentifier, LensAdaptor], + ], +) function matches(url: string, pattern: string) { const l = new URL(pattern) diff --git a/packages/mask/shared/site-adaptors/implementations/hey.xyz.ts b/packages/mask/shared/site-adaptors/implementations/hey.xyz.ts new file mode 100644 index 000000000000..6759da7a9095 --- /dev/null +++ b/packages/mask/shared/site-adaptors/implementations/hey.xyz.ts @@ -0,0 +1,13 @@ +import { EnhanceableSite } from '@masknet/shared-base' +import type { SiteAdaptor } from '../types.js' + +const origins = ['https://hey.xyz/*'] + +export const LensAdaptor: SiteAdaptor.Definition = { + name: 'Lens', + networkIdentifier: EnhanceableSite.Lens, + declarativePermissions: { origins }, + homepage: 'https://lens.xyz', + isSocialNetwork: true, + sortIndex: 1, +} diff --git a/packages/mask/shared/site-adaptors/types.d.ts b/packages/mask/shared/site-adaptors/types.d.ts index 9fca815db3eb..fc7d5fab6c8d 100644 --- a/packages/mask/shared/site-adaptors/types.d.ts +++ b/packages/mask/shared/site-adaptors/types.d.ts @@ -1,4 +1,4 @@ -import type { ProfileIdentifier } from '@masknet/shared-base' +import type { EnhanceableSite, ProfileIdentifier } from '@masknet/shared-base' // This file should be a .ts file, not a .d.ts file (that skips type checking). // but this causes "because it would overwrite input file" error in incremental compiling which is annoying. @@ -8,7 +8,7 @@ export declare namespace SiteAdaptor { } export interface Definition { name: string - networkIdentifier: string + networkIdentifier: EnhanceableSite // Note: if declarativePermissions is no longer sufficient, use "false" to indicate it need a load(). declarativePermissions: DeclarativePermissions homepage: string diff --git a/packages/plugins/RedPacket/src/locale/en-US.po b/packages/plugins/RedPacket/src/locale/en-US.po index 612c5a199f6c..324ee04178f7 100644 --- a/packages/plugins/RedPacket/src/locale/en-US.po +++ b/packages/plugins/RedPacket/src/locale/en-US.po @@ -11,8 +11,8 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" -#. placeholder {0}: format(fromUnixTime(timestamp), 'M/d/yyyy HH:mm') #. placeholder {0}: dateTimeFormat(locale, new Date(patchedHistory.creation_time)) +#. placeholder {0}: format(fromUnixTime(timestamp), 'M/d/yyyy HH:mm') #: src/SiteAdaptor/components/NftRedPacketRecord.tsx #: src/SiteAdaptor/components/RedPacketRecord.tsx msgid "{0} (UTC+8)" diff --git a/packages/plugins/RedPacket/src/locale/ja-JP.po b/packages/plugins/RedPacket/src/locale/ja-JP.po index bd51755dec13..7f0df0ea47e1 100644 --- a/packages/plugins/RedPacket/src/locale/ja-JP.po +++ b/packages/plugins/RedPacket/src/locale/ja-JP.po @@ -16,8 +16,8 @@ msgstr "" "X-Crowdin-File: /[DimensionDev.Maskbook] develop/packages/plugins/RedPacket/src/locale/en-US.po\n" "X-Crowdin-File-ID: 984\n" -#. placeholder {0}: format(fromUnixTime(timestamp), 'M/d/yyyy HH:mm') #. placeholder {0}: dateTimeFormat(locale, new Date(patchedHistory.creation_time)) +#. placeholder {0}: format(fromUnixTime(timestamp), 'M/d/yyyy HH:mm') #: src/SiteAdaptor/components/NftRedPacketRecord.tsx #: src/SiteAdaptor/components/RedPacketRecord.tsx msgid "{0} (UTC+8)" diff --git a/packages/plugins/RedPacket/src/locale/ko-KR.po b/packages/plugins/RedPacket/src/locale/ko-KR.po index e8076f35640e..032801170f4c 100644 --- a/packages/plugins/RedPacket/src/locale/ko-KR.po +++ b/packages/plugins/RedPacket/src/locale/ko-KR.po @@ -16,8 +16,8 @@ msgstr "" "X-Crowdin-File: /[DimensionDev.Maskbook] develop/packages/plugins/RedPacket/src/locale/en-US.po\n" "X-Crowdin-File-ID: 984\n" -#. placeholder {0}: format(fromUnixTime(timestamp), 'M/d/yyyy HH:mm') #. placeholder {0}: dateTimeFormat(locale, new Date(patchedHistory.creation_time)) +#. placeholder {0}: format(fromUnixTime(timestamp), 'M/d/yyyy HH:mm') #: src/SiteAdaptor/components/NftRedPacketRecord.tsx #: src/SiteAdaptor/components/RedPacketRecord.tsx msgid "{0} (UTC+8)" diff --git a/packages/plugins/RedPacket/src/locale/zh-CN.po b/packages/plugins/RedPacket/src/locale/zh-CN.po index c1bd325b2256..a2f2b2500b8b 100644 --- a/packages/plugins/RedPacket/src/locale/zh-CN.po +++ b/packages/plugins/RedPacket/src/locale/zh-CN.po @@ -16,8 +16,8 @@ msgstr "" "X-Crowdin-File: /[DimensionDev.Maskbook] develop/packages/plugins/RedPacket/src/locale/en-US.po\n" "X-Crowdin-File-ID: 984\n" -#. placeholder {0}: format(fromUnixTime(timestamp), 'M/d/yyyy HH:mm') #. placeholder {0}: dateTimeFormat(locale, new Date(patchedHistory.creation_time)) +#. placeholder {0}: format(fromUnixTime(timestamp), 'M/d/yyyy HH:mm') #: src/SiteAdaptor/components/NftRedPacketRecord.tsx #: src/SiteAdaptor/components/RedPacketRecord.tsx msgid "{0} (UTC+8)" diff --git a/packages/plugins/RedPacket/src/locale/zh-TW.po b/packages/plugins/RedPacket/src/locale/zh-TW.po index ee896a69b194..a07ff55c5bda 100644 --- a/packages/plugins/RedPacket/src/locale/zh-TW.po +++ b/packages/plugins/RedPacket/src/locale/zh-TW.po @@ -16,8 +16,8 @@ msgstr "" "X-Crowdin-File: /[DimensionDev.Maskbook] develop/packages/plugins/RedPacket/src/locale/en-US.po\n" "X-Crowdin-File-ID: 984\n" -#. placeholder {0}: format(fromUnixTime(timestamp), 'M/d/yyyy HH:mm') #. placeholder {0}: dateTimeFormat(locale, new Date(patchedHistory.creation_time)) +#. placeholder {0}: format(fromUnixTime(timestamp), 'M/d/yyyy HH:mm') #: src/SiteAdaptor/components/NftRedPacketRecord.tsx #: src/SiteAdaptor/components/RedPacketRecord.tsx msgid "{0} (UTC+8)" diff --git a/packages/shared-base/src/LegacySettings/settings.ts b/packages/shared-base/src/LegacySettings/settings.ts index d6c110da6bfe..1cc8ce0cddf1 100644 --- a/packages/shared-base/src/LegacySettings/settings.ts +++ b/packages/shared-base/src/LegacySettings/settings.ts @@ -24,6 +24,7 @@ export const pluginIDsSettings = createGlobalSettings = { /(?:^(?:firefly\.|firefly-staging\.|firefly-canary\.)?mask\.social|[\w-]+\.vercel\.app)$/iu : /^localhost:\d+$/u, [EnhanceableSite.Farcaster]: /(^|\.)farcaster\.xyz$/iu, + [EnhanceableSite.Lens]: /(^|\.)hey\.xyz$/iu, } const matchExtensionSitePathname: Record = { diff --git a/packages/shared-base/src/Site/types.ts b/packages/shared-base/src/Site/types.ts index 588328dcc262..d269eaf03420 100644 --- a/packages/shared-base/src/Site/types.ts +++ b/packages/shared-base/src/Site/types.ts @@ -3,6 +3,7 @@ export enum EnhanceableSite { Twitter = 'twitter.com', Facebook = 'facebook.com', Farcaster = 'farcaster.xyz', + Lens = 'lens.xyz', Minds = 'minds.com', Instagram = 'instagram.com', OpenSea = 'opensea.io', diff --git a/packages/shared-base/src/constants.ts b/packages/shared-base/src/constants.ts index 3ab4f7e2c478..e28a8741254c 100644 --- a/packages/shared-base/src/constants.ts +++ b/packages/shared-base/src/constants.ts @@ -12,6 +12,7 @@ export const SOCIAL_MEDIA_NAME: Record = { [EnhanceableSite.Mirror]: 'Mirror', [EnhanceableSite.Farcaster]: 'Farcaster', [EnhanceableSite.Firefly]: 'Firefly', + [EnhanceableSite.Lens]: 'Lens', } export const NEXT_ID_PLATFORM_SOCIAL_MEDIA_MAP: Record = { diff --git a/packages/shared-base/src/errors.ts b/packages/shared-base/src/errors.ts index 88b895dc2dbf..f1a7948834e8 100644 --- a/packages/shared-base/src/errors.ts +++ b/packages/shared-base/src/errors.ts @@ -39,3 +39,11 @@ export class FireflyAlreadyBoundError extends Error { super(`This ${source} account has already bound to another Firefly account.`) } } + +export class NotAllowedError extends Error { + override name = 'NotAllowedError' + + constructor(message?: string) { + super(message ?? 'Not allowed.') + } +} diff --git a/packages/shared-base/src/types/Routes.ts b/packages/shared-base/src/types/Routes.ts index ee1c25426d77..d9f8a557faef 100644 --- a/packages/shared-base/src/types/Routes.ts +++ b/packages/shared-base/src/types/Routes.ts @@ -89,6 +89,7 @@ export enum PopupRoutes { ExportPrivateKey = '/personas/export-private-key', PersonaAvatarSetting = '/personas/avatar-setting', ConnectFirefly = '/personas/connect-firefly', + ConnectLens = '/personas/connect-lens', Trader = '/trader', } export interface PopupRoutesParamsMap { diff --git a/packages/shared/src/constants.tsx b/packages/shared/src/constants.tsx index f2add927ae60..5e281ec85219 100644 --- a/packages/shared/src/constants.tsx +++ b/packages/shared/src/constants.tsx @@ -1,7 +1,7 @@ import { Icons, type GeneratedIcon } from '@masknet/icons' import { EnhanceableSite } from '@masknet/shared-base' -export const SOCIAL_MEDIA_ROUND_ICON_MAPPING: Record = { +export const SOCIAL_MEDIA_ROUND_ICON_MAPPING: Record = { [EnhanceableSite.Twitter]: Icons.TwitterXRound, [EnhanceableSite.Facebook]: Icons.FacebookRound, [EnhanceableSite.Minds]: Icons.MindsRound, @@ -11,7 +11,8 @@ export const SOCIAL_MEDIA_ROUND_ICON_MAPPING: Record export enum RSS3_NFT_SITE_KEY { TWITTER = '_nfts', diff --git a/packages/web3-hooks/base/src/useContext.tsx b/packages/web3-hooks/base/src/useContext.tsx index 5ced82d17aab..631c3fd631cd 100644 --- a/packages/web3-hooks/base/src/useContext.tsx +++ b/packages/web3-hooks/base/src/useContext.tsx @@ -22,6 +22,7 @@ interface ChainContextGetter { providerType?: Web3Helper.Definition[T]['ProviderType'] // If it's controlled, we prefer passed value over state inside controlled?: boolean + isPopupWallet?: boolean } interface ChainContextSetter { @@ -62,7 +63,7 @@ export function NetworkContextProvider({ */ export const ChainContextProvider = memo(function ChainContextProvider(props: PropsWithChildren) { const { pluginID } = useNetworkContext() - const { controlled } = props + const { controlled, isPopupWallet } = props const globalAccount = useAccount(pluginID) const globalChainId = useChainId(pluginID) @@ -78,7 +79,8 @@ export const ChainContextProvider = memo(function ChainContextProvider(props: Pr const [_providerType, setProviderType] = useState() const location = useLocation() - const is_popup_wallet_page = Sniffings.is_popup_page && location.hash?.includes(PopupRoutes.Wallet) + const is_popup_wallet_page = + Sniffings.is_popup_page && (location.hash?.includes(PopupRoutes.Wallet) || isPopupWallet) const account = controlled ? props.account : (_account ?? props.account ?? (is_popup_wallet_page ? maskAccount : globalAccount)) const chainId = diff --git a/packages/web3-providers/src/AvatarStore/helpers/getAvatar.ts b/packages/web3-providers/src/AvatarStore/helpers/getAvatar.ts index 12253261fca9..bd0927f83f89 100644 --- a/packages/web3-providers/src/AvatarStore/helpers/getAvatar.ts +++ b/packages/web3-providers/src/AvatarStore/helpers/getAvatar.ts @@ -18,6 +18,7 @@ const resolveRSS3Key = createLookupTableResolver { + throw new NotAllowedError() + } + + async destroy(): Promise { + throw new NotAllowedError() + } +} diff --git a/packages/web3-providers/src/Firefly/index.ts b/packages/web3-providers/src/Firefly/index.ts index f3242bc9bc2f..3b152effaa46 100644 --- a/packages/web3-providers/src/Firefly/index.ts +++ b/packages/web3-providers/src/Firefly/index.ts @@ -4,6 +4,7 @@ export * from './Twitter.js' export * from './Farcaster.js' export * from './Session.js' export * from './FarcasterSession.js' +export * from './LensSession.js' export * from './SessionHolder.js' export * from './helpers.js' export * from './constants.js' diff --git a/packages/web3-providers/src/FireflyAccount/index.ts b/packages/web3-providers/src/FireflyAccount/index.ts index f09dbf7e422e..55da72d1b7f6 100644 --- a/packages/web3-providers/src/FireflyAccount/index.ts +++ b/packages/web3-providers/src/FireflyAccount/index.ts @@ -28,5 +28,6 @@ export interface AccountOptions { } export async function addAccount(_account: FireflyAccount, _options?: AccountOptions) { + console.log('TODO: addAccount') return true } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c661daba2029..d7f378916de5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -375,6 +375,9 @@ importers: '@hookform/resolvers': specifier: ^3.6.0 version: 3.6.0(react-hook-form@7.52.0(react@0.0.0-experimental-58af67a8f8-20240628)) + '@lens-protocol/client': + specifier: 0.0.0-canary-20250408064617 + version: 0.0.0-canary-20250408064617(typescript@5.9.2) '@masknet/backup-format': specifier: workspace:^ version: link:../backup-format From 9d301d6729dfc4fd887dd6d37c8738d1ff03fef0 Mon Sep 17 00:00:00 2001 From: swkatmask Date: Mon, 8 Sep 2025 10:58:38 +0000 Subject: [PATCH 7/7] fix: linter --- packages/plugins/RedPacket/src/locale/en-US.po | 2 +- packages/plugins/RedPacket/src/locale/ja-JP.po | 2 +- packages/plugins/RedPacket/src/locale/ko-KR.po | 2 +- packages/plugins/RedPacket/src/locale/zh-CN.po | 2 +- packages/plugins/RedPacket/src/locale/zh-TW.po | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/plugins/RedPacket/src/locale/en-US.po b/packages/plugins/RedPacket/src/locale/en-US.po index 324ee04178f7..612c5a199f6c 100644 --- a/packages/plugins/RedPacket/src/locale/en-US.po +++ b/packages/plugins/RedPacket/src/locale/en-US.po @@ -11,8 +11,8 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" -#. placeholder {0}: dateTimeFormat(locale, new Date(patchedHistory.creation_time)) #. placeholder {0}: format(fromUnixTime(timestamp), 'M/d/yyyy HH:mm') +#. placeholder {0}: dateTimeFormat(locale, new Date(patchedHistory.creation_time)) #: src/SiteAdaptor/components/NftRedPacketRecord.tsx #: src/SiteAdaptor/components/RedPacketRecord.tsx msgid "{0} (UTC+8)" diff --git a/packages/plugins/RedPacket/src/locale/ja-JP.po b/packages/plugins/RedPacket/src/locale/ja-JP.po index 7f0df0ea47e1..bd51755dec13 100644 --- a/packages/plugins/RedPacket/src/locale/ja-JP.po +++ b/packages/plugins/RedPacket/src/locale/ja-JP.po @@ -16,8 +16,8 @@ msgstr "" "X-Crowdin-File: /[DimensionDev.Maskbook] develop/packages/plugins/RedPacket/src/locale/en-US.po\n" "X-Crowdin-File-ID: 984\n" -#. placeholder {0}: dateTimeFormat(locale, new Date(patchedHistory.creation_time)) #. placeholder {0}: format(fromUnixTime(timestamp), 'M/d/yyyy HH:mm') +#. placeholder {0}: dateTimeFormat(locale, new Date(patchedHistory.creation_time)) #: src/SiteAdaptor/components/NftRedPacketRecord.tsx #: src/SiteAdaptor/components/RedPacketRecord.tsx msgid "{0} (UTC+8)" diff --git a/packages/plugins/RedPacket/src/locale/ko-KR.po b/packages/plugins/RedPacket/src/locale/ko-KR.po index 032801170f4c..e8076f35640e 100644 --- a/packages/plugins/RedPacket/src/locale/ko-KR.po +++ b/packages/plugins/RedPacket/src/locale/ko-KR.po @@ -16,8 +16,8 @@ msgstr "" "X-Crowdin-File: /[DimensionDev.Maskbook] develop/packages/plugins/RedPacket/src/locale/en-US.po\n" "X-Crowdin-File-ID: 984\n" -#. placeholder {0}: dateTimeFormat(locale, new Date(patchedHistory.creation_time)) #. placeholder {0}: format(fromUnixTime(timestamp), 'M/d/yyyy HH:mm') +#. placeholder {0}: dateTimeFormat(locale, new Date(patchedHistory.creation_time)) #: src/SiteAdaptor/components/NftRedPacketRecord.tsx #: src/SiteAdaptor/components/RedPacketRecord.tsx msgid "{0} (UTC+8)" diff --git a/packages/plugins/RedPacket/src/locale/zh-CN.po b/packages/plugins/RedPacket/src/locale/zh-CN.po index a2f2b2500b8b..c1bd325b2256 100644 --- a/packages/plugins/RedPacket/src/locale/zh-CN.po +++ b/packages/plugins/RedPacket/src/locale/zh-CN.po @@ -16,8 +16,8 @@ msgstr "" "X-Crowdin-File: /[DimensionDev.Maskbook] develop/packages/plugins/RedPacket/src/locale/en-US.po\n" "X-Crowdin-File-ID: 984\n" -#. placeholder {0}: dateTimeFormat(locale, new Date(patchedHistory.creation_time)) #. placeholder {0}: format(fromUnixTime(timestamp), 'M/d/yyyy HH:mm') +#. placeholder {0}: dateTimeFormat(locale, new Date(patchedHistory.creation_time)) #: src/SiteAdaptor/components/NftRedPacketRecord.tsx #: src/SiteAdaptor/components/RedPacketRecord.tsx msgid "{0} (UTC+8)" diff --git a/packages/plugins/RedPacket/src/locale/zh-TW.po b/packages/plugins/RedPacket/src/locale/zh-TW.po index a07ff55c5bda..ee896a69b194 100644 --- a/packages/plugins/RedPacket/src/locale/zh-TW.po +++ b/packages/plugins/RedPacket/src/locale/zh-TW.po @@ -16,8 +16,8 @@ msgstr "" "X-Crowdin-File: /[DimensionDev.Maskbook] develop/packages/plugins/RedPacket/src/locale/en-US.po\n" "X-Crowdin-File-ID: 984\n" -#. placeholder {0}: dateTimeFormat(locale, new Date(patchedHistory.creation_time)) #. placeholder {0}: format(fromUnixTime(timestamp), 'M/d/yyyy HH:mm') +#. placeholder {0}: dateTimeFormat(locale, new Date(patchedHistory.creation_time)) #: src/SiteAdaptor/components/NftRedPacketRecord.tsx #: src/SiteAdaptor/components/RedPacketRecord.tsx msgid "{0} (UTC+8)"