ci(release): publish latest release

parent e40445e9
* @uniswap/web-admins
IPFS hash of the deployment: We are back with some new updates! Here’s the latest:
- CIDv0: `QmeVXtPfnqJ2PeigSXgwizJDYf2EmwfVrCE4Qi3dXhAbdL`
- CIDv1: `bafybeihqagapczekcmizn4zden7rrz6h4mfhtnd3dc3wa3wnatqc4eu6vm`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org). Integrated Flashbots Protect: This is a private RPC provider that protects users from getting sandwiched on Mainnet.
You can also access the Uniswap Interface from an IPFS gateway. Max Balance Education: Introduces copy to educate users on why the Max button is disabled when they don't have enough of the network token to cover gas costs.
**BEWARE**: The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported.
**You should always use an IPFS gateway that enforces origin separation**, or our hosted deployment of the latest release at [app.uniswap.org](https://app.uniswap.org).
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://bafybeihqagapczekcmizn4zden7rrz6h4mfhtnd3dc3wa3wnatqc4eu6vm.ipfs.dweb.link/
- https://bafybeihqagapczekcmizn4zden7rrz6h4mfhtnd3dc3wa3wnatqc4eu6vm.ipfs.cf-ipfs.com/
- [ipfs://QmeVXtPfnqJ2PeigSXgwizJDYf2EmwfVrCE4Qi3dXhAbdL/](ipfs://QmeVXtPfnqJ2PeigSXgwizJDYf2EmwfVrCE4Qi3dXhAbdL/)
### 5.67.4 (2025-01-23)
### Bug Fixes
* **web:** Phil/lp polling prod (#15462) 7533563
Other changes:
- Improved readability and color extraction of NFT Detail pages
- Improved fee detection of fee-on-transfer tokens
- More concise context menu on token detail pages
- Various bug fixes and performance improvements
web/5.67.4 mobile/1.43.1
\ No newline at end of file \ No newline at end of file
...@@ -25,3 +25,6 @@ dist-ssr ...@@ -25,3 +25,6 @@ dist-ssr
*.sw? *.sw?
.tamagui .tamagui
# Sentry Config File
.env.sentry-build-plugin
...@@ -9,14 +9,17 @@ ...@@ -9,14 +9,17 @@
"@ethersproject/providers": "5.7.2", "@ethersproject/providers": "5.7.2",
"@metamask/rpc-errors": "6.2.1", "@metamask/rpc-errors": "6.2.1",
"@reduxjs/toolkit": "1.9.3", "@reduxjs/toolkit": "1.9.3",
"@sentry/browser": "7.80.0",
"@sentry/react": "7.80.0",
"@sentry/webpack-plugin": "2.10.3",
"@svgr/webpack": "8.0.1", "@svgr/webpack": "8.0.1",
"@tamagui/core": "1.114.4", "@tamagui/core": "1.114.4",
"@types/uuid": "9.0.1", "@types/uuid": "9.0.1",
"@uniswap/analytics-events": "2.40.0", "@uniswap/analytics-events": "2.40.0",
"@uniswap/uniswapx-sdk": "3.0.0-beta.1", "@uniswap/uniswapx-sdk": "3.0.0-beta.1",
"@uniswap/universal-router-sdk": "4.10.0", "@uniswap/universal-router-sdk": "4.7.0",
"@uniswap/v3-sdk": "3.21.0", "@uniswap/v3-sdk": "3.19.0",
"@uniswap/v4-sdk": "1.15.0", "@uniswap/v4-sdk": "1.12.0",
"dotenv-webpack": "8.0.1", "dotenv-webpack": "8.0.1",
"ethers": "5.7.2", "ethers": "5.7.2",
"eventemitter3": "5.0.1", "eventemitter3": "5.0.1",
......
...@@ -2,8 +2,6 @@ body, ...@@ -2,8 +2,6 @@ body,
html { html {
height: 100%; height: 100%;
max-width: 100vw; max-width: 100vw;
font-feature-settings: 'liga' 0;
font-variant-ligatures: no-contextual;
} }
#root { #root {
......
...@@ -4,12 +4,11 @@ import 'symbol-observable' // Needed by `reduxed-chrome-storage` as polyfill, or ...@@ -4,12 +4,11 @@ import 'symbol-observable' // Needed by `reduxed-chrome-storage` as polyfill, or
import { useEffect } from 'react' import { useEffect } from 'react'
import { I18nextProvider } from 'react-i18next' import { I18nextProvider } from 'react-i18next'
import { RouteObject, RouterProvider, createHashRouter } from 'react-router-dom' import { RouteObject, RouterProvider } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
import { ExtensionStatsigProvider } from 'src/app/StatsigProvider' import { ExtensionStatsigProvider } from 'src/app/StatsigProvider'
import { GraphqlProvider } from 'src/app/apollo' import { GraphqlProvider } from 'src/app/apollo'
import { ErrorElement } from 'src/app/components/ErrorElement' import { ErrorElement } from 'src/app/components/ErrorElement'
import { DatadogAppNameTag } from 'src/app/datadog'
import { ClaimUnitagScreen } from 'src/app/features/onboarding/ClaimUnitagScreen' import { ClaimUnitagScreen } from 'src/app/features/onboarding/ClaimUnitagScreen'
import { Complete } from 'src/app/features/onboarding/Complete' import { Complete } from 'src/app/features/onboarding/Complete'
import { import {
...@@ -35,6 +34,7 @@ import { ScanToOnboard } from 'src/app/features/onboarding/scan/ScanToOnboard' ...@@ -35,6 +34,7 @@ import { ScanToOnboard } from 'src/app/features/onboarding/scan/ScanToOnboard'
import { ScantasticContextProvider } from 'src/app/features/onboarding/scan/ScantasticContextProvider' import { ScantasticContextProvider } from 'src/app/features/onboarding/scan/ScantasticContextProvider'
import { OnboardingRoutes, TopLevelRoutes } from 'src/app/navigation/constants' import { OnboardingRoutes, TopLevelRoutes } from 'src/app/navigation/constants'
import { setRouter, setRouterState } from 'src/app/navigation/state' import { setRouter, setRouterState } from 'src/app/navigation/state'
import { SentryAppNameTag, sentryCreateHashRouter } from 'src/app/sentry'
import { initExtensionAnalytics } from 'src/app/utils/analytics' import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { checksIfSupportsSidePanel } from 'src/app/utils/chrome' import { checksIfSupportsSidePanel } from 'src/app/utils/chrome'
import { PrimaryAppInstanceDebuggerLazy } from 'src/store/PrimaryAppInstanceDebuggerLazy' import { PrimaryAppInstanceDebuggerLazy } from 'src/store/PrimaryAppInstanceDebuggerLazy'
...@@ -135,7 +135,7 @@ const allRoutes = [ ...@@ -135,7 +135,7 @@ const allRoutes = [
}, },
] ]
const router = createHashRouter([ const router = sentryCreateHashRouter([
{ {
path: `/${TopLevelRoutes.Onboarding}`, path: `/${TopLevelRoutes.Onboarding}`,
element: <OnboardingWrapper />, element: <OnboardingWrapper />,
...@@ -188,7 +188,7 @@ export default function OnboardingApp(): JSX.Element { ...@@ -188,7 +188,7 @@ export default function OnboardingApp(): JSX.Element {
return ( return (
<Trace> <Trace>
<PersistGate persistor={getReduxPersistor()}> <PersistGate persistor={getReduxPersistor()}>
<ExtensionStatsigProvider appName={DatadogAppNameTag.Onboarding}> <ExtensionStatsigProvider appName={SentryAppNameTag.Onboarding}>
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}> <SharedWalletProvider reduxStore={getReduxStore()}>
<ErrorBoundary> <ErrorBoundary>
......
...@@ -4,14 +4,14 @@ import 'src/app/Global.css' ...@@ -4,14 +4,14 @@ import 'src/app/Global.css'
import { useEffect } from 'react' import { useEffect } from 'react'
import { I18nextProvider, useTranslation } from 'react-i18next' import { I18nextProvider, useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { RouterProvider, createHashRouter } from 'react-router-dom' import { RouterProvider } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
import { ExtensionStatsigProvider } from 'src/app/StatsigProvider' import { ExtensionStatsigProvider } from 'src/app/StatsigProvider'
import { GraphqlProvider } from 'src/app/apollo' import { GraphqlProvider } from 'src/app/apollo'
import { ErrorElement } from 'src/app/components/ErrorElement' import { ErrorElement } from 'src/app/components/ErrorElement'
import { TraceUserProperties } from 'src/app/components/Trace/TraceUserProperties' import { TraceUserProperties } from 'src/app/components/Trace/TraceUserProperties'
import { DatadogAppNameTag } from 'src/app/datadog'
import { DappContextProvider } from 'src/app/features/dapp/DappContext' import { DappContextProvider } from 'src/app/features/dapp/DappContext'
import { SentryAppNameTag, initializeSentry, sentryCreateHashRouter } from 'src/app/sentry'
import { initExtensionAnalytics } from 'src/app/utils/analytics' import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { getReduxPersistor, getReduxStore } from 'src/store/store' import { getReduxPersistor, getReduxStore } from 'src/store/store'
import { DeprecatedButton, Flex, Image, Text } from 'ui/src' import { DeprecatedButton, Flex, Image, Text } from 'ui/src'
...@@ -25,11 +25,23 @@ import { ElementName } from 'uniswap/src/features/telemetry/constants' ...@@ -25,11 +25,23 @@ import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context' import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context'
import i18n from 'uniswap/src/i18n' import i18n from 'uniswap/src/i18n'
import { ExtensionScreens } from 'uniswap/src/types/screens/extension' import { ExtensionScreens } from 'uniswap/src/types/screens/extension'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger'
import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary' import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks' import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks'
import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider' import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider'
const router = createHashRouter([ getUniqueId()
.then((userId) => {
initializeSentry(SentryAppNameTag.Popup, userId)
})
.catch((error) => {
logger.error(error, {
tags: { file: 'PopupApp.tsx', function: 'getUniqueId' },
})
})
const router = sentryCreateHashRouter([
{ {
path: '', path: '',
element: <PopupContent />, element: <PopupContent />,
...@@ -116,7 +128,7 @@ export default function PopupApp(): JSX.Element { ...@@ -116,7 +128,7 @@ export default function PopupApp(): JSX.Element {
return ( return (
<Trace> <Trace>
<PersistGate persistor={getReduxPersistor()}> <PersistGate persistor={getReduxPersistor()}>
<ExtensionStatsigProvider appName={DatadogAppNameTag.Popup}> <ExtensionStatsigProvider appName={SentryAppNameTag.Popup}>
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}> <SharedWalletProvider reduxStore={getReduxStore()}>
<ErrorBoundary> <ErrorBoundary>
......
...@@ -4,13 +4,12 @@ import 'src/app/Global.css' ...@@ -4,13 +4,12 @@ import 'src/app/Global.css'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { I18nextProvider } from 'react-i18next' import { I18nextProvider } from 'react-i18next'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { RouterProvider, createHashRouter } from 'react-router-dom' import { RouterProvider } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
import { ExtensionStatsigProvider } from 'src/app/StatsigProvider' import { ExtensionStatsigProvider } from 'src/app/StatsigProvider'
import { GraphqlProvider } from 'src/app/apollo' import { GraphqlProvider } from 'src/app/apollo'
import { ErrorElement } from 'src/app/components/ErrorElement' import { ErrorElement } from 'src/app/components/ErrorElement'
import { TraceUserProperties } from 'src/app/components/Trace/TraceUserProperties' import { TraceUserProperties } from 'src/app/components/Trace/TraceUserProperties'
import { DatadogAppNameTag } from 'src/app/datadog'
import { AccountSwitcherScreen } from 'src/app/features/accounts/AccountSwitcherScreen' import { AccountSwitcherScreen } from 'src/app/features/accounts/AccountSwitcherScreen'
import { DappContextProvider } from 'src/app/features/dapp/DappContext' import { DappContextProvider } from 'src/app/features/dapp/DappContext'
import { addRequest } from 'src/app/features/dappRequests/saga' import { addRequest } from 'src/app/features/dappRequests/saga'
...@@ -30,6 +29,7 @@ import { useIsWalletUnlocked } from 'src/app/hooks/useIsWalletUnlocked' ...@@ -30,6 +29,7 @@ import { useIsWalletUnlocked } from 'src/app/hooks/useIsWalletUnlocked'
import { AppRoutes, RemoveRecoveryPhraseRoutes, SettingsRoutes } from 'src/app/navigation/constants' import { AppRoutes, RemoveRecoveryPhraseRoutes, SettingsRoutes } from 'src/app/navigation/constants'
import { MainContent, WebNavigation } from 'src/app/navigation/navigation' import { MainContent, WebNavigation } from 'src/app/navigation/navigation'
import { setRouter, setRouterState } from 'src/app/navigation/state' import { setRouter, setRouterState } from 'src/app/navigation/state'
import { SentryAppNameTag, initializeSentry, sentryCreateHashRouter } from 'src/app/sentry'
import { initExtensionAnalytics } from 'src/app/utils/analytics' import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { import {
DappBackgroundPortChannel, DappBackgroundPortChannel,
...@@ -47,6 +47,7 @@ import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' ...@@ -47,6 +47,7 @@ import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { UnitagUpdaterContextProvider, useUnitagUpdater } from 'uniswap/src/features/unitags/context' import { UnitagUpdaterContextProvider, useUnitagUpdater } from 'uniswap/src/features/unitags/context'
import i18n from 'uniswap/src/i18n' import i18n from 'uniswap/src/i18n'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { isDevEnv } from 'utilities/src/environment/env' import { isDevEnv } from 'utilities/src/environment/env'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { ONE_SECOND_MS } from 'utilities/src/time/time' import { ONE_SECOND_MS } from 'utilities/src/time/time'
...@@ -55,7 +56,17 @@ import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary ...@@ -55,7 +56,17 @@ import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks' import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks'
import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider' import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider'
const router = createHashRouter([ getUniqueId()
.then((userId) => {
initializeSentry(SentryAppNameTag.Sidebar, userId)
})
.catch((error) => {
logger.error(error, {
tags: { file: 'SidebarApp.tsx', function: 'getUniqueId' },
})
})
const router = sentryCreateHashRouter([
{ {
path: '', path: '',
element: <SidebarWrapper />, element: <SidebarWrapper />,
...@@ -247,7 +258,7 @@ export default function SidebarApp(): JSX.Element { ...@@ -247,7 +258,7 @@ export default function SidebarApp(): JSX.Element {
return ( return (
<Trace> <Trace>
<PersistGate persistor={getReduxPersistor()}> <PersistGate persistor={getReduxPersistor()}>
<ExtensionStatsigProvider appName={DatadogAppNameTag.Sidebar}> <ExtensionStatsigProvider appName={SentryAppNameTag.Sidebar}>
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}> <SharedWalletProvider reduxStore={getReduxStore()}>
<ErrorBoundary> <ErrorBoundary>
......
...@@ -3,13 +3,12 @@ import 'src/app/Global.css' ...@@ -3,13 +3,12 @@ import 'src/app/Global.css'
import { PropsWithChildren, useEffect } from 'react' import { PropsWithChildren, useEffect } from 'react'
import { I18nextProvider } from 'react-i18next' import { I18nextProvider } from 'react-i18next'
import { Outlet, RouterProvider, createHashRouter, useSearchParams } from 'react-router-dom' import { Outlet, RouterProvider, useSearchParams } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
import { ExtensionStatsigProvider } from 'src/app/StatsigProvider' import { ExtensionStatsigProvider } from 'src/app/StatsigProvider'
import { GraphqlProvider } from 'src/app/apollo' import { GraphqlProvider } from 'src/app/apollo'
import { ErrorElement } from 'src/app/components/ErrorElement' import { ErrorElement } from 'src/app/components/ErrorElement'
import { TraceUserProperties } from 'src/app/components/Trace/TraceUserProperties' import { TraceUserProperties } from 'src/app/components/Trace/TraceUserProperties'
import { DatadogAppNameTag } from 'src/app/datadog'
import { import {
ClaimUnitagSteps, ClaimUnitagSteps,
OnboardingStepsProvider, OnboardingStepsProvider,
...@@ -24,6 +23,7 @@ import { UnitagCreateUsernameScreen } from 'src/app/features/unitags/UnitagCreat ...@@ -24,6 +23,7 @@ import { UnitagCreateUsernameScreen } from 'src/app/features/unitags/UnitagCreat
import { UnitagIntroScreen } from 'src/app/features/unitags/UnitagIntroScreen' import { UnitagIntroScreen } from 'src/app/features/unitags/UnitagIntroScreen'
import { UnitagClaimRoutes } from 'src/app/navigation/constants' import { UnitagClaimRoutes } from 'src/app/navigation/constants'
import { setRouter, setRouterState } from 'src/app/navigation/state' import { setRouter, setRouterState } from 'src/app/navigation/state'
import { SentryAppNameTag, initializeSentry, sentryCreateHashRouter } from 'src/app/sentry'
import { initExtensionAnalytics } from 'src/app/utils/analytics' import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { getReduxPersistor, getReduxStore } from 'src/store/store' import { getReduxPersistor, getReduxStore } from 'src/store/store'
import { Flex } from 'ui/src' import { Flex } from 'ui/src'
...@@ -32,6 +32,7 @@ import { LocalizationContextProvider } from 'uniswap/src/features/language/Local ...@@ -32,6 +32,7 @@ import { LocalizationContextProvider } from 'uniswap/src/features/language/Local
import Trace from 'uniswap/src/features/telemetry/Trace' import Trace from 'uniswap/src/features/telemetry/Trace'
import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context' import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context'
import i18n from 'uniswap/src/i18n' import i18n from 'uniswap/src/i18n'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { usePrevious } from 'utilities/src/react/hooks' import { usePrevious } from 'utilities/src/react/hooks'
import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary' import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary'
...@@ -39,7 +40,17 @@ import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testne ...@@ -39,7 +40,17 @@ import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testne
import { useAccountAddressFromUrlWithThrow } from 'wallet/src/features/wallet/hooks' import { useAccountAddressFromUrlWithThrow } from 'wallet/src/features/wallet/hooks'
import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider' import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider'
const router = createHashRouter([ getUniqueId()
.then((userId) => {
initializeSentry(SentryAppNameTag.UnitagClaim, userId)
})
.catch((error) => {
logger.error(error, {
tags: { file: 'UnitagClaimApp.tsx', function: 'getUniqueId' },
})
})
const router = sentryCreateHashRouter([
{ {
path: '', path: '',
element: <UnitagAppInner />, element: <UnitagAppInner />,
...@@ -151,7 +162,7 @@ export default function UnitagClaimApp(): JSX.Element { ...@@ -151,7 +162,7 @@ export default function UnitagClaimApp(): JSX.Element {
return ( return (
<Trace> <Trace>
<PersistGate persistor={getReduxPersistor()}> <PersistGate persistor={getReduxPersistor()}>
<ExtensionStatsigProvider appName={DatadogAppNameTag.UnitagClaim}> <ExtensionStatsigProvider appName={SentryAppNameTag.UnitagClaim}>
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}> <SharedWalletProvider reduxStore={getReduxStore()}>
<ErrorBoundary> <ErrorBoundary>
......
import { datadogLogs } from '@datadog/browser-logs' import { datadogLogs } from '@datadog/browser-logs'
import { RumEvent, datadogRum } from '@datadog/browser-rum' import { datadogRum } from '@datadog/browser-rum'
import { getDatadogEnvironment } from 'src/app/version' import { getDatadogEnvironment } from 'src/app/version'
import { config } from 'uniswap/src/config' import { config } from 'uniswap/src/config'
import { import {
DatadogIgnoredErrorsConfigKey, DatadogIgnoredErrorsConfigKey,
DatadogIgnoredErrorsValType, DatadogIgnoredErrorsValType,
DatadogSessionSampleRateKey,
DatadogSessionSampleRateValType,
DynamicConfigs, DynamicConfigs,
} from 'uniswap/src/features/gating/configs' } from 'uniswap/src/features/gating/configs'
import { Experiments } from 'uniswap/src/features/gating/experiments' import { Experiments } from 'uniswap/src/features/gating/experiments'
import { WALLET_FEATURE_FLAG_NAMES } from 'uniswap/src/features/gating/flags' import { FeatureFlags, WALLET_FEATURE_FLAG_NAMES, getFeatureFlagName } from 'uniswap/src/features/gating/flags'
import { getDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import { getDynamicConfigValue } from 'uniswap/src/features/gating/hooks'
import { Statsig } from 'uniswap/src/features/gating/sdk/statsig' import { Statsig } from 'uniswap/src/features/gating/sdk/statsig'
import { getUniqueId } from 'utilities/src/device/getUniqueId' import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { datadogEnabled, localDevDatadogEnabled } from 'utilities/src/environment/constants'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
// In case Statsig is not available
const EXTENSION_DEFAULT_DATADOG_SESSION_SAMPLE_RATE = 10 // percent
export const enum DatadogAppNameTag {
Sidebar = 'sidebar',
Onboarding = 'onboarding',
ContentScript = 'content-script',
Background = 'background',
Popup = 'popup',
UnitagClaim = 'unitag-claim',
}
function beforeSend(event: RumEvent): boolean {
// 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') {
if (event.error.source === 'console') {
return false
}
const ignoredErrors = getDynamicConfigValue<
DynamicConfigs.DatadogIgnoredErrors,
DatadogIgnoredErrorsConfigKey,
DatadogIgnoredErrorsValType
>(DynamicConfigs.DatadogIgnoredErrors, DatadogIgnoredErrorsConfigKey.Errors, [])
const ignoredError = ignoredErrors.find(({ messageContains }) => event.error?.message.includes(messageContains))
if (ignoredError && Math.random() > ignoredError.sampleRate) {
return false
}
Object.defineProperty(event.error, 'stack', {
value: event.error.stack?.replace(/chrome-extension:\/\/[a-z]{32}/gi, ''),
writable: false,
configurable: true,
})
}
return true
}
export async function initializeDatadog(appName: string): Promise<void> { export async function initializeDatadog(appName: string): Promise<void> {
const datadogEnabled = Statsig.checkGate(getFeatureFlagName(FeatureFlags.Datadog))
logger.setWalletDatadogEnabled(datadogEnabled)
if (!datadogEnabled) { if (!datadogEnabled) {
return return
} }
const sessionSampleRate = getDynamicConfigValue<
DynamicConfigs.DatadogSessionSampleRate,
DatadogSessionSampleRateKey,
DatadogSessionSampleRateValType
>(
DynamicConfigs.DatadogSessionSampleRate,
DatadogSessionSampleRateKey.Rate,
EXTENSION_DEFAULT_DATADOG_SESSION_SAMPLE_RATE,
)
const sharedDatadogConfig = { const sharedDatadogConfig = {
clientToken: config.datadogClientToken, clientToken: config.datadogClientToken,
service: `extension-${getDatadogEnvironment()}`, service: `extension-${getDatadogEnvironment()}`,
env: getDatadogEnvironment(), env: getDatadogEnvironment(),
version: process.env.VERSION, version: process.env.VERSION,
trackingConsent: undefined,
} }
datadogRum.init({ datadogRum.init({
...sharedDatadogConfig, ...sharedDatadogConfig,
applicationId: config.datadogProjectId, applicationId: config.datadogProjectId,
sessionSampleRate: localDevDatadogEnabled ? 100 : sessionSampleRate, sessionSampleRate: 100,
sessionReplaySampleRate: 0, sessionReplaySampleRate: 0,
trackResources: true, trackResources: true,
trackLongTasks: true, trackLongTasks: true,
trackUserInteractions: true, trackUserInteractions: true,
enablePrivacyForActionName: true, enablePrivacyForActionName: true,
beforeSend, 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') {
if (event.error.source === 'console') {
return false
}
const ignoredErrors = getDynamicConfigValue<
DynamicConfigs.DatadogIgnoredErrors,
DatadogIgnoredErrorsConfigKey,
DatadogIgnoredErrorsValType
>(DynamicConfigs.DatadogIgnoredErrors, DatadogIgnoredErrorsConfigKey.Errors, [])
const ignoredError = ignoredErrors.find(({ messageContains }) => event.error?.message.includes(messageContains))
if (ignoredError && Math.random() > ignoredError.sampleRate) {
return false
}
Object.defineProperty(event.error, 'stack', {
value: event.error.stack?.replace(/chrome-extension:\/\/[a-z]{32}/gi, ''),
writable: false,
configurable: true,
})
}
return true
},
}) })
// According to the Datadog RUM documentation: datadogLogs.init({
// https://docs.datadoghq.com/real_user_monitoring/browser/setup/client?tab=rum#access-internal-context ...sharedDatadogConfig,
// datadogRum.init() seems to be synchronous and internal context is immediately available. site: 'datadoghq.com',
// Local testing confirms this behavior, explaining why no "onInitialization" callback is needed. forwardErrorsToLogs: false,
const internalContext = datadogRum.getInternalContext() })
const sessionIsSampled = internalContext?.session_id !== undefined
// we do not want to log anything if session is not sampled
if (sessionIsSampled) {
datadogLogs.init({
...sharedDatadogConfig,
site: 'datadoghq.com',
forwardErrorsToLogs: false,
})
logger.setWalletDatadogEnabled(true)
}
try { try {
const userId = await getUniqueId() const userId = await getUniqueId()
......
...@@ -116,7 +116,7 @@ export default function AppRatingModal({ onClose }: AppRatingModalProps): JSX.El ...@@ -116,7 +116,7 @@ export default function AppRatingModal({ onClose }: AppRatingModalProps): JSX.El
}, [dispatch]) }, [dispatch])
return ( return (
<Modal isDismissible isModalOpen name={ModalName.AppRatingModal} backgroundColor="$surface1" onClose={close}> <Modal isDismissible isModalOpen name={ModalName.TokenWarningModal} backgroundColor="$surface1" onClose={close}>
<TouchableArea p="$spacing16" position="absolute" right={0} top={0} zIndex={zIndices.default} onPress={close}> <TouchableArea p="$spacing16" position="absolute" right={0} top={0} zIndex={zIndices.default} onPress={close}>
<X color="$neutral2" size="$icon.20" /> <X color="$neutral2" size="$icon.20" />
</TouchableArea> </TouchableArea>
......
...@@ -70,8 +70,7 @@ export function ApproveRequestContent({ ...@@ -70,8 +70,7 @@ export function ApproveRequestContent({
// To detect a revoke, both the transaction value and the parsed arg amount value must be zero // To detect a revoke, both the transaction value and the parsed arg amount value must be zero
const isArgAmountZero = parsedTransactionData?.args.some( const isArgAmountZero = parsedTransactionData?.args.some(
(arg) => (arg) => typeof arg === 'object' && arg._hex && BigNumber.from(arg._hex).isZero(),
arg !== null && typeof arg === 'object' && !Array.isArray(arg) && arg._hex && BigNumber.from(arg._hex).isZero(),
) )
const isRevoke = dappRequest.transaction.value === '0x0' && isArgAmountZero const isRevoke = dappRequest.transaction.value === '0x0' && isArgAmountZero
......
...@@ -66,13 +66,6 @@ export function SignTypedDataRequestContent({ dappRequest }: SignTypedDataReques ...@@ -66,13 +66,6 @@ export function SignTypedDataRequestContent({ dappRequest }: SignTypedDataReques
message: EIP712Message | EIP712Message[keyof EIP712Message], message: EIP712Message | EIP712Message[keyof EIP712Message],
i = 1, i = 1,
): Maybe<JSX.Element | JSX.Element[]> => { ): Maybe<JSX.Element | JSX.Element[]> => {
if (message === null || message === undefined) {
return (
<Text color="$neutral1" variant="body4">
{String(message)}
</Text>
)
}
if (typeof message === 'string' && isAddress(message) && chainId) { if (typeof message === 'string' && isAddress(message) && chainId) {
const href = getExplorerLink(chainId, message, ExplorerDataType.ADDRESS) const href = getExplorerLink(chainId, message, ExplorerDataType.ADDRESS)
return <MaybeExplorerLinkedAddress address={message} link={href} /> return <MaybeExplorerLinkedAddress address={message} link={href} />
...@@ -83,12 +76,6 @@ export function SignTypedDataRequestContent({ dappRequest }: SignTypedDataReques ...@@ -83,12 +76,6 @@ export function SignTypedDataRequestContent({ dappRequest }: SignTypedDataReques
{message.toString()} {message.toString()}
</Text> </Text>
) )
} else if (Array.isArray(message)) {
return (
<Text $platform-web={{ overflowWrap: 'anywhere' }} color="$neutral1" variant="body4">
{JSON.stringify(message)}
</Text>
)
} else if (typeof message === 'object') { } else if (typeof message === 'object') {
return Object.entries(message).map(([key, value], index) => ( return Object.entries(message).map(([key, value], index) => (
<Flex key={`${key}-${index}`} flexDirection="row" gap="$spacing8"> <Flex key={`${key}-${index}`} flexDirection="row" gap="$spacing8">
......
...@@ -251,11 +251,6 @@ export function* handleSendTransaction( ...@@ -251,11 +251,6 @@ export function* handleSendTransaction(
options: { request: transactionRequest }, options: { request: transactionRequest },
typeInfo: transactionTypeInfo ?? { typeInfo: transactionTypeInfo ?? {
type: TransactionType.Unknown, type: TransactionType.Unknown,
dappInfo: {
name: dappInfo.displayName,
address: request.transaction.to,
icon: dappInfo.iconUrl,
},
}, },
transactionOriginType: TransactionOriginType.External, transactionOriginType: TransactionOriginType.External,
} }
......
import { useCallback, useState } from 'react' import { useCallback } from 'react'
import { UnitagClaimRoutes } from 'src/app/navigation/constants' import { UnitagClaimRoutes } from 'src/app/navigation/constants'
import { focusOrCreateUnitagTab } from 'src/app/navigation/utils' import { focusOrCreateUnitagTab } from 'src/app/navigation/utils'
import { Flex } from 'ui/src' import { Flex } from 'ui/src'
import { UnichainIntroModal } from 'uniswap/src/components/unichain/UnichainIntroModal' import { PollingInterval } from 'uniswap/src/constants/misc'
import { AccountType } from 'uniswap/src/features/accounts/types' import { AccountType } from 'uniswap/src/features/accounts/types'
import { UniverseChainId } from 'uniswap/src/features/chains/types' import { usePortfolioTotalValue } from 'uniswap/src/features/dataApi/balances'
import { CurrencyField } from 'uniswap/src/types/currency'
import { IntroCardStack } from 'wallet/src/components/introCards/IntroCardStack' import { IntroCardStack } from 'wallet/src/components/introCards/IntroCardStack'
import { useSharedIntroCards } from 'wallet/src/components/introCards/useSharedIntroCards' import { useSharedIntroCards } from 'wallet/src/components/introCards/useSharedIntroCards'
import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
export function HomeIntroCardStack(): JSX.Element | null { export function HomeIntroCardStack(): JSX.Element | null {
const { navigateToSwapFlow } = useWalletNavigation()
const activeAccount = useActiveAccountWithThrow() const activeAccount = useActiveAccountWithThrow()
const isSignerAccount = activeAccount.type === AccountType.SignerMnemonic const isSignerAccount = activeAccount.type === AccountType.SignerMnemonic
const { data } = usePortfolioTotalValue({
address: activeAccount.address,
// Not needed often given usage, and will get updated from other sources
pollInterval: PollingInterval.Slow,
})
const navigateToUnitagClaim = useCallback(async () => { const navigateToUnitagClaim = useCallback(async () => {
await focusOrCreateUnitagTab(activeAccount.address, UnitagClaimRoutes.ClaimIntro) await focusOrCreateUnitagTab(activeAccount.address, UnitagClaimRoutes.ClaimIntro)
}, [activeAccount.address]) }, [activeAccount.address])
const [showUnichainIntroModal, setShowUnichainIntroModal] = useState(false)
const { cards } = useSharedIntroCards({ const { cards } = useSharedIntroCards({
showUnichainModal: () => setShowUnichainIntroModal(true),
navigateToUnitagClaim, navigateToUnitagClaim,
navigateToUnitagIntro: navigateToUnitagClaim, // No need to differentiate for extension navigateToUnitagIntro: navigateToUnitagClaim, // No need to differentiate for extension
hasTokens: (data?.balanceUSD ?? 0) > 0,
}) })
// Don't show cards if there are none // Don't show cards if there are none
...@@ -37,14 +37,6 @@ export function HomeIntroCardStack(): JSX.Element | null { ...@@ -37,14 +37,6 @@ export function HomeIntroCardStack(): JSX.Element | null {
return ( return (
<Flex py="$spacing4"> <Flex py="$spacing4">
<IntroCardStack cards={cards} /> <IntroCardStack cards={cards} />
{showUnichainIntroModal && (
<UnichainIntroModal
openSwapFlow={() =>
navigateToSwapFlow({ openTokenSelector: CurrencyField.OUTPUT, outputChainId: UniverseChainId.Unichain })
}
onClose={() => setShowUnichainIntroModal(false)}
/>
)}
</Flex> </Flex>
) )
} }
...@@ -6004,7 +6004,7 @@ exports[`ReceiveScreen renders without error 1`] = ` ...@@ -6004,7 +6004,7 @@ exports[`ReceiveScreen renders without error 1`] = `
class="font_body _display-inline _boxSizing-border-box _whiteSpace-pre-wrap _mt-0px _mr-0px _mb-0px _ml-0px _color-neutral2 _fontFamily-f-family _wordWrap-break-word _lineHeight-f-lineHeigh1263414315 _textAlign-center _fontSize-f-size-micr111 _fontWeight-f-weight-bo3548" class="font_body _display-inline _boxSizing-border-box _whiteSpace-pre-wrap _mt-0px _mr-0px _mb-0px _ml-0px _color-neutral2 _fontFamily-f-family _wordWrap-break-word _lineHeight-f-lineHeigh1263414315 _textAlign-center _fontSize-f-size-micr111 _fontWeight-f-weight-bo3548"
data-disable-theme="true" data-disable-theme="true"
> >
You can send and receive tokens and NFTs on all of our 13 supported networks. You can send and receive tokens and NFTs on all of our 12 supported networks.
</span> </span>
<span <span
class="t_sub_theme t_primary_Button _dsp_contents is_Theme" class="t_sub_theme t_primary_Button _dsp_contents is_Theme"
...@@ -12040,7 +12040,7 @@ exports[`ReceiveScreen renders without error 1`] = ` ...@@ -12040,7 +12040,7 @@ exports[`ReceiveScreen renders without error 1`] = `
class="font_body _display-inline _boxSizing-border-box _whiteSpace-pre-wrap _mt-0px _mr-0px _mb-0px _ml-0px _color-neutral2 _fontFamily-f-family _wordWrap-break-word _lineHeight-f-lineHeigh1263414315 _textAlign-center _fontSize-f-size-micr111 _fontWeight-f-weight-bo3548" class="font_body _display-inline _boxSizing-border-box _whiteSpace-pre-wrap _mt-0px _mr-0px _mb-0px _ml-0px _color-neutral2 _fontFamily-f-family _wordWrap-break-word _lineHeight-f-lineHeigh1263414315 _textAlign-center _fontSize-f-size-micr111 _fontWeight-f-weight-bo3548"
data-disable-theme="true" data-disable-theme="true"
> >
You can send and receive tokens and NFTs on all of our 13 supported networks. You can send and receive tokens and NFTs on all of our 12 supported networks.
</span> </span>
<span <span
class="t_sub_theme t_primary_Button _dsp_contents is_Theme" class="t_sub_theme t_primary_Button _dsp_contents is_Theme"
......
import { useCallback, useMemo, useRef } from 'react' import { useCallback, useMemo, useRef } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { NavigationType, Outlet, ScrollRestoration, useLocation } from 'react-router-dom' import { Outlet, ScrollRestoration, useLocation } from 'react-router-dom'
import { DappRequestQueue } from 'src/app/features/dappRequests/DappRequestQueue' import { DappRequestQueue } from 'src/app/features/dappRequests/DappRequestQueue'
import { HomeScreen } from 'src/app/features/home/HomeScreen' import { HomeScreen } from 'src/app/features/home/HomeScreen'
import { Locked } from 'src/app/features/lockScreen/Locked' import { Locked } from 'src/app/features/lockScreen/Locked'
...@@ -84,7 +84,7 @@ export function WebNavigation(): JSX.Element { ...@@ -84,7 +84,7 @@ export function WebNavigation(): JSX.Element {
const routerState = useRouterState() const routerState = useRouterState()
if (routeName != null) { if (routeName != null) {
towards = routeDirections[routeName] towards = routeDirections[routeName]
const isBackwards = routerState?.historyAction === NavigationType.Pop const isBackwards = routerState?.historyAction === 'POP'
if (isBackwards) { if (isBackwards) {
const lastRoute = getAppRouteFromPathName(history[1] || '') const lastRoute = getAppRouteFromPathName(history[1] || '')
const previousDirection = lastRoute ? routeDirections[lastRoute] : 'right' const previousDirection = lastRoute ? routeDirections[lastRoute] : 'right'
......
import { RouterState } from '@sentry/react/types/types'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Location, NavigationType, Router, createHashRouter } from 'react-router-dom' import { Router } from 'react-router-dom'
import { sentryCreateHashRouter } from 'src/app/sentry'
interface RouterState {
historyAction: NavigationType
location: Location
}
/** /**
* Note this file is separate from SidebarApp on purpose! * Note this file is separate from SidebarApp on purpose!
...@@ -57,7 +54,7 @@ export function useRouterState(): RouterState | null { ...@@ -57,7 +54,7 @@ export function useRouterState(): RouterState | null {
} }
// as far as i can tell, react-router-dom doesn't give us this type so have to work around // as far as i can tell, react-router-dom doesn't give us this type so have to work around
type Router = ReturnType<typeof createHashRouter> type Router = ReturnType<typeof sentryCreateHashRouter>
let router: Router | null = null let router: Router | null = null
......
import * as SentryBrowser from '@sentry/browser'
import * as Sentry from '@sentry/react'
import { setTag } from '@sentry/react'
import { useEffect } from 'react'
import {
createHashRouter,
createRoutesFromChildren,
matchRoutes,
useLocation,
useNavigationType,
} from 'react-router-dom'
import { getSentryEnvironment } from 'src/app/version'
import { config } from 'uniswap/src/config'
import { logger } from 'utilities/src/logger/logger'
import { beforeSend } from 'wallet/src/utils/sentry'
export const enum SentryAppNameTag {
Sidebar = 'sidebar',
Onboarding = 'onboarding',
ContentScript = 'content-script',
Background = 'background',
Popup = 'popup',
UnitagClaim = 'unitag-claim',
}
export function initializeSentry(appNameTag: SentryAppNameTag, sentryUserId: string): void {
if (__DEV__) {
return
}
Sentry.init({
environment: getSentryEnvironment(),
dsn: config.sentryDsn,
release: process.env.VERSION,
integrations: [
new Sentry.BrowserTracing({
// See docs for support of different versions of variation of react router
// https://docs.sentry.io/platforms/javascript/guides/react/configuration/integrations/react-router/
routingInstrumentation: Sentry.reactRouterV6Instrumentation(
useEffect,
useLocation,
useNavigationType,
createRoutesFromChildren,
matchRoutes,
),
}),
],
beforeSend,
...sentrySampleRateOptions,
})
setTag('appName', appNameTag)
Sentry.setUser({ id: sentryUserId })
}
export function initSentryForBrowserScripts(appNameTag: SentryAppNameTag, sentryUserId: string): void {
if (__DEV__) {
return
}
// Wrapped in try/catch because in this context it can fail silently
try {
SentryBrowser.init({
environment: getSentryEnvironment(),
dsn: config.sentryDsn,
release: process.env.VERSION,
// TODO (EXT-528): Look into adding tracing integration
beforeSend,
...sentrySampleRateOptions,
})
} catch (e) {
logger.debug('sentry.ts', 'initSentryForBrowserScripts', 'Error in Sentry init', e)
}
setTag('appName', appNameTag)
if (sentryUserId) {
SentryBrowser.setUser({ id: sentryUserId })
}
}
const sentrySampleRateOptions = {
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
tracesSampleRate: 1.0,
// Capture Replay for 10% of all sessions,
// plus for 100% of sessions with an error
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
}
export const sentryCreateHashRouter = Sentry.wrapCreateBrowserRouter(createHashRouter)
...@@ -14,6 +14,16 @@ export function getStatsigEnvironmentTier(): StatsigEnvironmentTier { ...@@ -14,6 +14,16 @@ export function getStatsigEnvironmentTier(): StatsigEnvironmentTier {
return StatsigEnvironmentTier.PROD return StatsigEnvironmentTier.PROD
} }
export function getSentryEnvironment(): SentryEnvironment {
if (isDevEnv()) {
return SentryEnvironment.DEV
}
if (isBetaEnv()) {
return SentryEnvironment.BETA
}
return SentryEnvironment.PROD
}
export function getDatadogEnvironment(): DatadogEnvironment { export function getDatadogEnvironment(): DatadogEnvironment {
if (isDevEnv()) { if (isDevEnv()) {
return DatadogEnvironment.DEV return DatadogEnvironment.DEV
...@@ -29,3 +39,9 @@ enum DatadogEnvironment { ...@@ -29,3 +39,9 @@ enum DatadogEnvironment {
BETA = 'beta', BETA = 'beta',
PROD = 'prod', PROD = 'prod',
} }
enum SentryEnvironment {
DEV = 'development',
BETA = 'beta',
PROD = 'production',
}
...@@ -2,6 +2,7 @@ import 'symbol-observable' // Needed by `reduxed-chrome-storage` as polyfill, or ...@@ -2,6 +2,7 @@ import 'symbol-observable' // Needed by `reduxed-chrome-storage` as polyfill, or
import { initStatSigForBrowserScripts } from 'src/app/StatsigProvider' import { initStatSigForBrowserScripts } from 'src/app/StatsigProvider'
import { focusOrCreateOnboardingTab } from 'src/app/navigation/utils' import { focusOrCreateOnboardingTab } from 'src/app/navigation/utils'
import { SentryAppNameTag, initSentryForBrowserScripts } from 'src/app/sentry'
import { initExtensionAnalytics } from 'src/app/utils/analytics' import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { initMessageBridge } from 'src/background/backgroundDappRequests' import { initMessageBridge } from 'src/background/backgroundDappRequests'
import { backgroundStore } from 'src/background/backgroundStore' import { backgroundStore } from 'src/background/backgroundStore'
...@@ -9,6 +10,7 @@ import { backgroundToSidePanelMessageChannel } from 'src/background/messagePassi ...@@ -9,6 +10,7 @@ import { backgroundToSidePanelMessageChannel } from 'src/background/messagePassi
import { BackgroundToSidePanelRequestType } from 'src/background/messagePassing/types/requests' import { BackgroundToSidePanelRequestType } from 'src/background/messagePassing/types/requests'
import { setSidePanelBehavior, setSidePanelOptions } from 'src/background/utils/chromeSidePanelUtils' import { setSidePanelBehavior, setSidePanelOptions } from 'src/background/utils/chromeSidePanelUtils'
import { readIsOnboardedFromStorage } from 'src/background/utils/persistedStateUtils' import { readIsOnboardedFromStorage } from 'src/background/utils/persistedStateUtils'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
export const EXTENSION_ID = chrome.runtime.id export const EXTENSION_ID = chrome.runtime.id
...@@ -16,6 +18,8 @@ export const EXTENSION_ID = chrome.runtime.id ...@@ -16,6 +18,8 @@ export const EXTENSION_ID = chrome.runtime.id
initMessageBridge() initMessageBridge()
async function initApp(): Promise<void> { async function initApp(): Promise<void> {
const userId = await getUniqueId()
initSentryForBrowserScripts(SentryAppNameTag.Background, userId)
await initStatSigForBrowserScripts() await initStatSigForBrowserScripts()
await initExtensionAnalytics() await initExtensionAnalytics()
......
...@@ -250,7 +250,7 @@ async function logError( ...@@ -250,7 +250,7 @@ async function logError(
await contentScriptUtilityMessageChannel.sendMessage(message) await contentScriptUtilityMessageChannel.sendMessage(message)
} }
// These go to Amplitude instead of Datadog since they are informational // These go to Amplitude instead of Sentry since they are informational
async function passAnalytics(message: string, tags: Record<string, string>): Promise<void> { async function passAnalytics(message: string, tags: Record<string, string>): Promise<void> {
const logMessage: AnalyticsLog = { const logMessage: AnalyticsLog = {
type: ContentScriptUtilityMessageType.AnalyticsLog, type: ContentScriptUtilityMessageType.AnalyticsLog,
......
...@@ -4,13 +4,24 @@ ...@@ -4,13 +4,24 @@
import React from 'react' import React from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import OnboardingApp from 'src/app/OnboardingApp' import OnboardingApp from 'src/app/OnboardingApp'
import { initializeSentry, SentryAppNameTag } from 'src/app/sentry'
import { initializeReduxStore } from 'src/store/store' import { initializeReduxStore } from 'src/store/store'
import { ExtensionAppLocation, StoreSynchronization } from 'src/store/storeSynchronization' import { ExtensionAppLocation, StoreSynchronization } from 'src/store/storeSynchronization'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any ;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any
// The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem // The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem
// see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326 // see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326
getUniqueId()
.then((userId) => {
initializeSentry(SentryAppNameTag.Onboarding, userId)
})
.catch((error) => {
logger.error(error, {
tags: { file: 'SidebarApp.tsx', function: 'getUniqueId' },
})
})
async function initOnboarding(): Promise<void> { async function initOnboarding(): Promise<void> {
await initializeReduxStore() await initializeReduxStore()
......
...@@ -4,12 +4,23 @@ ...@@ -4,12 +4,23 @@
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import PopupApp from 'src/app/PopupApp' import PopupApp from 'src/app/PopupApp'
import { initializeSentry, SentryAppNameTag } from 'src/app/sentry'
import { initializeReduxStore } from 'src/store/store' import { initializeReduxStore } from 'src/store/store'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any ;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any
// The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem // The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem
// see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326 // see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326
getUniqueId()
.then((userId) => {
initializeSentry(SentryAppNameTag.Popup, userId)
})
.catch((error) => {
logger.error(error, {
tags: { file: 'popup.tsx', function: 'getUniqueId' },
})
})
async function initPopup(): Promise<void> { async function initPopup(): Promise<void> {
await initializeReduxStore({ readOnly: true }) await initializeReduxStore({ readOnly: true })
......
...@@ -4,12 +4,23 @@ ...@@ -4,12 +4,23 @@
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import UnitagClaimApp from 'src/app/UnitagClaimApp' import UnitagClaimApp from 'src/app/UnitagClaimApp'
import { SentryAppNameTag, initializeSentry } from 'src/app/sentry'
import { initializeReduxStore } from 'src/store/store' import { initializeReduxStore } from 'src/store/store'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any ;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any
// The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem // The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem
// see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326 // see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326
getUniqueId()
.then((userId) => {
initializeSentry(SentryAppNameTag.UnitagClaim, userId)
})
.catch((error) => {
logger.error(error, {
tags: { file: 'unitagClaim.tsx', function: 'getUniqueId' },
})
})
async function initUnitagClaim(): Promise<void> { async function initUnitagClaim(): Promise<void> {
await initializeReduxStore({ readOnly: true }) await initializeReduxStore({ readOnly: true })
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "Uniswap Extension", "name": "Uniswap Extension",
"description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.", "description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.",
"version": "1.15.0", "version": "1.13.0",
"minimum_chrome_version": "116", "minimum_chrome_version": "116",
"icons": { "icons": {
"16": "assets/icon16.png", "16": "assets/icon16.png",
......
import { RankingType } from 'uniswap/src/data/types' import { RankingType } from 'wallet/src/features/wallet/types'
// only add fields that are persisted // only add fields that are persisted
export const initialSchema = { export const initialSchema = {
......
import { createReduxEnhancer } from '@sentry/react'
import { PreloadedState } from 'redux' import { PreloadedState } from 'redux'
import { persistReducer, persistStore } from 'redux-persist' import { persistReducer, persistStore } from 'redux-persist'
import { localStorage } from 'redux-persist-webextension-storage' import { localStorage } from 'redux-persist-webextension-storage'
...@@ -26,6 +27,18 @@ const persistConfig = { ...@@ -26,6 +27,18 @@ const persistConfig = {
const persistedReducer = enhancePersistReducer(persistReducer(persistConfig, extensionReducer)) const persistedReducer = enhancePersistReducer(persistReducer(persistConfig, extensionReducer))
const sentryReduxEnhancer = createReduxEnhancer({
// TODO(EXT-1022): uncomment this once we add an analytics opt-out setting.
// stateTransformer: (state: WebState): Maybe<WebState> => {
// Do not log the state if a user has opted out of analytics.
// if (state.telemetry.allowAnalytics) {
// return state
// } else {
// return null
// }
// },
})
const dataDogReduxEnhancer = createDatadogReduxEnhancer({ const dataDogReduxEnhancer = createDatadogReduxEnhancer({
shouldLogReduxState: (state: ExtensionState): boolean => { shouldLogReduxState: (state: ExtensionState): boolean => {
// Do not log the state if a user has opted out of analytics. // Do not log the state if a user has opted out of analytics.
...@@ -40,7 +53,7 @@ const setupStore = (preloadedState?: PreloadedState<ExtensionState>): ReturnType ...@@ -40,7 +53,7 @@ const setupStore = (preloadedState?: PreloadedState<ExtensionState>): ReturnType
additionalSagas: [rootExtensionSaga], additionalSagas: [rootExtensionSaga],
middlewareBefore: __DEV__ ? [loggerMiddleware] : [], middlewareBefore: __DEV__ ? [loggerMiddleware] : [],
middlewareAfter: [fiatOnRampAggregatorApi.middleware], middlewareAfter: [fiatOnRampAggregatorApi.middleware],
enhancers: [dataDogReduxEnhancer], enhancers: [sentryReduxEnhancer, dataDogReduxEnhancer],
}) })
} }
......
...@@ -7,6 +7,7 @@ const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin' ...@@ -7,6 +7,7 @@ const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'
const fs = require('fs') const fs = require('fs')
const DotenvPlugin = require('dotenv-webpack') const DotenvPlugin = require('dotenv-webpack')
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin') const NodePolyfillPlugin = require('node-polyfill-webpack-plugin')
const { sentryWebpackPlugin } = require('@sentry/webpack-plugin')
const NODE_ENV = process.env.NODE_ENV || 'development' const NODE_ENV = process.env.NODE_ENV || 'development'
const POLL_ENV = process.env.WEBPACK_POLLING_INTERVAL const POLL_ENV = process.env.WEBPACK_POLLING_INTERVAL
...@@ -355,6 +356,12 @@ module.exports = (env) => { ...@@ -355,6 +356,12 @@ module.exports = (env) => {
}, },
], ],
}), }),
sentryWebpackPlugin({
authToken: env.SENTRY_AUTH_TOKEN,
org: 'uniswap-labs',
project: 'extension-wallet',
telemetry: process.env.NODE_ENV === 'production',
}),
], ],
...extras, ...extras,
} }
......
/** @type {Detox.DetoxConfig} */
module.exports = {
testRunner: {
args: {
'$0': 'jest',
config: 'e2e/jest.config.js'
},
jest: {
setupTimeout: 300000
}
},
apps: {
'ios.debug': {
type: "ios.app",
binaryPath: "ios/build/Build/Products/Debug-iphonesimulator/Uniswap.app",
build: "RN_SRC_EXT=e2e.js,e2e.ts xcodebuild -workspace ios/Uniswap.xcworkspace -scheme Uniswap -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build -UseModernBuildSystem=YES -arch x86_64"
},
'ios.release': {
type: 'ios.app',
binaryPath: "ios/build/Build/Products/Dev-iphonesimulator/Uniswap.app",
build: "RN_SRC_EXT=e2e.js,e2e.ts xcodebuild -workspace ios/Uniswap.xcworkspace -scheme Uniswap -configuration Dev -sdk iphonesimulator -derivedDataPath ios/build -UseModernBuildSystem=YES -arch x86_64"
},
'android.debug': {
type: 'android.apk',
binaryPath: 'android/app/build/outputs/apk/dev/debug/app-dev-debug.apk',
testBinaryPath: "android/app/build/outputs/apk/androidTest/dev/debug/app-dev-debug-androidTest.apk",
build: 'cd android && ./gradlew assembleDevDebug assembleAndroidTest -DtestBuildType=debug',
reversePorts: [
8081
]
},
'android.release': {
type: 'android.apk',
binaryPath: 'android/app/build/outputs/apk/dev/release/app-dev-release.apk',
testBinaryPath: "android/app/build/outputs/apk/androidTest/dev/release/app-dev-release-androidTest.apk",
build: 'cd android && ./gradlew assembleDevRelease assembleAndroidTest -DtestBuildType=release'
}
},
devices: {
simulator: {
type: 'ios.simulator',
device: {
type: "iPhone 15"
}
},
attached: {
type: 'android.attached',
device: {
adbName: '.*'
}
},
emulator: {
type: 'android.emulator',
device: {
avdName: 'Pixel_6_API_34'
}
}
},
configurations: {
"ios.sim.debug": {
device: "simulator",
app: "ios.debug"
},
"ios.sim.release": {
device: "simulator",
app: "ios.release"
},
"android.emu.debug": {
device: "emulator",
app: "android.debug"
},
"android.emu.release": {
device: "emulator",
app: "android.release"
}
}
};
baselineBranch: main
executionOrder:
continueOnFailure: true
appId: com.uniswap.mobile.dev
---
- runFlow: subflows/start.yaml
- tapOn: 'Create a wallet'
- waitForAnimationToEnd
- tapOn: 'Skip' # unitag
- waitForAnimationToEnd
- tapOn: 'Skip' # notifications
- waitForAnimationToEnd
- tapOn: 'Skip' # faceid
- runFlow: subflows/biometrics-confirm.yaml
# home screen
appId: com.uniswap.mobile.dev appId: com.uniswap.mobile.dev
env:
E2E_RECOVERY_PHRASE: ${E2E_RECOVERY_PHRASE}
--- ---
- tapOn: 'Add an existing wallet' - launchApp:
appId: 'com.uniswap.mobile.dev'
clearState: true
clearKeychain: true # optional: clear *entire* iOS keychain
- extendedWaitUntil:
visible: 'Create a wallet'
- tapOn: 'Create a wallet'
- waitForAnimationToEnd - waitForAnimationToEnd
- tapOn: 'Import a wallet'
- waitForAnimationToEnd
- inputText: ${E2E_RECOVERY_PHRASE}
- waitForAnimationToEnd
- tapOn: 'Continue'
- waitForAnimationToEnd
- tapOn: 'Continue'
- tapOn: 'Skip' - tapOn: 'Skip'
- waitForAnimationToEnd
- tapOn: 'Skip' - tapOn: 'Skip'
- waitForAnimationToEnd
- tapOn: 'Skip' - tapOn: 'Skip'
- runFlow: biometrics-confirm.yaml - extendedWaitUntil:
visible:
id: 'confirm'
- tapOn:
id: 'confirm'
- waitForAnimationToEnd
- tapOn: 'Send'
- waitForAnimationToEnd - waitForAnimationToEnd
- 'back'
appId: com.uniswap.mobile.dev
---
- extendedWaitUntil:
visible:
id: 'confirm'
timeout: 10000
- tapOn:
id: 'confirm' # are you sure?
- waitForAnimationToEnd
appId: com.uniswap.mobile.dev
---
- launchApp:
permissions:
contacts: unset
notifications: unset
clearState: true
clearKeychain: true # optional: clear *entire* iOS keychain
appId: com.uniswap.mobile.dev
env:
E2E_RECOVERY_PHRASE: ${E2E_RECOVERY_PHRASE}
---
- runFlow: subflows/start.yaml
- runFlow: subflows/recover-fast.yaml
# Start of swap flow
- tapOn:
id: 'swap'
- tapOn:
id: 'choose-output-token-label'
- tapOn:
id: 'explore-search-input'
- inputText: btc
- tapOn:
id: 'token-option-8453-cbBTC'
- tapOn: '0 cbBTC'
- tapOn:
id: 'decimal-pad-0'
- tapOn:
id: 'decimal-pad-decimal'
- tapOn:
id: 'decimal-pad-0'
- tapOn:
id: 'decimal-pad-0'
- tapOn:
id: 'decimal-pad-0'
- tapOn:
id: 'decimal-pad-0'
- tapOn:
id: 'decimal-pad-0'
- tapOn:
id: 'decimal-pad-1'
- tapOn:
id: 'review-swap'
- tapOn: Show more
- tapOn:
id: 'swap'
- extendedWaitUntil:
visible: 'Swapped'
timeout: 10000
...@@ -150,7 +150,7 @@ Note: If you are indeed using an Apple Silicon Mac, we recommend setting up your ...@@ -150,7 +150,7 @@ Note: If you are indeed using an Apple Silicon Mac, we recommend setting up your
You should start with downloading Xcode if you don't already have it installed, since the file is so large. You can find it here: [developer.apple.com/xcode](https://developer.apple.com/xcode/) You should start with downloading Xcode if you don't already have it installed, since the file is so large. You can find it here: [developer.apple.com/xcode](https://developer.apple.com/xcode/)
You must use the [Required Xcode Version](https://github.com/Uniswap/universe/blob/main/apps/mobile/scripts/podinstall.sh#L5) to compile the app. [Older versions of xCode can be found here](https://developer.apple.com/download/all/?q=xcode). You must use **XCode 15** to compile the app. [Older versions of xCode can be found here](https://developer.apple.com/download/all/?q=xcode).
#### Add Xcode Command Line Tools #### Add Xcode Command Line Tools
......
...@@ -63,10 +63,10 @@ def reactNativeArchitectures() { ...@@ -63,10 +63,10 @@ def reactNativeArchitectures() {
} }
boolean isCI = System.getenv('CI') != null boolean isCI = System.getenv('CI') != null
boolean isE2E = System.getenv('E2E_MODE') != null boolean isDetox = System.getenv('DETOX_MODE') != null
boolean sentryPropertiesAvailable = System.getenv('SENTRY_AUTH_TOKEN') != null && System.getenv('SENTRY_PROJECT') != null && System.getenv('SENTRY_ORG') != null boolean sentryPropertiesAvailable = System.getenv('SENTRY_AUTH_TOKEN') != null && System.getenv('SENTRY_PROJECT') != null && System.getenv('SENTRY_ORG') != null
if (isCI && sentryPropertiesAvailable && !isE2E) { if (isCI && sentryPropertiesAvailable && !isDetox) {
project.ext.sentryCli = [ project.ext.sentryCli = [
logLevel: "info", logLevel: "info",
] ]
...@@ -85,13 +85,13 @@ if (isCI && sentryPropertiesAvailable && !isE2E) { ...@@ -85,13 +85,13 @@ if (isCI && sentryPropertiesAvailable && !isE2E) {
boolean datadogPropertiesAvailable = System.getenv('DATADOG_API_KEY') != null boolean datadogPropertiesAvailable = System.getenv('DATADOG_API_KEY') != null
if (isCI && datadogPropertiesAvailable && !isE2E) { if (isCI && datadogPropertiesAvailable && !isDetox) {
apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle" apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle"
} }
def devVersionName = "1.45" def devVersionName = "1.43.1"
def betaVersionName = "1.45" def betaVersionName = "1.43.1"
def prodVersionName = "1.45" def prodVersionName = "1.43.1"
android { android {
ndkVersion rootProject.ext.ndkVersion ndkVersion rootProject.ext.ndkVersion
...@@ -104,6 +104,8 @@ android { ...@@ -104,6 +104,8 @@ android {
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
} }
splits { splits {
abi { abi {
...@@ -249,20 +251,14 @@ dependencies { ...@@ -249,20 +251,14 @@ dependencies {
implementation("androidx.core:core-performance:$corePerf") implementation("androidx.core:core-performance:$corePerf")
implementation("androidx.core:core-performance-play-services:$corePerf") implementation("androidx.core:core-performance-play-services:$corePerf")
implementation 'com.onesignal:OneSignal:4.8.9'
implementation 'com.github.statsig-io:android-sdk:4.36.0'
// This is required for the backported AndroidX Photo Picker on versions of Android below 30
implementation("androidx.activity:activity:1.9.+")
// For animated GIF support
implementation 'com.facebook.fresco:animated-gif:3.6.0'
if (hermesEnabled.toBoolean()) { if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android") implementation("com.facebook.react:hermes-android")
} else { } else {
implementation jscFlavor implementation jscFlavor
} }
androidTestImplementation('com.wix:detox:+') {
exclude module: "protobuf-lite"
}
} }
apply from: file("${nodeModulesPath}/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) apply from: file("${nodeModulesPath}/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
package com.uniswap;
import com.wix.detox.Detox;
import com.wix.detox.config.DetoxConfig;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class DetoxTest {
@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false);
@Test
public void runDetoxTests() {
DetoxConfig detoxConfig = new DetoxConfig();
detoxConfig.idlePolicyConfig.masterTimeoutSec = 90;
detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60;
detoxConfig.rnContextLoadTimeoutSec = (BuildConfig.DEBUG ? 180 : 60);
Detox.runTests(mActivityRule, detoxConfig);
}
}
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
<application <application
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:ignore="GoogleAppIndexingWarning" tools:ignore="GoogleAppIndexingWarning"
tools:targetApi="28"> tools:targetApi="28"
android:networkSecurityConfig="@xml/network_security_config">
</application> </application>
</manifest> </manifest>
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">10.0.2.2</domain>
<domain includeSubdomains="true">localhost</domain>
</domain-config>
</network-security-config>
...@@ -9,10 +9,6 @@ ...@@ -9,10 +9,6 @@
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID"/> <uses-permission android:name="com.google.android.gms.permission.AD_ID"/>
<!-- This permission which may be added by expo modules. Unless it's used app-wide, should not be included in our app per Play Store rules -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" tools:node="remove" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" tools:node="remove" />
<application <application
android:name=".MainApplication" android:name=".MainApplication"
android:label="@string/app_name" android:label="@string/app_name"
...@@ -27,17 +23,6 @@ ...@@ -27,17 +23,6 @@
android:taskAffinity="" android:taskAffinity=""
android:excludeFromRecents="true"> android:excludeFromRecents="true">
<!-- Trigger Google Play services to install the backported photo picker module. -->
<service android:name="com.google.android.gms.metadata.ModuleDependencies"
android:enabled="false"
android:exported="false"
tools:ignore="MissingClass">
<intent-filter>
<action android:name="com.google.android.gms.metadata.MODULE_DEPENDENCIES" />
</intent-filter>
<meta-data android:name="photopicker_activity:0:required" android:value="" />
</service>
<meta-data <meta-data
android:name="com.onesignal.messaging.default_notification_icon" android:name="com.onesignal.messaging.default_notification_icon"
android:resource="@drawable/ic_stat_onesignal_default" /> android:resource="@drawable/ic_stat_onesignal_default" />
...@@ -46,9 +31,6 @@ ...@@ -46,9 +31,6 @@
android:name="com.onesignal.NotificationAccentColor.DEFAULT" android:name="com.onesignal.NotificationAccentColor.DEFAULT"
android:value="@string/notification_accent_color" /> android:value="@string/notification_accent_color" />
<meta-data android:name="com.onesignal.NotificationServiceExtension"
android:value="com.uniswap.notifications.NotificationExtension" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
......
package com.uniswap.notifications
import android.app.Application
import android.content.Context
import android.provider.Settings.Secure
import com.onesignal.OSNotificationReceivedEvent
import com.onesignal.OneSignal.OSRemoteNotificationReceivedHandler
import com.statsig.androidsdk.Statsig
import com.statsig.androidsdk.StatsigOptions
import com.statsig.androidsdk.StatsigUser
import com.uniswap.BuildConfig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/**
* OneSignal extension used to intercept notifications, integrating with Statsig to gate and expose
* test groups.
*/
class NotificationExtension : OSRemoteNotificationReceivedHandler {
private val scope = CoroutineScope(Dispatchers.IO)
override fun remoteNotificationReceived(
context: Context?,
notificationReceivedEvent: OSNotificationReceivedEvent
) {
val notification = notificationReceivedEvent.notification
val additionalData = notification.additionalData
val notificationType = if (additionalData.has(FIELD_NOTIFICATION_TYPE)) additionalData.getString(FIELD_NOTIFICATION_TYPE) else null
val isGatedNotification = notificationType == TYPE_UNFUNDED_WALLET_REMINDER ||
notificationType == TYPE_PRICE_ALERT
if (isGatedNotification) {
scope.launch(Dispatchers.IO) {
if (!Statsig.isInitialized()) {
val options = StatsigOptions(api = STATSIG_PROXY_URL, eventLoggingAPI = STATSIG_PROXY_URL).apply {
setEnvironmentParameter(STATSIG_ENVIRONMENT_KEY_TIER, getStatsigTier())
}
val deviceId = Secure.getString(context!!.contentResolver, Secure.ANDROID_ID)
val user = StatsigUser(userID = deviceId)
user.custom = mapOf("app" to "mobile")
Statsig.initialize(
context!!.applicationContext as Application,
STATSIG_SDK_KEY,
user,
options
)
}
val enabled = when(notificationType) {
TYPE_UNFUNDED_WALLET_REMINDER -> Statsig.checkGate(FEATURE_GATE_UNFUNDED_WALLET)
TYPE_PRICE_ALERT -> Statsig.checkGate(FEATURE_GATE_PRICE_ALERT)
else -> true
}
// Passing null will skip the notification
notificationReceivedEvent.complete(if (enabled) notification else null)
}
} else {
notificationReceivedEvent.complete(notification)
}
}
private fun getStatsigTier(): String = when(BuildConfig.FLAVOR) {
"dev" -> "development"
"beta" -> "beta"
"prod" -> "production"
else -> "production"
}
companion object {
// fake value that gets replaced by the proxy
private const val STATSIG_SDK_KEY = "client-000000000000000000000000000000000000000000"
private const val STATSIG_PROXY_URL =
"https://gating.android.wallet.gateway.uniswap.org/v1/statsig-proxy"
private const val STATSIG_ENVIRONMENT_KEY_TIER = "tier"
private const val FEATURE_GATE_UNFUNDED_WALLET = "notification_unfunded_wallet"
private const val FEATURE_GATE_PRICE_ALERT = "notification_price_alert"
private const val FIELD_NOTIFICATION_TYPE = "notification_type"
private const val TYPE_UNFUNDED_WALLET_REMINDER = "unfunded_wallet_reminder"
private const val TYPE_PRICE_ALERT = "price_alert"
}
}
...@@ -8,7 +8,6 @@ ...@@ -8,7 +8,6 @@
<locale android:name="fr"/> <locale android:name="fr"/>
<locale android:name="ja"/> <locale android:name="ja"/>
<locale android:name="pt"/> <locale android:name="pt"/>
<locale android:name="vi"/>
<locale android:name="es-ES"/> <locale android:name="es-ES"/>
<locale android:name="es-US"/> <locale android:name="es-US"/>
<locale android:name="es-419"/> <locale android:name="es-419"/>
......
...@@ -19,7 +19,6 @@ buildscript { ...@@ -19,7 +19,6 @@ buildscript {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven { url 'https://jitpack.io' }
} }
dependencies { dependencies {
classpath("com.android.tools.build:gradle") classpath("com.android.tools.build:gradle")
...@@ -37,6 +36,11 @@ plugins { ...@@ -37,6 +36,11 @@ plugins {
} }
allprojects { allprojects {
repositories {
maven {
url = rootProject.file("../../../node_modules/detox/Detox-android")
}
}
project.pluginManager.withPlugin("com.facebook.react") { project.pluginManager.withPlugin("com.facebook.react") {
react { react {
reactNativeDir = rootProject.file("../../../node_modules/react-native/") reactNativeDir = rootProject.file("../../../node_modules/react-native/")
......
...@@ -8,3 +8,6 @@ apply from: new File(["node", "--print", "require.resolve('../../../node_modules ...@@ -8,3 +8,6 @@ apply from: new File(["node", "--print", "require.resolve('../../../node_modules
useExpoModules() useExpoModules()
include ':@sentry_react-native' include ':@sentry_react-native'
include ':detox'
project(':detox').projectDir = new File('../../../node_modules/detox/android/detox')
import { HomeBasicInteractions } from 'e2e/usecases/home/HomeBasicInteractions'
import { WatchWallet } from 'e2e/usecases/onboarding/WatchWallet'
describe('Home', () => {
beforeEach(async () => {
await device.launchApp()
await WatchWallet()
})
it('tests basic home screen interactions', HomeBasicInteractions)
})
import { CreateNewWallet } from 'e2e/usecases/onboarding/CreateNewWallet'
import { ImportWallet } from 'e2e/usecases/onboarding/ImportWallet'
import { WatchWallet } from 'e2e/usecases/onboarding/WatchWallet'
describe('Onboarding', () => {
beforeEach(async () => {
await device.launchApp({ newInstance: true })
})
afterEach(async () => {
await device.clearKeychain()
await device.uninstallApp()
await device.installApp()
})
it('creates a new wallet', CreateNewWallet)
it('watches wallet', WatchWallet)
it('imports a testing wallet using recovery phrase', ImportWallet)
})
# E2E Tests
The e2e tests use [detox](https://github.com/wix/Detox).
## Running tests
### iOS
Detox environment requires installation of the same environment as the main iOS application and additionally the iPhone 15 simulator.
The choice of this simulator is hardcoded in order to reflect e2e environment setup and is dictated by the github actions virtual machine on which the e2e tests will take place.
#### Debug mode
To run tests in debug mode, run bundler:
```
yarn mobile e2e:packager
```
Build debug testing app:
```
yarn mobile e2e:ios:build:debug
```
Run ios e2e tests in debug mode:
```
yarn mobile e2e:ios:test:debug
```
Useful perameters:
`--testNamePattern test-name` to run a single test, replace `test-name` with test file name without extension e.g.: `Swap` or `Onboarding`.
`--reuse` to start the test from a current app state. Useful for testing nested screen behaviour without going through onboarding and navigation steps.
#### Release mode
To run tests in release mode:
```
yarn mobile e2e:ios:test:release
```
It builds and runs tests in one go.
## Mocking
E2E tests should remain as close as possible to production, but sometimes mocking is necessary.
Only mocking entire files is supported at the moment, so you may need to reorganize functions. To mock a file, create a new one with the same name and extension `mock.ts` (e.g. `AnimatedHeader.ts` -> `AnimatedHeader.mock.ts`) in the same directory. The metro bundler will override any file that has a `mock.ts` equivalent in Detox runs.
Android native views based on jetpack compose and libraries utilizing long-running asynchronouse background processes like sentry are not supported by detox currently. Imports mocking is unfortunatelly not supported by detox yet. If such problems occur, the entire component using problematic library needs to be mocked or a component exposing only targeted library needs to be created and then it can be mocked, precisely replacing only targeted library.
To mock a component for specific platform follow this pattern:
iOS: `AnimatedHeader.ts` -> `AnimatedHeader.ios.mock.ts`
Android: `AnimatedHeader.ts` -> `AnimatedHeader.android.mock.ts`
Read more here https://wix.github.io/Detox/docs/guide/mocking/
import { WatchWallet } from 'e2e/usecases/onboarding/WatchWallet'
import { SwapBasicInteractions } from 'e2e/usecases/swap/SwapBasicInteractions'
describe('Swap', () => {
beforeAll(async () => {
await device.launchApp({ newInstance: true })
await WatchWallet()
})
it('tests swap screen interactions', SwapBasicInteractions)
})
import { WatchWallet } from 'e2e/usecases/onboarding/WatchWallet'
import { TokenDetailsBasicInteractions } from 'e2e/usecases/tokenDetails/TokenDetailsBasicInteractions'
describe('TokenDetails', () => {
beforeAll(async () => {
await device.launchApp({ newInstance: true })
await WatchWallet()
})
it('tests token details screen interactions', TokenDetailsBasicInteractions)
})
import { device } from 'detox'
import { permissions } from './utils/fixtures'
beforeAll(async () => {
await device.installApp()
await device.launchApp({
newInstance: false,
permissions,
})
})
/** @type {import('@jest/types').Config.InitialOptions} */
module.exports = {
rootDir: '..',
testMatch: ['<rootDir>/e2e/**/*.e2e.ts'],
testTimeout: 240000,
maxWorkers: 1,
globalSetup: 'detox/runners/jest/globalSetup',
globalTeardown: 'detox/runners/jest/globalTeardown',
reporters: ['detox/runners/jest/reporter'],
testEnvironment: 'detox/runners/jest/testEnvironment',
verbose: true,
moduleDirectories: ['node_modules', '<rootDir>']
};
import { by, element, expect } from 'detox'
import { TestWatchedWallet } from 'e2e/utils/fixtures'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
export async function HomeBasicInteractions(): Promise<void> {
await expect(element(by.text(TestWatchedWallet.displayName))).toBeVisible()
await expect(element(by.id(TestID.Swap))).toBeVisible()
await expect(element(by.id(TestID.SearchTokensAndWallets))).toBeVisible()
// opens AccountSwitcherModal by clicking on account avatar
await expect(element(by.id(TestID.AccountHeaderAvatar))).toBeVisible()
// checks if portfolio balance is visible
await expect(element(by.id(TestID.PortfolioBalance))).toBeVisible()
// copies wallet address from AccountSwitcherModal
await element(by.id(TestID.AccountHeaderCopyAddress)).tap()
// checks if notification toast is visible with title "Address copied"
await expect(element(by.id(TestID.NotificationToastTitle))).toBeVisible()
await expect(element(by.id(TestID.NotificationToastTitle))).toHaveText('Address copied')
// checks if list was rendered properly by checking if the first item is visible
await expect(element(by.id('token-list-item-0'))).toBeVisible()
// scrolls to the bottom of the token list
await element(by.id('token-list-item-0')).swipe('up')
// checks if only tabs headers are visible then scrolled to bottom
await expect(element(by.id(TestID.AccountHeaderAvatar))).not.toBeVisible()
await expect(element(by.id(TestID.PortfolioBalance))).not.toBeVisible()
// for some reason react-native-tab-view renders headers twice, thats why first matching item was picked
await expect(element(by.id('home-tab-Tokens')).atIndex(0)).toBeVisible()
await expect(element(by.id('home-tab-NFTs')).atIndex(0)).toBeVisible()
await expect(element(by.id('home-tab-Activity')).atIndex(0)).toBeVisible()
// checks if the first item of hidden list is not visible
await expect(element(by.id('token-list-item-0'))).not.toBeVisible()
// hidden item does not exist
await expect(element(by.id('token-list-item-25'))).not.toExist()
// taps on "show" button to show hidden elements
await element(by.id(TestID.ShowHiddenTokens)).tap()
// checks if first hidden element is visible
await expect(element(by.id('token-list-item-25'))).toExist()
// taps on "hide" button to show hidden elements
await element(by.id(TestID.ShowHiddenTokens)).tap()
// checks if first item of the hidden item is not visible again
await expect(element(by.id('token-list-item-25'))).not.toExist()
// switches to NFTs tab
await element(by.id('home-tab-NFTs')).atIndex(0).tap()
// checks is if tokens are visible
await expect(element(by.id('nfts-list-item-0'))).toBeVisible()
// switches to Activity tab
await element(by.id('home-tab-Activity')).atIndex(0).tap()
// checks is if tokens are visible
await expect(element(by.id('activity-list-item-0'))).toBeVisible()
// switches back to tokens tab
await element(by.id('home-tab-Tokens')).atIndex(0).tap()
// scrolls to the bottom of the token list
await element(by.id('token-list-item-16')).swipe('down')
// checks if list of tokens was rendered properly by checking first token visibility
await expect(element(by.id('token-list-item-0'))).toBeVisible()
}
import { by, element, expect } from 'detox'
import { TestWallet } from 'e2e/utils/fixtures'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
export async function CreateNewWallet(): Promise<void> {
// Selects "Create a new wallet" option on the landing screen
await element(by.id(TestID.CreateAccount)).tap()
// Skips unitag flow
await element(by.id(TestID.Skip)).tap()
// Taps "Let's keep it safe" on QRAnimation screen
await element(by.id(TestID.Next)).tap()
// Check is both manual and cloud backup options are available on BackupScreen
await expect(element(by.id(TestID.AddCloudBackup))).toBeVisible()
await expect(element(by.id(TestID.AddManualBackup))).toBeVisible()
// Picks "Manual backup" option
await element(by.id(TestID.AddManualBackup)).tap()
// Checks if ManualBackupScreen warning displays and taps "I'm ready" button
await expect(element(by.id(TestID.Confirm))).toBeVisible()
await element(by.id(TestID.Confirm)).tap()
// Taps continue on ManualBackupScreen
await element(by.id(TestID.Next)).tap()
// Taps continue on manual backup confirmation screen. It is replaced by mock because detox
// can't interact with native screens
await element(by.id(TestID.Continue)).tap()
// Skips notification setup by tapping "Maybe later" button
await element(by.id(TestID.Skip)).tap()
// Skips biometrics setup by tapping "Maybe later" button
await element(by.id(TestID.Skip)).tap()
// Confirms by tapping "Skip" on warning modal
await element(by.id(TestID.Confirm)).tap()
// Confirms if user successfuly finished create new wallet flow by checking if provided wallet name is
// displayed and other
await expect(element(by.text(TestWallet.name))).toBeVisible()
await expect(element(by.id(TestID.Swap))).toBeVisible()
await expect(element(by.id(TestID.SearchTokensAndWallets))).toBeVisible()
}
import { by, element, expect } from 'detox'
import { TestWallet } from 'e2e/utils/fixtures'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
export async function ImportWallet(): Promise<void> {
// Selects "Add an existing wallet" option on the landing screen
await element(by.id(TestID.ImportAccount)).tap()
// Picks Import a wallet by recovery phase option
await element(by.id(TestID.OnboardingImportSeedPhrase)).tap()
// Checks if recovery phase input is in focus and types recovery phrase in
await element(by.id(TestID.ImportAccountInput)).typeText(TestWallet.recoveryPhrase)
// Taps continue navigating to SelectWalletScreen
await element(by.id(TestID.Continue)).tap()
// Taps continue on SelectWalletScreen
await waitFor(element(by.id(`${TestID.WalletCard}-1`)))
.toBeVisible()
.withTimeout(10000)
await element(by.id(TestID.Next)).tap()
// Skips cloud backup step on BackupScreen by clicking "Maybe later"
await expect(element(by.id(TestID.AddCloudBackup))).toBeVisible()
await element(by.id(TestID.Next)).tap()
// Skips notification setup by tapping "Maybe later" button
await element(by.id(TestID.Skip)).tap()
// Skips biometrics setup by tapping "Maybe later" button
await element(by.id(TestID.Skip)).tap()
// Confirms by tapping "Skip" on warning modal
await element(by.id(TestID.Confirm)).tap()
// Confirms if user successfuly finished create new wallet flow by checking if provided wallet name is
// displayed and other
await expect(element(by.text(TestWallet.name))).toBeVisible()
await expect(element(by.id(TestID.Swap))).toBeVisible()
await expect(element(by.id(TestID.SearchTokensAndWallets))).toBeVisible()
}
import { by, element, expect } from 'detox'
import { TestWatchedWallet } from 'e2e/utils/fixtures'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
export async function WatchWallet(): Promise<void> {
// Selects "Add an existing wallet" option on the landing screen
await element(by.id(TestID.ImportAccount)).tap()
// Picks Watch a wallet option on ImportMethodScreen
await element(by.id(TestID.WatchWallet)).tap()
// Checks if wallet name is in focus and types recovery phrase in
await expect(element(by.id(TestID.ImportAccountInput))).toBeFocused()
await element(by.id(TestID.ImportAccountInput)).typeText(TestWatchedWallet.ens)
// Confirms the entered wallet name by tapping "continue"
await element(by.id(TestID.Next)).tap()
// Checks if Home screen is displayed with a proper user name
await expect(element(by.text(TestWatchedWallet.displayName))).toBeVisible()
await expect(element(by.id(TestID.Swap))).toBeVisible()
await expect(element(by.id(TestID.SearchTokensAndWallets))).toBeVisible()
}
import { by, element, expect } from 'detox'
import { TestWatchedWallet } from 'e2e/utils/fixtures'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
export async function SwapBasicInteractions(): Promise<void> {
// Navigate to swap screen
await element(by.id(TestID.Swap)).tap()
// Checks if currency input is selected
await expect(element(by.id(TestID.AmountInputIn))).toBeFocused()
// Checks if "Max" button is available
await expect(element(by.id(TestID.SetMaxInput))).toBeVisible()
// Opens token selector modal on Swap screen
await element(by.id(TestID.ChooseOutputToken)).tap()
// Picks usdc output token
await element(by.text('USDC')).atIndex(0).tap()
// Taps .98765432101 into the swap input
await element(by.id('decimal-pad-.')).tap()
await element(by.id('decimal-pad-9')).tap()
await element(by.id('decimal-pad-8')).tap()
await element(by.id('decimal-pad-7')).tap()
await element(by.id('decimal-pad-6')).tap()
await element(by.id('decimal-pad-5')).tap()
await element(by.id('decimal-pad-4')).tap()
await element(by.id('decimal-pad-3')).tap()
await element(by.id('decimal-pad-2')).tap()
await element(by.id('decimal-pad-1')).tap()
await element(by.id('decimal-pad-0')).tap()
await element(by.id('decimal-pad-1')).tap()
// Taps a backspace button leaving .9876543210 value in the input field
await element(by.id('decimal-pad-backspace')).tap()
// Checks if expected input expected value: ".9876543210"
await expect(element(by.id(TestID.AmountInputIn))).toHaveText('.9876543210')
// Checks if expected error is displayed
await expect(element(by.text('You don’t have enough ETH'))).toBeVisible()
// Checks if expected output expected value: "0"
await expect(element(by.id(TestID.AmountInputOut))).not.toHaveText('0')
// Swaps input and output currencies
await element(by.id(TestID.SwitchCurrenciesButton)).tap()
// Checks if expected input expected value: "0"
await expect(element(by.id(TestID.AmountInputIn))).not.toHaveText('0')
// Checks if expected error is displayed
await expect(element(by.text('You don’t have enough USDC'))).toBeVisible()
// Checks if expected output expected value: ".9876543210"
await expect(element(by.id(TestID.AmountInputOut))).toHaveText('.9876543210')
// Swaps input and output currencies
await element(by.id(TestID.SwitchCurrenciesButton)).tap()
// Selects currency output
await element(by.id(TestID.AmountInputOut)).tap()
// Clears the output field
await element(by.id(TestID.AmountInputOut)).clearText()
await element(by.id('decimal-pad-1')).tap()
await element(by.id('decimal-pad-2')).tap()
await element(by.id('decimal-pad-3')).tap()
// Checks if output has expected value: "123"
await expect(element(by.id(TestID.AmountInputOut))).toHaveText('123')
// Checks if expected input value to be cleared
await expect(element(by.id(TestID.AmountInputIn))).not.toHaveText('0')
// Checks dollar value to be visible
await expect(element(by.text('$123.00'))).toBeVisible()
// Swipes swap modal by dragging down SwapFormHeader
await element(by.id(TestID.SwapFormHeader)).swipe('down', 'fast', 0.75)
// Checks if Home screen is visible and not covered
await expect(element(by.text(TestWatchedWallet.displayName))).toBeVisible()
await expect(element(by.id(TestID.Swap))).toBeVisible()
await expect(element(by.id(TestID.SearchTokensAndWallets))).toBeVisible()
}
import { by, element, expect } from 'detox'
import { TestWatchedWallet } from 'e2e/utils/fixtures'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
export async function TokenDetailsBasicInteractions(): Promise<void> {
// Opens "explore" modal
await element(by.id(TestID.SearchTokensAndWallets)).tap()
// Types "Uniswap" into "explore" screen search bar
await element(by.id(TestID.ExploreSearchInput)).typeText('Uniswap')
// Opnes "Uniswap" Mainnet token details screen
await element(by.id(`${TestID.SearchTokenItem}-Uniswap-${UniverseChainId.Mainnet}`)).tap()
// checks if ethereum title is displayed
await expect(element(by.id(TestID.TokenDetailsHeaderText))).toHaveText('Uniswap')
// checks if portfolio balance is visible
await expect(element(by.id(TestID.PriceExplorerAnimatedNumber))).toBeVisible()
// checks if relative price indicator is visible
await expect(element(by.id(TestID.RelativePriceChange))).toBeVisible()
// opens header "more" button dropdown menu
await expect(element(by.id(TestID.TokenDetailsMoreButton))).toBeVisible()
// checks if send button is not available
await expect(element(by.id(TestID.Send))).not.toBeVisible()
// checks if price exploerer chart is rendered
await expect(element(by.id(TestID.PriceExplorerChart))).toBeVisible()
// checks if all time ranges renders properly
await element(by.id('token-details-chart-time-range-button-1H')).tap()
await expect(element(by.id(TestID.PriceExplorerChart))).toBeVisible()
await element(by.id('token-details-chart-time-range-button-1W')).tap()
await expect(element(by.id(TestID.PriceExplorerChart))).toBeVisible()
await element(by.id('token-details-chart-time-range-button-1M')).tap()
await expect(element(by.id(TestID.PriceExplorerChart))).toBeVisible()
await element(by.id('token-details-chart-time-range-button-1Y')).tap()
await expect(element(by.id(TestID.PriceExplorerChart))).toBeVisible()
await element(by.id('token-details-chart-time-range-button-1D')).tap()
await expect(element(by.id(TestID.PriceExplorerChart))).toBeVisible()
// checks if sell and buy buttons are visible
await expect(element(by.id(TestID.TokenDetailsBuyButton))).toBeVisible()
await expect(element(by.id(TestID.TokenDetailsSellButton))).not.toBeVisible()
// scrolls to the bottom of the token details screen
await element(by.id(TestID.PriceExplorerChart)).swipe('up')
// cheks if token detels share links are available
await expect(element(by.id(TestID.TokenLinkEtherscan))).toBeVisible()
await expect(element(by.id(TestID.TokenLinkWebsite))).toBeVisible()
await expect(element(by.id(TestID.TokenLinkTwitter))).toBeVisible()
// taps on buy button
await element(by.id(TestID.TokenDetailsBuyButton)).tap()
// checks if it is displayed as expected
await expect(element(by.id(`${TestID.ChooseInputToken}-label`))).toHaveText('ETH')
await expect(element(by.id(`${TestID.ChooseOutputToken}-label`))).toHaveText('UNI')
await expect(element(by.id(TestID.ChooseInputToken))).toBeVisible()
await expect(element(by.id(TestID.AmountInputOut))).toBeFocused()
await expect(element(by.id(TestID.AmountInputOut))).toHaveText('')
// closes swap modal
await element(by.id(TestID.SwapFormHeader)).swipe('down')
// tests descreption read more button
await expect(element(by.id(TestID.ReadMoreButton))).toHaveText('Read more')
await element(by.id(TestID.ReadMoreButton)).tap()
await element(by.id(TestID.TokenDetailsAboutHeader)).swipe('up')
// tests descreption read less button
await expect(element(by.id(TestID.ReadMoreButton))).toHaveText('Read less')
await element(by.id(TestID.ReadMoreButton)).tap()
// navigates back to home screen
await element(by.id(TestID.Back)).tap()
// checks if home screen is rendered
await expect(element(by.text(TestWatchedWallet.displayName))).toBeVisible()
await expect(element(by.id(TestID.Swap))).toBeVisible()
await expect(element(by.id(TestID.SearchTokensAndWallets))).toBeVisible()
}
export const TestWallet = {
name: 'Wallet 1',
recoveryPhrase: 'oak reduce strong borrow control funny library disagree radio clarify degree pistol',
}
export const TestWatchedWallet = {
ens: 'Spenciefy',
displayName: 'spencer',
}
...@@ -9,11 +9,5 @@ ...@@ -9,11 +9,5 @@
<key>NSExtensionPrincipalClass</key> <key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).NotificationService</string> <string>$(PRODUCT_MODULE_NAME).NotificationService</string>
</dict> </dict>
<key>OneSignal_app_groups_key</key>
<string>group.com.uniswap.mobile.onesignal</string>
<key>STATSIG_SDK_KEY</key>
<string>$(STATSIG_SDK_KEY)</string>
<key>BUNDLE_ID_SUFFIX</key>
<string>$(BUNDLE_ID_SUFFIX)</string>
</dict> </dict>
</plist> </plist>
// File copied from Onesignal docs: https://documentation.onesignal.com/docs/react-native-sdk-setup
import UserNotifications import UserNotifications
import OneSignalExtension import OneSignalExtension
import Statsig
class NotificationService: UNNotificationServiceExtension { class NotificationService: UNNotificationServiceExtension {
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { var contentHandler: ((UNNotificationContent) -> Void)?
let bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) var receivedRequest: UNNotificationRequest!
var bestAttemptContent: UNMutableNotificationContent?
let userInfo = request.content.userInfo
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
// Fields per OneSignal docs self.receivedRequest = request
let custom = userInfo["custom"] as? [String: Any] self.contentHandler = contentHandler
let additionalData = custom?["a"] as? [String: Any] self.bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
let notificationType = additionalData?[Constants.fieldNotificationType] as? String if let bestAttemptContent = bestAttemptContent {
let isGatedNotification = notificationType == Constants.typeUnfundedWallet /* DEBUGGING: Uncomment the 2 lines below to check this extension is executing
|| notificationType == Constants.typePriceAlert Note, this extension only runs when mutable-content is set
Setting an attachment or action buttons automatically adds this */
if (!isGatedNotification) { #if DEBUG
OneSignalExtension.didReceiveNotificationExtensionRequest(request, with: bestAttemptContent, withContentHandler: contentHandler) print("Running NotificationServiceExtension")
return bestAttemptContent.body = "[Modified] " + bestAttemptContent.body
} #endif
func handleGatedNotification() { OneSignalExtension.didReceiveNotificationExtensionRequest(self.receivedRequest, with: bestAttemptContent, withContentHandler: self.contentHandler)
let enabled: Bool }
switch notificationType {
case Constants.typeUnfundedWallet:
enabled = Statsig.checkGate(Constants.gateUnfundedWallet)
case Constants.typePriceAlert:
enabled = Statsig.checkGate(Constants.gatePriceAlert)
default:
enabled = true
}
// Passing in empty notification content will skip the notif
OneSignalExtension.didReceiveNotificationExtensionRequest(
request,
with: enabled ? bestAttemptContent : UNMutableNotificationContent(),
withContentHandler: contentHandler)
}
if (!Statsig.isInitialized()) {
// The real sdk key is needed on iOS even though it's substituted in proxy
// Because the key is used to hash the feature gate names and wouldn't work properly otherwise
let statsigSdkKey = Bundle.main.object(forInfoDictionaryKey: "STATSIG_SDK_KEY") as? String ?? ""
let statsigUser = StatsigUser(
userID: UIDevice.current.identifierForVendor?.uuidString,
custom: [
"app": "mobile"
])
Statsig.initialize(
sdkKey: statsigSdkKey,
user: statsigUser,
options: StatsigOptions(
environment: StatsigEnvironment(tier: getStatsigEnvironemntTier()),
initializationURL: URL(string: "\(Constants.statsigProxyHost)/v1/statsig-proxy/initialize"),
eventLoggingURL: URL(string: "\(Constants.statsigProxyHost)/v1/statsig-proxy/rgstr")
)) { _errorMessage in
handleGatedNotification()
}
} else {
handleGatedNotification()
}
}
func getStatsigEnvironemntTier() -> String {
let bundleSuffix = Bundle.main.object(forInfoDictionaryKey: "BUNDLE_ID_SUFFIX") as? String
switch bundleSuffix {
case ".dev":
return "development"
case ".beta":
return "beta"
default:
return "production"
} }
}
}
struct Constants { override func serviceExtensionTimeWillExpire() {
static let statsigProxyHost = "https://gating.ios.wallet.gateway.uniswap.org" // Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
static let fieldNotificationType = "notification_type" if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
OneSignalExtension.serviceExtensionTimeWillExpireRequest(self.receivedRequest, with: self.bestAttemptContent)
static let typeUnfundedWallet = "unfunded_wallet_reminder" contentHandler(bestAttemptContent)
static let typePriceAlert = "price_alert" }
}
static let gateUnfundedWallet = "notification_unfunded_wallet"
static let gatePriceAlert = "notification_price_alert"
} }
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
<dict> <dict>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.com.uniswap.mobile.onesignal</string> <string>group.com.$(PRODUCT_NAME).onesignal</string>
</array> </array>
</dict> </dict>
</plist> </plist>
...@@ -57,8 +57,6 @@ end ...@@ -57,8 +57,6 @@ end
target 'OneSignalNotificationServiceExtension' do target 'OneSignalNotificationServiceExtension' do
use_frameworks! :linkage => :static use_frameworks! :linkage => :static
pod 'OneSignalXCFramework', '3.12.6' pod 'OneSignalXCFramework', '3.12.6'
pod 'Statsig', '1.49.0'
end end
def prepare_target_commons def prepare_target_commons
......
...@@ -1260,7 +1260,7 @@ PODS: ...@@ -1260,7 +1260,7 @@ PODS:
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0)
- FirebaseAppCheckInterop (11.6.0) - FirebaseAppCheckInterop (11.4.0)
- FirebaseAuth (11.2.0): - FirebaseAuth (11.2.0):
- FirebaseAppCheckInterop (~> 11.0) - FirebaseAppCheckInterop (~> 11.0)
- FirebaseAuthInterop (~> 11.0) - FirebaseAuthInterop (~> 11.0)
...@@ -1270,14 +1270,14 @@ PODS: ...@@ -1270,14 +1270,14 @@ PODS:
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GTMSessionFetcher/Core (~> 3.4) - GTMSessionFetcher/Core (~> 3.4)
- RecaptchaInterop (~> 100.0) - RecaptchaInterop (~> 100.0)
- FirebaseAuthInterop (11.6.0) - FirebaseAuthInterop (11.4.0)
- FirebaseCore (11.2.0): - FirebaseCore (11.2.0):
- FirebaseCoreInternal (~> 11.0) - FirebaseCoreInternal (~> 11.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0) - GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreExtension (11.4.1): - FirebaseCoreExtension (11.4.1):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- FirebaseCoreInternal (11.6.0): - FirebaseCoreInternal (11.4.2):
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseFirestore (11.2.0): - FirebaseFirestore (11.2.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
...@@ -1299,7 +1299,7 @@ PODS: ...@@ -1299,7 +1299,7 @@ PODS:
- gRPC-Core (~> 1.65.0) - gRPC-Core (~> 1.65.0)
- leveldb-library (~> 1.22) - leveldb-library (~> 1.22)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseSharedSwift (11.6.0) - FirebaseSharedSwift (11.4.0)
- fmt (6.2.1) - fmt (6.2.1)
- glog (0.3.5) - glog (0.3.5)
- GoogleUtilities/AppDelegateSwizzler (8.0.2): - GoogleUtilities/AppDelegateSwizzler (8.0.2):
...@@ -1434,9 +1434,9 @@ PODS: ...@@ -1434,9 +1434,9 @@ PODS:
- libwebp/sharpyuv (1.3.2) - libwebp/sharpyuv (1.3.2)
- libwebp/webp (1.3.2): - libwebp/webp (1.3.2):
- libwebp/sharpyuv - libwebp/sharpyuv
- MMKV (2.0.0): - MMKV (1.3.4):
- MMKVCore (~> 2.0.0) - MMKVCore (~> 1.3.4)
- MMKVCore (2.0.0) - MMKVCore (1.3.4)
- nanopb (3.30910.0): - nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0) - nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0) - nanopb/encode (= 3.30910.0)
...@@ -2337,9 +2337,7 @@ PODS: ...@@ -2337,9 +2337,7 @@ PODS:
- React - React
- react-native-get-random-values (1.8.0): - react-native-get-random-values (1.8.0):
- React-Core - React-Core
- react-native-image-picker (7.2.3): - react-native-image-picker (7.0.1):
- glog
- RCT-Folly (= 2022.05.16.00)
- React-Core - React-Core
- react-native-mmkv (2.10.1): - react-native-mmkv (2.10.1):
- MMKV (>= 1.2.13) - MMKV (>= 1.2.13)
...@@ -2612,7 +2610,6 @@ PODS: ...@@ -2612,7 +2610,6 @@ PODS:
- SocketRocket (0.6.1) - SocketRocket (0.6.1)
- sparkfabrik-react-native-idfa-aaid (1.2.0): - sparkfabrik-react-native-idfa-aaid (1.2.0):
- React - React
- Statsig (1.49.0)
- UIImageColors (2.1.0) - UIImageColors (2.1.0)
- Yoga (1.14.0) - Yoga (1.14.0)
- ZXingObjC/Core (3.6.9) - ZXingObjC/Core (3.6.9)
...@@ -2730,7 +2727,6 @@ DEPENDENCIES: ...@@ -2730,7 +2727,6 @@ DEPENDENCIES:
- RNScreens (from `../../../node_modules/react-native-screens`) - RNScreens (from `../../../node_modules/react-native-screens`)
- RNSVG (from `../../../node_modules/react-native-svg`) - RNSVG (from `../../../node_modules/react-native-svg`)
- "sparkfabrik-react-native-idfa-aaid (from `../../../node_modules/@sparkfabrik/react-native-idfa-aaid`)" - "sparkfabrik-react-native-idfa-aaid (from `../../../node_modules/@sparkfabrik/react-native-idfa-aaid`)"
- Statsig (= 1.49.0)
- UIImageColors (= 2.1.0) - UIImageColors (= 2.1.0)
- Yoga (from `../../../node_modules/react-native/ReactCommon/yoga`) - Yoga (from `../../../node_modules/react-native/ReactCommon/yoga`)
...@@ -2779,7 +2775,6 @@ SPEC REPOS: ...@@ -2779,7 +2775,6 @@ SPEC REPOS:
- SDWebImage - SDWebImage
- SDWebImageWebPCoder - SDWebImageWebPCoder
- SocketRocket - SocketRocket
- Statsig
- UIImageColors - UIImageColors
- ZXingObjC - ZXingObjC
...@@ -3032,15 +3027,15 @@ SPEC CHECKSUMS: ...@@ -3032,15 +3027,15 @@ SPEC CHECKSUMS:
FBReactNativeSpec: 91c0784dbf98ed9c434927ea46f41b780fe3a232 FBReactNativeSpec: 91c0784dbf98ed9c434927ea46f41b780fe3a232
Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c
FirebaseAppCheck: a6a1c1ca169d795212b9e70b5cfb880083a28e7c FirebaseAppCheck: a6a1c1ca169d795212b9e70b5cfb880083a28e7c
FirebaseAppCheckInterop: 347aa09a805219a31249b58fc956888e9fcb314b FirebaseAppCheckInterop: 1b9643ae2f1ee214488caa2f8e32b7bc2f0f3735
FirebaseAuth: 2a198b8cdbbbd457f08d74df7040feb0a0e7777a FirebaseAuth: 2a198b8cdbbbd457f08d74df7040feb0a0e7777a
FirebaseAuthInterop: a919d415797d23b7bfe195a04f322b86c65020ef FirebaseAuthInterop: 9ac948965ac13ec9d8a080f39490ddb2bda30520
FirebaseCore: a282032ae9295c795714ded2ec9c522fc237f8da FirebaseCore: a282032ae9295c795714ded2ec9c522fc237f8da
FirebaseCoreExtension: f1bc67a4702931a7caa097d8e4ac0a1b0d16720e FirebaseCoreExtension: f1bc67a4702931a7caa097d8e4ac0a1b0d16720e
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 FirebaseCoreInternal: 35731192cab10797b88411be84940d2beb33a238
FirebaseFirestore: 62708adbc1dfcd6d165a7c0a202067b441912dc9 FirebaseFirestore: 62708adbc1dfcd6d165a7c0a202067b441912dc9
FirebaseFirestoreInternal: ad9b9ee2d3d430c8f31333a69b3b6737a7206232 FirebaseFirestoreInternal: ad9b9ee2d3d430c8f31333a69b3b6737a7206232
FirebaseSharedSwift: a4e5dfca3e210633bb3a3dfb94176c019211948b FirebaseSharedSwift: 505dae2d05969dbf6d43749a642bb1bf230f0252
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
...@@ -3051,8 +3046,8 @@ SPEC CHECKSUMS: ...@@ -3051,8 +3046,8 @@ SPEC CHECKSUMS:
leveldb-library: cc8b8f8e013647a295ad3f8cd2ddf49a6f19be19 leveldb-library: cc8b8f8e013647a295ad3f8cd2ddf49a6f19be19
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
MMKV: f7d1d5945c8765f97f39c3d121f353d46735d801 MMKV: ed58ad794b3f88c24d604a5b74f3fba17fcbaf74
MMKVCore: c04b296010fcb1d1638f2c69405096aac12f6390 MMKVCore: a67a1cede26175c413176f404a7cedec43f96a0b
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
OneSignalXCFramework: ff1c970b7aeb4ac0fe48fb35393eb5d8bf378135 OneSignalXCFramework: ff1c970b7aeb4ac0fe48fb35393eb5d8bf378135
OpenTelemetrySwiftApi: 657da8071c2908caecce11548e006f779924ff9c OpenTelemetrySwiftApi: 657da8071c2908caecce11548e006f779924ff9c
...@@ -3083,7 +3078,7 @@ SPEC CHECKSUMS: ...@@ -3083,7 +3078,7 @@ SPEC CHECKSUMS:
react-native-compat: 100540c3cebb076da442cf058e375e8ca895ae28 react-native-compat: 100540c3cebb076da442cf058e375e8ca895ae28
react-native-context-menu-view: dcec18eb8882e20596dbb75802e7d19cb87dac02 react-native-context-menu-view: dcec18eb8882e20596dbb75802e7d19cb87dac02
react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a
react-native-image-picker: b049e0ea9d6b1b58c06262e19f8b66c87ac7b760 react-native-image-picker: 1569cfade34b3a011191ce262423e6ce2f8db5a1
react-native-mmkv: dea675cf9697ad35940f1687e98e133e1358ef9f react-native-mmkv: dea675cf9697ad35940f1687e98e133e1358ef9f
react-native-netinfo: 129bd99f607a2dc5bb096168f3e5c150fd1f1c95 react-native-netinfo: 129bd99f607a2dc5bb096168f3e5c150fd1f1c95
react-native-onesignal: ab800900cffeca4d9db70a05244013fc8a36ceb8 react-native-onesignal: ab800900cffeca4d9db70a05244013fc8a36ceb8
...@@ -3138,11 +3133,10 @@ SPEC CHECKSUMS: ...@@ -3138,11 +3133,10 @@ SPEC CHECKSUMS:
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
sparkfabrik-react-native-idfa-aaid: 1b72a6264a2175473e309ffa6434db87c58af264 sparkfabrik-react-native-idfa-aaid: 1b72a6264a2175473e309ffa6434db87c58af264
Statsig: 970abcd107e8e64bb68f6b8504a94c39d7f9e318
UIImageColors: d2ef3b0877d203cbb06489eeb78ea8b7788caabe UIImageColors: d2ef3b0877d203cbb06489eeb78ea8b7788caabe
Yoga: 805bf71192903b20fc14babe48080582fee65a80 Yoga: 805bf71192903b20fc14babe48080582fee65a80
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5 ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
PODFILE CHECKSUM: 18445ed90dc0fd39adbfc715d3fbf6316e3013ad PODFILE CHECKSUM: 525fd4a1c78879023ae05970b18e66b654c4c07a
COCOAPODS: 1.14.3 COCOAPODS: 1.14.3
...@@ -22,7 +22,6 @@ ...@@ -22,7 +22,6 @@
<string>fr</string> <string>fr</string>
<string>ja</string> <string>ja</string>
<string>pt</string> <string>pt</string>
<string>vi</string>
<string>es-ES</string> <string>es-ES</string>
<string>es-US</string> <string>es-US</string>
<string>es-419</string> <string>es-419</string>
...@@ -63,7 +62,7 @@ ...@@ -63,7 +62,7 @@
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>LSApplicationCategoryType</key> <key>LSApplicationCategoryType</key>
<string></string> <string/>
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>
<array> <array>
<string>itms-apps</string> <string>itms-apps</string>
...@@ -86,6 +85,8 @@ ...@@ -86,6 +85,8 @@
</dict> </dict>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) Wallet needs access to your Camera to scan QR codes</string> <string>$(PRODUCT_NAME) Wallet needs access to your Camera to scan QR codes</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>$(PRODUCT_NAME) Wallet needs access to your Camera Roll to choose an avatar for your username</string>
<key>NSFaceIDUsageDescription</key> <key>NSFaceIDUsageDescription</key>
<string>Enabling Face ID helps $(PRODUCT_NAME) Wallet keep your assets secure.</string> <string>Enabling Face ID helps $(PRODUCT_NAME) Wallet keep your assets secure.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key> <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
...@@ -94,8 +95,6 @@ ...@@ -94,8 +95,6 @@
<string>$(PRODUCT_NAME) Wallet does not require access to your location.</string> <string>$(PRODUCT_NAME) Wallet does not require access to your location.</string>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) Wallet does not require access to the microphone.</string> <string>$(PRODUCT_NAME) Wallet does not require access to the microphone.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>$(PRODUCT_NAME) Wallet needs access to your Camera Roll to choose an avatar for your username</string>
<key>NSUbiquitousContainers</key> <key>NSUbiquitousContainers</key>
<dict> <dict>
<key>iCloud.Uniswap</key> <key>iCloud.Uniswap</key>
...@@ -112,8 +111,6 @@ ...@@ -112,8 +111,6 @@
<array> <array>
<string>TokenPriceConfigurationIntent</string> <string>TokenPriceConfigurationIntent</string>
</array> </array>
<key>OneSignal_app_groups_key</key>
<string>group.com.uniswap.mobile.onesignal</string>
<key>OneSignal_suppress_launch_urls</key> <key>OneSignal_suppress_launch_urls</key>
<true/> <true/>
<key>UIAppFonts</key> <key>UIAppFonts</key>
......
...@@ -27,8 +27,8 @@ ...@@ -27,8 +27,8 @@
</array> </array>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.com.$(PRODUCT_NAME).onesignal</string>
<string>group.com.uniswap.widgets</string> <string>group.com.uniswap.widgets</string>
<string>group.com.uniswap.mobile.onesignal</string>
</array> </array>
</dict> </dict>
</plist> </plist>
...@@ -17,6 +17,7 @@ const workspaceRoot = path.resolve(mobileRoot, '../..') ...@@ -17,6 +17,7 @@ const workspaceRoot = path.resolve(mobileRoot, '../..')
const watchFolders = [mobileRoot, `${workspaceRoot}/node_modules`, `${workspaceRoot}/packages`] const watchFolders = [mobileRoot, `${workspaceRoot}/node_modules`, `${workspaceRoot}/packages`]
const detoxExtensions = process.env.DETOX_MODE === 'mocked' ? ['mock.tsx', 'mock.ts'] : []
const defaultConfig = getDefaultConfig(__dirname) const defaultConfig = getDefaultConfig(__dirname)
...@@ -28,7 +29,8 @@ const config = { ...@@ -28,7 +29,8 @@ const config = {
resolver: { resolver: {
nodeModulesPaths: [`${workspaceRoot}/node_modules`], nodeModulesPaths: [`${workspaceRoot}/node_modules`],
assetExts: assetExts.filter((ext) => ext !== 'svg'), assetExts: assetExts.filter((ext) => ext !== 'svg'),
sourceExts: [...sourceExts, 'svg', 'cjs'], // detox mocking works properly only being spreaded at the beginning of sourceExts array
sourceExts: [...detoxExtensions, ...sourceExts, 'svg', 'cjs'],
}, },
transformer: { transformer: {
getTransformOptions: async () => ({ getTransformOptions: async () => ({
...@@ -48,14 +50,11 @@ const config = { ...@@ -48,14 +50,11 @@ const config = {
watchFolders, watchFolders,
} }
const IS_STORYBOOK_ENABLED = process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test'
// Checkout more useful options in the docs: https://github.com/storybookjs/react-native?tab=readme-ov-file#options // Checkout more useful options in the docs: https://github.com/storybookjs/react-native?tab=readme-ov-file#options
module.exports = withStorybook(mergeConfig(defaultConfig, config), { module.exports = withStorybook(mergeConfig(defaultConfig, config), {
// Set to false to remove storybook specific options // Set to false to remove storybook specific options
// you can also use a env variable to set this // you can also use a env variable to set this
enabled: IS_STORYBOOK_ENABLED, enabled: process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test',
onDisabledRemoveStorybook: true,
// Path to your storybook config // Path to your storybook config
configPath: path.resolve(__dirname, './.storybook'), configPath: path.resolve(__dirname, './.storybook'),
}) })
...@@ -23,6 +23,14 @@ ...@@ -23,6 +23,14 @@
"env:local:upload": "bash ../../scripts/uploadEnvLocal.sh xmznnx7ozuojy5lnohcmt73aee ../../.env.defaults.local", "env:local:upload": "bash ../../scripts/uploadEnvLocal.sh xmznnx7ozuojy5lnohcmt73aee ../../.env.defaults.local",
"env:local:copy:swift": "python3 scripts/copy_env_vars_to_swift.py", "env:local:copy:swift": "python3 scripts/copy_env_vars_to_swift.py",
"e2e": "maestro test \".maestro/flows/$*\"", "e2e": "maestro test \".maestro/flows/$*\"",
"e2e:packager": "DETOX_MODE=mocked yarn start",
"e2e:android:build:debug": "DETOX_MODE=mocked detox build -c android.emu.debug",
"e2e:android:test:debug": "detox test -c android.emu.debug",
"e2e:android:build:release": "DETOX_MODE=mocked detox build -c android.emu.release",
"e2e:android:test:release": "DETOX_MODE=mocked detox test -c android.emu.release --cleanup --headless --record-logs all",
"e2e:ios:build:debug": "DETOX_MODE=mocked detox build -c ios.sim.debug",
"e2e:ios:test:debug": "detox test -c ios.sim.debug",
"e2e:ios:test:release": "DETOX_MODE=mocked detox build -c ios.sim.release && detox test -c ios.sim.release --cleanup --headless --record-logs all",
"firestore:deploy:rules": "firebase deploy --only firestore:rules", "firestore:deploy:rules": "firebase deploy --only firestore:rules",
"link:assets": "react-native-asset", "link:assets": "react-native-asset",
"graphql:generate:swift": "cd ios && ./Pods/Apollo/apollo-ios-cli generate", "graphql:generate:swift": "cd ios && ./Pods/Apollo/apollo-ios-cli generate",
...@@ -83,9 +91,9 @@ ...@@ -83,9 +91,9 @@
"@tanstack/react-query": "5.51.16", "@tanstack/react-query": "5.51.16",
"@uniswap/analytics": "1.7.0", "@uniswap/analytics": "1.7.0",
"@uniswap/analytics-events": "2.40.0", "@uniswap/analytics-events": "2.40.0",
"@uniswap/client-explore": "0.0.14", "@uniswap/client-explore": "0.0.12",
"@uniswap/ethers-rs-mobile": "0.0.5", "@uniswap/ethers-rs-mobile": "0.0.5",
"@uniswap/sdk-core": "7.1.0", "@uniswap/sdk-core": "6.1.0",
"@walletconnect/core": "2.17.1", "@walletconnect/core": "2.17.1",
"@walletconnect/react-native-compat": "2.17.1", "@walletconnect/react-native-compat": "2.17.1",
"@walletconnect/utils": "2.17.1", "@walletconnect/utils": "2.17.1",
...@@ -124,7 +132,7 @@ ...@@ -124,7 +132,7 @@
"react-native-gesture-handler": "2.19.0", "react-native-gesture-handler": "2.19.0",
"react-native-get-random-values": "1.8.0", "react-native-get-random-values": "1.8.0",
"react-native-image-colors": "1.5.2", "react-native-image-colors": "1.5.2",
"react-native-image-picker": "7.2.3", "react-native-image-picker": "7.0.1",
"react-native-localize": "2.2.6", "react-native-localize": "2.2.6",
"react-native-markdown-display": "7.0.0-alpha.2", "react-native-markdown-display": "7.0.0-alpha.2",
"react-native-mmkv": "2.10.1", "react-native-mmkv": "2.10.1",
...@@ -174,6 +182,7 @@ ...@@ -174,6 +182,7 @@
"babel-plugin-module-resolver": "5.0.0", "babel-plugin-module-resolver": "5.0.0",
"babel-plugin-react-native-web": "0.17.5", "babel-plugin-react-native-web": "0.17.5",
"core-js": "2.6.12", "core-js": "2.6.12",
"detox": "20.23.0",
"eslint": "8.44.0", "eslint": "8.44.0",
"expo-modules-core": "1.11.13", "expo-modules-core": "1.11.13",
"jest": "29.7.0", "jest": "29.7.0",
......
...@@ -15,7 +15,7 @@ import { SafeAreaProvider } from 'react-native-safe-area-context' ...@@ -15,7 +15,7 @@ import { SafeAreaProvider } from 'react-native-safe-area-context'
import { enableFreeze } from 'react-native-screens' import { enableFreeze } from 'react-native-screens'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
import { DatadogProviderWrapper, MOBILE_DEFAULT_DATADOG_SESSION_SAMPLE_RATE } from 'src/app/DatadogProviderWrapper' import { DatadogProviderWrapper } from 'src/app/DatadogProviderWrapper'
import { MobileWalletNavigationProvider } from 'src/app/MobileWalletNavigationProvider' import { MobileWalletNavigationProvider } from 'src/app/MobileWalletNavigationProvider'
import { AppModals } from 'src/app/modals/AppModals' import { AppModals } from 'src/app/modals/AppModals'
import { NavigationContainer } from 'src/app/navigation/NavigationContainer' import { NavigationContainer } from 'src/app/navigation/NavigationContainer'
...@@ -47,15 +47,10 @@ import { uniswapUrls } from 'uniswap/src/constants/urls' ...@@ -47,15 +47,10 @@ import { uniswapUrls } from 'uniswap/src/constants/urls'
import { BlankUrlProvider } from 'uniswap/src/contexts/UrlContext' import { BlankUrlProvider } from 'uniswap/src/contexts/UrlContext'
import { selectFavoriteTokens } from 'uniswap/src/features/favorites/selectors' import { selectFavoriteTokens } from 'uniswap/src/features/favorites/selectors'
import { useAppFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks' import { useAppFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks'
import {
DatadogSessionSampleRateKey,
DatadogSessionSampleRateValType,
DynamicConfigs,
} from 'uniswap/src/features/gating/configs'
import { DUMMY_STATSIG_SDK_KEY, StatsigCustomAppValue } from 'uniswap/src/features/gating/constants' import { DUMMY_STATSIG_SDK_KEY, StatsigCustomAppValue } from 'uniswap/src/features/gating/constants'
import { Experiments } from 'uniswap/src/features/gating/experiments' import { Experiments } from 'uniswap/src/features/gating/experiments'
import { FeatureFlags, WALLET_FEATURE_FLAG_NAMES } from 'uniswap/src/features/gating/flags' import { FeatureFlags, WALLET_FEATURE_FLAG_NAMES } from 'uniswap/src/features/gating/flags'
import { getDynamicConfigValue, useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { loadStatsigOverrides } from 'uniswap/src/features/gating/overrides/customPersistedOverrides' import { loadStatsigOverrides } from 'uniswap/src/features/gating/overrides/customPersistedOverrides'
import { Statsig, StatsigOptions, StatsigProvider, StatsigUser } from 'uniswap/src/features/gating/sdk/statsig' import { Statsig, StatsigOptions, StatsigProvider, StatsigUser } from 'uniswap/src/features/gating/sdk/statsig'
import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext' import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext'
...@@ -69,10 +64,9 @@ import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/conte ...@@ -69,10 +64,9 @@ import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/conte
import i18n from 'uniswap/src/i18n' import i18n from 'uniswap/src/i18n'
import { CurrencyId } from 'uniswap/src/types/currency' import { CurrencyId } from 'uniswap/src/types/currency'
import { getUniqueId } from 'utilities/src/device/getUniqueId' import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { datadogEnabled, isE2EMode } from 'utilities/src/environment/constants' import { datadogEnabled, isDetoxBuild } from 'utilities/src/environment/constants'
import { attachUnhandledRejectionHandler, setAttributesToDatadog } from 'utilities/src/logger/Datadog' import { attachUnhandledRejectionHandler, setAttributesToDatadog } from 'utilities/src/logger/Datadog'
import { registerConsoleOverrides } from 'utilities/src/logger/console' import { registerConsoleOverrides } from 'utilities/src/logger/console'
import { DDRumAction, DDRumTiming } from 'utilities/src/logger/datadogEvents'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { useAsyncData } from 'utilities/src/react/hooks' import { useAsyncData } from 'utilities/src/react/hooks'
import { AnalyticsNavigationContextProvider } from 'utilities/src/telemetry/trace/AnalyticsNavigationContext' import { AnalyticsNavigationContextProvider } from 'utilities/src/telemetry/trace/AnalyticsNavigationContext'
...@@ -98,9 +92,9 @@ if (__DEV__) { ...@@ -98,9 +92,9 @@ if (__DEV__) {
loadErrorMessages() loadErrorMessages()
} }
// Log boxes on simulators can block e2e tap event when they cover buttons placed at // Log boxes on simulators can block detox tap event when they cover buttons placed at
// the bottom of the screen and cause tests to fail. // the bottom of the screen and cause tests to fail.
if (isE2EMode) { if (isDetoxBuild) {
LogBox.ignoreAllLogs() LogBox.ignoreAllLogs()
} }
...@@ -110,7 +104,7 @@ initFirebaseAppCheck() ...@@ -110,7 +104,7 @@ initFirebaseAppCheck()
function App(): JSX.Element | null { function App(): JSX.Element | null {
useEffect(() => { useEffect(() => {
if (!__DEV__ && !isE2EMode) { if (!__DEV__ && !isDetoxBuild) {
attachUnhandledRejectionHandler() attachUnhandledRejectionHandler()
setAttributesToDatadog({ buildNumber: DeviceInfo.getBuildNumber() }).catch(() => undefined) setAttributesToDatadog({ buildNumber: DeviceInfo.getBuildNumber() }).catch(() => undefined)
} }
...@@ -127,8 +121,6 @@ function App(): JSX.Element | null { ...@@ -127,8 +121,6 @@ function App(): JSX.Element | null {
const deviceId = useAsyncData(fetchAndSetDeviceId).data const deviceId = useAsyncData(fetchAndSetDeviceId).data
const [datadogSessionSampleRate, setDatadogSessionSampleRate] = React.useState<number | undefined>(undefined)
const statSigOptions: { const statSigOptions: {
user: StatsigUser user: StatsigUser
options: StatsigOptions options: StatsigOptions
...@@ -142,22 +134,7 @@ function App(): JSX.Element | null { ...@@ -142,22 +134,7 @@ function App(): JSX.Element | null {
api: uniswapUrls.statsigProxyUrl, api: uniswapUrls.statsigProxyUrl,
disableAutoMetricsLogging: true, disableAutoMetricsLogging: true,
disableErrorLogging: true, disableErrorLogging: true,
initCompletionCallback: () => { initCompletionCallback: loadStatsigOverrides,
loadStatsigOverrides()
// we should move this logic inside DatadogProviderWrapper once we migrate to @statsig/js-client
// https://docs.statsig.com/client/javascript-sdk/migrating-from-statsig-js/#initcompletioncallback
setDatadogSessionSampleRate(
getDynamicConfigValue<
DynamicConfigs.DatadogSessionSampleRate,
DatadogSessionSampleRateKey,
DatadogSessionSampleRateValType
>(
DynamicConfigs.DatadogSessionSampleRate,
DatadogSessionSampleRateKey.Rate,
MOBILE_DEFAULT_DATADOG_SESSION_SAMPLE_RATE,
),
)
},
}, },
sdkKey: DUMMY_STATSIG_SDK_KEY, sdkKey: DUMMY_STATSIG_SDK_KEY,
user: { user: {
...@@ -171,7 +148,7 @@ function App(): JSX.Element | null { ...@@ -171,7 +148,7 @@ function App(): JSX.Element | null {
return ( return (
<StatsigProvider {...statSigOptions}> <StatsigProvider {...statSigOptions}>
<DatadogProviderWrapper sessionSampleRate={datadogSessionSampleRate}> <DatadogProviderWrapper>
<Trace> <Trace>
<StrictMode> <StrictMode>
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
...@@ -206,11 +183,6 @@ function AppOuter(): JSX.Element | null { ...@@ -206,11 +183,6 @@ function AppOuter(): JSX.Element | null {
}) })
const jsBundleLoadedRef = useRef(false) const jsBundleLoadedRef = useRef(false)
useEffect(() => {
// Dynamically load polyfills so that we save on bundle size and improve app startup time
import('src/polyfills/intl-delayed')
}, [])
/** /**
* Function called by the @shopify/react-native-performance PerformanceProfiler that returns a * Function called by the @shopify/react-native-performance PerformanceProfiler that returns a
* RenderPassReport. We then forward this report to Datadog, Amplitude, etc. * RenderPassReport. We then forward this report to Datadog, Amplitude, etc.
...@@ -219,13 +191,13 @@ function AppOuter(): JSX.Element | null { ...@@ -219,13 +191,13 @@ function AppOuter(): JSX.Element | null {
if (datadogEnabled) { if (datadogEnabled) {
const shouldLogJsBundleLoaded = report.timeToBootJsMillis && !jsBundleLoadedRef.current const shouldLogJsBundleLoaded = report.timeToBootJsMillis && !jsBundleLoadedRef.current
if (shouldLogJsBundleLoaded) { if (shouldLogJsBundleLoaded) {
await DdRum.addAction(RumActionType.CUSTOM, DDRumAction.ApplicationStartJs, { await DdRum.addAction(RumActionType.CUSTOM, 'application_start_js', {
loading_time: report.timeToBootJsMillis, loading_time: report.timeToBootJsMillis,
}) })
jsBundleLoadedRef.current = true jsBundleLoadedRef.current = true
} }
if (report.interactive) { if (report.interactive) {
await DdRum.addTiming(DDRumTiming.ScreenInteractive) await DdRum.addTiming('screenInteractive')
} }
} }
......
import { import {
BatchSize, BatchSize,
DatadogProvider, DatadogProvider,
DatadogProviderConfiguration,
DdRum, DdRum,
SdkVerbosity, SdkVerbosity,
TrackingConsent, TrackingConsent,
UploadFrequency, UploadFrequency,
} from '@datadog/mobile-react-native' } from '@datadog/mobile-react-native'
import { ErrorEventMapper } from '@datadog/mobile-react-native/lib/typescript/rum/eventMappers/errorEventMapper' import { ErrorEventMapper } from '@datadog/mobile-react-native/lib/typescript/rum/eventMappers/errorEventMapper'
import { PropsWithChildren, default as React, useEffect } from 'react' import { PropsWithChildren, default as React } from 'react'
import { getDatadogEnvironment } from 'src/utils/version' import { getDatadogEnvironment } from 'src/utils/version'
import { config } from 'uniswap/src/config' import { config } from 'uniswap/src/config'
import { import {
...@@ -16,85 +17,64 @@ import { ...@@ -16,85 +17,64 @@ import {
DynamicConfigs, DynamicConfigs,
} from 'uniswap/src/features/gating/configs' } from 'uniswap/src/features/gating/configs'
import { getDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import { getDynamicConfigValue } from 'uniswap/src/features/gating/hooks'
import { datadogEnabled, isE2EMode, isJestRun, localDevDatadogEnabled } from 'utilities/src/environment/constants' import { datadogEnabled, isDetoxBuild, isJestRun, localDevDatadogEnabled } from 'utilities/src/environment/constants'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
// In case Statsig is not available export const SESSION_SAMPLE_RATE = 10 // percent
export const MOBILE_DEFAULT_DATADOG_SESSION_SAMPLE_RATE = 10 // percent
// Configuration for Datadog's automatic monitoring features: const datadogConfig = new DatadogProviderConfiguration(
// - Error tracking: Captures and reports application errors config.datadogClientToken,
// - User interactions: Monitors user events and actions getDatadogEnvironment(),
// - Resource tracking: Traces network requests and API calls config.datadogProjectId,
// Note: Can buffer up to 100 RUM events before SDK initialization datadogEnabled, // trackInteractions
// https://docs.datadoghq.com/real_user_monitoring/mobile_and_tv_monitoring/react_native/advanced_configuration/#delaying-the-initialization datadogEnabled, // trackResources
const datadogAutoInstrumentation = { datadogEnabled, // trackErrors
trackErrors: datadogEnabled, localDevDatadogEnabled ? TrackingConsent.GRANTED : undefined,
trackInteractions: datadogEnabled, )
trackResources: datadogEnabled,
}
async function initializeDatadog(sessionSamplingRate: number | undefined): Promise<void> { Object.assign(datadogConfig, {
const datadogConfig = { site: 'US1',
clientToken: config.datadogClientToken, longTaskThresholdMs: 100,
env: getDatadogEnvironment(), nativeCrashReportEnabled: true,
applicationId: config.datadogProjectId, verbosity: SdkVerbosity.INFO,
trackingConsent: undefined, errorEventMapper: (event: ReturnType<ErrorEventMapper>) => {
site: 'US1', const ignoredErrors = getDynamicConfigValue<
longTaskThresholdMs: 100, DynamicConfigs.DatadogIgnoredErrors,
nativeCrashReportEnabled: true, DatadogIgnoredErrorsConfigKey,
verbosity: SdkVerbosity.INFO, DatadogIgnoredErrorsValType
errorEventMapper: (event: ReturnType<ErrorEventMapper>): ReturnType<ErrorEventMapper> | null => { >(DynamicConfigs.DatadogIgnoredErrors, DatadogIgnoredErrorsConfigKey.Errors, [])
const ignoredErrors = getDynamicConfigValue<
DynamicConfigs.DatadogIgnoredErrors,
DatadogIgnoredErrorsConfigKey,
DatadogIgnoredErrorsValType
>(DynamicConfigs.DatadogIgnoredErrors, DatadogIgnoredErrorsConfigKey.Errors, [])
const ignoredError = ignoredErrors.find(({ messageContains }) => event?.message.includes(messageContains)) const ignoredError = ignoredErrors.find(({ messageContains }) => event?.message.includes(messageContains))
if (ignoredError) { if (ignoredError) {
return Math.random() < ignoredError.sampleRate ? event : null return Math.random() < ignoredError.sampleRate ? event : null
} }
return event return event
}, },
sessionSamplingRate, sessionSampleRate: SESSION_SAMPLE_RATE,
} })
if (localDevDatadogEnabled) { if (localDevDatadogEnabled) {
Object.assign(datadogConfig, { Object.assign(datadogConfig, {
sessionSamplingRate: 100, sessionSamplingRate: 100,
uploadFrequency: UploadFrequency.FREQUENT, resourceTracingSamplingRate: 100,
batchSize: BatchSize.SMALL, uploadFrequency: UploadFrequency.FREQUENT,
verbosity: SdkVerbosity.DEBUG, batchSize: BatchSize.SMALL,
trackingConsent: TrackingConsent.GRANTED, verbosity: SdkVerbosity.DEBUG,
}) })
}
await DatadogProvider.initialize(datadogConfig)
} }
/** /**
* Wrapper component to provide Datadog to the app with our mobile app's * Wrapper component to provide Datadog to the app with our mobile app's
* specific configuration. * specific configuration.
*/ */
export function DatadogProviderWrapper({ export function DatadogProviderWrapper({ children }: PropsWithChildren): JSX.Element {
children, if (isDetoxBuild || isJestRun) {
sessionSampleRate,
}: PropsWithChildren<{ sessionSampleRate: number | undefined }>): JSX.Element {
useEffect(() => {
if (datadogEnabled && sessionSampleRate !== undefined) {
initializeDatadog(sessionSampleRate).catch(() => undefined)
}
}, [sessionSampleRate])
if (isE2EMode || isJestRun) {
return <>{children}</> return <>{children}</>
} }
logger.setWalletDatadogEnabled(true)
return ( return (
<DatadogProvider <DatadogProvider
configuration={datadogAutoInstrumentation} configuration={datadogConfig}
onInitialization={async () => { onInitialization={async () => {
const sessionId = await DdRum.getCurrentSessionId() const sessionId = await DdRum.getCurrentSessionId()
// we do not want to log anything if session is not sampled // we do not want to log anything if session is not sampled
......
...@@ -4,7 +4,7 @@ import { useDispatch } from 'react-redux' ...@@ -4,7 +4,7 @@ import { useDispatch } from 'react-redux'
import { exploreNavigationRef } from 'src/app/navigation/navigation' import { exploreNavigationRef } from 'src/app/navigation/navigation'
import { useAppStackNavigation } from 'src/app/navigation/types' import { useAppStackNavigation } from 'src/app/navigation/types'
import { closeModal, openModal } from 'src/features/modals/modalSlice' import { closeModal, openModal } from 'src/features/modals/modalSlice'
import { HomeScreenTabIndex } from 'src/screens/HomeScreen/HomeScreenTabIndex' import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
......
...@@ -3,7 +3,7 @@ import { useCallback, useContext } from 'react' ...@@ -3,7 +3,7 @@ import { useCallback, useContext } from 'react'
import { BackHandler } from 'react-native' import { BackHandler } from 'react-native'
import { navigate as rootNavigate } from 'src/app/navigation/rootNavigation' import { navigate as rootNavigate } from 'src/app/navigation/rootNavigation'
import { useAppStackNavigation, useExploreStackNavigation } from 'src/app/navigation/types' import { useAppStackNavigation, useExploreStackNavigation } from 'src/app/navigation/types'
import { HomeScreenTabIndex } from 'src/screens/HomeScreen/HomeScreenTabIndex' import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex'
import { useTransactionListLazyQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { useTransactionListLazyQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens } from 'uniswap/src/types/screens/mobile'
......
...@@ -37,7 +37,7 @@ import { ExternalProfileScreen } from 'src/screens/ExternalProfileScreen' ...@@ -37,7 +37,7 @@ import { ExternalProfileScreen } from 'src/screens/ExternalProfileScreen'
import { FiatOnRampConnectingScreen } from 'src/screens/FiatOnRampConnecting' import { FiatOnRampConnectingScreen } from 'src/screens/FiatOnRampConnecting'
import { FiatOnRampScreen } from 'src/screens/FiatOnRampScreen' import { FiatOnRampScreen } from 'src/screens/FiatOnRampScreen'
import { FiatOnRampServiceProvidersScreen } from 'src/screens/FiatOnRampServiceProviders' import { FiatOnRampServiceProvidersScreen } from 'src/screens/FiatOnRampServiceProviders'
import { HomeScreen } from 'src/screens/HomeScreen/HomeScreen' import { HomeScreen } from 'src/screens/HomeScreen'
import { ImportMethodScreen } from 'src/screens/Import/ImportMethodScreen' import { ImportMethodScreen } from 'src/screens/Import/ImportMethodScreen'
import { OnDeviceRecoveryScreen } from 'src/screens/Import/OnDeviceRecoveryScreen' import { OnDeviceRecoveryScreen } from 'src/screens/Import/OnDeviceRecoveryScreen'
import { OnDeviceRecoveryViewSeedPhraseScreen } from 'src/screens/Import/OnDeviceRecoveryViewSeedPhraseScreen' import { OnDeviceRecoveryViewSeedPhraseScreen } from 'src/screens/Import/OnDeviceRecoveryViewSeedPhraseScreen'
...@@ -64,7 +64,6 @@ import { SettingsCloudBackupPasswordConfirmScreen } from 'src/screens/SettingsCl ...@@ -64,7 +64,6 @@ import { SettingsCloudBackupPasswordConfirmScreen } from 'src/screens/SettingsCl
import { SettingsCloudBackupPasswordCreateScreen } from 'src/screens/SettingsCloudBackupPasswordCreateScreen' import { SettingsCloudBackupPasswordCreateScreen } from 'src/screens/SettingsCloudBackupPasswordCreateScreen'
import { SettingsCloudBackupProcessingScreen } from 'src/screens/SettingsCloudBackupProcessingScreen' import { SettingsCloudBackupProcessingScreen } from 'src/screens/SettingsCloudBackupProcessingScreen'
import { SettingsCloudBackupStatus } from 'src/screens/SettingsCloudBackupStatus' import { SettingsCloudBackupStatus } from 'src/screens/SettingsCloudBackupStatus'
import { SettingsNotificationsScreen } from 'src/screens/SettingsNotificationsScreen'
import { SettingsPrivacyScreen } from 'src/screens/SettingsPrivacyScreen' import { SettingsPrivacyScreen } from 'src/screens/SettingsPrivacyScreen'
import { SettingsScreen } from 'src/screens/SettingsScreen' import { SettingsScreen } from 'src/screens/SettingsScreen'
import { SettingsViewSeedPhraseScreen } from 'src/screens/SettingsViewSeedPhraseScreen' import { SettingsViewSeedPhraseScreen } from 'src/screens/SettingsViewSeedPhraseScreen'
...@@ -134,7 +133,6 @@ function SettingsStackGroup(): JSX.Element { ...@@ -134,7 +133,6 @@ function SettingsStackGroup(): JSX.Element {
<SettingsStack.Screen component={SettingsCloudBackupStatus} name={MobileScreens.SettingsCloudBackupStatus} /> <SettingsStack.Screen component={SettingsCloudBackupStatus} name={MobileScreens.SettingsCloudBackupStatus} />
<SettingsStack.Screen component={SettingsAppearanceScreen} name={MobileScreens.SettingsAppearance} /> <SettingsStack.Screen component={SettingsAppearanceScreen} name={MobileScreens.SettingsAppearance} />
<SettingsStack.Screen component={SettingsPrivacyScreen} name={MobileScreens.SettingsPrivacy} /> <SettingsStack.Screen component={SettingsPrivacyScreen} name={MobileScreens.SettingsPrivacy} />
<SettingsStack.Screen component={SettingsNotificationsScreen} name={MobileScreens.SettingsNotifications} />
</SettingsStack.Navigator> </SettingsStack.Navigator>
) )
} }
......
...@@ -5,7 +5,7 @@ import { ...@@ -5,7 +5,7 @@ import {
useNavigation, useNavigation,
} from '@react-navigation/native' } from '@react-navigation/native'
import { NativeStackNavigationProp, NativeStackScreenProps } from '@react-navigation/native-stack' import { NativeStackNavigationProp, NativeStackScreenProps } from '@react-navigation/native-stack'
import { HomeScreenTabIndex } from 'src/screens/HomeScreen/HomeScreenTabIndex' import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { import {
FiatOnRampScreens, FiatOnRampScreens,
...@@ -62,7 +62,6 @@ export type SettingsStackParamList = { ...@@ -62,7 +62,6 @@ export type SettingsStackParamList = {
[MobileScreens.SettingsCloudBackupStatus]: { address: Address } [MobileScreens.SettingsCloudBackupStatus]: { address: Address }
[MobileScreens.SettingsHelpCenter]: undefined [MobileScreens.SettingsHelpCenter]: undefined
[MobileScreens.SettingsLanguage]: undefined [MobileScreens.SettingsLanguage]: undefined
[MobileScreens.SettingsNotifications]: undefined
[MobileScreens.SettingsPrivacy]: undefined [MobileScreens.SettingsPrivacy]: undefined
[MobileScreens.SettingsViewSeedPhrase]: { address: Address; walletNeedsRestore?: boolean } [MobileScreens.SettingsViewSeedPhrase]: { address: Address; walletNeedsRestore?: boolean }
[MobileScreens.SettingsWallet]: { address: Address } [MobileScreens.SettingsWallet]: { address: Address }
......
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import { RankingType } from 'uniswap/src/data/types'
import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants' import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants'
import { Language } from 'uniswap/src/features/language/constants' import { Language } from 'uniswap/src/features/language/constants'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice' import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice'
import { RankingType } from 'wallet/src/features/wallet/types'
// only add fields that are persisted // only add fields that are persisted
export const initialSchema = { export const initialSchema = {
......
...@@ -24,7 +24,7 @@ import Trace from 'uniswap/src/features/telemetry/Trace' ...@@ -24,7 +24,7 @@ import Trace from 'uniswap/src/features/telemetry/Trace'
import { ElementNameType } from 'uniswap/src/features/telemetry/constants' import { ElementNameType } from 'uniswap/src/features/telemetry/constants'
import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { CurrencyId } from 'uniswap/src/types/currency' import { CurrencyId } from 'uniswap/src/types/currency'
import { isE2EMode } from 'utilities/src/environment/constants' import { isDetoxBuild } from 'utilities/src/environment/constants'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { isAndroid } from 'utilities/src/platform' import { isAndroid } from 'utilities/src/platform'
...@@ -126,7 +126,7 @@ export const PriceExplorerInner = memo(function _PriceExplorerInner(): JSX.Eleme ...@@ -126,7 +126,7 @@ export const PriceExplorerInner = memo(function _PriceExplorerInner(): JSX.Eleme
const { convertFiatAmount } = useLocalizationContext() const { convertFiatAmount } = useLocalizationContext()
const conversionRate = convertFiatAmount(1).amount const conversionRate = convertFiatAmount(1).amount
const shouldShowAnimatedDot = const shouldShowAnimatedDot =
(selectedDuration === HistoryDuration.Day || selectedDuration === HistoryDuration.Hour) && !isE2EMode (selectedDuration === HistoryDuration.Day || selectedDuration === HistoryDuration.Hour) && !isDetoxBuild
const additionalPadding = shouldShowAnimatedDot ? 40 : 0 const additionalPadding = shouldShowAnimatedDot ? 40 : 0
const { lastPricePoint, convertedPriceHistory } = useMemo(() => { const { lastPricePoint, convertedPriceHistory } = useMemo(() => {
......
query TokenPriceHistory( query TokenPriceHistory(
$contract: ContractInput! $contract: ContractInput!
$duration: HistoryDuration = DAY $duration: HistoryDuration = DAY
$maxHistoryLength: Int = 1000
) { ) {
tokenProjects(contracts: [$contract]) { tokenProjects(contracts: [$contract]) {
id id
...@@ -14,7 +13,7 @@ query TokenPriceHistory( ...@@ -14,7 +13,7 @@ query TokenPriceHistory(
pricePercentChange24h { pricePercentChange24h {
value value
} }
priceHistory(duration: $duration, maxLength: $maxHistoryLength) { priceHistory(duration: $duration) {
timestamp timestamp
value value
} }
...@@ -33,7 +32,7 @@ query TokenPriceHistory( ...@@ -33,7 +32,7 @@ query TokenPriceHistory(
pricePercentChange24h: pricePercentChange(duration: DAY) { pricePercentChange24h: pricePercentChange(duration: DAY) {
value value
} }
priceHistory(duration: $duration, maxLength: $maxHistoryLength) { priceHistory(duration: $duration) {
timestamp timestamp
value value
} }
......
...@@ -14,7 +14,8 @@ export const TIME_RANGES = [ ...@@ -14,7 +14,8 @@ export const TIME_RANGES = [
[HistoryDuration.Week, i18n.t('token.priceExplorer.timeRangeLabel.week'), ElementName.TimeFrame1W], [HistoryDuration.Week, i18n.t('token.priceExplorer.timeRangeLabel.week'), ElementName.TimeFrame1W],
[HistoryDuration.Month, i18n.t('token.priceExplorer.timeRangeLabel.month'), ElementName.TimeFrame1M], [HistoryDuration.Month, i18n.t('token.priceExplorer.timeRangeLabel.month'), ElementName.TimeFrame1M],
[HistoryDuration.Year, i18n.t('token.priceExplorer.timeRangeLabel.year'), ElementName.TimeFrame1Y], [HistoryDuration.Year, i18n.t('token.priceExplorer.timeRangeLabel.year'), ElementName.TimeFrame1Y],
[HistoryDuration.Max, i18n.t('common.all'), ElementName.TimeFrameAll], // TODO (MOB-3585): fix performance issue with All time range and re-enable
// [HistoryDuration.Max, i18n.t('token.priceExplorer.timeRangeLabel.all'), ElementName.TimeFrameAll],
] as const ] as const
export const NUM_GRAPHS = TIME_RANGES.length export const NUM_GRAPHS = TIME_RANGES.length
import { BigNumber } from 'ethers' import { BigNumber } from 'ethers'
import React, { PropsWithChildren } from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ScrollView } from 'react-native-gesture-handler' import { ScrollView } from 'react-native-gesture-handler'
import { LinkButton } from 'src/components/buttons/LinkButton' import { LinkButton } from 'src/components/buttons/LinkButton'
...@@ -9,7 +9,7 @@ import { TextVariantTokens, iconSizes } from 'ui/src/theme' ...@@ -9,7 +9,7 @@ import { TextVariantTokens, iconSizes } from 'ui/src/theme'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { UniverseChainId } from 'uniswap/src/features/chains/types' import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { toSupportedChainId } from 'uniswap/src/features/chains/utils' import { toSupportedChainId } from 'uniswap/src/features/chains/utils'
import { useENSName } from 'uniswap/src/features/ens/api' import { useENS } from 'uniswap/src/features/ens/useENS'
import { EthMethod, EthTransaction } from 'uniswap/src/types/walletConnect' import { EthMethod, EthTransaction } from 'uniswap/src/types/walletConnect'
import { getValidAddress } from 'uniswap/src/utils/addresses' import { getValidAddress } from 'uniswap/src/utils/addresses'
import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking'
...@@ -38,7 +38,7 @@ type AddressButtonProps = { ...@@ -38,7 +38,7 @@ type AddressButtonProps = {
} }
const AddressButton = ({ address, chainId, ...rest }: AddressButtonProps): JSX.Element => { const AddressButton = ({ address, chainId, ...rest }: AddressButtonProps): JSX.Element => {
const { data: name } = useENSName(address) const { name } = useENS(chainId, address, false)
const colors = useSporeColors() const colors = useSporeColors()
const { defaultChainId } = useEnabledChains() const { defaultChainId } = useEnabledChains()
const supportedChainId = toSupportedChainId(chainId) ?? defaultChainId const supportedChainId = toSupportedChainId(chainId) ?? defaultChainId
...@@ -55,23 +55,6 @@ const AddressButton = ({ address, chainId, ...rest }: AddressButtonProps): JSX.E ...@@ -55,23 +55,6 @@ const AddressButton = ({ address, chainId, ...rest }: AddressButtonProps): JSX.E
) )
} }
type KeyValueRowProps = {
objKey: string
} & PropsWithChildren
const KeyValueRow = ({ objKey, children }: KeyValueRowProps): JSX.Element => {
return (
<Flex key={objKey} row alignItems="flex-start" gap="$spacing8">
<Text color="$neutral2" py="$spacing4" variant="body3">
{objKey}
</Text>
<Flex shrink gap="$spacing16" py="$spacing4">
{children}
</Flex>
</Flex>
)
}
const MAX_TYPED_DATA_PARSE_DEPTH = 3 const MAX_TYPED_DATA_PARSE_DEPTH = 3
// recursively parses typed data objects and adds margin to left // recursively parses typed data objects and adds margin to left
...@@ -81,31 +64,45 @@ const getParsedObjectDisplay = (chainId: number, obj: any, depth = 0): JSX.Eleme ...@@ -81,31 +64,45 @@ const getParsedObjectDisplay = (chainId: number, obj: any, depth = 0): JSX.Eleme
return <Text variant="body3">...</Text> return <Text variant="body3">...</Text>
} }
if (Array.isArray(obj) || obj === null || obj === undefined || typeof obj !== 'object') {
return <Text variant="body3">{Array.isArray(obj) ? JSON.stringify(obj) : String(obj)}</Text>
}
return ( return (
<Flex gap="$spacing4"> <Flex gap="$spacing4">
{Object.keys(obj).map((objKey) => { {Object.keys(obj).map((objKey) => {
const childValue = obj[objKey] const childValue = obj[objKey]
// Special case for address strings if (typeof childValue === 'object') {
if (typeof childValue === 'string' && getValidAddress(childValue, true)) { return (
<Flex key={objKey} gap="$spacing4">
<Text color="$neutral2" variant="body3">
{objKey}
</Text>
{getParsedObjectDisplay(chainId, childValue, depth + 1)}
</Flex>
)
}
if (typeof childValue === 'string') {
return ( return (
<KeyValueRow key={objKey} objKey={objKey}> <Flex key={objKey} row alignItems="flex-start" gap="$spacing8">
<Flex> <Text color="$neutral2" py="$spacing4" variant="body3">
<AddressButton address={childValue} chainId={chainId} textVariant="body3" /> {objKey}
</Text>
<Flex shrink gap="$spacing16">
{getValidAddress(childValue, true) ? (
<Flex py="$spacing4">
<AddressButton address={childValue} chainId={chainId} textVariant="body3" />
</Flex>
) : (
<Text py="$spacing4" variant="body3">
{childValue}
</Text>
)}
</Flex> </Flex>
</KeyValueRow> </Flex>
) )
} }
return ( // TODO: [MOB-216] handle array child types
<KeyValueRow key={objKey} objKey={objKey}> return null
{getParsedObjectDisplay(chainId, childValue, depth + 1)}
</KeyValueRow>
)
})} })}
</Flex> </Flex>
) )
......
import { useState } from 'react' import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useSettingsStackNavigation } from 'src/app/navigation/types' import { useSettingsStackNavigation } from 'src/app/navigation/types'
import { DeprecatedButton, Flex, Text, TouchableArea } from 'ui/src' import { DeprecatedButton, Flex, Text, TouchableArea } from 'ui/src'
...@@ -7,16 +7,32 @@ import { iconSizes } from 'ui/src/theme' ...@@ -7,16 +7,32 @@ import { iconSizes } from 'ui/src/theme'
import { AccountType } from 'uniswap/src/features/accounts/types' import { AccountType } from 'uniswap/src/features/accounts/types'
import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { useAccountsList } from 'wallet/src/features/wallet/hooks' import { SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types'
import { useAccounts } from 'wallet/src/features/wallet/hooks'
const DEFAULT_ACCOUNTS_TO_DISPLAY = 6 const DEFAULT_ACCOUNTS_TO_DISPLAY = 6
export function WalletSettings(): JSX.Element { export function WalletSettings(): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const navigation = useSettingsStackNavigation() const navigation = useSettingsStackNavigation()
const allAccounts = useAccountsList() const addressToAccount = useAccounts()
const [showAll, setShowAll] = useState(false) const [showAll, setShowAll] = useState(false)
const allAccounts = useMemo(() => {
const accounts = Object.values(addressToAccount)
const _mnemonicWallets = accounts
.filter((a): a is SignerMnemonicAccount => a.type === AccountType.SignerMnemonic)
.sort((a, b) => {
return a.derivationIndex - b.derivationIndex
})
const _viewOnlyWallets = accounts
.filter((a) => a.type === AccountType.Readonly)
.sort((a, b) => {
return a.timeImportedMs - b.timeImportedMs
})
return [..._mnemonicWallets, ..._viewOnlyWallets]
}, [addressToAccount])
const toggleViewAll = (): void => { const toggleViewAll = (): void => {
setShowAll(!showAll) setShowAll(!showAll)
} }
......
...@@ -21,8 +21,6 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' ...@@ -21,8 +21,6 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' import { useAppInsets } from 'uniswap/src/hooks/useAppInsets'
import { CurrencyId } from 'uniswap/src/types/currency' import { CurrencyId } from 'uniswap/src/types/currency'
import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { DDRumManualTiming } from 'utilities/src/logger/datadogEvents'
import { usePerformanceLogger } from 'utilities/src/logger/usePerformanceLogger'
import { isAndroid } from 'utilities/src/platform' import { isAndroid } from 'utilities/src/platform'
import { useValueAsRef } from 'utilities/src/react/useValueAsRef' import { useValueAsRef } from 'utilities/src/react/useValueAsRef'
import { InformationBanner } from 'wallet/src/components/banners/InformationBanner' import { InformationBanner } from 'wallet/src/components/banners/InformationBanner'
...@@ -78,8 +76,6 @@ export const TokenBalanceListInner = forwardRef<FlatList<TokenBalanceListRow>, T ...@@ -78,8 +76,6 @@ export const TokenBalanceListInner = forwardRef<FlatList<TokenBalanceListRow>, T
const colors = useSporeColors() const colors = useSporeColors()
const insets = useAppInsets() const insets = useAppInsets()
usePerformanceLogger(DDRumManualTiming.RenderTokenBalanceList, [])
const { rows, balancesById } = useTokenBalanceListContext() const { rows, balancesById } = useTokenBalanceListContext()
const { onContentSizeChange, adaptiveFooter } = useAdaptiveFooter(containerProps?.contentContainerStyle) const { onContentSizeChange, adaptiveFooter } = useAdaptiveFooter(containerProps?.contentContainerStyle)
......
import React, { memo } from 'react' import React, { memo } from 'react'
import { useTokenDetailsContext } from 'src/components/TokenDetails/TokenDetailsContext' import { useTokenDetailsContext } from 'src/components/TokenDetails/TokenDetailsContext'
import { Flex, flexStyles, Text } from 'ui/src' import { Flex, flexStyles, Text, TouchableArea } from 'ui/src'
import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo' import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo'
import WarningIcon from 'uniswap/src/components/warnings/WarningIcon'
import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { import {
useTokenBasicInfoPartsFragment, useTokenBasicInfoPartsFragment,
useTokenBasicProjectPartsFragment, useTokenBasicProjectPartsFragment,
} from 'uniswap/src/data/graphql/uniswap-data-api/fragments' } from 'uniswap/src/data/graphql/uniswap-data-api/fragments'
import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { fromGraphQLChain } from 'uniswap/src/features/chains/utils'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { TestID } from 'uniswap/src/test/fixtures/testIDs'
export const TokenDetailsHeader = memo(function _TokenDetailsHeader(): JSX.Element { export const TokenDetailsHeader = memo(function _TokenDetailsHeader(): JSX.Element {
const { currencyId } = useTokenDetailsContext() const tokenProtectionEnabled = useFeatureFlag(FeatureFlags.TokenProtection)
const { currencyId, openTokenWarningModal } = useTokenDetailsContext()
const token = useTokenBasicInfoPartsFragment({ currencyId }).data const token = useTokenBasicInfoPartsFragment({ currencyId }).data
const project = useTokenBasicProjectPartsFragment({ currencyId }).data.project const project = useTokenBasicProjectPartsFragment({ currencyId }).data.project
const shouldShowWarningIcon =
!tokenProtectionEnabled &&
(project?.safetyLevel === SafetyLevel.StrongWarning || project?.safetyLevel === SafetyLevel.Blocked)
return ( return (
<Flex gap="$spacing12" mx="$spacing16"> <Flex gap="$spacing12" mx="$spacing16">
<TokenLogo <TokenLogo
...@@ -33,6 +44,12 @@ export const TokenDetailsHeader = memo(function _TokenDetailsHeader(): JSX.Eleme ...@@ -33,6 +44,12 @@ export const TokenDetailsHeader = memo(function _TokenDetailsHeader(): JSX.Eleme
> >
{token?.name ?? ''} {token?.name ?? ''}
</Text> </Text>
{shouldShowWarningIcon && (
<TouchableArea onPress={openTokenWarningModal}>
<WarningIcon safetyLevel={project?.safetyLevel} size="$icon.20" strokeColorOverride="$neutral3" />
</TouchableArea>
)}
</Flex> </Flex>
</Flex> </Flex>
) )
......
...@@ -55,17 +55,10 @@ const TokenDetailsMarketData = memo(function _TokenDetailsMarketData(): JSX.Elem ...@@ -55,17 +55,10 @@ const TokenDetailsMarketData = memo(function _TokenDetailsMarketData(): JSX.Elem
const tokenMarket = useTokenMarketPartsFragment({ currencyId }).data?.market const tokenMarket = useTokenMarketPartsFragment({ currencyId }).data?.market
const projectMarkets = useTokenProjectMarketsPartsFragment({ currencyId }).data.project?.markets const projectMarkets = useTokenProjectMarketsPartsFragment({ currencyId }).data.project?.markets
const price = projectMarkets?.[0]?.price?.value || tokenMarket?.price?.value || undefined
const marketCap = projectMarkets?.[0]?.marketCap?.value const marketCap = projectMarkets?.[0]?.marketCap?.value
const volume = tokenMarket?.volume?.value const volume = tokenMarket?.volume?.value
const rawPriceHigh52W = projectMarkets?.[0]?.priceHigh52W?.value || tokenMarket?.priceHigh52W?.value || undefined const priceHight52W = projectMarkets?.[0]?.priceHigh52W?.value ?? tokenMarket?.priceHigh52W?.value
const rawPriceLow52W = projectMarkets?.[0]?.priceLow52W?.value || tokenMarket?.priceLow52W?.value || undefined const priceLow52W = projectMarkets?.[0]?.priceLow52W?.value ?? tokenMarket?.priceLow52W?.value
// Use current price for 52w high/low if it exceeds the bounds
const priceHight52W =
price !== undefined && rawPriceHigh52W !== undefined ? Math.max(price, rawPriceHigh52W) : rawPriceHigh52W
const priceLow52W =
price !== undefined && rawPriceLow52W !== undefined ? Math.min(price, rawPriceLow52W) : rawPriceLow52W
const fullyDilutedValuation = projectMarkets?.[0]?.fullyDilutedValuation?.value const fullyDilutedValuation = projectMarkets?.[0]?.fullyDilutedValuation?.value
return ( return (
...@@ -143,7 +136,6 @@ export const TokenDetailsStats = memo(function _TokenDetailsStats(): JSX.Element ...@@ -143,7 +136,6 @@ export const TokenDetailsStats = memo(function _TokenDetailsStats(): JSX.Element
includeFrench: language === Language.French, includeFrench: language === Language.French,
includeJapanese: language === Language.Japanese, includeJapanese: language === Language.Japanese,
includePortuguese: language === Language.Portuguese, includePortuguese: language === Language.Portuguese,
includeVietnamese: language === Language.Vietnamese,
includeChineseSimplified: language === Language.ChineseSimplified, includeChineseSimplified: language === Language.ChineseSimplified,
includeChineseTraditional: language === Language.ChineseTraditional, includeChineseTraditional: language === Language.ChineseTraditional,
}, },
...@@ -158,7 +150,6 @@ export const TokenDetailsStats = memo(function _TokenDetailsStats(): JSX.Element ...@@ -158,7 +150,6 @@ export const TokenDetailsStats = memo(function _TokenDetailsStats(): JSX.Element
descriptions?.descriptionTranslations?.descriptionFrFr || descriptions?.descriptionTranslations?.descriptionFrFr ||
descriptions?.descriptionTranslations?.descriptionJaJp || descriptions?.descriptionTranslations?.descriptionJaJp ||
descriptions?.descriptionTranslations?.descriptionPtPt || descriptions?.descriptionTranslations?.descriptionPtPt ||
descriptions?.descriptionTranslations?.descriptionViVn ||
descriptions?.descriptionTranslations?.descriptionZhHans || descriptions?.descriptionTranslations?.descriptionZhHans ||
descriptions?.descriptionTranslations?.descriptionZhHant descriptions?.descriptionTranslations?.descriptionZhHant
......
...@@ -66,7 +66,7 @@ function TokenOptionItemWrapper({ ...@@ -66,7 +66,7 @@ function TokenOptionItemWrapper({
option={option} option={option}
quantity={option.quantity} quantity={option.quantity}
quantityFormatted={formatNumberOrString({ value: option.quantity, type: NumberType.TokenTx })} quantityFormatted={formatNumberOrString({ value: option.quantity, type: NumberType.TokenTx })}
showWarnings={false} showWarnings={true}
tokenWarningDismissed={tokenWarningDismissed} tokenWarningDismissed={tokenWarningDismissed}
onPress={onPress} onPress={onPress}
/> />
......
...@@ -11,7 +11,6 @@ import { ...@@ -11,7 +11,6 @@ import {
ethToken, ethToken,
tokenMarket, tokenMarket,
tokenProject, tokenProject,
tokenProjectMarket,
} from 'uniswap/src/test/fixtures' } from 'uniswap/src/test/fixtures'
import { queryResolvers } from 'uniswap/src/test/utils' import { queryResolvers } from 'uniswap/src/test/utils'
import { getSymbolDisplayText } from 'uniswap/src/utils/currency' import { getSymbolDisplayText } from 'uniswap/src/utils/currency'
...@@ -32,16 +31,7 @@ jest.mock('@react-navigation/native', () => { ...@@ -32,16 +31,7 @@ jest.mock('@react-navigation/native', () => {
const mockStore = configureMockStore() const mockStore = configureMockStore()
const favoriteToken = ethToken({ const favoriteToken = ethToken({
project: { project: tokenProject(),
...tokenProject(),
markets: [
{
...tokenProjectMarket(),
price: amount({ value: 76543.21 }),
pricePercentChange24h: amount({ value: 6.54 }),
},
],
},
market: tokenMarket({ market: tokenMarket({
price: amount({ value: 12345.67 }), price: amount({ value: 12345.67 }),
pricePercentChange: amount({ value: 4.56 }), pricePercentChange: amount({ value: 4.56 }),
...@@ -74,12 +64,12 @@ describe('FavoriteTokenCard', () => { ...@@ -74,12 +64,12 @@ describe('FavoriteTokenCard', () => {
it('renders loader', async () => { it('renders loader', async () => {
const { queryByTestId } = render(<FavoriteTokenCard {...defaultProps} />, { resolvers }) const { queryByTestId } = render(<FavoriteTokenCard {...defaultProps} />, { resolvers })
const loaderPrice = queryByTestId('loader/favorite/price') const loader = queryByTestId('loader/favorite')
const loaderPriceChange = queryByTestId('loader/favorite/priceChange')
expect(loaderPrice).toBeTruthy() // loading
expect(loaderPriceChange).toBeTruthy() expect(loader).toBeTruthy()
// loading finished
await waitFor(() => { await waitFor(() => {
expect(queryByTestId(touchableId)).toBeTruthy() expect(queryByTestId(touchableId)).toBeTruthy()
}) })
...@@ -89,8 +79,8 @@ describe('FavoriteTokenCard', () => { ...@@ -89,8 +79,8 @@ describe('FavoriteTokenCard', () => {
describe('when token data is available', () => { describe('when token data is available', () => {
const cases = [ const cases = [
{ test: 'symbol', value: getSymbolDisplayText(favoriteToken.symbol)! }, { test: 'symbol', value: getSymbolDisplayText(favoriteToken.symbol)! },
{ test: 'price', value: '$76,543.21' }, { test: 'price', value: '$12,345.67' },
{ test: 'relative price change', value: '6.54%' }, { test: 'relative price change', value: '4.56%' },
] ]
it.each(cases)('renders correct $test', async ({ value }) => { it.each(cases)('renders correct $test', async ({ value }) => {
...@@ -101,22 +91,6 @@ describe('FavoriteTokenCard', () => { ...@@ -101,22 +91,6 @@ describe('FavoriteTokenCard', () => {
}) })
}) })
it('falls back to token price if token project price is not available', async () => {
const { resolvers: modifiedResolvers } = queryResolvers({
token: () => ({
...favoriteToken,
project: { ...favoriteToken.project, markets: [] },
}),
})
const { queryByText } = render(<FavoriteTokenCard {...defaultProps} />, { resolvers: modifiedResolvers })
await waitFor(() => {
expect(queryByText('$12,345.67')).toBeTruthy()
expect(queryByText('4.56%')).toBeTruthy()
})
})
it('navigates to the token details screen when pressed', async () => { it('navigates to the token details screen when pressed', async () => {
const { findByTestId } = render(<FavoriteTokenCard {...defaultProps} />, { resolvers }) const { findByTestId } = render(<FavoriteTokenCard {...defaultProps} />, { resolvers })
......
...@@ -6,17 +6,15 @@ import { useDispatch } from 'react-redux' ...@@ -6,17 +6,15 @@ import { useDispatch } from 'react-redux'
import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks'
import RemoveButton from 'src/components/explore/RemoveButton' import RemoveButton from 'src/components/explore/RemoveButton'
import { useAnimatedCardDragStyle, useExploreTokenContextMenu } from 'src/components/explore/hooks' import { useAnimatedCardDragStyle, useExploreTokenContextMenu } from 'src/components/explore/hooks'
import { Loader } from 'src/components/loading/loaders'
import { disableOnPress } from 'src/utils/disableOnPress' import { disableOnPress } from 'src/utils/disableOnPress'
import { usePollOnFocusOnly } from 'src/utils/hooks' import { usePollOnFocusOnly } from 'src/utils/hooks'
import { AnimatedTouchableArea, Flex, Loader, Text, useIsDarkMode, useShadowPropsShort, useSporeColors } from 'ui/src' import { AnimatedTouchableArea, Flex, Text, useIsDarkMode, useShadowPropsShort, useSporeColors } from 'ui/src'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { borderRadii, fonts, imageSizes, opacify } from 'ui/src/theme' import { borderRadii, imageSizes, opacify } from 'ui/src/theme'
import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo' import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo'
import { PollingInterval } from 'uniswap/src/constants/misc' import { PollingInterval } from 'uniswap/src/constants/misc'
import { import { useFavoriteTokenCardQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
FavoriteTokenCardQuery,
useFavoriteTokenCardQuery,
} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { fromGraphQLChain } from 'uniswap/src/features/chains/utils'
import { currencyIdToContractInput } from 'uniswap/src/features/dataApi/utils' import { currencyIdToContractInput } from 'uniswap/src/features/dataApi/utils'
...@@ -68,10 +66,8 @@ function FavoriteTokenCard({ ...@@ -68,10 +66,8 @@ function FavoriteTokenCard({
// Mirror behavior in top tokens list, use first chain the token is on for the symbol // Mirror behavior in top tokens list, use first chain the token is on for the symbol
const chainId = fromGraphQLChain(token?.chain) ?? defaultChainId const chainId = fromGraphQLChain(token?.chain) ?? defaultChainId
// Coingecko price is more accurate but lacks long tail tokens const price = convertFiatAmountFormatted(token?.market?.price?.value, NumberType.FiatTokenPrice)
// Uniswap price comes from Uniswap pools, which may be updated less frequently const pricePercentChange = token?.market?.pricePercentChange?.value
const { price, pricePercentChange } = getCoingeckoPrice(token) ?? getUniswapPrice(token)
const priceFormatted = convertFiatAmountFormatted(price, NumberType.FiatTokenPrice)
const onRemove = useCallback(() => { const onRemove = useCallback(() => {
if (currencyId) { if (currencyId) {
...@@ -102,7 +98,9 @@ function FavoriteTokenCard({ ...@@ -102,7 +98,9 @@ function FavoriteTokenCard({
const shadowProps = useShadowPropsShort() const shadowProps = useShadowPropsShort()
const priceLoading = isNonPollingRequestInFlight(networkStatus) if (isNonPollingRequestInFlight(networkStatus)) {
return <Loader.Favorite height={FAVORITE_TOKEN_CARD_LOADER_HEIGHT} />
}
return ( return (
<AnimatedFlex borderRadius="$rounded16" style={animatedDragStyle}> <AnimatedFlex borderRadius="$rounded16" style={animatedDragStyle}>
...@@ -140,31 +138,15 @@ function FavoriteTokenCard({ ...@@ -140,31 +138,15 @@ function FavoriteTokenCard({
<RemoveButton visible={isEditing} onPress={onRemove} /> <RemoveButton visible={isEditing} onPress={onRemove} />
</Flex> </Flex>
<Flex gap="$spacing2"> <Flex gap="$spacing2">
{priceLoading ? ( <Text adjustsFontSizeToFit numberOfLines={1} variant="heading3">
<Loader.Box {price}
height={fonts.heading3.lineHeight} </Text>
width={fonts.heading3.lineHeight * 3} <RelativeChange
testID="loader/favorite/price" arrowSize="$icon.16"
/> change={pricePercentChange ?? undefined}
) : ( semanticColor={true}
<Text adjustsFontSizeToFit numberOfLines={1} variant="heading3"> variant="subheading2"
{priceFormatted} />
</Text>
)}
{priceLoading ? (
<Loader.Box
height={fonts.subheading2.lineHeight}
width={fonts.subheading2.lineHeight * 3}
testID="loader/favorite/priceChange"
/>
) : (
<RelativeChange
arrowSize="$icon.16"
change={pricePercentChange ?? undefined}
semanticColor={true}
variant="subheading2"
/>
)}
</Flex> </Flex>
</Flex> </Flex>
</AnimatedTouchableArea> </AnimatedTouchableArea>
...@@ -173,29 +155,4 @@ function FavoriteTokenCard({ ...@@ -173,29 +155,4 @@ function FavoriteTokenCard({
) )
} }
function getCoingeckoPrice(token?: FavoriteTokenCardQuery['token']): {
price: number | undefined
pricePercentChange: number | undefined
} | null {
const market = token?.project?.markets?.[0]
if (!market?.price?.value || !market?.pricePercentChange24h?.value) {
return null
}
return {
price: market.price.value,
pricePercentChange: market.pricePercentChange24h.value,
}
}
function getUniswapPrice(token?: FavoriteTokenCardQuery['token']): {
price: number | undefined
pricePercentChange: number | undefined
} {
return {
price: token?.market?.price?.value,
pricePercentChange: token?.market?.pricePercentChange?.value,
}
}
export default memo(FavoriteTokenCard) export default memo(FavoriteTokenCard)
...@@ -96,10 +96,10 @@ export function FavoriteTokensGrid({ showLoading, ...rest }: FavoriteTokensGridP ...@@ -96,10 +96,10 @@ export function FavoriteTokensGrid({ showLoading, ...rest }: FavoriteTokensGridP
function FavoriteTokensGridLoader(): JSX.Element { function FavoriteTokensGridLoader(): JSX.Element {
return ( return (
<Flex row> <Flex row>
<Flex mx="$spacing4" style={ITEM_FLEX}> <Flex m="$spacing4" style={ITEM_FLEX}>
<Loader.Favorite contrast height={FAVORITE_TOKEN_CARD_LOADER_HEIGHT} /> <Loader.Favorite contrast height={FAVORITE_TOKEN_CARD_LOADER_HEIGHT} />
</Flex> </Flex>
<Flex mx="$spacing4" style={ITEM_FLEX}> <Flex m="$spacing4" style={ITEM_FLEX}>
<Loader.Favorite contrast height={FAVORITE_TOKEN_CARD_LOADER_HEIGHT} /> <Loader.Favorite contrast height={FAVORITE_TOKEN_CARD_LOADER_HEIGHT} />
</Flex> </Flex>
</Flex> </Flex>
......
import { SortButton } from 'src/components/explore/SortButton' import { SortButton } from 'src/components/explore/SortButton'
import { act, render } from 'src/test/test-utils' import { act, render } from 'src/test/test-utils'
import { CustomRankingType, RankingType } from 'uniswap/src/data/types' import { CustomRankingType, ExploreOrderBy, RankingType } from 'wallet/src/features/wallet/types'
import { ExploreOrderBy } from 'wallet/src/features/wallet/types'
jest.mock('react-native-context-menu-view', () => { jest.mock('react-native-context-menu-view', () => {
// Use the actual implementation of `react-native-context-menu-view` as the mock implementation // Use the actual implementation of `react-native-context-menu-view` as the mock implementation
......
...@@ -15,12 +15,11 @@ import { ...@@ -15,12 +15,11 @@ import {
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { ActionSheetDropdown } from 'uniswap/src/components/dropdowns/ActionSheetDropdown' import { ActionSheetDropdown } from 'uniswap/src/components/dropdowns/ActionSheetDropdown'
import { MenuItemProp } from 'uniswap/src/components/modals/ActionSheetModal' import { MenuItemProp } from 'uniswap/src/components/modals/ActionSheetModal'
import { CustomRankingType, RankingType } from 'uniswap/src/data/types'
import { MobileEventName } from 'uniswap/src/features/telemetry/constants' import { MobileEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { setTokensOrderBy } from 'wallet/src/features/wallet/slice' import { setTokensOrderBy } from 'wallet/src/features/wallet/slice'
import { ExploreOrderBy } from 'wallet/src/features/wallet/types' import { CustomRankingType, ExploreOrderBy, RankingType } from 'wallet/src/features/wallet/types'
const MIN_MENU_ITEM_WIDTH = 220 const MIN_MENU_ITEM_WIDTH = 220
......
...@@ -9,11 +9,11 @@ import { Flex, Loader } from 'ui/src' ...@@ -9,11 +9,11 @@ import { Flex, Loader } from 'ui/src'
import { ProtectionResult, SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { ProtectionResult, SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { ALL_NETWORKS_ARG } from 'uniswap/src/data/rest/base' import { ALL_NETWORKS_ARG } from 'uniswap/src/data/rest/base'
import { useTokenRankingsQuery } from 'uniswap/src/data/rest/tokenRankings' import { useTokenRankingsQuery } from 'uniswap/src/data/rest/tokenRankings'
import { RankingType } from 'uniswap/src/data/types'
import { UniverseChainId } from 'uniswap/src/features/chains/types' import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { fromGraphQLChain } from 'uniswap/src/features/chains/utils'
import { TokenList } from 'uniswap/src/features/dataApi/types' import { TokenList } from 'uniswap/src/features/dataApi/types'
import { SearchResultType, TokenSearchResult } from 'uniswap/src/features/search/SearchResult' import { SearchResultType, TokenSearchResult } from 'uniswap/src/features/search/SearchResult'
import { RankingType } from 'wallet/src/features/wallet/types'
const MAX_TOKEN_RESULTS_AMOUNT = 8 const MAX_TOKEN_RESULTS_AMOUNT = 8
......
...@@ -28,14 +28,14 @@ export function useWalletSearchResults( ...@@ -28,14 +28,14 @@ export function useWalletSearchResults(
address: dotEthAddress, address: dotEthAddress,
name: dotEthName, name: dotEthName,
loading: dotEthLoading, loading: dotEthLoading,
} = useENS({ nameOrAddress: querySkippedIfValidAddress, autocompleteDomain: true }) } = useENS(UniverseChainId.Mainnet, querySkippedIfValidAddress, true)
// Search for exact match for ENS if not a valid address // Search for exact match for ENS if not a valid address
const { const {
address: ensAddress, address: ensAddress,
name: ensName, name: ensName,
loading: ensLoading, loading: ensLoading,
} = useENS({ nameOrAddress: querySkippedIfValidAddress, autocompleteDomain: false }) } = useENS(UniverseChainId.Mainnet, querySkippedIfValidAddress, false)
// Search for matching Unitag by name // Search for matching Unitag by name
const { unitag: unitagByName, loading: unitagLoading } = useUnitagByName(query) const { unitag: unitagByName, loading: unitagLoading } = useUnitagByName(query)
......
...@@ -12,8 +12,6 @@ import { Flex, useSporeColors } from 'ui/src' ...@@ -12,8 +12,6 @@ import { Flex, useSporeColors } from 'ui/src'
import { GQLQueries } from 'uniswap/src/data/graphql/uniswap-data-api/queries' import { GQLQueries } from 'uniswap/src/data/graphql/uniswap-data-api/queries'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' import { useAppInsets } from 'uniswap/src/hooks/useAppInsets'
import { DDRumManualTiming } from 'utilities/src/logger/datadogEvents'
import { usePerformanceLogger } from 'utilities/src/logger/usePerformanceLogger'
import { isAndroid } from 'utilities/src/platform' import { isAndroid } from 'utilities/src/platform'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
import { useActivityData } from 'wallet/src/features/activity/useActivityData' import { useActivityData } from 'wallet/src/features/activity/useActivityData'
...@@ -59,8 +57,6 @@ export const ActivityTab = memo( ...@@ -59,8 +57,6 @@ export const ActivityTab = memo(
onPressEmptyState: onPressReceive, onPressEmptyState: onPressReceive,
}) })
usePerformanceLogger(DDRumManualTiming.RenderActivityTabList, [])
const refreshControl = useMemo(() => { const refreshControl = useMemo(() => {
return ( return (
<RefreshControl <RefreshControl
......
...@@ -7,13 +7,10 @@ import { FundWalletModal } from 'src/components/home/introCards/FundWalletModal' ...@@ -7,13 +7,10 @@ import { FundWalletModal } from 'src/components/home/introCards/FundWalletModal'
import { openModal } from 'src/features/modals/modalSlice' import { openModal } from 'src/features/modals/modalSlice'
import { Flex } from 'ui/src' import { Flex } from 'ui/src'
import { Buy, ShieldCheck, UniswapLogo } from 'ui/src/components/icons' import { Buy, ShieldCheck, UniswapLogo } from 'ui/src/components/icons'
import { UnichainIntroModal } from 'uniswap/src/components/unichain/UnichainIntroModal'
import { AccountType } from 'uniswap/src/features/accounts/types' import { AccountType } from 'uniswap/src/features/accounts/types'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { ElementName, ModalName, WalletEventName } 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 { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { OnboardingCardLoggingName } from 'uniswap/src/features/telemetry/types' import { OnboardingCardLoggingName } from 'uniswap/src/features/telemetry/types'
import { CurrencyField } from 'uniswap/src/types/currency'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { MobileScreens, OnboardingScreens, UnitagScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens, OnboardingScreens, UnitagScreens } from 'uniswap/src/types/screens/mobile'
import { import {
...@@ -24,17 +21,16 @@ import { ...@@ -24,17 +21,16 @@ import {
} from 'wallet/src/components/introCards/IntroCard' } from 'wallet/src/components/introCards/IntroCard'
import { INTRO_CARD_MIN_HEIGHT, IntroCardStack } from 'wallet/src/components/introCards/IntroCardStack' import { INTRO_CARD_MIN_HEIGHT, IntroCardStack } from 'wallet/src/components/introCards/IntroCardStack'
import { useSharedIntroCards } from 'wallet/src/components/introCards/useSharedIntroCards' import { useSharedIntroCards } from 'wallet/src/components/introCards/useSharedIntroCards'
import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext'
import { selectHasViewedWelcomeWalletCard } from 'wallet/src/features/behaviorHistory/selectors' import { selectHasViewedWelcomeWalletCard } from 'wallet/src/features/behaviorHistory/selectors'
import { setHasViewedWelcomeWalletCard } from 'wallet/src/features/behaviorHistory/slice' import { setHasViewedWelcomeWalletCard } from 'wallet/src/features/behaviorHistory/slice'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
type OnboardingIntroCardStackProps = { type OnboardingIntroCardStackProps = {
isLoading?: boolean isLoading?: boolean
showEmptyWalletState: boolean hasTokens: boolean
} }
export function OnboardingIntroCardStack({ export function OnboardingIntroCardStack({
showEmptyWalletState, hasTokens,
isLoading = false, isLoading = false,
}: OnboardingIntroCardStackProps): JSX.Element | null { }: OnboardingIntroCardStackProps): JSX.Element | null {
const { t } = useTranslation() const { t } = useTranslation()
...@@ -47,8 +43,6 @@ export function OnboardingIntroCardStack({ ...@@ -47,8 +43,6 @@ export function OnboardingIntroCardStack({
const welcomeCardTitle = t('onboarding.home.intro.welcome.title') const welcomeCardTitle = t('onboarding.home.intro.welcome.title')
const hasViewedWelcomeWalletCard = useSelector(selectHasViewedWelcomeWalletCard) const hasViewedWelcomeWalletCard = useSelector(selectHasViewedWelcomeWalletCard)
const { navigateToSwapFlow } = useWalletNavigation()
const navigateToUnitagClaim = useCallback(() => { const navigateToUnitagClaim = useCallback(() => {
navigate(MobileScreens.UnitagStack, { navigate(MobileScreens.UnitagStack, {
screen: UnitagScreens.ClaimUnitag, screen: UnitagScreens.ClaimUnitag,
...@@ -68,15 +62,14 @@ export function OnboardingIntroCardStack({ ...@@ -68,15 +62,14 @@ export function OnboardingIntroCardStack({
) )
}, [dispatch, address]) }, [dispatch, address])
const [showFundModal, setShowFundModal] = useState(false)
const [showUnichainIntroModal, setShowUnichainIntroModal] = useState(false)
const { cards: sharedCards } = useSharedIntroCards({ const { cards: sharedCards } = useSharedIntroCards({
showUnichainModal: () => setShowUnichainIntroModal(true), hasTokens,
navigateToUnitagClaim, navigateToUnitagClaim,
navigateToUnitagIntro, navigateToUnitagIntro,
}) })
const [showFundModal, setShowFundModal] = useState(false)
const cards = useMemo((): IntroCardProps[] => { const cards = useMemo((): IntroCardProps[] => {
const output: IntroCardProps[] = [] const output: IntroCardProps[] = []
...@@ -85,7 +78,7 @@ export function OnboardingIntroCardStack({ ...@@ -85,7 +78,7 @@ export function OnboardingIntroCardStack({
return output return output
} }
if (showEmptyWalletState) { if (!hasTokens) {
output.push({ output.push({
loggingName: OnboardingCardLoggingName.FundWallet, loggingName: OnboardingCardLoggingName.FundWallet,
graphic: { graphic: {
...@@ -149,7 +142,7 @@ export function OnboardingIntroCardStack({ ...@@ -149,7 +142,7 @@ export function OnboardingIntroCardStack({
} }
return output return output
}, [hasBackups, showEmptyWalletState, hasViewedWelcomeWalletCard, isSignerAccount, sharedCards, t, welcomeCardTitle]) }, [hasBackups, hasTokens, hasViewedWelcomeWalletCard, isSignerAccount, sharedCards, t, welcomeCardTitle])
const handleSwiped = useCallback( const handleSwiped = useCallback(
(_card: IntroCardProps, index: number) => { (_card: IntroCardProps, index: number) => {
...@@ -167,31 +160,15 @@ export function OnboardingIntroCardStack({ ...@@ -167,31 +160,15 @@ export function OnboardingIntroCardStack({
[cards, dispatch, hasViewedWelcomeWalletCard, welcomeCardTitle], [cards, dispatch, hasViewedWelcomeWalletCard, welcomeCardTitle],
) )
const UnichainIntroModalInstance = useMemo((): JSX.Element => {
return (
<UnichainIntroModal
openSwapFlow={() =>
navigateToSwapFlow({ openTokenSelector: CurrencyField.OUTPUT, outputChainId: UniverseChainId.Unichain })
}
onClose={() => setShowUnichainIntroModal(false)}
/>
)
}, [navigateToSwapFlow])
if (cards.length) { if (cards.length) {
return ( return (
<Flex pt="$spacing12"> <Flex pt="$spacing12">
{isLoading ? <Flex height={INTRO_CARD_MIN_HEIGHT} /> : <IntroCardStack cards={cards} onSwiped={handleSwiped} />} {isLoading ? <Flex height={INTRO_CARD_MIN_HEIGHT} /> : <IntroCardStack cards={cards} onSwiped={handleSwiped} />}
{showFundModal && <FundWalletModal onClose={() => setShowFundModal(false)} />} {showFundModal && <FundWalletModal onClose={() => setShowFundModal(false)} />}
{showUnichainIntroModal && UnichainIntroModalInstance}
</Flex> </Flex>
) )
} }
if (showUnichainIntroModal) {
return UnichainIntroModalInstance
}
return null return null
} }
import React from 'react' import React from 'react'
import { SvgProps } from 'react-native-svg' import { SvgProps } from 'react-native-svg'
import { useIsDarkMode } from 'ui/src'
import { IconSizeTokens } from 'ui/src/theme' import { IconSizeTokens } from 'ui/src/theme'
import { UNIVERSE_CHAIN_LOGO } from 'uniswap/src/assets/chainLogos'
import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo'
import { useBlockExplorerLogo } from 'uniswap/src/features/chains/logos'
import { UniverseChainId } from 'uniswap/src/features/chains/types' import { UniverseChainId } from 'uniswap/src/features/chains/types'
type IconComponentProps = SvgProps & { size?: IconSizeTokens | number } type IconComponentProps = SvgProps & { size?: IconSizeTokens | number }
...@@ -11,10 +12,11 @@ const iconsCache = new Map<UniverseChainId, React.FC<IconComponentProps>>() ...@@ -11,10 +12,11 @@ const iconsCache = new Map<UniverseChainId, React.FC<IconComponentProps>>()
function buildIconComponent(chainId: UniverseChainId): React.FC<IconComponentProps> { function buildIconComponent(chainId: UniverseChainId): React.FC<IconComponentProps> {
const explorer = getChainInfo(chainId).explorer const explorer = getChainInfo(chainId).explorer
const explorerLogos = UNIVERSE_CHAIN_LOGO[chainId].explorer
const Component = ({ size }: IconComponentProps): JSX.Element => { const Component = ({ size }: IconComponentProps): JSX.Element => {
const Logo = useBlockExplorerLogo(chainId) const isDarkMode = useIsDarkMode()
return <Logo size={size} /> return isDarkMode ? <explorerLogos.logoDark size={size} /> : <explorerLogos.logoLight size={size} />
} }
Component.displayName = `BlockExplorerIcon_${explorer.name}` Component.displayName = `BlockExplorerIcon_${explorer.name}`
iconsCache.set(chainId, Component) iconsCache.set(chainId, Component)
......
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