ci(release): publish latest release

parent 84ef9a01
IPFS hash of the deployment:
- CIDv0: `QmZ8D4oPRH7CbNZ1ULVQedBkTLJhqv539kRTmifrva49GM`
- CIDv1: `bafybeifaiclxh6pc3bdtrrkpbvvqqxq6hz5r6htdzxaga4fikfpu2u56qi`
- CIDv0: `QmWZERyNmMf7JhDQJ8mXYLPdchuQjWNdc5Z3sKN6C9bsL9`
- CIDv1: `bafybeid2c2mdeiiysuldtcrlma2oaydsigt4ev55cttcb53zk6ldw7cozy`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
......@@ -10,15 +10,38 @@ You can also access the Uniswap Interface from an IPFS gateway.
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://bafybeifaiclxh6pc3bdtrrkpbvvqqxq6hz5r6htdzxaga4fikfpu2u56qi.ipfs.dweb.link/
- https://bafybeifaiclxh6pc3bdtrrkpbvvqqxq6hz5r6htdzxaga4fikfpu2u56qi.ipfs.cf-ipfs.com/
- [ipfs://QmZ8D4oPRH7CbNZ1ULVQedBkTLJhqv539kRTmifrva49GM/](ipfs://QmZ8D4oPRH7CbNZ1ULVQedBkTLJhqv539kRTmifrva49GM/)
- https://bafybeid2c2mdeiiysuldtcrlma2oaydsigt4ev55cttcb53zk6ldw7cozy.ipfs.dweb.link/
- https://bafybeid2c2mdeiiysuldtcrlma2oaydsigt4ev55cttcb53zk6ldw7cozy.ipfs.cf-ipfs.com/
- [ipfs://QmWZERyNmMf7JhDQJ8mXYLPdchuQjWNdc5Z3sKN6C9bsL9/](ipfs://QmWZERyNmMf7JhDQJ8mXYLPdchuQjWNdc5Z3sKN6C9bsL9/)
### 5.16.2 (2024-02-28)
## 5.17.0 (2024-03-06)
### Features
* **web:** [info] Shorthand for timestamps (#6675) 6b63c99
* **web:** [info] truncate long token names in Tables (#6682) 402ba22
* **web:** [info] Use sentence case for Swap in All Tx Table (#6687) f59d68d
* **web:** add disclaimer to limits in more places (#6609) 0467d43
* **web:** add more limits disclaimers 4345063
* **web:** add more limits disclaimers (#6738) 5dc1070
* **web:** adding USDC to Celo (#6641) a34ea26
* **web:** Rename Ether -> Ethereum (#6661) 1f2efb3
### Bug Fixes
* **web:** [hotfix] [limits] presets (#6638) 4f8964b
* **web:** [info] Fixes blocking Testlio feedback (#6724) 2bbc8f8
* **web:** [limits] presets breaking (#6634) 0036a7a
* **web:** fix react lifecycle warning by setting in useEffect (#6663) c4f9753
* **web:** fixes for Limits Menu on mobile layout (#6726) 7b44537
* **web:** limits gas estimates hotfix (#6729) 02de460
* **web:** merge Token fields in Apollo cache (#6611) 1d4853a
* **web:** use correct all-time swappers (#6635) c1394d6
### Code Refactoring
* **web:** Remove no longer needed ternary since BE update (#6688) 9af401a
web/5.16.2
\ No newline at end of file
web/5.17.0
\ No newline at end of file
......@@ -125,17 +125,17 @@ android {
dev {
isDefault(true)
applicationIdSuffix ".dev"
versionName "1.22"
versionName "1.23"
dimension "variant"
}
beta {
applicationIdSuffix ".beta"
versionName "1.22"
versionName "1.23"
dimension "variant"
}
prod {
dimension "variant"
versionName "1.22"
versionName "1.23"
}
}
......
......@@ -2450,7 +2450,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.22;
MARKETING_VERSION = 1.23;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -2496,7 +2496,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.22;
MARKETING_VERSION = 1.23;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets;
......@@ -2542,7 +2542,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.22;
MARKETING_VERSION = 1.23;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets;
......@@ -2588,7 +2588,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.22;
MARKETING_VERSION = 1.23;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets;
......@@ -2630,7 +2630,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.22;
MARKETING_VERSION = 1.23;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -2673,7 +2673,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.22;
MARKETING_VERSION = 1.23;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension;
......@@ -2716,7 +2716,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.22;
MARKETING_VERSION = 1.23;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension;
......@@ -2759,7 +2759,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.22;
MARKETING_VERSION = 1.23;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension;
......@@ -2795,7 +2795,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.22;
MARKETING_VERSION = 1.23;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -2833,7 +2833,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.22;
MARKETING_VERSION = 1.23;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3003,7 +3003,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.22;
MARKETING_VERSION = 1.23;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -3047,7 +3047,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.22;
MARKETING_VERSION = 1.23;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension;
......@@ -3143,7 +3143,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.22;
MARKETING_VERSION = 1.23;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3214,7 +3214,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.22;
MARKETING_VERSION = 1.23;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension;
......@@ -3310,7 +3310,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.22;
MARKETING_VERSION = 1.23;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3381,7 +3381,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.22;
MARKETING_VERSION = 1.23;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension;
......
......@@ -5,7 +5,8 @@ import mockRNCNetInfo from '@react-native-community/netinfo/jest/netinfo-mock.js
import 'core-js' // necessary so setImmediate works in tests
import { localizeMock as mockRNLocalize } from 'react-native-localize/mock'
import { AppearanceSettingType } from 'wallet/src/features/appearance/slice'
import { MockLocalizationContext } from 'wallet/src/test/utils'
import { initializeTranslation } from 'wallet/src/i18n/i18n'
import { mockLocalizationContext } from 'wallet/src/test/mocks/utils'
// avoids polluting console in test runs, while keeping important log levels
global.console = {
......@@ -18,6 +19,9 @@ global.console = {
// error: jest.fn(),
}
// Uses real translations for tests
initializeTranslation()
// Mock Sentry crash reporting
jest.mock('@sentry/react-native', () => ({
init: () => jest.fn(),
......@@ -83,7 +87,7 @@ jest.mock('@react-navigation/elements', () => ({
require('react-native-reanimated').setUpTests()
jest.mock('wallet/src/features/language/LocalizationContext', () => MockLocalizationContext)
jest.mock('wallet/src/features/language/LocalizationContext', () => mockLocalizationContext)
jest.mock('react-native/Libraries/Share/Share', () => ({
share: jest.fn(),
......@@ -111,22 +115,6 @@ jest.mock('react-native/Libraries/Linking/Linking', () => ({
getInitialURL: jest.fn(),
}))
jest.mock('react-i18next', () => ({
// this mock makes sure any components using the translate hook can use it without a warning being shown
useTranslation: () => {
return {
t: (str) => str,
i18n: {
changeLanguage: () => new Promise(jest.fn()),
},
}
},
initReactI18next: {
type: '3rdParty',
init: jest.fn(),
},
}))
// Mock the appearance hook for all tests
const mockAppearanceSetting = AppearanceSettingType.System
jest.mock('wallet/src/features/appearance/hooks', () => {
......
......@@ -143,6 +143,7 @@
"rive-react-native": "6.1.1",
"statsig-react-native": "4.11.0",
"typed-redux-saga": "1.5.0",
"uniswap": "workspace:^",
"utilities": "workspace:^",
"wallet": "workspace:^"
},
......
......@@ -72,8 +72,8 @@ function ErrorScreen({ error }: { error: Error }): JSX.Element {
<Flex centered grow gap="$spacing36">
<Image source={DEAD_LUNI} style={styles.errorImage} />
<Flex centered gap="$spacing8">
<Text variant="subheading1">{t('Uh oh!')}</Text>
<Text variant="body2">{t('Something crashed.')}</Text>
<Text variant="subheading1">{t('errors.crash.title')}</Text>
<Text variant="body2">{t('errors.crash.message')}</Text>
</Flex>
{error.message && __DEV__ && <Text variant="body2">{error.message}</Text>}
</Flex>
......@@ -82,7 +82,7 @@ function ErrorScreen({ error }: { error: Error }): JSX.Element {
onPress={(): void => {
RNRestart.Restart()
}}>
{t('Restart app')}
{t('errors.crash.restart')}
</Button>
</Flex>
</Flex>
......
......@@ -6,6 +6,7 @@ import { closeModal, openModal } from 'src/features/modals/modalSlice'
import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex'
import { Screens } from 'src/screens/Screens'
import {
NavigateToNftItemArgs,
NavigateToSwapFlowArgs,
WalletNavigationProvider,
} from 'wallet/src/contexts/WalletNavigationContext'
......@@ -16,6 +17,7 @@ export function MobileWalletNavigationProvider({ children }: PropsWithChildren):
const navigateToAccountActivityList = useNavigateToHomepageTab(HomeScreenTabIndex.Activity)
const navigateToAccountTokenList = useNavigateToHomepageTab(HomeScreenTabIndex.Tokens)
const navigateToBuyOrReceiveWithEmptyWallet = useNavigateToBuyOrReceiveWithEmptyWallet()
const navigateToNftDetails = useNavigateToNftDetails()
const navigateToSwapFlow = useNavigateToSwapFlow()
const navigateToTokenDetails = useNavigateToTokenDetails()
......@@ -24,6 +26,7 @@ export function MobileWalletNavigationProvider({ children }: PropsWithChildren):
navigateToAccountActivityList={navigateToAccountActivityList}
navigateToAccountTokenList={navigateToAccountTokenList}
navigateToBuyOrReceiveWithEmptyWallet={navigateToBuyOrReceiveWithEmptyWallet}
navigateToNftDetails={navigateToNftDetails}
navigateToSwapFlow={navigateToSwapFlow}
navigateToTokenDetails={navigateToTokenDetails}>
{children}
......@@ -64,6 +67,23 @@ function useNavigateToTokenDetails(): (currencyId: string) => void {
)
}
function useNavigateToNftDetails(): (args: NavigateToNftItemArgs) => void {
const navigation = useAppStackNavigation()
return useCallback(
({ owner, address, tokenId, isSpam, fallbackData }: NavigateToNftItemArgs): void => {
navigation.navigate(Screens.NFTItem, {
owner,
address,
tokenId,
isSpam,
fallbackData,
})
},
[navigation]
)
}
function useNavigateToBuyOrReceiveWithEmptyWallet(): () => void {
const dispatch = useAppDispatch()
......
......@@ -97,7 +97,25 @@ import {
} from 'wallet/src/features/wallet/accounts/types'
import { initialWalletState, SwapProtectionSetting } from 'wallet/src/features/wallet/slice'
import { ModalName } from 'wallet/src/telemetry/constants'
import { account, fiatOnRampTxDetailsFailed, txDetailsConfirmed } from 'wallet/src/test/fixtures'
import {
fiatPurchaseTransactionInfo,
signerMnemonicAccount,
transactionDetails,
} from 'wallet/src/test/fixtures'
const account = signerMnemonicAccount()
const txDetailsConfirmed = transactionDetails({
status: TransactionStatus.Success,
})
const fiatOnRampTxDetailsFailed = transactionDetails({
status: TransactionStatus.Failed,
typeInfo: fiatPurchaseTransactionInfo({
explorerUrl:
'https://buy-sandbox.moonpay.com/transaction_receipt?transactionId=d6c32bb5-7cd9-4c22-8f46-6bbe786c599f',
id: 'd6c32bb5-7cd9-4c22-8f46-6bbe786c599f',
}),
})
// helps with object assignment
// eslint-disable-next-line @typescript-eslint/no-explicit-any
......
......@@ -6,11 +6,11 @@ import { MobileState } from 'src/app/reducer'
import { initialModalState } from 'src/features/modals/modalSlice'
import { render } from 'src/test/test-utils'
import { ModalName } from 'wallet/src/telemetry/constants'
import { mockWalletPreloadedState } from 'wallet/src/test/fixtures'
import { noOpFunction } from 'wallet/src/test/utils'
import { ACCOUNT } from 'wallet/src/test/fixtures'
import { mockWalletPreloadedState, noOpFunction } from 'wallet/src/test/mocks'
const preloadedState = {
...mockWalletPreloadedState,
...mockWalletPreloadedState(ACCOUNT),
modals: {
...initialModalState,
[ModalName.AccountSwitcher]: { isOpen: true },
......
......@@ -190,17 +190,19 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
if (!cloudStorageAvailable) {
Alert.alert(
isAndroid ? t('Google Drive not available') : t('iCloud Drive not available'),
isAndroid
? t(
'Please verify that you are logged in to a Google account with Google Drive enabled on this device and try again.'
)
: t(
'Please verify that you are logged in to an Apple ID with iCloud Drive enabled on this device and try again.'
),
? t('account.cloud.error.unavailable.title.android')
: t('account.cloud.error.unavailable.title.ios'),
isAndroid
? t('account.cloud.error.unavailable.message.android')
: t('account.cloud.error.unavailable.message.ios'),
[
{ text: t('Go to settings'), onPress: openSettings, style: 'default' },
{ text: t('Not now'), style: 'cancel' },
{
text: t('account.cloud.error.unavailable.button.settings'),
onPress: openSettings,
style: 'default',
},
{ text: t('account.cloud.error.unavailable.button.cancel'), style: 'cancel' },
]
)
return
......@@ -224,7 +226,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
borderBottomColor="$surface3"
borderBottomWidth={1}
p="$spacing16">
<Text variant="body1">{t('Create a new wallet')}</Text>
<Text variant="body1">{t('account.wallet.button.create')}</Text>
</Flex>
),
},
......@@ -233,7 +235,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
onPress: onPressAddViewOnlyWallet,
render: () => (
<Flex alignItems="center" p="$spacing16">
<Text variant="body1">{t('Add a view-only wallet')}</Text>
<Text variant="body1">{t('account.wallet.button.addViewOnly')}</Text>
</Flex>
),
},
......@@ -242,7 +244,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
onPress: onPressImportWallet,
render: () => (
<Flex alignItems="center" borderTopColor="$surface3" borderTopWidth={1} p="$spacing16">
<Text variant="body1">{t('Import a new wallet')}</Text>
<Text variant="body1">{t('account.wallet.button.import')}</Text>
</Flex>
),
},
......@@ -255,7 +257,9 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
render: () => (
<Flex alignItems="center" borderTopColor="$surface3" borderTopWidth={1} p="$spacing16">
<Text variant="body1">
{isAndroid ? t('Restore from Google Drive') : t('Restore from iCloud')}
{isAndroid
? t('account.cloud.button.restore.android')
: t('account.cloud.button.restore.ios')}
</Text>
</Flex>
),
......@@ -295,7 +299,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
testID={ElementName.WalletSettings}
theme="secondary"
onPress={onManageWallet}>
{t('Manage wallet')}
{t('account.wallet.button.manage')}
</Button>
</Flex>
</Flex>
......@@ -310,7 +314,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
<Icons.Plus color="$neutral2" size="$icon.12" strokeWidth={2} />
</Flex>
<Text color="$neutral2" variant="buttonLabel3">
{t('Add wallet')}
{t('account.wallet.button.add')}
</Text>
</Flex>
</TouchableArea>
......
......@@ -46,11 +46,9 @@ export function ViewOnlyExplainerModal(): JSX.Element {
<WalletImage height="100%" preserveAspectRatio="xMidYMid slice" width="100%" />
</Flex>
<Flex alignItems="center" gap="$spacing4">
<Text variant="subheading1">{t('This wallet is view-only')}</Text>
<Text variant="subheading1">{t('account.wallet.viewOnly.title')}</Text>
<Text color="$neutral2" textAlign="center" variant="body2">
{t(
'To swap, buy, send, and receive tokens, you need to import this wallet’s recovery phrase.'
)}
{t('account.wallet.viewOnly.description')}
</Text>
</Flex>
</Flex>
......@@ -61,7 +59,7 @@ export function ViewOnlyExplainerModal(): JSX.Element {
px={40}
theme="primary"
onPress={onPressImportWallet}>
{t('Import wallet')}
{t('account.wallet.viewOnly.button')}
</Button>
<Button
alignSelf="center"
......@@ -71,7 +69,7 @@ export function ViewOnlyExplainerModal(): JSX.Element {
px={40}
theme="secondary"
onPress={onClose}>
{t('Maybe later')}
{t('common.button.later')}
</Button>
</Flex>
</Flex>
......
......@@ -6,7 +6,7 @@ import { MobileState } from 'src/app/reducer'
import { initialModalState } from 'src/features/modals/modalSlice'
import { renderWithProviders } from 'src/test/render'
import { ModalName } from 'wallet/src/telemetry/constants'
import { mockWalletPreloadedState } from 'wallet/src/test/fixtures'
import { mockWalletPreloadedState } from 'wallet/src/test/mocks'
const preloadedState = {
...mockWalletPreloadedState,
......
......@@ -174,7 +174,7 @@ const SwapFAB = memo(function _SwapFAB({ activeScale = 0.96 }: SwapTabBarButtonP
color="$sporeWhite"
numberOfLines={1}
variant="buttonLabel2">
{t('Swap')}
{t('common.button.swap')}
</Text>
</AnimatedFlex>
</TapGestureHandler>
......@@ -256,7 +256,7 @@ function ExploreTabBarButton({ activeScale = 0.98 }: ExploreTabBarButtonProps):
pr="$spacing48"
style={{ lineHeight: fonts.body1.lineHeight }}
variant="body1">
{t('Search')}
{t('common.input.search')}
</Text>
</Flex>
</BlurView>
......
......@@ -29,9 +29,9 @@ export function PriceExplorerError({
justifyContent="center"
overflow="hidden">
<BaseCard.ErrorState
description={t('Something went wrong.')}
retryButtonLabel={showRetry ? t('Retry') : undefined}
title={t('Couldn’t load price chart')}
description={t('token.priceExplorer.error.description')}
retryButtonLabel={showRetry ? t('common.button.retry') : undefined}
title={t('token.priceExplorer.error.title')}
onRetry={onRetry}
/>
</Flex>
......
......@@ -2,7 +2,7 @@ import React from 'react'
import * as charts from 'react-native-wagmi-charts'
import { DatetimeText, PriceText, RelativeChangeText } from 'src/components/PriceExplorer/Text'
import { render, within } from 'src/test/test-utils'
import { Amounts } from 'wallet/src/test/gqlFixtures'
import { amounts } from 'wallet/src/test/fixtures'
jest.mock('react-native-wagmi-charts')
const mockedUseLineChartPrice = charts.useLineChartPrice as jest.Mock
......@@ -12,7 +12,7 @@ const mockedUseLineChartDatetime = charts.useLineChartDatetime as jest.Mock
describe(PriceText, () => {
it('renders without error', () => {
mockedUseLineChartPrice.mockReturnValue({ value: '' })
mockedUseLineChart.mockReturnValue({ data: [{ timestamp: 0, value: Amounts.md.value }] })
mockedUseLineChart.mockReturnValue({ data: [{ timestamp: 0, value: amounts.md().value }] })
const tree = render(<PriceText loading={false} />)
......@@ -21,7 +21,7 @@ describe(PriceText, () => {
it('renders without error less than a dollar', () => {
mockedUseLineChartPrice.mockReturnValue({ value: '' })
mockedUseLineChart.mockReturnValue({ data: [{ timestamp: 0, value: Amounts.xs.value }] })
mockedUseLineChart.mockReturnValue({ data: [{ timestamp: 0, value: amounts.xs().value }] })
const tree = render(<PriceText loading={false} />)
......@@ -39,7 +39,7 @@ describe(PriceText, () => {
it('shows active price when scrubbing', async () => {
mockedUseLineChartPrice.mockReturnValue({
value: { value: Amounts.sm.value.toString() },
value: { value: amounts.sm().value.toString() },
})
const tree = render(<PriceText loading={false} />)
......@@ -48,7 +48,7 @@ describe(PriceText, () => {
const wholePart = await within(animatedText).findByTestId('wholePart')
const decimalPart = await within(animatedText).findByTestId('decimalPart')
expect(wholePart.props.text).toBe(`$${Amounts.sm.value}`)
expect(wholePart.props.text).toBe(`$${amounts.sm().value}`)
expect(decimalPart.props.text).toBe(`.00`)
})
})
......
......@@ -11,9 +11,25 @@ export const CURSOR_SIZE = CURSOR_INNER_SIZE + 6
export const LINE_WIDTH = 1
export const TIME_RANGES = [
[HistoryDuration.Hour, i18n.t('1H'), ElementName.TimeFrame1H],
[HistoryDuration.Day, i18n.t('1D'), ElementName.TimeFrame1D],
[HistoryDuration.Week, i18n.t('1W'), ElementName.TimeFrame1W],
[HistoryDuration.Month, i18n.t('1M'), ElementName.TimeFrame1M],
[HistoryDuration.Year, i18n.t('1Y'), ElementName.TimeFrame1Y],
[
HistoryDuration.Hour,
i18n.t('token.priceExplorer.timeRangeLabel.hour'),
ElementName.TimeFrame1H,
],
[HistoryDuration.Day, i18n.t('token.priceExplorer.timeRangeLabel.day'), ElementName.TimeFrame1D],
[
HistoryDuration.Week,
i18n.t('token.priceExplorer.timeRangeLabel.week'),
ElementName.TimeFrame1W,
],
[
HistoryDuration.Month,
i18n.t('token.priceExplorer.timeRangeLabel.month'),
ElementName.TimeFrame1M,
],
[
HistoryDuration.Year,
i18n.t('token.priceExplorer.timeRangeLabel.year'),
ElementName.TimeFrame1Y,
],
] as const
......@@ -94,7 +94,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
)[0]
if (!result) {
Alert.alert(t('No QR code found'))
Alert.alert(t('qrScanner.error.none'))
setIsReadingImageFile(false)
return
}
......@@ -109,16 +109,12 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
}
if (permissionStatus === PermissionStatus.DENIED) {
Alert.alert(
t('Camera is disabled'),
t('To scan a code, allow Camera access in system settings'),
[
{ text: t('Go to settings'), onPress: openSettings },
Alert.alert(t('qrScanner.error.camera.title'), t('qrScanner.error.camera.message'), [
{ text: t('common.navigation.systemSettings'), onPress: openSettings },
{
text: t('Not now'),
text: t('common.button.notNow'),
},
]
)
])
}
}, [permissionStatus, requestPermissionResponse, t])
......@@ -177,7 +173,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
width="100%"
onLayout={(event: LayoutChangeEvent): void => setInfoLayout(event.nativeEvent.layout)}>
<Text color="$neutral1" variant="heading3">
{t('Scan a QR code')}
{t('qrScanner.title')}
</Text>
</Flex>
{!shouldFreezeCamera ? (
......@@ -205,7 +201,9 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
</Flex>
<Flex style={{ marginTop: LOADER_SIZE + spacing.spacing24 }} />
<Text color="$neutral1" textAlign="center" variant="body1">
{isWalletConnectModal ? t('Connecting...') : t('Loading...')}
{isWalletConnectModal
? t('qrScanner.status.connecting')
: t('qrScanner.status.loading')}
</Text>
</Flex>
</Flex>
......@@ -268,11 +266,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
icon={<Icons.Global color="$neutral2" />}
theme="secondary"
onPress={props.onPressConnections}>
{props.numConnections === 1
? t('1 app connected')
: t('{{numConnections}} apps connected', {
numConnections: props.numConnections,
})}
{t('qrScanner.button.connections', { count: props.numConnections })}
</Button>
)}
</Flex>
......
......@@ -61,7 +61,7 @@ export function WalletQRCode({ address }: Props): JSX.Element | null {
/>
<Text color="$neutral2" lineHeight={20} textAlign="center" variant="body3">
{t('You can send tokens on all of our supported networks to this address.')}
{t('qrScanner.wallet.title')}
</Text>
<TouchableArea onPress={(): void => setShowModal(true)}>
<Flex row gap="$spacing4">
......@@ -78,10 +78,8 @@ export function WalletQRCode({ address }: Props): JSX.Element | null {
{showModal && (
<WarningModal
backgroundIconColor={colors.surface1.val}
caption={t(
'Uniswap Wallet supports tokens on Ethereum, Polygon, Arbitrum, Optimism, Base, and BNB Chain. Right now, we only support NFTs on Ethereum.'
)}
closeText={t('Close')}
caption={t('qrScanner.wallet.networks.description')}
closeText={t('common.button.close')}
icon={
<NetworkLogos
centered
......@@ -91,7 +89,7 @@ export function WalletQRCode({ address }: Props): JSX.Element | null {
/>
}
modalName={ModalName.QRCodeNetworkInfo}
title={t('Supported Networks')}
title={t('qrScanner.wallet.networks.title')}
onClose={(): void => setShowModal(false)}>
<LearnMoreLink url={uniswapUrls.helpArticleUrls.supportedNetworks} />
</WarningModal>
......
......@@ -44,18 +44,14 @@ export function RecipientScanModal({ onSelectRecipient, onClose }: Props): JSX.E
onSelectRecipient(supportedURI.value)
onClose()
} else {
Alert.alert(
t('Invalid QR Code'),
t('Make sure that you’re scanning a valid Ethereum address QR code before trying again.'),
[
Alert.alert(t('qrScanner.recipient.error.title'), t('qrScanner.recipient.error.message'), [
{
text: t('Try again'),
text: t('common.button.tryAgain'),
onPress: (): void => {
setShouldFreezeCamera(false)
},
},
]
)
])
}
}
......@@ -107,8 +103,8 @@ export function RecipientScanModal({ onSelectRecipient, onClose }: Props): JSX.E
)}
<Text color="$neutral1" variant="buttonLabel2">
{currentScreenState === ScannerModalState.ScanQr
? t('Show my QR code')
: t('Scan a QR code')}
? t('qrScanner.recipient.action.show')
: t('qrScanner.recipient.action.scan')}
</Text>
</Flex>
</TouchableArea>
......
......@@ -75,22 +75,22 @@ export function _RecipientSelect({
mt="$spacing16"
px="$spacing24">
<Flex row>
<Text variant="subheading1">{t('Send')}</Text>
<Text variant="subheading1">{t('qrScanner.recipient.label.send')}</Text>
</Flex>
<SearchBar
autoFocus
backgroundColor="$surface2"
endAdornment={<QRScannerIconButton onPress={onPressQRScanner} />}
placeholder={t('Search ENS or address')}
placeholder={t('qrScanner.recipient.input.placeholder')}
value={pattern ?? ''}
onBack={recipient ? onToggleShowRecipientSelector : undefined}
onChangeText={onChangePattern}
/>
{noResults ? (
<Flex centered gap="$spacing12" mt="$spacing24" px="$spacing24">
<Text variant="buttonLabel2">{t('No results found')}</Text>
<Text variant="buttonLabel2">{t('qrScanner.recipient.results.empty')}</Text>
<Text color="$neutral3" textAlign="center" variant="body1">
{t('The address you typed either does not exist or is spelled incorrectly.')}
{t('qrScanner.recipient.results.error')}
</Text>
</Flex>
) : (
......
......@@ -8,20 +8,34 @@ import { useRecipients } from 'wallet/src/components/RecipientSearch/hooks'
import { ChainId } from 'wallet/src/constants/chains'
import { SearchableRecipient } from 'wallet/src/features/address/types'
import { TransactionStateMap } from 'wallet/src/features/transactions/slice'
import { SendTokenTransactionInfo } from 'wallet/src/features/transactions/types'
import { TransactionStatus } from 'wallet/src/features/transactions/types'
import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice'
import {
account,
account2,
SAMPLE_SEED_ADDRESS_1,
SAMPLE_SEED_ADDRESS_2,
sendTxDetailsConfirmed,
sendTxDetailsFailed,
sendTxDetailsPending,
sendTokenTransactionInfo,
signerMnemonicAccount,
transactionDetails,
} from 'wallet/src/test/fixtures'
expect.extend({ toIncludeSameMembers })
const sendTxDetailsPending = transactionDetails({
status: TransactionStatus.Pending,
typeInfo: sendTokenTransactionInfo(),
addedTime: 1487076708000,
})
const sendTxDetailsConfirmed = transactionDetails({
status: TransactionStatus.Success,
typeInfo: sendTokenTransactionInfo(),
addedTime: 1487076708000,
})
const sendTxDetailsFailed = transactionDetails({
status: TransactionStatus.Failed,
typeInfo: sendTokenTransactionInfo(),
addedTime: 1487076710000,
})
/**
* Tests interaction of mobile state with useRecipients hook
*/
......@@ -58,8 +72,8 @@ const getPreloadedState = (props?: PreloadedStateProps): PreloadedState<MobileSt
}
}
const activeAccount = account
const inactiveAccount = account2
const activeAccount = signerMnemonicAccount()
const inactiveAccount = signerMnemonicAccount()
const validatedAddressRecipient: SearchableRecipient = {
address: SAMPLE_SEED_ADDRESS_1,
}
......@@ -75,15 +89,15 @@ const recentRecipientsSectionResult = {
title: 'Recent',
data: [
{
address: (sendTxDetailsFailed.typeInfo as SendTokenTransactionInfo).recipient,
address: sendTxDetailsFailed.typeInfo.recipient,
name: '',
},
{
address: (sendTxDetailsConfirmed.typeInfo as SendTokenTransactionInfo).recipient,
address: sendTxDetailsConfirmed.typeInfo.recipient,
name: '',
},
{
address: (sendTxDetailsPending.typeInfo as SendTokenTransactionInfo).recipient,
address: sendTxDetailsPending.typeInfo.recipient,
name: '',
},
],
......@@ -162,9 +176,7 @@ describe(useRecipients, () => {
expect(result.current.searchableRecipientOptions).toEqual(
expect.arrayContaining([
{
data: {
address: SAMPLE_SEED_ADDRESS_1,
},
data: expect.objectContaining({ address: SAMPLE_SEED_ADDRESS_1 }),
key: SAMPLE_SEED_ADDRESS_1,
},
])
......@@ -203,7 +215,7 @@ describe(useRecipients, () => {
title: 'Recent',
data: [
{
address: (sendTxDetailsPending.typeInfo as SendTokenTransactionInfo).recipient,
address: sendTxDetailsPending.typeInfo.recipient,
name: '',
},
],
......@@ -231,15 +243,15 @@ describe(useRecipients, () => {
// This method doesn't check the order of the elements
expect(section.data).toIncludeSameMembers([
{
address: (sendTxDetailsPending.typeInfo as SendTokenTransactionInfo).recipient,
address: sendTxDetailsPending.typeInfo.recipient,
name: '',
},
{
address: (sendTxDetailsConfirmed.typeInfo as SendTokenTransactionInfo).recipient,
address: sendTxDetailsConfirmed.typeInfo.recipient,
name: '',
},
{
address: (sendTxDetailsFailed.typeInfo as SendTokenTransactionInfo).recipient,
address: sendTxDetailsFailed.typeInfo.recipient,
name: '',
},
])
......
......@@ -29,9 +29,7 @@ export function RemoveLastMnemonicWalletFooter({
text={
<Flex>
<Text color="$neutral2" variant="body3">
{t(
'I backed up my recovery phrase and understand that Uniswap Labs can’t help me recover my wallets if I failed to do so.'
)}
{t('account.wallet.remove.check')}
</Text>
</Flex>
}
......@@ -46,7 +44,7 @@ export function RemoveLastMnemonicWalletFooter({
testID={ElementName.Confirm}
theme="detrimental"
onPress={onPress}>
{!inProgress ? t('Remove wallet') : undefined}
{!inProgress ? t('account.wallet.button.remove') : undefined}
</Button>
</Flex>
</>
......
......@@ -189,7 +189,7 @@ export function RemoveWalletModal(): JSX.Element | null {
<AnimatedFlex style={animatedCancelButtonSpanStyles} />
) : (
<Button fill disabled={inProgress} theme="outline" onPress={onClose}>
{t('Cancel')}
{t('common.button.cancel')}
</Button>
)}
<Button
......
......@@ -9,7 +9,7 @@ import WalletIcon from 'ui/src/assets/icons/wallet-filled.svg'
import { ThemeNames } from 'ui/src/theme'
import { Account, AccountType } from 'wallet/src/features/wallet/accounts/types'
import { useDisplayName } from 'wallet/src/features/wallet/hooks'
import { isAndroid } from 'wallet/src/utils/platform'
import { getCloudProviderName } from 'wallet/src/utils/platform'
export enum RemoveWalletStep {
Warning = 'warning',
......@@ -49,21 +49,19 @@ export const useModalContent = ({
if (isRemovingRecoveryPhrase && !isReplacing && currentStep === RemoveWalletStep.Warning) {
return {
title: (
<Trans t={t}>
<Text color="$neutral1" variant="body1">
You’re removing{' '}
<Trans i18nKey="account.seedPhrase.remove.initial.title">
You’re removing
<Text color="$statusCritical" variant="body1">
{{ wallet: displayName?.name }}
</Text>
{{ walletName: displayName?.name }}
</Text>
</Trans>
</Text>
),
description: t(
'This will remove your wallet from this device along with your recovery phrase.'
),
description: t('account.seedPhrase.remove.initial.description'),
Icon: TrashIcon,
iconColorLabel: 'statusCritical',
actionButtonLabel: t('Continue'),
actionButtonLabel: t('common.button.continue'),
actionButtonTheme: 'detrimental',
}
}
......@@ -71,13 +69,11 @@ export const useModalContent = ({
// 1st speed bump when replacing recovery phrase
if (isRemovingRecoveryPhrase && isReplacing && currentStep === RemoveWalletStep.Warning) {
return {
title: t('Import a new wallet'),
description: t(
'You can only store one recovery phrase at a time. To continue importing a new one, you’ll need to remove your current recovery phrase and any associated wallets from this device.'
),
title: t('account.wallet.button.import'),
description: t('account.seedPhrase.remove.import.description'),
Icon: WalletIcon,
iconColorLabel: 'neutral2',
actionButtonLabel: t('Continue'),
actionButtonLabel: t('common.button.continue'),
actionButtonTheme: 'secondary',
}
}
......@@ -86,25 +82,19 @@ export const useModalContent = ({
if (isRemovingRecoveryPhrase && currentStep === RemoveWalletStep.Final) {
return {
title: (
<Trans t={t}>
<Text color="$neutral1" variant="body1">
You’re removing your{' '}
<Trans i18nKey="account.seedPhrase.remove.final.title">
You’re removing your
<Text color="$neutral1" variant="body1">
recovery phrase
</Text>
</Text>
</Trans>
),
description: isAndroid ? (
<Trans t={t}>
Make sure you’ve written down your recovery phrase or backed it up on Google Drive.{' '}
<Text color="$statusCritical" maxFontSizeMultiplier={1.4} variant="body3">
You will not be able to access your funds otherwise.
</Text>
</Trans>
) : (
<Trans t={t}>
Make sure you’ve written down your recovery phrase or backed it up on iCloud.{' '}
),
description: (
<Trans i18nKey="account.seedPhrase.remove.final.description">
Make sure you’ve written down your recovery phrase or backed it up on
{{ cloudProviderName: getCloudProviderName() }}.
<Text color="$statusCritical" maxFontSizeMultiplier={1.4} variant="body3">
You will not be able to access your funds otherwise.
</Text>
......@@ -119,34 +109,32 @@ export const useModalContent = ({
if (account?.type === AccountType.SignerMnemonic && currentStep === RemoveWalletStep.Final) {
const associatedAccountNames = concatListOfAccountNames(
associatedAccounts.filter((aa) => aa.address !== account?.address),
t('and')
', '
)
return {
title: (
<Trans t={t}>
<Text color="$neutral1" variant="body1">
You’re removing{' '}
<Trans i18nKey="account.seedPhrase.remove.initial.title">
You’re removing
<Text color="$statusCritical" variant="body1">
{{ wallet: displayName?.name }}
</Text>
{{ walletName: displayName?.name }}
</Text>
</Trans>
</Text>
),
description: (
<Text color="$neutral2" variant="body3">
<Trans t={t}>
It shares the same recovery phrase as{' '}
<Trans i18nKey="account.seedPhrase.remove.mnemonic.description">
It shares the same recovery phrase as
<Text color="$neutral1" variant="body3">
{{ wallets: associatedAccountNames }}
{{ walletNames: associatedAccountNames }}
</Text>
. Your recovery phrase will remain stored until you delete all remaining wallets.
</Trans>
</Text>
),
Icon: TrashIcon,
iconColorLabel: 'statusCritical',
actionButtonLabel: t('Remove'),
actionButtonLabel: t('common.button.remove'),
actionButtonTheme: 'detrimental',
}
}
......@@ -155,21 +143,19 @@ export const useModalContent = ({
if (account?.type === AccountType.Readonly && currentStep === RemoveWalletStep.Final) {
return {
title: (
<Trans t={t}>
<Text color="$neutral1" variant="body1">
You’re removing{' '}
<Trans i18nKey="account.seedPhrase.remove.initial.title">
You’re removing
<Text color="$neutral2" variant="body1">
{{ wallet: displayName?.name }}
</Text>
{{ walletName: displayName?.name }}
</Text>
</Trans>
</Text>
),
description: t(
'You can always add back view-only wallets by entering the wallet’s address.'
),
description: t('account.wallet.remove.viewOnly'),
Icon: TrashIcon,
iconColorLabel: 'neutral2',
actionButtonLabel: t('Remove'),
actionButtonLabel: t('common.button.remove'),
actionButtonTheme: 'secondary',
}
}
......
......@@ -51,19 +51,17 @@ export function RestoreWalletModal(): JSX.Element | null {
/>
</Flex>
<Text textAlign="center" variant="body1">
{t('Restore wallet')}
{t('account.wallet.button.restore')}
</Text>
<Text color="$neutral2" textAlign="center" variant="body2">
{t(
'Because you’re on a new device, you’ll need to restore your recovery phrase. This will allow you to swap and send tokens.'
)}
{t('account.wallet.restore.description')}
</Text>
<Flex centered row gap="$spacing12" pt="$spacing12">
<Button fill theme="tertiary" onPress={onDismiss}>
{t('Dismiss')}
{t('common.button.dismiss')}
</Button>
<Button fill testID={ElementName.RestoreWallet} theme="primary" onPress={onRestore}>
{t('Restore')}
{t('common.button.restore')}
</Button>
</Flex>
</Flex>
......
......@@ -7,6 +7,7 @@ import {
} from 'wallet/src/components/modals/WarningModal/WarningModal'
import { WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types'
import { ModalName } from 'wallet/src/telemetry/constants'
import { isAndroid } from 'wallet/src/utils/platform'
type Props = {
isTouchIdDevice: boolean
......@@ -20,18 +21,19 @@ export function BiometricAuthWarningModal({
onClose,
}: Props): JSX.Element {
const { t } = useTranslation()
const authenticationTypeName = useBiometricName(isTouchIdDevice)
const biometricsMethod = useBiometricName(isTouchIdDevice)
return (
<WarningModal
caption={t(
'If you don’t turn on {{authenticationTypeName}}, anyone who gains access to your device can open Uniswap Wallet and make transactions.',
{ authenticationTypeName }
)}
closeText={t('Back')}
confirmText={t('Skip')}
caption={
isAndroid
? t('settings.setting.biometrics.warning.message.android')
: t('settings.setting.biometrics.warning.message.ios', { biometricsMethod })
}
closeText={t('common.button.back')}
confirmText={t('common.button.skip')}
modalName={ModalName.FaceIDWarning}
severity={WarningSeverity.Low}
title={t('Are you sure?')}
title={t('settings.setting.biometrics.warning.title')}
onClose={onClose}
onConfirm={onConfirm}
/>
......
......@@ -164,7 +164,7 @@ export const TokenBalanceListInner = forwardRef<
const ListHeaderComponent = useMemo(() => {
return hasError ? (
<AnimatedFlex entering={FadeInDown} exiting={FadeOut} px="$spacing24" py="$spacing8">
<BaseCard.InlineErrorState title={t('Failed to fetch token balances')} onRetry={refetch} />
<BaseCard.InlineErrorState title={t('home.tokens.error.fetch')} onRetry={refetch} />
</AnimatedFlex>
) : null
}, [hasError, refetch, t])
......@@ -204,8 +204,8 @@ export const TokenBalanceListInner = forwardRef<
) : (
<Flex fill grow justifyContent="center" style={containerProps?.emptyContainerStyle}>
<BaseCard.ErrorState
retryButtonLabel="Retry"
title={t('Couldn’t load token balances')}
retryButtonLabel={t('common.button.retry')}
title={t('home.tokens.error.load')}
onRetry={(): void | undefined => refetch?.()}
/>
</Flex>
......
......@@ -68,7 +68,7 @@ export function TokenBalances({
{hasOtherChainBalances && otherChainBalances ? (
<Flex gap="$spacing8">
<Text color="$neutral2" variant="subheading2">
{t('Balances on other networks')}
{t('token.balances.other')}
</Text>
<Flex gap="$spacing12">
{otherChainBalances.map((balance) => {
......@@ -106,7 +106,9 @@ export function CurrentChainBalance({
<Flex row>
<Flex fill gap="$spacing8">
<Text color="$neutral2" variant="subheading2">
{isReadonly ? t('{{owner}}’s balance', { owner: displayName }) : t('Your balance')}
{isReadonly
? t('token.balances.viewOnly', { ownerAddress: displayName })
: t('token.balances.main')}
</Text>
<Flex fill gap="$spacing4">
<Text variant="heading3">
......
......@@ -58,13 +58,13 @@ export function TokenDetailsActionButtons({
px="$spacing16">
<CTAButton
element={ElementName.Buy}
title={t('Buy')}
title={t('common.button.buy')}
tokenColor={tokenColor}
onPress={onPressBuy}
/>
<CTAButton
element={ElementName.Sell}
title={t('Sell')}
title={t('common.button.sell')}
tokenColor={tokenColor}
onPress={onPressSell}
/>
......
......@@ -35,7 +35,7 @@ export function TokenDetailsLinks({
<View style={{ marginHorizontal: -14 }}>
<Flex gap="$spacing8">
<Text color="$neutral2" mx="$spacing16" variant="subheading2">
{t('Links')}
{t('token.links.title')}
</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<Flex row gap="$spacing8" px="$spacing16">
......@@ -51,7 +51,7 @@ export function TokenDetailsLinks({
Icon={GlobeIcon}
buttonType={LinkButtonType.Link}
element={ElementName.TokenLinkWebsite}
label={t('Website')}
label={t('token.links.website')}
value={homepageUrl}
/>
)}
......@@ -60,7 +60,7 @@ export function TokenDetailsLinks({
Icon={TwitterIcon}
buttonType={LinkButtonType.Link}
element={ElementName.TokenLinkTwitter}
label={t('Twitter')}
label={t('token.links.twitter')}
value={getTwitterLink(twitterName)}
/>
)}
......@@ -68,7 +68,7 @@ export function TokenDetailsLinks({
<LinkButton
buttonType={LinkButtonType.Copy}
element={ElementName.Copy}
label={t('Contract')}
label={t('token.links.contract')}
value={address}
/>
)}
......
......@@ -57,7 +57,7 @@ export function TokenDetailsMarketData({
return (
<Flex gap="$spacing8">
<StatsRow
label={t('Market Cap')}
label={t('token.stats.marketCap')}
statsIcon={
<Icons.ChartPie color={tokenColor ?? defaultTokenColor} size={iconSizes.icon16} />
}>
......@@ -66,7 +66,7 @@ export function TokenDetailsMarketData({
</Text>
</StatsRow>
<StatsRow
label={t('Fully Diluted Valuation')}
label={t('token.stats.fullyDilutedValuation')}
statsIcon={
<Icons.ChartPie color={tokenColor ?? defaultTokenColor} size={iconSizes.icon16} />
}>
......@@ -75,7 +75,7 @@ export function TokenDetailsMarketData({
</Text>
</StatsRow>
<StatsRow
label={t('24h Volume')}
label={t('token.stats.volume')}
statsIcon={
<Icons.ChartBar color={tokenColor ?? defaultTokenColor} size={iconSizes.icon16} />
}>
......@@ -84,7 +84,7 @@ export function TokenDetailsMarketData({
</Text>
</StatsRow>
<StatsRow
label={t('52W High')}
label={t('token.stats.priceHighYear')}
statsIcon={
<Icons.TrendUp color={tokenColor ?? defaultTokenColor} size={iconSizes.icon16} />
}>
......@@ -93,7 +93,7 @@ export function TokenDetailsMarketData({
</Text>
</StatsRow>
<StatsRow
label={t('52W Low')}
label={t('token.stats.priceLowYear')}
statsIcon={
<Icons.TrendDown color={tokenColor ?? defaultTokenColor} size={iconSizes.icon16} />
}>
......@@ -147,7 +147,7 @@ export function TokenDetailsStats({
<Flex gap="$spacing4">
{name && (
<Text color="$neutral2" variant="subheading2">
{t('About {{ token }}', { token: name })}
{t('token.stats.section.about', { token: name })}
</Text>
)}
<Flex gap="$spacing16">
......@@ -177,7 +177,7 @@ export function TokenDetailsStats({
</Text>
</Flex>
<Text color="$blue400" variant="buttonLabel4">
{t('Show original')}
{t('token.stats.translation.original')}
</Text>
</Flex>
) : (
......@@ -185,7 +185,7 @@ export function TokenDetailsStats({
<Flex row alignItems="center" gap="$spacing12">
<Icons.Language color="$neutral2" size="$icon.20" />
<Text color="$neutral2" variant="body3">
{t('Translate to {{ language }}', {
{t('token.stats.translation.translate', {
language: currentLanguageInfo.displayName,
})}
</Text>
......@@ -199,7 +199,7 @@ export function TokenDetailsStats({
)}
<Flex gap="$spacing4">
<Text color="$neutral2" variant="subheading2">
{t('Stats')}
{t('token.stats.title')}
</Text>
<TokenDetailsMarketData
fullyDilutedValuation={fullyDilutedValuation}
......
import { useCrossChainBalances, useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks'
import { Screens } from 'src/screens/Screens'
import { act, renderHook, waitFor } from 'src/test/test-utils'
import { USDBC_BASE, USDC_ARBITRUM } from 'wallet/src/constants/tokens'
import { Chain } from 'wallet/src/data/__generated__/types-and-hooks'
import { fromGraphQLChain } from 'wallet/src/features/chains/utils'
import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils'
import { SAMPLE_CURRENCY_ID_1, mockWalletPreloadedState } from 'wallet/src/test/fixtures'
import { Portfolio, Portfolio2, PortfolioBalancesById } from 'wallet/src/test/gqlFixtures'
import {
SAMPLE_CURRENCY_ID_1,
portfolio,
portfolioBalances,
tokenBalance,
usdcArbitrumToken,
usdcBaseToken,
} from 'wallet/src/test/fixtures'
import { mockWalletPreloadedState } from 'wallet/src/test/mocks'
const mockedNavigation = {
navigate: jest.fn(),
......@@ -28,7 +32,7 @@ describe(useCrossChainBalances, () => {
describe('currentChainBalance', () => {
it('returns null if there are no balances for the specified currency', async () => {
const { result } = renderHook(() => useCrossChainBalances(SAMPLE_CURRENCY_ID_1, null), {
preloadedState: mockWalletPreloadedState,
preloadedState: mockWalletPreloadedState(),
})
await act(() => undefined)
......@@ -41,19 +45,25 @@ describe(useCrossChainBalances, () => {
})
it('returns balance if there is at least one for the specified currency', async () => {
const { result } = renderHook(() => useCrossChainBalances(SAMPLE_CURRENCY_ID_1, null), {
preloadedState: mockWalletPreloadedState,
const Portfolio = portfolio()
const currentChainBalance = portfolioBalances({ portfolio: Portfolio })[0]!
const { result } = renderHook(
() => useCrossChainBalances(currentChainBalance.currencyInfo.currencyId, null),
{
preloadedState: mockWalletPreloadedState(),
resolvers: {
Query: {
portfolios: () => [Portfolio],
},
},
})
}
)
await waitFor(() => {
expect(result.current).toEqual(
expect.objectContaining({
currentChainBalance: PortfolioBalancesById[SAMPLE_CURRENCY_ID_1],
currentChainBalance,
})
)
})
......@@ -61,15 +71,9 @@ describe(useCrossChainBalances, () => {
})
describe('otherChainBalances', () => {
// Current chain balance will be determined by the following currency id
const currencyId1 = `${fromGraphQLChain(Chain.Base)}-${USDBC_BASE.address.toLocaleLowerCase()}`
const currencyId2 = `${fromGraphQLChain(
Chain.Arbitrum
)}-${USDC_ARBITRUM.address.toLocaleLowerCase()}`
it('returns null if there are no bridged currencies', async () => {
const { result } = renderHook(() => useCrossChainBalances(SAMPLE_CURRENCY_ID_1, null), {
preloadedState: mockWalletPreloadedState,
preloadedState: mockWalletPreloadedState(),
})
await act(() => undefined)
......@@ -82,25 +86,35 @@ describe(useCrossChainBalances, () => {
})
it('does not include current chain balance in other chain balances', async () => {
const bridgeInfo: { chain: Chain; address?: string }[] = [
{ chain: Chain.Base, address: USDBC_BASE.address.toLocaleLowerCase() },
{ chain: Chain.Arbitrum, address: USDC_ARBITRUM.address.toLocaleLowerCase() },
const tokenBalances = [
tokenBalance({ token: usdcBaseToken() }),
tokenBalance({ token: usdcArbitrumToken() }),
]
const { result } = renderHook(() => useCrossChainBalances(currencyId1, bridgeInfo), {
preloadedState: mockWalletPreloadedState,
const bridgeInfo = tokenBalances.map((balance) => ({
chain: balance.token.chain,
address: balance.token?.address,
}))
const Portfolio = portfolio({ tokenBalances })
const [currentChainBalance, ...otherChainBalances] = portfolioBalances({
portfolio: Portfolio,
})
const { result } = renderHook(
() => useCrossChainBalances(currentChainBalance!.currencyInfo.currencyId, bridgeInfo),
{
preloadedState: mockWalletPreloadedState(),
resolvers: {
Query: {
portfolios: () => [Portfolio2],
portfolios: () => [Portfolio],
},
},
})
}
)
await waitFor(() => {
expect(result.current).toEqual(
expect.objectContaining({
currentChainBalance: PortfolioBalancesById[currencyId1],
otherChainBalances: [PortfolioBalancesById[currencyId2]],
})
expect.objectContaining({ currentChainBalance, otherChainBalances })
)
})
})
......
......@@ -71,8 +71,8 @@ function _TokenFiatOnRampList({
return (
<Flex centered grow>
<BaseCard.ErrorState
retryButtonLabel="Retry"
title={t('Couldn’t load tokens to buy')}
retryButtonLabel={t('common.button.retry')}
title={t('fiatOnRamp.error.load')}
onRetry={onRetry}
/>
</Flex>
......
......@@ -50,7 +50,7 @@ export function ConnectedDappsList({ backButton, sessions }: ConnectedDappsProps
</Flex>
<Flex alignItems="center" flexBasis="70%">
<Text color="$neutral1" numberOfLines={1} variant="body1">
{t('Manage connections')}
{t('walletConnect.dapps.manage.title')}
</Text>
</Flex>
<Flex alignItems="flex-end" flexBasis="15%">
......@@ -101,10 +101,10 @@ export function ConnectedDappsList({ backButton, sessions }: ConnectedDappsProps
paddingTop: fullHeight / 5,
}}>
<Text color="$neutral1" variant="subheading1">
{t('No apps connected')}
{t('walletConnect.dapps.manage.empty.title')}
</Text>
<Text color="$neutral2" textAlign="center" variant="body2">
{t('Connect to an app by scanning a code via WalletConnect')}
{t('walletConnect.dapps.empty.description')}
</Text>
</Flex>
)}
......
import { getSdkError } from '@walletconnect/utils'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Trans, useTranslation } from 'react-i18next'
import 'react-native-reanimated'
import { useAppDispatch } from 'src/app/hooks'
import { DappHeaderIcon } from 'src/components/WalletConnect/DappHeaderIcon'
import { wcWeb3Wallet } from 'src/features/walletConnect/saga'
import { removeSession, WalletConnectSession } from 'src/features/walletConnect/walletConnectSlice'
import { WalletConnectSession, removeSession } from 'src/features/walletConnect/walletConnectSlice'
import { Button, Flex, Text } from 'ui/src'
import { iconSizes } from 'ui/src/theme'
import { logger } from 'utilities/src/logger/logger'
......@@ -66,8 +66,10 @@ export function DappConnectedNetworkModal({
<Flex alignItems="center" gap="$spacing8">
<DappHeaderIcon dapp={dapp} />
<Text textAlign="center" variant="buttonLabel2">
<Text variant="body1">{t('Connected to ')}</Text>
{dapp.name || dapp.url}
<Trans i18nKey="walletConnect.dapps.connection">
<Text variant="body1">Connected to</Text>
{{ dappNameOrUrl: dapp.name || dapp.url }}
</Trans>
</Text>
<Text color="$accent1" numberOfLines={1} textAlign="center" variant="buttonLabel4">
{dapp.url}
......@@ -101,10 +103,10 @@ export function DappConnectedNetworkModal({
</Flex>
<Flex centered row gap="$spacing16">
<Button fill theme="secondary" onPress={onClose}>
{t('Close')}
{t('common.button.close')}
</Button>
<Button fill theme="detrimental" onPress={onDisconnect}>
{t('Disconnect')}
{t('common.button.disconnect')}
</Button>
</Flex>
</Flex>
......
......@@ -63,7 +63,9 @@ export function DappConnectionItem({
}
}
const menuActions = [{ title: t('Disconnect'), systemIcon: 'trash', destructive: true }]
const menuActions = [
{ title: t('common.button.disconnect'), systemIcon: 'trash', destructive: true },
]
const onPress = async (e: NativeSyntheticEvent<ContextMenuOnPressNativeEvent>): Promise<void> => {
if (e.nativeEvent.index === 0) {
......
import { Currency } from '@uniswap/sdk-core'
import React from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { Trans } from 'react-i18next'
import { truncateDappName } from 'src/components/WalletConnect/ScanSheet/util'
import { WalletConnectRequest } from 'src/features/walletConnect/walletConnectSlice'
import { Text } from 'ui/src'
......@@ -16,7 +16,6 @@ export function HeaderText({
permitAmount?: number
permitCurrency?: Currency | null
}): JSX.Element {
const { t } = useTranslation()
const { dapp, type: method } = request
if (permitCurrency) {
......@@ -27,39 +26,54 @@ export function HeaderText({
})?.toExact()
return readablePermitAmount ? (
<Trans t={t}>
<Text textAlign="center" variant="heading3">
Allow {dapp.name} to use up to
<Text fontWeight="bold"> {readablePermitAmount} </Text>
{permitCurrency?.symbol}?
</Text>
<Trans i18nKey="qrScanner.request.withAmount">
Allow {{ dappName: dapp.name }} to use up to
<Text fontWeight="bold"> {{ amount: readablePermitAmount }} </Text>
{{ currencySymbol: permitCurrency?.symbol }}?
</Trans>
</Text>
) : (
<Trans t={t}>
<Text textAlign="center" variant="heading3">
Allow <Text fontWeight="bold">{dapp.name}</Text> to use your {permitCurrency?.symbol}?
</Text>
<Trans i18nKey="qrScanner.request.withoutAmount">
Allow <Text fontWeight="bold">{{ dappName: dapp.name }}</Text> to use your
{{ currencySymbol: permitCurrency?.symbol }}?
</Trans>
</Text>
)
}
const getReadableMethodName = (ethMethod: EthMethod): string => {
const getReadableMethodName = (ethMethod: EthMethod, dappNameOrUrl: string): JSX.Element => {
switch (ethMethod) {
case EthMethod.PersonalSign:
case EthMethod.EthSign:
case EthMethod.SignTypedData:
return t('Signature request from')
return (
<Trans i18nKey="qrScanner.request.method.signature">
Signature request from
<Text fontWeight="bold">{{ dappNameOrUrl }}</Text>
</Trans>
)
case EthMethod.EthSendTransaction:
return t('Transaction request from')
return (
<Trans i18nKey="qrScanner.request.method.transaction">
Transaction request from
<Text fontWeight="bold">{{ dappNameOrUrl }}</Text>
</Trans>
)
}
return t('Request from')
return (
<Trans i18nKey="qrScanner.request.method.default">
Request from
<Text fontWeight="bold">{{ dappNameOrUrl }}</Text>
</Trans>
)
}
return (
<Text textAlign="center" variant="heading3">
<Text>{getReadableMethodName(method)}</Text>
<Text fontWeight="bold"> {truncateDappName(dapp.name || dapp.url)}</Text>
{getReadableMethodName(method, truncateDappName(dapp.name || dapp.url))}
</Text>
)
}
import { BigNumber } from 'ethers'
import { Transaction, TransactionDescription } from 'no-yolo-signatures'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Trans, useTranslation } from 'react-i18next'
import { ScrollView } from 'react-native-gesture-handler'
import { LinkButton } from 'src/components/buttons/LinkButton'
import { SpendingDetails } from 'src/components/WalletConnect/RequestModal/SpendingDetails'
......@@ -162,15 +162,18 @@ function TransactionDetails({
) : null}
{to ? (
<Flex row alignItems="center" gap="$spacing16">
<Trans i18nKey="walletConnect.request.details.recipient">
<Text color="$neutral2" variant="body2">
{t('To')}:
To:
</Text>
<AddressButton address={to} chainId={chainId} />
</Trans>
</Flex>
) : null}
<Flex row alignItems="center" gap="$spacing16">
<Trans i18nKey="walletConnect.request.details.function">
<Text color="$neutral2" variant="body2">
{t('Function')}:
Function:
</Text>
<Flex
backgroundColor={isLoading ? '$transparent' : '$surface3'}
......@@ -178,9 +181,10 @@ function TransactionDetails({
px="$spacing8"
py="$spacing4">
<Text color="$neutral1" loading={isLoading} variant="monospace">
{parsedData ? parsedData.name : t('Unknown')}
{{ functionName: parsedData ? parsedData.name : t('common.text.unknown') }}
</Text>
</Flex>
</Trans>
</Flex>
</Flex>
)
......@@ -216,7 +220,7 @@ function RequestDetailsContent({ request }: Props): JSX.Element {
<Text variant="body2">{message}</Text>
) : (
<Text color="$neutral2" variant="body2">
{t('No message found.')}
{t('qrScanner.request.message.unavailable')}
</Text>
)
}
......
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Trans } from 'react-i18next'
import { Flex, Text } from 'ui/src'
import { iconSizes } from 'ui/src/theme'
import { NumberType } from 'utilities/src/format/types'
......@@ -18,7 +18,6 @@ export function SpendingDetails({
value: string
chainId: ChainId
}): JSX.Element {
const { t } = useTranslation()
const { convertFiatAmountFormatted, formatCurrencyAmount } = useLocalizationContext()
const nativeCurrencyInfo = useNativeCurrencyInfo(chainId)
......@@ -31,21 +30,26 @@ export function SpendingDetails({
: null
const usdValue = useUSDValue(chainId, value)
const tokenAmountWithSymbol =
formatCurrencyAmount({ value: nativeCurrencyAmount, type: NumberType.TokenTx }) +
' ' +
getSymbolDisplayText(nativeCurrencyInfo?.currency.symbol)
const fiatAmount = convertFiatAmountFormatted(usdValue, NumberType.FiatTokenPrice)
return (
<Flex row alignItems="center" gap="$spacing16">
<Trans i18nKey="walletConnect.request.details.sending">
<Text color="$neutral2" variant="body2">
{t('Sending')}:
Sending:
</Text>
<Flex row alignItems="center" gap="$spacing4">
<CurrencyLogo currencyInfo={nativeCurrencyInfo} size={iconSizes.icon16} />
<Text variant="subheading2">
{formatCurrencyAmount({ value: nativeCurrencyAmount, type: NumberType.TokenTx })}{' '}
{getSymbolDisplayText(nativeCurrencyInfo?.currency.symbol)}
</Text>
<Text variant="subheading2">{{ tokenAmountWithSymbol }}</Text>
<Text color="$neutral2" loading={!usdValue} variant="subheading2">
({convertFiatAmountFormatted(usdValue, NumberType.FiatTokenPrice)})
({{ fiatAmount }})
</Text>
</Flex>
</Trans>
</Flex>
)
}
......@@ -314,7 +314,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
) : (
<Flex row alignItems="center" justifyContent="space-between">
<Text color="$neutral1" variant="subheading2">
{t('Network')}
{t('walletConnect.request.label.network')}
</Text>
<NetworkPill
showIcon
......@@ -333,8 +333,8 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
<AccountDetails address={request.account} />
{!hasSufficientFunds && (
<Text color="$DEP_accentWarning" pt="$spacing8" variant="body2">
{t('You don’t have enough {{symbol}} to complete this transaction.', {
symbol: nativeCurrency?.symbol,
{t('walletConnect.request.error.insufficientFunds', {
currencySymbol: nativeCurrency?.symbol,
})}
</Text>
)}
......@@ -351,7 +351,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
/>
}
textColor="$DEP_accentWarning"
title={t('Internet or network connection error')}
title={t('walletConnect.request.error.network')}
/>
) : (
<WarningSection
......@@ -367,7 +367,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
testID={ElementName.Cancel}
theme="tertiary"
onPress={onReject}>
{t('Cancel')}
{t('common.button.cancel')}
</Button>
<Button
fill
......@@ -381,7 +381,9 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
await onConfirm()
}
}}>
{isTransactionRequest(request) ? t('Accept') : t('Sign')}
{isTransactionRequest(request)
? t('common.button.accept')
: t('walletConnect.request.button.sign')}
</Button>
</Flex>
</Flex>
......@@ -419,9 +421,9 @@ function WarningSection({
width={iconSizes.icon16}
/>
<Text color="$neutral2" fontStyle="italic" variant="body3">
{t('Be careful: this {{ requestType }} may transfer assets', {
requestType: isTransactionRequest(request) ? 'transaction' : 'message',
})}
{isTransactionRequest(request)
? t('walletConnect.request.warning.general.transaction')
: t('walletConnect.request.warning.general.message')}
</Text>
</Flex>
)
......
......@@ -68,7 +68,7 @@ const SitePermissions = (): JSX.Element => {
allowFontScaling={false}
color="$neutral2"
variant="subheading2">
{t('App permissions')}
{t('walletConnect.permissions.title')}
</Text>
<Flex centered row gap="$spacing8">
<Icons.Check color="$statusSuccess" size={iconSizes.icon16} />
......@@ -78,7 +78,7 @@ const SitePermissions = (): JSX.Element => {
color="$neutral1"
flexGrow={1}
variant={normalInfoTextSize}>
{t('View your wallet address')}
{t('walletConnect.permissions.option.viewWalletAddress')}
</Text>
</Flex>
<Flex centered row gap="$spacing8">
......@@ -89,7 +89,7 @@ const SitePermissions = (): JSX.Element => {
color="$neutral1"
flexGrow={1}
variant={normalInfoTextSize}>
{t('View your token balances')}
{t('walletConnect.permissions.option.viewTokenBalances')}
</Text>
</Flex>
<Flex centered row gap="$spacing8">
......@@ -100,7 +100,7 @@ const SitePermissions = (): JSX.Element => {
color="$neutral1"
flexGrow={1}
variant={normalInfoTextSize}>
{t('Transfer your assets without consent')}
{t('walletConnect.permissions.option.transferAssets')}
</Text>
</Flex>
</Flex>
......@@ -124,7 +124,7 @@ const NetworksRow = ({ chains }: { chains: ChainId[] }): JSX.Element => {
allowFontScaling={false}
color="$neutral2"
variant="subheading2">
{t('Networks')}
{t('walletConnect.permissions.networks')}
</Text>
<NetworkLogos chains={chains} />
</Flex>
......@@ -258,7 +258,7 @@ export const PendingConnectionModal = ({ pendingSession, onClose }: Props): JSX.
px="$spacing24"
textAlign="center"
variant="heading3">
{t('{{ dappName }} wants to connect to your wallet', {
{t('walletConnect.pending.title', {
dappName: truncateDappName(dappName),
})}{' '}
</Text>
......@@ -287,13 +287,13 @@ export const PendingConnectionModal = ({ pendingSession, onClose }: Props): JSX.
testID="cancel-pending-connection"
theme="secondary"
onPress={(): Promise<void> => onPressSettleConnection(false)}>
{t('Cancel')}
{t('common.button.cancel')}
</Button>
<Button
fill
testID="connect-pending-connection"
onPress={(): Promise<void> => onPressSettleConnection(true)}>
{t('Connect')}
{t('walletConnect.pending.button.connect')}
</Button>
</Flex>
</AnimatedFlex>
......
......@@ -37,7 +37,7 @@ export const PendingConnectionSwitchAccountModal = ({
<ActionSheetModal
header={
<Flex centered gap="$spacing4" py="$spacing16">
<Text variant="buttonLabel2">{t('Switch Account')}</Text>
<Text variant="buttonLabel2">{t('walletConnect.pending.switchAccount')}</Text>
</Flex>
}
isVisible={true}
......
......@@ -63,7 +63,7 @@ export const PendingConnectionSwitchNetworkModal = ({
<ActionSheetModal
header={
<Flex centered gap="$spacing4" py="$spacing16">
<Text variant="buttonLabel2">{t('Switch Network')}</Text>
<Text variant="buttonLabel2">{t('walletConnect.pending.switchNetwork')}</Text>
</Flex>
}
isVisible={true}
......
......@@ -76,14 +76,12 @@ export function WalletConnectModal({
if (!supportedURI) {
setShouldFreezeCamera(true)
Alert.alert(
t('Invalid QR Code'),
t('walletConnect.error.unsupported.title'),
// TODO(EXT-495): Add Scantastic product name here when ready
t(
'Make sure that you’re scanning a valid WalletConnect or Ethereum address QR code before trying again.'
),
t('walletConnect.error.unsupported.message'),
[
{
text: t('Try again'),
text: t('common.button.tryAgain'),
onPress: (): void => {
setShouldFreezeCamera(false)
},
......@@ -103,13 +101,11 @@ export function WalletConnectModal({
if (supportedURI.type === URIType.WalletConnectURL) {
setShouldFreezeCamera(true)
Alert.alert(
t('Invalid QR Code'),
t(
'WalletConnect v1 is no longer supported. The application you’re trying to connect to needs to upgrade to WalletConnect v2.'
),
t('walletConnect.error.unsupportedV1.title'),
t('walletConnect.error.unsupportedV1.message'),
[
{
text: t('OK'),
text: t('common.button.ok'),
onPress: (): void => {
setShouldFreezeCamera(false)
},
......@@ -126,11 +122,11 @@ export function WalletConnectModal({
} catch (error) {
logger.error(error, { tags: { file: 'WalletConnectModal', function: 'onScanCode' } })
Alert.alert(
t('WalletConnect Error'),
t('There was an issue with WalletConnect. Please try again'),
t('walletConnect.error.general.title'),
t('walletConnect.error.general.message'),
[
{
text: t('OK'),
text: t('common.button.ok'),
onPress: (): void => {
setShouldFreezeCamera(false)
},
......@@ -168,14 +164,18 @@ export function WalletConnectModal({
const isAllowed = isAllowedUwULinkRequest(parsedUwulinkRequest)
if (!isAllowed) {
Alert.alert(t('UwU Link error'), t('This QR code is not supported.'), [
Alert.alert(
t('walletConnect.error.uwu.title'),
t('walletConnect.error.uwu.unsupported'),
[
{
text: t('OK'),
text: t('common.button.ok'),
onPress: (): void => {
setShouldFreezeCamera(false)
},
},
])
]
)
return
}
......@@ -201,7 +201,7 @@ export function WalletConnectModal({
onClose()
} catch (_) {
setShouldFreezeCamera(false)
Alert.alert(t('UwU Link error'), t('There was an issue scanning this QR code.'))
Alert.alert(t('walletConnect.error.uwu.title'), t('walletConnect.error.uwu.scan'))
}
}
......@@ -312,8 +312,8 @@ export function WalletConnectModal({
)}
<Text color="$neutral1" variant="buttonLabel2">
{currentScreenState === ScannerModalState.ScanQr
? t('Show my QR code')
: t('Scan a QR code')}
? t('qrScanner.recipient.action.show')
: t('qrScanner.recipient.action.scan')}
</Text>
</Flex>
</TouchableArea>
......
......@@ -104,10 +104,8 @@ function RequestModal({ currRequest }: RequestModalProps): JSX.Element {
if (!isRequestFromSignerAccount) {
return (
<WarningModal
caption={t(
'In order to sign messages or transactions, you’ll need to import the wallet’s recovery phrase.'
)}
closeText={t('Dismiss')}
caption={t('walletConnect.request.warning.message')}
closeText={t('common.button.dismiss')}
icon={
<EyeIcon
color={colors.neutral2.get()}
......@@ -118,7 +116,7 @@ function RequestModal({ currRequest }: RequestModalProps): JSX.Element {
}
modalName={ModalName.WCViewOnlyWarning}
severity={WarningSeverity.None}
title={t('This wallet is in view only mode')}
title={t('walletConnect.request.warning.title')}
onCancel={onClose}
onClose={onClose}>
<Flex
......
......@@ -57,7 +57,7 @@ function PortfolioValue({
return (
<Text color="$neutral2" loading={isLoading} variant="subheading2">
{portfolioValue === undefined
? t('N/A')
? t('common.text.notAvailable')
: convertFiatAmountFormatted(portfolioValue, NumberType.PortfolioBalance)}
</Text>
)
......@@ -100,9 +100,9 @@ export function AccountCardItem({
const menuActions = useMemo(() => {
return [
{ title: t('Copy wallet address'), systemIcon: 'doc.on.doc' },
{ title: t('Wallet settings'), systemIcon: 'gearshape' },
{ title: t('Remove wallet'), systemIcon: 'trash', destructive: true },
{ title: t('account.wallet.action.copy'), systemIcon: 'doc.on.doc' },
{ title: t('account.wallet.action.settings'), systemIcon: 'gearshape' },
{ title: t('account.wallet.button.remove'), systemIcon: 'trash', destructive: true },
]
}, [t])
......
import React from 'react'
import { AccountHeader } from 'src/components/accounts/AccountHeader'
import { render } from 'src/test/test-utils'
import { mockWalletPreloadedState } from 'wallet/src/test/fixtures'
import { ACCOUNT } from 'wallet/src/test/fixtures'
import { mockWalletPreloadedState } from 'wallet/src/test/mocks'
describe(AccountHeader, () => {
it('renders without error', () => {
const tree = render(<AccountHeader />, { preloadedState: mockWalletPreloadedState })
const tree = render(<AccountHeader />, { preloadedState: mockWalletPreloadedState(ACCOUNT) })
expect(tree.toJSON()).toMatchSnapshot()
})
......
......@@ -4,25 +4,23 @@ import { AccountList } from 'src/components/accounts/AccountList'
import { render, screen } from 'src/test/test-utils'
import { NumberType } from 'utilities/src/format/types'
import { Resolvers } from 'wallet/src/data/__generated__/types-and-hooks'
import { ON_PRESS_EVENT_PAYLOAD } from 'wallet/src/test/eventFixtures'
import { account } from 'wallet/src/test/fixtures'
import { Amounts, Portfolios } from 'wallet/src/test/gqlFixtures'
import { mockLocalizedFormatter } from 'wallet/src/test/utils'
import { ACCOUNT, ON_PRESS_EVENT_PAYLOAD, amounts } from 'wallet/src/test/fixtures'
import { mockLocalizedFormatter } from 'wallet/src/test/mocks'
const resolvers: Resolvers = {
Portfolio: {
tokensTotalDenominatedValue: () => Amounts.md,
tokensTotalDenominatedValue: () => amounts.md(),
},
}
describe(AccountList, () => {
it('renders without error', async () => {
const tree = render(<AccountList accounts={[account]} onPress={jest.fn()} />, { resolvers })
const tree = render(<AccountList accounts={[ACCOUNT]} onPress={jest.fn()} />, { resolvers })
expect(
await screen.findByText(
mockLocalizedFormatter.formatNumberOrString({
value: Portfolios[0].tokensTotalDenominatedValue?.value,
value: amounts.md().value,
type: NumberType.PortfolioBalance,
currencyCode: 'usd',
})
......@@ -33,21 +31,21 @@ describe(AccountList, () => {
it('handles press on card items', async () => {
const onPressSpy = jest.fn()
render(<AccountList accounts={[account]} onPress={onPressSpy} />, {
render(<AccountList accounts={[ACCOUNT]} onPress={onPressSpy} />, {
resolvers,
})
// go to success state
expect(
await screen.findByText(
mockLocalizedFormatter.formatNumberOrString({
value: Portfolios[0].tokensTotalDenominatedValue?.value,
value: amounts.md().value,
type: NumberType.PortfolioBalance,
currencyCode: 'usd',
})
)
).toBeDefined()
fireEvent.press(screen.getByTestId(`account_item/${account.address}`), ON_PRESS_EVENT_PAYLOAD)
fireEvent.press(screen.getByTestId(`account_item/${ACCOUNT.address}`), ON_PRESS_EVENT_PAYLOAD)
expect(onPressSpy).toHaveBeenCalledTimes(1)
})
......
......@@ -31,7 +31,7 @@ const ViewOnlyHeader = (): JSX.Element => {
return (
<Flex fill px="$spacing24" py="$spacing8">
<Text color="$neutral2" variant="subheading2">
{t('View only wallets')}
{t('account.wallet.header.viewOnly')}
</Text>
</Flex>
)
......@@ -42,7 +42,7 @@ const SignerHeader = (): JSX.Element => {
return (
<Flex fill px="$spacing24" py="$spacing8">
<Text color="$neutral2" variant="subheading2">
{t('Your other wallets')}
{t('account.wallet.header.other')}
</Text>
</Flex>
)
......
......@@ -40,7 +40,7 @@ export function OfflineBanner(): JSX.Element | null {
width={iconSizes.icon24}
/>
}
text={t('You are in offline mode')}
text={t('home.banner.offline')}
translateY={BANNER_HEIGHT - EXTRA_MARGIN}
/>
) : null
......
import React from 'react'
import { BackButton } from 'src/components/buttons/BackButton'
import { fireEvent, render, screen } from 'src/test/test-utils'
import { ON_PRESS_EVENT_PAYLOAD } from 'wallet/src/test/eventFixtures'
import { ON_PRESS_EVENT_PAYLOAD } from 'wallet/src/test/fixtures'
const mockedGoBack = jest.fn()
jest.mock('@react-navigation/native', () => {
......
......@@ -42,7 +42,7 @@ export function CopyTextButton({ copyText }: Props): JSX.Element {
return (
<Button icon={isCopied ? copiedIcon : copyIcon} theme="tertiary" onPress={onPress}>
{isCopied ? t`Copied` : t`Copy`}
{isCopied ? t('common.button.copied') : t('common.button.copy')}
</Button>
)
}
import React, { ComponentProps, ReactNode, useCallback, useContext, useMemo } from 'react'
import { Trans } from 'react-i18next'
import { Trans, useTranslation } from 'react-i18next'
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import { runOnJS } from 'react-native-reanimated'
import { OnboardingStackBaseParams, useOnboardingStackNavigation } from 'src/app/navigation/types'
......@@ -7,7 +7,7 @@ import { CloseButton } from 'src/components/buttons/CloseButton'
import { CarouselContext } from 'src/components/carousel/Carousel'
import { OnboardingScreens } from 'src/screens/Screens'
import { Flex, Text, useDeviceDimensions } from 'ui/src'
import { isAndroid } from 'wallet/src/utils/platform'
import { getCloudProviderName } from 'wallet/src/utils/platform'
function Page({
text,
......@@ -16,6 +16,7 @@ function Page({
text: ReactNode
params: OnboardingStackBaseParams
}): JSX.Element {
const { t } = useTranslation()
const { fullWidth } = useDeviceDimensions()
const { goToPrev, goToNext } = useContext(CarouselContext)
const navigation = useOnboardingStackNavigation()
......@@ -55,7 +56,7 @@ function Page({
px="$spacing24"
width={fullWidth}>
<Text color="$neutral2" variant="subheading2">
<Trans>What’s a recovery phrase?</Trans>
{t('onboarding.tooltip.recoveryPhrase.trigger')}
</Text>
<GestureDetector gesture={dismissGesture}>
<CloseButton color="$neutral2" onPress={(): void => undefined} />
......@@ -71,13 +72,14 @@ function Page({
)
}
const cloudProviderName = getCloudProviderName()
export const SeedPhraseEducationContent = (params: OnboardingStackBaseParams): JSX.Element[] => [
<Page
params={params}
text={
<CustomHeadingText>
<Trans>
A recovery phrase (or seed phrase) is a{' '}
<Trans i18nKey="account.seedPhrase.education.part1">
A recovery phrase (or seed phrase) is a
<CustomHeadingText color="$accent1">set of words</CustomHeadingText> required to access
your wallet, <CustomHeadingText color="$accent1">like a password.</CustomHeadingText>
</Trans>
......@@ -88,9 +90,9 @@ export const SeedPhraseEducationContent = (params: OnboardingStackBaseParams): J
params={params}
text={
<CustomHeadingText>
<Trans>
<Trans i18nKey="account.seedPhrase.education.part2">
You can <CustomHeadingText color="$accent1">enter</CustomHeadingText> your recovery phrase
on a new device{' '}
on a new device
<CustomHeadingText color="$accent1">to restore your wallet</CustomHeadingText> and its
contents.
</Trans>
......@@ -101,9 +103,9 @@ export const SeedPhraseEducationContent = (params: OnboardingStackBaseParams): J
params={params}
text={
<CustomHeadingText>
<Trans>
But, if you{' '}
<CustomHeadingText color="$accent1">lose your recovery phrase</CustomHeadingText>, you’ll{' '}
<Trans i18nKey="account.seedPhrase.education.part3">
But, if you
<CustomHeadingText color="$accent1">lose your recovery phrase</CustomHeadingText>, you’ll
<CustomHeadingText color="$accent1">lose access</CustomHeadingText> to your wallet.
</Trans>
</CustomHeadingText>
......@@ -113,19 +115,13 @@ export const SeedPhraseEducationContent = (params: OnboardingStackBaseParams): J
params={params}
text={
<CustomHeadingText>
{isAndroid ? (
<Trans>
Instead of memorizing your recovery phrase, you can{' '}
<CustomHeadingText color="$accent1">back it up to Google Drive</CustomHeadingText> and
protect it with a password.
</Trans>
) : (
<Trans>
Instead of memorizing your recovery phrase, you can{' '}
<CustomHeadingText color="$accent1">back it up to iCloud</CustomHeadingText> and protect
it with a password.
<Trans i18nKey="account.seedPhrase.education.part4">
Instead of memorizing your recovery phrase, you can
<CustomHeadingText color="$accent1">
back it up to {{ cloudProviderName }}
</CustomHeadingText>
and protect it with a password.
</Trans>
)}
</CustomHeadingText>
}
/>,
......@@ -133,8 +129,8 @@ export const SeedPhraseEducationContent = (params: OnboardingStackBaseParams): J
params={params}
text={
<CustomHeadingText>
<Trans>
You can also manually back up your recovery phrase by{' '}
<Trans i18nKey="account.seedPhrase.education.part5">
You can also manually back up your recovery phrase by
<CustomHeadingText color="$accent1">writing it down</CustomHeadingText> and storing it in
a safe place.
</Trans>
......@@ -145,8 +141,8 @@ export const SeedPhraseEducationContent = (params: OnboardingStackBaseParams): J
params={params}
text={
<CustomHeadingText>
<Trans>
We recommend using{' '}
<Trans i18nKey="account.seedPhrase.education.part6">
We recommend using
<CustomHeadingText color="$accent1">both types of backups</CustomHeadingText>, because if
you lose your recovery phrase, you won’t be able to restore your wallet.
</Trans>
......
......@@ -142,8 +142,8 @@ export function ExploreSections({ listRef }: ExploreSectionsProps): JSX.Element
return (
<Flex height="100%" pb="$spacing60">
<BaseCard.ErrorState
retryButtonLabel={t('Retry')}
title={t('Couldn’t load tokens')}
retryButtonLabel={t('common.button.retry')}
title={t('explore.tokens.error')}
onRetry={onRetry}
/>
</Flex>
......@@ -188,7 +188,7 @@ export function ExploreSections({ listRef }: ExploreSectionsProps): JSX.Element
mt="$spacing16"
pl="$spacing4">
<Text color="$neutral2" flexShrink={0} paddingEnd="$spacing8" variant="subheading2">
{t('Top tokens')}
{t('explore.tokens.top.title')}
</Text>
<Flex flexShrink={1}>
<SortButton orderBy={orderBy} />
......
......@@ -39,7 +39,7 @@ export function FavoriteHeaderRow({
) : (
<TouchableArea hitSlop={16} onPress={onPress}>
<Text color="$accent1" variant="buttonLabel3">
{t('Done')}
{t('common.button.done')}
</Text>
</TouchableArea>
)}
......
......@@ -72,9 +72,9 @@ export function FavoriteTokensGrid({
return (
<AnimatedFlex entering={FadeIn} style={animatedStyle}>
<FavoriteHeaderRow
editingTitle={t('Edit favorite tokens')}
editingTitle={t('explore.tokens.favorite.title.edit')}
isEditing={isEditing}
title={t('Favorite tokens')}
title={t('explore.tokens.favorite.title.default')}
onPress={(): void => setIsEditing(!isEditing)}
/>
{showLoading ? (
......
......@@ -52,8 +52,8 @@ function FavoriteWalletCard({
/// Options for long press context menu
const menuActions = useMemo(() => {
return [
{ title: t('Remove favorite'), systemIcon: 'heart.fill' },
{ title: t('Edit favorites'), systemIcon: 'square.and.pencil' },
{ title: t('explore.wallets.favorite.action.remove'), systemIcon: 'heart.fill' },
{ title: t('explore.wallets.favorite.action.edit'), systemIcon: 'square.and.pencil' },
]
}, [t])
......
......@@ -71,9 +71,9 @@ export function FavoriteWalletsGrid({
return (
<AnimatedFlex entering={FadeIn} style={animatedStyle}>
<FavoriteHeaderRow
editingTitle={t('Edit favorite wallets')}
editingTitle={t('explore.wallets.favorite.title.edit')}
isEditing={isEditing}
title={t('Favorite wallets')}
title={t('explore.wallets.favorite.title.default')}
onPress={(): void => setIsEditing(!isEditing)}
/>
{showLoading ? (
......
......@@ -74,12 +74,12 @@ export const TokenItem = memo(function _TokenItem({
const getMetadataSubtitle = (): string | undefined => {
switch (metadataDisplayType) {
case TokenMetadataDisplayType.MarketCap:
return t('{{num}} MCap', { num: marketCapFormatted })
return t('explore.tokens.metadata.marketCap', { number: marketCapFormatted })
case TokenMetadataDisplayType.Volume:
return t('{{num}} Vol', { num: volume24hFormatted })
return t('explore.tokens.metadata.volume', { number: volume24hFormatted })
case TokenMetadataDisplayType.TVL:
return t('{{num}} TVL', {
num: totalValueLockedFormatted,
return t('explore.tokens.metadata.totalValueLocked', {
number: totalValueLockedFormatted,
})
case TokenMetadataDisplayType.Symbol:
return symbol
......
......@@ -8,9 +8,9 @@ import { Resolvers } from 'wallet/src/data/__generated__/types-and-hooks'
import { FavoritesState } from 'wallet/src/features/favorites/slice'
import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types'
import { SectionName } from 'wallet/src/telemetry/constants'
import { DaiAsset } from 'wallet/src/test/gqlFixtures'
import { SAMPLE_SEED_ADDRESS_1 } from 'wallet/src/test/fixtures/constants'
const tokenId = DaiAsset.address?.toLowerCase() ?? ''
const tokenId = SAMPLE_SEED_ADDRESS_1
const currencyId = `1-${tokenId}`
const resolvers: Resolvers = {
......
......@@ -107,29 +107,31 @@ export function useExploreTokenContextMenu({
const menuActions = useMemo(
() => [
{
title: isFavorited ? t('Remove favorite') : t('Favorite token'),
title: isFavorited
? t('explore.tokens.favorite.action.remove')
: t('explore.tokens.favorite.action.add'),
systemIcon: isFavorited ? 'heart.fill' : 'heart',
onPress: onPressToggleFavorite,
},
...(onEditFavorites
? [
{
title: t('Edit favorites'),
title: t('explore.tokens.favorite.action.edit'),
systemIcon: 'square.and.pencil',
onPress: onEditFavorites,
},
]
: []),
{ title: t('Swap'), systemIcon: 'arrow.2.squarepath', onPress: onPressSwap },
{ title: t('common.button.swap'), systemIcon: 'arrow.2.squarepath', onPress: onPressSwap },
{
title: t('Receive'),
title: t('common.button.receive'),
systemIcon: 'qrcode',
onPress: onPressReceive,
},
...(!onEditFavorites
? [
{
title: t('Share'),
title: t('common.button.share'),
systemIcon: 'square.and.arrow.up',
onPress: onPressShare,
},
......
......@@ -73,10 +73,13 @@ export function SearchEmptySection(): JSX.Element {
gap="$spacing16"
justifyContent="space-between"
mb="$spacing4">
<SectionHeaderText icon={<RecentIcon />} title={t('Recent searches')} />
<SectionHeaderText
icon={<RecentIcon />}
title={t('explore.search.section.recent')}
/>
<TouchableArea onPress={onPressClearSearchHistory}>
<Text color="$accent1" variant="buttonLabel3">
{t('Clear all')}
{t('explore.search.action.clear')}
</Text>
</TouchableArea>
</Flex>
......@@ -89,16 +92,19 @@ export function SearchEmptySection(): JSX.Element {
</AnimatedFlex>
)}
<Flex gap="$spacing4">
<SectionHeaderText icon={<TrendIcon />} title={t('Popular tokens')} />
<SectionHeaderText icon={<TrendIcon />} title={t('explore.search.section.popularTokens')} />
<SearchPopularTokens />
</Flex>
<Flex gap="$spacing4">
<SectionHeaderText icon={<TrendIcon />} title={t('Popular NFT collections')} />
<SectionHeaderText icon={<TrendIcon />} title={t('explore.search.section.popularNFT')} />
<SearchPopularNFTCollections />
</Flex>
<FlatList
ListHeaderComponent={
<SectionHeaderText icon={<TrendIcon />} title={t('Suggested wallets')} />
<SectionHeaderText
icon={<TrendIcon />}
title={t('explore.search.section.suggestedWallets')}
/>
}
data={SUGGESTED_WALLETS}
keyExtractor={walletKey}
......
......@@ -2,12 +2,12 @@ import React from 'react'
import { SearchPopularTokens } from 'src/components/explore/search/SearchPopularTokens'
import { render, screen } from 'src/test/test-utils'
import { Resolvers } from 'wallet/src/data/__generated__/types-and-hooks'
import { EthToken, TopTokens } from 'wallet/src/test/gqlFixtures'
import { ethToken, usdcToken, wethToken } from 'wallet/src/test/fixtures'
const resolvers: Resolvers = {
Query: {
topTokens: () => TopTokens,
tokens: () => [{ ...EthToken, address: null }],
topTokens: () => [wethToken(), usdcToken()],
tokens: () => [ethToken({ address: null })],
},
}
......
......@@ -9,19 +9,19 @@ export const SearchResultsLoader = (): JSX.Element => {
return (
<Flex gap="$spacing16">
<Flex gap="$spacing12">
<SectionHeaderText title={t('Tokens')} />
<SectionHeaderText title={t('explore.search.section.tokens')} />
<AnimatedFlex entering={FadeIn} exiting={FadeOut} mx="$spacing8">
<Loader.Token repeat={2} />
</AnimatedFlex>
</Flex>
<Flex gap="$spacing12">
<SectionHeaderText title={t('NFT Collections')} />
<SectionHeaderText title={t('explore.search.section.nft')} />
<AnimatedFlex entering={FadeIn} exiting={FadeOut} mx="$spacing8">
<Loader.Token repeat={2} />
</AnimatedFlex>
</Flex>
<Flex gap="$spacing12">
<SectionHeaderText title={t('Wallets')} />
<SectionHeaderText title={t('explore.search.section.wallets')} />
<AnimatedFlex entering={FadeIn} exiting={FadeOut} mx="$spacing8">
<Loader.Token />
</AnimatedFlex>
......
......@@ -34,19 +34,19 @@ import { SearchResultOrHeader } from './types'
const WalletHeaderItem: SearchResultOrHeader = {
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('Wallets'),
title: i18n.t('explore.search.section.wallets'),
}
const TokenHeaderItem: SearchResultOrHeader = {
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('Tokens'),
title: i18n.t('explore.search.section.tokens'),
}
const NFTHeaderItem: SearchResultOrHeader = {
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('NFT Collections'),
title: i18n.t('explore.search.section.nft'),
}
const EtherscanHeaderItem: SearchResultOrHeader = {
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('View on {{ blockExplorerName }}', {
title: i18n.t('explore.search.action.viewEtherscan', {
blockExplorerName: CHAIN_INFO[ChainId.Mainnet].explorer.name,
}),
}
......@@ -170,8 +170,8 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }):
return (
<AnimatedFlex entering={FadeIn} exiting={FadeOut} pt="$spacing24">
<BaseCard.ErrorState
retryButtonLabel="Retry"
title={t('Couldn’t load search results')}
retryButtonLabel="common.button.retry"
title={t('explore.search.error')}
onRetry={onRetry}
/>
</AnimatedFlex>
......@@ -184,8 +184,8 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }):
ListEmptyComponent={
<AnimatedFlex entering={FadeIn} exiting={FadeOut} gap="$spacing8" mx="$spacing8">
<Text color="$neutral2" variant="body1">
<Trans t={t}>
No results found for <Text color="$neutral1">"{searchQuery}"</Text>
<Trans i18nKey="explore.search.empty.full">
No results found for <Text color="$neutral1">"{{ searchQuery }}"</Text>
</Trans>
</Text>
</AnimatedFlex>
......
......@@ -63,8 +63,8 @@ export function SearchENSAddressItem({
{showSecondLine ? (
<Text color="$neutral2" ellipsizeMode="tail" numberOfLines={1} variant="subheading2">
{showOwnedBy &&
t('Owned by {{owner}}', {
owner: primaryENSName || formattedAddress,
t('explore.search.label.ownedBy', {
ownerAddress: primaryENSName || formattedAddress,
})}
{showAddress && formattedAddress}
</Text>
......
......@@ -77,8 +77,8 @@ export function SearchWalletItemBase({
const menuActions = useMemo(() => {
return isFavorited
? [{ title: t('Remove favorite'), systemIcon: 'heart.fill' }]
: [{ title: t('Favorite wallet'), systemIcon: 'heart' }]
? [{ title: t('explore.wallets.favorite.action.remove'), systemIcon: 'heart.fill' }]
: [{ title: t('explore.wallets.favorite.action.add'), systemIcon: 'heart' }]
}, [isFavorited, t])
return (
......
......@@ -7,7 +7,16 @@ import {
import { Chain, ExploreSearchQuery } from 'wallet/src/data/__generated__/types-and-hooks'
import { fromGraphQLChain } from 'wallet/src/features/chains/utils'
import { SearchResultType } from 'wallet/src/features/search/SearchResult'
import { SearchTokens, TopNFTCollections } from 'wallet/src/test/gqlFixtures'
import {
amount,
ethToken,
nftCollection,
nftContract,
token,
tokenMarket,
tokenProject,
} from 'wallet/src/test/fixtures'
import { createArray } from 'wallet/src/test/utils'
type ExploreSearchResult = NonNullable<ExploreSearchQuery>
......@@ -17,29 +26,31 @@ describe(formatTokenSearchResults, () => {
})
it('filters out duplicate results', () => {
const data = [SearchTokens[0], SearchTokens[0]] as ExploreSearchResult['searchTokens']
const searchToken = token()
const data = createArray(2, () => searchToken)
const result = formatTokenSearchResults(data, '')
expect(result).toHaveLength(1)
expect(result?.[0]?.address).toEqual(SearchTokens?.[0]?.address)
expect(result?.[0]?.address).toEqual(data[0].address)
})
it('uses tokens with highest volume for duplicate results', () => {
it('uses tokens with highest volume for tokens with the same project id', () => {
const changedAddress = faker.finance.ethereumAddress()
const data = [
SearchTokens[0],
{
...SearchTokens[0],
// Tokens with the same address and chain will have the same project id
ethToken({
market: tokenMarket({ volume: amount({ value: 10 }) }),
}),
ethToken({
address: changedAddress,
market: {
volume: {
value: 100,
},
},
},
] as ExploreSearchResult['searchTokens']
market: tokenMarket({ volume: amount({ value: 100 }) }),
}),
ethToken({
market: tokenMarket({ volume: amount({ value: 20 }) }),
}),
]
const result = formatTokenSearchResults(data, '')
......@@ -49,22 +60,10 @@ describe(formatTokenSearchResults, () => {
expect(result?.[0]?.address).toEqual(changedAddress)
})
it('sorts results by search query match', () => {
it('sorts results by best search query match', () => {
const data: ExploreSearchResult['searchTokens'] = [
{
project: {
name: 'UniswapStartingName',
id: '2',
},
chain: Chain.Ethereum,
},
{
project: {
name: 'Uniswap',
id: '1',
},
chain: Chain.Ethereum,
},
ethToken({ project: tokenProject({ name: 'UniswapStartingName' }) }),
ethToken({ project: tokenProject({ name: 'Uniswap' }) }),
]
const result = formatTokenSearchResults(data, 'uniswap')
......@@ -75,59 +74,65 @@ describe(formatTokenSearchResults, () => {
})
it('properly formats token search result', () => {
const data = [SearchTokens[0]] as ExploreSearchResult['searchTokens']
const searchToken = token()
const data = [searchToken]
const result = formatTokenSearchResults(data, '')
expect(result).toHaveLength(1)
expect(result?.[0]?.type).toEqual(SearchResultType.Token)
expect(result?.[0]?.chainId).toEqual(fromGraphQLChain(SearchTokens[0]?.chain))
expect(result?.[0]?.address).toEqual(SearchTokens?.[0]?.address)
expect(result?.[0]?.name).toEqual(SearchTokens?.[0]?.project?.name)
expect(result?.[0]?.symbol).toEqual(SearchTokens?.[0]?.symbol)
expect(result?.[0]?.logoUrl).toEqual(SearchTokens?.[0]?.project?.logoUrl)
expect(result?.[0]?.safetyLevel).toEqual(SearchTokens?.[0]?.project?.safetyLevel)
expect(result?.[0]?.chainId).toEqual(fromGraphQLChain(searchToken.chain))
expect(result?.[0]?.address).toEqual(searchToken.address)
expect(result?.[0]?.name).toEqual(searchToken.project?.name)
expect(result?.[0]?.symbol).toEqual(searchToken.symbol)
expect(result?.[0]?.logoUrl).toEqual(searchToken.project?.logoUrl)
expect(result?.[0]?.safetyLevel).toEqual(searchToken.project?.safetyLevel)
})
})
describe(gqlNFTToNFTCollectionSearchResult, () => {
const node = TopNFTCollections[0]
describe(gqlNFTToNFTCollectionSearchResult, () => {
const collection = nftCollection({
nftContracts: [nftContract({ chain: Chain.Ethereum })],
})
it('returns null if required data is missing', () => {
expect(gqlNFTToNFTCollectionSearchResult({ ...node, name: null })).toEqual(null)
expect(gqlNFTToNFTCollectionSearchResult({ ...node, nftContracts: undefined })).toEqual(null)
expect(gqlNFTToNFTCollectionSearchResult({ ...node, nftContracts: [] })).toEqual(null)
expect(gqlNFTToNFTCollectionSearchResult({ ...collection, name: null })).toEqual(null)
expect(gqlNFTToNFTCollectionSearchResult({ ...collection, nftContracts: undefined })).toEqual(
null
)
expect(gqlNFTToNFTCollectionSearchResult({ ...collection, nftContracts: [] })).toEqual(null)
})
it('properly formats NFT collection search result', () => {
const result = gqlNFTToNFTCollectionSearchResult(node)
const result = gqlNFTToNFTCollectionSearchResult(collection)
expect(result?.type).toEqual(SearchResultType.NFTCollection)
expect(result?.chainId).toEqual(fromGraphQLChain(Chain.Ethereum))
expect(result?.address).toEqual(node?.nftContracts?.[0]?.address)
expect(result?.name).toEqual(node?.name)
expect(result?.imageUrl).toEqual(node?.image?.url)
expect(result?.isVerified).toEqual(node?.isVerified)
expect(result?.address).toEqual(collection.nftContracts[0]?.address)
expect(result?.name).toEqual(collection?.name)
expect(result?.imageUrl).toEqual(collection?.image?.url)
expect(result?.isVerified).toEqual(collection?.isVerified)
})
})
})
describe(formatNFTCollectionSearchResults, () => {
describe(formatNFTCollectionSearchResults, () => {
it('returns undefined if there is no data', () => {
expect(formatNFTCollectionSearchResults(null)).toEqual(undefined)
})
it('filters out nfts that cannot be formatted', () => {
const topNFTCollections = createArray(2, nftCollection)
const nftSearchResult = {
edges: [
...TopNFTCollections.map((nft) => ({ node: nft })),
{ node: { ...TopNFTCollections[0], name: null } },
...topNFTCollections.map((nft) => ({ node: nft })),
{ node: nftCollection({ name: null }) },
],
}
const result = formatNFTCollectionSearchResults(nftSearchResult)
expect(result).toHaveLength(2)
expect(result?.[0]?.address).toEqual(TopNFTCollections?.[0]?.nftContracts?.[0]?.address)
expect(result?.[1]?.address).toEqual(TopNFTCollections?.[1]?.nftContracts?.[0]?.address)
expect(result?.[0]?.address).toEqual(topNFTCollections[0].nftContracts[0]?.address)
expect(result?.[1]?.address).toEqual(topNFTCollections[1].nftContracts[0]?.address)
})
})
})
import { SEARCH_RESULT_HEADER_KEY } from 'src/components/explore/search/constants'
import { SearchResultOrHeader } from 'src/components/explore/search/types'
import { Chain, ExploreSearchQuery } from 'wallet/src/data/__generated__/types-and-hooks'
import { fromGraphQLChain } from 'wallet/src/features/chains/utils'
import {
......@@ -6,8 +8,6 @@ import {
TokenSearchResult,
} from 'wallet/src/features/search/SearchResult'
import { searchResultId } from 'wallet/src/features/search/searchHistorySlice'
import { SEARCH_RESULT_HEADER_KEY } from './constants'
import { SearchResultOrHeader } from './types'
const MAX_TOKEN_RESULTS_COUNT = 4
......@@ -109,7 +109,7 @@ export const gqlNFTToNFTCollectionSearchResult = (
): NFTCollectionSearchResult | null => {
const contract = node?.nftContracts?.[0]
// Only show NFT results that have fully populated results
const chainId = fromGraphQLChain(node?.nftContracts?.[0]?.chain ?? Chain.Ethereum)
const chainId = fromGraphQLChain(contract?.chain ?? Chain.Ethereum)
if (node.name && contract?.address && chainId) {
return {
type: SearchResultType.NFTCollection,
......
......@@ -25,7 +25,7 @@ export function FiatOnRampCtaButton({
}: FiatOnRampCtaButtonProps): JSX.Element {
const { t } = useTranslation()
const buttonAvailable = eligible || isLoading
const continueText = eligible ? continueButtonText : t('Not supported in region')
const continueText = eligible ? continueButtonText : t('fiatOnRamp.error.unsupported')
return (
<Trace
logPress
......
......@@ -90,13 +90,15 @@ export function FORQuoteItem({
<Flex alignItems="flex-end" gap="$spacing4">
{quoteAmount && (
<Text color="$neutral1" variant="body3">
{t('Receive {{amount}}', {
amount: `${quoteAmount + getSymbolDisplayText(currency?.symbol)}`,
{t('fiatOnRamp.quote.amount', {
tokenAmount: `${quoteAmount + getSymbolDisplayText(currency?.symbol)}`,
})}
</Text>
)}
<Text color="$neutral2" variant="body3">
{t('{{amount}} after fees', { amount: quoteEquivalentInSourceCurrencyAmount })}
{t('fiatOnRamp.quote.amountAfterFees', {
tokenAmount: quoteEquivalentInSourceCurrencyAmount,
})}
</Text>
</Flex>
{showCarret ? (
......
......@@ -65,22 +65,20 @@ export function ForceUpgradeModal(): JSX.Element {
<>
{isVisible && (
<WarningModal
confirmText={t('Update app')}
confirmText={t('forceUpgrade.action.confirm')}
hideHandlebar={upgradeStatus === UpgradeStatus.Required}
isDismissible={upgradeStatus !== UpgradeStatus.Required}
modalName={ModalName.ForceUpgradeModal}
severity={WarningSeverity.High}
title={t('Update the app to continue')}
title={t('forceUpgrade.title')}
onClose={onClose}
onConfirm={onPressConfirm}>
<Text color="$neutral2" textAlign="center" variant="body2">
{t(
'The version of Uniswap Wallet you’re using is out of date and is missing critical upgrades. If you don’t update the app or you don’t have your recovery phrase written down, you won’t be able to access your assets.'
)}
{t('forceUpgrade.description')}
</Text>
{mnemonicId && (
<Text color="$accent1" variant="buttonLabel3" onPress={onPressViewRecovery}>
{t('View recovery phrase')}
{t('forceUpgrade.action.seedPhrase')}
</Text>
)}
</WarningModal>
......@@ -96,7 +94,7 @@ export function ForceUpgradeModal(): JSX.Element {
<TouchableArea onPress={onDismiss}>
<BackButtonView size={BACK_BUTTON_SIZE} />
</TouchableArea>
<Text variant="subheading1">{t('Recovery phrase')}</Text>
<Text variant="subheading1">{t('forceUpgrade.label.seedPhrase')}</Text>
<Flex width={BACK_BUTTON_SIZE} />
</Flex>
<SeedPhraseDisplay mnemonicId={mnemonicId} onDismiss={onDismiss} />
......
......@@ -76,8 +76,8 @@ export const FeedTab = memo(
const errorCard = (
<Flex grow style={containerProps?.emptyContainerStyle}>
<BaseCard.ErrorState
retryButtonLabel={t('Retry')}
title={t('Couldn’t load activity')}
retryButtonLabel={t('common.button.retry')}
title={t('home.feed.error')}
onRetry={onRetry}
/>
</Flex>
......@@ -86,9 +86,9 @@ export const FeedTab = memo(
const emptyListView = (
<Flex grow style={containerProps?.emptyContainerStyle}>
<BaseCard.EmptyState
description={t('When your favorited wallets makes transactions, they’ll appear here.')}
description={t('home.feed.empty.description')}
icon={<NoTransactions />}
title={t('No activity yet')}
title={t('home.feed.empty.title')}
onPress={onPressReceive}
/>
</Flex>
......
......@@ -71,9 +71,9 @@ export const TokensTab = memo(
// Show different empty state on external profile pages
return isExternalProfile ? (
<BaseCard.EmptyState
description={t('When this wallet buys or receives tokens, they’ll appear here.')}
description={t('home.tokens.empty.description')}
icon={<NoTokens />}
title={t('No tokens yet')}
title={t('home.tokens.empty.title')}
onPress={onPressAction}
/>
) : (
......
......@@ -37,8 +37,8 @@ export function WalletEmptyState(): JSX.Element {
const options: { [key in ActionOption]: ActionCardItem } = useMemo(
() => ({
[ActionOption.Buy]: {
title: t('Buy crypto'),
blurb: t('You’ll need ETH to get started. Buy with a card or bank.'),
title: t('home.tokens.empty.action.buy.title'),
blurb: t('home.tokens.empty.action.buy.description'),
elementName: ElementName.EmptyStateBuy,
icon: (
<IconContainer
......@@ -54,8 +54,8 @@ export function WalletEmptyState(): JSX.Element {
onPress: () => dispatch(openModal({ name: ModalName.FiatOnRamp })),
},
[ActionOption.Receive]: {
title: t('Receive funds'),
blurb: t('Transfer tokens from another wallet or crypto exchange.'),
title: t('home.tokens.empty.action.receive.title'),
blurb: t('home.tokens.empty.action.receive.description'),
elementName: ElementName.EmptyStateReceive,
icon: (
<IconContainer
......@@ -77,8 +77,8 @@ export function WalletEmptyState(): JSX.Element {
),
},
[ActionOption.Import]: {
title: t('Import wallet'),
blurb: t(`Enter this wallet’s recovery phrase to begin swapping and sending.`),
title: t('home.tokens.empty.action.import.title'),
blurb: t('home.tokens.empty.action.import.description'),
elementName: ElementName.EmptyStateImport,
icon: (
<IconContainer
......
......@@ -16,7 +16,7 @@ export function BackButtonView({ size, color, showButtonLabel }: Props): JSX.Ele
<Icons.RotatableChevron color={color ?? '$neutral2'} height={size} width={size} />
{showButtonLabel && (
<Text color="$neutral2" variant="subheading1">
{t('Back')}
{t('common.button.back')}
</Text>
)}
</Flex>
......
......@@ -78,20 +78,22 @@ export function SeedPhraseDisplay({
testID={ElementName.Next}
theme="secondary"
onPress={(): void => setShowSeedPhrase(!showSeedPhrase)}>
{showSeedPhrase ? t('Hide recovery phrase') : t('Show recovery phrase')}
{showSeedPhrase
? t('setting.seedPhrase.action.hide')
: t('setting.seedPhrase.account.show')}
</Button>
</Flex>
{showSeedPhraseViewWarningModal && (
<WarningModal
hideHandlebar
caption={t('Anyone who knows your recovery phrase can access your wallet and funds.')}
closeText={t('Close')}
confirmText={t('View')}
caption={t('setting.seedPhrase.warning.view.message')}
closeText={t('common.button.close')}
confirmText={t('common.button.view')}
isDismissible={false}
modalName={ModalName.ViewSeedPhraseWarning}
severity={WarningSeverity.High}
title={t('View this in a private place')}
title={t('setting.seedPhrase.warning.view.title')}
onCancel={(): void => {
setShowSeedPhraseViewWarningModal(false)
if (!showSeedPhrase) {
......@@ -103,12 +105,10 @@ export function SeedPhraseDisplay({
)}
{showScreenShotWarningModal && (
<WarningModal
caption={t(
'Anyone who gains access to your photos can access your wallet. We recommend that you write down your words instead.'
)}
confirmText={t('Close')}
caption={t('setting.seedPhrase.warning.screenshot.message')}
confirmText={t('common.button.close')}
modalName={ModalName.ScreenshotWarning}
title={t('Screenshots aren’t secure')}
title={t('setting.seedPhrase.warning.screenshot.title')}
onConfirm={(): void => setShowScreenShotWarningModal(false)}
/>
)}
......
......@@ -118,7 +118,7 @@ export function LongMarkdownText(props: LongMarkdownTextProps): JSX.Element {
testID="read-more-button"
variant="buttonLabel3"
onPress={toggleExpanded}>
{expanded ? t('Read less') : t('Read more')}
{expanded ? t('common.longText.button.less') : t('common.longText.button.more')}
</Text>
) : null}
</Flex>
......
......@@ -69,7 +69,7 @@ export function LongText(props: LongTextProps): JSX.Element {
testID="read-more-button"
variant="buttonLabel3"
onPress={(): void => setExpanded(!expanded)}>
{expanded ? t('Read less') : t('Read more')}
{expanded ? t('common.longText.button.less') : t('common.longText.button.more')}
</Text>
) : null}
</Flex>
......
......@@ -49,7 +49,7 @@ export function TooltipInfoButton({
<WarningModal
backgroundIconColor={backgroundIconColor}
caption={modalText}
closeText={closeText ?? t('Close')}
closeText={closeText ?? t('common.button.close')}
icon={modalIcon}
modalName={ModalName.TooltipContent}
title={modalTitle}
......
......@@ -127,7 +127,7 @@ export function ChangeUnitagModal({
dispatch(
pushNotification({
type: AppNotificationType.Success,
title: t('Username changed'),
title: t('unitags.notification.username.title'),
})
)
navigation.goBack()
......@@ -141,7 +141,7 @@ export function ChangeUnitagModal({
dispatch(
pushNotification({
type: AppNotificationType.Error,
errorMessage: t('Could not change username. Try again later.'),
errorMessage: t('unitags.notification.username.error'),
})
)
onClose()
......@@ -209,7 +209,7 @@ export function ChangeUnitagModal({
pt="$spacing12"
px="$spacing24">
<Text textAlign="center" variant="subheading1">
{t('Edit username')}
{t('unitags.editUsername.title')}
</Text>
<Flex
row
......@@ -249,7 +249,7 @@ export function ChangeUnitagModal({
py="$spacing12"
width="100%">
<Text color="$statusCritical" variant="body3">
{t('You’ve reached the maximum number of 2 usernames changes.')}
{t('unitags.editUsername.warning.max')}
</Text>
</Flex>
) : (
......@@ -260,9 +260,7 @@ export function ChangeUnitagModal({
py="$spacing12"
width="100%">
<Text color="$neutral2" variant="body3">
{t(
'Once you change your username, you can never claim it again. You can only change it 2 times.'
)}
{t('unitags.editUsername.warning.default')}
</Text>
</Flex>
)}
......@@ -285,7 +283,7 @@ export function ChangeUnitagModal({
<ActivityIndicator color={colors.sporeWhite.val} />
</Flex>
) : (
t('Save changes')
t('unitags.editUsername.button.confirm')
)}
</Button>
</Flex>
......@@ -316,19 +314,17 @@ function ChangeUnitagConfirmModal({
<Icons.AlertTriangle color="$statusCritical" size="$icon.24" />
</Flex>
<Text textAlign="center" variant="subheading1">
{t('Are you sure?')}
{t('unitags.editUsername.confirm.title')}
</Text>
<Text color="$neutral2" textAlign="center" variant="body2">
{t(
'You’re about to change your username. Once you change it, you can never claim it again.'
)}
{t('unitags.editUsername.confirm.subtitle')}
</Text>
<Flex centered row gap="$spacing12" pt="$spacing24">
<Button fill testID={ElementName.Remove} theme="secondary" onPress={onClose}>
{t('Back')}
{t('common.button.back')}
</Button>
<Button fill testID={ElementName.Remove} theme="detrimental" onPress={onChangeSubmit}>
{t('Confirm')}
{t('common.button.confirm')}
</Button>
</Flex>
</Flex>
......
......@@ -128,9 +128,9 @@ const ChoosePhotoOption = ({ type }: { type: PhotoAction }): JSX.Element => {
color={type === PhotoAction.RemovePhoto ? '$statusCritical' : '$neutral1'}
numberOfLines={1}
variant="buttonLabel2">
{type === PhotoAction.BrowseCameraRoll && t('Choose from camera roll')}
{type === PhotoAction.BrowseNftsList && t('Choose an NFT')}
{type === PhotoAction.RemovePhoto && t('Remove profile picture')}
{type === PhotoAction.BrowseCameraRoll && t('unitags.choosePhoto.option.cameraRoll')}
{type === PhotoAction.BrowseNftsList && t('unitags.choosePhoto.option.nft')}
{type === PhotoAction.RemovePhoto && t('unitags.choosePhoto.option.remove')}
</Text>
</Flex>
</Flex>
......
......@@ -39,7 +39,7 @@ export function DeleteUnitagModal({
dispatch(
pushNotification({
type: AppNotificationType.Error,
errorMessage: t('Could not delete username. Try again later.'),
errorMessage: t('unitags.notification.delete.error'),
})
)
onClose()
......@@ -66,7 +66,7 @@ export function DeleteUnitagModal({
dispatch(
pushNotification({
type: AppNotificationType.Success,
title: t('Username deleted'),
title: t('unitags.notification.delete.title'),
})
)
navigation.goBack()
......@@ -93,12 +93,10 @@ export function DeleteUnitagModal({
<Icons.AlertTriangle color="$statusCritical" size="$icon.24" />
</Flex>
<Text textAlign="center" variant="subheading1">
{t('Are you sure?')}
{t('unitags.delete.confirm.title')}
</Text>
<Text color="$neutral2" textAlign="center" variant="body2">
{t(
'You’re about to delete your username and customizable profile details. You will not be able to reclaim it.'
)}
{t('unitags.delete.confirm.subtitle')}
</Text>
<Flex centered row gap="$spacing12" pt="$spacing24">
<Button
......@@ -112,7 +110,7 @@ export function DeleteUnitagModal({
<ActivityIndicator color={colors.sporeWhite.val} />
</Flex>
) : (
t('Delete')
t('common.button.delete')
)}
</Button>
</Flex>
......
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Trans, useTranslation } from 'react-i18next'
import { Keyboard, StyleProp, ViewStyle } from 'react-native'
import { useAppDispatch } from 'src/app/hooks'
import { navigate } from 'src/app/navigation/rootNavigation'
......@@ -110,24 +110,24 @@ export function UnitagBanner({
justifyContent="space-between"
onPress={onPressClaimNow}>
<Text color="$neutral2" variant="subheading2">
<Trans i18nKey="unitags.banner.title.compact">
<Text color="$accent1" variant="buttonLabel3">
{t('Claim your {{unitagSuffix}} username', {
unitagSuffix: UNITAG_SUFFIX_NO_LEADING_DOT,
})}
Claim your {{ unitagDomain: UNITAG_SUFFIX_NO_LEADING_DOT }} username
</Text>
{t(' and build out your customizable profile.')}
and build out your customizable profile.
</Trans>
</Text>
</Flex>
) : (
<Flex fill gap="$spacing16" justifyContent="space-between">
<Flex gap="$spacing4">
<Text variant="subheading2">
{t('Claim your {{unitagSuffix}} username', {
unitagSuffix: UNITAG_SUFFIX_NO_LEADING_DOT,
{t('unitags.banner.title.full', {
unitagDomain: UNITAG_SUFFIX_NO_LEADING_DOT,
})}
</Text>
<Text color="$neutral2" variant="body3">
{t('Build a personalized web3 profile and easily share your address with friends.')}
{t('unitags.banner.subtitle')}
</Text>
</Flex>
<Flex row gap="$spacing2">
......@@ -140,7 +140,7 @@ export function UnitagBanner({
testID={ElementName.Confirm}
onPress={onPressClaimNow}>
<Text color="white" variant="buttonLabel4">
{t('Claim now')}
{t('unitags.banner.button.claim')}
</Text>
</TouchableArea>
<TouchableArea
......@@ -151,7 +151,7 @@ export function UnitagBanner({
testID={ElementName.Cancel}
onPress={onPressMaybeLater}>
<Text color="$neutral2" variant="buttonLabel4">
{t('Maybe later')}
{t('common.button.later')}
</Text>
</TouchableArea>
</Flex>
......
......@@ -51,11 +51,9 @@ export function UnitagsIntroModal(): JSX.Element {
<BottomSheetModal name={ModalName.UnitagsIntro} onClose={onClose}>
<Flex gap="$spacing24" px="$spacing24" py="$spacing16">
<Flex alignItems="center" gap="$spacing12">
<Text variant="subheading1">{t('Introducing usernames')}</Text>
<Text variant="subheading1">{t('unitags.intro.title')}</Text>
<Text color="$neutral2" textAlign="center" variant="body2">
{t(
'Say goodbye to 0x addresses. Usernames are readable names that make it easier to send and receive crypto.'
)}
{t('unitags.intro.subtitle')}
</Text>
</Flex>
<Flex alignItems="center" maxHeight={105}>
......@@ -66,13 +64,13 @@ export function UnitagsIntroModal(): JSX.Element {
/>
</Flex>
<Flex gap="$spacing16" px="$spacing20">
<BodyItem Icon={Icons.UserSquare} title={t('Customizable profiles')} />
<BodyItem Icon={Icons.Ticket} title={t('Free to claim')} />
<BodyItem Icon={Icons.Lightning} title={t('Powered by ENS subdomains')} />
<BodyItem Icon={Icons.UserSquare} title={t('unitags.intro.features.profile')} />
<BodyItem Icon={Icons.Ticket} title={t('unitags.intro.features.free')} />
<BodyItem Icon={Icons.Lightning} title={t('unitags.intro.features.ens')} />
</Flex>
<Flex gap="$spacing8">
<Button size="medium" theme="primary" onPress={onPressClaimOneNow}>
{t('Continue')}
{t('common.button.continue')}
</Button>
</Flex>
<Flex $short={{ py: '$none', mx: '$spacing12' }} mx="$spacing24">
......
import React, { useRef, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useTranslation } from 'react-i18next'
import { Keyboard, TextInput } from 'react-native'
import { PasswordInput } from 'src/components/input/PasswordInput'
import { PasswordError } from 'src/features/onboarding/PasswordError'
......@@ -90,9 +90,9 @@ export function CloudBackupPasswordForm({
let errorText = ''
if (error === PasswordErrors.WeakPassword) {
errorText = t('Weak password')
errorText = t('settings.setting.backup.password.error.weak')
} else if (error === PasswordErrors.PasswordsDoNotMatch) {
errorText = t('Passwords do not match')
errorText = t('settings.setting.backup.password.error.mismatch')
} else if (error) {
// use the upstream zxcvbn error message
errorText = error
......@@ -104,7 +104,11 @@ export function CloudBackupPasswordForm({
<Flex gap="$spacing8">
<PasswordInput
ref={passwordInputRef}
placeholder={isConfirmation ? t('Confirm password') : t('Create password')}
placeholder={
isConfirmation
? t('settings.setting.backup.password.placeholder.confirm')
: t('settings.setting.backup.password.placeholder.create')
}
returnKeyType="next"
value={password}
onChangeText={(newText: string): void => {
......@@ -120,29 +124,42 @@ export function CloudBackupPasswordForm({
<Flex centered row gap="$spacing12" px="$spacing16">
<Icons.DiamondExclamation color="$neutral2" size={iconSizes.icon20} />
<Text color="$neutral2" variant="body3">
{t(
'Uniswap Labs does not store your password and can’t recover it, so it’s crucial you remember it.'
)}
{t('settings.setting.backup.password.disclaimer')}
</Text>
</Flex>
)}
</Flex>
<Button disabled={isButtonDisabled} testID={ElementName.Next} onPress={onPressNext}>
{t('Continue')}
{t('common.button.continue')}
</Button>
</>
)
}
function PasswordStrengthText({ strength }: { strength: PasswordStrength }): JSX.Element {
const { text, color } = getPasswordStrengthTextAndColor(strength)
const { t } = useTranslation()
const { color } = getPasswordStrengthTextAndColor(strength)
const hasPassword = strength !== PasswordStrength.NONE
let strengthText: string = ''
switch (strength) {
case PasswordStrength.STRONG:
strengthText = t('settings.setting.backup.password.strong')
break
case PasswordStrength.MEDIUM:
strengthText = t('settings.setting.backup.password.medium')
break
case PasswordStrength.WEAK:
strengthText = t('settings.setting.backup.password.weak')
break
default:
break
}
return (
<Flex centered row opacity={hasPassword ? 1 : 0} pt="$spacing12" px="$spacing8">
<Text color={color} variant="body3">
<Trans>This is a {text.toLowerCase()} password</Trans>
{strengthText}
</Text>
</Flex>
)
......
......@@ -19,7 +19,7 @@ import {
} from 'wallet/src/features/wallet/accounts/editAccountSaga'
import { AccountType, BackupType } from 'wallet/src/features/wallet/accounts/types'
import { useAccount } from 'wallet/src/features/wallet/hooks'
import { isAndroid } from 'wallet/src/utils/platform'
import { getCloudProviderName } from 'wallet/src/utils/platform'
type Props = {
accountAddress: Address
......@@ -80,17 +80,13 @@ export function CloudBackupProcessingAnimation({
})
Alert.alert(
isAndroid ? t('Google Drive error') : t('iCloud error'),
isAndroid
? t(
'Unable to backup recovery phrase to Google Drive. Please ensure you have Google Drive enabled with available storage space and try again.'
)
: t(
'Unable to backup recovery phrase to iCloud. Please ensure you have iCloud enabled with available storage space and try again.'
),
t('settings.setting.backup.error.title', { cloudProviderName: getCloudProviderName() }),
t('settings.setting.backup.error.message.full', {
cloudProviderName: getCloudProviderName(),
}),
[
{
text: t('OK'),
text: t('common.button.ok'),
style: 'default',
onPress: onErrorPress,
},
......@@ -118,7 +114,9 @@ export function CloudBackupProcessingAnimation({
<ActivityIndicator size="large" />
</Flex>
<Text variant="heading3">
{isAndroid ? t('Backing up to Google Drive...') : t('Backing up to iCloud...')}
{t('settings.setting.backup.status.inProgress', {
cloudProviderName: getCloudProviderName(),
})}
</Text>
</Flex>
) : (
......@@ -131,7 +129,9 @@ export function CloudBackupProcessingAnimation({
size={iconSize}
/>
<Text variant="heading3">
{isAndroid ? t('Backed up to Google Drive') : t('Backed up to iCloud')}
{t('settings.setting.backup.status.complete', {
cloudProviderName: getCloudProviderName(),
})}
</Text>
</Flex>
)
......
......@@ -7,12 +7,15 @@ import {
TransactionType,
} from 'wallet/src/features/transactions/types'
import { RootState } from 'wallet/src/state'
import { account, mockWalletPreloadedState } from 'wallet/src/test/fixtures'
import { signerMnemonicAccount } from 'wallet/src/test/fixtures'
import { mockWalletPreloadedState } from 'wallet/src/test/mocks'
const account = signerMnemonicAccount()
const MOCK_DATE_PROMPTED = Date.now()
const state = {
...mockWalletPreloadedState,
...mockWalletPreloadedState(),
wallet: {
appRatingProvidedMs: MOCK_DATE_PROMPTED,
},
......
......@@ -136,34 +136,34 @@ export function useTokenContextMenu({
const menuActions = useMemo(
() => [
{
title: t('Buy'),
title: t('common.button.buy'),
systemIcon: 'arrow.down',
onPress: () => onPressSwap(CurrencyField.OUTPUT),
},
{
title: t('Sell'),
title: t('common.button.sell'),
systemIcon: 'arrow.up',
onPress: () => onPressSwap(CurrencyField.INPUT),
},
{
title: t('Send'),
title: t('common.button.send'),
systemIcon: 'paperplane',
onPress: onPressSend,
},
{
title: t('Receive'),
title: t('common.button.receive'),
systemIcon: 'qrcode',
onPress: onPressReceive,
},
{
title: t('Share'),
title: t('common.button.share'),
systemIcon: 'square.and.arrow.up',
onPress: onPressShare,
},
...(activeAccountHoldsToken
? [
{
title: isHidden ? t('Unhide Token') : t('Hide Token'),
title: isHidden ? t('tokens.action.unhide') : t('tokens.action.hide'),
systemIcon: isHidden ? 'eye' : 'eye.slash',
destructive: !isHidden,
onPress: onPressHiddenStatus,
......
......@@ -47,8 +47,8 @@ export async function tryLocalAuthenticate(): Promise<BiometricAuthenticationSta
*/
const enrolled = await isEnrolledAsync()
const result = await authenticateAsync({
cancelLabel: i18n.t('Cancel'),
promptMessage: i18n.t('Please authenticate'),
cancelLabel: i18n.t('common.button.cancel'),
promptMessage: i18n.t('settings.setting.biometrics.auth'),
requireConfirmation: false,
})
......
......@@ -2,9 +2,9 @@
import { Contract, ContractInterface } from 'ethers'
import { useMemo } from 'react'
import ERC20_ABI from 'uniswap/src/abis/erc20.json'
import { Erc20 } from 'uniswap/src/abis/types'
import { logger } from 'utilities/src/logger/logger'
import ERC20_ABI from 'wallet/src/abis/erc20.json'
import { Erc20 } from 'wallet/src/abis/types'
import { ChainId } from 'wallet/src/constants/chains'
import { useContractManager, useProvider } from 'wallet/src/features/wallet/context'
......
import { act, renderHook, waitFor } from 'src/test/test-utils'
import {
SAMPLE_CURRENCY_ID_1,
SAMPLE_CURRENCY_ID_2,
mockWalletPreloadedState,
} from 'wallet/src/test/fixtures'
import { Portfolio, PortfolioBalancesById } from 'wallet/src/test/gqlFixtures'
import { SAMPLE_CURRENCY_ID_1, portfolio, portfolioBalances } from 'wallet/src/test/fixtures'
import { mockWalletPreloadedState } from 'wallet/src/test/mocks'
import { useBalances } from './balances'
const preloadedState = mockWalletPreloadedState()
describe(useBalances, () => {
it('returns null if no currency was specified', async () => {
const { result } = renderHook(() => useBalances(undefined), {
preloadedState: mockWalletPreloadedState,
preloadedState,
})
await act(() => undefined)
......@@ -20,7 +18,7 @@ describe(useBalances, () => {
it('returns empty array if no balances are available', async () => {
const { result } = renderHook(() => useBalances([SAMPLE_CURRENCY_ID_1]), {
preloadedState: mockWalletPreloadedState,
preloadedState,
})
expect(result.current).toEqual(null) // null while data is loading
......@@ -31,18 +29,23 @@ describe(useBalances, () => {
})
it('returns balances for specified currencies if they exist in the portfolio', async () => {
const { result } = renderHook(() => useBalances([SAMPLE_CURRENCY_ID_1, SAMPLE_CURRENCY_ID_2]), {
preloadedState: mockWalletPreloadedState,
const Portfolio = portfolio()
const balances = portfolioBalances({ portfolio: Portfolio })
const { result } = renderHook(
() => useBalances(balances.map(({ currencyInfo: { currencyId } }) => currencyId)),
{
preloadedState,
resolvers: {
Query: {
portfolios: () => [Portfolio],
},
},
})
}
)
await waitFor(() => {
// The response contains only the first currency as the second one is not in the portfolio
expect(result.current).toEqual([PortfolioBalancesById[SAMPLE_CURRENCY_ID_1]])
expect(result.current).toEqual(balances)
})
})
})
......@@ -19,13 +19,15 @@ import { UNISWAP_APP_HOSTNAME } from 'wallet/src/constants/urls'
import { setAccountAsActive } from 'wallet/src/features/wallet/slice'
import { ModalName } from 'wallet/src/telemetry/constants'
import {
account,
SAMPLE_CURRENCY_ID_1,
SAMPLE_CURRENCY_ID_2,
SAMPLE_SEED_ADDRESS_1,
SAMPLE_SEED_ADDRESS_2,
signerMnemonicAccount,
} from 'wallet/src/test/fixtures'
const account = signerMnemonicAccount()
const swapUrl = `https://uniswap.org/app?screen=swap&userAddress=${account.address}&inputCurrencyId=${SAMPLE_CURRENCY_ID_1}&outputCurrencyId=${SAMPLE_CURRENCY_ID_2}&currencyField=INPUT`
const transactionUrl = `https://uniswap.org/app?screen=transaction&userAddress=${account.address}`
const swapDeepLinkPayload = { url: swapUrl, coldStart: false }
......
......@@ -318,11 +318,9 @@ export function* handleWalletConnectDeepLink(wcUri: string) {
if (wcUriVersion === 1) {
Alert.alert(
i18n.t('Invalid QR Code'),
i18n.t(
'WalletConnect v1 is no longer supported. The application you’re trying to connect to needs to upgrade to WalletConnect v2.'
),
[{ text: i18n.t('OK') }]
i18n.t('walletConnect.error.unsupportedV1.title'),
i18n.t('walletConnect.error.unsupportedV1.message'),
[{ text: i18n.t('common.button.ok') }]
)
return
}
......@@ -335,8 +333,8 @@ export function* handleWalletConnectDeepLink(wcUri: string) {
tags: { file: 'handleDeepLinkSaga', function: 'handleWalletConnectDeepLink' },
})
Alert.alert(
i18n.t('WalletConnect Error'),
i18n.t('There was an issue with WalletConnect. Please try again')
i18n.t('walletConnect.error.general.title'),
i18n.t('walletConnect.error.general.message')
)
}
}
......
......@@ -10,7 +10,9 @@ import {
TransactionState,
} from 'wallet/src/features/transactions/transactionState/types'
import { ModalName } from 'wallet/src/telemetry/constants'
import { account } from 'wallet/src/test/fixtures'
import { signerMnemonicAccount } from 'wallet/src/test/fixtures'
const account = signerMnemonicAccount()
const formSwapUrl = (
userAddress?: Address,
......
......@@ -96,15 +96,15 @@ export function getTokenMetadataDisplayType(orderBy: TokensOrderBy): TokenMetada
export function getTokensOrderByMenuLabel(orderBy: TokensOrderBy, t: AppTFunction): string {
switch (orderBy) {
case TokenSortableField.MarketCap:
return t('Market cap')
return t('explore.tokens.sort.option.marketCap')
case TokenSortableField.Volume:
return t('Uniswap volume (24H)')
return t('explore.tokens.sort.option.volume')
case TokenSortableField.TotalValueLocked:
return t('Uniswap TVL')
return t('explore.tokens.sort.option.totalValueLocked')
case ClientTokensOrderBy.PriceChangePercentage24hDesc:
return t('Price increase (24H)')
return t('explore.tokens.sort.option.priceIncrease')
case ClientTokensOrderBy.PriceChangePercentage24hAsc:
return t('Price decrease (24H)')
return t('explore.tokens.sort.option.priceDecrease')
default:
throw new Error('Unexpected order by value ' + orderBy)
}
......@@ -114,15 +114,15 @@ export function getTokensOrderByMenuLabel(orderBy: TokensOrderBy, t: AppTFunctio
export function getTokensOrderBySelectedLabel(orderBy: TokensOrderBy, t: AppTFunction): string {
switch (orderBy) {
case TokenSortableField.MarketCap:
return t('Market cap')
return t('explore.tokens.sort.label.marketCap')
case TokenSortableField.Volume:
return t('Volume')
return t('explore.tokens.sort.label.volume')
case TokenSortableField.TotalValueLocked:
return t('TVL')
return t('explore.tokens.sort.label.totalValueLocked')
case ClientTokensOrderBy.PriceChangePercentage24hDesc:
return t('Price increase')
return t('explore.tokens.sort.label.priceIncrease')
case ClientTokensOrderBy.PriceChangePercentage24hAsc:
return t('Price decrease')
return t('explore.tokens.sort.label.priceDecrease')
default:
throw new Error('Unexpected order by value in option text ' + orderBy)
}
......
......@@ -70,26 +70,26 @@ export function ProfileContextMenu({ address }: { address: Address }): JSX.Eleme
const menuActions = useMemo(() => {
const options = [
{
title: t('View on {{ blockExplorerName }}', {
title: t('account.wallet.action.viewExplorer', {
blockExplorerName: CHAIN_INFO[ChainId.Mainnet].explorer.name,
}),
action: openExplorerLink,
systemIcon: 'link',
},
{
title: t('Copy address'),
title: t('account.wallet.action.copy'),
action: onPressCopyAddress,
systemIcon: 'square.on.square',
},
{
title: t('Share'),
title: t('common.button.share'),
action: onPressShare,
systemIcon: 'square.and.arrow.up',
},
]
if (unitag) {
options.push({
title: t('Report profile'),
title: t('account.wallet.action.report'),
action: onReportProfile,
systemIcon: 'flag',
})
......
......@@ -283,7 +283,7 @@ export const ProfileHeader = memo(function ProfileHeader({
color="$neutral2"
maxFontSizeMultiplier={1.2}
variant="buttonLabel2">
{t('Send')}
{t('common.button.send')}
</Text>
</Flex>
</TouchableArea>
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment