ci(release): publish latest release

parent 922a412c
IPFS hash of the deployment: IPFS hash of the deployment:
- CIDv0: `QmSFSxn2NQ8LP2HZckSX6WkmwGz9SQudXWqsQ9ShJKRZSb` - CIDv0: `QmQCnXjjekxSd3k2zzpWjh85rA7sgg1R3RiiiGMTpVgNE6`
- CIDv1: `bafybeib2dgh2xulteyu7s2lwepf2oyuwf63k3cpxeb5ogcnemn3cdsmpjq` - CIDv1: `bafybeia3wojcisyfrr4bcavcvvrza2e2jxcoqo557gmewxmgx4gcv7msum`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org). The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
...@@ -10,15 +10,84 @@ You can also access the Uniswap Interface from an IPFS gateway. ...@@ -10,15 +10,84 @@ You can also access the Uniswap Interface from an IPFS gateway.
Your Uniswap settings are never remembered across different URLs. Your Uniswap settings are never remembered across different URLs.
IPFS gateways: IPFS gateways:
- https://bafybeib2dgh2xulteyu7s2lwepf2oyuwf63k3cpxeb5ogcnemn3cdsmpjq.ipfs.dweb.link/ - https://bafybeia3wojcisyfrr4bcavcvvrza2e2jxcoqo557gmewxmgx4gcv7msum.ipfs.dweb.link/
- https://bafybeib2dgh2xulteyu7s2lwepf2oyuwf63k3cpxeb5ogcnemn3cdsmpjq.ipfs.cf-ipfs.com/ - https://bafybeia3wojcisyfrr4bcavcvvrza2e2jxcoqo557gmewxmgx4gcv7msum.ipfs.cf-ipfs.com/
- [ipfs://QmSFSxn2NQ8LP2HZckSX6WkmwGz9SQudXWqsQ9ShJKRZSb/](ipfs://QmSFSxn2NQ8LP2HZckSX6WkmwGz9SQudXWqsQ9ShJKRZSb/) - [ipfs://QmQCnXjjekxSd3k2zzpWjh85rA7sgg1R3RiiiGMTpVgNE6/](ipfs://QmQCnXjjekxSd3k2zzpWjh85rA7sgg1R3RiiiGMTpVgNE6/)
### 5.65.2 (2025-01-16) ## 5.66.0 (2025-01-21)
### Features
* **web:** add basic behavior history reset button (#14917) 80c38bf
* **web:** align main CTA buttons in all swap forms (#14954) 4d9d0b7
* **web:** design changes for pool out of sync state (#15178) 5b17719
* **web:** display dependent amount from Trading API response in Increase LP flow (#15116) 3fb84dd
* **web:** log sentry error for approve/increase/decrease/create/migrate [lp] (#15223) 6ee46f0
* **web:** log sentry error for claim [lp] (#15218) 6a8e41b
* **web:** unichain card (cold) (#14820) 56c5117
* **web:** unichain card (warm) (#14822) 1b42422
* **web:** update create trading api (#15159) 2d6e12e
### Bug Fixes ### Bug Fixes
* **web:** cherry-picks lping to reduce trading API errors (#15225) 1db3f5d * **mweb:** decimal separator on mweb (#15219) 2a75310
* **web:** add settings context provider to migrate page (#15195) c703139
* **web:** apply position list filters to saved pairs (#15050) 8463eef
* **web:** approve the wrapped token for v2 + v3 (#15217) 140fca5
* **web:** avoid showing incorrect Trading API error messages when skipping queries (#15038) ecfc228
* **web:** broken link on v2 migrate page (#15156) a813623
* **web:** disable range input when lacking data to visualize (#15067) 5ff362b
* **web:** DoubleLogo networkLogo bug (#15154) e95a795
* **web:** explore table should use chainId in ranking (#15103) b1c0e2e
* **web:** fix analytics toggle setting flash (#15128) 07e954a
* **web:** fix blocked token language (#15132) f6d06dd
* **web:** fix infinite loop on analytics toggle (#15070) fd57c35
* **web:** fix max token amount calc error (#15139) bb425a5
* **web:** fix mweb navigation test (#15014) 6600909
* **web:** fix numbers for LP flow analytics (#15140) 7fef11e
* **web:** fix scroll persist on transactions table tab (#14795) 621dc15
* **web:** fix the tokenA and tokenB deposit form for single-sided liquidity (#15174) 581d4d0
* **web:** fix wrapping/unwrapping for v3 (#15263) f29bcb8
* **web:** handle empty quotes array from new FOR service (#14955) ab29c6b
* **web:** import esm version of connectrpc (#14998) 9d4409b
* **web:** import esm version of i18next (#15006) 019c9c1
* **web:** import v2 positions fixes (#15151) fd0d075
* **web:** incorrect base/quote currency labels for price range on edit component (#15285) 88da0e4
* **web:** price range input bug fix (#15228) b0ce03b
* **web:** react key error in LP create flow (#15034) 01b6ab5
* **web:** remove auto-wrapping option for v4 increase / decrease (#15232) 1e203d8
* **web:** remove liquidity input 100 max (#14968) f96c4a5
* **web:** remove unnecessary tooltip from v2 migration page (#15155) 70cce38
* **web:** scroll on nav dropdown and adjust max height (#15087) c594c67
* **web:** search input padding ui fix for mweb safari (#15039) 2b28393
* **web:** single-sided liquidity crash (#15224) eecc9ab
* **web:** UI fixes for v4 position cards (#15053) c1ca09c
* **web:** unavailable price state for pool creation (#15071) eb19cbe
* **web:** update calls to trading api after new schema change (#15095) 77391b5
* **web:** update google conversion datetime format (#15021) 1cbb13f
* **web:** update input to trading api fro v3 + v4 create (#15205) 382c928
* **web:** use deeplink to default to unichain (#15318) 528b5f5
* **web:** v4 bug bash fixes (#15212) 62d63a8
* **web:** v4 hook flow fixes (#15052) 329f961
* **web:** v4 UI nit fixes (#15051) e20b2c1
### Styles
* **web:** staging copy nav dropdown item style (#15322) 94fff89
### Continuous Integration
* **web:** update sitemaps 55b34d7
### Code Refactoring
* **web:** use universe ens address hook (#14669) 1c229db
* **web:** use universe ens avatar hook (#14670) 7af231a
* **web:** use universe ens name hook (#14667) ce789a4
web/5.65.2 web/5.66.0
\ No newline at end of file \ No newline at end of file
...@@ -25,6 +25,3 @@ dist-ssr ...@@ -25,6 +25,3 @@ dist-ssr
*.sw? *.sw?
.tamagui .tamagui
# Sentry Config File
.env.sentry-build-plugin
...@@ -9,17 +9,14 @@ ...@@ -9,17 +9,14 @@
"@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.7.0", "@uniswap/universal-router-sdk": "4.10.0",
"@uniswap/v3-sdk": "3.19.0", "@uniswap/v3-sdk": "3.21.0",
"@uniswap/v4-sdk": "1.12.0", "@uniswap/v4-sdk": "1.15.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,6 +2,8 @@ body, ...@@ -2,6 +2,8 @@ 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,11 +4,12 @@ import 'symbol-observable' // Needed by `reduxed-chrome-storage` as polyfill, or ...@@ -4,11 +4,12 @@ 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 } from 'react-router-dom' import { RouteObject, RouterProvider, createHashRouter } 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 {
...@@ -34,7 +35,6 @@ import { ScanToOnboard } from 'src/app/features/onboarding/scan/ScanToOnboard' ...@@ -34,7 +35,6 @@ 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 = sentryCreateHashRouter([ const router = createHashRouter([
{ {
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={SentryAppNameTag.Onboarding}> <ExtensionStatsigProvider appName={DatadogAppNameTag.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 } from 'react-router-dom' import { RouterProvider, createHashRouter } 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,23 +25,11 @@ import { ElementName } from 'uniswap/src/features/telemetry/constants' ...@@ -25,23 +25,11 @@ 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'
getUniqueId() const router = createHashRouter([
.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 />,
...@@ -128,7 +116,7 @@ export default function PopupApp(): JSX.Element { ...@@ -128,7 +116,7 @@ export default function PopupApp(): JSX.Element {
return ( return (
<Trace> <Trace>
<PersistGate persistor={getReduxPersistor()}> <PersistGate persistor={getReduxPersistor()}>
<ExtensionStatsigProvider appName={SentryAppNameTag.Popup}> <ExtensionStatsigProvider appName={DatadogAppNameTag.Popup}>
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}> <SharedWalletProvider reduxStore={getReduxStore()}>
<ErrorBoundary> <ErrorBoundary>
......
...@@ -4,12 +4,13 @@ import 'src/app/Global.css' ...@@ -4,12 +4,13 @@ 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 } from 'react-router-dom' import { RouterProvider, createHashRouter } 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'
...@@ -29,7 +30,6 @@ import { useIsWalletUnlocked } from 'src/app/hooks/useIsWalletUnlocked' ...@@ -29,7 +30,6 @@ 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,7 +47,6 @@ import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' ...@@ -47,7 +47,6 @@ 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'
...@@ -56,17 +55,7 @@ import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary ...@@ -56,17 +55,7 @@ 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'
getUniqueId() const router = createHashRouter([
.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 />,
...@@ -258,7 +247,7 @@ export default function SidebarApp(): JSX.Element { ...@@ -258,7 +247,7 @@ export default function SidebarApp(): JSX.Element {
return ( return (
<Trace> <Trace>
<PersistGate persistor={getReduxPersistor()}> <PersistGate persistor={getReduxPersistor()}>
<ExtensionStatsigProvider appName={SentryAppNameTag.Sidebar}> <ExtensionStatsigProvider appName={DatadogAppNameTag.Sidebar}>
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}> <SharedWalletProvider reduxStore={getReduxStore()}>
<ErrorBoundary> <ErrorBoundary>
......
...@@ -3,12 +3,13 @@ import 'src/app/Global.css' ...@@ -3,12 +3,13 @@ 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, useSearchParams } from 'react-router-dom' import { Outlet, RouterProvider, createHashRouter, 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,
...@@ -23,7 +24,6 @@ import { UnitagCreateUsernameScreen } from 'src/app/features/unitags/UnitagCreat ...@@ -23,7 +24,6 @@ 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,7 +32,6 @@ import { LocalizationContextProvider } from 'uniswap/src/features/language/Local ...@@ -32,7 +32,6 @@ 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'
...@@ -40,17 +39,7 @@ import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testne ...@@ -40,17 +39,7 @@ 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'
getUniqueId() const router = createHashRouter([
.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 />,
...@@ -162,7 +151,7 @@ export default function UnitagClaimApp(): JSX.Element { ...@@ -162,7 +151,7 @@ export default function UnitagClaimApp(): JSX.Element {
return ( return (
<Trace> <Trace>
<PersistGate persistor={getReduxPersistor()}> <PersistGate persistor={getReduxPersistor()}>
<ExtensionStatsigProvider appName={SentryAppNameTag.UnitagClaim}> <ExtensionStatsigProvider appName={DatadogAppNameTag.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 { datadogRum } from '@datadog/browser-rum' import { RumEvent, 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 { FeatureFlags, WALLET_FEATURE_FLAG_NAMES, getFeatureFlagName } from 'uniswap/src/features/gating/flags' import { WALLET_FEATURE_FLAG_NAMES } 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'
export async function initializeDatadog(appName: string): Promise<void> { // In case Statsig is not available
const datadogEnabled = Statsig.checkGate(getFeatureFlagName(FeatureFlags.Datadog)) const EXTENSION_DEFAULT_DATADOG_SESSION_SAMPLE_RATE = 10 // percent
logger.setWalletDatadogEnabled(datadogEnabled)
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> {
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: 100, sessionSampleRate: localDevDatadogEnabled ? 100 : sessionSampleRate,
sessionReplaySampleRate: 0, sessionReplaySampleRate: 0,
trackResources: true, trackResources: true,
trackLongTasks: true, trackLongTasks: true,
trackUserInteractions: true, trackUserInteractions: true,
enablePrivacyForActionName: true, enablePrivacyForActionName: true,
beforeSend: (event) => { beforeSend,
// 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
},
}) })
datadogLogs.init({ // According to the Datadog RUM documentation:
...sharedDatadogConfig, // https://docs.datadoghq.com/real_user_monitoring/browser/setup/client?tab=rum#access-internal-context
site: 'datadoghq.com', // datadogRum.init() seems to be synchronous and internal context is immediately available.
forwardErrorsToLogs: false, // Local testing confirms this behavior, explaining why no "onInitialization" callback is needed.
}) 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.TokenWarningModal} backgroundColor="$surface1" onClose={close}> <Modal isDismissible isModalOpen name={ModalName.AppRatingModal} 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,7 +70,8 @@ export function ApproveRequestContent({ ...@@ -70,7 +70,8 @@ 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) => typeof arg === 'object' && arg._hex && BigNumber.from(arg._hex).isZero(), (arg) =>
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,6 +66,13 @@ export function SignTypedDataRequestContent({ dappRequest }: SignTypedDataReques ...@@ -66,6 +66,13 @@ 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} />
...@@ -76,6 +83,12 @@ export function SignTypedDataRequestContent({ dappRequest }: SignTypedDataReques ...@@ -76,6 +83,12 @@ 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,6 +251,11 @@ export function* handleSendTransaction( ...@@ -251,6 +251,11 @@ 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 } from 'react' import { useCallback, useState } 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 { PollingInterval } from 'uniswap/src/constants/misc' 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 { usePortfolioTotalValue } from 'uniswap/src/features/dataApi/balances' import { UniverseChainId } from 'uniswap/src/features/chains/types'
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,6 +37,14 @@ export function HomeIntroCardStack(): JSX.Element | null { ...@@ -37,6 +37,14 @@ 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 12 supported networks. You can send and receive tokens and NFTs on all of our 13 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 12 supported networks. You can send and receive tokens and NFTs on all of our 13 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 { Outlet, ScrollRestoration, useLocation } from 'react-router-dom' import { NavigationType, 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 === 'POP' const isBackwards = routerState?.historyAction === NavigationType.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 { Router } from 'react-router-dom' import { Location, NavigationType, Router, createHashRouter } 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!
...@@ -54,7 +57,7 @@ export function useRouterState(): RouterState | null { ...@@ -54,7 +57,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 sentryCreateHashRouter> type Router = ReturnType<typeof createHashRouter>
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,16 +14,6 @@ export function getStatsigEnvironmentTier(): StatsigEnvironmentTier { ...@@ -14,16 +14,6 @@ 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
...@@ -39,9 +29,3 @@ enum DatadogEnvironment { ...@@ -39,9 +29,3 @@ enum DatadogEnvironment {
BETA = 'beta', BETA = 'beta',
PROD = 'prod', PROD = 'prod',
} }
enum SentryEnvironment {
DEV = 'development',
BETA = 'beta',
PROD = 'production',
}
...@@ -2,7 +2,6 @@ import 'symbol-observable' // Needed by `reduxed-chrome-storage` as polyfill, or ...@@ -2,7 +2,6 @@ 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'
...@@ -10,7 +9,6 @@ import { backgroundToSidePanelMessageChannel } from 'src/background/messagePassi ...@@ -10,7 +9,6 @@ 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
...@@ -18,8 +16,6 @@ export const EXTENSION_ID = chrome.runtime.id ...@@ -18,8 +16,6 @@ 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 Sentry since they are informational // These go to Amplitude instead of Datadog 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,24 +4,13 @@ ...@@ -4,24 +4,13 @@
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,23 +4,12 @@ ...@@ -4,23 +4,12 @@
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,23 +4,12 @@ ...@@ -4,23 +4,12 @@
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.14.0", "version": "1.15.0",
"minimum_chrome_version": "116", "minimum_chrome_version": "116",
"icons": { "icons": {
"16": "assets/icon16.png", "16": "assets/icon16.png",
......
import { RankingType } from 'wallet/src/features/wallet/types' import { RankingType } from 'uniswap/src/data/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'
...@@ -27,18 +26,6 @@ const persistConfig = { ...@@ -27,18 +26,6 @@ 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.
...@@ -53,7 +40,7 @@ const setupStore = (preloadedState?: PreloadedState<ExtensionState>): ReturnType ...@@ -53,7 +40,7 @@ const setupStore = (preloadedState?: PreloadedState<ExtensionState>): ReturnType
additionalSagas: [rootExtensionSaga], additionalSagas: [rootExtensionSaga],
middlewareBefore: __DEV__ ? [loggerMiddleware] : [], middlewareBefore: __DEV__ ? [loggerMiddleware] : [],
middlewareAfter: [fiatOnRampAggregatorApi.middleware], middlewareAfter: [fiatOnRampAggregatorApi.middleware],
enhancers: [sentryReduxEnhancer, dataDogReduxEnhancer], enhancers: [dataDogReduxEnhancer],
}) })
} }
......
...@@ -7,7 +7,6 @@ const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin' ...@@ -7,7 +7,6 @@ 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
...@@ -356,12 +355,6 @@ module.exports = (env) => { ...@@ -356,12 +355,6 @@ 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
---
- extendedWaitUntil:
visible:
id: 'confirm'
timeout: 10000
- tapOn:
id: 'confirm' # are you sure?
- waitForAnimationToEnd
appId: com.uniswap.mobile.dev appId: com.uniswap.mobile.dev
env:
E2E_RECOVERY_PHRASE: ${E2E_RECOVERY_PHRASE}
--- ---
- launchApp: - tapOn: 'Add an existing wallet'
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: 'Skip' - tapOn: 'Import a wallet'
- waitForAnimationToEnd - waitForAnimationToEnd
- tapOn: 'Skip' - inputText: ${E2E_RECOVERY_PHRASE}
- waitForAnimationToEnd - waitForAnimationToEnd
- tapOn: 'Skip' - tapOn: 'Continue'
- extendedWaitUntil:
visible:
id: 'confirm'
- tapOn:
id: 'confirm'
- waitForAnimationToEnd - waitForAnimationToEnd
- tapOn: 'Send' - tapOn: 'Continue'
- tapOn: 'Skip'
- tapOn: 'Skip'
- tapOn: 'Skip'
- runFlow: biometrics-confirm.yaml
- waitForAnimationToEnd - waitForAnimationToEnd
- 'back'
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
...@@ -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 isDetox = System.getenv('DETOX_MODE') != null boolean isE2E = System.getenv('E2E_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 && !isDetox) { if (isCI && sentryPropertiesAvailable && !isE2E) {
project.ext.sentryCli = [ project.ext.sentryCli = [
logLevel: "info", logLevel: "info",
] ]
...@@ -85,13 +85,13 @@ if (isCI && sentryPropertiesAvailable && !isDetox) { ...@@ -85,13 +85,13 @@ if (isCI && sentryPropertiesAvailable && !isDetox) {
boolean datadogPropertiesAvailable = System.getenv('DATADOG_API_KEY') != null boolean datadogPropertiesAvailable = System.getenv('DATADOG_API_KEY') != null
if (isCI && datadogPropertiesAvailable && !isDetox) { if (isCI && datadogPropertiesAvailable && !isE2E) {
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.44" def devVersionName = "1.45"
def betaVersionName = "1.44" def betaVersionName = "1.45"
def prodVersionName = "1.44" def prodVersionName = "1.45"
android { android {
ndkVersion rootProject.ext.ndkVersion ndkVersion rootProject.ext.ndkVersion
...@@ -104,8 +104,6 @@ android { ...@@ -104,8 +104,6 @@ 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 {
...@@ -254,14 +252,17 @@ dependencies { ...@@ -254,14 +252,17 @@ dependencies {
implementation 'com.onesignal:OneSignal:4.8.9' implementation 'com.onesignal:OneSignal:4.8.9'
implementation 'com.github.statsig-io:android-sdk:4.36.0' 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,7 +5,6 @@ ...@@ -5,7 +5,6 @@
<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,6 +9,10 @@ ...@@ -9,6 +9,10 @@
<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"
...@@ -23,6 +27,17 @@ ...@@ -23,6 +27,17 @@
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" />
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
<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"/>
......
...@@ -37,11 +37,6 @@ plugins { ...@@ -37,11 +37,6 @@ 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,6 +8,3 @@ apply from: new File(["node", "--print", "require.resolve('../../../node_modules ...@@ -8,6 +8,3 @@ 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',
}
...@@ -2337,7 +2337,9 @@ PODS: ...@@ -2337,7 +2337,9 @@ 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.0.1): - react-native-image-picker (7.2.3):
- 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)
...@@ -3081,7 +3083,7 @@ SPEC CHECKSUMS: ...@@ -3081,7 +3083,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: 1569cfade34b3a011191ce262423e6ce2f8db5a1 react-native-image-picker: b049e0ea9d6b1b58c06262e19f8b66c87ac7b760
react-native-mmkv: dea675cf9697ad35940f1687e98e133e1358ef9f react-native-mmkv: dea675cf9697ad35940f1687e98e133e1358ef9f
react-native-netinfo: 129bd99f607a2dc5bb096168f3e5c150fd1f1c95 react-native-netinfo: 129bd99f607a2dc5bb096168f3e5c150fd1f1c95
react-native-onesignal: ab800900cffeca4d9db70a05244013fc8a36ceb8 react-native-onesignal: ab800900cffeca4d9db70a05244013fc8a36ceb8
......
...@@ -2230,7 +2230,7 @@ ...@@ -2230,7 +2230,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -2283,7 +2283,7 @@ ...@@ -2283,7 +2283,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
...@@ -2336,7 +2336,7 @@ ...@@ -2336,7 +2336,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
...@@ -2389,7 +2389,7 @@ ...@@ -2389,7 +2389,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
...@@ -2427,7 +2427,7 @@ ...@@ -2427,7 +2427,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -2463,7 +2463,7 @@ ...@@ -2463,7 +2463,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
...@@ -2498,7 +2498,7 @@ ...@@ -2498,7 +2498,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
...@@ -2533,7 +2533,7 @@ ...@@ -2533,7 +2533,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
...@@ -2580,7 +2580,7 @@ ...@@ -2580,7 +2580,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -2626,7 +2626,7 @@ ...@@ -2626,7 +2626,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets;
...@@ -2672,7 +2672,7 @@ ...@@ -2672,7 +2672,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets;
...@@ -2718,7 +2718,7 @@ ...@@ -2718,7 +2718,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets;
...@@ -2760,7 +2760,7 @@ ...@@ -2760,7 +2760,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -2803,7 +2803,7 @@ ...@@ -2803,7 +2803,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension;
...@@ -2846,7 +2846,7 @@ ...@@ -2846,7 +2846,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension;
...@@ -2889,7 +2889,7 @@ ...@@ -2889,7 +2889,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension;
...@@ -2925,7 +2925,7 @@ ...@@ -2925,7 +2925,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -2963,7 +2963,7 @@ ...@@ -2963,7 +2963,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -3164,7 +3164,7 @@ ...@@ -3164,7 +3164,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -3210,7 +3210,7 @@ ...@@ -3210,7 +3210,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension;
...@@ -3322,7 +3322,7 @@ ...@@ -3322,7 +3322,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -3394,7 +3394,7 @@ ...@@ -3394,7 +3394,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension;
...@@ -3506,7 +3506,7 @@ ...@@ -3506,7 +3506,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -3578,12 +3578,12 @@ ...@@ -3578,12 +3578,12 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.44; MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.uniswap.mobile.dev.OneSignalNotificationServiceExtension 1727888864"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.uniswap.mobile.dev.OneSignalNotificationServiceExtension";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
STATSIG_SDK_KEY = ""; STATSIG_SDK_KEY = "";
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;
......
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
<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>
......
...@@ -17,7 +17,6 @@ const workspaceRoot = path.resolve(mobileRoot, '../..') ...@@ -17,7 +17,6 @@ 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)
...@@ -29,8 +28,7 @@ const config = { ...@@ -29,8 +28,7 @@ const config = {
resolver: { resolver: {
nodeModulesPaths: [`${workspaceRoot}/node_modules`], nodeModulesPaths: [`${workspaceRoot}/node_modules`],
assetExts: assetExts.filter((ext) => ext !== 'svg'), assetExts: assetExts.filter((ext) => ext !== 'svg'),
// detox mocking works properly only being spreaded at the beginning of sourceExts array sourceExts: [...sourceExts, 'svg', 'cjs'],
sourceExts: [...detoxExtensions, ...sourceExts, 'svg', 'cjs'],
}, },
transformer: { transformer: {
getTransformOptions: async () => ({ getTransformOptions: async () => ({
...@@ -50,11 +48,14 @@ const config = { ...@@ -50,11 +48,14 @@ 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: process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test', enabled: IS_STORYBOOK_ENABLED,
onDisabledRemoveStorybook: true,
// Path to your storybook config // Path to your storybook config
configPath: path.resolve(__dirname, './.storybook'), configPath: path.resolve(__dirname, './.storybook'),
}) })
...@@ -23,14 +23,6 @@ ...@@ -23,14 +23,6 @@
"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",
...@@ -91,9 +83,9 @@ ...@@ -91,9 +83,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.12", "@uniswap/client-explore": "0.0.14",
"@uniswap/ethers-rs-mobile": "0.0.5", "@uniswap/ethers-rs-mobile": "0.0.5",
"@uniswap/sdk-core": "6.1.0", "@uniswap/sdk-core": "7.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",
...@@ -132,7 +124,7 @@ ...@@ -132,7 +124,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.0.1", "react-native-image-picker": "7.2.3",
"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",
...@@ -182,7 +174,6 @@ ...@@ -182,7 +174,6 @@
"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 } from 'src/app/DatadogProviderWrapper' import { DatadogProviderWrapper, MOBILE_DEFAULT_DATADOG_SESSION_SAMPLE_RATE } 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,10 +47,15 @@ import { uniswapUrls } from 'uniswap/src/constants/urls' ...@@ -47,10 +47,15 @@ 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 { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { getDynamicConfigValue, 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'
...@@ -64,9 +69,10 @@ import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/conte ...@@ -64,9 +69,10 @@ 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, isDetoxBuild } from 'utilities/src/environment/constants' import { datadogEnabled, isE2EMode } 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'
...@@ -92,9 +98,9 @@ if (__DEV__) { ...@@ -92,9 +98,9 @@ if (__DEV__) {
loadErrorMessages() loadErrorMessages()
} }
// Log boxes on simulators can block detox tap event when they cover buttons placed at // Log boxes on simulators can block e2e 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 (isDetoxBuild) { if (isE2EMode) {
LogBox.ignoreAllLogs() LogBox.ignoreAllLogs()
} }
...@@ -104,7 +110,7 @@ initFirebaseAppCheck() ...@@ -104,7 +110,7 @@ initFirebaseAppCheck()
function App(): JSX.Element | null { function App(): JSX.Element | null {
useEffect(() => { useEffect(() => {
if (!__DEV__ && !isDetoxBuild) { if (!__DEV__ && !isE2EMode) {
attachUnhandledRejectionHandler() attachUnhandledRejectionHandler()
setAttributesToDatadog({ buildNumber: DeviceInfo.getBuildNumber() }).catch(() => undefined) setAttributesToDatadog({ buildNumber: DeviceInfo.getBuildNumber() }).catch(() => undefined)
} }
...@@ -121,6 +127,8 @@ function App(): JSX.Element | null { ...@@ -121,6 +127,8 @@ 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
...@@ -134,7 +142,22 @@ function App(): JSX.Element | null { ...@@ -134,7 +142,22 @@ function App(): JSX.Element | null {
api: uniswapUrls.statsigProxyUrl, api: uniswapUrls.statsigProxyUrl,
disableAutoMetricsLogging: true, disableAutoMetricsLogging: true,
disableErrorLogging: true, disableErrorLogging: true,
initCompletionCallback: loadStatsigOverrides, initCompletionCallback: () => {
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: {
...@@ -148,7 +171,7 @@ function App(): JSX.Element | null { ...@@ -148,7 +171,7 @@ function App(): JSX.Element | null {
return ( return (
<StatsigProvider {...statSigOptions}> <StatsigProvider {...statSigOptions}>
<DatadogProviderWrapper> <DatadogProviderWrapper sessionSampleRate={datadogSessionSampleRate}>
<Trace> <Trace>
<StrictMode> <StrictMode>
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
...@@ -183,6 +206,11 @@ function AppOuter(): JSX.Element | null { ...@@ -183,6 +206,11 @@ 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.
...@@ -191,13 +219,13 @@ function AppOuter(): JSX.Element | null { ...@@ -191,13 +219,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, 'application_start_js', { await DdRum.addAction(RumActionType.CUSTOM, DDRumAction.ApplicationStartJs, {
loading_time: report.timeToBootJsMillis, loading_time: report.timeToBootJsMillis,
}) })
jsBundleLoadedRef.current = true jsBundleLoadedRef.current = true
} }
if (report.interactive) { if (report.interactive) {
await DdRum.addTiming('screenInteractive') await DdRum.addTiming(DDRumTiming.ScreenInteractive)
} }
} }
......
import { import {
BatchSize, BatchSize,
DatadogProvider, DatadogProvider,
DatadogProviderConfiguration, 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 } from 'react' import { PropsWithChildren, default as React, useEffect } 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,59 +16,92 @@ import { ...@@ -16,59 +16,92 @@ 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, isDetoxBuild, isJestRun, localDevDatadogEnabled } from 'utilities/src/environment/constants' import { datadogEnabled, isE2EMode, isJestRun, localDevDatadogEnabled } from 'utilities/src/environment/constants'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
const datadogConfig = new DatadogProviderConfiguration( // In case Statsig is not available
config.datadogClientToken, export const MOBILE_DEFAULT_DATADOG_SESSION_SAMPLE_RATE = 10 // percent
getDatadogEnvironment(),
config.datadogProjectId,
datadogEnabled, // trackInteractions
datadogEnabled, // trackResources
datadogEnabled, // trackErrors
localDevDatadogEnabled ? TrackingConsent.GRANTED : undefined,
)
Object.assign(datadogConfig, { // Configuration for Datadog's automatic monitoring features:
site: 'US1', // - Error tracking: Captures and reports application errors
longTaskThresholdMs: 100, // - User interactions: Monitors user events and actions
nativeCrashReportEnabled: true, // - Resource tracking: Traces network requests and API calls
verbosity: SdkVerbosity.INFO, // Note: Can buffer up to 100 RUM events before SDK initialization
errorEventMapper: (event: ReturnType<ErrorEventMapper>) => { // https://docs.datadoghq.com/real_user_monitoring/mobile_and_tv_monitoring/react_native/advanced_configuration/#delaying-the-initialization
const ignoredErrors = getDynamicConfigValue< const datadogAutoInstrumentation = {
DynamicConfigs.DatadogIgnoredErrors, trackErrors: datadogEnabled,
DatadogIgnoredErrorsConfigKey, trackInteractions: datadogEnabled,
DatadogIgnoredErrorsValType trackResources: datadogEnabled,
>(DynamicConfigs.DatadogIgnoredErrors, DatadogIgnoredErrorsConfigKey.Errors, []) }
const ignoredError = ignoredErrors.find(({ messageContains }) => event?.message.includes(messageContains)) async function initializeDatadog(sessionSamplingRate: number | undefined): Promise<void> {
if (ignoredError) { const datadogConfig = {
return Math.random() < ignoredError.sampleRate ? event : null clientToken: config.datadogClientToken,
} env: getDatadogEnvironment(),
applicationId: config.datadogProjectId,
trackingConsent: undefined,
site: 'US1',
longTaskThresholdMs: 100,
nativeCrashReportEnabled: true,
verbosity: SdkVerbosity.INFO,
errorEventMapper: (event: ReturnType<ErrorEventMapper>): ReturnType<ErrorEventMapper> | null => {
const ignoredErrors = getDynamicConfigValue<
DynamicConfigs.DatadogIgnoredErrors,
DatadogIgnoredErrorsConfigKey,
DatadogIgnoredErrorsValType
>(DynamicConfigs.DatadogIgnoredErrors, DatadogIgnoredErrorsConfigKey.Errors, [])
return event const ignoredError = ignoredErrors.find(({ messageContains }) => event?.message.includes(messageContains))
}, if (ignoredError) {
}) return Math.random() < ignoredError.sampleRate ? event : null
}
if (localDevDatadogEnabled) { return event
Object.assign(datadogConfig, { },
sessionSamplingRate: 100, sessionSamplingRate,
resourceTracingSamplingRate: 100, }
uploadFrequency: UploadFrequency.FREQUENT,
batchSize: BatchSize.SMALL, if (localDevDatadogEnabled) {
verbosity: SdkVerbosity.DEBUG, Object.assign(datadogConfig, {
}) sessionSamplingRate: 100,
uploadFrequency: UploadFrequency.FREQUENT,
batchSize: BatchSize.SMALL,
verbosity: SdkVerbosity.DEBUG,
trackingConsent: TrackingConsent.GRANTED,
})
}
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({ children }: PropsWithChildren): JSX.Element { export function DatadogProviderWrapper({
logger.setWalletDatadogEnabled(true) children,
sessionSampleRate,
}: PropsWithChildren<{ sessionSampleRate: number | undefined }>): JSX.Element {
useEffect(() => {
if (datadogEnabled && sessionSampleRate !== undefined) {
initializeDatadog(sessionSampleRate).catch(() => undefined)
}
}, [sessionSampleRate])
if (isDetoxBuild || isJestRun) { if (isE2EMode || isJestRun) {
return <>{children}</> return <>{children}</>
} }
return <DatadogProvider configuration={datadogConfig}>{children}</DatadogProvider> logger.setWalletDatadogEnabled(true)
return (
<DatadogProvider
configuration={datadogAutoInstrumentation}
onInitialization={async () => {
const sessionId = await DdRum.getCurrentSessionId()
// we do not want to log anything if session is not sampled
logger.setWalletDatadogEnabled(sessionId !== undefined)
}}
>
{children}
</DatadogProvider>
)
} }
...@@ -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/HomeScreenTabIndex' import { HomeScreenTabIndex } from 'src/screens/HomeScreen/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/HomeScreenTabIndex' import { HomeScreenTabIndex } from 'src/screens/HomeScreen/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' import { HomeScreen } from 'src/screens/HomeScreen/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'
......
...@@ -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/HomeScreenTabIndex' import { HomeScreenTabIndex } from 'src/screens/HomeScreen/HomeScreenTabIndex'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { import {
FiatOnRampScreens, FiatOnRampScreens,
......
/* 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 { isDetoxBuild } from 'utilities/src/environment/constants' import { isE2EMode } 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) && !isDetoxBuild (selectedDuration === HistoryDuration.Day || selectedDuration === HistoryDuration.Hour) && !isE2EMode
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
...@@ -13,7 +14,7 @@ query TokenPriceHistory( ...@@ -13,7 +14,7 @@ query TokenPriceHistory(
pricePercentChange24h { pricePercentChange24h {
value value
} }
priceHistory(duration: $duration) { priceHistory(duration: $duration, maxLength: $maxHistoryLength) {
timestamp timestamp
value value
} }
...@@ -32,7 +33,7 @@ query TokenPriceHistory( ...@@ -32,7 +33,7 @@ query TokenPriceHistory(
pricePercentChange24h: pricePercentChange(duration: DAY) { pricePercentChange24h: pricePercentChange(duration: DAY) {
value value
} }
priceHistory(duration: $duration) { priceHistory(duration: $duration, maxLength: $maxHistoryLength) {
timestamp timestamp
value value
} }
......
...@@ -14,8 +14,7 @@ export const TIME_RANGES = [ ...@@ -14,8 +14,7 @@ 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],
// TODO (MOB-3585): fix performance issue with All time range and re-enable [HistoryDuration.Max, i18n.t('common.all'), ElementName.TimeFrameAll],
// [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 from 'react' import React, { PropsWithChildren } 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 { useENS } from 'uniswap/src/features/ens/useENS' import { useENSName } from 'uniswap/src/features/ens/api'
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 { name } = useENS(chainId, address, false) const { data: name } = useENSName(address)
const colors = useSporeColors() const colors = useSporeColors()
const { defaultChainId } = useEnabledChains() const { defaultChainId } = useEnabledChains()
const supportedChainId = toSupportedChainId(chainId) ?? defaultChainId const supportedChainId = toSupportedChainId(chainId) ?? defaultChainId
...@@ -55,6 +55,23 @@ const AddressButton = ({ address, chainId, ...rest }: AddressButtonProps): JSX.E ...@@ -55,6 +55,23 @@ 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
...@@ -64,45 +81,31 @@ const getParsedObjectDisplay = (chainId: number, obj: any, depth = 0): JSX.Eleme ...@@ -64,45 +81,31 @@ 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]
if (typeof childValue === 'object') { // Special case for address strings
return ( if (typeof childValue === 'string' && getValidAddress(childValue, true)) {
<Flex key={objKey} gap="$spacing4">
<Text color="$neutral2" variant="body3">
{objKey}
</Text>
{getParsedObjectDisplay(chainId, childValue, depth + 1)}
</Flex>
)
}
if (typeof childValue === 'string') {
return ( return (
<Flex key={objKey} row alignItems="flex-start" gap="$spacing8"> <KeyValueRow key={objKey} objKey={objKey}>
<Text color="$neutral2" py="$spacing4" variant="body3"> <Flex>
{objKey} <AddressButton address={childValue} chainId={chainId} textVariant="body3" />
</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>
</Flex> </KeyValueRow>
) )
} }
// TODO: [MOB-216] handle array child types return (
return null <KeyValueRow key={objKey} objKey={objKey}>
{getParsedObjectDisplay(chainId, childValue, depth + 1)}
</KeyValueRow>
)
})} })}
</Flex> </Flex>
) )
......
...@@ -21,6 +21,8 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' ...@@ -21,6 +21,8 @@ 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'
...@@ -76,6 +78,8 @@ export const TokenBalanceListInner = forwardRef<FlatList<TokenBalanceListRow>, T ...@@ -76,6 +78,8 @@ 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, TouchableArea } from 'ui/src' import { Flex, flexStyles, Text } 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 tokenProtectionEnabled = useFeatureFlag(FeatureFlags.TokenProtection) const { currencyId } = useTokenDetailsContext()
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
...@@ -44,12 +33,6 @@ export const TokenDetailsHeader = memo(function _TokenDetailsHeader(): JSX.Eleme ...@@ -44,12 +33,6 @@ 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,10 +55,17 @@ const TokenDetailsMarketData = memo(function _TokenDetailsMarketData(): JSX.Elem ...@@ -55,10 +55,17 @@ 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 priceHight52W = projectMarkets?.[0]?.priceHigh52W?.value ?? tokenMarket?.priceHigh52W?.value const rawPriceHigh52W = projectMarkets?.[0]?.priceHigh52W?.value || tokenMarket?.priceHigh52W?.value || undefined
const priceLow52W = projectMarkets?.[0]?.priceLow52W?.value ?? tokenMarket?.priceLow52W?.value const rawPriceLow52W = projectMarkets?.[0]?.priceLow52W?.value || tokenMarket?.priceLow52W?.value || undefined
// 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 (
...@@ -136,6 +143,7 @@ export const TokenDetailsStats = memo(function _TokenDetailsStats(): JSX.Element ...@@ -136,6 +143,7 @@ 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,
}, },
...@@ -150,6 +158,7 @@ export const TokenDetailsStats = memo(function _TokenDetailsStats(): JSX.Element ...@@ -150,6 +158,7 @@ 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={true} showWarnings={false}
tokenWarningDismissed={tokenWarningDismissed} tokenWarningDismissed={tokenWarningDismissed}
onPress={onPress} onPress={onPress}
/> />
......
...@@ -74,12 +74,12 @@ describe('FavoriteTokenCard', () => { ...@@ -74,12 +74,12 @@ describe('FavoriteTokenCard', () => {
it('renders loader', async () => { it('renders loader', async () => {
const { queryByTestId } = render(<FavoriteTokenCard {...defaultProps} />, { resolvers }) const { queryByTestId } = render(<FavoriteTokenCard {...defaultProps} />, { resolvers })
const loader = queryByTestId('loader/favorite') const loaderPrice = queryByTestId('loader/favorite/price')
const loaderPriceChange = queryByTestId('loader/favorite/priceChange')
// loading expect(loaderPrice).toBeTruthy()
expect(loader).toBeTruthy() expect(loaderPriceChange).toBeTruthy()
// loading finished
await waitFor(() => { await waitFor(() => {
expect(queryByTestId(touchableId)).toBeTruthy() expect(queryByTestId(touchableId)).toBeTruthy()
}) })
......
...@@ -6,12 +6,11 @@ import { useDispatch } from 'react-redux' ...@@ -6,12 +6,11 @@ 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, Text, useIsDarkMode, useShadowPropsShort, useSporeColors } from 'ui/src' import { AnimatedTouchableArea, Flex, Loader, 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, imageSizes, opacify } from 'ui/src/theme' import { borderRadii, fonts, 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 {
...@@ -103,9 +102,7 @@ function FavoriteTokenCard({ ...@@ -103,9 +102,7 @@ function FavoriteTokenCard({
const shadowProps = useShadowPropsShort() const shadowProps = useShadowPropsShort()
if (isNonPollingRequestInFlight(networkStatus)) { const priceLoading = isNonPollingRequestInFlight(networkStatus)
return <Loader.Favorite height={FAVORITE_TOKEN_CARD_LOADER_HEIGHT} />
}
return ( return (
<AnimatedFlex borderRadius="$rounded16" style={animatedDragStyle}> <AnimatedFlex borderRadius="$rounded16" style={animatedDragStyle}>
...@@ -143,15 +140,31 @@ function FavoriteTokenCard({ ...@@ -143,15 +140,31 @@ function FavoriteTokenCard({
<RemoveButton visible={isEditing} onPress={onRemove} /> <RemoveButton visible={isEditing} onPress={onRemove} />
</Flex> </Flex>
<Flex gap="$spacing2"> <Flex gap="$spacing2">
<Text adjustsFontSizeToFit numberOfLines={1} variant="heading3"> {priceLoading ? (
{priceFormatted} <Loader.Box
</Text> height={fonts.heading3.lineHeight}
<RelativeChange width={fonts.heading3.lineHeight * 3}
arrowSize="$icon.16" testID="loader/favorite/price"
change={pricePercentChange ?? undefined} />
semanticColor={true} ) : (
variant="subheading2" <Text adjustsFontSizeToFit numberOfLines={1} variant="heading3">
/> {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>
......
...@@ -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 m="$spacing4" style={ITEM_FLEX}> <Flex mx="$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 m="$spacing4" style={ITEM_FLEX}> <Flex mx="$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, ExploreOrderBy, RankingType } from 'wallet/src/features/wallet/types' import { CustomRankingType, RankingType } from 'uniswap/src/data/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,11 +15,12 @@ import { ...@@ -15,11 +15,12 @@ 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 { CustomRankingType, ExploreOrderBy, RankingType } from 'wallet/src/features/wallet/types' import { ExploreOrderBy } 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(UniverseChainId.Mainnet, querySkippedIfValidAddress, true) } = useENS({ nameOrAddress: querySkippedIfValidAddress, autocompleteDomain: 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(UniverseChainId.Mainnet, querySkippedIfValidAddress, false) } = useENS({ nameOrAddress: querySkippedIfValidAddress, autocompleteDomain: 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,6 +12,8 @@ import { Flex, useSporeColors } from 'ui/src' ...@@ -12,6 +12,8 @@ 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'
...@@ -57,6 +59,8 @@ export const ActivityTab = memo( ...@@ -57,6 +59,8 @@ export const ActivityTab = memo(
onPressEmptyState: onPressReceive, onPressEmptyState: onPressReceive,
}) })
usePerformanceLogger(DDRumManualTiming.RenderActivityTabList, [])
const refreshControl = useMemo(() => { const refreshControl = useMemo(() => {
return ( return (
<RefreshControl <RefreshControl
......
...@@ -7,10 +7,13 @@ import { FundWalletModal } from 'src/components/home/introCards/FundWalletModal' ...@@ -7,10 +7,13 @@ 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 {
...@@ -21,16 +24,17 @@ import { ...@@ -21,16 +24,17 @@ 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
hasTokens: boolean showEmptyWalletState: boolean
} }
export function OnboardingIntroCardStack({ export function OnboardingIntroCardStack({
hasTokens, showEmptyWalletState,
isLoading = false, isLoading = false,
}: OnboardingIntroCardStackProps): JSX.Element | null { }: OnboardingIntroCardStackProps): JSX.Element | null {
const { t } = useTranslation() const { t } = useTranslation()
...@@ -43,6 +47,8 @@ export function OnboardingIntroCardStack({ ...@@ -43,6 +47,8 @@ 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,
...@@ -62,14 +68,15 @@ export function OnboardingIntroCardStack({ ...@@ -62,14 +68,15 @@ 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({
hasTokens, showUnichainModal: () => setShowUnichainIntroModal(true),
navigateToUnitagClaim, navigateToUnitagClaim,
navigateToUnitagIntro, navigateToUnitagIntro,
}) })
const [showFundModal, setShowFundModal] = useState(false)
const cards = useMemo((): IntroCardProps[] => { const cards = useMemo((): IntroCardProps[] => {
const output: IntroCardProps[] = [] const output: IntroCardProps[] = []
...@@ -78,7 +85,7 @@ export function OnboardingIntroCardStack({ ...@@ -78,7 +85,7 @@ export function OnboardingIntroCardStack({
return output return output
} }
if (!hasTokens) { if (showEmptyWalletState) {
output.push({ output.push({
loggingName: OnboardingCardLoggingName.FundWallet, loggingName: OnboardingCardLoggingName.FundWallet,
graphic: { graphic: {
...@@ -142,7 +149,7 @@ export function OnboardingIntroCardStack({ ...@@ -142,7 +149,7 @@ export function OnboardingIntroCardStack({
} }
return output return output
}, [hasBackups, hasTokens, hasViewedWelcomeWalletCard, isSignerAccount, sharedCards, t, welcomeCardTitle]) }, [hasBackups, showEmptyWalletState, hasViewedWelcomeWalletCard, isSignerAccount, sharedCards, t, welcomeCardTitle])
const handleSwiped = useCallback( const handleSwiped = useCallback(
(_card: IntroCardProps, index: number) => { (_card: IntroCardProps, index: number) => {
...@@ -160,15 +167,31 @@ export function OnboardingIntroCardStack({ ...@@ -160,15 +167,31 @@ 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 }
...@@ -12,11 +11,10 @@ const iconsCache = new Map<UniverseChainId, React.FC<IconComponentProps>>() ...@@ -12,11 +11,10 @@ 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 isDarkMode = useIsDarkMode() const Logo = useBlockExplorerLogo(chainId)
return isDarkMode ? <explorerLogos.logoDark size={size} /> : <explorerLogos.logoLight size={size} /> return <Logo size={size} />
} }
Component.displayName = `BlockExplorerIcon_${explorer.name}` Component.displayName = `BlockExplorerIcon_${explorer.name}`
iconsCache.set(chainId, Component) iconsCache.set(chainId, Component)
......
import React, { useEffect } from 'react'
import { ViewProps } from 'react-native'
import { Flex, flexStyles, HiddenFromScreenReaders, Text } from 'ui/src'
type MnemonicConfirmationProps = ViewProps & {
mnemonicId: Address
onConfirmComplete: () => void
}
/**
* Replaces MnemonicConfirmation native screen during e2e testing because detox do not support
* native components
*/
export function MnemonicConfirmation(props: MnemonicConfirmationProps): JSX.Element {
useEffect(() => {
props.onConfirmComplete()
}, [props])
return (
<HiddenFromScreenReaders style={flexStyles.fill}>
<Flex centered>
<Text variant="body1">Mocked confirmation screen</Text>
</Flex>
</HiddenFromScreenReaders>
)
}
...@@ -4,7 +4,8 @@ import { call, put } from 'typed-redux-saga' ...@@ -4,7 +4,8 @@ import { call, put } from 'typed-redux-saga'
import { AssetType, TradeableAsset } from 'uniswap/src/entities/assets' import { AssetType, TradeableAsset } from 'uniswap/src/entities/assets'
import { UniverseChainId } from 'uniswap/src/features/chains/types' import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { FiatOffRampMetaData, OffRampTransferDetailsResponse } from 'uniswap/src/features/fiatOnRamp/types' import { FiatOffRampMetaData, OffRampTransferDetailsResponse } from 'uniswap/src/features/fiatOnRamp/types'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { FiatOffRampEventName, ModalName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { TransactionScreen } from 'uniswap/src/features/transactions/TransactionModal/TransactionModalContext' import { TransactionScreen } from 'uniswap/src/features/transactions/TransactionModal/TransactionModalContext'
import { forceFetchFiatOnRampTransactions } from 'uniswap/src/features/transactions/slice' import { forceFetchFiatOnRampTransactions } from 'uniswap/src/features/transactions/slice'
import { CurrencyField } from 'uniswap/src/types/currency' import { CurrencyField } from 'uniswap/src/types/currency'
...@@ -26,15 +27,27 @@ export function* handleOffRampReturnLink(url: URL) { ...@@ -26,15 +27,27 @@ export function* handleOffRampReturnLink(url: URL) {
function* _handleOffRampReturnLink(url: URL) { function* _handleOffRampReturnLink(url: URL) {
const externalTransactionId = url.searchParams.get('externalTransactionId') const externalTransactionId = url.searchParams.get('externalTransactionId')
const currencyCode = url.searchParams.get('baseCurrencyCode')
const currencyAmount = url.searchParams.get('baseCurrencyAmount')
const walletAddress = url.searchParams.get('depositWalletAddress')
if (!externalTransactionId) { const hasValidMoonpayData = currencyCode && currencyAmount && walletAddress
throw new Error('Missing externalTransactionId in fiat offramp deep link') if (!externalTransactionId && !hasValidMoonpayData) {
throw new Error('Missing externalTransactionId or moonpay data in fiat offramp deep link')
} }
sendAnalyticsEvent(FiatOffRampEventName.FiatOffRampWidgetCompleted, { externalTransactionId })
let offRampTransferDetails: OffRampTransferDetailsResponse | undefined let offRampTransferDetails: OffRampTransferDetailsResponse | undefined
try { try {
offRampTransferDetails = yield* call(fetchOffRampTransferDetails, externalTransactionId) offRampTransferDetails = yield* call(
fetchOffRampTransferDetails,
externalTransactionId,
currencyCode,
Number(currencyAmount),
walletAddress,
)
} catch (error) { } catch (error) {
logger.error(error, { logger.error(error, {
tags: { file: 'handleOffRampReturnLinkSaga', function: 'handleOffRampReturnLink' }, tags: { file: 'handleOffRampReturnLinkSaga', function: 'handleOffRampReturnLink' },
...@@ -58,8 +71,15 @@ function* _handleOffRampReturnLink(url: URL) { ...@@ -58,8 +71,15 @@ function* _handleOffRampReturnLink(url: URL) {
const fiatOffRampMetaData: FiatOffRampMetaData = { const fiatOffRampMetaData: FiatOffRampMetaData = {
name: provider, name: provider,
logoUrl: logos.lightLogo, logoUrl: logos.lightLogo,
// TODO: update activity feed once transaction is submitted onSubmitCallback: () => {
onSubmitCallback: () => {}, sendAnalyticsEvent(FiatOffRampEventName.FiatOffRampFundsSent, {
cryptoCurrency: baseCurrencyCode,
currencyAmount: baseCurrencyAmount,
serviceProvider: provider,
chainId,
externalTransactionId,
})
},
moonpayCurrencyCode: baseCurrencyCode, moonpayCurrencyCode: baseCurrencyCode,
meldCurrencyCode: baseCurrencyCode, meldCurrencyCode: baseCurrencyCode,
} }
......
...@@ -2,7 +2,7 @@ import { call, put } from '@redux-saga/core/effects' ...@@ -2,7 +2,7 @@ import { call, put } from '@redux-saga/core/effects'
import { expectSaga } from 'redux-saga-test-plan' import { expectSaga } from 'redux-saga-test-plan'
import { navigate } from 'src/app/navigation/rootNavigation' import { navigate } from 'src/app/navigation/rootNavigation'
import { handleOnRampReturnLink } from 'src/features/deepLinking/handleOnRampReturnLinkSaga' import { handleOnRampReturnLink } from 'src/features/deepLinking/handleOnRampReturnLinkSaga'
import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex' import { HomeScreenTabIndex } from 'src/screens/HomeScreen/HomeScreenTabIndex'
import { forceFetchFiatOnRampTransactions } from 'uniswap/src/features/transactions/slice' import { forceFetchFiatOnRampTransactions } from 'uniswap/src/features/transactions/slice'
import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { dismissInAppBrowser } from 'wallet/src/utils/linking' import { dismissInAppBrowser } from 'wallet/src/utils/linking'
......
import { navigate } from 'src/app/navigation/rootNavigation' import { navigate } from 'src/app/navigation/rootNavigation'
import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex' import { HomeScreenTabIndex } from 'src/screens/HomeScreen/HomeScreenTabIndex'
import { call, put } from 'typed-redux-saga' import { call, put } from 'typed-redux-saga'
import { forceFetchFiatOnRampTransactions } from 'uniswap/src/features/transactions/slice' import { forceFetchFiatOnRampTransactions } from 'uniswap/src/features/transactions/slice'
import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens } from 'uniswap/src/types/screens/mobile'
......
...@@ -15,9 +15,9 @@ import { logger } from 'utilities/src/logger/logger' ...@@ -15,9 +15,9 @@ import { logger } from 'utilities/src/logger/logger'
* Opens swap modal with the provided swap link parameters; prompts testnet switch modal if necessary. * Opens swap modal with the provided swap link parameters; prompts testnet switch modal if necessary.
* *
* Testing deep links: * Testing deep links:
* Testnet mode – https://uniswap.org/mobile-redirect?screen=swap&userAddress=<YOUR_WALET_ADDRESS>&inputCurrencyId=41454-0x93EACdB111FF98dE9a8Ac5823d357BBc4842aE63&outputCurrencyId=41454-0xF5A8061bB2C5D9Dc9bC9c5C633D870DAC7bD351e&currencyField=output&amount=100000 * Testnet mode – https://uniswap.org/mobile-redirect?screen=swap&userAddress=<YOUR_WALET_ADDRESS>&inputCurrencyId=10143-0x760AfE86e5de5fa0Ee542fc7B7B713e1c5425701&outputCurrencyId=10143-0xF5A8061bB2C5D9Dc9bC9c5C633D870DAC7bD351e&currencyField=output&amount=100000
* Prod mode – https://uniswap.org/mobile-redirect?screen=swap&userAddress=<YOUR_WALET_ADDRESS>&inputCurrencyId=1-0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee&outputCurrencyId=10-0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee&currencyField=output&amount=100000 * Prod mode – https://uniswap.org/mobile-redirect?screen=swap&userAddress=<YOUR_WALET_ADDRESS>&inputCurrencyId=1-0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee&outputCurrencyId=10-0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee&currencyField=output&amount=100000
* Mixed – https://uniswap.org/mobile-redirect?screen=swap&userAddress=<YOUR_WALET_ADDRESS>&inputCurrencyId=1-0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee&outputCurrencyId=41454-0xF5A8061bB2C5D9Dc9bC9c5C633D870DAC7bD351e&currencyField=output&amount=100000 * Mixed – https://uniswap.org/mobile-redirect?screen=swap&userAddress=<YOUR_WALET_ADDRESS>&inputCurrencyId=1-0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee&outputCurrencyId=10143-0xF5A8061bB2C5D9Dc9bC9c5C633D870DAC7bD351e&currencyField=output&amount=100000
* *
* @param url - URL object containing the swap link * @param url - URL object containing the swap link
*/ */
......
...@@ -3,7 +3,7 @@ import { expectSaga } from 'redux-saga-test-plan' ...@@ -3,7 +3,7 @@ import { expectSaga } from 'redux-saga-test-plan'
import { navigate } from 'src/app/navigation/rootNavigation' import { navigate } from 'src/app/navigation/rootNavigation'
import { handleTransactionLink } from 'src/features/deepLinking/handleTransactionLinkSaga' import { handleTransactionLink } from 'src/features/deepLinking/handleTransactionLinkSaga'
import { closeAllModals } from 'src/features/modals/modalSlice' import { closeAllModals } from 'src/features/modals/modalSlice'
import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex' import { HomeScreenTabIndex } from 'src/screens/HomeScreen/HomeScreenTabIndex'
import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens } from 'uniswap/src/types/screens/mobile'
describe(handleTransactionLink, () => { describe(handleTransactionLink, () => {
......
import { navigate } from 'src/app/navigation/rootNavigation' import { navigate } from 'src/app/navigation/rootNavigation'
import { closeAllModals } from 'src/features/modals/modalSlice' import { closeAllModals } from 'src/features/modals/modalSlice'
import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex' import { HomeScreenTabIndex } from 'src/screens/HomeScreen/HomeScreenTabIndex'
import { call, put } from 'typed-redux-saga' import { call, put } from 'typed-redux-saga'
import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens } from 'uniswap/src/types/screens/mobile'
......
import { AppTFunction } from 'ui/src/i18n/types' import { AppTFunction } from 'ui/src/i18n/types'
import { import { CustomRankingType, RankingType } from 'uniswap/src/data/types'
CustomRankingType, import { ExploreOrderBy, TokenMetadataDisplayType } from 'wallet/src/features/wallet/types'
ExploreOrderBy,
RankingType,
TokenMetadataDisplayType,
} from 'wallet/src/features/wallet/types'
export function getTokenMetadataDisplayType(orderBy: ExploreOrderBy): TokenMetadataDisplayType { export function getTokenMetadataDisplayType(orderBy: ExploreOrderBy): TokenMetadataDisplayType {
switch (orderBy) { switch (orderBy) {
......
...@@ -20,13 +20,13 @@ import { useLocalizationContext } from 'uniswap/src/features/language/Localizati ...@@ -20,13 +20,13 @@ import { useLocalizationContext } from 'uniswap/src/features/language/Localizati
import { usePrevious } from 'utilities/src/react/hooks' import { usePrevious } from 'utilities/src/react/hooks'
import { DEFAULT_DELAY, useDebounce } from 'utilities/src/time/timing' import { DEFAULT_DELAY, useDebounce } from 'utilities/src/time/timing'
const MAX_INPUT_FONT_SIZE = 56 const MAX_INPUT_FONT_SIZE = 52
const MIN_INPUT_FONT_SIZE = 32 const MIN_INPUT_FONT_SIZE = 32
const MIN_SCREEN_HEIGHT = 667 // iPhone SE 3rd Gen const MIN_SCREEN_HEIGHT = 667 // iPhone SE 3rd Gen
// if font changes from `fontFamily.sansSerif.regular` or `MAX_INPUT_FONT_SIZE` // if font changes from `fontFamily.sansSerif.regular` or `MAX_INPUT_FONT_SIZE`
// changes from 36 then width value must be adjusted // changes from 46 then width value must be adjusted
const MAX_CHAR_PIXEL_WIDTH = 44 const MAX_CHAR_PIXEL_WIDTH = 46
const PREDEFINED_ONRAMP_AMOUNTS = [100, 300, 1000] const PREDEFINED_ONRAMP_AMOUNTS = [100, 300, 1000]
const PREDEFINED_OFFRAMP_PERCENTAGES = [25, 50, 75] const PREDEFINED_OFFRAMP_PERCENTAGES = [25, 50, 75]
...@@ -226,7 +226,7 @@ export const FiatOnRampAmountSection = forwardRef<FiatOnRampAmountSectionRef, Fi ...@@ -226,7 +226,7 @@ export const FiatOnRampAmountSection = forwardRef<FiatOnRampAmountSectionRef, Fi
height={fontSize} height={fontSize}
lineHeight={fontSize} lineHeight={fontSize}
> >
{isTokenInputMode ? currency.currencyInfo?.currency.symbol : fiatCurrencyInfo.symbol} {isTokenInputMode ? ' ' + currency.currencyInfo?.currency.symbol : fiatCurrencyInfo.symbol}
</Text> </Text>
<AmountInput <AmountInput
ref={inputRef} ref={inputRef}
...@@ -251,7 +251,7 @@ export const FiatOnRampAmountSection = forwardRef<FiatOnRampAmountSectionRef, Fi ...@@ -251,7 +251,7 @@ export const FiatOnRampAmountSection = forwardRef<FiatOnRampAmountSectionRef, Fi
minWidth={calculatedInputWidth} minWidth={calculatedInputWidth}
returnKeyType={undefined} returnKeyType={undefined}
showSoftInputOnFocus={false} showSoftInputOnFocus={false}
textAlign="left" textAlign={isTokenInputMode ? 'right' : 'left'}
value={value} value={value}
onChangeText={onEnterAmount} onChangeText={onEnterAmount}
onSelectionChange={onSelectionChange} onSelectionChange={onSelectionChange}
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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