ci(release): publish latest release

parent a745d199
---
name: Bug Report
about: Report a bug or unexpected behavior in the Uniswap interfaces.
title: "[Bug] "
labels: bug
---
## 📱 Interface Affected
Which application are you experiencing issues with?
- [ ] Web ([app.uniswap.org](https://app.uniswap.org))
- [ ] Wallet Extension ([wallet.uniswap.org](https://wallet.uniswap.org))
- [ ] Wallet Mobile App
- [ ] iOS
- [ ] Android
- [ ] Both
---
## 🧩 App Version
- Version (if known):
- [ ] Production build
- [ ] Development build
---
## 💻 System / Environment Info
Please provide details about your environment:
- Browser (name + version):
- OS / Platform (e.g. iOS 17, Windows 11, Android 14):
- Device (e.g. iPhone 14 Pro, Pixel 7, MacBook Pro 2023):
- Wallet used (e.g. Uniswap Wallet, MetaMask, Rainbow):
- Network (e.g. Ethereum Mainnet, Arbitrum, Base, etc.):
---
## 🔁 Steps to Reproduce
1. Go to '...'
2. Click on '...'
3. Observe the issue
---
## ✅ Expected Behavior
What should have happened?
---
## ❌ Actual Behavior
What actually happened?
---
## 📸 Screenshots or Screen Recording
Please upload any relevant screenshots or recordings to help us understand the issue better.
---
## 🧾 Additional Context
Any extra details? (e.g. logs, error messages, recent updates, beta flags enabled, etc.)
---
⚠️ *Please redact or avoid sharing sensitive data such as private keys, seed phrases, or personally identifying info.*
# Contributing to Uniswap Interface
👋 Thanks for your interest in contributing to Uniswap!
This repository is the **public mirror** of Uniswap Labs' front-end interfaces, including the web app, wallet mobile app, and wallet browser extension.
## Development Workflow
Uniswap Labs maintains and develops all interfaces in a **private repository**. At the end of each development cycle:
1. A **production release** is created internally.
2. The release is then **published to this public repository**.
3. All releases are tagged and visible in the [Releases](https://github.com/Uniswap/interface/releases) tab.
Because of this private development model:
**We do not accept pull requests to this repository.**
## How You *Can* Contribute
We still welcome your ideas, feedback, and issue reports. The best ways to contribute are:
### Reporting Bugs
Open a [GitHub Issue](https://github.com/Uniswap/interface/issues/new?template=bug_report.md) and fill out the template. Be sure to include:
- Which app is affected (web, mobile, or extension)
- Platform (iOS, Android, browser version, etc.)
- App version (Production or dev)
- Steps to reproduce, screenshots, logs, etc.
### Suggesting Features or Improvements
Start a [Discussion](https://github.com/Uniswap/interface/discussions) to propose ideas, gather feedback, or brainstorm improvements.
## Repo Overview
- Review the [README](README.md) to understand the repo's general architecture.
# Uniswap Labs: Front End Interfaces
An open source repository for all Uniswap front end interfaces maintained by Uniswap Labs. Uniswap is a protocol for decentralized exchange of Ethereum tokens.
This is the **public** repository for Uniswap Labs’ front-end interfaces, including the Web App, Wallet Mobile App, and Wallet Extension. Uniswap is a protocol for decentralized exchange of Ethereum-based assets.
## Interfaces
- Web: [app.uniswap.org](https://app.uniswap.org)
- Wallet (mobile + extension): [wallet.uniswap.org](https://wallet.uniswap.org)
## Install & Apps
```bash
git clone git@github.com:Uniswap/interface.git
yarn
yarn lfg
yarn web start
```
For instructions per application or package, see the README published for each application:
- [Web](apps/web/README.md)
- [Mobile](apps/mobile/README.md)
- [Extension](apps/extension/README.md)
## Contributing
For instructions on the best way to contribute, please review our [Contributing guide](CONTRIBUTING.md)!
## Socials / Contact
- Twitter: [@Uniswap](https://twitter.com/Uniswap)
- X (Formerly Twitter): [@Uniswap](https://x.com/Uniswap)
- Reddit: [/r/Uniswap](https://www.reddit.com/r/Uniswap/)
- Email: [contact@uniswap.org](mailto:contact@uniswap.org)
- Discord: [Uniswap](https://discord.gg/FCfyBSbCU5)
- Discord: [Uniswap](https://discord.com/invite/uniswap)
- LinkedIn: [Uniswap Labs](https://www.linkedin.com/company/uniswaporg)
## Uniswap Links
......@@ -26,26 +46,14 @@ An open source repository for all Uniswap front end interfaces maintained by Uni
- [V2](https://uniswap.org/whitepaper.pdf)
- [V1](https://hackmd.io/C-DvwDSfSxuh-Gd4WKE_ig)
## Apps
For instructions per application or package, see the README published for each application:
- [Web](apps/web/README.md)
- [Mobile](apps/mobile/README.md)
- [Extension](apps/extension/README.md)
## Releases
All interface releases are tagged and published to this repository. To browse them easily, see the [Github releases tab](https://github.com/Uniswap/interface/releases).
## Production & Release Process
## Translations
Uniswap Labs develops all front-end interfaces in a private repository.
At the end of each development cycle:
Translations for our applications are done through [crowdin](https://crowdin.com).
1. We publish the latest production-ready code to this public repository.
| App | Coverage |
| ------- | -------- |
| web | [![Crowdin](https://badges.crowdin.net/uniswap-interface/localized.svg)](https://crowdin.com/project/uniswap-interface) |
| wallet | [![Crowdin](https://badges.crowdin.net/uniswap-wallet/localized.svg)](https://crowdin.com/project/uniswap-wallet) |
2. Releases are automatically tagged — view them in the [Releases tab](https://github.com/Uniswap/interface/releases).
## 🗂 Directory Structure
......
IPFS hash of the deployment:
- CIDv0: `QmV6wxRgkV16x6Gemmdg8UW9HD5zDgjLef844S1wk32H1a`
- CIDv1: `bafybeidep4w2hwdtu4uhxrg6xuk7rdlfjf7j7zq53mo2whxi6k3jeuvfbe`
- CIDv0: `QmY6iPUuY57eq3gtoFaqtJ885ysk1SQDbHi4nawcXmYM7s`
- CIDv1: `bafybeierals42wkadswmgvlto7rbuywzpzfcmgqcywdlzqbvgylukagtoy`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
......@@ -10,14 +10,72 @@ You can also access the Uniswap Interface from an IPFS gateway.
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://bafybeidep4w2hwdtu4uhxrg6xuk7rdlfjf7j7zq53mo2whxi6k3jeuvfbe.ipfs.dweb.link/
- [ipfs://QmV6wxRgkV16x6Gemmdg8UW9HD5zDgjLef844S1wk32H1a/](ipfs://QmV6wxRgkV16x6Gemmdg8UW9HD5zDgjLef844S1wk32H1a/)
- https://bafybeierals42wkadswmgvlto7rbuywzpzfcmgqcywdlzqbvgylukagtoy.ipfs.dweb.link/
- [ipfs://QmY6iPUuY57eq3gtoFaqtJ885ysk1SQDbHi4nawcXmYM7s/](ipfs://QmY6iPUuY57eq3gtoFaqtJ885ysk1SQDbHi4nawcXmYM7s/)
### 5.81.1 (2025-04-29)
## 5.82.0 (2025-04-30)
### Features
* **web:** add more CF cacheing for CSS resources (#18689) 3ddb1e3
* **web:** add more CF caching headers for public assets (#18678) 4f9b98e
* **web:** add more menu to hover state search modal (#18469) d4a85b5
* **web:** add pools mock sections (#17257) 3a6bc2c
* **web:** add tabs to search modal (#17180) 4e62ad9
* **web:** Add verify to add/delete passkey (#18627) b872b40
* **web:** bidirectional table scroll buttons (#18876) c647127
* **web:** defer initializations of AssetActivityProvider and TokenBalancesProvider (#18057) b1e68cf
* **web:** feature-gate pool search and tabs on web (#18273) 78ea6ec
* **web:** implement keyboard focus hover state on OptionItem (#17179) ebfea5c
* **web:** lazy load top level modals (#18056) 6a38462
* **web:** minip updates (#18613) 248bdec
* **web:** more CF caching for js chunks (#18679) 89611fe
* **web:** pass uniquote enabled to trading api requests (#19018) 955bbac
* **web:** show help modal on passkey error (#18682) 15039cf
* **web:** top boosted pools (#18569) cbcb803
* **web:** useMutate for refreshing authenticators (#18628) 3a24751
### Bug Fixes
* **web:** add additional statsig api urls to our csp.json file (#19035) 209df5d
* **web:** [pdp] redirect if no pool found (#18412) 8e95934
* **web:** [tdp] redirect if no token found (#18408) 997977e
* **web:** [tdp] switch testnet/mainnet mode when wallet is disconnected (#18406) 571fced
* **web:** add additional statsig api urls to our csp.json file (#19036) d7a9320
* **web:** add more context to Datadog resource events (#18743) 00d6e11
* **web:** add web3modal to csp to fix wallet connect error (#18771) d57bd82
* **web:** bug bash polish (#18594) ba4f511
* **web:** clean up modal util hooks (#18535) db8ff2b
* **web:** dd- allow 100% sample rate on interface staging (#18267) abb113a
* **web:** DevFlagsBox behavior (#18749) 4a93d4e
* **web:** do not render top level modals when shouldOverridePageLayout=true (#18691) 7f63a56
* **web:** fix button size in FeeTierSearchModal (#18911) 5936720
* **web:** fix closeModal util hook (#18783) b76e2d8
* **web:** fix invalid robots.txt (#18690) f910fd3
* **web:** fix resetting of modal after closing (#18872) 317f04a
* **web:** fix uniwalletmodal opening bug (#18871) c4de0d2
* **web:** lp incentives bugfixes (#18830) 4e3b289
* **web:** migrate Card components to tamagui (#18481) 1f43821
* **web:** migrate containers in MigrateV2Pair to tamagui (#18482) 374dd77
* **web:** optimize images for app store logos and lazy-load QR code (#18635) cb119ef
* **web:** pass chainId to useReadContracts (#18663) c4aa5b5
* **web:** remove applied percent buffer logic from useMaxAmountSpend (#18834) f88ac25
* **web:** remove more dead feature flags (#18538) b9ef4a8
* **web:** replace dotted bg gradient png with css (#18803) bf8cfea
* **web:** search revamp web polish (#18416) 86f034e
* **web:** settings spacing fix (#18874) b36aa67
* **web:** show price for v2 create (#18917) e80273c
* **web:** slideOutMenu adjustments (#18629) 3715c68
* **web:** start migrating MigrateV2Pair to spore / tamagui (#18480) 5d8a2d0
* **web:** temp skip snapshot test on LimitPriceInputPanel.test.tsx (#18630) 435fe3b
* **web:** uninitialized v2 pools (#18807) 8f390da
* **web:** v2 migrate page UI fixes (#18479) 858807d
* **web:** v4 native pair liq chart fix (#18893) c008acc
### Continuous Integration
* **web:** update sitemaps a994dde
web/5.81.1
\ No newline at end of file
web/5.82.0
\ No newline at end of file
......@@ -89,7 +89,7 @@
"private": true,
"scripts": {
"build:production": "webpack --node-env=production --env BUILD_ENV=prod BUILD_NUM=${BUILD_NUM:-0}",
"check:circular": "concurrently \"../../scripts/check-circular-imports.sh ./src/entry/sidebar.tsx 1\" \"../../scripts/check-circular-imports.sh ./src/entry/onboarding.tsx 1\" \"../../scripts/check-circular-imports.sh ./src/entry/unitagClaim.tsx 1\"",
"check:circular": "concurrently \"../../scripts/check-circular-imports.sh ./src/entry/sidebar.tsx 0\" \"../../scripts/check-circular-imports.sh ./src/entry/onboarding.tsx 0\" \"../../scripts/check-circular-imports.sh ./src/entry/unitagClaim.tsx 0\"",
"check:deps:usage": "depcheck",
"env:local:download": "bash ../../scripts/downloadEnvLocal.sh m4dhqfltt3dokkqi3hqwigmf2a ../../.env",
"env:local:upload": "bash ../../scripts/uploadEnvLocal.sh m4dhqfltt3dokkqi3hqwigmf2a ../../.env",
......
......@@ -45,7 +45,6 @@ import { PrimaryAppInstanceDebuggerLazy } from 'src/store/PrimaryAppInstanceDebu
import { getReduxPersistor } from 'src/store/store'
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 { ExtensionOnboardingFlow } from 'uniswap/src/types/screens/extension'
const supportsSidePanel = checksIfSupportsSidePanel()
......@@ -214,10 +213,8 @@ export default function OnboardingApp(): JSX.Element {
return (
<PersistGate persistor={getReduxPersistor()}>
<BaseAppContainer appName={DatadogAppNameTag.Onboarding}>
<UnitagUpdaterContextProvider>
<PrimaryAppInstanceDebuggerLazy />
<RouterProvider router={router} />
</UnitagUpdaterContextProvider>
<PrimaryAppInstanceDebuggerLazy />
<RouterProvider router={router} />
</BaseAppContainer>
</PersistGate>
)
......
......@@ -11,7 +11,7 @@ import { BaseAppContainer } from 'src/app/core/BaseAppContainer'
import { DatadogAppNameTag } from 'src/app/datadog'
import { AccountSwitcherScreen } from 'src/app/features/accounts/AccountSwitcherScreen'
import { DappContextProvider } from 'src/app/features/dapp/DappContext'
import { addRequest } from 'src/app/features/dappRequests/saga'
import { addRequest } from 'src/app/features/dappRequests/actions'
import { ReceiveScreen } from 'src/app/features/receive/ReceiveScreen'
import { SendFlow } from 'src/app/features/send/SendFlow'
import { DevMenuScreen } from 'src/app/features/settings/DevMenuScreen'
......@@ -36,10 +36,10 @@ import {
import { BackgroundToSidePanelRequestType } from 'src/background/messagePassing/types/requests'
import { PrimaryAppInstanceDebuggerLazy } from 'src/store/PrimaryAppInstanceDebuggerLazy'
import { getReduxPersistor } from 'src/store/store'
import { useResetUnitagsQueries } from 'uniswap/src/data/apiClients/unitagsApi/useResetUnitagsQueries'
import { syncAppWithDeviceLanguage } from 'uniswap/src/features/settings/slice'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { UnitagUpdaterContextProvider, useUnitagUpdater } from 'uniswap/src/features/unitags/context'
import { isDevEnv } from 'utilities/src/environment/env'
import { logger } from 'utilities/src/logger/logger'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
......@@ -183,7 +183,7 @@ function SidebarWrapper(): JSX.Element {
useDappRequestPortListener()
useTestnetModeForLoggingAndAnalytics()
const { triggerRefetchUnitags } = useUnitagUpdater()
const resetUnitagsQueries = useResetUnitagsQueries()
useEffect(() => {
dispatch(syncAppWithDeviceLanguage())
......@@ -193,10 +193,10 @@ function SidebarWrapper(): JSX.Element {
return backgroundToSidePanelMessageChannel.addMessageListener(
BackgroundToSidePanelRequestType.RefreshUnitags,
() => {
triggerRefetchUnitags()
resetUnitagsQueries()
},
)
}, [triggerRefetchUnitags])
}, [resetUnitagsQueries])
return (
<>
......@@ -235,12 +235,10 @@ export default function SidebarApp(): JSX.Element {
return (
<PersistGate persistor={getReduxPersistor()}>
<BaseAppContainer appName={DatadogAppNameTag.Sidebar}>
<UnitagUpdaterContextProvider>
<DappContextProvider>
<PrimaryAppInstanceDebuggerLazy />
<RouterProvider router={router} />
</DappContextProvider>
</UnitagUpdaterContextProvider>
<DappContextProvider>
<PrimaryAppInstanceDebuggerLazy />
<RouterProvider router={router} />
</DappContextProvider>
</BaseAppContainer>
</PersistGate>
)
......
......@@ -22,7 +22,6 @@ import { UnitagClaimRoutes } from 'src/app/navigation/constants'
import { setRouter, setRouterState } from 'src/app/navigation/state'
import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { Flex } from 'ui/src'
import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context'
import { logger } from 'utilities/src/logger/logger'
import { usePrevious } from 'utilities/src/react/hooks'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics'
......@@ -139,9 +138,7 @@ export default function UnitagClaimApp(): JSX.Element {
return (
<BaseAppContainer appName={DatadogAppNameTag.UnitagClaim}>
<UnitagUpdaterContextProvider>
<RouterProvider router={router} />
</UnitagUpdaterContextProvider>
<RouterProvider router={router} />
</BaseAppContainer>
)
}
......@@ -35,6 +35,7 @@ import { MenuContentItem } from 'wallet/src/components/menu/types'
import { createOnboardingAccount } from 'wallet/src/features/onboarding/createOnboardingAccount'
import { useCanActiveAddressClaimUnitag } from 'wallet/src/features/unitags/hooks/useCanActiveAddressClaimUnitag'
import { BackupType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types'
import { hasBackup } from 'wallet/src/features/wallet/accounts/utils'
import { createAccountsActions } from 'wallet/src/features/wallet/create/createAccountsSaga'
import {
useActiveAccountAddressWithThrow,
......@@ -128,7 +129,7 @@ export function AccountSwitcherScreen(): JSX.Element {
wallet_type: ImportType.CreateAdditional,
accounts_imported_count: 1,
wallets_imported: [pendingWallet.address],
cloud_backup_used: pendingWallet.backups?.includes(BackupType.Cloud) ?? false,
cloud_backup_used: hasBackup(BackupType.Cloud, pendingWallet),
modal: ModalName.AccountSwitcher,
})
......
......@@ -4,9 +4,9 @@ import { Button, Flex, Text } from 'ui/src'
import { iconSizes } from 'ui/src/theme'
import { TextInput } from 'uniswap/src/components/input/TextInput'
import { Modal } from 'uniswap/src/components/modals/Modal'
import { AccountIcon } from 'uniswap/src/features/accounts/AccountIcon'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { shortenAddress } from 'utilities/src/addresses'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
import { SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types'
type CreateWalletModalProps = {
......
......@@ -8,11 +8,11 @@ import { Person } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
import { TextInput } from 'uniswap/src/components/input/TextInput'
import { Modal } from 'uniswap/src/components/modals/Modal'
import { AccountIcon } from 'uniswap/src/features/accounts/AccountIcon'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { OnboardingCardLoggingName } from 'uniswap/src/features/telemetry/types'
import { UNITAG_SUFFIX_NO_LEADING_DOT } from 'uniswap/src/features/unitags/constants'
import { shortenAddress } from 'utilities/src/addresses'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
import { CardType, IntroCard, IntroCardGraphicType } from 'wallet/src/components/introCards/IntroCard'
import { useCanActiveAddressClaimUnitag } from 'wallet/src/features/unitags/hooks/useCanActiveAddressClaimUnitag'
import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga'
......
import { PropsWithChildren, useCallback } from 'react'
import { PropsWithChildren } from 'react'
import { useTranslation } from 'react-i18next'
import { useDappLastChainId } from 'src/app/features/dapp/hooks'
import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext'
import { DappRequestStoreItem } from 'src/app/features/dappRequests/slice'
import { useIsDappRequestConfirming } from 'src/app/features/dappRequests/hooks'
import { DappRequestStoreItem } from 'src/app/features/dappRequests/shared'
import { Anchor, AnimatePresence, Button, Flex, Text, UniversalImage, UniversalImageResizeMode, styled } from 'ui/src'
import { borderRadii, iconSizes } from 'ui/src/theme'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
......@@ -15,6 +16,8 @@ import { TransactionTypeInfo } from 'uniswap/src/features/transactions/types/tra
import { extractNameFromUrl } from 'utilities/src/format/extractNameFromUrl'
import { formatDappURL } from 'utilities/src/format/urls'
import { logger } from 'utilities/src/logger/logger'
import { useEvent } from 'utilities/src/react/hooks'
import { useDebouncedCallback } from 'utilities/src/react/useDebouncedCallback'
import { DappIconPlaceholder } from 'wallet/src/components/WalletConnect/DappIconPlaceholder'
import { AddressFooter } from 'wallet/src/features/transactions/TransactionRequest/AddressFooter'
import { NetworkFeeFooter } from 'wallet/src/features/transactions/TransactionRequest/NetworkFeeFooter'
......@@ -184,6 +187,7 @@ function DappRequestFooter({
request.dappRequest.type === DappRequestType.SendTransaction ? request.dappRequest.transaction.chainId : undefined
const currentChainId = chainId || sendTransactionChainId || activeChain || defaultChainId
const { balance: nativeBalance } = useOnChainNativeCurrencyBalance(currentChainId, currentAccount.address)
const isRequestConfirming = useIsDappRequestConfirming(request.dappRequest.requestId)
const hasSufficientGas = hasSufficientFundsIncludingGas({
gasFee: transactionGasFeeResult?.value,
......@@ -198,7 +202,11 @@ function DappRequestFooter({
? transactionGasFeeResult?.value && hasSufficientGas
: true
const handleOnConfirm = useCallback(async () => {
const handleOnConfirm = useEvent(async () => {
if (isRequestConfirming) {
return
}
if (onConfirm) {
onConfirm()
} else {
......@@ -208,9 +216,12 @@ function DappRequestFooter({
if (maybeCloseOnConfirm && shouldCloseSidebar) {
setTimeout(window.close, WINDOW_CLOSE_DELAY)
}
}, [request, maybeCloseOnConfirm, onConfirm, defaultOnConfirm, shouldCloseSidebar])
})
const handleOnCancel = useCallback(async () => {
// This is strictly a UI debounce to prevent submitting the same confirmation multiple times.
const [debouncedHandleOnConfirm, isConfirming] = useDebouncedCallback(handleOnConfirm)
const handleOnCancel = useEvent(async () => {
if (onCancel) {
onCancel()
} else {
......@@ -220,7 +231,10 @@ function DappRequestFooter({
if (shouldCloseSidebar) {
setTimeout(window.close, WINDOW_CLOSE_DELAY)
}
}, [request, onCancel, defaultOnCancel, shouldCloseSidebar])
})
const isDisabled = !isConfirmEnabled || disableConfirm || isConfirming || isRequestConfirming
const isLoading = isRequestConfirming || isConfirming
return (
<>
......@@ -253,11 +267,12 @@ function DappRequestFooter({
{t('common.button.cancel')}
</Button>
<Button
isDisabled={!isConfirmEnabled || disableConfirm}
isDisabled={isDisabled}
loading={isLoading}
flexBasis={1}
size="medium"
variant="branded"
onPress={handleOnConfirm}
onPress={debouncedHandleOnConfirm}
>
{confirmText}
</Button>
......
......@@ -7,18 +7,17 @@ import {
DappRequestQueueProvider,
useDappRequestQueueContext,
} from 'src/app/features/dappRequests/DappRequestQueueContext'
import { rejectAllRequests } from 'src/app/features/dappRequests/actions'
import { ConnectionRequestContent } from 'src/app/features/dappRequests/requestContent/Connection/ConnectionRequestContent'
import { EthSendRequestContent } from 'src/app/features/dappRequests/requestContent/EthSend/EthSend'
import { PersonalSignRequestContent } from 'src/app/features/dappRequests/requestContent/PersonalSign/PersonalSignRequestContent'
import { SignTypedDataRequestContent } from 'src/app/features/dappRequests/requestContent/SignTypeData/SignTypedDataRequestContent'
import { rejectAllRequests } from 'src/app/features/dappRequests/saga'
import { isDappRequestStoreItemForEthSendTxn } from 'src/app/features/dappRequests/slice'
import { isDappRequestStoreItemForEthSendTxn, selectAllDappRequests } from 'src/app/features/dappRequests/slice'
import {
isConnectionRequest,
isSignMessageRequest,
isSignTypedDataRequest,
} from 'src/app/features/dappRequests/types/DappRequestTypes'
import { ExtensionState } from 'src/store/extensionReducer'
import { AnimatePresence, Flex, Text, TouchableArea, useSporeColors } from 'ui/src'
import { ReceiptText, RotatableChevron } from 'ui/src/components/icons'
import { iconSizes, zIndexes } from 'ui/src/theme'
......@@ -28,14 +27,14 @@ import { ModalName } from 'uniswap/src/features/telemetry/constants'
const REJECT_MESSAGE_HEIGHT = 48
export function DappRequestQueue(): JSX.Element {
const pendingDappRequests = useSelector((state: ExtensionState) => state.dappRequests.pending)
const areRequestsPending = pendingDappRequests.length > 0
const dappRequests = useSelector(selectAllDappRequests)
const requestsExist = dappRequests.length > 0
return (
<Modal
alignment="top"
backgroundColor="$transparent"
isModalOpen={areRequestsPending}
isModalOpen={requestsExist}
name={ModalName.DappRequest}
padding="$none"
zIndex={zIndexes.overlay}
......
import { providerErrors, serializeError } from '@metamask/rpc-errors'
import { PropsWithChildren, createContext, useContext, useEffect, useRef, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import {
confirmRequest,
confirmRequestNoDappInfo,
isDappRequestWithDappInfo,
rejectRequest,
} from 'src/app/features/dappRequests/saga'
import { DappRequestStoreItem } from 'src/app/features/dappRequests/slice'
import { ExtensionState } from 'src/store/extensionReducer'
import { confirmRequest, confirmRequestNoDappInfo, rejectRequest } from 'src/app/features/dappRequests/actions'
import { isDappRequestWithDappInfo } from 'src/app/features/dappRequests/saga'
import type { DappRequestStoreItem } from 'src/app/features/dappRequests/shared'
import { selectAllDappRequests } from 'src/app/features/dappRequests/slice'
import { DappResponseType } from 'uniswap/src/features/dappRequests/types'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { DappRequestAction } from 'uniswap/src/features/telemetry/types'
import { TransactionTypeInfo } from 'uniswap/src/features/transactions/types/transactionDetails'
import { extractBaseUrl } from 'utilities/src/format/urls'
import { useEvent } from 'utilities/src/react/hooks'
import { Account } from 'wallet/src/features/wallet/accounts/types'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
interface DappRequestQueueContextValue {
forwards: boolean // direction of sliding animation
increasing: boolean // direction of number increasing animation
......@@ -40,10 +36,10 @@ export function DappRequestQueueProvider({ children }: PropsWithChildren): JSX.E
const [currentIndex, setCurrentIndex] = useState(0)
// Show the top most pending request
const pendingRequests = useSelector((state: ExtensionState) => state.dappRequests.pending)
const dappRequests = useSelector(selectAllDappRequests)
const request = pendingRequests[currentIndex]
const totalRequestCount = pendingRequests.length
const request = dappRequests[currentIndex]
const totalRequestCount = dappRequests.length
const activeAccount = useActiveAccountWithThrow()
......@@ -77,37 +73,36 @@ export function DappRequestQueueProvider({ children }: PropsWithChildren): JSX.E
}
}
const onConfirm = async (
requestToConfirm: DappRequestStoreItem,
transactionTypeInfo?: TransactionTypeInfo,
): Promise<void> => {
const requestWithTxInfo = {
...requestToConfirm,
transactionTypeInfo,
}
if (requestToConfirm.dappInfo) {
const { activeConnectedAddress, lastChainId } = requestToConfirm.dappInfo
const connectedAddresses = requestToConfirm.dappInfo.connectedAccounts.map((account) => account.address)
sendAnalyticsEvent(ExtensionEventName.DappRequest, {
action: DappRequestAction.Accept,
requestType: requestToConfirm.dappRequest.type,
dappUrl: extractBaseUrl(requestToConfirm.senderTabInfo.url),
chainId: lastChainId,
activeConnectedAddress,
connectedAddresses,
})
}
if (isDappRequestWithDappInfo(requestWithTxInfo)) {
await dispatch(confirmRequest(requestWithTxInfo))
} else {
await dispatch(confirmRequestNoDappInfo(requestWithTxInfo))
}
setCurrentIndex((prev) => Math.max(0, prev - 1))
}
const onCancel = async (requestToCancel: DappRequestStoreItem): Promise<void> => {
const onConfirm = useEvent(
async (requestToConfirm: DappRequestStoreItem, transactionTypeInfo?: TransactionTypeInfo): Promise<void> => {
const requestWithTxInfo = {
...requestToConfirm,
transactionTypeInfo,
}
if (requestToConfirm.dappInfo) {
const { activeConnectedAddress, lastChainId } = requestToConfirm.dappInfo
const connectedAddresses = requestToConfirm.dappInfo.connectedAccounts.map((account) => account.address)
sendAnalyticsEvent(ExtensionEventName.DappRequest, {
action: DappRequestAction.Accept,
requestType: requestToConfirm.dappRequest.type,
dappUrl: extractBaseUrl(requestToConfirm.senderTabInfo.url),
chainId: lastChainId,
activeConnectedAddress,
connectedAddresses,
})
}
if (isDappRequestWithDappInfo(requestWithTxInfo)) {
await dispatch(confirmRequest(requestWithTxInfo))
} else {
await dispatch(confirmRequestNoDappInfo(requestWithTxInfo))
}
setCurrentIndex((prev) => Math.max(0, prev - 1))
},
)
const onCancel = useEvent(async (requestToCancel: DappRequestStoreItem): Promise<void> => {
if (requestToCancel.dappInfo) {
const { activeConnectedAddress, lastChainId } = requestToCancel.dappInfo
const connectedAddresses = requestToCancel.dappInfo.connectedAccounts.map((account) => account.address)
......@@ -132,7 +127,7 @@ export function DappRequestQueueProvider({ children }: PropsWithChildren): JSX.E
)
setCurrentIndex((prev) => Math.max(0, prev - 1))
}
})
const onPressNext = (): void => {
setForwards(true)
......
......@@ -4,7 +4,7 @@ import { providerErrors, serializeError } from '@metamask/rpc-errors'
import { saveDappConnection } from 'src/app/features/dapp/actions'
import { DappInfo, dappStore } from 'src/app/features/dapp/store'
import { getOrderedConnectedAddresses } from 'src/app/features/dapp/utils'
import { SenderTabInfo } from 'src/app/features/dappRequests/slice'
import type { SenderTabInfo } from 'src/app/features/dappRequests/shared'
import {
AccountResponse,
DappRequest,
......
import { createAction } from '@reduxjs/toolkit'
import type {
DappRequestNoDappInfo,
DappRequestRejectParams,
DappRequestWithDappInfo,
} from 'src/app/features/dappRequests/shared'
/** This is for requests where the dapp info is not passed along as part of the request because it
* does not exist yet (i.e. GetAccountRequest). In these cases the dappInfo will need to be saved.
*/
export const confirmRequestNoDappInfo = createAction<DappRequestNoDappInfo>('dappRequest/confirmSaveConnectionRequest')
export const confirmRequest = createAction<DappRequestWithDappInfo>(`dappRequest/confirmRequest`)
export const addRequest = createAction<DappRequestNoDappInfo>(`dappRequest/handleRequest`)
export const rejectRequest = createAction<DappRequestRejectParams>(`dappRequest/rejectRequest`)
export const rejectAllRequests = createAction('dappRequest/rejectAllRequests')
......@@ -2,6 +2,12 @@
import { providerErrors, serializeError } from '@metamask/rpc-errors'
import { PayloadAction } from '@reduxjs/toolkit'
import { getAccount, getAccountRequest } from 'src/app/features/dappRequests/accounts'
import {
confirmRequest,
confirmRequestNoDappInfo,
rejectAllRequests,
rejectRequest,
} from 'src/app/features/dappRequests/actions'
import { getChainId, getChainIdNoDappInfo } from 'src/app/features/dappRequests/getChainId'
import {
handleGetPermissionsRequest,
......@@ -9,22 +15,20 @@ import {
handleRevokePermissions,
} from 'src/app/features/dappRequests/permissions'
import {
DappRequestNoDappInfo,
DappRequestRejectParams,
DappRequestWithDappInfo,
changeChainSaga,
confirmRequest,
confirmRequestNoDappInfo,
handleGetCallsStatus,
handleSendCalls,
handleSendTransaction,
handleSignMessage,
handleSignTypedData,
handleUniswapOpenSidebarRequest,
rejectAllRequests,
rejectRequest,
} from 'src/app/features/dappRequests/saga'
import { dappRequestActions } from 'src/app/features/dappRequests/slice'
import type {
DappRequestNoDappInfo,
DappRequestRejectParams,
DappRequestWithDappInfo,
} from 'src/app/features/dappRequests/shared'
import { dappRequestActions, selectAllDappRequests } from 'src/app/features/dappRequests/slice'
import {
BaseSendTransactionRequest,
BaseSendTransactionRequestSchema,
......@@ -55,7 +59,6 @@ import {
UniswapOpenSidebarRequestSchema,
} from 'src/app/features/dappRequests/types/DappRequestTypes'
import { dappResponseMessageChannel } from 'src/background/messagePassing/messageChannels'
import { ExtensionState } from 'src/store/extensionReducer'
import { call, put, select, takeEvery } from 'typed-redux-saga'
import { DappRequestType, DappResponseType } from 'uniswap/src/features/dappRequests/types'
import { getEnabledChainIdsSaga } from 'uniswap/src/features/settings/saga'
......@@ -66,16 +69,16 @@ function* dappRequestApproval({
payload: request,
}: PayloadAction<DappRequestWithDappInfo | DappRequestNoDappInfo | DappRequestRejectParams>) {
if (type === rejectAllRequests.type) {
const pendingRequests = yield* select((state: ExtensionState) => state.dappRequests.pending)
const existingRequests = yield* select(selectAllDappRequests)
for (const pendingRequest of pendingRequests) {
for (const existingRequest of existingRequests) {
const errorResponse: ErrorResponse = {
type: DappResponseType.ErrorResponse,
error: serializeError(providerErrors.userRejectedRequest()),
requestId: pendingRequest.dappRequest.requestId,
requestId: existingRequest.dappRequest.requestId,
}
yield* call(dappResponseMessageChannel.sendMessageToTab, pendingRequest.senderTabInfo.id, errorResponse)
yield* call(dappResponseMessageChannel.sendMessageToTab, existingRequest.senderTabInfo.id, errorResponse)
}
yield* put(dappRequestActions.removeAll())
......
import { DappInfo } from 'src/app/features/dapp/store'
import { SenderTabInfo } from 'src/app/features/dappRequests/slice'
import type { SenderTabInfo } from 'src/app/features/dappRequests/shared'
import { ChainIdResponse, GetChainIdRequest } from 'src/app/features/dappRequests/types/DappRequestTypes'
import { dappResponseMessageChannel } from 'src/background/messagePassing/messageChannels'
import { call } from 'typed-redux-saga'
......
import { useIsDappRequestConfirming } from 'src/app/features/dappRequests/hooks'
import { DappRequestStatus } from 'src/app/features/dappRequests/shared'
import { ExtensionState } from 'src/store/extensionReducer'
import { renderHook, waitFor } from 'src/test/test-utils'
const MOCK_ID = 'mock-id'
describe('useIsDappRequestConfirming', () => {
it.each([
['returns false when request is not confirming', MOCK_ID, DappRequestStatus.Pending, false],
['returns true when request is confirming', MOCK_ID, DappRequestStatus.Confirming, true],
['returns false when request does not exist', 'non-existent-id', DappRequestStatus.Confirming, false],
])('%s', async (_, requestId, status, expected) => {
const preloadedState = {
dappRequests: {
requests: {
[MOCK_ID]: { status, createdAt: Date.now() },
},
},
} as unknown as Partial<ExtensionState>
const { result } = renderHook(() => useIsDappRequestConfirming(requestId), {
preloadedState,
})
await waitFor(() => expect(result.current).toBe(expected))
})
})
import { useMemo } from 'react'
import { useCallback, useMemo } from 'react'
import { useSelector } from 'react-redux'
import { DappRequestStoreItem } from 'src/app/features/dappRequests/slice'
import { DappRequestStoreItem } from 'src/app/features/dappRequests/shared'
import { selectIsRequestConfirming } from 'src/app/features/dappRequests/slice'
import {
isRequestAccountRequest,
isRequestPermissionsRequest,
} from 'src/app/features/dappRequests/types/DappRequestTypes'
import { ExtensionState } from 'src/store/extensionReducer'
import { getBridgingDappUrls } from 'uniswap/src/features/bridging/constants'
import { useBridgingSupportedChainIds, useNumBridgingChains } from 'uniswap/src/features/bridging/hooks/chains'
import { selectHasViewedDappRequestBridgingBanner } from 'wallet/src/features/behaviorHistory/selectors'
......@@ -42,3 +44,8 @@ export function useShouldShowBridgingRequestCard(
shouldShowBridgingRequestCard: isBridgingConnectionRequest && !hasViewedDappRequestBridgingBanner,
}
}
export function useIsDappRequestConfirming(requestId: string): boolean {
const selector = useCallback((state: ExtensionState) => selectIsRequestConfirming(state, requestId), [requestId])
return useSelector(selector)
}
......@@ -3,7 +3,7 @@ import { rpcErrors, serializeError } from '@metamask/rpc-errors'
import { removeDappConnection } from 'src/app/features/dapp/actions'
import { DappInfo } from 'src/app/features/dapp/store'
import { saveAccount } from 'src/app/features/dappRequests/accounts'
import { SenderTabInfo } from 'src/app/features/dappRequests/slice'
import type { SenderTabInfo } from 'src/app/features/dappRequests/shared'
import {
ErrorResponse,
GetPermissionsRequest,
......
......@@ -5,13 +5,13 @@ import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestCon
import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext'
import { isNonZeroBigNumber } from 'src/app/features/dappRequests/requestContent/EthSend/Swap/utils'
import { SendTransactionRequest } from 'src/app/features/dappRequests/types/DappRequestTypes'
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 { useCopyToClipboard } from 'wallet/src/components/copy/useCopyToClipboard'
import { ContentRow } from 'wallet/src/features/transactions/TransactionRequest/ContentRow'
import {
SpendingDetails,
......
......@@ -69,7 +69,7 @@ export function useSwapDetails(
if (v4Command) {
// Extract details using the V4 helper
const v4Details = getTokenDetailsFromV4SwapCommands(v4Command)
const v4Details = getTokenDetailsFromV4SwapCommands(v4Command, request.parsedCalldata.commands)
inputAddress = v4Details.inputAddress === ETH_ADDRESS ? DEFAULT_NATIVE_ADDRESS : v4Details.inputAddress
outputAddress = v4Details.outputAddress === ETH_ADDRESS ? DEFAULT_NATIVE_ADDRESS : v4Details.outputAddress
inputValue = v4Details.inputValue || '0'
......@@ -199,7 +199,10 @@ function getTokenAddressesFromV2V3SwapCommands(command: UniversalRouterCommand):
return { inputAddress, outputAddress }
}
function getTokenDetailsFromV4SwapCommands(command: UniversalRouterCommand): {
function getTokenDetailsFromV4SwapCommands(
command: UniversalRouterCommand,
allCommands?: UniversalRouterCommand[],
): {
inputAddress?: string
outputAddress?: string
inputValue?: string
......@@ -321,6 +324,21 @@ function getTokenDetailsFromV4SwapCommands(command: UniversalRouterCommand): {
}
}
// Handle edge case where amountOutMinimum is zero
if (allCommands && isZeroBigNumberParam(outputValue)) {
const sweepCommand = allCommands.find(isUrCommandSweep)
const unwrapWethCommand = allCommands.find(isUrCommandUnwrapWeth)
const sweepAmountOutParam = sweepCommand?.params.find(isAmountMinParam)
const unwrapWethAmountOutParam = unwrapWethCommand?.params.find(isAmountMinParam)
const fallbackOutputValue = sweepAmountOutParam?.value || unwrapWethAmountOutParam?.value
if (fallbackOutputValue) {
outputValue = fallbackOutputValue
}
}
return { inputAddress, outputAddress, inputValue, outputValue }
}
......
/* eslint-disable max-lines */
import { Provider, TransactionResponse } from '@ethersproject/providers'
import { providerErrors, rpcErrors, serializeError } from '@metamask/rpc-errors'
import { createAction } from '@reduxjs/toolkit'
import { createSearchParams } from 'react-router-dom'
import { changeChain } from 'src/app/features/dapp/changeChain'
import { DappInfo, dappStore } from 'src/app/features/dapp/store'
import { getActiveConnectedAccount } from 'src/app/features/dapp/utils'
import { DappRequestStoreItem, SenderTabInfo, dappRequestActions } from 'src/app/features/dappRequests/slice'
import {
addRequest,
confirmRequest,
confirmRequestNoDappInfo,
rejectRequest,
} from 'src/app/features/dappRequests/actions'
import type {
DappRequestNoDappInfo,
DappRequestRejectParams,
DappRequestWithDappInfo,
SenderTabInfo,
} from 'src/app/features/dappRequests/shared'
import { dappRequestActions, selectIsRequestConfirming } from 'src/app/features/dappRequests/slice'
import {
BaseSendTransactionRequest,
ChangeChainRequest,
......@@ -46,37 +57,20 @@ import {
} from 'uniswap/src/features/transactions/types/transactionDetails'
import { extractBaseUrl } from 'utilities/src/format/urls'
import { logger } from 'utilities/src/logger/logger'
import { SendTransactionParams, sendTransaction } from 'wallet/src/features/transactions/sendTransactionSaga'
import {
ExecuteTransactionParams,
executeTransaction,
} from 'wallet/src/features/transactions/executeTransaction/executeTransactionSaga'
import { getProvider, getSignerManager } from 'wallet/src/features/wallet/context'
import { selectActiveAccount } from 'wallet/src/features/wallet/selectors'
import { signMessage, signTypedDataMessage } from 'wallet/src/features/wallet/signing/signing'
export interface DappRequestRejectParams {
errorResponse: ErrorResponse
senderTabInfo: SenderTabInfo
}
type OptionalTransactionTypeInfo = {
transactionTypeInfo?: TransactionTypeInfo
}
export type DappRequestNoDappInfo = Omit<DappRequestStoreItem, 'dappInfo'> & OptionalTransactionTypeInfo
export type DappRequestWithDappInfo = Required<DappRequestStoreItem> & OptionalTransactionTypeInfo
export function isDappRequestWithDappInfo(
request: DappRequestNoDappInfo | DappRequestWithDappInfo,
): request is DappRequestWithDappInfo {
return 'dappInfo' in request && Boolean(request.dappInfo)
}
export const addRequest = createAction<DappRequestNoDappInfo>(`dappRequest/handleRequest`)
/** This is for requests where the dapp info is not passed along as part of the request because it
* does not exist yet (i.e. GetAccountRequest). In these cases the dappInfo will need to be saved.
*/
export const confirmRequestNoDappInfo = createAction<DappRequestNoDappInfo>('dappRequest/confirmSaveConnectionRequest')
export const confirmRequest = createAction<DappRequestWithDappInfo>(`dappRequest/confirmRequest`)
export const rejectRequest = createAction<DappRequestRejectParams>(`dappRequest/rejectRequest`)
export const rejectAllRequests = createAction('dappRequest/rejectAllRequests')
export function* dappRequestWatcher() {
while (true) {
const { payload, type } = yield* take(addRequest)
......@@ -101,7 +95,7 @@ function* handleRequest(requestParams: DappRequestNoDappInfo) {
if (requestParams.dappRequest.type === DappRequestType.UniswapOpenSidebar) {
// We can auto-confirm these requests since they are only for navigating to a certain tab
// At this point the sidebar is already open
yield* put(confirmRequestNoDappInfo(requestParams))
yield* call(handleConfirmRequestNoDappInfo, requestParams)
return
}
const activeAccount = yield* select(selectActiveAccount)
......@@ -148,9 +142,9 @@ function* handleRequest(requestParams: DappRequestNoDappInfo) {
const chainId = toSupportedChainId(hexadecimalStringToInt(requestParams.dappRequest.chainId))
if (chainId) {
if (dappInfo) {
yield* put(confirmRequest({ ...requestParams, dappInfo }))
yield* call(handleConfirmRequestWithDappInfo, { ...requestParams, dappInfo })
} else {
yield* put(confirmRequestNoDappInfo(requestParams))
yield* call(handleConfirmRequestNoDappInfo, requestParams)
}
if (isWalletUnlocked) {
yield* put(
......@@ -228,7 +222,7 @@ function* handleRequest(requestParams: DappRequestNoDappInfo) {
requestParams.dappRequest.type === DappRequestType.GetCallsStatus)
if (shouldAutoConfirmRequest) {
yield* put(confirmRequest({ ...requestParams, dappInfo }))
yield* call(handleConfirmRequestWithDappInfo, { ...requestParams, dappInfo })
} else {
yield* put(
dappRequestActions.add({
......@@ -257,7 +251,7 @@ export function* handleSendTransaction(
const provider = yield* call(getProvider, lastChainId)
const sendTransactionParams: SendTransactionParams = {
const sendTransactionParams: ExecuteTransactionParams = {
chainId: lastChainId,
account,
options: { request: transactionRequest },
......@@ -272,7 +266,7 @@ export function* handleSendTransaction(
transactionOriginType: TransactionOriginType.External,
}
const { transactionResponse } = yield* call(sendTransaction, sendTransactionParams)
const { transactionResponse } = yield* call(executeTransaction, sendTransactionParams)
// Trigger a pending transaction notification after we send the transaction to chain
yield* put(
......@@ -502,3 +496,22 @@ export function* handleGetCallsStatus(request: GetCallsStatusRequest, { id }: Se
yield* call(dappResponseMessageChannel.sendMessageToTab, id, response)
}
function* isRequestConfirming(requestId: string) {
const isConfirming = yield* select(selectIsRequestConfirming, requestId)
return isConfirming
}
function* handleConfirmRequestWithDappInfo(request: DappRequestWithDappInfo) {
if (yield* isRequestConfirming(request.dappRequest.requestId)) {
return
}
yield* put(confirmRequest(request))
}
function* handleConfirmRequestNoDappInfo(request: DappRequestNoDappInfo) {
if (yield* isRequestConfirming(request.dappRequest.requestId)) {
return
}
yield* put(confirmRequestNoDappInfo(request))
}
import type { DappInfo } from 'src/app/features/dapp/store'
import type { DappRequest, ErrorResponse } from 'src/app/features/dappRequests/types/DappRequestTypes'
import type { TransactionTypeInfo } from 'uniswap/src/features/transactions/types/transactionDetails'
export interface SenderTabInfo {
id: number
url: string
favIconUrl?: string
}
export enum DappRequestStatus {
Pending = 'pending',
Confirming = 'confirming',
}
export interface DappRequestStoreItem {
dappRequest: DappRequest
senderTabInfo: SenderTabInfo
dappInfo?: DappInfo
isSidebarClosed: boolean | undefined
}
type OptionalTransactionTypeInfo = {
transactionTypeInfo?: TransactionTypeInfo
}
export type DappRequestNoDappInfo = Omit<DappRequestStoreItem, 'dappInfo'> & OptionalTransactionTypeInfo
export type DappRequestWithDappInfo = Required<DappRequestStoreItem> & OptionalTransactionTypeInfo
export interface DappRequestRejectParams {
errorResponse: ErrorResponse
senderTabInfo: SenderTabInfo
}
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { DappInfo } from 'src/app/features/dapp/store'
import { confirmRequest, confirmRequestNoDappInfo } from 'src/app/features/dappRequests/actions'
import type { DappRequestStoreItem } from 'src/app/features/dappRequests/shared'
import { DappRequestStatus } from 'src/app/features/dappRequests/shared'
import {
DappRequest,
isConnectionRequest,
isSendTransactionRequest,
SendTransactionRequest,
type SendTransactionRequest,
} from 'src/app/features/dappRequests/types/DappRequestTypes'
export interface SenderTabInfo {
id: number
url: string
favIconUrl?: string
type RequestId = string
type WithMetadata<T> = T & {
createdAt: number
status: DappRequestStatus
}
interface DappRequestState {
pending: DappRequestStoreItem[]
export interface DappRequestState {
requests: Record<RequestId, WithMetadata<DappRequestStoreItem>>
}
const initialDappRequestState: DappRequestState = {
pending: [], // ordered array with the most recent request at the end
}
export interface DappRequestStoreItem {
dappRequest: DappRequest
senderTabInfo: SenderTabInfo
dappInfo?: DappInfo
isSidebarClosed: boolean | undefined
requests: {}, // record of requestId -> request
}
// Enforces that a request object in state is for an eth send txn request
export interface DappRequestStoreItemForEthSendTxn extends DappRequestStoreItem {
dappRequest: SendTransactionRequest
dappRequest: WithMetadata<SendTransactionRequest>
}
export function isDappRequestStoreItemForEthSendTxn(
......@@ -39,31 +32,64 @@ export function isDappRequestStoreItemForEthSendTxn(
return isSendTransactionRequest(request.dappRequest)
}
const selectDappRequestsArray = (state: DappRequestState) =>
// sort by createdAt ascending (oldest first) to preserve order of requests
Object.values(state.requests).sort((a, b) => a.createdAt - b.createdAt)
const slice = createSlice({
name: 'dappRequests',
initialState: initialDappRequestState,
reducers: {
add: (state, action: PayloadAction<DappRequestStoreItem>) => {
if (isConnectionRequest(action.payload.dappRequest)) {
// Remove existing connection requests from the same tab and of the same type
state.pending = state.pending.filter(
(request) =>
request.senderTabInfo.id !== action.payload.senderTabInfo.id ||
request.dappRequest.type !== action.payload.dappRequest.type,
)
const requests = selectDappRequestsArray(state)
for (const request of requests) {
// Remove existing connection requests from the same tab and of the same type
if (
request.senderTabInfo.id === action.payload.senderTabInfo.id &&
request.dappRequest.type === action.payload.dappRequest.type
) {
delete state.requests[request.dappRequest.requestId]
}
}
}
state.requests[action.payload.dappRequest.requestId] = {
...action.payload,
// set the status to pending state
status: DappRequestStatus.Pending,
// set the createdAt time so we can sort by time
createdAt: Date.now(),
}
state.pending.push(action.payload)
},
remove: (state, action: PayloadAction<string>) => {
const requestId = action.payload
const newState = state.pending.filter((tx) => tx.dappRequest.requestId !== requestId)
state.pending = newState
delete state.requests[requestId]
},
removeAll: (state) => {
state.pending = []
state.requests = {}
},
},
extraReducers: (builder) => {
// update status of request to confirming
// on confirmRequest and confirmRequestNoDappInfo
builder.addMatcher(
(action) => action.type === confirmRequest.type || action.type === confirmRequestNoDappInfo.type,
(state, action) => {
const { dappRequest } = action.payload
const request = state.requests[dappRequest.requestId]
if (request) {
request.status = DappRequestStatus.Confirming
}
},
)
},
})
export const selectAllDappRequests = (state: { dappRequests: DappRequestState }): DappRequestStoreItem[] =>
selectDappRequestsArray(state.dappRequests)
export const selectIsRequestConfirming = (state: { dappRequests: DappRequestState }, requestId: string): boolean =>
state.dappRequests.requests[requestId]?.status === DappRequestStatus.Confirming
export const dappRequestActions = slice.actions
export const dappRequestReducer = slice.reducer
......@@ -12,6 +12,7 @@ import { Circle, Flex, Popover, Text, TouchableArea, UniversalImage } from 'ui/s
import { animationPresets } from 'ui/src/animations'
import { CopyAlt, Globe, RotatableChevron, Settings } from 'ui/src/components/icons'
import { borderRadii, iconSizes } from 'ui/src/theme'
import { AccountIcon } from 'uniswap/src/features/accounts/AccountIcon'
import { useAvatar } from 'uniswap/src/features/address/avatar'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
......@@ -25,7 +26,6 @@ import { setClipboard } from 'uniswap/src/utils/clipboard'
import { shortenAddress } from 'utilities/src/addresses'
import { extractNameFromUrl } from 'utilities/src/format/extractNameFromUrl'
import { DappIconPlaceholder } from 'wallet/src/components/WalletConnect/DappIconPlaceholder'
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 { useDisplayName } from 'wallet/src/features/wallet/hooks'
......
import { Action, AuthenticationTypes } from '@uniswap/client-embeddedwallet/dist/uniswap/embeddedwallet/v1/service_pb'
import { useEffect, useRef } from 'react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Navigate, useLocation } from 'react-router-dom'
import { useOnboardingSteps } from 'src/app/features/onboarding/OnboardingStepsContext'
......@@ -10,7 +10,10 @@ import {
} from 'src/app/features/onboarding/import/types'
import { OnboardingRoutes, TopLevelRoutes } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state'
import { Flex, Text } from 'ui/src'
import { bringWindowToFront, closeWindow, openPopupWindow } from 'src/app/navigation/utils'
import { Button, Flex, IconButton, SpinningLoader, Text } from 'ui/src'
import { X } from 'ui/src/components/icons'
import { UniswapLogo } from 'ui/src/components/icons/UniswapLogo'
import { fetchChallengeRequest } from 'uniswap/src/data/rest/embeddedWallet/requests'
import { parseMessage } from 'uniswap/src/extension/messagePassing/platform'
import {
......@@ -24,6 +27,8 @@ import { useEmbeddedWalletBaseUrl } from 'uniswap/src/features/passkey/hooks/use
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ExtensionOnboardingFlow, ExtensionOnboardingScreens } from 'uniswap/src/types/screens/extension'
import { logger } from 'utilities/src/logger/logger'
import { useEvent } from 'utilities/src/react/hooks'
import { useInterval } from 'utilities/src/time/timing'
import { v4 as uuid } from 'uuid'
/**************************************************************************************************************
......@@ -70,6 +75,9 @@ import { v4 as uuid } from 'uuid'
*
**************************************************************************************************************/
const POPUP_WIDTH = 420
const POPUP_HEIGHT = 335
export function InitiatePasskeyAuth(): JSX.Element {
const locationState = useLocation().state as InitiatePasskeyAuthLocationState | undefined
......@@ -104,8 +112,9 @@ function InitiatePasskeyAuthContent(): JSX.Element {
})
}
const popupWindow = useRef<chrome.windows.Window | undefined>(undefined)
useEffect(() => {
let popupWindow: chrome.windows.Window | undefined
let handleMessagePasskeySignInFlowOpened: Parameters<typeof chrome.runtime.onMessageExternal.addListener>[0]
let handleMessagePasskeyCredentialRetrieved: Parameters<typeof chrome.runtime.onMessageExternal.addListener>[0]
......@@ -141,7 +150,7 @@ function InitiatePasskeyAuthContent(): JSX.Element {
return
}
closePopupWindow(popupWindow)
closeWindow(popupWindow.current)
importWithCredential(parsedMessage.credential)
goToNextStep()
}
......@@ -190,12 +199,10 @@ function InitiatePasskeyAuthContent(): JSX.Element {
chrome.runtime.onMessageExternal.addListener(handleMessagePasskeySignInFlowOpened)
// TODO(WALL-6374): center the popup window on the screen
popupWindow = await chrome.windows.create({
popupWindow.current = await openPopupWindow({
url: `${webAppBaseUrl}${EXTENSION_PASSKEY_AUTH_PATH}?request_id=${requestId}`,
type: 'popup',
width: 420,
height: 335,
width: POPUP_WIDTH,
height: POPUP_HEIGHT,
})
} catch (e) {
handleError(e, 'initiatePasskeyAuth')
......@@ -205,37 +212,85 @@ function InitiatePasskeyAuthContent(): JSX.Element {
initiatePasskeyAuth()
return () => {
closePopupWindow(popupWindow)
closeWindow(popupWindow.current)
chrome.runtime.onMessageExternal.removeListener(handleMessagePasskeySignInFlowOpened)
chrome.runtime.onMessageExternal.removeListener(handleMessagePasskeyCredentialRetrieved)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const [showBringWindowToFrontButton, setShowBringWindowToFrontButton] = useState(false)
// Checks if the popup window is still open.
// If it is not, then the user has closed the window and we simply navigate back to the select import method screen.
useInterval(async () => {
const windowId = popupWindow.current?.id ?? null
if (windowId === null) {
return
}
try {
// Will throw if window does not exist anymore.
await chrome.windows.get(windowId)
setShowBringWindowToFrontButton(true)
} catch (e) {
// Window does not exist anymore.
navigate(`/${TopLevelRoutes.Onboarding}/${OnboardingRoutes.SelectImportMethod}`, {
replace: true,
})
}
}, 1000)
const onBringWindowToFront = useEvent(async () => {
const windowId = popupWindow.current?.id ?? null
if (windowId === null) {
return
}
try {
await bringWindowToFront(windowId, { centered: true })
} catch (e) {
logger.error(e, {
tags: {
file: 'InitiatePasskeyAuth.tsx',
function: 'onBringWindowToFront',
},
})
}
})
return (
<Trace
logImpression
properties={{ flow: ExtensionOnboardingFlow.Import }}
screen={ExtensionOnboardingScreens.InitiatePasskeyAuth}
>
<Flex gap="$spacing16">
<Flex row position="absolute" top="$spacing24" right="$spacing24">
<IconButton
size="small"
emphasis="secondary"
icon={<X />}
onPress={() => navigate(`/${TopLevelRoutes.Onboarding}/${OnboardingRoutes.SelectImportMethod}`)}
/>
</Flex>
<Flex gap="$spacing32" centered>
<UniswapLogo size={80} />
<Text>{t('onboarding.importPasskey.continueInSecureWindow')}</Text>
<Flex row height={35} centered>
{showBringWindowToFrontButton ? (
<Button emphasis="secondary" size="small" onPress={onBringWindowToFront}>
{t('onboarding.importPasskey.bringWindowToFront')}
</Button>
) : (
<SpinningLoader />
)}
</Flex>
</Flex>
</Trace>
)
}
function closePopupWindow(popupWindow: chrome.windows.Window | undefined): void {
if (!popupWindow?.id) {
return
}
chrome.windows.remove(popupWindow.id).catch((error) => {
logger.error(error, {
tags: {
file: 'InitiatePasskeyAuth.tsx',
function: 'closePopupWindow',
},
})
})
}
......@@ -6,7 +6,7 @@ import { useExtensionNavigation } from 'src/app/navigation/utils'
import { Flex } from 'ui/src'
import { X } from 'ui/src/components/icons'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { TransactionModal } from 'uniswap/src/features/transactions/TransactionModal/TransactionModal'
import { TransactionModal } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModal'
import { SendContextProvider } from 'wallet/src/features/transactions/contexts/SendContext'
export function SendFlow(): JSX.Element {
......
......@@ -9,11 +9,11 @@ import { UniverseChainId } from 'uniswap/src/features/chains/types'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ModalName, SectionName, UniswapEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { InsufficientNativeTokenWarning } from 'uniswap/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning'
import { InsufficientNativeTokenWarning } from 'uniswap/src/features/transactions/components/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning'
import {
TransactionScreen,
useTransactionModalContext,
} from 'uniswap/src/features/transactions/TransactionModal/TransactionModalContext'
} from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModalContext'
import { useUSDCValue } from 'uniswap/src/features/transactions/hooks/useUSDCPrice'
import { useUSDTokenUpdater } from 'uniswap/src/features/transactions/hooks/useUSDTokenUpdater'
import { BlockedAddressWarning } from 'uniswap/src/features/transactions/modals/BlockedAddressWarning'
......
......@@ -32,6 +32,7 @@ import {
Lock,
RotatableChevron,
Settings,
Sliders,
Wrench,
} from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
......@@ -40,8 +41,11 @@ import { resetUniswapBehaviorHistory } from 'uniswap/src/features/behaviorHistor
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { FiatCurrency, ORDERED_CURRENCIES } from 'uniswap/src/features/fiatCurrency/constants'
import { getFiatCurrencyName, useAppFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { useCurrentLanguageInfo } from 'uniswap/src/features/language/hooks'
import { setCurrentFiatCurrency, setIsTestnetModeEnabled } from 'uniswap/src/features/settings/slice'
import { SmartWalletAdvancedSettingsModal } from 'uniswap/src/features/smartWallet/modals/SmartWalletAdvancedSettingsModal'
import { WalletEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { ConnectionCardLoggingName } from 'uniswap/src/features/telemetry/types'
......@@ -67,10 +71,12 @@ export function SettingsScreen(): JSX.Element {
const currentLanguageInfo = useCurrentLanguageInfo()
const appFiatCurrencyInfo = useAppFiatCurrencyInfo()
const hasViewedConnectionMigration = useSelector(selectHasViewedConnectionMigration)
const isSmartWalletEnabled = useFeatureFlag(FeatureFlags.SmartWallet)
const [isLanguageModalOpen, setIsLanguageModalOpen] = useState(false)
const [isPortfolioBalanceModalOpen, setIsPortfolioBalanceModalOpen] = useState(false)
const [isTestnetModalOpen, setIsTestnetModalOpen] = useState(false)
const [isAdvancedModalOpen, setIsAdvancedModalOpen] = useState(false)
const [isPermissionsModalOpen, setIsPermissionsModalOpen] = useState(false)
const [isDefaultProvider, setIsDefaultProvider] = useState(true)
......@@ -106,6 +112,8 @@ export function SettingsScreen(): JSX.Element {
}
const handleTestnetModalClose = useCallback(() => setIsTestnetModalOpen(false), [])
const handleAdvancedModalClose = useCallback(() => setIsAdvancedModalOpen(false), [])
useEffect(() => {
getIsDefaultProviderFromStorage()
.then((newIsDefaultProvider) => setIsDefaultProvider(newIsDefaultProvider))
......@@ -139,6 +147,12 @@ export function SettingsScreen(): JSX.Element {
/>
) : undefined}
<TestnetModeModal isOpen={isTestnetModalOpen} onClose={handleTestnetModalClose} />
<SmartWalletAdvancedSettingsModal
isTestnetEnabled={isTestnetModeEnabled}
onTestnetModeToggled={handleTestnetModeToggle}
isOpen={isAdvancedModalOpen}
onClose={handleAdvancedModalClose}
/>
<Flex fill backgroundColor="$surface1" gap="$spacing8">
<ScreenHeader title={t('settings.title')} />
<ScrollView showsVerticalScrollIndicator={false}>
......@@ -196,13 +210,20 @@ export function SettingsScreen(): JSX.Element {
title={t('settings.setting.smallBalances.title')}
onPress={(): void => setIsPortfolioBalanceModalOpen(true)}
/>
<SettingsToggleRow
Icon={Wrench}
checked={isTestnetModeEnabled}
title={t('settings.setting.wallet.testnetMode.title')}
onCheckedChange={handleTestnetModeToggle}
/>
{isSmartWalletEnabled ? (
<SettingsItem
Icon={Sliders}
title={t('settings.setting.advanced.title')}
onPress={(): void => setIsAdvancedModalOpen(true)}
/>
) : (
<SettingsToggleRow
Icon={Wrench}
checked={isTestnetModeEnabled}
title={t('settings.setting.wallet.testnetMode.title')}
onCheckedChange={handleTestnetModeToggle}
/>
)}
</SettingsSection>
{!hasViewedConnectionMigration && (
<Flex pt="$padding8">
......
import { PropsWithChildren, useCallback } from 'react'
import { createSearchParams, useNavigate } from 'react-router-dom'
import { navigateToInterfaceFiatOnRamp } from 'src/app/features/for/utils'
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'
......@@ -12,6 +11,7 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { ShareableEntity } from 'uniswap/src/types/sharing'
import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking'
import { logger } from 'utilities/src/logger/logger'
import { useCopyToClipboard } from 'wallet/src/components/copy/useCopyToClipboard'
import {
NavigateToFiatOnRampArgs,
NavigateToNftItemArgs,
......
......@@ -133,7 +133,7 @@ export function WebNavigation(): JSX.Element {
<NotificationToastWrapper />
{shouldRestoreScroll && <ScrollRestoration />}
{childrenMemo}
<ForceUpgradeModal />
{isLoggedIn && <ForceUpgradeModal />}
</WalletUniswapProvider>
</SideBarNavigationProvider>
)
......
......@@ -87,16 +87,14 @@ export async function focusOrCreateUnitagTab(address: Address, page: UnitagClaim
export async function focusOrCreateDappRequestWindow(tabId: number | undefined, windowId: number): Promise<void> {
const extension = await chrome.management.getSelf()
const window = await chrome.windows.getCurrent()
const tabs = await chrome.tabs.query({ url: `chrome-extension://${extension.id}/popup.html*` })
const tab = tabs[0]
// Centering within current window
const height = 410
const width = 330
const top = Math.round((window.top ?? 0) + ((window.height ?? height) - height) / 2)
const left = Math.round((window.left ?? 0) + ((window.width ?? width) - width) / 2)
const { left, top } = await calculatePopupWindowPosition({ width, height })
let url = `popup.html?windowId=${windowId}`
if (tabId) {
url += `&tabId=${tabId}`
......@@ -203,3 +201,77 @@ export async function closeCurrentTab(): Promise<void> {
})
}
}
/**
* Calculates the top and left position of a centered pop up window,
* making sure it's on the same monitor as the current window.
*/
async function calculatePopupWindowPosition({
width,
height,
}: {
width: number
height: number
}): Promise<{ left: number; top: number }> {
const currentWindow = await chrome.windows.getCurrent()
const currentWindowLeft = currentWindow.left ?? 0
const currentWindowTop = currentWindow.top ?? 0
const currentWindowWidth = currentWindow.width ?? width
const currentWindowHeight = currentWindow.height ?? height
return {
left: Math.round(currentWindowLeft + (currentWindowWidth - width) / 2),
top: Math.round(currentWindowTop + (currentWindowHeight - height) / 2),
}
}
/**
* Opens a popup window centered on the current window, making sure it's on the same monitor.
*/
export async function openPopupWindow({
url,
width,
height,
}: {
url: string
width: number
height: number
}): Promise<chrome.windows.Window> {
const { left, top } = await calculatePopupWindowPosition({ width, height })
const popupWindow = await chrome.windows.create({
url,
type: 'popup',
width,
height,
left,
top,
})
return popupWindow
}
export function closeWindow(window: chrome.windows.Window | undefined): void {
if (!window?.id) {
return
}
chrome.windows.remove(window.id).catch((error) => {
logger.error(error, {
tags: {
file: 'navigation/utils.ts',
function: 'closeWindow',
},
})
})
}
export async function bringWindowToFront(windowId: number, options?: { centered?: boolean }): Promise<void> {
if (options?.centered) {
const window = await chrome.windows.get(windowId)
const { left, top } = await calculatePopupWindowPosition({ width: window.width ?? 0, height: window.height ?? 0 })
await chrome.windows.update(windowId, { left, top })
}
await chrome.windows.update(windowId, { focused: true })
}
......@@ -2,7 +2,7 @@ import { rpcErrors, serializeError } from '@metamask/rpc-errors'
import { removeDappConnection } from 'src/app/features/dapp/actions'
import { changeChain } from 'src/app/features/dapp/changeChain'
import { dappStore } from 'src/app/features/dapp/store'
import { SenderTabInfo } from 'src/app/features/dappRequests/slice'
import type { SenderTabInfo } from 'src/app/features/dappRequests/shared'
import {
ChangeChainRequest,
DappRequest,
......
import { migratePendingDappRequestsToRecord } from 'src/store/extensionMigrations'
describe('migratePendingDappRequestsToRecord', () => {
it('empty pending → empty requests', () =>
expect(
migratePendingDappRequestsToRecord({
dappRequests: { pending: [] },
otherData: 'value',
}),
).toEqual({
dappRequests: { requests: {} },
otherData: 'value',
}))
it('sets sequential timestamps', () => {
const mockTime = 1000
jest.spyOn(Date, 'now').mockReturnValue(mockTime)
const result = migratePendingDappRequestsToRecord({
dappRequests: {
pending: [0, 1, 2].map((i) => ({ dappRequest: { requestId: `r${i}` } })),
},
})
expect(result.dappRequests.requests.r0.createdAt).toBe(mockTime)
expect(result.dappRequests.requests.r1.createdAt).toBe(mockTime + 1000)
expect(result.dappRequests.requests.r2.createdAt).toBe(mockTime + 2000)
jest.restoreAllMocks()
})
it('preserves data and handles missing IDs', () => {
const mockData = {
dappRequests: {
pending: [
{ dappRequest: { type: 'Missing' } }, // Missing ID
{ dappRequest: { requestId: 'id', data: { x: 1 } }, meta: 'kept' },
],
},
}
const result = migratePendingDappRequestsToRecord(mockData)
// Verify only valid request exists and contains original data
expect(Object.keys(result.dappRequests.requests)).toEqual(['id'])
expect(result.dappRequests.requests.id).toMatchObject({
dappRequest: { requestId: 'id', data: { x: 1 } },
meta: 'kept',
createdAt: expect.any(Number),
})
})
})
import { DappRequestStatus } from 'src/app/features/dappRequests/shared'
import type { DappRequestState } from 'src/app/features/dappRequests/slice'
export function removeDappInfoToChromeLocalStorage({ dapp: _dapp, ...state }: any): any {
return state
}
// migrates pending dapp requests array without status or timestamp to a record with status (pending|confirming) and timestamp
export function migratePendingDappRequestsToRecord(state: any): any {
// If there's no dappRequests state or it's already in the new format, return unchanged
if (!state.dappRequests || !state.dappRequests.pending || state.dappRequests.requests) {
return state
}
// Create new record object to hold requests
const requests: DappRequestState['requests'] = {}
// Convert each pending request to the record format with status
state.dappRequests.pending.forEach((item: unknown, index: number) => {
if (
item !== null &&
typeof item === 'object' &&
'dappRequest' in item &&
typeof item.dappRequest === 'object' &&
item.dappRequest !== null &&
'requestId' in item.dappRequest &&
typeof item.dappRequest.requestId === 'string'
) {
const updatedRequest = {
...item,
// Map to new structure with status and timestamp
status: DappRequestStatus.Pending,
createdAt: Date.now() + index * 1000, // Add timestamp for sorting
} as DappRequestState['requests'][string]
requests[item.dappRequest.requestId] = updatedRequest
}
})
// Return state with updated dappRequests slice
return {
...state,
dappRequests: {
requests,
},
}
}
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { migratePendingDappRequestsToRecord, removeDappInfoToChromeLocalStorage } from 'src/store/extensionMigrations'
import { unchecksumDismissedTokenWarningKeys } from 'uniswap/src/state/uniswapMigrations'
import {
activatePendingAccounts,
......@@ -29,9 +30,7 @@ export const migrations = {
1: removeUniconV2BehaviorState,
2: addRoutingFieldToTransactions,
3: activatePendingAccounts,
4: function removeDappInfoToChromeLocalStorage({ dapp: _dapp, ...state }: any) {
return state
},
4: removeDappInfoToChromeLocalStorage,
5: deleteBetaOnboardingState,
6: deleteExtensionOnboardingState,
7: deleteDefaultFavoritesFromFavoritesState,
......@@ -48,6 +47,7 @@ export const migrations = {
18: unchecksumDismissedTokenWarningKeys,
19: deleteWelcomeWalletCardBehaviorHistory,
20: moveTokenAndNFTVisibility,
21: migratePendingDappRequestsToRecord,
}
export const EXTENSION_STATE_VERSION = 20
export const EXTENSION_STATE_VERSION = 21
......@@ -205,31 +205,33 @@ const v17SchemaIntermediate = {
delete v17SchemaIntermediate.behaviorHistory.createdOnboardingRedesignAccount
export const v17Schema = v17SchemaIntermediate
const v18SchemaIntermediate = {
export const v18Schema = v17Schema
const v19SchemaIntermediate = {
...v17Schema,
behaviorHistory: {
...v17Schema.behaviorHistory,
hasViewedWelcomeWalletCard: undefined,
},
}
delete v18SchemaIntermediate.behaviorHistory.hasViewedWelcomeWalletCard
export const v18Schema = v18SchemaIntermediate
delete v19SchemaIntermediate.behaviorHistory.hasViewedWelcomeWalletCard
export const v19Schema = v19SchemaIntermediate
const v19SchemaIntermediate = {
...v18Schema,
const v20SchemaIntermediate = {
...v19Schema,
visibility: {
positions: {},
tokens: v18Schema.favorites.tokensVisibility,
nfts: v18Schema.favorites.nftsVisibility,
tokens: v19Schema.favorites.tokensVisibility,
nfts: v19Schema.favorites.nftsVisibility,
},
favorites: {
...v18Schema.favorites,
...v19Schema.favorites,
tokensVisibility: undefined,
nftsVisibility: undefined,
},
}
delete v19SchemaIntermediate.favorites.tokensVisibility
delete v19SchemaIntermediate.favorites.nftsVisibility
export const v19Schema = v19SchemaIntermediate
delete v20SchemaIntermediate.favorites.tokensVisibility
delete v20SchemaIntermediate.favorites.nftsVisibility
const v20Schema = v20SchemaIntermediate
export const getSchema = (): typeof v19Schema => v19Schema
export const getSchema = (): typeof v20Schema => v20Schema
......@@ -12,7 +12,6 @@ import React, { PropsWithChildren } from 'react'
import { ExtensionState, extensionReducer } from 'src/store/extensionReducer'
import { AppStore } from 'src/store/store'
import { Resolvers } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context'
import { AutoMockedApolloProvider } from 'uniswap/src/test/mocks'
import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider'
......@@ -50,9 +49,7 @@ export function renderWithProviders(
function Wrapper({ children }: PropsWithChildren<unknown>): JSX.Element {
return (
<AutoMockedApolloProvider resolvers={resolvers}>
<SharedWalletProvider reduxStore={store}>
<UnitagUpdaterContextProvider>{children}</UnitagUpdaterContextProvider>
</SharedWalletProvider>
<SharedWalletProvider reduxStore={store}>{children}</SharedWalletProvider>
</AutoMockedApolloProvider>
)
}
......
......@@ -72,7 +72,7 @@ class SeedPhraseInputView: UIView {
set { vc.rootView.viewModel.onPasteEnd = newValue }
get { return vc.rootView.viewModel.onPasteEnd }
}
@objc
var onSubmitError: RCTDirectEventBlock {
set { vc.rootView.viewModel.onSubmitError = newValue }
......@@ -138,6 +138,20 @@ struct SeedPhraseTextView: UIViewRepresentable {
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
}
func textViewDidBeginEditing(_ textView: UITextView) {
// Sync isFocused binding when user focuses the input
DispatchQueue.main.async {
self.parent.isFocused = true
}
}
func textViewDidEndEditing(_ textView: UITextView) {
// Sync isFocused binding when user blurs the input
DispatchQueue.main.async {
self.parent.isFocused = false
}
}
}
func makeCoordinator() -> Coordinator {
......@@ -160,7 +174,7 @@ struct SeedPhraseTextView: UIViewRepresentable {
uiView.text = text
if isFocused {
if !uiView.isFirstResponder {
if !uiView.isFirstResponder, uiView.window != nil {
uiView.becomeFirstResponder()
}
} else {
......
......@@ -70,7 +70,6 @@ import { syncAppWithDeviceLanguage } from 'uniswap/src/features/settings/slice'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { MobileEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context'
import i18n from 'uniswap/src/i18n'
import { CurrencyId } from 'uniswap/src/types/currency'
import { datadogEnabledBuild } from 'utilities/src/environment/constants'
......@@ -266,22 +265,20 @@ function AppOuter(): JSX.Element | null {
<LocalizationContextProvider>
<GestureHandlerRootView style={flexStyles.fill}>
<WalletContextProvider>
<UnitagUpdaterContextProvider>
<DataUpdaters />
<NavigationContainer>
<MobileWalletNavigationProvider>
<WalletUniswapProvider>
<BottomSheetModalProvider>
<AppModals />
<PerformanceProfiler onReportPrepared={onReportPrepared}>
<AppInner />
</PerformanceProfiler>
</BottomSheetModalProvider>
</WalletUniswapProvider>
<NotificationToastWrapper />
</MobileWalletNavigationProvider>
</NavigationContainer>
</UnitagUpdaterContextProvider>
<DataUpdaters />
<NavigationContainer>
<MobileWalletNavigationProvider>
<WalletUniswapProvider>
<BottomSheetModalProvider>
<AppModals />
<PerformanceProfiler onReportPrepared={onReportPrepared}>
<AppInner />
</PerformanceProfiler>
</BottomSheetModalProvider>
</WalletUniswapProvider>
<NotificationToastWrapper />
</MobileWalletNavigationProvider>
</NavigationContainer>
</WalletContextProvider>
</GestureHandlerRootView>
</LocalizationContextProvider>
......
......@@ -84,6 +84,7 @@ import {
v7Schema,
v80Schema,
v81Schema,
v82Schema,
v83Schema,
v84Schema,
v8Schema,
......@@ -1609,8 +1610,8 @@ describe('Redux state migrations', () => {
it('migrates from v82 to v83', () => {
// v82 didn't have a new schema
const v81Stub = { ...v81Schema }
const v83 = migrations[83](v81Stub)
const v82Stub = { ...v82Schema }
const v83 = migrations[83](v82Stub)
expect(v83.pushNotifications.generalUpdatesEnabled).toBe(false)
expect(v83.pushNotifications.priceAlertsEnabled).toBe(false)
......
......@@ -26,6 +26,7 @@ import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { PlusCircle } from 'wallet/src/components/icons/PlusCircle'
import { createOnboardingAccount } from 'wallet/src/features/onboarding/createOnboardingAccount'
import { BackupType } from 'wallet/src/features/wallet/accounts/types'
import { hasBackup } from 'wallet/src/features/wallet/accounts/utils'
import { createAccountsActions } from 'wallet/src/features/wallet/create/createAccountsSaga'
import { useActiveAccountAddress, useNativeAccountExists } from 'wallet/src/features/wallet/hooks'
import { selectAllAccountsSorted, selectSortedSignerMnemonicAccounts } from 'wallet/src/features/wallet/selectors'
......@@ -109,7 +110,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
wallet_type: ImportType.CreateAdditional,
accounts_imported_count: 1,
wallets_imported: [newAccount.address],
cloud_backup_used: newAccount.backups?.includes(BackupType.Cloud) ?? false,
cloud_backup_used: hasBackup(BackupType.Cloud, newAccount),
modal: ModalName.AccountSwitcher,
})
}
......
......@@ -36,8 +36,10 @@ import { SettingsBiometricModal } from 'src/components/Settings/SettingsBiometri
import { BuyNativeTokenModal } from 'src/components/TokenDetails/BuyNativeTokenModal'
import { FundWalletModal } from 'src/components/home/introCards/FundWalletModal'
import { HorizontalEdgeGestureTarget } from 'src/components/layout/screens/EdgeGestureTarget'
import { AdvancedSettingsModal } from 'src/components/modals/ReactNavigationModals/AdvancedSettingsModal'
import { HiddenTokenInfoModalScreen } from 'src/components/modals/ReactNavigationModals/HiddenTokenInfoModalScreen'
import { PasskeyHelpModalScreen } from 'src/components/modals/ReactNavigationModals/PasskeyHelpModalScreen'
import { PasskeyManagementModalScreen } from 'src/components/modals/ReactNavigationModals/PasskeyManagementModalScreen'
import { TestnetModeModalScreen } from 'src/components/modals/ReactNavigationModals/TestnetModeModalScreen'
import { UnitagsIntroModal } from 'src/components/unitags/UnitagsIntroModal'
import { ExchangeTransferModal } from 'src/features/fiatOnRamp/ExchangeTransferModal'
......@@ -383,6 +385,7 @@ export function AppStackNavigator(): JSX.Element {
<AppStack.Screen component={BuyNativeTokenModal} name={ModalName.BuyNativeToken} />
<AppStack.Screen component={HiddenTokenInfoModalScreen} name={ModalName.HiddenTokenInfoModal} />
<AppStack.Screen component={ScreenshotWarningModal} name={ModalName.ScreenshotWarning} />
<AppStack.Screen component={PasskeyManagementModalScreen} name={ModalName.PasskeyManagement} />
<AppStack.Screen component={PasskeyHelpModalScreen} name={ModalName.PasskeysHelp} />
<AppStack.Screen component={SettingsBiometricModal} name={ModalName.BiometricsModal} />
<AppStack.Screen component={SettingsFiatCurrencyModal} name={ModalName.FiatCurrencySelector} />
......@@ -390,6 +393,8 @@ export function AppStackNavigator(): JSX.Element {
<AppStack.Screen component={EditLabelSettingsModal} name={ModalName.EditLabelSettingsModal} />
<AppStack.Screen component={EditProfileSettingsModal} name={ModalName.EditProfileSettingsModal} />
<AppStack.Screen component={ConnectionsDappListModal} name={ModalName.ConnectionsDappListModal} />
<AppStack.Screen component={AdvancedSettingsModal} name={ModalName.SmartWalletAdvancedSettingsModal} />
{enabledInEnvOrDev &&
((): JSX.Element => {
return <AppStack.Screen component={ExperimentsModal} name={ModalName.Experiments} />
......
......@@ -17,6 +17,8 @@ import { TestnetSwitchModalState } from 'src/features/testnetMode/TestnetSwitchM
import { HomeScreenTabIndex } from 'src/screens/HomeScreen/HomeScreenTabIndex'
import { ReceiveCryptoModalState } from 'src/screens/ReceiveCryptoModalState'
import { FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types'
import { PasskeyManagementModalState } from 'uniswap/src/features/passkey/PasskeyManagementModal'
import { SmartWalletAdvancedSettingsModalState } from 'uniswap/src/features/smartWallet/modals/SmartWalletAdvancedSettingsModal'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { TestnetModeModalState } from 'uniswap/src/features/testnets/TestnetModeModal'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
......@@ -172,6 +174,7 @@ export type AppStackParamList = {
[ModalName.BuyNativeToken]: BuyNativeTokenModalState
[ModalName.HiddenTokenInfoModal]: undefined
[ModalName.ScreenshotWarning]: { acknowledgeText?: string } | undefined
[ModalName.PasskeyManagement]: PasskeyManagementModalState
[ModalName.PasskeysHelp]: undefined
[ModalName.BiometricsModal]: undefined
[ModalName.FiatCurrencySelector]: undefined
......@@ -179,6 +182,7 @@ export type AppStackParamList = {
[ModalName.EditLabelSettingsModal]: EditWalletSettingsModalState
[ModalName.EditProfileSettingsModal]: EditWalletSettingsModalState
[ModalName.ConnectionsDappListModal]: ConnectionsDappsListModalState
[ModalName.SmartWalletAdvancedSettingsModal]: SmartWalletAdvancedSettingsModalState
}
export type AppStackNavigationProp = NativeStackNavigationProp<AppStackParamList>
......
......@@ -637,7 +637,8 @@ const v81SchemaIntermediate = {
delete v81SchemaIntermediate.behaviorHistory.createdOnboardingRedesignAccount
export const v81Schema = v81SchemaIntermediate
// v82 had a migration but no schema update so skipping it here
export const v82Schema = v81Schema
export const v83Schema = {
...v81Schema,
pushNotifications: {
......
......@@ -9,7 +9,7 @@ import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { iconSizes } from 'ui/src/theme'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard/dismissNativeKeyboard'
import { RecipientList } from 'wallet/src/components/RecipientSearch/RecipientList'
import { RecipientSelectSpeedBumps } from 'wallet/src/components/RecipientSearch/RecipientSelectSpeedBumps'
import { useFilteredRecipientSections } from 'wallet/src/components/RecipientSearch/hooks'
......
......@@ -2,7 +2,7 @@ import React from 'react'
import { DappHeaderIcon } from 'src/components/Requests/DappHeaderIcon'
import { HeaderText } from 'src/components/Requests/RequestModal/HeaderText'
import { LinkButton } from 'src/components/buttons/LinkButton'
import { WalletConnectRequest } from 'src/features/walletConnect/walletConnectSlice'
import { WalletConnectSigningRequest } from 'src/features/walletConnect/walletConnectSlice'
import { Flex, useSporeColors } from 'ui/src'
import { iconSizes } from 'ui/src/theme'
import { useCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo'
......@@ -17,7 +17,7 @@ export function ClientDetails({
request,
permitInfo,
}: {
request: WalletConnectRequest
request: WalletConnectSigningRequest
permitInfo?: PermitInfo
}): JSX.Element {
const { dapp } = request
......
import { Currency } from '@uniswap/sdk-core'
import React from 'react'
import { Trans } from 'react-i18next'
import { WalletConnectRequest } from 'src/features/walletConnect/walletConnectSlice'
import { WalletConnectSigningRequest } from 'src/features/walletConnect/walletConnectSlice'
import { Text } from 'ui/src'
import { EthMethod } from 'uniswap/src/features/dappRequests/types'
import { ValueType, getCurrencyAmount } from 'uniswap/src/features/tokens/getCurrencyAmount'
......@@ -12,7 +12,7 @@ export function HeaderText({
permitAmount,
permitCurrency,
}: {
request: WalletConnectRequest
request: WalletConnectSigningRequest
permitAmount?: number
permitCurrency?: Currency | null
}): JSX.Element {
......
import { BigNumber } from '@ethersproject/bignumber'
import React, { PropsWithChildren } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleProp, ViewStyle } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler'
import { PermitInfo } from 'src/components/Requests/RequestModal/ClientDetails'
import { LinkButton } from 'src/components/buttons/LinkButton'
import { SignRequest, WalletConnectRequest, isTransactionRequest } from 'src/features/walletConnect/walletConnectSlice'
import {
SignRequest,
WalletConnectSigningRequest,
WalletSendCallsEncodedRequest,
isBatchedTransactionRequest,
isTransactionRequest,
} from 'src/features/walletConnect/walletConnectSlice'
import { Flex, Text, useSporeColors } from 'ui/src'
import { TextVariantTokens, iconSizes } from 'ui/src/theme'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
......@@ -16,6 +24,7 @@ import { getValidAddress } from 'uniswap/src/utils/addresses'
import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking'
import { shortenAddress } from 'utilities/src/addresses'
import { logger } from 'utilities/src/logger/logger'
import { ExpandoRow } from 'wallet/src/components/ExpandoRow/ExpandoRow'
import { ContentRow } from 'wallet/src/features/transactions/TransactionRequest/ContentRow'
import {
SpendingDetails,
......@@ -24,7 +33,9 @@ import {
import { useNoYoloParser } from 'wallet/src/utils/useNoYoloParser'
import { useTransactionCurrencies } from 'wallet/src/utils/useTransactionCurrencies'
const getStrMessage = (request: WalletConnectRequest): string => {
const MAX_MODAL_MESSAGE_HEIGHT = 200
const getStrMessage = (request: WalletConnectSigningRequest): string => {
if (request.type === EthMethod.PersonalSign || request.type === EthMethod.EthSign) {
return request.message || request.rawMessage
}
......@@ -159,10 +170,11 @@ function TransactionDetails({
}
type Props = {
request: WalletConnectRequest
request: WalletConnectSigningRequest
permitInfo?: PermitInfo
}
function isSignTypedDataRequest(request: WalletConnectRequest): request is SignRequest {
function isSignTypedDataRequest(request: WalletConnectSigningRequest): request is SignRequest {
return request.type === EthMethod.SignTypedData || request.type === EthMethod.SignTypedDataV4
}
......@@ -191,10 +203,50 @@ export function RequestDetailsContent({ request }: Props): JSX.Element {
)
}
export function RequestDetails({ request }: Props): JSX.Element {
function BatchRequestDetailsContent({ request: { calls } }: { request: WalletSendCallsEncodedRequest }): JSX.Element {
const { t } = useTranslation()
return (
<ScrollView>
<RequestDetailsContent request={request} />
</ScrollView>
<ExpandoRow
label={t('walletConnect.request.bundledTransactions.label', { count: calls.length })}
// TODO: Implement expanding logic
isExpanded={false}
onPress={() => {}}
/>
)
}
export function RequestDetails({ request, permitInfo }: Props): JSX.Element {
if (isBatchedTransactionRequest(request)) {
return <BatchRequestDetailsContent request={request} />
}
return (
<Flex backgroundColor="$surface2" borderColor="$surface3" borderRadius="$rounded16" borderWidth="$spacing1">
{!permitInfo && (
<SectionContainer style={requestMessageStyle}>
<ScrollView>
<RequestDetailsContent request={request} />
</ScrollView>
</SectionContainer>
)}
</Flex>
)
}
const requestMessageStyle: StyleProp<ViewStyle> = {
// need a fixed height here or else modal gets confused about total height
maxHeight: MAX_MODAL_MESSAGE_HEIGHT,
overflow: 'hidden',
}
export function SectionContainer({
children,
style,
}: PropsWithChildren<{ style?: StyleProp<ViewStyle> }>): JSX.Element | null {
return children ? (
<Flex p="$spacing16" style={style}>
{children}
</Flex>
) : null
}
......@@ -9,7 +9,7 @@ import { KidSuperCheckinModal } from 'src/components/Requests/RequestModal/KidSu
import { UwULinkErc20SendModal } from 'src/components/Requests/RequestModal/UwULinkErc20SendModal'
import {
WalletConnectRequestModalContent,
methodCostsGas,
getDoesMethodCostGas,
} from 'src/components/Requests/RequestModal/WalletConnectRequestModalContent'
import { useHasSufficientFunds } from 'src/components/Requests/RequestModal/hooks'
import { useBiometricAppSettings } from 'src/features/biometrics/useBiometricAppSettings'
......@@ -19,7 +19,8 @@ import { wcWeb3Wallet } from 'src/features/walletConnect/saga'
import { selectDidOpenFromDeepLink } from 'src/features/walletConnect/selectors'
import { signWcRequestActions } from 'src/features/walletConnect/signWcRequestSaga'
import {
WalletConnectRequest,
WalletConnectSigningRequest,
isBatchedTransactionRequest,
isTransactionRequest,
setDidOpenFromDeepLink,
} from 'src/features/walletConnect/walletConnectSlice'
......@@ -36,7 +37,7 @@ import { useSignerAccounts } from 'wallet/src/features/wallet/hooks'
interface Props {
onClose: () => void
request: WalletConnectRequest
request: WalletConnectSigningRequest
}
const VALID_REQUEST_TYPES = [
......@@ -46,6 +47,7 @@ const VALID_REQUEST_TYPES = [
EthMethod.EthSign,
EthMethod.EthSendTransaction,
UwULinkMethod.Erc20Send,
EthMethod.SendCalls,
]
export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Element | null {
......@@ -55,11 +57,13 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
const chainId = request.chainId
const tx: providers.TransactionRequest | undefined = useMemo(() => {
if (!isTransactionRequest(request)) {
return undefined
if (isTransactionRequest(request)) {
return { ...request.transaction, chainId }
}
return { ...request.transaction, chainId }
if (isBatchedTransactionRequest(request)) {
return { ...request.encodedTransaction, chainId }
}
return undefined
}, [chainId, request])
const signerAccounts = useSignerAccounts()
......@@ -70,7 +74,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
account: request.account,
chainId,
gasFee,
value: isTransactionRequest(request) ? request.transaction.value : undefined,
value: tx?.value?.toString(),
})
const { isBlocked: isSenderBlocked, isBlockedLoading: isSenderBlockedLoading } = useIsBlockedActiveAddress()
......@@ -92,7 +96,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
return false
}
if (methodCostsGas(request)) {
if (getDoesMethodCostGas(request)) {
return !!(tx && hasSufficientFunds && gasFee.value && !gasFee.error && !gasFee.isLoading)
}
......@@ -147,7 +151,11 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
return
}
if (request.type === EthMethod.EthSendTransaction || request.type === UwULinkMethod.Erc20Send) {
if (
request.type === EthMethod.EthSendTransaction ||
request.type === UwULinkMethod.Erc20Send ||
request.type === EthMethod.SendCalls
) {
if (!tx) {
return
}
......@@ -160,7 +168,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
signWcRequestActions.trigger({
sessionId: request.sessionId,
requestInternalId: request.internalId,
method: EthMethod.EthSendTransaction,
method: request.type === EthMethod.SendCalls ? EthMethod.SendCalls : EthMethod.EthSendTransaction,
transaction: txnWithFormattedGasEstimates,
account: signerAccount,
dapp: request.dapp,
......@@ -168,8 +176,6 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
request,
}),
)
} else if (request.type === EthMethod.SendCalls) {
// TODO: Implement
} else {
dispatch(
signWcRequestActions.trigger({
......@@ -248,15 +254,12 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
)
}
if (request.type === EthMethod.SendCalls) {
// TODO: Implement
return null
}
return (
<ModalWithOverlay
confirmationButtonText={
isTransactionRequest(request) ? t('common.button.accept') : t('walletConnect.request.button.sign')
isTransactionRequest(request) || isBatchedTransactionRequest(request)
? t('common.button.accept')
: t('walletConnect.request.button.sign')
}
disableConfirm={!confirmEnabled}
name={ModalName.WCSignRequest}
......
import { useBottomSheetInternal } from '@gorhom/bottom-sheet'
import { useNetInfo } from '@react-native-community/netinfo'
import React, { PropsWithChildren } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleProp, ViewStyle } from 'react-native'
import Animated, { useAnimatedStyle } from 'react-native-reanimated'
import { ClientDetails, PermitInfo } from 'src/components/Requests/RequestModal/ClientDetails'
import { RequestDetails } from 'src/components/Requests/RequestModal/RequestDetails'
import { RequestDetails, SectionContainer } from 'src/components/Requests/RequestModal/RequestDetails'
import {
SignRequest,
TransactionRequest,
WalletConnectRequest,
WalletConnectSigningRequest,
WalletSendCallsEncodedRequest,
isTransactionRequest,
} from 'src/features/walletConnect/walletConnectSlice'
import { Flex, Text, useSporeColors } from 'ui/src'
......@@ -26,15 +25,13 @@ import { logger } from 'utilities/src/logger/logger'
import { AddressFooter } from 'wallet/src/features/transactions/TransactionRequest/AddressFooter'
import { NetworkFeeFooter } from 'wallet/src/features/transactions/TransactionRequest/NetworkFeeFooter'
const MAX_MODAL_MESSAGE_HEIGHT = 200
const isPotentiallyUnsafe = (request: WalletConnectSigningRequest): boolean => request.type !== EthMethod.PersonalSign
const isPotentiallyUnsafe = (request: WalletConnectRequest): boolean => request.type !== EthMethod.PersonalSign
export const methodCostsGas = (request: WalletConnectRequest): request is TransactionRequest =>
request.type === EthMethod.EthSendTransaction
export const getDoesMethodCostGas = (request: WalletConnectSigningRequest): boolean =>
request.type === EthMethod.EthSendTransaction || request.type === EthMethod.SendCalls
/** If the request is a permit then parse the relevant information otherwise return undefined. */
const getPermitInfo = (request: WalletConnectRequest): PermitInfo | undefined => {
const getPermitInfo = (request: WalletConnectSigningRequest): PermitInfo | undefined => {
if (request.type !== EthMethod.SignTypedDataV4) {
return undefined
}
......@@ -61,7 +58,7 @@ const getPermitInfo = (request: WalletConnectRequest): PermitInfo | undefined =>
type WalletConnectRequestModalContentProps = {
gasFee: GasFeeResult
hasSufficientFunds: boolean
request: SignRequest | TransactionRequest
request: SignRequest | TransactionRequest | WalletSendCallsEncodedRequest
isBlocked: boolean
}
......@@ -85,19 +82,13 @@ export function WalletConnectRequestModalContent({
height: animatedFooterHeight.value,
}))
const hasGasFee = methodCostsGas(request)
const hasGasFee = getDoesMethodCostGas(request)
return (
<>
<ClientDetails permitInfo={permitInfo} request={request} />
<Flex pt="$spacing8">
<Flex backgroundColor="$surface2" borderColor="$surface3" borderRadius="$rounded16" borderWidth="$spacing1">
{!permitInfo && (
<SectionContainer style={requestMessageStyle}>
<RequestDetails request={request} />
</SectionContainer>
)}
</Flex>
<RequestDetails request={request} permitInfo={permitInfo} />
<Flex gap="$spacing8" mb="$spacing12" pt="$spacing20" px="$spacing4">
<NetworkFeeFooter
chainId={chainId}
......@@ -153,23 +144,12 @@ export function WalletConnectRequestModalContent({
)
}
function SectionContainer({
children,
style,
}: PropsWithChildren<{ style?: StyleProp<ViewStyle> }>): JSX.Element | null {
return children ? (
<Flex p="$spacing16" style={style}>
{children}
</Flex>
) : null
}
function WarningSection({
request,
showUnsafeWarning,
isBlockedAddress,
}: {
request: WalletConnectRequest
request: WalletConnectSigningRequest
showUnsafeWarning: boolean
isBlockedAddress: boolean
}): JSX.Element | null {
......@@ -197,9 +177,3 @@ function WarningSection({
return null
}
const requestMessageStyle: StyleProp<ViewStyle> = {
// need a fixed height here or else modal gets confused about total height
maxHeight: MAX_MODAL_MESSAGE_HEIGHT,
overflow: 'hidden',
}
import { parseEther } from 'ethers/lib/utils'
import { WalletConnectRequest } from 'src/features/walletConnect/walletConnectSlice'
import { WalletConnectSigningRequest } from 'src/features/walletConnect/walletConnectSlice'
import { AssetType } from 'uniswap/src/entities/assets'
import { EthMethod } from 'uniswap/src/features/dappRequests/types'
import {
......@@ -114,7 +114,7 @@ export async function getFormattedUwuLinkTxnRequest({
allowList,
providerManager,
contractManager,
}: HandleUwuLinkRequestParams): Promise<{ request: WalletConnectRequest; account: string }> {
}: HandleUwuLinkRequestParams): Promise<{ request: WalletConnectSigningRequest; account: string }> {
const newRequest = {
sessionId: UWULINK_PREFIX, // session/internalId is WalletConnect specific, but not needed here
internalId: UWULINK_PREFIX,
......
......@@ -7,7 +7,7 @@ import { WalletConnectModal } from 'src/components/Requests/ScanSheet/WalletConn
import { closeModal } from 'src/features/modals/modalSlice'
import { useWalletConnect } from 'src/features/walletConnect/useWalletConnect'
import {
WalletConnectRequest,
WalletConnectSigningRequest,
removePendingSession,
removeRequest,
setDidOpenFromDeepLink,
......@@ -105,7 +105,7 @@ export function WalletConnectModals(): JSX.Element {
}
type RequestModalProps = {
currRequest: WalletConnectRequest
currRequest: WalletConnectSigningRequest
}
function RequestModal({ currRequest }: RequestModalProps): JSX.Element {
......
......@@ -15,7 +15,7 @@ import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { sanitizeAddressText } from 'uniswap/src/utils/addresses'
import { shortenAddress } from 'utilities/src/addresses'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard/dismissNativeKeyboard'
import { isIOS } from 'utilities/src/platform'
import { NICKNAME_MAX_LENGTH } from 'wallet/src/constants/accounts'
import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga'
......
......@@ -15,7 +15,7 @@ import { useBottomSheetSafeKeyboard } from 'uniswap/src/components/modals/useBot
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard/dismissNativeKeyboard'
import { isIOS } from 'utilities/src/platform'
import { ChangeUnitagModal } from 'wallet/src/features/unitags/ChangeUnitagModal'
import { DeleteUnitagModal } from 'wallet/src/features/unitags/DeleteUnitagModal'
......
......@@ -10,12 +10,15 @@ import {
SettingsStackNavigationProp,
SettingsStackParamList,
} from 'src/app/navigation/types'
import { ConnectionsDappsListModalState } from 'src/components/Settings/ConnectionsDappModal/ConnectionsDappsListModalState'
import { EditWalletSettingsModalState } from 'src/components/Settings/EditWalletModal/EditWalletSettingsModalState'
import { openModal } from 'src/features/modals/modalSlice'
import { useIsScreenNavigationReady } from 'src/utils/useIsScreenNavigationReady'
import { Flex, Skeleton, Switch, Text, TouchableArea, useSporeColors } from 'ui/src'
import { Arrow } from 'ui/src/components/arrow/Arrow'
import { RotatableChevron } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
import { SmartWalletAdvancedSettingsModalState } from 'uniswap/src/features/smartWallet/modals/SmartWalletAdvancedSettingsModal'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { openUri } from 'uniswap/src/utils/linking'
......@@ -45,12 +48,18 @@ type SettingsNavigationModal =
| typeof ModalName.EditProfileSettingsModal
| typeof ModalName.EditLabelSettingsModal
| typeof ModalName.ConnectionsDappListModal
| typeof ModalName.SmartWalletAdvancedSettingsModal
| typeof ModalName.PasskeyManagement
export interface SettingsSectionItem {
screen?: keyof SettingsStackParamList | typeof MobileScreens.OnboardingStack
modal?: SettingsModal
navigationModal?: SettingsNavigationModal
screenProps?: ValueOf<SettingsStackParamList> | NavigatorScreenParams<OnboardingStackParamList>
navigationProps?:
| ConnectionsDappsListModalState
| EditWalletSettingsModalState
| SmartWalletAdvancedSettingsModalState
externalLink?: string
action?: JSX.Element
disabled?: boolean
......@@ -78,6 +87,7 @@ export const SettingsRow = memo(
modal,
navigationModal,
screenProps,
navigationProps,
externalLink,
disabled,
action,
......@@ -107,11 +117,22 @@ export const SettingsRow = memo(
} else if (modal) {
dispatch(openModal({ name: modal }))
} else if (navigationModal) {
navigate(navigationModal)
navigate(navigationModal, navigationProps)
} else if (externalLink) {
await openUri(externalLink)
}
}, [checkIfCanProceed, onToggle, screen, navigation, screenProps, modal, navigationModal, dispatch, externalLink])
}, [
checkIfCanProceed,
onToggle,
screen,
navigation,
screenProps,
navigationProps,
modal,
navigationModal,
dispatch,
externalLink,
])
return (
<TouchableArea disabled={Boolean(action)} onPress={handleRow}>
......
......@@ -13,8 +13,8 @@ import { getChainInfo } from 'uniswap/src/features/chains/chainInfo'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { useCurrencyInfo, useNativeCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo'
import { BridgeTokenButton } from 'uniswap/src/features/transactions/InsufficientNativeTokenWarning/BridgeTokenButton'
import { BuyNativeTokenButton } from 'uniswap/src/features/transactions/InsufficientNativeTokenWarning/BuyNativeTokenButton'
import { BridgeTokenButton } from 'uniswap/src/features/transactions/components/InsufficientNativeTokenWarning/BridgeTokenButton'
import { BuyNativeTokenButton } from 'uniswap/src/features/transactions/components/InsufficientNativeTokenWarning/BuyNativeTokenButton'
import { currencyIdToAddress } from 'uniswap/src/utils/currencyId'
import { useActiveAccountAddress } from 'wallet/src/features/wallet/hooks'
......
......@@ -23,6 +23,7 @@ import { useGatingUserPropertyUsernames } from 'wallet/src/features/gating/userP
import { selectAllowAnalytics } from 'wallet/src/features/telemetry/selectors'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
import { BackupType } from 'wallet/src/features/wallet/accounts/types'
import { hasBackup } from 'wallet/src/features/wallet/accounts/utils'
import {
useActiveAccount,
useSignerAccounts,
......@@ -116,7 +117,7 @@ export function TraceUserProperties(): null {
}
setUserProperty(MobileUserPropertyName.ActiveWalletAddress, activeAccount.address)
setUserProperty(MobileUserPropertyName.ActiveWalletType, activeAccount.type)
setUserProperty(MobileUserPropertyName.IsCloudBackedUp, Boolean(activeAccount.backups?.includes(BackupType.Cloud)))
setUserProperty(MobileUserPropertyName.IsCloudBackedUp, hasBackup(BackupType.Cloud, activeAccount))
setUserProperty(MobileUserPropertyName.IsPushEnabled, Boolean(activeAccount.pushNotificationsEnabled))
setUserProperty(MobileUserPropertyName.IsHideSmallBalancesEnabled, hideSmallBalances)
......
......@@ -8,6 +8,7 @@ import { openModal } from 'src/features/modals/modalSlice'
import { removePendingSession } from 'src/features/walletConnect/walletConnectSlice'
import { Flex, Text, TouchableArea } from 'ui/src'
import { CopyAlt, ScanHome, SettingsHome } from 'ui/src/components/icons'
import { AccountIcon } from 'uniswap/src/features/accounts/AccountIcon'
import { AccountType } from 'uniswap/src/features/accounts/types'
import { useAvatar } from 'uniswap/src/features/address/avatar'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
......@@ -22,7 +23,6 @@ import { setClipboard } from 'uniswap/src/utils/clipboard'
import { shortenAddress } from 'utilities/src/addresses'
import { isDevEnv } from 'utilities/src/environment/env'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
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 { useActiveAccount, useActiveAccountAddress, useDisplayName } from 'wallet/src/features/wallet/hooks'
......
......@@ -8,10 +8,10 @@ import RemoveButton from 'src/components/explore/RemoveButton'
import { disableOnPress } from 'src/utils/disableOnPress'
import { Flex, TouchableArea, useIsDarkMode, useShadowPropsShort, useSporeColors } from 'ui/src'
import { borderRadii, iconSizes, opacify } from 'ui/src/theme'
import { AccountIcon } from 'uniswap/src/features/accounts/AccountIcon'
import { useAvatar } from 'uniswap/src/features/address/avatar'
import { removeWatchedAddress } from 'uniswap/src/features/favorites/slice'
import { isIOS } from 'utilities/src/platform'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText'
import { useDisplayName } from 'wallet/src/features/wallet/hooks'
import { DisplayNameType } from 'wallet/src/features/wallet/types'
......
......@@ -18,7 +18,7 @@ import { UniverseChainId } from 'uniswap/src/features/chains/types'
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 { dismissNativeKeyboard } from 'utilities/src/device/keyboard'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard/dismissNativeKeyboard'
const TrendUpIcon = <TrendUp color="$neutral2" size="$icon.24" />
......
......@@ -6,7 +6,7 @@ import { SEARCH_ITEM_PX, SEARCH_ITEM_PY } from 'src/components/explore/search/co
import { SearchTokenItem } from 'src/components/explore/search/items/SearchTokenItem'
import { getSearchResultId } from 'src/components/explore/search/utils'
import { Flex, Loader } from 'ui/src'
import { MAX_DEFAULT_POPULAR_TOKEN_RESULTS_AMOUNT } from 'uniswap/src/components/TokenSelector/constants'
import { MAX_DEFAULT_TRENDING_TOKEN_RESULTS_AMOUNT } from 'uniswap/src/components/TokenSelector/constants'
import { ProtectionResult } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { ALL_NETWORKS_ARG } from 'uniswap/src/data/rest/base'
import { useTokenRankingsQuery } from 'uniswap/src/data/rest/tokenRankings'
......@@ -52,7 +52,7 @@ export function SearchPopularTokens({ selectedChain }: { selectedChain: Universe
const popularTokens = data?.tokenRankings?.[RankingType.Popularity]?.tokens.slice(
0,
MAX_DEFAULT_POPULAR_TOKEN_RESULTS_AMOUNT,
MAX_DEFAULT_TRENDING_TOKEN_RESULTS_AMOUNT,
)
const formattedTokens = useMemo(
......
......@@ -3,13 +3,13 @@ import { useTranslation } from 'react-i18next'
import { SEARCH_ITEM_ICON_SIZE, SEARCH_ITEM_PX, SEARCH_ITEM_PY } from 'src/components/explore/search/constants'
import { SearchWalletItemBase } from 'src/components/explore/search/items/SearchWalletItemBase'
import { Flex, Text } from 'ui/src'
import { AccountIcon } from 'uniswap/src/features/accounts/AccountIcon'
import { useENSAvatar, useENSName } from 'uniswap/src/features/ens/api'
import { getCompletedENSName } from 'uniswap/src/features/ens/useENS'
import { SearchContext } from 'uniswap/src/features/search/SearchContext'
import { ENSAddressSearchResult } from 'uniswap/src/features/search/SearchResult'
import { sanitizeAddressText } from 'uniswap/src/utils/addresses'
import { shortenAddress } from 'utilities/src/addresses'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
type SearchENSAddressItemProps = {
searchResult: ENSAddressSearchResult
......
......@@ -2,12 +2,12 @@ import React from 'react'
import { SEARCH_ITEM_ICON_SIZE, SEARCH_ITEM_PX, SEARCH_ITEM_PY } from 'src/components/explore/search/constants'
import { SearchWalletItemBase } from 'src/components/explore/search/items/SearchWalletItemBase'
import { Flex, Text } from 'ui/src'
import { AccountIcon } from 'uniswap/src/features/accounts/AccountIcon'
import { useAvatar } from 'uniswap/src/features/address/avatar'
import { SearchContext } from 'uniswap/src/features/search/SearchContext'
import { UnitagSearchResult } from 'uniswap/src/features/search/SearchResult'
import { sanitizeAddressText } from 'uniswap/src/utils/addresses'
import { shortenAddress } from 'utilities/src/addresses'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText'
import { DisplayNameType } from 'wallet/src/features/wallet/types'
......
......@@ -2,12 +2,12 @@ import React from 'react'
import { SEARCH_ITEM_ICON_SIZE, SEARCH_ITEM_PX, SEARCH_ITEM_PY } from 'src/components/explore/search/constants'
import { SearchWalletItemBase } from 'src/components/explore/search/items/SearchWalletItemBase'
import { Flex, Text } from 'ui/src'
import { AccountIcon } from 'uniswap/src/features/accounts/AccountIcon'
import { useENSAvatar, useENSName } from 'uniswap/src/features/ens/api'
import { SearchContext } from 'uniswap/src/features/search/SearchContext'
import { WalletByAddressSearchResult } from 'uniswap/src/features/search/SearchResult'
import { sanitizeAddressText } from 'uniswap/src/utils/addresses'
import { shortenAddress } from 'utilities/src/addresses'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
type SearchWalletByAddressItemProps = {
searchResult: WalletByAddressSearchResult
......
......@@ -28,6 +28,7 @@ import { INTRO_CARD_MIN_HEIGHT, IntroCardStack } from 'wallet/src/components/int
import { useSharedIntroCards } from 'wallet/src/components/introCards/useSharedIntroCards'
import { selectHasViewedNotificationsCard } from 'wallet/src/features/behaviorHistory/selectors'
import { setHasViewedNotificationsCard } from 'wallet/src/features/behaviorHistory/slice'
import { hasExternalBackup } from 'wallet/src/features/wallet/accounts/utils'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
type OnboardingIntroCardStackProps = {
......@@ -43,7 +44,7 @@ export function OnboardingIntroCardStack({
const activeAccount = useActiveAccountWithThrow()
const address = activeAccount.address
const isSignerAccount = activeAccount.type === AccountType.SignerMnemonic
const hasBackups = activeAccount.backups && activeAccount.backups.length > 0
const externalBackups = hasExternalBackup(activeAccount)
const { notificationPermissionsEnabled } = useNotificationOSPermissionsEnabled()
const notificationOnboardingCardEnabled = useFeatureFlag(FeatureFlags.NotificationOnboardingCard)
......@@ -102,7 +103,7 @@ export function OnboardingIntroCardStack({
})
}
if (!hasBackups) {
if (!externalBackups) {
output.push({
loggingName: OnboardingCardLoggingName.RecoveryBackup,
graphic: {
......@@ -149,7 +150,7 @@ export function OnboardingIntroCardStack({
})
}
return output
}, [hasBackups, showEmptyWalletState, isSignerAccount, sharedCards, t, showEnableNotificationsCard, dispatch])
}, [externalBackups, showEmptyWalletState, isSignerAccount, sharedCards, t, showEnableNotificationsCard, dispatch])
const handleSwiped = useCallback(
(_card: IntroCardProps, index: number) => {
......
import { AppStackScreenProp } from 'src/app/navigation/types'
import { ReactNavigationModal } from 'src/components/modals/ReactNavigationModals/ReactNavigationModal'
import { SmartWalletAdvancedSettingsModal } from 'uniswap/src/features/smartWallet/modals/SmartWalletAdvancedSettingsModal'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
export const AdvancedSettingsModal = (
props: AppStackScreenProp<typeof ModalName.SmartWalletAdvancedSettingsModal>,
): JSX.Element => {
return <ReactNavigationModal {...props} modalComponent={SmartWalletAdvancedSettingsModal} />
}
import { AppStackScreenProp } from 'src/app/navigation/types'
import { ReactNavigationModal } from 'src/components/modals/ReactNavigationModals/ReactNavigationModal'
import { PasskeyManagementModal } from 'uniswap/src/features/passkey/PasskeyManagementModal'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
export const PasskeyManagementModalScreen = (
props: AppStackScreenProp<typeof ModalName.PasskeyManagement>,
): JSX.Element => {
return <ReactNavigationModal {...props} modalComponent={PasskeyManagementModal} />
}
......@@ -2,7 +2,9 @@ import { memo, type ComponentType } from 'react'
import type { AppStackParamList, AppStackScreenProp } from 'src/app/navigation/types'
import { useReactNavigationModal } from 'src/components/modals/useReactNavigationModal'
import type { GetProps } from 'ui/src'
import { PasskeyManagementModal } from 'uniswap/src/features/passkey/PasskeyManagementModal'
import { PasskeysHelpModal } from 'uniswap/src/features/passkey/PasskeysHelpModal'
import { SmartWalletAdvancedSettingsModal } from 'uniswap/src/features/smartWallet/modals/SmartWalletAdvancedSettingsModal'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { TestnetModeModal } from 'uniswap/src/features/testnets/TestnetModeModal'
import { HiddenTokenInfoModal } from 'uniswap/src/features/transactions/modals/HiddenTokenInfoModal'
......@@ -10,13 +12,19 @@ import { HiddenTokenInfoModal } from 'uniswap/src/features/transactions/modals/H
// Define names of shared modals we're explicitly supporting on mobile
type ValidModalNames = keyof Pick<
AppStackParamList,
typeof ModalName.TestnetMode | typeof ModalName.HiddenTokenInfoModal | typeof ModalName.PasskeysHelp
| typeof ModalName.TestnetMode
| typeof ModalName.HiddenTokenInfoModal
| typeof ModalName.PasskeyManagement
| typeof ModalName.PasskeysHelp
| typeof ModalName.SmartWalletAdvancedSettingsModal
>
type ModalNameWithComponentProps = {
[ModalName.TestnetMode]: GetProps<typeof TestnetModeModal>
[ModalName.HiddenTokenInfoModal]: GetProps<typeof HiddenTokenInfoModal>
[ModalName.PasskeyManagement]: GetProps<typeof PasskeyManagementModal>
[ModalName.PasskeysHelp]: GetProps<typeof PasskeysHelpModal>
[ModalName.SmartWalletAdvancedSettingsModal]: GetProps<typeof SmartWalletAdvancedSettingsModal>
}
type NavigationModalProps<ModalName extends ValidModalNames> = {
......
......@@ -9,7 +9,7 @@ import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { UNITAG_SUFFIX_NO_LEADING_DOT } from 'uniswap/src/features/unitags/constants'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { MobileScreens, UnitagScreens } from 'uniswap/src/types/screens/mobile'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard/dismissNativeKeyboard'
import { useUnitagClaimHandler } from 'wallet/src/features/unitags/useUnitagClaimHandler'
const IMAGE_ASPECT_RATIO = 0.42
......@@ -62,11 +62,11 @@ export function UnitagBanner({
})
const onPressClaimNow = (): void => {
dismissNativeKeyboard()
handleClaim()
if (onPressClaim) {
onPressClaim()
}
dismissNativeKeyboard()
handleClaim()
}
const baseButtonStyle: TouchableAreaProps = {
......
import { PropsWithChildren, createContext, useCallback, useContext, useMemo, useState } from 'react'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard/dismissNativeKeyboard'
import {
PasswordErrors,
PasswordStrength,
......
......@@ -18,6 +18,7 @@ import { promiseMinDelay } from 'utilities/src/time/timing'
import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext'
import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga'
import { BackupType } from 'wallet/src/features/wallet/accounts/types'
import { hasBackup } from 'wallet/src/features/wallet/accounts/utils'
import { useSignerAccount } from 'wallet/src/features/wallet/hooks'
type Props = {
......@@ -59,14 +60,14 @@ export function CloudBackupProcessingAnimation({
// Handle finished backing up to Cloud
useEffect(() => {
if (account?.backups?.includes(BackupType.Cloud)) {
if (hasBackup(BackupType.Cloud, account)) {
doneProcessing()
// Show success state for 1s before navigating
const timer = setTimeout(onBackupComplete, ONE_SECOND_MS)
return () => clearTimeout(timer)
}
return undefined
}, [account?.backups, onBackupComplete])
}, [account, onBackupComplete])
// Handle backup to Cloud when screen appears
const backup = useCallback(async () => {
......
......@@ -7,7 +7,7 @@ import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { FiatOffRampMetaData, OffRampTransferDetailsResponse } from 'uniswap/src/features/fiatOnRamp/types'
import { FiatOffRampEventName, ModalName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { TransactionScreen } from 'uniswap/src/features/transactions/TransactionModal/TransactionModalContext'
import { TransactionScreen } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModalContext'
import { forceFetchFiatOnRampTransactions } from 'uniswap/src/features/transactions/slice'
import { CurrencyField } from 'uniswap/src/types/currency'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
......
import { FiatOnRampModalState } from 'src/screens/FiatOnRampModalState'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { TransactionScreen } from 'uniswap/src/features/transactions/TransactionModal/TransactionModalContext'
import { TransactionScreen } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModalContext'
import { TransactionState } from 'uniswap/src/features/transactions/types/transactionState'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
......
......@@ -2,7 +2,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { ModalsState } from 'src/features/modals/ModalsState'
import { FiatOnRampModalState } from 'src/screens/FiatOnRampModalState'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { TransactionScreen } from 'uniswap/src/features/transactions/TransactionModal/TransactionModalContext'
import { TransactionScreen } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModalContext'
import { TransactionState } from 'uniswap/src/features/transactions/types/transactionState'
import { getKeys } from 'utilities/src/primitives/objects'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
......
......@@ -10,7 +10,7 @@ import { BackupType } from 'wallet/src/features/wallet/accounts/types'
const PREVIEW_BOX_HEIGHT = 122
type BackupSpeedBumpModalProps = {
backupType: BackupType
backupType: BackupType.Cloud | BackupType.Manual
onContinue: () => void
onClose: () => void
......
......@@ -12,11 +12,11 @@ import { SendReviewScreen } from 'src/features/send/SendReviewScreen'
import { useWalletRestore } from 'src/features/wallet/hooks'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ModalName, SectionName } from 'uniswap/src/features/telemetry/constants'
import { TransactionModal } from 'uniswap/src/features/transactions/TransactionModal/TransactionModal'
import { TransactionModal } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModal'
import {
TransactionScreen,
useTransactionModalContext,
} from 'uniswap/src/features/transactions/TransactionModal/TransactionModalContext'
} from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModalContext'
import { SendContextProvider, useSendContext } from 'wallet/src/features/transactions/contexts/SendContext'
export function SendFlow(): JSX.Element {
......
......@@ -8,7 +8,7 @@ import { selectHasDismissedLowNetworkTokenWarning } from 'uniswap/src/features/b
import { UniswapEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency'
import { useTransactionModalContext } from 'uniswap/src/features/transactions/TransactionModal/TransactionModalContext'
import { useTransactionModalContext } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModalContext'
import { useIsBlocked } from 'uniswap/src/features/trm/hooks'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { useSendContext } from 'wallet/src/features/transactions/contexts/SendContext'
......
......@@ -21,11 +21,11 @@ import { ModalName } from 'uniswap/src/features/telemetry/constants'
import {
TransactionModalFooterContainer,
TransactionModalInnerContainer,
} from 'uniswap/src/features/transactions/TransactionModal/TransactionModal'
} from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModal'
import {
TransactionScreen,
useTransactionModalContext,
} from 'uniswap/src/features/transactions/TransactionModal/TransactionModalContext'
} from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModalContext'
import { LowNativeBalanceModal } from 'uniswap/src/features/transactions/modals/LowNativeBalanceModal'
import { CurrencyField } from 'uniswap/src/types/currency'
import { createTransactionId } from 'uniswap/src/utils/createTransactionId'
......
......@@ -2,8 +2,8 @@ import React, { useCallback, useEffect, useState } from 'react'
import { RecipientSelect } from 'src/components/RecipientSelect/RecipientSelect'
import { SEND_CONTENT_RENDER_DELAY_MS } from 'src/features/send/constants'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { TransactionModalInnerContainer } from 'uniswap/src/features/transactions/TransactionModal/TransactionModal'
import { useTransactionModalContext } from 'uniswap/src/features/transactions/TransactionModal/TransactionModalContext'
import { TransactionModalInnerContainer } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModal'
import { useTransactionModalContext } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModalContext'
import { useSendContext } from 'wallet/src/features/transactions/contexts/SendContext'
// We add a short hardcoded delay to allow the sheet to animate quickly both on first render and when going back from Review -> Form.
......
......@@ -2,8 +2,8 @@ import React, { useEffect, useState } from 'react'
import { SEND_CONTENT_RENDER_DELAY_MS } from 'src/features/send/constants'
import { useHapticFeedback } from 'src/utils/haptics/useHapticFeedback'
import { Flex } from 'ui/src/components/layout/Flex'
import { TransactionModalInnerContainer } from 'uniswap/src/features/transactions/TransactionModal/TransactionModal'
import { useTransactionModalContext } from 'uniswap/src/features/transactions/TransactionModal/TransactionModalContext'
import { TransactionModalInnerContainer } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModal'
import { useTransactionModalContext } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModalContext'
import { SendReviewDetails } from 'wallet/src/features/transactions/send/SendReviewDetails'
// We add a short hardcoded delay to allow the sheet to animate quickly both on first render and when going back from Review -> Form.
......
......@@ -18,8 +18,8 @@ import {
DecimalPadCalculatedSpaceId,
DecimalPadInput,
DecimalPadInputRef,
} from 'uniswap/src/features/transactions/DecimalPadInput/DecimalPadInput'
import { InsufficientNativeTokenWarning } from 'uniswap/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning'
} from 'uniswap/src/features/transactions/components/DecimalPadInput/DecimalPadInput'
import { InsufficientNativeTokenWarning } from 'uniswap/src/features/transactions/components/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning'
import { useUSDCValue } from 'uniswap/src/features/transactions/hooks/useUSDCPrice'
import { useUSDTokenUpdater } from 'uniswap/src/features/transactions/hooks/useUSDTokenUpdater'
import { BlockedAddressWarning } from 'uniswap/src/features/transactions/modals/BlockedAddressWarning'
......@@ -27,7 +27,7 @@ import { SwapArrowButton } from 'uniswap/src/features/transactions/swap/form/bod
import { TransactionType } from 'uniswap/src/features/transactions/types/transactionDetails'
import { useIsBlocked } from 'uniswap/src/features/trm/hooks'
import { CurrencyField } from 'uniswap/src/types/currency'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard/dismissNativeKeyboard'
import { truncateToMaxDecimals } from 'utilities/src/format/truncateToMaxDecimals'
import { RecipientInputPanel } from 'wallet/src/components/input/RecipientInputPanel'
import { NFTTransfer } from 'wallet/src/components/nfts/NFTTransfer'
......
......@@ -12,7 +12,7 @@ import { Ellipsis } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
import { useBottomSheetSafeKeyboard } from 'uniswap/src/components/modals/useBottomSheetSafeKeyboard'
import { MobileScreens, UnitagScreens } from 'uniswap/src/types/screens/mobile'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard/dismissNativeKeyboard'
import { isIOS } from 'utilities/src/platform'
import { ChangeUnitagModal } from 'wallet/src/features/unitags/ChangeUnitagModal'
import { DeleteUnitagModal } from 'wallet/src/features/unitags/DeleteUnitagModal'
......
export const getMockedEncodedBatchedTransaction = (
account: string,
): { from: string; to: string; value: string; gasLimit: undefined; data: string } => ({
from: account,
// in delegation flow we send the transaction to the same address as the account
to: account,
// mocked value & data (transaction to swap $5 worth of ETH to UNI),
value: '0x8bad563d223e9',
gasLimit: undefined,
data: '0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000067e4119700000000000000000000000000000000000000000000000000000000000000040b000604000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000008bad563d223e9000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000008bad563d223e9000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb81f9840a85d5af5bf1d1762f925bdaddc4201f98400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000001f9840a85d5af5bf1d1762f925bdaddc4201f984000000000000000000000000000000fee13a103a10d593b9ae06b3e05f2e7e1c000000000000000000000000000000000000000000000000000000000000001900000000000000000000000000000000000000000000000000000000000000600000000000000000000000001f9840a85d5af5bf1d1762f925bdaddc4201f984000000000000000000000000b554ebd6674480962b46770607fc9886735d5d1e0000000000000000000000000000000000000000000000000970145625a061070c',
})
......@@ -10,6 +10,7 @@ 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 { getMockedEncodedBatchedTransaction } from 'src/features/walletConnect/mocks'
import {
getAccountAddressFromEIP155String,
getChainIdFromEIP155String,
......@@ -27,7 +28,7 @@ import {
removeSession,
setHasPendingSessionError,
} from 'src/features/walletConnect/walletConnectSlice'
import { call, fork, put, select, take } from 'typed-redux-saga'
import { call, delay, fork, put, select, take } from 'typed-redux-saga'
import { config } from 'uniswap/src/config'
import { ALL_CHAIN_IDS, UniverseChainId } from 'uniswap/src/features/chains/types'
import { getChainLabel } from 'uniswap/src/features/chains/utils'
......@@ -325,23 +326,16 @@ function* handleSessionRequest(sessionRequest: PendingRequestTypes.Struct) {
}
case EthMethod.SendCalls: {
// capabilities as part of sendCallsRequest is subject to change
const { capabilities } = parseSendCallsRequest(topic, id, chainId, dapp, requestParams, accountAddress)
const request = parseSendCallsRequest(topic, id, chainId, dapp, requestParams, accountAddress)
// Mock response data
const response = {
id,
capabilities,
yield* delay(300) // to emulate a network request
const requestWithEncodedTransaction = {
...request,
// TODO: replace this with a real call to Wallet API /encode endpoint
encodedTransaction: getMockedEncodedBatchedTransaction(request.account),
}
yield* call([wcWeb3Wallet, wcWeb3Wallet.respondSessionRequest], {
topic,
response: {
id,
jsonrpc: '2.0',
result: response,
},
})
yield* put(addRequest(requestWithEncodedTransaction))
break
}
case EthMethod.GetCallsStatus: {
......
......@@ -2,8 +2,8 @@ import { createSelector, Selector } from '@reduxjs/toolkit'
import { MobileState } from 'src/app/mobileReducer'
import {
WalletConnectPendingSession,
WalletConnectRequest,
WalletConnectSession,
WalletConnectSigningRequest,
} from 'src/features/walletConnect/walletConnectSlice'
export const makeSelectSessions = (): Selector<MobileState, WalletConnectSession[] | undefined, [Maybe<Address>]> =>
......@@ -24,7 +24,7 @@ export const makeSelectSessions = (): Selector<MobileState, WalletConnectSession
},
)
export const selectPendingRequests = (state: MobileState): WalletConnectRequest[] => {
export const selectPendingRequests = (state: MobileState): WalletConnectSigningRequest[] => {
return state.walletConnect.pendingRequests
}
......
import { providers } from 'ethers'
import { wcWeb3Wallet } from 'src/features/walletConnect/saga'
import { TransactionRequest, UwuLinkErc20Request } from 'src/features/walletConnect/walletConnectSlice'
import {
TransactionRequest,
UwuLinkErc20Request,
WalletSendCallsEncodedRequest,
} from 'src/features/walletConnect/walletConnectSlice'
import { call, put } from 'typed-redux-saga'
import { AssetType } from 'uniswap/src/entities/assets'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
......@@ -12,7 +16,11 @@ import { TransactionOriginType, TransactionType } from 'uniswap/src/features/tra
import { DappInfo, EthSignMethod, UwULinkMethod, WalletConnectEvent } from 'uniswap/src/types/walletConnect'
import { createSaga } from 'uniswap/src/utils/saga'
import { logger } from 'utilities/src/logger/logger'
import { SendTransactionParams, sendTransaction } from 'wallet/src/features/transactions/sendTransactionSaga'
import { SendCallsResult } from 'wallet/src/features/dappRequests/types'
import {
ExecuteTransactionParams,
executeTransaction,
} from 'wallet/src/features/transactions/executeTransaction/executeTransactionSaga'
import { Account } from 'wallet/src/features/wallet/accounts/types'
import { getSignerManager } from 'wallet/src/features/wallet/context'
import { signMessage, signTypedDataMessage } from 'wallet/src/features/wallet/signing/signing'
......@@ -32,10 +40,10 @@ type SignTransactionParams = {
requestInternalId: string
transaction: providers.TransactionRequest
account: Account
method: EthMethod.EthSendTransaction
method: EthMethod.EthSendTransaction | EthMethod.SendCalls
dapp: DappInfo
chainId: UniverseChainId
request: TransactionRequest | UwuLinkErc20Request
request: TransactionRequest | UwuLinkErc20Request | WalletSendCallsEncodedRequest
}
function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
......@@ -43,9 +51,9 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
const { defaultChainId } = yield* getEnabledChainIdsSaga()
try {
const signerManager = yield* call(getSignerManager)
let signature = ''
let result: string | SendCallsResult = ''
if (method === EthMethod.PersonalSign || method === EthMethod.EthSign) {
signature = yield* call(signMessage, params.message, account, signerManager)
result = yield* call(signMessage, params.message, account, signerManager)
// TODO: add `isCheckIn` type to uwulink request info so that this can be generalized
if (params.dapp.source === 'uwulink' && params.dapp.name === 'Uniswap Cafe') {
......@@ -57,9 +65,9 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
)
}
} else if (method === EthMethod.SignTypedData || method === EthMethod.SignTypedDataV4) {
signature = yield* call(signTypedDataMessage, params.message, account, signerManager)
result = yield* call(signTypedDataMessage, params.message, account, signerManager)
} else if (method === EthMethod.EthSendTransaction && params.request.type === UwULinkMethod.Erc20Send) {
const txParams: SendTransactionParams = {
const txParams: ExecuteTransactionParams = {
chainId: params.transaction.chainId || defaultChainId,
account,
options: {
......@@ -74,10 +82,10 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
},
transactionOriginType: TransactionOriginType.External,
}
const { transactionResponse } = yield* call(sendTransaction, txParams)
signature = transactionResponse.hash
const { transactionResponse } = yield* call(executeTransaction, txParams)
result = transactionResponse.hash
} else if (method === EthMethod.EthSendTransaction) {
const txParams: SendTransactionParams = {
const txParams: ExecuteTransactionParams = {
chainId: params.transaction.chainId || defaultChainId,
account,
options: {
......@@ -89,8 +97,35 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
},
transactionOriginType: TransactionOriginType.External,
}
const { transactionResponse } = yield* call(sendTransaction, txParams)
signature = transactionResponse.hash
const { transactionResponse } = yield* call(executeTransaction, txParams)
result = transactionResponse.hash
// Trigger a pending transaction notification after we send the transaction to chain
yield* put(
pushNotification({
type: AppNotificationType.TransactionPending,
chainId: txParams.chainId,
}),
)
} else if (method === EthMethod.SendCalls) {
const txParams: ExecuteTransactionParams = {
chainId: params.transaction.chainId || defaultChainId,
account,
options: {
request: params.transaction,
},
typeInfo: {
type: TransactionType.WCConfirm,
dapp: params.dapp,
},
transactionOriginType: TransactionOriginType.External,
}
// TODO: When delegation/batching SC is deployed - add the actual send transaction here, but for now we just mock the data
const { transactionResponse } = {
transactionResponse: { hash: '0xade180ed77cf8198273df1f7eae17c3c7e46de3c5d20dc384339c862efc02817' },
}
result = { id: transactionResponse.hash, capabilities: {} }
// Trigger a pending transaction notification after we send the transaction to chain
yield* put(
......@@ -107,7 +142,7 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
response: {
id: Number(requestInternalId),
jsonrpc: '2.0',
result: signature,
result,
},
})
} else if (params.dapp.source === 'uwulink' && params.dapp.webhook) {
......@@ -117,7 +152,7 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ method: params.method, response: signature, chainId }),
body: JSON.stringify({ method: params.method, response: result, chainId }),
// TODO: consider adding analytics to track UwuLink usage
}).catch((error) =>
logger.error(error, {
......
......@@ -11,15 +11,15 @@ import {
} from 'src/features/walletConnect/selectors'
import {
WalletConnectPendingSession,
WalletConnectRequest,
WalletConnectSession,
WalletConnectSigningRequest,
} from 'src/features/walletConnect/walletConnectSlice'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
interface WalletConnect {
sessions: WalletConnectSession[]
pendingRequests: WalletConnectRequest[]
pendingRequests: WalletConnectSigningRequest[]
modalState: AppModalState<ScannerModalState>
pendingSession: WalletConnectPendingSession | null
hasPendingSessionError: boolean
......
......@@ -56,6 +56,10 @@ export interface WalletSendCallsRequest extends BaseRequest {
version: string
}
export interface WalletSendCallsEncodedRequest extends WalletSendCallsRequest {
encodedTransaction: EthTransaction
}
export interface WalletGetCallsStatusRequest extends BaseRequest {
id: string
type: EthMethod.GetCallsStatus
......@@ -77,11 +81,19 @@ export interface UwuLinkErc20Request extends BaseRequest {
transaction: EthTransaction // the formatted transaction, prepared by the wallet
}
export type WalletConnectRequest = SignRequest | TransactionRequest | UwuLinkErc20Request | WalletSendCallsRequest
export type WalletConnectSigningRequest =
| SignRequest
| TransactionRequest
| UwuLinkErc20Request
| WalletSendCallsEncodedRequest
export const isTransactionRequest = (request: WalletConnectRequest): request is TransactionRequest =>
export const isTransactionRequest = (request: WalletConnectSigningRequest): request is TransactionRequest =>
request.type === EthMethod.EthSendTransaction || request.type === UwULinkMethod.Erc20Send
export const isBatchedTransactionRequest = (
request: WalletConnectSigningRequest,
): request is WalletSendCallsEncodedRequest => request.type === EthMethod.SendCalls
export interface WalletConnectState {
byAccount: {
[accountId: string]: {
......@@ -89,7 +101,7 @@ export interface WalletConnectState {
}
}
pendingSession: WalletConnectPendingSession | null
pendingRequests: WalletConnectRequest[]
pendingRequests: WalletConnectSigningRequest[]
didOpenFromDeepLink?: boolean
hasPendingSessionError?: boolean
}
......@@ -143,7 +155,7 @@ const slice = createSlice({
state.pendingSession = null
},
addRequest: (state, action: PayloadAction<WalletConnectRequest>) => {
addRequest: (state, action: PayloadAction<WalletConnectSigningRequest>) => {
state.pendingRequests.push(action.payload)
},
......
......@@ -19,11 +19,12 @@ import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { SearchModalNoQueryList } from 'uniswap/src/features/search/SearchModal/SearchModalNoQueryList'
import { SearchModalResultsList } from 'uniswap/src/features/search/SearchModal/SearchModalResultsList'
import { SearchTab } from 'uniswap/src/features/search/SearchModal/types'
import { CancelBehaviorType, SearchTextInput } from 'uniswap/src/features/search/SearchTextInput'
import { MobileEventName, SectionName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard/dismissNativeKeyboard'
import { useDebounce } from 'utilities/src/time/timing'
// From design to avoid layout thrash as icons show and hide
......@@ -49,6 +50,7 @@ export function ExploreScreen(): JSX.Element {
const onSearchChangeText = (newSearchFilter: string): void => {
setSearchQuery(newSearchFilter)
textInputRef.current?.setNativeProps({ text: newSearchFilter })
}
const onSearchFocus = (): void => {
......@@ -114,10 +116,11 @@ export function ExploreScreen(): JSX.Element {
debouncedSearchFilter={debouncedSearchQuery}
parsedChainFilter={selectedChain}
searchFilter={searchQuery ?? ''}
activeTab={SearchTab.All}
onSelect={() => {}}
/>
) : (
<SearchModalNoQueryList chainFilter={selectedChain} onSelect={() => {}} />
<SearchModalNoQueryList chainFilter={selectedChain} activeTab={SearchTab.All} onSelect={() => {}} />
)
) : debouncedSearchQuery.length === 0 ? (
// Mimic ScrollView behavior with FlatList
......
......@@ -56,7 +56,7 @@ import {
DecimalPadCalculatedSpaceId,
DecimalPadInput,
DecimalPadInputRef,
} from 'uniswap/src/features/transactions/DecimalPadInput/DecimalPadInput'
} from 'uniswap/src/features/transactions/components/DecimalPadInput/DecimalPadInput'
import { useUSDTokenUpdater } from 'uniswap/src/features/transactions/hooks/useUSDTokenUpdater'
import { CurrencyField } from 'uniswap/src/types/currency'
import { FiatOnRampScreens } from 'uniswap/src/types/screens/mobile'
......
import { useEffect } from 'react'
import { useCallback } from 'react'
import { useSelector } from 'react-redux'
import { usePortfolioBalances } from 'uniswap/src/features/dataApi/balances'
import { selectFavoriteTokens } from 'uniswap/src/features/favorites/selectors'
import { setAttributesToDatadog } from 'utilities/src/logger/datadog/Datadog'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { useTimeout } from 'utilities/src/time/timing'
import {
selectActiveAccount,
selectSignerMnemonicAccounts,
selectViewOnlyAccounts,
} from 'wallet/src/features/wallet/selectors'
/**
* Helper hook for the home screen to track any user specific attributes.
*/
export function useHomeScreenTracking(): void {
const favoriteCurrencyIds = useSelector(selectFavoriteTokens)
const viewOnlyAccounts = useSelector(selectViewOnlyAccounts)
const signerAccounts = useSelector(selectSignerMnemonicAccounts)
const activeAccount = useSelector(selectActiveAccount)
const { data: balanceData } = usePortfolioBalances({
address: activeAccount?.address,
fetchPolicy: 'cache-only',
})
const tokenCount = balanceData ? Object.keys(balanceData).length : 0
useEffect(() => {
const setAttributes = useCallback(async () => {
setAttributesToDatadog({
tokenCount,
favoriteTokensCount: favoriteCurrencyIds.length,
viewOnlyAccountsCount: viewOnlyAccounts.length,
signerAccountsCount: signerAccounts.length,
}).catch(() => undefined)
}, [favoriteCurrencyIds.length])
}, [favoriteCurrencyIds.length, viewOnlyAccounts.length, signerAccounts.length, tokenCount])
// We are using a timeout here because the datadog initialization takes longer
// than this hook running. We have considered using a context api or redux
// but landed on a timeout for simplicity.
useTimeout(async () => {
await setAttributes()
}, ONE_SECOND_MS * 8)
}
......@@ -4,9 +4,9 @@ import { ViewProps } from 'react-native'
import { RecoveryWalletInfo, useOnDeviceRecoveryData } from 'src/screens/Import/useOnDeviceRecoveryData'
import { Button, Flex, FlexProps, Loader, Text, TouchableArea } from 'ui/src'
import { fonts, iconSizes } from 'ui/src/theme'
import { AccountIcon } from 'uniswap/src/features/accounts/AccountIcon'
import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext'
import { NumberType } from 'utilities/src/format/types'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
const cardProps: FlexProps & ViewProps = {
......
......@@ -13,6 +13,7 @@ import { PasskeyImportLoading } from 'wallet/src/features/onboarding/PasskeyImpo
import { WelcomeSplash } from 'wallet/src/features/onboarding/WelcomeSplash'
import { fetchSeedPhrase } from 'wallet/src/features/passkeys/passkeys'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
import { BackupType } from 'wallet/src/features/wallet/accounts/types'
type Props = NativeStackScreenProps<OnboardingStackParamList, OnboardingScreens.PasskeyImport>
......@@ -33,7 +34,7 @@ export function PasskeyImportScreen({ navigation, route: { params } }: Props): J
const importAndGenerateAccount = async (): Promise<void> => {
const mnemonic = await fetchSeedPhrase(params.passkeyCredential)
const importedAddress = await Keyring.importMnemonic(mnemonic)
await generateImportedAccounts({ mnemonicId: importedAddress })
await generateImportedAccounts({ mnemonicId: importedAddress, backupType: BackupType.Passkey })
if (!importedAddress) {
throw new Error(`Failed to generate account for mnemonic ${mnemonic}`)
}
......
......@@ -24,7 +24,7 @@ import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { ImportType } from 'uniswap/src/types/onboarding'
import { OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard/dismissNativeKeyboard'
import { MINUTES_IN_HOUR, ONE_HOUR_MS, ONE_MINUTE_MS } from 'utilities/src/time/time'
import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext'
import { BackupType } from 'wallet/src/features/wallet/accounts/types'
......
......@@ -20,7 +20,7 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { areAddressesEqual, getValidAddress } from 'uniswap/src/utils/addresses'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard/dismissNativeKeyboard'
import { normalizeTextInput } from 'utilities/src/primitives/string'
import { createViewOnlyAccount } from 'wallet/src/features/onboarding/createViewOnlyAccount'
import { useIsSmartContractAddress } from 'wallet/src/features/transactions/send/hooks/useIsSmartContractAddress'
......
......@@ -12,10 +12,14 @@ import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobi
import { TamaguiProvider } from 'wallet/src/providers/tamagui-provider'
import { ACCOUNT, preloadedWalletPackageState } from 'wallet/src/test/fixtures'
jest.mock('wallet/src/features/wallet/accounts/utils', () => ({
hasExternalBackup: jest.fn(),
hasBackup: jest.fn(),
}))
jest.mock('wallet/src/features/onboarding/OnboardingContext', () => ({
useOnboardingContext: jest.fn().mockReturnValue({
getOnboardingOrImportedAccount: jest.fn().mockReturnValue({ address: 'mockedAccountAddress' }),
hasBackup: jest.fn(),
}),
useCreateImportedAccountsFromMnemonicIfNone: jest.fn(),
}))
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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