ci(release): publish latest release

parent da9fc597
IPFS hash of the deployment: IPFS hash of the deployment:
- CIDv0: `QmYgGWW41t5rZ973ouoVCPSVJ7xSk1ouQS8hXJaVrwBydQ` - CIDv0: `QmXS4vd18Mi4tKCVNuk1qnB1wyNPN7XRR1iSygxNvnRKiD`
- CIDv1: `bafybeieztnrdyd2anpehiktteokw23mwgbjmnz42u4cpzosxjxignq2bf4` - CIDv1: `bafybeiehdr3z22tnf5jdnjnsj5kfb7ywjjefa2oyttljiuwjtu6mhocxjy`
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,73 @@ You can also access the Uniswap Interface from an IPFS gateway. ...@@ -10,14 +10,73 @@ 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://bafybeieztnrdyd2anpehiktteokw23mwgbjmnz42u4cpzosxjxignq2bf4.ipfs.dweb.link/ - https://bafybeiehdr3z22tnf5jdnjnsj5kfb7ywjjefa2oyttljiuwjtu6mhocxjy.ipfs.dweb.link/
- [ipfs://QmYgGWW41t5rZ973ouoVCPSVJ7xSk1ouQS8hXJaVrwBydQ/](ipfs://QmYgGWW41t5rZ973ouoVCPSVJ7xSk1ouQS8hXJaVrwBydQ/) - [ipfs://QmXS4vd18Mi4tKCVNuk1qnB1wyNPN7XRR1iSygxNvnRKiD/](ipfs://QmXS4vd18Mi4tKCVNuk1qnB1wyNPN7XRR1iSygxNvnRKiD/)
## 5.85.0 (2025-05-15) ## 5.86.0 (2025-05-21)
### Features ### Features
* **web:** gracefully handle upgrade prompt rejection [prod] (#19757) b76e682 * **web:** [lp] prefill hook and fee tier (#19820) 09bd7d7
* **web:** defer limit/buy/send form renders (#19445) e464d28
* **web:** defer render of mini portfolio (#19442) 9ce97c7
* **web:** defer top level Updaters (#19444) 4e57ea2
* **web:** gracefully handle upgrade prompt rejection (#19753) cf8beed
* **web:** lazy-import redux store in async code (#19441) 1ba4faa
* **web:** re sign in to EW when session ends (#19670) 8d003c0
* **web:** show backup EW card (#19317) 89f2297
* **web:** skip quotes for hidden positions, stabilize refs (#19755) 165b56d
* **web:** update animation direction for get started modal (#19717) 932f082
* **web:** virtualize positions list (#19754) 2460066
* **web:** vite support (#19713) 1cbf6ea
### Bug Fixes
* **web:** [lp] improve liquiditySaga error logging (#19546) f7857cf
* **web:** add custom range icon color (#19564) 3b8b585
* **web:** Add formAction None to CSP (#19333) d9ef80b
* **web:** add retries to the getstarted e2e test (#19513) 6fa8bd6
* **web:** add useInitialHeight prop to fix enter animation on liquidity modals (#19530) a3f0063
* **web:** Adds color extraction to token detail page charts (#19300) b2b0289
* **web:** allow press on disabled web3-status-connected (#19534) 87d8aac
* **web:** dedupe and cache wallet capabilities calls (#19739) 729472d
* **web:** delete unused GQL queries (#19506) 6eca1a3
* **web:** do not double stringify list authenticator credential (#19715) 25d9d16
* **web:** fix color of UniswapX gas fee tooltip (#19563) 5bb793e
* **web:** fix mobile status overflow on positions (#19350) ea732a4
* **web:** fix web pool index virtualized list dynamic heights (#20074) 9af7388
* **web:** improve LP incentive tooltip responsiveness (#19550) 563b9d4
* **web:** incorrect bridge card display for wrong network (#19541) 593c2bd
* **web:** only support batch swaps on supported status (#19565) c81568c
* **web:** REVERT "remove async imports in LanguageProvider and amplitude" (#19738) 081bfaa
* **web:** settings tooltips (#20059) (#20073) 4ef1352
* **web:** staging web update unwrap tx on smart wallet accounts (#20025) ac4f3a6
* **web:** temp remove disabled prop from Button (#19676) 41c2e61
* **web:** update EW endpoint for beta (#19519) b405d89
* **web:** update MoreHorizontal icon size (#19558) 57c4372
* **web:** use liquidity info badges on pool details page (#19809) 33ce214
* **web): Revert "feat(web:** defer render of mini portfolio (#19442) - staging (#19987) 1e04adb
### Continuous Integration
* **web:** update sitemaps 277df80
### Code Refactoring
* **web:** rename graphql folder (#19712) 06730e1
* **web:** use useScrollbarStyles from ui and remove web ScrollBarStyles (#19516) 6f0b63f
### Tests
* **web:** [errors] add wait for timeout after swap settings click (#19511) 6412e2b
* **web:** delete cypress fees test (#19537) 26a8096
* **web:** handle smart wallet delegations (#19677) 9824dec
* **web:** playwright amplitude extension and logging test (#19536) b234ac6
* **web:** token selector sections should be visible (#19529) df98029
web/5.85.0 web/5.86.0
\ No newline at end of file \ No newline at end of file
...@@ -45,7 +45,15 @@ globalThis.matchMedia = ...@@ -45,7 +45,15 @@ globalThis.matchMedia =
require('react-native-reanimated').setUpTests() require('react-native-reanimated').setUpTests()
global.chrome = chrome const MOCK_LANGUAGE = 'en-US'
global.chrome = {
...chrome,
i18n: {
...global.chrome.i18n,
getUILanguage: jest.fn().mockReturnValue(MOCK_LANGUAGE)
}
}
jest.mock('src/app/navigation/utils', () => ({ jest.mock('src/app/navigation/utils', () => ({
useExtensionNavigation: () => ({ useExtensionNavigation: () => ({
...@@ -69,4 +77,3 @@ jest.mock('wallet/src/features/appearance/hooks', () => { ...@@ -69,4 +77,3 @@ jest.mock('wallet/src/features/appearance/hooks', () => {
useSelectedColorScheme: () => 'light', useSelectedColorScheme: () => 'light',
} }
}) })
import { useSmartWalletNudges } from 'src/app/context/SmartWalletNudgesContext'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { PostSwapSmartWalletNudge } from 'wallet/src/components/smartWallet/modals/PostSwapSmartWalletNudge'
import { SmartWalletEnabledModal } from 'wallet/src/components/smartWallet/modals/SmartWalletEnabledModal'
export function SmartWalletNudgeModals(): JSX.Element | null {
const { activeModal, closeModal, openModal, dappInfo } = useSmartWalletNudges()
if (!activeModal) {
return null
}
switch (activeModal) {
case ModalName.PostSwapSmartWalletNudge:
return (
<PostSwapSmartWalletNudge
isOpen
onClose={closeModal}
dappInfo={dappInfo}
onEnableSmartWallet={() => openModal(ModalName.SmartWalletEnabledModal)}
/>
)
case ModalName.SmartWalletEnabledModal:
return <SmartWalletEnabledModal isOpen showReconnectDappPrompt={!!dappInfo} onClose={closeModal} />
default:
return null
}
}
import { ReactNode, createContext, useCallback, useContext, useState } from 'react'
import { ModalNameType } from 'uniswap/src/features/telemetry/constants'
type DappInfo = {
icon?: string
name?: string
}
type SmartWalletNudgesContextState = {
activeModal: ModalNameType | null
openModal: (modal: ModalNameType) => void
closeModal: () => void
dappInfo?: DappInfo
setDappInfo: (info?: DappInfo) => void
}
const SmartWalletNudgesContext = createContext<SmartWalletNudgesContextState | undefined>(undefined)
export function SmartWalletNudgesProvider({ children }: { children: ReactNode }): JSX.Element {
const [activeModal, setActiveModal] = useState<ModalNameType | null>(null)
const [dappInfo, setDappInfo] = useState<{
icon?: string
name?: string
}>()
const openModal = useCallback(
(modal: ModalNameType): void => {
setActiveModal(modal)
},
[setActiveModal],
)
const closeModal = useCallback((): void => {
setActiveModal(null)
}, [setActiveModal])
return (
<SmartWalletNudgesContext.Provider
value={{
activeModal,
openModal,
closeModal,
dappInfo,
setDappInfo,
}}
>
{children}
</SmartWalletNudgesContext.Provider>
)
}
export function useSmartWalletNudges(): SmartWalletNudgesContextState {
const context = useContext(SmartWalletNudgesContext)
if (!context) {
throw new Error('useSmartWalletNudges must be used within a SmartWalletNudgesProvider')
}
return context
}
...@@ -2,6 +2,7 @@ import { PropsWithChildren } from 'react' ...@@ -2,6 +2,7 @@ import { PropsWithChildren } from 'react'
import { I18nextProvider } from 'react-i18next' import { I18nextProvider } from 'react-i18next'
import { GraphqlProvider } from 'src/app/apollo' import { GraphqlProvider } from 'src/app/apollo'
import { TraceUserProperties } from 'src/app/components/Trace/TraceUserProperties' import { TraceUserProperties } from 'src/app/components/Trace/TraceUserProperties'
import { SmartWalletNudgesProvider } from 'src/app/context/SmartWalletNudgesContext'
import { ExtensionStatsigProvider } from 'src/app/core/StatsigProvider' import { ExtensionStatsigProvider } from 'src/app/core/StatsigProvider'
import { DatadogAppNameTag } from 'src/app/datadog' import { DatadogAppNameTag } from 'src/app/datadog'
import { getReduxStore } from 'src/store/store' import { getReduxStore } from 'src/store/store'
...@@ -24,10 +25,12 @@ export function BaseAppContainer({ ...@@ -24,10 +25,12 @@ export function BaseAppContainer({
<ErrorBoundary> <ErrorBoundary>
<GraphqlProvider> <GraphqlProvider>
<BlankUrlProvider> <BlankUrlProvider>
<LocalizationContextProvider> <SmartWalletNudgesProvider>
<TraceUserProperties /> <LocalizationContextProvider>
{children} <TraceUserProperties />
</LocalizationContextProvider> {children}
</LocalizationContextProvider>
</SmartWalletNudgesProvider>
</BlankUrlProvider> </BlankUrlProvider>
</GraphqlProvider> </GraphqlProvider>
</ErrorBoundary> </ErrorBoundary>
......
...@@ -5,7 +5,7 @@ import { initializeReduxStore } from 'src/store/store' ...@@ -5,7 +5,7 @@ import { initializeReduxStore } from 'src/store/store'
describe('OnboardingApp', () => { describe('OnboardingApp', () => {
// eslint-disable-next-line jest/expect-expect // eslint-disable-next-line jest/expect-expect
it('renders without error', async () => { it('renders without error', async () => {
await initializeReduxStore() initializeReduxStore()
render(<OnboardingApp />) render(<OnboardingApp />)
}) })
}) })
...@@ -39,10 +39,10 @@ import { setRouter, setRouterState } from 'src/app/navigation/state' ...@@ -39,10 +39,10 @@ import { setRouter, setRouterState } from 'src/app/navigation/state'
import { initExtensionAnalytics } from 'src/app/utils/analytics' import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { checksIfSupportsSidePanel } from 'src/app/utils/chrome' import { checksIfSupportsSidePanel } from 'src/app/utils/chrome'
import { PrimaryAppInstanceDebuggerLazy } from 'src/store/PrimaryAppInstanceDebuggerLazy' import { PrimaryAppInstanceDebuggerLazy } from 'src/store/PrimaryAppInstanceDebuggerLazy'
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 { ExtensionOnboardingFlow } from 'uniswap/src/types/screens/extension' import { ExtensionOnboardingFlow } from 'uniswap/src/types/screens/extension'
import { getReduxPersistor } from 'wallet/src/state/persistor'
const supportsSidePanel = checksIfSupportsSidePanel() const supportsSidePanel = checksIfSupportsSidePanel()
......
...@@ -3,16 +3,15 @@ import 'src/app/Global.css' ...@@ -3,16 +3,15 @@ import 'src/app/Global.css'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import { RouterProvider, createHashRouter } from 'react-router-dom' import { RouterProvider, createHashRouter } from 'react-router-dom'
import { ErrorElement } from 'src/app/components/ErrorElement' import { ErrorElement } from 'src/app/components/ErrorElement'
import { BaseAppContainer } from 'src/app/core/BaseAppContainer' import { BaseAppContainer } from 'src/app/core/BaseAppContainer'
import { DatadogAppNameTag } from 'src/app/datadog' import { DatadogAppNameTag } from 'src/app/datadog'
import { initExtensionAnalytics } from 'src/app/utils/analytics' import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { Button, Flex, Image, Text } from 'ui/src' import { Button, Flex, Image, Text } from 'ui/src'
import { CHROME_LOGO, UNISWAP_LOGO } from 'ui/src/assets' import { UNISWAP_LOGO } from 'ui/src/assets'
import { GoogleChromeLogo } from 'ui/src/components/logos/GoogleChromeLogo'
import { iconSizes, spacing } from 'ui/src/theme' import { iconSizes, spacing } from 'ui/src/theme'
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 { ElementName } from 'uniswap/src/features/telemetry/constants' import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { ExtensionScreens } from 'uniswap/src/types/screens/extension' import { ExtensionScreens } from 'uniswap/src/types/screens/extension'
...@@ -28,11 +27,7 @@ const router = createHashRouter([ ...@@ -28,11 +27,7 @@ const router = createHashRouter([
function PopupContent(): JSX.Element { function PopupContent(): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const dispatch = useDispatch()
useEffect(() => {
dispatch(syncAppWithDeviceLanguage())
}, [dispatch])
useTestnetModeForLoggingAndAnalytics() useTestnetModeForLoggingAndAnalytics()
const searchParams = new URLSearchParams(window.location.search) const searchParams = new URLSearchParams(window.location.search)
...@@ -58,7 +53,7 @@ function PopupContent(): JSX.Element { ...@@ -58,7 +53,7 @@ function PopupContent(): JSX.Element {
position="absolute" position="absolute"
right={-spacing.spacing4} right={-spacing.spacing4}
> >
<Image height={iconSizes.icon12} source={CHROME_LOGO} width={iconSizes.icon12} /> <GoogleChromeLogo size={iconSizes.icon12} />
</Flex> </Flex>
</Flex> </Flex>
</Flex> </Flex>
......
...@@ -7,7 +7,6 @@ import { useDispatch } from 'react-redux' ...@@ -7,7 +7,6 @@ import { useDispatch } from 'react-redux'
import { RouterProvider, createHashRouter } from 'react-router-dom' import { RouterProvider, createHashRouter } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
import { ErrorElement } from 'src/app/components/ErrorElement' import { ErrorElement } from 'src/app/components/ErrorElement'
import { ScreenHeader } from 'src/app/components/layout/ScreenHeader'
import { BaseAppContainer } from 'src/app/core/BaseAppContainer' 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'
...@@ -23,6 +22,7 @@ import { RemoveRecoveryPhraseWallets } from 'src/app/features/settings/SettingsR ...@@ -23,6 +22,7 @@ import { RemoveRecoveryPhraseWallets } from 'src/app/features/settings/SettingsR
import { ViewRecoveryPhraseScreen } from 'src/app/features/settings/SettingsRecoveryPhraseScreen/ViewRecoveryPhraseScreen' import { ViewRecoveryPhraseScreen } from 'src/app/features/settings/SettingsRecoveryPhraseScreen/ViewRecoveryPhraseScreen'
import { SettingsScreen } from 'src/app/features/settings/SettingsScreen' import { SettingsScreen } from 'src/app/features/settings/SettingsScreen'
import { SettingsScreenWrapper } from 'src/app/features/settings/SettingsScreenWrapper' import { SettingsScreenWrapper } from 'src/app/features/settings/SettingsScreenWrapper'
import { SmartWalletSettingsScreen } from 'src/app/features/settings/SmartWalletSettingsScreen'
import { SettingsChangePasswordScreen } from 'src/app/features/settings/password/SettingsChangePasswordScreen' import { SettingsChangePasswordScreen } from 'src/app/features/settings/password/SettingsChangePasswordScreen'
import { SwapFlowScreen } from 'src/app/features/swap/SwapFlowScreen' import { SwapFlowScreen } from 'src/app/features/swap/SwapFlowScreen'
import { useIsWalletUnlocked } from 'src/app/hooks/useIsWalletUnlocked' import { useIsWalletUnlocked } from 'src/app/hooks/useIsWalletUnlocked'
...@@ -37,20 +37,15 @@ import { ...@@ -37,20 +37,15 @@ import {
} from 'src/background/messagePassing/messageChannels' } from 'src/background/messagePassing/messageChannels'
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 { TouchableArea } from 'ui/src'
import { QuestionInCircleFilled } from 'ui/src/components/icons'
import { useResetUnitagsQueries } from 'uniswap/src/data/apiClients/unitagsApi/useResetUnitagsQueries' import { useResetUnitagsQueries } from 'uniswap/src/data/apiClients/unitagsApi/useResetUnitagsQueries'
import { syncAppWithDeviceLanguage } from 'uniswap/src/features/settings/slice'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import i18n from 'uniswap/src/i18n'
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'
import { useInterval } from 'utilities/src/time/timing' import { useInterval } from 'utilities/src/time/timing'
import { SmartWalletSettings } from 'wallet/src/features/smartWallet/SmartWalletSettings'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics' import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics'
import { getReduxPersistor } from 'wallet/src/state/persistor'
const router = createHashRouter([ const router = createHashRouter([
{ {
...@@ -111,25 +106,7 @@ const router = createHashRouter([ ...@@ -111,25 +106,7 @@ const router = createHashRouter([
}, },
{ {
path: SettingsRoutes.SmartWallet, path: SettingsRoutes.SmartWallet,
element: ( element: <SmartWalletSettingsScreen />,
<SmartWalletSettings
Header={
<ScreenHeader
title={i18n.t('settings.setting.smartWallet.action.smartWallet')}
rightColumn={
<TouchableArea
alignItems="center"
alignSelf="center"
py="$spacing12"
// TODO: add modal + event
>
<QuestionInCircleFilled color="$neutral2" size="$icon.20" />
</TouchableArea>
}
/>
}
/>
),
}, },
], ],
}, },
...@@ -211,16 +188,11 @@ function useDappRequestPortListener(): void { ...@@ -211,16 +188,11 @@ function useDappRequestPortListener(): void {
} }
function SidebarWrapper(): JSX.Element { function SidebarWrapper(): JSX.Element {
const dispatch = useDispatch()
useDappRequestPortListener() useDappRequestPortListener()
useTestnetModeForLoggingAndAnalytics() useTestnetModeForLoggingAndAnalytics()
const resetUnitagsQueries = useResetUnitagsQueries() const resetUnitagsQueries = useResetUnitagsQueries()
useEffect(() => {
dispatch(syncAppWithDeviceLanguage())
}, [dispatch])
useEffect(() => { useEffect(() => {
return backgroundToSidePanelMessageChannel.addMessageListener( return backgroundToSidePanelMessageChannel.addMessageListener(
BackgroundToSidePanelRequestType.RefreshUnitags, BackgroundToSidePanelRequestType.RefreshUnitags,
......
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 { SwapDisplay } from 'src/app/features/dappRequests/requestContent/EthSend/Swap/SwapDisplay' import { SwapDisplay } from 'src/app/features/dappRequests/requestContent/EthSend/Swap/SwapDisplay'
import { ETH_ADDRESS } from 'src/app/features/dappRequests/requestContent/EthSend/Swap/constants'
import { formatUnits, useSwapDetails } from 'src/app/features/dappRequests/requestContent/EthSend/Swap/utils' import { formatUnits, useSwapDetails } from 'src/app/features/dappRequests/requestContent/EthSend/Swap/utils'
import { UniswapXSwapRequest } from 'src/app/features/dappRequests/types/Permit2Types' import { UniswapXSwapRequest } from 'src/app/features/dappRequests/types/Permit2Types'
import { UniversalRouterCall } from 'src/app/features/dappRequests/types/UniversalRouterTypes' import { UniversalRouterCall } from 'src/app/features/dappRequests/types/UniversalRouterTypes'
import { DEFAULT_NATIVE_ADDRESS } from 'uniswap/src/features/chains/chainInfo' import { DEFAULT_NATIVE_ADDRESS, DEFAULT_NATIVE_ADDRESS_LEGACY } from 'uniswap/src/features/chains/chainInfo'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { toSupportedChainId } from 'uniswap/src/features/chains/utils' import { toSupportedChainId } from 'uniswap/src/features/chains/utils'
import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types'
...@@ -116,7 +115,7 @@ export function UniswapXSwapRequestContent({ typedData }: { typedData: UniswapXS ...@@ -116,7 +115,7 @@ export function UniswapXSwapRequestContent({ typedData }: { typedData: UniswapXS
const { token: outputToken, startAmount: lastAmountOutParam } = typedData.message.witness.baseOutputs[0] const { token: outputToken, startAmount: lastAmountOutParam } = typedData.message.witness.baseOutputs[0]
const inputCurrencyInfo = useCurrencyInfo(buildCurrencyId(activeChain, inputToken)) const inputCurrencyInfo = useCurrencyInfo(buildCurrencyId(activeChain, inputToken))
const nativeEthOrOutputToken = outputToken === ETH_ADDRESS ? DEFAULT_NATIVE_ADDRESS : outputToken const nativeEthOrOutputToken = outputToken === DEFAULT_NATIVE_ADDRESS ? DEFAULT_NATIVE_ADDRESS_LEGACY : outputToken
const outputCurrencyInfo = useCurrencyInfo(buildCurrencyId(activeChain, nativeEthOrOutputToken)) const outputCurrencyInfo = useCurrencyInfo(buildCurrencyId(activeChain, nativeEthOrOutputToken))
assert( assert(
......
import { BigNumber } from '@ethersproject/bignumber' import { BigNumber } from '@ethersproject/bignumber'
export const CONTRACT_BALANCE = BigNumber.from(2).pow(255) export const CONTRACT_BALANCE = BigNumber.from(2).pow(255)
export const ETH_ADDRESS = '0x0000000000000000000000000000000000000000'
export const MAX_UINT256 = BigNumber.from(2).pow(256).sub(1) export const MAX_UINT256 = BigNumber.from(2).pow(256).sub(1)
export const MAX_UINT160 = BigNumber.from(2).pow(160).sub(1) export const MAX_UINT160 = BigNumber.from(2).pow(160).sub(1)
...@@ -5,7 +5,6 @@ import { formatUnits as formatUnitsEthers } from 'ethers/lib/utils' ...@@ -5,7 +5,6 @@ import { formatUnits as formatUnitsEthers } from 'ethers/lib/utils'
import { useDappLastChainId } from 'src/app/features/dapp/hooks' import { useDappLastChainId } from 'src/app/features/dapp/hooks'
import { import {
CONTRACT_BALANCE, CONTRACT_BALANCE,
ETH_ADDRESS,
MAX_UINT160, MAX_UINT160,
MAX_UINT256, MAX_UINT256,
} from 'src/app/features/dappRequests/requestContent/EthSend/Swap/constants' } from 'src/app/features/dappRequests/requestContent/EthSend/Swap/constants'
...@@ -30,7 +29,7 @@ import { ...@@ -30,7 +29,7 @@ import {
isUrCommandSweep, isUrCommandSweep,
isUrCommandUnwrapWeth, isUrCommandUnwrapWeth,
} from 'src/app/features/dappRequests/types/UniversalRouterTypes' } from 'src/app/features/dappRequests/types/UniversalRouterTypes'
import { DEFAULT_NATIVE_ADDRESS } from 'uniswap/src/features/chains/chainInfo' import { DEFAULT_NATIVE_ADDRESS, DEFAULT_NATIVE_ADDRESS_LEGACY } from 'uniswap/src/features/chains/chainInfo'
import { buildCurrencyId } from 'uniswap/src/utils/currencyId' import { buildCurrencyId } from 'uniswap/src/utils/currencyId'
import { assert } from 'utilities/src/errors' import { assert } from 'utilities/src/errors'
...@@ -70,8 +69,10 @@ export function useSwapDetails( ...@@ -70,8 +69,10 @@ export function useSwapDetails(
if (v4Command) { if (v4Command) {
// Extract details using the V4 helper // Extract details using the V4 helper
const v4Details = getTokenDetailsFromV4SwapCommands(v4Command, parsedCalldata.commands) const v4Details = getTokenDetailsFromV4SwapCommands(v4Command, parsedCalldata.commands)
inputAddress = v4Details.inputAddress === ETH_ADDRESS ? DEFAULT_NATIVE_ADDRESS : v4Details.inputAddress inputAddress =
outputAddress = v4Details.outputAddress === ETH_ADDRESS ? DEFAULT_NATIVE_ADDRESS : v4Details.outputAddress v4Details.inputAddress === DEFAULT_NATIVE_ADDRESS ? DEFAULT_NATIVE_ADDRESS_LEGACY : v4Details.inputAddress
outputAddress =
v4Details.outputAddress === DEFAULT_NATIVE_ADDRESS ? DEFAULT_NATIVE_ADDRESS_LEGACY : v4Details.outputAddress
inputValue = v4Details.inputValue || '0' inputValue = v4Details.inputValue || '0'
outputValue = v4Details.outputValue || '0' outputValue = v4Details.outputValue || '0'
} else { } else {
......
...@@ -500,7 +500,11 @@ export function* handleGetCapabilities(request: GetCapabilitiesRequest, senderTa ...@@ -500,7 +500,11 @@ export function* handleGetCapabilities(request: GetCapabilitiesRequest, senderTa
// https://linear.app/uniswap/issue/WALL-6679/implement-getcapabilities-on-extensionwc-instead-of-hardcoded-values // https://linear.app/uniswap/issue/WALL-6679/implement-getcapabilities-on-extensionwc-instead-of-hardcoded-values
response: { response: {
[`0x${UniverseChainId.Sepolia.toString(16)}`]: { atomic: { status: 'supported' } }, [`0x${UniverseChainId.Sepolia.toString(16)}`]: { atomic: { status: 'supported' } },
[`0x${UniverseChainId.Mainnet.toString(16)}`]: { atomic: { status: 'supported' } },
[`0x${UniverseChainId.UnichainSepolia.toString(16)}`]: { atomic: { status: 'supported' } }, [`0x${UniverseChainId.UnichainSepolia.toString(16)}`]: { atomic: { status: 'supported' } },
[`0x${UniverseChainId.Unichain.toString(16)}`]: { atomic: { status: 'supported' } },
[`0x${UniverseChainId.Optimism.toString(16)}`]: { atomic: { status: 'supported' } },
[`0x${UniverseChainId.Base.toString(16)}`]: { atomic: { status: 'supported' } },
}, },
} }
yield* call(dappResponseMessageChannel.sendMessageToTab, senderTabInfo.id, response) yield* call(dappResponseMessageChannel.sendMessageToTab, senderTabInfo.id, response)
......
...@@ -83,7 +83,7 @@ const RotatingSettingsIcon = ({ onPressSettings }: { onPressSettings(): void }): ...@@ -83,7 +83,7 @@ const RotatingSettingsIcon = ({ onPressSettings }: { onPressSettings(): void }):
<TouchableArea <TouchableArea
hoverable hoverable
borderRadius="$roundedFull" borderRadius="$roundedFull"
p="$spacing8" p="$spacing6"
onHoverIn={onBegin} onHoverIn={onBegin}
onHoverOut={onCancel} onHoverOut={onCancel}
onPress={onPressSettingsLocal} onPress={onPressSettingsLocal}
...@@ -152,7 +152,7 @@ export const PortfolioHeader = memo(function _PortfolioHeader({ address }: Portf ...@@ -152,7 +152,7 @@ export const PortfolioHeader = memo(function _PortfolioHeader({ address }: Portf
return ( return (
<Flex gap="$spacing8"> <Flex gap="$spacing8">
<Flex row justifyContent="space-between"> <Flex row justifyContent="space-between" alignItems="flex-start">
<TouchableArea pressStyle={{ scale: 0.95 }} onPress={onPressAccount}> <TouchableArea pressStyle={{ scale: 0.95 }} onPress={onPressAccount}>
<Flex group row alignItems="center" gap="$spacing4"> <Flex group row alignItems="center" gap="$spacing4">
<Flex $group-hover={{ opacity: 0.6 }}> <Flex $group-hover={{ opacity: 0.6 }}>
...@@ -163,7 +163,7 @@ export const PortfolioHeader = memo(function _PortfolioHeader({ address }: Portf ...@@ -163,7 +163,7 @@ export const PortfolioHeader = memo(function _PortfolioHeader({ address }: Portf
</Flex> </Flex>
</Flex> </Flex>
</TouchableArea> </TouchableArea>
<Flex row alignItems="center" gap="$spacing4" justifyContent="space-around"> <Flex row alignItems="center" gap="$spacing6" justifyContent="space-around">
{showConnectionStatus && ( {showConnectionStatus && (
<Popover <Popover
offset={10} offset={10}
...@@ -177,7 +177,7 @@ export const PortfolioHeader = memo(function _PortfolioHeader({ address }: Portf ...@@ -177,7 +177,7 @@ export const PortfolioHeader = memo(function _PortfolioHeader({ address }: Portf
}} }}
> >
<Popover.Trigger onPress={toggleConnectPopup}> <Popover.Trigger onPress={toggleConnectPopup}>
<TouchableArea hoverable borderRadius="$roundedFull" p="$spacing8"> <TouchableArea hoverable borderRadius="$roundedFull" p="$spacing6">
<ConnectionStatusIcon <ConnectionStatusIcon
dappIconUrl={dappIconUrl} dappIconUrl={dappIconUrl}
dappUrl={dappUrl} dappUrl={dappUrl}
......
...@@ -32,7 +32,7 @@ type TokenBalanceListProps = { ...@@ -32,7 +32,7 @@ type TokenBalanceListProps = {
export const TokenBalanceList = memo(function _TokenBalanceList({ owner }: TokenBalanceListProps): JSX.Element { export const TokenBalanceList = memo(function _TokenBalanceList({ owner }: TokenBalanceListProps): JSX.Element {
return ( return (
<Flex grow> <Flex grow>
<TokenBalanceListContextProvider isExternalProfile={false} owner={owner} onPressToken={() => {}}> <TokenBalanceListContextProvider isExternalProfile={false} owner={owner}>
<TokenBalanceListInner /> <TokenBalanceListInner />
</TokenBalanceListContextProvider> </TokenBalanceListContextProvider>
</Flex> </Flex>
...@@ -202,14 +202,14 @@ function TokenContextMenu({ ...@@ -202,14 +202,14 @@ function TokenContextMenu({
}: PropsWithChildren<{ }: PropsWithChildren<{
portfolioBalance: PortfolioBalance portfolioBalance: PortfolioBalance
}>): JSX.Element { }>): JSX.Element {
const contextMenu = useTokenContextMenu({ const { menuActions } = useTokenContextMenu({
currencyId: portfolioBalance.currencyInfo.currencyId, currencyId: portfolioBalance.currencyInfo.currencyId,
isBlocked: portfolioBalance.currencyInfo.safetyInfo?.tokenList === TokenList.Blocked, isBlocked: portfolioBalance.currencyInfo.safetyInfo?.tokenList === TokenList.Blocked,
tokenSymbolForNotification: portfolioBalance?.currencyInfo?.currency?.symbol, tokenSymbolForNotification: portfolioBalance?.currencyInfo?.currency?.symbol,
portfolioBalance, portfolioBalance,
}) })
const menuOptions = contextMenu.menuActions.map((action) => ({ const menuOptions = menuActions.map((action) => ({
label: action.title, label: action.title,
onPress: action.onPress, onPress: action.onPress,
Icon: action.Icon, Icon: action.Icon,
......
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useDispatch } from 'react-redux'
import { Outlet } from 'react-router-dom' import { Outlet } from 'react-router-dom'
import { DevMenuModal } from 'src/app/core/DevMenuModal' import { DevMenuModal } from 'src/app/core/DevMenuModal'
import { StorageWarningModal } from 'src/app/features/warnings/StorageWarningModal' import { StorageWarningModal } from 'src/app/features/warnings/StorageWarningModal'
...@@ -7,7 +6,6 @@ import { ONBOARDING_BACKGROUND_DARK, ONBOARDING_BACKGROUND_LIGHT } from 'src/ass ...@@ -7,7 +6,6 @@ import { ONBOARDING_BACKGROUND_DARK, ONBOARDING_BACKGROUND_LIGHT } from 'src/ass
import { onboardingMessageChannel } from 'src/background/messagePassing/messageChannels' import { onboardingMessageChannel } from 'src/background/messagePassing/messageChannels'
import { OnboardingMessageType } from 'src/background/messagePassing/types/ExtensionMessages' import { OnboardingMessageType } from 'src/background/messagePassing/types/ExtensionMessages'
import { Flex, Image, useIsDarkMode } from 'ui/src' import { Flex, Image, useIsDarkMode } from 'ui/src'
import { syncAppWithDeviceLanguage } from 'uniswap/src/features/settings/slice'
import { isProdEnv } from 'utilities/src/environment/env' import { isProdEnv } from 'utilities/src/environment/env'
import { OnboardingContextProvider } from 'wallet/src/features/onboarding/OnboardingContext' import { OnboardingContextProvider } from 'wallet/src/features/onboarding/OnboardingContext'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics' import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics'
...@@ -15,14 +13,9 @@ import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testne ...@@ -15,14 +13,9 @@ import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testne
export function OnboardingWrapper(): JSX.Element { export function OnboardingWrapper(): JSX.Element {
const isDarkMode = useIsDarkMode() const isDarkMode = useIsDarkMode()
const [isHighlighted, setIsHighlighted] = useState(false) const [isHighlighted, setIsHighlighted] = useState(false)
const dispatch = useDispatch()
useTestnetModeForLoggingAndAnalytics() useTestnetModeForLoggingAndAnalytics()
useEffect(() => {
dispatch(syncAppWithDeviceLanguage())
}, [dispatch])
useEffect(() => { useEffect(() => {
return onboardingMessageChannel.addMessageListener(OnboardingMessageType.HighlightOnboardingTab, (_message) => { return onboardingMessageChannel.addMessageListener(OnboardingMessageType.HighlightOnboardingTab, (_message) => {
// When the onboarding tab regains focus, we do a quick background change to bring attention to it. // When the onboarding tab regains focus, we do a quick background change to bring attention to it.
......
...@@ -151,12 +151,18 @@ export function SettingsScreen(): JSX.Element { ...@@ -151,12 +151,18 @@ export function SettingsScreen(): JSX.Element {
return ( return (
<Trace logImpression screen={ExtensionScreens.Settings}> <Trace logImpression screen={ExtensionScreens.Settings}>
{isLanguageModalOpen ? <SettingsLanguageModal onClose={() => setIsLanguageModalOpen(false)} /> : undefined} {isLanguageModalOpen ? (
<SettingsLanguageModal isOpen={isLanguageModalOpen} onClose={() => setIsLanguageModalOpen(false)} />
) : undefined}
{isPortfolioBalanceModalOpen ? ( {isPortfolioBalanceModalOpen ? (
<PortfolioBalanceModal onClose={() => setIsPortfolioBalanceModalOpen(false)} /> <PortfolioBalanceModal
isOpen={isPortfolioBalanceModalOpen}
onClose={() => setIsPortfolioBalanceModalOpen(false)}
/>
) : undefined} ) : undefined}
{isPermissionsModalOpen ? ( {isPermissionsModalOpen ? (
<PermissionsModal <PermissionsModal
isOpen={isPermissionsModalOpen}
handleDefaultBrowserToggle={handleDefaultBrowserToggle} handleDefaultBrowserToggle={handleDefaultBrowserToggle}
isDefaultBrowserProvider={isDefaultProvider} isDefaultBrowserProvider={isDefaultProvider}
onClose={() => setIsPermissionsModalOpen(false)} onClose={() => setIsPermissionsModalOpen(false)}
......
import { useTranslation } from 'react-i18next'
import { ScreenHeader } from 'src/app/components/layout/ScreenHeader'
import { Flex } from 'ui/src'
import {
SmartWalletHelpIcon,
SmartWalletSettingsContent,
} from 'wallet/src/features/smartWallet/SmartWalletSettingsContent'
export function SmartWalletSettingsScreen(): JSX.Element {
const { t } = useTranslation()
return (
<Flex fill gap="$gap16">
<ScreenHeader
title={t('settings.setting.smartWallet.action.smartWallet')}
rightColumn={<SmartWalletHelpIcon />}
/>
<SmartWalletSettingsContent />
</Flex>
)
}
import { useState } from 'react' import { useCallback, useState } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { useSmartWalletNudges } from 'src/app/context/SmartWalletNudgesContext'
import { useExtensionNavigation } from 'src/app/navigation/utils' import { useExtensionNavigation } from 'src/app/navigation/utils'
import { Flex } from 'ui/src' import { Flex } from 'ui/src'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { useHighestBalanceNativeCurrencyId } from 'uniswap/src/features/dataApi/balances' import { useHighestBalanceNativeCurrencyId } from 'uniswap/src/features/dataApi/balances'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { selectFilteredChainIds } from 'uniswap/src/features/transactions/swap/contexts/selectors' import { selectFilteredChainIds } from 'uniswap/src/features/transactions/swap/contexts/selectors'
import { useSwapPrefilledState } from 'uniswap/src/features/transactions/swap/form/hooks/useSwapPrefilledState' import { useSwapPrefilledState } from 'uniswap/src/features/transactions/swap/form/hooks/useSwapPrefilledState'
import { prepareSwapFormState } from 'uniswap/src/features/transactions/types/transactionState' import { prepareSwapFormState } from 'uniswap/src/features/transactions/types/transactionState'
import { CurrencyField } from 'uniswap/src/types/currency' import { CurrencyField } from 'uniswap/src/types/currency'
import { WalletSwapFlow } from 'wallet/src/features/transactions/swap/WalletSwapFlow' import { WalletSwapFlow } from 'wallet/src/features/transactions/swap/WalletSwapFlow'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' import { useActiveAccountWithThrow, useHasSmartWalletConsent } from 'wallet/src/features/wallet/hooks'
export function SwapFlowScreen(): JSX.Element { export function SwapFlowScreen(): JSX.Element {
const { navigateBack, locationState } = useExtensionNavigation() const { navigateBack, locationState } = useExtensionNavigation()
...@@ -32,9 +34,25 @@ export function SwapFlowScreen(): JSX.Element { ...@@ -32,9 +34,25 @@ export function SwapFlowScreen(): JSX.Element {
const swapPrefilledState = useSwapPrefilledState(initialTransactionState) const swapPrefilledState = useSwapPrefilledState(initialTransactionState)
const { openModal, setDappInfo } = useSmartWalletNudges()
const hasSmartWalletConsent = useHasSmartWalletConsent()
const onSubmitSwap = useCallback(async () => {
// TODO(WALL-6765): check if wallet is already delegated
if (hasSmartWalletConsent === false) {
openModal(ModalName.PostSwapSmartWalletNudge)
setDappInfo(undefined)
}
}, [openModal, hasSmartWalletConsent, setDappInfo])
return ( return (
<Flex fill p="$spacing12"> <Flex fill p="$spacing12">
<WalletSwapFlow prefilledState={swapPrefilledState} walletNeedsRestore={false} onClose={navigateBack} /> <WalletSwapFlow
prefilledState={swapPrefilledState}
walletNeedsRestore={false}
onClose={navigateBack}
onSubmitSwap={onSubmitSwap}
/>
</Flex> </Flex>
) )
} }
import { useCallback, useMemo, useRef } from 'react' import { useCallback, useMemo, useRef } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { NavigationType, Outlet, ScrollRestoration, useLocation } from 'react-router-dom' import { NavigationType, Outlet, ScrollRestoration, useLocation } from 'react-router-dom'
import { SmartWalletNudgeModals } from 'src/app/components/modals/SmartWalletNudgeModals'
import { DappRequestQueue } from 'src/app/features/dappRequests/DappRequestQueue' import { DappRequestQueue } from 'src/app/features/dappRequests/DappRequestQueue'
import { ForceUpgradeModal } from 'src/app/features/forceUpgrade/ForceUpgradeModal' import { ForceUpgradeModal } from 'src/app/features/forceUpgrade/ForceUpgradeModal'
import { HomeScreen } from 'src/app/features/home/HomeScreen' import { HomeScreen } from 'src/app/features/home/HomeScreen'
...@@ -219,6 +220,8 @@ function LoggedIn(): JSX.Element { ...@@ -219,6 +220,8 @@ function LoggedIn(): JSX.Element {
{isChromeWindowFocused && <TransactionHistoryUpdater />} {isChromeWindowFocused && <TransactionHistoryUpdater />}
<DappRequestQueue /> <DappRequestQueue />
<SmartWalletNudgeModals />
</> </>
) )
} }
......
...@@ -2,9 +2,9 @@ import { initDappStore } from 'src/app/features/dapp/saga' ...@@ -2,9 +2,9 @@ import { initDappStore } from 'src/app/features/dapp/saga'
import { dappRequestApprovalWatcher } from 'src/app/features/dappRequests/dappRequestApprovalWatcherSaga' import { dappRequestApprovalWatcher } from 'src/app/features/dappRequests/dappRequestApprovalWatcherSaga'
import { dappRequestWatcher } from 'src/app/features/dappRequests/saga' import { dappRequestWatcher } from 'src/app/features/dappRequests/saga'
import { call, spawn } from 'typed-redux-saga' import { call, spawn } from 'typed-redux-saga'
import { appLanguageWatcherSaga } from 'uniswap/src/features/language/saga'
import { apolloClientRef } from 'wallet/src/data/apollo/usePersistedApolloClient' import { apolloClientRef } from 'wallet/src/data/apollo/usePersistedApolloClient'
import { authActions, authReducer, authSaga, authSagaName } from 'wallet/src/features/auth/saga' import { authActions, authReducer, authSaga, authSagaName } from 'wallet/src/features/auth/saga'
import { deviceLocaleWatcher } from 'wallet/src/features/i18n/deviceLocaleWatcherSaga'
import { initProviders } from 'wallet/src/features/providers/saga' import { initProviders } from 'wallet/src/features/providers/saga'
import { swapActions, swapReducer, swapSaga, swapSagaName } from 'wallet/src/features/transactions/swap/swapSaga' import { swapActions, swapReducer, swapSaga, swapSagaName } from 'wallet/src/features/transactions/swap/swapSaga'
import { import {
...@@ -13,7 +13,8 @@ import { ...@@ -13,7 +13,8 @@ import {
tokenWrapSaga, tokenWrapSaga,
tokenWrapSagaName, tokenWrapSagaName,
} from 'wallet/src/features/transactions/swap/wrapSaga' } from 'wallet/src/features/transactions/swap/wrapSaga'
import { transactionWatcher, watchTransactionEvents } from 'wallet/src/features/transactions/transactionWatcherSaga' import { watchTransactionEvents } from 'wallet/src/features/transactions/watcher/transactionFinalizationSaga'
import { transactionWatcher } from 'wallet/src/features/transactions/watcher/transactionWatcherSaga'
import { import {
editAccountActions, editAccountActions,
editAccountReducer, editAccountReducer,
...@@ -63,12 +64,12 @@ export const monitoredSagas: Record<string, MonitoredSaga> = { ...@@ -63,12 +64,12 @@ export const monitoredSagas: Record<string, MonitoredSaga> = {
} as const } as const
const sagasInitializedOnStartup = [ const sagasInitializedOnStartup = [
appLanguageWatcherSaga,
initDappStore, initDappStore,
dappRequestApprovalWatcher, dappRequestApprovalWatcher,
dappRequestWatcher, dappRequestWatcher,
initProviders, initProviders,
watchTransactionEvents, watchTransactionEvents,
deviceLocaleWatcher,
] as const ] as const
export const monitoredSagaReducers = getMonitoredSagaReducers(monitoredSagas) export const monitoredSagaReducers = getMonitoredSagaReducers(monitoredSagas)
......
import { isOnboardedSelector } from 'src/app/utils/isOnboardedSelector' import { isOnboardedSelector } from 'src/app/utils/isOnboardedSelector'
import { STATE_STORAGE_KEY } from 'src/store/constants' import { STATE_STORAGE_KEY } from 'src/store/constants'
import { ExtensionState } from 'src/store/extensionReducer' import { ExtensionState } from 'src/store/extensionReducer'
import { readDeprecatedReduxedChromeStorage } from 'src/store/reduxedChromeStorageToReduxPersistMigration'
export async function readReduxStateFromStorage(storageChanges?: { export async function readReduxStateFromStorage(storageChanges?: {
[key: string]: chrome.storage.StorageChange [key: string]: chrome.storage.StorageChange
...@@ -25,15 +24,6 @@ export async function readReduxStateFromStorage(storageChanges?: { ...@@ -25,15 +24,6 @@ export async function readReduxStateFromStorage(storageChanges?: {
} }
export async function readIsOnboardedFromStorage(): Promise<boolean> { export async function readIsOnboardedFromStorage(): Promise<boolean> {
// The migration will happen in the sidebar, not in the background script, const state = await readReduxStateFromStorage()
// because the background script never persists the state (only reads it).
// So we need to check both the old and new storage keys to avoid the onboarding
// flow re-opening the first time the migration needs to run.
const [oldReduxedChromeStorageState, newReduxPersistState] = await Promise.all([
readDeprecatedReduxedChromeStorage(),
readReduxStateFromStorage(),
])
const state = oldReduxedChromeStorageState ?? newReduxPersistState
return state ? isOnboardedSelector(state) : false return state ? isOnboardedSelector(state) : false
} }
...@@ -4,19 +4,16 @@ ...@@ -4,19 +4,16 @@
import React from 'react' import React from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import OnboardingApp from 'src/app/core/OnboardingApp' import OnboardingApp from 'src/app/core/OnboardingApp'
import { initializeReduxStore } from 'src/store/store'
import { ExtensionAppLocation, StoreSynchronization } from 'src/store/storeSynchronization' import { ExtensionAppLocation, StoreSynchronization } from 'src/store/storeSynchronization'
import { logger } from 'utilities/src/logger/logger'
;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any ;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any
// The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem // The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem
// see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326 // see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326
async function initOnboarding(): Promise<void> { function initOnboarding() {
await initializeReduxStore()
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const container = document.getElementById('onboarding-root')! const container = document.getElementById('onboarding-root')!
const root = createRoot(container) const root = createRoot(container)
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<OnboardingApp /> <OnboardingApp />
...@@ -24,20 +21,6 @@ async function initOnboarding(): Promise<void> { ...@@ -24,20 +21,6 @@ async function initOnboarding(): Promise<void> {
) )
} }
StoreSynchronization.init(ExtensionAppLocation.Tab).catch((error) => { StoreSynchronization.init(ExtensionAppLocation.Tab)
logger.error(error, {
tags: {
file: 'onboarding.ts',
function: 'initPrimaryInstanceHandler',
},
})
})
initOnboarding().catch((error) => { initOnboarding()
logger.error(error, {
tags: {
file: 'onboarding.ts',
function: 'initOnboarding',
},
})
})
...@@ -5,14 +5,11 @@ import { StrictMode } from 'react' ...@@ -5,14 +5,11 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import PopupApp from 'src/app/core/PopupApp' import PopupApp from 'src/app/core/PopupApp'
import { initializeReduxStore } from 'src/store/store' import { initializeReduxStore } from 'src/store/store'
import { logger } from 'utilities/src/logger/logger'
;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any ;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any
// The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem // The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem
// see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326 // see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326
async function initPopup(): Promise<void> { function initPopup() {
await initializeReduxStore({ readOnly: true })
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const container = document.getElementById('popup-root')! const container = document.getElementById('popup-root')!
const root = createRoot(container) const root = createRoot(container)
...@@ -24,11 +21,6 @@ async function initPopup(): Promise<void> { ...@@ -24,11 +21,6 @@ async function initPopup(): Promise<void> {
) )
} }
initPopup().catch((error) => { initializeReduxStore({ readOnly: true })
logger.error(error, {
tags: { initPopup()
file: 'popup.tsx',
function: 'initPopup',
},
})
})
...@@ -9,7 +9,6 @@ import { createRoot } from 'react-dom/client' ...@@ -9,7 +9,6 @@ import { createRoot } from 'react-dom/client'
import SidebarApp from 'src/app/core/SidebarApp' import SidebarApp from 'src/app/core/SidebarApp'
import { onboardingMessageChannel } from 'src/background/messagePassing/messageChannels' import { onboardingMessageChannel } from 'src/background/messagePassing/messageChannels'
import { OnboardingMessageType } from 'src/background/messagePassing/types/ExtensionMessages' import { OnboardingMessageType } from 'src/background/messagePassing/types/ExtensionMessages'
import { initializeReduxStore } from 'src/store/store'
import { ExtensionAppLocation, StoreSynchronization } from 'src/store/storeSynchronization' import { ExtensionAppLocation, StoreSynchronization } from 'src/store/storeSynchronization'
import { initializeScrollWatcher } from 'uniswap/src/components/modals/ScrollLock' import { initializeScrollWatcher } from 'uniswap/src/components/modals/ScrollLock'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
...@@ -17,15 +16,24 @@ import { logger } from 'utilities/src/logger/logger' ...@@ -17,15 +16,24 @@ import { logger } from 'utilities/src/logger/logger'
// The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem // The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem
// see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326 // see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326
async function initSidebar(): Promise<void> { function initSidebar(): void {
await initializeReduxStore() onboardingMessageChannel
await onboardingMessageChannel.sendMessage({ .sendMessage({
type: OnboardingMessageType.SidebarOpened, type: OnboardingMessageType.SidebarOpened,
}) })
.catch((error) => {
logger.error(error, {
tags: {
file: 'sidebar.ts',
function: 'onboardingMessageChannel.sendMessage',
},
})
})
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const container = window.document.querySelector('#root')! const container = window.document.querySelector('#root')!
const root = createRoot(container) const root = createRoot(container)
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<SidebarApp /> <SidebarApp />
...@@ -33,22 +41,8 @@ async function initSidebar(): Promise<void> { ...@@ -33,22 +41,8 @@ async function initSidebar(): Promise<void> {
) )
} }
StoreSynchronization.init(ExtensionAppLocation.SidePanel).catch((error) => { StoreSynchronization.init(ExtensionAppLocation.SidePanel)
logger.error(error, {
tags: { initSidebar()
file: 'sidebar.ts',
function: 'initPrimaryInstanceHandler',
},
})
})
initSidebar().catch((error) => {
logger.error(error, {
tags: {
file: 'sidebar.ts',
function: 'initSidebar',
},
})
})
initializeScrollWatcher() initializeScrollWatcher()
...@@ -5,14 +5,11 @@ import { StrictMode } from 'react' ...@@ -5,14 +5,11 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import UnitagClaimApp from 'src/app/core/UnitagClaimApp' import UnitagClaimApp from 'src/app/core/UnitagClaimApp'
import { initializeReduxStore } from 'src/store/store' import { initializeReduxStore } from 'src/store/store'
import { logger } from 'utilities/src/logger/logger'
;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any ;(globalThis as any).regeneratorRuntime = undefined // eslint-disable-line @typescript-eslint/no-explicit-any
// The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem // The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem
// see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326 // see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326
async function initUnitagClaim(): Promise<void> { function initUnitagClaim(): void {
await initializeReduxStore({ readOnly: true })
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const container = document.getElementById('unitag-claim-root')! const container = document.getElementById('unitag-claim-root')!
const root = createRoot(container) const root = createRoot(container)
...@@ -24,11 +21,6 @@ async function initUnitagClaim(): Promise<void> { ...@@ -24,11 +21,6 @@ async function initUnitagClaim(): Promise<void> {
) )
} }
initUnitagClaim().catch((error) => { initializeReduxStore({ readOnly: true })
logger.error(error, {
tags: { initUnitagClaim()
file: 'unitagClaim.tsx',
function: 'initUnitagClaim',
},
})
})
import { ExtensionState } from 'src/store/extensionReducer'
// TODO(EXT-1028): remove this file once the migration is no longer needed.
const REDUXED_STORAGE_KEY = 'reduxed'
// These functions are used to migrate the redux state persistence from `reduxed-chrome-storage` to `redux-persist`.
// The actual migration happens when the sidebar initializes the redux store. See `initializeReduxStore` in `store.ts`.
export async function readDeprecatedReduxedChromeStorage(): Promise<ExtensionState | undefined> {
const reduxedArray = (await chrome.storage.local.get(REDUXED_STORAGE_KEY))?.[REDUXED_STORAGE_KEY]
if (!reduxedArray) {
return undefined
}
// The `reduxed` storage is an array: [id, timestamp, state]
const [, , state] = reduxedArray
if (!state) {
return undefined
}
return state as ExtensionState
}
export async function deleteDeprecatedReduxedChromeStorage(): Promise<void> {
await chrome.storage.local.remove(REDUXED_STORAGE_KEY)
}
import { PreloadedState } from 'redux'
import { persistReducer, persistStore } from 'redux-persist' import { persistReducer, persistStore } from 'redux-persist'
import { localStorage } from 'redux-persist-webextension-storage' import { localStorage } from 'redux-persist-webextension-storage'
import { rootExtensionSaga } from 'src/app/saga' import { rootExtensionSaga } from 'src/app/saga'
...@@ -7,15 +6,13 @@ import { PERSIST_KEY } from 'src/store/constants' ...@@ -7,15 +6,13 @@ import { PERSIST_KEY } from 'src/store/constants'
import { enhancePersistReducer } from 'src/store/enhancePersistReducer' import { enhancePersistReducer } from 'src/store/enhancePersistReducer'
import { ExtensionState, extensionPersistedStateList, extensionReducer } from 'src/store/extensionReducer' import { ExtensionState, extensionPersistedStateList, extensionReducer } from 'src/store/extensionReducer'
import { EXTENSION_STATE_VERSION, migrations } from 'src/store/migrations' import { EXTENSION_STATE_VERSION, migrations } from 'src/store/migrations'
import {
deleteDeprecatedReduxedChromeStorage,
readDeprecatedReduxedChromeStorage,
} from 'src/store/reduxedChromeStorageToReduxPersistMigration'
import { fiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api' import { fiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api'
import { delegationListenerMiddleware } from 'uniswap/src/features/smartWallet/delegation/slice' import { delegationListenerMiddleware } from 'uniswap/src/features/smartWallet/delegation/slice'
import { createDatadogReduxEnhancer } from 'utilities/src/logger/datadog/Datadog' import { createDatadogReduxEnhancer } from 'utilities/src/logger/datadog/Datadog'
import { logger } from 'utilities/src/logger/logger'
import { createStore } from 'wallet/src/state' import { createStore } from 'wallet/src/state'
import { createMigrate } from 'wallet/src/state/createMigrate' import { createMigrate } from 'wallet/src/state/createMigrate'
import { setReduxPersistor } from 'wallet/src/state/persistor'
const persistConfig = { const persistConfig = {
key: PERSIST_KEY, key: PERSIST_KEY,
...@@ -34,10 +31,9 @@ const dataDogReduxEnhancer = createDatadogReduxEnhancer({ ...@@ -34,10 +31,9 @@ const dataDogReduxEnhancer = createDatadogReduxEnhancer({
}, },
}) })
const setupStore = (preloadedState?: PreloadedState<ExtensionState>): ReturnType<typeof createStore> => { const setupStore = (): ReturnType<typeof createStore> => {
return createStore({ return createStore({
reducer: persistedReducer, reducer: persistedReducer,
preloadedState,
additionalSagas: [rootExtensionSaga], additionalSagas: [rootExtensionSaga],
middlewareBefore: __DEV__ ? [loggerMiddleware] : [], middlewareBefore: __DEV__ ? [loggerMiddleware] : [],
middlewareAfter: [fiatOnRampAggregatorApi.middleware, delegationListenerMiddleware.middleware], middlewareAfter: [fiatOnRampAggregatorApi.middleware, delegationListenerMiddleware.middleware],
...@@ -46,32 +42,29 @@ const setupStore = (preloadedState?: PreloadedState<ExtensionState>): ReturnType ...@@ -46,32 +42,29 @@ const setupStore = (preloadedState?: PreloadedState<ExtensionState>): ReturnType
} }
let store: ReturnType<typeof setupStore> | undefined let store: ReturnType<typeof setupStore> | undefined
let persistor: ReturnType<typeof persistStore> | undefined
export async function initializeReduxStore(args?: { readOnly?: boolean }): Promise<{ export function initializeReduxStore(args?: { readOnly?: boolean }): void {
store: ReturnType<typeof setupStore> if (store) {
persistor: ReturnType<typeof persistStore> // This should never happen. It's only here to alert us if a bug is introduced in the future.
}> { logger.error(new Error('`initializeReduxStore` called when already initialized'), {
// Migrate the old `reduxed-chrome-storage` persisted state to `redux-persist`. tags: {
// TODO(EXT-985): we might need to pass the old store through `createMigrations` when we implement migrations. file: 'store.ts',
const oldStore = await readDeprecatedReduxedChromeStorage() function: 'initializeReduxStore',
},
})
store = setupStore(oldStore) return
persistor = persistStore(store) }
store = setupStore()
const persistor = persistStore(store)
setReduxPersistor(persistor)
if (args?.readOnly) { if (args?.readOnly) {
// This means the store will be initialized with the persisted state from disk, but it won't persist any changes. // This means the store will be initialized with the persisted state from disk, but it won't persist any changes.
// Only useful for use cases where we don't want to modify the state (for example, a popup window instead of the sidebar). // Only useful for use cases where we don't want to modify the state (for example, a popup window instead of the sidebar).
persistor.pause() persistor.pause()
} }
// We wait a few seconds to make sure the store is fully initialized and persisted before deleting the old storage.
// This is needed because otherwise the background script might think the user is not onboarded if it reads the storage while it's being migrated.
if (oldStore) {
setTimeout(deleteDeprecatedReduxedChromeStorage, 5000)
}
return { store, persistor }
} }
export function getReduxStore(): ReturnType<typeof setupStore> { export function getReduxStore(): ReturnType<typeof setupStore> {
...@@ -81,11 +74,4 @@ export function getReduxStore(): ReturnType<typeof setupStore> { ...@@ -81,11 +74,4 @@ export function getReduxStore(): ReturnType<typeof setupStore> {
return store return store
} }
export function getReduxPersistor(): ReturnType<typeof persistStore> {
if (!persistor) {
throw new Error('Invalid call to `getReduxPersistor` before store has been initialized')
}
return persistor
}
export type AppStore = ReturnType<typeof setupStore> export type AppStore = ReturnType<typeof setupStore>
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { getReduxPersistor, initializeReduxStore } from 'src/store/store' import { initializeReduxStore } from 'src/store/store'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { getReduxPersistor } from 'wallet/src/state/persistor'
import { PersistedStorage } from 'wallet/src/utils/persistedStorage' import { PersistedStorage } from 'wallet/src/utils/persistedStorage'
/** /**
...@@ -32,7 +33,7 @@ export enum ExtensionAppLocation { ...@@ -32,7 +33,7 @@ export enum ExtensionAppLocation {
Tab = 1, Tab = 1,
} }
async function initPrimaryInstanceHandler(appLocation: ExtensionAppLocation): Promise<void> { function initPrimaryInstanceHandler(appLocation: ExtensionAppLocation): void {
if (isInitialized) { if (isInitialized) {
// This is just to prevent bugs being introduced in the future. // This is just to prevent bugs being introduced in the future.
logger.error(new Error('`initPrimaryInstanceHandler` called when already initialized'), { logger.error(new Error('`initPrimaryInstanceHandler` called when already initialized'), {
...@@ -44,7 +45,7 @@ async function initPrimaryInstanceHandler(appLocation: ExtensionAppLocation): Pr ...@@ -44,7 +45,7 @@ async function initPrimaryInstanceHandler(appLocation: ExtensionAppLocation): Pr
return return
} }
await initializeReduxStore() initializeReduxStore()
const onStorageChangedListener: Parameters<typeof chrome.storage.onChanged.addListener>[0] = async ( const onStorageChangedListener: Parameters<typeof chrome.storage.onChanged.addListener>[0] = async (
changes, changes,
...@@ -103,7 +104,11 @@ async function initPrimaryInstanceHandler(appLocation: ExtensionAppLocation): Pr ...@@ -103,7 +104,11 @@ async function initPrimaryInstanceHandler(appLocation: ExtensionAppLocation): Pr
window.addEventListener('focus', onWindowFocusListener) window.addEventListener('focus', onWindowFocusListener)
// We always set the current app instance as the primary when it first launches. // We always set the current app instance as the primary when it first launches.
await sessionStorage.setItem(PRIMARY_APP_INSTANCE_ID_KEY, currentAppInstanceId) sessionStorage.setItem(PRIMARY_APP_INSTANCE_ID_KEY, currentAppInstanceId).catch((error) => {
logger.error(error, {
tags: { file: 'storeSynchronization.ts', function: 'sessionStorage.setItem' },
})
})
// This will be used in the onboarding flow when the user completes onboarding but the tab remains open. // This will be used in the onboarding flow when the user completes onboarding but the tab remains open.
// We don't want this tab to become the primary ever again when it's focused. // We don't want this tab to become the primary ever again when it's focused.
......
flows: flows:
- 'flows/onboarding/*' - 'flows/onboarding/*'
- 'flows/swap/*' - 'flows/swap/*'
- 'flows/restore/*'
baselineBranch: main baselineBranch: main
executionOrder: executionOrder:
continueOnFailure: true continueOnFailure: true
appId: com.uniswap.mobile.dev
---
- runFlow: ../../shared-flows/start.yaml
- runFlow: ../../shared-flows/recover-fast.yaml
- tapOn:
id: 'account-header-settings-icon'
- waitForAnimationToEnd
- swipe:
direction: 'up'
- waitForAnimationToEnd
- tapOn:
id: 'app-settings-dev-modal'
- waitForAnimationToEnd
- tapOn:
id: 'seed-phrase-private-keys-accordion'
- tapOn:
id: 'delete-seed-phrase-button'
- tapOn: 'Delete'
- tapOn:
id: 'delete-private-keys-button'
- tapOn: 'Delete'
- killApp
- launchApp
- waitForAnimationToEnd
- assertVisible:
text: 'Recover your wallet'
- assertVisible:
id: 'continue'
- assertNotVisible:
id: 'cancel'
- tapOn:
id: 'continue'
- waitForAnimationToEnd
- back
- waitForAnimationToEnd
- extendedWaitUntil:
visible:
text: 'No backups found'
timeout: 5000 # wait for cloud backup to fail
- inputText: ${E2E_RECOVERY_PHRASE}
- tapOn:
id: 'continue'
- waitForAnimationToEnd
- assertVisible:
id: 'account-header-avatar'
...@@ -4,6 +4,10 @@ appId: com.uniswap.mobile.dev ...@@ -4,6 +4,10 @@ appId: com.uniswap.mobile.dev
env: env:
E2E_RECOVERY_PHRASE: ${E2E_RECOVERY_PHRASE} E2E_RECOVERY_PHRASE: ${E2E_RECOVERY_PHRASE}
--- ---
- extendedWaitUntil:
visible:
id: ${output.testIds.ImportAccount}
timeout: 30000 # Starting the app during local dev loads the JS bundle which can take much longer than a normal build
- tapOn: - tapOn:
id: ${output.testIds.ImportAccount} id: ${output.testIds.ImportAccount}
- waitForAnimationToEnd - waitForAnimationToEnd
......
...@@ -22,7 +22,7 @@ import { AppModals } from 'src/app/modals/AppModals' ...@@ -22,7 +22,7 @@ import { AppModals } from 'src/app/modals/AppModals'
import { NavigationContainer } from 'src/app/navigation/NavigationContainer' import { NavigationContainer } from 'src/app/navigation/NavigationContainer'
import { useIsPartOfNavigationTree } from 'src/app/navigation/hooks' import { useIsPartOfNavigationTree } from 'src/app/navigation/hooks'
import { AppStackNavigator } from 'src/app/navigation/navigation' import { AppStackNavigator } from 'src/app/navigation/navigation'
import { persistor, store } from 'src/app/store' import { store } from 'src/app/store'
import { TraceUserProperties } from 'src/components/Trace/TraceUserProperties' import { TraceUserProperties } from 'src/components/Trace/TraceUserProperties'
import { OfflineBanner } from 'src/components/banners/OfflineBanner' import { OfflineBanner } from 'src/components/banners/OfflineBanner'
import { initAppsFlyer } from 'src/features/analytics/appsflyer' import { initAppsFlyer } from 'src/features/analytics/appsflyer'
...@@ -66,7 +66,6 @@ import { StatsigUser, Storage, getStatsigClient } from 'uniswap/src/features/gat ...@@ -66,7 +66,6 @@ import { StatsigUser, Storage, getStatsigClient } from 'uniswap/src/features/gat
import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext' import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext'
import { useCurrentLanguageInfo } from 'uniswap/src/features/language/hooks' import { useCurrentLanguageInfo } from 'uniswap/src/features/language/hooks'
import { clearNotificationQueue } from 'uniswap/src/features/notifications/slice' import { clearNotificationQueue } from 'uniswap/src/features/notifications/slice'
import { syncAppWithDeviceLanguage } from 'uniswap/src/features/settings/slice'
import Trace from 'uniswap/src/features/telemetry/Trace' import 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'
...@@ -82,6 +81,7 @@ import { isIOS } from 'utilities/src/platform' ...@@ -82,6 +81,7 @@ import { isIOS } from 'utilities/src/platform'
import { useAsyncData } from 'utilities/src/react/hooks' import { useAsyncData } from 'utilities/src/react/hooks'
import { AnalyticsNavigationContextProvider } from 'utilities/src/telemetry/trace/AnalyticsNavigationContext' import { AnalyticsNavigationContextProvider } from 'utilities/src/telemetry/trace/AnalyticsNavigationContext'
import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary' import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary'
import { getReduxPersistor } from 'wallet/src/state/persistor'
// eslint-disable-next-line @typescript-eslint/no-restricted-imports // eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { usePersistedApolloClient } from 'wallet/src/data/apollo/usePersistedApolloClient' import { usePersistedApolloClient } from 'wallet/src/data/apollo/usePersistedApolloClient'
import { useCurrentAppearanceSetting } from 'wallet/src/features/appearance/hooks' import { useCurrentAppearanceSetting } from 'wallet/src/features/appearance/hooks'
...@@ -259,7 +259,7 @@ function AppOuter(): JSX.Element | null { ...@@ -259,7 +259,7 @@ function AppOuter(): JSX.Element | null {
return ( return (
<ApolloProvider client={client}> <ApolloProvider client={client}>
<PersistGate loading={null} persistor={persistor}> <PersistGate loading={null} persistor={getReduxPersistor()}>
<ErrorBoundary> <ErrorBoundary>
<BlankUrlProvider> <BlankUrlProvider>
<LocalizationContextProvider> <LocalizationContextProvider>
...@@ -315,7 +315,6 @@ function AppInner(): JSX.Element { ...@@ -315,7 +315,6 @@ function AppInner(): JSX.Element {
useEffect(() => { useEffect(() => {
dispatch(clearNotificationQueue()) // clear all in-app toasts on app start dispatch(clearNotificationQueue()) // clear all in-app toasts on app start
dispatch(syncAppWithDeviceLanguage())
}, [dispatch]) }, [dispatch])
useEffect(() => { useEffect(() => {
......
...@@ -6,6 +6,8 @@ import { navigate } from 'src/app/navigation/rootNavigation' ...@@ -6,6 +6,8 @@ import { navigate } from 'src/app/navigation/rootNavigation'
import { AccountList } from 'src/components/accounts/AccountList' import { AccountList } from 'src/components/accounts/AccountList'
import { checkCloudBackupOrShowAlert } from 'src/components/mnemonic/cloudImportUtils' import { checkCloudBackupOrShowAlert } from 'src/components/mnemonic/cloudImportUtils'
import { useReactNavigationModal } from 'src/components/modals/useReactNavigationModal' import { useReactNavigationModal } from 'src/components/modals/useReactNavigationModal'
import { WalletRestoreType } from 'src/components/RestoreWalletModal/RestoreWalletModalState'
import { useWalletRestore } from 'src/features/wallet/useWalletRestore'
import { Button, Flex, Text, TouchableArea, useSporeColors } from 'ui/src' import { Button, Flex, Text, TouchableArea, useSporeColors } from 'ui/src'
import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions'
import { spacing } from 'ui/src/theme' import { spacing } from 'ui/src/theme'
...@@ -55,6 +57,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme ...@@ -55,6 +57,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
const dispatch = useDispatch() const dispatch = useDispatch()
const hasImportedSeedPhrase = useNativeAccountExists() const hasImportedSeedPhrase = useNativeAccountExists()
const isModalOpen = useIsFocused() const isModalOpen = useIsFocused()
const { openWalletRestoreModal, walletRestoreType } = useWalletRestore()
const sortedMnemonicAccounts = useSelector(selectSortedSignerMnemonicAccounts) const sortedMnemonicAccounts = useSelector(selectSortedSignerMnemonicAccounts)
...@@ -116,6 +119,12 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme ...@@ -116,6 +119,12 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
const onPressCreateNewWallet = async (): Promise<void> => { const onPressCreateNewWallet = async (): Promise<void> => {
setShowAddWalletModal(false) setShowAddWalletModal(false)
onClose() onClose()
if (walletRestoreType === WalletRestoreType.SeedPhrase) {
openWalletRestoreModal()
return
}
if (hasImportedSeedPhrase) { if (hasImportedSeedPhrase) {
await createAdditionalAccount() await createAdditionalAccount()
} else { } else {
...@@ -216,7 +225,16 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme ...@@ -216,7 +225,16 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
} }
return options return options
}, [activeAccountAddress, dispatch, hasImportedSeedPhrase, onClose, sortedMnemonicAccounts, t]) }, [
activeAccountAddress,
dispatch,
hasImportedSeedPhrase,
onClose,
sortedMnemonicAccounts,
t,
openWalletRestoreModal,
walletRestoreType,
])
const accountsWithoutActive = accounts.filter((a) => a.address !== activeAccountAddress) const accountsWithoutActive = accounts.filter((a) => a.address !== activeAccountAddress)
......
import React, { useCallback } from 'react' import React from 'react'
import { useDispatch } from 'react-redux'
import { LazyModalRenderer } from 'src/app/modals/LazyModalRenderer' import { LazyModalRenderer } from 'src/app/modals/LazyModalRenderer'
import { SendTokenModal } from 'src/app/modals/SendTokenModal' import { SendTokenModal } from 'src/app/modals/SendTokenModal'
import { SwapModal } from 'src/app/modals/SwapModal' import { SwapModal } from 'src/app/modals/SwapModal'
import { WalletConnectModals } from 'src/components/Requests/WalletConnectModals' import { WalletConnectModals } from 'src/components/Requests/WalletConnectModals'
import { SettingsAppearanceModal } from 'src/components/Settings/SettingsAppearanceModal'
import { ForceUpgradeModal } from 'src/components/forceUpgrade/ForceUpgradeModal' import { ForceUpgradeModal } from 'src/components/forceUpgrade/ForceUpgradeModal'
import { FiatOnRampAggregatorModal } from 'src/features/fiatOnRamp/FiatOnRampAggregatorModal' import { FiatOnRampAggregatorModal } from 'src/features/fiatOnRamp/FiatOnRampAggregatorModal'
import { LockScreenModal } from 'src/features/lockScreen/LockScreenModal' import { LockScreenModal } from 'src/features/lockScreen/LockScreenModal'
import { closeModal } from 'src/features/modals/modalSlice'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { SettingsLanguageModal } from 'wallet/src/components/settings/language/SettingsLanguageModal'
import { PermissionsModal } from 'wallet/src/components/settings/permissions/PermissionsModal'
import { PortfolioBalanceModal } from 'wallet/src/components/settings/portfolioBalance/PortfolioBalanceModal'
import { QueuedOrderModal } from 'wallet/src/features/transactions/swap/modals/QueuedOrderModal' import { QueuedOrderModal } from 'wallet/src/features/transactions/swap/modals/QueuedOrderModal'
/** /**
...@@ -26,20 +20,6 @@ import { QueuedOrderModal } from 'wallet/src/features/transactions/swap/modals/Q ...@@ -26,20 +20,6 @@ import { QueuedOrderModal } from 'wallet/src/features/transactions/swap/modals/Q
*/ */
export function AppModals(): JSX.Element { export function AppModals(): JSX.Element {
const dispatch = useDispatch()
const onCloseLanguageModal = useCallback(() => {
dispatch(closeModal({ name: ModalName.LanguageSelector }))
}, [dispatch])
const onClosePortfolioBalanceModal = useCallback(() => {
dispatch(closeModal({ name: ModalName.PortfolioBalanceModal }))
}, [dispatch])
const onClosePermissionsModal = useCallback(() => {
dispatch(closeModal({ name: ModalName.PermissionsModal }))
}, [dispatch])
return ( return (
<> <>
<LazyModalRenderer name={ModalName.FiatOnRampAggregator}> <LazyModalRenderer name={ModalName.FiatOnRampAggregator}>
...@@ -61,22 +41,6 @@ export function AppModals(): JSX.Element { ...@@ -61,22 +41,6 @@ export function AppModals(): JSX.Element {
<WalletConnectModals /> <WalletConnectModals />
<QueuedOrderModal /> <QueuedOrderModal />
<LazyModalRenderer name={ModalName.LanguageSelector}>
<SettingsLanguageModal onClose={onCloseLanguageModal} />
</LazyModalRenderer>
<LazyModalRenderer name={ModalName.PortfolioBalanceModal}>
<PortfolioBalanceModal onClose={onClosePortfolioBalanceModal} />
</LazyModalRenderer>
<LazyModalRenderer name={ModalName.PermissionsModal}>
<PermissionsModal onClose={onClosePermissionsModal} />
</LazyModalRenderer>
<LazyModalRenderer name={ModalName.SettingsAppearance}>
<SettingsAppearanceModal />
</LazyModalRenderer>
</> </>
) )
} }
import { DdRum } from '@datadog/mobile-react-native' import { DdRum } from '@datadog/mobile-react-native'
import React, { useCallback, useEffect } from 'react' import React, { useCallback, useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { navigate } from 'src/app/navigation/rootNavigation'
import { BiometricsIconProps, useBiometricsIcon } from 'src/components/icons/useBiometricsIcon' import { BiometricsIconProps, useBiometricsIcon } from 'src/components/icons/useBiometricsIcon'
import { WalletRestoreType } from 'src/components/RestoreWalletModal/RestoreWalletModalState'
import { useBiometricAppSettings } from 'src/features/biometrics/useBiometricAppSettings' import { useBiometricAppSettings } from 'src/features/biometrics/useBiometricAppSettings'
import { useOsBiometricAuthEnabled } from 'src/features/biometrics/useOsBiometricAuthEnabled' import { useOsBiometricAuthEnabled } from 'src/features/biometrics/useOsBiometricAuthEnabled'
import { useBiometricPrompt } from 'src/features/biometricsSettings/hooks' import { useBiometricPrompt } from 'src/features/biometricsSettings/hooks'
...@@ -9,18 +11,26 @@ import { closeModal } from 'src/features/modals/modalSlice' ...@@ -9,18 +11,26 @@ import { closeModal } from 'src/features/modals/modalSlice'
import { selectModalState } from 'src/features/modals/selectModalState' import { selectModalState } from 'src/features/modals/selectModalState'
import { useWalletRestore } from 'src/features/wallet/useWalletRestore' import { useWalletRestore } from 'src/features/wallet/useWalletRestore'
import { useHapticFeedback } from 'src/utils/haptics/useHapticFeedback' import { useHapticFeedback } from 'src/utils/haptics/useHapticFeedback'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { updateSwapStartTimestamp } from 'uniswap/src/features/timing/slice' import { updateSwapStartTimestamp } from 'uniswap/src/features/timing/slice'
import { useSwapPrefilledState } from 'uniswap/src/features/transactions/swap/form/hooks/useSwapPrefilledState' import { useSwapPrefilledState } from 'uniswap/src/features/transactions/swap/form/hooks/useSwapPrefilledState'
import { WalletSwapFlow } from 'wallet/src/features/transactions/swap/WalletSwapFlow' import { WalletSwapFlow } from 'wallet/src/features/transactions/swap/WalletSwapFlow'
import { useActiveAccount, useHasSmartWalletConsent } from 'wallet/src/features/wallet/hooks'
import { setSmartWalletConsent } from 'wallet/src/features/wallet/slice'
/* Need to track the swap modal manually until it's integrated in to react-navigation */ /* Need to track the swap modal manually until it's integrated in to react-navigation */
const DATADOG_VIEW_KEY = 'global-swap-modal' const DATADOG_VIEW_KEY = 'global-swap-modal'
export function SwapModal(): JSX.Element { export function SwapModal(): JSX.Element {
const appDispatch = useDispatch() const appDispatch = useDispatch()
const eip5792MethodsEnabled = useFeatureFlag(FeatureFlags.Eip5792Methods)
const { initialState } = useSelector(selectModalState(ModalName.Swap)) const { initialState } = useSelector(selectModalState(ModalName.Swap))
const { hapticFeedback } = useHapticFeedback() const { hapticFeedback } = useHapticFeedback()
const address = useActiveAccount()?.address
const hasSmartWalletConsent = useHasSmartWalletConsent()
const onClose = useCallback((): void => { const onClose = useCallback((): void => {
appDispatch(closeModal({ name: ModalName.Swap })) appDispatch(closeModal({ name: ModalName.Swap }))
...@@ -34,7 +44,7 @@ export function SwapModal(): JSX.Element { ...@@ -34,7 +44,7 @@ export function SwapModal(): JSX.Element {
appDispatch(updateSwapStartTimestamp({ timestamp })) appDispatch(updateSwapStartTimestamp({ timestamp }))
}, [appDispatch]) }, [appDispatch])
const { openWalletRestoreModal, walletNeedsRestore } = useWalletRestore() const { openWalletRestoreModal, walletRestoreType } = useWalletRestore()
const swapPrefilledState = useSwapPrefilledState(initialState) const swapPrefilledState = useSwapPrefilledState(initialState)
...@@ -48,8 +58,26 @@ export function SwapModal(): JSX.Element { ...@@ -48,8 +58,26 @@ export function SwapModal(): JSX.Element {
authTrigger={requiresBiometrics ? biometricsTrigger : undefined} authTrigger={requiresBiometrics ? biometricsTrigger : undefined}
openWalletRestoreModal={openWalletRestoreModal} openWalletRestoreModal={openWalletRestoreModal}
prefilledState={swapPrefilledState} prefilledState={swapPrefilledState}
walletNeedsRestore={Boolean(walletNeedsRestore)} walletNeedsRestore={walletRestoreType === WalletRestoreType.NewDevice}
onSubmitSwap={hapticFeedback.success} onSubmitSwap={async () => {
await hapticFeedback.success()
if (!eip5792MethodsEnabled) {
return
}
// TODO(WALL-6765): check if wallet is already delegated
if (address && hasSmartWalletConsent === false) {
navigate(ModalName.PostSwapSmartWalletNudge, {
onEnableSmartWallet: () => {
appDispatch(setSmartWalletConsent({ address, smartWalletConsent: true }))
navigate(ModalName.SmartWalletEnabledModal, {
showReconnectDappPrompt: false,
})
},
})
}
}}
onClose={onClose} onClose={onClose}
/> />
) )
......
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
import { NativeStackNavigationProp, NativeStackScreenProps } from '@react-navigation/native-stack' import { NativeStackNavigationProp, NativeStackScreenProps } from '@react-navigation/native-stack'
import { TokenWarningModalState } from 'src/app/modals/TokenWarningModalState' import { TokenWarningModalState } from 'src/app/modals/TokenWarningModalState'
import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState' import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState'
import { RestoreWalletModalState } from 'src/components/RestoreWalletModal/RestoreWalletModalState'
import { ConnectionsDappsListModalState } from 'src/components/Settings/ConnectionsDappModal/ConnectionsDappsListModalState' import { ConnectionsDappsListModalState } from 'src/components/Settings/ConnectionsDappModal/ConnectionsDappsListModalState'
import { EditWalletSettingsModalState } from 'src/components/Settings/EditWalletModal/EditWalletSettingsModalState' import { EditWalletSettingsModalState } from 'src/components/Settings/EditWalletModal/EditWalletSettingsModalState'
import { ManageWalletsModalState } from 'src/components/Settings/ManageWalletsModalState' import { ManageWalletsModalState } from 'src/components/Settings/ManageWalletsModalState'
...@@ -16,6 +17,7 @@ import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalSta ...@@ -16,6 +17,7 @@ import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalSta
import { TestnetSwitchModalState } from 'src/features/testnetMode/TestnetSwitchModalState' import { TestnetSwitchModalState } from 'src/features/testnetMode/TestnetSwitchModalState'
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 { ViewPrivateKeysScreenState } from 'src/screens/ViewPrivateKeys/ViewPrivateKeysScreenState'
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 { PasskeyManagementModalState } from 'uniswap/src/features/passkey/PasskeyManagementModal'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
...@@ -28,8 +30,12 @@ import { ...@@ -28,8 +30,12 @@ import {
SharedUnitagScreenParams, SharedUnitagScreenParams,
UnitagStackParamList, UnitagStackParamList,
} from 'uniswap/src/types/screens/mobile' } from 'uniswap/src/types/screens/mobile'
import { PostSwapSmartWalletNudgeState } from 'wallet/src/components/smartWallet/modals/PostSwapSmartWalletNudge'
import { SmartWalletEnabledModalState } from 'wallet/src/components/smartWallet/modals/SmartWalletEnabledModal'
import { NFTItem } from 'wallet/src/features/nfts/types' import { NFTItem } from 'wallet/src/features/nfts/types'
import { SmartWalletAdvancedSettingsModalState } from 'wallet/src/features/smartWallet/modals/SmartWalletAdvancedSettingsModal' import { SmartWalletAdvancedSettingsModalState } from 'wallet/src/features/smartWallet/modals/SmartWalletAdvancedSettingsModal'
import { SmartWalletConfirmModalState } from 'wallet/src/features/smartWallet/modals/SmartWalletConfirmModal'
import { SmartWalletInsufficientFundsOnNetworkModalState } from 'wallet/src/features/smartWallet/modals/SmartWalletInsufficientFundsOnNetworkModal'
type NFTItemScreenParams = { type NFTItemScreenParams = {
owner?: Address owner?: Address
...@@ -90,16 +96,17 @@ export type SettingsStackParamList = { ...@@ -90,16 +96,17 @@ export type SettingsStackParamList = {
[MobileScreens.SettingsLanguage]: undefined [MobileScreens.SettingsLanguage]: undefined
[MobileScreens.SettingsNotifications]: undefined [MobileScreens.SettingsNotifications]: undefined
[MobileScreens.SettingsPrivacy]: undefined [MobileScreens.SettingsPrivacy]: undefined
[MobileScreens.SettingsSmartWallet]: { Wrapper: React.FC<{ children: React.ReactNode }> } [MobileScreens.SettingsSmartWallet]: undefined
[MobileScreens.SettingsViewSeedPhrase]: { address: Address; walletNeedsRestore?: boolean } [MobileScreens.SettingsViewSeedPhrase]: { address: Address; walletNeedsRestore?: boolean }
[MobileScreens.SettingsWallet]: { address: Address } [MobileScreens.SettingsWallet]: { address: Address }
[MobileScreens.SettingsWalletEdit]: { address: Address } [MobileScreens.SettingsWalletEdit]: { address: Address }
[MobileScreens.SettingsWalletManageConnection]: { address: Address } [MobileScreens.SettingsWalletManageConnection]: { address: Address }
[MobileScreens.ViewPrivateKeys]?: ViewPrivateKeysScreenState
[MobileScreens.WebView]: { headerTitle: string; uriLink: string } [MobileScreens.WebView]: { headerTitle: string; uriLink: string }
[ModalName.Experiments]: undefined
[ModalName.NotificationsOSSettings]: undefined [ModalName.NotificationsOSSettings]: undefined
[ModalName.SettingsAppearance]: undefined
[ModalName.UnitagsIntro]: UnitagsIntroModalState [ModalName.UnitagsIntro]: UnitagsIntroModalState
[ModalName.RestoreWallet]: undefined [ModalName.RestoreWallet]: RestoreWalletModalState
} }
export type OnboardingStackBaseParams = { export type OnboardingStackBaseParams = {
...@@ -119,9 +126,11 @@ export type OnboardingStackParamList = { ...@@ -119,9 +126,11 @@ export type OnboardingStackParamList = {
[OnboardingScreens.WelcomeWallet]: OnboardingStackBaseParams [OnboardingScreens.WelcomeWallet]: OnboardingStackBaseParams
[OnboardingScreens.PasskeyImport]: PasskeyImportParams & OnboardingStackBaseParams [OnboardingScreens.PasskeyImport]: PasskeyImportParams & OnboardingStackBaseParams
[OnboardingScreens.Security]: OnboardingStackBaseParams [OnboardingScreens.Security]: OnboardingStackBaseParams
[MobileScreens.ViewPrivateKeys]?: ViewPrivateKeysScreenState
// import // import
[OnboardingScreens.ImportMethod]: OnboardingStackBaseParams [OnboardingScreens.ImportMethod]: OnboardingStackBaseParams
[OnboardingScreens.RestoreMethod]: OnboardingStackBaseParams
[OnboardingScreens.OnDeviceRecovery]: OnboardingStackBaseParams & { mnemonicIds: Address[] } [OnboardingScreens.OnDeviceRecovery]: OnboardingStackBaseParams & { mnemonicIds: Address[] }
[OnboardingScreens.OnDeviceRecoveryViewSeedPhrase]: { [OnboardingScreens.OnDeviceRecoveryViewSeedPhrase]: {
mnemonicId: string mnemonicId: string
...@@ -131,9 +140,12 @@ export type OnboardingStackParamList = { ...@@ -131,9 +140,12 @@ export type OnboardingStackParamList = {
[OnboardingScreens.RestoreCloudBackupPassword]: { [OnboardingScreens.RestoreCloudBackupPassword]: {
mnemonicId: string mnemonicId: string
} & OnboardingStackBaseParams } & OnboardingStackBaseParams
[OnboardingScreens.SeedPhraseInput]: OnboardingStackBaseParams [OnboardingScreens.SeedPhraseInput]: OnboardingStackBaseParams & {
showAsCloudBackupFallback?: boolean
}
[OnboardingScreens.SelectWallet]: OnboardingStackBaseParams [OnboardingScreens.SelectWallet]: OnboardingStackBaseParams
[OnboardingScreens.WatchWallet]: OnboardingStackBaseParams [OnboardingScreens.WatchWallet]: OnboardingStackBaseParams
[ModalName.PrivateKeySpeedBumpModal]: undefined
} & SharedUnitagScreenParams } & SharedUnitagScreenParams
export type AppStackParamList = { export type AppStackParamList = {
...@@ -152,6 +164,7 @@ export type AppStackParamList = { ...@@ -152,6 +164,7 @@ export type AppStackParamList = {
[MobileScreens.ExternalProfile]: { [MobileScreens.ExternalProfile]: {
address: string address: string
} }
[MobileScreens.ViewPrivateKeys]?: ViewPrivateKeysScreenState
[MobileScreens.WebView]: { headerTitle: string; uriLink: string } [MobileScreens.WebView]: { headerTitle: string; uriLink: string }
[MobileScreens.Storybook]: undefined [MobileScreens.Storybook]: undefined
[ModalName.Explore]: ExploreModalState | undefined [ModalName.Explore]: ExploreModalState | undefined
...@@ -164,7 +177,7 @@ export type AppStackParamList = { ...@@ -164,7 +177,7 @@ export type AppStackParamList = {
[ModalName.TokenWarning]: { initialState?: TokenWarningModalState } [ModalName.TokenWarning]: { initialState?: TokenWarningModalState }
[ModalName.ViewOnlyExplainer]: undefined [ModalName.ViewOnlyExplainer]: undefined
[ModalName.UnitagsIntro]: UnitagsIntroModalState [ModalName.UnitagsIntro]: UnitagsIntroModalState
[ModalName.RestoreWallet]: undefined [ModalName.RestoreWallet]: RestoreWalletModalState
[ModalName.AccountSwitcher]: undefined [ModalName.AccountSwitcher]: undefined
[ModalName.Scantastic]: ScantasticModalState [ModalName.Scantastic]: ScantasticModalState
[ModalName.BackupReminder]: undefined [ModalName.BackupReminder]: undefined
...@@ -183,7 +196,16 @@ export type AppStackParamList = { ...@@ -183,7 +196,16 @@ export type AppStackParamList = {
[ModalName.EditLabelSettingsModal]: EditWalletSettingsModalState [ModalName.EditLabelSettingsModal]: EditWalletSettingsModalState
[ModalName.EditProfileSettingsModal]: EditWalletSettingsModalState [ModalName.EditProfileSettingsModal]: EditWalletSettingsModalState
[ModalName.ConnectionsDappListModal]: ConnectionsDappsListModalState [ModalName.ConnectionsDappListModal]: ConnectionsDappsListModalState
[ModalName.SmartWalletEnabledModal]: SmartWalletEnabledModalState
[ModalName.SmartWalletAdvancedSettingsModal]: SmartWalletAdvancedSettingsModalState [ModalName.SmartWalletAdvancedSettingsModal]: SmartWalletAdvancedSettingsModalState
[ModalName.PrivateKeySpeedBumpModal]: undefined
[ModalName.SmartWalletConfirmModal]: SmartWalletConfirmModalState
[ModalName.SmartWalletInsufficientFundsOnNetworkModal]: SmartWalletInsufficientFundsOnNetworkModalState
[ModalName.PostSwapSmartWalletNudge]: PostSwapSmartWalletNudgeState
[ModalName.SettingsAppearance]: undefined
[ModalName.PermissionsModal]: undefined
[ModalName.PortfolioBalanceModal]: undefined
[ModalName.LanguageSelector]: undefined
} }
export type AppStackNavigationProp = NativeStackNavigationProp<AppStackParamList> export type AppStackNavigationProp = NativeStackNavigationProp<AppStackParamList>
......
...@@ -16,9 +16,9 @@ import { restoreMnemonicCompleteWatcher } from 'src/features/wallet/saga' ...@@ -16,9 +16,9 @@ import { restoreMnemonicCompleteWatcher } from 'src/features/wallet/saga'
import { walletConnectSaga } from 'src/features/walletConnect/saga' import { walletConnectSaga } from 'src/features/walletConnect/saga'
import { signWcRequestSaga } from 'src/features/walletConnect/signWcRequestSaga' import { signWcRequestSaga } from 'src/features/walletConnect/signWcRequestSaga'
import { call, fork, join, select, spawn, take } from 'typed-redux-saga' import { call, fork, join, select, spawn, take } from 'typed-redux-saga'
import { appLanguageWatcherSaga } from 'uniswap/src/features/language/saga'
import { apolloClientRef } from 'wallet/src/data/apollo/usePersistedApolloClient' import { apolloClientRef } from 'wallet/src/data/apollo/usePersistedApolloClient'
import { transactionWatcher } from 'wallet/src/features/transactions/transactionWatcherSaga' import { deviceLocaleWatcher } from 'wallet/src/features/i18n/deviceLocaleWatcherSaga'
import { transactionWatcher } from 'wallet/src/features/transactions/watcher/transactionWatcherSaga'
// These sagas are not persisted, so we can run them before rehydration // These sagas are not persisted, so we can run them before rehydration
const nonPersistedSagas = [appStateSaga, splashScreenSaga, biometricsSaga] const nonPersistedSagas = [appStateSaga, splashScreenSaga, biometricsSaga]
...@@ -26,7 +26,6 @@ const nonPersistedSagas = [appStateSaga, splashScreenSaga, biometricsSaga] ...@@ -26,7 +26,6 @@ const nonPersistedSagas = [appStateSaga, splashScreenSaga, biometricsSaga]
// All regular sagas must be included here // All regular sagas must be included here
const sagas = [ const sagas = [
lockScreenSaga, lockScreenSaga,
appLanguageWatcherSaga,
appRatingWatcherSaga, appRatingWatcherSaga,
cloudBackupsManagerSaga, cloudBackupsManagerSaga,
deepLinkWatcher, deepLinkWatcher,
...@@ -37,9 +36,10 @@ const sagas = [ ...@@ -37,9 +36,10 @@ const sagas = [
signWcRequestSaga, signWcRequestSaga,
telemetrySaga, telemetrySaga,
walletConnectSaga, walletConnectSaga,
deviceLocaleWatcher,
] ]
export function* rootMobileSaga() { export function* rootMobileSaga(): SagaIterator {
// Start non-persisted sagas // Start non-persisted sagas
for (const s of nonPersistedSagas) { for (const s of nonPersistedSagas) {
yield* spawn(s) yield* spawn(s)
......
...@@ -10,6 +10,7 @@ import { isNonJestDev } from 'utilities/src/environment/constants' ...@@ -10,6 +10,7 @@ import { isNonJestDev } from 'utilities/src/environment/constants'
import { createDatadogReduxEnhancer } from 'utilities/src/logger/datadog/Datadog' import { createDatadogReduxEnhancer } from 'utilities/src/logger/datadog/Datadog'
import { createStore } from 'wallet/src/state' import { createStore } from 'wallet/src/state'
import { createMigrate } from 'wallet/src/state/createMigrate' import { createMigrate } from 'wallet/src/state/createMigrate'
import { setReduxPersistor } from 'wallet/src/state/persistor'
const storage = new MMKV() const storage = new MMKV()
...@@ -66,6 +67,6 @@ const setupStore = ( ...@@ -66,6 +67,6 @@ const setupStore = (
enhancers, enhancers,
}) })
} }
export const store = setupStore()
export const persistor = persistStore(store) export const store = setupStore()
setReduxPersistor(persistStore(store))
...@@ -69,9 +69,9 @@ export function ConnectedDappsList({ backButton, sessions, selectedAddress }: Co ...@@ -69,9 +69,9 @@ export function ConnectedDappsList({ backButton, sessions, selectedAddress }: Co
pushNotification({ pushNotification({
type: AppNotificationType.WalletConnect, type: AppNotificationType.WalletConnect,
address, address,
dappName: session.dapp.name, dappName: session.dappRequestInfo.name,
event: WalletConnectEvent.Disconnected, event: WalletConnectEvent.Disconnected,
imageUrl: session.dapp.icon, imageUrl: session.dappRequestInfo.icon,
hideDelay: 3 * ONE_SECOND_MS, hideDelay: 3 * ONE_SECOND_MS,
}), }),
) )
......
...@@ -20,7 +20,7 @@ export function DappConnectionItem({ ...@@ -20,7 +20,7 @@ export function DappConnectionItem({
handleDisconnect: (session: WalletConnectSession) => Promise<void> handleDisconnect: (session: WalletConnectSession) => Promise<void>
}): JSX.Element { }): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const { dapp } = session const { dappRequestInfo } = session
const menuActions = [{ title: t('common.button.disconnect'), systemIcon: 'trash', destructive: true }] const menuActions = [{ title: t('common.button.disconnect'), systemIcon: 'trash', destructive: true }]
...@@ -74,12 +74,12 @@ export function DappConnectionItem({ ...@@ -74,12 +74,12 @@ export function DappConnectionItem({
)} )}
</Flex> </Flex>
<Flex grow centered gap="$gap8"> <Flex grow centered gap="$gap8">
<DappHeaderIcon size={iconSizes.icon36} dapp={dapp} /> <DappHeaderIcon size={iconSizes.icon36} dappRequestInfo={dappRequestInfo} />
<Text numberOfLines={2} textAlign="center" variant="body3" mt="$spacing4"> <Text numberOfLines={2} textAlign="center" variant="body3" mt="$spacing4">
{dapp.name || dapp.url} {dappRequestInfo.name || dappRequestInfo.url}
</Text> </Text>
<Text color="$neutral2" numberOfLines={1} textAlign="center" variant="body4"> <Text color="$neutral2" numberOfLines={1} textAlign="center" variant="body4">
{dapp.url} {dappRequestInfo.url}
</Text> </Text>
</Flex> </Flex>
</Flex> </Flex>
......
...@@ -3,15 +3,15 @@ import { Flex, UniversalImage } from 'ui/src' ...@@ -3,15 +3,15 @@ import { Flex, UniversalImage } from 'ui/src'
import { borderRadii, iconSizes } from 'ui/src/theme' import { borderRadii, iconSizes } from 'ui/src/theme'
import { CurrencyLogo } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo' import { CurrencyLogo } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo'
import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types'
import { DappInfo } from 'uniswap/src/types/walletConnect' import { DappRequestInfo } from 'uniswap/src/types/walletConnect'
import { DappIconPlaceholder } from 'wallet/src/components/WalletConnect/DappIconPlaceholder' import { DappIconPlaceholder } from 'wallet/src/components/WalletConnect/DappIconPlaceholder'
export function DappHeaderIcon({ export function DappHeaderIcon({
dapp, dappRequestInfo,
permitCurrencyInfo, permitCurrencyInfo,
size = iconSizes.icon40, size = iconSizes.icon40,
}: { }: {
dapp: DappInfo dappRequestInfo: DappRequestInfo
permitCurrencyInfo?: CurrencyInfo | null permitCurrencyInfo?: CurrencyInfo | null
size?: number size?: number
}): JSX.Element { }): JSX.Element {
...@@ -19,11 +19,11 @@ export function DappHeaderIcon({ ...@@ -19,11 +19,11 @@ export function DappHeaderIcon({
return <CurrencyLogo currencyInfo={permitCurrencyInfo} /> return <CurrencyLogo currencyInfo={permitCurrencyInfo} />
} }
const fallback = <DappIconPlaceholder iconSize={size} name={dapp.name} /> const fallback = <DappIconPlaceholder iconSize={size} name={dappRequestInfo.name} />
return ( return (
<Flex height={size} width={size}> <Flex height={size} width={size}>
{dapp.icon ? ( {dappRequestInfo.icon ? (
<UniversalImage <UniversalImage
fallback={fallback} fallback={fallback}
size={{ height: size, width: size }} size={{ height: size, width: size }}
...@@ -34,7 +34,7 @@ export function DappHeaderIcon({ ...@@ -34,7 +34,7 @@ export function DappHeaderIcon({
overflow: 'hidden', overflow: 'hidden',
}, },
}} }}
uri={dapp.icon} uri={dappRequestInfo.icon}
/> />
) : ( ) : (
fallback fallback
......
...@@ -13,7 +13,7 @@ import { ...@@ -13,7 +13,7 @@ import {
} from 'react-native' } from 'react-native'
import { AnimatedStyle, useDerivedValue } from 'react-native-reanimated' import { AnimatedStyle, useDerivedValue } from 'react-native-reanimated'
import { ScrollDownOverlay } from 'src/components/Requests/ModalWithOverlay/ScrollDownOverlay' import { ScrollDownOverlay } from 'src/components/Requests/ModalWithOverlay/ScrollDownOverlay'
import { Button, Flex } from 'ui/src' import { Button, ButtonProps, Flex } from 'ui/src'
import { spacing } from 'ui/src/theme' import { spacing } from 'ui/src/theme'
import { Modal } from 'uniswap/src/components/modals/Modal' import { Modal } from 'uniswap/src/components/modals/Modal'
import { ModalProps } from 'uniswap/src/components/modals/ModalProps' import { ModalProps } from 'uniswap/src/components/modals/ModalProps'
...@@ -24,14 +24,17 @@ import { TestID } from 'uniswap/src/test/fixtures/testIDs' ...@@ -24,14 +24,17 @@ import { TestID } from 'uniswap/src/test/fixtures/testIDs'
const MEASURE_LAYOUT_TIMEOUT = 100 const MEASURE_LAYOUT_TIMEOUT = 100
type ModalWithOverlayProps = PropsWithChildren< export type ModalWithOverlayProps = PropsWithChildren<
ModalProps & { ModalProps & {
confirmationButtonText?: string confirmationButtonText?: string
cancelButtonText?: string
scrollDownButtonText?: string scrollDownButtonText?: string
onReject: () => void onReject: () => void
onConfirm?: () => void onConfirm?: () => void
disableConfirm?: boolean disableConfirm?: boolean
contentContainerStyle?: StyleProp<AnimatedStyle<StyleProp<ViewStyle>>> contentContainerStyle?: StyleProp<AnimatedStyle<StyleProp<ViewStyle>>>
cancelButtonProps?: ButtonProps
confirmationButtonProps?: ButtonProps
} }
> >
...@@ -42,11 +45,14 @@ const isCloseToBottom = ({ layoutMeasurement, contentOffset, contentSize }: Nati ...@@ -42,11 +45,14 @@ const isCloseToBottom = ({ layoutMeasurement, contentOffset, contentSize }: Nati
export function ModalWithOverlay({ export function ModalWithOverlay({
children, children,
confirmationButtonText, confirmationButtonText,
cancelButtonText,
scrollDownButtonText, scrollDownButtonText,
onReject, onReject,
onConfirm, onConfirm,
disableConfirm, disableConfirm,
contentContainerStyle, contentContainerStyle,
cancelButtonProps,
confirmationButtonProps,
...bottomSheetModalProps ...bottomSheetModalProps
}: ModalWithOverlayProps): JSX.Element { }: ModalWithOverlayProps): JSX.Element {
const scrollViewRef = useRef<ScrollView>(null) const scrollViewRef = useRef<ScrollView>(null)
...@@ -129,10 +135,13 @@ export function ModalWithOverlay({ ...@@ -129,10 +135,13 @@ export function ModalWithOverlay({
</BottomSheetScrollView> </BottomSheetScrollView>
<ModalFooter <ModalFooter
cancelButtonText={cancelButtonText}
confirmationButtonText={confirmationButtonText} confirmationButtonText={confirmationButtonText}
confirmationEnabled={!disableConfirm && confirmationEnabled} confirmationEnabled={!disableConfirm && confirmationEnabled}
scrollDownButtonText={scrollDownButtonText} scrollDownButtonText={scrollDownButtonText}
showScrollDownOverlay={showOverlay && !eip5792MethodsEnabled} showScrollDownOverlay={showOverlay && !eip5792MethodsEnabled}
cancelButtonProps={cancelButtonProps}
confirmationButtonProps={confirmationButtonProps}
onConfirm={onConfirm} onConfirm={onConfirm}
onReject={onReject} onReject={onReject}
onScrollDownPress={handleScrollDown} onScrollDownPress={handleScrollDown}
...@@ -144,8 +153,11 @@ export function ModalWithOverlay({ ...@@ -144,8 +153,11 @@ export function ModalWithOverlay({
type ModalFooterProps = { type ModalFooterProps = {
confirmationEnabled: boolean confirmationEnabled: boolean
showScrollDownOverlay: boolean showScrollDownOverlay: boolean
cancelButtonText?: string
confirmationButtonText?: string confirmationButtonText?: string
scrollDownButtonText?: string scrollDownButtonText?: string
cancelButtonProps?: ButtonProps
confirmationButtonProps?: ButtonProps
onScrollDownPress: () => void onScrollDownPress: () => void
onReject: () => void onReject: () => void
onConfirm?: () => void onConfirm?: () => void
...@@ -155,7 +167,10 @@ function ModalFooter({ ...@@ -155,7 +167,10 @@ function ModalFooter({
confirmationEnabled, confirmationEnabled,
showScrollDownOverlay, showScrollDownOverlay,
scrollDownButtonText, scrollDownButtonText,
cancelButtonText,
confirmationButtonText, confirmationButtonText,
cancelButtonProps,
confirmationButtonProps,
onScrollDownPress, onScrollDownPress,
onReject, onReject,
onConfirm, onConfirm,
...@@ -188,8 +203,8 @@ function ModalFooter({ ...@@ -188,8 +203,8 @@ function ModalFooter({
pt="$spacing12" pt="$spacing12"
px="$spacing24" px="$spacing24"
> >
<Button size="large" testID={TestID.Cancel} emphasis="tertiary" onPress={onReject}> <Button size="large" testID={TestID.Cancel} emphasis="tertiary" onPress={onReject} {...cancelButtonProps}>
{t('common.button.cancel')} {cancelButtonText ?? t('common.button.cancel')}
</Button> </Button>
{confirmationButtonText && ( {confirmationButtonText && (
...@@ -199,6 +214,7 @@ function ModalFooter({ ...@@ -199,6 +214,7 @@ function ModalFooter({
size="large" size="large"
testID={TestID.Confirm} testID={TestID.Confirm}
onPress={onConfirm} onPress={onConfirm}
{...confirmationButtonProps}
> >
{confirmationButtonText} {confirmationButtonText}
</Button> </Button>
......
...@@ -20,26 +20,26 @@ export function ClientDetails({ ...@@ -20,26 +20,26 @@ export function ClientDetails({
request: WalletConnectSigningRequest request: WalletConnectSigningRequest
permitInfo?: PermitInfo permitInfo?: PermitInfo
}): JSX.Element { }): JSX.Element {
const { dapp } = request const { dappRequestInfo } = request
const colors = useSporeColors() const colors = useSporeColors()
const permitCurrencyInfo = useCurrencyInfo(permitInfo?.currencyId) const permitCurrencyInfo = useCurrencyInfo(permitInfo?.currencyId)
return ( return (
<Flex centered gap="$spacing12"> <Flex centered gap="$spacing12">
<DappHeaderIcon dapp={dapp} permitCurrencyInfo={permitCurrencyInfo} /> <DappHeaderIcon dappRequestInfo={dappRequestInfo} permitCurrencyInfo={permitCurrencyInfo} />
<HeaderText permitAmount={permitInfo?.amount} permitCurrency={permitCurrencyInfo?.currency} request={request} /> <HeaderText permitAmount={permitInfo?.amount} permitCurrency={permitCurrencyInfo?.currency} request={request} />
<LinkButton <LinkButton
color={colors.accent1.val} color={colors.accent1.val}
iconColor="$accent1" iconColor="$accent1"
label={formatDappURL(dapp.url)} label={formatDappURL(dappRequestInfo.url)}
mb="$spacing12" mb="$spacing12"
px="$spacing8" px="$spacing8"
py="$spacing4" py="$spacing4"
showIcon={false} showIcon={false}
size={iconSizes.icon12} size={iconSizes.icon12}
textVariant="buttonLabel2" textVariant="buttonLabel2"
url={dapp.url} url={dappRequestInfo.url}
/> />
</Flex> </Flex>
) )
......
...@@ -16,7 +16,7 @@ export function HeaderText({ ...@@ -16,7 +16,7 @@ export function HeaderText({
permitAmount?: number permitAmount?: number
permitCurrency?: Currency | null permitCurrency?: Currency | null
}): JSX.Element { }): JSX.Element {
const { dapp, type: method } = request const { dappRequestInfo, type: method } = request
if (permitCurrency) { if (permitCurrency) {
const readablePermitAmount = getCurrencyAmount({ const readablePermitAmount = getCurrencyAmount({
...@@ -32,7 +32,7 @@ export function HeaderText({ ...@@ -32,7 +32,7 @@ export function HeaderText({
components={{ highlight: <Text variant="heading3" fontWeight="bold" /> }} components={{ highlight: <Text variant="heading3" fontWeight="bold" /> }}
i18nKey="qrScanner.request.withAmount" i18nKey="qrScanner.request.withAmount"
values={{ values={{
dappName: dapp.name, dappName: dappRequestInfo.name,
currencySymbol: permitCurrency?.symbol, currencySymbol: permitCurrency?.symbol,
amount: readablePermitAmount, amount: readablePermitAmount,
}} }}
...@@ -45,7 +45,7 @@ export function HeaderText({ ...@@ -45,7 +45,7 @@ export function HeaderText({
components={{ highlight: <Text variant="heading3" fontWeight="bold" /> }} components={{ highlight: <Text variant="heading3" fontWeight="bold" /> }}
i18nKey="qrScanner.request.withoutAmount" i18nKey="qrScanner.request.withoutAmount"
values={{ values={{
dappName: dapp.name, dappName: dappRequestInfo.name,
currencySymbol: permitCurrency?.symbol, currencySymbol: permitCurrency?.symbol,
}} }}
/> />
...@@ -72,7 +72,7 @@ export function HeaderText({ ...@@ -72,7 +72,7 @@ export function HeaderText({
return ( return (
<Text textAlign="center" variant="subheading1"> <Text textAlign="center" variant="subheading1">
{getReadableMethodName(method, dapp.name || dapp.url)} {getReadableMethodName(method, dappRequestInfo.name || dappRequestInfo.url)}
</Text> </Text>
) )
} }
...@@ -33,7 +33,7 @@ import { useHasAccountMismatchCallback } from 'uniswap/src/features/smartWallet/ ...@@ -33,7 +33,7 @@ import { useHasAccountMismatchCallback } from 'uniswap/src/features/smartWallet/
import { MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants' import { MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { useIsBlocked } from 'uniswap/src/features/trm/hooks' import { useIsBlocked } from 'uniswap/src/features/trm/hooks'
import { UwULinkMethod, WCEventType, WCRequestOutcome } from 'uniswap/src/types/walletConnect' import { DappRequestType, UwULinkMethod, WCEventType, WCRequestOutcome } from 'uniswap/src/types/walletConnect'
import { areAddressesEqual } from 'uniswap/src/utils/addresses' import { areAddressesEqual } from 'uniswap/src/utils/addresses'
import { formatExternalTxnWithGasEstimates } from 'wallet/src/features/gas/formatExternalTxnWithGasEstimates' import { formatExternalTxnWithGasEstimates } from 'wallet/src/features/gas/formatExternalTxnWithGasEstimates'
import { useIsBlockedActiveAddress } from 'wallet/src/features/trm/hooks' import { useIsBlockedActiveAddress } from 'wallet/src/features/trm/hooks'
...@@ -89,9 +89,11 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem ...@@ -89,9 +89,11 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
const getHasMismatch = useHasAccountMismatchCallback() const getHasMismatch = useHasAccountMismatchCallback()
const hasMismatch = chainId ? getHasMismatch(chainId) : false const hasMismatch = chainId ? getHasMismatch(chainId) : false
// When link mode is active we can sign messages through universal links on device
const suppressOfflineWarning = request.isLinkModeSupported
const checkConfirmEnabled = (): boolean => { const checkConfirmEnabled = (): boolean => {
if (!netInfo.isInternetReachable) { if (!netInfo.isInternetReachable && !suppressOfflineWarning) {
return false return false
} }
...@@ -123,7 +125,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem ...@@ -123,7 +125,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
const rejectOnCloseRef = useRef(true) const rejectOnCloseRef = useRef(true)
const onReject = async (): Promise<void> => { const onReject = async (): Promise<void> => {
if (request.dapp.source === 'walletconnect') { if (request.dappRequestInfo.requestType === DappRequestType.WalletConnectSessionRequest) {
await wcWeb3Wallet.respondSessionRequest({ await wcWeb3Wallet.respondSessionRequest({
topic: request.sessionId, topic: request.sessionId,
response: { response: {
...@@ -139,8 +141,8 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem ...@@ -139,8 +141,8 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
sendAnalyticsEvent(MobileEventName.WalletConnectSheetCompleted, { sendAnalyticsEvent(MobileEventName.WalletConnectSheetCompleted, {
request_type: isTransactionRequest(request) ? WCEventType.TransactionRequest : WCEventType.SignRequest, request_type: isTransactionRequest(request) ? WCEventType.TransactionRequest : WCEventType.SignRequest,
eth_method: request.type, eth_method: request.type,
dapp_url: request.dapp.url, dapp_url: request.dappRequestInfo.url,
dapp_name: request.dapp.name, dapp_name: request.dappRequestInfo.name,
wc_version: '2', wc_version: '2',
chain_id: chainId, chain_id: chainId,
outcome: WCRequestOutcome.Reject, outcome: WCRequestOutcome.Reject,
...@@ -178,7 +180,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem ...@@ -178,7 +180,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
method: request.type === EthMethod.WalletSendCalls ? EthMethod.WalletSendCalls : EthMethod.EthSendTransaction, method: request.type === EthMethod.WalletSendCalls ? EthMethod.WalletSendCalls : EthMethod.EthSendTransaction,
transaction: txnWithFormattedGasEstimates, transaction: txnWithFormattedGasEstimates,
account: signerAccount, account: signerAccount,
dapp: request.dapp, dappRequestInfo: request.dappRequestInfo,
chainId, chainId,
request, request,
}), }),
...@@ -192,7 +194,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem ...@@ -192,7 +194,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
method: request.type, method: request.type,
message: request.message || request.rawMessage, message: request.message || request.rawMessage,
account: signerAccount, account: signerAccount,
dapp: request.dapp, dappRequestInfo: request.dappRequestInfo,
chainId, chainId,
}), }),
) )
...@@ -203,8 +205,8 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem ...@@ -203,8 +205,8 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
sendAnalyticsEvent(MobileEventName.WalletConnectSheetCompleted, { sendAnalyticsEvent(MobileEventName.WalletConnectSheetCompleted, {
request_type: isTransactionRequest(request) ? WCEventType.TransactionRequest : WCEventType.SignRequest, request_type: isTransactionRequest(request) ? WCEventType.TransactionRequest : WCEventType.SignRequest,
eth_method: request.type, eth_method: request.type,
dapp_url: request.dapp.url, dapp_url: request.dappRequestInfo.url,
dapp_name: request.dapp.name, dapp_name: request.dappRequestInfo.name,
wc_version: '2', wc_version: '2',
chain_id: chainId, chain_id: chainId,
outcome: WCRequestOutcome.Confirm, outcome: WCRequestOutcome.Confirm,
...@@ -259,7 +261,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem ...@@ -259,7 +261,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
} }
// KidSuper Uniswap Cafe check-in screen // KidSuper Uniswap Cafe check-in screen
if (request.type === EthMethod.PersonalSign && request.dapp.name === 'Uniswap Cafe') { if (request.type === EthMethod.PersonalSign && request.dappRequestInfo.name === 'Uniswap Cafe') {
return ( return (
<KidSuperCheckinModal request={request} onClose={handleClose} onConfirm={onConfirmPress} onReject={onReject} /> <KidSuperCheckinModal request={request} onClose={handleClose} onConfirm={onConfirmPress} onReject={onReject} />
) )
......
...@@ -82,6 +82,9 @@ export function WalletConnectRequestModalContent({ ...@@ -82,6 +82,9 @@ export function WalletConnectRequestModalContent({
const hasGasFee = getDoesMethodCostGas(request) const hasGasFee = getDoesMethodCostGas(request)
// If link mode is supported, we can sign messages through universal links on device
const suppressOfflineWarning = request.isLinkModeSupported
return ( return (
<> <>
<Flex px="$spacing24"> <Flex px="$spacing24">
...@@ -118,7 +121,7 @@ export function WalletConnectRequestModalContent({ ...@@ -118,7 +121,7 @@ export function WalletConnectRequestModalContent({
</Flex> </Flex>
)} )}
{!netInfo.isInternetReachable ? ( {!netInfo.isInternetReachable && !suppressOfflineWarning ? (
<BaseCard.InlineErrorState <BaseCard.InlineErrorState
backgroundColor="$statusWarning2" backgroundColor="$statusWarning2"
icon={<AlertTriangleFilled color="$statusWarning" size="$icon.16" />} icon={<AlertTriangleFilled color="$statusWarning" size="$icon.16" />}
......
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { WalletConnectVerifyStatus } from 'src/features/walletConnect/walletConnectSlice'
import { Flex, LabeledCheckbox, Text, TouchableArea } from 'ui/src'
import {
AlertCircleFilled,
AlertTriangleFilled,
CheckCircleFilled,
Clear,
GlobeFilled,
OctagonExclamation,
RotatableChevron,
} from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
import { useBooleanState } from 'utilities/src/react/useBooleanState'
const PERMISSIONS_MAX_LENGTH = 280
export const SitePermissions = ({
verifyStatus,
confirmedWarning,
onConfirmWarning,
}: {
verifyStatus: WalletConnectVerifyStatus
confirmedWarning?: boolean
onConfirmWarning?: (confirmed: boolean) => void
}): JSX.Element => {
const { t } = useTranslation()
// Always show expanded permissions for unverified apps
const isInitiallyExpanded = verifyStatus !== WalletConnectVerifyStatus.Verified
const { value: isExpanded, toggle: toggleExpanded, setValue: setIsExpanded } = useBooleanState(isInitiallyExpanded)
const infoTextSize = 'body3'
const handleConfirmWarning = useCallback(
(previousIsConfirmed: boolean) => {
onConfirmWarning?.(!previousIsConfirmed)
// Open options if previously confirmed, close if previously unconfirmed
setIsExpanded(previousIsConfirmed ? true : false)
},
[onConfirmWarning, setIsExpanded],
)
return (
<Flex gap="$spacing12">
<Flex
backgroundColor="$surface2"
borderColor={verifyStatus === WalletConnectVerifyStatus.Threat ? '$statusCritical' : '$surface3'}
borderRadius="$rounded16"
borderWidth="$spacing1"
minHeight={44}
>
<TouchableArea
pb="$spacing12"
p="$spacing16"
borderBottomWidth={isExpanded ? 1 : 0}
borderColor="$surface3"
onPress={toggleExpanded}
>
<Flex centered row justifyContent="space-between">
<Flex centered row gap="$spacing8">
<GlobeFilled color="$neutral2" size="$icon.16" />
<Text $short={{ variant: 'body3' }} allowFontScaling={false} color="$neutral2" variant="buttonLabel3">
{t('walletConnect.permissions.title')}
</Text>
</Flex>
<RotatableChevron
color="$neutral2"
direction={isExpanded ? 'up' : 'down'}
height={iconSizes.icon16}
width={iconSizes.icon16}
/>
</Flex>
</TouchableArea>
{isExpanded && (
<Flex gap="$spacing12" p="$spacing16">
<Flex centered row gap="$spacing8">
<CheckCircleFilled color="$statusSuccess" size="$icon.16" />
<Text
$short={{ variant: infoTextSize }}
allowFontScaling={false}
color="$neutral2"
flexGrow={1}
variant={infoTextSize}
>
{t('walletConnect.permissions.option.viewTokenBalances')}
</Text>
</Flex>
<Flex centered row gap="$spacing8">
<CheckCircleFilled color="$statusSuccess" size="$icon.16" />
<Text
$short={{ variant: infoTextSize }}
allowFontScaling={false}
color="$neutral2"
flexGrow={1}
variant={infoTextSize}
>
{t('walletConnect.permissions.option.requestApprovals')}
</Text>
</Flex>
<Flex centered row gap="$spacing8">
<Clear color="$statusCritical" size="$icon.16" />
<Text
$short={{ variant: infoTextSize }}
allowFontScaling={false}
color="$neutral2"
flexGrow={1}
variant={infoTextSize}
>
{t('walletConnect.permissions.option.transferAssets')}
</Text>
</Flex>
</Flex>
)}
</Flex>
{verifyStatus === WalletConnectVerifyStatus.Unverified && (
<Flex row backgroundColor="$surface2" borderRadius="$rounded12" p="$spacing12" justifyContent="space-between">
<Flex row gap="$spacing12">
<AlertTriangleFilled color="$statusWarning" size="$icon.20" />
<Flex gap="$spacing8" maxWidth={PERMISSIONS_MAX_LENGTH}>
<Text color="$statusWarning" variant="buttonLabel3">
{t('walletConnect.pending.unverified.title')}
</Text>
<Text color="$neutral2" variant="body3" textWrap="wrap">
{t('walletConnect.pending.unverified.description')}
</Text>
</Flex>
</Flex>
<AlertCircleFilled color="$neutral3" size="$icon.20" />
</Flex>
)}
{verifyStatus === WalletConnectVerifyStatus.Threat && (
<Flex row backgroundColor="$surface2" borderRadius="$rounded12" p="$spacing12" justifyContent="space-between">
<Flex row gap="$spacing12">
<OctagonExclamation color="$statusCritical" size="$icon.20" />
<Flex gap="$spacing8" maxWidth={PERMISSIONS_MAX_LENGTH}>
<Text color="$statusCritical" variant="buttonLabel3">
{t('walletConnect.pending.threat.title')}
</Text>
<Text color="$neutral2" variant="body3" textWrap="wrap">
{t('walletConnect.pending.threat.description')}
</Text>
<LabeledCheckbox
checked={Boolean(confirmedWarning)}
checkboxPosition="start"
gap="$spacing8"
size="$icon.16"
px="$none"
text={
<Text color="$neutral2" flexShrink={1} variant="body3">
{t('walletConnect.pending.threat.confirmationText')}
</Text>
}
onCheckPressed={handleConfirmWarning}
/>
</Flex>
</Flex>
<AlertCircleFilled color="$neutral3" size="$icon.20" />
</Flex>
)}
</Flex>
)
}
...@@ -10,7 +10,14 @@ import { ...@@ -10,7 +10,14 @@ import {
} from 'uniswap/src/features/gating/configs' } from 'uniswap/src/features/gating/configs'
import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks'
import { isUwULinkAllowlistType } from 'uniswap/src/features/gating/typeGuards' import { isUwULinkAllowlistType } from 'uniswap/src/features/gating/typeGuards'
import { EthTransaction, UwULinkErc20SendRequest, UwULinkMethod, UwULinkRequest } from 'uniswap/src/types/walletConnect' import {
DappRequestType,
EthTransaction,
UwULinkErc20SendRequest,
UwULinkMethod,
UwULinkRequest,
UwULinkRequestInfo,
} from 'uniswap/src/types/walletConnect'
import { areAddressesEqual } from 'uniswap/src/utils/addresses' import { areAddressesEqual } from 'uniswap/src/utils/addresses'
import { ContractManager } from 'wallet/src/features/contracts/ContractManager' import { ContractManager } from 'wallet/src/features/contracts/ContractManager'
import { ProviderManager } from 'wallet/src/features/providers/ProviderManager' import { ProviderManager } from 'wallet/src/features/providers/ProviderManager'
...@@ -115,15 +122,21 @@ export async function getFormattedUwuLinkTxnRequest({ ...@@ -115,15 +122,21 @@ export async function getFormattedUwuLinkTxnRequest({
providerManager, providerManager,
contractManager, contractManager,
}: HandleUwuLinkRequestParams): Promise<{ request: WalletConnectSigningRequest; account: string }> { }: HandleUwuLinkRequestParams): Promise<{ request: WalletConnectSigningRequest; account: string }> {
const newRequest = { const newRequest: {
sessionId: string
internalId: string
account: string
dappRequestInfo: UwULinkRequestInfo
chainId: number
} = {
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,
account: activeAccount?.address, account: activeAccount?.address,
dapp: { dappRequestInfo: {
name: '', name: '',
url: '', url: '',
...request.dapp, ...request.dapp,
source: UWULINK_PREFIX, requestType: DappRequestType.UwULink,
chain_id: request.chainId, chain_id: request.chainId,
webhook: request.webhook, webhook: request.webhook,
}, },
......
...@@ -61,10 +61,10 @@ export function WalletConnectModals(): JSX.Element { ...@@ -61,10 +61,10 @@ export function WalletConnectModals(): JSX.Element {
// When WalletConnectModal is open and a WC QR code is scanned to add a pendingSession, // When WalletConnectModal is open and a WC QR code is scanned to add a pendingSession,
// dismiss the scan modal in favor of showing PendingConnectionModal // dismiss the scan modal in favor of showing PendingConnectionModal
useEffect(() => { useEffect(() => {
if (modalState.isOpen && pendingSession) { if (modalState.isOpen && (pendingSession || currRequest)) {
dispatch(closeModal({ name: ModalName.WalletConnectScan })) dispatch(closeModal({ name: ModalName.WalletConnectScan }))
} }
}, [modalState.isOpen, pendingSession, dispatch]) }, [modalState.isOpen, pendingSession, currRequest, dispatch])
return ( return (
<> <>
......
import React from 'react'
import { PrivateKeySpeedBumpModal } from 'src/components/RestoreWalletModal/PrivateKeySpeedBumpModal'
import { useReactNavigationModal } from 'src/components/modals/useReactNavigationModal'
import { fireEvent, render } from 'src/test/test-utils'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
jest.mock('src/components/modals/useReactNavigationModal', () => ({
useReactNavigationModal: jest.fn(),
}))
jest.mock('@gorhom/bottom-sheet', () => {
const reactNative = jest.requireActual('react-native')
const { View } = reactNative
return {
__esModule: true,
default: View,
BottomSheetModal: View,
BottomSheetModalProvider: View,
BottomSheetView: View,
}
})
describe('PrivateKeySpeedBumpModal', () => {
const mockPreventCloseRef = { current: false }
const mockNavigation = { navigate: jest.fn() }
const mockOnClose = jest.fn()
beforeEach(() => {
jest.clearAllMocks()
;(useReactNavigationModal as jest.Mock).mockReturnValue({
onClose: mockOnClose,
preventCloseRef: mockPreventCloseRef,
})
})
it('renders correctly', () => {
// @ts-expect-error Mocking navigation object since it's not critical to this test
const { toJSON } = render(<PrivateKeySpeedBumpModal navigation={mockNavigation} />)
expect(toJSON()).toMatchSnapshot()
})
it('navigates to ViewPrivateKeys screen when Continue button is pressed', () => {
// @ts-expect-error Mocking navigation object since it's not critical to this test
const screen = render(<PrivateKeySpeedBumpModal navigation={mockNavigation} />)
const continueButton = screen.getByTestId(TestID.Continue)
fireEvent.press(continueButton)
expect(mockPreventCloseRef.current).toBe(true)
expect(mockNavigation.navigate).toHaveBeenCalledWith(MobileScreens.ViewPrivateKeys)
})
})
import React from 'react'
import { useTranslation } from 'react-i18next'
import { AppStackScreenProp } from 'src/app/navigation/types'
import { useReactNavigationModal } from 'src/components/modals/useReactNavigationModal'
import { Button, Flex, IconButton, InlineCard, Text, useSporeColors } from 'ui/src'
import { AlertTriangleFilled, Key } from 'ui/src/components/icons'
import { Modal } from 'uniswap/src/components/modals/Modal'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { SPACE_STRING } from 'utilities/src/primitives/string'
/**
* This modal is used as an informational speedbump before the user
* is sent to the screen to view their private keys.
*/
export function PrivateKeySpeedBumpModal({
navigation,
}: AppStackScreenProp<typeof ModalName.PrivateKeySpeedBumpModal>): JSX.Element | null {
const colors = useSporeColors()
const { onClose, preventCloseRef } = useReactNavigationModal()
const onContinue = (): void => {
preventCloseRef.current = true
navigation.navigate(MobileScreens.ViewPrivateKeys)
}
return (
<Modal backgroundColor={colors.surface1.val} name={ModalName.PrivateKeySpeedBumpModal} onClose={onClose}>
<PrivateKeySpeedBumpModalContent onClose={onClose} onContinue={onContinue} />
</Modal>
)
}
const PrivateKeySpeedBumpModalContent = ({
onClose,
onContinue,
}: {
onClose: () => void
onContinue: () => void
}): JSX.Element => {
const { t } = useTranslation()
return (
<Flex px="$spacing24" pt="$spacing8">
<Flex row justifyContent="center">
<IconButton size="medium" emphasis="secondary" icon={<Key />} onPress={onClose} />
</Flex>
<Text textAlign="center" variant="subheading1" pt="$spacing24">
{t('privateKeys.export.modal.title')}
</Text>
<Text textAlign="center" variant="body2" color="$neutral2" pt="$spacing8">
{t('privateKeys.export.modal.subtitle')}
<Text variant="body2" color="$neutral1" ml="$spacing4" onPress={() => {}}>
{/* TODO(ALL-6735): Add link to learn more about private keys */}
{SPACE_STRING + t('common.button.learn')}
</Text>
</Text>
<Flex pt="$spacing16">
<InlineCard
Icon={AlertTriangleFilled}
color="$neutral2"
description={
<Text variant="body3" color="$neutral2">
{t('privateKeys.export.modal.warning')}
</Text>
}
iconColor="$neutral2"
/>
</Flex>
<Flex row py="$spacing24">
<Button testID={TestID.Continue} variant="branded" emphasis="primary" size="medium" onPress={onContinue}>
{t('common.button.continue')}
</Button>
</Flex>
</Flex>
)
}
import React from 'react' import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { navigate } from 'src/app/navigation/rootNavigation' import { navigate } from 'src/app/navigation/rootNavigation'
import { AppStackScreenProp } from 'src/app/navigation/types'
import { useReactNavigationModal } from 'src/components/modals/useReactNavigationModal' import { useReactNavigationModal } from 'src/components/modals/useReactNavigationModal'
import { WalletRestoreType } from 'src/components/RestoreWalletModal/RestoreWalletModalState'
import { closeAllModals } from 'src/features/modals/modalSlice' import { closeAllModals } from 'src/features/modals/modalSlice'
import { Button, Flex, useSporeColors } from 'ui/src' import { Button, Flex, useSporeColors } from 'ui/src'
import { ArrowDownCircle, WalletFilled } from 'ui/src/components/icons' import { ArrowDownCircleFilledWithBorder, WalletFilled } from 'ui/src/components/icons'
import { spacing } from 'ui/src/theme/spacing' import { spacing } from 'ui/src/theme/spacing'
import { GenericHeader } from 'uniswap/src/components/misc/GenericHeader' import { GenericHeader } from 'uniswap/src/components/misc/GenericHeader'
import { Modal } from 'uniswap/src/components/modals/Modal' import { Modal } from 'uniswap/src/components/modals/Modal'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile'
...@@ -21,43 +24,72 @@ const SHADOW_OPACITY = 0.3 ...@@ -21,43 +24,72 @@ const SHADOW_OPACITY = 0.3
const SHADOW_OFFSET = { width: 0, height: 0 } as const const SHADOW_OFFSET = { width: 0, height: 0 } as const
const ICON_OFFSET = -spacing.spacing8 const ICON_OFFSET = -spacing.spacing8
function BackgroundRing({ size }: { size: number }): JSX.Element { /**
return ( * This modal is used to prompt the user to restore their wallet depending on the type of
<Flex * restoration needed.
position="absolute" */
borderRadius="$roundedFull" export function RestoreWalletModal({ route }: AppStackScreenProp<typeof ModalName.RestoreWallet>): JSX.Element | null {
borderColor="$surface3"
borderWidth="$spacing1"
height={size}
width={size}
top="50%"
left="50%"
transform={[{ translateX: -size / 2 }, { translateY: -size / 2 }]}
/>
)
}
export function RestoreWalletModal(): JSX.Element | null {
const { t } = useTranslation() const { t } = useTranslation()
const colors = useSporeColors() const colors = useSporeColors()
const dispatch = useDispatch() const dispatch = useDispatch()
const { onClose } = useReactNavigationModal() const { onClose } = useReactNavigationModal()
const restoreType = route.params?.restoreType ?? WalletRestoreType.None
const { title, description, isDismissible } = useMemo(() => {
switch (restoreType) {
case WalletRestoreType.SeedPhrase:
return {
title: t('account.wallet.restore.seed_phrase.title'),
description: t('account.wallet.restore.seed_phrase.description'),
isDismissible: true,
}
case WalletRestoreType.NewDevice:
return {
title: t('account.wallet.restore.new_device.title'),
description: t('account.wallet.restore.new_device.description'),
isDismissible: false,
}
default:
return {}
}
}, [restoreType, t])
const onRestore = (): void => { const onRestore = (): void => {
onClose() onClose()
dispatch(closeAllModals()) // still need this until all modals are migrated to react-navigation dispatch(closeAllModals()) // still need this until all modals are migrated to react-navigation
navigate(MobileScreens.OnboardingStack, {
screen: OnboardingScreens.RestoreCloudBackupLoading, switch (restoreType) {
params: { case WalletRestoreType.SeedPhrase: {
entryPoint: OnboardingEntryPoint.Sidebar, navigate(MobileScreens.OnboardingStack, {
importType: ImportType.RestoreMnemonic, screen: OnboardingScreens.RestoreMethod,
}, params: {
}) entryPoint: OnboardingEntryPoint.Sidebar,
importType: ImportType.RestoreMnemonic,
},
})
break
}
case WalletRestoreType.NewDevice: {
navigate(MobileScreens.OnboardingStack, {
screen: OnboardingScreens.RestoreCloudBackupLoading,
params: {
entryPoint: OnboardingEntryPoint.Sidebar,
importType: ImportType.RestoreMnemonic,
},
})
break
}
}
} }
return ( return (
<Modal hideHandlebar backgroundColor={colors.surface1.val} isDismissible={false} name={ModalName.RestoreWallet}> <Modal
hideHandlebar
backgroundColor={colors.surface1.val}
isDismissible={isDismissible}
name={ModalName.RestoreWallet}
onClose={onClose}
>
<Flex centered gap="$spacing24" px="$spacing24" py="$spacing12" backgroundColor="$surface1"> <Flex centered gap="$spacing24" px="$spacing24" py="$spacing12" backgroundColor="$surface1">
<Flex <Flex
centered centered
...@@ -87,23 +119,43 @@ export function RestoreWalletModal(): JSX.Element | null { ...@@ -87,23 +119,43 @@ export function RestoreWalletModal(): JSX.Element | null {
> >
<WalletFilled color="$neutral1" size="$icon.24" /> <WalletFilled color="$neutral1" size="$icon.24" />
<Flex position="absolute" bottom={ICON_OFFSET} right={ICON_OFFSET}> <Flex position="absolute" bottom={ICON_OFFSET} right={ICON_OFFSET}>
<ArrowDownCircle color="$accent1" size="$icon.24" /> <ArrowDownCircleFilledWithBorder color="$accent1" size="$icon.24" />
</Flex> </Flex>
</Flex> </Flex>
</Flex> </Flex>
<GenericHeader <GenericHeader title={title} titleVariant="subheading1" subtitle={description} subtitleVariant="body3" />
title={t('account.wallet.button.restore')} <Flex gap="$spacing8" width="100%">
titleVariant="subheading1" <Flex row>
subtitle={t('account.wallet.restore.description')} <Button testID={TestID.Continue} variant="branded" emphasis="primary" size="medium" onPress={onRestore}>
subtitleVariant="body3" {t('common.button.continue')}
/> </Button>
<Flex row> </Flex>
<Button variant="branded" emphasis="primary" size="large" onPress={onRestore}> {isDismissible && (
{t('common.button.continue')} <Flex row>
</Button> <Button testID={TestID.Cancel} variant="default" emphasis="secondary" size="medium" onPress={onClose}>
{t('common.button.notNow')}
</Button>
</Flex>
)}
</Flex> </Flex>
</Flex> </Flex>
</Modal> </Modal>
) )
} }
function BackgroundRing({ size }: { size: number }): JSX.Element {
return (
<Flex
position="absolute"
borderRadius="$roundedFull"
borderColor="$surface3"
borderWidth="$spacing1"
height={size}
width={size}
top="50%"
left="50%"
transform={[{ translateX: -size / 2 }, { translateY: -size / 2 }]}
/>
)
}
/**
* If the wallet needs to be restored such as migrating to a new device,
* this enum describes the type of restore that is needed.
*/
export enum WalletRestoreType {
None = 'none',
/**
* The wallet needs to be restored because it is a new device. This case is
* when the local app state has been restored but the native private keys and
* seed phrase are not present.
*/
NewDevice = 'device',
/**
* The wallet needs to be restored because the seed phrase is not present. This case
* is when the local app state is using a wallet but it's seed phrase is missing.
*/
SeedPhrase = 'seedPhrase',
}
export interface RestoreWalletModalState {
restoreType: WalletRestoreType
}
import { Action } from '@reduxjs/toolkit' import { default as React, useCallback } from 'react'
import { default as React } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { closeModal } from 'src/features/modals/modalSlice' import { useReactNavigationModal } from 'src/components/modals/useReactNavigationModal'
import { Flex, GeneratedIcon, Text, TouchableArea } from 'ui/src' import { Flex, GeneratedIcon, Text, TouchableArea } from 'ui/src'
import { Check, Contrast, Moon, Sun } from 'ui/src/components/icons' import { Check, Contrast, Moon, Sun } from 'ui/src/components/icons'
import { Modal } from 'uniswap/src/components/modals/Modal' import { Modal } from 'uniswap/src/components/modals/Modal'
...@@ -13,13 +12,10 @@ import { AppearanceSettingType, setSelectedAppearanceSettings } from 'wallet/src ...@@ -13,13 +12,10 @@ import { AppearanceSettingType, setSelectedAppearanceSettings } from 'wallet/src
export function SettingsAppearanceModal(): JSX.Element { export function SettingsAppearanceModal(): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const currentTheme = useCurrentAppearanceSetting() const currentTheme = useCurrentAppearanceSetting()
const dispatch = useDispatch() const { onClose } = useReactNavigationModal()
return ( return (
<Modal <Modal name={ModalName.SettingsAppearance} onClose={onClose}>
name={ModalName.SettingsAppearance}
onClose={(): Action => dispatch(closeModal({ name: ModalName.SettingsAppearance }))}
>
<Flex animation="fast" gap="$spacing16" pb="$spacing24" px="$spacing24" width="100%"> <Flex animation="fast" gap="$spacing16" pb="$spacing24" px="$spacing24" width="100%">
<Flex centered> <Flex centered>
<Text color="$neutral1" variant="subheading1"> <Text color="$neutral1" variant="subheading1">
...@@ -33,6 +29,7 @@ export function SettingsAppearanceModal(): JSX.Element { ...@@ -33,6 +29,7 @@ export function SettingsAppearanceModal(): JSX.Element {
option={AppearanceSettingType.System} option={AppearanceSettingType.System}
subtitle={t('settings.setting.appearance.option.device.subtitle')} subtitle={t('settings.setting.appearance.option.device.subtitle')}
title={t('settings.setting.appearance.option.device.title')} title={t('settings.setting.appearance.option.device.title')}
onClose={onClose}
/> />
<AppearanceOption <AppearanceOption
Icon={Sun} Icon={Sun}
...@@ -40,6 +37,7 @@ export function SettingsAppearanceModal(): JSX.Element { ...@@ -40,6 +37,7 @@ export function SettingsAppearanceModal(): JSX.Element {
option={AppearanceSettingType.Light} option={AppearanceSettingType.Light}
subtitle={t('settings.setting.appearance.option.light.subtitle')} subtitle={t('settings.setting.appearance.option.light.subtitle')}
title={t('settings.setting.appearance.option.light.title')} title={t('settings.setting.appearance.option.light.title')}
onClose={onClose}
/> />
<AppearanceOption <AppearanceOption
Icon={Moon} Icon={Moon}
...@@ -47,6 +45,7 @@ export function SettingsAppearanceModal(): JSX.Element { ...@@ -47,6 +45,7 @@ export function SettingsAppearanceModal(): JSX.Element {
option={AppearanceSettingType.Dark} option={AppearanceSettingType.Dark}
subtitle={t('settings.setting.appearance.option.dark.subtitle')} subtitle={t('settings.setting.appearance.option.dark.subtitle')}
title={t('settings.setting.appearance.option.dark.title')} title={t('settings.setting.appearance.option.dark.title')}
onClose={onClose}
/> />
</Flex> </Flex>
</Flex> </Flex>
...@@ -60,20 +59,26 @@ interface AppearanceOptionProps { ...@@ -60,20 +59,26 @@ interface AppearanceOptionProps {
subtitle: string subtitle: string
option: AppearanceSettingType option: AppearanceSettingType
Icon: GeneratedIcon Icon: GeneratedIcon
onClose: () => void
} }
function AppearanceOption({ active, title, subtitle, Icon, option }: AppearanceOptionProps): JSX.Element { function AppearanceOption({ active, title, subtitle, Icon, option, onClose }: AppearanceOptionProps): JSX.Element {
const dispatch = useDispatch() const dispatch = useDispatch()
const showCheckMarkOpacity = active ? 1 : 0 const showCheckMarkOpacity = active ? 1 : 0
const changeTheme = useCallback(async (): Promise<void> => {
dispatch(setSelectedAppearanceSettings(option))
onClose()
}, [dispatch, option, onClose])
return ( return (
<TouchableArea <TouchableArea
alignItems="center" alignItems="center"
flexDirection="row" flexDirection="row"
justifyContent="space-between" justifyContent="space-between"
py="$spacing12" py="$spacing12"
onPress={(): Action => dispatch(setSelectedAppearanceSettings(option))} onPress={changeTheme}
> >
<Icon color="$neutral2" size="$icon.24" strokeWidth={1.5} /> <Icon color="$neutral2" size="$icon.24" strokeWidth={1.5} />
<Flex row shrink> <Flex row shrink>
......
import { NavigatorScreenParams, useNavigation } from '@react-navigation/native' import { NavigatorScreenParams, useNavigation } from '@react-navigation/native'
import { memo, useCallback } from 'react' import { memo, useCallback } from 'react'
import { ValueOf } from 'react-native-gesture-handler/lib/typescript/typeUtils' import { ValueOf } from 'react-native-gesture-handler/lib/typescript/typeUtils'
import { useDispatch } from 'react-redux'
import { navigate } from 'src/app/navigation/rootNavigation' import { navigate } from 'src/app/navigation/rootNavigation'
import { import {
AppStackNavigationProp, AppStackNavigationProp,
...@@ -12,7 +11,6 @@ import { ...@@ -12,7 +11,6 @@ import {
} from 'src/app/navigation/types' } from 'src/app/navigation/types'
import { ConnectionsDappsListModalState } from 'src/components/Settings/ConnectionsDappModal/ConnectionsDappsListModalState' import { ConnectionsDappsListModalState } from 'src/components/Settings/ConnectionsDappModal/ConnectionsDappsListModalState'
import { EditWalletSettingsModalState } from 'src/components/Settings/EditWalletModal/EditWalletSettingsModalState' import { EditWalletSettingsModalState } from 'src/components/Settings/EditWalletModal/EditWalletSettingsModalState'
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'
...@@ -36,12 +34,6 @@ export interface SettingsSectionItemComponent { ...@@ -36,12 +34,6 @@ export interface SettingsSectionItemComponent {
isHidden?: boolean isHidden?: boolean
} }
type SettingsModal =
| typeof ModalName.LanguageSelector
| typeof ModalName.SettingsAppearance
| typeof ModalName.PortfolioBalanceModal
| typeof ModalName.PermissionsModal
type SettingsNavigationModal = type SettingsNavigationModal =
| typeof ModalName.BiometricsModal | typeof ModalName.BiometricsModal
| typeof ModalName.FiatCurrencySelector | typeof ModalName.FiatCurrencySelector
...@@ -50,11 +42,16 @@ type SettingsNavigationModal = ...@@ -50,11 +42,16 @@ type SettingsNavigationModal =
| typeof ModalName.ConnectionsDappListModal | typeof ModalName.ConnectionsDappListModal
| typeof ModalName.SmartWalletAdvancedSettingsModal | typeof ModalName.SmartWalletAdvancedSettingsModal
| typeof ModalName.PasskeyManagement | typeof ModalName.PasskeyManagement
| typeof ModalName.Experiments
| typeof ModalName.SettingsAppearance
| typeof ModalName.PermissionsModal
| typeof ModalName.PortfolioBalanceModal
| typeof ModalName.LanguageSelector
export interface SettingsSectionItem { export interface SettingsSectionItem {
screen?: keyof SettingsStackParamList | typeof MobileScreens.OnboardingStack screen?: keyof SettingsStackParamList | typeof MobileScreens.OnboardingStack
modal?: SettingsModal
navigationModal?: SettingsNavigationModal navigationModal?: SettingsNavigationModal
testID?: string
screenProps?: ValueOf<SettingsStackParamList> | NavigatorScreenParams<OnboardingStackParamList> screenProps?: ValueOf<SettingsStackParamList> | NavigatorScreenParams<OnboardingStackParamList>
navigationProps?: navigationProps?:
| ConnectionsDappsListModalState | ConnectionsDappsListModalState
...@@ -78,13 +75,13 @@ interface SettingsRowProps { ...@@ -78,13 +75,13 @@ interface SettingsRowProps {
page: SettingsSectionItem page: SettingsSectionItem
navigation: SettingsStackNavigationProp & OnboardingStackNavigationProp navigation: SettingsStackNavigationProp & OnboardingStackNavigationProp
checkIfCanProceed?: SettingsSectionItem['checkIfCanProceed'] checkIfCanProceed?: SettingsSectionItem['checkIfCanProceed']
testID?: string
} }
export const SettingsRow = memo( export const SettingsRow = memo(
({ ({
page: { page: {
screen, screen,
modal,
navigationModal, navigationModal,
screenProps, screenProps,
navigationProps, navigationProps,
...@@ -98,12 +95,12 @@ export const SettingsRow = memo( ...@@ -98,12 +95,12 @@ export const SettingsRow = memo(
onToggle, onToggle,
isToggleEnabled, isToggleEnabled,
count, count,
testID,
}, },
navigation, navigation,
checkIfCanProceed, checkIfCanProceed,
}: SettingsRowProps): JSX.Element => { }: SettingsRowProps): JSX.Element => {
const colors = useSporeColors() const colors = useSporeColors()
const dispatch = useDispatch()
const handleRow = useCallback(async (): Promise<void> => { const handleRow = useCallback(async (): Promise<void> => {
if (checkIfCanProceed && !checkIfCanProceed()) { if (checkIfCanProceed && !checkIfCanProceed()) {
...@@ -114,28 +111,15 @@ export const SettingsRow = memo( ...@@ -114,28 +111,15 @@ export const SettingsRow = memo(
return return
} else if (screen) { } else if (screen) {
navigation.navigate(screen, screenProps) navigation.navigate(screen, screenProps)
} else if (modal) {
dispatch(openModal({ name: modal }))
} else if (navigationModal) { } else if (navigationModal) {
navigate(navigationModal, navigationProps) navigate(navigationModal, navigationProps)
} else if (externalLink) { } else if (externalLink) {
await openUri(externalLink) await openUri(externalLink)
} }
}, [ }, [checkIfCanProceed, onToggle, screen, navigation, screenProps, navigationProps, navigationModal, externalLink])
checkIfCanProceed,
onToggle,
screen,
navigation,
screenProps,
navigationProps,
modal,
navigationModal,
dispatch,
externalLink,
])
return ( return (
<TouchableArea disabled={Boolean(action)} onPress={handleRow}> <TouchableArea disabled={Boolean(action)} testID={testID} onPress={handleRow}>
<Flex grow row alignItems="center" gap="$spacing12" minHeight={40}> <Flex grow row alignItems="center" gap="$spacing12" minHeight={40}>
<Flex grow row alignItems={subText ? 'flex-start' : 'center'} flexBasis={0} gap="$spacing12"> <Flex grow row alignItems={subText ? 'flex-start' : 'center'} flexBasis={0} gap="$spacing12">
<Flex centered height={32} width={32}> <Flex centered height={32} width={32}>
...@@ -157,7 +141,6 @@ export const SettingsRow = memo( ...@@ -157,7 +141,6 @@ export const SettingsRow = memo(
)} )}
<RowRightContent <RowRightContent
screen={screen} screen={screen}
modal={modal}
navigationModal={navigationModal} navigationModal={navigationModal}
externalLink={externalLink} externalLink={externalLink}
disabled={disabled} disabled={disabled}
...@@ -193,7 +176,6 @@ const LOADING_DIMENSIONS = { ...@@ -193,7 +176,6 @@ const LOADING_DIMENSIONS = {
const RowRightContent = memo( const RowRightContent = memo(
({ ({
screen, screen,
modal,
navigationModal, navigationModal,
externalLink, externalLink,
disabled, disabled,
...@@ -205,7 +187,6 @@ const RowRightContent = memo( ...@@ -205,7 +187,6 @@ const RowRightContent = memo(
}: Pick< }: Pick<
SettingsSectionItem, SettingsSectionItem,
| 'screen' | 'screen'
| 'modal'
| 'navigationModal' | 'navigationModal'
| 'externalLink' | 'externalLink'
| 'disabled' | 'disabled'
...@@ -236,7 +217,7 @@ const RowRightContent = memo( ...@@ -236,7 +217,7 @@ const RowRightContent = memo(
) )
} }
if (screen || modal || navigationModal) { if (screen || navigationModal) {
return ( return (
<Flex centered row> <Flex centered row>
{currentSetting && {currentSetting &&
......
...@@ -3,6 +3,7 @@ import { Alert } from 'react-native' ...@@ -3,6 +3,7 @@ import { Alert } from 'react-native'
import { Accordion, Flex, Text } from 'ui/src' import { Accordion, Flex, Text } from 'ui/src'
import { GatingButton } from 'uniswap/src/components/gating/GatingButton' import { GatingButton } from 'uniswap/src/components/gating/GatingButton'
import { AccordionHeader } from 'uniswap/src/components/gating/GatingOverrides' import { AccordionHeader } from 'uniswap/src/components/gating/GatingOverrides'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
export function MissileaneousDevSection(): JSX.Element { export function MissileaneousDevSection(): JSX.Element {
...@@ -11,11 +12,11 @@ export function MissileaneousDevSection(): JSX.Element { ...@@ -11,11 +12,11 @@ export function MissileaneousDevSection(): JSX.Element {
<Text variant="heading3">Misc.</Text> <Text variant="heading3">Misc.</Text>
<Flex flexDirection="column"> <Flex flexDirection="column">
<Accordion.Item value="other-configs"> <Accordion.Item value="other-configs">
<AccordionHeader title="🤯 Seed Phrase & Private Keys" /> <AccordionHeader title="🤯 Seed Phrase & Private Keys" testId={TestID.DevSeedPhrasePrivateKeysAccordion} />
<Accordion.Content gap="$spacing12"> <Accordion.Content testID={TestID.DevDeleteSeedPhraseButton} gap="$spacing12">
<GatingButton onPress={onDeleteSeedPhrase}>Delete Seed Phrase (Irreversible)</GatingButton> <GatingButton onPress={onDeleteSeedPhrase}>Delete Seed Phrase (Irreversible)</GatingButton>
</Accordion.Content> </Accordion.Content>
<Accordion.Content gap="$spacing12"> <Accordion.Content testID={TestID.DevDeletePrivateKeysButton} gap="$spacing12">
<GatingButton onPress={onDeletePrivateKeys}>Delete Private Keys (Irreversible)</GatingButton> <GatingButton onPress={onDeletePrivateKeys}>Delete Private Keys (Irreversible)</GatingButton>
</Accordion.Content> </Accordion.Content>
</Accordion.Item> </Accordion.Item>
......
...@@ -5,6 +5,7 @@ import { SearchEmptySection } from 'src/components/explore/search/SearchEmptySec ...@@ -5,6 +5,7 @@ import { SearchEmptySection } from 'src/components/explore/search/SearchEmptySec
import { SearchResultsSection } from 'src/components/explore/search/SearchResultsSection' import { SearchResultsSection } from 'src/components/explore/search/SearchResultsSection'
import { Flex, Text, TouchableArea, flexStyles } from 'ui/src' import { Flex, Text, TouchableArea, flexStyles } from 'ui/src'
import { useTranslation } from 'react-i18next'
import { UniverseChainId } from 'uniswap/src/features/chains/types' import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
...@@ -63,15 +64,33 @@ function NewExploreSearchResultsList({ ...@@ -63,15 +64,33 @@ function NewExploreSearchResultsList({
debouncedSearchQuery: string | null debouncedSearchQuery: string | null
debouncedParsedSearchQuery: string | null debouncedParsedSearchQuery: string | null
}): JSX.Element { }): JSX.Element {
const { t } = useTranslation()
const [activeTab, setActiveTab] = useState<SearchTab>(SearchTab.All) const [activeTab, setActiveTab] = useState<SearchTab>(SearchTab.All)
// So that the linter errors if someone adds a new tab without updating the switch statement
// eslint-disable-next-line consistent-return
const getTabLabel = (tab: SearchTab): string => {
switch (tab) {
case SearchTab.All:
return t('common.all')
case SearchTab.Tokens:
return t('common.tokens')
case SearchTab.Pools:
return t('common.pools')
case SearchTab.Wallets:
return t('explore.search.section.wallets')
case SearchTab.NFTCollections:
return t('common.nfts')
}
}
return ( return (
<Trace section={SectionName.ExploreSearch}> <Trace section={SectionName.ExploreSearch}>
<Flex row px="$spacing20" pt="$spacing16" pb="$spacing8" gap="$spacing16"> <Flex row px="$spacing20" pt="$spacing16" pb="$spacing8" gap="$spacing16">
{MOBILE_SEARCH_TABS.map((tab) => ( {MOBILE_SEARCH_TABS.map((tab) => (
<TouchableArea key={tab} onPress={() => setActiveTab(tab)}> <TouchableArea key={tab} onPress={() => setActiveTab(tab)}>
<Text color={activeTab === tab ? '$neutral1' : '$neutral2'} variant="buttonLabel2"> <Text color={activeTab === tab ? '$neutral1' : '$neutral2'} variant="buttonLabel2">
{tab} {getTabLabel(tab)}
</Text> </Text>
</TouchableArea> </TouchableArea>
))} ))}
......
import { useFocusEffect, useNavigation } from '@react-navigation/core'
import { addScreenshotListener } from 'expo-screen-capture' import { addScreenshotListener } from 'expo-screen-capture'
import React, { useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { usePrevious } from 'react-native-wagmi-charts'
import { navigate } from 'src/app/navigation/rootNavigation' import { navigate } from 'src/app/navigation/rootNavigation'
import { WalletRestoreType } from 'src/components/RestoreWalletModal/RestoreWalletModalState'
import { MnemonicDisplay } from 'src/components/mnemonic/MnemonicDisplay' import { MnemonicDisplay } from 'src/components/mnemonic/MnemonicDisplay'
import { useBiometricAppSettings } from 'src/features/biometrics/useBiometricAppSettings' import { useBiometricAppSettings } from 'src/features/biometrics/useBiometricAppSettings'
import { useBiometricPrompt } from 'src/features/biometricsSettings/hooks' import { useBiometricPrompt } from 'src/features/biometricsSettings/hooks'
...@@ -21,17 +22,27 @@ type Props = { ...@@ -21,17 +22,27 @@ type Props = {
export function SeedPhraseDisplay({ mnemonicId, onDismiss, walletNeedsRestore }: Props): JSX.Element { export function SeedPhraseDisplay({ mnemonicId, onDismiss, walletNeedsRestore }: Props): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const { isModalOpen: isWalletRestoreModalOpen } = useWalletRestore({ openModalImmediately: true }) const { walletRestoreType } = useWalletRestore({
openModalImmediately: true,
})
const [showSeedPhrase, setShowSeedPhrase] = useState(false) const [showSeedPhrase, setShowSeedPhrase] = useState(false)
const navigation = useNavigation()
const [showSeedPhraseViewWarningModal, setShowSeedPhraseViewWarningModal] = useState(!walletNeedsRestore) const [showSeedPhraseViewWarningModal, setShowSeedPhraseViewWarningModal] = useState(!walletNeedsRestore)
const prevIsWalletRestoreModalOpen = usePrevious(isWalletRestoreModalOpen) useFocusEffect(
useCallback(() => {
if (walletRestoreType !== WalletRestoreType.None) {
navigation.goBack()
useEffect(() => { // This is a very unlikely edge case if the user somehow get to this screen on a new device.
if (prevIsWalletRestoreModalOpen && !isWalletRestoreModalOpen) { // In this case, we want to back an additional time to dismiss the NewDevice modal which is
onDismiss?.() // will try to reopen anytime this screen is focused.
} if (walletRestoreType === WalletRestoreType.NewDevice) {
}) navigation.goBack()
}
}
}, [walletRestoreType, navigation]),
)
const onShowSeedPhraseConfirmed = (): void => { const onShowSeedPhraseConfirmed = (): void => {
setShowSeedPhrase(true) setShowSeedPhrase(true)
......
import { AppStackScreenProp } from 'src/app/navigation/types'
import { ReactNavigationModal } from 'src/components/modals/ReactNavigationModals/ReactNavigationModal'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { SettingsLanguageModal } from 'wallet/src/components/settings/language/SettingsLanguageModal'
export const LanguageSettingsScreen = (props: AppStackScreenProp<typeof ModalName.LanguageSelector>): JSX.Element => {
return <ReactNavigationModal {...props} modalComponent={SettingsLanguageModal} />
}
import { AppStackScreenProp } from 'src/app/navigation/types'
import { ReactNavigationModal } from 'src/components/modals/ReactNavigationModals/ReactNavigationModal'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { PermissionsModal } from 'wallet/src/components/settings/permissions/PermissionsModal'
export const PermissionsSettingsScreen = (
props: AppStackScreenProp<typeof ModalName.PermissionsModal>,
): JSX.Element => {
return <ReactNavigationModal {...props} modalComponent={PermissionsModal} />
}
import { AppStackScreenProp } from 'src/app/navigation/types'
import { ReactNavigationModal } from 'src/components/modals/ReactNavigationModals/ReactNavigationModal'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { PortfolioBalanceModal } from 'wallet/src/components/settings/portfolioBalance/PortfolioBalanceModal'
export const PortfolioBalanceSettingsScreen = (
props: AppStackScreenProp<typeof ModalName.PortfolioBalanceModal>,
): JSX.Element => {
return <ReactNavigationModal {...props} modalComponent={PortfolioBalanceModal} />
}
import { AppStackScreenProp } from 'src/app/navigation/types'
import { ReactNavigationModal } from 'src/components/modals/ReactNavigationModals/ReactNavigationModal'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { PostSwapSmartWalletNudge } from 'wallet/src/components/smartWallet/modals/PostSwapSmartWalletNudge'
export const PostSwapSmartWalletNudgeScreen = (
props: AppStackScreenProp<typeof ModalName.PostSwapSmartWalletNudge>,
): JSX.Element => {
return <ReactNavigationModal {...props} modalComponent={PostSwapSmartWalletNudge} />
}
...@@ -7,7 +7,14 @@ import { PasskeysHelpModal } from 'uniswap/src/features/passkey/PasskeysHelpModa ...@@ -7,7 +7,14 @@ import { PasskeysHelpModal } from 'uniswap/src/features/passkey/PasskeysHelpModa
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'
import { SettingsLanguageModal } from 'wallet/src/components/settings/language/SettingsLanguageModal'
import { PermissionsModal } from 'wallet/src/components/settings/permissions/PermissionsModal'
import { PortfolioBalanceModal } from 'wallet/src/components/settings/portfolioBalance/PortfolioBalanceModal'
import { PostSwapSmartWalletNudge } from 'wallet/src/components/smartWallet/modals/PostSwapSmartWalletNudge'
import { SmartWalletEnabledModal } from 'wallet/src/components/smartWallet/modals/SmartWalletEnabledModal'
import { SmartWalletAdvancedSettingsModal } from 'wallet/src/features/smartWallet/modals/SmartWalletAdvancedSettingsModal' import { SmartWalletAdvancedSettingsModal } from 'wallet/src/features/smartWallet/modals/SmartWalletAdvancedSettingsModal'
import { SmartWalletConfirmModal } from 'wallet/src/features/smartWallet/modals/SmartWalletConfirmModal'
import { SmartWalletInsufficientFundsOnNetworkModal } from 'wallet/src/features/smartWallet/modals/SmartWalletInsufficientFundsOnNetworkModal'
// 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<
...@@ -17,6 +24,13 @@ type ValidModalNames = keyof Pick< ...@@ -17,6 +24,13 @@ type ValidModalNames = keyof Pick<
| typeof ModalName.PasskeyManagement | typeof ModalName.PasskeyManagement
| typeof ModalName.PasskeysHelp | typeof ModalName.PasskeysHelp
| typeof ModalName.SmartWalletAdvancedSettingsModal | typeof ModalName.SmartWalletAdvancedSettingsModal
| typeof ModalName.SmartWalletConfirmModal
| typeof ModalName.SmartWalletEnabledModal
| typeof ModalName.SmartWalletInsufficientFundsOnNetworkModal
| typeof ModalName.PostSwapSmartWalletNudge
| typeof ModalName.PermissionsModal
| typeof ModalName.PortfolioBalanceModal
| typeof ModalName.LanguageSelector
> >
type ModalNameWithComponentProps = { type ModalNameWithComponentProps = {
...@@ -24,7 +38,14 @@ type ModalNameWithComponentProps = { ...@@ -24,7 +38,14 @@ type ModalNameWithComponentProps = {
[ModalName.HiddenTokenInfoModal]: GetProps<typeof HiddenTokenInfoModal> [ModalName.HiddenTokenInfoModal]: GetProps<typeof HiddenTokenInfoModal>
[ModalName.PasskeyManagement]: GetProps<typeof PasskeyManagementModal> [ModalName.PasskeyManagement]: GetProps<typeof PasskeyManagementModal>
[ModalName.PasskeysHelp]: GetProps<typeof PasskeysHelpModal> [ModalName.PasskeysHelp]: GetProps<typeof PasskeysHelpModal>
[ModalName.PostSwapSmartWalletNudge]: GetProps<typeof PostSwapSmartWalletNudge>
[ModalName.SmartWalletAdvancedSettingsModal]: GetProps<typeof SmartWalletAdvancedSettingsModal> [ModalName.SmartWalletAdvancedSettingsModal]: GetProps<typeof SmartWalletAdvancedSettingsModal>
[ModalName.SmartWalletConfirmModal]: GetProps<typeof SmartWalletConfirmModal>
[ModalName.SmartWalletEnabledModal]: GetProps<typeof SmartWalletEnabledModal>
[ModalName.SmartWalletInsufficientFundsOnNetworkModal]: GetProps<typeof SmartWalletInsufficientFundsOnNetworkModal>
[ModalName.PermissionsModal]: GetProps<typeof PermissionsModal>
[ModalName.PortfolioBalanceModal]: GetProps<typeof PortfolioBalanceModal>
[ModalName.LanguageSelector]: GetProps<typeof SettingsLanguageModal>
} }
type NavigationModalProps<ModalName extends ValidModalNames> = { type NavigationModalProps<ModalName extends ValidModalNames> = {
......
import { AppStackScreenProp } from 'src/app/navigation/types'
import { ReactNavigationModal } from 'src/components/modals/ReactNavigationModals/ReactNavigationModal'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { SmartWalletConfirmModal } from 'wallet/src/features/smartWallet/modals/SmartWalletConfirmModal'
export const SmartWalletConfirmModalScreen = (
props: AppStackScreenProp<typeof ModalName.SmartWalletConfirmModal>,
): JSX.Element => {
return <ReactNavigationModal {...props} modalComponent={SmartWalletConfirmModal} />
}
import { AppStackScreenProp } from 'src/app/navigation/types'
import { ReactNavigationModal } from 'src/components/modals/ReactNavigationModals/ReactNavigationModal'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { SmartWalletEnabledModal } from 'wallet/src/components/smartWallet/modals/SmartWalletEnabledModal'
export const SmartWalletEnabledModalScreen = (
props: AppStackScreenProp<typeof ModalName.SmartWalletEnabledModal>,
): JSX.Element => {
return <ReactNavigationModal {...props} modalComponent={SmartWalletEnabledModal} />
}
import { AppStackScreenProp } from 'src/app/navigation/types'
import { ReactNavigationModal } from 'src/components/modals/ReactNavigationModals/ReactNavigationModal'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { SmartWalletInsufficientFundsOnNetworkModal } from 'wallet/src/features/smartWallet/modals/SmartWalletInsufficientFundsOnNetworkModal'
export const SmartWalletInsufficientFundsOnNetworkScreen = (
props: AppStackScreenProp<typeof ModalName.SmartWalletInsufficientFundsOnNetworkModal>,
): JSX.Element => {
return <ReactNavigationModal {...props} modalComponent={SmartWalletInsufficientFundsOnNetworkModal} />
}
import { act, renderHook } from '@testing-library/react-hooks'
import { useReactNavigationModal } from 'src/components/modals/useReactNavigationModal'
const mockGoBack = jest.fn()
jest.mock('src/app/navigation/types', () => ({
useAppStackNavigation: jest.fn(() => ({
goBack: mockGoBack,
})),
}))
describe('useReactNavigationModal', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('should call navigation.goBack when onClose is called', () => {
const { result } = renderHook(() => useReactNavigationModal())
expect(result.current.preventCloseRef.current).toBe(false)
act(() => {
result.current.onClose()
result.current.onClose()
result.current.onClose()
result.current.onClose()
})
expect(mockGoBack).toHaveBeenCalledTimes(1)
expect(result.current.preventCloseRef.current).toBe(true)
})
it('should not call navigation.goBack when preventCloseRef is true', () => {
const { result } = renderHook(() => useReactNavigationModal())
act(() => {
result.current.preventCloseRef.current = true
})
act(() => {
result.current.onClose()
})
expect(mockGoBack).not.toHaveBeenCalled()
})
})
import { useCallback, useRef } from 'react' import { MutableRefObject, useCallback, useRef } from 'react'
import { useAppStackNavigation } from 'src/app/navigation/types' import { useAppStackNavigation } from 'src/app/navigation/types'
/** /**
...@@ -8,22 +8,26 @@ import { useAppStackNavigation } from 'src/app/navigation/types' ...@@ -8,22 +8,26 @@ import { useAppStackNavigation } from 'src/app/navigation/types'
*/ */
export function useReactNavigationModal(): { export function useReactNavigationModal(): {
onClose: () => void onClose: () => void
/**
* Needed to prevent the modal from being closed twice, which can
* happen if the modal is dismissed by pressing a close button in the
* modal and also when it gets called when the modal closes.
*/
preventCloseRef: MutableRefObject<boolean>
} { } {
const navigation = useAppStackNavigation() const navigation = useAppStackNavigation()
// Needed to prevent the modal from being closed twice, which can const preventCloseRef = useRef(false)
// happen if the modal is dismissed by pressing a close button in the
// modal and also when it gets called when the modal closes.
const closeHasBeenCalledRef = useRef(false)
const onClose = useCallback(() => { const onClose = useCallback(() => {
if (closeHasBeenCalledRef.current) { if (preventCloseRef.current) {
return return
} }
closeHasBeenCalledRef.current = true preventCloseRef.current = true
navigation.goBack() navigation.goBack()
}, [navigation]) }, [navigation])
return { return {
onClose, onClose,
preventCloseRef,
} }
} }
...@@ -12,12 +12,8 @@ export interface AppModalState<T> { ...@@ -12,12 +12,8 @@ export interface AppModalState<T> {
export interface ModalsState { export interface ModalsState {
[ModalName.Experiments]: AppModalState<undefined> [ModalName.Experiments]: AppModalState<undefined>
[ModalName.FiatOnRampAggregator]: AppModalState<FiatOnRampModalState> [ModalName.FiatOnRampAggregator]: AppModalState<FiatOnRampModalState>
[ModalName.LanguageSelector]: AppModalState<undefined>
[ModalName.PortfolioBalanceModal]: AppModalState<undefined>
[ModalName.PermissionsModal]: AppModalState<undefined>
[ModalName.QueuedOrderModal]: AppModalState<undefined> [ModalName.QueuedOrderModal]: AppModalState<undefined>
[ModalName.Send]: AppModalState<TransactionState & { sendScreen: TransactionScreen }> [ModalName.Send]: AppModalState<TransactionState & { sendScreen: TransactionScreen }>
[ModalName.Swap]: AppModalState<TransactionState> [ModalName.Swap]: AppModalState<TransactionState>
[ModalName.SettingsAppearance]: AppModalState<undefined>
[ModalName.WalletConnectScan]: AppModalState<ScannerModalState> [ModalName.WalletConnectScan]: AppModalState<ScannerModalState>
} }
...@@ -22,21 +22,6 @@ type FiatOnRampAggregatorModalParams = { ...@@ -22,21 +22,6 @@ type FiatOnRampAggregatorModalParams = {
initialState?: FiatOnRampModalState initialState?: FiatOnRampModalState
} }
type LanguageSelectorModalParams = {
name: typeof ModalName.LanguageSelector
initialState?: undefined
}
type SettingsAppearanceModalParams = {
name: typeof ModalName.SettingsAppearance
initialState?: undefined
}
type PortfolioBalanceModalParams = {
name: typeof ModalName.PortfolioBalanceModal
initialState?: undefined
}
type WalletConnectModalParams = { type WalletConnectModalParams = {
name: typeof ModalName.WalletConnectScan name: typeof ModalName.WalletConnectScan
initialState: ScannerModalState initialState: ScannerModalState
...@@ -51,18 +36,9 @@ type SendModalParams = { ...@@ -51,18 +36,9 @@ type SendModalParams = {
} }
} }
type PermissionsModalParams = {
name: typeof ModalName.PermissionsModal
initialState?: undefined
}
export type OpenModalParams = export type OpenModalParams =
| FiatOnRampAggregatorModalParams | FiatOnRampAggregatorModalParams
| PortfolioBalanceModalParams
| PermissionsModalParams
| LanguageSelectorModalParams
| SendModalParams | SendModalParams
| SettingsAppearanceModalParams
| SwapModalParams | SwapModalParams
| WalletConnectModalParams | WalletConnectModalParams
......
...@@ -10,7 +10,7 @@ import { ApplicationTransport } from 'utilities/src/telemetry/analytics/Applicat ...@@ -10,7 +10,7 @@ import { ApplicationTransport } from 'utilities/src/telemetry/analytics/Applicat
// eslint-disable-next-line @typescript-eslint/no-restricted-imports // eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { analytics } from 'utilities/src/telemetry/analytics/analytics' import { analytics } from 'utilities/src/telemetry/analytics/analytics'
import { selectAllowAnalytics } from 'wallet/src/features/telemetry/selectors' import { selectAllowAnalytics } from 'wallet/src/features/telemetry/selectors'
import { watchTransactionEvents } from 'wallet/src/features/transactions/transactionWatcherSaga' import { watchTransactionEvents } from 'wallet/src/features/transactions/watcher/transactionFinalizationSaga'
export function* telemetrySaga() { export function* telemetrySaga() {
yield* delay(1) yield* delay(1)
......
import { StackActions } from '@react-navigation/core' import { CommonActions } from '@react-navigation/core'
import { dispatchNavigationAction } from 'src/app/navigation/rootNavigation' import { dispatchNavigationAction } from 'src/app/navigation/rootNavigation'
import { call, put, takeEvery } from 'typed-redux-saga' import { call, put, takeEvery } from 'typed-redux-saga'
import { pushNotification } from 'uniswap/src/features/notifications/slice' import { pushNotification } from 'uniswap/src/features/notifications/slice'
...@@ -21,5 +21,11 @@ function* onRestoreMnemonicComplete() { ...@@ -21,5 +21,11 @@ function* onRestoreMnemonicComplete() {
title: i18n.t('notification.restore.success'), title: i18n.t('notification.restore.success'),
}), }),
) )
yield* call(dispatchNavigationAction, StackActions.replace(MobileScreens.Home)) yield* call(
dispatchNavigationAction,
CommonActions.reset({
index: 0,
routes: [{ name: MobileScreens.Home }],
}),
)
} }
import { checkWalletNeedsRestore, WalletRestoreType } from 'src/features/wallet/useWalletRestore' import { WalletRestoreType } from 'src/components/RestoreWalletModal/RestoreWalletModalState'
import { checkWalletNeedsRestore } from 'src/features/wallet/useWalletRestore'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
jest.mock('wallet/src/features/wallet/Keyring/Keyring', () => ({ jest.mock('wallet/src/features/wallet/Keyring/Keyring', () => ({
......
import { useFocusEffect, useIsFocused } from '@react-navigation/core' import { useFocusEffect } from '@react-navigation/core'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { navigate } from 'src/app/navigation/rootNavigation' import { navigate } from 'src/app/navigation/rootNavigation'
import { WalletRestoreType } from 'src/components/RestoreWalletModal/RestoreWalletModalState'
import { FeatureFlags } from 'uniswap/src/features/gating/flags' 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 { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
...@@ -8,53 +9,39 @@ import { logger } from 'utilities/src/logger/logger' ...@@ -8,53 +9,39 @@ import { logger } from 'utilities/src/logger/logger'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
import { useSignerAccounts } from 'wallet/src/features/wallet/hooks' import { useSignerAccounts } from 'wallet/src/features/wallet/hooks'
export enum WalletRestoreType {
None = 'none',
/**
* The wallet needs to be restored because it is a new device. This case is
* when the local app state has been restored but the native private keys and
* seed phrase are not present.
*/
NewDevice = 'device',
/**
* The wallet needs to be restored because the seed phrase is not present. This case
* is when the local app state is using a wallet but it's seed phrase is missing.
*/
SeedPhrase = 'seedPhrase',
}
type Props = { type Props = {
/** /**
* Whether the modal can be dismissed. Used when the restore modal is optional and managed by the parent component. * If true, this hook will be responsible for opening the restore modal. Otherwise
* the caller is responsible for opening the restore modal.
*/ */
openModalImmediately?: boolean openModalImmediately?: boolean
} }
/** /**
* Hook to determine if the wallet needs to be restored and what type of restoration is needed. * Hook to determine if the wallet needs to be restored and what type of restore is needed.
* If a restoration is needed, the relevant modal will be opened. * If a restore is needed, the relevant modal will be opened.
*/ */
export function useWalletRestore(params?: Props): { export function useWalletRestore(params?: Props): {
walletNeedsRestore: boolean walletNeedsRestore: boolean
openWalletRestoreModal: () => void openWalletRestoreModal: () => void
isModalOpen: boolean walletRestoreType: WalletRestoreType
} { } {
const shouldRestoreSeedPhraseFF = useFeatureFlag(FeatureFlags.RestoreSeedPhrase) const shouldRestoreSeedPhraseFF = useFeatureFlag(FeatureFlags.RestoreSeedPhrase)
const { openModalImmediately } = params ?? {}
const openedOnce = useRef(false)
const openModalImmediately = params?.openModalImmediately
const [walletRestoreType, setWalletRestoreType] = useState<WalletRestoreType>(WalletRestoreType.None) const [walletRestoreType, setWalletRestoreType] = useState<WalletRestoreType>(WalletRestoreType.None)
const walletNeedsRestore = walletRestoreType !== WalletRestoreType.None const walletNeedsRestore = walletRestoreType !== WalletRestoreType.None
const mnemonicIdFromLocalState = useSignerAccounts()[0]?.mnemonicId const mnemonicIdFromLocalState = useSignerAccounts()[0]?.mnemonicId
const isModalOpen = useIsFocused()
const openWalletRestoreModal = useCallback((): void => { const openWalletRestoreModal = useCallback((): void => {
switch (walletRestoreType) { switch (walletRestoreType) {
case WalletRestoreType.NewDevice: case WalletRestoreType.NewDevice:
navigate(ModalName.RestoreWallet) navigate(ModalName.RestoreWallet, { restoreType: WalletRestoreType.NewDevice })
break break
case WalletRestoreType.SeedPhrase: case WalletRestoreType.SeedPhrase:
navigate(ModalName.RestoreWallet) openedOnce.current = true
navigate(ModalName.RestoreWallet, { restoreType: WalletRestoreType.SeedPhrase })
break break
case WalletRestoreType.None: case WalletRestoreType.None:
break break
...@@ -71,13 +58,13 @@ export function useWalletRestore(params?: Props): { ...@@ -71,13 +58,13 @@ export function useWalletRestore(params?: Props): {
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
if (openModalImmediately && walletNeedsRestore) { if (openModalImmediately && walletNeedsRestore && !openedOnce.current) {
openWalletRestoreModal() openWalletRestoreModal()
} }
}, [openModalImmediately, openWalletRestoreModal, walletNeedsRestore]), }, [openModalImmediately, openWalletRestoreModal, walletNeedsRestore]),
) )
return { walletNeedsRestore, openWalletRestoreModal, isModalOpen } return { walletNeedsRestore, openWalletRestoreModal, walletRestoreType }
} }
/** /**
......
import { getInternalError, getSdkError } from '@walletconnect/utils' import { getInternalError, getSdkError } from '@walletconnect/utils'
import { navigate } from 'src/app/navigation/rootNavigation'
import { wcWeb3Wallet } from 'src/features/walletConnect/walletConnectClient' import { wcWeb3Wallet } from 'src/features/walletConnect/walletConnectClient'
import { WalletSendCallsRequest, addRequest } from 'src/features/walletConnect/walletConnectSlice' import { WalletSendCallsRequest, addRequest } from 'src/features/walletConnect/walletConnectSlice'
import { call, put } from 'typed-redux-saga' import { call, put, select } from 'typed-redux-saga'
import { UNISWAP_DELEGATION_ADDRESS } from 'uniswap/src/constants/addresses' import { UNISWAP_DELEGATION_ADDRESS } from 'uniswap/src/constants/addresses'
import { fetchWalletEncoding7702 } from 'uniswap/src/data/apiClients/tradingApi/TradingApiClient' import { fetchWalletEncoding7702 } from 'uniswap/src/data/apiClients/tradingApi/TradingApiClient'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { getFeatureFlag } from 'uniswap/src/features/gating/hooks' import { getFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { getCallsStatusHelper } from 'wallet/src/features/batchedTransactions/eip5792Utils' import { getCallsStatusHelper } from 'wallet/src/features/batchedTransactions/eip5792Utils'
import { transformCallsToTransactionRequests } from 'wallet/src/features/batchedTransactions/utils' import { transformCallsToTransactionRequests } from 'wallet/src/features/batchedTransactions/utils'
import { selectHasSmartWalletConsent } from 'wallet/src/features/wallet/selectors'
/** /**
* Checks if EIP-5792 methods are enabled via feature flag * Checks if EIP-5792 methods are enabled via feature flag
...@@ -115,6 +119,8 @@ export function* handleGetCapabilities( ...@@ -115,6 +119,8 @@ export function* handleGetCapabilities(
requestId: number, requestId: number,
accountAddress: string, accountAddress: string,
requestedAccount: string, requestedAccount: string,
dappName?: string,
dappIconUrl?: string,
) { ) {
const eip5792MethodsEnabled = isEip5792MethodsEnabled() const eip5792MethodsEnabled = isEip5792MethodsEnabled()
...@@ -128,13 +134,39 @@ export function* handleGetCapabilities( ...@@ -128,13 +134,39 @@ export function* handleGetCapabilities(
return return
} }
const hasSmartWalletConsent = yield* select(selectHasSmartWalletConsent, accountAddress)
// TODO(WALL-6765): check if wallet is already delegated
if (!hasSmartWalletConsent) {
const onEnableSmartWallet = () => {
navigate(ModalName.SmartWalletEnabledModal, {
showReconnectDappPrompt: true,
})
}
yield* call(navigate, ModalName.PostSwapSmartWalletNudge, {
onEnableSmartWallet,
dappInfo: {
icon: dappIconUrl,
name: dappName,
},
})
}
yield* call([wcWeb3Wallet, wcWeb3Wallet.respondSessionRequest], { yield* call([wcWeb3Wallet, wcWeb3Wallet.respondSessionRequest], {
topic, topic,
response: { response: {
id: requestId, id: requestId,
jsonrpc: '2.0', jsonrpc: '2.0',
// TODO: This would be where we add any changes in capabilities object (when decided) // TODO: This would be where we add any changes in capabilities object (when decided)
result: {}, result: {
[`0x${UniverseChainId.Sepolia.toString(16)}`]: { atomic: { status: 'supported' } },
[`0x${UniverseChainId.Mainnet.toString(16)}`]: { atomic: { status: 'supported' } },
[`0x${UniverseChainId.UnichainSepolia.toString(16)}`]: { atomic: { status: 'supported' } },
[`0x${UniverseChainId.Unichain.toString(16)}`]: { atomic: { status: 'supported' } },
[`0x${UniverseChainId.Optimism.toString(16)}`]: { atomic: { status: 'supported' } },
[`0x${UniverseChainId.Base.toString(16)}`]: { atomic: { status: 'supported' } },
},
}, },
}) })
} }
...@@ -11,8 +11,8 @@ export function fetchDappDetails( ...@@ -11,8 +11,8 @@ export function fetchDappDetails(
if (sessions && sessions[topic]) { if (sessions && sessions[topic]) {
const wcSession = sessions[topic] const wcSession = sessions[topic]
return { return {
dappIcon: wcSession?.dapp?.icon || null, dappIcon: wcSession?.dappRequestInfo?.icon || null,
dappName: wcSession?.dapp?.name || '', dappName: wcSession?.dappRequestInfo?.name || '',
} }
} }
} catch (error) { } catch (error) {
......
import { WalletKitTypes } from '@reown/walletkit'
import { ProposalTypes, Verify } from '@walletconnect/types'
import { buildApprovedNamespaces, populateAuthPayload } from '@walletconnect/utils'
import { expectSaga } from 'redux-saga-test-plan'
import { handleSessionAuthenticate, handleSessionProposal } from 'src/features/walletConnect/saga'
import { wcWeb3Wallet } from 'src/features/walletConnect/walletConnectClient'
import {
SignRequest,
WalletConnectVerifyStatus,
addPendingSession,
addRequest,
} from 'src/features/walletConnect/walletConnectSlice'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { EthMethod } from 'uniswap/src/features/dappRequests/types'
import { DappRequestInfo, DappRequestType, EthEvent } from 'uniswap/src/types/walletConnect'
import { selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors'
// Mock for WalletConnect utils
jest.mock('@walletconnect/utils', () => ({
...jest.requireActual('@walletconnect/utils'),
buildApprovedNamespaces: jest.fn(),
getSdkError: jest.fn(() => 'mocked-error'),
populateAuthPayload: jest.fn(),
parseVerifyStatus: jest.fn(() => 'VERIFIED'),
}))
// Mock dependencies
jest.mock('./walletConnectClient', () => ({
wcWeb3Wallet: {
rejectSession: jest.fn(),
formatAuthMessage: jest.fn(),
},
}))
jest.mock('react-native', () => ({
Alert: {
alert: jest.fn(),
},
}))
// Mock i18n
jest.mock('uniswap/src/i18n', () => ({
t: jest.fn((key) => key),
}))
describe('WalletConnect Saga', () => {
describe('handleSessionProposal', () => {
it('dispatches addPendingSession with correct parameters for valid proposal', () => {
// Create a mock verification context for the verified status
const mockVerifyContext: Verify.Context = {
verified: {
verifyUrl: 'https://verify.walletconnect.com',
validation: 'VALID',
origin: 'https://valid-dapp.com',
},
}
// Create a valid proposal with eip155 namespace
const mockProposal = {
id: 456,
proposer: {
publicKey: 'test-public-key',
metadata: {
name: 'Valid Dapp',
description: 'Valid Dapp Description',
url: 'https://valid-dapp.com',
icons: ['https://valid-dapp.com/icon.png'],
},
},
requiredNamespaces: {
eip155: {
chains: ['eip155:1'],
methods: ['eth_signTransaction'],
events: [],
},
},
relays: [],
optionalNamespaces: {},
pairingTopic: 'valid-pairing-topic',
expiryTimestamp: Date.now() + 1000 * 60 * 5,
verifyContext: mockVerifyContext,
} as unknown as ProposalTypes.Struct & { verifyContext?: Verify.Context }
const activeAccountAddress = '0x1234567890abcdef'
// Mock namespaces that would be returned by buildApprovedNamespaces
const mockNamespaces = {
eip155: {
accounts: [`eip155:1:${activeAccountAddress}`],
chains: ['eip155:1'],
methods: ['eth_signTransaction'],
events: [EthEvent.AccountsChanged, EthEvent.ChainChanged],
},
}
// Mock the buildApprovedNamespaces function to return our mock namespaces
const buildApprovedNamespacesMock = buildApprovedNamespaces as jest.Mock
buildApprovedNamespacesMock.mockReturnValue(mockNamespaces)
// Create properly typed dappRequestInfo
const dappRequestInfo: DappRequestInfo = {
name: 'Valid Dapp',
url: 'https://valid-dapp.com',
icon: 'https://valid-dapp.com/icon.png',
requestType: DappRequestType.WalletConnectSessionRequest,
}
const expectedPendingSession = {
wcSession: {
id: '456',
proposalNamespaces: mockNamespaces,
chains: [UniverseChainId.Mainnet],
dappRequestInfo,
verifyStatus: 'VERIFIED' as WalletConnectVerifyStatus,
},
}
return expectSaga(handleSessionProposal, mockProposal)
.provide({
select({ selector }, next) {
if (selector === selectActiveAccountAddress) {
return activeAccountAddress
}
return next()
},
})
.put(addPendingSession(expectedPendingSession))
.run()
})
})
describe('handleSessionAuthenticate', () => {
it('processes authentication request', async () => {
// Create a mock authentication request
const mockAuthenticate = {
id: 789,
params: {
requester: {
metadata: {
name: 'Auth Dapp',
description: 'Auth Dapp Description',
url: 'https://auth-dapp.com',
icons: ['https://auth-dapp.com/icon.png'],
},
},
authPayload: {
chains: ['eip155:1', 'eip155:137', 'solana:1'], // Include both supported and unsupported chains
domain: 'auth-dapp.com',
aud: 'https://auth-dapp.com/login',
nonce: '1234567890',
type: 'eip4361',
},
},
} as unknown as WalletKitTypes.SessionAuthenticate
// User's active account
const activeAccountAddress = '0x1234567890abcdef'
// Expected formatted chains after filtering non-eip155 chains
const formattedEip155Chains = ['eip155:1', 'eip155:137']
// Mock populated auth payload with version and iat to satisfy type requirements
const mockPopulatedAuthPayload = {
chains: formattedEip155Chains,
domain: 'auth-dapp.com',
aud: 'https://auth-dapp.com/login',
nonce: '1234567890',
type: 'eip4361',
methods: ['eth_signTransaction', 'eth_sign', 'personal_sign'],
version: '1',
iat: '2023-01-01T00:00:00Z',
}
// Set up mock for populateAuthPayload
const populateAuthPayloadMock = populateAuthPayload as jest.Mock
populateAuthPayloadMock.mockReturnValue(mockPopulatedAuthPayload)
// Mock auth message
const mockAuthMessage = 'SIWE Message: Auth request from auth-dapp.com (nonce: 1234567890)'
// Mock formatAuthMessage
wcWeb3Wallet.formatAuthMessage = jest.fn().mockReturnValue(mockAuthMessage)
const mockSignRequest: SignRequest = {
type: EthMethod.EthSign,
message: mockAuthMessage,
rawMessage: mockAuthMessage,
sessionId: '789',
internalId: `${UniverseChainId.Mainnet}:789`, // Fix: use actual chainId format without 'eip155:' prefix
chainId: UniverseChainId.Mainnet,
account: activeAccountAddress,
dappRequestInfo: {
name: 'Auth Dapp',
url: 'https://auth-dapp.com',
icon: 'https://auth-dapp.com/icon.png',
requestType: DappRequestType.WalletConnectAuthenticationRequest,
authPayload: mockPopulatedAuthPayload,
},
}
// Run the saga and verify action is dispatched
await expectSaga(handleSessionAuthenticate, mockAuthenticate)
.provide({
select({ selector }, next) {
if (selector === selectActiveAccountAddress) {
return activeAccountAddress
}
return next()
},
})
.put(addRequest(mockSignRequest))
.run()
})
})
})
import { AnyAction } from '@reduxjs/toolkit' import { AnyAction } from '@reduxjs/toolkit'
import { WalletKitTypes } from '@reown/walletkit' import { WalletKitTypes } from '@reown/walletkit'
import { PendingRequestTypes, ProposalTypes, SessionTypes } from '@walletconnect/types' import { PendingRequestTypes, ProposalTypes, SessionTypes, Verify } from '@walletconnect/types'
import { buildApprovedNamespaces, getSdkError } from '@walletconnect/utils' import { buildApprovedNamespaces, getSdkError, populateAuthPayload } from '@walletconnect/utils'
import { Alert } from 'react-native' import { Alert } from 'react-native'
import { EventChannel, eventChannel } from 'redux-saga' import { EventChannel, eventChannel } from 'redux-saga'
import { MobileState } from 'src/app/mobileReducer' import { MobileState } from 'src/app/mobileReducer'
...@@ -20,9 +20,11 @@ import { ...@@ -20,9 +20,11 @@ import {
parseSendCallsRequest, parseSendCallsRequest,
parseSignRequest, parseSignRequest,
parseTransactionRequest, parseTransactionRequest,
parseVerifyStatus,
} from 'src/features/walletConnect/utils' } from 'src/features/walletConnect/utils'
import { initializeWeb3Wallet, wcWeb3Wallet } from 'src/features/walletConnect/walletConnectClient' import { initializeWeb3Wallet, wcWeb3Wallet } from 'src/features/walletConnect/walletConnectClient'
import { import {
SignRequest,
addPendingSession, addPendingSession,
addRequest, addRequest,
addSession, addSession,
...@@ -38,22 +40,31 @@ import { getFeatureFlag } from 'uniswap/src/features/gating/hooks' ...@@ -38,22 +40,31 @@ import { getFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { pushNotification } from 'uniswap/src/features/notifications/slice' import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types' import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import i18n from 'uniswap/src/i18n' import i18n from 'uniswap/src/i18n'
import { EthEvent, WalletConnectEvent } from 'uniswap/src/types/walletConnect' import { DappRequestType, EthEvent, WalletConnectEvent } from 'uniswap/src/types/walletConnect'
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'
import { selectAccounts, selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors' import { selectAccounts, selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors'
const WC_SUPPORTED_METHODS = [
EthMethod.EthSign,
EthMethod.EthSendTransaction,
EthMethod.PersonalSign,
EthMethod.SignTypedData,
EthMethod.SignTypedDataV4,
EthMethod.WalletGetCapabilities,
EthMethod.WalletSendCalls,
EthMethod.WalletGetCallsStatus,
]
function createWalletConnectChannel(): EventChannel<AnyAction> { function createWalletConnectChannel(): EventChannel<AnyAction> {
return eventChannel<AnyAction>((emit) => { return eventChannel<AnyAction>((emit) => {
/* /*
* Handle incoming `session_proposal` events that contain the dapp attempting to pair * Handle incoming `session_proposal` events that contain the dapp attempting to pair
* and the proposal namespaces (chains, methods, events) * and the proposal namespaces (chains, methods, events)
*/ */
const sessionProposalHandler = async ( const sessionProposalHandler = async (proposalEvent: WalletKitTypes.SessionProposal): Promise<void> => {
proposalEvent: Omit<WalletKitTypes.BaseEventArgs<ProposalTypes.Struct>, 'topic'>, const { params: proposal, verifyContext } = proposalEvent
): Promise<void> => { emit({ type: 'session_proposal', proposal: { ...proposal, verifyContext } })
const { params: proposal } = proposalEvent
emit({ type: 'session_proposal', proposal })
} }
const sessionRequestHandler = async (request: WalletKitTypes.SessionRequest): Promise<void> => { const sessionRequestHandler = async (request: WalletKitTypes.SessionRequest): Promise<void> => {
...@@ -64,14 +75,20 @@ function createWalletConnectChannel(): EventChannel<AnyAction> { ...@@ -64,14 +75,20 @@ function createWalletConnectChannel(): EventChannel<AnyAction> {
emit({ type: 'session_delete', session }) emit({ type: 'session_delete', session })
} }
const sessionAuthenticateHandler = async (authenticate: WalletKitTypes.SessionAuthenticate): Promise<void> => {
emit({ type: 'session_authenticate', authenticate })
}
wcWeb3Wallet.on('session_proposal', sessionProposalHandler) wcWeb3Wallet.on('session_proposal', sessionProposalHandler)
wcWeb3Wallet.on('session_request', sessionRequestHandler) wcWeb3Wallet.on('session_request', sessionRequestHandler)
wcWeb3Wallet.on('session_delete', sessionDeleteHandler) wcWeb3Wallet.on('session_delete', sessionDeleteHandler)
wcWeb3Wallet.on('session_authenticate', sessionAuthenticateHandler)
const unsubscribe = (): void => { const unsubscribe = (): void => {
wcWeb3Wallet.off('session_proposal', sessionProposalHandler) wcWeb3Wallet.off('session_proposal', sessionProposalHandler)
wcWeb3Wallet.off('session_request', sessionRequestHandler) wcWeb3Wallet.off('session_request', sessionRequestHandler)
wcWeb3Wallet.off('session_delete', sessionDeleteHandler) wcWeb3Wallet.off('session_delete', sessionDeleteHandler)
wcWeb3Wallet.off('session_authenticate', sessionAuthenticateHandler)
} }
return unsubscribe return unsubscribe
...@@ -88,6 +105,8 @@ function* watchWalletConnectEvents() { ...@@ -88,6 +105,8 @@ function* watchWalletConnectEvents() {
yield* call(handleSessionProposal, event.proposal) yield* call(handleSessionProposal, event.proposal)
} else if (event.type === 'session_request') { } else if (event.type === 'session_request') {
yield* call(handleSessionRequest, event.request) yield* call(handleSessionRequest, event.request)
} else if (event.type === 'session_authenticate') {
yield* call(handleSessionAuthenticate, event.authenticate)
} else if (event.type === 'session_delete') { } else if (event.type === 'session_delete') {
yield* call(handleSessionDelete, event.session) yield* call(handleSessionDelete, event.session)
} }
...@@ -132,7 +151,7 @@ function* cancelErrorSession(dappName: string, chainLabels: string, proposalId: ...@@ -132,7 +151,7 @@ function* cancelErrorSession(dappName: string, chainLabels: string, proposalId:
yield* put(setHasPendingSessionError(false)) yield* put(setHasPendingSessionError(false))
} }
function* handleSessionProposal(proposal: ProposalTypes.Struct) { export function* handleSessionProposal(proposal: ProposalTypes.Struct & { verifyContext?: Verify.Context }) {
const activeAccountAddress = yield* select(selectActiveAccountAddress) const activeAccountAddress = yield* select(selectActiveAccountAddress)
const { const {
...@@ -158,16 +177,7 @@ function* handleSessionProposal(proposal: ProposalTypes.Struct) { ...@@ -158,16 +177,7 @@ function* handleSessionProposal(proposal: ProposalTypes.Struct) {
supportedNamespaces: { supportedNamespaces: {
eip155: { eip155: {
chains: supportedEip155Chains, chains: supportedEip155Chains,
methods: [ methods: WC_SUPPORTED_METHODS,
EthMethod.EthSign,
EthMethod.EthSendTransaction,
EthMethod.PersonalSign,
EthMethod.SignTypedData,
EthMethod.SignTypedDataV4,
EthMethod.WalletGetCapabilities,
EthMethod.WalletSendCalls,
EthMethod.WalletGetCallsStatus,
],
events: [EthEvent.AccountsChanged, EthEvent.ChainChanged], events: [EthEvent.AccountsChanged, EthEvent.ChainChanged],
accounts, accounts,
}, },
...@@ -183,17 +193,20 @@ function* handleSessionProposal(proposal: ProposalTypes.Struct) { ...@@ -183,17 +193,20 @@ function* handleSessionProposal(proposal: ProposalTypes.Struct) {
proposalChainIds.push(...(getSupportedWalletConnectChains(eip155Chains) ?? [])) proposalChainIds.push(...(getSupportedWalletConnectChains(eip155Chains) ?? []))
}) })
const verifyStatus = parseVerifyStatus(proposal.verifyContext)
yield* put( yield* put(
addPendingSession({ addPendingSession({
wcSession: { wcSession: {
id: id.toString(), id: id.toString(),
proposalNamespaces: namespaces, proposalNamespaces: namespaces,
chains: proposalChainIds, chains: proposalChainIds,
dapp: { verifyStatus,
dappRequestInfo: {
name: dapp.name, name: dapp.name,
url: dapp.url, url: dapp.url,
icon: dapp.icons[0] ?? null, icon: dapp.icons[0] ?? null,
source: 'walletconnect', requestType: DappRequestType.WalletConnectSessionRequest,
}, },
}, },
}), }),
...@@ -222,6 +235,67 @@ const eip5792Methods = [EthMethod.WalletGetCallsStatus, EthMethod.WalletSendCall ...@@ -222,6 +235,67 @@ const eip5792Methods = [EthMethod.WalletGetCallsStatus, EthMethod.WalletSendCall
(m) => m.valueOf(), (m) => m.valueOf(),
) )
/**
* Handles WalletConnect authentication requests, which are used for one-click sign in
* via WalletConnect's implementation of SIWE and ReCaps.
*
* @see https://docs.reown.com/walletkit/android/one-click-auth
*
* We only sign and broadcast a single signature for the first chain — the minimum required for a valid authenticated session.
* If a dapp wants to authenticate across multiple chains, it must request additional signatures separately.
* This tradeoff simplifies the user experience and remains aligned with the WalletConnect specification.
*/
export function* handleSessionAuthenticate(authenticate: WalletKitTypes.SessionAuthenticate) {
// Filter non wallet supported chains from auth payload, in eip155 format
const formattedEip155Chains = authenticate.params.authPayload.chains.filter((chain) =>
ALL_CHAIN_IDS.some((id) => chain === `eip155:${id}`),
)
const authPayload = populateAuthPayload({
authPayload: authenticate.params.authPayload,
chains: formattedEip155Chains,
methods: WC_SUPPORTED_METHODS,
})
const activeAccountAddress = yield* select(selectActiveAccountAddress)
if (!activeAccountAddress) {
throw new Error('WalletConnect 1-Click Auth request has no active account')
}
// To avoid multiple signature modals, we only sign for the first supported chain.
// If a dapp wants to authenticate across multiple chains, it must request additional signatures separately.
const chainForSigning = formattedEip155Chains[0] ? getChainIdFromEIP155String(formattedEip155Chains[0]) : undefined
if (!chainForSigning) {
throw new Error('WalletConnect 1-Click Auth request has invalid supported chain: ' + formattedEip155Chains[0])
}
const message = wcWeb3Wallet.formatAuthMessage({
request: authPayload,
iss: `eip155:${chainForSigning}:${activeAccountAddress}`,
})
const request: SignRequest = {
type: EthMethod.EthSign,
message,
rawMessage: message,
sessionId: authenticate.id.toString(),
internalId: `${chainForSigning}:${authenticate.id.toString()}`,
chainId: chainForSigning,
account: activeAccountAddress,
dappRequestInfo: {
name: authenticate.params.requester.metadata.name,
url: authenticate.params.requester.metadata.url,
icon: authenticate.params.requester.metadata.icons[0] ?? null,
requestType: DappRequestType.WalletConnectAuthenticationRequest,
authPayload,
},
}
yield* put(addRequest(request))
}
function* handleSessionRequest(sessionRequest: PendingRequestTypes.Struct) { function* handleSessionRequest(sessionRequest: PendingRequestTypes.Struct) {
const { topic, params, id } = sessionRequest const { topic, params, id } = sessionRequest
const { request: wcRequest, chainId: wcChainId } = params const { request: wcRequest, chainId: wcChainId } = params
...@@ -281,7 +355,7 @@ function* handleSessionRequest(sessionRequest: PendingRequestTypes.Struct) { ...@@ -281,7 +355,7 @@ function* handleSessionRequest(sessionRequest: PendingRequestTypes.Struct) {
} }
case EthMethod.WalletGetCapabilities: { case EthMethod.WalletGetCapabilities: {
const { account } = parseGetCapabilitiesRequest(method, topic, id, dapp, requestParams) const { account } = parseGetCapabilitiesRequest(method, topic, id, dapp, requestParams)
yield* call(handleGetCapabilities, topic, id, accountAddress, account) yield* call(handleGetCapabilities, topic, id, accountAddress, account, dapp.name, dapp.icons?.[0])
break break
} }
default: default:
...@@ -357,11 +431,11 @@ function* populateActiveSessions() { ...@@ -357,11 +431,11 @@ function* populateActiveSessions() {
addSession({ addSession({
wcSession: { wcSession: {
id: session.topic, id: session.topic,
dapp: { dappRequestInfo: {
name: session.peer.metadata.name, name: session.peer.metadata.name,
url: session.peer.metadata.url, url: session.peer.metadata.url,
icon: session.peer.metadata.icons[0] ?? null, icon: session.peer.metadata.icons[0] ?? null,
source: 'walletconnect', requestType: DappRequestType.WalletConnectSessionRequest,
}, },
chains, chains,
namespaces: session.namespaces, namespaces: session.namespaces,
......
/* eslint-disable complexity */
import { buildAuthObject, getSdkError } from '@walletconnect/utils'
import { providers } from 'ethers' import { providers } from 'ethers'
import { wcWeb3Wallet } from 'src/features/walletConnect/walletConnectClient' import { wcWeb3Wallet } from 'src/features/walletConnect/walletConnectClient'
import { import {
...@@ -13,7 +15,7 @@ import { pushNotification } from 'uniswap/src/features/notifications/slice' ...@@ -13,7 +15,7 @@ import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types' import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import { getEnabledChainIdsSaga } from 'uniswap/src/features/settings/saga' import { getEnabledChainIdsSaga } from 'uniswap/src/features/settings/saga'
import { TransactionOriginType, TransactionType } from 'uniswap/src/features/transactions/types/transactionDetails' import { TransactionOriginType, TransactionType } from 'uniswap/src/features/transactions/types/transactionDetails'
import { DappInfo, UwULinkMethod, WalletConnectEvent } from 'uniswap/src/types/walletConnect' import { DappRequestInfo, DappRequestType, 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 { addBatchedTransaction } from 'wallet/src/features/batchedTransactions/slice' import { addBatchedTransaction } from 'wallet/src/features/batchedTransactions/slice'
...@@ -32,7 +34,7 @@ type SignMessageParams = { ...@@ -32,7 +34,7 @@ type SignMessageParams = {
message: string message: string
account: Account account: Account
method: EthSignMethod method: EthSignMethod
dapp: DappInfo dappRequestInfo: DappRequestInfo
chainId: UniverseChainId chainId: UniverseChainId
} }
...@@ -42,7 +44,7 @@ type SignTransactionParams = { ...@@ -42,7 +44,7 @@ type SignTransactionParams = {
transaction: providers.TransactionRequest transaction: providers.TransactionRequest
account: Account account: Account
method: EthMethod.EthSendTransaction | EthMethod.WalletSendCalls method: EthMethod.EthSendTransaction | EthMethod.WalletSendCalls
dapp: DappInfo dappRequestInfo: DappRequestInfo
chainId: UniverseChainId chainId: UniverseChainId
request: TransactionRequest | UwuLinkErc20Request | WalletSendCallsEncodedRequest request: TransactionRequest | UwuLinkErc20Request | WalletSendCallsEncodedRequest
} }
...@@ -57,7 +59,10 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) { ...@@ -57,7 +59,10 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
result = 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.dappRequestInfo.requestType === DappRequestType.UwULink &&
params.dappRequestInfo.name === 'Uniswap Cafe'
) {
yield* put( yield* put(
pushNotification({ pushNotification({
type: AppNotificationType.Success, type: AppNotificationType.Success,
...@@ -94,7 +99,7 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) { ...@@ -94,7 +99,7 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
}, },
typeInfo: { typeInfo: {
type: TransactionType.WCConfirm, type: TransactionType.WCConfirm,
dapp: params.dapp, dappRequestInfo: params.dappRequestInfo,
}, },
transactionOriginType: TransactionOriginType.External, transactionOriginType: TransactionOriginType.External,
} }
...@@ -117,7 +122,7 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) { ...@@ -117,7 +122,7 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
}, },
typeInfo: { typeInfo: {
type: TransactionType.WCConfirm, type: TransactionType.WCConfirm,
dapp: params.dapp, dappRequestInfo: params.dappRequestInfo,
}, },
transactionOriginType: TransactionOriginType.External, transactionOriginType: TransactionOriginType.External,
} }
...@@ -144,7 +149,27 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) { ...@@ -144,7 +149,27 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
) )
} }
if (params.dapp.source === 'walletconnect') { if (params.dappRequestInfo.requestType === DappRequestType.WalletConnectAuthenticationRequest) {
const iss = `eip155:${chainId}:${account.address}`
// Check if signature is a string, if not throw an error
if (typeof result !== 'string') {
throw new Error('Expected signature to be a string in WalletConnectAuthenticationRequest')
}
const auth = buildAuthObject(
params.dappRequestInfo.authPayload,
{
t: 'eip191',
s: result,
},
iss,
)
yield* call(wcWeb3Wallet.approveSessionAuthenticate, {
id: Number(sessionId),
auths: [auth],
})
} else if (params.dappRequestInfo.requestType === DappRequestType.WalletConnectSessionRequest) {
yield* call(wcWeb3Wallet.respondSessionRequest, { yield* call(wcWeb3Wallet.respondSessionRequest, {
topic: sessionId, topic: sessionId,
response: { response: {
...@@ -153,8 +178,8 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) { ...@@ -153,8 +178,8 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
result, result,
}, },
}) })
} else if (params.dapp.source === 'uwulink' && params.dapp.webhook) { } else if (params.dappRequestInfo.requestType === DappRequestType.UwULink && params.dappRequestInfo.webhook) {
fetch(params.dapp.webhook, { fetch(params.dappRequestInfo.webhook, {
method: 'POST', method: 'POST',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
...@@ -169,7 +194,7 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) { ...@@ -169,7 +194,7 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
) )
} }
} catch (error) { } catch (error) {
if (params.dapp.source === 'walletconnect') { if (params.dappRequestInfo.requestType === DappRequestType.WalletConnectSessionRequest) {
yield* call(wcWeb3Wallet.respondSessionRequest, { yield* call(wcWeb3Wallet.respondSessionRequest, {
topic: sessionId, topic: sessionId,
response: { response: {
...@@ -178,14 +203,19 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) { ...@@ -178,14 +203,19 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
error: { code: 5000, message: `Signing error: ${error}` }, error: { code: 5000, message: `Signing error: ${error}` },
}, },
}) })
} else if (params.dappRequestInfo.requestType === DappRequestType.WalletConnectAuthenticationRequest) {
yield* call(wcWeb3Wallet.rejectSessionAuthenticate, {
id: Number(sessionId),
reason: getSdkError('USER_REJECTED'),
})
} }
yield* put( yield* put(
pushNotification({ pushNotification({
type: AppNotificationType.WalletConnect, type: AppNotificationType.WalletConnect,
event: WalletConnectEvent.TransactionFailed, event: WalletConnectEvent.TransactionFailed,
dappName: params.dapp.name, dappName: params.dappRequestInfo.name,
imageUrl: params.dapp.icon ?? null, imageUrl: params.dappRequestInfo.icon ?? null,
chainId, chainId,
address: account.address, address: account.address,
}), }),
......
...@@ -12,6 +12,7 @@ import { ...@@ -12,6 +12,7 @@ import {
} from 'src/features/walletConnect/utils' } from 'src/features/walletConnect/utils'
import { UniverseChainId } from 'uniswap/src/features/chains/types' import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { EthMethod } from 'uniswap/src/features/dappRequests/types' import { EthMethod } from 'uniswap/src/features/dappRequests/types'
import { DappRequestType } from 'uniswap/src/types/walletConnect'
const EIP155_MAINNET = 'eip155:1' const EIP155_MAINNET = 'eip155:1'
const EIP155_POLYGON = 'eip155:137' const EIP155_POLYGON = 'eip155:137'
...@@ -118,11 +119,12 @@ describe(parseGetCapabilitiesRequest, () => { ...@@ -118,11 +119,12 @@ describe(parseGetCapabilitiesRequest, () => {
internalId: String(mockInternalId), internalId: String(mockInternalId),
account: TEST_ADDRESS, account: TEST_ADDRESS,
chainIds: undefined, chainIds: undefined,
dapp: { isLinkModeSupported: false,
dappRequestInfo: {
name: mockDapp.name, name: mockDapp.name,
url: mockDapp.url, url: mockDapp.url,
icon: mockDapp.icons[0], icon: mockDapp.icons[0],
source: 'walletconnect', requestType: DappRequestType.WalletConnectSessionRequest,
}, },
}) })
}) })
...@@ -140,11 +142,12 @@ describe(parseGetCapabilitiesRequest, () => { ...@@ -140,11 +142,12 @@ describe(parseGetCapabilitiesRequest, () => {
internalId: String(mockInternalId), internalId: String(mockInternalId),
account: TEST_ADDRESS, account: TEST_ADDRESS,
chainIds, chainIds,
dapp: { isLinkModeSupported: false,
dappRequestInfo: {
name: mockDapp.name, name: mockDapp.name,
url: mockDapp.url, url: mockDapp.url,
icon: mockDapp.icons[0], icon: mockDapp.icons[0],
source: 'walletconnect', requestType: DappRequestType.WalletConnectSessionRequest,
}, },
}) })
}) })
...@@ -175,11 +178,12 @@ describe(parseSignRequest, () => { ...@@ -175,11 +178,12 @@ describe(parseSignRequest, () => {
chainId: mockChainId, chainId: mockChainId,
rawMessage: message, rawMessage: message,
message: 'Hello World', message: 'Hello World',
dapp: { isLinkModeSupported: false,
dappRequestInfo: {
name: mockDapp.name, name: mockDapp.name,
url: mockDapp.url, url: mockDapp.url,
icon: mockDapp.icons[0], icon: mockDapp.icons[0],
source: 'walletconnect', requestType: DappRequestType.WalletConnectSessionRequest,
}, },
}) })
}) })
...@@ -198,11 +202,12 @@ describe(parseSignRequest, () => { ...@@ -198,11 +202,12 @@ describe(parseSignRequest, () => {
chainId: mockChainId, chainId: mockChainId,
rawMessage: message, rawMessage: message,
message: 'Hello World', message: 'Hello World',
dapp: { isLinkModeSupported: false,
dappRequestInfo: {
name: mockDapp.name, name: mockDapp.name,
url: mockDapp.url, url: mockDapp.url,
icon: mockDapp.icons[0], icon: mockDapp.icons[0],
source: 'walletconnect', requestType: DappRequestType.WalletConnectSessionRequest,
}, },
}) })
}) })
...@@ -221,11 +226,12 @@ describe(parseSignRequest, () => { ...@@ -221,11 +226,12 @@ describe(parseSignRequest, () => {
chainId: mockChainId, chainId: mockChainId,
rawMessage: typedData, rawMessage: typedData,
message: null, message: null,
dapp: { isLinkModeSupported: false,
dappRequestInfo: {
name: mockDapp.name, name: mockDapp.name,
url: mockDapp.url, url: mockDapp.url,
icon: mockDapp.icons[0], icon: mockDapp.icons[0],
source: 'walletconnect', requestType: DappRequestType.WalletConnectSessionRequest,
}, },
}) })
}) })
...@@ -268,6 +274,7 @@ describe(parseTransactionRequest, () => { ...@@ -268,6 +274,7 @@ describe(parseTransactionRequest, () => {
internalId: String(mockInternalId), internalId: String(mockInternalId),
account: TEST_ADDRESS, account: TEST_ADDRESS,
chainId: mockChainId, chainId: mockChainId,
isLinkModeSupported: false,
transaction: { transaction: {
from: TEST_ADDRESS, from: TEST_ADDRESS,
to: '0x1234567890123456789012345678901234567890', to: '0x1234567890123456789012345678901234567890',
...@@ -276,11 +283,11 @@ describe(parseTransactionRequest, () => { ...@@ -276,11 +283,11 @@ describe(parseTransactionRequest, () => {
value: '0x0', value: '0x0',
// gasPrice and nonce should be omitted // gasPrice and nonce should be omitted
}, },
dapp: { dappRequestInfo: {
name: mockDapp.name, name: mockDapp.name,
url: mockDapp.url, url: mockDapp.url,
icon: mockDapp.icons[0], icon: mockDapp.icons[0],
source: 'walletconnect', requestType: DappRequestType.WalletConnectSessionRequest,
}, },
}) })
}) })
...@@ -336,11 +343,12 @@ describe(parseSendCallsRequest, () => { ...@@ -336,11 +343,12 @@ describe(parseSendCallsRequest, () => {
capabilities: sendCallsParams.capabilities, capabilities: sendCallsParams.capabilities,
id: sendCallsParams.id, id: sendCallsParams.id,
version: sendCallsParams.version, version: sendCallsParams.version,
dapp: { isLinkModeSupported: false,
dappRequestInfo: {
name: mockDapp.name, name: mockDapp.name,
url: mockDapp.url, url: mockDapp.url,
icon: mockDapp.icons[0], icon: mockDapp.icons[0],
source: 'walletconnect', requestType: DappRequestType.WalletConnectSessionRequest,
}, },
}) })
}) })
...@@ -397,11 +405,12 @@ describe(parseGetCallsStatusRequest, () => { ...@@ -397,11 +405,12 @@ describe(parseGetCallsStatusRequest, () => {
account, account,
chainId: mockChainId, chainId: mockChainId,
id: requestId, id: requestId,
dapp: { isLinkModeSupported: false,
dappRequestInfo: {
name: mockDapp.name, name: mockDapp.name,
url: mockDapp.url, url: mockDapp.url,
icon: mockDapp.icons[0], icon: mockDapp.icons[0],
source: 'walletconnect', requestType: DappRequestType.WalletConnectSessionRequest,
}, },
}) })
}) })
......
import { WalletKitTypes } from '@reown/walletkit' import { WalletKitTypes } from '@reown/walletkit'
import { PairingTypes, ProposalTypes, SessionTypes, SignClientTypes } from '@walletconnect/types' import { PairingTypes, ProposalTypes, SessionTypes, SignClientTypes, Verify } from '@walletconnect/types'
import { utils } from 'ethers' import { utils } from 'ethers'
import { wcWeb3Wallet } from 'src/features/walletConnect/walletConnectClient' import { wcWeb3Wallet } from 'src/features/walletConnect/walletConnectClient'
import { import {
SignRequest, SignRequest,
TransactionRequest, TransactionRequest,
WalletConnectVerifyStatus,
WalletGetCallsStatusRequest, WalletGetCallsStatusRequest,
WalletGetCapabilitiesRequest, WalletGetCapabilitiesRequest,
WalletSendCallsRequest, WalletSendCallsRequest,
...@@ -12,6 +13,7 @@ import { ...@@ -12,6 +13,7 @@ import {
import { UniverseChainId } from 'uniswap/src/features/chains/types' import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { toSupportedChainId } from 'uniswap/src/features/chains/utils' import { toSupportedChainId } from 'uniswap/src/features/chains/utils'
import { EthMethod, EthSignMethod, WalletConnectEthMethod } from 'uniswap/src/features/dappRequests/types' import { EthMethod, EthSignMethod, WalletConnectEthMethod } from 'uniswap/src/features/dappRequests/types'
import { DappRequestInfo, DappRequestType } from 'uniswap/src/types/walletConnect'
import { generateBatchId } from 'wallet/src/features/batchedTransactions/utils' import { generateBatchId } from 'wallet/src/features/batchedTransactions/utils'
import { GetCallsStatusParams, SendCallsParams } from 'wallet/src/features/dappRequests/types' import { GetCallsStatusParams, SendCallsParams } from 'wallet/src/features/dappRequests/types'
/** /**
...@@ -98,23 +100,20 @@ function createBaseRequest<T extends WalletConnectEthMethod>( ...@@ -98,23 +100,20 @@ function createBaseRequest<T extends WalletConnectEthMethod>(
sessionId: string sessionId: string
internalId: string internalId: string
account: Address account: Address
dapp: { isLinkModeSupported: boolean
name: string dappRequestInfo: DappRequestInfo
url: string
icon: string | null
source: 'walletconnect'
}
} { } {
return { return {
type: method, type: method,
sessionId: topic, sessionId: topic,
internalId: String(internalId), internalId: String(internalId),
account, account,
dapp: { isLinkModeSupported: Boolean(dapp.redirect?.linkMode),
dappRequestInfo: {
name: dapp.name, name: dapp.name,
url: dapp.url, url: dapp.url,
icon: dapp.icons[0] ?? null, icon: dapp.icons[0] ?? null,
source: 'walletconnect', requestType: DappRequestType.WalletConnectSessionRequest,
}, },
} }
} }
...@@ -295,3 +294,32 @@ export async function pairWithWalletConnectURI(uri: string): Promise<void | Pair ...@@ -295,3 +294,32 @@ export async function pairWithWalletConnectURI(uri: string): Promise<void | Pair
return Promise.reject(error instanceof Error ? error.message : '') return Promise.reject(error instanceof Error ? error.message : '')
} }
} }
/**
* Formats safety level based on the verify context from a wallet connect proposal or sesison request.
*
* See https://docs.reown.com/walletkit/ios/verify
*/
export function parseVerifyStatus(verifyContext?: Verify.Context): WalletConnectVerifyStatus {
if (!verifyContext) {
return WalletConnectVerifyStatus.Unverified
}
const { verified } = verifyContext
// Must check for isScam first, since valid URLs can still be scams
if (verified.validation === 'INVALID' || verified.isScam) {
return WalletConnectVerifyStatus.Threat
}
if (verified.validation === 'VALID') {
return WalletConnectVerifyStatus.Verified
}
if (verified.validation === 'UNKNOWN') {
return WalletConnectVerifyStatus.Unverified
}
// Default to unverified status to enforce stricter warning if verification information is empty
return WalletConnectVerifyStatus.Unverified
}
...@@ -49,6 +49,8 @@ export async function initializeWeb3Wallet(): Promise<void> { ...@@ -49,6 +49,8 @@ export async function initializeWeb3Wallet(): Promise<void> {
icons: ['https://gateway.pinata.cloud/ipfs/QmR1hYqhDMoyvJtwrQ6f1kVyfEKyK65XH3nbCimXBMkHJg'], icons: ['https://gateway.pinata.cloud/ipfs/QmR1hYqhDMoyvJtwrQ6f1kVyfEKyK65XH3nbCimXBMkHJg'],
redirect: { redirect: {
native: 'uniswap://', native: 'uniswap://',
universal: 'https://uniswap.org/app',
linkMode: true,
}, },
}, },
}) })
......
...@@ -2,20 +2,27 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' ...@@ -2,20 +2,27 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { ProposalTypes, SessionTypes } from '@walletconnect/types' import { ProposalTypes, SessionTypes } from '@walletconnect/types'
import { UniverseChainId } from 'uniswap/src/features/chains/types' import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { EthMethod, EthSignMethod } from 'uniswap/src/features/dappRequests/types' import { EthMethod, EthSignMethod } from 'uniswap/src/features/dappRequests/types'
import { DappInfo, EthTransaction, UwULinkMethod } from 'uniswap/src/types/walletConnect' import { DappRequestInfo, EthTransaction, UwULinkMethod } from 'uniswap/src/types/walletConnect'
import { Call, Capability } from 'wallet/src/features/dappRequests/types' import { Call, Capability } from 'wallet/src/features/dappRequests/types'
export enum WalletConnectVerifyStatus {
Verified = 'VERIFIED',
Unverified = 'UNVERIFIED',
Threat = 'THREAT',
}
export type WalletConnectPendingSession = { export type WalletConnectPendingSession = {
id: string id: string
chains: UniverseChainId[] chains: UniverseChainId[]
dapp: DappInfo dappRequestInfo: DappRequestInfo
proposalNamespaces: ProposalTypes.RequiredNamespaces proposalNamespaces: ProposalTypes.RequiredNamespaces
verifyStatus: WalletConnectVerifyStatus
} }
export type WalletConnectSession = { export type WalletConnectSession = {
id: string id: string
chains: UniverseChainId[] chains: UniverseChainId[]
dapp: DappInfo dappRequestInfo: DappRequestInfo
namespaces: SessionTypes.Namespaces namespaces: SessionTypes.Namespaces
} }
...@@ -27,8 +34,9 @@ interface BaseRequest { ...@@ -27,8 +34,9 @@ interface BaseRequest {
sessionId: string sessionId: string
internalId: string internalId: string
account: string account: string
dapp: DappInfo dappRequestInfo: DappRequestInfo
chainId: UniverseChainId chainId: UniverseChainId
isLinkModeSupported?: boolean
} }
export interface SignRequest extends BaseRequest { export interface SignRequest extends BaseRequest {
......
...@@ -145,7 +145,7 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element | ...@@ -145,7 +145,7 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element |
} }
await dispatchAddTransaction({ isOffRamp }) await dispatchAddTransaction({ isOffRamp })
await dispatch(forceFetchFiatOnRampTransactions()) await dispatch(forceFetchFiatOnRampTransactions())
openUri(widgetUrl).catch(onError) openUri(widgetUrl, false, false, undefined, true).catch(onError)
} }
if (!isOffRamp && timeoutElapsed && !widgetLoading && widgetData) { if (!isOffRamp && timeoutElapsed && !widgetLoading && widgetData) {
......
...@@ -16,10 +16,12 @@ import { Flex, Loader } from 'ui/src' ...@@ -16,10 +16,12 @@ import { Flex, Loader } from 'ui/src'
import { DownloadAlt, OSDynamicCloudIcon } from 'ui/src/components/icons' import { DownloadAlt, OSDynamicCloudIcon } from 'ui/src/components/icons'
import { imageSizes } from 'ui/src/theme' import { imageSizes } from 'ui/src/theme'
import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard'
import { config } from 'uniswap/src/config'
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 { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { isAndroid } from 'utilities/src/platform'
import { ONE_SECOND_MS } from 'utilities/src/time/time' import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { useSignerAccounts } from 'wallet/src/features/wallet/hooks' import { useSignerAccounts } from 'wallet/src/features/wallet/hooks'
...@@ -27,7 +29,13 @@ type Props = NativeStackScreenProps<OnboardingStackParamList, OnboardingScreens. ...@@ -27,7 +29,13 @@ type Props = NativeStackScreenProps<OnboardingStackParamList, OnboardingScreens.
const MIN_LOADING_UI_MS = ONE_SECOND_MS const MIN_LOADING_UI_MS = ONE_SECOND_MS
// 10s timeout time for query for backups, since we don't know when the query completes // 10s timeout time for query for backups, since we don't know when the query completes
const MAX_LOADING_TIMEOUT_MS = ONE_SECOND_MS * 10
const MAX_LOADING_TIMEOUT_MS = config.isE2ETest ? ONE_SECOND_MS : ONE_SECOND_MS * 10
/**
* Workaround for Android GDrive backup. There are many UXs depending on the API and
* at the moment we are only e2e testing seed phrase input.
*/
const ANDROID_E2E_WORKAROUND = config.isE2ETest && isAndroid
export function RestoreCloudBackupLoadingScreen({ navigation, route: { params } }: Props): JSX.Element { export function RestoreCloudBackupLoadingScreen({ navigation, route: { params } }: Props): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
...@@ -55,6 +63,10 @@ export function RestoreCloudBackupLoadingScreen({ navigation, route: { params } ...@@ -55,6 +63,10 @@ export function RestoreCloudBackupLoadingScreen({ navigation, route: { params }
// delays native oauth consent screen to avoid UI freezes // delays native oauth consent screen to avoid UI freezes
setTimeout(async () => { setTimeout(async () => {
try { try {
if (ANDROID_E2E_WORKAROUND) {
setIsError(false)
return
}
await startFetchingCloudStorageBackups() await startFetchingCloudStorageBackups()
} catch (e) { } catch (e) {
setIsError(true) setIsError(true)
...@@ -150,6 +162,7 @@ export function RestoreCloudBackupLoadingScreen({ navigation, route: { params } ...@@ -150,6 +162,7 @@ export function RestoreCloudBackupLoadingScreen({ navigation, route: { params }
if (isLoading === false && backups.length === 0) { if (isLoading === false && backups.length === 0) {
if (isRestoringMnemonic) { if (isRestoringMnemonic) {
navigation.replace(OnboardingScreens.SeedPhraseInput, { navigation.replace(OnboardingScreens.SeedPhraseInput, {
showAsCloudBackupFallback: true,
importType, importType,
entryPoint, entryPoint,
}) })
......
...@@ -7,21 +7,21 @@ import { useCloudBackups } from 'src/features/CloudBackup/hooks' ...@@ -7,21 +7,21 @@ import { useCloudBackups } from 'src/features/CloudBackup/hooks'
import { CloudStorageMnemonicBackup } from 'src/features/CloudBackup/types' import { CloudStorageMnemonicBackup } from 'src/features/CloudBackup/types'
import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen' import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen'
import { useNavigationHeader } from 'src/utils/useNavigationHeader' import { useNavigationHeader } from 'src/utils/useNavigationHeader'
import { Flex, Text, TouchableArea, Unicon, useIsDarkMode } from 'ui/src' import { Flex, Text, TouchableArea, useIsDarkMode } from 'ui/src'
import { DownloadAlt, RotatableChevron } from 'ui/src/components/icons' import { DownloadAlt, RotatableChevron, Unitag } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { AccountIcon } from 'uniswap/src/features/accounts/AccountIcon'
import { DisplayNameType } from 'uniswap/src/features/accounts/types'
import { useAvatar } from 'uniswap/src/features/address/avatar'
import { FORMAT_DATE_TIME_SHORT, useLocalizedDayjs } from 'uniswap/src/features/language/localizedDayjs' import { FORMAT_DATE_TIME_SHORT, useLocalizedDayjs } from 'uniswap/src/features/language/localizedDayjs'
import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { sanitizeAddressText } from 'uniswap/src/utils/addresses'
import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName' import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName'
import { shortenAddress } from 'utilities/src/addresses' import { useDisplayName } from 'wallet/src/features/wallet/hooks'
type Props = NativeStackScreenProps<OnboardingStackParamList, OnboardingScreens.RestoreCloudBackup> type Props = NativeStackScreenProps<OnboardingStackParamList, OnboardingScreens.RestoreCloudBackup>
export function RestoreCloudBackupScreen({ navigation, route: { params } }: Props): JSX.Element { export function RestoreCloudBackupScreen({ navigation, route: { params } }: Props): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const isDarkMode = useIsDarkMode()
const localizedDayjs = useLocalizedDayjs()
const backups = useCloudBackups() const backups = useCloudBackups()
const sortedBackups = backups.slice().sort((a, b) => b.createdAt - a.createdAt) const sortedBackups = backups.slice().sort((a, b) => b.createdAt - a.createdAt)
...@@ -44,44 +44,61 @@ export function RestoreCloudBackupScreen({ navigation, route: { params } }: Prop ...@@ -44,44 +44,61 @@ export function RestoreCloudBackupScreen({ navigation, route: { params } }: Prop
> >
<ScrollView> <ScrollView>
<Flex gap="$spacing8"> <Flex gap="$spacing8">
{sortedBackups.map((backup) => { {sortedBackups.map((backup) => (
const { mnemonicId, createdAt } = backup <BackupListItem key={backup.mnemonicId} backup={backup} onPressRestoreBackup={onPressRestoreBackup} />
return ( ))}
<TouchableArea
key={backup.mnemonicId}
backgroundColor={isDarkMode ? '$surface2' : '$surface1'}
borderColor="$surface3"
borderRadius="$rounded20"
borderWidth="$spacing1"
p="$spacing16"
shadowColor="$surface3"
shadowRadius={!isDarkMode ? '$spacing4' : undefined}
onPress={(): Promise<void> => onPressRestoreBackup(backup)}
>
<Flex row alignItems="center" justifyContent="space-between">
<Flex centered row gap="$spacing12">
<Unicon address={mnemonicId} size={32} />
<Flex>
<Text adjustsFontSizeToFit variant="subheading1">
{sanitizeAddressText(shortenAddress(mnemonicId))}
</Text>
<Text adjustsFontSizeToFit color="$neutral2" variant="body3">
{localizedDayjs.unix(createdAt).format(FORMAT_DATE_TIME_SHORT)}
</Text>
</Flex>
</Flex>
<RotatableChevron
color="$neutral2"
direction="end"
height={iconSizes.icon20}
width={iconSizes.icon20}
/>
</Flex>
</TouchableArea>
)
})}
</Flex> </Flex>
</ScrollView> </ScrollView>
</OnboardingScreen> </OnboardingScreen>
) )
} }
const BackupListItem = ({
backup,
onPressRestoreBackup,
}: {
backup: CloudStorageMnemonicBackup
onPressRestoreBackup: (backup: CloudStorageMnemonicBackup) => Promise<void>
}): JSX.Element => {
const { mnemonicId, createdAt } = backup
const isDarkMode = useIsDarkMode()
const localizedDayjs = useLocalizedDayjs()
const displayName = useDisplayName(mnemonicId)
const { avatar } = useAvatar(mnemonicId)
const isUnitag = displayName?.type === DisplayNameType.Unitag
return (
<TouchableArea
backgroundColor={isDarkMode ? '$surface2' : '$surface1'}
borderColor="$surface3"
borderRadius="$rounded20"
borderWidth="$spacing1"
p="$spacing16"
shadowColor="$surface3"
shadowRadius={!isDarkMode ? '$spacing4' : undefined}
onPress={(): Promise<void> => onPressRestoreBackup(backup)}
>
<Flex row alignItems="center" gap="$spacing12">
<AccountIcon avatarUri={avatar} address={mnemonicId} size={iconSizes.icon36} />
<Flex flex={1}>
<Flex row>
<Text adjustsFontSizeToFit variant="subheading1">
{displayName?.name}
</Text>
{isUnitag && (
<Flex alignSelf="center" pl="$spacing4">
<Unitag size="$icon.24" />
</Flex>
)}
</Flex>
<Text adjustsFontSizeToFit color="$neutral2" variant="body3">
{localizedDayjs.unix(createdAt).format(FORMAT_DATE_TIME_SHORT)}
</Text>
</Flex>
<Flex>
<RotatableChevron color="$neutral2" direction="end" height={iconSizes.icon20} width={iconSizes.icon20} />
</Flex>
</Flex>
</TouchableArea>
)
}
This diff is collapsed.
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
NativeSeedPhraseInputProps, NativeSeedPhraseInputProps,
NativeSeedPhraseInputRef, NativeSeedPhraseInputRef,
} from 'src/screens/Import/SeedPhraseInputScreen/SeedPhraseInput/types' } from 'src/screens/Import/SeedPhraseInputScreen/SeedPhraseInput/types'
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 { isAndroid } from 'utilities/src/platform' import { isAndroid } from 'utilities/src/platform'
...@@ -68,9 +69,10 @@ export const SeedPhraseInput = forwardRef<NativeSeedPhraseInputRef, SeedPhraseIn ...@@ -68,9 +69,10 @@ export const SeedPhraseInput = forwardRef<NativeSeedPhraseInputRef, SeedPhraseIn
return ( return (
<NativeSeedPhraseInput <NativeSeedPhraseInput
key={key}
// @ts-expect-error - TODO: figure out how to properly type the ref of a custom native component // @ts-expect-error - TODO: figure out how to properly type the ref of a custom native component
ref={inputRef} ref={inputRef}
key={key}
testID={TestID.NativeSeedPhraseInput}
style={calculatedStyle} style={calculatedStyle}
onHeightMeasured={handleOnHeightMeasured} onHeightMeasured={handleOnHeightMeasured}
{...rest} {...rest}
......
This diff is collapsed.
This diff is collapsed.
export type ViewPrivateKeysScreenState = {
/**
* Since this screen can be accessed via Settings or OnboardingStack,
* showHeader is used to determine if the header should be shown or
* to defer to the parent screen to show the header.
*/
showHeader?: boolean
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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