Skip to content

Commit 5c5001e

Browse files
committed
feat: add support for lens social network integration
1 parent e6855a9 commit 5c5001e

File tree

44 files changed

+436
-98
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+436
-98
lines changed

packages/mask/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@dimensiondev/mask-wallet-core": "0.1.0-20211013082857-eb62e5f",
3737
"@ethereumjs/util": "^9.0.3",
3838
"@hookform/resolvers": "^3.6.0",
39+
"@lens-protocol/client": "0.0.0-canary-20250408064617",
3940
"@masknet/backup-format": "workspace:^",
4041
"@masknet/encryption": "workspace:^",
4142
"@masknet/flags": "workspace:^",

packages/mask/popups/Popup.tsx

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { PageUIProvider, PersonaContext } from '@masknet/shared'
2-
import { jsxCompose, MaskMessages, PopupRoutes } from '@masknet/shared-base'
2+
import { MaskMessages, PopupRoutes } from '@masknet/shared-base'
33
import { PopupSnackbarProvider } from '@masknet/theme'
44
import { EVMWeb3ContextProvider } from '@masknet/web3-hooks-base'
55
import { ProviderType } from '@masknet/web3-shared-evm'
66
import { Box } from '@mui/material'
7-
import { Suspense, cloneElement, lazy, memo, useEffect, useMemo, useState, type ReactNode } from 'react'
7+
import { Suspense, lazy, memo, useEffect, useMemo, useState, type ReactNode } from 'react'
88
import { useIdleTimer } from 'react-idle-timer'
99
import {
1010
createHashRouter,
@@ -30,6 +30,7 @@ import { WalletFrame, walletRoutes } from './pages/Wallet/index.js'
3030
import { ContactsFrame, contactsRoutes } from './pages/Friends/index.js'
3131
import { ErrorBoundaryUIOfError } from '../../shared-base-ui/src/components/ErrorBoundary/ErrorBoundary.js'
3232
import { TraderFrame, traderRoutes } from './pages/Trader/index.js'
33+
import { InteractionWalletContext } from './pages/Wallet/Interaction/InteractionContext.js'
3334

3435
const personaInitialState = {
3536
queryOwnedPersonaInformation: Services.Identity.queryOwnedPersonaInformation,
@@ -108,23 +109,31 @@ export default function Popups() {
108109
throttle: 10000,
109110
})
110111

111-
return jsxCompose(
112-
<PersistQueryClientProvider client={queryClient} persistOptions={queryPersistOptions} />,
113-
// eslint-disable-next-line react-compiler/react-compiler
114-
<PageUIProvider useTheme={usePopupTheme} />,
115-
<PopupSnackbarProvider children={null!} />,
116-
<EVMWeb3ContextProvider providerType={ProviderType.MaskWallet} />,
117-
<PopupContext />,
118-
<PageTitleContext value={titleContext} />,
119-
)(
120-
cloneElement,
121-
<>
122-
{/* https://github.com/TanStack/query/issues/5417 */}
123-
{process.env.NODE_ENV === 'development' ?
124-
<ReactQueryDevtools buttonPosition="bottom-right" />
125-
: null}
126-
<RouterProvider router={router} fallbackElement={pending} future={{ v7_startTransition: true }} />
127-
</>,
112+
return (
113+
<PersistQueryClientProvider client={queryClient} persistOptions={queryPersistOptions}>
114+
{/* eslint-disable-next-line react-compiler/react-compiler */}
115+
<PageUIProvider useTheme={usePopupTheme}>
116+
<PopupSnackbarProvider>
117+
<EVMWeb3ContextProvider providerType={ProviderType.MaskWallet}>
118+
<InteractionWalletContext>
119+
<PopupContext>
120+
<PageTitleContext value={titleContext}>
121+
{/* https://github.com/TanStack/query/issues/5417 */}
122+
{process.env.NODE_ENV === 'development' ?
123+
<ReactQueryDevtools buttonPosition="bottom-right" />
124+
: null}
125+
<RouterProvider
126+
router={router}
127+
fallbackElement={pending}
128+
future={{ v7_startTransition: true }}
129+
/>
130+
</PageTitleContext>
131+
</PopupContext>
132+
</InteractionWalletContext>
133+
</EVMWeb3ContextProvider>
134+
</PopupSnackbarProvider>
135+
</PageUIProvider>
136+
</PersistQueryClientProvider>
128137
)
129138
}
130139

packages/mask/popups/components/SocialAccounts/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export const SocialAccounts = memo<SocialAccountsProps>(function SocialAccounts(
9191
<Box className={classes.accountItem} key={index} onClick={() => onAccountClick(account)}>
9292
<AccountAvatar
9393
avatar={account.avatar}
94-
network={account.identifier.network}
94+
network={account.identifier.network as EnhanceableSite}
9595
isValid={account.is_valid}
9696
classes={{ avatar: classes.avatar }}
9797
/>

packages/mask/popups/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ export const SOCIAL_MEDIA_ICON_FILTER_COLOR: Record<EnhanceableSite, string> = {
1313
[EnhanceableSite.Localhost]: '',
1414
[EnhanceableSite.Firefly]: '',
1515
[EnhanceableSite.Farcaster]: '',
16+
[EnhanceableSite.Lens]: '',
1617
}

packages/mask/popups/hooks/useSupportSocialNetworks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useQuery } from '@tanstack/react-query'
44

55
export function useSupportSocialNetworks() {
66
return useQuery({
7-
queryKey: ['@@Service.SiteAdaptor.getSupportedSites({ isSocialNetwork: true })'],
7+
queryKey: ['Service.SiteAdaptor.getSupportedSites({ isSocialNetwork: true })'],
88
queryFn: async () => {
99
const sites = await Service.SiteAdaptor.getSupportedSites({ isSocialNetwork: true })
1010
return sites.map((x) => x.networkIdentifier as EnhanceableSite)

packages/mask/popups/modals/ConnectSocialAccountModal/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ export const ConnectSocialAccountModal = memo<ActionModalBaseProps>(function Con
2121
const handleConnect = useCallback(
2222
async (networkIdentifier: EnhanceableSite) => {
2323
if (networkIdentifier === EnhanceableSite.Farcaster) {
24-
navigate(PopupRoutes.ConnectFirefly)
25-
return
24+
return navigate(PopupRoutes.ConnectFirefly)
25+
} else if (networkIdentifier === EnhanceableSite.Lens) {
26+
return navigate(PopupRoutes.ConnectLens)
2627
}
2728
if (!currentPersona) return
2829
if (!(await requestPermissionFromExtensionPage(networkIdentifier))) return

packages/mask/popups/pages/Personas/AccountDetail/UI.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Trans } from '@lingui/react/macro'
2-
import type { BindingProof, ProfileAccount } from '@masknet/shared-base'
2+
import type { BindingProof, EnhanceableSite, ProfileAccount } from '@masknet/shared-base'
33
import { makeStyles } from '@masknet/theme'
44
import { Box, Button, Typography } from '@mui/material'
55
import { memo, useCallback } from 'react'
@@ -52,7 +52,7 @@ export const AccountDetailUI = memo<AccountDetailUIProps>(function AccountDetail
5252
<Box className={classes.account}>
5353
<AccountAvatar
5454
avatar={account.avatar}
55-
network={account.identifier.network}
55+
network={account.identifier.network as EnhanceableSite}
5656
isValid={account.is_valid}
5757
classes={{ avatar: classes.avatar }}
5858
/>

packages/mask/popups/pages/Personas/ConnectFirefly/bindFireflySession.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import { FireflyAlreadyBoundError } from '@masknet/shared-base'
12
import {
23
FarcasterSession,
34
FIREFLY_ROOT_URL,
45
fireflySessionHolder,
56
patchFarcasterSessionRequired,
67
resolveFireflyResponseData,
8+
type LensSession,
79
} from '@masknet/web3-providers'
810
import { SessionType, type FireflyConfigAPI, type Session } from '@masknet/web3-providers/types'
911
import urlcat from 'urlcat'
@@ -39,14 +41,36 @@ async function bindFarcasterSessionToFirefly(session: FarcasterSession, signal?:
3941
isRelayService &&
4042
response.error?.some((x) => x.includes('This farcaster already bound to the other account'))
4143
) {
42-
throw new Error('This Farcaster account has already bound to another Firefly account.')
44+
throw new FireflyAlreadyBoundError('Farcaster')
4345
}
4446

4547
const data = resolveFireflyResponseData(response)
4648
patchFarcasterSessionRequired(session, data.fid, data.farcaster_signer_private_key)
4749
return data
4850
}
4951

52+
async function bindLensToFirefly(session: LensSession, signal?: AbortSignal) {
53+
const response = await fireflySessionHolder.fetch<FireflyConfigAPI.BindResponse>(
54+
urlcat(FIREFLY_ROOT_URL, '/v3/user/bindLens'),
55+
{
56+
method: 'POST',
57+
body: JSON.stringify({
58+
accessToken: session.token,
59+
isForce: false,
60+
version: 'v3',
61+
}),
62+
signal,
63+
},
64+
)
65+
66+
if (response.error?.some((x) => x.includes('This wallet already bound to the other account'))) {
67+
throw new FireflyAlreadyBoundError('Lens')
68+
}
69+
70+
const data = resolveFireflyResponseData(response)
71+
return data
72+
}
73+
5074
/**
5175
* Bind a lens or farcaster session to the currently logged-in Firefly session.
5276
* @param session
@@ -58,6 +82,8 @@ export async function bindFireflySession(session: Session, signal?: AbortSignal)
5882
fireflySessionHolder.assertSession()
5983
if (session.type === SessionType.Farcaster) {
6084
return bindFarcasterSessionToFirefly(session as FarcasterSession, signal)
85+
} else if (session.type === SessionType.Lens) {
86+
return bindLensToFirefly(session as LensSession, signal)
6187
} else if (session.type === SessionType.Firefly) {
6288
throw new Error('Not allowed')
6389
}

packages/mask/popups/pages/Personas/ConnectFirefly/index.tsx

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ function useLogin() {
4848
const account = await createAccount()
4949

5050
const done = await addAccount(account, options)
51-
console.log('created account', account)
5251
if (done) showSnackbar(<Trans>Your {Social.Source.Farcaster} account is now connected.</Trans>)
5352
} catch (error) {
5453
// skip if the error is abort error
@@ -92,26 +91,19 @@ export const Component = memo(function ConnectFireflyPage() {
9291
const { currentPersona } = PersonaContext.useContainer()
9392

9493
useMount(async () => {
95-
login(async () => {
96-
try {
97-
const account = await createAccountByRelayService((url) => {
98-
setUrl(url)
99-
})
100-
console.log('account', account)
101-
if (currentPersona) {
102-
await Services.Identity.attachProfile(
103-
ProfileIdentifier.of(EnhanceableSite.Farcaster, account.session.profileId).unwrap(),
104-
currentPersona.identifier,
105-
{ connectionConfirmState: 'pending' },
106-
{ token: account.session.token },
107-
)
108-
}
109-
console.log('account', account)
110-
return account
111-
} catch (err) {
112-
console.log('error', err)
113-
throw err
94+
await login(async () => {
95+
const account = await createAccountByRelayService((url) => {
96+
setUrl(url)
97+
})
98+
if (currentPersona) {
99+
await Services.Identity.attachProfile(
100+
ProfileIdentifier.of(EnhanceableSite.Farcaster, account.session.profileId).unwrap(),
101+
currentPersona.identifier,
102+
{ connectionConfirmState: 'pending' },
103+
{ token: account.session.token },
104+
)
114105
}
106+
return account
115107
})
116108
})
117109

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { SessionClient } from '@lens-protocol/client'
2+
import { LensSession } from '@masknet/web3-providers'
3+
import { ZERO_ADDRESS } from '@masknet/web3-shared-evm'
4+
5+
const SEVEN_DAYS = 1000 * 60 * 60 * 24 * 7
6+
7+
export function createLensSession(profileId: string, sessionClient: SessionClient) {
8+
const now = Date.now()
9+
const credentialsRe = sessionClient.getCredentials()
10+
if (credentialsRe.isErr()) {
11+
throw new Error(credentialsRe.error.message ?? 'Failed to get lens credentials')
12+
}
13+
const credentials = credentialsRe.value
14+
if (!credentials) throw new Error('Failed to get lens credentials')
15+
16+
const authenticatedRes = sessionClient.getAuthenticatedUser()
17+
if (!authenticatedRes.isOk()) {
18+
throw new Error(authenticatedRes.error.message)
19+
}
20+
const authenticated = authenticatedRes.value
21+
22+
const address = authenticated.address
23+
24+
const { accessToken, refreshToken } = credentials
25+
26+
return new LensSession(profileId, accessToken, now, now + SEVEN_DAYS, refreshToken, address ?? ZERO_ADDRESS)
27+
}

0 commit comments

Comments
 (0)