diff --git a/src/background/Wallet/GlobalPreferences.ts b/src/background/Wallet/GlobalPreferences.ts index 022c1d530..8d27ed5dc 100644 --- a/src/background/Wallet/GlobalPreferences.ts +++ b/src/background/Wallet/GlobalPreferences.ts @@ -27,6 +27,7 @@ export interface State { providerInjection?: ProviderInjection; autoLockTimeout?: number | 'none'; walletNameFlags?: Record; + enableNewTabOverride?: boolean | null; } export function getWalletNameFlagsChange(state: State, prevState: State) { @@ -59,6 +60,7 @@ export class GlobalPreferences extends PersistentStore { providerInjection: {}, walletNameFlags: {}, autoLockTimeout: HALF_DAY, + enableNewTabOverride: null, }; private async fetchDefaultWalletNameFlags() { diff --git a/src/background/index.ts b/src/background/index.ts index eef0b65b8..eeae011d0 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -6,6 +6,7 @@ import { SessionCacheService } from 'src/background/resource/sessionCacheService import { openOnboarding } from 'src/shared/openOnboarding'; import { userLifecycleStore } from 'src/shared/analytics/shared/UserLifecycle'; import { UrlContextParam } from 'src/shared/types/UrlContext'; +import { openNewTabOverride } from 'src/shared/openNewTabOverride'; import { initialize } from './initialize'; import { PortRegistry } from './messaging/PortRegistry'; import { createWalletMessageHandler } from './messaging/port-message-handlers/createWalletMessageHandler'; @@ -20,6 +21,7 @@ import { emitter } from './events'; import * as userActivity from './user-activity'; import { ContentScriptManager } from './ContentScriptManager'; import { TransactionService } from './transactions/TransactionService'; +import { globalPreferences } from './Wallet/GlobalPreferences'; Object.assign(globalThis, { ethers }); @@ -203,3 +205,28 @@ browser.runtime.onInstalled.addListener(({ reason }) => { openOnboarding(); } }); + +function overrideTabListener( + tabId: number, + _changes: unknown, + tab: browser.Tabs.Tab +) { + if (tab.url?.endsWith('://newtab/')) { + openNewTabOverride(tabId); + } +} + +function overrideNewTab(override: boolean) { + if (override) { + browser.tabs.onUpdated.addListener(overrideTabListener); + } else { + browser.tabs.onUpdated.removeListener(overrideTabListener); + } +} + +globalPreferences.getPreferences().then((preferences) => { + overrideNewTab(Boolean(preferences.enableNewTabOverride)); +}); +globalPreferences.on('change', (state) => { + overrideNewTab(Boolean(state.enableNewTabOverride)); +}); diff --git a/src/manifest.json b/src/manifest.json index c4111e0e2..64fbf1891 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -45,7 +45,8 @@ "alarms", "scripting", "storage", - "unlimitedStorage" + "unlimitedStorage", + "tabs" ], "host_permissions": ["http://*/*", "https://*/*"] } diff --git a/src/modules/zerion-api/requests/explore-info.ts b/src/modules/zerion-api/requests/explore-info.ts new file mode 100644 index 000000000..6eb2d38ef --- /dev/null +++ b/src/modules/zerion-api/requests/explore-info.ts @@ -0,0 +1,132 @@ +export interface Payload { + addresses: string[]; + currency: string; +} + +interface Collection { + id: string; + name: string | null; + iconUrl: string | null; + chain: string | null; + slug: string; + description: string | null; + bannerImageUrl: string | null; + category: string | null; + paymentTokenSymbol: string | null; + marketplaceData: { + floorPrice: number | null; + nftsCount: number | null; + ownersCount: number | null; + oneDayVolume: number | null; + oneDayChange: number | null; + totalVolume: number | null; + } | null; +} + +export interface TrandingNFTs { + id: 'trending_nfts'; + collections: Collection[]; +} + +interface Dapp { + name: string; + iconUrl: string; + url: string; +} + +export interface FeaturedDapps { + id: 'featured_dapps'; + dapps: Dapp[]; +} + +interface Fungible { + id: string; + name: string; + symbol: string; + iconUrl: string | null; + meta: { + price: number | null; + marketCap: number | null; + relativeChange1d: number | null; + relativeChange30d: number | null; + relativeChange90d: number | null; + }; +} + +export interface TopMovers { + id: 'top_movers'; + fungibles: Fungible[]; +} + +export interface TopTokens { + id: 'top_tokens'; + fungibles: Fungible[]; +} + +interface Mint { + id: string; + title: string; + type: 'opened' | 'hidden' | 'completed'; + isExclusive: boolean; + reason: { + title: string; + subtitle: string; + type: + | 'allowlist' + | 'tokengate' + | 'influencer' + | 'trending' + | 'creator' + | 'minted-from-creator-before' + | 'trending-in-community'; + wallets: { + name: string; + iconUrl: string | null; + address: string; + premium: boolean; + }[]; + }; + description: string; + openedAt: string | null; + imageUrl: string; + action: string; + chain: string | null; + contract: string | null; +} + +export interface MintsForYou { + id: 'mints_for_you'; + mints: Mint[]; +} + +interface Wallet { + name: string; + iconUrl: string | null; + address: string; + premium: boolean; +} + +export interface PopularWallets { + id: 'popular_wallets'; + wallets: Wallet[]; +} + +interface Data { + sections: ({ + title: string; + order: number; + isSearchEnabled: boolean; + } & ( + | TrandingNFTs + | FeaturedDapps + | TopMovers + | TopTokens + | MintsForYou + | PopularWallets + ))[]; +} + +export interface Response { + data: Data; + errors?: { title: string; detail: string }[]; +} diff --git a/src/modules/zerion-api/zerion-api.ts b/src/modules/zerion-api/zerion-api.ts index 0ae02ce2e..0346a97c4 100644 --- a/src/modules/zerion-api/zerion-api.ts +++ b/src/modules/zerion-api/zerion-api.ts @@ -27,6 +27,10 @@ import type { Payload as GetGasPricesPayload, Response as GetGasPricesResponse, } from './requests/get-gas-prices'; +import type { + Payload as ExploreSectionsPayload, + Response as ExportSectionsResponse, +} from './requests/explore-info'; export type NetworksSource = 'mainnet' | 'testnet'; @@ -138,4 +142,16 @@ export class ZerionAPI { `/paymaster/check-eligibility/v1?${params}` ); } + + static getExploreSections(payload: ExploreSectionsPayload) { + return ky + .get(new URL('explore/get-sections/v1', ZERION_API_URL), { + searchParams: { + addresses: payload.addresses.join(','), + currency: payload.currency, + }, + headers: getZpiHeaders(), + }) + .json(); + } } diff --git a/src/shared/openNewTabOverride.ts b/src/shared/openNewTabOverride.ts new file mode 100644 index 000000000..f8dedae11 --- /dev/null +++ b/src/shared/openNewTabOverride.ts @@ -0,0 +1,15 @@ +import browser from 'webextension-polyfill'; +import { getPopupUrl } from './getPopupUrl'; +import { setUrlContext } from './setUrlContext'; + +export function openNewTabOverride(tabId: number) { + const popupUrl = getPopupUrl(); + setUrlContext(popupUrl.searchParams, { + appMode: 'newTab', + windowType: 'tab', + windowLayout: 'page', + }); + browser.tabs.update(tabId, { + url: popupUrl.toString(), + }); +} diff --git a/src/shared/types/UrlContext.ts b/src/shared/types/UrlContext.ts index fae3b15b0..7d287bb68 100644 --- a/src/shared/types/UrlContext.ts +++ b/src/shared/types/UrlContext.ts @@ -1,4 +1,4 @@ -export type AppMode = 'onboarding' | 'wallet'; +export type AppMode = 'onboarding' | 'wallet' | 'newTab'; export type WindowLayout = 'column' | 'page'; export type WindowType = 'popup' | 'tab' | 'dialog'; diff --git a/src/ui/App/App.tsx b/src/ui/App/App.tsx index 268773170..8f8485e6e 100644 --- a/src/ui/App/App.tsx +++ b/src/ui/App/App.tsx @@ -78,6 +78,7 @@ import { Onboarding } from '../features/onboarding'; import { RevealPrivateKey } from '../pages/RevealPrivateKey'; import { urlContext } from '../../shared/UrlContext'; import { BackupPage } from '../pages/Backup/Backup'; +import { NewTab } from '../NewTab'; import { RouteRestoration, registerPersistentRoute } from './RouteRestoration'; const isProd = process.env.NODE_ENV === 'production'; @@ -445,6 +446,7 @@ export interface AppProps { export function App({ initialView, inspect }: AppProps) { const isOnboardingMode = urlContext.appMode === 'onboarding'; const isPageLayout = urlContext.windowLayout === 'page'; + const isNewTabView = urlContext.appMode === 'newTab'; const bodyClassList = useMemo(() => { const result = []; @@ -495,12 +497,14 @@ export function App({ initialView, inspect }: AppProps) { ) : null} - {!isOnboardingView && !isPageLayout ? ( + {!isOnboardingView && !isPageLayout && !isNewTabView ? ( // Render above so that it doesn't flicker ) : null} - {isOnboardingView ? ( + {isNewTabView ? ( + + ) : isOnboardingView ? ( ) : isPageLayout ? ( diff --git a/src/ui/NewTab/NewTab.tsx b/src/ui/NewTab/NewTab.tsx new file mode 100644 index 000000000..8901ee096 --- /dev/null +++ b/src/ui/NewTab/NewTab.tsx @@ -0,0 +1,500 @@ +import React, { useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { ZerionAPI } from 'src/modules/zerion-api/zerion-api'; +import { useAddressPortfolioDecomposition } from 'defi-sdk'; +import { + formatCurrencyToParts, + formatCurrencyValue, +} from 'src/shared/units/formatCurrencyValue'; +import { formatPercent } from 'src/shared/units/formatPercent/formatPercent'; +import type { ExternallyOwnedAccount } from 'src/shared/types/ExternallyOwnedAccount'; +import type { + MintsForYou as MintForYouType, + TopMovers as TopMoversType, + FeaturedDapps as FeaturedDappsType, + PopularWallets as PopularWalletsType, + TopTokens as TopTokensType, +} from 'src/modules/zerion-api/requests/explore-info'; +import { UIText } from '../ui-kit/UIText'; +import { useWalletAddresses } from '../pages/Networks/shared/useWalletAddresses'; +import { VerifyUser } from '../components/VerifyUser'; +import { HStack } from '../ui-kit/HStack'; +import { Frame } from '../ui-kit/Frame'; +import { VStack } from '../ui-kit/VStack'; +import { NeutralDecimals } from '../ui-kit/NeutralDecimals'; +import { minus, NBSP, noValueDash } from '../shared/typography'; +import { walletPort } from '../shared/channels'; +import { Media } from '../ui-kit/Media'; +import { WalletAvatar } from '../components/WalletAvatar'; +import { WalletSourceIcon } from '../components/WalletSourceIcon'; +import { WalletDisplayName } from '../components/WalletDisplayName'; +import { PortfolioValue } from '../shared/requests/PortfolioValue'; +import { UnstyledAnchor } from '../ui-kit/UnstyledAnchor'; +import { useBackgroundKind } from '../components/Background'; +import { TokenIcon } from '../ui-kit/TokenIcon'; +import { middleTruncate } from '../shared/middleTruncate'; + +function WalletItem({ + wallet, +}: { + wallet: ExternallyOwnedAccount & { groupId: string }; +}) { + return ( + + + + } + /> + } + text={ + + ( + + {data.value} + + )} + /> + + } + detailText={ + ( + + {entry.value ? ( + + ) : ( + NBSP + )} + + )} + /> + } + /> + + entry.data?.['portfolio-decomposition'].change_24h.relative ? ( + = 0 + ? 'var(--positive-500)' + : 'var(--negative-500)' + } + > + {entry.data['portfolio-decomposition'].change_24h.relative >= 0 + ? '+' + : minus} + {formatPercent( + Math.abs( + entry.data['portfolio-decomposition'].change_24h.relative + ), + 'en' + )} + % + + ) : ( +
+ ) + } + /> + + + ); +} + +function FeaturedDapps({ section }: { section: FeaturedDappsType }) { + return ( + + {section.dapps.map((dapp) => ( + + + + + {dapp.name} + + + + ))} + + ); +} + +function TokenList({ section }: { section: TopMoversType | TopTokensType }) { + return ( + + {section.fungibles.map((token) => ( + + + + + + {token.name} + + + + + = 0 + ? 'var(--positive-500)' + : 'var(--negative-500)' + } + > + {(token.meta.relativeChange1d || 0) >= 0 ? '+' : minus} + {formatPercent( + Math.abs(token.meta.relativeChange1d || 0), + 'en' + )} + % + + + + + ))} + + ); +} + +function MintsForYou({ section }: { section: MintForYouType }) { + return ( + + {section.mints.map((mint) => ( + + + {mint.title} + + + {mint.title} + + + {mint.reason.title} + + + + + ))} + + ); +} + +function PopularWallets({ section }: { section: PopularWalletsType }) { + return ( + + {section.wallets.map((wallet) => ( + + + + + {wallet.name === wallet.address + ? middleTruncate({ value: wallet.address }) + : wallet.name} + + + + ))} + + ); +} + +function Explore({ addresses }: { addresses: string[] }) { + const { data: exploreData } = useQuery({ + queryKey: ['getExploreSections', addresses], + queryFn: () => + addresses + ? ZerionAPI.getExploreSections({ addresses, currency: 'usd' }) + : null, + suspense: false, + }); + + const { data: portfolioData } = useAddressPortfolioDecomposition({ + addresses, + currency: 'usd', + }); + + const { data: walletGroups } = useQuery({ + queryKey: ['wallet/uiGetWalletGroups'], + queryFn: () => walletPort.request('uiGetWalletGroups'), + useErrorBoundary: true, + }); + + const wallets = useMemo(() => { + const result = []; + if (!walletGroups) { + return []; + } + for (const group of walletGroups) { + for (const wallet of group.walletContainer.wallets) { + result.push({ ...wallet, groupId: group.id }); + } + } + return result; + }, [walletGroups]); + + const totalValue = + portfolioData?.['portfolio-decomposition'].total_value || noValueDash; + const changeValue = portfolioData?.['portfolio-decomposition'].change_24h; + + return ( + +
+
+ + {exploreData?.data.sections.map((section) => ( + + {section.id === 'featured_dapps' ? ( + <> + {section.title} + + + ) : section.id === 'top_movers' || section.id === 'top_tokens' ? ( + <> + {section.title} + + + ) : section.id === 'mints_for_you' ? ( + <> + {section.title} + + + ) : section.id === 'popular_wallets' ? ( + <> + {section.title} + + + ) : null} + + ))} + +
+
+
+ + + + + Portfolio + + + + + {changeValue ? ( + = 0 + ? 'var(--positive-500)' + : 'var(--negative-500)' + } + > + {`${changeValue.relative >= 0 ? '+' : minus}${formatPercent( + Math.abs(changeValue.relative), + 'en' + )}% (${formatCurrencyValue( + Math.abs(changeValue.absolute), + 'en', + 'usd' + )}) Today`} + + ) : ( + {noValueDash} + )} + + + + Wallets + {wallets.map((wallet) => ( + + ))} + + +
+
+ + ); +} + +function LockedTab({ onSuccess }: { onSuccess: () => void }) { + return ( + +
+
+
+ +
+
+
+ ); +} + +export function NewTab() { + useBackgroundKind({ kind: 'neutral' }); + const { data: addresses, refetch } = useWalletAddresses(); + + if (!addresses) { + return ; + } + + return ; +} diff --git a/src/ui/NewTab/index.ts b/src/ui/NewTab/index.ts new file mode 100644 index 000000000..6ace15588 --- /dev/null +++ b/src/ui/NewTab/index.ts @@ -0,0 +1 @@ +export { NewTab } from './NewTab'; diff --git a/src/ui/components/VerifyUser/VerifyUser.tsx b/src/ui/components/VerifyUser/VerifyUser.tsx index 501bbb2c9..b1c101a1a 100644 --- a/src/ui/components/VerifyUser/VerifyUser.tsx +++ b/src/ui/components/VerifyUser/VerifyUser.tsx @@ -55,7 +55,7 @@ export function VerifyUser({ gridTemplateRows: 'auto 1fr', }} > - + Enter Password {text} diff --git a/src/ui/pages/History/AccelerateTransactionDialog/CancelTx/CancelTx.tsx b/src/ui/pages/History/AccelerateTransactionDialog/CancelTx/CancelTx.tsx index f15207fb6..5dd50cb83 100644 --- a/src/ui/pages/History/AccelerateTransactionDialog/CancelTx/CancelTx.tsx +++ b/src/ui/pages/History/AccelerateTransactionDialog/CancelTx/CancelTx.tsx @@ -213,14 +213,12 @@ function CancelTxContent({ > Back - {preferences ? ( - sendTransaction()} - holdToSign={preferences.enableHoldToSignButton} - /> - ) : null} + sendTransaction()} + holdToSign={true} + />
diff --git a/src/ui/pages/History/AccelerateTransactionDialog/SpeedUp/SpeedUp.tsx b/src/ui/pages/History/AccelerateTransactionDialog/SpeedUp/SpeedUp.tsx index 7f6995cc5..d287ffed3 100644 --- a/src/ui/pages/History/AccelerateTransactionDialog/SpeedUp/SpeedUp.tsx +++ b/src/ui/pages/History/AccelerateTransactionDialog/SpeedUp/SpeedUp.tsx @@ -207,14 +207,12 @@ export function SpeedUp({ > Back - {preferences ? ( - sendTransaction()} - holdToSign={preferences.enableHoldToSignButton} - /> - ) : null} + sendTransaction()} + holdToSign={true} + />
diff --git a/src/ui/pages/Settings/Settings.tsx b/src/ui/pages/Settings/Settings.tsx index 035edb88f..b1d7e93de 100644 --- a/src/ui/pages/Settings/Settings.tsx +++ b/src/ui/pages/Settings/Settings.tsx @@ -330,28 +330,45 @@ function DeveloperTools() { function Experiments() { const { preferences, setPreferences } = usePreferences(); + const { globalPreferences, setGlobalPreferences } = useGlobalPreferences(); useBackgroundKind({ kind: 'white' }); return ( - - { - setPreferences({ - enableHoldToSignButton: event.target.checked, - }); - }} - detailText={ - - Sign transactions with a long click to avoid accidental signing - - } - /> - + + + { + setPreferences({ + enableHoldToSignButton: event.target.checked, + }); + }} + detailText={ + + Sign transactions with a long click to avoid accidental signing + + } + /> + + + { + setGlobalPreferences({ + enableNewTabOverride: event.target.checked, + }); + }} + detailText={ + Turn new tab into the portal to Web3 universe + } + /> + + );