ci(release): publish latest release

parent 96851c80
......@@ -51,3 +51,6 @@ packages/uniswap/src/i18n/locales/source/*_old.json
# Vercel
.vercel
# CodeTours Extension
.tours/*
IPFS hash of the deployment:
- CIDv0: `QmYH4Tb7M6EoFHKRNyCBHHFeNadwnrmibmQmknWHSQxj8R`
- CIDv1: `bafybeietvftf6vmileh7kp3srslms4azyza5cmgu6ttrzikjzz4dkrryxy`
- CIDv0: `QmPq9sW2ih541PM9991Trh3Cocrdstrh63m6UWRbZb6Nqo`
- CIDv1: `bafybeiawfdwfe6ayrmkbbgbjos2noxf2w7ihz4tk7x5tu67ybggnmuvrni`
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.
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://bafybeietvftf6vmileh7kp3srslms4azyza5cmgu6ttrzikjzz4dkrryxy.ipfs.dweb.link/
- https://bafybeietvftf6vmileh7kp3srslms4azyza5cmgu6ttrzikjzz4dkrryxy.ipfs.cf-ipfs.com/
- [ipfs://QmYH4Tb7M6EoFHKRNyCBHHFeNadwnrmibmQmknWHSQxj8R/](ipfs://QmYH4Tb7M6EoFHKRNyCBHHFeNadwnrmibmQmknWHSQxj8R/)
- https://bafybeiawfdwfe6ayrmkbbgbjos2noxf2w7ihz4tk7x5tu67ybggnmuvrni.ipfs.dweb.link/
- https://bafybeiawfdwfe6ayrmkbbgbjos2noxf2w7ihz4tk7x5tu67ybggnmuvrni.ipfs.cf-ipfs.com/
- [ipfs://QmPq9sW2ih541PM9991Trh3Cocrdstrh63m6UWRbZb6Nqo/](ipfs://QmPq9sW2ih541PM9991Trh3Cocrdstrh63m6UWRbZb6Nqo/)
## 5.55.0 (2024-10-24)
## 5.56.0 (2024-10-29)
### 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
\ No newline at end of file
web/5.56.0
\ No newline at end of file
......@@ -12,7 +12,7 @@ module.exports = {
'manifest.json',
],
parserOptions: {
project: 'tsconfig.json',
project: 'tsconfig.eslint.json',
tsconfigRootDir: __dirname,
ecmaFeatures: {
jsx: true,
......
......@@ -4,6 +4,7 @@
"browserslist": "last 2 chrome versions",
"dependencies": {
"@apollo/client": "3.10.4",
"@datadog/browser-rum": "5.23.3",
"@ethersproject/providers": "5.7.2",
"@metamask/rpc-errors": "6.2.1",
"@reduxjs/toolkit": "1.9.3",
......@@ -14,10 +15,10 @@
"@tamagui/core": "1.108.4",
"@types/uuid": "9.0.1",
"@uniswap/analytics-events": "2.38.0",
"@uniswap/uniswapx-sdk": "^2.1.0-beta.14",
"@uniswap/universal-router-sdk": "4.2.0",
"@uniswap/v3-sdk": "3.17.0",
"@uniswap/v4-sdk": "1.10.0",
"@uniswap/uniswapx-sdk": "2.1.0-beta.18",
"@uniswap/universal-router-sdk": "4.5.2",
"@uniswap/v3-sdk": "3.18.1",
"@uniswap/v4-sdk": "1.10.3",
"dotenv-webpack": "8.0.1",
"ethers": "5.7.2",
"eventemitter3": "5.0.1",
......
......@@ -34,7 +34,7 @@ import { ScanToOnboard } from 'src/app/features/onboarding/scan/ScanToOnboard'
import { ScantasticContextProvider } from 'src/app/features/onboarding/scan/ScantasticContextProvider'
import { OnboardingRoutes, TopLevelRoutes } from 'src/app/navigation/constants'
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 { checksIfSupportsSidePanel } from 'src/app/utils/chrome'
import { PrimaryAppInstanceDebuggerLazy } from 'src/store/PrimaryAppInstanceDebuggerLazy'
......@@ -56,14 +56,6 @@ const unsupportedRoute: RouteObject = {
element: <UnsupportedBrowserScreen />,
}
const createSteps = {
[CreateOnboardingSteps.Password]: <PasswordCreate />,
[CreateOnboardingSteps.ViewMnemonic]: <ViewMnemonic />,
[CreateOnboardingSteps.TestMnemonic]: <TestMnemonic />,
[CreateOnboardingSteps.Naming]: <NameWallet />,
[CreateOnboardingSteps.Complete]: <Complete flow={ExtensionOnboardingFlow.New} />,
}
const allRoutes = [
{
path: '',
......@@ -75,7 +67,18 @@ const allRoutes = [
},
{
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,
......@@ -84,7 +87,10 @@ const allRoutes = [
key={OnboardingRoutes.Claim}
steps={{
[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 {
return (
<Trace>
<PersistGate persistor={getReduxPersistor()}>
<ExtensionStatsigProvider>
<ExtensionStatsigProvider appName={SentryAppNameTag.Onboarding}>
<I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}>
<ErrorBoundary>
......
......@@ -13,7 +13,6 @@ import { TraceUserProperties } from 'src/app/components/Trace/TraceUserPropertie
import { DappContextProvider } from 'src/app/features/dapp/DappContext'
import { SentryAppNameTag, initializeSentry, sentryCreateHashRouter } from 'src/app/sentry'
import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { getLocalUserId } from 'src/app/utils/storage'
import { getReduxPersistor, getReduxStore } from 'src/store/store'
import { Button, Flex, Image, Text } from 'ui/src'
import { CHROME_LOGO, UNISWAP_LOGO } from 'ui/src/assets'
......@@ -25,18 +24,19 @@ import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context'
import i18n from 'uniswap/src/i18n/i18n'
import { ExtensionScreens } from 'uniswap/src/types/screens/extension'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger'
import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks'
import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider'
getLocalUserId()
getUniqueId()
.then((userId) => {
initializeSentry(SentryAppNameTag.Popup, userId)
})
.catch((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 {
return (
<Trace>
<PersistGate persistor={getReduxPersistor()}>
<ExtensionStatsigProvider>
<ExtensionStatsigProvider appName={SentryAppNameTag.Popup}>
<I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}>
<ErrorBoundary>
......
......@@ -31,7 +31,6 @@ import { MainContent, WebNavigation } from 'src/app/navigation/navigation'
import { setRouter, setRouterState } from 'src/app/navigation/state'
import { SentryAppNameTag, initializeSentry, sentryCreateHashRouter } from 'src/app/sentry'
import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { getLocalUserId } from 'src/app/utils/storage'
import {
DappBackgroundPortChannel,
backgroundToSidePanelMessageChannel,
......@@ -47,6 +46,7 @@ import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { UnitagUpdaterContextProvider, useUnitagUpdater } from 'uniswap/src/features/unitags/context'
import i18n from 'uniswap/src/i18n/i18n'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { isDevEnv } from 'utilities/src/environment/env'
import { logger } from 'utilities/src/logger/logger'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
......@@ -55,13 +55,13 @@ import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks'
import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider'
getLocalUserId()
getUniqueId()
.then((userId) => {
initializeSentry(SentryAppNameTag.Sidebar, userId)
})
.catch((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 {
return (
<Trace>
<PersistGate persistor={getReduxPersistor()}>
<ExtensionStatsigProvider>
<ExtensionStatsigProvider appName={SentryAppNameTag.Sidebar}>
<I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}>
<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 Statsig from 'statsig-js' // Use JS package for browser
import { uniswapUrls } from 'uniswap/src/constants/urls'
import { DUMMY_STATSIG_SDK_KEY, StatsigCustomAppValue } from 'uniswap/src/features/gating/constants'
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'
async function getStatsigUser(): Promise<StatsigUser> {
return {
userID: await getLocalUserId(),
userID: await getUniqueId(),
appVersion: process.env.VERSION,
custom: {
app: StatsigCustomAppValue.Extension,
......@@ -16,16 +18,28 @@ async function getStatsigUser(): Promise<StatsigUser> {
}
}
export function ExtensionStatsigProvider({ children }: { children: React.ReactNode }): JSX.Element {
const { data: user } = useAsyncData(getStatsigUser)
const nonNullUser: StatsigUser = user ?? {
export function ExtensionStatsigProvider({
children,
appName,
}: {
children: React.ReactNode
appName: string
}): JSX.Element {
const { data: storedUser } = useAsyncData(getStatsigUser)
const [user, setUser] = useState<StatsigUser>({
userID: undefined,
custom: {
app: StatsigCustomAppValue.Extension,
},
appVersion: process.env.VERSION,
})
const [initFinished, setInitFinished] = useState(false)
useEffect(() => {
if (storedUser && initFinished) {
setUser(storedUser)
}
}, [storedUser, initFinished])
const options: StatsigOptions = {
environment: {
......@@ -34,10 +48,14 @@ export function ExtensionStatsigProvider({ children }: { children: React.ReactNo
api: uniswapUrls.statsigProxyUrl,
disableAutoMetricsLogging: true,
disableErrorLogging: true,
initCompletionCallback: () => {
setInitFinished(true)
initializeDatadog(appName).catch(() => undefined)
},
}
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}
</StatsigProvider>
)
......
import '@tamagui/core/reset.css'
import 'src/app/Global.css'
import { useEffect } from 'react'
import { PropsWithChildren, useEffect } from 'react'
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 { ExtensionStatsigProvider } from 'src/app/StatsigProvider'
import { GraphqlProvider } from 'src/app/apollo'
import { ErrorElement } from 'src/app/components/ErrorElement'
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 { UnitagChooseProfilePicScreen } from 'src/app/features/unitags/UnitagChooseProfilePicScreen'
import { UnitagClaimBackground } from 'src/app/features/unitags/UnitagClaimBackground'
import { UnitagClaimContextProvider } from 'src/app/features/unitags/UnitagClaimContext'
import { UnitagConfirmationScreen } from 'src/app/features/unitags/UnitagConfirmationScreen'
import { UnitagCreateUsernameScreen } from 'src/app/features/unitags/UnitagCreateUsernameScreen'
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 { SentryAppNameTag, initializeSentry, sentryCreateHashRouter } from 'src/app/sentry'
import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { getLocalUserId } from 'src/app/utils/storage'
import { getReduxPersistor, getReduxStore } from 'src/store/store'
import { Flex } from 'ui/src'
import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context'
import i18n from 'uniswap/src/i18n/i18n'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger'
import { usePrevious } from 'utilities/src/react/hooks'
import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks'
import { useAccountAddressFromUrlWithThrow } from 'wallet/src/features/wallet/hooks'
import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider'
getLocalUserId()
getUniqueId()
.then((userId) => {
initializeSentry(SentryAppNameTag.UnitagClaim, userId)
})
.catch((error) => {
logger.error(error, {
tags: { file: 'UnitagClaimApp.tsx', function: 'getLocalUserId' },
tags: { file: 'UnitagClaimApp.tsx', function: 'getUniqueId' },
})
})
const router = sentryCreateHashRouter([
{
path: '',
element: <UnitagClaimAppInner />,
element: <UnitagAppInner />,
children: [
{
path: UnitagClaimRoutes.ClaimIntro,
element: <UnitagClaimFlow />,
errorElement: <ErrorElement />,
},
{
path: OnboardingRoutes.EditProfile,
element: <EditProfileAppInner />,
path: UnitagClaimRoutes.EditProfile,
element: <UnitagEditProfileFlow />,
errorElement: <ErrorElement />,
},
],
},
])
/**
......@@ -66,10 +79,36 @@ router.subscribe((state) => {
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()
return <Outlet />
}
function UnitagClaimFlow(): JSX.Element {
return (
<Flex centered height="100vh" width="100%">
<Flex centered height="100%" width="100%">
<OnboardingStepsProvider
disableRedirect
steps={{
......@@ -79,22 +118,33 @@ function UnitagClaimAppInner(): JSX.Element {
[ClaimUnitagSteps.Confirmation]: <UnitagConfirmationScreen />,
[ClaimUnitagSteps.EditProfile]: <EditUnitagProfileScreen enableBack />,
}}
ContainerComponent={UnitagClaimContextProvider}
ContainerComponent={UnitagClaimAppWrapper}
/>
<Outlet />
</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 (
<Flex centered>
<Flex centered height="100%" width="100%">
<OnboardingStepsProvider
disableRedirect
steps={{
[ClaimUnitagSteps.Intro]: <EditUnitagProfileScreen />,
[ClaimUnitagSteps.EditProfile]: <EditUnitagProfileScreen />,
}}
ContainerComponent={UnitagClaimContextProvider}
ContainerComponent={UnitagClaimAppWrapper}
/>
<Outlet />
</Flex>
......@@ -111,7 +161,7 @@ export default function UnitagClaimApp(): JSX.Element {
return (
<Trace>
<PersistGate persistor={getReduxPersistor()}>
<ExtensionStatsigProvider>
<ExtensionStatsigProvider appName={SentryAppNameTag.UnitagClaim}>
<I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}>
<ErrorBoundary>
......
......@@ -2,7 +2,11 @@ import { useEffect } from 'react'
import { useColorScheme } from 'react-native'
import { useAppFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/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'
// eslint-disable-next-line no-restricted-imports
import { analytics } from 'utilities/src/telemetry/analytics/analytics'
......@@ -19,6 +23,7 @@ export function TraceUserProperties(): null {
const hideSpamTokens = useHideSpamTokensSetting()
const currentLanguage = useCurrentLanguage()
const appFiatCurrencyInfo = useAppFiatCurrencyInfo()
const { isTestnetModeEnabled } = useEnabledChains()
useGatingUserPropertyUsernames()
......@@ -63,5 +68,9 @@ export function TraceUserProperties(): null {
setUserProperty(ExtensionUserPropertyName.Currency, appFiatCurrencyInfo.code)
}, [appFiatCurrencyInfo])
useEffect(() => {
setUserProperty(ExtensionUserPropertyName.TestnetModeEnabled, isTestnetModeEnabled)
}, [isTestnetModeEnabled])
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'
import { useDispatch } from 'react-redux'
import { EditLabelModal } from 'src/app/features/accounts/EditLabelModal'
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 { iconSizes } from 'ui/src/theme'
import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal'
......@@ -17,6 +17,8 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { setClipboard } from 'uniswap/src/utils/clipboard'
import { NumberType } from 'utilities/src/format/types'
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 { useActiveAccountWithThrow, useDisplayName, useSignerAccounts } from 'wallet/src/features/wallet/hooks'
import { DisplayNameType } from 'wallet/src/features/wallet/types'
......
......@@ -11,10 +11,10 @@ import { updateDappConnectedAddressFromExtension } from 'src/app/features/dapp/a
import { useDappConnectedAccounts } from 'src/app/features/dapp/hooks'
import { isConnectedAccount } from 'src/app/features/dapp/utils'
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 { 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 { spacing } from 'ui/src/theme'
import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal'
......@@ -31,11 +31,18 @@ import { logger } from 'utilities/src/logger/logger'
import { sleep } from 'utilities/src/time/timing'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
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 { createOnboardingAccount } from 'wallet/src/features/onboarding/createOnboardingAccount'
import { BackupType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types'
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 { setAccountAsActive } from 'wallet/src/features/wallet/slice'
import { DisplayNameType } from 'wallet/src/features/wallet/types'
......@@ -284,11 +291,12 @@ export function AccountSwitcherScreen(): JSX.Element {
const UnitagActionButton = (): JSX.Element => {
const { t } = useTranslation()
const address = useActiveAccountAddressWithThrow()
const isClaimUnitagEnabled = useFeatureFlag(FeatureFlags.ExtensionClaimUnitag)
const onPressEditProfile = useCallback(async () => {
await focusOrCreateUnitagTab(OnboardingRoutes.EditProfile)
}, [])
await focusOrCreateUnitagTab(address, UnitagClaimRoutes.EditProfile)
}, [address])
if (isClaimUnitagEnabled) {
return (
......
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
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 { Person } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
import { TextInput } from 'uniswap/src/components/input/TextInput'
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 { OnboardingCardLoggingName } from 'uniswap/src/features/telemetry/types'
import { shortenAddress } from 'utilities/src/addresses'
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 { useDisplayName } from 'wallet/src/features/wallet/hooks'
import { DisplayNameType } from 'wallet/src/features/wallet/types'
......@@ -28,6 +37,9 @@ export function EditLabelModal({ isOpen, address, onClose }: EditLabelModalProps
const [inputText, setInputText] = useState<string>(defaultText)
const [isfocused, setIsFocused] = useState(false)
const { canClaimUnitag } = useCanActiveAddressClaimUnitag()
const unitagsClaimEnabled = useFeatureFlag(FeatureFlags.ExtensionClaimUnitag)
const onConfirm = useCallback(async () => {
await dispatch(
editAccountActions.trigger({
......@@ -39,8 +51,34 @@ export function EditLabelModal({ isOpen, address, onClose }: EditLabelModalProps
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 (
<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 gap="$spacing12" width="100%">
<AccountIcon address={address} size={iconSizes.icon48} />
......
......@@ -107,7 +107,13 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
>
<path
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-rule="evenodd"
/>
......@@ -127,9 +133,9 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
<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"
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>
</div>
</div>
......@@ -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"
data-disable-theme="true"
>
0x​e0c6...ea11
0x​9eb6...a2ca
</span>
<svg
fill="none"
......@@ -333,7 +339,13 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
>
<path
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-rule="evenodd"
/>
......@@ -353,9 +365,9 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
<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"
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>
</div>
</div>
......@@ -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"
data-disable-theme="true"
>
0x​e0c6...ea11
0x​9eb6...a2ca
</span>
<svg
fill="none"
......
......@@ -4,11 +4,11 @@ import { updateDisplayNameFromTab } from 'src/app/features/dapp/actions'
import { useDappConnectedAccounts, useDappLastChainId } from 'src/app/features/dapp/hooks'
import { dappStore } from 'src/app/features/dapp/store'
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 { backgroundToSidePanelMessageChannel } from 'src/background/messagePassing/messageChannels'
import { BackgroundToSidePanelRequestType } from 'src/background/messagePassing/types/requests'
import { UniverseChainId } from 'uniswap/src/types/chains'
import { extractBaseUrl } from 'utilities/src/format/urls'
import { useActiveAccountAddress } from 'wallet/src/features/wallet/hooks'
type DappContextState = {
......
......@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
import { useDappLastChainId } from 'src/app/features/dapp/hooks'
import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext'
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 { borderRadii, iconSizes } from 'ui/src/theme'
import { GasFeeResult } from 'uniswap/src/features/gas/types'
......@@ -184,7 +185,10 @@ export function DappRequestFooter({
const shouldCloseSidebar = request.isSidebarClosed && totalRequestCount <= 1
// 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 () => {
if (onConfirm) {
......
......@@ -2,6 +2,7 @@ import { memo } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { AnimatedPane, DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent'
import { DappRequestCards } from 'src/app/features/dappRequests/DappRequestQueueCards'
import {
DappRequestQueueProvider,
useDappRequestQueueContext,
......@@ -178,6 +179,7 @@ function DappRequestQueueContent(): JSX.Element {
)}
<DappRequest />
</Flex>
<DappRequestCards />
</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 {
} from 'src/app/features/dappRequests/saga'
import { DappRequestStoreItem } from 'src/app/features/dappRequests/slice'
import { DappResponseType } from 'src/app/features/dappRequests/types/DappRequestTypes'
import { extractBaseUrl } from 'src/app/features/dappRequests/utils'
import { ExtensionState } from 'src/store/extensionReducer'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { DappRequestAction } from 'uniswap/src/features/telemetry/types'
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 { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
......
......@@ -13,7 +13,6 @@ import {
GetAccountRequest,
RequestAccountRequest,
} from 'src/app/features/dappRequests/types/DappRequestTypes'
import { extractBaseUrl } from 'src/app/features/dappRequests/utils'
import { dappResponseMessageChannel } from 'src/background/messagePassing/messageChannels'
import { call, put, select } from 'typed-redux-saga'
import { chainIdToHexadecimalString } from 'uniswap/src/features/chains/utils'
......@@ -23,6 +22,7 @@ import { getEnabledChainIdsSaga } from 'uniswap/src/features/settings/saga'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { UniverseChainId } from 'uniswap/src/types/chains'
import { extractBaseUrl } from 'utilities/src/format/urls'
import { getProvider } from 'wallet/src/features/wallet/context'
import { selectActiveAccount } from 'wallet/src/features/wallet/selectors'
......@@ -61,6 +61,7 @@ function sendAccountResponseAnalyticsEvent(
connectedAddresses: accountResponse.connectedAddresses,
})
}
/**
* 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.
......
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 {
RevokePermissionsRequest,
RevokePermissionsResponse,
} from 'src/app/features/dappRequests/types/DappRequestTypes'
import { extractBaseUrl } from 'src/app/features/dappRequests/utils'
import { dappResponseMessageChannel } from 'src/background/messagePassing/messageChannels'
import { Permission } from 'src/contentScript/WindowEthereumRequestTypes'
import { ExtensionEthMethods } from 'src/contentScript/methodHandlers/requestMethods'
......@@ -23,6 +22,7 @@ import { call, put } from 'typed-redux-saga'
import { chainIdToHexadecimalString } from 'uniswap/src/features/chains/utils'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
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[] {
const permissions: Permission[] = []
......
......@@ -21,7 +21,6 @@ import {
UniswapOpenSidebarResponse,
} from 'src/app/features/dappRequests/types/DappRequestTypes'
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 { AppRoutes, HomeQueryParams } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state'
......@@ -35,6 +34,7 @@ import {
TransactionType,
TransactionTypeInfo,
} from 'uniswap/src/features/transactions/types/transactionDetails'
import { extractBaseUrl } from 'utilities/src/format/urls'
import { logger } from 'utilities/src/logger/logger'
import { SendTransactionParams, sendTransaction } from 'wallet/src/features/transactions/sendTransactionSaga'
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'
import { useDappContext } from 'src/app/features/dapp/DappContext'
import { removeDappConnection, saveDappChain } from 'src/app/features/dapp/actions'
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 { Anchor, Button, Flex, Popover, Separator, Text, getTokenValue } from 'ui/src'
import { Check, Power } from 'ui/src/components/icons'
......@@ -17,6 +16,7 @@ import { useEnabledChains } from 'uniswap/src/features/settings/hooks'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { UniverseChainId } from 'uniswap/src/types/chains'
import { extractUrlHost } from 'utilities/src/format/urls'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
const BUTTON_OFFSET = 20
......
......@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'
import { useInterfaceBuyNavigator } from 'src/app/features/for/utils'
import { AppRoutes } from 'src/app/navigation/constants'
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 { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard'
import { InfoLinkModal } from 'uniswap/src/components/modals/InfoLinkModal'
......@@ -15,6 +15,7 @@ import { useEnabledChains } from 'uniswap/src/features/settings/hooks'
import { ElementName, ModalName, SectionName, WalletEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { InformationBanner } from 'wallet/src/components/banners/InformationBanner'
import { ContextMenu } from 'wallet/src/components/menu/ContextMenu'
import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext'
import { isNonPollingRequestInFlight } from 'wallet/src/data/utils'
import { HiddenTokensRow } from 'wallet/src/features/portfolio/HiddenTokensRow'
......@@ -211,7 +212,12 @@ const TokenBalanceItemRow = memo(function TokenBalanceItemRow({ item }: { item:
return (
<TokenContextMenu portfolioBalance={portfolioBalance}>
<TokenBalanceItem isLoading={isWarmLoading} portfolioBalance={portfolioBalance} onPressToken={onPressToken} />
<TokenBalanceItem
isLoading={isWarmLoading}
portfolioBalanceId={portfolioBalance.id}
currencyInfo={portfolioBalance.currencyInfo}
onPressToken={onPressToken}
/>
</TokenContextMenu>
)
})
......
import { useCallback } from 'react'
import { UnitagClaimRoutes } from 'src/app/navigation/constants'
import { focusOrCreateUnitagTab } from 'src/app/navigation/utils'
import { Flex } from 'ui/src'
import { PollingInterval } from 'uniswap/src/constants/misc'
......@@ -18,8 +19,8 @@ export function HomeIntroCardStack(): JSX.Element | null {
})
const navigateToUnitagClaim = useCallback(async () => {
await focusOrCreateUnitagTab()
}, [])
await focusOrCreateUnitagTab(activeAccount.address, UnitagClaimRoutes.ClaimIntro)
}, [activeAccount.address])
const { cards } = useSharedIntroCards({
navigateToUnitagClaim,
......
......@@ -15,12 +15,16 @@ import { ClaimUnitagContent } from 'wallet/src/features/unitags/ClaimUnitagConte
export function ClaimUnitagScreen(): JSX.Element {
const { t } = useTranslation()
const { goToNextStep } = useOnboardingSteps()
const { resetOnboardingContextData, getOnboardingAccountAddress } = useOnboardingContext()
const { resetOnboardingContextData, getOnboardingAccountAddress, addUnitagClaim } = useOnboardingContext()
const onboardingAccountAddress = getOnboardingAccountAddress()
const onNextStep = useCallback(async () => {
const onComplete = useCallback(
(unitag: string) => {
addUnitagClaim({ username: unitag })
goToNextStep()
}, [goToNextStep])
},
[goToNextStep, addUnitagClaim],
)
const handleBack = useCallback(() => {
// reset the pending mnemonic when going back from password screen
......@@ -44,14 +48,14 @@ export function ClaimUnitagScreen(): JSX.Element {
subtitle={t('unitags.onboarding.claim.subtitle')}
title={t('unitags.onboarding.claim.title.choose')}
onBack={handleBack}
onSkip={onNextStep}
onSkip={goToNextStep}
>
<Flex gap="$spacing16" py="$spacing24" width="100%">
<ClaimUnitagContent
animateY={false}
entryPoint={ExtensionOnboardingFlow.New}
unitagAddress={onboardingAccountAddress}
onComplete={onNextStep}
onComplete={onComplete}
/>
</Flex>
</OnboardingScreen>
......
......@@ -15,15 +15,35 @@ import { iconSizes } from 'ui/src/theme'
import { uniswapUrls } from 'uniswap/src/constants/urls'
import { ExtensionOnboardingFlow } from 'uniswap/src/types/screens/extension'
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 { getOnboardingAccountAddress, addUnitagClaim, getUnitagClaim } = useOnboardingContext()
const address = getOnboardingAccountAddress()
const existingClaim = getUnitagClaim()
const [unitagClaimAttempted, setUnitagClaimAttempted] = 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
useFinishOnboarding(terminateStoreSynchronization, flow)
useFinishOnboarding(terminateStoreSynchronization, flow, tryToClaimUnitag && !unitagClaimAttempted)
useEffect(() => {
const onSidebarOpenedListener = onboardingMessageChannel.addMessageListener(
......
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import { terminateStoreSynchronization } from 'src/store/storeSynchronization'
import { Flex, Text } from 'ui/src'
import { Check, GraduationCap } from 'ui/src/components/icons'
import { uniswapUrls } from 'uniswap/src/constants/urls'
import { useFinishOnboarding } from 'wallet/src/features/onboarding/OnboardingContext'
export function ResetComplete(): JSX.Element {
......@@ -22,12 +24,18 @@ export function ResetComplete(): JSX.Element {
{t('onboarding.resetPassword.complete.subtitle')}
</Text>
</Flex>
<Link
style={{ textDecoration: 'none' }}
target="_blank"
to={uniswapUrls.helpArticleUrls.walletSecurityMeasures}
>
<Flex row alignItems="center" gap="$spacing8">
<GraduationCap color="$neutral3" size="$icon.20" />
<Text color="$neutral3" variant="buttonLabel2">
{t('onboarding.resetPassword.complete.safety')}
</Text>
</Flex>
</Link>
</Flex>
</>
)
......
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import { saveDappConnection } from 'src/app/features/dapp/actions'
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 { X } from 'ui/src/components/icons'
import { uniswapUrls } from 'uniswap/src/constants/urls'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { extractUrlHost } from 'utilities/src/format/urls'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
export function ConnectPopupContent({
......
......@@ -35,7 +35,7 @@ export function RecipientPanel({ chainId }: RecipientPanelProps): JSX.Element {
onSetShowRecipientSelector(!showRecipientSelector)
}, [onSetShowRecipientSelector, showRecipientSelector])
const sections = useFilteredRecipientSections(pattern)
const { sections } = useFilteredRecipientSections(pattern)
const onSelectRecipient = useCallback((newRecipient: string) => {
setSelectedRecipient(newRecipient)
......
......@@ -5,7 +5,6 @@ import { ScreenHeader } from 'src/app/components/layout/ScreenHeader'
import { removeDappConnection } from 'src/app/features/dapp/actions'
import { useAllDappConnectionsForActiveAccount } from 'src/app/features/dapp/hooks'
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 { NoDappConnections } from 'src/app/features/settings/SettingsManageConnectionsScreen/internal/NoDappConnections'
import { Flex, Text, TouchableArea, UniversalImage, useSporeColors } from 'ui/src'
......@@ -18,6 +17,7 @@ import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { ExtensionScreens } from 'uniswap/src/types/screens/extension'
import { extractNameFromUrl } from 'utilities/src/format/extractNameFromUrl'
import { extractUrlHost } from 'utilities/src/format/urls'
import { DappIconPlaceholder } from 'wallet/src/components/WalletConnect/DappIconPlaceholder'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
......
import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
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 { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
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'
const PowerCircle = (): JSX.Element => (
......
......@@ -37,7 +37,7 @@ export function SettingsRecoveryPhrase({
</Flex>
</Flex>
<Flex grow>{children}</Flex>
<Flex>
<Flex mt="$spacing12">
<Button
disabled={!nextButtonEnabled}
flexGrow={1}
......
......@@ -182,9 +182,8 @@ export function SettingsScreen(): JSX.Element {
/>
<SettingsToggleRow
Icon={ShieldQuestion}
checked={hideSpamTokens && !isTestnetModeEnabled}
checked={hideSpamTokens}
title={t('settings.setting.unknownTokens.title')}
disabled={isTestnetModeEnabled}
onCheckedChange={handleSpamTokensToggle}
/>
<SettingsItem
......
import { useMemo, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { OnboardingScreen } from 'src/app/features/onboarding/OnboardingScreen'
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 { iconSizes } from 'ui/src/theme'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks'
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 { DeleteUnitagModal } from 'wallet/src/features/unitags/DeleteUnitagModal'
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 {
const { t } = useTranslation()
const address = useActiveAccountAddressWithThrow()
const { unitag: retrievedUnitag } = useUnitagByAddress(address)
const address = useAccountAddressFromUrlWithThrow()
const { unitag: retrievedUnitag, pending, fetching } = useUnitagByAddress(address)
const unitag = retrievedUnitag?.username
useEffect(() => {
if (!pending && !fetching && !unitag) {
navigate(UnitagClaimRoutes.ClaimIntro)
}
}, [unitag, pending, fetching])
const { goToPreviousStep } = useOnboardingSteps()
const [showDeleteUnitagModal, setShowDeleteUnitagModal] = useState(false)
......@@ -40,6 +52,12 @@ export function EditUnitagProfileScreen({ enableBack = false }: { enableBack?: b
]
}, [t, setShowChangeUnitagModal, setShowDeleteUnitagModal])
const refreshUnitags = async (): Promise<void> => {
await backgroundToSidePanelMessageChannel.sendMessage({
type: BackgroundToSidePanelRequestType.RefreshUnitags,
})
}
return (
<Trace logImpression screen={UnitagScreens.EditProfile}>
<OnboardingScreen
......@@ -63,6 +81,7 @@ export function EditUnitagProfileScreen({ enableBack = false }: { enableBack?: b
<DeleteUnitagModal
address={address}
unitag={unitag}
onSuccess={refreshUnitags}
onClose={(): void => setShowDeleteUnitagModal(false)}
/>
)}
......@@ -70,6 +89,7 @@ export function EditUnitagProfileScreen({ enableBack = false }: { enableBack?: b
<ChangeUnitagModal
address={address}
unitag={unitag}
onSuccess={refreshUnitags}
onClose={(): void => setShowChangeUnitagModal(false)}
/>
)}
......
......@@ -13,12 +13,12 @@ import { ExtensionUnitagClaimScreens } from 'uniswap/src/types/screens/extension
import { logger } from 'utilities/src/logger/logger'
import { extensionNftModalProps } from 'wallet/src/features/unitags/ChooseNftModal'
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 {
const { goToNextStep, goToPreviousStep } = useOnboardingSteps()
const { unitag, entryPoint, setProfilePicUri } = useUnitagClaimContext()
const address = useActiveAccountAddressWithThrow()
const address = useAccountAddressFromUrlWithThrow()
const onNavigateContinue = useCallback(
async (imageUri: string | undefined) => {
......@@ -51,8 +51,8 @@ export function UnitagChooseProfilePicScreen(): JSX.Element {
<Person color="$neutral1" size={iconSizes.icon24} />
</Square>
}
title={t('unitags.onboarding.claim.title.choose')}
subtitle={t('unitags.onboarding.claim.subtitle')}
title={t('unitags.onboarding.profile.title')}
subtitle={t('unitags.onboarding.profile.subtitle')}
onBack={goToPreviousStep}
>
<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'
import { logger } from 'utilities/src/logger/logger'
import { UnitagWithProfilePicture } from 'wallet/src/features/unitags/UnitagWithProfilePicture'
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 {
const { t } = useTranslation()
const address = useActiveAccountAddressWithThrow()
const address = useAccountAddressFromUrlWithThrow()
const { unitag, profilePicUri } = useUnitagClaimContext()
const { goToNextStep } = useOnboardingSteps()
......@@ -35,7 +35,7 @@ export function UnitagConfirmationScreen(): JSX.Element {
return (
<OnboardingScreen>
<Flex grow gap="$spacing12" pt="$spacing24">
<Flex centered>
<Flex centered py="$spacing12">
<UnitagWithProfilePicture address={address} profilePictureUri={profilePicUri} unitag={unitag} />
</Flex>
<Flex centered gap="$spacing12">
......
......@@ -9,14 +9,14 @@ import { iconSizes } from 'ui/src/theme'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ExtensionScreens, ExtensionUnitagClaimScreens } from 'uniswap/src/types/screens/extension'
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>
export function UnitagCreateUsernameScreen(): JSX.Element {
const { goToNextStep, goToPreviousStep } = useOnboardingSteps()
const { setUnitag, setEntryPoint } = useUnitagClaimContext()
const address = useActiveAccountAddressWithThrow()
const address = useAccountAddressFromUrlWithThrow()
const onNavigateContinue = useCallback(
({ unitag, entryPoint }: Parameters<onNavigateContinueType>[0]) => {
......
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useOnboardingSteps } from 'src/app/features/onboarding/OnboardingStepsContext'
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 { 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 TERMS_WIDTH = 300
......@@ -11,6 +16,15 @@ export function UnitagIntroScreen(): JSX.Element {
const { t } = useTranslation()
const { goToNextStep } = useOnboardingSteps()
const address = useAccountAddressFromUrlWithThrow()
const { unitag } = useUnitagByAddress(address)
useEffect(() => {
if (unitag?.address) {
navigate(UnitagClaimRoutes.EditProfile)
}
}, [unitag])
return (
<Flex centered height="100%" width="100%">
<Flex centered width={CONTAINER_WIDTH} gap="$spacing40">
......
......@@ -13,6 +13,10 @@ export enum OnboardingRoutes {
Reset = 'reset',
ResetScan = 'reset-scan',
UnsupportedBrowser = 'unsupported-browser',
}
export enum UnitagClaimRoutes {
ClaimIntro = 'claim-intro',
EditProfile = 'edit-profile',
}
......
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 { onboardingMessageChannel } from 'src/background/messagePassing/messageChannels'
import { OnboardingMessageType } from 'src/background/messagePassing/types/ExtensionMessages'
......@@ -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 tabs = await chrome.tabs.query({ url: `chrome-extension://${extension.id}/unitagClaim.html*` })
const tab = tabs[0]
const url = `unitagClaim.html#/${page ?? ''}`
const url = `unitagClaim.html#/${page}?address=${address}`
if (!tab?.id) {
await chrome.tabs.create({ url })
......
......@@ -2,9 +2,9 @@ import '@tamagui/core/reset.css'
import 'src/app/Global.css'
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 { uniswapUrls } from 'uniswap/src/constants/urls'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { ApplicationTransport } from 'utilities/src/telemetry/analytics/ApplicationTransport'
// eslint-disable-next-line no-restricted-imports
import { analytics, getAnalyticsAtomDirect } from 'utilities/src/telemetry/analytics/analytics'
......@@ -18,6 +18,6 @@ export async function initExtensionAnalytics(): Promise<void> {
}),
analyticsAllowed,
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 {
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 {
DEV = 'development',
BETA = 'beta',
......
......@@ -4,13 +4,13 @@ import { initStatSigForBrowserScripts } from 'src/app/StatsigProvider'
import { focusOrCreateOnboardingTab } from 'src/app/navigation/utils'
import { SentryAppNameTag, initSentryForBrowserScripts } from 'src/app/sentry'
import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { getLocalUserId } from 'src/app/utils/storage'
import { initMessageBridge } from 'src/background/backgroundDappRequests'
import { backgroundStore } from 'src/background/backgroundStore'
import { backgroundToSidePanelMessageChannel } from 'src/background/messagePassing/messageChannels'
import { BackgroundToSidePanelRequestType } from 'src/background/messagePassing/types/requests'
import { setSidePanelBehavior, setSidePanelOptions } from 'src/background/utils/chromeSidePanelUtils'
import { readIsOnboardedFromStorage } from 'src/background/utils/persistedStateUtils'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger'
export const EXTENSION_ID = chrome.runtime.id
......@@ -18,7 +18,7 @@ export const EXTENSION_ID = chrome.runtime.id
initMessageBridge()
async function initApp(): Promise<void> {
const userId = await getLocalUserId()
const userId = await getUniqueId()
initSentryForBrowserScripts(SentryAppNameTag.Background, userId)
await initStatSigForBrowserScripts()
await initExtensionAnalytics()
......
......@@ -10,7 +10,6 @@ import {
DappResponseType,
RevokePermissionsRequest,
} from 'src/app/features/dappRequests/types/DappRequestTypes'
import { extractBaseUrl } from 'src/app/features/dappRequests/utils'
import { focusOrCreateOnboardingTab } from 'src/app/navigation/utils'
import {
DappBackgroundPortChannel,
......@@ -30,6 +29,7 @@ import { hexadecimalStringToInt, toSupportedChainId } from 'uniswap/src/features
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants/extension'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { WindowEthereumRequestProperties } from 'uniswap/src/features/telemetry/types'
import { extractBaseUrl } from 'utilities/src/format/urls'
import { logger } from 'utilities/src/logger/logger'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
import { walletContextValue } from 'wallet/src/features/wallet/context'
......
......@@ -5,7 +5,6 @@ import {
DappResponseType,
SendTransactionRequest,
} from 'src/app/features/dappRequests/types/DappRequestTypes'
import { extractBaseUrl } from 'src/app/features/dappRequests/utils'
import {
contentScriptToBackgroundMessageChannel,
dappResponseMessageChannel,
......@@ -41,6 +40,7 @@ import { WindowEthereumRequest } from 'src/contentScript/types'
import { chainIdToHexadecimalString } from 'uniswap/src/features/chains/utils'
import { UniverseChainId } from 'uniswap/src/types/chains'
import { areAddressesEqual } from 'uniswap/src/utils/addresses'
import { extractBaseUrl } from 'utilities/src/format/urls'
export class ExtensionEthMethodHandler extends BaseMethodHandler<WindowEthereumRequest> {
private readonly requestIdToSourceMap: Map<string, PendingResponseInfo> = new Map()
......
......@@ -5,21 +5,21 @@ import React from 'react'
import { createRoot } from 'react-dom/client'
import OnboardingApp from 'src/app/OnboardingApp'
import { initializeSentry, SentryAppNameTag } from 'src/app/sentry'
import { getLocalUserId } from 'src/app/utils/storage'
import { initializeReduxStore } from 'src/store/store'
import { ExtensionAppLocation, StoreSynchronization } from 'src/store/storeSynchronization'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger'
;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any
// The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem
// see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326
getLocalUserId()
getUniqueId()
.then((userId) => {
initializeSentry(SentryAppNameTag.Onboarding, userId)
})
.catch((error) => {
logger.error(error, {
tags: { file: 'SidebarApp.tsx', function: 'getLocalUserId' },
tags: { file: 'SidebarApp.tsx', function: 'getUniqueId' },
})
})
async function initOnboarding(): Promise<void> {
......
......@@ -5,20 +5,20 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import PopupApp from 'src/app/PopupApp'
import { initializeSentry, SentryAppNameTag } from 'src/app/sentry'
import { getLocalUserId } from 'src/app/utils/storage'
import { initializeReduxStore } from 'src/store/store'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger'
;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any
// The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem
// see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326
getLocalUserId()
getUniqueId()
.then((userId) => {
initializeSentry(SentryAppNameTag.Popup, userId)
})
.catch((error) => {
logger.error(error, {
tags: { file: 'popup.tsx', function: 'getLocalUserId' },
tags: { file: 'popup.tsx', function: 'getUniqueId' },
})
})
async function initPopup(): Promise<void> {
......
......@@ -5,20 +5,20 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import UnitagClaimApp from 'src/app/UnitagClaimApp'
import { SentryAppNameTag, initializeSentry } from 'src/app/sentry'
import { getLocalUserId } from 'src/app/utils/storage'
import { initializeReduxStore } from 'src/store/store'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger'
;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any
// The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem
// see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326
getLocalUserId()
getUniqueId()
.then((userId) => {
initializeSentry(SentryAppNameTag.UnitagClaim, userId)
})
.catch((error) => {
logger.error(error, {
tags: { file: 'unitagClaim.tsx', function: 'getLocalUserId' },
tags: { file: 'unitagClaim.tsx', function: 'getUniqueId' },
})
})
async function initUnitagClaim(): Promise<void> {
......
......@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Uniswap Extension",
"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",
"icons": {
"16": "assets/icon16.png",
......
......@@ -13,6 +13,7 @@ import {
readDeprecatedReduxedChromeStorage,
} from 'src/store/reduxedChromeStorageToReduxPersistMigration'
import { fiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api'
import { createDatadogReduxEnhancer } from 'utilities/src/logger/Datadog'
import { createStore } from 'wallet/src/state'
import { createMigrate } from 'wallet/src/state/createMigrate'
......@@ -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> => {
return createStore({
reducer: persistedReducer,
......@@ -45,7 +53,7 @@ const setupStore = (preloadedState?: PreloadedState<ExtensionState>): ReturnType
additionalSagas: [rootExtensionSaga],
middlewareBefore: __DEV__ ? [loggerMiddleware] : [],
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 = {
root: true,
extends: ['@uniswap/eslint-config/native'],
parserOptions: {
project: 'tsconfig.json',
project: 'tsconfig.eslint.json',
tsconfigRootDir: __dirname,
ecmaFeatures: {
jsx: true,
......
This diff is collapsed.
......@@ -90,9 +90,9 @@ if (isCI && datadogPropertiesAvailable && !isDetox) {
apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle"
}
def devVersionName = "1.38"
def betaVersionName = "1.38"
def prodVersionName = "1.38"
def devVersionName = "1.39"
def betaVersionName = "1.39"
def prodVersionName = "1.39"
android {
ndkVersion rootProject.ext.ndkVersion
......
......@@ -183,7 +183,7 @@ class SeedPhraseInputViewModel: ObservableObject {
error = nil
}
let canSubmit = error == nil && mnemonic != "" && firstInvalidWord == ""
let canSubmit = error == nil && mnemonic != "" && firstInvalidWord == "" && isValidLength
onInputValidated(["canSubmit": canSubmit])
}
}
......@@ -9,6 +9,7 @@
<string>applinks:uniswap.org</string>
<string>applinks:app.uniswap.org</string>
<string>applinks:app.corn-staging.com</string>
<string>webcredentials:app.uniswap.org</string>
</array>
<key>com.apple.developer.devicecheck.appattest-environment</key>
<string>production</string>
......
......@@ -90,7 +90,7 @@
"@uniswap/analytics-events": "2.38.0",
"@uniswap/client-explore": "0.0.10",
"@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/react-native-compat": "2.17.1",
"@walletconnect/utils": "2.17.1",
......
......@@ -14,7 +14,7 @@ import { PropsWithChildren, default as React, StrictMode, useCallback, useEffect
import { I18nextProvider } from 'react-i18next'
import { LogBox, NativeModules, StatusBar } from 'react-native'
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 { MMKV } from 'react-native-mmkv'
import { SafeAreaProvider } from 'react-native-safe-area-context'
......@@ -72,8 +72,9 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context'
import i18n from 'uniswap/src/i18n/i18n'
import { CurrencyId } from 'uniswap/src/types/currency'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
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 { logger } from 'utilities/src/logger/logger'
import { useAsyncData } from 'utilities/src/react/hooks'
......@@ -170,6 +171,7 @@ function App(): JSX.Element | null {
// Hence we always initiliase and later close it if Datadog is enabled.
Sentry.close().catch(() => undefined)
attachUnhandledRejectionHandler()
setAttributesToDatadog({ buildNumber: DeviceInfo.getBuildNumber() }).catch(() => undefined)
}
}, [isDatadogEnabled])
......@@ -246,6 +248,7 @@ function App(): JSX.Element | null {
function DatadogProviderWrapper({ children }: PropsWithChildren): JSX.Element {
const datadogEnabled = useFeatureFlagWithExposureLoggingDisabled(FeatureFlags.Datadog)
logger.setWalletDatadogEnabled(datadogEnabled)
if (isDetoxBuild || isJestRun || !datadogEnabled) {
return <>{children}</>
......
......@@ -20,6 +20,7 @@ import { useAppInsets } from 'uniswap/src/hooks/useAppInsets'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { areAddressesEqual } from 'uniswap/src/utils/addresses'
import { isAndroid } from 'utilities/src/platform'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { PlusCircle } from 'wallet/src/components/icons/PlusCircle'
......@@ -245,7 +246,8 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
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) {
return null
......
......@@ -6,7 +6,8 @@ import { closeModal, openModal } from 'src/features/modals/modalSlice'
import { LockPreviewImage } from 'src/features/onboarding/LockPreviewImage'
import { Button, Flex, Text } from 'ui/src'
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 { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { setBackupReminderLastSeenTs } from 'wallet/src/features/behaviorHistory/slice'
......@@ -58,6 +59,7 @@ export function BackupReminderModal(): JSX.Element {
</Text>
</Flex>
<Flex row gap="$spacing8">
<Trace logPress element={ElementName.MaybeLaterButton} modal={ModalName.BackupReminder}>
<Button
alignSelf="center"
color="$neutral2"
......@@ -68,9 +70,12 @@ export function BackupReminderModal(): JSX.Element {
>
{t('common.button.later')}
</Button>
</Trace>
<Trace logPress element={ElementName.Continue} modal={ModalName.BackupReminder}>
<Button alignSelf="center" flex={1} size="medium" theme="primary" onPress={onPressBackup}>
{t('common.button.continue')}
</Button>
</Trace>
</Flex>
</Flex>
</Modal>
......
......@@ -4,7 +4,8 @@ import { useDispatch } from 'react-redux'
import { closeModal, openModal } from 'src/features/modals/modalSlice'
import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal'
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'
export function BackupWarningModal(): JSX.Element {
......@@ -17,11 +18,18 @@ export function BackupWarningModal(): JSX.Element {
}
const checkForSwipeToDismiss = (): void => {
if (!closedByButtonRef.current) {
const markReminderAsSeen = !closedByButtonRef.current
if (markReminderAsSeen) {
// Modal was swiped to dismiss, should set backup reminder timestamp
dispatch(setBackupReminderLastSeenTs(Date.now()))
}
sendAnalyticsEvent(WalletEventName.ModalClosed, {
element: ElementName.BackButton,
modal: ModalName.BackupReminderWarning,
markReminderAsSeen,
})
// Reset the ref and close the modal
closedByButtonRef.current = false
onClose()
......
......@@ -17,7 +17,7 @@ import { processWidgetEvents } from 'src/features/widgets/widgets'
import { useSporeColors } from 'ui/src'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
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 { sleep } from 'utilities/src/time/timing'
......@@ -30,7 +30,7 @@ export const navigationRef = createNavigationContainerRef()
/** Wrapped `NavigationContainer` with telemetry tracing. */
export const NavigationContainer: FC<PropsWithChildren<Props>> = ({ children, onReady }: PropsWithChildren<Props>) => {
const colors = useSporeColors()
const [routeName, setRouteName] = useState<MobileAppScreen>()
const [routeName, setRouteName] = useState<MobileNavScreen>()
const [routeParams, setRouteParams] = useState<Record<string, unknown> | undefined>()
const [logImpression, setLogImpression] = useState<boolean>(false)
......@@ -51,7 +51,7 @@ export const NavigationContainer: FC<PropsWithChildren<Props>> = ({ children, on
processWidgetEvents().catch(() => undefined)
// setting initial route name for telemetry
const initialRoute = navigationRef.getCurrentRoute()?.name as MobileAppScreen
const initialRoute = navigationRef.getCurrentRoute()?.name as MobileNavScreen
setRouteName(initialRoute)
if (!__DEV__) {
......@@ -60,7 +60,7 @@ export const NavigationContainer: FC<PropsWithChildren<Props>> = ({ children, on
}}
onStateChange={(): void => {
const previousRouteName = routeName
const currentRouteName: MobileAppScreen = navigationRef.getCurrentRoute()?.name as MobileAppScreen
const currentRouteName: MobileNavScreen = navigationRef.getCurrentRoute()?.name as MobileNavScreen
if (
currentRouteName &&
......@@ -69,7 +69,7 @@ export const NavigationContainer: FC<PropsWithChildren<Props>> = ({ children, on
) {
const currentRouteParams = getEventParams(
currentRouteName,
navigationRef.getCurrentRoute()?.params as RootParamList[MobileAppScreen],
navigationRef.getCurrentRoute()?.params as RootParamList[MobileNavScreen],
)
setLogImpression(true)
setRouteName(currentRouteName)
......
import { SCREEN_WIDTH } from '@gorhom/bottom-sheet'
import _ from 'lodash'
import times from 'lodash/times'
import React, { useEffect, useState } from 'react'
import { StyleSheet, Text, View } from 'react-native'
import Animated, {
......@@ -252,9 +252,7 @@ const Numbers = ({
const commaIndex = numberOfDigits.left + Math.floor((numberOfDigits.left - 1) / 3)
return _.times(
numberOfDigits.left + numberOfDigits.right + Math.floor((numberOfDigits.left - 1) / 3) + 1,
(index) => (
return times(numberOfDigits.left + numberOfDigits.right + Math.floor((numberOfDigits.left - 1) / 3) + 1, (index) => (
<Animated.View
key={`$number_${index - commaIndex}`}
style={[{ height: DIGIT_HEIGHT }, AnimatedCharStyles.wrapperStyle]}
......@@ -270,8 +268,7 @@ const Numbers = ({
shouldAnimate={price.shouldAnimate}
/>
</Animated.View>
),
)
))
}
const LoadingWrapper = (): JSX.Element | null => {
......
import { maxBy } from 'lodash'
import maxBy from 'lodash/maxBy'
import { Dispatch, SetStateAction, useCallback, useMemo, useRef, useState } from 'react'
import { SharedValue, useDerivedValue } from 'react-native-reanimated'
import { TLineChartData } from 'react-native-wagmi-charts'
......
......@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import { TextInput } from 'react-native'
import { FadeIn, FadeOut } from 'react-native-reanimated'
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 { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { iconSizes } from 'ui/src/theme'
......@@ -51,7 +51,7 @@ export function _RecipientSelect({
const [showQRScanner, setShowQRScanner] = useState(false)
const [checkSpeedBumps, setCheckSpeedBumps] = useState(false)
const [selectedRecipient, setSelectedRecipient] = useState(recipient)
const sections = useFilteredRecipientSections(pattern)
const { sections, loading } = useFilteredRecipientSections(pattern)
useEffect(() => {
if (focusInput) {
......@@ -104,7 +104,9 @@ export function _RecipientSelect({
onBack={recipient ? onHideRecipientSelector : undefined}
onChangeText={setPattern}
/>
{!sections.length ? (
{loading ? (
<Loader.SearchResult />
) : !sections.length ? (
<Flex centered gap="$spacing12" mt="$spacing24" px="$spacing24">
<Text variant="buttonLabel2">{t('send.recipient.results.empty')}</Text>
<Text color="$neutral3" textAlign="center" variant="body1">
......@@ -112,7 +114,6 @@ export function _RecipientSelect({
</Text>
</Flex>
) : (
// Show either suggested recipients or filtered sections based on query
<RecipientList renderedInModal={renderedInModal} sections={sections} onPress={onSelect} />
)}
</AnimatedFlex>
......
......@@ -20,6 +20,7 @@ import { ElementName, ModalName, WalletEventName } from 'uniswap/src/features/te
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { areAddressesEqual } from 'uniswap/src/utils/addresses'
import { logger } from 'utilities/src/logger/logger'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga'
......@@ -43,7 +44,8 @@ export function RemoveWalletModal(): JSX.Element | null {
// This happens when user wants to replace mnemonic with a new one
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 isRemovingRecoveryPhrase = isReplacing || isRemovingLastMnemonic
......
import React from 'react'
import { Flex, Separator, Text, Unicon, useSporeColors } from 'ui/src'
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 { Account } from 'wallet/src/features/wallet/accounts/types'
import { useDisplayName } from 'wallet/src/features/wallet/hooks'
......@@ -32,7 +32,7 @@ export const SwitchAccountOption = ({ account, activeAccount }: Props): JSX.Elem
</Text>
</Flex>
<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} />
)}
</Flex>
......
......@@ -109,8 +109,8 @@ export function SettingsRow({
) : screen || modal ? (
<Flex centered row>
{currentSetting ? (
<Flex row shrink alignItems="flex-end" flexBasis="30%" justifyContent="flex-end">
<Text adjustsFontSizeToFit color="$neutral2" mr="$spacing8" numberOfLines={1} variant="body3">
<Flex shrink alignItems="flex-end" flexBasis="35%" justifyContent="flex-end">
<Text adjustsFontSizeToFit color="$neutral2" mr="$spacing8" numberOfLines={2} variant="body3">
{currentSetting}
</Text>
</Flex>
......
import React, { memo, useMemo } from 'react'
import React, { PropsWithChildren, memo, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import ContextMenu from 'react-native-context-menu-view'
import { borderRadii } from 'ui/src/theme'
......@@ -6,13 +6,12 @@ import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generat
import { PortfolioBalance } from 'uniswap/src/features/dataApi/types'
import { useTokenContextMenu } from 'wallet/src/features/portfolio/useTokenContextMenu'
export const TokenBalanceItemContextMenu = memo(function _TokenBalanceItem({
export const TokenBalanceItemContextMenu = memo(function _TokenBalanceItemContextMenu({
portfolioBalance,
children,
}: {
}: PropsWithChildren<{
portfolioBalance: PortfolioBalance
children: React.ReactNode
}) {
}>) {
const { t } = useTranslation()
const { menuActions, onContextMenuPress } = useTokenContextMenu({
......
import { BottomSheetFlatList } from '@gorhom/bottom-sheet'
import { useFocusEffect } from '@react-navigation/core'
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 { FlatList, RefreshControl } from 'react-native'
import Animated, { FadeInDown, FadeOut } from 'react-native-reanimated'
......@@ -22,6 +22,7 @@ import { useAppInsets } from 'uniswap/src/hooks/useAppInsets'
import { CurrencyId } from 'uniswap/src/types/currency'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { isAndroid } from 'utilities/src/platform'
import { useValueAsRef } from 'utilities/src/react/useValueAsRef'
import { InformationBanner } from 'wallet/src/components/banners/InformationBanner'
import { isError, isNonPollingRequestInFlight } from 'wallet/src/data/utils'
import { HiddenTokensRow } from 'wallet/src/features/portfolio/HiddenTokensRow'
......@@ -72,12 +73,10 @@ export const TokenBalanceListInner = forwardRef<FlatList<TokenBalanceListRow>, T
},
ref,
) {
const { t } = useTranslation()
const colors = useSporeColors()
const insets = useAppInsets()
const { rows, balancesById, networkStatus, refetch } = useTokenBalanceListContext()
const hasError = isError(networkStatus, !!balancesById)
const { rows, balancesById } = useTokenBalanceListContext()
const { onContentSizeChange, adaptiveFooter } = useAdaptiveFooter(containerProps?.contentContainerStyle)
......@@ -90,8 +89,7 @@ export const TokenBalanceListInner = forwardRef<FlatList<TokenBalanceListRow>, T
const [isFocused, setIsFocused] = useState<boolean>(true)
const [cachedRows, setCachedRows] = useState<TokenBalanceListRow[] | null>(null)
const rowsRef = useRef(rows)
rowsRef.current = rows
const rowsRef = useValueAsRef(rows)
useFocusEffect(
useCallback(() => {
......@@ -101,7 +99,7 @@ export const TokenBalanceListInner = forwardRef<FlatList<TokenBalanceListRow>, T
setCachedRows(rowsRef.current)
setIsFocused(false)
}
}, []),
}, [rowsRef]),
)
const navigation = useAppStackNavigation()
......@@ -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.
// 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(
({ item, index }: { item: TokenBalanceListRow; index: number }): JSX.Element => (
<TokenBalanceItemRow index={index} item={item} />
),
({ item }: { item: TokenBalanceListRow }): JSX.Element => <TokenBalanceItemRow item={item} />,
[],
)
const keyExtractor = useCallback((item: TokenBalanceListRow): string => item, [])
const ListEmptyComponent = useMemo(() => {
if (hasError) {
return (
<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])
return <EmptyComponent renderEmpty={empty} />
}, [empty])
const ListHeaderComponent = useMemo(() => {
return hasError ? (
<AnimatedFlex entering={FadeInDown} exiting={FadeOut} px="$spacing24" py="$spacing8">
<BaseCard.InlineErrorState title={t('home.tokens.error.fetch')} onRetry={refetch} />
</AnimatedFlex>
) : null
}, [hasError, refetch, t])
return <HeaderComponent />
}, [])
// add negative z index to prevent footer from covering hidden tokens row when minimized
const ListFooterComponentStyle = useMemo(() => ({ zIndex: zIndices.negative }), [])
......@@ -232,40 +200,119 @@ export const TokenBalanceListInner = forwardRef<FlatList<TokenBalanceListRow>, T
},
)
const TokenBalanceItemRow = memo(function TokenBalanceItemRow({
item,
index,
const HeaderComponent = memo(function _HeaderComponent(): JSX.Element | null {
const { t } = useTranslation()
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
index?: number
}) {
const {
balancesById,
hiddenTokensCount,
hiddenTokensExpanded,
isWarmLoading,
onPressToken,
setHiddenTokensExpanded,
} = useTokenBalanceListContext()
renderEmpty?: JSX.Element | null
}): JSX.Element {
const { t } = useTranslation()
const { balancesById, networkStatus, refetch } = useTokenBalanceListContext()
const shouldShowLoaderSkeleton = isNonPollingRequestInFlight(networkStatus)
const hasError = isError(networkStatus, !!balancesById)
if (hasError) {
return (
<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 { hiddenTokensCount, hiddenTokensExpanded, setHiddenTokensExpanded } = useTokenBalanceListContext()
const [isModalVisible, setModalVisible] = useState(false)
const handlePressToken = (): void => {
const handlePressToken = useCallback((): void => {
setModalVisible(true)
}
}, [])
const closeModal = (): void => {
const closeModal = useCallback((): void => {
setModalVisible(false)
}
}, [])
const handleAnalytics = (): void => {
const handleAnalytics = useCallback((): void => {
sendAnalyticsEvent(WalletEventName.ExternalLinkOpened, {
url: uniswapUrls.helpArticleUrls.hiddenTokenInfo,
})
}
}, [])
if (item === HIDDEN_TOKEN_BALANCES_ROW) {
return (
<Flex grow>
<HiddenTokensRow
......@@ -302,30 +349,4 @@ const TokenBalanceItemRow = memo(function TokenBalanceItemRow({
/>
</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 {
TokenDetailsScreenQuery,
} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
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'
export interface TokenDetailsHeaderProps {
......@@ -20,9 +22,12 @@ export function TokenDetailsHeader({
loading = false,
onPressWarningIcon,
}: TokenDetailsHeaderProps): JSX.Element {
const tokenProtectionEnabled = useFeatureFlag(FeatureFlags.TokenProtection)
const token = data?.token
const tokenProject = token?.project
const shouldShowWarningIcon =
!tokenProtectionEnabled &&
(tokenProject?.safetyLevel === SafetyLevel.StrongWarning || tokenProject?.safetyLevel === SafetyLevel.Blocked)
return (
<Flex gap="$spacing12" mx="$spacing16">
<TokenLogo
......@@ -42,9 +47,7 @@ export function TokenDetailsHeader({
>
{token?.name ?? ''}
</Text>
{/* Suppress warning icon on low warning level */}
{(tokenProject?.safetyLevel === SafetyLevel.StrongWarning ||
tokenProject?.safetyLevel === SafetyLevel.Blocked) && (
{shouldShowWarningIcon && (
<TouchableArea onPress={onPressWarningIcon}>
<WarningIcon safetyLevel={tokenProject?.safetyLevel} size="$icon.20" strokeColorOverride="$neutral3" />
</TouchableArea>
......
......@@ -48,7 +48,7 @@ function TokenOptionItemWrapper({
[currencyInfo, balanceUSD, quantity, isUnsupported],
)
const onPress = useCallback(() => onSelectCurrency?.(currency), [currency, onSelectCurrency])
const { tokenWarningDismissed, onDismissTokenWarning } = useDismissedTokenWarnings(currencyInfo?.currency)
const { tokenWarningDismissed } = useDismissedTokenWarnings(currencyInfo?.currency)
const { convertFiatAmountFormatted, formatNumberOrString } = useLocalizationContext()
if (!option) {
......@@ -62,7 +62,6 @@ function TokenOptionItemWrapper({
return (
<TokenOptionItem
balance={convertFiatAmountFormatted(option.balanceUSD, NumberType.FiatTokenPrice)}
dismissWarningCallback={onDismissTokenWarning}
isSelected={isSelected}
option={option}
quantity={option.quantity}
......@@ -149,6 +148,10 @@ function _TokenFiatOnRampList({
return <></>
}
if (section.data.length === 0) {
return <></>
}
return (
<Flex mt="$spacing12">
<ListSeparatorToggle
......
......@@ -132,8 +132,9 @@ describe('TraceUserProperties', () => {
expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.TransactionAuthMethod, AuthMethod.FaceId, undefined)
expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.Language, 'English', 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 () => {
......@@ -172,7 +173,8 @@ describe('TraceUserProperties', () => {
expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.WalletSignerCount, 0, undefined)
expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.AppOpenAuthMethod, 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'
import { useIsDarkMode } from 'ui/src'
import { useAppFiatCurrency } from 'uniswap/src/features/fiatCurrency/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 { isAndroid } from 'utilities/src/platform'
import { selectAllowAnalytics } from 'wallet/src/features/telemetry/selectors'
......@@ -36,6 +40,7 @@ export function TraceUserProperties(): null {
const currentFiatCurrency = useAppFiatCurrency()
const hideSpamTokens = useHideSpamTokensSetting()
const hideSmallBalances = useHideSmallBalancesSetting()
const { isTestnetModeEnabled } = useEnabledChains()
// Effects must check this and ensure they are setting properties for when analytics is reenabled
const allowAnalytics = useSelector(selectAllowAnalytics)
......@@ -111,5 +116,9 @@ export function TraceUserProperties(): null {
setUserProperty(MobileUserPropertyName.Currency, currentFiatCurrency)
}, [allowAnalytics, currentFiatCurrency])
useEffect(() => {
setUserProperty(MobileUserPropertyName.TestnetModeEnabled, isTestnetModeEnabled)
}, [allowAnalytics, isTestnetModeEnabled])
return null
}
......@@ -149,7 +149,7 @@ export function ExploreSections({ listRef }: ExploreSectionsProps): JSX.Element
visibleHeight={visibleListHeight}
/>
<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')}
</Text>
<Flex flexShrink={1}>
......@@ -217,7 +217,7 @@ function NetworkPillsRow({
)
return (
<Flex py="$spacing16">
<Flex py="$spacing8">
<FlatList
horizontal
ListHeaderComponent={
......
import React, { memo, useCallback } from 'react'
import { ViewProps } from 'react-native'
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 { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks'
import RemoveButton from 'src/components/explore/RemoveButton'
......@@ -125,7 +125,6 @@ function FavoriteTokenCard({
borderColor={opacify(0.05, colors.surface3.val)}
borderRadius="$rounded16"
borderWidth={isDarkMode ? '$none' : '$spacing1'}
entering={FadeIn}
hapticFeedback={!isEditing}
hapticStyle={ImpactFeedbackStyle.Light}
m="$spacing4"
......
......@@ -49,6 +49,8 @@ describe('FavoriteWalletCard', () => {
jest.spyOn(unitagHooks, 'useUnitagByAddress').mockReturnValue({
unitag: { username: 'unitagname' },
loading: false,
fetching: false,
pending: false,
})
const { queryByText } = render(<FavoriteWalletCard {...defaultProps} />)
......
......@@ -108,16 +108,10 @@ function _SortButton({ orderBy }: FilterGroupProps): JSX.Element {
}, [MenuItem, dispatch, menuActions])
return (
<ActionSheetDropdown
options={options}
showArrow={false}
styles={{
alignment: 'right',
}}
testID="chain-selector"
>
<ActionSheetDropdown options={options} showArrow={false} styles={{ alignment: 'right' }}>
<Flex
row
centered
backgroundColor="$surface3"
borderRadius="$rounded20"
gap="$spacing4"
......@@ -125,7 +119,7 @@ function _SortButton({ orderBy }: FilterGroupProps): JSX.Element {
pr="$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)}
</Text>
<RotatableChevron color="$neutral2" direction="down" height={iconSizes.icon20} width={iconSizes.icon20} />
......
......@@ -119,7 +119,7 @@ export const TokenItem = memo(function _TokenItem({
<Flex centered row gap="$spacing4">
{!hideNumberedList && (
<Flex minWidth={spacing.spacing16} mr="$spacing8">
<Text color="$neutral2" variant="buttonLabel2">
<Text color="$neutral2" variant="body3">
{index + 1}
</Text>
</Flex>
......
......@@ -39,11 +39,12 @@ exports[`SortButton renders without error 1`] = `
"paddingTop": 8,
}
}
testID="chain-selector"
testID="dropdown-toggle"
>
<View
style={
{
"alignItems": "center",
"backgroundColor": "rgba(34,34,34,0.05)",
"borderBottomLeftRadius": 20,
"borderBottomRightRadius": 20,
......@@ -51,6 +52,7 @@ exports[`SortButton renders without error 1`] = `
"borderTopRightRadius": 20,
"flexDirection": "row",
"gap": 4,
"justifyContent": "center",
"paddingBottom": 8,
"paddingLeft": 12,
"paddingRight": 8,
......@@ -68,9 +70,9 @@ exports[`SortButton renders without error 1`] = `
"color": "#222222",
"flexShrink": 1,
"fontFamily": "Basel Grotesk",
"fontSize": 17,
"fontSize": 15,
"fontWeight": "500",
"lineHeight": 19.549999999999997,
"lineHeight": 17.25,
}
}
suppressHighlighting={true}
......
......@@ -73,14 +73,14 @@ exports[`TokenItem renders without error 1`] = `
>
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.2}
maxFontSizeMultiplier={1.4}
style={
{
"color": "#7D7D7D",
"fontFamily": "Basel Grotesk",
"fontSize": 17,
"fontWeight": "500",
"lineHeight": 19.549999999999997,
"fontSize": 15,
"fontWeight": "400",
"lineHeight": 20,
}
}
suppressHighlighting={true}
......
......@@ -206,7 +206,7 @@ describe(useExploreTokenContextMenu, () => {
payload: {
name: 'swap-modal',
initialState: {
exactAmountToken: '0',
exactAmountToken: '',
exactCurrencyField: 'input',
[CurrencyField.INPUT]: null,
[CurrencyField.OUTPUT]: {
......
......@@ -59,7 +59,7 @@ export function useExploreTokenContextMenu({
const onPressSwap = useCallback(() => {
const swapFormState: TransactionState = {
exactCurrencyField: CurrencyField.INPUT,
exactAmountToken: '0',
exactAmountToken: '',
[CurrencyField.INPUT]: null,
[CurrencyField.OUTPUT]: {
chainId,
......
......@@ -4,9 +4,11 @@ import { FlatList, ListRenderItemInfo } from 'react-native'
import { SearchTokenItem } from 'src/components/explore/search/items/SearchTokenItem'
import { getSearchResultId } from 'src/components/explore/search/utils'
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 { useTokenRankingsQuery } from 'uniswap/src/data/rest/tokenRankings'
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 { UniverseChainId } from 'uniswap/src/types/chains'
import { RankingType } from 'wallet/src/features/wallet/types'
......@@ -32,7 +34,14 @@ function tokenStatsToTokenSearchResult(token: Maybe<TokenRankingsStat>): TokenSe
name,
symbol,
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 { useTranslation } from 'react-i18next'
import { FadeIn, FadeOut } from 'react-native-reanimated'
import { NFTHeaderItem, TokenHeaderItem, WalletHeaderItem } from 'src/components/explore/search/constants'
import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader'
import { SearchHeader } from 'src/components/explore/search/types'
import { Flex, Loader } from 'ui/src'
import { Coin, Gallery, Person } from 'ui/src/components/icons'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { UniverseChainId } from 'uniswap/src/types/chains'
export const SearchResultsLoader = (): JSX.Element => {
const { t } = useTranslation()
function SectionLoader({ searchHeader, repeat = 1 }: { searchHeader: SearchHeader; repeat?: number }): JSX.Element {
return (
<Flex gap="$spacing16">
<Flex gap="$spacing12">
<SectionHeaderText
icon={<Coin color="$neutral2" size="$icon.24" />}
title={t('explore.search.section.tokens')}
/>
<AnimatedFlex entering={FadeIn} exiting={FadeOut} mx="$spacing24">
<Loader.Token repeat={2} />
</AnimatedFlex>
<SectionHeaderText icon={searchHeader.icon} title={searchHeader.title} />
<Flex mx="$spacing24">
<Loader.SearchResult repeat={repeat} />
</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>
)
}
/**
* 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>
)
}
......@@ -4,7 +4,13 @@ import { FlatList, ListRenderItemInfo } from 'react-native'
import { FadeIn, FadeOut } from 'react-native-reanimated'
import { SearchResultsLoader } from 'src/components/explore/search/SearchResultsLoader'
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 { SearchENSAddressItem } from 'src/components/explore/search/items/SearchENSAddressItem'
import { SearchEtherscanItem } from 'src/components/explore/search/items/SearchEtherscanItem'
......@@ -14,16 +20,13 @@ import { SearchUnitagItem } from 'src/components/explore/search/items/SearchUnit
import { SearchWalletByAddressItem } from 'src/components/explore/search/items/SearchWalletByAddressItem'
import { SearchResultOrHeader } from 'src/components/explore/search/types'
import {
filterSearchResultsByChainId,
formatNFTCollectionSearchResults,
formatTokenSearchResults,
getSearchResultId,
} from 'src/components/explore/search/utils'
import { Flex, Text } from 'ui/src'
import { Coin, Gallery, Person } from 'ui/src/components/icons'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
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 { SearchContext } from 'uniswap/src/features/search/SearchContext'
import {
......@@ -32,36 +35,10 @@ import {
TokenSearchResult,
} from 'uniswap/src/features/search/SearchResult'
import { useEnabledChains } from 'uniswap/src/features/settings/hooks'
import i18n from 'uniswap/src/i18n/i18n'
import { UniverseChainId } from 'uniswap/src/types/chains'
import { getValidAddress } from 'uniswap/src/utils/addresses'
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']
export function SearchResultsSection({
......@@ -93,13 +70,7 @@ export function SearchResultsSection({
return undefined
}
const formattedTokenSearchResults = formatTokenSearchResults(searchResultsData.searchTokens, searchQuery)
if (!selectedChain) {
return formattedTokenSearchResults
}
return filterSearchResultsByChainId(formattedTokenSearchResults, selectedChain)
return formatTokenSearchResults(searchResultsData.searchTokens, searchQuery, selectedChain)
}, [selectedChain, searchQuery, searchResultsData])
// Search for matching NFT collections
......@@ -109,13 +80,7 @@ export function SearchResultsSection({
return undefined
}
const formattedNftCollectionSearchResults = formatNFTCollectionSearchResults(searchResultsData.nftCollections)
if (!selectedChain) {
return formattedNftCollectionSearchResults
}
return filterSearchResultsByChainId(formattedNftCollectionSearchResults, selectedChain)
return formatNFTCollectionSearchResults(searchResultsData.nftCollections, selectedChain)
}, [searchResultsData, selectedChain])
// Search for matching wallets
......@@ -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
if (searchResultsLoading) {
return <SearchResultsLoader />
return <SearchResultsLoader selectedChain={selectedChain} />
}
if (error) {
......@@ -209,7 +174,7 @@ export function SearchResultsSection({
<Flex grow gap="$spacing8" pb="$spacing36">
<FlatList
ListEmptyComponent={
<AnimatedFlex entering={FadeIn} exiting={FadeOut} gap="$spacing8" mx="$spacing8">
<AnimatedFlex entering={FadeIn} exiting={FadeOut} gap="$spacing8" mx="$spacing20">
<Text color="$neutral2" variant="body1">
<Trans
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'
import { Flex, ImpactFeedbackStyle, Text, TouchableArea } from 'ui/src'
import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo'
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 { SearchResultType, TokenSearchResult } from 'uniswap/src/features/search/SearchResult'
import { addToSearchHistory } from 'uniswap/src/features/search/searchHistorySlice'
......@@ -28,11 +28,12 @@ export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps):
const dispatch = useDispatch()
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 currencyInfo = useCurrencyInfo(currencyId)
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 => {
tokenDetailsNavigation.preload(currencyId)
......@@ -60,6 +61,7 @@ export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps):
logoUrl,
safetyLevel,
safetyInfo,
feeData,
},
}),
)
......
import { SEARCH_RESULT_HEADER_KEY } from 'src/components/explore/search/constants'
import { SearchResult } from 'uniswap/src/features/search/SearchResult'
// Header type used to render header text instead of SearchResult item
export type SearchResultOrHeader =
| SearchResult
| { type: typeof SEARCH_RESULT_HEADER_KEY; title: string; icon?: JSX.Element }
export type SearchHeaderKey = 'header'
export type SearchHeader = { type: SearchHeaderKey; title: string; icon?: JSX.Element }
export type SearchResultOrHeader = SearchResult | SearchHeader
......@@ -6,6 +6,7 @@ import {
} from 'src/components/explore/search/utils'
import { Chain, ExploreSearchQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
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 { amount, ethToken, nftCollection, nftContract, token, tokenMarket } from 'uniswap/src/test/fixtures'
import { createArray } from 'uniswap/src/test/utils'
......@@ -14,14 +15,14 @@ type ExploreSearchResult = NonNullable<ExploreSearchQuery>
describe(formatTokenSearchResults, () => {
it('returns undefined if there is no data', () => {
expect(formatTokenSearchResults(undefined, '')).toEqual(undefined)
expect(formatTokenSearchResults(undefined, '', null)).toEqual(undefined)
})
it('filters out duplicate results', () => {
const searchToken = token()
const data = createArray(2, () => searchToken)
const result = formatTokenSearchResults(data, '')
const result = formatTokenSearchResults(data, '', null)
expect(result).toHaveLength(1)
expect(result?.[0]?.address).toEqual(data[0].address)
......@@ -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)
expect(result).toHaveLength(1)
......@@ -58,7 +59,7 @@ describe(formatTokenSearchResults, () => {
token({ name: 'Uniswap' }),
]
const result = formatTokenSearchResults(data, 'uniswap')
const result = formatTokenSearchResults(data, 'uniswap', null)
expect(result).toHaveLength(2)
expect(result?.[0]?.name).toEqual('Uniswap')
......@@ -69,7 +70,7 @@ describe(formatTokenSearchResults, () => {
const searchToken = token()
const data = [searchToken]
const result = formatTokenSearchResults(data, '')
const result = formatTokenSearchResults(data, '', null)
expect(result).toHaveLength(1)
expect(result?.[0]?.type).toEqual(SearchResultType.Token)
......@@ -79,6 +80,10 @@ describe(formatTokenSearchResults, () => {
expect(result?.[0]?.symbol).toEqual(searchToken.symbol)
expect(result?.[0]?.logoUrl).toEqual(searchToken.project?.logoUrl)
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, () => {
......@@ -106,7 +111,7 @@ describe(formatTokenSearchResults, () => {
describe(formatNFTCollectionSearchResults, () => {
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', () => {
......@@ -115,7 +120,7 @@ describe(formatTokenSearchResults, () => {
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?.[0]?.address).toEqual(topNFTCollections[0].nftContracts[0]?.address)
......
......@@ -11,21 +11,15 @@ import {
import { searchResultId } from 'uniswap/src/features/search/searchHistorySlice'
import { UniverseChainId } from 'uniswap/src/types/chains'
const MAX_TOKEN_RESULTS_COUNT = 4
const MAX_TOKEN_RESULTS_COUNT = 8
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
export function formatTokenSearchResults(
data: ExploreSearchResult['searchTokens'],
searchQuery: string,
selectedChain: UniverseChainId | null,
): TokenSearchResult[] | undefined {
if (!data) {
return undefined
......@@ -39,10 +33,13 @@ export function formatTokenSearchResults(
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)
if (!chainId || !project) {
const shoulderFilterByChain = !!selectedChain
const chainMismatch = shoulderFilterByChain && selectedChain !== chainId
if (!chainId || !project || chainMismatch) {
return tokensMap
}
......@@ -58,6 +55,7 @@ export function formatTokenSearchResults(
logoUrl: logoUrl ?? null,
volume1D: market?.volume?.value ?? 0,
safetyInfo: getCurrencySafetyInfo(safetyLevel, protectionInfo),
feeData: feeData ?? null,
}
// 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
export function formatNFTCollectionSearchResults(
data: ExploreSearchResult['nftCollections'],
selectedChain: UniverseChainId | null,
): NFTCollectionSearchResult[] | undefined {
if (!data) {
return undefined
......@@ -100,7 +99,10 @@ export function formatNFTCollectionSearchResults(
return data.edges.reduce<NFTCollectionSearchResult[]>((accum, { node }) => {
const formatted = gqlNFTToNFTCollectionSearchResult(node)
if (formatted) {
const chainMismatch = selectedChain && formatted && formatted.chainId !== selectedChain
if (formatted && !chainMismatch) {
accum.push(formatted)
}
return accum
......
......@@ -16,7 +16,12 @@ import { OnboardingCardLoggingName } from 'uniswap/src/features/telemetry/types'
import { useTranslation } from 'uniswap/src/i18n'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
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 { useSharedIntroCards } from 'wallet/src/components/introCards/useSharedIntroCards'
import { selectHasViewedWelcomeWalletCard } from 'wallet/src/features/behaviorHistory/selectors'
......@@ -175,7 +180,7 @@ export function OnboardingIntroCardStack({
const handleSwiped = useCallback(
(_card: IntroCardProps, index: number) => {
const loggingName = cards[index]?.loggingName
if (loggingName) {
if (loggingName && isOnboardingCardLoggingName(loggingName)) {
sendAnalyticsEvent(WalletEventName.OnboardingIntroCardSwiped, {
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