ci(release): publish latest release

parent 40c26d35
IPFS hash of the deployment:
- CIDv0: `QmZ5GePYVnqnekAczBnSNAm8msBT3xzqgkJFwukVQZ1hBf`
- CIDv1: `bafybeie7p7swozshq4rt5lc2uzdtauviqpr2xhr5rghmyyuqoiggsgpv5i`
- CIDv0: `QmR4bcDmfwJ5AzVy1ypYxhRHq82RAjo2k44zT1ZupBCo8v`
- CIDv1: `bafybeibiozbm7r6yfdyfvjzkyxuopbbjhpfakucm2axx265hltuzjl357m`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
......@@ -10,15 +10,66 @@ You can also access the Uniswap Interface from an IPFS gateway.
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://bafybeie7p7swozshq4rt5lc2uzdtauviqpr2xhr5rghmyyuqoiggsgpv5i.ipfs.dweb.link/
- https://bafybeie7p7swozshq4rt5lc2uzdtauviqpr2xhr5rghmyyuqoiggsgpv5i.ipfs.cf-ipfs.com/
- [ipfs://QmZ5GePYVnqnekAczBnSNAm8msBT3xzqgkJFwukVQZ1hBf/](ipfs://QmZ5GePYVnqnekAczBnSNAm8msBT3xzqgkJFwukVQZ1hBf/)
- https://bafybeibiozbm7r6yfdyfvjzkyxuopbbjhpfakucm2axx265hltuzjl357m.ipfs.dweb.link/
- https://bafybeibiozbm7r6yfdyfvjzkyxuopbbjhpfakucm2axx265hltuzjl357m.ipfs.cf-ipfs.com/
- [ipfs://QmR4bcDmfwJ5AzVy1ypYxhRHq82RAjo2k44zT1ZupBCo8v/](ipfs://QmR4bcDmfwJ5AzVy1ypYxhRHq82RAjo2k44zT1ZupBCo8v/)
### 5.52.1 (2024-10-15)
## 5.53.0 (2024-10-16)
### Features
* **web:** add a popup to claim (#12801) 727178b
* **web:** add bridge toast to web swaps (#12808) 91b3100
* **web:** add chainName to PosDP URL params (#12749) c5caa75
* **web:** bridge status polling (#12930) 1817b76
* **web:** bridging saga (#12753) dd6c181
* **web:** bridging tx status notification (#12932) 6e468ef
* **web:** bridging types (#12748) a473031
* **web:** Give bridge bottom card hover style (#12810) 7be278a
* **web:** improve fingerprinting for swap errors [staging] (#13046) 7cc032c
* **web:** LP create - wrapping native currency step for v2/v3 (#12769) 9c2b4bc
* **web:** pull in completed bridging activity (#12874) 3ac9ca5
* **web:** remove red gas UI on web shared swap (#12947) 8639947
* **web:** remove uniswap extension launch announcement modal (#12791) e1397ce
* **web:** v4 LP improvements (#12737) e111131
* **web:** worldchain bridge banner + minikit provider (#12918) 2439960
* **web:** Zora Explore pages (#12908) 6c0beaf
### Bug Fixes
* **web:** allow pool creation on testnets (#13011) 74160b1
* **web:** 10 15 fix web allow pool creation on testnets staging (#13010) 78815dd
* **web:** Align Continue button text - staging (#13025) 8576cc4
* **web:** align web/mobile quicknode rpcs (#12976) 82adfe5
* **web:** bug with color extraction (#12757) 8fbfa57
* **web:** bump sdk-core for worldchain v2 lp (#12989) 44dd8f3
* **web:** cherrypick bridging analytics [staging] (#13035) 6593430
* **web:** conditionally add dependency array to useAnimatedScrollHandler (#12852) e523bfd
* **web:** correct currency logos in LP Create modal (#12751) a350498
* **web:** create v4 ui (#12638) 1c52b4e
* **web:** default to eth mainnet on landing page (#12958) e81c5ad
* **web:** display bridging options in unconnected state [staging] (#13047) 5a17a2f
* **web:** enable token swap on non mainnet tdp for legacy swap (#12899) a214ab2
* **web:** fix broken worldchain images (#13033) e31db30
* **web:** fix range formatting on my positions page (#12754) 8b13723
* **web:** hide astrochain usdc (#12961) 40ee988
* **web:** limits issues (allow cancelling insufficient funds) (#12912) f630284
* **web:** log missing swap analytics (#12967) 4c22de5
* **web:** log SWAP_SIGNED from uniswapx saga [main] (#12907) 39ccb30
* **web:** memoize connected chain ids (#12922) 3ed664d
* **web:** pass account to getSigner instead of using default (#12885) f508f20
* **web:** remove FOR feature flag and moonpay flow (#12547) b79e301
* **web:** Switch chains before cancelling limits (#12752) 8d2c4f1
* **web:** tapi key (#12866) bcc0529
* **web:** track wallet connect external provider (#12360) ff2c6e7
* **web:** update the toast for increase + decrease liquidity (#12786) c7c898e
* **web:** worldchain rpc (#12946) b1b7528
### Continuous Integration
* **web:** break down web testing jobs to speed up CI (#12766) cb3c56f
* **web:** update sitemaps 336fb63
web/5.52.1
\ No newline at end of file
web/5.53.0
\ No newline at end of file
......@@ -34,6 +34,7 @@ import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { getLocalUserId } from 'src/app/utils/storage'
import {
DappBackgroundPortChannel,
backgroundToSidePanelMessageChannel,
createBackgroundToSidePanelMessagePort,
} from 'src/background/messagePassing/messageChannels'
import { BackgroundToSidePanelRequestType } from 'src/background/messagePassing/types/requests'
......@@ -44,7 +45,7 @@ import { syncAppWithDeviceLanguage } from 'uniswap/src/features/settings/slice'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context'
import { UnitagUpdaterContextProvider, useUnitagUpdater } from 'uniswap/src/features/unitags/context'
import i18n from 'uniswap/src/i18n/i18n'
import { isDevEnv } from 'utilities/src/environment/env'
import { logger } from 'utilities/src/logger/logger'
......@@ -205,10 +206,21 @@ function SidebarWrapper(): JSX.Element {
useDappRequestPortListener()
useTestnetModeForLoggingAndAnalytics()
const { triggerRefetchUnitags } = useUnitagUpdater()
useEffect(() => {
dispatch(syncAppWithDeviceLanguage())
}, [dispatch])
useEffect(() => {
return backgroundToSidePanelMessageChannel.addMessageListener(
BackgroundToSidePanelRequestType.RefreshUnitags,
() => {
triggerRefetchUnitags()
},
)
}, [triggerRefetchUnitags])
return (
<>
<WebNavigation />
......
import { render } from '@testing-library/react'
import UnitagClaimApp from 'src/app/UnitagClaimApp'
import { initializeReduxStore } from 'src/store/store'
describe('UnitagClaimApp', () => {
it('renders without error', async () => {
await initializeReduxStore()
render(<UnitagClaimApp />)
})
})
......@@ -10,10 +10,13 @@ import { GraphqlProvider } from 'src/app/apollo'
import { ErrorElement } from 'src/app/components/ErrorElement'
import { TraceUserProperties } from 'src/app/components/Trace/TraceUserProperties'
import { ClaimUnitagSteps, OnboardingStepsProvider } from 'src/app/features/onboarding/OnboardingSteps'
import { EditUnitagProfileScreen } from 'src/app/features/unitags/EditUnitagProfileScreen'
import { UnitagChooseProfilePicScreen } from 'src/app/features/unitags/UnitagChooseProfilePicScreen'
import { UnitagClaimContextProvider } from 'src/app/features/unitags/UnitagClaimContext'
import { UnitagConfirmationScreen } from 'src/app/features/unitags/UnitagConfirmationScreen'
import { UnitagCreateUsernameScreen } from 'src/app/features/unitags/UnitagCreateUsernameScreen'
import { UnitagIntroScreen } from 'src/app/features/unitags/UnitagIntroScreen'
import { OnboardingRoutes } from 'src/app/navigation/constants'
import { setRouter, setRouterState } from 'src/app/navigation/state'
import { SentryAppNameTag, initializeSentry, sentryCreateHashRouter } from 'src/app/sentry'
import { initExtensionAnalytics } from 'src/app/utils/analytics'
......@@ -45,6 +48,11 @@ const router = sentryCreateHashRouter([
element: <UnitagClaimAppInner />,
errorElement: <ErrorElement />,
},
{
path: OnboardingRoutes.EditProfile,
element: <EditProfileAppInner />,
errorElement: <ErrorElement />,
},
])
/**
......@@ -61,13 +69,30 @@ setRouter(router)
function UnitagClaimAppInner(): JSX.Element {
useTestnetModeForLoggingAndAnalytics()
return (
<Flex alignItems="center" justifyContent="center" minHeight="100vh" width="100%">
<Flex centered height="100vh" width="100%">
<OnboardingStepsProvider
disableRedirect
steps={{
[ClaimUnitagSteps.Intro]: <UnitagIntroScreen />,
[ClaimUnitagSteps.CreateUsername]: <UnitagCreateUsernameScreen />,
[ClaimUnitagSteps.ChooseProfilePic]: <UnitagChooseProfilePicScreen />,
[ClaimUnitagSteps.Confirmation]: <UnitagConfirmationScreen />,
[ClaimUnitagSteps.EditProfile]: <EditUnitagProfileScreen enableBack />,
}}
ContainerComponent={UnitagClaimContextProvider}
/>
<Outlet />
</Flex>
)
}
function EditProfileAppInner(): JSX.Element {
return (
<Flex centered>
<OnboardingStepsProvider
disableRedirect
steps={{
[ClaimUnitagSteps.Intro]: <EditUnitagProfileScreen />,
}}
ContainerComponent={UnitagClaimContextProvider}
/>
......
......@@ -10,13 +10,13 @@ import { iconSizes } from 'ui/src/theme'
import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal'
import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types'
import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType, CopyNotificationType } from 'uniswap/src/features/notifications/types'
import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { setClipboard } from 'uniswap/src/utils/clipboard'
import { NumberType } from 'utilities/src/format/types'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types'
import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga'
import { useActiveAccountWithThrow, useDisplayName, useSignerAccounts } from 'wallet/src/features/wallet/hooks'
import { DisplayNameType } from 'wallet/src/features/wallet/types'
......
......@@ -11,14 +11,17 @@ import { updateDappConnectedAddressFromExtension } from 'src/app/features/dapp/a
import { useDappConnectedAccounts } from 'src/app/features/dapp/hooks'
import { isConnectedAccount } from 'src/app/features/dapp/utils'
import { PopupName, openPopup } from 'src/app/features/popups/slice'
import { AppRoutes, RemoveRecoveryPhraseRoutes, SettingsRoutes } from 'src/app/navigation/constants'
import { AppRoutes, OnboardingRoutes, RemoveRecoveryPhraseRoutes, SettingsRoutes } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state'
import { focusOrCreateUnitagTab } from 'src/app/navigation/utils'
import { Button, Flex, MenuContent, MenuContentItem, Popover, ScrollView, Text, useSporeColors } from 'ui/src'
import { WalletFilled, X } from 'ui/src/components/icons'
import { spacing } from 'ui/src/theme'
import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal'
import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types'
import { AccountType } from 'uniswap/src/features/accounts/types'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ModalName, WalletEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
......@@ -281,6 +284,20 @@ export function AccountSwitcherScreen(): JSX.Element {
const UnitagActionButton = (): JSX.Element => {
const { t } = useTranslation()
const isClaimUnitagEnabled = useFeatureFlag(FeatureFlags.ExtensionClaimUnitag)
const onPressEditProfile = useCallback(async () => {
await focusOrCreateUnitagTab(OnboardingRoutes.EditProfile)
}, [])
if (isClaimUnitagEnabled) {
return (
<Button color="$neutral1" size="small" testID={TestID.AccountCard} theme="tertiary" onPress={onPressEditProfile}>
{t('account.wallet.header.button.disabled.title')}
</Button>
)
}
return (
<ComingSoon placement="top">
<Button color="$neutral2" disabled={true} size="small" testID={TestID.AccountCard} theme="secondary">
......
......@@ -99,7 +99,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
<circle
cx="28"
cy="28"
fill="#4300B01F"
fill="#FFBF171F"
r="28"
/>
<g
......@@ -107,8 +107,8 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
>
<path
clip-rule="evenodd"
d="M13.2 6C9.22355 6 6 9.22355 6 13.2V34.8C6 38.7765 9.22355 42 13.2 42H34.8C38.7765 42 42 38.7765 42 34.8V13.2C42 9.22355 38.7765 6 34.8 6H13.2ZM26.1213 14.1213C24.9497 12.9497 23.0503 12.9497 21.8787 14.1213L14.1213 21.8787C12.9497 23.0503 12.9497 24.9497 14.1213 26.1213L21.8787 33.8787C23.0503 35.0503 24.9497 35.0503 26.1213 33.8787L33.8787 26.1213C35.0503 24.9497 35.0503 23.0503 33.8787 21.8787L26.1213 14.1213Z"
fill="#4300B0"
d="M28.541 24C28.541 23.6075 28.7998 23.264 29.1711 23.1367C35.8814 20.8368 40.9702 14.1754 41.9802 5.99363C42.1832 4.34926 40.8202 3 39.1634 3H8.8369C7.18005 3 5.81706 4.34926 6.02011 5.99363C7.03026 14.1742 12.1192 20.8349 18.8286 23.1357C19.2002 23.2632 19.4593 23.6071 19.4593 24C19.4593 24.3929 19.2002 24.7368 18.8286 24.8643C12.1192 27.1651 7.03026 33.8258 6.02011 42.0064C5.81706 43.6507 7.18005 45 8.8369 45H39.1634C40.8202 45 42.1832 43.6507 41.9802 42.0064C40.9702 33.8246 35.8814 27.1632 29.1711 24.8633C28.7998 24.736 28.541 24.3925 28.541 24Z"
fill="#FFBF17"
fill-rule="evenodd"
/>
</g>
......@@ -127,9 +127,9 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
<span
class="font_heading _display-inline _boxSizing-border-box _whiteSpace-nowrap _mt-0px _mr-0px _mb-0px _ml-0px _color-843134974 _wordWrap-break-word _fontFamily-299667014 _fontSize-18px _lineHeight-24px _fontWeight-233016202 _maxWidth-10037 _overflowX-hidden _overflowY-hidden _textOverflow-ellipsis _textAlign-center _flexShrink-1"
data-disable-theme="true"
data-testid="address-display/name/Jacob Haley"
data-testid="address-display/name/Tamara Brekke"
>
Jacob Haley
Tamara Brekke
</span>
</div>
</div>
......@@ -144,7 +144,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
class="font_body _display-inline _boxSizing-border-box _whiteSpace-pre-wrap _mt-0px _mr-0px _mb-0px _ml-0px _color-843135005 _fontFamily-299667014 _wordWrap-break-word _fontSize-229441158 _lineHeight-222976511 _fontWeight-233016202"
data-disable-theme="true"
>
0x​0fc6...be59
0x​e0c6...ea11
</span>
<svg
fill="none"
......@@ -325,7 +325,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
<circle
cx="28"
cy="28"
fill="#4300B01F"
fill="#FFBF171F"
r="28"
/>
<g
......@@ -333,8 +333,8 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
>
<path
clip-rule="evenodd"
d="M13.2 6C9.22355 6 6 9.22355 6 13.2V34.8C6 38.7765 9.22355 42 13.2 42H34.8C38.7765 42 42 38.7765 42 34.8V13.2C42 9.22355 38.7765 6 34.8 6H13.2ZM26.1213 14.1213C24.9497 12.9497 23.0503 12.9497 21.8787 14.1213L14.1213 21.8787C12.9497 23.0503 12.9497 24.9497 14.1213 26.1213L21.8787 33.8787C23.0503 35.0503 24.9497 35.0503 26.1213 33.8787L33.8787 26.1213C35.0503 24.9497 35.0503 23.0503 33.8787 21.8787L26.1213 14.1213Z"
fill="#4300B0"
d="M28.541 24C28.541 23.6075 28.7998 23.264 29.1711 23.1367C35.8814 20.8368 40.9702 14.1754 41.9802 5.99363C42.1832 4.34926 40.8202 3 39.1634 3H8.8369C7.18005 3 5.81706 4.34926 6.02011 5.99363C7.03026 14.1742 12.1192 20.8349 18.8286 23.1357C19.2002 23.2632 19.4593 23.6071 19.4593 24C19.4593 24.3929 19.2002 24.7368 18.8286 24.8643C12.1192 27.1651 7.03026 33.8258 6.02011 42.0064C5.81706 43.6507 7.18005 45 8.8369 45H39.1634C40.8202 45 42.1832 43.6507 41.9802 42.0064C40.9702 33.8246 35.8814 27.1632 29.1711 24.8633C28.7998 24.736 28.541 24.3925 28.541 24Z"
fill="#FFBF17"
fill-rule="evenodd"
/>
</g>
......@@ -353,9 +353,9 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
<span
class="font_heading _display-inline _boxSizing-border-box _whiteSpace-nowrap _mt-0px _mr-0px _mb-0px _ml-0px _color-843134974 _wordWrap-break-word _fontFamily-299667014 _fontSize-18px _lineHeight-24px _fontWeight-233016202 _maxWidth-10037 _overflowX-hidden _overflowY-hidden _textOverflow-ellipsis _textAlign-center _flexShrink-1"
data-disable-theme="true"
data-testid="address-display/name/Jacob Haley"
data-testid="address-display/name/Tamara Brekke"
>
Jacob Haley
Tamara Brekke
</span>
</div>
</div>
......@@ -370,7 +370,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `
class="font_body _display-inline _boxSizing-border-box _whiteSpace-pre-wrap _mt-0px _mr-0px _mb-0px _ml-0px _color-843135005 _fontFamily-299667014 _wordWrap-break-word _fontSize-229441158 _lineHeight-222976511 _fontWeight-233016202"
data-disable-theme="true"
>
0x​0fc6...be59
0x​e0c6...ea11
</span>
<svg
fill="none"
......
......@@ -5,7 +5,6 @@ import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRe
import { DappRequestStoreItem } from 'src/app/features/dappRequests/slice'
import { Anchor, AnimatePresence, Button, Flex, Text, UniversalImage, UniversalImageResizeMode, styled } from 'ui/src'
import { borderRadii, iconSizes } from 'ui/src/theme'
import { useUSDValueOfGasFee } from 'uniswap/src/features/gas/hooks'
import { GasFeeResult } from 'uniswap/src/features/gas/types'
import { hasSufficientFundsIncludingGas } from 'uniswap/src/features/gas/utils'
import { useOnChainNativeCurrencyBalance } from 'uniswap/src/features/portfolio/api'
......@@ -175,7 +174,6 @@ export function DappRequestFooter({
}
const currentChainId = chainId || activeChain || defaultChainId
const { value: gasFeeUSD } = useUSDValueOfGasFee(currentChainId, transactionGasFeeResult?.value)
const { balance: nativeBalance } = useOnChainNativeCurrencyBalance(currentChainId, currentAccount.address)
const hasSufficientGas = hasSufficientFundsIncludingGas({
......@@ -184,7 +182,9 @@ export function DappRequestFooter({
})
const shouldCloseSidebar = request.isSidebarClosed && totalRequestCount <= 1
const isConfirmDisabled = transactionGasFeeResult ? !gasFeeUSD || !hasSufficientGas : false
// Disable submission if no gas fee value
const isConfirmEnabled = transactionGasFeeResult?.value && hasSufficientGas
const handleOnConfirm = useCallback(async () => {
if (onConfirm) {
......@@ -240,7 +240,7 @@ export function DappRequestFooter({
{t('common.button.cancel')}
</Button>
<Button
disabled={isConfirmDisabled}
disabled={!isConfirmEnabled}
flex={1}
flexBasis={1}
size="medium"
......
......@@ -17,12 +17,12 @@ import { extractBaseUrl } from 'src/app/features/dappRequests/utils'
import { dappResponseMessageChannel } from 'src/background/messagePassing/messageChannels'
import { call, put, select } from 'typed-redux-saga'
import { chainIdToHexadecimalString } from 'uniswap/src/features/chains/utils'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import { getEnabledChainIdsSaga } from 'uniswap/src/features/settings/saga'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { UniverseChainId } from 'uniswap/src/types/chains'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType } from 'wallet/src/features/notifications/types'
import { getProvider } from 'wallet/src/features/wallet/context'
import { selectActiveAccount } from 'wallet/src/features/wallet/selectors'
......
......@@ -21,8 +21,8 @@ import { Permission } from 'src/contentScript/WindowEthereumRequestTypes'
import { ExtensionEthMethods } from 'src/contentScript/methodHandlers/requestMethods'
import { call, put } from 'typed-redux-saga'
import { chainIdToHexadecimalString } from 'uniswap/src/features/chains/utils'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType } from 'wallet/src/features/notifications/types'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types'
export function getPermissions(dappUrl: string | undefined, connectedAddresses: Address[] | undefined): Permission[] {
const permissions: Permission[] = []
......
......@@ -9,9 +9,9 @@ import { useCopyToClipboard } from 'src/app/hooks/useOnCopyToClipboard'
import { Anchor, Flex, Text, TouchableArea } from 'ui/src'
import { AnimatedCopySheets, ExternalLink } from 'ui/src/components/icons'
import { GasFeeResult } from 'uniswap/src/features/gas/types'
import { CopyNotificationType } from 'uniswap/src/features/notifications/types'
import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking'
import { ellipseMiddle, shortenAddress } from 'utilities/src/addresses'
import { CopyNotificationType } from 'wallet/src/features/notifications/types'
import { ContentRow } from 'wallet/src/features/transactions/TransactionRequest/ContentRow'
import {
SpendingDetails,
......
......@@ -6,6 +6,7 @@ import { formatUnits as formatUnitsEthers } from 'ethers/lib/utils'
import { useDappLastChainId } from 'src/app/features/dapp/hooks'
import {
CONTRACT_BALANCE,
ETH_ADDRESS,
MAX_UINT160,
MAX_UINT256,
} from 'src/app/features/dappRequests/requestContent/EthSend/Swap/constants'
......@@ -24,10 +25,14 @@ import {
V4SwapExactOutSingleParamSchema,
isAmountInMaxParam,
isAmountInParam,
isAmountMinParam,
isAmountOutMinParam,
isAmountOutParam,
isURCommandASwap,
isUrCommandSweep,
isUrCommandUnwrapWeth,
} from 'src/app/features/dappRequests/types/UniversalRouterTypes'
import { DEFAULT_NATIVE_ADDRESS } from 'uniswap/src/constants/chains'
import { buildCurrencyId } from 'uniswap/src/utils/currencyId'
import { assert } from 'utilities/src/errors'
......@@ -114,8 +119,8 @@ export function useSwapDetails(
if (v4Command) {
// Extract details using the V4 helper
const v4Details = getTokenDetailsFromV4SwapCommands(v4Command)
inputAddress = v4Details.inputAddress
outputAddress = v4Details.outputAddress
inputAddress = v4Details.inputAddress === ETH_ADDRESS ? DEFAULT_NATIVE_ADDRESS : v4Details.inputAddress
outputAddress = v4Details.outputAddress === ETH_ADDRESS ? DEFAULT_NATIVE_ADDRESS : v4Details.outputAddress
inputValue = v4Details.inputValue || '0'
outputValue = v4Details.outputValue || '0'
} else {
......@@ -157,12 +162,28 @@ function extractTokenAddresses(commands: UniversalRouterCommand[]): {
return { inputAddress, outputAddress }
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function isZeroBigNumber(bigNumberObj: any): boolean {
// The true type of bigNumberObj is { type: string; hex: string } but param.value is any type
try {
if (!bigNumberObj) {
return true
}
const bigNumber = BigNumber.from(bigNumberObj.hex)
return bigNumber.isZero()
} catch (error) {
return true // Treat as zero if there's any error
}
}
function getTokenAmounts(commands: UniversalRouterCommand[]): {
inputValue: string
outputValue: string
} {
const firstSwapCommand = commands.find(isURCommandASwap)
const lastSwapCommand = commands.findLast(isURCommandASwap)
const sweepCommand = commands.find(isUrCommandSweep)
const unwrapWethCommand = commands.find(isUrCommandUnwrapWeth)
assert(
firstSwapCommand && lastSwapCommand,
......@@ -171,15 +192,24 @@ function getTokenAmounts(commands: UniversalRouterCommand[]): {
const firstAmountInParam = firstSwapCommand?.params.find(isAmountInOrMaxParam)
const lastAmountOutParam = lastSwapCommand?.params.find(isAmountOutMinOrOutParam)
const sweepAmountOutParam = sweepCommand?.params.find(isAmountMinParam)
const unwrapWethAmountOutParam = unwrapWethCommand?.params.find(isAmountMinParam)
assert(
firstAmountInParam && lastAmountOutParam,
'SwapRequestContent: All swaps must have a defined input and output amount parameter.',
)
// There's a special case where V3_SWAP command's amountOutMin param is zero (0x00... some gas optimization slippage thing)
// In this case fallback to the amountMin from the SWEEP or UNWRAP_WETH command as the outputValue
const inputValue = firstAmountInParam?.value
const fallbackOutputValue = sweepAmountOutParam?.value || unwrapWethAmountOutParam?.value
const outputValue =
fallbackOutputValue && isZeroBigNumber(lastAmountOutParam?.value) ? fallbackOutputValue : lastAmountOutParam?.value
return {
inputValue: firstAmountInParam?.value || '0', // Safe due to assert
outputValue: lastAmountOutParam?.value || '0', // Safe due to assert
inputValue: inputValue || '0', // Safe due to assert
outputValue: outputValue || '0', // Safe due to assert
}
}
......
......@@ -28,14 +28,14 @@ import { navigate } from 'src/app/navigation/state'
import { dappResponseMessageChannel } from 'src/background/messagePassing/messageChannels'
import { call, put, select, take } from 'typed-redux-saga'
import { hexadecimalStringToInt, toSupportedChainId } from 'uniswap/src/features/chains/utils'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import {
TransactionOriginType,
TransactionType,
TransactionTypeInfo,
} from 'uniswap/src/features/transactions/types/transactionDetails'
import { logger } from 'utilities/src/logger/logger'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType } from 'wallet/src/features/notifications/types'
import { SendTransactionParams, sendTransaction } from 'wallet/src/features/transactions/sendTransactionSaga'
import { getProvider, getSignerManager } from 'wallet/src/features/wallet/context'
import { selectActiveAccount } from 'wallet/src/features/wallet/selectors'
......
......@@ -41,6 +41,13 @@ const AmountOutParamSchema = z.object({
})
export type AmountOutParam = z.infer<typeof AmountOutParamSchema>
const AmountMinParamSchema = z.object({
name: z.literal('amountMin'),
value: BigNumberSchema,
})
export type AmountMinParam = z.infer<typeof AmountMinParamSchema>
const FeeAmountSchema = z.nativeEnum(FeeAmountV3)
export type FeeAmount = z.infer<typeof FeeAmountSchema>
......@@ -188,6 +195,7 @@ export const ParamSchema = z.union([
AmountInMaxParamSchema,
AmountOutParamSchema,
AmountOutMinParamSchema,
AmountMinParamSchema,
V3PathParamSchema,
PayerIsUserParamSchema,
FallbackParamSchema,
......@@ -229,6 +237,21 @@ const V3SwapExactOutCommandSchema = z.object({
})
export type V3SwapExactOutCommand = z.infer<typeof V3SwapExactOutCommandSchema>
const SweepCommandSchema = z.object({
commandName: z.literal('SWEEP'),
commandType: z.literal(CommandType.SWEEP),
params: z.array(ParamSchema),
})
export type SweepCommand = z.infer<typeof SweepCommandSchema>
const UnwrapWethCommandSchema = z.object({
commandName: z.literal('UNWRAP_WETH'),
commandType: z.literal(CommandType.UNWRAP_WETH),
params: z.array(ParamSchema),
})
export type UnwrapWethCommand = z.infer<typeof UnwrapWethCommandSchema>
const V4SwapCommandSchema = z.object({
commandName: z.literal('V4_SWAP'),
commandType: z.literal(CommandType.V4_SWAP),
......@@ -246,7 +269,7 @@ export const UniversalRouterSwapCommandSchema = z.union([
V2SwapExactOutCommandSchema,
V3SwapExactInCommandSchema,
V3SwapExactOutCommandSchema,
V4SwapCommandSchema
V4SwapCommandSchema,
])
export type UniversalRouterSwapCommand = z.infer<typeof UniversalRouterSwapCommandSchema>
......@@ -256,7 +279,9 @@ const UniversalRouterCommandSchema = z.union([
V2SwapExactOutCommandSchema,
V3SwapExactInCommandSchema,
V3SwapExactOutCommandSchema,
V4SwapCommandSchema
V4SwapCommandSchema,
SweepCommandSchema,
UnwrapWethCommandSchema
])
export type UniversalRouterCommand = z.infer<typeof UniversalRouterCommandSchema>
......@@ -272,6 +297,14 @@ export function isURCommandASwap(
return UniversalRouterSwapCommandSchema.safeParse(command).success
}
export function isUrCommandSweep(command: UniversalRouterCommand): command is SweepCommand {
return SweepCommandSchema.safeParse(command).success
}
export function isUrCommandUnwrapWeth(command: UniversalRouterCommand): command is UnwrapWethCommand {
return UnwrapWethCommandSchema.safeParse(command).success
}
export function isAmountInParam(param: Param): param is AmountInParam {
return AmountInParamSchema.safeParse(param).success
}
......@@ -287,3 +320,7 @@ export function isAmountOutMinParam(param: Param): param is AmountOutMinParam {
export function isAmountOutParam(param: Param): param is AmountOutParam {
return AmountOutParamSchema.safeParse(param).success
}
export function isAmountMinParam(param: Param): param is AmountMinParam {
return AmountMinParamSchema.safeParse(param).success
}
......@@ -16,14 +16,14 @@ import { useOptimizedSearchParams } from 'src/app/hooks/useOptimizedSearchParams
import { HomeQueryParams, HomeTabs } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state'
import { Flex, Loader, Text, TouchableArea, styled } from 'ui/src'
import { useSelectAddressHasNotifications } from 'uniswap/src/features/notifications/hooks'
import { setNotificationStatus } from 'uniswap/src/features/notifications/slice'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { logger } from 'utilities/src/logger/logger'
import { ONE_MINUTE_MS, ONE_SECOND_MS } from 'utilities/src/time/time'
import { useTimeout } from 'utilities/src/time/timing'
import { NFTS_TAB_DATA_DEPENDENCIES } from 'wallet/src/components/nfts/NftsList'
import { PendingNotificationBadge } from 'wallet/src/features/notifications/components/PendingNotificationBadge'
import { useSelectAddressHasNotifications } from 'wallet/src/features/notifications/hooks'
import { setNotificationStatus } from 'wallet/src/features/notifications/slice'
import { PortfolioBalance } from 'wallet/src/features/portfolio/PortfolioBalance'
import { useHeartbeatReporter, useLastBalancesReporter } from 'wallet/src/features/telemetry/hooks'
import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks'
......@@ -230,7 +230,7 @@ const AnimatedTab = styled(Flex, {
true: {},
false: {
pointerEvents: 'none',
maxHeight: 300,
display: 'none',
},
},
......
......@@ -15,6 +15,8 @@ import { animationPresets } from 'ui/src/animations'
import { CopyAlt, Globe, RotatableChevron, Settings } from 'ui/src/components/icons'
import { borderRadii, iconSizes } from 'ui/src/theme'
import { useAvatar } from 'uniswap/src/features/address/avatar'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType, CopyNotificationType } from 'uniswap/src/features/notifications/types'
import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
......@@ -27,8 +29,6 @@ import { DappIconPlaceholder } from 'wallet/src/components/WalletConnect/DappIco
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
import { AnimatedUnitagDisplayName } from 'wallet/src/components/accounts/AnimatedUnitagDisplayName'
import useIsFocused from 'wallet/src/features/focus/useIsFocused'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types'
import { useDisplayName } from 'wallet/src/features/wallet/hooks'
import { DisplayNameType } from 'wallet/src/features/wallet/types'
......
......@@ -11,12 +11,12 @@ import { usePreventOverflowBelowFold } from 'ui/src/hooks/usePreventOverflowBelo
import { iconSizes } from 'ui/src/theme'
import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo'
import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import { useEnabledChains } from 'uniswap/src/features/settings/hooks'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { UniverseChainId } from 'uniswap/src/types/chains'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType } from 'wallet/src/features/notifications/types'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
const BUTTON_OFFSET = 20
......
......@@ -11,6 +11,7 @@ import { InfoLinkModal } from 'uniswap/src/components/modals/InfoLinkModal'
import { uniswapUrls } from 'uniswap/src/constants/urls'
import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { PortfolioBalance } from 'uniswap/src/features/dataApi/types'
import { useEnabledChains } from 'uniswap/src/features/settings/hooks'
import { ElementName, ModalName, SectionName, WalletEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { InformationBanner } from 'wallet/src/components/banners/InformationBanner'
......@@ -36,7 +37,13 @@ type TokenBalanceListProps = {
export const TokenBalanceList = memo(function _TokenBalanceList({ owner }: TokenBalanceListProps): JSX.Element {
const { navigateToTokenDetails } = useWalletNavigation()
const { isTestnetModeEnabled } = useEnabledChains()
const onPressToken = (currencyId: string): void => {
if (isTestnetModeEnabled) {
return
}
sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, {
element: ElementName.TokenItem,
section: SectionName.HomeTokensTab,
......
import { useCallback } from 'react'
import { focusOrCreateUnitagClaimTab } from 'src/app/navigation/utils'
import { focusOrCreateUnitagTab } from 'src/app/navigation/utils'
import { Flex } from 'ui/src'
import { PollingInterval } from 'uniswap/src/constants/misc'
import { AccountType } from 'uniswap/src/features/accounts/types'
......@@ -18,7 +18,7 @@ export function HomeIntroCardStack(): JSX.Element | null {
})
const navigateToUnitagClaim = useCallback(async () => {
await focusOrCreateUnitagClaimTab()
await focusOrCreateUnitagTab()
}, [])
const { cards } = useSharedIntroCards({
......
import { useSelector } from 'react-redux'
import { useSelectAddressNotifications } from 'uniswap/src/features/notifications/hooks'
import { AppNotification, AppNotificationType } from 'uniswap/src/features/notifications/types'
import { DappConnectedNotification } from 'wallet/src/features/notifications/components/DappConnectedNotification'
import { DappDisconnectedNotification } from 'wallet/src/features/notifications/components/DappDisconnectedNotification'
import { NotSupportedNetworkNotification } from 'wallet/src/features/notifications/components/NotSupportedNetworkNotification'
import { PasswordChangedNotification } from 'wallet/src/features/notifications/components/PasswordChangedNotification'
import { SharedNotificationToastRouter } from 'wallet/src/features/notifications/components/SharedNotificationToastRouter'
import { selectActiveAccountNotifications } from 'wallet/src/features/notifications/selectors'
import { AppNotification, AppNotificationType } from 'wallet/src/features/notifications/types'
import { useActiveAccountAddress } from 'wallet/src/features/wallet/hooks'
export function NotificationToastWrapper(): JSX.Element | null {
const notifications = useSelector(selectActiveAccountNotifications)
const activeAccountAddress = useActiveAccountAddress()
const notifications = useSelectAddressNotifications(activeAccountAddress)
const notification = notifications?.[0]
if (!notification) {
......
......@@ -17,6 +17,8 @@ export function OnboardingScreenFrame({
subtitle,
title,
warningSubtitle,
endAdornment,
noTopPadding,
}: Partial<OnboardingScreenProps>): JSX.Element {
const { t } = useTranslation()
......@@ -26,7 +28,7 @@ export function OnboardingScreenFrame({
return (
<>
<Flex alignItems="center" gap="$spacing16" pt="$spacing24">
<Flex alignItems="center" gap="$spacing16" pt={noTopPadding || '$spacing24'}>
{onBack && (
<TouchableArea
hoverable
......@@ -56,6 +58,11 @@ export function OnboardingScreenFrame({
</Text>
</TouchableArea>
)}
{endAdornment && (
<TouchableArea position="absolute" right="$none" top="$none">
{endAdornment}
</TouchableArea>
)}
{Icon}
<Flex alignItems="center" gap="$spacing4" px="$spacing24">
<Text textAlign="center" variant="subheading1">
......
......@@ -11,8 +11,10 @@ export type OnboardingScreenProps = {
onSubmit?: () => void
onSkip?: () => void
subtitle?: string
title: string | JSX.Element
title?: string | JSX.Element
warningSubtitle?: string
outsideContent?: JSX.Element
belowFrameContent?: JSX.Element
endAdornment?: JSX.Element
noTopPadding?: boolean
}
......@@ -37,6 +37,8 @@ export enum ClaimUnitagSteps {
Intro = 'intro',
CreateUsername = 'createUsername',
ChooseProfilePic = 'chooseProfilePic',
EditProfile = 'editProfile',
Confirmation = 'confirmation',
}
export type Step = CreateOnboardingSteps | ImportOnboardingSteps | ResetSteps | ScanOnboardingSteps | ClaimUnitagSteps
......
......@@ -11,14 +11,14 @@ import { NoDappConnections } from 'src/app/features/settings/SettingsManageConne
import { Flex, Text, TouchableArea, UniversalImage, useSporeColors } from 'ui/src'
import { MinusCircle } from 'ui/src/components/icons'
import { borderRadii, breakpoints, iconSizes } from 'ui/src/theme'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { ExtensionScreens } from 'uniswap/src/types/screens/extension'
import { extractNameFromUrl } from 'utilities/src/format/extractNameFromUrl'
import { DappIconPlaceholder } from 'wallet/src/components/WalletConnect/DappIconPlaceholder'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType } from 'wallet/src/features/notifications/types'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
const MIN_SCREEN_WIDTH = breakpoints.xxs
......
......@@ -3,10 +3,10 @@ import { useDispatch } from 'react-redux'
import { removeAllDappConnectionsForAccount } from 'src/app/features/dapp/actions'
import { ContextMenu, Flex, TouchableArea } from 'ui/src'
import { Ellipsis, Power } from 'ui/src/components/icons'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType } from 'wallet/src/features/notifications/types'
import { useActiveAccountAddress, useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
const PowerCircle = (): JSX.Element => (
......
......@@ -239,7 +239,9 @@ function SeedPhraseWord({
<Text color="$neutral3" minWidth={indexMinWidth} variant="body2" onLayout={onIndexLayout}>
{index}
</Text>
<Text variant="body2">{word}</Text>
<Text variant="body2" className="notranslate">
{word}
</Text>
</Flex>
)
}
......@@ -3,10 +3,10 @@ import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import { PADDING_STRENGTH_INDICATOR, PasswordInput } from 'src/app/components/PasswordInput'
import { Button, Flex, Text } from 'ui/src'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType } from 'wallet/src/features/notifications/types'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
import { usePasswordForm } from 'wallet/src/utils/password'
......
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { OnboardingScreen } from 'src/app/features/onboarding/OnboardingScreen'
import { useOnboardingSteps } from 'src/app/features/onboarding/OnboardingStepsContext'
import { AnimatePresence, ContextMenu, Flex, MenuContentItem } from 'ui/src'
import { Edit, Ellipsis, Trash } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks'
import { UnitagScreens } from 'uniswap/src/types/screens/mobile'
import { ChangeUnitagModal } from 'wallet/src/features/unitags/ChangeUnitagModal'
import { DeleteUnitagModal } from 'wallet/src/features/unitags/DeleteUnitagModal'
import { EditUnitagProfileContent } from 'wallet/src/features/unitags/EditUnitagProfileContent'
import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks'
export function EditUnitagProfileScreen({ enableBack = false }: { enableBack?: boolean }): JSX.Element {
const { t } = useTranslation()
const address = useActiveAccountAddressWithThrow()
const { unitag: retrievedUnitag } = useUnitagByAddress(address)
const unitag = retrievedUnitag?.username
const { goToPreviousStep } = useOnboardingSteps()
const [showDeleteUnitagModal, setShowDeleteUnitagModal] = useState(false)
const [showChangeUnitagModal, setShowChangeUnitagModal] = useState(false)
const menuOptions = useMemo((): MenuContentItem[] => {
return [
{
label: t('unitags.profile.action.edit'),
onPress: (): void => setShowChangeUnitagModal(true),
Icon: Edit,
},
{
label: t('unitags.profile.action.delete'),
onPress: (): void => setShowDeleteUnitagModal(true),
Icon: Trash,
destructive: true,
},
]
}, [t, setShowChangeUnitagModal, setShowDeleteUnitagModal])
return (
<Trace logImpression screen={UnitagScreens.EditProfile}>
<OnboardingScreen
noTopPadding
title={t('settings.setting.wallet.action.editProfile')}
endAdornment={
<ContextMenu closeOnClick itemId={address} menuOptions={menuOptions} onLeftClick>
<Flex>
<Ellipsis color="$neutral2" size={iconSizes.icon24} />
</Flex>
</ContextMenu>
}
onBack={enableBack ? goToPreviousStep : undefined}
>
<Flex gap="$spacing12" width="100%" pt="$spacing8">
{unitag && (
<>
<EditUnitagProfileContent address={address} unitag={unitag} entryPoint={UnitagScreens.EditProfile} />
<AnimatePresence>
{showDeleteUnitagModal && (
<DeleteUnitagModal
address={address}
unitag={unitag}
onClose={(): void => setShowDeleteUnitagModal(false)}
/>
)}
{showChangeUnitagModal && (
<ChangeUnitagModal
address={address}
unitag={unitag}
onClose={(): void => setShowChangeUnitagModal(false)}
/>
)}
</AnimatePresence>
</>
)}
</Flex>
</OnboardingScreen>
</Trace>
)
}
......@@ -3,25 +3,30 @@ import { useCallback, useEffect } from 'react'
import { OnboardingScreen } from 'src/app/features/onboarding/OnboardingScreen'
import { useOnboardingSteps } from 'src/app/features/onboarding/OnboardingStepsContext'
import { useUnitagClaimContext } from 'src/app/features/unitags/UnitagClaimContext'
import { backgroundToSidePanelMessageChannel } from 'src/background/messagePassing/messageChannels'
import { BackgroundToSidePanelRequestType } from 'src/background/messagePassing/types/requests'
import { Flex, Square } from 'ui/src'
import { Person } from 'ui/src/components/icons'
import { fonts, iconSizes, spacing } from 'ui/src/theme'
import { fonts, iconSizes } from 'ui/src/theme'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ExtensionUnitagClaimScreens } from 'uniswap/src/types/screens/extension'
import { logger } from 'utilities/src/logger/logger'
import { extensionNftModalProps } from 'wallet/src/features/unitags/ChooseNftModal'
import { UnitagChooseProfilePicContent } from 'wallet/src/features/unitags/UnitagChooseProfilePicContent'
import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks'
const NFT_MODAL_MAX_WIDTH = 610
export function UnitagChooseProfilePicScreen(): JSX.Element {
const { goToNextStep, goToPreviousStep } = useOnboardingSteps()
const { unitag, entryPoint, setProfilePicUri } = useUnitagClaimContext()
const address = useActiveAccountAddressWithThrow()
const onNavigateContinue = useCallback(
(imageUri: string | undefined) => {
async (imageUri: string | undefined) => {
setProfilePicUri(imageUri)
// TODO WALL-5067 move claim logic out of UnitagChooseProfilePicContent and integrate message sending
await backgroundToSidePanelMessageChannel.sendMessage({
type: BackgroundToSidePanelRequestType.RefreshUnitags,
})
goToNextStep()
},
[setProfilePicUri, goToNextStep],
......@@ -52,18 +57,12 @@ export function UnitagChooseProfilePicScreen(): JSX.Element {
>
<Flex gap="$spacing24" pt="$spacing24" width="100%">
<UnitagChooseProfilePicContent
shouldHandleClaim
entryPoint={entryPoint}
address={address}
unitag={unitag ?? ''}
shouldHandleClaim={false}
unitagFontSize={fonts.heading3.fontSize}
nftModalProps={{
includeContextMenu: false,
itemMargin: '$spacing6',
containerProps: { m: -spacing.spacing6 }, // Cancels out the margin on each NFT item
modalMaxWidth: NFT_MODAL_MAX_WIDTH,
numColumns: 4,
}}
nftModalProps={extensionNftModalProps}
onContinue={onNavigateContinue}
/>
</Flex>
......
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { OnboardingScreen } from 'src/app/features/onboarding/OnboardingScreen'
import { useOnboardingSteps } from 'src/app/features/onboarding/OnboardingStepsContext'
import { useUnitagClaimContext } from 'src/app/features/unitags/UnitagClaimContext'
import { closeCurrentTab } from 'src/app/navigation/utils'
import { Button, Flex, Text } from 'ui/src'
import { logger } from 'utilities/src/logger/logger'
import { UnitagWithProfilePicture } from 'wallet/src/features/unitags/UnitagWithProfilePicture'
import { UNITAG_SUFFIX } from 'wallet/src/features/unitags/constants'
import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks'
export function UnitagConfirmationScreen(): JSX.Element {
const { t } = useTranslation()
const address = useActiveAccountAddressWithThrow()
const { unitag, profilePicUri } = useUnitagClaimContext()
const { goToNextStep } = useOnboardingSteps()
const onPressCustomize = (): void => {
// Assumes edit profile screen is next step. Uses onboarding steps for consistent nav animation
goToNextStep()
}
useEffect(() => {
if (!unitag) {
logger.warn('UnitagConfirmationScreen.tsx', 'render', 'unitag is empty when it should have a value')
}
}, [unitag])
if (!unitag) {
return <></>
}
return (
<OnboardingScreen>
<Flex grow gap="$spacing12" pt="$spacing24">
<Flex centered>
<UnitagWithProfilePicture address={address} profilePictureUri={profilePicUri} unitag={unitag} />
</Flex>
<Flex centered gap="$spacing12">
<Text color="$neutral1" textAlign="center" variant="heading3">
{t('unitags.claim.confirmation.success.long')}
</Text>
<Text color="$neutral2" textAlign="center" variant="subheading2">
{t('unitags.claim.confirmation.description', {
unitagAddress: `${unitag}${UNITAG_SUFFIX}`,
})}
</Text>
</Flex>
<Flex gap="$spacing12" pt="$spacing12">
<Button size="medium" theme="primary" onPress={closeCurrentTab}>
{t('common.button.done')}
</Button>
<Button size="medium" theme="secondary" onPress={onPressCustomize}>
{t('unitags.claim.confirmation.customize')}
</Button>
</Flex>
</Flex>
</OnboardingScreen>
)
}
import { useCallback } from 'react'
import { useDispatch } from 'react-redux'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType, CopyNotificationType } from 'uniswap/src/features/notifications/types'
export function useCopyToClipboard(): ({
textToCopy,
......
......@@ -5,6 +5,7 @@ import { useCopyToClipboard } from 'src/app/hooks/useOnCopyToClipboard'
import { AppRoutes, HomeQueryParams, HomeTabs } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state'
import { SidebarLocationState, focusOrCreateTokensExploreTab } from 'src/app/navigation/utils'
import { CopyNotificationType } from 'uniswap/src/features/notifications/types'
import { useEnabledChains } from 'uniswap/src/features/settings/hooks'
import { WalletEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
......@@ -22,7 +23,6 @@ import {
getNavigateToSendFlowArgsInitialState,
getNavigateToSwapFlowArgsInitialState,
} from 'wallet/src/contexts/WalletNavigationContext'
import { CopyNotificationType } from 'wallet/src/features/notifications/types'
import { getNftUrl, getTokenUrl } from 'wallet/src/utils/linking'
export function SideBarNavigationProvider({ children }: PropsWithChildren): JSX.Element {
......
......@@ -13,6 +13,7 @@ export enum OnboardingRoutes {
Reset = 'reset',
ResetScan = 'reset-scan',
UnsupportedBrowser = 'unsupported-browser',
EditProfile = 'edit-profile',
}
export enum AppRoutes {
......
......@@ -68,13 +68,13 @@ export async function focusOrCreateOnboardingTab(page?: string): Promise<void> {
})
}
export async function focusOrCreateUnitagClaimTab(): Promise<void> {
export async function focusOrCreateUnitagTab(page?: string): Promise<void> {
const extension = await chrome.management.getSelf()
const tabs = await chrome.tabs.query({ url: `chrome-extension://${extension.id}/unitagClaim.html*` })
const tab = tabs[0]
const url = 'unitagClaim.html#/'
const url = `unitagClaim.html#/${page ?? ''}`
if (!tab?.id) {
await chrome.tabs.create({ url })
......@@ -205,3 +205,22 @@ export async function getCurrentTabAndWindowId(): Promise<{ tabId: number; windo
}
return { tabId: tabs[0].id, windowId: tabs[0].windowId }
}
export async function closeCurrentTab(): Promise<void> {
try {
const tab = await chrome.tabs.getCurrent()
if (tab?.id) {
await chrome.tabs.remove(tab.id)
} else {
throw new Error('chrome.tabs.getCurrent did not return a tab with an id')
}
} catch (e) {
logger.error(e, {
tags: {
file: 'utils.ts',
function: 'closeCurrentTab',
},
})
}
}
......@@ -76,6 +76,8 @@ import {
ExtensionToDappRequestType,
FocusOnboardingMessage,
FocusOnboardingMessageSchema,
RefreshUnitagsRequest,
RefreshUnitagsRequestSchema,
TabActivatedRequest,
TabActivatedRequestSchema,
UpdateConnectionRequest,
......@@ -121,6 +123,7 @@ export function createOnboardingMessagePort(
type BackgroundToSidePanelMessageSchemas = {
[BackgroundToSidePanelRequestType.DappRequestReceived]: DappRequestMessage
[BackgroundToSidePanelRequestType.TabActivated]: TabActivatedRequest
[BackgroundToSidePanelRequestType.RefreshUnitags]: RefreshUnitagsRequest
}
const backgroundToSidePanelMessageParsers: MessageParsers<
BackgroundToSidePanelRequestType,
......@@ -130,6 +133,8 @@ const backgroundToSidePanelMessageParsers: MessageParsers<
DappRequestMessageSchema.parse(message),
[BackgroundToSidePanelRequestType.TabActivated]: (message): TabActivatedRequest =>
TabActivatedRequestSchema.parse(message),
[BackgroundToSidePanelRequestType.RefreshUnitags]: (message): RefreshUnitagsRequest =>
RefreshUnitagsRequestSchema.parse(message),
}
function createBackgroundToSidePanelMessageChannel(): TypedRuntimeMessageChannel<
......@@ -139,6 +144,7 @@ function createBackgroundToSidePanelMessageChannel(): TypedRuntimeMessageChannel
return new TypedRuntimeMessageChannel<BackgroundToSidePanelRequestType, BackgroundToSidePanelMessageSchemas>({
channelName: MessageChannelName.DappBackground,
messageParsers: backgroundToSidePanelMessageParsers,
canReceiveFromWebPage: true,
})
}
......@@ -194,7 +200,7 @@ function createContentScriptToBackgroundMessageChannel(): TypedRuntimeMessageCha
return new TypedRuntimeMessageChannel<DappRequestType, ContentScriptToBackgroundMessageSchemas>({
channelName: MessageChannelName.DappContentScript,
messageParsers: contentScriptToBackgroundMessageParsers,
canReceiveFromContentScript: true,
canReceiveFromWebPage: true,
})
}
......@@ -315,7 +321,7 @@ export function createContentScriptUtilityMessageChannel(): TypedRuntimeMessageC
return new TypedRuntimeMessageChannel<ContentScriptUtilityMessageType, ContentScriptUtilityMessageSchemas>({
channelName: MessageChannelName.ContentScriptUtility,
messageParsers: contentScriptUtilityMessageParsers,
canReceiveFromContentScript: true,
canReceiveFromWebPage: true,
})
}
......
......@@ -13,10 +13,10 @@ class ChromeMessageChannel {
constructor({
channelName,
port,
canReceiveFromContentScript = false,
canReceiveFromWebPage = false,
}: {
channelName: string
canReceiveFromContentScript?: boolean
canReceiveFromWebPage?: boolean
port?: chrome.runtime.Port
}) {
this.channelName = channelName
......@@ -26,7 +26,7 @@ class ChromeMessageChannel {
const targetMessage = message[this.channelName]
if (targetMessage !== undefined) {
if (sender?.tab !== undefined && !canReceiveFromContentScript) {
if (sender?.tab !== undefined && !canReceiveFromWebPage) {
return
}
......@@ -113,18 +113,18 @@ abstract class TypedMessageChannel<
channelName,
port,
messageParsers,
canReceiveFromContentScript,
canReceiveFromWebPage,
}: {
channelName: string
port?: chrome.runtime.Port
messageParsers: MessageParsers<T, R>
canReceiveFromContentScript?: boolean
canReceiveFromWebPage?: boolean
}) {
this.messageParsers = messageParsers
this.chromeMessageChannel = new ChromeMessageChannel({
channelName,
port,
canReceiveFromContentScript,
canReceiveFromWebPage,
})
this.chromeMessageChannel.addMessageListener((message, sender) => {
......@@ -263,13 +263,13 @@ export class TypedRuntimeMessageChannel<
constructor({
channelName,
messageParsers,
canReceiveFromContentScript,
canReceiveFromWebPage,
}: {
channelName: string
messageParsers: MessageParsers<T, R>
canReceiveFromContentScript?: boolean
canReceiveFromWebPage?: boolean
}) {
super({ channelName, messageParsers, canReceiveFromContentScript })
super({ channelName, messageParsers, canReceiveFromWebPage })
}
}
......@@ -294,7 +294,7 @@ export class TypedPortMessageChannel<
port: chrome.runtime.Port
canReceiveFromContentScript?: boolean
}) {
super({ channelName, messageParsers, port, canReceiveFromContentScript })
super({ channelName, messageParsers, port, canReceiveFromWebPage: canReceiveFromContentScript })
this.port = port
}
}
......@@ -36,6 +36,7 @@ export type FocusOnboardingMessage = z.infer<typeof FocusOnboardingMessageSchema
export enum BackgroundToSidePanelRequestType {
TabActivated = 'TabActivated',
DappRequestReceived = 'DappRequestReceived',
RefreshUnitags = 'RefreshUnitags',
}
export const DappRequestMessageSchema = z.object({
......@@ -55,6 +56,11 @@ export const TabActivatedRequestSchema = MessageSchema.extend({
})
export type TabActivatedRequest = z.infer<typeof TabActivatedRequestSchema>
export const RefreshUnitagsRequestSchema = MessageSchema.extend({
type: z.literal(BackgroundToSidePanelRequestType.RefreshUnitags),
})
export type RefreshUnitagsRequest = z.infer<typeof RefreshUnitagsRequestSchema>
// Requests outgoing from the extension to the injected script
export enum ExtensionToDappRequestType {
UpdateConnections = 'UpdateConnections',
......
......@@ -10,6 +10,7 @@ import {
v12Schema,
v13Schema,
v14Schema,
v15Schema,
v1Schema,
v2Schema,
v3Schema,
......@@ -23,6 +24,7 @@ import {
import { initialUniswapBehaviorHistoryState } from 'uniswap/src/features/behaviorHistory/slice'
import { initialFavoritesState } from 'uniswap/src/features/favorites/slice'
import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants'
import { initialNotificationsState } from 'uniswap/src/features/notifications/slice'
import { initialSearchHistoryState } from 'uniswap/src/features/search/searchHistorySlice'
import { initialUserSettingsState } from 'uniswap/src/features/settings/slice'
import { initialTokensState } from 'uniswap/src/features/tokens/slice/slice'
......@@ -32,7 +34,6 @@ import { UniverseChainId } from 'uniswap/src/types/chains'
import { getAllKeysOfNestedObject } from 'utilities/src/primitives/objects'
import { initialAppearanceSettingsState } from 'wallet/src/features/appearance/slice'
import { initialBehaviorHistoryState } from 'wallet/src/features/behaviorHistory/slice'
import { initialNotificationsState } from 'wallet/src/features/notifications/slice'
import { initialWalletState } from 'wallet/src/features/wallet/slice'
import { createMigrate } from 'wallet/src/state/createMigrate'
import { HAYDEN_ETH_ADDRESS } from 'wallet/src/state/walletMigrations'
......@@ -45,6 +46,7 @@ import {
testMovedTokenWarnings,
testMovedUserSettings,
testRemoveHoldToSwap,
testUpdateExploreOrderByType,
} from 'wallet/src/state/walletMigrationsTests'
expect.extend({ toIncludeSameMembers })
......@@ -262,4 +264,8 @@ describe('Redux state migrations', () => {
it('migrates from v14 to v15', async () => {
testMovedCurrencySetting(migrations[15], v14Schema)
})
it('migrates from v15 to v16', async () => {
testUpdateExploreOrderByType(migrations[16], v15Schema)
})
})
......@@ -17,6 +17,7 @@ import {
moveUserSettings,
removeUniconV2BehaviorState,
removeWalletIsUnlockedState,
updateExploreOrderByType,
} from 'wallet/src/state/walletMigrations'
export const migrations = {
......@@ -38,6 +39,7 @@ export const migrations = {
13: moveDismissedTokenWarnings,
14: moveLanguageSetting,
15: moveCurrencySetting,
16: updateExploreOrderByType,
}
export const EXTENSION_STATE_VERSION = 15
export const EXTENSION_STATE_VERSION = 16
import { RankingType } from 'wallet/src/features/wallet/types'
// only add fields that are persisted
export const initialSchema = {
dapp: {},
......@@ -187,4 +189,9 @@ const v15SchemaIntermediate = {
delete v15SchemaIntermediate.fiatCurrencySettings
export const v15Schema = v15SchemaIntermediate
export const getSchema = (): typeof v15Schema => v15Schema
export const v16Schema = {
...v15Schema,
wallet: { ...v15Schema.wallet, settings: { ...v15Schema.wallet.settings, tokensOrderBy: RankingType.Volume } },
}
export const getSchema = (): typeof v16Schema => v16Schema
......@@ -19,7 +19,9 @@
android:fullBackupContent="@xml/backup_rules"
android:dataExtractionRules="@xml/data_extraction_rules"
android:localeConfig="@xml/locales_config"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
android:taskAffinity=""
android:excludeFromRecents="true">
<meta-data
android:name="com.onesignal.messaging.default_notification_icon"
......
......@@ -17,6 +17,7 @@ import com.shopify.reactnativeperformance.ReactNativePerformance
import com.uniswap.onboarding.scantastic.ScantasticEncryptionModule
import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ReactNativeHostWrapper
import com.uniswap.RedirectToSourceAppPackage
class MainApplication : MultiDexApplication(), ReactApplication {
override val reactNativeHost: ReactNativeHost =
......@@ -28,6 +29,7 @@ class MainApplication : MultiDexApplication(), ReactApplication {
add(UniswapPackage())
add(RNCloudStorageBackupsManagerModule())
add(ScantasticEncryptionModule())
add(RedirectToSourceAppPackage())
}
override fun getJSMainModuleName(): String {
return "index"
......
package com.uniswap
import android.content.Intent
import android.net.Uri
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
class RedirectToSourceAppModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
override fun getName(): String {
return "RedirectToSourceApp"
}
@ReactMethod
fun moveAppToBackground() {
currentActivity?.moveTaskToBack(true)
}
}
package com.uniswap
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
class RedirectToSourceAppPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return listOf(RedirectToSourceAppModule(reactContext))
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return emptyList()
}
}
......@@ -1777,7 +1777,7 @@ PODS:
- react-native-appsflyer (6.13.1):
- AppsFlyerFramework (= 6.13.1)
- React
- react-native-compat (2.11.2):
- react-native-compat (2.17.1):
- glog
- RCT-Folly (= 2022.05.16.00)
- React-Core
......@@ -2523,7 +2523,7 @@ SPEC CHECKSUMS:
React-logger: 3eb80a977f0d9669468ef641a5e1fabbc50a09ec
React-Mapbuffer: 84ea43c6c6232049135b1550b8c60b2faac19fab
react-native-appsflyer: 2cc1f96348065fc23e976fc7a27e371789fb349e
react-native-compat: 3af9add14d349701306d3d052638435f6795ac2c
react-native-compat: eddb3937dcc5dcf3a27ebc42564dc15a89415829
react-native-context-menu-view: dcec18eb8882e20596dbb75802e7d19cb87dac02
react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a
react-native-image-picker: 1569cfade34b3a011191ce262423e6ce2f8db5a1
......
......@@ -24,6 +24,8 @@ jest.mock('@sentry/react-native', () => ({
ReactNativeTracing: jest.fn(),
}))
jest.mock('@uniswap/client-explore/dist/uniswap/explore/v1/service-ExploreStatsService_connectquery', () => {})
// Disables animated driver warning
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper')
......
......@@ -33,7 +33,7 @@
"link:assets": "react-native-asset",
"graphql:generate:swift": "cd ios && ./Pods/Apollo/apollo-ios-cli generate",
"hardhat": "hardhat node",
"check:circular": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 6",
"check:circular": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 10",
"ios": "yarn ios:prebuild && SKIP_BUNDLING=1 react-native run-ios",
"ios:prebuild": "yarn graphql:generate:swift && cd ios/WidgetsCore/MobileSchema && rm -rf !(README.md) && cd ../../.. && yarn graphql:generate:swift && yarn env:local:copy:swift",
"ios:smol": "SKIP_BUNDLING=1 react-native run-ios --simulator=\"iPhone SE (3rd generation)\"",
......@@ -78,6 +78,7 @@
"@react-navigation/native-stack": "6.7.0",
"@react-navigation/stack": "6.2.2",
"@reduxjs/toolkit": "1.9.3",
"@reown/walletkit": "1.1.1",
"@sentry/react": "7.80.0",
"@sentry/react-native": "5.5.0",
"@shopify/flash-list": "1.6.3",
......@@ -87,12 +88,12 @@
"@sparkfabrik/react-native-idfa-aaid": "1.2.0",
"@uniswap/analytics": "1.7.0",
"@uniswap/analytics-events": "2.38.0",
"@uniswap/client-explore": "0.0.10",
"@uniswap/ethers-rs-mobile": "0.0.5",
"@uniswap/sdk-core": "5.8.0",
"@walletconnect/core": "2.11.2",
"@walletconnect/react-native-compat": "2.11.2",
"@walletconnect/utils": "2.11.2",
"@walletconnect/web3wallet": "1.10.2",
"@uniswap/sdk-core": "5.8.3",
"@walletconnect/core": "2.17.1",
"@walletconnect/react-native-compat": "2.17.1",
"@walletconnect/utils": "2.17.1",
"apollo3-cache-persist": "0.14.1",
"babel-plugin-transform-inline-environment-variables": "0.4.4",
"babel-plugin-transform-remove-console": "6.9.4",
......@@ -167,7 +168,7 @@
"@testing-library/react-native": "11.5.0",
"@types/redux-mock-store": "1.0.6",
"@uniswap/eslint-config": "workspace:^",
"@walletconnect/types": "2.11.2",
"@walletconnect/types": "2.17.1",
"@welldone-software/why-did-you-render": "8.0.1",
"babel-loader": "8.2.3",
"babel-plugin-module-resolver": "5.0.0",
......
......@@ -64,6 +64,7 @@ import { loadStatsigOverrides } from 'uniswap/src/features/gating/overrides/cust
import { Statsig, StatsigOptions, StatsigProvider, StatsigUser } from 'uniswap/src/features/gating/sdk/statsig'
import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext'
import { useCurrentLanguageInfo } from 'uniswap/src/features/language/hooks'
import { clearNotificationQueue } from 'uniswap/src/features/notifications/slice'
import { syncAppWithDeviceLanguage } from 'uniswap/src/features/settings/slice'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { MobileEventName } from 'uniswap/src/features/telemetry/constants'
......@@ -85,7 +86,6 @@ import { usePersistedApolloClient } from 'wallet/src/data/apollo/usePersistedApo
import { initFirebaseAppCheck } from 'wallet/src/features/appCheck/appCheck'
import { useCurrentAppearanceSetting } from 'wallet/src/features/appearance/hooks'
import { selectHapticsEnabled } from 'wallet/src/features/appearance/slice'
import { clearNotificationQueue } from 'wallet/src/features/notifications/slice'
import { TransactionHistoryUpdater } from 'wallet/src/features/transactions/TransactionHistoryUpdater'
import { WalletUniswapProvider } from 'wallet/src/features/transactions/contexts/WalletUniswapContext'
import { Account } from 'wallet/src/features/wallet/accounts/types'
......
......@@ -80,6 +80,7 @@ import {
v76Schema,
v77Schema,
v78Schema,
v79Schema,
v7Schema,
v8Schema,
v9Schema,
......@@ -95,6 +96,7 @@ import { AccountType } from 'uniswap/src/features/accounts/types'
import { initialUniswapBehaviorHistoryState } from 'uniswap/src/features/behaviorHistory/slice'
import { initialFavoritesState } from 'uniswap/src/features/favorites/slice'
import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants'
import { initialNotificationsState } from 'uniswap/src/features/notifications/slice'
import { initialSearchHistoryState } from 'uniswap/src/features/search/searchHistorySlice'
import { initialUserSettingsState } from 'uniswap/src/features/settings/slice'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
......@@ -107,7 +109,6 @@ import { getAllKeysOfNestedObject } from 'utilities/src/primitives/objects'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
import { initialAppearanceSettingsState } from 'wallet/src/features/appearance/slice'
import { initialBehaviorHistoryState } from 'wallet/src/features/behaviorHistory/slice'
import { initialNotificationsState } from 'wallet/src/features/notifications/slice'
import { initialTelemetryState } from 'wallet/src/features/telemetry/slice'
import { Account, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types'
import { initialWalletState, SwapProtectionSetting } from 'wallet/src/features/wallet/slice'
......@@ -122,6 +123,7 @@ import {
testMovedTokenWarnings,
testMovedUserSettings,
testRemoveHoldToSwap,
testUpdateExploreOrderByType,
} from 'wallet/src/state/walletMigrationsTests'
import { signerMnemonicAccount } from 'wallet/src/test/fixtures'
......@@ -1580,4 +1582,8 @@ describe('Redux state migrations', () => {
it('migrates from v78 to v79', async () => {
testMovedCurrencySetting(migrations[79], v78Schema)
})
it('migrates from v79 to v80', async () => {
testUpdateExploreOrderByType(migrations[80], v79Schema)
})
})
......@@ -35,6 +35,7 @@ import {
moveUserSettings,
removeUniconV2BehaviorState,
removeWalletIsUnlockedState,
updateExploreOrderByType,
} from 'wallet/src/state/walletMigrations'
export const OLD_DEMO_ACCOUNT_ADDRESS = '0xdd0E380579dF30E38524F9477808d9eE37E2dEa6'
......@@ -948,6 +949,8 @@ export const migrations = {
78: moveLanguageSetting,
79: moveCurrencySetting,
80: updateExploreOrderByType,
}
export const MOBILE_STATE_VERSION = 79
export const MOBILE_STATE_VERSION = 80
......@@ -3,6 +3,7 @@ import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants'
import { Language } from 'uniswap/src/features/language/constants'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice'
import { RankingType } from 'wallet/src/features/wallet/types'
// only add fields that are persisted
export const initialSchema = {
......@@ -615,6 +616,17 @@ const v79SchemaIntermediate = {
delete v79SchemaIntermediate.fiatCurrencySettings
export const v79Schema = v79SchemaIntermediate
export const v80Schema = {
...v79Schema,
wallet: {
...v79Schema.wallet,
settings: {
...v79Schema.wallet.settings,
tokensOrderBy: RankingType.Volume,
},
},
}
// TODO: [MOB-201] use function with typed output when API reducers are removed from rootReducer
// export const getSchema = (): RootState => v0Schema
export const getSchema = (): typeof v79Schema => v79Schema
export const getSchema = (): typeof v80Schema => v80Schema
......@@ -2,7 +2,6 @@ import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Alert } from 'react-native'
import 'react-native-reanimated'
import { useSelector } from 'react-redux'
import { QRCodeScanner } from 'src/components/QRCodeScanner/QRCodeScanner'
import { getSupportedURI, URIType } from 'src/components/Requests/ScanSheet/util'
import { Flex, Text, TouchableArea, useHapticFeedback, useIsDarkMode, useSporeColors } from 'ui/src'
......@@ -14,7 +13,7 @@ import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
import { WalletQRCode } from 'wallet/src/components/QRCodeScanner/WalletQRCode'
import { selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors'
import { useActiveAccountAddress } from 'wallet/src/features/wallet/hooks'
type Props = {
onClose: () => void
......@@ -24,7 +23,7 @@ type Props = {
export function RecipientScanModal({ onSelectRecipient, onClose }: Props): JSX.Element {
const { t } = useTranslation()
const colors = useSporeColors()
const activeAddress = useSelector(selectActiveAccountAddress)
const activeAddress = useActiveAccountAddress()
const [currentScreenState, setCurrentScreenState] = useState<ScannerModalState>(ScannerModalState.ScanQr)
const [shouldFreezeCamera, setShouldFreezeCamera] = useState(false)
const { hapticFeedback } = useHapticFeedback()
......
......@@ -11,12 +11,12 @@ import { iconSizes } from 'ui/src/theme'
import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo'
import { Modal } from 'uniswap/src/components/modals/Modal'
import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { WalletConnectEvent } from 'uniswap/src/types/walletConnect'
import { logger } from 'utilities/src/logger/logger'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType } from 'wallet/src/features/notifications/types'
import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks'
interface DappConnectedNetworkModalProps {
session: WalletConnectSession
......
......@@ -13,12 +13,12 @@ import { disableOnPress } from 'src/utils/disableOnPress'
import { AnimatedTouchableArea, Flex, ImpactFeedbackStyle, Text, TouchableArea } from 'ui/src'
import { iconSizes, spacing } from 'ui/src/theme'
import { NetworkLogos } from 'uniswap/src/components/network/NetworkLogos'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { WalletConnectEvent } from 'uniswap/src/types/walletConnect'
import { logger } from 'utilities/src/logger/logger'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType } from 'wallet/src/features/notifications/types'
import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks'
export function DappConnectionItem({
......
......@@ -17,7 +17,11 @@ import { returnToPreviousApp } from 'src/features/walletConnect/WalletConnect'
import { wcWeb3Wallet } from 'src/features/walletConnect/saga'
import { selectDidOpenFromDeepLink } from 'src/features/walletConnect/selectors'
import { signWcRequestActions } from 'src/features/walletConnect/signWcRequestSaga'
import { WalletConnectRequest, isTransactionRequest } from 'src/features/walletConnect/walletConnectSlice'
import {
WalletConnectRequest,
isTransactionRequest,
setDidOpenFromDeepLink,
} from 'src/features/walletConnect/walletConnectSlice'
import { useTransactionGasFee } from 'uniswap/src/features/gas/hooks'
import { MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
......@@ -131,7 +135,8 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
onClose()
if (didOpenFromDeepLink) {
returnToPreviousApp()
await returnToPreviousApp()
setDidOpenFromDeepLink(false)
}
}
......@@ -190,7 +195,8 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
onClose()
if (didOpenFromDeepLink) {
returnToPreviousApp()
await returnToPreviousApp()
setDidOpenFromDeepLink(false)
}
}
......
......@@ -16,18 +16,19 @@ import {
WalletConnectPendingSession,
addSession,
removePendingSession,
setDidOpenFromDeepLink,
} from 'src/features/walletConnect/walletConnectSlice'
import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src'
import { Check, RotatableChevron, X } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import { MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { WCEventType, WCRequestOutcome, WalletConnectEvent } from 'uniswap/src/types/walletConnect'
import { formatDappURL } from 'utilities/src/format/urls'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType } from 'wallet/src/features/notifications/types'
import { AddressFooter } from 'wallet/src/features/transactions/TransactionRequest/AddressFooter'
import {
useActiveAccountAddressWithThrow,
......@@ -198,7 +199,8 @@ export const PendingConnectionModal = ({ pendingSession, onClose }: Props): JSX.
onClose()
if (didOpenFromDeepLink) {
returnToPreviousApp()
await returnToPreviousApp()
setDidOpenFromDeepLink(false)
}
},
[activeAddress, dispatch, onClose, pendingSession, didOpenFromDeepLink],
......
......@@ -5,6 +5,8 @@ import { useDispatch } from 'react-redux'
import { Flex, IconProps, Text, TouchableArea, useSporeColors } from 'ui/src'
import CopyIcon from 'ui/src/assets/icons/copy-sheets.svg'
import { iconSizes } from 'ui/src/theme'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType, CopyNotificationType } from 'uniswap/src/features/notifications/types'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ElementName, ElementNameType } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
......@@ -12,8 +14,6 @@ import { TestIDType } from 'uniswap/src/test/fixtures/testIDs'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { setClipboard } from 'uniswap/src/utils/clipboard'
import { openUri } from 'uniswap/src/utils/linking'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types'
export enum LinkButtonType {
Copy = 'copy',
......
......@@ -10,6 +10,8 @@ import { disableOnPress } from 'src/utils/disableOnPress'
import { Flex, Text, TouchableArea, useHapticFeedback } from 'ui/src'
import { iconSizes } from 'ui/src/theme'
import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType, CopyNotificationType } from 'uniswap/src/features/notifications/types'
import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
......@@ -17,8 +19,6 @@ import { setClipboard } from 'uniswap/src/utils/clipboard'
import { NumberType } from 'utilities/src/format/types'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { useAccountList } from 'wallet/src/features/accounts/hooks'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types'
type AccountCardItemProps = {
address: Address
......
......@@ -2,13 +2,15 @@ import { SharedEventName } from '@uniswap/analytics-events'
import React, { useCallback, useEffect } from 'react'
import { Gesture, GestureDetector, State } from 'react-native-gesture-handler'
import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withDelay, withTiming } from 'react-native-reanimated'
import { useDispatch, useSelector } from 'react-redux'
import { useDispatch } from 'react-redux'
import { navigate } from 'src/app/navigation/rootNavigation'
import { openModal } from 'src/features/modals/modalSlice'
import { Flex, ImpactFeedbackStyle, Text, TouchableArea, useHapticFeedback } from 'ui/src'
import { CopyAlt, Settings } from 'ui/src/components/icons'
import { AccountType } from 'uniswap/src/features/accounts/types'
import { useAvatar } from 'uniswap/src/features/address/avatar'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType, CopyNotificationType } from 'uniswap/src/features/notifications/types'
import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { MobileUserPropertyName, setUserProperty } from 'uniswap/src/features/telemetry/user'
......@@ -20,10 +22,7 @@ import { isDevEnv } from 'utilities/src/environment/env'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
import { AnimatedUnitagDisplayName } from 'wallet/src/components/accounts/AnimatedUnitagDisplayName'
import useIsFocused from 'wallet/src/features/focus/useIsFocused'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types'
import { useDisplayName } from 'wallet/src/features/wallet/hooks'
import { selectActiveAccount, selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors'
import { useActiveAccount, useActiveAccountAddress, useDisplayName } from 'wallet/src/features/wallet/hooks'
import { DisplayNameType } from 'wallet/src/features/wallet/types'
const RotatingSettingsIcon = ({ onPressSettings }: { onPressSettings(): void }): JSX.Element => {
......@@ -68,8 +67,8 @@ const RotatingSettingsIcon = ({ onPressSettings }: { onPressSettings(): void }):
}
export function AccountHeader(): JSX.Element {
const activeAddress = useSelector(selectActiveAccountAddress)
const account = useSelector(selectActiveAccount)
const activeAddress = useActiveAccountAddress()
const account = useActiveAccount()
const dispatch = useDispatch()
const { hapticFeedback } = useHapticFeedback()
......
......@@ -28,11 +28,7 @@ export function Carousel({ slides, ...flatListProps }: CarouselProps): JSX.Eleme
const { fullWidth } = useDeviceDimensions()
const myRef = useRef<Animated.FlatList<unknown>>(null)
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
scroll.value = event.contentOffset.x
},
})
const scrollHandler = useAnimatedScrollHandler((event) => (scroll.value = event.contentOffset.x), [scroll])
const goToNext = useCallback(() => {
// @ts-expect-error https://github.com/software-mansion/react-native-reanimated/issues/2976
......
......@@ -10,10 +10,12 @@ export function FavoriteHeaderRow({
editingTitle,
isEditing,
onPress,
disabled,
}: {
title: string
editingTitle: string
isEditing: boolean
disabled?: boolean
onPress: () => void
}): JSX.Element {
const { t } = useTranslation()
......@@ -23,7 +25,7 @@ export function FavoriteHeaderRow({
{isEditing ? editingTitle : title}
</Text>
{!isEditing ? (
<TouchableArea hapticFeedback hitSlop={16} testID={TestID.Edit} onPress={onPress}>
<TouchableArea hapticFeedback hitSlop={16} testID={TestID.Edit} disabled={disabled} onPress={onPress}>
<Ellipsis color="$neutral2" size={iconSizes.icon20} strokeLinecap="round" strokeWidth={1} />
</TouchableArea>
) : (
......
......@@ -9,10 +9,17 @@ import { useAnimatedCardDragStyle, useExploreTokenContextMenu } from 'src/compon
import { Loader } from 'src/components/loading/loaders'
import { disableOnPress } from 'src/utils/disableOnPress'
import { usePollOnFocusOnly } from 'src/utils/hooks'
import { AnimatedTouchableArea, Flex, ImpactFeedbackStyle, Text } from 'ui/src'
import {
AnimatedTouchableArea,
Flex,
ImpactFeedbackStyle,
Text,
useIsDarkMode,
useShadowPropsShort,
useSporeColors,
} from 'ui/src'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { borderRadii, imageSizes } from 'ui/src/theme'
import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard'
import { borderRadii, imageSizes, opacify } from 'ui/src/theme'
import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo'
import { PollingInterval } from 'uniswap/src/constants/misc'
import { useFavoriteTokenCardQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
......@@ -50,6 +57,9 @@ function FavoriteTokenCard({
const tokenDetailsNavigation = useTokenDetailsNavigation()
const { convertFiatAmountFormatted } = useLocalizationContext()
const colors = useSporeColors()
const isDarkMode = useIsDarkMode()
const { data, networkStatus, startPolling, stopPolling } = useFavoriteTokenCardQuery({
variables: currencyIdToContractInput(currencyId),
// Rely on cache for fast favoriting UX, and poll for updates.
......@@ -94,12 +104,14 @@ function FavoriteTokenCard({
const animatedDragStyle = useAnimatedCardDragStyle(pressProgress, dragActivationProgress)
const shadowProps = useShadowPropsShort()
if (isNonPollingRequestInFlight(networkStatus)) {
return <Loader.Favorite height={FAVORITE_TOKEN_CARD_LOADER_HEIGHT} />
}
return (
<AnimatedFlex style={animatedDragStyle}>
<AnimatedFlex borderRadius="$rounded16" style={animatedDragStyle}>
<ContextMenu
actions={menuActions}
disabled={isEditing}
......@@ -109,8 +121,10 @@ function FavoriteTokenCard({
>
<AnimatedTouchableArea
activeOpacity={isEditing ? 1 : undefined}
backgroundColor="$surface2"
backgroundColor={isDarkMode ? '$surface2' : '$surface1'}
borderColor={opacify(0.05, colors.surface3.val)}
borderRadius="$rounded16"
borderWidth={isDarkMode ? '$none' : '$spacing1'}
entering={FadeIn}
hapticFeedback={!isEditing}
hapticStyle={ImpactFeedbackStyle.Light}
......@@ -118,35 +132,34 @@ function FavoriteTokenCard({
testID={`token-box-${token?.symbol}`}
onLongPress={disableOnPress}
onPress={onPress}
{...shadowProps}
>
<BaseCard.Shadow>
<Flex alignItems="flex-start" gap="$spacing8">
<Flex row gap="$spacing4" justifyContent="space-between">
<Flex grow row alignItems="center" gap="$spacing8">
<TokenLogo
chainId={chainId ?? undefined}
name={token?.name ?? undefined}
size={imageSizes.image20}
symbol={token?.symbol ?? undefined}
url={token?.project?.logoUrl ?? undefined}
/>
<Text variant="body1">{getSymbolDisplayText(token?.symbol)}</Text>
</Flex>
<RemoveButton visible={isEditing} onPress={onRemove} />
</Flex>
<Flex gap="$spacing2">
<Text adjustsFontSizeToFit numberOfLines={1} variant="heading3">
{price}
</Text>
<RelativeChange
arrowSize="$icon.16"
change={pricePercentChange ?? undefined}
semanticColor={true}
variant="subheading2"
<Flex alignItems="flex-start" gap="$spacing8" p="$spacing12">
<Flex row gap="$spacing4" justifyContent="space-between">
<Flex grow row alignItems="center" gap="$spacing8">
<TokenLogo
chainId={chainId ?? undefined}
name={token?.name ?? undefined}
size={imageSizes.image20}
symbol={token?.symbol ?? undefined}
url={token?.project?.logoUrl ?? undefined}
/>
<Text variant="body1">{getSymbolDisplayText(token?.symbol)}</Text>
</Flex>
<RemoveButton visible={isEditing} onPress={onRemove} />
</Flex>
<Flex gap="$spacing2">
<Text adjustsFontSizeToFit numberOfLines={1} variant="heading3">
{price}
</Text>
<RelativeChange
arrowSize="$icon.16"
change={pricePercentChange ?? undefined}
semanticColor={true}
variant="subheading2"
/>
</Flex>
</BaseCard.Shadow>
</Flex>
</AnimatedTouchableArea>
</ContextMenu>
</AnimatedFlex>
......
......@@ -63,6 +63,7 @@ export function FavoriteTokensGrid({ showLoading, ...rest }: FavoriteTokensGridP
return (
<AnimatedFlex entering={FadeIn} style={animatedStyle}>
<FavoriteHeaderRow
disabled={showLoading}
editingTitle={t('explore.tokens.favorite.title.edit')}
isEditing={isEditing}
title={t('explore.tokens.favorite.title.default')}
......
......@@ -8,10 +8,9 @@ import { useEagerExternalProfileNavigation } from 'src/app/navigation/hooks'
import RemoveButton from 'src/components/explore/RemoveButton'
import { useAnimatedCardDragStyle } from 'src/components/explore/hooks'
import { disableOnPress } from 'src/utils/disableOnPress'
import { Flex, ImpactFeedbackStyle, TouchableArea } from 'ui/src'
import { Flex, ImpactFeedbackStyle, TouchableArea, useIsDarkMode, useShadowPropsShort, useSporeColors } from 'ui/src'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { borderRadii, iconSizes } from 'ui/src/theme'
import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard'
import { borderRadii, iconSizes, opacify } from 'ui/src/theme'
import { useAvatar } from 'uniswap/src/features/address/avatar'
import { removeWatchedAddress } from 'uniswap/src/features/favorites/slice'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
......@@ -37,6 +36,9 @@ function FavoriteWalletCard({
}: FavoriteWalletCardProps): JSX.Element {
const { t } = useTranslation()
const dispatch = useDispatch()
const colors = useSporeColors()
const isDarkMode = useIsDarkMode()
const { preload, navigate } = useEagerExternalProfileNavigation()
const displayName = useDisplayName(address)
......@@ -60,6 +62,8 @@ function FavoriteWalletCard({
const animatedDragStyle = useAnimatedCardDragStyle(pressProgress, dragActivationProgress)
const shadowProps = useShadowPropsShort()
return (
<AnimatedFlex style={animatedDragStyle}>
<ContextMenu
......@@ -82,8 +86,10 @@ function FavoriteWalletCard({
<TouchableArea
hapticFeedback
activeOpacity={isEditing ? 1 : undefined}
backgroundColor="$surface2"
backgroundColor={isDarkMode ? '$surface2' : '$surface1'}
borderColor={opacify(0.05, colors.surface3.val)}
borderRadius="$rounded16"
borderWidth={isDarkMode ? '$none' : '$spacing1'}
disabled={isEditing}
hapticStyle={ImpactFeedbackStyle.Light}
m="$spacing4"
......@@ -95,22 +101,21 @@ function FavoriteWalletCard({
onPressIn={async (): Promise<void> => {
await preload(address)
}}
{...shadowProps}
>
<BaseCard.Shadow>
<Flex row gap="$spacing4" justifyContent="space-between">
<Flex row shrink alignItems="center" gap="$spacing8">
{icon}
<DisplayNameText
displayName={displayName}
textProps={{
adjustsFontSizeToFit: displayName?.type === DisplayNameType.Address,
variant: 'body1',
}}
/>
</Flex>
<RemoveButton visible={isEditing} onPress={onRemove} />
<Flex row gap="$spacing4" justifyContent="space-between" p="$spacing12">
<Flex row shrink alignItems="center" gap="$spacing8">
{icon}
<DisplayNameText
displayName={displayName}
textProps={{
adjustsFontSizeToFit: displayName?.type === DisplayNameType.Address,
variant: 'body1',
}}
/>
</Flex>
</BaseCard.Shadow>
<RemoveButton visible={isEditing} onPress={onRemove} />
</Flex>
</TouchableArea>
</ContextMenu>
</AnimatedFlex>
......
......@@ -67,6 +67,7 @@ export function FavoriteWalletsGrid({ showLoading, ...rest }: FavoriteWalletsGri
editingTitle={t('explore.wallets.favorite.title.edit')}
isEditing={isEditing}
title={t('explore.wallets.favorite.title.default')}
disabled={showLoading}
onPress={(): void => setIsEditing(!isEditing)}
/>
{showLoading ? (
......
import { SortButton } from 'src/components/explore/SortButton'
import { render } from 'src/test/test-utils'
import { TokenSortableField } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { ClientTokensOrderBy } from 'wallet/src/features/wallet/types'
import { CustomRankingType, ExploreOrderBy, RankingType } from 'wallet/src/features/wallet/types'
jest.mock('react-native-context-menu-view', () => {
// Use the actual implementation of `react-native-context-menu-view` as the mock implementation
......@@ -11,23 +10,23 @@ jest.mock('react-native-context-menu-view', () => {
describe('SortButton', () => {
it('renders without error', () => {
const tree = render(<SortButton orderBy={TokenSortableField.Volume} />)
const tree = render(<SortButton orderBy={RankingType.Volume} />)
expect(tree).toMatchSnapshot()
})
const cases = [
{ test: 'volume', orderBy: TokenSortableField.Volume, label: 'Volume' },
{ test: 'total value locked', orderBy: TokenSortableField.TotalValueLocked, label: 'TVL' },
{ test: 'market cap', orderBy: TokenSortableField.MarketCap, label: 'Market cap' },
const cases: Array<{ test: string; orderBy: ExploreOrderBy; label: string }> = [
{ test: 'volume', orderBy: RankingType.Volume, label: 'Volume' },
{ test: 'total value locked', orderBy: RankingType.TotalValueLocked, label: 'TVL' },
{ test: 'market cap', orderBy: RankingType.MarketCap, label: 'Market cap' },
{
test: 'price increase',
orderBy: ClientTokensOrderBy.PriceChangePercentage24hDesc,
orderBy: CustomRankingType.PricePercentChange1DayDesc,
label: 'Price increase',
},
{
test: 'price decrease',
orderBy: ClientTokensOrderBy.PriceChangePercentage24hAsc,
orderBy: CustomRankingType.PricePercentChange1DayAsc,
label: 'Price decrease',
},
]
......
......@@ -2,61 +2,85 @@ import React, { memo, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import { getTokensOrderByMenuLabel, getTokensOrderBySelectedLabel } from 'src/features/explore/utils'
import { Flex, Text, useIsDarkMode } from 'ui/src'
import { RotatableChevron } from 'ui/src/components/icons'
import { Flex, Text, useSporeColors } from 'ui/src'
import {
Chart,
ChartPie,
ChartPyramid,
CheckCircleFilled,
RotatableChevron,
TrendDown,
TrendUp,
} from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
import { ActionSheetDropdown } from 'uniswap/src/components/dropdowns/ActionSheetDropdown'
import { MenuItemProp } from 'uniswap/src/components/modals/ActionSheetModal'
import { TokenSortableField } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { MobileEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { logger } from 'utilities/src/logger/logger'
import { setTokensOrderBy } from 'wallet/src/features/wallet/slice'
import { ClientTokensOrderBy, TokensOrderBy } from 'wallet/src/features/wallet/types'
import { CustomRankingType, ExploreOrderBy, RankingType } from 'wallet/src/features/wallet/types'
const MIN_MENU_ITEM_WIDTH = 220
interface FilterGroupProps {
orderBy: TokensOrderBy
orderBy: ExploreOrderBy
}
function _SortButton({ orderBy }: FilterGroupProps): JSX.Element {
const isDarkMode = useIsDarkMode()
const dispatch = useDispatch()
const { t } = useTranslation()
const colors = useSporeColors()
const menuActions = useMemo(() => {
return [
{
title: getTokensOrderByMenuLabel(TokenSortableField.Volume, t),
systemIcon: orderBy === TokenSortableField.Volume ? 'checkmark' : '',
orderBy: TokenSortableField.Volume,
title: getTokensOrderByMenuLabel(RankingType.Volume, t),
orderBy: RankingType.Volume,
icon: <Chart color={colors.neutral2.val} size={iconSizes.icon16} />,
active: orderBy === RankingType.Volume,
},
{
title: getTokensOrderByMenuLabel(TokenSortableField.TotalValueLocked, t),
systemIcon: orderBy === TokenSortableField.TotalValueLocked ? 'checkmark' : '',
orderBy: TokenSortableField.TotalValueLocked,
title: getTokensOrderByMenuLabel(RankingType.TotalValueLocked, t),
orderBy: RankingType.TotalValueLocked,
icon: <ChartPyramid color={colors.neutral2.val} size={iconSizes.icon16} />,
active: orderBy === RankingType.TotalValueLocked,
},
{
title: getTokensOrderByMenuLabel(TokenSortableField.MarketCap, t),
systemIcon: orderBy === TokenSortableField.MarketCap ? 'checkmark' : '',
orderBy: TokenSortableField.MarketCap,
title: getTokensOrderByMenuLabel(RankingType.MarketCap, t),
orderBy: RankingType.MarketCap,
icon: <ChartPie color={colors.neutral2.val} size={iconSizes.icon16} />,
active: orderBy === RankingType.MarketCap,
},
{
title: getTokensOrderByMenuLabel(ClientTokensOrderBy.PriceChangePercentage24hDesc, t),
systemIcon: orderBy === ClientTokensOrderBy.PriceChangePercentage24hDesc ? 'checkmark' : '',
orderBy: ClientTokensOrderBy.PriceChangePercentage24hDesc,
title: getTokensOrderByMenuLabel(CustomRankingType.PricePercentChange1DayDesc, t),
orderBy: CustomRankingType.PricePercentChange1DayDesc,
icon: <TrendUp color={colors.neutral2.val} size={iconSizes.icon16} />,
active: orderBy === CustomRankingType.PricePercentChange1DayDesc,
},
{
title: getTokensOrderByMenuLabel(ClientTokensOrderBy.PriceChangePercentage24hAsc, t),
systemIcon: orderBy === ClientTokensOrderBy.PriceChangePercentage24hAsc ? 'checkmark' : '',
orderBy: ClientTokensOrderBy.PriceChangePercentage24hAsc,
title: getTokensOrderByMenuLabel(CustomRankingType.PricePercentChange1DayAsc, t),
orderBy: CustomRankingType.PricePercentChange1DayAsc,
icon: <TrendDown color={colors.neutral2.val} size={iconSizes.icon16} />,
active: orderBy === CustomRankingType.PricePercentChange1DayAsc,
},
]
}, [t, orderBy])
}, [t, colors.neutral2.val, orderBy])
const MenuItem = useCallback(({ label }: { label: string }) => {
const MenuItem = useCallback(({ label, icon, active }: { label: string; icon: JSX.Element; active: boolean }) => {
return (
<Flex grow style={{ padding: 5 }}>
<Flex
grow
row
alignItems="center"
gap="$spacing8"
minWidth={MIN_MENU_ITEM_WIDTH}
py="$spacing8"
style={{ padding: 5 }}
>
{icon && icon}
<Text>{label}</Text>
{active && <CheckCircleFilled color="$neutral1" size="$icon.16" />}
</Flex>
)
}, [])
......@@ -78,7 +102,7 @@ function _SortButton({ orderBy }: FilterGroupProps): JSX.Element {
filter_type: selectedMenuAction.orderBy,
})
},
render: () => <MenuItem label={option.title} />,
render: () => <MenuItem active={option.active} icon={option.icon} label={option.title} />,
}
})
}, [MenuItem, dispatch, menuActions])
......@@ -91,17 +115,17 @@ function _SortButton({ orderBy }: FilterGroupProps): JSX.Element {
alignment: 'right',
}}
testID="chain-selector"
onDismiss={() => {}}
>
<Flex
row
backgroundColor={isDarkMode ? '$DEP_backgroundOverlay' : '$surface1'}
backgroundColor="$surface3"
borderRadius="$rounded20"
gap="$spacing4"
px="$spacing16"
pl="$spacing12"
pr="$spacing8"
py="$spacing8"
>
<Text ellipse color="$neutral2" flexShrink={1} numberOfLines={1} variant="buttonLabel2">
<Text ellipse color="$neutral1" flexShrink={1} numberOfLines={1} variant="buttonLabel2">
{getTokensOrderBySelectedLabel(orderBy, t)}
</Text>
<RotatableChevron color="$neutral2" direction="down" height={iconSizes.icon20} width={iconSizes.icon20} />
......
......@@ -10,6 +10,7 @@ import { TokenMetadata } from 'src/components/tokens/TokenMetadata'
import { disableOnPress } from 'src/utils/disableOnPress'
import { Flex, ImpactFeedbackStyle, Text, TouchableArea, ViewProps } from 'ui/src'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { spacing } from 'ui/src/theme'
import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo'
import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext'
import { MobileEventName, SectionName } from 'uniswap/src/features/telemetry/constants'
......@@ -115,15 +116,15 @@ export const TokenItem = memo(function _TokenItem({
>
{overlay}
<AnimatedFlex grow row alignItems="center" gap="$spacing12" px="$spacing24" py="$spacing8" {...containerProps}>
<Flex centered row gap="$spacing4" overflow="hidden">
<Flex centered row gap="$spacing4">
{!hideNumberedList && (
<Flex minWidth={16}>
<Flex minWidth={spacing.spacing16} mr="$spacing8">
<Text color="$neutral2" variant="buttonLabel2">
{index + 1}
</Text>
</Flex>
)}
<TokenLogo name={name} symbol={symbol} url={logoUrl} />
<TokenLogo chainId={chainId} name={name} symbol={symbol} url={logoUrl} />
</Flex>
<Flex fill shrink gap="$spacing2">
<Text numberOfLines={1} variant="body1">
......
......@@ -44,7 +44,7 @@ exports[`SortButton renders without error 1`] = `
<View
style={
{
"backgroundColor": "#FFFFFF",
"backgroundColor": "rgba(34,34,34,0.05)",
"borderBottomLeftRadius": 20,
"borderBottomRightRadius": 20,
"borderTopLeftRadius": 20,
......@@ -52,8 +52,8 @@ exports[`SortButton renders without error 1`] = `
"flexDirection": "row",
"gap": 4,
"paddingBottom": 8,
"paddingLeft": 16,
"paddingRight": 16,
"paddingLeft": 12,
"paddingRight": 8,
"paddingTop": 8,
}
}
......@@ -65,7 +65,7 @@ exports[`SortButton renders without error 1`] = `
numberOfLines={1}
style={
{
"color": "#7D7D7D",
"color": "#222222",
"flexShrink": 1,
"fontFamily": "Basel Grotesk",
"fontSize": 17,
......
......@@ -59,7 +59,6 @@ exports[`TokenItem renders without error 1`] = `
"flexDirection": "row",
"gap": 4,
"justifyContent": "center",
"overflow": "hidden",
}
}
>
......@@ -67,6 +66,7 @@ exports[`TokenItem renders without error 1`] = `
style={
{
"flexDirection": "column",
"marginRight": 8,
"minWidth": 16,
}
}
......
......@@ -14,28 +14,15 @@ import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { iconSizes } from 'ui/src/theme'
import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal'
import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types'
import { SearchResultType, WalletSearchResult } from 'uniswap/src/features/search/SearchResult'
import { clearSearchHistory } from 'uniswap/src/features/search/searchHistorySlice'
import { selectSearchHistory } from 'uniswap/src/features/search/selectSearchHistory'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { UniverseChainId } from 'uniswap/src/types/chains'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard'
const TrendUpIcon = <TrendUp color="$neutral2" size="$icon.24" />
export const SUGGESTED_WALLETS: WalletSearchResult[] = [
{
type: SearchResultType.ENSAddress,
address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
ensName: 'vitalik.eth',
},
{
type: SearchResultType.ENSAddress,
address: '0x50EC05ADe8280758E2077fcBC08D878D4aef79C3',
ensName: 'hayden.eth',
},
]
export function SearchEmptySection(): JSX.Element {
export function SearchEmptySection({ selectedChain }: { selectedChain: UniverseChainId | null }): JSX.Element {
const { t } = useTranslation()
const colors = useSporeColors()
const dispatch = useDispatch()
......@@ -90,20 +77,12 @@ export function SearchEmptySection(): JSX.Element {
title={t('explore.search.section.popularTokens')}
onPress={onPopularTokenInfoPress}
/>
<SearchPopularTokens />
<SearchPopularTokens selectedChain={selectedChain} />
</Flex>
<Flex gap="$spacing4">
<SectionHeaderText icon={TrendUpIcon} title={t('explore.search.section.popularNFT')} />
<SearchPopularNFTCollections />
</Flex>
<FlatList
ListHeaderComponent={
<SectionHeaderText icon={TrendUpIcon} title={t('explore.search.section.suggestedWallets')} />
}
data={SUGGESTED_WALLETS}
keyExtractor={walletKey}
renderItem={renderSearchItem}
/>
</AnimatedFlex>
<WarningModal
backgroundIconColor={colors.surface2.get()}
......@@ -120,10 +99,6 @@ export function SearchEmptySection(): JSX.Element {
)
}
const walletKey = (wallet: WalletSearchResult): string => {
return wallet.address
}
export const RecentIcon = (): JSX.Element => {
const colors = useSporeColors()
return <ClockIcon color={colors.neutral2.get()} height={iconSizes.icon20} width={iconSizes.icon20} />
......
......@@ -3,6 +3,7 @@ import { SearchPopularTokens } from 'src/components/explore/search/SearchPopular
import { render, screen } from 'src/test/test-utils'
import { ethToken, usdcToken, wethToken } from 'uniswap/src/test/fixtures'
import { queryResolvers } from 'uniswap/src/test/utils'
import { UniverseChainId } from 'uniswap/src/types/chains'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
const { resolvers } = queryResolvers({
......@@ -14,7 +15,7 @@ describe(SearchPopularTokens, () => {
// TODO(MOB-3146): this test is flaky
jest.retryTimes(3)
it.skip('renders without error', async () => {
const tree = render(<SearchPopularTokens />, { resolvers })
const tree = render(<SearchPopularTokens selectedChain={UniverseChainId.Mainnet} />, { resolvers })
// Loading should show Token loader
expect(screen.getAllByText('Token Full Name')).toBeDefined()
......
import { TokenRankingsStat } from '@uniswap/client-explore/dist/uniswap/explore/v1/service_pb'
import React, { useMemo } from 'react'
import { FlatList, ListRenderItemInfo } from 'react-native'
import { SearchTokenItem } from 'src/components/explore/search/items/SearchTokenItem'
import { getSearchResultId } from 'src/components/explore/search/utils'
import { Flex, Loader } from 'ui/src'
import { ALL_NETWORKS_ARG } from 'uniswap/src/data/rest/base'
import { useTokenRankingsQuery } from 'uniswap/src/data/rest/tokenRankings'
import { fromGraphQLChain } from 'uniswap/src/features/chains/utils'
import { getCurrencySafetyInfo } from 'uniswap/src/features/dataApi/utils'
import { SearchResultType, TokenSearchResult } from 'uniswap/src/features/search/SearchResult'
import { TopToken, usePopularTokens } from 'uniswap/src/features/tokens/hooks'
import { UniverseChainId } from 'uniswap/src/types/chains'
import { RankingType } from 'wallet/src/features/wallet/types'
function gqlTokenToTokenSearchResult(token: Maybe<TopToken>): TokenSearchResult | null {
if (!token || !token.project) {
const MAX_TOKEN_RESULTS_AMOUNT = 8
function tokenStatsToTokenSearchResult(token: Maybe<TokenRankingsStat>): TokenSearchResult | null {
if (!token) {
return null
}
const { name, chain, address, symbol, project, protectionInfo } = token
const { chain, address, symbol, name, logo } = token
const chainId = fromGraphQLChain(chain)
if (!chainId || !symbol || !name) {
return null
}
......@@ -25,20 +31,24 @@ function gqlTokenToTokenSearchResult(token: Maybe<TopToken>): TokenSearchResult
address: address ?? null,
name,
symbol,
logoUrl: project?.logoUrl ?? null,
safetyLevel: project?.safetyLevel ?? null,
safetyInfo: getCurrencySafetyInfo(project.safetyLevel, protectionInfo),
logoUrl: logo ?? null,
safetyLevel: null,
}
}
export function SearchPopularTokens(): JSX.Element {
const { popularTokens, loading } = usePopularTokens()
const tokens = useMemo(
() => popularTokens?.map(gqlTokenToTokenSearchResult).filter((t): t is TokenSearchResult => Boolean(t)),
export function SearchPopularTokens({ selectedChain }: { selectedChain: UniverseChainId | null }): JSX.Element {
const { data, isLoading } = useTokenRankingsQuery({
chainId: selectedChain?.toString() ?? ALL_NETWORKS_ARG,
})
const popularTokens = data?.tokenRankings?.[RankingType.Popularity]?.tokens.slice(0, MAX_TOKEN_RESULTS_AMOUNT)
const formattedTokens = useMemo(
() => popularTokens?.map(tokenStatsToTokenSearchResult).filter((t): t is TokenSearchResult => Boolean(t)),
[popularTokens],
)
if (loading) {
if (isLoading) {
return (
<Flex px="$spacing24" py="$spacing8">
<Loader.Token repeat={2} />
......@@ -46,7 +56,7 @@ export function SearchPopularTokens(): JSX.Element {
)
}
return <FlatList data={tokens} keyExtractor={getSearchResultId} renderItem={renderTokenItem} />
return <FlatList data={formattedTokens} keyExtractor={getSearchResultId} renderItem={renderTokenItem} />
}
const renderTokenItem = ({ item }: ListRenderItemInfo<TokenSearchResult>): JSX.Element => (
......
......@@ -21,20 +21,20 @@ export const SearchResultsLoader = (): JSX.Element => {
</Flex>
<Flex gap="$spacing12">
<SectionHeaderText
icon={<Gallery color="$neutral2" size="$icon.24" />}
title={t('explore.search.section.nft')}
icon={<Person color="$neutral2" size="$icon.24" />}
title={t('explore.search.section.wallets')}
/>
<AnimatedFlex entering={FadeIn} exiting={FadeOut} mx="$spacing24">
<Loader.Token repeat={2} />
<Loader.Token />
</AnimatedFlex>
</Flex>
<Flex gap="$spacing12">
<SectionHeaderText
icon={<Person color="$neutral2" size="$icon.24" />}
title={t('explore.search.section.wallets')}
icon={<Gallery color="$neutral2" size="$icon.24" />}
title={t('explore.search.section.nft')}
/>
<AnimatedFlex entering={FadeIn} exiting={FadeOut} mx="$spacing24">
<Loader.Token />
<Loader.Token repeat={2} />
</AnimatedFlex>
</Flex>
</Flex>
......
......@@ -14,6 +14,7 @@ import { SearchUnitagItem } from 'src/components/explore/search/items/SearchUnit
import { SearchWalletByAddressItem } from 'src/components/explore/search/items/SearchWalletByAddressItem'
import { SearchResultOrHeader } from 'src/components/explore/search/types'
import {
filterSearchResultsByChainId,
formatNFTCollectionSearchResults,
formatTokenSearchResults,
getSearchResultId,
......@@ -63,7 +64,13 @@ const EtherscanHeaderItem: (chainId: UniverseChainId) => SearchResultOrHeader =
const IGNORED_ERRORS = ['Subgraph provider undefined not supported']
export function SearchResultsSection({ searchQuery }: { searchQuery: string }): JSX.Element {
export function SearchResultsSection({
searchQuery,
selectedChain,
}: {
searchQuery: string
selectedChain: UniverseChainId | null
}): JSX.Element {
const { t } = useTranslation()
const { defaultChainId } = useEnabledChains()
......@@ -86,8 +93,14 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }):
return undefined
}
return formatTokenSearchResults(searchResultsData.searchTokens, searchQuery)
}, [searchQuery, searchResultsData])
const formattedTokenSearchResults = formatTokenSearchResults(searchResultsData.searchTokens, searchQuery)
if (!selectedChain) {
return formattedTokenSearchResults
}
return filterSearchResultsByChainId(formattedTokenSearchResults, selectedChain)
}, [selectedChain, searchQuery, searchResultsData])
// Search for matching NFT collections
......@@ -96,12 +109,22 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }):
return undefined
}
return formatNFTCollectionSearchResults(searchResultsData.nftCollections)
}, [searchResultsData])
const formattedNftCollectionSearchResults = formatNFTCollectionSearchResults(searchResultsData.nftCollections)
if (!selectedChain) {
return formattedNftCollectionSearchResults
}
return filterSearchResultsByChainId(formattedNftCollectionSearchResults, selectedChain)
}, [searchResultsData, selectedChain])
// Search for matching wallets
const { wallets: walletSearchResults, exactENSMatch, exactUnitagMatch } = useWalletSearchResults(searchQuery)
const {
wallets: walletSearchResults,
exactENSMatch,
exactUnitagMatch,
} = useWalletSearchResults(searchQuery, selectedChain)
const validAddress: Address | undefined = useMemo(
() => getValidAddress(searchQuery, true, false) ?? undefined,
......@@ -139,8 +162,8 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }):
// NFTs, then wallets, then tokens
searchResultItems = [...nftsWithHeader, ...walletsWithHeader, ...tokensWithHeader]
} else {
// Tokens, then NFTs, then wallets
searchResultItems = [...tokensWithHeader, ...nftsWithHeader, ...walletsWithHeader]
// Tokens, then wallets, then NFTs,
searchResultItems = [...tokensWithHeader, ...walletsWithHeader, ...nftsWithHeader]
}
// Add etherscan items at end
......
......@@ -8,7 +8,10 @@ import { getValidAddress } from 'uniswap/src/utils/addresses'
import { useIsSmartContractAddress } from 'wallet/src/features/transactions/send/hooks/useIsSmartContractAddress'
// eslint-disable-next-line complexity
export function useWalletSearchResults(query: string): {
export function useWalletSearchResults(
query: string,
selectedChain: UniverseChainId | null,
): {
wallets: WalletSearchResult[]
loading: boolean
exactENSMatch: boolean
......@@ -42,7 +45,7 @@ export function useWalletSearchResults(query: string): {
// Search for matching EOA wallet address
const { isSmartContractAddress, loading: loadingIsSmartContractAddress } = useIsSmartContractAddress(
validAddress,
defaultChainId,
selectedChain ?? defaultChainId,
)
const hasENSResult = dotEthName && dotEthAddress
......
......@@ -4,7 +4,7 @@ import { useDispatch } from 'react-redux'
import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks'
import { useExploreTokenContextMenu } from 'src/components/explore/hooks'
import { disableOnPress } from 'src/utils/disableOnPress'
import { Flex, ImpactFeedbackStyle, Text, TouchableArea, useIsDarkMode } from 'ui/src'
import { Flex, ImpactFeedbackStyle, Text, TouchableArea } from 'ui/src'
import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo'
import WarningIcon from 'uniswap/src/components/warnings/WarningIcon'
import { getWarningIconColorOverride } from 'uniswap/src/components/warnings/utils'
......@@ -17,7 +17,6 @@ import { getTokenWarningSeverity } from 'uniswap/src/features/tokens/safetyUtils
import { useCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { UniverseChainId } from 'uniswap/src/types/chains'
import { shortenAddress } from 'uniswap/src/utils/addresses'
import { buildCurrencyId, buildNativeCurrencyId } from 'uniswap/src/utils/currencyId'
type SearchTokenItemProps = {
......@@ -26,7 +25,6 @@ type SearchTokenItemProps = {
}
export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps): JSX.Element {
const isDarkMode = useIsDarkMode()
const dispatch = useDispatch()
const tokenDetailsNavigation = useTokenDetailsNavigation()
......@@ -99,13 +97,6 @@ export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps):
<Text color="$neutral2" numberOfLines={1} variant="subheading2">
{symbol}
</Text>
{address && (
<Flex shrink>
<Text color={isDarkMode ? '$neutral3' : '$neutral2'} numberOfLines={1} variant="subheading2">
{shortenAddress(address)}
</Text>
</Flex>
)}
</Flex>
</Flex>
</Flex>
......
......@@ -9,11 +9,19 @@ import {
TokenSearchResult,
} from 'uniswap/src/features/search/SearchResult'
import { searchResultId } from 'uniswap/src/features/search/searchHistorySlice'
import { UniverseChainId } from 'uniswap/src/types/chains'
const MAX_TOKEN_RESULTS_COUNT = 4
type ExploreSearchResult = NonNullable<ExploreSearchQuery>
export function filterSearchResultsByChainId<T extends { chainId: null | UniverseChainId }>(
tokenSearchResults: Array<T> | undefined,
chainId: UniverseChainId | null,
): Array<T> | undefined {
return tokenSearchResults?.filter((searchResult): boolean => chainId === null || searchResult.chainId === chainId)
}
// Formats the tokens portion of explore search results into sorted array of TokenSearchResult
export function formatTokenSearchResults(
data: ExploreSearchResult['searchTokens'],
......
......@@ -47,14 +47,17 @@ export function HeaderScrollScreen({
const scrollY = useSharedValue(0)
// On scroll, centerElement and the bottom border fade in
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
scrollY.value = event.contentOffset.y
const scrollHandler = useAnimatedScrollHandler(
{
onScroll: (event) => {
scrollY.value = event.contentOffset.y
},
onEndDrag: (event) => {
scrollY.value = withTiming(event.contentOffset.y > 0 ? SHOW_HEADER_SCROLL_Y_DISTANCE : 0)
},
},
onEndDrag: (event) => {
scrollY.value = withTiming(event.contentOffset.y > 0 ? SHOW_HEADER_SCROLL_Y_DISTANCE : 0)
},
})
[scrollY],
)
return (
<Screen backgroundColor={backgroundColor} edges={['top', 'left', 'right']} noInsets={fullScreen}>
......
import React, { memo, PropsWithChildren } from 'react'
import { Flex } from 'ui/src'
import { useSelectAddressHasNotifications } from 'wallet/src/features/notifications/hooks'
import { useSelectAddressHasNotifications } from 'uniswap/src/features/notifications/hooks'
type Props = PropsWithChildren<{
address: Address
......
import { TokenItemData } from 'src/components/explore/TokenItemData'
import { AppTFunction } from 'ui/src/i18n/types'
import { TokenSortableField } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { ClientTokensOrderBy, TokenMetadataDisplayType, TokensOrderBy } from 'wallet/src/features/wallet/types'
/**
* Returns server and client orderBy values to use for topTokens query and client side sorting
*
* Uses server side sort by Volume if applying a client side sort after
* ex. % change sorting use the top 100 tokens by Uniswap Volume, then sorts by % change
*
* Note that server side sort by Volume (TokenSortableField.Volume) requires an
* additional client side sort because there may be a discrepancy in the server's
* sort by Volume list which is calculated once per 24h and each token's
* token.market.volume which is updated more frequently
*
* @param orderBy currently selected TokensOrderBy value to sort tokens by
* @returns serverOrderBy to be used in topTokens query, clientOrderBy to be used to determine if client side sort is necessary
*/
export function getTokensOrderByValues(orderBy: TokensOrderBy): {
serverOrderBy: TokenSortableField
clientOrderBy: ClientTokensOrderBy | undefined
} {
const requiresClientOrderBy = Object.values<string>(ClientTokensOrderBy).includes(orderBy)
return {
serverOrderBy: requiresClientOrderBy ? TokenSortableField.Volume : (orderBy as TokenSortableField),
clientOrderBy: requiresClientOrderBy
? (orderBy as ClientTokensOrderBy)
: orderBy === TokenSortableField.Volume
? ClientTokensOrderBy.Volume24hDesc
: undefined,
}
}
/**
* Returns a compare function to sort tokens client side.
*/
export function getClientTokensOrderByCompareFn(
orderBy: ClientTokensOrderBy,
): (a: TokenItemData, b: TokenItemData) => number {
let compareField: keyof TokenItemData
let direction = 0
import {
CustomRankingType,
ExploreOrderBy,
RankingType,
TokenMetadataDisplayType,
} from 'wallet/src/features/wallet/types'
export function getTokenMetadataDisplayType(orderBy: ExploreOrderBy): TokenMetadataDisplayType {
switch (orderBy) {
case ClientTokensOrderBy.PriceChangePercentage24hAsc:
compareField = 'pricePercentChange24h'
direction = 1
break
case ClientTokensOrderBy.PriceChangePercentage24hDesc:
compareField = 'pricePercentChange24h'
direction = -1
break
case ClientTokensOrderBy.Volume24hDesc:
compareField = 'volume24h'
direction = -1
break
}
return (a: TokenItemData, b: TokenItemData) => {
// undefined values sort to bottom
if (a[compareField] === undefined) {
return 1
}
if (b[compareField] === undefined) {
return -1
}
return Number(a[compareField]) - Number(b[compareField]) > 0 ? direction : -1 * direction
}
}
export function getTokenMetadataDisplayType(orderBy: TokensOrderBy): TokenMetadataDisplayType {
switch (orderBy) {
case TokenSortableField.MarketCap:
case RankingType.MarketCap:
return TokenMetadataDisplayType.MarketCap
case TokenSortableField.Volume:
case RankingType.Volume:
return TokenMetadataDisplayType.Volume
case TokenSortableField.TotalValueLocked:
case RankingType.TotalValueLocked:
return TokenMetadataDisplayType.TVL
case ClientTokensOrderBy.PriceChangePercentage24hDesc:
return TokenMetadataDisplayType.Symbol
case ClientTokensOrderBy.PriceChangePercentage24hAsc:
case CustomRankingType.PricePercentChange1DayDesc:
case CustomRankingType.PricePercentChange1DayAsc:
return TokenMetadataDisplayType.Symbol
default:
throw new Error('Unexpected order by value ' + orderBy)
......@@ -87,17 +23,17 @@ export function getTokenMetadataDisplayType(orderBy: TokensOrderBy): TokenMetada
}
// Label shown in the popover context menu.
export function getTokensOrderByMenuLabel(orderBy: TokensOrderBy, t: AppTFunction): string {
export function getTokensOrderByMenuLabel(orderBy: ExploreOrderBy, t: AppTFunction): string {
switch (orderBy) {
case TokenSortableField.MarketCap:
case RankingType.MarketCap:
return t('explore.tokens.sort.option.marketCap')
case TokenSortableField.Volume:
case RankingType.Volume:
return t('explore.tokens.sort.option.volume')
case TokenSortableField.TotalValueLocked:
case RankingType.TotalValueLocked:
return t('explore.tokens.sort.option.totalValueLocked')
case ClientTokensOrderBy.PriceChangePercentage24hDesc:
case CustomRankingType.PricePercentChange1DayDesc:
return t('explore.tokens.sort.option.priceIncrease')
case ClientTokensOrderBy.PriceChangePercentage24hAsc:
case CustomRankingType.PricePercentChange1DayAsc:
return t('explore.tokens.sort.option.priceDecrease')
default:
throw new Error('Unexpected order by value ' + orderBy)
......@@ -105,17 +41,17 @@ export function getTokensOrderByMenuLabel(orderBy: TokensOrderBy, t: AppTFunctio
}
// Label shown when option is selected in dropdown.
export function getTokensOrderBySelectedLabel(orderBy: TokensOrderBy, t: AppTFunction): string {
export function getTokensOrderBySelectedLabel(orderBy: ExploreOrderBy, t: AppTFunction): string {
switch (orderBy) {
case TokenSortableField.MarketCap:
case RankingType.MarketCap:
return t('explore.tokens.sort.label.marketCap')
case TokenSortableField.Volume:
case RankingType.Volume:
return t('explore.tokens.sort.label.volume')
case TokenSortableField.TotalValueLocked:
case RankingType.TotalValueLocked:
return t('explore.tokens.sort.label.totalValueLocked')
case ClientTokensOrderBy.PriceChangePercentage24hDesc:
case CustomRankingType.PricePercentChange1DayDesc:
return t('explore.tokens.sort.label.priceIncrease')
case ClientTokensOrderBy.PriceChangePercentage24hAsc:
case CustomRankingType.PricePercentChange1DayAsc:
return t('explore.tokens.sort.label.priceDecrease')
default:
throw new Error('Unexpected order by value in option text ' + orderBy)
......
......@@ -10,6 +10,8 @@ import { Flex, TouchableArea, useHapticFeedback } from 'ui/src'
import { iconSizes } from 'ui/src/theme'
import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains'
import { uniswapUrls } from 'uniswap/src/constants/urls'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType, CopyNotificationType } from 'uniswap/src/features/notifications/types'
import { useEnabledChains } from 'uniswap/src/features/settings/hooks'
import { ElementName, WalletEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
......@@ -19,8 +21,6 @@ import { ShareableEntity } from 'uniswap/src/types/sharing'
import { setClipboard } from 'uniswap/src/utils/clipboard'
import { ExplorerDataType, getExplorerLink, openUri } from 'uniswap/src/utils/linking'
import { logger } from 'utilities/src/logger/logger'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types'
import { getProfileUrl } from 'wallet/src/utils/linking'
type MenuAction = {
......
......@@ -2,7 +2,6 @@ import React, { memo, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { StatusBar, StyleSheet } from 'react-native'
import { FadeIn } from 'react-native-reanimated'
import Svg, { ClipPath, Defs, RadialGradient, Rect, Stop } from 'react-native-svg'
import { useDispatch, useSelector } from 'react-redux'
import { BackButton } from 'src/components/buttons/BackButton'
import { Favorite } from 'src/components/icons/Favorite'
......@@ -35,6 +34,7 @@ import { CurrencyField } from 'uniswap/src/types/currency'
import { openUri } from 'uniswap/src/utils/linking'
import { RecipientSelectSpeedBumps } from 'wallet/src/components/RecipientSearch/RecipientSelectSpeedBumps'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { HeaderRadial, solidHeaderProps } from 'wallet/src/features/unitags/HeaderRadial'
import { useDisplayName } from 'wallet/src/features/wallet/hooks'
import { DisplayNameType } from 'wallet/src/features/wallet/types'
......@@ -45,13 +45,6 @@ interface ProfileHeaderProps {
address: Address
}
const HEADER_SOLID_COLOR_OPACITY = 0.1
export const solidHeaderProps = {
minOpacity: HEADER_SOLID_COLOR_OPACITY,
maxOpacity: HEADER_SOLID_COLOR_OPACITY,
}
export const ProfileHeader = memo(function ProfileHeader({ address }: ProfileHeaderProps): JSX.Element {
const colors = useSporeColors()
const dispatch = useDispatch()
......@@ -265,40 +258,6 @@ export const ProfileHeader = memo(function ProfileHeader({ address }: ProfileHea
)
})
export const HeaderRadial = memo(function HeaderRadial({
color,
borderRadius,
minOpacity,
maxOpacity,
}: {
color: string
borderRadius?: number
minOpacity?: number
maxOpacity?: number
}): JSX.Element {
return (
<Svg height="100%" width="100%">
<Defs>
<ClipPath id="clip">
<Rect height="100%" rx={borderRadius} width="100%" />
</ClipPath>
<RadialGradient cy="-0.1" id="background" rx="0.8" ry="1.1">
<Stop offset="0" stopColor={color} stopOpacity={maxOpacity ?? '0.6'} />
<Stop offset="1" stopColor={color} stopOpacity={minOpacity ?? '0'} />
</RadialGradient>
</Defs>
<Rect
clipPath={borderRadius ? 'url(#clip)' : undefined}
fill="url(#background)"
height="100%"
width="100%"
x="0"
y="0"
/>
</Svg>
)
})
const styles = StyleSheet.create({
buttonShadow: {
elevation: 2,
......
......@@ -165,6 +165,35 @@ export const FiatOnRampAmountSection = forwardRef<FiatOnRampAmountSectionRef, Fi
currency.currencyInfo?.currency,
)
// Workaround to avoid incorrect input width calculations by react-native
// Decimal numbers were manually calculated for Basel Grotesk fonts and will
// require an adjustment when the font is changed
const calculatedInputWidth = [...value].reduce(
(acc, numStr) => {
switch (numStr) {
case '1':
return acc + fontSize * 0.393
case '2':
case '6':
case '8':
return acc + fontSize * 0.596
case '3':
return acc + fontSize * 0.595
case '4':
case '0':
default:
return acc + fontSize * 0.62
case '5':
case '7':
return acc + fontSize * 0.602
case '9':
return acc + fontSize * 0.607
}
},
// ensures a proper width for a "0" placeholder or adds 3 points for the input caret
value.length === 0 ? fontSize * 0.62 : 3,
)
return (
<Flex
alignItems="center"
......@@ -213,6 +242,7 @@ export const FiatOnRampAmountSection = forwardRef<FiatOnRampAmountSectionRef, Fi
placeholderTextColor="$neutral3"
px="$none"
py="$none"
minWidth={calculatedInputWidth}
returnKeyType={undefined}
showSoftInputOnFocus={false}
textAlign="left"
......
import React from 'react'
import { useSelector } from 'react-redux'
import { ScantasticCompleteNotification } from 'src/features/notifications/ScantasticCompleteNotification'
import { WCNotification } from 'src/features/notifications/WCNotification'
import { useSelectAddressNotifications } from 'uniswap/src/features/notifications/hooks'
import { AppNotification, AppNotificationType } from 'uniswap/src/features/notifications/types'
import { SharedNotificationToastRouter } from 'wallet/src/features/notifications/components/SharedNotificationToastRouter'
import { selectActiveAccountNotifications } from 'wallet/src/features/notifications/selectors'
import { AppNotification, AppNotificationType } from 'wallet/src/features/notifications/types'
import { useActiveAccountAddress } from 'wallet/src/features/wallet/hooks'
export function NotificationToastWrapper(): JSX.Element | null {
const notifications = useSelector(selectActiveAccountNotifications)
const activeAccountAddress = useActiveAccountAddress()
const notifications = useSelectAddressNotifications(activeAccountAddress)
const notification = notifications?.[0]
if (!notification) {
......
......@@ -2,8 +2,8 @@ import React from 'react'
import { useTranslation } from 'react-i18next'
import { Flex } from 'ui/src'
import { Check, Laptop } from 'ui/src/components/icons'
import { ScantasticCompleteNotification as ScantasticCompleteNotificationType } from 'uniswap/src/features/notifications/types'
import { NotificationToast } from 'wallet/src/features/notifications/components/NotificationToast'
import { ScantasticCompleteNotification as ScantasticCompleteNotificationType } from 'wallet/src/features/notifications/types'
export function ScantasticCompleteNotification({
notification: { hideDelay },
......
......@@ -3,13 +3,13 @@ import { useDispatch } from 'react-redux'
import { openModal } from 'src/features/modals/modalSlice'
import { iconSizes } from 'ui/src/theme'
import { toSupportedChainId } from 'uniswap/src/features/chains/utils'
import { WalletConnectNotification } from 'uniswap/src/features/notifications/types'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { WalletConnectEvent } from 'uniswap/src/types/walletConnect'
import { DappLogoWithTxStatus } from 'wallet/src/components/CurrencyLogo/LogoWithTxStatus'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
import { NotificationToast } from 'wallet/src/features/notifications/components/NotificationToast'
import { NOTIFICATION_ICON_SIZE } from 'wallet/src/features/notifications/constants'
import { WalletConnectNotification } from 'wallet/src/features/notifications/types'
import { formWCNotificationTitle } from 'wallet/src/features/notifications/utils'
export function WCNotification({ notification }: { notification: WalletConnectNotification }): JSX.Element {
......
......@@ -10,12 +10,12 @@ import { AlertTriangleFilled, Faceid, Laptop, LinkBrokenHorizontal, Wifi } from
import { iconSizes } from 'ui/src/theme'
import { Modal } from 'uniswap/src/components/modals/Modal'
import { uniswapUrls } from 'uniswap/src/constants/urls'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { logger } from 'utilities/src/logger/logger'
import { ONE_MINUTE_MS, ONE_SECOND_MS } from 'utilities/src/time/time'
import { useInterval } from 'utilities/src/time/timing'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType } from 'wallet/src/features/notifications/types'
import { useSignerAccounts } from 'wallet/src/features/wallet/hooks'
import { getOtpDurationString } from 'wallet/src/utils/duration'
......
......@@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'
import { navigate } from 'src/app/navigation/rootNavigation'
import { UnitagStackScreenProp } from 'src/app/navigation/types'
import { Screen } from 'src/components/layout/Screen'
import { UnitagWithProfilePicture } from 'src/components/unitags/UnitagWithProfilePicture'
import { AnimatePresence, Button, Flex, Text } from 'ui/src'
import { AnimateInOrder } from 'ui/src/animations'
import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions'
......@@ -21,6 +20,7 @@ import {
SwapElement,
TextElement,
} from 'wallet/src/components/landing/elements'
import { UnitagWithProfilePicture } from 'wallet/src/features/unitags/UnitagWithProfilePicture'
import { UNITAG_SUFFIX } from 'wallet/src/features/unitags/constants'
export function UnitagConfirmationScreen({
......
import { StackActions } from '@react-navigation/core'
import { dispatchNavigationAction } from 'src/app/navigation/rootNavigation'
import { call, put, takeEvery } from 'typed-redux-saga'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import i18n from 'uniswap/src/i18n/i18n'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType } from 'wallet/src/features/notifications/types'
import { restoreMnemonicComplete } from 'wallet/src/features/wallet/slice'
/**
......
......@@ -2,12 +2,11 @@
import { NativeModules } from 'react-native'
import { isAndroid } from 'utilities/src/platform'
const { RNWalletConnect } = NativeModules
const { RNWalletConnect, RedirectToSourceApp } = NativeModules
export const returnToPreviousApp = (): boolean => {
// TOOD(MOB-1680): Implement return to previous app for Android
export const returnToPreviousApp = async (): Promise<boolean> => {
if (isAndroid) {
return false
return RedirectToSourceApp.moveAppToBackground()
}
return RNWalletConnect.returnToPreviousApp()
}
import { WalletConnectState } from 'src/features/walletConnect/walletConnectSlice'
import { logger } from 'utilities/src/logger/logger'
export function fetchDappDetails(
topic: string,
currentState: Readonly<WalletConnectState>,
): { dappIcon: string | null; dappName: string } {
try {
const sessions = Object.values(currentState.byAccount).find((account) => account?.sessions?.[topic])?.sessions
if (sessions && sessions[topic]) {
const wcSession = sessions[topic]
return {
dappIcon: wcSession?.dapp?.icon || null,
dappName: wcSession?.dapp?.name || '',
}
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Error retrieving session data'
logger.warn('walletConnect/saga.ts', 'createDappNotification', message)
}
return { dappIcon: null, dappName: '' }
}
import { AnyAction } from '@reduxjs/toolkit'
import { IWalletKit, WalletKit, WalletKitTypes } from '@reown/walletkit'
import { Core } from '@walletconnect/core'
import '@walletconnect/react-native-compat'
import { PendingRequestTypes, ProposalTypes } from '@walletconnect/types'
import { buildApprovedNamespaces, getSdkError } from '@walletconnect/utils'
import { IWeb3Wallet, Web3Wallet, Web3WalletTypes } from '@walletconnect/web3wallet'
import { Alert } from 'react-native'
import { EventChannel, eventChannel } from 'redux-saga'
import { MobileState } from 'src/app/mobileReducer'
import { registerWCClientForPushNotifications } from 'src/features/walletConnect/api'
import { fetchDappDetails } from 'src/features/walletConnect/fetchDappDetails'
import {
getAccountAddressFromEIP155String,
getChainIdFromEIP155String,
......@@ -24,13 +26,16 @@ import {
import { call, fork, put, select, take } from 'typed-redux-saga'
import { config } from 'uniswap/src/config'
import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import i18n from 'uniswap/src/i18n/i18n'
import { COMBINED_CHAIN_IDS, UniverseChainId } from 'uniswap/src/types/chains'
import { EthEvent, EthMethod } from 'uniswap/src/types/walletConnect'
import { EthEvent, EthMethod, WalletConnectEvent } from 'uniswap/src/types/walletConnect'
import { logger } from 'utilities/src/logger/logger'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { selectAccounts, selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors'
export let wcWeb3Wallet: IWeb3Wallet
export let wcWeb3Wallet: IWalletKit
let wcWeb3WalletReadyResolve: () => void
let wcWeb3WalletReadyReject: (e: unknown) => void
......@@ -38,7 +43,6 @@ const wcWeb3WalletReady = new Promise<void>((resolve, reject) => {
wcWeb3WalletReadyResolve = resolve
wcWeb3WalletReadyReject = reject
})
export const waitForWcWeb3WalletIsReady = () => wcWeb3WalletReady
export async function initializeWeb3Wallet(): Promise<void> {
......@@ -47,7 +51,7 @@ export async function initializeWeb3Wallet(): Promise<void> {
projectId: config.walletConnectProjectId,
})
wcWeb3Wallet = await Web3Wallet.init({
wcWeb3Wallet = await WalletKit.init({
core: wcCore,
metadata: {
name: 'Uniswap Wallet',
......@@ -58,7 +62,7 @@ export async function initializeWeb3Wallet(): Promise<void> {
},
})
const clientId = await wcCore.crypto.getClientId()
const clientId = await wcWeb3Wallet.engine.signClient.core.crypto.getClientId()
await registerWCClientForPushNotifications(clientId)
wcWeb3WalletReadyResolve?.()
} catch (e) {
......@@ -73,17 +77,17 @@ function createWalletConnectChannel(): EventChannel<AnyAction> {
* and the proposal namespaces (chains, methods, events)
*/
const sessionProposalHandler = async (
proposalEvent: Omit<Web3WalletTypes.BaseEventArgs<ProposalTypes.Struct>, 'topic'>,
proposalEvent: Omit<WalletKitTypes.BaseEventArgs<ProposalTypes.Struct>, 'topic'>,
): Promise<void> => {
const { params: proposal } = proposalEvent
emit({ type: 'session_proposal', proposal })
}
const sessionRequestHandler = async (request: Web3WalletTypes.SessionRequest): Promise<void> => {
const sessionRequestHandler = async (request: WalletKitTypes.SessionRequest): Promise<void> => {
emit({ type: 'session_request', request })
}
const sessionDeleteHandler = async (session: Web3WalletTypes.SessionDelete): Promise<void> => {
const sessionDeleteHandler = async (session: WalletKitTypes.SessionDelete): Promise<void> => {
emit({ type: 'session_delete', session })
}
......@@ -133,6 +137,28 @@ function showAlert(title: string, message: string): Promise<boolean> {
})
}
function* cancelErrorSession(dappName: string, chainLabels: string, proposalId: number) {
yield* call([wcWeb3Wallet, wcWeb3Wallet.rejectSession], {
id: proposalId,
reason: getSdkError('UNSUPPORTED_CHAINS'),
})
yield* call(
showAlert,
i18n.t('walletConnect.error.connection.title'),
i18n.t('walletConnect.error.connection.message', {
chainNames: chainLabels,
dappName,
}),
)
// Set error state to cancel loading state in WalletConnectModal UI
yield* put(setHasPendingSessionError(true))
// Allow users to rescan again
yield* put(setHasPendingSessionError(false))
}
function* handleSessionProposal(proposal: ProposalTypes.Struct) {
const activeAccountAddress = yield* select(selectActiveAccountAddress)
......@@ -141,6 +167,15 @@ function* handleSessionProposal(proposal: ProposalTypes.Struct) {
proposer: { metadata: dapp },
} = proposal
const namespaceCheck = proposal.requiredNamespaces
const firstNamespace = Object.keys(namespaceCheck)[0]
if (firstNamespace && firstNamespace !== 'eip155') {
const chainLabels = COMBINED_CHAIN_IDS.map((chainId) => UNIVERSE_CHAIN_INFO[chainId].label).join(', ')
yield* cancelErrorSession(dapp.name, chainLabels, proposal.id)
return
}
try {
const supportedEip155Chains = COMBINED_CHAIN_IDS.map((chainId) => `eip155:${chainId}`)
const accounts = supportedEip155Chains.map((chain) => `${chain}:${activeAccountAddress}`)
......@@ -188,28 +223,9 @@ function* handleSessionProposal(proposal: ProposalTypes.Struct) {
}),
)
} catch (e) {
// Reject pending session if required namespaces includes non-EVM chains or unsupported EVM chains
yield* call([wcWeb3Wallet, wcWeb3Wallet.rejectSession], {
id: proposal.id,
reason: getSdkError('UNSUPPORTED_CHAINS'),
})
const chainLabels = COMBINED_CHAIN_IDS.map((chainId) => UNIVERSE_CHAIN_INFO[chainId].label).join(', ')
const confirmed = yield* call(
showAlert,
i18n.t('walletConnect.error.connection.title'),
i18n.t('walletConnect.error.connection.message', {
chainNames: chainLabels,
dappName: dapp.name,
}),
)
if (confirmed) {
yield* put(setHasPendingSessionError(false))
}
// Set error state to cancel loading state in WalletConnectModal UI
yield* put(setHasPendingSessionError(true))
yield* cancelErrorSession(dapp.name, chainLabels, proposal.id)
logger.debug(
'WalletConnectSaga',
......@@ -273,9 +289,23 @@ function* handleSessionRequest(sessionRequest: PendingRequestTypes.Struct) {
}
}
function* handleSessionDelete(event: Web3WalletTypes.SessionDelete) {
function* handleSessionDelete(event: WalletKitTypes.SessionDelete) {
const { topic } = event
const currentState = yield* select((state: MobileState) => state.walletConnect)
const { dappName, dappIcon } = fetchDappDetails(topic, currentState)
yield* put(
pushNotification({
type: AppNotificationType.WalletConnect,
event: WalletConnectEvent.Disconnected,
dappName,
imageUrl: dappIcon,
hideDelay: 3 * ONE_SECOND_MS,
}),
)
yield* put(removeSession({ sessionId: topic }))
}
......
......@@ -3,14 +3,14 @@ import { wcWeb3Wallet } from 'src/features/walletConnect/saga'
import { TransactionRequest, UwuLinkErc20Request } from 'src/features/walletConnect/walletConnectSlice'
import { call, put } from 'typed-redux-saga'
import { AssetType } from 'uniswap/src/entities/assets'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import { getEnabledChainIdsSaga } from 'uniswap/src/features/settings/saga'
import { TransactionOriginType, TransactionType } from 'uniswap/src/features/transactions/types/transactionDetails'
import { UniverseChainId } from 'uniswap/src/types/chains'
import { DappInfo, EthMethod, EthSignMethod, UwULinkMethod, WalletConnectEvent } from 'uniswap/src/types/walletConnect'
import { createSaga } from 'uniswap/src/utils/saga'
import { logger } from 'utilities/src/logger/logger'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType } from 'wallet/src/features/notifications/types'
import { SendTransactionParams, sendTransaction } from 'wallet/src/features/transactions/sendTransactionSaga'
import { Account } from 'wallet/src/features/wallet/accounts/types'
import { getSignerManager } from 'wallet/src/features/wallet/context'
......
import {
decodeMessage,
getAccountAddressFromEIP155String,
getChainIdFromEIP155String,
getSupportedWalletConnectChains,
isHexString,
} from 'src/features/walletConnect/utils'
import { UniverseChainId } from 'uniswap/src/types/chains'
......@@ -56,3 +58,41 @@ describe(getChainIdFromEIP155String, () => {
expect(getChainIdFromEIP155String(EIP155_LINEA_UNSUPPORTED)).toBeNull()
})
})
describe('isHexString', () => {
test('should return true for valid hex string', () => {
const validHex = '0x5468697320697320612074657374206d657373616765'
expect(isHexString(validHex)).toBe(true)
})
test('should return false for invalid hex string', () => {
const invalidHex = '546869732069732G20612074657374206d657373616765'
expect(isHexString(invalidHex)).toBe(false)
})
test('should return false for plain text', () => {
const plainText = 'This is a plain text message'
expect(isHexString(plainText)).toBe(false)
})
})
describe('decodeMessage', () => {
test('should decode hex-encoded message', () => {
const hexMessage = '0x5468697320697320612074657374206d657373616765'
const expectedMessage = 'This is a test message'
const result = decodeMessage(hexMessage)
expect(result).toBe(expectedMessage)
})
test('should return original hex string if decoding fails', () => {
const invalidHex = '0x12345'
const result = decodeMessage(invalidHex)
expect(result).toBe(invalidHex)
})
test('should return plain text message unchanged', () => {
const plainText = 'This is a plain text message'
const result = decodeMessage(plainText)
expect(result).toBe(plainText)
})
})
import { WalletKitTypes } from '@reown/walletkit'
import { PairingTypes, ProposalTypes, SessionTypes, SignClientTypes } from '@walletconnect/types'
import { Web3WalletTypes } from '@walletconnect/web3wallet'
import { utils } from 'ethers'
import { wcWeb3Wallet } from 'src/features/walletConnect/saga'
import { SignRequest, TransactionRequest } from 'src/features/walletConnect/walletConnectSlice'
......@@ -78,7 +78,7 @@ export const getAccountAddressFromEIP155String = (account: string): Address | nu
* @param {number} internalId id for the WalletConnect signature request
* @param {ChainId} chainId chain the signature is being requested on
* @param {SignClientTypes.Metadata} dapp metadata for the dapp requesting the signature
* @param {Web3WalletTypes.SessionRequest['params']['request']['params']} requestParams parameters of the request
* @param {WalletKitTypes.SessionRequest['params']['request']['params']} requestParams parameters of the request
* @returns {{Address, SignRequest}} address of the account receiving the request and formatted SignRequest object
*/
export const parseSignRequest = (
......@@ -87,7 +87,7 @@ export const parseSignRequest = (
internalId: number,
chainId: UniverseChainId,
dapp: SignClientTypes.Metadata,
requestParams: Web3WalletTypes.SessionRequest['params']['request']['params'],
requestParams: WalletKitTypes.SessionRequest['params']['request']['params'],
): { account: Address; request: SignRequest } => {
const { address, rawMessage, message } = getAddressAndMessageToSign(method, requestParams)
return {
......@@ -120,7 +120,7 @@ export const parseSignRequest = (
* @param {number} internalId id for the WalletConnect transaction request
* @param {UniverseChainId} chainId chain the signature is being requested on
* @param {SignClientTypes.Metadata} dapp metadata for the dapp requesting the transaction
* @param {Web3WalletTypes.SessionRequest['params']['request']['params']} requestParams parameters of the request
* @param {WalletKitTypes.SessionRequest['params']['request']['params']} requestParams parameters of the request
* @returns {{Address, TransactionRequest}} address of the account receiving the request and formatted TransactionRequest object
*/
export const parseTransactionRequest = (
......@@ -129,7 +129,7 @@ export const parseTransactionRequest = (
internalId: number,
chainId: UniverseChainId,
dapp: SignClientTypes.Metadata,
requestParams: Web3WalletTypes.SessionRequest['params']['request']['params'],
requestParams: WalletKitTypes.SessionRequest['params']['request']['params'],
): { account: Address; request: TransactionRequest } => {
// Omit gasPrice and nonce in tx sent from dapp since it is calculated later
const { from, to, data, gasLimit, value } = requestParams[0]
......@@ -159,6 +159,27 @@ export const parseTransactionRequest = (
}
}
export function isHexString(value: string): boolean {
// Check if it starts with '0x' and has an even length after the prefix
return /^0x[0-9a-fA-F]+$/.test(value)
}
export function decodeMessage(value: string): string {
if (isHexString(value)) {
try {
return utils.toUtf8String(value)
} catch (error) {
logger.error(error, {
tags: { file: 'walletConnect/util.ts', function: 'decodeMessage' },
})
return value
}
} else {
return value
}
}
/**
* Gets the address receiving the request, raw message, decoded message to sign based on the EthSignMethod.
* `personal_sign` params are ordered as [message, account]
......@@ -169,11 +190,11 @@ export const parseTransactionRequest = (
// eslint-disable-next-line consistent-return
export function getAddressAndMessageToSign(
ethMethod: EthSignMethod,
params: Web3WalletTypes.SessionRequest['params']['request']['params'],
params: WalletKitTypes.SessionRequest['params']['request']['params'],
): { address: string; rawMessage: string; message: string | null } {
switch (ethMethod) {
case EthMethod.PersonalSign:
return { address: params[1], rawMessage: params[0], message: utils.toUtf8String(params[0]) }
return { address: params[1], rawMessage: params[0], message: decodeMessage(params[0]) }
case EthMethod.EthSign:
return { address: params[0], rawMessage: params[1], message: utils.toUtf8String(params[1]) }
case EthMethod.SignTypedData:
......@@ -184,7 +205,7 @@ export function getAddressAndMessageToSign(
export async function pairWithWalletConnectURI(uri: string): Promise<void | PairingTypes.Struct> {
try {
return await wcWeb3Wallet.core.pairing.pair({ uri })
return await wcWeb3Wallet.pair({ uri })
} catch (error) {
logger.error(error, {
tags: { file: 'walletConnectV2/utils', function: 'pairWithWalletConnectURI' },
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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