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 + +
+
+ +
+ +
+ + } + loading={loading} + onClick={request}> + Continue + + + + + Mask needs the following permissions + + + + Sites + +
+ {XOAuthRequestOrigins.map((origin) => ( + + {origin} + + ))} +
+
+ + + + +
+
+ ) +}) 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 } }