diff --git a/packages/icons/brands/TwitterX.dark.svg b/packages/icons/brands/TwitterX.dark.svg
deleted file mode 100644
index da775cd209ef..000000000000
--- a/packages/icons/brands/TwitterX.dark.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/packages/icons/brands/TwitterX.light.svg b/packages/icons/brands/TwitterX.light.svg
deleted file mode 100644
index 880fe0f4a28c..000000000000
--- a/packages/icons/brands/TwitterX.light.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/packages/icons/brands/TwitterX.svg b/packages/icons/brands/TwitterX.svg
new file mode 100644
index 000000000000..39c0f7392f17
--- /dev/null
+++ b/packages/icons/brands/TwitterX.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/icons/icon-generated-as-jsx.js b/packages/icons/icon-generated-as-jsx.js
index 5ae81dd3bad5..1cd23e7ba8c3 100644
--- a/packages/icons/icon-generated-as-jsx.js
+++ b/packages/icons/icon-generated-as-jsx.js
@@ -605,12 +605,17 @@ export const TwitterRoundGray = /*#__PURE__*/ __createIcon('TwitterRoundGray', [
])
export const TwitterX = /*#__PURE__*/ __createIcon('TwitterX', [
{
- c: ['dark'],
- u: () => new URL('./brands/TwitterX.dark.svg', import.meta.url).href,
- },
- {
- c: ['light'],
- u: () => new URL('./brands/TwitterX.light.svg', import.meta.url).href,
+ j: () =>
+ /*#__PURE__*/ _jsx('svg', {
+ xmlns: 'http://www.w3.org/2000/svg',
+ fill: 'none',
+ viewBox: '0 0 24 24',
+ children: /*#__PURE__*/ _jsx('path', {
+ fill: 'currentColor',
+ d: 'M13.882 10.46 21.313 2h-1.76l-6.456 7.344L7.944 2H2l7.793 11.107L2 21.977h1.76l6.814-7.757 5.443 7.757h5.944L13.88 10.46Zm-2.413 2.744-.79-1.107L4.395 3.3H7.1l5.071 7.103.788 1.106 6.592 9.232h-2.705l-5.378-7.537Z',
+ }),
+ }),
+ s: true,
},
])
export const TwitterXRound = /*#__PURE__*/ __createIcon('TwitterXRound', [
diff --git a/packages/icons/icon-generated-as-url.js b/packages/icons/icon-generated-as-url.js
index 1abc6a3f97bc..866443afe0e2 100644
--- a/packages/icons/icon-generated-as-url.js
+++ b/packages/icons/icon-generated-as-url.js
@@ -108,8 +108,7 @@ export function twitter_colored_url() { return new URL("./brands/TwitterColored.
export function twitter_other_colored_url() { return new URL("./brands/TwitterOtherColored.svg", import.meta.url).href }
export function twitter_round_url() { return new URL("./brands/TwitterRound.svg", import.meta.url).href }
export function twitter_round_gray_url() { return new URL("./brands/TwitterRoundGray.svg", import.meta.url).href }
-export function twitter_x_dark_url() { return new URL("./brands/TwitterX.dark.svg", import.meta.url).href }
-export function twitter_x_light_url() { return new URL("./brands/TwitterX.light.svg", import.meta.url).href }
+export function twitter_x_url() { return new URL("./brands/TwitterX.svg", import.meta.url).href }
export function twitter_x_round_dark_url() { return new URL("./brands/TwitterXRound.dark.svg", import.meta.url).href }
export function twitter_x_round_light_url() { return new URL("./brands/TwitterXRound.light.svg", import.meta.url).href }
export function uniswap_url() { return new URL("./brands/Uniswap.svg", import.meta.url).href }
diff --git a/packages/mask/background/services/helper/oauth-x.ts b/packages/mask/background/services/helper/oauth-x.ts
index 59ee9f847b53..d970606ed00d 100644
--- a/packages/mask/background/services/helper/oauth-x.ts
+++ b/packages/mask/background/services/helper/oauth-x.ts
@@ -1,5 +1,6 @@
import { timeout } from '@masknet/kit'
import { requestExtensionPermissionFromContentScript } from './request-permission.js'
+import { XOAuthRequestOrigins } from '../../../shared/definitions/extension.js'
/** Modified from https://github.com/ddo/oauth-1.0a/blob/master/oauth-1.0a.js */
class OAuth {
@@ -184,15 +185,7 @@ const client = new OAuth(process.env.FIREFLY_X_CLIENT_ID, process.env.FIREFLY_X_
let pendingOAuth: PromiseWithResolvers<{ oauth_verifier: string; oauth_token: string }> | undefined
export async function requestXOAuthToken() {
await requestExtensionPermissionFromContentScript({
- origins: [
- // In order to send API request without CORS limit
- 'https://api.twitter.com/*',
- // In order to run content script on it
- 'https://firefly.social/api/mask/delegate-x-token',
- 'https://firefly.social/api/auth/callback/twitter',
- 'https://canary.firefly.social/api/mask/delegate-x-token',
- 'https://canary.firefly.social/api/auth/callback/twitter',
- ],
+ origins: XOAuthRequestOrigins,
})
await Promise.all([
fetch('https://firefly.social/api/mask/delegate-x-token'),
diff --git a/packages/mask/dashboard/pages/CreateMaskWallet/FireflyWallet/index.tsx b/packages/mask/dashboard/pages/CreateMaskWallet/FireflyWallet/index.tsx
new file mode 100644
index 000000000000..8417ee78b084
--- /dev/null
+++ b/packages/mask/dashboard/pages/CreateMaskWallet/FireflyWallet/index.tsx
@@ -0,0 +1,221 @@
+import Services from '#services'
+import { Trans } from '@lingui/react/macro'
+import { Icons } from '@masknet/icons'
+import { PopupRoutes } from '@masknet/shared-base'
+import { makeStyles } from '@masknet/theme'
+import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from '@mui/material'
+import { memo, useState } from 'react'
+import { useAsyncFn, useAsyncRetry } from 'react-use'
+import { PrimaryButton } from '../../../components/PrimaryButton/index.js'
+import { SetupFrameController } from '../../../components/SetupFrame/index.js'
+import { XOAuthRequestOrigins } from '../../../../shared/definitions/extension.js'
+
+const useStyles = makeStyles()((theme) => ({
+ container: {
+ width: '100%',
+ display: 'flex',
+ justifyContent: 'center',
+ flexDirection: 'column',
+ flex: 1,
+ },
+ title: {
+ fontSize: 30,
+ margin: '12px 0',
+ lineHeight: '120%',
+ color: theme.palette.maskColor.main,
+ },
+ tips: {
+ fontSize: 14,
+ lineHeight: '18px',
+ color: theme.palette.maskColor.second,
+ },
+ bold: {
+ fontWeight: 700,
+ },
+ notes: {
+ display: 'flex',
+ padding: theme.spacing(3, 2),
+ alignItems: 'center',
+ alignContent: 'stretch',
+ borderRadius: 12,
+ marginTop: theme.spacing(3),
+ background:
+ theme.palette.mode === 'dark' ?
+ 'linear-gradient(180deg, rgba(255, 255, 255, 0.10) 0%, rgba(255, 255, 255, 0.00) 100%)'
+ : 'linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, #FFF 100%), linear-gradient(90deg, rgba(98, 126, 234, 0.20) 0%, rgba(59, 153, 252, 0.20) 100%)',
+ },
+ fireflyLogo: {
+ width: 120,
+ height: 120,
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ list: {
+ listStyle: 'none',
+ color: theme.palette.maskColor.main,
+ fontSize: '13px',
+ lineHeight: '18px',
+ fontWeight: 400,
+ paddingLeft: 16,
+ margin: 0,
+ '& li': {
+ marginBottom: 12,
+ listStyle: 'disc',
+ },
+ },
+ dialog: {
+ width: 600,
+ boxSizing: 'border-box',
+ padding: 24,
+ borderRadius: 12,
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 24,
+ },
+ dialogTitle: {
+ color: theme.palette.maskColor.main,
+ fontSize: 18,
+ fontWeight: 700,
+ lineHeight: '22px',
+ padding: 0,
+ },
+ dialogContent: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 12,
+ padding: 0,
+ },
+ permissions: {
+ backgroundColor: theme.palette.maskColor.bg,
+ padding: 12,
+ borderRadius: 8,
+ height: 212,
+ boxSizing: 'border-box',
+ minHeight: 0,
+ flexGrow: 1,
+ overflow: 'auto',
+ },
+ dialogActions: {
+ display: 'flex',
+ justifyContent: 'center',
+ gap: 12,
+ padding: 0,
+ },
+ actionButton: {
+ minWidth: 110,
+ '&&': {
+ marginLeft: 0,
+ },
+ },
+}))
+
+export const Component = memo(function CreateWalletForm() {
+ const { classes, cx } = useStyles()
+ const [open, setOpen] = useState(false)
+
+ const { retry, value: hasPermission } = useAsyncRetry(() => {
+ const hasPermission = browser.permissions.contains({
+ origins: XOAuthRequestOrigins,
+ })
+ setOpen(!hasPermission)
+ return hasPermission
+ }, [])
+
+ const [{ loading }, request] = useAsyncFn(async () => {
+ if (!hasPermission) {
+ setOpen(true)
+ return
+ }
+ try {
+ const data = await Services.Helper.loginFireflyViaTwitter()
+ if (!data) return
+ await Services.Helper.openPopupWindow(PopupRoutes.CreateWallet, {
+ creatingFireflyWallet: true,
+ })
+ } catch (err) {
+ console.error('Failed to login firefly', err)
+ }
+ }, [hasPermission])
+
+ return (
+
+
+ Create a Firefly.social wallet
+
+
+ Create a Firefly.social wallet using an X account
+
+
+
+
+
+
+ -
+ Firefly.social wallet connects your Web3 identity to the entire Mask Network and firefly.social
+ ecosystem.
+
+ -
+ With a single wallet, you can like, post, collect, and transfer across multiple Web3 social
+ platforms — all in one seamless flow.
+
+ -
+ Log in securely in seconds with your X account and unify your EVM, Solana, and other chain
+ identities effortlessly.
+
+ -
+ Firefly.social wallet isn’t just a wallet — it’s your gateway to social, identity, and ownership
+ in Web3.
+
+
+
+
+ }
+ loading={loading}
+ onClick={request}>
+ Continue
+
+
+
+
+ )
+})
diff --git a/packages/mask/dashboard/pages/CreateMaskWallet/index.tsx b/packages/mask/dashboard/pages/CreateMaskWallet/index.tsx
index 1ee979e80250..6ae5ee6cd545 100644
--- a/packages/mask/dashboard/pages/CreateMaskWallet/index.tsx
+++ b/packages/mask/dashboard/pages/CreateMaskWallet/index.tsx
@@ -11,6 +11,7 @@ export const walletRoutes: RouteObject[] = [
{ path: r(DashboardRoutes.SignUpMaskWalletOnboarding), lazy: () => import('./Onboarding/index.js') },
{ path: r(DashboardRoutes.RecoveryMaskWallet), lazy: () => import('./Recovery/index.js') },
{ path: r(DashboardRoutes.AddDeriveWallet), lazy: () => import('./AddDeriveWallet/index.js') },
+ { path: r(DashboardRoutes.CreateFireflyWallet), lazy: () => import('./FireflyWallet/index.js') },
]
export function WalletFrame() {
diff --git a/packages/mask/dashboard/pages/SetupPersona/Onboarding/index.tsx b/packages/mask/dashboard/pages/SetupPersona/Onboarding/index.tsx
index 16a0e9b7c581..54c9d8f9f57f 100644
--- a/packages/mask/dashboard/pages/SetupPersona/Onboarding/index.tsx
+++ b/packages/mask/dashboard/pages/SetupPersona/Onboarding/index.tsx
@@ -169,13 +169,7 @@ export const Component = memo(function Onboarding() {
- }>
+ startIcon={}>
Experience in X
{!isCreate && count && !isZero(count) ?
diff --git a/packages/mask/dashboard/pages/SetupPersona/PermissionOnboarding/index.tsx b/packages/mask/dashboard/pages/SetupPersona/PermissionOnboarding/index.tsx
index 4600318c071d..51bcefe41c64 100644
--- a/packages/mask/dashboard/pages/SetupPersona/PermissionOnboarding/index.tsx
+++ b/packages/mask/dashboard/pages/SetupPersona/PermissionOnboarding/index.tsx
@@ -139,13 +139,7 @@ export const Component = memo(function Onboarding() {
- }>
+ startIcon={}>
Experience in X
diff --git a/packages/mask/dashboard/pages/routes.tsx b/packages/mask/dashboard/pages/routes.tsx
index efc9c4d02ed4..f269238f2141 100644
--- a/packages/mask/dashboard/pages/routes.tsx
+++ b/packages/mask/dashboard/pages/routes.tsx
@@ -10,6 +10,7 @@ const routes: RouteObject[] = [
{ path: DashboardRoutes.Setup, element: , children: personaRoutes },
{ path: DashboardRoutes.SignUp, element: , children: signUpRoutes },
{ path: DashboardRoutes.CreateMaskWallet, element: , children: walletRoutes },
+ { path: DashboardRoutes.CreateFireflyWallet, element: , children: walletRoutes },
]
const rootElement = (
<>
diff --git a/packages/mask/popups/pages/RequestPermission/index.tsx b/packages/mask/popups/pages/RequestPermission/index.tsx
index 7cda2e19e944..1459eb02da8b 100644
--- a/packages/mask/popups/pages/RequestPermission/index.tsx
+++ b/packages/mask/popups/pages/RequestPermission/index.tsx
@@ -2,15 +2,10 @@ import { Box } from '@mui/material'
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'
import { useAsyncRetry } from 'react-use'
-import { RequestPermission } from './RequestPermission.js'
import type { Manifest } from 'webextension-polyfill'
+import { CanRequestDynamically } from '../../../shared/definitions/extension.js'
+import { RequestPermission } from './RequestPermission.js'
-const CanRequestDynamically: readonly Manifest.OptionalPermission[] = [
- 'clipboardRead',
- 'clipboardWrite',
- 'notifications',
- 'webRequestBlocking',
-]
function canRequestDynamically(x: string): x is Manifest.OptionalPermission {
return (CanRequestDynamically as string[]).includes(x)
}
diff --git a/packages/mask/popups/pages/Wallet/components/ImportCreateWallet/index.tsx b/packages/mask/popups/pages/Wallet/components/ImportCreateWallet/index.tsx
index 56baf8ecad92..e5e149e80597 100644
--- a/packages/mask/popups/pages/Wallet/components/ImportCreateWallet/index.tsx
+++ b/packages/mask/popups/pages/Wallet/components/ImportCreateWallet/index.tsx
@@ -1,14 +1,19 @@
+import Services from '#services'
+import { Trans } from '@lingui/react/macro'
import { Icons } from '@masknet/icons'
-import { DashboardRoutes } from '@masknet/shared-base'
+import { useWallets } from '@privy-io/react-auth'
+import { timeout } from '@masknet/kit'
+import { LoadingStatus } from '@masknet/shared'
+import { DashboardRoutes, type NetworkPluginID, PopupRoutes } from '@masknet/shared-base'
import { ActionButton, makeStyles } from '@masknet/theme'
import { alpha, Box, Typography, type BoxProps } from '@mui/material'
-import { memo } from 'react'
-import { useAsyncFn } from 'react-use'
-import Services from '#services'
+import { memo, useCallback } from 'react'
+import { useNavigate, useSearchParams } from 'react-router-dom'
+import { useAsync, useAsyncFn } from 'react-use'
import urlcat from 'urlcat'
-import { Trans } from '@lingui/react/macro'
-import { LoadingStatus } from '@masknet/shared'
-import { timeout } from '@masknet/kit'
+import { useChainContext } from '@masknet/web3-hooks-base'
+import { EVMWeb3 } from '@masknet/web3-providers'
+import { ProviderType } from '@masknet/web3-shared-evm'
const useStyles = makeStyles()((theme) => {
return {
@@ -64,6 +69,10 @@ interface Props extends BoxProps {
}
async function loginFirefly() {
+ try {
+ await Services.Helper.loginFireflyViaTwitter()
+ return
+ } catch {}
const result = await Services.Helper.requestXOAuthToken()
if (result) {
await Services.Helper.loginFireflyViaTwitter()
@@ -71,6 +80,9 @@ async function loginFirefly() {
}
export const ImportCreateWallet = memo(function ImportCreateWallet({ onChoose, ...props }) {
const { classes, cx, theme } = useStyles()
+ const [params] = useSearchParams()
+ const { chainId } = useChainContext()
+ const navigate = useNavigate()
const [, handleChoose] = useAsyncFn(
async (route: DashboardRoutes) => {
const hasPassword = await Services.Wallet.hasPassword()
@@ -90,18 +102,48 @@ export const ImportCreateWallet = memo(function ImportCreateWallet({ onCh
const [{ loading: creatingPrivy, error }, createPrivyWallet] = useAsyncFn(async () => {
return timeout(loginFirefly(), 3 * 60 * 1000, timeoutMessage)
}, [])
+
+ const isCreatingFireflyWallet = !!params.get('creatingFireflyWallet')
+ const { wallets } = useWallets()
+ const selectPrivyWallet = useCallback(async () => {
+ if (!wallets.length) return
+ if (wallets.length > 1) {
+ navigate(PopupRoutes.SelectWallet)
+ return
+ }
+ await EVMWeb3.connect({
+ account: wallets[0].address,
+ chainId,
+ providerType: ProviderType.MaskWallet,
+ })
+ navigate(PopupRoutes.Wallet)
+ }, [wallets, chainId])
+
+ useAsync(async () => {
+ if (!isCreatingFireflyWallet) return
+ await createPrivyWallet()
+ await selectPrivyWallet()
+ }, [isCreatingFireflyWallet, selectPrivyWallet])
+
const oauthTimeout = error?.message === timeoutMessage
return (
-
+ {
+ await browser.tabs.create({
+ active: true,
+ url: browser.runtime.getURL(`/dashboard.html#${DashboardRoutes.CreateFireflyWallet}`),
+ })
+ }}>
-
+
diff --git a/packages/mask/shared/definitions/extension.ts b/packages/mask/shared/definitions/extension.ts
new file mode 100644
index 000000000000..6ba2e933d683
--- /dev/null
+++ b/packages/mask/shared/definitions/extension.ts
@@ -0,0 +1,18 @@
+import type { Manifest } from 'webextension-polyfill'
+
+export const CanRequestDynamically: Manifest.OptionalPermission[] = [
+ 'clipboardRead',
+ 'clipboardWrite',
+ 'notifications',
+ 'webRequestBlocking',
+]
+
+export const XOAuthRequestOrigins: string[] = [
+ // In order to send API request without CORS limit
+ 'https://api.twitter.com/*',
+ // In order to run content script on it
+ 'https://firefly.social/api/mask/delegate-x-token',
+ 'https://firefly.social/api/auth/callback/twitter',
+ 'https://canary.firefly.social/api/mask/delegate-x-token',
+ 'https://canary.firefly.social/api/auth/callback/twitter',
+]
diff --git a/packages/shared-base/src/types/Routes.ts b/packages/shared-base/src/types/Routes.ts
index 8195325eb9d1..00b700f235d6 100644
--- a/packages/shared-base/src/types/Routes.ts
+++ b/packages/shared-base/src/types/Routes.ts
@@ -25,6 +25,7 @@ export enum DashboardRoutes {
Personas = '/personas',
CreateMaskWallet = '/create-mask-wallet',
CreateMaskWalletForm = '/create-mask-wallet/form',
+ CreateFireflyWallet = '/create-mask-wallet/firefly',
RecoveryMaskWallet = '/create-mask-wallet/recovery',
CreateMaskWalletMnemonic = '/create-mask-wallet/mnemonic',
AddDeriveWallet = '/create-mask-wallet/add-derive-wallet',
@@ -121,4 +122,5 @@ export interface PopupRoutesParamsMap {
from?: string | null
}
[PopupRoutes.Contacts]: { selectedToken: string | undefined }
+ [PopupRoutes.CreateWallet]: { creatingFireflyWallet?: boolean }
}