ci(release): publish latest release

parent e8cdfff8
......@@ -21,6 +21,7 @@ An open source repository for all Uniswap front end interfaces maintained by Uni
## Whitepapers
- [V4](https://uniswap.org/whitepaper-v4.pdf)
- [V3](https://uniswap.org/whitepaper-v3.pdf)
- [V2](https://uniswap.org/whitepaper.pdf)
- [V1](https://hackmd.io/C-DvwDSfSxuh-Gd4WKE_ig)
......
IPFS hash of the deployment:
- CIDv0: `QmRHsud7WNRFUg4cTcXRxuwZYZRAhYMkYAcA97mtqwq9re`
- CIDv1: `bafybeibl3u67jwwr3vxssx7z6bbz35bk333qe6xdnkruce7pmma5uf2z74`
- CIDv0: `QmXkPV7sagZDwbUR5PRBNWAvFrJSuTUh5Fu1aJpFA1whLV`
- CIDv1: `bafybeielzxuhhhlrf7rtyfrqtzqawka7oyjb3qklvfvcexvwdumpsnujdi`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
......@@ -10,15 +10,61 @@ You can also access the Uniswap Interface from an IPFS gateway.
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://bafybeibl3u67jwwr3vxssx7z6bbz35bk333qe6xdnkruce7pmma5uf2z74.ipfs.dweb.link/
- https://bafybeibl3u67jwwr3vxssx7z6bbz35bk333qe6xdnkruce7pmma5uf2z74.ipfs.cf-ipfs.com/
- [ipfs://QmRHsud7WNRFUg4cTcXRxuwZYZRAhYMkYAcA97mtqwq9re/](ipfs://QmRHsud7WNRFUg4cTcXRxuwZYZRAhYMkYAcA97mtqwq9re/)
- https://bafybeielzxuhhhlrf7rtyfrqtzqawka7oyjb3qklvfvcexvwdumpsnujdi.ipfs.dweb.link/
- https://bafybeielzxuhhhlrf7rtyfrqtzqawka7oyjb3qklvfvcexvwdumpsnujdi.ipfs.cf-ipfs.com/
- [ipfs://QmXkPV7sagZDwbUR5PRBNWAvFrJSuTUh5Fu1aJpFA1whLV/](ipfs://QmXkPV7sagZDwbUR5PRBNWAvFrJSuTUh5Fu1aJpFA1whLV/)
### 5.64.2 (2025-01-13)
## 5.65.0 (2025-01-15)
### Features
* **web:** [EW] Add passkey sign in (#13883) 01fd45c
* **web:** [EW] Add show recovery phrase modal (#13894) 05748a7
* **web:** [EW] Add sign up (#13862) aa9771e
* **web:** [EW] Add signing txs (#13890) c779f0c
* **web:** add the v4 whitepaper (#14867) e87e686
* **web:** delete unused background updating code (#14612) aa26058
* **web:** replace usage of DoubleLogoWithChain with DoubleLogo (#15011) e723da8
* **web:** update uniswapx-sdk (#14654) ab645c7
* **web:** use dynamic config for blocked nft collections (#14643) a748576
### Bug Fixes
* **web:** fix infinite loop on analytics toggle 3a2b4dc
* **web:** 5859 get position owner from get position call (#14729) b412b95
* **web:** align chart headers in explore (#14830) 4a2e838
* **web:** consistent lp buttons (#14664) 4c3c0d9
* **web:** eth logo flash (#15013) 486ab05
* **web:** filter toggle behavior on TDP (#14898) dbebd9a
* **web:** fix crash on v4 create (#14891) 53b84d8
* **web:** fix double logo on DoubleCurrencyAndChainLogo (#14946) 0579dcf
* **web:** fix infinite loop on analytics toggle f4b5fde
* **web:** fix infinite loop on analytics toggle (#15068) ad1a014
* **web:** fix pdp price display (#14902) 2f00216
* **web:** fix range display for v4 positions in custom fee tiers (#14904) 3be2db0
* **web:** hero title bug when switching languages (#15022) d6511f5
* **web:** inverted initial price bug (#14994) 017e47e
* **web:** limit orders do not work with uniswapx v2 (#14995) b492069
* **web:** make the swap primary colors on the tdp use token extraction colors (#14696) ea7f43b
* **web:** move both price toggles together in the create flow (#14971) 8d8f595
* **web:** re-add images prop to PortfolioLogo and use split logo (#14864) 840edff
* **web:** reduce size impact of ui icons (#14613) f89a87e
* **web:** remove direct t imports and fix translations outside of React context on web (#14829) e040834
* **web:** remove worldchain minikit provider (#14544) 6794856
* **web:** scroll on nav dropdown and adjust max height (#15094) 3bbd575
* **web:** send speed bump font size fix (#15004) 0bd82df
* **web:** update google conversion datetime format (#15131) 6a7ca9a
* **web:** update input behavior on send tab (#14843) 8c4eab7
### Continuous Integration
* **web:** update sitemaps 8e5f49e
### Styles
* **web:** round ends of loading spinner component (#14779) 26b3a8b
web/5.64.2
\ No newline at end of file
web/5.65.0
\ No newline at end of file
......@@ -16,7 +16,7 @@
"@tamagui/core": "1.114.4",
"@types/uuid": "9.0.1",
"@uniswap/analytics-events": "2.40.0",
"@uniswap/uniswapx-sdk": "2.1.0-beta.18",
"@uniswap/uniswapx-sdk": "3.0.0-beta.1",
"@uniswap/universal-router-sdk": "4.7.0",
"@uniswap/v3-sdk": "3.19.0",
"@uniswap/v4-sdk": "1.12.0",
......@@ -91,8 +91,8 @@
"build:production": "webpack --node-env=production --env BUILD_ENV=prod BUILD_NUM=${BUILD_NUM:-0}",
"check:circular": "concurrently \"../../scripts/check-circular-imports.sh ./src/entry/sidebar.tsx 1\" \"../../scripts/check-circular-imports.sh ./src/entry/onboarding.tsx 1\" \"../../scripts/check-circular-imports.sh ./src/entry/unitagClaim.tsx 1\"",
"check:deps:usage": "depcheck",
"env:local:download": "bash ../../scripts/downloadEnvLocal.sh web-local-envs ../../.env",
"env:local:upload": "bash ../../scripts/uploadEnvLocal.sh web-local-envs ../../.env",
"env:local:download": "bash ../../scripts/downloadEnvLocal.sh m4dhqfltt3dokkqi3hqwigmf2a ../../.env",
"env:local:upload": "bash ../../scripts/uploadEnvLocal.sh m4dhqfltt3dokkqi3hqwigmf2a ../../.env",
"format": "../../scripts/prettier.sh",
"lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --ext ts,tsx --max-warnings=0",
"lint:fix": "NODE_OPTIONS=--max-old-space-size=8192 eslint . --ext ts,tsx --fix",
......
......@@ -14,7 +14,7 @@ import { DappContextProvider } from 'src/app/features/dapp/DappContext'
import { SentryAppNameTag, initializeSentry, sentryCreateHashRouter } from 'src/app/sentry'
import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { getReduxPersistor, getReduxStore } from 'src/store/store'
import { Button, Flex, Image, Text } from 'ui/src'
import { DeprecatedButton, Flex, Image, Text } from 'ui/src'
import { CHROME_LOGO, UNISWAP_LOGO } from 'ui/src/assets'
import { iconSizes, spacing } from 'ui/src/theme'
import { BlankUrlProvider } from 'uniswap/src/contexts/UrlContext'
......@@ -98,7 +98,7 @@ function PopupContent(): JSX.Element {
<Flex fill />
<Trace logPress element={ElementName.ExtensionPopupOpenButton}>
<Button
<DeprecatedButton
theme="primary"
width="100%"
onPress={async () => {
......@@ -110,7 +110,7 @@ function PopupContent(): JSX.Element {
}}
>
{t('extension.popup.chrome.button')}
</Button>
</DeprecatedButton>
</Trace>
</Flex>
</Trace>
......
......@@ -2,7 +2,7 @@ import { forwardRef } from 'react'
import { useTranslation } from 'react-i18next'
import { TextInput } from 'react-native'
import { Input, InputProps } from 'src/app/components/Input'
import { Button, Flex, FlexProps, IconProps, Text } from 'ui/src'
import { DeprecatedButton, Flex, FlexProps, IconProps, Text } from 'ui/src'
import { Eye, EyeOff } from 'ui/src/components/icons'
import { PasswordStrength, getPasswordStrengthTextAndColor } from 'wallet/src/utils/password'
......@@ -34,7 +34,7 @@ export const PasswordInput = forwardRef<TextInput, PasswordInputProps>(function
<StrengthIndicator strength={passwordStrength} />
) : (
onToggleHideInput && (
<Button
<DeprecatedButton
backgroundColor="$transparent"
hoverStyle={hoverStyle}
position="absolute"
......@@ -43,7 +43,7 @@ export const PasswordInput = forwardRef<TextInput, PasswordInputProps>(function
onPress={(): void => onToggleHideInput(!hideInput)}
>
{hideInput ? <Eye {...iconProps} /> : <EyeOff {...iconProps} />}
</Button>
</DeprecatedButton>
)
)}
</Flex>
......
import { ReactNode } from 'react'
import { Anchor, Button, Flex, Text, TouchableArea, useSporeColors } from 'ui/src'
import { Anchor, DeprecatedButton, Flex, Text, TouchableArea, useSporeColors } from 'ui/src'
import { X } from 'ui/src/components/icons'
import { zIndices } from 'ui/src/theme'
import { Modal } from 'uniswap/src/components/modals/Modal'
......@@ -60,9 +60,9 @@ export function InfoModal({
{description}
</Text>
</Flex>
<Button size="medium" theme={buttonTheme} width="100%" onPress={onButtonPress}>
<DeprecatedButton size="medium" theme={buttonTheme} width="100%" onPress={onButtonPress}>
{buttonText}
</Button>
</DeprecatedButton>
{linkText && linkUrl && (
<Anchor href={linkUrl} lineHeight={16} p="$spacing12" target="_blank" textDecorationLine="none">
<Text color="$neutral2" textAlign="center" variant="buttonLabel3">
......
......@@ -15,7 +15,7 @@ import { PopupName, openPopup } from 'src/app/features/popups/slice'
import { AppRoutes, RemoveRecoveryPhraseRoutes, SettingsRoutes, UnitagClaimRoutes } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state'
import { focusOrCreateUnitagTab } from 'src/app/navigation/utils'
import { Button, Flex, Popover, ScrollView, Text, useSporeColors } from 'ui/src'
import { DeprecatedButton, Flex, Popover, ScrollView, Text, useSporeColors } from 'ui/src'
import { WalletFilled, X } from 'ui/src/components/icons'
import { spacing } from 'ui/src/theme'
import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal'
......@@ -201,14 +201,14 @@ export function AccountSwitcherScreen(): JSX.Element {
{activeAccountHasUnitag ? (
<UnitagActionButton />
) : (
<Button
<DeprecatedButton
size="small"
testID={TestID.AccountCard}
theme="secondary"
onPress={() => setShowEditLabelModal(true)}
>
{t('account.wallet.header.button.title')}
</Button>
</DeprecatedButton>
)}
</Flex>
<ScrollView backgroundColor="$surface1" height="auto">
......@@ -291,17 +291,23 @@ const UnitagActionButton = (): JSX.Element => {
if (isClaimUnitagEnabled) {
return (
<Button color="$neutral1" size="small" testID={TestID.AccountCard} theme="tertiary" onPress={onPressEditProfile}>
<DeprecatedButton
color="$neutral1"
size="small"
testID={TestID.AccountCard}
theme="tertiary"
onPress={onPressEditProfile}
>
{t('account.wallet.header.button.disabled.title')}
</Button>
</DeprecatedButton>
)
}
return (
<ComingSoon placement="top">
<Button color="$neutral2" disabled={true} size="small" testID={TestID.AccountCard} theme="secondary">
<DeprecatedButton color="$neutral2" disabled={true} size="small" testID={TestID.AccountCard} theme="secondary">
{t('account.wallet.header.button.disabled.title')}
</Button>
</DeprecatedButton>
</ComingSoon>
)
}
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { OpaqueColorValue } from 'react-native'
import { Button, Flex, Text, getUniconColors, useIsDarkMode } from 'ui/src'
import { DeprecatedButton, Flex, Text, getUniconColors, useIsDarkMode } from 'ui/src'
import { iconSizes, opacify } from 'ui/src/theme'
import { TextInput } from 'uniswap/src/components/input/TextInput'
import { Modal } from 'uniswap/src/components/modals/Modal'
......@@ -44,7 +44,7 @@ export function CreateWalletModal({
? getUniconColors(onboardingAccountAddress, isDark)
: { color: '' }
// Cast because Button component doesnt acccept sytling outside of theme color values for hover and press states
// Cast because DeprecatedButton component doesnt acccept sytling outside of theme color values for hover and press states
const hoverAndPressButtonStyle = useMemo(() => {
return {
backgroundColor: opacify(15, uniconColor) as unknown as OpaqueColorValue,
......@@ -76,10 +76,10 @@ export function CreateWalletModal({
</Flex>
<Flex centered fill row gap="$spacing12" justifyContent="space-between" width="100%">
<Button color="$neutral1" flex={1} flexBasis={1} size="small" theme="secondary" onPress={onCancel}>
<DeprecatedButton color="$neutral1" flex={1} flexBasis={1} size="small" theme="secondary" onPress={onCancel}>
{t('common.button.cancel')}
</Button>
<Button
</DeprecatedButton>
<DeprecatedButton
flex={1}
flexBasis={1}
hoverStyle={hoverAndPressButtonStyle}
......@@ -89,7 +89,7 @@ export function CreateWalletModal({
onPress={onPressConfirm}
>
{t('common.button.create')}
</Button>
</DeprecatedButton>
</Flex>
</Flex>
</Modal>
......
......@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import { UnitagClaimRoutes } from 'src/app/navigation/constants'
import { focusOrCreateUnitagTab } from 'src/app/navigation/utils'
import { Button, Flex, Text } from 'ui/src'
import { DeprecatedButton, Flex, Text } from 'ui/src'
import { Person } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
import { TextInput } from 'uniswap/src/components/input/TextInput'
......@@ -100,12 +100,12 @@ export function EditLabelModal({ isOpen, address, onClose }: EditLabelModalProps
</Text>
</Flex>
<Flex centered fill row gap="$spacing12" justifyContent="space-between" width="100%">
<Button color="$neutral1" flex={1} flexBasis={1} size="small" theme="secondary" onPress={onClose}>
<DeprecatedButton color="$neutral1" flex={1} flexBasis={1} size="small" theme="secondary" onPress={onClose}>
{t('common.button.cancel')}
</Button>
<Button flex={1} flexBasis={1} size="small" theme="accentSecondary" onPress={onConfirm}>
</DeprecatedButton>
<DeprecatedButton flex={1} flexBasis={1} size="small" theme="accentSecondary" onPress={onConfirm}>
{t('common.button.save')}
</Button>
</DeprecatedButton>
</Flex>
</Flex>
</Modal>
......
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { Button, Flex, Text, TouchableArea } from 'ui/src'
import { DeprecatedButton, Flex, Text, TouchableArea } from 'ui/src'
import { Feedback, LikeSquare, MessageText, X } from 'ui/src/components/icons'
import { IconSizeTokens, zIndices } from 'ui/src/theme'
import { Modal } from 'uniswap/src/components/modals/Modal'
......@@ -133,12 +133,12 @@ export default function AppRatingModal({ onClose }: AppRatingModalProps): JSX.El
</Text>
</Flex>
<Flex row width="100%" gap="$spacing12">
<Button flex={1} flexBasis={1} size="small" theme="secondary" onPress={onSecondaryButtonPress}>
<DeprecatedButton flex={1} flexBasis={1} size="small" theme="secondary" onPress={onSecondaryButtonPress}>
{secondaryButtonText}
</Button>
<Button flex={1} flexBasis={1} size="small" theme="primary" onPress={onPrimaryButtonPress}>
</DeprecatedButton>
<DeprecatedButton flex={1} flexBasis={1} size="small" theme="primary" onPress={onPrimaryButtonPress}>
{primaryButtonText}
</Button>
</DeprecatedButton>
</Flex>
</Flex>
</Modal>
......
......@@ -4,7 +4,16 @@ import { useDappLastChainId } from 'src/app/features/dapp/hooks'
import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext'
import { DappRequestStoreItem } from 'src/app/features/dappRequests/slice'
import { DappRequestType } from 'src/app/features/dappRequests/types/DappRequestTypes'
import { Anchor, AnimatePresence, Button, Flex, Text, UniversalImage, UniversalImageResizeMode, styled } from 'ui/src'
import {
Anchor,
AnimatePresence,
DeprecatedButton,
Flex,
Text,
UniversalImage,
UniversalImageResizeMode,
styled,
} from 'ui/src'
import { borderRadii, iconSizes } from 'ui/src/theme'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
......@@ -240,10 +249,10 @@ export function DappRequestFooter({
px="$spacing8"
/>
<Flex row gap="$spacing12" pt="$spacing8">
<Button flex={1} flexBasis={1} size="medium" theme="secondary" onPress={handleOnCancel}>
<DeprecatedButton flex={1} flexBasis={1} size="medium" theme="secondary" onPress={handleOnCancel}>
{t('common.button.cancel')}
</Button>
<Button
</DeprecatedButton>
<DeprecatedButton
disabled={!isConfirmEnabled}
flex={1}
flexBasis={1}
......@@ -252,7 +261,7 @@ export function DappRequestFooter({
onPress={handleOnConfirm}
>
{confirmText}
</Button>
</DeprecatedButton>
</Flex>
</Flex>
</>
......
......@@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent'
import { SignMessageRequest } from 'src/app/features/dappRequests/types/DappRequestTypes'
import { Button, Flex, Text, Tooltip } from 'ui/src'
import { DeprecatedButton, Flex, Text, Tooltip } from 'ui/src'
import { AlertTriangleFilled, Code, StickyNoteTextSquare } from 'ui/src/components/icons'
import { containsNonPrintableChars } from 'utilities/src/primitives/string'
......@@ -86,7 +86,7 @@ export function PersonalSignRequestContent({ dappRequest }: PersonalSignRequestP
right={isScrollable ? '$spacing24' : '$spacing12'}
>
<Tooltip.Trigger>
<Button
<DeprecatedButton
borderColor="$surface3"
borderRadius="$rounded4"
icon={
......
import { useTranslation } from 'react-i18next'
import { ScreenHeader } from 'src/app/components/layout/ScreenHeader'
import { SeedPhraseDisplay } from 'src/app/features/settings/SettingsRecoveryPhraseScreen/SeedPhraseDisplay'
import { Flex, Text } from 'ui/src'
import { GraduationCap } from 'ui/src/components/icons/GraduationCap'
import { ForceUpgrade } from 'wallet/src/features/forceUpgrade/ForceUpgrade'
function SeedPhraseModalContent({ mnemonicId, onDismiss }: { mnemonicId: string; onDismiss: () => void }): JSX.Element {
const { t } = useTranslation()
return (
<Flex fill gap="$spacing24" py="$spacing8">
<ScreenHeader title={t('forceUpgrade.label.recoveryPhrase')} onBackClick={onDismiss} />
<Flex gap="$spacing16">
<SeedPhraseDisplay mnemonicId={mnemonicId} />
<Flex
row
alignItems="center"
backgroundColor="$surface2"
borderRadius="$rounded16"
gap="$spacing8"
p="$spacing12"
>
<Flex>
<GraduationCap color="$neutral2" size="$icon.20" />
</Flex>
<Flex shrink>
<Text color="$neutral2" variant="body4">
{t('onboarding.backup.manual.banner')}
</Text>
</Flex>
</Flex>
</Flex>
</Flex>
)
}
export function ForceUpgradeModal(): JSX.Element {
return <ForceUpgrade SeedPhraseModalContent={SeedPhraseModalContent} />
}
......@@ -7,6 +7,7 @@ import { ActivityTab } from 'src/app/components/tabs/ActivityTab'
import { NftsTab } from 'src/app/components/tabs/NftsTab'
import AppRatingModal from 'src/app/features/appRating/AppRatingModal'
import { useAppRating } from 'src/app/features/appRating/hooks/useAppRating'
import { ForceUpgradeModal } from 'src/app/features/forceUpgrade/ForceUpgradeModal'
import { PortfolioActionButtons } from 'src/app/features/home/PortfolioActionButtons'
import { PortfolioHeader } from 'src/app/features/home/PortfolioHeader'
import { TokenBalanceList } from 'src/app/features/home/TokenBalanceList'
......@@ -198,6 +199,7 @@ export const HomeScreen = memo(function _HomeScreen(): JSX.Element {
</Text>
)}
{appRatingModalVisible && <AppRatingModal onClose={onAppRatingModalClose} />}
<ForceUpgradeModal />
</Flex>
)
})
......
......@@ -4,7 +4,7 @@ import { useDappContext } from 'src/app/features/dapp/DappContext'
import { removeDappConnection, saveDappChain } from 'src/app/features/dapp/actions'
import { useDappLastChainId } from 'src/app/features/dapp/hooks'
import { PopupName, closePopup } from 'src/app/features/popups/slice'
import { Anchor, Button, Flex, Popover, Separator, Text, getTokenValue } from 'ui/src'
import { Anchor, DeprecatedButton, Flex, Popover, Separator, Text, getTokenValue } from 'ui/src'
import { Check, Power } from 'ui/src/components/icons'
import { usePreventOverflowBelowFold } from 'ui/src/hooks/usePreventOverflowBelowFold'
import { iconSizes } from 'ui/src/theme'
......@@ -76,7 +76,7 @@ export function SwitchNetworksModal(): JSX.Element {
{enabledChains.map((chain: UniverseChainId) => {
return (
<Popover.Close asChild>
<Button
<DeprecatedButton
key={chain}
borderRadius="$rounded12"
justifyContent="space-between"
......@@ -98,14 +98,14 @@ export function SwitchNetworksModal(): JSX.Element {
</Flex>
) : null}
</Flex>
</Button>
</DeprecatedButton>
</Popover.Close>
)
})}
</Flex>
<Popover.Close asChild>
<Button mt="$spacing8" size="small" theme="tertiary" onPress={onDisconnect}>
<DeprecatedButton mt="$spacing8" size="small" theme="tertiary" onPress={onDisconnect}>
<Flex centered row gap="$spacing8">
<Power color="$neutral1" size={getTokenValue('$icon.16')} />
{/* TODO(EXT-207 / EXT-208): fix button component styling and derive text color from theme */}{' '}
......@@ -113,7 +113,7 @@ export function SwitchNetworksModal(): JSX.Element {
{t('common.button.disconnect')}
</Text>
</Flex>
</Button>
</DeprecatedButton>
</Popover.Close>
</Flex>
)
......
import { SharedEventName } from '@uniswap/analytics-events'
import { PropsWithChildren, memo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useInterfaceBuyNavigator } from 'src/app/features/for/utils'
......@@ -10,13 +9,11 @@ import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard'
import { InfoLinkModal } from 'uniswap/src/components/modals/InfoLinkModal'
import { uniswapUrls } from 'uniswap/src/constants/urls'
import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { PortfolioBalance } from 'uniswap/src/features/dataApi/types'
import { ElementName, ModalName, SectionName, WalletEventName } from 'uniswap/src/features/telemetry/constants'
import { ElementName, ModalName, WalletEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { InformationBanner } from 'wallet/src/components/banners/InformationBanner'
import { ContextMenu } from 'wallet/src/components/menu/ContextMenu'
import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext'
import { isNonPollingRequestInFlight } from 'wallet/src/data/utils'
import { HiddenTokensRow } from 'wallet/src/features/portfolio/HiddenTokensRow'
import { PortfolioEmptyState } from 'wallet/src/features/portfolio/PortfolioEmptyState'
......@@ -36,25 +33,9 @@ type TokenBalanceListProps = {
}
export const TokenBalanceList = memo(function _TokenBalanceList({ owner }: TokenBalanceListProps): JSX.Element {
const { navigateToTokenDetails } = useWalletNavigation()
const { isTestnetModeEnabled } = useEnabledChains()
const onPressToken = (currencyId: string): void => {
if (isTestnetModeEnabled) {
return
}
sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, {
element: ElementName.TokenItem,
section: SectionName.HomeTokensTab,
})
navigateToTokenDetails(currencyId)
}
return (
<Flex grow>
<TokenBalanceListContextProvider isExternalProfile={false} owner={owner} onPressToken={onPressToken}>
<TokenBalanceListContextProvider isExternalProfile={false} owner={owner} onPressToken={() => {}}>
<TokenBalanceListInner />
</TokenBalanceListContextProvider>
</Flex>
......@@ -246,7 +227,13 @@ function TokenContextMenu({
const itemId = `${portfolioBalance.currencyInfo.currencyId}-${portfolioBalance.isHidden}`
return (
<ContextMenu itemId={itemId} menuOptions={menuOptions} menuStyleProps={{ minWidth: MIN_CONTEXT_MENU_WIDTH }}>
<ContextMenu
closeOnClick
itemId={itemId}
menuOptions={menuOptions}
menuStyleProps={{ minWidth: MIN_CONTEXT_MENU_WIDTH }}
onLeftClick
>
{children}
</ContextMenu>
)
......
......@@ -7,7 +7,7 @@ import { InfoModal, ModalProps } from 'src/app/components/modal/InfoModal'
import { useSagaStatus } from 'src/app/hooks/useSagaStatus'
import { OnboardingRoutes, TopLevelRoutes } from 'src/app/navigation/constants'
import { focusOrCreateOnboardingTab } from 'src/app/navigation/utils'
import { Button, Flex, InputProps, Text, TouchableArea } from 'ui/src'
import { DeprecatedButton, Flex, InputProps, Text, TouchableArea } from 'ui/src'
import { AlertTriangleFilled, Lock } from 'ui/src/components/icons'
import { spacing, zIndices } from 'ui/src/theme'
import { uniswapUrls } from 'uniswap/src/constants/urls'
......@@ -211,9 +211,9 @@ export function Locked(): JSX.Element {
</Flex>
<Flex gap="$spacing12" justifyContent="flex-end" zIndex={zIndices.sticky}>
<Button size="large" theme="primary" onPress={onPress}>
<DeprecatedButton size="large" theme="primary" onPress={onPress}>
{t('extension.lock.button.submit')}
</Button>
</DeprecatedButton>
<TouchableArea>
<Text
......
......@@ -8,7 +8,7 @@ import { onboardingMessageChannel } from 'src/background/messagePassing/messageC
import { OnboardingMessageType } from 'src/background/messagePassing/types/ExtensionMessages'
import { openSidePanel } from 'src/background/utils/chromeSidePanelUtils'
import { terminateStoreSynchronization } from 'src/store/storeSynchronization'
import { Button, Flex, Image, Text } from 'ui/src'
import { DeprecatedButton, Flex, Image, Text } from 'ui/src'
import { UNISWAP_LOGO } from 'ui/src/assets'
import { RightArrow } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
......@@ -92,7 +92,7 @@ export function Complete({
<KeyboardKey key={key.title} fontSize={key.fontSize} px={key.px} state={key.state} title={key.title} />
))}
</Flex>
<Button
<DeprecatedButton
iconAfter={openedSideBar ? <RightArrow /> : undefined}
size="large"
theme={openedSideBar ? 'primary' : 'secondary'}
......@@ -100,7 +100,7 @@ export function Complete({
onPress={openedSideBar ? handleOpenWebApp : handleOpenSidebar}
>
{openedSideBar ? t('onboarding.complete.go_to_uniswap') : t('onboarding.complete.button')}
</Button>
</DeprecatedButton>
</Flex>
</Flex>
</MainContentWrapper>
......
import { useTranslation } from 'react-i18next'
import { OnboardingScreenProps } from 'src/app/features/onboarding/OnboardingScreenProps'
import { Button, Flex, Text, TouchableArea } from 'ui/src'
import { DeprecatedButton, Flex, Text, TouchableArea } from 'ui/src'
import { BackArrow } from 'ui/src/components/icons'
import i18n from 'uniswap/src/i18n'
......@@ -85,7 +85,7 @@ export function OnboardingScreenFrame({
</Flex>
<Flex row gap="$spacing12" width="100%">
{Boolean(onSubmit) && nextButtonText && (
<Button
<DeprecatedButton
disabled={!nextButtonEnabled}
flexGrow={1}
icon={nextButtonIcon}
......@@ -93,7 +93,7 @@ export function OnboardingScreenFrame({
onPress={onSubmit}
>
{nextButtonText}
</Button>
</DeprecatedButton>
)}
</Flex>
</>
......
......@@ -13,7 +13,7 @@ import { useOnboardingSteps } from 'src/app/features/onboarding/OnboardingSteps'
import { SyncFromPhoneButton } from 'src/app/features/onboarding/SyncFromPhoneButton'
import { TopLevelRoutes } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state'
import { Button, Flex, FlexProps, Input, Square, Text, inputStyles } from 'ui/src'
import { DeprecatedButton, Flex, FlexProps, Input, Square, Text, inputStyles } from 'ui/src'
import { FileListLock, RotatableChevron } from 'ui/src/components/icons'
import { fonts, iconSizes } from 'ui/src/theme'
import Trace from 'uniswap/src/features/telemetry/Trace'
......@@ -247,7 +247,7 @@ export function ImportMnemonic(): JSX.Element {
),
)}
</Flex>
<Button
<DeprecatedButton
backgroundColor="$transparent"
gap="$spacing4"
hoverStyle={{ backgroundColor: 'transparent' } as FlexProps}
......@@ -262,7 +262,7 @@ export function ImportMnemonic(): JSX.Element {
: t('onboarding.importMnemonic.button.longPhrase')}
</Text>
<RotatableChevron color="$neutral3" direction={expanded ? 'up' : 'down'} width={iconSizes.icon20} />
</Button>
</DeprecatedButton>
</Flex>
</>
</OnboardingScreen>
......
......@@ -8,7 +8,7 @@ import { OnboardingRoutes, TopLevelRoutes } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state'
import { checksIfSupportsSidePanel } from 'src/app/utils/chrome'
import { isOnboardedSelector } from 'src/app/utils/isOnboardedSelector'
import { Button, Flex, Text } from 'ui/src'
import { DeprecatedButton, Flex, Text } from 'ui/src'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import Trace from 'uniswap/src/features/telemetry/Trace'
......@@ -43,7 +43,7 @@ export function IntroScreen(): JSX.Element {
>
<Flex gap="$spacing12" pb="$spacing16" pt="$spacing32">
<Flex backgroundColor="$surface1" borderRadius="$rounded16">
<Button
<DeprecatedButton
flexGrow={1}
theme="primary"
onPress={(): void =>
......@@ -53,15 +53,15 @@ export function IntroScreen(): JSX.Element {
}
>
{t('onboarding.landing.button.create')}
</Button>
</DeprecatedButton>
</Flex>
<Button
<DeprecatedButton
flexGrow={1}
theme="secondary"
onPress={(): void => navigate(`/${TopLevelRoutes.Onboarding}/${OnboardingRoutes.Import}`)}
>
{t('onboarding.intro.button.alreadyHave')}
</Button>
</DeprecatedButton>
</Flex>
<Flex row alignItems="center" gap="$spacing16" py="$spacing4">
<Flex fill backgroundColor="$surface3" height={1} />
......
......@@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import { useDappContext } from 'src/app/features/dapp/DappContext'
import { saveDappConnection } from 'src/app/features/dapp/actions'
import { Anchor, Button, Flex, Popover, Separator, Text, TouchableArea } from 'ui/src'
import { Anchor, DeprecatedButton, Flex, Popover, Separator, Text, TouchableArea } from 'ui/src'
import { X } from 'ui/src/components/icons'
import { uniswapUrls } from 'uniswap/src/constants/urls'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
......@@ -56,14 +56,14 @@ export function ConnectPopupContent({
{showConnectButton ? (
asPopover ? (
<Popover.Close onPress={onConnect}>
<Button mt="$spacing8" size="small" theme="tertiary">
<DeprecatedButton mt="$spacing8" size="small" theme="tertiary">
{t('common.button.connect')}
</Button>
</DeprecatedButton>
</Popover.Close>
) : (
<Button mt="$spacing8" size="small" theme="tertiary" onPress={onConnect}>
<DeprecatedButton mt="$spacing8" size="small" theme="tertiary" onPress={onConnect}>
{t('common.button.connect')}
</Button>
</DeprecatedButton>
)
) : (
<Link
......
import { useTranslation } from 'react-i18next'
import { Button, Flex, Text, isWeb } from 'ui/src'
import { DeprecatedButton, Flex, Text, isWeb } from 'ui/src'
import { WarningLabel } from 'uniswap/src/components/modals/WarningModal/types'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ElementName } from 'uniswap/src/features/telemetry/constants'
......@@ -35,7 +35,7 @@ export function ReviewButton({ onPress, disabled }: ReviewButtonProps): JSX.Elem
return (
<Flex gap="$spacing16">
<Trace logPress element={ElementName.SendReview}>
<Button
<DeprecatedButton
backgroundColor="$accent1"
disabled={disableReviewButton}
size={isWeb ? 'medium' : 'large'}
......@@ -45,7 +45,7 @@ export function ReviewButton({ onPress, disabled }: ReviewButtonProps): JSX.Elem
<Text color="white" variant="buttonLabel1">
{buttonText}
</Text>
</Button>
</DeprecatedButton>
</Trace>
</Flex>
)
......
import { useCallback } from 'react'
import { useCallback, useState } from 'react'
import { useSelector } from 'react-redux'
import { RecipientPanel } from 'src/app/features/send/SendFormScreen/RecipientPanel'
import { ReviewButton } from 'src/app/features/send/SendFormScreen/ReviewButton'
import { Flex, Separator, useSporeColors } from 'ui/src'
import { Modal } from 'uniswap/src/components/modals/Modal'
import { selectHasDismissedLowNetworkTokenWarning } from 'uniswap/src/features/behaviorHistory/selectors'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ModalName, SectionName } from 'uniswap/src/features/telemetry/constants'
import { ModalName, SectionName, WalletEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { InsufficientNativeTokenWarning } from 'uniswap/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning'
import {
TransactionScreen,
......@@ -14,6 +17,7 @@ import {
import { useUSDTokenUpdater } from 'uniswap/src/features/transactions/hooks/useUSDTokenUpdater'
import { BlockedAddressWarning } from 'uniswap/src/features/transactions/modals/BlockedAddressWarning'
import { useUSDCValue } from 'uniswap/src/features/transactions/swap/hooks/useUSDCPrice'
import { LowNativeBalanceModal } from 'uniswap/src/features/transactions/swap/modals/LowNativeBalanceModal'
import { useIsBlocked } from 'uniswap/src/features/trm/hooks'
import { CurrencyField } from 'uniswap/src/types/currency'
import { createTransactionId } from 'uniswap/src/utils/createTransactionId'
......@@ -28,6 +32,9 @@ import { useIsBlockedActiveAddress } from 'wallet/src/features/trm/hooks'
export function SendFormScreen(): JSX.Element {
const colors = useSporeColors()
const hasDismissedLowNetworkTokenWarning = useSelector(selectHasDismissedLowNetworkTokenWarning)
const [showMaxTransferModal, setShowMaxTransferModal] = useState(false)
const {
derivedSendInfo,
selectingCurrencyField,
......@@ -39,6 +46,7 @@ export function SendFormScreen(): JSX.Element {
recipient,
updateSendForm,
onSelectCurrency,
isMax,
} = useSendContext()
const { screen, setScreen } = useTransactionModalContext()
......@@ -83,12 +91,21 @@ export function SendFormScreen(): JSX.Element {
const isBlocked = isActiveBlocked || isRecipientBlocked
const isBlockedLoading = isActiveBlockedLoading || isRecipientBlockedLoading
const onPressReview = useCallback(() => {
const goToReview = useCallback(() => {
const txId = createTransactionId()
updateSendForm({ txId })
setScreen(TransactionScreen.Review)
}, [setScreen, updateSendForm])
const onPressReview = useCallback(() => {
if (!hasDismissedLowNetworkTokenWarning && isMax && currencyInInfo?.currency.isNative) {
sendAnalyticsEvent(WalletEventName.LowNetworkTokenInfoModalOpened, { location: 'send' })
setShowMaxTransferModal(true)
return
}
goToReview()
}, [goToReview, isMax, hasDismissedLowNetworkTokenWarning, setShowMaxTransferModal, currencyInInfo])
const onSetExactAmount = useCallback(
(amount: string) => {
updateSendForm(isFiatInput ? { exactAmountFiat: amount } : { exactAmountToken: amount })
......@@ -103,6 +120,16 @@ export function SendFormScreen(): JSX.Element {
[updateSendForm],
)
const hideLowNativeBalanceWarning = useCallback(() => {
setShowMaxTransferModal(false)
}, [setShowMaxTransferModal])
const onAcknowledgeLowNativeBalanceWarning = useCallback(() => {
hideLowNativeBalanceWarning()
goToReview()
}, [hideLowNativeBalanceWarning, goToReview])
const onHideTokenSelector = useCallback(() => {
updateSendForm({ selectingCurrencyField: undefined })
}, [updateSendForm])
......@@ -127,6 +154,11 @@ export function SendFormScreen(): JSX.Element {
<Modal alignment="top" isModalOpen={screen === TransactionScreen.Review} name={ModalName.SendReview}>
<SendReviewDetails />
</Modal>
<LowNativeBalanceModal
isOpen={showMaxTransferModal}
onClose={hideLowNativeBalanceWarning}
onAcknowledge={onAcknowledgeLowNativeBalanceWarning}
/>
<Flex fill gap="$spacing12">
<Flex
borderColor="$surface3"
......
import { useCallback, useState } from 'react'
import { LayoutChangeEvent } from 'react-native'
import { CopyButton } from 'src/app/components/buttons/CopyButton'
import { Flex, Separator, Text } from 'ui/src'
import { spacing } from 'ui/src/theme'
import { setClipboard } from 'uniswap/src/utils/clipboard'
import { logger } from 'utilities/src/logger/logger'
import { useAsyncData } from 'utilities/src/react/hooks'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
function SeedPhraseColumnGroup({ recoveryPhraseArray }: { recoveryPhraseArray: string[] }): JSX.Element {
const [largestIndexWidth, setLargestIndexWidth] = useState(0)
const halfLength = Math.ceil(recoveryPhraseArray.length / 2)
const firstHalfWords = recoveryPhraseArray.slice(0, halfLength)
const secondHalfWords = recoveryPhraseArray.slice(halfLength)
const onIndexLayout = (event: LayoutChangeEvent): void => {
const { width } = event.nativeEvent.layout
if (width > largestIndexWidth) {
setLargestIndexWidth(width)
}
}
return (
<Flex grow row gap="$spacing16" justifyContent="space-between">
<SeedPhraseColumn
indexOffset={1}
largestIndexWidth={largestIndexWidth}
words={firstHalfWords}
onIndexLayout={onIndexLayout}
/>
<Separator vertical />
<SeedPhraseColumn
indexOffset={halfLength + 1}
largestIndexWidth={largestIndexWidth}
words={secondHalfWords}
onIndexLayout={onIndexLayout}
/>
</Flex>
)
}
function SeedPhraseColumn({
words,
indexOffset,
largestIndexWidth,
onIndexLayout,
}: {
words: string[]
indexOffset: number
largestIndexWidth: number
onIndexLayout: (event: LayoutChangeEvent) => void
}): JSX.Element {
return (
<Flex fill gap="$spacing16">
{words.map((word, index) => (
<SeedPhraseWord
key={index}
index={index + indexOffset}
indexMinWidth={largestIndexWidth}
word={word}
onIndexLayout={onIndexLayout}
/>
))}
</Flex>
)
}
function SeedPhraseWord({
index,
word,
indexMinWidth,
onIndexLayout,
}: {
index: number
word: string
indexMinWidth: number
onIndexLayout: (event: LayoutChangeEvent) => void
}): JSX.Element {
return (
<Flex key={index} row gap="$spacing12">
<Text color="$neutral3" minWidth={indexMinWidth} variant="body2" onLayout={onIndexLayout}>
{index}
</Text>
<Text variant="body2" className="notranslate">
{word}
</Text>
</Flex>
)
}
export function SeedPhraseDisplay({ mnemonicId }: { mnemonicId: string }): JSX.Element {
const placeholderWordArrayLength = 12
const recoveryPhraseString = useAsyncData(
useCallback(async () => Keyring.retrieveMnemonicUnlocked(mnemonicId), [mnemonicId]),
).data
const recoveryPhraseArray = recoveryPhraseString?.split(' ') ?? Array(placeholderWordArrayLength).fill('')
const onCopyPress = async (): Promise<void> => {
try {
if (recoveryPhraseString) {
await setClipboard(recoveryPhraseString)
}
} catch (error) {
logger.error(error, {
tags: { file: 'SeedPhraseDisplay', function: 'onCopyPress' },
})
}
}
return (
<Flex
backgroundColor="$surface2"
borderColor="$surface3"
borderRadius="$rounded16"
borderWidth={1}
gap="$spacing12"
width="100%"
>
<Flex fill row pb="$spacing24" pt="$spacing32" px="$spacing24">
<SeedPhraseColumnGroup recoveryPhraseArray={recoveryPhraseArray} />
</Flex>
<Flex alignItems="center" position="absolute" top={-1 * spacing.spacing16} width="100%">
<CopyButton onCopyPress={onCopyPress} />
</Flex>
</Flex>
)
}
import { Button, Flex, Square, Text } from 'ui/src'
import { DeprecatedButton, Flex, Square, Text } from 'ui/src'
import { ThemeNames } from 'ui/src/theme'
export type SettingsRecoveryPhraseProps = {
......@@ -38,14 +38,14 @@ export function SettingsRecoveryPhrase({
</Flex>
<Flex grow>{children}</Flex>
<Flex mt="$spacing12">
<Button
<DeprecatedButton
disabled={!nextButtonEnabled}
flexGrow={1}
theme={nextButtonTheme as ThemeNames}
onPress={onNextPressed}
>
{nextButtonText}
</Button>
</DeprecatedButton>
</Flex>
</Flex>
)
......
import { useCallback, useEffect, useState } from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { LayoutChangeEvent } from 'react-native'
import { CopyButton } from 'src/app/components/buttons/CopyButton'
import { ScreenHeader } from 'src/app/components/layout/ScreenHeader'
import { SeedPhraseDisplay } from 'src/app/features/settings/SettingsRecoveryPhraseScreen/SeedPhraseDisplay'
import { SettingsRecoveryPhrase } from 'src/app/features/settings/SettingsRecoveryPhraseScreen/SettingsRecoveryPhrase'
import { EnterPasswordModal } from 'src/app/features/settings/password/EnterPasswordModal'
import { AppRoutes, RemoveRecoveryPhraseRoutes, SettingsRoutes } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state'
import { Button, Flex, Separator, Text } from 'ui/src'
import { DeprecatedButton, Flex, Text } from 'ui/src'
import { AlertTriangleFilled, Eye, Key, Laptop } from 'ui/src/components/icons'
import { spacing } from 'ui/src/theme'
import { WalletEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { setClipboard } from 'uniswap/src/utils/clipboard'
import { logger } from 'utilities/src/logger/logger'
import { useAsyncData } from 'utilities/src/react/hooks'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
import { useSignerAccounts } from 'wallet/src/features/wallet/hooks'
const enum ViewStep {
......@@ -35,25 +30,6 @@ export function SettingsViewRecoveryPhraseScreen(): JSX.Element {
throw new Error('Screen should not be accessed unless mnemonic account exists')
}
const placeholderWordArrayLength = 12
const recoveryPhraseString = useAsyncData(
useCallback(async () => Keyring.retrieveMnemonicUnlocked(mnemonicAccount.mnemonicId), [mnemonicAccount.mnemonicId]),
).data
const recoveryPhraseArray = recoveryPhraseString?.split(' ') ?? Array(placeholderWordArrayLength).fill('')
const onCopyPress = async (): Promise<void> => {
try {
if (recoveryPhraseString) {
await setClipboard(recoveryPhraseString)
}
} catch (error) {
logger.error(error, {
tags: { file: 'SettingsViewRecoveryPhraseScreen.tsx', function: 'onCopyPress' },
})
}
}
const showPasswordModal = (): void => {
setViewStep(ViewStep.Password)
}
......@@ -125,28 +101,14 @@ export function SettingsViewRecoveryPhraseScreen(): JSX.Element {
</SettingsRecoveryPhrase>
) : (
<Flex fill gap="$spacing24" pt="$spacing36">
<Flex
backgroundColor="$surface2"
borderColor="$surface3"
borderRadius="$rounded16"
borderWidth={1}
gap="$spacing12"
width="100%"
>
<Flex fill row pb="$spacing24" pt="$spacing32" px="$spacing24">
<SeedPhraseColumnGroup recoveryPhraseArray={recoveryPhraseArray} />
</Flex>
<Flex alignItems="center" position="absolute" top={-1 * spacing.spacing16} width="100%">
<CopyButton onCopyPress={onCopyPress} />
</Flex>
</Flex>
<SeedPhraseDisplay mnemonicId={mnemonicAccount.mnemonicId} />
<Flex alignItems="center" gap="$spacing8">
<Text color="$neutral2" textAlign="center" variant="body3">
{t('setting.recoveryPhrase.warning.view.message')}
</Text>
</Flex>
<Flex fill justifyContent="flex-end">
<Button
<DeprecatedButton
theme="detrimental"
onPress={(): void =>
navigate(
......@@ -156,92 +118,10 @@ export function SettingsViewRecoveryPhraseScreen(): JSX.Element {
}
>
{t('setting.recoveryPhrase.remove')}
</Button>
</DeprecatedButton>
</Flex>
</Flex>
)}
</Flex>
)
}
function SeedPhraseColumnGroup({ recoveryPhraseArray }: { recoveryPhraseArray: string[] }): JSX.Element {
const [largestIndexWidth, setLargestIndexWidth] = useState(0)
const halfLength = Math.ceil(recoveryPhraseArray.length / 2)
const firstHalfWords = recoveryPhraseArray.slice(0, halfLength)
const secondHalfWords = recoveryPhraseArray.slice(halfLength)
const onIndexLayout = (event: LayoutChangeEvent): void => {
const { width } = event.nativeEvent.layout
if (width > largestIndexWidth) {
setLargestIndexWidth(width)
}
}
return (
<Flex grow row gap="$spacing16" justifyContent="space-between">
<SeedPhraseColumn
indexOffset={1}
largestIndexWidth={largestIndexWidth}
words={firstHalfWords}
onIndexLayout={onIndexLayout}
/>
<Separator vertical borderWidth="$spacing1" />
<SeedPhraseColumn
indexOffset={halfLength + 1}
largestIndexWidth={largestIndexWidth}
words={secondHalfWords}
onIndexLayout={onIndexLayout}
/>
</Flex>
)
}
function SeedPhraseColumn({
words,
indexOffset,
largestIndexWidth,
onIndexLayout,
}: {
words: string[]
indexOffset: number
largestIndexWidth: number
onIndexLayout: (event: LayoutChangeEvent) => void
}): JSX.Element {
return (
<Flex fill gap="$spacing16">
{words.map((word, index) => (
<SeedPhraseWord
key={index}
index={index + indexOffset}
indexMinWidth={largestIndexWidth}
word={word}
onIndexLayout={onIndexLayout}
/>
))}
</Flex>
)
}
function SeedPhraseWord({
index,
word,
indexMinWidth,
onIndexLayout,
}: {
index: number
word: string
indexMinWidth: number
onIndexLayout: (event: LayoutChangeEvent) => void
}): JSX.Element {
return (
<Flex key={index} row gap="$spacing12">
<Text color="$neutral3" minWidth={indexMinWidth} variant="body2" onLayout={onIndexLayout}>
{index}
</Text>
<Text variant="body2" className="notranslate">
{word}
</Text>
</Flex>
)
}
......@@ -8,8 +8,8 @@ import { SettingsItemWithDropdown } from 'src/app/features/settings/SettingsItem
import { AppRoutes, SettingsRoutes } from 'src/app/navigation/constants'
import { useExtensionNavigation } from 'src/app/navigation/utils'
import {
Button,
ColorTokens,
DeprecatedButton,
Flex,
GeneratedIcon,
ScrollView,
......@@ -222,9 +222,9 @@ export function SettingsScreen(): JSX.Element {
<Text color="$neutral3" px="$spacing12" py="$spacing4" variant="body4">{`Version ${manifestVersion}`}</Text>
</SettingsSection>
</ScrollView>
<Button icon={<Lock />} theme="secondary" onPress={onPressLockWallet}>
<DeprecatedButton icon={<Lock />} theme="secondary" onPress={onPressLockWallet}>
{t('settings.action.lock')}
</Button>
</DeprecatedButton>
</Flex>
</>
)
......
......@@ -2,7 +2,7 @@ import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import { PADDING_STRENGTH_INDICATOR, PasswordInput } from 'src/app/components/PasswordInput'
import { Button, Flex, Text } from 'ui/src'
import { DeprecatedButton, Flex, Text } from 'ui/src'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
......@@ -67,9 +67,9 @@ export function ChangePasswordForm({ onNext }: { onNext: () => void }): JSX.Elem
{errorText || 'Placeholder text'}
</Text>
</Flex>
<Button disabled={!enableNext} theme="tertiary" onPress={onSubmit}>
<DeprecatedButton disabled={!enableNext} theme="tertiary" onPress={onSubmit}>
{t('common.button.save')}
</Button>
</DeprecatedButton>
</Flex>
)
}
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { PasswordInput } from 'src/app/components/PasswordInput'
import { Button, Flex, Text } from 'ui/src'
import { DeprecatedButton, Flex, Text } from 'ui/src'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
function useEnterPasswordForm(): {
......@@ -72,9 +72,9 @@ export function EnterPasswordForm({ onNext }: { onNext: () => void }): JSX.Eleme
</Text>
)}
</Flex>
<Button disabled={!submitEnabled} theme="tertiary" onPress={onContinue}>
<DeprecatedButton disabled={!submitEnabled} theme="tertiary" onPress={onContinue}>
{t('common.button.continue')}
</Button>
</DeprecatedButton>
</Flex>
)
}
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { PasswordInput } from 'src/app/components/PasswordInput'
import { Button, Flex, Square, Text, inputStyles, useSporeColors } from 'ui/src'
import { DeprecatedButton, Flex, Square, Text, inputStyles, useSporeColors } from 'ui/src'
import { Lock } from 'ui/src/components/icons'
import { Modal } from 'uniswap/src/components/modals/Modal'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
......@@ -69,9 +69,9 @@ export function EnterPasswordModal({
<Text color="$statusCritical" minHeight="$spacing24" textAlign="center" variant="body2">
{showPasswordError ? t('setting.recoveryPhrase.remove.password.error') : ''}
</Text>
<Button disabled={!password.length} theme="secondary" width="100%" onPress={checkPassword}>
<DeprecatedButton disabled={!password.length} theme="secondary" width="100%" onPress={checkPassword}>
{t('common.button.continue')}
</Button>
</DeprecatedButton>
</Flex>
</Modal>
)
......
......@@ -4,7 +4,7 @@ import { OnboardingScreen } from 'src/app/features/onboarding/OnboardingScreen'
import { useOnboardingSteps } from 'src/app/features/onboarding/OnboardingStepsContext'
import { useUnitagClaimContext } from 'src/app/features/unitags/UnitagClaimContext'
import { closeCurrentTab } from 'src/app/navigation/utils'
import { Button, Flex, Text } from 'ui/src'
import { DeprecatedButton, Flex, Text } from 'ui/src'
import { UNITAG_SUFFIX } from 'uniswap/src/features/unitags/constants'
import { logger } from 'utilities/src/logger/logger'
import { UnitagWithProfilePicture } from 'wallet/src/features/unitags/UnitagWithProfilePicture'
......@@ -49,12 +49,12 @@ export function UnitagConfirmationScreen(): JSX.Element {
</Text>
</Flex>
<Flex gap="$spacing12" pt="$spacing12">
<Button size="medium" theme="primary" onPress={closeCurrentTab}>
<DeprecatedButton size="medium" theme="primary" onPress={closeCurrentTab}>
{t('common.button.done')}
</Button>
<Button size="medium" theme="secondary" onPress={onPressCustomize}>
</DeprecatedButton>
<DeprecatedButton size="medium" theme="secondary" onPress={onPressCustomize}>
{t('unitags.claim.confirmation.customize')}
</Button>
</DeprecatedButton>
</Flex>
</Flex>
</OnboardingScreen>
......
......@@ -4,7 +4,7 @@ import { useOnboardingSteps } from 'src/app/features/onboarding/OnboardingStepsC
import { Terms } from 'src/app/features/onboarding/Terms'
import { UnitagClaimRoutes } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state'
import { Button, Flex, GeneratedIcon, Text } from 'ui/src'
import { DeprecatedButton, Flex, GeneratedIcon, Text } from 'ui/src'
import { Bolt, Coupon, UserSquare } from 'ui/src/components/icons'
import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks'
import { useAccountAddressFromUrlWithThrow } from 'wallet/src/features/wallet/hooks'
......@@ -46,9 +46,9 @@ export function UnitagIntroScreen(): JSX.Element {
</Flex>
<Flex gap="$spacing24">
<Button size="large" onPress={() => goToNextStep()}>
<DeprecatedButton size="large" onPress={() => goToNextStep()}>
{t('unitags.extension.intro.buttton')}
</Button>
</DeprecatedButton>
<Flex width={TERMS_WIDTH} alignSelf="center">
<Terms />
</Flex>
......
......@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Uniswap Extension",
"description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.",
"version": "1.13.0",
"version": "1.14.0",
"minimum_chrome_version": "116",
"icons": {
"16": "assets/icon16.png",
......
......@@ -12,7 +12,7 @@ import {
deleteDeprecatedReduxedChromeStorage,
readDeprecatedReduxedChromeStorage,
} from 'src/store/reduxedChromeStorageToReduxPersistMigration'
import { getFiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api'
import { fiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api'
import { createDatadogReduxEnhancer } from 'utilities/src/logger/Datadog'
import { createStore } from 'wallet/src/state'
import { createMigrate } from 'wallet/src/state/createMigrate'
......@@ -52,7 +52,7 @@ const setupStore = (preloadedState?: PreloadedState<ExtensionState>): ReturnType
preloadedState,
additionalSagas: [rootExtensionSaga],
middlewareBefore: __DEV__ ? [loggerMiddleware] : [],
middlewareAfter: [getFiatOnRampAggregatorApi().middleware],
middlewareAfter: [fiatOnRampAggregatorApi.middleware],
enhancers: [sentryReduxEnhancer, dataDogReduxEnhancer],
})
}
......
......@@ -83,6 +83,9 @@ artifacts/
# CocoaPods
/ios/Pods/
# Android jsbundle
*.android.bundle
# ccache
.ccache
......
appId: com.uniswap.mobile.dev
---
- launchApp
- tapOn:
id: 'search-tokens-and-wallets'
- 'scroll'
- 'scroll'
- 'scroll'
- 'back'
appId: com.uniswap.mobile.dev
---
- launchApp:
appId: 'com.uniswap.mobile.dev'
clearState: true
clearKeychain: true # optional: clear *entire* iOS keychain
- extendedWaitUntil:
visible: 'Create a wallet'
- tapOn: 'Create a wallet'
- waitForAnimationToEnd
- tapOn: 'Skip'
- waitForAnimationToEnd
- tapOn: 'Skip'
- waitForAnimationToEnd
- tapOn: 'Skip'
- extendedWaitUntil:
visible:
id: 'confirm'
- tapOn:
id: 'confirm'
- waitForAnimationToEnd
- tapOn: 'Send'
- waitForAnimationToEnd
- 'back'
......@@ -31,19 +31,6 @@ const normalizedStories = [
/^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/
),
},
{
titlePrefix: "",
directory: "../../packages/ui/src",
files: "**/*.mdx",
importPathMatcher:
/^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.mdx)$/,
// @ts-ignore
req: require.context(
"../../../packages/ui/src",
true,
/^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.mdx)$/
),
},
];
declare global {
......
......@@ -4,6 +4,32 @@
If you have suggestions on how we can improve the app, or would like to report a bug or a problem, check out the [Uniswap Help Center](https://support.uniswap.org/).
## Table of contents
- [Setup](#setup)
- [Packages and Software](#packages-and-software)
- [iOS Setup](#ios-setup)
- [Xcode](#xcode)
- [Add Xcode Command Line Tools](#add-xcode-command-line-tools)
- [Android Setup](#android-setup)
- [Deploying to Physical Android Device](#deploying-to-physical-android-device)
- [Development](#development)
- [Environment variables](#environment-variables)
- [Compile contract ABI types](#compile-contract-abi-types)
- [Run the app](#run-the-app)
- [Using Radon IDE](#using-radon-ide-vscodecursor-extension)
- [Running on a Physical iOS Device](#running-on-a-physical-ios-device)
- [Important Libraries and Tools](#important-libraries-and-tools)
- [Migrations](#migrations)
- [Testing & Performance](#testing--performance)
- [Build local app files](./docs/build-app-files.md)
- [E2E testing](./docs/e2e-testing.md)
- [Performance monitoring](./docs/perf-monitoring.md)
- [Troubleshooting](#troubleshooting)
- [Common issues](#common-issues)
- [Common fixes](#common-fixes)
- [Shell profile setup](#shell-profile-setup)
## Setup
This guide assumes that:
......@@ -124,7 +150,7 @@ Note: If you are indeed using an Apple Silicon Mac, we recommend setting up your
You should start with downloading Xcode if you don't already have it installed, since the file is so large. You can find it here: [developer.apple.com/xcode](https://developer.apple.com/xcode/)
You must use **XCode 15** to compile the app. [Older versions of xCode can be found here](https://developer.apple.com/download/all/?q=xcode).
You must use the [Required Xcode Version](https://github.com/Uniswap/universe/blob/main/apps/mobile/scripts/podinstall.sh#L5) to compile the app. [Older versions of xCode can be found here](https://developer.apple.com/download/all/?q=xcode).
#### Add Xcode Command Line Tools
......@@ -255,6 +281,13 @@ These are some tools you might want to familiarize yourself with to understand t
We use `redux-persist` to persist the Redux state between user sessions. Most of this state is shared between the mobile app and the extension. Please review the [Wallet Migrations README](../../packages/wallet/src/state//README.md) for details on how to write migrations when you add or remove anything from the Redux state structure.
## Testing & Performance
- [Build local app files](./docs/build-app-files.md)
- [E2E testing](./docs/e2e-testing.md)
- [Performance monitoring](./docs/perf-monitoring.md)
## Troubleshooting
### Common issues
......@@ -313,3 +346,4 @@ eval "$(/opt/homebrew/bin/brew shellenv)"
export NVM_DIR="$HOME/.nvm"
[ -s "/opt/homebrew/opt/nvm/nvm.sh" ] && \. "/opt/homebrew/opt/nvm/nvm.sh" # This loads nvm
[ -s "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" ] && \. "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" # This loads nvm bash_completion
......@@ -89,9 +89,9 @@ if (isCI && datadogPropertiesAvailable && !isDetox) {
apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle"
}
def devVersionName = "1.43"
def betaVersionName = "1.43"
def prodVersionName = "1.43"
def devVersionName = "1.44"
def betaVersionName = "1.44"
def prodVersionName = "1.44"
android {
ndkVersion rootProject.ext.ndkVersion
......@@ -251,6 +251,9 @@ dependencies {
implementation("androidx.core:core-performance:$corePerf")
implementation("androidx.core:core-performance-play-services:$corePerf")
implementation 'com.onesignal:OneSignal:4.8.9'
implementation 'com.github.statsig-io:android-sdk:4.36.0'
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
} else {
......
......@@ -31,6 +31,9 @@
android:name="com.onesignal.NotificationAccentColor.DEFAULT"
android:value="@string/notification_accent_color" />
<meta-data android:name="com.onesignal.NotificationServiceExtension"
android:value="com.uniswap.notifications.NotificationExtension" />
<activity
android:name=".MainActivity"
android:label="@string/app_name"
......
......@@ -11,6 +11,7 @@ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnable
import com.facebook.react.defaults.DefaultReactActivityDelegate
import com.facebook.react.modules.i18nmanager.I18nUtil
import expo.modules.ReactActivityDelegateWrapper
import com.zoontek.rnbootsplash.RNBootSplash;
class MainActivity : ReactActivity() {
......@@ -25,6 +26,8 @@ class MainActivity : ReactActivity() {
// Required for react-navigation to work on Android
override fun onCreate(savedInstanceState: Bundle?) {
RNBootSplash.init(this, R.style.AppTheme)
super.onCreate(null);
window.navigationBarColor = Color.TRANSPARENT
......
package com.uniswap.notifications
import android.app.Application
import android.content.Context
import android.provider.Settings.Secure
import com.onesignal.OSNotificationReceivedEvent
import com.onesignal.OneSignal.OSRemoteNotificationReceivedHandler
import com.statsig.androidsdk.Statsig
import com.statsig.androidsdk.StatsigOptions
import com.statsig.androidsdk.StatsigUser
import com.uniswap.BuildConfig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/**
* OneSignal extension used to intercept notifications, integrating with Statsig to gate and expose
* test groups.
*/
class NotificationExtension : OSRemoteNotificationReceivedHandler {
private val scope = CoroutineScope(Dispatchers.IO)
override fun remoteNotificationReceived(
context: Context?,
notificationReceivedEvent: OSNotificationReceivedEvent
) {
val notification = notificationReceivedEvent.notification
val additionalData = notification.additionalData
val notificationType = if (additionalData.has(FIELD_NOTIFICATION_TYPE)) additionalData.getString(FIELD_NOTIFICATION_TYPE) else null
val isGatedNotification = notificationType == TYPE_UNFUNDED_WALLET_REMINDER ||
notificationType == TYPE_PRICE_ALERT
if (isGatedNotification) {
scope.launch(Dispatchers.IO) {
if (!Statsig.isInitialized()) {
val options = StatsigOptions(api = STATSIG_PROXY_URL, eventLoggingAPI = STATSIG_PROXY_URL).apply {
setEnvironmentParameter(STATSIG_ENVIRONMENT_KEY_TIER, getStatsigTier())
}
val deviceId = Secure.getString(context!!.contentResolver, Secure.ANDROID_ID)
val user = StatsigUser(userID = deviceId)
user.custom = mapOf("app" to "mobile")
Statsig.initialize(
context!!.applicationContext as Application,
STATSIG_SDK_KEY,
user,
options
)
}
val enabled = when(notificationType) {
TYPE_UNFUNDED_WALLET_REMINDER -> Statsig.checkGate(FEATURE_GATE_UNFUNDED_WALLET)
TYPE_PRICE_ALERT -> Statsig.checkGate(FEATURE_GATE_PRICE_ALERT)
else -> true
}
// Passing null will skip the notification
notificationReceivedEvent.complete(if (enabled) notification else null)
}
} else {
notificationReceivedEvent.complete(notification)
}
}
private fun getStatsigTier(): String = when(BuildConfig.FLAVOR) {
"dev" -> "development"
"beta" -> "beta"
"prod" -> "production"
else -> "production"
}
companion object {
// fake value that gets replaced by the proxy
private const val STATSIG_SDK_KEY = "client-000000000000000000000000000000000000000000"
private const val STATSIG_PROXY_URL =
"https://gating.android.wallet.gateway.uniswap.org/v1/statsig-proxy"
private const val STATSIG_ENVIRONMENT_KEY_TIER = "tier"
private const val FEATURE_GATE_UNFUNDED_WALLET = "notification_unfunded_wallet"
private const val FEATURE_GATE_PRICE_ALERT = "notification_price_alert"
private const val FIELD_NOTIFICATION_TYPE = "notification_type"
private const val TYPE_UNFUNDED_WALLET_REMINDER = "unfunded_wallet_reminder"
private const val TYPE_PRICE_ALERT = "price_alert"
}
}
......@@ -54,6 +54,12 @@ class MnemonicDisplayViewManager : ViewGroupManager<ComposeView>() {
putDouble(FIELD_HEIGHT, it.toDouble())
}
sendEvent(id, EVENT_HEIGHT_MEASURED, bundle)
},
onEmptyMnemonic = {
val bundle = Arguments.createMap().apply {
putString("mnemonicId", it)
}
sendEvent(id, EVENT_EMPTY_MNEMONIC, bundle)
}
)
}
......@@ -75,6 +81,12 @@ class MnemonicDisplayViewManager : ViewGroupManager<ComposeView>() {
"bubbled" to EVENT_HEIGHT_MEASURED,
"captured" to EVENT_HEIGHT_MEASURED
)
),
EVENT_EMPTY_MNEMONIC to mapOf(
"phasedRegistrationNames" to mapOf(
"bubbled" to EVENT_EMPTY_MNEMONIC,
"captured" to EVENT_EMPTY_MNEMONIC
)
)
)
}
......@@ -103,6 +115,7 @@ class MnemonicDisplayViewManager : ViewGroupManager<ComposeView>() {
companion object {
private const val REACT_CLASS = "MnemonicDisplay"
private const val EVENT_HEIGHT_MEASURED = "onHeightMeasured"
private const val EVENT_EMPTY_MNEMONIC = "onEmptyMnemonic"
private const val FIELD_HEIGHT = "height"
}
}
......@@ -24,6 +24,7 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import com.uniswap.onboarding.shared.CopyButton
import com.uniswap.theme.relativeOffset
import kotlinx.coroutines.withTimeoutOrNull
import kotlin.math.abs
@Composable
......@@ -32,7 +33,8 @@ fun MnemonicDisplay(
mnemonicId: String,
copyText: String,
copiedText: String,
onHeightMeasured: (height: Float) -> Unit
onHeightMeasured: (height: Float) -> Unit,
onEmptyMnemonic: (mnemonicId: String) -> Unit
) {
val words by viewModel.words.collectAsState()
val textToCopy = AnnotatedString(words.joinToString(" ") { it.text })
......@@ -41,6 +43,16 @@ fun MnemonicDisplay(
LaunchedEffect(mnemonicId) {
viewModel.setup(mnemonicId)
// Check and log if the mnemonic is empty after 1 second to avoid calling onEmptyMnemonic too early
withTimeoutOrNull<Unit>(1000L) {
viewModel.words.collect { currentWords ->
if (currentWords.isEmpty() || currentWords.any { it.text.isBlank() }) {
onEmptyMnemonic(mnemonicId)
return@collect
}
}
}
}
BoxWithConstraints {
......
......@@ -12,5 +12,4 @@
<item name="android:navigationBarColor">@color/background_material_light</item>
<item name="android:editTextBackground">@android:color/transparent</item>
</style>
</resources>
......@@ -19,6 +19,7 @@ buildscript {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
dependencies {
classpath("com.android.tools.build:gradle")
......
......@@ -9,5 +9,11 @@
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
</dict>
<key>OneSignal_app_groups_key</key>
<string>group.com.uniswap.mobile.onesignal</string>
<key>STATSIG_SDK_KEY</key>
<string>$(STATSIG_SDK_KEY)</string>
<key>BUNDLE_ID_SUFFIX</key>
<string>$(BUNDLE_ID_SUFFIX)</string>
</dict>
</plist>
// File copied from Onesignal docs: https://documentation.onesignal.com/docs/react-native-sdk-setup
import UserNotifications
import OneSignalExtension
import Statsig
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var receivedRequest: UNNotificationRequest!
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.receivedRequest = request
self.contentHandler = contentHandler
self.bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
if let bestAttemptContent = bestAttemptContent {
/* DEBUGGING: Uncomment the 2 lines below to check this extension is executing
Note, this extension only runs when mutable-content is set
Setting an attachment or action buttons automatically adds this */
#if DEBUG
print("Running NotificationServiceExtension")
bestAttemptContent.body = "[Modified] " + bestAttemptContent.body
#endif
OneSignalExtension.didReceiveNotificationExtensionRequest(self.receivedRequest, with: bestAttemptContent, withContentHandler: self.contentHandler)
}
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
let bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
let userInfo = request.content.userInfo
// Fields per OneSignal docs
let custom = userInfo["custom"] as? [String: Any]
let additionalData = custom?["a"] as? [String: Any]
let notificationType = additionalData?[Constants.fieldNotificationType] as? String
let isGatedNotification = notificationType == Constants.typeUnfundedWallet
|| notificationType == Constants.typePriceAlert
if (!isGatedNotification) {
OneSignalExtension.didReceiveNotificationExtensionRequest(request, with: bestAttemptContent, withContentHandler: contentHandler)
return
}
override func serviceExtensionTimeWillExpire() {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
OneSignalExtension.serviceExtensionTimeWillExpireRequest(self.receivedRequest, with: self.bestAttemptContent)
contentHandler(bestAttemptContent)
}
func handleGatedNotification() {
let enabled: Bool
switch notificationType {
case Constants.typeUnfundedWallet:
enabled = Statsig.checkGate(Constants.gateUnfundedWallet)
case Constants.typePriceAlert:
enabled = Statsig.checkGate(Constants.gatePriceAlert)
default:
enabled = true
}
// Passing in empty notification content will skip the notif
OneSignalExtension.didReceiveNotificationExtensionRequest(
request,
with: enabled ? bestAttemptContent : UNMutableNotificationContent(),
withContentHandler: contentHandler)
}
if (!Statsig.isInitialized()) {
// The real sdk key is needed on iOS even though it's substituted in proxy
// Because the key is used to hash the feature gate names and wouldn't work properly otherwise
let statsigSdkKey = Bundle.main.object(forInfoDictionaryKey: "STATSIG_SDK_KEY") as? String ?? ""
let statsigUser = StatsigUser(
userID: UIDevice.current.identifierForVendor?.uuidString,
custom: [
"app": "mobile"
])
Statsig.initialize(
sdkKey: statsigSdkKey,
user: statsigUser,
options: StatsigOptions(
environment: StatsigEnvironment(tier: getStatsigEnvironemntTier()),
initializationURL: URL(string: "\(Constants.statsigProxyHost)/v1/statsig-proxy/initialize"),
eventLoggingURL: URL(string: "\(Constants.statsigProxyHost)/v1/statsig-proxy/rgstr")
)) { _errorMessage in
handleGatedNotification()
}
} else {
handleGatedNotification()
}
}
func getStatsigEnvironemntTier() -> String {
let bundleSuffix = Bundle.main.object(forInfoDictionaryKey: "BUNDLE_ID_SUFFIX") as? String
switch bundleSuffix {
case ".dev":
return "development"
case ".beta":
return "beta"
default:
return "production"
}
}
}
struct Constants {
static let statsigProxyHost = "https://gating.ios.wallet.gateway.uniswap.org"
static let fieldNotificationType = "notification_type"
static let typeUnfundedWallet = "unfunded_wallet_reminder"
static let typePriceAlert = "price_alert"
static let gateUnfundedWallet = "notification_unfunded_wallet"
static let gatePriceAlert = "notification_price_alert"
}
......@@ -4,7 +4,7 @@
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.$(PRODUCT_NAME).onesignal</string>
<string>group.com.uniswap.mobile.onesignal</string>
</array>
</dict>
</plist>
......@@ -57,6 +57,8 @@ end
target 'OneSignalNotificationServiceExtension' do
use_frameworks! :linkage => :static
pod 'OneSignalXCFramework', '3.12.6'
pod 'Statsig', '1.49.0'
end
def prepare_target_commons
......
......@@ -1235,11 +1235,6 @@ PODS:
- ExpoModulesCore
- EXScreenCapture (5.8.1):
- ExpoModulesCore
- EXSplashScreen (0.26.5):
- ExpoModulesCore
- glog
- RCT-Folly (= 2022.05.16.00)
- React-Core
- FBLazyVector (0.73.6)
- FBReactNativeSpec (0.73.6):
- RCT-Folly (= 2022.05.16.00)
......@@ -1265,7 +1260,7 @@ PODS:
- FirebaseCore (~> 11.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- FirebaseAppCheckInterop (11.4.0)
- FirebaseAppCheckInterop (11.6.0)
- FirebaseAuth (11.2.0):
- FirebaseAppCheckInterop (~> 11.0)
- FirebaseAuthInterop (~> 11.0)
......@@ -1275,14 +1270,14 @@ PODS:
- GoogleUtilities/Environment (~> 8.0)
- GTMSessionFetcher/Core (~> 3.4)
- RecaptchaInterop (~> 100.0)
- FirebaseAuthInterop (11.4.0)
- FirebaseAuthInterop (11.6.0)
- FirebaseCore (11.2.0):
- FirebaseCoreInternal (~> 11.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreExtension (11.4.1):
- FirebaseCore (~> 11.0)
- FirebaseCoreInternal (11.4.2):
- FirebaseCoreInternal (11.6.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseFirestore (11.2.0):
- FirebaseCore (~> 11.0)
......@@ -1304,7 +1299,7 @@ PODS:
- gRPC-Core (~> 1.65.0)
- leveldb-library (~> 1.22)
- nanopb (~> 3.30910.0)
- FirebaseSharedSwift (11.4.0)
- FirebaseSharedSwift (11.6.0)
- fmt (6.2.1)
- glog (0.3.5)
- GoogleUtilities/AppDelegateSwizzler (8.0.2):
......@@ -1439,9 +1434,9 @@ PODS:
- libwebp/sharpyuv (1.3.2)
- libwebp/webp (1.3.2):
- libwebp/sharpyuv
- MMKV (1.3.4):
- MMKVCore (~> 1.3.4)
- MMKVCore (1.3.4)
- MMKV (2.0.0):
- MMKVCore (~> 2.0.0)
- MMKVCore (2.0.0)
- nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0)
......@@ -2542,6 +2537,8 @@ PODS:
- ReactNativePerformance (4.1.2):
- React-Core
- RecaptchaInterop (100.0.0)
- RNBootSplash (6.3.1):
- React-Core
- RNCAsyncStorage (1.17.10):
- React-Core
- RNCMaskedView (0.2.9):
......@@ -2613,6 +2610,7 @@ PODS:
- SocketRocket (0.6.1)
- sparkfabrik-react-native-idfa-aaid (1.2.0):
- React
- Statsig (1.49.0)
- UIImageColors (2.1.0)
- Yoga (1.14.0)
- ZXingObjC/Core (3.6.9)
......@@ -2646,7 +2644,6 @@ DEPENDENCIES:
- ExpoStoreReview (from `../../../node_modules/expo-store-review/ios`)
- ExpoWebBrowser (from `../../../node_modules/expo-web-browser/ios`)
- EXScreenCapture (from `../../../node_modules/expo-screen-capture/ios`)
- EXSplashScreen (from `../../../node_modules/expo-splash-screen/ios`)
- FBLazyVector (from `../../../node_modules/react-native/Libraries/FBLazyVector`)
- FBReactNativeSpec (from `../../../node_modules/react-native/React/FBReactNativeSpec`)
- glog (from `../../../node_modules/react-native/third-party-podspecs/glog.podspec`)
......@@ -2712,6 +2709,7 @@ DEPENDENCIES:
- React-utils (from `../../../node_modules/react-native/ReactCommon/react/utils`)
- ReactCommon/turbomodule/core (from `../../../node_modules/react-native/ReactCommon`)
- "ReactNativePerformance (from `../../../node_modules/@shopify/react-native-performance`)"
- RNBootSplash (from `../../../node_modules/react-native-bootsplash`)
- "RNCAsyncStorage (from `../../../node_modules/@react-native-async-storage/async-storage`)"
- "RNCMaskedView (from `../../../node_modules/@react-native-masked-view/masked-view`)"
- "RNDateTimePicker (from `../../../node_modules/@react-native-community/datetimepicker`)"
......@@ -2730,6 +2728,7 @@ DEPENDENCIES:
- RNScreens (from `../../../node_modules/react-native-screens`)
- RNSVG (from `../../../node_modules/react-native-svg`)
- "sparkfabrik-react-native-idfa-aaid (from `../../../node_modules/@sparkfabrik/react-native-idfa-aaid`)"
- Statsig (= 1.49.0)
- UIImageColors (= 2.1.0)
- Yoga (from `../../../node_modules/react-native/ReactCommon/yoga`)
......@@ -2778,6 +2777,7 @@ SPEC REPOS:
- SDWebImage
- SDWebImageWebPCoder
- SocketRocket
- Statsig
- UIImageColors
- ZXingObjC
......@@ -2826,8 +2826,6 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/expo-web-browser/ios"
EXScreenCapture:
:path: "../../../node_modules/expo-screen-capture/ios"
EXSplashScreen:
:path: "../../../node_modules/expo-splash-screen/ios"
FBLazyVector:
:path: "../../../node_modules/react-native/Libraries/FBLazyVector"
FBReactNativeSpec:
......@@ -2951,6 +2949,8 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/react-native/ReactCommon"
ReactNativePerformance:
:path: "../../../node_modules/@shopify/react-native-performance"
RNBootSplash:
:path: "../../../node_modules/react-native-bootsplash"
RNCAsyncStorage:
:path: "../../../node_modules/@react-native-async-storage/async-storage"
RNCMaskedView:
......@@ -3026,20 +3026,19 @@ SPEC CHECKSUMS:
ExpoStoreReview: 3911cdf5a416230a421af41f93e763595bd7a681
ExpoWebBrowser: e31c97230ea93b51a2a74c6c2381b0d4fca5698c
EXScreenCapture: 7a491c16de021638078f50281d411e3352bb28c2
EXSplashScreen: 0fabdcf746d29e7f8b8969879cb09125cdd365d2
FBLazyVector: f64d1e2ea739b4d8f7e4740cde18089cd97fe864
FBReactNativeSpec: 91c0784dbf98ed9c434927ea46f41b780fe3a232
Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c
FirebaseAppCheck: a6a1c1ca169d795212b9e70b5cfb880083a28e7c
FirebaseAppCheckInterop: 1b9643ae2f1ee214488caa2f8e32b7bc2f0f3735
FirebaseAppCheckInterop: 347aa09a805219a31249b58fc956888e9fcb314b
FirebaseAuth: 2a198b8cdbbbd457f08d74df7040feb0a0e7777a
FirebaseAuthInterop: 9ac948965ac13ec9d8a080f39490ddb2bda30520
FirebaseAuthInterop: a919d415797d23b7bfe195a04f322b86c65020ef
FirebaseCore: a282032ae9295c795714ded2ec9c522fc237f8da
FirebaseCoreExtension: f1bc67a4702931a7caa097d8e4ac0a1b0d16720e
FirebaseCoreInternal: 35731192cab10797b88411be84940d2beb33a238
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
FirebaseFirestore: 62708adbc1dfcd6d165a7c0a202067b441912dc9
FirebaseFirestoreInternal: ad9b9ee2d3d430c8f31333a69b3b6737a7206232
FirebaseSharedSwift: 505dae2d05969dbf6d43749a642bb1bf230f0252
FirebaseSharedSwift: a4e5dfca3e210633bb3a3dfb94176c019211948b
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
......@@ -3050,8 +3049,8 @@ SPEC CHECKSUMS:
leveldb-library: cc8b8f8e013647a295ad3f8cd2ddf49a6f19be19
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
MMKV: ed58ad794b3f88c24d604a5b74f3fba17fcbaf74
MMKVCore: a67a1cede26175c413176f404a7cedec43f96a0b
MMKV: f7d1d5945c8765f97f39c3d121f353d46735d801
MMKVCore: c04b296010fcb1d1638f2c69405096aac12f6390
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
OneSignalXCFramework: ff1c970b7aeb4ac0fe48fb35393eb5d8bf378135
OpenTelemetrySwiftApi: 657da8071c2908caecce11548e006f779924ff9c
......@@ -3115,6 +3114,7 @@ SPEC CHECKSUMS:
ReactCommon: e168ee1704806f10f47ee8c7f825b5850b18eaea
ReactNativePerformance: ab7dee4c4862623d72c1530a9fc71b55458edf71
RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21
RNBootSplash: 66c8458007bda40cc25a3f25e4326244a71d9a73
RNCAsyncStorage: 0c357f3156fcb16c8589ede67cc036330b6698ca
RNCMaskedView: 949696f25ec596bfc697fc88e6f95cf0c79669b6
RNDateTimePicker: 40ffda97d071a98a10fdca4fa97e3977102ccd14
......@@ -3136,10 +3136,11 @@ SPEC CHECKSUMS:
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
sparkfabrik-react-native-idfa-aaid: 1b72a6264a2175473e309ffa6434db87c58af264
Statsig: 970abcd107e8e64bb68f6b8504a94c39d7f9e318
UIImageColors: d2ef3b0877d203cbb06489eeb78ea8b7788caabe
Yoga: 805bf71192903b20fc14babe48080582fee65a80
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
PODFILE CHECKSUM: 525fd4a1c78879023ae05970b18e66b654c4c07a
PODFILE CHECKSUM: 18445ed90dc0fd39adbfc715d3fbf6316e3013ad
COCOAPODS: 1.14.3
......@@ -8,6 +8,7 @@
#import <React/RCTBundleURLProvider.h>
#import <ReactNativePerformance/ReactNativePerformance.h>
#import <RCTAppSetupUtils.h>
#import <RNBootSplash.h>
@implementation AppDelegate
......@@ -48,6 +49,7 @@
[super application:application didFinishLaunchingWithOptions:newLaunchOptions];
[[RCTI18nUtil sharedInstance] allowRTL:NO];
[RNBootSplash initWithStoryboard:@"SplashScreen" rootView:self.window.rootViewController.view];
return YES;
}
......
......@@ -62,7 +62,7 @@
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationCategoryType</key>
<string/>
<string></string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>itms-apps</string>
......@@ -85,8 +85,6 @@
</dict>
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) Wallet needs access to your Camera to scan QR codes</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>$(PRODUCT_NAME) Wallet needs access to your Camera Roll to choose an avatar for your username</string>
<key>NSFaceIDUsageDescription</key>
<string>Enabling Face ID helps $(PRODUCT_NAME) Wallet keep your assets secure.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
......@@ -95,6 +93,8 @@
<string>$(PRODUCT_NAME) Wallet does not require access to your location.</string>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) Wallet does not require access to the microphone.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>$(PRODUCT_NAME) Wallet needs access to your Camera Roll to choose an avatar for your username</string>
<key>NSUbiquitousContainers</key>
<dict>
<key>iCloud.Uniswap</key>
......@@ -111,6 +111,8 @@
<array>
<string>TokenPriceConfigurationIntent</string>
</array>
<key>OneSignal_app_groups_key</key>
<string>group.com.uniswap.mobile.onesignal</string>
<key>OneSignal_suppress_launch_urls</key>
<true/>
<key>UIAppFonts</key>
......
......@@ -19,6 +19,7 @@ RCT_EXPORT_SWIFTUI_PROPERTY(mnemonicId, NSString, MnemonicDisplayView);
RCT_EXPORT_SWIFTUI_PROPERTY(copyText, NSString, MnemonicDisplayView);
RCT_EXPORT_SWIFTUI_PROPERTY(copiedText, NSString, MnemonicDisplayView);
RCT_EXPORT_SWIFTUI_CALLBACK(onHeightMeasured, RCTDirectEventBlock, MnemonicDisplayView)
RCT_EXPORT_SWIFTUI_CALLBACK(onEmptyMnemonic, RCTDirectEventBlock, MnemonicDisplayView)
- (UIView *)view {
MnemonicDisplayView *proxy = [[MnemonicDisplayView alloc] init];
......
......@@ -34,6 +34,14 @@ import SwiftUI
}
}
}
var onEmptyMnemonic: RCTDirectEventBlock? {
didSet {
vc.rootView.props.onEmptyMnemonic = { [weak self] mnemonicId in
self?.onEmptyMnemonic?(["mnemonicId": mnemonicId])
}
}
}
var view: UIView {
vc.view.backgroundColor = .clear
......@@ -47,6 +55,7 @@ class MnemonicDisplayProps: ObservableObject {
@Published var copiedText: String = ""
@Published var mnemonicWords: [String] = Array(repeating: "", count: 12)
var onHeightMeasured: ((CGFloat) -> Void)?
var onEmptyMnemonic: ((String) -> Void)?
}
struct MnemonicDisplay: View {
......@@ -137,5 +146,10 @@ struct MnemonicDisplay: View {
}
}
)
.onAppear {
if props.mnemonicWords.isEmpty || props.mnemonicWords.allSatisfy({ $0.isEmpty }) {
props.onEmptyMnemonic?(props.mnemonicId)
}
}
}
}
......@@ -27,8 +27,8 @@
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.$(PRODUCT_NAME).onesignal</string>
<string>group.com.uniswap.widgets</string>
<string>group.com.uniswap.mobile.onesignal</string>
</array>
</dict>
</plist>
......@@ -101,3 +101,14 @@ jest.mock('react-native/Libraries/Linking/Linking', () => ({
jest.mock('openai')
jest.mock("react-native-bootsplash", () => {
return {
hide: jest.fn().mockResolvedValue(),
isVisible: jest.fn().mockResolvedValue(false),
useHideAnimation: jest.fn().mockReturnValue({
container: {},
logo: { source: 0 },
brand: { source: 0 },
}),
};
});
......@@ -19,9 +19,10 @@
"env:android:keystore:download": "bash ./scripts/downloadAndroidKeystore.sh",
"env:fastlane:download": "bash ./scripts/downloadFastlaneEnv.sh",
"env:fastlane:upload": "bash ./scripts/uploadFastlaneEnv.sh",
"env:local:download": "bash ../../scripts/downloadEnvLocal.sh mobile-local-envs ../../.env.defaults.local && yarn env:local:copy:swift",
"env:local:upload": "bash ../../scripts/uploadEnvLocal.sh mobile-local-envs ../../.env.defaults.local",
"env:local:download": "bash ../../scripts/downloadEnvLocal.sh xmznnx7ozuojy5lnohcmt73aee ../../.env.defaults.local && yarn env:local:copy:swift",
"env:local:upload": "bash ../../scripts/uploadEnvLocal.sh xmznnx7ozuojy5lnohcmt73aee ../../.env.defaults.local",
"env:local:copy:swift": "python3 scripts/copy_env_vars_to_swift.py",
"e2e": "maestro test \".maestro/flows/$*\"",
"e2e:packager": "DETOX_MODE=mocked yarn start",
"e2e:android:build:debug": "DETOX_MODE=mocked detox build -c android.emu.debug",
"e2e:android:test:debug": "detox test -c android.emu.debug",
......@@ -114,7 +115,6 @@
"expo-local-authentication": "13.8.0",
"expo-localization": "14.8.3",
"expo-screen-capture": "5.8.1",
"expo-splash-screen": "0.26.5",
"expo-store-review": "6.8.3",
"fuse.js": "6.5.3",
"i18next": "23.10.0",
......@@ -125,6 +125,7 @@
"react-i18next": "14.1.0",
"react-native": "0.73.6",
"react-native-appsflyer": "6.13.1",
"react-native-bootsplash": "6.3.1",
"react-native-context-menu-view": "1.15.0",
"react-native-device-info": "10.0.2",
"react-native-fast-image": "8.6.3",
......
......@@ -2,17 +2,17 @@
set -e
REQUIRED_XCODE_VERSION="16.1"
REQUIRED_XCODE_VERSION="16.2"
check_xcode_version() {
local current_version=$(xcodebuild -version | grep "Xcode" | cut -d' ' -f2)
if [ "$current_version" != "$REQUIRED_XCODE_VERSION" ]; then
echo "Error: Xcode version mismatch"
echo "Required: $REQUIRED_XCODE_VERSION"
echo "Current: $current_version"
exit 1
fi
echo "Xcode version check passed: $current_version"
local current_version=$(xcodebuild -version | grep "Xcode" | cut -d' ' -f2)
if [ "$current_version" != "$REQUIRED_XCODE_VERSION" ]; then
echo "Error: Xcode version mismatch"
echo "Required: $REQUIRED_XCODE_VERSION"
echo "Current: $current_version"
exit 1
fi
echo "Xcode version check passed: $current_version"
}
# Check Xcode version
......@@ -23,4 +23,3 @@ cd ios/
bundle install
bundle exec pod install
cd ..
import { ApolloProvider } from '@apollo/client'
import { loadDevMessages, loadErrorMessages } from '@apollo/client/dev'
import { DdRum, DdSdkReactNative } from '@datadog/mobile-react-native'
import { DdRum, DdSdkReactNative, RumActionType } from '@datadog/mobile-react-native'
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'
import { PerformanceProfiler, RenderPassReport } from '@shopify/react-native-performance'
import { MMKVWrapper } from 'apollo3-cache-persist'
import * as SplashScreen from 'expo-splash-screen'
import { default as React, StrictMode, useCallback, useEffect } from 'react'
import { default as React, StrictMode, useCallback, useEffect, useRef } from 'react'
import { I18nextProvider } from 'react-i18next'
import { LogBox, NativeModules, StatusBar } from 'react-native'
import appsFlyer from 'react-native-appsflyer'
......@@ -65,7 +64,7 @@ import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/conte
import i18n from 'uniswap/src/i18n'
import { CurrencyId } from 'uniswap/src/types/currency'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { isDetoxBuild } from 'utilities/src/environment/constants'
import { datadogEnabled, isDetoxBuild } from 'utilities/src/environment/constants'
import { attachUnhandledRejectionHandler, setAttributesToDatadog } from 'utilities/src/logger/Datadog'
import { registerConsoleOverrides } from 'utilities/src/logger/console'
import { logger } from 'utilities/src/logger/logger'
......@@ -93,9 +92,6 @@ if (__DEV__) {
loadErrorMessages()
}
// Keep the splash screen visible while we fetch resources until one of our landing pages loads
SplashScreen.preventAutoHideAsync().catch(() => undefined)
// Log boxes on simulators can block detox tap event when they cover buttons placed at
// the bottom of the screen and cause tests to fail.
if (isDetoxBuild) {
......@@ -185,8 +181,26 @@ function AppOuter(): JSX.Element | null {
customEndpoint,
reduxStore: store,
})
const jsBundleLoadedRef = useRef(false)
/**
* Function called by the @shopify/react-native-performance PerformanceProfiler that returns a
* RenderPassReport. We then forward this report to Datadog, Amplitude, etc.
*/
const onReportPrepared = useCallback(async (report: RenderPassReport) => {
if (datadogEnabled) {
const shouldLogJsBundleLoaded = report.timeToBootJsMillis && !jsBundleLoadedRef.current
if (shouldLogJsBundleLoaded) {
await DdRum.addAction(RumActionType.CUSTOM, 'application_start_js', {
loading_time: report.timeToBootJsMillis,
})
jsBundleLoadedRef.current = true
}
if (report.interactive) {
await DdRum.addTiming('screenInteractive')
}
}
const onReportPrepared = useCallback((report: RenderPassReport) => {
sendAnalyticsEvent(MobileEventName.PerformanceReport, report)
}, [])
......
......@@ -8,7 +8,7 @@ import { AccountList } from 'src/components/accounts/AccountList'
import { isCloudStorageAvailable } from 'src/features/CloudBackup/RNCloudStorageBackupsManager'
import { closeModal, openModal } from 'src/features/modals/modalSlice'
import { selectModalState } from 'src/features/modals/selectModalState'
import { Button, Flex, Text, TouchableArea, useSporeColors } from 'ui/src'
import { DeprecatedButton, Flex, Text, TouchableArea, useSporeColors } from 'ui/src'
import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions'
import { spacing } from 'ui/src/theme'
import { ActionSheetModal, MenuItemProp } from 'uniswap/src/components/modals/ActionSheetModal'
......@@ -268,9 +268,9 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
variant="subheading1"
/>
<Flex px="$spacing24">
<Button size="small" testID={TestID.WalletSettings} theme="secondary" onPress={onManageWallet}>
<DeprecatedButton size="small" testID={TestID.WalletSettings} theme="secondary" onPress={onManageWallet}>
{t('account.wallet.button.manage')}
</Button>
</DeprecatedButton>
</Flex>
</Flex>
<Flex maxHeight={fullScreenContentHeight / 2}>
......
......@@ -4,7 +4,7 @@ import { useDispatch } from 'react-redux'
import { navigate } from 'src/app/navigation/rootNavigation'
import { closeModal, openModal } from 'src/features/modals/modalSlice'
import { LockPreviewImage } from 'src/features/onboarding/LockPreviewImage'
import { Button, Flex, Text } from 'ui/src'
import { DeprecatedButton, Flex, Text } from 'ui/src'
import { Modal } from 'uniswap/src/components/modals/Modal'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants'
......@@ -60,7 +60,7 @@ export function BackupReminderModal(): JSX.Element {
</Flex>
<Flex row gap="$spacing8">
<Trace logPress element={ElementName.MaybeLaterButton} modal={ModalName.BackupReminder}>
<Button
<DeprecatedButton
alignSelf="center"
color="$neutral2"
flex={1}
......@@ -69,12 +69,12 @@ export function BackupReminderModal(): JSX.Element {
onPress={onPressMaybeLater}
>
{t('common.button.later')}
</Button>
</DeprecatedButton>
</Trace>
<Trace logPress element={ElementName.Continue} modal={ModalName.BackupReminder}>
<Button alignSelf="center" flex={1} size="medium" theme="primary" onPress={onPressBackup}>
<DeprecatedButton alignSelf="center" flex={1} size="medium" theme="primary" onPress={onPressBackup}>
{t('common.button.continue')}
</Button>
</DeprecatedButton>
</Trace>
</Flex>
</Flex>
......
......@@ -6,7 +6,7 @@ import { Action } from 'redux'
import { closeModal } from 'src/features/modals/modalSlice'
import { selectCustomEndpoint } from 'src/features/tweaks/selectors'
import { setCustomEndpoint } from 'src/features/tweaks/slice'
import { Accordion, Button, Flex, Text } from 'ui/src'
import { Accordion, DeprecatedButton, Flex, Text } from 'ui/src'
import { spacing } from 'ui/src/theme'
import { TextInput } from 'uniswap/src/components/input/TextInput'
import { Modal } from 'uniswap/src/components/modals/Modal'
......@@ -78,13 +78,13 @@ export function ExperimentsModal(): JSX.Element {
</Flex>
<Flex grow row alignItems="center" gap="$spacing16">
<Button flex={1} size="small" onPress={setEndpoint}>
<DeprecatedButton flex={1} size="small" onPress={setEndpoint}>
Set
</Button>
</DeprecatedButton>
<Button flex={1} size="small" onPress={clearEndpoint}>
<DeprecatedButton flex={1} size="small" onPress={clearEndpoint}>
Clear
</Button>
</DeprecatedButton>
</Flex>
</Accordion.Content>
</Accordion.Item>
......@@ -93,9 +93,9 @@ export function ExperimentsModal(): JSX.Element {
<AccordionHeader title="🚀 Apollo Cache" />
<Accordion.Content>
<Button flex={1} size="small" onPress={async (): Promise<unknown> => await apollo.resetStore()}>
<DeprecatedButton flex={1} size="small" onPress={async (): Promise<unknown> => await apollo.resetStore()}>
Reset Cache
</Button>
</DeprecatedButton>
</Accordion.Content>
</Accordion.Item>
......
......@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import { Action } from 'redux'
import { closeModal } from 'src/features/modals/modalSlice'
import { Button, Flex, Image, Text, useIsDarkMode, useSporeColors } from 'ui/src'
import { DeprecatedButton, Flex, Image, Text, useIsDarkMode, useSporeColors } from 'ui/src'
import { CEX_TRANSFER_MODAL_BG_DARK, CEX_TRANSFER_MODAL_BG_LIGHT } from 'ui/src/assets'
import { Modal } from 'uniswap/src/components/modals/Modal'
import { uniswapUrls } from 'uniswap/src/constants/urls'
......@@ -39,14 +39,14 @@ export function KoreaCexTransferInfoModal(): JSX.Element {
{t('fiatOnRamp.cexTransferModal.description')}
</Text>
</Flex>
<Button
<DeprecatedButton
color="$neutral1"
mt="$spacing8"
theme="secondary"
onPress={() => openURL(uniswapUrls.helpArticleUrls.cexTransferKorea)}
>
{t('common.button.learn')}
</Button>
</DeprecatedButton>
</Flex>
</Modal>
)
......
......@@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import { navigate } from 'src/app/navigation/rootNavigation'
import { closeModal, openModal } from 'src/features/modals/modalSlice'
import { Button, Flex, Text, useIsDarkMode } from 'ui/src'
import { DeprecatedButton, Flex, Text, useIsDarkMode } from 'ui/src'
import ViewOnlyWalletDark from 'ui/src/assets/graphics/view-only-wallet-dark.svg'
import ViewOnlyWalletLight from 'ui/src/assets/graphics/view-only-wallet-light.svg'
import { Modal } from 'uniswap/src/components/modals/Modal'
......@@ -53,10 +53,16 @@ export function ViewOnlyExplainerModal(): JSX.Element {
</Flex>
</Flex>
<Flex gap="$spacing8">
<Button alignSelf="center" borderRadius="$rounded20" px={40} theme="primary" onPress={onPressImportWallet}>
<DeprecatedButton
alignSelf="center"
borderRadius="$rounded20"
px={40}
theme="primary"
onPress={onPressImportWallet}
>
{t('account.wallet.viewOnly.button')}
</Button>
<Button
</DeprecatedButton>
<DeprecatedButton
alignSelf="center"
backgroundColor={undefined}
borderRadius="$rounded20"
......@@ -66,7 +72,7 @@ export function ViewOnlyExplainerModal(): JSX.Element {
onPress={onClose}
>
{t('common.button.later')}
</Button>
</DeprecatedButton>
</Flex>
</Flex>
</Modal>
......
......@@ -64,6 +64,7 @@ import { SettingsCloudBackupPasswordConfirmScreen } from 'src/screens/SettingsCl
import { SettingsCloudBackupPasswordCreateScreen } from 'src/screens/SettingsCloudBackupPasswordCreateScreen'
import { SettingsCloudBackupProcessingScreen } from 'src/screens/SettingsCloudBackupProcessingScreen'
import { SettingsCloudBackupStatus } from 'src/screens/SettingsCloudBackupStatus'
import { SettingsNotificationsScreen } from 'src/screens/SettingsNotificationsScreen'
import { SettingsPrivacyScreen } from 'src/screens/SettingsPrivacyScreen'
import { SettingsScreen } from 'src/screens/SettingsScreen'
import { SettingsViewSeedPhraseScreen } from 'src/screens/SettingsViewSeedPhraseScreen'
......@@ -133,6 +134,7 @@ function SettingsStackGroup(): JSX.Element {
<SettingsStack.Screen component={SettingsCloudBackupStatus} name={MobileScreens.SettingsCloudBackupStatus} />
<SettingsStack.Screen component={SettingsAppearanceScreen} name={MobileScreens.SettingsAppearance} />
<SettingsStack.Screen component={SettingsPrivacyScreen} name={MobileScreens.SettingsPrivacy} />
<SettingsStack.Screen component={SettingsNotificationsScreen} name={MobileScreens.SettingsNotifications} />
</SettingsStack.Navigator>
)
}
......
......@@ -62,6 +62,7 @@ export type SettingsStackParamList = {
[MobileScreens.SettingsCloudBackupStatus]: { address: Address }
[MobileScreens.SettingsHelpCenter]: undefined
[MobileScreens.SettingsLanguage]: undefined
[MobileScreens.SettingsNotifications]: undefined
[MobileScreens.SettingsPrivacy]: undefined
[MobileScreens.SettingsViewSeedPhrase]: { address: Address; walletNeedsRestore?: boolean }
[MobileScreens.SettingsWallet]: { address: Address }
......
......@@ -4,7 +4,7 @@ import { Storage, persistReducer, persistStore } from 'redux-persist'
import { MOBILE_STATE_VERSION, migrations } from 'src/app/migrations'
import { MobileState, mobilePersistedStateList, mobileReducer } from 'src/app/mobileReducer'
import { rootMobileSaga } from 'src/app/saga'
import { getFiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api'
import { fiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api'
import { isNonJestDev } from 'utilities/src/environment/constants'
import { createDatadogReduxEnhancer } from 'utilities/src/logger/Datadog'
import { createStore } from 'wallet/src/state'
......@@ -51,7 +51,7 @@ if (isNonJestDev) {
enhancers.push(reactotron.createEnhancer())
}
const middlewares: Middleware[] = [getFiatOnRampAggregatorApi().middleware]
const middlewares: Middleware[] = [fiatOnRampAggregatorApi.middleware]
export const setupStore = (
preloadedState?: PreloadedState<MobileState>,
......
import React, { PropsWithChildren, ReactElement, memo, useCallback, useMemo, useState } from 'react'
import React, { PropsWithChildren, ReactElement, memo, useCallback, useEffect, useMemo, useState } from 'react'
import { I18nManager } from 'react-native'
import { SharedValue, useDerivedValue } from 'react-native-reanimated'
import { LineChart, LineChartProvider } from 'react-native-wagmi-charts'
......@@ -25,6 +25,7 @@ import { ElementNameType } from 'uniswap/src/features/telemetry/constants'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { CurrencyId } from 'uniswap/src/types/currency'
import { isDetoxBuild } from 'utilities/src/environment/constants'
import { logger } from 'utilities/src/logger/logger'
import { isAndroid } from 'utilities/src/platform'
type PriceTextProps = {
......@@ -102,6 +103,24 @@ export const PriceExplorerInner = memo(function _PriceExplorerInner(): JSX.Eleme
!isScreenNavigationReady,
)
// Log the number of points in the data
useEffect(() => {
if (data?.priceHistory) {
if (data.priceHistory.length < 10) {
logger.warn('PriceExplorer.tsx', 'PriceExplorerInner', 'Missing token details data points', {
currencyId,
duration: selectedDuration,
dataLength: data?.priceHistory?.length,
})
}
logger.info('PriceExplorer.tsx', 'PriceExplorerInner', 'Token details data length', {
currencyId,
duration: selectedDuration,
dataLength: data?.priceHistory?.length,
})
}
}, [data?.priceHistory, selectedDuration, currencyId])
const { hapticFeedback } = useHapticFeedback()
const { convertFiatAmount } = useLocalizationContext()
......
......@@ -8,7 +8,7 @@ import { launchImageLibrary } from 'react-native-image-picker'
import { FadeIn, FadeOut } from 'react-native-reanimated'
import { Defs, LinearGradient, Path, Rect, Stop, Svg } from 'react-native-svg'
import { Button, Flex, SpinningLoader, Text, ThemeName, TouchableArea, useSporeColors } from 'ui/src'
import { DeprecatedButton, Flex, SpinningLoader, Text, ThemeName, TouchableArea, useSporeColors } from 'ui/src'
import CameraScan from 'ui/src/assets/icons/camera-scan.svg'
import { Global, PhotoStacked } from 'ui/src/components/icons'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
......@@ -261,7 +261,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
</Flex>
{isWalletConnectModal && props.numConnections > 0 && (
<Button
<DeprecatedButton
fontFamily="$body"
icon={<Global color={colors.neutral2.val} />}
backgroundColor={colors.surface3.val}
......@@ -269,7 +269,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
onPress={props.onPressConnections}
>
{t('qrScanner.button.connections', { count: props.numConnections })}
</Button>
</DeprecatedButton>
)}
</Flex>
</Flex>
......
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Button, Flex, LabeledCheckbox, SpinningLoader, Text } from 'ui/src'
import { DeprecatedButton, Flex, LabeledCheckbox, SpinningLoader, Text } from 'ui/src'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
export function RemoveLastMnemonicWalletFooter({
......@@ -31,7 +31,7 @@ export function RemoveLastMnemonicWalletFooter({
/>
</Flex>
<Flex centered row mt="$spacing8">
<Button
<DeprecatedButton
fill
disabled={!checkBoxAccepted}
icon={inProgress ? <SpinningLoader color="$statusCritical" /> : undefined}
......@@ -40,7 +40,7 @@ export function RemoveLastMnemonicWalletFooter({
onPress={onPress}
>
{!inProgress ? t('account.wallet.button.remove') : undefined}
</Button>
</DeprecatedButton>
</Flex>
</>
)
......
......@@ -11,7 +11,7 @@ import { Delay } from 'src/components/layout/Delayed'
import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks'
import { closeModal } from 'src/features/modals/modalSlice'
import { selectModalState } from 'src/features/modals/selectModalState'
import { Button, Flex, SpinningLoader, Text, ThemeKeys, useSporeColors } from 'ui/src'
import { DeprecatedButton, Flex, SpinningLoader, Text, ThemeKeys, useSporeColors } from 'ui/src'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { iconSizes, opacify } from 'ui/src/theme'
import { Modal } from 'uniswap/src/components/modals/Modal'
......@@ -204,11 +204,11 @@ export function RemoveWalletModal(): JSX.Element | null {
{inProgress ? (
<AnimatedFlex style={animatedCancelButtonSpanStyles} />
) : (
<Button fill disabled={inProgress} theme="outline" onPress={onClose}>
<DeprecatedButton fill disabled={inProgress} theme="outline" onPress={onClose}>
{t('common.button.cancel')}
</Button>
</DeprecatedButton>
)}
<Button
<DeprecatedButton
fill
icon={inProgress ? <SpinningLoader color={`$${labelColor}`} /> : undefined}
testID={isRemovingRecoveryPhrase ? ElementName.Continue : ElementName.Remove}
......@@ -217,7 +217,7 @@ export function RemoveWalletModal(): JSX.Element | null {
onPress={onPress}
>
{inProgress ? undefined : actionButtonLabel}
</Button>
</DeprecatedButton>
</Flex>
)}
</Flex>
......
......@@ -6,7 +6,7 @@ import { useDispatch } from 'react-redux'
import { DappHeaderIcon } from 'src/components/Requests/DappHeaderIcon'
import { wcWeb3Wallet } from 'src/features/walletConnect/saga'
import { WalletConnectSession, removeSession } from 'src/features/walletConnect/walletConnectSlice'
import { Button, Flex, Text } from 'ui/src'
import { DeprecatedButton, Flex, Text } from 'ui/src'
import { iconSizes } from 'ui/src/theme'
import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo'
import { Modal } from 'uniswap/src/components/modals/Modal'
......@@ -94,12 +94,12 @@ export function DappConnectedNetworkModal({ session, onClose }: DappConnectedNet
</Flex>
</Flex>
<Flex centered row gap="$spacing16">
<Button fill theme="secondary" onPress={onClose}>
<DeprecatedButton fill theme="secondary" onPress={onClose}>
{t('common.button.close')}
</Button>
<Button fill theme="detrimental" onPress={onDisconnect}>
</DeprecatedButton>
<DeprecatedButton fill theme="detrimental" onPress={onDisconnect}>
{t('common.button.disconnect')}
</Button>
</DeprecatedButton>
</Flex>
</Flex>
</Modal>
......
......@@ -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 { DeprecatedButton, 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'
......@@ -184,12 +184,18 @@ function ModalFooter({
pt="$spacing12"
px="$spacing24"
>
<Button fill size="medium" testID={TestID.Cancel} theme="tertiary" onPress={onReject}>
<DeprecatedButton fill size="medium" testID={TestID.Cancel} theme="tertiary" onPress={onReject}>
{t('common.button.cancel')}
</Button>
<Button fill disabled={!confirmationEnabled} size="medium" testID={TestID.Confirm} onPress={onConfirm}>
</DeprecatedButton>
<DeprecatedButton
fill
disabled={!confirmationEnabled}
size="medium"
testID={TestID.Confirm}
onPress={onConfirm}
>
{confirmationButtonText ?? t('common.button.accept')}
</Button>
</DeprecatedButton>
</Flex>
</BottomSheetFooter>
)
......
......@@ -130,7 +130,7 @@ function RequestModal({ currRequest }: RequestModalProps): JSX.Element {
caption={t('walletConnect.request.warning.message')}
rejectText={t('common.button.dismiss')}
icon={
<EyeIcon color={colors.neutral2.get()} height={iconSizes.icon24} strokeWidth={1.5} width={iconSizes.icon24} />
<EyeIcon color={colors.neutral1.get()} height={iconSizes.icon24} strokeWidth={1.5} width={iconSizes.icon24} />
}
isOpen={!isRequestFromSignerAccount}
modalName={ModalName.WCViewOnlyWarning}
......
......@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import { navigate } from 'src/app/navigation/rootNavigation'
import { closeAllModals } from 'src/features/modals/modalSlice'
import { Button, Flex, Text, useSporeColors } from 'ui/src'
import { DeprecatedButton, Flex, Text, useSporeColors } from 'ui/src'
import { WalletFilled } from 'ui/src/components/icons'
import { iconSizes, opacify } from 'ui/src/theme'
import { Modal } from 'uniswap/src/components/modals/Modal'
......@@ -48,9 +48,9 @@ export function RestoreWalletModal(): JSX.Element | null {
{t('account.wallet.restore.description')}
</Text>
<Flex centered row gap="$spacing12" pt="$spacing12">
<Button fill testID={TestID.RestoreWallet} theme="primary" onPress={onRestore}>
<DeprecatedButton fill testID={TestID.RestoreWallet} theme="primary" onPress={onRestore}>
{t('common.button.restore')}
</Button>
</DeprecatedButton>
</Flex>
</Flex>
</Modal>
......
import { useMemo, useState } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSettingsStackNavigation } from 'src/app/navigation/types'
import { Button, Flex, Text, TouchableArea } from 'ui/src'
import { DeprecatedButton, Flex, Text, TouchableArea } from 'ui/src'
import { RotatableChevron } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
import { AccountType } from 'uniswap/src/features/accounts/types'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types'
import { useAccounts } from 'wallet/src/features/wallet/hooks'
import { useAccountsList } from 'wallet/src/features/wallet/hooks'
const DEFAULT_ACCOUNTS_TO_DISPLAY = 6
export function WalletSettings(): JSX.Element {
const { t } = useTranslation()
const navigation = useSettingsStackNavigation()
const addressToAccount = useAccounts()
const allAccounts = useAccountsList()
const [showAll, setShowAll] = useState(false)
const allAccounts = useMemo(() => {
const accounts = Object.values(addressToAccount)
const _mnemonicWallets = accounts
.filter((a): a is SignerMnemonicAccount => a.type === AccountType.SignerMnemonic)
.sort((a, b) => {
return a.derivationIndex - b.derivationIndex
})
const _viewOnlyWallets = accounts
.filter((a) => a.type === AccountType.Readonly)
.sort((a, b) => {
return a.timeImportedMs - b.timeImportedMs
})
return [..._mnemonicWallets, ..._viewOnlyWallets]
}, [addressToAccount])
const toggleViewAll = (): void => {
setShowAll(!showAll)
}
......@@ -74,11 +58,11 @@ export function WalletSettings(): JSX.Element {
)
})}
{allAccounts.length > DEFAULT_ACCOUNTS_TO_DISPLAY && (
<Button theme="tertiary" onPress={toggleViewAll}>
<DeprecatedButton theme="tertiary" onPress={toggleViewAll}>
<Text color="$neutral1" variant="buttonLabel2">
{showAll ? t('settings.section.wallet.button.viewLess') : t('settings.section.wallet.button.viewAll')}
</Text>
</Button>
</DeprecatedButton>
)}
</Flex>
)
......
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useTokenDetailsContext } from 'src/components/TokenDetails/TokenDetailsContext'
import { Button, Flex, useSporeColors } from 'ui/src'
import { DeprecatedButton, Flex, GeneratedIcon, useSporeColors } from 'ui/src'
import { SwapCoin } from 'ui/src/components/icons'
import { opacify, validColor } from 'ui/src/theme'
import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { useTokenBasicProjectPartsFragment } from 'uniswap/src/data/graphql/uniswap-data-api/fragments'
import { TokenList } from 'uniswap/src/features/dataApi/types'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ElementName, ElementNameType, SectionName } from 'uniswap/src/features/telemetry/constants'
import { TestID, TestIDType } from 'uniswap/src/test/fixtures/testIDs'
......@@ -19,6 +22,7 @@ function CTAButton({
testID,
tokenColor,
disabled,
icon,
}: {
title: string
element: ElementNameType
......@@ -27,12 +31,14 @@ function CTAButton({
testID?: TestIDType
tokenColor?: Maybe<string>
disabled?: boolean
icon?: GeneratedIcon
}): JSX.Element {
const colors = useSporeColors()
return (
<Trace logPress element={element} section={SectionName.TokenDetails}>
<Button
<DeprecatedButton
fill
icon={icon}
opacity={disabled ? 0.5 : 1}
color={tokenColor ? getContrastPassingTextColor(tokenColor) : '$white'}
pressStyle={{ backgroundColor: validColor(opacify(60, tokenColor ?? colors.accent1.val)) }}
......@@ -42,7 +48,7 @@ function CTAButton({
onPress={disabled ? onPressDisabled : onPress}
>
{title}
</Button>
</DeprecatedButton>
</Trace>
)
}
......@@ -50,15 +56,18 @@ function CTAButton({
export function TokenDetailsActionButtons({
onPressBuy,
onPressSell,
onPressSwap,
onPressDisabled,
userHasBalance,
}: {
onPressBuy: () => void
onPressSell: () => void
onPressSwap: () => void
onPressDisabled?: () => void
userHasBalance: boolean
}): JSX.Element {
const { t } = useTranslation()
const isOffRampEnabled = useFeatureFlag(FeatureFlags.FiatOffRamp)
const { currencyId, currencyInfo, isChainEnabled, tokenColor } = useTokenDetailsContext()
......@@ -80,25 +89,40 @@ export function TokenDetailsActionButtons({
pt="$spacing12"
px="$spacing16"
>
<CTAButton
disabled={disabled}
element={ElementName.Buy}
testID={TestID.TokenDetailsBuyButton}
title={t('common.button.buy')}
tokenColor={tokenColor}
onPress={onPressBuy}
onPressDisabled={onPressDisabled}
/>
{userHasBalance && (
{isOffRampEnabled ? (
<CTAButton
disabled={disabled}
element={ElementName.Sell}
testID={TestID.TokenDetailsSellButton}
title={t('common.button.sell')}
element={ElementName.Swap}
testID={TestID.TokenDetailsSwapButton}
title={t('common.button.swap')}
tokenColor={tokenColor}
onPress={onPressSell}
icon={SwapCoin}
onPress={onPressSwap}
onPressDisabled={onPressDisabled}
/>
) : (
<>
<CTAButton
disabled={disabled}
element={ElementName.Buy}
testID={TestID.TokenDetailsBuyButton}
title={t('common.button.buy')}
tokenColor={tokenColor}
onPress={onPressBuy}
onPressDisabled={onPressDisabled}
/>
{userHasBalance && (
<CTAButton
disabled={disabled}
element={ElementName.Sell}
testID={TestID.TokenDetailsSellButton}
title={t('common.button.sell')}
tokenColor={tokenColor}
onPress={onPressSell}
onPressDisabled={onPressDisabled}
/>
)}
</>
)}
</Flex>
)
......
......@@ -7,7 +7,7 @@ import { navigate } from 'src/app/navigation/rootNavigation'
import { openModal } from 'src/features/modals/modalSlice'
import { removePendingSession } from 'src/features/walletConnect/walletConnectSlice'
import { Flex, Text, TouchableArea } from 'ui/src'
import { CopyAlt, ScanHome, Settings } from 'ui/src/components/icons'
import { CopyAlt, ScanHome, SettingsHome } from 'ui/src/components/icons'
import { AccountType } from 'uniswap/src/features/accounts/types'
import { useAvatar } from 'uniswap/src/features/address/avatar'
import { pushNotification } from 'uniswap/src/features/notifications/slice'
......@@ -66,7 +66,7 @@ const RotatingSettingsIcon = ({ onPressSettings }: { onPressSettings(): void }):
return (
<GestureDetector gesture={tap}>
<Animated.View style={animatedStyle}>
<Settings color="$neutral2" size="$icon.24" />
<SettingsHome color="$neutral2" size="$icon.28" />
</Animated.View>
</GestureDetector>
)
......@@ -118,7 +118,7 @@ export function AccountHeader(): JSX.Element {
}
}
const onPressScan = useCallback(async () => {
// in case we received a pending session from a previous scan after closing modal
// in case we received a pending session from a previous scan after closing modal.
dispatch(removePendingSession())
dispatch(openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.ScanQr }))
}, [dispatch])
......@@ -166,12 +166,7 @@ export function AccountHeader(): JSX.Element {
</TouchableArea>
</Flex>
) : (
<TouchableArea
alignSelf="center"
hitSlop={20}
testID={TestID.AccountHeaderCopyAddress}
onPress={onPressCopyAddress}
>
<TouchableArea hitSlop={20} testID={TestID.AccountHeaderCopyAddress} onPress={onPressCopyAddress}>
<Flex centered row shrink gap="$spacing4">
<Text adjustsFontSizeToFit color="$neutral1" numberOfLines={1} variant="subheading2">
{sanitizeAddressText(shortenAddress(activeAddress))}
......@@ -181,9 +176,9 @@ export function AccountHeader(): JSX.Element {
</TouchableArea>
)}
</Flex>
<Flex row alignItems="flex-start" gap="$spacing16" pt="$spacing4">
<Flex row alignItems="flex-start" gap="$spacing16">
<TouchableArea scaleTo={SCAN_ICON_ACTIVE_SCALE} activeOpacity={1} onPress={onPressScan}>
<ScanHome color="$neutral2" size="$icon.24" />
<ScanHome color="$neutral2" size="$icon.28" />
</TouchableArea>
<RotatingSettingsIcon onPressSettings={onPressSettings} />
</Flex>
......
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Button, useSporeColors } from 'ui/src'
import { DeprecatedButton, useSporeColors } from 'ui/src'
import CheckCircle from 'ui/src/assets/icons/check-circle.svg'
import CopySheets from 'ui/src/assets/icons/copy-sheets.svg'
import { iconSizes } from 'ui/src/theme'
......@@ -39,8 +39,8 @@ export function CopyTextButton({ copyText }: Props): JSX.Element {
useTimeout(resetIsCopied, RESET_COPY_STATE_DELAY)
return (
<Button icon={isCopied ? copiedIcon : copyIcon} theme="tertiary" onPress={onPress}>
<DeprecatedButton icon={isCopied ? copiedIcon : copyIcon} theme="tertiary" onPress={onPress}>
{isCopied ? t('common.button.copied') : t('common.button.copy')}
</Button>
</DeprecatedButton>
)
}
......@@ -11,6 +11,7 @@ import {
ethToken,
tokenMarket,
tokenProject,
tokenProjectMarket,
} from 'uniswap/src/test/fixtures'
import { queryResolvers } from 'uniswap/src/test/utils'
import { getSymbolDisplayText } from 'uniswap/src/utils/currency'
......@@ -31,7 +32,16 @@ jest.mock('@react-navigation/native', () => {
const mockStore = configureMockStore()
const favoriteToken = ethToken({
project: tokenProject(),
project: {
...tokenProject(),
markets: [
{
...tokenProjectMarket(),
price: amount({ value: 76543.21 }),
pricePercentChange24h: amount({ value: 6.54 }),
},
],
},
market: tokenMarket({
price: amount({ value: 12345.67 }),
pricePercentChange: amount({ value: 4.56 }),
......@@ -79,8 +89,8 @@ describe('FavoriteTokenCard', () => {
describe('when token data is available', () => {
const cases = [
{ test: 'symbol', value: getSymbolDisplayText(favoriteToken.symbol)! },
{ test: 'price', value: '$12,345.67' },
{ test: 'relative price change', value: '4.56%' },
{ test: 'price', value: '$76,543.21' },
{ test: 'relative price change', value: '6.54%' },
]
it.each(cases)('renders correct $test', async ({ value }) => {
......@@ -91,6 +101,22 @@ describe('FavoriteTokenCard', () => {
})
})
it('falls back to token price if token project price is not available', async () => {
const { resolvers: modifiedResolvers } = queryResolvers({
token: () => ({
...favoriteToken,
project: { ...favoriteToken.project, markets: [] },
}),
})
const { queryByText } = render(<FavoriteTokenCard {...defaultProps} />, { resolvers: modifiedResolvers })
await waitFor(() => {
expect(queryByText('$12,345.67')).toBeTruthy()
expect(queryByText('4.56%')).toBeTruthy()
})
})
it('navigates to the token details screen when pressed', async () => {
const { findByTestId } = render(<FavoriteTokenCard {...defaultProps} />, { resolvers })
......
......@@ -14,7 +14,10 @@ import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { borderRadii, imageSizes, opacify } from 'ui/src/theme'
import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo'
import { PollingInterval } from 'uniswap/src/constants/misc'
import { useFavoriteTokenCardQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import {
FavoriteTokenCardQuery,
useFavoriteTokenCardQuery,
} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { fromGraphQLChain } from 'uniswap/src/features/chains/utils'
import { currencyIdToContractInput } from 'uniswap/src/features/dataApi/utils'
......@@ -55,7 +58,7 @@ function FavoriteTokenCard({
const { data, networkStatus, startPolling, stopPolling } = useFavoriteTokenCardQuery({
variables: currencyIdToContractInput(currencyId),
// Rely on cache for fast favoriting UX, and poll for updates.
fetchPolicy: 'cache-first',
fetchPolicy: 'cache-and-network',
returnPartialData: true,
})
......@@ -66,8 +69,10 @@ function FavoriteTokenCard({
// Mirror behavior in top tokens list, use first chain the token is on for the symbol
const chainId = fromGraphQLChain(token?.chain) ?? defaultChainId
const price = convertFiatAmountFormatted(token?.market?.price?.value, NumberType.FiatTokenPrice)
const pricePercentChange = token?.market?.pricePercentChange?.value
// Coingecko price is more accurate but lacks long tail tokens
// Uniswap price comes from Uniswap pools, which may be updated less frequently
const { price, pricePercentChange } = getCoingeckoPrice(token) ?? getUniswapPrice(token)
const priceFormatted = convertFiatAmountFormatted(price, NumberType.FiatTokenPrice)
const onRemove = useCallback(() => {
if (currencyId) {
......@@ -139,7 +144,7 @@ function FavoriteTokenCard({
</Flex>
<Flex gap="$spacing2">
<Text adjustsFontSizeToFit numberOfLines={1} variant="heading3">
{price}
{priceFormatted}
</Text>
<RelativeChange
arrowSize="$icon.16"
......@@ -155,4 +160,29 @@ function FavoriteTokenCard({
)
}
function getCoingeckoPrice(token?: FavoriteTokenCardQuery['token']): {
price: number | undefined
pricePercentChange: number | undefined
} | null {
const market = token?.project?.markets?.[0]
if (!market?.price?.value || !market?.pricePercentChange24h?.value) {
return null
}
return {
price: market.price.value,
pricePercentChange: market.pricePercentChange24h.value,
}
}
function getUniswapPrice(token?: FavoriteTokenCardQuery['token']): {
price: number | undefined
pricePercentChange: number | undefined
} {
return {
price: token?.market?.price?.value,
pricePercentChange: token?.market?.pricePercentChange?.value,
}
}
export default memo(FavoriteTokenCard)
......@@ -85,10 +85,10 @@ export function SearchEmptySection({ selectedChain }: { selectedChain: UniverseC
</Flex>
</AnimatedFlex>
<WarningModal
backgroundIconColor={colors.surface2.get()}
backgroundIconColor={colors.surface3.get()}
caption={t('explore.search.section.popularTokenInfo')}
rejectText={t('common.button.close')}
icon={<Star color="$neutral2" size="$icon.24" />}
icon={<Star color="$neutral1" size="$icon.24" />}
isOpen={showPopularInfo}
modalName={ModalName.NetworkFeeInfo}
severity={WarningSeverity.None}
......
import React, { useMemo } from 'react'
import { ListRenderItemInfo } from 'react-native'
import { FlatList } from 'react-native-gesture-handler'
import { SEARCH_ITEM_PX, SEARCH_ITEM_PY } from 'src/components/explore/search/constants'
import { SearchNFTCollectionItem } from 'src/components/explore/search/items/SearchNFTCollectionItem'
import { getSearchResultId, gqlNFTToNFTCollectionSearchResult } from 'src/components/explore/search/utils'
import { Flex, Loader } from 'ui/src'
......@@ -26,7 +27,7 @@ export function SearchPopularNFTCollections(): JSX.Element {
if (loading) {
return (
<Flex px="$spacing24" py="$spacing8">
<Flex px={SEARCH_ITEM_PX} py={SEARCH_ITEM_PY}>
<Loader.Token repeat={2} />
</Flex>
)
......
......@@ -14,6 +14,11 @@ query SearchPopularTokens {
protectionInfo {
result
attackTypes
blockaidFees {
buy
sell
transfer
}
}
}
# `topTokens` returns WETH rather than ETH
......@@ -33,6 +38,11 @@ query SearchPopularTokens {
protectionInfo {
result
attackTypes
blockaidFees {
buy
sell
transfer
}
}
}
}
......@@ -2,6 +2,7 @@ import { TokenRankingsStat } from '@uniswap/client-explore/dist/uniswap/explore/
import React, { useMemo } from 'react'
import { ListRenderItemInfo } from 'react-native'
import { FlatList } from 'react-native-gesture-handler'
import { SEARCH_ITEM_PX, SEARCH_ITEM_PY } from 'src/components/explore/search/constants'
import { SearchTokenItem } from 'src/components/explore/search/items/SearchTokenItem'
import { getSearchResultId } from 'src/components/explore/search/utils'
import { Flex, Loader } from 'ui/src'
......@@ -60,7 +61,7 @@ export function SearchPopularTokens({ selectedChain }: { selectedChain: Universe
if (isLoading) {
return (
<Flex px="$spacing24" py="$spacing8">
<Flex px={SEARCH_ITEM_PX} py={SEARCH_ITEM_PY}>
<Loader.Token repeat={2} />
</Flex>
)
......
import React from 'react'
import { NFTHeaderItem, TokenHeaderItem, WalletHeaderItem } from 'src/components/explore/search/constants'
import {
NFTHeaderItem,
SEARCH_ITEM_PX,
TokenHeaderItem,
WalletHeaderItem,
} from 'src/components/explore/search/constants'
import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader'
import { SearchHeader } from 'src/components/explore/search/types'
import { Flex, Loader } from 'ui/src'
......@@ -9,7 +14,7 @@ function SectionLoader({ searchHeader, repeat = 1 }: { searchHeader: SearchHeade
return (
<Flex gap="$spacing12">
<SectionHeaderText icon={searchHeader.icon} title={searchHeader.title} />
<Flex mx="$spacing24">
<Flex px={SEARCH_ITEM_PX}>
<Loader.SearchResult repeat={repeat} />
</Flex>
</Flex>
......
import { SearchHeader, SearchHeaderKey } from 'src/components/explore/search/types'
import { Coin, Gallery, Person } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
import { getChainInfo } from 'uniswap/src/features/chains/chainInfo'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import i18n from 'uniswap/src/i18n'
export const SEARCH_RESULT_HEADER_KEY: SearchHeaderKey = 'header'
export const SEARCH_ITEM_PX = '$spacing20'
export const SEARCH_ITEM_PY = '$spacing8'
export const SEARCH_ITEM_ICON_SIZE = iconSizes.icon36
const ICON_SIZE = '$icon.24'
const ICON_COLOR = '$neutral2'
......
import React from 'react'
import { useTranslation } from 'react-i18next'
import { SEARCH_ITEM_ICON_SIZE, SEARCH_ITEM_PX, SEARCH_ITEM_PY } from 'src/components/explore/search/constants'
import { SearchWalletItemBase } from 'src/components/explore/search/items/SearchWalletItemBase'
import { Flex, Text } from 'ui/src'
import { imageSizes } from 'ui/src/theme'
import { useENSAvatar, useENSName } from 'uniswap/src/features/ens/api'
import { getCompletedENSName } from 'uniswap/src/features/ens/useENS'
import { SearchContext } from 'uniswap/src/features/search/SearchContext'
......@@ -48,8 +48,8 @@ export function SearchENSAddressItem({ searchResult, searchContext }: SearchENSA
return (
<SearchWalletItemBase searchContext={searchContext} searchResult={searchResult}>
<Flex row alignItems="center" gap="$spacing12" px="$spacing24" py="$spacing12">
<AccountIcon address={address} avatarUri={avatar} size={imageSizes.image40} />
<Flex row alignItems="center" gap="$spacing12" px={SEARCH_ITEM_PX} py={SEARCH_ITEM_PY}>
<AccountIcon address={address} avatarUri={avatar} size={SEARCH_ITEM_ICON_SIZE} />
<Flex shrink>
<Text ellipsizeMode="tail" numberOfLines={1} testID={`address-display/name/${ensName}`} variant="body1">
{completedENSName || formattedAddress}
......
import { default as React } from 'react'
import { useDispatch } from 'react-redux'
import { SEARCH_ITEM_ICON_SIZE, SEARCH_ITEM_PX, SEARCH_ITEM_PY } from 'src/components/explore/search/constants'
import { getBlockExplorerIcon } from 'src/components/icons/BlockExplorerIcon'
import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src'
import { Arrow } from 'ui/src/components/arrow/Arrow'
......@@ -36,9 +37,16 @@ export function SearchEtherscanItem({ etherscanResult }: SearchEtherscanItemProp
return (
<TouchableArea testID={TestID.SearchEtherscanItem} onPress={onPressViewEtherscan}>
<Flex row alignItems="center" gap="$spacing12" justifyContent="space-between" px="$spacing24" py="$spacing12">
<Flex
row
alignItems="center"
gap="$spacing12"
justifyContent="space-between"
px={SEARCH_ITEM_PX}
py={SEARCH_ITEM_PY}
>
<Flex centered row gap="$spacing12">
<EtherscanIcon size="$icon.40" />
<EtherscanIcon size={SEARCH_ITEM_ICON_SIZE} />
<Text variant="body1">{shortenAddress(address)}</Text>
</Flex>
<Arrow color={colors.neutral2.val} direction="ne" size={iconSizes.icon24} />
......
import { default as React } from 'react'
import { useDispatch } from 'react-redux'
import { useAppStackNavigation } from 'src/app/navigation/types'
import { SEARCH_ITEM_ICON_SIZE, SEARCH_ITEM_PX, SEARCH_ITEM_PY } from 'src/components/explore/search/constants'
import { Flex, Text, TouchableArea } from 'ui/src'
import { Verified } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
import { SearchContext } from 'uniswap/src/features/search/SearchContext'
import { NFTCollectionSearchResult, SearchResultType } from 'uniswap/src/features/search/SearchResult'
import { addToSearchHistory } from 'uniswap/src/features/search/searchHistorySlice'
......@@ -57,14 +57,14 @@ export function SearchNFTCollectionItem({ collection, searchContext }: NFTCollec
return (
<TouchableArea testID={TestID.SearchNFTCollectionItem} onPress={onPress}>
<Flex row alignItems="center" gap="$spacing8" justifyContent="flex-start" px="$spacing24" py="$spacing12">
<Flex row alignItems="center" gap="$spacing8" justifyContent="flex-start" px={SEARCH_ITEM_PX} py={SEARCH_ITEM_PY}>
<Flex
centered
borderRadius="$roundedFull"
height={iconSizes.icon40}
height={SEARCH_ITEM_ICON_SIZE}
mr="$spacing4"
overflow="hidden"
width={iconSizes.icon40}
width={SEARCH_ITEM_ICON_SIZE}
>
{imageUrl ? (
<NFTViewer uri={imageUrl} />
......
......@@ -3,8 +3,9 @@ import ContextMenu from 'react-native-context-menu-view'
import { useDispatch } from 'react-redux'
import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks'
import { useExploreTokenContextMenu } from 'src/components/explore/hooks'
import { SEARCH_ITEM_ICON_SIZE, SEARCH_ITEM_PX, SEARCH_ITEM_PY } from 'src/components/explore/search/constants'
import { disableOnPress } from 'src/utils/disableOnPress'
import { Flex, Text, TouchableArea } from 'ui/src'
import { Flex, Text, TouchableArea, useIsDarkMode } from 'ui/src'
import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo'
import WarningIcon from 'uniswap/src/components/warnings/WarningIcon'
import { getWarningIconColors } from 'uniswap/src/components/warnings/utils'
......@@ -18,6 +19,7 @@ import { getTokenWarningSeverity } from 'uniswap/src/features/tokens/safetyUtils
import { useCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { buildCurrencyId, buildNativeCurrencyId } from 'uniswap/src/utils/currencyId'
import { shortenAddress } from 'utilities/src/addresses'
type SearchTokenItemProps = {
token: TokenSearchResult
......@@ -25,6 +27,7 @@ type SearchTokenItemProps = {
}
export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps): JSX.Element {
const isDarkMode = useIsDarkMode()
const dispatch = useDispatch()
const tokenDetailsNavigation = useTokenDetailsNavigation()
......@@ -80,12 +83,18 @@ export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps):
onLongPress={disableOnPress}
onPress={onPress}
>
<Flex row alignItems="center" gap="$spacing12" px="$spacing24" py="$spacing12">
<TokenLogo chainId={chainId} name={name} symbol={symbol} url={logoUrl ?? undefined} />
<Flex row alignItems="center" gap="$spacing12" px={SEARCH_ITEM_PX} py={SEARCH_ITEM_PY}>
<TokenLogo
chainId={chainId}
name={name}
symbol={symbol}
url={logoUrl ?? undefined}
size={SEARCH_ITEM_ICON_SIZE}
/>
<Flex shrink alignItems="flex-start">
<Flex centered row gap="$spacing8">
<Flex shrink>
<Text color="$neutral1" numberOfLines={1} variant="body1">
<Text color="$neutral1" numberOfLines={1} variant="subheading1">
{name}
</Text>
</Flex>
......@@ -94,9 +103,16 @@ export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps):
)}
</Flex>
<Flex centered row gap="$spacing8">
<Text color="$neutral2" numberOfLines={1} variant="subheading2">
<Text color="$neutral2" numberOfLines={1} variant="body2">
{symbol}
</Text>
{address && (
<Flex shrink>
<Text color={isDarkMode ? '$neutral3' : '$neutral2'} numberOfLines={1} variant="body3">
{shortenAddress(address)}
</Text>
</Flex>
)}
</Flex>
</Flex>
</Flex>
......
import React from 'react'
import { SEARCH_ITEM_ICON_SIZE, SEARCH_ITEM_PX, SEARCH_ITEM_PY } from 'src/components/explore/search/constants'
import { SearchWalletItemBase } from 'src/components/explore/search/items/SearchWalletItemBase'
import { Flex, Text } from 'ui/src'
import { imageSizes } from 'ui/src/theme'
import { useAvatar } from 'uniswap/src/features/address/avatar'
import { SearchContext } from 'uniswap/src/features/search/SearchContext'
import { UnitagSearchResult } from 'uniswap/src/features/search/SearchResult'
......@@ -24,8 +24,8 @@ export function SearchUnitagItem({ searchResult, searchContext }: SearchUnitagIt
return (
<SearchWalletItemBase searchContext={searchContext} searchResult={searchResult}>
<Flex row alignItems="center" gap="$spacing12" px="$spacing24" py="$spacing12">
<AccountIcon address={address} avatarUri={avatar} size={imageSizes.image40} />
<Flex row alignItems="center" gap="$spacing12" px={SEARCH_ITEM_PX} py={SEARCH_ITEM_PY}>
<AccountIcon address={address} avatarUri={avatar} size={SEARCH_ITEM_ICON_SIZE} />
<Flex alignItems="flex-start" justifyContent="center">
<DisplayNameText includeUnitagSuffix displayName={displayName} textProps={{ variant: 'body1' }} />
<Text color="$neutral2" variant="body2">
......
import React from 'react'
import { SEARCH_ITEM_ICON_SIZE, SEARCH_ITEM_PX, SEARCH_ITEM_PY } from 'src/components/explore/search/constants'
import { SearchWalletItemBase } from 'src/components/explore/search/items/SearchWalletItemBase'
import { Flex, Text } from 'ui/src'
import { imageSizes } from 'ui/src/theme'
import { useENSAvatar, useENSName } from 'uniswap/src/features/ens/api'
import { SearchContext } from 'uniswap/src/features/search/SearchContext'
import { WalletByAddressSearchResult } from 'uniswap/src/features/search/SearchResult'
......@@ -25,8 +25,8 @@ export function SearchWalletByAddressItem({
return (
<SearchWalletItemBase searchContext={searchContext} searchResult={searchResult}>
<Flex row alignItems="center" gap="$spacing12" px="$spacing24" py="$spacing12">
<AccountIcon address={address} avatarUri={avatar} size={imageSizes.image40} />
<Flex row alignItems="center" gap="$spacing12" px={SEARCH_ITEM_PX} py={SEARCH_ITEM_PY}>
<AccountIcon address={address} avatarUri={avatar} size={SEARCH_ITEM_ICON_SIZE} />
<Flex shrink>
<Text ellipsizeMode="tail" numberOfLines={1} testID={`address-display/name/${ensName}`} variant="body1">
{ensName || formattedAddress}
......
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Button, SpinningLoader, useIsShortMobileDevice } from 'ui/src'
import { DeprecatedButton, SpinningLoader, useIsShortMobileDevice } from 'ui/src'
import { InfoCircleFilled } from 'ui/src/components/icons'
interface FiatOnRampCtaButtonProps {
......@@ -23,7 +23,7 @@ export function FiatOnRampCtaButton({
const buttonAvailable = eligible || isLoading
const continueText = eligible ? continueButtonText : t('fiatOnRamp.error.unsupported')
return (
<Button
<DeprecatedButton
color={buttonAvailable ? '$white' : '$neutral2'}
disabled={disabled}
icon={
......@@ -35,6 +35,6 @@ export function FiatOnRampCtaButton({
onPress={onPress}
>
{!isLoading && continueText}
</Button>
</DeprecatedButton>
)
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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