ci(release): publish latest release

parent 73b107f9
......@@ -6,7 +6,28 @@ alwaysApply: false
# Mobile Styling Conventions
## Component Styling
- Prefer Tamagui inline props over other methods
- Use the `styled` function from `ui/src` for consistent styling
- Prefer Tamagui styling over React Native StyleSheet when possible
- Use StyleSheet.create for performance-critical styles or when Tamagui isn't suitable
## StyleSheet Usage
- Define styles outside of the component body to prevent recreation on renders
- Use descriptive names for style objects
- Group related styles together
Example:
```typescript
const styles = StyleSheet.create({
container: {
flex: 1,
padding: spacing.spacing16,
},
header: {
fontSize: 18,
fontWeight: 'bold',
},
})
```
## Theme Usage
- Use theme tokens from the UI package instead of hardcoded values
......@@ -19,11 +40,9 @@ alwaysApply: false
- Minimize view hierarchy depth
## Platform Specific Code
- Use `.<platform>.tsx` extensions for platform-specific components
- The bundler will grab the appropriate file during the build and always fallback to `.tsx`
- The `platform` variable must be one of the following: ios, android, macos, windows, web, native
- Use the `Platform.select` API for inline platform-specific code. This method expects an object keyed by `platform`.
- Also consider using our custom platform variables like `isMobileApp`, `isInterface`, etc. for more specific platform detection needs.
- Use `$platform-web` for platform-specific styling in Tamagui
- Use `.ios.tsx` and `.android.tsx` extensions for platform-specific components
- Use the `Platform.select` API for inline platform-specific code
## Performance
- Memoize complex style calculations
......
---
description:
globs:
alwaysApply: true
description: Component Structure and Best Practices
globs:
alwaysApply: false
---
# Component Structure and Best Practices
## Component Organization
- Place state and hooks at the top of the component
- Group related state variables together
- Extract complex logic into custom hooks
- Define handlers after state declarations
- Place JSX return statement at the end of the component
## Props
- Use interface for component props
- Place prop interface directly above component
- Complex or shared types can be moved to a types.ts file
- Use descriptive prop names
- Provide default props where appropriate
## Performance Optimizations
- Memoize expensive calculations with useMemo
- Memoize event handlers with useCallback or use our custom useEvent hook
- Use React.memo for pure components that render often
- Avoid anonymous functions in render
## Component Size
- Keep components focused on a single responsibility
- Extract complex components into smaller, reusable pieces
- Aim for less than 250 lines per component file
- Extract prop interfaces and types to separate files if they become complex
## Component Structure Example
```typescript
interface ExampleComponentProps {
prop1: string;
prop2: () => void;
}
export function ExampleComponent({ prop1, prop2 }: ExampleComponentProps): JSX.Element {
// State declarations
const [state1, setState1] = useState(false)
const [state2, setState2] = useState<string>('')
// Queries and mutations
const { data, isPending } = useQuery(exampleQueries.getData(prop1))
const mutation = useMutation({
mutationFn: () => exampleService.submit(prop1),
onSuccess: prop2
})
// Custom hooks
const { data, loading } = useCustomHook()
// Derived values
const derivedValue = useMemo(() => {
return someCalculation(state1, data)
}, [state1, data])
return someCalculation(state1, prop1)
}, [state1, prop1])
// Event handlers
const handleClick = useCallback(() => {
setState1(!state1)
mutation.mutate()
}, [state1, mutation])
}, [state1])
// Side effects
useEffect(() => {
......@@ -67,7 +38,7 @@ export function ExampleComponent({ prop1, prop2 }: ExampleComponentProps): JSX.E
}, [prop2])
// Conditional rendering logic
if (isPending) {
if (loading) {
return <LoadingSpinner />
}
......@@ -80,3 +51,21 @@ export function ExampleComponent({ prop1, prop2 }: ExampleComponentProps): JSX.E
)
}
```
## Props
- Use interface for component props
- Place prop interface directly above component or in a types.ts file
- Use descriptive prop names
- Provide default props where appropriate
## Performance Optimizations
- Memoize expensive calculations with useMemo
- Memoize event handlers with useCallback
- Use React.memo for pure components that render often
- Avoid anonymous functions in render
## Component Size
- Keep components focused on a single responsibility
- Extract complex components into smaller, reusable pieces
- Aim for less than 250 lines per component file
- Extract prop interfaces and types to separate files if they become complex
---
description:
globs:
alwaysApply: true
---
## Data Fetching Patterns
### Direct Query Usage (Preferred)
- Use direct `useQuery` and `useMutation` calls in components instead of wrapping them in custom hooks
- Place query/mutation calls at the top of the component with other hooks
- Use query factories from domain-specific files
```typescript
// ❌ Avoid: Unnecessary custom hook
function useTokenPrice(tokenAddress) {
const { data, isLoading } = useQuery(tokenQueries.price(tokenAddress));
return { data, isLoading }
}
// ✅ Preferred: Direct usage
function TokenPriceDisplay({ tokenAddress }) {
const { data, isLoading } = useQuery(tokenQueries.price(tokenAddress));
// ...
}
```
### Custom Hooks (Limited Cases)
Only create custom hooks when:
1. Combining multiple related queries
2. Implementing complex business logic beyond data fetching
3. Managing domain-specific operations that transcend data fetching
```typescript
// ✅ Valid: Complex business logic
function useUserPermissions() {
const { data: user } = useQuery(userQueries.current());
const { data: roles } = useQuery(roleQueries.forUser(user?.id));
const canEditProducts = useMemo(() => {
if (!user || !roles) return false;
return roles.some(r => r.permissions.includes('product:edit')) ||
user.isAdmin;
}, [user, roles]);
return { canEditProducts };
}
```
......@@ -6,7 +6,6 @@ ignores: [
'i18next',
'moti',
'wrangler',
'react-router',
# Dependencies that depcheck thinks are missing but are actually present or never used
'@yarnpkg/core',
'@yarnpkg/cli',
......
......@@ -69,4 +69,3 @@ CLAUDE.local.md
# cursor
.cursor/mcp.json
.cursor/rules/local-workflow.mdc
diff --git a/registry.js b/registry.js
index 64b2735d3bb5284bd2450bf0d06115c3de5dcf80..489ffa2f59a1d08d71826e2ceb0076d588636c7c 100644
--- a/registry.js
+++ b/registry.js
@@ -8,8 +8,9 @@
* @format
*/
-'use strict';
+"use strict";
+/*::
export type AssetDestPathResolver = 'android' | 'generic';
export type PackagerAsset = {
@@ -25,17 +26,18 @@ export type PackagerAsset = {
+resolver?: AssetDestPathResolver,
...
};
+*/
-const assets: Array<PackagerAsset> = [];
+const assets/*::: Array<PackagerAsset>*/ = [];
-function registerAsset(asset: PackagerAsset): number {
+function registerAsset(asset/*::: PackagerAsset*/)/*::: number*/ {
// `push` returns new array length, so the first asset will
// get id 1 (not 0) to make the value truthy
return assets.push(asset);
}
-function getAssetByID(assetId: number): PackagerAsset {
+function getAssetByID(assetId/*::: number*/)/*::: PackagerAsset*/ {
return assets[assetId - 1];
}
-module.exports = {registerAsset, getAssetByID};
+module.exports = { registerAsset, getAssetByID };
diff --git a/settings-plugin/src/main/kotlin/com/facebook/react/ReactSettingsExtension.kt b/settings-plugin/src/main/kotlin/com/facebook/react/ReactSettingsExtension.kt
index aea525747e08b811eabae78c4be486fe7b3c46ba..a70b9cd72346487d347337cc95c24a4a3d8652f8 100644
--- a/settings-plugin/src/main/kotlin/com/facebook/react/ReactSettingsExtension.kt
+++ b/settings-plugin/src/main/kotlin/com/facebook/react/ReactSettingsExtension.kt
@@ -29,7 +29,7 @@ abstract class ReactSettingsExtension @Inject constructor(val settings: Settings
settings.layout.rootDirectory.file("build/generated/autolinking/").asFile
private val defaultConfigCommand: List<String> =
- windowsAwareCommandLine(listOf("npx", "@react-native-community/cli", "config")).map {
+ windowsAwareCommandLine(listOf("node", "../../node_modules/@react-native-community/cli/build/bin.js", "config")).map {
it.toString()
}
diff --git a/RNFastImage.podspec b/RNFastImage.podspec
index db0fada63fc06191f8620d336d244edde6c3dba3..93d7291b183b9625ad8d50e812ae247a11bad9d3 100644
index db0fada63fc06191f8620d336d244edde6c3dba3..286fa816e47996fdff9f25261644d612c682ae0b 100644
--- a/RNFastImage.podspec
+++ b/RNFastImage.podspec
@@ -16,6 +16,6 @@ Pod::Spec.new do |s|
......@@ -7,7 +7,6 @@ index db0fada63fc06191f8620d336d244edde6c3dba3..93d7291b183b9625ad8d50e812ae247a
s.dependency 'React-Core'
- s.dependency 'SDWebImage', '~> 5.11.1'
- s.dependency 'SDWebImageWebPCoder', '~> 0.8.4'
+ s.dependency 'SDWebImage', '~> 5.21.0'
+ s.dependency 'SDWebImageWebPCoder', '~> 0.14.6'
+ s.dependency 'SDWebImage', '~> 5.15.5'
s.dependency 'SDWebImageWebPCoder', '~> 0.8.4'
end
* @uniswap/web-admins
IPFS hash of the deployment:
- CIDv0: `Qmaeh4NVwkRpuix9zou6KYZF8nXxPrjMZyKyqApeeJBYtQ`
- CIDv1: `bafybeifw5nnks3pmgjd5anjc4lx25edidvwaf7qkwhwwpjw6xhhiisruqe`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
You can also access the Uniswap Interface from an IPFS gateway.
**BEWARE**: The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported.
**You should always use an IPFS gateway that enforces origin separation**, or our hosted deployment of the latest release at [app.uniswap.org](https://app.uniswap.org).
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://bafybeifw5nnks3pmgjd5anjc4lx25edidvwaf7qkwhwwpjw6xhhiisruqe.ipfs.dweb.link/
- [ipfs://Qmaeh4NVwkRpuix9zou6KYZF8nXxPrjMZyKyqApeeJBYtQ/](ipfs://Qmaeh4NVwkRpuix9zou6KYZF8nXxPrjMZyKyqApeeJBYtQ/)
### 5.103.1 (2025-07-10)
We are back with some new updates! Here’s the latest:
Smart Wallet Management: Enjoy granular management of your smart wallet. Enable upgrades to benefit from faster, lower-cost transactions, view your active delegations, and revoke your delegations, all from Advanced Settings.
Other changes:
- Testnet Mode has now moved into Advanced Settings.
- WalletConnect improvements
- Various bug fixes and performance improvements
web/5.103.1
\ No newline at end of file
extension/1.24.0
\ No newline at end of file
......@@ -31,14 +31,14 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"react-i18next": "14.1.0",
"react-native": "0.77.2",
"react-native-gesture-handler": "2.22.1",
"react-native": "0.76.9",
"react-native-gesture-handler": "2.21.2",
"react-native-reanimated": "3.16.7",
"react-native-svg": "15.11.2",
"react-native-svg": "15.10.1",
"react-native-web": "0.19.13",
"react-qr-code": "2.0.12",
"react-redux": "8.0.5",
"react-router": "7.6.3",
"react-router-dom": "6.30.1",
"redux": "4.2.1",
"redux-logger": "3.0.6",
"redux-persist": "6.0.0",
......
import { PropsWithChildren } from 'react'
import { useRouteError } from 'react-router'
import { useRouteError } from 'react-router-dom'
export function ErrorElement({ children }: PropsWithChildren<unknown>): JSX.Element {
const error = useRouteError()
......
......@@ -3,7 +3,7 @@ import 'src/app/Global.css'
import 'symbol-observable' // Needed by `reduxed-chrome-storage` as polyfill, order matters
import { useEffect } from 'react'
import { RouteObject, RouterProvider, createHashRouter } from 'react-router'
import { RouteObject, RouterProvider, createHashRouter } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react'
import { ErrorElement } from 'src/app/components/ErrorElement'
import { BaseAppContainer } from 'src/app/core/BaseAppContainer'
......@@ -35,6 +35,7 @@ import { OTPInput } from 'src/app/features/onboarding/scan/OTPInput'
import { ScanToOnboard } from 'src/app/features/onboarding/scan/ScanToOnboard'
import { ScantasticContextProvider } from 'src/app/features/onboarding/scan/ScantasticContextProvider'
import { OnboardingRoutes, TopLevelRoutes } from 'src/app/navigation/constants'
import { ROUTER_FUTURE_FLAGS, ROUTER_PROVIDER_FUTURE_FLAGS } from 'src/app/navigation/routerConfig'
import { setRouter, setRouterState } from 'src/app/navigation/state'
import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { checksIfSupportsSidePanel } from 'src/app/utils/chrome'
......@@ -139,14 +140,19 @@ const allRoutes = [
},
]
const router = createHashRouter([
const router = createHashRouter(
[
{
path: `/${TopLevelRoutes.Onboarding}`,
element: <OnboardingWrapper />,
errorElement: <ErrorElement />,
children: !supportsSidePanel ? [unsupportedRoute] : allRoutes,
},
],
{
path: `/${TopLevelRoutes.Onboarding}`,
element: <OnboardingWrapper />,
errorElement: <ErrorElement />,
children: !supportsSidePanel ? [unsupportedRoute] : allRoutes,
future: ROUTER_FUTURE_FLAGS,
},
])
)
function ScantasticFlow({ isResetting = false }: { isResetting?: boolean }): JSX.Element {
return (
......@@ -193,7 +199,7 @@ export default function OnboardingApp(): JSX.Element {
<PersistGate persistor={getReduxPersistor()}>
<BaseAppContainer appName={DatadogAppNameTag.Onboarding}>
<PrimaryAppInstanceDebuggerLazy />
<RouterProvider router={router} />
<RouterProvider router={router} future={ROUTER_PROVIDER_FUTURE_FLAGS} />
</BaseAppContainer>
</PersistGate>
)
......
......@@ -3,10 +3,11 @@ import 'src/app/Global.css'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { RouterProvider, createHashRouter } from 'react-router'
import { RouterProvider, createHashRouter } from 'react-router-dom'
import { ErrorElement } from 'src/app/components/ErrorElement'
import { BaseAppContainer } from 'src/app/core/BaseAppContainer'
import { DatadogAppNameTag } from 'src/app/datadog'
import { ROUTER_FUTURE_FLAGS, ROUTER_PROVIDER_FUTURE_FLAGS } from 'src/app/navigation/routerConfig'
import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { Button, Flex, Image, Text } from 'ui/src'
import { UNISWAP_LOGO } from 'ui/src/assets'
......@@ -17,13 +18,18 @@ import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { ExtensionScreens } from 'uniswap/src/types/screens/extension'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics'
const router = createHashRouter([
const router = createHashRouter(
[
{
path: '',
element: <PopupContent />,
errorElement: <ErrorElement />,
},
],
{
path: '',
element: <PopupContent />,
errorElement: <ErrorElement />,
future: ROUTER_FUTURE_FLAGS,
},
])
)
function PopupContent(): JSX.Element {
const { t } = useTranslation()
......@@ -102,7 +108,7 @@ export default function PopupApp(): JSX.Element {
return (
<BaseAppContainer appName={DatadogAppNameTag.Popup}>
<RouterProvider router={router} />
<RouterProvider router={router} future={ROUTER_PROVIDER_FUTURE_FLAGS} />
</BaseAppContainer>
)
}
......@@ -4,7 +4,7 @@ import 'src/app/Global.css'
import { SharedEventName } from '@uniswap/analytics-events'
import { useEffect, useRef, useState } from 'react'
import { useDispatch } from 'react-redux'
import { RouterProvider, createHashRouter } from 'react-router'
import { RouterProvider, createHashRouter } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react'
import { ErrorElement } from 'src/app/components/ErrorElement'
import { BaseAppContainer } from 'src/app/core/BaseAppContainer'
......@@ -29,6 +29,7 @@ import { SwapFlowScreen } from 'src/app/features/swap/SwapFlowScreen'
import { useIsWalletUnlocked } from 'src/app/hooks/useIsWalletUnlocked'
import { AppRoutes, RemoveRecoveryPhraseRoutes, SettingsRoutes } from 'src/app/navigation/constants'
import { MainContent, WebNavigation } from 'src/app/navigation/navigation'
import { ROUTER_FUTURE_FLAGS, ROUTER_PROVIDER_FUTURE_FLAGS } from 'src/app/navigation/routerConfig'
import { setRouter, setRouterState } from 'src/app/navigation/state'
import { initExtensionAnalytics } from 'src/app/utils/analytics'
import {
......@@ -48,88 +49,93 @@ import { useInterval } from 'utilities/src/time/timing'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics'
import { getReduxPersistor } from 'wallet/src/state/persistor'
const router = createHashRouter([
const router = createHashRouter(
[
{
path: '',
element: <SidebarWrapper />,
errorElement: <ErrorElement />,
children: [
{
path: '',
element: <MainContent />,
},
{
path: AppRoutes.AccountSwitcher,
element: <AccountSwitcherScreen />,
},
{
path: AppRoutes.Settings,
element: <SettingsScreenWrapper />,
children: [
{
path: '',
element: <SettingsScreen />,
},
{
path: SettingsRoutes.ChangePassword,
element: <SettingsChangePasswordScreen />,
},
{
path: SettingsRoutes.DeviceAccess,
element: <DeviceAccessScreen />,
},
isDevEnv()
? {
path: SettingsRoutes.DevMenu,
element: <DevMenuScreen />,
}
: {},
{
path: SettingsRoutes.ViewRecoveryPhrase,
element: <ViewRecoveryPhraseScreen />,
},
{
path: SettingsRoutes.BackupRecoveryPhrase,
element: <BackupRecoveryPhraseScreen />,
},
{
path: SettingsRoutes.RemoveRecoveryPhrase,
children: [
{
path: RemoveRecoveryPhraseRoutes.Wallets,
element: <RemoveRecoveryPhraseWallets />,
},
{
path: RemoveRecoveryPhraseRoutes.Verify,
element: <RemoveRecoveryPhraseVerify />,
},
],
},
{
path: SettingsRoutes.ManageConnections,
element: <SettingsManageConnectionsScreen />,
},
{
path: SettingsRoutes.SmartWallet,
element: <SmartWalletSettingsScreen />,
},
],
},
{
path: AppRoutes.Send,
element: <SendFlow />,
},
{
path: AppRoutes.Swap,
element: <SwapFlowScreen />,
},
{
path: AppRoutes.Receive,
element: <ReceiveScreen />,
},
],
},
],
{
path: '',
element: <SidebarWrapper />,
errorElement: <ErrorElement />,
children: [
{
path: '',
element: <MainContent />,
},
{
path: AppRoutes.AccountSwitcher,
element: <AccountSwitcherScreen />,
},
{
path: AppRoutes.Settings,
element: <SettingsScreenWrapper />,
children: [
{
path: '',
element: <SettingsScreen />,
},
{
path: SettingsRoutes.ChangePassword,
element: <SettingsChangePasswordScreen />,
},
{
path: SettingsRoutes.DeviceAccess,
element: <DeviceAccessScreen />,
},
isDevEnv()
? {
path: SettingsRoutes.DevMenu,
element: <DevMenuScreen />,
}
: {},
{
path: SettingsRoutes.ViewRecoveryPhrase,
element: <ViewRecoveryPhraseScreen />,
},
{
path: SettingsRoutes.BackupRecoveryPhrase,
element: <BackupRecoveryPhraseScreen />,
},
{
path: SettingsRoutes.RemoveRecoveryPhrase,
children: [
{
path: RemoveRecoveryPhraseRoutes.Wallets,
element: <RemoveRecoveryPhraseWallets />,
},
{
path: RemoveRecoveryPhraseRoutes.Verify,
element: <RemoveRecoveryPhraseVerify />,
},
],
},
{
path: SettingsRoutes.ManageConnections,
element: <SettingsManageConnectionsScreen />,
},
{
path: SettingsRoutes.SmartWallet,
element: <SmartWalletSettingsScreen />,
},
],
},
{
path: AppRoutes.Send,
element: <SendFlow />,
},
{
path: AppRoutes.Swap,
element: <SwapFlowScreen />,
},
{
path: AppRoutes.Receive,
element: <ReceiveScreen />,
},
],
future: ROUTER_FUTURE_FLAGS,
},
])
)
const PORT_PING_INTERVAL = 5 * ONE_SECOND_MS
function useDappRequestPortListener(): void {
......@@ -246,7 +252,7 @@ export default function SidebarApp(): JSX.Element {
<BaseAppContainer appName={DatadogAppNameTag.Sidebar}>
<DappContextProvider>
<PrimaryAppInstanceDebuggerLazy />
<RouterProvider router={router} />
<RouterProvider router={router} future={ROUTER_PROVIDER_FUTURE_FLAGS} />
</DappContextProvider>
</BaseAppContainer>
</PersistGate>
......
......@@ -2,7 +2,7 @@ import '@tamagui/core/reset.css'
import 'src/app/Global.css'
import { PropsWithChildren, useEffect } from 'react'
import { Outlet, RouterProvider, createHashRouter, useSearchParams } from 'react-router'
import { Outlet, RouterProvider, createHashRouter, useSearchParams } from 'react-router-dom'
import { ErrorElement } from 'src/app/components/ErrorElement'
import { BaseAppContainer } from 'src/app/core/BaseAppContainer'
import { DatadogAppNameTag } from 'src/app/datadog'
......@@ -19,6 +19,7 @@ import { UnitagConfirmationScreen } from 'src/app/features/unitags/UnitagConfirm
import { UnitagCreateUsernameScreen } from 'src/app/features/unitags/UnitagCreateUsernameScreen'
import { UnitagIntroScreen } from 'src/app/features/unitags/UnitagIntroScreen'
import { UnitagClaimRoutes } from 'src/app/navigation/constants'
import { ROUTER_FUTURE_FLAGS, ROUTER_PROVIDER_FUTURE_FLAGS } from 'src/app/navigation/routerConfig'
import { setRouter, setRouterState } from 'src/app/navigation/state'
import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { Flex } from 'ui/src'
......@@ -27,24 +28,29 @@ import { usePrevious } from 'utilities/src/react/hooks'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics'
import { useAccountAddressFromUrlWithThrow } from 'wallet/src/features/wallet/hooks'
const router = createHashRouter([
const router = createHashRouter(
[
{
path: '',
element: <UnitagAppInner />,
children: [
{
path: UnitagClaimRoutes.ClaimIntro,
element: <UnitagClaimFlow />,
errorElement: <ErrorElement />,
},
{
path: UnitagClaimRoutes.EditProfile,
element: <UnitagEditProfileFlow />,
errorElement: <ErrorElement />,
},
],
},
],
{
path: '',
element: <UnitagAppInner />,
children: [
{
path: UnitagClaimRoutes.ClaimIntro,
element: <UnitagClaimFlow />,
errorElement: <ErrorElement />,
},
{
path: UnitagClaimRoutes.EditProfile,
element: <UnitagEditProfileFlow />,
errorElement: <ErrorElement />,
},
],
future: ROUTER_FUTURE_FLAGS,
},
])
)
/**
* Note: we are using a pattern here to avoid circular dependencies, because
......@@ -138,7 +144,7 @@ export default function UnitagClaimApp(): JSX.Element {
return (
<BaseAppContainer appName={DatadogAppNameTag.UnitagClaim}>
<RouterProvider router={router} />
<RouterProvider router={router} future={ROUTER_PROVIDER_FUTURE_FLAGS} />
</BaseAppContainer>
)
}
......@@ -236,8 +236,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
style="display: flex; flex-basis: 0px; box-sizing: border-box; position: relative; min-height: 0px; min-width: 0px; flex-shrink: 1; flex-direction: row; border-top-width: 1px; border-right-width: 1px; border-bottom-width: 1px; border-left-width: 1px; align-items: center; justify-content: center; background-color: rgba(34, 34, 34, 0.05); border-top-color: transparent; border-right-color: transparent; border-bottom-color: transparent; border-left-color: transparent; cursor: pointer; height: auto; align-self: stretch; flex-grow: 1; outline-color: rgba(0, 0, 0, 0); padding: 8px 12px 8px 12px; border-top-left-radius: 12px; border-top-right-radius: 12px; border-bottom-right-radius: 12px; border-bottom-left-radius: 12px; gap: 8px; border-bottom-style: solid; border-top-style: solid; border-left-style: solid; border-right-style: solid; transform: scale(1);"
>
<span
class="font_button _color-_groupitem-hover_neutral1Hov3121676 _display-inline _boxSizing-border-box _wordWrap-break-word _whiteSpace-nowrap _mt-0px _mr-0px _mb-0px _ml-0px _color-neutral1 _fontFamily-f-family _maxWidth-10037 _overflowX-hidden _overflowY-hidden _textOverflow-ellipsis _textAlign-center _fontSize-f-size-smal108 _fontWeight-f-weight-me3083741 _lineHeight-f-lineHeigh1269072494"
line-height-disabled="false"
class="font_button _color-_groupitem-hover_neutral1Hov3121676 _display-inline _boxSizing-border-box _wordWrap-break-word _whiteSpace-nowrap _mt-0px _mr-0px _mb-0px _ml-0px _color-neutral1 _fontFamily-f-family _maxWidth-10037 _overflowX-hidden _overflowY-hidden _textOverflow-ellipsis _fontSize-f-size-smal108 _fontWeight-f-weight-me3083741 _lineHeight-f-lineHeigh1269072494"
>
Edit label
</span>
......@@ -531,8 +530,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
style="display: flex; flex-basis: 0px; box-sizing: border-box; position: relative; min-height: 0px; min-width: 0px; flex-shrink: 1; flex-direction: row; border-top-width: 1px; border-right-width: 1px; border-bottom-width: 1px; border-left-width: 1px; align-items: center; justify-content: center; background-color: rgba(34, 34, 34, 0.05); border-top-color: transparent; border-right-color: transparent; border-bottom-color: transparent; border-left-color: transparent; cursor: pointer; height: auto; align-self: stretch; flex-grow: 1; outline-color: rgba(0, 0, 0, 0); padding: 8px 12px 8px 12px; border-top-left-radius: 12px; border-top-right-radius: 12px; border-bottom-right-radius: 12px; border-bottom-left-radius: 12px; gap: 8px; border-bottom-style: solid; border-top-style: solid; border-left-style: solid; border-right-style: solid; transform: scale(1);"
>
<span
class="font_button _color-_groupitem-hover_neutral1Hov3121676 _display-inline _boxSizing-border-box _wordWrap-break-word _whiteSpace-nowrap _mt-0px _mr-0px _mb-0px _ml-0px _color-neutral1 _fontFamily-f-family _maxWidth-10037 _overflowX-hidden _overflowY-hidden _textOverflow-ellipsis _textAlign-center _fontSize-f-size-smal108 _fontWeight-f-weight-me3083741 _lineHeight-f-lineHeigh1269072494"
line-height-disabled="false"
class="font_button _color-_groupitem-hover_neutral1Hov3121676 _display-inline _boxSizing-border-box _wordWrap-break-word _whiteSpace-nowrap _mt-0px _mr-0px _mb-0px _ml-0px _color-neutral1 _fontFamily-f-family _maxWidth-10037 _overflowX-hidden _overflowY-hidden _textOverflow-ellipsis _fontSize-f-size-smal108 _fontWeight-f-weight-me3083741 _lineHeight-f-lineHeigh1269072494"
>
Edit label
</span>
......
/* eslint-disable max-lines */
import { Provider, TransactionResponse } from '@ethersproject/providers'
import { providerErrors, rpcErrors, serializeError } from '@metamask/rpc-errors'
import { createSearchParams } from 'react-router'
import { createSearchParams } from 'react-router-dom'
import { changeChain } from 'src/app/features/dapp/changeChain'
import { DappInfo, dappStore } from 'src/app/features/dapp/store'
import { getActiveConnectedAccount } from 'src/app/features/dapp/utils'
......
import { useEffect, useState } from 'react'
import { Outlet } from 'react-router'
import { Outlet } from 'react-router-dom'
import { DevMenuModal } from 'src/app/core/DevMenuModal'
import { StorageWarningModal } from 'src/app/features/warnings/StorageWarningModal'
import { ONBOARDING_BACKGROUND_DARK, ONBOARDING_BACKGROUND_LIGHT } from 'src/assets'
......
import { PropsWithChildren } from 'react'
import { Trans } from 'react-i18next'
import { Link, LinkProps } from 'react-router'
import { Link, LinkProps } from 'react-router-dom'
import { Text } from 'ui/src'
import { uniswapUrls } from 'uniswap/src/constants/urls'
......
import { Action, AuthenticationTypes } from '@uniswap/client-embeddedwallet/dist/uniswap/embeddedwallet/v1/service_pb'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Navigate, useLocation } from 'react-router'
import { Navigate, useLocation } from 'react-router-dom'
import { useOnboardingSteps } from 'src/app/features/onboarding/OnboardingStepsContext'
import { usePasskeyImportContext } from 'src/app/features/onboarding/import/PasskeyImportContextProvider'
import {
......
import { useTranslation } from 'react-i18next'
import { useLocation } from 'react-router'
import { useLocation } from 'react-router-dom'
import { OptionCard } from 'src/app/components/buttons/OptionCard'
import { OnboardingScreen } from 'src/app/features/onboarding/OnboardingScreen'
import {
......
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router'
import { Link } from 'react-router-dom'
import { useFinishExtensionOnboarding } from 'src/app/features/onboarding/useFinishExtensionOnboarding'
import { terminateStoreSynchronization } from 'src/store/storeSynchronization'
import { Flex, Text } from 'ui/src'
......
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import { Link } from 'react-router'
import { Link } from 'react-router-dom'
import { useDappContext } from 'src/app/features/dapp/DappContext'
import { removeDappConnection } from 'src/app/features/dapp/actions'
import { SwitchNetworksModal } from 'src/app/features/home/SwitchNetworksModal'
......
......@@ -6015,8 +6015,7 @@ exports[`ReceiveScreen renders without error 1`] = `
style="display: flex; flex-basis: auto; box-sizing: border-box; position: relative; min-height: 0px; min-width: 0px; flex-shrink: 0; flex-direction: row; border-top-width: 1px; border-right-width: 1px; border-bottom-width: 1px; border-left-width: 1px; align-items: center; justify-content: center; background-color: rgba(34, 34, 34, 0.05); border-top-color: transparent; border-right-color: transparent; border-bottom-color: transparent; border-left-color: transparent; cursor: pointer; height: auto; outline-color: rgba(0, 0, 0, 0); padding: 4px 6px 4px 6px; border-top-left-radius: 12px; border-top-right-radius: 12px; border-bottom-right-radius: 12px; border-bottom-left-radius: 12px; gap: 4px; border-bottom-style: solid; border-top-style: solid; border-left-style: solid; border-right-style: solid; transform: scale(1);"
>
<span
class="font_button _color-_groupitem-hover_neutral1Hov3121676 _display-inline _boxSizing-border-box _wordWrap-break-word _whiteSpace-nowrap _mt-0px _mr-0px _mb-0px _ml-0px _color-neutral1 _fontFamily-f-family _maxWidth-10037 _overflowX-hidden _overflowY-hidden _textOverflow-ellipsis _textAlign-center _fontSize-f-size-micr111 _fontWeight-f-weight-me3083741 _lineHeight-f-lineHeigh1263414315"
line-height-disabled="false"
class="font_button _color-_groupitem-hover_neutral1Hov3121676 _display-inline _boxSizing-border-box _wordWrap-break-word _whiteSpace-nowrap _mt-0px _mr-0px _mb-0px _ml-0px _color-neutral1 _fontFamily-f-family _maxWidth-10037 _overflowX-hidden _overflowY-hidden _textOverflow-ellipsis _fontSize-f-size-micr111 _fontWeight-f-weight-me3083741 _lineHeight-f-lineHeigh1263414315"
>
Networks
</span>
......@@ -12052,8 +12051,7 @@ exports[`ReceiveScreen renders without error 1`] = `
style="display: flex; flex-basis: auto; box-sizing: border-box; position: relative; min-height: 0px; min-width: 0px; flex-shrink: 0; flex-direction: row; border-top-width: 1px; border-right-width: 1px; border-bottom-width: 1px; border-left-width: 1px; align-items: center; justify-content: center; background-color: rgba(34, 34, 34, 0.05); border-top-color: transparent; border-right-color: transparent; border-bottom-color: transparent; border-left-color: transparent; cursor: pointer; height: auto; outline-color: rgba(0, 0, 0, 0); padding: 4px 6px 4px 6px; border-top-left-radius: 12px; border-top-right-radius: 12px; border-bottom-right-radius: 12px; border-bottom-left-radius: 12px; gap: 4px; border-bottom-style: solid; border-top-style: solid; border-left-style: solid; border-right-style: solid; transform: scale(1);"
>
<span
class="font_button _color-_groupitem-hover_neutral1Hov3121676 _display-inline _boxSizing-border-box _wordWrap-break-word _whiteSpace-nowrap _mt-0px _mr-0px _mb-0px _ml-0px _color-neutral1 _fontFamily-f-family _maxWidth-10037 _overflowX-hidden _overflowY-hidden _textOverflow-ellipsis _textAlign-center _fontSize-f-size-micr111 _fontWeight-f-weight-me3083741 _lineHeight-f-lineHeigh1263414315"
line-height-disabled="false"
class="font_button _color-_groupitem-hover_neutral1Hov3121676 _display-inline _boxSizing-border-box _wordWrap-break-word _whiteSpace-nowrap _mt-0px _mr-0px _mb-0px _ml-0px _color-neutral1 _fontFamily-f-family _maxWidth-10037 _overflowX-hidden _overflowY-hidden _textOverflow-ellipsis _fontSize-f-size-micr111 _fontWeight-f-weight-me3083741 _lineHeight-f-lineHeigh1263414315"
>
Networks
</span>
......
import { useTranslation } from 'react-i18next'
import { Button, Flex } from 'ui/src'
import { WarningLabel } from 'uniswap/src/components/modals/WarningModal/types'
import { nativeOnChain } from 'uniswap/src/constants/tokens'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { isWeb } from 'utilities/src/platform'
import { useSendContext } from 'wallet/src/features/transactions/contexts/SendContext'
......@@ -21,7 +21,7 @@ export function ReviewButton({ onPress, disabled }: ReviewButtonProps): JSX.Elem
derivedSendInfo: { chainId },
} = useSendContext()
const nativeCurrencySymbol = nativeOnChain(chainId).symbol
const nativeCurrencySymbol = NativeCurrency.onChain(chainId).symbol
const insufficientGasFunds = warnings.warnings.some((warning) => warning.type === WarningLabel.InsufficientGasFunds)
......@@ -29,7 +29,7 @@ export function ReviewButton({ onPress, disabled }: ReviewButtonProps): JSX.Elem
const buttonText = insufficientGasFunds
? t('send.warning.insufficientFunds.title', {
currencySymbol: nativeCurrencySymbol ?? '',
currencySymbol: nativeCurrencySymbol,
})
: t('common.button.review')
......
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router'
import { Link } from 'react-router-dom'
import { Flex, Text } from 'ui/src'
import { uniswapUrls } from 'uniswap/src/constants/urls'
......
import { Outlet } from 'react-router'
import { Outlet } from 'react-router-dom'
import { Flex } from 'ui/src'
/**
......
import { Link } from 'react-router'
import { Link } from 'react-router-dom'
import { ColorTokens, Flex, GeneratedIcon, Text, TouchableArea, useSporeColors } from 'ui/src'
import { RotatableChevron } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
......
......@@ -4,8 +4,8 @@ import { useExtensionNavigation } from 'src/app/navigation/utils'
import { Flex } from 'ui/src'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { useHighestBalanceNativeCurrencyId } from 'uniswap/src/features/dataApi/balances'
import { selectFilteredChainIds } from 'uniswap/src/features/transactions/swap/contexts/selectors'
import { useSwapPrefilledState } from 'uniswap/src/features/transactions/swap/form/hooks/useSwapPrefilledState'
import { selectFilteredChainIds } from 'uniswap/src/features/transactions/swap/state/selectors'
import { prepareSwapFormState } from 'uniswap/src/features/transactions/types/transactionState'
import { CurrencyField } from 'uniswap/src/types/currency'
import { logger } from 'utilities/src/logger/logger'
......
import { useEffect, useState } from 'react'
import { createSearchParams } from 'react-router'
import { createSearchParams } from 'react-router-dom'
import { getRouter } from 'src/app/navigation/state'
import { sleep } from 'utilities/src/time/timing'
......@@ -14,7 +14,7 @@ export function useOptimizedSearchParams(): URLSearchParams {
useEffect(() => {
return getRouter().subscribe(async () => {
// react-router calls this before it actually updates the url bar :/
// react-router-dom calls this before it actually updates the url bar :/
await sleep(0)
setSearchParams((prev) => {
const next = getSearchParams()
......
import { PropsWithChildren, useCallback } from 'react'
import { createSearchParams, useNavigate } from 'react-router'
import { createSearchParams, useNavigate } from 'react-router-dom'
import { navigateToInterfaceFiatOnRamp } from 'src/app/features/for/utils'
import { AppRoutes, HomeQueryParams, HomeTabs } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state'
......@@ -123,7 +123,7 @@ function useNavigateToAccountActivityList(): () => void {
const navigateFix = useNavigate()
return useCallback(
(): void | Promise<void> =>
(): void =>
navigateFix({
pathname: AppRoutes.Home,
search: createSearchParams({
......@@ -139,7 +139,7 @@ function useNavigateToAccountTokenList(): () => void {
const navigateFix = useNavigate()
return useCallback(
(): void | Promise<void> =>
(): void =>
navigateFix({
pathname: AppRoutes.Home,
search: createSearchParams({
......
import { useMutation } from '@tanstack/react-query'
import { useEffect, useMemo, useRef } from 'react'
import { useSelector } from 'react-redux'
import { NavigationType, Outlet, ScrollRestoration, useLocation } from 'react-router'
import { NavigationType, Outlet, ScrollRestoration, useLocation } from 'react-router-dom'
import { SmartWalletNudgeModals } from 'src/app/components/modals/SmartWalletNudgeModals'
import { DappRequestQueue } from 'src/app/features/dappRequests/DappRequestQueue'
import { ForceUpgradeModal } from 'src/app/features/forceUpgrade/ForceUpgradeModal'
......
// Shared future flags configuration for createHashRouter
export const ROUTER_FUTURE_FLAGS = {
v7_relativeSplatPath: true,
v7_fetcherPersist: true,
v7_normalizeFormMethod: true,
v7_partialHydration: true,
}
// Shared future flags configuration for RouterProvider
export const ROUTER_PROVIDER_FUTURE_FLAGS = {
v7_startTransition: true,
}
import { useEffect, useState } from 'react'
import { Location, NavigationType, Router, createHashRouter } from 'react-router'
import { Location, NavigationType, Router, createHashRouter } from 'react-router-dom'
interface RouterState {
historyAction: NavigationType
......@@ -52,7 +52,7 @@ export function useRouterState(): RouterState | null {
return val
}
// as far as i can tell, react-router doesn't give us this type so have to work around
// as far as i can tell, react-router-dom doesn't give us this type so have to work around
type Router = ReturnType<typeof createHashRouter>
let router: Router | null = null
......
import { To, useLocation } from 'react-router'
import { To, useLocation } from 'react-router-dom'
import { TopLevelRoutes, UnitagClaimRoutes } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state'
import { onboardingMessageChannel } from 'src/background/messagePassing/messageChannels'
......
......@@ -39,7 +39,6 @@ import {
import { logContentScriptError } from 'src/contentScript/utils'
import { chainIdToHexadecimalString } from 'uniswap/src/features/chains/utils'
import { EthMethod } from 'uniswap/src/features/dappRequests/types'
import { Platform } from 'uniswap/src/features/platforms/types/Platform'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { getValidAddress } from 'uniswap/src/utils/addresses'
import { logger } from 'utilities/src/logger/logger'
......@@ -89,7 +88,7 @@ const setChainIdAndMaybeEmit = (newChainId: string): void => {
const setConnectedAddressesAndMaybeEmit = (newConnectedAddresses: Address[]): void => {
// Only emit if the addresses have changed, and it's not the first time
const normalizedNewAddresses: Address[] = newConnectedAddresses
.map((address) => getValidAddress({ address, platform: Platform.EVM }))
.map((address) => getValidAddress({ address }))
.filter((normalizedAddress): normalizedAddress is Address => normalizedAddress !== null)
if (!connectedAddresses || !arraysAreEqual(connectedAddresses, normalizedNewAddresses)) {
......
......@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Uniswap Extension",
"description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.",
"version": "1.25.0",
"version": "1.24.0",
"minimum_chrome_version": "116",
"icons": {
"16": "assets/icon16.png",
......
......@@ -15,7 +15,6 @@ const POLL_ENV = process.env.WEBPACK_POLLING_INTERVAL
process.env.NODE_ENV = NODE_ENV
const isDevelopment = NODE_ENV === 'development'
const isProduction = NODE_ENV === 'production'
const appDirectory = path.resolve(__dirname)
const manifest = require('./src/manifest.json')
......@@ -284,12 +283,6 @@ module.exports = (env) => {
src: path.resolve(__dirname, 'src'), // absolute imports in apps/web
'react-native-gesture-handler$': require.resolve('react-native-gesture-handler'),
'expo-blur': require.resolve('./__mocks__/expo-blur.js'),
'react-router': path.resolve(
__dirname,
isProduction
? '../../node_modules/react-router/dist/production/index.mjs'
: '../../node_modules/react-router/dist/development/index.mjs',
),
},
// Add support for web-based extensions so we can share code between mobile/extension
extensions: [
......
......@@ -4,8 +4,7 @@ gem 'fastlane', '2.214.0'
# Exclude problematic versions of cocoapods and activesupport that causes build failures.
gem 'cocoapods', '1.14.3'
gem 'activesupport', '7.1.2'
gem 'xcodeproj', '1.27.0'
gem 'concurrent-ruby', '1.3.4'
gem 'xcodeproj', '1.26.0'
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)
......@@ -81,7 +81,7 @@ GEM
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
concurrent-ruby (1.3.4)
concurrent-ruby (1.3.5)
connection_pool (2.5.0)
declarative (0.0.20)
digest-crc (0.6.5)
......@@ -222,7 +222,7 @@ GEM
multi_json (1.15.0)
multipart-post (2.3.0)
mutex_m (0.3.0)
nanaimo (0.4.0)
nanaimo (0.3.0)
nap (1.1.0)
naturally (2.2.1)
netrc (0.11.0)
......@@ -236,7 +236,7 @@ GEM
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.4.1)
rexml (3.2.6)
rouge (2.0.7)
ruby-macho (2.5.1)
ruby2_keywords (0.0.5)
......@@ -266,13 +266,13 @@ GEM
unicode-display_width (1.8.0)
webrick (1.8.1)
word_wrap (1.0.0)
xcodeproj (1.27.0)
xcodeproj (1.23.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
......@@ -284,11 +284,10 @@ PLATFORMS
DEPENDENCIES
activesupport (= 7.1.2)
cocoapods (= 1.14.3)
concurrent-ruby (= 1.3.4)
fastlane (= 2.214.0)
fastlane-plugin-get_version_name
fastlane-plugin-versioning_android
xcodeproj (= 1.27.0)
xcodeproj (= 1.26.0)
BUNDLED WITH
2.4.10
......@@ -6,7 +6,6 @@ plugins {
id 'com.google.gms.google-services'
id 'maven-publish'
id 'kotlin-android'
id 'org.jetbrains.kotlin.plugin.compose'
}
def nodeModulesPath = "../../../../node_modules"
......@@ -72,9 +71,9 @@ if (isCI && datadogPropertiesAvailable) {
apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle"
}
def devVersionName = "1.55"
def betaVersionName = "1.55"
def prodVersionName = "1.55"
def devVersionName = "1.54"
def betaVersionName = "1.54"
def prodVersionName = "1.54"
android {
ndkVersion rootProject.ext.ndkVersion
......
......@@ -6,8 +6,8 @@ buildscript {
minSdkVersion = 28
compileSdkVersion = 35
targetSdkVersion = 35
ndkVersion = "27.1.12297006"
kotlinVersion = "2.0.21"
ndkVersion = "26.1.10909125"
kotlinVersion = "1.9.25"
appCompat = "1.6.1"
compose = "1.4.3"
......@@ -33,7 +33,6 @@ buildscript {
plugins {
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlinVersion"
id 'org.jetbrains.kotlin.plugin.compose' version "$kotlinVersion" apply false
}
allprojects {
......
This diff is collapsed.
......@@ -5,7 +5,6 @@
#import "Uniswap-Swift.h"
#import <React/RCTBundleURLProvider.h>
#import <ReactAppDependencyProvider/RCTAppDependencyProvider.h>
#import <ReactNativePerformance/ReactNativePerformance.h>
#import <RCTAppSetupUtils.h>
#import <RNBootSplash.h>
......@@ -57,7 +56,6 @@ static NSString *const hasLaunchedOnceKey = @"HasLaunchedOnce";
}
self.moduleName = @"Uniswap";
self.dependencyProvider = [RCTAppDependencyProvider new];
self.initialProps = @{};
[self.window makeKeyAndVisible];
......
......@@ -101,7 +101,3 @@ jest.mock("react-native-bootsplash", () => {
}),
};
});
jest.mock("react-native-keyboard-controller", () =>
require("react-native-keyboard-controller/jest"),
);
......@@ -13,6 +13,7 @@
"check:deps:usage": "./scripts/checkDepsUsage.sh",
"check:bundlesize": "./scripts/checkBundleSize.sh",
"clean": "react-native-clean-project",
"debug": "react-devtools",
"debug:reactotron:install": "./scripts/installDebugger.sh",
"deduplicate": "yarn-deduplicate --strategy=fewer",
"depcheck": "depcheck",
......@@ -30,7 +31,6 @@
"link:assets": "react-native-asset",
"check:circular": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 0",
"ios": "rnef run:ios --scheme Uniswap --configuration Debug && yarn start",
"ios:interactive": "ts-node scripts/ios-build-interactive/main.ts",
"ios:smol": "rnef run:ios --device=\"iPhone SE (3rd generation)\"",
"ios:dev:release": "rnef run:ios --configuration Dev",
"ios:beta": "rnef run:ios --configuration Beta",
......@@ -72,11 +72,11 @@
"@react-native-firebase/auth": "21.0.0",
"@react-native-firebase/firestore": "21.0.0",
"@react-native-masked-view/masked-view": "0.3.2",
"@react-native/metro-config": "0.77.2",
"@react-navigation/core": "7.9.2",
"@react-navigation/native": "7.1.9",
"@react-navigation/native-stack": "7.3.13",
"@react-navigation/stack": "7.3.2",
"@react-native/metro-config": "0.76.9",
"@react-navigation/core": "6.2.2",
"@react-navigation/native": "6.0.11",
"@react-navigation/native-stack": "6.7.0",
"@react-navigation/stack": "6.2.2",
"@reduxjs/toolkit": "1.9.3",
"@reown/walletkit": "1.2.3",
"@rnef/cli": "0.7.18",
......@@ -87,7 +87,7 @@
"@shopify/flash-list": "1.7.3",
"@shopify/react-native-performance": "4.1.2",
"@shopify/react-native-performance-navigation": "3.0.0",
"@shopify/react-native-skia": "1.12.4",
"@shopify/react-native-skia": "1.7.2",
"@sparkfabrik/react-native-idfa-aaid": "1.2.0",
"@tanstack/react-query": "5.77.2",
"@testing-library/react-hooks": "8.0.1",
......@@ -126,18 +126,17 @@
"react": "18.3.1",
"react-freeze": "1.0.3",
"react-i18next": "14.1.0",
"react-native": "0.77.2",
"react-native": "0.76.9",
"react-native-appsflyer": "6.13.1",
"react-native-bootsplash": "6.3.1",
"react-native-context-menu-view": "1.15.0",
"react-native-device-info": "10.11.0",
"react-native-dotenv": "3.2.0",
"react-native-fast-image": "8.6.3",
"react-native-gesture-handler": "2.22.1",
"react-native-gesture-handler": "2.21.2",
"react-native-get-random-values": "1.11.0",
"react-native-image-colors": "1.5.2",
"react-native-image-picker": "7.1.0",
"react-native-keyboard-controller": "1.17.5",
"react-native-localize": "2.2.6",
"react-native-markdown-display": "7.0.0-alpha.2",
"react-native-mmkv": "2.10.1",
......@@ -147,15 +146,15 @@
"react-native-permissions": "4.1.5",
"react-native-reanimated": "3.16.7",
"react-native-restart": "0.0.27",
"react-native-safe-area-context": "5.1.0",
"react-native-screens": "4.11.0",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "4.1.0",
"react-native-sortables": "1.5.1",
"react-native-svg": "15.11.2",
"react-native-svg": "15.10.1",
"react-native-tab-view": "3.5.2",
"react-native-url-polyfill": "1.3.0",
"react-native-video": "6.13.0",
"react-native-wagmi-charts": "2.5.2",
"react-native-webview": "13.13.5",
"react-native-webview": "13.12.5",
"react-native-widgetkit": "1.0.9",
"react-redux": "8.0.5",
"redux": "4.2.1",
......@@ -186,9 +185,7 @@
"@storybook/react-native": "8.5.2",
"@tamagui/babel-plugin": "1.125.17",
"@testing-library/react-native": "13.0.0",
"@types/inquirer": "9.0.8",
"@types/jest": "29.5.14",
"@types/node": "22.13.1",
"@types/react": "18.3.18",
"@types/redux-mock-store": "1.0.6",
"@uniswap/eslint-config": "workspace:^",
......@@ -200,7 +197,6 @@
"core-js": "2.6.12",
"eslint": "8.44.0",
"expo-modules-core": "2.2.3",
"inquirer": "12.6.0",
"jest": "29.7.0",
"jest-expo": "52.0.3",
"jest-extended": "4.0.2",
......@@ -208,6 +204,7 @@
"madge": "6.1.0",
"mockdate": "3.0.5",
"postinstall-postinstall": "2.1.0",
"react-devtools": "4.28.0",
"react-dom": "18.3.1",
"react-native-asset": "2.1.1",
"react-native-clean-project": "4.0.1",
......@@ -224,12 +221,10 @@
"expo": {
"install": {
"exclude": [
"react-native@~0.76.9",
"react-native-reanimated@~3.16.7",
"react-native-gesture-handler@~2.20.0",
"react-native-screens@~4.4.0",
"react-native-safe-area-context@~4.12.0",
"react-native-webview@~13.12.5"
"react-native@~0.74.0",
"react-native-reanimated@~3.10.0",
"react-native-gesture-handler@~2.16.1",
"react-native-screens@~3.31.1"
]
},
"autolinking": {
......
#!/bin/bash
MAX_SIZE=23.75
MAX_SIZE=23.44
MAX_BUFFER=0.5
# Check OS type and use appropriate stat command
......
# iOS Build Interactive Tool
An interactive CLI tool for building iOS apps with various configurations.
## Structure
- `main.ts` - Main script containing the interactive build logic
- `utils.ts` - Shared utilities, constants, types, and helper functions
## Usage
From the `apps/mobile` directory:
```bash
yarn ios:interactive
```
## Features
- Choose between simulator and device builds
- Select Debug or Release configurations
- Pick from multiple app schemes (Uniswap, Dev, Beta, Production)
- Auto-detect available simulators and devices
- Metro bundler management
- Build cleaning and cache reset options
## Requirements
- Xcode
- Node.js
- Yarn
- CocoaPods
- iOS Simulator (for simulator builds)
- Connected iOS device (for device builds)
This diff is collapsed.
/* eslint-disable no-console */
import { exec, spawn } from 'child_process'
import { promisify } from 'util'
const execAsync = promisify(exec)
// Constants
export const CONSTANTS = {
PORTS: {
METRO: 8081,
},
PATHS: {
MOBILE_DIR: 'apps/mobile',
ENV_FILE: '.env.defaults.local',
BUILD_DIR: 'ios/build',
},
COMMANDS: {
XCODE_VERSION: 'xcodebuild -version',
NODE_VERSION: 'node --version',
YARN_VERSION: 'yarn --version',
POD_VERSION: 'pod --version',
LIST_SIMULATORS: 'xcrun simctl list devices',
LIST_SIMULATORS_JSON: 'xcrun simctl list devices --json',
LIST_DEVICES_NEW: 'xcrun devicectl list devices',
LIST_DEVICES_OLD: 'xcrun instruments -s devices',
CHECK_PORT: 'lsof -i :',
START_METRO: ['yarn', 'start'],
CLEAN_BUILD: 'rm -rf',
POD_INSTALL: 'yarn pod',
},
TIMEOUTS: {
METRO_START: 3000,
METRO_RESET: 2000,
},
MESSAGES: {
EMOJIS: {
CHECK: '🔍',
SUCCESS: '',
ERROR: '',
WARNING: '⚠️',
ROCKET: '🚀',
CLEAN: '🧹',
TRASH: '🗑️',
BUILD: '🔨',
BULB: '💡',
PHONE: '📱',
DEVICE: '📲',
PARTY: '🎉',
WAVE: '👋',
},
ERRORS: {
WRONG_DIR: 'Please run this script from the apps/mobile directory',
ENV_MISSING: 'Environment file missing!',
ENV_DOWNLOAD:
'yarn env:local:download (from apps/mobile) OR yarn mobile env:local:download (from workspace root)',
BUILD_FAILED: 'Build failed with exit code',
PREFLIGHT_FAILED: 'Pre-flight check failed:',
},
},
} as const
// Types
export type BuildType = 'simulator' | 'device'
export type Configuration = 'Debug' | 'Release'
export type Scheme = 'Uniswap'
export interface BuildConfig {
buildType: BuildType
configuration: Configuration
scheme: Scheme
simulator?: string
device?: string
cleanBuild: boolean
resetMetroCache: boolean
}
export interface SimulatorDevice {
name: string
udid: string
state: string
isAvailable: boolean
}
export interface PhysicalDevice {
name: string
udid: string
platform: string
}
export interface PreflightCheck {
name: string
command: string
}
// Utility functions
export const log = {
info: (message: string): void => console.log(message),
success: (message: string): void => console.log(`${CONSTANTS.MESSAGES.EMOJIS.SUCCESS} ${message}`),
error: (message: string): void => console.log(`${CONSTANTS.MESSAGES.EMOJIS.ERROR} ${message}`),
warning: (message: string): void => console.log(`${CONSTANTS.MESSAGES.EMOJIS.WARNING} ${message}`),
}
export const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))
export const runCommand = async (command: string): Promise<{ stdout: string; stderr: string }> => {
return execAsync(command)
}
export const spawnProcess = (command: string, args: string[]): Promise<void> => {
return new Promise((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const process = spawn(command, args, { stdio: 'inherit' } as any)
process.on('close', (code) => {
if (code === 0) {
resolve()
} else {
reject(new Error(`Process failed with exit code ${code}`))
}
})
})
}
export const parseDeviceFromLine = (line: string): PhysicalDevice | null => {
const match = line.match(/^(.+?)\s+\(([0-9.]+)\)\s+\[([A-F0-9-]+)\]/)
if (!match || line.includes('Simulator')) {
return null
}
const [, name, version, udid] = match
if (name && version && udid) {
return {
name: name.trim(),
udid,
platform: `iOS ${version}`,
}
}
return null
}
export const printBuildInfo = (config: BuildConfig, targetType: string): void => {
log.info(`${CONSTANTS.MESSAGES.EMOJIS.BUILD} Building for ${targetType}...`)
log.info(`Configuration: ${config.configuration}`)
log.info(`Scheme: ${config.scheme}`)
const targetName = config.buildType === 'simulator' ? config.simulator : config.device
if (targetName) {
log.info(`Target ${targetType}: ${targetName}`)
}
}
export const printTroubleshootingTips = (isDevice: boolean): void => {
log.info(`\n${CONSTANTS.MESSAGES.EMOJIS.BULB} Troubleshooting suggestions:`)
log.info('1. Try cleaning build folder and resetting Metro cache')
log.info(`2. Ensure all pods are installed: ${CONSTANTS.COMMANDS.POD_INSTALL}`)
log.info('3. Check Xcode for any signing or configuration issues')
if (isDevice) {
log.info('4. Ensure your device is connected and trusted')
log.info('5. Check your signing certificates in Xcode')
log.info('6. Verify your provisioning profiles are valid')
log.info('7. Make sure your device is registered in your Apple Developer account')
} else {
log.info('4. Verify the selected simulator is available')
}
}
export const printHelp = (): void => {
log.info(`${CONSTANTS.MESSAGES.EMOJIS.PHONE} iOS Build Interactive Tool`)
log.info('')
log.info('Interactive CLI tool for building iOS apps with various configurations.')
log.info('')
log.info('Features:')
log.info('• Choose between simulator and device builds')
log.info('• Select Debug or Release configurations')
log.info('• Pick from multiple app schemes')
log.info('• Auto-detect available simulators and devices')
log.info('• Metro bundler management')
log.info('• Build cleaning and cache reset options')
log.info('')
log.info('Usage: yarn ios:interactive')
}
// Prompt configurations
export const PROMPT_CONFIGS = {
buildType: {
type: 'list' as const,
name: 'buildType' as const,
message: 'What type of build do you want?',
choices: [
{ name: `${CONSTANTS.MESSAGES.EMOJIS.PHONE} iOS Simulator`, value: 'simulator' },
{ name: `${CONSTANTS.MESSAGES.EMOJIS.DEVICE} Physical Device`, value: 'device' },
],
},
configuration: {
type: 'list' as const,
name: 'configuration' as const,
message: 'Select build configuration:',
choices: [
{ name: 'Debug (faster build, debugging enabled)', value: 'Debug' },
{ name: 'Release (optimized, production-ready)', value: 'Release' },
],
default: 'Debug',
},
utilities: {
type: 'checkbox' as const,
name: 'utilities' as const,
message: 'Select additional options:',
choices: [
{ name: 'Clean build folder before building', value: 'clean' },
{ name: 'Reset Metro cache', value: 'resetCache' },
],
},
}
......@@ -2,7 +2,7 @@
set -e
REQUIRED_XCODE_VERSION="16.4"
REQUIRED_XCODE_VERSION="16.3"
UPDATE_REPOS=false
while [[ $# -gt 0 ]]; do
......
......@@ -10,7 +10,6 @@ import { LogBox, NativeModules, StatusBar } from 'react-native'
import appsFlyer from 'react-native-appsflyer'
import DeviceInfo, { getUniqueIdSync } from 'react-native-device-info'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
import { KeyboardProvider } from 'react-native-keyboard-controller'
import { MMKV } from 'react-native-mmkv'
import { OneSignal } from 'react-native-onesignal'
import { configureReanimatedLogger } from 'react-native-reanimated'
......@@ -156,16 +155,14 @@ function App(): JSX.Element | null {
<StrictMode>
<I18nextProvider i18n={i18n}>
<SafeAreaProvider>
<KeyboardProvider>
<SharedWalletReduxProvider reduxStore={store}>
<AnalyticsNavigationContextProvider
shouldLogScreen={shouldLogScreen}
useIsPartOfNavigationTree={useIsPartOfNavigationTree}
>
<AppOuter />
</AnalyticsNavigationContextProvider>
</SharedWalletReduxProvider>
</KeyboardProvider>
<SharedWalletReduxProvider reduxStore={store}>
<AnalyticsNavigationContextProvider
shouldLogScreen={shouldLogScreen}
useIsPartOfNavigationTree={useIsPartOfNavigationTree}
>
<AppOuter />
</AnalyticsNavigationContextProvider>
</SharedWalletReduxProvider>
</SafeAreaProvider>
</I18nextProvider>
</StrictMode>
......@@ -200,8 +197,8 @@ function AppOuter(): JSX.Element | null {
loading_time: report.timeToBootJsMillis,
})
jsBundleLoadedRef.current = true
// Note that we are not checking report.interactive here because it's not consistently reported.
// Additionally, we are not tracking interactive the same way @shopify/react-native-performance does.
}
if (report.interactive) {
await DdRum.addTiming(DDRumTiming.ScreenInteractive)
}
}
......
import { PropsWithChildren, useCallback } from 'react'
import { Share } from 'react-native'
import { useDispatch } from 'react-redux'
import { exploreNavigationRef, navigationRef } from 'src/app/navigation/navigationRef'
import { exploreNavigationRef } from 'src/app/navigation/navigationRef'
import { useAppStackNavigation } from 'src/app/navigation/types'
import { useReactNavigationModal } from 'src/components/modals/useReactNavigationModal'
import { closeAllModals, closeModal, openModal } from 'src/features/modals/modalSlice'
......@@ -195,22 +195,13 @@ function useNavigateToTokenDetails(): (currencyId: string) => void {
return useCallback(
(currencyId: string): void => {
const isExploreNavigationActuallyFocused = Boolean(
navigationRef.getCurrentRoute()?.name === ModalName.Explore &&
exploreNavigationRef.current &&
exploreNavigationRef.isFocused(),
)
closeKeyboardBeforeCallback(() => {
onClose()
dispatch(closeAllModals())
if (isExploreNavigationActuallyFocused) {
if (exploreNavigationRef.current && exploreNavigationRef.isFocused()) {
exploreNavigationRef.navigate(MobileScreens.TokenDetails, { currencyId })
} else {
onClose()
appNavigation.reset({
index: 1,
routes: [{ name: MobileScreens.Home }, { name: MobileScreens.TokenDetails, params: { currencyId } }],
})
appNavigation.navigate(MobileScreens.TokenDetails, { currencyId })
}
})
},
......
......@@ -261,13 +261,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
variant="subheading1"
/>
<Flex row px="$spacing12">
<Button
lineHeightDisabled
size="medium"
testID={TestID.WalletSettings}
emphasis="secondary"
onPress={onManageWallet}
>
<Button size="medium" testID={TestID.WalletSettings} emphasis="secondary" onPress={onManageWallet}>
{t('account.wallet.button.manage')}
</Button>
</Flex>
......@@ -283,7 +277,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
<TouchableArea mt="$spacing16" testID={TestID.AccountSwitcherAddWallet} onPress={onPressAddWallet}>
<Flex row alignItems="center" gap="$spacing8" ml="$spacing24">
<PlusCircle />
<Text numberOfLines={1} width="100%" color="$neutral1" variant="buttonLabel2">
<Text color="$neutral1" variant="buttonLabel2">
{t('account.wallet.button.add')}
</Text>
</Flex>
......
......@@ -89,64 +89,7 @@ exports[`AccountSwitcher renders correctly 1`] = `
"width": 56,
}
}
>
<View
style={
{
"height": 56,
"width": 56,
}
}
>
<skCircle
color="#00C3A01F"
cx={28}
cy={28}
r={28}
/>
<skGroup
transform={
[
{
"translateX": 9.333333333333332,
},
{
"translateY": 9.333333333333332,
},
]
}
>
<skGroup
transform={
[
{
"scale": 0.7777777777777778,
},
]
}
>
<skPath
clip-rule="evenodd"
color="#00C3A0"
end={1}
fillType="evenOdd"
path="M20.9454 0.0175773C22.4936 -0.167833 23.763 1.13917 23.763 2.72944V21.3625C23.763 22.9527 22.4993 24.2419 20.9405 24.2419L2.67549 24.2419C1.11665 24.2419 -0.16452 22.9469 0.0172307 21.3675C1.30306 10.1936 9.99222 1.32931 20.9454 0.0175773Z"
start={0}
strokeWidth={1}
/>
<skPath
clip-rule="evenodd"
color="#00C3A0"
end={1}
fillType="evenOdd"
path="M27.0546 47.9824C25.5064 48.1678 24.237 46.8608 24.237 45.2706V26.6375C24.237 25.0473 25.5007 23.7581 27.0595 23.7581L45.3245 23.7581C46.8833 23.7581 48.1645 25.0531 47.9828 26.6325C46.6969 37.8064 38.0078 46.6707 27.0546 47.9824Z"
start={0}
strokeWidth={1}
/>
</skGroup>
</skGroup>
</View>
</View>
/>
</View>
<View
style={
......@@ -414,7 +357,6 @@ exports[`AccountSwitcher renders correctly 1`] = `
testID="wallet-settings"
>
<Text
line-height-disabled="true"
maxFontSizeMultiplier={1.2}
numberOfLines={1}
onBlur={[Function]}
......@@ -430,7 +372,7 @@ exports[`AccountSwitcher renders correctly 1`] = `
"fontFamily": "Basel Grotesk",
"fontSize": 17,
"fontWeight": "500",
"textAlign": "center",
"lineHeight": 21.849999999999998,
}
}
suppressHighlighting={true}
......@@ -480,6 +422,16 @@ exports[`AccountSwitcher renders correctly 1`] = `
scrollEventThrottle={0.0001}
stickyHeaderIndices={[]}
viewabilityConfigCallbackPairs={[]}
waitFor={
[
{
"current": null,
},
{
"current": null,
},
]
}
>
<View />
</RCTScrollView>
......@@ -650,7 +602,6 @@ exports[`AccountSwitcher renders correctly 1`] = `
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.2}
numberOfLines={1}
style={
{
"color": {
......@@ -663,7 +614,6 @@ exports[`AccountSwitcher renders correctly 1`] = `
"fontSize": 17,
"fontWeight": "500",
"lineHeight": 19.549999999999997,
"width": "100%",
}
}
suppressHighlighting={true}
......
import { DefaultTheme, NavigationContainer, NavigationIndependentTree } from '@react-navigation/native'
import { NavigationContainer } from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import React from 'react'
import { navNativeStackOptions } from 'src/app/navigation/navStackOptions'
......@@ -20,42 +20,40 @@ export function ExploreStackNavigator(): JSX.Element {
const colors = useSporeColors()
return (
<NavigationIndependentTree>
<NavigationContainer
ref={exploreNavigationRef}
theme={{
...DefaultTheme,
dark: false,
colors: {
primary: 'transparent',
background: 'transparent',
card: 'transparent',
text: 'transparent',
border: 'transparent',
notification: 'transparent',
},
}}
onStateChange={stopTracking}
onReady={() => startTracking(exploreNavigationRef)}
<NavigationContainer
ref={exploreNavigationRef}
independent
theme={{
dark: false,
colors: {
primary: 'transparent',
background: 'transparent',
card: 'transparent',
text: 'transparent',
border: 'transparent',
notification: 'transparent',
},
}}
onStateChange={stopTracking}
onReady={() => startTracking(exploreNavigationRef)}
>
<HorizontalEdgeGestureTarget />
<ExploreStack.Navigator
initialRouteName={MobileScreens.Explore}
screenOptions={navNativeStackOptions.independentBsm}
>
<HorizontalEdgeGestureTarget />
<ExploreStack.Navigator
initialRouteName={MobileScreens.Explore}
screenOptions={navNativeStackOptions.independentBsm}
>
<ExploreStack.Screen component={ExploreScreen} name={MobileScreens.Explore} />
<ExploreStack.Group screenOptions={{ contentStyle: { backgroundColor: colors.surface1.val } }}>
<ExploreStack.Screen name={MobileScreens.ExternalProfile}>
{(props): JSX.Element => <ExternalProfileScreen {...props} renderedInModal />}
</ExploreStack.Screen>
<ExploreStack.Screen name={MobileScreens.NFTCollection}>
{(props): JSX.Element => <NFTCollectionScreen {...props} renderedInModal />}
</ExploreStack.Screen>
<ExploreStack.Screen component={NFTItemScreen} name={MobileScreens.NFTItem} />
<ExploreStack.Screen component={TokenDetailsScreen} name={MobileScreens.TokenDetails} />
</ExploreStack.Group>
</ExploreStack.Navigator>
</NavigationContainer>
</NavigationIndependentTree>
<ExploreStack.Screen component={ExploreScreen} name={MobileScreens.Explore} />
<ExploreStack.Group screenOptions={{ contentStyle: { backgroundColor: colors.surface1.val } }}>
<ExploreStack.Screen name={MobileScreens.ExternalProfile}>
{(props): JSX.Element => <ExternalProfileScreen {...props} renderedInModal />}
</ExploreStack.Screen>
<ExploreStack.Screen name={MobileScreens.NFTCollection}>
{(props): JSX.Element => <NFTCollectionScreen {...props} renderedInModal />}
</ExploreStack.Screen>
<ExploreStack.Screen component={NFTItemScreen} name={MobileScreens.NFTItem} />
<ExploreStack.Screen component={TokenDetailsScreen} name={MobileScreens.TokenDetails} />
</ExploreStack.Group>
</ExploreStack.Navigator>
</NavigationContainer>
)
}
......@@ -24,7 +24,7 @@ import { useHighestBalanceNativeCurrencyId } from 'uniswap/src/features/dataApi/
import { useHapticFeedback } from 'uniswap/src/features/settings/useHapticFeedback/useHapticFeedback'
import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { selectFilteredChainIds } from 'uniswap/src/features/transactions/swap/state/selectors'
import { selectFilteredChainIds } from 'uniswap/src/features/transactions/swap/contexts/selectors'
import { prepareSwapFormState } from 'uniswap/src/features/transactions/types/transactionState'
import { useAppInsets } from 'uniswap/src/hooks/useAppInsets'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
......
import { NavigationContainer, NavigationIndependentTree } from '@react-navigation/native'
import { NavigationContainer } from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { TransitionPresets, createStackNavigator } from '@react-navigation/stack'
import React, { useEffect } from 'react'
......@@ -196,28 +196,27 @@ function WrappedHomeScreen(props: AppStackScreenProp<MobileScreens.Home>): JSX.E
export function FiatOnRampStackNavigator(): JSX.Element {
return (
<NavigationIndependentTree>
<NavigationContainer
ref={fiatOnRampNavigationRef}
onReady={() => startTracking(fiatOnRampNavigationRef)}
onStateChange={stopTracking}
>
<HorizontalEdgeGestureTarget />
<FiatOnRampProvider>
<FiatOnRampStack.Navigator
initialRouteName={FiatOnRampScreens.AmountInput}
screenOptions={navNativeStackOptions.independentBsm}
>
<FiatOnRampStack.Screen component={FiatOnRampScreen} name={FiatOnRampScreens.AmountInput} />
<FiatOnRampStack.Screen
component={FiatOnRampServiceProvidersScreen}
name={FiatOnRampScreens.ServiceProviders}
/>
<FiatOnRampStack.Screen component={FiatOnRampConnectingScreen} name={FiatOnRampScreens.Connecting} />
</FiatOnRampStack.Navigator>
</FiatOnRampProvider>
</NavigationContainer>
</NavigationIndependentTree>
<NavigationContainer
ref={fiatOnRampNavigationRef}
independent
onReady={() => startTracking(fiatOnRampNavigationRef)}
onStateChange={stopTracking}
>
<HorizontalEdgeGestureTarget />
<FiatOnRampProvider>
<FiatOnRampStack.Navigator
initialRouteName={FiatOnRampScreens.AmountInput}
screenOptions={navNativeStackOptions.independentBsm}
>
<FiatOnRampStack.Screen component={FiatOnRampScreen} name={FiatOnRampScreens.AmountInput} />
<FiatOnRampStack.Screen
component={FiatOnRampServiceProvidersScreen}
name={FiatOnRampScreens.ServiceProviders}
/>
<FiatOnRampStack.Screen component={FiatOnRampConnectingScreen} name={FiatOnRampScreens.Connecting} />
</FiatOnRampStack.Navigator>
</FiatOnRampProvider>
</NavigationContainer>
)
}
......@@ -317,7 +316,7 @@ function UnitagStackNavigator(): JSX.Element {
screenOptions={{
headerMode: 'float',
headerTitle: '',
headerBackButtonDisplayMode: 'minimal',
headerBackTitleVisible: false,
headerBackImage: renderHeaderBackImage,
headerStatusBarHeight: insets.top + spacing.spacing8,
headerTransparent: true,
......@@ -391,7 +390,6 @@ export function AppStackNavigator(): JSX.Element {
<AppStack.Screen component={NFTCollectionScreen} name={MobileScreens.NFTCollection} />
<AppStack.Screen component={WebViewScreen} name={MobileScreens.WebView} />
<AppStack.Screen component={SettingsStackGroup} name={MobileScreens.SettingsStack} />
<AppStack.Screen component={ViewPrivateKeysScreen} name={MobileScreens.ViewPrivateKeys} />
<AppStack.Group screenOptions={navNativeStackOptions.presentationModal}>
<AppStack.Screen component={EducationScreen} name={MobileScreens.Education} />
</AppStack.Group>
......
import { createNavigationContainerRef } from '@react-navigation/native'
import { ExploreStackParamList, FiatOnRampStackParamList, RootParamList } from 'src/app/navigation/types'
import { ExploreStackParamList, FiatOnRampStackParamList } from 'src/app/navigation/types'
// this was moved to its own file to avoid circular dependencies
export const navigationRef = createNavigationContainerRef<RootParamList>()
export const navigationRef = createNavigationContainerRef()
export const exploreNavigationRef = createNavigationContainerRef<ExploreStackParamList>()
export const fiatOnRampNavigationRef = createNavigationContainerRef<FiatOnRampStackParamList>()
export const navRefs = [exploreNavigationRef, fiatOnRampNavigationRef, navigationRef]
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NavigationAction, NavigationState } from '@react-navigation/core'
import { navigationRef } from 'src/app/navigation/navigationRef'
import { RootParamList } from 'src/app/navigation/types'
......@@ -24,9 +23,9 @@ export function navigate<RouteName extends keyof RootParamList>(...args: RootNav
return
}
// Type assignment to `any` is a workaround until we figure out how to
// Type assignment to `never` is a workaround until we figure out how to
// type `createNavigationContainerRef` in a way that's compatible
navigationRef.navigate(routeName as any, params as never)
navigationRef.navigate(routeName as never, params as never)
}
export function dispatchNavigationAction(
......
......@@ -13,7 +13,6 @@ import {
import { Flex, Text } from 'ui/src'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { EthMethod } from 'uniswap/src/features/dappRequests/types'
import { Platform } from 'uniswap/src/features/platforms/types/Platform'
import { EthTransaction } from 'uniswap/src/types/walletConnect'
import { getValidAddress } from 'uniswap/src/utils/addresses'
import { logger } from 'utilities/src/logger/logger'
......@@ -93,11 +92,7 @@ const getParsedObjectDisplay = ({
const childValue = obj[objKey]
// Special case for address strings
// TODO(WALL-7065): Handle SVM address validation as well
if (
typeof childValue === 'string' &&
getValidAddress({ address: childValue, platform: Platform.EVM, withEVMChecksum: true })
) {
if (typeof childValue === 'string' && getValidAddress({ address: childValue, withChecksum: true })) {
return (
<KeyValueRow key={objKey} objKey={objKey}>
<Flex>
......
......@@ -8,13 +8,13 @@ import { Flex, SpinningLoader, Text, useIsDarkMode } from 'ui/src'
import { iconSizes, spacing } from 'ui/src/theme'
import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo'
import { NetworkFee } from 'uniswap/src/components/gas/NetworkFee'
import { nativeOnChain } from 'uniswap/src/constants/tokens'
import { getChainLabel } from 'uniswap/src/features/chains/utils'
import { CurrencyInfo } from 'uniswap/src/features/dataApi/types'
import { GasFeeResult } from 'uniswap/src/features/gas/types'
import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext'
import { useOnChainCurrencyBalance } from 'uniswap/src/features/portfolio/api'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency'
import { useCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo'
import { buildCurrencyId } from 'uniswap/src/utils/currencyId'
import { NumberType } from 'utilities/src/format/types'
......@@ -99,7 +99,7 @@ function UwULinkErc20SendModalContent({
const { convertFiatAmountFormatted } = useLocalizationContext()
const { chainId, isStablecoin } = request
const nativeCurrency = nativeOnChain(chainId)
const nativeCurrency = NativeCurrency.onChain(chainId)
if (loading || !currencyInfo) {
return (
......@@ -153,7 +153,7 @@ function UwULinkErc20SendModalContent({
{!hasSufficientGasFunds && (
<Text color="$statusWarning" pt="$spacing8" textAlign="center" variant="body3">
{t('walletConnect.request.error.insufficientFunds', {
currencySymbol: nativeCurrency.symbol ?? '',
currencySymbol: nativeCurrency.symbol,
})}
</Text>
)}
......
......@@ -12,9 +12,9 @@ import {
import { Flex, Text } from 'ui/src'
import { AlertTriangleFilled } from 'ui/src/components/icons'
import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard'
import { nativeOnChain } from 'uniswap/src/constants/tokens'
import { EthMethod } from 'uniswap/src/features/dappRequests/types'
import { GasFeeResult } from 'uniswap/src/features/gas/types'
import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency'
import { BlockedAddressWarning } from 'uniswap/src/features/transactions/modals/BlockedAddressWarning'
import { isPrimaryTypePermit } from 'uniswap/src/types/walletConnect'
import { buildCurrencyId } from 'uniswap/src/utils/currencyId'
......@@ -69,7 +69,7 @@ export function WalletConnectRequestModalContent({
}: WalletConnectRequestModalContentProps): JSX.Element {
const chainId = request.chainId
const permitInfo = getPermitInfo(request)
const nativeCurrency = nativeOnChain(chainId)
const nativeCurrency = NativeCurrency.onChain(chainId)
const { t } = useTranslation()
const { animatedFooterHeight } = useBottomSheetInternal()
......@@ -115,7 +115,7 @@ export function WalletConnectRequestModalContent({
<Flex p="$spacing16">
<Text color="$statusWarning" variant="body2">
{t('walletConnect.request.error.insufficientFunds', {
currencySymbol: nativeCurrency.symbol ?? '',
currencySymbol: nativeCurrency.symbol,
})}
</Text>
</Flex>
......
import { useMemo } from 'react'
import { nativeOnChain } from 'uniswap/src/constants/tokens'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { GasFeeResult } from 'uniswap/src/features/gas/types'
import { hasSufficientFundsIncludingGas } from 'uniswap/src/features/gas/utils'
import { useOnChainNativeCurrencyBalance } from 'uniswap/src/features/portfolio/api'
import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency'
import { ValueType, getCurrencyAmount } from 'uniswap/src/features/tokens/getCurrencyAmount'
export function useHasSufficientFunds({
......@@ -19,7 +19,7 @@ export function useHasSufficientFunds({
value?: string
}): boolean {
const { defaultChainId } = useEnabledChains()
const nativeCurrency = nativeOnChain(chainId || defaultChainId)
const nativeCurrency = NativeCurrency.onChain(chainId || defaultChainId)
const { balance: nativeBalance } = useOnChainNativeCurrencyBalance(chainId ?? defaultChainId, account)
const hasSufficientFunds = useMemo(() => {
......
......@@ -11,7 +11,6 @@ import {
UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM,
UNISWAP_WALLETCONNECT_URL,
} from 'src/features/deepLinking/constants'
import { Platform } from 'uniswap/src/features/platforms/types/Platform'
import { getValidAddress } from 'uniswap/src/utils/addresses'
import { logger } from 'utilities/src/logger/logger'
import { ScantasticParams, ScantasticParamsSchema } from 'wallet/src/features/scantastic/types'
......@@ -46,12 +45,7 @@ export async function getSupportedURI(
return undefined
}
const maybeAddress = getValidAddress({
address: uri,
platform: Platform.EVM,
withEVMChecksum: true,
log: false,
})
const maybeAddress = getValidAddress({ address: uri, withChecksum: true, log: false })
if (maybeAddress) {
return { type: URIType.Address, value: maybeAddress }
}
......@@ -129,7 +123,7 @@ function getMetamaskAddress(uri: string): Nullable<string> {
return null
}
return getValidAddress({ address: uriParts[1], platform: Platform.EVM, withEVMChecksum: true, log: false })
return getValidAddress({ address: uriParts[1], withChecksum: true, log: false })
}
// format is uniswap://scantastic?<params>
......
......@@ -47,7 +47,7 @@ describe('PrivateKeySpeedBumpModal', () => {
const continueButton = screen.getByTestId(TestID.Continue)
fireEvent.press(continueButton)
expect(mockOnClose).toHaveBeenCalled()
expect(mockPreventCloseRef.current).toBe(true)
expect(mockNavigation.navigate).toHaveBeenCalledWith(MobileScreens.ViewPrivateKeys)
})
})
......@@ -20,10 +20,10 @@ export function PrivateKeySpeedBumpModal({
navigation,
}: AppStackScreenProp<typeof ModalName.PrivateKeySpeedBumpModal>): JSX.Element | null {
const colors = useSporeColors()
const { onClose } = useReactNavigationModal()
const { onClose, preventCloseRef } = useReactNavigationModal()
const onContinue = (): void => {
onClose()
preventCloseRef.current = true
navigation.navigate(MobileScreens.ViewPrivateKeys)
}
......
......@@ -506,7 +506,6 @@ exports[`PrivateKeySpeedBumpModal renders correctly 1`] = `
testID="continue"
>
<Text
line-height-disabled="false"
maxFontSizeMultiplier={1.2}
numberOfLines={1}
onBlur={[Function]}
......@@ -523,7 +522,6 @@ exports[`PrivateKeySpeedBumpModal renders correctly 1`] = `
"fontSize": 17,
"fontWeight": "500",
"lineHeight": 21.849999999999998,
"textAlign": "center",
}
}
suppressHighlighting={true}
......
import { default as React, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { TextInput as NativeTextInput, StyleSheet } from 'react-native'
import { KeyboardAvoidingView, TextInput as NativeTextInput, StyleSheet } from 'react-native'
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import { KeyboardStickyView } from 'react-native-keyboard-controller'
import { useDispatch } from 'react-redux'
import { AppStackScreenProp } from 'src/app/navigation/types'
import { navigateBackFromEditingWallet } from 'src/components/Settings/EditWalletModal/EditWalletNavigation'
......@@ -18,6 +17,7 @@ import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { sanitizeAddressText } from 'uniswap/src/utils/addresses'
import { shortenAddress } from 'utilities/src/addresses'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard/dismissNativeKeyboard'
import { isIOS } from 'utilities/src/platform'
import { NICKNAME_MAX_LENGTH } from 'wallet/src/constants/accounts'
import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga'
import { useDisplayName } from 'wallet/src/features/wallet/hooks'
......@@ -72,7 +72,12 @@ export function EditLabelSettingsModal({
{/* This GestureDetector is used to consume all pan gestures and prevent
keyboard from flickering (see https://github.com/Uniswap/universe/pull/8242) */}
<GestureDetector gesture={Gesture.Pan()}>
<Flex style={styles.base}>
<KeyboardAvoidingView
enabled
behavior={isIOS ? 'padding' : undefined}
contentContainerStyle={styles.expand}
style={styles.base}
>
<BackHeader alignment="center" mx="$spacing16" pt="$spacing16" onPressBack={onPressBack}>
<Text variant="body1">{t('settings.setting.wallet.action.editLabel')}</Text>
</BackHeader>
......@@ -117,15 +122,13 @@ export function EditLabelSettingsModal({
</Flex>
)}
</Flex>
<KeyboardStickyView>
<Flex row alignSelf="stretch">
<Button emphasis="primary" variant="branded" onPress={onPressSaveChanges}>
{t('settings.setting.wallet.editLabel.save')}
</Button>
</Flex>
</KeyboardStickyView>
<Flex row alignSelf="stretch">
<Button emphasis="primary" variant="branded" onPress={onPressSaveChanges}>
{t('settings.setting.wallet.editLabel.save')}
</Button>
</Flex>
</Flex>
</Flex>
</KeyboardAvoidingView>
</GestureDetector>
</Modal>
)
......@@ -136,4 +139,7 @@ const styles = StyleSheet.create({
flex: 1,
justifyContent: 'flex-end',
},
expand: {
flexGrow: 1,
},
})
import { default as React, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet } from 'react-native'
import { KeyboardAvoidingView, StyleSheet } from 'react-native'
import ContextMenu from 'react-native-context-menu-view'
import { KeyboardAvoidingView } from 'react-native-keyboard-controller'
import { navigate } from 'src/app/navigation/rootNavigation'
import { AppStackScreenProp } from 'src/app/navigation/types'
import { navigateBackFromEditingWallet } from 'src/components/Settings/EditWalletModal/EditWalletNavigation'
import { BackHeader } from 'src/components/layout/BackHeader'
import { useReactNavigationModal } from 'src/components/modals/useReactNavigationModal'
import { navigateBackFromEditingWallet } from 'src/components/Settings/EditWalletModal/EditWalletNavigation'
import { Flex, Text } from 'ui/src'
import { Ellipsis } from 'ui/src/components/icons'
import { Modal } from 'uniswap/src/components/modals/Modal'
......@@ -76,6 +75,7 @@ export function EditProfileSettingsModal({
<Modal fullScreen name={ModalName.EditProfileSettingsModal} onClose={onClose}>
<KeyboardAvoidingView
behavior={isIOS ? 'padding' : undefined}
contentContainerStyle={styles.expand}
// Disable the keyboard avoiding view when the modals are open, otherwise background elements will shift up when the user is editing their username
enabled={!showDeleteUnitagModal && !showChangeUnitagModal}
style={styles.base}
......@@ -140,4 +140,7 @@ const styles = StyleSheet.create({
flex: 1,
justifyContent: 'flex-end',
},
expand: {
flexGrow: 1,
},
})
......@@ -188,7 +188,7 @@ export function ManageWalletsModal({ route }: AppStackScreenProp<typeof ModalNam
/>
</Flex>
<Flex row pb="$padding20" pt="$padding12">
<Button lineHeightDisabled variant="critical" emphasis="secondary" onPress={onRemoveWallet}>
<Button variant="critical" emphasis="secondary" onPress={onRemoveWallet}>
{t('settings.setting.wallet.action.remove')}
</Button>
</Flex>
......
......@@ -110,11 +110,7 @@ export const SettingsRow = memo(
if (onToggle) {
return
} else if (screen) {
/* eslint-disable @typescript-eslint/no-explicit-any */
// Type assignment to `any` is a workaround until we figure out how to
// properly type screen param. `navigate` function also brings some issues,
// where it accepts other screen's params, and not throws an error on required ones.
navigation.navigate(screen as any, screenProps)
navigation.navigate(screen, screenProps)
} else if (navigationModal) {
navigate(navigationModal, navigationProps)
} else if (externalLink) {
......
......@@ -140,64 +140,7 @@ exports[`AccountCardItem renders correctly 1`] = `
"width": 32,
}
}
>
<View
style={
{
"height": 32,
"width": 32,
}
}
>
<skCircle
color="#00C3A01F"
cx={16}
cy={16}
r={16}
/>
<skGroup
transform={
[
{
"translateX": 5.333333333333334,
},
{
"translateY": 5.333333333333334,
},
]
}
>
<skGroup
transform={
[
{
"scale": 0.4444444444444444,
},
]
}
>
<skPath
clip-rule="evenodd"
color="#00C3A0"
end={1}
fillType="evenOdd"
path="M20.9454 0.0175773C22.4936 -0.167833 23.763 1.13917 23.763 2.72944V21.3625C23.763 22.9527 22.4993 24.2419 20.9405 24.2419L2.67549 24.2419C1.11665 24.2419 -0.16452 22.9469 0.0172307 21.3675C1.30306 10.1936 9.99222 1.32931 20.9454 0.0175773Z"
start={0}
strokeWidth={1}
/>
<skPath
clip-rule="evenodd"
color="#00C3A0"
end={1}
fillType="evenOdd"
path="M27.0546 47.9824C25.5064 48.1678 24.237 46.8608 24.237 45.2706V26.6375C24.237 25.0473 25.5007 23.7581 27.0595 23.7581L45.3245 23.7581C46.8833 23.7581 48.1645 25.0531 47.9828 26.6325C46.6969 37.8064 38.0078 46.6707 27.0546 47.9824Z"
start={0}
strokeWidth={1}
/>
</skGroup>
</skGroup>
</View>
</View>
/>
</View>
</View>
<View
......
import { memo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { KeyboardAvoidingView } from 'react-native-keyboard-controller'
import { KeyboardAvoidingView } from 'react-native'
import { Flex, Text, TouchableArea, flexStyles } from 'ui/src'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { SearchModalNoQueryList } from 'uniswap/src/features/search/SearchModal/SearchModalNoQueryList'
......
import React, { PropsWithChildren, useState } from 'react'
import { ScrollView, ScrollViewProps, StyleSheet } from 'react-native'
import { KeyboardAvoidingView } from 'react-native-keyboard-controller'
import { KeyboardAvoidingView, ScrollView, ScrollViewProps, StyleSheet } from 'react-native'
import { Screen, ScreenProps } from 'src/components/layout/Screen'
import { Flex, flexStyles } from 'ui/src'
import { spacing } from 'ui/src/theme'
import { useKeyboardLayout } from 'uniswap/src/utils/useKeyboardLayout'
import { isAndroid, isIOS } from 'utilities/src/platform'
import { isIOS } from 'utilities/src/platform'
type OnboardingScreenProps = ScreenProps & {
header?: JSX.Element
......@@ -26,14 +25,19 @@ export function SafeKeyboardScreen({
const keyboard = useKeyboardLayout()
const compact = keyboard.isVisible && keyboard.containerHeight !== 0
const containerStyle = compact ? styles.compact : styles.expand
// This makes sure this component behaves just like `behavior="padding"` when
// there's enough space on the screen to show all components.
const minHeight = minHeightWhenKeyboardExpanded && compact ? keyboard.containerHeight - footerHeight : 0
return (
<Screen {...screenProps} noInsets={isAndroid}>
<KeyboardAvoidingView behavior={isIOS ? 'padding' : 'height'} style={styles.base}>
<Screen {...screenProps}>
<KeyboardAvoidingView
behavior={isIOS ? 'padding' : 'height'}
contentContainerStyle={containerStyle}
style={styles.base}
>
{header}
<ScrollView
keyboardDismissMode={keyboardDismissMode}
......@@ -41,7 +45,7 @@ export function SafeKeyboardScreen({
contentContainerStyle={flexStyles.grow}
keyboardShouldPersistTaps="handled"
>
<Flex minHeight={minHeight} px="$spacing16" style={[styles.expand, styles.container]}>
<Flex minHeight={minHeight} px="$spacing16" style={[containerStyle, styles.container]}>
{children}
</Flex>
</ScrollView>
......@@ -66,6 +70,9 @@ const styles = StyleSheet.create({
flex: 1,
justifyContent: 'flex-end',
},
compact: {
flexGrow: 0,
},
container: {
paddingBottom: spacing.spacing12,
},
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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