ci(release): publish latest release

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