ci(release): publish latest release

parent 89851955
* @uniswap/web-admins
Excited to share some new updates! Here’s what’s new:
Expanded support for NFTs — In addition to mainnet Ethereum, you are now able to see NFTs across all other networks that we currently support, including: Polygon, Arbitrum, Optimism, Base, BSC, and Blast.
Improved Unicons — We gave your wallet’s unique Unicon a makeover. Check out the rest of your accounts to see your upgraded icons.
IPFS hash of the deployment:
- CIDv0: `QmQgjnjsbdgihYv6dHDgLpnXBkTwSwi3bthr3YLBgry9We`
- CIDv1: `bafybeibc3s5567p7ggbzhvrxuqztosya2qdsixkjlre5onkgbmkiipjba4`
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://bafybeibc3s5567p7ggbzhvrxuqztosya2qdsixkjlre5onkgbmkiipjba4.ipfs.dweb.link/
- https://bafybeibc3s5567p7ggbzhvrxuqztosya2qdsixkjlre5onkgbmkiipjba4.ipfs.cf-ipfs.com/
- [ipfs://QmQgjnjsbdgihYv6dHDgLpnXBkTwSwi3bthr3YLBgry9We/](ipfs://QmQgjnjsbdgihYv6dHDgLpnXBkTwSwi3bthr3YLBgry9We/)
## 5.31.0 (2024-06-05)
### Features
* **web:** add utm tags to copyLink buttons (#8556) 7a70df6
* **web:** Only show identicon in send speedbump if ENS or Unitag (#8475) 5fea09e
* **web:** use max-image-preview for nft SEO (#8557) caf4d41
### Bug Fixes
* **web:** Correctly route back to previous page on AddLiq page (#8523) 21a8143
* **web:** don't disable swap settings for unconnected chains [staging] (#8653) b9731e2
* **web:** fix broken translations (#8581) 5a86cb6
* **web:** fix translations not loading (#8503) 12acb23
* **web:** improve connection loading/error state (#8533) 9371fb3
* **web:** properly filter buy/sell txs on TDP (#8511) 28ac0ef
* **web:** round Send input if greatern than max decimals allowed (#8525) f56c514
* **web:** update USDC address on Celo Alfajores testnet (#8484) d265731
* **web:** update useOnClickOutside to handle tooltips [staging] (#8705) 1a4610f
* **web:** use prev sitemaps for sitemap generation.. (#8524) b72a23f
### Continuous Integration
* **web:** update sitemaps ff410fb
mobile/1.28.2
\ No newline at end of file
web/5.31.0
\ No newline at end of file
......@@ -14,6 +14,7 @@ ignores: [
"@amplitude/analytics-react-native",
"@react-native-masked-view/masked-view",
"@react-native-firebase/app-check",
"@shopify/react-native-skia",
"react-native-image-colors",
"react-native-restart",
# Dependencies that depcheck thinks are missing but are actually present or never used
......
......@@ -96,11 +96,10 @@ And select the version that pops up.
Taken from [RN instructions](https://reactnative.dev/docs/environment-setup?guide=native&platform=android)
```
brew tap homebrew/cask-versions
brew install --cask zulu17
brew install --cask zulu@17
# Get path to where cask was installed to double-click installer
brew info --cask zulu17
brew info --cask zulu@17
```
Add the following to your .rc file
......@@ -176,18 +175,7 @@ These are some tools you might want to familiarize yourself with to understand t
## Migrations
We use `redux-persist` to persist Redux state between user sessions. When the Redux state schema is altered, a migration may be needed to transfer the existing persisted state to the new Redux schema. Failing to define a migration results in the app defaulting to the persisted schema, which will very likely cause `undefined` errors because the code has references to Redux state properties that were dropped in favor the persisted schema.
### When to define a migration
Anytime a required property is added or any property is renamed or deleted to/from Redux state. Migrations are not necessary when optional properties are added to an existing slice. Make sure to always add new required properties to the `schema.ts` file as well.
### How to migrate
1. Increment the `version` of `persistConfig` defined within `store.ts`
2. Create a migration function within `migrations.ts`. The migration key should be the same as the `version` defined in the previous step
3. Write a test for your migration within `migrations.test.ts`
4. Create a new schema within `schema.ts` and ensure it is being exported by the `getSchema` function at the bottom of the file
We use `redux-persist` to persist the Redux state between user sessions. Most of this state is shared between the mobile app and the extension. Please review the [Wallet Migrations README](../../packages/wallet/src/state//README.md) for details on how to write migrations when you add or remove anything from the Redux state structure.
## Troubleshooting
......
......@@ -136,12 +136,12 @@ android {
}
beta {
applicationIdSuffix ".beta"
versionName "1.28.2"
versionName "1.28"
dimension "variant"
}
prod {
dimension "variant"
versionName "1.28.2"
versionName "1.28"
}
}
......
......@@ -646,9 +646,6 @@ PODS:
- BoringSSL-GRPC/Interface (0.0.24)
- DoubleConversion (1.1.6)
- EthersRS (0.0.5)
- EXAV (13.10.5):
- ExpoModulesCore
- ReactCommon/turbomodule/core
- EXBarCodeScanner (12.9.3):
- EXImageLoader
- ExpoModulesCore
......@@ -2042,7 +2039,6 @@ DEPENDENCIES:
- boost (from `../../../node_modules/react-native/third-party-podspecs/boost.podspec`)
- DoubleConversion (from `../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- "EthersRS (from `../../../node_modules/@uniswap/ethers-rs-mobile`)"
- EXAV (from `../../../node_modules/expo-av/ios`)
- EXBarCodeScanner (from `../../../node_modules/expo-barcode-scanner/ios`)
- EXFont (from `../../../node_modules/expo-font/ios`)
- EXImageLoader (from `../../../node_modules/expo-image-loader/ios`)
......@@ -2196,8 +2192,6 @@ EXTERNAL SOURCES:
:podspec: "../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
EthersRS:
:path: "../../../node_modules/@uniswap/ethers-rs-mobile"
EXAV:
:path: "../../../node_modules/expo-av/ios"
EXBarCodeScanner:
:path: "../../../node_modules/expo-barcode-scanner/ios"
EXFont:
......@@ -2408,7 +2402,6 @@ SPEC CHECKSUMS:
BoringSSL-GRPC: 3175b25143e648463a56daeaaa499c6cb86dad33
DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953
EthersRS: 56b70e73d22d4e894b7e762eef1129159bcd3135
EXAV: 07e50f934907fa8274dd06fbcd20ee4b9478c619
EXBarCodeScanner: d59fd943cebee3f913ebf4ffde0d05d344da8b78
EXFont: f20669cb266ef48b004f1eb1f2b20db96cd1df9f
EXImageLoader: 55080616b2fe9da19ef8c7f706afd9814e279b6b
......
......@@ -2536,7 +2536,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.28.2;
MARKETING_VERSION = 1.28;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets;
......@@ -2628,7 +2628,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.28.2;
MARKETING_VERSION = 1.28;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets;
......@@ -2713,7 +2713,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.28.2;
MARKETING_VERSION = 1.28;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension;
......@@ -2799,7 +2799,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.28.2;
MARKETING_VERSION = 1.28;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension;
......@@ -2873,7 +2873,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.28.2;
MARKETING_VERSION = 1.28;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3103,7 +3103,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.28.2;
MARKETING_VERSION = 1.28;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension;
......@@ -3207,7 +3207,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.28.2;
MARKETING_VERSION = 1.28;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3278,7 +3278,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.28.2;
MARKETING_VERSION = 1.28;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension;
......
......@@ -9,17 +9,6 @@ import { localizeMock as mockRNLocalize } from 'react-native-localize/mock'
import { AppearanceSettingType } from 'wallet/src/features/appearance/slice'
import { mockLocalizationContext } from 'wallet/src/test/mocks/utils'
// avoids polluting console in test runs, while keeping important log levels
global.console = {
...console,
// uncomment to ignore a specific log level
log: jest.fn(),
debug: jest.fn(),
info: jest.fn(),
// warn: jest.fn(),
// error: jest.fn(),
}
// Mock Sentry crash reporting
jest.mock('@sentry/react-native', () => ({
init: () => jest.fn(),
......
......@@ -97,7 +97,6 @@
"dayjs": "1.11.7",
"ethers": "5.7.2",
"expo": "50.0.15",
"expo-av": "13.10.5",
"expo-barcode-scanner": "12.9.3",
"expo-blur": "12.9.2",
"expo-camera": "14.1.2",
......
......@@ -70,7 +70,7 @@ import { selectFavoriteTokens } from 'wallet/src/features/favorites/selectors'
import { useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks'
import { LocalizationContextProvider } from 'wallet/src/features/language/LocalizationContext'
import { useCurrentLanguageInfo } from 'wallet/src/features/language/hooks'
import { updateLanguage } from 'wallet/src/features/language/slice'
import { syncAppWithDeviceLanguage } from 'wallet/src/features/language/slice'
import { clearNotificationQueue } from 'wallet/src/features/notifications/slice'
import { TransactionHistoryUpdater } from 'wallet/src/features/transactions/TransactionHistoryUpdater'
import { Account } from 'wallet/src/features/wallet/accounts/types'
......@@ -255,11 +255,11 @@ function AppInner(): JSX.Element {
useEffect(() => {
if (allowAnalytics) {
appsFlyer.startSdk()
logger.info('AppsFlyer', 'status', 'started')
logger.debug('AppsFlyer', 'status', 'started')
} else {
appsFlyer.stop(!allowAnalytics, (res: unknown) => {
if (typeof res === 'string' && res === 'Success') {
logger.info('AppsFlyer', 'status', 'stopped')
logger.debug('AppsFlyer', 'status', 'stopped')
} else {
logger.warn(
'AppsFlyer',
......@@ -273,7 +273,7 @@ function AppInner(): JSX.Element {
useEffect(() => {
dispatch(clearNotificationQueue()) // clear all in-app toasts on app start
dispatch(updateLanguage(null))
dispatch(syncAppWithDeviceLanguage())
}, [dispatch])
useEffect(() => {
......
......@@ -104,6 +104,7 @@ import {
} from 'wallet/src/features/wallet/accounts/types'
import { initialWalletState, SwapProtectionSetting } from 'wallet/src/features/wallet/slice'
import { createMigrate } from 'wallet/src/state/createMigrate'
import { getAllKeysOfNestedObject } from 'wallet/src/state/testUtils'
import {
fiatPurchaseTransactionInfo,
signerMnemonicAccount,
......@@ -124,26 +125,6 @@ const fiatOnRampTxDetailsFailed = transactionDetails({
}),
})
// helps with object assignment
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const getAllKeysOfNestedObject = (obj: any, prefix = ''): string[] => {
const keys = Object.keys(obj)
if (!keys.length && prefix !== '') {
return [prefix.slice(0, -1)]
}
return keys.reduce<string[]>((res, el) => {
if (Array.isArray(obj[el])) {
return [...res]
}
if (typeof obj[el] === 'object' && obj[el] !== null) {
return [...res, ...getAllKeysOfNestedObject(obj[el], prefix + el + '.')]
}
return [...res, prefix + el]
}, [])
}
describe('Redux state migrations', () => {
it('is able to perform all migrations starting from the initial schema', async () => {
const initialSchemaStub = {
......@@ -204,6 +185,10 @@ describe('Redux state migrations', () => {
},
}
if (!migratedSchema) {
throw new Error('Migrated schema is undefined')
}
const migratedSchemaKeys = new Set(getAllKeysOfNestedObject(migratedSchema))
const latestSchemaKeys = new Set(getAllKeysOfNestedObject(getSchema()))
const initialStateKeys = new Set(getAllKeysOfNestedObject(initialState))
......
......@@ -19,6 +19,7 @@ import {
} from 'wallet/src/features/transactions/types'
import { Account, AccountType } from 'wallet/src/features/wallet/accounts/types'
import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice'
import { removeWalletIsUnlockedState } from 'wallet/src/state/sharedMigrations'
export const OLD_DEMO_ACCOUNT_ADDRESS = '0xdd0E380579dF30E38524F9477808d9eE37E2dEa6'
......@@ -876,10 +877,7 @@ export const migrations = {
return newState
},
63: function removeWalletIsUnlockedState(state: any) {
const newState = { ...state }
delete newState.wallet.isUnlocked
return newState
},
63: removeWalletIsUnlockedState,
}
export const MOBILE_STATE_VERSION = 63
......@@ -23,7 +23,7 @@ import { ElementName, MobileEventName, ModalName } from 'uniswap/src/features/te
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { isAndroid } from 'uniswap/src/utils/platform'
import { isAndroid } from 'utilities/src/platform'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { ActionSheetModal, MenuItemProp } from 'wallet/src/components/modals/ActionSheetModal'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
......
import React from 'react'
import React, { useCallback } from 'react'
import { AccountSwitcherModal } from 'src/app/modals/AccountSwitcherModal'
import { ExperimentsModal } from 'src/app/modals/ExperimentsModal'
import { ExploreModal } from 'src/app/modals/ExploreModal'
......@@ -15,14 +15,22 @@ import { LockScreenModal } from 'src/features/authentication/LockScreenModal'
import { ExchangeTransferModal } from 'src/features/fiatOnRamp/ExchangeTransferModal'
import { FiatOnRampAggregatorModal } from 'src/features/fiatOnRamp/FiatOnRampAggregatorModal'
import { FiatOnRampModal } from 'src/features/fiatOnRamp/FiatOnRampModal'
import { closeModal } from 'src/features/modals/modalSlice'
import { ExtensionWaitlistModal } from 'src/features/scantastic/ExtensionWaitlistModal'
import { ScantasticModal } from 'src/features/scantastic/ScantasticModal'
import { ReceiveCryptoModal } from 'src/screens/ReceiveCryptoModal'
import { SettingsFiatCurrencyModal } from 'src/screens/SettingsFiatCurrencyModal'
import { SettingsLanguageModal } from 'src/screens/SettingsLanguageModal'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { SettingsLanguageModal } from 'wallet/src/components/settings/language/SettingsLanguageModal'
import { useAppDispatch } from 'wallet/src/state'
export function AppModals(): JSX.Element {
const dispatch = useAppDispatch()
const onCloseLanguageModal = useCallback(() => {
dispatch(closeModal({ name: ModalName.LanguageSelector }))
}, [dispatch])
return (
<>
<LazyModalRenderer name={ModalName.ExchangeTransferModal}>
......@@ -84,7 +92,7 @@ export function AppModals(): JSX.Element {
</LazyModalRenderer>
<LazyModalRenderer name={ModalName.LanguageSelector}>
<SettingsLanguageModal />
<SettingsLanguageModal onClose={onCloseLanguageModal} />
</LazyModalRenderer>
<LazyModalRenderer name={ModalName.FiatCurrencySelector}>
......
......@@ -31,7 +31,7 @@ import { borderRadii, fonts } from 'ui/src/theme'
import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { isAndroid, isIOS } from 'uniswap/src/utils/platform'
import { isAndroid, isIOS } from 'utilities/src/platform'
import { useHighestBalanceNativeCurrencyId } from 'wallet/src/features/dataApi/balances'
import { prepareSwapFormState } from 'wallet/src/features/transactions/swap/utils'
import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks'
......
......@@ -3,7 +3,7 @@ import { isRejectedWithValue } from '@reduxjs/toolkit'
import * as Sentry from '@sentry/react'
import { MMKV } from 'react-native-mmkv'
import { Storage, persistReducer, persistStore } from 'redux-persist'
import { migrations } from 'src/app/migrations'
import { MOBILE_STATE_VERSION, migrations } from 'src/app/migrations'
import { isNonJestDev } from 'utilities/src/environment'
import { logger } from 'utilities/src/logger/logger'
import { fiatOnRampAggregatorApi, fiatOnRampApi } from 'wallet/src/features/fiatOnRamp/api'
......@@ -66,7 +66,7 @@ export const persistConfig = {
key: 'root',
storage: reduxStorage,
whitelist,
version: 63,
version: MOBILE_STATE_VERSION,
migrate: createMigrate(migrations),
}
......
......@@ -4,7 +4,7 @@ import { useLineChartDatetime } from 'react-native-wagmi-charts'
import { AnimatedText } from 'src/components/text/AnimatedText'
import { Flex, useSporeColors } from 'ui/src'
import { AnimatedCaretChange } from 'ui/src/components/icons'
import { isAndroid } from 'uniswap/src/utils/platform'
import { isAndroid } from 'utilities/src/platform'
import { FiatCurrency } from 'wallet/src/features/fiatCurrency/constants'
import { useAppFiatCurrency, useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks'
import { useCurrentLocale } from 'wallet/src/features/language/hooks'
......
......@@ -112,8 +112,8 @@ describe(useLineChartPrice, () => {
it('returns currentSpot if it is provided', async () => {
const spotPrice = makeMutable(1)
const { result } = renderHookWithProviders(useLineChartPrice, {
initialProps: [spotPrice],
const { result } = renderHookWithProviders(() => useLineChartPrice(spotPrice), {
initialProps: spotPrice,
})
expect(result.current).toEqual({
......@@ -149,7 +149,7 @@ describe(useLineChartPrice, () => {
it('returns active cursor price even if currentSpot and data are provided', async () => {
mockCursorPrice('3')
const { result } = renderHookWithProviders(useLineChartPrice, {
initialProps: [makeMutable(4)],
initialProps: makeMutable(4),
})
expect(result.current).toEqual({
......@@ -162,7 +162,7 @@ describe(useLineChartPrice, () => {
it('updates returned active cursor price when it changes', async () => {
mockCursorPrice('1')
const { result } = renderHookWithProviders(useLineChartPrice, {
initialProps: [makeMutable(4)],
initialProps: makeMutable(4),
})
expect(result.current).toEqual(
......
......@@ -2,7 +2,7 @@ import React from 'react'
import { useTranslation } from 'react-i18next'
import { useBiometricName } from 'src/features/biometrics/hooks'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { isAndroid } from 'uniswap/src/utils/platform'
import { isAndroid } from 'utilities/src/platform'
import {
WarningModal,
WarningModalProps,
......
......@@ -24,7 +24,7 @@ import {
import { zIndices } from 'ui/src/theme'
import { CurrencyId } from 'uniswap/src/types/currency'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { isAndroid } from 'uniswap/src/utils/platform'
import { isAndroid } from 'utilities/src/platform'
import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard'
import { isError, isNonPollingRequestInFlight } from 'wallet/src/data/utils'
import { HiddenTokensRow } from 'wallet/src/features/portfolio/HiddenTokensRow'
......
......@@ -41,10 +41,12 @@ export function TokenDetailsActionButtons({
onPressBuy,
onPressSell,
tokenColor,
userHasBalance,
}: {
onPressBuy: () => void
onPressSell: () => void
tokenColor?: Maybe<string>
userHasBalance: boolean
}): JSX.Element {
const { t } = useTranslation()
......@@ -64,12 +66,14 @@ export function TokenDetailsActionButtons({
tokenColor={tokenColor}
onPress={onPressBuy}
/>
<CTAButton
element={ElementName.Sell}
title={t('common.button.sell')}
tokenColor={tokenColor}
onPress={onPressSell}
/>
{userHasBalance && (
<CTAButton
element={ElementName.Sell}
title={t('common.button.sell')}
tokenColor={tokenColor}
onPress={onPressSell}
/>
)}
</Flex>
)
}
......@@ -10,7 +10,7 @@ import { getAuthMethod } from 'src/features/telemetry/utils'
import { getFullAppVersion } from 'src/utils/version'
import { useIsDarkMode } from 'ui/src'
import { MobileUserPropertyName, setUserProperty } from 'uniswap/src/features/telemetry/user'
import { isAndroid } from 'uniswap/src/utils/platform'
import { isAndroid } from 'utilities/src/platform'
// eslint-disable-next-line no-restricted-imports
import { analytics } from 'utilities/src/telemetry/analytics/analytics'
import { useAppFiatCurrency } from 'wallet/src/features/fiatCurrency/hooks'
......
......@@ -87,7 +87,6 @@ export function WalletConnectModal({
setShouldFreezeCamera(true)
Alert.alert(
t('walletConnect.error.unsupported.title'),
// TODO(EXT-495): Add Scantastic product name here when ready
t('walletConnect.error.unsupported.message'),
[
{
......
......@@ -100,25 +100,22 @@ exports[`TokenItem renders without error 1`] = `
testID="token-logo"
>
<Image
resizeMode="contain"
height={40}
source={
{
"uri": "https://loremflickr.com/640/480",
}
}
style={
[
{
"backgroundColor": "#2222220D",
"borderColor": "#2222220D",
"borderRadius": 20,
"borderWidth": 0.5,
"height": 40,
"width": 40,
},
]
{
"aspectRatio": undefined,
"backgroundColor": "#2222220D",
"borderRadius": 20,
"flex": undefined,
}
}
testID="token-image"
testID="img-token-image"
width={40}
/>
</View>
</View>
......
......@@ -30,7 +30,7 @@ import { Jiggly } from 'ui/src/animations'
import { ONBOARDING_LANDING_DARK, ONBOARDING_LANDING_LIGHT, UNISWAP_APP_ICON } from 'ui/src/assets'
import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions'
import { imageSizes } from 'ui/src/theme'
import { isAndroid } from 'uniswap/src/utils/platform'
import { isAndroid } from 'utilities/src/platform'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { useTimeout } from 'utilities/src/time/timing'
import { Language } from 'wallet/src/features/language/constants'
......
......@@ -14,7 +14,7 @@ import { removePendingSession } from 'src/features/walletConnect/walletConnectSl
import { Flex, useDeviceInsets, useSporeColors } from 'ui/src'
import { GQLQueries } from 'uniswap/src/data/graphql/uniswap-data-api/queries'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { isAndroid } from 'uniswap/src/utils/platform'
import { isAndroid } from 'utilities/src/platform'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
import { useActivityData } from 'wallet/src/features/activity/useActivityData'
......
......@@ -13,7 +13,7 @@ import { Flex, Text, useDeviceInsets, useSporeColors } from 'ui/src'
import { NoTransactions } from 'ui/src/components/icons'
import { GQLQueries } from 'uniswap/src/data/graphql/uniswap-data-api/queries'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { isAndroid } from 'uniswap/src/utils/platform'
import { isAndroid } from 'utilities/src/platform'
import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
import { useFormattedTransactionDataForFeed } from 'wallet/src/features/activity/hooks'
......
......@@ -8,7 +8,7 @@ import { TAB_BAR_HEIGHT, TabProps } from 'src/components/layout/TabHelpers'
import { Flex, useDeviceInsets, useSporeColors } from 'ui/src'
import { GQLQueries } from 'uniswap/src/data/graphql/uniswap-data-api/queries'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { isAndroid } from 'uniswap/src/utils/platform'
import { isAndroid } from 'utilities/src/platform'
import { NftsList } from 'wallet/src/components/nfts/NftsList'
import { NFTItem } from 'wallet/src/features/nfts/types'
......
......@@ -10,8 +10,8 @@ import { ElementName, ModalName, UnitagEventName } from 'uniswap/src/features/te
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { useUnitagUpdater } from 'uniswap/src/features/unitags/context'
import { UnitagErrorCodes } from 'uniswap/src/features/unitags/types'
import { isIOS } from 'uniswap/src/utils/platform'
import { logger } from 'utilities/src/logger/logger'
import { isIOS } from 'utilities/src/platform'
import { useAsyncData } from 'utilities/src/react/hooks'
import { TextInput } from 'wallet/src/components/input/TextInput'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
......
......@@ -7,8 +7,8 @@ import { FeatureFlags, WALLET_FEATURE_FLAG_NAMES } from 'uniswap/src/features/ga
import { Statsig } from 'uniswap/src/features/gating/sdk/statsig'
import { MobileEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { isAndroid } from 'uniswap/src/utils/platform'
import { logger } from 'utilities/src/logger/logger'
import { isAndroid } from 'utilities/src/platform'
import { ONE_DAY_MS, ONE_SECOND_MS } from 'utilities/src/time/time'
import { finalizeTransaction } from 'wallet/src/features/transactions/slice'
import { TransactionStatus, TransactionType } from 'wallet/src/features/transactions/types'
......@@ -81,19 +81,19 @@ function* maybeRequestAppRating() {
const shouldPrompt = consecutiveSwapsCondition && (hasNeverPrompted || reminderCondition)
if (!shouldPrompt) {
logger.debug(
'appRating',
'maybeRequestAppRating',
`Skipping app rating (lastPrompt: ${appRatingPromptedMs}, lastProvided: ${appRatingProvidedMs}, consecutiveSwapsCondition: ${consecutiveSwapsCondition})`
)
logger.debug('appRating', 'maybeRequestAppRating', 'Skipping app rating', {
lastPrompt: appRatingPromptedMs,
lastProvided: appRatingProvidedMs,
consecutiveSwapsCondition,
})
return
}
logger.info(
'appRating',
'maybeRequestAppRating',
`Requesting app rating (lastPrompt: ${appRatingPromptedMs}, lastProvided: ${appRatingProvidedMs}, consecutiveSwapsCondition: ${consecutiveSwapsCondition})`
)
logger.info('appRating', 'maybeRequestAppRating', 'Requesting app rating', {
lastPrompt: appRatingPromptedMs,
lastProvided: appRatingProvidedMs,
consecutiveSwapsCondition,
})
// Alerts
const shouldShowNativeReviewModal = yield* call(openRatingOptionsAlert)
......
......@@ -5,7 +5,7 @@ import { useLockScreenContext } from 'src/features/authentication/lockScreenCont
import { useBiometricPrompt } from 'src/features/biometrics/hooks'
import { Flex, TouchableArea, useDeviceDimensions, useDeviceInsets, useIsDarkMode } from 'ui/src'
import { UNISWAP_LOGO_LARGE } from 'ui/src/assets'
import { isAndroid } from 'uniswap/src/utils/platform'
import { isAndroid } from 'utilities/src/platform'
export const SPLASH_SCREEN = { uri: 'SplashScreen' }
......
......@@ -8,7 +8,7 @@ import { useAppSelector } from 'src/app/hooks'
import { BiometricAuthenticationStatus, tryLocalAuthenticate } from 'src/features/biometrics'
import { useBiometricContext } from 'src/features/biometrics/context'
import { BiometricSettingsState } from 'src/features/biometrics/slice'
import { isAndroid } from 'uniswap/src/utils/platform'
import { isAndroid } from 'utilities/src/platform'
import { useAsyncData } from 'utilities/src/react/hooks'
type TriggerArgs<T> = {
......
import React, { useEffect, useRef, useState } from 'react'
import {
Keyboard,
LayoutChangeEvent,
LayoutRectangle,
TextInput as NativeTextInput,
StyleSheet,
} from 'react-native'
import { Keyboard, TextInput as NativeTextInput } from 'react-native'
import InputWithSuffix from 'src/features/import/InputWithSuffix'
import { Flex, Text, useMedia } from 'ui/src'
import { fonts } from 'ui/src/theme'
......@@ -51,7 +45,6 @@ export function GenericImportForm({
shouldUseMinHeight = true,
}: Props): JSX.Element {
const [focused, setFocused] = useState(false)
const [layout, setLayout] = useState<LayoutRectangle | null>()
const textInputRef = useRef<NativeTextInput>(null)
const isKeyboardVisibleRef = useRef(false)
const media = useMedia()
......@@ -97,9 +90,7 @@ export function GenericImportForm({
const INPUT_MIN_HEIGHT = 120
const INPUT_MIN_HEIGHT_SHORT = 90
// Absolutely positioned paste button needs top padding to be vertically centered on bottom border of Flex
const PASTE_BUTTON_TOP_PADDING = INPUT_MIN_HEIGHT / 2 + 4
const SHORT_PASTE_BUTTON_TOP_PADDING = INPUT_MIN_HEIGHT_SHORT / 2 - 12
const showError = errorMessage && value && (liveCheck || !focused)
return (
<Trace section={SectionName.ImportAccountForm}>
......@@ -119,7 +110,7 @@ export function GenericImportForm({
minHeight: shouldUseMinHeight ? INPUT_MIN_HEIGHT_SHORT : undefined,
}}
backgroundColor="$surface1"
borderColor="$surface3"
borderColor={showError ? '$statusCritical' : '$surface3'}
borderRadius="$rounded20"
borderWidth={1}
minHeight={shouldUseMinHeight ? INPUT_MIN_HEIGHT : undefined}
......@@ -134,7 +125,6 @@ export function GenericImportForm({
inputFontSize={INPUT_FONT_SIZE}
inputMaxFontSizeMultiplier={INPUT_MAX_FONT_SIZE_MULTIPLIER}
inputSuffix={inputSuffix}
layout={layout}
textAlign={textAlign}
textInputRef={textInputRef}
value={value}
......@@ -143,39 +133,21 @@ export function GenericImportForm({
onFocus={handleFocus}
onSubmitEditing={handleSubmit}
/>
{!value && (
{!value && placeholderLabel && (
<Flex
grow
row
alignItems="center"
gap="$spacing8"
centered
bottom={shouldUseMinHeight ? undefined : 0}
left="$spacing24"
position="absolute"
pt="$spacing16"
px="$spacing24"
width="100%"
onLayout={(event: LayoutChangeEvent): void => setLayout(event.nativeEvent.layout)}>
<Text
adjustsFontSizeToFit
$short={{ variant: 'body3' }}
color="$neutral2"
maxFontSizeMultiplier={INPUT_MAX_FONT_SIZE_MULTIPLIER}
numberOfLines={1}
style={styles.placeholderLabelStyle}
variant="subheading2">
py="$spacing16"
top={0}>
<Text color="$neutral2" fontSize={INPUT_FONT_SIZE} pointerEvents="none">
{placeholderLabel}
</Text>
</Flex>
)}
{!value && !shouldUseMinHeight && (
<Flex
row
alignItems="flex-end"
justifyContent="flex-end"
position="absolute"
pr="$spacing12"
pt="$spacing12"
right={0}
width="100%">
<Flex bottom={0} justifyContent="center" position="absolute" right="$spacing12" top={0}>
<PasteButton
afterClipboardReceived={afterPasteButtonPress}
beforePress={beforePasteButtonPress}
......@@ -184,12 +156,8 @@ export function GenericImportForm({
</Flex>
)}
{!value && shouldUseMinHeight && (
<Flex centered width="100%">
<Flex
$short={{ pt: SHORT_PASTE_BUTTON_TOP_PADDING }}
position="absolute"
pt={PASTE_BUTTON_TOP_PADDING}
top={0}>
<Flex centered bottom={0} height={0} left={0} position="absolute" right={0}>
<Flex position="absolute">
<PasteButton
afterClipboardReceived={afterPasteButtonPress}
beforePress={beforePasteButtonPress}
......@@ -200,7 +168,7 @@ export function GenericImportForm({
)}
</Flex>
<Flex>
{errorMessage && value && (liveCheck || !focused) && (
{showError && (
<Flex centered row gap="$spacing12">
<Text color="$statusCritical" variant="body3">
{errorMessage}
......@@ -212,9 +180,3 @@ export function GenericImportForm({
</Trace>
)
}
const styles = StyleSheet.create({
placeholderLabelStyle: {
flexShrink: 1,
},
})
import { TextInput as NativeTextInput } from 'react-native'
import { ColorTokens } from 'ui/src'
export interface InputWithSuffixProps {
alwaysShowInputSuffix?: boolean
autoCorrect: boolean
blurOnSubmit: boolean
inputAlignment: 'center' | 'flex-start'
value?: string
inputFontSize: number
inputMaxFontSizeMultiplier: number
inputSuffix?: string
inputSuffixColor?: ColorTokens
multiline?: boolean
textAlign?: 'left' | 'right' | 'center'
textInputRef: React.RefObject<NativeTextInput>
onBlur?: () => void
onFocus?: () => void
onChangeText?: (text: string) => void
onSubmitEditing?: () => void
}
import { useCallback, useRef, useState } from 'react'
import { LayoutChangeEvent } from 'react-native'
import { Flex } from 'ui/src'
import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { isIOS } from 'utilities/src/platform'
import { TextInput } from 'wallet/src/components/input/TextInput'
import { InputWithSuffixProps } from './InputWIthSuffixProps'
const EPS = 1
export default function InputWithSuffix({
alwaysShowInputSuffix = false,
value,
inputSuffix,
inputSuffixColor,
inputAlignment: inputAlignmentProp,
inputFontSize,
inputMaxFontSizeMultiplier,
multiline = true,
textAlign,
textInputRef,
...inputProps
}: InputWithSuffixProps): JSX.Element {
const textInputWidth = useRef<number>(0)
const [shouldWrapLine, setShouldWrapLine] = useState(false)
const measureMaxWidth = useCallback((e: LayoutChangeEvent) => {
textInputWidth.current = e.nativeEvent.layout.width
}, [])
const measureInputWidth = useCallback(
(e: LayoutChangeEvent) => {
if (multiline) {
const contentWidth = e.nativeEvent.layout.width
const maxContentWidth = textInputWidth.current
// Check if the input content doesn't fit in a single line
// (add EPS to avoid rounding errors)
setShouldWrapLine(contentWidth + EPS >= maxContentWidth)
}
},
[multiline]
)
const isInputEmpty = !value?.length
// On iOS use just the multiline prop to determine if the input should wrap
// On Android, wrap the input only if it's content doesn't fit in a single line
// and the input is multiline
const isMultiline = multiline && (isIOS || shouldWrapLine)
const fallbackTextInputAlignment = inputAlignmentProp === 'flex-start' ? 'left' : 'center'
const textInputAlignment = textAlign ?? fallbackTextInputAlignment
const suffix =
inputSuffix && (alwaysShowInputSuffix || (value && !value.includes(inputSuffix))) ? (
<TextInput
backgroundColor="$transparent"
color={inputSuffixColor ?? '$neutral2'}
editable={false}
fontSize={inputFontSize}
lineHeight={inputFontSize}
maxFontSizeMultiplier={inputMaxFontSizeMultiplier}
px="$none"
py="$none"
scrollEnabled={false}
textAlignVertical="bottom"
value={inputSuffix}
/>
) : null
return (
<Flex row alignItems="flex-end" justifyContent={inputAlignmentProp}>
{/*
Helper Flex to measure the max width of the input and switch to multiline if needed
(multiline input behavior on Android is weird and the input flickers when the width
of the TextInput component changes. As a workaround, we measure the max width of the input
and switch to multiline if the content doesn't fit in a single line)
*/}
<Flex row opacity={0} position="absolute" width="100%">
<Flex grow onLayout={measureMaxWidth} />
{suffix}
</Flex>
<TextInput
ref={textInputRef}
autoFocus
autoCapitalize="none"
backgroundColor="$transparent"
color="$neutral1"
flexShrink={1}
fontSize={inputFontSize}
lineHeight={inputFontSize}
maxFontSizeMultiplier={inputMaxFontSizeMultiplier}
multiline={isMultiline}
px="$none"
py="$none"
returnKeyType="done"
scrollEnabled={false}
spellCheck={false}
testID={ElementName.ImportAccountInput}
textAlign={isInputEmpty ? 'left' : textInputAlignment}
textAlignVertical={isInputEmpty ? 'center' : 'bottom'}
value={value}
onLayout={measureInputWidth}
{...inputProps}
/>
{suffix}
</Flex>
)
}
import { Flex } from 'ui/src'
import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { TextInput } from 'wallet/src/components/input/TextInput'
import { InputWithSuffixProps } from './InputWIthSuffixProps'
export default function InputWithSuffix({
alwaysShowInputSuffix = false,
value,
inputSuffix,
inputSuffixColor,
inputAlignment: inputAlignmentProp,
inputFontSize,
inputMaxFontSizeMultiplier,
multiline = true,
textAlign,
textInputRef,
...inputProps
}: InputWithSuffixProps): JSX.Element {
const isInputEmpty = !value?.length
const fallbackTextInputAlignment = inputAlignmentProp === 'flex-start' ? 'left' : 'center'
const textInputAlignment = textAlign ?? fallbackTextInputAlignment
return (
<Flex row alignItems="flex-end" justifyContent={inputAlignmentProp}>
<TextInput
ref={textInputRef}
autoFocus
autoCapitalize="none"
backgroundColor="$transparent"
color="$neutral1"
flexShrink={1}
fontSize={inputFontSize}
lineHeight={inputFontSize}
maxFontSizeMultiplier={inputMaxFontSizeMultiplier}
multiline={multiline}
px="$none"
py="$none"
returnKeyType="done"
scrollEnabled={false}
spellCheck={false}
testID={ElementName.ImportAccountInput}
textAlign={isInputEmpty ? 'left' : textInputAlignment}
textAlignVertical="bottom"
value={value}
{...inputProps}
/>
{inputSuffix && (alwaysShowInputSuffix || (value && !value.includes(inputSuffix))) ? (
<TextInput
backgroundColor="$transparent"
color={inputSuffixColor ?? '$neutral2'}
editable={false}
fontSize={inputFontSize}
lineHeight={inputFontSize}
maxFontSizeMultiplier={inputMaxFontSizeMultiplier}
multiline={multiline}
px="$none"
py="$none"
scrollEnabled={false}
textAlignVertical="bottom"
value={inputSuffix}
/>
) : null}
</Flex>
)
}
import { useCallback, useState } from 'react'
import {
LayoutRectangle,
NativeSyntheticEvent,
TextInput as NativeTextInput,
TextInputContentSizeChangeEventData,
} from 'react-native'
import { ColorTokens, Flex, useSporeColors } from 'ui/src'
import { spacing } from 'ui/src/theme'
import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { isAndroid } from 'uniswap/src/utils/platform'
import { TextInput } from 'wallet/src/components/input/TextInput'
import { NotImplementedError } from 'utilities/src/errors'
import { InputWithSuffixProps } from './InputWIthSuffixProps'
interface Props {
alwaysShowInputSuffix?: boolean
autoCorrect: boolean
blurOnSubmit: boolean
inputAlignment: 'center' | 'flex-start'
value?: string
inputFontSize: number
inputMaxFontSizeMultiplier: number
inputSuffix?: string
inputSuffixColor?: ColorTokens
multiline?: boolean
textAlign?: 'left' | 'right' | 'center'
textInputRef: React.RefObject<NativeTextInput>
layout?: LayoutRectangle | null
onBlur?: () => void
onFocus?: () => void
onChangeText?: (text: string) => void
onSubmitEditing?: () => void
}
export default function InputWithSuffix(props: Props): JSX.Element {
return isAndroid ? (
<Flex width="100%">
<Inputs {...props} layerType="foreground" />
<Inputs {...props} layerType="background" />
</Flex>
) : (
<Inputs {...props} />
)
}
function Inputs({
alwaysShowInputSuffix = false,
value,
layout,
inputSuffix,
inputSuffixColor,
inputAlignment,
inputFontSize,
inputMaxFontSizeMultiplier,
multiline = true,
textAlign,
textInputRef,
layerType,
...inputProps
}: Props & { layerType?: 'foreground' | 'background' }): JSX.Element {
const colors = useSporeColors()
const [isMultiline, setIsMultiline] = useState(false)
const handleContentSizeChange = useCallback(
(e: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => {
if (multiline && textInputRef.current) {
setIsMultiline(Math.floor(e.nativeEvent.contentSize.height / inputFontSize) > 1)
}
},
[textInputRef, inputFontSize, multiline]
)
const isInputEmpty = !value?.length
const foregroundFallbackTextAlignment =
isMultiline || inputAlignment === 'flex-start' ? 'left' : 'center'
const foregroundTextAlignment = textAlign ?? foregroundFallbackTextAlignment
const fallbackBackgroundTextAlignment = inputAlignment === 'flex-start' ? 'left' : 'center'
const backgroundTextAlignment = textAlign ?? fallbackBackgroundTextAlignment
return (
<Flex
alignItems="flex-end"
flexDirection="row"
justifyContent={inputAlignment}
width="100%"
{...(layerType === 'foreground' ? { position: 'absolute', left: 0, bottom: 0 } : {})}
{...(layerType === 'background' ? { pointerEvents: 'none' } : {})}>
{layerType === 'foreground' ? (
<TextInput
ref={textInputRef}
autoFocus
backgroundColor="$transparent"
color="$neutral1"
fontSize={inputFontSize}
justifyContent="flex-start"
lineHeight={inputFontSize}
maxFontSizeMultiplier={inputMaxFontSizeMultiplier}
multiline={multiline}
px="$none"
py="$none"
scrollEnabled={false}
textAlign={isInputEmpty ? 'left' : foregroundTextAlignment}
textAlignVertical="bottom"
value={value}
width="100%"
onContentSizeChange={handleContentSizeChange}
{...inputProps}
/>
) : (
<TextInput
autoCapitalize="none"
backgroundColor="$transparent"
color="$neutral1"
fontSize={inputFontSize}
justifyContent="center"
lineHeight={inputFontSize}
maxFontSizeMultiplier={inputMaxFontSizeMultiplier}
multiline={multiline}
px="$none"
py="$none"
returnKeyType="done"
scrollEnabled={false}
selectionColor={colors.neutral1.val}
spellCheck={false}
testID={ElementName.ImportAccountInput}
textAlign={isInputEmpty ? 'left' : backgroundTextAlignment}
textAlignVertical="bottom"
value={value}
width={value ? 'auto' : (layout?.width || 0) + spacing.spacing8}
{...(layerType === 'background'
? { opacity: 0, editable: false }
: { ...inputProps, ref: textInputRef, autoFocus: true })}
/>
)}
{inputSuffix && (alwaysShowInputSuffix || (value && !value.includes(inputSuffix))) ? (
<TextInput
backgroundColor="$transparent"
color={inputSuffixColor ?? '$neutral2'}
editable={false}
fontSize={inputFontSize}
justifyContent="center"
lineHeight={inputFontSize}
maxFontSizeMultiplier={inputMaxFontSizeMultiplier}
multiline={multiline}
px="$none"
py="$none"
scrollEnabled={false}
textAlignVertical="bottom"
value={inputSuffix}
{...(layerType === 'foreground' ? { opacity: 0 } : {})}
/>
) : null}
</Flex>
)
export default function InputWithSuffix(_props: InputWithSuffixProps): JSX.Element {
throw new NotImplementedError('InputWithSuffix component is not implemented')
}
......@@ -45,7 +45,6 @@ exports[`GenericImportForm renders a placeholder when there is no value 1`] = `
"alignItems": "flex-end",
"flexDirection": "row",
"justifyContent": "center",
"width": "100%",
}
}
>
......@@ -66,7 +65,7 @@ exports[`GenericImportForm renders a placeholder when there is no value 1`] = `
placeholderTextColor="#CECECE"
returnKeyType="done"
scrollEnabled={false}
selectionColor="#222222"
selectionColor="#CECECE"
spellCheck={false}
style={
{
......@@ -85,10 +84,10 @@ exports[`GenericImportForm renders a placeholder when there is no value 1`] = `
"borderTopRightRadius": 12,
"borderTopWidth": 1,
"color": "#222222",
"flexShrink": 1,
"fontFamily": "Basel-Book",
"fontSize": 17,
"height": "auto",
"justifyContent": "center",
"lineHeight": 17,
"minWidth": 0,
"paddingBottom": 0,
......@@ -96,7 +95,6 @@ exports[`GenericImportForm renders a placeholder when there is no value 1`] = `
"paddingRight": 0,
"paddingTop": 0,
"textAlign": "left",
"width": 8,
}
}
testID="import-account-input"
......@@ -104,30 +102,26 @@ exports[`GenericImportForm renders a placeholder when there is no value 1`] = `
/>
</View>
<View
onLayout={[Function]}
style={
{
"alignItems": "center",
"flexDirection": "row",
"flexGrow": 1,
"gap": 8,
"paddingLeft": 24,
"paddingRight": 24,
"flexDirection": "column",
"justifyContent": "center",
"left": 24,
"paddingBottom": 16,
"paddingTop": 16,
"position": "absolute",
"width": "100%",
"top": 0,
}
}
>
<Text
adjustsFontSizeToFit={true}
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
numberOfLines={1}
pointerEvents="none"
style={
{
"color": "#7D7D7D",
"flexShrink": 1,
"fontFamily": "Basel-Book",
"fontSize": 17,
"lineHeight": 24,
......@@ -142,9 +136,13 @@ exports[`GenericImportForm renders a placeholder when there is no value 1`] = `
style={
{
"alignItems": "center",
"bottom": 0,
"flexDirection": "column",
"height": 0,
"justifyContent": "center",
"width": "100%",
"left": 0,
"position": "absolute",
"right": 0,
}
}
>
......@@ -152,9 +150,7 @@ exports[`GenericImportForm renders a placeholder when there is no value 1`] = `
style={
{
"flexDirection": "column",
"paddingTop": 64,
"position": "absolute",
"top": 0,
}
}
>
......
......@@ -4,8 +4,8 @@ import { StyleSheet } from 'react-native'
import { ColorTokens, Flex, FlexProps, Logos, SpaceTokens, Text, useSporeColors } from 'ui/src'
import { TextVariantTokens, borderRadii, iconSizes, spacing } from 'ui/src/theme'
import { IAmount } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { isIOS } from 'uniswap/src/utils/platform'
import { NumberType } from 'utilities/src/format/types'
import { isIOS } from 'utilities/src/platform'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
type ListPriceProps = FlexProps & {
......
......@@ -5,7 +5,7 @@ import { FadeIn, FadeOut } from 'react-native-reanimated'
import { SHORT_SCREEN_HEADER_HEIGHT_RATIO, Screen } from 'src/components/layout/Screen'
import { AnimatedFlex, Flex, SpaceTokens, Text, useDeviceInsets, useMedia } from 'ui/src'
import { fonts } from 'ui/src/theme'
import { isIOS } from 'uniswap/src/utils/platform'
import { isIOS } from 'utilities/src/platform'
type OnboardingScreenProps = {
subtitle?: string
......
......@@ -6,7 +6,7 @@ import { FadeIn, FadeOut } from 'react-native-reanimated'
import { Screen } from 'src/components/layout/Screen'
import { AnimatedFlex, Flex, SpaceTokens, Text, flexStyles, useMedia, useSporeColors } from 'ui/src'
import { opacify, spacing } from 'ui/src/theme'
import { isIOS } from 'uniswap/src/utils/platform'
import { isIOS } from 'utilities/src/platform'
import { useKeyboardLayout } from 'wallet/src/utils/useKeyboardLayout'
type OnboardingScreenProps = {
......
// eslint-disable-next-line no-restricted-imports
import { OriginApplication } from '@uniswap/analytics'
import DeviceInfo from 'react-native-device-info'
import DeviceInfo, { getDeviceId } from 'react-native-device-info'
import { selectAllowAnalytics } from 'src/features/telemetry/selectors'
import { call, delay, fork, select, takeEvery } from 'typed-redux-saga'
import { uniswapUrls } from 'uniswap/src/constants/urls'
......@@ -21,7 +21,9 @@ export function* telemetrySaga() {
originOverride: uniswapUrls.apiOrigin,
appBuild: DeviceInfo.getBundleId(),
}),
allowAnalytics
allowAnalytics,
undefined,
async () => getDeviceId()
)
yield* fork(watchTransactionEvents)
}
......
......@@ -35,8 +35,8 @@ import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks'
import { ProfileMetadata } from 'uniswap/src/features/unitags/types'
import { ChainId } from 'uniswap/src/types/chains'
import { MobileScreens, UnitagScreens } from 'uniswap/src/types/screens/mobile'
import { isIOS } from 'uniswap/src/utils/platform'
import { logger } from 'utilities/src/logger/logger'
import { isIOS } from 'utilities/src/platform'
import { normalizeTwitterUsername } from 'utilities/src/primitives/string'
import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText'
import { TextInput } from 'wallet/src/components/input/TextInput'
......
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { NativeModules } from 'react-native'
import { isAndroid } from 'uniswap/src/utils/platform'
import { isAndroid } from 'utilities/src/platform'
const { RNWalletConnect } = NativeModules
......
import { getOneSignalPushToken } from 'src/features/notifications/Onesignal'
import { config } from 'uniswap/src/config'
import { isAndroid } from 'uniswap/src/utils/platform'
import { isJestRun } from 'utilities/src/environment'
import { logger } from 'utilities/src/logger/logger'
import { isAndroid } from 'utilities/src/platform'
const WC_HOSTED_PUSH_SERVER_URL = `https://echo.walletconnect.com/${config.walletConnectProjectId}`
......
......@@ -5,7 +5,7 @@ import { MobileEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { CurrencyId } from 'uniswap/src/types/currency'
import { WidgetEvent } from 'uniswap/src/types/widgets'
import { isAndroid } from 'uniswap/src/utils/platform'
import { isAndroid } from 'utilities/src/platform'
// eslint-disable-next-line no-restricted-imports
import { analytics } from 'utilities/src/telemetry/analytics/analytics'
import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils'
......
/* eslint-disable @typescript-eslint/no-unused-expressions */
import { isAndroid } from 'uniswap/src/utils/platform'
import { isAndroid } from 'utilities/src/platform'
// TODO: [MOB-247] remove polyfill once Hermes support it
// https://github.com/facebook/hermes/issues/23
......
......@@ -4,7 +4,7 @@ import { Carousel } from 'src/components/carousel/Carousel'
import { educationContent } from 'src/components/education'
import { Screen } from 'src/components/layout/Screen'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { isIOS } from 'uniswap/src/utils/platform'
import { isIOS } from 'utilities/src/platform'
export function EducationScreen({
route: {
......
......@@ -18,7 +18,7 @@ import Trace from 'uniswap/src/features/telemetry/Trace'
import { ElementName, ElementNameType } from 'uniswap/src/features/telemetry/constants'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { isAndroid } from 'uniswap/src/utils/platform'
import { isAndroid } from 'utilities/src/platform'
import {
PendingAccountActions,
pendingAccountActions,
......
......@@ -32,7 +32,7 @@ import {
} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { isIOS } from 'uniswap/src/utils/platform'
import { isIOS } from 'utilities/src/platform'
import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard'
import { isError } from 'wallet/src/data/utils'
import { NFTViewer } from 'wallet/src/features/images/NFTViewer'
......
......@@ -38,7 +38,7 @@ import Trace from 'uniswap/src/features/telemetry/Trace'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { ChainId } from 'uniswap/src/types/chains'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { isAndroid, isIOS } from 'uniswap/src/utils/platform'
import { isAndroid, isIOS } from 'utilities/src/platform'
import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard'
import { NetworkLogo } from 'wallet/src/components/CurrencyLogo/NetworkLogo'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
......
......@@ -23,7 +23,7 @@ import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { ImportType } from 'uniswap/src/types/onboarding'
import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName'
import { isAndroid } from 'uniswap/src/utils/platform'
import { isAndroid } from 'utilities/src/platform'
import { useAsyncData } from 'utilities/src/react/hooks'
import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext'
import { BackupType } from 'wallet/src/features/wallet/accounts/types'
......
......@@ -16,7 +16,7 @@ import { ElementName } from 'uniswap/src/features/telemetry/constants'
import i18n from 'uniswap/src/i18n/i18n'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { isIOS } from 'uniswap/src/utils/platform'
import { isIOS } from 'utilities/src/platform'
import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext'
import { useNativeAccountExists } from 'wallet/src/features/wallet/hooks'
import { openSettings } from 'wallet/src/utils/linking'
......
import {
Easing,
EntryExitAnimationFunction,
StyleProps,
withDelay,
withSequence,
withTiming,
} from 'react-native-reanimated'
// Because the animation spec is denominated in frames, but Reanimated only works with milliseconds, this object stores all the frame values as their millisecond value equivalent. The conversion assumes 60fps, using a formula of (1000 / 60) * frames. This lets us keep the code readable according to frame numbers without recomputing these values every time.
const FPMS = {
29: 483.33,
60: 1000,
130: 2166.67,
165: 2750,
179: 2983.33,
180: 3000,
228: 3800,
241: 4016.67,
258: 4300,
360: 6000,
}
// 1. QR slide up animation
// Moves QR code box from bottom to top and fades it in
// - Moves QR from y:-64 to y:0 from frame 29 to frame 60
// - Fades in QR from 0 to 1 opacity from frame 29 to frame 60
// - Triggers etching video as a callback when complete
export const qrSlideUpAndFadeInConfig = {
opacity: {
startValue: 0,
endValue: 1,
delay: FPMS[29],
duration: FPMS[60] - FPMS[29],
},
translateY: {
startValue: 64,
endValue: 0,
delay: FPMS[29],
duration: FPMS[60] - FPMS[29],
},
}
// 2. QR scale in animation
// Scales QR code box from 80% to 100% scale from frame 130 to frame 179
const qrScaleInConfig = {
scale: { startValue: 0.8, endValue: 1, delay: FPMS[130], duration: FPMS[179] - FPMS[130] },
}
export const qrScaleIn: EntryExitAnimationFunction = () => {
'worklet'
const animations: StyleProps = {
transform: [
{
scale: withDelay(
qrScaleInConfig.scale.delay,
withTiming(qrScaleInConfig.scale.endValue, {
duration: qrScaleInConfig.scale.duration,
easing: Easing.bezierFn(0.66, 0.0, 0.34, 1.0),
})
),
},
],
}
const initialValues: StyleProps = {
transform: [{ scale: qrScaleInConfig.scale.startValue }],
}
return {
initialValues,
animations,
}
}
// 3. QR inner glow animation
// Animates inner glowing circle of QR code box to slowly fade in
// NOTE: for now, it animates the inner glow's blur amount, as a proxy for its size because to animate its size makes it hard to keep it centered in the canvas. In order to do so, we would need to run the computing of the inner glow's size on the UI thread instead of the JS thread in order to transform the x and y of the Canvas Group the Oval and Blur are inside of by the correct amount as it animates (size / 2).
// NOTE 2: the actual animation had to be kept coupled to the component because it would become too unwieldy to abstract it here.
export const qrInnerBlurConfig = {
opacity: {
startValue: 0,
endValue: 1,
delay: 0,
duration: FPMS[360] - FPMS[180],
},
size: {
startValue: 100,
endValue: 25,
delay: 0,
duration: FPMS[360] - FPMS[180],
},
}
// 4. Flash wipe animation
// Covers the etching video with a flash wipe animation
// - Fades in flash wipe from 80% to 100% scale from frame 130 to frame 179
// - Fades in flash wipe from 0% opacity to 100% opacity from frame 165 to frame 180
// - Fades out flash wipe from 100% to 0% opacity from frame 180 to frame 228
export const flashWipeConfig = {
scale: {
startValue: 0.8,
endValue: 1,
delay: FPMS[130],
duration: FPMS[179] - FPMS[130],
},
opacityIn: {
startValue: 0,
endValue: 1,
delay: FPMS[165],
duration: FPMS[180] - FPMS[165],
},
opacityOut: {
startValue: 1,
endValue: 0,
delay: 0,
duration: FPMS[228] - FPMS[180],
},
}
export const flashWipeAnimation: EntryExitAnimationFunction = () => {
'worklet'
const animations: StyleProps = {
opacity: withSequence(
withDelay(
flashWipeConfig.opacityIn.delay,
withTiming(flashWipeConfig.opacityIn.endValue, {
duration: flashWipeConfig.opacityIn.duration,
easing: Easing.bezierFn(0.4, 0.0, 0.68, 0.06),
})
),
withDelay(
flashWipeConfig.opacityOut.delay,
withTiming(flashWipeConfig.opacityOut.endValue, {
duration: flashWipeConfig.opacityOut.duration,
easing: Easing.bezierFn(0.66, 0.0, 0.34, 1.0),
})
)
),
transform: [
{
scale: withDelay(
flashWipeConfig.scale.delay,
withTiming(flashWipeConfig.scale.endValue, { duration: flashWipeConfig.scale.duration })
),
},
],
}
const initialValues: StyleProps = {
opacity: flashWipeConfig.opacityIn.startValue,
transform: [{ scale: flashWipeConfig.scale.startValue }],
}
return {
initialValues,
animations,
}
}
// 5. Video fade out
// Fades out video after flash wipe has covered etching video
export const videoFadeOut: EntryExitAnimationFunction = () => {
'worklet'
const animations = {
opacity: withDelay(
flashWipeConfig.opacityIn.delay + flashWipeConfig.opacityIn.duration,
withTiming(0, { duration: 1, easing: Easing.bezierFn(0.4, 0.0, 0.68, 0.06) })
),
}
const initialValues = {
opacity: 1,
}
return {
initialValues,
animations,
}
}
// 6. QR top glow fade in
// Fades in glow on top of real QR code and unicon after flash wipe
export const realQrTopGlowFadeIn: EntryExitAnimationFunction = () => {
'worklet'
const animations = {
opacity: withDelay(
flashWipeConfig.opacityIn.delay,
withTiming(flashWipeConfig.opacityIn.endValue, {
duration: flashWipeConfig.opacityIn.duration,
easing: Easing.bezierFn(0.4, 0.0, 0.68, 0.06),
})
),
}
const initialValues = {
opacity: 0,
}
return {
initialValues,
animations,
}
}
// 7. Real QR code fade in
// Show the real QR code and Unicon after the flash wipe
export const realQrFadeIn: EntryExitAnimationFunction = () => {
'worklet'
const animations = {
opacity: withDelay(
flashWipeConfig.opacityIn.delay + flashWipeConfig.opacityIn.duration,
withTiming(1, { duration: 1, easing: Easing.bezierFn(0.4, 0.0, 0.68, 0.06) })
),
}
const initialValues = {
opacity: 0,
}
return {
initialValues,
animations,
}
}
// 8. Text slide up and fade in
const textSlideUpAndFadeInConfig = {
opacityIn: {
startValue: 0,
endValue: 1,
delay: FPMS[241],
duration: FPMS[241],
},
opacityOut: {
startValue: 0,
endValue: 1,
delay: FPMS[241],
duration: FPMS[258] - FPMS[241],
},
translateY: {
startValue: 32,
endValue: 0,
delay: FPMS[228],
duration: FPMS[258] - FPMS[228],
},
}
export const textSlideUpAtEnd: EntryExitAnimationFunction = () => {
'worklet'
const animations: StyleProps = {
opacity: withDelay(
textSlideUpAndFadeInConfig.opacityOut.delay,
withTiming(textSlideUpAndFadeInConfig.opacityOut.endValue, {
duration: textSlideUpAndFadeInConfig.opacityOut.duration,
easing: Easing.bezierFn(0.66, 0.0, 0.34, 1.0),
})
),
transform: [
{
translateY: withDelay(
textSlideUpAndFadeInConfig.translateY.delay,
withTiming(textSlideUpAndFadeInConfig.translateY.endValue, {
duration: textSlideUpAndFadeInConfig.translateY.duration,
easing: Easing.bezierFn(0.66, 0.0, 0.34, 1.0),
})
),
},
],
}
const initialValues: StyleProps = {
transform: [{ translateY: textSlideUpAndFadeInConfig.translateY.startValue }],
opacity: textSlideUpAndFadeInConfig.opacityOut.startValue,
}
return {
initialValues,
animations,
}
}
// 9. Button slide up and fade in
export const letsGoButtonFadeIn: EntryExitAnimationFunction = () => {
'worklet'
const animations = {
opacity: withDelay(
textSlideUpAndFadeInConfig.opacityOut.delay,
withTiming(textSlideUpAndFadeInConfig.opacityOut.endValue, {
duration: textSlideUpAndFadeInConfig.opacityOut.duration,
})
),
}
const initialValues = {
opacity: textSlideUpAndFadeInConfig.opacityOut.startValue,
}
return {
initialValues,
animations,
}
}
// 10. Whole QR container (showing real QR and Unicon now) slides up
const qrSlideUpAtEndConfig = {
translateY: {
startValue: 0,
endValue: -64,
delay: FPMS[228],
duration: FPMS[258] - FPMS[228],
},
}
export const qrSlideUpAtEnd: EntryExitAnimationFunction = () => {
'worklet'
const animations: StyleProps = {
transform: [
{
translateY: withDelay(
qrSlideUpAtEndConfig.translateY.delay,
withTiming(qrSlideUpAtEndConfig.translateY.endValue, {
duration: qrSlideUpAtEndConfig.translateY.duration,
easing: Easing.bezierFn(0.66, 0.0, 0.34, 1.0),
})
),
},
],
}
const initialValues: StyleProps = {
transform: [{ translateY: qrSlideUpAndFadeInConfig.translateY.startValue }],
}
return {
initialValues,
animations,
}
}
......@@ -25,7 +25,7 @@ import Trace from 'uniswap/src/features/telemetry/Trace'
import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { ImportType } from 'uniswap/src/types/onboarding'
import { OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { isIOS } from 'uniswap/src/utils/platform'
import { isIOS } from 'utilities/src/platform'
import { opacify } from 'wallet/src/utils/colors'
import { openSettings } from 'wallet/src/utils/linking'
......
......@@ -20,7 +20,7 @@ import {
setRequiredForTransactions,
} from 'src/features/biometrics/slice'
import { Flex, Text, TouchableArea } from 'ui/src'
import { isAndroid, isIOS } from 'uniswap/src/utils/platform'
import { isAndroid, isIOS } from 'utilities/src/platform'
import { Switch } from 'wallet/src/components/buttons/Switch'
import { openSettings } from 'wallet/src/utils/linking'
......
......@@ -62,7 +62,7 @@ import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName'
import { isAndroid } from 'uniswap/src/utils/platform'
import { isAndroid } from 'utilities/src/platform'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { useTimeout } from 'utilities/src/time/timing'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
......
......@@ -17,7 +17,7 @@ import { Button, Flex, Text } from 'ui/src'
import { PenLine } from 'ui/src/components/icons'
import { fonts } from 'ui/src/theme'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { isIOS } from 'uniswap/src/utils/platform'
import { isIOS } from 'utilities/src/platform'
import { TextInput } from 'wallet/src/components/input/TextInput'
import { NICKNAME_MAX_LENGTH } from 'wallet/src/constants/accounts'
import { useCanAddressClaimUnitag } from 'wallet/src/features/unitags/hooks'
......
......@@ -329,6 +329,7 @@ function TokenDetails({
style={{ marginBottom: insets.bottom }}>
<TokenDetailsActionButtons
tokenColor={tokenColor}
userHasBalance={Boolean(currentChainBalance)}
onPressBuy={(): void => onPressSwap(CurrencyField.OUTPUT)}
onPressSell={(): void => onPressSwap(CurrencyField.INPUT)}
/>
......
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NavigationContainer } from '@react-navigation/native'
import type { EnhancedStore, PreloadedState } from '@reduxjs/toolkit'
import { configureStore } from '@reduxjs/toolkit'
......@@ -78,12 +77,9 @@ type ExtendedRenderHookOptions<P> = RenderHookOptions<P> & {
store?: AppStore
}
type RenderHookWithProvidersResult<R, P extends any[] | undefined = undefined> = Omit<
RenderHookResult<R, P>,
'rerender'
> & {
type RenderHookWithProvidersResult<R, P = undefined> = Omit<RenderHookResult<R, P>, 'rerender'> & {
store: EnhancedStore
rerender: P extends any[] ? (args: P) => void : () => void
rerender: (args?: P) => void
}
// Don't require hookOptions if hook doesn't take any arguments
......@@ -93,8 +89,8 @@ export function renderHookWithProviders<R>(
): RenderHookWithProvidersResult<R>
// Require hookOptions if hook takes arguments
export function renderHookWithProviders<R, P extends any[]>(
hook: (...args: P) => R,
export function renderHookWithProviders<R, P>(
hook: (args: P) => R,
hookOptions: ExtendedRenderHookOptions<P>
): RenderHookWithProvidersResult<R, P>
......@@ -105,8 +101,8 @@ export function renderHookWithProviders<R, P extends any[]>(
* @param preloadedState and store
* @returns `hook` wrapped with providers
*/
export function renderHookWithProviders<P extends any[], R>(
hook: (...args: P) => R,
export function renderHookWithProviders<P, R>(
hook: (args: P) => R,
hookOptions?: ExtendedRenderHookOptions<P>
): RenderHookWithProvidersResult<R, P> {
const {
......@@ -140,12 +136,12 @@ export function renderHookWithProviders<P extends any[], R>(
...(renderOptions as RenderHookOptions<P>),
}
const { rerender, ...rest } = RNRenderHook<R, P>((args: P) => hook(...(args ?? [])), options)
const { rerender, ...rest } = RNRenderHook<R, P>((args: P) => hook(args), options)
// Return an object with the store and all of RTL's query functions
return {
store,
rerender: rerender as P extends any[] ? (args: P) => void : () => void,
rerender: rerender as (args?: P) => void,
...rest,
}
}
......@@ -72,7 +72,7 @@ module.exports = {
message: 'Default import from zustand is deprecated. Import `{ create }` instead.',
},
{
name: 'uniswap/src/utils/platform',
name: 'utilities/src/platform',
importNames: ['isIOS', 'isAndroid'],
message:
'Importing isIOS and isAndroid from platform is not allowed. Use isWebIOS and isWebAndroid instead.',
......@@ -82,6 +82,11 @@ module.exports = {
importNames: ['useChainId', 'useAccount'],
message: 'Import properly typed account data from `hooks/useAccount` instead.',
},
{
name: 'wagmi',
importNames: ['useConnect'],
message: 'Import wrapped useConnect util from `hooks/useConnect` instead.',
},
],
},
],
......
......@@ -292,15 +292,7 @@ describe('UniswapX v1', () => {
submitUniswapXOrder()
cy.get(getTestSelector('confirmation-close-icon')).click()
cy.intercept(/(?:interface|beta).gateway.uniswap.org\/v1\/graphql/, (req) => {
if (req.body.operationName === 'PortfolioBalancesWeb') {
req.alias = 'PortfolioBalancesWeb'
// Reply with a fixture to speed up test
req.reply({ fixture: 'mini-portfolio/tokens.json' })
} else {
req.continue()
}
})
cy.interceptGraphqlOperation('PortfolioBalancesWeb', 'mini-portfolio/tokens.json')
// Expect balances to refetch after filling
cy.wait('@orderStatusOpen')
......
......@@ -287,15 +287,7 @@ describe('UniswapX v2', () => {
submitUniswapXOrder()
cy.get(getTestSelector('confirmation-close-icon')).click()
cy.intercept(/(?:interface|beta).gateway.uniswap.org\/v1\/graphql/, (req) => {
if (req.body.operationName === 'PortfolioBalancesWeb') {
req.alias = 'PortfolioBalancesWeb'
// Reply with a fixture to speed up test
req.reply({ fixture: 'mini-portfolio/tokens.json' })
} else {
req.continue()
}
})
cy.interceptGraphqlOperation('PortfolioBalancesWeb', 'mini-portfolio/tokens.json')
// Expect balances to refetch after filling
cy.wait('@orderStatusOpen')
......
......@@ -80,9 +80,10 @@ describe('Token details', () => {
cy.contains(shortenAddress('0x1eFBB78C8b917f67986BcE54cE575069c0143681')).should('exist')
// Warning label should show if relevant ([spec](https://www.notion.so/3f7fce6f93694be08a94a6984d50298e))
cy.get('[data-cy="token-safety-message"]')
.should('include.text', 'Warning')
.and('include.text', "This token isn't traded on leading U.S. centralized exchanges")
cy.get('[data-cy="token-safety-message"]').contains(/Warning/)
cy.get('[data-cy="token-safety-description"]').contains(
/This token isn’t traded on leading U.S. centralized exchanges or frequently swapped on Uniswap./
)
})
describe('swapping', () => {
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -110,23 +110,23 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
const isUnclaimed = useUserHasAvailableClaim(account)
const openClaimModal = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
const [accountDrawerOpen, toggleAccountDrawer] = useAccountDrawer()
const accountDrawer = useAccountDrawer()
const navigateToProfile = useCallback(() => {
toggleAccountDrawer()
accountDrawer.close()
resetSellAssets()
setSellPageState(ProfilePageStateType.VIEWING)
clearCollectionFilters()
navigate('/nfts/profile')
closeModal()
}, [clearCollectionFilters, closeModal, navigate, resetSellAssets, setSellPageState, toggleAccountDrawer])
}, [clearCollectionFilters, closeModal, navigate, resetSellAssets, setSellPageState, accountDrawer])
const openFiatOnrampModal = useOpenModal(ApplicationModal.FIAT_ONRAMP)
const openFoRModalWithAnalytics = useCallback(() => {
toggleAccountDrawer()
accountDrawer.close()
sendAnalyticsEvent(InterfaceEventName.FIAT_ONRAMP_WIDGET_OPENED)
openFiatOnrampModal()
}, [openFiatOnrampModal, toggleAccountDrawer])
}, [openFiatOnrampModal, accountDrawer])
const [shouldCheck, setShouldCheck] = useState(false)
const {
......@@ -147,7 +147,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
error || (!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) || fiatOnrampAvailabilityLoading
)
const { data: portfolioBalances } = useTokenBalancesQuery({ cacheOnly: !accountDrawerOpen })
const { data: portfolioBalances } = useTokenBalancesQuery({ cacheOnly: !accountDrawer.isOpen })
const portfolio = portfolioBalances?.portfolios?.[0]
const totalBalance = portfolio?.tokensTotalDenominatedValue?.value
const absoluteChange = portfolio?.tokensTotalDenominatedValueChange?.absolute?.value
......
......@@ -47,7 +47,7 @@ function useCancelLimitsDialogContent(
<Plural
value={orders.length}
one={t('common.limit.cancel')}
other={t(`common.limit.cancel.amount`, { amount: orders.length })}
other={t(`common.limit.cancel.amount`, { count: orders.length })}
/>
),
icon: <Slash />,
......@@ -143,7 +143,7 @@ export function CancelLimitsDialog(
}
buttonsConfig={{
left: {
title: <Trans i18nKey="common.nevermind" />,
title: <Trans i18nKey="common.neverMind" />,
onClick: onCancel,
textColor: 'neutral1',
},
......
......@@ -10,7 +10,7 @@ import { ThemedText } from 'theme/components'
import { OpenLimitOrdersButton } from 'components/AccountDrawer/MiniPortfolio/Limits/OpenLimitOrdersButton'
import { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow'
import { useToggleAccountDrawer } from '../hooks'
import { useAccountDrawer } from '../hooks'
import { ActivityRow } from './ActivityRow'
import { useAllActivities } from './hooks'
import { createGroups } from './utils'
......@@ -26,7 +26,7 @@ const OpenLimitOrdersActivityButton = styled(OpenLimitOrdersButton)`
`
export function ActivityTab({ account }: { account: string }) {
const toggleAccountDrawer = useToggleAccountDrawer()
const accountDrawer = useAccountDrawer()
const setMenu = useUpdateAtom(miniPortfolioMenuStateAtom)
const { activities, loading } = useAllActivities(account)
......@@ -46,7 +46,7 @@ export function ActivityTab({ account }: { account: string }) {
return (
<>
<OpenLimitOrdersActivityButton openLimitsMenu={() => setMenu(MenuState.LIMITS)} account={account} />
<EmptyWalletModule type="activity" onNavigateClick={toggleAccountDrawer} />
<EmptyWalletModule type="activity" onNavigateClick={accountDrawer.close} />
</>
)
}
......
......@@ -2,7 +2,7 @@ import { useOpenLimitOrders } from 'components/AccountDrawer/MiniPortfolio/Activ
import Column from 'components/Column'
import { TimeForwardIcon } from 'components/Icons/TimeForward'
import Row from 'components/Row'
import { Plural, t, Trans } from 'i18n'
import { Plural, Trans, t } from 'i18n'
import { ChevronRight } from 'react-feather'
import styled, { useTheme } from 'styled-components'
import { ClickableStyle, ThemedText } from 'theme/components'
......
......@@ -12,7 +12,7 @@ import { ThemedText } from 'theme/components'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { useTrace } from 'utilities/src/telemetry/trace/TraceContext'
import { NumberType, useFormatter } from 'utils/formatNumbers'
import { useToggleAccountDrawer } from '../hooks'
import { useAccountDrawer } from '../hooks'
const FloorPrice = styled(Row)`
opacity: 0;
......@@ -47,12 +47,12 @@ export function NFT({
mediaShouldBePlaying: boolean
setCurrentTokenPlayingMedia: (tokenId: string | undefined) => void
}) {
const toggleWalletDrawer = useToggleAccountDrawer()
const accountDrawer = useAccountDrawer()
const navigate = useNavigate()
const trace = useTrace()
const navigateToNFTDetails = () => {
toggleWalletDrawer()
accountDrawer.close()
navigate(detailsHref(asset))
}
......
......@@ -10,7 +10,7 @@ import { useAccountDrawer } from '../hooks'
import { NFT } from './NFTItem'
export default function NFTs({ account }: { account: string }) {
const [walletDrawerOpen, toggleWalletDrawer] = useAccountDrawer()
const accountDrawer = useAccountDrawer()
const { walletAssets, loading, hasNext, loadMore } = useNftBalance(
account,
[],
......@@ -19,7 +19,7 @@ export default function NFTs({ account }: { account: string }) {
undefined,
undefined,
undefined,
!walletDrawerOpen
!accountDrawer.isOpen
)
const [currentTokenPlayingMedia, setCurrentTokenPlayingMedia] = useState<string | undefined>()
......@@ -33,7 +33,7 @@ export default function NFTs({ account }: { account: string }) {
}
if (!walletAssets || walletAssets?.length === 0) {
return <EmptyWalletModule onNavigateClick={toggleWalletDrawer} />
return <EmptyWalletModule onNavigateClick={accountDrawer.close} />
}
return (
......
......@@ -17,7 +17,7 @@ import { NumberType, useFormatter } from 'utils/formatNumbers'
import { ExpandoRow } from '../ExpandoRow'
import { PortfolioLogo } from '../PortfolioLogo'
import PortfolioRow, { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow'
import { useToggleAccountDrawer } from '../hooks'
import { useAccountDrawer } from '../hooks'
import { PositionInfo } from './cache'
import { useFeeValues } from './hooks'
import useMultiChainPositions from './useMultiChainPositions'
......@@ -64,14 +64,14 @@ export default function Pools({ account }: { account: string }) {
return [openPositions, closedPositions]
}, [filteredPositions])
const toggleWalletDrawer = useToggleAccountDrawer()
const accountDrawer = useAccountDrawer()
if (!filteredPositions || loading) {
return <PortfolioSkeleton />
}
if (filteredPositions.length === 0) {
return <EmptyWalletModule type="pool" onNavigateClick={toggleWalletDrawer} />
return <EmptyWalletModule type="pool" onNavigateClick={accountDrawer.close} />
}
return (
......@@ -128,16 +128,16 @@ function PositionListItem({ positionInfo }: { positionInfo: PositionInfo }) {
const liquidityValue = calculateLiquidityValue(priceA, priceB, position)
const navigate = useNavigate()
const toggleWalletDrawer = useToggleAccountDrawer()
const accountDrawer = useAccountDrawer()
const { chainId: walletChainId } = useWeb3React()
const switchChain = useSwitchChain()
const onClick = useCallback(async () => {
if (walletChainId !== chainId) {
await switchChain(chainId)
}
toggleWalletDrawer()
accountDrawer.close()
navigate('/pool/' + details.tokenId)
}, [walletChainId, chainId, switchChain, toggleWalletDrawer, navigate, details.tokenId])
}, [walletChainId, chainId, switchChain, accountDrawer, navigate, details.tokenId])
const analyticsEventProperties = useMemo(
() => ({
chain_id: chainId,
......
......@@ -19,15 +19,15 @@ import { hideSmallBalancesAtom } from '../../SmallBalanceToggle'
import { ExpandoRow } from '../ExpandoRow'
import { PortfolioLogo } from '../PortfolioLogo'
import PortfolioRow, { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow'
import { useAccountDrawer, useToggleAccountDrawer } from '../hooks'
import { useAccountDrawer } from '../hooks'
export default function Tokens() {
const [accountDrawerOpen, toggleAccountDrawer] = useAccountDrawer()
const accountDrawer = useAccountDrawer()
const hideSmallBalances = useAtomValue(hideSmallBalancesAtom)
const hideSpam = useAtomValue(hideSpamAtom)
const [showHiddenTokens, setShowHiddenTokens] = useState(false)
const { data } = useTokenBalancesQuery({ cacheOnly: !accountDrawerOpen })
const { data } = useTokenBalancesQuery({ cacheOnly: !accountDrawer.isOpen })
const tokenBalances = data?.portfolios?.[0]?.tokenBalances
......@@ -42,7 +42,7 @@ export default function Tokens() {
if (tokenBalances?.length === 0) {
// TODO: consider launching moonpay here instead of just closing the drawer
return <EmptyWalletModule type="token" onNavigateClick={toggleAccountDrawer} />
return <EmptyWalletModule type="token" onNavigateClick={accountDrawer.close} />
}
const toggleHiddenTokens = () => setShowHiddenTokens((showHiddenTokens) => !showHiddenTokens)
......@@ -80,12 +80,12 @@ function TokenRow({
const percentChange = tokenProjectMarket?.pricePercentChange?.value ?? 0
const navigate = useNavigate()
const toggleWalletDrawer = useToggleAccountDrawer()
const accountDrawer = useAccountDrawer()
const navigateToTokenDetails = useCallback(async () => {
navigate(getTokenDetailsURL({ ...token }))
toggleWalletDrawer()
}, [navigate, token, toggleWalletDrawer])
accountDrawer.close()
}, [navigate, token, accountDrawer])
const { formatNumber } = useFormatter()
const currency = gqlToCurrency(token)
......
import { atom, useAtom } from 'jotai'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { useCallback } from 'react'
import { useCallback, useMemo } from 'react'
const accountDrawerOpenAtom = atom(false)
const showMoonpayTextAtom = atom(false)
export function useToggleAccountDrawer() {
const [open, updateAccountDrawerOpen] = useAtom(accountDrawerOpenAtom)
export function useAccountDrawer() {
const [isOpen, updateAccountDrawerOpen] = useAtom(accountDrawerOpenAtom)
const setShowMoonpayTextInDrawer = useSetShowMoonpayText()
return useCallback(() => {
updateAccountDrawerOpen(!open)
if (open) {
setShowMoonpayTextInDrawer(false)
}
}, [open, setShowMoonpayTextInDrawer, updateAccountDrawerOpen])
}
export function useCloseAccountDrawer() {
const updateAccountDrawerOpen = useUpdateAtom(accountDrawerOpenAtom)
return useCallback(() => updateAccountDrawerOpen(false), [updateAccountDrawerOpen])
}
const open = useCallback(() => {
updateAccountDrawerOpen(true)
}, [updateAccountDrawerOpen])
export function useOpenAccountDrawer() {
const updateAccountDrawerOpen = useUpdateAtom(accountDrawerOpenAtom)
return useCallback(() => updateAccountDrawerOpen(true), [updateAccountDrawerOpen])
}
const close = useCallback(() => {
setShowMoonpayTextInDrawer(false)
updateAccountDrawerOpen(false)
}, [setShowMoonpayTextInDrawer, updateAccountDrawerOpen])
export function useAccountDrawer(): [boolean, () => void] {
const accountDrawerOpen = useAtomValue(accountDrawerOpenAtom)
return [accountDrawerOpen, useToggleAccountDrawer()]
return useMemo(() => ({ isOpen, open, close }), [isOpen, open, close])
}
// Only show Moonpay text if the user opens the Account Drawer by clicking 'Buy'
......
......@@ -4,14 +4,14 @@ import Modal from 'components/Modal'
import { RowBetween } from 'components/Row'
import { useConnectorWithId } from 'components/WalletModal/useOrderedConnections'
import { CONNECTION } from 'components/Web3Provider/constants'
import { useConnect } from 'hooks/useConnect'
import { Trans } from 'i18n'
import { QRCodeSVG } from 'qrcode.react'
import { useCallback, useEffect, useState } from 'react'
import styled, { useTheme } from 'styled-components'
import { CloseIcon, ThemedText } from 'theme/components'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { isWebAndroid, isWebIOS } from 'uniswap/src/utils/platform'
import { useAccountEffect, useConnect, useDisconnect } from 'wagmi'
import { isWebAndroid, isWebIOS } from 'utilities/src/platform'
import uniPng from '../../assets/images/uniwallet_modal_icon.png'
import { DownloadButton } from './DownloadButton'
......@@ -37,18 +37,12 @@ const Divider = styled.div`
export default function UniwalletModal() {
const [uri, setUri] = useState<string>()
const connection = useConnect()
// Displays the modal if not on iOS/Android, a Uniswap Wallet Connection is pending, & qrcode URI is available
const onLaunchedMobilePlatform = isWebIOS || isWebAndroid
const open = !onLaunchedMobilePlatform && !!uri
const open = !onLaunchedMobilePlatform && !!uri && connection.isPending
const { disconnect } = useDisconnect()
const { connectors } = useConnect()
useAccountEffect({
onConnect: () => {
setUri(undefined)
},
})
const uniswapWalletConnectConnector = useConnectorWithId(CONNECTION.UNISWAP_WALLET_CONNECT_CONNECTOR_ID, {
shouldThrow: true,
})
......@@ -65,12 +59,12 @@ export default function UniwalletModal() {
return () => {
uniswapWalletConnectConnector.emitter.off('message', listener)
}
}, [connectors, uniswapWalletConnectConnector.emitter])
}, [uniswapWalletConnectConnector.emitter])
const close = useCallback(() => {
disconnect()
connection?.reset()
setUri(undefined)
}, [disconnect])
}, [connection])
useEffect(() => {
if (open) {
......
......@@ -11,7 +11,7 @@ import { BREAKPOINTS } from 'theme'
import { ClickableStyle } from 'theme/components'
import { Z_INDEX } from 'theme/zIndex'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { isMobile } from 'uniswap/src/utils/platform'
import { isMobile } from 'utilities/src/platform'
import DefaultMenu from './DefaultMenu'
import { useAccountDrawer } from './MiniPortfolio/hooks'
......@@ -152,21 +152,21 @@ const CloseDrawer = styled.div`
`
function AccountDrawer() {
const [walletDrawerOpen, toggleWalletDrawer] = useAccountDrawer()
const wasWalletDrawerOpen = usePrevious(walletDrawerOpen)
const accountDrawer = useAccountDrawer()
const wasAccountDrawerOpen = usePrevious(accountDrawer.isOpen)
const scrollRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (wasWalletDrawerOpen && !walletDrawerOpen) {
if (wasAccountDrawerOpen && !accountDrawer.isOpen) {
scrollRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
}
}, [walletDrawerOpen, wasWalletDrawerOpen])
}, [accountDrawer, wasAccountDrawerOpen])
// close on escape keypress
useEffect(() => {
const escapeKeyDownHandler = (event: KeyboardEvent) => {
if (event.key === 'Escape' && walletDrawerOpen) {
if (event.key === 'Escape' && accountDrawer.isOpen) {
event.preventDefault()
toggleWalletDrawer()
accountDrawer.close()
}
}
......@@ -175,12 +175,12 @@ function AccountDrawer() {
return () => {
document.removeEventListener('keydown', escapeKeyDownHandler)
}
}, [walletDrawerOpen, toggleWalletDrawer])
}, [accountDrawer])
// useStates for detecting swipe gestures
const [yPosition, setYPosition] = useState(0)
const [dragStartTop, setDragStartTop] = useState(true)
useDisableScrolling(walletDrawerOpen)
useDisableScrolling(accountDrawer.isOpen)
// useGesture hook for detecting swipe gestures
const bind = useGesture({
......@@ -194,11 +194,11 @@ function AccountDrawer() {
}
} else if (
(state.movement[1] > 300 || (state.velocity > 3 && state.direction[1] > 0)) &&
walletDrawerOpen &&
accountDrawer.isOpen &&
dragStartTop
) {
toggleWalletDrawer()
} else if (walletDrawerOpen && dragStartTop && state.movement[1] > 0) {
accountDrawer.close()
} else if (accountDrawer.isOpen && dragStartTop && state.movement[1] > 0) {
setYPosition(state.movement[1])
if (scrollRef.current) {
scrollRef.current.style.overflowY = 'hidden'
......@@ -227,16 +227,16 @@ function AccountDrawer() {
return (
<Container>
{walletDrawerOpen && (
{accountDrawer.isOpen && (
<Trace logPress eventOnTrigger={InterfaceEventName.MINI_PORTFOLIO_TOGGLED} properties={{ type: 'close' }}>
<CloseDrawer onClick={toggleWalletDrawer} data-testid="close-account-drawer">
<CloseDrawer onClick={accountDrawer.close} data-testid="close-account-drawer">
<CloseIcon />
</CloseDrawer>
</Trace>
)}
<Scrim onClick={toggleWalletDrawer} $open={walletDrawerOpen} />
<Scrim onClick={accountDrawer.close} $open={accountDrawer.isOpen} />
<AccountDrawerWrapper
open={walletDrawerOpen}
open={accountDrawer.isOpen}
{...(isMobile
? {
...bind(),
......@@ -246,7 +246,7 @@ function AccountDrawer() {
>
{/* id used for child InfiniteScrolls to reference when it has reached the bottom of the component */}
<AccountDrawerScrollWrapper ref={scrollRef} id="wallet-dropdown-scroll-wrapper">
<DefaultMenu drawerOpen={walletDrawerOpen} />
<DefaultMenu drawerOpen={accountDrawer.isOpen} />
</AccountDrawerScrollWrapper>
</AccountDrawerWrapper>
</Container>
......
......@@ -3,7 +3,7 @@ import Row from 'components/Row'
import Tooltip, { TooltipSize } from 'components/Tooltip'
import { useScreenSize } from 'hooks/screenSize'
import useCopyClipboard from 'hooks/useCopyClipboard'
import { t, Trans } from 'i18n'
import { Trans, t } from 'i18n'
import { useCallback, useState } from 'react'
import { Copy } from 'react-feather'
import { Link } from 'react-router-dom'
......
......@@ -8,7 +8,7 @@ import { ExternalLink } from 'theme/components'
const StyledExternalLink = styled(ExternalLink)`
width: fit-content;
border-radius: 16px;
padding: 4px 6px;
padding: 4px 8px;
font-size: 14px;
font-weight: 485;
line-height: 20px;
......
......@@ -247,7 +247,7 @@ function CandlestickTooltip({ data }: { data: PriceChartData }) {
<div>{formatFiatPrice({ price: data.low })}</div>
</RowBetween>
<RowBetween gap="sm">
<Trans i18nKey="chart.price.close" />
<Trans i18nKey="common.close" />
<div>{formatFiatPrice({ price: data.close })}</div>
</RowBetween>
</TooltipText>
......
......@@ -14,7 +14,7 @@ describe('ConfirmSwapModal/Error', () => {
['limit order', PendingModalError.CONFIRMATION_ERROR, LIMIT_ORDER_TRADE, 'Limit failed'],
['limit order', PendingModalError.WRAP_ERROR, LIMIT_ORDER_TRADE, 'Wrap failed'],
])('renders %p correctly, with error= %p', async (testCaseName, errorType, trade, expectedError) => {
const { asFragment } = render(<Error errorType={errorType} trade={trade} onRetry={jest.fn()} />)
const { asFragment } = render(<Error errorType={errorType} trade={trade} onRetry={jest.fn()} showTrade={true} />)
expect(asFragment()).toMatchSnapshot()
expect(screen.getByText(expectedError)).toBeInTheDocument()
})
......
......@@ -4,7 +4,6 @@ import { SwapResult } from 'hooks/useSwapCallback'
import { Trans } from 'i18n'
import { InterfaceTrade, TradeFillType } from 'state/routing/types'
import { isLimitTrade, isUniswapXTrade } from 'state/routing/utils'
import { useTheme } from 'styled-components'
import { TradeSummary } from 'components/ConfirmSwapModal/TradeSummary'
import { DialogButtonType, DialogContent } from 'components/Dialog/Dialog'
......@@ -24,6 +23,7 @@ export enum PendingModalError {
interface ErrorModalContentProps {
errorType: PendingModalError
trade?: InterfaceTrade
showTrade?: boolean
swapResult?: SwapResult
onRetry: () => void
}
......@@ -49,7 +49,7 @@ function getErrorContent({ errorType, trade }: { errorType: PendingModalError; t
case PendingModalError.XV2_HARD_QUOTE_ERROR:
return {
title: <Trans i18nKey="common.swap.failed" />,
message: <Trans i18nKey="common.swap.failed.uniswapX" />,
message: <Trans i18nKey="swap.fail.uniswapX" />,
supportArticleURL: SupportArticleURL.UNISWAP_X_FAILURE,
}
case PendingModalError.CONFIRMATION_ERROR:
......@@ -61,7 +61,7 @@ function getErrorContent({ errorType, trade }: { errorType: PendingModalError; t
} else {
return {
title: <Trans i18nKey="common.swap.failed" />,
message: <Trans i18nKey="common.swap.failed.message" />,
message: <Trans i18nKey="swap.fail.message" />,
supportArticleURL: isUniswapXTrade(trade)
? SupportArticleURL.UNISWAP_X_FAILURE
: SupportArticleURL.TRANSACTION_FAILURE,
......@@ -81,19 +81,18 @@ function getErrorContent({ errorType, trade }: { errorType: PendingModalError; t
}
}
export default function Error({ errorType, trade, swapResult, onRetry }: ErrorModalContentProps) {
const theme = useTheme()
export default function Error({ errorType, trade, showTrade, swapResult, onRetry }: ErrorModalContentProps) {
const { title, message, supportArticleURL } = getErrorContent({ errorType, trade })
return (
<DialogContent
isVisible={true}
icon={<AlertTriangleFilled data-testid="pending-modal-failure-icon" fill={theme.neutral2} size="24px" />}
icon={<AlertTriangleFilled data-testid="pending-modal-failure-icon" size="24px" />}
title={title}
description={message}
body={
<ColumnCenter gap="md">
{trade && <TradeSummary trade={trade} />}
<ColumnCenter gap="sm">
{showTrade && trade && <TradeSummary trade={trade} />}
{supportArticleURL && (
<ExternalLink href={supportArticleURL}>
<Trans i18nKey="common.learnMore.link" />
......@@ -115,7 +114,7 @@ export default function Error({ errorType, trade, swapResult, onRetry }: ErrorMo
}
buttonsConfig={{
left: {
type: DialogButtonType.Accent,
type: DialogButtonType.Primary,
title: <Trans i18nKey="common.tryAgain.error" />,
onClick: onRetry,
},
......
......@@ -5,7 +5,7 @@ import Row from 'components/Row'
import { SupportArticleURL } from 'constants/supportArticles'
import { SwapResult } from 'hooks/useSwapCallback'
import { useUnmountingAnimation } from 'hooks/useUnmountingAnimation'
import { t, Trans } from 'i18n'
import { Trans, t } from 'i18n'
import { ReactNode, useMemo, useRef } from 'react'
import { InterfaceTrade, TradeFillType } from 'state/routing/types'
import { isLimitTrade, isUniswapXTradeType } from 'state/routing/utils'
......@@ -26,8 +26,8 @@ import {
LoadingIndicatorOverlay,
LogoContainer,
} from '../AccountDrawer/MiniPortfolio/Activity/Logos'
import { slideInAnimation, slideOutAnimation } from './animations'
import { TradeSummary } from './TradeSummary'
import { slideInAnimation, slideOutAnimation } from './animations'
const Container = styled(ColumnCenter)`
margin: 48px 0 8px;
......
......@@ -130,7 +130,7 @@ exports[`ConfirmSwapModal/Head should render correctly for a Limit order 1`] = `
width: -moz-fit-content;
width: fit-content;
border-radius: 16px;
padding: 4px 6px;
padding: 4px 8px;
font-size: 14px;
font-weight: 485;
line-height: 20px;
......@@ -380,7 +380,7 @@ exports[`ConfirmSwapModal/Head should render correctly for a classic swap 1`] =
width: -moz-fit-content;
width: fit-content;
border-radius: 16px;
padding: 4px 6px;
padding: 4px 8px;
font-size: 14px;
font-weight: 485;
line-height: 20px;
......
......@@ -272,19 +272,22 @@ export function ConfirmSwapModal({
)}
{/* Error screen handles all error types with custom messaging and retry logic */}
{errorType && showError && (
<SwapError
trade={trade}
swapResult={swapResult}
errorType={errorType}
onRetry={() => {
if (errorType === PendingModalError.XV2_HARD_QUOTE_ERROR) {
onXV2RetryWithClassic?.()
resetToReviewScreen()
} else {
startSwapFlow()
}
}}
/>
<Container $padding="16px">
<SwapError
trade={trade}
showTrade={errorType !== PendingModalError.XV2_HARD_QUOTE_ERROR}
swapResult={swapResult}
errorType={errorType}
onRetry={() => {
if (errorType === PendingModalError.XV2_HARD_QUOTE_ERROR) {
onXV2RetryWithClassic?.()
resetToReviewScreen()
} else {
startSwapFlow()
}
}}
/>
</Container>
)}
</SwapModal>
</ThemeProvider>
......
......@@ -97,12 +97,14 @@ export interface DialogProps {
export function DialogContent({ icon, title, description, body, buttonsConfig }: DialogProps) {
const { left, right, gap } = buttonsConfig ?? {}
return (
<>
<ColumnCenter gap="md">
<ColumnCenter gap="lg">
<ColumnCenter gap="16px">
<IconContainer>{icon}</IconContainer>
<TitleText>{title}</TitleText>
<DescriptionText>{description}</DescriptionText>
{body}
<ColumnCenter gap="sm">
<TitleText>{title}</TitleText>
<DescriptionText>{description}</DescriptionText>
{body}
</ColumnCenter>
</ColumnCenter>
<Row align="center" justify="center" gap={gap ?? 'md'}>
{left && (
......@@ -128,7 +130,7 @@ export function DialogContent({ icon, title, description, body, buttonsConfig }:
</StyledButton>
)}
</Row>
</>
</ColumnCenter>
)
}
......
......@@ -5,14 +5,11 @@ import { StyledSVG } from './shared'
export default function AlertTriangleFilled({ size = '16px', ...rest }: { size?: string; [k: string]: any }) {
const theme = useTheme()
return (
<StyledSVG
viewBox="0 0 16 16"
fill={theme.deprecated_accentWarning}
xmlns="http://www.w3.org/2000/svg"
size={size}
{...rest}
>
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2a1 1 0 0 1 0-2z" />
<StyledSVG fill={theme.neutral2} viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" size={size} {...rest}>
<path
d="m25.2086 20.0103-7.7081-14.41555c-1.4933-2.793-5.5055-2.793-7 0l-7.70806 14.41555c-1.36966 2.562.49005 5.6559 3.40088 5.6559h15.61438c2.9097 0 4.7706-3.095 3.4009-5.6559zm-12.0831-8.3441c0-.483.392-.875.875-.875s.875.392.875.875v4.6667c0 .483-.392.875-.875.875s-.875-.392-.875-.875zm.8984 9.3333c-.644 0-1.1727-.5226-1.1727-1.1666s.517-1.1667 1.161-1.1667h.0117c.6452 0 1.1667.5227 1.1667 1.1667s-.5227 1.1666-1.1667 1.1666z"
fill="#9b9b9b"
/>
</StyledSVG>
)
}
......@@ -4,7 +4,7 @@ import { animated, easings, useSpring, useTransition } from 'react-spring'
import { useGesture } from 'react-use-gesture'
import styled, { css } from 'styled-components'
import { Z_INDEX } from 'theme/zIndex'
import { isMobile } from 'uniswap/src/utils/platform'
import { isMobile } from 'utilities/src/platform'
export const MODAL_TRANSITION_DURATION = 200
......
......@@ -12,7 +12,7 @@ import styled, { useTheme } from 'styled-components'
import { BREAKPOINTS } from 'theme'
import { ThemedText } from 'theme/components'
import { Z_INDEX } from 'theme/zIndex'
import { isWebAndroid, isWebIOS } from 'uniswap/src/utils/platform'
import { isWebAndroid, isWebIOS } from 'utilities/src/platform'
import { getWalletMeta } from 'utils/walletMeta'
const Wrapper = styled.div`
......
......@@ -10,7 +10,7 @@ import useDebounce from 'hooks/useDebounce'
import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes'
import { useIsNftPage } from 'hooks/useIsNftPage'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { useTranslation } from 'i18n'
import { useTranslation } from 'i18n/useTranslation'
import { organizeSearchResults } from 'lib/utils/searchBar'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
......
import { t, Trans } from 'i18n'
import { Trans, t } from 'i18n'
import { useOpenModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
import styled from 'styled-components'
......
......@@ -101,19 +101,17 @@ const Navbar = ({ blur }: { blur: boolean }) => {
const isNavSearchInputVisible = useIsNavSearchInputVisible()
const { account } = useWeb3React()
const [accountDrawerOpen, toggleAccountDrawer] = useAccountDrawer()
const accountDrawer = useAccountDrawer()
const handleUniIconClick = useCallback(() => {
if (account) {
return
}
if (accountDrawerOpen) {
toggleAccountDrawer()
}
accountDrawer.close()
navigate({
pathname: '/',
search: '?intro=true',
})
}, [account, accountDrawerOpen, navigate, toggleAccountDrawer])
}, [account, accountDrawer, navigate])
return (
<>
......
......@@ -3,7 +3,7 @@ import SettingsTab from 'components/Settings'
import { Trans } from 'i18n'
import { ReactNode } from 'react'
import { ArrowLeft } from 'react-feather'
import { Link, useNavigate } from 'react-router-dom'
import { Link, useLocation } from 'react-router-dom'
import { Box } from 'rebass'
import { useAppDispatch } from 'state/hooks'
import { resetMintState } from 'state/mint/actions'
......@@ -68,11 +68,13 @@ export function AddRemoveTabs({
adding,
creating,
autoSlippage,
positionID,
children,
}: {
adding: boolean
creating: boolean
autoSlippage: Percent
positionID?: string
showBackLink?: boolean
children?: ReactNode
}) {
......@@ -80,17 +82,23 @@ export function AddRemoveTabs({
const theme = useTheme()
// reset states on back
const dispatch = useAppDispatch()
const navigate = useNavigate()
const { state } = useLocation()
const location = useLocation()
// detect if back should redirect to v3 or v2 pool page
const poolLink = location.pathname.includes('add/v2')
? '/pools/v2'
: '/pools' + (positionID ? `/${positionID.toString()}` : '')
// If the 'from' state is set by the previous page route back to the previous page, if not, route back to base pool
const target = state?.from ?? poolLink
return (
<Tabs>
<RowBetween style={{ padding: '1rem 1rem 0 1rem' }} align="center">
<StyledLink
to=".."
onClick={(e) => {
e.preventDefault()
navigate(-1)
to={target}
onClick={() => {
if (adding) {
// not 100% sure both of these are needed
dispatch(resetMintState())
......
......@@ -60,14 +60,18 @@ interface InputProps extends Omit<React.HTMLProps<HTMLInputElement>, 'ref' | 'on
maxDecimals?: number
}
export function isInputGreaterThanDecimals(value: string, maxDecimals?: number): boolean {
const decimalGroups = value.split('.')
return !!maxDecimals && decimalGroups.length > 1 && decimalGroups[1].length > maxDecimals
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ value, onUserInput, placeholder, prependSymbol, maxDecimals, ...rest }: InputProps, ref) => {
const { formatterLocale } = useFormatterLocales()
const enforcer = (nextUserInput: string) => {
if (nextUserInput === '' || inputRegex.test(escapeRegExp(nextUserInput))) {
const decimalGroups = nextUserInput.split('.')
if (maxDecimals && decimalGroups.length > 1 && decimalGroups[1].length > maxDecimals) {
if (isInputGreaterThanDecimals(nextUserInput, maxDecimals)) {
return
}
......
......@@ -54,15 +54,18 @@ const StyledChart: typeof Chart = styled(Chart)`
const PDPChartTypeSelector = ({
chartType,
onChartTypeChange,
disabledOption,
}: {
chartType: PoolsDetailsChartType
onChartTypeChange: (c: PoolsDetailsChartType) => void
disabledOption?: PoolsDetailsChartType
}) => (
<ChartTypeSelectorContainer>
<ChartTypeDropdown
options={PDP_CHART_SELECTOR_OPTIONS}
currentChartType={chartType}
onSelectOption={onChartTypeChange}
disabledOption={disabledOption}
/>
</ChartTypeSelectorContainer>
)
......@@ -196,11 +199,17 @@ export default function ChartSection(props: ChartSectionProps) {
return DEFAULT_PILL_TIME_SELECTOR_OPTIONS
}, [activeQuery.chartType, setTimePeriod, timePeriod])
const disabledChartOption = props.poolData?.protocolVersion === ProtocolVersion.V2 ? ChartType.LIQUIDITY : undefined
return (
<div data-testid="pdp-chart-container">
{ChartBody}
<ChartActionsContainer>
<PDPChartTypeSelector chartType={activeQuery.chartType} onChartTypeChange={setChartType} />
<PDPChartTypeSelector
chartType={activeQuery.chartType}
onChartTypeChange={setChartType}
disabledOption={disabledChartOption}
/>
{activeQuery.chartType !== ChartType.LIQUIDITY && (
<TimePeriodSelectorContainer>
<PillMultiToggle
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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