ci(release): publish latest release

parent c1daf021
IPFS hash of the deployment:
- CIDv0: `QmYsHGBHp98EzfBdS8e3inLYfg78s7iWwjJ6gcQwAC9pxH`
- CIDv1: `bafybeie4nwp4jxdsqscdvwbjj43pgjz5eta3keqy3wftdszlmacbbourwi`
- CIDv0: `QmPejsjfiS8N5GaQYKoMirK9GXMJCEhyTbCEsU6mpzooNJ`
- CIDv1: `bafybeiatpx5zjwrh3bsmjdrt77hlkjuztsnqm63dfzr4mf2nbbit6hwx5m`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
......@@ -10,15 +10,24 @@ You can also access the Uniswap Interface from an IPFS gateway.
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://bafybeie4nwp4jxdsqscdvwbjj43pgjz5eta3keqy3wftdszlmacbbourwi.ipfs.dweb.link/
- https://bafybeie4nwp4jxdsqscdvwbjj43pgjz5eta3keqy3wftdszlmacbbourwi.ipfs.cf-ipfs.com/
- [ipfs://QmYsHGBHp98EzfBdS8e3inLYfg78s7iWwjJ6gcQwAC9pxH/](ipfs://QmYsHGBHp98EzfBdS8e3inLYfg78s7iWwjJ6gcQwAC9pxH/)
- https://bafybeiatpx5zjwrh3bsmjdrt77hlkjuztsnqm63dfzr4mf2nbbit6hwx5m.ipfs.dweb.link/
- https://bafybeiatpx5zjwrh3bsmjdrt77hlkjuztsnqm63dfzr4mf2nbbit6hwx5m.ipfs.cf-ipfs.com/
- [ipfs://QmPejsjfiS8N5GaQYKoMirK9GXMJCEhyTbCEsU6mpzooNJ/](ipfs://QmPejsjfiS8N5GaQYKoMirK9GXMJCEhyTbCEsU6mpzooNJ/)
## 5.15.0 (2024-02-27)
## 5.16.0 (2024-02-28)
### Features
* **web:** [info] enable by default (#6623) (#6624) 7442c9e
* **web:** [info] enable by default (#6623) 0573173
* **web:** swap smarter banner (#6579) c715d1f
### Bug Fixes
* **web:** [info] remove markets from pool token (#6613) 4fdad80
* **web:** fix analytics for swap tab clicked and page name (#6600) f018e73
* **web:** fix math for calculating prices for small decimals (#6612) 59900ff
* **web:** Landing page style fixes at small heights (#6603) 0975cfa
web/5.15.0
\ No newline at end of file
web/5.16.0
\ No newline at end of file
......@@ -80,10 +80,10 @@
"@uniswap/ethers-rs-mobile": "0.0.5",
"@uniswap/sdk-core": "4.1.2",
"@uniswap/v3-sdk": "3.10.2",
"@walletconnect/core": "2.10.1",
"@walletconnect/react-native-compat": "2.10.1",
"@walletconnect/utils": "2.10.1",
"@walletconnect/web3wallet": "1.9.1",
"@walletconnect/core": "2.11.2",
"@walletconnect/react-native-compat": "2.11.2",
"@walletconnect/utils": "2.11.2",
"@walletconnect/web3wallet": "1.10.2",
"apollo3-cache-persist": "0.14.1",
"babel-plugin-transform-inline-environment-variables": "0.4.4",
"babel-plugin-transform-remove-console": "6.9.4",
......@@ -162,7 +162,7 @@
"@types/react-native": "0.71.3",
"@types/redux-mock-store": "1.0.6",
"@uniswap/eslint-config": "workspace:^",
"@walletconnect/types": "2.8.6",
"@walletconnect/types": "2.11.2",
"@welldone-software/why-did-you-render": "7.0.1",
"babel-jest": "29.6.1",
"babel-loader": "8.2.3",
......
......@@ -127,10 +127,10 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
})
const addWalletOptions = useMemo<MenuItemProp[]>(() => {
const onPressCreateNewWallet = async (): Promise<void> => {
const onPressCreateNewWallet = (): void => {
// Ensure no pending accounts
await dispatch(pendingAccountActions.trigger(PendingAccountActions.Delete))
await dispatch(createAccountActions.trigger())
dispatch(pendingAccountActions.trigger(PendingAccountActions.ActivateOneAndDelete))
dispatch(createAccountActions.trigger())
if (unitagsFeatureFlagEnabled) {
if (hasImportedSeedPhrase) {
......
......@@ -134,11 +134,15 @@ export const useModalContent = ({
</Trans>
),
description: (
<Text color="$neutral2" variant="body3">
<Trans t={t}>
It shares the same recovery phrase as{' '}
<Text color="$neutral1">{{ wallets: associatedAccountNames }}</Text>. Your recovery
phrase will remain stored until you delete all remaining wallets.
<Text color="$neutral1" variant="body3">
{{ wallets: associatedAccountNames }}
</Text>
. Your recovery phrase will remain stored until you delete all remaining wallets.
</Trans>
</Text>
),
Icon: TrashIcon,
iconColorLabel: 'statusCritical',
......
......@@ -35,10 +35,15 @@ export function DappConnectedNetworkModal({
const onDisconnect = async (): Promise<void> => {
try {
dispatch(removeSession({ account: address, sessionId: id }))
// Explicitly verify that WalletConnect has this session id as an active session
// It's possible that the session was already disconnected on WC but wasn't updated locally in redux
const sessions = wcWeb3Wallet.getActiveSessions()
if (sessions[session.id]) {
await wcWeb3Wallet.disconnectSession({
topic: id,
topic: session.id,
reason: getSdkError('USER_DISCONNECTED'),
})
}
dispatch(
pushNotification({
type: AppNotificationType.WalletConnect,
......
......@@ -39,10 +39,15 @@ export function DappConnectionItem({
const onDisconnect = async (): Promise<void> => {
try {
dispatch(removeSession({ account: address, sessionId: session.id }))
// Explicitly verify that WalletConnect has this session id as an active session
// It's possible that the session was already disconnected on WC but wasn't updated locally in redux
const sessions = wcWeb3Wallet.getActiveSessions()
if (sessions[session.id]) {
await wcWeb3Wallet.disconnectSession({
topic: session.id,
reason: getSdkError('USER_DISCONNECTED'),
})
}
dispatch(
pushNotification({
type: AppNotificationType.WalletConnect,
......
......@@ -6,6 +6,7 @@ import { Loader } from 'src/components/loading'
import { useFormatExactCurrencyAmount } from 'src/features/fiatOnRamp/hooks'
import { Flex, Icons, Text, TouchableArea, useIsDarkMode } from 'ui/src'
import { fonts, iconSizes } from 'ui/src/theme'
import { NumberType } from 'utilities/src/format/types'
import { FiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks'
import { FORQuote, FORServiceProvider } from 'wallet/src/features/fiatOnRamp/types'
import { getServiceProviderLogo } from 'wallet/src/features/fiatOnRamp/utils'
......@@ -29,7 +30,7 @@ export function FORQuoteItem({
showCarret,
active,
}: {
quote: FORQuote | undefined
quote: FORQuote
serviceProvider: FORServiceProvider | undefined
currency: Maybe<Currency>
loading: boolean
......@@ -39,17 +40,17 @@ export function FORQuoteItem({
active?: boolean
}): JSX.Element {
const { t } = useTranslation()
const { addFiatSymbolToNumber } = useLocalizationContext()
const { formatNumberOrString } = useLocalizationContext()
const quoteAmount = useFormatExactCurrencyAmount(
(quote?.destinationAmount || 0).toString(),
currency
)
const quoteEquivalentInSourceCurrencyAmount = addFiatSymbolToNumber({
value: quote?.sourceAmount || 0,
currencyCode: baseCurrency.code,
currencySymbol: baseCurrency.symbol,
const quoteEquivalentInSourceCurrencyAmount = formatNumberOrString({
value: quote.sourceAmount - quote.totalFee,
type: NumberType.FiatStandard,
currencyCode: baseCurrency.code.toLowerCase(),
})
const isDarkMode = useIsDarkMode()
......
import { useNavigation } from '@react-navigation/native'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Button, Flex, Icons, Text } from 'ui/src'
import { ActivityIndicator } from 'react-native'
import { Button, Flex, Icons, Text, useSporeColors } from 'ui/src'
import { fonts } from 'ui/src/theme'
import { logger } from 'utilities/src/logger/logger'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { pushNotification } from 'wallet/src/features/notifications/slice'
......@@ -23,13 +26,16 @@ export function DeleteUnitagModal({
onClose: () => void
}): JSX.Element {
const { t } = useTranslation()
const colors = useSporeColors()
const navigation = useNavigation()
const dispatch = useAppDispatch()
const { triggerRefetchUnitags } = useUnitagUpdater()
const account = useAccount(address)
const signerManager = useWalletSigners()
const [isDeleting, setIsDeleting] = useState(false)
const handleDeleteError = (): void => {
setIsDeleting(false)
dispatch(
pushNotification({
type: AppNotificationType.Error,
......@@ -41,11 +47,14 @@ export function DeleteUnitagModal({
const onDelete = async (): Promise<void> => {
try {
setIsDeleting(true)
const { data: deleteResponse } = await deleteUnitag({
username: unitag,
account,
signerManager,
})
setIsDeleting(false)
if (!deleteResponse?.success) {
handleDeleteError()
return
......@@ -92,8 +101,19 @@ export function DeleteUnitagModal({
)}
</Text>
<Flex centered row gap="$spacing12" pt="$spacing24">
<Button fill testID={ElementName.Remove} theme="detrimental" onPress={onDelete}>
{t('Delete')}
<Button
fill
disabled={isDeleting}
testID={ElementName.Remove}
theme="detrimental"
onPress={onDelete}>
{isDeleting ? (
<Flex height={fonts.buttonLabel1.lineHeight}>
<ActivityIndicator color={colors.sporeWhite.val} />
</Flex>
) : (
t('Delete')
)}
</Button>
</Flex>
</Flex>
......
......@@ -11,7 +11,10 @@ import {
useAppFiatCurrencyInfo,
useFiatCurrencyInfo,
} from 'wallet/src/features/fiatCurrency/hooks'
import { useFiatOnRampAggregatorCryptoQuoteQuery } from 'wallet/src/features/fiatOnRamp/api'
import {
useFiatOnRampAggregatorCryptoQuoteQuery,
useFiatOnRampAggregatorSupportedFiatCurrenciesQuery,
} from 'wallet/src/features/fiatOnRamp/api'
import { FORQuote } from 'wallet/src/features/fiatOnRamp/types'
import {
isFiatOnRampApiError,
......@@ -21,10 +24,7 @@ import {
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { useActiveAccountAddress } from 'wallet/src/features/wallet/hooks'
// TODO: https://linear.app/uniswap/issue/MOB-2532/implement-fetching-of-available-fiat-currencies-from-meld
const MELD_FIAT_CURRENCY_CODES = ['usd', 'eur']
export function useMeldFiatCurrencySupportInfo(): {
export function useMeldFiatCurrencySupportInfo(countryCode: string): {
appFiatCurrencySupportedInMeld: boolean
meldSupportedFiatCurrency: FiatCurrencyInfo
} {
......@@ -33,12 +33,22 @@ export function useMeldFiatCurrencySupportInfo(): {
const fallbackCurrencyInfo = useFiatCurrencyInfo(FiatCurrency.UnitedStatesDollar)
const appFiatCurrencyCode = appFiatCurrencyInfo.code.toLowerCase()
const appFiatCurrencySupported = MELD_FIAT_CURRENCY_CODES.includes(appFiatCurrencyCode)
const currency = appFiatCurrencySupported ? appFiatCurrencyInfo : fallbackCurrencyInfo
const { data: supportedFiatCurrencies } = useFiatOnRampAggregatorSupportedFiatCurrenciesQuery({
countryCode,
})
const appFiatCurrencySupported =
!supportedFiatCurrencies ||
supportedFiatCurrencies.fiatCurrencies.some(
(currency): boolean => appFiatCurrencyCode === currency.fiatCurrencyCode.toLowerCase()
)
const meldSupportedFiatCurrency = appFiatCurrencySupported
? appFiatCurrencyInfo
: fallbackCurrencyInfo
return {
appFiatCurrencySupportedInMeld: appFiatCurrencySupported,
meldSupportedFiatCurrency: currency,
meldSupportedFiatCurrency,
}
}
......@@ -105,11 +115,14 @@ export function useParseFiatOnRampError(
const { formatNumberOrString } = useLocalizationContext()
let errorText, errorColor: ColorTokens | undefined
if (!isFiatOnRampApiError(error)) {
if (!error) {
return { errorText, errorColor }
}
errorText = t('Something went wrong.')
errorColor = '$DEP_accentWarning'
if (isFiatOnRampApiError(error)) {
if (isInvalidRequestAmountTooLow(error)) {
const formattedAmount = formatNumberOrString({
value: error.data.context.minimumAllowed,
......@@ -126,9 +139,7 @@ export function useParseFiatOnRampError(
})
errorText = t('Maximum {{amount}}', { amount: formattedAmount })
errorColor = '$statusCritical'
} else {
errorText = t('Something went wrong.')
errorColor = '$DEP_accentWarning'
}
}
return { errorText, errorColor }
......
......@@ -43,12 +43,26 @@ export function ScantasticModal(): JSX.Element | null {
)
const pubKey: JsonWebKey = initialState?.pubKey ? JSON.parse(initialState?.pubKey) : undefined
const uuid = initialState?.uuid
const device = initialState?.vendor + ' ' + initialState?.model || ''
const device = (initialState?.vendor + ' ' + initialState?.model || '').trim()
const browser = initialState?.browser || ''
const [expired, setExpired] = useState(false)
const [redeemed, setRedeemed] = useState(false)
const [error, setError] = useState('')
const [expiryText, setExpiryText] = useState('')
const setExpirationText = useCallback(() => {
const timeLeft = expirationTimestamp - Date.now()
if (timeLeft <= 0) {
return setExpiryText(t('Expired'))
}
return setExpiryText(
t('Expires in {{duration}}', {
duration: getDurationRemainingString(expirationTimestamp),
})
)
}, [expirationTimestamp, t])
useInterval(setExpirationText, ONE_SECOND_MS)
if (redeemed) {
dispatch(
......@@ -63,14 +77,7 @@ export function ScantasticModal(): JSX.Element | null {
useEffect(() => {
const interval = setInterval(() => {
const timeLeft = expirationTimestamp - Date.now()
if (timeLeft <= 0) {
return setExpiryText(t('Expired'))
}
return setExpiryText(
t('New code in {{duration}}', {
duration: getDurationRemainingString(expirationTimestamp),
})
)
setExpired(timeLeft <= 0)
}, ONE_SECOND_MS)
return () => clearInterval(interval)
......@@ -81,16 +88,27 @@ export function ScantasticModal(): JSX.Element | null {
}, [dispatch])
const onEncryptSeedphrase = async (): Promise<void> => {
setError('')
let encryptedSeedphrase = ''
const { n, e } = pubKey
try {
if (!pubKey.n || !pubKey.e) {
throw new Error(t('Invalid public key'))
if (!n || !e) {
throw new Error(t('Invalid public key.'))
}
encryptedSeedphrase = await getEncryptedMnemonic(account?.address || '', pubKey.n, pubKey.e)
} catch (e) {
// TODO(EXT-485): improve error handling
logger.error(e, { tags: { file: 'ScantasticModal', function: 'getEncryptedMnemonic' } })
encryptedSeedphrase = await getEncryptedMnemonic(account?.address || '', n, e)
} catch (err) {
setError('Failed to prepare seed phrase.')
logger.error(err, {
tags: {
file: 'ScantasticModal',
function: 'onEncryptSeedphrase->getEncryptedMnemonic',
},
extra: {
address: account?.address,
n,
e,
},
})
}
try {
......@@ -108,18 +126,24 @@ export function ScantasticModal(): JSX.Element | null {
}),
})
if (!response.ok) {
throw new Error(t('Failed to send.'))
throw new Error(`Failed to post blob: ${await response.text()}`)
}
const data = await response.json()
if (!data?.otp) {
throw new Error(t('OTP unavailable'))
throw new Error('OTP unavailable')
} else {
setExpirationTimestamp(Date.now() + ONE_MINUTE_MS * 2)
setOTP(data.otp)
}
} catch (e) {
// TODO(EXT-485): improve error handling
logger.error(e, { tags: { file: 'ScantasticModal', function: 'fetch' } })
} catch (err) {
setError(t('No OTP received. Please try again.'))
logger.error(err, {
tags: {
file: 'ScantasticModal',
function: `onEncryptSeedphrase->fetch`,
},
extra: { uuid },
})
}
}
......@@ -153,12 +177,12 @@ export function ScantasticModal(): JSX.Element | null {
}
)
if (!response.ok) {
return
throw new Error(`Failed to check OTP state: ${await response.text()}`)
}
const data: OtpStateApiResponse = await response.json()
const otpState = data.otp
if (!otpState) {
return
throw new Error('No OTP state received.')
}
if (data.expiresAtInSeconds) {
setExpirationTimestamp(data.expiresAtInSeconds * ONE_SECOND_MS)
......@@ -170,7 +194,13 @@ export function ScantasticModal(): JSX.Element | null {
setExpired(true)
}
} catch (e) {
logger.warn('ScanToOnboard.tsx', 'checkOTPState', e as string)
logger.error(e, {
tags: {
file: 'ScantasticModal',
function: `checkOTPState`,
},
extra: { uuid },
})
}
}, [OTP, uuid])
......@@ -219,7 +249,7 @@ export function ScantasticModal(): JSX.Element | null {
<Text variant="heading1">{OTP.substring(0, 3).split('').join(' ')}</Text>
<Text variant="heading1">{OTP.substring(3).split('').join(' ')}</Text>
</Flex>
<Text color="$neutral3" variant="body3">
<Text color="$neutral3" variant="body2">
{expiryText}
</Text>
</Flex>
......@@ -227,6 +257,34 @@ export function ScantasticModal(): JSX.Element | null {
)
}
if (error) {
return (
<BottomSheetModal
backgroundColor={colors.surface1.get()}
name={ModalName.OtpScanInput}
onClose={onClose}>
<Flex centered gap="$spacing16" px="$spacing16" py="$spacing12">
<Flex centered backgroundColor="$accent2" borderRadius="$rounded12" p="$spacing12">
<Icons.AlertTriangle color="$statusCritical" size={iconSizes.icon24} />
</Flex>
<Text variant="subheading1">
<Trans>Error</Trans>
</Text>
<Text color="$neutral2" textAlign="center" variant="body3">
{error}
</Text>
<Flex flexDirection="column" gap="$spacing4" mt="$spacing12" width="100%">
<Button alignItems="center" theme="secondary" onPress={onClose}>
<Text variant="buttonLabel2">
<Trans>Close</Trans>
</Text>
</Button>
</Flex>
</Flex>
</BottomSheetModal>
)
}
return (
<BottomSheetModal
backgroundColor={colors.surface1.get()}
......
......@@ -32,7 +32,11 @@ import { LearnMoreLink } from 'wallet/src/components/text/LearnMoreLink'
import { Pill } from 'wallet/src/components/text/Pill'
import { uniswapUrls } from 'wallet/src/constants/urls'
import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types'
import { UNITAG_SUFFIX, UNITAG_SUFFIX_NO_LEADING_DOT } from 'wallet/src/features/unitags/constants'
import {
UNITAG_SUFFIX,
UNITAG_SUFFIX_NO_LEADING_DOT,
UNITAG_VALID_REGEX,
} from 'wallet/src/features/unitags/constants'
import { useCanClaimUnitagName } from 'wallet/src/features/unitags/hooks'
import { usePendingAccounts } from 'wallet/src/features/wallet/hooks'
import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry'
......@@ -63,7 +67,7 @@ export function ClaimUnitagScreen({ navigation, route }: Props): JSX.Element {
const { t } = useTranslation()
const colors = useSporeColors()
const inputPlaceholder = t('yourname')
const inputPlaceholder = getYourNameString(t('yourname'))
// In onboarding flow, delete pending accounts and create account actions happen right before navigation
// So pendingAccountAddress must be fetched in this component and can't be passed in params
......@@ -397,7 +401,7 @@ const InfoModal = ({
}): JSX.Element => {
const colors = useSporeColors()
const { t } = useTranslation()
const usernamePlaceholder = t('yourname')
const usernamePlaceholder = getYourNameString(t('yourname'))
return (
<WarningModal
......@@ -479,3 +483,14 @@ const ClaimPeriodInfoModal = ({
</WarningModal>
)
}
// Util to handle translations of `yourname`
// If translated string only contains valid Unitag characters, return it lowercased and without spaces
// Otherwise, return 'yourname'
const getYourNameString = (yourname: string): string => {
const noSpacesLowercase = yourname.replaceAll(' ', '').toLowerCase()
if (UNITAG_VALID_REGEX.test(noSpacesLowercase)) {
return noSpacesLowercase
}
return 'yourname'
}
......@@ -8,16 +8,16 @@ import { useAppDispatch, useShouldShowNativeKeyboard } from 'src/app/hooks'
import { FiatOnRampStackParamList } from 'src/app/navigation/types'
import { FiatOnRampCtaButton } from 'src/components/fiatOnRamp/CtaButton'
import { Screen } from 'src/components/layout/Screen'
import {
useFiatOnRampQuotes,
useMeldFiatCurrencySupportInfo,
useParseFiatOnRampError,
} from 'src/features/fiatOnRamp/aggregatorHooks'
import { FiatOnRampAmountSection } from 'src/features/fiatOnRamp/FiatOnRampAmountSection'
import { useFiatOnRampContext } from 'src/features/fiatOnRamp/FiatOnRampContext'
import { FiatOnRampCountryListModal } from 'src/features/fiatOnRamp/FiatOnRampCountryListModal'
import { FiatOnRampCountryPicker } from 'src/features/fiatOnRamp/FiatOnRampCountryPicker'
import { FiatOnRampTokenSelectorModal } from 'src/features/fiatOnRamp/FiatOnRampTokenSelector'
import {
useFiatOnRampQuotes,
useMeldFiatCurrencySupportInfo,
useParseFiatOnRampError,
} from 'src/features/fiatOnRamp/aggregatorHooks'
import { useFiatOnRampSupportedTokens } from 'src/features/fiatOnRamp/hooks'
import { FiatOnRampCurrency, InitialQuoteSelection } from 'src/features/fiatOnRamp/types'
import { sendMobileAnalyticsEvent } from 'src/features/telemetry'
......@@ -26,6 +26,7 @@ import { MobileEventProperties } from 'src/features/telemetry/types'
import { FiatOnRampScreens } from 'src/screens/Screens'
import { AnimatedFlex, Flex, Text, useIsDarkMode } from 'ui/src'
import { usePrevious } from 'utilities/src/react/hooks'
import { DEFAULT_DELAY, useDebounce } from 'utilities/src/time/timing'
import { DecimalPadLegacy } from 'wallet/src/components/legacy/DecimalPadLegacy'
import { useBottomSheetContext } from 'wallet/src/components/modals/BottomSheetContext'
import { HandleBar } from 'wallet/src/components/modals/HandleBar'
......@@ -111,19 +112,22 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
useShouldShowNativeKeyboard()
const { appFiatCurrencySupportedInMeld, meldSupportedFiatCurrency } =
useMeldFiatCurrencySupportInfo()
useMeldFiatCurrencySupportInfo(countryCode)
const debouncedAmount = useDebounce(amount, DEFAULT_DELAY * 2)
const {
error: quotesError,
loading: quotesLoading,
quotes,
} = useFiatOnRampQuotes({
baseCurrencyAmount: amount,
baseCurrencyAmount: debouncedAmount,
baseCurrencyCode: meldSupportedFiatCurrency.code,
quoteCurrencyCode: quoteCurrency.currencyInfo?.currency.symbol,
countryCode,
})
const selectTokenLoading = quotesLoading || amount !== debouncedAmount
const {
currentData: serviceProvidersResponse,
isFetching: serviceProvidersLoading,
......@@ -218,7 +222,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
const buttonDisabled =
serviceProvidersLoading ||
!!serviceProvidersError ||
quotesLoading ||
selectTokenLoading ||
!!quotesError ||
!selectedQuote?.destinationAmount
......@@ -275,7 +279,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
inputRef={inputRef}
quoteAmount={selectedQuote?.destinationAmount ?? 0}
quoteCurrencyAmountReady={Boolean(amount && selectedQuote)}
selectTokenLoading={quotesLoading}
selectTokenLoading={selectTokenLoading}
setSelection={setSelection}
showNativeKeyboard={showNativeKeyboard}
showSoftInputOnFocus={showNativeKeyboard}
......
......@@ -120,7 +120,7 @@
"@vanilla-extract/jest-transform": "1.1.1",
"@vanilla-extract/webpack-plugin": "2.3.1",
"@vercel/og": "0.5.8",
"@walletconnect/types": "2.8.6",
"@walletconnect/types": "2.11.2",
"babel-jest": "29.6.1",
"browser-cache-mock": "0.1.7",
"concurrently": "^8.0.1",
......
......@@ -36,7 +36,9 @@
"https://s2.coinmarketcap.com/",
"https://static.optimism.io/",
"https://vercel.com",
"https://vercel.live/"
"https://vercel.live/",
"https://trustwallet.com/",
"https://cloudflare-ipfs.com/"
],
"frameSrc": [
"'self'",
......@@ -51,6 +53,9 @@
"'self'",
"blob:",
"data:",
"https://*.gateway.uniswap.org",
"https://gateway.uniswap.org",
"https://statsigapi.net",
"https://api.moonpay.com/",
"https://api.opensea.io",
"https://api.thegraph.com/",
......@@ -84,7 +89,6 @@
"wss://www.walletlink.org/rpc"
],
"workerSrc": [
"*",
"'self'",
"blob:"
]
......
import { Trans } from '@lingui/macro'
import { PopupContainer } from 'components/Banner/shared/styled'
import { ButtonEmphasis, ButtonSize, ThemeButton } from 'components/Button'
import Column from 'components/Column'
import Row from 'components/Row'
import styled, { css } from 'styled-components'
import { BREAKPOINTS } from 'theme'
import { colors } from 'theme/colors'
import { SwapSmarterBannerBackground, UniswapLogoWithStar } from './icons'
import { useSwapSmarterBanner } from './useSwapSmarterBanner'
const StyledPopupContainer = styled(PopupContainer)`
height: 150px;
width: 350px;
right: 28px;
bottom: 46px;
@media screen and (max-width: ${BREAKPOINTS.md}px) {
right: unset;
left: unset;
bottom: 68px;
}
@media screen and (max-width: 350px) {
display: none;
}
border: none;
background: none;
overflow: hidden;
`
const Wrapper = styled.div`
position: relative;
height: 100%;
width: 100%;
padding: 15.24px 16px;
position: relative;
background: ${colors.pinkBase};
border: 1px solid ${({ theme }) => theme.surface3};
border-radius: 20px;
box-shadow: 0px 0px 10px 0px rgba(34, 34, 34, 0.04);
overflow: hidden;
`
const ContentContainer = styled(Column)`
position: relative;
justify-content: center;
z-index: 1;
gap: 6px;
`
const ComingSoonContainer = styled.div`
width: max-content;
height: 19.68px;
border-top-left-radius: 4.34px;
border-top-right-radius: 4.34px;
background-color: ${colors.pinkVibrant};
color: white;
font-size: 11.78px;
font-weight: 535;
line-height: 14.14px;
text-align: center;
padding: 3.67px 6.62px 1.69px 6.62px;
`
const BrowserExtensionContainer = styled(Row)`
width: max-content;
background-color: white;
padding: 2.1px 4.95px 0 5.8px;
border-top-right-radius: 4.34px;
border-bottom-right-radius: 4.34px;
color: black;
font-size: 30.66px;
font-weight: 535;
line-height: 36.8px;
text-align: center;
gap: 1.45px;
letter-spacing: -0.04em;
`
const SubtitleContainer = styled.div`
width: max-content;
background-color: white;
padding: 0px 6.79px 6.28px 6.79px;
border-bottom-left-radius: 4.34px;
border-bottom-right-radius: 4.34px;
color: black;
font-size: 16.84px;
font-weight: 500;
line-height: 18.86px;
letter-spacing: -0.04em;
`
const ButtonStyles = css`
height: 29.76px;
width: 100%;
border-radius: 12px;
font-size: 14px;
font-weight: 535;
line-height: 20px;
`
const LearnMoreButton = styled(ThemeButton)`
color: white;
background-color: black;
${ButtonStyles}
`
const DismissButton = styled(ThemeButton)`
color: #361a37;
background-color: #ffffff4d;
border: 1px solid #ffffff1f;
${ButtonStyles}
`
export function SwapSmarterBanner() {
const { shouldShowBanner, handleAccept, handleReject } = useSwapSmarterBanner()
return (
<StyledPopupContainer show={shouldShowBanner} data-testid="swap-smarter-banner">
<Wrapper>
<ContentContainer>
<Column>
<ComingSoonContainer>
<Trans>COMING SOON</Trans>
</ComingSoonContainer>
<BrowserExtensionContainer>
<UniswapLogoWithStar />
<Trans>Browser Extension</Trans>
</BrowserExtensionContainer>
<SubtitleContainer>
<Trans>and all new products to swap smarter</Trans>
</SubtitleContainer>
</Column>
<Row gap="sm">
<LearnMoreButton size={ButtonSize.medium} emphasis={ButtonEmphasis.promotional} onClick={handleAccept}>
<Trans>Learn more</Trans>
</LearnMoreButton>
<DismissButton size={ButtonSize.medium} emphasis={ButtonEmphasis.promotional} onClick={handleReject}>
<Trans>Dismiss</Trans>
</DismissButton>
</Row>
</ContentContainer>
<SwapSmarterBannerBackground />
</Wrapper>
</StyledPopupContainer>
)
}
This diff is collapsed.
import { useSwapSmarterEnabled } from 'featureFlags/flags/swapSmarter'
import { useIsLandingPage } from 'hooks/useIsLandingPage'
import { useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
import { useCallback } from 'react'
const shouldHideSwapSmarterBannerAtom = atomWithStorage<boolean>('shouldHideSwapSmarterBanner', false)
export function useSwapSmarterBanner() {
const isLandingPage = useIsLandingPage()
const isEnabled = useSwapSmarterEnabled()
const [shouldHideSwapSmarterBanner, updateShouldHideSwapSmarterBanner] = useAtom(shouldHideSwapSmarterBannerAtom)
const handleAccept = useCallback(() => {
updateShouldHideSwapSmarterBanner(true)
window.open(
'http://smarter.uniswap.org/?utm_medium=banner&utm_source=uniswap&utm_campaign=swap-smarter&utm_creative=',
'_blank'
)
}, [updateShouldHideSwapSmarterBanner])
const handleReject = useCallback(() => {
updateShouldHideSwapSmarterBanner(true)
}, [updateShouldHideSwapSmarterBanner])
return {
shouldShowBanner: isEnabled && isLandingPage && !shouldHideSwapSmarterBanner,
handleAccept,
handleReject,
}
}
......@@ -8,12 +8,15 @@ import { getValidUrlChainId } from 'graphql/data/util'
import { useMemo } from 'react'
import { useLocation } from 'react-router-dom'
import { getCurrentPageFromLocation } from 'utils/urlRoutes'
import { SwapSmarterBanner } from '../SwapSmarterBanner/SwapSmarterBanner'
import { useSwapSmarterBanner } from '../SwapSmarterBanner/useSwapSmarterBanner'
import { LargeUniTagBanner } from '../UniTag/LargeUniTagBanner'
export function Banners() {
const { pathname } = useLocation()
const currentPage = getCurrentPageFromLocation(pathname)
const isUniTagsEnabled = useUniTagsEnabled()
const { shouldShowBanner: shouldShowSwapSmarterBanner } = useSwapSmarterBanner()
const outageBanners = useOutageBanners()
......@@ -46,6 +49,10 @@ export function Banners() {
return <OutageBanner chainId={pageChainId} />
}
if (shouldShowSwapSmarterBanner) {
return <SwapSmarterBanner />
}
if (isUniTagsEnabled) {
return <LargeUniTagBanner />
}
......
......@@ -23,6 +23,7 @@ import {
import { useProgressIndicatorV2Flag } from 'featureFlags/flags/progressIndicatorV2'
import { useQuickRouteMainnetFlag } from 'featureFlags/flags/quickRouteMainnet'
import { useSendEnabledFlag } from 'featureFlags/flags/send'
import { useSwapSmarterFlag } from 'featureFlags/flags/swapSmarter'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import { useUniTagsFlag } from 'featureFlags/flags/uniTags'
import { useUniswapXSyntheticQuoteFlag } from 'featureFlags/flags/uniswapXUseSyntheticQuote'
......@@ -368,6 +369,12 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.uniTags}
label="UniTags support"
/>
<FeatureFlagOption
variant={BaseVariant}
value={useSwapSmarterFlag()}
featureFlag={FeatureFlag.swapSmarter}
label="Swap Smarter"
/>
<FeatureFlagGroup name="Quick routes">
<FeatureFlagOption
variant={BaseVariant}
......
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useSwapSmarterFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.swapSmarter)
}
export function useSwapSmarterEnabled(): boolean {
return useSwapSmarterFlag() === BaseVariant.Enabled
}
......@@ -33,6 +33,7 @@ export enum FeatureFlag {
outageBannerOptimism = 'outage_banner_feb_2024_optimism',
outageBannerArbitrum = 'outage_banner_feb_2024_arbitrum',
outageBannerPolygon = 'outage_banner_feb_2024_polygon',
swapSmarter = 'swap_smarter',
uniTags = 'uni_tags',
}
......
import styled, { css } from 'styled-components'
import { BREAKPOINTS } from 'theme'
const H1Styles = css<{ color?: string }>`
padding: 0;
......@@ -73,6 +74,9 @@ export const Subheading = styled.p`
line-height: 24px; /* 133.333% */
max-width: 430px;
letter-spacing: -0.01em;
@media (max-height: ${BREAKPOINTS.md}px) {
font-size: 16px;
}
`
export type BoxProps = {
......
......@@ -8,6 +8,7 @@ import styled, { css, keyframes } from 'styled-components'
import { ThemedText } from 'theme/components'
import { BREAKPOINTS } from 'theme'
import { heightBreakpoints } from 'ui/src/theme'
import { Box, H1, Subheading } from '../components/Generics'
import { TokenCloud } from '../components/TokenCloud/index'
import { Hover, RiseIn, RiseInText } from '../components/animations'
......@@ -43,6 +44,9 @@ const StyledH1 = styled(H1)`
@media (max-width: 464px) {
font-size: 36px;
}
@media (max-height: 668px) {
font-size: 28px;
}
`
const shrinkAndFade = keyframes`
0% {
......@@ -59,11 +63,14 @@ const Center = styled(Box)<{ transition?: boolean }>`
pointer-events: none;
padding: 48px 0px;
@media (max-width: 464px), (max-height: 700px) {
padding: 0px;
padding-top: 24px;
}
@media (max-width: 464px), (max-height: 668px) {
padding-top: 8px;
}
gap: 20px;
gap: 24px;
@media (max-height: 800px) {
gap: 0px;
gap: 16px;
}
${({ transition }) =>
transition &&
......@@ -76,6 +83,10 @@ const LearnMoreContainer = styled(Box)`
@media (max-width: ${BREAKPOINTS.md}px) {
bottom: 64px;
}
@media (max-height: ${heightBreakpoints.short}px) {
display: none;
}
`
interface HeroProps {
......@@ -113,7 +124,7 @@ export function Hero({ scrollToRef, transition }: HeroProps) {
<Center
direction="column"
align="center"
maxWidth="75vw"
maxWidth="85vw"
transition={transition}
style={{ transform: `translate(0px, ${translateY}px)`, opacity: opacityY }}
>
......@@ -165,7 +176,7 @@ export function Hero({ scrollToRef, transition }: HeroProps) {
<Hover>
<ColumnCenter>
<ThemedText.BodySecondary>
<Trans>Scroll to Learn More</Trans>
<Trans>Scroll to learn more</Trans>
</ThemedText.BodySecondary>
<ChevronDown />
</ColumnCenter>
......
......@@ -32,7 +32,8 @@ export const colors = {
pink700: '#55072A',
pink800: '#350318',
pink900: '#2B000B',
pinkVibrant: '#F51A70',
pinkBase: '#FC74FE',
pinkVibrant: '#F50DB4',
red50: '#FAECEA',
red100: '#FED5CF',
red200: '#FEA79B',
......
......@@ -45,8 +45,8 @@ const light = {
colorHover: colorsLight.accent1,
colorPress: colorsLight.accent1,
colorFocus: colorsLight.accent1,
shadowColor: colorsLight.none,
shadowColorHover: colorsLight.none,
shadowColor: 'rgba(0,0,0,0.15)',
shadowColorHover: 'rgba(0,0,0,0.2)',
}
type BaseTheme = typeof light
......@@ -66,8 +66,8 @@ const dark: BaseTheme = {
colorHover: colorsDark.accent1,
colorPress: colorsDark.accent1,
colorFocus: colorsDark.accent1,
shadowColor: colorsDark.none,
shadowColorHover: colorsDark.none,
shadowColor: 'rgba(0,0,0,0.4)',
shadowColorHover: 'rgba(0,0,0,0.5)',
}
// if you need to add non-token values, use createTheme
......
......@@ -21,7 +21,7 @@ export function SelectTokenButton({
return (
<TouchableArea
hapticFeedback
backgroundColor={selectedCurrencyInfo ? '$surface2' : '$accent1'}
backgroundColor={selectedCurrencyInfo ? '$surface3' : '$accent1'}
borderRadius="$roundedFull"
testID={`currency-selector-toggle-${showNonZeroBalancesOnly ? 'in' : 'out'}`}
onPress={onPress}>
......
import { pick } from 'lodash'
import { ComponentProps } from 'react'
import { ComponentProps, useEffect, useState } from 'react'
import { Flex, Sheet, useSporeColors } from 'ui/src'
import { Trace } from 'utilities/src/telemetry/trace/Trace'
import { TextInput } from 'wallet/src/components/input/TextInput'
......@@ -49,6 +49,8 @@ export function BottomSheetDetachedModal(props: BottomSheetModalProps): JSX.Elem
return <WebBottomSheetModal {...supportedProps} />
}
const ANIMATION_MS = 200
function WebBottomSheetModal({
children,
name,
......@@ -60,6 +62,25 @@ function WebBottomSheetModal({
isCentered = true,
}: WebBottomSheetProps): JSX.Element {
const colors = useSporeColors()
const [fullyClosed, setFullyClosed] = useState(false)
if (fullyClosed && isModalOpen) {
setFullyClosed(false)
}
// Not the greatest, we are syncing 200 here to 200ms animation
// TODO(EXT-745): Add Tamagui onFullyClosed callback and replace here
useEffect(() => {
if (!isModalOpen) {
const tm = setTimeout(() => {
setFullyClosed(true)
}, ANIMATION_MS)
return () => {
clearTimeout(tm)
}
}
}, [isModalOpen])
return (
<Trace logImpression={isModalOpen} modal={name}>
......@@ -67,7 +88,7 @@ function WebBottomSheetModal({
<Sheet
disableDrag
modal
animation="200ms"
animation={`${ANIMATION_MS}ms`}
dismissOnOverlayPress={false}
dismissOnSnapToBottom={false}
open={isModalOpen}
......@@ -97,7 +118,7 @@ function WebBottomSheetModal({
style={{ backgroundColor: backgroundColor ?? colors.surface1.val }}
width="100%">
{/* To keep this consistent with how the `BottomSheetModal` works on native mobile, we only mount the children when the modal is open. */}
{isModalOpen ? children : null}
{fullyClosed ? null : children}
</Flex>
</Sheet.Frame>
</Sheet>
......
......@@ -38,7 +38,7 @@ export const uniswapUrls = {
appUrl: `https://${UNISWAP_APP_HOSTNAME}`,
interfaceUrl: `https://${UNISWAP_APP_HOSTNAME}/#/swap`,
extensionFeedbackFormUrl: 'https://forms.gle/RGFhKnABUjdPiYQH6', // TODO(EXT-668): Remove this after F&F launch
interfaceTokensUrl: `https://${UNISWAP_APP_HOSTNAME}/tokens`,
interfaceTokensUrl: `https://${UNISWAP_APP_HOSTNAME}/explore/tokens`,
unitagsApiUrl: getUnitagsApiUrl(),
tradingApiPaths: {
quote: getTradingApiQuotePath(),
......
......@@ -12,6 +12,8 @@ import {
FORQuoteResponse,
FORServiceProvidersResponse,
FORSupportedCountriesResponse,
FORSupportedFiatCurrenciesRequest,
FORSupportedFiatCurrenciesResponse,
FORSupportedTokensRequest,
FORSupportedTokensResponse,
FORTransactionsRequest,
......@@ -238,6 +240,12 @@ export const fiatOnRampAggregatorApi = createApi({
>({
query: (request) => `/supported-tokens?${new URLSearchParams(request).toString()}`,
}),
fiatOnRampAggregatorSupportedFiatCurrencies: builder.query<
FORSupportedFiatCurrenciesResponse,
FORSupportedFiatCurrenciesRequest
>({
query: (request) => `/supported-fiat-currencies?${new URLSearchParams(request).toString()}`,
}),
fiatOnRampAggregatorTransferInstitutions: builder.query<
FORTransferInstitutionsResponse,
FORTransferInstitutionsRequest
......@@ -302,6 +310,7 @@ export const {
useFiatOnRampAggregatorCryptoQuoteQuery,
useFiatOnRampAggregatorServiceProvidersQuery,
useFiatOnRampAggregatorSupportedTokensQuery,
useFiatOnRampAggregatorSupportedFiatCurrenciesQuery,
useFiatOnRampAggregatorTransferInstitutionsQuery,
useFiatOnRampAggregatorWidgetQuery,
useFiatOnRampAggregatorTransferWidgetQuery,
......
......@@ -198,6 +198,7 @@ export type FORQuote = {
destinationAmount: number
destinationCurrencyCode: string
serviceProvider: string
totalFee: number
}
export type FORQuoteResponse = {
......@@ -244,6 +245,22 @@ export type FORSupportedTokensResponse = {
supportedTokens: FORSupportedToken[]
}
// /supported-fiat-currencies
export type FORSupportedFiatCurrenciesRequest = {
countryCode: string
}
export type FORSupportedFiatCurrency = {
fiatCurrencyCode: string
displayName: string
symbol: string
}
export type FORSupportedFiatCurrenciesResponse = {
fiatCurrencies: FORSupportedFiatCurrency[]
}
// /transfer-institutions
export type FORTransferInstitutionsRequest = {
......@@ -300,36 +317,23 @@ export type FORCryptoDetails = {
walletAddress: string
networkFee: number
transactionFee: number
partnerFee: number | null
totalFee: number
networkFeeInUsd: number | null
transactionFeeInUsd: number | null
partnerFeeInUsd: number | null
totalFeeInUsd: number | null
blockchainTransactionId: string
institution: string | null
chainId: string
}
export type FORTransaction = {
key: string
id: string
paymentMethod: string | null
transactionType: string
status: string
sourceAmount: number
sourceCurrencyCode: string
destinationAmount: number
destinationCurrencyCode: string
paymentMethodType: string
serviceProvider: string
description: string | null
cryptoDetails: FORCryptoDetails
createdAt: string
updatedAt: string
countryCode: string
externalSessionId: string
sourceAmountInUsd: number
}
export type FORTransactionsRequest = {
......
......@@ -17,7 +17,11 @@ export function HiddenTokensRow({
const { t } = useTranslation()
return (
<TouchableArea hapticFeedback hapticStyle={ImpactFeedbackStyle.Light} onPress={onPress}>
<TouchableArea
hapticFeedback
activeOpacity={1}
hapticStyle={ImpactFeedbackStyle.Light}
onPress={onPress}>
<Flex
row
alignItems="center"
......@@ -27,6 +31,8 @@ export function HiddenTokensRow({
<Text color="$neutral2" variant="subheading2">
{t('Hidden ({{numHidden}})', { numHidden })}
</Text>
{/* just used for opacity styling, the parent TouchableArea handles event */}
<TouchableArea>
<Flex
row
alignItems="center"
......@@ -68,6 +74,7 @@ export function HiddenTokensRow({
width={iconSizes.icon20}
/>
</Flex>
</TouchableArea>
</Flex>
</TouchableArea>
)
......
......@@ -6,10 +6,8 @@ import {
TextInput,
TextInputProps,
TextInputSelectionChangeEventData,
ViewStyle,
} from 'react-native'
import {
AnimatedStyleProp,
Easing,
useAnimatedStyle,
useSharedValue,
......@@ -18,7 +16,7 @@ import {
withTiming,
} from 'react-native-reanimated'
import { AnimatedFlex, Flex, FlexProps, Icons, Text, TouchableArea, useSporeColors } from 'ui/src'
import { fonts, spacing } from 'ui/src/theme'
import { fonts } from 'ui/src/theme'
import { NumberType } from 'utilities/src/format/types'
import { useForwardRef, usePrevious } from 'utilities/src/react/hooks'
import { AmountInput } from 'wallet/src/components/input/AmountInput'
......@@ -38,7 +36,6 @@ type CurrentInputPanelProps = {
currencyField: CurrencyField
currencyInfo: Maybe<CurrencyInfo>
isLoading?: boolean
isCollapsed: boolean
focus?: boolean
isFiatMode?: boolean
onPressIn?: () => void
......@@ -55,7 +52,7 @@ type CurrentInputPanelProps = {
resetSelection: (args: { start: number; end?: number; currencyField?: CurrencyField }) => void
} & FlexProps
const MAX_INPUT_FONT_SIZE = 42
const MAX_INPUT_FONT_SIZE = 36
const MIN_INPUT_FONT_SIZE = 24
// if font changes from `fontFamily.sansSerif.regular` or `MAX_INPUT_FONT_SIZE`
......@@ -73,7 +70,6 @@ export const CurrencyInputPanel = memo(
currencyField,
currencyInfo,
isLoading,
isCollapsed,
focus,
isFiatMode = false,
onPressIn,
......@@ -170,8 +166,7 @@ export const CurrencyInputPanel = memo(
// Hide balance if panel is output, and no balance
const hideCurrencyBalance = isOutput && currencyBalance?.equalTo(0)
// Only show max button on output if 0 balance, always show on input
const showMaxButton = !isOutput || (isOutput && currencyBalance?.equalTo(0))
const showMaxButton = !isOutput
// when there is no input value, the color should be lighter to account for $ sign when in fiat input mode
const emptyColor = !value ? '$neutral3' : '$neutral1'
......@@ -217,8 +212,22 @@ export const CurrencyInputPanel = memo(
const previousValue = usePrevious(value)
const loadingTextValue = previousValue && previousValue !== '' ? previousValue : '0'
const { animatedContainerStyle, animatedAmountInputStyle, animatedInfoRowStyle } =
useAnimatedContainerStyles(isLoading, isCollapsed)
const loadingFlexProgress = useSharedValue(1)
loadingFlexProgress.value = withRepeat(
withSequence(
withTiming(0.4, { duration: 400, easing: Easing.ease }),
withTiming(1, { duration: 400, easing: Easing.ease })
),
-1,
true
)
const loadingStyle = useAnimatedStyle(
() => ({
opacity: isLoading ? loadingFlexProgress.value : 1,
}),
[isLoading]
)
const { symbol: fiatCurrencySymbol } = useAppFiatCurrencyInfo()
......@@ -227,15 +236,13 @@ export const CurrencyInputPanel = memo(
return (
<TouchableArea hapticFeedback onPress={currencyInfo ? onPressIn : onShowTokenSelector}>
<AnimatedFlex {...rest} overflow="hidden" px="$spacing16" style={animatedContainerStyle}>
<AnimatedFlex
<Flex {...rest} overflow="hidden" px="$spacing16" py="$spacing20">
<Flex
row
alignItems="center"
gap="$spacing8"
justifyContent={!currencyInfo ? 'flex-end' : 'space-between'}
// Extra space in case choose token text overlaps swap arrow
pb={!isOutput && !currencyInfo ? '$spacing8' : '$none'}
pt={isOutput && !currencyInfo ? '$spacing8' : '$none'}>
py="$spacing8">
<AnimatedFlex
fill
grow
......@@ -243,16 +250,10 @@ export const CurrencyInputPanel = memo(
alignItems="center"
height={MAX_INPUT_FONT_SIZE}
overflow="hidden"
style={animatedAmountInputStyle}
style={loadingStyle}
onLayout={onLayout}>
{isFiatMode && (
<Text
allowFontScaling
color={inputColor}
style={{
fontSize: isCollapsed ? MIN_INPUT_FONT_SIZE : fontSize,
}}
variant="heading2">
<Text allowFontScaling color={inputColor} fontSize={fontSize} variant="heading2">
{fiatCurrencySymbol}
</Text>
)}
......@@ -267,7 +268,7 @@ export const CurrencyInputPanel = memo(
flex={1}
focusable={Boolean(currencyInfo)}
fontFamily="$heading"
fontSize={isCollapsed ? MIN_INPUT_FONT_SIZE : fontSize}
fontSize={fontSize}
maxDecimals={currencyInfo.currency.decimals}
maxFontSizeMultiplier={fonts.heading2.maxFontSizeMultiplier}
// This is a hacky workaround for Android to prevent text from being cut off
......@@ -291,7 +292,7 @@ export const CurrencyInputPanel = memo(
/>
) : (
<TouchableArea hapticFeedback onPress={onShowTokenSelector}>
<Text color="$neutral3" variant="heading3">
<Text color="$neutral3" fontSize={fontSize} variant="heading2">
0
</Text>
</TouchableArea>
......@@ -304,20 +305,9 @@ export const CurrencyInputPanel = memo(
onPress={onShowTokenSelector}
/>
</Flex>
</AnimatedFlex>
<AnimatedFlex
row
gap="$spacing8"
height={spacing.spacing36}
justifyContent="space-between"
left={spacing.spacing16}
position="absolute"
pt="$spacing16"
right={spacing.spacing16}
style={animatedInfoRowStyle}>
{/* Keep the animated parent container so animation styles are always mounted */}
</Flex>
{currencyInfo && (
<>
<Flex row gap="$spacing8" justifyContent="space-between">
<TouchableArea disabled={fiatModeFeatureEnabled} onPress={_onToggleIsFiatMode}>
<Flex centered row shrink gap="$spacing4">
<Text color="$neutral2" numberOfLines={1} variant="body3">
......@@ -342,62 +332,10 @@ export const CurrencyInputPanel = memo(
<MaxAmountButton currencyField={currencyField} onSetMax={onSetMax} />
)}
</Flex>
</>
</Flex>
)}
</AnimatedFlex>
</AnimatedFlex>
</Flex>
</TouchableArea>
)
})
)
function useAnimatedContainerStyles(
isLoading: boolean | undefined,
isCollapsed: boolean | undefined
): {
animatedContainerStyle: AnimatedStyleProp<ViewStyle>
animatedAmountInputStyle: AnimatedStyleProp<ViewStyle>
animatedInfoRowStyle: AnimatedStyleProp<ViewStyle>
} {
const animatedContainerStyle = useAnimatedStyle(() => {
return {
paddingTop: withTiming(isCollapsed ? spacing.spacing16 : spacing.spacing24, {
duration: 300,
}),
paddingBottom: withTiming(isCollapsed ? spacing.spacing16 : spacing.spacing48, {
duration: 300,
}),
}
}, [isCollapsed])
const loadingFlexProgress = useSharedValue(1)
loadingFlexProgress.value = withRepeat(
withSequence(
withTiming(0.4, { duration: 400, easing: Easing.ease }),
withTiming(1, { duration: 400, easing: Easing.ease })
),
-1,
true
)
const animatedAmountInputStyle = useAnimatedStyle(
() => ({
opacity: isLoading ? loadingFlexProgress.value : 1,
}),
[isLoading]
)
const animatedInfoRowStyle = useAnimatedStyle(() => {
return {
bottom: withTiming(isCollapsed ? -spacing.spacing24 : spacing.spacing16, {
duration: 300,
}),
}
}, [isCollapsed])
return {
animatedContainerStyle,
animatedAmountInputStyle,
animatedInfoRowStyle,
}
}
......@@ -81,14 +81,16 @@ export function GasAndWarningRows({ renderEmptyRows }: { renderEmptyRows: boolea
)}
<Flex centered row>
{isWeb && (
<Flex fill>
<SwapRateRatio initialInverse={true} styling="secondary" trade={trade.trade} />
</Flex>
)}
{showGasFee && (
<TouchableArea hapticFeedback onPress={(): void => setShowGasInfoModal(true)}>
<AnimatedFlex centered row entering={FadeIn} gap="$spacing4">
<Icons.Gas color={colors.neutral2.val} size="$icon.16" />
<Text color="$neutral2" variant="body4">
<Text color="$neutral2" variant="body3">
{gasFeeFormatted}
</Text>
</AnimatedFlex>
......
......@@ -58,7 +58,7 @@ export function MaxAmountButton({
style={style}
onPress={onPress}>
<Text color="$accent1" variant="buttonLabel4">
{currencyField === CurrencyField.OUTPUT ? t('Get max') : t('Max')}
{t('Max')}
</Text>
</TouchableArea>
</Trace>
......
......@@ -83,7 +83,7 @@ function CurrentScreen({
</Trace>
{/*
We want to render the `BottomSheetModal` from the start to allow the tamagui toast animation to happen once we switch the `isModalOpen` prop to `true`.
We want to render the `BottomSheetModal` from the start to allow the tamagui animation to happen once we switch the `isModalOpen` prop to `true`.
We only render `SwapReviewScreen` once the user is truly on that step though.
*/}
<BottomSheetModal
......
......@@ -427,7 +427,6 @@ function SwapFormContent(): JSX.Element {
currencyField={CurrencyField.INPUT}
currencyInfo={currencies[CurrencyField.INPUT]}
focus={focusOnCurrencyField === CurrencyField.INPUT}
isCollapsed={decimalPadControlledField !== CurrencyField.INPUT}
isFiatMode={isFiatMode && exactFieldIsInput}
isLoading={!exactFieldIsInput && isSwapDataLoading}
resetSelection={resetSelection}
......@@ -471,7 +470,6 @@ function SwapFormContent(): JSX.Element {
currencyField={CurrencyField.OUTPUT}
currencyInfo={currencies[CurrencyField.OUTPUT]}
focus={focusOnCurrencyField === CurrencyField.OUTPUT}
isCollapsed={decimalPadControlledField !== CurrencyField.OUTPUT}
isFiatMode={isFiatMode && exactFieldIsOutput}
isLoading={!exactFieldIsOutput && isSwapDataLoading}
resetSelection={resetSelection}
......@@ -517,7 +515,7 @@ function SwapFormContent(): JSX.Element {
{!showWebOutputTokenSelector && (
<>
<Flex fill={isWeb} mt="$spacing8">
<Flex fill={isWeb} mt={isWeb ? '$spacing8' : '$spacing12'}>
<GasAndWarningRows renderEmptyRows />
</Flex>
</>
......
......@@ -89,13 +89,26 @@ export const useCanAddressClaimUnitag = (
): { canClaimUnitag: boolean; errorCode?: UnitagErrorCodes } => {
const unitagsFeatureFlagEnabled = useFeatureFlag(FEATURE_FLAGS.Unitags)
const { data: deviceId } = useAsyncData(getUniqueId)
const { refetchUnitagsCounter } = useUnitagUpdater()
const skip = !unitagsFeatureFlagEnabled || !deviceId
const { loading, data } = useUnitagClaimEligibilityQuery({
const { loading, data, refetch } = useUnitagClaimEligibilityQuery({
address,
deviceId: deviceId ?? '', // this is fine since we skip if deviceId is undefined
isUsernameChange,
skip,
})
// Force refetch of canClaimUnitag if refetchUnitagsCounter changes
useEffect(() => {
if (skip || loading) {
return
}
refetch?.()
// Skip is included in the dependency array here bc of useAsyncData -- on mount deviceId is undefined so refetch would be skipped if not included
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [refetchUnitagsCounter, skip])
return {
canClaimUnitag: !loading && !!data?.canClaim,
errorCode: data?.errorCode,
......
import { put } from 'typed-redux-saga'
import { logger } from 'utilities/src/logger/logger'
import { selectPendingAccounts } from 'wallet/src/features/wallet/selectors'
import {
selectActiveAccountAddress,
selectPendingAccounts,
} from 'wallet/src/features/wallet/selectors'
import {
removeAccounts,
setAccountAsActive,
setAccountsNonPending,
} from 'wallet/src/features/wallet/slice'
import { appSelect } from 'wallet/src/state'
import { areAddressesEqual } from 'wallet/src/utils/addresses'
import { createSaga } from 'wallet/src/utils/saga'
export enum PendingAccountActions {
Activate = 'Activate',
ActivateAndSelect = 'ActivateAndSelect',
Delete = 'Delete',
ActivateOneAndDelete = 'ActivateOneAndDelete',
}
/**
......@@ -20,6 +25,7 @@ export enum PendingAccountActions {
*/
export function* managePendingAccounts(pendingAccountAction: PendingAccountActions) {
const pendingAccounts = yield* appSelect(selectPendingAccounts)
const activeAddress = yield* appSelect(selectActiveAccountAddress)
const pendingAddresses = Object.keys(pendingAccounts)
if (!pendingAddresses.length) {
// It does not make sense to make updates, when there is nothing to update
......@@ -27,6 +33,7 @@ export function* managePendingAccounts(pendingAccountAction: PendingAccountActio
logger.debug('pendingAccountsSaga', 'managePendingAccounts', 'No pending accounts found.')
return
}
if (pendingAccountAction === PendingAccountActions.Activate) {
yield* put(setAccountsNonPending(pendingAddresses))
} else if (pendingAccountAction === PendingAccountActions.ActivateAndSelect) {
......@@ -37,6 +44,24 @@ export function* managePendingAccounts(pendingAccountAction: PendingAccountActio
} else if (pendingAccountAction === PendingAccountActions.Delete) {
// TODO: [MOB-244] cleanup low level RS key storage.
yield* put(removeAccounts(pendingAddresses))
} else if (pendingAccountAction === PendingAccountActions.ActivateOneAndDelete) {
// Used to cover a case where we want to activate the current active account in case the user was in a bad state and delete the others
if (activeAddress && pendingAddresses.find((addr) => areAddressesEqual(addr, activeAddress))) {
yield* put(setAccountsNonPending([activeAddress]))
}
// Delete any addresses that are pending and not the active address
const addressesToDelete = pendingAddresses.filter(
(addr) => !areAddressesEqual(addr, activeAddress)
)
if (addressesToDelete.length > 0) {
logger.info(
'pendingAccountsSaga',
'managePendingAccounts',
'Got additional accounts to delete when activating a single account'
)
yield* put(removeAccounts(addressesToDelete))
}
}
logger.debug('pendingAccountsSaga', 'managePendingAccounts', 'Updated pending accounts.')
......
......@@ -251,12 +251,15 @@
"Enter your recovery phrase below, or try searching for backups again.": "Enter your recovery phrase below, or try searching for backups again.",
"Enter your recovery phrase from another crypto wallet": "Enter your recovery phrase from another crypto wallet",
"Enter your recovery phrase instead": "Enter your recovery phrase instead",
"Error": "Error",
"Error importing wallets": "Error importing wallets",
"Error loading accounts": "Error loading accounts",
"Error while checking transaction status": "Error while checking transaction status",
"Error while importing backups": "Error while importing backups",
"Euro": "Euro",
"Expired": "Expired",
"Expires in {{duration}}": "Expires in {{duration}}",
"Expires in {{duration}}...": "Expires in {{duration}}...",
"Explore tokens & NFTs": "Explore tokens & NFTs",
"Failed attempts: {failedAttemptCount.toString()}": "Failed attempts: {failedAttemptCount.toString()}",
"Failed to {{action}}": "Failed to {{action}}",
......@@ -265,7 +268,6 @@
"Failed to fetch token balances": "Failed to fetch token balances",
"Failed to import backups due to lack of permissions, interruption of authorization, or due to a cloud error": "Failed to import backups due to lack of permissions, interruption of authorization, or due to a cloud error",
"Failed to send {{assetInfo}} to {{senderOrRecipient}}.": "Failed to send {{assetInfo}} to {{senderOrRecipient}}.",
"Failed to send.": "Failed to send.",
"Failed to swap {{inputAssetInfo}} for {{outputAssetInfo}}.": "Failed to swap {{inputAssetInfo}} for {{outputAssetInfo}}.",
"Failed to transact{{addressText}}.": "Failed to transact{{addressText}}.",
"Failed to unwrap {{inputAssetInfo}}.": "Failed to unwrap {{inputAssetInfo}}.",
......@@ -289,7 +291,6 @@
"Function": "Function",
"Fund your wallet by transferring crypto from another wallet or account": "Fund your wallet by transferring crypto from another wallet or account",
"Get help": "Get help",
"Get max": "Get max",
"Get notified when your transfers, swaps, and approvals complete.": "Get notified when your transfers, swaps, and approvals complete.",
"Get tokens at the best prices in web3 with Uniswap Wallet.": "Get tokens at the best prices in web3 with Uniswap Wallet.",
"Give your wallet a nickname": "Give your wallet a nickname",
......@@ -346,7 +347,7 @@
"Introducing usernames": "Introducing usernames",
"Invalid password. Please try again.": "Invalid password. Please try again.",
"Invalid phrase": "Invalid phrase",
"Invalid public key": "Invalid public key",
"Invalid public key.": "Invalid public key.",
"Invalid QR Code": "Invalid QR Code",
"Invalid word: ": "Invalid word: ",
"Invalid word: {{word}}": "Invalid word: {{word}}",
......@@ -404,7 +405,6 @@
"Networks": "Networks",
"Never enter it to any websites or apps": "Never enter it to any websites or apps",
"New address": "New address",
"New code in {{duration}}": "New code in {{duration}}",
"New input": "New input",
"New output": "New output",
"New password": "New password",
......@@ -419,6 +419,7 @@
"No message found.": "No message found.",
"No NFTs found": "No NFTs found",
"No NFTs yet": "No NFTs yet",
"No OTP received. Please try again.": "No OTP received. Please try again.",
"No QR code found": "No QR code found",
"No results found": "No results found",
"No results found for <1>\"{searchFilter}\"</1>": "No results found for <1>\"{searchFilter}\"</1>",
......@@ -440,7 +441,6 @@
"Only continue if you are syncing with the Uniswap Extension on a trusted device.": "Only continue if you are syncing with the Uniswap Extension on a trusted device.",
"Oops! Something went wrong.": "Oops! Something went wrong.",
"Other options": "Other options",
"OTP unavailable": "OTP unavailable",
"Owned by": "Owned by",
"Owned by {{owner}}": "Owned by {{owner}}",
"Owners": "Owners",
......@@ -505,7 +505,6 @@
"Request from": "Request from",
"Require {{authenticationTypeName}} to open app": "Require {{authenticationTypeName}} to open app",
"Require {{authenticationTypeName}} to transact": "Require {{authenticationTypeName}} to transact",
"Resend code": "Resend code",
"Reset your password": "Reset your password",
"Restart app": "Restart app",
"Restore": "Restore",
......@@ -582,6 +581,7 @@
"Something crashed.": "Something crashed.",
"Something went wrong": "Something went wrong",
"Something went wrong.": "Something went wrong.",
"Sorry, we are unable to load the QR code right now. Please try another onboarding method.": "Sorry, we are unable to load the QR code right now. Please try another onboarding method.",
"Spanish (Latin America)": "Spanish (Latin America)",
"Spanish (Spain)": "Spanish (Spain)",
"Spanish (US)": "Spanish (US)",
......
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