ci(release): publish latest release

parent da9fc597
IPFS hash of the deployment:
- CIDv0: `QmYgGWW41t5rZ973ouoVCPSVJ7xSk1ouQS8hXJaVrwBydQ`
- CIDv1: `bafybeieztnrdyd2anpehiktteokw23mwgbjmnz42u4cpzosxjxignq2bf4`
- CIDv0: `QmXS4vd18Mi4tKCVNuk1qnB1wyNPN7XRR1iSygxNvnRKiD`
- CIDv1: `bafybeiehdr3z22tnf5jdnjnsj5kfb7ywjjefa2oyttljiuwjtu6mhocxjy`
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.
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://bafybeieztnrdyd2anpehiktteokw23mwgbjmnz42u4cpzosxjxignq2bf4.ipfs.dweb.link/
- [ipfs://QmYgGWW41t5rZ973ouoVCPSVJ7xSk1ouQS8hXJaVrwBydQ/](ipfs://QmYgGWW41t5rZ973ouoVCPSVJ7xSk1ouQS8hXJaVrwBydQ/)
- https://bafybeiehdr3z22tnf5jdnjnsj5kfb7ywjjefa2oyttljiuwjtu6mhocxjy.ipfs.dweb.link/
- [ipfs://QmXS4vd18Mi4tKCVNuk1qnB1wyNPN7XRR1iSygxNvnRKiD/](ipfs://QmXS4vd18Mi4tKCVNuk1qnB1wyNPN7XRR1iSygxNvnRKiD/)
## 5.85.0 (2025-05-15)
## 5.86.0 (2025-05-21)
### 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
\ No newline at end of file
web/5.86.0
\ No newline at end of file
......@@ -45,7 +45,15 @@ globalThis.matchMedia =
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', () => ({
useExtensionNavigation: () => ({
......@@ -69,4 +77,3 @@ jest.mock('wallet/src/features/appearance/hooks', () => {
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'
import { I18nextProvider } from 'react-i18next'
import { GraphqlProvider } from 'src/app/apollo'
import { TraceUserProperties } from 'src/app/components/Trace/TraceUserProperties'
import { SmartWalletNudgesProvider } from 'src/app/context/SmartWalletNudgesContext'
import { ExtensionStatsigProvider } from 'src/app/core/StatsigProvider'
import { DatadogAppNameTag } from 'src/app/datadog'
import { getReduxStore } from 'src/store/store'
......@@ -24,10 +25,12 @@ export function BaseAppContainer({
<ErrorBoundary>
<GraphqlProvider>
<BlankUrlProvider>
<LocalizationContextProvider>
<TraceUserProperties />
{children}
</LocalizationContextProvider>
<SmartWalletNudgesProvider>
<LocalizationContextProvider>
<TraceUserProperties />
{children}
</LocalizationContextProvider>
</SmartWalletNudgesProvider>
</BlankUrlProvider>
</GraphqlProvider>
</ErrorBoundary>
......
......@@ -5,7 +5,7 @@ import { initializeReduxStore } from 'src/store/store'
describe('OnboardingApp', () => {
// eslint-disable-next-line jest/expect-expect
it('renders without error', async () => {
await initializeReduxStore()
initializeReduxStore()
render(<OnboardingApp />)
})
})
......@@ -39,10 +39,10 @@ import { setRouter, setRouterState } from 'src/app/navigation/state'
import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { checksIfSupportsSidePanel } from 'src/app/utils/chrome'
import { PrimaryAppInstanceDebuggerLazy } from 'src/store/PrimaryAppInstanceDebuggerLazy'
import { getReduxPersistor } from 'src/store/store'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { ExtensionOnboardingFlow } from 'uniswap/src/types/screens/extension'
import { getReduxPersistor } from 'wallet/src/state/persistor'
const supportsSidePanel = checksIfSupportsSidePanel()
......
......@@ -3,16 +3,15 @@ import 'src/app/Global.css'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import { RouterProvider, createHashRouter } from 'react-router-dom'
import { ErrorElement } from 'src/app/components/ErrorElement'
import { BaseAppContainer } from 'src/app/core/BaseAppContainer'
import { DatadogAppNameTag } from 'src/app/datadog'
import { initExtensionAnalytics } from 'src/app/utils/analytics'
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 { syncAppWithDeviceLanguage } from 'uniswap/src/features/settings/slice'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { ExtensionScreens } from 'uniswap/src/types/screens/extension'
......@@ -28,11 +27,7 @@ const router = createHashRouter([
function PopupContent(): JSX.Element {
const { t } = useTranslation()
const dispatch = useDispatch()
useEffect(() => {
dispatch(syncAppWithDeviceLanguage())
}, [dispatch])
useTestnetModeForLoggingAndAnalytics()
const searchParams = new URLSearchParams(window.location.search)
......@@ -58,7 +53,7 @@ function PopupContent(): JSX.Element {
position="absolute"
right={-spacing.spacing4}
>
<Image height={iconSizes.icon12} source={CHROME_LOGO} width={iconSizes.icon12} />
<GoogleChromeLogo size={iconSizes.icon12} />
</Flex>
</Flex>
</Flex>
......
......@@ -7,7 +7,6 @@ import { useDispatch } from 'react-redux'
import { RouterProvider, createHashRouter } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react'
import { ErrorElement } from 'src/app/components/ErrorElement'
import { ScreenHeader } from 'src/app/components/layout/ScreenHeader'
import { BaseAppContainer } from 'src/app/core/BaseAppContainer'
import { DatadogAppNameTag } from 'src/app/datadog'
import { AccountSwitcherScreen } from 'src/app/features/accounts/AccountSwitcherScreen'
......@@ -23,6 +22,7 @@ import { RemoveRecoveryPhraseWallets } from 'src/app/features/settings/SettingsR
import { ViewRecoveryPhraseScreen } from 'src/app/features/settings/SettingsRecoveryPhraseScreen/ViewRecoveryPhraseScreen'
import { SettingsScreen } from 'src/app/features/settings/SettingsScreen'
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 { SwapFlowScreen } from 'src/app/features/swap/SwapFlowScreen'
import { useIsWalletUnlocked } from 'src/app/hooks/useIsWalletUnlocked'
......@@ -37,20 +37,15 @@ import {
} from 'src/background/messagePassing/messageChannels'
import { BackgroundToSidePanelRequestType } from 'src/background/messagePassing/types/requests'
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 { syncAppWithDeviceLanguage } from 'uniswap/src/features/settings/slice'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import i18n from 'uniswap/src/i18n'
import { isDevEnv } from 'utilities/src/environment/env'
import { logger } from 'utilities/src/logger/logger'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
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 { getReduxPersistor } from 'wallet/src/state/persistor'
const router = createHashRouter([
{
......@@ -111,25 +106,7 @@ const router = createHashRouter([
},
{
path: SettingsRoutes.SmartWallet,
element: (
<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>
}
/>
}
/>
),
element: <SmartWalletSettingsScreen />,
},
],
},
......@@ -211,16 +188,11 @@ function useDappRequestPortListener(): void {
}
function SidebarWrapper(): JSX.Element {
const dispatch = useDispatch()
useDappRequestPortListener()
useTestnetModeForLoggingAndAnalytics()
const resetUnitagsQueries = useResetUnitagsQueries()
useEffect(() => {
dispatch(syncAppWithDeviceLanguage())
}, [dispatch])
useEffect(() => {
return backgroundToSidePanelMessageChannel.addMessageListener(
BackgroundToSidePanelRequestType.RefreshUnitags,
......
import { useDappLastChainId } from 'src/app/features/dapp/hooks'
import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext'
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 { UniswapXSwapRequest } from 'src/app/features/dappRequests/types/Permit2Types'
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 { toSupportedChainId } from 'uniswap/src/features/chains/utils'
import { CurrencyInfo } from 'uniswap/src/features/dataApi/types'
......@@ -116,7 +115,7 @@ export function UniswapXSwapRequestContent({ typedData }: { typedData: UniswapXS
const { token: outputToken, startAmount: lastAmountOutParam } = typedData.message.witness.baseOutputs[0]
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))
assert(
......
import { BigNumber } from '@ethersproject/bignumber'
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_UINT160 = BigNumber.from(2).pow(160).sub(1)
......@@ -5,7 +5,6 @@ import { formatUnits as formatUnitsEthers } from 'ethers/lib/utils'
import { useDappLastChainId } from 'src/app/features/dapp/hooks'
import {
CONTRACT_BALANCE,
ETH_ADDRESS,
MAX_UINT160,
MAX_UINT256,
} from 'src/app/features/dappRequests/requestContent/EthSend/Swap/constants'
......@@ -30,7 +29,7 @@ import {
isUrCommandSweep,
isUrCommandUnwrapWeth,
} 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 { assert } from 'utilities/src/errors'
......@@ -70,8 +69,10 @@ export function useSwapDetails(
if (v4Command) {
// Extract details using the V4 helper
const v4Details = getTokenDetailsFromV4SwapCommands(v4Command, parsedCalldata.commands)
inputAddress = v4Details.inputAddress === ETH_ADDRESS ? DEFAULT_NATIVE_ADDRESS : v4Details.inputAddress
outputAddress = v4Details.outputAddress === ETH_ADDRESS ? DEFAULT_NATIVE_ADDRESS : v4Details.outputAddress
inputAddress =
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'
outputValue = v4Details.outputValue || '0'
} else {
......
......@@ -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
response: {
[`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' } },
},
}
yield* call(dappResponseMessageChannel.sendMessageToTab, senderTabInfo.id, response)
......
......@@ -83,7 +83,7 @@ const RotatingSettingsIcon = ({ onPressSettings }: { onPressSettings(): void }):
<TouchableArea
hoverable
borderRadius="$roundedFull"
p="$spacing8"
p="$spacing6"
onHoverIn={onBegin}
onHoverOut={onCancel}
onPress={onPressSettingsLocal}
......@@ -152,7 +152,7 @@ export const PortfolioHeader = memo(function _PortfolioHeader({ address }: Portf
return (
<Flex gap="$spacing8">
<Flex row justifyContent="space-between">
<Flex row justifyContent="space-between" alignItems="flex-start">
<TouchableArea pressStyle={{ scale: 0.95 }} onPress={onPressAccount}>
<Flex group row alignItems="center" gap="$spacing4">
<Flex $group-hover={{ opacity: 0.6 }}>
......@@ -163,7 +163,7 @@ export const PortfolioHeader = memo(function _PortfolioHeader({ address }: Portf
</Flex>
</Flex>
</TouchableArea>
<Flex row alignItems="center" gap="$spacing4" justifyContent="space-around">
<Flex row alignItems="center" gap="$spacing6" justifyContent="space-around">
{showConnectionStatus && (
<Popover
offset={10}
......@@ -177,7 +177,7 @@ export const PortfolioHeader = memo(function _PortfolioHeader({ address }: Portf
}}
>
<Popover.Trigger onPress={toggleConnectPopup}>
<TouchableArea hoverable borderRadius="$roundedFull" p="$spacing8">
<TouchableArea hoverable borderRadius="$roundedFull" p="$spacing6">
<ConnectionStatusIcon
dappIconUrl={dappIconUrl}
dappUrl={dappUrl}
......
......@@ -32,7 +32,7 @@ type TokenBalanceListProps = {
export const TokenBalanceList = memo(function _TokenBalanceList({ owner }: TokenBalanceListProps): JSX.Element {
return (
<Flex grow>
<TokenBalanceListContextProvider isExternalProfile={false} owner={owner} onPressToken={() => {}}>
<TokenBalanceListContextProvider isExternalProfile={false} owner={owner}>
<TokenBalanceListInner />
</TokenBalanceListContextProvider>
</Flex>
......@@ -202,14 +202,14 @@ function TokenContextMenu({
}: PropsWithChildren<{
portfolioBalance: PortfolioBalance
}>): JSX.Element {
const contextMenu = useTokenContextMenu({
const { menuActions } = useTokenContextMenu({
currencyId: portfolioBalance.currencyInfo.currencyId,
isBlocked: portfolioBalance.currencyInfo.safetyInfo?.tokenList === TokenList.Blocked,
tokenSymbolForNotification: portfolioBalance?.currencyInfo?.currency?.symbol,
portfolioBalance,
})
const menuOptions = contextMenu.menuActions.map((action) => ({
const menuOptions = menuActions.map((action) => ({
label: action.title,
onPress: action.onPress,
Icon: action.Icon,
......
import { useEffect, useState } from 'react'
import { useDispatch } from 'react-redux'
import { Outlet } from 'react-router-dom'
import { DevMenuModal } from 'src/app/core/DevMenuModal'
import { StorageWarningModal } from 'src/app/features/warnings/StorageWarningModal'
......@@ -7,7 +6,6 @@ import { ONBOARDING_BACKGROUND_DARK, ONBOARDING_BACKGROUND_LIGHT } from 'src/ass
import { onboardingMessageChannel } from 'src/background/messagePassing/messageChannels'
import { OnboardingMessageType } from 'src/background/messagePassing/types/ExtensionMessages'
import { Flex, Image, useIsDarkMode } from 'ui/src'
import { syncAppWithDeviceLanguage } from 'uniswap/src/features/settings/slice'
import { isProdEnv } from 'utilities/src/environment/env'
import { OnboardingContextProvider } from 'wallet/src/features/onboarding/OnboardingContext'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics'
......@@ -15,14 +13,9 @@ import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testne
export function OnboardingWrapper(): JSX.Element {
const isDarkMode = useIsDarkMode()
const [isHighlighted, setIsHighlighted] = useState(false)
const dispatch = useDispatch()
useTestnetModeForLoggingAndAnalytics()
useEffect(() => {
dispatch(syncAppWithDeviceLanguage())
}, [dispatch])
useEffect(() => {
return onboardingMessageChannel.addMessageListener(OnboardingMessageType.HighlightOnboardingTab, (_message) => {
// 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 {
return (
<Trace logImpression screen={ExtensionScreens.Settings}>
{isLanguageModalOpen ? <SettingsLanguageModal onClose={() => setIsLanguageModalOpen(false)} /> : undefined}
{isLanguageModalOpen ? (
<SettingsLanguageModal isOpen={isLanguageModalOpen} onClose={() => setIsLanguageModalOpen(false)} />
) : undefined}
{isPortfolioBalanceModalOpen ? (
<PortfolioBalanceModal onClose={() => setIsPortfolioBalanceModalOpen(false)} />
<PortfolioBalanceModal
isOpen={isPortfolioBalanceModalOpen}
onClose={() => setIsPortfolioBalanceModalOpen(false)}
/>
) : undefined}
{isPermissionsModalOpen ? (
<PermissionsModal
isOpen={isPermissionsModalOpen}
handleDefaultBrowserToggle={handleDefaultBrowserToggle}
isDefaultBrowserProvider={isDefaultProvider}
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 { useSmartWalletNudges } from 'src/app/context/SmartWalletNudgesContext'
import { useExtensionNavigation } from 'src/app/navigation/utils'
import { Flex } from 'ui/src'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
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 { useSwapPrefilledState } from 'uniswap/src/features/transactions/swap/form/hooks/useSwapPrefilledState'
import { prepareSwapFormState } from 'uniswap/src/features/transactions/types/transactionState'
import { CurrencyField } from 'uniswap/src/types/currency'
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 {
const { navigateBack, locationState } = useExtensionNavigation()
......@@ -32,9 +34,25 @@ export function SwapFlowScreen(): JSX.Element {
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 (
<Flex fill p="$spacing12">
<WalletSwapFlow prefilledState={swapPrefilledState} walletNeedsRestore={false} onClose={navigateBack} />
<WalletSwapFlow
prefilledState={swapPrefilledState}
walletNeedsRestore={false}
onClose={navigateBack}
onSubmitSwap={onSubmitSwap}
/>
</Flex>
)
}
import { useCallback, useMemo, useRef } from 'react'
import { useSelector } from 'react-redux'
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 { ForceUpgradeModal } from 'src/app/features/forceUpgrade/ForceUpgradeModal'
import { HomeScreen } from 'src/app/features/home/HomeScreen'
......@@ -219,6 +220,8 @@ function LoggedIn(): JSX.Element {
{isChromeWindowFocused && <TransactionHistoryUpdater />}
<DappRequestQueue />
<SmartWalletNudgeModals />
</>
)
}
......
......@@ -2,9 +2,9 @@ import { initDappStore } from 'src/app/features/dapp/saga'
import { dappRequestApprovalWatcher } from 'src/app/features/dappRequests/dappRequestApprovalWatcherSaga'
import { dappRequestWatcher } from 'src/app/features/dappRequests/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 { 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 { swapActions, swapReducer, swapSaga, swapSagaName } from 'wallet/src/features/transactions/swap/swapSaga'
import {
......@@ -13,7 +13,8 @@ import {
tokenWrapSaga,
tokenWrapSagaName,
} 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 {
editAccountActions,
editAccountReducer,
......@@ -63,12 +64,12 @@ export const monitoredSagas: Record<string, MonitoredSaga> = {
} as const
const sagasInitializedOnStartup = [
appLanguageWatcherSaga,
initDappStore,
dappRequestApprovalWatcher,
dappRequestWatcher,
initProviders,
watchTransactionEvents,
deviceLocaleWatcher,
] as const
export const monitoredSagaReducers = getMonitoredSagaReducers(monitoredSagas)
......
import { isOnboardedSelector } from 'src/app/utils/isOnboardedSelector'
import { STATE_STORAGE_KEY } from 'src/store/constants'
import { ExtensionState } from 'src/store/extensionReducer'
import { readDeprecatedReduxedChromeStorage } from 'src/store/reduxedChromeStorageToReduxPersistMigration'
export async function readReduxStateFromStorage(storageChanges?: {
[key: string]: chrome.storage.StorageChange
......@@ -25,15 +24,6 @@ export async function readReduxStateFromStorage(storageChanges?: {
}
export async function readIsOnboardedFromStorage(): Promise<boolean> {
// The migration will happen in the sidebar, not in the background script,
// 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
const state = await readReduxStateFromStorage()
return state ? isOnboardedSelector(state) : false
}
......@@ -4,19 +4,16 @@
import React from 'react'
import { createRoot } from 'react-dom/client'
import OnboardingApp from 'src/app/core/OnboardingApp'
import { initializeReduxStore } from 'src/store/store'
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
// The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem
// see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326
async function initOnboarding(): Promise<void> {
await initializeReduxStore()
function initOnboarding() {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const container = document.getElementById('onboarding-root')!
const root = createRoot(container)
root.render(
<React.StrictMode>
<OnboardingApp />
......@@ -24,20 +21,6 @@ async function initOnboarding(): Promise<void> {
)
}
StoreSynchronization.init(ExtensionAppLocation.Tab).catch((error) => {
logger.error(error, {
tags: {
file: 'onboarding.ts',
function: 'initPrimaryInstanceHandler',
},
})
})
StoreSynchronization.init(ExtensionAppLocation.Tab)
initOnboarding().catch((error) => {
logger.error(error, {
tags: {
file: 'onboarding.ts',
function: 'initOnboarding',
},
})
})
initOnboarding()
......@@ -5,14 +5,11 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import PopupApp from 'src/app/core/PopupApp'
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
// The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem
// see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326
async function initPopup(): Promise<void> {
await initializeReduxStore({ readOnly: true })
function initPopup() {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const container = document.getElementById('popup-root')!
const root = createRoot(container)
......@@ -24,11 +21,6 @@ async function initPopup(): Promise<void> {
)
}
initPopup().catch((error) => {
logger.error(error, {
tags: {
file: 'popup.tsx',
function: 'initPopup',
},
})
})
initializeReduxStore({ readOnly: true })
initPopup()
......@@ -9,7 +9,6 @@ import { createRoot } from 'react-dom/client'
import SidebarApp from 'src/app/core/SidebarApp'
import { onboardingMessageChannel } from 'src/background/messagePassing/messageChannels'
import { OnboardingMessageType } from 'src/background/messagePassing/types/ExtensionMessages'
import { initializeReduxStore } from 'src/store/store'
import { ExtensionAppLocation, StoreSynchronization } from 'src/store/storeSynchronization'
import { initializeScrollWatcher } from 'uniswap/src/components/modals/ScrollLock'
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
// see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326
async function initSidebar(): Promise<void> {
await initializeReduxStore()
await onboardingMessageChannel.sendMessage({
type: OnboardingMessageType.SidebarOpened,
})
function initSidebar(): void {
onboardingMessageChannel
.sendMessage({
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
const container = window.document.querySelector('#root')!
const root = createRoot(container)
root.render(
<React.StrictMode>
<SidebarApp />
......@@ -33,22 +41,8 @@ async function initSidebar(): Promise<void> {
)
}
StoreSynchronization.init(ExtensionAppLocation.SidePanel).catch((error) => {
logger.error(error, {
tags: {
file: 'sidebar.ts',
function: 'initPrimaryInstanceHandler',
},
})
})
initSidebar().catch((error) => {
logger.error(error, {
tags: {
file: 'sidebar.ts',
function: 'initSidebar',
},
})
})
StoreSynchronization.init(ExtensionAppLocation.SidePanel)
initSidebar()
initializeScrollWatcher()
......@@ -5,14 +5,11 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import UnitagClaimApp from 'src/app/core/UnitagClaimApp'
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
// The globalThis.regeneratorRuntime = undefined addresses a potentially unsafe-eval problem
// see https://github.com/facebook/regenerator/issues/378#issuecomment-802628326
async function initUnitagClaim(): Promise<void> {
await initializeReduxStore({ readOnly: true })
function initUnitagClaim(): void {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const container = document.getElementById('unitag-claim-root')!
const root = createRoot(container)
......@@ -24,11 +21,6 @@ async function initUnitagClaim(): Promise<void> {
)
}
initUnitagClaim().catch((error) => {
logger.error(error, {
tags: {
file: 'unitagClaim.tsx',
function: 'initUnitagClaim',
},
})
})
initializeReduxStore({ readOnly: true })
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 { localStorage } from 'redux-persist-webextension-storage'
import { rootExtensionSaga } from 'src/app/saga'
......@@ -7,15 +6,13 @@ import { PERSIST_KEY } from 'src/store/constants'
import { enhancePersistReducer } from 'src/store/enhancePersistReducer'
import { ExtensionState, extensionPersistedStateList, extensionReducer } from 'src/store/extensionReducer'
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 { delegationListenerMiddleware } from 'uniswap/src/features/smartWallet/delegation/slice'
import { createDatadogReduxEnhancer } from 'utilities/src/logger/datadog/Datadog'
import { logger } from 'utilities/src/logger/logger'
import { createStore } from 'wallet/src/state'
import { createMigrate } from 'wallet/src/state/createMigrate'
import { setReduxPersistor } from 'wallet/src/state/persistor'
const persistConfig = {
key: PERSIST_KEY,
......@@ -34,10 +31,9 @@ const dataDogReduxEnhancer = createDatadogReduxEnhancer({
},
})
const setupStore = (preloadedState?: PreloadedState<ExtensionState>): ReturnType<typeof createStore> => {
const setupStore = (): ReturnType<typeof createStore> => {
return createStore({
reducer: persistedReducer,
preloadedState,
additionalSagas: [rootExtensionSaga],
middlewareBefore: __DEV__ ? [loggerMiddleware] : [],
middlewareAfter: [fiatOnRampAggregatorApi.middleware, delegationListenerMiddleware.middleware],
......@@ -46,32 +42,29 @@ const setupStore = (preloadedState?: PreloadedState<ExtensionState>): ReturnType
}
let store: ReturnType<typeof setupStore> | undefined
let persistor: ReturnType<typeof persistStore> | undefined
export async function initializeReduxStore(args?: { readOnly?: boolean }): Promise<{
store: ReturnType<typeof setupStore>
persistor: ReturnType<typeof persistStore>
}> {
// Migrate the old `reduxed-chrome-storage` persisted state to `redux-persist`.
// TODO(EXT-985): we might need to pass the old store through `createMigrations` when we implement migrations.
const oldStore = await readDeprecatedReduxedChromeStorage()
export function initializeReduxStore(args?: { readOnly?: boolean }): void {
if (store) {
// 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'), {
tags: {
file: 'store.ts',
function: 'initializeReduxStore',
},
})
store = setupStore(oldStore)
persistor = persistStore(store)
return
}
store = setupStore()
const persistor = persistStore(store)
setReduxPersistor(persistor)
if (args?.readOnly) {
// 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).
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> {
......@@ -81,11 +74,4 @@ export function getReduxStore(): ReturnType<typeof setupStore> {
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>
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 { v4 as uuid } from 'uuid'
import { getReduxPersistor } from 'wallet/src/state/persistor'
import { PersistedStorage } from 'wallet/src/utils/persistedStorage'
/**
......@@ -32,7 +33,7 @@ export enum ExtensionAppLocation {
Tab = 1,
}
async function initPrimaryInstanceHandler(appLocation: ExtensionAppLocation): Promise<void> {
function initPrimaryInstanceHandler(appLocation: ExtensionAppLocation): void {
if (isInitialized) {
// This is just to prevent bugs being introduced in the future.
logger.error(new Error('`initPrimaryInstanceHandler` called when already initialized'), {
......@@ -44,7 +45,7 @@ async function initPrimaryInstanceHandler(appLocation: ExtensionAppLocation): Pr
return
}
await initializeReduxStore()
initializeReduxStore()
const onStorageChangedListener: Parameters<typeof chrome.storage.onChanged.addListener>[0] = async (
changes,
......@@ -103,7 +104,11 @@ async function initPrimaryInstanceHandler(appLocation: ExtensionAppLocation): Pr
window.addEventListener('focus', onWindowFocusListener)
// 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.
// We don't want this tab to become the primary ever again when it's focused.
......
flows:
- 'flows/onboarding/*'
- 'flows/swap/*'
- 'flows/restore/*'
baselineBranch: main
executionOrder:
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
env:
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:
id: ${output.testIds.ImportAccount}
- waitForAnimationToEnd
......
......@@ -22,7 +22,7 @@ import { AppModals } from 'src/app/modals/AppModals'
import { NavigationContainer } from 'src/app/navigation/NavigationContainer'
import { useIsPartOfNavigationTree } from 'src/app/navigation/hooks'
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 { OfflineBanner } from 'src/components/banners/OfflineBanner'
import { initAppsFlyer } from 'src/features/analytics/appsflyer'
......@@ -66,7 +66,6 @@ import { StatsigUser, Storage, getStatsigClient } from 'uniswap/src/features/gat
import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext'
import { useCurrentLanguageInfo } from 'uniswap/src/features/language/hooks'
import { clearNotificationQueue } from 'uniswap/src/features/notifications/slice'
import { syncAppWithDeviceLanguage } from 'uniswap/src/features/settings/slice'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { MobileEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
......@@ -82,6 +81,7 @@ import { isIOS } from 'utilities/src/platform'
import { useAsyncData } from 'utilities/src/react/hooks'
import { AnalyticsNavigationContextProvider } from 'utilities/src/telemetry/trace/AnalyticsNavigationContext'
import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary'
import { getReduxPersistor } from 'wallet/src/state/persistor'
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { usePersistedApolloClient } from 'wallet/src/data/apollo/usePersistedApolloClient'
import { useCurrentAppearanceSetting } from 'wallet/src/features/appearance/hooks'
......@@ -259,7 +259,7 @@ function AppOuter(): JSX.Element | null {
return (
<ApolloProvider client={client}>
<PersistGate loading={null} persistor={persistor}>
<PersistGate loading={null} persistor={getReduxPersistor()}>
<ErrorBoundary>
<BlankUrlProvider>
<LocalizationContextProvider>
......@@ -315,7 +315,6 @@ function AppInner(): JSX.Element {
useEffect(() => {
dispatch(clearNotificationQueue()) // clear all in-app toasts on app start
dispatch(syncAppWithDeviceLanguage())
}, [dispatch])
useEffect(() => {
......
......@@ -6,6 +6,8 @@ import { navigate } from 'src/app/navigation/rootNavigation'
import { AccountList } from 'src/components/accounts/AccountList'
import { checkCloudBackupOrShowAlert } from 'src/components/mnemonic/cloudImportUtils'
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 { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions'
import { spacing } from 'ui/src/theme'
......@@ -55,6 +57,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
const dispatch = useDispatch()
const hasImportedSeedPhrase = useNativeAccountExists()
const isModalOpen = useIsFocused()
const { openWalletRestoreModal, walletRestoreType } = useWalletRestore()
const sortedMnemonicAccounts = useSelector(selectSortedSignerMnemonicAccounts)
......@@ -116,6 +119,12 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
const onPressCreateNewWallet = async (): Promise<void> => {
setShowAddWalletModal(false)
onClose()
if (walletRestoreType === WalletRestoreType.SeedPhrase) {
openWalletRestoreModal()
return
}
if (hasImportedSeedPhrase) {
await createAdditionalAccount()
} else {
......@@ -216,7 +225,16 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
}
return options
}, [activeAccountAddress, dispatch, hasImportedSeedPhrase, onClose, sortedMnemonicAccounts, t])
}, [
activeAccountAddress,
dispatch,
hasImportedSeedPhrase,
onClose,
sortedMnemonicAccounts,
t,
openWalletRestoreModal,
walletRestoreType,
])
const accountsWithoutActive = accounts.filter((a) => a.address !== activeAccountAddress)
......
import React, { useCallback } from 'react'
import { useDispatch } from 'react-redux'
import React from 'react'
import { LazyModalRenderer } from 'src/app/modals/LazyModalRenderer'
import { SendTokenModal } from 'src/app/modals/SendTokenModal'
import { SwapModal } from 'src/app/modals/SwapModal'
import { WalletConnectModals } from 'src/components/Requests/WalletConnectModals'
import { SettingsAppearanceModal } from 'src/components/Settings/SettingsAppearanceModal'
import { ForceUpgradeModal } from 'src/components/forceUpgrade/ForceUpgradeModal'
import { FiatOnRampAggregatorModal } from 'src/features/fiatOnRamp/FiatOnRampAggregatorModal'
import { LockScreenModal } from 'src/features/lockScreen/LockScreenModal'
import { closeModal } from 'src/features/modals/modalSlice'
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'
/**
......@@ -26,20 +20,6 @@ import { QueuedOrderModal } from 'wallet/src/features/transactions/swap/modals/Q
*/
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 (
<>
<LazyModalRenderer name={ModalName.FiatOnRampAggregator}>
......@@ -61,22 +41,6 @@ export function AppModals(): JSX.Element {
<WalletConnectModals />
<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 React, { useCallback, useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { navigate } from 'src/app/navigation/rootNavigation'
import { BiometricsIconProps, useBiometricsIcon } from 'src/components/icons/useBiometricsIcon'
import { WalletRestoreType } from 'src/components/RestoreWalletModal/RestoreWalletModalState'
import { useBiometricAppSettings } from 'src/features/biometrics/useBiometricAppSettings'
import { useOsBiometricAuthEnabled } from 'src/features/biometrics/useOsBiometricAuthEnabled'
import { useBiometricPrompt } from 'src/features/biometricsSettings/hooks'
......@@ -9,18 +11,26 @@ import { closeModal } from 'src/features/modals/modalSlice'
import { selectModalState } from 'src/features/modals/selectModalState'
import { useWalletRestore } from 'src/features/wallet/useWalletRestore'
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 { updateSwapStartTimestamp } from 'uniswap/src/features/timing/slice'
import { useSwapPrefilledState } from 'uniswap/src/features/transactions/swap/form/hooks/useSwapPrefilledState'
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 */
const DATADOG_VIEW_KEY = 'global-swap-modal'
export function SwapModal(): JSX.Element {
const appDispatch = useDispatch()
const eip5792MethodsEnabled = useFeatureFlag(FeatureFlags.Eip5792Methods)
const { initialState } = useSelector(selectModalState(ModalName.Swap))
const { hapticFeedback } = useHapticFeedback()
const address = useActiveAccount()?.address
const hasSmartWalletConsent = useHasSmartWalletConsent()
const onClose = useCallback((): void => {
appDispatch(closeModal({ name: ModalName.Swap }))
......@@ -34,7 +44,7 @@ export function SwapModal(): JSX.Element {
appDispatch(updateSwapStartTimestamp({ timestamp }))
}, [appDispatch])
const { openWalletRestoreModal, walletNeedsRestore } = useWalletRestore()
const { openWalletRestoreModal, walletRestoreType } = useWalletRestore()
const swapPrefilledState = useSwapPrefilledState(initialState)
......@@ -48,8 +58,26 @@ export function SwapModal(): JSX.Element {
authTrigger={requiresBiometrics ? biometricsTrigger : undefined}
openWalletRestoreModal={openWalletRestoreModal}
prefilledState={swapPrefilledState}
walletNeedsRestore={Boolean(walletNeedsRestore)}
onSubmitSwap={hapticFeedback.success}
walletNeedsRestore={walletRestoreType === WalletRestoreType.NewDevice}
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}
/>
)
......
......@@ -7,6 +7,7 @@ import {
import { NativeStackNavigationProp, NativeStackScreenProps } from '@react-navigation/native-stack'
import { TokenWarningModalState } from 'src/app/modals/TokenWarningModalState'
import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState'
import { RestoreWalletModalState } from 'src/components/RestoreWalletModal/RestoreWalletModalState'
import { ConnectionsDappsListModalState } from 'src/components/Settings/ConnectionsDappModal/ConnectionsDappsListModalState'
import { EditWalletSettingsModalState } from 'src/components/Settings/EditWalletModal/EditWalletSettingsModalState'
import { ManageWalletsModalState } from 'src/components/Settings/ManageWalletsModalState'
......@@ -16,6 +17,7 @@ import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalSta
import { TestnetSwitchModalState } from 'src/features/testnetMode/TestnetSwitchModalState'
import { HomeScreenTabIndex } from 'src/screens/HomeScreen/HomeScreenTabIndex'
import { ReceiveCryptoModalState } from 'src/screens/ReceiveCryptoModalState'
import { ViewPrivateKeysScreenState } from 'src/screens/ViewPrivateKeys/ViewPrivateKeysScreenState'
import { FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types'
import { PasskeyManagementModalState } from 'uniswap/src/features/passkey/PasskeyManagementModal'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
......@@ -28,8 +30,12 @@ import {
SharedUnitagScreenParams,
UnitagStackParamList,
} 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 { 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 = {
owner?: Address
......@@ -90,16 +96,17 @@ export type SettingsStackParamList = {
[MobileScreens.SettingsLanguage]: undefined
[MobileScreens.SettingsNotifications]: undefined
[MobileScreens.SettingsPrivacy]: undefined
[MobileScreens.SettingsSmartWallet]: { Wrapper: React.FC<{ children: React.ReactNode }> }
[MobileScreens.SettingsSmartWallet]: undefined
[MobileScreens.SettingsViewSeedPhrase]: { address: Address; walletNeedsRestore?: boolean }
[MobileScreens.SettingsWallet]: { address: Address }
[MobileScreens.SettingsWalletEdit]: { address: Address }
[MobileScreens.SettingsWalletManageConnection]: { address: Address }
[MobileScreens.ViewPrivateKeys]?: ViewPrivateKeysScreenState
[MobileScreens.WebView]: { headerTitle: string; uriLink: string }
[ModalName.Experiments]: undefined
[ModalName.NotificationsOSSettings]: undefined
[ModalName.SettingsAppearance]: undefined
[ModalName.UnitagsIntro]: UnitagsIntroModalState
[ModalName.RestoreWallet]: undefined
[ModalName.RestoreWallet]: RestoreWalletModalState
}
export type OnboardingStackBaseParams = {
......@@ -119,9 +126,11 @@ export type OnboardingStackParamList = {
[OnboardingScreens.WelcomeWallet]: OnboardingStackBaseParams
[OnboardingScreens.PasskeyImport]: PasskeyImportParams & OnboardingStackBaseParams
[OnboardingScreens.Security]: OnboardingStackBaseParams
[MobileScreens.ViewPrivateKeys]?: ViewPrivateKeysScreenState
// import
[OnboardingScreens.ImportMethod]: OnboardingStackBaseParams
[OnboardingScreens.RestoreMethod]: OnboardingStackBaseParams
[OnboardingScreens.OnDeviceRecovery]: OnboardingStackBaseParams & { mnemonicIds: Address[] }
[OnboardingScreens.OnDeviceRecoveryViewSeedPhrase]: {
mnemonicId: string
......@@ -131,9 +140,12 @@ export type OnboardingStackParamList = {
[OnboardingScreens.RestoreCloudBackupPassword]: {
mnemonicId: string
} & OnboardingStackBaseParams
[OnboardingScreens.SeedPhraseInput]: OnboardingStackBaseParams
[OnboardingScreens.SeedPhraseInput]: OnboardingStackBaseParams & {
showAsCloudBackupFallback?: boolean
}
[OnboardingScreens.SelectWallet]: OnboardingStackBaseParams
[OnboardingScreens.WatchWallet]: OnboardingStackBaseParams
[ModalName.PrivateKeySpeedBumpModal]: undefined
} & SharedUnitagScreenParams
export type AppStackParamList = {
......@@ -152,6 +164,7 @@ export type AppStackParamList = {
[MobileScreens.ExternalProfile]: {
address: string
}
[MobileScreens.ViewPrivateKeys]?: ViewPrivateKeysScreenState
[MobileScreens.WebView]: { headerTitle: string; uriLink: string }
[MobileScreens.Storybook]: undefined
[ModalName.Explore]: ExploreModalState | undefined
......@@ -164,7 +177,7 @@ export type AppStackParamList = {
[ModalName.TokenWarning]: { initialState?: TokenWarningModalState }
[ModalName.ViewOnlyExplainer]: undefined
[ModalName.UnitagsIntro]: UnitagsIntroModalState
[ModalName.RestoreWallet]: undefined
[ModalName.RestoreWallet]: RestoreWalletModalState
[ModalName.AccountSwitcher]: undefined
[ModalName.Scantastic]: ScantasticModalState
[ModalName.BackupReminder]: undefined
......@@ -183,7 +196,16 @@ export type AppStackParamList = {
[ModalName.EditLabelSettingsModal]: EditWalletSettingsModalState
[ModalName.EditProfileSettingsModal]: EditWalletSettingsModalState
[ModalName.ConnectionsDappListModal]: ConnectionsDappsListModalState
[ModalName.SmartWalletEnabledModal]: SmartWalletEnabledModalState
[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>
......
......@@ -16,9 +16,9 @@ import { restoreMnemonicCompleteWatcher } from 'src/features/wallet/saga'
import { walletConnectSaga } from 'src/features/walletConnect/saga'
import { signWcRequestSaga } from 'src/features/walletConnect/signWcRequestSaga'
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 { 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
const nonPersistedSagas = [appStateSaga, splashScreenSaga, biometricsSaga]
......@@ -26,7 +26,6 @@ const nonPersistedSagas = [appStateSaga, splashScreenSaga, biometricsSaga]
// All regular sagas must be included here
const sagas = [
lockScreenSaga,
appLanguageWatcherSaga,
appRatingWatcherSaga,
cloudBackupsManagerSaga,
deepLinkWatcher,
......@@ -37,9 +36,10 @@ const sagas = [
signWcRequestSaga,
telemetrySaga,
walletConnectSaga,
deviceLocaleWatcher,
]
export function* rootMobileSaga() {
export function* rootMobileSaga(): SagaIterator {
// Start non-persisted sagas
for (const s of nonPersistedSagas) {
yield* spawn(s)
......
......@@ -10,6 +10,7 @@ import { isNonJestDev } from 'utilities/src/environment/constants'
import { createDatadogReduxEnhancer } from 'utilities/src/logger/datadog/Datadog'
import { createStore } from 'wallet/src/state'
import { createMigrate } from 'wallet/src/state/createMigrate'
import { setReduxPersistor } from 'wallet/src/state/persistor'
const storage = new MMKV()
......@@ -66,6 +67,6 @@ const setupStore = (
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
pushNotification({
type: AppNotificationType.WalletConnect,
address,
dappName: session.dapp.name,
dappName: session.dappRequestInfo.name,
event: WalletConnectEvent.Disconnected,
imageUrl: session.dapp.icon,
imageUrl: session.dappRequestInfo.icon,
hideDelay: 3 * ONE_SECOND_MS,
}),
)
......
......@@ -20,7 +20,7 @@ export function DappConnectionItem({
handleDisconnect: (session: WalletConnectSession) => Promise<void>
}): JSX.Element {
const { t } = useTranslation()
const { dapp } = session
const { dappRequestInfo } = session
const menuActions = [{ title: t('common.button.disconnect'), systemIcon: 'trash', destructive: true }]
......@@ -74,12 +74,12 @@ export function DappConnectionItem({
)}
</Flex>
<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">
{dapp.name || dapp.url}
{dappRequestInfo.name || dappRequestInfo.url}
</Text>
<Text color="$neutral2" numberOfLines={1} textAlign="center" variant="body4">
{dapp.url}
{dappRequestInfo.url}
</Text>
</Flex>
</Flex>
......
......@@ -3,15 +3,15 @@ import { Flex, UniversalImage } from 'ui/src'
import { borderRadii, iconSizes } from 'ui/src/theme'
import { CurrencyLogo } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo'
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'
export function DappHeaderIcon({
dapp,
dappRequestInfo,
permitCurrencyInfo,
size = iconSizes.icon40,
}: {
dapp: DappInfo
dappRequestInfo: DappRequestInfo
permitCurrencyInfo?: CurrencyInfo | null
size?: number
}): JSX.Element {
......@@ -19,11 +19,11 @@ export function DappHeaderIcon({
return <CurrencyLogo currencyInfo={permitCurrencyInfo} />
}
const fallback = <DappIconPlaceholder iconSize={size} name={dapp.name} />
const fallback = <DappIconPlaceholder iconSize={size} name={dappRequestInfo.name} />
return (
<Flex height={size} width={size}>
{dapp.icon ? (
{dappRequestInfo.icon ? (
<UniversalImage
fallback={fallback}
size={{ height: size, width: size }}
......@@ -34,7 +34,7 @@ export function DappHeaderIcon({
overflow: 'hidden',
},
}}
uri={dapp.icon}
uri={dappRequestInfo.icon}
/>
) : (
fallback
......
......@@ -13,7 +13,7 @@ import {
} from 'react-native'
import { AnimatedStyle, useDerivedValue } from 'react-native-reanimated'
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 { Modal } from 'uniswap/src/components/modals/Modal'
import { ModalProps } from 'uniswap/src/components/modals/ModalProps'
......@@ -24,14 +24,17 @@ import { TestID } from 'uniswap/src/test/fixtures/testIDs'
const MEASURE_LAYOUT_TIMEOUT = 100
type ModalWithOverlayProps = PropsWithChildren<
export type ModalWithOverlayProps = PropsWithChildren<
ModalProps & {
confirmationButtonText?: string
cancelButtonText?: string
scrollDownButtonText?: string
onReject: () => void
onConfirm?: () => void
disableConfirm?: boolean
contentContainerStyle?: StyleProp<AnimatedStyle<StyleProp<ViewStyle>>>
cancelButtonProps?: ButtonProps
confirmationButtonProps?: ButtonProps
}
>
......@@ -42,11 +45,14 @@ const isCloseToBottom = ({ layoutMeasurement, contentOffset, contentSize }: Nati
export function ModalWithOverlay({
children,
confirmationButtonText,
cancelButtonText,
scrollDownButtonText,
onReject,
onConfirm,
disableConfirm,
contentContainerStyle,
cancelButtonProps,
confirmationButtonProps,
...bottomSheetModalProps
}: ModalWithOverlayProps): JSX.Element {
const scrollViewRef = useRef<ScrollView>(null)
......@@ -129,10 +135,13 @@ export function ModalWithOverlay({
</BottomSheetScrollView>
<ModalFooter
cancelButtonText={cancelButtonText}
confirmationButtonText={confirmationButtonText}
confirmationEnabled={!disableConfirm && confirmationEnabled}
scrollDownButtonText={scrollDownButtonText}
showScrollDownOverlay={showOverlay && !eip5792MethodsEnabled}
cancelButtonProps={cancelButtonProps}
confirmationButtonProps={confirmationButtonProps}
onConfirm={onConfirm}
onReject={onReject}
onScrollDownPress={handleScrollDown}
......@@ -144,8 +153,11 @@ export function ModalWithOverlay({
type ModalFooterProps = {
confirmationEnabled: boolean
showScrollDownOverlay: boolean
cancelButtonText?: string
confirmationButtonText?: string
scrollDownButtonText?: string
cancelButtonProps?: ButtonProps
confirmationButtonProps?: ButtonProps
onScrollDownPress: () => void
onReject: () => void
onConfirm?: () => void
......@@ -155,7 +167,10 @@ function ModalFooter({
confirmationEnabled,
showScrollDownOverlay,
scrollDownButtonText,
cancelButtonText,
confirmationButtonText,
cancelButtonProps,
confirmationButtonProps,
onScrollDownPress,
onReject,
onConfirm,
......@@ -188,8 +203,8 @@ function ModalFooter({
pt="$spacing12"
px="$spacing24"
>
<Button size="large" testID={TestID.Cancel} emphasis="tertiary" onPress={onReject}>
{t('common.button.cancel')}
<Button size="large" testID={TestID.Cancel} emphasis="tertiary" onPress={onReject} {...cancelButtonProps}>
{cancelButtonText ?? t('common.button.cancel')}
</Button>
{confirmationButtonText && (
......@@ -199,6 +214,7 @@ function ModalFooter({
size="large"
testID={TestID.Confirm}
onPress={onConfirm}
{...confirmationButtonProps}
>
{confirmationButtonText}
</Button>
......
......@@ -20,26 +20,26 @@ export function ClientDetails({
request: WalletConnectSigningRequest
permitInfo?: PermitInfo
}): JSX.Element {
const { dapp } = request
const { dappRequestInfo } = request
const colors = useSporeColors()
const permitCurrencyInfo = useCurrencyInfo(permitInfo?.currencyId)
return (
<Flex centered gap="$spacing12">
<DappHeaderIcon dapp={dapp} permitCurrencyInfo={permitCurrencyInfo} />
<DappHeaderIcon dappRequestInfo={dappRequestInfo} permitCurrencyInfo={permitCurrencyInfo} />
<HeaderText permitAmount={permitInfo?.amount} permitCurrency={permitCurrencyInfo?.currency} request={request} />
<LinkButton
color={colors.accent1.val}
iconColor="$accent1"
label={formatDappURL(dapp.url)}
label={formatDappURL(dappRequestInfo.url)}
mb="$spacing12"
px="$spacing8"
py="$spacing4"
showIcon={false}
size={iconSizes.icon12}
textVariant="buttonLabel2"
url={dapp.url}
url={dappRequestInfo.url}
/>
</Flex>
)
......
......@@ -16,7 +16,7 @@ export function HeaderText({
permitAmount?: number
permitCurrency?: Currency | null
}): JSX.Element {
const { dapp, type: method } = request
const { dappRequestInfo, type: method } = request
if (permitCurrency) {
const readablePermitAmount = getCurrencyAmount({
......@@ -32,7 +32,7 @@ export function HeaderText({
components={{ highlight: <Text variant="heading3" fontWeight="bold" /> }}
i18nKey="qrScanner.request.withAmount"
values={{
dappName: dapp.name,
dappName: dappRequestInfo.name,
currencySymbol: permitCurrency?.symbol,
amount: readablePermitAmount,
}}
......@@ -45,7 +45,7 @@ export function HeaderText({
components={{ highlight: <Text variant="heading3" fontWeight="bold" /> }}
i18nKey="qrScanner.request.withoutAmount"
values={{
dappName: dapp.name,
dappName: dappRequestInfo.name,
currencySymbol: permitCurrency?.symbol,
}}
/>
......@@ -72,7 +72,7 @@ export function HeaderText({
return (
<Text textAlign="center" variant="subheading1">
{getReadableMethodName(method, dapp.name || dapp.url)}
{getReadableMethodName(method, dappRequestInfo.name || dappRequestInfo.url)}
</Text>
)
}
......@@ -33,7 +33,7 @@ import { useHasAccountMismatchCallback } from 'uniswap/src/features/smartWallet/
import { MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
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 { formatExternalTxnWithGasEstimates } from 'wallet/src/features/gas/formatExternalTxnWithGasEstimates'
import { useIsBlockedActiveAddress } from 'wallet/src/features/trm/hooks'
......@@ -89,9 +89,11 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
const getHasMismatch = useHasAccountMismatchCallback()
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 => {
if (!netInfo.isInternetReachable) {
if (!netInfo.isInternetReachable && !suppressOfflineWarning) {
return false
}
......@@ -123,7 +125,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
const rejectOnCloseRef = useRef(true)
const onReject = async (): Promise<void> => {
if (request.dapp.source === 'walletconnect') {
if (request.dappRequestInfo.requestType === DappRequestType.WalletConnectSessionRequest) {
await wcWeb3Wallet.respondSessionRequest({
topic: request.sessionId,
response: {
......@@ -139,8 +141,8 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
sendAnalyticsEvent(MobileEventName.WalletConnectSheetCompleted, {
request_type: isTransactionRequest(request) ? WCEventType.TransactionRequest : WCEventType.SignRequest,
eth_method: request.type,
dapp_url: request.dapp.url,
dapp_name: request.dapp.name,
dapp_url: request.dappRequestInfo.url,
dapp_name: request.dappRequestInfo.name,
wc_version: '2',
chain_id: chainId,
outcome: WCRequestOutcome.Reject,
......@@ -178,7 +180,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
method: request.type === EthMethod.WalletSendCalls ? EthMethod.WalletSendCalls : EthMethod.EthSendTransaction,
transaction: txnWithFormattedGasEstimates,
account: signerAccount,
dapp: request.dapp,
dappRequestInfo: request.dappRequestInfo,
chainId,
request,
}),
......@@ -192,7 +194,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
method: request.type,
message: request.message || request.rawMessage,
account: signerAccount,
dapp: request.dapp,
dappRequestInfo: request.dappRequestInfo,
chainId,
}),
)
......@@ -203,8 +205,8 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
sendAnalyticsEvent(MobileEventName.WalletConnectSheetCompleted, {
request_type: isTransactionRequest(request) ? WCEventType.TransactionRequest : WCEventType.SignRequest,
eth_method: request.type,
dapp_url: request.dapp.url,
dapp_name: request.dapp.name,
dapp_url: request.dappRequestInfo.url,
dapp_name: request.dappRequestInfo.name,
wc_version: '2',
chain_id: chainId,
outcome: WCRequestOutcome.Confirm,
......@@ -259,7 +261,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
}
// 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 (
<KidSuperCheckinModal request={request} onClose={handleClose} onConfirm={onConfirmPress} onReject={onReject} />
)
......
......@@ -82,6 +82,9 @@ export function WalletConnectRequestModalContent({
const hasGasFee = getDoesMethodCostGas(request)
// If link mode is supported, we can sign messages through universal links on device
const suppressOfflineWarning = request.isLinkModeSupported
return (
<>
<Flex px="$spacing24">
......@@ -118,7 +121,7 @@ export function WalletConnectRequestModalContent({
</Flex>
)}
{!netInfo.isInternetReachable ? (
{!netInfo.isInternetReachable && !suppressOfflineWarning ? (
<BaseCard.InlineErrorState
backgroundColor="$statusWarning2"
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 {
} from 'uniswap/src/features/gating/configs'
import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks'
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 { ContractManager } from 'wallet/src/features/contracts/ContractManager'
import { ProviderManager } from 'wallet/src/features/providers/ProviderManager'
......@@ -115,15 +122,21 @@ export async function getFormattedUwuLinkTxnRequest({
providerManager,
contractManager,
}: 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
internalId: UWULINK_PREFIX,
account: activeAccount?.address,
dapp: {
dappRequestInfo: {
name: '',
url: '',
...request.dapp,
source: UWULINK_PREFIX,
requestType: DappRequestType.UwULink,
chain_id: request.chainId,
webhook: request.webhook,
},
......
......@@ -61,10 +61,10 @@ export function WalletConnectModals(): JSX.Element {
// When WalletConnectModal is open and a WC QR code is scanned to add a pendingSession,
// dismiss the scan modal in favor of showing PendingConnectionModal
useEffect(() => {
if (modalState.isOpen && pendingSession) {
if (modalState.isOpen && (pendingSession || currRequest)) {
dispatch(closeModal({ name: ModalName.WalletConnectScan }))
}
}, [modalState.isOpen, pendingSession, dispatch])
}, [modalState.isOpen, pendingSession, currRequest, dispatch])
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 { useDispatch } from 'react-redux'
import { navigate } from 'src/app/navigation/rootNavigation'
import { AppStackScreenProp } from 'src/app/navigation/types'
import { useReactNavigationModal } from 'src/components/modals/useReactNavigationModal'
import { WalletRestoreType } from 'src/components/RestoreWalletModal/RestoreWalletModalState'
import { closeAllModals } from 'src/features/modals/modalSlice'
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 { GenericHeader } from 'uniswap/src/components/misc/GenericHeader'
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 { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile'
......@@ -21,43 +24,72 @@ const SHADOW_OPACITY = 0.3
const SHADOW_OFFSET = { width: 0, height: 0 } as const
const ICON_OFFSET = -spacing.spacing8
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 }]}
/>
)
}
export function RestoreWalletModal(): JSX.Element | null {
/**
* This modal is used to prompt the user to restore their wallet depending on the type of
* restoration needed.
*/
export function RestoreWalletModal({ route }: AppStackScreenProp<typeof ModalName.RestoreWallet>): JSX.Element | null {
const { t } = useTranslation()
const colors = useSporeColors()
const dispatch = useDispatch()
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 => {
onClose()
dispatch(closeAllModals()) // still need this until all modals are migrated to react-navigation
navigate(MobileScreens.OnboardingStack, {
screen: OnboardingScreens.RestoreCloudBackupLoading,
params: {
entryPoint: OnboardingEntryPoint.Sidebar,
importType: ImportType.RestoreMnemonic,
},
})
switch (restoreType) {
case WalletRestoreType.SeedPhrase: {
navigate(MobileScreens.OnboardingStack, {
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 (
<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
......@@ -87,23 +119,43 @@ export function RestoreWalletModal(): JSX.Element | null {
>
<WalletFilled color="$neutral1" size="$icon.24" />
<Flex position="absolute" bottom={ICON_OFFSET} right={ICON_OFFSET}>
<ArrowDownCircle color="$accent1" size="$icon.24" />
<ArrowDownCircleFilledWithBorder color="$accent1" size="$icon.24" />
</Flex>
</Flex>
</Flex>
<GenericHeader
title={t('account.wallet.button.restore')}
titleVariant="subheading1"
subtitle={t('account.wallet.restore.description')}
subtitleVariant="body3"
/>
<Flex row>
<Button variant="branded" emphasis="primary" size="large" onPress={onRestore}>
{t('common.button.continue')}
</Button>
<GenericHeader title={title} titleVariant="subheading1" subtitle={description} subtitleVariant="body3" />
<Flex gap="$spacing8" width="100%">
<Flex row>
<Button testID={TestID.Continue} variant="branded" emphasis="primary" size="medium" onPress={onRestore}>
{t('common.button.continue')}
</Button>
</Flex>
{isDismissible && (
<Flex row>
<Button testID={TestID.Cancel} variant="default" emphasis="secondary" size="medium" onPress={onClose}>
{t('common.button.notNow')}
</Button>
</Flex>
)}
</Flex>
</Flex>
</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 } from 'react'
import { default as React, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
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 { Check, Contrast, Moon, Sun } from 'ui/src/components/icons'
import { Modal } from 'uniswap/src/components/modals/Modal'
......@@ -13,13 +12,10 @@ import { AppearanceSettingType, setSelectedAppearanceSettings } from 'wallet/src
export function SettingsAppearanceModal(): JSX.Element {
const { t } = useTranslation()
const currentTheme = useCurrentAppearanceSetting()
const dispatch = useDispatch()
const { onClose } = useReactNavigationModal()
return (
<Modal
name={ModalName.SettingsAppearance}
onClose={(): Action => dispatch(closeModal({ name: ModalName.SettingsAppearance }))}
>
<Modal name={ModalName.SettingsAppearance} onClose={onClose}>
<Flex animation="fast" gap="$spacing16" pb="$spacing24" px="$spacing24" width="100%">
<Flex centered>
<Text color="$neutral1" variant="subheading1">
......@@ -33,6 +29,7 @@ export function SettingsAppearanceModal(): JSX.Element {
option={AppearanceSettingType.System}
subtitle={t('settings.setting.appearance.option.device.subtitle')}
title={t('settings.setting.appearance.option.device.title')}
onClose={onClose}
/>
<AppearanceOption
Icon={Sun}
......@@ -40,6 +37,7 @@ export function SettingsAppearanceModal(): JSX.Element {
option={AppearanceSettingType.Light}
subtitle={t('settings.setting.appearance.option.light.subtitle')}
title={t('settings.setting.appearance.option.light.title')}
onClose={onClose}
/>
<AppearanceOption
Icon={Moon}
......@@ -47,6 +45,7 @@ export function SettingsAppearanceModal(): JSX.Element {
option={AppearanceSettingType.Dark}
subtitle={t('settings.setting.appearance.option.dark.subtitle')}
title={t('settings.setting.appearance.option.dark.title')}
onClose={onClose}
/>
</Flex>
</Flex>
......@@ -60,20 +59,26 @@ interface AppearanceOptionProps {
subtitle: string
option: AppearanceSettingType
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 showCheckMarkOpacity = active ? 1 : 0
const changeTheme = useCallback(async (): Promise<void> => {
dispatch(setSelectedAppearanceSettings(option))
onClose()
}, [dispatch, option, onClose])
return (
<TouchableArea
alignItems="center"
flexDirection="row"
justifyContent="space-between"
py="$spacing12"
onPress={(): Action => dispatch(setSelectedAppearanceSettings(option))}
onPress={changeTheme}
>
<Icon color="$neutral2" size="$icon.24" strokeWidth={1.5} />
<Flex row shrink>
......
import { NavigatorScreenParams, useNavigation } from '@react-navigation/native'
import { memo, useCallback } from 'react'
import { ValueOf } from 'react-native-gesture-handler/lib/typescript/typeUtils'
import { useDispatch } from 'react-redux'
import { navigate } from 'src/app/navigation/rootNavigation'
import {
AppStackNavigationProp,
......@@ -12,7 +11,6 @@ import {
} from 'src/app/navigation/types'
import { ConnectionsDappsListModalState } from 'src/components/Settings/ConnectionsDappModal/ConnectionsDappsListModalState'
import { EditWalletSettingsModalState } from 'src/components/Settings/EditWalletModal/EditWalletSettingsModalState'
import { openModal } from 'src/features/modals/modalSlice'
import { useIsScreenNavigationReady } from 'src/utils/useIsScreenNavigationReady'
import { Flex, Skeleton, Switch, Text, TouchableArea, useSporeColors } from 'ui/src'
import { Arrow } from 'ui/src/components/arrow/Arrow'
......@@ -36,12 +34,6 @@ export interface SettingsSectionItemComponent {
isHidden?: boolean
}
type SettingsModal =
| typeof ModalName.LanguageSelector
| typeof ModalName.SettingsAppearance
| typeof ModalName.PortfolioBalanceModal
| typeof ModalName.PermissionsModal
type SettingsNavigationModal =
| typeof ModalName.BiometricsModal
| typeof ModalName.FiatCurrencySelector
......@@ -50,11 +42,16 @@ type SettingsNavigationModal =
| typeof ModalName.ConnectionsDappListModal
| typeof ModalName.SmartWalletAdvancedSettingsModal
| typeof ModalName.PasskeyManagement
| typeof ModalName.Experiments
| typeof ModalName.SettingsAppearance
| typeof ModalName.PermissionsModal
| typeof ModalName.PortfolioBalanceModal
| typeof ModalName.LanguageSelector
export interface SettingsSectionItem {
screen?: keyof SettingsStackParamList | typeof MobileScreens.OnboardingStack
modal?: SettingsModal
navigationModal?: SettingsNavigationModal
testID?: string
screenProps?: ValueOf<SettingsStackParamList> | NavigatorScreenParams<OnboardingStackParamList>
navigationProps?:
| ConnectionsDappsListModalState
......@@ -78,13 +75,13 @@ interface SettingsRowProps {
page: SettingsSectionItem
navigation: SettingsStackNavigationProp & OnboardingStackNavigationProp
checkIfCanProceed?: SettingsSectionItem['checkIfCanProceed']
testID?: string
}
export const SettingsRow = memo(
({
page: {
screen,
modal,
navigationModal,
screenProps,
navigationProps,
......@@ -98,12 +95,12 @@ export const SettingsRow = memo(
onToggle,
isToggleEnabled,
count,
testID,
},
navigation,
checkIfCanProceed,
}: SettingsRowProps): JSX.Element => {
const colors = useSporeColors()
const dispatch = useDispatch()
const handleRow = useCallback(async (): Promise<void> => {
if (checkIfCanProceed && !checkIfCanProceed()) {
......@@ -114,28 +111,15 @@ export const SettingsRow = memo(
return
} else if (screen) {
navigation.navigate(screen, screenProps)
} else if (modal) {
dispatch(openModal({ name: modal }))
} else if (navigationModal) {
navigate(navigationModal, navigationProps)
} else if (externalLink) {
await openUri(externalLink)
}
}, [
checkIfCanProceed,
onToggle,
screen,
navigation,
screenProps,
navigationProps,
modal,
navigationModal,
dispatch,
externalLink,
])
}, [checkIfCanProceed, onToggle, screen, navigation, screenProps, navigationProps, navigationModal, externalLink])
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={subText ? 'flex-start' : 'center'} flexBasis={0} gap="$spacing12">
<Flex centered height={32} width={32}>
......@@ -157,7 +141,6 @@ export const SettingsRow = memo(
)}
<RowRightContent
screen={screen}
modal={modal}
navigationModal={navigationModal}
externalLink={externalLink}
disabled={disabled}
......@@ -193,7 +176,6 @@ const LOADING_DIMENSIONS = {
const RowRightContent = memo(
({
screen,
modal,
navigationModal,
externalLink,
disabled,
......@@ -205,7 +187,6 @@ const RowRightContent = memo(
}: Pick<
SettingsSectionItem,
| 'screen'
| 'modal'
| 'navigationModal'
| 'externalLink'
| 'disabled'
......@@ -236,7 +217,7 @@ const RowRightContent = memo(
)
}
if (screen || modal || navigationModal) {
if (screen || navigationModal) {
return (
<Flex centered row>
{currentSetting &&
......
......@@ -3,6 +3,7 @@ import { Alert } from 'react-native'
import { Accordion, Flex, Text } from 'ui/src'
import { GatingButton } from 'uniswap/src/components/gating/GatingButton'
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'
export function MissileaneousDevSection(): JSX.Element {
......@@ -11,11 +12,11 @@ export function MissileaneousDevSection(): JSX.Element {
<Text variant="heading3">Misc.</Text>
<Flex flexDirection="column">
<Accordion.Item value="other-configs">
<AccordionHeader title="🤯 Seed Phrase & Private Keys" />
<Accordion.Content gap="$spacing12">
<AccordionHeader title="🤯 Seed Phrase & Private Keys" testId={TestID.DevSeedPhrasePrivateKeysAccordion} />
<Accordion.Content testID={TestID.DevDeleteSeedPhraseButton} gap="$spacing12">
<GatingButton onPress={onDeleteSeedPhrase}>Delete Seed Phrase (Irreversible)</GatingButton>
</Accordion.Content>
<Accordion.Content gap="$spacing12">
<Accordion.Content testID={TestID.DevDeletePrivateKeysButton} gap="$spacing12">
<GatingButton onPress={onDeletePrivateKeys}>Delete Private Keys (Irreversible)</GatingButton>
</Accordion.Content>
</Accordion.Item>
......
......@@ -5,6 +5,7 @@ import { SearchEmptySection } from 'src/components/explore/search/SearchEmptySec
import { SearchResultsSection } from 'src/components/explore/search/SearchResultsSection'
import { Flex, Text, TouchableArea, flexStyles } from 'ui/src'
import { useTranslation } from 'react-i18next'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
......@@ -63,15 +64,33 @@ function NewExploreSearchResultsList({
debouncedSearchQuery: string | null
debouncedParsedSearchQuery: string | null
}): JSX.Element {
const { t } = useTranslation()
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 (
<Trace section={SectionName.ExploreSearch}>
<Flex row px="$spacing20" pt="$spacing16" pb="$spacing8" gap="$spacing16">
{MOBILE_SEARCH_TABS.map((tab) => (
<TouchableArea key={tab} onPress={() => setActiveTab(tab)}>
<Text color={activeTab === tab ? '$neutral1' : '$neutral2'} variant="buttonLabel2">
{tab}
{getTabLabel(tab)}
</Text>
</TouchableArea>
))}
......
import { useFocusEffect, useNavigation } from '@react-navigation/core'
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 { usePrevious } from 'react-native-wagmi-charts'
import { navigate } from 'src/app/navigation/rootNavigation'
import { WalletRestoreType } from 'src/components/RestoreWalletModal/RestoreWalletModalState'
import { MnemonicDisplay } from 'src/components/mnemonic/MnemonicDisplay'
import { useBiometricAppSettings } from 'src/features/biometrics/useBiometricAppSettings'
import { useBiometricPrompt } from 'src/features/biometricsSettings/hooks'
......@@ -21,17 +22,27 @@ type Props = {
export function SeedPhraseDisplay({ mnemonicId, onDismiss, walletNeedsRestore }: Props): JSX.Element {
const { t } = useTranslation()
const { isModalOpen: isWalletRestoreModalOpen } = useWalletRestore({ openModalImmediately: true })
const { walletRestoreType } = useWalletRestore({
openModalImmediately: true,
})
const [showSeedPhrase, setShowSeedPhrase] = useState(false)
const navigation = useNavigation()
const [showSeedPhraseViewWarningModal, setShowSeedPhraseViewWarningModal] = useState(!walletNeedsRestore)
const prevIsWalletRestoreModalOpen = usePrevious(isWalletRestoreModalOpen)
useFocusEffect(
useCallback(() => {
if (walletRestoreType !== WalletRestoreType.None) {
navigation.goBack()
useEffect(() => {
if (prevIsWalletRestoreModalOpen && !isWalletRestoreModalOpen) {
onDismiss?.()
}
})
// This is a very unlikely edge case if the user somehow get to this screen on a new device.
// In this case, we want to back an additional time to dismiss the NewDevice modal which is
// will try to reopen anytime this screen is focused.
if (walletRestoreType === WalletRestoreType.NewDevice) {
navigation.goBack()
}
}
}, [walletRestoreType, navigation]),
)
const onShowSeedPhraseConfirmed = (): void => {
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
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { TestnetModeModal } from 'uniswap/src/features/testnets/TestnetModeModal'
import { HiddenTokenInfoModal } from 'uniswap/src/features/transactions/modals/HiddenTokenInfoModal'
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 { 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
type ValidModalNames = keyof Pick<
......@@ -17,6 +24,13 @@ type ValidModalNames = keyof Pick<
| typeof ModalName.PasskeyManagement
| typeof ModalName.PasskeysHelp
| 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 = {
......@@ -24,7 +38,14 @@ type ModalNameWithComponentProps = {
[ModalName.HiddenTokenInfoModal]: GetProps<typeof HiddenTokenInfoModal>
[ModalName.PasskeyManagement]: GetProps<typeof PasskeyManagementModal>
[ModalName.PasskeysHelp]: GetProps<typeof PasskeysHelpModal>
[ModalName.PostSwapSmartWalletNudge]: GetProps<typeof PostSwapSmartWalletNudge>
[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> = {
......
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'
/**
......@@ -8,22 +8,26 @@ import { useAppStackNavigation } from 'src/app/navigation/types'
*/
export function useReactNavigationModal(): {
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()
// 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.
const closeHasBeenCalledRef = useRef(false)
const preventCloseRef = useRef(false)
const onClose = useCallback(() => {
if (closeHasBeenCalledRef.current) {
if (preventCloseRef.current) {
return
}
closeHasBeenCalledRef.current = true
preventCloseRef.current = true
navigation.goBack()
}, [navigation])
return {
onClose,
preventCloseRef,
}
}
......@@ -12,12 +12,8 @@ export interface AppModalState<T> {
export interface ModalsState {
[ModalName.Experiments]: AppModalState<undefined>
[ModalName.FiatOnRampAggregator]: AppModalState<FiatOnRampModalState>
[ModalName.LanguageSelector]: AppModalState<undefined>
[ModalName.PortfolioBalanceModal]: AppModalState<undefined>
[ModalName.PermissionsModal]: AppModalState<undefined>
[ModalName.QueuedOrderModal]: AppModalState<undefined>
[ModalName.Send]: AppModalState<TransactionState & { sendScreen: TransactionScreen }>
[ModalName.Swap]: AppModalState<TransactionState>
[ModalName.SettingsAppearance]: AppModalState<undefined>
[ModalName.WalletConnectScan]: AppModalState<ScannerModalState>
}
......@@ -22,21 +22,6 @@ type FiatOnRampAggregatorModalParams = {
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 = {
name: typeof ModalName.WalletConnectScan
initialState: ScannerModalState
......@@ -51,18 +36,9 @@ type SendModalParams = {
}
}
type PermissionsModalParams = {
name: typeof ModalName.PermissionsModal
initialState?: undefined
}
export type OpenModalParams =
| FiatOnRampAggregatorModalParams
| PortfolioBalanceModalParams
| PermissionsModalParams
| LanguageSelectorModalParams
| SendModalParams
| SettingsAppearanceModalParams
| SwapModalParams
| WalletConnectModalParams
......
......@@ -10,7 +10,7 @@ import { ApplicationTransport } from 'utilities/src/telemetry/analytics/Applicat
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { analytics } from 'utilities/src/telemetry/analytics/analytics'
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() {
yield* delay(1)
......
import { StackActions } from '@react-navigation/core'
import { CommonActions } from '@react-navigation/core'
import { dispatchNavigationAction } from 'src/app/navigation/rootNavigation'
import { call, put, takeEvery } from 'typed-redux-saga'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
......@@ -21,5 +21,11 @@ function* onRestoreMnemonicComplete() {
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'
jest.mock('wallet/src/features/wallet/Keyring/Keyring', () => ({
......
import { useFocusEffect, useIsFocused } from '@react-navigation/core'
import { useCallback, useEffect, useState } from 'react'
import { useFocusEffect } from '@react-navigation/core'
import { useCallback, useEffect, useRef, useState } from 'react'
import { navigate } from 'src/app/navigation/rootNavigation'
import { WalletRestoreType } from 'src/components/RestoreWalletModal/RestoreWalletModalState'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
......@@ -8,53 +9,39 @@ import { logger } from 'utilities/src/logger/logger'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
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 = {
/**
* 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
}
/**
* Hook to determine if the wallet needs to be restored and what type of restoration is needed.
* If a restoration is needed, the relevant modal will be opened.
* Hook to determine if the wallet needs to be restored and what type of restore is needed.
* If a restore is needed, the relevant modal will be opened.
*/
export function useWalletRestore(params?: Props): {
walletNeedsRestore: boolean
openWalletRestoreModal: () => void
isModalOpen: boolean
walletRestoreType: WalletRestoreType
} {
const shouldRestoreSeedPhraseFF = useFeatureFlag(FeatureFlags.RestoreSeedPhrase)
const { openModalImmediately } = params ?? {}
const openedOnce = useRef(false)
const openModalImmediately = params?.openModalImmediately
const [walletRestoreType, setWalletRestoreType] = useState<WalletRestoreType>(WalletRestoreType.None)
const walletNeedsRestore = walletRestoreType !== WalletRestoreType.None
const mnemonicIdFromLocalState = useSignerAccounts()[0]?.mnemonicId
const isModalOpen = useIsFocused()
const openWalletRestoreModal = useCallback((): void => {
switch (walletRestoreType) {
case WalletRestoreType.NewDevice:
navigate(ModalName.RestoreWallet)
navigate(ModalName.RestoreWallet, { restoreType: WalletRestoreType.NewDevice })
break
case WalletRestoreType.SeedPhrase:
navigate(ModalName.RestoreWallet)
openedOnce.current = true
navigate(ModalName.RestoreWallet, { restoreType: WalletRestoreType.SeedPhrase })
break
case WalletRestoreType.None:
break
......@@ -71,13 +58,13 @@ export function useWalletRestore(params?: Props): {
useFocusEffect(
useCallback(() => {
if (openModalImmediately && walletNeedsRestore) {
if (openModalImmediately && walletNeedsRestore && !openedOnce.current) {
openWalletRestoreModal()
}
}, [openModalImmediately, openWalletRestoreModal, walletNeedsRestore]),
)
return { walletNeedsRestore, openWalletRestoreModal, isModalOpen }
return { walletNeedsRestore, openWalletRestoreModal, walletRestoreType }
}
/**
......
import { getInternalError, getSdkError } from '@walletconnect/utils'
import { navigate } from 'src/app/navigation/rootNavigation'
import { wcWeb3Wallet } from 'src/features/walletConnect/walletConnectClient'
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 { 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 { getFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { logger } from 'utilities/src/logger/logger'
import { getCallsStatusHelper } from 'wallet/src/features/batchedTransactions/eip5792Utils'
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
......@@ -115,6 +119,8 @@ export function* handleGetCapabilities(
requestId: number,
accountAddress: string,
requestedAccount: string,
dappName?: string,
dappIconUrl?: string,
) {
const eip5792MethodsEnabled = isEip5792MethodsEnabled()
......@@ -128,13 +134,39 @@ export function* handleGetCapabilities(
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], {
topic,
response: {
id: requestId,
jsonrpc: '2.0',
// 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(
if (sessions && sessions[topic]) {
const wcSession = sessions[topic]
return {
dappIcon: wcSession?.dapp?.icon || null,
dappName: wcSession?.dapp?.name || '',
dappIcon: wcSession?.dappRequestInfo?.icon || null,
dappName: wcSession?.dappRequestInfo?.name || '',
}
}
} 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 { WalletKitTypes } from '@reown/walletkit'
import { PendingRequestTypes, ProposalTypes, SessionTypes } from '@walletconnect/types'
import { buildApprovedNamespaces, getSdkError } from '@walletconnect/utils'
import { PendingRequestTypes, ProposalTypes, SessionTypes, Verify } from '@walletconnect/types'
import { buildApprovedNamespaces, getSdkError, populateAuthPayload } from '@walletconnect/utils'
import { Alert } from 'react-native'
import { EventChannel, eventChannel } from 'redux-saga'
import { MobileState } from 'src/app/mobileReducer'
......@@ -20,9 +20,11 @@ import {
parseSendCallsRequest,
parseSignRequest,
parseTransactionRequest,
parseVerifyStatus,
} from 'src/features/walletConnect/utils'
import { initializeWeb3Wallet, wcWeb3Wallet } from 'src/features/walletConnect/walletConnectClient'
import {
SignRequest,
addPendingSession,
addRequest,
addSession,
......@@ -38,22 +40,31 @@ import { getFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types'
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 { ONE_SECOND_MS } from 'utilities/src/time/time'
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> {
return eventChannel<AnyAction>((emit) => {
/*
* Handle incoming `session_proposal` events that contain the dapp attempting to pair
* and the proposal namespaces (chains, methods, events)
*/
const sessionProposalHandler = async (
proposalEvent: Omit<WalletKitTypes.BaseEventArgs<ProposalTypes.Struct>, 'topic'>,
): Promise<void> => {
const { params: proposal } = proposalEvent
emit({ type: 'session_proposal', proposal })
const sessionProposalHandler = async (proposalEvent: WalletKitTypes.SessionProposal): Promise<void> => {
const { params: proposal, verifyContext } = proposalEvent
emit({ type: 'session_proposal', proposal: { ...proposal, verifyContext } })
}
const sessionRequestHandler = async (request: WalletKitTypes.SessionRequest): Promise<void> => {
......@@ -64,14 +75,20 @@ function createWalletConnectChannel(): EventChannel<AnyAction> {
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_request', sessionRequestHandler)
wcWeb3Wallet.on('session_delete', sessionDeleteHandler)
wcWeb3Wallet.on('session_authenticate', sessionAuthenticateHandler)
const unsubscribe = (): void => {
wcWeb3Wallet.off('session_proposal', sessionProposalHandler)
wcWeb3Wallet.off('session_request', sessionRequestHandler)
wcWeb3Wallet.off('session_delete', sessionDeleteHandler)
wcWeb3Wallet.off('session_authenticate', sessionAuthenticateHandler)
}
return unsubscribe
......@@ -88,6 +105,8 @@ function* watchWalletConnectEvents() {
yield* call(handleSessionProposal, event.proposal)
} else if (event.type === 'session_request') {
yield* call(handleSessionRequest, event.request)
} else if (event.type === 'session_authenticate') {
yield* call(handleSessionAuthenticate, event.authenticate)
} else if (event.type === 'session_delete') {
yield* call(handleSessionDelete, event.session)
}
......@@ -132,7 +151,7 @@ function* cancelErrorSession(dappName: string, chainLabels: string, proposalId:
yield* put(setHasPendingSessionError(false))
}
function* handleSessionProposal(proposal: ProposalTypes.Struct) {
export function* handleSessionProposal(proposal: ProposalTypes.Struct & { verifyContext?: Verify.Context }) {
const activeAccountAddress = yield* select(selectActiveAccountAddress)
const {
......@@ -158,16 +177,7 @@ function* handleSessionProposal(proposal: ProposalTypes.Struct) {
supportedNamespaces: {
eip155: {
chains: supportedEip155Chains,
methods: [
EthMethod.EthSign,
EthMethod.EthSendTransaction,
EthMethod.PersonalSign,
EthMethod.SignTypedData,
EthMethod.SignTypedDataV4,
EthMethod.WalletGetCapabilities,
EthMethod.WalletSendCalls,
EthMethod.WalletGetCallsStatus,
],
methods: WC_SUPPORTED_METHODS,
events: [EthEvent.AccountsChanged, EthEvent.ChainChanged],
accounts,
},
......@@ -183,17 +193,20 @@ function* handleSessionProposal(proposal: ProposalTypes.Struct) {
proposalChainIds.push(...(getSupportedWalletConnectChains(eip155Chains) ?? []))
})
const verifyStatus = parseVerifyStatus(proposal.verifyContext)
yield* put(
addPendingSession({
wcSession: {
id: id.toString(),
proposalNamespaces: namespaces,
chains: proposalChainIds,
dapp: {
verifyStatus,
dappRequestInfo: {
name: dapp.name,
url: dapp.url,
icon: dapp.icons[0] ?? null,
source: 'walletconnect',
requestType: DappRequestType.WalletConnectSessionRequest,
},
},
}),
......@@ -222,6 +235,67 @@ const eip5792Methods = [EthMethod.WalletGetCallsStatus, EthMethod.WalletSendCall
(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) {
const { topic, params, id } = sessionRequest
const { request: wcRequest, chainId: wcChainId } = params
......@@ -281,7 +355,7 @@ function* handleSessionRequest(sessionRequest: PendingRequestTypes.Struct) {
}
case EthMethod.WalletGetCapabilities: {
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
}
default:
......@@ -357,11 +431,11 @@ function* populateActiveSessions() {
addSession({
wcSession: {
id: session.topic,
dapp: {
dappRequestInfo: {
name: session.peer.metadata.name,
url: session.peer.metadata.url,
icon: session.peer.metadata.icons[0] ?? null,
source: 'walletconnect',
requestType: DappRequestType.WalletConnectSessionRequest,
},
chains,
namespaces: session.namespaces,
......
/* eslint-disable complexity */
import { buildAuthObject, getSdkError } from '@walletconnect/utils'
import { providers } from 'ethers'
import { wcWeb3Wallet } from 'src/features/walletConnect/walletConnectClient'
import {
......@@ -13,7 +15,7 @@ import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import { getEnabledChainIdsSaga } from 'uniswap/src/features/settings/saga'
import { TransactionOriginType, TransactionType } from 'uniswap/src/features/transactions/types/transactionDetails'
import { 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 { logger } from 'utilities/src/logger/logger'
import { addBatchedTransaction } from 'wallet/src/features/batchedTransactions/slice'
......@@ -32,7 +34,7 @@ type SignMessageParams = {
message: string
account: Account
method: EthSignMethod
dapp: DappInfo
dappRequestInfo: DappRequestInfo
chainId: UniverseChainId
}
......@@ -42,7 +44,7 @@ type SignTransactionParams = {
transaction: providers.TransactionRequest
account: Account
method: EthMethod.EthSendTransaction | EthMethod.WalletSendCalls
dapp: DappInfo
dappRequestInfo: DappRequestInfo
chainId: UniverseChainId
request: TransactionRequest | UwuLinkErc20Request | WalletSendCallsEncodedRequest
}
......@@ -57,7 +59,10 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
result = yield* call(signMessage, params.message, account, signerManager)
// TODO: add `isCheckIn` type to uwulink request info so that this can be generalized
if (params.dapp.source === 'uwulink' && params.dapp.name === 'Uniswap Cafe') {
if (
params.dappRequestInfo.requestType === DappRequestType.UwULink &&
params.dappRequestInfo.name === 'Uniswap Cafe'
) {
yield* put(
pushNotification({
type: AppNotificationType.Success,
......@@ -94,7 +99,7 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
},
typeInfo: {
type: TransactionType.WCConfirm,
dapp: params.dapp,
dappRequestInfo: params.dappRequestInfo,
},
transactionOriginType: TransactionOriginType.External,
}
......@@ -117,7 +122,7 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
},
typeInfo: {
type: TransactionType.WCConfirm,
dapp: params.dapp,
dappRequestInfo: params.dappRequestInfo,
},
transactionOriginType: TransactionOriginType.External,
}
......@@ -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, {
topic: sessionId,
response: {
......@@ -153,8 +178,8 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
result,
},
})
} else if (params.dapp.source === 'uwulink' && params.dapp.webhook) {
fetch(params.dapp.webhook, {
} else if (params.dappRequestInfo.requestType === DappRequestType.UwULink && params.dappRequestInfo.webhook) {
fetch(params.dappRequestInfo.webhook, {
method: 'POST',
headers: {
Accept: 'application/json',
......@@ -169,7 +194,7 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
)
}
} catch (error) {
if (params.dapp.source === 'walletconnect') {
if (params.dappRequestInfo.requestType === DappRequestType.WalletConnectSessionRequest) {
yield* call(wcWeb3Wallet.respondSessionRequest, {
topic: sessionId,
response: {
......@@ -178,14 +203,19 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) {
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(
pushNotification({
type: AppNotificationType.WalletConnect,
event: WalletConnectEvent.TransactionFailed,
dappName: params.dapp.name,
imageUrl: params.dapp.icon ?? null,
dappName: params.dappRequestInfo.name,
imageUrl: params.dappRequestInfo.icon ?? null,
chainId,
address: account.address,
}),
......
......@@ -12,6 +12,7 @@ import {
} from 'src/features/walletConnect/utils'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { EthMethod } from 'uniswap/src/features/dappRequests/types'
import { DappRequestType } from 'uniswap/src/types/walletConnect'
const EIP155_MAINNET = 'eip155:1'
const EIP155_POLYGON = 'eip155:137'
......@@ -118,11 +119,12 @@ describe(parseGetCapabilitiesRequest, () => {
internalId: String(mockInternalId),
account: TEST_ADDRESS,
chainIds: undefined,
dapp: {
isLinkModeSupported: false,
dappRequestInfo: {
name: mockDapp.name,
url: mockDapp.url,
icon: mockDapp.icons[0],
source: 'walletconnect',
requestType: DappRequestType.WalletConnectSessionRequest,
},
})
})
......@@ -140,11 +142,12 @@ describe(parseGetCapabilitiesRequest, () => {
internalId: String(mockInternalId),
account: TEST_ADDRESS,
chainIds,
dapp: {
isLinkModeSupported: false,
dappRequestInfo: {
name: mockDapp.name,
url: mockDapp.url,
icon: mockDapp.icons[0],
source: 'walletconnect',
requestType: DappRequestType.WalletConnectSessionRequest,
},
})
})
......@@ -175,11 +178,12 @@ describe(parseSignRequest, () => {
chainId: mockChainId,
rawMessage: message,
message: 'Hello World',
dapp: {
isLinkModeSupported: false,
dappRequestInfo: {
name: mockDapp.name,
url: mockDapp.url,
icon: mockDapp.icons[0],
source: 'walletconnect',
requestType: DappRequestType.WalletConnectSessionRequest,
},
})
})
......@@ -198,11 +202,12 @@ describe(parseSignRequest, () => {
chainId: mockChainId,
rawMessage: message,
message: 'Hello World',
dapp: {
isLinkModeSupported: false,
dappRequestInfo: {
name: mockDapp.name,
url: mockDapp.url,
icon: mockDapp.icons[0],
source: 'walletconnect',
requestType: DappRequestType.WalletConnectSessionRequest,
},
})
})
......@@ -221,11 +226,12 @@ describe(parseSignRequest, () => {
chainId: mockChainId,
rawMessage: typedData,
message: null,
dapp: {
isLinkModeSupported: false,
dappRequestInfo: {
name: mockDapp.name,
url: mockDapp.url,
icon: mockDapp.icons[0],
source: 'walletconnect',
requestType: DappRequestType.WalletConnectSessionRequest,
},
})
})
......@@ -268,6 +274,7 @@ describe(parseTransactionRequest, () => {
internalId: String(mockInternalId),
account: TEST_ADDRESS,
chainId: mockChainId,
isLinkModeSupported: false,
transaction: {
from: TEST_ADDRESS,
to: '0x1234567890123456789012345678901234567890',
......@@ -276,11 +283,11 @@ describe(parseTransactionRequest, () => {
value: '0x0',
// gasPrice and nonce should be omitted
},
dapp: {
dappRequestInfo: {
name: mockDapp.name,
url: mockDapp.url,
icon: mockDapp.icons[0],
source: 'walletconnect',
requestType: DappRequestType.WalletConnectSessionRequest,
},
})
})
......@@ -336,11 +343,12 @@ describe(parseSendCallsRequest, () => {
capabilities: sendCallsParams.capabilities,
id: sendCallsParams.id,
version: sendCallsParams.version,
dapp: {
isLinkModeSupported: false,
dappRequestInfo: {
name: mockDapp.name,
url: mockDapp.url,
icon: mockDapp.icons[0],
source: 'walletconnect',
requestType: DappRequestType.WalletConnectSessionRequest,
},
})
})
......@@ -397,11 +405,12 @@ describe(parseGetCallsStatusRequest, () => {
account,
chainId: mockChainId,
id: requestId,
dapp: {
isLinkModeSupported: false,
dappRequestInfo: {
name: mockDapp.name,
url: mockDapp.url,
icon: mockDapp.icons[0],
source: 'walletconnect',
requestType: DappRequestType.WalletConnectSessionRequest,
},
})
})
......
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 { wcWeb3Wallet } from 'src/features/walletConnect/walletConnectClient'
import {
SignRequest,
TransactionRequest,
WalletConnectVerifyStatus,
WalletGetCallsStatusRequest,
WalletGetCapabilitiesRequest,
WalletSendCallsRequest,
......@@ -12,6 +13,7 @@ import {
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { toSupportedChainId } from 'uniswap/src/features/chains/utils'
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 { GetCallsStatusParams, SendCallsParams } from 'wallet/src/features/dappRequests/types'
/**
......@@ -98,23 +100,20 @@ function createBaseRequest<T extends WalletConnectEthMethod>(
sessionId: string
internalId: string
account: Address
dapp: {
name: string
url: string
icon: string | null
source: 'walletconnect'
}
isLinkModeSupported: boolean
dappRequestInfo: DappRequestInfo
} {
return {
type: method,
sessionId: topic,
internalId: String(internalId),
account,
dapp: {
isLinkModeSupported: Boolean(dapp.redirect?.linkMode),
dappRequestInfo: {
name: dapp.name,
url: dapp.url,
icon: dapp.icons[0] ?? null,
source: 'walletconnect',
requestType: DappRequestType.WalletConnectSessionRequest,
},
}
}
......@@ -295,3 +294,32 @@ export async function pairWithWalletConnectURI(uri: string): Promise<void | Pair
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> {
icons: ['https://gateway.pinata.cloud/ipfs/QmR1hYqhDMoyvJtwrQ6f1kVyfEKyK65XH3nbCimXBMkHJg'],
redirect: {
native: 'uniswap://',
universal: 'https://uniswap.org/app',
linkMode: true,
},
},
})
......
......@@ -2,20 +2,27 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { ProposalTypes, SessionTypes } from '@walletconnect/types'
import { UniverseChainId } from 'uniswap/src/features/chains/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'
export enum WalletConnectVerifyStatus {
Verified = 'VERIFIED',
Unverified = 'UNVERIFIED',
Threat = 'THREAT',
}
export type WalletConnectPendingSession = {
id: string
chains: UniverseChainId[]
dapp: DappInfo
dappRequestInfo: DappRequestInfo
proposalNamespaces: ProposalTypes.RequiredNamespaces
verifyStatus: WalletConnectVerifyStatus
}
export type WalletConnectSession = {
id: string
chains: UniverseChainId[]
dapp: DappInfo
dappRequestInfo: DappRequestInfo
namespaces: SessionTypes.Namespaces
}
......@@ -27,8 +34,9 @@ interface BaseRequest {
sessionId: string
internalId: string
account: string
dapp: DappInfo
dappRequestInfo: DappRequestInfo
chainId: UniverseChainId
isLinkModeSupported?: boolean
}
export interface SignRequest extends BaseRequest {
......
......@@ -145,7 +145,7 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element |
}
await dispatchAddTransaction({ isOffRamp })
await dispatch(forceFetchFiatOnRampTransactions())
openUri(widgetUrl).catch(onError)
openUri(widgetUrl, false, false, undefined, true).catch(onError)
}
if (!isOffRamp && timeoutElapsed && !widgetLoading && widgetData) {
......
......@@ -16,10 +16,12 @@ import { Flex, Loader } from 'ui/src'
import { DownloadAlt, OSDynamicCloudIcon } from 'ui/src/components/icons'
import { imageSizes } from 'ui/src/theme'
import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard'
import { config } from 'uniswap/src/config'
import { ImportType } from 'uniswap/src/types/onboarding'
import { OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName'
import { logger } from 'utilities/src/logger/logger'
import { isAndroid } from 'utilities/src/platform'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { useSignerAccounts } from 'wallet/src/features/wallet/hooks'
......@@ -27,7 +29,13 @@ type Props = NativeStackScreenProps<OnboardingStackParamList, OnboardingScreens.
const MIN_LOADING_UI_MS = ONE_SECOND_MS
// 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 {
const { t } = useTranslation()
......@@ -55,6 +63,10 @@ export function RestoreCloudBackupLoadingScreen({ navigation, route: { params }
// delays native oauth consent screen to avoid UI freezes
setTimeout(async () => {
try {
if (ANDROID_E2E_WORKAROUND) {
setIsError(false)
return
}
await startFetchingCloudStorageBackups()
} catch (e) {
setIsError(true)
......@@ -150,6 +162,7 @@ export function RestoreCloudBackupLoadingScreen({ navigation, route: { params }
if (isLoading === false && backups.length === 0) {
if (isRestoringMnemonic) {
navigation.replace(OnboardingScreens.SeedPhraseInput, {
showAsCloudBackupFallback: true,
importType,
entryPoint,
})
......
......@@ -7,21 +7,21 @@ import { useCloudBackups } from 'src/features/CloudBackup/hooks'
import { CloudStorageMnemonicBackup } from 'src/features/CloudBackup/types'
import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen'
import { useNavigationHeader } from 'src/utils/useNavigationHeader'
import { Flex, Text, TouchableArea, Unicon, useIsDarkMode } from 'ui/src'
import { DownloadAlt, RotatableChevron } from 'ui/src/components/icons'
import { Flex, Text, TouchableArea, useIsDarkMode } from 'ui/src'
import { DownloadAlt, RotatableChevron, Unitag } from 'ui/src/components/icons'
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 { OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { sanitizeAddressText } from 'uniswap/src/utils/addresses'
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>
export function RestoreCloudBackupScreen({ navigation, route: { params } }: Props): JSX.Element {
const { t } = useTranslation()
const isDarkMode = useIsDarkMode()
const localizedDayjs = useLocalizedDayjs()
const backups = useCloudBackups()
const sortedBackups = backups.slice().sort((a, b) => b.createdAt - a.createdAt)
......@@ -44,44 +44,61 @@ export function RestoreCloudBackupScreen({ navigation, route: { params } }: Prop
>
<ScrollView>
<Flex gap="$spacing8">
{sortedBackups.map((backup) => {
const { mnemonicId, createdAt } = backup
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>
)
})}
{sortedBackups.map((backup) => (
<BackupListItem key={backup.mnemonicId} backup={backup} onPressRestoreBackup={onPressRestoreBackup} />
))}
</Flex>
</ScrollView>
</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 {
NativeSeedPhraseInputProps,
NativeSeedPhraseInputRef,
} from 'src/screens/Import/SeedPhraseInputScreen/SeedPhraseInput/types'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { isAndroid } from 'utilities/src/platform'
......@@ -68,9 +69,10 @@ export const SeedPhraseInput = forwardRef<NativeSeedPhraseInputRef, SeedPhraseIn
return (
<NativeSeedPhraseInput
key={key}
// @ts-expect-error - TODO: figure out how to properly type the ref of a custom native component
ref={inputRef}
key={key}
testID={TestID.NativeSeedPhraseInput}
style={calculatedStyle}
onHeightMeasured={handleOnHeightMeasured}
{...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