ci(release): publish latest release

parent 922a412c
IPFS hash of the deployment:
- CIDv0: `QmSFSxn2NQ8LP2HZckSX6WkmwGz9SQudXWqsQ9ShJKRZSb`
- CIDv1: `bafybeib2dgh2xulteyu7s2lwepf2oyuwf63k3cpxeb5ogcnemn3cdsmpjq`
- CIDv0: `QmQCnXjjekxSd3k2zzpWjh85rA7sgg1R3RiiiGMTpVgNE6`
- CIDv1: `bafybeia3wojcisyfrr4bcavcvvrza2e2jxcoqo557gmewxmgx4gcv7msum`
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.
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://bafybeib2dgh2xulteyu7s2lwepf2oyuwf63k3cpxeb5ogcnemn3cdsmpjq.ipfs.dweb.link/
- https://bafybeib2dgh2xulteyu7s2lwepf2oyuwf63k3cpxeb5ogcnemn3cdsmpjq.ipfs.cf-ipfs.com/
- [ipfs://QmSFSxn2NQ8LP2HZckSX6WkmwGz9SQudXWqsQ9ShJKRZSb/](ipfs://QmSFSxn2NQ8LP2HZckSX6WkmwGz9SQudXWqsQ9ShJKRZSb/)
- https://bafybeia3wojcisyfrr4bcavcvvrza2e2jxcoqo557gmewxmgx4gcv7msum.ipfs.dweb.link/
- https://bafybeia3wojcisyfrr4bcavcvvrza2e2jxcoqo557gmewxmgx4gcv7msum.ipfs.cf-ipfs.com/
- [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
* **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
\ No newline at end of file
web/5.66.0
\ No newline at end of file
......@@ -25,6 +25,3 @@ dist-ssr
*.sw?
.tamagui
# Sentry Config File
.env.sentry-build-plugin
......@@ -9,17 +9,14 @@
"@ethersproject/providers": "5.7.2",
"@metamask/rpc-errors": "6.2.1",
"@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",
"@tamagui/core": "1.114.4",
"@types/uuid": "9.0.1",
"@uniswap/analytics-events": "2.40.0",
"@uniswap/uniswapx-sdk": "3.0.0-beta.1",
"@uniswap/universal-router-sdk": "4.7.0",
"@uniswap/v3-sdk": "3.19.0",
"@uniswap/v4-sdk": "1.12.0",
"@uniswap/universal-router-sdk": "4.10.0",
"@uniswap/v3-sdk": "3.21.0",
"@uniswap/v4-sdk": "1.15.0",
"dotenv-webpack": "8.0.1",
"ethers": "5.7.2",
"eventemitter3": "5.0.1",
......
......@@ -2,6 +2,8 @@ body,
html {
height: 100%;
max-width: 100vw;
font-feature-settings: 'liga' 0;
font-variant-ligatures: no-contextual;
}
#root {
......
......@@ -4,11 +4,12 @@ import 'symbol-observable' // Needed by `reduxed-chrome-storage` as polyfill, or
import { useEffect } from 'react'
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 { ExtensionStatsigProvider } from 'src/app/StatsigProvider'
import { GraphqlProvider } from 'src/app/apollo'
import { ErrorElement } from 'src/app/components/ErrorElement'
import { DatadogAppNameTag } from 'src/app/datadog'
import { ClaimUnitagScreen } from 'src/app/features/onboarding/ClaimUnitagScreen'
import { Complete } from 'src/app/features/onboarding/Complete'
import {
......@@ -34,7 +35,6 @@ import { ScanToOnboard } from 'src/app/features/onboarding/scan/ScanToOnboard'
import { ScantasticContextProvider } from 'src/app/features/onboarding/scan/ScantasticContextProvider'
import { OnboardingRoutes, TopLevelRoutes } from 'src/app/navigation/constants'
import { setRouter, setRouterState } from 'src/app/navigation/state'
import { SentryAppNameTag, sentryCreateHashRouter } from 'src/app/sentry'
import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { checksIfSupportsSidePanel } from 'src/app/utils/chrome'
import { PrimaryAppInstanceDebuggerLazy } from 'src/store/PrimaryAppInstanceDebuggerLazy'
......@@ -135,7 +135,7 @@ const allRoutes = [
},
]
const router = sentryCreateHashRouter([
const router = createHashRouter([
{
path: `/${TopLevelRoutes.Onboarding}`,
element: <OnboardingWrapper />,
......@@ -188,7 +188,7 @@ export default function OnboardingApp(): JSX.Element {
return (
<Trace>
<PersistGate persistor={getReduxPersistor()}>
<ExtensionStatsigProvider appName={SentryAppNameTag.Onboarding}>
<ExtensionStatsigProvider appName={DatadogAppNameTag.Onboarding}>
<I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}>
<ErrorBoundary>
......
......@@ -4,14 +4,14 @@ import 'src/app/Global.css'
import { useEffect } from 'react'
import { I18nextProvider, useTranslation } from 'react-i18next'
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 { ExtensionStatsigProvider } from 'src/app/StatsigProvider'
import { GraphqlProvider } from 'src/app/apollo'
import { ErrorElement } from 'src/app/components/ErrorElement'
import { TraceUserProperties } from 'src/app/components/Trace/TraceUserProperties'
import { DatadogAppNameTag } from 'src/app/datadog'
import { DappContextProvider } from 'src/app/features/dapp/DappContext'
import { SentryAppNameTag, initializeSentry, sentryCreateHashRouter } from 'src/app/sentry'
import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { getReduxPersistor, getReduxStore } from 'src/store/store'
import { DeprecatedButton, Flex, Image, Text } from 'ui/src'
......@@ -25,23 +25,11 @@ import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context'
import i18n from 'uniswap/src/i18n'
import { ExtensionScreens } from 'uniswap/src/types/screens/extension'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger'
import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks'
import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider'
getUniqueId()
.then((userId) => {
initializeSentry(SentryAppNameTag.Popup, userId)
})
.catch((error) => {
logger.error(error, {
tags: { file: 'PopupApp.tsx', function: 'getUniqueId' },
})
})
const router = sentryCreateHashRouter([
const router = createHashRouter([
{
path: '',
element: <PopupContent />,
......@@ -128,7 +116,7 @@ export default function PopupApp(): JSX.Element {
return (
<Trace>
<PersistGate persistor={getReduxPersistor()}>
<ExtensionStatsigProvider appName={SentryAppNameTag.Popup}>
<ExtensionStatsigProvider appName={DatadogAppNameTag.Popup}>
<I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}>
<ErrorBoundary>
......
......@@ -4,12 +4,13 @@ import 'src/app/Global.css'
import { useEffect, useRef, useState } from 'react'
import { I18nextProvider } from 'react-i18next'
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 { ExtensionStatsigProvider } from 'src/app/StatsigProvider'
import { GraphqlProvider } from 'src/app/apollo'
import { ErrorElement } from 'src/app/components/ErrorElement'
import { TraceUserProperties } from 'src/app/components/Trace/TraceUserProperties'
import { DatadogAppNameTag } from 'src/app/datadog'
import { AccountSwitcherScreen } from 'src/app/features/accounts/AccountSwitcherScreen'
import { DappContextProvider } from 'src/app/features/dapp/DappContext'
import { addRequest } from 'src/app/features/dappRequests/saga'
......@@ -29,7 +30,6 @@ import { useIsWalletUnlocked } from 'src/app/hooks/useIsWalletUnlocked'
import { AppRoutes, RemoveRecoveryPhraseRoutes, SettingsRoutes } from 'src/app/navigation/constants'
import { MainContent, WebNavigation } from 'src/app/navigation/navigation'
import { setRouter, setRouterState } from 'src/app/navigation/state'
import { SentryAppNameTag, initializeSentry, sentryCreateHashRouter } from 'src/app/sentry'
import { initExtensionAnalytics } from 'src/app/utils/analytics'
import {
DappBackgroundPortChannel,
......@@ -47,7 +47,6 @@ import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { UnitagUpdaterContextProvider, useUnitagUpdater } from 'uniswap/src/features/unitags/context'
import i18n from 'uniswap/src/i18n'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { isDevEnv } from 'utilities/src/environment/env'
import { logger } from 'utilities/src/logger/logger'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
......@@ -56,17 +55,7 @@ import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks'
import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider'
getUniqueId()
.then((userId) => {
initializeSentry(SentryAppNameTag.Sidebar, userId)
})
.catch((error) => {
logger.error(error, {
tags: { file: 'SidebarApp.tsx', function: 'getUniqueId' },
})
})
const router = sentryCreateHashRouter([
const router = createHashRouter([
{
path: '',
element: <SidebarWrapper />,
......@@ -258,7 +247,7 @@ export default function SidebarApp(): JSX.Element {
return (
<Trace>
<PersistGate persistor={getReduxPersistor()}>
<ExtensionStatsigProvider appName={SentryAppNameTag.Sidebar}>
<ExtensionStatsigProvider appName={DatadogAppNameTag.Sidebar}>
<I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}>
<ErrorBoundary>
......
......@@ -3,12 +3,13 @@ import 'src/app/Global.css'
import { PropsWithChildren, useEffect } from 'react'
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 { ExtensionStatsigProvider } from 'src/app/StatsigProvider'
import { GraphqlProvider } from 'src/app/apollo'
import { ErrorElement } from 'src/app/components/ErrorElement'
import { TraceUserProperties } from 'src/app/components/Trace/TraceUserProperties'
import { DatadogAppNameTag } from 'src/app/datadog'
import {
ClaimUnitagSteps,
OnboardingStepsProvider,
......@@ -23,7 +24,6 @@ import { UnitagCreateUsernameScreen } from 'src/app/features/unitags/UnitagCreat
import { UnitagIntroScreen } from 'src/app/features/unitags/UnitagIntroScreen'
import { UnitagClaimRoutes } from 'src/app/navigation/constants'
import { setRouter, setRouterState } from 'src/app/navigation/state'
import { SentryAppNameTag, initializeSentry, sentryCreateHashRouter } from 'src/app/sentry'
import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { getReduxPersistor, getReduxStore } from 'src/store/store'
import { Flex } from 'ui/src'
......@@ -32,7 +32,6 @@ import { LocalizationContextProvider } from 'uniswap/src/features/language/Local
import Trace from 'uniswap/src/features/telemetry/Trace'
import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context'
import i18n from 'uniswap/src/i18n'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger'
import { usePrevious } from 'utilities/src/react/hooks'
import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary'
......@@ -40,17 +39,7 @@ import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testne
import { useAccountAddressFromUrlWithThrow } from 'wallet/src/features/wallet/hooks'
import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider'
getUniqueId()
.then((userId) => {
initializeSentry(SentryAppNameTag.UnitagClaim, userId)
})
.catch((error) => {
logger.error(error, {
tags: { file: 'UnitagClaimApp.tsx', function: 'getUniqueId' },
})
})
const router = sentryCreateHashRouter([
const router = createHashRouter([
{
path: '',
element: <UnitagAppInner />,
......@@ -162,7 +151,7 @@ export default function UnitagClaimApp(): JSX.Element {
return (
<Trace>
<PersistGate persistor={getReduxPersistor()}>
<ExtensionStatsigProvider appName={SentryAppNameTag.UnitagClaim}>
<ExtensionStatsigProvider appName={DatadogAppNameTag.UnitagClaim}>
<I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}>
<ErrorBoundary>
......
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 { config } from 'uniswap/src/config'
import {
DatadogIgnoredErrorsConfigKey,
DatadogIgnoredErrorsValType,
DatadogSessionSampleRateKey,
DatadogSessionSampleRateValType,
DynamicConfigs,
} from 'uniswap/src/features/gating/configs'
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 { Statsig } from 'uniswap/src/features/gating/sdk/statsig'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { datadogEnabled, localDevDatadogEnabled } from 'utilities/src/environment/constants'
import { logger } from 'utilities/src/logger/logger'
export async function initializeDatadog(appName: string): Promise<void> {
const datadogEnabled = Statsig.checkGate(getFeatureFlagName(FeatureFlags.Datadog))
logger.setWalletDatadogEnabled(datadogEnabled)
// In case Statsig is not available
const EXTENSION_DEFAULT_DATADOG_SESSION_SAMPLE_RATE = 10 // percent
export const enum DatadogAppNameTag {
Sidebar = 'sidebar',
Onboarding = 'onboarding',
ContentScript = 'content-script',
Background = 'background',
Popup = 'popup',
UnitagClaim = 'unitag-claim',
}
function beforeSend(event: RumEvent): boolean {
// otherwise DataDog will ignore error events
event.view.url = event.view.url.replace(/^chrome-extension:\/\/[a-z]{32}\//i, '')
if (event.error && event.type === 'error') {
if (event.error.source === 'console') {
return false
}
const ignoredErrors = getDynamicConfigValue<
DynamicConfigs.DatadogIgnoredErrors,
DatadogIgnoredErrorsConfigKey,
DatadogIgnoredErrorsValType
>(DynamicConfigs.DatadogIgnoredErrors, DatadogIgnoredErrorsConfigKey.Errors, [])
const ignoredError = ignoredErrors.find(({ messageContains }) => event.error?.message.includes(messageContains))
if (ignoredError && Math.random() > ignoredError.sampleRate) {
return false
}
Object.defineProperty(event.error, 'stack', {
value: event.error.stack?.replace(/chrome-extension:\/\/[a-z]{32}/gi, ''),
writable: false,
configurable: true,
})
}
return true
}
export async function initializeDatadog(appName: string): Promise<void> {
if (!datadogEnabled) {
return
}
const sessionSampleRate = getDynamicConfigValue<
DynamicConfigs.DatadogSessionSampleRate,
DatadogSessionSampleRateKey,
DatadogSessionSampleRateValType
>(
DynamicConfigs.DatadogSessionSampleRate,
DatadogSessionSampleRateKey.Rate,
EXTENSION_DEFAULT_DATADOG_SESSION_SAMPLE_RATE,
)
const sharedDatadogConfig = {
clientToken: config.datadogClientToken,
service: `extension-${getDatadogEnvironment()}`,
env: getDatadogEnvironment(),
version: process.env.VERSION,
trackingConsent: undefined,
}
datadogRum.init({
...sharedDatadogConfig,
applicationId: config.datadogProjectId,
sessionSampleRate: 100,
sessionSampleRate: localDevDatadogEnabled ? 100 : sessionSampleRate,
sessionReplaySampleRate: 0,
trackResources: true,
trackLongTasks: true,
trackUserInteractions: true,
enablePrivacyForActionName: true,
beforeSend: (event) => {
// otherwise DataDog will ignore error events
event.view.url = event.view.url.replace(/^chrome-extension:\/\/[a-z]{32}\//i, '')
if (event.error && event.type === 'error') {
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
},
beforeSend,
})
datadogLogs.init({
...sharedDatadogConfig,
site: 'datadoghq.com',
forwardErrorsToLogs: false,
})
// According to the Datadog RUM documentation:
// https://docs.datadoghq.com/real_user_monitoring/browser/setup/client?tab=rum#access-internal-context
// datadogRum.init() seems to be synchronous and internal context is immediately available.
// 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 {
const userId = await getUniqueId()
......
......@@ -116,7 +116,7 @@ export default function AppRatingModal({ onClose }: AppRatingModalProps): JSX.El
}, [dispatch])
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}>
<X color="$neutral2" size="$icon.20" />
</TouchableArea>
......
......@@ -70,7 +70,8 @@ export function ApproveRequestContent({
// To detect a revoke, both the transaction value and the parsed arg amount value must be zero
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
......
......@@ -66,6 +66,13 @@ export function SignTypedDataRequestContent({ dappRequest }: SignTypedDataReques
message: EIP712Message | EIP712Message[keyof EIP712Message],
i = 1,
): 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) {
const href = getExplorerLink(chainId, message, ExplorerDataType.ADDRESS)
return <MaybeExplorerLinkedAddress address={message} link={href} />
......@@ -76,6 +83,12 @@ export function SignTypedDataRequestContent({ dappRequest }: SignTypedDataReques
{message.toString()}
</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') {
return Object.entries(message).map(([key, value], index) => (
<Flex key={`${key}-${index}`} flexDirection="row" gap="$spacing8">
......
......@@ -251,6 +251,11 @@ export function* handleSendTransaction(
options: { request: transactionRequest },
typeInfo: transactionTypeInfo ?? {
type: TransactionType.Unknown,
dappInfo: {
name: dappInfo.displayName,
address: request.transaction.to,
icon: dappInfo.iconUrl,
},
},
transactionOriginType: TransactionOriginType.External,
}
......
import { useCallback } from 'react'
import { useCallback, useState } from 'react'
import { UnitagClaimRoutes } from 'src/app/navigation/constants'
import { focusOrCreateUnitagTab } from 'src/app/navigation/utils'
import { Flex } from 'ui/src'
import { PollingInterval } from 'uniswap/src/constants/misc'
import { UnichainIntroModal } from 'uniswap/src/components/unichain/UnichainIntroModal'
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 { useSharedIntroCards } from 'wallet/src/components/introCards/useSharedIntroCards'
import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
export function HomeIntroCardStack(): JSX.Element | null {
const { navigateToSwapFlow } = useWalletNavigation()
const activeAccount = useActiveAccountWithThrow()
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 () => {
await focusOrCreateUnitagTab(activeAccount.address, UnitagClaimRoutes.ClaimIntro)
}, [activeAccount.address])
const [showUnichainIntroModal, setShowUnichainIntroModal] = useState(false)
const { cards } = useSharedIntroCards({
showUnichainModal: () => setShowUnichainIntroModal(true),
navigateToUnitagClaim,
navigateToUnitagIntro: navigateToUnitagClaim, // No need to differentiate for extension
hasTokens: (data?.balanceUSD ?? 0) > 0,
})
// Don't show cards if there are none
......@@ -37,6 +37,14 @@ export function HomeIntroCardStack(): JSX.Element | null {
return (
<Flex py="$spacing4">
<IntroCardStack cards={cards} />
{showUnichainIntroModal && (
<UnichainIntroModal
openSwapFlow={() =>
navigateToSwapFlow({ openTokenSelector: CurrencyField.OUTPUT, outputChainId: UniverseChainId.Unichain })
}
onClose={() => setShowUnichainIntroModal(false)}
/>
)}
</Flex>
)
}
......@@ -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"
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
class="t_sub_theme t_primary_Button _dsp_contents is_Theme"
......@@ -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"
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
class="t_sub_theme t_primary_Button _dsp_contents is_Theme"
......
import { useCallback, useMemo, useRef } from 'react'
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 { HomeScreen } from 'src/app/features/home/HomeScreen'
import { Locked } from 'src/app/features/lockScreen/Locked'
......@@ -84,7 +84,7 @@ export function WebNavigation(): JSX.Element {
const routerState = useRouterState()
if (routeName != null) {
towards = routeDirections[routeName]
const isBackwards = routerState?.historyAction === 'POP'
const isBackwards = routerState?.historyAction === NavigationType.Pop
if (isBackwards) {
const lastRoute = getAppRouteFromPathName(history[1] || '')
const previousDirection = lastRoute ? routeDirections[lastRoute] : 'right'
......
import { RouterState } from '@sentry/react/types/types'
import { useEffect, useState } from 'react'
import { Router } from 'react-router-dom'
import { sentryCreateHashRouter } from 'src/app/sentry'
import { Location, NavigationType, Router, createHashRouter } from 'react-router-dom'
interface RouterState {
historyAction: NavigationType
location: Location
}
/**
* Note this file is separate from SidebarApp on purpose!
......@@ -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
type Router = ReturnType<typeof sentryCreateHashRouter>
type Router = ReturnType<typeof createHashRouter>
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 {
return StatsigEnvironmentTier.PROD
}
export function getSentryEnvironment(): SentryEnvironment {
if (isDevEnv()) {
return SentryEnvironment.DEV
}
if (isBetaEnv()) {
return SentryEnvironment.BETA
}
return SentryEnvironment.PROD
}
export function getDatadogEnvironment(): DatadogEnvironment {
if (isDevEnv()) {
return DatadogEnvironment.DEV
......@@ -39,9 +29,3 @@ enum DatadogEnvironment {
BETA = 'beta',
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
import { initStatSigForBrowserScripts } from 'src/app/StatsigProvider'
import { focusOrCreateOnboardingTab } from 'src/app/navigation/utils'
import { SentryAppNameTag, initSentryForBrowserScripts } from 'src/app/sentry'
import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { initMessageBridge } from 'src/background/backgroundDappRequests'
import { backgroundStore } from 'src/background/backgroundStore'
......@@ -10,7 +9,6 @@ import { backgroundToSidePanelMessageChannel } from 'src/background/messagePassi
import { BackgroundToSidePanelRequestType } from 'src/background/messagePassing/types/requests'
import { setSidePanelBehavior, setSidePanelOptions } from 'src/background/utils/chromeSidePanelUtils'
import { readIsOnboardedFromStorage } from 'src/background/utils/persistedStateUtils'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger'
export const EXTENSION_ID = chrome.runtime.id
......@@ -18,8 +16,6 @@ export const EXTENSION_ID = chrome.runtime.id
initMessageBridge()
async function initApp(): Promise<void> {
const userId = await getUniqueId()
initSentryForBrowserScripts(SentryAppNameTag.Background, userId)
await initStatSigForBrowserScripts()
await initExtensionAnalytics()
......
......@@ -250,7 +250,7 @@ async function logError(
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> {
const logMessage: AnalyticsLog = {
type: ContentScriptUtilityMessageType.AnalyticsLog,
......
......@@ -4,24 +4,13 @@
import React from 'react'
import { createRoot } from 'react-dom/client'
import OnboardingApp from 'src/app/OnboardingApp'
import { initializeSentry, SentryAppNameTag } from 'src/app/sentry'
import { initializeReduxStore } from 'src/store/store'
import { ExtensionAppLocation, StoreSynchronization } from 'src/store/storeSynchronization'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger'
;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any
// The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem
// see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326
getUniqueId()
.then((userId) => {
initializeSentry(SentryAppNameTag.Onboarding, userId)
})
.catch((error) => {
logger.error(error, {
tags: { file: 'SidebarApp.tsx', function: 'getUniqueId' },
})
})
async function initOnboarding(): Promise<void> {
await initializeReduxStore()
......
......@@ -4,23 +4,12 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import PopupApp from 'src/app/PopupApp'
import { initializeSentry, SentryAppNameTag } from 'src/app/sentry'
import { initializeReduxStore } from 'src/store/store'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger'
;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any
// The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem
// see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326
getUniqueId()
.then((userId) => {
initializeSentry(SentryAppNameTag.Popup, userId)
})
.catch((error) => {
logger.error(error, {
tags: { file: 'popup.tsx', function: 'getUniqueId' },
})
})
async function initPopup(): Promise<void> {
await initializeReduxStore({ readOnly: true })
......
......@@ -4,23 +4,12 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import UnitagClaimApp from 'src/app/UnitagClaimApp'
import { SentryAppNameTag, initializeSentry } from 'src/app/sentry'
import { initializeReduxStore } from 'src/store/store'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger'
;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any
// The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem
// see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326
getUniqueId()
.then((userId) => {
initializeSentry(SentryAppNameTag.UnitagClaim, userId)
})
.catch((error) => {
logger.error(error, {
tags: { file: 'unitagClaim.tsx', function: 'getUniqueId' },
})
})
async function initUnitagClaim(): Promise<void> {
await initializeReduxStore({ readOnly: true })
......
......@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Uniswap Extension",
"description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.",
"version": "1.14.0",
"version": "1.15.0",
"minimum_chrome_version": "116",
"icons": {
"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
export const initialSchema = {
......
import { createReduxEnhancer } from '@sentry/react'
import { PreloadedState } from 'redux'
import { persistReducer, persistStore } from 'redux-persist'
import { localStorage } from 'redux-persist-webextension-storage'
......@@ -27,18 +26,6 @@ const persistConfig = {
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({
shouldLogReduxState: (state: ExtensionState): boolean => {
// Do not log the state if a user has opted out of analytics.
......@@ -53,7 +40,7 @@ const setupStore = (preloadedState?: PreloadedState<ExtensionState>): ReturnType
additionalSagas: [rootExtensionSaga],
middlewareBefore: __DEV__ ? [loggerMiddleware] : [],
middlewareAfter: [fiatOnRampAggregatorApi.middleware],
enhancers: [sentryReduxEnhancer, dataDogReduxEnhancer],
enhancers: [dataDogReduxEnhancer],
})
}
......
......@@ -7,7 +7,6 @@ const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'
const fs = require('fs')
const DotenvPlugin = require('dotenv-webpack')
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin')
const { sentryWebpackPlugin } = require('@sentry/webpack-plugin')
const NODE_ENV = process.env.NODE_ENV || 'development'
const POLL_ENV = process.env.WEBPACK_POLLING_INTERVAL
......@@ -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,
}
......
/** @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
env:
E2E_RECOVERY_PHRASE: ${E2E_RECOVERY_PHRASE}
---
- launchApp:
appId: 'com.uniswap.mobile.dev'
clearState: true
clearKeychain: true # optional: clear *entire* iOS keychain
- extendedWaitUntil:
visible: 'Create a wallet'
- tapOn: 'Create a wallet'
- tapOn: 'Add an existing wallet'
- waitForAnimationToEnd
- tapOn: 'Skip'
- tapOn: 'Import a wallet'
- waitForAnimationToEnd
- tapOn: 'Skip'
- inputText: ${E2E_RECOVERY_PHRASE}
- waitForAnimationToEnd
- tapOn: 'Skip'
- extendedWaitUntil:
visible:
id: 'confirm'
- tapOn:
id: 'confirm'
- tapOn: 'Continue'
- waitForAnimationToEnd
- tapOn: 'Send'
- tapOn: 'Continue'
- tapOn: 'Skip'
- tapOn: 'Skip'
- tapOn: 'Skip'
- runFlow: biometrics-confirm.yaml
- 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() {
}
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
if (isCI && sentryPropertiesAvailable && !isDetox) {
if (isCI && sentryPropertiesAvailable && !isE2E) {
project.ext.sentryCli = [
logLevel: "info",
]
......@@ -85,13 +85,13 @@ if (isCI && sentryPropertiesAvailable && !isDetox) {
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"
}
def devVersionName = "1.44"
def betaVersionName = "1.44"
def prodVersionName = "1.44"
def devVersionName = "1.45"
def betaVersionName = "1.45"
def prodVersionName = "1.45"
android {
ndkVersion rootProject.ext.ndkVersion
......@@ -104,8 +104,6 @@ android {
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
splits {
abi {
......@@ -254,14 +252,17 @@ dependencies {
implementation 'com.onesignal:OneSignal:4.8.9'
implementation 'com.github.statsig-io:android-sdk:4.36.0'
// This is required for the backported AndroidX Photo Picker on versions of Android below 30
implementation("androidx.activity:activity:1.9.+")
// For animated GIF support
implementation 'com.facebook.fresco:animated-gif:3.6.0'
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
} else {
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)
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 @@
<application
android:usesCleartextTraffic="true"
tools:ignore="GoogleAppIndexingWarning"
tools:targetApi="28"
android:networkSecurityConfig="@xml/network_security_config">
tools:targetApi="28">
</application>
</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 @@
<uses-permission android:name="android.permission.VIBRATE" />
<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
android:name=".MainApplication"
android:label="@string/app_name"
......@@ -23,6 +27,17 @@
android:taskAffinity=""
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
android:name="com.onesignal.messaging.default_notification_icon"
android:resource="@drawable/ic_stat_onesignal_default" />
......
......@@ -8,6 +8,7 @@
<locale android:name="fr"/>
<locale android:name="ja"/>
<locale android:name="pt"/>
<locale android:name="vi"/>
<locale android:name="es-ES"/>
<locale android:name="es-US"/>
<locale android:name="es-419"/>
......
......@@ -37,11 +37,6 @@ plugins {
}
allprojects {
repositories {
maven {
url = rootProject.file("../../../node_modules/detox/Detox-android")
}
}
project.pluginManager.withPlugin("com.facebook.react") {
react {
reactNativeDir = rootProject.file("../../../node_modules/react-native/")
......
......@@ -8,6 +8,3 @@ apply from: new File(["node", "--print", "require.resolve('../../../node_modules
useExpoModules()
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:
- React
- react-native-get-random-values (1.8.0):
- 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-native-mmkv (2.10.1):
- MMKV (>= 1.2.13)
......@@ -3081,7 +3083,7 @@ SPEC CHECKSUMS:
react-native-compat: 100540c3cebb076da442cf058e375e8ca895ae28
react-native-context-menu-view: dcec18eb8882e20596dbb75802e7d19cb87dac02
react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a
react-native-image-picker: 1569cfade34b3a011191ce262423e6ce2f8db5a1
react-native-image-picker: b049e0ea9d6b1b58c06262e19f8b66c87ac7b760
react-native-mmkv: dea675cf9697ad35940f1687e98e133e1358ef9f
react-native-netinfo: 129bd99f607a2dc5bb096168f3e5c150fd1f1c95
react-native-onesignal: ab800900cffeca4d9db70a05244013fc8a36ceb8
......
......@@ -2230,7 +2230,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -2283,7 +2283,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
......@@ -2336,7 +2336,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
......@@ -2389,7 +2389,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
......@@ -2427,7 +2427,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -2463,7 +2463,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
......@@ -2498,7 +2498,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
......@@ -2533,7 +2533,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
......@@ -2580,7 +2580,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -2626,7 +2626,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets;
......@@ -2672,7 +2672,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets;
......@@ -2718,7 +2718,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets;
......@@ -2760,7 +2760,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -2803,7 +2803,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension;
......@@ -2846,7 +2846,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension;
......@@ -2889,7 +2889,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension;
......@@ -2925,7 +2925,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -2963,7 +2963,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3164,7 +3164,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -3210,7 +3210,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension;
......@@ -3322,7 +3322,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3394,7 +3394,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension;
......@@ -3506,7 +3506,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3578,12 +3578,12 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.44;
MARKETING_VERSION = 1.45;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension;
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;
STATSIG_SDK_KEY = "";
SWIFT_COMPILATION_MODE = wholemodule;
......
......@@ -22,6 +22,7 @@
<string>fr</string>
<string>ja</string>
<string>pt</string>
<string>vi</string>
<string>es-ES</string>
<string>es-US</string>
<string>es-419</string>
......
......@@ -17,7 +17,6 @@ const workspaceRoot = path.resolve(mobileRoot, '../..')
const watchFolders = [mobileRoot, `${workspaceRoot}/node_modules`, `${workspaceRoot}/packages`]
const detoxExtensions = process.env.DETOX_MODE === 'mocked' ? ['mock.tsx', 'mock.ts'] : []
const defaultConfig = getDefaultConfig(__dirname)
......@@ -29,8 +28,7 @@ const config = {
resolver: {
nodeModulesPaths: [`${workspaceRoot}/node_modules`],
assetExts: assetExts.filter((ext) => ext !== 'svg'),
// detox mocking works properly only being spreaded at the beginning of sourceExts array
sourceExts: [...detoxExtensions, ...sourceExts, 'svg', 'cjs'],
sourceExts: [...sourceExts, 'svg', 'cjs'],
},
transformer: {
getTransformOptions: async () => ({
......@@ -50,11 +48,14 @@ const config = {
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
module.exports = withStorybook(mergeConfig(defaultConfig, config), {
// Set to false to remove storybook specific options
// 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
configPath: path.resolve(__dirname, './.storybook'),
})
......@@ -23,14 +23,6 @@
"env:local:upload": "bash ../../scripts/uploadEnvLocal.sh xmznnx7ozuojy5lnohcmt73aee ../../.env.defaults.local",
"env:local:copy:swift": "python3 scripts/copy_env_vars_to_swift.py",
"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",
"link:assets": "react-native-asset",
"graphql:generate:swift": "cd ios && ./Pods/Apollo/apollo-ios-cli generate",
......@@ -91,9 +83,9 @@
"@tanstack/react-query": "5.51.16",
"@uniswap/analytics": "1.7.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/sdk-core": "6.1.0",
"@uniswap/sdk-core": "7.1.0",
"@walletconnect/core": "2.17.1",
"@walletconnect/react-native-compat": "2.17.1",
"@walletconnect/utils": "2.17.1",
......@@ -132,7 +124,7 @@
"react-native-gesture-handler": "2.19.0",
"react-native-get-random-values": "1.8.0",
"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-markdown-display": "7.0.0-alpha.2",
"react-native-mmkv": "2.10.1",
......@@ -182,7 +174,6 @@
"babel-plugin-module-resolver": "5.0.0",
"babel-plugin-react-native-web": "0.17.5",
"core-js": "2.6.12",
"detox": "20.23.0",
"eslint": "8.44.0",
"expo-modules-core": "1.11.13",
"jest": "29.7.0",
......
......@@ -15,7 +15,7 @@ import { SafeAreaProvider } from 'react-native-safe-area-context'
import { enableFreeze } from 'react-native-screens'
import { useDispatch, useSelector } from 'react-redux'
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 { AppModals } from 'src/app/modals/AppModals'
import { NavigationContainer } from 'src/app/navigation/NavigationContainer'
......@@ -47,10 +47,15 @@ import { uniswapUrls } from 'uniswap/src/constants/urls'
import { BlankUrlProvider } from 'uniswap/src/contexts/UrlContext'
import { selectFavoriteTokens } from 'uniswap/src/features/favorites/selectors'
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 { Experiments } from 'uniswap/src/features/gating/experiments'
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 { Statsig, StatsigOptions, StatsigProvider, StatsigUser } from 'uniswap/src/features/gating/sdk/statsig'
import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext'
......@@ -64,9 +69,10 @@ import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/conte
import i18n from 'uniswap/src/i18n'
import { CurrencyId } from 'uniswap/src/types/currency'
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 { registerConsoleOverrides } from 'utilities/src/logger/console'
import { DDRumAction, DDRumTiming } from 'utilities/src/logger/datadogEvents'
import { logger } from 'utilities/src/logger/logger'
import { useAsyncData } from 'utilities/src/react/hooks'
import { AnalyticsNavigationContextProvider } from 'utilities/src/telemetry/trace/AnalyticsNavigationContext'
......@@ -92,9 +98,9 @@ if (__DEV__) {
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.
if (isDetoxBuild) {
if (isE2EMode) {
LogBox.ignoreAllLogs()
}
......@@ -104,7 +110,7 @@ initFirebaseAppCheck()
function App(): JSX.Element | null {
useEffect(() => {
if (!__DEV__ && !isDetoxBuild) {
if (!__DEV__ && !isE2EMode) {
attachUnhandledRejectionHandler()
setAttributesToDatadog({ buildNumber: DeviceInfo.getBuildNumber() }).catch(() => undefined)
}
......@@ -121,6 +127,8 @@ function App(): JSX.Element | null {
const deviceId = useAsyncData(fetchAndSetDeviceId).data
const [datadogSessionSampleRate, setDatadogSessionSampleRate] = React.useState<number | undefined>(undefined)
const statSigOptions: {
user: StatsigUser
options: StatsigOptions
......@@ -134,7 +142,22 @@ function App(): JSX.Element | null {
api: uniswapUrls.statsigProxyUrl,
disableAutoMetricsLogging: 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,
user: {
......@@ -148,7 +171,7 @@ function App(): JSX.Element | null {
return (
<StatsigProvider {...statSigOptions}>
<DatadogProviderWrapper>
<DatadogProviderWrapper sessionSampleRate={datadogSessionSampleRate}>
<Trace>
<StrictMode>
<I18nextProvider i18n={i18n}>
......@@ -183,6 +206,11 @@ function AppOuter(): JSX.Element | null {
})
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
* RenderPassReport. We then forward this report to Datadog, Amplitude, etc.
......@@ -191,13 +219,13 @@ function AppOuter(): JSX.Element | null {
if (datadogEnabled) {
const shouldLogJsBundleLoaded = report.timeToBootJsMillis && !jsBundleLoadedRef.current
if (shouldLogJsBundleLoaded) {
await DdRum.addAction(RumActionType.CUSTOM, 'application_start_js', {
await DdRum.addAction(RumActionType.CUSTOM, DDRumAction.ApplicationStartJs, {
loading_time: report.timeToBootJsMillis,
})
jsBundleLoadedRef.current = true
}
if (report.interactive) {
await DdRum.addTiming('screenInteractive')
await DdRum.addTiming(DDRumTiming.ScreenInteractive)
}
}
......
import {
BatchSize,
DatadogProvider,
DatadogProviderConfiguration,
DdRum,
SdkVerbosity,
TrackingConsent,
UploadFrequency,
} from '@datadog/mobile-react-native'
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 { config } from 'uniswap/src/config'
import {
......@@ -16,59 +16,92 @@ import {
DynamicConfigs,
} from 'uniswap/src/features/gating/configs'
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'
const datadogConfig = new DatadogProviderConfiguration(
config.datadogClientToken,
getDatadogEnvironment(),
config.datadogProjectId,
datadogEnabled, // trackInteractions
datadogEnabled, // trackResources
datadogEnabled, // trackErrors
localDevDatadogEnabled ? TrackingConsent.GRANTED : undefined,
)
// In case Statsig is not available
export const MOBILE_DEFAULT_DATADOG_SESSION_SAMPLE_RATE = 10 // percent
Object.assign(datadogConfig, {
site: 'US1',
longTaskThresholdMs: 100,
nativeCrashReportEnabled: true,
verbosity: SdkVerbosity.INFO,
errorEventMapper: (event: ReturnType<ErrorEventMapper>) => {
const ignoredErrors = getDynamicConfigValue<
DynamicConfigs.DatadogIgnoredErrors,
DatadogIgnoredErrorsConfigKey,
DatadogIgnoredErrorsValType
>(DynamicConfigs.DatadogIgnoredErrors, DatadogIgnoredErrorsConfigKey.Errors, [])
// Configuration for Datadog's automatic monitoring features:
// - Error tracking: Captures and reports application errors
// - User interactions: Monitors user events and actions
// - Resource tracking: Traces network requests and API calls
// Note: Can buffer up to 100 RUM events before SDK initialization
// https://docs.datadoghq.com/real_user_monitoring/mobile_and_tv_monitoring/react_native/advanced_configuration/#delaying-the-initialization
const datadogAutoInstrumentation = {
trackErrors: datadogEnabled,
trackInteractions: datadogEnabled,
trackResources: datadogEnabled,
}
const ignoredError = ignoredErrors.find(({ messageContains }) => event?.message.includes(messageContains))
if (ignoredError) {
return Math.random() < ignoredError.sampleRate ? event : null
}
async function initializeDatadog(sessionSamplingRate: number | undefined): Promise<void> {
const datadogConfig = {
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) {
Object.assign(datadogConfig, {
sessionSamplingRate: 100,
resourceTracingSamplingRate: 100,
uploadFrequency: UploadFrequency.FREQUENT,
batchSize: BatchSize.SMALL,
verbosity: SdkVerbosity.DEBUG,
})
return event
},
sessionSamplingRate,
}
if (localDevDatadogEnabled) {
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
* specific configuration.
*/
export function DatadogProviderWrapper({ children }: PropsWithChildren): JSX.Element {
logger.setWalletDatadogEnabled(true)
export function DatadogProviderWrapper({
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 <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'
import { exploreNavigationRef } from 'src/app/navigation/navigation'
import { useAppStackNavigation } from 'src/app/navigation/types'
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 { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
......
......@@ -3,7 +3,7 @@ import { useCallback, useContext } from 'react'
import { BackHandler } from 'react-native'
import { navigate as rootNavigate } from 'src/app/navigation/rootNavigation'
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 { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
......
......@@ -37,7 +37,7 @@ import { ExternalProfileScreen } from 'src/screens/ExternalProfileScreen'
import { FiatOnRampConnectingScreen } from 'src/screens/FiatOnRampConnecting'
import { FiatOnRampScreen } from 'src/screens/FiatOnRampScreen'
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 { OnDeviceRecoveryScreen } from 'src/screens/Import/OnDeviceRecoveryScreen'
import { OnDeviceRecoveryViewSeedPhraseScreen } from 'src/screens/Import/OnDeviceRecoveryViewSeedPhraseScreen'
......
......@@ -5,7 +5,7 @@ import {
useNavigation,
} from '@react-navigation/native'
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 {
FiatOnRampScreens,
......
/* eslint-disable max-lines */
import { RankingType } from 'uniswap/src/data/types'
import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants'
import { Language } from 'uniswap/src/features/language/constants'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice'
import { RankingType } from 'wallet/src/features/wallet/types'
// only add fields that are persisted
export const initialSchema = {
......
......@@ -24,7 +24,7 @@ import Trace from 'uniswap/src/features/telemetry/Trace'
import { ElementNameType } from 'uniswap/src/features/telemetry/constants'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
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 { isAndroid } from 'utilities/src/platform'
......@@ -126,7 +126,7 @@ export const PriceExplorerInner = memo(function _PriceExplorerInner(): JSX.Eleme
const { convertFiatAmount } = useLocalizationContext()
const conversionRate = convertFiatAmount(1).amount
const shouldShowAnimatedDot =
(selectedDuration === HistoryDuration.Day || selectedDuration === HistoryDuration.Hour) && !isDetoxBuild
(selectedDuration === HistoryDuration.Day || selectedDuration === HistoryDuration.Hour) && !isE2EMode
const additionalPadding = shouldShowAnimatedDot ? 40 : 0
const { lastPricePoint, convertedPriceHistory } = useMemo(() => {
......
query TokenPriceHistory(
$contract: ContractInput!
$duration: HistoryDuration = DAY
$maxHistoryLength: Int = 1000
) {
tokenProjects(contracts: [$contract]) {
id
......@@ -13,7 +14,7 @@ query TokenPriceHistory(
pricePercentChange24h {
value
}
priceHistory(duration: $duration) {
priceHistory(duration: $duration, maxLength: $maxHistoryLength) {
timestamp
value
}
......@@ -32,7 +33,7 @@ query TokenPriceHistory(
pricePercentChange24h: pricePercentChange(duration: DAY) {
value
}
priceHistory(duration: $duration) {
priceHistory(duration: $duration, maxLength: $maxHistoryLength) {
timestamp
value
}
......
......@@ -14,8 +14,7 @@ export const TIME_RANGES = [
[HistoryDuration.Week, i18n.t('token.priceExplorer.timeRangeLabel.week'), ElementName.TimeFrame1W],
[HistoryDuration.Month, i18n.t('token.priceExplorer.timeRangeLabel.month'), ElementName.TimeFrame1M],
[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('token.priceExplorer.timeRangeLabel.all'), ElementName.TimeFrameAll],
[HistoryDuration.Max, i18n.t('common.all'), ElementName.TimeFrameAll],
] as const
export const NUM_GRAPHS = TIME_RANGES.length
import { BigNumber } from 'ethers'
import React from 'react'
import React, { PropsWithChildren } from 'react'
import { useTranslation } from 'react-i18next'
import { ScrollView } from 'react-native-gesture-handler'
import { LinkButton } from 'src/components/buttons/LinkButton'
......@@ -9,7 +9,7 @@ import { TextVariantTokens, iconSizes } from 'ui/src/theme'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
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 { getValidAddress } from 'uniswap/src/utils/addresses'
import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking'
......@@ -38,7 +38,7 @@ type AddressButtonProps = {
}
const AddressButton = ({ address, chainId, ...rest }: AddressButtonProps): JSX.Element => {
const { name } = useENS(chainId, address, false)
const { data: name } = useENSName(address)
const colors = useSporeColors()
const { defaultChainId } = useEnabledChains()
const supportedChainId = toSupportedChainId(chainId) ?? defaultChainId
......@@ -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
// recursively parses typed data objects and adds margin to left
......@@ -64,45 +81,31 @@ const getParsedObjectDisplay = (chainId: number, obj: any, depth = 0): JSX.Eleme
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 (
<Flex gap="$spacing4">
{Object.keys(obj).map((objKey) => {
const childValue = obj[objKey]
if (typeof childValue === 'object') {
return (
<Flex key={objKey} gap="$spacing4">
<Text color="$neutral2" variant="body3">
{objKey}
</Text>
{getParsedObjectDisplay(chainId, childValue, depth + 1)}
</Flex>
)
}
if (typeof childValue === 'string') {
// Special case for address strings
if (typeof childValue === 'string' && getValidAddress(childValue, true)) {
return (
<Flex key={objKey} row alignItems="flex-start" gap="$spacing8">
<Text color="$neutral2" py="$spacing4" variant="body3">
{objKey}
</Text>
<Flex shrink gap="$spacing16">
{getValidAddress(childValue, true) ? (
<Flex py="$spacing4">
<AddressButton address={childValue} chainId={chainId} textVariant="body3" />
</Flex>
) : (
<Text py="$spacing4" variant="body3">
{childValue}
</Text>
)}
<KeyValueRow key={objKey} objKey={objKey}>
<Flex>
<AddressButton address={childValue} chainId={chainId} textVariant="body3" />
</Flex>
</Flex>
</KeyValueRow>
)
}
// TODO: [MOB-216] handle array child types
return null
return (
<KeyValueRow key={objKey} objKey={objKey}>
{getParsedObjectDisplay(chainId, childValue, depth + 1)}
</KeyValueRow>
)
})}
</Flex>
)
......
......@@ -21,6 +21,8 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { useAppInsets } from 'uniswap/src/hooks/useAppInsets'
import { CurrencyId } from 'uniswap/src/types/currency'
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 { useValueAsRef } from 'utilities/src/react/useValueAsRef'
import { InformationBanner } from 'wallet/src/components/banners/InformationBanner'
......@@ -76,6 +78,8 @@ export const TokenBalanceListInner = forwardRef<FlatList<TokenBalanceListRow>, T
const colors = useSporeColors()
const insets = useAppInsets()
usePerformanceLogger(DDRumManualTiming.RenderTokenBalanceList, [])
const { rows, balancesById } = useTokenBalanceListContext()
const { onContentSizeChange, adaptiveFooter } = useAdaptiveFooter(containerProps?.contentContainerStyle)
......
import React, { memo } from 'react'
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 WarningIcon from 'uniswap/src/components/warnings/WarningIcon'
import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import {
useTokenBasicInfoPartsFragment,
useTokenBasicProjectPartsFragment,
} from 'uniswap/src/data/graphql/uniswap-data-api/fragments'
import { fromGraphQLChain } from 'uniswap/src/features/chains/utils'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
export const TokenDetailsHeader = memo(function _TokenDetailsHeader(): JSX.Element {
const tokenProtectionEnabled = useFeatureFlag(FeatureFlags.TokenProtection)
const { currencyId, openTokenWarningModal } = useTokenDetailsContext()
const { currencyId } = useTokenDetailsContext()
const token = useTokenBasicInfoPartsFragment({ currencyId }).data
const project = useTokenBasicProjectPartsFragment({ currencyId }).data.project
const shouldShowWarningIcon =
!tokenProtectionEnabled &&
(project?.safetyLevel === SafetyLevel.StrongWarning || project?.safetyLevel === SafetyLevel.Blocked)
return (
<Flex gap="$spacing12" mx="$spacing16">
<TokenLogo
......@@ -44,12 +33,6 @@ export const TokenDetailsHeader = memo(function _TokenDetailsHeader(): JSX.Eleme
>
{token?.name ?? ''}
</Text>
{shouldShowWarningIcon && (
<TouchableArea onPress={openTokenWarningModal}>
<WarningIcon safetyLevel={project?.safetyLevel} size="$icon.20" strokeColorOverride="$neutral3" />
</TouchableArea>
)}
</Flex>
</Flex>
)
......
......@@ -55,10 +55,17 @@ const TokenDetailsMarketData = memo(function _TokenDetailsMarketData(): JSX.Elem
const tokenMarket = useTokenMarketPartsFragment({ currencyId }).data?.market
const projectMarkets = useTokenProjectMarketsPartsFragment({ currencyId }).data.project?.markets
const price = projectMarkets?.[0]?.price?.value || tokenMarket?.price?.value || undefined
const marketCap = projectMarkets?.[0]?.marketCap?.value
const volume = tokenMarket?.volume?.value
const priceHight52W = projectMarkets?.[0]?.priceHigh52W?.value ?? tokenMarket?.priceHigh52W?.value
const priceLow52W = projectMarkets?.[0]?.priceLow52W?.value ?? tokenMarket?.priceLow52W?.value
const rawPriceHigh52W = projectMarkets?.[0]?.priceHigh52W?.value || tokenMarket?.priceHigh52W?.value || undefined
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
return (
......@@ -136,6 +143,7 @@ export const TokenDetailsStats = memo(function _TokenDetailsStats(): JSX.Element
includeFrench: language === Language.French,
includeJapanese: language === Language.Japanese,
includePortuguese: language === Language.Portuguese,
includeVietnamese: language === Language.Vietnamese,
includeChineseSimplified: language === Language.ChineseSimplified,
includeChineseTraditional: language === Language.ChineseTraditional,
},
......@@ -150,6 +158,7 @@ export const TokenDetailsStats = memo(function _TokenDetailsStats(): JSX.Element
descriptions?.descriptionTranslations?.descriptionFrFr ||
descriptions?.descriptionTranslations?.descriptionJaJp ||
descriptions?.descriptionTranslations?.descriptionPtPt ||
descriptions?.descriptionTranslations?.descriptionViVn ||
descriptions?.descriptionTranslations?.descriptionZhHans ||
descriptions?.descriptionTranslations?.descriptionZhHant
......
......@@ -66,7 +66,7 @@ function TokenOptionItemWrapper({
option={option}
quantity={option.quantity}
quantityFormatted={formatNumberOrString({ value: option.quantity, type: NumberType.TokenTx })}
showWarnings={true}
showWarnings={false}
tokenWarningDismissed={tokenWarningDismissed}
onPress={onPress}
/>
......
......@@ -74,12 +74,12 @@ describe('FavoriteTokenCard', () => {
it('renders loader', async () => {
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(loader).toBeTruthy()
expect(loaderPrice).toBeTruthy()
expect(loaderPriceChange).toBeTruthy()
// loading finished
await waitFor(() => {
expect(queryByTestId(touchableId)).toBeTruthy()
})
......
......@@ -6,12 +6,11 @@ import { useDispatch } from 'react-redux'
import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks'
import RemoveButton from 'src/components/explore/RemoveButton'
import { useAnimatedCardDragStyle, useExploreTokenContextMenu } from 'src/components/explore/hooks'
import { Loader } from 'src/components/loading/loaders'
import { disableOnPress } from 'src/utils/disableOnPress'
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 { 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 { PollingInterval } from 'uniswap/src/constants/misc'
import {
......@@ -103,9 +102,7 @@ function FavoriteTokenCard({
const shadowProps = useShadowPropsShort()
if (isNonPollingRequestInFlight(networkStatus)) {
return <Loader.Favorite height={FAVORITE_TOKEN_CARD_LOADER_HEIGHT} />
}
const priceLoading = isNonPollingRequestInFlight(networkStatus)
return (
<AnimatedFlex borderRadius="$rounded16" style={animatedDragStyle}>
......@@ -143,15 +140,31 @@ function FavoriteTokenCard({
<RemoveButton visible={isEditing} onPress={onRemove} />
</Flex>
<Flex gap="$spacing2">
<Text adjustsFontSizeToFit numberOfLines={1} variant="heading3">
{priceFormatted}
</Text>
<RelativeChange
arrowSize="$icon.16"
change={pricePercentChange ?? undefined}
semanticColor={true}
variant="subheading2"
/>
{priceLoading ? (
<Loader.Box
height={fonts.heading3.lineHeight}
width={fonts.heading3.lineHeight * 3}
testID="loader/favorite/price"
/>
) : (
<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>
</AnimatedTouchableArea>
......
......@@ -96,10 +96,10 @@ export function FavoriteTokensGrid({ showLoading, ...rest }: FavoriteTokensGridP
function FavoriteTokensGridLoader(): JSX.Element {
return (
<Flex row>
<Flex m="$spacing4" style={ITEM_FLEX}>
<Flex mx="$spacing4" style={ITEM_FLEX}>
<Loader.Favorite contrast height={FAVORITE_TOKEN_CARD_LOADER_HEIGHT} />
</Flex>
<Flex m="$spacing4" style={ITEM_FLEX}>
<Flex mx="$spacing4" style={ITEM_FLEX}>
<Loader.Favorite contrast height={FAVORITE_TOKEN_CARD_LOADER_HEIGHT} />
</Flex>
</Flex>
......
import { SortButton } from 'src/components/explore/SortButton'
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', () => {
// Use the actual implementation of `react-native-context-menu-view` as the mock implementation
......
......@@ -15,11 +15,12 @@ import {
import { iconSizes } from 'ui/src/theme'
import { ActionSheetDropdown } from 'uniswap/src/components/dropdowns/ActionSheetDropdown'
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 { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { logger } from 'utilities/src/logger/logger'
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
......
......@@ -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 { ALL_NETWORKS_ARG } from 'uniswap/src/data/rest/base'
import { useTokenRankingsQuery } from 'uniswap/src/data/rest/tokenRankings'
import { RankingType } from 'uniswap/src/data/types'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { fromGraphQLChain } from 'uniswap/src/features/chains/utils'
import { TokenList } from 'uniswap/src/features/dataApi/types'
import { SearchResultType, TokenSearchResult } from 'uniswap/src/features/search/SearchResult'
import { RankingType } from 'wallet/src/features/wallet/types'
const MAX_TOKEN_RESULTS_AMOUNT = 8
......
......@@ -28,14 +28,14 @@ export function useWalletSearchResults(
address: dotEthAddress,
name: dotEthName,
loading: dotEthLoading,
} = useENS(UniverseChainId.Mainnet, querySkippedIfValidAddress, true)
} = useENS({ nameOrAddress: querySkippedIfValidAddress, autocompleteDomain: true })
// Search for exact match for ENS if not a valid address
const {
address: ensAddress,
name: ensName,
loading: ensLoading,
} = useENS(UniverseChainId.Mainnet, querySkippedIfValidAddress, false)
} = useENS({ nameOrAddress: querySkippedIfValidAddress, autocompleteDomain: false })
// Search for matching Unitag by name
const { unitag: unitagByName, loading: unitagLoading } = useUnitagByName(query)
......
......@@ -12,6 +12,8 @@ import { Flex, useSporeColors } from 'ui/src'
import { GQLQueries } from 'uniswap/src/data/graphql/uniswap-data-api/queries'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
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 { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
import { useActivityData } from 'wallet/src/features/activity/useActivityData'
......@@ -57,6 +59,8 @@ export const ActivityTab = memo(
onPressEmptyState: onPressReceive,
})
usePerformanceLogger(DDRumManualTiming.RenderActivityTabList, [])
const refreshControl = useMemo(() => {
return (
<RefreshControl
......
......@@ -7,10 +7,13 @@ import { FundWalletModal } from 'src/components/home/introCards/FundWalletModal'
import { openModal } from 'src/features/modals/modalSlice'
import { Flex } from 'ui/src'
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 { UniverseChainId } from 'uniswap/src/features/chains/types'
import { ElementName, ModalName, WalletEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { OnboardingCardLoggingName } from 'uniswap/src/features/telemetry/types'
import { CurrencyField } from 'uniswap/src/types/currency'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { MobileScreens, OnboardingScreens, UnitagScreens } from 'uniswap/src/types/screens/mobile'
import {
......@@ -21,16 +24,17 @@ import {
} from 'wallet/src/components/introCards/IntroCard'
import { INTRO_CARD_MIN_HEIGHT, IntroCardStack } from 'wallet/src/components/introCards/IntroCardStack'
import { useSharedIntroCards } from 'wallet/src/components/introCards/useSharedIntroCards'
import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext'
import { selectHasViewedWelcomeWalletCard } from 'wallet/src/features/behaviorHistory/selectors'
import { setHasViewedWelcomeWalletCard } from 'wallet/src/features/behaviorHistory/slice'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
type OnboardingIntroCardStackProps = {
isLoading?: boolean
hasTokens: boolean
showEmptyWalletState: boolean
}
export function OnboardingIntroCardStack({
hasTokens,
showEmptyWalletState,
isLoading = false,
}: OnboardingIntroCardStackProps): JSX.Element | null {
const { t } = useTranslation()
......@@ -43,6 +47,8 @@ export function OnboardingIntroCardStack({
const welcomeCardTitle = t('onboarding.home.intro.welcome.title')
const hasViewedWelcomeWalletCard = useSelector(selectHasViewedWelcomeWalletCard)
const { navigateToSwapFlow } = useWalletNavigation()
const navigateToUnitagClaim = useCallback(() => {
navigate(MobileScreens.UnitagStack, {
screen: UnitagScreens.ClaimUnitag,
......@@ -62,14 +68,15 @@ export function OnboardingIntroCardStack({
)
}, [dispatch, address])
const [showFundModal, setShowFundModal] = useState(false)
const [showUnichainIntroModal, setShowUnichainIntroModal] = useState(false)
const { cards: sharedCards } = useSharedIntroCards({
hasTokens,
showUnichainModal: () => setShowUnichainIntroModal(true),
navigateToUnitagClaim,
navigateToUnitagIntro,
})
const [showFundModal, setShowFundModal] = useState(false)
const cards = useMemo((): IntroCardProps[] => {
const output: IntroCardProps[] = []
......@@ -78,7 +85,7 @@ export function OnboardingIntroCardStack({
return output
}
if (!hasTokens) {
if (showEmptyWalletState) {
output.push({
loggingName: OnboardingCardLoggingName.FundWallet,
graphic: {
......@@ -142,7 +149,7 @@ export function OnboardingIntroCardStack({
}
return output
}, [hasBackups, hasTokens, hasViewedWelcomeWalletCard, isSignerAccount, sharedCards, t, welcomeCardTitle])
}, [hasBackups, showEmptyWalletState, hasViewedWelcomeWalletCard, isSignerAccount, sharedCards, t, welcomeCardTitle])
const handleSwiped = useCallback(
(_card: IntroCardProps, index: number) => {
......@@ -160,15 +167,31 @@ export function OnboardingIntroCardStack({
[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) {
return (
<Flex pt="$spacing12">
{isLoading ? <Flex height={INTRO_CARD_MIN_HEIGHT} /> : <IntroCardStack cards={cards} onSwiped={handleSwiped} />}
{showFundModal && <FundWalletModal onClose={() => setShowFundModal(false)} />}
{showUnichainIntroModal && UnichainIntroModalInstance}
</Flex>
)
}
if (showUnichainIntroModal) {
return UnichainIntroModalInstance
}
return null
}
import React from 'react'
import { SvgProps } from 'react-native-svg'
import { useIsDarkMode } from 'ui/src'
import { IconSizeTokens } from 'ui/src/theme'
import { UNIVERSE_CHAIN_LOGO } from 'uniswap/src/assets/chainLogos'
import { getChainInfo } from 'uniswap/src/features/chains/chainInfo'
import { useBlockExplorerLogo } from 'uniswap/src/features/chains/logos'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
type IconComponentProps = SvgProps & { size?: IconSizeTokens | number }
......@@ -12,11 +11,10 @@ const iconsCache = new Map<UniverseChainId, React.FC<IconComponentProps>>()
function buildIconComponent(chainId: UniverseChainId): React.FC<IconComponentProps> {
const explorer = getChainInfo(chainId).explorer
const explorerLogos = UNIVERSE_CHAIN_LOGO[chainId].explorer
const Component = ({ size }: IconComponentProps): JSX.Element => {
const isDarkMode = useIsDarkMode()
return isDarkMode ? <explorerLogos.logoDark size={size} /> : <explorerLogos.logoLight size={size} />
const Logo = useBlockExplorerLogo(chainId)
return <Logo size={size} />
}
Component.displayName = `BlockExplorerIcon_${explorer.name}`
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'
import { AssetType, TradeableAsset } from 'uniswap/src/entities/assets'
import { UniverseChainId } from 'uniswap/src/features/chains/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 { forceFetchFiatOnRampTransactions } from 'uniswap/src/features/transactions/slice'
import { CurrencyField } from 'uniswap/src/types/currency'
......@@ -26,15 +27,27 @@ export function* handleOffRampReturnLink(url: URL) {
function* _handleOffRampReturnLink(url: URL) {
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) {
throw new Error('Missing externalTransactionId in fiat offramp deep link')
const hasValidMoonpayData = currencyCode && currencyAmount && walletAddress
if (!externalTransactionId && !hasValidMoonpayData) {
throw new Error('Missing externalTransactionId or moonpay data in fiat offramp deep link')
}
sendAnalyticsEvent(FiatOffRampEventName.FiatOffRampWidgetCompleted, { externalTransactionId })
let offRampTransferDetails: OffRampTransferDetailsResponse | undefined
try {
offRampTransferDetails = yield* call(fetchOffRampTransferDetails, externalTransactionId)
offRampTransferDetails = yield* call(
fetchOffRampTransferDetails,
externalTransactionId,
currencyCode,
Number(currencyAmount),
walletAddress,
)
} catch (error) {
logger.error(error, {
tags: { file: 'handleOffRampReturnLinkSaga', function: 'handleOffRampReturnLink' },
......@@ -58,8 +71,15 @@ function* _handleOffRampReturnLink(url: URL) {
const fiatOffRampMetaData: FiatOffRampMetaData = {
name: provider,
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,
meldCurrencyCode: baseCurrencyCode,
}
......
......@@ -2,7 +2,7 @@ import { call, put } from '@redux-saga/core/effects'
import { expectSaga } from 'redux-saga-test-plan'
import { navigate } from 'src/app/navigation/rootNavigation'
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 { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { dismissInAppBrowser } from 'wallet/src/utils/linking'
......
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 { forceFetchFiatOnRampTransactions } from 'uniswap/src/features/transactions/slice'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
......
......@@ -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.
*
* 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
* 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
*/
......
......@@ -3,7 +3,7 @@ import { expectSaga } from 'redux-saga-test-plan'
import { navigate } from 'src/app/navigation/rootNavigation'
import { handleTransactionLink } from 'src/features/deepLinking/handleTransactionLinkSaga'
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'
describe(handleTransactionLink, () => {
......
import { navigate } from 'src/app/navigation/rootNavigation'
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 { MobileScreens } from 'uniswap/src/types/screens/mobile'
......
import { AppTFunction } from 'ui/src/i18n/types'
import {
CustomRankingType,
ExploreOrderBy,
RankingType,
TokenMetadataDisplayType,
} from 'wallet/src/features/wallet/types'
import { CustomRankingType, RankingType } from 'uniswap/src/data/types'
import { ExploreOrderBy, TokenMetadataDisplayType } from 'wallet/src/features/wallet/types'
export function getTokenMetadataDisplayType(orderBy: ExploreOrderBy): TokenMetadataDisplayType {
switch (orderBy) {
......
......@@ -20,13 +20,13 @@ import { useLocalizationContext } from 'uniswap/src/features/language/Localizati
import { usePrevious } from 'utilities/src/react/hooks'
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_SCREEN_HEIGHT = 667 // iPhone SE 3rd Gen
// if font changes from `fontFamily.sansSerif.regular` or `MAX_INPUT_FONT_SIZE`
// changes from 36 then width value must be adjusted
const MAX_CHAR_PIXEL_WIDTH = 44
// changes from 46 then width value must be adjusted
const MAX_CHAR_PIXEL_WIDTH = 46
const PREDEFINED_ONRAMP_AMOUNTS = [100, 300, 1000]
const PREDEFINED_OFFRAMP_PERCENTAGES = [25, 50, 75]
......@@ -226,7 +226,7 @@ export const FiatOnRampAmountSection = forwardRef<FiatOnRampAmountSectionRef, Fi
height={fontSize}
lineHeight={fontSize}
>
{isTokenInputMode ? currency.currencyInfo?.currency.symbol : fiatCurrencyInfo.symbol}
{isTokenInputMode ? ' ' + currency.currencyInfo?.currency.symbol : fiatCurrencyInfo.symbol}
</Text>
<AmountInput
ref={inputRef}
......@@ -251,7 +251,7 @@ export const FiatOnRampAmountSection = forwardRef<FiatOnRampAmountSectionRef, Fi
minWidth={calculatedInputWidth}
returnKeyType={undefined}
showSoftInputOnFocus={false}
textAlign="left"
textAlign={isTokenInputMode ? 'right' : 'left'}
value={value}
onChangeText={onEnterAmount}
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