ci(release): publish latest release

parent 0b9f7711
......@@ -5,6 +5,7 @@ ignores: [
'@uniswap/eslint-config',
'i18next',
'moti',
'wrangler',
# Dependencies that depcheck thinks are missing but are actually present or never used
'@yarnpkg/core',
'@yarnpkg/cli',
......
......@@ -60,3 +60,9 @@ apps/mobile/.maestro/scripts/testIds.js
# RNEF
.rnef/
# claude
claude.md
claude.local.md
CLAUDE.md
CLAUDE.local.md
......@@ -11,11 +11,11 @@ __mocks__
*.html
*.inc
*.json
*.jsonc
*.md
*.yml
build
craco.config.cjs
cypress
dist
jest-setup.js
jest.config.js
......
IPFS hash of the deployment:
- CIDv0: `QmbnHL5TQqNFjVrQtUdXNUci2KGYPBypTjbTEm8PQJjBSX`
- CIDv1: `bafybeighxdnvvampcjgznnlroji2h4t4fes5acglqdugvuezoob3hvkmga`
- CIDv0: `QmVzsLbRpsi2d5BKNShX4XHtieNHiwLTMAnf78ehMM1YZ8`
- CIDv1: `bafybeidrzqulidzlilytjjw4hihdvu2iyod7xvuy2f35koh6vebadxnf4m`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
......@@ -10,14 +10,14 @@ You can also access the Uniswap Interface from an IPFS gateway.
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://bafybeighxdnvvampcjgznnlroji2h4t4fes5acglqdugvuezoob3hvkmga.ipfs.dweb.link/
- [ipfs://QmbnHL5TQqNFjVrQtUdXNUci2KGYPBypTjbTEm8PQJjBSX/](ipfs://QmbnHL5TQqNFjVrQtUdXNUci2KGYPBypTjbTEm8PQJjBSX/)
- https://bafybeidrzqulidzlilytjjw4hihdvu2iyod7xvuy2f35koh6vebadxnf4m.ipfs.dweb.link/
- [ipfs://QmVzsLbRpsi2d5BKNShX4XHtieNHiwLTMAnf78ehMM1YZ8/](ipfs://QmVzsLbRpsi2d5BKNShX4XHtieNHiwLTMAnf78ehMM1YZ8/)
### 5.99.3 (2025-06-18)
### 5.100.1 (2025-06-25)
### Bug Fixes
* **web:** dynamic fee hotfix prod (#21010) 6fb9aea
* **web:** use SERVICE_ACCOUNT_PAT instead of GITHUB_TOKEN (#21251) b92a468
web/5.99.3
\ No newline at end of file
web/5.100.1
\ No newline at end of file
......@@ -2,6 +2,10 @@
## Developer Quickstart
### Environment variables
Before running the extension, you need to get the environment variables from 1password in order to get full functionality. Run the command `yarn extension env:local:download` to copy them to your root folder.
### Running the extension locally
To run the extension, run the following from the top level of the monorepo:
......@@ -11,11 +15,7 @@ yarn
yarn extension start
```
### Environment variables
You need to get the environment variables from 1password in order to get full functionality. Run the command `yarn extension env:local:download` to copy them to your root folder.
### Loading the extension into Chrome
Then, load the extension into Chrome:
1. Go to **chrome://extensions**
2. At the top right, turn on **Developer mode**
......
......@@ -38,7 +38,7 @@
"react-native-web": "0.19.13",
"react-qr-code": "2.0.12",
"react-redux": "8.0.5",
"react-router-dom": "6.10.0",
"react-router-dom": "6.30.1",
"redux": "4.2.1",
"redux-logger": "3.0.6",
"redux-persist": "6.0.0",
......
import { useQuery } from '@tanstack/react-query'
import { forwardRef } from 'react'
import { useTranslation } from 'react-i18next'
import { TextInput } from 'react-native'
import { Input, InputProps } from 'src/app/components/Input'
import { biometricUnlockCredentialQuery } from 'src/app/features/biometricUnlock/biometricUnlockCredentialQuery'
import { useShouldShowBiometricUnlock } from 'src/app/features/biometricUnlock/useShouldShowBiometricUnlock'
import { Flex, FlexProps, IconProps, Text, TouchableArea } from 'ui/src'
import { Eye, EyeOff, Fingerprint } from 'ui/src/components/icons'
import { PasswordStrength, getPasswordStrengthTextAndColor } from 'wallet/src/utils/password'
......@@ -57,8 +56,7 @@ export const PasswordInputWithBiometrics = forwardRef<
TextInput,
PasswordInputProps & { onPressBiometricUnlock: () => void }
>(function PasswordInputWithBiometrics({ onPressBiometricUnlock, ...passwordInputProps }, ref): JSX.Element {
const { data: biometricUnlockCredential } = useQuery(biometricUnlockCredentialQuery())
const hasBiometricUnlockCredential = !!biometricUnlockCredential
const shouldShowBiometricUnlock = useShouldShowBiometricUnlock()
return (
<Flex row alignItems="center">
......@@ -66,14 +64,14 @@ export const PasswordInputWithBiometrics = forwardRef<
<PasswordInput
ref={ref}
{...passwordInputProps}
{...(hasBiometricUnlockCredential && {
{...(shouldShowBiometricUnlock && {
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
})}
/>
</Flex>
{hasBiometricUnlockCredential && (
{shouldShowBiometricUnlock && (
<TouchableArea
height="100%"
justifyContent="center"
......
......@@ -35,6 +35,7 @@ import { OTPInput } from 'src/app/features/onboarding/scan/OTPInput'
import { ScanToOnboard } from 'src/app/features/onboarding/scan/ScanToOnboard'
import { ScantasticContextProvider } from 'src/app/features/onboarding/scan/ScantasticContextProvider'
import { OnboardingRoutes, TopLevelRoutes } from 'src/app/navigation/constants'
import { ROUTER_FUTURE_FLAGS, ROUTER_PROVIDER_FUTURE_FLAGS } from 'src/app/navigation/routerConfig'
import { setRouter, setRouterState } from 'src/app/navigation/state'
import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { checksIfSupportsSidePanel } from 'src/app/utils/chrome'
......@@ -139,14 +140,19 @@ const allRoutes = [
},
]
const router = createHashRouter([
const router = createHashRouter(
[
{
path: `/${TopLevelRoutes.Onboarding}`,
element: <OnboardingWrapper />,
errorElement: <ErrorElement />,
children: !supportsSidePanel ? [unsupportedRoute] : allRoutes,
},
],
{
path: `/${TopLevelRoutes.Onboarding}`,
element: <OnboardingWrapper />,
errorElement: <ErrorElement />,
children: !supportsSidePanel ? [unsupportedRoute] : allRoutes,
future: ROUTER_FUTURE_FLAGS,
},
])
)
function ScantasticFlow({ isResetting = false }: { isResetting?: boolean }): JSX.Element {
return (
......@@ -193,7 +199,7 @@ export default function OnboardingApp(): JSX.Element {
<PersistGate persistor={getReduxPersistor()}>
<BaseAppContainer appName={DatadogAppNameTag.Onboarding}>
<PrimaryAppInstanceDebuggerLazy />
<RouterProvider router={router} />
<RouterProvider router={router} future={ROUTER_PROVIDER_FUTURE_FLAGS} />
</BaseAppContainer>
</PersistGate>
)
......
......@@ -7,6 +7,7 @@ import { RouterProvider, createHashRouter } from 'react-router-dom'
import { ErrorElement } from 'src/app/components/ErrorElement'
import { BaseAppContainer } from 'src/app/core/BaseAppContainer'
import { DatadogAppNameTag } from 'src/app/datadog'
import { ROUTER_FUTURE_FLAGS, ROUTER_PROVIDER_FUTURE_FLAGS } from 'src/app/navigation/routerConfig'
import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { Button, Flex, Image, Text } from 'ui/src'
import { UNISWAP_LOGO } from 'ui/src/assets'
......@@ -17,13 +18,18 @@ import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { ExtensionScreens } from 'uniswap/src/types/screens/extension'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics'
const router = createHashRouter([
const router = createHashRouter(
[
{
path: '',
element: <PopupContent />,
errorElement: <ErrorElement />,
},
],
{
path: '',
element: <PopupContent />,
errorElement: <ErrorElement />,
future: ROUTER_FUTURE_FLAGS,
},
])
)
function PopupContent(): JSX.Element {
const { t } = useTranslation()
......@@ -102,7 +108,7 @@ export default function PopupApp(): JSX.Element {
return (
<BaseAppContainer appName={DatadogAppNameTag.Popup}>
<RouterProvider router={router} />
<RouterProvider router={router} future={ROUTER_PROVIDER_FUTURE_FLAGS} />
</BaseAppContainer>
)
}
......@@ -29,6 +29,7 @@ import { SwapFlowScreen } from 'src/app/features/swap/SwapFlowScreen'
import { useIsWalletUnlocked } from 'src/app/hooks/useIsWalletUnlocked'
import { AppRoutes, RemoveRecoveryPhraseRoutes, SettingsRoutes } from 'src/app/navigation/constants'
import { MainContent, WebNavigation } from 'src/app/navigation/navigation'
import { ROUTER_FUTURE_FLAGS, ROUTER_PROVIDER_FUTURE_FLAGS } from 'src/app/navigation/routerConfig'
import { setRouter, setRouterState } from 'src/app/navigation/state'
import { initExtensionAnalytics } from 'src/app/utils/analytics'
import {
......@@ -48,88 +49,93 @@ import { useInterval } from 'utilities/src/time/timing'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics'
import { getReduxPersistor } from 'wallet/src/state/persistor'
const router = createHashRouter([
const router = createHashRouter(
[
{
path: '',
element: <SidebarWrapper />,
errorElement: <ErrorElement />,
children: [
{
path: '',
element: <MainContent />,
},
{
path: AppRoutes.AccountSwitcher,
element: <AccountSwitcherScreen />,
},
{
path: AppRoutes.Settings,
element: <SettingsScreenWrapper />,
children: [
{
path: '',
element: <SettingsScreen />,
},
{
path: SettingsRoutes.ChangePassword,
element: <SettingsChangePasswordScreen />,
},
{
path: SettingsRoutes.DeviceAccess,
element: <DeviceAccessScreen />,
},
isDevEnv()
? {
path: SettingsRoutes.DevMenu,
element: <DevMenuScreen />,
}
: {},
{
path: SettingsRoutes.ViewRecoveryPhrase,
element: <ViewRecoveryPhraseScreen />,
},
{
path: SettingsRoutes.BackupRecoveryPhrase,
element: <BackupRecoveryPhraseScreen />,
},
{
path: SettingsRoutes.RemoveRecoveryPhrase,
children: [
{
path: RemoveRecoveryPhraseRoutes.Wallets,
element: <RemoveRecoveryPhraseWallets />,
},
{
path: RemoveRecoveryPhraseRoutes.Verify,
element: <RemoveRecoveryPhraseVerify />,
},
],
},
{
path: SettingsRoutes.ManageConnections,
element: <SettingsManageConnectionsScreen />,
},
{
path: SettingsRoutes.SmartWallet,
element: <SmartWalletSettingsScreen />,
},
],
},
{
path: AppRoutes.Send,
element: <SendFlow />,
},
{
path: AppRoutes.Swap,
element: <SwapFlowScreen />,
},
{
path: AppRoutes.Receive,
element: <ReceiveScreen />,
},
],
},
],
{
path: '',
element: <SidebarWrapper />,
errorElement: <ErrorElement />,
children: [
{
path: '',
element: <MainContent />,
},
{
path: AppRoutes.AccountSwitcher,
element: <AccountSwitcherScreen />,
},
{
path: AppRoutes.Settings,
element: <SettingsScreenWrapper />,
children: [
{
path: '',
element: <SettingsScreen />,
},
{
path: SettingsRoutes.ChangePassword,
element: <SettingsChangePasswordScreen />,
},
{
path: SettingsRoutes.DeviceAccess,
element: <DeviceAccessScreen />,
},
isDevEnv()
? {
path: SettingsRoutes.DevMenu,
element: <DevMenuScreen />,
}
: {},
{
path: SettingsRoutes.ViewRecoveryPhrase,
element: <ViewRecoveryPhraseScreen />,
},
{
path: SettingsRoutes.BackupRecoveryPhrase,
element: <BackupRecoveryPhraseScreen />,
},
{
path: SettingsRoutes.RemoveRecoveryPhrase,
children: [
{
path: RemoveRecoveryPhraseRoutes.Wallets,
element: <RemoveRecoveryPhraseWallets />,
},
{
path: RemoveRecoveryPhraseRoutes.Verify,
element: <RemoveRecoveryPhraseVerify />,
},
],
},
{
path: SettingsRoutes.ManageConnections,
element: <SettingsManageConnectionsScreen />,
},
{
path: SettingsRoutes.SmartWallet,
element: <SmartWalletSettingsScreen />,
},
],
},
{
path: AppRoutes.Send,
element: <SendFlow />,
},
{
path: AppRoutes.Swap,
element: <SwapFlowScreen />,
},
{
path: AppRoutes.Receive,
element: <ReceiveScreen />,
},
],
future: ROUTER_FUTURE_FLAGS,
},
])
)
const PORT_PING_INTERVAL = 5 * ONE_SECOND_MS
function useDappRequestPortListener(): void {
......@@ -246,7 +252,7 @@ export default function SidebarApp(): JSX.Element {
<BaseAppContainer appName={DatadogAppNameTag.Sidebar}>
<DappContextProvider>
<PrimaryAppInstanceDebuggerLazy />
<RouterProvider router={router} />
<RouterProvider router={router} future={ROUTER_PROVIDER_FUTURE_FLAGS} />
</DappContextProvider>
</BaseAppContainer>
</PersistGate>
......
......@@ -19,6 +19,7 @@ import { UnitagConfirmationScreen } from 'src/app/features/unitags/UnitagConfirm
import { UnitagCreateUsernameScreen } from 'src/app/features/unitags/UnitagCreateUsernameScreen'
import { UnitagIntroScreen } from 'src/app/features/unitags/UnitagIntroScreen'
import { UnitagClaimRoutes } from 'src/app/navigation/constants'
import { ROUTER_FUTURE_FLAGS, ROUTER_PROVIDER_FUTURE_FLAGS } from 'src/app/navigation/routerConfig'
import { setRouter, setRouterState } from 'src/app/navigation/state'
import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { Flex } from 'ui/src'
......@@ -27,31 +28,36 @@ import { usePrevious } from 'utilities/src/react/hooks'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics'
import { useAccountAddressFromUrlWithThrow } from 'wallet/src/features/wallet/hooks'
const router = createHashRouter([
const router = createHashRouter(
[
{
path: '',
element: <UnitagAppInner />,
children: [
{
path: UnitagClaimRoutes.ClaimIntro,
element: <UnitagClaimFlow />,
errorElement: <ErrorElement />,
},
{
path: UnitagClaimRoutes.EditProfile,
element: <UnitagEditProfileFlow />,
errorElement: <ErrorElement />,
},
],
},
],
{
path: '',
element: <UnitagAppInner />,
children: [
{
path: UnitagClaimRoutes.ClaimIntro,
element: <UnitagClaimFlow />,
errorElement: <ErrorElement />,
},
{
path: UnitagClaimRoutes.EditProfile,
element: <UnitagEditProfileFlow />,
errorElement: <ErrorElement />,
},
],
future: ROUTER_FUTURE_FLAGS,
},
])
)
/**
* Note: we are using a pattern here to avoid circular dependencies, because
* this is the root of the app and it imports all sub-pages, we need to push the
* router/router state to a different file so it can be imported by those pages
*/
router.subscribe((state) => {
router.subscribe((state: any) => {
setRouterState(state)
})
......@@ -75,7 +81,7 @@ function UnitagAppInner(): JSX.Element {
// needed to reload on address param change for hash router
router
.navigate(0)
.catch((e) => logger.error(e, { tags: { file: 'UnitagClaimApp.tsx', function: 'UnitagClaimAppInner' } }))
.catch((e: any) => logger.error(e, { tags: { file: 'UnitagClaimApp.tsx', function: 'UnitagClaimAppInner' } }))
}
}, [address, prevAddress])
......@@ -138,7 +144,7 @@ export default function UnitagClaimApp(): JSX.Element {
return (
<BaseAppContainer appName={DatadogAppNameTag.UnitagClaim}>
<RouterProvider router={router} />
<RouterProvider router={router} future={ROUTER_PROVIDER_FUTURE_FLAGS} />
</BaseAppContainer>
)
}
......@@ -115,7 +115,7 @@ export function AccountItem({ address, onAccountSelect, balanceUSD }: AccountIte
e.preventDefault()
e.stopPropagation()
navigateTo(`${AppRoutes.Settings}/${SettingsRoutes.ManageConnections}`)
navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.ManageConnections}`)
},
Icon: Globe,
},
......
......@@ -178,7 +178,7 @@ export function AccountSwitcherScreen(): JSX.Element {
{
label: t('account.wallet.menu.manageConnections'),
onPress: () => navigateTo(`${AppRoutes.Settings}/${SettingsRoutes.ManageConnections}`),
onPress: () => navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.ManageConnections}`),
Icon: Globe,
},
{
......
import { useQuery } from '@tanstack/react-query'
import { biometricUnlockCredentialQuery } from 'src/app/features/biometricUnlock/biometricUnlockCredentialQuery'
import { DynamicConfigs, ExtensionBiometricUnlockConfigKey } from 'uniswap/src/features/gating/configs'
import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks'
export function useShouldShowBiometricUnlock(): boolean {
const isEnabled = useDynamicConfigValue({
config: DynamicConfigs.ExtensionBiometricUnlock,
key: ExtensionBiometricUnlockConfigKey.EnableUnlocking,
defaultValue: true,
})
const hasBiometricUnlockCredential = useHasBiometricUnlockCredential()
return isEnabled && hasBiometricUnlockCredential
}
export function useHasBiometricUnlockCredential(): boolean {
const { data: biometricUnlockCredential } = useQuery(biometricUnlockCredentialQuery())
return !!biometricUnlockCredential
}
import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { builtInBiometricCapabilitiesQuery } from 'src/app/utils/device/builtInBiometricCapabilitiesQuery'
import { DynamicConfigs, ExtensionBiometricUnlockConfigKey } from 'uniswap/src/features/gating/configs'
import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks'
export function useShouldShowBiometricUnlockEnrollment({ flow }: { flow: 'onboarding' | 'settings' }): boolean {
const { t } = useTranslation()
const isEnabled = useDynamicConfigValue({
config: DynamicConfigs.ExtensionBiometricUnlock,
key:
flow === 'onboarding'
? ExtensionBiometricUnlockConfigKey.EnableOnboardingEnrollment
: ExtensionBiometricUnlockConfigKey.EnableSettingsEnrollment,
defaultValue: false,
})
const { data: biometricCapabilities } = useQuery(builtInBiometricCapabilitiesQuery({ t }))
const shouldShowBiometricUnlockEnrollment = isEnabled && Boolean(biometricCapabilities?.hasBuiltInBiometricSensor)
return shouldShowBiometricUnlockEnrollment
}
......@@ -17,7 +17,7 @@ export function HomeIntroCardStack(): JSX.Element | null {
}, [activeAccount.address])
const navigateToBackupFlow = useCallback((): void => {
navigateTo(`${AppRoutes.Settings}/${SettingsRoutes.BackupRecoveryPhrase}`)
navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.BackupRecoveryPhrase}`)
}, [navigateTo])
const { cards } = useSharedIntroCards({
......
import { useQuery } from '@tanstack/react-query'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { PADDING_STRENGTH_INDICATOR, PasswordInput } from 'src/app/components/PasswordInput'
import { useShouldShowBiometricUnlockEnrollment } from 'src/app/features/biometricUnlock/useShouldShowBiometricUnlockEnrollment'
import { BiometricUnlockSetUp } from 'src/app/features/onboarding/BiometricUnlockSetUp'
import { OnboardingScreen } from 'src/app/features/onboarding/OnboardingScreen'
import { useOnboardingSteps } from 'src/app/features/onboarding/OnboardingSteps'
import { TopLevelRoutes } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state'
import { builtInBiometricCapabilitiesQuery } from 'src/app/utils/device/builtInBiometricCapabilitiesQuery'
import { Flex, Square, Text } from 'ui/src'
import { Lock } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ExtensionOnboardingFlow, ExtensionOnboardingScreens } from 'uniswap/src/types/screens/extension'
import { useEvent } from 'utilities/src/react/hooks'
......@@ -28,17 +25,14 @@ export function Password({
onComplete: (password: string) => Promise<void>
onBack?: () => void
}): JSX.Element {
const { t } = useTranslation()
const { resetOnboardingContextData } = useOnboardingContext()
const [password, setPassword] = useState<null | string>(null)
const isBiometricUnlockEnabled = useFeatureFlag(FeatureFlags.ExtensionBiometricUnlock)
const { data: biometricCapabilities } = useQuery(builtInBiometricCapabilitiesQuery({ t }))
const shouldShowBiometricUnlockSetUp = isBiometricUnlockEnabled && biometricCapabilities?.hasBuiltInBiometricSensor
const shouldShowBiometricUnlockEnrollment = useShouldShowBiometricUnlockEnrollment({ flow: 'onboarding' })
const onPasswordNext = useEvent(async (password: string) => {
if (shouldShowBiometricUnlockSetUp) {
if (shouldShowBiometricUnlockEnrollment) {
setPassword(password)
} else {
await onComplete(password)
......
......@@ -21,7 +21,6 @@ import WalletPreviewCard from 'wallet/src/components/WalletPreviewCard/WalletPre
import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext'
import { useImportableAccounts } from 'wallet/src/features/onboarding/hooks/useImportableAccounts'
import { useSelectAccounts } from 'wallet/src/features/onboarding/hooks/useSelectAccounts'
import { useAnyAccountEligibleForDelegation } from 'wallet/src/features/smartWallet/hooks/useAnyAccountEligibleForDelegation'
import { BackupType } from 'wallet/src/features/wallet/accounts/types'
export function SelectWallets({ flow }: { flow: ExtensionOnboardingFlow }): JSX.Element {
......@@ -37,15 +36,11 @@ export function SelectWallets({ flow }: { flow: ExtensionOnboardingFlow }): JSX.
const { importableAccounts, isLoading, showError, refetch } = useImportableAccounts(generatedAddresses)
const { eligible: isAnyAccountEligibleForDelegation, loading: isDelegationChecksLoading } =
useAnyAccountEligibleForDelegation(importableAccounts)
const { selectedAddresses, toggleAddressSelection } = useSelectAccounts(importableAccounts)
const smartWalletEnabled = useFeatureFlag(FeatureFlags.SmartWallet)
const enableSubmit =
(showError || (selectedAddresses.length > 0 && !isLoading)) && !(isDelegationChecksLoading && smartWalletEnabled)
const enableSubmit = showError || (selectedAddresses.length > 0 && !isLoading)
const onSubmit = useEvent(async () => {
if (!enableSubmit) {
......@@ -67,8 +62,8 @@ export function SelectWallets({ flow }: { flow: ExtensionOnboardingFlow }): JSX.
useSubmitOnEnter(showError ? refetch : onSubmit)
const belowFrameContent = useMemo(
() => (smartWalletEnabled && isAnyAccountEligibleForDelegation ? <SmartWalletTooltip /> : undefined),
[smartWalletEnabled, isAnyAccountEligibleForDelegation],
() => (smartWalletEnabled ? <SmartWalletTooltip /> : undefined),
[smartWalletEnabled],
)
return (
......@@ -101,7 +96,7 @@ export function SelectWallets({ flow }: { flow: ExtensionOnboardingFlow }): JSX.
<Text color="$statusCritical" textAlign="center" variant="buttonLabel2">
{t('onboarding.selectWallets.error')}
</Text>
) : isDelegationChecksLoading ? (
) : isLoading ? (
<Flex>
<SelectWalletsSkeleton repeat={3} />
</Flex>
......
......@@ -68,7 +68,7 @@ export function ConnectPopupContent({
}
const openManageConnections = (): void => {
navigateTo(`${AppRoutes.Settings}/${SettingsRoutes.ManageConnections}`)
navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.ManageConnections}`)
}
const fallbackIcon = <DappIconPlaceholder iconSize={iconSizes.icon40} name={dappUrl} />
......
import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { biometricUnlockCredentialQuery } from 'src/app/features/biometricUnlock/biometricUnlockCredentialQuery'
import { useBiometricUnlockDisableMutation } from 'src/app/features/biometricUnlock/useBiometricUnlockDisableMutation'
import { useBiometricUnlockSetupMutation } from 'src/app/features/biometricUnlock/useBiometricUnlockSetupMutation'
import { useHasBiometricUnlockCredential } from 'src/app/features/biometricUnlock/useShouldShowBiometricUnlock'
import { useShouldShowBiometricUnlockEnrollment } from 'src/app/features/biometricUnlock/useShouldShowBiometricUnlockEnrollment'
import { SettingsToggleRow } from 'src/app/features/settings/components/SettingsToggleRow'
import { EnterPasswordModal } from 'src/app/features/settings/password/EnterPasswordModal'
import { builtInBiometricCapabilitiesQuery } from 'src/app/utils/device/builtInBiometricCapabilitiesQuery'
import { Fingerprint } from 'ui/src/components/icons'
import { useEvent } from 'utilities/src/react/hooks'
import { useBooleanState } from 'utilities/src/react/useBooleanState'
......@@ -13,44 +15,49 @@ export function BiometricUnlockSettingsToggleRow(): JSX.Element | null {
const { t } = useTranslation()
const { value: isPasswordModalOpen, setTrue: showPasswordModal, setFalse: hidePasswordModal } = useBooleanState(false)
const { data: biometricUnlockCredential } = useQuery(biometricUnlockCredentialQuery())
const hasBiometricUnlockCredential = useHasBiometricUnlockCredential()
const showBiometricUnlockEnrollment = useShouldShowBiometricUnlockEnrollment({ flow: 'settings' })
// We want to show the toggle when the user has a credential even if enrollment is not available,
// so that they can remove their passkey if they want to.
const showBiometricUnlockToggle = hasBiometricUnlockCredential || showBiometricUnlockEnrollment
const { data: biometricCapabilities } = useQuery(builtInBiometricCapabilitiesQuery({ t }))
const { mutate: setupBiometricUnlock } = useBiometricUnlockSetupMutation()
const { mutate: disableBiometricUnlock } = useBiometricUnlockDisableMutation()
const hasBiometricUnlockCredential = !!biometricUnlockCredential
const onPasswordModalNext = useEvent((password?: string): void => {
hidePasswordModal()
if (!password) {
return
}
const handleToggleChange = useEvent(() => {
if (hasBiometricUnlockCredential) {
disableBiometricUnlock()
} else {
showPasswordModal()
setupBiometricUnlock(password)
}
})
if (!biometricCapabilities?.hasBuiltInBiometricSensor) {
if (!showBiometricUnlockToggle) {
return null
}
return (
<>
<SettingsToggleRow
Icon={biometricCapabilities.icon}
title={biometricCapabilities.name}
Icon={biometricCapabilities?.icon ?? Fingerprint}
title={biometricCapabilities?.name ?? t('common.biometrics.generic')}
checked={hasBiometricUnlockCredential}
onCheckedChange={handleToggleChange}
onCheckedChange={showPasswordModal}
/>
{isPasswordModalOpen && (
<EnterPasswordModal
isOpen={true}
onNext={(password): void => {
hidePasswordModal()
if (password) {
setupBiometricUnlock(password)
}
}}
onNext={onPasswordModalNext}
onClose={hidePasswordModal}
shouldReturnPassword
/>
......
import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { ScreenHeader } from 'src/app/components/layout/ScreenHeader'
import { BiometricUnlockSettingsToggleRow } from 'src/app/features/settings/BiometricUnlock/BiometricUnlockSettingsToggleRow'
import { SettingsItem } from 'src/app/features/settings/components/SettingsItem'
import { AppRoutes, SettingsRoutes } from 'src/app/navigation/constants'
import { useExtensionNavigation } from 'src/app/navigation/utils'
import { builtInBiometricCapabilitiesQuery } from 'src/app/utils/device/builtInBiometricCapabilitiesQuery'
import { Flex, ScrollView } from 'ui/src'
import { Key } from 'ui/src/components/icons/Key'
......@@ -11,9 +13,11 @@ export function DeviceAccessScreen(): JSX.Element {
const { t } = useTranslation()
const { navigateTo } = useExtensionNavigation()
const title = useDeviceAccessScreenTitle()
return (
<Flex fill backgroundColor="$surface1" gap="$spacing8">
<ScreenHeader title={t('settings.setting.deviceAccess.title')} />
<ScreenHeader title={title} />
<ScrollView showsVerticalScrollIndicator={false}>
<BiometricUnlockSettingsToggleRow />
......@@ -21,9 +25,18 @@ export function DeviceAccessScreen(): JSX.Element {
<SettingsItem
Icon={Key}
title={t('settings.setting.password.title')}
onPress={(): void => navigateTo(`${AppRoutes.Settings}/${SettingsRoutes.ChangePassword}`)}
onPress={(): void => navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.ChangePassword}`)}
/>
</ScrollView>
</Flex>
)
}
export function useDeviceAccessScreenTitle(): string {
const { t } = useTranslation()
const { data: biometricCapabilities } = useQuery(builtInBiometricCapabilitiesQuery({ t }))
return biometricCapabilities?.os === 'mac'
? t('settings.setting.deviceAccess.title.touchId')
: t('settings.setting.deviceAccess.title.biometrics')
}
......@@ -32,7 +32,7 @@ export function RemoveRecoveryPhraseWallets(): JSX.Element {
title={t('setting.recoveryPhrase.remove.initial.title')}
onNextPressed={(): void => {
navigateTo(
`${AppRoutes.Settings}/${SettingsRoutes.RemoveRecoveryPhrase}/${RemoveRecoveryPhraseRoutes.Verify}`,
`/${AppRoutes.Settings}/${SettingsRoutes.RemoveRecoveryPhrase}/${RemoveRecoveryPhraseRoutes.Verify}`,
)
}}
>
......
......@@ -2,6 +2,9 @@ import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { ScreenHeader } from 'src/app/components/layout/ScreenHeader'
import { useShouldShowBiometricUnlock } from 'src/app/features/biometricUnlock/useShouldShowBiometricUnlock'
import { useShouldShowBiometricUnlockEnrollment } from 'src/app/features/biometricUnlock/useShouldShowBiometricUnlockEnrollment'
import { useDeviceAccessScreenTitle } from 'src/app/features/settings/DeviceAccessScreen'
import { SettingsItemWithDropdown } from 'src/app/features/settings/SettingsItemWithDropdown'
import ThemeToggle from 'src/app/features/settings/ThemeToggle'
import { SettingsItem } from 'src/app/features/settings/components/SettingsItem'
......@@ -69,7 +72,12 @@ export function SettingsScreen(): JSX.Element {
const appFiatCurrencyInfo = useAppFiatCurrencyInfo()
const hasViewedConnectionMigration = useSelector(selectHasViewedConnectionMigration)
const isBiometricUnlockEnabled = useFeatureFlag(FeatureFlags.ExtensionBiometricUnlock)
const hasBiometricUnlockCredential = useShouldShowBiometricUnlock()
const showBiometricUnlockEnrollment = useShouldShowBiometricUnlockEnrollment({ flow: 'settings' })
const showNewDeviceAccessPage = hasBiometricUnlockCredential || showBiometricUnlockEnrollment
const deviceAccessScreenTitle = useDeviceAccessScreenTitle()
const isSmartWalletEnabled = useFeatureFlag(FeatureFlags.SmartWalletSettings)
const signerAccount = useSignerAccounts()[0]
......@@ -118,7 +126,7 @@ export function SettingsScreen(): JSX.Element {
const handleAdvancedModalClose = useCallback(() => setIsAdvancedModalOpen(false), [])
const handleSmartWalletPress = useCallback(() => {
navigateTo(`${AppRoutes.Settings}/${SettingsRoutes.SmartWallet}`)
navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.SmartWallet}`)
setIsAdvancedModalOpen(false)
}, [navigateTo])
......@@ -184,7 +192,7 @@ export function SettingsScreen(): JSX.Element {
<SettingsItem
Icon={Settings}
title="Developer Settings"
onPress={(): void => navigateTo(`${AppRoutes.Settings}/${SettingsRoutes.DevMenu}`)}
onPress={(): void => navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.DevMenu}`)}
/>
)}
</>
......@@ -273,23 +281,23 @@ export function SettingsScreen(): JSX.Element {
)}
<Flex pt="$padding16">
<SettingsSection title={t('settings.section.privacyAndSecurity')}>
{isBiometricUnlockEnabled ? (
{showNewDeviceAccessPage ? (
<SettingsItem
Icon={Lock}
title={t('settings.setting.deviceAccess.title')}
onPress={(): void => navigateTo(`${AppRoutes.Settings}/${SettingsRoutes.DeviceAccess}`)}
title={deviceAccessScreenTitle}
onPress={(): void => navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.DeviceAccess}`)}
/>
) : (
<SettingsItem
Icon={Key}
title={t('settings.setting.password.title')}
onPress={(): void => navigateTo(`${AppRoutes.Settings}/${SettingsRoutes.ChangePassword}`)}
onPress={(): void => navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.ChangePassword}`)}
/>
)}
<SettingsItem
Icon={FileListLock}
title={t('settings.setting.recoveryPhrase.title')}
onPress={(): void => navigateTo(`${AppRoutes.Settings}/${SettingsRoutes.ViewRecoveryPhrase}`)}
onPress={(): void => navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.ViewRecoveryPhrase}`)}
/>
<>
{hasPasskeyBackup && (
......
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { useExtensionNavigation } from 'src/app/navigation/utils'
import { Flex } from 'ui/src'
......@@ -8,8 +8,10 @@ import { selectFilteredChainIds } from 'uniswap/src/features/transactions/swap/c
import { useSwapPrefilledState } from 'uniswap/src/features/transactions/swap/form/hooks/useSwapPrefilledState'
import { prepareSwapFormState } from 'uniswap/src/features/transactions/types/transactionState'
import { CurrencyField } from 'uniswap/src/types/currency'
import { logger } from 'utilities/src/logger/logger'
import { WalletSwapFlow } from 'wallet/src/features/transactions/swap/WalletSwapFlow'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
import { invalidateAndRefetchWalletDelegationQueries } from 'wallet/src/features/transactions/watcher/transactionFinalizationSaga'
import { useActiveAccountWithThrow, useSignerAccounts } from 'wallet/src/features/wallet/hooks'
export function SwapFlowScreen(): JSX.Element {
const { navigateBack, locationState } = useExtensionNavigation()
......@@ -27,6 +29,17 @@ export function SwapFlowScreen(): JSX.Element {
filteredChainIdsOverride: ignorePersistedFilteredChainIds ? undefined : persistedFilteredChainIds,
})
const signerMnemonicAccounts = useSignerAccounts()
const chains = useEnabledChains()
const accountAddresses = signerMnemonicAccounts.map((account) => account.address)
// Update flow start timestamp every time modal is opened for logging
useEffect(() => {
invalidateAndRefetchWalletDelegationQueries({ accountAddresses, chainIds: chains.chains }).catch((error) =>
logger.debug('SwapFlowScreen', 'useEffect', 'Failed to invalidate and refetch wallet delegation queries', error),
)
}, [accountAddresses, chains.chains])
/** Initialize the initial state once. On navigation the locationState changes causing an unwanted re-render. */
const [initialTransactionState] = useState(() => locationState?.initialTransactionState ?? initialState)
......
......@@ -33,7 +33,7 @@ export function StorageWarningModal({ isOnboarding }: StorageWarningModalProps):
? undefined
: (): void => {
onStorageWarningClose()
navigateTo(`${AppRoutes.Settings}/${SettingsRoutes.ViewRecoveryPhrase}`)
navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.ViewRecoveryPhrase}`)
}
}
/>
......
......@@ -24,6 +24,7 @@ import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { TransactionHistoryUpdater } from 'wallet/src/features/transactions/TransactionHistoryUpdater'
import { WalletUniswapProvider } from 'wallet/src/features/transactions/contexts/WalletUniswapContext'
import { QueuedOrderModal } from 'wallet/src/features/transactions/swap/modals/QueuedOrderModal'
import { NativeWalletProvider } from 'wallet/src/features/wallet/providers/NativeWalletProvider'
export function MainContent(): JSX.Element {
const isOnboarded = useSelector(isOnboardedSelector)
......@@ -131,12 +132,14 @@ export function WebNavigation(): JSX.Element {
return (
<SideBarNavigationProvider>
<WalletUniswapProvider>
<NotificationToastWrapper />
{shouldRestoreScroll && <ScrollRestoration />}
{childrenMemo}
{isLoggedIn && <ForceUpgradeModal />}
</WalletUniswapProvider>
<NativeWalletProvider>
<WalletUniswapProvider>
<NotificationToastWrapper />
{shouldRestoreScroll && <ScrollRestoration />}
{childrenMemo}
{isLoggedIn && <ForceUpgradeModal />}
</WalletUniswapProvider>
</NativeWalletProvider>
</SideBarNavigationProvider>
)
}
......
// Shared future flags configuration for createHashRouter
export const ROUTER_FUTURE_FLAGS = {
v7_relativeSplatPath: true,
v7_fetcherPersist: true,
v7_normalizeFormMethod: true,
v7_partialHydration: true,
}
// Shared future flags configuration for RouterProvider
export const ROUTER_PROVIDER_FUTURE_FLAGS = {
v7_startTransition: true,
}
......@@ -11,6 +11,7 @@ type BuiltInBiometricCapabilities = {
name: string
icon: GeneratedIcon
hasBuiltInBiometricSensor: boolean
os: chrome.runtime.PlatformOs
}
export function builtInBiometricCapabilitiesQuery({ t }: { t: TFunction }) {
......@@ -27,6 +28,7 @@ async function getBuiltInBiometricCapabilities({ t }: { t: TFunction }): Promise
const { os } = await getChromeRuntimeWithThrow().getPlatformInfo()
return {
os,
hasBuiltInBiometricSensor: await isUserVerifyingPlatformAuthenticatorAvailable(),
...getPlatformAuthenticatorNameAndIcon({ os, t }),
}
......
......@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Uniswap Extension",
"description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.",
"version": "1.23.0",
"version": "1.24.0",
"minimum_chrome_version": "116",
"icons": {
"16": "assets/icon16.png",
......
......@@ -71,9 +71,9 @@ if (isCI && datadogPropertiesAvailable) {
apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle"
}
def devVersionName = "1.53"
def betaVersionName = "1.53"
def prodVersionName = "1.53"
def devVersionName = "1.54"
def betaVersionName = "1.54"
def prodVersionName = "1.54"
android {
ndkVersion rootProject.ext.ndkVersion
......
......@@ -4,7 +4,7 @@
"private": true,
"license": "GPL-3.0-or-later",
"scripts": {
"android": "rnef run:android --variant=devDebug --app-id-suffix=dev",
"android": "rnef run:android --variant=devDebug --app-id-suffix=dev && yarn start",
"android:release": "rnef run:android --variant=devRelease --app-id-suffix=dev",
"android:beta": "rnef run:android --variant=betaDebug --app-id-suffix=beta",
"android:beta:release": "rnef run:android --variant=betaRelease --app-id-suffix=beta",
......@@ -30,7 +30,7 @@
"firestore:deploy:rules": "firebase deploy --only firestore:rules",
"link:assets": "react-native-asset",
"check:circular": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 0",
"ios": "rnef run:ios --scheme Uniswap --configuration Debug",
"ios": "rnef run:ios --scheme Uniswap --configuration Debug && yarn start",
"ios:smol": "rnef run:ios --device=\"iPhone SE (3rd generation)\"",
"ios:dev:release": "rnef run:ios --configuration Dev",
"ios:beta": "rnef run:ios --configuration Beta",
......@@ -39,7 +39,7 @@
"format": "../../scripts/prettier.sh",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings=0",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"start": "NODE_ENV=development rnef start",
"start": "NODE_ENV=development rnef start --client-logs",
"start:production": "NODE_ENV=production rnef start --reset-cache",
"test": "node --max-old-space-size=8912 ../../node_modules/.bin/jest",
"snapshots": "jest -u",
......
#!/bin/bash
MAX_SIZE=23.33
MAX_SIZE=23.44
MAX_BUFFER=0.5
# Check OS type and use appropriate stat command
......
......@@ -87,7 +87,8 @@ import { WalletUniswapProvider } from 'wallet/src/features/transactions/contexts
import { Account } from 'wallet/src/features/wallet/accounts/types'
import { WalletContextProvider } from 'wallet/src/features/wallet/context'
import { useAccounts } from 'wallet/src/features/wallet/hooks'
import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider'
import { NativeWalletProvider } from 'wallet/src/features/wallet/providers/NativeWalletProvider'
import { SharedWalletProvider as SharedWalletReduxProvider } from 'wallet/src/providers/SharedWalletProvider'
enableFreeze(true)
......@@ -152,14 +153,14 @@ function App(): JSX.Element | null {
<StrictMode>
<I18nextProvider i18n={i18n}>
<SafeAreaProvider>
<SharedWalletProvider reduxStore={store}>
<SharedWalletReduxProvider reduxStore={store}>
<AnalyticsNavigationContextProvider
shouldLogScreen={shouldLogScreen}
useIsPartOfNavigationTree={useIsPartOfNavigationTree}
>
<AppOuter />
</AnalyticsNavigationContextProvider>
</SharedWalletProvider>
</SharedWalletReduxProvider>
</SafeAreaProvider>
</I18nextProvider>
</StrictMode>
......@@ -254,14 +255,16 @@ function AppOuter(): JSX.Element | null {
<DataUpdaters />
<NavigationContainer>
<MobileWalletNavigationProvider>
<WalletUniswapProvider>
<BottomSheetModalProvider>
<AppModals />
<PerformanceProfiler onReportPrepared={onReportPrepared}>
<AppInner />
</PerformanceProfiler>
</BottomSheetModalProvider>
</WalletUniswapProvider>
<NativeWalletProvider>
<WalletUniswapProvider>
<BottomSheetModalProvider>
<AppModals />
<PerformanceProfiler onReportPrepared={onReportPrepared}>
<AppInner />
</PerformanceProfiler>
</BottomSheetModalProvider>
</WalletUniswapProvider>
</NativeWalletProvider>
<NotificationToastWrapper />
</MobileWalletNavigationProvider>
</NavigationContainer>
......
......@@ -7,7 +7,7 @@ import {
import { NativeStackNavigationProp, NativeStackScreenProps } from '@react-navigation/native-stack'
import { TokenWarningModalState } from 'src/app/modals/TokenWarningModalState'
import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState'
import { RestoreWalletModalState } from 'src/components/RestoreWalletModal/RestoreWalletModalState'
import { RestoreWalletModalState, WalletRestoreType } from 'src/components/RestoreWalletModal/RestoreWalletModalState'
import { ConnectionsDappsListModalState } from 'src/components/Settings/ConnectionsDappModal/ConnectionsDappsListModalState'
import { EditWalletSettingsModalState } from 'src/components/Settings/EditWalletModal/EditWalletSettingsModalState'
import { ManageWalletsModalState } from 'src/components/Settings/ManageWalletsModalState'
......@@ -113,6 +113,7 @@ export type SettingsStackParamList = {
export type OnboardingStackBaseParams = {
importType: ImportType
entryPoint: OnboardingEntryPoint
restoreType?: WalletRestoreType
}
export type OnboardingStackParamList = {
......
......@@ -11,7 +11,8 @@ import { ArrowDownCircleFilledWithBorder, WalletFilled } from 'ui/src/components
import { spacing } from 'ui/src/theme'
import { GenericHeader } from 'uniswap/src/components/misc/GenericHeader'
import { Modal } from 'uniswap/src/components/modals/Modal'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile'
......@@ -35,19 +36,21 @@ export function RestoreWalletModal({ route }: AppStackScreenProp<typeof ModalNam
const { onClose } = useReactNavigationModal()
const restoreType = route.params.restoreType
const { title, description, isDismissible } = useMemo(() => {
const { title, description, isDismissible, modalName } = useMemo(() => {
switch (restoreType) {
case WalletRestoreType.SeedPhrase:
return {
title: t('account.wallet.restore.seed_phrase.title'),
description: t('account.wallet.restore.seed_phrase.description'),
isDismissible: true,
modalName: ModalName.RestoreWalletSeedPhrase,
}
case WalletRestoreType.NewDevice:
return {
title: t('account.wallet.restore.new_device.title'),
description: t('account.wallet.restore.new_device.description'),
isDismissible: false,
modalName: ModalName.RestoreWallet,
}
default:
return {}
......@@ -65,6 +68,7 @@ export function RestoreWalletModal({ route }: AppStackScreenProp<typeof ModalNam
params: {
entryPoint: OnboardingEntryPoint.Sidebar,
importType: ImportType.RestoreMnemonic,
restoreType,
},
})
break
......@@ -75,6 +79,7 @@ export function RestoreWalletModal({ route }: AppStackScreenProp<typeof ModalNam
params: {
entryPoint: OnboardingEntryPoint.Sidebar,
importType: ImportType.RestoreMnemonic,
restoreType,
},
})
break
......@@ -87,7 +92,7 @@ export function RestoreWalletModal({ route }: AppStackScreenProp<typeof ModalNam
hideHandlebar
backgroundColor={colors.surface1.val}
isDismissible={isDismissible}
name={ModalName.RestoreWallet}
name={modalName ?? ModalName.RestoreWallet}
onClose={onClose}
>
<Flex centered gap="$spacing24" px="$spacing24" py="$spacing12" backgroundColor="$surface1">
......@@ -127,15 +132,19 @@ export function RestoreWalletModal({ route }: AppStackScreenProp<typeof ModalNam
<GenericHeader title={title} titleVariant="subheading1" subtitle={description} subtitleVariant="body3" />
<Flex gap="$spacing8" width="100%">
<Flex row>
<Button testID={TestID.Continue} variant="branded" emphasis="primary" size="medium" onPress={onRestore}>
{t('common.button.continue')}
</Button>
<Trace logPress element={ElementName.Continue}>
<Button testID={TestID.Continue} variant="branded" emphasis="primary" size="medium" onPress={onRestore}>
{t('common.button.continue')}
</Button>
</Trace>
</Flex>
{isDismissible && (
<Flex row>
<Button testID={TestID.Cancel} variant="default" emphasis="secondary" size="medium" onPress={onClose}>
{t('common.button.notNow')}
</Button>
<Trace logPress element={ElementName.Cancel}>
<Button testID={TestID.Cancel} variant="default" emphasis="secondary" size="medium" onPress={onClose}>
{t('common.button.notNow')}
</Button>
</Trace>
</Flex>
)}
</Flex>
......
......@@ -14,10 +14,12 @@ export const ExploreScreenSearchResultsList = memo(function _ExploreScreenSearch
searchQuery,
parsedSearchQuery,
chainFilter,
parsedChainFilter,
}: {
searchQuery: string
parsedSearchQuery: string | null
chainFilter: UniverseChainId | null
parsedChainFilter: UniverseChainId | null
}): JSX.Element {
const debouncedSearchQuery = useDebounce(searchQuery)
const debouncedParsedSearchQuery = useDebounce(parsedSearchQuery)
......@@ -58,6 +60,7 @@ export const ExploreScreenSearchResultsList = memo(function _ExploreScreenSearch
{searchQuery && searchQuery.length > 0 ? (
<SearchModalResultsList
chainFilter={chainFilter}
parsedChainFilter={parsedChainFilter}
debouncedParsedSearchFilter={debouncedParsedSearchQuery}
debouncedSearchFilter={debouncedSearchQuery}
searchFilter={searchQuery}
......
......@@ -46,6 +46,10 @@ export function CloudBackupProcessingAnimation({
const account = activeAccount || onboardingContextAccount
// Compute backup status at component level to avoid stale closure - ensures fresh evaluation on each render
// when onboardingContextAccount updates with new backup state
const accountHasCloudBackup = hasBackup(BackupType.Cloud, account)
if (!account) {
throw Error('No account available for backup')
}
......@@ -56,14 +60,14 @@ export function CloudBackupProcessingAnimation({
// Handle finished backing up to Cloud
useEffect(() => {
if (hasBackup(BackupType.Cloud, account)) {
if (accountHasCloudBackup) {
doneProcessing()
// Show success state for 1s before navigating
const timer = setTimeout(onBackupComplete, ONE_SECOND_MS)
return () => clearTimeout(timer)
}
return undefined
}, [account, onBackupComplete])
}, [accountHasCloudBackup, onBackupComplete])
// Handle backup to Cloud when screen appears
const backup = useCallback(async () => {
......
......@@ -92,7 +92,8 @@ export function ExploreScreen(): JSX.Element {
<ExploreScreenSearchResultsList
searchQuery={searchFilter ?? ''}
parsedSearchQuery={parsedSearchFilter}
chainFilter={chainFilter ?? parsedChainFilter}
chainFilter={chainFilter}
parsedChainFilter={parsedChainFilter}
/>
) : (
isSheetReady && canRenderList && <ExploreSections listRef={listRef} />
......
......@@ -40,10 +40,8 @@ const ANDROID_E2E_WORKAROUND = config.isE2ETest && isAndroid
export function RestoreCloudBackupLoadingScreen({ navigation, route: { params } }: Props): JSX.Element {
const { t } = useTranslation()
const dispatch = useDispatch()
const entryPoint = params.entryPoint
const importType = params.importType
const isRestoringMnemonic = importType === ImportType.RestoreMnemonic
const isRestoringMnemonic = params.importType === ImportType.RestoreMnemonic
// inits with null before fetchCloudStorageBackups starts fetching
const [isLoading, setIsLoading] = useState<boolean | null>(null)
const [isError, setIsError] = useState(false)
......@@ -132,17 +130,15 @@ export function RestoreCloudBackupLoadingScreen({ navigation, route: { params }
}
if (backups.length === 1 && backups[0]) {
navigation.replace(OnboardingScreens.RestoreCloudBackupPassword, {
importType,
entryPoint,
...params,
mnemonicId: backups[0].mnemonicId,
})
} else {
navigation.replace(OnboardingScreens.RestoreCloudBackup, {
importType,
entryPoint,
...params,
})
}
}, [backups, entryPoint, importType, isLoading, navigation])
}, [backups, isLoading, navigation, params])
if (isError) {
return (
......@@ -162,9 +158,8 @@ export function RestoreCloudBackupLoadingScreen({ navigation, route: { params }
if (isLoading === false && backups.length === 0) {
if (isRestoringMnemonic) {
navigation.replace(OnboardingScreens.SeedPhraseInput, {
...params,
showAsCloudBackupFallback: true,
importType,
entryPoint,
})
} else {
return (
......
......@@ -39,7 +39,7 @@ export function RestoreMethodScreen({ navigation, route: { params } }: Props): J
const handleOnPress = async (nav: OnboardingScreens, importType: ImportType): Promise<void> => {
navigation.navigate({
name: nav,
params: { importType, entryPoint },
params: { ...params, importType, entryPoint },
merge: true,
})
}
......
......@@ -18,7 +18,8 @@ import { Button, Flex, MobileDeviceHeight, Text, TouchableArea, useIsShortMobile
import { PapersText, QuestionInCircleFilled } from 'ui/src/components/icons'
import { uniswapUrls } from 'uniswap/src/constants/urls'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { ElementName, MobileEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { ImportType } from 'uniswap/src/types/onboarding'
import { OnboardingScreens } from 'uniswap/src/types/screens/mobile'
......@@ -77,6 +78,7 @@ export function SeedPhraseInputScreen({ navigation, route: { params } }: SeedPhr
)
const handleSubmitError: NativeSeedPhraseInputProps['onSubmitError'] = useCallback(() => {
sendAnalyticsEvent(MobileEventName.SeedPhraseInputSubmitError)
setIsSubmitEnabled(true)
}, [setIsSubmitEnabled])
......
import { NativeStackScreenProps } from '@react-navigation/native-stack'
import React, { ComponentProps, useCallback } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { ScrollView, StyleSheet } from 'react-native'
import { ScrollView } from 'react-native'
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
import { navigate } from 'src/app/navigation/rootNavigation'
import { OnboardingStackParamList } from 'src/app/navigation/types'
import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen'
import {
Button,
Flex,
LinearGradient,
Loader,
Text,
TouchableArea,
useLayoutAnimationOnChange,
useSporeColors,
} from 'ui/src'
import { Button, Flex, Loader, Text, TouchableArea, useLayoutAnimationOnChange } from 'ui/src'
import { WalletFilled } from 'ui/src/components/icons'
import { opacify, spacing } from 'ui/src/theme'
import { spacing } from 'ui/src/theme'
import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
......@@ -30,7 +21,6 @@ import WalletPreviewCard from 'wallet/src/components/WalletPreviewCard/WalletPre
import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext'
import { useImportableAccounts } from 'wallet/src/features/onboarding/hooks/useImportableAccounts'
import { useSelectAccounts } from 'wallet/src/features/onboarding/hooks/useSelectAccounts'
import { useAnyAccountEligibleForDelegation } from 'wallet/src/features/smartWallet/hooks/useAnyAccountEligibleForDelegation'
const ANIMATION_DURATION = 300
......@@ -40,7 +30,6 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS
const { t } = useTranslation()
const { selectImportedAccounts, getImportedAccountsAddresses } = useOnboardingContext()
const importedAddresses = getImportedAccountsAddresses()
const colors = useSporeColors()
const {
importableAccounts,
......@@ -78,13 +67,9 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS
const highlightComponent = <CustomHighlightText />
const { eligible: isAnyAccountEligibleForDelegation, loading: isDelegationChecksLoading } =
useAnyAccountEligibleForDelegation(importableAccounts)
const isContinueButtonDisabled = isLoading || !!showError || selectedAddresses.length === 0
const isContinueButtonDisabled =
isLoading || !!showError || selectedAddresses.length === 0 || (isDelegationChecksLoading && smartWalletEnabled)
const showSmartWalletDisclaimer = smartWalletEnabled && !isContinueButtonDisabled && isAnyAccountEligibleForDelegation
const showSmartWalletDisclaimer = smartWalletEnabled && !isContinueButtonDisabled
const opacityStyle = useAnimatedStyle(
() => ({
......@@ -108,59 +93,44 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS
title={t('account.wallet.select.error')}
onRetry={refetchAccounts}
/>
) : isLoading || isDelegationChecksLoading ? (
) : isLoading ? (
<Flex grow justifyContent="space-between" px="$spacing16">
<Loader.Wallets repeat={5} />
</Flex>
) : (
<Flex flexGrow={1} flexShrink={1}>
<LinearGradient
colors={[colors.surface1.val, opacify(0, colors.surface1.val)]}
end={{ x: 0, y: 1 }}
start={{ x: 0, y: 0 }}
style={ListSheet.topGradient}
/>
<ScrollView testID={TestID.SelectWalletScreenLoaded}>
<Flex height="$spacing12" />
<Flex gap="$gap12">
{importableAccounts?.map((account, i) => {
const { address, balance } = account
// prevents flickering and incorrect width calculation for long wallet names on Android
// it's not possible to deselect last wallet
if (selectedAddresses.length === 0) {
return null
}
return (
<Flex key={address} px="$spacing16">
<WalletPreviewCard
key={address}
address={address}
balance={balance}
hideSelectionCircle={isOnlyOneAccount}
name={ElementName.WalletCard}
selected={selectedAddresses.includes(address)}
testID={`${TestID.WalletCard}-${i + 1}`}
onSelect={toggleAddressSelection}
/>
</Flex>
)
})}
</Flex>
</ScrollView>
<LinearGradient
colors={[opacify(0, colors.surface1.val), colors.surface1.val]}
end={{ x: 0, y: 1 }}
start={{ x: 0, y: 0 }}
style={ListSheet.bottomGradient}
/>
</Flex>
<ScrollView testID={TestID.SelectWalletScreenLoaded}>
<Flex height="$spacing12" />
<Flex gap="$gap12">
{importableAccounts?.map((account, i) => {
const { address, balance } = account
// prevents flickering and incorrect width calculation for long wallet names on Android
// it's not possible to deselect last wallet
if (selectedAddresses.length === 0) {
return null
}
return (
<Flex key={address} px="$spacing16">
<WalletPreviewCard
key={address}
address={address}
balance={balance}
hideSelectionCircle={isOnlyOneAccount}
name={ElementName.WalletCard}
selected={selectedAddresses.includes(address)}
testID={`${TestID.WalletCard}-${i + 1}`}
onSelect={toggleAddressSelection}
/>
</Flex>
)
})}
</Flex>
</ScrollView>
)}
<Animated.View
style={[
opacityStyle,
{
marginBottom: spacing.spacing16,
marginTop: spacing.spacing16,
marginHorizontal: spacing.spacing24,
},
]}
......@@ -185,7 +155,7 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS
<Flex opacity={showError ? 0 : 1} px="$spacing16">
<Flex row>
<Button
isDisabled={isLoading || !!showError || selectedAddresses.length === 0}
isDisabled={isContinueButtonDisabled}
variant="branded"
size="large"
testID={TestID.Continue}
......@@ -200,24 +170,6 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS
)
}
const ListSheet = StyleSheet.create({
bottomGradient: {
bottom: 0,
height: spacing.spacing16,
left: 0,
position: 'absolute',
width: '100%',
},
topGradient: {
height: spacing.spacing16,
left: 0,
position: 'absolute',
top: 0,
width: '100%',
zIndex: 1,
},
})
function CustomHighlightText(props: ComponentProps<typeof Text>): JSX.Element {
return <Text variant="buttonLabel4" color="$neutral1" {...props} />
}
......@@ -38,6 +38,7 @@ export function onRestoreComplete({
sendAnalyticsEvent(MobileEventName.RestoreSuccess, {
is_restoring_mnemonic: isRestoringMnemonic,
import_type: params.importType,
restore_type: params.restoreType,
screen,
})
}
......@@ -91,18 +91,22 @@ export function ViewPrivateKeysScreen({ navigation, route }: Props): JSX.Element
<BulletRow Icon={Laptop} description={t('privateKeys.export.modal.speedbump.bullet3')} />
</Flex>
<Flex row py="$spacing24" gap="$gap8">
<Button variant="default" emphasis="secondary" size="medium" onPress={navigation.goBack}>
{t('common.button.close')}
</Button>
<Button
variant="branded"
emphasis="primary"
size="medium"
testID={TestID.Continue}
onPress={onBiometricContinue}
>
{t('common.button.continue')}
</Button>
<Trace logPress element={ElementName.Cancel}>
<Button variant="default" emphasis="secondary" size="medium" onPress={navigation.goBack}>
{t('common.button.close')}
</Button>
</Trace>
<Trace logPress element={ElementName.Continue}>
<Button
variant="branded"
emphasis="primary"
size="medium"
testID={TestID.Continue}
onPress={onBiometricContinue}
>
{t('common.button.continue')}
</Button>
</Trace>
</Flex>
</Flex>
)
......
......@@ -48,8 +48,6 @@ ignores: [
'detect-package-manager',
'eslint-plugin-storybook',
'prop-types',
## Testing
'@types/testing-library__cypress',
## i18n
'dotenv-cli',
'@crowdin/cli',
......@@ -70,6 +68,7 @@ ignores: [
'constants',
'dev',
'featureFlags',
'features',
'hooks',
'lib',
'locales',
......
......@@ -12,9 +12,6 @@ module.exports = {
},
},
rules: {
// TODO: had to add this rule to avoid errors on monorepo migration that didnt happen in interface
'cypress/unsafe-to-chain-command': 'off',
// let prettier do things:
semi: 0,
quotes: 0,
......@@ -27,7 +24,6 @@ module.exports = {
{
files: [
'src/index.tsx',
'cypress/utils/index.ts',
'src/tracing/index.ts',
'src/state/index.ts',
'src/state/explore/index.tsx',
......
......@@ -30,6 +30,8 @@ bundlemeta.json
# misc
.DS_Store
tsconfig.tsbuildinfo
!.env
.env.local
......@@ -52,10 +54,6 @@ notes.txt
package-lock.json
cypress/downloads
cypress/videos
cypress/screenshots
.vercel
.wrangler
......
import { defineConfig } from 'cypress'
import { setupHardhatEvents } from 'cypress-hardhat'
const TIMEOUT = 24000 // 2x average block time
export default defineConfig({
projectId: 'fabfoi',
defaultCommandTimeout: TIMEOUT,
requestTimeout: TIMEOUT,
chromeWebSecurity: false,
experimentalMemoryManagement: true, // better memory management, see https://github.com/cypress-io/cypress/pull/25462
retries: { runMode: process.env.CYPRESS_RETRIES ? +process.env.CYPRESS_RETRIES : 1 },
video: false, // GH provides 2 CPUs, and cypress video eats one up, see https://github.com/cypress-io/cypress/issues/20468#issuecomment-1307608025
e2e: {
async setupNodeEvents(on, config) {
await setupHardhatEvents(on, config)
return config
},
baseUrl: 'http://localhost:3000',
specPattern: 'cypress/{e2e,staging}/**/*.test.ts',
},
})
This diff is collapsed.
// see https://github.com/Uniswap/interface/pull/4115
describe('Link', () => {
it('should update route', () => {
cy.viewport(2000, 1600)
cy.visit('/swap')
cy.contains('Pool').click()
cy.get('[data-cy="join-pool-button"]').should('exist')
})
})
import { BigNumber } from '@ethersproject/bignumber'
import { MaxUint160, MaxUint256 } from '@uniswap/permit2-sdk'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { DAI, USDT } from 'uniswap/src/constants/tokens'
import { HARDHAT_TIMEOUT, setupHardhat } from '../utils'
/** Initiates a swap. */
function initiateSwap(swapButtonText?: string) {
// The swap button is re-rendered once enabled, so we must wait until the original button is not disabled to re-select the appropriate button.
cy.get('#swap-button').should('not.be.disabled')
// Completes the swap.
cy.get('#swap-button').click()
cy.contains(swapButtonText ?? 'Confirm swap').click()
}
describe('Permit2', () => {
function setupInputs(inputToken: Token, outputToken: Token) {
// Sets up a swap between inputToken and outputToken.
cy.visit(`/swap/?inputCurrency=${inputToken.address}&outputCurrency=${outputToken.address}`)
cy.get('#swap-currency-input .token-amount-input').type('0.01')
}
/** Asserts permit2 has a max approval for spend of the input token on-chain. */
function expectTokenAllowanceForPermit2ToBeMax(inputToken: Token) {
// check token approval
cy.hardhat()
.then(({ approval, wallet }) => approval.getTokenAllowanceForPermit2({ owner: wallet, token: inputToken }))
.then((allowance) => {
Cypress.log({ name: `Token allowance: ${allowance.toString()}` })
cy.wrap(allowance).should('deep.equal', MaxUint256)
})
}
/** Asserts the universal router has a max permit2 approval for spend of the input token on-chain. */
function expectPermit2AllowanceForUniversalRouterToBeMax(inputToken: Token) {
cy.hardhat()
.then(({ approval, wallet }) => approval.getPermit2Allowance({ owner: wallet, token: inputToken }))
.then((allowance) => {
Cypress.log({ name: `Permit2 allowance: ${allowance.amount.toString()}` })
cy.wrap(allowance.amount).should('deep.equal', MaxUint160)
// Asserts that the on-chain expiration is in 30 days, within a tolerance of 40 seconds.
const THIRTY_DAYS_SECONDS = 2_592_000
const expected = Math.floor(Date.now() / 1000 + THIRTY_DAYS_SECONDS)
cy.wrap(allowance.expiration).should('be.closeTo', expected, 40)
})
}
setupHardhat(async (hardhat) => {
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(DAI, 1e18))
await hardhat.mine()
})
describe('approval process (with intermediate screens)', () => {
// Turn off automine so that intermediate screens are available to assert on.
before(() => cy.hardhat({ automine: false }))
after(() => cy.hardhat({ automine: true }))
it('swaps after completing full permit2 approval process', () => {
setupInputs(DAI, USDT)
initiateSwap('Approve and swap')
// verify that the modal retains its state when the window loses focus
cy.window().trigger('blur')
// Verify token approval
cy.contains('Approval pending...')
cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine())
expectTokenAllowanceForPermit2ToBeMax(DAI)
// Verify permit2 approval
cy.wait('@eth_signTypedData_v4')
cy.wait('@eth_sendRawTransaction')
cy.contains('Swap pending...')
cy.hardhat().then((hardhat) => hardhat.mine())
cy.contains('Swap success!')
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
})
it('swaps with existing permit approval and missing token approval', () => {
setupInputs(DAI, USDT)
cy.hardhat().then({ timeout: HARDHAT_TIMEOUT }, async (hardhat) => {
await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: DAI })
await hardhat.mine()
})
initiateSwap('Approve and swap')
// Verify token approval
cy.contains('Approval pending...')
cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine())
expectTokenAllowanceForPermit2ToBeMax(DAI)
// Verify transaction
cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine())
cy.contains('Swap success!')
})
/**
* On mainnet, you have to revoke USDT approval before increasing it.
* From the token contract:
* To change the approve amount you first have to reduce the addresses`
* allowance to zero by calling `approve(_spender, 0)` if it is not
* already 0 to mitigate the race condition described here:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*/
it('swaps USDT with existing but insufficient approval permit', () => {
cy.hardhat().then({ timeout: HARDHAT_TIMEOUT }, async (hardhat) => {
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDT, 2e6))
await hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: USDT }, 1e6)
await hardhat.mine()
await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: USDT })
await hardhat.mine()
})
setupInputs(USDT, DAI)
cy.get('#swap-currency-input .token-amount-input').clear().type('2')
initiateSwap('Approve and swap')
// Verify allowance revocation
cy.contains('Resetting USDT limit...')
cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine())
cy.hardhat()
.then(({ approval, wallet }) => approval.getTokenAllowanceForPermit2({ owner: wallet, token: USDT }))
.should('deep.equal', BigNumber.from(0))
// Verify token approval
cy.contains('Approval pending...')
cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine())
expectTokenAllowanceForPermit2ToBeMax(USDT)
// Verify transaction
cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine())
cy.contains('Swap success!')
})
})
it('swaps when user has already approved token and permit2', () => {
cy.hardhat().then({ timeout: HARDHAT_TIMEOUT }, async (hardhat) => {
await hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: DAI })
await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: DAI })
})
setupInputs(DAI, USDT)
initiateSwap()
// Verify transaction
cy.contains('Swap success!')
})
it('swaps after handling user rejection of both approval and signature', () => {
setupInputs(DAI, USDT)
const USER_REJECTION = { code: 4001 }
cy.hardhat().then((hardhat) => {
// Reject token approval
const tokenApprovalStub = cy.stub(hardhat.wallet, 'sendTransaction').log(true)
tokenApprovalStub.rejects(USER_REJECTION) // rejects token approval
initiateSwap('Approve and swap')
// Verify token approval rejection
cy.wrap(tokenApprovalStub).should('be.calledOnce')
cy.contains('Approve and swap')
// Allow token approval
cy.then(() => tokenApprovalStub.restore())
// Reject permit2 approval
const permitApprovalStub = cy.stub(hardhat.provider, 'send').log(false)
permitApprovalStub.withArgs('eth_signTypedData_v4').rejects(USER_REJECTION) // rejects permit approval
permitApprovalStub.callThrough() // allows non-eth_signTypedData_v4 send calls to return non-stubbed values
cy.contains('Approve and swap').click()
// Verify token approval
cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine())
expectTokenAllowanceForPermit2ToBeMax(DAI)
// Verify permit2 approval rejection
cy.wrap(permitApprovalStub).should('be.calledWith', 'eth_signTypedData_v4')
cy.contains('Sign and swap')
// Allow permit2 approval
cy.then(() => permitApprovalStub.restore())
cy.contains('Sign and swap').click()
// Verify permit2 approval
cy.contains('Swap success!')
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
})
})
it('prompts token approval when existing approval amount is too low', () => {
setupInputs(DAI, USDT)
cy.hardhat().then({ timeout: HARDHAT_TIMEOUT }, async (hardhat) => {
await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: DAI })
await hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: DAI }, 1)
})
initiateSwap('Approve and swap')
// Verify token approval
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
})
it('prompts signature when existing permit approval is expired', () => {
const expiredAllowance = { expiration: Math.floor((Date.now() - 1) / 1000) }
cy.hardhat()
.then({ timeout: HARDHAT_TIMEOUT }, async (hardhat) => {
await hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: DAI })
await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: DAI }, expiredAllowance)
})
.then(() => {
setupInputs(DAI, USDT)
initiateSwap('Sign and swap')
})
// Verify permit2 approval
cy.wait('@eth_signTypedData_v4')
cy.contains('Swap success!')
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
})
it('prompts signature when existing permit approval amount is too low', () => {
const smallAllowance = { amount: 1 }
cy.hardhat()
.then({ timeout: HARDHAT_TIMEOUT }, async (hardhat) => {
await hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: DAI })
await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: DAI }, smallAllowance)
})
.then(() => {
setupInputs(DAI, USDT)
initiateSwap('Sign and swap')
})
// Verify permit2 approval
cy.wait('@eth_signTypedData_v4')
cy.contains('Swap success!')
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
})
})
describe('Pool', () => {
beforeEach(() => {
cy.visit('/pool').then(() => {
cy.wait('@eth_blockNumber')
})
})
it('add liquidity links to /add/ETH', () => {
cy.get('body').then(() => {
cy.get('#join-pool-button')
.click()
.then(() => {
cy.url().should('contain', '/add/ETH')
})
})
})
})
describe('Redirect', () => {
it('should redirect to /vote/create-proposal when visiting /create-proposal', () => {
cy.visit('/create-proposal')
cy.url().should('match', /\/vote.uniswapfoundation.org/)
})
it('should redirect to /not-found when visiting nonexist url', () => {
cy.visit('/none-exist-url')
cy.url().should('match', /\/not-found/)
})
})
describe('RedirectExplore', () => {
it('should redirect from /tokens/ to /explore', () => {
cy.visit('/tokens')
cy.url().should('match', /\/explore/)
cy.visit('/tokens/ethereum')
cy.url().should('match', /\/explore\/tokens\/ethereum/)
cy.visit('/tokens/optimism/NATIVE')
cy.url().should('match', /\/explore\/tokens\/optimism\/NATIVE/)
})
})
describe('Legacy Pool Redirects', () => {
it('should redirect /pool to /positions', () => {
cy.visit('/pool')
cy.url().should('match', /\/positions/)
})
it('should redirect /pool/:tokenId with chain param to /positions/v3/:chainName/:tokenId', () => {
cy.visit('/pool/123?chain=mainnet')
cy.url().should('match', /\/positions\/v3\/ethereum\/123/)
})
it('should redirect add v2 liquidity to positions create page', () => {
cy.visit('/add/v2/0x318400242bFdE3B20F49237a9490b8eBB6bdB761/ETH')
cy.url().should('match', /\/positions\/create\/v2\?currencyA=0x318400242bFdE3B20F49237a9490b8eBB6bdB761&currencyB=ETH/)
})
it('should redirect add v3 liquidity to positions create page', () => {
cy.visit('/add/0x318400242bFdE3B20F49237a9490b8eBB6bdB761/ETH')
cy.url().should('match', /\/positions\/create\/v3\?currencyA=0x318400242bFdE3B20F49237a9490b8eBB6bdB761&currencyB=ETH/)
})
it('should redirect remove v2 liquidity to positions page', () => {
cy.visit('/remove/v2/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2')
cy.url().should('match', /\/positions\/v2\/ethereum\/0xBb2b8038a1640196FbE3e38816F3e67Cba72D940/)
})
it('should redirect remove v3 liquidity to positions page', () => {
cy.visit('/remove/825708')
cy.url().should('match', /\/positions\/v3\/ethereum\/825708/)
})
})
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { UNI } from 'uniswap/src/constants/tokens'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { getTestSelector } from '../utils'
const UNI_ADDRESS = UNI[UniverseChainId.Mainnet].address.toLowerCase()
describe('Universal search bar', () => {
function openSearch() {
// can't just type "/" because on mobile it doesn't respond to that
return cy.get('[data-cy="nav-search-container"] [data-cy="nav-search-icon"]').first().click()
}
beforeEach(() => {
cy.visit('/')
})
function getSearchBar() {
return cy.get('[data-cy="nav-search-container"] input').click()
}
it('should yield clickable result that is then added to recent searches', () => {
// Search for UNI token by name.
openSearch()
getSearchBar().clear().type('uni')
cy.get(getTestSelector(`searchbar-token-row-ETHEREUM-${UNI_ADDRESS}`))
.should('contain.text', 'Uniswap')
.and('contain.text', 'UNI')
.and('contain.text', '$')
.and('contain.text', '%')
.click()
cy.location('pathname').should('equal', '/explore/tokens/ethereum/0x1f9840a85d5af5bf1d1762f925bdaddc4201f984')
openSearch()
cy.get(getTestSelector('searchbar-dropdown'))
.contains(getTestSelector('searchbar-dropdown'), 'Recent searches')
.find(getTestSelector(`searchbar-token-row-ETHEREUM-${UNI_ADDRESS}`))
.should('exist')
})
it('should go to the selected result when recent results are shown', () => {
// Seed recent results with UNI.
openSearch()
getSearchBar().type('uni')
cy.get(getTestSelector(`searchbar-token-row-ETHEREUM-${UNI_ADDRESS}`))
getSearchBar().clear().type('{esc}')
// Search a different token by name.
openSearch()
getSearchBar().type('eth')
cy.get(getTestSelector(`searchbar-token-row-ETHEREUM-${NATIVE_CHAIN_ID}`))
// Validate that we go to the searched/selected result.
cy.get(getTestSelector(`searchbar-token-row-ETHEREUM-${NATIVE_CHAIN_ID}`)).click()
cy.url().should('contain', `/explore/tokens/ethereum/${NATIVE_CHAIN_ID}`)
})
})
import { getTestSelector, resetHardhatChain } from '../../utils'
// TODO(WEB-4923): Re-enable network switching tests when theres a non-UI way to switch networks
// function switchChain(chain: string) {
// cy.get(getTestSelector('chain-selector')).click()
// cy.contains(chain).click()
// }
describe('network switching', () => {
beforeEach(() => {
cy.visit('/swap')
cy.get(getTestSelector('web3-status-connected'))
})
afterEach(resetHardhatChain)
// function rejectsNetworkSwitchWith(rejection: unknown) {
// cy.hardhat().then((hardhat) => {
// // Reject network switch
// const sendStub = cy.stub(hardhat.provider, 'send').log(false).as('switch')
// sendStub.withArgs('wallet_switchEthereumChain').rejects(rejection)
// sendStub.callThrough() // allows other calls to return non-stubbed values
// })
// switchChain('Polygon')
// // Verify rejected network switch
// cy.get('@switch').should('have.been.calledWith', 'wallet_switchEthereumChain')
// cy.get(getTestSelector('web3-status-connected'))
// }
// it('should not display message on user rejection', () => {
// const USER_REJECTION = { code: 4001 }
// rejectsNetworkSwitchWith(USER_REJECTION)
// cy.get(getTestSelector('popups')).should('not.contain', 'Failed to switch networks')
// })
// it('should display message on unknown error', () => {
// rejectsNetworkSwitchWith(new Error('Unknown error'))
// cy.get(getTestSelector('popups')).contains('Failed to switch networks')
// })
// it('should add missing chain', () => {
// cy.hardhat().then((hardhat) => {
// // https://docs.metamask.io/guide/rpc-api.html#unrestricted-methods
// const CHAIN_NOT_ADDED = { code: 4902 } // missing message in useSelectChain
// // Reject network switch with CHAIN_NOT_ADDED
// const sendStub = cy.stub(hardhat.provider, 'send').log(false).as('switch')
// let added = false
// sendStub
// .withArgs('wallet_switchEthereumChain')
// .callsFake(() => (added ? Promise.resolve(null) : Promise.reject(CHAIN_NOT_ADDED)))
// sendStub.withArgs('wallet_addEthereumChain').callsFake(() => {
// added = true
// return Promise.resolve(null)
// })
// sendStub.callThrough() // allows other calls to return non-stubbed values
// })
// switchChain('Polygon')
// // Verify the network was added
// cy.get('@switch').should('have.been.calledWith', 'wallet_switchEthereumChain')
// cy.get('@switch').should('have.been.calledWith', 'wallet_addEthereumChain')
// })
// it('should not disconnect while switching', () => {
// // Defer the connection so we can see the pending state
// cy.hardhat().then((hardhat) => {
// const sendStub = cy.stub(hardhat.provider, 'send').log(false).as('switch')
// sendStub.withArgs('wallet_switchEthereumChain').returns(new Promise(() => {}))
// sendStub.callThrough() // allows other calls to return non-stubbed values
// })
// switchChain('Polygon')
// // Verify there is no disconnection
// cy.get('@switch').should('have.been.calledWith', 'wallet_switchEthereumChain')
// cy.contains('Connecting to Polygon')
// cy.get(getTestSelector('web3-status-connected')).should('be.disabled')
// })
// it('switches networks', () => {
// // Select an output currency
// cy.get('#swap-currency-output .open-currency-select-button').click()
// cy.get(getTestSelector('token-option-1-USDT')).click()
// // Populate input/output fields
// cy.get('#swap-currency-input .token-amount-input').clear().type('1')
// cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
// // Switch network
// switchChain('Polygon')
// // Verify network switch
// cy.wait('@wallet_switchEthereumChain')
// cy.get(getTestSelector('web3-status-connected'))
// cy.url().should('contain', 'chain=polygon')
// // Verify that the input/output fields were reset
// cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
// cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'MATIC')
// cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value')
// cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'Select token')
// })
// describe('from URL param', () => {
// it('should switch network from URL param', () => {
// cy.visit('/swap?chain=optimism')
// cy.wait('@wallet_switchEthereumChain')
// cy.get(getTestSelector('web3-status-connected'))
// })
// it('should switch network with inputCurrency from URL param', () => {
// cy.visit('/swap?chain=optimism&outputCurrency=0x0b2c639c533813f4aa9d7837caf62653d097ff85')
// cy.wait('@wallet_switchEthereumChain')
// cy.get(getTestSelector('web3-status-connected'))
// cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'USDC')
// })
// it('should not switch network with no chain in param', () => {
// cy.hardhat().then((hardhat) => {
// cy.visit('/swap?outputCurrency=0x2260fac5e5542a773aa44fbcfedf7c193bc2c599')
// const sendSpy = cy.spy(hardhat.provider, 'send')
// cy.wrap(sendSpy).should('not.be.calledWith', 'wallet_switchEthereumChain')
// cy.wrap(hardhat.provider.network.chainId).should('eq', 1)
// cy.get(getTestSelector('web3-status-connected'))
// cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'WBTC')
// })
// })
// it('should be able to switch network after loading from URL param', () => {
// cy.visit('/swap?chain=optimism&outputCurrency=0x0b2c639c533813f4aa9d7837caf62653d097ff85')
// cy.wait('@wallet_switchEthereumChain')
// cy.get(getTestSelector('web3-status-connected'))
// cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'USDC')
// // switching to another chain clears query param
// switchChain('Ethereum')
// cy.wait('@wallet_switchEthereumChain')
// cy.url().should('not.contain', 'chain=optimism')
// cy.url().should('not.contain', 'outputCurrency=0xff970a61a04b1ca14834a43f5de4533ebddb5cc8')
// })
// })
describe('multichain', () => {
it('does not switchEthereumChain when multichain is enabled', () => {
cy.hardhat().then((hardhat) => {
cy.visit('/swap')
cy.get('#swap-currency-input .open-currency-select-button').click()
cy.get(getTestSelector('chain-selector')).last().click()
cy.contains('Arbitrum').click()
const sendSpy = cy.spy(hardhat.provider, 'send')
cy.wrap(sendSpy).should('not.be.calledWith', 'wallet_switchEthereumChain')
cy.wrap(hardhat.provider.network.chainId).should('eq', 1)
})
})
})
})
import { getTestSelector } from '../utils'
describe('Wallet Dropdown', () => {
function itChangesTheme() {
it('should change theme', () => {
cy.get(getTestSelector('theme-light')).click()
cy.get('html').should('have.class', 't_light')
cy.get(getTestSelector('theme-dark')).click()
cy.get('html').should('have.class', 't_dark')
cy.get(getTestSelector('theme-auto')).click()
cy.get('html').should('not.have.class', 't_light')
})
}
function itChangesLocale() {
it('should change locale', () => {
cy.contains('Uniswap available in: English').should('not.exist')
cy.get(getTestSelector('language-settings-button')).click()
cy.get(getTestSelector('wallet-language-item')).contains('Afrikaans').click({ force: true })
cy.location('search').should('include', 'lng=af-ZA')
cy.get(getTestSelector('wallet-language-item')).contains('English').click({ force: true })
cy.location('search').should('include', 'lng=en-US')
})
}
describe('connected', () => {
beforeEach(() => {
cy.visit('/')
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
})
itChangesTheme()
itChangesLocale()
it('should not show buy crypto button in uk', () => {
cy.document().then((doc) => {
const meta = document.createElement('meta')
meta.setAttribute('property', 'x:blocked-paths')
meta.setAttribute('content', '/,/nfts,/buy')
doc.head.appendChild(meta)
})
cy.get(getTestSelector('wallet-buy-crypto')).should('not.exist')
})
})
describe('do not render buy button when /buy is blocked', () => {
beforeEach(() => {
cy.document().then((doc) => {
const meta = document.createElement('meta')
meta.setAttribute('property', 'x:blocked-paths')
meta.setAttribute('content', '/buy')
doc.head.appendChild(meta)
})
cy.visit('/')
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
})
it('should not render buy button', () => {
cy.get(getTestSelector('wallet-buy-crypto')).should('not.exist')
})
})
describe('should change locale with feature flag', () => {
beforeEach(() => {
cy.visit('/')
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
})
itChangesLocale()
})
describe('testnet toggle', () => {
beforeEach(() => {
cy.visit('/swap')
it('should toggle testnet visibility', () => {
cy.get('#swap-currency-input .open-currency-select-button').click()
cy.get(getTestSelector('chain-selector')).last().click()
cy.get(getTestSelector('network-button-11155111')).should('not.exist')
cy.get(getTestSelector('web3-status-connected')).click({ force: true })
cy.get(getTestSelector('wallet-settings')).click()
cy.get(getTestSelector('testnets-toggle')).click()
cy.contains('Close').click()
cy.get(getTestSelector('close-account-drawer')).click()
cy.get(getTestSelector('choose-input-token')).click()
cy.get(getTestSelector('token-option-11155111-ETH')).should('exist')
})
})
describe('disconnected', () => {
beforeEach(() => {
cy.visit('/')
cy.get(getTestSelector('web3-status-connected')).click()
// click twice, first time to show confirmation, second to confirm
cy.get(getTestSelector('wallet-disconnect')).click()
cy.get(getTestSelector('wallet-disconnect')).should('contain', 'Disconnect')
cy.get(getTestSelector('wallet-disconnect')).click()
})
it('wallet settings should not be accessible', () => {
cy.get(getTestSelector('wallet-settings')).should('not.exist')
})
})
// TODO(WEB-3905): Fix tamagui error causing these tests to fail.
describe.skip('with color theme', () => {
function visitSwapWithColorTheme({ dark }: { dark: boolean }) {
cy.visit('/swap', {
onBeforeLoad(win) {
cy.stub(win, 'matchMedia')
.withArgs('(prefers-color-scheme: dark)')
.returns({
matches: dark,
addEventListener() {
/* noop */
},
removeEventListener() {
/* noop */
},
})
},
})
}
it('should properly use dark system theme when auto theme setting is selected', () => {
visitSwapWithColorTheme({ dark: true })
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
cy.get(getTestSelector('theme-auto')).click()
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(155, 155, 155)')
})
it('should properly use light system theme when auto theme setting is selected', () => {
visitSwapWithColorTheme({ dark: false })
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
cy.get(getTestSelector('theme-auto')).click()
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(125, 125, 125)')
})
})
describe('mobile', () => {
beforeEach(() => {
cy.viewport('iphone-6').visit('/')
})
it('should dismiss the wallet bottom sheet when clicking buy crypto', () => {
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-buy-crypto')).click()
cy.get(getTestSelector('wallet-settings')).should('not.be.visible')
})
it('should use a bottom sheet and dismiss when on a mobile screen size', () => {
cy.get(getTestSelector('web3-status-connected')).click()
cy.root().click(15, 20)
cy.get(getTestSelector('wallet-settings')).should('not.be.visible')
})
})
describe('local currency', () => {
it('loads local currency from the query param', () => {
cy.visit('/')
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
cy.contains('USD')
cy.visit('/?cur=AUD')
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
cy.contains('AUD')
})
it('loads local currency from menu', () => {
cy.visit('/')
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
cy.contains('USD')
cy.get(getTestSelector('local-currency-settings-button')).click()
cy.get(getTestSelector('wallet-local-currency-item')).contains('AUD').click({ force: true })
cy.location('search').should('include', 'cur=AUD')
cy.contains('AUD')
cy.get(getTestSelector('wallet-local-currency-item')).contains('USD').click({ force: true })
cy.location('search').should('include', 'cur=USD')
cy.contains('USD')
})
})
})
})
{
"data": {
"v3PoolsForTokenPair": [
{
"feeTier": 3000.0,
"token0Supply": 3084.78636993,
"token1Supply": 76658.86823272712,
"__typename": "V3Pool"
},
{
"feeTier": 500.0,
"token0Supply": 917.54087138,
"token1Supply": 22788.333363067817,
"__typename": "V3Pool"
},
{
"feeTier": 10000.0,
"token0Supply": 14.67505995,
"token1Supply": 276.9691974507059,
"__typename": "V3Pool"
},
{
"feeTier": 100.0,
"token0Supply": 0.04782301,
"token1Supply": 1.677630863841559,
"__typename": "V3Pool"
}
]
}
}
{
"countryCode": "US",
"displayName": "United States",
"state": "US-NY"
}
{
"error": null,
"message": null,
"quotes": [
{
"countryCode": "US",
"destinationAmount": 0.03131402821107613,
"destinationCurrencyCode": "ETH",
"isMostRecentlyUsedProvider": false,
"serviceProvider": "STRIPE",
"serviceProviderDetails": {
"logos": {
"darkLogo": "https://images-serviceprovider.meld.io/STRIPE/short_logo_light.png",
"lightLogo": "https://images-serviceprovider.meld.io/STRIPE/short_logo_light.png"
},
"name": "Stripe",
"paymentMethods": [
"Debit Card",
"ACH"
],
"serviceProvider": "STRIPE",
"supportUrl": "https://support.stripe.com/",
"url": "http://www.stripe.com"
},
"sourceAmount": 100,
"sourceCurrencyCode": "USD",
"totalFee": 2.19
},
{
"countryCode": "US",
"destinationAmount": 0.03034965,
"destinationCurrencyCode": "ETH",
"isMostRecentlyUsedProvider": true,
"serviceProvider": "COINBASEPAY",
"serviceProviderDetails": {
"logos": {
"darkLogo": "https://images-serviceprovider.meld.io/COINBASEPAY/short_logo_light.png",
"lightLogo": "https://images-serviceprovider.meld.io/COINBASEPAY/short_logo_light.png"
},
"name": "Coinbase",
"paymentMethods": [
"Debit Card",
"ACH"
],
"serviceProvider": "COINBASEPAY",
"supportUrl": "https://help.coinbase.com/",
"url": "https://www.coinbase.com/"
},
"sourceAmount": 100,
"sourceCurrencyCode": "USD",
"totalFee": 2.83
},
{
"countryCode": "US",
"destinationAmount": 0.0303,
"destinationCurrencyCode": "ETH",
"isMostRecentlyUsedProvider": false,
"serviceProvider": "MOONPAY",
"serviceProviderDetails": {
"logos": {
"darkLogo": "https://images-serviceprovider.meld.io/MOONPAY/short_logo_light.png",
"lightLogo": "https://images-serviceprovider.meld.io/MOONPAY/short_logo_light.png"
},
"name": "MoonPay",
"serviceProvider": "MOONPAY",
"supportUrl": "https://www.moonpay.com/contact-us",
"url": "https://buy.moonpay.com",
"paymentMethods": [
"Debit Card",
"Apple Pay",
"Google Pay",
"PayPal"
]
},
"sourceAmount": 100,
"sourceCurrencyCode": "USD",
"totalFee": 5.040000000000006
}
]
}
{
"fiatCurrencies": [
{
"displayName": "Argentine Peso",
"fiatCurrencyCode": "ARS",
"symbol": "https://images-currency.meld.io/fiat/ARS/symbol.png"
},
{
"displayName": "Australian Dollar",
"fiatCurrencyCode": "AUD",
"symbol": "https://images-currency.meld.io/fiat/AUD/symbol.png"
},
{
"displayName": "Azerbaijan Manat",
"fiatCurrencyCode": "AZN",
"symbol": "https://images-currency.meld.io/fiat/AZN/symbol.png"
},
{
"displayName": "Balboa",
"fiatCurrencyCode": "PAB",
"symbol": "https://images-currency.meld.io/fiat/PAB/symbol.png"
},
{
"displayName": "Boliviano",
"fiatCurrencyCode": "BOB",
"symbol": "https://images-currency.meld.io/fiat/BOB/symbol.png"
},
{
"displayName": "Brazilian Real",
"fiatCurrencyCode": "BRL",
"symbol": "https://images-currency.meld.io/fiat/BRL/symbol.png"
},
{
"displayName": "Bulgarian Lev",
"fiatCurrencyCode": "BGN",
"symbol": "https://images-currency.meld.io/fiat/BGN/symbol.png"
},
{
"displayName": "CFA Franc BCEAO",
"fiatCurrencyCode": "XOF",
"symbol": "https://images-currency.meld.io/fiat/XOF/symbol.png"
},
{
"displayName": "CFA Franc BEAC",
"fiatCurrencyCode": "XAF",
"symbol": "https://images-currency.meld.io/fiat/XAF/symbol.png"
},
{
"displayName": "Canadian Dollar",
"fiatCurrencyCode": "CAD",
"symbol": "https://images-currency.meld.io/fiat/CAD/symbol.png"
},
{
"displayName": "Chilean Peso",
"fiatCurrencyCode": "CLP",
"symbol": "https://images-currency.meld.io/fiat/CLP/symbol.png"
},
{
"displayName": "Colombian Peso",
"fiatCurrencyCode": "COP",
"symbol": "https://images-currency.meld.io/fiat/COP/symbol.png"
},
{
"displayName": "Costa Rican Colon",
"fiatCurrencyCode": "CRC",
"symbol": "https://images-currency.meld.io/fiat/CRC/symbol.png"
},
{
"displayName": "Croatian kuna",
"fiatCurrencyCode": "HRK",
"symbol": "https://images-currency.meld.io/fiat/HRK/symbol.png"
},
{
"displayName": "Czech Koruna",
"fiatCurrencyCode": "CZK",
"symbol": "https://images-currency.meld.io/fiat/CZK/symbol.png"
},
{
"displayName": "Danish Krone",
"fiatCurrencyCode": "DKK",
"symbol": "https://images-currency.meld.io/fiat/DKK/symbol.png"
},
{
"displayName": "Dominican Peso",
"fiatCurrencyCode": "DOP",
"symbol": "https://images-currency.meld.io/fiat/DOP/symbol.png"
},
{
"displayName": "Euro",
"fiatCurrencyCode": "EUR",
"symbol": "https://images-currency.meld.io/fiat/EUR/symbol.png"
},
{
"displayName": "Forint",
"fiatCurrencyCode": "HUF",
"symbol": "https://images-currency.meld.io/fiat/HUF/symbol.png"
},
{
"displayName": "Ghana Cedi",
"fiatCurrencyCode": "GHS",
"symbol": "https://images-currency.meld.io/fiat/GHS/symbol.png"
},
{
"displayName": "Hong Kong Dollar",
"fiatCurrencyCode": "HKD",
"symbol": "https://images-currency.meld.io/fiat/HKD/symbol.png"
},
{
"displayName": "Hryvnia",
"fiatCurrencyCode": "UAH",
"symbol": "https://images-currency.meld.io/fiat/UAH/symbol.png"
},
{
"displayName": "Iceland Krona",
"fiatCurrencyCode": "ISK",
"symbol": "https://images-currency.meld.io/fiat/ISK/symbol.png"
},
{
"displayName": "Indian Rupee",
"fiatCurrencyCode": "INR",
"symbol": "https://images-currency.meld.io/fiat/INR/symbol.png"
},
{
"displayName": "Jamaican Dollar",
"fiatCurrencyCode": "JMD",
"symbol": "https://images-currency.meld.io/fiat/JMD/symbol.png"
},
{
"displayName": "Jordanian Dinar",
"fiatCurrencyCode": "JOD",
"symbol": "https://images-currency.meld.io/fiat/JOD/symbol.png"
},
{
"displayName": "Kenyan Shilling",
"fiatCurrencyCode": "KES",
"symbol": "https://images-currency.meld.io/fiat/KES/symbol.png"
},
{
"displayName": "Kuwaiti Dinar",
"fiatCurrencyCode": "KWD",
"symbol": "https://images-currency.meld.io/fiat/KWD/symbol.png"
},
{
"displayName": "Malagasy Ariary",
"fiatCurrencyCode": "MGA",
"symbol": "https://images-currency.meld.io/fiat/MGA/symbol.png"
},
{
"displayName": "Mexican Peso",
"fiatCurrencyCode": "MXN",
"symbol": "https://images-currency.meld.io/fiat/MXN/symbol.png"
},
{
"displayName": "Naira",
"fiatCurrencyCode": "NGN",
"symbol": "https://images-currency.meld.io/fiat/NGN/symbol.png"
},
{
"displayName": "Nepalese Rupee",
"fiatCurrencyCode": "NPR",
"symbol": "https://images-currency.meld.io/fiat/NPR/symbol.png"
},
{
"displayName": "New Taiwan Dollar",
"fiatCurrencyCode": "TWD",
"symbol": "https://images-currency.meld.io/fiat/TWD/symbol.png"
},
{
"displayName": "New Zealand Dollar",
"fiatCurrencyCode": "NZD",
"symbol": "https://images-currency.meld.io/fiat/NZD/symbol.png"
},
{
"displayName": "Norwegian Krone",
"fiatCurrencyCode": "NOK",
"symbol": "https://images-currency.meld.io/fiat/NOK/symbol.png"
},
{
"displayName": "Pakistan Rupee",
"fiatCurrencyCode": "PKR",
"symbol": "https://images-currency.meld.io/fiat/PKR/symbol.png"
},
{
"displayName": "Peso Uruguayo",
"fiatCurrencyCode": "UYU",
"symbol": "https://images-currency.meld.io/fiat/UYU/symbol.png"
},
{
"displayName": "Philippine Peso",
"fiatCurrencyCode": "PHP",
"symbol": "https://images-currency.meld.io/fiat/PHP/symbol.png"
},
{
"displayName": "Pound Sterling",
"fiatCurrencyCode": "GBP",
"symbol": "https://images-currency.meld.io/fiat/GBP/symbol.png"
},
{
"displayName": "Quetzal",
"fiatCurrencyCode": "GTQ",
"symbol": "https://images-currency.meld.io/fiat/GTQ/symbol.png"
},
{
"displayName": "Rand",
"fiatCurrencyCode": "ZAR",
"symbol": "https://images-currency.meld.io/fiat/ZAR/symbol.png"
},
{
"displayName": "Riel",
"fiatCurrencyCode": "KHR",
"symbol": "https://images-currency.meld.io/fiat/KHR/symbol.png"
},
{
"displayName": "Romanian Leu",
"fiatCurrencyCode": "RON",
"symbol": "https://images-currency.meld.io/fiat/RON/symbol.png"
},
{
"displayName": "Rupiah",
"fiatCurrencyCode": "IDR",
"symbol": "https://images-currency.meld.io/fiat/IDR/symbol.png"
},
{
"displayName": "Serbian Dinar",
"fiatCurrencyCode": "RSD",
"symbol": "https://images-currency.meld.io/fiat/RSD/symbol.png"
},
{
"displayName": "Singapore Dollar",
"fiatCurrencyCode": "SGD",
"symbol": "https://images-currency.meld.io/fiat/SGD/symbol.png"
},
{
"displayName": "Sol",
"fiatCurrencyCode": "PEN",
"symbol": "https://images-currency.meld.io/fiat/PEN/symbol.png"
},
{
"displayName": "Sri Lanka Rupee",
"fiatCurrencyCode": "LKR",
"symbol": "https://images-currency.meld.io/fiat/LKR/symbol.png"
},
{
"displayName": "Swedish Krona",
"fiatCurrencyCode": "SEK",
"symbol": "https://images-currency.meld.io/fiat/SEK/symbol.png"
},
{
"displayName": "Swiss Franc",
"fiatCurrencyCode": "CHF",
"symbol": "https://images-currency.meld.io/fiat/CHF/symbol.png"
},
{
"displayName": "Tenge",
"fiatCurrencyCode": "KZT",
"symbol": "https://images-currency.meld.io/fiat/KZT/symbol.png"
},
{
"displayName": "Tugrik",
"fiatCurrencyCode": "MNT",
"symbol": "https://images-currency.meld.io/fiat/MNT/symbol.png"
},
{
"displayName": "Turkish Lira",
"fiatCurrencyCode": "TRY",
"symbol": "https://images-currency.meld.io/fiat/TRY/symbol.png"
},
{
"displayName": "UAE Dirham",
"fiatCurrencyCode": "AED",
"symbol": "https://images-currency.meld.io/fiat/AED/symbol.png"
},
{
"displayName": "US Dollar",
"fiatCurrencyCode": "USD",
"symbol": "https://images-currency.meld.io/fiat/USD/symbol.png"
},
{
"displayName": "Uganda Shilling",
"fiatCurrencyCode": "UGX",
"symbol": "https://images-currency.meld.io/fiat/UGX/symbol.png"
},
{
"displayName": "Uzbekistan Sum",
"fiatCurrencyCode": "UZS",
"symbol": "https://images-currency.meld.io/fiat/UZS/symbol.png"
},
{
"displayName": "Yemeni Rial",
"fiatCurrencyCode": "YER",
"symbol": "https://images-currency.meld.io/fiat/YER/symbol.png"
},
{
"displayName": "Zambian Kwacha",
"fiatCurrencyCode": "ZMW",
"symbol": "https://images-currency.meld.io/fiat/ZMW/symbol.png"
},
{
"displayName": "Zloty",
"fiatCurrencyCode": "PLN",
"symbol": "https://images-currency.meld.io/fiat/PLN/symbol.png"
},
{
"fiatCurrencyCode": "EGP",
"displayName": "Egyptian Pound",
"symbol": "https://images-currency.meld.io/fiat/EGP/symbol.png"
},
{
"fiatCurrencyCode": "ILS",
"displayName": "Israeli New Shekel",
"symbol": "https://images-currency.meld.io/fiat/ILS/symbol.png"
},
{
"fiatCurrencyCode": "OMR",
"displayName": "Omani Rial",
"symbol": "https://images-currency.meld.io/fiat/OMR/symbol.png"
},
{
"fiatCurrencyCode": "VND",
"displayName": "Vietnamese Dong",
"symbol": "https://images-currency.meld.io/fiat/VND/symbol.png"
}
]
}
{
"supportedTokens": [
{
"cryptoCurrencyCode": "ETH",
"displayName": "Ethereum",
"address": null,
"cryptoCurrencyChain": "Ethereum",
"chainId": "1",
"symbol": "https://images-currency.meld.io/crypto/ETH/symbol.png"
},
{
"cryptoCurrencyCode": "USDC",
"displayName": "USD Coin",
"address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"cryptoCurrencyChain": "Ethereum",
"chainId": "1",
"symbol": "https://images-currency.meld.io/crypto/USDC/symbol.png"
},
{
"cryptoCurrencyCode": "USDT",
"displayName": "Tether",
"address": "0xdac17f958d2ee523a2206206994597c13d831ec7",
"cryptoCurrencyChain": "Ethereum",
"chainId": "1",
"symbol": "https://images-currency.meld.io/crypto/USDT/symbol.png"
},
{
"cryptoCurrencyCode": "DAI",
"displayName": "Dai",
"address": "0x6b175474e89094c44da98b954eedeac495271d0f",
"cryptoCurrencyChain": "Ethereum",
"chainId": "1",
"symbol": "https://images-currency.meld.io/crypto/DAI/symbol.png"
},
{
"cryptoCurrencyCode": "WBTC",
"displayName": "Wrapped Bitcoin",
"address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599",
"cryptoCurrencyChain": "Ethereum",
"chainId": "1",
"symbol": "https://images-currency.meld.io/crypto/WBTC/symbol.png"
},
{
"cryptoCurrencyCode": "WETH",
"displayName": "Wrapped Ether (ERC-20)",
"address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"cryptoCurrencyChain": "ethereum",
"chainId": "1",
"symbol": ""
},
{
"cryptoCurrencyCode": "ETH_BASE",
"displayName": "Ethereum",
"address": null,
"cryptoCurrencyChain": "Base",
"chainId": "8453",
"symbol": "https://images-currency.meld.io/crypto/ETH_BASE/symbol.png"
},
{
"cryptoCurrencyCode": "USDC_BASE",
"displayName": "USD Coin",
"address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"cryptoCurrencyChain": "Base",
"chainId": "8453",
"symbol": "https://images-currency.meld.io/crypto/USDC_BASE/symbol.png"
},
{
"cryptoCurrencyCode": "ETH_OPTIMISM",
"displayName": "Ethereum",
"address": null,
"cryptoCurrencyChain": "Optimism",
"chainId": "10",
"symbol": "https://images-currency.meld.io/crypto/ETH_OPTIMISM/symbol.png"
},
{
"cryptoCurrencyCode": "USDC_OPTIMISM",
"displayName": "USD Coin",
"address": "0x7f5c764cbc14f9669b88837ca1490cca17c31607",
"cryptoCurrencyChain": "Optimism",
"chainId": "10",
"symbol": "https://images-currency.meld.io/crypto/USDC_OPTIMISM/symbol.png"
},
{
"cryptoCurrencyCode": "ETH_ARBITRUM",
"displayName": "Ethereum",
"address": null,
"cryptoCurrencyChain": "Arbitrum",
"chainId": "42161",
"symbol": "https://images-currency.meld.io/crypto/ETH_ARBITRUM/symbol.png"
},
{
"cryptoCurrencyCode": "USDC_ARBITRUM",
"displayName": "USD Coin",
"address": "0xaf88d065e77c8cc2239327c5edb3a432268e5831",
"cryptoCurrencyChain": "Arbitrum",
"chainId": "42161",
"symbol": "https://images-currency.meld.io/crypto/USDC_ARBITRUM/symbol.png"
},
{
"cryptoCurrencyCode": "MATIC",
"displayName": "Matic",
"address": null,
"cryptoCurrencyChain": "Polygon",
"chainId": "137",
"symbol": "https://images-currency.meld.io/crypto/MATIC/symbol.png"
}
]
}
{
"errorCode": "QUOTE_ERROR",
"detail": "No quotes available",
"id": "63363cc1-d474-4584-b386-7c356814b79f"
}
\ No newline at end of file
{
"alpha2": "US",
"alpha3": "USA",
"country": "United States of America",
"ipAddress": "142.154.213.230",
"isAllowed": true,
"isBuyAllowed": false,
"isNftAllowed": true,
"isSellAllowed": false,
"isBalanceLedgerWithdrawAllowed": true,
"isLowLimitEnabled": false,
"state": "NY"
}
{
"alpha2": "US",
"alpha3": "USA",
"country": "United States of America",
"ipAddress": "142.154.213.230",
"isAllowed": true,
"isBuyAllowed": true,
"isNftAllowed": true,
"isSellAllowed": true,
"isBalanceLedgerWithdrawAllowed": true,
"isLowLimitEnabled": true,
"state": "NY"
}
This diff is collapsed.
This diff is collapsed.
{"data":{"searchTokens":[{"id":"VG9rZW46WktTWU5DX251bGw=","chain":"ZKSYNC","address":null,"decimals":18,"symbol":"ETH","name":"Ethereum","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGxfRXRoZXJldW0=","logoUrl":"https://token-icons.s3.amazonaws.com/eth.png","safetyLevel":"VERIFIED","__typename":"TokenProject"},"protectionInfo":null,"__typename":"Token"},{"id":"VG9rZW46V09STERDSEFJTl9udWxs","chain":"WORLDCHAIN","address":null,"decimals":18,"symbol":"ETH","name":"Ethereum","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGxfRXRoZXJldW0=","logoUrl":"https://token-icons.s3.amazonaws.com/eth.png","safetyLevel":"VERIFIED","__typename":"TokenProject"},"protectionInfo":null,"__typename":"Token"},{"id":"VG9rZW46QkxBU1RfbnVsbA==","chain":"BLAST","address":null,"decimals":18,"symbol":"ETH","name":"Ethereum","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGxfRXRoZXJldW0=","logoUrl":"https://token-icons.s3.amazonaws.com/eth.png","safetyLevel":"VERIFIED","__typename":"TokenProject"},"protectionInfo":null,"__typename":"Token"},{"id":"VG9rZW46Wk9SQV9udWxs","chain":"ZORA","address":null,"decimals":18,"symbol":"ETH","name":"Ethereum","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGxfRXRoZXJldW0=","logoUrl":"https://token-icons.s3.amazonaws.com/eth.png","safetyLevel":"VERIFIED","__typename":"TokenProject"},"protectionInfo":null,"__typename":"Token"},{"id":"VG9rZW46RVRIRVJFVU1fbnVsbA==","chain":"ETHEREUM","address":null,"decimals":18,"symbol":"ETH","name":"Ethereum","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGxfRXRoZXJldW0=","logoUrl":"https://token-icons.s3.amazonaws.com/eth.png","safetyLevel":"VERIFIED","__typename":"TokenProject"},"protectionInfo":null,"__typename":"Token"},{"id":"VG9rZW46QVJCSVRSVU1fbnVsbA==","chain":"ARBITRUM","address":null,"decimals":18,"symbol":"ETH","name":"Ethereum","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGxfRXRoZXJldW0=","logoUrl":"https://token-icons.s3.amazonaws.com/eth.png","safetyLevel":"VERIFIED","__typename":"TokenProject"},"protectionInfo":null,"__typename":"Token"},{"id":"VG9rZW46T1BUSU1JU01fbnVsbA==","chain":"OPTIMISM","address":null,"decimals":18,"symbol":"ETH","name":"Ethereum","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGxfRXRoZXJldW0=","logoUrl":"https://token-icons.s3.amazonaws.com/eth.png","safetyLevel":"VERIFIED","__typename":"TokenProject"},"protectionInfo":null,"__typename":"Token"},{"id":"VG9rZW46T1BUSU1JU01fbnVsbA==","chain":"BASE","address":null,"decimals":18,"symbol":"ETH","name":"Ethereum","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGxfRXRoZXJldW0=","logoUrl":"https://token-icons.s3.amazonaws.com/eth.png","safetyLevel":"VERIFIED","__typename":"TokenProject"},"protectionInfo":null,"__typename":"Token"},{"id":"VG9rZW46RVRIRVJFVU1fMHhDMDJhYUEzOWIyMjNGRThEMEEwZTVDNEYyN2VBRDkwODNDNzU2Q2My","chain":"ETHEREUM","address":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","decimals":18,"symbol":"WETH","name":"Wrapped Ether","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4YzAyYWFhMzliMjIzZmU4ZDBhMGU1YzRmMjdlYWQ5MDgzYzc1NmNjMl9XRVRI","logoUrl":"https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png","safetyLevel":"VERIFIED","__typename":"TokenProject"},"protectionInfo":{"result":"BENIGN","attackTypes":[],"__typename":"ProtectionInfo"},"__typename":"Token"},{"id":"VG9rZW46QVJCSVRSVU1fMHg4MmFmNDk0NDdkOGEwN2UzYmQ5NWJkMGQ1NmYzNTI0MTUyM2ZiYWIx","chain":"ARBITRUM","address":"0x82af49447d8a07e3bd95bd0d56f35241523fbab1","decimals":18,"symbol":"WETH","name":"Wrapped Ether","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4YzAyYWFhMzliMjIzZmU4ZDBhMGU1YzRmMjdlYWQ5MDgzYzc1NmNjMl9BcmJpdHJ1bSBCcmlkZ2VkIFdFVEggKEFyYml0cnVtIE9uZSk=","logoUrl":"https://coin-images.coingecko.com/coins/images/39713/large/WETH.PNG?1723731496","safetyLevel":"VERIFIED","__typename":"TokenProject"},"protectionInfo":{"result":"BENIGN","attackTypes":[],"__typename":"ProtectionInfo"},"__typename":"Token"}]}}
\ No newline at end of file
{"data":{"searchTokens":[{"id":"VG9rZW46RVRIRVJFVU1fMHhhMGI4Njk5MWM2MjE4YjM2YzFkMTlkNGEyZTllYjBjZTM2MDZlYjQ4","chain":"ETHEREUM","address":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","decimals":6,"symbol":"USDC","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4YTBiODY5OTFjNjIxOGIzNmMxZDE5ZDRhMmU5ZWIwY2UzNjA2ZWI0OF9VU0RD","name":"USDC","logoUrl":"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694","safetyLevel":"VERIFIED","__typename":"TokenProject"},"__typename":"Token"}]}}
\ No newline at end of file
{
"data": {
"token": {
"id": "VG9rZW46RVRIRVJFVU1fbnVsbA==",
"address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"chain": "ETHEREUM",
"symbol": "USDC",
"name": "USD Coin",
"decimals": 6,
"standard": "ERC20",
"project": {
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGxfRXRoZXJldW0=",
"name": "Circle: USDC",
"logo": {
"id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=",
"url": "https://etherscan.io/token/images/centre-usdc_28.png",
"__typename": "Image"
},
"safetyLevel": "VERIFIED",
"logoUrl": "https://etherscan.io/token/images/centre-usdc_28.png",
"isSpam": false,
"__typename": "TokenProject"
},
"__typename": "Token"
}
}
}
{"data":{"tokenProjects":[{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGxfRXRoZXJldW0=","name":"Ethereum","logoUrl":"https://token-icons.s3.amazonaws.com/eth.png","safetyLevel":"VERIFIED","tokens":[{"chain":"ETHEREUM","address":null,"decimals":18,"symbol":"ETH","__typename":"Token"},{"chain":"ARBITRUM","address":null,"decimals":18,"symbol":"ETH","__typename":"Token"},{"chain":"OPTIMISM","address":null,"decimals":18,"symbol":"ETH","__typename":"Token"},{"chain":"BASE","address":null,"decimals":18,"symbol":"ETH","__typename":"Token"},{"chain":"BLAST","address":null,"decimals":18,"symbol":"ETH","__typename":"Token"},{"chain":"ZORA","address":null,"decimals":18,"symbol":"ETH","__typename":"Token"},{"chain":"ZKSYNC","address":null,"decimals":18,"symbol":"ETH","__typename":"Token"}],"__typename":"TokenProject"}]}}
\ No newline at end of file
This diff is collapsed.
{"data":{"token":{"id":"VG9rZW46RVRIRVJFVU1fMHgxZjk4NDBhODVkNWFmNWJmMWQxNzYyZjkyNWJkYWRkYzQyMDFmOTg0","address":"0x1f9840a85d5af5bf1d1762f925bdaddc4201f984","chain":"ETHEREUM","decimals":18,"name":"Uniswap","standard":"ERC20","symbol":"UNI","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4MWY5ODQwYTg1ZDVhZjViZjFkMTc2MmY5MjViZGFkZGM0MjAxZjk4NF9Vbmlzd2Fw","isSpam":false,"logoUrl":"https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png","name":"Uniswap","safetyLevel":"VERIFIED","__typename":"TokenProject"},"feeData":{"buyFeeBps":null,"sellFeeBps":null,"__typename":"FeeData"},"__typename":"Token"}}}
\ No newline at end of file
import { getTestSelector } from '../utils'
describe('translations', () => {
// TODO re-enable web test
it.skip('loads locale from the query param', () => {
cy.visit('/?lng=fr-FR')
cy.contains('Échanger')
cy.contains('Uniswap disponible en : English')
})
// TODO re-enable web test
it.skip('loads locale from menu', () => {
cy.visit('/')
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
cy.get(getTestSelector('wallet-language-item')).contains('français').click({ force: true })
cy.location('search').should('match', /\?lng=fr-FR$/)
cy.contains('Échanger')
cy.contains('Uniswap disponible en : English')
})
})
import 'cypress-hardhat/lib/browser'
import { Eip1193 } from 'cypress-hardhat/lib/browser/eip1193'
import { FeatureFlagClient, FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/gating/flags'
import { ALLOW_ANALYTICS_ATOM_KEY } from 'utilities/src/telemetry/analytics/constants'
import { UserState, initialState } from '../../src/state/user/reducer'
import { setInitialUserState } from '../utils/user-state'
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface ApplicationWindow {
ethereum: Eip1193
}
interface Chainable<Subject> {
/**
* Wait for a specific event to be sent to amplitude. If the event is found, the subject will be the event.
*
* @param {string} eventName - The type of the event to search for e.g. SwapEventName.SwapTransactionCompleted
* @param {number} timeout - The maximum amount of time (in ms) to wait for the event.
* @returns {Chainable<Subject>}
*/
waitForAmplitudeEvent(eventName: string, requiredProperties?: string[]): Chainable<Subject>
/**
* Intercepts a specific graphql operation and responds with the given fixture.
* @param {string} operationName - The name of the graphql operation to intercept.
* @param {string} fixturePath - The path to the fixture to respond with.
*/
interceptGraphqlOperation(operationName: string, fixturePath: string): Chainable<Subject>
/**
* Intercepts a quote request and responds with the given fixture.
* @param {string} fixturePath - The path to the fixture to respond with.
*/
interceptQuoteRequest(fixturePath: string): Chainable<Subject>
}
interface Cypress {
eagerlyConnect?: boolean
}
interface VisitOptions {
featureFlags?: Array<{ flag: FeatureFlags; value: boolean }>
/**
* Initial user state.
* @default {@type import('../utils/user-state').CONNECTED_WALLET_USER_STATE}
*/
userState?: Partial<UserState>
/**
* If false, prevents the app from eagerly connecting to the injected provider.
* @default true
*/
eagerlyConnect?: false
}
}
}
export function registerCommands() {
// sets up the injected provider to be a mock ethereum provider with the given mnemonic/index
// eslint-disable-next-line no-undef
Cypress.Commands.overwrite(
'visit',
// eslint-disable-next-line max-params
(original, url: string | Partial<Cypress.VisitOptions>, options?: Partial<Cypress.VisitOptions>) => {
if (typeof url !== 'string') {
throw new Error('Invalid arguments. The first argument to cy.visit must be the path.')
}
// Parse overrides
const flagsOn: FeatureFlags[] = []
const flagsOff: FeatureFlags[] = []
options?.featureFlags?.forEach((f) => {
if (f.value) {
flagsOn.push(f.flag)
} else {
flagsOff.push(f.flag)
}
})
// Format into URL parameters
const overrideParams = new URLSearchParams()
if (flagsOn.length > 0) {
overrideParams.append(
'featureFlagOverride',
flagsOn.map((flag) => getFeatureFlagName(flag, FeatureFlagClient.Web)).join(','),
)
}
if (flagsOff.length > 0) {
overrideParams.append(
'featureFlagOverrideOff',
flagsOn.map((flag) => getFeatureFlagName(flag, FeatureFlagClient.Web)).join(','),
)
}
return cy.provider().then((provider) =>
original({
...options,
url:
[...overrideParams.entries()].length === 0
? url
: url.includes('?')
? `${url}&${overrideParams.toString()}`
: `${url}?${overrideParams.toString()}`,
onBeforeLoad(win) {
options?.onBeforeLoad?.(win)
setInitialUserState(win, {
...initialState,
...(options?.userState ?? {}),
})
win.ethereum = provider
win.Cypress.eagerlyConnect = options?.eagerlyConnect ?? true
win.localStorage.setItem(ALLOW_ANALYTICS_ATOM_KEY, 'true')
win.localStorage.setItem('showUniswapExtensionLaunchAtom', 'false')
},
}),
)
},
)
Cypress.Commands.add('waitForAmplitudeEvent', (eventName, requiredProperties) => {
function findAndDiscardEventsUpToTarget() {
const events = Cypress.env('amplitudeEventCache')
const targetEventIndex = events.findIndex((event) => {
if (event.event_type !== eventName) {
return false
}
if (requiredProperties) {
return requiredProperties.every((prop) => event.event_properties[prop])
}
return true
})
if (targetEventIndex !== -1) {
const event = events[targetEventIndex]
Cypress.env('amplitudeEventCache', events.slice(targetEventIndex + 1))
return cy.wrap(event)
} else {
// If not found, retry after waiting for more events to be sent.
return cy.wait('@amplitude').then(findAndDiscardEventsUpToTarget)
}
}
return findAndDiscardEventsUpToTarget()
})
Cypress.env('graphqlInterceptions', new Map())
Cypress.Commands.add('interceptGraphqlOperation', (operationName, fixturePath) => {
const graphqlInterceptions = Cypress.env('graphqlInterceptions')
cy.intercept(/(?:interface|beta).gateway.uniswap.org\/v1\/graphql/, (req) => {
req.headers['origin'] = 'https://app.uniswap.org'
const currentOperationName = req.body.operationName
if (graphqlInterceptions.has(currentOperationName)) {
const fixturePath = graphqlInterceptions.get(currentOperationName)
req.reply({ fixture: fixturePath })
} else {
req.continue()
}
}).as(operationName)
graphqlInterceptions.set(operationName, fixturePath)
})
Cypress.Commands.add('interceptQuoteRequest', (fixturePath) => {
return cy.intercept(/(?:interface|beta).gateway.uniswap.org\/v2\/quote/, (req) => {
req.headers['origin'] = 'https://app.uniswap.org'
req.reply({ fixture: fixturePath })
})
})
}
// ***********************************************************
// This file is processed and loaded automatically before your test files.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
import { registerCommands } from './commands'
import { registerSetupTests } from './setupTests'
// In order to use cypress commands with sideEffects set in package.json
// we need to import the commands and setupTests files here.
// See: https://github.com/cypress-io/cypress-documentation/pull/5454/files
registerCommands()
registerSetupTests()
// Squelch logs from fetches, as they clutter the logs so much as to make them unusable.
// See https://docs.cypress.io/api/commands/intercept#Disabling-logs-for-a-request.
// TODO(https://github.com/cypress-io/cypress/issues/26069): Squelch only wildcard logs once Cypress allows it.
const log = Cypress.log
Cypress.log = function (options, ...args) {
if (options.displayName === 'script' || options.name === 'request') {
return undefined
}
return log(options, ...args)
} as typeof log
Cypress.on('uncaught:exception', () => {
// returning false here prevents Cypress from failing the test
return false
})
import { CyHttpMessages } from 'cypress/types/net-stubbing'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { revertHardhat, setupHardhat } from '../utils'
export function registerSetupTests() {
beforeEach(() => {
// Many API calls enforce that requests come from our app, so we must mock Origin and Referer.
cy.intercept('*', (req) => {
req.headers['referer'] = 'https://app.uniswap.org'
req.headers['origin'] = 'https://app.uniswap.org'
})
// Network RPCs are disabled for cypress tests - calls should be routed through the connected wallet instead.
cy.intercept(/infura.io/, { statusCode: 404 })
cy.intercept(/quiknode.pro/, { statusCode: 404 })
// Log requests to hardhat.
cy.intercept(/:8545/, logJsonRpc)
Cypress.env('amplitudeEventCache', [])
// Mock analytics responses to avoid analytics in tests.
cy.intercept('https://metrics.interface.gateway.uniswap.org/v1/amplitude-proxy', (req) => {
const requestBody = JSON.stringify(req.body)
const byteSize = new Blob([requestBody]).size
req.alias = 'amplitude'
req.reply(
JSON.stringify({
code: 200,
server_upload_time: Date.now(),
payload_size_bytes: byteSize,
events_ingested: req.body.events.length,
}),
{
'origin-country': 'US',
},
)
Cypress.env('amplitudeEventCache').push(...req.body.events)
})
// Mock statsig to allow us to mock flags.
cy.intercept(/statsig/, { statusCode: 409 })
// Mock graphql ops required by TokenSelector
cy.interceptGraphqlOperation('PortfolioBalances', 'portfolio_balances.json').as('PortfolioBalances')
cy.interceptGraphqlOperation('QuickTokenBalancesWeb', 'quick_token_balances.json').as('QuickTokenBalancesWeb')
cy.interceptGraphqlOperation('TopTokens', 'top_tokens.json').as('TopTokens')
cy.interceptGraphqlOperation('TokenProjects', 'token_projects.json').as('TokenProjects')
})
// Reset hardhat between suites to ensure isolation.
// This resets the fork, as well as options like automine.
before(() => cy.hardhat().then((hardhat) => hardhat.reset(UniverseChainId.Mainnet)))
// Reverts hardhat between tests to ensure isolation.
// This reverts the fork, but not options like automine.
setupHardhat()
beforeEach(revertHardhat)
}
function logJsonRpc(req: CyHttpMessages.IncomingHttpRequest) {
req.alias = req.body.method
const log = Cypress.log({
autoEnd: false,
name: req.body.method,
message: req.body.params?.map((param: any) =>
typeof param === 'object' ? '{...}' : param?.toString().substring(0, 10),
),
})
req.on('after:response', (res) => {
if (res.statusCode === 200) {
log.end()
} else {
log.error(new Error(`${res.statusCode}: ${res.statusMessage}`))
}
})
}
{
"extends": "../tsconfig.json",
"compilerOptions": {
"composite": false,
"incremental": true,
"isolatedModules": false,
"noImplicitAny": false,
"sourceMap": false,
"target": "ES6",
"tsBuildInfoFile": "../node_modules/.cache/tsbuildinfo/cypress", // avoid clobbering the build tsbuildinfo
"types": ["cypress", "node"],
},
"include": ["**/*.ts"],
"references": [
{
"path": "../../../packages/ui"
},
{
"path": "../../../packages/utilities"
},
{
"path": "../../../packages/uniswap"
}
]
}
import { HardhatProvider } from 'cypress-hardhat/lib/browser/provider'
import { Utils } from 'cypress-hardhat/lib/browser/utils'
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Mocha {
interface Context {
snapshots?: string[]
}
}
}
// Extended timeout for hardhat to mine a single block.
// Use as part of cy.hardhat().then({ timeout: HARDHAT_TIMEOUT }, (hardhat) => ...),
// but limit the hardhat usage *per thennable* to a single mined block.
export const HARDHAT_TIMEOUT = 48_000
export const getTestSelector = (selectorId: string) => `[data-testid=${selectorId}]`
/**
* Sets up hardhat, and reverts it after tests to ensure isolation.
* This reverts the fork, but not options like automine.
*/
export function setupHardhat(fn?: (hardhat: Utils) => Promise<void>) {
let snapshot: string
before(function () {
// This stack - on the Mocha Context - tracks all snapshots derived from setupHardhat.
this.snapshots ||= []
return cy.hardhat().then({ timeout: HARDHAT_TIMEOUT }, async (hardhat) => {
await fn?.(hardhat)
snapshot = await hardhat.send('evm_snapshot', [])
this.snapshots?.push(snapshot)
})
})
after(function () {
this.snapshots?.pop()
})
}
//
// Reverts hardhat to the top snapshot on the stack.
// Must only be called once per test. Should only be called in setupTests.ts.
export function revertHardhat(this: Mocha.Context) {
return cy.hardhat().then({ timeout: HARDHAT_TIMEOUT }, async (hardhat) => {
const snapshot = this.snapshots?.pop()
// Only revert the latest snapshot, as reverting past that will invalidate other snapshots.
if (snapshot) {
await hardhat.send('evm_revert', [snapshot])
// Providers will not "rewind" to an older block number nor notice chain changes, so they must be reset.
hardhat.providers.forEach((provider) => (provider as HardhatProvider).reset())
this.snapshots?.push(await hardhat.send('evm_snapshot', []))
}
})
}
/** Revert back to MAINNET. Used after each test which changes chains. */
export function resetHardhatChain() {
cy.hardhat().then((hardhat) => {
// Intentionally not awaited for, to avoid stalling in case the method is stubbed.
hardhat.send('wallet_switchEthereumChain', [{ chainId: '0x1' }])
})
}
import { UserState } from '../../src/state/user/reducer'
/**
* This sets the initial value of the "user" slice in localStorage.
* Other persisted slices are not set, so they will be filled with their respective initial values
* when the app runs.
*/
export function setInitialUserState(win: Cypress.AUTWindow, state: UserState) {
// We want to test from a clean state, so we clear the local storage (which clears redux).
win.localStorage.clear()
// Set initial user state.
win.localStorage.setItem(
'redux/persist:interface',
JSON.stringify({
user: state,
})
)
}
......@@ -94,7 +94,7 @@ function PoolImage({
)
}
export const onRequest: PagesFunction = async ({ params, request }) => {
export const onRequest: PagesFunction = async ({ params, request, env }) => {
try {
const origin = new URL(request.url).origin
const { index } = params
......@@ -112,7 +112,7 @@ export const onRequest: PagesFunction = async ({ params, request }) => {
return new Response('Pool not found.', { status: 404 })
}
const [fontData] = await Promise.all([getFont(origin)])
const [fontData] = await Promise.all([getFont(origin, env)])
const networkLogo = getNetworkLogoUrl(networkName.toUpperCase(), origin)
return new ImageResponse(
......@@ -156,6 +156,7 @@ export const onRequest: PagesFunction = async ({ params, request }) => {
<img
src={networkLogo}
width="48px"
height="48px"
style={{
position: 'absolute',
right: '2px',
......
......@@ -8,7 +8,7 @@ import { getRGBColor } from '../../../utils/getRGBColor'
import { getRequest } from '../../../utils/getRequest'
import getToken from '../../../utils/getToken'
export const onRequest: PagesFunction = async ({ params, request }) => {
export const onRequest: PagesFunction = async ({ params, request, env }) => {
try {
const origin = new URL(request.url).origin
const { index } = params
......@@ -27,7 +27,7 @@ export const onRequest: PagesFunction = async ({ params, request }) => {
return new Response('Token not found.', { status: 404 })
}
const [fontData, palette] = await Promise.all([getFont(origin), getRGBColor(data.ogImage, true)])
const [fontData, palette] = await Promise.all([getFont(origin, env), getRGBColor(data.ogImage, true)])
const networkLogo = getNetworkLogoUrl(networkName.toUpperCase(), origin)
......@@ -66,11 +66,12 @@ export const onRequest: PagesFunction = async ({ params, request }) => {
}}
>
{ogImage ? (
<img src={ogImage} width="144px" style={{ borderRadius: '100%' }}>
<img src={ogImage} width="144px" height="144px" style={{ borderRadius: '100%' }}>
{networkLogo != '' && (
<img
src={networkLogo}
width="48px"
height="48px"
style={{
position: 'absolute',
right: '2px',
......@@ -105,6 +106,7 @@ export const onRequest: PagesFunction = async ({ params, request }) => {
<img
src={networkLogo}
width="48px"
height="48px"
style={{
position: 'absolute',
right: '2px',
......
......@@ -121,6 +121,7 @@ window.$RefreshSig$ = () => (type) => type;</script>
<div id="root"></div>
<div id="background-radial-gradient"></div>
<script type="module" src="/zone-events.js"></script>
<!-- Vite entry point -->
<script type="module" src="/src/index.tsx"></script>
</body>
......@@ -249,6 +250,7 @@ window.$RefreshSig$ = () => (type) => type;</script>
<div id="root"></div>
<div id="background-radial-gradient"></div>
<script type="module" src="/zone-events.js"></script>
<!-- Vite entry point -->
<script type="module" src="/src/index.tsx"></script>
</body>
......@@ -377,6 +379,7 @@ window.$RefreshSig$ = () => (type) => type;</script>
<div id="root"></div>
<div id="background-radial-gradient"></div>
<script type="module" src="/zone-events.js"></script>
<!-- Vite entry point -->
<script type="module" src="/src/index.tsx"></script>
</body>
......
......@@ -121,6 +121,7 @@ window.$RefreshSig$ = () => (type) => type;</script>
<div id="root"></div>
<div id="background-radial-gradient"></div>
<script type="module" src="/zone-events.js"></script>
<!-- Vite entry point -->
<script type="module" src="/src/index.tsx"></script>
</body>
......@@ -249,6 +250,7 @@ window.$RefreshSig$ = () => (type) => type;</script>
<div id="root"></div>
<div id="background-radial-gradient"></div>
<script type="module" src="/zone-events.js"></script>
<!-- Vite entry point -->
<script type="module" src="/src/index.tsx"></script>
</body>
......@@ -377,6 +379,7 @@ window.$RefreshSig$ = () => (type) => type;</script>
<div id="root"></div>
<div id="background-radial-gradient"></div>
<script type="module" src="/zone-events.js"></script>
<!-- Vite entry point -->
<script type="module" src="/src/index.tsx"></script>
</body>
......@@ -505,6 +508,7 @@ window.$RefreshSig$ = () => (type) => type;</script>
<div id="root"></div>
<div id="background-radial-gradient"></div>
<script type="module" src="/zone-events.js"></script>
<!-- Vite entry point -->
<script type="module" src="/src/index.tsx"></script>
</body>
......
export default async function getFont(origin: string) {
const url = origin + '/fonts/Inter-normal.var.ttf'
const font = await fetch(url)
return font.arrayBuffer()
export default async function getFont(
origin: string,
env: {
ASSETS: {
fetch: typeof fetch
}
},
) {
try {
// Cloudflare Workers needs a full URL to fetch from,
// but will only use the pathname to fetch from the ASSETS binding.
const req = new Request('https://dummy.example/fonts/Inter-normal.var.ttf', {
headers: { 'Accept-Encoding': 'identity' }, // prevent TTF → WOFF2 rewrite
})
const font = await env.ASSETS.fetch(req)
if (!font.ok) {
throw new Error('Failed to fetch font from ASSETS binding')
}
return font.arrayBuffer()
} catch (e) {
// Fallback to fetching from the origin if the ASSETS binding is not available.
const url = origin + '/fonts/Inter-normal.var.ttf'
const font = await fetch(url)
return font.arrayBuffer()
}
}
import { ChainId } from '@uniswap/sdk-core'
/* eslint-env node */
require('dotenv').config()
const forkingConfig = {
httpHeaders: {
Origin: 'localhost:3000', // infura allowlists requests by origin
},
}
const forks = {
[ChainId.MAINNET]: {
url: `https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
...forkingConfig,
},
[ChainId.POLYGON]: {
url: `https://polygon-mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
...forkingConfig,
},
[ChainId.OPTIMISM]: {
url: `https://optimism-mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
...forkingConfig,
},
}
module.exports = {
forks,
networks: {
hardhat: {
loggingEnabled: !process.env.CI,
chainId: ChainId.MAINNET,
forking: forks[ChainId.MAINNET],
accounts: {
count: 2,
},
mining: {
auto: true, // automine to make tests easier to write.
interval: 0, // do not interval mine so that tests remain deterministic
},
},
},
}
......@@ -111,6 +111,7 @@
<div id="root"></div>
<div id="background-radial-gradient"></div>
<script type="module" src="/zone-events.js"></script>
<!-- Vite entry point -->
<script type="module" src="/src/index.tsx"></script>
</body>
......
......@@ -5,8 +5,8 @@
"license": "GPL-3.0-or-later",
"scripts": {
"ajv": "node scripts/compile-ajv-validators.js",
"anvil:mainnet": "dotenv -- bash -c 'RUST_LOG=debug anvil --fork-url https://${REACT_APP_QUICKNODE_ENDPOINT_NAME}.quiknode.pro/${REACT_APP_QUICKNODE_ENDPOINT_TOKEN} --hardfork prague'",
"anvil:base": "dotenv -- bash -c 'RUST_LOG=debug anvil --fork-url https://${REACT_APP_QUICKNODE_ENDPOINT_NAME}.base-mainnet.quiknode.pro/${REACT_APP_QUICKNODE_ENDPOINT_TOKEN} --hardfork prague --port 8546'",
"anvil:mainnet": "dotenv -- bash -c 'RUST_LOG=debug anvil --print-traces --fork-url https://${REACT_APP_QUICKNODE_ENDPOINT_NAME}.quiknode.pro/${REACT_APP_QUICKNODE_ENDPOINT_TOKEN} --hardfork prague'",
"anvil:base": "dotenv -- bash -c 'RUST_LOG=debug anvil --print-traces --fork-url https://${REACT_APP_QUICKNODE_ENDPOINT_NAME}.base-mainnet.quiknode.pro/${REACT_APP_QUICKNODE_ENDPOINT_TOKEN} --hardfork prague --port 8546'",
"check:deps:usage": "depcheck",
"check:circular": "concurrently \"../../scripts/check-circular-imports.sh ./src/pages/App.tsx 3\" \"../../scripts/check-circular-imports.sh ./src/setupTests.ts 0\"",
"sitemap:generate": "node scripts/generate-sitemap.js",
......@@ -15,6 +15,7 @@
"vite:build:production": "ROLLDOWN_OPTIONS_VALIDATION=loose vite build",
"vite:build:staging": "ROLLDOWN_OPTIONS_VALIDATION=loose vite build --mode staging",
"vite:preview": "ROLLDOWN_OPTIONS_VALIDATION=loose vite preview",
"functions:build": "wrangler --config ./wrangler-vite-worker.jsonc pages functions build --outdir=./build/worker/",
"start:cloud": "NODE_OPTIONS=--dns-result-order=ipv4first REACT_APP_SKIP_CSP=1 npx wrangler@4.14.4 pages dev --compatibility-flags=nodejs_compat --compatibility-date=2023-08-01 --proxy=3001 --port=3000 -- yarn vite:dev --port 3001",
"build:production": "NODE_OPTIONS=--max-old-space-size=8192 craco build",
"build:production:analyze": "UNISWAP_ANALYZE_BUNDLE_SIZE=static craco build",
......@@ -25,9 +26,8 @@
"lint": "yarn eslint --ignore-path .gitignore --cache --cache-location node_modules/.cache/eslint/ --max-warnings=0 .",
"lint:fix": "yarn eslint --ignore-path .gitignore --cache --cache-location node_modules/.cache/eslint/ . --fix",
"playwright:test": "playwright test",
"typecheck": "tsc && yarn typecheck:cloud && yarn typecheck:cypress",
"typecheck": "tsc && yarn typecheck:cloud",
"typecheck:cloud": "tsc -p functions/tsconfig.json",
"typecheck:cypress": "tsc -p cypress/tsconfig.json",
"find:unused": "bash scripts/delete-unused-assets.sh",
"test": "NODE_OPTIONS='--no-deprecation' vitest run",
"test:set1": "NODE_OPTIONS='--no-deprecation' vitest run src/components",
......@@ -38,8 +38,6 @@
"test:bundle": "node -r esbuild-register ./src/test-utils/bundle-size-test.ts",
"snapshots": "NODE_OPTIONS='--no-deprecation' vitest run -u",
"test:cloud": "NODE_OPTIONS='--no-deprecation' vitest run functions --config functions/vitest.config.ts",
"cypress:open": "cypress open --browser chrome --e2e",
"cypress:run": "cypress run --browser chrome --e2e",
"deduplicate": "yarn-deduplicate --strategy=highest",
"storybook:run": "storybook dev -p 6006",
"storybook:run:with-tests": "concurrently -s second -n \"SB RUN,SB TEST WATCH\" -c \"magenta,blue\" \"yarn storybook:run --quiet\" \"wait-on tcp:127.0.0.1:6006 -t 600000 && yarn storybook:test --watch\"",
......@@ -106,7 +104,6 @@
"@types/react-window": "1.8.2",
"@types/rebass": "4.0.7",
"@types/styled-components": "5.1.25",
"@types/testing-library__cypress": "5.0.13",
"@types/uuid": "9.0.1",
"@types/wcag-contrast": "3.0.0",
"@types/xml2js": "0.4.14",
......@@ -119,8 +116,6 @@
"babel-plugin-react-compiler": "19.1.0-rc.2",
"browser-cache-mock": "0.1.7",
"concurrently": "8.2.2",
"cypress": "12.17.4",
"cypress-hardhat": "2.5.3",
"depcheck": "1.4.7",
"detect-package-manager": "3.0.2",
"dotenv": "16.0.3",
......@@ -129,7 +124,6 @@
"eslint": "8.44.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-storybook": "0.8.0",
"hardhat": "2.22.16",
"http-server": "14.1.1",
"husky": "8.0.3",
"jest-extended": "4.0.2",
......@@ -167,7 +161,7 @@
"webpack": "5.90.0",
"webpack-bundle-analyzer": "4.10.2",
"webpack-retry-chunk-load-plugin": "3.1.1",
"wrangler": "3.15.0",
"wrangler": "4.20.0",
"yarn-deduplicate": "6.0.0"
},
"dependencies": {
......@@ -260,7 +254,7 @@
"react-native-reanimated": "3.16.7",
"react-popper": "2.3.0",
"react-redux": "8.0.5",
"react-router-dom": "6.10.0",
"react-router-dom": "6.30.1",
"react-scroll-sync": "0.11.2",
"react-virtualized-auto-sizer": "1.0.20",
"react-window": "1.8.9",
......@@ -283,8 +277,7 @@
"wagmi": "2.15.4",
"wcag-contrast": "3.0.0",
"web-vitals": "2.1.4",
"xml2js": "0.6.2",
"zone.js": "0.15.1"
"xml2js": "0.6.2"
},
"engines": {
"npm": "please-use-yarn",
......
......@@ -43,6 +43,9 @@
<link rel="preload" href="%PUBLIC_URL%/fonts/Basel-Grotesk-Book.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="%PUBLIC_URL%/fonts/Basel-Grotesk-Medium.woff2" as="font" type="font/woff2" crossorigin />
<!-- Load zone-events.js with highest priority before any other JS resources -->
<script src="%PUBLIC_URL%/zone-events.js"></script>
<!-- Include <link rel="preload"> for js assets so that they are loaded with high priority. -->
<%= htmlWebpackPlugin.files.js.map((href) => `<link rel="preload" href="${href}" as="script" />`).join('\n ') %>
......
This diff is collapsed.
This diff is collapsed.
/**
* zone-events.ts – TEMP shim for Tamagui scroll-lock bug
* → Must run **before** `@tamagui/polyfill-dev` is imported.
*
* Adapted from Zone.js (MIT); only the add/removeEventListener patch is kept.
*/
;(() => {
const nativeAdd = EventTarget.prototype.addEventListener
const nativeRm = EventTarget.prototype.removeEventListener
function normalizeOpts(opts) {
if (opts == null) {
return false
}
if (typeof opts === 'boolean') {
return opts
}
return !!opts.capture
}
// eslint-disable-next-line max-params
EventTarget.prototype.addEventListener = function (type, cb, opts) {
return nativeAdd.call(this, type, cb, normalizeOpts(opts))
}
// eslint-disable-next-line max-params
EventTarget.prototype.removeEventListener = function (type, cb, opts) {
return nativeRm.call(this, type, cb, normalizeOpts(opts))
}
})()
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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