ci(release): publish latest release

parent b42b73d0
IPFS hash of the deployment:
- CIDv0: `QmXAfrFgiMF3naNbfoknu5e6H1ussSDJbvb1C1Paw5Ugpi`
- CIDv1: `bafybeiedfku63hfv4tr7zi3kx357dkdjvcoxp4wfv5i75y3qfabtwo3eym`
- CIDv0: `QmbEmY4ebbPrQirzx9PNZGKqyjK7pgUMVce2F2tMtJHbJ5`
- CIDv1: `bafybeif7uyvbcyhd5ib6xkikxoyvm4voodg4pgagjckny4m52o7qtclpsy`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
......@@ -10,15 +10,51 @@ You can also access the Uniswap Interface from an IPFS gateway.
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://bafybeiedfku63hfv4tr7zi3kx357dkdjvcoxp4wfv5i75y3qfabtwo3eym.ipfs.dweb.link/
- https://bafybeiedfku63hfv4tr7zi3kx357dkdjvcoxp4wfv5i75y3qfabtwo3eym.ipfs.cf-ipfs.com/
- [ipfs://QmXAfrFgiMF3naNbfoknu5e6H1ussSDJbvb1C1Paw5Ugpi/](ipfs://QmXAfrFgiMF3naNbfoknu5e6H1ussSDJbvb1C1Paw5Ugpi/)
- https://bafybeif7uyvbcyhd5ib6xkikxoyvm4voodg4pgagjckny4m52o7qtclpsy.ipfs.dweb.link/
- https://bafybeif7uyvbcyhd5ib6xkikxoyvm4voodg4pgagjckny4m52o7qtclpsy.ipfs.cf-ipfs.com/
- [ipfs://QmbEmY4ebbPrQirzx9PNZGKqyjK7pgUMVce2F2tMtJHbJ5/](ipfs://QmbEmY4ebbPrQirzx9PNZGKqyjK7pgUMVce2F2tMtJHbJ5/)
### 5.23.5 (2024-04-15)
## 5.24.0 (2024-04-17)
### Features
* **web:** [wagmi] Add viem to ethers adapters (#7237) 276faae
* **web:** deprecate token logo lookups (#6921) 0ade412
* **web:** deprecate+delete token safety lookups (#7132) b4642e9
* **web:** update signatures from subscription (#7389) f867870
### Bug Fixes
* **web:** parse native MATIC correctly (#7523) 12a6205
* **web:** allow TrustWallet nodes in CSP (#7515) 0a5b28d
* **web:** change background color of LP warning banner (#7491) f543ab0
* **web:** change warning icon color; add learn more link (#7483) 352be3b
* **web:** checkbox color on open-limits drawer (#7410) 05dfaed
* **web:** dismiss chart tooltip when clicking outside of chart (#7285) 3d97a14
* **web:** ellipsis on unitag text in side drawer (#7386) 471661c
* **web:** fix cypress tests (#7347) e54ceb9
* **web:** fix icon cutoff (#7583) a01e106
* **web:** fix translations with JSX rendering as [object Object] (#7336) 67df76c
* **web:** fix uniswapx e2e tests (#7458) c6de35e
* **web:** fix x-chain token logos (#7374) 4367e3a
* **web:** functions pass-through (#7338) ace3062
* **web:** make sure SimpleToken hits the cache in all cases (#7488) 1be2954
* **web:** parse matic correctly from gql response (#7519) 800eae5
* **web:** remove assets repo fallback for all tokens (#7476) 8e2142c
* **web:** Send crashing on useENSAvatar while disconnected (#7595) 131c6a3
* **web:** set usePoolData errorPolicy to all (#7466) 55e3e8a
* **web:** SimpleTokenDetails fragment to be used in all queries (#7549) 678e624
* **web:** switch currency when input equals output (staging) 20277d2
* **web:** TokenBalanceProvider account change (#7432) 516b781
* **web:** use accent warning soft for outage banner icon wrapper (#7428) cda7ea9
* **web:** use cache-first policy for TopTokens query (#7484) 25d0950
### Code Refactoring
* **web:** isolate the subscription updater (#7387) 6caabbd
* **web:** split out parseRemote signature (#7388) 223e766
* **web:** use a single codepath for functions response transform (#7363) 60471a8
web/5.23.5
\ No newline at end of file
web/5.24.0
\ No newline at end of file
......@@ -111,6 +111,7 @@ Add the following to your .rc file
Install [Android Studio](https://developer.android.com/studio)
Add the following to your .rc file
```
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/emulator
......@@ -175,7 +176,7 @@ These are some tools you might want to familiarize yourself with to understand t
## Migrations
We use `redux-persist` to persist Redux state between user sessions. When the Redux state schema is altered, a migration may be needed to transfer the existing persisted state to the new Redux schema. Failing to define a migration results in the app defaulting to the persisted schema, which will very likely cause `undefined` errors because the code has references to Redux state properties that were dropped in favor the the persisted schema.
We use `redux-persist` to persist Redux state between user sessions. When the Redux state schema is altered, a migration may be needed to transfer the existing persisted state to the new Redux schema. Failing to define a migration results in the app defaulting to the persisted schema, which will very likely cause `undefined` errors because the code has references to Redux state properties that were dropped in favor the persisted schema.
### When to define a migration
......
......@@ -1151,12 +1151,8 @@ PODS:
- React-Core
- react-native-restart (0.0.27):
- React-Core
- react-native-safe-area-context (4.5.0):
- RCT-Folly
- RCTRequired
- RCTTypeSafety
- react-native-safe-area-context (4.9.0):
- React-Core
- ReactCommon/turbomodule/core
- react-native-skia (0.1.187):
- React
- React-callinvoker
......@@ -1749,7 +1745,7 @@ SPEC CHECKSUMS:
react-native-onesignal: ab800900cffeca4d9db70a05244013fc8a36ceb8
react-native-pager-view: 3051346698a0ba0c4e13e40097cc11b00ee03cca
react-native-restart: 7595693413fe3ca15893702f2c8306c62a708162
react-native-safe-area-context: 39c2d8be3328df5d437ac1700f4f3a4f75716acc
react-native-safe-area-context: b97eb6f9e3b7f437806c2ce5983f479f8eb5de4b
react-native-skia: e7385e2f5ebe284df53f0def573198fe69a7bd72
react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
react-native-webview: d33e2db8925d090871ffeb232dfa50cb3a727581
......
......@@ -4,10 +4,6 @@ const preset = require('../../config/jest-presets/jest/jest-preset')
module.exports = {
...preset,
preset: 'jest-expo',
transform: {
...preset.transform,
'^.+\\.jsx?$': 'babel-jest',
},
displayName: 'Mobile Wallet',
collectCoverageFrom: [
'src/**/*.{js,ts,tsx}',
......
......@@ -171,7 +171,6 @@
"@uniswap/eslint-config": "workspace:^",
"@walletconnect/types": "2.11.2",
"@welldone-software/why-did-you-render": "7.0.1",
"babel-jest": "29.6.1",
"babel-loader": "8.2.3",
"babel-plugin-react-native-web": "0.17.5",
"babel-plugin-react-require": "4.0.0",
......
......@@ -45,15 +45,14 @@ import {
getSentryTracesSamplingRate,
getStatsigEnvironmentTier,
} from 'src/utils/version'
import { Statsig, StatsigProvider } from 'statsig-react-native'
import { StatsigProvider } from 'statsig-react-native'
import { flexStyles, useIsDarkMode } from 'ui/src'
import { config } from 'uniswap/src/config'
import { uniswapUrls } from 'uniswap/src/constants/urls'
import {
DUMMY_STATSIG_SDK_KEY,
ExperimentsWallet,
} from 'uniswap/src/features/experiments/constants'
import { WALLET_FEATURE_FLAG_NAMES } from 'uniswap/src/features/experiments/flags'
import { DUMMY_STATSIG_SDK_KEY } from 'uniswap/src/features/statsig/constants'
import { WALLET_EXPERIMENTS } from 'uniswap/src/features/statsig/experiments'
import { WALLET_FEATURE_FLAG_NAMES } from 'uniswap/src/features/statsig/flags'
import { Statsig } from 'uniswap/src/features/statsig/sdk/statsig'
import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context'
import i18n from 'uniswap/src/i18n/i18n'
import { CurrencyId } from 'uniswap/src/types/currency'
......@@ -175,12 +174,13 @@ function SentryTags({ children }: PropsWithChildren): JSX.Element {
Sentry.setTag(`featureFlag.${flagKey}`, Statsig.checkGateWithExposureLoggingDisabled(flagKey))
}
Object.entries(ExperimentsWallet).map(([_, experimentName]) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const [_, experimentDef] of WALLET_EXPERIMENTS.entries()) {
Sentry.setTag(
`experiment.${experimentName}`,
Statsig.getExperimentWithExposureLoggingDisabled(experimentName).getGroupName()
`experiment.${experimentDef.name}`,
Statsig.getExperimentWithExposureLoggingDisabled(experimentDef.name).getGroupName()
)
})
}
}, [])
return <>{children}</>
......
......@@ -6,25 +6,21 @@ import { useAppStackNavigation } from 'src/app/navigation/types'
import { closeModal, openModal } from 'src/features/modals/modalSlice'
import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex'
import { Screens } from 'src/screens/Screens'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlag } from 'uniswap/src/features/statsig/hooks'
import { logger } from 'utilities/src/logger/logger'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
import {
NavigateToNftItemArgs,
NavigateToSendArgs,
NavigateToSendFlowArgs,
NavigateToSwapFlowArgs,
ShareNftArgs,
ShareTokenArgs,
WalletNavigationProvider,
getNavigateToSendFlowArgsInitialState,
getNavigateToSwapFlowArgsInitialState,
} from 'wallet/src/contexts/WalletNavigationContext'
import { AssetType } from 'wallet/src/entities/assets'
import { useFiatOnRampIpAddressQuery } from 'wallet/src/features/fiatOnRamp/api'
import {
CurrencyField,
TransactionState,
} from 'wallet/src/features/transactions/transactionState/types'
import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry'
import { ModalName, ShareableEntity, WalletEventName } from 'wallet/src/telemetry/constants'
import { getNftUrl, getTokenUrl } from 'wallet/src/utils/linking'
......@@ -122,24 +118,12 @@ function useNavigateToReceive(): () => void {
}, [dispatch])
}
function useNavigateToSend(): (args: NavigateToSendArgs) => void {
function useNavigateToSend(): (args: NavigateToSendFlowArgs) => void {
const dispatch = useAppDispatch()
return useCallback(
(args: NavigateToSendArgs) => {
const initialSendState: TransactionState = {
exactCurrencyField: CurrencyField.INPUT,
exactAmountToken: '',
[CurrencyField.INPUT]: args
? {
address: args.currencyAddress,
chainId: args.chainId,
type: AssetType.Currency,
}
: null,
[CurrencyField.OUTPUT]: null,
showRecipientSelector: true,
}
(args: NavigateToSendFlowArgs) => {
const initialSendState = getNavigateToSendFlowArgsInitialState(args)
dispatch(openModal({ name: ModalName.Send, initialState: initialSendState }))
},
[dispatch]
......@@ -160,9 +144,18 @@ function useNavigateToSwapFlow(): (args: NavigateToSwapFlowArgs) => void {
}
function useNavigateToTokenDetails(): (currencyId: string) => void {
return useCallback((currencyId: string): void => {
exploreNavigationRef.navigate(Screens.TokenDetails, { currencyId })
}, [])
const appNavigation = useAppStackNavigation()
return useCallback(
(currencyId: string): void => {
if (exploreNavigationRef.isFocused()) {
exploreNavigationRef.navigate(Screens.TokenDetails, { currencyId })
} else {
appNavigation.navigate(Screens.TokenDetails, { currencyId })
}
},
[appNavigation]
)
}
function useNavigateToNftDetails(): (args: NavigateToNftItemArgs) => void {
......
......@@ -62,6 +62,7 @@ import {
v5Schema,
v60Schema,
v61Schema,
v62Schema,
v6Schema,
v7Schema,
v8Schema,
......@@ -1390,4 +1391,11 @@ describe('Redux state migrations', () => {
expect(v62.behaviorHistory.extensionOnboardingState).toBe(ExtensionOnboardingState.Undefined)
})
it('migrates from v62 to 63', () => {
const v62Stub = { ...v62Schema }
const v63 = migrations[63](v62Stub)
expect(v63.wallet.isUnlocked).toBe(undefined)
})
})
......@@ -875,4 +875,11 @@ export const migrations = {
return newState
},
63: function removeWalletIsUnlockedState(state: any) {
const newState = { ...state }
delete newState.wallet.isUnlocked
return newState
},
}
......@@ -22,8 +22,8 @@ import {
useSporeColors,
} from 'ui/src'
import { spacing } from 'ui/src/theme'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlag } from 'uniswap/src/features/statsig/hooks'
import { isAndroid } from 'uniswap/src/utils/platform'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { ActionSheetModal, MenuItemProp } from 'wallet/src/components/modals/ActionSheetModal'
......
......@@ -6,11 +6,6 @@ import { useAppDispatch, useAppSelector } from 'src/app/hooks'
import { closeModal } from 'src/features/modals/modalSlice'
import { selectCustomEndpoint } from 'src/features/tweaks/selectors'
import { setCustomEndpoint } from 'src/features/tweaks/slice'
import {
ConfigResult,
Statsig,
useExperimentWithExposureLoggingDisabled,
} from 'statsig-react-native'
import {
Accordion,
Button,
......@@ -23,15 +18,20 @@ import {
} from 'ui/src'
import { spacing } from 'ui/src/theme'
import {
EXPERIMENT_VALUES_BY_EXPERIMENT,
ExperimentsWallet,
} from 'uniswap/src/features/experiments/constants'
Experiments,
WALLET_EXPERIMENTS,
getExperimentDefinition,
} from 'uniswap/src/features/statsig/experiments'
import {
FeatureFlags,
WALLET_FEATURE_FLAG_NAMES,
getFeatureFlagName,
} from 'uniswap/src/features/experiments/flags'
import { useFeatureFlagWithExposureLoggingDisabled } from 'uniswap/src/features/experiments/hooks'
} from 'uniswap/src/features/statsig/flags'
import {
useExperimentValueWithExposureLoggingDisabled,
useFeatureFlagWithExposureLoggingDisabled,
} from 'uniswap/src/features/statsig/hooks'
import { Statsig } from 'uniswap/src/features/statsig/sdk/statsig'
import { Switch } from 'wallet/src/components/buttons/Switch'
import { TextInput } from 'wallet/src/components/input/TextInput'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
......@@ -65,11 +65,16 @@ export function ExperimentsModal(): JSX.Element {
}
}
const featureFlagRows = []
const featureFlagRows: JSX.Element[] = []
for (const [flag, flagName] of WALLET_FEATURE_FLAG_NAMES.entries()) {
featureFlagRows.push(<FeatureFlagRow key={flagName} flag={flag} />)
}
const experimentRows: JSX.Element[] = []
for (const [experiment, experimentDef] of WALLET_EXPERIMENTS.entries()) {
experimentRows.push(<ExperimentRow key={experimentDef.name} experiment={experiment} />)
}
return (
<BottomSheetModal
fullScreen
......@@ -150,9 +155,7 @@ export function ExperimentsModal(): JSX.Element {
</Text>
<Flex gap="$spacing24" mt="$spacing12">
{Object.values(ExperimentsWallet).map((experiment) => {
return <ExperimentRow key={experiment} name={experiment} />
})}
{experimentRows}
</Flex>
</Accordion.Content>
</Accordion.Item>
......@@ -196,75 +199,52 @@ function FeatureFlagRow({ flag }: { flag: FeatureFlags }): JSX.Element {
)
}
function ExperimentRow({ name }: { name: string }): JSX.Element {
const experiment = useExperimentWithExposureLoggingDisabled(name)
const params = Object.entries(experiment.config.value).map(([key, value]) => (
<Flex
key={key}
row
alignItems="center"
gap="$spacing16"
justifyContent="space-between"
paddingStart="$spacing16">
<Text variant="body2">{key}</Text>
<ExperimentValueSwitch
configValueContent={value}
configValueName={key}
experiment={experiment}
/>
</Flex>
))
function ExperimentRow({ experiment }: { experiment: Experiments }): JSX.Element {
const experimentDef = getExperimentDefinition(experiment)
return (
<>
<Separator />
<Flex>
<Text variant="body1">{name}</Text>
<Flex gap="$spacing4">{params}</Flex>
<Text variant="body1">{experimentDef.name}</Text>
<Flex gap="$spacing4">
<Flex
key={experimentDef.name}
row
alignItems="center"
gap="$spacing16"
justifyContent="space-between"
paddingStart="$spacing16">
<Text variant="body2" />
<ExperimentValueSwitch experiment={experiment} />
</Flex>
</Flex>
</Flex>
</>
)
}
function ExperimentValueSwitch({
experiment,
configValueContent,
configValueName,
}: {
experiment: ConfigResult
configValueContent: unknown
configValueName: string
}): JSX.Element {
function ExperimentValueSwitch({ experiment }: { experiment: Experiments }): JSX.Element {
const colors = useSporeColors()
const experimentName = experiment.config.getName()
const onValueChange = (newValue: boolean | string): void => {
Statsig.overrideConfig(experimentName, {
...experiment.config.value,
[configValueName]: newValue,
})
}
const experimentDef = getExperimentDefinition(experiment)
const currentValue = useExperimentValueWithExposureLoggingDisabled(experiment)
if (typeof configValueContent === 'boolean') {
return <Switch value={configValueContent} onValueChange={onValueChange} />
}
const variants = EXPERIMENT_VALUES_BY_EXPERIMENT[experimentName]?.[configValueName]
if (variants && typeof configValueContent === 'string') {
return (
<Flex gap="$spacing8">
{Object.entries(variants).map(([_, value]) => (
<Flex key={value} gap="$spacing4" onPressOut={(): void => onValueChange(value)}>
<Text color={value === configValueContent ? colors.accent1.val : colors.neutral1.val}>
{value}
</Text>
</Flex>
))}
</Flex>
)
}
return <Text variant="body3">Unknown Variants</Text>
return (
<Flex gap="$spacing8">
{experimentDef.values.map((value) => (
<Flex
key={value}
gap="$spacing4"
onPressOut={() => {
Statsig.overrideConfig(experimentDef.name, {
[experimentDef.key]: value,
})
}}>
<Text color={value === currentValue ? colors.accent1.val : colors.neutral1.val}>
{value}
</Text>
</Flex>
))}
</Flex>
)
}
......@@ -5,8 +5,8 @@ import { selectModalState } from 'src/features/modals/selectModalState'
import { TransferFlow } from 'src/features/transactions/transfer/TransferFlow'
import { TransferFlow as TransferFlowRewrite } from 'src/features/transactions/transfer/transferRewrite/TransferFlow'
import { useSporeColors } from 'ui/src'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlag } from 'uniswap/src/features/statsig/hooks'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { ModalName } from 'wallet/src/telemetry/constants'
......
......@@ -93,7 +93,7 @@ export function useEagerExternalProfileRootNavigation(): {
/**
* Utility hook that checks if the caller is part of the navigation tree.
*
* Inspired by how the navigation library checks if the the navigation object exists.
* Inspired by how the navigation library checks if the navigation object exists.
* https://github.com/react-navigation/react-navigation/blob/d7032ba8bb6ae24030a47f0724b61b561132fca6/packages/core/src/useNavigation.tsx#L18
*/
export function useIsPartOfNavigationTree(): boolean {
......
......@@ -64,8 +64,8 @@ import { TokenDetailsScreen } from 'src/screens/TokenDetailsScreen'
import { WebViewScreen } from 'src/screens/WebViewScreen'
import { Icons, useDeviceInsets, useSporeColors } from 'ui/src'
import { spacing } from 'ui/src/theme'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlag } from 'uniswap/src/features/statsig/hooks'
import { OnboardingEntryPoint } from 'wallet/src/features/onboarding/types'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
import { selectFinishedOnboarding } from 'wallet/src/features/wallet/selectors'
......
......@@ -475,6 +475,19 @@ export const v62Schema = {
},
}
const v63SchemaIntermediate = {
...v62Schema,
wallet: {
...v62Schema.wallet,
isUnlocked: undefined,
},
}
// We will no longer keep track of this in the redux state.
delete v63SchemaIntermediate.wallet.isUnlocked
export const v63Schema = v63SchemaIntermediate
// TODO: [MOB-201] use function with typed output when API reducers are removed from rootReducer
// export const getSchema = (): RootState => v0Schema
export const getSchema = (): typeof v62Schema => v62Schema
export const getSchema = (): typeof v63Schema => v63Schema
......@@ -75,7 +75,7 @@ export const persistConfig = {
key: 'root',
storage: reduxStorage,
whitelist,
version: 62,
version: 63,
migrate: createMigrate(migrations),
}
......
......@@ -128,7 +128,9 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
}, [permissionStatus, requestPermissionResponse, t])
const overlayWidth = (overlayLayout?.height ?? 0) / CAMERA_ASPECT_RATIO
const scannerSize = Math.min(overlayWidth, dimensions.fullWidth) * SCAN_ICON_WIDTH_RATIO
const cameraWidth = dimensions.fullWidth
const cameraHeight = CAMERA_ASPECT_RATIO * cameraWidth
const scannerSize = Math.min(overlayWidth, cameraWidth) * SCAN_ICON_WIDTH_RATIO
return (
<AnimatedFlex
......@@ -138,10 +140,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
exiting={FadeOut}
overflow="hidden">
<Flex justifyContent="center" style={StyleSheet.absoluteFill}>
<Flex
height={Math.max(dimensions.fullHeight, CAMERA_ASPECT_RATIO * dimensions.fullWidth)}
overflow="hidden"
width={dimensions.fullWidth}>
<Flex height={cameraHeight} overflow="hidden" width={cameraWidth}>
{permissionStatus === PermissionStatus.GRANTED && !isReadingImageFile && (
<Camera
barCodeScannerSettings={{
......
......@@ -61,7 +61,6 @@ const getPreloadedState = (props?: PreloadedStateProps): PreloadedState<MobileSt
...(hasInactiveAccounts && { [inactiveAccount.address]: inactiveAccount }),
},
activeAccountAddress: activeAccount.address,
isUnlocked: true,
settings: {
swapProtection: SwapProtectionSetting.On,
hideSmallBalances: false,
......
......@@ -29,6 +29,7 @@ type ModalWithOverlayProps = PropsWithChildren<
scrollDownButtonText?: string
onReject: () => void
onConfirm: () => void
disableConfirm?: boolean
}
>
......@@ -46,6 +47,7 @@ export function ModalWithOverlay({
scrollDownButtonText,
onReject,
onConfirm,
disableConfirm,
...bottomSheetModalProps
}: ModalWithOverlayProps): JSX.Element {
const scrollViewRef = useRef<ScrollView>(null)
......@@ -124,7 +126,7 @@ export function ModalWithOverlay({
<ModalFooter
confirmationButtonText={confirmationButtonText}
confirmationEnabled={confirmationEnabled}
confirmationEnabled={!disableConfirm && confirmationEnabled}
scrollDownButtonText={scrollDownButtonText}
showScrollDownOverlay={showOverlay}
onConfirm={onConfirm}
......
......@@ -3,7 +3,7 @@ import React from 'react'
import { Trans } from 'react-i18next'
import { WalletConnectRequest } from 'src/features/walletConnect/walletConnectSlice'
import { Text } from 'ui/src'
import { EthMethod } from 'wallet/src/features/walletConnect/types'
import { EthMethod, UwULinkMethod } from 'wallet/src/features/walletConnect/types'
import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount'
export function HeaderText({
......@@ -54,13 +54,17 @@ export function HeaderText({
)
}
const getReadableMethodName = (ethMethod: EthMethod, dappNameOrUrl: string): JSX.Element => {
const getReadableMethodName = (
ethMethod: EthMethod | UwULinkMethod,
dappNameOrUrl: string
): JSX.Element => {
switch (ethMethod) {
case EthMethod.PersonalSign:
case EthMethod.EthSign:
case EthMethod.SignTypedData:
return <Trans i18nKey="qrScanner.request.method.signature" values={{ dappNameOrUrl }} />
case EthMethod.EthSendTransaction:
case UwULinkMethod.Erc20Send:
return <Trans i18nKey="qrScanner.request.method.transaction" values={{ dappNameOrUrl }} />
}
......
import { useBottomSheetInternal } from '@gorhom/bottom-sheet'
import { formatUnits } from 'ethers/lib/utils'
import { useTranslation } from 'react-i18next'
import Animated, { useAnimatedStyle } from 'react-native-reanimated'
import { ModalWithOverlay } from 'src/components/WalletConnect/ModalWithOverlay/ModalWithOverlay'
import { UwuLinkErc20Request } from 'src/features/walletConnect/walletConnectSlice'
import { Flex, Text } from 'ui/src'
import { iconSizes } from 'ui/src/theme'
import { CurrencyInfo } from 'uniswap/src/features/dataApi/types'
import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo'
import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader'
import { CHAIN_INFO } from 'wallet/src/constants/chains'
import { useOnChainCurrencyBalance } from 'wallet/src/features/portfolio/api'
import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency'
import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo'
import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks'
import { ModalName } from 'wallet/src/telemetry/constants'
import { buildCurrencyId } from 'wallet/src/utils/currencyId'
type Props = {
onClose: () => void
onConfirm: () => void
onReject: () => void
request: UwuLinkErc20Request
hasSufficientGasFunds: boolean
}
export function UwULinkErc20SendModal({
onClose,
onConfirm,
onReject,
request,
hasSufficientGasFunds,
}: Props): JSX.Element {
const { t } = useTranslation()
const activeAccountAddress = useActiveAccountAddressWithThrow()
// TODO: wallet should determine if the currency is stablecoin
const { chainId, tokenAddress, amount } = request
const currencyInfo = useCurrencyInfo(buildCurrencyId(chainId, tokenAddress))
const { balance } = useOnChainCurrencyBalance(currencyInfo?.currency, activeAccountAddress)
const hasSufficientTokenFunds = !balance?.lessThan(amount)
return (
<ModalWithOverlay
confirmationButtonText={t('common.button.pay')}
disableConfirm={!hasSufficientTokenFunds || !hasSufficientGasFunds}
name={ModalName.UwULinkErc20SendModal}
scrollDownButtonText={t('walletConnect.request.button.scrollDown')}
onClose={onClose}
onConfirm={onConfirm}
onReject={onReject}>
<UwULinkErc20SendModalContent
currencyInfo={currencyInfo}
hasSufficientGasFunds={hasSufficientGasFunds}
hasSufficientTokenFunds={hasSufficientTokenFunds}
loading={!balance || !currencyInfo}
request={request}
/>
</ModalWithOverlay>
)
}
function UwULinkErc20SendModalContent({
request,
loading,
currencyInfo,
hasSufficientGasFunds,
hasSufficientTokenFunds,
}: {
request: UwuLinkErc20Request
loading: boolean
hasSufficientGasFunds: boolean
hasSufficientTokenFunds: boolean
currencyInfo: Maybe<CurrencyInfo>
}): JSX.Element {
const { t } = useTranslation()
const { animatedFooterHeight } = useBottomSheetInternal()
const bottomSpacerStyle = useAnimatedStyle(() => ({
height: animatedFooterHeight.value,
}))
const { chainId, isStablecoin } = request
const nativeCurrency = chainId && NativeCurrency.onChain(chainId)
if (loading || !currencyInfo) {
return (
<Flex centered py="$spacing12">
<SpinningLoader color="$accent1" size={iconSizes.icon64} />
<Animated.View style={bottomSpacerStyle} />
</Flex>
)
}
const {
logoUrl,
currency: { name, symbol, decimals },
} = currencyInfo
return (
<Flex centered gap="$spacing12" justifyContent="space-between">
<Text variant="subheading1">{request.recipient.name}</Text>
<Flex centered flex={1} gap="$spacing12" py="$spacing16">
{!hasSufficientTokenFunds && (
<Text color="red">
{t('uwulink.error.insufficientTokens', {
tokenSymbol: symbol,
chain: CHAIN_INFO[chainId].label,
})}
</Text>
)}
<Text fontSize={64} my="$spacing4" pt={42}>{`${isStablecoin ? '$' : ''}${formatUnits(
request.amount,
decimals
)}`}</Text>
<Flex row gap="$spacing4">
<TokenLogo
chainId={chainId}
name={name}
size={iconSizes.icon24}
symbol={symbol}
url={logoUrl}
/>
<Text>{symbol}</Text>
</Flex>
</Flex>
{!hasSufficientGasFunds && (
<Text color="$DEP_accentWarning" pt="$spacing8" textAlign="center" variant="body3">
{t('walletConnect.request.error.insufficientFunds', {
currencySymbol: nativeCurrency?.symbol,
})}
</Text>
)}
<Animated.View style={bottomSpacerStyle} />
</Flex>
)
}
......@@ -18,21 +18,26 @@ import { wcWeb3Wallet } from 'src/features/walletConnect/saga'
import { selectDidOpenFromDeepLink } from 'src/features/walletConnect/selectors'
import { signWcRequestActions } from 'src/features/walletConnect/signWcRequestSaga'
import {
SignRequest,
TransactionRequest,
WalletConnectRequest,
isTransactionRequest,
} from 'src/features/walletConnect/walletConnectSlice'
import { useTransactionGasFee } from 'wallet/src/features/gas/hooks'
import { GasSpeed } from 'wallet/src/features/gas/types'
import { useIsBlocked, useIsBlockedActiveAddress } from 'wallet/src/features/trm/hooks'
import { useSignerAccounts } from 'wallet/src/features/wallet/hooks'
import { EthMethod, WCEventType, WCRequestOutcome } from 'wallet/src/features/walletConnect/types'
import {
EthMethod,
UwULinkMethod,
WCEventType,
WCRequestOutcome,
} from 'wallet/src/features/walletConnect/types'
import { ModalName } from 'wallet/src/telemetry/constants'
import { areAddressesEqual } from 'wallet/src/utils/addresses'
import { UwULinkErc20SendModal } from './UwULinkErc20SendModal'
interface Props {
onClose: () => void
request: SignRequest | TransactionRequest
request: WalletConnectRequest
}
const VALID_REQUEST_TYPES = [
......@@ -41,6 +46,7 @@ const VALID_REQUEST_TYPES = [
EthMethod.SignTypedDataV4,
EthMethod.EthSign,
EthMethod.EthSendTransaction,
UwULinkMethod.Erc20Send,
]
export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Element | null {
......@@ -62,6 +68,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
areAddressesEqual(account.address, request.account)
)
const gasFee = useTransactionGasFee(tx, GasSpeed.Urgent)
const hasSufficientFunds = useHasSufficientFunds({
account: request.account,
chainId,
......@@ -145,7 +152,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
if (!confirmEnabled || !signerAccount) {
return
}
if (request.type === EthMethod.EthSendTransaction) {
if (request.type === EthMethod.EthSendTransaction || request.type === UwULinkMethod.Erc20Send) {
if (!gasFee.params) {
return
} // appeasing typescript
......@@ -153,7 +160,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
signWcRequestActions.trigger({
sessionId: request.sessionId,
requestInternalId: request.internalId,
method: request.type,
method: EthMethod.EthSendTransaction,
transaction: { ...tx, ...gasFee.params },
account: signerAccount,
dapp: request.dapp,
......@@ -198,6 +205,14 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
const { trigger: actionButtonTrigger } = useBiometricPrompt(onConfirm)
const { requiredForTransactions } = useBiometricAppSettings()
const onConfirmPress = async (): Promise<void> => {
if (requiredForTransactions) {
await actionButtonTrigger()
} else {
await onConfirm()
}
}
if (!VALID_REQUEST_TYPES.includes(request.type)) {
return null
}
......@@ -210,6 +225,18 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
}
}
if (request.type === UwULinkMethod.Erc20Send) {
return (
<UwULinkErc20SendModal
hasSufficientGasFunds={hasSufficientFunds}
request={request}
onClose={handleClose}
onConfirm={onConfirmPress}
onReject={onReject}
/>
)
}
return (
<ModalWithOverlay
confirmationButtonText={
......@@ -217,16 +244,11 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
? t('common.button.accept')
: t('walletConnect.request.button.sign')
}
disableConfirm={!hasSufficientFunds || Boolean(gasFee.error)}
name={ModalName.WCSignRequest}
scrollDownButtonText={t('walletConnect.request.button.scrollDown')}
onClose={handleClose}
onConfirm={async (): Promise<void> => {
if (requiredForTransactions) {
await actionButtonTrigger()
} else {
await onConfirm()
}
}}
onConfirm={onConfirmPress}
onReject={onReject}>
<WalletConnectRequestModalContent
gasFee={gasFee}
......
......@@ -9,6 +9,7 @@ import Animated, { useAnimatedStyle } from 'react-native-reanimated'
import { ClientDetails, PermitInfo } from 'src/components/WalletConnect/RequestModal/ClientDetails'
import { RequestDetails } from 'src/components/WalletConnect/RequestModal/RequestDetails'
import {
SignRequest,
TransactionRequest,
WalletConnectRequest,
isTransactionRequest,
......@@ -52,7 +53,9 @@ const getPermitInfo = (request: WalletConnectRequest): PermitInfo | undefined =>
return { currencyId, amount }
} catch (error) {
logger.error(error, { tags: { file: 'WalletConnectRequestModal', function: 'getPermitInfo' } })
logger.error(error, {
tags: { file: 'WalletConnectRequestModal', function: 'getPermitInfo' },
})
return undefined
}
}
......@@ -60,7 +63,7 @@ const getPermitInfo = (request: WalletConnectRequest): PermitInfo | undefined =>
type WalletConnectRequestModalContentProps = {
gasFee: GasFeeResult
hasSufficientFunds: boolean
request: WalletConnectRequest
request: SignRequest | TransactionRequest
isBlocked: boolean
}
......
import React from 'react'
import { Flex, Separator, Text, Unicon, UniconV2, useSporeColors } from 'ui/src'
import Check from 'ui/src/assets/icons/check.svg'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlag } from 'uniswap/src/features/statsig/hooks'
import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText'
import { Account } from 'wallet/src/features/wallet/accounts/types'
import { useDisplayName } from 'wallet/src/features/wallet/hooks'
......
......@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Alert } from 'react-native'
import 'react-native-reanimated'
import { useAppDispatch, useAppSelector } from 'src/app/hooks'
import { useAppDispatch } from 'src/app/hooks'
import { useEagerExternalProfileRootNavigation } from 'src/app/navigation/hooks'
import { QRCodeScanner } from 'src/components/QRCodeScanner/QRCodeScanner'
import Trace from 'src/components/Trace/Trace'
......@@ -10,8 +10,10 @@ import { ConnectedDappsList } from 'src/components/WalletConnect/ConnectedDapps/
import {
URIType,
UWULINK_PREFIX,
findAllowedTokenRecipient,
getSupportedURI,
isAllowedUwuLinkRequest,
toTokenTransferRequest,
useUwuLinkContractAllowlist,
} from 'src/components/WalletConnect/ScanSheet/util'
import { BackButtonView } from 'src/components/layout/BackButtonView'
......@@ -23,14 +25,15 @@ import { Flex, HapticFeedback, Text, TouchableArea, useIsDarkMode, useSporeColor
import Scan from 'ui/src/assets/icons/receive.svg'
import ScanQRIcon from 'ui/src/assets/icons/scan.svg'
import { iconSizes } from 'ui/src/theme'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlag } from 'uniswap/src/features/statsig/hooks'
import { logger } from 'utilities/src/logger/logger'
import { WalletQRCode } from 'wallet/src/components/QRCodeScanner/WalletQRCode'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors'
import { EthMethod, UwULinkRequest } from 'wallet/src/features/walletConnect/types'
import { useContractManager, useProviderManager } from 'wallet/src/features/wallet/context'
import { useActiveAccount } from 'wallet/src/features/wallet/hooks'
import { EthMethod, UwULinkMethod, UwULinkRequest } from 'wallet/src/features/walletConnect/types'
import { ElementName, ModalName } from 'wallet/src/telemetry/constants'
type Props = {
......@@ -45,8 +48,8 @@ export function WalletConnectModal({
const { t } = useTranslation()
const colors = useSporeColors()
const isDarkMode = useIsDarkMode()
const activeAddress = useAppSelector(selectActiveAccountAddress)
const { sessions, hasPendingSessionError } = useWalletConnect(activeAddress)
const activeAccount = useActiveAccount()
const { sessions, hasPendingSessionError } = useWalletConnect(activeAccount?.address)
const [currentScreenState, setCurrentScreenState] =
useState<ScannerModalState>(initialScreenState)
const [shouldFreezeCamera, setShouldFreezeCamera] = useState(false)
......@@ -57,6 +60,9 @@ export function WalletConnectModal({
const uwuLinkContractAllowlist = useUwuLinkContractAllowlist()
const providerManager = useProviderManager()
const contractManager = useContractManager()
// Update QR scanner states when pending session error alert is shown from WCv2 saga event channel
useEffect(() => {
if (hasPendingSessionError) {
......@@ -68,7 +74,7 @@ export function WalletConnectModal({
const onScanCode = useCallback(
async (uri: string) => {
// don't scan any QR codes if there is an error popup open or camera is frozen
if (!activeAddress || hasPendingSessionError || shouldFreezeCamera) {
if (!activeAccount || hasPendingSessionError || shouldFreezeCamera) {
return
}
await HapticFeedback.selection()
......@@ -149,7 +155,6 @@ export function WalletConnectModal({
try {
const parsedUwulinkRequest: UwULinkRequest = JSON.parse(supportedURI.value)
const isAllowed = isAllowedUwuLinkRequest(parsedUwulinkRequest, uwuLinkContractAllowlist)
if (!isAllowed) {
Alert.alert(
t('walletConnect.error.uwu.title'),
......@@ -166,25 +171,62 @@ export function WalletConnectModal({
return
}
dispatch(
addRequest({
account: activeAddress,
request: {
type: EthMethod.EthSendTransaction,
transaction: { from: activeAddress, ...parsedUwulinkRequest.value },
sessionId: UWULINK_PREFIX, // session/internalId is WalletConnect specific, but not needed here
internalId: UWULINK_PREFIX,
account: activeAddress,
dapp: {
...parsedUwulinkRequest.dapp,
source: UWULINK_PREFIX,
chain_id: parsedUwulinkRequest.chainId,
webhook: parsedUwulinkRequest.webhook,
const newRequest = {
sessionId: UWULINK_PREFIX, // session/internalId is WalletConnect specific, but not needed here
internalId: UWULINK_PREFIX,
account: activeAccount?.address,
dapp: {
name: '',
url: '',
...parsedUwulinkRequest.dapp,
source: UWULINK_PREFIX,
chain_id: parsedUwulinkRequest.chainId,
webhook: parsedUwulinkRequest.webhook,
},
chainId: parsedUwulinkRequest.chainId,
}
if (parsedUwulinkRequest.method === UwULinkMethod.Erc20Send) {
const preparedTransaction = await toTokenTransferRequest(
parsedUwulinkRequest,
activeAccount,
providerManager,
contractManager
)
const tokenRecipient = findAllowedTokenRecipient(
parsedUwulinkRequest,
uwuLinkContractAllowlist
)
dispatch(
addRequest({
account: activeAccount.address,
request: {
...newRequest,
type: UwULinkMethod.Erc20Send,
recipient: {
address: parsedUwulinkRequest.recipient,
name: tokenRecipient?.name ?? '',
},
amount: parsedUwulinkRequest.amount,
tokenAddress: parsedUwulinkRequest.tokenAddress,
isStablecoin: parsedUwulinkRequest.isStablecoin,
transaction: { from: activeAccount.address, ...preparedTransaction },
},
chainId: parsedUwulinkRequest.chainId,
},
})
)
})
)
} else {
dispatch(
addRequest({
account: activeAccount.address,
request: {
...newRequest,
type: EthMethod.EthSendTransaction,
transaction: { from: activeAccount.address, ...parsedUwulinkRequest.value },
},
})
)
}
onClose()
} catch (_) {
setShouldFreezeCamera(false)
......@@ -206,7 +248,7 @@ export function WalletConnectModal({
}
},
[
activeAddress,
activeAccount,
hasPendingSessionError,
shouldFreezeCamera,
isUwULinkEnabled,
......@@ -217,6 +259,8 @@ export function WalletConnectModal({
onClose,
dispatch,
uwuLinkContractAllowlist,
providerManager,
contractManager,
]
)
......@@ -236,7 +280,7 @@ export function WalletConnectModal({
setCurrentScreenState(ScannerModalState.ScanQr)
}
if (!activeAddress) {
if (!activeAccount) {
return null
}
......@@ -269,7 +313,7 @@ export function WalletConnectModal({
)}
{currentScreenState === ScannerModalState.WalletQr && (
<Trace logImpression element={ElementName.WalletQRCode}>
<WalletQRCode address={activeAddress} />
<WalletQRCode address={activeAccount.address} />
</Trace>
)}
<Flex centered mb="$spacing12" mt="$spacing16" mx="$spacing16">
......
......@@ -6,12 +6,24 @@ import {
UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM,
UNISWAP_WALLETCONNECT_URL,
} from 'src/features/deepLinking/constants'
import { DynamicConfigs } from 'uniswap/src/features/experiments/configs'
import { useDynamicConfig } from 'uniswap/src/features/experiments/hooks'
import { DynamicConfigs } from 'uniswap/src/features/statsig/configs'
import { useDynamicConfig } from 'uniswap/src/features/statsig/hooks'
import { logger } from 'utilities/src/logger/logger'
import { RPCType } from 'wallet/src/constants/chains'
import { AssetType } from 'wallet/src/entities/assets'
import { ContractManager } from 'wallet/src/features/contracts/ContractManager'
import { ProviderManager } from 'wallet/src/features/providers'
import { ScantasticParams, ScantasticParamsSchema } from 'wallet/src/features/scantastic/types'
import { UwULinkRequest } from 'wallet/src/features/walletConnect/types'
import { getValidAddress } from 'wallet/src/utils/addresses'
import { getTokenTransferRequest } from 'wallet/src/features/transactions/transfer/hooks/useTransferTransactionRequest'
import { TransferCurrencyParams } from 'wallet/src/features/transactions/transfer/types'
import { Account } from 'wallet/src/features/wallet/accounts/types'
import {
EthTransaction,
UwULinkErc20SendRequest,
UwULinkMethod,
UwULinkRequest,
} from 'wallet/src/features/walletConnect/types'
import { areAddressesEqual, getValidAddress } from 'wallet/src/utils/addresses'
export enum URIType {
WalletConnectURL = 'walletconnect',
......@@ -34,19 +46,23 @@ interface EnabledFeatureFlags {
// This type must match the format in statsig dynamic config for uwulink
// https://console.statsig.com/5HjUux4OvSGzgqWIfKFt8i/dynamic_configs/uwulink_config
type UwuLinkAllowlistItem = {
type UwULinkAllowlistItem = {
chainId: number
contractAddress: string
address: string
name: string
icon?: string
}
type UwuLinkAllowlist = UwuLinkAllowlistItem[]
type UwULinkAllowlist = {
contracts: UwULinkAllowlistItem[]
tokenRecipients: UwULinkAllowlistItem[]
}
const UWULINK_MAX_TXN_VALUE = '0.001'
const EASTER_EGG_QR_CODE = 'DO_NOT_SCAN_OR_ELSE_YOU_WILL_GO_TO_MOBILE_TEAM_JAIL'
export const CUSTOM_UNI_QR_CODE_PREFIX = 'hello_uniwallet:'
export const UWULINK_PREFIX = 'uwulink'
export const UWULINK_PREFIX = 'uwulink' as const
export const truncateQueryParams = (url: string): string => {
// In fact, the first element will be always returned below. url is
......@@ -102,8 +118,9 @@ export async function getSupportedURI(
return { type: URIType.EasterEgg, value: uri }
}
if (enabledFeatureFlags?.isUwULinkEnabled && isUwULink(uri)) {
return { type: URIType.UwULink, value: uri.slice(UWULINK_PREFIX.length) }
if (isUwULink(uri)) {
// remove escape strings from the stringified JSON before parsing it
return { type: URIType.UwULink, value: uri.slice(UWULINK_PREFIX.length).replaceAll('\\', '') }
}
}
......@@ -132,11 +149,24 @@ function isUwULink(uri: string): boolean {
// Gets the UWULink contract allow list from statsig dynamic config.
// We can safely cast as long as the statsig config format matches our `UwuLinkAllowlist` type.
export function useUwuLinkContractAllowlist(): UwuLinkAllowlist {
export function useUwuLinkContractAllowlist(): UwULinkAllowlist {
const uwuLinkConfig = useDynamicConfig(DynamicConfigs.UwuLink)
return uwuLinkConfig.getValue('allowlist') as UwuLinkAllowlist
return uwuLinkConfig.getValue('allowlist') as UwULinkAllowlist
}
export function findAllowedTokenRecipient(
request: UwULinkRequest,
allowlist: UwULinkAllowlist
): UwULinkAllowlistItem | undefined {
if (request.method !== UwULinkMethod.Erc20Send) {
return
}
const { chainId, recipient } = request
return allowlist.tokenRecipients.find(
(item) => item.chainId === chainId && areAddressesEqual(item.address, recipient)
)
}
/**
* Util function to check if a UwULinkRequest is valid.
*
......@@ -144,17 +174,26 @@ export function useUwuLinkContractAllowlist(): UwuLinkAllowlist {
* 1. The to address is in the UWULINK_CONTRACT_ALLOWLIST
* 2. The value is less than or equal to UWULINK_MAX_TXN_VALUE
*
* TODO: also check for validity of the entire request object (e.g. all the required fields exist)
*
* @param request parsed UwULinkRequest
* @returns boolean for whether the UwULinkRequest is allowed
*/
export function isAllowedUwuLinkRequest(
request: UwULinkRequest,
allowList: UwuLinkAllowlist
allowlist: UwULinkAllowlist
): boolean {
// token sends
if (request.method === UwULinkMethod.Erc20Send) {
return Boolean(findAllowedTokenRecipient(request, allowlist))
}
// generic transactions
const { to, value } = request.value
const belowMaximumValue =
value && parseFloat(value) <= parseEther(UWULINK_MAX_TXN_VALUE).toNumber()
const isAllowedContractAddress = to && allowList.some((item) => item.contractAddress === to)
const isAllowedContractAddress =
to && allowlist.contracts.some((item) => areAddressesEqual(item.address, to))
if (!belowMaximumValue || !isAllowedContractAddress) {
return false
......@@ -238,3 +277,22 @@ export function parseScantasticParams(uri: string): ScantasticParams | undefined
})
}
}
export async function toTokenTransferRequest(
request: UwULinkErc20SendRequest,
account: Account,
providerManager: ProviderManager,
contractManager: ContractManager
): Promise<EthTransaction> {
const provider = providerManager.getProvider(request.chainId, RPCType.Public)
const params: TransferCurrencyParams = {
type: AssetType.Currency,
account,
chainId: request.chainId,
toAddress: request.recipient,
tokenAddress: request.tokenAddress,
amountInWei: request.amount.toString(),
}
const transaction = await getTokenTransferRequest(params, provider, contractManager)
return transaction as EthTransaction
}
......@@ -10,8 +10,8 @@ import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHe
import { AnimatedFlex, Flex, Icons, Text, TouchableArea, useSporeColors } from 'ui/src'
import ClockIcon from 'ui/src/assets/icons/clock.svg'
import { iconSizes } from 'ui/src/theme'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlag } from 'uniswap/src/features/statsig/hooks'
import { clearSearchHistory } from 'wallet/src/features/search/searchHistorySlice'
import {
SearchResult,
......
import React from 'react'
import { useTranslation } from 'react-i18next'
import Trace from 'src/components/Trace/Trace'
import { MobileEventName } from 'src/features/telemetry/constants'
import { Button, Icons } from 'ui/src'
import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader'
import { ElementName } from 'wallet/src/telemetry/constants'
interface FiatOnRampCtaButtonProps {
onPress: () => void
isLoading?: boolean
eligible: boolean
disabled: boolean
analyticsProperties?: Record<string, unknown>
continueButtonText: string
}
......@@ -20,35 +16,28 @@ export function FiatOnRampCtaButton({
isLoading,
eligible,
disabled,
analyticsProperties,
onPress,
}: FiatOnRampCtaButtonProps): JSX.Element {
const { t } = useTranslation()
const buttonAvailable = eligible || isLoading
const continueText = eligible ? continueButtonText : t('fiatOnRamp.error.unsupported')
return (
<Trace
logPress
element={ElementName.FiatOnRampWidgetButton}
pressEvent={MobileEventName.FiatOnRampWidgetOpened}
properties={analyticsProperties}>
<Button
// TODO: remove when https://linear.app/uniswap/issue/MOB-3182/disabled-ui-on-enabled-button is fixed
key={Math.random()}
color={buttonAvailable ? '$white' : '$neutral2'}
disabled={disabled}
icon={
isLoading ? (
<SpinningLoader color="$sporeWhite" />
) : !eligible ? (
<Icons.InfoCircleFilled color="$neutral3" />
) : undefined
}
size="large"
theme={buttonAvailable ? 'primary' : 'tertiary'}
onPress={onPress}>
{!isLoading && continueText}
</Button>
</Trace>
<Button
// TODO: remove when https://linear.app/uniswap/issue/MOB-3182/disabled-ui-on-enabled-button is fixed
key={Math.random()}
color={buttonAvailable ? '$white' : '$neutral2'}
disabled={disabled}
icon={
isLoading ? (
<SpinningLoader color="$sporeWhite" />
) : !eligible ? (
<Icons.InfoCircleFilled color="$neutral3" />
) : undefined
}
size="large"
theme={buttonAvailable ? 'primary' : 'tertiary'}
onPress={onPress}>
{!isLoading && continueText}
</Button>
)
}
......@@ -5,8 +5,8 @@ import { SeedPhraseDisplay } from 'src/components/mnemonic/SeedPhraseDisplay'
import { APP_STORE_LINK } from 'src/constants/urls'
import { UpgradeStatus } from 'src/features/forceUpgrade/types'
import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src'
import { DynamicConfigs } from 'uniswap/src/features/experiments/configs'
import { useDynamicConfig } from 'uniswap/src/features/experiments/hooks'
import { DynamicConfigs } from 'uniswap/src/features/statsig/configs'
import { useDynamicConfig } from 'uniswap/src/features/statsig/hooks'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal'
import { WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types'
......
......@@ -2,13 +2,13 @@ import { FlashList } from '@shopify/flash-list'
import React, { forwardRef, memo, useCallback, useMemo } from 'react'
import { RefreshControl } from 'react-native'
import { useAppStackNavigation } from 'src/app/navigation/types'
import { NftView } from 'src/components/NFT/NftView'
import { useAdaptiveFooter } from 'src/components/home/hooks'
import { TAB_BAR_HEIGHT, TabProps } from 'src/components/layout/TabHelpers'
import { Screens } from 'src/screens/Screens'
import { Flex, useDeviceInsets, useSporeColors } from 'ui/src'
import { GQLQueries } from 'uniswap/src/data/graphql/uniswap-data-api/queries'
import { isAndroid } from 'uniswap/src/utils/platform'
import { NftView } from 'wallet/src/components/nfts/NftView'
import { NftsList } from 'wallet/src/components/nfts/NftsList'
import { NFTItem } from 'wallet/src/features/nfts/types'
......
......@@ -6,8 +6,8 @@ import { openModal } from 'src/features/modals/modalSlice'
import { Flex, Icons, Text, TouchableArea } from 'ui/src'
import PaperStackIcon from 'ui/src/assets/icons/paper-stack.svg'
import { iconSizes, colors as rawColors } from 'ui/src/theme'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlag } from 'uniswap/src/features/statsig/hooks'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
import { AccountType } from 'wallet/src/features/wallet/accounts/types'
import { useActiveAccount } from 'wallet/src/features/wallet/hooks'
......
import { NftView } from 'src/components/NFT/NftView'
import { useDeviceInsets, useSporeColors } from 'ui/src'
import { spacing } from 'ui/src/theme'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { NftView } from 'wallet/src/components/nfts/NftView'
import { NftsList } from 'wallet/src/components/nfts/NftsList'
import { NFTItem } from 'wallet/src/features/nfts/types'
import { ModalName } from 'wallet/src/telemetry/constants'
......
......@@ -8,8 +8,8 @@ import { sendMobileAnalyticsEvent } from 'src/features/telemetry'
import { MobileEventName } from 'src/features/telemetry/constants'
import { selectCustomEndpoint } from 'src/features/tweaks/selectors'
import { uniswapUrls } from 'uniswap/src/constants/urls'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlag } from 'uniswap/src/features/statsig/hooks'
import { isNonJestDev } from 'utilities/src/environment'
import { logger } from 'utilities/src/logger/logger'
import { useAsyncData } from 'utilities/src/react/hooks'
......
# Universal Links
Universal links allow 3rd parties to prompt the app to open to specific screens when it is installed on their device. If the app isn't installed it will open that page in Safari (a 404 on uniswap.org in this case). All universal links must use the the prefix `https://uniswap.org/app`.
Universal links allow 3rd parties to prompt the app to open to specific screens when it is installed on their device. If the app isn't installed it will open that page in Safari (a 404 on uniswap.org in this case). All universal links must use the prefix `https://uniswap.org/app`.
## Supported Screens
......
......@@ -27,7 +27,7 @@ import { Screens } from 'src/screens/Screens'
import { Statsig } from 'statsig-react-native'
import { call, put, takeLatest } from 'typed-redux-saga'
import { UNISWAP_APP_HOSTNAME } from 'uniswap/src/constants/urls'
import { FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/experiments/flags'
import { FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/statsig/flags'
import i18n from 'uniswap/src/i18n/i18n'
import { logger } from 'utilities/src/logger/logger'
import { selectExtensionOnboardingState } from 'wallet/src/features/behaviorHistory/selectors'
......
......@@ -27,8 +27,8 @@ import {
} from 'ui/src'
import { ENS_LOGO } from 'ui/src/assets'
import { iconSizes, imageSizes } from 'ui/src/theme'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlag } from 'uniswap/src/features/statsig/hooks'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { useENSDescription, useENSName, useENSTwitterUsername } from 'wallet/src/features/ens/api'
import { selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors'
......
......@@ -131,6 +131,14 @@ function FiatOnRampContent({ onClose }: { onClose: () => void }): JSX.Element {
useTimeout(
async () => {
if (fiatOnRampHostUrl) {
if (currency?.moonpayCurrencyCode) {
sendWalletAnalyticsEvent(FiatOnRampEventName.FiatOnRampWidgetOpened, {
externalTransactionId,
serviceProvider: 'MOONPAY',
fiatCurrency: moonpaySupportedFiatCurrency.code.toLowerCase(),
cryptoCurrency: currency.moonpayCurrencyCode.toLowerCase(),
})
}
await openUri(fiatOnRampHostUrl)
dispatchAddTransaction()
onClose()
......@@ -241,7 +249,6 @@ function FiatOnRampContent({ onClose }: { onClose: () => void }): JSX.Element {
/>
)}
<FiatOnRampCtaButton
analyticsProperties={{ externalTransactionId }}
continueButtonText={
appFiatCurrencySupportedInMoonpay
? t('fiatOnRamp.button.continueCheckout')
......
......@@ -4,8 +4,8 @@ import { OnboardingStackBaseParams, useOnboardingStackNavigation } from 'src/app
import { sendMobileAnalyticsEvent } from 'src/features/telemetry'
import { MobileEventName } from 'src/features/telemetry/constants'
import { Screens } from 'src/screens/Screens'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlag } from 'uniswap/src/features/statsig/hooks'
import { useTrace } from 'utilities/src/telemetry/trace/TraceContext'
import {
setHasSkippedUnitagPrompt,
......
......@@ -4,7 +4,12 @@ import { MobileEventName } from 'src/features/telemetry/constants'
import { WidgetEvent, WidgetType } from 'src/features/widgets/widgets'
import { TraceProps } from 'utilities/src/telemetry/trace/Trace'
import { ImportType } from 'wallet/src/features/onboarding/types'
import { EthMethod, WCEventType, WCRequestOutcome } from 'wallet/src/features/walletConnect/types'
import {
EthMethod,
UwULinkMethod,
WCEventType,
WCRequestOutcome,
} from 'wallet/src/features/walletConnect/types'
import { ShareableEntity } from 'wallet/src/telemetry/constants'
// Events related to Moonpay internal transactions
......@@ -86,7 +91,7 @@ export type MobileEventProperties = {
[MobileEventName.WalletAdded]: OnboardingCompletedProps & TraceProps
[MobileEventName.WalletConnectSheetCompleted]: {
request_type: WCEventType
eth_method?: EthMethod
eth_method?: EthMethod | UwULinkMethod
dapp_url: string
dapp_name: string
wc_version: string
......
......@@ -28,7 +28,7 @@ import { useParsedSendWarnings } from 'wallet/src/features/transactions/hooks/us
import { useTokenSelectorActionHandlers } from 'wallet/src/features/transactions/hooks/useTokenSelectorActionHandlers'
import { useTransactionGasWarning } from 'wallet/src/features/transactions/hooks/useTransactionGasWarning'
import {
initialState as emptyState,
INITIAL_TRANSACTION_STATE,
transactionStateReducer,
} from 'wallet/src/features/transactions/transactionState/transactionState'
import {
......@@ -67,7 +67,10 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS
const { fullWidth } = useDeviceDimensions()
const { isSheetReady } = useBottomSheetContext()
const [state, dispatch] = useReducer(transactionStateReducer, prefilledState || emptyState)
const [state, dispatch] = useReducer(
transactionStateReducer,
prefilledState || INITIAL_TRANSACTION_STATE
)
const derivedTransferInfo = useDerivedTransferInfo(state)
const [showViewOnlyModal, setShowViewOnlyModal] = useState(false)
const [step, setStep] = useState<TransactionStep>(TransactionStep.FORM)
......@@ -89,9 +92,8 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS
)
const transferTxWithGasSettings = useMemo(
(): providers.TransactionRequest | undefined =>
gasFee?.params ? { ...txRequest, ...gasFee.params } : txRequest,
[gasFee?.params, txRequest]
(): providers.TransactionRequest => ({ ...txRequest, ...gasFee.params }),
[gasFee.params, txRequest]
)
const gasWarning = useTransactionGasWarning({
......
......@@ -297,7 +297,7 @@ export function ClaimUnitagScreen({ navigation, route }: Props): JSX.Element {
borderWidth={0}
fontFamily="$heading"
fontSize={fontSize}
fontWeight="$large"
fontWeight="$medium"
numberOfLines={1}
p="$none"
placeholder={inputPlaceholder}
......
......@@ -28,8 +28,8 @@ import {
} from 'ui/src'
import { borderRadii, fonts, iconSizes, imageSizes, spacing } from 'ui/src/theme'
import { useExtractedColors } from 'ui/src/utils/colors'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlag } from 'uniswap/src/features/statsig/hooks'
import { useUnitagUpdater } from 'uniswap/src/features/unitags/context'
import { ProfileMetadata } from 'uniswap/src/features/unitags/types'
import { isIOS } from 'uniswap/src/utils/platform'
......
......@@ -2,8 +2,8 @@ import { useCallback, useEffect, useState } from 'react'
import { useAppSelector } from 'src/app/hooks'
import { openModal } from 'src/features/modals/modalSlice'
import { selectModalState } from 'src/features/modals/selectModalState'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlag } from 'uniswap/src/features/statsig/hooks'
import { logger } from 'utilities/src/logger/logger'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
import { useNativeAccountExists } from 'wallet/src/features/wallet/hooks'
......
......@@ -82,7 +82,7 @@ export function* signWcRequest(params: SignMessageParams | SignTransactionParams
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ method: 'eth_sendTransaction', response: signature }),
body: JSON.stringify({ method: 'eth_sendTransaction', response: signature, chainId }),
// TODO: consider adding analytics to track UwuLink usage
}).catch((error) =>
logger.error(error, {
......@@ -107,7 +107,7 @@ export function* signWcRequest(params: SignMessageParams | SignTransactionParams
type: AppNotificationType.WalletConnect,
event: WalletConnectEvent.TransactionFailed,
dappName: params.dapp.name,
imageUrl: params.dapp.icon,
imageUrl: params.dapp.icon ?? null,
chainId,
address: account.address,
})
......
......@@ -6,6 +6,7 @@ import {
EthMethod,
EthSignMethod,
EthTransaction,
UwULinkMethod,
} from 'wallet/src/features/walletConnect/types'
export type WalletConnectPendingSession = {
......@@ -45,11 +46,24 @@ export interface TransactionRequest extends BaseRequest {
transaction: EthTransaction
}
export type WalletConnectRequest = SignRequest | TransactionRequest
export interface UwuLinkErc20Request extends BaseRequest {
type: UwULinkMethod.Erc20Send
recipient: {
address: string
name: string
}
tokenAddress: string
amount: string
isStablecoin: boolean
transaction: EthTransaction // the formatted transaction, prepared by the wallet
}
export type WalletConnectRequest = SignRequest | TransactionRequest | UwuLinkErc20Request
export const isTransactionRequest = (
request: WalletConnectRequest
): request is TransactionRequest => request.type === EthMethod.EthSendTransaction
): request is TransactionRequest =>
request.type === EthMethod.EthSendTransaction || request.type === UwULinkMethod.Erc20Send
export interface WalletConnectState {
byAccount: {
......
......@@ -8,7 +8,7 @@
// Import the crypto getRandomValues shim BEFORE ethers shims
import 'react-native-get-random-values'
// Import the the ethers shims BEFORE ethers
// Import the ethers shims BEFORE ethers
import '@ethersproject/shims'
// Add .at() method to Array if necessary (missing before iOS 15)
import 'src/polyfills/arrayAt'
......
......@@ -120,7 +120,7 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element |
sendWalletAnalyticsEvent(FiatOnRampEventName.FiatOnRampWidgetOpened, {
externalTransactionId,
serviceProvider: serviceProvider.serviceProvider,
preselectedServiceProvider: serviceProvider.serviceProvider,
preselectedServiceProvider: quotesSections?.[0]?.data?.[0]?.serviceProvider,
countryCode,
countryState,
fiatCurrency: baseCurrencyInfo?.code.toLowerCase(),
......
......@@ -67,8 +67,8 @@ import BuyIcon from 'ui/src/assets/icons/buy.svg'
import ScanIcon from 'ui/src/assets/icons/scan-home.svg'
import SendIcon from 'ui/src/assets/icons/send-action.svg'
import { iconSizes, spacing } from 'ui/src/theme'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlag } from 'uniswap/src/features/statsig/hooks'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { useInterval, useTimeout } from 'utilities/src/time/timing'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
......
......@@ -11,8 +11,8 @@ import { OnboardingScreens } from 'src/screens/Screens'
import { useAddBackButton } from 'src/utils/useAddBackButton'
import { Flex, Icons, Text, TouchableArea, Unicon, UniconV2, useIsDarkMode } from 'ui/src'
import { iconSizes } from 'ui/src/theme'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlag } from 'uniswap/src/features/statsig/hooks'
import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName'
import {
FORMAT_DATE_TIME_SHORT,
......
......@@ -13,7 +13,6 @@ import { Loader } from 'src/components/loading'
import { LongMarkdownText } from 'src/components/text/LongMarkdownText'
import { selectModalState } from 'src/features/modals/selectModalState'
import { PriceAmount } from 'src/features/nfts/collection/ListPriceCard'
import { useNFTMenu } from 'src/features/nfts/hooks'
import { BlurredImageBackground } from 'src/features/nfts/item/BlurredImageBackground'
import { CollectionPreviewCard } from 'src/features/nfts/item/CollectionPreviewCard'
import { NFTTraitList } from 'src/features/nfts/item/traits'
......@@ -43,6 +42,7 @@ import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { PollingInterval } from 'wallet/src/constants/misc'
import { NFTViewer } from 'wallet/src/features/images/NFTViewer'
import { GQLNftAsset } from 'wallet/src/features/nfts/hooks'
import { useNFTContextMenu } from 'wallet/src/features/nfts/useNftContextMenu'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types'
import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks'
......@@ -422,7 +422,7 @@ function RightElement({
}): JSX.Element {
const colors = useSporeColors()
const { menuActions, onContextMenuPress, onlyShare } = useNFTMenu({
const { menuActions, onContextMenuPress, onlyShare } = useNFTContextMenu({
contractAddress: asset?.nftContract?.address,
tokenId: asset?.tokenId,
owner,
......
......@@ -12,8 +12,8 @@ import { OnboardingScreens, UnitagScreens } from 'src/screens/Screens'
import { hideSplashScreen } from 'src/utils/splashScreen'
import { isDevBuild } from 'src/utils/version'
import { Button, Flex, HapticFeedback, Text, TouchableArea, useIsDarkMode } from 'ui/src'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlag } from 'uniswap/src/features/statsig/hooks'
import { useTimeout } from 'utilities/src/time/timing'
import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types'
import { useCanAddressClaimUnitag } from 'wallet/src/features/unitags/hooks'
......
......@@ -51,8 +51,8 @@ import { ONBOARDING_QR_ETCHING_VIDEO_DARK, ONBOARDING_QR_ETCHING_VIDEO_LIGHT } f
import LockIcon from 'ui/src/assets/icons/lock.svg'
import { AnimatedFlex, flexStyles } from 'ui/src/components/layout'
import { fonts, iconSizes, opacify, spacing } from 'ui/src/theme'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlag } from 'uniswap/src/features/statsig/hooks'
import { QRCodeDisplay } from 'wallet/src/components/QRCodeScanner/QRCode'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { Arrow } from 'wallet/src/components/icons/Arrow'
......
......@@ -47,8 +47,8 @@ import MessageQuestion from 'ui/src/assets/icons/message-question.svg'
import UniswapIcon from 'ui/src/assets/icons/uniswap-logo.svg'
import { iconSizes, spacing } from 'ui/src/theme'
import { uniswapUrls } from 'uniswap/src/constants/urls'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlag } from 'uniswap/src/features/statsig/hooks'
import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName'
import { isAndroid } from 'uniswap/src/utils/platform'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
......
......@@ -34,8 +34,8 @@ import NotificationIcon from 'ui/src/assets/icons/bell.svg'
import GlobalIcon from 'ui/src/assets/icons/global.svg'
import TextEditIcon from 'ui/src/assets/icons/textEdit.svg'
import { iconSizes, spacing } from 'ui/src/theme'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlag } from 'uniswap/src/features/statsig/hooks'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { Switch } from 'wallet/src/components/buttons/Switch'
import { ChainId } from 'wallet/src/constants/chains'
......
......@@ -14,8 +14,8 @@ import { Screen } from 'src/components/layout/Screen'
import { UnitagBanner } from 'src/components/unitags/UnitagBanner'
import { Button, Flex, Icons, Text } from 'ui/src'
import { fonts } from 'ui/src/theme'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlag } from 'uniswap/src/features/statsig/hooks'
import { isIOS } from 'uniswap/src/utils/platform'
import { TextInput } from 'wallet/src/components/input/TextInput'
import { NICKNAME_MAX_LENGTH } from 'wallet/src/constants/accounts'
......
......@@ -58,13 +58,11 @@ describe('Uni tags support', () => {
cy.contains(haydenUnitag).should('be.visible')
cy.contains('0x50EC...79C3').should('be.visible')
})
cy.get(getTestSelector('secondary-identifiers'))
.trigger('mouseover')
.click()
.within(() => {
cy.contains(haydenENS).should('be.visible')
cy.contains('0x50EC...79C3').should('be.visible')
})
cy.get(getTestSelector('secondary-identifiers')).trigger('mouseover').click()
cy.get(getTestSelector('secondary-identifiers-dropdown')).within(() => {
cy.contains(haydenENS)
cy.contains('0x50EC...79C3')
})
})
})
})
......@@ -106,7 +106,8 @@ describe('Permit2', () => {
* already 0 to mitigate the race condition described here:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*/
it('swaps USDT with existing permit, and existing but insufficient token approval', () => {
// TODO re-enable web test
it.skip('swaps USDT with existing permit, and existing but insufficient token approval', () => {
cy.hardhat().then(async (hardhat) => {
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDT, 2e6))
await hardhat.mine()
......
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { getTestSelector } from '../utils'
describe('Wallet Dropdown', () => {
......
......@@ -2,7 +2,7 @@ import 'cypress-hardhat/lib/browser'
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
import { FeatureFlagClient, FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/experiments/flags'
import { FeatureFlagClient, FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/statsig/flags'
import { UserState, initialState } from '../../src/state/user/reducer'
import { CONNECTED_WALLET_USER_STATE, setInitialUserState } from '../utils/user-state'
......
......@@ -4,7 +4,7 @@
/* eslint-disable import/no-unused-modules */
import { paths } from '../src/pages/paths'
import { MetaTagInjector } from './components/metaTagInjector'
import { transformResponse } from './utils/transformResponse'
function doesMatchPath(path: string): boolean {
const regexPaths = paths.map((p) => '^' + p.replace(/:[^/]+/g, '[^/]+').replace(/\*/g, '.*') + '$')
......@@ -21,13 +21,13 @@ export const onRequest: PagesFunction = async ({ request, next }) => {
url: request.url,
description: 'Swap or provide liquidity on the Uniswap Protocol',
}
const res = next()
const response = next()
if (doesMatchPath(requestURL.pathname)) {
try {
return new HTMLRewriter().on('head', new MetaTagInjector(data, request)).transform(await res)
return transformResponse(request, await response, data)
} catch (e) {
return res
return response
}
}
return res
return response
}
......@@ -10,6 +10,8 @@ type MetaTagInjectorInput = {
* to inject meta tags into the <head> of an HTML document.
*/
export class MetaTagInjector implements HTMLRewriterElementContentHandlers {
static SELECTOR = 'head'
constructor(private input: MetaTagInjectorInput, private request: Request) {}
append(element: Element, property: string, content: string) {
......
/* eslint-disable import/no-unused-modules */
import { getMetadataRequest } from '../../utils/getRequest'
import getToken from '../../utils/getToken'
import { transformResponse } from '../../utils/transformResponse'
export const onRequest: PagesFunction = async ({ params, request, next }) => {
const res = next()
const response = next()
try {
const { index } = params
const networkName = index[0]?.toString()
const tokenAddress = index[1]?.toString()
if (!tokenAddress) {
return res
return response
}
return getMetadataRequest(res, request, () => getToken(networkName, tokenAddress, request.url))
return transformResponse(request, await response, () => getToken(networkName, tokenAddress, request.url))
} catch (e) {
return res
return response
}
}
/* eslint-disable import/no-unused-modules */
import getAsset from '../../utils/getAsset'
import { getMetadataRequest } from '../../utils/getRequest'
import { transformResponse } from '../../utils/transformResponse'
export const onRequest: PagesFunction = async ({ params, request, next }) => {
const res = next()
const response = next()
try {
const { index } = params
const collectionAddress = index[0]?.toString()
const tokenId = index[1]?.toString()
return getMetadataRequest(res, request, () => getAsset(collectionAddress, tokenId, request.url))
return transformResponse(request, await response, () => getAsset(collectionAddress, tokenId, request.url))
} catch (e) {
return res
return response
}
}
/* eslint-disable import/no-unused-modules */
import getCollection from '../../utils/getCollection'
import { getMetadataRequest } from '../../utils/getRequest'
import { transformResponse } from '../../utils/transformResponse'
export const onRequest: PagesFunction = async ({ params, request, next }) => {
const res = next()
const response = next()
try {
const { index } = params
const collectionAddress = index?.toString()
return getMetadataRequest(res, request, () => getCollection(collectionAddress, request.url))
return transformResponse(request, await response, () => getCollection(collectionAddress, request.url))
} catch (e) {
return res
return response
}
}
import { MetaTagInjector } from '../components/metaTagInjector'
import Cache, { Data } from './cache'
export async function getMetadataRequest(
res: Promise<Response>,
request: Request,
getData: () => Promise<Data | undefined>
) {
try {
const cachedData = await getRequest(request.url, getData, (data): data is Data => true)
if (cachedData) {
return new HTMLRewriter().on('head', new MetaTagInjector(cachedData, request)).transform(await res)
} else {
return res
}
} catch (e) {
return res
}
}
export async function getRequest<T extends Data>(
url: string,
getData: () => Promise<T | undefined>,
......
/* eslint-disable import/no-unused-modules */
import { MetaTagInjector } from '../components/metaTagInjector'
import { Data } from './cache'
import { getRequest } from './getRequest'
export async function transformResponse(
request: Request,
response: Response,
data: (() => Promise<Data | undefined>) | Data | undefined
) {
try {
if (typeof data === 'function') {
data = await getRequest(request.url, data, (data): data is Data => true)
}
if (data) {
return new HTMLRewriter().on(MetaTagInjector.SELECTOR, new MetaTagInjector(data, request)).transform(response)
} else {
return response
}
} catch (e) {
return response
}
}
......@@ -155,7 +155,7 @@
"ts-jest": "^29.1.1",
"tsafe": "1.6.4",
"typescript": "5.3.3",
"webpack": "5.89.0",
"webpack": "5.90.0",
"webpack-retry-chunk-load-plugin": "3.1.1",
"wrangler": "3.15.0",
"yarn-deduplicate": "6.0.0"
......@@ -184,6 +184,7 @@
"@sentry/types": "7.80.0",
"@tamagui/core": "1.94.3",
"@tamagui/react-native-svg": "1.94.3",
"@tanstack/react-query": "5.28.14",
"@tanstack/react-table": "8.10.7",
"@types/poisson-disk-sampling": "2.2.4",
"@types/react-scroll-sync": "0.8.7",
......@@ -290,6 +291,8 @@
"utilities": "workspace:^",
"uuid": "9.0.0",
"video-extensions": "1.2.0",
"viem": "2.x",
"wagmi": "2.5.19",
"wcag-contrast": "3.0.0",
"web-vitals": "2.1.4",
"xml2js": "0.6.2",
......
......@@ -27,7 +27,7 @@
"https://*.uniswap.org",
"https://uniswap.org",
"https://assets.coingecko.com/",
"https://*.amazonaws.com",
"https://*.amazonaws.com",
"https://basescan.org",
"https://celo-org.github.io/",
"https://cdn.center.app/",
......@@ -60,43 +60,63 @@
"wss://*.uniswap.org",
"https://*.uniswap.org",
"https://uniswap.org",
"https://assets.coingecko.com/",
"https://arb1.arbitrum.io",
"https://*.coingecko.com/",
"https://*.alchemy.com",
"https://buy.moonpay.com/",
"https://bsc-dataseed1.binance.org/",
"https://cdn.center.app/",
"https://cdn.jsdelivr.net/npm/@rive-app/canvas@2.8.3/rive.wasm",
"https://chain-proxy.wallet.coinbase.com",
"https://*.coinbase.com",
"https://statsigapi.net",
"https://api.moonpay.com/",
"https://api.opensea.io",
"https://api.thegraph.com/",
"https://arbitrum-mainnet.infura.io/",
"https://assets.coingecko.com",
"https://avalanche-mainnet.infura.io/",
"https://base-mainnet.infura.io/",
"https://bridge.arbitrum.io",
"https://celo-mainnet.infura.io/",
"https://celo-org.github.io",
"https://cloudflare-ipfs.com",
"https://explorer-api.walletconnect.com",
"https://*.zerion.io",
"https://*.drpc.org/",
"https://*.base.org/",
"https://*.walletconnect.com",
"https://ethereum-optimism.github.io/",
"https://*.twnodes.com",
"https://forno.celo.org/",
"https://www.gemini.com",
"https://*.gemini.com",
"https://gateway.ipfs.io/",
"https://i.seadn.io/",
"https://ipfs.io/",
"https://lh3.googleusercontent.com/",
"https://mainnet.infura.io",
"https://*.nodereal.io",
"https://o1037921.ingest.sentry.io",
"https://old-wispy-arrow.bsc.quiknode.pro/",
"https://openseauserdata.com/",
"https://optimism-mainnet.infura.io/",
"https://polygon-mainnet.infura.io/",
"https://performance.radar.cloudflare.com/",
"https://valid.rpki.cloudflare.com",
"https://sparrow.cloudflare.com/",
"https://ipv4-check-perf.radar.cloudflare.com",
"https://ipv6-check-perf.radar.cloudflare.com/",
"https://invalid.rpki.cloudflare.com/",
"https://raw.githubusercontent.com",
"https://raw.seadn.io/",
"https://s2.coinmarketcap.com/",
"https://static.optimism.io",
"https://rpc.ankr.com",
"https://rpc.degen.tips",
"https://rpc-mainnet.maticvigil.com",
"https://rpc.mevblocker.io/",
"https://rpc.scroll.io/",
"https://*.coinmarketcap.com/",
"https://*.optimism.io",
"https://sockjs-us3.pusher.com/",
"https://api.studio.thegraph.com/",
"https://*.googleapis.com",
"https://trustwallet.com",
"https://tokenlist.arbitrum.io",
"https://*.arbitrum.io",
"https://tokens.coingecko.com",
"https://*.twnodes.com",
"https://ultra-blue-flower.quiknode.pro",
......@@ -104,10 +124,13 @@
"https://us-central1-uniswap-mobile.cloudfunctions.net/",
"https://vercel.com",
"https://vercel.live/",
"https://wallet.crypto.com",
"https://web3.1inch.io",
"https://www.gemini.com",
"https://*.quiknode.pro",
"https://*.infura.io",
"wss://relay.walletconnect.com",
"wss://relay.walletconnect.org",
"wss://www.walletlink.org",
"wss://ws-us3.pusher.com/"
],
......
......@@ -65,6 +65,7 @@ const UNIButton = styled(WalletButton)`
const IconContainer = styled.div`
display: flex;
flex: 0 0 auto;
align-items: center;
& > a,
& > button {
......
import 'test-utils/tokens/mocks'
import { ChainId, WETH9 } from '@uniswap/sdk-core'
import { formatTimestamp } from 'components/AccountDrawer/MiniPortfolio/formatTimestamp'
import { DAI, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import { useCurrency } from 'hooks/Tokens'
import { DAI } from 'constants/tokens'
import { SignatureType } from 'state/signatures/types'
import { mocked } from 'test-utils/mocked'
import { render } from 'test-utils/render'
import { UniswapXOrderStatus } from 'types/uniswapx'
import { UniswapXOrderStatus } from 'types/uniswapx'
import { OrderContent } from './OffchainActivityModal'
jest.mock('hooks/Tokens', () => ({
useCurrency: jest.fn(),
}))
jest.mock('components/AccountDrawer/MiniPortfolio/formatTimestamp', () => ({
formatTimestamp: jest.fn(),
}))
describe('OrderContent', () => {
beforeEach(() => {
mocked(useCurrency).mockImplementation((currencyId: Maybe<string>) => {
if (currencyId === WETH9[ChainId.MAINNET].address) {
return WRAPPED_NATIVE_CURRENCY[ChainId.MAINNET]
} else {
return DAI
}
})
mocked(formatTimestamp).mockImplementation(() => {
return 'Mock Date' // This ensures consistent test behavior across local and CI
})
......@@ -45,7 +36,7 @@ describe('OrderContent', () => {
isUniswapXOrder: true,
type: 1,
tradeType: 0,
inputCurrencyId: '0x6b175474e89094c44da98b954eedeac495271d0f',
inputCurrencyId: DAI.address,
outputCurrencyId: WETH9[ChainId.MAINNET].address,
inputCurrencyAmountRaw: '252074033564766400000',
expectedOutputCurrencyAmountRaw: '106841079134757921',
......
......@@ -101,7 +101,7 @@ const InsufficientFundsCopyContainer = styled(Row)`
const AlertIconContainer = styled.div`
display: flex;
flex-shrink: 0;
background-color: #1f1e02;
background-color: ${({ theme }) => theme.deprecated_accentWarningSoft};
width: 40px;
height: 40px;
justify-content: center;
......
......@@ -66,7 +66,46 @@ Object {
}
`;
exports[`parseRemote parseRemoteActivities should parse closed UniswapX order 1`] = `
exports[`parseRemote parseRemoteActivities should parse eth wrap 1`] = `
Object {
"chainId": 1,
"currencies": Array [
ExtendedEther {
"chainId": 1,
"decimals": 18,
"isNative": true,
"isToken": false,
"name": "Ethereum",
"symbol": "ETH",
},
Token {
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"buyFeeBps": undefined,
"chainId": 1,
"decimals": 18,
"isNative": false,
"isToken": true,
"name": "Wrapped Ether",
"sellFeeBps": undefined,
"symbol": "WETH",
},
],
"descriptor": "100 ETH for 100 WETH",
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
"hash": "someHash",
"isSpam": false,
"logos": Array [
"https://token-icons.s3.amazonaws.com/eth.png",
"https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png",
],
"nonce": 12345,
"status": "CONFIRMED",
"timestamp": 10000,
"title": "Wrapped",
}
`;
exports[`parseRemote parseRemoteActivities should parse expired UniswapX order 1`] = `
Object {
"chainId": 1,
"currencies": Array [
......@@ -104,6 +143,7 @@ Object {
"addedTime": 10000,
"chainId": 1,
"encodedOrder": undefined,
"expiry": undefined,
"id": "someId",
"offerer": "someOfferer",
"orderHash": "someHash",
......@@ -115,11 +155,11 @@ Object {
"isUniswapXOrder": true,
"minimumOutputCurrencyAmountRaw": "200000000000000000000",
"outputCurrencyId": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"settledOutputCurrencyAmountRaw": "200000000000000000000",
"settledOutputCurrencyAmountRaw": undefined,
"tradeType": 0,
"type": 1,
},
"txHash": "someHash",
"txHash": undefined,
"type": undefined,
},
"prefixIconSrc": "bolt.svg",
......@@ -130,17 +170,20 @@ Object {
}
`;
exports[`parseRemote parseRemoteActivities should parse eth wrap 1`] = `
exports[`parseRemote parseRemoteActivities should parse filledUniswapX order 1`] = `
Object {
"chainId": 1,
"currencies": Array [
ExtendedEther {
Token {
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
"buyFeeBps": undefined,
"chainId": 1,
"decimals": 18,
"isNative": true,
"isToken": false,
"name": "Ethereum",
"symbol": "ETH",
"isNative": false,
"isToken": true,
"name": "DAI",
"sellFeeBps": undefined,
"symbol": "DAI",
},
Token {
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
......@@ -154,18 +197,40 @@ Object {
"symbol": "WETH",
},
],
"descriptor": "100 ETH for 100 WETH",
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
"descriptor": "100 DAI for 200 WETH",
"from": "someOfferer",
"hash": "someHash",
"isSpam": false,
"logos": Array [
"https://token-icons.s3.amazonaws.com/eth.png",
"https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png",
"someUrl",
"someUrl",
],
"nonce": 12345,
"offchainOrderDetails": Object {
"addedTime": 10000,
"chainId": 1,
"encodedOrder": undefined,
"expiry": undefined,
"id": "someId",
"offerer": "someOfferer",
"orderHash": "someHash",
"status": "filled",
"swapInfo": Object {
"expectedOutputCurrencyAmountRaw": "200000000000000000000",
"inputCurrencyAmountRaw": "100000000000000000000",
"inputCurrencyId": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
"isUniswapXOrder": true,
"minimumOutputCurrencyAmountRaw": "200000000000000000000",
"outputCurrencyId": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"settledOutputCurrencyAmountRaw": "200000000000000000000",
"tradeType": 0,
"type": 1,
},
"txHash": "someHash",
"type": undefined,
},
"prefixIconSrc": "bolt.svg",
"status": "CONFIRMED",
"timestamp": 10000,
"title": "Wrapped",
"title": "Swapped",
}
`;
......
......@@ -63,56 +63,6 @@ const mockAssetActivityPartsFragment = {
},
}
const mockSwapOrderDetailsPartsFragment = {
__typename: 'SwapOrderDetails',
id: 'someId',
offerer: 'someOfferer',
hash: 'someHash',
inputTokenQuantity: '100',
outputTokenQuantity: '200',
orderStatus: SwapOrderStatus.Open,
inputToken: {
__typename: 'Token',
id: DAI.address,
name: 'DAI',
symbol: DAI.symbol,
address: DAI.address,
decimals: 18,
chain: Chain.Ethereum,
standard: TokenStandard.Erc20,
project: {
__typename: 'TokenProject',
id: 'projectId',
isSpam: false,
logo: {
__typename: 'Image',
id: 'imageId',
url: 'someUrl',
},
},
},
outputToken: {
__typename: 'Token',
id: WETH9[1].address,
name: 'Wrapped Ether',
symbol: 'WETH',
address: WETH9[1].address,
decimals: 18,
chain: Chain.Ethereum,
standard: TokenStandard.Erc20,
project: {
__typename: 'TokenProject',
id: 'projectId',
isSpam: false,
logo: {
__typename: 'Image',
id: 'imageId',
url: 'someUrl',
},
},
},
}
const mockNftApprovalPartsFragment: NftApprovalPartsFragment = {
__typename: 'NftApproval',
id: 'approvalId',
......@@ -395,19 +345,6 @@ const mockTokenApprovalPartsFragment: TokenApprovalPartsFragment = {
},
}
export const MockOpenUniswapXOrder = {
...mockAssetActivityPartsFragment,
details: mockSwapOrderDetailsPartsFragment,
} as AssetActivityPartsFragment
export const MockClosedUniswapXOrder = {
...mockAssetActivityPartsFragment,
details: {
...mockSwapOrderDetailsPartsFragment,
orderStatus: SwapOrderStatus.Expired,
},
} as AssetActivityPartsFragment
const commonTransactionDetailsFields = {
__typename: 'TransactionDetails',
from: MockSenderAddress,
......
import { act, renderHook } from '@testing-library/react'
import ms from 'ms'
import { MockExpiredUniswapXOrder, MockFilledUniswapXOrder, MockOpenUniswapXOrder } from 'state/signatures/fixtures'
import {
MockClosedUniswapXOrder,
MockMint,
MockMoonpayPurchase,
MockNFTApproval,
MockNFTApprovalForAll,
MockNFTPurchase,
MockNFTReceive,
MockOpenUniswapXOrder,
MockRemoveLiquidity,
MockSenderAddress,
MockSpamMint,
......@@ -19,10 +18,10 @@ import {
MockTokenApproval,
MockTokenReceive,
MockTokenSend,
MockWrap,
mockTokenTransferInPartsFragment,
mockTokenTransferOutPartsFragment,
mockTransactionDetailsPartsFragment,
MockWrap,
} from './fixtures/activity'
import {
offchainOrderDetailsFromGraphQLTransactionActivity,
......@@ -48,8 +47,12 @@ describe('parseRemote', () => {
const result = parseRemoteActivities([MockOpenUniswapXOrder], '', jest.fn())
expect(result).toEqual({})
})
it('should parse closed UniswapX order', () => {
const result = parseRemoteActivities([MockClosedUniswapXOrder], '', jest.fn())
it('should parse expired UniswapX order', () => {
const result = parseRemoteActivities([MockExpiredUniswapXOrder], '', jest.fn())
expect(result?.['someHash']).toMatchSnapshot()
})
it('should parse filledUniswapX order', () => {
const result = parseRemoteActivities([MockFilledUniswapXOrder], '', jest.fn())
expect(result?.['someHash']).toMatchSnapshot()
})
it('should parse NFT approval', () => {
......
import { ChainId, Currency, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES, TradeType, UNI_ADDRESSES } from '@uniswap/sdk-core'
import UniswapXBolt from 'assets/svg/bolt.svg'
import moonpayLogoSrc from 'assets/svg/moonpay.svg'
import { asSupportedChain } from 'constants/chains'
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
import { BigNumber } from 'ethers/lib/ethers'
import { formatUnits, parseUnits } from 'ethers/lib/utils'
......@@ -8,9 +9,8 @@ import { gqlToCurrency, logSentryErrorForUnsupportedChain, supportedChainIdFromG
import { t } from 'i18n'
import ms from 'ms'
import { useEffect, useState } from 'react'
import store from 'state'
import { addSignature } from 'state/signatures/reducer'
import { SignatureType, UniswapXOrderDetails } from 'state/signatures/types'
import { OrderActivity, parseRemote as parseRemoteSignature } from 'state/signatures/parseRemote'
import { UniswapXOrderDetails } from 'state/signatures/types'
import { TransactionType as LocalTransactionType } from 'state/transactions/types'
import { UniswapXOrderStatus } from 'types/uniswapx'
import {
......@@ -19,9 +19,6 @@ import {
NftApprovalPartsFragment,
NftApproveForAllPartsFragment,
NftTransferPartsFragment,
SwapOrderDetailsPartsFragment,
SwapOrderStatus,
SwapOrderType,
TokenApprovalPartsFragment,
TokenAssetPartsFragment,
TokenTransferPartsFragment,
......@@ -29,9 +26,8 @@ import {
TransactionType,
} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { isAddress, isSameAddress } from 'utilities/src/addresses'
import { currencyId } from 'utils/currencyId'
import { NumberType, useFormatter } from 'utils/formatNumbers'
import { MOONPAY_SENDER_ADDRESSES, OrderStatusTable, OrderTextTable } from '../constants'
import { MOONPAY_SENDER_ADDRESSES, OrderTextTable } from '../constants'
import { Activity } from './types'
type TransactionChanges = {
......@@ -276,7 +272,7 @@ export function offchainOrderDetailsFromGraphQLTransactionActivity(
changes: TransactionChanges,
formatNumberOrString: FormatNumberOrStringFunctionType
): UniswapXOrderDetails | undefined {
const chainId = supportedChainIdFromGQLChain(activity.chain)
const chainId = asSupportedChain(supportedChainIdFromGQLChain(activity.chain))
if (!activity || !activity.details || !chainId) return undefined
if (changes.TokenTransfer.length < 2) return undefined
......@@ -333,7 +329,6 @@ function parseLPTransfers(changes: TransactionChanges, formatNumberOrString: For
}
type TransactionActivity = AssetActivityPartsFragment & { details: TransactionDetailsPartsFragment }
type OrderActivity = AssetActivityPartsFragment & { details: SwapOrderDetailsPartsFragment }
function parseSendReceive(
changes: TransactionChanges,
......@@ -448,112 +443,31 @@ function getLogoSrcs(changes: TransactionChanges): Array<string | undefined> {
return Array.from(logoSet)
}
function swapOrderTypeToSignatureType(swapOrderType: SwapOrderType): SignatureType {
switch (swapOrderType) {
case SwapOrderType.Limit:
return SignatureType.SIGN_LIMIT
case SwapOrderType.Dutch:
return SignatureType.SIGN_UNISWAPX_ORDER
case SwapOrderType.DutchV2:
return SignatureType.SIGN_UNISWAPX_V2_ORDER
}
}
function parseUniswapXOrder({ details, chain, timestamp }: OrderActivity): Activity | undefined {
const supportedChain = supportedChainIdFromGQLChain(chain)
if (!supportedChain) {
logSentryErrorForUnsupportedChain({
extras: { details },
errorMessage: 'Invalid activity from unsupported chain received from GQL',
})
return undefined
}
// If the order is open, maybe add it to our local records (if it was initiated on this device, this will be a no-op).
if (details.orderStatus === SwapOrderStatus.Open) {
const inputCurrency = gqlToCurrency(details.inputToken)
const outputCurrency = gqlToCurrency(details.outputToken)
const inputTokenQuantity = parseUnits(details.inputTokenQuantity, details.inputToken.decimals).toString()
const outputTokenQuantity = parseUnits(details.outputTokenQuantity, details.outputToken.decimals).toString()
function parseUniswapXOrder(activity: OrderActivity): Activity | undefined {
const signature = parseRemoteSignature(activity)
if (inputTokenQuantity === '0' || outputTokenQuantity === '0') {
// TODO(WEB-3765): This is a temporary mitigation for a bug where the backend sends "0.000000" for small amounts.
throw new Error('Invalid activity received from GQL')
}
store.dispatch(
addSignature({
type: swapOrderTypeToSignatureType(details.swapOrderType),
offerer: details.offerer,
id: details.hash,
chainId: supportedChain,
orderHash: details.hash,
expiry: details.expiry,
encodedOrder: details.encodedOrder,
swapInfo: {
type: LocalTransactionType.SWAP,
inputCurrencyId: currencyId(inputCurrency),
outputCurrencyId: currencyId(outputCurrency),
isUniswapXOrder: true,
// This doesn't affect the display, but we don't know this value from the remote activity.
tradeType: TradeType.EXACT_INPUT,
inputCurrencyAmountRaw: inputTokenQuantity,
expectedOutputCurrencyAmountRaw: outputTokenQuantity,
minimumOutputCurrencyAmountRaw: outputTokenQuantity,
},
status: UniswapXOrderStatus.OPEN,
addedTime: timestamp * 1000,
})
)
return undefined
// If the order is open, do not render it.
if (signature.status === UniswapXOrderStatus.OPEN) {
return
}
// If the order is not open, render it like any other remote activity.
const { inputToken, inputTokenQuantity, outputToken, outputTokenQuantity, orderStatus } = details
const uniswapXOrderStatus = OrderStatusTable[orderStatus]
const { status, statusMessage, title } = OrderTextTable[uniswapXOrderStatus]
const descriptor = getSwapDescriptor({
tokenIn: inputToken,
inputAmount: inputTokenQuantity,
tokenOut: outputToken,
outputAmount: outputTokenQuantity,
})
const { inputToken, inputTokenQuantity, outputToken, outputTokenQuantity } = activity.details
return {
hash: details.hash,
chainId: supportedChain,
status,
statusMessage,
offchainOrderDetails: {
id: details.id,
type: swapOrderTypeToSignatureType(details.swapOrderType),
encodedOrder: details.encodedOrder,
txHash: details.hash,
orderHash: details.hash,
offerer: details.offerer,
chainId: supportedChain,
status: uniswapXOrderStatus,
addedTime: timestamp,
swapInfo: {
isUniswapXOrder: true,
type: LocalTransactionType.SWAP,
tradeType: TradeType.EXACT_INPUT,
inputCurrencyId: inputToken.address ?? '',
outputCurrencyId: outputToken.address ?? '',
inputCurrencyAmountRaw: parseUnits(inputTokenQuantity, inputToken.decimals).toString(),
expectedOutputCurrencyAmountRaw: parseUnits(outputTokenQuantity, outputToken.decimals).toString(),
minimumOutputCurrencyAmountRaw: parseUnits(outputTokenQuantity, outputToken.decimals).toString(),
settledOutputCurrencyAmountRaw: parseUnits(outputTokenQuantity, outputToken.decimals).toString(),
},
},
timestamp,
hash: signature.orderHash,
chainId: signature.chainId,
offchainOrderDetails: signature,
timestamp: activity.timestamp,
logos: [inputToken.project?.logo?.url, outputToken.project?.logo?.url],
currencies: [gqlToCurrency(inputToken), gqlToCurrency(outputToken)],
title,
descriptor,
from: details.offerer,
descriptor: getSwapDescriptor({
tokenIn: inputToken,
inputAmount: inputTokenQuantity,
tokenOut: outputToken,
outputAmount: outputTokenQuantity,
}),
from: signature.offerer,
prefixIconSrc: UniswapXBolt,
...OrderTextTable[signature.status],
}
}
......
......@@ -16,6 +16,7 @@ export type Activity = {
title: string
descriptor?: string
logos?: Array<string | undefined>
// TODO(WEB-3839): replace Currency with CurrencyInfo
currencies?: Array<Currency | undefined>
otherAccount?: string
from: string
......
import 'test-utils/tokens/mocks'
import { ChainId, WETH9 } from '@uniswap/sdk-core'
import { Activity } from 'components/AccountDrawer/MiniPortfolio/Activity/types'
import { LimitDetailActivityRow } from 'components/AccountDrawer/MiniPortfolio/Limits/LimitDetailActivityRow'
......@@ -14,20 +16,6 @@ jest.mock('components/AccountDrawer/MiniPortfolio/formatTimestamp', () => {
}
})
jest.mock('hooks/Tokens', () => {
return {
useCurrency: (address?: string) => {
if (address?.toLowerCase() === DAI.address.toLowerCase()) {
return DAI
}
if (address?.toLowerCase() === WETH9[ChainId.MAINNET].address.toLowerCase()) {
return WETH9[ChainId.MAINNET]
}
return undefined
},
}
})
const mockOrderDetails: UniswapXOrderDetails = {
type: SignatureType.SIGN_LIMIT,
orderHash: '0x1234',
......@@ -60,6 +48,7 @@ const mockOrder: Activity = {
title: 'Limit pending',
from: '0x456',
offchainOrderDetails: mockOrderDetails,
currencies: [DAI, WETH9[ChainId.MAINNET]],
}
describe('LimitDetailActivityRow', () => {
......
......@@ -9,7 +9,7 @@ import { FormatType, formatTimestamp } from 'components/AccountDrawer/MiniPortfo
import Column from 'components/Column'
import Row from 'components/Row'
import { parseUnits } from 'ethers/lib/utils'
import useTokenLogoSource from 'hooks/useAssetLogoSource'
import { useCurrencyInfo } from 'hooks/Tokens'
import { useScreenSize } from 'hooks/useScreenSize'
import { Trans } from 'i18n'
import { Checkbox } from 'nft/components/layout/Checkbox'
......@@ -50,7 +50,9 @@ const CircleLogoImage = styled.img<{ size: string }>`
export function LimitDetailActivityRow({ order, onToggleSelect, selected }: LimitDetailActivityRowProps) {
const theme = useTheme()
const { chainId, logos, currencies, offchainOrderDetails } = order
const { logos, currencies, offchainOrderDetails } = order
const inputCurrencyInfo = useCurrencyInfo(currencies?.[0])
const outputCurrencyInfo = useCurrencyInfo(currencies?.[1])
const openOffchainActivityModal = useOpenOffchainActivityModal()
const { formatReviewSwapCurrencyAmount } = useFormatter()
const [hovered, setHovered] = useState(false)
......@@ -70,19 +72,11 @@ export function LimitDetailActivityRow({ order, onToggleSelect, selected }: Limi
)
}, [amounts?.inputAmount, amounts?.outputAmount, amountsDefined])
const [inputLogoSrc, nextInputLogoSrc] = useTokenLogoSource({
address: currencies?.[0]?.wrapped.address,
chainId,
isNative: currencies?.[0]?.isNative,
})
const [outputLogoSrc, nextOutputLogoSrc2] = useTokenLogoSource({
address: currencies?.[1]?.wrapped.address,
chainId,
isNative: currencies?.[1]?.isNative,
})
if (!offchainOrderDetails || !amountsDefined) return null
const inputLogo = logos?.[0] ?? inputCurrencyInfo?.logoUrl
const outputLogo = logos?.[1] ?? outputCurrencyInfo?.logoUrl
return (
<Row onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)}>
<StyledPortfolioRow
......@@ -99,12 +93,12 @@ export function LimitDetailActivityRow({ order, onToggleSelect, selected }: Limi
descriptor={
<Column>
<TradeSummaryContainer gap="xs" align="center">
<CircleLogoImage src={logos?.[0] ?? inputLogoSrc} size="16px" onError={nextInputLogoSrc} />
{inputLogo && <CircleLogoImage src={inputLogo} size="16px" />}
<ThemedText.SubHeader color="neutral1">
{formatReviewSwapCurrencyAmount(amounts.inputAmount)} {amounts.inputAmount.currency.symbol}
</ThemedText.SubHeader>
<ArrowRight color={theme.neutral1} size="12px" />
<CircleLogoImage src={logos?.[1] ?? outputLogoSrc} size="16px" onError={nextOutputLogoSrc2} />
{outputLogo && <CircleLogoImage src={outputLogo} size="16px" />}
<ThemedText.SubHeader color="neutral1">
{formatReviewSwapCurrencyAmount(amounts.outputAmount)} {amounts.outputAmount.currency.symbol}
</ThemedText.SubHeader>
......
import 'test-utils/tokens/mocks'
import { ChainId, WETH9 } from '@uniswap/sdk-core'
import { useOpenLimitOrders } from 'components/AccountDrawer/MiniPortfolio/Activity/hooks'
import { Activity } from 'components/AccountDrawer/MiniPortfolio/Activity/types'
......@@ -19,20 +21,6 @@ jest.mock('components/AccountDrawer/MiniPortfolio/formatTimestamp', () => ({
formatTimestamp: () => 'January 26, 2024 at 1:52PM',
}))
jest.mock('hooks/Tokens', () => {
return {
useCurrency: (address?: string) => {
if (address?.toLowerCase() === DAI.address.toLowerCase()) {
return DAI
}
if (address?.toLowerCase() === WETH9[ChainId.MAINNET].address.toLowerCase()) {
return WETH9[ChainId.MAINNET]
}
return undefined
},
}
})
const mockOrderDetails: UniswapXOrderDetails = {
type: SignatureType.SIGN_LIMIT,
orderHash: '0x1234',
......
......@@ -117,7 +117,7 @@ exports[`LimitDetailActivityRow should render with valid details 1`] = `
}
.c14 {
border-color: #22222212;
border-color: #CECECE;
display: inline-block;
margin-right: 1px;
border-radius: 4px;
......@@ -217,6 +217,7 @@ exports[`LimitDetailActivityRow should render with valid details 1`] = `
>
<img
class="c9"
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png"
/>
<div
class="c10 css-n8z49y"
......@@ -248,6 +249,7 @@ exports[`LimitDetailActivityRow should render with valid details 1`] = `
</svg>
<img
class="c9"
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png"
/>
<div
class="c10 css-n8z49y"
......
......@@ -58,7 +58,7 @@ exports[`LimitsMenu should render when there are two open orders 1`] = `
letter-spacing: -0.01em;
}
.c21 {
.c20 {
-webkit-letter-spacing: -0.01em;
-moz-letter-spacing: -0.01em;
-ms-letter-spacing: -0.01em;
......@@ -146,7 +146,7 @@ exports[`LimitsMenu should render when there are two open orders 1`] = `
cursor: pointer;
}
.c22 {
.c21 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
......@@ -161,8 +161,8 @@ exports[`LimitsMenu should render when there are two open orders 1`] = `
line-height: 1;
}
.c24 {
border-color: #22222212;
.c23 {
border-color: #CECECE;
display: inline-block;
margin-right: 1px;
border-radius: 4px;
......@@ -178,15 +178,15 @@ exports[`LimitsMenu should render when there are two open orders 1`] = `
transition-duration: 125ms;
}
.c24:hover {
.c23:hover {
opacity: 0.6;
}
.c24:active {
.c23:active {
opacity: 0.4;
}
.c25 {
.c24 {
position: absolute;
top: -24px;
-webkit-clip: rect(0 0 0 0);
......@@ -200,7 +200,7 @@ exports[`LimitsMenu should render when there are two open orders 1`] = `
width: 1px;
}
.c26 {
.c25 {
display: none;
height: 18px;
width: 18px;
......@@ -217,7 +217,7 @@ exports[`LimitsMenu should render when there are two open orders 1`] = `
text-overflow: ellipsis;
}
.c23 {
.c22 {
opacity: 0;
}
......@@ -228,12 +228,6 @@ exports[`LimitsMenu should render when there are two open orders 1`] = `
text-overflow: ellipsis;
}
.c20 {
width: 16px;
height: 16px;
border-radius: 50%;
}
.c1 {
width: 100%;
overflow: auto;
......@@ -410,9 +404,6 @@ exports[`LimitsMenu should render when there are two open orders 1`] = `
<div
class="c13 c18 c19"
>
<img
class="c20"
/>
<div
class="c5 css-n8z49y"
>
......@@ -441,9 +432,6 @@ exports[`LimitsMenu should render when there are two open orders 1`] = `
points="12 5 19 12 12 19"
/>
</svg>
<img
class="c20"
/>
<div
class="c5 css-n8z49y"
>
......@@ -453,7 +441,7 @@ exports[`LimitsMenu should render when there are two open orders 1`] = `
</div>
</div>
<div
class="c21 css-142zc9n"
class="c20 css-142zc9n"
>
when 0.00042 WETH/DAI
</div>
......@@ -461,20 +449,20 @@ exports[`LimitsMenu should render when there are two open orders 1`] = `
</div>
</div>
<label
class="c22 c23"
class="c21 c22"
>
<span
aria-hidden="true"
class="c24"
class="c23"
size="18"
/>
<input
class="c25"
class="c24"
size="18"
type="checkbox"
/>
<svg
class="c26"
class="c25"
fill="none"
height="16"
size="18"
......@@ -514,9 +502,6 @@ exports[`LimitsMenu should render when there are two open orders 1`] = `
<div
class="c13 c18 c19"
>
<img
class="c20"
/>
<div
class="c5 css-n8z49y"
>
......@@ -545,9 +530,6 @@ exports[`LimitsMenu should render when there are two open orders 1`] = `
points="12 5 19 12 12 19"
/>
</svg>
<img
class="c20"
/>
<div
class="c5 css-n8z49y"
>
......@@ -557,7 +539,7 @@ exports[`LimitsMenu should render when there are two open orders 1`] = `
</div>
</div>
<div
class="c21 css-142zc9n"
class="c20 css-142zc9n"
>
when 0.00042 WETH/DAI
</div>
......@@ -565,20 +547,20 @@ exports[`LimitsMenu should render when there are two open orders 1`] = `
</div>
</div>
<label
class="c22 c23"
class="c21 c22"
>
<span
aria-hidden="true"
class="c24"
class="c23"
size="18"
/>
<input
class="c25"
class="c24"
size="18"
type="checkbox"
/>
<svg
class="c26"
class="c25"
fill="none"
height="16"
size="18"
......@@ -663,7 +645,7 @@ exports[`LimitsMenu should render when there is one open order 1`] = `
letter-spacing: -0.01em;
}
.c21 {
.c20 {
-webkit-letter-spacing: -0.01em;
-moz-letter-spacing: -0.01em;
-ms-letter-spacing: -0.01em;
......@@ -751,7 +733,7 @@ exports[`LimitsMenu should render when there is one open order 1`] = `
cursor: pointer;
}
.c22 {
.c21 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
......@@ -766,8 +748,8 @@ exports[`LimitsMenu should render when there is one open order 1`] = `
line-height: 1;
}
.c24 {
border-color: #22222212;
.c23 {
border-color: #CECECE;
display: inline-block;
margin-right: 1px;
border-radius: 4px;
......@@ -783,15 +765,15 @@ exports[`LimitsMenu should render when there is one open order 1`] = `
transition-duration: 125ms;
}
.c24:hover {
.c23:hover {
opacity: 0.6;
}
.c24:active {
.c23:active {
opacity: 0.4;
}
.c25 {
.c24 {
position: absolute;
top: -24px;
-webkit-clip: rect(0 0 0 0);
......@@ -805,7 +787,7 @@ exports[`LimitsMenu should render when there is one open order 1`] = `
width: 1px;
}
.c26 {
.c25 {
display: none;
height: 18px;
width: 18px;
......@@ -822,7 +804,7 @@ exports[`LimitsMenu should render when there is one open order 1`] = `
text-overflow: ellipsis;
}
.c23 {
.c22 {
opacity: 0;
}
......@@ -833,12 +815,6 @@ exports[`LimitsMenu should render when there is one open order 1`] = `
text-overflow: ellipsis;
}
.c20 {
width: 16px;
height: 16px;
border-radius: 50%;
}
.c1 {
width: 100%;
overflow: auto;
......@@ -1015,9 +991,6 @@ exports[`LimitsMenu should render when there is one open order 1`] = `
<div
class="c13 c18 c19"
>
<img
class="c20"
/>
<div
class="c5 css-n8z49y"
>
......@@ -1046,9 +1019,6 @@ exports[`LimitsMenu should render when there is one open order 1`] = `
points="12 5 19 12 12 19"
/>
</svg>
<img
class="c20"
/>
<div
class="c5 css-n8z49y"
>
......@@ -1058,7 +1028,7 @@ exports[`LimitsMenu should render when there is one open order 1`] = `
</div>
</div>
<div
class="c21 css-142zc9n"
class="c20 css-142zc9n"
>
when 0.00042 WETH/DAI
</div>
......@@ -1066,20 +1036,20 @@ exports[`LimitsMenu should render when there is one open order 1`] = `
</div>
</div>
<label
class="c22 c23"
class="c21 c22"
>
<span
aria-hidden="true"
class="c24"
class="c23"
size="18"
/>
<input
class="c25"
class="c24"
size="18"
type="checkbox"
/>
<svg
class="c26"
class="c25"
fill="none"
height="16"
size="18"
......
......@@ -16,9 +16,9 @@ import { ThemedText } from 'theme/components'
import { NumberType, useFormatter } from 'utils/formatNumbers'
import { ExpandoRow } from '../ExpandoRow'
import { useToggleAccountDrawer } from '../hooks'
import { PortfolioLogo } from '../PortfolioLogo'
import PortfolioRow, { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow'
import { useToggleAccountDrawer } from '../hooks'
import { PositionInfo } from './cache'
import { useFeeValues } from './hooks'
import useMultiChainPositions from './useMultiChainPositions'
......
import 'test-utils/tokens/mocks'
import { ChainId } from '@uniswap/sdk-core'
import { DAI, DAI_ARBITRUM_ONE, USDC_ARBITRUM, USDC_MAINNET } from 'constants/tokens'
import { render } from 'test-utils/render'
......
......@@ -4,7 +4,7 @@ import { ReactComponent as UnknownStatus } from 'assets/svg/contract-interaction
import { MissingImageLogo } from 'components/Logo/AssetLogo'
import { ChainLogo, getDefaultBorderRadius } from 'components/Logo/ChainLogo'
import { Unicon } from 'components/Unicon'
import useTokenLogoSource from 'hooks/useAssetLogoSource'
import { useCurrencyInfo } from 'hooks/Tokens'
import useENSAvatar from 'hooks/useENSAvatar'
import React from 'react'
import { Loader } from 'react-feather'
......@@ -12,8 +12,8 @@ import styled from 'styled-components'
import { useIsDarkMode } from 'theme/components/ThemeToggle'
import { UniconV2 } from 'ui/src/components/UniconV2'
import { useLogolessColorScheme } from 'ui/src/utils/colors'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlag } from 'uniswap/src/features/statsig/hooks'
const UnknownContract = styled(UnknownStatus)`
color: ${({ theme }) => theme.neutral2};
......@@ -94,29 +94,19 @@ function DoubleLogo({ logo1, onError1, logo2, onError2, size }: DoubleLogoProps)
interface DoubleCurrencyLogoProps {
chainId: ChainId
currencies: Array<Currency | undefined>
images?: Array<string | undefined>
backupImages?: Array<string | undefined>
size: string
}
function DoubleCurrencyLogo({ chainId, currencies, images, size }: DoubleCurrencyLogoProps) {
const [src, nextSrc] = useTokenLogoSource({
address: currencies?.[0]?.wrapped.address,
chainId,
isNative: currencies?.[0]?.isNative,
primaryImg: images?.[0],
})
const [src2, nextSrc2] = useTokenLogoSource({
address: currencies?.[1]?.wrapped.address,
chainId,
isNative: currencies?.[1]?.isNative,
primaryImg: images?.[1],
})
if (currencies.length === 1 && src) {
return <CircleLogoImage size={size} src={src} onError={nextSrc} />
function DoubleCurrencyLogo({ currencies, size, backupImages }: DoubleCurrencyLogoProps) {
const currencyInfos = [useCurrencyInfo(currencies?.[0]), useCurrencyInfo(currencies?.[1])]
if (currencies.length === 1 && currencyInfos[0]?.logoUrl) {
return <CircleLogoImage size={size} src={currencyInfos[0].logoUrl} />
}
if (currencies.length > 1) {
return <DoubleLogo logo1={src} onError1={nextSrc} logo2={src2} onError2={nextSrc2} size={size} />
const logo1 = currencyInfos[0]?.logoUrl ?? backupImages?.[0]
const logo2 = currencyInfos[1]?.logoUrl ?? backupImages?.[1]
if (currencies.length > 1 && (logo1 || logo2)) {
return <DoubleLogo logo1={logo1} logo2={logo2} size={size} />
}
return <LogolessPlaceholder currency={currencies?.[0]} size={size} />
}
......@@ -191,7 +181,7 @@ function getLogo({ chainId, accountAddress, currencies, images, size = '40px' }:
return <PortfolioAvatar accountAddress={accountAddress} size={size} />
}
if (currencies && currencies.length) {
return <DoubleCurrencyLogo chainId={chainId} currencies={currencies} images={images} size={size} />
return <DoubleCurrencyLogo chainId={chainId} currencies={currencies} size={size} />
}
if (images?.length === 1) {
return <CircleLogoImage size={size} src={images[0] ?? blankTokenUrl} />
......
......@@ -105,14 +105,7 @@ function TokenRow({
properties={{ chain_id: currency.chainId, token_name: token?.name, address: token?.address }}
>
<PortfolioRow
left={
<PortfolioLogo
chainId={currency.chainId}
currencies={[currency]}
images={[tokenProjectMarket?.tokenProject.logoUrl]}
size="40px"
/>
}
left={<PortfolioLogo chainId={currency.chainId} currencies={[currency]} size="40px" />}
title={<TokenNameText>{token?.name}</TokenNameText>}
descriptor={
<TokenBalanceText>
......
import { t } from 'i18n'
import { TransactionType } from 'state/transactions/types'
import { UniswapXOrderStatus } from 'types/uniswapx'
import {
SwapOrderStatus,
TransactionStatus,
} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { TransactionStatus } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
// use even number because rows are in groups of 2
export const DEFAULT_NFT_QUERY_AMOUNT = 26
......@@ -269,13 +266,3 @@ export const MOONPAY_SENDER_ADDRESSES = [
'0xb287eac48ab21c5fb1d3723830d60b4c797555b0',
'0xd108fd0e8c8e71552a167e7a44ff1d345d233ba6',
]
// Converts GQL backend orderStatus enum to the enum used by the frontend and UniswapX backend
export const OrderStatusTable: { [key in SwapOrderStatus]: UniswapXOrderStatus } = {
[SwapOrderStatus.Open]: UniswapXOrderStatus.OPEN,
[SwapOrderStatus.Expired]: UniswapXOrderStatus.EXPIRED,
[SwapOrderStatus.Error]: UniswapXOrderStatus.ERROR,
[SwapOrderStatus.InsufficientFunds]: UniswapXOrderStatus.INSUFFICIENT_FUNDS,
[SwapOrderStatus.Filled]: UniswapXOrderStatus.FILLED,
[SwapOrderStatus.Cancelled]: UniswapXOrderStatus.CANCELLED,
}
......@@ -9,8 +9,8 @@ import { ChevronRight } from 'react-feather'
import styled from 'styled-components'
import { ClickableStyle, ThemedText } from 'theme/components'
import ThemeToggle from 'theme/components/ThemeToggle'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlag } from 'uniswap/src/features/statsig/hooks'
import { AnalyticsToggle } from './AnalyticsToggle'
import { GitVersionRow } from './GitVersionRow'
import { LanguageMenuItems } from './LanguageMenu'
......
......@@ -2,31 +2,37 @@ import Column from 'components/Column'
import { ENS } from 'components/Icons/ENS'
import { EthMini } from 'components/Icons/EthMini'
import StatusIcon from 'components/Identicon/StatusIcon'
import Popover from 'components/Popover'
import Row from 'components/Row'
import { Connection } from 'connection/types'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { useRef, useState } from 'react'
import { MoreHorizontal } from 'react-feather'
import styled from 'styled-components'
import { ClickableStyle, CopyHelper, ThemedText } from 'theme/components'
import { ClickableStyle, CopyHelper, EllipsisStyle, ThemedText } from 'theme/components'
import { Unitag } from 'ui/src/components/icons/Unitag'
import { shortenAddress } from 'utilities/src/addresses'
const Container = styled.div`
display: inline-block;
width: 70%;
max-width: 70%;
display: flex;
padding-right: 8px;
display: inline-flex;
`
const Identifiers = styled.div`
white-space: nowrap;
display: flex;
width: 100%;
flex-direction: column;
justify-content: center;
margin-left: 8px;
user-select: none;
overflow: hidden;
flex: 1 1 auto;
`
const IdentifierText = styled.span`
${EllipsisStyle}
max-width: 120px;
@media screen and (min-width: 1440px) {
max-width: 180px;
}
`
const SecondaryIdentifiersContainer = styled(Row)`
position: relative;
......@@ -35,18 +41,15 @@ const SecondaryIdentifiersContainer = styled(Row)`
display: inline-block;
}
`
const MoreIcon = styled(MoreHorizontal)<{ $isActive: boolean }>`
const MoreIcon = styled(MoreHorizontal)`
height: 16px;
width: 16px;
color: ${({ theme }) => theme.neutral2};
cursor: pointer;
display: ${({ $isActive }) => !$isActive && 'none'};
${ClickableStyle}
`
const Dropdown = styled(Column)`
width: 240px;
position: absolute;
top: 20px;
gap: 2px;
padding: 8px;
border-radius: 20px;
......@@ -97,14 +100,19 @@ function SecondaryIdentifiers({
<SecondaryIdentifiersContainer data-testid="secondary-identifiers" ref={ref}>
<Row onClick={() => setIsDropdownOpen(!isDropdownOpen)} gap="8px">
<ThemedText.BodySmall color="neutral2">{shortenAddress(account)}</ThemedText.BodySmall>
<MoreIcon id="more-identifiers-icon" $isActive={isDropdownOpen} />
<Popover
show={isDropdownOpen}
placement="bottom"
content={
<Dropdown data-testid="secondary-identifiers-dropdown">
<SecondaryIdentifier Icon={EnsIcon} displayValue={ensUsername} copyValue={ensUsername} />
<SecondaryIdentifier Icon={EthMini} displayValue={shortenAddress(account)} copyValue={account} />
</Dropdown>
}
>
<MoreIcon id="more-identifiers-icon" />
</Popover>
</Row>
{isDropdownOpen && (
<Dropdown>
<SecondaryIdentifier Icon={EnsIcon} displayValue={ensUsername} copyValue={ensUsername} />
<SecondaryIdentifier Icon={EthMini} displayValue={shortenAddress(account)} copyValue={account} />
</Dropdown>
)}
</SecondaryIdentifiersContainer>
)
}
......@@ -141,7 +149,7 @@ export function Status({
toCopy={uniswapUsername ? uniswapUsername + '.uni.eth' : ensUsername ? ensUsername : account}
>
<Row gap="2px">
{uniswapUsername ?? ensUsername ?? shortenAddress(account)}
<IdentifierText>{uniswapUsername ?? ensUsername ?? shortenAddress(account)}</IdentifierText>
{uniswapUsername && <Unitag size={18} />}
</Row>
</CopyHelper>
......
......@@ -62,6 +62,7 @@ export const Scrim = (props: ScrimBackgroundProps) => {
const AccountDrawerScrollWrapper = styled.div`
overflow-y: auto;
overflow-x: hidden;
${ScrollBarStyles}
......@@ -111,10 +112,12 @@ const AccountDrawerWrapper = styled.div<{ open: boolean }>`
@media screen and (min-width: 1440px) {
margin-right: ${({ open }) => (open ? 0 : `-${DRAWER_WIDTH_XL}`)};
width: ${DRAWER_WIDTH_XL};
max-width: ${DRAWER_WIDTH_XL};
}
border-radius: 12px;
width: ${DRAWER_WIDTH};
max-width: ${DRAWER_WIDTH};
font-size: 16px;
background-color: ${({ theme }) => theme.surface1};
border: ${({ theme }) => `1px solid ${theme.surface3}`};
......
......@@ -16,7 +16,8 @@ const IconContainer = styled.div`
`
const IconBackground = styled.div`
background-color: #1f1e02;
display: flex;
background-color: ${({ theme }) => theme.deprecated_accentWarningSoft};
padding: 10px;
border-radius: 12px;
`
......
......@@ -2,6 +2,7 @@ import { formatTickMarks } from 'components/Charts/utils'
import Row from 'components/Row'
import { MissingDataBars } from 'components/Table/icons'
import { useActiveLocale } from 'hooks/useActiveLocale'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { useScreenSize } from 'hooks/useScreenSize'
import { Trans } from 'i18n'
import { useUpdateAtom } from 'jotai/utils'
......@@ -279,6 +280,8 @@ export function Chart<TParamType extends ChartDataParams<TDataType>, TDataType e
}
}, [setRefitChartContent])
useOnClickOutside({ current: chartDivElement } as React.RefObject<HTMLDivElement>, () => setCrosshairData(undefined))
return (
<ChartDiv
ref={setChartDivElement}
......
import 'test-utils/tokens/mocks'
import { LIMIT_ORDER_TRADE, TEST_TRADE_EXACT_INPUT } from 'test-utils/constants'
import { render, screen } from 'test-utils/render'
......
import 'test-utils/tokens/mocks'
import { ChainId, WETH9 } from '@uniswap/sdk-core'
import { Pending } from 'components/ConfirmSwapModal/Pending'
import { BigNumber } from 'ethers/lib/ethers'
......
......@@ -69,7 +69,7 @@ describe('LimitPriceInputPanel', () => {
</LimitContext.Provider>
</SwapAndLimitContext.Provider>
)
expect(screen.getByText('DAI')).toBeVisible()
expect(screen.getAllByText('DAI')).toHaveLength(2) // subheader and token symbol
expect(screen.getByPlaceholderText('0')).toBeVisible()
})
......@@ -90,7 +90,7 @@ describe('LimitPriceInputPanel', () => {
</LimitContext.Provider>
</SwapAndLimitContext.Provider>
)
expect(screen.getByText('DAI')).toBeVisible() // subheader
expect(screen.getAllByText('DAI')).toHaveLength(2) // subheader and token symbol
expect(container.querySelector('.token-symbol-container')).toHaveTextContent('USDC')
expect(screen.getByPlaceholderText('0')).toBeVisible()
})
......
......@@ -6,13 +6,13 @@ import { PropsWithChildren, ReactNode } from 'react'
import { X } from 'react-feather'
import { useModalIsOpen, useToggleFeatureFlags } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
import { Statsig } from 'statsig-react'
import styled from 'styled-components'
import { BREAKPOINTS } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
import { DynamicConfigs, getConfigName } from 'uniswap/src/features/experiments/configs'
import { FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlagWithExposureLoggingDisabled } from 'uniswap/src/features/experiments/hooks'
import { DynamicConfigs, getConfigName } from 'uniswap/src/features/statsig/configs'
import { FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlagWithExposureLoggingDisabled } from 'uniswap/src/features/statsig/hooks'
import { Statsig } from 'uniswap/src/features/statsig/sdk/statsig'
const StyledModal = styled.div`
position: fixed;
......
......@@ -7,8 +7,8 @@ import styled from 'styled-components'
import { useIsDarkMode } from 'theme/components/ThemeToggle'
import { flexColumnNoWrap } from 'theme/styles'
import { UniconV2 } from 'ui/src/components/UniconV2'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { FeatureFlags } from 'uniswap/src/features/statsig/flags'
import { useFeatureFlag } from 'uniswap/src/features/statsig/hooks'
import { useUnitagByAddressWithoutFlag } from 'uniswap/src/features/unitags/hooksWithoutFlags'
import { getWalletMeta } from 'utils/walletMeta'
import sockImg from '../../assets/svg/socks.svg'
......@@ -98,7 +98,7 @@ const MiniWalletIcon = ({ connection, side }: { connection: Connection; side: 'l
const MainWalletIcon = ({ account, connection, size }: { account: string; connection: Connection; size: number }) => {
const { unitag } = useUnitagByAddressWithoutFlag(account, Boolean(account))
const { avatar } = useENSAvatar(account ?? undefined)
const { avatar } = useENSAvatar(account)
const uniconV2Enabled = useFeatureFlag(FeatureFlags.UniconsV2)
if (!account) return null
......
......@@ -18,7 +18,7 @@ const StyledAvatar = styled.img`
`
export default function Identicon({ account, size }: { account: string; size?: number }) {
const { avatar } = useENSAvatar(account ?? undefined)
const { avatar } = useENSAvatar(account)
const [fetchable, setFetchable] = useState(true)
const iconSize = size ?? 24
......
......@@ -36,13 +36,7 @@ const LogoContainer = styled.div`
/**
* Renders an image by prioritizing a list of sources, and then eventually a fallback triangle alert
*/
export default function AssetLogo({
currency,
chainId = ChainId.MAINNET,
size = '24px',
style,
}: AssetLogoProps) {
export default function AssetLogo({ currency, chainId = ChainId.MAINNET, size = '24px', style }: AssetLogoProps) {
return (
<LogoContainer style={{ height: size, width: size, ...style }}>
<PortfolioLogo currencies={currency ? [currency] : []} size={size} chainId={chainId} />
......
......@@ -4,8 +4,8 @@ import { SearchToken } from 'graphql/data/SearchTokens'
import { TokenQueryData } from 'graphql/data/Token'
import { TopToken } from 'graphql/data/TopTokens'
import { gqlToCurrency, supportedChainIdFromGQLChain } from 'graphql/data/util'
import { useMemo } from 'react'
import { useMemo } from 'react'
import { AssetLogoBaseProps } from './AssetLogo'
export default function QueryTokenLogo(
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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