ci(release): publish latest release

parent 96851c80
...@@ -51,3 +51,6 @@ packages/uniswap/src/i18n/locales/source/*_old.json ...@@ -51,3 +51,6 @@ packages/uniswap/src/i18n/locales/source/*_old.json
# Vercel # Vercel
.vercel .vercel
# CodeTours Extension
.tours/*
IPFS hash of the deployment: IPFS hash of the deployment:
- CIDv0: `QmYH4Tb7M6EoFHKRNyCBHHFeNadwnrmibmQmknWHSQxj8R` - CIDv0: `QmPq9sW2ih541PM9991Trh3Cocrdstrh63m6UWRbZb6Nqo`
- CIDv1: `bafybeietvftf6vmileh7kp3srslms4azyza5cmgu6ttrzikjzz4dkrryxy` - CIDv1: `bafybeiawfdwfe6ayrmkbbgbjos2noxf2w7ihz4tk7x5tu67ybggnmuvrni`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org). The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
...@@ -10,15 +10,104 @@ You can also access the Uniswap Interface from an IPFS gateway. ...@@ -10,15 +10,104 @@ You can also access the Uniswap Interface from an IPFS gateway.
Your Uniswap settings are never remembered across different URLs. Your Uniswap settings are never remembered across different URLs.
IPFS gateways: IPFS gateways:
- https://bafybeietvftf6vmileh7kp3srslms4azyza5cmgu6ttrzikjzz4dkrryxy.ipfs.dweb.link/ - https://bafybeiawfdwfe6ayrmkbbgbjos2noxf2w7ihz4tk7x5tu67ybggnmuvrni.ipfs.dweb.link/
- https://bafybeietvftf6vmileh7kp3srslms4azyza5cmgu6ttrzikjzz4dkrryxy.ipfs.cf-ipfs.com/ - https://bafybeiawfdwfe6ayrmkbbgbjos2noxf2w7ihz4tk7x5tu67ybggnmuvrni.ipfs.cf-ipfs.com/
- [ipfs://QmYH4Tb7M6EoFHKRNyCBHHFeNadwnrmibmQmknWHSQxj8R/](ipfs://QmYH4Tb7M6EoFHKRNyCBHHFeNadwnrmibmQmknWHSQxj8R/) - [ipfs://QmPq9sW2ih541PM9991Trh3Cocrdstrh63m6UWRbZb6Nqo/](ipfs://QmPq9sW2ih541PM9991Trh3Cocrdstrh63m6UWRbZb6Nqo/)
## 5.55.0 (2024-10-24) ## 5.56.0 (2024-10-29)
### Features ### Features
* **web:** only show bridging card on swap tab- prod (#13338) 95e03e4 * **web:** add empty states for not connected wallets and wallets with no positions (#12973) 31320f1
* **web:** add entry points for new lp flow (#13053) 1a7a2d9
* **web:** add hook parsing util (#13364) ed1eeb6
* **web:** add mainnet to bridge banner (#13296) 9f2fe44
* **web:** add new TokenWarningCard to tdp and pdp (#12667) 36688f1
* **web:** add the hook modal (#13371) d18aae7
* **web:** add warning icon to search bar (#12768) 830022a
* **web:** adding liquidity create step (#13014) 5ec4b90
* **web:** adding v4 to the liquidity flow (#12793) 966a85d
* **web:** closed Positions CTA at bottom of positions list (#13308) 2220383
* **web:** handle insufficient swap approvals (#13201) 60f699a
* **web:** improve fingerprinting for swap errors (#13045) cc066b9
* **web:** improve remove liquidity modal (#12936) 1ab10ca
* **web:** include poolId on positionInfo object (#13269) 14c26a1
* **web:** LP creation default one input to native currency (#13167) 9de127c
* **web:** migrate v3 liquidity review modal, saga logic (#13008) 3017b06
* **web:** mweb layouts for new lp pages (#13317) d0836e0
* **web:** redesigned pool table tabs (#13291) 918ee0f
* **web:** remove manual wrapping step (#13022) 956377a
* **web:** Remove Vanilla Extract from non-nft code (#12504) 07440e5
* **web:** support v4 position NFT images (#13349) 88062c8
* **web:** truncate bridge activity for smaller screens (#13074) 1a79bda
* **web:** UI updates for the pdp page for v4 (#12878) 491748a
* **web:** updates types in Create flow to support native (#13024) 5657b0e
* **web:** use live fee tier data for position creation flow (#12880) 9939608
* **web:** use tickspacings when fees are selected (#12945) c5908fd
* **web:** v2 create flow setup (#12767) 4a17ba4
* **web:** v3-v4 migrate calldata query (#12902) c4e9646
* **web:** v4 create flow creating a pool (#12747) 64dcd7d
* **web:** v4 url redirects (#13237) d91a04c
### Bug Fixes
* **web:** [v4] fix "New" button styling on positions page (#13143) 24e8bb9
* **web:** [v4] fix reset button (#13160) 8b10c23
* **web:** [v4] normalize language to collect fees (#13150) 97b4fee
* **web:** [v4] polish (#13204) 9735bed
* **web:** Add 3s delay to portfolio balance refetch (#13367) 4605961
* **web:** add help center links (#13147) a9239c4
* **web:** add missing breadcrumb to LP create page (#13306) f0743d2
* **web:** Align Continue button text (#13023) 9bfcaf5
* **web:** allow pool creation on testnets (#13009) 05dfe00
* **web:** bugs in create flow when initializing pool (#13282) c333def
* **web:** check wrapped input approvals for all uniswapx types (#13377) 59a8378
* **web:** create fee tier alignment and nan (#13157) 701c7a9
* **web:** default price range fix (#13169) adc95d6
* **web:** display bridging options in unconnected state (#13048) 0961f12
* **web:** dont hide position filters (#13194) 2a6b72a
* **web:** fallback to local activity if remote is empty (#13135) b5129d8
* **web:** fix blocked tokens on TDP (#12742) d364216
* **web:** fix broken worldchain images (#13028) b9d436d
* **web:** fix crashe in create flow when changing tokens (#13264) 63b9734
* **web:** Fix explore table only scrolling once (#13110) 73083d7
* **web:** fix fee modal crash (#13083) 91983fc
* **web:** fix formatting for closed positions (#13172) bfdedd9
* **web:** fix link to PosDP from migratev3 page (#13311) a8e2050
* **web:** fix network filter on explore (#12876) e6aaa50
* **web:** handle account chain id switch (#12994) 45fcd58
* **web:** handle selecting coin on diff chain (#13149) 49c903d
* **web:** keep old data in positions list while loading new filter results (#13299) 6cc7159
* **web:** mock pair and mock pool price numerator and denominators are switched (#13279) efe13f3
* **web:** navbar links for v4 positions pages (#13271) 2b3821e
* **web:** numeric input validation in fee tiers search (#13304) b51bf90
* **web:** Only poll for bridging status updates if pending txs (#13066) f31d0fc
* **web:** only show bridging card on swap tab (#13333) c1f3eba
* **web:** persist positions filters and remove "closed" from default filter (#13168) f10fc62
* **web:** prevent swap flow from continuing when approval has not bee… (#13374) 7421208
* **web:** Redirect to security measures article while clicking button in ResetComplete step (#12606) e917818
* **web:** remove outputPositionLiquidity from migration request (#13156) 2b4d71c
* **web:** remove second status on pdp (#13210) db5f3e1
* **web:** remove v2 liquidity (add approve step) (#13314) ce48a33
* **web:** show liqudity info badge in step and confirmation (#13312) 0c23eff
* **web:** show Not Found on PosDP if it doesn't exist (#13250) 0177752
* **web:** temp endpoints (#13093) a50b98d
* **web:** unichain modal button widths (#13092) 0eb5abe
* **web:** Unstick continue button from SettingsRecoveryPhrase screen (#12609) 10b17a2
* **web:** update approved token (#13357) de06e23
* **web:** update v2 remove on L2 functionality (#13037) 6971b0b
* **web:** use position chain id (#13001) 3872b9a
* **web:** use prod url for positions API (#13351) e25b4ca
* **web:** v4 create flow - reset tokens on chain changed (#13249) ae6ef1a
* **web:** v4 fixes (#13223) 6ad8335
* **web:** v4 poolsQueryEnabledCheck (#13078) b9f210d
* **web:** various trading api calls fixes (#13102) ecb1c30
### Continuous Integration
* **web:** update sitemaps afffa8d
web/5.55.0 web/5.56.0
\ No newline at end of file \ No newline at end of file
...@@ -12,7 +12,7 @@ module.exports = { ...@@ -12,7 +12,7 @@ module.exports = {
'manifest.json', 'manifest.json',
], ],
parserOptions: { parserOptions: {
project: 'tsconfig.json', project: 'tsconfig.eslint.json',
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
ecmaFeatures: { ecmaFeatures: {
jsx: true, jsx: true,
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
"browserslist": "last 2 chrome versions", "browserslist": "last 2 chrome versions",
"dependencies": { "dependencies": {
"@apollo/client": "3.10.4", "@apollo/client": "3.10.4",
"@datadog/browser-rum": "5.23.3",
"@ethersproject/providers": "5.7.2", "@ethersproject/providers": "5.7.2",
"@metamask/rpc-errors": "6.2.1", "@metamask/rpc-errors": "6.2.1",
"@reduxjs/toolkit": "1.9.3", "@reduxjs/toolkit": "1.9.3",
...@@ -14,10 +15,10 @@ ...@@ -14,10 +15,10 @@
"@tamagui/core": "1.108.4", "@tamagui/core": "1.108.4",
"@types/uuid": "9.0.1", "@types/uuid": "9.0.1",
"@uniswap/analytics-events": "2.38.0", "@uniswap/analytics-events": "2.38.0",
"@uniswap/uniswapx-sdk": "^2.1.0-beta.14", "@uniswap/uniswapx-sdk": "2.1.0-beta.18",
"@uniswap/universal-router-sdk": "4.2.0", "@uniswap/universal-router-sdk": "4.5.2",
"@uniswap/v3-sdk": "3.17.0", "@uniswap/v3-sdk": "3.18.1",
"@uniswap/v4-sdk": "1.10.0", "@uniswap/v4-sdk": "1.10.3",
"dotenv-webpack": "8.0.1", "dotenv-webpack": "8.0.1",
"ethers": "5.7.2", "ethers": "5.7.2",
"eventemitter3": "5.0.1", "eventemitter3": "5.0.1",
......
...@@ -34,7 +34,7 @@ import { ScanToOnboard } from 'src/app/features/onboarding/scan/ScanToOnboard' ...@@ -34,7 +34,7 @@ import { ScanToOnboard } from 'src/app/features/onboarding/scan/ScanToOnboard'
import { ScantasticContextProvider } from 'src/app/features/onboarding/scan/ScantasticContextProvider' import { ScantasticContextProvider } from 'src/app/features/onboarding/scan/ScantasticContextProvider'
import { OnboardingRoutes, TopLevelRoutes } from 'src/app/navigation/constants' import { OnboardingRoutes, TopLevelRoutes } from 'src/app/navigation/constants'
import { setRouter, setRouterState } from 'src/app/navigation/state' import { setRouter, setRouterState } from 'src/app/navigation/state'
import { sentryCreateHashRouter } from 'src/app/sentry' import { SentryAppNameTag, sentryCreateHashRouter } from 'src/app/sentry'
import { initExtensionAnalytics } from 'src/app/utils/analytics' import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { checksIfSupportsSidePanel } from 'src/app/utils/chrome' import { checksIfSupportsSidePanel } from 'src/app/utils/chrome'
import { PrimaryAppInstanceDebuggerLazy } from 'src/store/PrimaryAppInstanceDebuggerLazy' import { PrimaryAppInstanceDebuggerLazy } from 'src/store/PrimaryAppInstanceDebuggerLazy'
...@@ -56,14 +56,6 @@ const unsupportedRoute: RouteObject = { ...@@ -56,14 +56,6 @@ const unsupportedRoute: RouteObject = {
element: <UnsupportedBrowserScreen />, element: <UnsupportedBrowserScreen />,
} }
const createSteps = {
[CreateOnboardingSteps.Password]: <PasswordCreate />,
[CreateOnboardingSteps.ViewMnemonic]: <ViewMnemonic />,
[CreateOnboardingSteps.TestMnemonic]: <TestMnemonic />,
[CreateOnboardingSteps.Naming]: <NameWallet />,
[CreateOnboardingSteps.Complete]: <Complete flow={ExtensionOnboardingFlow.New} />,
}
const allRoutes = [ const allRoutes = [
{ {
path: '', path: '',
...@@ -75,7 +67,18 @@ const allRoutes = [ ...@@ -75,7 +67,18 @@ const allRoutes = [
}, },
{ {
path: OnboardingRoutes.Create, path: OnboardingRoutes.Create,
element: <OnboardingStepsProvider key={OnboardingRoutes.Create} steps={createSteps} />, element: (
<OnboardingStepsProvider
key={OnboardingRoutes.Create}
steps={{
[CreateOnboardingSteps.Password]: <PasswordCreate />,
[CreateOnboardingSteps.ViewMnemonic]: <ViewMnemonic />,
[CreateOnboardingSteps.TestMnemonic]: <TestMnemonic />,
[CreateOnboardingSteps.Naming]: <NameWallet />,
[CreateOnboardingSteps.Complete]: <Complete flow={ExtensionOnboardingFlow.New} />,
}}
/>
),
}, },
{ {
path: OnboardingRoutes.Claim, path: OnboardingRoutes.Claim,
...@@ -84,7 +87,10 @@ const allRoutes = [ ...@@ -84,7 +87,10 @@ const allRoutes = [
key={OnboardingRoutes.Claim} key={OnboardingRoutes.Claim}
steps={{ steps={{
[CreateOnboardingSteps.ClaimUnitag]: <ClaimUnitagScreen />, [CreateOnboardingSteps.ClaimUnitag]: <ClaimUnitagScreen />,
...createSteps, [CreateOnboardingSteps.Password]: <PasswordCreate />,
[CreateOnboardingSteps.ViewMnemonic]: <ViewMnemonic />,
[CreateOnboardingSteps.TestMnemonic]: <TestMnemonic />,
[CreateOnboardingSteps.Complete]: <Complete tryToClaimUnitag flow={ExtensionOnboardingFlow.New} />,
}} }}
/> />
), ),
...@@ -181,7 +187,7 @@ export default function OnboardingApp(): JSX.Element { ...@@ -181,7 +187,7 @@ export default function OnboardingApp(): JSX.Element {
return ( return (
<Trace> <Trace>
<PersistGate persistor={getReduxPersistor()}> <PersistGate persistor={getReduxPersistor()}>
<ExtensionStatsigProvider> <ExtensionStatsigProvider appName={SentryAppNameTag.Onboarding}>
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}> <SharedWalletProvider reduxStore={getReduxStore()}>
<ErrorBoundary> <ErrorBoundary>
......
...@@ -13,7 +13,6 @@ import { TraceUserProperties } from 'src/app/components/Trace/TraceUserPropertie ...@@ -13,7 +13,6 @@ import { TraceUserProperties } from 'src/app/components/Trace/TraceUserPropertie
import { DappContextProvider } from 'src/app/features/dapp/DappContext' import { DappContextProvider } from 'src/app/features/dapp/DappContext'
import { SentryAppNameTag, initializeSentry, sentryCreateHashRouter } from 'src/app/sentry' import { SentryAppNameTag, initializeSentry, sentryCreateHashRouter } from 'src/app/sentry'
import { initExtensionAnalytics } from 'src/app/utils/analytics' import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { getLocalUserId } from 'src/app/utils/storage'
import { getReduxPersistor, getReduxStore } from 'src/store/store' import { getReduxPersistor, getReduxStore } from 'src/store/store'
import { Button, Flex, Image, Text } from 'ui/src' import { Button, Flex, Image, Text } from 'ui/src'
import { CHROME_LOGO, UNISWAP_LOGO } from 'ui/src/assets' import { CHROME_LOGO, UNISWAP_LOGO } from 'ui/src/assets'
...@@ -25,18 +24,19 @@ import { ElementName } from 'uniswap/src/features/telemetry/constants' ...@@ -25,18 +24,19 @@ import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context' import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context'
import i18n from 'uniswap/src/i18n/i18n' import i18n from 'uniswap/src/i18n/i18n'
import { ExtensionScreens } from 'uniswap/src/types/screens/extension' import { ExtensionScreens } from 'uniswap/src/types/screens/extension'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary' import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks' import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks'
import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider' import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider'
getLocalUserId() getUniqueId()
.then((userId) => { .then((userId) => {
initializeSentry(SentryAppNameTag.Popup, userId) initializeSentry(SentryAppNameTag.Popup, userId)
}) })
.catch((error) => { .catch((error) => {
logger.error(error, { logger.error(error, {
tags: { file: 'PopupApp.tsx', function: 'getLocalUserId' }, tags: { file: 'PopupApp.tsx', function: 'getUniqueId' },
}) })
}) })
...@@ -127,7 +127,7 @@ export default function PopupApp(): JSX.Element { ...@@ -127,7 +127,7 @@ export default function PopupApp(): JSX.Element {
return ( return (
<Trace> <Trace>
<PersistGate persistor={getReduxPersistor()}> <PersistGate persistor={getReduxPersistor()}>
<ExtensionStatsigProvider> <ExtensionStatsigProvider appName={SentryAppNameTag.Popup}>
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}> <SharedWalletProvider reduxStore={getReduxStore()}>
<ErrorBoundary> <ErrorBoundary>
......
...@@ -31,7 +31,6 @@ import { MainContent, WebNavigation } from 'src/app/navigation/navigation' ...@@ -31,7 +31,6 @@ import { MainContent, WebNavigation } from 'src/app/navigation/navigation'
import { setRouter, setRouterState } from 'src/app/navigation/state' import { setRouter, setRouterState } from 'src/app/navigation/state'
import { SentryAppNameTag, initializeSentry, sentryCreateHashRouter } from 'src/app/sentry' import { SentryAppNameTag, initializeSentry, sentryCreateHashRouter } from 'src/app/sentry'
import { initExtensionAnalytics } from 'src/app/utils/analytics' import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { getLocalUserId } from 'src/app/utils/storage'
import { import {
DappBackgroundPortChannel, DappBackgroundPortChannel,
backgroundToSidePanelMessageChannel, backgroundToSidePanelMessageChannel,
...@@ -47,6 +46,7 @@ import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' ...@@ -47,6 +46,7 @@ import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { UnitagUpdaterContextProvider, useUnitagUpdater } from 'uniswap/src/features/unitags/context' import { UnitagUpdaterContextProvider, useUnitagUpdater } from 'uniswap/src/features/unitags/context'
import i18n from 'uniswap/src/i18n/i18n' import i18n from 'uniswap/src/i18n/i18n'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { isDevEnv } from 'utilities/src/environment/env' import { isDevEnv } from 'utilities/src/environment/env'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { ONE_SECOND_MS } from 'utilities/src/time/time' import { ONE_SECOND_MS } from 'utilities/src/time/time'
...@@ -55,13 +55,13 @@ import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary ...@@ -55,13 +55,13 @@ import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks' import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks'
import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider' import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider'
getLocalUserId() getUniqueId()
.then((userId) => { .then((userId) => {
initializeSentry(SentryAppNameTag.Sidebar, userId) initializeSentry(SentryAppNameTag.Sidebar, userId)
}) })
.catch((error) => { .catch((error) => {
logger.error(error, { logger.error(error, {
tags: { file: 'SidebarApp.tsx', function: 'getLocalUserId' }, tags: { file: 'SidebarApp.tsx', function: 'getUniqueId' },
}) })
}) })
...@@ -257,7 +257,7 @@ export default function SidebarApp(): JSX.Element { ...@@ -257,7 +257,7 @@ export default function SidebarApp(): JSX.Element {
return ( return (
<Trace> <Trace>
<PersistGate persistor={getReduxPersistor()}> <PersistGate persistor={getReduxPersistor()}>
<ExtensionStatsigProvider> <ExtensionStatsigProvider appName={SentryAppNameTag.Sidebar}>
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}> <SharedWalletProvider reduxStore={getReduxStore()}>
<ErrorBoundary> <ErrorBoundary>
......
import { getLocalUserId } from 'src/app/utils/storage' import { useEffect, useState } from 'react'
import { initializeDatadog } from 'src/app/datadog'
import { getStatsigEnvironmentTier } from 'src/app/version' import { getStatsigEnvironmentTier } from 'src/app/version'
import Statsig from 'statsig-js' // Use JS package for browser import Statsig from 'statsig-js' // Use JS package for browser
import { uniswapUrls } from 'uniswap/src/constants/urls' import { uniswapUrls } from 'uniswap/src/constants/urls'
import { DUMMY_STATSIG_SDK_KEY, StatsigCustomAppValue } from 'uniswap/src/features/gating/constants' import { DUMMY_STATSIG_SDK_KEY, StatsigCustomAppValue } from 'uniswap/src/features/gating/constants'
import { StatsigOptions, StatsigProvider, StatsigUser } from 'uniswap/src/features/gating/sdk/statsig' import { StatsigOptions, StatsigProvider, StatsigUser } from 'uniswap/src/features/gating/sdk/statsig'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { useAsyncData } from 'utilities/src/react/hooks' import { useAsyncData } from 'utilities/src/react/hooks'
async function getStatsigUser(): Promise<StatsigUser> { async function getStatsigUser(): Promise<StatsigUser> {
return { return {
userID: await getLocalUserId(), userID: await getUniqueId(),
appVersion: process.env.VERSION, appVersion: process.env.VERSION,
custom: { custom: {
app: StatsigCustomAppValue.Extension, app: StatsigCustomAppValue.Extension,
...@@ -16,16 +18,28 @@ async function getStatsigUser(): Promise<StatsigUser> { ...@@ -16,16 +18,28 @@ async function getStatsigUser(): Promise<StatsigUser> {
} }
} }
export function ExtensionStatsigProvider({ children }: { children: React.ReactNode }): JSX.Element { export function ExtensionStatsigProvider({
const { data: user } = useAsyncData(getStatsigUser) children,
appName,
const nonNullUser: StatsigUser = user ?? { }: {
children: React.ReactNode
appName: string
}): JSX.Element {
const { data: storedUser } = useAsyncData(getStatsigUser)
const [user, setUser] = useState<StatsigUser>({
userID: undefined, userID: undefined,
custom: { custom: {
app: StatsigCustomAppValue.Extension, app: StatsigCustomAppValue.Extension,
}, },
appVersion: process.env.VERSION, appVersion: process.env.VERSION,
})
const [initFinished, setInitFinished] = useState(false)
useEffect(() => {
if (storedUser && initFinished) {
setUser(storedUser)
} }
}, [storedUser, initFinished])
const options: StatsigOptions = { const options: StatsigOptions = {
environment: { environment: {
...@@ -34,10 +48,14 @@ export function ExtensionStatsigProvider({ children }: { children: React.ReactNo ...@@ -34,10 +48,14 @@ export function ExtensionStatsigProvider({ children }: { children: React.ReactNo
api: uniswapUrls.statsigProxyUrl, api: uniswapUrls.statsigProxyUrl,
disableAutoMetricsLogging: true, disableAutoMetricsLogging: true,
disableErrorLogging: true, disableErrorLogging: true,
initCompletionCallback: () => {
setInitFinished(true)
initializeDatadog(appName).catch(() => undefined)
},
} }
return ( return (
<StatsigProvider options={options} sdkKey={DUMMY_STATSIG_SDK_KEY} user={nonNullUser} waitForInitialization={false}> <StatsigProvider options={options} sdkKey={DUMMY_STATSIG_SDK_KEY} user={user} waitForInitialization={false}>
{children} {children}
</StatsigProvider> </StatsigProvider>
) )
......
import '@tamagui/core/reset.css' import '@tamagui/core/reset.css'
import 'src/app/Global.css' import 'src/app/Global.css'
import { useEffect } from 'react' import { PropsWithChildren, useEffect } from 'react'
import { I18nextProvider } from 'react-i18next' import { I18nextProvider } from 'react-i18next'
import { Outlet, RouterProvider } from 'react-router-dom' import { Outlet, RouterProvider, useSearchParams } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
import { ExtensionStatsigProvider } from 'src/app/StatsigProvider' import { ExtensionStatsigProvider } from 'src/app/StatsigProvider'
import { GraphqlProvider } from 'src/app/apollo' import { GraphqlProvider } from 'src/app/apollo'
import { ErrorElement } from 'src/app/components/ErrorElement' import { ErrorElement } from 'src/app/components/ErrorElement'
import { TraceUserProperties } from 'src/app/components/Trace/TraceUserProperties' import { TraceUserProperties } from 'src/app/components/Trace/TraceUserProperties'
import { ClaimUnitagSteps, OnboardingStepsProvider } from 'src/app/features/onboarding/OnboardingSteps' import {
ClaimUnitagSteps,
OnboardingStepsProvider,
useOnboardingSteps,
} from 'src/app/features/onboarding/OnboardingSteps'
import { EditUnitagProfileScreen } from 'src/app/features/unitags/EditUnitagProfileScreen' import { EditUnitagProfileScreen } from 'src/app/features/unitags/EditUnitagProfileScreen'
import { UnitagChooseProfilePicScreen } from 'src/app/features/unitags/UnitagChooseProfilePicScreen' import { UnitagChooseProfilePicScreen } from 'src/app/features/unitags/UnitagChooseProfilePicScreen'
import { UnitagClaimBackground } from 'src/app/features/unitags/UnitagClaimBackground'
import { UnitagClaimContextProvider } from 'src/app/features/unitags/UnitagClaimContext' import { UnitagClaimContextProvider } from 'src/app/features/unitags/UnitagClaimContext'
import { UnitagConfirmationScreen } from 'src/app/features/unitags/UnitagConfirmationScreen' import { UnitagConfirmationScreen } from 'src/app/features/unitags/UnitagConfirmationScreen'
import { UnitagCreateUsernameScreen } from 'src/app/features/unitags/UnitagCreateUsernameScreen' import { UnitagCreateUsernameScreen } from 'src/app/features/unitags/UnitagCreateUsernameScreen'
import { UnitagIntroScreen } from 'src/app/features/unitags/UnitagIntroScreen' import { UnitagIntroScreen } from 'src/app/features/unitags/UnitagIntroScreen'
import { OnboardingRoutes } from 'src/app/navigation/constants' import { UnitagClaimRoutes } from 'src/app/navigation/constants'
import { setRouter, setRouterState } from 'src/app/navigation/state' import { setRouter, setRouterState } from 'src/app/navigation/state'
import { SentryAppNameTag, initializeSentry, sentryCreateHashRouter } from 'src/app/sentry' import { SentryAppNameTag, initializeSentry, sentryCreateHashRouter } from 'src/app/sentry'
import { initExtensionAnalytics } from 'src/app/utils/analytics' import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { getLocalUserId } from 'src/app/utils/storage'
import { getReduxPersistor, getReduxStore } from 'src/store/store' import { getReduxPersistor, getReduxStore } from 'src/store/store'
import { Flex } from 'ui/src' import { Flex } from 'ui/src'
import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext' import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext'
import Trace from 'uniswap/src/features/telemetry/Trace' import Trace from 'uniswap/src/features/telemetry/Trace'
import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context' import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context'
import i18n from 'uniswap/src/i18n/i18n' import i18n from 'uniswap/src/i18n/i18n'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { usePrevious } from 'utilities/src/react/hooks'
import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary' import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks' import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks'
import { useAccountAddressFromUrlWithThrow } from 'wallet/src/features/wallet/hooks'
import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider' import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider'
getLocalUserId() getUniqueId()
.then((userId) => { .then((userId) => {
initializeSentry(SentryAppNameTag.UnitagClaim, userId) initializeSentry(SentryAppNameTag.UnitagClaim, userId)
}) })
.catch((error) => { .catch((error) => {
logger.error(error, { logger.error(error, {
tags: { file: 'UnitagClaimApp.tsx', function: 'getLocalUserId' }, tags: { file: 'UnitagClaimApp.tsx', function: 'getUniqueId' },
}) })
}) })
const router = sentryCreateHashRouter([ const router = sentryCreateHashRouter([
{ {
path: '', path: '',
element: <UnitagClaimAppInner />, element: <UnitagAppInner />,
children: [
{
path: UnitagClaimRoutes.ClaimIntro,
element: <UnitagClaimFlow />,
errorElement: <ErrorElement />, errorElement: <ErrorElement />,
}, },
{ {
path: OnboardingRoutes.EditProfile, path: UnitagClaimRoutes.EditProfile,
element: <EditProfileAppInner />, element: <UnitagEditProfileFlow />,
errorElement: <ErrorElement />, errorElement: <ErrorElement />,
}, },
],
},
]) ])
/** /**
...@@ -66,10 +79,36 @@ router.subscribe((state) => { ...@@ -66,10 +79,36 @@ router.subscribe((state) => {
setRouter(router) setRouter(router)
function UnitagClaimAppInner(): JSX.Element { function UnitagAppInner(): JSX.Element {
const [searchParams, setSearchParams] = useSearchParams()
const address = useAccountAddressFromUrlWithThrow()
const prevAddress = usePrevious(address)
// Ensures that address in url search params is consistent with hook
useEffect(() => {
if (searchParams.get('address') !== address) {
setSearchParams({ address })
}
}, [searchParams, address, setSearchParams])
useEffect(() => {
if (prevAddress && address !== prevAddress) {
// needed to reload on address param change for hash router
router
.navigate(0)
.catch((e) => logger.error(e, { tags: { file: 'UnitagClaimApp.tsx', function: 'UnitagClaimAppInner' } }))
}
}, [address, prevAddress])
useTestnetModeForLoggingAndAnalytics() useTestnetModeForLoggingAndAnalytics()
return <Outlet />
}
function UnitagClaimFlow(): JSX.Element {
return ( return (
<Flex centered height="100vh" width="100%"> <Flex centered height="100%" width="100%">
<OnboardingStepsProvider <OnboardingStepsProvider
disableRedirect disableRedirect
steps={{ steps={{
...@@ -79,22 +118,33 @@ function UnitagClaimAppInner(): JSX.Element { ...@@ -79,22 +118,33 @@ function UnitagClaimAppInner(): JSX.Element {
[ClaimUnitagSteps.Confirmation]: <UnitagConfirmationScreen />, [ClaimUnitagSteps.Confirmation]: <UnitagConfirmationScreen />,
[ClaimUnitagSteps.EditProfile]: <EditUnitagProfileScreen enableBack />, [ClaimUnitagSteps.EditProfile]: <EditUnitagProfileScreen enableBack />,
}} }}
ContainerComponent={UnitagClaimContextProvider} ContainerComponent={UnitagClaimAppWrapper}
/> />
<Outlet /> <Outlet />
</Flex> </Flex>
) )
} }
function EditProfileAppInner(): JSX.Element { function UnitagClaimAppWrapper({ children }: PropsWithChildren): JSX.Element {
const { step } = useOnboardingSteps()
const blurAllBackground = step !== ClaimUnitagSteps.Intro
return (
<UnitagClaimContextProvider>
<UnitagClaimBackground blurAll={blurAllBackground}>{children}</UnitagClaimBackground>
</UnitagClaimContextProvider>
)
}
function UnitagEditProfileFlow(): JSX.Element {
return ( return (
<Flex centered> <Flex centered height="100%" width="100%">
<OnboardingStepsProvider <OnboardingStepsProvider
disableRedirect disableRedirect
steps={{ steps={{
[ClaimUnitagSteps.Intro]: <EditUnitagProfileScreen />, [ClaimUnitagSteps.EditProfile]: <EditUnitagProfileScreen />,
}} }}
ContainerComponent={UnitagClaimContextProvider} ContainerComponent={UnitagClaimAppWrapper}
/> />
<Outlet /> <Outlet />
</Flex> </Flex>
...@@ -111,7 +161,7 @@ export default function UnitagClaimApp(): JSX.Element { ...@@ -111,7 +161,7 @@ export default function UnitagClaimApp(): JSX.Element {
return ( return (
<Trace> <Trace>
<PersistGate persistor={getReduxPersistor()}> <PersistGate persistor={getReduxPersistor()}>
<ExtensionStatsigProvider> <ExtensionStatsigProvider appName={SentryAppNameTag.UnitagClaim}>
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}> <SharedWalletProvider reduxStore={getReduxStore()}>
<ErrorBoundary> <ErrorBoundary>
......
...@@ -2,7 +2,11 @@ import { useEffect } from 'react' ...@@ -2,7 +2,11 @@ import { useEffect } from 'react'
import { useColorScheme } from 'react-native' import { useColorScheme } from 'react-native'
import { useAppFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks' import { useAppFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks'
import { useCurrentLanguage } from 'uniswap/src/features/language/hooks' import { useCurrentLanguage } from 'uniswap/src/features/language/hooks'
import { useHideSmallBalancesSetting, useHideSpamTokensSetting } from 'uniswap/src/features/settings/hooks' import {
useEnabledChains,
useHideSmallBalancesSetting,
useHideSpamTokensSetting,
} from 'uniswap/src/features/settings/hooks'
import { ExtensionUserPropertyName, setUserProperty } from 'uniswap/src/features/telemetry/user' import { ExtensionUserPropertyName, setUserProperty } from 'uniswap/src/features/telemetry/user'
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { analytics } from 'utilities/src/telemetry/analytics/analytics' import { analytics } from 'utilities/src/telemetry/analytics/analytics'
...@@ -19,6 +23,7 @@ export function TraceUserProperties(): null { ...@@ -19,6 +23,7 @@ export function TraceUserProperties(): null {
const hideSpamTokens = useHideSpamTokensSetting() const hideSpamTokens = useHideSpamTokensSetting()
const currentLanguage = useCurrentLanguage() const currentLanguage = useCurrentLanguage()
const appFiatCurrencyInfo = useAppFiatCurrencyInfo() const appFiatCurrencyInfo = useAppFiatCurrencyInfo()
const { isTestnetModeEnabled } = useEnabledChains()
useGatingUserPropertyUsernames() useGatingUserPropertyUsernames()
...@@ -63,5 +68,9 @@ export function TraceUserProperties(): null { ...@@ -63,5 +68,9 @@ export function TraceUserProperties(): null {
setUserProperty(ExtensionUserPropertyName.Currency, appFiatCurrencyInfo.code) setUserProperty(ExtensionUserPropertyName.Currency, appFiatCurrencyInfo.code)
}, [appFiatCurrencyInfo]) }, [appFiatCurrencyInfo])
useEffect(() => {
setUserProperty(ExtensionUserPropertyName.TestnetModeEnabled, isTestnetModeEnabled)
}, [isTestnetModeEnabled])
return null return null
} }
import { datadogRum } from '@datadog/browser-rum'
import { getDatadogEnvironment } from 'src/app/version'
import { config } from 'uniswap/src/config'
import { Experiments } from 'uniswap/src/features/gating/experiments'
import { FeatureFlags, WALLET_FEATURE_FLAG_NAMES, getFeatureFlagName } from 'uniswap/src/features/gating/flags'
import { Statsig } from 'uniswap/src/features/gating/sdk/statsig'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger'
export async function initializeDatadog(appName: string): Promise<void> {
const datadogEnabled = Statsig.checkGate(getFeatureFlagName(FeatureFlags.Datadog))
logger.setWalletDatadogEnabled(datadogEnabled)
if (__DEV__ || !datadogEnabled) {
return
}
datadogRum.init({
applicationId: config.datadogProjectId,
clientToken: config.datadogClientToken,
service: `extension-${getDatadogEnvironment()}`,
env: getDatadogEnvironment(),
version: process.env.VERSION,
sessionSampleRate: 100,
sessionReplaySampleRate: 0,
trackResources: true,
trackLongTasks: true,
trackUserInteractions: true,
enablePrivacyForActionName: true,
beforeSend: (event) => {
// otherwise DataDog will ignore error events
event.view.url = event.view.url.replace(/^chrome-extension:\/\/[a-z]{32}\//i, '')
if (event.error && event.type === 'error') {
Object.defineProperty(event.error, 'stack', {
value: event.error.stack?.replace(/chrome-extension:\/\/[a-z]{32}/gi, ''),
writable: false,
configurable: true,
})
}
return true
},
})
try {
const userId = await getUniqueId()
datadogRum.setUser({
id: userId,
})
} catch (e) {
logger.error(e, {
tags: { file: 'datadog.ts', function: 'initializeDatadog' },
})
}
datadogRum.setGlobalContextProperty('app', appName)
for (const [_, flagKey] of WALLET_FEATURE_FLAG_NAMES.entries()) {
datadogRum.addFeatureFlagEvaluation(
// Datadog has a limited set of accepted symbols in feature flags
// https://docs.datadoghq.com/real_user_monitoring/guide/setup-feature-flag-data-collection/?tab=reactnative#feature-flag-naming
flagKey.replaceAll('-', '_'),
Statsig.checkGateWithExposureLoggingDisabled(flagKey),
)
}
for (const experiment of Object.values(Experiments)) {
datadogRum.addFeatureFlagEvaluation(
// Datadog has a limited set of accepted symbols in feature flags
// https://docs.datadoghq.com/real_user_monitoring/guide/setup-feature-flag-data-collection/?tab=reactnative#feature-flag-naming
`experiment_${experiment.replaceAll('-', '_')}`,
Statsig.getExperimentWithExposureLoggingDisabled(experiment).getGroupName(),
)
}
}
...@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' ...@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { EditLabelModal } from 'src/app/features/accounts/EditLabelModal' import { EditLabelModal } from 'src/app/features/accounts/EditLabelModal'
import { removeAllDappConnectionsForAccount } from 'src/app/features/dapp/actions' import { removeAllDappConnectionsForAccount } from 'src/app/features/dapp/actions'
import { ContextMenu, Flex, MenuContentItem, Text, TouchableArea } from 'ui/src' import { Flex, Text, TouchableArea } from 'ui/src'
import { CopySheets, Edit, Ellipsis, TrashFilled } from 'ui/src/components/icons' import { CopySheets, Edit, Ellipsis, TrashFilled } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal' import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal'
...@@ -17,6 +17,8 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' ...@@ -17,6 +17,8 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { setClipboard } from 'uniswap/src/utils/clipboard' import { setClipboard } from 'uniswap/src/utils/clipboard'
import { NumberType } from 'utilities/src/format/types' import { NumberType } from 'utilities/src/format/types'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { ContextMenu } from 'wallet/src/components/menu/ContextMenu'
import { MenuContentItem } from 'wallet/src/components/menu/types'
import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga' import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga'
import { useActiveAccountWithThrow, useDisplayName, useSignerAccounts } from 'wallet/src/features/wallet/hooks' import { useActiveAccountWithThrow, useDisplayName, useSignerAccounts } from 'wallet/src/features/wallet/hooks'
import { DisplayNameType } from 'wallet/src/features/wallet/types' import { DisplayNameType } from 'wallet/src/features/wallet/types'
......
...@@ -11,10 +11,10 @@ import { updateDappConnectedAddressFromExtension } from 'src/app/features/dapp/a ...@@ -11,10 +11,10 @@ import { updateDappConnectedAddressFromExtension } from 'src/app/features/dapp/a
import { useDappConnectedAccounts } from 'src/app/features/dapp/hooks' import { useDappConnectedAccounts } from 'src/app/features/dapp/hooks'
import { isConnectedAccount } from 'src/app/features/dapp/utils' import { isConnectedAccount } from 'src/app/features/dapp/utils'
import { PopupName, openPopup } from 'src/app/features/popups/slice' import { PopupName, openPopup } from 'src/app/features/popups/slice'
import { AppRoutes, OnboardingRoutes, RemoveRecoveryPhraseRoutes, SettingsRoutes } from 'src/app/navigation/constants' import { AppRoutes, RemoveRecoveryPhraseRoutes, SettingsRoutes, UnitagClaimRoutes } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state' import { navigate } from 'src/app/navigation/state'
import { focusOrCreateUnitagTab } from 'src/app/navigation/utils' import { focusOrCreateUnitagTab } from 'src/app/navigation/utils'
import { Button, Flex, MenuContent, MenuContentItem, Popover, ScrollView, Text, useSporeColors } from 'ui/src' import { Button, Flex, Popover, ScrollView, Text, useSporeColors } from 'ui/src'
import { WalletFilled, X } from 'ui/src/components/icons' import { WalletFilled, X } from 'ui/src/components/icons'
import { spacing } from 'ui/src/theme' import { spacing } from 'ui/src/theme'
import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal' import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal'
...@@ -31,11 +31,18 @@ import { logger } from 'utilities/src/logger/logger' ...@@ -31,11 +31,18 @@ import { logger } from 'utilities/src/logger/logger'
import { sleep } from 'utilities/src/time/timing' import { sleep } from 'utilities/src/time/timing'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { PlusCircle } from 'wallet/src/components/icons/PlusCircle' import { PlusCircle } from 'wallet/src/components/icons/PlusCircle'
import { MenuContent } from 'wallet/src/components/menu/MenuContent'
import { MenuContentItem } from 'wallet/src/components/menu/types'
import { useAccountList } from 'wallet/src/features/accounts/hooks' import { useAccountList } from 'wallet/src/features/accounts/hooks'
import { createOnboardingAccount } from 'wallet/src/features/onboarding/createOnboardingAccount' import { createOnboardingAccount } from 'wallet/src/features/onboarding/createOnboardingAccount'
import { BackupType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' import { BackupType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types'
import { createAccountsActions } from 'wallet/src/features/wallet/create/createAccountsSaga' import { createAccountsActions } from 'wallet/src/features/wallet/create/createAccountsSaga'
import { useActiveAccountWithThrow, useDisplayName, useSignerAccounts } from 'wallet/src/features/wallet/hooks' import {
useActiveAccountAddressWithThrow,
useActiveAccountWithThrow,
useDisplayName,
useSignerAccounts,
} from 'wallet/src/features/wallet/hooks'
import { selectSortedSignerMnemonicAccounts } from 'wallet/src/features/wallet/selectors' import { selectSortedSignerMnemonicAccounts } from 'wallet/src/features/wallet/selectors'
import { setAccountAsActive } from 'wallet/src/features/wallet/slice' import { setAccountAsActive } from 'wallet/src/features/wallet/slice'
import { DisplayNameType } from 'wallet/src/features/wallet/types' import { DisplayNameType } from 'wallet/src/features/wallet/types'
...@@ -284,11 +291,12 @@ export function AccountSwitcherScreen(): JSX.Element { ...@@ -284,11 +291,12 @@ export function AccountSwitcherScreen(): JSX.Element {
const UnitagActionButton = (): JSX.Element => { const UnitagActionButton = (): JSX.Element => {
const { t } = useTranslation() const { t } = useTranslation()
const address = useActiveAccountAddressWithThrow()
const isClaimUnitagEnabled = useFeatureFlag(FeatureFlags.ExtensionClaimUnitag) const isClaimUnitagEnabled = useFeatureFlag(FeatureFlags.ExtensionClaimUnitag)
const onPressEditProfile = useCallback(async () => { const onPressEditProfile = useCallback(async () => {
await focusOrCreateUnitagTab(OnboardingRoutes.EditProfile) await focusOrCreateUnitagTab(address, UnitagClaimRoutes.EditProfile)
}, []) }, [address])
if (isClaimUnitagEnabled) { if (isClaimUnitagEnabled) {
return ( return (
......
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { UnitagClaimRoutes } from 'src/app/navigation/constants'
import { focusOrCreateUnitagTab } from 'src/app/navigation/utils'
import { Button, Flex, Text } from 'ui/src' import { Button, Flex, Text } from 'ui/src'
import { Person } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { TextInput } from 'uniswap/src/components/input/TextInput' import { TextInput } from 'uniswap/src/components/input/TextInput'
import { Modal } from 'uniswap/src/components/modals/Modal' import { Modal } from 'uniswap/src/components/modals/Modal'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { OnboardingCardLoggingName } from 'uniswap/src/features/telemetry/types'
import { shortenAddress } from 'utilities/src/addresses' import { shortenAddress } from 'utilities/src/addresses'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
import { CardType, IntroCard, IntroCardGraphicType } from 'wallet/src/components/introCards/IntroCard'
import { UNITAG_SUFFIX_NO_LEADING_DOT } from 'wallet/src/features/unitags/constants'
import { useCanActiveAddressClaimUnitag } from 'wallet/src/features/unitags/hooks'
import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga' import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga'
import { useDisplayName } from 'wallet/src/features/wallet/hooks' import { useDisplayName } from 'wallet/src/features/wallet/hooks'
import { DisplayNameType } from 'wallet/src/features/wallet/types' import { DisplayNameType } from 'wallet/src/features/wallet/types'
...@@ -28,6 +37,9 @@ export function EditLabelModal({ isOpen, address, onClose }: EditLabelModalProps ...@@ -28,6 +37,9 @@ export function EditLabelModal({ isOpen, address, onClose }: EditLabelModalProps
const [inputText, setInputText] = useState<string>(defaultText) const [inputText, setInputText] = useState<string>(defaultText)
const [isfocused, setIsFocused] = useState(false) const [isfocused, setIsFocused] = useState(false)
const { canClaimUnitag } = useCanActiveAddressClaimUnitag()
const unitagsClaimEnabled = useFeatureFlag(FeatureFlags.ExtensionClaimUnitag)
const onConfirm = useCallback(async () => { const onConfirm = useCallback(async () => {
await dispatch( await dispatch(
editAccountActions.trigger({ editAccountActions.trigger({
...@@ -39,8 +51,34 @@ export function EditLabelModal({ isOpen, address, onClose }: EditLabelModalProps ...@@ -39,8 +51,34 @@ export function EditLabelModal({ isOpen, address, onClose }: EditLabelModalProps
onClose() onClose()
}, [address, dispatch, inputText, onClose]) }, [address, dispatch, inputText, onClose])
const navigateToUnitagClaim = useCallback(async () => {
await focusOrCreateUnitagTab(address, UnitagClaimRoutes.ClaimIntro)
}, [address])
const unitagClaimCard = (
<IntroCard
loggingName={OnboardingCardLoggingName.ClaimUnitag}
graphic={{ type: IntroCardGraphicType.Icon, Icon: Person }}
title={t('onboarding.home.intro.unitag.title', {
unitagDomain: UNITAG_SUFFIX_NO_LEADING_DOT,
})}
description={t('onboarding.home.intro.unitag.description')}
cardType={CardType.Default}
containerProps={{
borderWidth: 0,
backgroundColor: '$surface1',
}}
onPress={navigateToUnitagClaim}
/>
)
return ( return (
<Modal isModalOpen={isOpen} name={ModalName.AccountEditLabel} onClose={onClose}> <Modal
isModalOpen={isOpen}
name={ModalName.AccountEditLabel}
bottomAttachment={canClaimUnitag && unitagsClaimEnabled ? unitagClaimCard : undefined}
onClose={onClose}
>
<Flex centered fill borderRadius="$rounded16" gap="$spacing24" mt="$spacing16"> <Flex centered fill borderRadius="$rounded16" gap="$spacing24" mt="$spacing16">
<Flex centered gap="$spacing12" width="100%"> <Flex centered gap="$spacing12" width="100%">
<AccountIcon address={address} size={iconSizes.icon48} /> <AccountIcon address={address} size={iconSizes.icon48} />
......
...@@ -107,7 +107,13 @@ exports[`AccountSwitcherScreen renders correctly 1`] = ` ...@@ -107,7 +107,13 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
> >
<path <path
clip-rule="evenodd" clip-rule="evenodd"
d="M28.541 24C28.541 23.6075 28.7998 23.264 29.1711 23.1367C35.8814 20.8368 40.9702 14.1754 41.9802 5.99363C42.1832 4.34926 40.8202 3 39.1634 3H8.8369C7.18005 3 5.81706 4.34926 6.02011 5.99363C7.03026 14.1742 12.1192 20.8349 18.8286 23.1357C19.2002 23.2632 19.4593 23.6071 19.4593 24C19.4593 24.3929 19.2002 24.7368 18.8286 24.8643C12.1192 27.1651 7.03026 33.8258 6.02011 42.0064C5.81706 43.6507 7.18005 45 8.8369 45H39.1634C40.8202 45 42.1832 43.6507 41.9802 42.0064C40.9702 33.8246 35.8814 27.1632 29.1711 24.8633C28.7998 24.736 28.541 24.3925 28.541 24Z" d="M43.858 25.3653C45.0968 24.7606 45.0968 22.8141 43.858 22.2094C35.8073 18.2791 29.2954 11.7672 25.3651 3.71643C24.7603 2.47766 22.8139 2.47766 22.2091 3.71643C18.2789 11.7672 11.7669 18.2791 3.71619 22.2094C2.47742 22.8141 2.47742 24.7606 3.71618 25.3653C11.7669 29.2956 18.2789 35.8076 22.2091 43.8583C22.8139 45.097 24.7603 45.097 25.3651 43.8583C29.2954 35.8076 35.8073 29.2956 43.858 25.3653ZM31.6468 24.8374C32.2267 24.2575 32.2267 23.3172 31.6468 22.7373L24.8371 15.9276C24.2572 15.3477 23.317 15.3477 22.7371 15.9276L15.9274 22.7373C15.3475 23.3172 15.3475 24.2575 15.9274 24.8374L22.7371 31.6471C23.317 32.227 24.2572 32.227 24.8371 31.6471L31.6468 24.8374Z"
fill="#FFBF17"
fill-rule="evenodd"
/>
<path
clip-rule="evenodd"
d="M26.9998 24.0001C26.9998 25.6569 25.6567 27.0001 23.9998 27.0001C22.343 27.0001 20.9998 25.6569 20.9998 24.0001C20.9998 22.3432 22.343 21.0001 23.9998 21.0001C25.6567 21.0001 26.9998 22.3432 26.9998 24.0001Z"
fill="#FFBF17" fill="#FFBF17"
fill-rule="evenodd" fill-rule="evenodd"
/> />
...@@ -127,9 +133,9 @@ exports[`AccountSwitcherScreen renders correctly 1`] = ` ...@@ -127,9 +133,9 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
<span <span
class="font_heading _display-inline _boxSizing-border-box _whiteSpace-nowrap _mt-0px _mr-0px _mb-0px _ml-0px _color-843134974 _wordWrap-break-word _fontFamily-299667014 _fontSize-18px _lineHeight-24px _fontWeight-233016202 _maxWidth-10037 _overflowX-hidden _overflowY-hidden _textOverflow-ellipsis _textAlign-center _flexShrink-1" class="font_heading _display-inline _boxSizing-border-box _whiteSpace-nowrap _mt-0px _mr-0px _mb-0px _ml-0px _color-843134974 _wordWrap-break-word _fontFamily-299667014 _fontSize-18px _lineHeight-24px _fontWeight-233016202 _maxWidth-10037 _overflowX-hidden _overflowY-hidden _textOverflow-ellipsis _textAlign-center _flexShrink-1"
data-disable-theme="true" data-disable-theme="true"
data-testid="address-display/name/Tamara Brekke" data-testid="address-display/name/Colin Schowalter Jr."
> >
Tamara Brekke Colin Schowalter Jr.
</span> </span>
</div> </div>
</div> </div>
...@@ -144,7 +150,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = ` ...@@ -144,7 +150,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
class="font_body _display-inline _boxSizing-border-box _whiteSpace-pre-wrap _mt-0px _mr-0px _mb-0px _ml-0px _color-843135005 _fontFamily-299667014 _wordWrap-break-word _fontSize-229441158 _lineHeight-222976511 _fontWeight-233016202" class="font_body _display-inline _boxSizing-border-box _whiteSpace-pre-wrap _mt-0px _mr-0px _mb-0px _ml-0px _color-843135005 _fontFamily-299667014 _wordWrap-break-word _fontSize-229441158 _lineHeight-222976511 _fontWeight-233016202"
data-disable-theme="true" data-disable-theme="true"
> >
0x​e0c6...ea11 0x​9eb6...a2ca
</span> </span>
<svg <svg
fill="none" fill="none"
...@@ -333,7 +339,13 @@ exports[`AccountSwitcherScreen renders correctly 1`] = ` ...@@ -333,7 +339,13 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
> >
<path <path
clip-rule="evenodd" clip-rule="evenodd"
d="M28.541 24C28.541 23.6075 28.7998 23.264 29.1711 23.1367C35.8814 20.8368 40.9702 14.1754 41.9802 5.99363C42.1832 4.34926 40.8202 3 39.1634 3H8.8369C7.18005 3 5.81706 4.34926 6.02011 5.99363C7.03026 14.1742 12.1192 20.8349 18.8286 23.1357C19.2002 23.2632 19.4593 23.6071 19.4593 24C19.4593 24.3929 19.2002 24.7368 18.8286 24.8643C12.1192 27.1651 7.03026 33.8258 6.02011 42.0064C5.81706 43.6507 7.18005 45 8.8369 45H39.1634C40.8202 45 42.1832 43.6507 41.9802 42.0064C40.9702 33.8246 35.8814 27.1632 29.1711 24.8633C28.7998 24.736 28.541 24.3925 28.541 24Z" d="M43.858 25.3653C45.0968 24.7606 45.0968 22.8141 43.858 22.2094C35.8073 18.2791 29.2954 11.7672 25.3651 3.71643C24.7603 2.47766 22.8139 2.47766 22.2091 3.71643C18.2789 11.7672 11.7669 18.2791 3.71619 22.2094C2.47742 22.8141 2.47742 24.7606 3.71618 25.3653C11.7669 29.2956 18.2789 35.8076 22.2091 43.8583C22.8139 45.097 24.7603 45.097 25.3651 43.8583C29.2954 35.8076 35.8073 29.2956 43.858 25.3653ZM31.6468 24.8374C32.2267 24.2575 32.2267 23.3172 31.6468 22.7373L24.8371 15.9276C24.2572 15.3477 23.317 15.3477 22.7371 15.9276L15.9274 22.7373C15.3475 23.3172 15.3475 24.2575 15.9274 24.8374L22.7371 31.6471C23.317 32.227 24.2572 32.227 24.8371 31.6471L31.6468 24.8374Z"
fill="#FFBF17"
fill-rule="evenodd"
/>
<path
clip-rule="evenodd"
d="M26.9998 24.0001C26.9998 25.6569 25.6567 27.0001 23.9998 27.0001C22.343 27.0001 20.9998 25.6569 20.9998 24.0001C20.9998 22.3432 22.343 21.0001 23.9998 21.0001C25.6567 21.0001 26.9998 22.3432 26.9998 24.0001Z"
fill="#FFBF17" fill="#FFBF17"
fill-rule="evenodd" fill-rule="evenodd"
/> />
...@@ -353,9 +365,9 @@ exports[`AccountSwitcherScreen renders correctly 1`] = ` ...@@ -353,9 +365,9 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
<span <span
class="font_heading _display-inline _boxSizing-border-box _whiteSpace-nowrap _mt-0px _mr-0px _mb-0px _ml-0px _color-843134974 _wordWrap-break-word _fontFamily-299667014 _fontSize-18px _lineHeight-24px _fontWeight-233016202 _maxWidth-10037 _overflowX-hidden _overflowY-hidden _textOverflow-ellipsis _textAlign-center _flexShrink-1" class="font_heading _display-inline _boxSizing-border-box _whiteSpace-nowrap _mt-0px _mr-0px _mb-0px _ml-0px _color-843134974 _wordWrap-break-word _fontFamily-299667014 _fontSize-18px _lineHeight-24px _fontWeight-233016202 _maxWidth-10037 _overflowX-hidden _overflowY-hidden _textOverflow-ellipsis _textAlign-center _flexShrink-1"
data-disable-theme="true" data-disable-theme="true"
data-testid="address-display/name/Tamara Brekke" data-testid="address-display/name/Colin Schowalter Jr."
> >
Tamara Brekke Colin Schowalter Jr.
</span> </span>
</div> </div>
</div> </div>
...@@ -370,7 +382,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = ` ...@@ -370,7 +382,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
class="font_body _display-inline _boxSizing-border-box _whiteSpace-pre-wrap _mt-0px _mr-0px _mb-0px _ml-0px _color-843135005 _fontFamily-299667014 _wordWrap-break-word _fontSize-229441158 _lineHeight-222976511 _fontWeight-233016202" class="font_body _display-inline _boxSizing-border-box _whiteSpace-pre-wrap _mt-0px _mr-0px _mb-0px _ml-0px _color-843135005 _fontFamily-299667014 _wordWrap-break-word _fontSize-229441158 _lineHeight-222976511 _fontWeight-233016202"
data-disable-theme="true" data-disable-theme="true"
> >
0x​e0c6...ea11 0x​9eb6...a2ca
</span> </span>
<svg <svg
fill="none" fill="none"
......
...@@ -4,11 +4,11 @@ import { updateDisplayNameFromTab } from 'src/app/features/dapp/actions' ...@@ -4,11 +4,11 @@ import { updateDisplayNameFromTab } from 'src/app/features/dapp/actions'
import { useDappConnectedAccounts, useDappLastChainId } from 'src/app/features/dapp/hooks' import { useDappConnectedAccounts, useDappLastChainId } from 'src/app/features/dapp/hooks'
import { dappStore } from 'src/app/features/dapp/store' import { dappStore } from 'src/app/features/dapp/store'
import { isConnectedAccount } from 'src/app/features/dapp/utils' import { isConnectedAccount } from 'src/app/features/dapp/utils'
import { extractBaseUrl } from 'src/app/features/dappRequests/utils'
import { closePopup, PopupName } from 'src/app/features/popups/slice' import { closePopup, PopupName } from 'src/app/features/popups/slice'
import { backgroundToSidePanelMessageChannel } from 'src/background/messagePassing/messageChannels' import { backgroundToSidePanelMessageChannel } from 'src/background/messagePassing/messageChannels'
import { BackgroundToSidePanelRequestType } from 'src/background/messagePassing/types/requests' import { BackgroundToSidePanelRequestType } from 'src/background/messagePassing/types/requests'
import { UniverseChainId } from 'uniswap/src/types/chains' import { UniverseChainId } from 'uniswap/src/types/chains'
import { extractBaseUrl } from 'utilities/src/format/urls'
import { useActiveAccountAddress } from 'wallet/src/features/wallet/hooks' import { useActiveAccountAddress } from 'wallet/src/features/wallet/hooks'
type DappContextState = { type DappContextState = {
......
...@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next' ...@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
import { useDappLastChainId } from 'src/app/features/dapp/hooks' import { useDappLastChainId } from 'src/app/features/dapp/hooks'
import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext' import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext'
import { DappRequestStoreItem } from 'src/app/features/dappRequests/slice' import { DappRequestStoreItem } from 'src/app/features/dappRequests/slice'
import { DappRequestType } from 'src/app/features/dappRequests/types/DappRequestTypes'
import { Anchor, AnimatePresence, Button, Flex, Text, UniversalImage, UniversalImageResizeMode, styled } from 'ui/src' import { Anchor, AnimatePresence, Button, Flex, Text, UniversalImage, UniversalImageResizeMode, styled } from 'ui/src'
import { borderRadii, iconSizes } from 'ui/src/theme' import { borderRadii, iconSizes } from 'ui/src/theme'
import { GasFeeResult } from 'uniswap/src/features/gas/types' import { GasFeeResult } from 'uniswap/src/features/gas/types'
...@@ -184,7 +185,10 @@ export function DappRequestFooter({ ...@@ -184,7 +185,10 @@ export function DappRequestFooter({
const shouldCloseSidebar = request.isSidebarClosed && totalRequestCount <= 1 const shouldCloseSidebar = request.isSidebarClosed && totalRequestCount <= 1
// Disable submission if no gas fee value // Disable submission if no gas fee value
const isConfirmEnabled = transactionGasFeeResult?.value && hasSufficientGas const isConfirmEnabled =
request.dappRequest.type === DappRequestType.SendTransaction
? transactionGasFeeResult?.value && hasSufficientGas
: true
const handleOnConfirm = useCallback(async () => { const handleOnConfirm = useCallback(async () => {
if (onConfirm) { if (onConfirm) {
......
...@@ -2,6 +2,7 @@ import { memo } from 'react' ...@@ -2,6 +2,7 @@ import { memo } from 'react'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { AnimatedPane, DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent' import { AnimatedPane, DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent'
import { DappRequestCards } from 'src/app/features/dappRequests/DappRequestQueueCards'
import { import {
DappRequestQueueProvider, DappRequestQueueProvider,
useDappRequestQueueContext, useDappRequestQueueContext,
...@@ -178,6 +179,7 @@ function DappRequestQueueContent(): JSX.Element { ...@@ -178,6 +179,7 @@ function DappRequestQueueContent(): JSX.Element {
)} )}
<DappRequest /> <DappRequest />
</Flex> </Flex>
<DappRequestCards />
</Flex> </Flex>
) )
} }
......
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext'
import { useShouldShowBridgingRequestCard } from 'src/app/features/dappRequests/hooks'
import { BRIDGING_BANNER } from 'ui/src/assets'
import { DappRequestCardLoggingName } from 'uniswap/src/features/telemetry/types'
import { CurrencyField } from 'uniswap/src/types/currency'
import { CardType, IntroCard, IntroCardGraphicType, IntroCardProps } from 'wallet/src/components/introCards/IntroCard'
import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext'
import { setHasViewedDappRequestBridgingBanner } from 'wallet/src/features/behaviorHistory/slice'
export function DappRequestCards(): JSX.Element | null {
const { t } = useTranslation()
const dispatch = useDispatch()
const { request, dappUrl, onCancel, totalRequestCount } = useDappRequestQueueContext()
const { navigateToSwapFlow } = useWalletNavigation()
const { numBridgingChains, shouldShowBridgingRequestCard } = useShouldShowBridgingRequestCard(request, dappUrl)
const card = useMemo(
(): IntroCardProps => ({
graphic: {
type: IntroCardGraphicType.Image,
image: BRIDGING_BANNER,
},
title: t('dapp.request.bridge.title'),
description: t('dapp.request.bridge.description', { numChains: numBridgingChains }),
cardType: CardType.Dismissible,
loggingName: DappRequestCardLoggingName.BridgingBanner,
onClose: (): void => {
dispatch(setHasViewedDappRequestBridgingBanner({ dappUrl, hasViewed: true }))
},
onPress: (): void => {
if (request) {
onCancel(request).catch(() => {})
}
dispatch(setHasViewedDappRequestBridgingBanner({ dappUrl, hasViewed: true }))
navigateToSwapFlow({ openTokenSelector: CurrencyField.OUTPUT })
},
containerProps: {
borderWidth: 0,
backgroundColor: '$surface1',
},
}),
[t, numBridgingChains, dispatch, dappUrl, onCancel, request, navigateToSwapFlow],
)
if (!request || !shouldShowBridgingRequestCard || totalRequestCount !== 1) {
return null
}
return <IntroCard {...card} />
}
...@@ -9,12 +9,12 @@ import { ...@@ -9,12 +9,12 @@ import {
} from 'src/app/features/dappRequests/saga' } from 'src/app/features/dappRequests/saga'
import { DappRequestStoreItem } from 'src/app/features/dappRequests/slice' import { DappRequestStoreItem } from 'src/app/features/dappRequests/slice'
import { DappResponseType } from 'src/app/features/dappRequests/types/DappRequestTypes' import { DappResponseType } from 'src/app/features/dappRequests/types/DappRequestTypes'
import { extractBaseUrl } from 'src/app/features/dappRequests/utils'
import { ExtensionState } from 'src/store/extensionReducer' import { ExtensionState } from 'src/store/extensionReducer'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { DappRequestAction } from 'uniswap/src/features/telemetry/types' import { DappRequestAction } from 'uniswap/src/features/telemetry/types'
import { TransactionTypeInfo } from 'uniswap/src/features/transactions/types/transactionDetails' import { TransactionTypeInfo } from 'uniswap/src/features/transactions/types/transactionDetails'
import { extractBaseUrl } from 'utilities/src/format/urls'
import { Account } from 'wallet/src/features/wallet/accounts/types' import { Account } from 'wallet/src/features/wallet/accounts/types'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
......
...@@ -13,7 +13,6 @@ import { ...@@ -13,7 +13,6 @@ import {
GetAccountRequest, GetAccountRequest,
RequestAccountRequest, RequestAccountRequest,
} from 'src/app/features/dappRequests/types/DappRequestTypes' } from 'src/app/features/dappRequests/types/DappRequestTypes'
import { extractBaseUrl } from 'src/app/features/dappRequests/utils'
import { dappResponseMessageChannel } from 'src/background/messagePassing/messageChannels' import { dappResponseMessageChannel } from 'src/background/messagePassing/messageChannels'
import { call, put, select } from 'typed-redux-saga' import { call, put, select } from 'typed-redux-saga'
import { chainIdToHexadecimalString } from 'uniswap/src/features/chains/utils' import { chainIdToHexadecimalString } from 'uniswap/src/features/chains/utils'
...@@ -23,6 +22,7 @@ import { getEnabledChainIdsSaga } from 'uniswap/src/features/settings/saga' ...@@ -23,6 +22,7 @@ import { getEnabledChainIdsSaga } from 'uniswap/src/features/settings/saga'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { UniverseChainId } from 'uniswap/src/types/chains' import { UniverseChainId } from 'uniswap/src/types/chains'
import { extractBaseUrl } from 'utilities/src/format/urls'
import { getProvider } from 'wallet/src/features/wallet/context' import { getProvider } from 'wallet/src/features/wallet/context'
import { selectActiveAccount } from 'wallet/src/features/wallet/selectors' import { selectActiveAccount } from 'wallet/src/features/wallet/selectors'
...@@ -61,6 +61,7 @@ function sendAccountResponseAnalyticsEvent( ...@@ -61,6 +61,7 @@ function sendAccountResponseAnalyticsEvent(
connectedAddresses: accountResponse.connectedAddresses, connectedAddresses: accountResponse.connectedAddresses,
}) })
} }
/** /**
* Gets the active account, and returns the account address, chainId, and providerUrl. * Gets the active account, and returns the account address, chainId, and providerUrl.
* Chain id + provider url are from the last connected chain for the dApp and wallet. If this has not been set, it will be the default chain and provider. * Chain id + provider url are from the last connected chain for the dApp and wallet. If this has not been set, it will be the default chain and provider.
......
import { useMemo } from 'react'
import { useSelector } from 'react-redux'
import { DappRequestStoreItem } from 'src/app/features/dappRequests/slice'
import {
isRequestAccountRequest,
isRequestPermissionsRequest,
} from 'src/app/features/dappRequests/types/DappRequestTypes'
import { getBridgingDappUrls } from 'uniswap/src/features/bridging/constants'
import { useBridgingSupportedChainIds, useNumBridgingChains } from 'uniswap/src/features/bridging/hooks/chains'
import { selectHasViewedDappRequestBridgingBanner } from 'wallet/src/features/behaviorHistory/selectors'
import { WalletState } from 'wallet/src/state/walletReducer'
export function useShouldShowBridgingRequestCard(
request: DappRequestStoreItem | undefined,
dappUrl: string,
): {
numBridgingChains: number
shouldShowBridgingRequestCard: boolean
} {
const numBridgingChains = useNumBridgingChains()
const bridgingChainIds = useBridgingSupportedChainIds()
const bridgingDappUrls = useMemo(() => getBridgingDappUrls(bridgingChainIds), [bridgingChainIds])
const hasViewedDappRequestBridgingBanner = useSelector((state: WalletState) =>
selectHasViewedDappRequestBridgingBanner(state, dappUrl),
)
const isConnectRequest = useMemo(
() =>
(request && (isRequestAccountRequest(request.dappRequest) || isRequestPermissionsRequest(request.dappRequest))) ??
false,
[request],
)
const isBridgingConnectionRequest = useMemo(
() => isConnectRequest && bridgingDappUrls.includes(dappUrl),
[isConnectRequest, bridgingDappUrls, dappUrl],
)
return {
numBridgingChains,
shouldShowBridgingRequestCard: isBridgingConnectionRequest && !hasViewedDappRequestBridgingBanner,
}
}
...@@ -15,7 +15,6 @@ import { ...@@ -15,7 +15,6 @@ import {
RevokePermissionsRequest, RevokePermissionsRequest,
RevokePermissionsResponse, RevokePermissionsResponse,
} from 'src/app/features/dappRequests/types/DappRequestTypes' } from 'src/app/features/dappRequests/types/DappRequestTypes'
import { extractBaseUrl } from 'src/app/features/dappRequests/utils'
import { dappResponseMessageChannel } from 'src/background/messagePassing/messageChannels' import { dappResponseMessageChannel } from 'src/background/messagePassing/messageChannels'
import { Permission } from 'src/contentScript/WindowEthereumRequestTypes' import { Permission } from 'src/contentScript/WindowEthereumRequestTypes'
import { ExtensionEthMethods } from 'src/contentScript/methodHandlers/requestMethods' import { ExtensionEthMethods } from 'src/contentScript/methodHandlers/requestMethods'
...@@ -23,6 +22,7 @@ import { call, put } from 'typed-redux-saga' ...@@ -23,6 +22,7 @@ import { call, put } from 'typed-redux-saga'
import { chainIdToHexadecimalString } from 'uniswap/src/features/chains/utils' import { chainIdToHexadecimalString } from 'uniswap/src/features/chains/utils'
import { pushNotification } from 'uniswap/src/features/notifications/slice' import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types' import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import { extractBaseUrl } from 'utilities/src/format/urls'
export function getPermissions(dappUrl: string | undefined, connectedAddresses: Address[] | undefined): Permission[] { export function getPermissions(dappUrl: string | undefined, connectedAddresses: Address[] | undefined): Permission[] {
const permissions: Permission[] = [] const permissions: Permission[] = []
......
...@@ -21,7 +21,6 @@ import { ...@@ -21,7 +21,6 @@ import {
UniswapOpenSidebarResponse, UniswapOpenSidebarResponse,
} from 'src/app/features/dappRequests/types/DappRequestTypes' } from 'src/app/features/dappRequests/types/DappRequestTypes'
import { HexadecimalNumberSchema } from 'src/app/features/dappRequests/types/utilityTypes' import { HexadecimalNumberSchema } from 'src/app/features/dappRequests/types/utilityTypes'
import { extractBaseUrl } from 'src/app/features/dappRequests/utils'
import { isWalletUnlocked } from 'src/app/hooks/useIsWalletUnlocked' import { isWalletUnlocked } from 'src/app/hooks/useIsWalletUnlocked'
import { AppRoutes, HomeQueryParams } from 'src/app/navigation/constants' import { AppRoutes, HomeQueryParams } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state' import { navigate } from 'src/app/navigation/state'
...@@ -35,6 +34,7 @@ import { ...@@ -35,6 +34,7 @@ import {
TransactionType, TransactionType,
TransactionTypeInfo, TransactionTypeInfo,
} from 'uniswap/src/features/transactions/types/transactionDetails' } from 'uniswap/src/features/transactions/types/transactionDetails'
import { extractBaseUrl } from 'utilities/src/format/urls'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { SendTransactionParams, sendTransaction } from 'wallet/src/features/transactions/sendTransactionSaga' import { SendTransactionParams, sendTransaction } from 'wallet/src/features/transactions/sendTransactionSaga'
import { getProvider, getSignerManager } from 'wallet/src/features/wallet/context' import { getProvider, getSignerManager } from 'wallet/src/features/wallet/context'
......
import { logger } from 'utilities/src/logger/logger'
function parseUrl(url?: string): URL | undefined {
if (!url) {
return undefined
}
try {
return new URL(url)
} catch (error) {
logger.error(error, {
tags: { file: 'dappRequests/utils', function: 'extractBaseUrl' },
extra: { url },
})
return undefined
}
}
/** Returns the url host (doesn't include http or https) */
export function extractUrlHost(url?: string): string | undefined {
return parseUrl(url)?.host
}
/** Returns the url origin (includes http or https) */
export function extractBaseUrl(url?: string): string | undefined {
return parseUrl(url)?.origin
}
...@@ -3,7 +3,6 @@ import { useDispatch } from 'react-redux' ...@@ -3,7 +3,6 @@ import { useDispatch } from 'react-redux'
import { useDappContext } from 'src/app/features/dapp/DappContext' import { useDappContext } from 'src/app/features/dapp/DappContext'
import { removeDappConnection, saveDappChain } from 'src/app/features/dapp/actions' import { removeDappConnection, saveDappChain } from 'src/app/features/dapp/actions'
import { useDappLastChainId } from 'src/app/features/dapp/hooks' import { useDappLastChainId } from 'src/app/features/dapp/hooks'
import { extractUrlHost } from 'src/app/features/dappRequests/utils'
import { PopupName, closePopup } from 'src/app/features/popups/slice' import { PopupName, closePopup } from 'src/app/features/popups/slice'
import { Anchor, Button, Flex, Popover, Separator, Text, getTokenValue } from 'ui/src' import { Anchor, Button, Flex, Popover, Separator, Text, getTokenValue } from 'ui/src'
import { Check, Power } from 'ui/src/components/icons' import { Check, Power } from 'ui/src/components/icons'
...@@ -17,6 +16,7 @@ import { useEnabledChains } from 'uniswap/src/features/settings/hooks' ...@@ -17,6 +16,7 @@ import { useEnabledChains } from 'uniswap/src/features/settings/hooks'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { UniverseChainId } from 'uniswap/src/types/chains' import { UniverseChainId } from 'uniswap/src/types/chains'
import { extractUrlHost } from 'utilities/src/format/urls'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
const BUTTON_OFFSET = 20 const BUTTON_OFFSET = 20
......
...@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' ...@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'
import { useInterfaceBuyNavigator } from 'src/app/features/for/utils' import { useInterfaceBuyNavigator } from 'src/app/features/for/utils'
import { AppRoutes } from 'src/app/navigation/constants' import { AppRoutes } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state' import { navigate } from 'src/app/navigation/state'
import { AnimatePresence, ContextMenu, Flex, Loader } from 'ui/src' import { AnimatePresence, Flex, Loader } from 'ui/src'
import { ShieldCheck } from 'ui/src/components/icons' import { ShieldCheck } from 'ui/src/components/icons'
import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard'
import { InfoLinkModal } from 'uniswap/src/components/modals/InfoLinkModal' import { InfoLinkModal } from 'uniswap/src/components/modals/InfoLinkModal'
...@@ -15,6 +15,7 @@ import { useEnabledChains } from 'uniswap/src/features/settings/hooks' ...@@ -15,6 +15,7 @@ import { useEnabledChains } from 'uniswap/src/features/settings/hooks'
import { ElementName, ModalName, SectionName, WalletEventName } from 'uniswap/src/features/telemetry/constants' import { ElementName, ModalName, SectionName, WalletEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { InformationBanner } from 'wallet/src/components/banners/InformationBanner' import { InformationBanner } from 'wallet/src/components/banners/InformationBanner'
import { ContextMenu } from 'wallet/src/components/menu/ContextMenu'
import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext'
import { isNonPollingRequestInFlight } from 'wallet/src/data/utils' import { isNonPollingRequestInFlight } from 'wallet/src/data/utils'
import { HiddenTokensRow } from 'wallet/src/features/portfolio/HiddenTokensRow' import { HiddenTokensRow } from 'wallet/src/features/portfolio/HiddenTokensRow'
...@@ -211,7 +212,12 @@ const TokenBalanceItemRow = memo(function TokenBalanceItemRow({ item }: { item: ...@@ -211,7 +212,12 @@ const TokenBalanceItemRow = memo(function TokenBalanceItemRow({ item }: { item:
return ( return (
<TokenContextMenu portfolioBalance={portfolioBalance}> <TokenContextMenu portfolioBalance={portfolioBalance}>
<TokenBalanceItem isLoading={isWarmLoading} portfolioBalance={portfolioBalance} onPressToken={onPressToken} /> <TokenBalanceItem
isLoading={isWarmLoading}
portfolioBalanceId={portfolioBalance.id}
currencyInfo={portfolioBalance.currencyInfo}
onPressToken={onPressToken}
/>
</TokenContextMenu> </TokenContextMenu>
) )
}) })
......
import { useCallback } from 'react' import { useCallback } from 'react'
import { UnitagClaimRoutes } from 'src/app/navigation/constants'
import { focusOrCreateUnitagTab } from 'src/app/navigation/utils' import { focusOrCreateUnitagTab } from 'src/app/navigation/utils'
import { Flex } from 'ui/src' import { Flex } from 'ui/src'
import { PollingInterval } from 'uniswap/src/constants/misc' import { PollingInterval } from 'uniswap/src/constants/misc'
...@@ -18,8 +19,8 @@ export function HomeIntroCardStack(): JSX.Element | null { ...@@ -18,8 +19,8 @@ export function HomeIntroCardStack(): JSX.Element | null {
}) })
const navigateToUnitagClaim = useCallback(async () => { const navigateToUnitagClaim = useCallback(async () => {
await focusOrCreateUnitagTab() await focusOrCreateUnitagTab(activeAccount.address, UnitagClaimRoutes.ClaimIntro)
}, []) }, [activeAccount.address])
const { cards } = useSharedIntroCards({ const { cards } = useSharedIntroCards({
navigateToUnitagClaim, navigateToUnitagClaim,
......
...@@ -15,12 +15,16 @@ import { ClaimUnitagContent } from 'wallet/src/features/unitags/ClaimUnitagConte ...@@ -15,12 +15,16 @@ import { ClaimUnitagContent } from 'wallet/src/features/unitags/ClaimUnitagConte
export function ClaimUnitagScreen(): JSX.Element { export function ClaimUnitagScreen(): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const { goToNextStep } = useOnboardingSteps() const { goToNextStep } = useOnboardingSteps()
const { resetOnboardingContextData, getOnboardingAccountAddress } = useOnboardingContext() const { resetOnboardingContextData, getOnboardingAccountAddress, addUnitagClaim } = useOnboardingContext()
const onboardingAccountAddress = getOnboardingAccountAddress() const onboardingAccountAddress = getOnboardingAccountAddress()
const onNextStep = useCallback(async () => { const onComplete = useCallback(
(unitag: string) => {
addUnitagClaim({ username: unitag })
goToNextStep() goToNextStep()
}, [goToNextStep]) },
[goToNextStep, addUnitagClaim],
)
const handleBack = useCallback(() => { const handleBack = useCallback(() => {
// reset the pending mnemonic when going back from password screen // reset the pending mnemonic when going back from password screen
...@@ -44,14 +48,14 @@ export function ClaimUnitagScreen(): JSX.Element { ...@@ -44,14 +48,14 @@ export function ClaimUnitagScreen(): JSX.Element {
subtitle={t('unitags.onboarding.claim.subtitle')} subtitle={t('unitags.onboarding.claim.subtitle')}
title={t('unitags.onboarding.claim.title.choose')} title={t('unitags.onboarding.claim.title.choose')}
onBack={handleBack} onBack={handleBack}
onSkip={onNextStep} onSkip={goToNextStep}
> >
<Flex gap="$spacing16" py="$spacing24" width="100%"> <Flex gap="$spacing16" py="$spacing24" width="100%">
<ClaimUnitagContent <ClaimUnitagContent
animateY={false} animateY={false}
entryPoint={ExtensionOnboardingFlow.New} entryPoint={ExtensionOnboardingFlow.New}
unitagAddress={onboardingAccountAddress} unitagAddress={onboardingAccountAddress}
onComplete={onNextStep} onComplete={onComplete}
/> />
</Flex> </Flex>
</OnboardingScreen> </OnboardingScreen>
......
...@@ -15,15 +15,35 @@ import { iconSizes } from 'ui/src/theme' ...@@ -15,15 +15,35 @@ import { iconSizes } from 'ui/src/theme'
import { uniswapUrls } from 'uniswap/src/constants/urls' import { uniswapUrls } from 'uniswap/src/constants/urls'
import { ExtensionOnboardingFlow } from 'uniswap/src/types/screens/extension' import { ExtensionOnboardingFlow } from 'uniswap/src/types/screens/extension'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { useFinishOnboarding } from 'wallet/src/features/onboarding/OnboardingContext' import { useFinishOnboarding, useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext'
export function Complete({ flow }: { flow?: ExtensionOnboardingFlow }): JSX.Element { export function Complete({
flow,
tryToClaimUnitag,
}: {
flow?: ExtensionOnboardingFlow
tryToClaimUnitag?: boolean
}): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const { getOnboardingAccountAddress, addUnitagClaim, getUnitagClaim } = useOnboardingContext()
const address = getOnboardingAccountAddress()
const existingClaim = getUnitagClaim()
const [unitagClaimAttempted, setUnitagClaimAttempted] = useState(false)
const [openedSideBar, setOpenedSideBar] = useState(false) const [openedSideBar, setOpenedSideBar] = useState(false)
useEffect(() => {
if (!tryToClaimUnitag || !address || unitagClaimAttempted) {
return
}
setUnitagClaimAttempted(true)
if (existingClaim?.username) {
addUnitagClaim({ address, username: existingClaim.username })
}
}, [existingClaim, address, tryToClaimUnitag, unitagClaimAttempted, addUnitagClaim])
// Activates onboarding accounts on component mount // Activates onboarding accounts on component mount
useFinishOnboarding(terminateStoreSynchronization, flow) useFinishOnboarding(terminateStoreSynchronization, flow, tryToClaimUnitag && !unitagClaimAttempted)
useEffect(() => { useEffect(() => {
const onSidebarOpenedListener = onboardingMessageChannel.addMessageListener( const onSidebarOpenedListener = onboardingMessageChannel.addMessageListener(
......
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import { terminateStoreSynchronization } from 'src/store/storeSynchronization' import { terminateStoreSynchronization } from 'src/store/storeSynchronization'
import { Flex, Text } from 'ui/src' import { Flex, Text } from 'ui/src'
import { Check, GraduationCap } from 'ui/src/components/icons' import { Check, GraduationCap } from 'ui/src/components/icons'
import { uniswapUrls } from 'uniswap/src/constants/urls'
import { useFinishOnboarding } from 'wallet/src/features/onboarding/OnboardingContext' import { useFinishOnboarding } from 'wallet/src/features/onboarding/OnboardingContext'
export function ResetComplete(): JSX.Element { export function ResetComplete(): JSX.Element {
...@@ -22,12 +24,18 @@ export function ResetComplete(): JSX.Element { ...@@ -22,12 +24,18 @@ export function ResetComplete(): JSX.Element {
{t('onboarding.resetPassword.complete.subtitle')} {t('onboarding.resetPassword.complete.subtitle')}
</Text> </Text>
</Flex> </Flex>
<Link
style={{ textDecoration: 'none' }}
target="_blank"
to={uniswapUrls.helpArticleUrls.walletSecurityMeasures}
>
<Flex row alignItems="center" gap="$spacing8"> <Flex row alignItems="center" gap="$spacing8">
<GraduationCap color="$neutral3" size="$icon.20" /> <GraduationCap color="$neutral3" size="$icon.20" />
<Text color="$neutral3" variant="buttonLabel2"> <Text color="$neutral3" variant="buttonLabel2">
{t('onboarding.resetPassword.complete.safety')} {t('onboarding.resetPassword.complete.safety')}
</Text> </Text>
</Flex> </Flex>
</Link>
</Flex> </Flex>
</> </>
) )
......
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { saveDappConnection } from 'src/app/features/dapp/actions'
import { useDappContext } from 'src/app/features/dapp/DappContext' import { useDappContext } from 'src/app/features/dapp/DappContext'
import { extractUrlHost } from 'src/app/features/dappRequests/utils' import { saveDappConnection } from 'src/app/features/dapp/actions'
import { Anchor, Button, Flex, Popover, Separator, Text, TouchableArea } from 'ui/src' import { Anchor, Button, Flex, Popover, Separator, Text, TouchableArea } from 'ui/src'
import { X } from 'ui/src/components/icons' import { X } from 'ui/src/components/icons'
import { uniswapUrls } from 'uniswap/src/constants/urls' import { uniswapUrls } from 'uniswap/src/constants/urls'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { extractUrlHost } from 'utilities/src/format/urls'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
export function ConnectPopupContent({ export function ConnectPopupContent({
......
...@@ -35,7 +35,7 @@ export function RecipientPanel({ chainId }: RecipientPanelProps): JSX.Element { ...@@ -35,7 +35,7 @@ export function RecipientPanel({ chainId }: RecipientPanelProps): JSX.Element {
onSetShowRecipientSelector(!showRecipientSelector) onSetShowRecipientSelector(!showRecipientSelector)
}, [onSetShowRecipientSelector, showRecipientSelector]) }, [onSetShowRecipientSelector, showRecipientSelector])
const sections = useFilteredRecipientSections(pattern) const { sections } = useFilteredRecipientSections(pattern)
const onSelectRecipient = useCallback((newRecipient: string) => { const onSelectRecipient = useCallback((newRecipient: string) => {
setSelectedRecipient(newRecipient) setSelectedRecipient(newRecipient)
......
...@@ -5,7 +5,6 @@ import { ScreenHeader } from 'src/app/components/layout/ScreenHeader' ...@@ -5,7 +5,6 @@ import { ScreenHeader } from 'src/app/components/layout/ScreenHeader'
import { removeDappConnection } from 'src/app/features/dapp/actions' import { removeDappConnection } from 'src/app/features/dapp/actions'
import { useAllDappConnectionsForActiveAccount } from 'src/app/features/dapp/hooks' import { useAllDappConnectionsForActiveAccount } from 'src/app/features/dapp/hooks'
import { dappStore } from 'src/app/features/dapp/store' import { dappStore } from 'src/app/features/dapp/store'
import { extractUrlHost } from 'src/app/features/dappRequests/utils'
import { EllipsisDropdown } from 'src/app/features/settings/SettingsManageConnectionsScreen/internal/EllipsisDropdown' import { EllipsisDropdown } from 'src/app/features/settings/SettingsManageConnectionsScreen/internal/EllipsisDropdown'
import { NoDappConnections } from 'src/app/features/settings/SettingsManageConnectionsScreen/internal/NoDappConnections' import { NoDappConnections } from 'src/app/features/settings/SettingsManageConnectionsScreen/internal/NoDappConnections'
import { Flex, Text, TouchableArea, UniversalImage, useSporeColors } from 'ui/src' import { Flex, Text, TouchableArea, UniversalImage, useSporeColors } from 'ui/src'
...@@ -18,6 +17,7 @@ import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' ...@@ -18,6 +17,7 @@ import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { ExtensionScreens } from 'uniswap/src/types/screens/extension' import { ExtensionScreens } from 'uniswap/src/types/screens/extension'
import { extractNameFromUrl } from 'utilities/src/format/extractNameFromUrl' import { extractNameFromUrl } from 'utilities/src/format/extractNameFromUrl'
import { extractUrlHost } from 'utilities/src/format/urls'
import { DappIconPlaceholder } from 'wallet/src/components/WalletConnect/DappIconPlaceholder' import { DappIconPlaceholder } from 'wallet/src/components/WalletConnect/DappIconPlaceholder'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
......
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { removeAllDappConnectionsForAccount } from 'src/app/features/dapp/actions' import { removeAllDappConnectionsForAccount } from 'src/app/features/dapp/actions'
import { ContextMenu, Flex, TouchableArea } from 'ui/src' import { Flex, TouchableArea } from 'ui/src'
import { Ellipsis, Power } from 'ui/src/components/icons' import { Ellipsis, Power } from 'ui/src/components/icons'
import { pushNotification } from 'uniswap/src/features/notifications/slice' import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types' import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { ContextMenu } from 'wallet/src/components/menu/ContextMenu'
import { useActiveAccountAddress, useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' import { useActiveAccountAddress, useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
const PowerCircle = (): JSX.Element => ( const PowerCircle = (): JSX.Element => (
......
...@@ -37,7 +37,7 @@ export function SettingsRecoveryPhrase({ ...@@ -37,7 +37,7 @@ export function SettingsRecoveryPhrase({
</Flex> </Flex>
</Flex> </Flex>
<Flex grow>{children}</Flex> <Flex grow>{children}</Flex>
<Flex> <Flex mt="$spacing12">
<Button <Button
disabled={!nextButtonEnabled} disabled={!nextButtonEnabled}
flexGrow={1} flexGrow={1}
......
...@@ -182,9 +182,8 @@ export function SettingsScreen(): JSX.Element { ...@@ -182,9 +182,8 @@ export function SettingsScreen(): JSX.Element {
/> />
<SettingsToggleRow <SettingsToggleRow
Icon={ShieldQuestion} Icon={ShieldQuestion}
checked={hideSpamTokens && !isTestnetModeEnabled} checked={hideSpamTokens}
title={t('settings.setting.unknownTokens.title')} title={t('settings.setting.unknownTokens.title')}
disabled={isTestnetModeEnabled}
onCheckedChange={handleSpamTokensToggle} onCheckedChange={handleSpamTokensToggle}
/> />
<SettingsItem <SettingsItem
......
import { useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { OnboardingScreen } from 'src/app/features/onboarding/OnboardingScreen' import { OnboardingScreen } from 'src/app/features/onboarding/OnboardingScreen'
import { useOnboardingSteps } from 'src/app/features/onboarding/OnboardingStepsContext' import { useOnboardingSteps } from 'src/app/features/onboarding/OnboardingStepsContext'
import { AnimatePresence, ContextMenu, Flex, MenuContentItem } from 'ui/src' import { UnitagClaimRoutes } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state'
import { backgroundToSidePanelMessageChannel } from 'src/background/messagePassing/messageChannels'
import { BackgroundToSidePanelRequestType } from 'src/background/messagePassing/types/requests'
import { AnimatePresence, Flex } from 'ui/src'
import { Edit, Ellipsis, Trash } from 'ui/src/components/icons' import { Edit, Ellipsis, Trash } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import Trace from 'uniswap/src/features/telemetry/Trace' import Trace from 'uniswap/src/features/telemetry/Trace'
import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks' import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks'
import { UnitagScreens } from 'uniswap/src/types/screens/mobile' import { UnitagScreens } from 'uniswap/src/types/screens/mobile'
import { ContextMenu } from 'wallet/src/components/menu/ContextMenu'
import { MenuContentItem } from 'wallet/src/components/menu/types'
import { ChangeUnitagModal } from 'wallet/src/features/unitags/ChangeUnitagModal' import { ChangeUnitagModal } from 'wallet/src/features/unitags/ChangeUnitagModal'
import { DeleteUnitagModal } from 'wallet/src/features/unitags/DeleteUnitagModal' import { DeleteUnitagModal } from 'wallet/src/features/unitags/DeleteUnitagModal'
import { EditUnitagProfileContent } from 'wallet/src/features/unitags/EditUnitagProfileContent' import { EditUnitagProfileContent } from 'wallet/src/features/unitags/EditUnitagProfileContent'
import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' import { useAccountAddressFromUrlWithThrow } from 'wallet/src/features/wallet/hooks'
export function EditUnitagProfileScreen({ enableBack = false }: { enableBack?: boolean }): JSX.Element { export function EditUnitagProfileScreen({ enableBack = false }: { enableBack?: boolean }): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const address = useActiveAccountAddressWithThrow() const address = useAccountAddressFromUrlWithThrow()
const { unitag: retrievedUnitag } = useUnitagByAddress(address) const { unitag: retrievedUnitag, pending, fetching } = useUnitagByAddress(address)
const unitag = retrievedUnitag?.username const unitag = retrievedUnitag?.username
useEffect(() => {
if (!pending && !fetching && !unitag) {
navigate(UnitagClaimRoutes.ClaimIntro)
}
}, [unitag, pending, fetching])
const { goToPreviousStep } = useOnboardingSteps() const { goToPreviousStep } = useOnboardingSteps()
const [showDeleteUnitagModal, setShowDeleteUnitagModal] = useState(false) const [showDeleteUnitagModal, setShowDeleteUnitagModal] = useState(false)
...@@ -40,6 +52,12 @@ export function EditUnitagProfileScreen({ enableBack = false }: { enableBack?: b ...@@ -40,6 +52,12 @@ export function EditUnitagProfileScreen({ enableBack = false }: { enableBack?: b
] ]
}, [t, setShowChangeUnitagModal, setShowDeleteUnitagModal]) }, [t, setShowChangeUnitagModal, setShowDeleteUnitagModal])
const refreshUnitags = async (): Promise<void> => {
await backgroundToSidePanelMessageChannel.sendMessage({
type: BackgroundToSidePanelRequestType.RefreshUnitags,
})
}
return ( return (
<Trace logImpression screen={UnitagScreens.EditProfile}> <Trace logImpression screen={UnitagScreens.EditProfile}>
<OnboardingScreen <OnboardingScreen
...@@ -63,6 +81,7 @@ export function EditUnitagProfileScreen({ enableBack = false }: { enableBack?: b ...@@ -63,6 +81,7 @@ export function EditUnitagProfileScreen({ enableBack = false }: { enableBack?: b
<DeleteUnitagModal <DeleteUnitagModal
address={address} address={address}
unitag={unitag} unitag={unitag}
onSuccess={refreshUnitags}
onClose={(): void => setShowDeleteUnitagModal(false)} onClose={(): void => setShowDeleteUnitagModal(false)}
/> />
)} )}
...@@ -70,6 +89,7 @@ export function EditUnitagProfileScreen({ enableBack = false }: { enableBack?: b ...@@ -70,6 +89,7 @@ export function EditUnitagProfileScreen({ enableBack = false }: { enableBack?: b
<ChangeUnitagModal <ChangeUnitagModal
address={address} address={address}
unitag={unitag} unitag={unitag}
onSuccess={refreshUnitags}
onClose={(): void => setShowChangeUnitagModal(false)} onClose={(): void => setShowChangeUnitagModal(false)}
/> />
)} )}
......
...@@ -13,12 +13,12 @@ import { ExtensionUnitagClaimScreens } from 'uniswap/src/types/screens/extension ...@@ -13,12 +13,12 @@ import { ExtensionUnitagClaimScreens } from 'uniswap/src/types/screens/extension
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { extensionNftModalProps } from 'wallet/src/features/unitags/ChooseNftModal' import { extensionNftModalProps } from 'wallet/src/features/unitags/ChooseNftModal'
import { UnitagChooseProfilePicContent } from 'wallet/src/features/unitags/UnitagChooseProfilePicContent' import { UnitagChooseProfilePicContent } from 'wallet/src/features/unitags/UnitagChooseProfilePicContent'
import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' import { useAccountAddressFromUrlWithThrow } from 'wallet/src/features/wallet/hooks'
export function UnitagChooseProfilePicScreen(): JSX.Element { export function UnitagChooseProfilePicScreen(): JSX.Element {
const { goToNextStep, goToPreviousStep } = useOnboardingSteps() const { goToNextStep, goToPreviousStep } = useOnboardingSteps()
const { unitag, entryPoint, setProfilePicUri } = useUnitagClaimContext() const { unitag, entryPoint, setProfilePicUri } = useUnitagClaimContext()
const address = useActiveAccountAddressWithThrow() const address = useAccountAddressFromUrlWithThrow()
const onNavigateContinue = useCallback( const onNavigateContinue = useCallback(
async (imageUri: string | undefined) => { async (imageUri: string | undefined) => {
...@@ -51,8 +51,8 @@ export function UnitagChooseProfilePicScreen(): JSX.Element { ...@@ -51,8 +51,8 @@ export function UnitagChooseProfilePicScreen(): JSX.Element {
<Person color="$neutral1" size={iconSizes.icon24} /> <Person color="$neutral1" size={iconSizes.icon24} />
</Square> </Square>
} }
title={t('unitags.onboarding.claim.title.choose')} title={t('unitags.onboarding.profile.title')}
subtitle={t('unitags.onboarding.claim.subtitle')} subtitle={t('unitags.onboarding.profile.subtitle')}
onBack={goToPreviousStep} onBack={goToPreviousStep}
> >
<Flex gap="$spacing24" pt="$spacing24" width="100%"> <Flex gap="$spacing24" pt="$spacing24" width="100%">
......
import { PropsWithChildren } from 'react'
import { Flex, Image, ImageProps, useIsDarkMode, useWindowDimensions } from 'ui/src'
import {
UNITAGS_ADRIAN_DARK,
UNITAGS_ADRIAN_LIGHT,
UNITAGS_ANDREW_DARK,
UNITAGS_ANDREW_LIGHT,
UNITAGS_BRYAN_DARK,
UNITAGS_BRYAN_LIGHT,
UNITAGS_CALLIL_DARK,
UNITAGS_CALLIL_LIGHT,
UNITAGS_FRED_DARK,
UNITAGS_FRED_LIGHT,
UNITAGS_MAGGIE_DARK,
UNITAGS_MAGGIE_LIGHT,
UNITAGS_PHIL_DARK,
UNITAGS_PHIL_LIGHT,
UNITAGS_SPENCER_DARK,
UNITAGS_SPENCER_LIGHT,
} from 'ui/src/assets'
import { zIndices } from 'ui/src/theme'
// Makes it easier to change later if needed
const MODIFIER = 1
// TODO WALL-5162 replace this static background with one using unitag components and interactable
export function UnitagClaimBackground({ children, blurAll }: PropsWithChildren<{ blurAll: boolean }>): JSX.Element {
const isDarkMode = useIsDarkMode()
const { height, width } = useWindowDimensions()
const heightFactor = height * MODIFIER
const widthFactor = width * MODIFIER
const blurAllValue = 'blur(10px)'
const imageProps: ImageProps = {
position: 'absolute',
objectFit: 'contain',
resizeMode: 'contain',
filter: blurAll ? blurAllValue : undefined,
}
return (
<Flex height="100%" width="100%">
<Flex centered height="100%" width="100%" zIndex={zIndices.default}>
{children}
</Flex>
<Flex position="absolute" height="100%" width="100%" zIndex={zIndices.background}>
<Image
{...imageProps}
src={isDarkMode ? UNITAGS_MAGGIE_DARK : UNITAGS_MAGGIE_LIGHT}
height={0.188 * heightFactor}
width={0.253 * widthFactor}
top={-0.045 * heightFactor}
left={-0.015 * widthFactor}
/>
<Image
{...imageProps}
src={isDarkMode ? UNITAGS_SPENCER_DARK : UNITAGS_SPENCER_LIGHT}
height={0.166 * heightFactor}
width={0.239 * widthFactor}
top={0.057 * heightFactor}
ml="auto"
mr="auto"
right={0}
left={0}
transform={`translate(${0.005 * widthFactor}px, 0px)`}
/>
<Image
{...imageProps}
src={isDarkMode ? UNITAGS_ADRIAN_DARK : UNITAGS_ADRIAN_LIGHT}
height={0.203 * heightFactor}
width={0.248 * widthFactor}
top={-0.05 * heightFactor}
right={-0.072 * widthFactor}
/>
<Image
{...imageProps}
src={isDarkMode ? UNITAGS_ANDREW_DARK : UNITAGS_ANDREW_LIGHT}
height={0.214 * heightFactor}
width={0.26 * widthFactor}
top={0}
bottom={0}
mt="auto"
mb="auto"
left={-0.15 * widthFactor}
filter={blurAll ? blurAllValue : 'blur(2px)'}
/>
<Image
{...imageProps}
src={isDarkMode ? UNITAGS_CALLIL_DARK : UNITAGS_CALLIL_LIGHT}
height={0.189 * heightFactor}
width={0.206 * widthFactor}
bottom={0.05 * heightFactor}
left={-0.01 * widthFactor}
/>
<Image
{...imageProps}
src={isDarkMode ? UNITAGS_PHIL_DARK : UNITAGS_PHIL_LIGHT}
height={0.19 * heightFactor}
width={0.266 * widthFactor}
bottom={-0.08 * heightFactor}
ml="auto"
mr="auto"
right={0}
left={0}
transform={`translate(${-0.015 * widthFactor}px, 0px)`}
filter={blurAll ? blurAllValue : 'blur(2px)'}
/>
<Image
{...imageProps}
src={isDarkMode ? UNITAGS_FRED_DARK : UNITAGS_FRED_LIGHT}
height={0.206 * heightFactor}
width={0.209 * widthFactor}
bottom={0.044 * heightFactor}
right={-0.009 * widthFactor}
/>
<Image
{...imageProps}
src={isDarkMode ? UNITAGS_BRYAN_DARK : UNITAGS_BRYAN_LIGHT}
height={0.206 * heightFactor}
width={0.209 * widthFactor}
top={0}
bottom={0}
mt="auto"
mb="auto"
right={-0.085 * widthFactor}
transform={`translate(0px, ${0.012 * heightFactor}px)`}
filter={blurAll ? blurAllValue : 'blur(4px)'}
/>
</Flex>
</Flex>
)
}
...@@ -8,12 +8,12 @@ import { Button, Flex, Text } from 'ui/src' ...@@ -8,12 +8,12 @@ import { Button, Flex, Text } from 'ui/src'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { UnitagWithProfilePicture } from 'wallet/src/features/unitags/UnitagWithProfilePicture' import { UnitagWithProfilePicture } from 'wallet/src/features/unitags/UnitagWithProfilePicture'
import { UNITAG_SUFFIX } from 'wallet/src/features/unitags/constants' import { UNITAG_SUFFIX } from 'wallet/src/features/unitags/constants'
import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' import { useAccountAddressFromUrlWithThrow } from 'wallet/src/features/wallet/hooks'
export function UnitagConfirmationScreen(): JSX.Element { export function UnitagConfirmationScreen(): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const address = useActiveAccountAddressWithThrow() const address = useAccountAddressFromUrlWithThrow()
const { unitag, profilePicUri } = useUnitagClaimContext() const { unitag, profilePicUri } = useUnitagClaimContext()
const { goToNextStep } = useOnboardingSteps() const { goToNextStep } = useOnboardingSteps()
...@@ -35,7 +35,7 @@ export function UnitagConfirmationScreen(): JSX.Element { ...@@ -35,7 +35,7 @@ export function UnitagConfirmationScreen(): JSX.Element {
return ( return (
<OnboardingScreen> <OnboardingScreen>
<Flex grow gap="$spacing12" pt="$spacing24"> <Flex grow gap="$spacing12" pt="$spacing24">
<Flex centered> <Flex centered py="$spacing12">
<UnitagWithProfilePicture address={address} profilePictureUri={profilePicUri} unitag={unitag} /> <UnitagWithProfilePicture address={address} profilePictureUri={profilePicUri} unitag={unitag} />
</Flex> </Flex>
<Flex centered gap="$spacing12"> <Flex centered gap="$spacing12">
......
...@@ -9,14 +9,14 @@ import { iconSizes } from 'ui/src/theme' ...@@ -9,14 +9,14 @@ import { iconSizes } from 'ui/src/theme'
import Trace from 'uniswap/src/features/telemetry/Trace' import Trace from 'uniswap/src/features/telemetry/Trace'
import { ExtensionScreens, ExtensionUnitagClaimScreens } from 'uniswap/src/types/screens/extension' import { ExtensionScreens, ExtensionUnitagClaimScreens } from 'uniswap/src/types/screens/extension'
import { ClaimUnitagContent, ClaimUnitagContentProps } from 'wallet/src/features/unitags/ClaimUnitagContent' import { ClaimUnitagContent, ClaimUnitagContentProps } from 'wallet/src/features/unitags/ClaimUnitagContent'
import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' import { useAccountAddressFromUrlWithThrow } from 'wallet/src/features/wallet/hooks'
type onNavigateContinueType = Exclude<ClaimUnitagContentProps['onNavigateContinue'], undefined> type onNavigateContinueType = Exclude<ClaimUnitagContentProps['onNavigateContinue'], undefined>
export function UnitagCreateUsernameScreen(): JSX.Element { export function UnitagCreateUsernameScreen(): JSX.Element {
const { goToNextStep, goToPreviousStep } = useOnboardingSteps() const { goToNextStep, goToPreviousStep } = useOnboardingSteps()
const { setUnitag, setEntryPoint } = useUnitagClaimContext() const { setUnitag, setEntryPoint } = useUnitagClaimContext()
const address = useActiveAccountAddressWithThrow() const address = useAccountAddressFromUrlWithThrow()
const onNavigateContinue = useCallback( const onNavigateContinue = useCallback(
({ unitag, entryPoint }: Parameters<onNavigateContinueType>[0]) => { ({ unitag, entryPoint }: Parameters<onNavigateContinueType>[0]) => {
......
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useOnboardingSteps } from 'src/app/features/onboarding/OnboardingStepsContext' import { useOnboardingSteps } from 'src/app/features/onboarding/OnboardingStepsContext'
import { Terms } from 'src/app/features/onboarding/Terms' import { Terms } from 'src/app/features/onboarding/Terms'
import { UnitagClaimRoutes } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state'
import { Button, Flex, GeneratedIcon, Text } from 'ui/src' import { Button, Flex, GeneratedIcon, Text } from 'ui/src'
import { Bolt, Coupon, UserSquare } from 'ui/src/components/icons' import { Bolt, Coupon, UserSquare } from 'ui/src/components/icons'
import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks'
import { useAccountAddressFromUrlWithThrow } from 'wallet/src/features/wallet/hooks'
const CONTAINER_WIDTH = 531 const CONTAINER_WIDTH = 531
const TERMS_WIDTH = 300 const TERMS_WIDTH = 300
...@@ -11,6 +16,15 @@ export function UnitagIntroScreen(): JSX.Element { ...@@ -11,6 +16,15 @@ export function UnitagIntroScreen(): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const { goToNextStep } = useOnboardingSteps() const { goToNextStep } = useOnboardingSteps()
const address = useAccountAddressFromUrlWithThrow()
const { unitag } = useUnitagByAddress(address)
useEffect(() => {
if (unitag?.address) {
navigate(UnitagClaimRoutes.EditProfile)
}
}, [unitag])
return ( return (
<Flex centered height="100%" width="100%"> <Flex centered height="100%" width="100%">
<Flex centered width={CONTAINER_WIDTH} gap="$spacing40"> <Flex centered width={CONTAINER_WIDTH} gap="$spacing40">
......
...@@ -13,6 +13,10 @@ export enum OnboardingRoutes { ...@@ -13,6 +13,10 @@ export enum OnboardingRoutes {
Reset = 'reset', Reset = 'reset',
ResetScan = 'reset-scan', ResetScan = 'reset-scan',
UnsupportedBrowser = 'unsupported-browser', UnsupportedBrowser = 'unsupported-browser',
}
export enum UnitagClaimRoutes {
ClaimIntro = 'claim-intro',
EditProfile = 'edit-profile', EditProfile = 'edit-profile',
} }
......
import { To, matchPath, useLocation } from 'react-router-dom' import { To, matchPath, useLocation } from 'react-router-dom'
import { TopLevelRoutes } from 'src/app/navigation/constants' import { TopLevelRoutes, UnitagClaimRoutes } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state' import { navigate } from 'src/app/navigation/state'
import { onboardingMessageChannel } from 'src/background/messagePassing/messageChannels' import { onboardingMessageChannel } from 'src/background/messagePassing/messageChannels'
import { OnboardingMessageType } from 'src/background/messagePassing/types/ExtensionMessages' import { OnboardingMessageType } from 'src/background/messagePassing/types/ExtensionMessages'
...@@ -68,13 +68,13 @@ export async function focusOrCreateOnboardingTab(page?: string): Promise<void> { ...@@ -68,13 +68,13 @@ export async function focusOrCreateOnboardingTab(page?: string): Promise<void> {
}) })
} }
export async function focusOrCreateUnitagTab(page?: string): Promise<void> { export async function focusOrCreateUnitagTab(address: Address, page: UnitagClaimRoutes): Promise<void> {
const extension = await chrome.management.getSelf() const extension = await chrome.management.getSelf()
const tabs = await chrome.tabs.query({ url: `chrome-extension://${extension.id}/unitagClaim.html*` }) const tabs = await chrome.tabs.query({ url: `chrome-extension://${extension.id}/unitagClaim.html*` })
const tab = tabs[0] const tab = tabs[0]
const url = `unitagClaim.html#/${page ?? ''}` const url = `unitagClaim.html#/${page}?address=${address}`
if (!tab?.id) { if (!tab?.id) {
await chrome.tabs.create({ url }) await chrome.tabs.create({ url })
......
...@@ -2,9 +2,9 @@ import '@tamagui/core/reset.css' ...@@ -2,9 +2,9 @@ import '@tamagui/core/reset.css'
import 'src/app/Global.css' import 'src/app/Global.css'
import 'symbol-observable' // Needed by `reduxed-chrome-storage` as polyfill, order matters import 'symbol-observable' // Needed by `reduxed-chrome-storage` as polyfill, order matters
import { getLocalUserId } from 'src/app/utils/storage'
import { EXTENSION_ORIGIN_APPLICATION } from 'src/app/version' import { EXTENSION_ORIGIN_APPLICATION } from 'src/app/version'
import { uniswapUrls } from 'uniswap/src/constants/urls' import { uniswapUrls } from 'uniswap/src/constants/urls'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { ApplicationTransport } from 'utilities/src/telemetry/analytics/ApplicationTransport' import { ApplicationTransport } from 'utilities/src/telemetry/analytics/ApplicationTransport'
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { analytics, getAnalyticsAtomDirect } from 'utilities/src/telemetry/analytics/analytics' import { analytics, getAnalyticsAtomDirect } from 'utilities/src/telemetry/analytics/analytics'
...@@ -18,6 +18,6 @@ export async function initExtensionAnalytics(): Promise<void> { ...@@ -18,6 +18,6 @@ export async function initExtensionAnalytics(): Promise<void> {
}), }),
analyticsAllowed, analyticsAllowed,
undefined, undefined,
getLocalUserId, getUniqueId,
) )
} }
import { v4 as uuidv4 } from 'uuid'
import { PersistedStorage } from 'wallet/src/utils/persistedStorage'
const STORAGE_AREA_KEY = 'local'
export const USER_ID_KEY = 'USER_ID'
export const LOCAL_STORAGE = new PersistedStorage(STORAGE_AREA_KEY)
export async function getLocalUserId(): Promise<string> {
let userId: string | undefined = await LOCAL_STORAGE.getItem(USER_ID_KEY)
if (userId) {
return userId
}
userId = uuidv4()
await LOCAL_STORAGE.setItem(USER_ID_KEY, userId)
return userId
}
...@@ -24,6 +24,22 @@ export function getSentryEnvironment(): SentryEnvironment { ...@@ -24,6 +24,22 @@ export function getSentryEnvironment(): SentryEnvironment {
return SentryEnvironment.PROD return SentryEnvironment.PROD
} }
export function getDatadogEnvironment(): DatadogEnvironment {
if (isDevEnv()) {
return DatadogEnvironment.DEV
}
if (isBetaEnv()) {
return DatadogEnvironment.BETA
}
return DatadogEnvironment.PROD
}
enum DatadogEnvironment {
DEV = 'dev',
BETA = 'beta',
PROD = 'prod',
}
enum SentryEnvironment { enum SentryEnvironment {
DEV = 'development', DEV = 'development',
BETA = 'beta', BETA = 'beta',
......
...@@ -4,13 +4,13 @@ import { initStatSigForBrowserScripts } from 'src/app/StatsigProvider' ...@@ -4,13 +4,13 @@ import { initStatSigForBrowserScripts } from 'src/app/StatsigProvider'
import { focusOrCreateOnboardingTab } from 'src/app/navigation/utils' import { focusOrCreateOnboardingTab } from 'src/app/navigation/utils'
import { SentryAppNameTag, initSentryForBrowserScripts } from 'src/app/sentry' import { SentryAppNameTag, initSentryForBrowserScripts } from 'src/app/sentry'
import { initExtensionAnalytics } from 'src/app/utils/analytics' import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { getLocalUserId } from 'src/app/utils/storage'
import { initMessageBridge } from 'src/background/backgroundDappRequests' import { initMessageBridge } from 'src/background/backgroundDappRequests'
import { backgroundStore } from 'src/background/backgroundStore' import { backgroundStore } from 'src/background/backgroundStore'
import { backgroundToSidePanelMessageChannel } from 'src/background/messagePassing/messageChannels' import { backgroundToSidePanelMessageChannel } from 'src/background/messagePassing/messageChannels'
import { BackgroundToSidePanelRequestType } from 'src/background/messagePassing/types/requests' import { BackgroundToSidePanelRequestType } from 'src/background/messagePassing/types/requests'
import { setSidePanelBehavior, setSidePanelOptions } from 'src/background/utils/chromeSidePanelUtils' import { setSidePanelBehavior, setSidePanelOptions } from 'src/background/utils/chromeSidePanelUtils'
import { readIsOnboardedFromStorage } from 'src/background/utils/persistedStateUtils' import { readIsOnboardedFromStorage } from 'src/background/utils/persistedStateUtils'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
export const EXTENSION_ID = chrome.runtime.id export const EXTENSION_ID = chrome.runtime.id
...@@ -18,7 +18,7 @@ export const EXTENSION_ID = chrome.runtime.id ...@@ -18,7 +18,7 @@ export const EXTENSION_ID = chrome.runtime.id
initMessageBridge() initMessageBridge()
async function initApp(): Promise<void> { async function initApp(): Promise<void> {
const userId = await getLocalUserId() const userId = await getUniqueId()
initSentryForBrowserScripts(SentryAppNameTag.Background, userId) initSentryForBrowserScripts(SentryAppNameTag.Background, userId)
await initStatSigForBrowserScripts() await initStatSigForBrowserScripts()
await initExtensionAnalytics() await initExtensionAnalytics()
......
...@@ -10,7 +10,6 @@ import { ...@@ -10,7 +10,6 @@ import {
DappResponseType, DappResponseType,
RevokePermissionsRequest, RevokePermissionsRequest,
} from 'src/app/features/dappRequests/types/DappRequestTypes' } from 'src/app/features/dappRequests/types/DappRequestTypes'
import { extractBaseUrl } from 'src/app/features/dappRequests/utils'
import { focusOrCreateOnboardingTab } from 'src/app/navigation/utils' import { focusOrCreateOnboardingTab } from 'src/app/navigation/utils'
import { import {
DappBackgroundPortChannel, DappBackgroundPortChannel,
...@@ -30,6 +29,7 @@ import { hexadecimalStringToInt, toSupportedChainId } from 'uniswap/src/features ...@@ -30,6 +29,7 @@ import { hexadecimalStringToInt, toSupportedChainId } from 'uniswap/src/features
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants/extension' import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants/extension'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { WindowEthereumRequestProperties } from 'uniswap/src/features/telemetry/types' import { WindowEthereumRequestProperties } from 'uniswap/src/features/telemetry/types'
import { extractBaseUrl } from 'utilities/src/format/urls'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
import { walletContextValue } from 'wallet/src/features/wallet/context' import { walletContextValue } from 'wallet/src/features/wallet/context'
......
...@@ -5,7 +5,6 @@ import { ...@@ -5,7 +5,6 @@ import {
DappResponseType, DappResponseType,
SendTransactionRequest, SendTransactionRequest,
} from 'src/app/features/dappRequests/types/DappRequestTypes' } from 'src/app/features/dappRequests/types/DappRequestTypes'
import { extractBaseUrl } from 'src/app/features/dappRequests/utils'
import { import {
contentScriptToBackgroundMessageChannel, contentScriptToBackgroundMessageChannel,
dappResponseMessageChannel, dappResponseMessageChannel,
...@@ -41,6 +40,7 @@ import { WindowEthereumRequest } from 'src/contentScript/types' ...@@ -41,6 +40,7 @@ import { WindowEthereumRequest } from 'src/contentScript/types'
import { chainIdToHexadecimalString } from 'uniswap/src/features/chains/utils' import { chainIdToHexadecimalString } from 'uniswap/src/features/chains/utils'
import { UniverseChainId } from 'uniswap/src/types/chains' import { UniverseChainId } from 'uniswap/src/types/chains'
import { areAddressesEqual } from 'uniswap/src/utils/addresses' import { areAddressesEqual } from 'uniswap/src/utils/addresses'
import { extractBaseUrl } from 'utilities/src/format/urls'
export class ExtensionEthMethodHandler extends BaseMethodHandler<WindowEthereumRequest> { export class ExtensionEthMethodHandler extends BaseMethodHandler<WindowEthereumRequest> {
private readonly requestIdToSourceMap: Map<string, PendingResponseInfo> = new Map() private readonly requestIdToSourceMap: Map<string, PendingResponseInfo> = new Map()
......
...@@ -5,21 +5,21 @@ import React from 'react' ...@@ -5,21 +5,21 @@ import React from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import OnboardingApp from 'src/app/OnboardingApp' import OnboardingApp from 'src/app/OnboardingApp'
import { initializeSentry, SentryAppNameTag } from 'src/app/sentry' import { initializeSentry, SentryAppNameTag } from 'src/app/sentry'
import { getLocalUserId } from 'src/app/utils/storage'
import { initializeReduxStore } from 'src/store/store' import { initializeReduxStore } from 'src/store/store'
import { ExtensionAppLocation, StoreSynchronization } from 'src/store/storeSynchronization' import { ExtensionAppLocation, StoreSynchronization } from 'src/store/storeSynchronization'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any ;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any
// The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem // The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem
// see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326 // see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326
getLocalUserId() getUniqueId()
.then((userId) => { .then((userId) => {
initializeSentry(SentryAppNameTag.Onboarding, userId) initializeSentry(SentryAppNameTag.Onboarding, userId)
}) })
.catch((error) => { .catch((error) => {
logger.error(error, { logger.error(error, {
tags: { file: 'SidebarApp.tsx', function: 'getLocalUserId' }, tags: { file: 'SidebarApp.tsx', function: 'getUniqueId' },
}) })
}) })
async function initOnboarding(): Promise<void> { async function initOnboarding(): Promise<void> {
......
...@@ -5,20 +5,20 @@ import { StrictMode } from 'react' ...@@ -5,20 +5,20 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import PopupApp from 'src/app/PopupApp' import PopupApp from 'src/app/PopupApp'
import { initializeSentry, SentryAppNameTag } from 'src/app/sentry' import { initializeSentry, SentryAppNameTag } from 'src/app/sentry'
import { getLocalUserId } from 'src/app/utils/storage'
import { initializeReduxStore } from 'src/store/store' import { initializeReduxStore } from 'src/store/store'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any ;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any
// The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem // The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem
// see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326 // see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326
getLocalUserId() getUniqueId()
.then((userId) => { .then((userId) => {
initializeSentry(SentryAppNameTag.Popup, userId) initializeSentry(SentryAppNameTag.Popup, userId)
}) })
.catch((error) => { .catch((error) => {
logger.error(error, { logger.error(error, {
tags: { file: 'popup.tsx', function: 'getLocalUserId' }, tags: { file: 'popup.tsx', function: 'getUniqueId' },
}) })
}) })
async function initPopup(): Promise<void> { async function initPopup(): Promise<void> {
......
...@@ -5,20 +5,20 @@ import { StrictMode } from 'react' ...@@ -5,20 +5,20 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import UnitagClaimApp from 'src/app/UnitagClaimApp' import UnitagClaimApp from 'src/app/UnitagClaimApp'
import { SentryAppNameTag, initializeSentry } from 'src/app/sentry' import { SentryAppNameTag, initializeSentry } from 'src/app/sentry'
import { getLocalUserId } from 'src/app/utils/storage'
import { initializeReduxStore } from 'src/store/store' import { initializeReduxStore } from 'src/store/store'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any ;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any
// The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem // The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem
// see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326 // see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326
getLocalUserId() getUniqueId()
.then((userId) => { .then((userId) => {
initializeSentry(SentryAppNameTag.UnitagClaim, userId) initializeSentry(SentryAppNameTag.UnitagClaim, userId)
}) })
.catch((error) => { .catch((error) => {
logger.error(error, { logger.error(error, {
tags: { file: 'unitagClaim.tsx', function: 'getLocalUserId' }, tags: { file: 'unitagClaim.tsx', function: 'getUniqueId' },
}) })
}) })
async function initUnitagClaim(): Promise<void> { async function initUnitagClaim(): Promise<void> {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "Uniswap Extension", "name": "Uniswap Extension",
"description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.", "description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.",
"version": "1.8.0", "version": "1.9.0",
"minimum_chrome_version": "116", "minimum_chrome_version": "116",
"icons": { "icons": {
"16": "assets/icon16.png", "16": "assets/icon16.png",
......
...@@ -13,6 +13,7 @@ import { ...@@ -13,6 +13,7 @@ import {
readDeprecatedReduxedChromeStorage, readDeprecatedReduxedChromeStorage,
} from 'src/store/reduxedChromeStorageToReduxPersistMigration' } from 'src/store/reduxedChromeStorageToReduxPersistMigration'
import { fiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api' import { fiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api'
import { createDatadogReduxEnhancer } from 'utilities/src/logger/Datadog'
import { createStore } from 'wallet/src/state' import { createStore } from 'wallet/src/state'
import { createMigrate } from 'wallet/src/state/createMigrate' import { createMigrate } from 'wallet/src/state/createMigrate'
...@@ -38,6 +39,13 @@ const sentryReduxEnhancer = createReduxEnhancer({ ...@@ -38,6 +39,13 @@ const sentryReduxEnhancer = createReduxEnhancer({
// }, // },
}) })
const dataDogReduxEnhancer = createDatadogReduxEnhancer({
shouldLogReduxState: (state: ExtensionState): boolean => {
// Do not log the state if a user has opted out of analytics.
return !!state.telemetry.allowAnalytics
},
})
const setupStore = (preloadedState?: PreloadedState<ExtensionState>): ReturnType<typeof createStore> => { const setupStore = (preloadedState?: PreloadedState<ExtensionState>): ReturnType<typeof createStore> => {
return createStore({ return createStore({
reducer: persistedReducer, reducer: persistedReducer,
...@@ -45,7 +53,7 @@ const setupStore = (preloadedState?: PreloadedState<ExtensionState>): ReturnType ...@@ -45,7 +53,7 @@ const setupStore = (preloadedState?: PreloadedState<ExtensionState>): ReturnType
additionalSagas: [rootExtensionSaga], additionalSagas: [rootExtensionSaga],
middlewareBefore: __DEV__ ? [loggerMiddleware] : [], middlewareBefore: __DEV__ ? [loggerMiddleware] : [],
middlewareAfter: [fiatOnRampAggregatorApi.middleware], middlewareAfter: [fiatOnRampAggregatorApi.middleware],
enhancers: [sentryReduxEnhancer], enhancers: [sentryReduxEnhancer, dataDogReduxEnhancer],
}) })
} }
......
// same as tsconfig.json but without references which caused performance issues with typescript-eslint
{
"extends": "./tsconfig.json",
"references": []
}
...@@ -2,7 +2,7 @@ module.exports = { ...@@ -2,7 +2,7 @@ module.exports = {
root: true, root: true,
extends: ['@uniswap/eslint-config/native'], extends: ['@uniswap/eslint-config/native'],
parserOptions: { parserOptions: {
project: 'tsconfig.json', project: 'tsconfig.eslint.json',
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
ecmaFeatures: { ecmaFeatures: {
jsx: true, jsx: true,
......
This diff is collapsed.
...@@ -90,9 +90,9 @@ if (isCI && datadogPropertiesAvailable && !isDetox) { ...@@ -90,9 +90,9 @@ if (isCI && datadogPropertiesAvailable && !isDetox) {
apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle" apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle"
} }
def devVersionName = "1.38" def devVersionName = "1.39"
def betaVersionName = "1.38" def betaVersionName = "1.39"
def prodVersionName = "1.38" def prodVersionName = "1.39"
android { android {
ndkVersion rootProject.ext.ndkVersion ndkVersion rootProject.ext.ndkVersion
......
...@@ -183,7 +183,7 @@ class SeedPhraseInputViewModel: ObservableObject { ...@@ -183,7 +183,7 @@ class SeedPhraseInputViewModel: ObservableObject {
error = nil error = nil
} }
let canSubmit = error == nil && mnemonic != "" && firstInvalidWord == "" let canSubmit = error == nil && mnemonic != "" && firstInvalidWord == "" && isValidLength
onInputValidated(["canSubmit": canSubmit]) onInputValidated(["canSubmit": canSubmit])
} }
} }
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
<string>applinks:uniswap.org</string> <string>applinks:uniswap.org</string>
<string>applinks:app.uniswap.org</string> <string>applinks:app.uniswap.org</string>
<string>applinks:app.corn-staging.com</string> <string>applinks:app.corn-staging.com</string>
<string>webcredentials:app.uniswap.org</string>
</array> </array>
<key>com.apple.developer.devicecheck.appattest-environment</key> <key>com.apple.developer.devicecheck.appattest-environment</key>
<string>production</string> <string>production</string>
......
...@@ -90,7 +90,7 @@ ...@@ -90,7 +90,7 @@
"@uniswap/analytics-events": "2.38.0", "@uniswap/analytics-events": "2.38.0",
"@uniswap/client-explore": "0.0.10", "@uniswap/client-explore": "0.0.10",
"@uniswap/ethers-rs-mobile": "0.0.5", "@uniswap/ethers-rs-mobile": "0.0.5",
"@uniswap/sdk-core": "5.8.4", "@uniswap/sdk-core": "5.9.0",
"@walletconnect/core": "2.17.1", "@walletconnect/core": "2.17.1",
"@walletconnect/react-native-compat": "2.17.1", "@walletconnect/react-native-compat": "2.17.1",
"@walletconnect/utils": "2.17.1", "@walletconnect/utils": "2.17.1",
......
...@@ -14,7 +14,7 @@ import { PropsWithChildren, default as React, StrictMode, useCallback, useEffect ...@@ -14,7 +14,7 @@ import { PropsWithChildren, default as React, StrictMode, useCallback, useEffect
import { I18nextProvider } from 'react-i18next' import { I18nextProvider } from 'react-i18next'
import { LogBox, NativeModules, StatusBar } from 'react-native' import { LogBox, NativeModules, StatusBar } from 'react-native'
import appsFlyer from 'react-native-appsflyer' import appsFlyer from 'react-native-appsflyer'
import { getUniqueId } from 'react-native-device-info' import DeviceInfo from 'react-native-device-info'
import { GestureHandlerRootView } from 'react-native-gesture-handler' import { GestureHandlerRootView } from 'react-native-gesture-handler'
import { MMKV } from 'react-native-mmkv' import { MMKV } from 'react-native-mmkv'
import { SafeAreaProvider } from 'react-native-safe-area-context' import { SafeAreaProvider } from 'react-native-safe-area-context'
...@@ -72,8 +72,9 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' ...@@ -72,8 +72,9 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context' import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context'
import i18n from 'uniswap/src/i18n/i18n' import i18n from 'uniswap/src/i18n/i18n'
import { CurrencyId } from 'uniswap/src/types/currency' import { CurrencyId } from 'uniswap/src/types/currency'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { isDetoxBuild, isJestRun } from 'utilities/src/environment/constants' import { isDetoxBuild, isJestRun } from 'utilities/src/environment/constants'
import { attachUnhandledRejectionHandler } from 'utilities/src/logger/Datadog' import { attachUnhandledRejectionHandler, setAttributesToDatadog } from 'utilities/src/logger/Datadog'
import { registerConsoleOverrides } from 'utilities/src/logger/console' import { registerConsoleOverrides } from 'utilities/src/logger/console'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { useAsyncData } from 'utilities/src/react/hooks' import { useAsyncData } from 'utilities/src/react/hooks'
...@@ -170,6 +171,7 @@ function App(): JSX.Element | null { ...@@ -170,6 +171,7 @@ function App(): JSX.Element | null {
// Hence we always initiliase and later close it if Datadog is enabled. // Hence we always initiliase and later close it if Datadog is enabled.
Sentry.close().catch(() => undefined) Sentry.close().catch(() => undefined)
attachUnhandledRejectionHandler() attachUnhandledRejectionHandler()
setAttributesToDatadog({ buildNumber: DeviceInfo.getBuildNumber() }).catch(() => undefined)
} }
}, [isDatadogEnabled]) }, [isDatadogEnabled])
...@@ -246,6 +248,7 @@ function App(): JSX.Element | null { ...@@ -246,6 +248,7 @@ function App(): JSX.Element | null {
function DatadogProviderWrapper({ children }: PropsWithChildren): JSX.Element { function DatadogProviderWrapper({ children }: PropsWithChildren): JSX.Element {
const datadogEnabled = useFeatureFlagWithExposureLoggingDisabled(FeatureFlags.Datadog) const datadogEnabled = useFeatureFlagWithExposureLoggingDisabled(FeatureFlags.Datadog)
logger.setWalletDatadogEnabled(datadogEnabled)
if (isDetoxBuild || isJestRun || !datadogEnabled) { if (isDetoxBuild || isJestRun || !datadogEnabled) {
return <>{children}</> return <>{children}</>
......
...@@ -20,6 +20,7 @@ import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' ...@@ -20,6 +20,7 @@ import { useAppInsets } from 'uniswap/src/hooks/useAppInsets'
import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { areAddressesEqual } from 'uniswap/src/utils/addresses'
import { isAndroid } from 'utilities/src/platform' import { isAndroid } from 'utilities/src/platform'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { PlusCircle } from 'wallet/src/components/icons/PlusCircle' import { PlusCircle } from 'wallet/src/components/icons/PlusCircle'
...@@ -245,7 +246,8 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme ...@@ -245,7 +246,8 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
const accountsWithoutActive = accounts.filter((a) => a.address !== activeAccountAddress) const accountsWithoutActive = accounts.filter((a) => a.address !== activeAccountAddress)
const isViewOnly = accounts.find((a) => a.address === activeAccountAddress)?.type === AccountType.Readonly const isViewOnly =
accounts.find((a) => areAddressesEqual(a.address, activeAccountAddress))?.type === AccountType.Readonly
if (!activeAccountAddress) { if (!activeAccountAddress) {
return null return null
......
...@@ -6,7 +6,8 @@ import { closeModal, openModal } from 'src/features/modals/modalSlice' ...@@ -6,7 +6,8 @@ import { closeModal, openModal } from 'src/features/modals/modalSlice'
import { LockPreviewImage } from 'src/features/onboarding/LockPreviewImage' import { LockPreviewImage } from 'src/features/onboarding/LockPreviewImage'
import { Button, Flex, Text } from 'ui/src' import { Button, Flex, Text } from 'ui/src'
import { Modal } from 'uniswap/src/components/modals/Modal' import { Modal } from 'uniswap/src/components/modals/Modal'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import Trace from 'uniswap/src/features/telemetry/Trace'
import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { setBackupReminderLastSeenTs } from 'wallet/src/features/behaviorHistory/slice' import { setBackupReminderLastSeenTs } from 'wallet/src/features/behaviorHistory/slice'
...@@ -58,6 +59,7 @@ export function BackupReminderModal(): JSX.Element { ...@@ -58,6 +59,7 @@ export function BackupReminderModal(): JSX.Element {
</Text> </Text>
</Flex> </Flex>
<Flex row gap="$spacing8"> <Flex row gap="$spacing8">
<Trace logPress element={ElementName.MaybeLaterButton} modal={ModalName.BackupReminder}>
<Button <Button
alignSelf="center" alignSelf="center"
color="$neutral2" color="$neutral2"
...@@ -68,9 +70,12 @@ export function BackupReminderModal(): JSX.Element { ...@@ -68,9 +70,12 @@ export function BackupReminderModal(): JSX.Element {
> >
{t('common.button.later')} {t('common.button.later')}
</Button> </Button>
</Trace>
<Trace logPress element={ElementName.Continue} modal={ModalName.BackupReminder}>
<Button alignSelf="center" flex={1} size="medium" theme="primary" onPress={onPressBackup}> <Button alignSelf="center" flex={1} size="medium" theme="primary" onPress={onPressBackup}>
{t('common.button.continue')} {t('common.button.continue')}
</Button> </Button>
</Trace>
</Flex> </Flex>
</Flex> </Flex>
</Modal> </Modal>
......
...@@ -4,7 +4,8 @@ import { useDispatch } from 'react-redux' ...@@ -4,7 +4,8 @@ import { useDispatch } from 'react-redux'
import { closeModal, openModal } from 'src/features/modals/modalSlice' import { closeModal, openModal } from 'src/features/modals/modalSlice'
import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal' import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal'
import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types' import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ElementName, ModalName, WalletEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { setBackupReminderLastSeenTs } from 'wallet/src/features/behaviorHistory/slice' import { setBackupReminderLastSeenTs } from 'wallet/src/features/behaviorHistory/slice'
export function BackupWarningModal(): JSX.Element { export function BackupWarningModal(): JSX.Element {
...@@ -17,11 +18,18 @@ export function BackupWarningModal(): JSX.Element { ...@@ -17,11 +18,18 @@ export function BackupWarningModal(): JSX.Element {
} }
const checkForSwipeToDismiss = (): void => { const checkForSwipeToDismiss = (): void => {
if (!closedByButtonRef.current) { const markReminderAsSeen = !closedByButtonRef.current
if (markReminderAsSeen) {
// Modal was swiped to dismiss, should set backup reminder timestamp // Modal was swiped to dismiss, should set backup reminder timestamp
dispatch(setBackupReminderLastSeenTs(Date.now())) dispatch(setBackupReminderLastSeenTs(Date.now()))
} }
sendAnalyticsEvent(WalletEventName.ModalClosed, {
element: ElementName.BackButton,
modal: ModalName.BackupReminderWarning,
markReminderAsSeen,
})
// Reset the ref and close the modal // Reset the ref and close the modal
closedByButtonRef.current = false closedByButtonRef.current = false
onClose() onClose()
......
...@@ -17,7 +17,7 @@ import { processWidgetEvents } from 'src/features/widgets/widgets' ...@@ -17,7 +17,7 @@ import { processWidgetEvents } from 'src/features/widgets/widgets'
import { useSporeColors } from 'ui/src' import { useSporeColors } from 'ui/src'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import Trace from 'uniswap/src/features/telemetry/Trace' import Trace from 'uniswap/src/features/telemetry/Trace'
import { MobileAppScreen } from 'uniswap/src/types/screens/mobile' import { MobileNavScreen } from 'uniswap/src/types/screens/mobile'
import { useAsyncData } from 'utilities/src/react/hooks' import { useAsyncData } from 'utilities/src/react/hooks'
import { sleep } from 'utilities/src/time/timing' import { sleep } from 'utilities/src/time/timing'
...@@ -30,7 +30,7 @@ export const navigationRef = createNavigationContainerRef() ...@@ -30,7 +30,7 @@ export const navigationRef = createNavigationContainerRef()
/** Wrapped `NavigationContainer` with telemetry tracing. */ /** Wrapped `NavigationContainer` with telemetry tracing. */
export const NavigationContainer: FC<PropsWithChildren<Props>> = ({ children, onReady }: PropsWithChildren<Props>) => { export const NavigationContainer: FC<PropsWithChildren<Props>> = ({ children, onReady }: PropsWithChildren<Props>) => {
const colors = useSporeColors() const colors = useSporeColors()
const [routeName, setRouteName] = useState<MobileAppScreen>() const [routeName, setRouteName] = useState<MobileNavScreen>()
const [routeParams, setRouteParams] = useState<Record<string, unknown> | undefined>() const [routeParams, setRouteParams] = useState<Record<string, unknown> | undefined>()
const [logImpression, setLogImpression] = useState<boolean>(false) const [logImpression, setLogImpression] = useState<boolean>(false)
...@@ -51,7 +51,7 @@ export const NavigationContainer: FC<PropsWithChildren<Props>> = ({ children, on ...@@ -51,7 +51,7 @@ export const NavigationContainer: FC<PropsWithChildren<Props>> = ({ children, on
processWidgetEvents().catch(() => undefined) processWidgetEvents().catch(() => undefined)
// setting initial route name for telemetry // setting initial route name for telemetry
const initialRoute = navigationRef.getCurrentRoute()?.name as MobileAppScreen const initialRoute = navigationRef.getCurrentRoute()?.name as MobileNavScreen
setRouteName(initialRoute) setRouteName(initialRoute)
if (!__DEV__) { if (!__DEV__) {
...@@ -60,7 +60,7 @@ export const NavigationContainer: FC<PropsWithChildren<Props>> = ({ children, on ...@@ -60,7 +60,7 @@ export const NavigationContainer: FC<PropsWithChildren<Props>> = ({ children, on
}} }}
onStateChange={(): void => { onStateChange={(): void => {
const previousRouteName = routeName const previousRouteName = routeName
const currentRouteName: MobileAppScreen = navigationRef.getCurrentRoute()?.name as MobileAppScreen const currentRouteName: MobileNavScreen = navigationRef.getCurrentRoute()?.name as MobileNavScreen
if ( if (
currentRouteName && currentRouteName &&
...@@ -69,7 +69,7 @@ export const NavigationContainer: FC<PropsWithChildren<Props>> = ({ children, on ...@@ -69,7 +69,7 @@ export const NavigationContainer: FC<PropsWithChildren<Props>> = ({ children, on
) { ) {
const currentRouteParams = getEventParams( const currentRouteParams = getEventParams(
currentRouteName, currentRouteName,
navigationRef.getCurrentRoute()?.params as RootParamList[MobileAppScreen], navigationRef.getCurrentRoute()?.params as RootParamList[MobileNavScreen],
) )
setLogImpression(true) setLogImpression(true)
setRouteName(currentRouteName) setRouteName(currentRouteName)
......
import { SCREEN_WIDTH } from '@gorhom/bottom-sheet' import { SCREEN_WIDTH } from '@gorhom/bottom-sheet'
import _ from 'lodash' import times from 'lodash/times'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { StyleSheet, Text, View } from 'react-native' import { StyleSheet, Text, View } from 'react-native'
import Animated, { import Animated, {
...@@ -252,9 +252,7 @@ const Numbers = ({ ...@@ -252,9 +252,7 @@ const Numbers = ({
const commaIndex = numberOfDigits.left + Math.floor((numberOfDigits.left - 1) / 3) const commaIndex = numberOfDigits.left + Math.floor((numberOfDigits.left - 1) / 3)
return _.times( return times(numberOfDigits.left + numberOfDigits.right + Math.floor((numberOfDigits.left - 1) / 3) + 1, (index) => (
numberOfDigits.left + numberOfDigits.right + Math.floor((numberOfDigits.left - 1) / 3) + 1,
(index) => (
<Animated.View <Animated.View
key={`$number_${index - commaIndex}`} key={`$number_${index - commaIndex}`}
style={[{ height: DIGIT_HEIGHT }, AnimatedCharStyles.wrapperStyle]} style={[{ height: DIGIT_HEIGHT }, AnimatedCharStyles.wrapperStyle]}
...@@ -270,8 +268,7 @@ const Numbers = ({ ...@@ -270,8 +268,7 @@ const Numbers = ({
shouldAnimate={price.shouldAnimate} shouldAnimate={price.shouldAnimate}
/> />
</Animated.View> </Animated.View>
), ))
)
} }
const LoadingWrapper = (): JSX.Element | null => { const LoadingWrapper = (): JSX.Element | null => {
......
import { maxBy } from 'lodash' import maxBy from 'lodash/maxBy'
import { Dispatch, SetStateAction, useCallback, useMemo, useRef, useState } from 'react' import { Dispatch, SetStateAction, useCallback, useMemo, useRef, useState } from 'react'
import { SharedValue, useDerivedValue } from 'react-native-reanimated' import { SharedValue, useDerivedValue } from 'react-native-reanimated'
import { TLineChartData } from 'react-native-wagmi-charts' import { TLineChartData } from 'react-native-wagmi-charts'
......
...@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next' ...@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import { TextInput } from 'react-native' import { TextInput } from 'react-native'
import { FadeIn, FadeOut } from 'react-native-reanimated' import { FadeIn, FadeOut } from 'react-native-reanimated'
import { RecipientScanModal } from 'src/components/RecipientSelect/RecipientScanModal' import { RecipientScanModal } from 'src/components/RecipientSelect/RecipientScanModal'
import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' import { Flex, Loader, Text, TouchableArea, useSporeColors } from 'ui/src'
import ScanQRIcon from 'ui/src/assets/icons/scan.svg' import ScanQRIcon from 'ui/src/assets/icons/scan.svg'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
...@@ -51,7 +51,7 @@ export function _RecipientSelect({ ...@@ -51,7 +51,7 @@ export function _RecipientSelect({
const [showQRScanner, setShowQRScanner] = useState(false) const [showQRScanner, setShowQRScanner] = useState(false)
const [checkSpeedBumps, setCheckSpeedBumps] = useState(false) const [checkSpeedBumps, setCheckSpeedBumps] = useState(false)
const [selectedRecipient, setSelectedRecipient] = useState(recipient) const [selectedRecipient, setSelectedRecipient] = useState(recipient)
const sections = useFilteredRecipientSections(pattern) const { sections, loading } = useFilteredRecipientSections(pattern)
useEffect(() => { useEffect(() => {
if (focusInput) { if (focusInput) {
...@@ -104,7 +104,9 @@ export function _RecipientSelect({ ...@@ -104,7 +104,9 @@ export function _RecipientSelect({
onBack={recipient ? onHideRecipientSelector : undefined} onBack={recipient ? onHideRecipientSelector : undefined}
onChangeText={setPattern} onChangeText={setPattern}
/> />
{!sections.length ? ( {loading ? (
<Loader.SearchResult />
) : !sections.length ? (
<Flex centered gap="$spacing12" mt="$spacing24" px="$spacing24"> <Flex centered gap="$spacing12" mt="$spacing24" px="$spacing24">
<Text variant="buttonLabel2">{t('send.recipient.results.empty')}</Text> <Text variant="buttonLabel2">{t('send.recipient.results.empty')}</Text>
<Text color="$neutral3" textAlign="center" variant="body1"> <Text color="$neutral3" textAlign="center" variant="body1">
...@@ -112,7 +114,6 @@ export function _RecipientSelect({ ...@@ -112,7 +114,6 @@ export function _RecipientSelect({
</Text> </Text>
</Flex> </Flex>
) : ( ) : (
// Show either suggested recipients or filtered sections based on query
<RecipientList renderedInModal={renderedInModal} sections={sections} onPress={onSelect} /> <RecipientList renderedInModal={renderedInModal} sections={sections} onPress={onSelect} />
)} )}
</AnimatedFlex> </AnimatedFlex>
......
...@@ -20,6 +20,7 @@ import { ElementName, ModalName, WalletEventName } from 'uniswap/src/features/te ...@@ -20,6 +20,7 @@ import { ElementName, ModalName, WalletEventName } from 'uniswap/src/features/te
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { areAddressesEqual } from 'uniswap/src/utils/addresses'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga' import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga'
...@@ -43,7 +44,8 @@ export function RemoveWalletModal(): JSX.Element | null { ...@@ -43,7 +44,8 @@ export function RemoveWalletModal(): JSX.Element | null {
// This happens when user wants to replace mnemonic with a new one // This happens when user wants to replace mnemonic with a new one
const isReplacing = !address const isReplacing = !address
const isRemovingMnemonic = Boolean(associatedAccounts.find((acc) => address === acc.address)) const isRemovingMnemonic = Boolean(associatedAccounts.find((acc) => areAddressesEqual(address, acc.address)))
const isRemovingLastMnemonic = isRemovingMnemonic && associatedAccounts.length === 1 const isRemovingLastMnemonic = isRemovingMnemonic && associatedAccounts.length === 1
const isRemovingRecoveryPhrase = isReplacing || isRemovingLastMnemonic const isRemovingRecoveryPhrase = isReplacing || isRemovingLastMnemonic
......
import React from 'react' import React from 'react'
import { Flex, Separator, Text, Unicon, useSporeColors } from 'ui/src' import { Flex, Separator, Text, Unicon, useSporeColors } from 'ui/src'
import Check from 'ui/src/assets/icons/check.svg' import Check from 'ui/src/assets/icons/check.svg'
import { shortenAddress } from 'uniswap/src/utils/addresses' import { areAddressesEqual, shortenAddress } from 'uniswap/src/utils/addresses'
import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText' import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText'
import { Account } from 'wallet/src/features/wallet/accounts/types' import { Account } from 'wallet/src/features/wallet/accounts/types'
import { useDisplayName } from 'wallet/src/features/wallet/hooks' import { useDisplayName } from 'wallet/src/features/wallet/hooks'
...@@ -32,7 +32,7 @@ export const SwitchAccountOption = ({ account, activeAccount }: Props): JSX.Elem ...@@ -32,7 +32,7 @@ export const SwitchAccountOption = ({ account, activeAccount }: Props): JSX.Elem
</Text> </Text>
</Flex> </Flex>
<Flex height={ICON_SIZE} width={ICON_SIZE}> <Flex height={ICON_SIZE} width={ICON_SIZE}>
{activeAccount?.address === account.address && ( {areAddressesEqual(activeAccount?.address, account.address) && (
<Check color={colors.accent1.get()} height={ICON_SIZE} width={ICON_SIZE} /> <Check color={colors.accent1.get()} height={ICON_SIZE} width={ICON_SIZE} />
)} )}
</Flex> </Flex>
......
...@@ -109,8 +109,8 @@ export function SettingsRow({ ...@@ -109,8 +109,8 @@ export function SettingsRow({
) : screen || modal ? ( ) : screen || modal ? (
<Flex centered row> <Flex centered row>
{currentSetting ? ( {currentSetting ? (
<Flex row shrink alignItems="flex-end" flexBasis="30%" justifyContent="flex-end"> <Flex shrink alignItems="flex-end" flexBasis="35%" justifyContent="flex-end">
<Text adjustsFontSizeToFit color="$neutral2" mr="$spacing8" numberOfLines={1} variant="body3"> <Text adjustsFontSizeToFit color="$neutral2" mr="$spacing8" numberOfLines={2} variant="body3">
{currentSetting} {currentSetting}
</Text> </Text>
</Flex> </Flex>
......
import React, { memo, useMemo } from 'react' import React, { PropsWithChildren, memo, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ContextMenu from 'react-native-context-menu-view' import ContextMenu from 'react-native-context-menu-view'
import { borderRadii } from 'ui/src/theme' import { borderRadii } from 'ui/src/theme'
...@@ -6,13 +6,12 @@ import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generat ...@@ -6,13 +6,12 @@ import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generat
import { PortfolioBalance } from 'uniswap/src/features/dataApi/types' import { PortfolioBalance } from 'uniswap/src/features/dataApi/types'
import { useTokenContextMenu } from 'wallet/src/features/portfolio/useTokenContextMenu' import { useTokenContextMenu } from 'wallet/src/features/portfolio/useTokenContextMenu'
export const TokenBalanceItemContextMenu = memo(function _TokenBalanceItem({ export const TokenBalanceItemContextMenu = memo(function _TokenBalanceItemContextMenu({
portfolioBalance, portfolioBalance,
children, children,
}: { }: PropsWithChildren<{
portfolioBalance: PortfolioBalance portfolioBalance: PortfolioBalance
children: React.ReactNode }>) {
}) {
const { t } = useTranslation() const { t } = useTranslation()
const { menuActions, onContextMenuPress } = useTokenContextMenu({ const { menuActions, onContextMenuPress } = useTokenContextMenu({
......
import { BottomSheetFlatList } from '@gorhom/bottom-sheet' import { BottomSheetFlatList } from '@gorhom/bottom-sheet'
import { useFocusEffect } from '@react-navigation/core' import { useFocusEffect } from '@react-navigation/core'
import { ReactNavigationPerformanceView } from '@shopify/react-native-performance-navigation' import { ReactNavigationPerformanceView } from '@shopify/react-native-performance-navigation'
import { forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { forwardRef, memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FlatList, RefreshControl } from 'react-native' import { FlatList, RefreshControl } from 'react-native'
import Animated, { FadeInDown, FadeOut } from 'react-native-reanimated' import Animated, { FadeInDown, FadeOut } from 'react-native-reanimated'
...@@ -22,6 +22,7 @@ import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' ...@@ -22,6 +22,7 @@ import { useAppInsets } from 'uniswap/src/hooks/useAppInsets'
import { CurrencyId } from 'uniswap/src/types/currency' import { CurrencyId } from 'uniswap/src/types/currency'
import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { isAndroid } from 'utilities/src/platform' import { isAndroid } from 'utilities/src/platform'
import { useValueAsRef } from 'utilities/src/react/useValueAsRef'
import { InformationBanner } from 'wallet/src/components/banners/InformationBanner' import { InformationBanner } from 'wallet/src/components/banners/InformationBanner'
import { isError, isNonPollingRequestInFlight } from 'wallet/src/data/utils' import { isError, isNonPollingRequestInFlight } from 'wallet/src/data/utils'
import { HiddenTokensRow } from 'wallet/src/features/portfolio/HiddenTokensRow' import { HiddenTokensRow } from 'wallet/src/features/portfolio/HiddenTokensRow'
...@@ -72,12 +73,10 @@ export const TokenBalanceListInner = forwardRef<FlatList<TokenBalanceListRow>, T ...@@ -72,12 +73,10 @@ export const TokenBalanceListInner = forwardRef<FlatList<TokenBalanceListRow>, T
}, },
ref, ref,
) { ) {
const { t } = useTranslation()
const colors = useSporeColors() const colors = useSporeColors()
const insets = useAppInsets() const insets = useAppInsets()
const { rows, balancesById, networkStatus, refetch } = useTokenBalanceListContext() const { rows, balancesById } = useTokenBalanceListContext()
const hasError = isError(networkStatus, !!balancesById)
const { onContentSizeChange, adaptiveFooter } = useAdaptiveFooter(containerProps?.contentContainerStyle) const { onContentSizeChange, adaptiveFooter } = useAdaptiveFooter(containerProps?.contentContainerStyle)
...@@ -90,8 +89,7 @@ export const TokenBalanceListInner = forwardRef<FlatList<TokenBalanceListRow>, T ...@@ -90,8 +89,7 @@ export const TokenBalanceListInner = forwardRef<FlatList<TokenBalanceListRow>, T
const [isFocused, setIsFocused] = useState<boolean>(true) const [isFocused, setIsFocused] = useState<boolean>(true)
const [cachedRows, setCachedRows] = useState<TokenBalanceListRow[] | null>(null) const [cachedRows, setCachedRows] = useState<TokenBalanceListRow[] | null>(null)
const rowsRef = useRef(rows) const rowsRef = useValueAsRef(rows)
rowsRef.current = rows
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
...@@ -101,7 +99,7 @@ export const TokenBalanceListInner = forwardRef<FlatList<TokenBalanceListRow>, T ...@@ -101,7 +99,7 @@ export const TokenBalanceListInner = forwardRef<FlatList<TokenBalanceListRow>, T
setCachedRows(rowsRef.current) setCachedRows(rowsRef.current)
setIsFocused(false) setIsFocused(false)
} }
}, []), }, [rowsRef]),
) )
const navigation = useAppStackNavigation() const navigation = useAppStackNavigation()
...@@ -132,49 +130,19 @@ export const TokenBalanceListInner = forwardRef<FlatList<TokenBalanceListRow>, T ...@@ -132,49 +130,19 @@ export const TokenBalanceListInner = forwardRef<FlatList<TokenBalanceListRow>, T
// In order to avoid unnecessary re-renders of the entire FlatList, the `renderItem` function should never change. // In order to avoid unnecessary re-renders of the entire FlatList, the `renderItem` function should never change.
// That's why we use a context provider so that each row can read from there instead of passing down new props every time the data changes. // That's why we use a context provider so that each row can read from there instead of passing down new props every time the data changes.
const renderItem = useCallback( const renderItem = useCallback(
({ item, index }: { item: TokenBalanceListRow; index: number }): JSX.Element => ( ({ item }: { item: TokenBalanceListRow }): JSX.Element => <TokenBalanceItemRow item={item} />,
<TokenBalanceItemRow index={index} item={item} />
),
[], [],
) )
const keyExtractor = useCallback((item: TokenBalanceListRow): string => item, []) const keyExtractor = useCallback((item: TokenBalanceListRow): string => item, [])
const ListEmptyComponent = useMemo(() => { const ListEmptyComponent = useMemo(() => {
if (hasError) { return <EmptyComponent renderEmpty={empty} />
return ( }, [empty])
<Flex pt="$spacing24">
<BaseCard.ErrorState
retryButtonLabel={t('common.button.retry')}
title={t('home.tokens.error.load')}
onRetry={(): void | undefined => refetch?.()}
/>
</Flex>
)
}
if (isNonPollingRequestInFlight(networkStatus)) {
return (
<Flex px="$spacing24">
<Loader.Token withPrice repeat={6} />
</Flex>
)
}
return (
<Flex grow px="$spacing24">
{empty}
</Flex>
)
}, [hasError, empty, t, networkStatus, refetch])
const ListHeaderComponent = useMemo(() => { const ListHeaderComponent = useMemo(() => {
return hasError ? ( return <HeaderComponent />
<AnimatedFlex entering={FadeInDown} exiting={FadeOut} px="$spacing24" py="$spacing8"> }, [])
<BaseCard.InlineErrorState title={t('home.tokens.error.fetch')} onRetry={refetch} />
</AnimatedFlex>
) : null
}, [hasError, refetch, t])
// add negative z index to prevent footer from covering hidden tokens row when minimized // add negative z index to prevent footer from covering hidden tokens row when minimized
const ListFooterComponentStyle = useMemo(() => ({ zIndex: zIndices.negative }), []) const ListFooterComponentStyle = useMemo(() => ({ zIndex: zIndices.negative }), [])
...@@ -232,40 +200,119 @@ export const TokenBalanceListInner = forwardRef<FlatList<TokenBalanceListRow>, T ...@@ -232,40 +200,119 @@ export const TokenBalanceListInner = forwardRef<FlatList<TokenBalanceListRow>, T
}, },
) )
const TokenBalanceItemRow = memo(function TokenBalanceItemRow({ const HeaderComponent = memo(function _HeaderComponent(): JSX.Element | null {
item, const { t } = useTranslation()
index, const { balancesById, networkStatus, refetch } = useTokenBalanceListContext()
const hasError = isError(networkStatus, !!balancesById)
return hasError ? (
<AnimatedFlex entering={FadeInDown} exiting={FadeOut} px="$spacing24" py="$spacing8">
<BaseCard.InlineErrorState title={t('home.tokens.error.fetch')} onRetry={refetch} />
</AnimatedFlex>
) : null
})
const EmptyComponent = memo(function _EmptyComponent({
renderEmpty,
}: { }: {
item: TokenBalanceListRow renderEmpty?: JSX.Element | null
index?: number }): JSX.Element {
}) { const { t } = useTranslation()
const { const { balancesById, networkStatus, refetch } = useTokenBalanceListContext()
balancesById,
hiddenTokensCount, const shouldShowLoaderSkeleton = isNonPollingRequestInFlight(networkStatus)
hiddenTokensExpanded, const hasError = isError(networkStatus, !!balancesById)
isWarmLoading,
onPressToken, if (hasError) {
setHiddenTokensExpanded, return (
} = useTokenBalanceListContext() <Flex pt="$spacing24">
<BaseCard.ErrorState
retryButtonLabel={t('common.button.retry')}
title={t('home.tokens.error.load')}
onRetry={(): void | undefined => refetch?.()}
/>
</Flex>
)
}
if (shouldShowLoaderSkeleton) {
return (
<Flex px="$spacing24">
<Loader.Token withPrice repeat={6} />
</Flex>
)
}
return (
<Flex grow px="$spacing24">
{renderEmpty}
</Flex>
)
})
const TokenBalanceItemRow = memo(function TokenBalanceItemRow({ item }: { item: TokenBalanceListRow }) {
const { balancesById, isWarmLoading, onPressToken } = useTokenBalanceListContext()
const portfolioBalance = balancesById?.[item]
const hasPortfolioBalance = !!portfolioBalance
const tokenBalanceItem = useMemo(() => {
if (!hasPortfolioBalance) {
return null
}
return (
<TokenBalanceItem
padded
portfolioBalanceId={portfolioBalance.id}
isLoading={isWarmLoading}
currencyInfo={portfolioBalance.currencyInfo}
onPressToken={onPressToken}
/>
)
}, [hasPortfolioBalance, portfolioBalance?.id, portfolioBalance?.currencyInfo, isWarmLoading, onPressToken])
if (item === HIDDEN_TOKEN_BALANCES_ROW) {
return <HiddenTokensRowWrapper />
}
if (!portfolioBalance) {
// This can happen when the view is out of focus and the user sells/sends 100% of a token's balance.
// In that case, the token is removed from the `balancesById` object, but the FlatList is still using the cached array of IDs until the view comes back into focus.
// As soon as the view comes back into focus, the FlatList will re-render with the latest data, so users won't really see this Skeleton for more than a few milliseconds when this happens.
return (
<Flex height={ESTIMATED_TOKEN_ITEM_HEIGHT} px="$spacing24">
<Loader.Token />
</Flex>
)
}
return (
<TokenBalanceItemContextMenu portfolioBalance={portfolioBalance}>{tokenBalanceItem}</TokenBalanceItemContextMenu>
)
})
const HiddenTokensRowWrapper = memo(function HiddenTokensRowWrapper(): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const { hiddenTokensCount, hiddenTokensExpanded, setHiddenTokensExpanded } = useTokenBalanceListContext()
const [isModalVisible, setModalVisible] = useState(false) const [isModalVisible, setModalVisible] = useState(false)
const handlePressToken = (): void => { const handlePressToken = useCallback((): void => {
setModalVisible(true) setModalVisible(true)
} }, [])
const closeModal = (): void => { const closeModal = useCallback((): void => {
setModalVisible(false) setModalVisible(false)
} }, [])
const handleAnalytics = (): void => { const handleAnalytics = useCallback((): void => {
sendAnalyticsEvent(WalletEventName.ExternalLinkOpened, { sendAnalyticsEvent(WalletEventName.ExternalLinkOpened, {
url: uniswapUrls.helpArticleUrls.hiddenTokenInfo, url: uniswapUrls.helpArticleUrls.hiddenTokenInfo,
}) })
} }, [])
if (item === HIDDEN_TOKEN_BALANCES_ROW) {
return ( return (
<Flex grow> <Flex grow>
<HiddenTokensRow <HiddenTokensRow
...@@ -302,30 +349,4 @@ const TokenBalanceItemRow = memo(function TokenBalanceItemRow({ ...@@ -302,30 +349,4 @@ const TokenBalanceItemRow = memo(function TokenBalanceItemRow({
/> />
</Flex> </Flex>
) )
}
const portfolioBalance = balancesById?.[item]
if (!portfolioBalance) {
// This can happen when the view is out of focus and the user sells/sends 100% of a token's balance.
// In that case, the token is removed from the `balancesById` object, but the FlatList is still using the cached array of IDs until the view comes back into focus.
// As soon as the view comes back into focus, the FlatList will re-render with the latest data, so users won't really see this Skeleton for more than a few milliseconds when this happens.
return (
<Flex height={ESTIMATED_TOKEN_ITEM_HEIGHT} px="$spacing24">
<Loader.Token />
</Flex>
)
}
return (
<TokenBalanceItemContextMenu portfolioBalance={portfolioBalance}>
<TokenBalanceItem
padded
index={index}
isLoading={isWarmLoading}
portfolioBalance={portfolioBalance}
onPressToken={onPressToken}
/>
</TokenBalanceItemContextMenu>
)
}) })
...@@ -7,6 +7,8 @@ import { ...@@ -7,6 +7,8 @@ import {
TokenDetailsScreenQuery, TokenDetailsScreenQuery,
} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { fromGraphQLChain } from 'uniswap/src/features/chains/utils'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { TestID } from 'uniswap/src/test/fixtures/testIDs'
export interface TokenDetailsHeaderProps { export interface TokenDetailsHeaderProps {
...@@ -20,9 +22,12 @@ export function TokenDetailsHeader({ ...@@ -20,9 +22,12 @@ export function TokenDetailsHeader({
loading = false, loading = false,
onPressWarningIcon, onPressWarningIcon,
}: TokenDetailsHeaderProps): JSX.Element { }: TokenDetailsHeaderProps): JSX.Element {
const tokenProtectionEnabled = useFeatureFlag(FeatureFlags.TokenProtection)
const token = data?.token const token = data?.token
const tokenProject = token?.project const tokenProject = token?.project
const shouldShowWarningIcon =
!tokenProtectionEnabled &&
(tokenProject?.safetyLevel === SafetyLevel.StrongWarning || tokenProject?.safetyLevel === SafetyLevel.Blocked)
return ( return (
<Flex gap="$spacing12" mx="$spacing16"> <Flex gap="$spacing12" mx="$spacing16">
<TokenLogo <TokenLogo
...@@ -42,9 +47,7 @@ export function TokenDetailsHeader({ ...@@ -42,9 +47,7 @@ export function TokenDetailsHeader({
> >
{token?.name ?? ''} {token?.name ?? ''}
</Text> </Text>
{/* Suppress warning icon on low warning level */} {shouldShowWarningIcon && (
{(tokenProject?.safetyLevel === SafetyLevel.StrongWarning ||
tokenProject?.safetyLevel === SafetyLevel.Blocked) && (
<TouchableArea onPress={onPressWarningIcon}> <TouchableArea onPress={onPressWarningIcon}>
<WarningIcon safetyLevel={tokenProject?.safetyLevel} size="$icon.20" strokeColorOverride="$neutral3" /> <WarningIcon safetyLevel={tokenProject?.safetyLevel} size="$icon.20" strokeColorOverride="$neutral3" />
</TouchableArea> </TouchableArea>
......
...@@ -48,7 +48,7 @@ function TokenOptionItemWrapper({ ...@@ -48,7 +48,7 @@ function TokenOptionItemWrapper({
[currencyInfo, balanceUSD, quantity, isUnsupported], [currencyInfo, balanceUSD, quantity, isUnsupported],
) )
const onPress = useCallback(() => onSelectCurrency?.(currency), [currency, onSelectCurrency]) const onPress = useCallback(() => onSelectCurrency?.(currency), [currency, onSelectCurrency])
const { tokenWarningDismissed, onDismissTokenWarning } = useDismissedTokenWarnings(currencyInfo?.currency) const { tokenWarningDismissed } = useDismissedTokenWarnings(currencyInfo?.currency)
const { convertFiatAmountFormatted, formatNumberOrString } = useLocalizationContext() const { convertFiatAmountFormatted, formatNumberOrString } = useLocalizationContext()
if (!option) { if (!option) {
...@@ -62,7 +62,6 @@ function TokenOptionItemWrapper({ ...@@ -62,7 +62,6 @@ function TokenOptionItemWrapper({
return ( return (
<TokenOptionItem <TokenOptionItem
balance={convertFiatAmountFormatted(option.balanceUSD, NumberType.FiatTokenPrice)} balance={convertFiatAmountFormatted(option.balanceUSD, NumberType.FiatTokenPrice)}
dismissWarningCallback={onDismissTokenWarning}
isSelected={isSelected} isSelected={isSelected}
option={option} option={option}
quantity={option.quantity} quantity={option.quantity}
...@@ -149,6 +148,10 @@ function _TokenFiatOnRampList({ ...@@ -149,6 +148,10 @@ function _TokenFiatOnRampList({
return <></> return <></>
} }
if (section.data.length === 0) {
return <></>
}
return ( return (
<Flex mt="$spacing12"> <Flex mt="$spacing12">
<ListSeparatorToggle <ListSeparatorToggle
......
...@@ -132,8 +132,9 @@ describe('TraceUserProperties', () => { ...@@ -132,8 +132,9 @@ describe('TraceUserProperties', () => {
expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.TransactionAuthMethod, AuthMethod.FaceId, undefined) expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.TransactionAuthMethod, AuthMethod.FaceId, undefined)
expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.Language, 'English', undefined) expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.Language, 'English', undefined)
expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.Currency, 'USD', undefined) expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.Currency, 'USD', undefined)
expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.TestnetModeEnabled, false, undefined)
expect(mocked).toHaveBeenCalledTimes(17) expect(mocked).toHaveBeenCalledTimes(18)
}) })
it('sets user properties without active account', async () => { it('sets user properties without active account', async () => {
...@@ -172,7 +173,8 @@ describe('TraceUserProperties', () => { ...@@ -172,7 +173,8 @@ describe('TraceUserProperties', () => {
expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.WalletSignerCount, 0, undefined) expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.WalletSignerCount, 0, undefined)
expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.AppOpenAuthMethod, AuthMethod.None, undefined) expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.AppOpenAuthMethod, AuthMethod.None, undefined)
expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.TransactionAuthMethod, AuthMethod.None, undefined) expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.TransactionAuthMethod, AuthMethod.None, undefined)
expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.TestnetModeEnabled, false, undefined)
expect(mocked).toHaveBeenCalledTimes(11) expect(mocked).toHaveBeenCalledTimes(12)
}) })
}) })
...@@ -7,7 +7,11 @@ import { getFullAppVersion } from 'src/utils/version' ...@@ -7,7 +7,11 @@ import { getFullAppVersion } from 'src/utils/version'
import { useIsDarkMode } from 'ui/src' import { useIsDarkMode } from 'ui/src'
import { useAppFiatCurrency } from 'uniswap/src/features/fiatCurrency/hooks' import { useAppFiatCurrency } from 'uniswap/src/features/fiatCurrency/hooks'
import { useCurrentLanguageInfo } from 'uniswap/src/features/language/hooks' import { useCurrentLanguageInfo } from 'uniswap/src/features/language/hooks'
import { useHideSmallBalancesSetting, useHideSpamTokensSetting } from 'uniswap/src/features/settings/hooks' import {
useEnabledChains,
useHideSmallBalancesSetting,
useHideSpamTokensSetting,
} from 'uniswap/src/features/settings/hooks'
import { MobileUserPropertyName, setUserProperty } from 'uniswap/src/features/telemetry/user' import { MobileUserPropertyName, setUserProperty } from 'uniswap/src/features/telemetry/user'
import { isAndroid } from 'utilities/src/platform' import { isAndroid } from 'utilities/src/platform'
import { selectAllowAnalytics } from 'wallet/src/features/telemetry/selectors' import { selectAllowAnalytics } from 'wallet/src/features/telemetry/selectors'
...@@ -36,6 +40,7 @@ export function TraceUserProperties(): null { ...@@ -36,6 +40,7 @@ export function TraceUserProperties(): null {
const currentFiatCurrency = useAppFiatCurrency() const currentFiatCurrency = useAppFiatCurrency()
const hideSpamTokens = useHideSpamTokensSetting() const hideSpamTokens = useHideSpamTokensSetting()
const hideSmallBalances = useHideSmallBalancesSetting() const hideSmallBalances = useHideSmallBalancesSetting()
const { isTestnetModeEnabled } = useEnabledChains()
// Effects must check this and ensure they are setting properties for when analytics is reenabled // Effects must check this and ensure they are setting properties for when analytics is reenabled
const allowAnalytics = useSelector(selectAllowAnalytics) const allowAnalytics = useSelector(selectAllowAnalytics)
...@@ -111,5 +116,9 @@ export function TraceUserProperties(): null { ...@@ -111,5 +116,9 @@ export function TraceUserProperties(): null {
setUserProperty(MobileUserPropertyName.Currency, currentFiatCurrency) setUserProperty(MobileUserPropertyName.Currency, currentFiatCurrency)
}, [allowAnalytics, currentFiatCurrency]) }, [allowAnalytics, currentFiatCurrency])
useEffect(() => {
setUserProperty(MobileUserPropertyName.TestnetModeEnabled, isTestnetModeEnabled)
}, [allowAnalytics, isTestnetModeEnabled])
return null return null
} }
...@@ -149,7 +149,7 @@ export function ExploreSections({ listRef }: ExploreSectionsProps): JSX.Element ...@@ -149,7 +149,7 @@ export function ExploreSections({ listRef }: ExploreSectionsProps): JSX.Element
visibleHeight={visibleListHeight} visibleHeight={visibleListHeight}
/> />
<Flex row alignItems="center" justifyContent="space-between" px="$spacing20"> <Flex row alignItems="center" justifyContent="space-between" px="$spacing20">
<Text color="$neutral2" flexShrink={0} paddingEnd="$spacing8" variant="subheading2"> <Text color="$neutral2" flexShrink={0} paddingEnd="$spacing8" variant="subheading1">
{t('explore.tokens.top.title')} {t('explore.tokens.top.title')}
</Text> </Text>
<Flex flexShrink={1}> <Flex flexShrink={1}>
...@@ -217,7 +217,7 @@ function NetworkPillsRow({ ...@@ -217,7 +217,7 @@ function NetworkPillsRow({
) )
return ( return (
<Flex py="$spacing16"> <Flex py="$spacing8">
<FlatList <FlatList
horizontal horizontal
ListHeaderComponent={ ListHeaderComponent={
......
import React, { memo, useCallback } from 'react' import React, { memo, useCallback } from 'react'
import { ViewProps } from 'react-native' import { ViewProps } from 'react-native'
import ContextMenu from 'react-native-context-menu-view' import ContextMenu from 'react-native-context-menu-view'
import { FadeIn, SharedValue } from 'react-native-reanimated' import { SharedValue } from 'react-native-reanimated'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks'
import RemoveButton from 'src/components/explore/RemoveButton' import RemoveButton from 'src/components/explore/RemoveButton'
...@@ -125,7 +125,6 @@ function FavoriteTokenCard({ ...@@ -125,7 +125,6 @@ function FavoriteTokenCard({
borderColor={opacify(0.05, colors.surface3.val)} borderColor={opacify(0.05, colors.surface3.val)}
borderRadius="$rounded16" borderRadius="$rounded16"
borderWidth={isDarkMode ? '$none' : '$spacing1'} borderWidth={isDarkMode ? '$none' : '$spacing1'}
entering={FadeIn}
hapticFeedback={!isEditing} hapticFeedback={!isEditing}
hapticStyle={ImpactFeedbackStyle.Light} hapticStyle={ImpactFeedbackStyle.Light}
m="$spacing4" m="$spacing4"
......
...@@ -49,6 +49,8 @@ describe('FavoriteWalletCard', () => { ...@@ -49,6 +49,8 @@ describe('FavoriteWalletCard', () => {
jest.spyOn(unitagHooks, 'useUnitagByAddress').mockReturnValue({ jest.spyOn(unitagHooks, 'useUnitagByAddress').mockReturnValue({
unitag: { username: 'unitagname' }, unitag: { username: 'unitagname' },
loading: false, loading: false,
fetching: false,
pending: false,
}) })
const { queryByText } = render(<FavoriteWalletCard {...defaultProps} />) const { queryByText } = render(<FavoriteWalletCard {...defaultProps} />)
......
...@@ -108,16 +108,10 @@ function _SortButton({ orderBy }: FilterGroupProps): JSX.Element { ...@@ -108,16 +108,10 @@ function _SortButton({ orderBy }: FilterGroupProps): JSX.Element {
}, [MenuItem, dispatch, menuActions]) }, [MenuItem, dispatch, menuActions])
return ( return (
<ActionSheetDropdown <ActionSheetDropdown options={options} showArrow={false} styles={{ alignment: 'right' }}>
options={options}
showArrow={false}
styles={{
alignment: 'right',
}}
testID="chain-selector"
>
<Flex <Flex
row row
centered
backgroundColor="$surface3" backgroundColor="$surface3"
borderRadius="$rounded20" borderRadius="$rounded20"
gap="$spacing4" gap="$spacing4"
...@@ -125,7 +119,7 @@ function _SortButton({ orderBy }: FilterGroupProps): JSX.Element { ...@@ -125,7 +119,7 @@ function _SortButton({ orderBy }: FilterGroupProps): JSX.Element {
pr="$spacing8" pr="$spacing8"
py="$spacing8" py="$spacing8"
> >
<Text ellipse color="$neutral1" flexShrink={1} numberOfLines={1} variant="buttonLabel2"> <Text ellipse color="$neutral1" flexShrink={1} numberOfLines={1} variant="buttonLabel3">
{getTokensOrderBySelectedLabel(orderBy, t)} {getTokensOrderBySelectedLabel(orderBy, t)}
</Text> </Text>
<RotatableChevron color="$neutral2" direction="down" height={iconSizes.icon20} width={iconSizes.icon20} /> <RotatableChevron color="$neutral2" direction="down" height={iconSizes.icon20} width={iconSizes.icon20} />
......
...@@ -119,7 +119,7 @@ export const TokenItem = memo(function _TokenItem({ ...@@ -119,7 +119,7 @@ export const TokenItem = memo(function _TokenItem({
<Flex centered row gap="$spacing4"> <Flex centered row gap="$spacing4">
{!hideNumberedList && ( {!hideNumberedList && (
<Flex minWidth={spacing.spacing16} mr="$spacing8"> <Flex minWidth={spacing.spacing16} mr="$spacing8">
<Text color="$neutral2" variant="buttonLabel2"> <Text color="$neutral2" variant="body3">
{index + 1} {index + 1}
</Text> </Text>
</Flex> </Flex>
......
...@@ -39,11 +39,12 @@ exports[`SortButton renders without error 1`] = ` ...@@ -39,11 +39,12 @@ exports[`SortButton renders without error 1`] = `
"paddingTop": 8, "paddingTop": 8,
} }
} }
testID="chain-selector" testID="dropdown-toggle"
> >
<View <View
style={ style={
{ {
"alignItems": "center",
"backgroundColor": "rgba(34,34,34,0.05)", "backgroundColor": "rgba(34,34,34,0.05)",
"borderBottomLeftRadius": 20, "borderBottomLeftRadius": 20,
"borderBottomRightRadius": 20, "borderBottomRightRadius": 20,
...@@ -51,6 +52,7 @@ exports[`SortButton renders without error 1`] = ` ...@@ -51,6 +52,7 @@ exports[`SortButton renders without error 1`] = `
"borderTopRightRadius": 20, "borderTopRightRadius": 20,
"flexDirection": "row", "flexDirection": "row",
"gap": 4, "gap": 4,
"justifyContent": "center",
"paddingBottom": 8, "paddingBottom": 8,
"paddingLeft": 12, "paddingLeft": 12,
"paddingRight": 8, "paddingRight": 8,
...@@ -68,9 +70,9 @@ exports[`SortButton renders without error 1`] = ` ...@@ -68,9 +70,9 @@ exports[`SortButton renders without error 1`] = `
"color": "#222222", "color": "#222222",
"flexShrink": 1, "flexShrink": 1,
"fontFamily": "Basel Grotesk", "fontFamily": "Basel Grotesk",
"fontSize": 17, "fontSize": 15,
"fontWeight": "500", "fontWeight": "500",
"lineHeight": 19.549999999999997, "lineHeight": 17.25,
} }
} }
suppressHighlighting={true} suppressHighlighting={true}
......
...@@ -73,14 +73,14 @@ exports[`TokenItem renders without error 1`] = ` ...@@ -73,14 +73,14 @@ exports[`TokenItem renders without error 1`] = `
> >
<Text <Text
allowFontScaling={true} allowFontScaling={true}
maxFontSizeMultiplier={1.2} maxFontSizeMultiplier={1.4}
style={ style={
{ {
"color": "#7D7D7D", "color": "#7D7D7D",
"fontFamily": "Basel Grotesk", "fontFamily": "Basel Grotesk",
"fontSize": 17, "fontSize": 15,
"fontWeight": "500", "fontWeight": "400",
"lineHeight": 19.549999999999997, "lineHeight": 20,
} }
} }
suppressHighlighting={true} suppressHighlighting={true}
......
...@@ -206,7 +206,7 @@ describe(useExploreTokenContextMenu, () => { ...@@ -206,7 +206,7 @@ describe(useExploreTokenContextMenu, () => {
payload: { payload: {
name: 'swap-modal', name: 'swap-modal',
initialState: { initialState: {
exactAmountToken: '0', exactAmountToken: '',
exactCurrencyField: 'input', exactCurrencyField: 'input',
[CurrencyField.INPUT]: null, [CurrencyField.INPUT]: null,
[CurrencyField.OUTPUT]: { [CurrencyField.OUTPUT]: {
......
...@@ -59,7 +59,7 @@ export function useExploreTokenContextMenu({ ...@@ -59,7 +59,7 @@ export function useExploreTokenContextMenu({
const onPressSwap = useCallback(() => { const onPressSwap = useCallback(() => {
const swapFormState: TransactionState = { const swapFormState: TransactionState = {
exactCurrencyField: CurrencyField.INPUT, exactCurrencyField: CurrencyField.INPUT,
exactAmountToken: '0', exactAmountToken: '',
[CurrencyField.INPUT]: null, [CurrencyField.INPUT]: null,
[CurrencyField.OUTPUT]: { [CurrencyField.OUTPUT]: {
chainId, chainId,
......
...@@ -4,9 +4,11 @@ import { FlatList, ListRenderItemInfo } from 'react-native' ...@@ -4,9 +4,11 @@ import { FlatList, ListRenderItemInfo } from 'react-native'
import { SearchTokenItem } from 'src/components/explore/search/items/SearchTokenItem' import { SearchTokenItem } from 'src/components/explore/search/items/SearchTokenItem'
import { getSearchResultId } from 'src/components/explore/search/utils' import { getSearchResultId } from 'src/components/explore/search/utils'
import { Flex, Loader } from 'ui/src' import { Flex, Loader } from 'ui/src'
import { ProtectionResult, SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { ALL_NETWORKS_ARG } from 'uniswap/src/data/rest/base' import { ALL_NETWORKS_ARG } from 'uniswap/src/data/rest/base'
import { useTokenRankingsQuery } from 'uniswap/src/data/rest/tokenRankings' import { useTokenRankingsQuery } from 'uniswap/src/data/rest/tokenRankings'
import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { fromGraphQLChain } from 'uniswap/src/features/chains/utils'
import { TokenList } from 'uniswap/src/features/dataApi/types'
import { SearchResultType, TokenSearchResult } from 'uniswap/src/features/search/SearchResult' import { SearchResultType, TokenSearchResult } from 'uniswap/src/features/search/SearchResult'
import { UniverseChainId } from 'uniswap/src/types/chains' import { UniverseChainId } from 'uniswap/src/types/chains'
import { RankingType } from 'wallet/src/features/wallet/types' import { RankingType } from 'wallet/src/features/wallet/types'
...@@ -32,7 +34,14 @@ function tokenStatsToTokenSearchResult(token: Maybe<TokenRankingsStat>): TokenSe ...@@ -32,7 +34,14 @@ function tokenStatsToTokenSearchResult(token: Maybe<TokenRankingsStat>): TokenSe
name, name,
symbol, symbol,
logoUrl: logo ?? null, logoUrl: logo ?? null,
safetyLevel: null, // BE has confirmed that all of these TokenRankingsStat tokens are Verified SafetyLevel, and design confirmed that we can hide the warning icon here
safetyLevel: SafetyLevel.Verified,
safetyInfo: {
tokenList: TokenList.Default,
attackType: undefined,
protectionResult: ProtectionResult.Benign,
},
feeData: null,
} }
} }
......
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { NFTHeaderItem, TokenHeaderItem, WalletHeaderItem } from 'src/components/explore/search/constants'
import { FadeIn, FadeOut } from 'react-native-reanimated'
import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader' import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader'
import { SearchHeader } from 'src/components/explore/search/types'
import { Flex, Loader } from 'ui/src' import { Flex, Loader } from 'ui/src'
import { Coin, Gallery, Person } from 'ui/src/components/icons' import { UniverseChainId } from 'uniswap/src/types/chains'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
export const SearchResultsLoader = (): JSX.Element => { function SectionLoader({ searchHeader, repeat = 1 }: { searchHeader: SearchHeader; repeat?: number }): JSX.Element {
const { t } = useTranslation()
return ( return (
<Flex gap="$spacing16">
<Flex gap="$spacing12"> <Flex gap="$spacing12">
<SectionHeaderText <SectionHeaderText icon={searchHeader.icon} title={searchHeader.title} />
icon={<Coin color="$neutral2" size="$icon.24" />} <Flex mx="$spacing24">
title={t('explore.search.section.tokens')} <Loader.SearchResult repeat={repeat} />
/>
<AnimatedFlex entering={FadeIn} exiting={FadeOut} mx="$spacing24">
<Loader.Token repeat={2} />
</AnimatedFlex>
</Flex> </Flex>
<Flex gap="$spacing12">
<SectionHeaderText
icon={<Person color="$neutral2" size="$icon.24" />}
title={t('explore.search.section.wallets')}
/>
<AnimatedFlex entering={FadeIn} exiting={FadeOut} mx="$spacing24">
<Loader.Token />
</AnimatedFlex>
</Flex>
<Flex gap="$spacing12">
<SectionHeaderText
icon={<Gallery color="$neutral2" size="$icon.24" />}
title={t('explore.search.section.nft')}
/>
<AnimatedFlex entering={FadeIn} exiting={FadeOut} mx="$spacing24">
<Loader.Token repeat={2} />
</AnimatedFlex>
</Flex> </Flex>
)
}
/**
* Placeholder component used while a search is loading.
*/
export function SearchResultsLoader({ selectedChain }: { selectedChain: UniverseChainId | null }): JSX.Element {
// Only mainnet or "all" networks support nfts, hide loader otherwise
const hideNftLoading = selectedChain !== null && selectedChain !== UniverseChainId.Mainnet
return (
<Flex gap="$spacing16">
<SectionLoader searchHeader={TokenHeaderItem} repeat={2} />
<SectionLoader searchHeader={WalletHeaderItem} />
{!hideNftLoading && <SectionLoader searchHeader={NFTHeaderItem} repeat={2} />}
</Flex> </Flex>
) )
} }
...@@ -4,7 +4,13 @@ import { FlatList, ListRenderItemInfo } from 'react-native' ...@@ -4,7 +4,13 @@ import { FlatList, ListRenderItemInfo } from 'react-native'
import { FadeIn, FadeOut } from 'react-native-reanimated' import { FadeIn, FadeOut } from 'react-native-reanimated'
import { SearchResultsLoader } from 'src/components/explore/search/SearchResultsLoader' import { SearchResultsLoader } from 'src/components/explore/search/SearchResultsLoader'
import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader' import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader'
import { SEARCH_RESULT_HEADER_KEY } from 'src/components/explore/search/constants' import {
EtherscanHeaderItem,
NFTHeaderItem,
SEARCH_RESULT_HEADER_KEY,
TokenHeaderItem,
WalletHeaderItem,
} from 'src/components/explore/search/constants'
import { useWalletSearchResults } from 'src/components/explore/search/hooks' import { useWalletSearchResults } from 'src/components/explore/search/hooks'
import { SearchENSAddressItem } from 'src/components/explore/search/items/SearchENSAddressItem' import { SearchENSAddressItem } from 'src/components/explore/search/items/SearchENSAddressItem'
import { SearchEtherscanItem } from 'src/components/explore/search/items/SearchEtherscanItem' import { SearchEtherscanItem } from 'src/components/explore/search/items/SearchEtherscanItem'
...@@ -14,16 +20,13 @@ import { SearchUnitagItem } from 'src/components/explore/search/items/SearchUnit ...@@ -14,16 +20,13 @@ import { SearchUnitagItem } from 'src/components/explore/search/items/SearchUnit
import { SearchWalletByAddressItem } from 'src/components/explore/search/items/SearchWalletByAddressItem' import { SearchWalletByAddressItem } from 'src/components/explore/search/items/SearchWalletByAddressItem'
import { SearchResultOrHeader } from 'src/components/explore/search/types' import { SearchResultOrHeader } from 'src/components/explore/search/types'
import { import {
filterSearchResultsByChainId,
formatNFTCollectionSearchResults, formatNFTCollectionSearchResults,
formatTokenSearchResults, formatTokenSearchResults,
getSearchResultId, getSearchResultId,
} from 'src/components/explore/search/utils' } from 'src/components/explore/search/utils'
import { Flex, Text } from 'ui/src' import { Flex, Text } from 'ui/src'
import { Coin, Gallery, Person } from 'ui/src/components/icons'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard'
import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains'
import { useExploreSearchQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { useExploreSearchQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { SearchContext } from 'uniswap/src/features/search/SearchContext' import { SearchContext } from 'uniswap/src/features/search/SearchContext'
import { import {
...@@ -32,36 +35,10 @@ import { ...@@ -32,36 +35,10 @@ import {
TokenSearchResult, TokenSearchResult,
} from 'uniswap/src/features/search/SearchResult' } from 'uniswap/src/features/search/SearchResult'
import { useEnabledChains } from 'uniswap/src/features/settings/hooks' import { useEnabledChains } from 'uniswap/src/features/settings/hooks'
import i18n from 'uniswap/src/i18n/i18n'
import { UniverseChainId } from 'uniswap/src/types/chains' import { UniverseChainId } from 'uniswap/src/types/chains'
import { getValidAddress } from 'uniswap/src/utils/addresses' import { getValidAddress } from 'uniswap/src/utils/addresses'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
const ICON_SIZE = '$icon.24'
const ICON_COLOR = '$neutral2'
const WalletHeaderItem: SearchResultOrHeader = {
icon: <Person color={ICON_COLOR} size={ICON_SIZE} />,
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('explore.search.section.wallets'),
}
const TokenHeaderItem: SearchResultOrHeader = {
icon: <Coin color={ICON_COLOR} size={ICON_SIZE} />,
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('explore.search.section.tokens'),
}
const NFTHeaderItem: SearchResultOrHeader = {
icon: <Gallery color={ICON_COLOR} size={ICON_SIZE} />,
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('explore.search.section.nft'),
}
const EtherscanHeaderItem: (chainId: UniverseChainId) => SearchResultOrHeader = (chainId: UniverseChainId) => ({
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('explore.search.action.viewEtherscan', {
blockExplorerName: UNIVERSE_CHAIN_INFO[chainId].explorer.name,
}),
})
const IGNORED_ERRORS = ['Subgraph provider undefined not supported'] const IGNORED_ERRORS = ['Subgraph provider undefined not supported']
export function SearchResultsSection({ export function SearchResultsSection({
...@@ -93,13 +70,7 @@ export function SearchResultsSection({ ...@@ -93,13 +70,7 @@ export function SearchResultsSection({
return undefined return undefined
} }
const formattedTokenSearchResults = formatTokenSearchResults(searchResultsData.searchTokens, searchQuery) return formatTokenSearchResults(searchResultsData.searchTokens, searchQuery, selectedChain)
if (!selectedChain) {
return formattedTokenSearchResults
}
return filterSearchResultsByChainId(formattedTokenSearchResults, selectedChain)
}, [selectedChain, searchQuery, searchResultsData]) }, [selectedChain, searchQuery, searchResultsData])
// Search for matching NFT collections // Search for matching NFT collections
...@@ -109,13 +80,7 @@ export function SearchResultsSection({ ...@@ -109,13 +80,7 @@ export function SearchResultsSection({
return undefined return undefined
} }
const formattedNftCollectionSearchResults = formatNFTCollectionSearchResults(searchResultsData.nftCollections) return formatNFTCollectionSearchResults(searchResultsData.nftCollections, selectedChain)
if (!selectedChain) {
return formattedNftCollectionSearchResults
}
return filterSearchResultsByChainId(formattedNftCollectionSearchResults, selectedChain)
}, [searchResultsData, selectedChain]) }, [searchResultsData, selectedChain])
// Search for matching wallets // Search for matching wallets
...@@ -187,7 +152,7 @@ export function SearchResultsSection({ ...@@ -187,7 +152,7 @@ export function SearchResultsSection({
// Don't wait for wallet search results if there are already token search results, do wait for token results // Don't wait for wallet search results if there are already token search results, do wait for token results
if (searchResultsLoading) { if (searchResultsLoading) {
return <SearchResultsLoader /> return <SearchResultsLoader selectedChain={selectedChain} />
} }
if (error) { if (error) {
...@@ -209,7 +174,7 @@ export function SearchResultsSection({ ...@@ -209,7 +174,7 @@ export function SearchResultsSection({
<Flex grow gap="$spacing8" pb="$spacing36"> <Flex grow gap="$spacing8" pb="$spacing36">
<FlatList <FlatList
ListEmptyComponent={ ListEmptyComponent={
<AnimatedFlex entering={FadeIn} exiting={FadeOut} gap="$spacing8" mx="$spacing8"> <AnimatedFlex entering={FadeIn} exiting={FadeOut} gap="$spacing8" mx="$spacing20">
<Text color="$neutral2" variant="body1"> <Text color="$neutral2" variant="body1">
<Trans <Trans
components={{ highlight: <Text color="$neutral1" variant="body1" /> }} components={{ highlight: <Text color="$neutral1" variant="body1" /> }}
......
export const SEARCH_RESULT_HEADER_KEY = 'header' import { SearchHeader, SearchHeaderKey } from 'src/components/explore/search/types'
import { Coin, Gallery, Person } from 'ui/src/components/icons'
import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains'
import i18n from 'uniswap/src/i18n/i18n'
import { UniverseChainId } from 'uniswap/src/types/chains'
export const SEARCH_RESULT_HEADER_KEY: SearchHeaderKey = 'header'
const ICON_SIZE = '$icon.24'
const ICON_COLOR = '$neutral2'
export const WalletHeaderItem: SearchHeader = {
icon: <Person color={ICON_COLOR} size={ICON_SIZE} />,
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('explore.search.section.wallets'),
}
export const TokenHeaderItem: SearchHeader = {
icon: <Coin color={ICON_COLOR} size={ICON_SIZE} />,
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('explore.search.section.tokens'),
}
export const NFTHeaderItem: SearchHeader = {
icon: <Gallery color={ICON_COLOR} size={ICON_SIZE} />,
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('explore.search.section.nft'),
}
export const EtherscanHeaderItem: (chainId: UniverseChainId) => SearchHeader = (chainId: UniverseChainId) => ({
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('explore.search.action.viewEtherscan', {
blockExplorerName: UNIVERSE_CHAIN_INFO[chainId].explorer.name,
}),
})
...@@ -7,7 +7,7 @@ import { disableOnPress } from 'src/utils/disableOnPress' ...@@ -7,7 +7,7 @@ import { disableOnPress } from 'src/utils/disableOnPress'
import { Flex, ImpactFeedbackStyle, Text, TouchableArea } from 'ui/src' import { Flex, ImpactFeedbackStyle, Text, TouchableArea } from 'ui/src'
import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo' import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo'
import WarningIcon from 'uniswap/src/components/warnings/WarningIcon' import WarningIcon from 'uniswap/src/components/warnings/WarningIcon'
import { getWarningIconColorOverride } from 'uniswap/src/components/warnings/utils' import { getWarningIconColors } from 'uniswap/src/components/warnings/utils'
import { SearchContext } from 'uniswap/src/features/search/SearchContext' import { SearchContext } from 'uniswap/src/features/search/SearchContext'
import { SearchResultType, TokenSearchResult } from 'uniswap/src/features/search/SearchResult' import { SearchResultType, TokenSearchResult } from 'uniswap/src/features/search/SearchResult'
import { addToSearchHistory } from 'uniswap/src/features/search/searchHistorySlice' import { addToSearchHistory } from 'uniswap/src/features/search/searchHistorySlice'
...@@ -28,11 +28,12 @@ export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps): ...@@ -28,11 +28,12 @@ export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps):
const dispatch = useDispatch() const dispatch = useDispatch()
const tokenDetailsNavigation = useTokenDetailsNavigation() const tokenDetailsNavigation = useTokenDetailsNavigation()
const { chainId, address, name, symbol, logoUrl, safetyLevel, safetyInfo } = token const { chainId, address, name, symbol, logoUrl, safetyLevel, safetyInfo, feeData } = token
const currencyId = address ? buildCurrencyId(chainId, address) : buildNativeCurrencyId(chainId as UniverseChainId) const currencyId = address ? buildCurrencyId(chainId, address) : buildNativeCurrencyId(chainId as UniverseChainId)
const currencyInfo = useCurrencyInfo(currencyId) const currencyInfo = useCurrencyInfo(currencyId)
const severity = getTokenWarningSeverity(currencyInfo) const severity = getTokenWarningSeverity(currencyInfo)
const warningIconColor = getWarningIconColorOverride(severity) // in mobile search, we only show the warning icon if token is >=Medium severity
const { colorSecondary: warningIconColor } = getWarningIconColors(severity)
const onPress = (): void => { const onPress = (): void => {
tokenDetailsNavigation.preload(currencyId) tokenDetailsNavigation.preload(currencyId)
...@@ -60,6 +61,7 @@ export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps): ...@@ -60,6 +61,7 @@ export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps):
logoUrl, logoUrl,
safetyLevel, safetyLevel,
safetyInfo, safetyInfo,
feeData,
}, },
}), }),
) )
......
import { SEARCH_RESULT_HEADER_KEY } from 'src/components/explore/search/constants'
import { SearchResult } from 'uniswap/src/features/search/SearchResult' import { SearchResult } from 'uniswap/src/features/search/SearchResult'
// Header type used to render header text instead of SearchResult item export type SearchHeaderKey = 'header'
export type SearchHeader = { type: SearchHeaderKey; title: string; icon?: JSX.Element }
export type SearchResultOrHeader = export type SearchResultOrHeader = SearchResult | SearchHeader
| SearchResult
| { type: typeof SEARCH_RESULT_HEADER_KEY; title: string; icon?: JSX.Element }
...@@ -6,6 +6,7 @@ import { ...@@ -6,6 +6,7 @@ import {
} from 'src/components/explore/search/utils' } from 'src/components/explore/search/utils'
import { Chain, ExploreSearchQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { Chain, ExploreSearchQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { fromGraphQLChain } from 'uniswap/src/features/chains/utils'
import { getCurrencySafetyInfo } from 'uniswap/src/features/dataApi/utils'
import { SearchResultType } from 'uniswap/src/features/search/SearchResult' import { SearchResultType } from 'uniswap/src/features/search/SearchResult'
import { amount, ethToken, nftCollection, nftContract, token, tokenMarket } from 'uniswap/src/test/fixtures' import { amount, ethToken, nftCollection, nftContract, token, tokenMarket } from 'uniswap/src/test/fixtures'
import { createArray } from 'uniswap/src/test/utils' import { createArray } from 'uniswap/src/test/utils'
...@@ -14,14 +15,14 @@ type ExploreSearchResult = NonNullable<ExploreSearchQuery> ...@@ -14,14 +15,14 @@ type ExploreSearchResult = NonNullable<ExploreSearchQuery>
describe(formatTokenSearchResults, () => { describe(formatTokenSearchResults, () => {
it('returns undefined if there is no data', () => { it('returns undefined if there is no data', () => {
expect(formatTokenSearchResults(undefined, '')).toEqual(undefined) expect(formatTokenSearchResults(undefined, '', null)).toEqual(undefined)
}) })
it('filters out duplicate results', () => { it('filters out duplicate results', () => {
const searchToken = token() const searchToken = token()
const data = createArray(2, () => searchToken) const data = createArray(2, () => searchToken)
const result = formatTokenSearchResults(data, '') const result = formatTokenSearchResults(data, '', null)
expect(result).toHaveLength(1) expect(result).toHaveLength(1)
expect(result?.[0]?.address).toEqual(data[0].address) expect(result?.[0]?.address).toEqual(data[0].address)
...@@ -44,7 +45,7 @@ describe(formatTokenSearchResults, () => { ...@@ -44,7 +45,7 @@ describe(formatTokenSearchResults, () => {
}), }),
] ]
const result = formatTokenSearchResults(data, '') const result = formatTokenSearchResults(data, '', null)
// Filters out the first token (both tokens share the same project id) // Filters out the first token (both tokens share the same project id)
expect(result).toHaveLength(1) expect(result).toHaveLength(1)
...@@ -58,7 +59,7 @@ describe(formatTokenSearchResults, () => { ...@@ -58,7 +59,7 @@ describe(formatTokenSearchResults, () => {
token({ name: 'Uniswap' }), token({ name: 'Uniswap' }),
] ]
const result = formatTokenSearchResults(data, 'uniswap') const result = formatTokenSearchResults(data, 'uniswap', null)
expect(result).toHaveLength(2) expect(result).toHaveLength(2)
expect(result?.[0]?.name).toEqual('Uniswap') expect(result?.[0]?.name).toEqual('Uniswap')
...@@ -69,7 +70,7 @@ describe(formatTokenSearchResults, () => { ...@@ -69,7 +70,7 @@ describe(formatTokenSearchResults, () => {
const searchToken = token() const searchToken = token()
const data = [searchToken] const data = [searchToken]
const result = formatTokenSearchResults(data, '') const result = formatTokenSearchResults(data, '', null)
expect(result).toHaveLength(1) expect(result).toHaveLength(1)
expect(result?.[0]?.type).toEqual(SearchResultType.Token) expect(result?.[0]?.type).toEqual(SearchResultType.Token)
...@@ -79,6 +80,10 @@ describe(formatTokenSearchResults, () => { ...@@ -79,6 +80,10 @@ describe(formatTokenSearchResults, () => {
expect(result?.[0]?.symbol).toEqual(searchToken.symbol) expect(result?.[0]?.symbol).toEqual(searchToken.symbol)
expect(result?.[0]?.logoUrl).toEqual(searchToken.project?.logoUrl) expect(result?.[0]?.logoUrl).toEqual(searchToken.project?.logoUrl)
expect(result?.[0]?.safetyLevel).toEqual(searchToken.project?.safetyLevel) expect(result?.[0]?.safetyLevel).toEqual(searchToken.project?.safetyLevel)
expect(result?.[0]?.feeData).toEqual(searchToken.feeData)
expect(result?.[0]?.safetyInfo).toEqual(
getCurrencySafetyInfo(searchToken.project?.safetyLevel, searchToken.protectionInfo),
)
}) })
describe(gqlNFTToNFTCollectionSearchResult, () => { describe(gqlNFTToNFTCollectionSearchResult, () => {
...@@ -106,7 +111,7 @@ describe(formatTokenSearchResults, () => { ...@@ -106,7 +111,7 @@ describe(formatTokenSearchResults, () => {
describe(formatNFTCollectionSearchResults, () => { describe(formatNFTCollectionSearchResults, () => {
it('returns undefined if there is no data', () => { it('returns undefined if there is no data', () => {
expect(formatNFTCollectionSearchResults(undefined)).toEqual(undefined) expect(formatNFTCollectionSearchResults(undefined, null)).toEqual(undefined)
}) })
it('filters out nfts that cannot be formatted', () => { it('filters out nfts that cannot be formatted', () => {
...@@ -115,7 +120,7 @@ describe(formatTokenSearchResults, () => { ...@@ -115,7 +120,7 @@ describe(formatTokenSearchResults, () => {
edges: [...topNFTCollections.map((nft) => ({ node: nft })), { node: nftCollection({ name: undefined }) }], edges: [...topNFTCollections.map((nft) => ({ node: nft })), { node: nftCollection({ name: undefined }) }],
} }
const result = formatNFTCollectionSearchResults(nftSearchResult) const result = formatNFTCollectionSearchResults(nftSearchResult, null)
expect(result).toHaveLength(2) expect(result).toHaveLength(2)
expect(result?.[0]?.address).toEqual(topNFTCollections[0].nftContracts[0]?.address) expect(result?.[0]?.address).toEqual(topNFTCollections[0].nftContracts[0]?.address)
......
...@@ -11,21 +11,15 @@ import { ...@@ -11,21 +11,15 @@ import {
import { searchResultId } from 'uniswap/src/features/search/searchHistorySlice' import { searchResultId } from 'uniswap/src/features/search/searchHistorySlice'
import { UniverseChainId } from 'uniswap/src/types/chains' import { UniverseChainId } from 'uniswap/src/types/chains'
const MAX_TOKEN_RESULTS_COUNT = 4 const MAX_TOKEN_RESULTS_COUNT = 8
type ExploreSearchResult = NonNullable<ExploreSearchQuery> type ExploreSearchResult = NonNullable<ExploreSearchQuery>
export function filterSearchResultsByChainId<T extends { chainId: null | UniverseChainId }>(
tokenSearchResults: Array<T> | undefined,
chainId: UniverseChainId | null,
): Array<T> | undefined {
return tokenSearchResults?.filter((searchResult): boolean => chainId === null || searchResult.chainId === chainId)
}
// Formats the tokens portion of explore search results into sorted array of TokenSearchResult // Formats the tokens portion of explore search results into sorted array of TokenSearchResult
export function formatTokenSearchResults( export function formatTokenSearchResults(
data: ExploreSearchResult['searchTokens'], data: ExploreSearchResult['searchTokens'],
searchQuery: string, searchQuery: string,
selectedChain: UniverseChainId | null,
): TokenSearchResult[] | undefined { ): TokenSearchResult[] | undefined {
if (!data) { if (!data) {
return undefined return undefined
...@@ -39,10 +33,13 @@ export function formatTokenSearchResults( ...@@ -39,10 +33,13 @@ export function formatTokenSearchResults(
return tokensMap return tokensMap
} }
const { name, chain, address, symbol, project, market, protectionInfo } = token const { name, chain, address, symbol, project, market, protectionInfo, feeData } = token
const chainId = fromGraphQLChain(chain) const chainId = fromGraphQLChain(chain)
if (!chainId || !project) { const shoulderFilterByChain = !!selectedChain
const chainMismatch = shoulderFilterByChain && selectedChain !== chainId
if (!chainId || !project || chainMismatch) {
return tokensMap return tokensMap
} }
...@@ -58,6 +55,7 @@ export function formatTokenSearchResults( ...@@ -58,6 +55,7 @@ export function formatTokenSearchResults(
logoUrl: logoUrl ?? null, logoUrl: logoUrl ?? null,
volume1D: market?.volume?.value ?? 0, volume1D: market?.volume?.value ?? 0,
safetyInfo: getCurrencySafetyInfo(safetyLevel, protectionInfo), safetyInfo: getCurrencySafetyInfo(safetyLevel, protectionInfo),
feeData: feeData ?? null,
} }
// For token results that share the same TokenProject id, use the token with highest volume // For token results that share the same TokenProject id, use the token with highest volume
...@@ -93,6 +91,7 @@ function isExactTokenSearchResultMatch(searchResult: TokenSearchResult, query: s ...@@ -93,6 +91,7 @@ function isExactTokenSearchResultMatch(searchResult: TokenSearchResult, query: s
export function formatNFTCollectionSearchResults( export function formatNFTCollectionSearchResults(
data: ExploreSearchResult['nftCollections'], data: ExploreSearchResult['nftCollections'],
selectedChain: UniverseChainId | null,
): NFTCollectionSearchResult[] | undefined { ): NFTCollectionSearchResult[] | undefined {
if (!data) { if (!data) {
return undefined return undefined
...@@ -100,7 +99,10 @@ export function formatNFTCollectionSearchResults( ...@@ -100,7 +99,10 @@ export function formatNFTCollectionSearchResults(
return data.edges.reduce<NFTCollectionSearchResult[]>((accum, { node }) => { return data.edges.reduce<NFTCollectionSearchResult[]>((accum, { node }) => {
const formatted = gqlNFTToNFTCollectionSearchResult(node) const formatted = gqlNFTToNFTCollectionSearchResult(node)
if (formatted) {
const chainMismatch = selectedChain && formatted && formatted.chainId !== selectedChain
if (formatted && !chainMismatch) {
accum.push(formatted) accum.push(formatted)
} }
return accum return accum
......
...@@ -16,7 +16,12 @@ import { OnboardingCardLoggingName } from 'uniswap/src/features/telemetry/types' ...@@ -16,7 +16,12 @@ import { OnboardingCardLoggingName } from 'uniswap/src/features/telemetry/types'
import { useTranslation } from 'uniswap/src/i18n' import { useTranslation } from 'uniswap/src/i18n'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { MobileScreens, OnboardingScreens, UnitagScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens, OnboardingScreens, UnitagScreens } from 'uniswap/src/types/screens/mobile'
import { CardType, IntroCardGraphicType, IntroCardProps } from 'wallet/src/components/introCards/IntroCard' import {
CardType,
IntroCardGraphicType,
IntroCardProps,
isOnboardingCardLoggingName,
} from 'wallet/src/components/introCards/IntroCard'
import { INTRO_CARD_MIN_HEIGHT, IntroCardStack } from 'wallet/src/components/introCards/IntroCardStack' import { INTRO_CARD_MIN_HEIGHT, IntroCardStack } from 'wallet/src/components/introCards/IntroCardStack'
import { useSharedIntroCards } from 'wallet/src/components/introCards/useSharedIntroCards' import { useSharedIntroCards } from 'wallet/src/components/introCards/useSharedIntroCards'
import { selectHasViewedWelcomeWalletCard } from 'wallet/src/features/behaviorHistory/selectors' import { selectHasViewedWelcomeWalletCard } from 'wallet/src/features/behaviorHistory/selectors'
...@@ -175,7 +180,7 @@ export function OnboardingIntroCardStack({ ...@@ -175,7 +180,7 @@ export function OnboardingIntroCardStack({
const handleSwiped = useCallback( const handleSwiped = useCallback(
(_card: IntroCardProps, index: number) => { (_card: IntroCardProps, index: number) => {
const loggingName = cards[index]?.loggingName const loggingName = cards[index]?.loggingName
if (loggingName) { if (loggingName && isOnboardingCardLoggingName(loggingName)) {
sendAnalyticsEvent(WalletEventName.OnboardingIntroCardSwiped, { sendAnalyticsEvent(WalletEventName.OnboardingIntroCardSwiped, {
card_name: loggingName, card_name: loggingName,
}) })
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment