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 # 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 ## Interfaces
- Web: [app.uniswap.org](https://app.uniswap.org) - Web: [app.uniswap.org](https://app.uniswap.org)
- Wallet (mobile + extension): [wallet.uniswap.org](https://wallet.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 ## 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/) - Reddit: [/r/Uniswap](https://www.reddit.com/r/Uniswap/)
- Email: [contact@uniswap.org](mailto:contact@uniswap.org) - 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 ## Uniswap Links
...@@ -26,26 +46,14 @@ An open source repository for all Uniswap front end interfaces maintained by Uni ...@@ -26,26 +46,14 @@ An open source repository for all Uniswap front end interfaces maintained by Uni
- [V2](https://uniswap.org/whitepaper.pdf) - [V2](https://uniswap.org/whitepaper.pdf)
- [V1](https://hackmd.io/C-DvwDSfSxuh-Gd4WKE_ig) - [V1](https://hackmd.io/C-DvwDSfSxuh-Gd4WKE_ig)
## Apps ## Production & Release Process
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).
## 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 | 2. Releases are automatically tagged — view them in the [Releases tab](https://github.com/Uniswap/interface/releases).
| ------- | -------- |
| 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) |
## 🗂 Directory Structure ## 🗂 Directory Structure
......
IPFS hash of the deployment: IPFS hash of the deployment:
- CIDv0: `QmV6wxRgkV16x6Gemmdg8UW9HD5zDgjLef844S1wk32H1a` - CIDv0: `QmY6iPUuY57eq3gtoFaqtJ885ysk1SQDbHi4nawcXmYM7s`
- CIDv1: `bafybeidep4w2hwdtu4uhxrg6xuk7rdlfjf7j7zq53mo2whxi6k3jeuvfbe` - CIDv1: `bafybeierals42wkadswmgvlto7rbuywzpzfcmgqcywdlzqbvgylukagtoy`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org). 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. ...@@ -10,14 +10,72 @@ You can also access the Uniswap Interface from an IPFS gateway.
Your Uniswap settings are never remembered across different URLs. Your Uniswap settings are never remembered across different URLs.
IPFS gateways: IPFS gateways:
- https://bafybeidep4w2hwdtu4uhxrg6xuk7rdlfjf7j7zq53mo2whxi6k3jeuvfbe.ipfs.dweb.link/ - https://bafybeierals42wkadswmgvlto7rbuywzpzfcmgqcywdlzqbvgylukagtoy.ipfs.dweb.link/
- [ipfs://QmV6wxRgkV16x6Gemmdg8UW9HD5zDgjLef844S1wk32H1a/](ipfs://QmV6wxRgkV16x6Gemmdg8UW9HD5zDgjLef844S1wk32H1a/) - [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 ### 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 web/5.82.0
\ No newline at end of file \ No newline at end of file
...@@ -89,7 +89,7 @@ ...@@ -89,7 +89,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"build:production": "webpack --node-env=production --env BUILD_ENV=prod BUILD_NUM=${BUILD_NUM:-0}", "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", "check:deps:usage": "depcheck",
"env:local:download": "bash ../../scripts/downloadEnvLocal.sh m4dhqfltt3dokkqi3hqwigmf2a ../../.env", "env:local:download": "bash ../../scripts/downloadEnvLocal.sh m4dhqfltt3dokkqi3hqwigmf2a ../../.env",
"env:local:upload": "bash ../../scripts/uploadEnvLocal.sh m4dhqfltt3dokkqi3hqwigmf2a ../../.env", "env:local:upload": "bash ../../scripts/uploadEnvLocal.sh m4dhqfltt3dokkqi3hqwigmf2a ../../.env",
......
...@@ -45,7 +45,6 @@ import { PrimaryAppInstanceDebuggerLazy } from 'src/store/PrimaryAppInstanceDebu ...@@ -45,7 +45,6 @@ import { PrimaryAppInstanceDebuggerLazy } from 'src/store/PrimaryAppInstanceDebu
import { getReduxPersistor } from 'src/store/store' import { getReduxPersistor } from 'src/store/store'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context'
import { ExtensionOnboardingFlow } from 'uniswap/src/types/screens/extension' import { ExtensionOnboardingFlow } from 'uniswap/src/types/screens/extension'
const supportsSidePanel = checksIfSupportsSidePanel() const supportsSidePanel = checksIfSupportsSidePanel()
...@@ -214,10 +213,8 @@ export default function OnboardingApp(): JSX.Element { ...@@ -214,10 +213,8 @@ export default function OnboardingApp(): JSX.Element {
return ( return (
<PersistGate persistor={getReduxPersistor()}> <PersistGate persistor={getReduxPersistor()}>
<BaseAppContainer appName={DatadogAppNameTag.Onboarding}> <BaseAppContainer appName={DatadogAppNameTag.Onboarding}>
<UnitagUpdaterContextProvider> <PrimaryAppInstanceDebuggerLazy />
<PrimaryAppInstanceDebuggerLazy /> <RouterProvider router={router} />
<RouterProvider router={router} />
</UnitagUpdaterContextProvider>
</BaseAppContainer> </BaseAppContainer>
</PersistGate> </PersistGate>
) )
......
...@@ -11,7 +11,7 @@ import { BaseAppContainer } from 'src/app/core/BaseAppContainer' ...@@ -11,7 +11,7 @@ import { BaseAppContainer } from 'src/app/core/BaseAppContainer'
import { DatadogAppNameTag } from 'src/app/datadog' import { DatadogAppNameTag } from 'src/app/datadog'
import { AccountSwitcherScreen } from 'src/app/features/accounts/AccountSwitcherScreen' import { AccountSwitcherScreen } from 'src/app/features/accounts/AccountSwitcherScreen'
import { DappContextProvider } from 'src/app/features/dapp/DappContext' 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 { ReceiveScreen } from 'src/app/features/receive/ReceiveScreen'
import { SendFlow } from 'src/app/features/send/SendFlow' import { SendFlow } from 'src/app/features/send/SendFlow'
import { DevMenuScreen } from 'src/app/features/settings/DevMenuScreen' import { DevMenuScreen } from 'src/app/features/settings/DevMenuScreen'
...@@ -36,10 +36,10 @@ import { ...@@ -36,10 +36,10 @@ import {
import { BackgroundToSidePanelRequestType } from 'src/background/messagePassing/types/requests' import { BackgroundToSidePanelRequestType } from 'src/background/messagePassing/types/requests'
import { PrimaryAppInstanceDebuggerLazy } from 'src/store/PrimaryAppInstanceDebuggerLazy' import { PrimaryAppInstanceDebuggerLazy } from 'src/store/PrimaryAppInstanceDebuggerLazy'
import { getReduxPersistor } from 'src/store/store' import { getReduxPersistor } from 'src/store/store'
import { useResetUnitagsQueries } from 'uniswap/src/data/apiClients/unitagsApi/useResetUnitagsQueries'
import { syncAppWithDeviceLanguage } from 'uniswap/src/features/settings/slice' import { syncAppWithDeviceLanguage } from 'uniswap/src/features/settings/slice'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' 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 { isDevEnv } from 'utilities/src/environment/env'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { ONE_SECOND_MS } from 'utilities/src/time/time' import { ONE_SECOND_MS } from 'utilities/src/time/time'
...@@ -183,7 +183,7 @@ function SidebarWrapper(): JSX.Element { ...@@ -183,7 +183,7 @@ function SidebarWrapper(): JSX.Element {
useDappRequestPortListener() useDappRequestPortListener()
useTestnetModeForLoggingAndAnalytics() useTestnetModeForLoggingAndAnalytics()
const { triggerRefetchUnitags } = useUnitagUpdater() const resetUnitagsQueries = useResetUnitagsQueries()
useEffect(() => { useEffect(() => {
dispatch(syncAppWithDeviceLanguage()) dispatch(syncAppWithDeviceLanguage())
...@@ -193,10 +193,10 @@ function SidebarWrapper(): JSX.Element { ...@@ -193,10 +193,10 @@ function SidebarWrapper(): JSX.Element {
return backgroundToSidePanelMessageChannel.addMessageListener( return backgroundToSidePanelMessageChannel.addMessageListener(
BackgroundToSidePanelRequestType.RefreshUnitags, BackgroundToSidePanelRequestType.RefreshUnitags,
() => { () => {
triggerRefetchUnitags() resetUnitagsQueries()
}, },
) )
}, [triggerRefetchUnitags]) }, [resetUnitagsQueries])
return ( return (
<> <>
...@@ -235,12 +235,10 @@ export default function SidebarApp(): JSX.Element { ...@@ -235,12 +235,10 @@ export default function SidebarApp(): JSX.Element {
return ( return (
<PersistGate persistor={getReduxPersistor()}> <PersistGate persistor={getReduxPersistor()}>
<BaseAppContainer appName={DatadogAppNameTag.Sidebar}> <BaseAppContainer appName={DatadogAppNameTag.Sidebar}>
<UnitagUpdaterContextProvider> <DappContextProvider>
<DappContextProvider> <PrimaryAppInstanceDebuggerLazy />
<PrimaryAppInstanceDebuggerLazy /> <RouterProvider router={router} />
<RouterProvider router={router} /> </DappContextProvider>
</DappContextProvider>
</UnitagUpdaterContextProvider>
</BaseAppContainer> </BaseAppContainer>
</PersistGate> </PersistGate>
) )
......
...@@ -22,7 +22,6 @@ import { UnitagClaimRoutes } from 'src/app/navigation/constants' ...@@ -22,7 +22,6 @@ import { UnitagClaimRoutes } from 'src/app/navigation/constants'
import { setRouter, setRouterState } from 'src/app/navigation/state' import { setRouter, setRouterState } from 'src/app/navigation/state'
import { initExtensionAnalytics } from 'src/app/utils/analytics' import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { Flex } from 'ui/src' import { Flex } from 'ui/src'
import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { usePrevious } from 'utilities/src/react/hooks' import { usePrevious } from 'utilities/src/react/hooks'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics' import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics'
...@@ -139,9 +138,7 @@ export default function UnitagClaimApp(): JSX.Element { ...@@ -139,9 +138,7 @@ export default function UnitagClaimApp(): JSX.Element {
return ( return (
<BaseAppContainer appName={DatadogAppNameTag.UnitagClaim}> <BaseAppContainer appName={DatadogAppNameTag.UnitagClaim}>
<UnitagUpdaterContextProvider> <RouterProvider router={router} />
<RouterProvider router={router} />
</UnitagUpdaterContextProvider>
</BaseAppContainer> </BaseAppContainer>
) )
} }
...@@ -35,6 +35,7 @@ import { MenuContentItem } from 'wallet/src/components/menu/types' ...@@ -35,6 +35,7 @@ import { MenuContentItem } from 'wallet/src/components/menu/types'
import { createOnboardingAccount } from 'wallet/src/features/onboarding/createOnboardingAccount' import { createOnboardingAccount } from 'wallet/src/features/onboarding/createOnboardingAccount'
import { useCanActiveAddressClaimUnitag } from 'wallet/src/features/unitags/hooks/useCanActiveAddressClaimUnitag' import { useCanActiveAddressClaimUnitag } from 'wallet/src/features/unitags/hooks/useCanActiveAddressClaimUnitag'
import { BackupType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' 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 { createAccountsActions } from 'wallet/src/features/wallet/create/createAccountsSaga'
import { import {
useActiveAccountAddressWithThrow, useActiveAccountAddressWithThrow,
...@@ -128,7 +129,7 @@ export function AccountSwitcherScreen(): JSX.Element { ...@@ -128,7 +129,7 @@ export function AccountSwitcherScreen(): JSX.Element {
wallet_type: ImportType.CreateAdditional, wallet_type: ImportType.CreateAdditional,
accounts_imported_count: 1, accounts_imported_count: 1,
wallets_imported: [pendingWallet.address], wallets_imported: [pendingWallet.address],
cloud_backup_used: pendingWallet.backups?.includes(BackupType.Cloud) ?? false, cloud_backup_used: hasBackup(BackupType.Cloud, pendingWallet),
modal: ModalName.AccountSwitcher, modal: ModalName.AccountSwitcher,
}) })
......
...@@ -4,9 +4,9 @@ import { Button, Flex, Text } from 'ui/src' ...@@ -4,9 +4,9 @@ import { Button, Flex, Text } from 'ui/src'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { TextInput } from 'uniswap/src/components/input/TextInput' import { TextInput } from 'uniswap/src/components/input/TextInput'
import { Modal } from 'uniswap/src/components/modals/Modal' 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 { ModalName } from 'uniswap/src/features/telemetry/constants'
import { shortenAddress } from 'utilities/src/addresses' import { shortenAddress } from 'utilities/src/addresses'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
import { SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' import { SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types'
type CreateWalletModalProps = { type CreateWalletModalProps = {
......
...@@ -8,11 +8,11 @@ import { Person } from 'ui/src/components/icons' ...@@ -8,11 +8,11 @@ import { Person } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { TextInput } from 'uniswap/src/components/input/TextInput' import { TextInput } from 'uniswap/src/components/input/TextInput'
import { Modal } from 'uniswap/src/components/modals/Modal' 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 { ModalName } from 'uniswap/src/features/telemetry/constants'
import { OnboardingCardLoggingName } from 'uniswap/src/features/telemetry/types' import { OnboardingCardLoggingName } from 'uniswap/src/features/telemetry/types'
import { UNITAG_SUFFIX_NO_LEADING_DOT } from 'uniswap/src/features/unitags/constants' import { UNITAG_SUFFIX_NO_LEADING_DOT } from 'uniswap/src/features/unitags/constants'
import { shortenAddress } from 'utilities/src/addresses' 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 { CardType, IntroCard, IntroCardGraphicType } from 'wallet/src/components/introCards/IntroCard'
import { useCanActiveAddressClaimUnitag } from 'wallet/src/features/unitags/hooks/useCanActiveAddressClaimUnitag' import { useCanActiveAddressClaimUnitag } from 'wallet/src/features/unitags/hooks/useCanActiveAddressClaimUnitag'
import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga' 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 { useTranslation } from 'react-i18next'
import { useDappLastChainId } from 'src/app/features/dapp/hooks' import { useDappLastChainId } from 'src/app/features/dapp/hooks'
import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext' 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 { Anchor, AnimatePresence, Button, Flex, Text, UniversalImage, UniversalImageResizeMode, styled } from 'ui/src'
import { borderRadii, iconSizes } from 'ui/src/theme' import { borderRadii, iconSizes } from 'ui/src/theme'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
...@@ -15,6 +16,8 @@ import { TransactionTypeInfo } from 'uniswap/src/features/transactions/types/tra ...@@ -15,6 +16,8 @@ import { TransactionTypeInfo } from 'uniswap/src/features/transactions/types/tra
import { extractNameFromUrl } from 'utilities/src/format/extractNameFromUrl' import { extractNameFromUrl } from 'utilities/src/format/extractNameFromUrl'
import { formatDappURL } from 'utilities/src/format/urls' import { formatDappURL } from 'utilities/src/format/urls'
import { logger } from 'utilities/src/logger/logger' 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 { DappIconPlaceholder } from 'wallet/src/components/WalletConnect/DappIconPlaceholder'
import { AddressFooter } from 'wallet/src/features/transactions/TransactionRequest/AddressFooter' import { AddressFooter } from 'wallet/src/features/transactions/TransactionRequest/AddressFooter'
import { NetworkFeeFooter } from 'wallet/src/features/transactions/TransactionRequest/NetworkFeeFooter' import { NetworkFeeFooter } from 'wallet/src/features/transactions/TransactionRequest/NetworkFeeFooter'
...@@ -184,6 +187,7 @@ function DappRequestFooter({ ...@@ -184,6 +187,7 @@ function DappRequestFooter({
request.dappRequest.type === DappRequestType.SendTransaction ? request.dappRequest.transaction.chainId : undefined request.dappRequest.type === DappRequestType.SendTransaction ? request.dappRequest.transaction.chainId : undefined
const currentChainId = chainId || sendTransactionChainId || activeChain || defaultChainId const currentChainId = chainId || sendTransactionChainId || activeChain || defaultChainId
const { balance: nativeBalance } = useOnChainNativeCurrencyBalance(currentChainId, currentAccount.address) const { balance: nativeBalance } = useOnChainNativeCurrencyBalance(currentChainId, currentAccount.address)
const isRequestConfirming = useIsDappRequestConfirming(request.dappRequest.requestId)
const hasSufficientGas = hasSufficientFundsIncludingGas({ const hasSufficientGas = hasSufficientFundsIncludingGas({
gasFee: transactionGasFeeResult?.value, gasFee: transactionGasFeeResult?.value,
...@@ -198,7 +202,11 @@ function DappRequestFooter({ ...@@ -198,7 +202,11 @@ function DappRequestFooter({
? transactionGasFeeResult?.value && hasSufficientGas ? transactionGasFeeResult?.value && hasSufficientGas
: true : true
const handleOnConfirm = useCallback(async () => { const handleOnConfirm = useEvent(async () => {
if (isRequestConfirming) {
return
}
if (onConfirm) { if (onConfirm) {
onConfirm() onConfirm()
} else { } else {
...@@ -208,9 +216,12 @@ function DappRequestFooter({ ...@@ -208,9 +216,12 @@ function DappRequestFooter({
if (maybeCloseOnConfirm && shouldCloseSidebar) { if (maybeCloseOnConfirm && shouldCloseSidebar) {
setTimeout(window.close, WINDOW_CLOSE_DELAY) 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) { if (onCancel) {
onCancel() onCancel()
} else { } else {
...@@ -220,7 +231,10 @@ function DappRequestFooter({ ...@@ -220,7 +231,10 @@ function DappRequestFooter({
if (shouldCloseSidebar) { if (shouldCloseSidebar) {
setTimeout(window.close, WINDOW_CLOSE_DELAY) setTimeout(window.close, WINDOW_CLOSE_DELAY)
} }
}, [request, onCancel, defaultOnCancel, shouldCloseSidebar]) })
const isDisabled = !isConfirmEnabled || disableConfirm || isConfirming || isRequestConfirming
const isLoading = isRequestConfirming || isConfirming
return ( return (
<> <>
...@@ -253,11 +267,12 @@ function DappRequestFooter({ ...@@ -253,11 +267,12 @@ function DappRequestFooter({
{t('common.button.cancel')} {t('common.button.cancel')}
</Button> </Button>
<Button <Button
isDisabled={!isConfirmEnabled || disableConfirm} isDisabled={isDisabled}
loading={isLoading}
flexBasis={1} flexBasis={1}
size="medium" size="medium"
variant="branded" variant="branded"
onPress={handleOnConfirm} onPress={debouncedHandleOnConfirm}
> >
{confirmText} {confirmText}
</Button> </Button>
......
...@@ -7,18 +7,17 @@ import { ...@@ -7,18 +7,17 @@ import {
DappRequestQueueProvider, DappRequestQueueProvider,
useDappRequestQueueContext, useDappRequestQueueContext,
} from 'src/app/features/dappRequests/DappRequestQueueContext' } 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 { ConnectionRequestContent } from 'src/app/features/dappRequests/requestContent/Connection/ConnectionRequestContent'
import { EthSendRequestContent } from 'src/app/features/dappRequests/requestContent/EthSend/EthSend' import { EthSendRequestContent } from 'src/app/features/dappRequests/requestContent/EthSend/EthSend'
import { PersonalSignRequestContent } from 'src/app/features/dappRequests/requestContent/PersonalSign/PersonalSignRequestContent' import { PersonalSignRequestContent } from 'src/app/features/dappRequests/requestContent/PersonalSign/PersonalSignRequestContent'
import { SignTypedDataRequestContent } from 'src/app/features/dappRequests/requestContent/SignTypeData/SignTypedDataRequestContent' import { SignTypedDataRequestContent } from 'src/app/features/dappRequests/requestContent/SignTypeData/SignTypedDataRequestContent'
import { rejectAllRequests } from 'src/app/features/dappRequests/saga' import { isDappRequestStoreItemForEthSendTxn, selectAllDappRequests } from 'src/app/features/dappRequests/slice'
import { isDappRequestStoreItemForEthSendTxn } from 'src/app/features/dappRequests/slice'
import { import {
isConnectionRequest, isConnectionRequest,
isSignMessageRequest, isSignMessageRequest,
isSignTypedDataRequest, isSignTypedDataRequest,
} from 'src/app/features/dappRequests/types/DappRequestTypes' } from 'src/app/features/dappRequests/types/DappRequestTypes'
import { ExtensionState } from 'src/store/extensionReducer'
import { AnimatePresence, Flex, Text, TouchableArea, useSporeColors } from 'ui/src' import { AnimatePresence, Flex, Text, TouchableArea, useSporeColors } from 'ui/src'
import { ReceiptText, RotatableChevron } from 'ui/src/components/icons' import { ReceiptText, RotatableChevron } from 'ui/src/components/icons'
import { iconSizes, zIndexes } from 'ui/src/theme' import { iconSizes, zIndexes } from 'ui/src/theme'
...@@ -28,14 +27,14 @@ import { ModalName } from 'uniswap/src/features/telemetry/constants' ...@@ -28,14 +27,14 @@ import { ModalName } from 'uniswap/src/features/telemetry/constants'
const REJECT_MESSAGE_HEIGHT = 48 const REJECT_MESSAGE_HEIGHT = 48
export function DappRequestQueue(): JSX.Element { export function DappRequestQueue(): JSX.Element {
const pendingDappRequests = useSelector((state: ExtensionState) => state.dappRequests.pending) const dappRequests = useSelector(selectAllDappRequests)
const areRequestsPending = pendingDappRequests.length > 0 const requestsExist = dappRequests.length > 0
return ( return (
<Modal <Modal
alignment="top" alignment="top"
backgroundColor="$transparent" backgroundColor="$transparent"
isModalOpen={areRequestsPending} isModalOpen={requestsExist}
name={ModalName.DappRequest} name={ModalName.DappRequest}
padding="$none" padding="$none"
zIndex={zIndexes.overlay} zIndex={zIndexes.overlay}
......
import { providerErrors, serializeError } from '@metamask/rpc-errors' import { providerErrors, serializeError } from '@metamask/rpc-errors'
import { PropsWithChildren, createContext, useContext, useEffect, useRef, useState } from 'react' import { PropsWithChildren, createContext, useContext, useEffect, useRef, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { import { confirmRequest, confirmRequestNoDappInfo, rejectRequest } from 'src/app/features/dappRequests/actions'
confirmRequest, import { isDappRequestWithDappInfo } from 'src/app/features/dappRequests/saga'
confirmRequestNoDappInfo, import type { DappRequestStoreItem } from 'src/app/features/dappRequests/shared'
isDappRequestWithDappInfo, import { selectAllDappRequests } from 'src/app/features/dappRequests/slice'
rejectRequest,
} from 'src/app/features/dappRequests/saga'
import { DappRequestStoreItem } from 'src/app/features/dappRequests/slice'
import { ExtensionState } from 'src/store/extensionReducer'
import { DappResponseType } from 'uniswap/src/features/dappRequests/types' import { DappResponseType } from 'uniswap/src/features/dappRequests/types'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { DappRequestAction } from 'uniswap/src/features/telemetry/types' import { DappRequestAction } from 'uniswap/src/features/telemetry/types'
import { TransactionTypeInfo } from 'uniswap/src/features/transactions/types/transactionDetails' import { TransactionTypeInfo } from 'uniswap/src/features/transactions/types/transactionDetails'
import { extractBaseUrl } from 'utilities/src/format/urls' import { extractBaseUrl } from 'utilities/src/format/urls'
import { useEvent } from 'utilities/src/react/hooks'
import { Account } from 'wallet/src/features/wallet/accounts/types' import { Account } from 'wallet/src/features/wallet/accounts/types'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
interface DappRequestQueueContextValue { interface DappRequestQueueContextValue {
forwards: boolean // direction of sliding animation forwards: boolean // direction of sliding animation
increasing: boolean // direction of number increasing animation increasing: boolean // direction of number increasing animation
...@@ -40,10 +36,10 @@ export function DappRequestQueueProvider({ children }: PropsWithChildren): JSX.E ...@@ -40,10 +36,10 @@ export function DappRequestQueueProvider({ children }: PropsWithChildren): JSX.E
const [currentIndex, setCurrentIndex] = useState(0) const [currentIndex, setCurrentIndex] = useState(0)
// Show the top most pending request // Show the top most pending request
const pendingRequests = useSelector((state: ExtensionState) => state.dappRequests.pending) const dappRequests = useSelector(selectAllDappRequests)
const request = pendingRequests[currentIndex] const request = dappRequests[currentIndex]
const totalRequestCount = pendingRequests.length const totalRequestCount = dappRequests.length
const activeAccount = useActiveAccountWithThrow() const activeAccount = useActiveAccountWithThrow()
...@@ -77,37 +73,36 @@ export function DappRequestQueueProvider({ children }: PropsWithChildren): JSX.E ...@@ -77,37 +73,36 @@ export function DappRequestQueueProvider({ children }: PropsWithChildren): JSX.E
} }
} }
const onConfirm = async ( const onConfirm = useEvent(
requestToConfirm: DappRequestStoreItem, async (requestToConfirm: DappRequestStoreItem, transactionTypeInfo?: TransactionTypeInfo): Promise<void> => {
transactionTypeInfo?: TransactionTypeInfo, const requestWithTxInfo = {
): Promise<void> => { ...requestToConfirm,
const requestWithTxInfo = { transactionTypeInfo,
...requestToConfirm, }
transactionTypeInfo, if (requestToConfirm.dappInfo) {
} const { activeConnectedAddress, lastChainId } = requestToConfirm.dappInfo
if (requestToConfirm.dappInfo) { const connectedAddresses = requestToConfirm.dappInfo.connectedAccounts.map((account) => account.address)
const { activeConnectedAddress, lastChainId } = requestToConfirm.dappInfo sendAnalyticsEvent(ExtensionEventName.DappRequest, {
const connectedAddresses = requestToConfirm.dappInfo.connectedAccounts.map((account) => account.address) action: DappRequestAction.Accept,
sendAnalyticsEvent(ExtensionEventName.DappRequest, { requestType: requestToConfirm.dappRequest.type,
action: DappRequestAction.Accept, dappUrl: extractBaseUrl(requestToConfirm.senderTabInfo.url),
requestType: requestToConfirm.dappRequest.type, chainId: lastChainId,
dappUrl: extractBaseUrl(requestToConfirm.senderTabInfo.url), activeConnectedAddress,
chainId: lastChainId, connectedAddresses,
activeConnectedAddress, })
connectedAddresses, }
})
} if (isDappRequestWithDappInfo(requestWithTxInfo)) {
await dispatch(confirmRequest(requestWithTxInfo))
if (isDappRequestWithDappInfo(requestWithTxInfo)) { } else {
await dispatch(confirmRequest(requestWithTxInfo)) await dispatch(confirmRequestNoDappInfo(requestWithTxInfo))
} else { }
await dispatch(confirmRequestNoDappInfo(requestWithTxInfo))
} setCurrentIndex((prev) => Math.max(0, prev - 1))
},
setCurrentIndex((prev) => Math.max(0, prev - 1)) )
}
const onCancel = useEvent(async (requestToCancel: DappRequestStoreItem): Promise<void> => {
const onCancel = async (requestToCancel: DappRequestStoreItem): Promise<void> => {
if (requestToCancel.dappInfo) { if (requestToCancel.dappInfo) {
const { activeConnectedAddress, lastChainId } = requestToCancel.dappInfo const { activeConnectedAddress, lastChainId } = requestToCancel.dappInfo
const connectedAddresses = requestToCancel.dappInfo.connectedAccounts.map((account) => account.address) const connectedAddresses = requestToCancel.dappInfo.connectedAccounts.map((account) => account.address)
...@@ -132,7 +127,7 @@ export function DappRequestQueueProvider({ children }: PropsWithChildren): JSX.E ...@@ -132,7 +127,7 @@ export function DappRequestQueueProvider({ children }: PropsWithChildren): JSX.E
) )
setCurrentIndex((prev) => Math.max(0, prev - 1)) setCurrentIndex((prev) => Math.max(0, prev - 1))
} })
const onPressNext = (): void => { const onPressNext = (): void => {
setForwards(true) setForwards(true)
......
...@@ -4,7 +4,7 @@ import { providerErrors, serializeError } from '@metamask/rpc-errors' ...@@ -4,7 +4,7 @@ import { providerErrors, serializeError } from '@metamask/rpc-errors'
import { saveDappConnection } from 'src/app/features/dapp/actions' import { saveDappConnection } from 'src/app/features/dapp/actions'
import { DappInfo, dappStore } from 'src/app/features/dapp/store' import { DappInfo, dappStore } from 'src/app/features/dapp/store'
import { getOrderedConnectedAddresses } from 'src/app/features/dapp/utils' 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 { import {
AccountResponse, AccountResponse,
DappRequest, 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 @@ ...@@ -2,6 +2,12 @@
import { providerErrors, serializeError } from '@metamask/rpc-errors' import { providerErrors, serializeError } from '@metamask/rpc-errors'
import { PayloadAction } from '@reduxjs/toolkit' import { PayloadAction } from '@reduxjs/toolkit'
import { getAccount, getAccountRequest } from 'src/app/features/dappRequests/accounts' 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 { getChainId, getChainIdNoDappInfo } from 'src/app/features/dappRequests/getChainId'
import { import {
handleGetPermissionsRequest, handleGetPermissionsRequest,
...@@ -9,22 +15,20 @@ import { ...@@ -9,22 +15,20 @@ import {
handleRevokePermissions, handleRevokePermissions,
} from 'src/app/features/dappRequests/permissions' } from 'src/app/features/dappRequests/permissions'
import { import {
DappRequestNoDappInfo,
DappRequestRejectParams,
DappRequestWithDappInfo,
changeChainSaga, changeChainSaga,
confirmRequest,
confirmRequestNoDappInfo,
handleGetCallsStatus, handleGetCallsStatus,
handleSendCalls, handleSendCalls,
handleSendTransaction, handleSendTransaction,
handleSignMessage, handleSignMessage,
handleSignTypedData, handleSignTypedData,
handleUniswapOpenSidebarRequest, handleUniswapOpenSidebarRequest,
rejectAllRequests,
rejectRequest,
} from 'src/app/features/dappRequests/saga' } 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 { import {
BaseSendTransactionRequest, BaseSendTransactionRequest,
BaseSendTransactionRequestSchema, BaseSendTransactionRequestSchema,
...@@ -55,7 +59,6 @@ import { ...@@ -55,7 +59,6 @@ import {
UniswapOpenSidebarRequestSchema, UniswapOpenSidebarRequestSchema,
} from 'src/app/features/dappRequests/types/DappRequestTypes' } from 'src/app/features/dappRequests/types/DappRequestTypes'
import { dappResponseMessageChannel } from 'src/background/messagePassing/messageChannels' import { dappResponseMessageChannel } from 'src/background/messagePassing/messageChannels'
import { ExtensionState } from 'src/store/extensionReducer'
import { call, put, select, takeEvery } from 'typed-redux-saga' import { call, put, select, takeEvery } from 'typed-redux-saga'
import { DappRequestType, DappResponseType } from 'uniswap/src/features/dappRequests/types' import { DappRequestType, DappResponseType } from 'uniswap/src/features/dappRequests/types'
import { getEnabledChainIdsSaga } from 'uniswap/src/features/settings/saga' import { getEnabledChainIdsSaga } from 'uniswap/src/features/settings/saga'
...@@ -66,16 +69,16 @@ function* dappRequestApproval({ ...@@ -66,16 +69,16 @@ function* dappRequestApproval({
payload: request, payload: request,
}: PayloadAction<DappRequestWithDappInfo | DappRequestNoDappInfo | DappRequestRejectParams>) { }: PayloadAction<DappRequestWithDappInfo | DappRequestNoDappInfo | DappRequestRejectParams>) {
if (type === rejectAllRequests.type) { 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 = { const errorResponse: ErrorResponse = {
type: DappResponseType.ErrorResponse, type: DappResponseType.ErrorResponse,
error: serializeError(providerErrors.userRejectedRequest()), 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()) yield* put(dappRequestActions.removeAll())
......
import { DappInfo } from 'src/app/features/dapp/store' 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 { ChainIdResponse, GetChainIdRequest } from 'src/app/features/dappRequests/types/DappRequestTypes'
import { dappResponseMessageChannel } from 'src/background/messagePassing/messageChannels' import { dappResponseMessageChannel } from 'src/background/messagePassing/messageChannels'
import { call } from 'typed-redux-saga' 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 { 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 { import {
isRequestAccountRequest, isRequestAccountRequest,
isRequestPermissionsRequest, isRequestPermissionsRequest,
} from 'src/app/features/dappRequests/types/DappRequestTypes' } from 'src/app/features/dappRequests/types/DappRequestTypes'
import { ExtensionState } from 'src/store/extensionReducer'
import { getBridgingDappUrls } from 'uniswap/src/features/bridging/constants' import { getBridgingDappUrls } from 'uniswap/src/features/bridging/constants'
import { useBridgingSupportedChainIds, useNumBridgingChains } from 'uniswap/src/features/bridging/hooks/chains' import { useBridgingSupportedChainIds, useNumBridgingChains } from 'uniswap/src/features/bridging/hooks/chains'
import { selectHasViewedDappRequestBridgingBanner } from 'wallet/src/features/behaviorHistory/selectors' import { selectHasViewedDappRequestBridgingBanner } from 'wallet/src/features/behaviorHistory/selectors'
...@@ -42,3 +44,8 @@ export function useShouldShowBridgingRequestCard( ...@@ -42,3 +44,8 @@ export function useShouldShowBridgingRequestCard(
shouldShowBridgingRequestCard: isBridgingConnectionRequest && !hasViewedDappRequestBridgingBanner, 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' ...@@ -3,7 +3,7 @@ import { rpcErrors, serializeError } from '@metamask/rpc-errors'
import { removeDappConnection } from 'src/app/features/dapp/actions' import { removeDappConnection } from 'src/app/features/dapp/actions'
import { DappInfo } from 'src/app/features/dapp/store' import { DappInfo } from 'src/app/features/dapp/store'
import { saveAccount } from 'src/app/features/dappRequests/accounts' 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 { import {
ErrorResponse, ErrorResponse,
GetPermissionsRequest, GetPermissionsRequest,
......
...@@ -5,13 +5,13 @@ import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestCon ...@@ -5,13 +5,13 @@ import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestCon
import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext' import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext'
import { isNonZeroBigNumber } from 'src/app/features/dappRequests/requestContent/EthSend/Swap/utils' import { isNonZeroBigNumber } from 'src/app/features/dappRequests/requestContent/EthSend/Swap/utils'
import { SendTransactionRequest } from 'src/app/features/dappRequests/types/DappRequestTypes' 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 { Anchor, Flex, Text, TouchableArea } from 'ui/src'
import { AnimatedCopySheets, ExternalLink } from 'ui/src/components/icons' import { AnimatedCopySheets, ExternalLink } from 'ui/src/components/icons'
import { GasFeeResult } from 'uniswap/src/features/gas/types' import { GasFeeResult } from 'uniswap/src/features/gas/types'
import { CopyNotificationType } from 'uniswap/src/features/notifications/types' import { CopyNotificationType } from 'uniswap/src/features/notifications/types'
import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking'
import { ellipseMiddle, shortenAddress } from 'utilities/src/addresses' 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 { ContentRow } from 'wallet/src/features/transactions/TransactionRequest/ContentRow'
import { import {
SpendingDetails, SpendingDetails,
......
...@@ -69,7 +69,7 @@ export function useSwapDetails( ...@@ -69,7 +69,7 @@ export function useSwapDetails(
if (v4Command) { if (v4Command) {
// Extract details using the V4 helper // 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 inputAddress = v4Details.inputAddress === ETH_ADDRESS ? DEFAULT_NATIVE_ADDRESS : v4Details.inputAddress
outputAddress = v4Details.outputAddress === ETH_ADDRESS ? DEFAULT_NATIVE_ADDRESS : v4Details.outputAddress outputAddress = v4Details.outputAddress === ETH_ADDRESS ? DEFAULT_NATIVE_ADDRESS : v4Details.outputAddress
inputValue = v4Details.inputValue || '0' inputValue = v4Details.inputValue || '0'
...@@ -199,7 +199,10 @@ function getTokenAddressesFromV2V3SwapCommands(command: UniversalRouterCommand): ...@@ -199,7 +199,10 @@ function getTokenAddressesFromV2V3SwapCommands(command: UniversalRouterCommand):
return { inputAddress, outputAddress } return { inputAddress, outputAddress }
} }
function getTokenDetailsFromV4SwapCommands(command: UniversalRouterCommand): { function getTokenDetailsFromV4SwapCommands(
command: UniversalRouterCommand,
allCommands?: UniversalRouterCommand[],
): {
inputAddress?: string inputAddress?: string
outputAddress?: string outputAddress?: string
inputValue?: string inputValue?: string
...@@ -321,6 +324,21 @@ function getTokenDetailsFromV4SwapCommands(command: UniversalRouterCommand): { ...@@ -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 } return { inputAddress, outputAddress, inputValue, outputValue }
} }
......
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import { Provider, TransactionResponse } from '@ethersproject/providers' import { Provider, TransactionResponse } from '@ethersproject/providers'
import { providerErrors, rpcErrors, serializeError } from '@metamask/rpc-errors' import { providerErrors, rpcErrors, serializeError } from '@metamask/rpc-errors'
import { createAction } from '@reduxjs/toolkit'
import { createSearchParams } from 'react-router-dom' import { createSearchParams } from 'react-router-dom'
import { changeChain } from 'src/app/features/dapp/changeChain' import { changeChain } from 'src/app/features/dapp/changeChain'
import { DappInfo, dappStore } from 'src/app/features/dapp/store' import { DappInfo, dappStore } from 'src/app/features/dapp/store'
import { getActiveConnectedAccount } from 'src/app/features/dapp/utils' 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 { import {
BaseSendTransactionRequest, BaseSendTransactionRequest,
ChangeChainRequest, ChangeChainRequest,
...@@ -46,37 +57,20 @@ import { ...@@ -46,37 +57,20 @@ import {
} from 'uniswap/src/features/transactions/types/transactionDetails' } from 'uniswap/src/features/transactions/types/transactionDetails'
import { extractBaseUrl } from 'utilities/src/format/urls' import { extractBaseUrl } from 'utilities/src/format/urls'
import { logger } from 'utilities/src/logger/logger' 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 { getProvider, getSignerManager } from 'wallet/src/features/wallet/context'
import { selectActiveAccount } from 'wallet/src/features/wallet/selectors' import { selectActiveAccount } from 'wallet/src/features/wallet/selectors'
import { signMessage, signTypedDataMessage } from 'wallet/src/features/wallet/signing/signing' 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( export function isDappRequestWithDappInfo(
request: DappRequestNoDappInfo | DappRequestWithDappInfo, request: DappRequestNoDappInfo | DappRequestWithDappInfo,
): request is DappRequestWithDappInfo { ): request is DappRequestWithDappInfo {
return 'dappInfo' in request && Boolean(request.dappInfo) 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() { export function* dappRequestWatcher() {
while (true) { while (true) {
const { payload, type } = yield* take(addRequest) const { payload, type } = yield* take(addRequest)
...@@ -101,7 +95,7 @@ function* handleRequest(requestParams: DappRequestNoDappInfo) { ...@@ -101,7 +95,7 @@ function* handleRequest(requestParams: DappRequestNoDappInfo) {
if (requestParams.dappRequest.type === DappRequestType.UniswapOpenSidebar) { if (requestParams.dappRequest.type === DappRequestType.UniswapOpenSidebar) {
// We can auto-confirm these requests since they are only for navigating to a certain tab // We can auto-confirm these requests since they are only for navigating to a certain tab
// At this point the sidebar is already open // At this point the sidebar is already open
yield* put(confirmRequestNoDappInfo(requestParams)) yield* call(handleConfirmRequestNoDappInfo, requestParams)
return return
} }
const activeAccount = yield* select(selectActiveAccount) const activeAccount = yield* select(selectActiveAccount)
...@@ -148,9 +142,9 @@ function* handleRequest(requestParams: DappRequestNoDappInfo) { ...@@ -148,9 +142,9 @@ function* handleRequest(requestParams: DappRequestNoDappInfo) {
const chainId = toSupportedChainId(hexadecimalStringToInt(requestParams.dappRequest.chainId)) const chainId = toSupportedChainId(hexadecimalStringToInt(requestParams.dappRequest.chainId))
if (chainId) { if (chainId) {
if (dappInfo) { if (dappInfo) {
yield* put(confirmRequest({ ...requestParams, dappInfo })) yield* call(handleConfirmRequestWithDappInfo, { ...requestParams, dappInfo })
} else { } else {
yield* put(confirmRequestNoDappInfo(requestParams)) yield* call(handleConfirmRequestNoDappInfo, requestParams)
} }
if (isWalletUnlocked) { if (isWalletUnlocked) {
yield* put( yield* put(
...@@ -228,7 +222,7 @@ function* handleRequest(requestParams: DappRequestNoDappInfo) { ...@@ -228,7 +222,7 @@ function* handleRequest(requestParams: DappRequestNoDappInfo) {
requestParams.dappRequest.type === DappRequestType.GetCallsStatus) requestParams.dappRequest.type === DappRequestType.GetCallsStatus)
if (shouldAutoConfirmRequest) { if (shouldAutoConfirmRequest) {
yield* put(confirmRequest({ ...requestParams, dappInfo })) yield* call(handleConfirmRequestWithDappInfo, { ...requestParams, dappInfo })
} else { } else {
yield* put( yield* put(
dappRequestActions.add({ dappRequestActions.add({
...@@ -257,7 +251,7 @@ export function* handleSendTransaction( ...@@ -257,7 +251,7 @@ export function* handleSendTransaction(
const provider = yield* call(getProvider, lastChainId) const provider = yield* call(getProvider, lastChainId)
const sendTransactionParams: SendTransactionParams = { const sendTransactionParams: ExecuteTransactionParams = {
chainId: lastChainId, chainId: lastChainId,
account, account,
options: { request: transactionRequest }, options: { request: transactionRequest },
...@@ -272,7 +266,7 @@ export function* handleSendTransaction( ...@@ -272,7 +266,7 @@ export function* handleSendTransaction(
transactionOriginType: TransactionOriginType.External, 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 // Trigger a pending transaction notification after we send the transaction to chain
yield* put( yield* put(
...@@ -502,3 +496,22 @@ export function* handleGetCallsStatus(request: GetCallsStatusRequest, { id }: Se ...@@ -502,3 +496,22 @@ export function* handleGetCallsStatus(request: GetCallsStatusRequest, { id }: Se
yield* call(dappResponseMessageChannel.sendMessageToTab, id, response) 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 { 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 { import {
DappRequest,
isConnectionRequest, isConnectionRequest,
isSendTransactionRequest, isSendTransactionRequest,
SendTransactionRequest, type SendTransactionRequest,
} from 'src/app/features/dappRequests/types/DappRequestTypes' } from 'src/app/features/dappRequests/types/DappRequestTypes'
export interface SenderTabInfo { type RequestId = string
id: number type WithMetadata<T> = T & {
url: string createdAt: number
favIconUrl?: string status: DappRequestStatus
} }
export interface DappRequestState {
interface DappRequestState { requests: Record<RequestId, WithMetadata<DappRequestStoreItem>>
pending: DappRequestStoreItem[]
} }
const initialDappRequestState: DappRequestState = { const initialDappRequestState: DappRequestState = {
pending: [], // ordered array with the most recent request at the end requests: {}, // record of requestId -> request
}
export interface DappRequestStoreItem {
dappRequest: DappRequest
senderTabInfo: SenderTabInfo
dappInfo?: DappInfo
isSidebarClosed: boolean | undefined
} }
// Enforces that a request object in state is for an eth send txn request // Enforces that a request object in state is for an eth send txn request
export interface DappRequestStoreItemForEthSendTxn extends DappRequestStoreItem { export interface DappRequestStoreItemForEthSendTxn extends DappRequestStoreItem {
dappRequest: SendTransactionRequest dappRequest: WithMetadata<SendTransactionRequest>
} }
export function isDappRequestStoreItemForEthSendTxn( export function isDappRequestStoreItemForEthSendTxn(
...@@ -39,31 +32,64 @@ export function isDappRequestStoreItemForEthSendTxn( ...@@ -39,31 +32,64 @@ export function isDappRequestStoreItemForEthSendTxn(
return isSendTransactionRequest(request.dappRequest) 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({ const slice = createSlice({
name: 'dappRequests', name: 'dappRequests',
initialState: initialDappRequestState, initialState: initialDappRequestState,
reducers: { reducers: {
add: (state, action: PayloadAction<DappRequestStoreItem>) => { add: (state, action: PayloadAction<DappRequestStoreItem>) => {
if (isConnectionRequest(action.payload.dappRequest)) { if (isConnectionRequest(action.payload.dappRequest)) {
// Remove existing connection requests from the same tab and of the same type const requests = selectDappRequestsArray(state)
state.pending = state.pending.filter( for (const request of requests) {
(request) => // Remove existing connection requests from the same tab and of the same type
request.senderTabInfo.id !== action.payload.senderTabInfo.id || if (
request.dappRequest.type !== action.payload.dappRequest.type, 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>) => { remove: (state, action: PayloadAction<string>) => {
const requestId = action.payload const requestId = action.payload
const newState = state.pending.filter((tx) => tx.dappRequest.requestId !== requestId) delete state.requests[requestId]
state.pending = newState
}, },
removeAll: (state) => { 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 dappRequestActions = slice.actions
export const dappRequestReducer = slice.reducer export const dappRequestReducer = slice.reducer
...@@ -12,6 +12,7 @@ import { Circle, Flex, Popover, Text, TouchableArea, UniversalImage } from 'ui/s ...@@ -12,6 +12,7 @@ import { Circle, Flex, Popover, Text, TouchableArea, UniversalImage } from 'ui/s
import { animationPresets } from 'ui/src/animations' import { animationPresets } from 'ui/src/animations'
import { CopyAlt, Globe, RotatableChevron, Settings } from 'ui/src/components/icons' import { CopyAlt, Globe, RotatableChevron, Settings } from 'ui/src/components/icons'
import { borderRadii, iconSizes } from 'ui/src/theme' import { borderRadii, iconSizes } from 'ui/src/theme'
import { AccountIcon } from 'uniswap/src/features/accounts/AccountIcon'
import { useAvatar } from 'uniswap/src/features/address/avatar' import { useAvatar } from 'uniswap/src/features/address/avatar'
import { UniverseChainId } from 'uniswap/src/features/chains/types' import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { pushNotification } from 'uniswap/src/features/notifications/slice' import { pushNotification } from 'uniswap/src/features/notifications/slice'
...@@ -25,7 +26,6 @@ import { setClipboard } from 'uniswap/src/utils/clipboard' ...@@ -25,7 +26,6 @@ import { setClipboard } from 'uniswap/src/utils/clipboard'
import { shortenAddress } from 'utilities/src/addresses' import { shortenAddress } from 'utilities/src/addresses'
import { extractNameFromUrl } from 'utilities/src/format/extractNameFromUrl' import { extractNameFromUrl } from 'utilities/src/format/extractNameFromUrl'
import { DappIconPlaceholder } from 'wallet/src/components/WalletConnect/DappIconPlaceholder' 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 { AnimatedUnitagDisplayName } from 'wallet/src/components/accounts/AnimatedUnitagDisplayName'
import useIsFocused from 'wallet/src/features/focus/useIsFocused' import useIsFocused from 'wallet/src/features/focus/useIsFocused'
import { useDisplayName } from 'wallet/src/features/wallet/hooks' import { useDisplayName } from 'wallet/src/features/wallet/hooks'
......
import { Action, AuthenticationTypes } from '@uniswap/client-embeddedwallet/dist/uniswap/embeddedwallet/v1/service_pb' 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 { useTranslation } from 'react-i18next'
import { Navigate, useLocation } from 'react-router-dom' import { Navigate, useLocation } from 'react-router-dom'
import { useOnboardingSteps } from 'src/app/features/onboarding/OnboardingStepsContext' import { useOnboardingSteps } from 'src/app/features/onboarding/OnboardingStepsContext'
...@@ -10,7 +10,10 @@ import { ...@@ -10,7 +10,10 @@ import {
} from 'src/app/features/onboarding/import/types' } from 'src/app/features/onboarding/import/types'
import { OnboardingRoutes, TopLevelRoutes } from 'src/app/navigation/constants' import { OnboardingRoutes, TopLevelRoutes } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state' 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 { fetchChallengeRequest } from 'uniswap/src/data/rest/embeddedWallet/requests'
import { parseMessage } from 'uniswap/src/extension/messagePassing/platform' import { parseMessage } from 'uniswap/src/extension/messagePassing/platform'
import { import {
...@@ -24,6 +27,8 @@ import { useEmbeddedWalletBaseUrl } from 'uniswap/src/features/passkey/hooks/use ...@@ -24,6 +27,8 @@ import { useEmbeddedWalletBaseUrl } from 'uniswap/src/features/passkey/hooks/use
import Trace from 'uniswap/src/features/telemetry/Trace' import Trace from 'uniswap/src/features/telemetry/Trace'
import { ExtensionOnboardingFlow, ExtensionOnboardingScreens } from 'uniswap/src/types/screens/extension' import { ExtensionOnboardingFlow, ExtensionOnboardingScreens } from 'uniswap/src/types/screens/extension'
import { logger } from 'utilities/src/logger/logger' 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' import { v4 as uuid } from 'uuid'
/************************************************************************************************************** /**************************************************************************************************************
...@@ -70,6 +75,9 @@ 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 { export function InitiatePasskeyAuth(): JSX.Element {
const locationState = useLocation().state as InitiatePasskeyAuthLocationState | undefined const locationState = useLocation().state as InitiatePasskeyAuthLocationState | undefined
...@@ -104,8 +112,9 @@ function InitiatePasskeyAuthContent(): JSX.Element { ...@@ -104,8 +112,9 @@ function InitiatePasskeyAuthContent(): JSX.Element {
}) })
} }
const popupWindow = useRef<chrome.windows.Window | undefined>(undefined)
useEffect(() => { useEffect(() => {
let popupWindow: chrome.windows.Window | undefined
let handleMessagePasskeySignInFlowOpened: Parameters<typeof chrome.runtime.onMessageExternal.addListener>[0] let handleMessagePasskeySignInFlowOpened: Parameters<typeof chrome.runtime.onMessageExternal.addListener>[0]
let handleMessagePasskeyCredentialRetrieved: Parameters<typeof chrome.runtime.onMessageExternal.addListener>[0] let handleMessagePasskeyCredentialRetrieved: Parameters<typeof chrome.runtime.onMessageExternal.addListener>[0]
...@@ -141,7 +150,7 @@ function InitiatePasskeyAuthContent(): JSX.Element { ...@@ -141,7 +150,7 @@ function InitiatePasskeyAuthContent(): JSX.Element {
return return
} }
closePopupWindow(popupWindow) closeWindow(popupWindow.current)
importWithCredential(parsedMessage.credential) importWithCredential(parsedMessage.credential)
goToNextStep() goToNextStep()
} }
...@@ -190,12 +199,10 @@ function InitiatePasskeyAuthContent(): JSX.Element { ...@@ -190,12 +199,10 @@ function InitiatePasskeyAuthContent(): JSX.Element {
chrome.runtime.onMessageExternal.addListener(handleMessagePasskeySignInFlowOpened) chrome.runtime.onMessageExternal.addListener(handleMessagePasskeySignInFlowOpened)
// TODO(WALL-6374): center the popup window on the screen popupWindow.current = await openPopupWindow({
popupWindow = await chrome.windows.create({
url: `${webAppBaseUrl}${EXTENSION_PASSKEY_AUTH_PATH}?request_id=${requestId}`, url: `${webAppBaseUrl}${EXTENSION_PASSKEY_AUTH_PATH}?request_id=${requestId}`,
type: 'popup', width: POPUP_WIDTH,
width: 420, height: POPUP_HEIGHT,
height: 335,
}) })
} catch (e) { } catch (e) {
handleError(e, 'initiatePasskeyAuth') handleError(e, 'initiatePasskeyAuth')
...@@ -205,37 +212,85 @@ function InitiatePasskeyAuthContent(): JSX.Element { ...@@ -205,37 +212,85 @@ function InitiatePasskeyAuthContent(): JSX.Element {
initiatePasskeyAuth() initiatePasskeyAuth()
return () => { return () => {
closePopupWindow(popupWindow) closeWindow(popupWindow.current)
chrome.runtime.onMessageExternal.removeListener(handleMessagePasskeySignInFlowOpened) chrome.runtime.onMessageExternal.removeListener(handleMessagePasskeySignInFlowOpened)
chrome.runtime.onMessageExternal.removeListener(handleMessagePasskeyCredentialRetrieved) chrome.runtime.onMessageExternal.removeListener(handleMessagePasskeyCredentialRetrieved)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // 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 ( return (
<Trace <Trace
logImpression logImpression
properties={{ flow: ExtensionOnboardingFlow.Import }} properties={{ flow: ExtensionOnboardingFlow.Import }}
screen={ExtensionOnboardingScreens.InitiatePasskeyAuth} 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> <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> </Flex>
</Trace> </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' ...@@ -6,7 +6,7 @@ import { useExtensionNavigation } from 'src/app/navigation/utils'
import { Flex } from 'ui/src' import { Flex } from 'ui/src'
import { X } from 'ui/src/components/icons' import { X } from 'ui/src/components/icons'
import { ModalName } from 'uniswap/src/features/telemetry/constants' 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' import { SendContextProvider } from 'wallet/src/features/transactions/contexts/SendContext'
export function SendFlow(): JSX.Element { export function SendFlow(): JSX.Element {
......
...@@ -9,11 +9,11 @@ import { UniverseChainId } from 'uniswap/src/features/chains/types' ...@@ -9,11 +9,11 @@ import { UniverseChainId } from 'uniswap/src/features/chains/types'
import Trace from 'uniswap/src/features/telemetry/Trace' import Trace from 'uniswap/src/features/telemetry/Trace'
import { ModalName, SectionName, UniswapEventName } from 'uniswap/src/features/telemetry/constants' import { ModalName, SectionName, UniswapEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' 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 { import {
TransactionScreen, TransactionScreen,
useTransactionModalContext, 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 { useUSDCValue } from 'uniswap/src/features/transactions/hooks/useUSDCPrice'
import { useUSDTokenUpdater } from 'uniswap/src/features/transactions/hooks/useUSDTokenUpdater' import { useUSDTokenUpdater } from 'uniswap/src/features/transactions/hooks/useUSDTokenUpdater'
import { BlockedAddressWarning } from 'uniswap/src/features/transactions/modals/BlockedAddressWarning' import { BlockedAddressWarning } from 'uniswap/src/features/transactions/modals/BlockedAddressWarning'
......
...@@ -32,6 +32,7 @@ import { ...@@ -32,6 +32,7 @@ import {
Lock, Lock,
RotatableChevron, RotatableChevron,
Settings, Settings,
Sliders,
Wrench, Wrench,
} from 'ui/src/components/icons' } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
...@@ -40,8 +41,11 @@ import { resetUniswapBehaviorHistory } from 'uniswap/src/features/behaviorHistor ...@@ -40,8 +41,11 @@ import { resetUniswapBehaviorHistory } from 'uniswap/src/features/behaviorHistor
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { FiatCurrency, ORDERED_CURRENCIES } from 'uniswap/src/features/fiatCurrency/constants' import { FiatCurrency, ORDERED_CURRENCIES } from 'uniswap/src/features/fiatCurrency/constants'
import { getFiatCurrencyName, useAppFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks' 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 { useCurrentLanguageInfo } from 'uniswap/src/features/language/hooks'
import { setCurrentFiatCurrency, setIsTestnetModeEnabled } from 'uniswap/src/features/settings/slice' 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 { WalletEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { ConnectionCardLoggingName } from 'uniswap/src/features/telemetry/types' import { ConnectionCardLoggingName } from 'uniswap/src/features/telemetry/types'
...@@ -67,10 +71,12 @@ export function SettingsScreen(): JSX.Element { ...@@ -67,10 +71,12 @@ export function SettingsScreen(): JSX.Element {
const currentLanguageInfo = useCurrentLanguageInfo() const currentLanguageInfo = useCurrentLanguageInfo()
const appFiatCurrencyInfo = useAppFiatCurrencyInfo() const appFiatCurrencyInfo = useAppFiatCurrencyInfo()
const hasViewedConnectionMigration = useSelector(selectHasViewedConnectionMigration) const hasViewedConnectionMigration = useSelector(selectHasViewedConnectionMigration)
const isSmartWalletEnabled = useFeatureFlag(FeatureFlags.SmartWallet)
const [isLanguageModalOpen, setIsLanguageModalOpen] = useState(false) const [isLanguageModalOpen, setIsLanguageModalOpen] = useState(false)
const [isPortfolioBalanceModalOpen, setIsPortfolioBalanceModalOpen] = useState(false) const [isPortfolioBalanceModalOpen, setIsPortfolioBalanceModalOpen] = useState(false)
const [isTestnetModalOpen, setIsTestnetModalOpen] = useState(false) const [isTestnetModalOpen, setIsTestnetModalOpen] = useState(false)
const [isAdvancedModalOpen, setIsAdvancedModalOpen] = useState(false)
const [isPermissionsModalOpen, setIsPermissionsModalOpen] = useState(false) const [isPermissionsModalOpen, setIsPermissionsModalOpen] = useState(false)
const [isDefaultProvider, setIsDefaultProvider] = useState(true) const [isDefaultProvider, setIsDefaultProvider] = useState(true)
...@@ -106,6 +112,8 @@ export function SettingsScreen(): JSX.Element { ...@@ -106,6 +112,8 @@ export function SettingsScreen(): JSX.Element {
} }
const handleTestnetModalClose = useCallback(() => setIsTestnetModalOpen(false), []) const handleTestnetModalClose = useCallback(() => setIsTestnetModalOpen(false), [])
const handleAdvancedModalClose = useCallback(() => setIsAdvancedModalOpen(false), [])
useEffect(() => { useEffect(() => {
getIsDefaultProviderFromStorage() getIsDefaultProviderFromStorage()
.then((newIsDefaultProvider) => setIsDefaultProvider(newIsDefaultProvider)) .then((newIsDefaultProvider) => setIsDefaultProvider(newIsDefaultProvider))
...@@ -139,6 +147,12 @@ export function SettingsScreen(): JSX.Element { ...@@ -139,6 +147,12 @@ export function SettingsScreen(): JSX.Element {
/> />
) : undefined} ) : undefined}
<TestnetModeModal isOpen={isTestnetModalOpen} onClose={handleTestnetModalClose} /> <TestnetModeModal isOpen={isTestnetModalOpen} onClose={handleTestnetModalClose} />
<SmartWalletAdvancedSettingsModal
isTestnetEnabled={isTestnetModeEnabled}
onTestnetModeToggled={handleTestnetModeToggle}
isOpen={isAdvancedModalOpen}
onClose={handleAdvancedModalClose}
/>
<Flex fill backgroundColor="$surface1" gap="$spacing8"> <Flex fill backgroundColor="$surface1" gap="$spacing8">
<ScreenHeader title={t('settings.title')} /> <ScreenHeader title={t('settings.title')} />
<ScrollView showsVerticalScrollIndicator={false}> <ScrollView showsVerticalScrollIndicator={false}>
...@@ -196,13 +210,20 @@ export function SettingsScreen(): JSX.Element { ...@@ -196,13 +210,20 @@ export function SettingsScreen(): JSX.Element {
title={t('settings.setting.smallBalances.title')} title={t('settings.setting.smallBalances.title')}
onPress={(): void => setIsPortfolioBalanceModalOpen(true)} onPress={(): void => setIsPortfolioBalanceModalOpen(true)}
/> />
{isSmartWalletEnabled ? (
<SettingsToggleRow <SettingsItem
Icon={Wrench} Icon={Sliders}
checked={isTestnetModeEnabled} title={t('settings.setting.advanced.title')}
title={t('settings.setting.wallet.testnetMode.title')} onPress={(): void => setIsAdvancedModalOpen(true)}
onCheckedChange={handleTestnetModeToggle} />
/> ) : (
<SettingsToggleRow
Icon={Wrench}
checked={isTestnetModeEnabled}
title={t('settings.setting.wallet.testnetMode.title')}
onCheckedChange={handleTestnetModeToggle}
/>
)}
</SettingsSection> </SettingsSection>
{!hasViewedConnectionMigration && ( {!hasViewedConnectionMigration && (
<Flex pt="$padding8"> <Flex pt="$padding8">
......
import { PropsWithChildren, useCallback } from 'react' import { PropsWithChildren, useCallback } from 'react'
import { createSearchParams, useNavigate } from 'react-router-dom' import { createSearchParams, useNavigate } from 'react-router-dom'
import { navigateToInterfaceFiatOnRamp } from 'src/app/features/for/utils' 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 { AppRoutes, HomeQueryParams, HomeTabs } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state' import { navigate } from 'src/app/navigation/state'
import { SidebarLocationState, focusOrCreateTokensExploreTab } from 'src/app/navigation/utils' import { SidebarLocationState, focusOrCreateTokensExploreTab } from 'src/app/navigation/utils'
...@@ -12,6 +11,7 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' ...@@ -12,6 +11,7 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { ShareableEntity } from 'uniswap/src/types/sharing' import { ShareableEntity } from 'uniswap/src/types/sharing'
import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { useCopyToClipboard } from 'wallet/src/components/copy/useCopyToClipboard'
import { import {
NavigateToFiatOnRampArgs, NavigateToFiatOnRampArgs,
NavigateToNftItemArgs, NavigateToNftItemArgs,
......
...@@ -133,7 +133,7 @@ export function WebNavigation(): JSX.Element { ...@@ -133,7 +133,7 @@ export function WebNavigation(): JSX.Element {
<NotificationToastWrapper /> <NotificationToastWrapper />
{shouldRestoreScroll && <ScrollRestoration />} {shouldRestoreScroll && <ScrollRestoration />}
{childrenMemo} {childrenMemo}
<ForceUpgradeModal /> {isLoggedIn && <ForceUpgradeModal />}
</WalletUniswapProvider> </WalletUniswapProvider>
</SideBarNavigationProvider> </SideBarNavigationProvider>
) )
......
...@@ -87,16 +87,14 @@ export async function focusOrCreateUnitagTab(address: Address, page: UnitagClaim ...@@ -87,16 +87,14 @@ export async function focusOrCreateUnitagTab(address: Address, page: UnitagClaim
export async function focusOrCreateDappRequestWindow(tabId: number | undefined, windowId: number): Promise<void> { export async function focusOrCreateDappRequestWindow(tabId: number | undefined, windowId: number): Promise<void> {
const extension = await chrome.management.getSelf() 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 tabs = await chrome.tabs.query({ url: `chrome-extension://${extension.id}/popup.html*` })
const tab = tabs[0] const tab = tabs[0]
// Centering within current window
const height = 410 const height = 410
const width = 330 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}` let url = `popup.html?windowId=${windowId}`
if (tabId) { if (tabId) {
url += `&tabId=${tabId}` url += `&tabId=${tabId}`
...@@ -203,3 +201,77 @@ export async function closeCurrentTab(): Promise<void> { ...@@ -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' ...@@ -2,7 +2,7 @@ import { rpcErrors, serializeError } from '@metamask/rpc-errors'
import { removeDappConnection } from 'src/app/features/dapp/actions' import { removeDappConnection } from 'src/app/features/dapp/actions'
import { changeChain } from 'src/app/features/dapp/changeChain' import { changeChain } from 'src/app/features/dapp/changeChain'
import { dappStore } from 'src/app/features/dapp/store' 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 { import {
ChangeChainRequest, ChangeChainRequest,
DappRequest, 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/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { migratePendingDappRequestsToRecord, removeDappInfoToChromeLocalStorage } from 'src/store/extensionMigrations'
import { unchecksumDismissedTokenWarningKeys } from 'uniswap/src/state/uniswapMigrations' import { unchecksumDismissedTokenWarningKeys } from 'uniswap/src/state/uniswapMigrations'
import { import {
activatePendingAccounts, activatePendingAccounts,
...@@ -29,9 +30,7 @@ export const migrations = { ...@@ -29,9 +30,7 @@ export const migrations = {
1: removeUniconV2BehaviorState, 1: removeUniconV2BehaviorState,
2: addRoutingFieldToTransactions, 2: addRoutingFieldToTransactions,
3: activatePendingAccounts, 3: activatePendingAccounts,
4: function removeDappInfoToChromeLocalStorage({ dapp: _dapp, ...state }: any) { 4: removeDappInfoToChromeLocalStorage,
return state
},
5: deleteBetaOnboardingState, 5: deleteBetaOnboardingState,
6: deleteExtensionOnboardingState, 6: deleteExtensionOnboardingState,
7: deleteDefaultFavoritesFromFavoritesState, 7: deleteDefaultFavoritesFromFavoritesState,
...@@ -48,6 +47,7 @@ export const migrations = { ...@@ -48,6 +47,7 @@ export const migrations = {
18: unchecksumDismissedTokenWarningKeys, 18: unchecksumDismissedTokenWarningKeys,
19: deleteWelcomeWalletCardBehaviorHistory, 19: deleteWelcomeWalletCardBehaviorHistory,
20: moveTokenAndNFTVisibility, 20: moveTokenAndNFTVisibility,
21: migratePendingDappRequestsToRecord,
} }
export const EXTENSION_STATE_VERSION = 20 export const EXTENSION_STATE_VERSION = 21
...@@ -205,31 +205,33 @@ const v17SchemaIntermediate = { ...@@ -205,31 +205,33 @@ const v17SchemaIntermediate = {
delete v17SchemaIntermediate.behaviorHistory.createdOnboardingRedesignAccount delete v17SchemaIntermediate.behaviorHistory.createdOnboardingRedesignAccount
export const v17Schema = v17SchemaIntermediate export const v17Schema = v17SchemaIntermediate
const v18SchemaIntermediate = { export const v18Schema = v17Schema
const v19SchemaIntermediate = {
...v17Schema, ...v17Schema,
behaviorHistory: { behaviorHistory: {
...v17Schema.behaviorHistory, ...v17Schema.behaviorHistory,
hasViewedWelcomeWalletCard: undefined, hasViewedWelcomeWalletCard: undefined,
}, },
} }
delete v18SchemaIntermediate.behaviorHistory.hasViewedWelcomeWalletCard delete v19SchemaIntermediate.behaviorHistory.hasViewedWelcomeWalletCard
export const v18Schema = v18SchemaIntermediate export const v19Schema = v19SchemaIntermediate
const v19SchemaIntermediate = { const v20SchemaIntermediate = {
...v18Schema, ...v19Schema,
visibility: { visibility: {
positions: {}, positions: {},
tokens: v18Schema.favorites.tokensVisibility, tokens: v19Schema.favorites.tokensVisibility,
nfts: v18Schema.favorites.nftsVisibility, nfts: v19Schema.favorites.nftsVisibility,
}, },
favorites: { favorites: {
...v18Schema.favorites, ...v19Schema.favorites,
tokensVisibility: undefined, tokensVisibility: undefined,
nftsVisibility: undefined, nftsVisibility: undefined,
}, },
} }
delete v19SchemaIntermediate.favorites.tokensVisibility delete v20SchemaIntermediate.favorites.tokensVisibility
delete v19SchemaIntermediate.favorites.nftsVisibility delete v20SchemaIntermediate.favorites.nftsVisibility
export const v19Schema = v19SchemaIntermediate const v20Schema = v20SchemaIntermediate
export const getSchema = (): typeof v19Schema => v19Schema export const getSchema = (): typeof v20Schema => v20Schema
...@@ -12,7 +12,6 @@ import React, { PropsWithChildren } from 'react' ...@@ -12,7 +12,6 @@ import React, { PropsWithChildren } from 'react'
import { ExtensionState, extensionReducer } from 'src/store/extensionReducer' import { ExtensionState, extensionReducer } from 'src/store/extensionReducer'
import { AppStore } from 'src/store/store' import { AppStore } from 'src/store/store'
import { Resolvers } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' 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 { AutoMockedApolloProvider } from 'uniswap/src/test/mocks'
import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider' import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider'
...@@ -50,9 +49,7 @@ export function renderWithProviders( ...@@ -50,9 +49,7 @@ export function renderWithProviders(
function Wrapper({ children }: PropsWithChildren<unknown>): JSX.Element { function Wrapper({ children }: PropsWithChildren<unknown>): JSX.Element {
return ( return (
<AutoMockedApolloProvider resolvers={resolvers}> <AutoMockedApolloProvider resolvers={resolvers}>
<SharedWalletProvider reduxStore={store}> <SharedWalletProvider reduxStore={store}>{children}</SharedWalletProvider>
<UnitagUpdaterContextProvider>{children}</UnitagUpdaterContextProvider>
</SharedWalletProvider>
</AutoMockedApolloProvider> </AutoMockedApolloProvider>
) )
} }
......
...@@ -72,7 +72,7 @@ class SeedPhraseInputView: UIView { ...@@ -72,7 +72,7 @@ class SeedPhraseInputView: UIView {
set { vc.rootView.viewModel.onPasteEnd = newValue } set { vc.rootView.viewModel.onPasteEnd = newValue }
get { return vc.rootView.viewModel.onPasteEnd } get { return vc.rootView.viewModel.onPasteEnd }
} }
@objc @objc
var onSubmitError: RCTDirectEventBlock { var onSubmitError: RCTDirectEventBlock {
set { vc.rootView.viewModel.onSubmitError = newValue } set { vc.rootView.viewModel.onSubmitError = newValue }
...@@ -138,6 +138,20 @@ struct SeedPhraseTextView: UIViewRepresentable { ...@@ -138,6 +138,20 @@ struct SeedPhraseTextView: UIViewRepresentable {
func textViewDidChange(_ textView: UITextView) { func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text 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 { func makeCoordinator() -> Coordinator {
...@@ -160,7 +174,7 @@ struct SeedPhraseTextView: UIViewRepresentable { ...@@ -160,7 +174,7 @@ struct SeedPhraseTextView: UIViewRepresentable {
uiView.text = text uiView.text = text
if isFocused { if isFocused {
if !uiView.isFirstResponder { if !uiView.isFirstResponder, uiView.window != nil {
uiView.becomeFirstResponder() uiView.becomeFirstResponder()
} }
} else { } else {
......
...@@ -70,7 +70,6 @@ import { syncAppWithDeviceLanguage } from 'uniswap/src/features/settings/slice' ...@@ -70,7 +70,6 @@ import { syncAppWithDeviceLanguage } from 'uniswap/src/features/settings/slice'
import Trace from 'uniswap/src/features/telemetry/Trace' import Trace from 'uniswap/src/features/telemetry/Trace'
import { MobileEventName } from 'uniswap/src/features/telemetry/constants' import { MobileEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context'
import i18n from 'uniswap/src/i18n' import i18n from 'uniswap/src/i18n'
import { CurrencyId } from 'uniswap/src/types/currency' import { CurrencyId } from 'uniswap/src/types/currency'
import { datadogEnabledBuild } from 'utilities/src/environment/constants' import { datadogEnabledBuild } from 'utilities/src/environment/constants'
...@@ -266,22 +265,20 @@ function AppOuter(): JSX.Element | null { ...@@ -266,22 +265,20 @@ function AppOuter(): JSX.Element | null {
<LocalizationContextProvider> <LocalizationContextProvider>
<GestureHandlerRootView style={flexStyles.fill}> <GestureHandlerRootView style={flexStyles.fill}>
<WalletContextProvider> <WalletContextProvider>
<UnitagUpdaterContextProvider> <DataUpdaters />
<DataUpdaters /> <NavigationContainer>
<NavigationContainer> <MobileWalletNavigationProvider>
<MobileWalletNavigationProvider> <WalletUniswapProvider>
<WalletUniswapProvider> <BottomSheetModalProvider>
<BottomSheetModalProvider> <AppModals />
<AppModals /> <PerformanceProfiler onReportPrepared={onReportPrepared}>
<PerformanceProfiler onReportPrepared={onReportPrepared}> <AppInner />
<AppInner /> </PerformanceProfiler>
</PerformanceProfiler> </BottomSheetModalProvider>
</BottomSheetModalProvider> </WalletUniswapProvider>
</WalletUniswapProvider> <NotificationToastWrapper />
<NotificationToastWrapper /> </MobileWalletNavigationProvider>
</MobileWalletNavigationProvider> </NavigationContainer>
</NavigationContainer>
</UnitagUpdaterContextProvider>
</WalletContextProvider> </WalletContextProvider>
</GestureHandlerRootView> </GestureHandlerRootView>
</LocalizationContextProvider> </LocalizationContextProvider>
......
...@@ -84,6 +84,7 @@ import { ...@@ -84,6 +84,7 @@ import {
v7Schema, v7Schema,
v80Schema, v80Schema,
v81Schema, v81Schema,
v82Schema,
v83Schema, v83Schema,
v84Schema, v84Schema,
v8Schema, v8Schema,
...@@ -1609,8 +1610,8 @@ describe('Redux state migrations', () => { ...@@ -1609,8 +1610,8 @@ describe('Redux state migrations', () => {
it('migrates from v82 to v83', () => { it('migrates from v82 to v83', () => {
// v82 didn't have a new schema // v82 didn't have a new schema
const v81Stub = { ...v81Schema } const v82Stub = { ...v82Schema }
const v83 = migrations[83](v81Stub) const v83 = migrations[83](v82Stub)
expect(v83.pushNotifications.generalUpdatesEnabled).toBe(false) expect(v83.pushNotifications.generalUpdatesEnabled).toBe(false)
expect(v83.pushNotifications.priceAlertsEnabled).toBe(false) expect(v83.pushNotifications.priceAlertsEnabled).toBe(false)
......
...@@ -26,6 +26,7 @@ import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' ...@@ -26,6 +26,7 @@ import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { PlusCircle } from 'wallet/src/components/icons/PlusCircle' import { PlusCircle } from 'wallet/src/components/icons/PlusCircle'
import { createOnboardingAccount } from 'wallet/src/features/onboarding/createOnboardingAccount' import { createOnboardingAccount } from 'wallet/src/features/onboarding/createOnboardingAccount'
import { BackupType } from 'wallet/src/features/wallet/accounts/types' 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 { createAccountsActions } from 'wallet/src/features/wallet/create/createAccountsSaga'
import { useActiveAccountAddress, useNativeAccountExists } from 'wallet/src/features/wallet/hooks' import { useActiveAccountAddress, useNativeAccountExists } from 'wallet/src/features/wallet/hooks'
import { selectAllAccountsSorted, selectSortedSignerMnemonicAccounts } from 'wallet/src/features/wallet/selectors' import { selectAllAccountsSorted, selectSortedSignerMnemonicAccounts } from 'wallet/src/features/wallet/selectors'
...@@ -109,7 +110,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme ...@@ -109,7 +110,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
wallet_type: ImportType.CreateAdditional, wallet_type: ImportType.CreateAdditional,
accounts_imported_count: 1, accounts_imported_count: 1,
wallets_imported: [newAccount.address], wallets_imported: [newAccount.address],
cloud_backup_used: newAccount.backups?.includes(BackupType.Cloud) ?? false, cloud_backup_used: hasBackup(BackupType.Cloud, newAccount),
modal: ModalName.AccountSwitcher, modal: ModalName.AccountSwitcher,
}) })
} }
......
...@@ -36,8 +36,10 @@ import { SettingsBiometricModal } from 'src/components/Settings/SettingsBiometri ...@@ -36,8 +36,10 @@ import { SettingsBiometricModal } from 'src/components/Settings/SettingsBiometri
import { BuyNativeTokenModal } from 'src/components/TokenDetails/BuyNativeTokenModal' import { BuyNativeTokenModal } from 'src/components/TokenDetails/BuyNativeTokenModal'
import { FundWalletModal } from 'src/components/home/introCards/FundWalletModal' import { FundWalletModal } from 'src/components/home/introCards/FundWalletModal'
import { HorizontalEdgeGestureTarget } from 'src/components/layout/screens/EdgeGestureTarget' 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 { HiddenTokenInfoModalScreen } from 'src/components/modals/ReactNavigationModals/HiddenTokenInfoModalScreen'
import { PasskeyHelpModalScreen } from 'src/components/modals/ReactNavigationModals/PasskeyHelpModalScreen' 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 { TestnetModeModalScreen } from 'src/components/modals/ReactNavigationModals/TestnetModeModalScreen'
import { UnitagsIntroModal } from 'src/components/unitags/UnitagsIntroModal' import { UnitagsIntroModal } from 'src/components/unitags/UnitagsIntroModal'
import { ExchangeTransferModal } from 'src/features/fiatOnRamp/ExchangeTransferModal' import { ExchangeTransferModal } from 'src/features/fiatOnRamp/ExchangeTransferModal'
...@@ -383,6 +385,7 @@ export function AppStackNavigator(): JSX.Element { ...@@ -383,6 +385,7 @@ export function AppStackNavigator(): JSX.Element {
<AppStack.Screen component={BuyNativeTokenModal} name={ModalName.BuyNativeToken} /> <AppStack.Screen component={BuyNativeTokenModal} name={ModalName.BuyNativeToken} />
<AppStack.Screen component={HiddenTokenInfoModalScreen} name={ModalName.HiddenTokenInfoModal} /> <AppStack.Screen component={HiddenTokenInfoModalScreen} name={ModalName.HiddenTokenInfoModal} />
<AppStack.Screen component={ScreenshotWarningModal} name={ModalName.ScreenshotWarning} /> <AppStack.Screen component={ScreenshotWarningModal} name={ModalName.ScreenshotWarning} />
<AppStack.Screen component={PasskeyManagementModalScreen} name={ModalName.PasskeyManagement} />
<AppStack.Screen component={PasskeyHelpModalScreen} name={ModalName.PasskeysHelp} /> <AppStack.Screen component={PasskeyHelpModalScreen} name={ModalName.PasskeysHelp} />
<AppStack.Screen component={SettingsBiometricModal} name={ModalName.BiometricsModal} /> <AppStack.Screen component={SettingsBiometricModal} name={ModalName.BiometricsModal} />
<AppStack.Screen component={SettingsFiatCurrencyModal} name={ModalName.FiatCurrencySelector} /> <AppStack.Screen component={SettingsFiatCurrencyModal} name={ModalName.FiatCurrencySelector} />
...@@ -390,6 +393,8 @@ export function AppStackNavigator(): JSX.Element { ...@@ -390,6 +393,8 @@ export function AppStackNavigator(): JSX.Element {
<AppStack.Screen component={EditLabelSettingsModal} name={ModalName.EditLabelSettingsModal} /> <AppStack.Screen component={EditLabelSettingsModal} name={ModalName.EditLabelSettingsModal} />
<AppStack.Screen component={EditProfileSettingsModal} name={ModalName.EditProfileSettingsModal} /> <AppStack.Screen component={EditProfileSettingsModal} name={ModalName.EditProfileSettingsModal} />
<AppStack.Screen component={ConnectionsDappListModal} name={ModalName.ConnectionsDappListModal} /> <AppStack.Screen component={ConnectionsDappListModal} name={ModalName.ConnectionsDappListModal} />
<AppStack.Screen component={AdvancedSettingsModal} name={ModalName.SmartWalletAdvancedSettingsModal} />
{enabledInEnvOrDev && {enabledInEnvOrDev &&
((): JSX.Element => { ((): JSX.Element => {
return <AppStack.Screen component={ExperimentsModal} name={ModalName.Experiments} /> return <AppStack.Screen component={ExperimentsModal} name={ModalName.Experiments} />
......
...@@ -17,6 +17,8 @@ import { TestnetSwitchModalState } from 'src/features/testnetMode/TestnetSwitchM ...@@ -17,6 +17,8 @@ import { TestnetSwitchModalState } from 'src/features/testnetMode/TestnetSwitchM
import { HomeScreenTabIndex } from 'src/screens/HomeScreen/HomeScreenTabIndex' import { HomeScreenTabIndex } from 'src/screens/HomeScreen/HomeScreenTabIndex'
import { ReceiveCryptoModalState } from 'src/screens/ReceiveCryptoModalState' import { ReceiveCryptoModalState } from 'src/screens/ReceiveCryptoModalState'
import { FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types' 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 { ModalName } from 'uniswap/src/features/telemetry/constants'
import { TestnetModeModalState } from 'uniswap/src/features/testnets/TestnetModeModal' import { TestnetModeModalState } from 'uniswap/src/features/testnets/TestnetModeModal'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
...@@ -172,6 +174,7 @@ export type AppStackParamList = { ...@@ -172,6 +174,7 @@ export type AppStackParamList = {
[ModalName.BuyNativeToken]: BuyNativeTokenModalState [ModalName.BuyNativeToken]: BuyNativeTokenModalState
[ModalName.HiddenTokenInfoModal]: undefined [ModalName.HiddenTokenInfoModal]: undefined
[ModalName.ScreenshotWarning]: { acknowledgeText?: string } | undefined [ModalName.ScreenshotWarning]: { acknowledgeText?: string } | undefined
[ModalName.PasskeyManagement]: PasskeyManagementModalState
[ModalName.PasskeysHelp]: undefined [ModalName.PasskeysHelp]: undefined
[ModalName.BiometricsModal]: undefined [ModalName.BiometricsModal]: undefined
[ModalName.FiatCurrencySelector]: undefined [ModalName.FiatCurrencySelector]: undefined
...@@ -179,6 +182,7 @@ export type AppStackParamList = { ...@@ -179,6 +182,7 @@ export type AppStackParamList = {
[ModalName.EditLabelSettingsModal]: EditWalletSettingsModalState [ModalName.EditLabelSettingsModal]: EditWalletSettingsModalState
[ModalName.EditProfileSettingsModal]: EditWalletSettingsModalState [ModalName.EditProfileSettingsModal]: EditWalletSettingsModalState
[ModalName.ConnectionsDappListModal]: ConnectionsDappsListModalState [ModalName.ConnectionsDappListModal]: ConnectionsDappsListModalState
[ModalName.SmartWalletAdvancedSettingsModal]: SmartWalletAdvancedSettingsModalState
} }
export type AppStackNavigationProp = NativeStackNavigationProp<AppStackParamList> export type AppStackNavigationProp = NativeStackNavigationProp<AppStackParamList>
......
...@@ -637,7 +637,8 @@ const v81SchemaIntermediate = { ...@@ -637,7 +637,8 @@ const v81SchemaIntermediate = {
delete v81SchemaIntermediate.behaviorHistory.createdOnboardingRedesignAccount delete v81SchemaIntermediate.behaviorHistory.createdOnboardingRedesignAccount
export const v81Schema = v81SchemaIntermediate export const v81Schema = v81SchemaIntermediate
// v82 had a migration but no schema update so skipping it here export const v82Schema = v81Schema
export const v83Schema = { export const v83Schema = {
...v81Schema, ...v81Schema,
pushNotifications: { pushNotifications: {
......
...@@ -9,7 +9,7 @@ import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' ...@@ -9,7 +9,7 @@ import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { UniverseChainId } from 'uniswap/src/features/chains/types' import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { TestID } from 'uniswap/src/test/fixtures/testIDs' 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 { RecipientList } from 'wallet/src/components/RecipientSearch/RecipientList'
import { RecipientSelectSpeedBumps } from 'wallet/src/components/RecipientSearch/RecipientSelectSpeedBumps' import { RecipientSelectSpeedBumps } from 'wallet/src/components/RecipientSearch/RecipientSelectSpeedBumps'
import { useFilteredRecipientSections } from 'wallet/src/components/RecipientSearch/hooks' import { useFilteredRecipientSections } from 'wallet/src/components/RecipientSearch/hooks'
......
...@@ -2,7 +2,7 @@ import React from 'react' ...@@ -2,7 +2,7 @@ import React from 'react'
import { DappHeaderIcon } from 'src/components/Requests/DappHeaderIcon' import { DappHeaderIcon } from 'src/components/Requests/DappHeaderIcon'
import { HeaderText } from 'src/components/Requests/RequestModal/HeaderText' import { HeaderText } from 'src/components/Requests/RequestModal/HeaderText'
import { LinkButton } from 'src/components/buttons/LinkButton' 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 { Flex, useSporeColors } from 'ui/src'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { useCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo' import { useCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo'
...@@ -17,7 +17,7 @@ export function ClientDetails({ ...@@ -17,7 +17,7 @@ export function ClientDetails({
request, request,
permitInfo, permitInfo,
}: { }: {
request: WalletConnectRequest request: WalletConnectSigningRequest
permitInfo?: PermitInfo permitInfo?: PermitInfo
}): JSX.Element { }): JSX.Element {
const { dapp } = request const { dapp } = request
......
import { Currency } from '@uniswap/sdk-core' import { Currency } from '@uniswap/sdk-core'
import React from 'react' import React from 'react'
import { Trans } from 'react-i18next' 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 { Text } from 'ui/src'
import { EthMethod } from 'uniswap/src/features/dappRequests/types' import { EthMethod } from 'uniswap/src/features/dappRequests/types'
import { ValueType, getCurrencyAmount } from 'uniswap/src/features/tokens/getCurrencyAmount' import { ValueType, getCurrencyAmount } from 'uniswap/src/features/tokens/getCurrencyAmount'
...@@ -12,7 +12,7 @@ export function HeaderText({ ...@@ -12,7 +12,7 @@ export function HeaderText({
permitAmount, permitAmount,
permitCurrency, permitCurrency,
}: { }: {
request: WalletConnectRequest request: WalletConnectSigningRequest
permitAmount?: number permitAmount?: number
permitCurrency?: Currency | null permitCurrency?: Currency | null
}): JSX.Element { }): JSX.Element {
......
import { BigNumber } from '@ethersproject/bignumber' import { BigNumber } from '@ethersproject/bignumber'
import React, { PropsWithChildren } from 'react' import React, { PropsWithChildren } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyleProp, ViewStyle } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler' import { ScrollView } from 'react-native-gesture-handler'
import { PermitInfo } from 'src/components/Requests/RequestModal/ClientDetails'
import { LinkButton } from 'src/components/buttons/LinkButton' 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 { Flex, Text, useSporeColors } from 'ui/src'
import { TextVariantTokens, iconSizes } from 'ui/src/theme' import { TextVariantTokens, iconSizes } from 'ui/src/theme'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
...@@ -16,6 +24,7 @@ import { getValidAddress } from 'uniswap/src/utils/addresses' ...@@ -16,6 +24,7 @@ import { getValidAddress } from 'uniswap/src/utils/addresses'
import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking'
import { shortenAddress } from 'utilities/src/addresses' import { shortenAddress } from 'utilities/src/addresses'
import { logger } from 'utilities/src/logger/logger' 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 { ContentRow } from 'wallet/src/features/transactions/TransactionRequest/ContentRow'
import { import {
SpendingDetails, SpendingDetails,
...@@ -24,7 +33,9 @@ import { ...@@ -24,7 +33,9 @@ import {
import { useNoYoloParser } from 'wallet/src/utils/useNoYoloParser' import { useNoYoloParser } from 'wallet/src/utils/useNoYoloParser'
import { useTransactionCurrencies } from 'wallet/src/utils/useTransactionCurrencies' 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) { if (request.type === EthMethod.PersonalSign || request.type === EthMethod.EthSign) {
return request.message || request.rawMessage return request.message || request.rawMessage
} }
...@@ -159,10 +170,11 @@ function TransactionDetails({ ...@@ -159,10 +170,11 @@ function TransactionDetails({
} }
type Props = { 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 return request.type === EthMethod.SignTypedData || request.type === EthMethod.SignTypedDataV4
} }
...@@ -191,10 +203,50 @@ export function RequestDetailsContent({ request }: Props): JSX.Element { ...@@ -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 ( return (
<ScrollView> <ExpandoRow
<RequestDetailsContent request={request} /> label={t('walletConnect.request.bundledTransactions.label', { count: calls.length })}
</ScrollView> // 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 ...@@ -9,7 +9,7 @@ import { KidSuperCheckinModal } from 'src/components/Requests/RequestModal/KidSu
import { UwULinkErc20SendModal } from 'src/components/Requests/RequestModal/UwULinkErc20SendModal' import { UwULinkErc20SendModal } from 'src/components/Requests/RequestModal/UwULinkErc20SendModal'
import { import {
WalletConnectRequestModalContent, WalletConnectRequestModalContent,
methodCostsGas, getDoesMethodCostGas,
} from 'src/components/Requests/RequestModal/WalletConnectRequestModalContent' } from 'src/components/Requests/RequestModal/WalletConnectRequestModalContent'
import { useHasSufficientFunds } from 'src/components/Requests/RequestModal/hooks' import { useHasSufficientFunds } from 'src/components/Requests/RequestModal/hooks'
import { useBiometricAppSettings } from 'src/features/biometrics/useBiometricAppSettings' import { useBiometricAppSettings } from 'src/features/biometrics/useBiometricAppSettings'
...@@ -19,7 +19,8 @@ import { wcWeb3Wallet } from 'src/features/walletConnect/saga' ...@@ -19,7 +19,8 @@ import { wcWeb3Wallet } from 'src/features/walletConnect/saga'
import { selectDidOpenFromDeepLink } from 'src/features/walletConnect/selectors' import { selectDidOpenFromDeepLink } from 'src/features/walletConnect/selectors'
import { signWcRequestActions } from 'src/features/walletConnect/signWcRequestSaga' import { signWcRequestActions } from 'src/features/walletConnect/signWcRequestSaga'
import { import {
WalletConnectRequest, WalletConnectSigningRequest,
isBatchedTransactionRequest,
isTransactionRequest, isTransactionRequest,
setDidOpenFromDeepLink, setDidOpenFromDeepLink,
} from 'src/features/walletConnect/walletConnectSlice' } from 'src/features/walletConnect/walletConnectSlice'
...@@ -36,7 +37,7 @@ import { useSignerAccounts } from 'wallet/src/features/wallet/hooks' ...@@ -36,7 +37,7 @@ import { useSignerAccounts } from 'wallet/src/features/wallet/hooks'
interface Props { interface Props {
onClose: () => void onClose: () => void
request: WalletConnectRequest request: WalletConnectSigningRequest
} }
const VALID_REQUEST_TYPES = [ const VALID_REQUEST_TYPES = [
...@@ -46,6 +47,7 @@ const VALID_REQUEST_TYPES = [ ...@@ -46,6 +47,7 @@ const VALID_REQUEST_TYPES = [
EthMethod.EthSign, EthMethod.EthSign,
EthMethod.EthSendTransaction, EthMethod.EthSendTransaction,
UwULinkMethod.Erc20Send, UwULinkMethod.Erc20Send,
EthMethod.SendCalls,
] ]
export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Element | null { export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Element | null {
...@@ -55,11 +57,13 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem ...@@ -55,11 +57,13 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
const chainId = request.chainId const chainId = request.chainId
const tx: providers.TransactionRequest | undefined = useMemo(() => { const tx: providers.TransactionRequest | undefined = useMemo(() => {
if (!isTransactionRequest(request)) { if (isTransactionRequest(request)) {
return undefined return { ...request.transaction, chainId }
} }
if (isBatchedTransactionRequest(request)) {
return { ...request.transaction, chainId } return { ...request.encodedTransaction, chainId }
}
return undefined
}, [chainId, request]) }, [chainId, request])
const signerAccounts = useSignerAccounts() const signerAccounts = useSignerAccounts()
...@@ -70,7 +74,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem ...@@ -70,7 +74,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
account: request.account, account: request.account,
chainId, chainId,
gasFee, gasFee,
value: isTransactionRequest(request) ? request.transaction.value : undefined, value: tx?.value?.toString(),
}) })
const { isBlocked: isSenderBlocked, isBlockedLoading: isSenderBlockedLoading } = useIsBlockedActiveAddress() const { isBlocked: isSenderBlocked, isBlockedLoading: isSenderBlockedLoading } = useIsBlockedActiveAddress()
...@@ -92,7 +96,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem ...@@ -92,7 +96,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
return false return false
} }
if (methodCostsGas(request)) { if (getDoesMethodCostGas(request)) {
return !!(tx && hasSufficientFunds && gasFee.value && !gasFee.error && !gasFee.isLoading) return !!(tx && hasSufficientFunds && gasFee.value && !gasFee.error && !gasFee.isLoading)
} }
...@@ -147,7 +151,11 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem ...@@ -147,7 +151,11 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
return 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) { if (!tx) {
return return
} }
...@@ -160,7 +168,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem ...@@ -160,7 +168,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
signWcRequestActions.trigger({ signWcRequestActions.trigger({
sessionId: request.sessionId, sessionId: request.sessionId,
requestInternalId: request.internalId, requestInternalId: request.internalId,
method: EthMethod.EthSendTransaction, method: request.type === EthMethod.SendCalls ? EthMethod.SendCalls : EthMethod.EthSendTransaction,
transaction: txnWithFormattedGasEstimates, transaction: txnWithFormattedGasEstimates,
account: signerAccount, account: signerAccount,
dapp: request.dapp, dapp: request.dapp,
...@@ -168,8 +176,6 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem ...@@ -168,8 +176,6 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
request, request,
}), }),
) )
} else if (request.type === EthMethod.SendCalls) {
// TODO: Implement
} else { } else {
dispatch( dispatch(
signWcRequestActions.trigger({ signWcRequestActions.trigger({
...@@ -248,15 +254,12 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem ...@@ -248,15 +254,12 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
) )
} }
if (request.type === EthMethod.SendCalls) {
// TODO: Implement
return null
}
return ( return (
<ModalWithOverlay <ModalWithOverlay
confirmationButtonText={ 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} disableConfirm={!confirmEnabled}
name={ModalName.WCSignRequest} name={ModalName.WCSignRequest}
......
import { useBottomSheetInternal } from '@gorhom/bottom-sheet' import { useBottomSheetInternal } from '@gorhom/bottom-sheet'
import { useNetInfo } from '@react-native-community/netinfo' import { useNetInfo } from '@react-native-community/netinfo'
import React, { PropsWithChildren } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyleProp, ViewStyle } from 'react-native'
import Animated, { useAnimatedStyle } from 'react-native-reanimated' import Animated, { useAnimatedStyle } from 'react-native-reanimated'
import { ClientDetails, PermitInfo } from 'src/components/Requests/RequestModal/ClientDetails' 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 { import {
SignRequest, SignRequest,
TransactionRequest, TransactionRequest,
WalletConnectRequest, WalletConnectSigningRequest,
WalletSendCallsEncodedRequest,
isTransactionRequest, isTransactionRequest,
} from 'src/features/walletConnect/walletConnectSlice' } from 'src/features/walletConnect/walletConnectSlice'
import { Flex, Text, useSporeColors } from 'ui/src' import { Flex, Text, useSporeColors } from 'ui/src'
...@@ -26,15 +25,13 @@ import { logger } from 'utilities/src/logger/logger' ...@@ -26,15 +25,13 @@ import { logger } from 'utilities/src/logger/logger'
import { AddressFooter } from 'wallet/src/features/transactions/TransactionRequest/AddressFooter' import { AddressFooter } from 'wallet/src/features/transactions/TransactionRequest/AddressFooter'
import { NetworkFeeFooter } from 'wallet/src/features/transactions/TransactionRequest/NetworkFeeFooter' 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 getDoesMethodCostGas = (request: WalletConnectSigningRequest): boolean =>
request.type === EthMethod.EthSendTransaction || request.type === EthMethod.SendCalls
export const methodCostsGas = (request: WalletConnectRequest): request is TransactionRequest =>
request.type === EthMethod.EthSendTransaction
/** If the request is a permit then parse the relevant information otherwise return undefined. */ /** 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) { if (request.type !== EthMethod.SignTypedDataV4) {
return undefined return undefined
} }
...@@ -61,7 +58,7 @@ const getPermitInfo = (request: WalletConnectRequest): PermitInfo | undefined => ...@@ -61,7 +58,7 @@ const getPermitInfo = (request: WalletConnectRequest): PermitInfo | undefined =>
type WalletConnectRequestModalContentProps = { type WalletConnectRequestModalContentProps = {
gasFee: GasFeeResult gasFee: GasFeeResult
hasSufficientFunds: boolean hasSufficientFunds: boolean
request: SignRequest | TransactionRequest request: SignRequest | TransactionRequest | WalletSendCallsEncodedRequest
isBlocked: boolean isBlocked: boolean
} }
...@@ -85,19 +82,13 @@ export function WalletConnectRequestModalContent({ ...@@ -85,19 +82,13 @@ export function WalletConnectRequestModalContent({
height: animatedFooterHeight.value, height: animatedFooterHeight.value,
})) }))
const hasGasFee = methodCostsGas(request) const hasGasFee = getDoesMethodCostGas(request)
return ( return (
<> <>
<ClientDetails permitInfo={permitInfo} request={request} /> <ClientDetails permitInfo={permitInfo} request={request} />
<Flex pt="$spacing8"> <Flex pt="$spacing8">
<Flex backgroundColor="$surface2" borderColor="$surface3" borderRadius="$rounded16" borderWidth="$spacing1"> <RequestDetails request={request} permitInfo={permitInfo} />
{!permitInfo && (
<SectionContainer style={requestMessageStyle}>
<RequestDetails request={request} />
</SectionContainer>
)}
</Flex>
<Flex gap="$spacing8" mb="$spacing12" pt="$spacing20" px="$spacing4"> <Flex gap="$spacing8" mb="$spacing12" pt="$spacing20" px="$spacing4">
<NetworkFeeFooter <NetworkFeeFooter
chainId={chainId} chainId={chainId}
...@@ -153,23 +144,12 @@ export function WalletConnectRequestModalContent({ ...@@ -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({ function WarningSection({
request, request,
showUnsafeWarning, showUnsafeWarning,
isBlockedAddress, isBlockedAddress,
}: { }: {
request: WalletConnectRequest request: WalletConnectSigningRequest
showUnsafeWarning: boolean showUnsafeWarning: boolean
isBlockedAddress: boolean isBlockedAddress: boolean
}): JSX.Element | null { }): JSX.Element | null {
...@@ -197,9 +177,3 @@ function WarningSection({ ...@@ -197,9 +177,3 @@ function WarningSection({
return null 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 { 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 { AssetType } from 'uniswap/src/entities/assets'
import { EthMethod } from 'uniswap/src/features/dappRequests/types' import { EthMethod } from 'uniswap/src/features/dappRequests/types'
import { import {
...@@ -114,7 +114,7 @@ export async function getFormattedUwuLinkTxnRequest({ ...@@ -114,7 +114,7 @@ export async function getFormattedUwuLinkTxnRequest({
allowList, allowList,
providerManager, providerManager,
contractManager, contractManager,
}: HandleUwuLinkRequestParams): Promise<{ request: WalletConnectRequest; account: string }> { }: HandleUwuLinkRequestParams): Promise<{ request: WalletConnectSigningRequest; account: string }> {
const newRequest = { const newRequest = {
sessionId: UWULINK_PREFIX, // session/internalId is WalletConnect specific, but not needed here sessionId: UWULINK_PREFIX, // session/internalId is WalletConnect specific, but not needed here
internalId: UWULINK_PREFIX, internalId: UWULINK_PREFIX,
......
...@@ -7,7 +7,7 @@ import { WalletConnectModal } from 'src/components/Requests/ScanSheet/WalletConn ...@@ -7,7 +7,7 @@ import { WalletConnectModal } from 'src/components/Requests/ScanSheet/WalletConn
import { closeModal } from 'src/features/modals/modalSlice' import { closeModal } from 'src/features/modals/modalSlice'
import { useWalletConnect } from 'src/features/walletConnect/useWalletConnect' import { useWalletConnect } from 'src/features/walletConnect/useWalletConnect'
import { import {
WalletConnectRequest, WalletConnectSigningRequest,
removePendingSession, removePendingSession,
removeRequest, removeRequest,
setDidOpenFromDeepLink, setDidOpenFromDeepLink,
...@@ -105,7 +105,7 @@ export function WalletConnectModals(): JSX.Element { ...@@ -105,7 +105,7 @@ export function WalletConnectModals(): JSX.Element {
} }
type RequestModalProps = { type RequestModalProps = {
currRequest: WalletConnectRequest currRequest: WalletConnectSigningRequest
} }
function RequestModal({ currRequest }: RequestModalProps): JSX.Element { function RequestModal({ currRequest }: RequestModalProps): JSX.Element {
......
...@@ -15,7 +15,7 @@ import { ModalName } from 'uniswap/src/features/telemetry/constants' ...@@ -15,7 +15,7 @@ import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { sanitizeAddressText } from 'uniswap/src/utils/addresses' import { sanitizeAddressText } from 'uniswap/src/utils/addresses'
import { shortenAddress } from 'utilities/src/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 { isIOS } from 'utilities/src/platform'
import { NICKNAME_MAX_LENGTH } from 'wallet/src/constants/accounts' import { NICKNAME_MAX_LENGTH } from 'wallet/src/constants/accounts'
import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga' import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga'
......
...@@ -15,7 +15,7 @@ import { useBottomSheetSafeKeyboard } from 'uniswap/src/components/modals/useBot ...@@ -15,7 +15,7 @@ import { useBottomSheetSafeKeyboard } from 'uniswap/src/components/modals/useBot
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks' import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks'
import { MobileScreens } from 'uniswap/src/types/screens/mobile' 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 { isIOS } from 'utilities/src/platform'
import { ChangeUnitagModal } from 'wallet/src/features/unitags/ChangeUnitagModal' import { ChangeUnitagModal } from 'wallet/src/features/unitags/ChangeUnitagModal'
import { DeleteUnitagModal } from 'wallet/src/features/unitags/DeleteUnitagModal' import { DeleteUnitagModal } from 'wallet/src/features/unitags/DeleteUnitagModal'
......
...@@ -10,12 +10,15 @@ import { ...@@ -10,12 +10,15 @@ import {
SettingsStackNavigationProp, SettingsStackNavigationProp,
SettingsStackParamList, SettingsStackParamList,
} from 'src/app/navigation/types' } 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 { openModal } from 'src/features/modals/modalSlice'
import { useIsScreenNavigationReady } from 'src/utils/useIsScreenNavigationReady' import { useIsScreenNavigationReady } from 'src/utils/useIsScreenNavigationReady'
import { Flex, Skeleton, Switch, Text, TouchableArea, useSporeColors } from 'ui/src' import { Flex, Skeleton, Switch, Text, TouchableArea, useSporeColors } from 'ui/src'
import { Arrow } from 'ui/src/components/arrow/Arrow' import { Arrow } from 'ui/src/components/arrow/Arrow'
import { RotatableChevron } from 'ui/src/components/icons' import { RotatableChevron } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { SmartWalletAdvancedSettingsModalState } from 'uniswap/src/features/smartWallet/modals/SmartWalletAdvancedSettingsModal'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { openUri } from 'uniswap/src/utils/linking' import { openUri } from 'uniswap/src/utils/linking'
...@@ -45,12 +48,18 @@ type SettingsNavigationModal = ...@@ -45,12 +48,18 @@ type SettingsNavigationModal =
| typeof ModalName.EditProfileSettingsModal | typeof ModalName.EditProfileSettingsModal
| typeof ModalName.EditLabelSettingsModal | typeof ModalName.EditLabelSettingsModal
| typeof ModalName.ConnectionsDappListModal | typeof ModalName.ConnectionsDappListModal
| typeof ModalName.SmartWalletAdvancedSettingsModal
| typeof ModalName.PasskeyManagement
export interface SettingsSectionItem { export interface SettingsSectionItem {
screen?: keyof SettingsStackParamList | typeof MobileScreens.OnboardingStack screen?: keyof SettingsStackParamList | typeof MobileScreens.OnboardingStack
modal?: SettingsModal modal?: SettingsModal
navigationModal?: SettingsNavigationModal navigationModal?: SettingsNavigationModal
screenProps?: ValueOf<SettingsStackParamList> | NavigatorScreenParams<OnboardingStackParamList> screenProps?: ValueOf<SettingsStackParamList> | NavigatorScreenParams<OnboardingStackParamList>
navigationProps?:
| ConnectionsDappsListModalState
| EditWalletSettingsModalState
| SmartWalletAdvancedSettingsModalState
externalLink?: string externalLink?: string
action?: JSX.Element action?: JSX.Element
disabled?: boolean disabled?: boolean
...@@ -78,6 +87,7 @@ export const SettingsRow = memo( ...@@ -78,6 +87,7 @@ export const SettingsRow = memo(
modal, modal,
navigationModal, navigationModal,
screenProps, screenProps,
navigationProps,
externalLink, externalLink,
disabled, disabled,
action, action,
...@@ -107,11 +117,22 @@ export const SettingsRow = memo( ...@@ -107,11 +117,22 @@ export const SettingsRow = memo(
} else if (modal) { } else if (modal) {
dispatch(openModal({ name: modal })) dispatch(openModal({ name: modal }))
} else if (navigationModal) { } else if (navigationModal) {
navigate(navigationModal) navigate(navigationModal, navigationProps)
} else if (externalLink) { } else if (externalLink) {
await openUri(externalLink) await openUri(externalLink)
} }
}, [checkIfCanProceed, onToggle, screen, navigation, screenProps, modal, navigationModal, dispatch, externalLink]) }, [
checkIfCanProceed,
onToggle,
screen,
navigation,
screenProps,
navigationProps,
modal,
navigationModal,
dispatch,
externalLink,
])
return ( return (
<TouchableArea disabled={Boolean(action)} onPress={handleRow}> <TouchableArea disabled={Boolean(action)} onPress={handleRow}>
......
...@@ -13,8 +13,8 @@ import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' ...@@ -13,8 +13,8 @@ import { getChainInfo } from 'uniswap/src/features/chains/chainInfo'
import { UniverseChainId } from 'uniswap/src/features/chains/types' import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { useCurrencyInfo, useNativeCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo' import { useCurrencyInfo, useNativeCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo'
import { BridgeTokenButton } from 'uniswap/src/features/transactions/InsufficientNativeTokenWarning/BridgeTokenButton' import { BridgeTokenButton } from 'uniswap/src/features/transactions/components/InsufficientNativeTokenWarning/BridgeTokenButton'
import { BuyNativeTokenButton } from 'uniswap/src/features/transactions/InsufficientNativeTokenWarning/BuyNativeTokenButton' import { BuyNativeTokenButton } from 'uniswap/src/features/transactions/components/InsufficientNativeTokenWarning/BuyNativeTokenButton'
import { currencyIdToAddress } from 'uniswap/src/utils/currencyId' import { currencyIdToAddress } from 'uniswap/src/utils/currencyId'
import { useActiveAccountAddress } from 'wallet/src/features/wallet/hooks' import { useActiveAccountAddress } from 'wallet/src/features/wallet/hooks'
......
...@@ -23,6 +23,7 @@ import { useGatingUserPropertyUsernames } from 'wallet/src/features/gating/userP ...@@ -23,6 +23,7 @@ import { useGatingUserPropertyUsernames } from 'wallet/src/features/gating/userP
import { selectAllowAnalytics } from 'wallet/src/features/telemetry/selectors' import { selectAllowAnalytics } from 'wallet/src/features/telemetry/selectors'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
import { BackupType } from 'wallet/src/features/wallet/accounts/types' import { BackupType } from 'wallet/src/features/wallet/accounts/types'
import { hasBackup } from 'wallet/src/features/wallet/accounts/utils'
import { import {
useActiveAccount, useActiveAccount,
useSignerAccounts, useSignerAccounts,
...@@ -116,7 +117,7 @@ export function TraceUserProperties(): null { ...@@ -116,7 +117,7 @@ export function TraceUserProperties(): null {
} }
setUserProperty(MobileUserPropertyName.ActiveWalletAddress, activeAccount.address) setUserProperty(MobileUserPropertyName.ActiveWalletAddress, activeAccount.address)
setUserProperty(MobileUserPropertyName.ActiveWalletType, activeAccount.type) 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.IsPushEnabled, Boolean(activeAccount.pushNotificationsEnabled))
setUserProperty(MobileUserPropertyName.IsHideSmallBalancesEnabled, hideSmallBalances) setUserProperty(MobileUserPropertyName.IsHideSmallBalancesEnabled, hideSmallBalances)
......
...@@ -8,6 +8,7 @@ import { openModal } from 'src/features/modals/modalSlice' ...@@ -8,6 +8,7 @@ import { openModal } from 'src/features/modals/modalSlice'
import { removePendingSession } from 'src/features/walletConnect/walletConnectSlice' import { removePendingSession } from 'src/features/walletConnect/walletConnectSlice'
import { Flex, Text, TouchableArea } from 'ui/src' import { Flex, Text, TouchableArea } from 'ui/src'
import { CopyAlt, ScanHome, SettingsHome } from 'ui/src/components/icons' 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 { AccountType } from 'uniswap/src/features/accounts/types'
import { useAvatar } from 'uniswap/src/features/address/avatar' import { useAvatar } from 'uniswap/src/features/address/avatar'
import { pushNotification } from 'uniswap/src/features/notifications/slice' import { pushNotification } from 'uniswap/src/features/notifications/slice'
...@@ -22,7 +23,6 @@ import { setClipboard } from 'uniswap/src/utils/clipboard' ...@@ -22,7 +23,6 @@ import { setClipboard } from 'uniswap/src/utils/clipboard'
import { shortenAddress } from 'utilities/src/addresses' import { shortenAddress } from 'utilities/src/addresses'
import { isDevEnv } from 'utilities/src/environment/env' import { isDevEnv } from 'utilities/src/environment/env'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' 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 { AnimatedUnitagDisplayName } from 'wallet/src/components/accounts/AnimatedUnitagDisplayName'
import useIsFocused from 'wallet/src/features/focus/useIsFocused' import useIsFocused from 'wallet/src/features/focus/useIsFocused'
import { useActiveAccount, useActiveAccountAddress, useDisplayName } from 'wallet/src/features/wallet/hooks' import { useActiveAccount, useActiveAccountAddress, useDisplayName } from 'wallet/src/features/wallet/hooks'
......
...@@ -8,10 +8,10 @@ import RemoveButton from 'src/components/explore/RemoveButton' ...@@ -8,10 +8,10 @@ import RemoveButton from 'src/components/explore/RemoveButton'
import { disableOnPress } from 'src/utils/disableOnPress' import { disableOnPress } from 'src/utils/disableOnPress'
import { Flex, TouchableArea, useIsDarkMode, useShadowPropsShort, useSporeColors } from 'ui/src' import { Flex, TouchableArea, useIsDarkMode, useShadowPropsShort, useSporeColors } from 'ui/src'
import { borderRadii, iconSizes, opacify } from 'ui/src/theme' 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 { useAvatar } from 'uniswap/src/features/address/avatar'
import { removeWatchedAddress } from 'uniswap/src/features/favorites/slice' import { removeWatchedAddress } from 'uniswap/src/features/favorites/slice'
import { isIOS } from 'utilities/src/platform' import { isIOS } from 'utilities/src/platform'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText' import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText'
import { useDisplayName } from 'wallet/src/features/wallet/hooks' import { useDisplayName } from 'wallet/src/features/wallet/hooks'
import { DisplayNameType } from 'wallet/src/features/wallet/types' import { DisplayNameType } from 'wallet/src/features/wallet/types'
......
...@@ -18,7 +18,7 @@ import { UniverseChainId } from 'uniswap/src/features/chains/types' ...@@ -18,7 +18,7 @@ import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { clearSearchHistory } from 'uniswap/src/features/search/searchHistorySlice' import { clearSearchHistory } from 'uniswap/src/features/search/searchHistorySlice'
import { selectSearchHistory } from 'uniswap/src/features/search/selectSearchHistory' import { selectSearchHistory } from 'uniswap/src/features/search/selectSearchHistory'
import { ModalName } from 'uniswap/src/features/telemetry/constants' 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" /> 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 ...@@ -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 { SearchTokenItem } from 'src/components/explore/search/items/SearchTokenItem'
import { getSearchResultId } from 'src/components/explore/search/utils' import { getSearchResultId } from 'src/components/explore/search/utils'
import { Flex, Loader } from 'ui/src' 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 { ProtectionResult } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { ALL_NETWORKS_ARG } from 'uniswap/src/data/rest/base' import { ALL_NETWORKS_ARG } from 'uniswap/src/data/rest/base'
import { useTokenRankingsQuery } from 'uniswap/src/data/rest/tokenRankings' import { useTokenRankingsQuery } from 'uniswap/src/data/rest/tokenRankings'
...@@ -52,7 +52,7 @@ export function SearchPopularTokens({ selectedChain }: { selectedChain: Universe ...@@ -52,7 +52,7 @@ export function SearchPopularTokens({ selectedChain }: { selectedChain: Universe
const popularTokens = data?.tokenRankings?.[RankingType.Popularity]?.tokens.slice( const popularTokens = data?.tokenRankings?.[RankingType.Popularity]?.tokens.slice(
0, 0,
MAX_DEFAULT_POPULAR_TOKEN_RESULTS_AMOUNT, MAX_DEFAULT_TRENDING_TOKEN_RESULTS_AMOUNT,
) )
const formattedTokens = useMemo( const formattedTokens = useMemo(
......
...@@ -3,13 +3,13 @@ import { useTranslation } from 'react-i18next' ...@@ -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 { 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 { SearchWalletItemBase } from 'src/components/explore/search/items/SearchWalletItemBase'
import { Flex, Text } from 'ui/src' import { Flex, Text } from 'ui/src'
import { AccountIcon } from 'uniswap/src/features/accounts/AccountIcon'
import { useENSAvatar, useENSName } from 'uniswap/src/features/ens/api' import { useENSAvatar, useENSName } from 'uniswap/src/features/ens/api'
import { getCompletedENSName } from 'uniswap/src/features/ens/useENS' import { getCompletedENSName } from 'uniswap/src/features/ens/useENS'
import { SearchContext } from 'uniswap/src/features/search/SearchContext' import { SearchContext } from 'uniswap/src/features/search/SearchContext'
import { ENSAddressSearchResult } from 'uniswap/src/features/search/SearchResult' import { ENSAddressSearchResult } from 'uniswap/src/features/search/SearchResult'
import { sanitizeAddressText } from 'uniswap/src/utils/addresses' import { sanitizeAddressText } from 'uniswap/src/utils/addresses'
import { shortenAddress } from 'utilities/src/addresses' import { shortenAddress } from 'utilities/src/addresses'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
type SearchENSAddressItemProps = { type SearchENSAddressItemProps = {
searchResult: ENSAddressSearchResult searchResult: ENSAddressSearchResult
......
...@@ -2,12 +2,12 @@ import React from 'react' ...@@ -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 { 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 { SearchWalletItemBase } from 'src/components/explore/search/items/SearchWalletItemBase'
import { Flex, Text } from 'ui/src' import { Flex, Text } from 'ui/src'
import { AccountIcon } from 'uniswap/src/features/accounts/AccountIcon'
import { useAvatar } from 'uniswap/src/features/address/avatar' import { useAvatar } from 'uniswap/src/features/address/avatar'
import { SearchContext } from 'uniswap/src/features/search/SearchContext' import { SearchContext } from 'uniswap/src/features/search/SearchContext'
import { UnitagSearchResult } from 'uniswap/src/features/search/SearchResult' import { UnitagSearchResult } from 'uniswap/src/features/search/SearchResult'
import { sanitizeAddressText } from 'uniswap/src/utils/addresses' import { sanitizeAddressText } from 'uniswap/src/utils/addresses'
import { shortenAddress } from 'utilities/src/addresses' import { shortenAddress } from 'utilities/src/addresses'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText' import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText'
import { DisplayNameType } from 'wallet/src/features/wallet/types' import { DisplayNameType } from 'wallet/src/features/wallet/types'
......
...@@ -2,12 +2,12 @@ import React from 'react' ...@@ -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 { 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 { SearchWalletItemBase } from 'src/components/explore/search/items/SearchWalletItemBase'
import { Flex, Text } from 'ui/src' import { Flex, Text } from 'ui/src'
import { AccountIcon } from 'uniswap/src/features/accounts/AccountIcon'
import { useENSAvatar, useENSName } from 'uniswap/src/features/ens/api' import { useENSAvatar, useENSName } from 'uniswap/src/features/ens/api'
import { SearchContext } from 'uniswap/src/features/search/SearchContext' import { SearchContext } from 'uniswap/src/features/search/SearchContext'
import { WalletByAddressSearchResult } from 'uniswap/src/features/search/SearchResult' import { WalletByAddressSearchResult } from 'uniswap/src/features/search/SearchResult'
import { sanitizeAddressText } from 'uniswap/src/utils/addresses' import { sanitizeAddressText } from 'uniswap/src/utils/addresses'
import { shortenAddress } from 'utilities/src/addresses' import { shortenAddress } from 'utilities/src/addresses'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
type SearchWalletByAddressItemProps = { type SearchWalletByAddressItemProps = {
searchResult: WalletByAddressSearchResult searchResult: WalletByAddressSearchResult
......
...@@ -28,6 +28,7 @@ import { INTRO_CARD_MIN_HEIGHT, IntroCardStack } from 'wallet/src/components/int ...@@ -28,6 +28,7 @@ import { INTRO_CARD_MIN_HEIGHT, IntroCardStack } from 'wallet/src/components/int
import { useSharedIntroCards } from 'wallet/src/components/introCards/useSharedIntroCards' import { useSharedIntroCards } from 'wallet/src/components/introCards/useSharedIntroCards'
import { selectHasViewedNotificationsCard } from 'wallet/src/features/behaviorHistory/selectors' import { selectHasViewedNotificationsCard } from 'wallet/src/features/behaviorHistory/selectors'
import { setHasViewedNotificationsCard } from 'wallet/src/features/behaviorHistory/slice' 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' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
type OnboardingIntroCardStackProps = { type OnboardingIntroCardStackProps = {
...@@ -43,7 +44,7 @@ export function OnboardingIntroCardStack({ ...@@ -43,7 +44,7 @@ export function OnboardingIntroCardStack({
const activeAccount = useActiveAccountWithThrow() const activeAccount = useActiveAccountWithThrow()
const address = activeAccount.address const address = activeAccount.address
const isSignerAccount = activeAccount.type === AccountType.SignerMnemonic const isSignerAccount = activeAccount.type === AccountType.SignerMnemonic
const hasBackups = activeAccount.backups && activeAccount.backups.length > 0 const externalBackups = hasExternalBackup(activeAccount)
const { notificationPermissionsEnabled } = useNotificationOSPermissionsEnabled() const { notificationPermissionsEnabled } = useNotificationOSPermissionsEnabled()
const notificationOnboardingCardEnabled = useFeatureFlag(FeatureFlags.NotificationOnboardingCard) const notificationOnboardingCardEnabled = useFeatureFlag(FeatureFlags.NotificationOnboardingCard)
...@@ -102,7 +103,7 @@ export function OnboardingIntroCardStack({ ...@@ -102,7 +103,7 @@ export function OnboardingIntroCardStack({
}) })
} }
if (!hasBackups) { if (!externalBackups) {
output.push({ output.push({
loggingName: OnboardingCardLoggingName.RecoveryBackup, loggingName: OnboardingCardLoggingName.RecoveryBackup,
graphic: { graphic: {
...@@ -149,7 +150,7 @@ export function OnboardingIntroCardStack({ ...@@ -149,7 +150,7 @@ export function OnboardingIntroCardStack({
}) })
} }
return output return output
}, [hasBackups, showEmptyWalletState, isSignerAccount, sharedCards, t, showEnableNotificationsCard, dispatch]) }, [externalBackups, showEmptyWalletState, isSignerAccount, sharedCards, t, showEnableNotificationsCard, dispatch])
const handleSwiped = useCallback( const handleSwiped = useCallback(
(_card: IntroCardProps, index: number) => { (_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' ...@@ -2,7 +2,9 @@ import { memo, type ComponentType } from 'react'
import type { AppStackParamList, AppStackScreenProp } from 'src/app/navigation/types' import type { AppStackParamList, AppStackScreenProp } from 'src/app/navigation/types'
import { useReactNavigationModal } from 'src/components/modals/useReactNavigationModal' import { useReactNavigationModal } from 'src/components/modals/useReactNavigationModal'
import type { GetProps } from 'ui/src' import type { GetProps } from 'ui/src'
import { PasskeyManagementModal } from 'uniswap/src/features/passkey/PasskeyManagementModal'
import { PasskeysHelpModal } from 'uniswap/src/features/passkey/PasskeysHelpModal' 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 { ModalName } from 'uniswap/src/features/telemetry/constants'
import { TestnetModeModal } from 'uniswap/src/features/testnets/TestnetModeModal' import { TestnetModeModal } from 'uniswap/src/features/testnets/TestnetModeModal'
import { HiddenTokenInfoModal } from 'uniswap/src/features/transactions/modals/HiddenTokenInfoModal' import { HiddenTokenInfoModal } from 'uniswap/src/features/transactions/modals/HiddenTokenInfoModal'
...@@ -10,13 +12,19 @@ import { HiddenTokenInfoModal } from 'uniswap/src/features/transactions/modals/H ...@@ -10,13 +12,19 @@ import { HiddenTokenInfoModal } from 'uniswap/src/features/transactions/modals/H
// Define names of shared modals we're explicitly supporting on mobile // Define names of shared modals we're explicitly supporting on mobile
type ValidModalNames = keyof Pick< type ValidModalNames = keyof Pick<
AppStackParamList, 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 = { type ModalNameWithComponentProps = {
[ModalName.TestnetMode]: GetProps<typeof TestnetModeModal> [ModalName.TestnetMode]: GetProps<typeof TestnetModeModal>
[ModalName.HiddenTokenInfoModal]: GetProps<typeof HiddenTokenInfoModal> [ModalName.HiddenTokenInfoModal]: GetProps<typeof HiddenTokenInfoModal>
[ModalName.PasskeyManagement]: GetProps<typeof PasskeyManagementModal>
[ModalName.PasskeysHelp]: GetProps<typeof PasskeysHelpModal> [ModalName.PasskeysHelp]: GetProps<typeof PasskeysHelpModal>
[ModalName.SmartWalletAdvancedSettingsModal]: GetProps<typeof SmartWalletAdvancedSettingsModal>
} }
type NavigationModalProps<ModalName extends ValidModalNames> = { type NavigationModalProps<ModalName extends ValidModalNames> = {
......
...@@ -9,7 +9,7 @@ import { ModalName } from 'uniswap/src/features/telemetry/constants' ...@@ -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 { UNITAG_SUFFIX_NO_LEADING_DOT } from 'uniswap/src/features/unitags/constants'
import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { MobileScreens, UnitagScreens } from 'uniswap/src/types/screens/mobile' 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' import { useUnitagClaimHandler } from 'wallet/src/features/unitags/useUnitagClaimHandler'
const IMAGE_ASPECT_RATIO = 0.42 const IMAGE_ASPECT_RATIO = 0.42
...@@ -62,11 +62,11 @@ export function UnitagBanner({ ...@@ -62,11 +62,11 @@ export function UnitagBanner({
}) })
const onPressClaimNow = (): void => { const onPressClaimNow = (): void => {
dismissNativeKeyboard()
handleClaim()
if (onPressClaim) { if (onPressClaim) {
onPressClaim() onPressClaim()
} }
dismissNativeKeyboard()
handleClaim()
} }
const baseButtonStyle: TouchableAreaProps = { const baseButtonStyle: TouchableAreaProps = {
......
import { PropsWithChildren, createContext, useCallback, useContext, useMemo, useState } from 'react' 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 { import {
PasswordErrors, PasswordErrors,
PasswordStrength, PasswordStrength,
......
...@@ -18,6 +18,7 @@ import { promiseMinDelay } from 'utilities/src/time/timing' ...@@ -18,6 +18,7 @@ import { promiseMinDelay } from 'utilities/src/time/timing'
import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext' import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext'
import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga' import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga'
import { BackupType } from 'wallet/src/features/wallet/accounts/types' 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' import { useSignerAccount } from 'wallet/src/features/wallet/hooks'
type Props = { type Props = {
...@@ -59,14 +60,14 @@ export function CloudBackupProcessingAnimation({ ...@@ -59,14 +60,14 @@ export function CloudBackupProcessingAnimation({
// Handle finished backing up to Cloud // Handle finished backing up to Cloud
useEffect(() => { useEffect(() => {
if (account?.backups?.includes(BackupType.Cloud)) { if (hasBackup(BackupType.Cloud, account)) {
doneProcessing() doneProcessing()
// Show success state for 1s before navigating // Show success state for 1s before navigating
const timer = setTimeout(onBackupComplete, ONE_SECOND_MS) const timer = setTimeout(onBackupComplete, ONE_SECOND_MS)
return () => clearTimeout(timer) return () => clearTimeout(timer)
} }
return undefined return undefined
}, [account?.backups, onBackupComplete]) }, [account, onBackupComplete])
// Handle backup to Cloud when screen appears // Handle backup to Cloud when screen appears
const backup = useCallback(async () => { const backup = useCallback(async () => {
......
...@@ -7,7 +7,7 @@ import { UniverseChainId } from 'uniswap/src/features/chains/types' ...@@ -7,7 +7,7 @@ import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { FiatOffRampMetaData, OffRampTransferDetailsResponse } from 'uniswap/src/features/fiatOnRamp/types' import { FiatOffRampMetaData, OffRampTransferDetailsResponse } from 'uniswap/src/features/fiatOnRamp/types'
import { FiatOffRampEventName, ModalName } from 'uniswap/src/features/telemetry/constants' import { FiatOffRampEventName, ModalName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' 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 { forceFetchFiatOnRampTransactions } from 'uniswap/src/features/transactions/slice'
import { CurrencyField } from 'uniswap/src/types/currency' import { CurrencyField } from 'uniswap/src/types/currency'
import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens } from 'uniswap/src/types/screens/mobile'
......
import { FiatOnRampModalState } from 'src/screens/FiatOnRampModalState' import { FiatOnRampModalState } from 'src/screens/FiatOnRampModalState'
import { ModalName } from 'uniswap/src/features/telemetry/constants' 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 { TransactionState } from 'uniswap/src/features/transactions/types/transactionState'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
......
...@@ -2,7 +2,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' ...@@ -2,7 +2,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { ModalsState } from 'src/features/modals/ModalsState' import { ModalsState } from 'src/features/modals/ModalsState'
import { FiatOnRampModalState } from 'src/screens/FiatOnRampModalState' import { FiatOnRampModalState } from 'src/screens/FiatOnRampModalState'
import { ModalName } from 'uniswap/src/features/telemetry/constants' 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 { TransactionState } from 'uniswap/src/features/transactions/types/transactionState'
import { getKeys } from 'utilities/src/primitives/objects' import { getKeys } from 'utilities/src/primitives/objects'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
......
...@@ -10,7 +10,7 @@ import { BackupType } from 'wallet/src/features/wallet/accounts/types' ...@@ -10,7 +10,7 @@ import { BackupType } from 'wallet/src/features/wallet/accounts/types'
const PREVIEW_BOX_HEIGHT = 122 const PREVIEW_BOX_HEIGHT = 122
type BackupSpeedBumpModalProps = { type BackupSpeedBumpModalProps = {
backupType: BackupType backupType: BackupType.Cloud | BackupType.Manual
onContinue: () => void onContinue: () => void
onClose: () => void onClose: () => void
......
...@@ -12,11 +12,11 @@ import { SendReviewScreen } from 'src/features/send/SendReviewScreen' ...@@ -12,11 +12,11 @@ import { SendReviewScreen } from 'src/features/send/SendReviewScreen'
import { useWalletRestore } from 'src/features/wallet/hooks' import { useWalletRestore } from 'src/features/wallet/hooks'
import Trace from 'uniswap/src/features/telemetry/Trace' import Trace from 'uniswap/src/features/telemetry/Trace'
import { ModalName, SectionName } from 'uniswap/src/features/telemetry/constants' 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 { import {
TransactionScreen, TransactionScreen,
useTransactionModalContext, 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' import { SendContextProvider, useSendContext } from 'wallet/src/features/transactions/contexts/SendContext'
export function SendFlow(): JSX.Element { export function SendFlow(): JSX.Element {
......
...@@ -8,7 +8,7 @@ import { selectHasDismissedLowNetworkTokenWarning } from 'uniswap/src/features/b ...@@ -8,7 +8,7 @@ import { selectHasDismissedLowNetworkTokenWarning } from 'uniswap/src/features/b
import { UniswapEventName } from 'uniswap/src/features/telemetry/constants' import { UniswapEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' 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 { useIsBlocked } from 'uniswap/src/features/trm/hooks'
import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { useSendContext } from 'wallet/src/features/transactions/contexts/SendContext' import { useSendContext } from 'wallet/src/features/transactions/contexts/SendContext'
......
...@@ -21,11 +21,11 @@ import { ModalName } from 'uniswap/src/features/telemetry/constants' ...@@ -21,11 +21,11 @@ import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { import {
TransactionModalFooterContainer, TransactionModalFooterContainer,
TransactionModalInnerContainer, TransactionModalInnerContainer,
} from 'uniswap/src/features/transactions/TransactionModal/TransactionModal' } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModal'
import { import {
TransactionScreen, TransactionScreen,
useTransactionModalContext, 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 { LowNativeBalanceModal } from 'uniswap/src/features/transactions/modals/LowNativeBalanceModal'
import { CurrencyField } from 'uniswap/src/types/currency' import { CurrencyField } from 'uniswap/src/types/currency'
import { createTransactionId } from 'uniswap/src/utils/createTransactionId' import { createTransactionId } from 'uniswap/src/utils/createTransactionId'
......
...@@ -2,8 +2,8 @@ import React, { useCallback, useEffect, useState } from 'react' ...@@ -2,8 +2,8 @@ import React, { useCallback, useEffect, useState } from 'react'
import { RecipientSelect } from 'src/components/RecipientSelect/RecipientSelect' import { RecipientSelect } from 'src/components/RecipientSelect/RecipientSelect'
import { SEND_CONTENT_RENDER_DELAY_MS } from 'src/features/send/constants' import { SEND_CONTENT_RENDER_DELAY_MS } from 'src/features/send/constants'
import { UniverseChainId } from 'uniswap/src/features/chains/types' import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { TransactionModalInnerContainer } from 'uniswap/src/features/transactions/TransactionModal/TransactionModal' import { TransactionModalInnerContainer } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModal'
import { useTransactionModalContext } from 'uniswap/src/features/transactions/TransactionModal/TransactionModalContext' import { useTransactionModalContext } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModalContext'
import { useSendContext } from 'wallet/src/features/transactions/contexts/SendContext' 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. // 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' ...@@ -2,8 +2,8 @@ import React, { useEffect, useState } from 'react'
import { SEND_CONTENT_RENDER_DELAY_MS } from 'src/features/send/constants' import { SEND_CONTENT_RENDER_DELAY_MS } from 'src/features/send/constants'
import { useHapticFeedback } from 'src/utils/haptics/useHapticFeedback' import { useHapticFeedback } from 'src/utils/haptics/useHapticFeedback'
import { Flex } from 'ui/src/components/layout/Flex' import { Flex } from 'ui/src/components/layout/Flex'
import { TransactionModalInnerContainer } from 'uniswap/src/features/transactions/TransactionModal/TransactionModal' import { TransactionModalInnerContainer } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModal'
import { useTransactionModalContext } from 'uniswap/src/features/transactions/TransactionModal/TransactionModalContext' import { useTransactionModalContext } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModalContext'
import { SendReviewDetails } from 'wallet/src/features/transactions/send/SendReviewDetails' 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. // 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 { ...@@ -18,8 +18,8 @@ import {
DecimalPadCalculatedSpaceId, DecimalPadCalculatedSpaceId,
DecimalPadInput, DecimalPadInput,
DecimalPadInputRef, DecimalPadInputRef,
} from 'uniswap/src/features/transactions/DecimalPadInput/DecimalPadInput' } from 'uniswap/src/features/transactions/components/DecimalPadInput/DecimalPadInput'
import { InsufficientNativeTokenWarning } from 'uniswap/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning' import { InsufficientNativeTokenWarning } from 'uniswap/src/features/transactions/components/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning'
import { useUSDCValue } from 'uniswap/src/features/transactions/hooks/useUSDCPrice' import { useUSDCValue } from 'uniswap/src/features/transactions/hooks/useUSDCPrice'
import { useUSDTokenUpdater } from 'uniswap/src/features/transactions/hooks/useUSDTokenUpdater' import { useUSDTokenUpdater } from 'uniswap/src/features/transactions/hooks/useUSDTokenUpdater'
import { BlockedAddressWarning } from 'uniswap/src/features/transactions/modals/BlockedAddressWarning' import { BlockedAddressWarning } from 'uniswap/src/features/transactions/modals/BlockedAddressWarning'
...@@ -27,7 +27,7 @@ import { SwapArrowButton } from 'uniswap/src/features/transactions/swap/form/bod ...@@ -27,7 +27,7 @@ import { SwapArrowButton } from 'uniswap/src/features/transactions/swap/form/bod
import { TransactionType } from 'uniswap/src/features/transactions/types/transactionDetails' import { TransactionType } from 'uniswap/src/features/transactions/types/transactionDetails'
import { useIsBlocked } from 'uniswap/src/features/trm/hooks' import { useIsBlocked } from 'uniswap/src/features/trm/hooks'
import { CurrencyField } from 'uniswap/src/types/currency' 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 { truncateToMaxDecimals } from 'utilities/src/format/truncateToMaxDecimals'
import { RecipientInputPanel } from 'wallet/src/components/input/RecipientInputPanel' import { RecipientInputPanel } from 'wallet/src/components/input/RecipientInputPanel'
import { NFTTransfer } from 'wallet/src/components/nfts/NFTTransfer' import { NFTTransfer } from 'wallet/src/components/nfts/NFTTransfer'
......
...@@ -12,7 +12,7 @@ import { Ellipsis } from 'ui/src/components/icons' ...@@ -12,7 +12,7 @@ import { Ellipsis } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { useBottomSheetSafeKeyboard } from 'uniswap/src/components/modals/useBottomSheetSafeKeyboard' import { useBottomSheetSafeKeyboard } from 'uniswap/src/components/modals/useBottomSheetSafeKeyboard'
import { MobileScreens, UnitagScreens } from 'uniswap/src/types/screens/mobile' 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 { isIOS } from 'utilities/src/platform'
import { ChangeUnitagModal } from 'wallet/src/features/unitags/ChangeUnitagModal' import { ChangeUnitagModal } from 'wallet/src/features/unitags/ChangeUnitagModal'
import { DeleteUnitagModal } from 'wallet/src/features/unitags/DeleteUnitagModal' 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' ...@@ -10,6 +10,7 @@ import { EventChannel, eventChannel } from 'redux-saga'
import { MobileState } from 'src/app/mobileReducer' import { MobileState } from 'src/app/mobileReducer'
import { registerWCClientForPushNotifications } from 'src/features/walletConnect/api' import { registerWCClientForPushNotifications } from 'src/features/walletConnect/api'
import { fetchDappDetails } from 'src/features/walletConnect/fetchDappDetails' import { fetchDappDetails } from 'src/features/walletConnect/fetchDappDetails'
import { getMockedEncodedBatchedTransaction } from 'src/features/walletConnect/mocks'
import { import {
getAccountAddressFromEIP155String, getAccountAddressFromEIP155String,
getChainIdFromEIP155String, getChainIdFromEIP155String,
...@@ -27,7 +28,7 @@ import { ...@@ -27,7 +28,7 @@ import {
removeSession, removeSession,
setHasPendingSessionError, setHasPendingSessionError,
} from 'src/features/walletConnect/walletConnectSlice' } 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 { config } from 'uniswap/src/config'
import { ALL_CHAIN_IDS, UniverseChainId } from 'uniswap/src/features/chains/types' import { ALL_CHAIN_IDS, UniverseChainId } from 'uniswap/src/features/chains/types'
import { getChainLabel } from 'uniswap/src/features/chains/utils' import { getChainLabel } from 'uniswap/src/features/chains/utils'
...@@ -325,23 +326,16 @@ function* handleSessionRequest(sessionRequest: PendingRequestTypes.Struct) { ...@@ -325,23 +326,16 @@ function* handleSessionRequest(sessionRequest: PendingRequestTypes.Struct) {
} }
case EthMethod.SendCalls: { case EthMethod.SendCalls: {
// capabilities as part of sendCallsRequest is subject to change // 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 yield* delay(300) // to emulate a network request
const response = { const requestWithEncodedTransaction = {
id, ...request,
capabilities, // TODO: replace this with a real call to Wallet API /encode endpoint
encodedTransaction: getMockedEncodedBatchedTransaction(request.account),
} }
yield* call([wcWeb3Wallet, wcWeb3Wallet.respondSessionRequest], { yield* put(addRequest(requestWithEncodedTransaction))
topic,
response: {
id,
jsonrpc: '2.0',
result: response,
},
})
break break
} }
case EthMethod.GetCallsStatus: { case EthMethod.GetCallsStatus: {
......
...@@ -2,8 +2,8 @@ import { createSelector, Selector } from '@reduxjs/toolkit' ...@@ -2,8 +2,8 @@ import { createSelector, Selector } from '@reduxjs/toolkit'
import { MobileState } from 'src/app/mobileReducer' import { MobileState } from 'src/app/mobileReducer'
import { import {
WalletConnectPendingSession, WalletConnectPendingSession,
WalletConnectRequest,
WalletConnectSession, WalletConnectSession,
WalletConnectSigningRequest,
} from 'src/features/walletConnect/walletConnectSlice' } from 'src/features/walletConnect/walletConnectSlice'
export const makeSelectSessions = (): Selector<MobileState, WalletConnectSession[] | undefined, [Maybe<Address>]> => export const makeSelectSessions = (): Selector<MobileState, WalletConnectSession[] | undefined, [Maybe<Address>]> =>
...@@ -24,7 +24,7 @@ export const makeSelectSessions = (): Selector<MobileState, WalletConnectSession ...@@ -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 return state.walletConnect.pendingRequests
} }
......
import { providers } from 'ethers' import { providers } from 'ethers'
import { wcWeb3Wallet } from 'src/features/walletConnect/saga' 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 { call, put } from 'typed-redux-saga'
import { AssetType } from 'uniswap/src/entities/assets' import { AssetType } from 'uniswap/src/entities/assets'
import { UniverseChainId } from 'uniswap/src/features/chains/types' import { UniverseChainId } from 'uniswap/src/features/chains/types'
...@@ -12,7 +16,11 @@ import { TransactionOriginType, TransactionType } from 'uniswap/src/features/tra ...@@ -12,7 +16,11 @@ import { TransactionOriginType, TransactionType } from 'uniswap/src/features/tra
import { DappInfo, EthSignMethod, UwULinkMethod, WalletConnectEvent } from 'uniswap/src/types/walletConnect' import { DappInfo, EthSignMethod, UwULinkMethod, WalletConnectEvent } from 'uniswap/src/types/walletConnect'
import { createSaga } from 'uniswap/src/utils/saga' import { createSaga } from 'uniswap/src/utils/saga'
import { logger } from 'utilities/src/logger/logger' 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 { Account } from 'wallet/src/features/wallet/accounts/types'
import { getSignerManager } from 'wallet/src/features/wallet/context' import { getSignerManager } from 'wallet/src/features/wallet/context'
import { signMessage, signTypedDataMessage } from 'wallet/src/features/wallet/signing/signing' import { signMessage, signTypedDataMessage } from 'wallet/src/features/wallet/signing/signing'
...@@ -32,10 +40,10 @@ type SignTransactionParams = { ...@@ -32,10 +40,10 @@ type SignTransactionParams = {
requestInternalId: string requestInternalId: string
transaction: providers.TransactionRequest transaction: providers.TransactionRequest
account: Account account: Account
method: EthMethod.EthSendTransaction method: EthMethod.EthSendTransaction | EthMethod.SendCalls
dapp: DappInfo dapp: DappInfo
chainId: UniverseChainId chainId: UniverseChainId
request: TransactionRequest | UwuLinkErc20Request request: TransactionRequest | UwuLinkErc20Request | WalletSendCallsEncodedRequest
} }
function* signWcRequest(params: SignMessageParams | SignTransactionParams) { function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
...@@ -43,9 +51,9 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) { ...@@ -43,9 +51,9 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
const { defaultChainId } = yield* getEnabledChainIdsSaga() const { defaultChainId } = yield* getEnabledChainIdsSaga()
try { try {
const signerManager = yield* call(getSignerManager) const signerManager = yield* call(getSignerManager)
let signature = '' let result: string | SendCallsResult = ''
if (method === EthMethod.PersonalSign || method === EthMethod.EthSign) { 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 // TODO: add `isCheckIn` type to uwulink request info so that this can be generalized
if (params.dapp.source === 'uwulink' && params.dapp.name === 'Uniswap Cafe') { if (params.dapp.source === 'uwulink' && params.dapp.name === 'Uniswap Cafe') {
...@@ -57,9 +65,9 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) { ...@@ -57,9 +65,9 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
) )
} }
} else if (method === EthMethod.SignTypedData || method === EthMethod.SignTypedDataV4) { } 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) { } else if (method === EthMethod.EthSendTransaction && params.request.type === UwULinkMethod.Erc20Send) {
const txParams: SendTransactionParams = { const txParams: ExecuteTransactionParams = {
chainId: params.transaction.chainId || defaultChainId, chainId: params.transaction.chainId || defaultChainId,
account, account,
options: { options: {
...@@ -74,10 +82,10 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) { ...@@ -74,10 +82,10 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
}, },
transactionOriginType: TransactionOriginType.External, transactionOriginType: TransactionOriginType.External,
} }
const { transactionResponse } = yield* call(sendTransaction, txParams) const { transactionResponse } = yield* call(executeTransaction, txParams)
signature = transactionResponse.hash result = transactionResponse.hash
} else if (method === EthMethod.EthSendTransaction) { } else if (method === EthMethod.EthSendTransaction) {
const txParams: SendTransactionParams = { const txParams: ExecuteTransactionParams = {
chainId: params.transaction.chainId || defaultChainId, chainId: params.transaction.chainId || defaultChainId,
account, account,
options: { options: {
...@@ -89,8 +97,35 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) { ...@@ -89,8 +97,35 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
}, },
transactionOriginType: TransactionOriginType.External, transactionOriginType: TransactionOriginType.External,
} }
const { transactionResponse } = yield* call(sendTransaction, txParams) const { transactionResponse } = yield* call(executeTransaction, txParams)
signature = transactionResponse.hash 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 // Trigger a pending transaction notification after we send the transaction to chain
yield* put( yield* put(
...@@ -107,7 +142,7 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) { ...@@ -107,7 +142,7 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
response: { response: {
id: Number(requestInternalId), id: Number(requestInternalId),
jsonrpc: '2.0', jsonrpc: '2.0',
result: signature, result,
}, },
}) })
} else if (params.dapp.source === 'uwulink' && params.dapp.webhook) { } else if (params.dapp.source === 'uwulink' && params.dapp.webhook) {
...@@ -117,7 +152,7 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) { ...@@ -117,7 +152,7 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
Accept: 'application/json', Accept: 'application/json',
'Content-Type': '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 // TODO: consider adding analytics to track UwuLink usage
}).catch((error) => }).catch((error) =>
logger.error(error, { logger.error(error, {
......
...@@ -11,15 +11,15 @@ import { ...@@ -11,15 +11,15 @@ import {
} from 'src/features/walletConnect/selectors' } from 'src/features/walletConnect/selectors'
import { import {
WalletConnectPendingSession, WalletConnectPendingSession,
WalletConnectRequest,
WalletConnectSession, WalletConnectSession,
WalletConnectSigningRequest,
} from 'src/features/walletConnect/walletConnectSlice' } from 'src/features/walletConnect/walletConnectSlice'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
interface WalletConnect { interface WalletConnect {
sessions: WalletConnectSession[] sessions: WalletConnectSession[]
pendingRequests: WalletConnectRequest[] pendingRequests: WalletConnectSigningRequest[]
modalState: AppModalState<ScannerModalState> modalState: AppModalState<ScannerModalState>
pendingSession: WalletConnectPendingSession | null pendingSession: WalletConnectPendingSession | null
hasPendingSessionError: boolean hasPendingSessionError: boolean
......
...@@ -56,6 +56,10 @@ export interface WalletSendCallsRequest extends BaseRequest { ...@@ -56,6 +56,10 @@ export interface WalletSendCallsRequest extends BaseRequest {
version: string version: string
} }
export interface WalletSendCallsEncodedRequest extends WalletSendCallsRequest {
encodedTransaction: EthTransaction
}
export interface WalletGetCallsStatusRequest extends BaseRequest { export interface WalletGetCallsStatusRequest extends BaseRequest {
id: string id: string
type: EthMethod.GetCallsStatus type: EthMethod.GetCallsStatus
...@@ -77,11 +81,19 @@ export interface UwuLinkErc20Request extends BaseRequest { ...@@ -77,11 +81,19 @@ export interface UwuLinkErc20Request extends BaseRequest {
transaction: EthTransaction // the formatted transaction, prepared by the wallet 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 request.type === EthMethod.EthSendTransaction || request.type === UwULinkMethod.Erc20Send
export const isBatchedTransactionRequest = (
request: WalletConnectSigningRequest,
): request is WalletSendCallsEncodedRequest => request.type === EthMethod.SendCalls
export interface WalletConnectState { export interface WalletConnectState {
byAccount: { byAccount: {
[accountId: string]: { [accountId: string]: {
...@@ -89,7 +101,7 @@ export interface WalletConnectState { ...@@ -89,7 +101,7 @@ export interface WalletConnectState {
} }
} }
pendingSession: WalletConnectPendingSession | null pendingSession: WalletConnectPendingSession | null
pendingRequests: WalletConnectRequest[] pendingRequests: WalletConnectSigningRequest[]
didOpenFromDeepLink?: boolean didOpenFromDeepLink?: boolean
hasPendingSessionError?: boolean hasPendingSessionError?: boolean
} }
...@@ -143,7 +155,7 @@ const slice = createSlice({ ...@@ -143,7 +155,7 @@ const slice = createSlice({
state.pendingSession = null state.pendingSession = null
}, },
addRequest: (state, action: PayloadAction<WalletConnectRequest>) => { addRequest: (state, action: PayloadAction<WalletConnectSigningRequest>) => {
state.pendingRequests.push(action.payload) state.pendingRequests.push(action.payload)
}, },
......
...@@ -19,11 +19,12 @@ import { FeatureFlags } from 'uniswap/src/features/gating/flags' ...@@ -19,11 +19,12 @@ import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { SearchModalNoQueryList } from 'uniswap/src/features/search/SearchModal/SearchModalNoQueryList' import { SearchModalNoQueryList } from 'uniswap/src/features/search/SearchModal/SearchModalNoQueryList'
import { SearchModalResultsList } from 'uniswap/src/features/search/SearchModal/SearchModalResultsList' 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 { CancelBehaviorType, SearchTextInput } from 'uniswap/src/features/search/SearchTextInput'
import { MobileEventName, SectionName } from 'uniswap/src/features/telemetry/constants' import { MobileEventName, SectionName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { MobileScreens } from 'uniswap/src/types/screens/mobile' 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' import { useDebounce } from 'utilities/src/time/timing'
// From design to avoid layout thrash as icons show and hide // From design to avoid layout thrash as icons show and hide
...@@ -49,6 +50,7 @@ export function ExploreScreen(): JSX.Element { ...@@ -49,6 +50,7 @@ export function ExploreScreen(): JSX.Element {
const onSearchChangeText = (newSearchFilter: string): void => { const onSearchChangeText = (newSearchFilter: string): void => {
setSearchQuery(newSearchFilter) setSearchQuery(newSearchFilter)
textInputRef.current?.setNativeProps({ text: newSearchFilter })
} }
const onSearchFocus = (): void => { const onSearchFocus = (): void => {
...@@ -114,10 +116,11 @@ export function ExploreScreen(): JSX.Element { ...@@ -114,10 +116,11 @@ export function ExploreScreen(): JSX.Element {
debouncedSearchFilter={debouncedSearchQuery} debouncedSearchFilter={debouncedSearchQuery}
parsedChainFilter={selectedChain} parsedChainFilter={selectedChain}
searchFilter={searchQuery ?? ''} searchFilter={searchQuery ?? ''}
activeTab={SearchTab.All}
onSelect={() => {}} onSelect={() => {}}
/> />
) : ( ) : (
<SearchModalNoQueryList chainFilter={selectedChain} onSelect={() => {}} /> <SearchModalNoQueryList chainFilter={selectedChain} activeTab={SearchTab.All} onSelect={() => {}} />
) )
) : debouncedSearchQuery.length === 0 ? ( ) : debouncedSearchQuery.length === 0 ? (
// Mimic ScrollView behavior with FlatList // Mimic ScrollView behavior with FlatList
......
...@@ -56,7 +56,7 @@ import { ...@@ -56,7 +56,7 @@ import {
DecimalPadCalculatedSpaceId, DecimalPadCalculatedSpaceId,
DecimalPadInput, DecimalPadInput,
DecimalPadInputRef, 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 { useUSDTokenUpdater } from 'uniswap/src/features/transactions/hooks/useUSDTokenUpdater'
import { CurrencyField } from 'uniswap/src/types/currency' import { CurrencyField } from 'uniswap/src/types/currency'
import { FiatOnRampScreens } from 'uniswap/src/types/screens/mobile' import { FiatOnRampScreens } from 'uniswap/src/types/screens/mobile'
......
import { useEffect } from 'react' import { useCallback } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { usePortfolioBalances } from 'uniswap/src/features/dataApi/balances'
import { selectFavoriteTokens } from 'uniswap/src/features/favorites/selectors' import { selectFavoriteTokens } from 'uniswap/src/features/favorites/selectors'
import { setAttributesToDatadog } from 'utilities/src/logger/datadog/Datadog' 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. * Helper hook for the home screen to track any user specific attributes.
*/ */
export function useHomeScreenTracking(): void { export function useHomeScreenTracking(): void {
const favoriteCurrencyIds = useSelector(selectFavoriteTokens) 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({ setAttributesToDatadog({
tokenCount,
favoriteTokensCount: favoriteCurrencyIds.length, favoriteTokensCount: favoriteCurrencyIds.length,
viewOnlyAccountsCount: viewOnlyAccounts.length,
signerAccountsCount: signerAccounts.length,
}).catch(() => undefined) }).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' ...@@ -4,9 +4,9 @@ import { ViewProps } from 'react-native'
import { RecoveryWalletInfo, useOnDeviceRecoveryData } from 'src/screens/Import/useOnDeviceRecoveryData' import { RecoveryWalletInfo, useOnDeviceRecoveryData } from 'src/screens/Import/useOnDeviceRecoveryData'
import { Button, Flex, FlexProps, Loader, Text, TouchableArea } from 'ui/src' import { Button, Flex, FlexProps, Loader, Text, TouchableArea } from 'ui/src'
import { fonts, iconSizes } from 'ui/src/theme' import { fonts, iconSizes } from 'ui/src/theme'
import { AccountIcon } from 'uniswap/src/features/accounts/AccountIcon'
import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext'
import { NumberType } from 'utilities/src/format/types' import { NumberType } from 'utilities/src/format/types'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
const cardProps: FlexProps & ViewProps = { const cardProps: FlexProps & ViewProps = {
......
...@@ -13,6 +13,7 @@ import { PasskeyImportLoading } from 'wallet/src/features/onboarding/PasskeyImpo ...@@ -13,6 +13,7 @@ import { PasskeyImportLoading } from 'wallet/src/features/onboarding/PasskeyImpo
import { WelcomeSplash } from 'wallet/src/features/onboarding/WelcomeSplash' import { WelcomeSplash } from 'wallet/src/features/onboarding/WelcomeSplash'
import { fetchSeedPhrase } from 'wallet/src/features/passkeys/passkeys' import { fetchSeedPhrase } from 'wallet/src/features/passkeys/passkeys'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
import { BackupType } from 'wallet/src/features/wallet/accounts/types'
type Props = NativeStackScreenProps<OnboardingStackParamList, OnboardingScreens.PasskeyImport> type Props = NativeStackScreenProps<OnboardingStackParamList, OnboardingScreens.PasskeyImport>
...@@ -33,7 +34,7 @@ export function PasskeyImportScreen({ navigation, route: { params } }: Props): J ...@@ -33,7 +34,7 @@ export function PasskeyImportScreen({ navigation, route: { params } }: Props): J
const importAndGenerateAccount = async (): Promise<void> => { const importAndGenerateAccount = async (): Promise<void> => {
const mnemonic = await fetchSeedPhrase(params.passkeyCredential) const mnemonic = await fetchSeedPhrase(params.passkeyCredential)
const importedAddress = await Keyring.importMnemonic(mnemonic) const importedAddress = await Keyring.importMnemonic(mnemonic)
await generateImportedAccounts({ mnemonicId: importedAddress }) await generateImportedAccounts({ mnemonicId: importedAddress, backupType: BackupType.Passkey })
if (!importedAddress) { if (!importedAddress) {
throw new Error(`Failed to generate account for mnemonic ${mnemonic}`) throw new Error(`Failed to generate account for mnemonic ${mnemonic}`)
} }
......
...@@ -24,7 +24,7 @@ import { TestID } from 'uniswap/src/test/fixtures/testIDs' ...@@ -24,7 +24,7 @@ import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { ImportType } from 'uniswap/src/types/onboarding' import { ImportType } from 'uniswap/src/types/onboarding'
import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName' 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 { MINUTES_IN_HOUR, ONE_HOUR_MS, ONE_MINUTE_MS } from 'utilities/src/time/time'
import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext' import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext'
import { BackupType } from 'wallet/src/features/wallet/accounts/types' import { BackupType } from 'wallet/src/features/wallet/accounts/types'
......
...@@ -20,7 +20,7 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' ...@@ -20,7 +20,7 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { areAddressesEqual, getValidAddress } from 'uniswap/src/utils/addresses' 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 { normalizeTextInput } from 'utilities/src/primitives/string'
import { createViewOnlyAccount } from 'wallet/src/features/onboarding/createViewOnlyAccount' import { createViewOnlyAccount } from 'wallet/src/features/onboarding/createViewOnlyAccount'
import { useIsSmartContractAddress } from 'wallet/src/features/transactions/send/hooks/useIsSmartContractAddress' import { useIsSmartContractAddress } from 'wallet/src/features/transactions/send/hooks/useIsSmartContractAddress'
......
...@@ -12,10 +12,14 @@ import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobi ...@@ -12,10 +12,14 @@ import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobi
import { TamaguiProvider } from 'wallet/src/providers/tamagui-provider' import { TamaguiProvider } from 'wallet/src/providers/tamagui-provider'
import { ACCOUNT, preloadedWalletPackageState } from 'wallet/src/test/fixtures' 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', () => ({ jest.mock('wallet/src/features/onboarding/OnboardingContext', () => ({
useOnboardingContext: jest.fn().mockReturnValue({ useOnboardingContext: jest.fn().mockReturnValue({
getOnboardingOrImportedAccount: jest.fn().mockReturnValue({ address: 'mockedAccountAddress' }), getOnboardingOrImportedAccount: jest.fn().mockReturnValue({ address: 'mockedAccountAddress' }),
hasBackup: jest.fn(),
}), }),
useCreateImportedAccountsFromMnemonicIfNone: 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