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()}>
<ExtensionStatsigProvider appName={DatadogAppNameTag.Onboarding}> <BaseAppContainer appName={DatadogAppNameTag.Onboarding}>
<I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}>
<ErrorBoundary>
<GraphqlProvider>
<BlankUrlProvider>
<LocalizationContextProvider>
<UnitagUpdaterContextProvider> <UnitagUpdaterContextProvider>
<PrimaryAppInstanceDebuggerLazy /> <PrimaryAppInstanceDebuggerLazy />
<RouterProvider router={router} /> <RouterProvider router={router} />
</UnitagUpdaterContextProvider> </UnitagUpdaterContextProvider>
</LocalizationContextProvider> </BaseAppContainer>
</BlankUrlProvider>
</GraphqlProvider>
</ErrorBoundary>
</SharedWalletProvider>
</I18nextProvider>
</ExtensionStatsigProvider>
</PersistGate> </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()}>
<ExtensionStatsigProvider appName={DatadogAppNameTag.Popup}>
<I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}>
<ErrorBoundary>
<GraphqlProvider>
<BlankUrlProvider>
<LocalizationContextProvider>
<UnitagUpdaterContextProvider>
<TraceUserProperties />
<DappContextProvider>
<RouterProvider router={router} /> <RouterProvider router={router} />
</DappContextProvider> </BaseAppContainer>
</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()}>
<ExtensionStatsigProvider appName={DatadogAppNameTag.Sidebar}> <BaseAppContainer appName={DatadogAppNameTag.Sidebar}>
<I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}>
<ErrorBoundary>
<GraphqlProvider>
<BlankUrlProvider>
<LocalizationContextProvider>
<UnitagUpdaterContextProvider> <UnitagUpdaterContextProvider>
<TraceUserProperties />
<DappContextProvider> <DappContextProvider>
<PrimaryAppInstanceDebuggerLazy /> <PrimaryAppInstanceDebuggerLazy />
<RouterProvider router={router} /> <RouterProvider router={router} />
</DappContextProvider> </DappContextProvider>
</UnitagUpdaterContextProvider> </UnitagUpdaterContextProvider>
</LocalizationContextProvider> </BaseAppContainer>
</BlankUrlProvider>
</GraphqlProvider>
</ErrorBoundary>
</SharedWalletProvider>
</I18nextProvider>
</ExtensionStatsigProvider>
</PersistGate> </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()}>
<ExtensionStatsigProvider appName={DatadogAppNameTag.UnitagClaim}>
<I18nextProvider i18n={i18n}>
<SharedWalletProvider reduxStore={getReduxStore()}>
<ErrorBoundary>
<GraphqlProvider>
<BlankUrlProvider>
<LocalizationContextProvider>
<UnitagUpdaterContextProvider> <UnitagUpdaterContextProvider>
<TraceUserProperties />
<RouterProvider router={router} /> <RouterProvider router={router} />
</UnitagUpdaterContextProvider> </UnitagUpdaterContextProvider>
</LocalizationContextProvider> </BaseAppContainer>
</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,19 +119,31 @@ export function WebNavigation(): JSX.Element { ...@@ -96,19 +119,31 @@ 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}>
<MotionFlex
key={pathname} key={pathname}
animation={[ variants={animationVariant}
isVertical(towards) ? 'quicker' : '100ms', custom={towards}
{ initial="initial"
opacity: { animate="animate"
overshootClamping: true, exit="exit"
}, onAnimationComplete={() => {
}, setIsTransitioning(false)
]} }}
> >
<Flex fill grow overflow="visible"> <Flex fill grow overflow="visible">
<TestnetModeBanner /> <TestnetModeBanner />
...@@ -122,10 +157,11 @@ export function WebNavigation(): JSX.Element { ...@@ -122,10 +157,11 @@ export function WebNavigation(): JSX.Element {
<LoggedOut /> <LoggedOut />
)} )}
</Flex> </Flex>
</AnimatedPane> </MotionFlex>
</AnimatePresence> </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
...@@ -10,7 +10,7 @@ else ...@@ -10,7 +10,7 @@ else
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')) 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'))
} 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>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<Flex row gap="$spacing8" px="$spacing16"> <Flex row gap="$spacing8" px="$spacing16">
<LinkButton <FlatList
Icon={getBlockExplorerIcon(chainId)} horizontal
buttonType={LinkButtonType.Link} showsHorizontalScrollIndicator={false}
element={ElementName.TokenLinkEtherscan} data={links}
label={explorerName} renderItem={({ item }) => <LinkButton {...item} />}
testID={TestID.TokenLinkEtherscan} keyExtractor={(item) => item.testID}
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> </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,14 +88,11 @@ function FavoriteTokenCard({ ...@@ -98,14 +88,11 @@ 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}
...@@ -118,6 +105,7 @@ function FavoriteTokenCard({ ...@@ -118,6 +105,7 @@ function FavoriteTokenCard({
backgroundColor={isDarkMode ? '$surface2' : '$surface1'} backgroundColor={isDarkMode ? '$surface2' : '$surface1'}
borderColor={opacify(0.05, colors.surface3.val)} borderColor={opacify(0.05, colors.surface3.val)}
borderRadius="$rounded16" borderRadius="$rounded16"
overflow={isIOS ? 'hidden' : 'visible'}
borderWidth={isDarkMode ? '$none' : '$spacing1'} borderWidth={isDarkMode ? '$none' : '$spacing1'}
m="$spacing4" m="$spacing4"
testID={`token-box-${token?.symbol}`} testID={`token-box-${token?.symbol}`}
...@@ -169,7 +157,6 @@ function FavoriteTokenCard({ ...@@ -169,7 +157,6 @@ function FavoriteTokenCard({
</Flex> </Flex>
</AnimatedTouchableArea> </AnimatedTouchableArea>
</ContextMenu> </ContextMenu>
</AnimatedFlex>
) )
} }
......
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,33 +36,23 @@ export function FavoriteTokensGrid({ showLoading, ...rest }: FavoriteTokensGridP ...@@ -35,33 +36,23 @@ 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>
<AnimatedFlex entering={FadeIn}>
<FavoriteHeaderRow <FavoriteHeaderRow
disabled={showLoading} disabled={showLoading}
editingTitle={t('explore.tokens.favorite.title.edit')} editingTitle={t('explore.tokens.favorite.title.edit')}
...@@ -72,24 +63,20 @@ export function FavoriteTokensGrid({ showLoading, ...rest }: FavoriteTokensGridP ...@@ -72,24 +63,20 @@ export function FavoriteTokensGrid({ showLoading, ...rest }: FavoriteTokensGridP
{showLoading ? ( {showLoading ? (
<FavoriteTokensGridLoader /> <FavoriteTokensGridLoader />
) : ( ) : (
<SortableGrid <Sortable.Grid
{...rest} {...rest}
activeItemOpacity={1} animateHeight
animateContainerHeight={false} scrollableRef={listRef}
data={favoriteCurrencyIds} data={favoriteCurrencyIds}
editable={isEditing} sortEnabled={isEditing}
numColumns={NUM_COLUMNS} autoScrollActivationOffset={[75, 100]}
columns={NUM_COLUMNS}
renderItem={renderItem} renderItem={renderItem}
onChange={handleOrderChange} onDragEnd={handleDragEnd}
onDragStart={(): void => {
isTokenDragged.value = true
}}
onDrop={(): void => {
isTokenDragged.value = false
}}
/> />
)} )}
</AnimatedFlex> </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,12 +49,9 @@ function FavoriteWalletCard({ ...@@ -60,12 +49,9 @@ 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} disabled={isEditing}
...@@ -84,6 +70,7 @@ function FavoriteWalletCard({ ...@@ -84,6 +70,7 @@ function FavoriteWalletCard({
{...rest} {...rest}
> >
<TouchableArea <TouchableArea
overflow={isIOS ? 'hidden' : 'visible'}
activeOpacity={isEditing ? 1 : undefined} activeOpacity={isEditing ? 1 : undefined}
backgroundColor={isDarkMode ? '$surface2' : '$surface1'} backgroundColor={isDarkMode ? '$surface2' : '$surface1'}
borderColor={opacify(0.05, colors.surface3.val)} borderColor={opacify(0.05, colors.surface3.val)}
...@@ -116,7 +103,6 @@ function FavoriteWalletCard({ ...@@ -116,7 +103,6 @@ function FavoriteWalletCard({
</Flex> </Flex>
</TouchableArea> </TouchableArea>
</ContextMenu> </ContextMenu>
</AnimatedFlex>
) )
} }
......
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,33 +37,23 @@ export function FavoriteWalletsGrid({ showLoading, ...rest }: FavoriteWalletsGri ...@@ -36,33 +37,23 @@ 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>
<AnimatedFlex entering={FadeIn}>
<FavoriteHeaderRow <FavoriteHeaderRow
editingTitle={t('explore.wallets.favorite.title.edit')} editingTitle={t('explore.wallets.favorite.title.edit')}
isEditing={isEditing} isEditing={isEditing}
...@@ -73,24 +64,20 @@ export function FavoriteWalletsGrid({ showLoading, ...rest }: FavoriteWalletsGri ...@@ -73,24 +64,20 @@ export function FavoriteWalletsGrid({ showLoading, ...rest }: FavoriteWalletsGri
{showLoading ? ( {showLoading ? (
<FavoriteWalletsGridLoader /> <FavoriteWalletsGridLoader />
) : ( ) : (
<SortableGrid <Sortable.Grid
{...rest} {...rest}
activeItemOpacity={1} animateHeight
animateContainerHeight={false} scrollableRef={listRef}
autoScrollActivationOffset={[75, 100]}
data={watchedWalletsList} data={watchedWalletsList}
editable={isEditing} sortEnabled={isEditing}
numColumns={NUM_COLUMNS} columns={NUM_COLUMNS}
renderItem={renderItem} renderItem={renderItem}
onChange={handleOrderChange} onDragEnd={handleDragEnd}
onDragStart={(): void => {
isTokenDragged.value = true
}}
onDrop={(): void => {
isTokenDragged.value = false
}}
/> />
)} )}
</AnimatedFlex> </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' }}>
......
...@@ -2,19 +2,6 @@ ...@@ -2,19 +2,6 @@
exports[`FavoriteTokenCard renders without error 1`] = ` exports[`FavoriteTokenCard renders without error 1`] = `
<View <View
collapsable={false}
style={
{
"borderBottomLeftRadius": 16,
"borderBottomRightRadius": 16,
"borderTopLeftRadius": 16,
"borderTopRightRadius": 16,
"flexDirection": "column",
"opacity": 1,
}
}
>
<View
actions={ actions={
[ [
{ {
...@@ -46,7 +33,7 @@ exports[`FavoriteTokenCard renders without error 1`] = ` ...@@ -46,7 +33,7 @@ exports[`FavoriteTokenCard renders without error 1`] = `
"borderRadius": 16, "borderRadius": 16,
} }
} }
> >
<View <View
collapsable={false} collapsable={false}
hitSlop={ hitSlop={
...@@ -93,6 +80,7 @@ exports[`FavoriteTokenCard renders without error 1`] = ` ...@@ -93,6 +80,7 @@ exports[`FavoriteTokenCard renders without error 1`] = `
"marginRight": 4, "marginRight": 4,
"marginTop": 4, "marginTop": 4,
"opacity": 1, "opacity": 1,
"overflow": "hidden",
"shadowColor": "rgb(0,0,0)", "shadowColor": "rgb(0,0,0)",
"shadowOffset": { "shadowOffset": {
"height": 1, "height": 1,
...@@ -382,6 +370,5 @@ exports[`FavoriteTokenCard renders without error 1`] = ` ...@@ -382,6 +370,5 @@ exports[`FavoriteTokenCard renders without error 1`] = `
</View> </View>
</View> </View>
</View> </View>
</View>
</View> </View>
`; `;
...@@ -2,15 +2,6 @@ ...@@ -2,15 +2,6 @@
exports[`FavoriteWalletCard renders without error 1`] = ` exports[`FavoriteWalletCard renders without error 1`] = `
<View <View
collapsable={false}
style={
{
"flexDirection": "column",
"opacity": 1,
}
}
>
<View
actions={ actions={
[ [
{ {
...@@ -30,7 +21,7 @@ exports[`FavoriteWalletCard renders without error 1`] = ` ...@@ -30,7 +21,7 @@ exports[`FavoriteWalletCard renders without error 1`] = `
"borderRadius": 16, "borderRadius": 16,
} }
} }
> >
<View <View
disabled={false} disabled={false}
hitSlop={ hitSlop={
...@@ -77,6 +68,7 @@ exports[`FavoriteWalletCard renders without error 1`] = ` ...@@ -77,6 +68,7 @@ exports[`FavoriteWalletCard renders without error 1`] = `
"marginRight": 4, "marginRight": 4,
"marginTop": 4, "marginTop": 4,
"opacity": 1, "opacity": 1,
"overflow": "hidden",
"shadowColor": "rgb(0,0,0)", "shadowColor": "rgb(0,0,0)",
"shadowOffset": { "shadowOffset": {
"height": 1, "height": 1,
...@@ -272,6 +264,5 @@ exports[`FavoriteWalletCard renders without error 1`] = ` ...@@ -272,6 +264,5 @@ exports[`FavoriteWalletCard renders without error 1`] = `
</View> </View>
</View> </View>
</View> </View>
</View>
</View> </View>
`; `;
...@@ -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
}
import { PropsWithChildren } from 'react'
import { FlatList, ScrollView, View } from 'react-native'
import { SharedValue } from 'react-native-reanimated'
export type Vector = {
x: number
y: number
}
export type Dimensions = {
width: number
height: number
}
export type AutoScrollProps = {
scrollableRef: React.RefObject<FlatList | ScrollView>
visibleHeight: SharedValue<number>
scrollY: SharedValue<number>
// The parent container inside the scrollable that wraps the grid
// (e.g. when the grid is rendered inside the FlatList header)
// if not provided, we assume that the grid is the first child in
// the scrollable container
containerRef?: React.RefObject<View>
}
export type ActiveItemDecorationSettings = {
activeItemScale: number
activeItemOpacity: number
activeItemShadowOpacity: number
}
export type SortableGridChangeEvent<I> = {
data: I[]
fromIndex: number
toIndex: number
}
export type SortableGridDragStartEvent<I> = {
item: I
index: number
}
export type SortableGridDropEvent<I> = {
item: I
index: number
}
export type SortableGridRenderItemInfo<I> = {
item: I
pressProgress: SharedValue<number>
dragActivationProgress: SharedValue<number>
}
export type SortableGridRenderItem<I> = (info: SortableGridRenderItemInfo<I>) => JSX.Element
export type DragContextType = {
// DRAG SETTINGS
editable: boolean
// ACTIVE ITEM
activeItemKey: SharedValue<string | null>
activeItemDropped: SharedValue<boolean>
// DRAGA ACTIVATION
activationProgress: SharedValue<number>
activeItemPosition: SharedValue<Vector>
// ACTIVE ITEM DECORATION
activeItemScale: SharedValue<number>
activeItemOpacity: SharedValue<number>
activeItemShadowOpacity: SharedValue<number>
}
export type DragContextProviderProps<I> = PropsWithChildren<
Partial<ActiveItemDecorationSettings> & {
data: I[]
itemKeys: string[]
editable?: boolean
onChange?: (e: SortableGridChangeEvent<I>) => void
onDragStart?: (e: SortableGridDragStartEvent<I>) => void
onDrop?: (e: SortableGridDropEvent<I>) => void
keyExtractor: (item: I, index: number) => string
}
>
export type AutoScrollContextType = {
// REFS
gridContainerRef: React.RefObject<View>
// MEASUREMENTS
containerStartOffset: SharedValue<number>
containerEndOffset: SharedValue<number>
scrollOffsetDiff: SharedValue<number>
startScrollOffset: SharedValue<number>
scrollY: SharedValue<number>
}
...@@ -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'))
} }
......
...@@ -70,7 +70,6 @@ export function FiatOnRampProvider({ children }: { children: React.ReactNode }): ...@@ -70,7 +70,6 @@ export function FiatOnRampProvider({ children }: { children: React.ReactNode }):
const [countryCode, setCountryCode] = useState<string>(getCountry()) const [countryCode, setCountryCode] = useState<string>(getCountry())
const [countryState, setCountryState] = useState<string | undefined>() const [countryState, setCountryState] = useState<string | undefined>()
const [baseCurrencyInfo, setBaseCurrencyInfo] = useState<FiatCurrencyInfo>() const [baseCurrencyInfo, setBaseCurrencyInfo] = useState<FiatCurrencyInfo>()
const [isOffRamp, setIsOffRamp] = useState<boolean>(false)
const [isTokenInputMode, setIsTokenInputMode] = useState<boolean>(false) const [isTokenInputMode, setIsTokenInputMode] = useState<boolean>(false)
const [fiatAmount, setFiatAmount] = useState<number | undefined>() const [fiatAmount, setFiatAmount] = useState<number | undefined>()
const [tokenAmount, setTokenAmount] = useState<number | undefined>() const [tokenAmount, setTokenAmount] = useState<number | undefined>()
...@@ -90,13 +89,17 @@ export function FiatOnRampProvider({ children }: { children: React.ReactNode }): ...@@ -90,13 +89,17 @@ export function FiatOnRampProvider({ children }: { children: React.ReactNode }):
[ethCurrencyInfo], [ethCurrencyInfo],
) )
const [quoteCurrency, setQuoteCurrency] = useState<FiatOnRampCurrency>(prefilledCurrency ?? defaultCurrency) const [quoteCurrency, setQuoteCurrency] = useState<FiatOnRampCurrency>(prefilledCurrency ?? defaultCurrency)
const [isOffRamp, setIsOffRamp] = useState<boolean>(initialModalState?.isOfframp ?? false)
useEffect(() => { useEffect(() => {
if (prefilledCurrency) {
return
}
// Addresses a race condition where the quoteCurrency could be set before ethCurrencyInfo is loaded // Addresses a race condition where the quoteCurrency could be set before ethCurrencyInfo is loaded
if (ethCurrencyInfo) { if (ethCurrencyInfo) {
setQuoteCurrency(defaultCurrency) setQuoteCurrency(defaultCurrency)
} }
}, [ethCurrencyInfo, defaultCurrency]) }, [ethCurrencyInfo, defaultCurrency, prefilledCurrency])
return ( return (
<FiatOnRampContext.Provider <FiatOnRampContext.Provider
......
import { BlurView } from 'expo-blur' import { BlurView } from 'expo-blur'
import React, { memo } from 'react' import React, { memo } from 'react'
import { TouchableWithoutFeedback } from 'react-native'
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated' import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
import { SplashScreen } from 'src/features/appLoading/SplashScreen' import { SplashScreen } from 'src/features/appLoading/SplashScreen'
import { useBiometricPrompt } from 'src/features/biometricsSettings/hooks' import { useBiometricPrompt } from 'src/features/biometricsSettings/hooks'
import { useLockScreenState } from 'src/features/lockScreen/useLockScreenState' import { useLockScreenState } from 'src/features/lockScreen/useLockScreenState'
import { flexStyles, useIsDarkMode } from 'ui/src' import { TouchableArea, flexStyles, useIsDarkMode } from 'ui/src'
import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions'
import { zIndexes } from 'ui/src/theme' import { zIndexes } from 'ui/src/theme'
import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { FeatureFlags } from 'uniswap/src/features/gating/flags'
...@@ -24,9 +23,9 @@ export const LockScreenModal = memo(function LockScreenModal(): JSX.Element | nu ...@@ -24,9 +23,9 @@ export const LockScreenModal = memo(function LockScreenModal(): JSX.Element | nu
// the lock screen on error, hence we fallback to the global error boundary // the lock screen on error, hence we fallback to the global error boundary
return ( return (
<FullScreenFader> <FullScreenFader>
<TouchableWithoutFeedback style={flexStyles.fill} onPress={(): Promise<void> => trigger()}> <TouchableArea activeOpacity={1} style={flexStyles.fill} onPress={(): Promise<void> => trigger()}>
<LockScreenModalContent /> <LockScreenModalContent />
</TouchableWithoutFeedback> </TouchableArea>
</FullScreenFader> </FullScreenFader>
) )
}) })
......
import { useDispatch } from 'react-redux'
import { setPreventLock } from 'src/features/lockScreen/lockScreenSlice'
import { waitFrame } from 'utilities/src/react/delayUtils'
import { useEvent } from 'utilities/src/react/hooks'
/**
* Custom hook to prevent the app from entering a background state when calling a function.
*
* There are times when we call a function and sometimes the app will enter a background state, or on
* android the app will enter an "inactive" state. This `preventLock` function will temporarily
* prevent the app from locking if enabled (biometric auth app access)
*/
export function usePreventLock(): {
preventLock: <T>(operation: () => Promise<T>) => Promise<T>
} {
const dispatch = useDispatch()
const preventLock = useEvent(async <T>(operation: () => Promise<T>): Promise<T> => {
dispatch(setPreventLock(true))
try {
await waitFrame()
return await operation()
} finally {
await waitFrame()
// always reset preventLock to false
dispatch(setPreventLock(false))
}
})
return { preventLock }
}
...@@ -13,6 +13,7 @@ import { ...@@ -13,6 +13,7 @@ import {
LockScreenVisibility, LockScreenVisibility,
selectIsLockScreenVisible, selectIsLockScreenVisible,
selectLockScreenOnBlur, selectLockScreenOnBlur,
selectPreventLock,
setLockScreenVisibility, setLockScreenVisibility,
} from 'src/features/lockScreen/lockScreenSlice' } from 'src/features/lockScreen/lockScreenSlice'
import { call, put, select, takeEvery, takeLatest } from 'typed-redux-saga' import { call, put, select, takeEvery, takeLatest } from 'typed-redux-saga'
...@@ -66,6 +67,10 @@ function* toBackgroundTransition(): SagaIterator { ...@@ -66,6 +67,10 @@ function* toBackgroundTransition(): SagaIterator {
} }
function* invalidateAuthentication(): SagaIterator { function* invalidateAuthentication(): SagaIterator {
const preventLock = yield* select(selectPreventLock)
if (preventLock) {
return
}
if (yield* select(selectRequiredForAppAccess)) { if (yield* select(selectRequiredForAppAccess)) {
yield* put(setAuthenticationStatus(BiometricAuthenticationStatus.Invalid)) yield* put(setAuthenticationStatus(BiometricAuthenticationStatus.Invalid))
} }
...@@ -90,6 +95,10 @@ function* shouldDismissLockScreen(): SagaIterator<boolean> { ...@@ -90,6 +95,10 @@ function* shouldDismissLockScreen(): SagaIterator<boolean> {
} }
function* shouldPresentLockScreen(): SagaIterator<boolean> { function* shouldPresentLockScreen(): SagaIterator<boolean> {
const preventLock = yield* select(selectPreventLock)
if (preventLock) {
return false
}
const requiredForAppAccess = yield* select(selectRequiredForAppAccess) const requiredForAppAccess = yield* select(selectRequiredForAppAccess)
const lockScreenOnBlur = yield* select(selectLockScreenOnBlur) const lockScreenOnBlur = yield* select(selectLockScreenOnBlur)
return requiredForAppAccess || lockScreenOnBlur return requiredForAppAccess || lockScreenOnBlur
......
...@@ -13,11 +13,13 @@ export enum LockScreenVisibility { ...@@ -13,11 +13,13 @@ export enum LockScreenVisibility {
export interface LockScreenState { export interface LockScreenState {
visibility: LockScreenVisibility visibility: LockScreenVisibility
onBlur: boolean onBlur: boolean
preventLock: boolean
} }
const initialState: LockScreenState = { const initialState: LockScreenState = {
visibility: LockScreenVisibility.Init, visibility: LockScreenVisibility.Init,
onBlur: false, onBlur: false,
preventLock: false,
} }
export const lockScreenSlice = createSlice({ export const lockScreenSlice = createSlice({
...@@ -33,10 +35,13 @@ export const lockScreenSlice = createSlice({ ...@@ -33,10 +35,13 @@ export const lockScreenSlice = createSlice({
setLockScreenOnBlur: (state, action: PayloadAction<boolean>) => { setLockScreenOnBlur: (state, action: PayloadAction<boolean>) => {
state.onBlur = action.payload state.onBlur = action.payload
}, },
setPreventLock: (state, action: PayloadAction<boolean>) => {
state.preventLock = action.payload
},
}, },
}) })
export const { setLockScreenVisibility, setLockScreenOnBlur } = lockScreenSlice.actions export const { setLockScreenVisibility, setLockScreenOnBlur, setPreventLock } = lockScreenSlice.actions
export const lockScreenReducer = lockScreenSlice.reducer export const lockScreenReducer = lockScreenSlice.reducer
//------------------------------ //------------------------------
...@@ -53,3 +58,5 @@ export const selectIsLockScreenVisible = (state: { lockScreen: LockScreenState } ...@@ -53,3 +58,5 @@ export const selectIsLockScreenVisible = (state: { lockScreen: LockScreenState }
state.lockScreen.visibility === LockScreenVisibility.Visible state.lockScreen.visibility === LockScreenVisibility.Visible
export const selectLockScreenOnBlur = (state: { lockScreen: LockScreenState }): boolean => state.lockScreen.onBlur export const selectLockScreenOnBlur = (state: { lockScreen: LockScreenState }): boolean => state.lockScreen.onBlur
export const selectPreventLock = (state: { lockScreen: LockScreenState }): boolean => state.lockScreen.preventLock
...@@ -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)
......
import { useMutation } from '@tanstack/react-query' import { useMutation } from '@tanstack/react-query'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { promptPushPermission } from 'src/features/notifications/Onesignal'
import { NotifSettingType } from 'src/features/notifications/constants' import { NotifSettingType } from 'src/features/notifications/constants'
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 { selectAllPushNotificationSettings } from 'src/features/notifications/selectors' import { selectAllPushNotificationSettings } from 'src/features/notifications/selectors'
import { updateNotifSettings } from 'src/features/notifications/slice' import { updateNotifSettings } from 'src/features/notifications/slice'
import { showNotificationSettingsAlert } from 'src/screens/Onboarding/NotificationsSetupScreen' import { showNotificationSettingsAlert } from 'src/screens/Onboarding/NotificationsSetupScreen'
import { waitFrame } from 'utilities/src/react/delayUtils'
import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga' import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga'
import { useSelectAccountNotificationSetting } from 'wallet/src/features/wallet/hooks' import { useSelectAccountNotificationSetting } from 'wallet/src/features/wallet/hooks'
// Wait for next frame to ensure UI updates without flashing
// https://corbt.com/posts/2015/12/22/breaking-up-heavy-processing-in-react-native.html
const waitFrame = async (): Promise<void> => {
await new Promise(requestAnimationFrame)
}
enum NotificationError { enum NotificationError {
OsPermissionDenied = 'OS_PERMISSION_DENIED', OsPermissionDenied = 'OS_PERMISSION_DENIED',
} }
...@@ -141,7 +136,7 @@ function useBaseNotificationToggle({ ...@@ -141,7 +136,7 @@ function useBaseNotificationToggle({
// Optimistic UI state // Optimistic UI state
const [optimisticEnabled, setOptimisticEnabled] = useState<boolean>(isEnabled) const [optimisticEnabled, setOptimisticEnabled] = useState<boolean>(isEnabled)
const promptPushPermission = usePromptPushPermission()
// Helper to handle OS permission request and state update // Helper to handle OS permission request and state update
const requestOSPermissions = useCallback(async (): Promise<true> => { const requestOSPermissions = useCallback(async (): Promise<true> => {
const granted = await promptPushPermission() const granted = await promptPushPermission()
...@@ -153,7 +148,7 @@ function useBaseNotificationToggle({ ...@@ -153,7 +148,7 @@ function useBaseNotificationToggle({
throw new Error(NotificationError.OsPermissionDenied) throw new Error(NotificationError.OsPermissionDenied)
} }
return true return true
}, [onToggle]) }, [onToggle, promptPushPermission])
// Reset optimistic state if real state changes // Reset optimistic state if real state changes
useEffect(() => { useEffect(() => {
......
import { usePreventLock } from 'src/features/lockScreen/hooks/usePreventLock'
import { promptPushPermission } from 'src/features/notifications/Onesignal'
import { useEvent } from 'utilities/src/react/hooks'
/**
* Custom hook to handle push notification 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.
*
* @see https://github.com/OneSignal/react-native-onesignal/issues/1658#issuecomment-1974849646
*/
export function usePromptPushPermission(): () => Promise<boolean> {
const { preventLock } = usePreventLock()
return useEvent(async (): Promise<boolean> => {
return preventLock(promptPushPermission)
})
}
import React, { Dispatch, SetStateAction, useCallback } from 'react' import React, { Dispatch, SetStateAction, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { DeprecatedButton } from 'ui/src' import { DeprecatedButton } from 'ui/src'
...@@ -8,6 +8,7 @@ import { selectHasDismissedLowNetworkTokenWarning } from 'uniswap/src/features/b ...@@ -8,6 +8,7 @@ import { selectHasDismissedLowNetworkTokenWarning } from 'uniswap/src/features/b
import { WalletEventName } from 'uniswap/src/features/telemetry/constants' import { WalletEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency'
import { ValueType, getCurrencyAmount } from 'uniswap/src/features/tokens/getCurrencyAmount'
import { useTransactionModalContext } from 'uniswap/src/features/transactions/TransactionModal/TransactionModalContext' import { useTransactionModalContext } from 'uniswap/src/features/transactions/TransactionModal/TransactionModalContext'
import { useIsBlocked } from 'uniswap/src/features/trm/hooks' import { useIsBlocked } from 'uniswap/src/features/trm/hooks'
import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { TestID } from 'uniswap/src/test/fixtures/testIDs'
...@@ -34,9 +35,29 @@ export function SendFormButton({ ...@@ -34,9 +35,29 @@ export function SendFormButton({
recipient, recipient,
isMax, isMax,
derivedSendInfo: { chainId, currencyInInfo }, derivedSendInfo: { chainId, currencyInInfo },
exactAmountToken,
exactAmountFiat,
} = useSendContext() } = useSendContext()
const { walletNeedsRestore } = useTransactionModalContext() const { walletNeedsRestore } = useTransactionModalContext()
const hasValueGreaterThanZero = useMemo(() => {
if (exactAmountToken) {
return getCurrencyAmount({
value: exactAmountToken,
valueType: ValueType.Exact,
currency: currencyInInfo?.currency,
})?.greaterThan(0)
}
if (exactAmountFiat) {
return getCurrencyAmount({
value: exactAmountFiat,
valueType: ValueType.Exact,
currency: currencyInInfo?.currency,
})?.greaterThan(0)
}
return false
}, [exactAmountToken, exactAmountFiat, currencyInInfo?.currency])
const isViewOnlyWallet = account.type === AccountType.Readonly const isViewOnlyWallet = account.type === AccountType.Readonly
const { isBlocked: isActiveBlocked, isBlockedLoading: isActiveBlockedLoading } = useIsBlockedActiveAddress() const { isBlocked: isActiveBlocked, isBlockedLoading: isActiveBlockedLoading } = useIsBlockedActiveAddress()
...@@ -46,7 +67,8 @@ export function SendFormButton({ ...@@ -46,7 +67,8 @@ export function SendFormButton({
const insufficientGasFunds = warnings.warnings.some((warning) => warning.type === WarningLabel.InsufficientGasFunds) const insufficientGasFunds = warnings.warnings.some((warning) => warning.type === WarningLabel.InsufficientGasFunds)
const actionButtonDisabled = !!warnings.blockingWarning || isBlocked || isBlockedLoading || walletNeedsRestore const actionButtonDisabled =
!!warnings.blockingWarning || isBlocked || isBlockedLoading || walletNeedsRestore || !hasValueGreaterThanZero
const onPressReview = useCallback(() => { const onPressReview = useCallback(() => {
if (isViewOnlyWallet) { if (isViewOnlyWallet) {
......
...@@ -35,10 +35,14 @@ export function EditUnitagProfileScreen({ route }: UnitagStackScreenProp<UnitagS ...@@ -35,10 +35,14 @@ export function EditUnitagProfileScreen({ route }: UnitagStackScreenProp<UnitagS
navigation.goBack() navigation.goBack()
} }
const onClose = (): void => { const onCloseChangeModal = (): void => {
setShowChangeUnitagModal(false) setShowChangeUnitagModal(false)
} }
const onCloseDeleteModal = (): void => {
setShowDeleteUnitagModal(false)
}
const menuActions = useMemo(() => { const menuActions = useMemo(() => {
return [ return [
{ title: t('unitags.profile.action.edit'), systemIcon: 'pencil' }, { title: t('unitags.profile.action.edit'), systemIcon: 'pencil' },
...@@ -93,7 +97,7 @@ export function EditUnitagProfileScreen({ route }: UnitagStackScreenProp<UnitagS ...@@ -93,7 +97,7 @@ export function EditUnitagProfileScreen({ route }: UnitagStackScreenProp<UnitagS
<EditUnitagProfileContent address={address} unitag={unitag} entryPoint={entryPoint} onNavigate={onNavigate} /> <EditUnitagProfileContent address={address} unitag={unitag} entryPoint={entryPoint} onNavigate={onNavigate} />
</KeyboardAvoidingView> </KeyboardAvoidingView>
{showDeleteUnitagModal && ( {showDeleteUnitagModal && (
<DeleteUnitagModal address={address} unitag={unitag} onSuccess={onBack} onClose={onClose} /> <DeleteUnitagModal address={address} unitag={unitag} onSuccess={onBack} onClose={onCloseDeleteModal} />
)} )}
{showChangeUnitagModal && ( {showChangeUnitagModal && (
<ChangeUnitagModal <ChangeUnitagModal
...@@ -101,7 +105,7 @@ export function EditUnitagProfileScreen({ route }: UnitagStackScreenProp<UnitagS ...@@ -101,7 +105,7 @@ export function EditUnitagProfileScreen({ route }: UnitagStackScreenProp<UnitagS
unitag={unitag} unitag={unitag}
keyboardHeight={keyboardHeight} keyboardHeight={keyboardHeight}
onSuccess={onBack} onSuccess={onBack}
onClose={onClose} onClose={onCloseChangeModal}
/> />
)} )}
</Screen> </Screen>
......
...@@ -31,12 +31,19 @@ import { pushNotification } from 'uniswap/src/features/notifications/slice' ...@@ -31,12 +31,19 @@ import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types' import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import i18n from 'uniswap/src/i18n' import i18n from 'uniswap/src/i18n'
import { EthEvent, EthMethod, WalletConnectEvent } from 'uniswap/src/types/walletConnect' import { EthEvent, EthMethod, WalletConnectEvent } from 'uniswap/src/types/walletConnect'
import { isBetaEnv, 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 { selectAccounts, selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors' import { selectAccounts, selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors'
export let wcWeb3Wallet: IWalletKit export let wcWeb3Wallet: IWalletKit
const PROJECT_ID = {
dev: config.walletConnectProjectIdDev,
beta: config.walletConnectProjectIdBeta,
default: config.walletConnectProjectId,
}
let wcWeb3WalletReadyResolve: () => void let wcWeb3WalletReadyResolve: () => void
let wcWeb3WalletReadyReject: (e: unknown) => void let wcWeb3WalletReadyReject: (e: unknown) => void
const wcWeb3WalletReady = new Promise<void>((resolve, reject) => { const wcWeb3WalletReady = new Promise<void>((resolve, reject) => {
...@@ -45,10 +52,20 @@ const wcWeb3WalletReady = new Promise<void>((resolve, reject) => { ...@@ -45,10 +52,20 @@ const wcWeb3WalletReady = new Promise<void>((resolve, reject) => {
}) })
export const waitForWcWeb3WalletIsReady = () => wcWeb3WalletReady export const waitForWcWeb3WalletIsReady = () => wcWeb3WalletReady
function getProjectId() {
if (isDevEnv()) {
return PROJECT_ID.dev
}
if (isBetaEnv()) {
return PROJECT_ID.beta
}
return PROJECT_ID.default
}
export async function initializeWeb3Wallet(): Promise<void> { export async function initializeWeb3Wallet(): Promise<void> {
try { try {
const wcCore = new Core({ const wcCore = new Core({
projectId: config.walletConnectProjectId, projectId: getProjectId(),
}) })
wcWeb3Wallet = await WalletKit.init({ wcWeb3Wallet = await WalletKit.init({
......
...@@ -6,7 +6,6 @@ import { SignRequest, TransactionRequest } from 'src/features/walletConnect/wall ...@@ -6,7 +6,6 @@ import { SignRequest, TransactionRequest } from 'src/features/walletConnect/wall
import { UniverseChainId } from 'uniswap/src/features/chains/types' import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { toSupportedChainId } from 'uniswap/src/features/chains/utils' import { toSupportedChainId } from 'uniswap/src/features/chains/utils'
import { EthMethod, EthSignMethod } from 'uniswap/src/types/walletConnect' import { EthMethod, EthSignMethod } from 'uniswap/src/types/walletConnect'
import { logger } from 'utilities/src/logger/logger'
/** /**
* Construct WalletConnect 2.0 session namespaces to complete a new pairing. Used when approving a new pairing request. * Construct WalletConnect 2.0 session namespaces to complete a new pairing. Used when approving a new pairing request.
...@@ -201,10 +200,6 @@ export async function pairWithWalletConnectURI(uri: string): Promise<void | Pair ...@@ -201,10 +200,6 @@ export async function pairWithWalletConnectURI(uri: string): Promise<void | Pair
try { try {
return await wcWeb3Wallet.pair({ uri }) return await wcWeb3Wallet.pair({ uri })
} catch (error) { } catch (error) {
logger.error(error, {
tags: { file: 'walletConnectV2/utils', function: 'pairWithWalletConnectURI' },
})
return Promise.reject(error instanceof Error ? error.message : '') return Promise.reject(error instanceof Error ? error.message : '')
} }
} }
...@@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react' ...@@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { KeyboardAvoidingView, TextInput } from 'react-native' import { KeyboardAvoidingView, TextInput } from 'react-native'
import { FlatList } from 'react-native-gesture-handler' import { FlatList } from 'react-native-gesture-handler'
import { useAnimatedRef } from 'react-native-reanimated'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { useExploreStackNavigation } from 'src/app/navigation/types' import { useExploreStackNavigation } from 'src/app/navigation/types'
import { ExploreSections } from 'src/components/explore/ExploreSections' import { ExploreSections } from 'src/components/explore/ExploreSections'
...@@ -45,7 +46,7 @@ export function ExploreScreen(): JSX.Element { ...@@ -45,7 +46,7 @@ export function ExploreScreen(): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const listRef = useRef(null) const listRef = useAnimatedRef<FlatList>()
useScrollToTop(listRef) useScrollToTop(listRef)
const [searchQuery, setSearchQuery] = useState<string>('') const [searchQuery, setSearchQuery] = useState<string>('')
......
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 }
...@@ -29,6 +29,7 @@ import { useFiatOnRampAggregatorGetCountryQuery } from 'uniswap/src/features/fia ...@@ -29,6 +29,7 @@ import { useFiatOnRampAggregatorGetCountryQuery } from 'uniswap/src/features/fia
import { import {
useFiatOnRampQuotes, useFiatOnRampQuotes,
useFiatOnRampSupportedTokens, useFiatOnRampSupportedTokens,
useIsFORLoading,
useMeldFiatCurrencySupportInfo, useMeldFiatCurrencySupportInfo,
useParseFiatOnRampError, useParseFiatOnRampError,
} from 'uniswap/src/features/fiatOnRamp/hooks' } from 'uniswap/src/features/fiatOnRamp/hooks'
...@@ -211,7 +212,12 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { ...@@ -211,7 +212,12 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
// always enforce the amount used in the request to backend service // always enforce the amount used in the request to backend service
const hasValidAmount = isOffRamp ? !!tokenAmount : !!fiatAmount const hasValidAmount = isOffRamp ? !!tokenAmount : !!fiatAmount
const selectTokenLoading = hasValidAmount && (quotesLoading || !debouncedAmountsMatch) && !exceedsBalanceError const isFORLoading = useIsFORLoading({
hasValidAmount,
debouncedAmountsMatch,
quotesLoading,
exceedsBalanceError,
})
const { currentData: ipCountryData } = useFiatOnRampAggregatorGetCountryQuery() const { currentData: ipCountryData } = useFiatOnRampAggregatorGetCountryQuery()
...@@ -464,10 +470,9 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { ...@@ -464,10 +470,9 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
}) })
} }
// we only show loading when there are no errors and quote value is not empty
const buttonDisabled = const buttonDisabled =
notAvailableInThisRegion || notAvailableInThisRegion ||
selectTokenLoading || isFORLoading ||
!!quotesError || !!quotesError ||
!selectedQuote?.destinationAmount || !selectedQuote?.destinationAmount ||
exceedsBalanceError exceedsBalanceError
...@@ -483,7 +488,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { ...@@ -483,7 +488,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
<OffRampPopover <OffRampPopover
triggerContent={ triggerContent={
<PillMultiToggle <PillMultiToggle
defaultOption={RampToggle.BUY} defaultOption={isOffRamp ? RampToggle.SELL : RampToggle.BUY}
options={[ options={[
{ value: RampToggle.BUY, display: t('common.button.buy') }, { value: RampToggle.BUY, display: t('common.button.buy') },
{ value: RampToggle.SELL, display: t('common.button.sell') }, { value: RampToggle.SELL, display: t('common.button.sell') },
...@@ -523,7 +528,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { ...@@ -523,7 +528,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
quoteAmount={selectedQuote?.destinationAmount ?? 0} quoteAmount={selectedQuote?.destinationAmount ?? 0}
sourceAmount={selectedQuote?.sourceAmount ?? 0} sourceAmount={selectedQuote?.sourceAmount ?? 0}
quoteCurrencyAmountReady={Boolean(fiatAmount && selectedQuote)} quoteCurrencyAmountReady={Boolean(fiatAmount && selectedQuote)}
selectTokenLoading={selectTokenLoading} selectTokenLoading={isFORLoading}
value={value} value={value}
onChoosePredefinedValue={(val: string): void => { onChoosePredefinedValue={(val: string): void => {
if (isOffRamp) { if (isOffRamp) {
...@@ -599,7 +604,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { ...@@ -599,7 +604,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
eligible eligible
continueButtonText={t('common.button.continue')} continueButtonText={t('common.button.continue')}
disabled={buttonDisabled} disabled={buttonDisabled}
isLoading={selectTokenLoading} isLoading={isFORLoading}
onPress={onContinue} onPress={onContinue}
/> />
</AnimatedFlex> </AnimatedFlex>
......
import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import { openModal } from 'src/features/modals/modalSlice'
import { useHapticFeedback } from 'src/utils/haptics/useHapticFeedback'
import { Flex, GeneratedIcon, Text, TouchableArea, useSporeColors } from 'ui/src'
import { ArrowDownCircle, Bank, Buy, SendAction } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
import { useCexTransferProviders } from 'uniswap/src/features/fiatOnRamp/useCexTransferProviders'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ElementName, ElementNameType, MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
export type QuickAction = {
/* Icon to display for the action */
Icon: GeneratedIcon
/* Event name to log when the action is triggered */
eventName?: MobileEventName
/* Label to display for the action */
label: string
/* Name of the element to log when the action is triggered */
name: ElementNameType
/* Callback to execute when the action is triggered */
onPress: () => void
}
/**
* CTA buttons that appear at top of the screen showing actions such as
* "Send", "Receive", "Buy" etc.
*/
export function HomeScreenQuickActions({ onPressBuy }: { onPressBuy: () => void }): JSX.Element {
const colors = useSporeColors()
const iconSize = iconSizes.icon24
const contentColor = colors.accent1.val
const activeScale = 0.96
const { t } = useTranslation()
const dispatch = useDispatch()
const { hapticFeedback } = useHapticFeedback()
const cexTransferProviders = useCexTransferProviders()
const isOffRampEnabled = useFeatureFlag(FeatureFlags.FiatOffRamp)
const triggerHaptics = useCallback(async () => await hapticFeedback.light(), [hapticFeedback])
const onPressSend = useCallback(async () => {
dispatch(openModal({ name: ModalName.Send }))
await triggerHaptics()
}, [dispatch, triggerHaptics])
const onPressReceive = useCallback(async () => {
dispatch(
openModal(
cexTransferProviders.length > 0
? { name: ModalName.ReceiveCryptoModal, initialState: cexTransferProviders }
: { name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr },
),
)
await triggerHaptics()
}, [dispatch, cexTransferProviders, triggerHaptics])
// PR #4621 Necessary to declare these as direct dependencies due to race
// condition with initializing react-i18next and useMemo
const buyLabel = t('home.label.buy')
const forLabel = t('home.label.for')
const sendLabel = t('home.label.send')
const receiveLabel = t('home.label.receive')
const actions = useMemo(
() => [
{
Icon: isOffRampEnabled ? Bank : Buy,
eventName: MobileEventName.FiatOnRampQuickActionButtonPressed,
label: isOffRampEnabled ? forLabel : buyLabel,
name: ElementName.Buy,
onPress: onPressBuy,
},
{
Icon: SendAction,
label: sendLabel,
name: ElementName.Send,
onPress: onPressSend,
},
{
Icon: ArrowDownCircle,
label: receiveLabel,
name: ElementName.Receive,
onPress: onPressReceive,
},
],
[isOffRampEnabled, onPressBuy, onPressSend, onPressReceive, buyLabel, forLabel, sendLabel, receiveLabel],
)
return (
<Flex centered row gap="$spacing8" px="$spacing12">
{actions.map(({ eventName, name, label, Icon, onPress }) => (
<Trace key={name} logPress element={name} eventOnTrigger={eventName}>
<TouchableArea flex={1} scaleTo={activeScale} onPress={onPress}>
<Flex
fill
backgroundColor="$accent2"
borderRadius="$rounded20"
py="$spacing16"
px="$spacing12"
gap="$spacing12"
justifyContent="space-between"
>
<Icon color={contentColor} size={iconSize} strokeWidth={2} />
<Text color={contentColor} variant="buttonLabel2">
{label}
</Text>
</Flex>
</TouchableArea>
</Trace>
))}
</Flex>
)
}
import { FlashList } from '@shopify/flash-list'
import { useCallback } from 'react'
import { FlatList } from 'react-native'
import { useAnimatedRef, useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'
import { TokenBalanceListRow } from 'wallet/src/features/portfolio/TokenBalanceListContext'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type FlashListAnyType = FlashList<any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type FlatListAnyType = FlatList<any>
type ScrollRefType = FlashListAnyType | FlatListAnyType
export interface ScrollRefs {
tokensTabScrollValue: ReturnType<typeof useSharedValue<number>>
nftsTabScrollValue: ReturnType<typeof useSharedValue<number>>
activityTabScrollValue: ReturnType<typeof useSharedValue<number>>
exploreTabScrollValue: ReturnType<typeof useSharedValue<number>>
tokensTabScrollHandler: ReturnType<typeof useAnimatedScrollHandler>
nftsTabScrollHandler: ReturnType<typeof useAnimatedScrollHandler>
activityTabScrollHandler: ReturnType<typeof useAnimatedScrollHandler>
exploreTabScrollHandler: ReturnType<typeof useAnimatedScrollHandler>
tokensTabScrollRef: ReturnType<typeof useAnimatedRef<FlatList<TokenBalanceListRow>>>
nftsTabScrollRef: ReturnType<typeof useAnimatedRef<FlashListAnyType>>
activityTabScrollRef: ReturnType<typeof useAnimatedRef<FlatListAnyType>>
exploreTabScrollRef: ReturnType<typeof useAnimatedRef<FlatListAnyType>>
resetScrollState: () => void
}
/**
* Helper function to create the same scroll ref for all tabs
*/
const useCreateScrollRef = <T extends ScrollRefType>(): {
scrollValue: ReturnType<typeof useSharedValue<number>>
scrollRef: ReturnType<typeof useAnimatedRef<T>>
scrollHandler: ReturnType<typeof useAnimatedScrollHandler>
} => {
const scrollValue = useSharedValue(0)
const scrollRef = useAnimatedRef<T>()
const scrollHandler = useAnimatedScrollHandler((event) => (scrollValue.value = event.contentOffset.y), [scrollValue])
return { scrollValue, scrollRef, scrollHandler }
}
/**
* This hook manages the creation of all the scroll refs for the home screen
* as well as provide any scroll related actions such as resetting the scroll state
*/
export function useHomeScrollRefs(): ScrollRefs {
const tokensTabScrollRef = useCreateScrollRef<FlatList<TokenBalanceListRow>>()
const nftsTabScrollRef = useCreateScrollRef<FlashListAnyType>()
const activityTabScrollRef = useCreateScrollRef<FlatListAnyType>()
const exploreTabScrollRef = useCreateScrollRef<FlatListAnyType>()
const resetScrollState = useCallback(() => {
tokensTabScrollRef.scrollValue.value = 0
nftsTabScrollRef.scrollValue.value = 0
activityTabScrollRef.scrollValue.value = 0
exploreTabScrollRef.scrollValue.value = 0
tokensTabScrollRef.scrollRef.current?.scrollToOffset({ offset: 0, animated: true })
nftsTabScrollRef.scrollRef.current?.scrollToOffset({ offset: 0, animated: true })
activityTabScrollRef.scrollRef.current?.scrollToOffset({ offset: 0, animated: true })
exploreTabScrollRef.scrollRef.current?.scrollToOffset({ offset: 0, animated: true })
}, [tokensTabScrollRef, nftsTabScrollRef, activityTabScrollRef, exploreTabScrollRef])
return {
tokensTabScrollValue: tokensTabScrollRef.scrollValue,
tokensTabScrollHandler: tokensTabScrollRef.scrollHandler,
tokensTabScrollRef: tokensTabScrollRef.scrollRef,
nftsTabScrollValue: nftsTabScrollRef.scrollValue,
nftsTabScrollHandler: nftsTabScrollRef.scrollHandler,
nftsTabScrollRef: nftsTabScrollRef.scrollRef,
activityTabScrollValue: activityTabScrollRef.scrollValue,
activityTabScrollHandler: activityTabScrollRef.scrollHandler,
activityTabScrollRef: activityTabScrollRef.scrollRef,
exploreTabScrollValue: exploreTabScrollRef.scrollValue,
exploreTabScrollHandler: exploreTabScrollRef.scrollHandler,
exploreTabScrollRef: exploreTabScrollRef.scrollRef,
resetScrollState,
}
}
...@@ -6,7 +6,7 @@ import { OnboardingStackParamList } from 'src/app/navigation/types' ...@@ -6,7 +6,7 @@ import { OnboardingStackParamList } from 'src/app/navigation/types'
import { NotificationsBackgroundImage } from 'src/components/notifications/NotificationsBGImage' import { NotificationsBackgroundImage } from 'src/components/notifications/NotificationsBGImage'
import { useBiometricAppSettings } from 'src/features/biometrics/useBiometricAppSettings' import { useBiometricAppSettings } from 'src/features/biometrics/useBiometricAppSettings'
import { useBiometricsState } from 'src/features/biometrics/useBiometricsState' import { useBiometricsState } from 'src/features/biometrics/useBiometricsState'
import { promptPushPermission } from 'src/features/notifications/Onesignal' import { usePromptPushPermission } from 'src/features/notifications/hooks/usePromptPushPermission'
import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen' import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen'
import { useCompleteOnboardingCallback } from 'src/features/onboarding/hooks' import { useCompleteOnboardingCallback } from 'src/features/onboarding/hooks'
import { DeprecatedButton, Flex } from 'ui/src' import { DeprecatedButton, Flex } from 'ui/src'
...@@ -20,7 +20,6 @@ import { useNativeAccountExists } from 'wallet/src/features/wallet/hooks' ...@@ -20,7 +20,6 @@ import { useNativeAccountExists } from 'wallet/src/features/wallet/hooks'
import { openSettings } from 'wallet/src/utils/linking' import { openSettings } from 'wallet/src/utils/linking'
type Props = NativeStackScreenProps<OnboardingStackParamList, OnboardingScreens.Notifications> type Props = NativeStackScreenProps<OnboardingStackParamList, OnboardingScreens.Notifications>
export const showNotificationSettingsAlert = (): void => { export const showNotificationSettingsAlert = (): void => {
Alert.alert( Alert.alert(
i18n.t('onboarding.notification.permission.title'), i18n.t('onboarding.notification.permission.title'),
...@@ -39,7 +38,7 @@ export function NotificationsSetupScreen({ navigation, route: { params } }: Prop ...@@ -39,7 +38,7 @@ export function NotificationsSetupScreen({ navigation, route: { params } }: Prop
const { requiredForTransactions: isBiometricAuthEnabled } = useBiometricAppSettings() const { requiredForTransactions: isBiometricAuthEnabled } = useBiometricAppSettings()
const hasSeedPhrase = useNativeAccountExists() const hasSeedPhrase = useNativeAccountExists()
const { deviceSupportsBiometrics } = useBiometricsState() const { deviceSupportsBiometrics } = useBiometricsState()
const promptPushPermission = usePromptPushPermission()
const onCompleteOnboarding = useCompleteOnboardingCallback(params) const onCompleteOnboarding = useCompleteOnboardingCallback(params)
const navigateToNextScreen = useCallback(async () => { const navigateToNextScreen = useCallback(async () => {
...@@ -63,7 +62,7 @@ export function NotificationsSetupScreen({ navigation, route: { params } }: Prop ...@@ -63,7 +62,7 @@ export function NotificationsSetupScreen({ navigation, route: { params } }: Prop
} }
await navigateToNextScreen() await navigateToNextScreen()
}, [navigateToNextScreen]) }, [navigateToNextScreen, promptPushPermission])
return ( return (
<OnboardingScreen <OnboardingScreen
......
...@@ -11,7 +11,6 @@ import EllipsisIcon from 'ui/src/assets/icons/ellipsis.svg' ...@@ -11,7 +11,6 @@ import EllipsisIcon from 'ui/src/assets/icons/ellipsis.svg'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { iconSizes, spacing } from 'ui/src/theme' import { iconSizes, spacing } from 'ui/src/theme'
import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo' import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo'
import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { import {
useTokenBasicInfoPartsFragment, useTokenBasicInfoPartsFragment,
useTokenBasicProjectPartsFragment, useTokenBasicProjectPartsFragment,
...@@ -82,19 +81,15 @@ export const HeaderRightElement = memo(function HeaderRightElement(): JSX.Elemen ...@@ -82,19 +81,15 @@ export const HeaderRightElement = memo(function HeaderRightElement(): JSX.Elemen
const { currencyId, currencyInfo } = useTokenDetailsContext() const { currencyId, currencyInfo } = useTokenDetailsContext()
const token = useTokenBasicInfoPartsFragment({ currencyId }).data
const project = useTokenBasicProjectPartsFragment({ currencyId }).data?.project
const currentChainBalance = useTokenDetailsCurrentChainBalance() const currentChainBalance = useTokenDetailsCurrentChainBalance()
const safetyLevel = project?.safetyLevel const isBlocked = currencyInfo?.safetyInfo?.tokenList === TokenList.Blocked
const isBlocked = safetyLevel === SafetyLevel.Blocked || currencyInfo?.safetyInfo?.tokenList === TokenList.Blocked
const { menuActions, onContextMenuPress } = useTokenContextMenu({ const { menuActions, onContextMenuPress } = useTokenContextMenu({
currencyId, currencyId,
isBlocked, isBlocked,
excludedActions: EXCLUDED_ACTIONS, excludedActions: EXCLUDED_ACTIONS,
tokenSymbolForNotification: token?.symbol, tokenSymbolForNotification: currencyInfo?.currency.symbol,
portfolioBalance: currentChainBalance, portfolioBalance: currentChainBalance,
}) })
......
import { useApolloClient } from '@apollo/client' import { useApolloClient } from '@apollo/client'
import { ReactNavigationPerformanceView } from '@shopify/react-native-performance-navigation' import { ReactNavigationPerformanceView } from '@shopify/react-native-performance-navigation'
import React, { memo, useCallback, useEffect, useMemo } from 'react' import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FadeInDown, FadeOutDown } from 'react-native-reanimated' import { FadeInDown, FadeOutDown } from 'react-native-reanimated'
import { useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { AppStackScreenProp } from 'src/app/navigation/types' import { AppStackScreenProp } from 'src/app/navigation/types'
import { PriceExplorer } from 'src/components/PriceExplorer/PriceExplorer' import { PriceExplorer } from 'src/components/PriceExplorer/PriceExplorer'
import { BuyNativeTokenModal } from 'src/components/TokenDetails/BuyNativeTokenModal' import { BuyNativeTokenModal } from 'src/components/TokenDetails/BuyNativeTokenModal'
...@@ -15,6 +15,7 @@ import { TokenDetailsLinks } from 'src/components/TokenDetails/TokenDetailsLinks ...@@ -15,6 +15,7 @@ import { TokenDetailsLinks } from 'src/components/TokenDetails/TokenDetailsLinks
import { TokenDetailsStats } from 'src/components/TokenDetails/TokenDetailsStats' import { TokenDetailsStats } from 'src/components/TokenDetails/TokenDetailsStats'
import { useTokenDetailsCurrentChainBalance } from 'src/components/TokenDetails/useTokenDetailsCurrentChainBalance' import { useTokenDetailsCurrentChainBalance } from 'src/components/TokenDetails/useTokenDetailsCurrentChainBalance'
import { HeaderScrollScreen } from 'src/components/layout/screens/HeaderScrollScreen' import { HeaderScrollScreen } from 'src/components/layout/screens/HeaderScrollScreen'
import { closeModal } from 'src/features/modals/modalSlice'
import { selectModalState } from 'src/features/modals/selectModalState' import { selectModalState } from 'src/features/modals/selectModalState'
import { HeaderRightElement, HeaderTitleElement } from 'src/screens/TokenDetailsHeaders' import { HeaderRightElement, HeaderTitleElement } from 'src/screens/TokenDetailsHeaders'
import { useIsScreenNavigationReady } from 'src/utils/useIsScreenNavigationReady' import { useIsScreenNavigationReady } from 'src/utils/useIsScreenNavigationReady'
...@@ -25,7 +26,6 @@ import { PollingInterval } from 'uniswap/src/constants/misc' ...@@ -25,7 +26,6 @@ import { PollingInterval } from 'uniswap/src/constants/misc'
import { useCrossChainBalances } from 'uniswap/src/data/balances/hooks/useCrossChainBalances' import { useCrossChainBalances } from 'uniswap/src/data/balances/hooks/useCrossChainBalances'
import { import {
Chain, Chain,
SafetyLevel,
useTokenDetailsScreenQuery, useTokenDetailsScreenQuery,
} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { import {
...@@ -102,6 +102,23 @@ const TokenDetailsQuery = memo(function _TokenDetailsQuery(): JSX.Element { ...@@ -102,6 +102,23 @@ const TokenDetailsQuery = memo(function _TokenDetailsQuery(): JSX.Element {
const TokenDetails = memo(function _TokenDetails(): JSX.Element { const TokenDetails = memo(function _TokenDetails(): JSX.Element {
const inModal = useSelector(selectModalState(ModalName.Explore)).isOpen const inModal = useSelector(selectModalState(ModalName.Explore)).isOpen
const dispatch = useDispatch()
// #region Handle modal race condition
// This is a workaround to prevent a crash. The TDP can open as a full screen
// or in the Explore BSM. When this happens the TDP switches to components
// that can only be used in a modal causing a crash.
// This code closes the Explore modal when the TDP is opened as a full screen
// screen and disallows dynamically changing the TDP components based on the
// Explore modal state.
// This behavior should not be needed when we make the TDP full screen only.
const initialModalState = useRef<boolean | null>(null)
if (initialModalState.current === null) {
initialModalState.current = inModal
} else if (initialModalState.current !== null && initialModalState.current !== inModal && inModal) {
dispatch(closeModal({ name: ModalName.Explore }))
}
// #endregion
const centerElement = useMemo(() => <HeaderTitleElement />, []) const centerElement = useMemo(() => <HeaderTitleElement />, [])
const rightElement = useMemo(() => <HeaderRightElement />, []) const rightElement = useMemo(() => <HeaderRightElement />, [])
...@@ -109,8 +126,8 @@ const TokenDetails = memo(function _TokenDetails(): JSX.Element { ...@@ -109,8 +126,8 @@ const TokenDetails = memo(function _TokenDetails(): JSX.Element {
return ( return (
<> <>
<HeaderScrollScreen <HeaderScrollScreen
showHandleBar={inModal} showHandleBar={initialModalState.current}
renderedInModal={inModal} renderedInModal={initialModalState.current}
centerElement={centerElement} centerElement={centerElement}
rightElement={rightElement} rightElement={rightElement}
> >
...@@ -241,10 +258,7 @@ const TokenDetailsActionButtonsWrapper = memo(function _TokenDetailsActionButton ...@@ -241,10 +258,7 @@ const TokenDetailsActionButtonsWrapper = memo(function _TokenDetailsActionButton
const { navigateToFiatOnRamp, navigateToSwapFlow } = useWalletNavigation() const { navigateToFiatOnRamp, navigateToSwapFlow } = useWalletNavigation()
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 isNativeCurrency = isNativeCurrencyAddress(chainId, address) const isNativeCurrency = isNativeCurrencyAddress(chainId, address)
const nativeCurrencyAddress = getChainInfo(chainId).nativeCurrency.address const nativeCurrencyAddress = getChainInfo(chainId).nativeCurrency.address
......
...@@ -39,8 +39,12 @@ ignores: [ ...@@ -39,8 +39,12 @@ ignores: [
'@storybook/addon-essentials', '@storybook/addon-essentials',
'@storybook/addon-interactions', '@storybook/addon-interactions',
'@storybook/addon-onboarding', '@storybook/addon-onboarding',
'@storybook/testing-library',
'@storybook/blocks', '@storybook/blocks',
'@storybook/preset-create-react-app', '@storybook/preset-create-react-app',
'storybook-addon-pseudo-states',
'wait-on',
'detect-package-manager',
'eslint-plugin-storybook', 'eslint-plugin-storybook',
'prop-types', 'prop-types',
## Testing ## Testing
......
...@@ -10,13 +10,18 @@ function getAbsolutePath(value: string): any { ...@@ -10,13 +10,18 @@ function getAbsolutePath(value: string): any {
return dirname(require.resolve(join(value, 'package.json'))) return dirname(require.resolve(join(value, 'package.json')))
} }
const config: StorybookConfig = { const config: StorybookConfig = {
stories: ['../../../packages/ui/**/*.stories.?(ts|tsx)', '../../../packages/ui/**/*.mdx'], stories: [
'../../../packages/ui/**/*.stories.?(ts|tsx)',
'../../../packages/ui/**/*.mdx',
'../../../packages/uniswap/src/**/*.stories.?(ts|tsx|js|jsx)',
'../../../packages/uniswap/**/*.mdx',
],
addons: [ addons: [
getAbsolutePath('@storybook/preset-create-react-app'), getAbsolutePath('@storybook/preset-create-react-app'),
getAbsolutePath('@storybook/addon-onboarding'),
getAbsolutePath('@storybook/addon-essentials'), getAbsolutePath('@storybook/addon-essentials'),
getAbsolutePath('@chromatic-com/storybook'), getAbsolutePath('@chromatic-com/storybook'),
getAbsolutePath('@storybook/addon-interactions'), getAbsolutePath('@storybook/addon-interactions'),
getAbsolutePath('storybook-addon-pseudo-states'),
], ],
framework: { framework: {
name: getAbsolutePath('@storybook/react-webpack5'), name: getAbsolutePath('@storybook/react-webpack5'),
......
{
"$schema": "https://www.chromatic.com/config-file.schema.json",
"onlyChanged": true,
"projectId": "Project:61d89aa649fc7d003ae21c76",
"storybookBaseDir": "apps/web",
"zip": true,
"buildScriptName": "storybook:build"
}
...@@ -37,7 +37,10 @@ ...@@ -37,7 +37,10 @@
"cypress:run": "cypress run --browser chrome --e2e", "cypress:run": "cypress run --browser chrome --e2e",
"deduplicate": "yarn-deduplicate --strategy=highest", "deduplicate": "yarn-deduplicate --strategy=highest",
"storybook:run": "storybook dev -p 6006", "storybook:run": "storybook dev -p 6006",
"storybook:build": "storybook build" "storybook:run:with-tests": "concurrently -s second -n \"SB RUN,SB TEST WATCH\" -c \"magenta,blue\" \"yarn storybook:run --quiet\" \"wait-on tcp:127.0.0.1:6006 -t 600000 && yarn storybook:test --watch\"",
"storybook:build": "storybook build",
"storybook:test": "test-storybook --excludeTags=\"no-tests\" --testTimeout 60000",
"storybook:test:standalone": "concurrently -k -s first -n \"SB BUILD,SB TEST\" -c \"magenta,blue\" \"yarn storybook:build && http-server storybook-static --port 6006 --silent\" \"wait-on --timeout 600000 tcp:127.0.0.1:6006 && yarn storybook:test --maxWorkers=2\""
}, },
"husky": { "husky": {
"hooks": { "hooks": {
...@@ -77,20 +80,21 @@ ...@@ -77,20 +80,21 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/preset-env": "7.26.0", "@babel/preset-env": "7.26.0",
"@chromatic-com/storybook": "3.2.2", "@chromatic-com/storybook": "3.2.4",
"@cloudflare/workers-types": "4.20231025.0", "@cloudflare/workers-types": "4.20231025.0",
"@craco/craco": "7.1.0", "@craco/craco": "7.1.0",
"@crowdin/cli": "3.14.0", "@crowdin/cli": "3.14.0",
"@ethersproject/experimental": "5.7.0", "@ethersproject/experimental": "5.7.0",
"@playwright/test": "1.49.1", "@playwright/test": "1.49.1",
"@storybook/addon-essentials": "8.4.2", "@storybook/addon-essentials": "8.5.2",
"@storybook/addon-interactions": "8.4.2", "@storybook/addon-interactions": "8.5.2",
"@storybook/addon-onboarding": "8.4.2", "@storybook/blocks": "8.5.2",
"@storybook/blocks": "8.4.2", "@storybook/preset-create-react-app": "8.5.2",
"@storybook/preset-create-react-app": "8.4.2", "@storybook/react": "8.5.2",
"@storybook/react": "8.4.2", "@storybook/react-webpack5": "8.5.2",
"@storybook/react-webpack5": "8.4.2", "@storybook/test": "8.5.2",
"@storybook/test": "8.4.2", "@storybook/test-runner": "0.21.0",
"@storybook/testing-library": "0.2.2",
"@swc/core": "1.3.72", "@swc/core": "1.3.72",
"@swc/jest": "0.2.29", "@swc/jest": "0.2.29",
"@swc/plugin-styled-components": "1.5.97", "@swc/plugin-styled-components": "1.5.97",
...@@ -106,7 +110,7 @@ ...@@ -106,7 +110,7 @@
"@types/jest": "29.5.14", "@types/jest": "29.5.14",
"@types/ms": "0.7.31", "@types/ms": "0.7.31",
"@types/multicodec": "1.0.0", "@types/multicodec": "1.0.0",
"@types/node": "18.16.0", "@types/node": "22.13.1",
"@types/qs": "6.9.2", "@types/qs": "6.9.2",
"@types/react": "18.3.18", "@types/react": "18.3.18",
"@types/react-dom": "18.3.1", "@types/react-dom": "18.3.1",
...@@ -131,6 +135,7 @@ ...@@ -131,6 +135,7 @@
"cypress": "12.17.4", "cypress": "12.17.4",
"cypress-hardhat": "2.5.3", "cypress-hardhat": "2.5.3",
"depcheck": "1.4.7", "depcheck": "1.4.7",
"detect-package-manager": "3.0.2",
"dotenv": "16.0.3", "dotenv": "16.0.3",
"dotenv-cli": "7.1.0", "dotenv-cli": "7.1.0",
"esbuild-register": "3.6.0", "esbuild-register": "3.6.0",
...@@ -139,6 +144,7 @@ ...@@ -139,6 +144,7 @@
"eslint-plugin-rulesdir": "0.2.2", "eslint-plugin-rulesdir": "0.2.2",
"eslint-plugin-storybook": "0.8.0", "eslint-plugin-storybook": "0.8.0",
"hardhat": "2.22.16", "hardhat": "2.22.16",
"http-server": "14.1.1",
"husky": "8.0.3", "husky": "8.0.3",
"jest": "29.7.0", "jest": "29.7.0",
"jest-extended": "4.0.2", "jest-extended": "4.0.2",
...@@ -158,13 +164,15 @@ ...@@ -158,13 +164,15 @@
"serve": "14.2.4", "serve": "14.2.4",
"source-map-explorer": "2.5.3", "source-map-explorer": "2.5.3",
"start-server-and-test": "2.0.0", "start-server-and-test": "2.0.0",
"storybook": "8.4.2", "storybook": "8.5.2",
"storybook-addon-pseudo-states": "4.0.2",
"swc-loader": "0.2.6", "swc-loader": "0.2.6",
"terser": "5.24.0", "terser": "5.24.0",
"terser-webpack-plugin": "5.3.9", "terser-webpack-plugin": "5.3.9",
"ts-jest": "29.2.5", "ts-jest": "29.2.5",
"tsafe": "1.6.4", "tsafe": "1.6.4",
"typescript": "5.3.3", "typescript": "5.3.3",
"wait-on": "8.0.2",
"webpack": "5.90.0", "webpack": "5.90.0",
"webpack-bundle-analyzer": "4.10.2", "webpack-bundle-analyzer": "4.10.2",
"webpack-retry-chunk-load-plugin": "3.1.1", "webpack-retry-chunk-load-plugin": "3.1.1",
...@@ -279,7 +287,6 @@ ...@@ -279,7 +287,6 @@
"react-router-dom": "6.10.0", "react-router-dom": "6.10.0",
"react-scroll-sync": "0.11.2", "react-scroll-sync": "0.11.2",
"react-table": "7.8.0", "react-table": "7.8.0",
"react-use-gesture": "6.0.14",
"react-virtualized-auto-sizer": "1.0.20", "react-virtualized-auto-sizer": "1.0.20",
"react-window": "1.8.9", "react-window": "1.8.9",
"react-window-infinite-loader": "1.0.9", "react-window-infinite-loader": "1.0.9",
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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