ci(release): publish latest release

parent 1bf6943a
IPFS hash of the deployment: IPFS hash of the deployment:
- CIDv0: `QmSHXp5oNAxME6KY7LJy4ndnuBfL3ujQ67aMiPuTUuQ66g` - CIDv0: `QmbeaM3MCweTaV4GCYLVMEKm5ErB89zu5bD6JaZ1Eoxjgc`
- CIDv1: `bafybeib2ui2plf3zbinsp24o4d5ir66yr4a3qlg55kswt2rmlgkoomvigu` - CIDv1: `bafybeigfx5zxz364o5wjk7wwfil27fg27o6morrlgfik4cc3gohrlezape`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org). The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
...@@ -10,14 +10,65 @@ You can also access the Uniswap Interface from an IPFS gateway. ...@@ -10,14 +10,65 @@ You can also access the Uniswap Interface from an IPFS gateway.
Your Uniswap settings are never remembered across different URLs. Your Uniswap settings are never remembered across different URLs.
IPFS gateways: IPFS gateways:
- https://bafybeib2ui2plf3zbinsp24o4d5ir66yr4a3qlg55kswt2rmlgkoomvigu.ipfs.dweb.link/ - https://bafybeigfx5zxz364o5wjk7wwfil27fg27o6morrlgfik4cc3gohrlezape.ipfs.dweb.link/
- [ipfs://QmSHXp5oNAxME6KY7LJy4ndnuBfL3ujQ67aMiPuTUuQ66g/](ipfs://QmSHXp5oNAxME6KY7LJy4ndnuBfL3ujQ67aMiPuTUuQ66g/) - [ipfs://QmbeaM3MCweTaV4GCYLVMEKm5ErB89zu5bD6JaZ1Eoxjgc/](ipfs://QmbeaM3MCweTaV4GCYLVMEKm5ErB89zu5bD6JaZ1Eoxjgc/)
## 5.73.0 (2025-02-26) ## 5.74.0 (2025-02-27)
### Features ### Features
* **web:** reduce monad testnet quote polling interval (#16719) c99cc6c * **web:** add unichain default rpc - main (#16124) 95aff86
* **web:** implement new design for the position detail page (#15895) df9fe66
* **web:** remove unichain beta toggle - main (#16168) (#16186) f479a33
### Bug Fixes
* **web:** [styled-components] migrate SparklineChart/index.tsx (#16557) 66fefda
* **web:** add space at bottom of Swap page (#16149) 600d027
* **web:** align New Position page in center on med-small screens (#16512) 45905e6
* **web:** analytics for 'swap submit button clicked' native eth transactions (#15914) bfe6da6
* **web:** create position spacing and alignment issues (#16470) 83ece23
* **web:** fix bug to showing blockaid logo (#16471) a0f3bdb
* **web:** landing page input output initial value (#16597) 26660cf
* **web:** LiquidityPositionRangeChart inverted prices bug (#16550) 8b658e8
* **web:** migrate FiatValue to tamagui (#16342) aea37e7
* **web:** migrate LoadingBubble to tamagui (#16341) 761a362
* **web:** migrate styled components usage in ukDisclaimerModal (#16422) c39acb0
* **web:** migrate styled-components usage in TaxTooltipBody (#16425) c346b04
* **web:** playwright browser cache (#16256) 4afc1bb
* **web:** positioning of loading liquidity bars in the range chart (#16222) dda5ff8
* **web:** re-add position page old design, use feature flag (#16583) cc5b299
* **web:** regression affecting Android keyboard opening (#16403) ab0fbed
* **web:** set loading status to false if no pagination result (#16296) 90f7a50
* **web:** sync table head and container scrollables (#16548) f83d68a
### Styles
* **web:** fix holiday nav icon size and use accent1 var from theme (#16558) 39e530a
### Continuous Integration
* **web:** Increase JS heap for web quality checks (#16395) fe4a390
* **web:** update sitemaps dae503c
### Code Refactoring
* **web:** add alignRight prop (#16142) e68d57f
* **web:** create mockMediaSize test util (#16414) b33ae5f
* **web:** don't adapt to sheet by default (#16139) c3aa280
* **web:** dropdown use children instead of internalMenuItems (#16130) 7ad312c
* **web:** empty wallet content deprecate styled components (#16440) 46fc931
* **web:** kill useSingleContractMultipleData (#15058) e908b3c
* **web:** split out AdaptiveDropdown (#16132) ad16962
* **web:** update AccountDrawer and use WebBottomSheet for small screens (#16340) 0b5615b
* **web:** use DropdownSelector for positions dropdown (#16191) b70cc11
* **web:** useReadContract instead of useSingleCallResult in block timestamp hooks (#14745) a5dde3f
* **web:** useReadContract instead of useSingleCallResult in migrate v2 hooks (#14746) ee407d6
* **web:** useReadContract instead of useSingleCallResult in misc hooks (#14717) f70fad4
web/5.73.0 web/5.74.0
\ No newline at end of file \ No newline at end of file
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
"dotenv-webpack": "8.0.1", "dotenv-webpack": "8.0.1",
"ethers": "5.7.2", "ethers": "5.7.2",
"eventemitter3": "5.0.1", "eventemitter3": "5.0.1",
"framer-motion": "10.17.6",
"i18next": "23.10.0", "i18next": "23.10.0",
"node-polyfill-webpack-plugin": "2.0.1", "node-polyfill-webpack-plugin": "2.0.1",
"react": "18.3.1", "react": "18.3.1",
......
import { render } from '@testing-library/react'
import UnitagClaimApp from 'src/app/UnitagClaimApp'
import { initializeReduxStore } from 'src/store/store'
describe('UnitagClaimApp', () => {
it('renders without error', async () => {
await initializeReduxStore()
render(<UnitagClaimApp />)
})
})
import { PropsWithChildren } from 'react'
import { I18nextProvider } from 'react-i18next'
import { GraphqlProvider } from 'src/app/apollo'
import { TraceUserProperties } from 'src/app/components/Trace/TraceUserProperties'
import { ExtensionStatsigProvider } from 'src/app/core/StatsigProvider'
import { DatadogAppNameTag } from 'src/app/datadog'
import { getReduxStore } from 'src/store/store'
import { BlankUrlProvider } from 'uniswap/src/contexts/UrlContext'
import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext'
import Trace from 'uniswap/src/features/telemetry/Trace'
import i18n from 'uniswap/src/i18n'
import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary'
import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider'
export function BaseAppContainer({
children,
appName,
}: PropsWithChildren<{ appName: DatadogAppNameTag }>): JSX.Element {
return (
<Trace>
<ExtensionStatsigProvider appName={appName}>
<I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}>
<ErrorBoundary>
<GraphqlProvider>
<BlankUrlProvider>
<LocalizationContextProvider>
<TraceUserProperties />
{children}
</LocalizationContextProvider>
</BlankUrlProvider>
</GraphqlProvider>
</ErrorBoundary>
</SharedWalletProvider>
</I18nextProvider>
</ExtensionStatsigProvider>
</Trace>
)
}
import { render } from '@testing-library/react' import { render } from '@testing-library/react'
import OnboardingApp from 'src/app/OnboardingApp' import OnboardingApp from 'src/app/core/OnboardingApp'
import { initializeReduxStore } from 'src/store/store' import { initializeReduxStore } from 'src/store/store'
describe('OnboardingApp', () => { describe('OnboardingApp', () => {
......
...@@ -3,12 +3,10 @@ import 'src/app/Global.css' ...@@ -3,12 +3,10 @@ import 'src/app/Global.css'
import 'symbol-observable' // Needed by `reduxed-chrome-storage` as polyfill, order matters import 'symbol-observable' // Needed by `reduxed-chrome-storage` as polyfill, order matters
import { useEffect } from 'react' import { useEffect } from 'react'
import { I18nextProvider } from 'react-i18next'
import { RouteObject, RouterProvider, createHashRouter } from 'react-router-dom' import { RouteObject, RouterProvider, createHashRouter } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
import { ExtensionStatsigProvider } from 'src/app/StatsigProvider'
import { GraphqlProvider } from 'src/app/apollo'
import { ErrorElement } from 'src/app/components/ErrorElement' import { ErrorElement } from 'src/app/components/ErrorElement'
import { BaseAppContainer } from 'src/app/core/BaseAppContainer'
import { DatadogAppNameTag } from 'src/app/datadog' import { DatadogAppNameTag } from 'src/app/datadog'
import { ClaimUnitagScreen } from 'src/app/features/onboarding/ClaimUnitagScreen' import { ClaimUnitagScreen } from 'src/app/features/onboarding/ClaimUnitagScreen'
import { Complete } from 'src/app/features/onboarding/Complete' import { Complete } from 'src/app/features/onboarding/Complete'
...@@ -38,17 +36,11 @@ import { setRouter, setRouterState } from 'src/app/navigation/state' ...@@ -38,17 +36,11 @@ import { setRouter, setRouterState } from 'src/app/navigation/state'
import { initExtensionAnalytics } from 'src/app/utils/analytics' import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { checksIfSupportsSidePanel } from 'src/app/utils/chrome' import { checksIfSupportsSidePanel } from 'src/app/utils/chrome'
import { PrimaryAppInstanceDebuggerLazy } from 'src/store/PrimaryAppInstanceDebuggerLazy' import { PrimaryAppInstanceDebuggerLazy } from 'src/store/PrimaryAppInstanceDebuggerLazy'
import { getReduxPersistor, getReduxStore } from 'src/store/store' import { getReduxPersistor } from 'src/store/store'
import { BlankUrlProvider } from 'uniswap/src/contexts/UrlContext'
import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context' import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context'
import i18n from 'uniswap/src/i18n'
import { ExtensionOnboardingFlow } from 'uniswap/src/types/screens/extension' import { ExtensionOnboardingFlow } from 'uniswap/src/types/screens/extension'
import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary'
import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider'
const supportsSidePanel = checksIfSupportsSidePanel() const supportsSidePanel = checksIfSupportsSidePanel()
...@@ -186,27 +178,13 @@ export default function OnboardingApp(): JSX.Element { ...@@ -186,27 +178,13 @@ export default function OnboardingApp(): JSX.Element {
}, []) }, [])
return ( return (
<Trace> <PersistGate persistor={getReduxPersistor()}>
<PersistGate persistor={getReduxPersistor()}> <BaseAppContainer appName={DatadogAppNameTag.Onboarding}>
<ExtensionStatsigProvider appName={DatadogAppNameTag.Onboarding}> <UnitagUpdaterContextProvider>
<I18nextProvider i18n={i18n}> <PrimaryAppInstanceDebuggerLazy />
<SharedWalletProvider reduxStore={getReduxStore()}> <RouterProvider router={router} />
<ErrorBoundary> </UnitagUpdaterContextProvider>
<GraphqlProvider> </BaseAppContainer>
<BlankUrlProvider> </PersistGate>
<LocalizationContextProvider>
<UnitagUpdaterContextProvider>
<PrimaryAppInstanceDebuggerLazy />
<RouterProvider router={router} />
</UnitagUpdaterContextProvider>
</LocalizationContextProvider>
</BlankUrlProvider>
</GraphqlProvider>
</ErrorBoundary>
</SharedWalletProvider>
</I18nextProvider>
</ExtensionStatsigProvider>
</PersistGate>
</Trace>
) )
} }
...@@ -2,32 +2,21 @@ import '@tamagui/core/reset.css' ...@@ -2,32 +2,21 @@ import '@tamagui/core/reset.css'
import 'src/app/Global.css' import 'src/app/Global.css'
import { useEffect } from 'react' import { useEffect } from 'react'
import { I18nextProvider, useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { RouterProvider, createHashRouter } 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 { ErrorElement } from 'src/app/components/ErrorElement'
import { TraceUserProperties } from 'src/app/components/Trace/TraceUserProperties' import { BaseAppContainer } from 'src/app/core/BaseAppContainer'
import { DatadogAppNameTag } from 'src/app/datadog' import { DatadogAppNameTag } from 'src/app/datadog'
import { DappContextProvider } from 'src/app/features/dapp/DappContext'
import { initExtensionAnalytics } from 'src/app/utils/analytics' import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { getReduxPersistor, getReduxStore } from 'src/store/store'
import { DeprecatedButton, Flex, Image, Text } from 'ui/src' import { DeprecatedButton, Flex, Image, Text } from 'ui/src'
import { CHROME_LOGO, UNISWAP_LOGO } from 'ui/src/assets' import { CHROME_LOGO, UNISWAP_LOGO } from 'ui/src/assets'
import { iconSizes, spacing } from 'ui/src/theme' import { iconSizes, spacing } from 'ui/src/theme'
import { BlankUrlProvider } from 'uniswap/src/contexts/UrlContext'
import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext'
import { syncAppWithDeviceLanguage } from 'uniswap/src/features/settings/slice' import { syncAppWithDeviceLanguage } from 'uniswap/src/features/settings/slice'
import Trace from 'uniswap/src/features/telemetry/Trace' import Trace from 'uniswap/src/features/telemetry/Trace'
import { ElementName } from 'uniswap/src/features/telemetry/constants' 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 { ExtensionScreens } from 'uniswap/src/types/screens/extension'
import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary' import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks'
import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider'
const router = createHashRouter([ const router = createHashRouter([
{ {
...@@ -114,29 +103,8 @@ export default function PopupApp(): JSX.Element { ...@@ -114,29 +103,8 @@ export default function PopupApp(): JSX.Element {
}, []) }, [])
return ( return (
<Trace> <BaseAppContainer appName={DatadogAppNameTag.Popup}>
<PersistGate persistor={getReduxPersistor()}> <RouterProvider router={router} />
<ExtensionStatsigProvider appName={DatadogAppNameTag.Popup}> </BaseAppContainer>
<I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}>
<ErrorBoundary>
<GraphqlProvider>
<BlankUrlProvider>
<LocalizationContextProvider>
<UnitagUpdaterContextProvider>
<TraceUserProperties />
<DappContextProvider>
<RouterProvider router={router} />
</DappContextProvider>
</UnitagUpdaterContextProvider>
</LocalizationContextProvider>
</BlankUrlProvider>
</GraphqlProvider>
</ErrorBoundary>
</SharedWalletProvider>
</I18nextProvider>
</ExtensionStatsigProvider>
</PersistGate>
</Trace>
) )
} }
...@@ -3,14 +3,11 @@ import 'src/app/Global.css' ...@@ -3,14 +3,11 @@ import 'src/app/Global.css'
import { SharedEventName } from '@uniswap/analytics-events' import { SharedEventName } from '@uniswap/analytics-events'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { I18nextProvider } from 'react-i18next'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { RouterProvider, createHashRouter } from 'react-router-dom' import { RouterProvider, createHashRouter } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
import { ExtensionStatsigProvider } from 'src/app/StatsigProvider'
import { GraphqlProvider } from 'src/app/apollo'
import { ErrorElement } from 'src/app/components/ErrorElement' import { ErrorElement } from 'src/app/components/ErrorElement'
import { TraceUserProperties } from 'src/app/components/Trace/TraceUserProperties' import { BaseAppContainer } from 'src/app/core/BaseAppContainer'
import { DatadogAppNameTag } from 'src/app/datadog' import { DatadogAppNameTag } from 'src/app/datadog'
import { AccountSwitcherScreen } from 'src/app/features/accounts/AccountSwitcherScreen' import { AccountSwitcherScreen } from 'src/app/features/accounts/AccountSwitcherScreen'
import { DappContextProvider } from 'src/app/features/dapp/DappContext' import { DappContextProvider } from 'src/app/features/dapp/DappContext'
...@@ -39,22 +36,16 @@ import { ...@@ -39,22 +36,16 @@ import {
} from 'src/background/messagePassing/messageChannels' } from 'src/background/messagePassing/messageChannels'
import { BackgroundToSidePanelRequestType } from 'src/background/messagePassing/types/requests' import { BackgroundToSidePanelRequestType } from 'src/background/messagePassing/types/requests'
import { PrimaryAppInstanceDebuggerLazy } from 'src/store/PrimaryAppInstanceDebuggerLazy' import { PrimaryAppInstanceDebuggerLazy } from 'src/store/PrimaryAppInstanceDebuggerLazy'
import { getReduxPersistor, getReduxStore } from 'src/store/store' import { getReduxPersistor } from 'src/store/store'
import { BlankUrlProvider } from 'uniswap/src/contexts/UrlContext'
import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext'
import { syncAppWithDeviceLanguage } from 'uniswap/src/features/settings/slice' import { syncAppWithDeviceLanguage } from 'uniswap/src/features/settings/slice'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { UnitagUpdaterContextProvider, useUnitagUpdater } from 'uniswap/src/features/unitags/context' import { UnitagUpdaterContextProvider, useUnitagUpdater } from 'uniswap/src/features/unitags/context'
import i18n from 'uniswap/src/i18n'
import { isDevEnv } from 'utilities/src/environment/env' import { isDevEnv } from 'utilities/src/environment/env'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { ONE_SECOND_MS } from 'utilities/src/time/time' import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { useInterval } from 'utilities/src/time/timing' import { useInterval } from 'utilities/src/time/timing'
import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary' import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks'
import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider'
const router = createHashRouter([ const router = createHashRouter([
{ {
...@@ -248,30 +239,15 @@ export default function SidebarApp(): JSX.Element { ...@@ -248,30 +239,15 @@ export default function SidebarApp(): JSX.Element {
}, [isLoggedIn]) }, [isLoggedIn])
return ( return (
<Trace> <PersistGate persistor={getReduxPersistor()}>
<PersistGate persistor={getReduxPersistor()}> <BaseAppContainer appName={DatadogAppNameTag.Sidebar}>
<ExtensionStatsigProvider appName={DatadogAppNameTag.Sidebar}> <UnitagUpdaterContextProvider>
<I18nextProvider i18n={i18n}> <DappContextProvider>
<SharedWalletProvider reduxStore={getReduxStore()}> <PrimaryAppInstanceDebuggerLazy />
<ErrorBoundary> <RouterProvider router={router} />
<GraphqlProvider> </DappContextProvider>
<BlankUrlProvider> </UnitagUpdaterContextProvider>
<LocalizationContextProvider> </BaseAppContainer>
<UnitagUpdaterContextProvider> </PersistGate>
<TraceUserProperties />
<DappContextProvider>
<PrimaryAppInstanceDebuggerLazy />
<RouterProvider router={router} />
</DappContextProvider>
</UnitagUpdaterContextProvider>
</LocalizationContextProvider>
</BlankUrlProvider>
</GraphqlProvider>
</ErrorBoundary>
</SharedWalletProvider>
</I18nextProvider>
</ExtensionStatsigProvider>
</PersistGate>
</Trace>
) )
} }
...@@ -2,13 +2,9 @@ import '@tamagui/core/reset.css' ...@@ -2,13 +2,9 @@ import '@tamagui/core/reset.css'
import 'src/app/Global.css' import 'src/app/Global.css'
import { PropsWithChildren, useEffect } from 'react' import { PropsWithChildren, useEffect } from 'react'
import { I18nextProvider } from 'react-i18next'
import { Outlet, RouterProvider, createHashRouter, 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 { ErrorElement } from 'src/app/components/ErrorElement'
import { TraceUserProperties } from 'src/app/components/Trace/TraceUserProperties' import { BaseAppContainer } from 'src/app/core/BaseAppContainer'
import { DatadogAppNameTag } from 'src/app/datadog' import { DatadogAppNameTag } from 'src/app/datadog'
import { import {
ClaimUnitagSteps, ClaimUnitagSteps,
...@@ -25,19 +21,12 @@ import { UnitagIntroScreen } from 'src/app/features/unitags/UnitagIntroScreen' ...@@ -25,19 +21,12 @@ import { UnitagIntroScreen } from 'src/app/features/unitags/UnitagIntroScreen'
import { UnitagClaimRoutes } from 'src/app/navigation/constants' import { UnitagClaimRoutes } from 'src/app/navigation/constants'
import { setRouter, setRouterState } from 'src/app/navigation/state' import { setRouter, setRouterState } from 'src/app/navigation/state'
import { initExtensionAnalytics } from 'src/app/utils/analytics' import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { getReduxPersistor, getReduxStore } from 'src/store/store'
import { Flex } from 'ui/src' import { Flex } from 'ui/src'
import { BlankUrlProvider } from 'uniswap/src/contexts/UrlContext'
import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context' import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context'
import i18n from 'uniswap/src/i18n'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { usePrevious } from 'utilities/src/react/hooks' import { usePrevious } from 'utilities/src/react/hooks'
import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary' import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks'
import { useAccountAddressFromUrlWithThrow } from 'wallet/src/features/wallet/hooks' import { useAccountAddressFromUrlWithThrow } from 'wallet/src/features/wallet/hooks'
import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider'
const router = createHashRouter([ const router = createHashRouter([
{ {
...@@ -149,27 +138,10 @@ export default function UnitagClaimApp(): JSX.Element { ...@@ -149,27 +138,10 @@ export default function UnitagClaimApp(): JSX.Element {
}, []) }, [])
return ( return (
<Trace> <BaseAppContainer appName={DatadogAppNameTag.UnitagClaim}>
<PersistGate persistor={getReduxPersistor()}> <UnitagUpdaterContextProvider>
<ExtensionStatsigProvider appName={DatadogAppNameTag.UnitagClaim}> <RouterProvider router={router} />
<I18nextProvider i18n={i18n}> </UnitagUpdaterContextProvider>
<SharedWalletProvider reduxStore={getReduxStore()}> </BaseAppContainer>
<ErrorBoundary>
<GraphqlProvider>
<BlankUrlProvider>
<LocalizationContextProvider>
<UnitagUpdaterContextProvider>
<TraceUserProperties />
<RouterProvider router={router} />
</UnitagUpdaterContextProvider>
</LocalizationContextProvider>
</BlankUrlProvider>
</GraphqlProvider>
</ErrorBoundary>
</SharedWalletProvider>
</I18nextProvider>
</ExtensionStatsigProvider>
</PersistGate>
</Trace>
) )
} }
...@@ -5,7 +5,7 @@ import { useInterfaceBuyNavigator } from 'src/app/features/for/utils' ...@@ -5,7 +5,7 @@ import { useInterfaceBuyNavigator } from 'src/app/features/for/utils'
import { AppRoutes } from 'src/app/navigation/constants' import { AppRoutes } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state' import { navigate } from 'src/app/navigation/state'
import { Flex, Text, getTokenValue, useMedia } from 'ui/src' import { Flex, Text, getTokenValue, useMedia } from 'ui/src'
import { ArrowDownCircle, Buy, CoinConvert, SendAction } from 'ui/src/components/icons' import { ArrowDownCircle, Bank, CoinConvert, SendAction } from 'ui/src/components/icons'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { ElementName } from 'uniswap/src/features/telemetry/constants' import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
...@@ -124,7 +124,7 @@ export const PortfolioActionButtons = memo(function _PortfolioActionButtons(): J ...@@ -124,7 +124,7 @@ export const PortfolioActionButtons = memo(function _PortfolioActionButtons(): J
/> />
<Flex row shrink gap="$spacing8" width={isGrid ? '100%' : '50%'}> <Flex row shrink gap="$spacing8" width={isGrid ? '100%' : '50%'}>
<ActionButton Icon={<CoinConvert />} label={t('home.label.swap')} onClick={onSwapClick} /> <ActionButton Icon={<CoinConvert />} label={t('home.label.swap')} onClick={onSwapClick} />
<ActionButton Icon={<Buy />} label={t('home.label.buy')} onClick={onBuyClick} /> <ActionButton Icon={<Bank />} label={t('home.label.buy')} onClick={onBuyClick} />
</Flex> </Flex>
<Flex row shrink gap="$spacing8" width={isGrid ? '100%' : '50%'}> <Flex row shrink gap="$spacing8" width={isGrid ? '100%' : '50%'}>
<ActionButton Icon={<SendAction />} label={t('home.label.send')} onClick={onSendClick} /> <ActionButton Icon={<SendAction />} label={t('home.label.send')} onClick={onSendClick} />
......
...@@ -8,8 +8,7 @@ import { ShieldCheck } from 'ui/src/components/icons' ...@@ -8,8 +8,7 @@ import { ShieldCheck } from 'ui/src/components/icons'
import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard'
import { InfoLinkModal } from 'uniswap/src/components/modals/InfoLinkModal' import { InfoLinkModal } from 'uniswap/src/components/modals/InfoLinkModal'
import { uniswapUrls } from 'uniswap/src/constants/urls' import { uniswapUrls } from 'uniswap/src/constants/urls'
import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { PortfolioBalance, TokenList } from 'uniswap/src/features/dataApi/types'
import { PortfolioBalance } from 'uniswap/src/features/dataApi/types'
import { ElementName, ModalName, WalletEventName } from 'uniswap/src/features/telemetry/constants' import { ElementName, ModalName, WalletEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { InformationBanner } from 'wallet/src/components/banners/InformationBanner' import { InformationBanner } from 'wallet/src/components/banners/InformationBanner'
...@@ -211,7 +210,7 @@ function TokenContextMenu({ ...@@ -211,7 +210,7 @@ function TokenContextMenu({
}>): JSX.Element { }>): JSX.Element {
const contextMenu = useTokenContextMenu({ const contextMenu = useTokenContextMenu({
currencyId: portfolioBalance.currencyInfo.currencyId, currencyId: portfolioBalance.currencyInfo.currencyId,
isBlocked: portfolioBalance.currencyInfo.safetyLevel === SafetyLevel.Blocked, isBlocked: portfolioBalance.currencyInfo.safetyInfo?.tokenList === TokenList.Blocked,
tokenSymbolForNotification: portfolioBalance?.currencyInfo?.currency?.symbol, tokenSymbolForNotification: portfolioBalance?.currencyInfo?.currency?.symbol,
portfolioBalance, portfolioBalance,
}) })
......
...@@ -8,7 +8,7 @@ import { OnboardingMessageType } from 'src/background/messagePassing/types/Exten ...@@ -8,7 +8,7 @@ import { OnboardingMessageType } from 'src/background/messagePassing/types/Exten
import { Flex, Image, useIsDarkMode } from 'ui/src' import { Flex, Image, useIsDarkMode } from 'ui/src'
import { syncAppWithDeviceLanguage } from 'uniswap/src/features/settings/slice' import { syncAppWithDeviceLanguage } from 'uniswap/src/features/settings/slice'
import { OnboardingContextProvider } from 'wallet/src/features/onboarding/OnboardingContext' import { OnboardingContextProvider } from 'wallet/src/features/onboarding/OnboardingContext'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks' import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics'
export function OnboardingWrapper(): JSX.Element { export function OnboardingWrapper(): JSX.Element {
const isDarkMode = useIsDarkMode() const isDarkMode = useIsDarkMode()
......
import { useCallback, useMemo, useRef } from 'react' import { AnimatePresence, Variants, motion } from 'framer-motion'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { NavigationType, Outlet, ScrollRestoration, useLocation } from 'react-router-dom' import { NavigationType, Outlet, ScrollRestoration, useLocation } from 'react-router-dom'
import { DappRequestQueue } from 'src/app/features/dappRequests/DappRequestQueue' import { DappRequestQueue } from 'src/app/features/dappRequests/DappRequestQueue'
...@@ -10,10 +11,10 @@ import { useIsWalletUnlocked } from 'src/app/hooks/useIsWalletUnlocked' ...@@ -10,10 +11,10 @@ import { useIsWalletUnlocked } from 'src/app/hooks/useIsWalletUnlocked'
import { HideContentsWhenSidebarBecomesInactive } from 'src/app/navigation/HideContentsWhenSidebarBecomesInactive' import { HideContentsWhenSidebarBecomesInactive } from 'src/app/navigation/HideContentsWhenSidebarBecomesInactive'
import { SideBarNavigationProvider } from 'src/app/navigation/SideBarNavigationProvider' import { SideBarNavigationProvider } from 'src/app/navigation/SideBarNavigationProvider'
import { AppRoutes } from 'src/app/navigation/constants' import { AppRoutes } from 'src/app/navigation/constants'
import { useRouterState } from 'src/app/navigation/state' import { RouterState, subscribeToRouterState, useRouterState } from 'src/app/navigation/state'
import { focusOrCreateOnboardingTab } from 'src/app/navigation/utils' import { focusOrCreateOnboardingTab } from 'src/app/navigation/utils'
import { isOnboardedSelector } from 'src/app/utils/isOnboardedSelector' import { isOnboardedSelector } from 'src/app/utils/isOnboardedSelector'
import { AnimatePresence, Flex, SpinningLoader, styled } from 'ui/src' import { Flex, SpinningLoader, styled } from 'ui/src'
import { TestnetModeBanner } from 'uniswap/src/components/banners/TestnetModeBanner' import { TestnetModeBanner } from 'uniswap/src/components/banners/TestnetModeBanner'
import { useIsChromeWindowFocusedWithTimeout } from 'uniswap/src/extension/useIsChromeWindowFocused' import { useIsChromeWindowFocusedWithTimeout } from 'uniswap/src/extension/useIsChromeWindowFocused'
import { useAsyncData, usePrevious } from 'utilities/src/react/hooks' import { useAsyncData, usePrevious } from 'utilities/src/react/hooks'
...@@ -71,7 +72,29 @@ const getAppRouteFromPathName = (pathname: string): AppRoutes | null => { ...@@ -71,7 +72,29 @@ const getAppRouteFromPathName = (pathname: string): AppRoutes | null => {
return null return null
} }
const animationVariant: Variants = {
initial: (dir: Direction) => ({
x: isVertical(dir) ? 0 : dir === 'right' ? -30 : 30,
y: !isVertical(dir) ? 0 : dir === 'down' ? -15 : 15,
opacity: 0,
zIndex: 1,
}),
animate: {
x: 0,
y: 0,
opacity: 1,
zIndex: 1,
},
exit: (dir: Direction) => ({
x: isVertical(dir) ? 0 : dir === 'left' ? -30 : 30,
y: !isVertical(dir) ? 0 : dir === 'up' ? -15 : 15,
opacity: 0,
zIndex: 0,
}),
}
export function WebNavigation(): JSX.Element { export function WebNavigation(): JSX.Element {
const [isTransitioning, setIsTransitioning] = useState(false)
const isLoggedIn = useIsWalletUnlocked() const isLoggedIn = useIsWalletUnlocked()
const { pathname } = useLocation() const { pathname } = useLocation()
const history = useRef<string[]>([]).current const history = useRef<string[]>([]).current
...@@ -96,36 +119,49 @@ export function WebNavigation(): JSX.Element { ...@@ -96,36 +119,49 @@ export function WebNavigation(): JSX.Element {
const prevPathname = usePrevious(pathname) const prevPathname = usePrevious(pathname)
const shouldRestoreScroll = pathname !== prevPathname const shouldRestoreScroll = pathname !== prevPathname
useEffect(() => {
// We're using subscribeToRouterState subscriber to detect, whether we will
// navigate to another page, which will lead to the start of the animation.
subscribeToRouterState(({ historyAction, location }: RouterState) => {
const trimmedPathname = location.pathname.replace('/', '') as AppRoutes
if (historyAction !== NavigationType.Replace && Object.values(AppRoutes).includes(trimmedPathname)) {
setIsTransitioning(true)
}
})
}, [])
const childrenMemo = useMemo(() => { const childrenMemo = useMemo(() => {
return ( return (
<AnimatePresence custom={{ towards }} initial={false}> <OverflowControlledFlex isTransitioning={isTransitioning}>
<AnimatedPane <AnimatePresence initial={false}>
key={pathname} <MotionFlex
animation={[ key={pathname}
isVertical(towards) ? 'quicker' : '100ms', variants={animationVariant}
{ custom={towards}
opacity: { initial="initial"
overshootClamping: true, animate="animate"
}, exit="exit"
}, onAnimationComplete={() => {
]} setIsTransitioning(false)
> }}
<Flex fill grow overflow="visible"> >
<TestnetModeBanner /> <Flex fill grow overflow="visible">
{isLoggedIn === null ? ( <TestnetModeBanner />
<Loading /> {isLoggedIn === null ? (
) : isLoggedIn === true ? ( <Loading />
<HideContentsWhenSidebarBecomesInactive> ) : isLoggedIn === true ? (
<LoggedIn /> <HideContentsWhenSidebarBecomesInactive>
</HideContentsWhenSidebarBecomesInactive> <LoggedIn />
) : ( </HideContentsWhenSidebarBecomesInactive>
<LoggedOut /> ) : (
)} <LoggedOut />
</Flex> )}
</AnimatedPane> </Flex>
</AnimatePresence> </MotionFlex>
</AnimatePresence>
</OverflowControlledFlex>
) )
}, [isLoggedIn, pathname, towards]) }, [isLoggedIn, pathname, towards, isTransitioning])
return ( return (
<SideBarNavigationProvider> <SideBarNavigationProvider>
...@@ -138,16 +174,7 @@ export function WebNavigation(): JSX.Element { ...@@ -138,16 +174,7 @@ export function WebNavigation(): JSX.Element {
) )
} }
// TODO(EXT-994): improve this loading screen. const MotionFlex = styled(motion(Flex), {
function Loading(): JSX.Element {
return (
<Flex centered grow>
<SpinningLoader />
</Flex>
)
}
const AnimatedPane = styled(Flex, {
zIndex: 1, zIndex: 1,
fill: true, fill: true,
position: 'absolute', position: 'absolute',
...@@ -158,25 +185,31 @@ const AnimatedPane = styled(Flex, { ...@@ -158,25 +185,31 @@ const AnimatedPane = styled(Flex, {
minHeight: '100vh', minHeight: '100vh',
mx: 'auto', mx: 'auto',
width: '100%', width: '100%',
variants: {
towards: (dir: Direction) => ({
enterStyle: {
x: isVertical(dir) ? 0 : dir === 'right' ? 30 : -30,
y: !isVertical(dir) ? 0 : dir === 'down' ? 15 : -15,
opacity: 0,
zIndex: 1,
},
exitStyle: {
zIndex: 0,
x: isVertical(dir) ? 0 : dir === 'left' ? 30 : -30,
y: !isVertical(dir) ? 0 : dir === 'up' ? 15 : -15,
opacity: 0,
},
}),
} as const,
}) })
function OverflowControlledFlex({
children,
isTransitioning,
}: React.PropsWithChildren & { isTransitioning: boolean }): JSX.Element {
if (!isTransitioning) {
return <Flex fill>{children}</Flex>
}
return (
<Flex fill overflow="hidden">
{children}
</Flex>
)
}
// TODO(EXT-994): improve this loading screen.
function Loading(): JSX.Element {
return (
<Flex centered grow>
<SpinningLoader />
</Flex>
)
}
const isVertical = (dir: Direction): boolean => dir === 'up' || dir === 'down' const isVertical = (dir: Direction): boolean => dir === 'up' || dir === 'down'
function useConstant<A>(c: A): A { function useConstant<A>(c: A): A {
......
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Location, NavigationType, Router, createHashRouter } from 'react-router-dom' import { Location, NavigationType, Router, createHashRouter } from 'react-router-dom'
interface RouterState { export interface RouterState {
historyAction: NavigationType historyAction: NavigationType
location: Location location: Location
} }
......
import 'symbol-observable' // Needed by `reduxed-chrome-storage` as polyfill, order matters import 'symbol-observable' // Needed by `reduxed-chrome-storage` as polyfill, order matters
import { initStatSigForBrowserScripts } from 'src/app/StatsigProvider' import { initStatSigForBrowserScripts } from 'src/app/core/StatsigProvider'
import { focusOrCreateOnboardingTab } from 'src/app/navigation/utils' import { focusOrCreateOnboardingTab } from 'src/app/navigation/utils'
import { initExtensionAnalytics } from 'src/app/utils/analytics' import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { initMessageBridge } from 'src/background/backgroundDappRequests' import { initMessageBridge } from 'src/background/backgroundDappRequests'
......
import { focusOrCreateDappRequestWindow } from 'src/app/navigation/utils' import { focusOrCreateDappRequestWindow } from 'src/app/navigation/utils'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
export async function openSidePanel(tabId: number | undefined, windowId: number): Promise<void> { export async function openSidePanel(tabId: number | undefined, windowId: number): Promise<void> {
let hasError = false
try { try {
// eslint-disable-next-line security/detect-non-literal-fs-filename // eslint-disable-next-line security/detect-non-literal-fs-filename
await chrome.sidePanel.open({ await chrome.sidePanel.open({
...@@ -13,12 +16,15 @@ export async function openSidePanel(tabId: number | undefined, windowId: number) ...@@ -13,12 +16,15 @@ export async function openSidePanel(tabId: number | undefined, windowId: number)
// Consider removing this once the issue is resolved or leaving as fallback // Consider removing this once the issue is resolved or leaving as fallback
await focusOrCreateDappRequestWindow(tabId, windowId) await focusOrCreateDappRequestWindow(tabId, windowId)
hasError = true
logger.error(error, { logger.error(error, {
tags: { tags: {
file: 'background/background.ts', file: 'background/background.ts',
function: 'openSidebar', function: 'openSidebar',
}, },
}) })
} finally {
sendAnalyticsEvent(ExtensionEventName.BackgroundAttemptedToOpenSidebar, { hasError })
} }
} }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
import React from 'react' import React from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import OnboardingApp from 'src/app/OnboardingApp' import OnboardingApp from 'src/app/core/OnboardingApp'
import { initializeReduxStore } from 'src/store/store' import { initializeReduxStore } from 'src/store/store'
import { ExtensionAppLocation, StoreSynchronization } from 'src/store/storeSynchronization' import { ExtensionAppLocation, StoreSynchronization } from 'src/store/storeSynchronization'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import PopupApp from 'src/app/PopupApp' import PopupApp from 'src/app/core/PopupApp'
import { initializeReduxStore } from 'src/store/store' import { initializeReduxStore } from 'src/store/store'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any ;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any
......
...@@ -6,7 +6,7 @@ import 'symbol-observable' // Needed by `reduxed-chrome-storage` as polyfill, or ...@@ -6,7 +6,7 @@ import 'symbol-observable' // Needed by `reduxed-chrome-storage` as polyfill, or
import React from 'react' import React from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import SidebarApp from 'src/app/SidebarApp' import SidebarApp from 'src/app/core/SidebarApp'
import { onboardingMessageChannel } from 'src/background/messagePassing/messageChannels' import { onboardingMessageChannel } from 'src/background/messagePassing/messageChannels'
import { OnboardingMessageType } from 'src/background/messagePassing/types/ExtensionMessages' import { OnboardingMessageType } from 'src/background/messagePassing/types/ExtensionMessages'
import { initializeReduxStore } from 'src/store/store' import { initializeReduxStore } from 'src/store/store'
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import UnitagClaimApp from 'src/app/UnitagClaimApp' import UnitagClaimApp from 'src/app/core/UnitagClaimApp'
import { initializeReduxStore } from 'src/store/store' import { initializeReduxStore } from 'src/store/store'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any ;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "Uniswap Extension", "name": "Uniswap Extension",
"description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.", "description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.",
"version": "1.16.0", "version": "1.17.0",
"minimum_chrome_version": "116", "minimum_chrome_version": "116",
"icons": { "icons": {
"16": "assets/icon16.png", "16": "assets/icon16.png",
......
...@@ -12,7 +12,7 @@ import { ...@@ -12,7 +12,7 @@ import {
readDeprecatedReduxedChromeStorage, readDeprecatedReduxedChromeStorage,
} from 'src/store/reduxedChromeStorageToReduxPersistMigration' } from 'src/store/reduxedChromeStorageToReduxPersistMigration'
import { fiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api' import { fiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api'
import { createDatadogReduxEnhancer } from 'utilities/src/logger/Datadog' import { createDatadogReduxEnhancer } from 'utilities/src/logger/datadog/Datadog'
import { createStore } from 'wallet/src/state' import { createStore } from 'wallet/src/state'
import { createMigrate } from 'wallet/src/state/createMigrate' import { createMigrate } from 'wallet/src/state/createMigrate'
......
...@@ -18,7 +18,6 @@ ignores: [ ...@@ -18,7 +18,6 @@ ignores: [
## React Native Usage ## React Native Usage
'@amplitude/analytics-react-native', '@amplitude/analytics-react-native',
'@react-native-masked-view/masked-view', '@react-native-masked-view/masked-view',
'@react-native-firebase/app-check',
'@shopify/react-native-skia', '@shopify/react-native-skia',
'react-native-image-colors', 'react-native-image-colors',
'react-native-restart', 'react-native-restart',
......
import { StorybookConfig } from '@storybook/react-native' import type { StorybookConfig } from '@storybook/react-native'
const main: StorybookConfig = { const config: StorybookConfig = {
stories: ['../src/**/*.stories.?(ts|tsx|js|jsx)', '../../../packages/ui/src/**/*.stories.?(ts|tsx|js|jsx)'], stories: [
'../src/**/*.stories.?(ts|tsx|js|jsx)',
'../../../packages/ui/src/**/*.stories.?(ts|tsx|js|jsx)',
'../../../packages/uniswap/src/**/*.stories.?(ts|tsx|js|jsx)',
],
addons: ['@storybook/addon-ondevice-controls'], addons: ['@storybook/addon-ondevice-controls'],
} }
export default main export default config
...@@ -31,6 +31,19 @@ const normalizedStories = [ ...@@ -31,6 +31,19 @@ const normalizedStories = [
/^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/ /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/
), ),
}, },
{
titlePrefix: "",
directory: "../../packages/uniswap/src",
files: "**/*.stories.?(ts|tsx|js|jsx)",
importPathMatcher:
/^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/,
// @ts-ignore
req: require.context(
"../../../packages/uniswap/src",
true,
/^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/
),
},
]; ];
declare global { declare global {
......
...@@ -72,9 +72,9 @@ if (isCI && datadogPropertiesAvailable && !isE2E) { ...@@ -72,9 +72,9 @@ if (isCI && datadogPropertiesAvailable && !isE2E) {
apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle" apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle"
} }
def devVersionName = "1.46" def devVersionName = "1.47"
def betaVersionName = "1.46" def betaVersionName = "1.47"
def prodVersionName = "1.46" def prodVersionName = "1.47"
android { android {
ndkVersion rootProject.ext.ndkVersion ndkVersion rootProject.ext.ndkVersion
......
...@@ -1150,10 +1150,6 @@ PODS: ...@@ -1150,10 +1150,6 @@ PODS:
- Apollo (1.2.1): - Apollo (1.2.1):
- Apollo/Core (= 1.2.1) - Apollo/Core (= 1.2.1)
- Apollo/Core (1.2.1) - Apollo/Core (1.2.1)
- AppCheckCore (11.2.0):
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4)
- AppsFlyerFramework (6.13.1): - AppsFlyerFramework (6.13.1):
- AppsFlyerFramework/Main (= 6.13.1) - AppsFlyerFramework/Main (= 6.13.1)
- AppsFlyerFramework/Main (6.13.1) - AppsFlyerFramework/Main (6.13.1)
...@@ -1248,9 +1244,6 @@ PODS: ...@@ -1248,9 +1244,6 @@ PODS:
- ExpoWebBrowser (13.0.3): - ExpoWebBrowser (13.0.3):
- ExpoModulesCore - ExpoModulesCore
- FBLazyVector (0.76.6) - FBLazyVector (0.76.6)
- Firebase/AppCheck (11.2.0):
- Firebase/CoreOnly
- FirebaseAppCheck (~> 11.2.0)
- Firebase/Auth (11.2.0): - Firebase/Auth (11.2.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseAuth (~> 11.2.0) - FirebaseAuth (~> 11.2.0)
...@@ -1259,12 +1252,6 @@ PODS: ...@@ -1259,12 +1252,6 @@ PODS:
- Firebase/Firestore (11.2.0): - Firebase/Firestore (11.2.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseFirestore (~> 11.2.0) - FirebaseFirestore (~> 11.2.0)
- FirebaseAppCheck (11.2.0):
- AppCheckCore (~> 11.0)
- FirebaseAppCheckInterop (~> 11.0)
- FirebaseCore (~> 11.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- FirebaseAppCheckInterop (11.7.0) - FirebaseAppCheckInterop (11.7.0)
- FirebaseAuth (11.2.0): - FirebaseAuth (11.2.0):
- FirebaseAppCheckInterop (~> 11.0) - FirebaseAppCheckInterop (~> 11.0)
...@@ -1328,9 +1315,6 @@ PODS: ...@@ -1328,9 +1315,6 @@ PODS:
- GoogleUtilities/Reachability (8.0.2): - GoogleUtilities/Reachability (8.0.2):
- GoogleUtilities/Logger - GoogleUtilities/Logger
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
- GoogleUtilities/UserDefaults (8.0.2):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- "gRPC-C++ (1.65.5)": - "gRPC-C++ (1.65.5)":
- "gRPC-C++/Implementation (= 1.65.5)" - "gRPC-C++/Implementation (= 1.65.5)"
- "gRPC-C++/Interface (= 1.65.5)" - "gRPC-C++/Interface (= 1.65.5)"
...@@ -1458,7 +1442,6 @@ PODS: ...@@ -1458,7 +1442,6 @@ PODS:
- OneSignalXCFramework/OneSignalCore - OneSignalXCFramework/OneSignalCore
- OpenTelemetrySwiftApi (1.6.0) - OpenTelemetrySwiftApi (1.6.0)
- PLCrashReporter (1.11.2) - PLCrashReporter (1.11.2)
- PromisesObjC (2.4.0)
- RCT-Folly (2024.01.01.00): - RCT-Folly (2024.01.01.00):
- boost - boost
- DoubleConversion - DoubleConversion
...@@ -2697,7 +2680,7 @@ PODS: ...@@ -2697,7 +2680,7 @@ PODS:
- react-native-appsflyer (6.13.1): - react-native-appsflyer (6.13.1):
- AppsFlyerFramework (= 6.13.1) - AppsFlyerFramework (= 6.13.1)
- React - React
- react-native-compat (2.17.1): - react-native-compat (2.18.0):
- DoubleConversion - DoubleConversion
- glog - glog
- hermes-engine - hermes-engine
...@@ -3149,10 +3132,6 @@ PODS: ...@@ -3149,10 +3132,6 @@ PODS:
- RNFBApp (21.0.0): - RNFBApp (21.0.0):
- Firebase/CoreOnly (= 11.2.0) - Firebase/CoreOnly (= 11.2.0)
- React-Core - React-Core
- RNFBAppCheck (21.0.0):
- Firebase/AppCheck (= 11.2.0)
- React-Core
- RNFBApp
- RNFBAuth (21.0.0): - RNFBAuth (21.0.0):
- Firebase/Auth (= 11.2.0) - Firebase/Auth (= 11.2.0)
- React-Core - React-Core
...@@ -3209,6 +3188,9 @@ PODS: ...@@ -3209,6 +3188,9 @@ PODS:
- React-Core - React-Core
- RNPermissions (4.1.5): - RNPermissions (4.1.5):
- React-Core - React-Core
- RNQrGenerator (1.4.3):
- React
- ZXingObjC
- RNReanimated (3.16.7): - RNReanimated (3.16.7):
- DoubleConversion - DoubleConversion
- glog - glog
...@@ -3332,6 +3314,9 @@ PODS: ...@@ -3332,6 +3314,9 @@ PODS:
- Statsig (1.49.0) - Statsig (1.49.0)
- UIImageColors (2.1.0) - UIImageColors (2.1.0)
- Yoga (0.0.0) - Yoga (0.0.0)
- ZXingObjC (3.6.9):
- ZXingObjC/All (= 3.6.9)
- ZXingObjC/All (3.6.9)
- ZXingObjC/Core (3.6.9) - ZXingObjC/Core (3.6.9)
- ZXingObjC/OneD (3.6.9): - ZXingObjC/OneD (3.6.9):
- ZXingObjC/Core - ZXingObjC/Core
...@@ -3449,7 +3434,6 @@ DEPENDENCIES: ...@@ -3449,7 +3434,6 @@ DEPENDENCIES:
- RNDeviceInfo (from `../../../node_modules/react-native-device-info`) - RNDeviceInfo (from `../../../node_modules/react-native-device-info`)
- RNFastImage (from `../../../node_modules/react-native-fast-image`) - RNFastImage (from `../../../node_modules/react-native-fast-image`)
- "RNFBApp (from `../../../node_modules/@react-native-firebase/app`)" - "RNFBApp (from `../../../node_modules/@react-native-firebase/app`)"
- "RNFBAppCheck (from `../../../node_modules/@react-native-firebase/app-check`)"
- "RNFBAuth (from `../../../node_modules/@react-native-firebase/auth`)" - "RNFBAuth (from `../../../node_modules/@react-native-firebase/auth`)"
- "RNFBFirestore (from `../../../node_modules/@react-native-firebase/firestore`)" - "RNFBFirestore (from `../../../node_modules/@react-native-firebase/firestore`)"
- "RNFlashList (from `../../../node_modules/@shopify/flash-list`)" - "RNFlashList (from `../../../node_modules/@shopify/flash-list`)"
...@@ -3457,6 +3441,7 @@ DEPENDENCIES: ...@@ -3457,6 +3441,7 @@ DEPENDENCIES:
- RNImageColors (from `../../../node_modules/react-native-image-colors`) - RNImageColors (from `../../../node_modules/react-native-image-colors`)
- RNLocalize (from `../../../node_modules/react-native-localize`) - RNLocalize (from `../../../node_modules/react-native-localize`)
- RNPermissions (from `../../../node_modules/react-native-permissions`) - RNPermissions (from `../../../node_modules/react-native-permissions`)
- RNQrGenerator (from `../../../node_modules/rn-qr-generator`)
- RNReanimated (from `../../../node_modules/react-native-reanimated`) - RNReanimated (from `../../../node_modules/react-native-reanimated`)
- RNScreens (from `../../../node_modules/react-native-screens`) - RNScreens (from `../../../node_modules/react-native-screens`)
- RNSVG (from `../../../node_modules/react-native-svg`) - RNSVG (from `../../../node_modules/react-native-svg`)
...@@ -3469,7 +3454,6 @@ SPEC REPOS: ...@@ -3469,7 +3454,6 @@ SPEC REPOS:
trunk: trunk:
- abseil - abseil
- Apollo - Apollo
- AppCheckCore
- AppsFlyerFramework - AppsFlyerFramework
- Argon2Swift - Argon2Swift
- BoringSSL-GRPC - BoringSSL-GRPC
...@@ -3481,7 +3465,6 @@ SPEC REPOS: ...@@ -3481,7 +3465,6 @@ SPEC REPOS:
- DatadogTrace - DatadogTrace
- DatadogWebViewTracking - DatadogWebViewTracking
- Firebase - Firebase
- FirebaseAppCheck
- FirebaseAppCheckInterop - FirebaseAppCheckInterop
- FirebaseAuth - FirebaseAuth
- FirebaseAuthInterop - FirebaseAuthInterop
...@@ -3503,7 +3486,6 @@ SPEC REPOS: ...@@ -3503,7 +3486,6 @@ SPEC REPOS:
- OneSignalXCFramework - OneSignalXCFramework
- OpenTelemetrySwiftApi - OpenTelemetrySwiftApi
- PLCrashReporter - PLCrashReporter
- PromisesObjC
- RecaptchaInterop - RecaptchaInterop
- SDWebImage - SDWebImage
- SDWebImageWebPCoder - SDWebImageWebPCoder
...@@ -3724,8 +3706,6 @@ EXTERNAL SOURCES: ...@@ -3724,8 +3706,6 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/react-native-fast-image" :path: "../../../node_modules/react-native-fast-image"
RNFBApp: RNFBApp:
:path: "../../../node_modules/@react-native-firebase/app" :path: "../../../node_modules/@react-native-firebase/app"
RNFBAppCheck:
:path: "../../../node_modules/@react-native-firebase/app-check"
RNFBAuth: RNFBAuth:
:path: "../../../node_modules/@react-native-firebase/auth" :path: "../../../node_modules/@react-native-firebase/auth"
RNFBFirestore: RNFBFirestore:
...@@ -3740,6 +3720,8 @@ EXTERNAL SOURCES: ...@@ -3740,6 +3720,8 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/react-native-localize" :path: "../../../node_modules/react-native-localize"
RNPermissions: RNPermissions:
:path: "../../../node_modules/react-native-permissions" :path: "../../../node_modules/react-native-permissions"
RNQrGenerator:
:path: "../../../node_modules/rn-qr-generator"
RNReanimated: RNReanimated:
:path: "../../../node_modules/react-native-reanimated" :path: "../../../node_modules/react-native-reanimated"
RNScreens: RNScreens:
...@@ -3755,7 +3737,6 @@ SPEC CHECKSUMS: ...@@ -3755,7 +3737,6 @@ SPEC CHECKSUMS:
abseil: d121da9ef7e2ff4cab7666e76c5a3e0915ae08c3 abseil: d121da9ef7e2ff4cab7666e76c5a3e0915ae08c3
amplitude-react-native: 9d57e1bcc4175039e36283390aa3daeaea9441a5 amplitude-react-native: 9d57e1bcc4175039e36283390aa3daeaea9441a5
Apollo: fe380f40e55e501a2499dd5885fab0cdf082b2bb Apollo: fe380f40e55e501a2499dd5885fab0cdf082b2bb
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
AppsFlyerFramework: 971521cf5b890c2afeab2f2c91734547b8b169ca AppsFlyerFramework: 971521cf5b890c2afeab2f2c91734547b8b169ca
Argon2Swift: 99482c1b8122a03524b61e41c4903a9548e7c33b Argon2Swift: 99482c1b8122a03524b61e41c4903a9548e7c33b
boost: 1dca942403ed9342f98334bf4c3621f011aa7946 boost: 1dca942403ed9342f98334bf4c3621f011aa7946
...@@ -3789,7 +3770,6 @@ SPEC CHECKSUMS: ...@@ -3789,7 +3770,6 @@ SPEC CHECKSUMS:
ExpoWebBrowser: 7595ccac6938eb65b076385fd23d035db9ecdc8e ExpoWebBrowser: 7595ccac6938eb65b076385fd23d035db9ecdc8e
FBLazyVector: be509404b5de73a64a74284edcaf73a5d1e128b1 FBLazyVector: be509404b5de73a64a74284edcaf73a5d1e128b1
Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c
FirebaseAppCheck: a6a1c1ca169d795212b9e70b5cfb880083a28e7c
FirebaseAppCheckInterop: 2376d3ec5cb4267facad4fe754ab4f301a5a519b FirebaseAppCheckInterop: 2376d3ec5cb4267facad4fe754ab4f301a5a519b
FirebaseAuth: 2a198b8cdbbbd457f08d74df7040feb0a0e7777a FirebaseAuth: 2a198b8cdbbbd457f08d74df7040feb0a0e7777a
FirebaseAuthInterop: a6973d72aa242ea88ffb6be9c9b06c65455071da FirebaseAuthInterop: a6973d72aa242ea88ffb6be9c9b06c65455071da
...@@ -3814,7 +3794,6 @@ SPEC CHECKSUMS: ...@@ -3814,7 +3794,6 @@ SPEC CHECKSUMS:
OneSignalXCFramework: ff1c970b7aeb4ac0fe48fb35393eb5d8bf378135 OneSignalXCFramework: ff1c970b7aeb4ac0fe48fb35393eb5d8bf378135
OpenTelemetrySwiftApi: 657da8071c2908caecce11548e006f779924ff9c OpenTelemetrySwiftApi: 657da8071c2908caecce11548e006f779924ff9c
PLCrashReporter: 499c53b0104f95c302d94fd723ebb03c56d9bac8 PLCrashReporter: 499c53b0104f95c302d94fd723ebb03c56d9bac8
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
RCT-Folly: bf5c0376ffe4dd2cf438dcf86db385df9fdce648 RCT-Folly: bf5c0376ffe4dd2cf438dcf86db385df9fdce648
RCTDeprecation: 063fc281b30b7dc944c98fe53a7e266dab1a8706 RCTDeprecation: 063fc281b30b7dc944c98fe53a7e266dab1a8706
RCTRequired: 8eda2a5a745f6081157a4f34baac40b65fe02b31 RCTRequired: 8eda2a5a745f6081157a4f34baac40b65fe02b31
...@@ -3845,7 +3824,7 @@ SPEC CHECKSUMS: ...@@ -3845,7 +3824,7 @@ SPEC CHECKSUMS:
React-Mapbuffer: eab34f6d54d26931c5f70eb19da1e36162d87bbd React-Mapbuffer: eab34f6d54d26931c5f70eb19da1e36162d87bbd
React-microtasksnativemodule: 9866981c8f1e80bb819e34f4ea45870cb3e6afaa React-microtasksnativemodule: 9866981c8f1e80bb819e34f4ea45870cb3e6afaa
react-native-appsflyer: 2cc1f96348065fc23e976fc7a27e371789fb349e react-native-appsflyer: 2cc1f96348065fc23e976fc7a27e371789fb349e
react-native-compat: ea7b6b73dbd2503594a287d7fb41660ee4f83f40 react-native-compat: f493f09bd0e990f72188d628dfc0a89cbb3122ed
react-native-context-menu-view: dcec18eb8882e20596dbb75802e7d19cb87dac02 react-native-context-menu-view: dcec18eb8882e20596dbb75802e7d19cb87dac02
react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06 react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06
react-native-image-picker: 00f0e4aae2710ad1ffbc72f65dfe0e396f6b6508 react-native-image-picker: 00f0e4aae2710ad1ffbc72f65dfe0e396f6b6508
...@@ -3895,7 +3874,6 @@ SPEC CHECKSUMS: ...@@ -3895,7 +3874,6 @@ SPEC CHECKSUMS:
RNDeviceInfo: 0a7c1d2532aa7691f9b9925a27e43af006db4dae RNDeviceInfo: 0a7c1d2532aa7691f9b9925a27e43af006db4dae
RNFastImage: 246de6b52d7642992cfd01e2005dda36d00a6660 RNFastImage: 246de6b52d7642992cfd01e2005dda36d00a6660
RNFBApp: 4122dd41d8d7ff017b6ecf777a6224f5b349ca04 RNFBApp: 4122dd41d8d7ff017b6ecf777a6224f5b349ca04
RNFBAppCheck: 2625a4cd0bcb11b409cbe47186e29104603d4d34
RNFBAuth: 1632cefd787a43ba952fa52ff016e7b69fe355cb RNFBAuth: 1632cefd787a43ba952fa52ff016e7b69fe355cb
RNFBFirestore: 5f110e37b7f7f3d6e03c85044dd4cf3ebacec38b RNFBFirestore: 5f110e37b7f7f3d6e03c85044dd4cf3ebacec38b
RNFlashList: 997257a094906e3e254183ef90fcf7a5a6a2612d RNFlashList: 997257a094906e3e254183ef90fcf7a5a6a2612d
...@@ -3903,6 +3881,7 @@ SPEC CHECKSUMS: ...@@ -3903,6 +3881,7 @@ SPEC CHECKSUMS:
RNImageColors: 9ac05083b52d5c350e6972650ae3ba0e556466c1 RNImageColors: 9ac05083b52d5c350e6972650ae3ba0e556466c1
RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81
RNPermissions: 87aac13521bea6dcb6dfd60b03ac69741ccef2b4 RNPermissions: 87aac13521bea6dcb6dfd60b03ac69741ccef2b4
RNQrGenerator: ac6a6c766e80dd3625038929ed2b13e2f3edcafb
RNReanimated: 283b723ad4ac5295f1513519c938cb6c282c508f RNReanimated: 283b723ad4ac5295f1513519c938cb6c282c508f
RNScreens: 906192367b418a8d644090d7375d4657d5a5aab0 RNScreens: 906192367b418a8d644090d7375d4657d5a5aab0
RNSVG: 7ff26379b2d1871b8571e6f9bc9630de6baf9bdf RNSVG: 7ff26379b2d1871b8571e6f9bc9630de6baf9bdf
......
...@@ -2226,7 +2226,7 @@ ...@@ -2226,7 +2226,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -2279,7 +2279,7 @@ ...@@ -2279,7 +2279,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
...@@ -2332,7 +2332,7 @@ ...@@ -2332,7 +2332,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
...@@ -2385,7 +2385,7 @@ ...@@ -2385,7 +2385,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
...@@ -2423,7 +2423,7 @@ ...@@ -2423,7 +2423,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.1; IPHONEOS_DEPLOYMENT_TARGET = 15.1;
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -2459,7 +2459,7 @@ ...@@ -2459,7 +2459,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.1; IPHONEOS_DEPLOYMENT_TARGET = 15.1;
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
...@@ -2494,7 +2494,7 @@ ...@@ -2494,7 +2494,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.1; IPHONEOS_DEPLOYMENT_TARGET = 15.1;
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
...@@ -2529,7 +2529,7 @@ ...@@ -2529,7 +2529,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.1; IPHONEOS_DEPLOYMENT_TARGET = 15.1;
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
...@@ -2576,7 +2576,7 @@ ...@@ -2576,7 +2576,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -2622,7 +2622,7 @@ ...@@ -2622,7 +2622,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets;
...@@ -2668,7 +2668,7 @@ ...@@ -2668,7 +2668,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets;
...@@ -2714,7 +2714,7 @@ ...@@ -2714,7 +2714,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets;
...@@ -2756,7 +2756,7 @@ ...@@ -2756,7 +2756,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -2799,7 +2799,7 @@ ...@@ -2799,7 +2799,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension;
...@@ -2842,7 +2842,7 @@ ...@@ -2842,7 +2842,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension;
...@@ -2885,7 +2885,7 @@ ...@@ -2885,7 +2885,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension;
...@@ -2921,7 +2921,7 @@ ...@@ -2921,7 +2921,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -2959,7 +2959,7 @@ ...@@ -2959,7 +2959,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -3161,7 +3161,7 @@ ...@@ -3161,7 +3161,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -3206,7 +3206,7 @@ ...@@ -3206,7 +3206,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension;
...@@ -3317,7 +3317,7 @@ ...@@ -3317,7 +3317,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -3389,7 +3389,7 @@ ...@@ -3389,7 +3389,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension;
...@@ -3500,7 +3500,7 @@ ...@@ -3500,7 +3500,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -3572,7 +3572,7 @@ ...@@ -3572,7 +3572,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.46; MARKETING_VERSION = 1.47;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension;
......
#import "AppDelegate.h" #import "AppDelegate.h"
#import "RNFBAppCheckModule.h"
#import <Firebase.h> #import <Firebase.h>
#import "Uniswap-Swift.h" #import "Uniswap-Swift.h"
...@@ -17,8 +16,6 @@ ...@@ -17,8 +16,6 @@
// Must be first line in startup routine // Must be first line in startup routine
[ReactNativePerformance onAppStarted]; [ReactNativePerformance onAppStarted];
// Must be before [FIRApp configure], initializes RNFBAppCheckModule
[RNFBAppCheckModule sharedInstance];
[FIRApp configure]; [FIRApp configure];
// This is needed so universal links opened from OneSignal notifications navigate to the proper page. // This is needed so universal links opened from OneSignal notifications navigate to the proper page.
......
...@@ -73,14 +73,6 @@ jest.mock('@react-native-firebase/auth', () => () => ({ ...@@ -73,14 +73,6 @@ jest.mock('@react-native-firebase/auth', () => () => ({
signInAnonymously: jest.fn(), signInAnonymously: jest.fn(),
})) }))
jest.mock('@react-native-firebase/app-check', () => () => ({
appCheck: jest.fn(),
newReactNativeFirebaseAppCheckProvider: jest.fn(() => ({
configure: jest.fn(),
})),
initializeAppCheck: jest.fn().mockReturnValue(Promise.resolve()), // Return a resolved Promise
}))
jest.mock('react-native/Libraries/Linking/Linking', () => ({ jest.mock('react-native/Libraries/Linking/Linking', () => ({
openURL: jest.fn(), openURL: jest.fn(),
addEventListener: jest.fn(), addEventListener: jest.fn(),
......
...@@ -71,7 +71,6 @@ ...@@ -71,7 +71,6 @@
"@react-native-community/cli-platform-ios": "15.0.1", "@react-native-community/cli-platform-ios": "15.0.1",
"@react-native-community/netinfo": "11.4.1", "@react-native-community/netinfo": "11.4.1",
"@react-native-firebase/app": "21.0.0", "@react-native-firebase/app": "21.0.0",
"@react-native-firebase/app-check": "21.0.0",
"@react-native-firebase/auth": "21.0.0", "@react-native-firebase/auth": "21.0.0",
"@react-native-firebase/firestore": "21.0.0", "@react-native-firebase/firestore": "21.0.0",
"@react-native-masked-view/masked-view": "0.3.2", "@react-native-masked-view/masked-view": "0.3.2",
...@@ -93,9 +92,9 @@ ...@@ -93,9 +92,9 @@
"@uniswap/client-explore": "0.0.15", "@uniswap/client-explore": "0.0.15",
"@uniswap/ethers-rs-mobile": "0.0.5", "@uniswap/ethers-rs-mobile": "0.0.5",
"@uniswap/sdk-core": "7.5.0", "@uniswap/sdk-core": "7.5.0",
"@walletconnect/core": "2.17.1", "@walletconnect/core": "2.18.0",
"@walletconnect/react-native-compat": "2.17.1", "@walletconnect/react-native-compat": "2.18.0",
"@walletconnect/utils": "2.17.1", "@walletconnect/utils": "2.18.0",
"apollo3-cache-persist": "0.14.1", "apollo3-cache-persist": "0.14.1",
"babel-plugin-transform-inline-environment-variables": "0.4.4", "babel-plugin-transform-inline-environment-variables": "0.4.4",
"babel-plugin-transform-remove-console": "6.9.4", "babel-plugin-transform-remove-console": "6.9.4",
...@@ -142,6 +141,7 @@ ...@@ -142,6 +141,7 @@
"react-native-restart": "0.0.27", "react-native-restart": "0.0.27",
"react-native-safe-area-context": "4.12.0", "react-native-safe-area-context": "4.12.0",
"react-native-screens": "4.1.0", "react-native-screens": "4.1.0",
"react-native-sortables": "1.1.1",
"react-native-svg": "15.10.1", "react-native-svg": "15.10.1",
"react-native-tab-view": "3.5.2", "react-native-tab-view": "3.5.2",
"react-native-url-polyfill": "1.3.0", "react-native-url-polyfill": "1.3.0",
...@@ -153,6 +153,7 @@ ...@@ -153,6 +153,7 @@
"redux-mock-store": "1.5.4", "redux-mock-store": "1.5.4",
"redux-persist": "6.0.0", "redux-persist": "6.0.0",
"redux-saga": "1.2.2", "redux-saga": "1.2.2",
"rn-qr-generator": "1.4.3",
"typed-redux-saga": "1.5.0", "typed-redux-saga": "1.5.0",
"uniswap": "workspace:^", "uniswap": "workspace:^",
"utilities": "workspace:^", "utilities": "workspace:^",
...@@ -168,9 +169,9 @@ ...@@ -168,9 +169,9 @@
"@faker-js/faker": "7.6.0", "@faker-js/faker": "7.6.0",
"@react-native-community/datetimepicker": "8.2.0", "@react-native-community/datetimepicker": "8.2.0",
"@react-native-community/slider": "4.5.5", "@react-native-community/slider": "4.5.5",
"@storybook/addon-ondevice-controls": "8.4.2", "@storybook/addon-ondevice-controls": "8.5.2",
"@storybook/react": "8.4.2", "@storybook/react": "8.5.2",
"@storybook/react-native": "8.4.2", "@storybook/react-native": "8.5.2",
"@tamagui/babel-plugin": "1.121.7", "@tamagui/babel-plugin": "1.121.7",
"@testing-library/react-native": "13.0.0", "@testing-library/react-native": "13.0.0",
"@types/jest": "29.5.14", "@types/jest": "29.5.14",
......
#!/bin/bash #!/bin/bash
MAX_SIZE=20.75 MAX_SIZE=23
# Check OS type and use appropriate stat command # Check OS type and use appropriate stat command
if [[ "$OSTYPE" == "darwin"* ]]; then if [[ "$OSTYPE" == "darwin"* ]]; then
# MacOS # MacOS
BUNDLE_SIZE=$(stat -f %z ios/main.jsbundle | awk '{print $1/1024/1024}') BUNDLE_SIZE=$(stat -f %z ios/main.jsbundle | awk '{print $1/1024/1024}')
else else
# Linux and others # Linux and others
BUNDLE_SIZE=$(stat --format=%s ios/main.jsbundle | awk '{print $1/1024/1024}') BUNDLE_SIZE=$(stat --format=%s ios/main.jsbundle | awk '{print $1/1024/1024}')
fi fi
if (( $(echo "$BUNDLE_SIZE > $MAX_SIZE" | bc -l) )); then if (($(echo "$BUNDLE_SIZE > $MAX_SIZE" | bc -l))); then
echo "Bundle size ($BUNDLE_SIZE MB) exceeds limit ($MAX_SIZE MB)" echo "Bundle size ($BUNDLE_SIZE MB) exceeds limit ($MAX_SIZE MB)"
exit 1 exit 1
else else
......
...@@ -28,6 +28,7 @@ urls=( ...@@ -28,6 +28,7 @@ urls=(
"https://uniswap.org/app/wc?uri=wc:af098@2?relay-protocol=irn&symKey=51e" "https://uniswap.org/app/wc?uri=wc:af098@2?relay-protocol=irn&symKey=51e"
"uniswap://app/fiatonramp?userAddress=$user_id&source=push" "uniswap://app/fiatonramp?userAddress=$user_id&source=push"
"uniswap://app/tokendetails?currencyId=10-0x6fd9d7ad17242c41f7131d257212c54a0e816691&source=push" "uniswap://app/tokendetails?currencyId=10-0x6fd9d7ad17242c41f7131d257212c54a0e816691&source=push"
"uniswap://app/tokendetails?currencyId=0-fwefe&source=push" # invalid currencyId
) )
xcrun simctl terminate booted "$bundle_id" xcrun simctl terminate booted "$bundle_id"
......
...@@ -72,9 +72,9 @@ import { CurrencyId } from 'uniswap/src/types/currency' ...@@ -72,9 +72,9 @@ import { CurrencyId } from 'uniswap/src/types/currency'
import { getUniqueId } from 'utilities/src/device/getUniqueId' import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { datadogEnabled, isE2EMode } from 'utilities/src/environment/constants' import { datadogEnabled, isE2EMode } from 'utilities/src/environment/constants'
import { isTestEnv } from 'utilities/src/environment/env' import { isTestEnv } from 'utilities/src/environment/env'
import { attachUnhandledRejectionHandler, setAttributesToDatadog } from 'utilities/src/logger/Datadog'
import { registerConsoleOverrides } from 'utilities/src/logger/console' import { registerConsoleOverrides } from 'utilities/src/logger/console'
import { DDRumAction, DDRumTiming } from 'utilities/src/logger/datadogEvents' import { attachUnhandledRejectionHandler, setAttributesToDatadog } from 'utilities/src/logger/datadog/Datadog'
import { DDRumAction, DDRumTiming } from 'utilities/src/logger/datadog/datadogEvents'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { isIOS } from 'utilities/src/platform' import { isIOS } from 'utilities/src/platform'
import { useAsyncData } from 'utilities/src/react/hooks' import { useAsyncData } from 'utilities/src/react/hooks'
...@@ -82,10 +82,9 @@ import { AnalyticsNavigationContextProvider } from 'utilities/src/telemetry/trac ...@@ -82,10 +82,9 @@ import { AnalyticsNavigationContextProvider } from 'utilities/src/telemetry/trac
import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary' import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary'
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { usePersistedApolloClient } from 'wallet/src/data/apollo/usePersistedApolloClient' import { usePersistedApolloClient } from 'wallet/src/data/apollo/usePersistedApolloClient'
import { initFirebaseAppCheck } from 'wallet/src/features/appCheck/appCheck'
import { useCurrentAppearanceSetting } from 'wallet/src/features/appearance/hooks' import { useCurrentAppearanceSetting } from 'wallet/src/features/appearance/hooks'
import { selectAllowAnalytics } from 'wallet/src/features/telemetry/selectors' import { selectAllowAnalytics } from 'wallet/src/features/telemetry/selectors'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks' import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics'
import { TransactionHistoryUpdater } from 'wallet/src/features/transactions/TransactionHistoryUpdater' import { TransactionHistoryUpdater } from 'wallet/src/features/transactions/TransactionHistoryUpdater'
import { WalletUniswapProvider } from 'wallet/src/features/transactions/contexts/WalletUniswapContext' import { WalletUniswapProvider } from 'wallet/src/features/transactions/contexts/WalletUniswapContext'
import { Account } from 'wallet/src/features/wallet/accounts/types' import { Account } from 'wallet/src/features/wallet/accounts/types'
...@@ -114,7 +113,6 @@ if (isE2EMode) { ...@@ -114,7 +113,6 @@ if (isE2EMode) {
initOneSignal() initOneSignal()
initAppsFlyer() initAppsFlyer()
initFirebaseAppCheck()
function App(): JSX.Element | null { function App(): JSX.Element | null {
useEffect(() => { useEffect(() => {
......
...@@ -246,8 +246,8 @@ function useNavigateToFiatOnRamp(): (args: NavigateToFiatOnRampArgs) => void { ...@@ -246,8 +246,8 @@ function useNavigateToFiatOnRamp(): (args: NavigateToFiatOnRampArgs) => void {
const dispatch = useDispatch() const dispatch = useDispatch()
return useCallback( return useCallback(
({ prefilledCurrency }: NavigateToFiatOnRampArgs): void => { ({ prefilledCurrency, isOfframp }: NavigateToFiatOnRampArgs): void => {
dispatch(openModal({ name: ModalName.FiatOnRampAggregator, initialState: { prefilledCurrency } })) dispatch(openModal({ name: ModalName.FiatOnRampAggregator, initialState: { prefilledCurrency, isOfframp } }))
}, },
[dispatch], [dispatch],
) )
......
...@@ -3,11 +3,11 @@ import React, { useCallback, useMemo } from 'react' ...@@ -3,11 +3,11 @@ import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { SettingsStackNavigationProp } from 'src/app/navigation/types' import { SettingsStackNavigationProp } from 'src/app/navigation/types'
import { NotificationsBackgroundImage } from 'src/components/notifications/NotificationsBGImage' import { NotificationsBackgroundImage } from 'src/components/notifications/NotificationsBGImage'
import { promptPushPermission } from 'src/features/notifications/Onesignal'
import { import {
NotificationPermission, NotificationPermission,
useNotificationOSPermissionsEnabled, useNotificationOSPermissionsEnabled,
} from 'src/features/notifications/hooks/useNotificationOSPermissionsEnabled' } from 'src/features/notifications/hooks/useNotificationOSPermissionsEnabled'
import { usePromptPushPermission } from 'src/features/notifications/hooks/usePromptPushPermission'
import { DeprecatedButton, Flex } from 'ui/src' import { DeprecatedButton, Flex } from 'ui/src'
import { BellOn } from 'ui/src/components/icons/BellOn' import { BellOn } from 'ui/src/components/icons/BellOn'
import { GenericHeader } from 'uniswap/src/components/misc/GenericHeader' import { GenericHeader } from 'uniswap/src/components/misc/GenericHeader'
...@@ -27,7 +27,7 @@ type NotificationsOSSettingsModalProps = { ...@@ -27,7 +27,7 @@ type NotificationsOSSettingsModalProps = {
*/ */
export function NotificationsOSSettingsModal({ navigation }: NotificationsOSSettingsModalProps): JSX.Element { export function NotificationsOSSettingsModal({ navigation }: NotificationsOSSettingsModalProps): JSX.Element {
const { notificationPermissionsEnabled, checkNotificationPermissions } = useNotificationOSPermissionsEnabled() const { notificationPermissionsEnabled, checkNotificationPermissions } = useNotificationOSPermissionsEnabled()
const promptPushPermission = usePromptPushPermission()
const { t } = useTranslation() const { t } = useTranslation()
const shouldNavigateToSettings = useMemo(() => { const shouldNavigateToSettings = useMemo(() => {
...@@ -55,7 +55,7 @@ export function NotificationsOSSettingsModal({ navigation }: NotificationsOSSett ...@@ -55,7 +55,7 @@ export function NotificationsOSSettingsModal({ navigation }: NotificationsOSSett
} else { } else {
await checkNotificationPermissions() await checkNotificationPermissions()
} }
}, [checkNotificationPermissions]) }, [checkNotificationPermissions, promptPushPermission])
const onClose = useCallback(() => { const onClose = useCallback(() => {
if (shouldNavigateToSettings) { if (shouldNavigateToSettings) {
......
...@@ -2,7 +2,6 @@ import { useCallback } from 'react' ...@@ -2,7 +2,6 @@ import { useCallback } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { closeModal } from 'src/features/modals/modalSlice' import { closeModal } from 'src/features/modals/modalSlice'
import { selectModalState } from 'src/features/modals/selectModalState' import { selectModalState } from 'src/features/modals/selectModalState'
import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { TokenList } from 'uniswap/src/features/dataApi/types' import { TokenList } from 'uniswap/src/features/dataApi/types'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
...@@ -43,11 +42,11 @@ export function TokenWarningModalWrapper(): JSX.Element | null { ...@@ -43,11 +42,11 @@ export function TokenWarningModalWrapper(): JSX.Element | null {
return null return null
} }
const safetyLevel = currencyInfo.safetyLevel const tokenList = currencyInfo.safetyInfo?.tokenList
const isBlocked = safetyLevel === SafetyLevel.Blocked || currencyInfo.safetyInfo?.tokenList === TokenList.Blocked const isBlocked = tokenList === TokenList.Blocked
// If token is verified or warning was dismissed and not blocked, skip warning and proceed to SwapFlow // If token is verified or warning was dismissed and not blocked, skip warning and proceed to SwapFlow
if (!isBlocked && (safetyLevel === SafetyLevel.Verified || tokenWarningDismissed)) { if (!isBlocked && (tokenList === TokenList.Default || tokenWarningDismissed)) {
onAcknowledge?.() onAcknowledge?.()
onClose() onClose()
return null return null
......
...@@ -6,7 +6,7 @@ import { MobileState, mobilePersistedStateList, mobileReducer } from 'src/app/mo ...@@ -6,7 +6,7 @@ import { MobileState, mobilePersistedStateList, mobileReducer } from 'src/app/mo
import { rootMobileSaga } from 'src/app/saga' import { rootMobileSaga } from 'src/app/saga'
import { fiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api' import { fiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api'
import { isNonJestDev } from 'utilities/src/environment/constants' import { isNonJestDev } from 'utilities/src/environment/constants'
import { createDatadogReduxEnhancer } from 'utilities/src/logger/Datadog' import { createDatadogReduxEnhancer } from 'utilities/src/logger/datadog/Datadog'
import { createStore } from 'wallet/src/state' import { createStore } from 'wallet/src/state'
import { createMigrate } from 'wallet/src/state/createMigrate' import { createMigrate } from 'wallet/src/state/createMigrate'
......
import { BarcodeScanningResult, CameraView, CameraViewProps, scanFromURLAsync, useCameraPermissions } from 'expo-camera' import { BarcodeScanningResult, CameraView, CameraViewProps } from 'expo-camera'
import { PermissionStatus } from 'expo-modules-core' import { PermissionStatus } from 'expo-modules-core'
import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
...@@ -7,6 +7,8 @@ import DeviceInfo from 'react-native-device-info' ...@@ -7,6 +7,8 @@ import DeviceInfo from 'react-native-device-info'
import { launchImageLibrary } from 'react-native-image-picker' import { launchImageLibrary } from 'react-native-image-picker'
import { FadeIn, FadeOut } from 'react-native-reanimated' import { FadeIn, FadeOut } from 'react-native-reanimated'
import { Defs, LinearGradient, Path, Rect, Stop, Svg } from 'react-native-svg' import { Defs, LinearGradient, Path, Rect, Stop, Svg } from 'react-native-svg'
import RNQRGenerator from 'rn-qr-generator'
import { useCameraPermission } from 'src/components/QRCodeScanner/hooks/useCameraPermission'
import { DeprecatedButton, Flex, SpinningLoader, Text, ThemeName, useSporeColors } from 'ui/src' import { DeprecatedButton, Flex, SpinningLoader, Text, ThemeName, useSporeColors } from 'ui/src'
import CameraScan from 'ui/src/assets/icons/camera-scan.svg' import CameraScan from 'ui/src/assets/icons/camera-scan.svg'
import { Global, PhotoStacked } from 'ui/src/components/icons' import { Global, PhotoStacked } from 'ui/src/components/icons'
...@@ -55,8 +57,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E ...@@ -55,8 +57,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
const colors = useSporeColorsForTheme(theme) const colors = useSporeColorsForTheme(theme)
const dimensions = useDeviceDimensions() const dimensions = useDeviceDimensions()
const [permission, requestPermission] = useCameraPermission()
const [permission, requestPermission] = useCameraPermissions()
const [isReadingImageFile, setIsReadingImageFile] = useState(false) const [isReadingImageFile, setIsReadingImageFile] = useState(false)
const [overlayLayout, setOverlayLayout] = useState<LayoutRectangle | null>() const [overlayLayout, setOverlayLayout] = useState<LayoutRectangle | null>()
...@@ -94,16 +95,25 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E ...@@ -94,16 +95,25 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
return return
} }
const result = (await scanFromURLAsync(uri, [BarcodeType.QR]))[0] // TODO (WALL-6014): Migrate to expo-camera once Android issue is fixed
try {
const results = await RNQRGenerator.detect({ uri })
if (!result) { if (results.values[0]) {
const data = results.values[0]
onScanCode(data)
} else {
Alert.alert(t('qrScanner.error.none'))
}
} catch (error) {
logger.error(`Cannot detect QR code in image: ${error}`, {
tags: { file: 'QRCodeScanner.tsx', function: 'onPickImageFilePress' },
})
Alert.alert(t('qrScanner.error.none')) Alert.alert(t('qrScanner.error.none'))
} finally {
setIsReadingImageFile(false) setIsReadingImageFile(false)
return
} }
}, [isReadingImageFile, onScanCode, t])
handleBarcodeScanned(result)
}, [handleBarcodeScanned, isReadingImageFile, t])
useEffect(() => { useEffect(() => {
const handlePermissionStatus = async (): Promise<void> => { const handlePermissionStatus = async (): Promise<void> => {
......
import { useCameraPermissions } from 'expo-camera'
import { usePreventLock } from 'src/features/lockScreen/hooks/usePreventLock'
import { useEvent } from 'utilities/src/react/hooks'
type UseCameraPermissionsResult = ReturnType<typeof useCameraPermissions>
/**
* Custom hook to handle camera permissions with Android-specific considerations.
*
* On Android, requesting permissions causes the app to briefly enter a background state.
* We use preventLock to ensure this temporary background state doesn't trigger any
* app lock mechanisms during the permission request flow.
*/
export const useCameraPermission = (): UseCameraPermissionsResult => {
const [permission, _requestPermission, ...rest] = useCameraPermissions()
const { preventLock } = usePreventLock()
const requestPermission = useEvent(async () => {
return preventLock(_requestPermission)
})
return [permission, requestPermission, ...rest] as const
}
...@@ -127,6 +127,7 @@ export function WalletConnectModal({ ...@@ -127,6 +127,7 @@ export function WalletConnectModal({
} catch (error) { } catch (error) {
logger.error(error, { logger.error(error, {
tags: { file: 'WalletConnectModal', function: 'onScanCode' }, tags: { file: 'WalletConnectModal', function: 'onScanCode' },
extra: { wcUri: supportedURI.value },
}) })
const title = t('walletConnect.error.general.title') const title = t('walletConnect.error.general.title')
......
...@@ -86,13 +86,7 @@ type ProcessedRow = ...@@ -86,13 +86,7 @@ type ProcessedRow =
| { type: 'footer'; data: SectionInfo } | { type: 'footer'; data: SectionInfo }
function processSections(sections: SettingsSection[]): ProcessedRow[] { function processSections(sections: SettingsSection[]): ProcessedRow[] {
const resultSize = sections.reduce((acc, section) => { const result: ProcessedRow[] = []
const dataLength = section.data.length
return acc + (section.subTitle ? 1 : 0) + dataLength
}, 0)
const result: ProcessedRow[] = new Array(resultSize)
let index = 0
for (const section of sections) { for (const section of sections) {
if (section.isHidden) { if (section.isHidden) {
...@@ -100,12 +94,12 @@ function processSections(sections: SettingsSection[]): ProcessedRow[] { ...@@ -100,12 +94,12 @@ function processSections(sections: SettingsSection[]): ProcessedRow[] {
} }
if (section.subTitle) { if (section.subTitle) {
result[index++] = { result.push({
type: 'header', type: 'header',
data: { data: {
section, section,
}, },
} })
} }
for (const data of section.data) { for (const data of section.data) {
...@@ -113,19 +107,19 @@ function processSections(sections: SettingsSection[]): ProcessedRow[] { ...@@ -113,19 +107,19 @@ function processSections(sections: SettingsSection[]): ProcessedRow[] {
continue continue
} }
result[index++] = { result.push({
type: 'item', type: 'item',
data, data,
} })
} }
if (section.subTitle) { if (section.subTitle) {
result[index++] = { result.push({
type: 'footer', type: 'footer',
data: { data: {
section, section,
}, },
} })
} }
} }
......
...@@ -2,8 +2,7 @@ import React, { PropsWithChildren, memo, useMemo } from 'react' ...@@ -2,8 +2,7 @@ import React, { PropsWithChildren, memo, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ContextMenu from 'react-native-context-menu-view' import ContextMenu from 'react-native-context-menu-view'
import { borderRadii } from 'ui/src/theme' import { borderRadii } from 'ui/src/theme'
import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { PortfolioBalance, TokenList } from 'uniswap/src/features/dataApi/types'
import { PortfolioBalance } from 'uniswap/src/features/dataApi/types'
import { useTokenContextMenu } from 'wallet/src/features/portfolio/useTokenContextMenu' import { useTokenContextMenu } from 'wallet/src/features/portfolio/useTokenContextMenu'
export const TokenBalanceItemContextMenu = memo(function _TokenBalanceItemContextMenu({ export const TokenBalanceItemContextMenu = memo(function _TokenBalanceItemContextMenu({
...@@ -16,7 +15,7 @@ export const TokenBalanceItemContextMenu = memo(function _TokenBalanceItemContex ...@@ -16,7 +15,7 @@ export const TokenBalanceItemContextMenu = memo(function _TokenBalanceItemContex
const { menuActions, onContextMenuPress } = useTokenContextMenu({ const { menuActions, onContextMenuPress } = useTokenContextMenu({
currencyId: portfolioBalance.currencyInfo.currencyId, currencyId: portfolioBalance.currencyInfo.currencyId,
isBlocked: portfolioBalance.currencyInfo.safetyLevel === SafetyLevel.Blocked, isBlocked: portfolioBalance.currencyInfo.safetyInfo?.tokenList === TokenList.Blocked,
portfolioBalance, portfolioBalance,
tokenSymbolForNotification: t('walletConnect.request.details.label.token'), tokenSymbolForNotification: t('walletConnect.request.details.label.token'),
}) })
......
...@@ -22,7 +22,7 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' ...@@ -22,7 +22,7 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' import { useAppInsets } from 'uniswap/src/hooks/useAppInsets'
import { CurrencyId } from 'uniswap/src/types/currency' import { CurrencyId } from 'uniswap/src/types/currency'
import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { DDRumManualTiming } from 'utilities/src/logger/datadogEvents' import { DDRumManualTiming } from 'utilities/src/logger/datadog/datadogEvents'
import { usePerformanceLogger } from 'utilities/src/logger/usePerformanceLogger' import { usePerformanceLogger } from 'utilities/src/logger/usePerformanceLogger'
import { isAndroid } from 'utilities/src/platform' import { isAndroid } from 'utilities/src/platform'
import { useValueAsRef } from 'utilities/src/react/useValueAsRef' import { useValueAsRef } from 'utilities/src/react/useValueAsRef'
......
...@@ -4,8 +4,6 @@ import { useTokenDetailsContext } from 'src/components/TokenDetails/TokenDetails ...@@ -4,8 +4,6 @@ import { useTokenDetailsContext } from 'src/components/TokenDetails/TokenDetails
import { DeprecatedButton, Flex, GeneratedIcon, useSporeColors } from 'ui/src' import { DeprecatedButton, Flex, GeneratedIcon, useSporeColors } from 'ui/src'
import { SwapCoin } from 'ui/src/components/icons' import { SwapCoin } from 'ui/src/components/icons'
import { opacify, validColor } from 'ui/src/theme' import { opacify, validColor } from 'ui/src/theme'
import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { useTokenBasicProjectPartsFragment } from 'uniswap/src/data/graphql/uniswap-data-api/fragments'
import { TokenList } from 'uniswap/src/features/dataApi/types' import { TokenList } from 'uniswap/src/features/dataApi/types'
import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
...@@ -69,12 +67,9 @@ export function TokenDetailsActionButtons({ ...@@ -69,12 +67,9 @@ export function TokenDetailsActionButtons({
const { t } = useTranslation() const { t } = useTranslation()
const isOffRampEnabled = useFeatureFlag(FeatureFlags.FiatOffRamp) const isOffRampEnabled = useFeatureFlag(FeatureFlags.FiatOffRamp)
const { currencyId, currencyInfo, isChainEnabled, tokenColor } = useTokenDetailsContext() const { currencyInfo, isChainEnabled, tokenColor } = useTokenDetailsContext()
const project = useTokenBasicProjectPartsFragment({ currencyId }).data?.project const isBlocked = currencyInfo?.safetyInfo?.tokenList === TokenList.Blocked
const safetyLevel = project?.safetyLevel
const isBlocked = safetyLevel === SafetyLevel.Blocked || currencyInfo?.safetyInfo?.tokenList === TokenList.Blocked
const disabled = isBlocked || !isChainEnabled const disabled = isBlocked || !isChainEnabled
......
import React from 'react' import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ScrollView, View } from 'react-native' import { View } from 'react-native'
import { FlatList } from 'react-native-gesture-handler'
import { LinkButton, LinkButtonType } from 'src/components/TokenDetails/LinkButton' import { LinkButton, LinkButtonType } from 'src/components/TokenDetails/LinkButton'
import { useTokenDetailsContext } from 'src/components/TokenDetails/TokenDetailsContext' import { useTokenDetailsContext } from 'src/components/TokenDetails/TokenDetailsContext'
import { getBlockExplorerIcon } from 'src/components/icons/BlockExplorerIcon' import { getBlockExplorerIcon } from 'src/components/icons/BlockExplorerIcon'
...@@ -11,8 +12,8 @@ import { useTokenProjectUrlsPartsFragment } from 'uniswap/src/data/graphql/unisw ...@@ -11,8 +12,8 @@ import { useTokenProjectUrlsPartsFragment } from 'uniswap/src/data/graphql/unisw
import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo'
import { ElementName } from 'uniswap/src/features/telemetry/constants' import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { isDefaultNativeAddress } from 'uniswap/src/utils/currencyId'
import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking'
import { isDefaultNativeAddress } from 'wallet/src/utils/currencyId'
import { getTwitterLink } from 'wallet/src/utils/linking' import { getTwitterLink } from 'wallet/src/utils/linking'
export function TokenDetailsLinks(): JSX.Element { export function TokenDetailsLinks(): JSX.Element {
...@@ -25,57 +26,63 @@ export function TokenDetailsLinks(): JSX.Element { ...@@ -25,57 +26,63 @@ export function TokenDetailsLinks(): JSX.Element {
const explorerLink = getExplorerLink(chainId, address, ExplorerDataType.TOKEN) const explorerLink = getExplorerLink(chainId, address, ExplorerDataType.TOKEN)
const explorerName = getChainInfo(chainId).explorer.name const explorerName = getChainInfo(chainId).explorer.name
const links = useMemo(() => {
return [
{
Icon: getBlockExplorerIcon(chainId),
buttonType: LinkButtonType.Link,
element: ElementName.TokenLinkEtherscan,
label: explorerName,
testID: TestID.TokenLinkEtherscan,
value: explorerLink,
},
homepageUrl
? {
Icon: GlobeIcon,
buttonType: LinkButtonType.Link,
element: ElementName.TokenLinkWebsite,
label: t('token.links.website'),
testID: TestID.TokenLinkWebsite,
value: homepageUrl,
}
: null,
twitterName
? {
Icon: TwitterIcon,
buttonType: LinkButtonType.Link,
element: ElementName.TokenLinkTwitter,
label: t('token.links.twitter'),
testID: TestID.TokenLinkTwitter,
value: getTwitterLink(twitterName),
}
: null,
!isDefaultNativeAddress(address)
? {
buttonType: LinkButtonType.Copy,
element: ElementName.Copy,
label: t('common.text.contract'),
testID: TestID.TokenLinkCopy,
value: address,
}
: null,
].filter((item): item is NonNullable<typeof item> => Boolean(item))
}, [chainId, address, homepageUrl, twitterName, explorerName, explorerLink, t])
return ( return (
<View style={{ marginHorizontal: -14 }}> <View style={{ marginHorizontal: -14 }}>
<Flex gap="$spacing8"> <Flex gap="$spacing8">
<Text color="$neutral2" mx="$spacing16" variant="subheading2"> <Text color="$neutral2" mx="$spacing16" variant="subheading2">
{t('token.links.title')} {t('token.links.title')}
</Text> </Text>
<Flex row gap="$spacing8" px="$spacing16">
<ScrollView horizontal showsHorizontalScrollIndicator={false}> <FlatList
<Flex row gap="$spacing8" px="$spacing16"> horizontal
<LinkButton showsHorizontalScrollIndicator={false}
Icon={getBlockExplorerIcon(chainId)} data={links}
buttonType={LinkButtonType.Link} renderItem={({ item }) => <LinkButton {...item} />}
element={ElementName.TokenLinkEtherscan} keyExtractor={(item) => item.testID}
label={explorerName} />
testID={TestID.TokenLinkEtherscan} </Flex>
value={explorerLink}
/>
{homepageUrl && (
<LinkButton
Icon={GlobeIcon}
buttonType={LinkButtonType.Link}
element={ElementName.TokenLinkWebsite}
label={t('token.links.website')}
testID={TestID.TokenLinkWebsite}
value={homepageUrl}
/>
)}
{twitterName && (
<LinkButton
Icon={TwitterIcon}
buttonType={LinkButtonType.Link}
element={ElementName.TokenLinkTwitter}
label={t('token.links.twitter')}
testID={TestID.TokenLinkTwitter}
value={getTwitterLink(twitterName)}
/>
)}
{!isDefaultNativeAddress(address) && (
<LinkButton
buttonType={LinkButtonType.Copy}
element={ElementName.Copy}
label={t('common.text.contract')}
testID={TestID.TokenLinkCopy}
value={address}
/>
)}
</Flex>
</ScrollView>
</Flex> </Flex>
</View> </View>
) )
......
import type { Meta, StoryObj } from '@storybook/react'
import { CopyTextButton } from 'src/components/buttons/CopyTextButton'
const meta = {
title: 'Components/Buttons',
component: CopyTextButton,
} satisfies Meta<typeof CopyTextButton>
type Story = StoryObj<typeof meta>
const CopyTextButtonStory: Story = {
storyName: 'CopyTextButton',
args: {
copyText: 'You copied me!',
},
}
export default meta
export { CopyTextButtonStory }
...@@ -3,18 +3,18 @@ import { ...@@ -3,18 +3,18 @@ import {
TokenRankingsStat, TokenRankingsStat,
TokenStats, TokenStats,
} from '@uniswap/client-explore/dist/uniswap/explore/v1/service_pb' } from '@uniswap/client-explore/dist/uniswap/explore/v1/service_pb'
import React, { useCallback, useMemo, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ListRenderItem, ListRenderItemInfo, StyleSheet, useWindowDimensions } from 'react-native' import { ListRenderItem, ListRenderItemInfo, StyleSheet, useWindowDimensions } from 'react-native'
import { FlatList } from 'react-native-gesture-handler' import { FlatList } from 'react-native-gesture-handler'
import { SharedValue, useSharedValue } from 'react-native-reanimated' import { AnimatedRef } from 'react-native-reanimated'
import { useSelector } from 'react-redux' import Sortable from 'react-native-sortables'
import { useDispatch, useSelector } from 'react-redux'
import { FavoriteTokensGrid } from 'src/components/explore/FavoriteTokensGrid' import { FavoriteTokensGrid } from 'src/components/explore/FavoriteTokensGrid'
import { FavoriteWalletsGrid } from 'src/components/explore/FavoriteWalletsGrid' import { FavoriteWalletsGrid } from 'src/components/explore/FavoriteWalletsGrid'
import { SortButton } from 'src/components/explore/SortButton' import { SortButton } from 'src/components/explore/SortButton'
import { TokenItem } from 'src/components/explore/TokenItem' import { TokenItem } from 'src/components/explore/TokenItem'
import { TokenItemData } from 'src/components/explore/TokenItemData' import { TokenItemData } from 'src/components/explore/TokenItemData'
import { AutoScrollProps } from 'src/components/sortableGrid/types'
import { getTokenMetadataDisplayType } from 'src/features/explore/utils' import { getTokenMetadataDisplayType } from 'src/features/explore/utils'
import { Flex, Loader, Text, TouchableArea, useSporeColors } from 'ui/src' import { Flex, Loader, Text, TouchableArea, useSporeColors } from 'ui/src'
import { AnimatedBottomSheetFlashList } from 'ui/src/components/AnimatedFlashList/AnimatedFlashList' import { AnimatedBottomSheetFlashList } from 'ui/src/components/AnimatedFlashList/AnimatedFlashList'
...@@ -32,16 +32,17 @@ import { MobileEventName } from 'uniswap/src/features/telemetry/constants' ...@@ -32,16 +32,17 @@ import { MobileEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' import { useAppInsets } from 'uniswap/src/hooks/useAppInsets'
import { buildCurrencyId, buildNativeCurrencyId } from 'uniswap/src/utils/currencyId' import { buildCurrencyId, buildNativeCurrencyId } from 'uniswap/src/utils/currencyId'
import { DDRumManualTiming } from 'utilities/src/logger/datadogEvents' import { DDRumManualTiming } from 'utilities/src/logger/datadog/datadogEvents'
import { usePerformanceLogger } from 'utilities/src/logger/usePerformanceLogger' import { usePerformanceLogger } from 'utilities/src/logger/usePerformanceLogger'
import { selectTokensOrderBy } from 'wallet/src/features/wallet/selectors' import { selectTokensOrderBy } from 'wallet/src/features/wallet/selectors'
import { setTokensOrderBy } from 'wallet/src/features/wallet/slice'
import { ExploreOrderBy, TokenMetadataDisplayType } from 'wallet/src/features/wallet/types' import { ExploreOrderBy, TokenMetadataDisplayType } from 'wallet/src/features/wallet/types'
const TOKEN_ITEM_SIZE = 68 const TOKEN_ITEM_SIZE = 68
const AMOUNT_TO_DRAW = 18 const AMOUNT_TO_DRAW = 18
type ExploreSectionsProps = { type ExploreSectionsProps = {
listRef: React.MutableRefObject<null> listRef: AnimatedRef<FlatList>
} }
type TokenItemDataWithMetadata = { tokenItemData: TokenItemData; tokenMetadataDisplayType: TokenMetadataDisplayType } type TokenItemDataWithMetadata = { tokenItemData: TokenItemData; tokenMetadataDisplayType: TokenMetadataDisplayType }
...@@ -49,11 +50,9 @@ type TokenItemDataWithMetadata = { tokenItemData: TokenItemData; tokenMetadataDi ...@@ -49,11 +50,9 @@ type TokenItemDataWithMetadata = { tokenItemData: TokenItemData; tokenMetadataDi
export function ExploreSections({ listRef }: ExploreSectionsProps): JSX.Element { export function ExploreSections({ listRef }: ExploreSectionsProps): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const insets = useAppInsets() const insets = useAppInsets()
const scrollY = useSharedValue(0)
const visibleListHeight = useSharedValue(0)
const dimensions = useWindowDimensions() const dimensions = useWindowDimensions()
// Top tokens sorting // Top tokens sorting
const orderBy = useSelector(selectTokensOrderBy) const { uiOrderBy, orderBy, onOrderByChange } = useOrderBy()
// Network filtering // Network filtering
const [selectedNetwork, setSelectedNetwork] = useState<UniverseChainId | null>(null) const [selectedNetwork, setSelectedNetwork] = useState<UniverseChainId | null>(null)
...@@ -108,30 +107,18 @@ export function ExploreSections({ listRef }: ExploreSectionsProps): JSX.Element ...@@ -108,30 +107,18 @@ export function ExploreSections({ listRef }: ExploreSectionsProps): JSX.Element
} }
return ( return (
// Pass onLayout callback to the list wrapper component as it returned <Flex fill animation="100ms">
// incorrect values when it was passed to the list itself
<Flex
fill
animation="100ms"
onLayout={({
nativeEvent: {
layout: { height },
},
}): void => {
visibleListHeight.value = height
}}
>
<AnimatedBottomSheetFlashList <AnimatedBottomSheetFlashList
ref={listRef} ref={listRef}
ListEmptyComponent={ListEmptyComponent} ListEmptyComponent={ListEmptyComponent}
ListHeaderComponent={ ListHeaderComponent={
<ListHeaderComponent <ListHeaderComponent
listRef={listRef} listRef={listRef}
orderBy={orderBy} orderBy={uiOrderBy}
scrollY={scrollY} showLoading={isLoadingOrFetching}
selectedNetwork={selectedNetwork} selectedNetwork={selectedNetwork}
visibleListHeight={visibleListHeight}
onSelectNetwork={onSelectNetwork} onSelectNetwork={onSelectNetwork}
onOrderByChange={onOrderByChange}
/> />
} }
ListHeaderComponentStyle={styles.foreground} ListHeaderComponentStyle={styles.foreground}
...@@ -260,8 +247,9 @@ function tokenRankingStatsToTokenItemData(tokenRankingStat: TokenRankingsStat): ...@@ -260,8 +247,9 @@ function tokenRankingStatsToTokenItemData(tokenRankingStat: TokenRankingsStat):
} }
} }
type FavoritesSectionProps = AutoScrollProps & { type FavoritesSectionProps = {
showLoading: boolean showLoading: boolean
listRef: AnimatedRef<FlatList>
} }
function FavoritesSection(props: FavoritesSectionProps): JSX.Element | null { function FavoritesSection(props: FavoritesSectionProps): JSX.Element | null {
...@@ -348,37 +336,32 @@ function useTokenItems( ...@@ -348,37 +336,32 @@ function useTokenItems(
} }
type ListHeaderProps = { type ListHeaderProps = {
listRef: React.MutableRefObject<null> listRef: AnimatedRef<FlatList>
scrollY: SharedValue<number>
visibleListHeight: SharedValue<number>
orderBy: ExploreOrderBy orderBy: ExploreOrderBy
showLoading: boolean
onOrderByChange: (orderBy: ExploreOrderBy) => void
} }
const ListHeader = React.memo(function ListHeader({ const ListHeader = React.memo(function ListHeader({
listRef, listRef,
scrollY,
visibleListHeight,
orderBy, orderBy,
showLoading,
onOrderByChange,
}: ListHeaderProps): JSX.Element { }: ListHeaderProps): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Flex> <Sortable.Layer>
<FavoritesSection <FavoritesSection showLoading={showLoading} listRef={listRef} />
showLoading={false}
scrollY={scrollY}
scrollableRef={listRef}
visibleHeight={visibleListHeight}
/>
<Flex row alignItems="center" justifyContent="space-between" px="$spacing20"> <Flex row alignItems="center" justifyContent="space-between" px="$spacing20">
<Text color="$neutral2" flexShrink={0} paddingEnd="$spacing8" variant="subheading1"> <Text color="$neutral2" flexShrink={0} paddingEnd="$spacing8" variant="subheading1">
{t('explore.tokens.top.title')} {t('explore.tokens.top.title')}
</Text> </Text>
<Flex flexShrink={1}> <Flex flexShrink={1}>
<SortButton orderBy={orderBy} /> <SortButton orderBy={orderBy} onOrderByChange={onOrderByChange} />
</Flex> </Flex>
</Flex> </Flex>
</Flex> </Sortable.Layer>
) )
}) })
...@@ -402,13 +385,13 @@ const ListHeaderComponent = ({ ...@@ -402,13 +385,13 @@ const ListHeaderComponent = ({
listRef, listRef,
onSelectNetwork, onSelectNetwork,
orderBy, orderBy,
scrollY,
selectedNetwork, selectedNetwork,
visibleListHeight, showLoading,
onOrderByChange,
}: ListHeaderProps & NetworkPillsProps): JSX.Element => { }: ListHeaderProps & NetworkPillsProps): JSX.Element => {
return ( return (
<> <>
<ListHeader listRef={listRef} orderBy={orderBy} scrollY={scrollY} visibleListHeight={visibleListHeight} /> <ListHeader listRef={listRef} orderBy={orderBy} showLoading={showLoading} onOrderByChange={onOrderByChange} />
<NetworkPills selectedNetwork={selectedNetwork} onSelectNetwork={onSelectNetwork} /> <NetworkPills selectedNetwork={selectedNetwork} onSelectNetwork={onSelectNetwork} />
</> </>
) )
...@@ -419,3 +402,32 @@ const ListEmptyComponent = (): JSX.Element => ( ...@@ -419,3 +402,32 @@ const ListEmptyComponent = (): JSX.Element => (
<Loader.Token repeat={5} /> <Loader.Token repeat={5} />
</Flex> </Flex>
) )
function useOrderBy(): {
uiOrderBy: ExploreOrderBy
orderBy: ExploreOrderBy
onOrderByChange: (orderBy: ExploreOrderBy) => void
} {
const dispatch = useDispatch()
const orderBy = useSelector(selectTokensOrderBy)
// local state for immediate UI feedback
const [uiOrderBy, setUiOrderBy] = useState<ExploreOrderBy>(orderBy)
// When Redux orderBy changes, sync UI
useEffect(() => {
setUiOrderBy(orderBy)
}, [orderBy])
const onOrderByChange = useCallback(
(newTokensOrderBy: ExploreOrderBy) => {
setUiOrderBy(newTokensOrderBy)
requestAnimationFrame(() => {
dispatch(setTokensOrderBy({ newTokensOrderBy }))
})
},
[dispatch],
)
return { uiOrderBy, orderBy, onOrderByChange }
}
import { makeMutable } from 'react-native-reanimated'
import configureMockStore from 'redux-mock-store' import configureMockStore from 'redux-mock-store'
import FavoriteTokenCard, { FavoriteTokenCardProps } from 'src/components/explore/FavoriteTokenCard' import FavoriteTokenCard, { FavoriteTokenCardProps } from 'src/components/explore/FavoriteTokenCard'
import { act, cleanup, fireEvent, render, waitFor } from 'src/test/test-utils' import { act, cleanup, fireEvent, render, waitFor } from 'src/test/test-utils'
...@@ -52,8 +51,6 @@ const touchableId = `token-box-${favoriteToken.symbol}` ...@@ -52,8 +51,6 @@ const touchableId = `token-box-${favoriteToken.symbol}`
const defaultProps: FavoriteTokenCardProps = { const defaultProps: FavoriteTokenCardProps = {
currencyId: SAMPLE_CURRENCY_ID_1, currencyId: SAMPLE_CURRENCY_ID_1,
pressProgress: makeMutable(0),
dragActivationProgress: makeMutable(0),
setIsEditing: jest.fn(), setIsEditing: jest.fn(),
isEditing: false, isEditing: false,
} }
......
import React, { memo, useCallback } from 'react' import React, { memo, useCallback } from 'react'
import { ViewProps } from 'react-native' import { ViewProps } from 'react-native'
import ContextMenu from 'react-native-context-menu-view' import ContextMenu from 'react-native-context-menu-view'
import { SharedValue } from 'react-native-reanimated'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks'
import RemoveButton from 'src/components/explore/RemoveButton' import RemoveButton from 'src/components/explore/RemoveButton'
import { useAnimatedCardDragStyle, useExploreTokenContextMenu } from 'src/components/explore/hooks' import { useExploreTokenContextMenu } from 'src/components/explore/hooks'
import { disableOnPress } from 'src/utils/disableOnPress' import { disableOnPress } from 'src/utils/disableOnPress'
import { usePollOnFocusOnly } from 'src/utils/hooks' import { usePollOnFocusOnly } from 'src/utils/hooks'
import { AnimatedTouchableArea, Flex, Loader, Text, useIsDarkMode, useShadowPropsShort, useSporeColors } from 'ui/src' import { AnimatedTouchableArea, Flex, Loader, Text, useIsDarkMode, useShadowPropsShort, useSporeColors } from 'ui/src'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { borderRadii, fonts, imageSizes, opacify } from 'ui/src/theme' import { borderRadii, fonts, imageSizes, opacify } from 'ui/src/theme'
import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo' import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo'
import { RelativeChange } from 'uniswap/src/components/RelativeChange/RelativeChange' import { RelativeChange } from 'uniswap/src/components/RelativeChange/RelativeChange'
...@@ -26,26 +24,18 @@ import { useLocalizationContext } from 'uniswap/src/features/language/Localizati ...@@ -26,26 +24,18 @@ import { useLocalizationContext } from 'uniswap/src/features/language/Localizati
import { SectionName } from 'uniswap/src/features/telemetry/constants' import { SectionName } from 'uniswap/src/features/telemetry/constants'
import { getSymbolDisplayText } from 'uniswap/src/utils/currency' import { getSymbolDisplayText } from 'uniswap/src/utils/currency'
import { NumberType } from 'utilities/src/format/types' import { NumberType } from 'utilities/src/format/types'
import { isIOS } from 'utilities/src/platform'
import { isNonPollingRequestInFlight } from 'wallet/src/data/utils' import { isNonPollingRequestInFlight } from 'wallet/src/data/utils'
export const FAVORITE_TOKEN_CARD_LOADER_HEIGHT = 114 export const FAVORITE_TOKEN_CARD_LOADER_HEIGHT = 114
export type FavoriteTokenCardProps = { export type FavoriteTokenCardProps = {
currencyId: string currencyId: string
pressProgress: SharedValue<number>
dragActivationProgress: SharedValue<number>
isEditing?: boolean isEditing?: boolean
setIsEditing: (update: boolean) => void setIsEditing: (update: boolean) => void
} & ViewProps } & ViewProps
function FavoriteTokenCard({ function FavoriteTokenCard({ currencyId, isEditing, setIsEditing, ...rest }: FavoriteTokenCardProps): JSX.Element {
currencyId,
isEditing,
pressProgress,
dragActivationProgress,
setIsEditing,
...rest
}: FavoriteTokenCardProps): JSX.Element {
const dispatch = useDispatch() const dispatch = useDispatch()
const { defaultChainId } = useEnabledChains() const { defaultChainId } = useEnabledChains()
const tokenDetailsNavigation = useTokenDetailsNavigation() const tokenDetailsNavigation = useTokenDetailsNavigation()
...@@ -98,78 +88,75 @@ function FavoriteTokenCard({ ...@@ -98,78 +88,75 @@ function FavoriteTokenCard({
tokenDetailsNavigation.navigate(currencyId) tokenDetailsNavigation.navigate(currencyId)
} }
const animatedDragStyle = useAnimatedCardDragStyle(pressProgress, dragActivationProgress)
const shadowProps = useShadowPropsShort() const shadowProps = useShadowPropsShort()
const priceLoading = isNonPollingRequestInFlight(networkStatus) const priceLoading = isNonPollingRequestInFlight(networkStatus)
return ( return (
<AnimatedFlex borderRadius="$rounded16" style={animatedDragStyle}> <ContextMenu
<ContextMenu actions={menuActions}
actions={menuActions} disabled={isEditing}
disabled={isEditing} style={{ borderRadius: borderRadii.rounded16 }}
style={{ borderRadius: borderRadii.rounded16 }} onPress={onContextMenuPress}
onPress={onContextMenuPress} {...rest}
{...rest} >
<AnimatedTouchableArea
activeOpacity={isEditing ? 1 : undefined}
backgroundColor={isDarkMode ? '$surface2' : '$surface1'}
borderColor={opacify(0.05, colors.surface3.val)}
borderRadius="$rounded16"
overflow={isIOS ? 'hidden' : 'visible'}
borderWidth={isDarkMode ? '$none' : '$spacing1'}
m="$spacing4"
testID={`token-box-${token?.symbol}`}
onLongPress={disableOnPress}
onPress={onPress}
{...shadowProps}
> >
<AnimatedTouchableArea <Flex alignItems="flex-start" gap="$spacing8" p="$spacing12">
activeOpacity={isEditing ? 1 : undefined} <Flex row gap="$spacing4" justifyContent="space-between">
backgroundColor={isDarkMode ? '$surface2' : '$surface1'} <Flex grow row alignItems="center" gap="$spacing8">
borderColor={opacify(0.05, colors.surface3.val)} <TokenLogo
borderRadius="$rounded16" chainId={chainId ?? undefined}
borderWidth={isDarkMode ? '$none' : '$spacing1'} name={token?.name ?? undefined}
m="$spacing4" size={imageSizes.image20}
testID={`token-box-${token?.symbol}`} symbol={token?.symbol ?? undefined}
onLongPress={disableOnPress} url={token?.project?.logoUrl ?? undefined}
onPress={onPress} />
{...shadowProps} <Text variant="body1">{getSymbolDisplayText(token?.symbol)}</Text>
>
<Flex alignItems="flex-start" gap="$spacing8" p="$spacing12">
<Flex row gap="$spacing4" justifyContent="space-between">
<Flex grow row alignItems="center" gap="$spacing8">
<TokenLogo
chainId={chainId ?? undefined}
name={token?.name ?? undefined}
size={imageSizes.image20}
symbol={token?.symbol ?? undefined}
url={token?.project?.logoUrl ?? undefined}
/>
<Text variant="body1">{getSymbolDisplayText(token?.symbol)}</Text>
</Flex>
<RemoveButton visible={isEditing} onPress={onRemove} />
</Flex>
<Flex gap="$spacing2">
{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>
<RemoveButton visible={isEditing} onPress={onRemove} />
</Flex>
<Flex gap="$spacing2">
{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> </Flex>
</ContextMenu> </AnimatedTouchableArea>
</AnimatedFlex> </ContextMenu>
) )
} }
......
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FadeIn, useAnimatedStyle, useSharedValue } from 'react-native-reanimated' import { FlatList } from 'react-native-gesture-handler'
import { AnimatedRef, FadeIn } from 'react-native-reanimated'
import type { SortableGridDragEndCallback, SortableGridRenderItem } from 'react-native-sortables'
import Sortable from 'react-native-sortables'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { FavoriteHeaderRow } from 'src/components/explore/FavoriteHeaderRow' import { FavoriteHeaderRow } from 'src/components/explore/FavoriteHeaderRow'
import FavoriteTokenCard, { FAVORITE_TOKEN_CARD_LOADER_HEIGHT } from 'src/components/explore/FavoriteTokenCard' import FavoriteTokenCard, { FAVORITE_TOKEN_CARD_LOADER_HEIGHT } from 'src/components/explore/FavoriteTokenCard'
import { Loader } from 'src/components/loading/loaders' import { Loader } from 'src/components/loading/loaders'
import { SortableGrid } from 'src/components/sortableGrid/SortableGrid'
import { AutoScrollProps, SortableGridChangeEvent, SortableGridRenderItem } from 'src/components/sortableGrid/types'
import { Flex } from 'ui/src' import { Flex } from 'ui/src'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { selectFavoriteTokens } from 'uniswap/src/features/favorites/selectors' import { selectFavoriteTokens } from 'uniswap/src/features/favorites/selectors'
...@@ -15,17 +16,17 @@ import { setFavoriteTokens } from 'uniswap/src/features/favorites/slice' ...@@ -15,17 +16,17 @@ import { setFavoriteTokens } from 'uniswap/src/features/favorites/slice'
const NUM_COLUMNS = 2 const NUM_COLUMNS = 2
const ITEM_FLEX = { flex: 1 / NUM_COLUMNS } const ITEM_FLEX = { flex: 1 / NUM_COLUMNS }
type FavoriteTokensGridProps = AutoScrollProps & { type FavoriteTokensGridProps = {
showLoading: boolean showLoading: boolean
listRef: AnimatedRef<FlatList>
} }
/** Renders the favorite tokens section on the Explore tab */ /** Renders the favorite tokens section on the Explore tab */
export function FavoriteTokensGrid({ showLoading, ...rest }: FavoriteTokensGridProps): JSX.Element | null { export function FavoriteTokensGrid({ showLoading, listRef, ...rest }: FavoriteTokensGridProps): JSX.Element | null {
const { t } = useTranslation() const { t } = useTranslation()
const dispatch = useDispatch() const dispatch = useDispatch()
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const isTokenDragged = useSharedValue(false)
const favoriteCurrencyIds = useSelector(selectFavoriteTokens) const favoriteCurrencyIds = useSelector(selectFavoriteTokens)
// Reset edit mode when there are no favorite tokens // Reset edit mode when there are no favorite tokens
...@@ -35,61 +36,47 @@ export function FavoriteTokensGrid({ showLoading, ...rest }: FavoriteTokensGridP ...@@ -35,61 +36,47 @@ export function FavoriteTokensGrid({ showLoading, ...rest }: FavoriteTokensGridP
} }
}, [favoriteCurrencyIds.length]) }, [favoriteCurrencyIds.length])
const handleOrderChange = useCallback( const handleDragEnd = useCallback<SortableGridDragEndCallback<string>>(
({ data }: SortableGridChangeEvent<string>) => { ({ data }) => {
dispatch(setFavoriteTokens({ currencyIds: data })) dispatch(setFavoriteTokens({ currencyIds: data }))
}, },
[dispatch], [dispatch],
) )
const renderItem = useCallback<SortableGridRenderItem<string>>( const renderItem = useCallback<SortableGridRenderItem<string>>(
({ item: currencyId, pressProgress, dragActivationProgress }): JSX.Element => ( ({ item: currencyId }): JSX.Element => (
<FavoriteTokenCard <FavoriteTokenCard currencyId={currencyId} isEditing={isEditing} setIsEditing={setIsEditing} />
key={currencyId}
currencyId={currencyId}
dragActivationProgress={dragActivationProgress}
isEditing={isEditing}
pressProgress={pressProgress}
setIsEditing={setIsEditing}
/>
), ),
[isEditing], [isEditing],
) )
const animatedStyle = useAnimatedStyle(() => ({
zIndex: isTokenDragged.value ? 1 : 0,
}))
return ( return (
<AnimatedFlex entering={FadeIn} style={animatedStyle}> <Sortable.Layer>
<FavoriteHeaderRow <AnimatedFlex entering={FadeIn}>
disabled={showLoading} <FavoriteHeaderRow
editingTitle={t('explore.tokens.favorite.title.edit')} disabled={showLoading}
isEditing={isEditing} editingTitle={t('explore.tokens.favorite.title.edit')}
title={t('explore.tokens.favorite.title.default')} isEditing={isEditing}
onPress={(): void => setIsEditing(!isEditing)} title={t('explore.tokens.favorite.title.default')}
/> onPress={(): void => setIsEditing(!isEditing)}
{showLoading ? (
<FavoriteTokensGridLoader />
) : (
<SortableGrid
{...rest}
activeItemOpacity={1}
animateContainerHeight={false}
data={favoriteCurrencyIds}
editable={isEditing}
numColumns={NUM_COLUMNS}
renderItem={renderItem}
onChange={handleOrderChange}
onDragStart={(): void => {
isTokenDragged.value = true
}}
onDrop={(): void => {
isTokenDragged.value = false
}}
/> />
)} {showLoading ? (
</AnimatedFlex> <FavoriteTokensGridLoader />
) : (
<Sortable.Grid
{...rest}
animateHeight
scrollableRef={listRef}
data={favoriteCurrencyIds}
sortEnabled={isEditing}
autoScrollActivationOffset={[75, 100]}
columns={NUM_COLUMNS}
renderItem={renderItem}
onDragEnd={handleDragEnd}
/>
)}
</AnimatedFlex>
</Sortable.Layer>
) )
} }
......
import { makeMutable } from 'react-native-reanimated'
import configureMockStore from 'redux-mock-store' import configureMockStore from 'redux-mock-store'
import FavoriteWalletCard, { FavoriteWalletCardProps } from 'src/components/explore/FavoriteWalletCard' import FavoriteWalletCard, { FavoriteWalletCardProps } from 'src/components/explore/FavoriteWalletCard'
import { preloadedMobileState } from 'src/test/fixtures' import { preloadedMobileState } from 'src/test/fixtures'
...@@ -28,9 +27,7 @@ const mockStore = configureMockStore() ...@@ -28,9 +27,7 @@ const mockStore = configureMockStore()
const defaultProps: FavoriteWalletCardProps = { const defaultProps: FavoriteWalletCardProps = {
address: SAMPLE_SEED_ADDRESS_1, address: SAMPLE_SEED_ADDRESS_1,
pressProgress: makeMutable(0),
isEditing: false, isEditing: false,
dragActivationProgress: makeMutable(0),
setIsEditing: jest.fn(), setIsEditing: jest.fn(),
} }
......
...@@ -2,17 +2,15 @@ import { memo, useCallback, useMemo } from 'react' ...@@ -2,17 +2,15 @@ import { memo, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ViewProps } from 'react-native' import { ViewProps } from 'react-native'
import ContextMenu from 'react-native-context-menu-view' import ContextMenu from 'react-native-context-menu-view'
import { SharedValue } from 'react-native-reanimated'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { useEagerExternalProfileNavigation } from 'src/app/navigation/hooks' import { useEagerExternalProfileNavigation } from 'src/app/navigation/hooks'
import RemoveButton from 'src/components/explore/RemoveButton' import RemoveButton from 'src/components/explore/RemoveButton'
import { useAnimatedCardDragStyle } from 'src/components/explore/hooks'
import { disableOnPress } from 'src/utils/disableOnPress' import { disableOnPress } from 'src/utils/disableOnPress'
import { Flex, TouchableArea, useIsDarkMode, useShadowPropsShort, useSporeColors } from 'ui/src' import { Flex, TouchableArea, useIsDarkMode, useShadowPropsShort, useSporeColors } from 'ui/src'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { borderRadii, iconSizes, opacify } from 'ui/src/theme' import { borderRadii, iconSizes, opacify } from 'ui/src/theme'
import { useAvatar } from 'uniswap/src/features/address/avatar' import { useAvatar } from 'uniswap/src/features/address/avatar'
import { removeWatchedAddress } from 'uniswap/src/features/favorites/slice' import { removeWatchedAddress } from 'uniswap/src/features/favorites/slice'
import { isIOS } from 'utilities/src/platform'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText' import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText'
import { useDisplayName } from 'wallet/src/features/wallet/hooks' import { useDisplayName } from 'wallet/src/features/wallet/hooks'
...@@ -21,19 +19,10 @@ import { DisplayNameType } from 'wallet/src/features/wallet/types' ...@@ -21,19 +19,10 @@ import { DisplayNameType } from 'wallet/src/features/wallet/types'
export type FavoriteWalletCardProps = { export type FavoriteWalletCardProps = {
address: Address address: Address
isEditing?: boolean isEditing?: boolean
pressProgress: SharedValue<number>
dragActivationProgress: SharedValue<number>
setIsEditing: (update: boolean) => void setIsEditing: (update: boolean) => void
} & ViewProps } & ViewProps
function FavoriteWalletCard({ function FavoriteWalletCard({ address, isEditing, setIsEditing, ...rest }: FavoriteWalletCardProps): JSX.Element {
address,
isEditing,
pressProgress,
dragActivationProgress,
setIsEditing,
...rest
}: FavoriteWalletCardProps): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const dispatch = useDispatch() const dispatch = useDispatch()
const colors = useSporeColors() const colors = useSporeColors()
...@@ -60,63 +49,60 @@ function FavoriteWalletCard({ ...@@ -60,63 +49,60 @@ function FavoriteWalletCard({
] ]
}, [t]) }, [t])
const animatedDragStyle = useAnimatedCardDragStyle(pressProgress, dragActivationProgress)
const shadowProps = useShadowPropsShort() const shadowProps = useShadowPropsShort()
return ( return (
<AnimatedFlex style={animatedDragStyle}> <ContextMenu
<ContextMenu actions={menuActions}
actions={menuActions} disabled={isEditing}
style={{ borderRadius: borderRadii.rounded16 }}
onPress={(e): void => {
// Emitted index based on order of menu action array
// remove favorite action
if (e.nativeEvent.index === 0) {
onRemove()
}
// Edit mode toggle action
if (e.nativeEvent.index === 1) {
setIsEditing(true)
}
}}
{...rest}
>
<TouchableArea
overflow={isIOS ? 'hidden' : 'visible'}
activeOpacity={isEditing ? 1 : undefined}
backgroundColor={isDarkMode ? '$surface2' : '$surface1'}
borderColor={opacify(0.05, colors.surface3.val)}
borderRadius="$rounded16"
borderWidth={isDarkMode ? '$none' : '$spacing1'}
disabled={isEditing} disabled={isEditing}
style={{ borderRadius: borderRadii.rounded16 }} m="$spacing4"
onPress={(e): void => { testID="favorite-wallet-card"
// Emitted index based on order of menu action array onLongPress={disableOnPress}
// remove favorite action onPress={(): void => {
if (e.nativeEvent.index === 0) { navigate(address)
onRemove() }}
} onPressIn={async (): Promise<void> => {
// Edit mode toggle action await preload(address)
if (e.nativeEvent.index === 1) {
setIsEditing(true)
}
}} }}
{...rest} {...shadowProps}
> >
<TouchableArea <Flex row gap="$spacing4" justifyContent="space-between" p="$spacing12">
activeOpacity={isEditing ? 1 : undefined} <Flex row shrink alignItems="center" gap="$spacing8">
backgroundColor={isDarkMode ? '$surface2' : '$surface1'} {icon}
borderColor={opacify(0.05, colors.surface3.val)} <DisplayNameText
borderRadius="$rounded16" displayName={displayName}
borderWidth={isDarkMode ? '$none' : '$spacing1'} textProps={{
disabled={isEditing} adjustsFontSizeToFit: displayName?.type === DisplayNameType.Address,
m="$spacing4" variant: 'body1',
testID="favorite-wallet-card" }}
onLongPress={disableOnPress} />
onPress={(): void => {
navigate(address)
}}
onPressIn={async (): Promise<void> => {
await preload(address)
}}
{...shadowProps}
>
<Flex row gap="$spacing4" justifyContent="space-between" p="$spacing12">
<Flex row shrink alignItems="center" gap="$spacing8">
{icon}
<DisplayNameText
displayName={displayName}
textProps={{
adjustsFontSizeToFit: displayName?.type === DisplayNameType.Address,
variant: 'body1',
}}
/>
</Flex>
<RemoveButton visible={isEditing} onPress={onRemove} />
</Flex> </Flex>
</TouchableArea> <RemoveButton visible={isEditing} onPress={onRemove} />
</ContextMenu> </Flex>
</AnimatedFlex> </TouchableArea>
</ContextMenu>
) )
} }
......
import { default as React, useCallback, useEffect, useMemo, useState } from 'react' import { default as React, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FadeIn, useAnimatedStyle, useSharedValue } from 'react-native-reanimated' import { FlatList } from 'react-native-gesture-handler'
import { AnimatedRef, FadeIn } from 'react-native-reanimated'
import type { SortableGridDragEndCallback, SortableGridRenderItem } from 'react-native-sortables'
import Sortable from 'react-native-sortables'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { FavoriteHeaderRow } from 'src/components/explore/FavoriteHeaderRow' import { FavoriteHeaderRow } from 'src/components/explore/FavoriteHeaderRow'
import FavoriteWalletCard from 'src/components/explore/FavoriteWalletCard' import FavoriteWalletCard from 'src/components/explore/FavoriteWalletCard'
import { Loader } from 'src/components/loading/loaders' import { Loader } from 'src/components/loading/loaders'
import { SortableGrid } from 'src/components/sortableGrid/SortableGrid'
import { AutoScrollProps, SortableGridChangeEvent, SortableGridRenderItem } from 'src/components/sortableGrid/types'
import { Flex } from 'ui/src' import { Flex } from 'ui/src'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { selectWatchedAddressSet } from 'uniswap/src/features/favorites/selectors' import { selectWatchedAddressSet } from 'uniswap/src/features/favorites/selectors'
...@@ -15,17 +16,17 @@ import { setFavoriteWallets } from 'uniswap/src/features/favorites/slice' ...@@ -15,17 +16,17 @@ import { setFavoriteWallets } from 'uniswap/src/features/favorites/slice'
const NUM_COLUMNS = 2 const NUM_COLUMNS = 2
const ITEM_FLEX = { flex: 1 / NUM_COLUMNS } const ITEM_FLEX = { flex: 1 / NUM_COLUMNS }
type FavoriteWalletsGridProps = AutoScrollProps & { type FavoriteWalletsGridProps = {
showLoading: boolean showLoading: boolean
listRef: AnimatedRef<FlatList>
} }
/** Renders the favorite wallets section on the Explore tab */ /** Renders the favorite wallets section on the Explore tab */
export function FavoriteWalletsGrid({ showLoading, ...rest }: FavoriteWalletsGridProps): JSX.Element { export function FavoriteWalletsGrid({ showLoading, listRef, ...rest }: FavoriteWalletsGridProps): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const dispatch = useDispatch() const dispatch = useDispatch()
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const isTokenDragged = useSharedValue(false)
const watchedWalletsSet = useSelector(selectWatchedAddressSet) const watchedWalletsSet = useSelector(selectWatchedAddressSet)
const watchedWalletsList = useMemo(() => Array.from(watchedWalletsSet), [watchedWalletsSet]) const watchedWalletsList = useMemo(() => Array.from(watchedWalletsSet), [watchedWalletsSet])
...@@ -36,61 +37,47 @@ export function FavoriteWalletsGrid({ showLoading, ...rest }: FavoriteWalletsGri ...@@ -36,61 +37,47 @@ export function FavoriteWalletsGrid({ showLoading, ...rest }: FavoriteWalletsGri
} }
}, [watchedWalletsSet.size]) }, [watchedWalletsSet.size])
const handleOrderChange = useCallback( const handleDragEnd = useCallback<SortableGridDragEndCallback<string>>(
({ data }: SortableGridChangeEvent<string>) => { ({ data }) => {
dispatch(setFavoriteWallets({ addresses: data })) dispatch(setFavoriteWallets({ addresses: data }))
}, },
[dispatch], [dispatch],
) )
const renderItem = useCallback<SortableGridRenderItem<string>>( const renderItem = useCallback<SortableGridRenderItem<string>>(
({ item: address, pressProgress, dragActivationProgress }): JSX.Element => ( ({ item: address }): JSX.Element => (
<FavoriteWalletCard <FavoriteWalletCard address={address} isEditing={isEditing} setIsEditing={setIsEditing} />
key={address}
address={address}
dragActivationProgress={dragActivationProgress}
isEditing={isEditing}
pressProgress={pressProgress}
setIsEditing={setIsEditing}
/>
), ),
[isEditing], [isEditing],
) )
const animatedStyle = useAnimatedStyle(() => ({
zIndex: isTokenDragged.value ? 1 : 0,
}))
return ( return (
<AnimatedFlex entering={FadeIn} style={animatedStyle}> <Sortable.Layer>
<FavoriteHeaderRow <AnimatedFlex entering={FadeIn}>
editingTitle={t('explore.wallets.favorite.title.edit')} <FavoriteHeaderRow
isEditing={isEditing} editingTitle={t('explore.wallets.favorite.title.edit')}
title={t('explore.wallets.favorite.title.default')} isEditing={isEditing}
disabled={showLoading} title={t('explore.wallets.favorite.title.default')}
onPress={(): void => setIsEditing(!isEditing)} disabled={showLoading}
/> onPress={(): void => setIsEditing(!isEditing)}
{showLoading ? (
<FavoriteWalletsGridLoader />
) : (
<SortableGrid
{...rest}
activeItemOpacity={1}
animateContainerHeight={false}
data={watchedWalletsList}
editable={isEditing}
numColumns={NUM_COLUMNS}
renderItem={renderItem}
onChange={handleOrderChange}
onDragStart={(): void => {
isTokenDragged.value = true
}}
onDrop={(): void => {
isTokenDragged.value = false
}}
/> />
)} {showLoading ? (
</AnimatedFlex> <FavoriteWalletsGridLoader />
) : (
<Sortable.Grid
{...rest}
animateHeight
scrollableRef={listRef}
autoScrollActivationOffset={[75, 100]}
data={watchedWalletsList}
sortEnabled={isEditing}
columns={NUM_COLUMNS}
renderItem={renderItem}
onDragEnd={handleDragEnd}
/>
)}
</AnimatedFlex>
</Sortable.Layer>
) )
} }
......
...@@ -19,7 +19,7 @@ describe('SortButton', () => { ...@@ -19,7 +19,7 @@ describe('SortButton', () => {
}) })
it('renders without error', async () => { it('renders without error', async () => {
const tree = render(<SortButton orderBy={RankingType.Volume} />) const tree = render(<SortButton orderBy={RankingType.Volume} onOrderByChange={() => {}} />)
await act(async () => { await act(async () => {
jest.runAllTimers() jest.runAllTimers()
...@@ -46,7 +46,7 @@ describe('SortButton', () => { ...@@ -46,7 +46,7 @@ describe('SortButton', () => {
describe.each(cases)('when ordering by $test', ({ orderBy, label }) => { describe.each(cases)('when ordering by $test', ({ orderBy, label }) => {
it(`renders ${label} as the selected option`, async () => { it(`renders ${label} as the selected option`, async () => {
const { queryByText } = render(<SortButton orderBy={orderBy} />) const { queryByText } = render(<SortButton orderBy={orderBy} onOrderByChange={() => {}} />)
await act(async () => { await act(async () => {
jest.runAllTimers() jest.runAllTimers()
}) })
......
import React, { memo, useCallback, useMemo } from 'react' import React, { memo, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import { getTokensOrderByMenuLabel, getTokensOrderBySelectedLabel } from 'src/features/explore/utils' import { getTokensOrderByMenuLabel, getTokensOrderBySelectedLabel } from 'src/features/explore/utils'
import { Flex, Text, useSporeColors } from 'ui/src' import { Flex, Text, useSporeColors } from 'ui/src'
import { import {
...@@ -19,17 +18,16 @@ import { CustomRankingType, RankingType } from 'uniswap/src/data/types' ...@@ -19,17 +18,16 @@ import { CustomRankingType, RankingType } from 'uniswap/src/data/types'
import { MobileEventName } from 'uniswap/src/features/telemetry/constants' import { MobileEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { setTokensOrderBy } from 'wallet/src/features/wallet/slice'
import { ExploreOrderBy } from 'wallet/src/features/wallet/types' import { ExploreOrderBy } from 'wallet/src/features/wallet/types'
const MIN_MENU_ITEM_WIDTH = 220 const MIN_MENU_ITEM_WIDTH = 220
interface FilterGroupProps { interface FilterGroupProps {
orderBy: ExploreOrderBy orderBy: ExploreOrderBy
onOrderByChange: (orderBy: ExploreOrderBy) => void
} }
function _SortButton({ orderBy }: FilterGroupProps): JSX.Element { function _SortButton({ orderBy, onOrderByChange }: FilterGroupProps): JSX.Element {
const dispatch = useDispatch()
const { t } = useTranslation() const { t } = useTranslation()
const colors = useSporeColors() const colors = useSporeColors()
...@@ -86,6 +84,16 @@ function _SortButton({ orderBy }: FilterGroupProps): JSX.Element { ...@@ -86,6 +84,16 @@ function _SortButton({ orderBy }: FilterGroupProps): JSX.Element {
) )
}, []) }, [])
const handleOrderByChange = useCallback(
(newOrderBy: ExploreOrderBy) => {
onOrderByChange(newOrderBy)
sendAnalyticsEvent(MobileEventName.ExploreFilterSelected, {
filter_type: newOrderBy,
})
},
[onOrderByChange],
)
const options = useMemo<MenuItemProp[]>(() => { const options = useMemo<MenuItemProp[]>(() => {
return menuActions.map((option, index) => { return menuActions.map((option, index) => {
return { return {
...@@ -98,15 +106,12 @@ function _SortButton({ orderBy }: FilterGroupProps): JSX.Element { ...@@ -98,15 +106,12 @@ function _SortButton({ orderBy }: FilterGroupProps): JSX.Element {
}) })
return return
} }
dispatch(setTokensOrderBy({ newTokensOrderBy: option.orderBy })) handleOrderByChange(selectedMenuAction.orderBy)
sendAnalyticsEvent(MobileEventName.ExploreFilterSelected, {
filter_type: selectedMenuAction.orderBy,
})
}, },
render: () => <MenuItem active={option.active} icon={option.icon} label={option.title} />, render: () => <MenuItem active={option.active} icon={option.icon} label={option.title} />,
} }
}) })
}, [MenuItem, dispatch, menuActions]) }, [MenuItem, menuActions, handleOrderByChange])
return ( return (
<ActionSheetDropdown options={options} showArrow={false} styles={{ alignment: 'right' }}> <ActionSheetDropdown options={options} showArrow={false} styles={{ alignment: 'right' }}>
......
...@@ -13,7 +13,7 @@ import { Flex, useSporeColors } from 'ui/src' ...@@ -13,7 +13,7 @@ import { Flex, useSporeColors } from 'ui/src'
import { GQLQueries } from 'uniswap/src/data/graphql/uniswap-data-api/queries' import { GQLQueries } from 'uniswap/src/data/graphql/uniswap-data-api/queries'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' import { useAppInsets } from 'uniswap/src/hooks/useAppInsets'
import { DDRumManualTiming } from 'utilities/src/logger/datadogEvents' import { DDRumManualTiming } from 'utilities/src/logger/datadog/datadogEvents'
import { usePerformanceLogger } from 'utilities/src/logger/usePerformanceLogger' import { usePerformanceLogger } from 'utilities/src/logger/usePerformanceLogger'
import { isAndroid } from 'utilities/src/platform' import { isAndroid } from 'utilities/src/platform'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
......
import { LayoutChangeEvent, MeasureLayoutOnSuccessCallback, StyleSheet } from 'react-native'
import Animated, { LayoutAnimationConfig, useAnimatedStyle } from 'react-native-reanimated'
import { useAutoScrollContext } from 'src/components/sortableGrid/contexts/AutoScrollContextProvider'
import { useLayoutContext } from 'src/components/sortableGrid/contexts/LayoutContextProvider'
import { SortableGridProvider } from 'src/components/sortableGrid/internal/SortableGirdProvider'
import SortableGridItem from 'src/components/sortableGrid/internal/SortableGridItem'
import { useItemOrderUpdater } from 'src/components/sortableGrid/internal/hooks'
import { defaultKeyExtractor, useStableCallback } from 'src/components/sortableGrid/internal/utils'
import {
ActiveItemDecorationSettings,
AutoScrollProps,
SortableGridChangeEvent,
SortableGridDragStartEvent,
SortableGridDropEvent,
SortableGridRenderItem,
} from 'src/components/sortableGrid/types'
type SortableGridProps<I> = AutoScrollProps &
Partial<ActiveItemDecorationSettings> & {
data: I[]
renderItem: SortableGridRenderItem<I>
numColumns?: number
editable?: boolean
animateContainerHeight?: boolean
keyExtractor?: (item: I, index: number) => string
onChange?: (e: SortableGridChangeEvent<I>) => void
onDragStart?: (e: SortableGridDragStartEvent<I>) => void
onDrop?: (e: SortableGridDropEvent<I>) => void
}
export function SortableGrid<I>({
data,
renderItem,
numColumns = 1,
keyExtractor = defaultKeyExtractor,
containerRef,
...rest
}: SortableGridProps<I>): JSX.Element {
const stableKeyExtractor = useStableCallback(keyExtractor)
const sharedProps = {
data,
numColumns,
keyExtractor: stableKeyExtractor,
}
return (
<SortableGridProvider {...sharedProps} {...rest}>
<SortableGridInner {...sharedProps} containerRef={containerRef} renderItem={renderItem} />
</SortableGridProvider>
)
}
type SortableGridInnerProps<I> = Pick<
SortableGridProps<I>,
'data' | 'renderItem' | 'numColumns' | 'keyExtractor' | 'containerRef'
>
function SortableGridInner<I>({
data,
renderItem,
containerRef,
numColumns = 1,
keyExtractor = defaultKeyExtractor,
}: SortableGridInnerProps<I>): JSX.Element {
const { containerHeight, containerWidth, appliedContainerHeight } = useLayoutContext()
const { gridContainerRef, containerStartOffset } = useAutoScrollContext()
useItemOrderUpdater(numColumns)
const handleGridMeasurement = ({
nativeEvent: {
layout: { height, width },
},
}: LayoutChangeEvent): void => {
if (containerHeight.value !== -1) {
return
}
// Measure container using onLayout only once, on the initial render
// (container dimensions will be updated from the context provider
// when data changes, so we don't want to re-measure the container)
if (containerHeight.value === -1) {
containerHeight.value = height
containerWidth.value = width
}
// Measure offset relative to the specifiec container (if containerRef
// is provided, otherwise assume that the grid component is the first
// child of the the parent container)
const onSuccess: MeasureLayoutOnSuccessCallback = (_, y) => {
containerStartOffset.value = y
}
const parentNode = containerRef?.current
const gridNode = gridContainerRef.current
if (parentNode && gridNode) {
gridNode.measureLayout(parentNode, onSuccess)
}
}
const handleHelperMeasurement = ({
nativeEvent: {
layout: { height },
},
}: LayoutChangeEvent): void => {
if (appliedContainerHeight.value === -1 && height === 0) {
return
}
appliedContainerHeight.value = height
}
const animatedContainerStyle = useAnimatedStyle(() => ({
width: containerWidth.value === -1 ? 'auto' : containerWidth.value,
height: containerHeight.value === -1 ? 'auto' : containerHeight.value,
}))
return (
<LayoutAnimationConfig skipExiting>
<Animated.View
ref={gridContainerRef}
style={[styles.gridContainer, animatedContainerStyle]}
onLayout={handleGridMeasurement}
>
{data.map((item, index) => {
const key = keyExtractor(item, index)
return (
<SortableGridItem key={key} item={item} itemKey={key} numColumns={numColumns} renderItem={renderItem} />
)
})}
</Animated.View>
{/* This dummy Animated.View is used only to determine if the containerHeight
from the animated style was applied. We can't use onLayout on the grid items wrapper component because it already has the same height as containerHeight
value, thus the onLayout callback won't be called again, because the size
of the component doesn't change. */}
<Animated.View style={[styles.helperView, animatedContainerStyle]} onLayout={handleHelperMeasurement} />
</LayoutAnimationConfig>
)
}
const styles = StyleSheet.create({
gridContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
},
helperView: {
opacity: 0,
pointerEvents: 'none',
position: 'absolute',
},
})
import { PropsWithChildren, createContext, useContext, useMemo, useRef } from 'react'
import { View } from 'react-native'
import { runOnJS, useAnimatedReaction, useDerivedValue, useSharedValue } from 'react-native-reanimated'
import { useDragContext } from 'src/components/sortableGrid/contexts/DragContextProvider'
import { useLayoutContext } from 'src/components/sortableGrid/contexts/LayoutContextProvider'
import { AUTO_SCROLL_THRESHOLD } from 'src/components/sortableGrid/internal/constants'
import { useStableCallback } from 'src/components/sortableGrid/internal/utils'
import { AutoScrollContextType, AutoScrollProps } from 'src/components/sortableGrid/types'
const AutoScrollContext = createContext<AutoScrollContextType | null>(null)
export function useAutoScrollContext(): AutoScrollContextType {
const context = useContext(AutoScrollContext)
if (!context) {
throw new Error('useAutoScrollContext must be used within a AutoScrollProvider')
}
return context
}
export type AutoScrollProviderProps = PropsWithChildren<Omit<AutoScrollProps, 'containerRef'>>
export function AutoScrollProvider({
children,
scrollableRef,
scrollY: scrollYValue,
visibleHeight: visibleHeightValue,
}: AutoScrollProviderProps): JSX.Element {
const { itemDimensions, targetContainerHeight } = useLayoutContext()
const { activeItemKey, activeItemPosition } = useDragContext()
/**
* VARIABLES
*/
// HELPER VARIABLES
const scrollTarget = useSharedValue(0)
const scrollDirection = useSharedValue(0) // 1 = down, -1 = up
const activeItemHeight = useDerivedValue(
() => (activeItemKey.value ? itemDimensions.value[activeItemKey.value]?.height : -1) ?? -1,
)
// REFS
const gridContainerRef = useRef<View>(null)
// MEASUREMENTS
// Values used to scroll the container to the proper offset
// (updated from the SortableGridInner component)
const containerStartOffset = useSharedValue(0)
const containerEndOffset = useDerivedValue(() => containerStartOffset.value + targetContainerHeight.value)
const startScrollOffset = useSharedValue(0)
const scrollOffsetDiff = useDerivedValue(() =>
activeItemKey.value === null ? 0 : scrollYValue.value - startScrollOffset.value,
)
/**
* HANDLERS
*/
const scrollToOffset = useStableCallback((offset: number) => {
const scrollable = scrollableRef.current
if (!scrollable || activeItemKey.value === null) {
return
}
if ('scrollTo' in scrollable) {
scrollable.scrollTo({ y: offset, animated: true })
} else {
scrollable.scrollToOffset({ offset, animated: true })
}
})
/**
* REACTIONS
*/
// Reset scroll properties when the active item changes
useAnimatedReaction(
() => activeItemKey.value,
() => {
// Reset when the active index changes
scrollDirection.value = 0
},
)
// AUTO SCROLL HANDLER
// Automatically scrolls the container when the active item is near the edge
useAnimatedReaction(
() => {
if (activeItemHeight.value === -1) {
return null
}
return {
itemAbsoluteY: activeItemPosition.value.y + containerStartOffset.value + scrollOffsetDiff.value,
activeHeight: activeItemHeight.value,
minOffset: containerStartOffset.value,
maxOffset: containerEndOffset.value - visibleHeightValue.value,
visibleHeight: visibleHeightValue.value,
scrollY: scrollYValue.value,
}
},
(props) => {
if (!props) {
return
}
const { itemAbsoluteY, scrollY, minOffset, maxOffset, activeHeight, visibleHeight } = props
let currentScrollTarget = scrollTarget.value
let currentScrollDirection = scrollDirection.value
/**
* |----------------------|
* | content above grid |
* |----------------------| <- minOffset (- threshold to scroll a bit above the grid)
* | invisible grid above |
* | (optional) | - if the scrollable container was scrolled down
* |----------------------|
* | visible grid part |
* |----------------------|
* | invisible grid below | - if the scrollable container was scrolled up enough
* | (optional) |
* |----------------------| <- maxOffset (+ threshold to scroll a bit below the grid)
* | content below grid |
* |----------------------|
*/
// If the active item is above the current scroll position (with small threshold
// to start scrolling earlier) and the scroll position is not at the top of the
// grid, scroll up
if (itemAbsoluteY < scrollY + AUTO_SCROLL_THRESHOLD && scrollY > minOffset - AUTO_SCROLL_THRESHOLD) {
currentScrollTarget = Math.max(minOffset - AUTO_SCROLL_THRESHOLD, scrollY - activeHeight)
currentScrollDirection = -1
}
// If the active item is below the current scroll position (with small threshold
// to start scrolling earlier) and the scroll position is not at the bottom of the
// grid, scroll down
else if (
itemAbsoluteY + activeHeight > scrollY + visibleHeight - AUTO_SCROLL_THRESHOLD &&
scrollY < maxOffset + AUTO_SCROLL_THRESHOLD
) {
currentScrollTarget = Math.min(maxOffset + AUTO_SCROLL_THRESHOLD, scrollY + activeHeight)
currentScrollDirection = 1
}
const scrollDiff = Math.abs(currentScrollTarget - scrollTarget.value)
if (
// Don't scroll if the difference is too small (limit JS thread updates that
// become laggy when too many are triggered) and the scroll direction is the same
// as before and the scroll target is still far enough from the min/max offset
(scrollDiff < 0.75 * activeHeight &&
currentScrollDirection === scrollDirection.value &&
currentScrollTarget > minOffset - AUTO_SCROLL_THRESHOLD &&
currentScrollTarget < maxOffset + AUTO_SCROLL_THRESHOLD) ||
// Don't scroll if the difference is too small and the target can be considered
// reached
Math.abs(scrollDiff) < 2
) {
return
}
scrollDirection.value = currentScrollDirection
scrollTarget.value = currentScrollTarget
runOnJS(scrollToOffset)(currentScrollTarget)
},
)
/**
* CONTEXT VALUE
*/
const contextValue = useMemo(
() => ({
gridContainerRef,
containerStartOffset,
containerEndOffset,
scrollOffsetDiff,
startScrollOffset,
scrollY: scrollYValue,
}),
[gridContainerRef, containerStartOffset, containerEndOffset, scrollOffsetDiff, startScrollOffset, scrollYValue],
)
return <AutoScrollContext.Provider value={contextValue}>{children}</AutoScrollContext.Provider>
}
import { createContext, useContext, useMemo } from 'react'
import { runOnJS, useAnimatedReaction, useDerivedValue, useSharedValue } from 'react-native-reanimated'
import { useLayoutContext } from 'src/components/sortableGrid/contexts/LayoutContextProvider'
import { useStableCallback } from 'src/components/sortableGrid/internal/utils'
import { DragContextProviderProps, DragContextType, Vector } from 'src/components/sortableGrid/types'
const DragContext = createContext<DragContextType | null>(null)
export function useDragContext(): DragContextType {
const context = useContext(DragContext)
if (!context) {
throw new Error('useDragContext must be used within a DragContextProvider')
}
return context
}
export function DragContextProvider<I>({
data,
itemKeys,
editable = true,
activeItemScale: activeItemScaleProp = 1.1,
activeItemOpacity: activeItemOpacityProp = 0.7,
activeItemShadowOpacity: activeItemShadowOpacityProp = 0.5,
onDragStart,
onDrop,
onChange,
keyExtractor,
children,
}: DragContextProviderProps<I>): JSX.Element {
const { keyToIndex } = useLayoutContext()
/**
* VARIABLES
*/
// ACTIVE ITEM
const activeItemKey = useSharedValue<string | null>(null)
const prevActiveItemKey = useSharedValue<string | null>(null)
const activeItemDropped = useSharedValue(false)
// DRAG ACTIVATION
const activationProgress = useSharedValue(0)
const activeItemPosition = useSharedValue<Vector>({ x: 0, y: 0 })
// ACTIVE ITEM DECORATION
const activeItemScale = useDerivedValue(() => activeItemScaleProp)
const activeItemOpacity = useDerivedValue(() => activeItemOpacityProp)
const activeItemShadowOpacity = useDerivedValue(() => activeItemShadowOpacityProp)
/**
* HANDLERS
*/
const handleDragStart = useStableCallback(async (key: string, keyToIdx: Record<string, number>) => {
const index = keyToIdx[key]
if (index === undefined) {
return
}
const item = data[index]
if (!onDragStart || !item) {
return
}
onDragStart({ index, item })
})
const handleDrop = useStableCallback((key: string, keyToIdx: Record<string, number>) => {
const index = keyToIdx[key]
if (index === undefined) {
return
}
const item = data[index]
if (!onDrop || index === undefined || !item) {
return
}
onDrop({ index, item })
})
const handleChange = useStableCallback(async (swappedKey: string, keyToIdx: Record<string, number>) => {
if (!onChange) {
return
}
const toIndex = keyToIdx[swappedKey]
if (toIndex === undefined) {
return
}
const fromIndex = itemKeys.indexOf(swappedKey)
const reorderedData: I[] = []
for (let i = 0; i < data.length; i++) {
const item = data[i]
if (!item) {
return
}
const itemKey = keyExtractor(item, i)
const index = keyToIdx[itemKey]
if (index === undefined) {
return
}
reorderedData[index] = item
}
onChange({ data: reorderedData, fromIndex, toIndex })
})
/**
* REACTIONS
*/
// Handle drag start and order change (on drag end)
useAnimatedReaction(
() => activeItemKey.value,
(key, prevKey) => {
if (key !== null && prevKey === null) {
runOnJS(handleDragStart)(key, keyToIndex.value)
} else if (key === null && prevKey !== null) {
runOnJS(handleChange)(prevKey, keyToIndex.value)
}
if (key !== null) {
prevActiveItemKey.value = key
}
},
[handleDragStart, handleChange],
)
// Handle drop (after animation of the active item is finished
// and the item is dropped in the new position)
useAnimatedReaction(
() => activeItemDropped.value,
(dropped) => {
if (dropped && prevActiveItemKey.value !== null) {
runOnJS(handleDrop)(prevActiveItemKey.value, keyToIndex.value)
}
},
[handleDrop],
)
/**
* CONTEXT VALUE
*/
const contextValue = useMemo(
() => ({
editable,
activeItemKey,
activeItemDropped,
activationProgress,
activeItemPosition,
activeItemScale,
activeItemOpacity,
activeItemShadowOpacity,
}),
[
editable,
activeItemKey,
activeItemDropped,
activationProgress,
activeItemPosition,
activeItemScale,
activeItemOpacity,
activeItemShadowOpacity,
],
)
return <DragContext.Provider value={contextValue}>{children}</DragContext.Provider>
}
import { PropsWithChildren, createContext, useContext, useEffect, useMemo, useRef } from 'react'
import {
SharedValue,
useAnimatedReaction,
useDerivedValue,
useSharedValue,
withDelay,
withTiming,
} from 'react-native-reanimated'
import { ITEM_ANIMATION_DURATION, OFFSET_EPS } from 'src/components/sortableGrid/internal/constants'
import { areArraysDifferent, getColumnIndex, getRowIndex } from 'src/components/sortableGrid/internal/utils'
import { Dimensions, Vector } from 'src/components/sortableGrid/types'
const EMPTY_ARRAY: unknown[] = []
const EMPTY_OBJECT = {}
export type LayoutContextType = {
// HELPER VALUES
initialRenderCompleted: SharedValue<boolean>
measuredItemsCount: SharedValue<number>
// DIMENSIONS
rowOffsets: SharedValue<number[]>
itemDimensions: SharedValue<Record<string, Dimensions>>
containerWidth: SharedValue<number>
containerHeight: SharedValue<number>
targetContainerHeight: SharedValue<number>
appliedContainerHeight: SharedValue<number>
columnWidth: SharedValue<number>
// KEY-INDEX MAPPINGS
keyToIndex: SharedValue<Record<string, number>>
indexToKey: SharedValue<string[]>
// POSITIONING
itemPositions: SharedValue<Record<string, Vector>>
}
const LayoutContext = createContext<LayoutContextType | null>(null)
export function useLayoutContext(): LayoutContextType {
const context = useContext(LayoutContext)
if (!context) {
throw new Error('useLayoutContext must be used within a LayoutContextProvider')
}
return context
}
export type LayoutContextProviderProps = PropsWithChildren<{
itemKeys: string[]
numColumns: number
animateContainerHeight?: boolean
}>
export function LayoutContextProvider({
itemKeys,
numColumns,
animateContainerHeight = true,
children,
}: LayoutContextProviderProps): JSX.Element {
/**
* VARIABLES
*/
// HELPER VALUES
const prevKeysRef = useRef<string[]>([])
const rowsCount = Math.ceil(itemKeys.length / numColumns)
const initialRenderCompleted = useSharedValue(false)
const appliedContainerHeight = useSharedValue(-1)
const measuredItemsCount = useSharedValue(0)
// DIMENSIONS
const rowOffsets = useSharedValue<number[]>([])
const itemDimensions = useSharedValue<Record<string, Dimensions>>({})
const containerWidth = useSharedValue(-1)
const containerHeight = useSharedValue(-1)
const targetContainerHeight = useSharedValue(-1)
const columnWidth = useDerivedValue(() => (containerWidth.value === -1 ? -1 : containerWidth.value / numColumns))
// KEY-INDEX MAPPINGS
const indexToKey = useSharedValue<string[]>([])
const keyToIndex = useDerivedValue(() => Object.fromEntries(indexToKey.value.map((key, index) => [key, index])))
// POSITIONING
const itemPositions = useDerivedValue<Record<string, Vector>>(() => {
// Return empty object if columnWidth is not yet calculated or if the number
// of rows is not yet known
if (columnWidth.value === -1 || rowOffsets.value.length < rowsCount) {
return EMPTY_OBJECT
}
// Calculate item positions based on their order in the grid
return Object.fromEntries(
Object.entries(indexToKey.value).map(([index, key]) => [
key,
{
x: columnWidth.value * getColumnIndex(parseInt(index, 10), numColumns),
y: rowOffsets.value[getRowIndex(parseInt(index, 10), numColumns)] ?? 0,
},
]),
)
}, [rowsCount, columnWidth, rowOffsets, indexToKey, numColumns])
/**
* EFFECTS
*/
// Update indexToKey when itemKeys change (only if the arrays are different in
// terms of their elements, not just their references - this prevents unnecessary
// value updates if the array is the new object but has all the same contents)
useEffect(() => {
if (areArraysDifferent(itemKeys, prevKeysRef.current)) {
indexToKey.value = itemKeys
prevKeysRef.current = itemKeys
}
}, [itemKeys, indexToKey])
// ITEM DIMENSIONS UPDATER
useAnimatedReaction(
() => measuredItemsCount.value,
(count) => {
// Re-create the item dimensions object if all items have been measured
// (this is done to prevent unnecessary object updates after each item measurement)
if (count === itemKeys.length) {
itemDimensions.value = { ...itemDimensions.value }
}
},
[itemKeys],
)
// ROW OFFSETS UPDATER
useAnimatedReaction(
() => ({
dimensions: itemDimensions.value,
idxToKey: indexToKey.value,
}),
({ dimensions, idxToKey }) => {
// Return an empty array if items haven't been measured yet
if (Object.keys(dimensions).length === 0) {
return EMPTY_ARRAY
}
const offsets = [0]
for (const [itemIndex, key] of Object.entries(idxToKey)) {
const rowIndex = getRowIndex(parseInt(itemIndex, 10), numColumns)
offsets[rowIndex + 1] = Math.max(
offsets[rowIndex + 1] ?? 0,
(offsets[rowIndex] ?? 0) + (dimensions[key]?.height ?? 0),
)
}
// Update row offsets only if they have changed
if (areArraysDifferent(offsets, rowOffsets.value, (a, b) => Math.abs(a - b) < OFFSET_EPS)) {
if (!rowOffsets.value.length) {
initialRenderCompleted.value = true
}
rowOffsets.value = offsets
}
return undefined
},
[numColumns],
)
useAnimatedReaction(
() => rowOffsets.value,
(offsets) => {
const newHeight = offsets[offsets.length - 1] ?? -1
targetContainerHeight.value = newHeight
if (newHeight === -1) {
return
}
const duration = animateContainerHeight ? ITEM_ANIMATION_DURATION : 0
// If container is expanded, animate its height immediately
if (newHeight > containerHeight.value) {
containerHeight.value = withTiming(newHeight, { duration })
}
// If container is shrunk, delay the animation to allow the items to disappear
else if (newHeight < containerHeight.value) {
const delay = (animateContainerHeight ? 0.25 : 1) * ITEM_ANIMATION_DURATION
containerHeight.value = withDelay(delay, withTiming(newHeight, { duration }))
}
// In all other
else {
containerHeight.value = newHeight
}
},
[animateContainerHeight],
)
/**
* CONTEXT VALUE
*/
const contextValue = useMemo<LayoutContextType>(
() => ({
initialRenderCompleted,
appliedContainerHeight,
measuredItemsCount,
rowOffsets,
itemDimensions,
containerWidth,
containerHeight,
targetContainerHeight,
columnWidth,
keyToIndex,
indexToKey,
itemPositions,
}),
[
initialRenderCompleted,
appliedContainerHeight,
measuredItemsCount,
rowOffsets,
itemDimensions,
containerWidth,
containerHeight,
targetContainerHeight,
columnWidth,
keyToIndex,
indexToKey,
itemPositions,
],
)
return <LayoutContext.Provider value={contextValue}>{children}</LayoutContext.Provider>
}
import { PropsWithChildren, useMemo } from 'react'
import {
AutoScrollProvider,
AutoScrollProviderProps,
} from 'src/components/sortableGrid/contexts/AutoScrollContextProvider'
import { DragContextProvider } from 'src/components/sortableGrid/contexts/DragContextProvider'
import {
LayoutContextProvider,
LayoutContextProviderProps,
} from 'src/components/sortableGrid/contexts/LayoutContextProvider'
import { DragContextProviderProps } from 'src/components/sortableGrid/types'
type SortableGridProviderProps<I> = PropsWithChildren<
Omit<LayoutContextProviderProps & DragContextProviderProps<I> & AutoScrollProviderProps, 'itemKeys'>
>
export function SortableGridProvider<I>({
children,
data,
numColumns,
editable,
animateContainerHeight,
activeItemScale,
activeItemOpacity,
activeItemShadowOpacity,
scrollableRef,
visibleHeight,
scrollY,
onChange,
onDragStart,
onDrop,
keyExtractor,
}: SortableGridProviderProps<I>): JSX.Element {
const itemKeys = useMemo(() => data.map(keyExtractor), [data, keyExtractor])
const sharedProps = {
itemKeys,
numColumns,
}
return (
<LayoutContextProvider {...sharedProps} animateContainerHeight={animateContainerHeight}>
<DragContextProvider
{...sharedProps}
activeItemOpacity={activeItemOpacity}
activeItemScale={activeItemScale}
activeItemShadowOpacity={activeItemShadowOpacity}
data={data}
editable={editable}
keyExtractor={keyExtractor}
onChange={onChange}
onDragStart={onDragStart}
onDrop={onDrop}
>
<AutoScrollProvider scrollY={scrollY} scrollableRef={scrollableRef} visibleHeight={visibleHeight}>
{children}
</AutoScrollProvider>
</DragContextProvider>
</LayoutContextProvider>
)
}
import { memo, useCallback, useEffect, useMemo } from 'react'
import { LayoutChangeEvent } from 'react-native'
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import Animated, {
interpolate,
interpolateColor,
runOnUI,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
withDelay,
withTiming,
} from 'react-native-reanimated'
import { useAutoScrollContext } from 'src/components/sortableGrid/contexts/AutoScrollContextProvider'
import { useDragContext } from 'src/components/sortableGrid/contexts/DragContextProvider'
import { useLayoutContext } from 'src/components/sortableGrid/contexts/LayoutContextProvider'
import {
ACTIVATE_PAN_ANIMATION_DELAY,
ITEM_ANIMATION_DURATION,
OFFSET_EPS,
TIME_TO_ACTIVATE_PAN,
} from 'src/components/sortableGrid/internal/constants'
import { useItemPosition } from 'src/components/sortableGrid/internal/hooks'
import { GridItemExiting } from 'src/components/sortableGrid/internal/layoutAnimations'
import { getItemZIndex } from 'src/components/sortableGrid/internal/utils'
import { SortableGridRenderItem } from 'src/components/sortableGrid/types'
type SortableGridItemProps<I> = {
item: I
itemKey: string
renderItem: SortableGridRenderItem<I>
numColumns: number
}
function SortableGridItem<I>({ item, itemKey, renderItem, numColumns }: SortableGridItemProps<I>): JSX.Element {
const {
measuredItemsCount,
targetContainerHeight,
initialRenderCompleted,
appliedContainerHeight,
itemDimensions,
itemPositions,
columnWidth,
} = useLayoutContext()
const {
activeItemScale,
activeItemOpacity,
activeItemShadowOpacity,
activeItemPosition,
activationProgress,
activeItemDropped,
activeItemKey,
editable,
} = useDragContext()
const { scrollY, startScrollOffset } = useAutoScrollContext()
const isTouched = useSharedValue(false)
const isActive = useDerivedValue(() => activeItemKey.value === itemKey)
const itemHeight = useDerivedValue(() => itemDimensions.value[itemKey]?.height ?? 0)
const pressProgress = useSharedValue(0)
const position = useItemPosition(itemKey)
const dragStartPosition = useSharedValue({ x: 0, y: 0 })
const targetItemPosition = useDerivedValue(() => itemPositions.value[itemKey])
useEffect(() => {
return (): void => {
// Remove item dimensions when the item is unmounted
runOnUI((key: string) => {
delete itemDimensions.value[key]
measuredItemsCount.value -= 1
// If was active, reset active item key
if (activeItemKey.value === key) {
activeItemKey.value = null
}
})(itemKey)
}
}, [itemKey, activeItemKey, itemDimensions, measuredItemsCount])
const measureItem = useCallback(
({
nativeEvent: {
layout: { width, height },
},
}: LayoutChangeEvent) => {
runOnUI((key: string) => {
// Store item dimensions without re-creating the dimensions object
if (!itemDimensions.value[key]) {
measuredItemsCount.value += 1
}
itemDimensions.value[key] = { width, height }
})(itemKey)
},
[itemKey, itemDimensions, measuredItemsCount],
)
const handleDragEnd = useCallback(() => {
'worklet'
isTouched.value = false
activeItemKey.value = null
pressProgress.value = withTiming(0, { duration: TIME_TO_ACTIVATE_PAN })
activationProgress.value = withTiming(0, { duration: TIME_TO_ACTIVATE_PAN }, () => {
activeItemDropped.value = true
})
}, [activationProgress, activeItemDropped, activeItemKey, isTouched, pressProgress])
const panGesture = useMemo(
() =>
Gesture.Pan()
.activateAfterLongPress(TIME_TO_ACTIVATE_PAN)
.onTouchesDown(() => {
isTouched.value = true
const progress = withDelay(
ACTIVATE_PAN_ANIMATION_DELAY,
withTiming(1, { duration: TIME_TO_ACTIVATE_PAN - ACTIVATE_PAN_ANIMATION_DELAY }),
)
pressProgress.value = progress
activationProgress.value = progress
})
.onStart(() => {
if (!isTouched.value) {
return
}
dragStartPosition.value = activeItemPosition.value = {
x: position.x.value ?? 0,
y: position.y.value ?? 0,
}
activeItemKey.value = itemKey
startScrollOffset.value = scrollY.value
activeItemDropped.value = false
})
.onUpdate((e) => {
if (!isActive.value) {
return
}
activeItemPosition.value = {
x: dragStartPosition.value.x + e.translationX,
y: dragStartPosition.value.y + e.translationY,
}
})
.onFinalize(handleDragEnd)
.enabled(editable),
[
editable,
activationProgress,
activeItemDropped,
activeItemKey,
activeItemPosition,
dragStartPosition,
handleDragEnd,
isActive,
isTouched,
itemKey,
position,
pressProgress,
scrollY,
startScrollOffset,
],
)
// ITEM POSITIONING AND ANIMATION
const animatedItemStyle = useAnimatedStyle(() => {
// INITIAL RENDER
// (relative placements - no absolute positioning yet)
// This ensures there is no blank space when grid items are being measured
if (!initialRenderCompleted.value || appliedContainerHeight.value === -1 || columnWidth.value === -1) {
return {
width: `${100 / numColumns}%`,
}
}
const x = position.x.value
const y = position.y.value
// ADDED ITEM AFTER INITIAL RENDER
// (item is not yet measured -> don't render it)
// This ensures the item is not misplaced when it is added to the grid
if (
x === null ||
y === null ||
// If the item bottom edge is rendered below the container bottom edge
(y + itemHeight.value - appliedContainerHeight.value > OFFSET_EPS &&
// And the container height is lower than the target height
targetContainerHeight.value - appliedContainerHeight.value > OFFSET_EPS &&
// And the item is not being dragged
!isActive.value)
) {
return {
pointerEvents: 'none',
position: 'absolute',
transform: [{ scale: 0.5 }],
opacity: 0,
width: columnWidth.value,
}
}
// ABSOLUTE POSITIONING
// (item is measured and rendered)
// This ensures the item is rendered in the correct position and responds
// to grid items order changes and drag events
return {
pointerEvents: 'auto',
position: 'absolute',
opacity: withTiming(1, { duration: ITEM_ANIMATION_DURATION }),
transform: [{ scale: withTiming(1, { duration: ITEM_ANIMATION_DURATION }) }],
top: y,
left: x,
width: columnWidth.value,
zIndex: getItemZIndex(isActive.value, pressProgress.value, { x, y }, targetItemPosition.value),
}
})
// ITEM DECORATION
// (only for the active item being dragged)
const animatedItemDecorationStyle = useAnimatedStyle(() => ({
transform: [{ scale: interpolate(pressProgress.value, [0, 1], [1, activeItemScale.value]) }],
opacity: interpolate(pressProgress.value, [0, 1], [1, activeItemOpacity.value]),
shadowColor: interpolateColor(
pressProgress.value,
[0, 1],
['transparent', `rgba(0, 0, 0, ${activeItemShadowOpacity.value})`],
),
}))
const content = useMemo(
() =>
renderItem({
item,
pressProgress,
dragActivationProgress: activationProgress,
}),
[item, renderItem, activationProgress, pressProgress],
)
return (
<Animated.View exiting={GridItemExiting} pointerEvents="box-none" style={animatedItemStyle} onLayout={measureItem}>
<GestureDetector gesture={panGesture}>
<Animated.View style={animatedItemDecorationStyle}>{content}</Animated.View>
</GestureDetector>
</Animated.View>
)
}
export default memo(SortableGridItem) as typeof SortableGridItem
export const ITEM_ANIMATION_DURATION = 300
export const TIME_TO_ACTIVATE_PAN = 500
export const ACTIVATE_PAN_ANIMATION_DELAY = 250
export const AUTO_SCROLL_THRESHOLD = 50
export const OFFSET_EPS = 1
import { SharedValue, useAnimatedReaction, useSharedValue, withTiming } from 'react-native-reanimated'
import { useAutoScrollContext } from 'src/components/sortableGrid/contexts/AutoScrollContextProvider'
import { useDragContext } from 'src/components/sortableGrid/contexts/DragContextProvider'
import { useLayoutContext } from 'src/components/sortableGrid/contexts/LayoutContextProvider'
import { getColumnIndex, getRowIndex } from 'src/components/sortableGrid/internal/utils'
export function useItemPosition(key: string): {
x: SharedValue<number | null>
y: SharedValue<number | null>
} {
const { itemPositions } = useLayoutContext()
const { activeItemKey, activeItemPosition } = useDragContext()
const { scrollOffsetDiff } = useAutoScrollContext()
const x = useSharedValue<number | null>(null)
const y = useSharedValue<number | null>(null)
useAnimatedReaction(
() => ({
position: itemPositions.value[key],
isActive: activeItemKey.value === key,
}),
({ position, isActive }) => {
if (!position || isActive) {
return
}
x.value = x.value === null ? position.x : withTiming(position.x)
y.value = y.value === null ? position.y : withTiming(position.y)
},
[key],
)
useAnimatedReaction(
() => ({
position: activeItemPosition.value,
offsetDiff: scrollOffsetDiff.value,
}),
({ position, offsetDiff }) => {
if (activeItemKey.value === key) {
x.value = position.x
y.value = position.y + offsetDiff
}
},
)
return { x, y }
}
export function useItemOrderUpdater(numColumns: number): void {
const { keyToIndex, indexToKey, rowOffsets, targetContainerHeight, itemDimensions } = useLayoutContext()
const { activeItemKey, activeItemPosition } = useDragContext()
const { scrollOffsetDiff } = useAutoScrollContext()
useAnimatedReaction(
() => ({
activeKey: activeItemKey.value,
activePosition: activeItemPosition.value,
offsetDiff: scrollOffsetDiff.value,
}),
({ activeKey, activePosition, offsetDiff }) => {
if (activeKey === null) {
return
}
const dimensions = itemDimensions.value[activeKey]
if (!dimensions) {
return
}
const centerY = activePosition.y + dimensions.height / 2 + offsetDiff
const centerX = activePosition.x + dimensions.width / 2
const activeIndex = keyToIndex.value[activeKey]
const itemsCount = indexToKey.value.length
if (activeIndex === undefined) {
return
}
const rowIndex = getRowIndex(activeIndex, numColumns)
const columnIndex = getColumnIndex(activeIndex, numColumns)
// Get active item bounding box
const yOffsetAbove = rowOffsets.value[rowIndex]
if (yOffsetAbove === undefined) {
return
}
const yOffsetBelow = rowOffsets.value[rowIndex + 1]
const xOffsetLeft = columnIndex * dimensions.width
const xOffsetRight = (columnIndex + 1) * dimensions.width
// Check if the center of the active item is over the top or bottom edge of the container
let dy = 0
if (yOffsetAbove > 0 && centerY < yOffsetAbove) {
dy = -1
} else if (yOffsetBelow !== undefined && yOffsetBelow < targetContainerHeight.value && centerY > yOffsetBelow) {
dy = 1
}
// Check if the center of the active item is over the left or right edge of the container
let dx = 0
if (xOffsetLeft > 0 && centerX < xOffsetLeft) {
dx = -1
} else if (columnIndex < numColumns - 1 && activeIndex < itemsCount && centerX > xOffsetRight) {
dx = 1
}
const indexOffset = dy * numColumns + dx
// Swap the active item with the item at the new index
const newIndex = activeIndex + indexOffset
if (newIndex === activeIndex || newIndex < 0 || newIndex >= itemsCount) {
return
}
// Swap the order of the current item and the active item
if (newIndex < activeIndex) {
indexToKey.value = [
...indexToKey.value.slice(0, newIndex),
activeKey,
...indexToKey.value.slice(newIndex, activeIndex),
...indexToKey.value.slice(activeIndex + 1),
]
} else {
indexToKey.value = [
...indexToKey.value.slice(0, activeIndex),
...indexToKey.value.slice(activeIndex + 1, newIndex + 1),
activeKey,
...indexToKey.value.slice(newIndex + 1),
]
}
},
[],
)
}
import { LayoutAnimation, withTiming } from 'react-native-reanimated'
import { ITEM_ANIMATION_DURATION } from 'src/components/sortableGrid/internal/constants'
export const GridItemExiting = (): LayoutAnimation => {
'worklet'
const animations = {
opacity: withTiming(0, {
duration: ITEM_ANIMATION_DURATION,
}),
transform: [
{
scale: withTiming(0.5, {
duration: ITEM_ANIMATION_DURATION,
}),
},
],
}
const initialValues = {
opacity: 1,
transform: [{ scale: 1 }],
}
return {
initialValues,
animations,
}
}
import { useCallback, useRef } from 'react'
import { Vector } from 'src/components/sortableGrid/types'
export function useStableCallback<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
C extends (...args: Array<any>) => any,
>(callback?: C): C {
const callbackRef = useRef(callback)
callbackRef.current = callback
return useCallback(
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return
(...args: Array<any>) => callbackRef.current?.(...args),
[],
) as C
}
export const areArraysDifferent = <T>(arr1: T[], arr2: T[], areEqual = (a: T, b: T): boolean => a === b): boolean => {
'worklet'
return arr1.length !== arr2.length || arr1.some((item, index) => !areEqual(item, arr2[index] as T))
}
const hasProp = <O extends object, P extends string>(object: O, prop: P): object is O & Record<P, unknown> => {
return prop in object
}
export const defaultKeyExtractor = <I>(item: I, index: number): string => {
if (typeof item === 'string') {
return item
}
if (typeof item === 'object' && item !== null) {
if (hasProp(item, 'id')) {
return String(item.id)
}
if (hasProp(item, 'key')) {
return String(item.key)
}
}
return String(index)
}
export const getRowIndex = (index: number, numColumns: number): number => {
'worklet'
return Math.floor(index / numColumns)
}
export const getColumnIndex = (index: number, numColumns: number): number => {
'worklet'
return index % numColumns
}
export const getItemsInColumnCount = (index: number, numColumns: number, itemsCount: number): number => {
'worklet'
const columnIndex = getColumnIndex(index, numColumns)
return Math.floor(itemsCount / numColumns) + (columnIndex < itemsCount % numColumns ? 1 : 0)
}
export const getItemZIndex = (
isActive: boolean,
pressProgress: number,
position: Vector,
targetPosition?: Vector,
): number => {
'worklet'
if (isActive) {
return 3
}
if (pressProgress > 0) {
return 2
}
// If the item is being re-ordered but is not dragged
if (targetPosition && (position.x !== targetPosition.x || position.y !== targetPosition.y)) {
return 1
}
return 0
}
This diff is collapsed.
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
UNISWAP_WALLETCONNECT_URL, UNISWAP_WALLETCONNECT_URL,
} from 'src/features/deepLinking/constants' } from 'src/features/deepLinking/constants'
import { UNISWAP_WEB_HOSTNAME } from 'uniswap/src/constants/urls' import { UNISWAP_WEB_HOSTNAME } from 'uniswap/src/constants/urls'
import { isCurrencyIdValid } from 'uniswap/src/utils/currencyId'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
const UNISWAP_URL_SCHEME_WIDGET = 'uniswap://widget/' const UNISWAP_URL_SCHEME_WIDGET = 'uniswap://widget/'
...@@ -118,6 +119,9 @@ export function parseDeepLinkUrl(urlString: string): DeepLinkActionResult { ...@@ -118,6 +119,9 @@ export function parseDeepLinkUrl(urlString: string): DeepLinkActionResult {
if (!currencyId) { if (!currencyId) {
return logAndReturnError('No currencyId found', DeepLinkAction.TokenDetails, urlString, data) return logAndReturnError('No currencyId found', DeepLinkAction.TokenDetails, urlString, data)
} }
if (!isCurrencyIdValid(currencyId)) {
return logAndReturnError('Invalid currencyId found', DeepLinkAction.TokenDetails, urlString, data)
}
return { return {
action: DeepLinkAction.TokenDetails, action: DeepLinkAction.TokenDetails,
data: { ...data, currencyId }, data: { ...data, currencyId },
......
...@@ -283,6 +283,7 @@ export function* handleDeepLink(action: ReturnType<typeof openDeepLink>) { ...@@ -283,6 +283,7 @@ export function* handleDeepLink(action: ReturnType<typeof openDeepLink>) {
break break
} }
case DeepLinkAction.TokenDetails: { case DeepLinkAction.TokenDetails: {
yield* put(closeAllModals())
yield* call(handleGoToTokenDetailsDeepLink, deepLinkAction.data.currencyId) yield* call(handleGoToTokenDetailsDeepLink, deepLinkAction.data.currencyId)
break break
} }
...@@ -295,6 +296,7 @@ export function* handleDeepLink(action: ReturnType<typeof openDeepLink>) { ...@@ -295,6 +296,7 @@ export function* handleDeepLink(action: ReturnType<typeof openDeepLink>) {
} catch (error) { } catch (error) {
yield* call(logger.error, error, { yield* call(logger.error, error, {
tags: { file: 'handleDeepLinkSaga', function: 'handleDeepLink' }, tags: { file: 'handleDeepLinkSaga', function: 'handleDeepLink' },
extra: { coldStart: action.payload.coldStart, url: action.payload.url },
}) })
} }
} }
...@@ -344,6 +346,7 @@ export function* handleWalletConnectDeepLink(wcUri: string) { ...@@ -344,6 +346,7 @@ export function* handleWalletConnectDeepLink(wcUri: string) {
} catch (error) { } catch (error) {
logger.error(error, { logger.error(error, {
tags: { file: 'handleDeepLinkSaga', function: 'handleWalletConnectDeepLink' }, tags: { file: 'handleDeepLinkSaga', function: 'handleWalletConnectDeepLink' },
extra: { wcUri },
}) })
Alert.alert(i18n.t('walletConnect.error.general.title'), i18n.t('walletConnect.error.general.message')) Alert.alert(i18n.t('walletConnect.error.general.title'), i18n.t('walletConnect.error.general.message'))
} }
......
This diff is collapsed.
...@@ -2,7 +2,7 @@ import { PayloadAction } from '@reduxjs/toolkit' ...@@ -2,7 +2,7 @@ import { PayloadAction } from '@reduxjs/toolkit'
import { closeModal, CloseModalParams, openModal, OpenModalParams } from 'src/features/modals/modalSlice' import { closeModal, CloseModalParams, openModal, OpenModalParams } from 'src/features/modals/modalSlice'
import { takeEvery } from 'typed-redux-saga' import { takeEvery } from 'typed-redux-saga'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { setAttributesToDatadog } from 'utilities/src/logger/Datadog' import { setAttributesToDatadog } from 'utilities/src/logger/datadog/Datadog'
export function* modalWatcher() { export function* modalWatcher() {
yield* takeEvery(openModal, handleOpenModalAction) yield* takeEvery(openModal, handleOpenModalAction)
......
This diff is collapsed.
import { FiatOnRampCurrency } from 'uniswap/src/features/fiatOnRamp/types' import { FiatOnRampCurrency } from 'uniswap/src/features/fiatOnRamp/types'
export type FiatOnRampModalState = { prefilledCurrency?: FiatOnRampCurrency } export type FiatOnRampModalState = { prefilledCurrency?: FiatOnRampCurrency; isOfframp?: boolean }
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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