Skip to content

Commit 8f55dff

Browse files
committed
feat(firefly): add firefly session bind and restore with farcaster integration
1 parent 21cd336 commit 8f55dff

File tree

28 files changed

+483
-58
lines changed

28 files changed

+483
-58
lines changed

cspell.json

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
11
{
22
"version": "0.2",
33
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
4-
"dictionaries": [
5-
"typescript",
6-
"node",
7-
"npm",
8-
"html",
9-
"css",
10-
"fonts"
11-
],
4+
"dictionaries": ["typescript", "node", "npm", "html", "css", "fonts"],
125
"ignorePaths": [
136
"./packages/gun-utils/gun.js",
147
"./packages/icons/**",
@@ -34,11 +27,7 @@
3427
"pnpm-workspace.yaml",
3528
"qya-aa.json"
3629
],
37-
"TODO: fix those words": [
38-
"bridgable",
39-
"clonable",
40-
"sniffings"
41-
],
30+
"TODO: fix those words": ["bridgable", "clonable", "sniffings"],
4231
"ignoreWords": [
4332
"aeth",
4433
"algr",
@@ -252,15 +241,12 @@
252241
"zksync",
253242
"zora"
254243
],
255-
"ignoreRegExpList": [
256-
"/[A-Za-z0-9]{44}/",
257-
"/[A-Za-z0-9]{46}/",
258-
"/[A-Za-z0-9]{59}/"
259-
],
244+
"ignoreRegExpList": ["/[A-Za-z0-9]{44}/", "/[A-Za-z0-9]{46}/", "/[A-Za-z0-9]{59}/"],
260245
"overrides": [],
261246
"words": [
262247
"arbitrum",
263248
"boba",
249+
"Bsky",
264250
"cashtags",
265251
"celo",
266252
"deeplink",
@@ -269,6 +255,7 @@
269255
"linkedin",
270256
"luma",
271257
"muln",
258+
"pathnames",
272259
"reposted",
273260
"reposts",
274261
"sepolia",
@@ -278,6 +265,8 @@
278265
"txid",
279266
"waitlist",
280267
"WARPCAST",
268+
"webm",
281269
"youtube"
282270
]
283-
}
271+
}
272+

packages/mask/background/database/persona/type.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ export interface PersonaRecord {
139139
createdAt: Date
140140
updatedAt: Date
141141
hasLogout?: boolean
142+
fireflyToken?: string
142143
/**
143144
* create a dummy persona which should hide to the user until
144145
* connected at least one website

packages/mask/content-script/components/InjectedComponents/SetupGuide/AccountConnectStatus.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Trans } from '@lingui/react/macro'
12
import { Icons } from '@masknet/icons'
23
import { BindingDialog, LoadingStatus, SOCIAL_MEDIA_ROUND_ICON_MAPPING, type BindingDialogProps } from '@masknet/shared'
34
import { Sniffings, SOCIAL_MEDIA_NAME } from '@masknet/shared-base'
@@ -6,7 +7,6 @@ import { Box, Button, Typography } from '@mui/material'
67
import { memo } from 'react'
78
import { activatedSiteAdaptorUI } from '../../../site-adaptor-infra/ui.js'
89
import { SetupGuideContext } from './SetupGuideContext.js'
9-
import { Trans } from '@lingui/react/macro'
1010

1111
const useStyles = makeStyles()((theme) => {
1212
return {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import { EMPTY_LIST, EnhanceableSite, PopupRoutes } from '@masknet/shared-base'
55
import { Telemetry } from '@masknet/web3-telemetry'
66
import { EventType } from '@masknet/web3-telemetry/types'
77
import { memo, useCallback } from 'react'
8+
import { useNavigate } from 'react-router-dom'
89
import { requestPermissionFromExtensionPage } from '../../../shared-ui/index.js'
910
import { EventMap } from '../../../shared/definitions/event.js'
1011
import { ConnectSocialAccounts } from '../../components/ConnectSocialAccounts/index.js'
1112
import { ActionModal, type ActionModalBaseProps } from '../../components/index.js'
1213
import { useSupportSocialNetworks } from '../../hooks/index.js'
13-
import { useNavigate } from 'react-router-dom'
1414

1515
export const ConnectSocialAccountModal = memo<ActionModalBaseProps>(function ConnectSocialAccountModal(props) {
1616
const { data: definedSocialNetworks = EMPTY_LIST } = useSupportSocialNetworks()
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {
2+
FarcasterSession,
3+
FIREFLY_ROOT_URL,
4+
fireflySessionHolder,
5+
patchFarcasterSessionRequired,
6+
resolveFireflyResponseData,
7+
} from '@masknet/web3-providers'
8+
import { SessionType, type FireflyConfigAPI, type Session } from '@masknet/web3-providers/types'
9+
import urlcat from 'urlcat'
10+
11+
async function bindFarcasterSessionToFirefly(session: FarcasterSession, signal?: AbortSignal) {
12+
const isGrantByPermission = FarcasterSession.isGrantByPermission(session, true)
13+
const isRelayService = FarcasterSession.isRelayService(session)
14+
15+
if (!isGrantByPermission && !isRelayService)
16+
throw new Error(
17+
'[bindFarcasterSessionToFirefly] Only grant-by-permission or relay service sessions are allowed.',
18+
)
19+
20+
const response = await fireflySessionHolder.fetch<FireflyConfigAPI.BindResponse>(
21+
urlcat(FIREFLY_ROOT_URL, '/v3/user/bindFarcaster'),
22+
{
23+
method: 'POST',
24+
body: JSON.stringify({
25+
token: isGrantByPermission ? session.signerRequestToken : undefined,
26+
channelToken: isRelayService ? session.channelToken : undefined,
27+
isForce: false,
28+
}),
29+
signal,
30+
},
31+
)
32+
33+
if (response.error?.some((x) => x.includes('Farcaster binding timed out'))) {
34+
throw new Error('Bind Farcaster account to Firefly timeout.')
35+
}
36+
37+
// If the farcaster is already bound to another account, throw an error.
38+
if (
39+
isRelayService &&
40+
response.error?.some((x) => x.includes('This farcaster already bound to the other account'))
41+
) {
42+
throw new Error('This Farcaster account has already bound to another Firefly account.')
43+
}
44+
45+
const data = resolveFireflyResponseData(response)
46+
patchFarcasterSessionRequired(session, data.fid, data.farcaster_signer_private_key)
47+
return data
48+
}
49+
50+
/**
51+
* Bind a lens or farcaster session to the currently logged-in Firefly session.
52+
* @param session
53+
* @param signal
54+
* @returns
55+
*/
56+
export async function bindFireflySession(session: Session, signal?: AbortSignal) {
57+
// Ensure that the Firefly session is resumed before calling this function.
58+
fireflySessionHolder.assertSession()
59+
if (session.type === SessionType.Farcaster) {
60+
return bindFarcasterSessionToFirefly(session as FarcasterSession, signal)
61+
} else if (session.type === SessionType.Firefly) {
62+
throw new Error('Not allowed')
63+
}
64+
throw new Error(`Unknown session type: ${session.type}`)
65+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { fireflySessionHolder } from '@masknet/web3-providers'
2+
import type { Session } from '@masknet/web3-providers/types'
3+
import { bindFireflySession } from './bindFireflySession'
4+
import { restoreFireflySession } from './restoreFireflySession'
5+
6+
export async function bindOrRestoreFireflySession(session: Session, signal?: AbortSignal) {
7+
try {
8+
if (fireflySessionHolder.session) {
9+
await bindFireflySession(session, signal)
10+
11+
// this will return the existing session
12+
return fireflySessionHolder.assertSession(
13+
'[bindOrRestoreFireflySession] Failed to bind farcaster session with firefly.',
14+
)
15+
} else {
16+
throw new Error('[bindOrRestoreFireflySession] Firefly session is not available.')
17+
}
18+
} catch (error) {
19+
// this will create a new session
20+
return restoreFireflySession(session, signal)
21+
}
22+
}

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import type { Account } from '@masknet/shared-base'
21
import { fetchJSON } from '@masknet/web3-providers/helpers'
3-
import { FarcasterSession } from '@masknet/web3-providers'
2+
import { FarcasterSession, getFarcasterProfileById, type FireflyAccount } from '@masknet/web3-providers'
43
import urlcat from 'urlcat'
4+
import { bindOrRestoreFireflySession } from './bindOrRestoreFireflySession'
55

66
const FARCASTER_REPLY_URL = 'https://relay.farcaster.xyz'
77
const NOT_DEPEND_SECRET = '[TO_BE_REPLACED_LATER]'
@@ -20,9 +20,13 @@ async function createSession(signal?: AbortSignal) {
2020
const url = urlcat(FARCASTER_REPLY_URL, '/v1/channel')
2121
const response = await fetchJSON<FarcasterReplyResponse>(url, {
2222
method: 'POST',
23+
headers: {
24+
'Content-Type': 'application/json',
25+
},
2326
body: JSON.stringify({
24-
siteUri: 'https://www.mask.io',
25-
domain: 'www.mask.io',
27+
// cspell: disable-next-line
28+
siweUri: 'https://firefly.social',
29+
domain: 'firefly.social',
2630
}),
2731
signal,
2832
})
@@ -51,6 +55,7 @@ export async function createAccountByRelayService(callback?: (url: string) => vo
5155

5256
// polling for the session to be ready
5357
const fireflySession = await bindOrRestoreFireflySession(session, signal)
58+
console.log('fireflySession', fireflySession)
5459

5560
// profile id is available after the session is ready
5661
const profile = await getFarcasterProfileById(session.profileId)
@@ -60,5 +65,5 @@ export async function createAccountByRelayService(callback?: (url: string) => vo
6065
session,
6166
profile,
6267
fireflySession,
63-
} satisfies Account
68+
} satisfies FireflyAccount
6469
}
Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,106 @@
1-
import { useLingui } from '@lingui/react/macro'
1+
import { Trans, useLingui } from '@lingui/react/macro'
22
import { PopupHomeTabType } from '@masknet/shared'
3-
import { QRCode } from 'react-qrcode-logo'
4-
import { PopupRoutes } from '@masknet/shared-base'
5-
import { makeStyles } from '@masknet/theme'
3+
import {
4+
AbortError,
5+
FarcasterPatchSignerError,
6+
FireflyAlreadyBoundError,
7+
FireflyBindTimeoutError,
8+
PopupRoutes,
9+
TimeoutError,
10+
} from '@masknet/shared-base'
11+
import { LoadingBase, makeStyles, usePopupCustomSnackbar } from '@masknet/theme'
12+
import { addAccount, type AccountOptions, type FireflyAccount } from '@masknet/web3-providers'
13+
import { Social } from '@masknet/web3-providers/types'
614
import { Box } from '@mui/material'
7-
import { memo, useCallback } from 'react'
15+
import { memo, useCallback, useState } from 'react'
16+
import { QRCode } from 'react-qrcode-logo'
817
import { useNavigate } from 'react-router-dom'
918
import urlcat from 'urlcat'
1019
import { useTitle } from '../../../hooks/index.js'
20+
import { useAsyncFn, useMount } from 'react-use'
21+
import { createAccountByRelayService } from './createAccountByRelayService.js'
1122

1223
const useStyles = makeStyles()({
13-
container: {},
24+
container: {
25+
display: 'flex',
26+
justifyContent: 'center',
27+
alignItems: 'center',
28+
position: 'relative',
29+
},
30+
loading: {
31+
backgroundColor: 'rgba(255,255,255,0.5)',
32+
position: 'absolute',
33+
inset: 0,
34+
display: 'flex',
35+
justifyContent: 'center',
36+
alignItems: 'center',
37+
},
1438
})
1539

40+
function useLogin() {
41+
const { showSnackbar } = usePopupCustomSnackbar()
42+
return useAsyncFn(
43+
async function login(createAccount: () => Promise<FireflyAccount>, options?: Omit<AccountOptions, 'source'>) {
44+
try {
45+
const account = await createAccount()
46+
47+
const done = await addAccount(account, options)
48+
console.log('created account', account)
49+
if (done) showSnackbar(<Trans>Your {Social.Source.Farcaster} account is now connected.</Trans>)
50+
} catch (error) {
51+
// skip if the error is abort error
52+
if (AbortError.is(error)) return
53+
54+
// if login timed out, let the user refresh the QR code
55+
if (error instanceof TimeoutError || error instanceof FireflyBindTimeoutError) {
56+
showSnackbar(<Trans>This QR code is longer valid. Please scan a new one to continue.</Trans>)
57+
return
58+
}
59+
60+
// failed to patch the signer
61+
if (error instanceof FarcasterPatchSignerError) throw error
62+
63+
// if any error occurs, close the modal
64+
// by this we don't need to do error handling in UI part.
65+
// if the account is already bound to another account, show a warning message
66+
if (error instanceof FireflyAlreadyBoundError) {
67+
showSnackbar(
68+
<Trans>
69+
The account you are trying to log in with is already linked to a different Firefly account.
70+
</Trans>,
71+
)
72+
return
73+
}
74+
75+
throw error
76+
}
77+
},
78+
[showSnackbar],
79+
)
80+
}
81+
1682
export const Component = memo(function ConnectFireflyPage() {
1783
const { t } = useLingui()
1884
const { classes } = useStyles()
85+
const [url, setUrl] = useState('')
1986

2087
const navigate = useNavigate()
88+
const [{ loading }, login] = useLogin()
89+
90+
useMount(async () => {
91+
login(async () => {
92+
try {
93+
const account = await createAccountByRelayService((url) => {
94+
setUrl(url)
95+
})
96+
console.log('account', account)
97+
return account
98+
} catch (err) {
99+
console.log('error', err)
100+
throw err
101+
}
102+
})
103+
})
21104

22105
const handleBack = useCallback(() => {
23106
navigate(urlcat(PopupRoutes.Personas, { tab: PopupHomeTabType.ConnectedWallets }), {
@@ -29,7 +112,12 @@ export const Component = memo(function ConnectFireflyPage() {
29112

30113
return (
31114
<Box className={classes.container}>
32-
<QRCode value="hello" ecLevel="L" size={220} quietZone={16} eyeRadius={100} qrStyle="dots" />
115+
<QRCode value={url} ecLevel="L" size={220} quietZone={16} eyeRadius={100} qrStyle="dots" />
116+
{loading ?
117+
<div className={classes.loading}>
118+
<LoadingBase size={30} />
119+
</div>
120+
: null}
33121
</Box>
34122
)
35123
})

0 commit comments

Comments
 (0)