diff --git a/RELEASE b/RELEASE index fc4ee4e41ff..49513f2661b 100644 --- a/RELEASE +++ b/RELEASE @@ -1,6 +1,6 @@ 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 diff --git a/VERSION b/VERSION index 611583bb308..0fdbe7d426e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -web/5.16.2 \ No newline at end of file +web/5.17.0 \ No newline at end of file diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle index 38a752f29a8..e6cb5e7688a 100644 --- a/apps/mobile/android/app/build.gradle +++ b/apps/mobile/android/app/build.gradle @@ -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" } } diff --git a/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj b/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj index f4ef4c25bcd..4d4a78b6f65 100644 --- a/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj +++ b/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj @@ -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; diff --git a/apps/mobile/jest-setup.js b/apps/mobile/jest-setup.js index c3a7d2a0980..798a90928b9 100644 --- a/apps/mobile/jest-setup.js +++ b/apps/mobile/jest-setup.js @@ -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', () => { diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 5ce011d5c42..6d84ac2c9a3 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -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:^" }, diff --git a/apps/mobile/src/app/ErrorBoundary.tsx b/apps/mobile/src/app/ErrorBoundary.tsx index 6b291416284..8a2dd913737 100644 --- a/apps/mobile/src/app/ErrorBoundary.tsx +++ b/apps/mobile/src/app/ErrorBoundary.tsx @@ -72,8 +72,8 @@ function ErrorScreen({ error }: { error: Error }): JSX.Element { - {t('Uh oh!')} - {t('Something crashed.')} + {t('errors.crash.title')} + {t('errors.crash.message')} {error.message && __DEV__ && {error.message}} @@ -82,7 +82,7 @@ function ErrorScreen({ error }: { error: Error }): JSX.Element { onPress={(): void => { RNRestart.Restart() }}> - {t('Restart app')} + {t('errors.crash.restart')} diff --git a/apps/mobile/src/app/MobileWalletNavigationProvider.tsx b/apps/mobile/src/app/MobileWalletNavigationProvider.tsx index a1119bce9d3..975e793bfdb 100644 --- a/apps/mobile/src/app/MobileWalletNavigationProvider.tsx +++ b/apps/mobile/src/app/MobileWalletNavigationProvider.tsx @@ -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() diff --git a/apps/mobile/src/app/migrations.test.ts b/apps/mobile/src/app/migrations.test.ts index 8429ae76cec..a882a3b686e 100644 --- a/apps/mobile/src/app/migrations.test.ts +++ b/apps/mobile/src/app/migrations.test.ts @@ -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 diff --git a/apps/mobile/src/app/modals/AccountSwitcherModal.test.tsx b/apps/mobile/src/app/modals/AccountSwitcherModal.test.tsx index 163656e96a9..8013c48d740 100644 --- a/apps/mobile/src/app/modals/AccountSwitcherModal.test.tsx +++ b/apps/mobile/src/app/modals/AccountSwitcherModal.test.tsx @@ -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 }, diff --git a/apps/mobile/src/app/modals/AccountSwitcherModal.tsx b/apps/mobile/src/app/modals/AccountSwitcherModal.tsx index c90f27dc011..c82b45fdfd9 100644 --- a/apps/mobile/src/app/modals/AccountSwitcherModal.tsx +++ b/apps/mobile/src/app/modals/AccountSwitcherModal.tsx @@ -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"> - {t('Create a new wallet')} + {t('account.wallet.button.create')} ), }, @@ -233,7 +235,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme onPress: onPressAddViewOnlyWallet, render: () => ( - {t('Add a view-only wallet')} + {t('account.wallet.button.addViewOnly')} ), }, @@ -242,7 +244,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme onPress: onPressImportWallet, render: () => ( - {t('Import a new wallet')} + {t('account.wallet.button.import')} ), }, @@ -255,7 +257,9 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme render: () => ( - {isAndroid ? t('Restore from Google Drive') : t('Restore from iCloud')} + {isAndroid + ? t('account.cloud.button.restore.android') + : t('account.cloud.button.restore.ios')} ), @@ -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')} @@ -310,7 +314,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme - {t('Add wallet')} + {t('account.wallet.button.add')} diff --git a/apps/mobile/src/app/modals/ViewOnlyExplainerModal.tsx b/apps/mobile/src/app/modals/ViewOnlyExplainerModal.tsx index a5644c679bc..d648aed7899 100644 --- a/apps/mobile/src/app/modals/ViewOnlyExplainerModal.tsx +++ b/apps/mobile/src/app/modals/ViewOnlyExplainerModal.tsx @@ -46,11 +46,9 @@ export function ViewOnlyExplainerModal(): JSX.Element { - {t('This wallet is view-only')} + {t('account.wallet.viewOnly.title')} - {t( - 'To swap, buy, send, and receive tokens, you need to import this wallet’s recovery phrase.' - )} + {t('account.wallet.viewOnly.description')} @@ -61,7 +59,7 @@ export function ViewOnlyExplainerModal(): JSX.Element { px={40} theme="primary" onPress={onPressImportWallet}> - {t('Import wallet')} + {t('account.wallet.viewOnly.button')} diff --git a/apps/mobile/src/app/modals/utils.test.tsx b/apps/mobile/src/app/modals/utils.test.tsx index 2ab8728c5d1..aafbcb2cae7 100644 --- a/apps/mobile/src/app/modals/utils.test.tsx +++ b/apps/mobile/src/app/modals/utils.test.tsx @@ -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, diff --git a/apps/mobile/src/app/navigation/NavBar.tsx b/apps/mobile/src/app/navigation/NavBar.tsx index 5c0b38177f6..11a7ab19e9e 100644 --- a/apps/mobile/src/app/navigation/NavBar.tsx +++ b/apps/mobile/src/app/navigation/NavBar.tsx @@ -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')} @@ -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')} diff --git a/apps/mobile/src/components/PriceExplorer/PriceExplorerError.tsx b/apps/mobile/src/components/PriceExplorer/PriceExplorerError.tsx index a819d7472d9..a7240680e15 100644 --- a/apps/mobile/src/components/PriceExplorer/PriceExplorerError.tsx +++ b/apps/mobile/src/components/PriceExplorer/PriceExplorerError.tsx @@ -29,9 +29,9 @@ export function PriceExplorerError({ justifyContent="center" overflow="hidden"> diff --git a/apps/mobile/src/components/PriceExplorer/Text.test.tsx b/apps/mobile/src/components/PriceExplorer/Text.test.tsx index 6cf7893d236..0b85c64195d 100644 --- a/apps/mobile/src/components/PriceExplorer/Text.test.tsx +++ b/apps/mobile/src/components/PriceExplorer/Text.test.tsx @@ -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() @@ -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() @@ -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() @@ -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`) }) }) diff --git a/apps/mobile/src/components/PriceExplorer/constants.ts b/apps/mobile/src/components/PriceExplorer/constants.ts index a323b61f3cc..4f2bc269e27 100644 --- a/apps/mobile/src/components/PriceExplorer/constants.ts +++ b/apps/mobile/src/components/PriceExplorer/constants.ts @@ -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 diff --git a/apps/mobile/src/components/PriceExplorer/usePriceHistory.test.ts b/apps/mobile/src/components/PriceExplorer/usePriceHistory.test.ts index e42dad6017f..4bfd2399b63 100644 --- a/apps/mobile/src/components/PriceExplorer/usePriceHistory.test.ts +++ b/apps/mobile/src/components/PriceExplorer/usePriceHistory.test.ts @@ -4,44 +4,24 @@ import { useTokenPriceHistory } from 'src/components/PriceExplorer/usePriceHisto import { renderHookWithProviders } from 'src/test/render' import { HistoryDuration, + Resolvers, TimestampedAmount, - TokenMarket as TokenMarketType, TokenProject as TokenProjectType, } from 'wallet/src/data/__generated__/types-and-hooks' -import { SAMPLE_CURRENCY_ID_1, faker } from 'wallet/src/test/fixtures' import { - EthToken, - TokenDayPriceHistory, - TokenMarket, - TokenProjectDay, - TokenProjectWeek, - TokenProjectYear, - TokenProjects, - TokenWeekPriceHistory, - TokenYearPriceHistory, -} from 'wallet/src/test/gqlFixtures' - -const mockHistoryPrice = (price: number): TimestampedAmount => ({ - id: faker.datatype.uuid(), - timestamp: faker.date.past(/*year=*/ 2).getMilliseconds(), - value: price, -}) - -const mockTokenProject = (priceHistory: TokenMarketType['priceHistory']): TokenProjectType => ({ - ...TokenProjectDay, - markets: [ - { - ...TokenProjectDay.markets![0]!, - priceHistory, - }, - ], -}) + SAMPLE_CURRENCY_ID_1, + priceHistory, + timestampedAmount, + token, + tokenMarket, + usdcTokenProject, +} from 'wallet/src/test/fixtures' const mockTokenProjectsQuery = (historyPrices: number[]) => (): TokenProjectType[] => - [mockTokenProject(historyPrices.map(mockHistoryPrice))] + [usdcTokenProject({ priceHistory: historyPrices.map((value) => timestampedAmount({ value })) })] -const formatPriceHistory = (priceHistory: TimestampedAmount[]): Omit[] => - priceHistory.map(({ timestamp, value }) => ({ value, timestamp: timestamp * 1000 })) +const formatPriceHistory = (history: TimestampedAmount[]): Omit[] => + history.map(({ timestamp, value }) => ({ value, timestamp: timestamp * 1000 })) describe(useTokenPriceHistory, () => { it('returns correct initial values', async () => { @@ -66,15 +46,11 @@ describe(useTokenPriceHistory, () => { }) it('returns on-chain spot price if off-chain spot price is not available', async () => { + const market = tokenMarket() const { result } = renderHookWithProviders(() => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), { resolvers: { Query: { - tokenProjects: () => - TokenProjects.map((project) => ({ - ...project, - markets: null, - tokens: [{ ...EthToken, market: TokenMarket }], - })), + tokenProjects: () => [usdcTokenProject({ markets: null, tokens: [token({ market })] })], }, }, }) @@ -85,8 +61,8 @@ describe(useTokenPriceHistory, () => { }) expect(result.current.data?.spot).toEqual({ - value: { value: TokenMarket.price?.value }, - relativeChange: { value: TokenMarket.pricePercentChange?.value }, + value: { value: market.price?.value }, + relativeChange: { value: market.pricePercentChange?.value }, }) }) @@ -154,14 +130,21 @@ describe(useTokenPriceHistory, () => { describe('correct price history', () => { it('properly formats price history entries', async () => { - const { result } = renderHookWithProviders(() => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1)) + const history = priceHistory() + const { result } = renderHookWithProviders(() => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), { + resolvers: { + Query: { + tokenProjects: () => [usdcTokenProject({ priceHistory: history })], + }, + }, + }) await waitFor(() => { expect(result.current.loading).toBe(false) expect(result.current.error).toBe(false) }) - expect(result.current.data?.priceHistory).toEqual(formatPriceHistory(TokenDayPriceHistory)) + expect(result.current.data?.priceHistory).toEqual(formatPriceHistory(history)) }) it('filters out invalid price history entries', async () => { @@ -169,7 +152,14 @@ describe(useTokenPriceHistory, () => { resolvers: { Query: { tokenProjects: () => [ - mockTokenProject([null, mockHistoryPrice(1), null, mockHistoryPrice(2)]), + usdcTokenProject({ + priceHistory: [ + null, + timestampedAmount({ value: 1 }), + null, + timestampedAmount({ value: 2 }), + ], + }), ], }, }, @@ -193,149 +183,198 @@ describe(useTokenPriceHistory, () => { }) }) - describe('when duration is set to default value (day)', () => { - it('returns correct price history', async () => { - const { result } = renderHookWithProviders(() => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1)) + describe('different durations', () => { + const dayPriceHistory = priceHistory({ duration: HistoryDuration.Day }) + const weekPriceHistory = priceHistory({ duration: HistoryDuration.Week }) + const monthPriceHistory = priceHistory({ duration: HistoryDuration.Month }) + const yearPriceHistory = priceHistory({ duration: HistoryDuration.Year }) + + const dayTokenProject = usdcTokenProject({ priceHistory: dayPriceHistory }) + const weekTokenProject = usdcTokenProject({ priceHistory: weekPriceHistory }) + const monthTokenProject = usdcTokenProject({ priceHistory: monthPriceHistory }) + const yearTokenProject = usdcTokenProject({ priceHistory: yearPriceHistory }) + + const resolvers: Resolvers = { + Query: { + tokenProjects: (parent, args, context, info) => { + switch (info.variableValues.duration) { + case HistoryDuration.Day: + return [dayTokenProject] + case HistoryDuration.Week: + return [weekTokenProject] + case HistoryDuration.Month: + return [monthTokenProject] + case HistoryDuration.Year: + return [yearTokenProject] + default: + return [dayTokenProject] + } + }, + }, + } - await waitFor(() => { - expect(result.current).toEqual( - expect.objectContaining({ - data: { - priceHistory: formatPriceHistory(TokenDayPriceHistory), - spot: expect.anything(), - }, - selectedDuration: HistoryDuration.Day, - }) + describe('when duration is set to default value (day)', () => { + it('returns correct price history', async () => { + const { result } = renderHookWithProviders( + () => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), + { resolvers } ) + + await waitFor(() => { + expect(result.current).toEqual( + expect.objectContaining({ + data: { + priceHistory: formatPriceHistory(dayPriceHistory), + spot: expect.anything(), + }, + selectedDuration: HistoryDuration.Day, + }) + ) + }) }) - }) - it('returns correct spot price', async () => { - const { result } = renderHookWithProviders(() => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1)) + it('returns correct spot price', async () => { + const { result } = renderHookWithProviders( + () => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), + { resolvers } + ) - await waitFor(() => { - expect(result.current.data?.spot).toEqual({ - value: { value: TokenProjectDay.markets?.[0]?.price?.value }, - relativeChange: { value: TokenProjectDay.markets?.[0]?.pricePercentChange24h?.value }, + await waitFor(() => { + expect(result.current.data?.spot).toEqual({ + value: { value: dayTokenProject.markets[0]?.price.value }, + relativeChange: { value: dayTokenProject.markets[0]?.pricePercentChange24h.value }, + }) }) }) }) - }) - describe('when duration is set to non-default value (year)', () => { - it('returns correct price history', async () => { - const { result } = renderHookWithProviders(() => - useTokenPriceHistory(SAMPLE_CURRENCY_ID_1, jest.fn(), HistoryDuration.Year) - ) + describe('when duration is set to non-default value (year)', () => { + it('returns correct price history', async () => { + const { result } = renderHookWithProviders( + () => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1, jest.fn(), HistoryDuration.Year), + { resolvers } + ) - await waitFor(() => { - expect(result.current).toEqual( - expect.objectContaining({ - data: { - priceHistory: formatPriceHistory(TokenYearPriceHistory), - spot: expect.anything(), - }, - selectedDuration: HistoryDuration.Year, - }) + await waitFor(() => { + expect(result.current).toEqual( + expect.objectContaining({ + data: { + priceHistory: formatPriceHistory(yearPriceHistory), + spot: expect.anything(), + }, + selectedDuration: HistoryDuration.Year, + }) + ) + }) + }) + + it('returns correct spot price', async () => { + const { result } = renderHookWithProviders( + () => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1, jest.fn(), HistoryDuration.Year), + { resolvers } ) + await waitFor(() => { + expect(result.current.data?.spot).toEqual({ + value: { value: yearTokenProject.markets[0]?.price?.value }, + relativeChange: { value: yearTokenProject.markets[0]?.pricePercentChange24h?.value }, + }) + }) }) }) - it('returns correct spot price', async () => { - const { result } = renderHookWithProviders(() => - useTokenPriceHistory(SAMPLE_CURRENCY_ID_1, jest.fn(), HistoryDuration.Year) - ) + describe('when duration is changed', () => { + it('re-fetches data', async () => { + const onCompleted = jest.fn() + const { result } = renderHookWithProviders( + () => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1, onCompleted), + { resolvers } + ) - await waitFor(() => { - expect(result.current.data?.spot).toEqual({ - value: { value: TokenProjectYear.markets?.[0]?.price?.value }, - relativeChange: { value: TokenProjectYear.markets?.[0]?.pricePercentChange24h?.value }, + await waitFor(() => { + expect(result.current).toEqual( + expect.objectContaining({ + loading: false, + error: false, + selectedDuration: HistoryDuration.Day, + }) + ) }) - }) - }) - }) - describe('when duration is changed', () => { - it('re-fetches data', async () => { - const onCompleted = jest.fn() - const { result } = renderHookWithProviders(() => - useTokenPriceHistory(SAMPLE_CURRENCY_ID_1, onCompleted) - ) + expect(onCompleted).toHaveBeenCalledTimes(1) - await waitFor(() => { - expect(result.current).toEqual( - expect.objectContaining({ - loading: false, - error: false, - selectedDuration: HistoryDuration.Day, - }) - ) - }) - expect(onCompleted).toHaveBeenCalledTimes(1) + // Change duration + await act(() => { + result.current.setDuration(HistoryDuration.Week) + }) - // Change duration - await act(() => { - result.current.setDuration(HistoryDuration.Week) - }) + await waitFor(() => { + expect(result.current).toEqual( + expect.objectContaining({ + loading: false, + error: false, + selectedDuration: HistoryDuration.Week, + }) + ) + }) - await waitFor(() => { - expect(result.current).toEqual( - expect.objectContaining({ - loading: false, - error: false, - selectedDuration: HistoryDuration.Week, - }) - ) + expect(onCompleted).toHaveBeenCalledTimes(2) }) - expect(onCompleted).toHaveBeenCalledTimes(2) - }) - it('returns new price history and spot price', async () => { - const { result } = renderHookWithProviders(() => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1)) + it('returns new price history and spot price', async () => { + const { result } = renderHookWithProviders( + () => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), + { resolvers } + ) - await waitFor(() => { - expect(result.current.data).toEqual({ - priceHistory: formatPriceHistory(TokenDayPriceHistory), - spot: { - value: { value: TokenProjectDay.markets?.[0]?.price?.value }, - relativeChange: { value: TokenProjectDay.markets?.[0]?.pricePercentChange24h?.value }, - }, + await waitFor(() => { + expect(result.current.data).toEqual({ + priceHistory: formatPriceHistory(dayPriceHistory), + spot: { + value: { value: dayTokenProject.markets[0]?.price.value }, + relativeChange: { value: dayTokenProject.markets[0]?.pricePercentChange24h.value }, + }, + }) }) - }) - // Change duration - await act(() => { - result.current.setDuration(HistoryDuration.Week) - }) + // Change duration + await act(() => { + result.current.setDuration(HistoryDuration.Week) + }) - await waitFor(() => { - expect(result.current.data).toEqual({ - priceHistory: formatPriceHistory(TokenWeekPriceHistory), - spot: { - value: { value: TokenProjectWeek.markets?.[0]?.price?.value }, - relativeChange: { value: TokenProjectWeek.markets?.[0]?.pricePercentChange24h?.value }, - }, + await waitFor(() => { + expect(result.current.data).toEqual({ + priceHistory: formatPriceHistory(weekPriceHistory), + spot: { + value: { value: weekTokenProject.markets[0]?.price?.value }, + relativeChange: { + value: weekTokenProject.markets[0]?.pricePercentChange24h?.value, + }, + }, + }) }) }) }) - }) - describe('error handling', () => { - it('returns error if query has no data and there is no loading state', async () => { - jest.spyOn(console, 'error').mockImplementation(() => undefined) - const { result } = renderHookWithProviders(() => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), { - resolvers: { - Query: { - tokenProjects: () => { - throw new Error('error') + describe('error handling', () => { + it('returns error if query has no data and there is no loading state', async () => { + jest.spyOn(console, 'error').mockImplementation(() => undefined) + const { result } = renderHookWithProviders( + () => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), + { + resolvers: { + Query: { + tokenProjects: () => { + throw new Error('error') + }, + }, }, - }, - }, - }) + } + ) - await waitFor(() => { - expect(result.current.loading).toBe(false) - expect(result.current.error).toBe(true) + await waitFor(() => { + expect(result.current.loading).toBe(false) + expect(result.current.error).toBe(true) + }) }) }) }) diff --git a/apps/mobile/src/components/QRCodeScanner/QRCodeScanner.tsx b/apps/mobile/src/components/QRCodeScanner/QRCodeScanner.tsx index 1e985703f6a..8ff531163f8 100644 --- a/apps/mobile/src/components/QRCodeScanner/QRCodeScanner.tsx +++ b/apps/mobile/src/components/QRCodeScanner/QRCodeScanner.tsx @@ -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 }, - { - text: t('Not now'), - }, - ] - ) + Alert.alert(t('qrScanner.error.camera.title'), t('qrScanner.error.camera.message'), [ + { text: t('common.navigation.systemSettings'), onPress: openSettings }, + { + 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)}> - {t('Scan a QR code')} + {t('qrScanner.title')} {!shouldFreezeCamera ? ( @@ -205,7 +201,9 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E - {isWalletConnectModal ? t('Connecting...') : t('Loading...')} + {isWalletConnectModal + ? t('qrScanner.status.connecting') + : t('qrScanner.status.loading')} @@ -268,11 +266,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E icon={} 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 })} )} diff --git a/apps/mobile/src/components/QRCodeScanner/WalletQRCode.tsx b/apps/mobile/src/components/QRCodeScanner/WalletQRCode.tsx index eacef8e4551..dc1f67fd512 100644 --- a/apps/mobile/src/components/QRCodeScanner/WalletQRCode.tsx +++ b/apps/mobile/src/components/QRCodeScanner/WalletQRCode.tsx @@ -61,7 +61,7 @@ export function WalletQRCode({ address }: Props): JSX.Element | null { /> - {t('You can send tokens on all of our supported networks to this address.')} + {t('qrScanner.wallet.title')} setShowModal(true)}> @@ -78,10 +78,8 @@ export function WalletQRCode({ address }: Props): JSX.Element | null { {showModal && ( } modalName={ModalName.QRCodeNetworkInfo} - title={t('Supported Networks')} + title={t('qrScanner.wallet.networks.title')} onClose={(): void => setShowModal(false)}> diff --git a/apps/mobile/src/components/RecipientSelect/RecipientScanModal.tsx b/apps/mobile/src/components/RecipientSelect/RecipientScanModal.tsx index 5d8415efd98..cd9c9f81539 100644 --- a/apps/mobile/src/components/RecipientSelect/RecipientScanModal.tsx +++ b/apps/mobile/src/components/RecipientSelect/RecipientScanModal.tsx @@ -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.'), - [ - { - text: t('Try again'), - onPress: (): void => { - setShouldFreezeCamera(false) - }, + Alert.alert(t('qrScanner.recipient.error.title'), t('qrScanner.recipient.error.message'), [ + { + text: t('common.button.tryAgain'), + onPress: (): void => { + setShouldFreezeCamera(false) }, - ] - ) + }, + ]) } } @@ -107,8 +103,8 @@ export function RecipientScanModal({ onSelectRecipient, onClose }: Props): JSX.E )} {currentScreenState === ScannerModalState.ScanQr - ? t('Show my QR code') - : t('Scan a QR code')} + ? t('qrScanner.recipient.action.show') + : t('qrScanner.recipient.action.scan')} diff --git a/apps/mobile/src/components/RecipientSelect/RecipientSelect.tsx b/apps/mobile/src/components/RecipientSelect/RecipientSelect.tsx index 555dfe9dc2e..9d5d2f527a4 100644 --- a/apps/mobile/src/components/RecipientSelect/RecipientSelect.tsx +++ b/apps/mobile/src/components/RecipientSelect/RecipientSelect.tsx @@ -75,22 +75,22 @@ export function _RecipientSelect({ mt="$spacing16" px="$spacing24"> - {t('Send')} + {t('qrScanner.recipient.label.send')} } - placeholder={t('Search ENS or address')} + placeholder={t('qrScanner.recipient.input.placeholder')} value={pattern ?? ''} onBack={recipient ? onToggleShowRecipientSelector : undefined} onChangeText={onChangePattern} /> {noResults ? ( - {t('No results found')} + {t('qrScanner.recipient.results.empty')} - {t('The address you typed either does not exist or is spelled incorrectly.')} + {t('qrScanner.recipient.results.error')} ) : ( diff --git a/apps/mobile/src/components/RecipientSelect/hooks.test.ts b/apps/mobile/src/components/RecipientSelect/hooks.test.ts index dbdc1cb40fb..06fcc306558 100644 --- a/apps/mobile/src/components/RecipientSelect/hooks.test.ts +++ b/apps/mobile/src/components/RecipientSelect/hooks.test.ts @@ -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 { 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: '', }, ]) diff --git a/apps/mobile/src/components/RemoveWallet/RemoveLastMnemonicWalletFooter.tsx b/apps/mobile/src/components/RemoveWallet/RemoveLastMnemonicWalletFooter.tsx index dcf36b62be9..d32cb603fa3 100644 --- a/apps/mobile/src/components/RemoveWallet/RemoveLastMnemonicWalletFooter.tsx +++ b/apps/mobile/src/components/RemoveWallet/RemoveLastMnemonicWalletFooter.tsx @@ -29,9 +29,7 @@ export function RemoveLastMnemonicWalletFooter({ text={ - {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')} } @@ -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} diff --git a/apps/mobile/src/components/RemoveWallet/RemoveWalletModal.tsx b/apps/mobile/src/components/RemoveWallet/RemoveWalletModal.tsx index 459cfddec0e..25fc43a1542 100644 --- a/apps/mobile/src/components/RemoveWallet/RemoveWalletModal.tsx +++ b/apps/mobile/src/components/RemoveWallet/RemoveWalletModal.tsx @@ -189,7 +189,7 @@ export function RemoveWalletModal(): JSX.Element | null { ) : ( )} diff --git a/apps/mobile/src/components/Settings/BiometricAuthWarningModal.tsx b/apps/mobile/src/components/Settings/BiometricAuthWarningModal.tsx index 93809b8c7e2..7d0daa4fbfb 100644 --- a/apps/mobile/src/components/Settings/BiometricAuthWarningModal.tsx +++ b/apps/mobile/src/components/Settings/BiometricAuthWarningModal.tsx @@ -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 ( diff --git a/apps/mobile/src/components/TokenBalanceList/TokenBalanceList.tsx b/apps/mobile/src/components/TokenBalanceList/TokenBalanceList.tsx index 8bf2090ef5d..3484d61bae3 100644 --- a/apps/mobile/src/components/TokenBalanceList/TokenBalanceList.tsx +++ b/apps/mobile/src/components/TokenBalanceList/TokenBalanceList.tsx @@ -164,7 +164,7 @@ export const TokenBalanceListInner = forwardRef< const ListHeaderComponent = useMemo(() => { return hasError ? ( - + ) : null }, [hasError, refetch, t]) @@ -204,8 +204,8 @@ export const TokenBalanceListInner = forwardRef< ) : ( refetch?.()} /> diff --git a/apps/mobile/src/components/TokenDetails/TokenBalances.tsx b/apps/mobile/src/components/TokenDetails/TokenBalances.tsx index 3de1ccb1065..a864686e1c0 100644 --- a/apps/mobile/src/components/TokenDetails/TokenBalances.tsx +++ b/apps/mobile/src/components/TokenDetails/TokenBalances.tsx @@ -68,7 +68,7 @@ export function TokenBalances({ {hasOtherChainBalances && otherChainBalances ? ( - {t('Balances on other networks')} + {t('token.balances.other')} {otherChainBalances.map((balance) => { @@ -106,7 +106,9 @@ export function CurrentChainBalance({ - {isReadonly ? t('{{owner}}’s balance', { owner: displayName }) : t('Your balance')} + {isReadonly + ? t('token.balances.viewOnly', { ownerAddress: displayName }) + : t('token.balances.main')} diff --git a/apps/mobile/src/components/TokenDetails/TokenDetailsActionButtons.tsx b/apps/mobile/src/components/TokenDetails/TokenDetailsActionButtons.tsx index 653d3e1712d..f5cfd168ed1 100644 --- a/apps/mobile/src/components/TokenDetails/TokenDetailsActionButtons.tsx +++ b/apps/mobile/src/components/TokenDetails/TokenDetailsActionButtons.tsx @@ -58,13 +58,13 @@ export function TokenDetailsActionButtons({ px="$spacing16"> diff --git a/apps/mobile/src/components/TokenDetails/TokenDetailsLinks.tsx b/apps/mobile/src/components/TokenDetails/TokenDetailsLinks.tsx index a2a9f81e89c..cea8d8c25dc 100644 --- a/apps/mobile/src/components/TokenDetails/TokenDetailsLinks.tsx +++ b/apps/mobile/src/components/TokenDetails/TokenDetailsLinks.tsx @@ -35,7 +35,7 @@ export function TokenDetailsLinks({ - {t('Links')} + {t('token.links.title')} @@ -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({ )} diff --git a/apps/mobile/src/components/TokenDetails/TokenDetailsStats.tsx b/apps/mobile/src/components/TokenDetails/TokenDetailsStats.tsx index 22847a2d4df..68044f3a014 100644 --- a/apps/mobile/src/components/TokenDetails/TokenDetailsStats.tsx +++ b/apps/mobile/src/components/TokenDetails/TokenDetailsStats.tsx @@ -57,7 +57,7 @@ export function TokenDetailsMarketData({ return ( }> @@ -66,7 +66,7 @@ export function TokenDetailsMarketData({ }> @@ -75,7 +75,7 @@ export function TokenDetailsMarketData({ }> @@ -84,7 +84,7 @@ export function TokenDetailsMarketData({ }> @@ -93,7 +93,7 @@ export function TokenDetailsMarketData({ }> @@ -147,7 +147,7 @@ export function TokenDetailsStats({ {name && ( - {t('About {{ token }}', { token: name })} + {t('token.stats.section.about', { token: name })} )} @@ -177,7 +177,7 @@ export function TokenDetailsStats({ - {t('Show original')} + {t('token.stats.translation.original')} ) : ( @@ -185,7 +185,7 @@ export function TokenDetailsStats({ - {t('Translate to {{ language }}', { + {t('token.stats.translation.translate', { language: currentLanguageInfo.displayName, })} @@ -199,7 +199,7 @@ export function TokenDetailsStats({ )} - {t('Stats')} + {t('token.stats.title')} { 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, - resolvers: { - Query: { - portfolios: () => [Portfolio], + 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, - resolvers: { - Query: { - portfolios: () => [Portfolio2], - }, - }, + + 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: () => [Portfolio], + }, + }, + } + ) + await waitFor(() => { expect(result.current).toEqual( - expect.objectContaining({ - currentChainBalance: PortfolioBalancesById[currencyId1], - otherChainBalances: [PortfolioBalancesById[currencyId2]], - }) + expect.objectContaining({ currentChainBalance, otherChainBalances }) ) }) }) diff --git a/apps/mobile/src/components/TokenSelector/TokenFiatOnRampList.tsx b/apps/mobile/src/components/TokenSelector/TokenFiatOnRampList.tsx index 17e4fcad043..940ac176c20 100644 --- a/apps/mobile/src/components/TokenSelector/TokenFiatOnRampList.tsx +++ b/apps/mobile/src/components/TokenSelector/TokenFiatOnRampList.tsx @@ -71,8 +71,8 @@ function _TokenFiatOnRampList({ return ( diff --git a/apps/mobile/src/components/WalletConnect/ConnectedDapps/ConnectedDappsList.tsx b/apps/mobile/src/components/WalletConnect/ConnectedDapps/ConnectedDappsList.tsx index 739d0ef0ccc..c39cd46c100 100644 --- a/apps/mobile/src/components/WalletConnect/ConnectedDapps/ConnectedDappsList.tsx +++ b/apps/mobile/src/components/WalletConnect/ConnectedDapps/ConnectedDappsList.tsx @@ -50,7 +50,7 @@ export function ConnectedDappsList({ backButton, sessions }: ConnectedDappsProps - {t('Manage connections')} + {t('walletConnect.dapps.manage.title')} @@ -101,10 +101,10 @@ export function ConnectedDappsList({ backButton, sessions }: ConnectedDappsProps paddingTop: fullHeight / 5, }}> - {t('No apps connected')} + {t('walletConnect.dapps.manage.empty.title')} - {t('Connect to an app by scanning a code via WalletConnect')} + {t('walletConnect.dapps.empty.description')} )} diff --git a/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectedNetworksModal.tsx b/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectedNetworksModal.tsx index e2d6fbc6737..d19302ad1ff 100644 --- a/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectedNetworksModal.tsx +++ b/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectedNetworksModal.tsx @@ -1,11 +1,11 @@ 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({ - {t('Connected to ')} - {dapp.name || dapp.url} + + Connected to + {{ dappNameOrUrl: dapp.name || dapp.url }} + {dapp.url} @@ -101,10 +103,10 @@ export function DappConnectedNetworkModal({ diff --git a/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectionItem.tsx b/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectionItem.tsx index 71ad6d781ba..d2189026207 100644 --- a/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectionItem.tsx +++ b/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectionItem.tsx @@ -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): Promise => { if (e.nativeEvent.index === 0) { diff --git a/apps/mobile/src/components/WalletConnect/RequestModal/HeaderText.tsx b/apps/mobile/src/components/WalletConnect/RequestModal/HeaderText.tsx index 056464b6cd3..42f95175973 100644 --- a/apps/mobile/src/components/WalletConnect/RequestModal/HeaderText.tsx +++ b/apps/mobile/src/components/WalletConnect/RequestModal/HeaderText.tsx @@ -1,6 +1,6 @@ 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 ? ( - - - Allow {dapp.name} to use up to - {readablePermitAmount} - {permitCurrency?.symbol}? - - + + + Allow {{ dappName: dapp.name }} to use up to + {{ amount: readablePermitAmount }} + {{ currencySymbol: permitCurrency?.symbol }}? + + ) : ( - - - Allow {dapp.name} to use your {permitCurrency?.symbol}? - - + + + Allow {{ dappName: dapp.name }} to use your + {{ currencySymbol: permitCurrency?.symbol }}? + + ) } - 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 ( + + Signature request from + {{ dappNameOrUrl }} + + ) case EthMethod.EthSendTransaction: - return t('Transaction request from') + return ( + + Transaction request from + {{ dappNameOrUrl }} + + ) } - return t('Request from') + return ( + + Request from + {{ dappNameOrUrl }} + + ) } return ( - {getReadableMethodName(method)} - {truncateDappName(dapp.name || dapp.url)} + {getReadableMethodName(method, truncateDappName(dapp.name || dapp.url))} ) } diff --git a/apps/mobile/src/components/WalletConnect/RequestModal/RequestDetails.tsx b/apps/mobile/src/components/WalletConnect/RequestModal/RequestDetails.tsx index 68514111a57..7e73b5c1c51 100644 --- a/apps/mobile/src/components/WalletConnect/RequestModal/RequestDetails.tsx +++ b/apps/mobile/src/components/WalletConnect/RequestModal/RequestDetails.tsx @@ -1,7 +1,7 @@ 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,25 +162,29 @@ function TransactionDetails({ ) : null} {to ? ( - - {t('To')}: - - + + + To: + + + ) : null} - - {t('Function')}: - - - - {parsedData ? parsedData.name : t('Unknown')} + + + Function: - + + + {{ functionName: parsedData ? parsedData.name : t('common.text.unknown') }} + + + ) @@ -216,7 +220,7 @@ function RequestDetailsContent({ request }: Props): JSX.Element { {message} ) : ( - {t('No message found.')} + {t('qrScanner.request.message.unavailable')} ) } diff --git a/apps/mobile/src/components/WalletConnect/RequestModal/SpendingDetails.tsx b/apps/mobile/src/components/WalletConnect/RequestModal/SpendingDetails.tsx index 6e23096c33a..32b29e9ba99 100644 --- a/apps/mobile/src/components/WalletConnect/RequestModal/SpendingDetails.tsx +++ b/apps/mobile/src/components/WalletConnect/RequestModal/SpendingDetails.tsx @@ -1,5 +1,5 @@ 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 ( - - {t('Sending')}: - - - - - {formatCurrencyAmount({ value: nativeCurrencyAmount, type: NumberType.TokenTx })}{' '} - {getSymbolDisplayText(nativeCurrencyInfo?.currency.symbol)} - - - ({convertFiatAmountFormatted(usdValue, NumberType.FiatTokenPrice)}) + + + Sending: - + + + {{ tokenAmountWithSymbol }} + + ({{ fiatAmount }}) + + + ) } diff --git a/apps/mobile/src/components/WalletConnect/RequestModal/WalletConnectRequestModal.tsx b/apps/mobile/src/components/WalletConnect/RequestModal/WalletConnectRequestModal.tsx index 8b0195da3ba..95dfa998c41 100644 --- a/apps/mobile/src/components/WalletConnect/RequestModal/WalletConnectRequestModal.tsx +++ b/apps/mobile/src/components/WalletConnect/RequestModal/WalletConnectRequestModal.tsx @@ -314,7 +314,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem ) : ( - {t('Network')} + {t('walletConnect.request.label.network')} {!hasSufficientFunds && ( - {t('You don’t have enough {{symbol}} to complete this transaction.', { - symbol: nativeCurrency?.symbol, + {t('walletConnect.request.error.insufficientFunds', { + currencySymbol: nativeCurrency?.symbol, })} )} @@ -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')} /> ) : ( - {t('Cancel')} + {t('common.button.cancel')} @@ -419,9 +421,9 @@ function WarningSection({ width={iconSizes.icon16} /> - {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')} ) diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionModal.tsx b/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionModal.tsx index 666e018dd37..3734fe0d608 100644 --- a/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionModal.tsx +++ b/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionModal.tsx @@ -68,7 +68,7 @@ const SitePermissions = (): JSX.Element => { allowFontScaling={false} color="$neutral2" variant="subheading2"> - {t('App permissions')} + {t('walletConnect.permissions.title')} @@ -78,7 +78,7 @@ const SitePermissions = (): JSX.Element => { color="$neutral1" flexGrow={1} variant={normalInfoTextSize}> - {t('View your wallet address')} + {t('walletConnect.permissions.option.viewWalletAddress')} @@ -89,7 +89,7 @@ const SitePermissions = (): JSX.Element => { color="$neutral1" flexGrow={1} variant={normalInfoTextSize}> - {t('View your token balances')} + {t('walletConnect.permissions.option.viewTokenBalances')} @@ -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')} @@ -124,7 +124,7 @@ const NetworksRow = ({ chains }: { chains: ChainId[] }): JSX.Element => { allowFontScaling={false} color="$neutral2" variant="subheading2"> - {t('Networks')} + {t('walletConnect.permissions.networks')} @@ -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), })}{' '} @@ -287,13 +287,13 @@ export const PendingConnectionModal = ({ pendingSession, onClose }: Props): JSX. testID="cancel-pending-connection" theme="secondary" onPress={(): Promise => onPressSettleConnection(false)}> - {t('Cancel')} + {t('common.button.cancel')} diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchAccountModal.tsx b/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchAccountModal.tsx index a50c93622b4..971e79274cb 100644 --- a/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchAccountModal.tsx +++ b/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchAccountModal.tsx @@ -37,7 +37,7 @@ export const PendingConnectionSwitchAccountModal = ({ - {t('Switch Account')} + {t('walletConnect.pending.switchAccount')} } isVisible={true} diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchNetworkModal.tsx b/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchNetworkModal.tsx index 409b8d3327d..a80ebf5d5e8 100644 --- a/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchNetworkModal.tsx +++ b/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchNetworkModal.tsx @@ -63,7 +63,7 @@ export const PendingConnectionSwitchNetworkModal = ({ - {t('Switch Network')} + {t('walletConnect.pending.switchNetwork')} } isVisible={true} diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/WalletConnectModal.tsx b/apps/mobile/src/components/WalletConnect/ScanSheet/WalletConnectModal.tsx index cf4ed597a3b..082a5b1a2f9 100644 --- a/apps/mobile/src/components/WalletConnect/ScanSheet/WalletConnectModal.tsx +++ b/apps/mobile/src/components/WalletConnect/ScanSheet/WalletConnectModal.tsx @@ -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.'), [ - { - text: t('OK'), - onPress: (): void => { - setShouldFreezeCamera(false) + Alert.alert( + t('walletConnect.error.uwu.title'), + t('walletConnect.error.uwu.unsupported'), + [ + { + 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({ )} {currentScreenState === ScannerModalState.ScanQr - ? t('Show my QR code') - : t('Scan a QR code')} + ? t('qrScanner.recipient.action.show') + : t('qrScanner.recipient.action.scan')} diff --git a/apps/mobile/src/components/WalletConnect/WalletConnectModals.tsx b/apps/mobile/src/components/WalletConnect/WalletConnectModals.tsx index 7b41deda1ef..2be7ff49f0d 100644 --- a/apps/mobile/src/components/WalletConnect/WalletConnectModals.tsx +++ b/apps/mobile/src/components/WalletConnect/WalletConnectModals.tsx @@ -104,10 +104,8 @@ function RequestModal({ currRequest }: RequestModalProps): JSX.Element { if (!isRequestFromSignerAccount) { return ( {portfolioValue === undefined - ? t('N/A') + ? t('common.text.notAvailable') : convertFiatAmountFormatted(portfolioValue, NumberType.PortfolioBalance)} ) @@ -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]) diff --git a/apps/mobile/src/components/accounts/AccountHeader.test.tsx b/apps/mobile/src/components/accounts/AccountHeader.test.tsx index 375b61d03c3..36b09fb70ce 100644 --- a/apps/mobile/src/components/accounts/AccountHeader.test.tsx +++ b/apps/mobile/src/components/accounts/AccountHeader.test.tsx @@ -1,11 +1,12 @@ 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(, { preloadedState: mockWalletPreloadedState }) + const tree = render(, { preloadedState: mockWalletPreloadedState(ACCOUNT) }) expect(tree.toJSON()).toMatchSnapshot() }) diff --git a/apps/mobile/src/components/accounts/AccountList.test.tsx b/apps/mobile/src/components/accounts/AccountList.test.tsx index e109549d794..709675593ff 100644 --- a/apps/mobile/src/components/accounts/AccountList.test.tsx +++ b/apps/mobile/src/components/accounts/AccountList.test.tsx @@ -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(, { resolvers }) + const tree = render(, { 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(, { + render(, { 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) }) diff --git a/apps/mobile/src/components/accounts/AccountList.tsx b/apps/mobile/src/components/accounts/AccountList.tsx index 8107b31e425..508a59aa792 100644 --- a/apps/mobile/src/components/accounts/AccountList.tsx +++ b/apps/mobile/src/components/accounts/AccountList.tsx @@ -31,7 +31,7 @@ const ViewOnlyHeader = (): JSX.Element => { return ( - {t('View only wallets')} + {t('account.wallet.header.viewOnly')} ) @@ -42,7 +42,7 @@ const SignerHeader = (): JSX.Element => { return ( - {t('Your other wallets')} + {t('account.wallet.header.other')} ) diff --git a/apps/mobile/src/components/banners/OfflineBanner.tsx b/apps/mobile/src/components/banners/OfflineBanner.tsx index 51e0b712634..0589572f627 100644 --- a/apps/mobile/src/components/banners/OfflineBanner.tsx +++ b/apps/mobile/src/components/banners/OfflineBanner.tsx @@ -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 diff --git a/apps/mobile/src/components/buttons/BackButton.test.tsx b/apps/mobile/src/components/buttons/BackButton.test.tsx index b2a46d0cb79..bc86e708984 100644 --- a/apps/mobile/src/components/buttons/BackButton.test.tsx +++ b/apps/mobile/src/components/buttons/BackButton.test.tsx @@ -1,7 +1,7 @@ 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', () => { diff --git a/apps/mobile/src/components/buttons/CopyTextButton.tsx b/apps/mobile/src/components/buttons/CopyTextButton.tsx index 4f2aec39f93..ae610352a75 100644 --- a/apps/mobile/src/components/buttons/CopyTextButton.tsx +++ b/apps/mobile/src/components/buttons/CopyTextButton.tsx @@ -42,7 +42,7 @@ export function CopyTextButton({ copyText }: Props): JSX.Element { return ( ) } diff --git a/apps/mobile/src/components/education/SeedPhrase.tsx b/apps/mobile/src/components/education/SeedPhrase.tsx index 1eedf368b50..4e2e9053cb0 100644 --- a/apps/mobile/src/components/education/SeedPhrase.tsx +++ b/apps/mobile/src/components/education/SeedPhrase.tsx @@ -1,5 +1,5 @@ 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}> - What’s a recovery phrase? + {t('onboarding.tooltip.recoveryPhrase.trigger')} undefined} /> @@ -71,13 +72,14 @@ function Page({ ) } +const cloudProviderName = getCloudProviderName() export const SeedPhraseEducationContent = (params: OnboardingStackBaseParams): JSX.Element[] => [ - - A recovery phrase (or seed phrase) is a{' '} + + A recovery phrase (or seed phrase) is a set of words required to access your wallet, like a password. @@ -88,9 +90,9 @@ export const SeedPhraseEducationContent = (params: OnboardingStackBaseParams): J params={params} text={ - + You can enter your recovery phrase - on a new device{' '} + on a new device to restore your wallet and its contents. @@ -101,9 +103,9 @@ export const SeedPhraseEducationContent = (params: OnboardingStackBaseParams): J params={params} text={ - - But, if you{' '} - lose your recovery phrase, you’ll{' '} + + But, if you + lose your recovery phrase, you’ll lose access to your wallet. @@ -113,19 +115,13 @@ export const SeedPhraseEducationContent = (params: OnboardingStackBaseParams): J params={params} text={ - {isAndroid ? ( - - Instead of memorizing your recovery phrase, you can{' '} - back it up to Google Drive and - protect it with a password. - - ) : ( - - Instead of memorizing your recovery phrase, you can{' '} - back it up to iCloud and protect - it with a password. - - )} + + Instead of memorizing your recovery phrase, you can + + back it up to {{ cloudProviderName }} + + and protect it with a password. + } />, @@ -133,8 +129,8 @@ export const SeedPhraseEducationContent = (params: OnboardingStackBaseParams): J params={params} text={ - - You can also manually back up your recovery phrase by{' '} + + You can also manually back up your recovery phrase by writing it down and storing it in a safe place. @@ -145,8 +141,8 @@ export const SeedPhraseEducationContent = (params: OnboardingStackBaseParams): J params={params} text={ - - We recommend using{' '} + + We recommend using both types of backups, because if you lose your recovery phrase, you won’t be able to restore your wallet. diff --git a/apps/mobile/src/components/explore/ExploreSections.tsx b/apps/mobile/src/components/explore/ExploreSections.tsx index 3442fd1a624..f2a1dca4fda 100644 --- a/apps/mobile/src/components/explore/ExploreSections.tsx +++ b/apps/mobile/src/components/explore/ExploreSections.tsx @@ -142,8 +142,8 @@ export function ExploreSections({ listRef }: ExploreSectionsProps): JSX.Element return ( @@ -188,7 +188,7 @@ export function ExploreSections({ listRef }: ExploreSectionsProps): JSX.Element mt="$spacing16" pl="$spacing4"> - {t('Top tokens')} + {t('explore.tokens.top.title')} diff --git a/apps/mobile/src/components/explore/FavoriteHeaderRow.tsx b/apps/mobile/src/components/explore/FavoriteHeaderRow.tsx index c9149287594..db3fc6e781a 100644 --- a/apps/mobile/src/components/explore/FavoriteHeaderRow.tsx +++ b/apps/mobile/src/components/explore/FavoriteHeaderRow.tsx @@ -39,7 +39,7 @@ export function FavoriteHeaderRow({ ) : ( - {t('Done')} + {t('common.button.done')} )} diff --git a/apps/mobile/src/components/explore/FavoriteTokensGrid.tsx b/apps/mobile/src/components/explore/FavoriteTokensGrid.tsx index a0c90caebe1..a2c22c69e3c 100644 --- a/apps/mobile/src/components/explore/FavoriteTokensGrid.tsx +++ b/apps/mobile/src/components/explore/FavoriteTokensGrid.tsx @@ -72,9 +72,9 @@ export function FavoriteTokensGrid({ return ( setIsEditing(!isEditing)} /> {showLoading ? ( diff --git a/apps/mobile/src/components/explore/FavoriteWalletCard.tsx b/apps/mobile/src/components/explore/FavoriteWalletCard.tsx index 4226defe438..e8561dba5e9 100644 --- a/apps/mobile/src/components/explore/FavoriteWalletCard.tsx +++ b/apps/mobile/src/components/explore/FavoriteWalletCard.tsx @@ -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]) diff --git a/apps/mobile/src/components/explore/FavoriteWalletsGrid.tsx b/apps/mobile/src/components/explore/FavoriteWalletsGrid.tsx index bc2fd2e7a5b..b67b0372e3f 100644 --- a/apps/mobile/src/components/explore/FavoriteWalletsGrid.tsx +++ b/apps/mobile/src/components/explore/FavoriteWalletsGrid.tsx @@ -71,9 +71,9 @@ export function FavoriteWalletsGrid({ return ( setIsEditing(!isEditing)} /> {showLoading ? ( diff --git a/apps/mobile/src/components/explore/TokenItem.tsx b/apps/mobile/src/components/explore/TokenItem.tsx index 88bb2a60e1a..8fc5bf52b92 100644 --- a/apps/mobile/src/components/explore/TokenItem.tsx +++ b/apps/mobile/src/components/explore/TokenItem.tsx @@ -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 diff --git a/apps/mobile/src/components/explore/hooks.test.ts b/apps/mobile/src/components/explore/hooks.test.ts index 188b43e66dc..5ec9109c284 100644 --- a/apps/mobile/src/components/explore/hooks.test.ts +++ b/apps/mobile/src/components/explore/hooks.test.ts @@ -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 = { diff --git a/apps/mobile/src/components/explore/hooks.ts b/apps/mobile/src/components/explore/hooks.ts index 1c6dae1b7c3..867313d9679 100644 --- a/apps/mobile/src/components/explore/hooks.ts +++ b/apps/mobile/src/components/explore/hooks.ts @@ -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, }, diff --git a/apps/mobile/src/components/explore/search/SearchEmptySection.tsx b/apps/mobile/src/components/explore/search/SearchEmptySection.tsx index 99d09b7ca23..3e2fc41ae70 100644 --- a/apps/mobile/src/components/explore/search/SearchEmptySection.tsx +++ b/apps/mobile/src/components/explore/search/SearchEmptySection.tsx @@ -73,10 +73,13 @@ export function SearchEmptySection(): JSX.Element { gap="$spacing16" justifyContent="space-between" mb="$spacing4"> - } title={t('Recent searches')} /> + } + title={t('explore.search.section.recent')} + /> - {t('Clear all')} + {t('explore.search.action.clear')} @@ -89,16 +92,19 @@ export function SearchEmptySection(): JSX.Element { )} - } title={t('Popular tokens')} /> + } title={t('explore.search.section.popularTokens')} /> - } title={t('Popular NFT collections')} /> + } title={t('explore.search.section.popularNFT')} /> } title={t('Suggested wallets')} /> + } + title={t('explore.search.section.suggestedWallets')} + /> } data={SUGGESTED_WALLETS} keyExtractor={walletKey} diff --git a/apps/mobile/src/components/explore/search/SearchPopularTokens.test.tsx b/apps/mobile/src/components/explore/search/SearchPopularTokens.test.tsx index 12ba3db5494..4e7c890c85b 100644 --- a/apps/mobile/src/components/explore/search/SearchPopularTokens.test.tsx +++ b/apps/mobile/src/components/explore/search/SearchPopularTokens.test.tsx @@ -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 })], }, } diff --git a/apps/mobile/src/components/explore/search/SearchResultsLoader.tsx b/apps/mobile/src/components/explore/search/SearchResultsLoader.tsx index 4164b79d84e..326559d26bc 100644 --- a/apps/mobile/src/components/explore/search/SearchResultsLoader.tsx +++ b/apps/mobile/src/components/explore/search/SearchResultsLoader.tsx @@ -9,19 +9,19 @@ export const SearchResultsLoader = (): JSX.Element => { return ( - + - + - + diff --git a/apps/mobile/src/components/explore/search/SearchResultsSection.tsx b/apps/mobile/src/components/explore/search/SearchResultsSection.tsx index 8d70d072af3..8b03872a684 100644 --- a/apps/mobile/src/components/explore/search/SearchResultsSection.tsx +++ b/apps/mobile/src/components/explore/search/SearchResultsSection.tsx @@ -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 ( @@ -184,8 +184,8 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }): ListEmptyComponent={ - - No results found for "{searchQuery}" + + No results found for "{{ searchQuery }}" diff --git a/apps/mobile/src/components/explore/search/items/SearchENSAddressItem.tsx b/apps/mobile/src/components/explore/search/items/SearchENSAddressItem.tsx index 4f17b1ea8a8..8348dd92172 100644 --- a/apps/mobile/src/components/explore/search/items/SearchENSAddressItem.tsx +++ b/apps/mobile/src/components/explore/search/items/SearchENSAddressItem.tsx @@ -63,8 +63,8 @@ export function SearchENSAddressItem({ {showSecondLine ? ( {showOwnedBy && - t('Owned by {{owner}}', { - owner: primaryENSName || formattedAddress, + t('explore.search.label.ownedBy', { + ownerAddress: primaryENSName || formattedAddress, })} {showAddress && formattedAddress} diff --git a/apps/mobile/src/components/explore/search/items/SearchWalletItemBase.tsx b/apps/mobile/src/components/explore/search/items/SearchWalletItemBase.tsx index 07745480ec4..7f2dfea1b13 100644 --- a/apps/mobile/src/components/explore/search/items/SearchWalletItemBase.tsx +++ b/apps/mobile/src/components/explore/search/items/SearchWalletItemBase.tsx @@ -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 ( diff --git a/apps/mobile/src/components/explore/search/utils.test.ts b/apps/mobile/src/components/explore/search/utils.test.ts index 510548e6814..f6d98ee6779 100644 --- a/apps/mobile/src/components/explore/search/utils.test.ts +++ b/apps/mobile/src/components/explore/search/utils.test.ts @@ -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 @@ -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) - }) -}) - -describe(gqlNFTToNFTCollectionSearchResult, () => { - const node = TopNFTCollections[0] - - 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) - }) - - it('properly formats NFT collection search result', () => { - const result = gqlNFTToNFTCollectionSearchResult(node) - - 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?.[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(formatNFTCollectionSearchResults, () => { - it('returns undefined if there is no data', () => { - expect(formatNFTCollectionSearchResults(null)).toEqual(undefined) + describe(gqlNFTToNFTCollectionSearchResult, () => { + const collection = nftCollection({ + nftContracts: [nftContract({ chain: Chain.Ethereum })], + }) + + it('returns null if required data is missing', () => { + 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(collection) + + expect(result?.type).toEqual(SearchResultType.NFTCollection) + expect(result?.chainId).toEqual(fromGraphQLChain(Chain.Ethereum)) + 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) + }) }) - it('filters out nfts that cannot be formatted', () => { - const nftSearchResult = { - edges: [ - ...TopNFTCollections.map((nft) => ({ node: nft })), - { node: { ...TopNFTCollections[0], 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) + 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: 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) + }) }) }) diff --git a/apps/mobile/src/components/explore/search/utils.ts b/apps/mobile/src/components/explore/search/utils.ts index 0b4a806231c..fbc23a16cb2 100644 --- a/apps/mobile/src/components/explore/search/utils.ts +++ b/apps/mobile/src/components/explore/search/utils.ts @@ -1,3 +1,5 @@ +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, diff --git a/apps/mobile/src/components/fiatOnRamp/CtaButton.tsx b/apps/mobile/src/components/fiatOnRamp/CtaButton.tsx index c70db1ba625..19cfd02ad4f 100644 --- a/apps/mobile/src/components/fiatOnRamp/CtaButton.tsx +++ b/apps/mobile/src/components/fiatOnRamp/CtaButton.tsx @@ -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 ( {quoteAmount && ( - {t('Receive {{amount}}', { - amount: `${quoteAmount + getSymbolDisplayText(currency?.symbol)}`, + {t('fiatOnRamp.quote.amount', { + tokenAmount: `${quoteAmount + getSymbolDisplayText(currency?.symbol)}`, })} )} - {t('{{amount}} after fees', { amount: quoteEquivalentInSourceCurrencyAmount })} + {t('fiatOnRamp.quote.amountAfterFees', { + tokenAmount: quoteEquivalentInSourceCurrencyAmount, + })} {showCarret ? ( diff --git a/apps/mobile/src/components/forceUpgrade/ForceUpgradeModal.tsx b/apps/mobile/src/components/forceUpgrade/ForceUpgradeModal.tsx index f7e27a452f1..52e9abbbd32 100644 --- a/apps/mobile/src/components/forceUpgrade/ForceUpgradeModal.tsx +++ b/apps/mobile/src/components/forceUpgrade/ForceUpgradeModal.tsx @@ -65,22 +65,20 @@ export function ForceUpgradeModal(): JSX.Element { <> {isVisible && ( - {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')} {mnemonicId && ( - {t('View recovery phrase')} + {t('forceUpgrade.action.seedPhrase')} )} @@ -96,7 +94,7 @@ export function ForceUpgradeModal(): JSX.Element { - {t('Recovery phrase')} + {t('forceUpgrade.label.seedPhrase')} diff --git a/apps/mobile/src/components/home/FeedTab.tsx b/apps/mobile/src/components/home/FeedTab.tsx index 930a7ceaf8d..cd25f9967e0 100644 --- a/apps/mobile/src/components/home/FeedTab.tsx +++ b/apps/mobile/src/components/home/FeedTab.tsx @@ -76,8 +76,8 @@ export const FeedTab = memo( const errorCard = ( @@ -86,9 +86,9 @@ export const FeedTab = memo( const emptyListView = ( } - title={t('No activity yet')} + title={t('home.feed.empty.title')} onPress={onPressReceive} /> diff --git a/apps/mobile/src/components/home/TokensTab.tsx b/apps/mobile/src/components/home/TokensTab.tsx index 9bc5c16ef78..40207ec4518 100644 --- a/apps/mobile/src/components/home/TokensTab.tsx +++ b/apps/mobile/src/components/home/TokensTab.tsx @@ -71,9 +71,9 @@ export const TokensTab = memo( // Show different empty state on external profile pages return isExternalProfile ? ( } - title={t('No tokens yet')} + title={t('home.tokens.empty.title')} onPress={onPressAction} /> ) : ( diff --git a/apps/mobile/src/components/home/WalletEmptyState.tsx b/apps/mobile/src/components/home/WalletEmptyState.tsx index a2398838cdd..c8c71b2f019 100644 --- a/apps/mobile/src/components/home/WalletEmptyState.tsx +++ b/apps/mobile/src/components/home/WalletEmptyState.tsx @@ -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: ( 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: ( {showButtonLabel && ( - {t('Back')} + {t('common.button.back')} )} diff --git a/apps/mobile/src/components/mnemonic/SeedPhraseDisplay.tsx b/apps/mobile/src/components/mnemonic/SeedPhraseDisplay.tsx index 8af6474b970..83c42ee1d11 100644 --- a/apps/mobile/src/components/mnemonic/SeedPhraseDisplay.tsx +++ b/apps/mobile/src/components/mnemonic/SeedPhraseDisplay.tsx @@ -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')} {showSeedPhraseViewWarningModal && ( { setShowSeedPhraseViewWarningModal(false) if (!showSeedPhrase) { @@ -103,12 +105,10 @@ export function SeedPhraseDisplay({ )} {showScreenShotWarningModal && ( setShowScreenShotWarningModal(false)} /> )} diff --git a/apps/mobile/src/components/text/LongMarkdownText.tsx b/apps/mobile/src/components/text/LongMarkdownText.tsx index 79fe4a81b8d..491fda5507c 100644 --- a/apps/mobile/src/components/text/LongMarkdownText.tsx +++ b/apps/mobile/src/components/text/LongMarkdownText.tsx @@ -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')} ) : null} diff --git a/apps/mobile/src/components/text/LongText.tsx b/apps/mobile/src/components/text/LongText.tsx index c20ede11758..b9f727d15fc 100644 --- a/apps/mobile/src/components/text/LongText.tsx +++ b/apps/mobile/src/components/text/LongText.tsx @@ -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')} ) : null} diff --git a/apps/mobile/src/components/tooltip/TooltipButton.tsx b/apps/mobile/src/components/tooltip/TooltipButton.tsx index 67328552951..e7b891d588d 100644 --- a/apps/mobile/src/components/tooltip/TooltipButton.tsx +++ b/apps/mobile/src/components/tooltip/TooltipButton.tsx @@ -49,7 +49,7 @@ export function TooltipInfoButton({ - {t('Edit username')} + {t('unitags.editUsername.title')} - {t('You’ve reached the maximum number of 2 usernames changes.')} + {t('unitags.editUsername.warning.max')} ) : ( @@ -260,9 +260,7 @@ export function ChangeUnitagModal({ py="$spacing12" width="100%"> - {t( - 'Once you change your username, you can never claim it again. You can only change it 2 times.' - )} + {t('unitags.editUsername.warning.default')} )} @@ -285,7 +283,7 @@ export function ChangeUnitagModal({ ) : ( - t('Save changes') + t('unitags.editUsername.button.confirm') )} @@ -316,19 +314,17 @@ function ChangeUnitagConfirmModal({ - {t('Are you sure?')} + {t('unitags.editUsername.confirm.title')} - {t( - 'You’re about to change your username. Once you change it, you can never claim it again.' - )} + {t('unitags.editUsername.confirm.subtitle')} diff --git a/apps/mobile/src/components/unitags/ChoosePhotoOptionsModal.tsx b/apps/mobile/src/components/unitags/ChoosePhotoOptionsModal.tsx index 7bec1aa3825..3a9e7c42bae 100644 --- a/apps/mobile/src/components/unitags/ChoosePhotoOptionsModal.tsx +++ b/apps/mobile/src/components/unitags/ChoosePhotoOptionsModal.tsx @@ -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')} diff --git a/apps/mobile/src/components/unitags/DeleteUnitagModal.tsx b/apps/mobile/src/components/unitags/DeleteUnitagModal.tsx index 66d1f4701ed..a97976512b8 100644 --- a/apps/mobile/src/components/unitags/DeleteUnitagModal.tsx +++ b/apps/mobile/src/components/unitags/DeleteUnitagModal.tsx @@ -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({ - {t('Are you sure?')} + {t('unitags.delete.confirm.title')} - {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')} diff --git a/apps/mobile/src/components/unitags/UnitagBanner.tsx b/apps/mobile/src/components/unitags/UnitagBanner.tsx index 4332f67baf0..2de09a79148 100644 --- a/apps/mobile/src/components/unitags/UnitagBanner.tsx +++ b/apps/mobile/src/components/unitags/UnitagBanner.tsx @@ -1,5 +1,5 @@ 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}> - - {t('Claim your {{unitagSuffix}} username', { - unitagSuffix: UNITAG_SUFFIX_NO_LEADING_DOT, - })} - - {t(' and build out your customizable profile.')} + + + Claim your {{ unitagDomain: UNITAG_SUFFIX_NO_LEADING_DOT }} username + + and build out your customizable profile. + ) : ( - {t('Claim your {{unitagSuffix}} username', { - unitagSuffix: UNITAG_SUFFIX_NO_LEADING_DOT, + {t('unitags.banner.title.full', { + unitagDomain: UNITAG_SUFFIX_NO_LEADING_DOT, })} - {t('Build a personalized web3 profile and easily share your address with friends.')} + {t('unitags.banner.subtitle')} @@ -140,7 +140,7 @@ export function UnitagBanner({ testID={ElementName.Confirm} onPress={onPressClaimNow}> - {t('Claim now')} + {t('unitags.banner.button.claim')} - {t('Maybe later')} + {t('common.button.later')} diff --git a/apps/mobile/src/components/unitags/UnitagsIntroModal.tsx b/apps/mobile/src/components/unitags/UnitagsIntroModal.tsx index 1f021195206..bd866d41a14 100644 --- a/apps/mobile/src/components/unitags/UnitagsIntroModal.tsx +++ b/apps/mobile/src/components/unitags/UnitagsIntroModal.tsx @@ -51,11 +51,9 @@ export function UnitagsIntroModal(): JSX.Element { - {t('Introducing usernames')} + {t('unitags.intro.title')} - {t( - 'Say goodbye to 0x addresses. Usernames are readable names that make it easier to send and receive crypto.' - )} + {t('unitags.intro.subtitle')} @@ -66,13 +64,13 @@ export function UnitagsIntroModal(): JSX.Element { /> - - - + + + diff --git a/apps/mobile/src/features/CloudBackup/CloudBackupPasswordForm.tsx b/apps/mobile/src/features/CloudBackup/CloudBackupPasswordForm.tsx index 9c00441f16e..bd0d1c014ee 100644 --- a/apps/mobile/src/features/CloudBackup/CloudBackupPasswordForm.tsx +++ b/apps/mobile/src/features/CloudBackup/CloudBackupPasswordForm.tsx @@ -1,5 +1,5 @@ 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({ { @@ -120,29 +124,42 @@ export function CloudBackupPasswordForm({ - {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')} )} ) } 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 ( - This is a {text.toLowerCase()} password + {strengthText} ) diff --git a/apps/mobile/src/features/CloudBackup/CloudBackupProcessingAnimation.tsx b/apps/mobile/src/features/CloudBackup/CloudBackupProcessingAnimation.tsx index 46932637f6b..63071ad2c58 100644 --- a/apps/mobile/src/features/CloudBackup/CloudBackupProcessingAnimation.tsx +++ b/apps/mobile/src/features/CloudBackup/CloudBackupProcessingAnimation.tsx @@ -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({ - {isAndroid ? t('Backing up to Google Drive...') : t('Backing up to iCloud...')} + {t('settings.setting.backup.status.inProgress', { + cloudProviderName: getCloudProviderName(), + })} ) : ( @@ -131,7 +129,9 @@ export function CloudBackupProcessingAnimation({ size={iconSize} /> - {isAndroid ? t('Backed up to Google Drive') : t('Backed up to iCloud')} + {t('settings.setting.backup.status.complete', { + cloudProviderName: getCloudProviderName(), + })} ) diff --git a/apps/mobile/src/features/appRating/selectors.test.ts b/apps/mobile/src/features/appRating/selectors.test.ts index 63492207d3c..ce8639ad7b7 100644 --- a/apps/mobile/src/features/appRating/selectors.test.ts +++ b/apps/mobile/src/features/appRating/selectors.test.ts @@ -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, }, diff --git a/apps/mobile/src/features/balances/hooks.ts b/apps/mobile/src/features/balances/hooks.ts index 3c2f4e084fd..8a85a0f1a56 100644 --- a/apps/mobile/src/features/balances/hooks.ts +++ b/apps/mobile/src/features/balances/hooks.ts @@ -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, diff --git a/apps/mobile/src/features/biometrics/index.ts b/apps/mobile/src/features/biometrics/index.ts index 4f32462bf88..66132c6d40c 100644 --- a/apps/mobile/src/features/biometrics/index.ts +++ b/apps/mobile/src/features/biometrics/index.ts @@ -47,8 +47,8 @@ export async function tryLocalAuthenticate(): Promise { 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, - resolvers: { - Query: { - portfolios: () => [Portfolio], + 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) }) }) }) diff --git a/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.test.ts b/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.test.ts index 3a32e58ef68..b9326177358 100644 --- a/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.test.ts +++ b/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.test.ts @@ -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}¤cyField=INPUT` const transactionUrl = `https://uniswap.org/app?screen=transaction&userAddress=${account.address}` const swapDeepLinkPayload = { url: swapUrl, coldStart: false } diff --git a/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.ts b/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.ts index bb372a3a114..827fd08d424 100644 --- a/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.ts +++ b/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.ts @@ -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') ) } } diff --git a/apps/mobile/src/features/deepLinking/handleSwapLinkSaga.test.ts b/apps/mobile/src/features/deepLinking/handleSwapLinkSaga.test.ts index f4e45787ebd..1da77942c10 100644 --- a/apps/mobile/src/features/deepLinking/handleSwapLinkSaga.test.ts +++ b/apps/mobile/src/features/deepLinking/handleSwapLinkSaga.test.ts @@ -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, diff --git a/apps/mobile/src/features/explore/utils.ts b/apps/mobile/src/features/explore/utils.ts index 7f903e968c5..fe566c6254a 100644 --- a/apps/mobile/src/features/explore/utils.ts +++ b/apps/mobile/src/features/explore/utils.ts @@ -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) } diff --git a/apps/mobile/src/features/externalProfile/ProfileContextMenu.tsx b/apps/mobile/src/features/externalProfile/ProfileContextMenu.tsx index 3c9ab5a2d36..2506a106165 100644 --- a/apps/mobile/src/features/externalProfile/ProfileContextMenu.tsx +++ b/apps/mobile/src/features/externalProfile/ProfileContextMenu.tsx @@ -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', }) diff --git a/apps/mobile/src/features/externalProfile/ProfileHeader.tsx b/apps/mobile/src/features/externalProfile/ProfileHeader.tsx index 5b3964e51c2..c30b5bbef0d 100644 --- a/apps/mobile/src/features/externalProfile/ProfileHeader.tsx +++ b/apps/mobile/src/features/externalProfile/ProfileHeader.tsx @@ -283,7 +283,7 @@ export const ProfileHeader = memo(function ProfileHeader({ color="$neutral2" maxFontSizeMultiplier={1.2} variant="buttonLabel2"> - {t('Send')} + {t('common.button.send')} diff --git a/apps/mobile/src/features/fiatOnRamp/FiatOnRampAmountSection.tsx b/apps/mobile/src/features/fiatOnRamp/FiatOnRampAmountSection.tsx index 8c129544d0e..e1b2cdfc5c3 100644 --- a/apps/mobile/src/features/fiatOnRamp/FiatOnRampAmountSection.tsx +++ b/apps/mobile/src/features/fiatOnRamp/FiatOnRampAmountSection.tsx @@ -200,7 +200,7 @@ export function FiatOnRampAmountSection({ {!appFiatCurrencySupported ? ( - {t('Only available to purchase in USD')} + {t('fiatOnRamp.error.usd')} ) : null} diff --git a/apps/mobile/src/features/fiatOnRamp/FiatOnRampBanner.tsx b/apps/mobile/src/features/fiatOnRamp/FiatOnRampBanner.tsx index 753269546b1..a906c5177c6 100644 --- a/apps/mobile/src/features/fiatOnRamp/FiatOnRampBanner.tsx +++ b/apps/mobile/src/features/fiatOnRamp/FiatOnRampBanner.tsx @@ -32,12 +32,12 @@ export function FiatOnRampBanner(props: TouchableAreaProps): JSX.Element { - {t('Buy crypto')} + {t('fiatOnRamp.banner.title')} - {t('Get tokens at the best prices in web3 with Uniswap Wallet.')} + {t('fiatOnRamp.banner.subtitle')} diff --git a/apps/mobile/src/features/fiatOnRamp/FiatOnRampConnecting.tsx b/apps/mobile/src/features/fiatOnRamp/FiatOnRampConnecting.tsx index 76d9e58a7eb..d47b6365611 100644 --- a/apps/mobile/src/features/fiatOnRamp/FiatOnRampConnecting.tsx +++ b/apps/mobile/src/features/fiatOnRamp/FiatOnRampConnecting.tsx @@ -47,13 +47,13 @@ export function FiatOnRampConnectingView({ - {t('Connecting you to {{serviceProvider}}', { serviceProvider: serviceProviderName })} + {t('fiatOnRamp.connection.message', { serviceProvider: serviceProviderName })} {quoteCurrencyCode && amount && ( - {t('Buying {{amount}} worth of {{quoteCurrencyCode}}', { + {t('fiatOnRamp.connection.quote', { amount, - quoteCurrencyCode, + currencySymbol: quoteCurrencyCode, })} )} diff --git a/apps/mobile/src/features/fiatOnRamp/FiatOnRampCountryListModal.tsx b/apps/mobile/src/features/fiatOnRamp/FiatOnRampCountryListModal.tsx index 05ce4dd58d2..53b32486652 100644 --- a/apps/mobile/src/features/fiatOnRamp/FiatOnRampCountryListModal.tsx +++ b/apps/mobile/src/features/fiatOnRamp/FiatOnRampCountryListModal.tsx @@ -97,11 +97,11 @@ function CountrySelectorContent({ return ( - {t('Select your region')} + {t('fiatOnRamp.region.title')} void }): JSX.Element { px="$spacing24" width="100%"> - {t('Buy')} + {t('common.button.buy')} void }): JSX.Element { - {t('Choose a token')} + {t('fiatOnRamp.button.chooseToken')} { it('renders a placeholder when there is no value', async () => { diff --git a/apps/mobile/src/features/nfts/collection/NFTCollectionContextMenu.tsx b/apps/mobile/src/features/nfts/collection/NFTCollectionContextMenu.tsx index aa9c2baa4bc..bf117726596 100644 --- a/apps/mobile/src/features/nfts/collection/NFTCollectionContextMenu.tsx +++ b/apps/mobile/src/features/nfts/collection/NFTCollectionContextMenu.tsx @@ -77,13 +77,13 @@ export function NFTCollectionContextMenu({ : undefined, homepageUrl ? { - title: t('Collection website'), + title: t('tokens.nfts.link.collection'), action: openExplorerLink, } : undefined, shareURL ? { - title: t('Share'), + title: t('common.button.share'), action: onSharePress, } : undefined, diff --git a/apps/mobile/src/features/nfts/collection/NFTCollectionHeader.tsx b/apps/mobile/src/features/nfts/collection/NFTCollectionHeader.tsx index 1ebe33b8829..7c5ba1613de 100644 --- a/apps/mobile/src/features/nfts/collection/NFTCollectionHeader.tsx +++ b/apps/mobile/src/features/nfts/collection/NFTCollectionHeader.tsx @@ -153,7 +153,7 @@ export function NFTCollectionHeader({ - {t('Items')} + {t('tokens.nfts.collection.label.items')} {formatNumberOrString({ @@ -164,7 +164,7 @@ export function NFTCollectionHeader({ - {t('Owners')} + {t('tokens.nfts.collection.label.owners')} {formatNumberOrString({ @@ -175,7 +175,7 @@ export function NFTCollectionHeader({ - {t('Floor')} + {t('tokens.nfts.collection.label.priceFloor')} @@ -191,7 +191,7 @@ export function NFTCollectionHeader({ - {t('Volume')} + {t('tokens.nfts.collection.label.swapVolume')} diff --git a/apps/mobile/src/features/nfts/hooks.ts b/apps/mobile/src/features/nfts/hooks.ts index 7a812da67cd..42a0004b10e 100644 --- a/apps/mobile/src/features/nfts/hooks.ts +++ b/apps/mobile/src/features/nfts/hooks.ts @@ -94,13 +94,15 @@ export function useNFTMenu({ isAddressAndTokenOk ? [ { - title: t('Share'), + title: t('common.button.share'), systemIcon: 'square.and.arrow.up', onPress: onPressShare, }, ...((isLocalAccount && [ { - title: hidden ? t('Unhide NFT') : t('Hide NFT'), + title: hidden + ? t('tokens.nfts.hidden.action.unhide') + : t('tokens.nfts.hidden.action.hide'), systemIcon: hidden ? 'eye' : 'eye.slash', destructive: !hidden, onPress: onPressHiddenStatus, diff --git a/apps/mobile/src/features/nfts/item/CollectionPreviewCard.test.tsx b/apps/mobile/src/features/nfts/item/CollectionPreviewCard.test.tsx index 82bcee46ace..1187d74ded2 100644 --- a/apps/mobile/src/features/nfts/item/CollectionPreviewCard.test.tsx +++ b/apps/mobile/src/features/nfts/item/CollectionPreviewCard.test.tsx @@ -1,15 +1,11 @@ import React from 'react' import { CollectionPreviewCard } from 'src/features/nfts/item/CollectionPreviewCard' import { render } from 'src/test/test-utils' -import { TopNFTCollections } from 'wallet/src/test/gqlFixtures' +import { NFT_COLLECTION } from 'wallet/src/test/fixtures' it('renders collection preview card', () => { const tree = render( - null} - /> + null} /> ) expect(tree).toMatchSnapshot() }) diff --git a/apps/mobile/src/features/nfts/item/CollectionPreviewCard.tsx b/apps/mobile/src/features/nfts/item/CollectionPreviewCard.tsx index 3c25124950a..9af8a1a5efc 100644 --- a/apps/mobile/src/features/nfts/item/CollectionPreviewCard.tsx +++ b/apps/mobile/src/features/nfts/item/CollectionPreviewCard.tsx @@ -75,7 +75,7 @@ export function CollectionPreviewCard({ {collection?.markets?.[0]?.floorPrice && ( - {t('Floor')}: + {t('tokens.nfts.collection.label.priceFloor')}: { - const tree = render() + const tree = render() expect(tree).toMatchSnapshot() }) diff --git a/apps/mobile/src/features/notifications/ScantasticCompleteNotification.tsx b/apps/mobile/src/features/notifications/ScantasticCompleteNotification.tsx index 83c695bf2ba..2f882dff413 100644 --- a/apps/mobile/src/features/notifications/ScantasticCompleteNotification.tsx +++ b/apps/mobile/src/features/notifications/ScantasticCompleteNotification.tsx @@ -29,8 +29,8 @@ export function ScantasticCompleteNotification({ } - subtitle={t('Continue on Uniswap Extension')} - title={t('Success')} + subtitle={t('notifications.scantastic.subtitle')} + title={t('notifications.scantastic.title')} /> ) } diff --git a/apps/mobile/src/features/scantastic/ScantasticModal.tsx b/apps/mobile/src/features/scantastic/ScantasticModal.tsx index 8708128b8c9..cb47cc57848 100644 --- a/apps/mobile/src/features/scantastic/ScantasticModal.tsx +++ b/apps/mobile/src/features/scantastic/ScantasticModal.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useState } from 'react' -import { Trans, useTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks' import { closeAllModals } from 'src/features/modals/modalSlice' @@ -7,7 +7,6 @@ import { selectModalState } from 'src/features/modals/selectModalState' import { Button, Flex, Icons, Text, TouchableArea, useSporeColors } from 'ui/src' import { iconSizes } from 'ui/src/theme' import { logger } from 'utilities/src/logger/logger' -import { getDurationRemainingString } from 'utilities/src/time/duration' import { ONE_MINUTE_MS, ONE_SECOND_MS } from 'utilities/src/time/time' import { useInterval } from 'utilities/src/time/timing' import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' @@ -16,6 +15,7 @@ import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' import { useActiveAccount } from 'wallet/src/features/wallet/hooks' import { ModalName } from 'wallet/src/telemetry/constants' +import { getOtpDurationString } from 'wallet/src/utils/duration' import { getEncryptedMnemonic } from './ScantasticEncryption' enum OtpState { @@ -52,16 +52,9 @@ export function ScantasticModal(): JSX.Element | null { const [expiryText, setExpiryText] = useState('') const setExpirationText = useCallback(() => { - const timeLeft = expirationTimestamp - Date.now() - if (timeLeft <= 0) { - return setExpiryText(t('Expired')) - } - return setExpiryText( - t('Expires in {{duration}}', { - duration: getDurationRemainingString(expirationTimestamp), - }) - ) - }, [expirationTimestamp, t]) + const expirationString = getOtpDurationString(expirationTimestamp) + setExpiryText(expirationString) + }, [expirationTimestamp]) useInterval(setExpirationText, ONE_SECOND_MS) if (redeemed) { @@ -93,11 +86,11 @@ export function ScantasticModal(): JSX.Element | null { const { n, e } = pubKey try { if (!n || !e) { - throw new Error(t('Invalid public key.')) + throw new Error('Invalid public key.') } encryptedSeedphrase = await getEncryptedMnemonic(account?.address || '', n, e) } catch (err) { - setError('Failed to prepare seed phrase.') + setError(t('scantastic.error.encryption')) logger.error(err, { tags: { file: 'ScantasticModal', @@ -136,7 +129,7 @@ export function ScantasticModal(): JSX.Element | null { setOTP(data.otp) } } catch (err) { - setError(t('No OTP received. Please try again.')) + setError(t('scantastic.error.noCode')) logger.error(err, { tags: { file: 'ScantasticModal', @@ -216,12 +209,12 @@ export function ScantasticModal(): JSX.Element | null { - {t('Your connection timed out')} + {t('scantastic.error.timeout.title')} - {t('Scan the QR code on the Uniswap Extension again to continue syncing your wallet.')} + {t('scantastic.error.timeout.message')} @@ -238,12 +231,9 @@ export function ScantasticModal(): JSX.Element | null { - {t('Uniswap one-time code')} + {t('scantastic.code.title')} - - Enter this code in the Uniswap Extension. Your recovery phrase will be safely - encrypted and transferred. - + {t('scantastic.code.subtitle')} {OTP.substring(0, 3).split('').join(' ')} @@ -267,17 +257,13 @@ export function ScantasticModal(): JSX.Element | null { - - Error - + {t('common.text.error')} {error} @@ -294,9 +280,9 @@ export function ScantasticModal(): JSX.Element | null { - {t('Is this your device?')} + {t('scantastic.confirmation.title')} - {t('Only continue if you are syncing with the Uniswap Extension on a trusted device.')} + {t('scantastic.confirmation.subtitle')} - {t('Device')} + {t('scantastic.confirmation.label.device')} {device} @@ -316,7 +302,7 @@ export function ScantasticModal(): JSX.Element | null { {browser && ( - {t('Browser')} + {t('scantastic.confirmation.label.browser')} {browser} @@ -328,11 +314,11 @@ export function ScantasticModal(): JSX.Element | null { mb="$spacing4" theme="primary" onPress={onConfirmSync}> - {t('Yes, continue')} + {t('scantastic.confirmation.button.continue')} - {t('Cancel')} + {t('common.button.cancel')} diff --git a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/FiatPurchaseSummaryItem.stories.tsx b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/FiatPurchaseSummaryItem.stories.tsx index 96276715e12..4fd34a1e747 100644 --- a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/FiatPurchaseSummaryItem.stories.tsx +++ b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/FiatPurchaseSummaryItem.stories.tsx @@ -102,7 +102,7 @@ export const FiatPurchase: StoryObj = { layoutElement={TransactionSummaryLayout} transaction={{ ...baseFaitPurchaseTx, - status: TransactionStatus.Cancelled, + status: TransactionStatus.Canceled, }} /> - {t('Try again')} + {t('common.button.tryAgain')} ) : null} @@ -63,11 +63,11 @@ export function TransactionPending({ testID="transaction-pending-view" theme="tertiary" onPress={onPressViewTransaction}> - {t('View transaction')} + {t('swap.button.view')} ) : null} diff --git a/apps/mobile/src/features/transactions/transfer/TransferFlow.tsx b/apps/mobile/src/features/transactions/transfer/TransferFlow.tsx index e12a5da1b6e..36ed8eb4a76 100644 --- a/apps/mobile/src/features/transactions/transfer/TransferFlow.tsx +++ b/apps/mobile/src/features/transactions/transfer/TransferFlow.tsx @@ -143,7 +143,7 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS {step !== TransactionStep.SUBMITTED && ( setShowViewOnlyModal(false)} onConfirm={(): void => setShowViewOnlyModal(false)} /> diff --git a/apps/mobile/src/features/transactions/transfer/TransferHeader.tsx b/apps/mobile/src/features/transactions/transfer/TransferHeader.tsx index 09dbe113fcd..8e1c46bf986 100644 --- a/apps/mobile/src/features/transactions/transfer/TransferHeader.tsx +++ b/apps/mobile/src/features/transactions/transfer/TransferHeader.tsx @@ -78,7 +78,7 @@ export function TransferHeader({ width={iconSizes.icon16} /> - {t('View-only')} + {t('swap.header.viewOnly')} diff --git a/apps/mobile/src/features/transactions/transfer/TransferStatus.tsx b/apps/mobile/src/features/transactions/transfer/TransferStatus.tsx index 62fb6f4bc23..237d37caacc 100644 --- a/apps/mobile/src/features/transactions/transfer/TransferStatus.tsx +++ b/apps/mobile/src/features/transactions/transfer/TransferStatus.tsx @@ -47,8 +47,8 @@ const getTextFromTransferStatus = ( // TODO: [MOB-240] should never go into this state but should probably do some // error display here as well as log to sentry or amplitude return { - title: t('Sending'), - description: t('We’ll notify you once your transaction is complete.'), + title: t('send.status.inProgress.title'), + description: t('send.status.inProgress.description'), } } const status = transactionDetails.status @@ -59,35 +59,32 @@ const getTextFromTransferStatus = ( currencySymbol: fiatCurrencyInfo.symbol, }) return { - title: t('Send successful!'), - description: t( - 'You sent {{ currencyAmount }}{{ tokenName }}{{ fiatValue }} to {{ recipient }}.', - { - currencyAmount: nftIn - ? '' - : formatter.formatCurrencyAmount({ - value: currencyAmounts[CurrencyField.INPUT], - type: NumberType.TokenTx, - }), - fiatValue: isFiatInput ? ` (${formattedFiatValue})` : '', - tokenName: nftIn?.name ?? ` ${currencyInInfo?.currency.symbol}` ?? ' tokens', - recipient, - } - ), + title: t('send.status.success.title'), + description: t('send.status.success.description', { + currencyAmount: nftIn + ? '' + : formatter.formatCurrencyAmount({ + value: currencyAmounts[CurrencyField.INPUT], + type: NumberType.TokenTx, + }), + fiatValue: isFiatInput ? ` (${formattedFiatValue})` : '', + tokenName: nftIn?.name ?? ` ${currencyInInfo?.currency.symbol}` ?? ' tokens', + recipient, + }), } } if (status === TransactionStatus.Failed) { return { - title: t('Send failed'), - description: t('Keep in mind that the network fee is still charged for failed transfers.'), + title: t('send.status.failed.title'), + description: t('send.status.fail.description'), } } // TODO: [MOB-241] handle TransactionStatus.Unknown state return { - title: t('Sending'), - description: t('We’ll notify you once your transaction is complete.'), + title: t('send.status.inProgress.title'), + description: t('send.status.inProgress.description'), } } diff --git a/apps/mobile/src/features/unitags/ChooseProfilePictureScreen.tsx b/apps/mobile/src/features/unitags/ChooseProfilePictureScreen.tsx index 9815d14e15e..24bb0f58108 100644 --- a/apps/mobile/src/features/unitags/ChooseProfilePictureScreen.tsx +++ b/apps/mobile/src/features/unitags/ChooseProfilePictureScreen.tsx @@ -113,10 +113,8 @@ export function ChooseProfilePictureScreen({ return ( + subtitle={t('unitags.onboarding.profile.subtitle')} + title={t('unitags.onboarding.profile.title')}> @@ -154,7 +152,7 @@ export function ChooseProfilePictureScreen({ ) : ( - t('Continue') + t('common.button.continue') )} {showModal && ( diff --git a/apps/mobile/src/features/unitags/ClaimUnitagScreen.tsx b/apps/mobile/src/features/unitags/ClaimUnitagScreen.tsx index 605716d243e..9160112dd42 100644 --- a/apps/mobile/src/features/unitags/ClaimUnitagScreen.tsx +++ b/apps/mobile/src/features/unitags/ClaimUnitagScreen.tsx @@ -1,7 +1,7 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' import { ADDRESS_ZERO } from '@uniswap/v3-sdk' import { default as React, useCallback, useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' +import { Trans, useTranslation } from 'react-i18next' import { ActivityIndicator, Keyboard } from 'react-native' import { useAnimatedStyle, useSharedValue, withDelay, withTiming } from 'react-native-reanimated' import { navigate } from 'src/app/navigation/rootNavigation' @@ -67,7 +67,7 @@ export function ClaimUnitagScreen({ navigation, route }: Props): JSX.Element { const { t } = useTranslation() const colors = useSporeColors() - const inputPlaceholder = getYourNameString(t('yourname')) + const inputPlaceholder = getYourNameString(t('unitags.claim.username.default')) // In onboarding flow, delete pending accounts and create account actions happen right before navigation // So pendingAccountAddress must be fetched in this component and can't be passed in params @@ -249,12 +249,13 @@ export function ClaimUnitagScreen({ navigation, route }: Props): JSX.Element { setShowClaimPeriodInfoModal(true) } - const title = entryPoint === Screens.Home ? t('Claim your username') : t('Choose your username') + const title = + entryPoint === Screens.Home + ? t('unitags.onboarding.claim.title.claim') + : t('unitags.onboarding.claim.title.choose') return ( - + - {canClaimUnitagNameError} {requiresENSMatch && t('Learn more about our')}{' '} + {canClaimUnitagNameError}{' '} {requiresENSMatch && ( - - {t('claim period')} - + + Learn more about our + + claim period + + . + )} - {requiresENSMatch && '.'} )} @@ -360,7 +364,7 @@ export function ClaimUnitagScreen({ navigation, route }: Props): JSX.Element { - {t('Maybe later')} + {t('common.button.later')} @@ -375,7 +379,7 @@ export function ClaimUnitagScreen({ navigation, route }: Props): JSX.Element { ) : ( - t('Continue') + t('common.button.continue') )} @@ -401,16 +405,15 @@ const InfoModal = ({ }): JSX.Element => { const colors = useSporeColors() const { t } = useTranslation() - const usernamePlaceholder = getYourNameString(t('yourname')) + const usernamePlaceholder = getYourNameString(t('unitags.claim.username.default')) return ( } modalName={ModalName.TooltipContent} - title={t('A simplified address')} + title={t('unitags.onboarding.info.title')} onClose={onClose} /> ) @@ -463,11 +466,8 @@ const ClaimPeriodInfoModal = ({ return ( } modalName={ModalName.ENSClaimPeriod} - title={t('ENS claim period')} + title={t('unitags.onboarding.claimPeriod.title')} onClose={onClose}> diff --git a/apps/mobile/src/features/unitags/EditUnitagProfileScreen.tsx b/apps/mobile/src/features/unitags/EditUnitagProfileScreen.tsx index a70f01a57b4..160ea3198cd 100644 --- a/apps/mobile/src/features/unitags/EditUnitagProfileScreen.tsx +++ b/apps/mobile/src/features/unitags/EditUnitagProfileScreen.tsx @@ -239,7 +239,7 @@ export function EditUnitagProfileScreen({ dispatch( pushNotification({ type: AppNotificationType.Success, - title: t('Profile updated'), + title: t('unitags.notification.profile.title'), }) ) triggerRefetchUnitags() @@ -258,15 +258,15 @@ export function EditUnitagProfileScreen({ dispatch( pushNotification({ type: AppNotificationType.Error, - errorMessage: t('Could not update profile. Try again later.'), + errorMessage: t('unitags.notification.profile.error'), }) ) } const menuActions = useMemo(() => { return [ - { title: t('Edit username'), systemIcon: 'pencil' }, - { title: t('Delete username'), systemIcon: 'trash', destructive: true }, + { title: t('unitags.profile.action.edit'), systemIcon: 'pencil' }, + { title: t('unitags.profile.action.delete'), systemIcon: 'trash', destructive: true }, ] }, [t]) @@ -311,7 +311,7 @@ export function EditUnitagProfileScreen({ ? (): void => navigate(Screens.Home) : undefined }> - {t('Edit profile')} + {t('settings.setting.wallet.action.editProfile')} - {t('Bio')} + {t('unitags.profile.bio.label')} {!loading ? ( - {t('Twitter')} + {t('unitags.profile.links.twitter')} {!loading ? ( @@ -427,7 +427,7 @@ export function EditUnitagProfileScreen({ fontFamily="$body" fontSize="$small" p="$none" - placeholder={t('username')} + placeholder={t('unitags.editProfile.placeholder')} placeholderTextColor="$neutral3" returnKeyType="done" textAlign="left" @@ -440,7 +440,7 @@ export function EditUnitagProfileScreen({ {ensName && ( - {t('ENS')} + ENS {ensName} @@ -458,7 +458,7 @@ export function EditUnitagProfileScreen({ size="medium" theme="primary" onPress={onPressSaveChanges}> - {t('Save')} + {t('common.button.save')} {showAvatarModal && ( , coordinates: { x: 9, y: 7 } }, { element: , coordinates: { x: 10, y: 10 } }, { element: , coordinates: { x: 1, y: 8.5 } }, - { element: , coordinates: { x: 0, y: 5 } }, + { + element: , + coordinates: { x: 0, y: 5 }, + }, { element: , coordinates: { x: 1, y: 2 } }, { element: , coordinates: { x: 3.5, y: 2.5 } }, ], @@ -115,21 +118,20 @@ export function UnitagConfirmationScreen({ - {t('You got it!')} + {t('unitags.claim.confirmation.success.long')} - {t( - '{{unitag}}{{unitagSuffix}} is ready to send and receive crypto. Continue to build out your wallet by customizing your web3 profile.', - { unitag, unitagSuffix: UNITAG_SUFFIX } - )} + {t('unitags.claim.confirmation.description', { + unitagAddress: `${unitag}${UNITAG_SUFFIX}`, + })} diff --git a/apps/mobile/src/features/wallet/saga.ts b/apps/mobile/src/features/wallet/saga.ts index c5321e72747..7a1148e76bc 100644 --- a/apps/mobile/src/features/wallet/saga.ts +++ b/apps/mobile/src/features/wallet/saga.ts @@ -18,7 +18,7 @@ function* onRestoreMnemonicComplete() { yield* put( pushNotification({ type: AppNotificationType.Success, - title: i18n.t('Wallet restored!'), + title: i18n.t('notification.restore.success'), }) ) yield* call(dispatchNavigationAction, StackActions.replace(Screens.Home)) diff --git a/apps/mobile/src/features/walletConnect/saga.ts b/apps/mobile/src/features/walletConnect/saga.ts index 2a745ba7320..ee1382ed076 100644 --- a/apps/mobile/src/features/walletConnect/saga.ts +++ b/apps/mobile/src/features/walletConnect/saga.ts @@ -202,14 +202,11 @@ function* handleSessionProposal(proposal: ProposalTypes.Struct) { const confirmed = yield* call( showAlert, - i18n.t('Connection Error'), - i18n.t( - `Uniswap Wallet currently supports {{ chains }}. Please only use "{{ dappName }}" on these chains`, - { - chains: chainLabels, - dappName: dapp.name, - } - ) + i18n.t('walletConnect.error.connection.title'), + i18n.t('walletConnect.error.connection.message', { + chainsNames: chainLabels, + dappName: dapp.name, + }) ) if (confirmed) { yield* put(setHasPendingSessionError(false)) diff --git a/apps/mobile/src/screens/ExchangeTransferConnecting.tsx b/apps/mobile/src/screens/ExchangeTransferConnecting.tsx index dbe184a6cb2..7f1702e9b8c 100644 --- a/apps/mobile/src/screens/ExchangeTransferConnecting.tsx +++ b/apps/mobile/src/screens/ExchangeTransferConnecting.tsx @@ -52,7 +52,7 @@ export function ExchangeTransferConnecting({ dispatch( pushNotification({ type: AppNotificationType.Error, - errorMessage: t('Something went wrong.'), + errorMessage: t('common.error.general'), }) ) onClose() diff --git a/apps/mobile/src/screens/ExploreScreen.tsx b/apps/mobile/src/screens/ExploreScreen.tsx index 634452f7878..77f66af384e 100644 --- a/apps/mobile/src/screens/ExploreScreen.tsx +++ b/apps/mobile/src/screens/ExploreScreen.tsx @@ -81,7 +81,7 @@ export function ExploreScreen(): JSX.Element { [ - { key: SectionName.ProfileTokensTab, title: t('Tokens') }, - { key: SectionName.ProfileNftsTab, title: t('NFTs') }, - { key: SectionName.ProfileActivityTab, title: t('Activity') }, + { key: SectionName.ProfileTokensTab, title: t('home.tokens.title') }, + { key: SectionName.ProfileNftsTab, title: t('home.nfts.title') }, + { key: SectionName.ProfileActivityTab, title: t('home.activity.title') }, ], [t] ) diff --git a/apps/mobile/src/screens/FiatOnRampConnecting.tsx b/apps/mobile/src/screens/FiatOnRampConnecting.tsx index 4e8d8b674ea..7c2f6c023b6 100644 --- a/apps/mobile/src/screens/FiatOnRampConnecting.tsx +++ b/apps/mobile/src/screens/FiatOnRampConnecting.tsx @@ -62,7 +62,7 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element | dispatch( pushNotification({ type: AppNotificationType.Error, - errorMessage: t('Something went wrong.'), + errorMessage: t('common.error.general'), }) ) navigation.goBack() diff --git a/apps/mobile/src/screens/FiatOnRampScreen.tsx b/apps/mobile/src/screens/FiatOnRampScreen.tsx index 6faadcc9762..a4c405d7032 100644 --- a/apps/mobile/src/screens/FiatOnRampScreen.tsx +++ b/apps/mobile/src/screens/FiatOnRampScreen.tsx @@ -261,7 +261,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { px="$spacing24" width="100%"> - {t('Buy')} + {t('common.button.buy')} { @@ -312,7 +312,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { )} diff --git a/apps/mobile/src/screens/FiatOnRampServiceProviders.tsx b/apps/mobile/src/screens/FiatOnRampServiceProviders.tsx index 7f530477cb9..b8c5ad8c2c9 100644 --- a/apps/mobile/src/screens/FiatOnRampServiceProviders.tsx +++ b/apps/mobile/src/screens/FiatOnRampServiceProviders.tsx @@ -73,14 +73,14 @@ export function FiatOnRampServiceProvidersScreen({ navigation }: Props): JSX.Ele }): JSX.Element => ( {type === InitialQuoteSelection.Best ? ( - + ) : type === InitialQuoteSelection.MostRecent ? ( - + ) : ( - {t('Other options')} + {t('fiatOnRamp.quote.type.other')} @@ -108,7 +108,7 @@ export function FiatOnRampServiceProvidersScreen({ navigation }: Props): JSX.Ele px="$spacing16"> - {t('Checkout with')} + {t('fiatOnRamp.checkout.title')} @@ -134,7 +134,7 @@ export function FiatOnRampServiceProvidersScreen({ navigation }: Props): JSX.Ele element={ElementName.FiatOnRampWidgetButton} pressEvent={MobileEventName.FiatOnRampWidgetOpened}> diff --git a/apps/mobile/src/screens/HomeScreen.tsx b/apps/mobile/src/screens/HomeScreen.tsx index d5552e3388c..da348d39a70 100644 --- a/apps/mobile/src/screens/HomeScreen.tsx +++ b/apps/mobile/src/screens/HomeScreen.tsx @@ -76,7 +76,15 @@ import { setNotificationStatus } from 'wallet/src/features/notifications/slice' import { TokenBalanceListRow } from 'wallet/src/features/portfolio/TokenBalanceListContext' import { useCanActiveAddressClaimUnitag } from 'wallet/src/features/unitags/hooks' import { AccountType } from 'wallet/src/features/wallet/accounts/types' -import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' +import { + PendingAccountActions, + pendingAccountActions, +} from 'wallet/src/features/wallet/create/pendingAccountsSaga' +import { + useActiveAccountWithThrow, + useNonPendingSignerAccounts, +} from 'wallet/src/features/wallet/hooks' +import { selectFinishedOnboarding } from 'wallet/src/features/wallet/selectors' import { ElementName, ElementNameType, @@ -105,6 +113,15 @@ export function HomeScreen(props?: AppStackScreenProp): JSX.Elemen const isModalOpen = useAppSelector(selectSomeModalOpen) const isHomeScreenBlur = !isFocused || isModalOpen + // Ensure if a user is here and has completed onboarding, they have at least one non-pending signer account + const finishedOnboarding = useAppSelector(selectFinishedOnboarding) + const nonPendingSignerAccounts = useNonPendingSignerAccounts() + useEffect(() => { + if (finishedOnboarding && activeAccount.pending && nonPendingSignerAccounts.length === 0) { + dispatch(pendingAccountActions.trigger(PendingAccountActions.ActivateOneAndDelete)) + } + }, [activeAccount, dispatch, finishedOnboarding, nonPendingSignerAccounts.length]) + const hasSkippedUnitagPrompt = useAppSelector(selectHasSkippedUnitagPrompt) const showFeedTab = useFeatureFlag(FEATURE_FLAGS.FeedTab) @@ -123,10 +140,10 @@ export function HomeScreen(props?: AppStackScreenProp): JSX.Elemen const [tabIndex, setTabIndex] = useState(props?.route?.params?.tab ?? HomeScreenTabIndex.Tokens) // Necessary to declare these as direct dependencies due to race condition with initializing react-i18next and useMemo - const tokensTitle = t('Tokens') - const nftsTitle = t('NFTs') - const activityTitle = t('Activity') - const feedTitle = t('Feed') + const tokensTitle = t('home.tokens.title') + const nftsTitle = t('home.nfts.title') + const activityTitle = t('home.activity.title') + const feedTitle = t('home.feed.title') const routes = useMemo(() => { const tabs: Array<{ key: SectionNameType; title: string }> = [ @@ -329,10 +346,10 @@ export function HomeScreen(props?: AppStackScreenProp): JSX.Elemen // Hide actions when active account isn't a signer account. const isSignerAccount = activeAccount.type === AccountType.SignerMnemonic // Necessary to declare these as direct dependencies due to race condition with initializing react-i18next and useMemo - const buyLabel = t('Buy') - const sendLabel = t('Send') - const receiveLabel = t('Receive') - const scanLabel = t('Scan') + const buyLabel = t('home.label.buy') + const sendLabel = t('home.label.send') + const receiveLabel = t('home.label.receive') + const scanLabel = t('home.label.scan') const actions = useMemo( (): QuickAction[] => [ @@ -385,7 +402,7 @@ export function HomeScreen(props?: AppStackScreenProp): JSX.Elemen const shouldPromptUnitag = activeAccount.type === AccountType.SignerMnemonic && !hasSkippedUnitagPrompt && canClaimUnitag - const viewOnlyLabel = t('This is a view-only wallet') + const viewOnlyLabel = t('home.warning.viewOnly') const contentHeader = useMemo(() => { return ( diff --git a/apps/mobile/src/screens/Import/ImportMethodScreen.tsx b/apps/mobile/src/screens/Import/ImportMethodScreen.tsx index 85e650eb5b4..732048235de 100644 --- a/apps/mobile/src/screens/Import/ImportMethodScreen.tsx +++ b/apps/mobile/src/screens/Import/ImportMethodScreen.tsx @@ -35,19 +35,19 @@ interface ImportMethodOption { const options: ImportMethodOption[] = [ { - title: (t: AppTFunction) => t('Import a wallet'), - blurb: (t: AppTFunction) => t('Enter your recovery phrase from another crypto wallet'), + title: (t: AppTFunction) => t('onboarding.import.method.import.title'), + blurb: (t: AppTFunction) => t('onboarding.import.method.import.message'), icon: , nav: OnboardingScreens.SeedPhraseInput, importType: ImportType.SeedPhrase, name: ElementName.OnboardingImportSeedPhrase, }, { - title: (t: AppTFunction) => t('Restore a wallet'), + title: (t: AppTFunction) => t('onboarding.import.method.restore.title'), blurb: (t: AppTFunction) => isAndroid - ? t(`Add wallets you’ve backed up to your Google Drive account`) - : t(`Add wallets you’ve backed up to your iCloud account`), + ? t(`onboarding.import.method.restore.message.android`) + : t(`onboarding.import.method.restore.message.ios`), icon: , nav: OnboardingScreens.RestoreCloudBackup, importType: ImportType.Restore, @@ -71,17 +71,19 @@ export function ImportMethodScreen({ navigation, route: { params } }: Props): JS 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 @@ -116,7 +118,7 @@ export function ImportMethodScreen({ navigation, route: { params } }: Props): JS : options return ( - + => handleOnPress(OnboardingScreens.WatchWallet, ImportType.Watch) }> - {t('Watch a wallet')} + {t('account.wallet.button.watch')} diff --git a/apps/mobile/src/screens/Import/RestoreCloudBackupLoadingScreen.tsx b/apps/mobile/src/screens/Import/RestoreCloudBackupLoadingScreen.tsx index dab1aa7189b..38ef1b1bb96 100644 --- a/apps/mobile/src/screens/Import/RestoreCloudBackupLoadingScreen.tsx +++ b/apps/mobile/src/screens/Import/RestoreCloudBackupLoadingScreen.tsx @@ -20,7 +20,7 @@ import { ONE_SECOND_MS } from 'utilities/src/time/time' import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' import { ImportType } from 'wallet/src/features/onboarding/types' import { useNonPendingSignerAccounts } from 'wallet/src/features/wallet/hooks' -import { isAndroid } from 'wallet/src/utils/platform' +import { getCloudProviderName } from 'wallet/src/utils/platform' type Props = NativeStackScreenProps< OnboardingStackParamList, @@ -138,12 +138,10 @@ export function RestoreCloudBackupLoadingScreen({ return ( } - retryButtonLabel={t('Retry')} - title={t('Error while importing backups')} + retryButtonLabel={t('common.button.retry')} + title={t('account.cloud.error.backup.title')} onRetry={fetchCloudStorageBackups} /> @@ -161,14 +159,12 @@ export function RestoreCloudBackupLoadingScreen({ return ( } - retryButtonLabel={t('Retry')} - title={t('0 backups found')} + retryButtonLabel={t('common.button.retry')} + title={t('account.cloud.empty.title')} onRetry={fetchCloudStorageBackups} /> @@ -177,7 +173,7 @@ export function RestoreCloudBackupLoadingScreen({ } return ( - + ) diff --git a/apps/mobile/src/screens/Import/RestoreCloudBackupPasswordScreen.tsx b/apps/mobile/src/screens/Import/RestoreCloudBackupPasswordScreen.tsx index 8cf60d4c158..ff53f57f69a 100644 --- a/apps/mobile/src/screens/Import/RestoreCloudBackupPasswordScreen.tsx +++ b/apps/mobile/src/screens/Import/RestoreCloudBackupPasswordScreen.tsx @@ -19,13 +19,13 @@ import { PasswordError } from 'src/features/onboarding/PasswordError' import { OnboardingScreens } from 'src/screens/Screens' import { useAddBackButton } from 'src/utils/useAddBackButton' import { Button, Flex, Text, TouchableArea } from 'ui/src' -import { ONE_HOUR_MS, ONE_MINUTE_MS } from 'utilities/src/time/time' +import { MINUTES_IN_HOUR, ONE_HOUR_MS, ONE_MINUTE_MS } from 'utilities/src/time/time' import { ImportType } from 'wallet/src/features/onboarding/types' import { importAccountActions } from 'wallet/src/features/wallet/import/importAccountSaga' import { ImportAccountType } from 'wallet/src/features/wallet/import/types' import { NUMBER_OF_WALLETS_TO_IMPORT } from 'wallet/src/features/wallet/import/utils' import { ElementName } from 'wallet/src/telemetry/constants' -import { isAndroid } from 'wallet/src/utils/platform' +import { getCloudProviderName } from 'wallet/src/utils/platform' type Props = NativeStackScreenProps< OnboardingStackParamList, @@ -60,13 +60,14 @@ function calculateLockoutEndTime(attemptCount: number): number | undefined { return undefined } -function getLockoutTimeMessage(remainingLockoutTime: number): string { +function useLockoutTimeMessage(remainingLockoutTime: number): string { + const { t } = useTranslation() const minutes = Math.ceil(remainingLockoutTime / ONE_MINUTE_MS) - if (minutes >= 60) { - return '1 hour' + if (minutes >= MINUTES_IN_HOUR) { + return t('account.cloud.lockout.time.hours', { count: Math.floor(minutes / MINUTES_IN_HOUR) }) } - return minutes === 1 ? '1 minute' : `${minutes} minutes` + return t('account.cloud.lockout.time.minutes', { count: Math.floor(minutes) }) } export function RestoreCloudBackupPasswordScreen({ @@ -87,15 +88,12 @@ export function RestoreCloudBackupPasswordScreen({ const remainingLockoutTime = lockoutEndTime ? Math.max(0, lockoutEndTime - Date.now()) : 0 const isLockedOut = remainingLockoutTime > 0 + const lockoutMessage = useLockoutTimeMessage(remainingLockoutTime) useFocusEffect( useCallback(() => { if (isLockedOut) { - setErrorMessage( - t('Too many attempts. Try again in {{time}}.', { - time: getLockoutTimeMessage(remainingLockoutTime), - }) - ) + setErrorMessage(lockoutMessage) const timer = setTimeout(() => { setErrorMessage(undefined) @@ -105,7 +103,7 @@ export function RestoreCloudBackupPasswordScreen({ return () => clearTimeout(timer) } - }, [isLockedOut, t, dispatch, remainingLockoutTime]) + }, [isLockedOut, lockoutMessage, remainingLockoutTime, dispatch]) ) useAddBackButton(navigation) @@ -137,7 +135,7 @@ export function RestoreCloudBackupPasswordScreen({ if (updatedLockoutEndTime) { dispatch(setLockoutEndTime({ lockoutEndTime: updatedLockoutEndTime })) } else { - setErrorMessage(t('Invalid password. Please try again.')) + setErrorMessage(t('account.cloud.error.password.title')) inputRef.current?.focus() } } @@ -154,18 +152,14 @@ export function RestoreCloudBackupPasswordScreen({ return ( + subtitle={t('account.cloud.password.subtitle', { cloudProviderName: getCloudProviderName() })} + title={t('account.cloud.password.title')}> { if (!isLockedOut) { @@ -181,7 +175,7 @@ export function RestoreCloudBackupPasswordScreen({ {isRestoringMnemonic && ( - {t('Enter your recovery phrase instead')} + {t('account.cloud.password.recoveryPhrase')} )} @@ -189,7 +183,7 @@ export function RestoreCloudBackupPasswordScreen({ disabled={!enteredPassword || isLockedOut} testID={ElementName.Submit} onPress={onPasswordSubmit}> - {t('Continue')} + {t('common.button.continue')} diff --git a/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.tsx b/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.tsx index 7c9e29254b1..02d155f1bea 100644 --- a/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.tsx +++ b/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.tsx @@ -1,5 +1,4 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' -import dayjs from 'dayjs' import React from 'react' import { useTranslation } from 'react-i18next' import { ScrollView } from 'react-native-gesture-handler' @@ -14,12 +13,16 @@ import { Flex, Icons, Text, TouchableArea, Unicon, UniconV2, useIsDarkMode } fro import { iconSizes } from 'ui/src/theme' import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' +import { + FORMAT_DATE_TIME_SHORT, + useLocalizedDayjs, +} from 'wallet/src/features/language/localizedDayjs' import { PendingAccountActions, pendingAccountActions, } from 'wallet/src/features/wallet/create/pendingAccountsSaga' import { sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses' -import { isAndroid } from 'wallet/src/utils/platform' +import { getCloudProviderName } from 'wallet/src/utils/platform' type Props = NativeStackScreenProps @@ -27,6 +30,8 @@ export function RestoreCloudBackupScreen({ navigation, route: { params } }: Prop const { t } = useTranslation() const dispatch = useAppDispatch() const isDarkMode = useIsDarkMode() + const localizedDayjs = useLocalizedDayjs() + // const backups = useMockCloudBackups(4) // returns 4 mock backups with random mnemonicIds and createdAt dates const backups = useCloudBackups() const sortedBackups = backups.slice().sort((a, b) => b.createdAt - a.createdAt) @@ -47,12 +52,8 @@ export function RestoreCloudBackupScreen({ navigation, route: { params } }: Prop return ( + subtitle={t('account.cloud.backup.subtitle', { cloudProviderName: getCloudProviderName() })} + title={t('account.cloud.backup.title')}> {sortedBackups.map((backup) => { @@ -80,7 +81,7 @@ export function RestoreCloudBackupScreen({ navigation, route: { params } }: Prop {sanitizeAddressText(shortenAddress(mnemonicId))} - {dayjs.unix(createdAt).format('MMM D, YYYY [at] h:mma')} + {localizedDayjs.unix(createdAt).format(FORMAT_DATE_TIME_SHORT)} diff --git a/apps/mobile/src/screens/Import/SeedPhraseInputScreen.tsx b/apps/mobile/src/screens/Import/SeedPhraseInputScreen.tsx index e205db7c06b..03089e32906 100644 --- a/apps/mobile/src/screens/Import/SeedPhraseInputScreen.tsx +++ b/apps/mobile/src/screens/Import/SeedPhraseInputScreen.tsx @@ -66,7 +66,7 @@ export function SeedPhraseInputScreen({ navigation, route: { params } }: Props): if (mnemonicId && validMnemonic) { const generatedMnemonicId = await Keyring.generateAddressForMnemonic(validMnemonic, 0) if (generatedMnemonicId !== mnemonicId) { - setErrorMessage(t('Wrong recovery phrase')) + setErrorMessage(t('account.seedPhrase.error.wrong')) return } } @@ -119,10 +119,14 @@ export function SeedPhraseInputScreen({ navigation, route: { params } }: Props): + title={ + isRestoringMnemonic + ? t('account.seedPhrase.title.restoring') + : t('account.seedPhrase.title.import') + }> setPastePermissionModalOpen(true)} errorMessage={errorMessage} inputAlignment="flex-start" - placeholderLabel={t('Type your recovery phrase')} + placeholderLabel={t('account.seedPhrase.input')} textAlign="left" value={value} onBlur={onBlur} @@ -148,8 +152,8 @@ export function SeedPhraseInputScreen({ navigation, route: { params } }: Props): {isRestoringMnemonic - ? t('Try searching again') - : t('How do I find my recovery phrase?')} + ? t('account.seedPhrase.helpText.restoring') + : t('account.seedPhrase.helpText.import')} @@ -157,7 +161,7 @@ export function SeedPhraseInputScreen({ navigation, route: { params } }: Props): diff --git a/apps/mobile/src/screens/Import/SeedPhraseInputScreenV2.tsx b/apps/mobile/src/screens/Import/SeedPhraseInputScreenV2.tsx index f3f8c987653..8108155921e 100644 --- a/apps/mobile/src/screens/Import/SeedPhraseInputScreenV2.tsx +++ b/apps/mobile/src/screens/Import/SeedPhraseInputScreenV2.tsx @@ -80,23 +80,27 @@ export function SeedPhraseInputScreenV2({ navigation, route: { params } }: Props + title={ + isRestoringMnemonic + ? t('account.seedPhrase.title.restoring') + : t('account.seedPhrase.title.import') + }> {/* */} { seedPhraseInputRef.current?.handleSubmit() }}> - {t('Continue')} + {t('common.button.continue')} diff --git a/apps/mobile/src/screens/Import/SelectWalletScreen.tsx b/apps/mobile/src/screens/Import/SelectWalletScreen.tsx index a5c47eebb1e..b906ccd0efd 100644 --- a/apps/mobile/src/screens/Import/SelectWalletScreen.tsx +++ b/apps/mobile/src/screens/Import/SelectWalletScreen.tsx @@ -171,7 +171,7 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS editAccountActions.trigger({ type: EditAccountAction.Rename, address, - newName: t('Wallet {{ number }}', { number: account.derivationIndex + 1 }), + newName: t('onboarding.wallet.defaultName', { number: account.derivationIndex + 1 }), }) ) } @@ -195,12 +195,10 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS const isLoading = loading || isForcedLoading || isImportingAccounts const title = isLoading - ? t('Searching for wallets') - : isOnlyOneAccount - ? t('One wallet found') - : t('Select wallets to import') + ? t('account.wallet.select.loading.title') + : t('account.wallet.select.title_one', { count: initialShownAccounts?.length ?? 0 }) - const subtitle = isLoading ? t('Your wallets will appear below.') : undefined + const subtitle = isLoading ? t('account.wallet.select.loading.subtitle') : undefined return ( <> @@ -209,8 +207,8 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS title={!showError ? title : ''}> {showError ? ( ) : isLoading ? ( @@ -245,7 +243,7 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS } testID={ElementName.Next} onPress={onSubmit}> - {t('Continue')} + {t('common.button.continue')} diff --git a/apps/mobile/src/screens/Import/WatchWalletScreen.tsx b/apps/mobile/src/screens/Import/WatchWalletScreen.tsx index 0eaa2d62054..1cc4fb100c1 100644 --- a/apps/mobile/src/screens/Import/WatchWalletScreen.tsx +++ b/apps/mobile/src/screens/Import/WatchWalletScreen.tsx @@ -62,11 +62,11 @@ const getErrorText = ({ t: TFunction }): string | undefined => { if (walletExists) { - return t('This address is already imported') + return t('account.wallet.watch.error.alreadyImported') } else if (isSmartContractAddress) { - return t('Address is a smart contract') + return t('account.wallet.watch.error.smartContract') } else if (!loading) { - return t('Address not found') + return t('account.wallet.watch.error.notFound') } return undefined } @@ -165,7 +165,7 @@ export function WatchWalletScreen({ navigation, route: { params } }: Props): JSX }, [value]) return ( - + - {t( - 'Adding a view-only wallet allows you to try out the app or track a wallet. You will not be able to swap or send funds.' - )} + {t('account.wallet.watch.message')} ) diff --git a/apps/mobile/src/screens/NFTCollectionScreen.tsx b/apps/mobile/src/screens/NFTCollectionScreen.tsx index 0a36f106eb1..5831b1a9560 100644 --- a/apps/mobile/src/screens/NFTCollectionScreen.tsx +++ b/apps/mobile/src/screens/NFTCollectionScreen.tsx @@ -234,9 +234,9 @@ export function NFTCollectionScreen({ @@ -272,7 +272,9 @@ export function NFTCollectionScreen({ + gridDataLoading ? null : ( + + ) } ListHeaderComponent={ > => refetch?.() } @@ -315,7 +315,7 @@ function NFTItemScreenContents({ {listingPrice?.value ? ( 0 ? ( - {t('Traits')} + {t('tokens.nfts.details.traits')} diff --git a/apps/mobile/src/screens/Onboarding/BackupScreen.test.tsx b/apps/mobile/src/screens/Onboarding/BackupScreen.test.tsx index 27191c4e934..23c4f22cc66 100644 --- a/apps/mobile/src/screens/Onboarding/BackupScreen.test.tsx +++ b/apps/mobile/src/screens/Onboarding/BackupScreen.test.tsx @@ -10,7 +10,8 @@ import { renderWithProviders } from 'src/test/render' import { render } from 'src/test/test-utils' import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' import { TamaguiProvider } from 'wallet/src/provider/tamagui-provider' -import { mockWalletPreloadedState } from 'wallet/src/test/fixtures' +import { ACCOUNT } from 'wallet/src/test/fixtures' +import { mockWalletPreloadedState } from 'wallet/src/test/mocks' const navigationProp = {} as CompositeNavigationProp< StackNavigationProp, @@ -40,7 +41,7 @@ describe(BackupScreen, () => { , { - preloadedState: mockWalletPreloadedState, + preloadedState: mockWalletPreloadedState(ACCOUNT), } ) diff --git a/apps/mobile/src/screens/Onboarding/BackupScreen.tsx b/apps/mobile/src/screens/Onboarding/BackupScreen.tsx index 91e400a5990..4b5ebfd3723 100644 --- a/apps/mobile/src/screens/Onboarding/BackupScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/BackupScreen.tsx @@ -25,7 +25,7 @@ import { BackupType } from 'wallet/src/features/wallet/accounts/types' import { useActiveAccount } from 'wallet/src/features/wallet/hooks' import { ElementName } from 'wallet/src/telemetry/constants' import { openSettings } from 'wallet/src/utils/linking' -import { isAndroid } from 'wallet/src/utils/platform' +import { getCloudProviderName, isAndroid } from 'wallet/src/utils/platform' type Props = CompositeScreenProps< StackScreenProps, @@ -82,21 +82,19 @@ export function BackupScreen({ navigation, route: { params } }: Props): JSX.Elem const onPressCloudBackup = (): void => { 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'), + text: t('account.cloud.error.unavailable.button.settings'), onPress: openSettings, style: 'default', }, - { text: t('Not now'), style: 'cancel' }, + { text: t('account.cloud.error.unavailable.button.cancel'), style: 'cancel' }, ] ) return @@ -123,16 +121,20 @@ export function BackupScreen({ navigation, route: { params } }: Props): JSX.Elem const hasManualBackup = activeAccountBackups?.some((backup) => backup === BackupType.Manual) const isCreatingNew = params?.importType === ImportType.CreateNew - const screenTitle = isCreatingNew ? t('Choose a backup method') : t('Back up your wallet') + const screenTitle = isCreatingNew + ? t('onboarding.backup.title.new') + : t('onboarding.backup.title.existing') const options = [] options.push( } - title={isAndroid ? t('Google Drive backup') : t('iCloud backup')} + title={t('onboarding.backup.option.cloud.title', { + cloudProviderName: getCloudProviderName(), + })} onPress={onPressCloudBackup} /> ) @@ -140,20 +142,18 @@ export function BackupScreen({ navigation, route: { params } }: Props): JSX.Elem options.push( } - title={t('Manual backup')} + title={t('onboarding.backup.option.manual.title')} onPress={onPressManualBackup} /> ) } return ( - + )} @@ -200,7 +200,7 @@ function RecoveryPhraseTooltip({ onPress={onPressEducationButton}> - {t('What is a recovery phrase?')} + {t('onboarding.tooltip.recoveryPhrase.trigger')} ) diff --git a/apps/mobile/src/screens/Onboarding/CloudBackupPasswordConfirmScreen.tsx b/apps/mobile/src/screens/Onboarding/CloudBackupPasswordConfirmScreen.tsx index df1334717e1..cb59fcdc6ae 100644 --- a/apps/mobile/src/screens/Onboarding/CloudBackupPasswordConfirmScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/CloudBackupPasswordConfirmScreen.tsx @@ -28,10 +28,8 @@ export function CloudBackupPasswordConfirmScreen({ return ( + subtitle={t('onboarding.cloud.confirm.description')} + title={t('onboarding.cloud.confirm.title')}> + subtitle={t('onboarding.cloud.createPassword.description')} + title={t('onboarding.cloud.createPassword.title')}> ) diff --git a/apps/mobile/src/screens/Onboarding/EditNameScreen.tsx b/apps/mobile/src/screens/Onboarding/EditNameScreen.tsx index fda52e7a118..3bf88ad04f1 100644 --- a/apps/mobile/src/screens/Onboarding/EditNameScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/EditNameScreen.tsx @@ -1,6 +1,6 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' import React, { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react' -import { useTranslation } from 'react-i18next' +import { Trans, useTranslation } from 'react-i18next' import { ActivityIndicator, TextInput as NativeTextInput, StyleSheet } from 'react-native' import { useAppDispatch } from 'src/app/hooks' import { OnboardingStackParamList } from 'src/app/navigation/types' @@ -41,7 +41,7 @@ export function EditNameScreen({ navigation, route: { params } }: Props): JSX.El if (pendingAccount && pendingAccount.type !== AccountType.Readonly) { setNewAccountName( pendingAccount.name || - t('Wallet {{ number }}', { number: pendingAccount.derivationIndex + 1 }) || + t('onboarding.wallet.defaultName', { number: pendingAccount.derivationIndex + 1 }) || '' ) } @@ -83,8 +83,8 @@ export function EditNameScreen({ navigation, route: { params } }: Props): JSX.El return ( + subtitle={t('onboarding.editName.subtitle')} + title={t('onboarding.editName.title')}> {pendingAccount ? ( - + @@ -122,6 +122,7 @@ function CustomizationSection({ const focusInputWithKeyboard = (): void => { textInputRef.current?.focus() } + const walletAddress = shortenAddress(address) return ( - {t('Your public address will be')} - - - {shortenAddress(address)} + + Your public address will be + + {{ walletAddress }} + + diff --git a/apps/mobile/src/screens/Onboarding/LandingScreen.tsx b/apps/mobile/src/screens/Onboarding/LandingScreen.tsx index b037cfd720a..02332209918 100644 --- a/apps/mobile/src/screens/Onboarding/LandingScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/LandingScreen.tsx @@ -91,7 +91,7 @@ export function LandingScreen({ navigation }: Props): JSX.Element { $short={{ size: 'medium' }} size="large" onPress={onPressCreateWallet}> - {t('Create a new wallet')} + {t('onboarding.landing.button.create')} @@ -110,7 +110,7 @@ export function LandingScreen({ navigation }: Props): JSX.Element { $short={{ variant: 'buttonLabel2', fontSize: '$medium' }} color="$accent1" variant="buttonLabel1"> - {t('Add an existing wallet')} + {t('onboarding.landing.button.add')} diff --git a/apps/mobile/src/screens/Onboarding/ManualBackupScreen.tsx b/apps/mobile/src/screens/Onboarding/ManualBackupScreen.tsx index f8c8404de09..b5cd24a4657 100644 --- a/apps/mobile/src/screens/Onboarding/ManualBackupScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/ManualBackupScreen.tsx @@ -80,12 +80,6 @@ export function ManualBackupScreen({ navigation, route: { params } }: Props): JS } }, [continueButtonPressed, activeAccount?.backups, navigation, params]) - const responsiveTitle = media.short ? undefined : t('Confirm your recovery phrase') - - const responsiveSubtitle = media.short - ? t('Confirm your recovery phrase') + '. ' + t('Select the missing words in order.') - : t('Select the missing words in order.') - // Manually log as page views as these screens are not captured in navigation events useEffect(() => { switch (view) { @@ -105,16 +99,14 @@ export function ManualBackupScreen({ navigation, route: { params } }: Props): JS case View.SeedPhrase: return ( + subtitle={t('onboarding.seedPhrase.view.subtitle')} + title={t('onboarding.seedPhrase.view.title')}> {showScreenShotWarningModal && ( setShowScreenShotWarningModal(false)} /> )} @@ -128,7 +120,7 @@ export function ManualBackupScreen({ navigation, route: { params } }: Props): JS @@ -139,7 +131,13 @@ export function ManualBackupScreen({ navigation, route: { params } }: Props): JS ) case View.SeedPhraseConfirm: return ( - + - {t('Continue')} + {t('common.button.continue')} @@ -179,15 +177,13 @@ const SeedWarningModal = ({ onPress }: { onPress: () => void }): JSX.Element => /> - {t('Do this step in a private place')} + {t('onboarding.seedPhrase.warning.final.title')} - {t( - 'Your recovery phrase is what grants you (and anyone who has it) access to your funds. Be sure to keep it to yourself.' - )} + {t('onboarding.seedPhrase.warning.final.message')} diff --git a/apps/mobile/src/screens/Onboarding/NotificationsSetupScreen.tsx b/apps/mobile/src/screens/Onboarding/NotificationsSetupScreen.tsx index 4830c3cf7af..0c90ede9159 100644 --- a/apps/mobile/src/screens/Onboarding/NotificationsSetupScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/NotificationsSetupScreen.tsx @@ -30,14 +30,12 @@ type Props = NativeStackScreenProps { Alert.alert( - i18n.t('Notifications permission'), - i18n.t( - 'To receive notifications, turn on notifications for Uniswap Wallet in your device’s settings.' - ), + i18n.t('onboarding.notification.permission.title'), + i18n.t('onboarding.notification.permission.message'), [ - { text: i18n.t('Settings'), onPress: openSettings }, + { text: i18n.t('common.navigation.settings'), onPress: openSettings }, { - text: i18n.t('Cancel'), + text: i18n.t('common.button.cancel'), }, ] ) @@ -121,8 +119,8 @@ export function NotificationsSetupScreen({ navigation, route: { params } }: Prop return ( + subtitle={t('onboarding.notification.subtitle')} + title={t('onboarding.notification.title')}> @@ -130,13 +128,13 @@ export function NotificationsSetupScreen({ navigation, route: { params } }: Prop - {t('Maybe later')} + {t('common.button.later')} diff --git a/apps/mobile/src/screens/Onboarding/QRAnimation/QRAnimation.tsx b/apps/mobile/src/screens/Onboarding/QRAnimation/QRAnimation.tsx index b1e3018702a..ee0e4e663bc 100644 --- a/apps/mobile/src/screens/Onboarding/QRAnimation/QRAnimation.tsx +++ b/apps/mobile/src/screens/Onboarding/QRAnimation/QRAnimation.tsx @@ -295,7 +295,7 @@ export function QRAnimation({ pb="$spacing12" textAlign="center" variant="subheading1"> - {t('Welcome to your new wallet')} + {t('onboarding.wallet.title')} {isNewWallet - ? t('Your personal space for tokens, NFTs, and all your trades.') - : t( - 'Check out your tokens and NFTs, follow crypto wallets, and stay up to date on the go.' - )} + ? t('onboarding.wallet.description.new') + : t('onboarding.wallet.description.existing')} @@ -328,7 +326,7 @@ export function QRAnimation({ /> - {t('Let’s keep it safe')} + {t('onboarding.wallet.continue')} diff --git a/apps/mobile/src/screens/Onboarding/SecuritySetupScreen.tsx b/apps/mobile/src/screens/Onboarding/SecuritySetupScreen.tsx index ec7be471550..869804119f0 100644 --- a/apps/mobile/src/screens/Onboarding/SecuritySetupScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/SecuritySetupScreen.tsx @@ -43,7 +43,7 @@ export function SecuritySetupScreen({ route: { params } }: Props): JSX.Element { const [isLoadingAccount, setIsLoadingAccount] = useState(false) const [showWarningModal, setShowWarningModal] = useState(false) const { touchId: isTouchIdDevice } = useDeviceSupportsBiometricAuth() - const authenticationTypeName = useBiometricName(isTouchIdDevice) + const biometricsMethod = useBiometricName(isTouchIdDevice) const onCompleteOnboarding = useCompleteOnboardingCallback(params) @@ -66,27 +66,28 @@ export function SecuritySetupScreen({ route: { params } }: Props): JSX.Element { const onPressEnableSecurity = useCallback(async () => { const authStatus = await tryLocalAuthenticate() - const authTypeCapitalized = - authenticationTypeName.charAt(0).toUpperCase() + authenticationTypeName.slice(1) - if ( authStatus === BiometricAuthenticationStatus.Unsupported || authStatus === BiometricAuthenticationStatus.MissingEnrollment ) { isIOS ? Alert.alert( - t('{{authTypeCapitalized}} is disabled', { authTypeCapitalized }), - t('To use {{authenticationTypeName}}, allow access in system settings', { - authenticationTypeName, + t('onboarding.security.alert.biometrics.title.ios', { biometricsMethod }), + t('onboarding.security.alert.biometrics.message.ios', { + biometricsMethod, }), - [{ text: t('Go to settings'), onPress: openSettings }, { text: t('Not now') }] + [ + { text: t('common.navigation.systemSettings'), onPress: openSettings }, + { text: t('common.button.notNow') }, + ] ) : Alert.alert( - t('{{authTypeCapitalized}} is disabled', { authTypeCapitalized }), - t('To use {{authenticationTypeName}}, set up it first in settings', { - authenticationTypeName, - }), - [{ text: t('Set up'), onPress: enroll }, { text: t('Not now') }] + t('onboarding.security.alert.biometrics.title.android'), + t('onboarding.security.alert.biometrics.message.android'), + [ + { text: t('onboarding.security.button.setup'), onPress: enroll }, + { text: t('common.button.notNow') }, + ] ) } @@ -94,7 +95,7 @@ export function SecuritySetupScreen({ route: { params } }: Props): JSX.Element { dispatch(setRequiredForTransactions(true)) await onPressNext() } - }, [t, authenticationTypeName, dispatch, onPressNext]) + }, [t, biometricsMethod, dispatch, onPressNext]) const onCloseModal = useCallback(() => setShowWarningModal(false), []) @@ -120,13 +121,14 @@ export function SecuritySetupScreen({ route: { params } }: Props): JSX.Element { )} + subtitle={ + isIOS + ? t('onboarding.security.subtitle.ios', { + biometricsMethod, + }) + : t('onboarding.security.subtitle.android') + } + title={t('onboarding.security.title')}> @@ -166,15 +168,15 @@ export function SecuritySetupScreen({ route: { params } }: Props): JSX.Element { - {t('Maybe later')} + {t('common.button.later')} diff --git a/apps/mobile/src/screens/Onboarding/TermsOfService.tsx b/apps/mobile/src/screens/Onboarding/TermsOfService.tsx index 4b200ce49e2..54478016c39 100644 --- a/apps/mobile/src/screens/Onboarding/TermsOfService.tsx +++ b/apps/mobile/src/screens/Onboarding/TermsOfService.tsx @@ -1,27 +1,30 @@ -import { Trans, useTranslation } from 'react-i18next' -import { Text } from 'ui/src' +import { Trans } from 'react-i18next' +import { Flex, Text } from 'ui/src' import { uniswapUrls } from 'wallet/src/constants/urls' import { openUri } from 'wallet/src/utils/linking' export function TermsOfService(): JSX.Element { - const { t } = useTranslation() return ( - - By continuing, I agree to the{' '} - => openUri(uniswapUrls.termsOfServiceUrl)}> - Terms of Service - {' '} - and consent to the{' '} - => openUri(uniswapUrls.privacyPolicyUrl)}> - Privacy Policy - + + By continuing, I agree to the + + => openUri(uniswapUrls.termsOfServiceUrl)}> + Terms of Service + + + and consent to the + + => openUri(uniswapUrls.privacyPolicyUrl)}> + Privacy Policy + + . diff --git a/apps/mobile/src/screens/Onboarding/WelcomeWalletScreen.tsx b/apps/mobile/src/screens/Onboarding/WelcomeWalletScreen.tsx index e282221c069..ac753ce9e3c 100644 --- a/apps/mobile/src/screens/Onboarding/WelcomeWalletScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/WelcomeWalletScreen.tsx @@ -108,7 +108,7 @@ export function WelcomeWalletScreen({ navigation, route: { params } }: Props): J maxFontSizeMultiplier={media.short ? 1.1 : fonts.heading3.maxFontSizeMultiplier} textAlign="center" variant="heading3"> - {t('Welcome to your new wallet')} + {t('onboarding.wallet.title')} - {t( - 'This is your personal space for tokens, NFTs, and all of your trades. Finish setting it up to keep your funds safe.' - )} + {t('onboarding.wallet.description.full')} @@ -138,7 +136,7 @@ export function WelcomeWalletScreen({ navigation, route: { params } }: Props): J /> - {t('Let’s keep it safe')} + {t('onboarding.wallet.continue')} diff --git a/apps/mobile/src/screens/Onboarding/__snapshots__/BackupScreen.test.tsx.snap b/apps/mobile/src/screens/Onboarding/__snapshots__/BackupScreen.test.tsx.snap index 476391cc238..122f3f57dc5 100644 --- a/apps/mobile/src/screens/Onboarding/__snapshots__/BackupScreen.test.tsx.snap +++ b/apps/mobile/src/screens/Onboarding/__snapshots__/BackupScreen.test.tsx.snap @@ -658,7 +658,7 @@ exports[`BackupScreen renders backup options when none are completed 1`] = ` } suppressHighlighting={true} > - What is a recovery phrase? + What’s a recovery phrase? @@ -1325,7 +1325,7 @@ exports[`BackupScreen renders backup options when some are completed 1`] = ` } suppressHighlighting={true} > - What is a recovery phrase? + What’s a recovery phrase? diff --git a/apps/mobile/src/screens/ReceiveCryptoModal.tsx b/apps/mobile/src/screens/ReceiveCryptoModal.tsx index b0b189d00dc..656a9c35850 100644 --- a/apps/mobile/src/screens/ReceiveCryptoModal.tsx +++ b/apps/mobile/src/screens/ReceiveCryptoModal.tsx @@ -116,17 +116,17 @@ export function ReceiveCryptoModal(): JSX.Element { - {t('Receive crypto')} + {t('home.upsell.receive.title')} - {t('Fund your wallet by transferring crypto from another wallet or account')} + {t('home.upsell.receive.description')} - {t('Link an account')} + {t('home.upsell.receive.cta')} diff --git a/apps/mobile/src/screens/SettingsAppearanceScreen.tsx b/apps/mobile/src/screens/SettingsAppearanceScreen.tsx index 84c77abcd2e..eb96c4982ee 100644 --- a/apps/mobile/src/screens/SettingsAppearanceScreen.tsx +++ b/apps/mobile/src/screens/SettingsAppearanceScreen.tsx @@ -24,29 +24,29 @@ export function SettingsAppearanceScreen(): JSX.Element { return ( - {t('Appearance')} + {t('settings.screen.appearance.title')} diff --git a/apps/mobile/src/screens/SettingsBiometricAuthScreen.tsx b/apps/mobile/src/screens/SettingsBiometricAuthScreen.tsx index 008bd0c9eec..1761cc3dc5d 100644 --- a/apps/mobile/src/screens/SettingsBiometricAuthScreen.tsx +++ b/apps/mobile/src/screens/SettingsBiometricAuthScreen.tsx @@ -22,7 +22,7 @@ import { import { Flex, Text, TouchableArea } from 'ui/src' import { Switch } from 'wallet/src/components/buttons/Switch' import { openSettings } from 'wallet/src/utils/linking' -import { isIOS } from 'wallet/src/utils/platform' +import { isAndroid, isIOS } from 'wallet/src/utils/platform' interface BiometricAuthSetting { onValueChange: (newValue: boolean) => void @@ -47,9 +47,7 @@ export function SettingsBiometricAuthScreen(): JSX.Element { const onCloseModal = useCallback(() => setShowUnsafeWarningModal(false), []) const { touchId } = useDeviceSupportsBiometricAuth() - const authenticationTypeName = useBiometricName(touchId) - const capitalizedAuthTypeName = - authenticationTypeName.charAt(0).toUpperCase() + authenticationTypeName.slice(1) + const biometricsMethod = useBiometricName(touchId) const { requiredForAppAccess, requiredForTransactions } = useBiometricAppSettings() const { trigger } = useBiometricPrompt( @@ -73,23 +71,28 @@ export function SettingsBiometricAuthScreen(): JSX.Element { const handleOSBiometricAuthTurnedOff = (): void => { isIOS ? Alert.alert( - t('{{capitalizedAuthTypeName}} is turned off', { capitalizedAuthTypeName }), - t( - '{{capitalizedAuthTypeName}} is currently turned off for Uniswap Wallet—you can turn it on in your system settings.', - { capitalizedAuthTypeName } - ), - [{ text: t('Settings'), onPress: openSettings }, { text: t('Cancel') }] + isAndroid + ? t('settings.setting.biometrics.off.title.android') + : t('settings.setting.biometrics.off.title.ios', { biometricsMethod }), + isAndroid + ? t('settings.setting.biometrics.off.message.android') + : t('settings.setting.biometrics.off.message.ios', { biometricsMethod }), + [ + { text: t('common.navigation.systemSettings'), onPress: openSettings }, + { text: t('common.button.cancel') }, + ] ) : Alert.alert( - t('{{capitalizedAuthTypeName}} is not setup', { - capitalizedAuthTypeName, - authenticationTypeName, - }), - t( - '{{capitalizedAuthTypeName}} is not setup on your device. To use {{authenticationTypeName}}, set it up first in Settings.', - { capitalizedAuthTypeName, authenticationTypeName } - ), - [{ text: t('Set up'), onPress: enroll }, { text: t('Cancel') }] + isAndroid + ? t('settings.setting.biometrics.unavailable.title.android') + : t('settings.setting.biometrics.unavailable.title.ios', { biometricsMethod }), + isAndroid + ? t('settings.setting.biometrics.unavailable.message.android') + : t('settings.setting.biometrics.unavailable.message.ios', { biometricsMethod }), + [ + { text: t('common.button.setup'), onPress: enroll }, + { text: t('common.button.cancel') }, + ] ) } @@ -121,8 +124,10 @@ export function SettingsBiometricAuthScreen(): JSX.Element { }) }, value: requiredForAppAccess, - text: t('App access'), - subText: t('Require {{authenticationTypeName}} to open app', { authenticationTypeName }), + text: t('settings.setting.biometrics.appAccess.title'), + subText: isAndroid + ? t('settings.setting.biometrics.appAccess.subtitle.android') + : t('settings.setting.biometrics.appAccess.subtitle.ios', { biometricsMethod }), }, { onValueChange: async (newRequiredForTransactionsValue): Promise => { @@ -151,18 +156,13 @@ export function SettingsBiometricAuthScreen(): JSX.Element { }) }, value: requiredForTransactions, - text: t('Transactions'), - subText: t('Require {{authenticationTypeName}} to transact', { authenticationTypeName }), + text: t('settings.setting.biometrics.transactions.title'), + subText: isAndroid + ? t('settings.setting.biometrics.transactions.subtitle.android') + : t('settings.setting.biometrics.transactions.subtitle.ios', { biometricsMethod }), }, ] - }, [ - requiredForAppAccess, - t, - authenticationTypeName, - capitalizedAuthTypeName, - requiredForTransactions, - trigger, - ]) + }, [requiredForAppAccess, t, biometricsMethod, requiredForTransactions, trigger]) const renderItem = ({ item: { text, subText, value, onValueChange }, @@ -214,7 +214,7 @@ export function SettingsBiometricAuthScreen(): JSX.Element { - {t('{{capitalizedAuthTypeName}}', { capitalizedAuthTypeName })} + {isAndroid ? t('settings.setting.biometrics.title') : biometricsMethod} diff --git a/apps/mobile/src/screens/SettingsCloudBackupPasswordConfirmScreen.tsx b/apps/mobile/src/screens/SettingsCloudBackupPasswordConfirmScreen.tsx index 9af2a93c677..5d5d1de4ce2 100644 --- a/apps/mobile/src/screens/SettingsCloudBackupPasswordConfirmScreen.tsx +++ b/apps/mobile/src/screens/SettingsCloudBackupPasswordConfirmScreen.tsx @@ -35,12 +35,10 @@ export function SettingsCloudBackupPasswordConfirmScreen({ - {t('Confirm your backup password')} + {t('onboarding.cloud.confirm.title')} - {t( - 'You’ll need to enter this password to recover your account. It’s not stored anywhere, so it can’t be recovered by anyone else.' - )} + {t('onboarding.cloud.confirm.description')} - {isAndroid ? t('Back up to Google Drive') : t('Back up to iCloud')} + {t('settings.setting.backup.create.title', { + cloudProviderName: getCloudProviderName(), + })} - {isAndroid - ? t( - 'Setting a password will encrypt your recovery phrase backup, adding an extra level of protection if your Google Drive account is ever compromised.' - ) - : t( - 'Setting a password will encrypt your recovery phrase backup, adding an extra level of protection if your iCloud account is ever compromised.' - )} + {t('settings.setting.backup.create.description', { + cloudProviderName: getCloudProviderName(), + })} @@ -69,29 +67,21 @@ export function SettingsCloudBackupPasswordCreateScreen({ - {isAndroid - ? t('Back up recovery phrase to Google Drive?') - : t('Back up recovery phrase to iCloud?')} + {t('settings.setting.backup.modal.title')} - {isAndroid - ? t( - 'You haven’t backed up your recovery phrase to Google Drive yet. By doing so, you can recover your wallet just by being logged into Google Drive on any device.' - ) - : t( - 'You haven’t backed up your recovery phrase to iCloud yet. By doing so, you can recover your wallet just by being logged into iCloud on any device.' - )} + {t('settings.setting.backup.modal.description')} diff --git a/apps/mobile/src/screens/SettingsCloudBackupStatus.tsx b/apps/mobile/src/screens/SettingsCloudBackupStatus.tsx index 31c4ca73b99..8461deba5a6 100644 --- a/apps/mobile/src/screens/SettingsCloudBackupStatus.tsx +++ b/apps/mobile/src/screens/SettingsCloudBackupStatus.tsx @@ -27,7 +27,7 @@ import { } from 'wallet/src/features/wallet/accounts/types' import { useAccounts } from 'wallet/src/features/wallet/hooks' import { ElementName, ModalName } from 'wallet/src/telemetry/constants' -import { isAndroid } from 'wallet/src/utils/platform' +import { getCloudProviderName } from 'wallet/src/utils/platform' type Props = NativeStackScreenProps @@ -73,9 +73,9 @@ export function SettingsCloudBackupStatus({ logger.error(error, { tags: { file: 'SettingsCloudBackupStatus', function: 'deleteBackup' } }) Alert.alert( - isAndroid ? t('Google Drive error') : t('iCloud error'), - t('Unable to delete backup'), - [{ text: t('OK'), style: 'default' }] + t('settings.setting.backup.error.title', { cloudProviderName: getCloudProviderName() }), + t('settings.setting.backup.error.message.short'), + [{ text: t('common.button.ok'), style: 'default' }] ) } } @@ -92,28 +92,26 @@ export function SettingsCloudBackupStatus({ return ( - {isAndroid ? t('Google Drive backup') : t('iCloud backup')} + + {t('settings.setting.backup.status.title', { cloudProviderName: getCloudProviderName() })} + - {isAndroid - ? t( - 'By having your recovery phrase backed up to Google Drive, you can recover your wallet just by being logged into your Google account on any device.' - ) - : t( - 'By having your recovery phrase backed up to iCloud, you can recover your wallet just by being logged into your iCloud on any device.' - )} + {t('settings.setting.backup.status.description', { + cloudProviderName: getCloudProviderName(), + })} - {t('Recovery phrase')} + {t('settings.setting.backup.seedPhrase.label')} - {t('Backed up')} + {t('settings.setting.backup.status.seedPhrase.backed')} {/* @TODO: [MOB-249] Add non-backed up state once we have more options on this page */} @@ -137,25 +135,19 @@ export function SettingsCloudBackupStatus({ onPress={(): void => { setShowBackupDeleteWarning(true) }}> - {isAndroid ? t('Delete backup') : t('Delete backup')} + {t('settings.setting.backup.status.action.delete')} {showBackupDeleteWarning && ( { setShowBackupDeleteWarning(false) }} @@ -163,9 +155,7 @@ export function SettingsCloudBackupStatus({ {associatedAccounts.length > 1 && ( - {t( - 'Because these wallets share a recovery phrase, it will also delete the backups for:' - )} + {t('settings.setting.backup.delete.confirm.message')} {associatedAccounts.map((account) => ( diff --git a/apps/mobile/src/screens/SettingsFiatCurrencyModal.tsx b/apps/mobile/src/screens/SettingsFiatCurrencyModal.tsx index d92dd2ff267..ffb3de61691 100644 --- a/apps/mobile/src/screens/SettingsFiatCurrencyModal.tsx +++ b/apps/mobile/src/screens/SettingsFiatCurrencyModal.tsx @@ -21,7 +21,7 @@ export function SettingsFiatCurrencyModal(): JSX.Element { name={ModalName.FiatCurrencySelector} onClose={(): Action => dispatch(closeModal({ name: ModalName.FiatCurrencySelector }))}> - {t('Local currency')} + {t('settings.setting.currency.title')} - {t('Change preferred language')} + {t('settings.setting.language.title')} - {t( - 'Uniswap defaults to your device‘s language settings. To change your preferred language, go to “Uniswap” in your device settings and tap on “Language”' - )} + {t('settings.setting.language.description')} diff --git a/apps/mobile/src/screens/SettingsPrivacyScreen.tsx b/apps/mobile/src/screens/SettingsPrivacyScreen.tsx index 0cc1b201ada..c58a155c805 100644 --- a/apps/mobile/src/screens/SettingsPrivacyScreen.tsx +++ b/apps/mobile/src/screens/SettingsPrivacyScreen.tsx @@ -20,15 +20,13 @@ export function SettingsPrivacyScreen(): JSX.Element { return ( - {t('Privacy')} + {t('settings.setting.privacy.title')} - {t('Allow analytics')} + {t('settings.setting.privacy.analytics.title')} - {t( - 'We use anonymous usage data to enhance your experience across Uniswap Labs products. When disabled, we only track errors and essential usage.' - )} + {t('settings.setting.privacy.analytics.description')} diff --git a/apps/mobile/src/screens/SettingsScreen.tsx b/apps/mobile/src/screens/SettingsScreen.tsx index dc05057138b..d790076d824 100644 --- a/apps/mobile/src/screens/SettingsScreen.tsx +++ b/apps/mobile/src/screens/SettingsScreen.tsx @@ -75,7 +75,7 @@ import { setHideSpamTokens, } from 'wallet/src/features/wallet/slice' import { ModalName } from 'wallet/src/telemetry/constants' -import { isAndroid } from 'wallet/src/utils/platform' +import { getCloudProviderName, isAndroid } from 'wallet/src/utils/platform' export function SettingsScreen(): JSX.Element { const navigation = useNavigation() @@ -91,7 +91,7 @@ export function SettingsScreen(): JSX.Element { const { touchId: isTouchIdSupported, faceId: isFaceIdSupported } = useDeviceSupportsBiometricAuth() - const authenticationTypeName = useBiometricName(isTouchIdSupported, true) + const biometricsMethod = useBiometricName(isTouchIdSupported) const currentAppearanceSetting = useCurrentAppearanceSetting() const currentFiatCurrencyInfo = useAppFiatCurrencyInfo() const { originName: currentLanguage } = useCurrentLanguageInfo() @@ -132,17 +132,17 @@ export function SettingsScreen(): JSX.Element { // Defining them inline instead of outside component b.c. they need t() return [ { - subTitle: t('Preferences'), + subTitle: t('settings.section.preferences'), data: [ { screen: Screens.SettingsAppearance, - text: t('Appearance'), + text: t('settings.setting.appearance.title'), currentSetting: currentAppearanceSetting === 'system' - ? t('Device') + ? t('settings.setting.appearance.option.device.title') : currentAppearanceSetting === 'dark' - ? t('Dark mode') - : t('Light mode'), + ? t('settings.setting.appearance.option.dark.title') + : t('settings.setting.appearance.option.light.title'), icon: , }, @@ -150,7 +150,7 @@ export function SettingsScreen(): JSX.Element { ? ([ { modal: ModalName.FiatCurrencySelector, - text: t('Local currency'), + text: t('settings.setting.currency.title'), currentSetting: currentFiatCurrencyInfo.code, icon: , }, @@ -158,23 +158,23 @@ export function SettingsScreen(): JSX.Element { : []), { modal: ModalName.LanguageSelector, - text: t('Language'), + text: t('settings.setting.language.title'), currentSetting: currentLanguage, icon: , }, { screen: Screens.SettingsPrivacy, - text: t('Privacy'), + text: t('settings.setting.privacy.title'), icon: , }, { - text: t('Hide small balances'), + text: t('settings.setting.smallBalances.title'), icon: , isToggleEnabled: hideSmallBalances, onToggle: onToggleHideSmallBalances, }, { - text: t('Hide unknown tokens'), + text: t('settings.setting.unknownTokens.title'), icon: , isToggleEnabled: hideSpamTokens, onToggle: onToggleHideSpamTokens, @@ -183,7 +183,7 @@ export function SettingsScreen(): JSX.Element { ], }, { - subTitle: t('Security'), + subTitle: t('settings.section.security'), isHidden: noSignerAccountImported, data: [ ...(deviceSupportsBiometrics @@ -191,7 +191,7 @@ export function SettingsScreen(): JSX.Element { { screen: Screens.SettingsBiometricAuth as Screens.SettingsBiometricAuth, isHidden: !isTouchIdSupported && !isFaceIdSupported, - text: authenticationTypeName, + text: isAndroid ? t('settings.setting.biometrics.title') : biometricsMethod, icon: isTouchIdSupported ? ( ) : ( @@ -202,7 +202,7 @@ export function SettingsScreen(): JSX.Element { : []), { screen: Screens.SettingsViewSeedPhrase, - text: t('Recovery phrase'), + text: t('settings.setting.seedPhrase.title'), icon: , screenProps: { address: signerAccount?.address ?? '', walletNeedsRestore }, isHidden: noSignerAccountImported, @@ -222,54 +222,56 @@ export function SettingsScreen(): JSX.Element { }, } : { address: signerAccount?.address ?? '' }, - text: isAndroid ? t('Google Drive backup') : t('iCloud backup'), + text: t('settings.setting.backup.selected', { + cloudProviderName: getCloudProviderName(), + }), icon: , isHidden: noSignerAccountImported, }, ], }, { - subTitle: t('Support'), + subTitle: t('settings.section.support'), data: [ { screen: Screens.WebView, screenProps: { uriLink: APP_FEEDBACK_LINK, - headerTitle: t('Send feedback'), + headerTitle: t('settings.action.feedback'), }, - text: t('Send feedback'), + text: t('settings.action.feedback'), icon: , }, { screen: Screens.WebView, screenProps: { uriLink: uniswapUrls.helpUrl, - headerTitle: t('Get help'), + headerTitle: t('settings.action.help'), }, - text: t('Get help'), + text: t('settings.action.help'), icon: , }, ], }, { - subTitle: t('About'), + subTitle: t('settings.section.about'), data: [ { screen: Screens.WebView, screenProps: { uriLink: uniswapUrls.privacyPolicyUrl, - headerTitle: t('Privacy policy'), + headerTitle: t('settings.action.privacy'), }, - text: t('Privacy policy'), + text: t('settings.action.privacy'), icon: , }, { screen: Screens.WebView, screenProps: { uriLink: uniswapUrls.termsOfServiceUrl, - headerTitle: t('Terms of service'), + headerTitle: t('settings.action.terms'), }, - text: t('Terms of service'), + text: t('settings.action.terms'), icon: , }, ], @@ -298,14 +300,14 @@ export function SettingsScreen(): JSX.Element { onToggleHideSmallBalances, hideSpamTokens, onToggleHideSpamTokens, + noSignerAccountImported, deviceSupportsBiometrics, isTouchIdSupported, isFaceIdSupported, - authenticationTypeName, + biometricsMethod, + signerAccount?.address, walletNeedsRestore, - noSignerAccountImported, hasCloudBackup, - signerAccount?.address, ]) const renderItem = ({ @@ -325,7 +327,7 @@ export function SettingsScreen(): JSX.Element { return ( {t('Settings')}}> + centerElement={{t('settings.title')}}> - {t('Wallet settings')} + {t('settings.section.wallet.title')} {allAccounts @@ -452,7 +454,9 @@ function WalletSettings(): JSX.Element { {allAccounts.length > DEFAULT_ACCOUNTS_TO_DISPLAY && ( )} @@ -486,10 +490,7 @@ function FooterSettings(): JSX.Element { mt="$spacing16"> - {t('Made with love, ')} - - - {t('Uniswap Team 🦄')} + {t('settings.footer')} {isDarkMode ? ( @@ -507,7 +508,7 @@ function FooterSettings(): JSX.Element { onLongPress={(): void => { setShowSignature(true) }}> - {t('Version {{appVersion}}', { appVersion: getFullAppVersion() })} + {t('settings.version', { appVersion: getFullAppVersion() })} ) diff --git a/apps/mobile/src/screens/SettingsViewSeedPhraseScreen.tsx b/apps/mobile/src/screens/SettingsViewSeedPhraseScreen.tsx index 54ee073a6bc..0b3f92cfc8f 100644 --- a/apps/mobile/src/screens/SettingsViewSeedPhraseScreen.tsx +++ b/apps/mobile/src/screens/SettingsViewSeedPhraseScreen.tsx @@ -31,7 +31,7 @@ export function SettingsViewSeedPhraseScreen({ return ( - {t('Recovery phrase')} + {t('settings.setting.seedPhrase.title')} , screenProps: { address }, isHidden: !!ensName || !!unitag?.username, @@ -141,7 +141,7 @@ export function SettingsWallet({ const sections: SettingsSection[] = [ { - subTitle: t('Wallet preferences'), + subTitle: t('settings.setting.wallet.preferences.title'), data: [ ...(showEditProfile ? [] : [editNicknameSectionOption]), { @@ -152,12 +152,12 @@ export function SettingsWallet({ onValueChange={onChangeNotificationSettings} /> ), - text: t('Notifications'), + text: t('settings.setting.wallet.notifications.title'), icon: , }, { screen: Screens.SettingsWalletManageConnection, - text: t('Manage connections'), + text: t('settings.setting.wallet.connections.title'), icon: , screenProps: { address }, isHidden: readonly, @@ -225,7 +225,7 @@ export function SettingsWallet({ /> @@ -275,7 +275,9 @@ function AddressDisplayHeader({ address }: { address: Address }): JSX.Element { size="medium" theme="secondary_Button" onPress={onPressEditProfile}> - {unitag?.username ? t('Edit profile') : t('Edit label')} + {unitag?.username + ? t('settings.setting.wallet.action.editProfile') + : t('settings.setting.wallet.action.editLabel')} )} diff --git a/apps/mobile/src/screens/SettingsWalletEdit.tsx b/apps/mobile/src/screens/SettingsWalletEdit.tsx index be6e063d248..e30928fc5d5 100644 --- a/apps/mobile/src/screens/SettingsWalletEdit.tsx +++ b/apps/mobile/src/screens/SettingsWalletEdit.tsx @@ -84,7 +84,7 @@ export function SettingsWalletEdit({ contentContainerStyle={styles.expand} style={styles.base}> - {t('Edit label')} + {t('settings.setting.wallet.action.editLabel')} {accountNameIsEditable && ( - - {t('Labels are not public. They are stored locally and only visible to you.')} - + {t('settings.setting.wallet.editLabel.description')} )} {showUnitagBanner && ( @@ -152,7 +150,7 @@ export function SettingsWalletEdit({ size="medium" theme="primary" onPress={onPressSaveChanges}> - {t('Save changes')} + {t('settings.setting.wallet.editLabel.save')} diff --git a/apps/mobile/src/screens/TokenDetailsScreen.tsx b/apps/mobile/src/screens/TokenDetailsScreen.tsx index 42803a139ff..e5eaec0e926 100644 --- a/apps/mobile/src/screens/TokenDetailsScreen.tsx +++ b/apps/mobile/src/screens/TokenDetailsScreen.tsx @@ -96,7 +96,7 @@ function HeaderTitleElement({ url={logo} /> - {symbol ?? t('Unknown token')} + {symbol ?? t('token.error.unknown')} diff --git a/apps/mobile/src/test/render.tsx b/apps/mobile/src/test/render.tsx index 32bb12cba5b..d98a86964aa 100644 --- a/apps/mobile/src/test/render.tsx +++ b/apps/mobile/src/test/render.tsx @@ -18,7 +18,7 @@ import { persistedReducer } from 'src/app/store' import { Resolvers } from 'wallet/src/data/__generated__/types-and-hooks' import { UnitagUpdaterContextProvider } from 'wallet/src/features/unitags/context' import { SharedProvider } from 'wallet/src/provider' -import { AutoMockedApolloProvider } from 'wallet/src/test/mocks/provider' +import { AutoMockedApolloProvider } from 'wallet/src/test/mocks/gql/provider' // This type extends the default options for render from RTL, as well // as allows the user to specify other things such as initialState, store. diff --git a/apps/mobile/tsconfig.json b/apps/mobile/tsconfig.json index 3ca7d054034..6234ab12475 100644 --- a/apps/mobile/tsconfig.json +++ b/apps/mobile/tsconfig.json @@ -11,6 +11,9 @@ }, { "path": "../../packages/wallet" + }, + { + "path": "../../packages/uniswap" } ], "compilerOptions": { diff --git a/apps/web/.env b/apps/web/.env index 1c23fa84ea2..912e094db8a 100644 --- a/apps/web/.env +++ b/apps/web/.env @@ -14,7 +14,7 @@ REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.s REACT_APP_STATSIG_PROXY_URL="https://interface.gateway.uniswap.org/v1/statsig-proxy" REACT_APP_TEMP_API_URL="https://temp.gateway.uniswap.org/v1" REACT_APP_UNISWAP_API_URL="https://interface.gateway.uniswap.org/v2" -REACT_APP_UNISWAP_BASE_API_URL="https://interface.gateway.uniswap.org/" +REACT_APP_UNISWAP_BASE_API_URL="https://interface.gateway.uniswap.org" REACT_APP_UNISWAP_GATEWAY_DNS="https://interface.gateway.uniswap.org/v2" REACT_APP_UNITAGS_API_URL="https://gateway.uniswap.org/v2/unitags" REACT_APP_WALLET_CONNECT_PROJECT_ID="c6c9bacd35afa3eb9e6cccf6d8464395" diff --git a/apps/web/.eslintrc.js b/apps/web/.eslintrc.js index 794b9347c44..4b374514abb 100644 --- a/apps/web/.eslintrc.js +++ b/apps/web/.eslintrc.js @@ -46,6 +46,21 @@ module.exports = { message: 'Only import types, unless you are in the client-side SOR, to preserve lazy-loading.', allowTypeImports: true, }, + { + name: 'moment', + // tree-shaking for moment is not configured because it degrades performance - see craco.config.cjs. + message: 'moment is not configured for tree-shaking. If you use it, update the Webpack configuration.', + }, + { + name: 'react-helmet-async', + // default package's esm export is broken, but the explicit cjs export works. + message: `Import from 'react-helment-async/lib/index' instead.`, + }, + { + name: 'zustand', + importNames: ['default'], + message: 'Default import from zustand is deprecated. Import `{ create }` instead.', + }, ], }, ], @@ -60,23 +75,6 @@ module.exports = { ], }, ], - 'no-restricted-imports': [ - 'error', - { - paths: [ - { - name: 'moment', - // tree-shaking for moment is not configured because it degrades performance - see craco.config.cjs. - message: 'moment is not configured for tree-shaking. If you use it, update the Webpack configuration.', - }, - { - name: 'zustand', - importNames: ['default'], - message: 'Default import from zustand is deprecated. Import `{ create }` instead.', - }, - ], - }, - ], 'no-restricted-syntax': [ 'error', { @@ -90,6 +88,7 @@ module.exports = { files: ['**/*.ts', '**/*.tsx'], excludedFiles: ['src/analytics/*'], rules: { + // Uses 'no-restricted-imports' to avoid overriding the above rules in '@typescript-eslint/no-restricted-imports' 'no-restricted-imports': [ 'error', { diff --git a/apps/web/package.json b/apps/web/package.json index 1009ab3d323..7639e57a29a 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -188,7 +188,6 @@ "@sentry/types": "7.80.0", "@tanstack/react-table": "8.10.7", "@types/poisson-disk-sampling": "2.2.4", - "@types/react-helmet": "6.1.7", "@types/react-scroll-sync": "0.8.7", "@types/react-window-infinite-loader": "1.0.6", "@uniswap/analytics": "1.7.0", @@ -265,7 +264,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-feather": "2.0.10", - "react-helmet": "6.1.0", + "react-helmet-async": "2.0.4", "react-infinite-scroll-component": "6.1.0", "react-is": "18.2.0", "react-markdown": "4.3.1", @@ -286,6 +285,7 @@ "statsig-react": "1.32.0", "styled-components": "5.3.11", "tiny-invariant": "1.3.1", + "uniswap": "workspace:^", "use-resize-observer": "9.1.0", "utilities": "workspace:^", "uuid": "9.0.0", diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/CancelLimitsDialog.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/CancelLimitsDialog.tsx index 9a8abc80a00..d6df2e2c942 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/CancelLimitsDialog.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/CancelLimitsDialog.tsx @@ -1,5 +1,6 @@ import { Plural, Trans } from '@lingui/macro' -import { useCreateCancelTransactionRequest } from 'components/AccountDrawer/MiniPortfolio/Activity/utils' +import { ChainId, CurrencyAmount } from '@uniswap/sdk-core' +import { useCancelLimitsGasEstimate } from 'components/AccountDrawer/MiniPortfolio/Limits/hooks/useCancelLimitsGasEstimate' import GetHelp from 'components/Button/GetHelp' import Column from 'components/Column' import { Container, Dialog, DialogButtonType, DialogProps } from 'components/Dialog/Dialog' @@ -8,9 +9,8 @@ import Modal from 'components/Modal' import Row from 'components/Row' import { DetailLineItem } from 'components/swap/DetailLineItem' import { ConfirmedIcon, LogoContainer, SubmittedIcon } from 'components/swap/PendingModalContent/Logos' -import { formatEther } from 'ethers/lib/utils' -import { GasSpeed, useTransactionGasFee } from 'hooks/useTransactionGasFee' -import { useMemo } from 'react' +import { nativeOnChain } from 'constants/tokens' +import { useStablecoinValue } from 'hooks/useStablecoinPrice' import { Slash } from 'react-feather' import { UniswapXOrderDetails } from 'state/signatures/types' import styled, { useTheme } from 'styled-components' @@ -81,19 +81,10 @@ export function CancelLimitsDialog( ) { const { orders, cancelState, cancelTxHash, onConfirm, onCancel } = props - const { formatNumber } = useFormatter() - const cancelTransactionParams = useMemo( - () => ({ - encodedOrders: orders.map((order) => order.encodedOrder as string), - chainId: orders[0]?.chainId, - }), - [orders] - ) - const cancelTransaction = useCreateCancelTransactionRequest(cancelTransactionParams) - const gasEstimate = useTransactionGasFee(cancelTransaction, GasSpeed.Fast) - const { title, icon } = useCancelLimitsDialogContent(cancelState, orders) + const gasEstimate = useCancelLimitsGasEstimate(orders) + if ( [CancellationState.PENDING_SIGNATURE, CancellationState.PENDING_CONFIRMATION, CancellationState.CANCELLED].includes( cancelState @@ -144,23 +135,7 @@ export function CancelLimitsDialog( one="Your swap could execute before cancellation is processed. Your network costs cannot be refunded. Do you wish to proceed?" other="Your swaps could execute before cancellation is processed. Your network costs cannot be refunded. Do you wish to proceed?" /> - {gasEstimate?.value && ( - - Network cost, - Value: () => ( - - {formatNumber({ - input: Number(formatEther(gasEstimate.value as string)), - type: NumberType.FiatGasPrice, - })} - - ), - }} - /> - - )} + } buttonsConfig={{ @@ -184,3 +159,23 @@ export function CancelLimitsDialog( return null } } + +function GasEstimateDisplay({ gasEstimateValue, chainId }: { gasEstimateValue?: string; chainId: ChainId }) { + const gasFeeCurrencyAmount = CurrencyAmount.fromRawAmount(nativeOnChain(chainId), gasEstimateValue ?? '0') + const gasFeeUSD = useStablecoinValue(gasFeeCurrencyAmount) + const { formatCurrencyAmount } = useFormatter() + const gasFeeFormatted = formatCurrencyAmount({ + amount: gasFeeUSD, + type: NumberType.PortfolioBalance, + }) + return ( + + Network cost, + Value: () => {gasEstimateValue ? gasFeeFormatted : '-'}, + }} + /> + + ) +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal.tsx index 9c520ff7c67..357e093efb5 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal.tsx @@ -30,10 +30,11 @@ import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk' import { sendAnalyticsEvent } from 'analytics' import { cancelMultipleUniswapXOrders } from 'components/AccountDrawer/MiniPortfolio/Activity/utils' import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled' +import { LimitDisclaimer } from 'components/swap/LimitDisclaimer' import { ContractTransaction } from 'ethers/lib/ethers' import { useContract } from 'hooks/useContract' -import PERMIT2_ABI from 'wallet/src/abis/permit2.json' -import { Permit2 } from 'wallet/src/abis/types/Permit2' +import PERMIT2_ABI from 'uniswap/src/abis/permit2.json' +import { Permit2 } from 'uniswap/src/abis/types/Permit2' import { PortfolioLogo } from '../PortfolioLogo' import { OffchainOrderLineItem, OffchainOrderLineItemProps, OffchainOrderLineItemType } from './OffchainOrderLineItem' @@ -67,6 +68,7 @@ export function useOpenOffchainActivityModal() { const Wrapper = styled(AutoColumn).attrs({ gap: 'md', grow: true })` padding: 12px 20px 20px 20px; width: 100%; + background-color: ${({ theme }) => theme.surface1}; ` const StyledXButton = styled(X)` @@ -272,7 +274,7 @@ export function OrderContent({ Cancel limit )} - {order.status === UniswapXOrderStatus.INSUFFICIENT_FUNDS && ( + {order.status === UniswapXOrderStatus.INSUFFICIENT_FUNDS ? ( @@ -286,6 +288,8 @@ export function OrderContent({ + ) : ( + )} ) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/OffchainActivityModal.test.tsx.snap b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/OffchainActivityModal.test.tsx.snap index 113ec65f1ce..94532b3ec12 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/OffchainActivityModal.test.tsx.snap +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/OffchainActivityModal.test.tsx.snap @@ -250,6 +250,17 @@ exports[`OrderContent should render without error, filled order 1`] = ` display: flex; } +.c23 { + background-color: #F9F9F9; + border-radius: 12px; + padding: 12px; + margin-top: 12px; +} + +.c24 { + line-height: 16px; +} + .c21 { background-color: transparent; border: none; @@ -505,6 +516,32 @@ exports[`OrderContent should render without error, filled order 1`] = ` +
+
+ Please be aware that the execution for limits may vary based on real-time market fluctuations and Ethereum network congestion. Limits may not execute exactly when tokens reach the specified price. +
+
+ Canceling a limit has a network cost. +
+ +
@@ -601,6 +638,25 @@ exports[`OrderContent should render without error, open order 1`] = ` letter-spacing: -0.01em; } +.c28 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + color: #FC72FF; + stroke: #FC72FF; + font-weight: 500; +} + +.c28:hover { + opacity: 0.6; +} + +.c28:active { + opacity: 0.4; +} + .c8 { width: 100%; height: 1px; @@ -814,6 +870,17 @@ exports[`OrderContent should render without error, open order 1`] = ` display: flex; } +.c26 { + background-color: #F9F9F9; + border-radius: 12px; + padding: 12px; + margin-top: 12px; +} + +.c27 { + line-height: 16px; +} + .c21 { background-color: transparent; border: none; @@ -1076,6 +1143,32 @@ exports[`OrderContent should render without error, open order 1`] = ` /> Cancel limit +
+
+ Please be aware that the execution for limits may vary based on real-time market fluctuations and Ethereum network congestion. Limits may not execute exactly when tokens reach the specified price. +
+
+ Canceling a limit has a network cost. +
+ +
diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/parseRemote.test.tsx.snap b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/parseRemote.test.tsx.snap index a70db79e6fe..2ec526c2520 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/parseRemote.test.tsx.snap +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/parseRemote.test.tsx.snap @@ -138,7 +138,7 @@ Object { "decimals": 18, "isNative": true, "isToken": false, - "name": "Ether", + "name": "Ethereum", "symbol": "ETH", }, Token { diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/utils.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/utils.ts index 59e5ae38985..f5f2dd379be 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/utils.ts +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/utils.ts @@ -5,9 +5,9 @@ import { ChainId } from '@uniswap/sdk-core' import { DutchOrder } from '@uniswap/uniswapx-sdk' import { getYear, isSameDay, isSameMonth, isSameWeek, isSameYear } from 'date-fns' import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks' +import PERMIT2_ABI from 'uniswap/src/abis/permit2.json' +import { Permit2 } from 'uniswap/src/abis/types' import { didUserReject } from 'utils/swapErrorToUserReadableMessage' -import PERMIT2_ABI from 'wallet/src/abis/permit2.json' -import { Permit2 } from 'wallet/src/abis/types' import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk' import { BigNumber, ContractTransaction } from 'ethers/lib/ethers' @@ -173,18 +173,19 @@ async function getCancelMultipleUniswapXOrdersTransaction( } } -export function useCreateCancelTransactionRequest({ - encodedOrders, - chainId, -}: { - encodedOrders?: string[] - chainId: ChainId -}): TransactionRequest | undefined { +export function useCreateCancelTransactionRequest( + params: + | { + encodedOrders?: string[] + chainId: ChainId + } + | undefined +): TransactionRequest | undefined { const permit2 = useContract(PERMIT2_ADDRESS, PERMIT2_ABI, true) const transactionFetcher = useCallback(() => { - if (!encodedOrders || encodedOrders.filter(Boolean).length === 0 || !permit2) return - return getCancelMultipleUniswapXOrdersTransaction(encodedOrders, chainId, permit2) - }, [encodedOrders, chainId, permit2]) + if (!params || !params.encodedOrders || params.encodedOrders.filter(Boolean).length === 0 || !permit2) return + return getCancelMultipleUniswapXOrdersTransaction(params.encodedOrders, params.chainId, permit2) + }, [params, permit2]) return useAsyncData(transactionFetcher).data } diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitDetailActivityRow.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitDetailActivityRow.tsx index d0c3f3c7e5f..0895058fe6e 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitDetailActivityRow.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitDetailActivityRow.tsx @@ -11,6 +11,7 @@ import Column from 'components/Column' import Row from 'components/Row' import { parseUnits } from 'ethers/lib/utils' import useTokenLogoSource from 'hooks/useAssetLogoSource' +import { useScreenSize } from 'hooks/useScreenSize' import { Checkbox } from 'nft/components/layout/Checkbox' import { useMemo, useState } from 'react' import { ArrowRight } from 'react-feather' @@ -53,6 +54,7 @@ export function LimitDetailActivityRow({ order, onToggleSelect, selected }: Limi const openOffchainActivityModal = useOpenOffchainActivityModal() const { formatReviewSwapCurrencyAmount } = useFormatter() const [hovered, setHovered] = useState(false) + const isSmallScreen = !useScreenSize()['sm'] const amounts = useOrderAmounts(order.offchainOrderDetails) const amountsDefined = !!amounts?.inputAmount?.currency && !!amounts?.outputAmount?.currency @@ -124,7 +126,7 @@ export function LimitDetailActivityRow({ order, onToggleSelect, selected }: Limi }} /> `${theme.breakpoint.sm}px`}) { + position: relative; + margin: 24px 0 24px; + } +` + +const StyledLimitsDisclaimer = styled(LimitDisclaimer)` + margin-bottom: 24px; ` function useCancelMultipleOrders(orders?: UniswapXOrderDetails[]): () => Promise { @@ -53,51 +62,57 @@ function useCancelMultipleOrders(orders?: UniswapXOrderDetails[]): () => Promise export function LimitsMenu({ onClose, account }: { account: string; onClose: () => void }) { const { openLimitOrders } = useOpenLimitOrders(account) - const [selectedOrders, setSelectedOrders] = useState>({}) + const [selectedOrdersByHash, setSelectedOrdersByHash] = useState>({}) const [cancelState, setCancelState] = useState(CancellationState.NOT_STARTED) const [cancelTxHash, setCancelTxHash] = useState() - const cancelOrders = useCancelMultipleOrders(Object.values(selectedOrders)) + + const selectedOrders = useMemo(() => { + return Object.values(selectedOrdersByHash) + }, [selectedOrdersByHash]) + + const cancelOrders = useCancelMultipleOrders(selectedOrders) const toggleOrderSelection = (order: Activity) => { - const newSelectedOrders = { ...selectedOrders } - if (order.hash in selectedOrders) { + const newSelectedOrders = { ...selectedOrdersByHash } + if (order.hash in selectedOrdersByHash) { delete newSelectedOrders[order.hash] } else if (order.offchainOrderDetails) { newSelectedOrders[order.hash] = order.offchainOrderDetails } - setSelectedOrders(newSelectedOrders) + setSelectedOrdersByHash(newSelectedOrders) } return ( Open limits} onClose={onClose}> + {openLimitOrders.map((order) => ( ))} - {Boolean(Object.keys(selectedOrders).length) && ( + {Boolean(Object.keys(selectedOrdersByHash).length) && ( setCancelState(CancellationState.REVIEWING_CANCELLATION)} size={ButtonSize.medium} - disabled={cancelState !== CancellationState.NOT_STARTED || Object.keys(selectedOrders).length === 0} + disabled={cancelState !== CancellationState.NOT_STARTED || selectedOrders.length === 0} > )} setCancelState(CancellationState.NOT_STARTED)} onConfirm={async () => { setCancelState(CancellationState.PENDING_SIGNATURE) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/OpenLimitOrdersButton.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/OpenLimitOrdersButton.tsx index 7c0d517276d..259d8a8a820 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/OpenLimitOrdersButton.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/OpenLimitOrdersButton.tsx @@ -12,7 +12,7 @@ const Container = styled.button` border: none; background: ${({ theme }) => theme.surface2}; padding: 12px 16px; - margin-top: 8px; + margin-top: 12px; ${ClickableStyle} ` diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/LimitsMenu.test.tsx.snap b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/LimitsMenu.test.tsx.snap index 4948580c6bb..35991f4093e 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/LimitsMenu.test.tsx.snap +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/LimitsMenu.test.tsx.snap @@ -1,13 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`LimitsMenu should render when there are two open orders 1`] = ` -.c7 { +.c13 { box-sizing: border-box; margin: 0; min-width: 0; } -.c8 { +.c14 { width: 100%; display: -webkit-box; display: -webkit-flex; @@ -24,7 +24,7 @@ exports[`LimitsMenu should render when there are two open orders 1`] = ` justify-content: flex-start; } -.c13 { +.c18 { width: 100%; display: -webkit-box; display: -webkit-flex; @@ -50,7 +50,7 @@ exports[`LimitsMenu should render when there are two open orders 1`] = ` letter-spacing: -0.01em; } -.c12 { +.c9 { color: #7D7D7D; -webkit-letter-spacing: -0.01em; -moz-letter-spacing: -0.01em; @@ -58,13 +58,32 @@ exports[`LimitsMenu should render when there are two open orders 1`] = ` letter-spacing: -0.01em; } -.c16 { +.c21 { -webkit-letter-spacing: -0.01em; -moz-letter-spacing: -0.01em; -ms-letter-spacing: -0.01em; letter-spacing: -0.01em; } +.c11 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + color: #FC72FF; + stroke: #FC72FF; + font-weight: 500; +} + +.c11:hover { + opacity: 0.6; +} + +.c11:active { + opacity: 0.4; +} + .c0 { display: -webkit-box; display: -webkit-flex; @@ -79,7 +98,22 @@ exports[`LimitsMenu should render when there are two open orders 1`] = ` justify-content: flex-start; } -.c11 { +.c6 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 8px; +} + +.c17 { display: grid; grid-auto-rows: auto; -webkit-box-flex: 1; @@ -88,7 +122,18 @@ exports[`LimitsMenu should render when there are two open orders 1`] = ` flex-grow: 1; } -.c9 { +.c7 { + background-color: #F9F9F9; + border-radius: 12px; + padding: 12px; + margin-top: 12px; +} + +.c10 { + line-height: 16px; +} + +.c15 { gap: 12px; height: 68px; padding: 0 16px; @@ -97,11 +142,11 @@ exports[`LimitsMenu should render when there are two open orders 1`] = ` cursor: pointer; } -.c9:hover { +.c15:hover { cursor: pointer; } -.c17 { +.c22 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -116,7 +161,7 @@ exports[`LimitsMenu should render when there are two open orders 1`] = ` line-height: 1; } -.c19 { +.c24 { border-color: #22222212; display: inline-block; margin-right: 1px; @@ -133,15 +178,15 @@ exports[`LimitsMenu should render when there are two open orders 1`] = ` transition-duration: 125ms; } -.c19:hover { +.c24:hover { opacity: 0.6; } -.c19:active { +.c24:active { opacity: 0.4; } -.c20 { +.c25 { position: absolute; top: -24px; -webkit-clip: rect(0 0 0 0); @@ -155,7 +200,7 @@ exports[`LimitsMenu should render when there are two open orders 1`] = ` width: 1px; } -.c21 { +.c26 { display: none; height: 18px; width: 18px; @@ -164,7 +209,7 @@ exports[`LimitsMenu should render when there are two open orders 1`] = ` right: 1px; } -.c10 { +.c16 { padding: 8px 0; height: unset; white-space: nowrap; @@ -172,18 +217,18 @@ exports[`LimitsMenu should render when there are two open orders 1`] = ` text-overflow: ellipsis; } -.c18 { +.c23 { opacity: 0; } -.c14 * { +.c19 * { max-width: 40%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.c15 { +.c20 { width: 16px; height: 16px; border-radius: 50%; @@ -260,11 +305,15 @@ exports[`LimitsMenu should render when there are two open orders 1`] = ` margin-bottom: 20px; } -.c6 { +.c12 { height: 100%; position: relative; } +.c8 { + margin-bottom: 24px; +} +
+
+ Please be aware that the execution for limits may vary based on real-time market fluctuations and Ethereum network congestion. Limits may not execute exactly when tokens reach the specified price. +
+
+ Canceling a limit has a network cost. +
+ +
+
Expires January 26, 2024 at 1:52PM
@@ -333,10 +408,10 @@ exports[`LimitsMenu should render when there are two open orders 1`] = ` class="c0" >
when 0.00042 WETH/DAI
@@ -386,20 +461,20 @@ exports[`LimitsMenu should render when there are two open orders 1`] = `
Expires January 26, 2024 at 1:52PM
@@ -437,10 +512,10 @@ exports[`LimitsMenu should render when there are two open orders 1`] = ` class="c0" >
when 0.00042 WETH/DAI
@@ -490,20 +565,20 @@ exports[`LimitsMenu should render when there are two open orders 1`] = `
+
+ Please be aware that the execution for limits may vary based on real-time market fluctuations and Ethereum network congestion. Limits may not execute exactly when tokens reach the specified price. +
+
+ Canceling a limit has a network cost. +
+ +
+
Expires January 26, 2024 at 1:52PM
@@ -863,10 +1013,10 @@ exports[`LimitsMenu should render when there is one open order 1`] = ` class="c0" >
when 0.00042 WETH/DAI
@@ -916,20 +1066,20 @@ exports[`LimitsMenu should render when there is one open order 1`] = `
@@ -1058,7 +1058,7 @@ exports[`PoolDetailsTransactionsTable renders error state 1`] = ` } .c9 { - min-width: 164px; + min-width: 120px; -webkit-flex: 0; -ms-flex: 0; flex: 0; @@ -1080,7 +1080,7 @@ exports[`PoolDetailsTransactionsTable renders error state 1`] = ` } .c14 { - min-width: 100px; + min-width: 144px; -webkit-flex: 0; -ms-flex: 0; flex: 0; @@ -2193,7 +2193,7 @@ exports[`PoolDetailsTransactionsTable renders loading state 1`] = ` } .c9 { - min-width: 164px; + min-width: 120px; -webkit-flex: 0; -ms-flex: 0; flex: 0; @@ -2215,7 +2215,7 @@ exports[`PoolDetailsTransactionsTable renders loading state 1`] = ` } .c14 { - min-width: 100px; + min-width: 144px; -webkit-flex: 0; -ms-flex: 0; flex: 0; diff --git a/apps/web/src/components/Table/__snapshots__/styled.test.tsx.snap b/apps/web/src/components/Table/__snapshots__/styled.test.tsx.snap index 1106a68a190..d5e09c3acef 100644 --- a/apps/web/src/components/Table/__snapshots__/styled.test.tsx.snap +++ b/apps/web/src/components/Table/__snapshots__/styled.test.tsx.snap @@ -99,6 +99,7 @@ exports[`TokenLinkCell renders known token on a different chain 1`] = ` .c7 { white-space: nowrap; overflow: hidden; + text-overflow: ellipsis; } - {getLocaleTimeString(timestamp * 1000, locale)} + {getAbbreviatedTimeString(timestamp * 1000)} @@ -228,8 +228,7 @@ export const TimestampCell = ({ timestamp, link }: { timestamp: number; link: st } const TokenSymbolText = styled(ThemedText.BodyPrimary)` - white-space: nowrap; - overflow: hidden; + ${EllipsisStyle} ` /** * Given a token displays the Token's Logo and Symbol with a link to its TDP diff --git a/apps/web/src/components/Table/utils.ts b/apps/web/src/components/Table/utils.ts index 4671cf365c7..2fc8b769402 100644 --- a/apps/web/src/components/Table/utils.ts +++ b/apps/web/src/components/Table/utils.ts @@ -1,5 +1,4 @@ import { t } from '@lingui/macro' -import { DEFAULT_LOCALE } from 'constants/locales' /** * Displays the time as a human-readable string. @@ -8,25 +7,19 @@ import { DEFAULT_LOCALE } from 'constants/locales' * @param {number} locale - BCP 47 language tag (e.g. en-US). * @returns {string} Message to display. */ -export function getLocaleTimeString(timestamp: number, locale = DEFAULT_LOCALE) { +export function getAbbreviatedTimeString(timestamp: number) { const now = Date.now() const timeSince = now - timestamp const secondsPassed = Math.floor(timeSince / 1000) const minutesPassed = Math.floor(secondsPassed / 60) const hoursPassed = Math.floor(minutesPassed / 60) + const daysPassed = Math.floor(hoursPassed / 24) + const monthsPassed = Math.floor(daysPassed / 30) - if (hoursPassed > 24) { - const options: Intl.DateTimeFormatOptions = { - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - } - const date = new Date(timestamp) - return date - .toLocaleString(locale, options) - .toLocaleLowerCase(locale) - .replace(/\s(am|pm)/, '$1') + if (monthsPassed > 0) { + return t`${monthsPassed}mo ago` + } else if (daysPassed > 0) { + return t`${daysPassed}d ago` } else if (hoursPassed > 0) { return t`${hoursPassed}h ago` } else if (minutesPassed > 0) { diff --git a/apps/web/src/components/Tokens/TokenDetails/tables/TokenDetailsPoolsTable.tsx b/apps/web/src/components/Tokens/TokenDetails/tables/TokenDetailsPoolsTable.tsx index ec592c79409..87f31b1e53c 100644 --- a/apps/web/src/components/Tokens/TokenDetails/tables/TokenDetailsPoolsTable.tsx +++ b/apps/web/src/components/Tokens/TokenDetails/tables/TokenDetailsPoolsTable.tsx @@ -3,14 +3,17 @@ import { PoolTableColumns, PoolsTable, sortAscendingAtom, sortMethodAtom } from import { usePoolsFromTokenAddress } from 'graphql/data/pools/usePoolsFromTokenAddress' import { OrderDirection } from 'graphql/data/util' import { useAtomValue, useResetAtom } from 'jotai/utils' -import { useEffect } from 'react' +import { useEffect, useMemo } from 'react' const HIDDEN_COLUMNS = [PoolTableColumns.Transactions] export function TokenDetailsPoolsTable({ chainId, referenceToken }: { chainId: ChainId; referenceToken: Token }) { const sortMethod = useAtomValue(sortMethodAtom) const sortAscending = useAtomValue(sortAscendingAtom) - const sortState = { sortBy: sortMethod, sortDirection: sortAscending ? OrderDirection.Asc : OrderDirection.Desc } + const sortState = useMemo( + () => ({ sortBy: sortMethod, sortDirection: sortAscending ? OrderDirection.Asc : OrderDirection.Desc }), + [sortAscending, sortMethod] + ) const { pools, loading, error, loadMore } = usePoolsFromTokenAddress(referenceToken.address, sortState, chainId) const resetSortMethod = useResetAtom(sortMethodAtom) diff --git a/apps/web/src/components/Tokens/TokenDetails/tables/TransactionsTable.tsx b/apps/web/src/components/Tokens/TokenDetails/tables/TransactionsTable.tsx index e24a8e01e6d..095a2556d73 100644 --- a/apps/web/src/components/Tokens/TokenDetails/tables/TransactionsTable.tsx +++ b/apps/web/src/components/Tokens/TokenDetails/tables/TransactionsTable.tsx @@ -80,11 +80,12 @@ export function TransactionsTable({ chainId, referenceToken }: { chainId: ChainI amount: parseFloat(transaction.token1Quantity), token: transaction.token1, } + const token0IsBeingSold = parseFloat(transaction.token0Quantity) < 0 return { hash: transaction.hash, timestamp: transaction.timestamp, - input: swapLeg0, - output: swapLeg1, + input: token0IsBeingSold ? swapLeg0 : swapLeg1, + output: token0IsBeingSold ? swapLeg1 : swapLeg0, usdValue: transaction.usdValue.value, makerAddress: transaction.account, } @@ -100,7 +101,7 @@ export function TransactionsTable({ chainId, referenceToken }: { chainId: ChainI columnHelper.accessor((row) => row, { id: 'timestamp', header: () => ( - + {sortState.sortBy === Swap_OrderBy.Timestamp && } @@ -110,7 +111,7 @@ export function TransactionsTable({ chainId, referenceToken }: { chainId: ChainI ), cell: (row) => ( - + row.makerAddress, { id: 'maker-address', header: () => ( - + Wallet ), cell: (makerAddress) => ( - + - {shortenAddress(makerAddress.getValue?.(), 0)} + {shortenAddress(makerAddress.getValue?.())} ), diff --git a/apps/web/src/components/swap/LimitDisclaimer.tsx b/apps/web/src/components/swap/LimitDisclaimer.tsx new file mode 100644 index 00000000000..4d42e49127f --- /dev/null +++ b/apps/web/src/components/swap/LimitDisclaimer.tsx @@ -0,0 +1,36 @@ +import { Trans } from '@lingui/macro' +import Column from 'components/Column' +import styled from 'styled-components' +import { ExternalLink, ThemedText } from 'theme/components' + +const Container = styled(Column)` + background-color: ${({ theme }) => theme.surface2}; + border-radius: 12px; + padding: 12px; + margin-top: 12px; +` + +const DisclaimerText = styled(ThemedText.LabelMicro)` + line-height: 16px; +` + +export function LimitDisclaimer({ className }: { className?: string }) { + return ( + + + + Please be aware that the execution for limits may vary based on real-time market fluctuations and Ethereum + network congestion. Limits may not execute exactly when tokens reach the specified price. + + + + Canceling a limit has a network cost. + + + + Learn more + + + + ) +} diff --git a/apps/web/src/components/swap/SwapModalFooter.test.tsx b/apps/web/src/components/swap/SwapModalFooter.test.tsx index 6ab7629ea6c..399f5b4dba8 100644 --- a/apps/web/src/components/swap/SwapModalFooter.test.tsx +++ b/apps/web/src/components/swap/SwapModalFooter.test.tsx @@ -126,7 +126,7 @@ describe('SwapModalFooter.tsx', () => { expect(screen.getByText('Network cost')).toBeInTheDocument() expect( screen.getByText( - 'Please be aware that the execution for this limit order may vary based on real-time market fluctuations and Ethereum network congestion. Canceling a limit has a network cost.' + 'Please be aware that the execution for limits may vary based on real-time market fluctuations and Ethereum network congestion. Limits may not execute exactly when tokens reach the specified price.' ) ).toBeInTheDocument() }) diff --git a/apps/web/src/components/swap/SwapModalFooter.tsx b/apps/web/src/components/swap/SwapModalFooter.tsx index 1719beed28d..118a3fec5e8 100644 --- a/apps/web/src/components/swap/SwapModalFooter.tsx +++ b/apps/web/src/components/swap/SwapModalFooter.tsx @@ -22,7 +22,7 @@ import { ExternalLink, Separator, ThemedText } from 'theme/components' import getRoutingDiagramEntries from 'utils/getRoutingDiagramEntries' import { formatSwapButtonClickEventProperties } from 'utils/loggingFormatters' -import { Info } from 'components/Icons/Info' +import { LimitDisclaimer } from 'components/swap/LimitDisclaimer' import { ReactComponent as ExpandoIconClosed } from '../../assets/svg/expando-icon-closed.svg' import { ReactComponent as ExpandoIconOpened } from '../../assets/svg/expando-icon-opened.svg' import { ButtonError, SmallButtonPrimary } from '../Button' @@ -327,12 +327,6 @@ function ExpandableLineItems(props: { trade: InterfaceTrade; allowedSlippage: Pe ) } -const StyledInfoIcon = styled(Info)` - margin-top: 2px; - align-self: flex-start; - flex-shrink: 0; -` - function LimitLineItems({ trade }: { trade: LimitOrderTrade }) { return ( <> @@ -340,18 +334,7 @@ function LimitLineItems({ trade }: { trade: LimitOrderTrade }) { - - - - - Please be aware that the execution for this limit order may vary based on real-time market fluctuations and - Ethereum network congestion. Canceling a limit has a network cost.{' '} - - Learn more - - - - + ) } diff --git a/apps/web/src/constants/routing.test.ts b/apps/web/src/constants/routing.test.ts index 5f95697c926..90dca39c19e 100644 --- a/apps/web/src/constants/routing.test.ts +++ b/apps/web/src/constants/routing.test.ts @@ -22,7 +22,7 @@ describe('Routing', () => { }) it('contains all coins for celo', () => { const symbols = COMMON_BASES[ChainId.CELO].map((coin) => coin.symbol) - expect(symbols).toEqual(['CELO', 'cEUR', 'cUSD', 'ETH', 'USDCet', 'WBTC']) + expect(symbols).toEqual(['CELO', 'cEUR', 'cUSD', 'ETH', 'USDC', 'WBTC']) }) it('contains all coins for bsc', () => { const symbols = COMMON_BASES[ChainId.BNB].map((coin) => coin.symbol) diff --git a/apps/web/src/constants/routing.ts b/apps/web/src/constants/routing.ts index 78633352c8b..8ec41a7c0f2 100644 --- a/apps/web/src/constants/routing.ts +++ b/apps/web/src/constants/routing.ts @@ -16,15 +16,14 @@ import { DAI_OPTIMISM, DAI_POLYGON, ETH_BSC, - nativeOnChain, OP, PORTAL_ETH_CELO, - PORTAL_USDC_CELO, USDC_ARBITRUM, USDC_ARBITRUM_GOERLI, USDC_AVALANCHE, USDC_BASE, USDC_BSC, + USDC_CELO, USDC_MAINNET, USDC_OPTIMISM, USDC_OPTIMISM_GOERLI, @@ -45,6 +44,7 @@ import { WETH_POLYGON, WETH_POLYGON_MUMBAI, WRAPPED_NATIVE_CURRENCY, + nativeOnChain, } from './tokens' type ChainTokenList = { @@ -119,7 +119,7 @@ export const COMMON_BASES: ChainCurrencyList = { WETH_POLYGON_MUMBAI, ], - [ChainId.CELO]: [nativeOnChain(ChainId.CELO), CEUR_CELO, CUSD_CELO, PORTAL_ETH_CELO, PORTAL_USDC_CELO, WBTC_CELO], + [ChainId.CELO]: [nativeOnChain(ChainId.CELO), CEUR_CELO, CUSD_CELO, PORTAL_ETH_CELO, USDC_CELO, WBTC_CELO], [ChainId.CELO_ALFAJORES]: [nativeOnChain(ChainId.CELO_ALFAJORES), CUSD_CELO_ALFAJORES, CEUR_CELO_ALFAJORES], [ChainId.BNB]: [nativeOnChain(ChainId.BNB), DAI_BSC, USDC_BSC, USDT_BSC, ETH_BSC, BTC_BSC, BUSD_BSC], diff --git a/apps/web/src/constants/tokens.ts b/apps/web/src/constants/tokens.ts index 46dd7252a5d..e79c38fff0e 100644 --- a/apps/web/src/constants/tokens.ts +++ b/apps/web/src/constants/tokens.ts @@ -1,4 +1,4 @@ -import { ChainId, Currency, Ether, NativeCurrency, Token, UNI_ADDRESSES, WETH9 } from '@uniswap/sdk-core' +import { ChainId, Currency, NativeCurrency, Token, UNI_ADDRESSES, WETH9 } from '@uniswap/sdk-core' import invariant from 'tiny-invariant' export const NATIVE_CHAIN_ID = 'NATIVE' @@ -54,13 +54,7 @@ export const USDC_POLYGON_MUMBAI = new Token( 'USDC', 'USD Coin' ) -export const PORTAL_USDC_CELO = new Token( - ChainId.CELO, - '0x37f750B7cC259A2f741AF45294f6a16572CF5cAd', - 6, - 'USDCet', - 'USDC (Portal from Ethereum)' -) +export const USDC_CELO = new Token(ChainId.CELO, '0xceba9300f2b948710d2653dd7b07f33a8b32118c', 6, 'USDC', 'USD Coin') export const USDC_BASE = new Token(ChainId.BASE, '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', 6, 'USDC', 'USD Coin') export const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin') @@ -423,18 +417,26 @@ class AvaxNativeCurrency extends NativeCurrency { } } -class ExtendedEther extends Ether { +class ExtendedEther extends NativeCurrency { public get wrapped(): Token { const wrapped = WRAPPED_NATIVE_CURRENCY[this.chainId] if (wrapped) return wrapped throw new Error(`Unsupported chain ID: ${this.chainId}`) } + protected constructor(chainId: number) { + super(chainId, 18, 'ETH', 'Ethereum') + } + private static _cachedExtendedEther: { [chainId: number]: NativeCurrency } = {} public static onChain(chainId: number): ExtendedEther { return this._cachedExtendedEther[chainId] ?? (this._cachedExtendedEther[chainId] = new ExtendedEther(chainId)) } + + public equals(other: Currency): boolean { + return other.isNative && other.chainId === this.chainId + } } const cachedNativeCurrency: { [chainId: number]: NativeCurrency | Token } = {} @@ -466,8 +468,8 @@ export const TOKEN_SHORTHANDS: { [shorthand: string]: { [chainId in ChainId]?: s [ChainId.POLYGON_MUMBAI]: USDC_POLYGON_MUMBAI.address, [ChainId.BNB]: USDC_BSC.address, [ChainId.BASE]: USDC_BASE.address, - [ChainId.CELO]: PORTAL_USDC_CELO.address, - [ChainId.CELO_ALFAJORES]: PORTAL_USDC_CELO.address, + [ChainId.CELO]: USDC_CELO.address, + [ChainId.CELO_ALFAJORES]: USDC_CELO.address, [ChainId.GOERLI]: USDC_GOERLI.address, [ChainId.SEPOLIA]: USDC_SEPOLIA.address, [ChainId.AVALANCHE]: USDC_AVALANCHE.address, @@ -484,8 +486,8 @@ const STABLECOINS: { [chainId in ChainId]: Token[] } = { [ChainId.POLYGON_MUMBAI]: [USDC_POLYGON_MUMBAI], [ChainId.BNB]: [USDC_BSC], [ChainId.BASE]: [USDC_BASE], - [ChainId.CELO]: [PORTAL_USDC_CELO], - [ChainId.CELO_ALFAJORES]: [PORTAL_USDC_CELO], + [ChainId.CELO]: [USDC_CELO], + [ChainId.CELO_ALFAJORES]: [USDC_CELO], [ChainId.GOERLI]: [USDC_GOERLI], [ChainId.SEPOLIA]: [USDC_SEPOLIA], [ChainId.AVALANCHE]: [USDC_AVALANCHE], diff --git a/apps/web/src/graphql/data/Token.ts b/apps/web/src/graphql/data/Token.ts index 1aa286264b2..643c5a78ad6 100644 --- a/apps/web/src/graphql/data/Token.ts +++ b/apps/web/src/graphql/data/Token.ts @@ -56,6 +56,7 @@ gql` address } markets(currencies: [USD]) { + id fullyDilutedValuation { id value diff --git a/apps/web/src/graphql/data/TopTokens.ts b/apps/web/src/graphql/data/TopTokens.ts index fc0733e4993..0a4a5d20281 100644 --- a/apps/web/src/graphql/data/TopTokens.ts +++ b/apps/web/src/graphql/data/TopTokens.ts @@ -72,6 +72,7 @@ gql` id logoUrl markets(currencies: [USD]) { + id fullyDilutedValuation { id value @@ -108,7 +109,7 @@ const TokenSortMethods = { [TokenSortMethod.DEPRECATE_PERCENT_CHANGE]: (a: TopToken, b: TopToken) => (b?.market?.pricePercentChange?.value ?? 0) - (a?.market?.pricePercentChange?.value ?? 0), [TokenSortMethod.DAY_CHANGE]: (a: TopToken, b: TopToken) => - (b?.market?.pricePercentChange?.value ?? 0) - (a?.market?.pricePercentChange?.value ?? 0), + (b?.market?.pricePercentChange1Day?.value ?? 0) - (a?.market?.pricePercentChange1Day?.value ?? 0), [TokenSortMethod.HOUR_CHANGE]: (a: TopToken, b: TopToken) => (b?.market?.pricePercentChange1Hour?.value ?? 0) - (a?.market?.pricePercentChange1Hour?.value ?? 0), [TokenSortMethod.DEPRECATE_TOTAL_VALUE_LOCKED]: (a: TopToken, b: TopToken) => diff --git a/apps/web/src/graphql/data/apollo.ts b/apps/web/src/graphql/data/apollo.ts index bc102c4f66b..0a2660a52e6 100644 --- a/apps/web/src/graphql/data/apollo.ts +++ b/apps/web/src/graphql/data/apollo.ts @@ -20,30 +20,24 @@ export const apolloClient = new ApolloClient({ nftBalances: relayStylePagination(['ownerAddress', 'filter']), nftAssets: relayStylePagination(), nftActivity: relayStylePagination(), - // tell apollo client how to reference Token items in the cache after being fetched by queries that return Token[] token: { + // Tokens should be cached by their chain/address, *not* by the ID returned by the server. + // This is because the ID may change depending on fields requested. read(_, { args, toReference }): Reference | undefined { - return toReference({ - __typename: 'Token', - chain: args?.chain, - address: args?.address, - }) + return toReference({ __typename: 'Token', chain: args?.chain, address: args?.address }) }, }, }, }, Token: { - // key by chain, address combination so that Token(chain, address) endpoint can read from cache - /** - * NOTE: In any query for `token` or `tokens`, you must include the `chain` and `address` fields - * in order for result to normalize properly in the cache. - */ + // Tokens are cached by their chain/address (see Query.fields.token, above). + // In any query for `token` or `tokens`, you *must* include `chain` and `address` fields in order + // to properly normalize the result in the cache. keyFields: ['chain', 'address'], fields: { address: { + // Always cache lowercased for consistency (backend sometimes returns checksummed). read(address: string | null): string | null { - // backend endpoint sometimes returns checksummed, sometimes lowercased addresses - // always use lowercased addresses in our app for consistency return address?.toLowerCase() ?? null }, }, @@ -52,19 +46,27 @@ export const apolloClient = new ApolloClient({ TokenProject: { fields: { tokens: { - // cache data may be lost when replacing the tokens array - merge(existing, incoming) { - if (!existing) { - return incoming - } else if (Array.isArray(existing)) { - return [...existing, ...incoming] + // Cache data may be lost when replacing the tokens array, so retain all known tokens. + merge(existing: unknown[] | undefined, incoming: unknown[] | undefined, { toReference }) { + if (!existing || !incoming) { + return existing ?? incoming } else { - return [existing, ...incoming] + // Arrays must not be concatenated, or the cached array will grow indefinitely. + // Instead, only append *new* elements to the array. + const refs: Reference[] = existing.map((token: any) => toReference(token, true) as Reference) + const refSet = refs.reduce((refSet, ref) => refSet.add(ref.__ref), new Set()) + const newRefs = incoming + .map((token: any) => toReference(token, true) as Reference) + .filter((ref) => !refSet.has(ref.__ref)) + return [...refs, ...newRefs] } }, }, }, }, + TokenMarket: { + keyFields: ['id'], + }, }, }), defaultOptions: { diff --git a/apps/web/src/graphql/data/useAllTransactions.ts b/apps/web/src/graphql/data/useAllTransactions.ts index a90f8d89975..3fabb799937 100644 --- a/apps/web/src/graphql/data/useAllTransactions.ts +++ b/apps/web/src/graphql/data/useAllTransactions.ts @@ -13,7 +13,7 @@ export enum TransactionType { BURN = 'Remove', } -const BETypeToTransactionType: { [key: string]: TransactionType } = { +export const BETypeToTransactionType: { [key: string]: TransactionType } = { [PoolTransactionType.Swap]: TransactionType.SWAP, [PoolTransactionType.Remove]: TransactionType.BURN, [PoolTransactionType.Add]: TransactionType.MINT, diff --git a/apps/web/src/graphql/data/useTokenTransactions.ts b/apps/web/src/graphql/data/useTokenTransactions.ts index 29fb0254b8f..cd919a865c1 100644 --- a/apps/web/src/graphql/data/useTokenTransactions.ts +++ b/apps/web/src/graphql/data/useTokenTransactions.ts @@ -107,14 +107,16 @@ export function useTokenTransactions( () => [ ...(dataV3?.token?.v3Transactions?.filter((tx) => { - const isSell = tx.token0.address?.toLowerCase() === address.toLowerCase() + const tokenBeingSold = parseFloat(tx.token0Quantity) < 0 ? tx.token0 : tx.token1 + const isSell = tokenBeingSold.address?.toLowerCase() === address.toLowerCase() return ( tx.type === PoolTransactionType.Swap && filter.includes(isSell ? TokenTransactionType.SELL : TokenTransactionType.BUY) ) }) ?? []), ...(dataV2?.token?.v2Transactions?.filter((tx) => { - const isSell = tx.token0.address?.toLowerCase() === address.toLowerCase() + const tokenBeingSold = parseFloat(tx.token0Quantity) < 0 ? tx.token0 : tx.token1 + const isSell = tokenBeingSold.address?.toLowerCase() === address.toLowerCase() return ( tx.type === PoolTransactionType.Swap && filter.includes(isSell ? TokenTransactionType.SELL : TokenTransactionType.BUY) diff --git a/apps/web/src/hooks/useArgentWalletContract.ts b/apps/web/src/hooks/useArgentWalletContract.ts index 4c36518b6bd..dc373faf275 100644 --- a/apps/web/src/hooks/useArgentWalletContract.ts +++ b/apps/web/src/hooks/useArgentWalletContract.ts @@ -1,6 +1,6 @@ import { useWeb3React } from '@web3-react/core' -import ArgentWalletContractABI from 'wallet/src/abis/argent-wallet-contract.json' -import { ArgentWalletContract } from 'wallet/src/abis/types' +import ArgentWalletContractABI from 'uniswap/src/abis/argent-wallet-contract.json' +import { ArgentWalletContract } from 'uniswap/src/abis/types' import { useContract } from './useContract' import useIsArgentWallet from './useIsArgentWallet' diff --git a/apps/web/src/hooks/useContract.ts b/apps/web/src/hooks/useContract.ts index d9de5a64596..61c5a0cfda6 100644 --- a/apps/web/src/hooks/useContract.ts +++ b/apps/web/src/hooks/useContract.ts @@ -11,24 +11,23 @@ import { } from '@uniswap/sdk-core' import IUniswapV2PairJson from '@uniswap/v2-core/build/IUniswapV2Pair.json' import IUniswapV2Router02Json from '@uniswap/v2-periphery/build/IUniswapV2Router02.json' -import UniswapInterfaceMulticallJson from '@uniswap/v3-periphery/artifacts/contracts/lens/UniswapInterfaceMulticall.sol/UniswapInterfaceMulticall.json' import NonfungiblePositionManagerJson from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json' import V3MigratorJson from '@uniswap/v3-periphery/artifacts/contracts/V3Migrator.sol/V3Migrator.json' +import UniswapInterfaceMulticallJson from '@uniswap/v3-periphery/artifacts/contracts/lens/UniswapInterfaceMulticall.sol/UniswapInterfaceMulticall.json' import { useWeb3React } from '@web3-react/core' import { sendAnalyticsEvent } from 'analytics' import { DEPRECATED_RPC_PROVIDERS, RPC_PROVIDERS } from 'constants/providers' import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens' import { useFallbackProviderEnabled } from 'featureFlags/flags/fallbackProvider' import { useEffect, useMemo } from 'react' -import { getContract } from 'utilities/src/contracts/getContract' -import ARGENT_WALLET_DETECTOR_ABI from 'wallet/src/abis/argent-wallet-detector.json' -import EIP_2612 from 'wallet/src/abis/eip_2612.json' -import ENS_PUBLIC_RESOLVER_ABI from 'wallet/src/abis/ens-public-resolver.json' -import ENS_ABI from 'wallet/src/abis/ens-registrar.json' -import ERC1155_ABI from 'wallet/src/abis/erc1155.json' -import ERC20_ABI from 'wallet/src/abis/erc20.json' -import ERC20_BYTES32_ABI from 'wallet/src/abis/erc20_bytes32.json' -import ERC721_ABI from 'wallet/src/abis/erc721.json' +import ARGENT_WALLET_DETECTOR_ABI from 'uniswap/src/abis/argent-wallet-detector.json' +import EIP_2612 from 'uniswap/src/abis/eip_2612.json' +import ENS_PUBLIC_RESOLVER_ABI from 'uniswap/src/abis/ens-public-resolver.json' +import ENS_ABI from 'uniswap/src/abis/ens-registrar.json' +import ERC1155_ABI from 'uniswap/src/abis/erc1155.json' +import ERC20_ABI from 'uniswap/src/abis/erc20.json' +import ERC20_BYTES32_ABI from 'uniswap/src/abis/erc20_bytes32.json' +import ERC721_ABI from 'uniswap/src/abis/erc721.json' import { ArgentWalletDetector, EnsPublicResolver, @@ -37,10 +36,11 @@ import { Erc20, Erc721, Weth, -} from 'wallet/src/abis/types' -import { NonfungiblePositionManager, UniswapInterfaceMulticall } from 'wallet/src/abis/types/v3' -import { V3Migrator } from 'wallet/src/abis/types/v3/V3Migrator' -import WETH_ABI from 'wallet/src/abis/weth.json' +} from 'uniswap/src/abis/types' +import { NonfungiblePositionManager, UniswapInterfaceMulticall } from 'uniswap/src/abis/types/v3' +import { V3Migrator } from 'uniswap/src/abis/types/v3/V3Migrator' +import WETH_ABI from 'uniswap/src/abis/weth.json' +import { getContract } from 'utilities/src/contracts/getContract' const { abi: IUniswapV2PairABI } = IUniswapV2PairJson const { abi: IUniswapV2Router02ABI } = IUniswapV2Router02Json diff --git a/apps/web/src/hooks/usePermitAllowance.ts b/apps/web/src/hooks/usePermitAllowance.ts index 970cc67d949..6ca6ad1a141 100644 --- a/apps/web/src/hooks/usePermitAllowance.ts +++ b/apps/web/src/hooks/usePermitAllowance.ts @@ -5,11 +5,11 @@ import { useContract } from 'hooks/useContract' import { useSingleCallResult } from 'lib/hooks/multicall' import ms from 'ms' import { useCallback, useEffect, useMemo, useState } from 'react' +import PERMIT2_ABI from 'uniswap/src/abis/permit2.json' +import { Permit2 } from 'uniswap/src/abis/types' import { UserRejectedRequestError, toReadableError } from 'utils/errors' import { signTypedData } from 'utils/signing' import { didUserReject } from 'utils/swapErrorToUserReadableMessage' -import PERMIT2_ABI from 'wallet/src/abis/permit2.json' -import { Permit2 } from 'wallet/src/abis/types' const PERMIT_EXPIRATION = ms(`30d`) const PERMIT_SIG_EXPIRATION = ms(`30m`) diff --git a/apps/web/src/hooks/usePools.ts b/apps/web/src/hooks/usePools.ts index d09949c17ff..81680db0bd0 100644 --- a/apps/web/src/hooks/usePools.ts +++ b/apps/web/src/hooks/usePools.ts @@ -7,8 +7,8 @@ import { useContractMultichain } from 'components/AccountDrawer/MiniPortfolio/Po import JSBI from 'jsbi' import { useMultipleContractSingleData } from 'lib/hooks/multicall' import { useEffect, useMemo, useState } from 'react' -import { IUniswapV3PoolStateInterface } from 'wallet/src/abis/types/v3/IUniswapV3PoolState' -import { UniswapV3Pool } from 'wallet/src/abis/types/v3/UniswapV3Pool' +import { IUniswapV3PoolStateInterface } from 'uniswap/src/abis/types/v3/IUniswapV3PoolState' +import { UniswapV3Pool } from 'uniswap/src/abis/types/v3/UniswapV3Pool' const POOL_STATE_INTERFACE = new Interface(IUniswapV3PoolStateJSON.abi) as IUniswapV3PoolStateInterface diff --git a/apps/web/src/hooks/useSwapTaxes.ts b/apps/web/src/hooks/useSwapTaxes.ts index 59d9cca1a52..50009f4cc2d 100644 --- a/apps/web/src/hooks/useSwapTaxes.ts +++ b/apps/web/src/hooks/useSwapTaxes.ts @@ -5,8 +5,8 @@ import { useWeb3React } from '@web3-react/core' import { sendAnalyticsEvent } from 'analytics' import { BIPS_BASE, ZERO_PERCENT } from 'constants/misc' import { useEffect, useState } from 'react' -import FOT_DETECTOR_ABI from 'wallet/src/abis/fee-on-transfer-detector.json' -import { FeeOnTransferDetector } from 'wallet/src/abis/types' +import FOT_DETECTOR_ABI from 'uniswap/src/abis/fee-on-transfer-detector.json' +import { FeeOnTransferDetector } from 'uniswap/src/abis/types' import { useContract } from './useContract' diff --git a/apps/web/src/hooks/useTokenContractsConstant.ts b/apps/web/src/hooks/useTokenContractsConstant.ts index dbd2813c779..c2fb03acccd 100644 --- a/apps/web/src/hooks/useTokenContractsConstant.ts +++ b/apps/web/src/hooks/useTokenContractsConstant.ts @@ -1,7 +1,7 @@ import { Interface } from '@ethersproject/abi' import { NEVER_RELOAD, useMultipleContractSingleData } from 'lib/hooks/multicall' -import ERC20ABI from 'wallet/src/abis/erc20.json' -import { Erc20Interface } from 'wallet/src/abis/types/Erc20' +import ERC20ABI from 'uniswap/src/abis/erc20.json' +import { Erc20Interface } from 'uniswap/src/abis/types/Erc20' const ERC20Interface = new Interface(ERC20ABI) as Erc20Interface diff --git a/apps/web/src/hooks/useTransactionGasFee.ts b/apps/web/src/hooks/useTransactionGasFee.ts index 1d7f56c63cd..ff3d5213ed2 100644 --- a/apps/web/src/hooks/useTransactionGasFee.ts +++ b/apps/web/src/hooks/useTransactionGasFee.ts @@ -52,7 +52,7 @@ type TransactionEip1559FeeParams = { gasLimit: string } -interface GasFeeResult { +export interface GasFeeResult { value?: string isLoading: boolean params?: TransactionLegacyFeeParams | TransactionEip1559FeeParams diff --git a/apps/web/src/index.tsx b/apps/web/src/index.tsx index 61bf53868ce..72cbd408bda 100644 --- a/apps/web/src/index.tsx +++ b/apps/web/src/index.tsx @@ -11,7 +11,7 @@ import { BlockNumberProvider } from 'lib/hooks/useBlockNumber' import { MulticallUpdater } from 'lib/state/multicall' import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' -import { Helmet } from 'react-helmet' +import { Helmet, HelmetProvider } from 'react-helmet-async/lib/index' import { QueryClient, QueryClientProvider } from 'react-query' import { Provider } from 'react-redux' import { BrowserRouter, HashRouter, useLocation } from 'react-router-dom' @@ -66,31 +66,33 @@ const Router = isBrowserRouterEnabled() ? BrowserRouter : HashRouter createRoot(container).render( - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + ) diff --git a/apps/web/src/lib/hooks/useCurrencyBalance.ts b/apps/web/src/lib/hooks/useCurrencyBalance.ts index 2f846fa1752..e3ec58dcb8c 100644 --- a/apps/web/src/lib/hooks/useCurrencyBalance.ts +++ b/apps/web/src/lib/hooks/useCurrencyBalance.ts @@ -4,8 +4,8 @@ import { useWeb3React } from '@web3-react/core' import JSBI from 'jsbi' import { useMultipleContractSingleData, useSingleContractMultipleData } from 'lib/hooks/multicall' import { useMemo } from 'react' -import ERC20ABI from 'wallet/src/abis/erc20.json' -import { Erc20Interface } from 'wallet/src/abis/types/Erc20' +import ERC20ABI from 'uniswap/src/abis/erc20.json' +import { Erc20Interface } from 'uniswap/src/abis/types/Erc20' import { isAddress } from 'utilities/src/addresses' import { nativeOnChain } from '../../constants/tokens' diff --git a/apps/web/src/nft/hooks/useSendTransaction.ts b/apps/web/src/nft/hooks/useSendTransaction.ts index e99857e58fc..98ddf99a745 100644 --- a/apps/web/src/nft/hooks/useSendTransaction.ts +++ b/apps/web/src/nft/hooks/useSendTransaction.ts @@ -5,8 +5,8 @@ import { ContractReceipt } from '@ethersproject/contracts' import type { JsonRpcSigner } from '@ethersproject/providers' import { NFTEventName } from '@uniswap/analytics-events' import { sendAnalyticsEvent } from 'analytics' -import ERC1155 from 'wallet/src/abis/erc1155.json' -import ERC721 from 'wallet/src/abis/erc721.json' +import ERC1155 from 'uniswap/src/abis/erc1155.json' +import ERC721 from 'uniswap/src/abis/erc721.json' import { create } from 'zustand' import { devtools } from 'zustand/middleware' diff --git a/apps/web/src/nft/pages/asset/Asset.tsx b/apps/web/src/nft/pages/asset/Asset.tsx index b9dd63a378c..d6711a9a856 100644 --- a/apps/web/src/nft/pages/asset/Asset.tsx +++ b/apps/web/src/nft/pages/asset/Asset.tsx @@ -5,7 +5,7 @@ import { useNftAssetDetails } from 'graphql/data/nft/Details' import { AssetDetails } from 'nft/components/details/AssetDetails' import { AssetDetailsLoading } from 'nft/components/details/AssetDetailsLoading' import { AssetPriceDetails } from 'nft/components/details/AssetPriceDetails' -import { Helmet } from 'react-helmet' +import { Helmet } from 'react-helmet-async/lib/index' import { useParams } from 'react-router-dom' import styled from 'styled-components' diff --git a/apps/web/src/nft/pages/collection/index.tsx b/apps/web/src/nft/pages/collection/index.tsx index 389eefa2302..0fa3eeb7bb8 100644 --- a/apps/web/src/nft/pages/collection/index.tsx +++ b/apps/web/src/nft/pages/collection/index.tsx @@ -19,7 +19,7 @@ import { useBag, useCollectionFilters, useFiltersExpanded, useIsMobile } from 'n import * as styles from 'nft/pages/collection/index.css' import { blocklistedCollections } from 'nft/utils' import { Suspense, useEffect } from 'react' -import { Helmet } from 'react-helmet' +import { Helmet } from 'react-helmet-async/lib/index' import { useLocation, useNavigate, useParams } from 'react-router-dom' import { animated, easings, useSpring } from 'react-spring' import styled from 'styled-components' diff --git a/apps/web/src/nft/pages/profile/index.tsx b/apps/web/src/nft/pages/profile/index.tsx index 696f0c53f66..a585b56b0f8 100644 --- a/apps/web/src/nft/pages/profile/index.tsx +++ b/apps/web/src/nft/pages/profile/index.tsx @@ -11,7 +11,7 @@ import { ProfilePage } from 'nft/components/profile/view/ProfilePage' import { useBag, useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks' import { ProfilePageStateType } from 'nft/types' import { useEffect, useRef } from 'react' -import { Helmet } from 'react-helmet' +import { Helmet } from 'react-helmet-async/lib/index' import styled from 'styled-components' import { BREAKPOINTS } from 'theme' import { ThemedText } from 'theme/components' diff --git a/apps/web/src/nft/utils/blocklist.ts b/apps/web/src/nft/utils/blocklist.ts index 101b0396ff3..c90db36db52 100644 --- a/apps/web/src/nft/utils/blocklist.ts +++ b/apps/web/src/nft/utils/blocklist.ts @@ -10,4 +10,5 @@ export const blocklistedCollections = [ '0x8e52fb89b6311bd9ec36bd7cea9a0c311fd27a92', '0x2079c2765462af6d78a9ccbddb6ff3c6d4ba2e24', '0xd4d871419714b778ebec2e22c7c53572b573706e', + '0x7219f3a405844a4173ac822ee18994823bec2b4f', ] diff --git a/apps/web/src/nft/utils/listNfts.ts b/apps/web/src/nft/utils/listNfts.ts index 54bee70480a..0b4f41a116d 100644 --- a/apps/web/src/nft/utils/listNfts.ts +++ b/apps/web/src/nft/utils/listNfts.ts @@ -20,8 +20,8 @@ import { OPENSEA_SEAPORT_V1_5_CONTRACT, } from 'nft/queries/openSea/constants' import { getX2Y2OrderId, newX2Y2Order } from 'nft/queries/x2y2' -import ERC1155 from 'wallet/src/abis/erc1155.json' -import ERC721 from 'wallet/src/abis/erc721.json' +import ERC1155 from 'uniswap/src/abis/erc1155.json' +import ERC721 from 'uniswap/src/abis/erc721.json' import { ListingMarket, ListingStatus, WalletAsset } from '../types' import { OfferItem, OrderPayload, createSellOrder, encodeOrder, signOrderData } from './x2y2' diff --git a/apps/web/src/pages/App.tsx b/apps/web/src/pages/App.tsx index ffc026cd2d5..b0e8fcd078b 100644 --- a/apps/web/src/pages/App.tsx +++ b/apps/web/src/pages/App.tsx @@ -9,7 +9,7 @@ import { useFeatureFlagsIsLoaded, useFeatureFlagURLOverrides } from 'featureFlag import { useAtom } from 'jotai' import { useBag } from 'nft/hooks/useBag' import { lazy, memo, Suspense, useEffect, useLayoutEffect, useMemo, useState } from 'react' -import { Helmet } from 'react-helmet' +import { Helmet } from 'react-helmet-async/lib/index' import { Navigate, Route, Routes, useLocation, useSearchParams } from 'react-router-dom' import { shouldDisableNFTRoutesAtom } from 'state/application/atoms' import { useAppSelector } from 'state/hooks' @@ -147,7 +147,7 @@ export default function App() { {/* This is where *static* page titles are injected into the tag. If you want to set a page title based on data that's dynamic or not available on first render, - you can set it later in the page component itself, since react-helmet prefers the most recently rendered title. + you can set it later in the page component itself, since react-helmet-async prefers the most recently rendered title. */} {findRouteByPath(pathname)?.getTitle(pathname) ?? 'Uniswap Interface'} diff --git a/apps/web/src/pages/Explore/charts/ExploreChartsSection.tsx b/apps/web/src/pages/Explore/charts/ExploreChartsSection.tsx index 35ec90cebb9..deae060a115 100644 --- a/apps/web/src/pages/Explore/charts/ExploreChartsSection.tsx +++ b/apps/web/src/pages/Explore/charts/ExploreChartsSection.tsx @@ -144,9 +144,7 @@ function VolumeChartSection({ chainId }: { chainId: number }) { {(() => { if (dataQuality === DataQuality.INVALID) { const errorText = loading ? undefined : ( - - Unable to display historical volume data for the current chain. - + Unable to display historical volume data for the current chain. ) return ( @@ -196,9 +194,7 @@ function TVLChartSection({ chainId }: { chainId: number }) { {(() => { if (dataQuality === DataQuality.INVALID) { const errorText = loading ? undefined : ( - - Unable to display historical TVL data for the current chain. - + Unable to display historical TVL data for the current chain. ) return } diff --git a/apps/web/src/pages/Explore/tables/RecentTransactions.tsx b/apps/web/src/pages/Explore/tables/RecentTransactions.tsx index 62f939d0551..6978c5250ba 100644 --- a/apps/web/src/pages/Explore/tables/RecentTransactions.tsx +++ b/apps/web/src/pages/Explore/tables/RecentTransactions.tsx @@ -13,7 +13,7 @@ import { TokenLinkCell, } from 'components/Table/styled' import { PoolTransaction, PoolTransactionType } from 'graphql/data/__generated__/types-and-hooks' -import { TransactionType, useAllTransactions } from 'graphql/data/useAllTransactions' +import { BETypeToTransactionType, TransactionType, useAllTransactions } from 'graphql/data/useAllTransactions' import { supportedChainIdFromGQLChain, validateUrlChainParam } from 'graphql/data/util' import { OrderDirection, Transaction_OrderBy } from 'graphql/thegraph/__generated__/types-and-hooks' import { useActiveLocalCurrency } from 'hooks/useActiveLocalCurrency' @@ -55,7 +55,7 @@ export default function RecentTransactions() { columnHelper.accessor((transaction) => transaction, { id: 'timestamp', header: () => ( - + {sortState.sortBy === Transaction_OrderBy.Timestamp && ( @@ -67,7 +67,7 @@ export default function RecentTransactions() { ), cell: (transaction) => ( - + transaction, { id: 'swap-type', header: () => ( - + toggleFilterModal()}> ), cell: (transaction) => ( - + - {transaction.getValue?.().type} + + {BETypeToTransactionType[transaction.getValue?.().type]} + {transaction.getValue?.().type === PoolTransactionType.Swap ? for : and} @@ -167,16 +169,16 @@ export default function RecentTransactions() { columnHelper.accessor((transaction) => transaction.account, { id: 'maker-address', header: () => ( - + Wallet ), cell: (makerAddress) => ( - + - {shortenAddress(makerAddress.getValue?.(), 0)} + {shortenAddress(makerAddress.getValue?.())} ), diff --git a/apps/web/src/pages/Pool/PositionPage.tsx b/apps/web/src/pages/Pool/PositionPage.tsx index 70dfe45ff8c..dbd29607fa3 100644 --- a/apps/web/src/pages/Pool/PositionPage.tsx +++ b/apps/web/src/pages/Pool/PositionPage.tsx @@ -30,7 +30,7 @@ import { useV3PositionFromTokenId } from 'hooks/useV3Positions' import { useSingleCallResult } from 'lib/hooks/multicall' import useNativeCurrency from 'lib/hooks/useNativeCurrency' import { PropsWithChildren, useCallback, useMemo, useRef, useState } from 'react' -import { Helmet } from 'react-helmet' +import { Helmet } from 'react-helmet-async/lib/index' import { Link, useParams } from 'react-router-dom' import { Bound } from 'state/mint/v3/actions' import { useIsTransactionPending, useTransactionAdder } from 'state/transactions/hooks' diff --git a/apps/web/src/pages/PoolDetails/index.tsx b/apps/web/src/pages/PoolDetails/index.tsx index 834a9bb0b6c..5f742459a72 100644 --- a/apps/web/src/pages/PoolDetails/index.tsx +++ b/apps/web/src/pages/PoolDetails/index.tsx @@ -16,7 +16,7 @@ import { useColor } from 'hooks/useColor' import NotFound from 'pages/NotFound' import { getPoolDetailPageTitle } from 'pages/PoolDetails/utils' import { useReducer } from 'react' -import { Helmet } from 'react-helmet' +import { Helmet } from 'react-helmet-async/lib/index' import { useParams } from 'react-router-dom' import { Text } from 'rebass' import styled, { useTheme } from 'styled-components' diff --git a/apps/web/src/pages/Swap/Limit/LimitForm.tsx b/apps/web/src/pages/Swap/Limit/LimitForm.tsx index 4f22478d61d..95e988b3469 100644 --- a/apps/web/src/pages/Swap/Limit/LimitForm.tsx +++ b/apps/web/src/pages/Swap/Limit/LimitForm.tsx @@ -38,10 +38,13 @@ import { maxAmountSpend } from 'utils/maxAmountSpend' import { MenuState, miniPortfolioMenuStateAtom } from 'components/AccountDrawer/DefaultMenu' import { OpenLimitOrdersButton } from 'components/AccountDrawer/MiniPortfolio/Limits/OpenLimitOrdersButton' import { useCurrentPriceAdjustment } from 'components/CurrencyInputPanel/LimitPriceInputPanel/useCurrentPriceAdjustment' +import Row from 'components/Row' import { CurrencySearchFilters } from 'components/SearchModal/CurrencySearch' import { useAtom } from 'jotai' import { LimitPriceError } from 'pages/Swap/Limit/LimitPriceError' import { getDefaultPriceInverted } from 'state/limit/hooks' +import { ExternalLink, ThemedText } from 'theme/components' +import { AlertTriangle } from 'ui/src/components/icons' import { LimitExpirySection } from './LimitExpirySection' const CustomHeightSwapSection = styled(SwapSection)` @@ -53,6 +56,24 @@ const ShortArrowWrapper = styled(ArrowWrapper)` margin-bottom: -22px; ` +const StyledAlertIcon = styled(AlertTriangle)` + align-self: flex-start; + flex-shrink: 0; + margin-right: 12px; + fill: ${({ theme }) => theme.neutral2}; +` + +const LimitDisclaimerContainer = styled(Row)` + background-color: ${({ theme }) => theme.surface2}; + border-radius: 12px; + padding: 12px; + margin-top: 12px; +` + +const DisclaimerText = styled(ThemedText.LabelSmall)` + line-height: 20px; +` + export const LIMIT_FORM_CURRENCY_SEARCH_FILTERS: CurrencySearchFilters = { showCommonBases: true, } @@ -330,6 +351,26 @@ function LimitForm({ onCurrencyChange }: LimitFormProps) { priceInverted={limitState.limitPriceInverted} /> )} + {account && ( + { + setMenu(MenuState.LIMITS) + openAccountDrawer() + }} + /> + )} + + + + + Limits may not execute exactly when tokens reach the specified price.{' '} + + Learn more + + + + {limitOrderTrade && showConfirm && ( )} - {account && ( - { - setMenu(MenuState.LIMITS) - openAccountDrawer() - }} - /> - )} ) } diff --git a/apps/web/src/pages/TokenDetails/index.tsx b/apps/web/src/pages/TokenDetails/index.tsx index 6c7ea803c80..74bf524fdba 100644 --- a/apps/web/src/pages/TokenDetails/index.tsx +++ b/apps/web/src/pages/TokenDetails/index.tsx @@ -16,7 +16,7 @@ import { useCurrency } from 'hooks/Tokens' import { useSrcColor } from 'hooks/useColor' import { UNKNOWN_TOKEN_SYMBOL } from 'lib/hooks/useCurrency' import { useMemo } from 'react' -import { Helmet } from 'react-helmet' +import { Helmet } from 'react-helmet-async/lib/index' import { useLocation, useParams } from 'react-router-dom' import styled, { useTheme } from 'styled-components' import { ThemeProvider } from 'theme' diff --git a/apps/web/src/state/governance/hooks.ts b/apps/web/src/state/governance/hooks.ts index f2d695fffad..3686bf6dcd0 100644 --- a/apps/web/src/state/governance/hooks.ts +++ b/apps/web/src/state/governance/hooks.ts @@ -23,8 +23,8 @@ import { UNISWAP_GRANTS_PROPOSAL_DESCRIPTION } from 'constants/proposals/uniswap import { useContract } from 'hooks/useContract' import { useSingleCallResult, useSingleContractMultipleData } from 'lib/hooks/multicall' import { useCallback, useMemo } from 'react' +import GOVERNOR_BRAVO_ABI from 'uniswap/src/abis/governor-bravo.json' import { calculateGasMargin } from 'utils/calculateGasMargin' -import GOVERNOR_BRAVO_ABI from 'wallet/src/abis/governor-bravo.json' import { BRAVO_START_BLOCK, diff --git a/apps/web/src/state/routing/gas.ts b/apps/web/src/state/routing/gas.ts index 73ca32754f2..8475f26e646 100644 --- a/apps/web/src/state/routing/gas.ts +++ b/apps/web/src/state/routing/gas.ts @@ -3,10 +3,10 @@ import { ChainId, Currency } from '@uniswap/sdk-core' import { SupportedInterfaceChain } from 'constants/chains' import { DEPRECATED_RPC_PROVIDERS } from 'constants/providers' import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens' +import ERC20_ABI from 'uniswap/src/abis/erc20.json' +import { Erc20, Weth } from 'uniswap/src/abis/types' +import WETH_ABI from 'uniswap/src/abis/weth.json' import { getContract } from 'utilities/src/contracts/getContract' -import ERC20_ABI from 'wallet/src/abis/erc20.json' -import { Erc20, Weth } from 'wallet/src/abis/types' -import WETH_ABI from 'wallet/src/abis/weth.json' import { ApproveInfo, WrapInfo } from './types' diff --git a/apps/web/src/test-utils/render.tsx b/apps/web/src/test-utils/render.tsx index ebc2497a9ba..31ab5ae5800 100644 --- a/apps/web/src/test-utils/render.tsx +++ b/apps/web/src/test-utils/render.tsx @@ -8,6 +8,7 @@ import { BlockNumberProvider } from 'lib/hooks/useBlockNumber' import catalog from 'locales/en-US' import { en } from 'make-plural/plurals' import { ReactElement, ReactNode } from 'react' +import { HelmetProvider } from 'react-helmet-async/lib/index' import { QueryClient, QueryClientProvider } from 'react-query' import { Provider } from 'react-redux' import { BrowserRouter } from 'react-router-dom' @@ -29,27 +30,29 @@ const queryClient = new QueryClient() const WithProviders = ({ children }: { children?: ReactNode }) => { return ( - - - - - {/* - * Web3Provider is mocked through setupTests.ts - * To test behavior that depends on Web3Provider, use jest.unmock('@web3-react/core') - */} - - - - - {children} - - - - - - - - + + + + + + {/* + * Web3Provider is mocked through setupTests.ts + * To test behavior that depends on Web3Provider, use jest.unmock('@web3-react/core') + */} + + + + + {children} + + + + + + + + + ) } diff --git a/apps/web/src/utils/approveAmountCalldata.ts b/apps/web/src/utils/approveAmountCalldata.ts index b7b15f4de4a..63a54fa5e31 100644 --- a/apps/web/src/utils/approveAmountCalldata.ts +++ b/apps/web/src/utils/approveAmountCalldata.ts @@ -1,7 +1,7 @@ import { Interface } from '@ethersproject/abi' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { toHex } from '@uniswap/v3-sdk' -import { Erc20Interface } from 'wallet/src/abis/types/Erc20' +import { Erc20Interface } from 'uniswap/src/abis/types/Erc20' const ERC20_INTERFACE = new Interface([ { diff --git a/apps/web/src/utils/transfer.ts b/apps/web/src/utils/transfer.ts index 21a8a132b50..e3a3c5dc04c 100644 --- a/apps/web/src/utils/transfer.ts +++ b/apps/web/src/utils/transfer.ts @@ -4,9 +4,9 @@ import { ChainId, Currency, CurrencyAmount } from '@uniswap/sdk-core' import { useWeb3React } from '@web3-react/core' import { useAsyncData } from 'hooks/useAsyncData' import { useCallback } from 'react' +import ERC20_ABI from 'uniswap/src/abis/erc20.json' +import { Erc20 } from 'uniswap/src/abis/types' import { getContract } from 'utilities/src/contracts/getContract' -import ERC20_ABI from 'wallet/src/abis/erc20.json' -import { Erc20 } from 'wallet/src/abis/types' interface TransferInfo { provider?: Web3Provider diff --git a/i18next-parser.config.js b/i18next-parser.config.js index 1a0ac352b00..07826949a43 100644 --- a/i18next-parser.config.js +++ b/i18next-parser.config.js @@ -18,8 +18,7 @@ module.exports = { keepRemoved: false, // Key separator used in your translation keys - // Disabling instead of default '.' so we can use plain english keys - keySeparator: false, + keySeparator: '.', // see below for more details lexers: { @@ -72,10 +71,6 @@ module.exports = { // Whether or not to sort the catalog. Can also be a [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#parameters) sort: true, - defaultValue: function (locale, namespace, key, value) { - return key - }, - // Display info about the parsing including some stats verbose: false, diff --git a/index.d.ts b/index.d.ts index 0d190adc9dc..fb72858f806 100644 --- a/index.d.ts +++ b/index.d.ts @@ -7,3 +7,15 @@ declare type Maybe = Nullable | undefined declare type Primitive = number | string | boolean | bigint | symbol | null | undefined declare type ValuesOf = T[number] + +declare type ArrayOfLength = Acc['length'] extends L + ? Acc extends [] + ? L extends 0 + ? [] + : T[] + : Acc + : ArrayOfLength + +declare type Require = T & Required> + +declare type RequireNonNullable = T & { [P in K]-?: NonNullable } diff --git a/package.json b/package.json index 6ed2e77f1dc..559d99e94f7 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "g:lint": "turbo run lint --parallel", "g:lint:fix": "turbo run lint:fix --parallel", "g:prepare": "turbo run prepare --parallel", - "g:rm:local-packages": "rm -rf ./node_modules/utilities ./node_modules/wallet ./node_modules/ui", + "g:rm:local-packages": "rm -rf ./node_modules/utilities ./node_modules/wallet ./node_modules/ui ./node_modules/uniswap", "g:rm:nodemodules": "rm -rf node_modules", "g:run-fast-checks": "turbo run typecheck lint build --parallel --filter=\"[HEAD]\"", "g:run-all-checks": "turbo run typecheck lint test build check:circular --parallel", diff --git a/packages/eslint-config/native.js b/packages/eslint-config/native.js index d106da37e09..9970718daaa 100644 --- a/packages/eslint-config/native.js +++ b/packages/eslint-config/native.js @@ -302,6 +302,8 @@ module.exports = { 'abcabcabc', 'abc', 'aaa', + 'biometrics', + 'cta', 'They’re', '’s', 'device’s', @@ -320,6 +322,7 @@ module.exports = { 'cancelling', 'can’t', 'dapp', + 'dapps', 'don’t', 'eth', 'etherscan', @@ -327,11 +330,19 @@ module.exports = { 'haven’t', 'isn’t', 'it’s', + 'otp', 'num', + 'nft', + 'nfts', + 'scantastic', 'they’ll', + 'tooltip', + 'unformatted', 'unhidden', 'unhide', + 'upsell', 'usd', + 'uwu', 'wallet’s', 'we’re', 'won’t', @@ -341,8 +352,6 @@ module.exports = { 'Arbitrum', 'blockchain', 'validators', - 'Naira', - 'Hryvnia', 'customizable', 'subdomains', 'unitag', @@ -354,6 +363,42 @@ module.exports = { 'Unitags', 'Uw', 'Passcode', + + // currencies and countries + 'aud', + 'brl', + 'cny', + 'eur', + 'gbp', + 'hkd', + 'idr', + 'inr', + 'jpy', + 'ngn', + 'pkr', + 'sgd', + 'thb', + 'uah', + 'vnd', + 'spanish', + 'Latam', + 'chinese', + 'english', + 'hindi', + 'indonesian', + 'japanese', + 'malay', + 'portuguese', + 'russian', + 'spanish', + 'spanish', + 'thai', + 'turkish', + 'ukrainian', + 'urdu', + 'vietnamese', + 'Naira', + 'Hryvnia', ], }, ], diff --git a/packages/uniswap/.depcheckrc b/packages/uniswap/.depcheckrc new file mode 100644 index 00000000000..a143c871851 --- /dev/null +++ b/packages/uniswap/.depcheckrc @@ -0,0 +1,11 @@ +ignores: [ + # Dependencies that depcheck thinks are unused but are actually used + "ethers", + "@typechain/ethers-v5", + "jest-presets", + # Dependencies that depcheck thinks are missing but are actually present or never used + ## Internal packages / workspaces + "uniswap", + "src", + "tsconfig", + ] diff --git a/packages/uniswap/.eslintignore b/packages/uniswap/.eslintignore new file mode 100644 index 00000000000..41d48b2e3f2 --- /dev/null +++ b/packages/uniswap/.eslintignore @@ -0,0 +1,2 @@ +# Generated contract types +src/abis/types diff --git a/packages/uniswap/.eslintrc.js b/packages/uniswap/.eslintrc.js new file mode 100644 index 00000000000..b859d3c44d4 --- /dev/null +++ b/packages/uniswap/.eslintrc.js @@ -0,0 +1,29 @@ +module.exports = { + root: true, + extends: ['@uniswap/eslint-config/native'], + ignorePatterns: ['node_modules', '.turbo', '.eslintrc.js', 'codegen.ts'], + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 2018, + sourceType: 'module', + }, + overrides: [ + { + files: ['*.ts', '*.tsx', '*.js', '*.jsx'], + rules: {}, + }, + { + files: ['*.ts', '*.tsx'], + rules: {}, + }, + { + files: ['*.js', '*.jsx'], + rules: {}, + }, + ], + rules: {}, +} diff --git a/packages/uniswap/.gitignore b/packages/uniswap/.gitignore new file mode 100644 index 00000000000..9687411666f --- /dev/null +++ b/packages/uniswap/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +.DS_Store +THUMBS_DB +tsconfig.tsbuildinfo + +# Generated contract types +src/abis/types diff --git a/packages/uniswap/babel.config.js b/packages/uniswap/babel.config.js new file mode 100644 index 00000000000..125708dbd06 --- /dev/null +++ b/packages/uniswap/babel.config.js @@ -0,0 +1,35 @@ +const { NODE_ENV } = process.env + +const inProduction = NODE_ENV === 'production' + +module.exports = function (api) { + api.cache.using(() => process.env.NODE_ENV) + + var plugins = [ + // https://github.com/software-mansion/react-native-reanimated/issues/3364#issuecomment-1268591867 + '@babel/plugin-proposal-export-namespace-from', + [ + 'react-native-reanimated/plugin', + { + globals: ['__scanCodes', '__scanOCR'], + }, + ], + 'transform-inline-environment-variables', + // TypeScript compiles this, but in production builds, metro doesn't use tsc + '@babel/plugin-proposal-logical-assignment-operators', + // metro doesn't like these + '@babel/plugin-proposal-numeric-separator', + // automatically require React when using JSX + 'react-require', + ] + + if (inProduction) { + // Remove all console statements in production + plugins = [...plugins, 'transform-remove-console'] + } + + return { + presets: ['module:metro-react-native-babel-preset'], + plugins, + } +} diff --git a/packages/uniswap/jest-setup.js b/packages/uniswap/jest-setup.js new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/packages/uniswap/jest-setup.js @@ -0,0 +1 @@ + diff --git a/packages/uniswap/jest.config.js b/packages/uniswap/jest.config.js new file mode 100644 index 00000000000..c4da583fa31 --- /dev/null +++ b/packages/uniswap/jest.config.js @@ -0,0 +1,34 @@ +// this allows us to use es6, es2017, es2018 syntax (const, spread operators outside of array literals, etc.) +/* eslint-env es6, es2017, es2018 */ + +const preset = require('../../config/jest-presets/jest/jest-preset') + +module.exports = { + ...preset, + preset: 'jest-expo', + displayName: 'Uniswap Package', + collectCoverageFrom: [ + 'src/**/*.{js,ts,tsx}', + '!src/**/*.stories.**', + '!src/abis/**', // auto-generated abis + '!**/node_modules/**', + ], + coverageThreshold: { + global: { + lines: 0, + }, + }, + haste: { + defaultPlatform: 'ios', + // avoid native because wallet tests assume no .native.ts + platforms: ['web', 'ios', 'android'], + }, + setupFiles: [ + './jest-setup.js', + ], + // we map core to tamagui's test bundle, this just makes setup simpler for jest + moduleNameMapper: { + ...preset.moduleNameMapper, + '@tamagui/core': '@tamagui/core/native-test', + }, +} diff --git a/packages/uniswap/package.json b/packages/uniswap/package.json new file mode 100644 index 00000000000..68e08a4a935 --- /dev/null +++ b/packages/uniswap/package.json @@ -0,0 +1,34 @@ +{ + "name": "uniswap", + "version": "0.0.0", + "scripts": { + "prepare": "yarn contracts", + "contracts:compile:abi": "typechain --target ethers-v5 --out-dir src/abis/types \"./src/abis/**/*.json\"", + "contracts:compile:v3": "typechain --target ethers-v5 --out-dir src/abis/types/v3 \"../../node_modules/@uniswap/**/artifacts/contracts/**/*[!dbg].json\"", + "contracts": "yarn contracts:compile:abi && yarn contracts:compile:v3", + "check:deps:usage": "depcheck", + "lint": "eslint . --ext ts,tsx --max-warnings=0", + "lint:fix": "eslint . --ext ts,tsx --fix", + "test": "jest --passWithNoTests", + "snapshots": "jest -u", + "typecheck": "tsc -b" + }, + "dependencies": { + "@typechain/ethers-v5": "7.2.0", + "ethers": "5.7.2" + }, + "devDependencies": { + "@uniswap/eslint-config": "workspace:^", + "depcheck": "1.4.7", + "eslint": "8.44.0", + "jest": "29.6.4", + "jest-presets": "workspace:^", + "typechain": "5.2.0", + "typescript": "5.3.3" + }, + "main": "src/index.ts", + "private": true, + "sideEffects": [ + "*.css" + ] +} diff --git a/packages/wallet/src/abis/argent-wallet-contract.json b/packages/uniswap/src/abis/argent-wallet-contract.json similarity index 100% rename from packages/wallet/src/abis/argent-wallet-contract.json rename to packages/uniswap/src/abis/argent-wallet-contract.json diff --git a/packages/wallet/src/abis/argent-wallet-detector.json b/packages/uniswap/src/abis/argent-wallet-detector.json similarity index 100% rename from packages/wallet/src/abis/argent-wallet-detector.json rename to packages/uniswap/src/abis/argent-wallet-detector.json diff --git a/packages/wallet/src/abis/eip_2612.json b/packages/uniswap/src/abis/eip_2612.json similarity index 100% rename from packages/wallet/src/abis/eip_2612.json rename to packages/uniswap/src/abis/eip_2612.json diff --git a/packages/wallet/src/abis/ens-public-resolver.json b/packages/uniswap/src/abis/ens-public-resolver.json similarity index 100% rename from packages/wallet/src/abis/ens-public-resolver.json rename to packages/uniswap/src/abis/ens-public-resolver.json diff --git a/packages/wallet/src/abis/ens-registrar.json b/packages/uniswap/src/abis/ens-registrar.json similarity index 100% rename from packages/wallet/src/abis/ens-registrar.json rename to packages/uniswap/src/abis/ens-registrar.json diff --git a/packages/wallet/src/abis/erc1155.json b/packages/uniswap/src/abis/erc1155.json similarity index 100% rename from packages/wallet/src/abis/erc1155.json rename to packages/uniswap/src/abis/erc1155.json diff --git a/packages/wallet/src/abis/erc20.json b/packages/uniswap/src/abis/erc20.json similarity index 100% rename from packages/wallet/src/abis/erc20.json rename to packages/uniswap/src/abis/erc20.json diff --git a/packages/wallet/src/abis/erc20_bytes32.json b/packages/uniswap/src/abis/erc20_bytes32.json similarity index 100% rename from packages/wallet/src/abis/erc20_bytes32.json rename to packages/uniswap/src/abis/erc20_bytes32.json diff --git a/packages/wallet/src/abis/erc721.json b/packages/uniswap/src/abis/erc721.json similarity index 100% rename from packages/wallet/src/abis/erc721.json rename to packages/uniswap/src/abis/erc721.json diff --git a/packages/wallet/src/abis/fee-on-transfer-detector.json b/packages/uniswap/src/abis/fee-on-transfer-detector.json similarity index 100% rename from packages/wallet/src/abis/fee-on-transfer-detector.json rename to packages/uniswap/src/abis/fee-on-transfer-detector.json diff --git a/packages/wallet/src/abis/governor-bravo.json b/packages/uniswap/src/abis/governor-bravo.json similarity index 100% rename from packages/wallet/src/abis/governor-bravo.json rename to packages/uniswap/src/abis/governor-bravo.json diff --git a/packages/wallet/src/abis/permit2.json b/packages/uniswap/src/abis/permit2.json similarity index 100% rename from packages/wallet/src/abis/permit2.json rename to packages/uniswap/src/abis/permit2.json diff --git a/packages/wallet/src/abis/uniswap-nft-airdrop-claim.json b/packages/uniswap/src/abis/uniswap-nft-airdrop-claim.json similarity index 100% rename from packages/wallet/src/abis/uniswap-nft-airdrop-claim.json rename to packages/uniswap/src/abis/uniswap-nft-airdrop-claim.json diff --git a/packages/wallet/src/abis/weth.json b/packages/uniswap/src/abis/weth.json similarity index 100% rename from packages/wallet/src/abis/weth.json rename to packages/uniswap/src/abis/weth.json diff --git a/packages/uniswap/src/index.ts b/packages/uniswap/src/index.ts new file mode 100644 index 00000000000..a90d1918da3 --- /dev/null +++ b/packages/uniswap/src/index.ts @@ -0,0 +1,7 @@ +// eslint-disable-next-line @typescript-eslint/triple-slash-reference +/// + +// leave this blank +// don't re-export files from this workspace. it'll break next.js tree shaking +// https://github.com/vercel/next.js/issues/12557 +export {} diff --git a/packages/uniswap/tsconfig.json b/packages/uniswap/tsconfig.json new file mode 100644 index 00000000000..f5cf6a96517 --- /dev/null +++ b/packages/uniswap/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "tsconfig/app.json", + "include": [ + "src/**/*.ts", + "src/**/*.d.ts", + "src/**/*.tsx", + "src/**/*.json" + ], + "references": [ + { + "path": "../ui" + }, + ], + "compilerOptions": { + "types": [ + "jest" + ] + } +} diff --git a/packages/utilities/src/time/duration.ts b/packages/utilities/src/time/duration.ts index 3d6da151271..962ae752ae7 100644 --- a/packages/utilities/src/time/duration.ts +++ b/packages/utilities/src/time/duration.ts @@ -13,3 +13,21 @@ export function getDurationRemainingString(expirationTime: number): string { const h = Math.floor(timeLeft / ONE_HOUR_MS) return `${h}h ${m}m ${s}s` } + +export function getDurationRemaining(expirationTime: number): { + seconds: number + minutes?: number + hours?: number +} { + const timeLeft = expirationTime - Date.now() + const seconds = Math.floor((timeLeft % ONE_MINUTE_MS) / ONE_SECOND_MS) + if (timeLeft <= ONE_MINUTE_MS) { + return { seconds } + } + const minutes = Math.floor(timeLeft / ONE_MINUTE_MS) + if (timeLeft <= ONE_HOUR_MS) { + return { seconds, minutes } + } + const hours = Math.floor(timeLeft / ONE_HOUR_MS) + return { seconds, minutes, hours } +} diff --git a/packages/utilities/src/time/time.ts b/packages/utilities/src/time/time.ts index 34e2957c46b..d009fdb9381 100644 --- a/packages/utilities/src/time/time.ts +++ b/packages/utilities/src/time/time.ts @@ -1,10 +1,14 @@ // TODO: use compile time macro import dayjs from 'dayjs' +export const SECONDS_IN_MINUTE = 60 +export const MINUTES_IN_HOUR = 60 +export const HOURS_IN_DAY = 24 + export const ONE_SECOND_MS = 1000 -export const ONE_MINUTE_MS = 60 * ONE_SECOND_MS -export const ONE_HOUR_MS = 60 * ONE_MINUTE_MS -export const ONE_DAY_MS = 24 * ONE_HOUR_MS +export const ONE_MINUTE_MS = SECONDS_IN_MINUTE * ONE_SECOND_MS +export const ONE_HOUR_MS = MINUTES_IN_HOUR * ONE_MINUTE_MS +export const ONE_DAY_MS = HOURS_IN_DAY * ONE_HOUR_MS export function isStale(lastUpdated: number | null, staleTime: number): boolean { return !lastUpdated || Date.now() - lastUpdated > staleTime diff --git a/packages/wallet/.depcheckrc b/packages/wallet/.depcheckrc index 2809e092046..562c13b97bb 100644 --- a/packages/wallet/.depcheckrc +++ b/packages/wallet/.depcheckrc @@ -1,6 +1,5 @@ ignores: [ # Dependencies that depcheck thinks are unused but are actually used - "@typechain/ethers-v5", "jest-presets", # Dependencies that depcheck thinks are missing but are actually present or never used ## Internal packages / workspaces diff --git a/packages/wallet/.eslintignore b/packages/wallet/.eslintignore index 8348ea6d71d..644c128adc6 100644 --- a/packages/wallet/.eslintignore +++ b/packages/wallet/.eslintignore @@ -1,4 +1 @@ **/__generated__/** - -# Generated contract types -src/abis/types \ No newline at end of file diff --git a/packages/wallet/.gitignore b/packages/wallet/.gitignore index 63c0259c570..77c2e51e146 100644 --- a/packages/wallet/.gitignore +++ b/packages/wallet/.gitignore @@ -3,8 +3,5 @@ node_modules/ THUMBS_DB tsconfig.tsbuildinfo -# Generated contract types -src/abis/types - # generated graphql and api files src/**/__generated__ diff --git a/packages/wallet/jest-setup.js b/packages/wallet/jest-setup.js index 80c95541296..2b6967750cc 100644 --- a/packages/wallet/jest-setup.js +++ b/packages/wallet/jest-setup.js @@ -1,9 +1,13 @@ 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' + +// Uses real translations for tests +initializeTranslation() jest.mock('react-native-localize', () => mockRNLocalize) -jest.mock('wallet/src/features/language/LocalizationContext', () => MockLocalizationContext) +jest.mock('wallet/src/features/language/LocalizationContext', () => mockLocalizationContext) // Mock the appearance hook for all tests const mockAppearanceSetting = AppearanceSettingType.System diff --git a/packages/wallet/package.json b/packages/wallet/package.json index 38c73351f8b..e903f5a2d93 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -2,10 +2,7 @@ "name": "wallet", "version": "0.0.0", "scripts": { - "prepare": "yarn contracts && yarn tradingapi:generate", - "contracts:compile:abi": "typechain --target ethers-v5 --out-dir src/abis/types \"./src/abis/**/*.json\"", - "contracts:compile:v3": "typechain --target ethers-v5 --out-dir src/abis/types/v3 \"../../node_modules/@uniswap/**/artifacts/contracts/**/*[!dbg].json\"", - "contracts": "yarn contracts:compile:abi && yarn contracts:compile:v3", + "prepare": "yarn tradingapi:generate", "graphql:generate": "graphql-codegen --config codegen.ts", "graphql:schema": "get-graphql-schema https://api.uniswap.org/v1/graphql -h Origin=https://app.uniswap.org > ./src/data/schema.graphql", "check:deps:usage": "depcheck", @@ -32,7 +29,6 @@ "@reduxjs/toolkit": "1.9.3", "@sentry/types": "7.80.0", "@shopify/flash-list": "1.6.3", - "@typechain/ethers-v5": "7.2.0", "@uniswap/analytics-events": "2.31.0", "@uniswap/permit2-sdk": "1.2.0", "@uniswap/router-sdk": "1.8.0", @@ -77,6 +73,7 @@ "typed-redux-saga": "1.5.0", "ua-parser-js": "1.0.37", "ui": "workspace:^", + "uniswap": "workspace:^", "utilities": "workspace:^", "uuid": "9.0.0", "wcag-contrast": "3.0.0", @@ -110,7 +107,6 @@ "jest-presets": "workspace:^", "react-native-dotenv": "3.2.0", "react-test-renderer": "18.2.0", - "typechain": "5.2.0", "typescript": "5.3.3" }, "main": "src/index.ts", diff --git a/packages/wallet/src/components/BaseCard/BaseCard.test.tsx b/packages/wallet/src/components/BaseCard/BaseCard.test.tsx index 740e1770eca..5c9d808fe24 100644 --- a/packages/wallet/src/components/BaseCard/BaseCard.test.tsx +++ b/packages/wallet/src/components/BaseCard/BaseCard.test.tsx @@ -1,5 +1,5 @@ import { View } from 'react-native' -import { ON_PRESS_EVENT_PAYLOAD } from 'wallet/src/test/eventFixtures' +import { ON_PRESS_EVENT_PAYLOAD } from 'wallet/src/test/fixtures' import { fireEvent, render } from 'wallet/src/test/test-utils' import { BaseCard } from './BaseCard' diff --git a/packages/wallet/src/components/BaseCard/BaseCard.tsx b/packages/wallet/src/components/BaseCard/BaseCard.tsx index b113d2a7f56..fac73ca706c 100644 --- a/packages/wallet/src/components/BaseCard/BaseCard.tsx +++ b/packages/wallet/src/components/BaseCard/BaseCard.tsx @@ -148,7 +148,13 @@ type ErrorStateProps = { function ErrorState(props: ErrorStateProps): JSX.Element { const { t } = useTranslation() - const { title, description = t('Something went wrong'), retryButtonLabel, onRetry, icon } = props + const { + title, + description = t('common.card.error.description'), + retryButtonLabel, + onRetry, + icon, + } = props return ( @@ -187,9 +193,9 @@ function InlineErrorState(props: InlineErrorStateProps): JSX.Element { const { backgroundColor = '$surface2', textColor = '$neutral1', - title = t('Oops! Something went wrong.'), + title = t('common.card.error.title'), onRetry: retry, - retryButtonLabel = t('Retry'), + retryButtonLabel = t('common.button.retry'), icon = , } = props diff --git a/packages/wallet/src/components/CurrencyLogo/__snapshots__/index.test.tsx.snap b/packages/wallet/src/components/CurrencyLogo/__snapshots__/index.test.tsx.snap index 05d4cfd9aa7..a38928bfe94 100644 --- a/packages/wallet/src/components/CurrencyLogo/__snapshots__/index.test.tsx.snap +++ b/packages/wallet/src/components/CurrencyLogo/__snapshots__/index.test.tsx.snap @@ -15,7 +15,7 @@ exports[`renders a currency logo with network logo 1`] = ` 'ethereum-logo.png') it('renders a currency logo without network logo', () => { - const tree = renderWithProviders() + const tree = renderWithProviders() expect(tree).toMatchSnapshot() }) it('renders a currency logo with network logo', () => { const tree = renderWithProviders( - + ) expect(tree).toMatchSnapshot() }) diff --git a/packages/wallet/src/components/RecipientSearch/filter.test.ts b/packages/wallet/src/components/RecipientSearch/filter.test.ts index 69f49f02f55..e19f97cfc25 100644 --- a/packages/wallet/src/components/RecipientSearch/filter.test.ts +++ b/packages/wallet/src/components/RecipientSearch/filter.test.ts @@ -6,19 +6,21 @@ import { filterRecipientsByName, } from 'wallet/src/components/RecipientSearch/filter' import { SearchableRecipient } from 'wallet/src/features/address/types' -import { SearchableRecipients } from 'wallet/src/test/fixtures' - -const options: [AutocompleteOption, AutocompleteOption] = - [ - { - data: SearchableRecipients[0], - key: SearchableRecipients[0].address, - }, - { - data: SearchableRecipients[1], - key: SearchableRecipients[1].address, - }, - ] +import { searchableRecipient } from 'wallet/src/test/fixtures' + +const recipient1 = searchableRecipient({ name: 'Recipient 1 name', address: '0x123456' }) +const recipient2 = searchableRecipient({ name: 'Recipient 2 name', address: '0x789012' }) + +const options: ArrayOfLength<2, AutocompleteOption> = [ + { + data: recipient1, + key: recipient1.address, + }, + { + data: recipient2, + key: recipient2.address, + }, +] describe(filterRecipientsByName, () => { it('returns empty array if searchPattern is empty', () => { @@ -91,24 +93,15 @@ describe(filterRecipientsByAddress, () => { describe(filterRecipientByNameAndAddress, () => { const option1: AutocompleteOption = { - data: { - address: '0x123', - name: 'Recipient123', - }, + data: searchableRecipient({ name: 'Recipient123', address: '0x123' }), key: '0x123', } const option2: AutocompleteOption = { - data: { - address: '0x456', - name: 'Recipient2', - }, + data: searchableRecipient({ name: 'Recipient2', address: '0x456' }), key: '0x456', } const option3: AutocompleteOption = { - data: { - address: '0x789', - name: 'Recipient0x456123', - }, + data: searchableRecipient({ name: 'Recipient0x456123', address: '0x789' }), key: '0x789', } diff --git a/packages/wallet/src/components/RecipientSearch/hooks.ts b/packages/wallet/src/components/RecipientSearch/hooks.ts index 95b1b65b297..c2c3519db1b 100644 --- a/packages/wallet/src/components/RecipientSearch/hooks.ts +++ b/packages/wallet/src/components/RecipientSearch/hooks.ts @@ -125,28 +125,28 @@ export function useRecipients(): { if (validatedAddressRecipients.length) { sectionsArr.push({ - title: t('Search results'), + title: t('send.recipient.section.search'), data: validatedAddressRecipients, }) } if (recentRecipients.length) { sectionsArr.push({ - title: t('Recent'), + title: t('send.recipient.section.recent'), data: recentRecipients, }) } if (inactiveLocalAccounts.length) { sectionsArr.push({ - title: t('Your wallets'), + title: t('send.recipient.section.yours'), data: inactiveLocalAccounts, }) } if (watchedWallets.size) { sectionsArr.push({ - title: t('Favorite wallets'), + title: t('send.recipient.section.favorite'), data: Array.from(watchedWallets).map( (address) => { diff --git a/packages/wallet/src/components/RecipientSearch/utils.test.ts b/packages/wallet/src/components/RecipientSearch/utils.test.ts index a086de89090..886cee4c472 100644 --- a/packages/wallet/src/components/RecipientSearch/utils.test.ts +++ b/packages/wallet/src/components/RecipientSearch/utils.test.ts @@ -1,37 +1,49 @@ +import { faker } from '@faker-js/faker' +import { SectionListData } from 'react-native' import { filterSections } from 'wallet/src/components/RecipientSearch/utils' +import { SearchableRecipient } from 'wallet/src/features/address/types' import { - RecipientSections, SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2, + recipientSection, } from 'wallet/src/test/fixtures' +const recipientSections: ArrayOfLength<4, SectionListData> = [ + recipientSection({ addresses: [SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2] }), + recipientSection({ addresses: [SAMPLE_SEED_ADDRESS_1] }), + recipientSection({ addresses: [faker.finance.ethereumAddress()] }), + recipientSection({ addresses: [SAMPLE_SEED_ADDRESS_2] }), +] + describe(filterSections, () => { it('returns empty array if filteredAddresses is empty', () => { - expect(filterSections(RecipientSections, [])).toEqual([]) + expect(filterSections(recipientSections, [])).toEqual([]) }) - it('filters out empty sections', () => { - // SAMPLE_SEED_ADDRESS_1 and SAMPLE_SEED_ADDRESS_2 are all addresses used in the fixture - expect( - filterSections(RecipientSections, [SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2]) - ).toEqual([RecipientSections[0], RecipientSections[1], RecipientSections[3]]) + it('filters out sections without filteredAddresses', () => { + const filteredAddresses = [SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2] + expect(filterSections(recipientSections, filteredAddresses)).toEqual([ + recipientSections[0], + recipientSections[1], + recipientSections[3], + ]) }) it('returns sections corresponding to the filtered addresses with matching addresses', () => { - expect(filterSections(RecipientSections, [SAMPLE_SEED_ADDRESS_1])).toEqual([ + expect(filterSections(recipientSections, [SAMPLE_SEED_ADDRESS_1])).toEqual([ { - title: RecipientSections[0].title, - data: [RecipientSections[0].data[0]], // only the first item in the first section matches + title: recipientSections[0].title, + data: [recipientSections[0].data[0]], // only the first item in the first section matches }, - RecipientSections[1], + recipientSections[1], ]) - expect(filterSections(RecipientSections, [SAMPLE_SEED_ADDRESS_2])).toEqual([ + expect(filterSections(recipientSections, [SAMPLE_SEED_ADDRESS_2])).toEqual([ { - title: RecipientSections[0].title, - data: [RecipientSections[0].data[1]], // only the second item in the first section matches + title: recipientSections[0].title, + data: [recipientSections[0].data[1]], // only the second item in the first section matches }, - RecipientSections[3], + recipientSections[3], ]) }) }) diff --git a/packages/wallet/src/components/TokenSelector/SelectTokenButton.tsx b/packages/wallet/src/components/TokenSelector/SelectTokenButton.tsx index cb3869cd7fd..f158046c20e 100644 --- a/packages/wallet/src/components/TokenSelector/SelectTokenButton.tsx +++ b/packages/wallet/src/components/TokenSelector/SelectTokenButton.tsx @@ -35,7 +35,7 @@ export function SelectTokenButton({ ) : ( - {t('Choose token')} + {t('tokens.selector.button.choose')} )} diff --git a/packages/wallet/src/components/TokenSelector/TokenSelector.tsx b/packages/wallet/src/components/TokenSelector/TokenSelector.tsx index fbcb0387058..122964a80f1 100644 --- a/packages/wallet/src/components/TokenSelector/TokenSelector.tsx +++ b/packages/wallet/src/components/TokenSelector/TokenSelector.tsx @@ -194,7 +194,7 @@ function TokenSelectorContent({ : null} - placeholder={t('Search tokens')} + placeholder={t('tokens.selector.search.placeholder')} px={isWeb ? '$none' : '$spacing16'} py="$none" value={searchFilter ?? ''} diff --git a/packages/wallet/src/components/TokenSelector/TokenSelectorEmptySearchList.tsx b/packages/wallet/src/components/TokenSelector/TokenSelectorEmptySearchList.tsx index cb3ebab20c9..7f1e8f37c2d 100644 --- a/packages/wallet/src/components/TokenSelector/TokenSelectorEmptySearchList.tsx +++ b/packages/wallet/src/components/TokenSelector/TokenSelectorEmptySearchList.tsx @@ -66,7 +66,7 @@ function ClearAll({ onPress }: { onPress: () => void }): JSX.Element { return ( - {t('Clear all')} + {t('tokens.selector.button.clear')} ) @@ -88,7 +88,7 @@ function useTokenSectionsForEmptySearch(): GqlResult { const sections = useMemo( () => [ ...(getTokenOptionsSection( - t('Recent searches'), + t('tokens.selector.section.recent'), currencyInfosToTokenOptions( searchHistory .filter( @@ -100,7 +100,7 @@ function useTokenSectionsForEmptySearch(): GqlResult { ) ?? []), ...(getTokenOptionsSection( - t('Popular tokens'), + t('tokens.selector.section.popular'), currencyInfosToTokenOptions(popularTokens?.map(gqlTokenToCurrencyInfo)) ) ?? []), ], @@ -128,7 +128,7 @@ function _TokenSelectorEmptySearchList({ return ( diff --git a/packages/wallet/src/components/TokenSelector/TokenSelectorSearchResultsList.tsx b/packages/wallet/src/components/TokenSelector/TokenSelectorSearchResultsList.tsx index e538d33bf9d..099d68e1f6d 100644 --- a/packages/wallet/src/components/TokenSelector/TokenSelectorSearchResultsList.tsx +++ b/packages/wallet/src/components/TokenSelector/TokenSelectorSearchResultsList.tsx @@ -26,10 +26,10 @@ function EmptyResults({ searchFilter }: { searchFilter: string }): JSX.Element { const { t } = useTranslation() return ( - + - - No results found for "{searchFilter}" + + No results found for "{{ searchText: searchFilter }}" @@ -78,7 +78,7 @@ function useTokenSectionsForSearchResults( const sections = useMemo( () => getTokenOptionsSection( - t('Search results'), + t('tokens.selector.section.search'), // Use local search when only searching balances isBalancesOnlySearch ? portfolioTokenOptions : searchResults ), @@ -160,7 +160,7 @@ function _TokenSelectorSearchResultsList({ showTokenAddress chainFilter={chainFilter} emptyElement={emptyElement} - errorText={t('Couldn’t load search results')} + errorText={t('token.selector.search.error')} hasError={Boolean(error)} loading={userIsTyping || loading} refetch={refetch} diff --git a/packages/wallet/src/components/TokenSelector/TokenSelectorSendList.tsx b/packages/wallet/src/components/TokenSelector/TokenSelectorSendList.tsx index 6eb5b31a256..6fd38fb642f 100644 --- a/packages/wallet/src/components/TokenSelector/TokenSelectorSendList.tsx +++ b/packages/wallet/src/components/TokenSelector/TokenSelectorSendList.tsx @@ -32,7 +32,7 @@ function useTokenSectionsForSend(chainFilter: ChainId | null): GqlResult getTokenOptionsSection(t('Your tokens'), portfolioTokenOptions), + () => getTokenOptionsSection(t('tokens.selector.section.yours'), portfolioTokenOptions), [portfolioTokenOptions, t] ) @@ -59,7 +59,7 @@ function EmptyList({ onEmptyActionPress }: { onEmptyActionPress: () => void }): return ( - + {isLoading ? ( @@ -67,13 +67,17 @@ function EmptyList({ onEmptyActionPress }: { onEmptyActionPress: () => void }): ) : ( )} diff --git a/packages/wallet/src/components/TokenSelector/TokenSelectorSwapInputList.tsx b/packages/wallet/src/components/TokenSelector/TokenSelectorSwapInputList.tsx index 70587b7a44c..03c5fa1e628 100644 --- a/packages/wallet/src/components/TokenSelector/TokenSelectorSwapInputList.tsx +++ b/packages/wallet/src/components/TokenSelector/TokenSelectorSwapInputList.tsx @@ -61,8 +61,11 @@ function useTokenSectionsForSwapInput( ) return [ - ...(getTokenOptionsSection(t('Your tokens'), portfolioTokenOptions) ?? []), - ...(getTokenOptionsSection(t('Popular tokens'), popularMinusPortfolioTokens) ?? []), + ...(getTokenOptionsSection(t('tokens.selector.section.yours'), portfolioTokenOptions) ?? []), + ...(getTokenOptionsSection( + t('tokens.selector.section.popular'), + popularMinusPortfolioTokens + ) ?? []), ] satisfies TokenSection[] }, [loading, popularTokenOptions, portfolioTokenOptions, t]) diff --git a/packages/wallet/src/components/TokenSelector/TokenSelectorSwapOutputList.tsx b/packages/wallet/src/components/TokenSelector/TokenSelectorSwapOutputList.tsx index 0a64db2525e..991fde91b9a 100644 --- a/packages/wallet/src/components/TokenSelector/TokenSelectorSwapOutputList.tsx +++ b/packages/wallet/src/components/TokenSelector/TokenSelectorSwapOutputList.tsx @@ -66,10 +66,13 @@ function useTokenSectionsForSwapOutput( return [ // we draw the pills as a single item of a section list, so `data` is an array of Token[] - { title: t('Suggested'), data: [commonTokenOptions ?? []] }, + { title: t('tokens.selector.section.suggested'), data: [commonTokenOptions ?? []] }, // TODO temporarily hiding favorites from extension until we add favorites functionality - ...(isWeb ? [] : getTokenOptionsSection(t('Favorites'), favoriteTokenOptions) ?? []), - ...(getTokenOptionsSection(t('Popular tokens'), popularTokenOptions) ?? []), + ...(isWeb + ? [] + : getTokenOptionsSection(t('tokens.selector.section.favorite'), favoriteTokenOptions) ?? + []), + ...(getTokenOptionsSection(t('tokens.selector.section.popular'), popularTokenOptions) ?? []), ] }, [commonTokenOptions, favoriteTokenOptions, loading, popularTokenOptions, t]) diff --git a/packages/wallet/src/components/accounts/AccountDetails.test.tsx b/packages/wallet/src/components/accounts/AccountDetails.test.tsx index 173a3dcf4b5..5147574946c 100644 --- a/packages/wallet/src/components/accounts/AccountDetails.test.tsx +++ b/packages/wallet/src/components/accounts/AccountDetails.test.tsx @@ -1,17 +1,17 @@ import { AccountDetails } from 'wallet/src/components/accounts/AccountDetails' -import { account } from 'wallet/src/test/fixtures' +import { ACCOUNT } from 'wallet/src/test/fixtures' import { renderWithProviders } from 'wallet/src/test/render' describe(AccountDetails, () => { it('renders without error', () => { - const tree = renderWithProviders() + const tree = renderWithProviders() expect(tree.toJSON()).toMatchSnapshot() }) it('renders without error with chevron', () => { const tree = renderWithProviders( - + ) expect(tree.toJSON()).toMatchSnapshot() diff --git a/packages/wallet/src/components/accounts/DisplayNameText.test.tsx b/packages/wallet/src/components/accounts/DisplayNameText.test.tsx index 86ea52877df..f5afd9dd920 100644 --- a/packages/wallet/src/components/accounts/DisplayNameText.test.tsx +++ b/packages/wallet/src/components/accounts/DisplayNameText.test.tsx @@ -1,13 +1,13 @@ import { AccountDetails } from 'wallet/src/components/accounts/AccountDetails' import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText' import { DisplayName, DisplayNameType } from 'wallet/src/features/wallet/types' -import { account } from 'wallet/src/test/fixtures' +import { ACCOUNT } from 'wallet/src/test/fixtures' import { render } from 'wallet/src/test/test-utils' const unitagDisplayName: DisplayName = { name: 'luni', type: DisplayNameType.Unitag } const ensDisplayName: DisplayName = { name: 'vitalik.eth', type: DisplayNameType.ENS } const localDisplayName: DisplayName = { name: 'Wallet 1', type: DisplayNameType.Local } -const addressDisplayName: DisplayName = { name: account.address, type: DisplayNameType.Address } +const addressDisplayName: DisplayName = { name: ACCOUNT.address, type: DisplayNameType.Address } describe(AccountDetails, () => { it('renders unitag without error', () => { diff --git a/packages/wallet/src/components/buttons/PasteButton.tsx b/packages/wallet/src/components/buttons/PasteButton.tsx index e6c3f45d4b8..b9401532973 100644 --- a/packages/wallet/src/components/buttons/PasteButton.tsx +++ b/packages/wallet/src/components/buttons/PasteButton.tsx @@ -15,7 +15,7 @@ export default function PasteButton({ }): JSX.Element { const { t } = useTranslation() - const label = t('Paste') + const label = t('common.button.paste') const onPressButton = async (): Promise => { const clipboard = await getClipboard() diff --git a/packages/wallet/src/components/dropdowns/ActionSheetDropdown.test.tsx b/packages/wallet/src/components/dropdowns/ActionSheetDropdown.test.tsx index 790afe7ec1a..830fd963d49 100644 --- a/packages/wallet/src/components/dropdowns/ActionSheetDropdown.test.tsx +++ b/packages/wallet/src/components/dropdowns/ActionSheetDropdown.test.tsx @@ -3,7 +3,7 @@ import '@testing-library/jest-native/extend-expect' import { ReactNode } from 'react' import { Text } from 'ui/src' import { MenuItemProp } from 'wallet/src/components/modals/ActionSheetModal' -import { ON_PRESS_EVENT_PAYLOAD } from 'wallet/src/test/eventFixtures' +import { ON_PRESS_EVENT_PAYLOAD } from 'wallet/src/test/fixtures' import { fireEvent, render, screen, waitFor } from 'wallet/src/test/test-utils' import { ActionSheetDropdown } from './ActionSheetDropdown' diff --git a/packages/wallet/src/components/input/MaxAmountButton.tsx b/packages/wallet/src/components/input/MaxAmountButton.tsx index b05f19bd2e3..f78ac2b585e 100644 --- a/packages/wallet/src/components/input/MaxAmountButton.tsx +++ b/packages/wallet/src/components/input/MaxAmountButton.tsx @@ -41,7 +41,7 @@ export function MaxAmountButton({ - {t('Max')} + {t('swap.button.max')} diff --git a/packages/wallet/src/components/input/RecipientInputPanel.tsx b/packages/wallet/src/components/input/RecipientInputPanel.tsx index ca36267b401..ab20d6fc7e0 100644 --- a/packages/wallet/src/components/input/RecipientInputPanel.tsx +++ b/packages/wallet/src/components/input/RecipientInputPanel.tsx @@ -44,9 +44,7 @@ export function RecipientPrevTransfers({ recipient }: { recipient: string }): JS return ( - {prevTxnsCount === 1 - ? t('{{ prevTxnsCount }} previous transfer', { prevTxnsCount }) - : t('{{ prevTxnsCount }} previous transfers', { prevTxnsCount })} + {t('send.recipient.previous', { count: prevTxnsCount })} ) } diff --git a/packages/wallet/src/components/legacy/CurrencyInputPanelLegacy.tsx b/packages/wallet/src/components/legacy/CurrencyInputPanelLegacy.tsx index 6c9dbe18288..d8027b1617a 100644 --- a/packages/wallet/src/components/legacy/CurrencyInputPanelLegacy.tsx +++ b/packages/wallet/src/components/legacy/CurrencyInputPanelLegacy.tsx @@ -264,7 +264,7 @@ export function _CurrencyInputPanel(props: CurrentInputPanelProps): JSX.Element - {t('Balance')}:{' '} + {t('swap.form.balance')}:{' '} {formatCurrencyAmount({ value: currencyBalance, type: NumberType.TokenNonTx, diff --git a/packages/wallet/src/components/modals/ActionSheetModal.tsx b/packages/wallet/src/components/modals/ActionSheetModal.tsx index 3e580d4edc0..e4707a0372c 100644 --- a/packages/wallet/src/components/modals/ActionSheetModal.tsx +++ b/packages/wallet/src/components/modals/ActionSheetModal.tsx @@ -22,7 +22,7 @@ export function ActionSheetModalContent(props: ActionSheetModalContentProps): JS const { t } = useTranslation() const { fullHeight } = useDeviceDimensions() - const { header, closeButtonLabel = t('Cancel'), options, onClose } = props + const { header, closeButtonLabel = t('common.button.cancel'), options, onClose } = props return ( diff --git a/packages/wallet/src/components/modals/WarningModal/BlockedAddressModal.tsx b/packages/wallet/src/components/modals/WarningModal/BlockedAddressModal.tsx index 87b0a429db3..f30da85430a 100644 --- a/packages/wallet/src/components/modals/WarningModal/BlockedAddressModal.tsx +++ b/packages/wallet/src/components/modals/WarningModal/BlockedAddressModal.tsx @@ -8,13 +8,11 @@ export function BlockedAddressModal({ onClose }: { onClose: () => void }): JSX.E return ( ) diff --git a/packages/wallet/src/components/network/NetworkFee.test.tsx b/packages/wallet/src/components/network/NetworkFee.test.tsx index ebc40b8ab51..21df67c4837 100644 --- a/packages/wallet/src/components/network/NetworkFee.test.tsx +++ b/packages/wallet/src/components/network/NetworkFee.test.tsx @@ -1,6 +1,6 @@ import { ChainId } from 'wallet/src/constants/chains' +import { noOpFunction } from 'wallet/src/test/mocks' import { render } from 'wallet/src/test/test-utils' -import { noOpFunction } from 'wallet/src/test/utils' import { NetworkFee } from './NetworkFee' jest.mock('wallet/src/features/gas/hooks', () => { diff --git a/packages/wallet/src/components/network/NetworkFee.tsx b/packages/wallet/src/components/network/NetworkFee.tsx index 03e340635ce..ed007200723 100644 --- a/packages/wallet/src/components/network/NetworkFee.tsx +++ b/packages/wallet/src/components/network/NetworkFee.tsx @@ -29,7 +29,7 @@ export function NetworkFee({ - {t('Network cost')} + {t('transaction.networkCost.label')}   @@ -41,7 +41,7 @@ export function NetworkFee({ ) : gasFee.error ? ( - {t('N/A')} + {t('common.text.notAvailable')} ) : ( diff --git a/packages/wallet/src/components/network/NetworkOption.tsx b/packages/wallet/src/components/network/NetworkOption.tsx index 7e5261f6ccc..fd60b5f24b3 100644 --- a/packages/wallet/src/components/network/NetworkOption.tsx +++ b/packages/wallet/src/components/network/NetworkOption.tsx @@ -33,7 +33,7 @@ export function NetworkOption({ - {t('All networks')} + {t('transaction.network.all')} ) diff --git a/packages/wallet/src/components/nfts/NFTHiddenRow.tsx b/packages/wallet/src/components/nfts/NFTHiddenRow.tsx index 6781737be2f..a4287742940 100644 --- a/packages/wallet/src/components/nfts/NFTHiddenRow.tsx +++ b/packages/wallet/src/components/nfts/NFTHiddenRow.tsx @@ -18,7 +18,7 @@ export function HiddenNftsRowLeft({ numHidden }: { numHidden: number }): JSX.Ele my="$spacing16" py="$spacing4"> - {t('Hidden ({{numHidden}})', { numHidden })} + {t('tokens.nfts.hidden.label', { numHidden })} ) @@ -39,7 +39,7 @@ export function HiddenNftsRowRight({ return { transform: [{ rotateZ: `${chevronRotate.value}deg` }], } - }) + }, [chevronRotate]) const onPressRow = useCallback(() => { chevronRotate.value = withTiming(chevronRotate.value === 0 ? 180 : 0, { @@ -65,7 +65,7 @@ export function HiddenNftsRowRight({ pr="$spacing8" py="$spacing4"> - {isExpanded ? t('Hide') : t('Show')} + {isExpanded ? t('common.button.hide') : t('common.button.show')} , NftsListProps>(function _ isError(networkStatus, !!data) ? ( @@ -181,18 +181,22 @@ export const NftsList = forwardRef, NftsListProps>(function _ // empty view } - title={t('No NFTs yet')} + title={t('tokens.nfts.list.none.title')} onPress={onPressEmptyState} /> diff --git a/packages/wallet/src/components/text/LearnMoreLink.test.tsx b/packages/wallet/src/components/text/LearnMoreLink.test.tsx index c84da50179d..39219537d75 100644 --- a/packages/wallet/src/components/text/LearnMoreLink.test.tsx +++ b/packages/wallet/src/components/text/LearnMoreLink.test.tsx @@ -1,5 +1,5 @@ import { fireEvent } from '@testing-library/react-native' -import { ON_PRESS_EVENT_PAYLOAD } from 'wallet/src/test/eventFixtures' +import { ON_PRESS_EVENT_PAYLOAD } from 'wallet/src/test/fixtures' import { renderWithProviders } from 'wallet/src/test/render' import { openUri } from 'wallet/src/utils/linking' import { LearnMoreLink } from './LearnMoreLink' diff --git a/packages/wallet/src/components/text/LearnMoreLink.tsx b/packages/wallet/src/components/text/LearnMoreLink.tsx index 8a693846d9f..8f5dbf68f47 100644 --- a/packages/wallet/src/components/text/LearnMoreLink.tsx +++ b/packages/wallet/src/components/text/LearnMoreLink.tsx @@ -11,7 +11,7 @@ export const LearnMoreLink = ({ url }: { url: string }): JSX.Element => { return ( => onPressLearnMore(url)}> - {t('Learn more')} + {t('common.button.learn')} ) diff --git a/packages/wallet/src/components/text/RelativeChange.test.tsx b/packages/wallet/src/components/text/RelativeChange.test.tsx index 884a0d6b25b..7b53aab4528 100644 --- a/packages/wallet/src/components/text/RelativeChange.test.tsx +++ b/packages/wallet/src/components/text/RelativeChange.test.tsx @@ -2,7 +2,7 @@ import renderer from 'react-test-renderer' import { FiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' import { Locale } from 'wallet/src/features/language/constants' import { TamaguiProvider } from 'wallet/src/provider/tamagui-provider' -import { MockLocalizationContext } from 'wallet/src/test/utils' +import { mockLocalizationContext } from 'wallet/src/test/mocks' import { RelativeChange } from './RelativeChange' const mockLocale = Locale.EnglishUnitedStates @@ -29,7 +29,7 @@ jest.mock('wallet/src/features/fiatCurrency/hooks', () => { } }) -jest.mock('wallet/src/features/language/LocalizationContext', () => MockLocalizationContext) +jest.mock('wallet/src/features/language/LocalizationContext', () => mockLocalizationContext) it('renders a relative change', () => { const tree = renderer.create( diff --git a/packages/wallet/src/constants/urls.ts b/packages/wallet/src/constants/urls.ts index 6481c86e036..0171b2e7fe9 100644 --- a/packages/wallet/src/constants/urls.ts +++ b/packages/wallet/src/constants/urls.ts @@ -39,6 +39,7 @@ export const uniswapUrls = { interfaceUrl: `https://${UNISWAP_APP_HOSTNAME}/#/swap`, extensionFeedbackFormUrl: 'https://forms.gle/RGFhKnABUjdPiYQH6', // TODO(EXT-668): Remove this after F&F launch interfaceTokensUrl: `https://${UNISWAP_APP_HOSTNAME}/explore/tokens`, + interfaceNftItemUrl: `https://${UNISWAP_APP_HOSTNAME}/nfts/asset`, unitagsApiUrl: getUnitagsApiUrl(), tradingApiPaths: { quote: getTradingApiQuotePath(), diff --git a/packages/wallet/src/contexts/WalletNavigationContext.tsx b/packages/wallet/src/contexts/WalletNavigationContext.tsx index 49371d542d1..d6b5318b355 100644 --- a/packages/wallet/src/contexts/WalletNavigationContext.tsx +++ b/packages/wallet/src/contexts/WalletNavigationContext.tsx @@ -1,13 +1,23 @@ import { createContext, ReactNode, useContext } from 'react' +import { NFTItem } from 'wallet/src/features/nfts/types' import { TransactionState } from 'wallet/src/features/transactions/transactionState/types' export type NavigateToSwapFlowArgs = { initialState: TransactionState } | undefined +export type NavigateToNftItemArgs = { + owner?: Address + address: Address + tokenId: string + isSpam?: boolean + fallbackData?: NFTItem +} + export type WalletNavigationContextState = { navigateToAccountActivityList: () => void navigateToAccountTokenList: () => void // Action that should be taken when the user presses the "Buy crypto" or "Receive tokens" button when they open the Send flow with an empty wallet. navigateToBuyOrReceiveWithEmptyWallet: () => void + navigateToNftDetails: (args: NavigateToNftItemArgs) => void navigateToSwapFlow: (args: NavigateToSwapFlowArgs) => void navigateToTokenDetails: (currencyId: string) => void } diff --git a/packages/wallet/src/data/links.ts b/packages/wallet/src/data/links.ts index 432da4ffc8c..4acd2d4e14c 100644 --- a/packages/wallet/src/data/links.ts +++ b/packages/wallet/src/data/links.ts @@ -116,7 +116,7 @@ export function getErrorLink( graphQLErrors.forEach(({ message, locations, path }) => { sample( () => - logger.error('GraphQL error', { + logger.error(`GraphQL error: ${message}`, { tags: { file: 'data/links', function: 'getErrorLink', diff --git a/packages/wallet/src/features/activity/useActivityData.tsx b/packages/wallet/src/features/activity/useActivityData.tsx index d6ed512222d..214af3a0ea2 100644 --- a/packages/wallet/src/features/activity/useActivityData.tsx +++ b/packages/wallet/src/features/activity/useActivityData.tsx @@ -94,8 +94,8 @@ export function useActivityData({ const errorCard = ( @@ -104,16 +104,14 @@ export function useActivityData({ const emptyListView = ( } - title={t('No activity yet')} + title={t('home.activity.empty.title')} onPress={onPressEmptyState} /> diff --git a/packages/wallet/src/features/dataApi/balances.test.ts b/packages/wallet/src/features/dataApi/balances.test.ts index ed35bac378c..5cb3c66d827 100644 --- a/packages/wallet/src/features/dataApi/balances.test.ts +++ b/packages/wallet/src/features/dataApi/balances.test.ts @@ -3,24 +3,30 @@ import { ApolloError, NetworkStatus } from '@apollo/client' import { Chain, PortfolioBalanceDocument, - Portfolio as PortfolioType, Resolvers, } from 'wallet/src/data/__generated__/types-and-hooks' import { setupWalletCache } from 'wallet/src/data/cache' -import { PortfolioBalance as PortfolioBalanceType } from 'wallet/src/features/dataApi/types' +import { PortfolioBalance } from 'wallet/src/features/dataApi/types' import { FavoritesState, initialFavoritesState } from 'wallet/src/features/favorites/slice' import { WalletState, initialWalletState } from 'wallet/src/features/wallet/slice' import { - PortfolioBalance, - PortfolioBalanceWithoutUSD, - PortfolioBalancesWithUSD, + ARBITRUM_CURRENCY, + BASE_CURRENCY, + MAINNET_CURRENCY, + OPTIMISM_CURRENCY, + POLYGON_CURRENCY, SAMPLE_CURRENCY_ID_1, SAMPLE_CURRENCY_ID_2, SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2, + currencyInfo, + daiToken, + ethToken, + portfolio, + portfolioBalance, + tokenBalance, } from 'wallet/src/test/fixtures' -import { Portfolio, PortfolioBalancesById, Portfolios } from 'wallet/src/test/gqlFixtures' -import { act, renderHook, waitFor } from 'wallet/src/test/test-utils' +import { act, createArray, renderHook, waitFor } from 'wallet/src/test/test-utils' import { sortPortfolioBalances, useHighestBalanceNativeCurrencyId, @@ -32,17 +38,17 @@ import { useTokenBalancesGroupedByVisibility, } from './balances' -const currencyId1 = '1-0x6b175474e89094c44da98b954eedeac495271d0f' -const currencyId2 = '1-0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' - -const BalancesById = { - [currencyId1]: PortfolioBalancesById[currencyId1], - [currencyId2]: PortfolioBalancesById[currencyId2], -} +const daiTokenBalance = tokenBalance({ token: daiToken(), isHidden: true }) +const ethTokenBalance = tokenBalance({ token: ethToken(), isHidden: false }) +const daiPortfolioBalance = portfolioBalance({ from: daiTokenBalance }) +const ethPortfolioBalance = portfolioBalance({ from: ethTokenBalance }) +const Portfolio = portfolio({ tokenBalances: [daiTokenBalance, ethTokenBalance] }) +const daiCurrencyId = daiPortfolioBalance.currencyInfo.currencyId +const ethCurrencyId = ethPortfolioBalance.currencyInfo.currencyId const portfolioResolvers: Resolvers = { Query: { - portfolios: () => [Portfolio as PortfolioType], + portfolios: () => [Portfolio], }, } @@ -228,7 +234,7 @@ describe(usePortfolioBalances, () => { }) it('returns loading set to true when data is being fetched', async () => { - const { result } = renderHook(() => usePortfolioBalances({ address: SAMPLE_SEED_ADDRESS_1 }), { + const { result } = renderHook(() => usePortfolioBalances({ address: Portfolio.ownerAddress }), { resolvers: portfolioResolvers, }) @@ -246,7 +252,7 @@ describe(usePortfolioBalances, () => { it('returns error when query fails', async () => { jest.spyOn(console, 'error').mockImplementation(() => undefined) - const { result } = renderHook(() => usePortfolioBalances({ address: SAMPLE_SEED_ADDRESS_1 }), { + const { result } = renderHook(() => usePortfolioBalances({ address: Portfolio.ownerAddress }), { resolvers: { Query: { portfolios: () => { @@ -268,7 +274,7 @@ describe(usePortfolioBalances, () => { }) it('returns undefined when no balances for the specified address are found', async () => { - const { result } = renderHook(() => usePortfolioBalances({ address: SAMPLE_SEED_ADDRESS_1 }), { + const { result } = renderHook(() => usePortfolioBalances({ address: Portfolio.ownerAddress }), { resolvers: { Query: { portfolios: () => [], @@ -296,7 +302,10 @@ describe(usePortfolioBalances, () => { await waitFor(() => { expect(result.current).toEqual({ - data: BalancesById, + data: { + [daiCurrencyId]: daiPortfolioBalance, + [ethCurrencyId]: ethPortfolioBalance, + }, loading: false, networkStatus: NetworkStatus.ready, refetch: expect.any(Function), @@ -308,7 +317,7 @@ describe(usePortfolioBalances, () => { it('calls onCompleted callback when query completes', async () => { const onCompleted = jest.fn() const { result } = renderHook( - () => usePortfolioBalances({ address: SAMPLE_SEED_ADDRESS_1, onCompleted }), + () => usePortfolioBalances({ address: daiCurrencyId, onCompleted }), { resolvers: portfolioResolvers } ) @@ -337,10 +346,8 @@ describe(usePortfolioTotalValue, () => { it('returns loading set to true when data is being fetched', async () => { const { result } = renderHook( - () => usePortfolioTotalValue({ address: SAMPLE_SEED_ADDRESS_1 }), - { - resolvers: portfolioResolvers, - } + () => usePortfolioTotalValue({ address: Portfolio.ownerAddress }), + { resolvers: portfolioResolvers } ) expect(result.current).toEqual({ @@ -358,7 +365,7 @@ describe(usePortfolioTotalValue, () => { jest.spyOn(console, 'error').mockImplementation(() => undefined) const { result } = renderHook( - () => usePortfolioTotalValue({ address: SAMPLE_SEED_ADDRESS_1 }), + () => usePortfolioTotalValue({ address: Portfolio.ownerAddress }), { resolvers: { Query: { @@ -383,7 +390,7 @@ describe(usePortfolioTotalValue, () => { it('retruns undefined when no balances for the specified address are found', async () => { const { result } = renderHook( - () => usePortfolioTotalValue({ address: SAMPLE_SEED_ADDRESS_1 }), + () => usePortfolioTotalValue({ address: Portfolio.ownerAddress }), { resolvers: { Query: { @@ -435,12 +442,7 @@ describe(useHighestBalanceNativeCurrencyId, () => { const { result } = renderHook(() => useHighestBalanceNativeCurrencyId(SAMPLE_SEED_ADDRESS_1), { resolvers: { Query: { - portfolios: () => [ - { - ...Portfolio, - tokenBalances: [Portfolio?.tokenBalances?.[0]], // the first balance is not native - } as PortfolioType, - ], + portfolios: () => [portfolio({ tokenBalances: [daiTokenBalance] })], }, }, }) @@ -451,28 +453,21 @@ describe(useHighestBalanceNativeCurrencyId, () => { }) it('returns native currency id with the highest balance', async () => { - const { result } = renderHook( - () => useHighestBalanceNativeCurrencyId(SAMPLE_SEED_ADDRESS_1.toLocaleLowerCase()), - { - resolvers: portfolioResolvers, - } - ) + const { result } = renderHook(() => useHighestBalanceNativeCurrencyId(SAMPLE_SEED_ADDRESS_1), { + resolvers: portfolioResolvers, + }) await act(() => undefined) // wait for query to complete await waitFor(() => { - expect(result.current).toEqual('1-0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee') + expect(result.current).toEqual(ethCurrencyId) // ETH currency is native }) }) }) describe(useTokenBalancesGroupedByVisibility, () => { - const hiddenBalances = [{ ...PortfolioBalancesWithUSD[0], isHidden: true }] as [ - PortfolioBalanceType - ] - const visibleBalances = [{ ...PortfolioBalancesWithUSD[1], isHidden: false }] as [ - PortfolioBalanceType - ] + const hiddenBalances = [daiPortfolioBalance] + const visibleBalances = [ethPortfolioBalance] it('shownTokens and hiddenTokens are undefined when no balances are passed', () => { const { result } = renderHook(() => useTokenBalancesGroupedByVisibility({})) @@ -487,8 +482,8 @@ describe(useTokenBalancesGroupedByVisibility, () => { const { result } = renderHook(() => useTokenBalancesGroupedByVisibility({ balancesById: { - [visibleBalances[0].cacheId]: visibleBalances[0], - [hiddenBalances[0].cacheId]: hiddenBalances[0], + [daiPortfolioBalance.cacheId]: daiPortfolioBalance, + [ethPortfolioBalance.cacheId]: ethPortfolioBalance, }, }) ) @@ -503,7 +498,7 @@ describe(useTokenBalancesGroupedByVisibility, () => { describe(useSortedPortfolioBalances, () => { it('returns loading set to true when data is being fetched', () => { const { result } = renderHook(() => - useSortedPortfolioBalances({ address: SAMPLE_SEED_ADDRESS_1 }) + useSortedPortfolioBalances({ address: Portfolio.ownerAddress }) ) expect(result.current).toEqual({ @@ -519,7 +514,7 @@ describe(useSortedPortfolioBalances, () => { it('returns balances grouped by visibility when data is fetched', async () => { const { result } = renderHook( - () => useSortedPortfolioBalances({ address: 'SAMPLE_SEED_ADDRESS_1' }), + () => useSortedPortfolioBalances({ address: Portfolio.ownerAddress }), { resolvers: portfolioResolvers, } @@ -528,8 +523,8 @@ describe(useSortedPortfolioBalances, () => { await waitFor(() => { expect(result.current).toEqual({ data: { - balances: [PortfolioBalancesById['1-0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee']], - hiddenBalances: [PortfolioBalancesById['1-0x6b175474e89094c44da98b954eedeac495271d0f']], + balances: [ethPortfolioBalance], + hiddenBalances: [daiPortfolioBalance], }, loading: false, networkStatus: NetworkStatus.ready, @@ -540,35 +535,55 @@ describe(useSortedPortfolioBalances, () => { }) describe(sortPortfolioBalances, () => { + const balancesWithUSD = createArray(3, portfolioBalance) + const balancesWithoutUSD: ArrayOfLength<5, PortfolioBalance> = [ + portfolioBalance({ + balanceUSD: null, + currencyInfo: currencyInfo({ currency: POLYGON_CURRENCY }), + }), + portfolioBalance({ balanceUSD: null, currencyInfo: currencyInfo({ currency: BASE_CURRENCY }) }), + portfolioBalance({ + balanceUSD: null, + currencyInfo: currencyInfo({ currency: ARBITRUM_CURRENCY }), + }), + portfolioBalance({ + balanceUSD: null, + currencyInfo: currencyInfo({ currency: MAINNET_CURRENCY }), + }), + portfolioBalance({ + balanceUSD: null, + currencyInfo: currencyInfo({ currency: OPTIMISM_CURRENCY }), + }), + ] + it('returns balances with USD value before balances without USD value', () => { - const result = sortPortfolioBalances([ - ...PortfolioBalanceWithoutUSD, - ...PortfolioBalancesWithUSD, - ]) + const result = sortPortfolioBalances([...balancesWithoutUSD, ...balancesWithUSD]) expect(result).toEqual([ - expect.objectContaining({ balanceUSD: expect.any(Number) }), - expect.objectContaining({ balanceUSD: expect.any(Number) }), - expect.objectContaining({ balanceUSD: expect.any(Number) }), - expect.objectContaining({ balanceUSD: null }), - expect.objectContaining({ balanceUSD: null }), - expect.objectContaining({ balanceUSD: null }), + ...createArray(balancesWithUSD.length, () => + expect.objectContaining({ balanceUSD: expect.any(Number) }) + ), + ...createArray(balancesWithoutUSD.length, () => + expect.objectContaining({ balanceUSD: null }) + ), ]) }) it('sorts balances with USD value by USD value in descending order', () => { - const result = sortPortfolioBalances(PortfolioBalancesWithUSD) + const result = sortPortfolioBalances(balancesWithUSD) - expect(result).toEqual(PortfolioBalancesWithUSD.sort((a, b) => b.balanceUSD! - a.balanceUSD!)) + expect(result).toEqual(balancesWithUSD.sort((a, b) => b.balanceUSD! - a.balanceUSD!)) }) it('sorts balances without USD value by name', () => { - const result = sortPortfolioBalances(PortfolioBalanceWithoutUSD) + const result = sortPortfolioBalances(balancesWithoutUSD) expect(result).toEqual([ - PortfolioBalanceWithoutUSD[2], - PortfolioBalanceWithoutUSD[0], - PortfolioBalanceWithoutUSD[1], + balancesWithoutUSD[2], + balancesWithoutUSD[1], + balancesWithoutUSD[3], + balancesWithoutUSD[4], + balancesWithoutUSD[0], ]) }) }) @@ -576,6 +591,7 @@ describe(sortPortfolioBalances, () => { describe(usePortfolioCacheUpdater, () => { const cache = setupWalletCache() const modifyMock = jest.spyOn(cache, 'modify') + const balance = portfolioBalance() beforeEach(async () => { await cache.reset() @@ -583,7 +599,7 @@ describe(usePortfolioCacheUpdater, () => { cache.writeQuery({ query: PortfolioBalanceDocument, - data: { portfolios: Portfolios }, + data: { portfolios: [Portfolio] }, variables: { owner: SAMPLE_SEED_ADDRESS_1 }, }) }) @@ -594,10 +610,10 @@ describe(usePortfolioCacheUpdater, () => { resolvers: portfolioResolvers, }) - result.current(true, PortfolioBalance) + result.current(true, balance) expect(modifyMock).toHaveBeenCalledWith({ - id: PortfolioBalance.cacheId, + id: balance.cacheId, fields: { isHidden: expect.any(Function), }, @@ -610,7 +626,7 @@ describe(usePortfolioCacheUpdater, () => { resolvers: portfolioResolvers, }) - result.current(true, PortfolioBalance) + result.current(true, balance) expect(modifyMock).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/packages/wallet/src/features/dataApi/searchTokens.test.ts b/packages/wallet/src/features/dataApi/searchTokens.test.ts index 7f5a5af63a5..30179ddf83c 100644 --- a/packages/wallet/src/features/dataApi/searchTokens.test.ts +++ b/packages/wallet/src/features/dataApi/searchTokens.test.ts @@ -2,13 +2,20 @@ import { Resolvers } from '@apollo/client' import { waitFor } from '@testing-library/react-native' import { useTokenProjects } from 'wallet/src/features/dataApi/tokenProjects' import { gqlTokenToCurrencyInfo } from 'wallet/src/features/dataApi/utils' -import { SearchTokens } from 'wallet/src/test/gqlFixtures' -import { renderHook } from 'wallet/src/test/test-utils' +import { token, tokenProject } from 'wallet/src/test/fixtures' +import { createArray, renderHook } from 'wallet/src/test/test-utils' import { useSearchTokens } from './searchTokens' +const searchTokens = createArray(5, () => + token({ + // There is no isSpam field in the query document, so we remove it from the token object + project: tokenProject({ isSpam: null }), + }) +) + const resolvers: Resolvers = { Query: { - searchTokens: () => SearchTokens, + searchTokens: () => searchTokens, }, } @@ -34,17 +41,7 @@ describe(useTokenProjects, () => { }) await waitFor(() => { - expect(result.current.data).toEqual([ - gqlTokenToCurrencyInfo(SearchTokens[0]!), - gqlTokenToCurrencyInfo(SearchTokens[1]!), - gqlTokenToCurrencyInfo(SearchTokens[2]!), - gqlTokenToCurrencyInfo(SearchTokens[3]!), - gqlTokenToCurrencyInfo(SearchTokens[4]!), - gqlTokenToCurrencyInfo(SearchTokens[5]!), - gqlTokenToCurrencyInfo(SearchTokens[6]!), - gqlTokenToCurrencyInfo(SearchTokens[7]!), - gqlTokenToCurrencyInfo(SearchTokens[8]!), - ]) + expect(result.current.data).toEqual(searchTokens.map(gqlTokenToCurrencyInfo)) }) }) }) diff --git a/packages/wallet/src/features/dataApi/tokenProject.test.tsx b/packages/wallet/src/features/dataApi/tokenProject.test.tsx index 0b84b9ef21d..68f2b6c1816 100644 --- a/packages/wallet/src/features/dataApi/tokenProject.test.tsx +++ b/packages/wallet/src/features/dataApi/tokenProject.test.tsx @@ -1,8 +1,7 @@ import { waitFor } from '@testing-library/react-native' import { useTokenProjects } from 'wallet/src/features/dataApi/tokenProjects' import { tokenProjectToCurrencyInfos } from 'wallet/src/features/dataApi/utils' -import { SAMPLE_CURRENCY_ID_1 } from 'wallet/src/test/fixtures' -import { TokenProjects } from 'wallet/src/test/gqlFixtures' +import { SAMPLE_CURRENCY_ID_1, usdcTokenProject } from 'wallet/src/test/fixtures' import { renderHook } from 'wallet/src/test/test-utils' describe(useTokenProjects, () => { @@ -22,17 +21,18 @@ describe(useTokenProjects, () => { }) it('renders without error', async () => { + const projects = [usdcTokenProject()] const { result } = renderHook(() => useTokenProjects([SAMPLE_CURRENCY_ID_1]), { resolvers: { Query: { - tokenProjects: () => TokenProjects, + tokenProjects: () => projects, }, }, }) await waitFor(() => { const data = result.current.data - expect(data).toEqual(tokenProjectToCurrencyInfos(TokenProjects)) + expect(data).toEqual(tokenProjectToCurrencyInfos(projects)) }) }) }) diff --git a/packages/wallet/src/features/dataApi/topTokens.test.ts b/packages/wallet/src/features/dataApi/topTokens.test.ts index 64bbbf681f8..20e27da0286 100644 --- a/packages/wallet/src/features/dataApi/topTokens.test.ts +++ b/packages/wallet/src/features/dataApi/topTokens.test.ts @@ -1,7 +1,9 @@ +import { faker } from '@faker-js/faker' import { ChainId } from 'wallet/src/constants/chains' import { gqlTokenToCurrencyInfo } from 'wallet/src/features/dataApi/utils' -import { TopTokens } from 'wallet/src/test/gqlFixtures' +import { token, tokenProject } from 'wallet/src/test/fixtures' import { act, renderHook, waitFor } from 'wallet/src/test/test-utils' +import { createArray } from 'wallet/src/test/utils' import { usePopularTokens } from './topTokens' describe(usePopularTokens, () => { @@ -42,20 +44,23 @@ describe(usePopularTokens, () => { }) it('returns data when data fetching succeeds', async () => { + const topToken = createArray(3, () => + token({ + // We have to provide the isSpam property as it is specified in the popular tokens query + project: tokenProject({ isSpam: faker.datatype.boolean() }), + }) + ) const { result } = renderHook(() => usePopularTokens(ChainId.Mainnet), { resolvers: { Query: { - topTokens: () => TopTokens, + topTokens: () => topToken, }, }, }) await waitFor(() => { expect(result.current).toEqual({ - data: TopTokens.map((token) => { - token.address = token?.address?.toLowerCase() - return gqlTokenToCurrencyInfo(token) - }), + data: topToken.map(gqlTokenToCurrencyInfo), loading: false, error: undefined, refetch: expect.any(Function), diff --git a/packages/wallet/src/features/dataApi/utils.test.ts b/packages/wallet/src/features/dataApi/utils.test.ts index b9e6c14e16f..f91847559ae 100644 --- a/packages/wallet/src/features/dataApi/utils.test.ts +++ b/packages/wallet/src/features/dataApi/utils.test.ts @@ -1,12 +1,20 @@ import { ApolloError } from '@apollo/client' import { Token } from '@uniswap/sdk-core' import { ChainId } from 'wallet/src/constants/chains' -import { Token as GQLToken, TokenProject } from 'wallet/src/data/__generated__/types-and-hooks' +import { + Chain, + Token as GQLToken, + TokenProject, +} from 'wallet/src/data/__generated__/types-and-hooks' import { fromGraphQLChain } from 'wallet/src/features/chains/utils' import { CurrencyInfo } from 'wallet/src/features/dataApi/types' import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' -import { SAMPLE_CURRENCY_ID_1, SAMPLE_CURRENCY_ID_2 } from 'wallet/src/test/fixtures' -import { EthToken, TokenProjectDay } from 'wallet/src/test/gqlFixtures' +import { + SAMPLE_CURRENCY_ID_1, + SAMPLE_CURRENCY_ID_2, + ethToken, + usdcTokenProject, +} from 'wallet/src/test/fixtures' import { renderHook } from 'wallet/src/test/test-utils' import { buildCurrency, @@ -30,7 +38,7 @@ describe(currencyIdToContractInput, () => { }) describe(tokenProjectToCurrencyInfos, () => { - const project = TokenProjectDay + const project = usdcTokenProject() const getExpectedResult = (proj: TokenProject, token: GQLToken): CurrencyInfo => ({ @@ -127,28 +135,26 @@ describe(buildCurrency, () => { describe(gqlTokenToCurrencyInfo, () => { it('returns formatted CurrencyInfo for a given token', () => { - const result = gqlTokenToCurrencyInfo(EthToken) + const token = ethToken() + const result = gqlTokenToCurrencyInfo(token) expect(result).toEqual({ currency: buildCurrency({ - chainId: fromGraphQLChain(EthToken.chain), - address: EthToken.address, - decimals: EthToken.decimals, - symbol: EthToken.symbol, - name: EthToken.project?.name, + chainId: fromGraphQLChain(token.chain), + address: token.address, + decimals: token.decimals, + symbol: token.symbol, + name: token.project?.name, }), - currencyId: `${fromGraphQLChain(EthToken.chain)}-${EthToken.address}`, - logoUrl: EthToken.project?.logoUrl, - safetyLevel: EthToken.project?.safetyLevel, - isSpam: false, + currencyId: `${fromGraphQLChain(token.chain)}-${token.address}`, + logoUrl: token.project?.logoUrl, + safetyLevel: token.project?.safetyLevel, + isSpam: token.project?.isSpam, }) }) it('returns null if currency is invalid', () => { - const result = gqlTokenToCurrencyInfo({ - ...EthToken, - chain: 'INVALID', - } as unknown as GQLToken) + const result = gqlTokenToCurrencyInfo(ethToken({ chain: 'INVALID' as Chain })) expect(result).toBeNull() }) diff --git a/packages/wallet/src/features/fiatCurrency/conversion.ts b/packages/wallet/src/features/fiatCurrency/conversion.ts index fcb78cb5835..6ce8ac92e2b 100644 --- a/packages/wallet/src/features/fiatCurrency/conversion.ts +++ b/packages/wallet/src/features/fiatCurrency/conversion.ts @@ -96,7 +96,7 @@ const SOURCE_CURRENCY = Currency.Usd // Assuming all currency data comes from US /** * Hook used to return a converter with a set of all necessary conversion logic needed for - * fiat currency. This is based off of the currently selected langauge and fiat currency + * fiat currency. This is based off of the currently selected language and fiat currency * in settings, using a graphql endpoint to retrieve the conversion rate. * This ensures that the converted and formatted values are properly localized. If any addditonal * conversion logic is needed, please add them here. diff --git a/packages/wallet/src/features/fiatCurrency/hooks.ts b/packages/wallet/src/features/fiatCurrency/hooks.ts index 7e354e603f9..51f4d6d70e3 100644 --- a/packages/wallet/src/features/fiatCurrency/hooks.ts +++ b/packages/wallet/src/features/fiatCurrency/hooks.ts @@ -47,25 +47,25 @@ export function useFiatCurrencyName(currency: FiatCurrency): string { const currencyToCurrencyName = useMemo((): Record => { return { - [FiatCurrency.AustrialianDollor]: t('Australian Dollar'), - [FiatCurrency.BrazilianReal]: t('Brazilian Real'), - [FiatCurrency.CanadianDollar]: t('Canadian Dollar'), - [FiatCurrency.ChineseYuan]: t('Chinese Yuan'), - [FiatCurrency.Euro]: t('Euro'), - [FiatCurrency.BritishPound]: t('British Pound'), - [FiatCurrency.HongKongDollar]: t('Hong Kong Dollar'), - [FiatCurrency.IndonesianRupiah]: t('Indonesian Rupiah'), - [FiatCurrency.IndianRupee]: t('Indian Rupee'), - [FiatCurrency.JapaneseYen]: t('Japanese Yen'), - [FiatCurrency.NigerianNaira]: t('Nigerian Naira'), - [FiatCurrency.PakistaniRupee]: t('Pakistani Rupee'), - [FiatCurrency.RussianRuble]: t('Russian Ruble'), - [FiatCurrency.SingaporeDollar]: t('Singapore Dollar'), - [FiatCurrency.ThaiBaht]: t('Thai Baht'), - [FiatCurrency.TurkishLira]: t('Turkish Lira'), - [FiatCurrency.UkrainianHryvnia]: t('Ukrainian Hryvnia'), - [FiatCurrency.UnitedStatesDollar]: t('United States Dollar'), - [FiatCurrency.VietnameseDong]: t('Vietnamese Dong'), + [FiatCurrency.AustrialianDollor]: t('currency.aud'), + [FiatCurrency.BrazilianReal]: t('currency.brl'), + [FiatCurrency.CanadianDollar]: t('currency.cad'), + [FiatCurrency.ChineseYuan]: t('currency.cny'), + [FiatCurrency.Euro]: t('currency.eur'), + [FiatCurrency.BritishPound]: t('currency.gbp'), + [FiatCurrency.HongKongDollar]: t('currency.hkd'), + [FiatCurrency.IndonesianRupiah]: t('currency.idr'), + [FiatCurrency.IndianRupee]: t('currency.inr'), + [FiatCurrency.JapaneseYen]: t('currency.jpy'), + [FiatCurrency.NigerianNaira]: t('currency.ngn'), + [FiatCurrency.PakistaniRupee]: t('currency.pkr'), + [FiatCurrency.RussianRuble]: t('currency.rub'), + [FiatCurrency.SingaporeDollar]: t('currency.sgd'), + [FiatCurrency.ThaiBaht]: t('currency.thb'), + [FiatCurrency.TurkishLira]: t('currency.try'), + [FiatCurrency.UkrainianHryvnia]: t('currency.uah'), + [FiatCurrency.UnitedStatesDollar]: t('currency.usd'), + [FiatCurrency.VietnameseDong]: t('currency.vnd'), } }, [t]) diff --git a/packages/wallet/src/features/images/NFTViewer.tsx b/packages/wallet/src/features/images/NFTViewer.tsx index 8bdcb11bee7..5324f4ccffd 100644 --- a/packages/wallet/src/features/images/NFTViewer.tsx +++ b/packages/wallet/src/features/images/NFTViewer.tsx @@ -56,7 +56,7 @@ export function NFTViewer(props: Props): JSX.Element { maxHeight={maxHeight ?? '100%'} width="100%"> - {placeholderContent || t('Content not available')} + {placeholderContent || t('tokens.nfts.error.unavailable')} ), diff --git a/packages/wallet/src/features/language/constants.ts b/packages/wallet/src/features/language/constants.ts index f16858e56bb..a970f3487b6 100644 --- a/packages/wallet/src/features/language/constants.ts +++ b/packages/wallet/src/features/language/constants.ts @@ -1,5 +1,5 @@ /** - * List of supported langauges in app, represented by ISO 639 language code. + * List of supported languages in app, represented by ISO 639 language code. * If you add a new locale here, be sure to add polyfills for it in intl.js, * resource strings in i18n.ts, and supported localizations in the Uniswap Xcode project. */ @@ -64,7 +64,7 @@ export const SUPPORTED_LANGUAGES: Language[] = [ ] /** - * Internal app mapping between langauge and locale enums + * Internal app mapping between language and locale enums * This is needed because we not support all locales and default languages to specific locales */ export const mapLanguageToLocale: Record = { @@ -90,7 +90,7 @@ export const mapLanguageToLocale: Record = { } /** - * Internal app mapping between langauge and locale enums + * Internal app mapping between language and locale enums * This is needed because we not support all locales and default languages to specific locales */ export const mapLocaleToLanguage: Record = { diff --git a/packages/wallet/src/features/language/hooks.tsx b/packages/wallet/src/features/language/hooks.tsx index 1200a07a0f8..0669d265e3f 100644 --- a/packages/wallet/src/features/language/hooks.tsx +++ b/packages/wallet/src/features/language/hooks.tsx @@ -42,116 +42,118 @@ export function useLanguageInfo(language: Language): LanguageInfo { const languageToLanguageInfo = useMemo((): Record => { return { [Language.ChineseSimplified]: { - displayName: t('Chinese, Simplified'), - originName: t('Chinese, Simplified', { lng: getLocale(Language.ChineseSimplified) }), + displayName: t('language.chineseSimplified'), + originName: t('language.chineseSimplified', { lng: getLocale(Language.ChineseSimplified) }), loggingName: 'Chinese, Simplified', locale: getLocale(Language.ChineseSimplified), }, [Language.ChineseTraditional]: { - displayName: t('Chinese, Traditional'), - originName: t('Chinese, Traditional', { lng: getLocale(Language.ChineseTraditional) }), + displayName: t('language.chineseTraditional'), + originName: t('language.chineseTraditional', { + lng: getLocale(Language.ChineseTraditional), + }), loggingName: 'Chinese, Traditional', locale: getLocale(Language.ChineseTraditional), }, [Language.Dutch]: { - displayName: t('Dutch'), - originName: t('Dutch', { lng: getLocale(Language.Dutch) }), + displayName: t('language.dutch'), + originName: t('language.dutch', { lng: getLocale(Language.Dutch) }), loggingName: 'Dutch', locale: getLocale(Language.Dutch), }, [Language.English]: { - displayName: t('English'), - originName: t('English', { lng: getLocale(Language.English) }), + displayName: t('language.english'), + originName: t('language.english', { lng: getLocale(Language.English) }), loggingName: 'English', locale: getLocale(Language.English), }, [Language.French]: { - displayName: t('French'), - originName: t('French', { lng: getLocale(Language.French) }), + displayName: t('language.french'), + originName: t('language.french', { lng: getLocale(Language.French) }), loggingName: 'French', locale: getLocale(Language.French), }, [Language.Hindi]: { - displayName: t('Hindi'), - originName: t('Hindi', { lng: getLocale(Language.Hindi) }), + displayName: t('language.hindi'), + originName: t('language.hindi', { lng: getLocale(Language.Hindi) }), loggingName: 'Hindi', locale: getLocale(Language.Hindi), }, [Language.Indonesian]: { - displayName: t('Indonesian'), - originName: t('Indonesian', { lng: getLocale(Language.Indonesian) }), + displayName: t('language.indonesian'), + originName: t('language.indonesian', { lng: getLocale(Language.Indonesian) }), loggingName: 'Indonesian', locale: getLocale(Language.Indonesian), }, [Language.Japanese]: { - displayName: t('Japanese'), - originName: t('Japanese', { lng: getLocale(Language.Japanese) }), + displayName: t('language.japanese'), + originName: t('language.japanese', { lng: getLocale(Language.Japanese) }), loggingName: 'Japanese', locale: getLocale(Language.Japanese), }, [Language.Malay]: { - displayName: t('Malay'), - originName: t('Malay', { lng: getLocale(Language.Malay) }), + displayName: t('language.malay'), + originName: t('language.malay', { lng: getLocale(Language.Malay) }), loggingName: 'Malay', locale: getLocale(Language.Malay), }, [Language.Portuguese]: { - displayName: t('Portuguese'), - originName: t('Portuguese', { lng: getLocale(Language.Portuguese) }), + displayName: t('language.portuguese'), + originName: t('language.portuguese', { lng: getLocale(Language.Portuguese) }), loggingName: 'Portuguese', locale: getLocale(Language.Portuguese), }, [Language.Russian]: { - displayName: t('Russian'), - originName: t('Russian', { lng: getLocale(Language.Russian) }), + displayName: t('language.russian'), + originName: t('language.russian', { lng: getLocale(Language.Russian) }), loggingName: 'Russian', locale: getLocale(Language.Russian), }, [Language.SpanishSpain]: { - displayName: t('Spanish (Spain)'), - originName: t('Spanish (Spain)', { lng: getLocale(Language.SpanishSpain) }), + displayName: t('language.spanishSpain'), + originName: t('language.spanishSpain', { lng: getLocale(Language.SpanishSpain) }), loggingName: 'Spanish (Spain)', locale: getLocale(Language.SpanishSpain), }, [Language.SpanishLatam]: { - displayName: t('Spanish (Latin America)'), - originName: t('Spanish (Latin America)', { lng: getLocale(Language.SpanishLatam) }), + displayName: t('language.spanishLatam'), + originName: t('language.spanishLatam', { lng: getLocale(Language.SpanishLatam) }), loggingName: 'Spanish (Latin America)', locale: getLocale(Language.SpanishLatam), }, [Language.SpanishUnitedStates]: { - displayName: t('Spanish (US)'), - originName: t('Spanish (US)', { lng: getLocale(Language.SpanishUnitedStates) }), + displayName: t('language.spanishUs'), + originName: t('language.spanishUs', { lng: getLocale(Language.SpanishUnitedStates) }), loggingName: 'Spanish (US)', locale: getLocale(Language.SpanishUnitedStates), }, [Language.Thai]: { - displayName: t('Thai'), - originName: t('Thai', { lng: getLocale(Language.Thai) }), + displayName: t('language.thai'), + originName: t('language.thai', { lng: getLocale(Language.Thai) }), loggingName: 'Thai', locale: getLocale(Language.Thai), }, [Language.Turkish]: { - displayName: t('Turkish'), - originName: t('Turkish', { lng: getLocale(Language.Turkish) }), + displayName: t('language.turkish'), + originName: t('language.turkish', { lng: getLocale(Language.Turkish) }), loggingName: 'Turkish', locale: getLocale(Language.Turkish), }, [Language.Ukrainian]: { - displayName: t('Ukrainian'), - originName: t('Ukrainian', { lng: getLocale(Language.Ukrainian) }), + displayName: t('language.ukrainian'), + originName: t('language.ukrainian', { lng: getLocale(Language.Ukrainian) }), loggingName: 'Ukrainian', locale: getLocale(Language.Ukrainian), }, [Language.Urdu]: { - displayName: t('Urdu'), - originName: t('Urdu', { lng: getLocale(Language.Urdu) }), + displayName: t('language.urdu'), + originName: t('language.urdu', { lng: getLocale(Language.Urdu) }), loggingName: 'Urdu', locale: getLocale(Language.Urdu), }, [Language.Vietnamese]: { - displayName: t('Vietnamese'), - originName: t('Vietnamese', { lng: getLocale(Language.Vietnamese) }), + displayName: t('language.vietnamese'), + originName: t('language.vietnamese', { lng: getLocale(Language.Vietnamese) }), loggingName: 'Vietnamese', locale: getLocale(Language.Vietnamese), }, diff --git a/packages/wallet/src/features/language/localizedDayjs.ts b/packages/wallet/src/features/language/localizedDayjs.ts index ddf47a72594..2621cf5859c 100644 --- a/packages/wallet/src/features/language/localizedDayjs.ts +++ b/packages/wallet/src/features/language/localizedDayjs.ts @@ -24,22 +24,22 @@ import { useCurrentLanguageInfo } from 'wallet/src/features/language/hooks' dayjs.extend(localizedFormat) export type DateFormat = 'l' | 'll' | 'LL' -export const FORMAT_DATE_SHORT: DateFormat = 'l' -export const FORMAT_DATE_MEDIUM: DateFormat = 'll' -export const FORMAT_DATE_LONG: DateFormat = 'LL' -export const FORMAT_DATE_MONTH = 'MMMM' +export const FORMAT_DATE_SHORT: DateFormat = 'l' // M/D/YYYY e.g. 8/16/2018 +export const FORMAT_DATE_MEDIUM: DateFormat = 'll' // MMM D, YYYY e.g. Aug 16, 2018 +export const FORMAT_DATE_LONG: DateFormat = 'LL' // MMMM D, YYYY e.g. August 16, 2018 +export const FORMAT_DATE_MONTH = 'MMMM' // January-December export const FORMAT_DATE_MONTH_YEAR = 'MMMM YYYY' -export const FORMAT_DATE_MONTH_DAY = 'MMM D' +export const FORMAT_DATE_MONTH_DAY = 'MMM D' // Jan-Dec 1-31 export type TimeFormat = 'LT' | 'LTS' -export const FORMAT_TIME_SHORT: TimeFormat = 'LT' -export const FORMAT_TIME_MEDIUM: TimeFormat = 'LTS' +export const FORMAT_TIME_SHORT: TimeFormat = 'LT' // h:mm A e.g. 8:02 PM +export const FORMAT_TIME_MEDIUM: TimeFormat = 'LTS' // h:mm:ss A e.g. 8:02:18 PM export type DateTimeFormat = 'lll' | 'LLL' | 'llll' | 'LLLL' -export const FORMAT_DATE_TIME_SHORT = 'lll' -export const FORMAT_DATE_TIME_MEDIUM = 'LLL' -export const FORMAT_DATE_TIME_LONG = 'llll' -export const FORMAT_DATE_TIME_FULL = 'LLLL' +export const FORMAT_DATE_TIME_SHORT = 'lll' // MMM D, YYYY h:mm A e.g. Aug 16, 2018 8:02 PM +export const FORMAT_DATE_TIME_MEDIUM = 'LLL' // MMMM D, YYYY h:mm A e.g. August 16, 2018 8:02 PM +export const FORMAT_DATE_TIME_LONG = 'llll' // ddd, MMM D, YYYY h:mm A e.g. Thu, Aug 16, 2018 8:02 PM +export const FORMAT_DATE_TIME_FULL = 'LLLL' // dddd, MMMM D, YYYY h:mm A e.g. Thursday, August 16, 2018 8:02 PM export type LocalizedDayjs = typeof dayjs diff --git a/packages/wallet/src/features/notifications/builtReceiveNotification.test.ts b/packages/wallet/src/features/notifications/builtReceiveNotification.test.ts index 9446c905814..3bebc29f49f 100644 --- a/packages/wallet/src/features/notifications/builtReceiveNotification.test.ts +++ b/packages/wallet/src/features/notifications/builtReceiveNotification.test.ts @@ -1,30 +1,28 @@ -import { ChainId } from 'wallet/src/constants/chains' import { AssetType } from 'wallet/src/entities/assets' import { buildReceiveNotification } from 'wallet/src/features/notifications/buildReceiveNotification' import { createFinalizedTxAction } from 'wallet/src/features/notifications/notificationWatcherSaga.test' -import { AppNotificationType } from 'wallet/src/features/notifications/types' import { ReceiveTokenTransactionInfo, TransactionStatus, TransactionType, } from 'wallet/src/features/transactions/types' -import { SAMPLE_SEED_ADDRESS_1, account } from 'wallet/src/test/fixtures' +import { + receiveCurrencyTxNotification, + receiveNFTNotification, + receiveTokenTransactionInfo, + signerMnemonicAccount, +} from 'wallet/src/test/fixtures' + +const account = signerMnemonicAccount() -const receiveCurrencyTypeInfo: ReceiveTokenTransactionInfo = { - type: TransactionType.Receive, +const receiveCurrencyTypeInfo: ReceiveTokenTransactionInfo = receiveTokenTransactionInfo({ assetType: AssetType.Currency, - currencyAmountRaw: '1000', - sender: '0x000123abc456def', - tokenAddress: '0xUniswapToken', -} +}) -const receiveNftTypeInfo: ReceiveTokenTransactionInfo = { - type: TransactionType.Receive, +const receiveNftTypeInfo: ReceiveTokenTransactionInfo = receiveTokenTransactionInfo({ assetType: AssetType.ERC1155, - sender: '0x000123abc456def', - tokenAddress: '0xUniswapToken', tokenId: '420', -} +}) describe(buildReceiveNotification, () => { it('returns undefined if not successful status', () => { @@ -36,7 +34,7 @@ describe(buildReceiveNotification, () => { it('returns undefined if not receive', () => { const { payload: testTransaction } = createFinalizedTxAction(receiveCurrencyTypeInfo) - testTransaction.typeInfo.type = TransactionType.Send // overwite type to incorrect type + testTransaction.typeInfo.type = TransactionType.Send // overwite type to incorrect type expect(buildReceiveNotification(testTransaction, account.address)).toBeUndefined() }) @@ -44,37 +42,36 @@ describe(buildReceiveNotification, () => { it('builds correct notification object for nft receive', () => { const { payload: testTransaction } = createFinalizedTxAction(receiveNftTypeInfo) - expect(buildReceiveNotification(testTransaction, account.address)).toEqual({ - address: SAMPLE_SEED_ADDRESS_1, - assetType: AssetType.ERC1155, - chainId: ChainId.Mainnet, - sender: '0x000123abc456def', - tokenAddress: '0xUniswapToken', - tokenId: '420', - txHash: '0x123', - txId: 'uuid-4', - txStatus: TransactionStatus.Success, - txType: TransactionType.Receive, - type: AppNotificationType.Transaction, - }) + expect(buildReceiveNotification(testTransaction, account.address)).toEqual( + receiveNFTNotification({ + assetType: AssetType.ERC1155, + tokenId: receiveNftTypeInfo.tokenId, + chainId: testTransaction.chainId, + sender: receiveNftTypeInfo.sender, + address: account.address, + tokenAddress: receiveNftTypeInfo.tokenAddress, + txHash: testTransaction.hash, + txId: testTransaction.id, + txStatus: testTransaction.status, + }) + ) }) it('builds correct notification object for currency receive', () => { const { payload: testTransaction } = createFinalizedTxAction(receiveCurrencyTypeInfo) testTransaction.typeInfo.type = TransactionType.Receive // overwrite to correct txn type (default is send) - expect(buildReceiveNotification(testTransaction, account.address)).toEqual({ - address: SAMPLE_SEED_ADDRESS_1, - assetType: AssetType.Currency, - chainId: ChainId.Mainnet, - sender: '0x000123abc456def', - tokenAddress: '0xUniswapToken', - txHash: '0x123', - txId: 'uuid-4', - txStatus: TransactionStatus.Success, - txType: TransactionType.Receive, - type: AppNotificationType.Transaction, - currencyAmountRaw: '1000', - }) + expect(buildReceiveNotification(testTransaction, account.address)).toEqual( + receiveCurrencyTxNotification({ + address: account.address, + chainId: testTransaction.chainId, + currencyAmountRaw: receiveCurrencyTypeInfo.currencyAmountRaw, + sender: receiveCurrencyTypeInfo.sender, + tokenAddress: receiveCurrencyTypeInfo.tokenAddress, + txHash: testTransaction.hash, + txId: testTransaction.id, + txStatus: testTransaction.status, + }) + ) }) }) diff --git a/packages/wallet/src/features/notifications/components/ChangeAssetVisibilityNotification.tsx b/packages/wallet/src/features/notifications/components/ChangeAssetVisibilityNotification.tsx index 0286dca4942..fbd6efcac12 100644 --- a/packages/wallet/src/features/notifications/components/ChangeAssetVisibilityNotification.tsx +++ b/packages/wallet/src/features/notifications/components/ChangeAssetVisibilityNotification.tsx @@ -35,8 +35,8 @@ export function ChangeAssetVisibilityNotification({ } title={ visible - ? t('{{assetName}} hidden', { assetName }) - : t('{{assetName}} unhidden', { assetName }) + ? t('notification.assetVisibility.hidden', { assetName }) + : t('notification.assetVisibility.unhidden', { assetName }) } /> ) diff --git a/packages/wallet/src/features/notifications/components/ChooseCountryNotification.tsx b/packages/wallet/src/features/notifications/components/ChooseCountryNotification.tsx index fc309d564f3..7e8fd6e175f 100644 --- a/packages/wallet/src/features/notifications/components/ChooseCountryNotification.tsx +++ b/packages/wallet/src/features/notifications/components/ChooseCountryNotification.tsx @@ -22,7 +22,7 @@ export function ChooseCountryNotification({ } - title={t('Switched to {{name}}', { name: countryName })} + title={t('notification.countryChange', { countryName })} /> ) } diff --git a/packages/wallet/src/features/notifications/components/CopiedNotification.tsx b/packages/wallet/src/features/notifications/components/CopiedNotification.tsx index 1c5abc2ce4b..c317016b945 100644 --- a/packages/wallet/src/features/notifications/components/CopiedNotification.tsx +++ b/packages/wallet/src/features/notifications/components/CopiedNotification.tsx @@ -12,16 +12,16 @@ export function CopiedNotification({ let title switch (copyType) { case CopyNotificationType.Address: - title = t('Address copied') + title = t('notification.copied.address') break case CopyNotificationType.ContractAddress: - title = t('Contract address copied') + title = t('notification.copied.contractAddress') break case CopyNotificationType.TransactionId: - title = t('Transaction ID copied') + title = t('notification.copied.transactionId') break case CopyNotificationType.Image: - title = t('Image copied') + title = t('notification.copied.image') break } diff --git a/packages/wallet/src/features/notifications/components/CopyFailedNotification.tsx b/packages/wallet/src/features/notifications/components/CopyFailedNotification.tsx index 080aaf4b623..bfb599171c6 100644 --- a/packages/wallet/src/features/notifications/components/CopyFailedNotification.tsx +++ b/packages/wallet/src/features/notifications/components/CopyFailedNotification.tsx @@ -8,7 +8,7 @@ export function CopyFailedNotification({ notification: CopyFailedNotificationType }): JSX.Element | null { const { t } = useTranslation() - const title = t('Failed to copy to clipboard') + const title = t('notification.copied.failed') return } diff --git a/packages/wallet/src/features/notifications/components/SwapNetworkNotification.tsx b/packages/wallet/src/features/notifications/components/SwapNetworkNotification.tsx index 723531492d7..128b3aad875 100644 --- a/packages/wallet/src/features/notifications/components/SwapNetworkNotification.tsx +++ b/packages/wallet/src/features/notifications/components/SwapNetworkNotification.tsx @@ -18,7 +18,7 @@ export function SwapNetworkNotification({ smallToast hideDelay={hideDelay} icon={} - title={t('Swapping on {{ network }}', { network })} + title={t('notification.swap.network', { network })} /> ) } diff --git a/packages/wallet/src/features/notifications/components/SwapNotification.tsx b/packages/wallet/src/features/notifications/components/SwapNotification.tsx index b80f0eba667..b39a473445b 100644 --- a/packages/wallet/src/features/notifications/components/SwapNotification.tsx +++ b/packages/wallet/src/features/notifications/components/SwapNotification.tsx @@ -55,7 +55,7 @@ export function SwapNotification({ const retryButton = txStatus === TransactionStatus.Failed ? { - title: t('Retry'), + title: t('common.button.retry'), onPress: onRetry, } : undefined diff --git a/packages/wallet/src/features/notifications/components/SwapPendingNotification.tsx b/packages/wallet/src/features/notifications/components/SwapPendingNotification.tsx index 2ced0616a97..84ee4f49020 100644 --- a/packages/wallet/src/features/notifications/components/SwapPendingNotification.tsx +++ b/packages/wallet/src/features/notifications/components/SwapPendingNotification.tsx @@ -31,10 +31,10 @@ export function SwapPendingNotification({ function getNotificationText(wrapType: WrapType, t: TFunction): string { switch (wrapType) { case WrapType.NotApplicable: - return t('Swap pending') + return t('notification.swap.pending.swap') case WrapType.Unwrap: - return t('Unwrap pending') + return t('notification.swap.pending.unwrap') case WrapType.Wrap: - return t('Wrap pending') + return t('notification.swap.pending.wrap') } } diff --git a/packages/wallet/src/features/notifications/components/TransferCurrencyPendingNotification.tsx b/packages/wallet/src/features/notifications/components/TransferCurrencyPendingNotification.tsx index b5579cfaf66..5d6531186f5 100644 --- a/packages/wallet/src/features/notifications/components/TransferCurrencyPendingNotification.tsx +++ b/packages/wallet/src/features/notifications/components/TransferCurrencyPendingNotification.tsx @@ -18,7 +18,7 @@ export function TransferCurrencyPendingNotification({ smallToast hideDelay={TRANSACTION_PENDING_NOTIFICATION_DELAY} icon={} - title={t(`{{symbol}} transfer pending`, { symbol: currencyInfo.currency.symbol })} + title={t('notification.transfer.pending', { currencySymbol: currencyInfo.currency.symbol })} /> ) } diff --git a/packages/wallet/src/features/notifications/components/WrapNotification.tsx b/packages/wallet/src/features/notifications/components/WrapNotification.tsx index 5c3a4f83ecc..bf35a7f5626 100644 --- a/packages/wallet/src/features/notifications/components/WrapNotification.tsx +++ b/packages/wallet/src/features/notifications/components/WrapNotification.tsx @@ -52,7 +52,7 @@ export function WrapNotification({ const retryButton = txStatus === TransactionStatus.Failed ? { - title: t('Retry'), + title: t('common.button.retry'), onPress: onRetry, } : undefined diff --git a/packages/wallet/src/features/notifications/notificationWatcherSaga.test.ts b/packages/wallet/src/features/notifications/notificationWatcherSaga.test.ts index 24ea5fb37bf..5570863e0e8 100644 --- a/packages/wallet/src/features/notifications/notificationWatcherSaga.test.ts +++ b/packages/wallet/src/features/notifications/notificationWatcherSaga.test.ts @@ -17,7 +17,9 @@ import { TransactionTypeInfo, UnknownTransactionInfo, } from 'wallet/src/features/transactions/types' -import { finalizedTxAction } from 'wallet/src/test/fixtures' +import { finalizedTransactionAction } from 'wallet/src/test/fixtures' + +const finalizedTxAction = finalizedTransactionAction() const txId = 'uuid-4' diff --git a/packages/wallet/src/features/notifications/types.ts b/packages/wallet/src/features/notifications/types.ts index 2c58bde588a..4660d962462 100644 --- a/packages/wallet/src/features/notifications/types.ts +++ b/packages/wallet/src/features/notifications/types.ts @@ -26,7 +26,7 @@ export enum AppNotificationType { ScantasticComplete, } -interface AppNotificationBase { +export interface AppNotificationBase { type: AppNotificationType address?: Address hideDelay?: number @@ -80,7 +80,7 @@ export interface WrapTxNotification extends TransactionNotificationBase { unwrapped: boolean } -interface TransferCurrencyTxNotificationBase extends TransactionNotificationBase { +export interface TransferCurrencyTxNotificationBase extends TransactionNotificationBase { txType: TransactionType.Send | TransactionType.Receive assetType: AssetType.Currency tokenAddress: string @@ -97,7 +97,7 @@ export interface ReceiveCurrencyTxNotification extends TransferCurrencyTxNotific sender: Address } -interface TransferNFTNotificationBase extends TransactionNotificationBase { +export interface TransferNFTNotificationBase extends TransactionNotificationBase { txType: TransactionType.Send | TransactionType.Receive assetType: AssetType.ERC1155 | AssetType.ERC721 tokenAddress: string diff --git a/packages/wallet/src/features/notifications/utils.test.ts b/packages/wallet/src/features/notifications/utils.test.ts index 46fd35ad19c..318f77d01bf 100644 --- a/packages/wallet/src/features/notifications/utils.test.ts +++ b/packages/wallet/src/features/notifications/utils.test.ts @@ -2,7 +2,7 @@ import { TradeType } from '@uniswap/sdk-core' import { DAI, USDC } from 'wallet/src/constants/tokens' import { TransactionStatus } from 'wallet/src/features/transactions/types' import { initializeTranslation } from 'wallet/src/i18n/i18n' -import { mockLocalizedFormatter } from 'wallet/src/test/utils' +import { mockLocalizedFormatter } from 'wallet/src/test/mocks' import { formSwapNotificationTitle } from './utils' describe(formSwapNotificationTitle, () => { @@ -45,7 +45,7 @@ describe(formSwapNotificationTitle, () => { expect( formSwapNotificationTitle( mockLocalizedFormatter, - TransactionStatus.Cancelled, + TransactionStatus.Canceled, DAI, USDC, '1-DAI', diff --git a/packages/wallet/src/features/notifications/utils.ts b/packages/wallet/src/features/notifications/utils.ts index 0e2df8f3ec7..41c8e94700b 100644 --- a/packages/wallet/src/features/notifications/utils.ts +++ b/packages/wallet/src/features/notifications/utils.ts @@ -20,23 +20,23 @@ export const formWCNotificationTitle = (appNotification: WalletConnectNotificati switch (event) { case WalletConnectEvent.Connected: - return i18n.t('Connected') + return i18n.t('notification.walletConnect.connected') case WalletConnectEvent.Disconnected: - return i18n.t('Disconnected') + return i18n.t('notification.walletConnect.disconnected') case WalletConnectEvent.NetworkChanged: { const supportedChainId = toSupportedChainId(chainId) if (supportedChainId) { - return i18n.t('Switched to {{name}}', { - name: CHAIN_INFO[supportedChainId].label, + return i18n.t('notification.walletConnect.networkChanged.full', { + networkName: CHAIN_INFO[supportedChainId].label, }) } } - return i18n.t('Switched networks') + return i18n.t('notification.walletConnect.networkChanged.short') case WalletConnectEvent.TransactionConfirmed: - return i18n.t('Transaction confirmed with {{dappName}}', { dappName }) + return i18n.t('notification.walletConnect.confirmed', { dappName }) case WalletConnectEvent.TransactionFailed: - return i18n.t('Transaction failed with {{dappName}}', { dappName }) + return i18n.t('notification.walletConnect.failed', { dappName }) } } @@ -49,16 +49,15 @@ export const formApproveNotificationTitle = ( const currencyDisplayText = getCurrencyDisplayText(currency, tokenAddress) const address = shortenAddress(spender) return txStatus === TransactionStatus.Success - ? i18n.t('Approved {{currencySymbol}} for use with {{address}}.', { + ? i18n.t('notification.transaction.approve.success', { currencySymbol: currencyDisplayText, address, }) - : txStatus === TransactionStatus.Cancelled - ? i18n.t('Canceled {{currencySymbol}} approve.', { + : txStatus === TransactionStatus.Canceled + ? i18n.t('notification.transaction.approve.canceled', { currencySymbol: currencyDisplayText, - address, }) - : i18n.t('Failed to approve {{currencySymbol}} for use with {{address}}.', { + : i18n.t('notification.transaction.approve.fail', { currencySymbol: currencyDisplayText, address, }) @@ -97,22 +96,22 @@ export const formSwapNotificationTitle = ( tradeType === TradeType.EXACT_INPUT ) - const inputAssetInfo = `${inputAmount}${inputCurrencySymbol}` - const outputAssetInfo = `${outputAmount}${outputCurrencySymbol}` + const inputCurrencyAmountWithSymbol = `${inputAmount}${inputCurrencySymbol}` + const outputCurrencyAmountWithSymbol = `${outputAmount}${outputCurrencySymbol}` return txStatus === TransactionStatus.Success - ? i18n.t('Swapped {{inputAssetInfo}} for {{outputAssetInfo}}.', { - inputAssetInfo, - outputAssetInfo, + ? i18n.t('notification.transaction.swap.success', { + inputCurrencyAmountWithSymbol, + outputCurrencyAmountWithSymbol, }) - : txStatus === TransactionStatus.Cancelled - ? i18n.t('Canceled {{inputCurrencySymbol}}-{{outputCurrencySymbol}} swap.', { + : txStatus === TransactionStatus.Canceled + ? i18n.t('notification.transaction.swap.canceled', { inputCurrencySymbol, outputCurrencySymbol, }) - : i18n.t('Failed to swap {{inputAssetInfo}} for {{outputAssetInfo}}.', { - inputAssetInfo, - outputAssetInfo, + : i18n.t('notification.transaction.swap.fail', { + inputCurrencyAmountWithSymbol, + outputCurrencyAmountWithSymbol, }) } @@ -130,34 +129,34 @@ export const formWrapNotificationTitle = ( const inputAmount = getFormattedCurrencyAmount(inputCurrency, currencyAmountRaw, formatter) const outputAmount = getFormattedCurrencyAmount(outputCurrency, currencyAmountRaw, formatter) - const inputAssetInfo = `${inputAmount}${inputCurrencySymbol}` - const outputAssetInfo = `${outputAmount}${outputCurrencySymbol}` + const inputCurrencyAmountWithSymbol = `${inputAmount}${inputCurrencySymbol}` + const outputCurrencyAmountWithSymbol = `${outputAmount}${outputCurrencySymbol}` if (unwrapped) { return txStatus === TransactionStatus.Success - ? i18n.t('Unwrapped {{inputAssetInfo}} and received {{outputAssetInfo}}.', { - inputAssetInfo, - outputAssetInfo, + ? i18n.t('notification.transaction.unwrap.success', { + inputCurrencyAmountWithSymbol, + outputCurrencyAmountWithSymbol, }) - : txStatus === TransactionStatus.Cancelled - ? i18n.t('Canceled {{inputCurrencySymbol}} unwrap.', { + : txStatus === TransactionStatus.Canceled + ? i18n.t('notification.transaction.unwrap.canceled', { inputCurrencySymbol, }) - : i18n.t('Failed to unwrap {{inputAssetInfo}}.', { - inputAssetInfo, + : i18n.t('notification.transaction.unwrap.fail', { + inputCurrencyAmountWithSymbol, }) } return txStatus === TransactionStatus.Success - ? i18n.t('Wrapped {{inputAssetInfo}} and received {{outputAssetInfo}}.', { - inputAssetInfo, - outputAssetInfo, + ? i18n.t('notification.transaction.wrap.success', { + inputCurrencyAmountWithSymbol, + outputCurrencyAmountWithSymbol, }) - : txStatus === TransactionStatus.Cancelled - ? i18n.t('Canceled {{inputCurrencySymbol}} wrap.', { + : txStatus === TransactionStatus.Canceled + ? i18n.t('notification.transaction.wrap.canceled', { inputCurrencySymbol, }) - : i18n.t('Failed to wrap {{inputAssetInfo}}.', { - inputAssetInfo, + : i18n.t('notification.transaction.wrap.fail', { + inputCurrencyAmountWithSymbol, }) } @@ -194,41 +193,47 @@ export const formUnknownTxTitle = ( tokenAddress: Address | undefined, ensName: string | null ): string => { - let addressText - if (ensName) { - addressText = i18n.t(' with {{ensName}}', { ensName }) - } else if (tokenAddress) { - const address = shortenAddress(tokenAddress) - addressText = i18n.t(' with {{address}}', { address }) - } else { - addressText = '' + const address = tokenAddress && shortenAddress(tokenAddress) + const target = ensName ?? address + + if (txStatus === TransactionStatus.Success) { + if (target) { + return i18n.t('notification.transaction.unknown.success.full', { addressOrEnsName: target }) + } + return i18n.t('notification.transaction.unknown.success.short') } - return txStatus === TransactionStatus.Success - ? i18n.t('Transacted{{addressText}}.', { addressText }) - : i18n.t('Failed to transact{{addressText}}.', { addressText }) + if (target) { + return i18n.t('notification.transaction.unknown.fail.full', { addressOrEnsName: target }) + } + return i18n.t('notification.transaction.unknown.fail.short') } const formTransferTxTitle = ( txType: TransactionType, txStatus: TransactionStatus, - assetInfo: string, - senderOrRecipient: string + tokenNameOrAddress: string, + walletNameOrAddress: string ): string => { if (txType === TransactionType.Send) { return txStatus === TransactionStatus.Success - ? i18n.t('Sent {{assetInfo}} to {{senderOrRecipient}}.', { assetInfo, senderOrRecipient }) - : txStatus === TransactionStatus.Cancelled - ? i18n.t('Canceled {{assetInfo}} send.', { assetInfo, senderOrRecipient }) - : i18n.t('Failed to send {{assetInfo}} to {{senderOrRecipient}}.', { - assetInfo, - senderOrRecipient, + ? i18n.t('notification.transaction.transfer.success', { + tokenNameOrAddress, + walletNameOrAddress, + }) + : txStatus === TransactionStatus.Canceled + ? i18n.t('notification.transaction.transfer.canceled', { + tokenNameOrAddress, + }) + : i18n.t('notification.transaction.transfer.fail', { + tokenNameOrAddress, + walletNameOrAddress, }) } - return i18n.t('Received {{assetInfo}} from {{senderOrRecipient}}.', { - assetInfo, - senderOrRecipient, + return i18n.t('notification.transaction.transfer.received', { + tokenNameOrAddress, + walletNameOrAddress, }) } diff --git a/packages/wallet/src/features/portfolio/HiddenTokensRow.tsx b/packages/wallet/src/features/portfolio/HiddenTokensRow.tsx index 1f47782d159..55bb547c1ff 100644 --- a/packages/wallet/src/features/portfolio/HiddenTokensRow.tsx +++ b/packages/wallet/src/features/portfolio/HiddenTokensRow.tsx @@ -29,10 +29,10 @@ export function HiddenTokensRow({ px={padded ? '$spacing24' : '$none'} py="$spacing12"> - {t('Hidden ({{numHidden}})', { numHidden })} + {t('tokens.hidden.label', { numHidden })} {/* just used for opacity styling, the parent TouchableArea handles event */} - + - {isExpanded ? t('Hide') : t('Show')} + {isExpanded ? t('common.button.hide') : t('common.button.show')} diff --git a/packages/wallet/src/features/portfolio/TokenBalanceItem.tsx b/packages/wallet/src/features/portfolio/TokenBalanceItem.tsx index fef023b4c90..c8b0a5d9aca 100644 --- a/packages/wallet/src/features/portfolio/TokenBalanceItem.tsx +++ b/packages/wallet/src/features/portfolio/TokenBalanceItem.tsx @@ -77,7 +77,7 @@ export const TokenBalanceItem = memo(function _TokenBalanceItem({ {!portfolioBalance.balanceUSD ? ( - {t('N/A')} + {t('common.text.notAvailable')} ) : ( diff --git a/packages/wallet/src/features/portfolio/api.ts b/packages/wallet/src/features/portfolio/api.ts index ed77f664724..a6b77781d61 100644 --- a/packages/wallet/src/features/portfolio/api.ts +++ b/packages/wallet/src/features/portfolio/api.ts @@ -1,6 +1,6 @@ import { Currency, CurrencyAmount, NativeCurrency as NativeCurrencyClass } from '@uniswap/sdk-core' import { useMemo } from 'react' -import ERC20_ABI from 'wallet/src/abis/erc20.json' +import ERC20_ABI from 'uniswap/src/abis/erc20.json' import { ChainId } from 'wallet/src/constants/chains' import { useRestQuery } from 'wallet/src/data/rest' import { getPollingIntervalByBlocktime } from 'wallet/src/features/chains/utils' diff --git a/packages/wallet/src/features/search/SearchTextInput.tsx b/packages/wallet/src/features/search/SearchTextInput.tsx index 1ed2685e1ca..817977e884d 100644 --- a/packages/wallet/src/features/search/SearchTextInput.tsx +++ b/packages/wallet/src/features/search/SearchTextInput.tsx @@ -235,7 +235,7 @@ export const SearchTextInput = forwardRef x={isFocus ? 0 : dimensions.fullWidth} onLayout={onCancelButtonLayout}> - {t('Cancel')} + {t('common.button.cancel')} )} diff --git a/packages/wallet/src/features/tokens/TokenWarningModal.tsx b/packages/wallet/src/features/tokens/TokenWarningModal.tsx index 3623726a100..83d5078a5c4 100644 --- a/packages/wallet/src/features/tokens/TokenWarningModal.tsx +++ b/packages/wallet/src/features/tokens/TokenWarningModal.tsx @@ -15,15 +15,11 @@ import { ElementName, ModalName } from 'wallet/src/telemetry/constants' function getTokenSafetyBodyText(safetyLevel: Maybe, t: AppTFunction): string { switch (safetyLevel) { case SafetyLevel.MediumWarning: - return t( - 'This token isn’t traded on leading U.S. centralized exchanges. Always conduct your own research before trading.' - ) + return t('token.safetyLevel.medium.message') case SafetyLevel.StrongWarning: - return t( - 'This token isn’t traded on leading U.S. centralized exchanges or frequently swapped on Uniswap. Always conduct your own research before trading.' - ) + return t('token.safetyLevel.strong.message') case SafetyLevel.Blocked: - return t('You can’t trade this token using the Uniswap Wallet.') + return t('token.safetyLevel.blocked.message') default: return '' } @@ -57,7 +53,7 @@ export default function TokenWarningModal({ // always hide accept button if blocked token const hideAcceptButton = disableAccept || safetyLevel === SafetyLevel.Blocked - const closeButtonText = hideAcceptButton ? t('Close') : t('Back') + const closeButtonText = hideAcceptButton ? t('common.button.close') : t('common.button.back') const showWarningIcon = safetyLevel === SafetyLevel.StrongWarning || safetyLevel === SafetyLevel.Blocked @@ -101,7 +97,7 @@ export default function TokenWarningModal({ testID={ElementName.TokenWarningAccept} theme={getButtonTheme(safetyLevel)} onPress={onAccept}> - {showWarningIcon ? t('I understand') : t('Continue')} + {showWarningIcon ? t('common.button.understand') : t('common.button.continue')} )} diff --git a/packages/wallet/src/features/tokens/utils.ts b/packages/wallet/src/features/tokens/utils.ts index f1156de647a..b440e637a4f 100644 --- a/packages/wallet/src/features/tokens/utils.ts +++ b/packages/wallet/src/features/tokens/utils.ts @@ -7,10 +7,10 @@ export function getTokenSafetyHeaderText( ): string | undefined { switch (safetyLevel) { case SafetyLevel.MediumWarning: - return t('Caution') + return t('token.safetyLevel.medium.header') case SafetyLevel.StrongWarning: - return t('Warning') + return t('token.safetyLevel.strong.header') case SafetyLevel.Blocked: - return t('Not available') + return t('token.safetyLevel.blocked.header') } } diff --git a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/ApproveSummaryItem.tsx b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/ApproveSummaryItem.tsx index 613b672d262..8ca8b5c6d75 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/ApproveSummaryItem.tsx +++ b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/ApproveSummaryItem.tsx @@ -37,7 +37,7 @@ export function ApproveSummaryItem({ const amount = approvalAmount === INFINITE_AMOUNT - ? t('Unlimited') + ? t('transaction.amount.unlimited') : approvalAmount && approvalAmount !== ZERO_AMOUNT ? formatNumberOrString({ value: approvalAmount, type: NumberType.TokenNonTx }) : '' diff --git a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/CancelConfirmationView.tsx b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/CancelConfirmationView.tsx index 5f3da6665bb..8135f702613 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/CancelConfirmationView.tsx +++ b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/CancelConfirmationView.tsx @@ -73,11 +73,9 @@ export function CancelConfirmationView({ - {t('Cancel this transaction?')} + {t('transaction.action.cancel.title')} - {t( - 'If you cancel this transaction before it’s processed by the network, you’ll pay a new network fee instead of the original one.' - )} + {t('transaction.action.cancel.description')} - {t('Network cost')} + {t('transaction.networkCost.label')} {!gasFeeUSD ? : {gasFee}} {accountAddress && ( @@ -106,7 +104,7 @@ export function CancelConfirmationView({ diff --git a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/FiatPurchaseSummaryItem.tsx b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/FiatPurchaseSummaryItem.tsx index a270da7f948..d7b3ea42196 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/FiatPurchaseSummaryItem.tsx +++ b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/FiatPurchaseSummaryItem.tsx @@ -53,7 +53,9 @@ export function FiatPurchaseSummaryItem({ }) const cryptoSymbol = - outputSymbol ?? getSymbolDisplayText(outputCurrencyInfo?.currency.symbol) ?? t('unknown token') + outputSymbol ?? + getSymbolDisplayText(outputCurrencyInfo?.currency.symbol) ?? + t('transaction.currency.unknown') const cryptoPurchaseAmount = formatNumberOrString({ value: outputCurrencyAmount }) + ' ' + cryptoSymbol @@ -67,7 +69,7 @@ export function FiatPurchaseSummaryItem({ outputCurrencyAmount !== undefined && outputCurrencyAmount !== null ? isTransfer ? cryptoPurchaseAmount - : t('{{cryptoAmount}} for {{fiatAmount}}', { + : t('fiatOnRamp.summary.total', { cryptoAmount: cryptoPurchaseAmount, fiatAmount: fiatPurchaseAmount, }) diff --git a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionActionsModal.tsx b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionActionsModal.tsx index 1b361ad15d6..1d350571280 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionActionsModal.tsx +++ b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionActionsModal.tsx @@ -92,7 +92,7 @@ export default function TransactionActionsModal({ key: inputCurrencyInfo.currencyId, onPress: () => onViewTokenDetails(inputCurrencyInfo.currencyId), render: renderOptionItem( - t('View {{ tokenSymbol }}', { + t('transaction.action.view', { tokenSymbol: inputCurrencyInfo?.currency.symbol, }) ), @@ -101,7 +101,7 @@ export default function TransactionActionsModal({ key: outputCurrencyInfo.currencyId, onPress: () => onViewTokenDetails(outputCurrencyInfo.currencyId), render: renderOptionItem( - t('View {{ tokenSymbol }}', { + t('transaction.action.view', { tokenSymbol: outputCurrencyInfo?.currency.symbol, }) ), @@ -114,7 +114,7 @@ export default function TransactionActionsModal({ { key: ElementName.MoonpayExplorerView, onPress: onViewMoonpay, - render: renderOptionItem(t('View on MoonPay')), + render: renderOptionItem(t('transaction.action.viewMoonPay')), }, ] : [] @@ -127,7 +127,7 @@ export default function TransactionActionsModal({ key: ElementName.EtherscanView, onPress: onExplore, render: renderOptionItem( - t('View on {{ blockExplorerName }}', { + t('transaction.action.viewEtherscan', { blockExplorerName: chainInfo.explorer.name, }) ), @@ -156,8 +156,8 @@ export default function TransactionActionsModal({ handleClose() }, render: onViewMoonpay - ? renderOptionItem(t('Copy MoonPay transaction ID')) - : renderOptionItem(t('Copy transaction ID')), + ? renderOptionItem(t('transaction.action.copyMoonPay')) + : renderOptionItem(t('transaction.action.copy')), }, ] : [] @@ -178,14 +178,14 @@ export default function TransactionActionsModal({ handleClose() }, - render: renderOptionItem(t('Get help')), + render: renderOptionItem(t('settings.action.help')), }, ] if (showCancelButton) { transactionActionOptions.push({ key: ElementName.Cancel, onPress: onCancel, - render: renderOptionItem(t('Cancel transaction'), '$statusCritical'), + render: renderOptionItem(t('transaction.action.cancel.button'), '$statusCritical'), }) } return transactionActionOptions @@ -213,7 +213,7 @@ export default function TransactionActionsModal({ - {t('Submitted on') + ' ' + dateString} + {t('transaction.date', { date: dateString })} } options={options} diff --git a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout.tsx b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout.tsx index bec2f94ccf3..878ed097f1e 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout.tsx +++ b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout.tsx @@ -59,7 +59,7 @@ function TransactionSummaryLayout({ const inProgress = status === TransactionStatus.Cancelling || status === TransactionStatus.Pending const inCancelling = - status === TransactionStatus.Cancelled || status === TransactionStatus.Cancelling + status === TransactionStatus.Canceled || status === TransactionStatus.Cancelling // Monitor latest nonce to identify queued transactions. const lowestPendingNonce = useLowestPendingNonce() @@ -163,7 +163,7 @@ function TransactionSummaryLayout({ {status === TransactionStatus.Failed && onRetry && ( - {t('Retry')} + {t('common.button.retry')} )} diff --git a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransferTokenSummaryItem.tsx b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransferTokenSummaryItem.tsx index 6cb93fb6531..ecfdd577849 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransferTokenSummaryItem.tsx +++ b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransferTokenSummaryItem.tsx @@ -92,17 +92,21 @@ export function TransferTokenSummaryItem({ const { unitag } = useUnitagByAddress(otherAddress) const personDisplayName = unitag?.username ?? ensName ?? shortenAddress(otherAddress) - const translateOptions = { - what: isCurrency - ? (currencyAmount ?? '') + (getSymbolDisplayText(currencyInfo?.currency?.symbol) ?? '') - : transaction.typeInfo.nftSummaryInfo?.name, - } + const tokenAmountWithSymbol = isCurrency + ? (currencyAmount ?? '') + (getSymbolDisplayText(currencyInfo?.currency?.symbol) ?? '') + : transaction.typeInfo.nftSummaryInfo?.name let caption = '' if (transactionType === TransactionType.Send) { - caption = t('{{what}} to {{recipient}}', { recipient: personDisplayName, ...translateOptions }) + caption = t('transaction.summary.received', { + recipientAddress: personDisplayName, + tokenAmountWithSymbol, + }) } else { - caption = t('{{what}} from {{sender}}', { sender: personDisplayName, ...translateOptions }) + caption = t('transaction.summary.sent', { + senderAddress: personDisplayName, + tokenAmountWithSymbol, + }) } return createElement(layoutElement as React.FunctionComponent, { diff --git a/packages/wallet/src/features/transactions/SummaryCards/utils.ts b/packages/wallet/src/features/transactions/SummaryCards/utils.ts index 2de4e5cc3c1..f966a0206b0 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/utils.ts +++ b/packages/wallet/src/features/transactions/SummaryCards/utils.ts @@ -115,46 +115,149 @@ export function generateActivityItemRenderer( function getTransactionTypeVerbs( typeInfo: TransactionDetails['typeInfo'], t: AppTFunction -): [string, string?, string?] { +): { + success: string + successDapp: string + pending?: string + failed?: string + canceling?: string + canceled?: string +} { + const externalDappName = typeInfo.externalDappInfo?.name + switch (typeInfo.type) { case TransactionType.Swap: - return [t('Swapped'), t('Swapping'), t('swap')] + return { + success: t('transaction.status.swap.success'), + successDapp: t('transaction.status.swap.successDapp', { externalDappName }), + pending: t('transaction.status.swap.pending'), + failed: t('transaction.status.swap.failed'), + canceling: t('transaction.status.swap.canceling'), + canceled: t('transaction.status.swap.canceled'), + } case TransactionType.Receive: - return [t('Received')] + return { + success: t('transaction.status.receive.success'), + successDapp: t('transaction.status.receive.successDapp', { externalDappName }), + } case TransactionType.Send: - return [t('Sent'), t('Sending'), t('send')] + return { + success: t('transaction.status.send.success'), + successDapp: t('transaction.status.send.successDapp', { externalDappName }), + pending: t('transaction.status.send.pending'), + failed: t('transaction.status.send.failed'), + canceling: t('transaction.status.send.canceling'), + canceled: t('transaction.status.send.canceled'), + } case TransactionType.Wrap: if (typeInfo.unwrapped) { - return [t('Unwrapped'), t('Unwrapping'), t('unwrap')] + return { + success: t('transaction.status.unwrap.success'), + successDapp: t('transaction.status.unwrap.successDapp', { externalDappName }), + pending: t('transaction.status.unwrap.pending'), + failed: t('transaction.status.unwrap.failed'), + canceling: t('transaction.status.unwrap.canceling'), + canceled: t('transaction.status.unwrap.canceled'), + } } else { - return [t('Wrapped'), t('Wrapping'), t('wrap')] + return { + success: t('transaction.status.wrap.success'), + successDapp: t('transaction.status.wrap.successDapp', { externalDappName }), + pending: t('transaction.status.wrap.pending'), + failed: t('transaction.status.wrap.failed'), + canceling: t('transaction.status.wrap.canceling'), + canceled: t('transaction.status.wrap.canceled'), + } } case TransactionType.Approve: if (typeInfo.approvalAmount === '0.0') { - return [t('Revoked'), t('Revoking'), t('revoke')] + return { + success: t('transaction.status.revoke.success'), + successDapp: t('transaction.status.revoke.successDapp', { externalDappName }), + pending: t('transaction.status.revoke.pending'), + failed: t('transaction.status.revoke.failed'), + canceling: t('transaction.status.revoke.canceling'), + canceled: t('transaction.status.revoke.canceled'), + } } else { - return [t('Approved'), t('Approving'), t('approve')] + return { + success: t('transaction.status.approve.success'), + successDapp: t('transaction.status.approve.successDapp', { externalDappName }), + pending: t('transaction.status.approve.pending'), + failed: t('transaction.status.approve.failed'), + canceling: t('transaction.status.approve.canceling'), + canceled: t('transaction.status.approve.canceled'), + } } case TransactionType.NFTApprove: - return [t('Approved'), t('Approving'), t('approve')] + return { + success: t('transaction.status.approve.success'), + successDapp: t('transaction.status.approve.successDapp', { externalDappName }), + pending: t('transaction.status.approve.pending'), + failed: t('transaction.status.approve.failed'), + canceling: t('transaction.status.approve.canceling'), + canceled: t('transaction.status.approve.canceled'), + } case TransactionType.NFTMint: - return [t('Minted'), t('Minting'), t('mint')] + return { + success: t('transaction.status.mint.success'), + successDapp: t('transaction.status.mint.successDapp', { externalDappName }), + pending: t('transaction.status.mint.pending'), + failed: t('transaction.status.mint.failed'), + canceling: t('transaction.status.mint.canceling'), + canceled: t('transaction.status.mint.canceled'), + } case TransactionType.NFTTrade: if (typeInfo.tradeType === NFTTradeType.BUY) { - return [t('Bought'), t('Buying'), t('buy')] + return { + success: t('transaction.status.buy.success'), + successDapp: t('transaction.status.buy.successDapp', { externalDappName }), + pending: t('transaction.status.buy.pending'), + failed: t('transaction.status.buy.failed'), + canceling: t('transaction.status.buy.canceling'), + canceled: t('transaction.status.buy.canceled'), + } } else { - return [t('Sold'), t('Selling'), t('sell')] + return { + success: t('transaction.status.sell.success'), + successDapp: t('transaction.status.sell.successDapp', { externalDappName }), + pending: t('transaction.status.sell.pending'), + failed: t('transaction.status.sell.failed'), + canceling: t('transaction.status.sell.canceling'), + canceled: t('transaction.status.sell.canceled'), + } } case TransactionType.FiatPurchase: if (typeInfo.inputSymbol && typeInfo.inputSymbol === typeInfo.outputSymbol) { - return [t('Received'), t('Receiving'), t('receive')] + return { + success: t('transaction.status.receive.success'), + successDapp: t('transaction.status.receive.successDapp', { externalDappName }), + pending: t('transaction.status.receive.pending'), + failed: t('transaction.status.receive.failed'), + canceling: t('transaction.status.receive.canceling'), + canceled: t('transaction.status.receive.canceled'), + } } else { - return [t('Purchased'), t('Purchasing'), t('purchase')] + return { + success: t('transaction.status.purchase.success'), + successDapp: t('transaction.status.purchase.successDapp', { externalDappName }), + pending: t('transaction.status.purchase.pending'), + failed: t('transaction.status.purchase.failed'), + canceling: t('transaction.status.purchase.canceling'), + canceled: t('transaction.status.purchase.canceled'), + } } case TransactionType.Unknown: case TransactionType.WCConfirm: default: - return [t('Transaction confirmed'), t('Transaction in progress'), t('confirm')] + return { + success: t('transaction.status.confirm.success'), + successDapp: t('transaction.status.confirm.successDapp', { externalDappName }), + pending: t('transaction.status.confirm.pending'), + failed: t('transaction.status.confirm.failed'), + canceling: t('transaction.status.confirm.canceling'), + canceled: t('transaction.status.confirm.canceled'), + } } } @@ -162,22 +265,26 @@ export function getTransactionSummaryTitle( tx: TransactionDetails, t: AppTFunction ): string | undefined { - const [completed, inProgress, action] = getTransactionTypeVerbs(tx.typeInfo, t) + const { success, successDapp, pending, failed, canceling, canceled } = getTransactionTypeVerbs( + tx.typeInfo, + t + ) const externalDappName = tx.typeInfo.externalDappInfo?.name + switch (tx.status) { case TransactionStatus.Pending: - return inProgress + return pending case TransactionStatus.Cancelling: - return t('Cancelling {{action}}', { action }) - case TransactionStatus.Cancelled: - return t('Cancelled {{action}}', { action }) + return canceling + case TransactionStatus.Canceled: + return canceled case TransactionStatus.Failed: - return t('Failed to {{action}}', { action }) + return failed case TransactionStatus.Success: if (externalDappName) { - return t('{{completed}} on {{externalDappName}}', { completed, externalDappName }) + return successDapp } - return completed + return success default: return undefined } diff --git a/packages/wallet/src/features/transactions/TransactionDetails/FeeOnTransferInfo.tsx b/packages/wallet/src/features/transactions/TransactionDetails/FeeOnTransferInfo.tsx index 1331018f6c5..cc1a3ac006a 100644 --- a/packages/wallet/src/features/transactions/TransactionDetails/FeeOnTransferInfo.tsx +++ b/packages/wallet/src/features/transactions/TransactionDetails/FeeOnTransferInfo.tsx @@ -53,7 +53,7 @@ function FeeOnTransferInfoRow({ onPress={onShowInfo}> - {t('{{ token }} fee', { token: feeInfo.tokenSymbol })} + {t('swap.details.feeOnTransfer', { tokenSymbol: feeInfo.tokenSymbol })} diff --git a/packages/wallet/src/features/transactions/TransactionDetails/SwapFee.tsx b/packages/wallet/src/features/transactions/TransactionDetails/SwapFee.tsx index ec0cb3bebd2..3a39334eadf 100644 --- a/packages/wallet/src/features/transactions/TransactionDetails/SwapFee.tsx +++ b/packages/wallet/src/features/transactions/TransactionDetails/SwapFee.tsx @@ -21,7 +21,7 @@ export function SwapFee({ onShowSwapFeeInfo(swapFeeInfo.noFeeCharged)}> - {t('Fee')} + {t('swap.details.uniswapFee')} {!swapFeeInfo.noFeeCharged && ` (${swapFeeInfo.formattedPercent})`} diff --git a/packages/wallet/src/features/transactions/TransactionDetails/TransactionDetails.tsx b/packages/wallet/src/features/transactions/TransactionDetails/TransactionDetails.tsx index ad908514d1d..ab35a286126 100644 --- a/packages/wallet/src/features/transactions/TransactionDetails/TransactionDetails.tsx +++ b/packages/wallet/src/features/transactions/TransactionDetails/TransactionDetails.tsx @@ -92,7 +92,7 @@ export function TransactionDetails({ borderRadius="$rounded16" mb="$spacing12" p="$spacing12"> - {t('This transaction is expected to fail')} + {t('swap.warning.expectedFailure')} )} {!showWarning && banner && {banner}} @@ -107,7 +107,7 @@ export function TransactionDetails({ pt="$spacing8" onPress={onPressToggleShowChildren}> - {showChildren ? t('Show less') : t('Show more')} + {showChildren ? t('swap.details.action.less') : t('swap.details.action.more')} {showChildren ? ( = { - [account.address]: account, + [account1.address]: account1, [account2.address]: account2, } @@ -80,22 +84,20 @@ const assetActivities2 = [ }, ] as AssetActivity[] -const portfolioData = [ - { - ...Portfolios[0], - ownerAddress: account.address, - assetActivities, - }, - { - ...Portfolios[1], - ownerAddress: account2.address, - assetActivities: assetActivities2, - }, +const portfolios = [ + portfolio({ ownerAddress: account1.address, assetActivities }), + portfolio({ ownerAddress: account2.address, assetActivities: assetActivities2 }), ] +const receiveAssetActivity = erc20ReceiveAssetActivity() +const portfolioWithReceive = portfolio({ + ownerAddress: account1.address, + assetActivities: [receiveAssetActivity], +}) + const resolvers: Resolvers = { Query: { - portfolios: () => portfolioData, + portfolios: () => portfolios, }, } @@ -126,7 +128,7 @@ describe(TransactionHistoryUpdater, () => { notificationQueue: [], notificationStatus: {}, lastTxNotificationUpdate: { - [account.address]: past.valueOf(), + [account1.address]: past.valueOf(), [account2.address]: past.valueOf(), }, }, @@ -137,11 +139,11 @@ describe(TransactionHistoryUpdater, () => { preloadedState: reduxState, }) - const element = await tree.findByTestId(`AddressTransactionHistoryUpdater/${account.address}`) + const element = await tree.findByTestId(`AddressTransactionHistoryUpdater/${account1.address}`) expect(element).toBeDefined() const notificationStatusState = tree.store.getState().notifications.notificationStatus - expect(notificationStatusState[account.address]).toBeTruthy() + expect(notificationStatusState[account1.address]).toBeTruthy() expect(notificationStatusState[account2.address]).toBeTruthy() expect(mockedRefetchQueries).toHaveBeenCalled() }) @@ -153,7 +155,7 @@ describe(TransactionHistoryUpdater, () => { notificationQueue: [], notificationStatus: {}, lastTxNotificationUpdate: { - [account.address]: future.valueOf(), + [account1.address]: future.valueOf(), [account2.address]: future.valueOf(), }, }, @@ -164,11 +166,11 @@ describe(TransactionHistoryUpdater, () => { preloadedState: reduxState, }) - const element = await tree.findByTestId(`AddressTransactionHistoryUpdater/${account.address}`) + const element = await tree.findByTestId(`AddressTransactionHistoryUpdater/${account1.address}`) expect(element).toBeDefined() const notificationStatusState = tree.store.getState().notifications.notificationStatus - expect(notificationStatusState[account.address]).toBeFalsy() + expect(notificationStatusState[account1.address]).toBeFalsy() expect(notificationStatusState[account2.address]).toBeFalsy() expect(mockedRefetchQueries).not.toHaveBeenCalled() }) @@ -190,49 +192,53 @@ describe(TransactionHistoryUpdater, () => { preloadedState: reduxState, }) - const element = await tree.findByTestId(`AddressTransactionHistoryUpdater/${account.address}`) + const element = await tree.findByTestId(`AddressTransactionHistoryUpdater/${account1.address}`) expect(element).toBeDefined() const notificationStatusState = tree.store.getState().notifications.notificationStatus - expect(notificationStatusState[account.address]).toBeFalsy() + expect(notificationStatusState[account1.address]).toBeFalsy() expect(notificationStatusState[account2.address]).toBeFalsy() }) }) describe(getReceiveNotificationFromData, () => { it('returns app notification object with new receive', () => { - const txnData = { portfolios: PortfoliosWithReceive } + const txnData = { portfolios: [portfolioWithReceive] } // Ensure all transactions will be "new" compared to this const newTimestamp = 1 - const notification = getReceiveNotificationFromData(txnData, account.address, newTimestamp) - - expect(notification).toEqual({ - txStatus: TransactionStatus.Success, - chainId: ChainId.Mainnet, - txHash: PortfoliosWithReceive[0].assetActivities[0]?.details.hash, // generated - address: account.address, - txId: '0x9b0e1021d79e2a85b7a419f47cfa364ea6ae10bf', - type: AppNotificationType.Transaction, - txType: TransactionType.Receive, - assetType: AssetType.Currency, - tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F', - currencyAmountRaw: '1000000000000000000', - sender: SAMPLE_SEED_ADDRESS_1, - }) + const notification = getReceiveNotificationFromData(txnData, account1.address, newTimestamp) + + const assetChange = receiveAssetActivity.details.assetChanges[0]! + + expect(notification).toEqual( + receiveCurrencyTxNotification({ + address: account1.address, + txStatus: TransactionStatus.Success, + txHash: receiveAssetActivity.details.hash, + txId: receiveAssetActivity.details.hash, + sender: assetChange.sender, + tokenAddress: assetChange.asset.address, + chainId: fromGraphQLChain(assetChange.asset.chain)!, + // This is calculated based on a few different fields and we don't + // have to check if the calculation is correct in this test. + // It's better to test the calculation in a separate test. + currencyAmountRaw: expect.any(String), + }) + ) }) it('returns undefined if no receive txns found', () => { // No receive type txn in this mock - const txnDataWithoutReceiveTxns = { portfolios: Portfolios } + const txnDataWithoutReceiveTxns = { portfolios } as TransactionListQuery // Ensure all transactions will be "new" compared to this const newTimestamp = 1 const notification = getReceiveNotificationFromData( txnDataWithoutReceiveTxns, - account.address, + account1.address, newTimestamp ) @@ -240,12 +246,12 @@ describe(getReceiveNotificationFromData, () => { }) it('returns undefined if receive is older than lastest status update timestamp', () => { - const txnData = { portfolios: PortfoliosWithReceive } + const txnData = { portfolios: [portfolioWithReceive] } // Ensure all transactions will be "old" compared to this const oldTimestamp = (MAX_FIXTURE_TIMESTAMP + 1) * 1000 // convert to ms - const notification = getReceiveNotificationFromData(txnData, account.address, oldTimestamp) + const notification = getReceiveNotificationFromData(txnData, account1.address, oldTimestamp) expect(notification).toBeUndefined() }) diff --git a/packages/wallet/src/features/transactions/TransactionReview/TransactionReview.tsx b/packages/wallet/src/features/transactions/TransactionReview/TransactionReview.tsx index 073a510731e..b833d62f8ce 100644 --- a/packages/wallet/src/features/transactions/TransactionReview/TransactionReview.tsx +++ b/packages/wallet/src/features/transactions/TransactionReview/TransactionReview.tsx @@ -117,7 +117,8 @@ export function TransactionReview({ {recipient && ( - {t('Sending')} + {/* TODO gary to come back and fix this later. More complicated with nested components */} + {t('send.review.summary.sending')} )} - {t('To')} + {/* TODO gary to come back and fix this later. More complicated with nested components */} + {t('send.review.summary.to')} ({ type: WarningLabel.NetworkError, severity: WarningSeverity.Low, action: WarningAction.DisableReview, - title: t('You’re offline'), + title: t('swap.warning.offline.title'), icon: WifiIcon, - message: t( - 'You may have lost internet connection or the network may be down. Please check your internet connection and try again.' - ), + message: t('swap.warning.offline.message'), }) diff --git a/packages/wallet/src/features/transactions/history/conversion/extractTransactionDetails.ts b/packages/wallet/src/features/transactions/history/conversion/extractTransactionDetails.ts index 198b7eb0077..fcb88e641f0 100644 --- a/packages/wallet/src/features/transactions/history/conversion/extractTransactionDetails.ts +++ b/packages/wallet/src/features/transactions/history/conversion/extractTransactionDetails.ts @@ -35,7 +35,7 @@ function remoteTxStatusToLocalTxStatus( return TransactionStatus.Pending case RemoteTransactionStatus.Confirmed: if (type === RemoteTransactionType.Cancel) { - return TransactionStatus.Cancelled + return TransactionStatus.Canceled } return TransactionStatus.Success } diff --git a/packages/wallet/src/features/transactions/hooks/useSwapWarnings.test.ts b/packages/wallet/src/features/transactions/hooks/useSwapWarnings.test.ts index efeb935c9ee..52965fc814f 100644 --- a/packages/wallet/src/features/transactions/hooks/useSwapWarnings.test.ts +++ b/packages/wallet/src/features/transactions/hooks/useSwapWarnings.test.ts @@ -44,7 +44,7 @@ const swapState: DerivedSwapInfo = { [CurrencyField.OUTPUT]: undefined, }, currencies: { - [CurrencyField.INPUT]: ethCurrencyInfo, + [CurrencyField.INPUT]: ethCurrencyInfo(), [CurrencyField.OUTPUT]: undefined, }, exactCurrencyField: CurrencyField.INPUT, @@ -66,8 +66,8 @@ const insufficientBalanceState: DerivedSwapInfo = { [CurrencyField.OUTPUT]: CurrencyAmount.fromRawAmount(DAI, '0'), }, currencies: { - [CurrencyField.INPUT]: ethCurrencyInfo, - [CurrencyField.OUTPUT]: daiCurrencyInfo, + [CurrencyField.INPUT]: ethCurrencyInfo(), + [CurrencyField.OUTPUT]: daiCurrencyInfo(), }, exactCurrencyField: CurrencyField.INPUT, trade: { loading: false, error: undefined, trade: null }, @@ -88,8 +88,8 @@ const tradeErrorState: DerivedSwapInfo = { [CurrencyField.OUTPUT]: CurrencyAmount.fromRawAmount(ETH, '0'), }, currencies: { - [CurrencyField.INPUT]: daiCurrencyInfo, - [CurrencyField.OUTPUT]: ethCurrencyInfo, + [CurrencyField.INPUT]: daiCurrencyInfo(), + [CurrencyField.OUTPUT]: ethCurrencyInfo(), }, exactCurrencyField: CurrencyField.INPUT, trade: { @@ -103,7 +103,7 @@ const mockTranslate = jest.fn() describe(getSwapWarnings, () => { it('catches incomplete form errors', async () => { - const warnings = getSwapWarnings(mockTranslate, swapState, isOffline(networkUp)) + const warnings = getSwapWarnings(mockTranslate, swapState, isOffline(networkUp())) expect(warnings.length).toBe(1) expect(warnings[0]?.type).toEqual(WarningLabel.FormIncomplete) }) @@ -113,7 +113,7 @@ describe(getSwapWarnings, () => { mockTranslate, insufficientBalanceState, - isOffline(networkUp) + isOffline(networkUp()) ) expect(warnings.length).toBe(1) expect(warnings[0]?.type).toEqual(WarningLabel.InsufficientFunds) @@ -132,23 +132,23 @@ describe(getSwapWarnings, () => { mockTranslate, incompleteAndInsufficientBalanceState, - isOffline(networkUp) + isOffline(networkUp()) ) expect(warnings.length).toBe(2) }) it('catches errors returned by the routing api', () => { - const warnings = getSwapWarnings(mockTranslate, tradeErrorState, isOffline(networkUp)) + const warnings = getSwapWarnings(mockTranslate, tradeErrorState, isOffline(networkUp())) expect(warnings.find((warning) => warning.type === WarningLabel.SwapRouterError)).toBeTruthy() }) it('errors if there is no internet', () => { - const warnings = getSwapWarnings(mockTranslate, tradeErrorState, isOffline(networkDown)) + const warnings = getSwapWarnings(mockTranslate, tradeErrorState, isOffline(networkDown())) expect(warnings.find((warning) => warning.type === WarningLabel.NetworkError)).toBeTruthy() }) it('does not error when network state is unknown', () => { - const warnings = getSwapWarnings(mockTranslate, tradeErrorState, isOffline(networkUnknown)) + const warnings = getSwapWarnings(mockTranslate, tradeErrorState, isOffline(networkUnknown())) expect(warnings.find((warning) => warning.type === WarningLabel.NetworkError)).toBeFalsy() }) }) diff --git a/packages/wallet/src/features/transactions/hooks/useSwapWarnings.tsx b/packages/wallet/src/features/transactions/hooks/useSwapWarnings.tsx index f10f34ffb05..1c08cba8768 100644 --- a/packages/wallet/src/features/transactions/hooks/useSwapWarnings.tsx +++ b/packages/wallet/src/features/transactions/hooks/useSwapWarnings.tsx @@ -46,11 +46,11 @@ export function getSwapWarnings( type: WarningLabel.InsufficientFunds, severity: WarningSeverity.None, action: WarningAction.DisableReview, - title: t('You don’t have enough {{ symbol }}', { - symbol: currencyAmountIn.currency?.symbol, + title: t('swap.warning.insufficientBalance.title', { + currencySymbol: currencyAmountIn.currency?.symbol, }), buttonText: isWeb - ? t('Not enough {{ currencySymbol }}', { + ? t('swap.warning.insufficientBalance.button', { currencySymbol: currencyAmountIn.currency?.symbol, }) : undefined, @@ -70,18 +70,16 @@ export function getSwapWarnings( type: WarningLabel.LowLiquidity, severity: WarningSeverity.Medium, action: WarningAction.DisableReview, - title: t('Not enough liquidity'), - message: t( - 'There isn’t currently enough liquidity available between these tokens to perform a swap. Please try again later or select another token.' - ), + title: t('swap.warning.lowLiquidity.title'), + message: t('swap.warning.lowLiquidity.message'), }) } else if (errorData?.data?.errorCode === API_RATE_LIMIT_ERROR) { warnings.push({ type: WarningLabel.RateLimit, severity: WarningSeverity.Medium, action: WarningAction.DisableReview, - title: t('Rate limit exceeded'), - message: t('Please try again in a few minutes.'), + title: t('swap.warning.rateLimit.title'), + message: t('swap.warning.rateLimit.message'), }) } else { // catch all other router errors in a generic swap router error message @@ -89,10 +87,8 @@ export function getSwapWarnings( type: WarningLabel.SwapRouterError, severity: WarningSeverity.Medium, action: WarningAction.DisableReview, - title: t('This trade cannot be completed right now'), - message: t( - 'You may have lost connection or the network may be down. If the problem persists, please try again later.' - ), + title: t('swap.warning.router.title'), + message: t('swap.warning.router.message'), }) } } @@ -114,16 +110,13 @@ export function getSwapWarnings( type: highImpact ? WarningLabel.PriceImpactHigh : WarningLabel.PriceImpactMedium, severity: highImpact ? WarningSeverity.High : WarningSeverity.Medium, action: WarningAction.WarnBeforeSubmit, - title: t('High price impact ({{ swapSize }})', { + title: t('swap.warning.priceImpact.title', { swapSize: formatPriceImpact(priceImpact), }), - message: t( - 'Due to the amount of {{ currencyOut }} liquidity currently available, the more {{ currencyIn }} you try to swap, the less {{ currencyOut }} you will receive.', - { - currencyIn: currencies[CurrencyField.INPUT]?.currency.symbol, - currencyOut: currencies[CurrencyField.OUTPUT]?.currency.symbol, - } - ), + message: t('swap.warning.priceImpact.message', { + outputCurrencySymbol: currencies[CurrencyField.INPUT]?.currency.symbol, + inputCurrencySymbol: currencies[CurrencyField.OUTPUT]?.currency.symbol, + }), }) } diff --git a/packages/wallet/src/features/transactions/hooks/useTransactionGasWarning.tsx b/packages/wallet/src/features/transactions/hooks/useTransactionGasWarning.tsx index e17d4716153..1e29f0272f4 100644 --- a/packages/wallet/src/features/transactions/hooks/useTransactionGasWarning.tsx +++ b/packages/wallet/src/features/transactions/hooks/useTransactionGasWarning.tsx @@ -51,11 +51,11 @@ export function useTransactionGasWarning({ type: WarningLabel.InsufficientGasFunds, severity: WarningSeverity.Medium, action: WarningAction.DisableSubmit, - title: t('You don’t have enough {{ nativeCurrency }} to cover the network cost', { - nativeCurrency: nativeCurrencyBalance.currency.symbol, + title: t('swap.warning.insufficientGas.title', { + currencySymbol: nativeCurrencyBalance.currency.symbol, }), buttonText: isWeb - ? t('Not enough {{ currencySymbol }}', { + ? t('swap.warning.insufficientGas.button', { currencySymbol: nativeCurrencyBalance.currency.symbol, }) : undefined, diff --git a/packages/wallet/src/features/transactions/replaceTransactionSaga.test.ts b/packages/wallet/src/features/transactions/replaceTransactionSaga.test.ts index 947addaf3ca..c321e091510 100644 --- a/packages/wallet/src/features/transactions/replaceTransactionSaga.test.ts +++ b/packages/wallet/src/features/transactions/replaceTransactionSaga.test.ts @@ -19,29 +19,23 @@ import { } from 'wallet/src/features/wallet/context' import { selectAccounts } from 'wallet/src/features/wallet/selectors' import { - account, - provider, - providerManager, - signerManager, - txDetailsPending, - txRequest, - txResponse, - txTypeInfo, + ACCOUNT, + ethersTransactionRequest, + getTxFixtures, + transactionDetails, } from 'wallet/src/test/fixtures' +import { provider, providerManager, signerManager } from 'wallet/src/test/mocks' const NEW_UNIQUE_ID = faker.datatype.uuid() // Structure with valid request address (to avoid address validation within saga) -const transaction = { - ...txDetailsPending, +const transaction = transactionDetails({ options: { - ...txDetailsPending.options, - request: { - ...txDetailsPending.options.request, - from: account.address, - }, + request: ethersTransactionRequest({ from: ACCOUNT.address }), }, -} +}) + +const { txRequest, txResponse, txTypeInfo } = getTxFixtures(transaction) const present = dayjs('2022-02-01') @@ -65,7 +59,7 @@ describe(sendTransaction, () => { return expectSaga(attemptReplaceTransaction, transaction, transaction.options.request, false) .withState({ transactions: { - [account.address]: { + [ACCOUNT.address]: { [transaction.chainId]: { [transaction.id]: transaction, }, @@ -73,12 +67,12 @@ describe(sendTransaction, () => { }, wallet: { accounts: { - [account.address]: account, + [ACCOUNT.address]: ACCOUNT, }, }, }) .provide([ - [selectAccounts, { [transaction.from]: account }], + [selectAccounts, { [transaction.from]: ACCOUNT }], [call(getProvider, transaction.chainId), provider], [call(getProviderManager), providerManager], [call(getSignerManager), signerManager], @@ -86,7 +80,7 @@ describe(sendTransaction, () => { call( signAndSendTransaction, transaction.options.request, - account, + ACCOUNT, provider as providers.Provider, signerManager ), diff --git a/packages/wallet/src/features/transactions/replaceTransactionSaga.ts b/packages/wallet/src/features/transactions/replaceTransactionSaga.ts index 4fd6b058388..546fe55fe15 100644 --- a/packages/wallet/src/features/transactions/replaceTransactionSaga.ts +++ b/packages/wallet/src/features/transactions/replaceTransactionSaga.ts @@ -96,8 +96,8 @@ export function* attemptReplaceTransaction( type: AppNotificationType.Error, address: transaction.from, errorMessage: isCancellation - ? i18n.t('Unable to cancel transaction') - : i18n.t('Unable to replace transaction'), + ? i18n.t('transaction.notification.error.cancel') + : i18n.t('transaction.notification.error.replace'), }) ) } diff --git a/packages/wallet/src/features/transactions/sendTransactionSaga.test.ts b/packages/wallet/src/features/transactions/sendTransactionSaga.test.ts index c61425f57db..08954ec38a9 100644 --- a/packages/wallet/src/features/transactions/sendTransactionSaga.test.ts +++ b/packages/wallet/src/features/transactions/sendTransactionSaga.test.ts @@ -15,15 +15,12 @@ import { getProviderManager, getSignerManager, } from 'wallet/src/features/wallet/context' -import { - account, - provider, - providerManager, - signerManager, - txRequest, - txResponse, - txTypeInfo, -} from 'wallet/src/test/fixtures' +import { getTxFixtures, signerMnemonicAccount } from 'wallet/src/test/fixtures' +import { provider, providerManager, signerManager } from 'wallet/src/test/mocks' + +const account = signerMnemonicAccount() + +const { txRequest, txResponse, txTypeInfo } = getTxFixtures() const sendParams = { txId: '0', diff --git a/packages/wallet/src/features/transactions/slice.test.ts b/packages/wallet/src/features/transactions/slice.test.ts index d5947a3d9ef..d90f8e0d3cd 100644 --- a/packages/wallet/src/features/transactions/slice.test.ts +++ b/packages/wallet/src/features/transactions/slice.test.ts @@ -17,7 +17,9 @@ import { TransactionType, TransactionTypeInfo, } from 'wallet/src/features/transactions/types' -import { finalizedTxAction } from 'wallet/src/test/fixtures' +import { finalizedTransactionAction } from 'wallet/src/test/fixtures' + +const finalizedTxAction = finalizedTransactionAction() const address = '0x123' @@ -147,9 +149,9 @@ describe('transaction reducer', () => { } store.dispatch(addTransaction(transaction)) - store.dispatch(updateTransaction({ ...transaction, status: TransactionStatus.Cancelled })) + store.dispatch(updateTransaction({ ...transaction, status: TransactionStatus.Canceled })) const tx = store.getState()[address]?.[chainId]?.[id] - expect(tx?.status).toEqual(TransactionStatus.Cancelled) + expect(tx?.status).toEqual(TransactionStatus.Canceled) }) }) @@ -159,7 +161,9 @@ describe('transaction reducer', () => { store.dispatch(finalizeTransaction(finalizedTxAction.payload)) } catch (error) { expect(error).toEqual( - Error('finalizeTransaction: Attempted to finalize a missing tx with id 0') + Error( + `finalizeTransaction: Attempted to finalize a missing tx with id ${finalizedTxAction.payload.id}` + ) ) } expect(store.getState()).toEqual({}) diff --git a/packages/wallet/src/features/transactions/swap/MaxAmountButton.tsx b/packages/wallet/src/features/transactions/swap/MaxAmountButton.tsx index ec55175cf59..0e8540495e8 100644 --- a/packages/wallet/src/features/transactions/swap/MaxAmountButton.tsx +++ b/packages/wallet/src/features/transactions/swap/MaxAmountButton.tsx @@ -58,7 +58,7 @@ export function MaxAmountButton({ style={style} onPress={onPress}> - {t('Max')} + {t('swap.button.max')} diff --git a/packages/wallet/src/features/transactions/swap/SwapBuyTokenRow.tsx b/packages/wallet/src/features/transactions/swap/SwapBuyTokenRow.tsx index 3a0b99edb60..1afb9ed6b1d 100644 --- a/packages/wallet/src/features/transactions/swap/SwapBuyTokenRow.tsx +++ b/packages/wallet/src/features/transactions/swap/SwapBuyTokenRow.tsx @@ -26,9 +26,9 @@ export function SwapBuyTokenRow(): JSX.Element | null { borderWidth={1} p="$spacing12"> - + - You need more{' '} + You need more {{ currencySymbol }} {' '} @@ -45,7 +45,9 @@ export function SwapBuyTokenRow(): JSX.Element | null { px="$spacing12" py="$spacing8"> - {t(`Buy {{ currencySymbol }}`, { currencySymbol: warning.currency?.symbol })} + {t('swap.warning.insufficientGas.cta.button', { + currencySymbol: warning.currency?.symbol, + })} diff --git a/packages/wallet/src/features/transactions/swap/SwapDetails.tsx b/packages/wallet/src/features/transactions/swap/SwapDetails.tsx index 789433c6890..bc6fbaf0d73 100644 --- a/packages/wallet/src/features/transactions/swap/SwapDetails.tsx +++ b/packages/wallet/src/features/transactions/swap/SwapDetails.tsx @@ -155,7 +155,7 @@ export function SwapDetails({ onShowWarning={onShowWarning}> - {t('Rate')} + {t('swap.details.rate')} @@ -165,7 +165,7 @@ export function SwapDetails({ - {t('Max slippage')} + {t('swap.details.slippage')}   @@ -180,7 +180,7 @@ export function SwapDetails({ px="$spacing4" py="$spacing2"> - {t('Auto')} + {t('swap.settings.slippage.control.auto')} ) : null} @@ -240,8 +240,8 @@ function AcceptNewQuoteRow({ {derivedSwapInfo.exactCurrencyField === CurrencyField.INPUT - ? t('New output') - : t('New input')} + ? t('swap.details.newQuote.output') + : t('swap.details.newQuote.input')} - {t('Accept')} + {t('common.button.accept')} diff --git a/packages/wallet/src/features/transactions/swap/SwapFormButton.tsx b/packages/wallet/src/features/transactions/swap/SwapFormButton.tsx index 668e342d3e6..aac504c29df 100644 --- a/packages/wallet/src/features/transactions/swap/SwapFormButton.tsx +++ b/packages/wallet/src/features/transactions/swap/SwapFormButton.tsx @@ -93,7 +93,7 @@ export function SwapFormButton(): JSX.Element { const holdButtonText = useMemo(() => getHoldButtonActionText(wrapType, t), [t, wrapType]) const hasButtonWarning = !!blockingWarning?.buttonText - const buttonText = blockingWarning?.buttonText ?? t('Review') + const buttonText = blockingWarning?.buttonText ?? t('common.button.review') const buttonTextColor = hasButtonWarning ? '$neutral2' : '$white' const buttonBgColor = hasButtonWarning ? '$surface3' @@ -148,7 +148,7 @@ function HoldToInstantSwapRow(): JSX.Element { - {t('Tip: Hold to instant swap')} + {t('swap.hold.tip')} ) @@ -160,10 +160,10 @@ function getHoldButtonActionText( ): string { switch (wrapType) { case WrapType.Wrap: - return t('Hold to wrap') + return t('swap.hold.wrap') case WrapType.Unwrap: - return t('Hold to unwrap') + return t('swap.hold.unwrap') case WrapType.NotApplicable: - return t('Hold to swap') + return t('swap.hold.swap') } } diff --git a/packages/wallet/src/features/transactions/swap/SwapFormHeader.tsx b/packages/wallet/src/features/transactions/swap/SwapFormHeader.tsx index f708337b050..99f81b6b9c3 100644 --- a/packages/wallet/src/features/transactions/swap/SwapFormHeader.tsx +++ b/packages/wallet/src/features/transactions/swap/SwapFormHeader.tsx @@ -79,7 +79,7 @@ export function SwapFormHeader(): JSX.Element { )} - {t('Swap')} + {t('swap.form.header')} @@ -98,7 +98,7 @@ export function SwapFormHeader(): JSX.Element { width={iconSizes.icon16} /> - {t('View-only')} + {t('swap.header.viewOnly')} @@ -119,7 +119,7 @@ export function SwapFormHeader(): JSX.Element { py="$spacing4"> {customSlippageTolerance ? ( - {t('{{slippageTolerancePercent}} slippage', { + {t('swap.form.slippage', { slippageTolerancePercent: formatPercent(customSlippageTolerance), })} @@ -151,12 +151,12 @@ const ViewOnlyModal = ({ onDismiss }: { onDismiss: () => void }): JSX.Element => const { t } = useTranslation() return ( } modalName={ModalName.SwapWarning} severity={WarningSeverity.Low} - title={t('This wallet is view-only')} + title={t('account.wallet.viewOnly.title')} onClose={onDismiss} onConfirm={onDismiss} /> diff --git a/packages/wallet/src/features/transactions/swap/SwapFormScreen.tsx b/packages/wallet/src/features/transactions/swap/SwapFormScreen.tsx index d6e82f5698d..72b5d2dc897 100644 --- a/packages/wallet/src/features/transactions/swap/SwapFormScreen.tsx +++ b/packages/wallet/src/features/transactions/swap/SwapFormScreen.tsx @@ -505,7 +505,7 @@ function SwapFormContent(): JSX.Element { size="$icon.20" /> - {t('Restore your wallet to swap')} + {t('swap.form.warning.restore')} diff --git a/packages/wallet/src/features/transactions/swap/SwapReviewScreen.tsx b/packages/wallet/src/features/transactions/swap/SwapReviewScreen.tsx index 7ab9f2eccda..80062e5aa0c 100644 --- a/packages/wallet/src/features/transactions/swap/SwapReviewScreen.tsx +++ b/packages/wallet/src/features/transactions/swap/SwapReviewScreen.tsx @@ -353,8 +353,8 @@ export function SwapReviewScreen({ hideContent }: { hideContent: boolean }): JSX {showWarningModal && reviewScreenWarning?.warning.title && ( } modalName={ModalName.SwapWarning} severity={warning.severity} diff --git a/packages/wallet/src/features/transactions/swap/TransactionAmountsReview.tsx b/packages/wallet/src/features/transactions/swap/TransactionAmountsReview.tsx index eb695f1029c..990c222a133 100644 --- a/packages/wallet/src/features/transactions/swap/TransactionAmountsReview.tsx +++ b/packages/wallet/src/features/transactions/swap/TransactionAmountsReview.tsx @@ -93,7 +93,7 @@ export function TransactionAmountsReview({ - {t('You’re swapping')} + {t('swap.review.summary')} {isWeb && ( diff --git a/packages/wallet/src/features/transactions/swap/modals/FeeOnTransferInfoModal.tsx b/packages/wallet/src/features/transactions/swap/modals/FeeOnTransferInfoModal.tsx index a48b71ee426..772f68a114d 100644 --- a/packages/wallet/src/features/transactions/swap/modals/FeeOnTransferInfoModal.tsx +++ b/packages/wallet/src/features/transactions/swap/modals/FeeOnTransferInfoModal.tsx @@ -13,10 +13,8 @@ export function FeeOnTransferInfoModal({ onClose }: { onClose: () => void }): JS return ( void }): JS /> } modalName={ModalName.FOTInfo} - title={t('Why is there an additional fee?')} + title={t('swap.warning.feeOnTransfer.title')} onClose={onClose}> diff --git a/packages/wallet/src/features/transactions/swap/modals/NetworkFeeInfoModal.tsx b/packages/wallet/src/features/transactions/swap/modals/NetworkFeeInfoModal.tsx index 129a3288cd3..e3298ed8b06 100644 --- a/packages/wallet/src/features/transactions/swap/modals/NetworkFeeInfoModal.tsx +++ b/packages/wallet/src/features/transactions/swap/modals/NetworkFeeInfoModal.tsx @@ -13,14 +13,12 @@ export function NetworkFeeInfoModal({ onClose }: { onClose: () => void }): JSX.E return ( } modalName={ModalName.NetworkFeeInfo} severity={WarningSeverity.None} - title={t('Network cost')} + title={t('swap.warning.networkFee.title')} onClose={onClose}> diff --git a/packages/wallet/src/features/transactions/swap/modals/SlippageInfoModal.tsx b/packages/wallet/src/features/transactions/swap/modals/SlippageInfoModal.tsx index 689e04971eb..22a88008424 100644 --- a/packages/wallet/src/features/transactions/swap/modals/SlippageInfoModal.tsx +++ b/packages/wallet/src/features/transactions/swap/modals/SlippageInfoModal.tsx @@ -1,5 +1,5 @@ import { Currency, TradeType } from '@uniswap/sdk-core' -import { useTranslation } from 'react-i18next' +import { Trans, useTranslation } from 'react-i18next' import { Button, Flex, Icons, Text, useSporeColors } from 'ui/src' import AlertTriangleIcon from 'ui/src/assets/icons/alert-triangle.svg' import { iconSizes } from 'ui/src/theme' @@ -11,7 +11,6 @@ import { useLocalizationContext } from 'wallet/src/features/language/Localizatio import { Trade } from 'wallet/src/features/transactions/swap/trade/types' import { slippageToleranceToPercent } from 'wallet/src/features/transactions/swap/utils' import { ModalName } from 'wallet/src/telemetry/constants' -import { getSymbolDisplayText } from 'wallet/src/utils/currency' export type SlippageInfoModalProps = { trade: Trade @@ -40,7 +39,7 @@ export function SlippageInfoModal({ : trade.maximumAmountIn(slippageTolerancePercent), type: NumberType.TokenTx, }) - const symbol = + const tokenSymbol = trade.tradeType === TradeType.EXACT_INPUT ? trade.outputAmount.currency.symbol : trade.inputAmount.currency.symbol @@ -55,16 +54,12 @@ export function SlippageInfoModal({ - {t('Maximum slippage')} + {t('swap.settings.slippage.control.title')} {tradeType === TradeType.EXACT_INPUT - ? t( - 'If the price slips any further, your transaction will revert. Below is the minimum amount you are guaranteed to receive.' - ) - : t( - 'If the price slips any further, your transaction will revert. Below is the maximum amount you would need to spend.' - )}{' '} + ? t('swap.settings.slippage.input.message') + : t('swap.settings.slippage.output.message')} - {t('Max slippage')} + {t('swap.settings.slippage.control.title')} {!isCustomSlippage ? ( @@ -85,7 +80,7 @@ export function SlippageInfoModal({ borderRadius="$roundedFull" px="$spacing8"> - {t('Auto')} + {t('swap.settings.slippage.control.auto')} ) : null} @@ -97,12 +92,25 @@ export function SlippageInfoModal({ - - {tradeType === TradeType.EXACT_INPUT ? t('Receive at least') : t('Spend at most')} - - - {amount + ' ' + getSymbolDisplayText(symbol)} - + {tradeType === TradeType.EXACT_INPUT ? ( + + + Receive at least + + + {{ amount }} {{ tokenSymbol }} + + + ) : ( + + + Spend at most + + + {{ amount }} {{ tokenSymbol }} + + + )} {showSlippageWarning ? ( @@ -113,14 +121,14 @@ export function SlippageInfoModal({ width={iconSizes.icon16} /> - {t('Slippage may be higher than necessary')} + {t('swap.settings.slippage.warning.message')} ) : null} diff --git a/packages/wallet/src/features/transactions/swap/modals/SwapFeeInfoModal.tsx b/packages/wallet/src/features/transactions/swap/modals/SwapFeeInfoModal.tsx index aacb60723a0..84fa8c3c38f 100644 --- a/packages/wallet/src/features/transactions/swap/modals/SwapFeeInfoModal.tsx +++ b/packages/wallet/src/features/transactions/swap/modals/SwapFeeInfoModal.tsx @@ -26,21 +26,17 @@ export function SwapFeeInfoModal({ backgroundIconColor={colors.surface2.get()} caption={ noFee - ? t( - 'Fees are applied on a few select tokens to ensure the best experience with Uniswap. There is no fee associated with this swap.' - ) - : t( - 'Fees are applied on a few select tokens to ensure the best experience with Uniswap, and have already been factored into this quote.' - ) + ? t('swap.warning.uniswapFee.message.default') + : t('swap.warning.uniswapFee.message.included') } - closeText={t('Close')} + closeText={t('common.button.close')} modalName={ModalName.NetworkFeeInfo} severity={WarningSeverity.None} - title={t('Swap fee')} + title={t('swap.warning.uniswapFee.title')} onClose={onClose}> - {t('Learn more')} + {t('common.button.learn')} diff --git a/packages/wallet/src/features/transactions/swap/modals/SwapProtectionModal.tsx b/packages/wallet/src/features/transactions/swap/modals/SwapProtectionModal.tsx index b734148a45f..42fd97eca8f 100644 --- a/packages/wallet/src/features/transactions/swap/modals/SwapProtectionModal.tsx +++ b/packages/wallet/src/features/transactions/swap/modals/SwapProtectionModal.tsx @@ -12,13 +12,11 @@ export function SwapProtectionInfoModal({ onClose }: { onClose: () => void }): J return ( } modalName={ModalName.SwapProtection} - title={t('Swap Protection')} + title={t('swap.settings.protection.title')} onClose={onClose}> diff --git a/packages/wallet/src/features/transactions/swap/modals/SwapSettingsModal.tsx b/packages/wallet/src/features/transactions/swap/modals/SwapSettingsModal.tsx index 005a8a5e395..9568ee88fb0 100644 --- a/packages/wallet/src/features/transactions/swap/modals/SwapSettingsModal.tsx +++ b/packages/wallet/src/features/transactions/swap/modals/SwapSettingsModal.tsx @@ -77,9 +77,9 @@ export function SwapSettingsModal({ const getTitle = (): string => { switch (view) { case SwapSettingsModalView.Options: - return t('Swap Settings') + return t('swap.settings.title') case SwapSettingsModalView.Slippage: - return t('Slippage Settings') + return t('swap.slippage.settings.title') } } @@ -134,7 +134,7 @@ export function SwapSettingsModal({ {innerContent} @@ -161,14 +161,14 @@ function SwapSettingsOptions({ - {t('Max slippage')} + {t('swap.settings.slippage.control.title')} setView(SwapSettingsModalView.Slippage)}> {!isCustomSlippage ? ( - {t('Auto')} + {t('swap.settings.slippage.control.auto')} ) : null} @@ -208,8 +208,8 @@ function SwapProtectionSettingsRow({ chainId }: { chainId: ChainId }): JSX.Eleme const privateRpcSupportedOnChain = isPrivateRpcSupportedOnChain(chainId) const chainName = CHAIN_INFO[chainId].label const subText = privateRpcSupportedOnChain - ? t('{{chainName}} Network', { chainName }) - : t('Not available on {{chainName}}', { chainName }) + ? t('swap.settings.protection.subtitle.supported', { chainName }) + : t('swap.settings.protection.subtitle.unavailable', { chainName }) return ( <> @@ -221,7 +221,7 @@ function SwapProtectionSettingsRow({ chainId }: { chainId: ChainId }): JSX.Eleme - {t('Swap protection')} + {t('swap.settings.protection.title')} @@ -316,12 +316,12 @@ function SlippageSettings({ const isZero = parsedValue === 0 if (isZero) { - setInputWarning(t('Enter a value larger than 0')) + setInputWarning(t('swap.settings.slippage.warning.min')) } if (overMaxTolerance) { setInputWarning( - t('Enter a value less than {{ maxSlippageTolerance }}', { + t('swap.settings.slippage.warning.max', { maxSlippageTolerance: MAX_CUSTOM_SLIPPAGE_TOLERANCE, }) ) @@ -382,7 +382,7 @@ function SlippageSettings({ : Math.max(newSlippage, 0) if (constrainedNewSlippage === 0) { - setInputWarning(t('Enter a value larger than 0')) + setInputWarning(t('swap.settings.slippage.warning.min')) } else { setInputWarning(undefined) } @@ -396,7 +396,7 @@ function SlippageSettings({ return ( - {t('Your transaction will revert if the price changes more than the slippage percentage.')}{' '} + {t('swap.settings.slippage.description')} @@ -418,7 +418,7 @@ function SlippageSettings({ style={inputAnimatedStyle}> - {t('Auto')} + {t('swap.settings.slippage.control.auto')} {trade.tradeType === TradeType.EXACT_INPUT - ? t('Receive at least {{amount}} {{symbol}}', { + ? t('swap.settings.slippage.input.receive.unformatted', { amount: formatCurrencyAmount({ value: trade.minimumAmountOut(slippageTolerancePercent), type: NumberType.TokenTx, }), symbol: getSymbolDisplayText(trade.outputAmount.currency.symbol), }) - : t('Spend at most {{amount}} {{symbol}}', { + : t('swap.settings.slippage.output.spend.unformatted', { amount: formatCurrencyAmount({ value: trade.maximumAmountIn(slippageTolerancePercent), type: NumberType.TokenTx, @@ -523,7 +523,7 @@ function BottomLabel({ width={iconSizes.icon16} /> - {t('Slippage may be higher than necessary')} + {t('swap.settings.slippage.warning.message')} ) : null} diff --git a/packages/wallet/src/features/transactions/swap/swapSaga.test.ts b/packages/wallet/src/features/transactions/swap/swapSaga.test.ts index 68aae928567..4313847acbf 100644 --- a/packages/wallet/src/features/transactions/swap/swapSaga.test.ts +++ b/packages/wallet/src/features/transactions/swap/swapSaga.test.ts @@ -19,12 +19,17 @@ import { import { getProvider } from 'wallet/src/features/wallet/context' import { selectWalletSwapProtectionSetting } from 'wallet/src/features/wallet/selectors' import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice' -import { account, mockProvider } from 'wallet/src/test/fixtures' +import { signerMnemonicAccount } from 'wallet/src/test/fixtures' +import { getTxProvidersMocks } from 'wallet/src/test/mocks' import { currencyId } from 'wallet/src/utils/currencyId' +const account = signerMnemonicAccount() + const CHAIN_ID = ChainId.Goerli const universalRouterAddress = UNIVERSAL_ROUTER_ADDRESS(CHAIN_ID) +const { mockProvider } = getTxProvidersMocks() + const transactionTypeInfo: ExactInputSwapTransactionInfo = { type: TransactionType.Swap, tradeType: TradeType.EXACT_INPUT, diff --git a/packages/wallet/src/features/transactions/swap/trade/legacy/hooks.ts b/packages/wallet/src/features/transactions/swap/trade/legacy/hooks.ts index 6a5ee24f9b3..f297d806776 100644 --- a/packages/wallet/src/features/transactions/swap/trade/legacy/hooks.ts +++ b/packages/wallet/src/features/transactions/swap/trade/legacy/hooks.ts @@ -6,11 +6,11 @@ import { FlatFeeOptions, UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-rou import { FeeOptions } from '@uniswap/v3-sdk' import { providers } from 'ethers' import { useCallback, useEffect, 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 { flattenObjectOfObjects } from 'utilities/src/primitives/objects' import { useAsyncData, usePrevious } from 'utilities/src/react/hooks' -import ERC20_ABI from 'wallet/src/abis/erc20.json' -import { Erc20 } from 'wallet/src/abis/types' import { ChainId } from 'wallet/src/constants/chains' import { ContractManager } from 'wallet/src/features/contracts/ContractManager' import { useTransactionGasFee } from 'wallet/src/features/gas/hooks' diff --git a/packages/wallet/src/features/transactions/swap/utils.test.ts b/packages/wallet/src/features/transactions/swap/utils.test.ts index 73c1ef470d9..e06a76c7b04 100644 --- a/packages/wallet/src/features/transactions/swap/utils.test.ts +++ b/packages/wallet/src/features/transactions/swap/utils.test.ts @@ -5,7 +5,7 @@ import { UNI, WBTC, wrappedNativeCurrency } from 'wallet/src/constants/tokens' import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' import { Trade } from 'wallet/src/features/transactions/swap/trade/types' import { WrapType } from 'wallet/src/features/transactions/types' -import { mockPool } from 'wallet/src/test/fixtures' +import { mockPool } from 'wallet/src/test/mocks' import { getWrapType, requireAcceptNewTrade, serializeQueryParams } from './utils' describe(serializeQueryParams, () => { diff --git a/packages/wallet/src/features/transactions/swap/utils.ts b/packages/wallet/src/features/transactions/swap/utils.ts index 6940d9f0960..cb1d771155e 100644 --- a/packages/wallet/src/features/transactions/swap/utils.ts +++ b/packages/wallet/src/features/transactions/swap/utils.ts @@ -198,11 +198,11 @@ export const getRateToDisplay = ( export const getActionName = (t: AppTFunction, wrapType: WrapType): string => { switch (wrapType) { case WrapType.Unwrap: - return t('Unwrap') + return t('swap.button.unwrap') case WrapType.Wrap: - return t('Wrap') + return t('swap.button.wrap') default: - return t('Swap') + return t('swap.button.swap') } } diff --git a/packages/wallet/src/features/transactions/swap/wrapSaga.test.ts b/packages/wallet/src/features/transactions/swap/wrapSaga.test.ts index cbfac52cc1f..1504458d994 100644 --- a/packages/wallet/src/features/transactions/swap/wrapSaga.test.ts +++ b/packages/wallet/src/features/transactions/swap/wrapSaga.test.ts @@ -5,7 +5,9 @@ import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' import { sendTransaction } from 'wallet/src/features/transactions/sendTransactionSaga' import { wrap, WrapParams } from 'wallet/src/features/transactions/swap/wrapSaga' import { TransactionType, WrapTransactionInfo } from 'wallet/src/features/transactions/types' -import { account } from 'wallet/src/test/fixtures' +import { ethersTransactionRequest, signerMnemonicAccount } from 'wallet/src/test/fixtures' + +const account = signerMnemonicAccount() const wrapTxInfo: WrapTransactionInfo = { type: TransactionType.Wrap, @@ -18,17 +20,12 @@ const unwrapTxInfo: WrapTransactionInfo = { unwrapped: true, } -const transaction = { - from: account.address, - to: '0xabc', - data: '0x01', - chainId: ChainId.Mainnet, -} +const txRequest = ethersTransactionRequest() const params: WrapParams = { txId: '1', account, - txRequest: transaction, + txRequest, inputCurrencyAmount: CurrencyAmount.fromRawAmount( NativeCurrency.onChain(ChainId.Mainnet), '200000' @@ -44,7 +41,7 @@ describe(wrap, () => { chainId: ChainId.Mainnet, account: params.account, typeInfo: wrapTxInfo, - options: { request: transaction }, + options: { request: txRequest }, }) .next() .isDone() @@ -65,7 +62,7 @@ describe(wrap, () => { chainId: ChainId.Mainnet, account: params.account, typeInfo: unwrapTxInfo, - options: { request: transaction }, + options: { request: txRequest }, }) .next() .isDone() diff --git a/packages/wallet/src/features/transactions/swap/wrapSaga.ts b/packages/wallet/src/features/transactions/swap/wrapSaga.ts index ce90b32a092..37696b50976 100644 --- a/packages/wallet/src/features/transactions/swap/wrapSaga.ts +++ b/packages/wallet/src/features/transactions/swap/wrapSaga.ts @@ -1,9 +1,9 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { Contract, providers } from 'ethers' import { call } from 'typed-redux-saga' +import { Weth } from 'uniswap/src/abis/types' +import WETH_ABI from 'uniswap/src/abis/weth.json' import { logger } from 'utilities/src/logger/logger' -import { Weth } from 'wallet/src/abis/types' -import WETH_ABI from 'wallet/src/abis/weth.json' import { getWrappedNativeAddress } from 'wallet/src/constants/addresses' import { ChainId } from 'wallet/src/constants/chains' import { sendTransaction } from 'wallet/src/features/transactions/sendTransactionSaga' diff --git a/packages/wallet/src/features/transactions/transactionWatcherSaga.test.ts b/packages/wallet/src/features/transactions/transactionWatcherSaga.test.ts index 1b8098bd612..3a3964552c9 100644 --- a/packages/wallet/src/features/transactions/transactionWatcherSaga.test.ts +++ b/packages/wallet/src/features/transactions/transactionWatcherSaga.test.ts @@ -23,22 +23,35 @@ import { import { TransactionDetails, TransactionStatus } from 'wallet/src/features/transactions/types' import { getProvider, getProviderManager } from 'wallet/src/features/wallet/context' import { - fiatOnRampTxDetailsPending, - finalizedTxAction, - mockProvider, - mockProviderManager, - txDetailsPending, - txReceipt, + approveTransactionInfo, + fiatPurchaseTransactionInfo, + getTxFixtures, + transactionDetails, } from 'wallet/src/test/fixtures' +import { getTxProvidersMocks } from 'wallet/src/test/mocks' + +const { + ethersTxReceipt, + txReceipt, + finalizedTxAction, + txDetailsPending: txDetailsPending, +} = getTxFixtures(transactionDetails({ typeInfo: fiatPurchaseTransactionInfo() })) + +const { mockProvider, mockProviderManager } = getTxProvidersMocks(ethersTxReceipt) describe(transactionWatcher, () => { it('Triggers watchers successfully', () => { + const approveTxDetailsPending = transactionDetails({ + typeInfo: approveTransactionInfo(), + status: TransactionStatus.Pending, + }) + return expectSaga(transactionWatcher, { apolloClient: null }) .withState({ transactions: { byChainId: { [ChainId.Mainnet]: { - '0': txDetailsPending, + '0': approveTxDetailsPending, }, }, }, @@ -47,11 +60,11 @@ describe(transactionWatcher, () => { [call(getProvider, ChainId.Mainnet), mockProvider], [call(getProviderManager), mockProviderManager], ]) - .fork(watchTransaction, { transaction: txDetailsPending, apolloClient: null }) - .dispatch(addTransaction(txDetailsPending)) - .fork(watchTransaction, { transaction: txDetailsPending, apolloClient: null }) - .dispatch(updateTransaction(txDetailsPending)) - .fork(watchTransaction, { transaction: txDetailsPending, apolloClient: null }) + .fork(watchTransaction, { transaction: approveTxDetailsPending, apolloClient: null }) + .dispatch(addTransaction(approveTxDetailsPending)) + .fork(watchTransaction, { transaction: approveTxDetailsPending, apolloClient: null }) + .dispatch(updateTransaction(approveTxDetailsPending)) + .fork(watchTransaction, { transaction: approveTxDetailsPending, apolloClient: null }) .silentRun() }) }) @@ -59,7 +72,7 @@ describe(transactionWatcher, () => { describe(watchTransaction, () => { let dateNowSpy: jest.SpyInstance beforeAll(() => { - dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => 1400000000000) + dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => txReceipt.confirmedTime) }) afterAll(() => { dateNowSpy?.mockRestore() @@ -69,7 +82,7 @@ describe(watchTransaction, () => { it('Finalizes successful transaction', () => { const receiptProvider = { - waitForTransaction: jest.fn(() => txReceipt), + waitForTransaction: jest.fn(() => ethersTxReceipt), } return expectSaga(watchTransaction, { transaction: txDetailsPending, apolloClient: null }) .provide([[call(getProvider, chainId), receiptProvider]]) @@ -102,6 +115,7 @@ describe(watchTransaction, () => { return null }), } + return expectSaga(watchTransaction, { transaction: txDetailsPending, apolloClient: null }) .provide([ [call(getProvider, chainId), receiptProvider], @@ -115,10 +129,10 @@ describe(watchTransaction, () => { describe(watchFiatOnRampTransaction, () => { it('removes transactions on 404 when stale', () => { - const staleTx = { ...fiatOnRampTxDetailsPending, status: TransactionStatus.Unknown } + const staleTx = { ...txDetailsPending, status: TransactionStatus.Unknown } return ( - expectSaga(watchFiatOnRampTransaction, fiatOnRampTxDetailsPending) - .provide([[call(fetchMoonpayTransaction, fiatOnRampTxDetailsPending), staleTx]]) + expectSaga(watchFiatOnRampTransaction, txDetailsPending) + .provide([[call(fetchMoonpayTransaction, txDetailsPending), staleTx]]) .put( transactionActions.deleteTransaction({ address: staleTx.from, @@ -133,7 +147,7 @@ describe(watchFiatOnRampTransaction, () => { }) it('keeps a transaction on 404 when not yet stale', () => { - const tx = { ...fiatOnRampTxDetailsPending, addedTime: Date.now() } + const tx = { ...txDetailsPending, addedTime: Date.now() } const confirmedTx = { ...tx, status: TransactionStatus.Success } let fetchCalledCount = 0 @@ -165,7 +179,7 @@ describe(watchFiatOnRampTransaction, () => { }) it('keeps a transaction on 404 when not yet stale, when fetch is forced', () => { - const tx = { ...fiatOnRampTxDetailsPending, addedTime: Date.now() } + const tx = { ...txDetailsPending, addedTime: Date.now() } const confirmedTx = { ...tx, status: TransactionStatus.Success } let fetchCalledCount = 0 @@ -198,9 +212,9 @@ describe(watchFiatOnRampTransaction, () => { }) it('updates a transactions on success network request', () => { - const confirmedTx = { ...fiatOnRampTxDetailsPending, status: TransactionStatus.Success } - return expectSaga(watchFiatOnRampTransaction, fiatOnRampTxDetailsPending) - .provide([[call(fetchMoonpayTransaction, fiatOnRampTxDetailsPending), confirmedTx]]) + const confirmedTx = { ...txDetailsPending, status: TransactionStatus.Success } + return expectSaga(watchFiatOnRampTransaction, txDetailsPending) + .provide([[call(fetchMoonpayTransaction, txDetailsPending), confirmedTx]]) .put(transactionActions.upsertFiatOnRampTransaction(confirmedTx)) .not.call.fn(sleep) .run() diff --git a/packages/wallet/src/features/transactions/transactionWatcherSaga.ts b/packages/wallet/src/features/transactions/transactionWatcherSaga.ts index 590e56d0dea..4a5d5a24c48 100644 --- a/packages/wallet/src/features/transactions/transactionWatcherSaga.ts +++ b/packages/wallet/src/features/transactions/transactionWatcherSaga.ts @@ -90,7 +90,7 @@ export function* transactionWatcher({ pushNotification({ type: AppNotificationType.Error, address: transaction.from, - errorMessage: i18n.t('Error while checking transaction status'), + errorMessage: i18n.t('transaction.watcher.error.status'), }) ) } @@ -239,7 +239,7 @@ export function* watchTransaction({ pushNotification({ type: AppNotificationType.Error, address: transaction.from, - errorMessage: i18n.t('Unable to cancel transaction'), + errorMessage: i18n.t('transaction.watcher.error.cancel'), }) ) } @@ -375,7 +375,7 @@ export function logTransactionEvent( type StatusOverride = | TransactionStatus.Success | TransactionStatus.Failed - | TransactionStatus.Cancelled + | TransactionStatus.Canceled function* finalizeTransaction({ apolloClient, diff --git a/packages/wallet/src/features/transactions/transfer/TransferFormWarnings.tsx b/packages/wallet/src/features/transactions/transfer/TransferFormWarnings.tsx index f9799f1ca3d..8afcab744e4 100644 --- a/packages/wallet/src/features/transactions/transfer/TransferFormWarnings.tsx +++ b/packages/wallet/src/features/transactions/transfer/TransferFormWarnings.tsx @@ -68,28 +68,24 @@ export function TransferFormSpeedbumps({ <> {showSpeedbumpModal && shouldWarnSmartContract && ( )} {showSpeedbumpModal && shouldWarnNewAddress && ( - {t('Restore your wallet to send')} + {t('send.warning.restore')} @@ -423,7 +423,7 @@ export function TransferTokenForm({ size="large" testID={ElementName.ReviewTransfer} onPress={onPressReview}> - {t('Review transfer')} + {t('send.button.review')} diff --git a/packages/wallet/src/features/transactions/transfer/hooks/useTransferTransactionRequest.ts b/packages/wallet/src/features/transactions/transfer/hooks/useTransferTransactionRequest.ts index aecf0010991..df6e44bc4e5 100644 --- a/packages/wallet/src/features/transactions/transfer/hooks/useTransferTransactionRequest.ts +++ b/packages/wallet/src/features/transactions/transfer/hooks/useTransferTransactionRequest.ts @@ -1,10 +1,10 @@ import { providers } from 'ethers' import { useCallback } from 'react' +import ERC1155_ABI from 'uniswap/src/abis/erc1155.json' +import ERC20_ABI from 'uniswap/src/abis/erc20.json' +import ERC721_ABI from 'uniswap/src/abis/erc721.json' +import { Erc1155, Erc20, Erc721 } from 'uniswap/src/abis/types' import { useAsyncData } from 'utilities/src/react/hooks' -import ERC1155_ABI from 'wallet/src/abis/erc1155.json' -import ERC20_ABI from 'wallet/src/abis/erc20.json' -import ERC721_ABI from 'wallet/src/abis/erc721.json' -import { Erc1155, Erc20, Erc721 } from 'wallet/src/abis/types' import { ChainId } from 'wallet/src/constants/chains' import { AssetType } from 'wallet/src/entities/assets' import { toSupportedChainId } from 'wallet/src/features/chains/utils' diff --git a/packages/wallet/src/features/transactions/transfer/hooks/useTransferWarnings.test.ts b/packages/wallet/src/features/transactions/transfer/hooks/useTransferWarnings.test.ts index f0f4adcde28..4084f944702 100644 --- a/packages/wallet/src/features/transactions/transfer/hooks/useTransferWarnings.test.ts +++ b/packages/wallet/src/features/transactions/transfer/hooks/useTransferWarnings.test.ts @@ -37,7 +37,7 @@ const transferState: DerivedTransferInfo = { [CurrencyField.INPUT]: CurrencyAmount.fromRawAmount(ETH, '20000'), }, chainId: ChainId.Mainnet, - currencyInInfo: uniCurrencyInfo, + currencyInInfo: uniCurrencyInfo(), nftIn: undefined, } @@ -51,7 +51,7 @@ const transferState2: DerivedTransferInfo = { }, recipient: '0x0eae044f00b0af300500f090ea00027097d03000', chainId: ChainId.Mainnet, - currencyInInfo: uniCurrencyInfo, + currencyInInfo: uniCurrencyInfo(), nftIn: undefined, } @@ -107,7 +107,7 @@ const transferCurrency: DerivedTransferInfo = { }, recipient: '0x0eae044f00b0af300500f090ea00027097d03000', chainId: ChainId.Mainnet, - currencyInInfo: uniCurrencyInfo, + currencyInInfo: uniCurrencyInfo(), nftIn: undefined, } @@ -121,7 +121,7 @@ const insufficientBalanceState: DerivedTransferInfo = { }, recipient: '0x0eae044f00b0af300500f090ea00027097d03000', chainId: ChainId.Mainnet, - currencyInInfo: uniCurrencyInfo, + currencyInInfo: uniCurrencyInfo(), nftIn: undefined, } @@ -129,33 +129,33 @@ const mockTranslate = jest.fn() describe(getTransferWarnings, () => { it('does not error when Currency with balances and amounts is provided', () => { - const warnings = getTransferWarnings(mockTranslate, transferCurrency, isOffline(networkUp)) + const warnings = getTransferWarnings(mockTranslate, transferCurrency, isOffline(networkUp())) expect(warnings.length).toBe(0) }) it('errors if there is no internet', () => { - const warnings = getTransferWarnings(mockTranslate, transferCurrency, isOffline(networkDown)) + const warnings = getTransferWarnings(mockTranslate, transferCurrency, isOffline(networkDown())) expect(warnings.length).toBe(1) }) it('does not error when network state is unknown', () => { - const warnings = getTransferWarnings(mockTranslate, transferNFT, isOffline(networkUnknown)) + const warnings = getTransferWarnings(mockTranslate, transferNFT, isOffline(networkUnknown())) expect(warnings.length).toBe(0) }) it('does not error when correctly formed NFT is provided', () => { - const warnings = getTransferWarnings(mockTranslate, transferNFT, isOffline(networkUp)) + const warnings = getTransferWarnings(mockTranslate, transferNFT, isOffline(networkUp())) expect(warnings.length).toBe(0) }) it('catches incomplete form errors: no recipient', async () => { - const warnings = getTransferWarnings(mockTranslate, transferState, isOffline(networkUp)) + const warnings = getTransferWarnings(mockTranslate, transferState, isOffline(networkUp())) expect(warnings.length).toBe(1) expect(warnings[0]?.type).toEqual(WarningLabel.FormIncomplete) }) it('catches incomplete form errors: no amount', async () => { - const warnings = getTransferWarnings(mockTranslate, transferState2, isOffline(networkUp)) + const warnings = getTransferWarnings(mockTranslate, transferState2, isOffline(networkUp())) expect(warnings.length).toBe(1) expect(warnings[0]?.type).toEqual(WarningLabel.FormIncomplete) }) @@ -164,7 +164,7 @@ describe(getTransferWarnings, () => { const warnings = getTransferWarnings( mockTranslate, insufficientBalanceState, - isOffline(networkUp) + isOffline(networkUp()) ) expect(warnings.length).toBe(1) expect(warnings[0]?.type).toEqual(WarningLabel.InsufficientFunds) @@ -182,7 +182,7 @@ describe(getTransferWarnings, () => { const warnings = getTransferWarnings( mockTranslate, incompleteAndInsufficientBalanceState, - isOffline(networkUp) + isOffline(networkUp()) ) expect(warnings.length).toBe(2) }) diff --git a/packages/wallet/src/features/transactions/transfer/hooks/useTransferWarnings.ts b/packages/wallet/src/features/transactions/transfer/hooks/useTransferWarnings.ts index d0f40b7fc7b..c1ec536408b 100644 --- a/packages/wallet/src/features/transactions/transfer/hooks/useTransferWarnings.ts +++ b/packages/wallet/src/features/transactions/transfer/hooks/useTransferWarnings.ts @@ -48,13 +48,12 @@ export function getTransferWarnings( type: WarningLabel.InsufficientFunds, severity: WarningSeverity.None, action: WarningAction.DisableReview, - title: t('Not enough {{ symbol }}.', { - symbol: currencyAmountIn.currency?.symbol, + title: t('send.warning.insufficientFunds.title', { + currencySymbol: currencyAmountIn.currency?.symbol, + }), + message: t('send.warning.insufficientFunds.message', { + currencySymbol: currencyAmountIn.currency?.symbol, }), - message: t( - 'Your {{ symbol }} balance has decreased since you entered the amount you’d like to send', - { symbol: currencyAmountIn.currency?.symbol } - ), }) } diff --git a/packages/wallet/src/features/transactions/transfer/transferTokenSaga.test.ts b/packages/wallet/src/features/transactions/transfer/transferTokenSaga.test.ts index 6a94a380ff4..e247148c2f8 100644 --- a/packages/wallet/src/features/transactions/transfer/transferTokenSaga.test.ts +++ b/packages/wallet/src/features/transactions/transfer/transferTokenSaga.test.ts @@ -14,7 +14,13 @@ import { } from 'wallet/src/features/transactions/transfer/types' import { SendTokenTransactionInfo, TransactionType } from 'wallet/src/features/transactions/types' import { getContractManager, getProvider } from 'wallet/src/features/wallet/context' -import { account, mockContractManager, mockProvider, txRequest } from 'wallet/src/test/fixtures' +import { getTxFixtures, signerMnemonicAccount } from 'wallet/src/test/fixtures' +import { getTxProvidersMocks, mockContractManager } from 'wallet/src/test/mocks' + +const account = signerMnemonicAccount() + +const { txRequest, ethersTxReceipt } = getTxFixtures() +const { mockProvider } = getTxProvidersMocks(ethersTxReceipt) const erc20TranferParams: TransferCurrencyParams = { txId: '1', diff --git a/packages/wallet/src/features/transactions/transfer/transferTokenSaga.ts b/packages/wallet/src/features/transactions/transfer/transferTokenSaga.ts index ebea2228389..7835360a1c0 100644 --- a/packages/wallet/src/features/transactions/transfer/transferTokenSaga.ts +++ b/packages/wallet/src/features/transactions/transfer/transferTokenSaga.ts @@ -1,10 +1,10 @@ import { BigNumber, BigNumberish, providers } from 'ethers' import { call } from 'typed-redux-saga' +import ERC1155_ABI from 'uniswap/src/abis/erc1155.json' +import ERC20_ABI from 'uniswap/src/abis/erc20.json' +import ERC721_ABI from 'uniswap/src/abis/erc721.json' +import { Erc1155, Erc20, Erc721 } from 'uniswap/src/abis/types' import { logger } from 'utilities/src/logger/logger' -import ERC1155_ABI from 'wallet/src/abis/erc1155.json' -import ERC20_ABI from 'wallet/src/abis/erc20.json' -import ERC721_ABI from 'wallet/src/abis/erc721.json' -import { Erc1155, Erc20, Erc721 } from 'wallet/src/abis/types' import { AssetType } from 'wallet/src/entities/assets' import { sendTransaction } from 'wallet/src/features/transactions/sendTransactionSaga' import { TransferTokenParams } from 'wallet/src/features/transactions/transfer/types' diff --git a/packages/wallet/src/features/transactions/types.ts b/packages/wallet/src/features/transactions/types.ts index 8a519140c9d..1da2e6b2084 100644 --- a/packages/wallet/src/features/transactions/types.ts +++ b/packages/wallet/src/features/transactions/types.ts @@ -61,7 +61,7 @@ export interface TransactionDetails extends TransactionId { } export enum TransactionStatus { - Cancelled = 'cancelled', + Canceled = 'cancelled', Cancelling = 'cancelling', FailedCancel = 'failedCancel', Success = 'confirmed', @@ -76,7 +76,7 @@ export enum TransactionStatus { export type FinalizedTransactionStatus = | TransactionStatus.Success | TransactionStatus.Failed - | TransactionStatus.Cancelled + | TransactionStatus.Canceled | TransactionStatus.FailedCancel export interface FinalizedTransactionDetails extends TransactionDetails { @@ -293,7 +293,7 @@ export function isFinalizedTx( return ( tx.status === TransactionStatus.Success || tx.status === TransactionStatus.Failed || - tx.status === TransactionStatus.Cancelled || + tx.status === TransactionStatus.Canceled || tx.status === TransactionStatus.FailedCancel ) } diff --git a/packages/wallet/src/features/transactions/utils.test.ts b/packages/wallet/src/features/transactions/utils.test.ts index 6aa6deebf6d..7288e47f371 100644 --- a/packages/wallet/src/features/transactions/utils.test.ts +++ b/packages/wallet/src/features/transactions/utils.test.ts @@ -1,9 +1,9 @@ import { CurrencyAmount } from '@uniswap/sdk-core' import { hasSufficientFundsIncludingGas, isOffline } from 'wallet/src/features/transactions/utils' -import { ETH, networkDown, networkUnknown, networkUp } from 'wallet/src/test/fixtures' +import { MAINNET_CURRENCY, networkDown, networkUnknown, networkUp } from 'wallet/src/test/fixtures' -const ZERO_ETH = CurrencyAmount.fromRawAmount(ETH, 0) -const ONE_ETH = CurrencyAmount.fromRawAmount(ETH, 1e18) +const ZERO_ETH = CurrencyAmount.fromRawAmount(MAINNET_CURRENCY, 0) +const ONE_ETH = CurrencyAmount.fromRawAmount(MAINNET_CURRENCY, 1e18) const TEN_ETH = ONE_ETH.multiply(10) describe(hasSufficientFundsIncludingGas, () => { @@ -50,12 +50,12 @@ describe(hasSufficientFundsIncludingGas, () => { describe(isOffline, () => { it('returns true for not connected state', () => { - expect(isOffline(networkDown)).toBe(true) + expect(isOffline(networkDown())).toBe(true) }) it('returns false for connected state', () => { - expect(isOffline(networkUp)).toBe(false) + expect(isOffline(networkUp())).toBe(false) }) it('returns true for unknown state', () => { - expect(isOffline(networkUnknown)).toBe(false) + expect(isOffline(networkUnknown())).toBe(false) }) }) diff --git a/packages/wallet/src/features/transactions/utils.ts b/packages/wallet/src/features/transactions/utils.ts index 256c1865b2c..6aa4f5750a0 100644 --- a/packages/wallet/src/features/transactions/utils.ts +++ b/packages/wallet/src/features/transactions/utils.ts @@ -95,7 +95,7 @@ export function getFinalizedTransactionStatus( return TransactionStatus.Failed } if (currentStatus === TransactionStatus.Cancelling) { - return TransactionStatus.Cancelled + return TransactionStatus.Canceled } return TransactionStatus.Success } diff --git a/packages/wallet/src/features/trm/BlockedAddressWarning.tsx b/packages/wallet/src/features/trm/BlockedAddressWarning.tsx index 02700ebb125..aa5d7d1f3a8 100644 --- a/packages/wallet/src/features/trm/BlockedAddressWarning.tsx +++ b/packages/wallet/src/features/trm/BlockedAddressWarning.tsx @@ -32,7 +32,9 @@ export function BlockedAddressWarning({ width={iconSizes.icon16} /> - {isRecipientBlocked ? t('Recipient wallet is blocked') : t('This wallet is blocked')} + {isRecipientBlocked + ? t('send.warning.blocked.recipient') + : t('send.warning.blocked.default')} diff --git a/packages/wallet/src/features/unitags/hooks.ts b/packages/wallet/src/features/unitags/hooks.ts index 79c5358a428..3c766a10d5c 100644 --- a/packages/wallet/src/features/unitags/hooks.ts +++ b/packages/wallet/src/features/unitags/hooks.ts @@ -162,15 +162,15 @@ export const useUnitagByName = ( // Helper function to enforce unitag length and alphanumeric characters export const getUnitagFormatError = (unitag: string, t: TFunction): string | undefined => { if (unitag.length < MIN_UNITAG_LENGTH) { - return t(`Usernames must be at least {{ minUnitagLength }} characters`, { - minUnitagLength: MIN_UNITAG_LENGTH, + return t('unitags.username.error.min', { + number: MIN_UNITAG_LENGTH, }) } else if (unitag.length > MAX_UNITAG_LENGTH) { - return t(`Usernames cannot be more than {{ maxUnitagLength }} characters`, { - maxUnitagLength: MAX_UNITAG_LENGTH, + return t('unitags.username.error.max', { + number: MAX_UNITAG_LENGTH, }) } else if (!UNITAG_VALID_REGEX.test(unitag)) { - return t('Usernames can only contain lowercase letters and numbers') + return t('unitags.username.error.chars') } return undefined } @@ -194,10 +194,10 @@ export const useCanClaimUnitagName = ( const dataLoaded = !loading && !!data const ensAddressMatchesUnitagAddress = areAddressesEqual(unitagAddress, ensAddress) if (dataLoaded && !data.available) { - error = t('This username is not available') + error = t('unitags.claim.error.unavailable') } if (dataLoaded && data.requiresEnsMatch && !ensAddressMatchesUnitagAddress) { - error = t('This username is not currently available.') + error = t('unitags.claim.error.ensMismatch') } return { error, loading, requiresENSMatch: data?.requiresEnsMatch ?? false } } @@ -218,7 +218,7 @@ export const useClaimUnitag = (): (( return async (claim: UnitagClaim, context: UnitagClaimContext) => { const claimAccount = pendingAccounts[claim.address] || accounts[claim.address] if (!claimAccount || !deviceId) { - return { claimError: t('Could not claim username. Try again later.') } + return { claimError: t('unitags.claim.error.default') } } try { @@ -226,7 +226,7 @@ export const useClaimUnitag = (): (( if (unitagsDeviceAttestationEnabled) { firebaseAppCheckToken = await getFirebaseAppCheckToken() if (!firebaseAppCheckToken) { - return { claimError: t('Could not claim username. Please try again tomorrow.') } + return { claimError: t('unitags.claim.error.appCheck') } } } @@ -263,7 +263,7 @@ export const useClaimUnitag = (): (( dispatch( pushNotification({ type: AppNotificationType.Error, - errorMessage: t('Could not set avatar. Try again later.'), + errorMessage: t('unitags.claim.error.avatar'), }) ) } @@ -276,7 +276,7 @@ export const useClaimUnitag = (): (( return { claimError: undefined } } catch (e) { logger.error(e, { tags: { file: 'useClaimUnitag', function: 'claimUnitag' } }) - return { claimError: t('Could not claim username. Try again later.') } + return { claimError: t('unitags.claim.error.default') } } } } diff --git a/packages/wallet/src/features/unitags/utils.ts b/packages/wallet/src/features/unitags/utils.ts index 41f5dac4a25..eea4566b1d9 100644 --- a/packages/wallet/src/features/unitags/utils.ts +++ b/packages/wallet/src/features/unitags/utils.ts @@ -8,20 +8,18 @@ export function parseUnitagErrorCode( ): string { switch (errorCode) { case UnitagErrorCodes.UnitagNotAvailable: - return t('This username is not available') + return t('unitags.claim.error.unavailable') case UnitagErrorCodes.RequiresENSMatch: - return t('To claim this username you must own the {{ unitag }}.eth ENS', { unitag }) + return t('unitags.claim.error.ens', { username: unitag }) case UnitagErrorCodes.IPLimitReached: case UnitagErrorCodes.AddressLimitReached: case UnitagErrorCodes.DeviceLimitReached: - return t('Unable to claim username') + return t('unitags.claim.error.general') case UnitagErrorCodes.DeviceActiveLimitReached: - return t('You have hit the maximum number of usernames that can be active for this device') + return t('unitags.claim.error.deviceLimit') case UnitagErrorCodes.AddressActiveLimitReached: - return t( - 'You already have made the maximum number of changes to your username for this address' - ) + return t('unitags.claim.error.addressLimit') default: - return t('Unknown error') + return t('unitags.claim.error.unknown') } } diff --git a/packages/wallet/src/features/wallet/accounts/editAccountSaga.ts b/packages/wallet/src/features/wallet/accounts/editAccountSaga.ts index 2940bd366c9..b82d357978a 100644 --- a/packages/wallet/src/features/wallet/accounts/editAccountSaga.ts +++ b/packages/wallet/src/features/wallet/accounts/editAccountSaga.ts @@ -1,4 +1,5 @@ import { all, call, put } from 'typed-redux-saga' +import { isWeb } from 'ui/src' import { logger } from 'utilities/src/logger/logger' import { unique } from 'utilities/src/primitives/array' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' @@ -128,7 +129,10 @@ function* removeAccount(params: RemoveParams) { logger.debug('editAccountSaga', 'removeAccount', 'Removing account', address) // TODO [MOB-243] cleanup account artifacts in native-land (i.e. keystore) yield* put(removeInStore(address)) - yield* call([Keyring, Keyring.removePrivateKey], address) + if (isWeb) { + // Adding multiple calls to this one function was causing race condition errors since `removeAccount` is often called in a loop so limiting to web for now + yield* call([Keyring, Keyring.removePrivateKey], address) + } } function* removeAccounts(params: RemoveBulkParams) { diff --git a/packages/wallet/src/features/wallet/import/importAccountSaga.test.ts b/packages/wallet/src/features/wallet/import/importAccountSaga.test.ts index 2f860d2e2d6..18576f30e11 100644 --- a/packages/wallet/src/features/wallet/import/importAccountSaga.test.ts +++ b/packages/wallet/src/features/wallet/import/importAccountSaga.test.ts @@ -12,8 +12,8 @@ import { SAMPLE_SEED, SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2, - signerManager, } from 'wallet/src/test/fixtures' +import { signerManager } from 'wallet/src/test/mocks' import { importAccount } from './importAccountSaga' import { ImportAccountType, ImportAddressAccountParams, ImportMnemonicAccountParams } from './types' diff --git a/packages/wallet/src/i18n/i18n.ts b/packages/wallet/src/i18n/i18n.ts index b2544622ac1..49a1656845f 100644 --- a/packages/wallet/src/i18n/i18n.ts +++ b/packages/wallet/src/i18n/i18n.ts @@ -53,6 +53,7 @@ export function initializeTranslation(): void { .init({ defaultNS, lng: 'en-US', + fallbackLng: 'en-US', resources, interpolation: { escapeValue: false, // react already safes from xss diff --git a/packages/wallet/src/i18n/locales/en-US.json b/packages/wallet/src/i18n/locales/en-US.json index c844490fe58..aaaec065a8e 100644 --- a/packages/wallet/src/i18n/locales/en-US.json +++ b/packages/wallet/src/i18n/locales/en-US.json @@ -1,846 +1,1963 @@ { - " and build out your customizable profile.": " and build out your customizable profile.", - " with {{address}}": " with {{address}}", - " with {{ensName}}": " with {{ensName}}", - "{{ dappName }} wants to connect to your wallet": "{{ dappName }} wants to connect to your wallet", - "{{ numErrors }} word is invalid or misspelled": "{{ numErrors }} word is invalid or misspelled", - "{{ numErrors }} words are invalid or misspelled": "{{ numErrors }} words are invalid or misspelled", - "{{ prevTxnsCount }} previous transfer": "{{ prevTxnsCount }} previous transfer", - "{{ prevTxnsCount }} previous transfers": "{{ prevTxnsCount }} previous transfers", - "{{ token }} fee": "{{ token }} fee", - "{{amount}} after fees": "{{amount}} after fees", - "{{assetName}} hidden": "{{assetName}} hidden", - "{{assetName}} unhidden": "{{assetName}} unhidden", - "{{authTypeCapitalized}} is disabled": "{{authTypeCapitalized}} is disabled", - "{{capitalizedAuthTypeName}}": "{{capitalizedAuthTypeName}}", - "{{capitalizedAuthTypeName}} is currently turned off for Uniswap Wallet—you can turn it on in your system settings.": "{{capitalizedAuthTypeName}} is currently turned off for Uniswap Wallet—you can turn it on in your system settings.", - "{{capitalizedAuthTypeName}} is not setup": "{{capitalizedAuthTypeName}} is not setup", - "{{capitalizedAuthTypeName}} is not setup on your device. To use {{authenticationTypeName}}, set it up first in Settings.": "{{capitalizedAuthTypeName}} is not setup on your device. To use {{authenticationTypeName}}, set it up first in Settings.", - "{{capitalizedAuthTypeName}} is turned off": "{{capitalizedAuthTypeName}} is turned off", - "{{chainName}} Network": "{{chainName}} Network", - "{{completed}} on {{externalDappName}}": "{{completed}} on {{externalDappName}}", - "{{cryptoAmount}} for {{fiatAmount}}": "{{cryptoAmount}} for {{fiatAmount}}", - "{{num}} MCap": "{{num}} MCap", - "{{num}} TVL": "{{num}} TVL", - "{{num}} Vol": "{{num}} Vol", - "{{numConnections}} apps connected": "{{numConnections}} apps connected", - "{{owner}}’s balance": "{{owner}}’s balance", - "{{slippageTolerancePercent}} slippage": "{{slippageTolerancePercent}} slippage", - "{{step}}/{{totalSteps}} completed": "{{step}}/{{totalSteps}} completed", - "{{symbol}} transfer pending": "{{symbol}} transfer pending", - "{{totalSteps}}/{{totalSteps}} completed": "{{totalSteps}}/{{totalSteps}} completed", - "{{unitag}}{{unitagSuffix}} is ready to send and receive crypto. Continue to build out your wallet by customizing your web3 profile.": "{{unitag}}{{unitagSuffix}} is ready to send and receive crypto. Continue to build out your wallet by customizing your web3 profile.", - "{{what}} from {{sender}}": "{{what}} from {{sender}}", - "{{what}} to {{recipient}}": "{{what}} to {{recipient}}", - "<0>Allow {dapp.name} to use up to<3> {readablePermitAmount} {permitCurrency?.symbol}?": "<0>Allow {dapp.name} to use up to<3> {readablePermitAmount} {permitCurrency?.symbol}?", - "<0>Allow <1>{dapp.name} to use your {permitCurrency?.symbol}?": "<0>Allow <1>{dapp.name} to use your {permitCurrency?.symbol}?", - "<0>This isn’t your currently active wallet.<1>Make sure it’s the right one": "<0>This isn’t your currently active wallet.<1>Make sure it’s the right one", - "<0>You need more <2>{{currencySymbol}} to cover the network cost for this transaction.": "<0>You need more <2>{{currencySymbol}} to cover the network cost for this transaction.", - "<0>You’re removing <2>{{wallet}}": "<0>You’re removing <2>{{wallet}}", - "<0>You’re removing your <2>recovery phrase": "<0>You’re removing your <2>recovery phrase", - "0 backups found": "0 backups found", - "1 app connected": "1 app connected", - "1D": "1D", - "1H": "1H", - "1M": "1M", - "1W": "1W", - "1Y": "1Y", - "24h Volume": "24h Volume", - "52W High": "52W High", - "52W Low": "52W Low", - "A recovery phrase (or seed phrase) is a <2>set of words required to access your wallet, <4>like a password.": "A recovery phrase (or seed phrase) is a <2>set of words required to access your wallet, <4>like a password.", - "A signature is required to prove that you own the wallet without exposing your private keys": "A signature is required to prove that you own the wallet without exposing your private keys", - "A simplified address": "A simplified address", - "About": "About", - "About {{ token }}": "About {{ token }}", - "Accept": "Accept", - "Activity": "Activity", - "Add a view-only wallet": "Add a view-only wallet", - "Add an existing wallet": "Add an existing wallet", - "Add an extra layer of security by requiring {{ authenticationTypeName }} to send transactions.": "Add an extra layer of security by requiring {{ authenticationTypeName }} to send transactions.", - "Add wallet": "Add wallet", - "Add wallets you’ve backed up to your Google Drive account": "Add wallets you’ve backed up to your Google Drive account", - "Add wallets you’ve backed up to your iCloud account": "Add wallets you’ve backed up to your iCloud account", - "Adding a view-only wallet allows you to try out the app or track a wallet. You will not be able to swap or send funds.": "Adding a view-only wallet allows you to try out the app or track a wallet. You will not be able to swap or send funds.", - "Address copied": "Address copied", - "Address is a smart contract": "Address is a smart contract", - "Address not found": "Address not found", - "All networks": "All networks", - "Allow analytics": "Allow analytics", - "Always use dark mode": "Always use dark mode", - "Always use light mode": "Always use light mode", - "and": "and", - "Anyone who gains access to your photos can access your wallet. We recommend that you write down your words instead.": "Anyone who gains access to your photos can access your wallet. We recommend that you write down your words instead.", - "Anyone who knows your recovery phrase can access your wallet and funds": "Anyone who knows your recovery phrase can access your wallet and funds", - "Anyone who knows your recovery phrase can access your wallet and funds.": "Anyone who knows your recovery phrase can access your wallet and funds.", - "App access": "App access", - "App permissions": "App permissions", - "Appearance": "Appearance", - "approve": "approve", - "Approved": "Approved", - "Approved {{currencySymbol}} for use with {{address}}.": "Approved {{currencySymbol}} for use with {{address}}.", - "Approving": "Approving", - "Are you sure?": "Are you sure?", - "Australian Dollar": "Australian Dollar", - "Auto": "Auto", - "Back": "Back", - "Back up recovery phrase to Google Drive?": "Back up recovery phrase to Google Drive?", - "Back up recovery phrase to iCloud?": "Back up recovery phrase to iCloud?", - "Back up to Google Drive": "Back up to Google Drive", - "Back up to iCloud": "Back up to iCloud", - "Back up your wallet": "Back up your wallet", - "Backed up": "Backed up", - "Backed up to Google Drive": "Backed up to Google Drive", - "Backed up to iCloud": "Backed up to iCloud", - "Backing up to Google Drive...": "Backing up to Google Drive...", - "Backing up to iCloud...": "Backing up to iCloud...", - "Backups let you restore your wallet if you delete the app or lose your device": "Backups let you restore your wallet if you delete the app or lose your device", - "Balance": "Balance", - "Balances on other networks": "Balances on other networks", - "Be careful: this {{ requestType }} may transfer assets": "Be careful: this {{ requestType }} may transfer assets", - "Because these wallets share a recovery phrase, it will also delete the backups for:": "Because these wallets share a recovery phrase, it will also delete the backups for:", - "Because you’re on a new device, you’ll need to restore your recovery phrase. This will allow you to swap and send tokens.": "Because you’re on a new device, you’ll need to restore your recovery phrase. This will allow you to swap and send tokens.", - "Before you continue": "Before you continue", - "Best overall": "Best overall", - "Bio": "Bio", - "Blocked address": "Blocked address", - "Bought": "Bought", - "Brazilian Real": "Brazilian Real", - "British Pound": "British Pound", - "Browser": "Browser", - "Build a personalized web3 profile and easily share your address with friends.": "Build a personalized web3 profile and easily share your address with friends.", - "But, if you <2>lose your recovery phrase, you’ll <5>lose access to your wallet.": "But, if you <2>lose your recovery phrase, you’ll <5>lose access to your wallet.", - "buy": "buy", - "Buy": "Buy", - "Buy {{ currencySymbol }}": "Buy {{ currencySymbol }}", - "Buy crypto": "Buy crypto", - "Buy crypto with a card or bank to send tokens.": "Buy crypto with a card or bank to send tokens.", - "Buy or transfer crypto": "Buy or transfer crypto", - "Buying": "Buying", - "Buying {{amount}} worth of {{quoteCurrencyCode}}": "Buying {{amount}} worth of {{quoteCurrencyCode}}", - "By continuing, I agree to the <2>Terms of Service and consent to the <6>Privacy Policy.": "By continuing, I agree to the <2>Terms of Service and consent to the <6>Privacy Policy.", - "By having your recovery phrase backed up to Google Drive, you can recover your wallet just by being logged into your Google account on any device.": "By having your recovery phrase backed up to Google Drive, you can recover your wallet just by being logged into your Google account on any device.", - "By having your recovery phrase backed up to iCloud, you can recover your wallet just by being logged into your iCloud on any device.": "By having your recovery phrase backed up to iCloud, you can recover your wallet just by being logged into your iCloud on any device.", - "Camera is disabled": "Camera is disabled", - "Canadian Dollar": "Canadian Dollar", - "Cancel": "Cancel", - "Cancel this transaction?": "Cancel this transaction?", - "Cancel transaction": "Cancel transaction", - "Canceled {{assetInfo}} send.": "Canceled {{assetInfo}} send.", - "Canceled {{currencySymbol}} approve.": "Canceled {{currencySymbol}} approve.", - "Canceled {{inputCurrencySymbol}} unwrap.": "Canceled {{inputCurrencySymbol}} unwrap.", - "Canceled {{inputCurrencySymbol}} wrap.": "Canceled {{inputCurrencySymbol}} wrap.", - "Canceled {{inputCurrencySymbol}}-{{outputCurrencySymbol}} swap.": "Canceled {{inputCurrencySymbol}}-{{outputCurrencySymbol}} swap.", - "Cancelled {{action}}": "Cancelled {{action}}", - "Cancelling {{action}}": "Cancelling {{action}}", - "Caution": "Caution", - "Change password": "Change password", - "Change preferred language": "Change preferred language", - "Check out your tokens and NFTs, follow crypto wallets, and stay up to date on the go.": "Check out your tokens and NFTs, follow crypto wallets, and stay up to date on the go.", - "Check your Uniswap mobile app for the 6-character code": "Check your Uniswap mobile app for the 6-character code", - "Checkout": "Checkout", - "Checkout with": "Checkout with", - "Chinese Yuan": "Chinese Yuan", - "Chinese, Simplified": "Chinese, Simplified", - "Chinese, Traditional": "Chinese, Traditional", - "Choose a backup method": "Choose a backup method", - "Choose a profile photo": "Choose a profile photo", - "Choose a token": "Choose a token", - "Choose an NFT": "Choose an NFT", - "Choose from camera roll": "Choose from camera roll", - "Choose how you want to add your wallet": "Choose how you want to add your wallet", - "Choose token": "Choose token", - "Choose wallets to import": "Choose wallets to import", - "Choose your username": "Choose your username", - "Claim now": "Claim now", - "claim period": "claim period", - "Claim your {{unitagSuffix}} username": "Claim your {{unitagSuffix}} username", - "Claim your username": "Claim your username", - "Clear all": "Clear all", - "Close": "Close", - "Collection website": "Collection website", - "confirm": "confirm", - "Confirm": "Confirm", - "Confirm password": "Confirm password", - "Confirm your backup password": "Confirm your backup password", - "Confirm your recovery phrase": "Confirm your recovery phrase", - "Connect": "Connect", - "Connect to an app by scanning a code via WalletConnect": "Connect to an app by scanning a code via WalletConnect", - "Connected": "Connected", - "Connected to ": "Connected to ", - "Connecting you to {{serviceProvider}}": "Connecting you to {{serviceProvider}}", - "Connecting...": "Connecting...", - "Connection Error": "Connection Error", - "Content not available": "Content not available", - "Continue": "Continue", - "Continue on Uniswap Extension": "Continue on Uniswap Extension", - "Continue to checkout": "Continue to checkout", - "Contract": "Contract", - "Contract address copied": "Contract address copied", - "Copied": "Copied", - "Copy": "Copy", - "Copy address": "Copy address", - "Copy MoonPay transaction ID": "Copy MoonPay transaction ID", - "Copy transaction ID": "Copy transaction ID", - "Copy wallet address": "Copy wallet address", - "Could not change username. Try again later.": "Could not change username. Try again later.", - "Could not claim username. Please try again tomorrow.": "Could not claim username. Please try again tomorrow.", - "Could not claim username. Try again later.": "Could not claim username. Try again later.", - "Could not delete username. Try again later.": "Could not delete username. Try again later.", - "Could not set avatar. Try again later.": "Could not set avatar. Try again later.", - "Could not update profile. Try again later.": "Could not update profile. Try again later.", - "Couldn’t load activity": "Couldn’t load activity", - "Couldn’t load addresses": "Couldn’t load addresses", - "Couldn’t load NFT collection": "Couldn’t load NFT collection", - "Couldn’t load NFT details": "Couldn’t load NFT details", - "Couldn’t load NFTs": "Couldn’t load NFTs", - "Couldn’t load price chart": "Couldn’t load price chart", - "Couldn’t load search results": "Couldn’t load search results", - "Couldn’t load token balances": "Couldn’t load token balances", - "Couldn’t load tokens": "Couldn’t load tokens", - "Couldn’t load tokens to buy": "Couldn’t load tokens to buy", - "Create a new wallet": "Create a new wallet", - "Create a password": "Create a password", - "Create password": "Create password", - "Create wallet": "Create wallet", - "Create your backup password": "Create your backup password", - "Current password": "Current password", - "Current price": "Current price", - "Customizable profiles": "Customizable profiles", - "Customize profile": "Customize profile", - "Dark mode": "Dark mode", - "Default to your device’s appearance": "Default to your device’s appearance", - "Delete": "Delete", - "Delete backup": "Delete backup", - "Delete username": "Delete username", - "Device": "Device", - "Device settings": "Device settings", - "Disconnect": "Disconnect", - "Disconnected": "Disconnected", - "Dismiss": "Dismiss", - "Do not share this with anyone": "Do not share this with anyone", - "Do this step in a private place": "Do this step in a private place", - "Done": "Done", - "Due to the amount of {{ currencyOut }} liquidity currently available, the more {{ currencyIn }} you try to swap, the less {{ currencyOut }} you will receive.": "Due to the amount of {{ currencyOut }} liquidity currently available, the more {{ currencyIn }} you try to swap, the less {{ currencyOut }} you will receive.", - "Dutch": "Dutch", - "Edit favorite tokens": "Edit favorite tokens", - "Edit favorite wallets": "Edit favorite wallets", - "Edit favorites": "Edit favorites", - "Edit label": "Edit label", - "Edit nickname": "Edit nickname", - "Edit profile": "Edit profile", - "Edit username": "Edit username", - "Enable": "Enable", - "Encrypt your recovery phrase with a secure password": "Encrypt your recovery phrase with a secure password", - "English": "English", - "ENS": "ENS", - "ENS claim period": "ENS claim period", - "ENS or address": "ENS or address", - "Enter a value larger than 0": "Enter a value larger than 0", - "Enter a value less than {{ maxSlippageTolerance }}": "Enter a value less than {{ maxSlippageTolerance }}", - "Enter a wallet address": "Enter a wallet address", - "Enter backup password": "Enter backup password", - "Enter one-time code": "Enter one-time code", - "Enter password": "Enter password", - "Enter recovery phrase": "Enter recovery phrase", - "Enter this code in the Uniswap Extension. Your recovery phrase will be safely encrypted and transferred.": "Enter this code in the Uniswap Extension. Your recovery phrase will be safely encrypted and transferred.", - "Enter this wallet’s recovery phrase to begin swapping and sending.": "Enter this wallet’s recovery phrase to begin swapping and sending.", - "Enter your current password": "Enter your current password", - "Enter your password to continue": "Enter your password to continue", - "Enter your recovery phrase": "Enter your recovery phrase", - "Enter your recovery phrase below, or try searching for backups again.": "Enter your recovery phrase below, or try searching for backups again.", - "Enter your recovery phrase from another crypto wallet": "Enter your recovery phrase from another crypto wallet", - "Enter your recovery phrase instead": "Enter your recovery phrase instead", - "Error": "Error", - "Error importing wallets": "Error importing wallets", - "Error loading accounts": "Error loading accounts", - "Error while checking transaction status": "Error while checking transaction status", - "Error while importing backups": "Error while importing backups", - "Euro": "Euro", - "Expired": "Expired", - "Expires in {{duration}}": "Expires in {{duration}}", - "Expires in {{duration}}...": "Expires in {{duration}}...", - "Explore tokens & NFTs": "Explore tokens & NFTs", - "Failed attempts: {failedAttemptCount.toString()}": "Failed attempts: {failedAttemptCount.toString()}", - "Failed to {{action}}": "Failed to {{action}}", - "Failed to approve {{currencySymbol}} for use with {{address}}.": "Failed to approve {{currencySymbol}} for use with {{address}}.", - "Failed to copy to clipboard": "Failed to copy to clipboard", - "Failed to fetch token balances": "Failed to fetch token balances", - "Failed to import backups due to lack of permissions, interruption of authorization, or due to a cloud error": "Failed to import backups due to lack of permissions, interruption of authorization, or due to a cloud error", - "Failed to send {{assetInfo}} to {{senderOrRecipient}}.": "Failed to send {{assetInfo}} to {{senderOrRecipient}}.", - "Failed to swap {{inputAssetInfo}} for {{outputAssetInfo}}.": "Failed to swap {{inputAssetInfo}} for {{outputAssetInfo}}.", - "Failed to transact{{addressText}}.": "Failed to transact{{addressText}}.", - "Failed to unwrap {{inputAssetInfo}}.": "Failed to unwrap {{inputAssetInfo}}.", - "Failed to wrap {{inputAssetInfo}}.": "Failed to wrap {{inputAssetInfo}}.", - "Favorite token": "Favorite token", - "Favorite tokens": "Favorite tokens", - "Favorite wallet": "Favorite wallet", - "Favorite wallets": "Favorite wallets", - "Favorites": "Favorites", - "Fee": "Fee", - "Feed": "Feed", - "Fees are applied on a few select tokens to ensure the best experience with Uniswap, and have already been factored into this quote.": "Fees are applied on a few select tokens to ensure the best experience with Uniswap, and have already been factored into this quote.", - "Fees are applied on a few select tokens to ensure the best experience with Uniswap. There is no fee associated with this swap.": "Fees are applied on a few select tokens to ensure the best experience with Uniswap. There is no fee associated with this swap.", - "Floor": "Floor", - "Follow the instructions on the browser web page to reset your password": "Follow the instructions on the browser web page to reset your password", - "For a limited time, the username {{username}} is reserved. Import the wallet that owns {{username}}.eth ENS to claim this username or try again after the claim period.": "For a limited time, the username {{username}} is reserved. Import the wallet that owns {{username}}.eth ENS to claim this username or try again after the claim period.", - "Forgot password?": "Forgot password?", - "Free to claim": "Free to claim", - "French": "French", - "Fully Diluted Valuation": "Fully Diluted Valuation", - "Function": "Function", - "Fund your wallet by transferring crypto from another wallet or account": "Fund your wallet by transferring crypto from another wallet or account", - "Get help": "Get help", - "Get notified when your transfers, swaps, and approvals complete.": "Get notified when your transfers, swaps, and approvals complete.", - "Get tokens at the best prices in web3 with Uniswap Wallet.": "Get tokens at the best prices in web3 with Uniswap Wallet.", - "Give your wallet a nickname": "Give your wallet a nickname", - "Go to settings": "Go to settings", - "Google Drive backup": "Google Drive backup", - "Google Drive error": "Google Drive error", - "Google Drive not available": "Google Drive not available", - "got it!": "got it!", - "Have the Uniswap app?": "Have the Uniswap app?", - "Help center": "Help center", - "Hidden ({{numHidden}})": "Hidden ({{numHidden}})", - "Hide": "Hide", - "Hide NFT": "Hide NFT", - "Hide recovery phrase": "Hide recovery phrase", - "Hide small balances": "Hide small balances", - "Hide Token": "Hide Token", - "Hide unknown tokens": "Hide unknown tokens", - "Hide wallets": "Hide wallets", - "High price impact ({{ swapSize }})": "High price impact ({{ swapSize }})", - "Hindi": "Hindi", - "Hold to swap": "Hold to swap", - "Hold to unwrap": "Hold to unwrap", - "Hold to wrap": "Hold to wrap", - "Hong Kong Dollar": "Hong Kong Dollar", - "How do I find my recovery phrase?": "How do I find my recovery phrase?", - "I already have a wallet": "I already have a wallet", - "I backed up my recovery phrase and understand that Uniswap Labs can’t help me recover my wallets if I failed to do so.": "I backed up my recovery phrase and understand that Uniswap Labs can’t help me recover my wallets if I failed to do so.", - "I saved my recovery phrase": "I saved my recovery phrase", - "I understand": "I understand", - "I understand that if I lose my recovery phrase, Uniswap Labs cannot help me restore it": "I understand that if I lose my recovery phrase, Uniswap Labs cannot help me restore it", - "I understand that Uniswap Labs can’t help me recover my wallet if I failed to do so": "I understand that Uniswap Labs can’t help me recover my wallet if I failed to do so", - "I’m ready": "I’m ready", - "iCloud backup": "iCloud backup", - "iCloud Drive not available": "iCloud Drive not available", - "iCloud error": "iCloud error", - "If the price slips any further, your transaction will revert. Below is the maximum amount you would need to spend.": "If the price slips any further, your transaction will revert. Below is the maximum amount you would need to spend.", - "If the price slips any further, your transaction will revert. Below is the minimum amount you are guaranteed to receive.": "If the price slips any further, your transaction will revert. Below is the minimum amount you are guaranteed to receive.", - "If you cancel this transaction before it’s processed by the network, you’ll pay a new network fee instead of the original one.": "If you cancel this transaction before it’s processed by the network, you’ll pay a new network fee instead of the original one.", - "If you delete your Google Drive backup, you’ll only be able to recover your wallet with a manual backup of your recovery phrase. Uniswap Labs can’t recover your assets if you lose your recovery phrase.": "If you delete your Google Drive backup, you’ll only be able to recover your wallet with a manual backup of your recovery phrase. Uniswap Labs can’t recover your assets if you lose your recovery phrase.", - "If you delete your iCloud backup, you’ll only be able to recover your wallet with a manual backup of your recovery phrase. Uniswap Labs can’t recover your assets if you lose your recovery phrase.": "If you delete your iCloud backup, you’ll only be able to recover your wallet with a manual backup of your recovery phrase. Uniswap Labs can’t recover your assets if you lose your recovery phrase.", - "If you don’t turn on {{authenticationTypeName}}, anyone who gains access to your device can open Uniswap Wallet and make transactions.": "If you don’t turn on {{authenticationTypeName}}, anyone who gains access to your device can open Uniswap Wallet and make transactions.", - "Image copied": "Image copied", - "Import a new wallet": "Import a new wallet", - "Import a wallet": "Import a wallet", - "Import from your phone": "Import from your phone", - "Import wallet": "Import wallet", - "In order to sign messages or transactions, you’ll need to import the wallet’s recovery phrase.": "In order to sign messages or transactions, you’ll need to import the wallet’s recovery phrase.", - "Indian Rupee": "Indian Rupee", - "Indonesian": "Indonesian", - "Indonesian Rupiah": "Indonesian Rupiah", - "Instead of memorizing your recovery phrase, you can <2>back it up to Google Drive and protect it with a password.": "Instead of memorizing your recovery phrase, you can <2>back it up to Google Drive and protect it with a password.", - "Instead of memorizing your recovery phrase, you can <2>back it up to iCloud and protect it with a password.": "Instead of memorizing your recovery phrase, you can <2>back it up to iCloud and protect it with a password.", - "Internet or network connection error": "Internet or network connection error", - "Introducing usernames": "Introducing usernames", - "Invalid password. Please try again.": "Invalid password. Please try again.", - "Invalid phrase": "Invalid phrase", - "Invalid public key.": "Invalid public key.", - "Invalid QR Code": "Invalid QR Code", - "Invalid word: ": "Invalid word: ", - "Invalid word: {{word}}": "Invalid word: {{word}}", - "Is this a wallet address?": "Is this a wallet address?", - "Is this your device?": "Is this your device?", - "It looks like you haven’t backed up any of your seed phrases to Google Drive.": "It looks like you haven’t backed up any of your seed phrases to Google Drive.", - "It looks like you haven’t backed up any of your seed phrases to iCloud.": "It looks like you haven’t backed up any of your seed phrases to iCloud.", - "It shares the same recovery phrase as <2>{{wallets}}. Your recovery phrase will remain stored until you delete all remaining wallets.": "It shares the same recovery phrase as <2>{{wallets}}. Your recovery phrase will remain stored until you delete all remaining wallets.", - "Items": "Items", - "Japanese": "Japanese", - "Japanese Yen": "Japanese Yen", - "Keep in mind that the network fee is still charged for failed transfers.": "Keep in mind that the network fee is still charged for failed transfers.", - "Labels are not public. They are stored locally and only visible to you.": "Labels are not public. They are stored locally and only visible to you.", - "Language": "Language", - "Last sale price": "Last sale price", - "Learn how to use the Uniswap Wallet": "Learn how to use the Uniswap Wallet", - "Learn more": "Learn more", - "Learn more about our": "Learn more about our", - "Learn more about wallet safety": "Learn more about wallet safety", - "Let’s keep it safe": "Let’s keep it safe", - "Let’s make sure you’ve recorded it correctly": "Let’s make sure you’ve recorded it correctly", - "Light mode": "Light mode", - "Link an account": "Link an account", - "Links": "Links", - "Loading...": "Loading...", - "Local currency": "Local currency", - "Lock wallet": "Lock wallet", - "Made with love, ": "Made with love, ", - "Make sure that you’re scanning a valid Ethereum address QR code before trying again.": "Make sure that you’re scanning a valid Ethereum address QR code before trying again.", - "Make sure that you’re scanning a valid WalletConnect or Ethereum address QR code before trying again.": "Make sure that you’re scanning a valid WalletConnect or Ethereum address QR code before trying again.", - "Make sure you’ve saved your recovery phrase. You will lose access to your funds otherwise": "Make sure you’ve saved your recovery phrase. You will lose access to your funds otherwise", - "Make sure you’ve written down your recovery phrase or backed it up on Google Drive. <2>You will not be able to access your funds otherwise.": "Make sure you’ve written down your recovery phrase or backed it up on Google Drive. <2>You will not be able to access your funds otherwise.", - "Make sure you’ve written down your recovery phrase or backed it up on iCloud. <2>You will not be able to access your funds otherwise.": "Make sure you’ve written down your recovery phrase or backed it up on iCloud. <2>You will not be able to access your funds otherwise.", - "Malay": "Malay", - "Manage connections": "Manage connections", - "Manage wallet": "Manage wallet", - "Manual backup": "Manual backup", - "Market cap": "Market cap", - "Market Cap": "Market Cap", - "Max": "Max", - "Max slippage": "Max slippage", - "Maximum {{amount}}": "Maximum {{amount}}", - "Maximum slippage": "Maximum slippage", - "Maybe later": "Maybe later", - "Medium": "Medium", - "Minimum {{amount}}": "Minimum {{amount}}", - "mint": "mint", - "Minted": "Minted", - "Minting": "Minting", - "My recovery phrase is 12 words": "My recovery phrase is 12 words", - "My recovery phrase is longer than 12 words": "My recovery phrase is longer than 12 words", - "N/A": "N/A", - "Network": "Network", - "Network cost": "Network cost", - "Networks": "Networks", - "Never enter it to any websites or apps": "Never enter it to any websites or apps", - "New address": "New address", - "New input": "New input", - "New output": "New output", - "New password": "New password", - "NFT Collections": "NFT Collections", - "NFTs": "NFTs", - "Nickname": "Nickname", - "Nigerian Naira": "Nigerian Naira", - "No activity yet": "No activity yet", - "No approvals pending": "No approvals pending", - "No apps connected": "No apps connected", - "No backups found": "No backups found", - "No message found.": "No message found.", - "No NFTs found": "No NFTs found", - "No NFTs yet": "No NFTs yet", - "No OTP received. Please try again.": "No OTP received. Please try again.", - "No QR code found": "No QR code found", - "No results found": "No results found", - "No results found for <1>\"{searchFilter}\"": "No results found for <1>\"{searchFilter}\"", - "No results found for <1>\"{searchQuery}\"": "No results found for <1>\"{searchQuery}\"", - "No tokens yet": "No tokens yet", - "Not available": "Not available", - "Not available on {{chainName}}": "Not available on {{chainName}}", - "Not enough {{ currencySymbol }}": "Not enough {{ currencySymbol }}", - "Not enough {{ symbol }}.": "Not enough {{ symbol }}.", - "Not enough liquidity": "Not enough liquidity", - "Not now": "Not now", - "Not supported in region": "Not supported in region", - "Notifications": "Notifications", - "Notifications permission": "Notifications permission", - "OK": "OK", - "Once you change your username, you can never claim it again. You can only change it 2 times.": "Once you change your username, you can never claim it again. You can only change it 2 times.", - "One wallet found": "One wallet found", - "Only available to purchase in USD": "Only available to purchase in USD", - "Only continue if you are syncing with the Uniswap Extension on a trusted device.": "Only continue if you are syncing with the Uniswap Extension on a trusted device.", - "Oops! Something went wrong.": "Oops! Something went wrong.", - "Other options": "Other options", - "Owned by": "Owned by", - "Owned by {{owner}}": "Owned by {{owner}}", - "Owners": "Owners", - "Pakistani Rupee": "Pakistani Rupee", - "Password": "Password", - "Password reset": "Password reset", - "Passwords do not match": "Passwords do not match", - "Passwords don’t match": "Passwords don’t match", - "Paste": "Paste", - "Pin Uniswap Wallet to your browser toolbar by clicking on": "Pin Uniswap Wallet to your browser toolbar by clicking on", - "Please authenticate": "Please authenticate", - "Please try again in a few minutes.": "Please try again in a few minutes.", - "Please verify that you are logged in to a Google account with Google Drive enabled on this device and try again.": "Please verify that you are logged in to a Google account with Google Drive enabled on this device and try again.", - "Please verify that you are logged in to an Apple ID with iCloud Drive enabled on this device and try again.": "Please verify that you are logged in to an Apple ID with iCloud Drive enabled on this device and try again.", - "Popular NFT collections": "Popular NFT collections", - "Popular tokens": "Popular tokens", - "Portuguese": "Portuguese", - "Powered by ENS subdomains": "Powered by ENS subdomains", - "Preferences": "Preferences", - "Price decrease": "Price decrease", - "Price decrease (24H)": "Price decrease (24H)", - "Price increase": "Price increase", - "Price increase (24H)": "Price increase (24H)", - "Privacy": "Privacy", - "Privacy policy": "Privacy policy", - "Profile updated": "Profile updated", - "Protect your wallet": "Protect your wallet", - "purchase": "purchase", - "Purchase with a card, or transfer from an exchange": "Purchase with a card, or transfer from an exchange", - "Purchased": "Purchased", - "Purchasing": "Purchasing", - "Rate": "Rate", - "Rate limit exceeded": "Rate limit exceeded", - "Read less": "Read less", - "Read more": "Read more", - "Read the following carefully before continuing": "Read the following carefully before continuing", - "receive": "receive", - "Receive": "Receive", - "Receive {{amount}}": "Receive {{amount}}", - "Receive at least": "Receive at least", - "Receive at least {{amount}} {{symbol}}": "Receive at least {{amount}} {{symbol}}", - "Receive crypto": "Receive crypto", - "Receive funds": "Receive funds", - "Receive NFTs": "Receive NFTs", - "Receive tokens": "Receive tokens", - "Receive tokens or NFTs": "Receive tokens or NFTs", - "Received": "Received", - "Received {{assetInfo}} from {{senderOrRecipient}}.": "Received {{assetInfo}} from {{senderOrRecipient}}.", - "Receiving": "Receiving", - "Recent": "Recent", - "Recent searches": "Recent searches", - "Recently used": "Recently used", - "Recipient wallet is blocked": "Recipient wallet is blocked", - "Recovery phrase": "Recovery phrase", - "Recovery phrase must be 12-24 words": "Recovery phrase must be 12-24 words", - "Remove": "Remove", - "Remove favorite": "Remove favorite", - "Remove profile picture": "Remove profile picture", - "Remove recovery phrase": "Remove recovery phrase", - "Remove wallet": "Remove wallet", - "Report profile": "Report profile", - "Request from": "Request from", - "Require {{authenticationTypeName}} to open app": "Require {{authenticationTypeName}} to open app", - "Require {{authenticationTypeName}} to transact": "Require {{authenticationTypeName}} to transact", - "Reset your password": "Reset your password", - "Restart app": "Restart app", - "Restore": "Restore", - "Restore a wallet": "Restore a wallet", - "Restore from Google Drive": "Restore from Google Drive", - "Restore from iCloud": "Restore from iCloud", - "Restore wallet": "Restore wallet", - "Restore your wallet to send": "Restore your wallet to send", - "Restore your wallet to swap": "Restore your wallet to swap", - "Retry": "Retry", - "Review": "Review", - "Review transfer": "Review transfer", - "revoke": "revoke", - "Revoked": "Revoked", - "Revoking": "Revoking", - "Russian": "Russian", - "Russian Ruble": "Russian Ruble", - "Save": "Save", - "Save changes": "Save changes", - "Save your recovery phrase": "Save your recovery phrase", - "Say goodbye to 0x addresses. Usernames are readable names that make it easier to send and receive crypto.": "Say goodbye to 0x addresses. Usernames are readable names that make it easier to send and receive crypto.", - "Scan": "Scan", - "Scan a QR code": "Scan a QR code", - "Scan the QR code on the Uniswap Extension again to continue syncing your wallet.": "Scan the QR code on the Uniswap Extension again to continue syncing your wallet.", - "Scan the QR code with the Uniswap app to import your wallet": "Scan the QR code with the Uniswap app to import your wallet", - "Scan with Uniswap app": "Scan with Uniswap app", - "Screenshots aren’t secure": "Screenshots aren’t secure", - "Search": "Search", - "Search and browse trending tokens and NFTs": "Search and browse trending tokens and NFTs", - "Search by country or region": "Search by country or region", - "Search ENS or address": "Search ENS or address", - "Search results": "Search results", - "Search tokens": "Search tokens", - "Search tokens and wallets": "Search tokens and wallets", - "Searching for backups...": "Searching for backups...", - "Searching for wallets": "Searching for wallets", - "Secret word": "Secret word", - "Security": "Security", - "Select a backup to restore": "Select a backup to restore", - "Select the missing words in order.": "Select the missing words in order.", - "Select wallets to import": "Select wallets to import", - "Select your region": "Select your region", - "sell": "sell", - "Sell": "Sell", - "Selling": "Selling", - "send": "send", - "Send": "Send", - "Send failed": "Send failed", - "Send feedback": "Send feedback", - "Send successful!": "Send successful!", - "Sending": "Sending", - "Sent": "Sent", - "Sent {{assetInfo}} to {{senderOrRecipient}}.": "Sent {{assetInfo}} to {{senderOrRecipient}}.", - "Set up": "Set up", - "Setting a password will encrypt your recovery phrase backup, adding an extra level of protection if your Google Drive account is ever compromised.": "Setting a password will encrypt your recovery phrase backup, adding an extra level of protection if your Google Drive account is ever compromised.", - "Setting a password will encrypt your recovery phrase backup, adding an extra level of protection if your iCloud account is ever compromised.": "Setting a password will encrypt your recovery phrase backup, adding an extra level of protection if your iCloud account is ever compromised.", - "Settings": "Settings", - "Share": "Share", - "Show": "Show", - "Show all {{numberOfWallets}} wallets": "Show all {{numberOfWallets}} wallets", - "Show less": "Show less", - "Show more": "Show more", - "Show my QR code": "Show my QR code", - "Show original": "Show original", - "Show recovery phrase": "Show recovery phrase", - "Sign": "Sign", - "Signature request from": "Signature request from", - "Singapore Dollar": "Singapore Dollar", - "Skip": "Skip", - "Slippage may be higher than necessary": "Slippage may be higher than necessary", - "Slippage Settings": "Slippage Settings", - "Sold": "Sold", - "Some tokens take a fee when they are bought or sold, which is set by the token issuer. Uniswap does not receive any share of these fees.": "Some tokens take a fee when they are bought or sold, which is set by the token issuer. Uniswap does not receive any share of these fees.", - "Something crashed.": "Something crashed.", - "Something went wrong": "Something went wrong", - "Something went wrong.": "Something went wrong.", - "Sorry, we are unable to load the QR code right now. Please try another onboarding method.": "Sorry, we are unable to load the QR code right now. Please try another onboarding method.", - "Spanish (Latin America)": "Spanish (Latin America)", - "Spanish (Spain)": "Spanish (Spain)", - "Spanish (US)": "Spanish (US)", - "Spend at most": "Spend at most", - "Spend at most {{amount}} {{symbol}}": "Spend at most {{amount}} {{symbol}}", - "Start swapping": "Start swapping", - "Stats": "Stats", - "Strong": "Strong", - "Submitted on": "Submitted on", - "Success": "Success", - "Suggested": "Suggested", - "Suggested wallets": "Suggested wallets", - "Support": "Support", - "Supported Networks": "Supported Networks", - "swap": "swap", - "Swap": "Swap", - "Swap {currency0?.symbol} → {currency1?.symbol}": "Swap {currency0?.symbol} → {currency1?.symbol}", - "Swap fee": "Swap fee", - "Swap pending": "Swap pending", - "Swap protection": "Swap protection", - "Swap Protection": "Swap Protection", - "Swap Settings": "Swap Settings", - "Swap Tokens": "Swap Tokens", - "Swapped": "Swapped", - "Swapped {{inputAssetInfo}} for {{outputAssetInfo}}.": "Swapped {{inputAssetInfo}} for {{outputAssetInfo}}.", - "Swapping": "Swapping", - "Swapping on {{ network }}": "Swapping on {{ network }}", - "Switch Account": "Switch Account", - "Switch Network": "Switch Network", - "Switched networks": "Switched networks", - "Switched to {{name}}": "Switched to {{name}}", - "Sync from your phone": "Sync from your phone", - "Terms of service": "Terms of service", - "Thai": "Thai", - "Thai Baht": "Thai Baht", - "The address you typed either does not exist or is spelled incorrectly.": "The address you typed either does not exist or is spelled incorrectly.", - "The code you submitted is incorrect, or there was an error submitting. Please try again.": "The code you submitted is incorrect, or there was an error submitting. Please try again.", - "The phrase you entered is invalid": "The phrase you entered is invalid", - "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.": "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.", - "There are multiple recovery phrases backed up to your Google Drive.": "There are multiple recovery phrases backed up to your Google Drive.", - "There are multiple recovery phrases backed up to your iCloud.": "There are multiple recovery phrases backed up to your iCloud.", - "There isn’t currently enough liquidity available between these tokens to perform a swap. Please try again later or select another token.": "There isn’t currently enough liquidity available between these tokens to perform a swap. Please try again later or select another token.", - "There was an issue scanning this QR code.": "There was an issue scanning this QR code.", - "There was an issue with WalletConnect. Please try again": "There was an issue with WalletConnect. Please try again", - "This address is already imported": "This address is already imported", - "This address is blocked on Uniswap Wallet because it is associated with one or more blocked activities. If you believe this is an error, please email compliance@uniswap.org.": "This address is blocked on Uniswap Wallet because it is associated with one or more blocked activities. If you believe this is an error, please email compliance@uniswap.org.", - "This is a {text.toLowerCase()} password": "This is a {text.toLowerCase()} password", - "This is a view-only wallet": "This is a view-only wallet", - "This is the cost to process your transaction on the blockchain. Uniswap does not receive any share of these fees.": "This is the cost to process your transaction on the blockchain. Uniswap does not receive any share of these fees.", - "This is your personal space for tokens, NFTs, and all of your trades. Finish setting it up to keep your funds safe.": "This is your personal space for tokens, NFTs, and all of your trades. Finish setting it up to keep your funds safe.", - "This is your unique name that anyone can send crypto to.": "This is your unique name that anyone can send crypto to.", - "This nickname is only visible to you": "This nickname is only visible to you", - "This nickname is only visible to you.": "This nickname is only visible to you.", - "This password is required to recover your recovery phrase backup from Google Drive.": "This password is required to recover your recovery phrase backup from Google Drive.", - "This password is required to recover your recovery phrase backup from iCloud.": "This password is required to recover your recovery phrase backup from iCloud.", - "This QR code is not supported.": "This QR code is not supported.", - "This recovery phrase gives you full access to your wallet and funds": "This recovery phrase gives you full access to your wallet and funds", - "This token isn’t traded on leading U.S. centralized exchanges or frequently swapped on Uniswap. Always conduct your own research before trading.": "This token isn’t traded on leading U.S. centralized exchanges or frequently swapped on Uniswap. Always conduct your own research before trading.", - "This token isn’t traded on leading U.S. centralized exchanges. Always conduct your own research before trading.": "This token isn’t traded on leading U.S. centralized exchanges. Always conduct your own research before trading.", - "This trade cannot be completed right now": "This trade cannot be completed right now", - "This transaction is expected to fail": "This transaction is expected to fail", - "This username is not available": "This username is not available", - "This username is not currently available.": "This username is not currently available.", - "This wallet is blocked": "This wallet is blocked", - "This wallet is in view only mode": "This wallet is in view only mode", - "This wallet is view-only": "This wallet is view-only", - "This will remove your wallet from this device along with your recovery phrase.": "This will remove your wallet from this device along with your recovery phrase.", - "Tip: Hold to instant swap": "Tip: Hold to instant swap", - "To": "To", - "To claim this username you must own the {{ unitag }}.eth ENS": "To claim this username you must own the {{ unitag }}.eth ENS", - "To create more wallets, open the account switcher inside the extension popup, or reinstall the extension to start over": "To create more wallets, open the account switcher inside the extension popup, or reinstall the extension to start over", - "To receive notifications, turn on notifications for Uniswap Wallet in your device’s settings.": "To receive notifications, turn on notifications for Uniswap Wallet in your device’s settings.", - "To reset your password, enter your wallet’s recovery phrase. Uniswap cannot help recover your password.": "To reset your password, enter your wallet’s recovery phrase. Uniswap cannot help recover your password.", - "To scan a code, allow Camera access in system settings": "To scan a code, allow Camera access in system settings", - "To swap, buy, send, and receive tokens, you need to import this wallet’s recovery phrase.": "To swap, buy, send, and receive tokens, you need to import this wallet’s recovery phrase.", - "To use {{authenticationTypeName}}, allow access in system settings": "To use {{authenticationTypeName}}, allow access in system settings", - "To use {{authenticationTypeName}}, set up it first in settings": "To use {{authenticationTypeName}}, set up it first in settings", - "Tokens": "Tokens", - "Too many attempts. Try again in {{time}}.": "Too many attempts. Try again in {{time}}.", - "Top tokens": "Top tokens", - "Traits": "Traits", - "Transacted{{addressText}}.": "Transacted{{addressText}}.", - "Transaction confirmed": "Transaction confirmed", - "Transaction confirmed with {{dappName}}": "Transaction confirmed with {{dappName}}", - "Transaction failed with {{dappName}}": "Transaction failed with {{dappName}}", - "Transaction ID copied": "Transaction ID copied", - "Transaction in progress": "Transaction in progress", - "Transaction request from": "Transaction request from", - "Transactions": "Transactions", - "Transfer NFTs from another wallet to get started.": "Transfer NFTs from another wallet to get started.", - "Transfer tokens from a centralized exchange or another wallet to send tokens.": "Transfer tokens from a centralized exchange or another wallet to send tokens.", - "Transfer tokens from another wallet or crypto exchange.": "Transfer tokens from another wallet or crypto exchange.", - "Transfer your assets without consent": "Transfer your assets without consent", - "Translate to {{ language }}": "Translate to {{ language }}", - "Try again": "Try again", - "Try searching again": "Try searching again", - "Turkish": "Turkish", - "Turkish Lira": "Turkish Lira", - "Turn on push notifications": "Turn on push notifications", - "TVL": "TVL", - "Twitter": "Twitter", - "Type a bio for your profile": "Type a bio for your profile", - "Type or paste your 12 or 24-word recovery phrase": "Type or paste your 12 or 24-word recovery phrase", - "Type your recovery phrase": "Type your recovery phrase", - "Uh oh!": "Uh oh!", - "Ukrainian": "Ukrainian", - "Ukrainian Hryvnia": "Ukrainian Hryvnia", - "Unable to backup recovery phrase to Google Drive. Please ensure you have Google Drive enabled with available storage space and try again.": "Unable to backup recovery phrase to Google Drive. Please ensure you have Google Drive enabled with available storage space and try again.", - "Unable to backup recovery phrase to iCloud. Please ensure you have iCloud enabled with available storage space and try again.": "Unable to backup recovery phrase to iCloud. Please ensure you have iCloud enabled with available storage space and try again.", - "Unable to cancel transaction": "Unable to cancel transaction", - "Unable to claim username": "Unable to claim username", - "Unable to delete backup": "Unable to delete backup", - "Unable to replace transaction": "Unable to replace transaction", - "Unhide NFT": "Unhide NFT", - "Unhide Token": "Unhide Token", - "Uniswap defaults to your device‘s language settings. To change your preferred language, go to “Uniswap” in your device settings and tap on “Language”": "Uniswap defaults to your device‘s language settings. To change your preferred language, go to “Uniswap” in your device settings and tap on “Language”", - "Uniswap Labs does not store your password and can’t recover it, so it’s crucial you remember it.": "Uniswap Labs does not store your password and can’t recover it, so it’s crucial you remember it.", - "Uniswap one-time code": "Uniswap one-time code", - "Uniswap Team 🦄": "Uniswap Team 🦄", - "Uniswap TVL": "Uniswap TVL", - "Uniswap volume (24H)": "Uniswap volume (24H)", - "Uniswap Wallet": "Uniswap Wallet", - "Uniswap Wallet currently supports {{ chains }}. Please only use \"{{ dappName }}\" on these chains": "Uniswap Wallet currently supports {{ chains }}. Please only use \"{{ dappName }}\" on these chains", - "Uniswap Wallet supports tokens on Ethereum, Polygon, Arbitrum, Optimism, Base, and BNB Chain. Right now, we only support NFTs on Ethereum.": "Uniswap Wallet supports tokens on Ethereum, Polygon, Arbitrum, Optimism, Base, and BNB Chain. Right now, we only support NFTs on Ethereum.", - "United States Dollar": "United States Dollar", - "Unknown": "Unknown", - "Unknown error": "Unknown error", - "unknown token": "unknown token", - "Unknown token": "Unknown token", - "Unlimited": "Unlimited", - "Unlock": "Unlock", - "unwrap": "unwrap", - "Unwrap": "Unwrap", - "Unwrap pending": "Unwrap pending", - "Unwrapped": "Unwrapped", - "Unwrapped {{inputAssetInfo}} and received {{outputAssetInfo}}.": "Unwrapped {{inputAssetInfo}} and received {{outputAssetInfo}}.", - "Unwrapping": "Unwrapping", - "Update app": "Update app", - "Update the app to continue": "Update the app to continue", - "Upload your own or stick with your unique Unicon. You can always change this later.": "Upload your own or stick with your unique Unicon. You can always change this later.", - "Urdu": "Urdu", - "Use your new password to unlock your wallet.": "Use your new password to unlock your wallet.", - "username": "username", - "Username changed": "Username changed", - "Username deleted": "Username deleted", - "Usernames can only contain lowercase letters and numbers": "Usernames can only contain lowercase letters and numbers", - "Usernames cannot be more than {{ maxUnitagLength }} characters": "Usernames cannot be more than {{ maxUnitagLength }} characters", - "Usernames must be at least {{ minUnitagLength }} characters": "Usernames must be at least {{ minUnitagLength }} characters", - "Usernames transform complex 0x addresses into readable names. By claiming a {{unitagSuffix}} username, you can easily send and receive crypto and build out a public web3 profile.": "Usernames transform complex 0x addresses into readable names. By claiming a {{unitagSuffix}} username, you can easily send and receive crypto and build out a public web3 profile.", - "UwU Link error": "UwU Link error", - "Version {{appVersion}}": "Version {{appVersion}}", - "Vietnamese": "Vietnamese", - "Vietnamese Dong": "Vietnamese Dong", - "View": "View", - "View {{ tokenSymbol }}": "View {{ tokenSymbol }}", - "View all": "View all", - "View less": "View less", - "View on {{ blockExplorerName }}": "View on {{ blockExplorerName }}", - "View on MoonPay": "View on MoonPay", - "View only wallets": "View only wallets", - "View recovery phrase": "View recovery phrase", - "View this in a private place": "View this in a private place", - "View this in private": "View this in private", - "View this in private and <1>do not share it with anyone": "View this in private and <1>do not share it with anyone", - "View transaction": "View transaction", - "View your token balances": "View your token balances", - "View your wallet address": "View your wallet address", - "View-only": "View-only", - "Volume": "Volume", - "Wallet": "Wallet", - "Wallet {{ number }}": "Wallet {{ number }}", - "Wallet preferences": "Wallet preferences", - "Wallet restored!": "Wallet restored!", - "Wallet settings": "Wallet settings", - "WalletConnect Error": "WalletConnect Error", - "WalletConnect v1 is no longer supported. The application you’re trying to connect to needs to upgrade to WalletConnect v2.": "WalletConnect v1 is no longer supported. The application you’re trying to connect to needs to upgrade to WalletConnect v2.", - "Wallets": "Wallets", - "Warning": "Warning", - "Watch a wallet": "Watch a wallet", - "We recommend using <2>both types of backups, because if you lose your recovery phrase, you won’t be able to restore your wallet.": "We recommend using <2>both types of backups, because if you lose your recovery phrase, you won’t be able to restore your wallet.", - "We use anonymous usage data to enhance your experience across Uniswap Labs products. When disabled, we only track errors and essential usage.": "We use anonymous usage data to enhance your experience across Uniswap Labs products. When disabled, we only track errors and essential usage.", - "We’ll notify you once your transaction is complete.": "We’ll notify you once your transaction is complete.", - "Weak": "Weak", - "Weak password": "Weak password", - "Website": "Website", - "Welcome to <1> Uniswap Wallet": "Welcome to <1> Uniswap Wallet", - "Welcome to your new wallet": "Welcome to your new wallet", - "What is a recovery phrase?": "What is a recovery phrase?", - "What’s a recovery phrase?": "What’s a recovery phrase?", - "What’s a signature request?": "What’s a signature request?", - "What’s the <1>{numberWithOrdinal} word in your recovery phrase?": "What’s the <1>{numberWithOrdinal} word in your recovery phrase?", - "When this wallet buys or receives NFTs, they’ll appear here.": "When this wallet buys or receives NFTs, they’ll appear here.", - "When this wallet buys or receives tokens, they’ll appear here.": "When this wallet buys or receives tokens, they’ll appear here.", - "When this wallet makes transactions, they’ll appear here.": "When this wallet makes transactions, they’ll appear here.", - "When you approve, trade, or transfer tokens or NFTs, your transactions will appear here.": "When you approve, trade, or transfer tokens or NFTs, your transactions will appear here.", - "When your favorited wallets makes transactions, they’ll appear here.": "When your favorited wallets makes transactions, they’ll appear here.", - "Why is there an additional fee?": "Why is there an additional fee?", - "With swap protection on, your Ethereum transactions will be protected from sandwich attacks, with reduced chances of failure.": "With swap protection on, your Ethereum transactions will be protected from sandwich attacks, with reduced chances of failure.", - "wrap": "wrap", - "Wrap": "Wrap", - "Wrap pending": "Wrap pending", - "Wrapped": "Wrapped", - "Wrapped {{inputAssetInfo}} and received {{outputAssetInfo}}.": "Wrapped {{inputAssetInfo}} and received {{outputAssetInfo}}.", - "Wrapping": "Wrapping", - "Write down your recovery phrase in order": "Write down your recovery phrase in order", - "Write it down and keep it in a safe place": "Write it down and keep it in a safe place", - "Write your recovery phrase down and store it in a safe location": "Write your recovery phrase down and store it in a safe location", - "Wrong password": "Wrong password", - "Wrong password, try again": "Wrong password, try again", - "Wrong password. Try again": "Wrong password. Try again", - "Wrong recovery phrase": "Wrong recovery phrase", - "Yes, continue": "Yes, continue", - "You already have made the maximum number of changes to your username for this address": "You already have made the maximum number of changes to your username for this address", - "You are in offline mode": "You are in offline mode", - "You can <1>enter your recovery phrase on a new device <4>to restore your wallet and its contents.": "You can <1>enter your recovery phrase on a new device <4>to restore your wallet and its contents.", - "You can also manually back up your recovery phrase by <2>writing it down and storing it in a safe place.": "You can also manually back up your recovery phrase by <2>writing it down and storing it in a safe place.", - "You can always add back view-only wallets by entering the wallet’s address.": "You can always add back view-only wallets by entering the wallet’s address.", - "You can check this in settings at any time.": "You can check this in settings at any time.", - "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.": "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.", - "You can send tokens on all of our supported networks to this address.": "You can send tokens on all of our supported networks to this address.", - "You can’t trade this token using the Uniswap Wallet.": "You can’t trade this token using the Uniswap Wallet.", - "You don’t have enough {{ nativeCurrency }} to cover the network cost": "You don’t have enough {{ nativeCurrency }} to cover the network cost", - "You don’t have enough {{ symbol }}": "You don’t have enough {{ symbol }}", - "You don’t have enough {{symbol}} to complete this transaction.": "You don’t have enough {{symbol}} to complete this transaction.", - "You got it!": "You got it!", - "You have hit the maximum number of usernames that can be active for this device": "You have hit the maximum number of usernames that can be active for this device", - "You haven’t backed up your recovery phrase to Google Drive yet. By doing so, you can recover your wallet just by being logged into Google Drive on any device.": "You haven’t backed up your recovery phrase to Google Drive yet. By doing so, you can recover your wallet just by being logged into Google Drive on any device.", - "You haven’t backed up your recovery phrase to iCloud yet. By doing so, you can recover your wallet just by being logged into iCloud on any device.": "You haven’t backed up your recovery phrase to iCloud yet. By doing so, you can recover your wallet just by being logged into iCloud on any device.", - "You haven’t transacted with this address before. Please confirm that the address is correct before continuing.": "You haven’t transacted with this address before. Please confirm that the address is correct before continuing.", - "You may have lost connection or the network may be down. If the problem persists, please try again later.": "You may have lost connection or the network may be down. If the problem persists, please try again later.", - "You may have lost internet connection or the network may be down. Please check your internet connection and try again.": "You may have lost internet connection or the network may be down. Please check your internet connection and try again.", - "You need to import this wallet via recovery phrase to send assets.": "You need to import this wallet via recovery phrase to send assets.", - "You need to import this wallet via recovery phrase to swap tokens.": "You need to import this wallet via recovery phrase to swap tokens.", - "You sent {{ currencyAmount }}{{ tokenName }}{{ fiatValue }} to {{ recipient }}.": "You sent {{ currencyAmount }}{{ tokenName }}{{ fiatValue }} to {{ recipient }}.", - "You’ll need ETH to get started. Buy with a card or bank.": "You’ll need ETH to get started. Buy with a card or bank.", - "You’ll need this to unlock your wallet and access your recovery phrase": "You’ll need this to unlock your wallet and access your recovery phrase", - "You’ll need to enter this password to recover your account. It’s not stored anywhere, so it can’t be recovered by anyone else.": "You’ll need to enter this password to recover your account. It’s not stored anywhere, so it can’t be recovered by anyone else.", - "You’ll need to enter this password to recover your wallet.": "You’ll need to enter this password to recover your wallet.", - "You’re about to change your username. Once you change it, you can never claim it again.": "You’re about to change your username. Once you change it, you can never claim it again.", - "You’re about to delete your username and customizable profile details. You will not be able to reclaim it.": "You’re about to delete your username and customizable profile details. You will not be able to reclaim it.", - "You’re about to send tokens to a special type of address—a smart contract. Double-check it’s the address you intended to send to. If it’s wrong, your tokens could be lost forever.": "You’re about to send tokens to a special type of address—a smart contract. Double-check it’s the address you intended to send to. If it’s wrong, your tokens could be lost forever.", - "You’re offline": "You’re offline", - "You’re removing your recovery phrase": "You’re removing your recovery phrase", - "You’re swapping": "You’re swapping", - "You’ve already completed onboarding": "You’ve already completed onboarding", - "You’ve reached the maximum number of 2 usernames changes.": "You’ve reached the maximum number of 2 usernames changes.", - "Your {{ symbol }} balance has decreased since you entered the amount you’d like to send": "Your {{ symbol }} balance has decreased since you entered the amount you’d like to send", - "Your balance": "Your balance", - "Your connection timed out": "Your connection timed out", - "Your other wallets": "Your other wallets", - "Your personal space for tokens, NFTs, and all your trades.": "Your personal space for tokens, NFTs, and all your trades.", - "Your public address will be": "Your public address will be", - "Your recovery phrase is what grants you (and anyone who has it) access to your funds. Be sure to keep it to yourself.": "Your recovery phrase is what grants you (and anyone who has it) access to your funds. Be sure to keep it to yourself.", - "Your recovery phrase will only be stored locally on your device.": "Your recovery phrase will only be stored locally on your device.", - "Your tokens": "Your tokens", - "Your transaction will revert if the price changes more than the slippage percentage.": "Your transaction will revert if the price changes more than the slippage percentage.", - "Your wallet is ready! Start by funding your wallet by buying or transferring crypto to your wallet.": "Your wallet is ready! Start by funding your wallet by buying or transferring crypto to your wallet.", - "Your wallet isn’t connected to this site.": "Your wallet isn’t connected to this site.", - "Your wallets": "Your wallets", - "Your wallets will appear below.": "Your wallets will appear below.", - "yourname": "yourname" + "account": { + "cloud": { + "backup": { + "subtitle": "There are multiple recovery phrases backed up to your {{ cloudProviderName }}.", + "title": "Select a backup to restore" + }, + "button": { + "restore": { + "android": "Restore from Google Drive", + "ios": "Restore from iCloud" + } + }, + "empty": { + "description": "It looks like you haven’t backed up any of your seed phrases to {{ cloudProviderName }}.", + "title": "0 backups found" + }, + "error": { + "backup": { + "message": "Failed to import backups due to lack of permissions, interruption of authorization, or due to a cloud error", + "title": "Error while importing backups" + }, + "password": { + "title": "Invalid password. Please try again." + }, + "unavailable": { + "button": { + "cancel": "Not now", + "settings": "Go to settings" + }, + "message": { + "android": "Please verify that you are logged in to a Google account with Google Drive enabled on this device and try again.", + "ios": "Please verify that you are logged in to an Apple ID with iCloud Drive enabled on this device and try again." + }, + "title": { + "android": "Google Drive not available", + "ios": "iCloud Drive not available" + } + } + }, + "loading": { + "title": "Searching for backups..." + }, + "lockout": { + "time": { + "hours_one": "Too many attempts. Try again in 1 hour.", + "hours_other": "Too many attempts. Try again in {{ count }} hours.", + "minutes_one": "Too many attempts. Try again in 1 minute.", + "minutes_other": "Too many attempts. Try again in {{ count }} minutes." + } + }, + "password": { + "input": "Enter password", + "recoveryPhrase": "Enter your recovery phrase instead", + "subtitle": "This password is required to recover your recovery phrase backup from {{ cloudProviderName }}.", + "title": "Enter backup password" + } + }, + "seedPhrase": { + "education": { + "part1": "A recovery phrase (or seed phrase) is a <2>set of words required to access your wallet, <4>like a password.", + "part2": "You can <1>enter your recovery phrase on a new device <4>to restore your wallet and its contents.", + "part3": "But, if you <2>lose your recovery phrase, you’ll <5>lose access to your wallet.", + "part4": "Instead of memorizing your recovery phrase, you can <2>back it up to {{ cloudProviderName }} and protect it with a password.", + "part5": "You can also manually back up your recovery phrase by <2>writing it down and storing it in a safe place.", + "part6": "We recommend using <2>both types of backups, because if you lose your recovery phrase, you won’t be able to restore your wallet." + }, + "error": { + "invalid": "Invalid phrase", + "invalidWord": "Invalid word: {{ word }}", + "phraseLength": "Recovery phrase must be 12-24 words", + "wrong": "Wrong recovery phrase" + }, + "helpText": { + "import": "How do I find my recovery phrase?", + "restoring": "Try searching again" + }, + "input": "Type your recovery phrase", + "remove": { + "final": { + "description": "Make sure you’ve written down your recovery phrase or backed it up on {{ cloudProviderName }}. <5>You will not be able to access your funds otherwise.", + "title": "You’re removing your <2>recovery phrase" + }, + "import": { + "description": "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." + }, + "initial": { + "description": "It shares the same recovery phrase as <2>{{ walletNames }}. Your recovery phrase will remain stored until you delete all remaining wallets.", + "title": "You’re removing <1>{{ walletName }}" + }, + "mnemonic": { + "description": "It shares the same recovery phrase as <2>{{ walletNames }}. Your recovery phrase will remain stored until you delete all remaining wallets." + } + }, + "subtitle": { + "import": "Your recovery phrase will only be stored locally on your device.", + "restoring": "Enter your recovery phrase below, or try searching for backups again." + }, + "title": { + "import": "Enter your recovery phrase", + "restoring": "No backups found" + } + }, + "wallet": { + "action": { + "copy": "Copy wallet address", + "report": "Report profile", + "settings": "Wallet settings", + "viewExplorer": "View on {{ blockExplorerName }}" + }, + "button": { + "add": "Add wallet", + "addViewOnly": "Add a view-only wallet", + "create": "Create a new wallet", + "import": "Import a new wallet", + "manage": "Manage wallet", + "remove": "Remove wallet", + "restore": "Restore wallet", + "watch": "Watch a wallet" + }, + "header": { + "other": "Your other wallets", + "viewOnly": "View only wallets" + }, + "remove": { + "check": "I backed up my recovery phrase and understand that Uniswap Labs can’t help me recover my wallets if I failed to do so.", + "viewOnly": "You can always add back view-only wallets by entering the wallet’s address." + }, + "restore": { + "description": "Because you’re on a new device, you’ll need to restore your recovery phrase. This will allow you to swap and send tokens." + }, + "select": { + "error": "Couldn’t load addresses", + "loading": { + "subtitle": "Your wallets will appear below.", + "title": "Searching for wallets" + }, + "title_one_one": "One wallet found", + "title_one_other": "Select wallets to import" + }, + "title": "Your wallets", + "viewOnly": { + "button": "Import wallet", + "description": "To swap, buy, send, and receive tokens, you need to import this wallet’s recovery phrase.", + "title": "This wallet is view-only" + }, + "watch": { + "error": { + "alreadyImported": "This address is already imported", + "notFound": "Address not found", + "smartContract": "Address is a smart contract" + }, + "message": "Adding a view-only wallet allows you to try out the app or track a wallet. You will not be able to swap or send funds.", + "placeholder": "ENS or address", + "title": "Enter a wallet address" + } + } + }, + "common": { + "app": "Uniswap Wallet", + "button": { + "accept": "Accept", + "back": "Back", + "buy": "Buy", + "cancel": "Cancel", + "close": "Close", + "confirm": "Confirm", + "connect": "Connect", + "continue": "Continue", + "copied": "Copied", + "copy": "Copy", + "delete": "Delete", + "disconnect": "Disconnect", + "dismiss": "Dismiss", + "done": "Done", + "enable": "Enable", + "hide": "Hide", + "later": "Maybe later", + "learn": "Learn more", + "next": "Next", + "notNow": "Not now", + "ok": "OK", + "paste": "Paste", + "receive": "Receive", + "remove": "Remove", + "restore": "Restore", + "retry": "Retry", + "review": "Review", + "save": "Save", + "sell": "Sell", + "send": "Send", + "setup": "Set up", + "share": "Share", + "show": "Show", + "sign": "Sign", + "skip": "Skip", + "swap": "Swap", + "tryAgain": "Try again", + "understand": "I understand", + "view": "View" + }, + "card": { + "error": { + "description": "Something went wrong", + "title": "Oops! Something went wrong." + } + }, + "error": { + "general": "Something went wrong." + }, + "input": { + "password": { + "confirm": "Confirm password", + "error": { + "mismatch": "Passwords don’t match" + }, + "new": "New password", + "placeholder": "Password", + "strength": { + "medium": "Medium", + "strong": "Strong", + "weak": "Weak" + } + }, + "search": "Search" + }, + "longText": { + "button": { + "less": "Read less", + "more": "Read more" + } + }, + "navigation": { + "settings": "Settings", + "systemSettings": "Settings" + }, + "text": { + "error": "Error", + "loading": "Loading", + "notAvailable": "N/A", + "unknown": "Unknown" + } + }, + "currency": { + "aud": "Australian Dollar", + "brl": "Brazilian Real", + "cad": "Canadian Dollar", + "cny": "Chinese Yuan", + "eur": "Euro", + "gbp": "British Pound", + "hkd": "Hong Kong Dollar", + "idr": "Indonesian Rupiah", + "inr": "Indian Rupee", + "jpy": "Japanese Yen", + "ngn": "Nigerian Naira", + "pkr": "Pakistani Rupee", + "rub": "Russian Ruble", + "sgd": "Singapore Dollar", + "thb": "Thai Baht", + "try": "Turkish Lira", + "uah": "Ukrainian Hryvnia", + "usd": "United States Dollar", + "vnd": "Vietnamese Dong" + }, + "dapp": { + "request": { + "approve": { + "label": "Wallet" + }, + "error": { + "none": "No approvals pending" + }, + "signature": { + "education": { + "description": "A signature is required to prove that you own the wallet without exposing your private keys", + "title": "What’s a signature request?" + } + }, + "warning": { + "notActive": { + "message": "Make sure it’s the right one", + "title": "This is not your active wallet" + } + } + } + }, + "errors": { + "crash": { + "message": "Something crashed.", + "restart": "Restart app", + "title": "Uh oh!" + } + }, + "explore": { + "search": { + "action": { + "clear": "Clear all", + "viewEtherscan": "View on {{ blockExplorerName }}" + }, + "empty": { + "full": "No results found for <1>\"{{searchQuery}}\"" + }, + "error": "Couldn’t load search results", + "label": { + "ownedBy": "Owned by {{ ownerAddress }}" + }, + "placeholder": "Search tokens and wallets", + "section": { + "nft": "NFT Collections", + "popularNFT": "Popular NFT collections", + "popularTokens": "Popular tokens", + "recent": "Recent searches", + "suggestedWallets": "Suggested wallets", + "tokens": "Tokens", + "wallets": "Wallets" + } + }, + "tokens": { + "error": "Couldn’t load tokens", + "favorite": { + "action": { + "add": "Favorite token", + "edit": "Edit favorites", + "remove": "Remove favorite" + }, + "title": { + "default": "Favorite tokens", + "edit": "Edit favorite tokens" + } + }, + "metadata": { + "marketCap": "{{ number }} MCap", + "totalValueLocked": "{{ number }} TVL", + "volume": "{{ number }} Vol" + }, + "sort": { + "label": { + "marketCap": "Market cap", + "priceDecrease": "Price decrease", + "priceIncrease": "Price increase", + "totalValueLocked": "TVL", + "volume": "Volume" + }, + "option": { + "marketCap": "Market cap", + "priceDecrease": "Price decrease (24H)", + "priceIncrease": "Price increase (24H)", + "totalValueLocked": "Uniswap TVL", + "volume": "Uniswap volume (24H)" + } + }, + "top": { + "title": "Top tokens" + } + }, + "wallets": { + "favorite": { + "action": { + "add": "Favorite wallet", + "edit": "Edit favorites", + "remove": "Remove favorite" + }, + "title": { + "default": "Favorite wallets", + "edit": "Edit favorite wallets" + } + } + } + }, + "extension": { + "connection": { + "popup": "Your wallet isn’t connected to this site." + }, + "lock": { + "button": { + "forgot": "Forgot password?", + "help": "Get help", + "seedPhrase": "Enter recovery phrase", + "submit": "Unlock" + }, + "password": { + "error": "Wrong password. Try again", + "reset": { + "description": { + "default": "To reset your password, enter your wallet’s recovery phrase. Uniswap cannot help recover your password.", + "inProgress": "Follow the instructions on the browser web page to reset your password" + }, + "title": "Forgot password?" + } + }, + "subtitle": "Enter your password to unlock", + "title": "Welcome back" + }, + "settings": { + "password": { + "enter": { + "title": "Enter your current password" + }, + "error": { + "wrong": "Wrong password" + }, + "placeholder": "Current password" + } + } + }, + "fiatOnRamp": { + "banner": { + "subtitle": "Get tokens at the best prices in web3 with Uniswap Wallet.", + "title": "Buy crypto" + }, + "button": { + "chooseToken": "Choose a token", + "continueCheckout": "Continue to checkout" + }, + "checkout": { + "button": "Checkout", + "title": "Checkout with" + }, + "connection": { + "message": "Connecting you to {{ serviceProvider }}", + "quote": "Buying {{ amount }} worth of {{ currencySymbol }}" + }, + "error": { + "default": "Something went wrong.", + "load": "Couldn’t load tokens to buy", + "max": "Maximum {{ amount }}", + "min": "Minimum {{ amount }}", + "unsupported": "Not supported in region", + "usd": "Only available to purchase in USD" + }, + "quote": { + "amount": "Receive {{ tokenAmount }}", + "amountAfterFees": "{{ tokenAmount }} after fees", + "type": { + "best": "Best overall", + "other": "Other options", + "recent": "Recently used" + } + }, + "region": { + "placeholder": "Search by country or region", + "title": "Select your region" + }, + "summary": { + "total": "{{ cryptoAmount }} for {{ fiatAmount }}" + } + }, + "forceUpgrade": { + "action": { + "confirm": "Update app", + "seedPhrase": "View recovery phrase" + }, + "description": "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.", + "label": { + "seedPhrase": "Recovery phrase" + }, + "title": "Update the app to continue" + }, + "home": { + "activity": { + "empty": { + "button": "Receive tokens or NFTs", + "description": { + "default": "When you approve, trade, or transfer tokens or NFTs, your transactions will appear here.", + "external": "When this wallet makes transactions, they’ll appear here." + }, + "title": "No activity yet" + }, + "error": { + "load": "Couldn’t load activity" + }, + "title": "Activity" + }, + "banner": { + "offline": "You are in offline mode" + }, + "extension": { + "error": "Error loading accounts", + "pin": "Pin Uniswap Wallet to your browser toolbar by clicking on the <1><0>{{ icon }}" + }, + "feed": { + "empty": { + "description": "When your favorited wallets makes transactions, they’ll appear here.", + "title": "No activity yet" + }, + "error": "Couldn’t load activity", + "title": "Feed" + }, + "label": { + "buy": "Buy", + "receive": "Receive", + "scan": "Scan", + "send": "Send", + "swap": "Swap" + }, + "nfts": { + "title": "NFTs" + }, + "tokens": { + "empty": { + "action": { + "buy": { + "description": "You’ll need ETH to get started. Buy with a card or bank.", + "title": "Buy crypto" + }, + "import": { + "description": "Enter this wallet’s recovery phrase to begin swapping and sending.", + "title": "Import wallet" + }, + "receive": { + "description": "Transfer tokens from another wallet or crypto exchange.", + "title": "Receive funds" + } + }, + "description": "When this wallet buys or receives tokens, they’ll appear here.", + "title": "No tokens yet" + }, + "error": { + "fetch": "Failed to fetch token balances", + "load": "Couldn’t load token balances" + }, + "title": "Tokens" + }, + "upsell": { + "receive": { + "cta": "Link an account", + "description": "Fund your wallet by transferring crypto from another wallet or account", + "title": "Receive crypto" + } + }, + "warning": { + "viewOnly": "This is a view-only wallet" + } + }, + "language": { + "chineseSimplified": "Chinese, Simplified", + "chineseTraditional": "Chinese, Traditional", + "dutch": "Dutch", + "english": "English", + "french": "French", + "hindi": "Hindi", + "indonesian": "Indonesian", + "japanese": "Japanese", + "malay": "Malay", + "portuguese": "Portuguese", + "russian": "Russian", + "spanishLatam": "Spanish (Latin America)", + "spanishSpain": "Spanish (Spain)", + "spanishUs": "Spanish (US)", + "thai": "Thai", + "turkish": "Turkish", + "ukrainian": "Ukrainian", + "urdu": "Urdu", + "vietnamese": "Vietnamese" + }, + "notification": { + "assetVisibility": { + "hidden": "{{ assetName }} hidden", + "unhidden": "{{ assetName }} unhidden" + }, + "copied": { + "address": "Address copied", + "contractAddress": "Contract address copied", + "failed": "Failed to copy to clipboard", + "image": "Image copied", + "transactionId": "Transaction ID copied" + }, + "countryChange": "Switched to {{ countryName }}", + "restore": { + "success": "Wallet restored!" + }, + "swap": { + "network": "Swapping on {{ network }}", + "pending": { + "swap": "Swap pending", + "unwrap": "Unwrap pending", + "wrap": "Wrap pending" + } + }, + "transaction": { + "approve": { + "canceled": "Canceled {{ currencySymbol }} approve.", + "fail": "Failed to approve {{ currencySymbol }} for use with {{ address }}.", + "success": "Approved {{ currencySymbol }} for use with {{ address }}." + }, + "swap": { + "canceled": "Canceled {{ inputCurrencySymbol }}-{{ outputCurrencySymbol }} swap.", + "fail": "Failed to swap {{ inputCurrencyAmountWithSymbol }} for {{ outputCurrencyAmountWithSymbol }}.", + "success": "Swapped {{ inputCurrencyAmountWithSymbol }} for {{ outputCurrencyAmountWithSymbol }}." + }, + "transfer": { + "canceled": "Canceled {{ tokenNameOrAddress }} send.", + "fail": "Failed to send {{ tokenNameOrAddress }} to {{ walletNameOrAddress }}.", + "received": "Received {{ tokenNameOrAddress }} from {{ walletNameOrAddress }}.", + "success": "Sent {{ tokenNameOrAddress }} to {{ walletNameOrAddress }}." + }, + "unknown": { + "fail": { + "full": "Failed to transact with {{ addressOrEnsName }}", + "short": "Failed to transact" + }, + "success": { + "full": "Transacted with {{ addressOrEnsName }}", + "short": "Transacted" + } + }, + "unwrap": { + "canceled": "Canceled {{ inputCurrencySymbol }} unwrap.", + "fail": "Failed to unwrap {{ inputCurrencyAmountWithSymbol }}.", + "success": "Unwrapped {{ inputCurrencyAmountWithSymbol }} and received {{ outputCurrencyAmountWithSymbol}}." + }, + "wrap": { + "canceled": "Canceled {{ inputCurrencySymbol }} wrap.", + "fail": "Failed to wrap {{ inputCurrencyAmountWithSymbol }}.", + "success": "Wrapped {{ inputCurrencyAmountWithSymbol }} and received {{ outputCurrencyAmountWithSymbol}}." + } + }, + "transfer": { + "pending": "{{ currencySymbol }} transfer pending" + }, + "walletConnect": { + "confirmed": "Transaction confirmed with {{ dappName }}", + "connected": "Connected", + "disconnected": "Disconnected", + "failed": "Transaction failed with {{ dappName }}", + "networkChanged": { + "full": "Switched to {{ networkName }}", + "short": "Switched networks" + } + } + }, + "notifications": { + "scantastic": { + "subtitle": "Continue on Uniswap Extension", + "title": "Success" + } + }, + "onboarding": { + "backup": { + "manual": { + "placeholder": "Secret word", + "progress": "{{ completedStepsCount }}/{{ totalStepsCount }} completed", + "subtitle_one": "What’s the <1>{{count}}st word in your recovery phrase?", + "subtitle_two": "What’s the <1>{{count}}nd word in your recovery phrase?", + "subtitle_few": "What’s the <1>{{count}}rd word in your recovery phrase?", + "subtitle_other": "What’s the <1>{{count}}th word in your recovery phrase?", + "title": "Let’s make sure you’ve recorded it correctly" + }, + "option": { + "cloud": { + "description": "Encrypt your recovery phrase with a secure password", + "title": "{{ cloudProviderName }} backup" + }, + "manual": { + "description": "Write your recovery phrase down and store it in a safe location", + "title": "Manual backup" + } + }, + "subtitle": "Backups let you restore your wallet if you delete the app or lose your device", + "title": { + "existing": "Back up your wallet", + "new": "Choose a backup method" + }, + "view": { + "disclaimer": "I understand that if I lose my recovery phrase, Uniswap Labs cannot help me restore it", + "subtitle": { + "write": "Read the following carefully before continuing" + }, + "title": "Save your recovery phrase", + "warning": { + "message1": "This recovery phrase gives you full access to your wallet and funds", + "message2": "Write it down and keep it in a safe place", + "message3": "View this in private and <1>do not share it with anyone" + } + } + }, + "cloud": { + "confirm": { + "description": "You’ll need to enter this password to recover your account. It’s not stored anywhere, so it can’t be recovered by anyone else.", + "title": "Confirm your backup password" + }, + "createPassword": { + "description": "You’ll need to enter this password to recover your wallet.", + "title": "Create your backup password" + } + }, + "complete": { + "card": { + "buy": { + "description": "Purchase with a card, or transfer from an exchange", + "title": "Buy or transfer crypto" + }, + "explore": { + "description": "Search and browse trending tokens and NFTs", + "title": "Explore tokens & NFTs" + }, + "swap": { + "description": "Purchase with a card, or transfer from an exchange", + "title": "Start swapping" + } + }, + "description": "Your wallet is ready! Start by funding your wallet by buying or transferring crypto to your wallet.", + "footer": "Learn how to use the Uniswap Wallet", + "pin": "<0>Pin the extension to your browser window<1><0>by clicking on the<1><2>icon, and then the pin" + }, + "editName": { + "button": { + "create": "Create wallet" + }, + "label": "Nickname", + "subtitle": "Give your wallet a nickname", + "title": "This nickname is only visible to you", + "walletAddress": "Your public address will be <1>{{ walletAddress }}" + }, + "extension": { + "connectMobile": { + "button": "Import from your phone", + "title": "Have the Uniswap app?" + }, + "password": { + "subtitle": "You’ll need this to unlock your wallet and access your recovery phrase", + "title": { + "default": "Create a password", + "reset": "Reset your password" + } + } + }, + "import": { + "error": { + "invalidWords_one": "1 word is invalid or misspelled", + "invalidWords_other": "{{ count }} words are invalid or misspelled" + }, + "method": { + "import": { + "message": "Enter your recovery phrase from another crypto wallet", + "title": "Import a wallet" + }, + "restore": { + "message": { + "android": "Add wallets you’ve backed up to your Google Drive account", + "ios": "Add wallets you’ve backed up to your iCloud account" + }, + "title": "Restore a wallet" + } + }, + "title": "Choose how you want to add your wallet" + }, + "importMnemonic": { + "button": { + "default": "My recovery phrase is 12 words", + "longPhrase": "My recovery phrase is longer than 12 words" + }, + "error": { + "invalidPhrase": "The phrase you entered is invalid" + }, + "subtitle": "Type or paste your 12 or 24-word recovery phrase", + "title": "Enter your recovery phrase" + }, + "intro": { + "alreadyComplete": { + "subtitle": "To create more wallets, open the account switcher inside the extension popup, or reinstall the extension to start over", + "title": "You’ve already completed onboarding" + }, + "button": { + "alreadyHave": "I already have a wallet" + }, + "title": "Welcome to <1> Uniswap Wallet" + }, + "landing": { + "button": { + "add": "Add an existing wallet", + "create": "Create a new wallet" + } + }, + "notification": { + "permission": { + "message": "To receive notifications, turn on notifications for Uniswap Wallet in your device’s settings.", + "title": "Notifications permission" + }, + "subtitle": "Get notified when your transfers, swaps, and approvals complete.", + "title": "Turn on push notifications" + }, + "resetPassword": { + "complete": { + "safety": "Learn more about wallet safety", + "subtitle": "Use your new password to unlock your wallet.", + "title": "Password reset" + } + }, + "scan": { + "button": "Scan with Uniswap app", + "error": "Sorry, we are unable to load the QR code right now. Please try another onboarding method.", + "otp": { + "error": "The code you submitted is incorrect, or there was an error submitting. Please try again.", + "failed": "Failed attempts: {{ count }}", + "subtitle": "Check your Uniswap mobile app for the 6-character code", + "title": "Enter one-time code" + }, + "subtitle": "Scan the QR code with the Uniswap app to import your wallet", + "title": "Sync from your phone" + }, + "security": { + "alert": { + "biometrics": { + "message": { + "android": "To use biometrics, set up it first in settings", + "ios": "To use {{ biometricsMethod }}, allow access in system settings" + }, + "title": { + "android": "Biometrics is disabled", + "ios": "{{ biometricsMethod }} is disabled" + } + } + }, + "button": { + "confirm": { + "android": "Enable biometrics", + "ios": "Enable {{ biometricsMethod }}" + }, + "setup": "Set up" + }, + "subtitle": { + "android": "Add an extra layer of security by requiring biometrics to send transactions.", + "ios": "Add an extra layer of security by requiring {{ biometricsMethod }} to send transactions." + }, + "title": "Protect your wallet" + }, + "seedPhrase": { + "confirm": { + "subtitle": { + "combined": "Confirm your recovery phrase. Select the missing words in order.", + "default": "Select the missing words in order." + }, + "title": "Confirm your recovery phrase" + }, + "view": { + "subtitle": "You can check this in settings at any time.", + "title": "Write down your recovery phrase in order" + }, + "warning": { + "final": { + "button": "I’m ready", + "message": "Your recovery phrase is what grants you (and anyone who has it) access to your funds. Be sure to keep it to yourself.", + "title": "Do this step in a private place" + }, + "screenshot": { + "message": "Anyone who gains access to your photos can access your wallet. We recommend that you write down your words instead.", + "title": "Screenshots aren’t secure" + } + } + }, + "selectWallets": { + "error": "Couldn’t load addresses", + "title": { + "default": "Choose wallets to import", + "error": "Error importing wallets" + } + }, + "termsOfService": "By continuing, I agree to the <2><0> Terms of Service and consent to the <5><0> Privacy Policy .", + "tooltip": { + "recoveryPhrase": { + "trigger": "What’s a recovery phrase?" + } + }, + "wallet": { + "continue": "Let’s keep it safe", + "defaultName": "Wallet {{ number }}", + "description": { + "existing": "Check out your tokens and NFTs, follow crypto wallets, and stay up to date on the go.", + "full": "This is your personal space for tokens, NFTs, and all of your trades. Finish setting it up to keep your funds safe.", + "new": "Your personal space for tokens, NFTs, and all your trades." + }, + "title": "Welcome to your new wallet" + } + }, + "qrScanner": { + "button": { + "connections_one": "1 app connected", + "connections_other": "{{ count }} apps connected" + }, + "error": { + "camera": { + "message": "To scan a code, allow Camera access in system settings", + "title": "Camera is disabled" + }, + "none": "No QR code found" + }, + "recipient": { + "action": { + "scan": "Scan a QR code", + "show": "Show my QR code" + }, + "error": { + "message": "Make sure that you’re scanning a valid Ethereum address QR code before trying again.", + "title": "Invalid QR Code" + }, + "input": { + "placeholder": "Search ENS or address" + }, + "label": { + "send": "Send" + }, + "results": { + "empty": "No results found", + "error": "The address you typed either does not exist or is spelled incorrectly." + } + }, + "request": { + "message": { + "unavailable": "No message found." + }, + "method": { + "default": "Request from <1>{{ dappNameOrUrl }}", + "signature": "Signature request from <1>{{ dappNameOrUrl }}", + "transaction": "Transaction request from <1>{{ dappNameOrUrl }}" + }, + "withAmount": "Allow {{ dappName }} to use up to<3> {{ amount }} {{ currencySymbol }}?", + "withoutAmount": "Allow <1>{{ dappName }} to use your {{ currencySymbol }}?" + }, + "status": { + "connecting": "Connecting...", + "loading": "Loading..." + }, + "title": "Scan a QR code", + "wallet": { + "networks": { + "description": "Uniswap Wallet supports tokens on Ethereum, Polygon, Arbitrum, Optimism, Base, and BNB Chain. Right now, we only support NFTs on Ethereum.", + "title": "Supported Networks" + }, + "title": "You can send tokens on all of our supported networks to this address." + } + }, + "scantastic": { + "code": { + "expired": "Expired", + "subtitle": "Enter this code in the Uniswap Extension. Your recovery phrase will be safely encrypted and transferred.", + "timeRemaining": { + "shorthand": { + "hours": "New code in {{ hours }}h {{ minutes }}m {{ seconds }}s", + "minutes": "New code in {{ minutes }}m {{ seconds }}s", + "seconds": "New code in {{ seconds }}s" + } + }, + "title": "Uniswap one-time code" + }, + "confirmation": { + "button": { + "continue": "Yes, continue" + }, + "label": { + "browser": "Browser", + "device": "Device" + }, + "subtitle": "Only continue if you are syncing with the Uniswap Extension on a trusted device.", + "title": "Is this your device?" + }, + "error": { + "encryption": "Failed to prepare seed phrase.", + "noCode": "No OTP received. Please try again.", + "timeout": { + "message": "Scan the QR code on the Uniswap Extension again to continue syncing your wallet.", + "title": "Your connection timed out" + } + } + }, + "send": { + "button": { + "review": "Review transfer", + "send": "Send" + }, + "recipient": { + "previous_one": "{{ count }} previous transfer", + "previous_other": "{{ count }} previous transfers", + "section": { + "favorite": "Favorite wallets", + "recent": "Recent", + "search": "Search results", + "yours": "Your wallets" + } + }, + "review": { + "summary": { + "sending": "Sending", + "to": "To" + } + }, + "search": { + "empty": { + "subtitle": "The address you typed either does not exist or is spelled incorrectly.", + "title": "No results found" + }, + "placeholder": "Search ENS or address" + }, + "status": { + "fail": { + "description": "Keep in mind that the network fee is still charged for failed transfers." + }, + "failed": { + "title": "Send failed" + }, + "inProgress": { + "description": "We’ll notify you once your transaction is complete.", + "title": "Sending" + }, + "success": { + "description": "You sent {{ currencyAmount }}{{ tokenName }}{{ fiatValue }} to {{ recipient }}.", + "title": "Send successful!" + } + }, + "title": "Send", + "warning": { + "blocked": { + "default": "This wallet is blocked", + "modal": { + "message": "This address is blocked on Uniswap Wallet because it is associated with one or more blocked activities. If you believe this is an error, please email compliance@uniswap.org.", + "title": "Blocked address" + }, + "recipient": "Recipient wallet is blocked" + }, + "insufficientFunds": { + "message": "Your {{ currencySymbol }} balance has decreased since you entered the amount you’d like to send", + "title": "Not enough {{ currencySymbol }}." + }, + "newAddress": { + "message": "You haven’t transacted with this address before. Please confirm that the address is correct before continuing.", + "title": "New address" + }, + "restore": "Restore your wallet to send", + "smartContract": { + "message": "You’re about to send tokens to a special type of address—a smart contract. Double-check it’s the address you intended to send to. If it’s wrong, your tokens could be lost forever.", + "title": "Is this a wallet address?" + }, + "viewOnly": { + "message": "You need to import this wallet via recovery phrase to send assets.", + "title": "This wallet is view-only" + } + } + }, + "setting": { + "seedPhrase": { + "account": { + "show": "Show recovery phrase" + }, + "action": { + "hide": "Hide recovery phrase" + }, + "remove": { + "button": "Remove recovery phrase", + "confirm": { + "subtitle": "I understand that Uniswap Labs can’t help me recover my wallet if I failed to do so", + "title": "I saved my recovery phrase" + }, + "initial": { + "subtitle": "Make sure you’ve saved your recovery phrase. You will lose access to your funds otherwise", + "title": "Before you continue" + }, + "password": { + "error": "Wrong password. Try again", + "input": "Enter password" + }, + "subtitle": "Enter your password to continue", + "title": "You’re removing your recovery phrase" + }, + "view": { + "error": "Wrong password, try again", + "warning": { + "message1": "Anyone who knows your recovery phrase can access your wallet and funds", + "message2": "View this in private", + "message3": "Do not share this with anyone", + "message4": "Never enter it to any websites or apps", + "title": "Before you continue" + } + }, + "warning": { + "screenshot": { + "message": "Anyone who gains access to your photos can access your wallet. We recommend that you write down your words instead.", + "title": "Screenshots aren’t secure" + }, + "view": { + "message": "Anyone who knows your recovery phrase can access your wallet and funds.", + "title": "View this in a private place" + } + } + } + }, + "settings": { + "action": { + "feedback": "Send feedback", + "help": "Get help", + "lock": "Lock wallet", + "privacy": "Privacy policy", + "terms": "Terms of service" + }, + "footer": "Made with love, \nUniswap Team 🦄", + "screen": { + "appearance": { + "title": "Appearance" + } + }, + "section": { + "about": "About", + "preferences": "Preferences", + "security": "Security", + "support": "Support", + "wallet": { + "action": { + "hide": "Hide wallets", + "showAll_one": "Show one wallet", + "showAll_other": "Show all {{ count }} wallets" + }, + "button": { + "viewAll": "View all", + "viewLess": "View less" + }, + "title": "Wallet settings" + } + }, + "setting": { + "appearance": { + "option": { + "dark": { + "subtitle": "Always use dark mode", + "title": "Dark mode" + }, + "device": { + "subtitle": "Default to your device’s appearance", + "title": "Device settings" + }, + "light": { + "subtitle": "Always use light mode", + "title": "Light mode" + } + }, + "title": "Appearance" + }, + "backup": { + "create": { + "description": "Setting a password will encrypt your recovery phrase backup, adding an extra level of protection if your {{ cloudProviderName }} account is ever compromised.", + "title": "Back up to {{ cloudProviderName }}" + }, + "delete": { + "confirm": { + "message": "Because these wallets share a recovery phrase, it will also delete the backups for these wallets below", + "title": "Are you sure?" + }, + "warning": "If you delete your {{ cloudProviderName }} backup, you’ll only be able to recover your wallet with a manual backup of your recovery phrase. Uniswap Labs can’t recover your assets if you lose your recovery phrase." + }, + "error": { + "message": { + "full": "Unable to backup recovery phrase to {{ cloudProviderName }}. Please ensure you have {{ cloudProviderName }} enabled with available storage space and try again.", + "short": "Unable to delete backup" + }, + "title": "{{ cloudProviderName }} error" + }, + "modal": { + "description": "You haven’t backed up your recovery phrase to {{ cloudProviderName }} yet. By doing so, you can recover your wallet just by being logged into {{ cloudProviderName }} on any device.", + "title": "Back up recovery phrase to {{ cloudProviderName }}?" + }, + "password": { + "disclaimer": "Uniswap Labs does not store your password and can’t recover it, so it’s crucial you remember it.", + "error": { + "mismatch": "Passwords do not match", + "weak": "Weak password" + }, + "medium": "This is a medium password", + "placeholder": { + "confirm": "Confirm password", + "create": "Create password" + }, + "strong": "This is a strong password", + "weak": "This is a weak password" + }, + "seedPhrase": { + "label": "Recovery phrase" + }, + "selected": "{{ cloudProviderName }} backup", + "status": { + "action": { + "delete": "Delete backup" + }, + "complete": "Backed up to {{ cloudProviderName }}", + "description": "By having your recovery phrase backed up to {{ cloudProviderName }}, you can recover your wallet just by being logged into your {{ cloudProviderName }} account on any device.", + "inProgress": "Backing up to {{ cloudProviderName }}...", + "seedPhrase": { + "backed": "Backed up" + }, + "title": "{{ cloudProviderName }} backup" + } + }, + "biometrics": { + "appAccess": { + "subtitle": { + "android": "Require biometrics to open app", + "ios": "Require {{ biometricsMethod }} to open app" + }, + "title": "App access" + }, + "auth": "Please authenticate", + "off": { + "message": { + "android": "Biometrics is currently turned off for Uniswap Wallet—you can turn it on in your system settings.", + "ios": "{{ biometricsMethod }} is currently turned off for Uniswap Wallet—you can turn it on in your system settings." + }, + "title": { + "android": "Biometrics is turned off", + "ios": "{{ biometricsMethod }} is turned off" + } + }, + "title": "Biometrics", + "transactions": { + "subtitle": { + "android": "Require biometrics to transact", + "ios": "Require {{ biometricsMethod }} to transact" + }, + "title": "Transactions" + }, + "unavailable": { + "message": { + "android": "Biometrics is not setup on your device. To use biometrics, set it up first in Settings.", + "ios": "{{ biometricsMethod }} is not setup on your device. To use {{ biometricsMethod }}, set it up first in Settings." + }, + "title": { + "android": "Biometrics is not setup", + "ios": "{{ biometricsMethod }} is not setup" + } + }, + "warning": { + "message": { + "android": "If you don’t turn on {{ biometricsMethod }}, anyone who gains access to your device can open Uniswap Wallet and make transactions.", + "ios": "If you don’t turn on biometrics, anyone who gains access to your device can open Uniswap Wallet and make transactions." + }, + "title": "Are you sure?" + } + }, + "currency": { + "title": "Local currency" + }, + "helpCenter": { + "title": "Help center" + }, + "language": { + "button": { + "navigate": "Go to settings" + }, + "description": "Uniswap defaults to your device‘s language settings. To change your preferred language, go to “Uniswap” in your device settings and tap on “Language”", + "title": "Language" + }, + "password": { + "title": "Change password" + }, + "privacy": { + "analytics": { + "description": "We use anonymous usage data to enhance your experience across Uniswap Labs products. When disabled, we only track errors and essential usage.", + "title": "Allow analytics" + }, + "title": "Privacy" + }, + "seedPhrase": { + "remove": "Remove recovery phrase", + "title": "Recovery phrase", + "view": "View recovery phrase" + }, + "smallBalances": { + "title": "Hide small balances" + }, + "unknownTokens": { + "title": "Hide unknown tokens" + }, + "wallet": { + "action": { + "editLabel": "Edit label", + "editProfile": "Edit profile", + "remove": "Remove wallet" + }, + "connections": { + "title": "Manage connections" + }, + "editLabel": { + "description": "Labels are not public. They are stored locally and only visible to you.", + "disclaimer": "This nickname is only visible to you.", + "save": "Save changes", + "title": "Edit nickname" + }, + "label": "Nickname", + "notifications": { + "title": "Notifications" + }, + "preferences": { + "title": "Wallet preferences" + } + } + }, + "title": "Settings", + "version": "Version {{ appVersion }}" + }, + "swap": { + "button": { + "max": "Max", + "swap": "Swap", + "unwrap": "Unwrap", + "view": "View transaction", + "wrap": "Wrap" + }, + "details": { + "action": { + "less": "Show less", + "more": "Show more" + }, + "feeOnTransfer": "{{ tokenSymbol }} fee", + "newQuote": { + "input": "New input", + "output": "New output" + }, + "rate": "Rate", + "slippage": "Max slippage", + "uniswapFee": "Fee" + }, + "form": { + "balance": "Balance", + "header": "Swap", + "slippage": "{{ slippageTolerancePercent }} slippage", + "warning": { + "restore": "Restore your wallet to swap" + } + }, + "header": { + "viewOnly": "View-only" + }, + "hold": { + "swap": "Hold to swap", + "tip": "Tip: Hold to instant swap", + "unwrap": "Hold to unwrap", + "wrap": "Hold to wrap" + }, + "request": { + "details": { + "header": "You’re swapping" + }, + "title": { + "full": "Swap {{ inputCurrencySymbol }} → {{ outputCurrencySymbol }}", + "short": "Swap Tokens" + } + }, + "review": { + "summary": "You’re swapping" + }, + "settings": { + "protection": { + "description": "With swap protection on, your Ethereum transactions will be protected from sandwich attacks, with reduced chances of failure.", + "subtitle": { + "supported": "{{ chainName }} Network", + "unavailable": "Not available on {{ chainName }}" + }, + "title": "Swap Protection" + }, + "slippage": { + "control": { + "auto": "Auto", + "title": "Max slippage" + }, + "description": "Your transaction will revert if the price changes more than the slippage percentage.", + "input": { + "message": "If the price slips any further, your transaction will revert. Below is the minimum amount you are guaranteed to receive.", + "receive": { + "formatted": "<0>Receive at least <1>{{ amount }} {{ tokenSymbol }}", + "unformatted": "Receive at least {{ amount }} {{ tokenSymbol }}" + } + }, + "output": { + "message": "If the price slips any further, your transaction will revert. Below is the maximum amount you would need to spend.", + "spend": { + "formatted": "<0>Spend at most <1>{{ amount }} {{ tokenSymbol }}", + "unformatted": "Spend at most {{ amount }} {{ tokenSymbol }}" + } + }, + "warning": { + "max": "Enter a value less than {{ maxSlippageTolerance }}", + "message": "Slippage may be higher than necessary", + "min": "Enter a value larger than 0" + } + }, + "title": "Swap Settings" + }, + "slippage": { + "settings": { + "title": "Slippage Settings" + } + }, + "warning": { + "expectedFailure": "This transaction is expected to fail", + "feeOnTransfer": { + "message": "Some tokens take a fee when they are bought or sold, which is set by the token issuer. Uniswap does not receive any share of these fees.", + "title": "Why is there an additional fee?" + }, + "insufficientBalance": { + "button": "Not enough {{ currencyBalance }}", + "title": "You don’t have enough {{ currencyBalance }}" + }, + "insufficientGas": { + "button": "Not enough {{ currencySymbol }}", + "cta": { + "button": "Buy {{ currencySymbol }}", + "message": "<0>You need more <2>{{currencySymbol}} to cover the network cost for this transaction." + }, + "title": "You don’t have enough {{ currencySymbol }} to cover the network cost" + }, + "lowLiquidity": { + "message": "There isn’t currently enough liquidity available between these tokens to perform a swap. Please try again later or select another token.", + "title": "Not enough liquidity" + }, + "networkFee": { + "message": "This is the cost to process your transaction on the blockchain. Uniswap does not receive any share of these fees.", + "title": "Network cost" + }, + "offline": { + "message": "You may have lost internet connection or the network may be down. Please check your internet connection and try again.", + "title": "You’re offline" + }, + "priceImpact": { + "message": "Due to the amount of {{ outputCurrencySymbol }} liquidity currently available, the more {{ inputCurrencySymbol }} you try to swap, the less {{ outputCurrencySymbol }} you will receive.", + "title": "High price impact ({ priceImpactValue })" + }, + "rateLimit": { + "message": "Please try again in a few minutes.", + "title": "Rate limit exceeded" + }, + "router": { + "message": "You may have lost connection or the network may be down. If the problem persists, please try again later.", + "title": "This trade cannot be completed right now" + }, + "uniswapFee": { + "message": { + "default": "Fees are applied on a few select tokens to ensure the best experience with Uniswap. There is no fee associated with this swap.", + "included": "Fees are applied on a few select tokens to ensure the best experience with Uniswap, and have already been factored into this quote." + }, + "title": "Swap fee" + }, + "viewOnly": { + "message": "You need to import this wallet via recovery phrase to swap tokens." + } + } + }, + "token": { + "balances": { + "main": "Your balance", + "other": "Balances on other networks", + "viewOnly": "{{ ownerAddress }}’s balance" + }, + "error": { + "unknown": "Unknown token" + }, + "links": { + "contract": "Contract", + "title": "Links", + "twitter": "Twitter", + "website": "Website" + }, + "priceExplorer": { + "error": { + "description": "Something went wrong.", + "title": "Couldn’t load price chart" + }, + "timeRangeLabel": { + "day": "1D", + "hour": "1H", + "month": "1M", + "week": "1W", + "year": "1Y" + } + }, + "safetyLevel": { + "blocked": { + "header": "Not available", + "message": "You can’t trade this token using the Uniswap Wallet." + }, + "medium": { + "header": "Caution", + "message": "This token isn’t traded on leading U.S. centralized exchanges. Always conduct your own research before trading." + }, + "strong": { + "header": "Warning", + "message": "This token isn’t traded on leading U.S. centralized exchanges or frequently swapped on Uniswap. Always conduct your own research before trading." + } + }, + "selector": { + "search": { + "error": "Couldn’t load search results" + } + }, + "stats": { + "fullyDilutedValuation": "Fully Diluted Valuation", + "marketCap": "Market Cap", + "priceHighYear": "52W High", + "priceLowYear": "52W Low", + "section": { + "about": "About {{ token }}" + }, + "title": "Stats", + "translation": { + "original": "Show original", + "translate": "Translate to {{ language }}" + }, + "volume": "24h Volume" + } + }, + "tokens": { + "action": { + "hide": "Hide Token", + "unhide": "Unhide Token" + }, + "hidden": { + "label": "Hidden ({{ numHidden }})" + }, + "nfts": { + "collection": { + "error": { + "load": { + "title": "Couldn’t load NFT collection" + } + }, + "label": { + "items": "Items", + "owners": "Owners", + "priceFloor": "Floor", + "swapVolume": "Volume" + } + }, + "details": { + "error": { + "load": { + "title": "Couldn’t load NFT details" + } + }, + "owner": "Owned by", + "price": "Current price", + "recentPrice": "Last sale price", + "traits": "Traits" + }, + "empty": { + "description": "No NFTs found" + }, + "error": { + "unavailable": "Content not available" + }, + "hidden": { + "action": { + "hide": "Hide NFT", + "unhide": "Unhide NFT" + }, + "label": "Hidden ({{ numHidden }})" + }, + "link": { + "collection": "Collection website" + }, + "list": { + "error": { + "load": { + "title": "Couldn’t load NFTs" + } + }, + "none": { + "button": "Receive NFTs", + "description": { + "default": "Transfer NFTs from another wallet to get started.", + "external": "When this wallet buys or receives NFTs, they’ll appear here." + }, + "title": "No NFTs yet" + } + } + }, + "selector": { + "button": { + "choose": "Choose token", + "clear": "Clear all" + }, + "empty": { + "buy": { + "message": "Buy crypto with a card or bank to send tokens.", + "title": "Buy crypto" + }, + "receive": { + "message": "Transfer tokens from a centralized exchange or another wallet to send tokens.", + "title": "Receive tokens" + }, + "title": "No tokens yet" + }, + "error": { + "load": "Couldn’t load tokens" + }, + "search": { + "empty": "No results found for <0>{{ searchText }}", + "placeholder": "Search tokens" + }, + "section": { + "favorite": "Favorites", + "popular": "Popular tokens", + "recent": "Recent searches", + "search": "Search results", + "suggested": "Suggested", + "yours": "Your tokens" + } + } + }, + "transaction": { + "action": { + "cancel": { + "button": "Cancel transaction", + "description": "If you cancel this transaction before it’s processed by the network, you’ll pay a new network fee instead of the original one.", + "title": "Cancel this transaction?" + }, + "copy": "Copy transaction ID", + "copyMoonPay": "Copy MoonPay transaction ID", + "view": "View {{ tokenSymbol }}", + "viewEtherscan": "View on {{ blockExplorerName }}", + "viewMoonPay": "View on MoonPay" + }, + "amount": { + "unlimited": "Unlimited" + }, + "currency": { + "unknown": "unknown token" + }, + "date": "Submitted on {{ date }}", + "details": { + "send": { + "function": "<0>Function <1><0>{{contractFunction}}", + "recipient": "<0>to <1><0>{{recipientAddress}}...", + "sending": "<0><0>Sending <1><0>{{tokenAmount}}<1><0>to <1><0>{{recipientAddress}}...<2><0>Function <1><0>{{contractFunction}}" + } + }, + "network": { + "all": "All networks" + }, + "networkCost": { + "label": "Network cost" + }, + "notification": { + "error": { + "cancel": "Unable to cancel transaction", + "replace": "Unable to replace transaction" + } + }, + "status": { + "approve": { + "canceled": "Canceled approve", + "canceling": "Canceling approve", + "failed": "Failed to approve", + "pending": "Approving", + "success": "Approved", + "successDapp": "Approved on {{ externalDappName }}" + }, + "buy": { + "canceled": "Canceled buy", + "canceling": "Canceling buy", + "failed": "Failed to buy", + "pending": "Buying", + "success": "Bought", + "successDapp": "Bought on {{ externalDappName }}" + }, + "confirm": { + "canceled": "Canceled confirm", + "canceling": "Canceling confirm", + "failed": "Failed to confirm", + "pending": "Transaction in progress", + "success": "Transaction confirmed", + "successDapp": "Transaction confirmed on {{ externalDappName }}" + }, + "mint": { + "canceled": "Canceled mint", + "canceling": "Canceling mint", + "failed": "Failed to mint", + "pending": "Minting", + "success": "Minted", + "successDapp": "Minted on {{ externalDappName }}" + }, + "purchase": { + "canceled": "Canceled purchase", + "canceling": "Canceling purchase", + "failed": "Failed to purchase", + "pending": "Purchasing", + "success": "Purchased", + "successDapp": "Purchased on {{ externalDappName }}" + }, + "receive": { + "canceled": "Canceled receive", + "canceling": "Canceling receive", + "failed": "Failed to receive", + "pending": "Receiving", + "success": "Received", + "successDapp": "Received on {{ externalDappName }}" + }, + "revoke": { + "canceled": "Canceled revoke", + "canceling": "Canceling revoke", + "failed": "Failed to revoke", + "pending": "Revoking", + "success": "Revoked", + "successDapp": "Revoked on {{ externalDappName }}" + }, + "sell": { + "canceled": "Canceled sell", + "canceling": "Canceling sell", + "failed": "Failed to sell", + "pending": "Selling", + "success": "Sold", + "successDapp": "Sold on {{ externalDappName }}" + }, + "send": { + "canceled": "Canceled send", + "canceling": "Canceling send", + "failed": "Failed to send", + "pending": "Sending", + "success": "Sent", + "successDapp": "Sent on {{ externalDappName }}" + }, + "swap": { + "canceled": "Canceled swap", + "canceling": "Canceling swap", + "failed": "Failed to swap", + "pending": "Swapping", + "success": "Swapped", + "successDapp": "Swapped on {{ externalDappName }}" + }, + "unwrap": { + "canceled": "Canceled unwrap", + "canceling": "Canceling unwrap", + "failed": "Failed to unwrap", + "pending": "Unwrapping", + "success": "Unwrapped", + "successDapp": "Unwrapped on {{ externalDappName }}" + }, + "wrap": { + "canceled": "Canceled wrap", + "canceling": "Canceling wrap", + "failed": "Failed to wrap", + "pending": "Wrapping", + "success": "Wrapped", + "successDapp": "Wrapped on {{ externalDappName }}" + } + }, + "summary": { + "received": "{{ tokenAmountWithSymbol }} to {{ recipientAddress }}", + "sent": "{{ tokenAmountWithSymbol }} from {{ senderAddress }}" + }, + "watcher": { + "error": { + "cancel": "Unable to cancel transaction", + "status": "Error while checking transaction status" + } + } + }, + "unitags": { + "banner": { + "button": { + "claim": "Claim now" + }, + "subtitle": "Build a personalized web3 profile and easily share your address with friends.", + "title": { + "compact": "<0>Claim your {{ unitagDomain }} username and build out your customizable profile.", + "full": "Claim your {{ unitagDomain }} username" + } + }, + "choosePhoto": { + "option": { + "cameraRoll": "Choose from camera roll", + "nft": "Choose an NFT", + "remove": "Remove profile picture" + } + }, + "claim": { + "confirmation": { + "customize": "Customize profile", + "description": "{{ unitagAddress }} is ready to send and receive crypto. Continue to build out your wallet by customizing your web3 profile.", + "success": { + "long": "You got it!", + "short": "got it!" + } + }, + "error": { + "addressLimit": "You already have made the maximum number of changes to your username for this address", + "appCheck": "Could not claim username. Please try again tomorrow.", + "avatar": "Could not set avatar. Try again later.", + "default": "Could not claim username. Try again later.", + "deviceLimit": "You have hit the maximum number of usernames that can be active for this device", + "ens": "To claim this username you must own the {{ username }}.eth ENS", + "ensMismatch": "This username is not currently available.", + "general": "Unable to claim username", + "unavailable": "This username is not available", + "unknown": "Unknown error" + }, + "username": { + "default": "yourname" + } + }, + "delete": { + "confirm": { + "subtitle": "You’re about to delete your username and customizable profile details. You will not be able to reclaim it.", + "title": "Are you sure?" + } + }, + "editProfile": { + "placeholder": "username" + }, + "editUsername": { + "button": { + "confirm": "Save changes" + }, + "confirm": { + "subtitle": "You’re about to change your username. Once you change it, you can never claim it again.", + "title": "Are you sure?" + }, + "title": "Edit username", + "warning": { + "default": "Once you change your username, you can never claim it again. You can only change it 2 times.", + "max": "You’ve reached the maximum number of 2 usernames changes." + } + }, + "intro": { + "features": { + "ens": "Powered by ENS subdomains", + "free": "Free to claim", + "profile": "Customizable profiles" + }, + "subtitle": "Say goodbye to 0x addresses. Usernames are readable names that make it easier to send and receive crypto.", + "title": "Introducing usernames" + }, + "notification": { + "delete": { + "error": "Could not delete username. Try again later.", + "title": "Username deleted" + }, + "profile": { + "error": "Could not update profile. Try again later.", + "title": "Profile updated" + }, + "username": { + "error": "Could not change username. Try again later.", + "title": "Username changed" + } + }, + "onboarding": { + "claim": { + "subtitle": "This is your unique name that anyone can send crypto to.", + "title": { + "choose": "Choose your username", + "claim": "Claim your username" + } + }, + "claimPeriod": { + "description": "For a limited time, the username {{ username }} is reserved. Import the wallet that owns {{ username }}.eth ENS to claim this username or try again after the claim period.", + "link": "Learn more about our <1>claim period.", + "title": "ENS claim period" + }, + "info": { + "description": "Usernames transform complex 0x addresses into readable names. By claiming a {{ unitagDomain }} username, you can easily send and receive crypto and build out a public web3 profile.", + "title": "A simplified address" + }, + "profile": { + "subtitle": "Upload your own or stick with your unique Unicon. You can always change this later.", + "title": "Choose a profile photo" + } + }, + "profile": { + "action": { + "delete": "Delete username", + "edit": "Edit username" + }, + "bio": { + "label": "Bio", + "placeholder": "Type a bio for your profile" + }, + "links": { + "twitter": "Twitter" + } + }, + "username": { + "error": { + "chars": "Usernames can only contain lowercase letters and numbers", + "max": "Usernames cannot be more than {{ number }} characters", + "min": "Usernames must be at least {{ number }} characters" + } + } + }, + "walletConnect": { + "dapps": { + "connection": "<0>Connected to {{ dappNameOrUrl }}", + "empty": { + "description": "Connect to an app by scanning a code via WalletConnect" + }, + "manage": { + "empty": { + "title": "No apps connected" + }, + "title": "Manage connections" + } + }, + "error": { + "connection": { + "message": "Uniswap Wallet currently supports {{ chainNames }}. Please only use \"{{ dappName }}\" on these chains", + "title": "Connection Error" + }, + "general": { + "message": "There was an issue with WalletConnect. Please try again", + "title": "WalletConnect Error" + }, + "unsupported": { + "message": "Make sure that you’re scanning a valid WalletConnect or Ethereum address QR code before trying again.", + "title": "Invalid QR Code" + }, + "unsupportedV1": { + "message": "WalletConnect v1 is no longer supported. The application you’re trying to connect to needs to upgrade to WalletConnect v2.", + "title": "Invalid QR Code" + }, + "uwu": { + "scan": "There was an issue scanning this QR code.", + "title": "UwU Link error", + "unsupported": "This QR code is not supported." + } + }, + "pending": { + "button": { + "connect": "Connect" + }, + "switchAccount": "Switch Account", + "switchNetwork": "Switch Network", + "title": "{{ dappName }} wants to connect to your wallet" + }, + "permissions": { + "networks": "Networks", + "option": { + "transferAssets": "Transfer your assets without consent", + "viewTokenBalances": "View your token balances", + "viewWalletAddress": "View your wallet address" + }, + "title": "App permissions" + }, + "request": { + "button": { + "sign": "Sign" + }, + "details": { + "function": "<0>Function: <1><0>{{ functionName }}", + "recipient": "<0>To: <1>{{ recipientAddress }}", + "sending": "<0>Sending: <1><0><1>{{ tokenAmountWithSymbol }} <2>({{ fiatAmount }})" + }, + "error": { + "insufficientFunds": "You don’t have enough {{ currencySymbol }} to complete this transaction.", + "network": "Internet or network connection error" + }, + "label": { + "network": "Network" + }, + "warning": { + "general": { + "message": "Be careful: this message may transfer assets", + "transaction": "Be careful: this transaction may transfer assets" + }, + "message": "In order to sign messages or transactions, you’ll need to import the wallet’s recovery phrase.", + "title": "This wallet is in view only mode" + } + } + } } diff --git a/packages/wallet/src/test/README.md b/packages/wallet/src/test/README.md new file mode 100644 index 00000000000..69354f5db5e --- /dev/null +++ b/packages/wallet/src/test/README.md @@ -0,0 +1,122 @@ +# Tests + +This directory contains fixtures, mocks and utilities useful while writing tests. + +## 1. Structure of directories and files + +### 1.1. fixtures + +- All test fixtures should be stored in this directory, +- Subdirectories group fixtures based on their type declaration location: + - `gql` - contains all graphql-related fixtures. If the object type was declared in the `wallet/src/data/generated/types-and-hooks` file, its fixture should be added in this directory, + - `lib` - stores library-related fixtures (e.g. for transaction types from the `ethers` library, for token types from the @uniswap/sdk-core library, etc.), + - `wallet` - if the type of an object was specified somewhere in the wallet package files, its fixture should be added in this directory. + - remaining files - `constants` (contains constants used in tests), `events` (contains event payload fixtures) + +### 1.2. mocks + +- Contains all mocks (i.e. mocked providers, resolvers, default values for non-mocked graphql resolvers, etc.). In short, all mocks that aren't just simple objects with primitive values (or values with nested objects) should be located in this ditectory. + +## 2. Usage + +### 2.1. Creating fixtures + +#### 2.1.1. Basics + +`createFixture` is the core function used to create fixtures which is declared in the `packages/wallet/src/test/utils/factory.ts` file. + +Take a look at the usage example: + +```tsx +export const networkUnknown = createFixture()(() => ({ + isConnected: null, + type: NetInfoStateType.unknown, + isInternetReachable: null, + details: null, +})); +``` + +To create a fixture, we have to use the `createFixture` function and provide one type argument which will be the type of the object the fixture corresponds to (`NetInfoUnknownState` in the example above). In the simplest scenario, the `createFixture` fixture is called with no arguments (`createFixture()`). This call returns another function that takes a callback which should return the resulting fixture object. + +To make the result more similar to real-world scenario, we can use the `faker` library to generate values for respective fields in the fixture. + +The result (`networkUnknown` variable in the example) is a function that can be called to get the fixture object. This is a function to ensure that the fixture is not a static object which fields are easily predictable (this may lead to `false` positives in tests where we expect certain values because our hardcoded fixture contains exactly the same fields). + +The type of the object returned by our fixture function (`networkUnknown`) that we created with `createFixture` will be automatically adjusted to match the type of the object specified in the callback function that contains values for mocked fields. This is better than using the base type (`NetInfoUnknownState` in this case) which may have all fields marked as optional (common in graphql) what can lead to type errors in tests where we expect some fields to be present. + +#### 2.1.2. Custom options + +The first function returned by `createFixture` takes an optional `options` parameter. This parameter should be an object containing any custom fields which may alter the resulting fixture. e.g. we can create the graphql `Token` type from the sdk token passed as a parameter. + +##### Example implementation + +```tsx +type TokenOptions = { + sdkToken: SDKToken | null; +}; + +export const token = createFixture({ sdkToken: null })(({ sdkToken }) => ({ + __typename: "Token", + id: faker.datatype.uuid(), + name: sdkToken?.name ?? faker.lorem.word(), + symbol: sdkToken?.symbol ?? faker.lorem.word(), + decimals: sdkToken?.decimals ?? faker.datatype.number({ min: 1, max: 18 }), + chain: (sdkToken ? toGraphQLChain(sdkToken.chainId) : null) ?? randomChoice(GQL_CHAINS), + address: sdkToken?.address.toLocaleLowerCase() ?? faker.finance.ethereumAddress(), + market: null, + project: tokenProjectBase(), +})); +``` + +To be able to use custom options, we have to pass a second type to the `createFixture` function that specifies the type of the custom options object. In the example above, when the `sdkToken` is provided, its field values will be used to create a fixture. Otherwise, field values will be generated with `faker` library. + +##### Another example + +```tsx +type NftCollectionOptions = { + contractsCount: number; +}; + +export const nftCollection = createFixture({ + contractsCount: 2, +})(({ contractsCount }) => ({ + __typename: "NftCollection", + id: faker.datatype.uuid(), + name: faker.lorem.word(), + collectionId: faker.datatype.uuid(), + isVerified: faker.datatype.boolean(), + nftContracts: createArray(contractsCount, nftContract), + image: image(), +})); +``` + +Thanks to custom options, we can easily manipulate the number of items in the array. `createArray` is a utility function taking the number of array items and a callback function called for all items in the resulting array. + +#### 2.2. Using fixtures + +The usage is very simple. To create an object, we have to just call the fixture function we created with the `createFixture` factory function. If no additional parameters are provided, it will create a fixture based on default options (if specified) and default values returned in the `getValues` callback function while creating a fixture. + +We can easily override any of fixture fields based on per-test requirements. If the object should contain more fields than specified in the fixture, which are optional in the base type, we can just pass an object as the fixture function argument with the value for the specific field/fields. In the same way, we can override fields that are already specified in the fixture factory callback. + +##### 2.2.1. Example usage + +###### In resolver mocks + +```tsx +const resolvers: Resolvers = { + Query: { + topTokens: () => [wethToken(), usdcToken()], + tokens: () => [ethToken({ address: null })], + }, +}; +``` + +###### Combining fixtures + +Fixtures can be used to override fields in other fixtures as shown in the example below. + +```tsx +const collection = nftCollection({ + nftContracts: [nftContract({ chain: Chain.Ethereum })], +}); +``` diff --git a/packages/wallet/src/test/fixtures.ts b/packages/wallet/src/test/fixtures.ts deleted file mode 100644 index c34a53e24e0..00000000000 --- a/packages/wallet/src/test/fixtures.ts +++ /dev/null @@ -1,569 +0,0 @@ -/* eslint-disable max-lines */ -import { faker } from '@faker-js/faker' -import { - NetInfoConnectedStates, - NetInfoNoConnectionState, - NetInfoStateType, - NetInfoUnknownState, -} from '@react-native-community/netinfo' -import { TradeType } from '@uniswap/sdk-core' -import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk' -import { FeeAmount, Pool } from '@uniswap/v3-sdk' -import { BigNumber, providers } from 'ethers' -import { SectionListData } from 'react-native' -import ERC20_ABI from 'wallet/src/abis/erc20.json' -import { Erc20, Weth } from 'wallet/src/abis/types' -import WETH_ABI from 'wallet/src/abis/weth.json' -import { config } from 'wallet/src/config' -import { getNativeAddress, getWrappedNativeAddress } from 'wallet/src/constants/addresses' -import { ChainId } from 'wallet/src/constants/chains' -import { DAI, DAI_ARBITRUM_ONE, UNI, WBTC } from 'wallet/src/constants/tokens' -import { HistoryDuration, SafetyLevel } from 'wallet/src/data/__generated__/types-and-hooks' -import { AssetType } from 'wallet/src/entities/assets' -import { SearchableRecipient } from 'wallet/src/features/address/types' -import { ContractManager } from 'wallet/src/features/contracts/ContractManager' -import { - CurrencyInfo, - PortfolioBalance as PortfolioBalanceType, -} from 'wallet/src/features/dataApi/types' -import { FiatOnRampTransactionDetails } from 'wallet/src/features/fiatOnRamp/types' -import { AppNotificationType } from 'wallet/src/features/notifications/types' -import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' -import { finalizeTransaction } from 'wallet/src/features/transactions/slice' -import { - ApproveTransactionInfo, - FiatPurchaseTransactionInfo, - SendTokenTransactionInfo, - TransactionDetails, - TransactionStatus, - TransactionType, -} from 'wallet/src/features/transactions/types' -import { Account, AccountType, BackupType } from 'wallet/src/features/wallet/accounts/types' -import { SignerManager } from 'wallet/src/features/wallet/signing/SignerManager' -import { initialWalletState } from 'wallet/src/features/wallet/slice' -import { WalletConnectEvent } from 'wallet/src/features/walletConnect/types' -import { currencyId } from 'wallet/src/utils/currencyId' - -// Ensures stable fixtures -export const FAKER_SEED = 123 -export const MAX_FIXTURE_TIMESTAMP = 1609459200 - -faker.seed(FAKER_SEED) - -export const mockSigner = new (class { - signTransaction = (): string => faker.finance.ethereumAddress() - connect = (): this => this -})() - -export const SAMPLE_PASSWORD = 'my-super-strong-password' -export const SAMPLE_SEED = [ - 'dove', - 'lumber', - 'quote', - 'board', - 'young', - 'robust', - 'kit', - 'invite', - 'plastic', - 'regular', - 'skull', - 'history', -].join(' ') - -export const SAMPLE_SEED_ADDRESS_1 = '0x82D56A352367453f74FC0dC7B071b311da373Fa6' -export const SAMPLE_SEED_ADDRESS_2 = '0x55f4B664C68F398f9e81EFf63ef4444A1A184F98' -export const SAMPLE_CURRENCY_ID_1 = '1-0x6b175474e89094c44da98b954eedeac495271d0f' -export const SAMPLE_CURRENCY_ID_2 = '1-0x4d224452801aced8b2f0aebe155379bb5d594381' - -export const MainnetEth = NativeCurrency.onChain(ChainId.Mainnet) -export const PolygonMatic = NativeCurrency.onChain(ChainId.Polygon) -export const ArbitrumEth = NativeCurrency.onChain(ChainId.ArbitrumOne) -export const OptimismEth = NativeCurrency.onChain(ChainId.Optimism) -export const BaseEth = NativeCurrency.onChain(ChainId.Base) - -export { faker } - -export const account: Account = { - type: AccountType.SignerMnemonic, - address: SAMPLE_SEED_ADDRESS_1, - derivationIndex: 0, - name: 'Test Account', - timeImportedMs: 10, - mnemonicId: SAMPLE_SEED_ADDRESS_1, - backups: [BackupType.Cloud], -} - -export const account2: Account = { - type: AccountType.Readonly, - address: SAMPLE_SEED_ADDRESS_2, - name: 'Test Account', - timeImportedMs: 10, -} - -const mockFeeData = { - maxFeePerPrice: BigNumber.from('1000'), - maxPriorityFeePerGas: BigNumber.from('10000'), - gasPrice: BigNumber.from('10000'), -} - -export const mockProvider = { - getBalance: (): BigNumber => BigNumber.from('1000000000000000000'), - getGasPrice: (): BigNumber => BigNumber.from('100000000000'), - getTransactionCount: (): number => 1000, - estimateGas: (): BigNumber => BigNumber.from('30000'), - sendTransaction: (): { hash: string } => ({ hash: '0xabcdef' }), - detectNetwork: (): { name: string; chainId: ChainId } => ({ - name: 'mainnet', - chainId: 1, - }), - getTransactionReceipt: (): typeof txReceipt => txReceipt, - waitForTransaction: (): typeof txReceipt => txReceipt, - getFeeData: (): typeof mockFeeData => mockFeeData, -} - -export const mockProviderManager = { - getProvider: (): typeof mockProvider => mockProvider, -} - -export const signerManager = new SignerManager() - -export const provider = new providers.JsonRpcProvider() -export const providerManager = { - getProvider: (): typeof provider => provider, -} - -export const mockContractManager = { - getOrCreateContract: (): typeof mockTokenContract => mockTokenContract, -} - -export const mockTokenContract = { - balanceOf: (): BigNumber => BigNumber.from('1000000000000000000'), - populateTransaction: { - transfer: (): typeof txRequest => txRequest, - transferFrom: (): typeof txRequest => txRequest, - safeTransferFrom: (): typeof txRequest => txRequest, - }, -} - -export const contractManager = new ContractManager() -contractManager.getOrCreateContract(ChainId.Goerli, DAI.address, provider, ERC20_ABI) -contractManager.getOrCreateContract( - ChainId.Goerli, - getWrappedNativeAddress(ChainId.Goerli), - provider, - WETH_ABI -) -export const tokenContract = contractManager.getContract(ChainId.Goerli, DAI.address) as Erc20 -export const wethContract = contractManager.getContract( - ChainId.Goerli, - getWrappedNativeAddress(ChainId.Goerli) -) as Weth - -/** - * Transactions - */ -export const txRequest: providers.TransactionRequest = { - from: '0x123', - to: '0x456', - value: '0x0', - data: '0x789', - nonce: 10, - gasPrice: mockFeeData.gasPrice, -} - -export const txReceipt = { - transactionHash: '0x123', - blockHash: '0x123', - blockNumber: 1, - transactionIndex: 1, - confirmations: 1, - status: 1, - confirmedTime: 1400000000000, - gasUsed: BigNumber.from('100000'), - effectiveGasPrice: BigNumber.from('1000000000'), -} - -export const txResponse = { - hash: '0x123', - wait: (): typeof txReceipt => txReceipt, -} - -export const txTypeInfo: ApproveTransactionInfo = { - type: TransactionType.Approve, - tokenAddress: tokenContract.address, - spender: UNIVERSAL_ROUTER_ADDRESS(ChainId.Goerli), -} - -export const sendTxTypeInfo: SendTokenTransactionInfo = { - type: TransactionType.Send, - tokenAddress: tokenContract.address, - recipient: '0x123', - assetType: AssetType.Currency, -} - -export const txDetailsPending = { - chainId: ChainId.Mainnet, - id: '0', - from: account.address, - options: { - request: txRequest, - }, - typeInfo: txTypeInfo, - status: TransactionStatus.Pending, - addedTime: 1487076708000, - hash: '0x123', -} - -export const txDetailsConfirmed = { - ...txDetailsPending, - status: TransactionStatus.Success, - receipt: { - blockHash: txReceipt.blockHash, - blockNumber: txReceipt.blockNumber, - transactionIndex: txReceipt.transactionIndex, - confirmations: txReceipt.confirmations, - confirmedTime: txReceipt.confirmedTime, - gasUsed: txReceipt.gasUsed.toNumber(), - effectiveGasPrice: txReceipt.effectiveGasPrice.toNumber(), - }, -} - -export const fiatOnRampTxDetailsPending: FiatOnRampTransactionDetails = { - chainId: ChainId.Mainnet, - id: '0', - from: account.address, - options: { - request: txRequest, - }, - typeInfo: { ...txTypeInfo, syncedWithBackend: false, type: TransactionType.FiatPurchase }, - status: TransactionStatus.Pending, - addedTime: 1487076708000, -} - -export const fiatOnRampTxDetailsFailed: FiatOnRampTransactionDetails & { - typeInfo: FiatPurchaseTransactionInfo -} = { - chainId: ChainId.Mainnet, - id: '0', - from: account.address, - options: { - request: txRequest, - }, - typeInfo: { - type: TransactionType.FiatPurchase, - explorerUrl: - 'https://buy-sandbox.moonpay.com/transaction_receipt?transactionId=d6c32bb5-7cd9-4c22-8f46-6bbe786c599f', - id: 'd6c32bb5-7cd9-4c22-8f46-6bbe786c599f', - syncedWithBackend: true, - }, - status: TransactionStatus.Failed, - addedTime: 1487076708000, - hash: '0x123', -} - -export const finalizedTxAction: ReturnType = { - payload: { ...txDetailsConfirmed, status: TransactionStatus.Success }, - type: 'transactions/finalizeTransaction', -} - -export const sendTxDetailsPending: TransactionDetails = { - chainId: ChainId.Mainnet, - id: '0', - from: account.address, - options: { - request: txRequest, - }, - typeInfo: { - ...sendTxTypeInfo, - recipient: faker.finance.ethereumAddress(), - }, - status: TransactionStatus.Pending, - addedTime: 1487076708000, -} - -export const sendTxDetailsConfirmed: TransactionDetails = { - ...sendTxDetailsPending, - typeInfo: { - ...sendTxTypeInfo, - recipient: faker.finance.ethereumAddress(), - }, - status: TransactionStatus.Success, - receipt: { - blockHash: txReceipt.blockHash, - blockNumber: txReceipt.blockNumber, - transactionIndex: txReceipt.transactionIndex, - confirmations: txReceipt.confirmations, - confirmedTime: txReceipt.confirmedTime, - gasUsed: txReceipt.gasUsed.toNumber(), - effectiveGasPrice: txReceipt.effectiveGasPrice.toNumber(), - }, - addedTime: 1487076709000, -} - -export const sendTxDetailsFailed: TransactionDetails = { - ...sendTxDetailsPending, - typeInfo: { - ...sendTxTypeInfo, - recipient: faker.finance.ethereumAddress(), - }, - status: TransactionStatus.Failed, - addedTime: 1487076710000, -} - -export const swapNotification = { - type: AppNotificationType.Transaction, - chainId: ChainId.Mainnet, - txId: 'uid-1234', - txHash: '0x01', - txType: TransactionType.Swap, - txStatus: TransactionStatus.Success, - inputCurrencyId: `1-${getNativeAddress(ChainId.Mainnet)}`, - outputCurrencyId: '1-0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984', - inputCurrencyAmountRaw: '230000000000000000', - outputCurrencyAmountRaw: '123000000000000000000', - tradeType: TradeType.EXACT_INPUT, -} - -export const transferCurrencyNotification = { - type: AppNotificationType.Transaction, - chainId: ChainId.Mainnet, - txId: 'uid-1234', - txHash: '0x000', - txType: TransactionType.Send, - txStatus: TransactionStatus.Success, - assetType: AssetType.Currency, - tokenAddress: '0x4d224452801ACEd8B2F0aebE155379bb5D594381', - currencyAmountRaw: '1000000000000000000', - recipient: '0x11E4857Bb9993a50c685A79AFad4E6F65D518DDa', - // sender: '0x939C8d89EBC11fA45e576215E2353673AD0bA18A', -} - -export const transferNFTNotification = { - type: AppNotificationType.Transaction, - chainId: ChainId.Mainnet, - txId: 'uid-1234', - txHash: '0x000', - txType: TransactionType.Send, - txStatus: TransactionStatus.Success, - assetType: AssetType.ERC1155, - tokenAddress: '0x7Bd29408f11D2bFC23c34f18275bBf23bB716Bc7', - tokenId: '4334', - recipient: '0x11E4857Bb9993a50c685A79AFad4E6F65D518DDa', - // sender: '0x11E4857Bb9993a50c685A79AFad4E6F65D518DDa', -} - -export const wcNotification = { - type: AppNotificationType.WalletConnect, - chainId: ChainId.Mainnet, - event: WalletConnectEvent.Connected, - dappName: 'Uniswap', - imageUrl: `${config.uniswapAppUrl}/images/192x192_App_Icon.png`, -} - -export const approveNotification = { - type: AppNotificationType.Transaction, - chainId: ChainId.Mainnet, - txId: 'uid-1234', - txHash: '0x000', - txType: TransactionType.Approve, - txStatus: TransactionStatus.Success, - tokenAddress: '0x4d224452801ACEd8B2F0aebE155379bb5D594381', - spender: '0x939C8d89EBC11fA45e576215E2353673AD0bA18A', -} - -export const unknownNotification = { - type: AppNotificationType.Transaction, - chainId: ChainId.Mainnet, - txId: 'uid-1234', - txHash: '0x000', - txType: TransactionType.Unknown, - txStatus: TransactionStatus.Success, - tokenAddress: '0x939C8d89EBC11fA45e576215E2353673AD0bA18A', -} - -export const networkUnknown: NetInfoUnknownState = { - isConnected: null, - type: NetInfoStateType.unknown, - isInternetReachable: null, - details: null, -} - -export const networkDown: NetInfoNoConnectionState = { - isConnected: false, - type: NetInfoStateType.none, - isInternetReachable: false, - details: null, -} - -export const ETH = NativeCurrency.onChain(ChainId.Mainnet) - -export const networkUp: NetInfoConnectedStates = { - isConnected: true, - type: NetInfoStateType.other, - isInternetReachable: true, - details: { isConnectionExpensive: false }, -} - -export const ethCurrencyInfo: CurrencyInfo = { - currencyId: currencyId(ETH), - currency: ETH, - logoUrl: 'https://token-icons.s3.amazonaws.com/eth.png', - safetyLevel: SafetyLevel.Verified, -} - -export const uniCurrencyInfo: CurrencyInfo = { - currencyId: currencyId(UNI[ChainId.Mainnet]), - currency: UNI[ChainId.Mainnet], - logoUrl: - 'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png', - safetyLevel: SafetyLevel.Verified, -} - -export const daiCurrencyInfo: CurrencyInfo = { - currencyId: currencyId(DAI), - currency: DAI, - logoUrl: - 'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png', - safetyLevel: SafetyLevel.Verified, -} - -export const arbitrumDaiCurrencyInfo: CurrencyInfo = { - currencyId: currencyId(DAI_ARBITRUM_ONE), - currency: DAI_ARBITRUM_ONE, - logoUrl: - 'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png', - safetyLevel: SafetyLevel.Verified, -} - -// Useful when passing in preloaded state where active account is required -export const mockWalletPreloadedState = { - wallet: { - ...initialWalletState, - accounts: { [account.address]: account }, - activeAccountAddress: account.address, - }, -} - -export const mockPool = new Pool( - UNI[ChainId.Mainnet], - WBTC, - FeeAmount.HIGH, - '2437312313659959819381354528', - '10272714736694327408', - -69633 -) - -export const SearchableRecipients: [SearchableRecipient, SearchableRecipient] = [ - { - address: SAMPLE_SEED_ADDRESS_1, - name: 'Recipient 1 name', - }, - { - address: SAMPLE_SEED_ADDRESS_2, - name: 'Recipient 2 name', - }, -] - -export const RecipientSections: [ - SectionListData, - SectionListData, - SectionListData, - SectionListData -] = [ - { - title: 'Section 1', - data: SearchableRecipients, - }, - { - title: 'Section 2', - data: [SearchableRecipients[0]], - }, - { - title: 'Section3', - data: [], - }, - { - title: 'Section 4', - data: [SearchableRecipients[1]], - }, -] - -export const hourMs = 60 * 60 * 1000 -export const dayMs = 24 * hourMs -export const weekMs = 7 * dayMs -export const monthMs = 30 * dayMs -export const yearMs = 365 * dayMs - -export const historyDurationMs: Record = { - [HistoryDuration.Hour]: hourMs, - [HistoryDuration.Day]: dayMs, - [HistoryDuration.Week]: weekMs, - [HistoryDuration.Month]: monthMs, - [HistoryDuration.Year]: yearMs, - [HistoryDuration.Max]: 5 * yearMs, -} - -export const PortfolioBalancesWithUSD: [ - PortfolioBalanceType, - PortfolioBalanceType, - PortfolioBalanceType -] = [ - { - cacheId: faker.datatype.uuid(), - quantity: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }), - balanceUSD: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }), - currencyInfo: ethCurrencyInfo, - relativeChange24: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }), - isHidden: faker.datatype.boolean(), - }, - { - cacheId: faker.datatype.uuid(), - quantity: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }), - balanceUSD: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }), - currencyInfo: uniCurrencyInfo, - relativeChange24: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }), - isHidden: faker.datatype.boolean(), - }, - { - cacheId: faker.datatype.uuid(), - quantity: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }), - balanceUSD: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }), - currencyInfo: daiCurrencyInfo, - relativeChange24: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }), - isHidden: faker.datatype.boolean(), - }, -] - -export const PortfolioBalanceWithoutUSD: [ - PortfolioBalanceType, - PortfolioBalanceType, - PortfolioBalanceType -] = [ - { - cacheId: faker.datatype.uuid(), - quantity: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }), - balanceUSD: null, - currencyInfo: ethCurrencyInfo, - relativeChange24: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }), - isHidden: faker.datatype.boolean(), - }, - { - cacheId: faker.datatype.uuid(), - quantity: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }), - balanceUSD: null, - currencyInfo: uniCurrencyInfo, - relativeChange24: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }), - isHidden: faker.datatype.boolean(), - }, - { - cacheId: faker.datatype.uuid(), - quantity: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }), - balanceUSD: null, - currencyInfo: daiCurrencyInfo, - relativeChange24: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }), - isHidden: faker.datatype.boolean(), - }, -] - -export const PortfolioBalance = PortfolioBalancesWithUSD[0] diff --git a/packages/wallet/src/test/fixtures/constants.ts b/packages/wallet/src/test/fixtures/constants.ts new file mode 100644 index 00000000000..c80f13b0935 --- /dev/null +++ b/packages/wallet/src/test/fixtures/constants.ts @@ -0,0 +1,20 @@ +export const SAMPLE_PASSWORD = 'my-super-strong-password' +export const SAMPLE_SEED = [ + 'dove', + 'lumber', + 'quote', + 'board', + 'young', + 'robust', + 'kit', + 'invite', + 'plastic', + 'regular', + 'skull', + 'history', +].join(' ') + +export const SAMPLE_SEED_ADDRESS_1 = '0x82D56A352367453f74FC0dC7B071b311da373Fa6' +export const SAMPLE_SEED_ADDRESS_2 = '0x55f4B664C68F398f9e81EFf63ef4444A1A184F98' +export const SAMPLE_CURRENCY_ID_1 = '1-0x6b175474e89094c44da98b954eedeac495271d0f' +export const SAMPLE_CURRENCY_ID_2 = '1-0x4d224452801aced8b2f0aebe155379bb5d594381' diff --git a/packages/wallet/src/test/eventFixtures.ts b/packages/wallet/src/test/fixtures/events.ts similarity index 100% rename from packages/wallet/src/test/eventFixtures.ts rename to packages/wallet/src/test/fixtures/events.ts diff --git a/packages/wallet/src/test/fixtures/gql/activities/index.ts b/packages/wallet/src/test/fixtures/gql/activities/index.ts new file mode 100644 index 00000000000..34513b55ce8 --- /dev/null +++ b/packages/wallet/src/test/fixtures/gql/activities/index.ts @@ -0,0 +1,80 @@ +import { + ActivityType, + AssetActivity, + AssetChange, + Chain, + TransactionStatus, + TransactionType, +} from 'wallet/src/data/__generated__/types-and-hooks' +import { + erc20ApproveAssetChange, + erc20TransferIn, +} from 'wallet/src/test/fixtures/gql/activities/tokens' +import { GQL_CHAINS } from 'wallet/src/test/fixtures/gql/misc' +import { gqlTransaction, gqlTransactionDetails } from 'wallet/src/test/fixtures/gql/transactions' +import { MAX_FIXTURE_TIMESTAMP, faker } from 'wallet/src/test/shared' +import { createFixture, randomChoice, randomEnumValue } from 'wallet/src/test/utils' +import { erc20TokenTransferOut } from './tokens' +export * from './nfts' +export * from './swap' +export * from './tokens' + +/** + * Base fixtures + */ + +export const assetActivity = createFixture()(() => ({ + id: faker.datatype.uuid(), + chain: randomChoice(GQL_CHAINS), + /** @deprecated use assetChanges field in details */ + assetChanges: [] as AssetChange[], + details: gqlTransactionDetails(), + timestamp: faker.datatype.number({ max: MAX_FIXTURE_TIMESTAMP }), + /** @deprecated use type field in details */ + transaction: gqlTransaction(), + /** @deprecated use type field in details */ + type: randomEnumValue(ActivityType), +})) + +/** + * Derived fixtures + */ + +export const approveAssetActivity = createFixture()(() => + assetActivity({ + chain: Chain.Ethereum, + /** @deprecated use type field in details */ + type: ActivityType.Approve, + details: gqlTransactionDetails({ + type: TransactionType.Approve, + transactionStatus: TransactionStatus.Confirmed, + assetChanges: [erc20ApproveAssetChange()], + }), + }) +) + +export const erc20SwapAssetActivity = createFixture()(() => + assetActivity({ + chain: Chain.Ethereum, + /** @deprecated use type field in details */ + type: ActivityType.Swap, + details: gqlTransactionDetails({ + type: TransactionType.Swap, + transactionStatus: TransactionStatus.Confirmed, + assetChanges: [erc20TransferIn(), erc20TokenTransferOut()], + }), + }) +) + +export const erc20ReceiveAssetActivity = createFixture()(() => + assetActivity({ + chain: Chain.Ethereum, + /** @deprecated use type field in details */ + type: ActivityType.Receive, + details: gqlTransactionDetails({ + type: TransactionType.Receive, + transactionStatus: TransactionStatus.Confirmed, + assetChanges: [erc20TransferIn()], + }), + }) +) diff --git a/packages/wallet/src/test/fixtures/gql/activities/nfts.ts b/packages/wallet/src/test/fixtures/gql/activities/nfts.ts new file mode 100644 index 00000000000..57b138bd6b5 --- /dev/null +++ b/packages/wallet/src/test/fixtures/gql/activities/nfts.ts @@ -0,0 +1,37 @@ +import { + NftApproval, + NftApproveForAll, + NftStandard, + NftTransfer, + TransactionDirection, +} from 'wallet/src/data/__generated__/types-and-hooks' +import { nftAsset } from 'wallet/src/test/fixtures/gql/assets' +import { faker } from 'wallet/src/test/shared' +import { createFixture, randomEnumValue } from 'wallet/src/test/utils' + +export const nftApproval = createFixture()(() => ({ + __typename: 'NftApproval', + id: faker.datatype.uuid(), + approvedAddress: faker.finance.ethereumAddress(), + nftStandard: randomEnumValue(NftStandard), + asset: nftAsset(), +})) + +export const nftApproveForAll = createFixture()(() => ({ + __typename: 'NftApproveForAll', + id: faker.datatype.uuid(), + approved: faker.datatype.boolean(), + nftStandard: randomEnumValue(NftStandard), + operatorAddress: faker.finance.ethereumAddress(), + asset: nftAsset(), +})) + +export const nftTransfer = createFixture()(() => ({ + __typename: 'NftTransfer', + id: faker.datatype.uuid(), + sender: faker.finance.ethereumAddress(), + recipient: faker.finance.ethereumAddress(), + direction: randomEnumValue(TransactionDirection), + nftStandard: randomEnumValue(NftStandard), + asset: nftAsset(), +})) diff --git a/packages/wallet/src/test/fixtures/gql/activities/swap.ts b/packages/wallet/src/test/fixtures/gql/activities/swap.ts new file mode 100644 index 00000000000..d74e35c72f3 --- /dev/null +++ b/packages/wallet/src/test/fixtures/gql/activities/swap.ts @@ -0,0 +1,19 @@ +import { SwapOrderDetails, SwapOrderStatus } from 'wallet/src/data/__generated__/types-and-hooks' +import { daiToken, ethToken } from 'wallet/src/test/fixtures/gql/assets' +import { faker } from 'wallet/src/test/shared' +import { createFixture, randomEnumValue } from 'wallet/src/test/utils' + +export const swapOrderDetails = createFixture()(() => ({ + __typename: 'SwapOrderDetails', + id: faker.datatype.uuid(), + hash: faker.datatype.uuid(), + expiry: faker.date.future().getTime(), + inputToken: ethToken(), + inputTokenQuantity: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }).toString(), + offerer: faker.finance.ethereumAddress(), + outputToken: daiToken(), + outputTokenQuantity: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }).toString(), + /** @deprecated use swapOrderStatus to disambiguate from transactionStatus */ + status: randomEnumValue(SwapOrderStatus), + swapOrderStatus: randomEnumValue(SwapOrderStatus), +})) diff --git a/packages/wallet/src/test/fixtures/gql/activities/tokens.ts b/packages/wallet/src/test/fixtures/gql/activities/tokens.ts new file mode 100644 index 00000000000..0adb199d63f --- /dev/null +++ b/packages/wallet/src/test/fixtures/gql/activities/tokens.ts @@ -0,0 +1,56 @@ +import { + Currency, + TokenApproval, + TokenStandard, + TokenTransfer, + TransactionDirection, +} from 'wallet/src/data/__generated__/types-and-hooks' +import { amount } from 'wallet/src/test/fixtures/gql/amounts' +import { daiToken, ethToken } from 'wallet/src/test/fixtures/gql/assets' +import { faker } from 'wallet/src/test/shared' +import { createFixture, randomEnumValue } from 'wallet/src/test/utils' + +/** + * Base fixtures + */ + +export const tokenApproval = createFixture()(() => ({ + __typename: 'TokenApproval', + id: faker.datatype.uuid(), + approvedAddress: faker.finance.ethereumAddress(), + quantity: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }).toString(), + asset: ethToken(), + tokenStandard: randomEnumValue(TokenStandard), +})) + +export const tokenTransfer = createFixture()(() => ({ + __typename: 'TokenTransfer', + id: faker.datatype.uuid(), + asset: ethToken(), + direction: randomEnumValue(TransactionDirection), + quantity: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }).toString(), + recipient: faker.finance.ethereumAddress(), + sender: faker.finance.ethereumAddress(), + tokenStandard: randomEnumValue(TokenStandard), +})) + +/** + * Derived fixtures + */ + +export const erc20ApproveAssetChange = createFixture()(() => + tokenApproval({ asset: daiToken(), tokenStandard: TokenStandard.Erc20 }) +) + +export const erc20TokenTransferOut = createFixture()(() => + tokenTransfer({ + asset: daiToken(), + tokenStandard: TokenStandard.Erc20, + direction: TransactionDirection.Out, + transactedValue: amount({ value: 1, currency: Currency.Usd }), + }) +) + +export const erc20TransferIn = createFixture()(() => + erc20TokenTransferOut({ direction: TransactionDirection.In }) +) diff --git a/packages/wallet/src/test/fixtures/gql/amounts.ts b/packages/wallet/src/test/fixtures/gql/amounts.ts new file mode 100644 index 00000000000..bed6144a7c5 --- /dev/null +++ b/packages/wallet/src/test/fixtures/gql/amounts.ts @@ -0,0 +1,31 @@ +import { Amount, Currency, TimestampedAmount } from 'wallet/src/data/__generated__/types-and-hooks' +import { MAX_FIXTURE_TIMESTAMP, faker } from 'wallet/src/test/shared' +import { createFixture, randomEnumValue } from 'wallet/src/test/utils' + +export const amount = createFixture()(() => ({ + __typename: 'Amount', + id: faker.datatype.uuid(), + value: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }), + currency: randomEnumValue(Currency), +})) + +const usdAmountFactory = + (value: number) => + (currency = Currency.Usd): Amount => + amount({ value, currency }) + +export const amounts = { + none: usdAmountFactory(0), + xs: usdAmountFactory(0.05), + sm: usdAmountFactory(5), + md: usdAmountFactory(55), + lg: usdAmountFactory(5500), + xl: usdAmountFactory(500000), +} + +export const timestampedAmount = createFixture()(() => ({ + __typename: 'TimestampedAmount', + id: faker.datatype.uuid(), + timestamp: faker.datatype.number({ max: MAX_FIXTURE_TIMESTAMP }), + value: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }), +})) diff --git a/packages/wallet/src/test/fixtures/gql/assets/index.ts b/packages/wallet/src/test/fixtures/gql/assets/index.ts new file mode 100644 index 00000000000..776b36b4145 --- /dev/null +++ b/packages/wallet/src/test/fixtures/gql/assets/index.ts @@ -0,0 +1,2 @@ +export * from './nfts' +export * from './tokens' diff --git a/packages/wallet/src/test/fixtures/gql/assets/nfts.ts b/packages/wallet/src/test/fixtures/gql/assets/nfts.ts new file mode 100644 index 00000000000..14a299a35b8 --- /dev/null +++ b/packages/wallet/src/test/fixtures/gql/assets/nfts.ts @@ -0,0 +1,65 @@ +import { + NftAsset, + NftAssetTrait, + NftCollection, + NftContract, +} from 'wallet/src/data/__generated__/types-and-hooks' +import { GQL_CHAINS, image } from 'wallet/src/test/fixtures/gql/misc' +import { faker } from 'wallet/src/test/shared' +import { createArray, createFixture, randomChoice } from 'wallet/src/test/utils' + +/** + * Base fixtures + */ + +export const nftAsset = createFixture()(() => ({ + __typename: 'NftAsset', + id: faker.datatype.uuid(), + tokenId: faker.datatype.uuid(), +})) + +export const nftAssetTrait = createFixture()(() => ({ + __typename: 'NftAssetTrait', + id: faker.datatype.uuid(), + name: faker.lorem.word(), + value: faker.lorem.word(), +})) + +export const nftContract = createFixture()(() => ({ + __typename: 'NftContract', + id: faker.datatype.uuid(), + chain: randomChoice(GQL_CHAINS), + address: faker.finance.ethereumAddress(), +})) + +type NftCollectionOptions = { + contractsCount: number +} + +export const nftCollection = createFixture({ + contractsCount: 2, +})(({ contractsCount }) => ({ + __typename: 'NftCollection', + id: faker.datatype.uuid(), + name: faker.lorem.word(), + collectionId: faker.datatype.uuid(), + isVerified: faker.datatype.boolean(), + nftContracts: createArray(contractsCount, nftContract), + image: image(), +})) + +/** + * Static fixtures + */ + +export const NFT_ASSET_TRAIT = nftAssetTrait({ + name: 'traitName', + value: 'traitValue', +}) + +export const NFT_COLLECTION = nftCollection({ + nftContracts: [nftContract()], + name: 'Test NFT 1', + image: image({ url: 'image.url' }), + isVerified: true, +}) diff --git a/packages/wallet/src/test/fixtures/gql/assets/tokens.ts b/packages/wallet/src/test/fixtures/gql/assets/tokens.ts new file mode 100644 index 00000000000..d486cd03305 --- /dev/null +++ b/packages/wallet/src/test/fixtures/gql/assets/tokens.ts @@ -0,0 +1,175 @@ +import { Token as SDKToken } from '@uniswap/sdk-core' +import { + Currency, + HistoryDuration, + PriceSource, + SafetyLevel, + TimestampedAmount, + Token, + TokenBalance, + TokenMarket, + TokenProject, + TokenProjectMarket, +} from 'wallet/src/data/__generated__/types-and-hooks' +import { toGraphQLChain } from 'wallet/src/features/chains/utils' +import { amounts } from 'wallet/src/test/fixtures/gql/amounts' +import { + get24hPriceChange, + getLatestPrice, + priceHistory, +} from 'wallet/src/test/fixtures/gql/history' +import { GQL_CHAINS } from 'wallet/src/test/fixtures/gql/misc' +import { + DAI, + ETH, + USDBC_BASE, + USDC, + USDC_ARBITRUM, + USDC_OPTIMISM, + USDC_POLYGON, + WETH, +} from 'wallet/src/test/fixtures/lib' +import { MAX_FIXTURE_TIMESTAMP, faker } from 'wallet/src/test/shared' +import { createFixture, randomChoice, randomEnumValue } from 'wallet/src/test/utils' + +/** + * Base fixtures + */ + +type TokenOptions = { + sdkToken: SDKToken | null +} + +export const token = createFixture({ sdkToken: null })(({ sdkToken }) => ({ + __typename: 'Token', + id: faker.datatype.uuid(), + name: sdkToken?.name ?? faker.lorem.word(), + symbol: sdkToken?.symbol ?? faker.lorem.word(), + decimals: sdkToken?.decimals ?? faker.datatype.number({ min: 1, max: 18 }), + chain: (sdkToken ? toGraphQLChain(sdkToken.chainId) : null) ?? randomChoice(GQL_CHAINS), + address: sdkToken?.address.toLocaleLowerCase() ?? faker.finance.ethereumAddress(), + market: null, + project: tokenProjectBase(), +})) + +export const tokenBalance = createFixture()(() => ({ + __typename: 'TokenBalance', + id: faker.datatype.uuid(), + blockNumber: faker.datatype.number({ max: 1000000 }), + blockTimestamp: faker.datatype.number({ max: MAX_FIXTURE_TIMESTAMP }), + denominatedValue: amounts.md(), + isHidden: faker.datatype.boolean(), + ownerAddress: faker.finance.ethereumAddress(), + quantity: faker.datatype.number({ min: 1, max: 1000 }), + token: token(), +})) + +type TokenMarketOptions = { + priceHistory: (TimestampedAmount | null)[] +} + +export const tokenMarket = createFixture(() => ({ + priceHistory: priceHistory({ duration: HistoryDuration.Week, size: 7 }), +}))(({ priceHistory: history }) => ({ + __typename: 'TokenMarket', + id: faker.datatype.uuid(), + token: ethToken(), + priceSource: randomEnumValue(PriceSource), + priceHistory: history, + price: history ? getLatestPrice(history) : null, + pricePercentChange: history ? get24hPriceChange(history) : null, +})) + +type TokenProjectMarketOptions = { + priceHistory: (TimestampedAmount | null)[] +} + +export const tokenProjectMarket = createFixture( + () => ({ + priceHistory: priceHistory({ duration: HistoryDuration.Week, size: 7 }), + }) +)(({ priceHistory: history }) => ({ + __typename: 'TokenProjectMarket', + id: faker.datatype.uuid(), + priceHistory: history, + price: getLatestPrice(history), + pricePercentChange24h: get24hPriceChange(history), + currency: randomEnumValue(Currency), + tokenProject: tokenProjectBase(), +})) + +const tokenProjectBase = createFixture()(() => ({ + __typename: 'TokenProject', + id: faker.datatype.uuid(), + name: faker.lorem.word(), + tokens: [] as Token[], + safetyLevel: randomEnumValue(SafetyLevel), + logoUrl: faker.image.imageUrl(), + isSpam: faker.datatype.boolean(), +})) + +type TokenProjectOptions = { + priceHistory: (TimestampedAmount | null)[] +} + +export const tokenProject = createFixture(() => ({ + priceHistory: priceHistory({ duration: HistoryDuration.Week, size: 7 }), +}))(({ priceHistory: history }) => ({ + ...tokenProjectBase({ + markets: [tokenProjectMarket({ priceHistory: history })], + }), +})) + +export const usdcTokenProject = createFixture(() => ({ + priceHistory: priceHistory({ duration: HistoryDuration.Week, size: 7 }), +}))(({ priceHistory: history }) => + tokenProject({ + priceHistory: history, + tokens: [ + token({ sdkToken: USDC, market: tokenMarket() }), + token({ sdkToken: USDC_POLYGON }), + token({ sdkToken: USDC_ARBITRUM }), + token({ sdkToken: USDBC_BASE, market: tokenMarket() }), + token({ sdkToken: USDC_OPTIMISM }), + ], + }) +) + +/** + * Derived fixtures + */ + +const ethProject = tokenProject({ + name: 'Ethereum', + safetyLevel: SafetyLevel.Verified, + isSpam: false, +}) + +export const ethToken = createFixture()(() => token({ sdkToken: ETH, project: ethProject })) +export const wethToken = createFixture()(() => + token({ sdkToken: WETH, project: ethProject }) +) + +const daiProject = tokenProject({ + name: 'Dai Stablecoin', + safetyLevel: SafetyLevel.Verified, + isSpam: false, +}) + +export const daiToken = createFixture()(() => token({ sdkToken: DAI, project: daiProject })) + +const usdcProject = tokenProject({ + name: 'USD Coin', + safetyLevel: SafetyLevel.Verified, + isSpam: false, +}) + +export const usdcToken = createFixture()(() => + token({ sdkToken: USDC, project: usdcProject }) +) +export const usdcBaseToken = createFixture()(() => + token({ sdkToken: USDBC_BASE, project: usdcProject }) +) +export const usdcArbitrumToken = createFixture()(() => + token({ sdkToken: USDC_ARBITRUM, project: usdcProject }) +) diff --git a/packages/wallet/src/test/fixtures/gql/history.ts b/packages/wallet/src/test/fixtures/gql/history.ts new file mode 100644 index 00000000000..01ca9c40c23 --- /dev/null +++ b/packages/wallet/src/test/fixtures/gql/history.ts @@ -0,0 +1,75 @@ +import { + Amount, + HistoryDuration, + TimestampedAmount, +} from 'wallet/src/data/__generated__/types-and-hooks' +import { amount, timestampedAmount } from 'wallet/src/test/fixtures/gql/amounts' +import { faker } from 'wallet/src/test/shared' +import { createArray, createFixture, randomEnumValue } from 'wallet/src/test/utils' + +/** + * Constants + */ + +export const hourMs = 60 * 60 * 1000 +export const dayMs = 24 * hourMs +export const weekMs = 7 * dayMs +export const monthMs = 30 * dayMs +export const yearMs = 365 * dayMs + +export const historyDurationsMs: Record = { + [HistoryDuration.Hour]: hourMs, + [HistoryDuration.Day]: dayMs, + [HistoryDuration.Week]: weekMs, + [HistoryDuration.Month]: monthMs, + [HistoryDuration.Year]: yearMs, + [HistoryDuration.Max]: 5 * yearMs, +} + +/** + * Base fixtures + */ + +type PriceHistoryOptions = { + duration: HistoryDuration + size: number +} + +export const priceHistory = createFixture(() => ({ + duration: randomEnumValue(HistoryDuration), + size: faker.datatype.number({ min: 10, max: 20 }), +}))(({ size, duration }) => { + const durationMs = historyDurationsMs[duration] + const endDate = durationMs + faker.date.past().getMilliseconds() + const startDate = endDate - durationMs + + return createArray(size, (i) => + timestampedAmount({ + // Timestamp in seconds + timestamp: Math.floor((startDate + (endDate - startDate) * (i / size)) / 1000), + }) + ) as TimestampedAmount[] // Simplify type +}) + +/** + * Helper functions + */ + +export const getLatestPrice = (history: Maybe[]): Amount => { + const filteredHistory = history.filter((item) => item !== null) as TimestampedAmount[] + return amount({ value: filteredHistory[filteredHistory.length - 1]?.value ?? 0 }) +} + +export const get24hPriceChange = (history: Maybe[]): Amount => { + const price = history[history.length - 1]?.value ?? 0 + const prevPrice = history[history.length - 2]?.value ?? 0 + const priceTimestamp = history[history.length - 1]?.timestamp ?? 0 + const prevPriceTimestamp = history[history.length - 2]?.timestamp ?? 0 + + const timeDiff = priceTimestamp - prevPriceTimestamp + const priceDiff = price - prevPrice + + const dayPriceDiff = timeDiff > 0 ? priceDiff * (dayMs / timeDiff) * 100 : 0 + + return amount({ value: prevPrice > 0 ? dayPriceDiff / prevPrice : 0 }) +} diff --git a/packages/wallet/src/test/fixtures/gql/index.ts b/packages/wallet/src/test/fixtures/gql/index.ts new file mode 100644 index 00000000000..de3abec7d6b --- /dev/null +++ b/packages/wallet/src/test/fixtures/gql/index.ts @@ -0,0 +1,6 @@ +export * from './activities' +export * from './amounts' +export * from './history' +export * from './misc' +export * from './portfolio' +export * from './transactions' diff --git a/packages/wallet/src/test/fixtures/gql/misc.ts b/packages/wallet/src/test/fixtures/gql/misc.ts new file mode 100644 index 00000000000..460648b1395 --- /dev/null +++ b/packages/wallet/src/test/fixtures/gql/misc.ts @@ -0,0 +1,19 @@ +import { Chain, Image } from 'wallet/src/data/__generated__/types-and-hooks' +import { faker } from 'wallet/src/test/shared' +import { createFixture } from 'wallet/src/test/utils' + +export const GQL_CHAINS = [ + Chain.Ethereum, + Chain.Arbitrum, + Chain.EthereumGoerli, + Chain.Optimism, + Chain.Polygon, + Chain.Base, + Chain.Bnb, +] + +export const image = createFixture()(() => ({ + __typename: 'Image', + id: faker.datatype.uuid(), + url: faker.image.imageUrl(), +})) diff --git a/packages/wallet/src/test/fixtures/gql/portfolio.ts b/packages/wallet/src/test/fixtures/gql/portfolio.ts new file mode 100644 index 00000000000..b32169c4469 --- /dev/null +++ b/packages/wallet/src/test/fixtures/gql/portfolio.ts @@ -0,0 +1,38 @@ +import { Portfolio } from 'wallet/src/data/__generated__/types-and-hooks' +import { assetActivity } from 'wallet/src/test/fixtures/gql/activities' +import { amount } from 'wallet/src/test/fixtures/gql/amounts' +import { tokenBalance } from 'wallet/src/test/fixtures/gql/assets' +import { faker } from 'wallet/src/test/shared' +import { createArray, createFixture } from 'wallet/src/test/utils' + +type PortfolioOptions = { + activitiesCount: number + tokenBalancesCount: number +} + +export const portfolio = createFixture({ + activitiesCount: 2, + tokenBalancesCount: 2, +})(({ tokenBalancesCount, activitiesCount }) => ({ + __typename: 'Portfolio', + id: faker.datatype.uuid(), + ownerAddress: faker.finance.ethereumAddress(), + // Optional properties based on token balances count + ...(tokenBalancesCount > 0 + ? { + tokensTotalDenominatedValue: amount(), + tokensTotalDenominatedValueChange: { + id: faker.datatype.uuid(), + absolute: amount(), + percentage: amount(), + }, + tokenBalances: createArray(tokenBalancesCount, tokenBalance), + } + : {}), + // Optional properties based on activitiesCount + ...(activitiesCount + ? { + assetActivities: createArray(activitiesCount, assetActivity), + } + : {}), +})) diff --git a/packages/wallet/src/test/fixtures/gql/transactions.ts b/packages/wallet/src/test/fixtures/gql/transactions.ts new file mode 100644 index 00000000000..6d46f9999c0 --- /dev/null +++ b/packages/wallet/src/test/fixtures/gql/transactions.ts @@ -0,0 +1,43 @@ +import { + AssetChange, + Transaction, + TransactionDetails, + TransactionStatus, + TransactionType, +} from 'wallet/src/data/__generated__/types-and-hooks' +import { faker } from 'wallet/src/test/shared' +import { createFixture, randomEnumValue } from 'wallet/src/test/utils' + +export const gqlTransaction = createFixture()(() => ({ + __typename: 'Transaction', + id: faker.datatype.uuid(), + hash: faker.datatype.uuid(), + blockNumber: faker.datatype.number(), + from: faker.finance.ethereumAddress(), + to: faker.finance.ethereumAddress(), + nonce: faker.datatype.number(), + status: randomEnumValue(TransactionStatus), +})) + +type TransactionDetailsBaseOptions = { + transactionStatus: TransactionStatus +} + +export const gqlTransactionDetails = createFixture< + TransactionDetails, + TransactionDetailsBaseOptions +>({ + transactionStatus: randomEnumValue(TransactionStatus), +})(({ transactionStatus }) => ({ + __typename: 'TransactionDetails', + id: faker.datatype.uuid(), + hash: faker.datatype.uuid(), + from: faker.finance.ethereumAddress(), + to: faker.finance.ethereumAddress(), + nonce: faker.datatype.number(), + /** @deprecated use transactionStatus to disambiguate from swapOrderStatus */ + status: transactionStatus, + transactionStatus, + type: randomEnumValue(TransactionType), + assetChanges: [] as AssetChange[], +})) diff --git a/packages/wallet/src/test/fixtures/index.ts b/packages/wallet/src/test/fixtures/index.ts new file mode 100644 index 00000000000..4ac06fd041a --- /dev/null +++ b/packages/wallet/src/test/fixtures/index.ts @@ -0,0 +1,6 @@ +export * from './constants' +export * from './events' +export * from './gql' +export * from './gql/assets' +export * from './lib' +export * from './wallet' diff --git a/packages/wallet/src/test/fixtures/lib/ethers.ts b/packages/wallet/src/test/fixtures/lib/ethers.ts new file mode 100644 index 00000000000..337731a33d3 --- /dev/null +++ b/packages/wallet/src/test/fixtures/lib/ethers.ts @@ -0,0 +1,51 @@ +import { + TransactionReceipt, + TransactionRequest, + TransactionResponse, +} from '@ethersproject/providers' +import { BigNumber, Transaction } from 'ethers' +import { faker } from 'wallet/src/test/shared' +import { createFixture } from 'wallet/src/test/utils' + +export const ethersTransaction = createFixture()(() => ({ + chainId: faker.datatype.number(), + data: faker.datatype.uuid(), + nonce: faker.datatype.number(), + gasLimit: BigNumber.from(faker.datatype.number()), + value: BigNumber.from(faker.datatype.number()), +})) + +export const ethersTransactionReceipt = createFixture()(() => ({ + to: faker.finance.ethereumAddress(), + from: faker.finance.ethereumAddress(), + contractAddress: faker.finance.ethereumAddress(), + transactionIndex: faker.datatype.number(), + gasUsed: BigNumber.from(faker.datatype.number()), + logsBloom: faker.datatype.uuid(), + blockHash: faker.datatype.uuid(), + transactionHash: faker.datatype.uuid(), + logs: [], + blockNumber: faker.datatype.number(), + confirmations: faker.datatype.number(), + cumulativeGasUsed: BigNumber.from(faker.datatype.number()), + effectiveGasPrice: BigNumber.from(faker.datatype.number()), + byzantium: faker.datatype.boolean(), + type: faker.datatype.number(), +})) + +export const ethersTransactionRequest = createFixture()(() => ({ + from: faker.finance.ethereumAddress(), + to: faker.finance.ethereumAddress(), + value: faker.datatype.number().toString(), + data: faker.datatype.uuid(), + nonce: BigNumber.from(faker.datatype.number()), + gasPrice: faker.datatype.number().toString(), +})) + +export const ethersTransactionResponse = createFixture()(() => ({ + ...ethersTransaction(), + hash: faker.datatype.uuid(), + confirmations: faker.datatype.number(), + from: faker.finance.ethereumAddress(), + wait: (): Promise => Promise.resolve(ethersTransactionReceipt()), +})) diff --git a/packages/wallet/src/test/fixtures/lib/index.ts b/packages/wallet/src/test/fixtures/lib/index.ts new file mode 100644 index 00000000000..0bc1077ff8d --- /dev/null +++ b/packages/wallet/src/test/fixtures/lib/index.ts @@ -0,0 +1,3 @@ +export * from './ethers' +export * from './netinfo' +export * from './sdk' diff --git a/packages/wallet/src/test/fixtures/lib/netinfo.ts b/packages/wallet/src/test/fixtures/lib/netinfo.ts new file mode 100644 index 00000000000..889854351bc --- /dev/null +++ b/packages/wallet/src/test/fixtures/lib/netinfo.ts @@ -0,0 +1,28 @@ +import { + NetInfoConnectedStates, + NetInfoNoConnectionState, + NetInfoStateType, + NetInfoUnknownState, +} from '@react-native-community/netinfo' +import { createFixture } from 'wallet/src/test/utils' + +export const networkUnknown = createFixture()(() => ({ + isConnected: null, + type: NetInfoStateType.unknown, + isInternetReachable: null, + details: null, +})) + +export const networkDown = createFixture()(() => ({ + isConnected: false, + type: NetInfoStateType.none, + isInternetReachable: false, + details: null, +})) + +export const networkUp = createFixture()(() => ({ + isConnected: true, + type: NetInfoStateType.other, + isInternetReachable: true, + details: { isConnectionExpensive: false }, +})) diff --git a/packages/wallet/src/test/fixtures/lib/sdk.ts b/packages/wallet/src/test/fixtures/lib/sdk.ts new file mode 100644 index 00000000000..95dc0d7176d --- /dev/null +++ b/packages/wallet/src/test/fixtures/lib/sdk.ts @@ -0,0 +1,122 @@ +import { Token } from '@uniswap/sdk-core' +import { getWrappedNativeAddress } from 'wallet/src/constants/addresses' +import { ChainId } from 'wallet/src/constants/chains' + +export const ETH = new Token( + ChainId.Mainnet, + '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + 18, + 'ETH', + 'Ethereum' +) + +export const WETH = new Token( + ChainId.Mainnet, + getWrappedNativeAddress(ChainId.Mainnet), + 18, + 'WETH', + 'Wrapped Ether' +) + +export const DAI = new Token( + ChainId.Mainnet, + '0x6b175474e89094c44da98b954eedeac495271d0f', + 18, + 'DAI', + 'Dai Stablecoin' +) + +export const DAI_ARBITRUM_ONE = new Token( + ChainId.ArbitrumOne, + '0xda10009cbd5d07dd0cecc66161fc93d7c9000da1', + 18, + 'DAI', + 'Dai stable coin' +) + +export const USDC = new Token( + ChainId.Mainnet, + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + 6, + 'USDC', + 'USD//C' +) + +export const USDC_ARBITRUM = new Token( + ChainId.ArbitrumOne, + '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', + 6, + 'USDC', + 'USD//C' +) + +export const USDBC_BASE = new Token( + ChainId.Base, + '0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca', + 6, + 'USDbC', + 'USD Base Coin' +) + +export const USDC_OPTIMISM = new Token( + ChainId.Optimism, + '0x7f5c764cbc14f9669b88837ca1490cca17c31607', + 6, + 'USDC', + 'USD//C' +) + +export const USDC_POLYGON = new Token( + ChainId.Polygon, + '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', + 6, + 'USDC', + 'USD//C' +) + +export const USDC_GOERLI = new Token( + ChainId.Polygon, + '0x07865c6e87b9f70255377e024ace6630c1eaa37f', + 6, + 'USDC', + 'USD//C' +) + +export const USDT = new Token( + ChainId.Mainnet, + '0xdac17f958d2ee523a2206206994597c13d831ec7', + 6, + 'USDT', + 'Tether USD' +) + +export const USDT_BNB = new Token( + ChainId.Bnb, + '0x55d398326f99059ff775485246999027b3197955', + 18, + 'USDT', + 'TetherUSD' +) + +export const WBTC = new Token( + ChainId.Mainnet, + '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', + 8, + 'WBTC', + 'Wrapped BTC' +) + +export const SDK_TOKENS = [ + ETH, + WETH, + DAI, + USDC, + USDC_ARBITRUM, + USDBC_BASE, + USDC_OPTIMISM, + USDC_POLYGON, + USDC_GOERLI, + USDT, + USDT_BNB, + WBTC, +] diff --git a/packages/wallet/src/test/fixtures/wallet/accounts.ts b/packages/wallet/src/test/fixtures/wallet/accounts.ts new file mode 100644 index 00000000000..abc31bcd7aa --- /dev/null +++ b/packages/wallet/src/test/fixtures/wallet/accounts.ts @@ -0,0 +1,48 @@ +import { + AccountBase, + AccountType, + BackupType, + ReadOnlyAccount, + SignerMnemonicAccount, +} from 'wallet/src/features/wallet/accounts/types' +import { SAMPLE_SEED_ADDRESS_1 } from 'wallet/src/test/fixtures/constants' +import { faker } from 'wallet/src/test/shared' +import { createFixture, randomEnumValue } from 'wallet/src/test/utils' + +/** + * Base fixtures + */ + +export const accountBase = createFixture()(() => ({ + type: randomEnumValue(AccountType), + address: faker.finance.ethereumAddress(), + timeImportedMs: faker.datatype.number(), + name: faker.name.fullName(), +})) + +export const signerMnemonicAccount = createFixture()(() => ({ + ...accountBase(), + type: AccountType.SignerMnemonic, + derivationIndex: faker.datatype.number(), + mnemonicId: faker.datatype.uuid(), + backups: [randomEnumValue(BackupType)], +})) + +export const readOnlyAccount = createFixture()(() => ({ + ...accountBase(), + type: AccountType.Readonly, +})) + +/** + * Static fixtures + */ + +export const ACCOUNT = signerMnemonicAccount({ + type: AccountType.SignerMnemonic, + address: SAMPLE_SEED_ADDRESS_1, + derivationIndex: 0, + name: 'Test Account', + timeImportedMs: 10, + mnemonicId: SAMPLE_SEED_ADDRESS_1, + backups: [BackupType.Cloud], +}) diff --git a/packages/wallet/src/test/fixtures/wallet/balances.ts b/packages/wallet/src/test/fixtures/wallet/balances.ts new file mode 100644 index 00000000000..a6da5f308aa --- /dev/null +++ b/packages/wallet/src/test/fixtures/wallet/balances.ts @@ -0,0 +1,79 @@ +import { Portfolio, TokenBalance } from 'wallet/src/data/__generated__/types-and-hooks' +import { fromGraphQLChain } from 'wallet/src/features/chains/utils' +import { PortfolioBalance } from 'wallet/src/features/dataApi/types' +import { buildCurrency } from 'wallet/src/features/dataApi/utils' +import { portfolio } from 'wallet/src/test/fixtures/gql' +import { currencyInfo } from 'wallet/src/test/fixtures/wallet/currencies' +import { faker } from 'wallet/src/test/shared' +import { createFixture } from 'wallet/src/test/utils' +import { currencyId } from 'wallet/src/utils/currencyId' + +const portfolioBalanceBase = createFixture()(() => ({ + cacheId: faker.datatype.uuid(), + quantity: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }), + balanceUSD: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }), + currencyInfo: currencyInfo(), + relativeChange24: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }), + isHidden: faker.datatype.boolean(), +})) + +type PortfolioBalanceOptions = { + from?: RequireNonNullable | null +} + +export const portfolioBalance = createFixture({ + from: null, +})(({ from: balance }) => { + if (!balance) { + return portfolioBalanceBase() + } + + const currency = buildCurrency({ + chainId: fromGraphQLChain(balance.token.chain), + address: balance.token.address, + decimals: balance.token.decimals, + symbol: balance.token.symbol, + name: balance.token.project?.name, + }) + + if (!currency) { + return portfolioBalanceBase() + } + + return { + cacheId: `${balance.__typename}:${balance.id}`, + quantity: balance.quantity, + balanceUSD: balance.denominatedValue?.value, + isHidden: balance.isHidden, + // This field is normally calculated dynamically. We cannot mock it in the + // fixture returned by the mocked resolver as it is ignored and replaced + // by randomly generated Amount mock. As a result, we expect any number here. + relativeChange24: expect.any(Number), + currencyInfo: { + currency, + currencyId: currencyId(currency), + logoUrl: balance.token.project?.logoUrl, + isSpam: balance.token.project?.isSpam, + safetyLevel: balance.token.project?.safetyLevel, + }, + } +}) + +type PortfolioBalancesOptions = { + portfolio: Portfolio +} + +export const portfolioBalances = createFixture({ + portfolio: portfolio(), +})( + ({ portfolio: { tokenBalances } }) => + (tokenBalances + ?.map((tokenBalance) => { + if (tokenBalance?.quantity && tokenBalance?.token) { + return portfolioBalance({ + from: tokenBalance as RequireNonNullable, + }) + } + }) + .filter(Boolean) as PortfolioBalance[]) ?? [] +) diff --git a/packages/wallet/src/test/fixtures/wallet/currencies.ts b/packages/wallet/src/test/fixtures/wallet/currencies.ts new file mode 100644 index 00000000000..c1c2f61536f --- /dev/null +++ b/packages/wallet/src/test/fixtures/wallet/currencies.ts @@ -0,0 +1,57 @@ +import { ChainId } from 'wallet/src/constants/chains' +import { SafetyLevel } from 'wallet/src/data/__generated__/types-and-hooks' +import { CurrencyInfo } from 'wallet/src/features/dataApi/types' +import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' +import { faker } from 'wallet/src/test/shared' +import { createFixture } from 'wallet/src/test/utils' +import { currencyId } from 'wallet/src/utils/currencyId' + +export const MAINNET_CURRENCY = NativeCurrency.onChain(ChainId.Mainnet) +export const BASE_CURRENCY = NativeCurrency.onChain(ChainId.Base) +export const ARBITRUM_CURRENCY = NativeCurrency.onChain(ChainId.ArbitrumOne) +export const OPTIMISM_CURRENCY = NativeCurrency.onChain(ChainId.Optimism) +export const POLYGON_CURRENCY = NativeCurrency.onChain(ChainId.Polygon) + +type CurrencyInfoOptions = { + nativeCurrency: NativeCurrency +} + +export const currencyInfo = createFixture({ + nativeCurrency: MAINNET_CURRENCY, +})(({ nativeCurrency }) => ({ + currencyId: currencyId(nativeCurrency), + currency: nativeCurrency, + logoUrl: faker.image.imageUrl(), + safetyLevel: SafetyLevel.Verified, +})) + +export const ethCurrencyInfo = createFixture()(() => + currencyInfo({ + nativeCurrency: MAINNET_CURRENCY, + logoUrl: 'https://token-icons.s3.amazonaws.com/eth.png', + }) +) + +export const uniCurrencyInfo = createFixture()(() => + currencyInfo({ + nativeCurrency: MAINNET_CURRENCY, + logoUrl: + 'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png', + }) +) + +export const daiCurrencyInfo = createFixture()(() => + currencyInfo({ + nativeCurrency: MAINNET_CURRENCY, + logoUrl: + 'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png', + }) +) + +export const arbitrumDaiCurrencyInfo = createFixture()(() => + currencyInfo({ + nativeCurrency: ARBITRUM_CURRENCY, + logoUrl: + 'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png', + }) +) diff --git a/packages/wallet/src/test/fixtures/wallet/index.ts b/packages/wallet/src/test/fixtures/wallet/index.ts new file mode 100644 index 00000000000..7bcadf1faa9 --- /dev/null +++ b/packages/wallet/src/test/fixtures/wallet/index.ts @@ -0,0 +1,7 @@ +export * from './accounts' +export * from './balances' +export * from './currencies' +export * from './notifications' +export * from './recipients' +export * from './transactions' +export * from './walletConnect' diff --git a/packages/wallet/src/test/fixtures/wallet/notifications.ts b/packages/wallet/src/test/fixtures/wallet/notifications.ts new file mode 100644 index 00000000000..5cccd9302bb --- /dev/null +++ b/packages/wallet/src/test/fixtures/wallet/notifications.ts @@ -0,0 +1,203 @@ +import { ChainId } from 'wallet/src/constants/chains' +import { AssetType } from 'wallet/src/entities/assets' +import { + AppErrorNotification, + AppNotificationBase, + AppNotificationDefault, + AppNotificationType, + ApproveTxNotification, + ChangeAssetVisibilityNotification, + ChooseCountryNotification, + CopyNotification, + CopyNotificationType, + ReceiveCurrencyTxNotification, + ReceiveNFTNotification, + ScantasticCompleteNotification, + SendCurrencyTxNotification, + SendNFTNotification, + SuccessNotification, + SwapNetworkNotification, + SwapPendingNotification, + SwapTxNotification, + TransactionNotificationBase, + TransferCurrencyPendingNotification, + TransferCurrencyTxNotificationBase, + TransferNFTNotificationBase, + WalletConnectNotification, + WrapTxNotification, +} from 'wallet/src/features/notifications/types' +import { + FinalizedTransactionStatus, + TransactionStatus, + TransactionType, + WrapType, +} from 'wallet/src/features/transactions/types' +import { WalletConnectEvent } from 'wallet/src/features/walletConnect/types' +import { currencyInfo } from 'wallet/src/test/fixtures/wallet/currencies' +import { faker } from 'wallet/src/test/shared' +import { createFixture, randomChoice, randomEnumValue } from 'wallet/src/test/utils' + +export const FINALIZED_TRANSACTION_STATUSES: FinalizedTransactionStatus[] = [ + TransactionStatus.Success, + TransactionStatus.Failed, + TransactionStatus.Canceled, + TransactionStatus.FailedCancel, +] + +const appNotificationBase = createFixture()(() => ({ + type: randomEnumValue(AppNotificationType), +})) + +export const appNotificationDefault = createFixture()(() => ({ + ...appNotificationBase(), + type: AppNotificationType.Default, + title: faker.lorem.words(), +})) + +export const appErrorNotification = createFixture()(() => ({ + ...appNotificationBase(), + type: AppNotificationType.Error, + errorMessage: faker.lorem.words(), +})) + +export const walletConnectNotification = createFixture()(() => ({ + ...appNotificationBase(), + type: AppNotificationType.WalletConnect, + event: randomEnumValue(WalletConnectEvent), + dappName: faker.lorem.words(), + imageUrl: faker.image.imageUrl(), +})) + +const transactionNotificationBase = createFixture()(() => ({ + ...appNotificationBase(), + type: AppNotificationType.Transaction, + txType: randomEnumValue(TransactionType), + txStatus: randomChoice(FINALIZED_TRANSACTION_STATUSES), + txHash: faker.datatype.uuid(), + txId: faker.datatype.uuid(), + chainId: randomEnumValue(ChainId), +})) + +export const approveTxNotification = createFixture()(() => ({ + ...transactionNotificationBase(), + txType: TransactionType.Approve, + tokenAddress: faker.finance.ethereumAddress(), + spender: faker.finance.ethereumAddress(), +})) + +export const swapTxNotification = createFixture()(() => ({ + ...transactionNotificationBase(), + txType: TransactionType.Swap, + inputCurrencyId: faker.datatype.uuid(), + outputCurrencyId: faker.datatype.uuid(), + inputCurrencyAmountRaw: faker.datatype.number().toString(), + outputCurrencyAmountRaw: faker.datatype.number().toString(), +})) + +export const wrapTxNotification = createFixture()(() => ({ + ...transactionNotificationBase(), + txType: TransactionType.Wrap, + currencyAmountRaw: faker.datatype.number().toString(), + unwrapped: faker.datatype.boolean(), +})) + +const transferCurrencyTxNotificationBase = createFixture()( + () => ({ + ...transactionNotificationBase(), + txType: randomChoice([TransactionType.Send, TransactionType.Receive]), + assetType: AssetType.Currency, + tokenAddress: faker.finance.ethereumAddress(), + currencyAmountRaw: faker.datatype.number().toString(), + }) +) + +export const sendCurrencyTxNotification = createFixture()(() => ({ + ...transferCurrencyTxNotificationBase(), + txType: TransactionType.Send, + recipient: faker.finance.ethereumAddress(), +})) + +export const receiveCurrencyTxNotification = createFixture()(() => ({ + ...transferCurrencyTxNotificationBase(), + txType: TransactionType.Receive, + sender: faker.finance.ethereumAddress(), +})) + +const transferNFTNotificationBase = createFixture()(() => ({ + ...transactionNotificationBase(), + txType: randomChoice([TransactionType.Send, TransactionType.Receive]), + assetType: randomChoice([AssetType.ERC1155, AssetType.ERC721]), + tokenAddress: faker.finance.ethereumAddress(), + tokenId: faker.datatype.uuid(), +})) + +export const sendNFTTxNotification = createFixture()(() => ({ + ...transferNFTNotificationBase(), + txType: TransactionType.Send, + recipient: faker.finance.ethereumAddress(), +})) + +export const receiveNFTNotification = createFixture()(() => ({ + ...transferNFTNotificationBase(), + txType: TransactionType.Receive, + sender: faker.finance.ethereumAddress(), +})) + +export const unknownTxNotification = createFixture()(() => ({ + ...transactionNotificationBase(), + txType: TransactionType.Unknown, +})) + +export const copyNotification = createFixture()(() => ({ + ...appNotificationBase(), + type: AppNotificationType.Copied, + copyType: randomEnumValue(CopyNotificationType), +})) + +export const successNotification = createFixture()(() => ({ + ...appNotificationBase(), + type: AppNotificationType.Success, + title: faker.lorem.words(), +})) + +export const swapNetworkNotification = createFixture()(() => ({ + ...appNotificationBase(), + type: AppNotificationType.SwapNetwork, + chainId: randomEnumValue(ChainId), +})) + +export const chooseCountryNotification = createFixture()(() => ({ + ...appNotificationBase(), + type: AppNotificationType.ChooseCountry, + countryName: faker.address.country(), + countryCode: faker.address.countryCode(), +})) + +export const changeAssetVisibilityNotifiation = createFixture()( + () => ({ + ...appNotificationBase(), + type: AppNotificationType.AssetVisibility, + visible: faker.datatype.boolean(), + assetName: faker.lorem.words(), + }) +) + +export const swapPendingNotification = createFixture()(() => ({ + ...appNotificationBase(), + type: AppNotificationType.SwapPending, + wrapType: randomEnumValue(WrapType), +})) + +export const transferCurrencyPendingNotification = + createFixture()(() => ({ + ...appNotificationBase(), + type: AppNotificationType.TransferCurrencyPending, + currencyInfo: currencyInfo(), + })) + +export const scantasticCompleteNotification = createFixture()( + () => ({ + ...appNotificationBase(), + type: AppNotificationType.ScantasticComplete, + }) +) diff --git a/packages/wallet/src/test/fixtures/wallet/recipients.ts b/packages/wallet/src/test/fixtures/wallet/recipients.ts new file mode 100644 index 00000000000..10adcdf56c8 --- /dev/null +++ b/packages/wallet/src/test/fixtures/wallet/recipients.ts @@ -0,0 +1,23 @@ +import { SectionListData } from 'react-native' +import { SearchableRecipient } from 'wallet/src/features/address/types' +import { faker } from 'wallet/src/test/shared' +import { createFixture } from 'wallet/src/test/utils' + +export const searchableRecipient = createFixture()(() => ({ + address: faker.finance.ethereumAddress(), + name: faker.name.fullName(), +})) + +type RecipientSectionOptions = { + addresses: string[] +} + +export const recipientSection = createFixture< + SectionListData, + RecipientSectionOptions +>(() => ({ + addresses: [faker.finance.ethereumAddress(), faker.finance.ethereumAddress()], +}))(({ addresses }) => ({ + title: faker.lorem.words(), + data: addresses.map((address) => searchableRecipient({ address })), +})) diff --git a/packages/wallet/src/test/fixtures/wallet/transactions/fixtures.ts b/packages/wallet/src/test/fixtures/wallet/transactions/fixtures.ts new file mode 100644 index 00000000000..127b1c01642 --- /dev/null +++ b/packages/wallet/src/test/fixtures/wallet/transactions/fixtures.ts @@ -0,0 +1,175 @@ +import { TransactionRequest } from '@ethersproject/providers' +import { TradeType } from '@uniswap/sdk-core' +import { ChainId } from 'wallet/src/constants/chains' +import { AssetType } from 'wallet/src/entities/assets' +import { finalizeTransaction } from 'wallet/src/features/transactions/slice' +import { + ApproveTransactionInfo, + BaseSwapTransactionInfo, + ConfirmedSwapTransactionInfo, + ExactInputSwapTransactionInfo, + ExactOutputSwapTransactionInfo, + FiatPurchaseTransactionInfo, + FinalizedTransactionDetails, + NFTApproveTransactionInfo, + NFTMintTransactionInfo, + NFTSummaryInfo, + NFTTradeTransactionInfo, + NFTTradeType, + ReceiveTokenTransactionInfo, + SendTokenTransactionInfo, + TransactionDetails, + TransactionId, + TransactionOptions, + TransactionReceipt, + TransactionStatus, + TransactionType, + UnknownTransactionInfo, + WCConfirmInfo, + WrapTransactionInfo, +} from 'wallet/src/features/transactions/types' +import { dappInfoWC } from 'wallet/src/test/fixtures/wallet/walletConnect' +import { faker } from 'wallet/src/test/shared' +import { createFixture, randomEnumValue } from 'wallet/src/test/utils' + +export const transactionId = createFixture()(() => ({ + id: faker.datatype.uuid(), + chainId: randomEnumValue(ChainId), +})) + +export const nftSummaryInfo = createFixture()(() => ({ + tokenId: faker.datatype.uuid(), + name: faker.lorem.words(), + collectionName: faker.lorem.words(), + imageURL: faker.image.imageUrl(), +})) + +export const approveTransactionInfo = createFixture()(() => ({ + type: TransactionType.Approve, + tokenAddress: faker.finance.ethereumAddress(), + spender: faker.finance.ethereumAddress(), +})) + +export const baseSwapTransactionInfo = createFixture()(() => ({ + type: TransactionType.Swap, + inputCurrencyId: faker.datatype.uuid(), + outputCurrencyId: faker.datatype.uuid(), +})) + +export const extractInputSwapTransactionInfo = createFixture()( + () => ({ + ...baseSwapTransactionInfo(), + tradeType: TradeType.EXACT_INPUT, + inputCurrencyAmountRaw: faker.datatype.number().toString(), + expectedOutputCurrencyAmountRaw: faker.datatype.number().toString(), + minimumOutputCurrencyAmountRaw: faker.datatype.number().toString(), + }) +) + +export const extractOutputSwapTransactionInfo = createFixture()( + () => ({ + ...baseSwapTransactionInfo(), + tradeType: TradeType.EXACT_OUTPUT, + outputCurrencyAmountRaw: faker.datatype.number().toString(), + expectedInputCurrencyAmountRaw: faker.datatype.number().toString(), + maximumInputCurrencyAmountRaw: faker.datatype.number().toString(), + }) +) + +export const confirmedSwapTransactionInfo = createFixture()(() => ({ + ...baseSwapTransactionInfo(), + inputCurrencyAmountRaw: faker.datatype.number().toString(), + outputCurrencyAmountRaw: faker.datatype.number().toString(), +})) + +export const wrapTransactionInfo = createFixture()(() => ({ + type: TransactionType.Wrap, + unwrapped: faker.datatype.boolean(), + currencyAmountRaw: faker.datatype.number().toString(), +})) + +export const sendTokenTransactionInfo = createFixture()(() => ({ + type: TransactionType.Send, + assetType: randomEnumValue(AssetType), + recipient: faker.finance.ethereumAddress(), + tokenAddress: faker.finance.ethereumAddress(), +})) + +export const receiveTokenTransactionInfo = createFixture()(() => ({ + type: TransactionType.Receive, + assetType: randomEnumValue(AssetType), + sender: faker.finance.ethereumAddress(), + tokenAddress: faker.finance.ethereumAddress(), + currencyAmountRaw: faker.datatype.number().toString(), +})) + +export const fiatPurchaseTransactionInfo = createFixture()(() => ({ + type: TransactionType.FiatPurchase, + syncedWithBackend: faker.datatype.boolean(), +})) + +export const nftMintTransactionInfo = createFixture()(() => ({ + type: TransactionType.NFTMint, + nftSummaryInfo: nftSummaryInfo(), +})) + +export const nftTradeTransactionInfo = createFixture()(() => ({ + type: TransactionType.NFTTrade, + nftSummaryInfo: nftSummaryInfo(), + purchaseCurrencyId: faker.datatype.uuid(), + purchaseCurrencyAmountRaw: faker.datatype.number().toString(), + tradeType: randomEnumValue(NFTTradeType), +})) + +export const nftApproveTransactionInfo = createFixture()(() => ({ + type: TransactionType.NFTApprove, + nftSummaryInfo: nftSummaryInfo(), + spender: faker.finance.ethereumAddress(), +})) + +export const wcConfirmInfo = createFixture()(() => ({ + type: TransactionType.WCConfirm, + dapp: dappInfoWC(), +})) + +export const unknownTransactionInfo = createFixture()(() => ({ + type: TransactionType.Unknown, +})) + +export const transactionOptions = createFixture()(() => ({ + request: {} as TransactionRequest, +})) + +export const transactionDetails = createFixture()(() => ({ + ...transactionId(), + from: faker.finance.ethereumAddress(), + typeInfo: approveTransactionInfo(), + status: randomEnumValue(TransactionStatus), + addedTime: faker.date.recent().getTime(), + options: transactionOptions(), +})) + +export const finalizedTransactionDetails = createFixture()(() => ({ + ...transactionDetails(), + hash: faker.datatype.uuid(), + // Successful by default + status: TransactionStatus.Success, + receipt: transactionReceipt(), +})) + +export const transactionReceipt = createFixture()(() => ({ + transactionIndex: faker.datatype.number(), + blockNumber: faker.datatype.number(), + blockHash: faker.datatype.uuid(), + confirmedTime: faker.date.recent().getTime(), + confirmations: faker.datatype.number(), + gasUsed: faker.datatype.number(), + effectiveGasPrice: faker.datatype.number(), +})) + +export const finalizedTransactionAction = createFixture>()( + () => ({ + payload: finalizedTransactionDetails(), + type: 'transactions/finalizeTransaction', + }) +) diff --git a/packages/wallet/src/test/fixtures/wallet/transactions/helpers.ts b/packages/wallet/src/test/fixtures/wallet/transactions/helpers.ts new file mode 100644 index 00000000000..d3460a75ebc --- /dev/null +++ b/packages/wallet/src/test/fixtures/wallet/transactions/helpers.ts @@ -0,0 +1,100 @@ +import { TransactionRequest, TransactionResponse } from '@ethersproject/providers' +import { BigNumber, providers } from 'ethers' +import { merge } from 'lodash' +import { finalizeTransaction } from 'wallet/src/features/transactions/slice' +import { + TransactionDetails, + TransactionReceipt, + TransactionStatus, +} from 'wallet/src/features/transactions/types' +import { + ethersTransactionReceipt, + ethersTransactionRequest, + ethersTransactionResponse, +} from 'wallet/src/test/fixtures/lib/ethers' +import { + finalizedTransactionAction, + finalizedTransactionDetails, + transactionDetails, + transactionReceipt, +} from 'wallet/src/test/fixtures/wallet/transactions/fixtures' +import { faker } from 'wallet/src/test/shared' + +type TxFixtures = { + txDetailsPending: T + txDetailsSuccess: T + txDetailsFailed: T + txRequest: TransactionRequest + txResponse: TransactionResponse + txTypeInfo: T['typeInfo'] + txReceipt: TransactionReceipt + ethersTxReceipt: providers.TransactionReceipt + finalizedTxAction: ReturnType +} + +export const getTxFixtures = (transaction?: T): TxFixtures => { + const txBase = merge( + {}, + transactionDetails({ + hash: faker.datatype.uuid(), + options: { + request: ethersTransactionRequest(), + }, + }), + transaction + ) + + // Transaction flow + // 1. Generate the pending version of the transaction + const txDetailsPending = transactionDetails({ ...txBase, status: TransactionStatus.Pending }) + + // 2. Generate the transaction receipt and response + const txReceipt = transactionReceipt() + const ethersTxReceipt = ethersTransactionReceipt({ + from: txDetailsPending.from, + to: txDetailsPending.options.request.to, + transactionHash: txDetailsPending.hash, + blockNumber: txReceipt.blockNumber, + confirmations: txReceipt.confirmations, + transactionIndex: txReceipt.transactionIndex, + gasUsed: BigNumber.from(txReceipt.gasUsed), + blockHash: txReceipt.blockHash, + cumulativeGasUsed: BigNumber.from(txReceipt.gasUsed), + effectiveGasPrice: BigNumber.from(txReceipt.effectiveGasPrice), + status: 1, // Must be non-zero for successful finalized transaction status + }) + + const txResponse = ethersTransactionResponse({ + hash: txDetailsPending.hash, + confirmations: txReceipt.confirmations, + from: txDetailsPending.from, + wait: () => Promise.resolve(ethersTxReceipt), + }) + + // 3. Create successful/failed transaction + const txDetailsSuccess = finalizedTransactionDetails({ + ...txDetailsPending, + status: TransactionStatus.Success, + receipt: txReceipt, + }) + const txDetailsFailed = finalizedTransactionDetails({ + ...txDetailsPending, + status: TransactionStatus.Failed, + }) + // 4. Generate finalized transaction action + const finalizedTxAction = finalizedTransactionAction({ + payload: txDetailsSuccess, + }) + + return { + txDetailsPending, + txDetailsSuccess, + ethersTxReceipt, + txDetailsFailed, + finalizedTxAction, + txReceipt, + txRequest: txBase.options.request, + txResponse, + txTypeInfo: txBase.typeInfo, + } +} diff --git a/packages/wallet/src/test/fixtures/wallet/transactions/index.ts b/packages/wallet/src/test/fixtures/wallet/transactions/index.ts new file mode 100644 index 00000000000..41289ca2786 --- /dev/null +++ b/packages/wallet/src/test/fixtures/wallet/transactions/index.ts @@ -0,0 +1,2 @@ +export * from './fixtures' +export * from './helpers' diff --git a/packages/wallet/src/test/fixtures/wallet/walletConnect.ts b/packages/wallet/src/test/fixtures/wallet/walletConnect.ts new file mode 100644 index 00000000000..626f5579e8b --- /dev/null +++ b/packages/wallet/src/test/fixtures/wallet/walletConnect.ts @@ -0,0 +1,18 @@ +import { DappInfoUwULink, DappInfoWC } from 'wallet/src/features/walletConnect/types' +import { faker } from 'wallet/src/test/shared' +import { createFixture } from 'wallet/src/test/utils' + +export const dappInfoWC = createFixture()(() => ({ + source: 'walletconnect', + name: faker.lorem.words(), + url: faker.internet.url(), + icon: faker.image.imageUrl(), +})) + +export const dappInfoUwULink = createFixture()(() => ({ + source: 'uwulink', + name: faker.lorem.words(), + url: faker.internet.url(), + icon: faker.image.imageUrl(), + chain_id: faker.datatype.number(), +})) diff --git a/packages/wallet/src/test/gqlFixtures.ts b/packages/wallet/src/test/gqlFixtures.ts deleted file mode 100644 index 03c7570a441..00000000000 --- a/packages/wallet/src/test/gqlFixtures.ts +++ /dev/null @@ -1,569 +0,0 @@ -/* eslint-disable max-lines */ -import { faker } from '@faker-js/faker' -import { NativeCurrency } from '@uniswap/sdk-core' -import { getWrappedNativeAddress } from 'wallet/src/constants/addresses' -import { ChainId } from 'wallet/src/constants/chains' -import { DAI, USDBC_BASE, USDC, USDC_ARBITRUM } from 'wallet/src/constants/tokens' -import { - Amount, - AssetActivity, - AssetChange, - Chain, - Currency, - HistoryDuration, - NftAssetTrait, - NftCollection, - Portfolio as PortfolioType, - PriceSource, - SafetyLevel, - SearchTokensQuery, - Token, - TokenApproval, - TokenBalance, - TokenMarket as TokenMarketType, - TokenProject as TokenProjectType, - TokenStandard, - TokenTransfer, - TransactionDetails, - TransactionDirection, - TransactionStatus, - TransactionType, -} from 'wallet/src/data/__generated__/types-and-hooks' -import { PortfolioBalance } from 'wallet/src/features/dataApi/types' -import { - ETH, - FAKER_SEED, - MAX_FIXTURE_TIMESTAMP, - SAMPLE_SEED_ADDRESS_1, - SAMPLE_SEED_ADDRESS_2, -} from 'wallet/src/test/fixtures' -import { - createTokenAsset, - createTokenBalance, - mockTokenPriceHistory, - mockTokenProject, -} from 'wallet/src/test/helpers' - -faker.seed(FAKER_SEED) - -export const Amounts: Record<'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl', Amount> = { - none: { - id: faker.datatype.uuid(), - value: 0, - currency: Currency.Usd, - }, - xs: { - id: faker.datatype.uuid(), - value: 0.05, - currency: Currency.Usd, - }, - sm: { - id: faker.datatype.uuid(), - value: 5, - currency: Currency.Usd, - }, - md: { - id: faker.datatype.uuid(), - value: 55, - currency: Currency.Usd, - }, - lg: { - id: faker.datatype.uuid(), - value: 5500, - currency: Currency.Usd, - }, - xl: { - id: faker.datatype.uuid(), - value: 500000, - currency: Currency.Usd, - }, -} - -export const EthAsset = createTokenAsset(ETH, Chain.Ethereum) -export const DaiAsset = createTokenAsset(DAI, Chain.Ethereum) -export const UsdBaseAsset = createTokenAsset(USDBC_BASE, Chain.Base) -export const UsdArbitrumAsset = createTokenAsset(USDC_ARBITRUM, Chain.Arbitrum) - -/** - * Must explicitly define the returned typename in order - * for MockedResponse to infer correct assetActivity response type. - */ - -type RequiredAssetActivity = Omit & { - details: TransactionDetails & { - assetChanges: (AssetChange & { __typename: 'TokenApproval' | 'TokenTransfer' })[] - } -} -type PortfolioWithActivityAndTokenBalances = Omit & { - assetActivities: RequiredAssetActivity[] - tokenBalances: TokenBalance[] -} - -const AssetActivityBase = { - __typeName: 'AssetActivity', - timestamp: faker.datatype.number({ max: MAX_FIXTURE_TIMESTAMP }), - chain: Chain.Ethereum, - details: { - __typename: 'TransactionDetails' as const, - id: 'base_tranaction_id', - status: TransactionStatus.Confirmed, - to: SAMPLE_SEED_ADDRESS_2, - from: SAMPLE_SEED_ADDRESS_1, - nonce: faker.datatype.number(), - blockNumber: 1, - assetChanges: [], - }, -} - -const Erc20TransferOutAssetChange: TokenTransfer & { __typename: 'TokenTransfer' } = { - __typename: 'TokenTransfer', - id: faker.datatype.uuid(), - asset: DaiAsset, - tokenStandard: TokenStandard.Erc20, - quantity: '1', - sender: SAMPLE_SEED_ADDRESS_1, - recipient: SAMPLE_SEED_ADDRESS_2, - direction: TransactionDirection.Out, - transactedValue: { - id: faker.datatype.uuid(), - currency: Currency.Usd, - value: 1, - }, -} - -const Erc20TransferInAssetChange: TokenTransfer & { __typename: 'TokenTransfer' } = { - ...Erc20TransferOutAssetChange, - __typename: 'TokenTransfer', - id: faker.datatype.uuid(), - direction: TransactionDirection.In, -} - -const Erc20ApproveAssetChange: TokenApproval & { __typename: 'TokenApproval' } = { - __typename: 'TokenApproval', - id: faker.datatype.uuid(), - asset: DaiAsset, - tokenStandard: TokenStandard.Erc20, - approvedAddress: SAMPLE_SEED_ADDRESS_2, - quantity: '1', -} - -const ApproveAssetActivity: RequiredAssetActivity = { - ...AssetActivityBase, - id: faker.datatype.uuid(), - details: { - ...AssetActivityBase.details, - hash: faker.finance.ethereumAddress(), // need unique ID - type: TransactionType.Approve, - assetChanges: [Erc20ApproveAssetChange], - transactionStatus: TransactionStatus.Confirmed, - }, -} - -export const Erc20SwapAssetActivity: RequiredAssetActivity = { - ...AssetActivityBase, - id: faker.datatype.uuid(), - details: { - ...AssetActivityBase.details, - hash: faker.finance.ethereumAddress(), // need unique ID - type: TransactionType.Swap, - assetChanges: [Erc20TransferInAssetChange, Erc20TransferOutAssetChange], - transactionStatus: TransactionStatus.Confirmed, - }, -} - -export const Erc20ReceiveAssetActivity: RequiredAssetActivity = { - ...AssetActivityBase, - id: faker.datatype.uuid(), - details: { - ...AssetActivityBase.details, - hash: faker.finance.ethereumAddress(), // need unique ID - type: TransactionType.Receive, - assetChanges: [Erc20TransferInAssetChange], - transactionStatus: TransactionStatus.Confirmed, - }, -} - -export const TokenBalances: [TokenBalance, TokenBalance] = [ - createTokenBalance(SAMPLE_SEED_ADDRESS_1, DaiAsset, true), - createTokenBalance(SAMPLE_SEED_ADDRESS_2, EthAsset, false), -] - -// These are with different chanins -export const TokenBalances2: [TokenBalance, TokenBalance] = [ - createTokenBalance(SAMPLE_SEED_ADDRESS_1, UsdBaseAsset, false), - createTokenBalance(SAMPLE_SEED_ADDRESS_2, UsdArbitrumAsset, true), -] - -export const Portfolios: [ - PortfolioWithActivityAndTokenBalances, - PortfolioWithActivityAndTokenBalances -] = [ - { - id: faker.datatype.uuid(), - ownerAddress: SAMPLE_SEED_ADDRESS_1, - tokensTotalDenominatedValue: Amounts.md, - tokensTotalDenominatedValueChange: { - id: faker.datatype.uuid(), - absolute: Amounts.sm, - percentage: Amounts.xs, - }, - tokenBalances: TokenBalances, - assetActivities: [ApproveAssetActivity, Erc20SwapAssetActivity], - }, - { - id: faker.datatype.uuid(), - ownerAddress: SAMPLE_SEED_ADDRESS_2, - tokensTotalDenominatedValue: Amounts.md, - tokensTotalDenominatedValueChange: { - id: faker.datatype.uuid(), - absolute: Amounts.sm, - percentage: Amounts.xs, - }, - tokenBalances: TokenBalances2, - assetActivities: [ApproveAssetActivity, Erc20SwapAssetActivity], - }, -] - -export const PortfoliosWithReceive: [PortfolioWithActivityAndTokenBalances] = [ - { - id: faker.datatype.uuid(), - ownerAddress: SAMPLE_SEED_ADDRESS_1, - tokensTotalDenominatedValue: Amounts.md, - tokensTotalDenominatedValueChange: { - id: faker.datatype.uuid(), - absolute: Amounts.sm, - percentage: Amounts.xs, - }, - tokenBalances: TokenBalances, - assetActivities: [Erc20ReceiveAssetActivity], - }, -] - -export const Portfolio = Portfolios[0] as PortfolioType -export const Portfolio2 = Portfolios[1] as PortfolioType - -export const PortfolioBalancesById: Record = { - '1-0x6b175474e89094c44da98b954eedeac495271d0f': { - cacheId: 'TokenBalance:d76790c0-657f-4c9c-929c-372e55d4874e', - quantity: 146, - balanceUSD: 55, - currencyInfo: { - currency: { - ...DAI, - address: DAI.address.toLocaleLowerCase(), - } as typeof DAI, - currencyId: '1-0x6b175474e89094c44da98b954eedeac495271d0f', - logoUrl: 'I%hYU9(rWW', - isSpam: false, - safetyLevel: SafetyLevel.Verified, - }, - relativeChange24: expect.any(Number), - isHidden: true, - }, - '1-0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee': { - cacheId: 'TokenBalance:da5cfdf1-6aa9-46a4-8164-b426920f017a', - quantity: 442, - balanceUSD: 55, - currencyInfo: { - currency: { - chainId: 1, - decimals: 18, - name: 'Ethereum', - symbol: 'ETH', - isNative: true, - isToken: false, - address: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - } as unknown as NativeCurrency, - safetyLevel: SafetyLevel.Verified, - currencyId: '1-0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - isSpam: false, - logoUrl: 't<|U8cUQlA', - }, - relativeChange24: expect.any(Number), - isHidden: false, - }, - '8453-0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca': { - balanceUSD: 55, - cacheId: 'TokenBalance:e645fd94-101e-49dc-8285-1f1ed567a9f0', - currencyInfo: { - currency: { - ...USDBC_BASE, - address: USDBC_BASE.address.toLocaleLowerCase(), - } as typeof USDBC_BASE, - currencyId: '8453-0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca', - isSpam: false, - logoUrl: '#_fELb,zS)', - safetyLevel: SafetyLevel.Verified, - }, - isHidden: false, - quantity: 63, - relativeChange24: expect.any(Number), - }, - '42161-0xff970a61a04b1ca14834a43f5de4533ebddb5cc8': { - balanceUSD: 55, - cacheId: 'TokenBalance:0a02bee6-7769-48dd-ba33-6c32c219f34b', - currencyInfo: { - currency: { - ...USDC_ARBITRUM, - address: USDC_ARBITRUM.address.toLocaleLowerCase(), - } as typeof USDC_ARBITRUM, - currencyId: '42161-0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', - isSpam: false, - logoUrl: '7$l = [ - { - __typename: 'Token', - address: faker.finance.ethereumAddress(), - chain: Chain.Arbitrum, - decimals: 18, - id: faker.datatype.uuid(), - project: { - __typename: 'TokenProject', - id: '1', - logoUrl: faker.image.imageUrl(), - name: 'Dai Stablecoin', - safetyLevel: SafetyLevel.Verified, - }, - symbol: 'DAI', - }, - { - __typename: 'Token', - address: faker.finance.ethereumAddress(), - chain: Chain.Ethereum, - decimals: 18, - id: faker.datatype.uuid(), - project: { - __typename: 'TokenProject', - id: '1', - logoUrl: faker.image.imageUrl(), - name: 'Dai Stablecoin', - safetyLevel: SafetyLevel.Verified, - }, - symbol: 'DAI', - }, - { - __typename: 'Token', - address: faker.finance.ethereumAddress(), - chain: Chain.Optimism, - decimals: 18, - id: faker.datatype.uuid(), - project: { - __typename: 'TokenProject', - id: '1', - logoUrl: faker.image.imageUrl(), - name: 'Dai Stablecoin', - safetyLevel: SafetyLevel.Verified, - }, - symbol: 'DAI', - }, - { - __typename: 'Token', - address: faker.finance.ethereumAddress(), - chain: Chain.Polygon, - decimals: 18, - id: faker.datatype.uuid(), - project: { - __typename: 'TokenProject', - id: '1', - logoUrl: faker.image.imageUrl(), - name: 'Dai Stablecoin', - safetyLevel: SafetyLevel.Verified, - }, - symbol: 'DAI', - }, - { - __typename: 'Token', - address: faker.finance.ethereumAddress(), - chain: Chain.Polygon, - decimals: 18, - id: faker.datatype.uuid(), - project: { - __typename: 'TokenProject', - id: '2', - logoUrl: faker.image.imageUrl(), - name: 'DIA', - safetyLevel: SafetyLevel.Verified, - }, - symbol: 'DIA', - }, - { - __typename: 'Token', - address: faker.finance.ethereumAddress(), - chain: Chain.Ethereum, - decimals: 18, - id: faker.datatype.uuid(), - project: { - __typename: 'TokenProject', - id: '3', - logoUrl: faker.image.imageUrl(), - name: 'DefiPulse Index', - safetyLevel: SafetyLevel.Verified, - }, - symbol: 'DPI', - }, - { - __typename: 'Token', - address: faker.finance.ethereumAddress(), - chain: Chain.Arbitrum, - decimals: 18, - id: faker.datatype.uuid(), - project: { - __typename: 'TokenProject', - id: '2', - logoUrl: faker.image.imageUrl(), - name: 'DIA', - safetyLevel: SafetyLevel.Verified, - }, - symbol: 'DIA', - }, - { - __typename: 'Token', - address: faker.finance.ethereumAddress(), - chain: Chain.Polygon, - decimals: 18, - id: faker.datatype.uuid(), - project: { - __typename: 'TokenProject', - id: '3', - logoUrl: faker.image.imageUrl(), - name: 'DefiPulse Index', - safetyLevel: SafetyLevel.Verified, - }, - symbol: 'DPI', - }, - { - __typename: 'Token', - address: faker.finance.ethereumAddress(), - chain: Chain.Optimism, - decimals: 18, - id: faker.datatype.uuid(), - project: { - __typename: 'TokenProject', - id: '4', - logoUrl: faker.image.imageUrl(), - name: 'Rai Reflex Index', - safetyLevel: SafetyLevel.Verified, - }, - symbol: 'RAI', - }, -] - -export const EthToken: Token = { - id: faker.datatype.uuid(), - address: faker.finance.ethereumAddress(), - chain: Chain.Ethereum, - decimals: 2, - symbol: 'ETH', - project: { - safetyLevel: SafetyLevel.Verified, - id: faker.datatype.uuid(), - logoUrl: faker.image.imageUrl(), - name: 'Ethereum', - tokens: [], - }, -} - -export const TopNFTCollections: [NftCollection, NftCollection] = [ - { - id: faker.datatype.uuid(), - name: 'Test NFT 1', - collectionId: faker.datatype.uuid(), - isVerified: true, - nftContracts: [ - { - id: faker.datatype.uuid(), - chain: Chain.Ethereum, - address: faker.finance.ethereumAddress(), - }, - ], - image: { - id: faker.datatype.uuid(), - url: 'image.url', - }, - }, - { - id: faker.datatype.uuid(), - name: 'Test NFT 2', - collectionId: faker.datatype.uuid(), - isVerified: true, - nftContracts: [ - { - id: faker.datatype.uuid(), - chain: Chain.Ethereum, - address: faker.finance.ethereumAddress(), - }, - ], - image: { - id: faker.datatype.uuid(), - url: 'image.url', - }, - }, -] - -export const TopTokens: [Token, Token] = [ - { - id: faker.datatype.uuid(), - address: getWrappedNativeAddress(ChainId.Mainnet), - chain: Chain.Ethereum, - decimals: 18, - symbol: 'WETH', - project: { - id: faker.datatype.uuid(), - isSpam: false, - logoUrl: faker.image.imageUrl(), - name: 'Wrapped Ether', - safetyLevel: SafetyLevel.Verified, - tokens: [], - }, - }, - { - id: faker.datatype.uuid(), - address: USDC.address, - chain: Chain.Ethereum, - decimals: USDC.decimals, - symbol: 'USDC', - project: { - id: faker.datatype.uuid(), - isSpam: true, - logoUrl: faker.image.imageUrl(), - name: 'USD Coin', - safetyLevel: SafetyLevel.Verified, - tokens: [], - }, - }, -] - -export const NFTTrait: NftAssetTrait = { - __typename: 'NftAssetTrait', - id: faker.datatype.uuid(), - name: 'traitName', - value: 'traitValue', -} - -export const TokenMarket: TokenMarketType = { - id: faker.datatype.uuid(), - priceSource: PriceSource.SubgraphV3, - token: EthToken, - price: Amounts.md, - pricePercentChange: Amounts.xs, -} diff --git a/packages/wallet/src/test/helpers.ts b/packages/wallet/src/test/helpers.ts deleted file mode 100644 index a108a840bd4..00000000000 --- a/packages/wallet/src/test/helpers.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { faker } from '@faker-js/faker' -import { Token as NativeToken } from '@uniswap/sdk-core' -import { - Chain, - Currency, - HistoryDuration, - PriceSource, - SafetyLevel, - TimestampedAmount, - Token, - TokenBalance, - TokenMarket, - TokenProject, -} from 'wallet/src/data/__generated__/types-and-hooks' -import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' -import { MAX_FIXTURE_TIMESTAMP, dayMs, historyDurationMs } from 'wallet/src/test/fixtures' -import { Amounts, EthToken } from 'wallet/src/test/gqlFixtures' - -export const mockTokenPriceHistory = ( - duration: HistoryDuration, - size = 10 -): TimestampedAmount[] => { - const durationMs = historyDurationMs[duration] - const endDate = durationMs + faker.date.past().getMilliseconds() - const startDate = endDate - durationMs - - const result: TimestampedAmount[] = [] - - for (let i = 0; i < size; i++) { - // Timestamp in seconds - const timestamp = Math.floor((startDate + (endDate - startDate) * (i / size)) / 1000) - - result.push({ - id: faker.datatype.uuid(), - timestamp, - value: faker.datatype.float({ min: 0, max: 1000, precision: 0.01 }), - }) - } - - return result -} - -export const mockTokenMarket = (token: Token, priceHistory: TimestampedAmount[]): TokenMarket => { - // Calculate price change for the last 24h - const price = priceHistory[priceHistory.length - 1]?.value ?? 0 - const prevPrice = priceHistory[priceHistory.length - 2]?.value ?? 0 - const priceTimestamp = priceHistory[priceHistory.length - 1]?.timestamp ?? 0 - const prevPriceTimestamp = priceHistory[priceHistory.length - 2]?.timestamp ?? 0 - - const timeDiff = priceTimestamp - prevPriceTimestamp - const priceDiff = price - prevPrice - - const dayPriceDiff = timeDiff > 0 ? priceDiff * (dayMs / timeDiff) * 100 : 0 - const percentChange24h = prevPrice > 0 ? dayPriceDiff / prevPrice : 0 - - return { - id: faker.datatype.uuid(), - priceHistory, - price: { - id: faker.datatype.uuid(), - value: price, - }, - pricePercentChange: { - id: faker.datatype.uuid(), - value: percentChange24h, - }, - priceSource: PriceSource.SubgraphV3, - token, - } -} - -export const mockTokenProject = (priceHistory: TimestampedAmount[]): TokenProject => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { __typename, pricePercentChange, ...market } = mockTokenMarket(EthToken, priceHistory) - - return { - id: faker.datatype.uuid(), - description: faker.lorem.sentence(), - logoUrl: faker.image.imageUrl(), - name: faker.lorem.word(), - safetyLevel: SafetyLevel.Verified, - tokens: [ - { - id: faker.datatype.uuid(), - address: faker.finance.ethereumAddress(), - chain: Chain.Ethereum, - decimals: 6, - symbol: faker.lorem.word(), - market, - }, - { - id: faker.datatype.uuid(), - address: faker.finance.ethereumAddress(), - chain: Chain.Arbitrum, - decimals: 6, - symbol: faker.lorem.word(), - }, - { - id: faker.datatype.uuid(), - address: faker.finance.ethereumAddress(), - chain: Chain.Optimism, - decimals: 6, - symbol: faker.lorem.word(), - }, - { - id: faker.datatype.uuid(), - address: faker.finance.ethereumAddress(), - chain: Chain.Polygon, - decimals: 6, - symbol: faker.lorem.word(), - }, - ], - markets: [ - { - ...market, - currency: Currency.Eth, - tokenProject: { id: faker.datatype.uuid(), tokens: [] }, - pricePercentChange24h: pricePercentChange, - }, - ], - } -} - -export const createTokenAsset = (currency: NativeCurrency | NativeToken, chain: Chain): Token => ({ - id: faker.datatype.uuid(), - name: currency.name, - symbol: currency.symbol, - decimals: currency.decimals, - chain, - address: currency.address, - project: { - id: faker.datatype.uuid(), - isSpam: false, - name: currency.name, - logoUrl: faker.datatype.string(), - tokens: [], - safetyLevel: SafetyLevel.Verified, - }, -}) - -export const createTokenBalance = ( - ownerAddress: string, - token: Token, - hidden = false -): TokenBalance => ({ - id: faker.datatype.uuid(), - blockNumber: 1, - blockTimestamp: faker.datatype.number({ max: MAX_FIXTURE_TIMESTAMP }), - denominatedValue: Amounts.md, - isHidden: hidden, - ownerAddress, - quantity: faker.datatype.number({ min: 1, max: 1000 }), - token, -}) diff --git a/packages/wallet/src/test/mocks/gql/index.ts b/packages/wallet/src/test/mocks/gql/index.ts new file mode 100644 index 00000000000..6f29423e243 --- /dev/null +++ b/packages/wallet/src/test/mocks/gql/index.ts @@ -0,0 +1 @@ +export * from './provider' diff --git a/packages/wallet/src/test/mocks/mocks.ts b/packages/wallet/src/test/mocks/gql/mocks.ts similarity index 82% rename from packages/wallet/src/test/mocks/mocks.ts rename to packages/wallet/src/test/mocks/gql/mocks.ts index 9f122456c03..93c3fb0ed6a 100644 --- a/packages/wallet/src/test/mocks/mocks.ts +++ b/packages/wallet/src/test/mocks/gql/mocks.ts @@ -1,21 +1,13 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ import { - Chain, Currency, SafetyLevel, SwapOrderStatus, TransactionStatus, } from 'wallet/src/data/__generated__/types-and-hooks' -import { FAKER_SEED, faker } from 'wallet/src/test/fixtures' - -faker.seed(FAKER_SEED) - -export const MAX_FIXTURE_TIMESTAMP = 1609459200 - -const randomEnumValue = >(enumObj: T): T[keyof T] => { - const values = Object.values(enumObj) - return values[Math.floor(Math.random() * values.length)] as T[keyof T] -} +import { GQL_CHAINS } from 'wallet/src/test/fixtures' +import { MAX_FIXTURE_TIMESTAMP, faker } from 'wallet/src/test/shared' +import { randomChoice, randomEnumValue } from 'wallet/src/test/utils' export const mocks = { TokenProject: { @@ -26,19 +18,19 @@ export const mocks = { safetyLevel: () => SafetyLevel.Verified, tokens: () => new Array(4), }, - Token: { - id: () => faker.datatype.uuid(), - address: () => faker.finance.ethereumAddress(), - chain: () => randomEnumValue(Chain), - decimals: () => 6, - symbol: () => faker.lorem.word(), - }, TokenProjectMarket: { currency: () => Currency.Eth, id: () => faker.datatype.uuid(), tokenProject: () => ({ id: faker.datatype.uuid(), tokens: [] }), priceHistory: () => new Array(2), }, + Token: { + id: () => faker.datatype.uuid(), + address: () => faker.finance.ethereumAddress(), + chain: () => randomChoice(GQL_CHAINS), + decimals: () => 6, + symbol: () => faker.lorem.word(), + }, Amount: { id: () => faker.datatype.uuid(), value: () => faker.datatype.number(), @@ -57,7 +49,7 @@ export const mocks = { }, AssetActivity: { timestamp: () => faker.datatype.number({ max: MAX_FIXTURE_TIMESTAMP }), - chain: () => randomEnumValue(Chain), + chain: () => randomChoice(GQL_CHAINS), }, TransactionDetails: { id: () => faker.datatype.uuid(), @@ -76,7 +68,7 @@ export const mocks = { }, ApplicationContract: { id: () => faker.datatype.uuid(), - chain: () => randomEnumValue(Chain), + chain: () => randomChoice(GQL_CHAINS), address: () => faker.finance.ethereumAddress(), }, NftCollection: { @@ -88,7 +80,7 @@ export const mocks = { }, NftContract: { id: () => faker.datatype.uuid(), - chain: () => randomEnumValue(Chain), + chain: () => randomChoice(GQL_CHAINS), address: () => faker.finance.ethereumAddress(), }, Image: { diff --git a/packages/wallet/src/test/mocks/provider.tsx b/packages/wallet/src/test/mocks/gql/provider.tsx similarity index 91% rename from packages/wallet/src/test/mocks/provider.tsx rename to packages/wallet/src/test/mocks/gql/provider.tsx index fc80f0d1533..8c47a6a6211 100644 --- a/packages/wallet/src/test/mocks/provider.tsx +++ b/packages/wallet/src/test/mocks/gql/provider.tsx @@ -10,9 +10,9 @@ import { Resolvers } from 'wallet/src/data/__generated__/types-and-hooks' import { setupWalletCache } from 'wallet/src/data/cache' import { getErrorLink, getRestLink } from 'wallet/src/data/links' import { mocks as defaultMocks } from './mocks' -import { resolvers as defaultResolvers } from './resolvers' +import { defaultResolvers } from './resolvers' -const GQL_SCHEMA_PATH = path.join(__dirname, '../../data/schema.graphql') +const GQL_SCHEMA_PATH = path.join(__dirname, '../../../data/schema.graphql') const baseSchema = loadSchemaSync(GQL_SCHEMA_PATH, { loaders: [new GraphQLFileLoader()] }) diff --git a/packages/wallet/src/test/mocks/resolvers.ts b/packages/wallet/src/test/mocks/gql/resolvers.ts similarity index 64% rename from packages/wallet/src/test/mocks/resolvers.ts rename to packages/wallet/src/test/mocks/gql/resolvers.ts index d6a585657b9..912e2cca1b0 100644 --- a/packages/wallet/src/test/mocks/resolvers.ts +++ b/packages/wallet/src/test/mocks/gql/resolvers.ts @@ -1,28 +1,16 @@ import { GraphQLJSON } from 'graphql-scalars' import { HistoryDuration, Resolvers } from 'wallet/src/data/__generated__/types-and-hooks' -import { - TokenProjectDay, - TokenProjectMonth, - TokenProjectWeek, - TokenProjectYear, -} from 'wallet/src/test/gqlFixtures' +import { priceHistory, tokenProject } from 'wallet/src/test/fixtures' -export const resolvers: Resolvers = { +export const defaultResolvers: Resolvers = { Query: { - tokenProjects: (parent, args, context, info) => { - // Select token project based on the duration - switch (info.variableValues.duration) { - case HistoryDuration.Year: - return [TokenProjectYear] - case HistoryDuration.Month: - return [TokenProjectMonth] - case HistoryDuration.Week: - return [TokenProjectWeek] - case HistoryDuration.Day: - default: - return [TokenProjectDay] - } - }, + tokenProjects: (parent, args, context, info) => [ + tokenProject({ + priceHistory: priceHistory({ + duration: (info.variableValues.duration as HistoryDuration) ?? HistoryDuration.Day, + }), + }), + ], }, AWSJSON: GraphQLJSON, ActivityDetails: { diff --git a/packages/wallet/src/test/mocks/index.ts b/packages/wallet/src/test/mocks/index.ts new file mode 100644 index 00000000000..4e7407ae5b1 --- /dev/null +++ b/packages/wallet/src/test/mocks/index.ts @@ -0,0 +1,5 @@ +export * from './gql' +export * from './providers' +export * from './redux' +export * from './sdk' +export * from './utils' diff --git a/packages/wallet/src/test/mocks/providers.ts b/packages/wallet/src/test/mocks/providers.ts new file mode 100644 index 00000000000..df825872752 --- /dev/null +++ b/packages/wallet/src/test/mocks/providers.ts @@ -0,0 +1,76 @@ +import { TransactionReceipt } from '@ethersproject/providers' +import { BigNumber, providers } from 'ethers' +import ERC20_ABI from 'uniswap/src/abis/erc20.json' +import { Erc20 } from 'uniswap/src/abis/types' +import WETH_ABI from 'uniswap/src/abis/weth.json' +import { getWrappedNativeAddress } from 'wallet/src/constants/addresses' +import { ChainId } from 'wallet/src/constants/chains' +import { DAI } from 'wallet/src/constants/tokens' +import { ContractManager } from 'wallet/src/features/contracts/ContractManager' +import { SignerManager } from 'wallet/src/features/wallet/signing/SignerManager' +import { ethersTransactionReceipt } from 'wallet/src/test/fixtures' + +export const signerManager = new SignerManager() + +export const provider = new providers.JsonRpcProvider() +export const providerManager = { + getProvider: (): typeof provider => provider, +} + +const mockFeeData = { + maxFeePerPrice: BigNumber.from('1000'), + maxPriorityFeePerGas: BigNumber.from('10000'), + gasPrice: BigNumber.from('10000'), +} + +type TxProvidersMocks = { + mockProvider: typeof provider + mockProviderManager: typeof providerManager +} + +export const getTxProvidersMocks = (txReceipt?: TransactionReceipt): TxProvidersMocks => { + const receipt = txReceipt ?? ethersTransactionReceipt() + + const mockProvider = { + getBalance: (): BigNumber => BigNumber.from('1000000000000000000'), + getGasPrice: (): BigNumber => BigNumber.from('100000000000'), + getTransactionCount: (): number => 1000, + estimateGas: (): BigNumber => BigNumber.from('30000'), + sendTransaction: (): { hash: string } => ({ hash: '0xabcdef' }), + detectNetwork: (): { name: string; chainId: ChainId } => ({ + name: 'mainnet', + chainId: 1, + }), + getTransactionReceipt: (): typeof receipt => receipt, + waitForTransaction: (): typeof receipt => receipt, + getFeeData: (): typeof mockFeeData => mockFeeData, + } + + const mockProviderManager = { + getProvider: (): typeof mockProvider => mockProvider, + } + + return { + mockProvider, + mockProviderManager, + } as unknown as TxProvidersMocks +} + +export const contractManager = new ContractManager() +contractManager.getOrCreateContract(ChainId.Goerli, DAI.address, provider, ERC20_ABI) +contractManager.getOrCreateContract( + ChainId.Goerli, + getWrappedNativeAddress(ChainId.Goerli), + provider, + WETH_ABI +) +export const tokenContract = contractManager.getContract(ChainId.Goerli, DAI.address) as Erc20 + +export const mockTokenContract = { + balanceOf: (): BigNumber => BigNumber.from('1000000000000000000'), + populateTransaction: {}, +} + +export const mockContractManager = { + getOrCreateContract: (): typeof mockTokenContract => mockTokenContract, +} diff --git a/packages/wallet/src/test/mocks/redux.ts b/packages/wallet/src/test/mocks/redux.ts new file mode 100644 index 00000000000..3a44823fd04 --- /dev/null +++ b/packages/wallet/src/test/mocks/redux.ts @@ -0,0 +1,19 @@ +import { PreloadedState } from 'redux' +import { Account } from 'wallet/src/features/wallet/accounts/types' +import { WalletState, initialWalletState } from 'wallet/src/features/wallet/slice' +import { signerMnemonicAccount } from 'wallet/src/test/fixtures' + +// Useful when passing in preloaded state where active account is required +export function mockWalletPreloadedState( + account?: Account +): PreloadedState<{ wallet: WalletState }> { + const acc = account ?? signerMnemonicAccount() + + return { + wallet: { + ...initialWalletState, + accounts: { [acc.address]: acc }, + activeAccountAddress: acc.address, + }, + } +} diff --git a/packages/wallet/src/test/mocks/sdk.ts b/packages/wallet/src/test/mocks/sdk.ts new file mode 100644 index 00000000000..8a36ce171d6 --- /dev/null +++ b/packages/wallet/src/test/mocks/sdk.ts @@ -0,0 +1,12 @@ +import { FeeAmount, Pool } from '@uniswap/v3-sdk' +import { ChainId } from 'wallet/src/constants/chains' +import { UNI, WBTC } from 'wallet/src/constants/tokens' + +export const mockPool = new Pool( + UNI[ChainId.Mainnet], + WBTC, + FeeAmount.HIGH, + '2437312313659959819381354528', + '10272714736694327408', + -69633 +) diff --git a/packages/wallet/src/test/utils.ts b/packages/wallet/src/test/mocks/utils.ts similarity index 98% rename from packages/wallet/src/test/utils.ts rename to packages/wallet/src/test/mocks/utils.ts index 19a975aec02..e2105c381c8 100644 --- a/packages/wallet/src/test/utils.ts +++ b/packages/wallet/src/test/mocks/utils.ts @@ -82,7 +82,7 @@ export const mockFiatConverter: LocalizationContextState = { }, } -export const MockLocalizationContext = { +export const mockLocalizationContext = { LocalizationContextProvider: ({ children }: PropsWithChildren): ReactNode => children, useLocalizationContext: (): LocalizationContextState => ({ convertFiatAmount: mockFiatConverter.convertFiatAmount, diff --git a/packages/wallet/src/test/render.tsx b/packages/wallet/src/test/render.tsx index 3e4722dec8d..b0bd1dd2d61 100644 --- a/packages/wallet/src/test/render.tsx +++ b/packages/wallet/src/test/render.tsx @@ -15,7 +15,7 @@ import { Resolvers } from 'wallet/src/data/__generated__/types-and-hooks' import { UnitagUpdaterContextProvider } from 'wallet/src/features/unitags/context' import { SharedProvider } from 'wallet/src/provider' import { sharedRootReducer, type SharedState } from 'wallet/src/state/reducer' -import { AutoMockedApolloProvider } from 'wallet/src/test/mocks/provider' +import { AutoMockedApolloProvider } from 'wallet/src/test/mocks' // This type extends the default options for render from RTL, as well // as allows the user to specify other things such as initialState, store. diff --git a/packages/wallet/src/test/shared.ts b/packages/wallet/src/test/shared.ts new file mode 100644 index 00000000000..72882aa8bf7 --- /dev/null +++ b/packages/wallet/src/test/shared.ts @@ -0,0 +1,9 @@ +import { faker } from '@faker-js/faker' + +export const MAX_FIXTURE_TIMESTAMP = 1609459200 + +const FAKER_SEED = 123 + +faker.seed(FAKER_SEED) + +export { faker } diff --git a/packages/wallet/src/test/test-utils.ts b/packages/wallet/src/test/test-utils.ts index bbc18700d3a..5545d659b79 100644 --- a/packages/wallet/src/test/test-utils.ts +++ b/packages/wallet/src/test/test-utils.ts @@ -1,5 +1,8 @@ import { renderHookWithProviders, renderWithProviders } from './render' +export { MAX_FIXTURE_TIMESTAMP, faker } from './shared' +export { createArray } from './utils' + // re-export everything export * from '@testing-library/react-native' // override render method diff --git a/packages/wallet/src/test/utils/array.ts b/packages/wallet/src/test/utils/array.ts new file mode 100644 index 00000000000..d5667a30773 --- /dev/null +++ b/packages/wallet/src/test/utils/array.ts @@ -0,0 +1,34 @@ +/** + * Generates an array of a specified length, where each element is created using a provided factory function. + * The type of the returned array reflects the exact number of elements specified, providing stronger type safety + * than a standard array type. + * + * @param factory - A no-argument function that returns an element of type T to populate the array. + * @param length - The desired length of the resulting array, specified as a number. + * @returns An array of type `ArrayOfLength`, where `L` is the specified length and `T` is the type of elements returned by the factory function. + * + * @example + * ```typescript + * // Number factory function + * const numFactory = () => Math.floor(Math.random() * 100); + * // Create an array of 4 numbers + * const nums = createArray(numFactory, 4); + * console.log(nums); // [23, 45, 67, 89] (example output) + * + * // String factory function + * const stringFactory = () => "hello"; + * // Create an array of 2 strings + * const strings = createArray(stringFactory, 2); + * console.log(strings); // ["hello", "hello"] + * ``` + */ +export const createArray = ( + length: L, + factory: (index: number) => T +): ArrayOfLength => { + const result = [] + for (let i = 0; i < length; i++) { + result.push(factory(i)) + } + return result as ArrayOfLength +} diff --git a/packages/wallet/src/test/utils/factory.ts b/packages/wallet/src/test/utils/factory.ts new file mode 100644 index 00000000000..4e30bb55c38 --- /dev/null +++ b/packages/wallet/src/test/utils/factory.ts @@ -0,0 +1,117 @@ +import { omit, pick } from 'lodash' + +/** + * This utility function, `createFixture`, generates a factory function for creating test data fixtures. It is designed to support + * both static and dynamic test data generation with an emphasis on customization through custom options and per-call overrides. + * The utility offers three modes of operation to accommodate various use cases: without custom options, with static custom + * options, and with dynamic custom options provided via a getter function. + * + * Modes of Operation: + * 1. **Without Custom Options**: For simple data generation that does not require custom options. + * 2. **With Static Custom Options**: Allows specifying a static object of custom options to influence the data generation logic. + * 3. **With Dynamic Custom Options**: Utilizes a getter function to provide dynamic default options, offering more flexibility. + * + * The returned factory function takes a `getValues` function, responsible for generating the base structure of the fixture. + * This function can optionally use the provided custom options. An `overrides` object can also be passed to the factory function + * for per-call customizations, allowing modification of both the initial custom options and the properties of the generated data. + * + * @typeparam T - The base type of the data generated by the fixture. + * @typeparam P - The type of the custom options object, optional. + * @param defaultOptionsOrGetter - An optional object of custom options or a function returning such an object. + * These options are used within the `getValues` function to dynamically generate data. + * + * @returns A factory function that accepts a `getValues` function for generating the fixture's base data. + * The factory function can be further invoked with an `overrides` object to customize the generated data per call. + * + * @example + * Without custom options: + * ```typescript + * export const user = createFixture()(() => ({ + * id: faker.datatype.uuid(), + * name: faker.name.findName(), + * })); + * ``` + * + * With custom options influencing data generation (not directly included in the output): + * ```typescript + * export const complexUserData = createFixture({ + * isActive: true, + * })(({ isActive }) => ({ + * id: faker.datatype.uuid(), + * name: faker.name.findName(), + * status: isActive ? 'active' : 'inactive', + * lastLogin: isActive ? new Date() : null, + * })); + * ``` + * + * With dynamic custom options for flexible and context-specific data generation: + * ```typescript + * export const token = createFixture(() => ({ + * sdkToken: randomChoice(TOKENS), + * }))(({ sdkToken }) => ({ + * ...contract(), + * id: faker.datatype.uuid(), + * name: sdkToken.name, + * symbol: sdkToken.symbol, + * decimals: sdkToken.decimals, + * chain: toGraphQLChain(sdkToken.chainId) ?? Chain.Ethereum, + * address: sdkToken.address, + * })) + * ``` + */ +// If there are no custom options +export function createFixture(): { + (getValues: () => V): { + // If some fields returned by getValues are overridden + >(overrides: O): Omit & O + // If no fields are overridden + (): V + } +} + +// If there are custom options with default values object +export function createFixture( + defaultOptions: Required

// defaultOptions is an object with default options +): { + (getValues: (options: P) => V): { + // If some fields returned by getValues are overridden + >(overrides: O): Omit> & Omit + // If no fields are overridden + (): V + } +} + +// If there are custom options with default values getter function +export function createFixture( + getDefaultOptions: () => Required

// getDefaultOptions is a function that returns an object with default options +): { + (getValues: (options: P) => V): { + // If some fields returned by getValues are overridden + >(overrides: O): Omit> & Omit + // If no fields are overridden + (): V + } +} + +export function createFixture( + defaultOptionsOrGetter?: Required

| (() => Required

) +) { + return (getValues: (options?: P) => V) => { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + return | Partial>(overrides?: O) => { + // Get default options (if they exist) + const defaultOptions = + typeof defaultOptionsOrGetter === 'function' + ? defaultOptionsOrGetter() + : defaultOptionsOrGetter + // Get overrides for options + const optionOverrides = defaultOptions ? pick(overrides, Object.keys(defaultOptions)) : {} + // Get values with getValues function + const mergedOptions = defaultOptions ? { ...defaultOptions, ...optionOverrides } : undefined + const values = getValues(mergedOptions) + // Get overrides for values + const valueOverrides = overrides ? omit(overrides, Object.keys(defaultOptions || {})) : {} + return Array.isArray(values) ? values : { ...values, ...valueOverrides } + } + } +} diff --git a/packages/wallet/src/test/utils/index.ts b/packages/wallet/src/test/utils/index.ts new file mode 100644 index 00000000000..c6872e3a265 --- /dev/null +++ b/packages/wallet/src/test/utils/index.ts @@ -0,0 +1,3 @@ +export * from './array' +export * from './factory' +export * from './random' diff --git a/packages/wallet/src/test/utils/random.ts b/packages/wallet/src/test/utils/random.ts new file mode 100644 index 00000000000..9f9bdeea742 --- /dev/null +++ b/packages/wallet/src/test/utils/random.ts @@ -0,0 +1,40 @@ +/** + * Returns a random value from the given enum. + * + * @param enumObj The enum object from which a random value is to be selected. This object should be a TypeScript enum + * where the enum values are of type string. + * @returns A random value from the specified enum. + * + * @example + * ```typescript + * enum Colors { + * Red = 'RED', + * Green = 'GREEN', + * Blue = 'BLUE' + * } + * + * const randomColor = randomEnumValue(Colors); + * console.log(randomColor); // Outputs: 'RED', 'GREEN', or 'BLUE' (randomly selected) + * ``` + * + * @typeparam T Type of the enum object (will be automatically inferred from the provided argument). + */ +export const randomEnumValue = >( + enumObj: T +): T[keyof T] => { + // If enum has different types for keys and values (keys are always strings, + // values can be strings or numbers), we need to filter out the keys + const keys = Object.keys(enumObj).filter((key) => isNaN(Number(key))) + const randomKey = randomChoice(keys) + return enumObj[randomKey] as T[keyof T] +} + +/** + * Returns a random value from the array of choices. + * + * @returns A random value from the specified array. + */ +export const randomChoice = (choices: T[]): T => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return choices[Math.floor(Math.random() * choices.length)]! +} diff --git a/packages/wallet/src/utils/balance.test.ts b/packages/wallet/src/utils/balance.test.ts index 9fe8895d9a1..da34a0e9cbd 100644 --- a/packages/wallet/src/utils/balance.test.ts +++ b/packages/wallet/src/utils/balance.test.ts @@ -1,7 +1,12 @@ import { CurrencyAmount } from '@uniswap/sdk-core' import JSBI from 'jsbi' import { DAI } from 'wallet/src/constants/tokens' -import { ArbitrumEth, MainnetEth, OptimismEth, PolygonMatic } from 'wallet/src/test/fixtures' +import { + ARBITRUM_CURRENCY, + MAINNET_CURRENCY, + OPTIMISM_CURRENCY, + POLYGON_CURRENCY, +} from 'wallet/src/test/fixtures' import { MIN_ARBITRUM_FOR_GAS, MIN_ETH_FOR_GAS, @@ -25,7 +30,7 @@ describe(maxAmountSpend, () => { it('reserves gas for large amounts on ETH Mainnet', () => { const amount = CurrencyAmount.fromRawAmount( - MainnetEth, + MAINNET_CURRENCY, JSBI.add(JSBI.BigInt(99), JSBI.BigInt(MIN_ETH_FOR_GAS)) ) const amount1Spend = maxAmountSpend(amount) @@ -34,7 +39,7 @@ describe(maxAmountSpend, () => { it('handles small amounts on ETH Mainnet', () => { const amount = CurrencyAmount.fromRawAmount( - MainnetEth, + MAINNET_CURRENCY, JSBI.subtract(JSBI.BigInt(99), JSBI.BigInt(MIN_ETH_FOR_GAS)) ) const amount1Spend = maxAmountSpend(amount) @@ -45,7 +50,7 @@ describe(maxAmountSpend, () => { it('reserves gas for large amounts on Polygon', () => { const amount = CurrencyAmount.fromRawAmount( - PolygonMatic, + POLYGON_CURRENCY, JSBI.add(JSBI.BigInt(99), JSBI.BigInt(MIN_POLYGON_FOR_GAS)) ) const amount1Spend = maxAmountSpend(amount) @@ -54,7 +59,7 @@ describe(maxAmountSpend, () => { it('handles small amounts on Polygon', () => { const amount = CurrencyAmount.fromRawAmount( - PolygonMatic, + POLYGON_CURRENCY, JSBI.subtract(JSBI.BigInt(99), JSBI.BigInt(MIN_POLYGON_FOR_GAS)) ) const amount1Spend = maxAmountSpend(amount) @@ -65,7 +70,7 @@ describe(maxAmountSpend, () => { it('reserves gas for large amounts on Arbitrum', () => { const amount = CurrencyAmount.fromRawAmount( - ArbitrumEth, + ARBITRUM_CURRENCY, JSBI.add(JSBI.BigInt(99), JSBI.BigInt(MIN_ARBITRUM_FOR_GAS)) ) const amount1Spend = maxAmountSpend(amount) @@ -74,7 +79,7 @@ describe(maxAmountSpend, () => { it('handles small amounts on Arbitrum', () => { const amount = CurrencyAmount.fromRawAmount( - ArbitrumEth, + ARBITRUM_CURRENCY, JSBI.subtract(JSBI.BigInt(99), JSBI.BigInt(MIN_ARBITRUM_FOR_GAS)) ) const amount1Spend = maxAmountSpend(amount) @@ -85,7 +90,7 @@ describe(maxAmountSpend, () => { it('reserves gas for large amounts on Optimism', () => { const amount = CurrencyAmount.fromRawAmount( - OptimismEth, + OPTIMISM_CURRENCY, JSBI.add(JSBI.BigInt(99), JSBI.BigInt(MIN_OPTIMISM_FOR_GAS)) ) const amount1Spend = maxAmountSpend(amount) @@ -94,7 +99,7 @@ describe(maxAmountSpend, () => { it('handles small amounts on Optimism', () => { const amount = CurrencyAmount.fromRawAmount( - OptimismEth, + OPTIMISM_CURRENCY, JSBI.subtract(JSBI.BigInt(99), JSBI.BigInt(MIN_OPTIMISM_FOR_GAS)) ) const amount1Spend = maxAmountSpend(amount) diff --git a/packages/wallet/src/utils/currency.test.ts b/packages/wallet/src/utils/currency.test.ts index 1253bc86f72..c0c428ff700 100644 --- a/packages/wallet/src/utils/currency.test.ts +++ b/packages/wallet/src/utils/currency.test.ts @@ -1,5 +1,5 @@ import { DAI, USDC } from 'wallet/src/constants/tokens' -import { mockLocalizedFormatter } from 'wallet/src/test/utils' +import { mockLocalizedFormatter } from 'wallet/src/test/mocks/utils' import { getCurrencyDisplayText, getFormattedCurrencyAmount } from './currency' describe(getFormattedCurrencyAmount, () => { diff --git a/packages/wallet/src/utils/duration.ts b/packages/wallet/src/utils/duration.ts new file mode 100644 index 00000000000..a04ae999790 --- /dev/null +++ b/packages/wallet/src/utils/duration.ts @@ -0,0 +1,29 @@ +import { getDurationRemaining } from 'utilities/src/time/duration' +import i18n from 'wallet/src/i18n/i18n' + +export function getOtpDurationString(expirationTime: number): string { + const timeLeft = expirationTime - Date.now() + if (timeLeft <= 0) { + return i18n.t('scantastic.code.expired') + } + + const { seconds, minutes, hours } = getDurationRemaining(expirationTime) + + if (minutes) { + if (hours) { + return i18n.t('scantastic.code.timeRemaining.shorthand.hours', { + seconds, + minutes, + hours, + }) + } + + return i18n.t('scantastic.code.timeRemaining.shorthand.minutes', { + seconds, + minutes, + }) + } + return i18n.t('scantastic.code.timeRemaining.shorthand.seconds', { + seconds, + }) +} diff --git a/packages/wallet/src/utils/mnemonics.test.ts b/packages/wallet/src/utils/mnemonics.test.ts index ded5735b1ad..85a213baa2d 100644 --- a/packages/wallet/src/utils/mnemonics.test.ts +++ b/packages/wallet/src/utils/mnemonics.test.ts @@ -1,26 +1,26 @@ +import i18n from 'wallet/src/i18n/i18n' import { MnemonicValidationError, translateMnemonicErrorMessage } from 'wallet/src/utils/mnemonics' describe(translateMnemonicErrorMessage, () => { - const t = (str: string): string => str - it('correct invalid phrase message', () => { - expect(translateMnemonicErrorMessage(MnemonicValidationError.InvalidPhrase, undefined, t)).toBe( - 'Invalid phrase' - ) + expect( + translateMnemonicErrorMessage(MnemonicValidationError.InvalidPhrase, undefined, i18n.t) + ).toBe('Invalid phrase') }) it('correct invalid word message', () => { - expect(translateMnemonicErrorMessage(MnemonicValidationError.InvalidWord, 't', t)).toBe( - 'Invalid word: {{word}}' - ) + const invalidWord = 'gibberish' + expect( + translateMnemonicErrorMessage(MnemonicValidationError.InvalidWord, invalidWord, i18n.t) + ).toBe(`Invalid word: ${invalidWord}`) }) it('correct incorrect number of words message', () => { - expect(translateMnemonicErrorMessage(MnemonicValidationError.TooManyWords, undefined, t)).toBe( - 'Recovery phrase must be 12-24 words' - ) expect( - translateMnemonicErrorMessage(MnemonicValidationError.NotEnoughWords, undefined, t) + translateMnemonicErrorMessage(MnemonicValidationError.TooManyWords, undefined, i18n.t) + ).toBe('Recovery phrase must be 12-24 words') + expect( + translateMnemonicErrorMessage(MnemonicValidationError.NotEnoughWords, undefined, i18n.t) ).toBe('Recovery phrase must be 12-24 words') }) }) diff --git a/packages/wallet/src/utils/mnemonics.ts b/packages/wallet/src/utils/mnemonics.ts index ba9a16ae49b..ce015323613 100644 --- a/packages/wallet/src/utils/mnemonics.ts +++ b/packages/wallet/src/utils/mnemonics.ts @@ -17,12 +17,12 @@ export function translateMnemonicErrorMessage( ): string { switch (error) { case MnemonicValidationError.InvalidPhrase: - return t('Invalid phrase') + return t('account.seedPhrase.error.invalid') case MnemonicValidationError.InvalidWord: - return t('Invalid word: {{word}}', { word: invalidWord }) + return t('account.seedPhrase.error.invalidWord', { word: invalidWord }) case MnemonicValidationError.TooManyWords: case MnemonicValidationError.NotEnoughWords: - return t('Recovery phrase must be 12-24 words') + return t('account.seedPhrase.error.phraseLength') default: throw new Error(`Unhandled MnemonicValidationError case: ${error}`) } diff --git a/packages/wallet/src/utils/password.test.ts b/packages/wallet/src/utils/password.test.ts index 34fc14ffa25..fef2b741e59 100644 --- a/packages/wallet/src/utils/password.test.ts +++ b/packages/wallet/src/utils/password.test.ts @@ -5,12 +5,6 @@ import { isPasswordStrongEnough, } from './password' -jest.mock('i18next', () => { - return { - t: (key: string): string => key, - } -}) - describe(isPasswordStrongEnough, () => { it('returns true for equal strengths', () => { expect( diff --git a/packages/wallet/src/utils/password.ts b/packages/wallet/src/utils/password.ts index 76be8052a5c..4445dae4f9a 100644 --- a/packages/wallet/src/utils/password.ts +++ b/packages/wallet/src/utils/password.ts @@ -43,11 +43,14 @@ export function getPasswordStrengthTextAndColor(strength: PasswordStrength): { } { switch (strength) { case PasswordStrength.WEAK: - return { text: t('Weak'), color: '$statusCritical' } + return { text: t('common.input.password.strength.weak'), color: '$statusCritical' } case PasswordStrength.MEDIUM: - return { text: t('Medium'), color: '$DEP_accentWarning' } + return { + text: t('common.input.password.strength.medium'), + color: '$DEP_accentWarning', + } case PasswordStrength.STRONG: - return { text: t('Strong'), color: '$statusSuccess' } + return { text: t('common.input.password.strength.strong'), color: '$statusSuccess' } default: return { text: '', color: '$neutral1' } } diff --git a/packages/wallet/src/utils/platform/index.native.ts b/packages/wallet/src/utils/platform/index.native.ts index b9cd675acd0..d4a5f2bcc74 100644 --- a/packages/wallet/src/utils/platform/index.native.ts +++ b/packages/wallet/src/utils/platform/index.native.ts @@ -4,3 +4,14 @@ export const isMobile = true export const isIOS = Platform.OS === 'ios' export const isAndroid = Platform.OS === 'android' export const isNonSupportedDevice = !isIOS && !isAndroid + +export function getCloudProviderName(): string { + switch (Platform.OS) { + case 'android': + return 'Google Drive' + case 'ios': + return 'iCloud' + default: + return '' + } +} diff --git a/packages/wallet/src/utils/platform/index.ts b/packages/wallet/src/utils/platform/index.ts index 079e52cf52d..c515fd44212 100644 --- a/packages/wallet/src/utils/platform/index.ts +++ b/packages/wallet/src/utils/platform/index.ts @@ -10,3 +10,14 @@ export const isIOS = platform === 'iOS' export const isAndroid = platform === 'Android' export const isNonSupportedDevice = !isIOS && !isAndroid && type === 'mobile' export const isMobileSafari = isMobile && isIOS && name?.toLowerCase().includes('safari') + +export function getCloudProviderName(): string { + switch (platform) { + case 'Android': + return 'Google Drive' + case 'iOS': + return 'iCloud' + default: + return '' + } +} diff --git a/packages/wallet/tsconfig.json b/packages/wallet/tsconfig.json index 4ad23921347..4c279287ded 100644 --- a/packages/wallet/tsconfig.json +++ b/packages/wallet/tsconfig.json @@ -10,6 +10,9 @@ { "path": "../ui" }, + { + "path": "../uniswap" + }, { "path": "../utilities" } diff --git a/yarn.lock b/yarn.lock index 0474b6e9524..fc48a5fc684 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12677,15 +12677,6 @@ __metadata: languageName: node linkType: hard -"@types/react-helmet@npm:6.1.7": - version: 6.1.7 - resolution: "@types/react-helmet@npm:6.1.7" - dependencies: - "@types/react": "*" - checksum: d5ee8343f10f8746a022249c6401493baa73241a90b5002b59e8a6ce31650b9e1ab38d398eba5564af026f68eda107c8fcdca01fc046a163a96dc422710f6a77 - languageName: node - linkType: hard - "@types/react-native@npm:0.71.3": version: 0.71.3 resolution: "@types/react-native@npm:0.71.3" @@ -13621,7 +13612,6 @@ __metadata: "@types/qs": 6.9.2 "@types/react": ^18.0.15 "@types/react-dom": ^18.0.6 - "@types/react-helmet": 6.1.7 "@types/react-redux": 7.1.30 "@types/react-scroll-sync": 0.8.7 "@types/react-table": 7.7.12 @@ -13739,7 +13729,7 @@ __metadata: react: 18.2.0 react-dom: 18.2.0 react-feather: 2.0.10 - react-helmet: 6.1.0 + react-helmet-async: 2.0.4 react-infinite-scroll-component: 6.1.0 react-is: 18.2.0 react-markdown: 4.3.1 @@ -13771,6 +13761,7 @@ __metadata: ts-jest: ^29.1.1 tsafe: 1.6.4 typescript: 5.3.3 + uniswap: "workspace:^" use-resize-observer: 9.1.0 utilities: "workspace:^" uuid: 9.0.0 @@ -13967,6 +13958,7 @@ __metadata: statsig-react-native: 4.11.0 typed-redux-saga: 1.5.0 typescript: 5.3.3 + uniswap: "workspace:^" utilities: "workspace:^" wallet: "workspace:^" yarn-deduplicate: 6.0.0 @@ -37736,7 +37728,7 @@ __metadata: languageName: node linkType: hard -"react-fast-compare@npm:^3.0.1, react-fast-compare@npm:^3.1.1": +"react-fast-compare@npm:^3.0.1, react-fast-compare@npm:^3.2.2": version: 3.2.2 resolution: "react-fast-compare@npm:3.2.2" checksum: 2071415b4f76a3e6b55c84611c4d24dcb12ffc85811a2840b5a3f1ff2d1a99be1020d9437ee7c6e024c9f4cbb84ceb35e48cf84f28fcb00265ad2dfdd3947704 @@ -37783,17 +37775,17 @@ __metadata: languageName: node linkType: hard -"react-helmet@npm:6.1.0": - version: 6.1.0 - resolution: "react-helmet@npm:6.1.0" +"react-helmet-async@npm:2.0.4": + version: 2.0.4 + resolution: "react-helmet-async@npm:2.0.4" dependencies: - object-assign: ^4.1.1 - prop-types: ^15.7.2 - react-fast-compare: ^3.1.1 - react-side-effect: ^2.1.0 + invariant: ^2.2.4 + react-fast-compare: ^3.2.2 + shallowequal: ^1.1.0 peerDependencies: - react: ">=16.3.0" - checksum: a4998479dab7fc1c2799eddefb1870a9d881b5f71cfdf97979a9882e42f4bb50402d55335f308f461e735e01a06f46b16cc7b4e6bcb22c7a4a6f85a753c5c106 + react: ^16.6.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.6.0 || ^17.0.0 || ^18.0.0 + checksum: 1bd16e6be6d15cf3d4b4c0853d1e122941a05d3fb2bad1fb1c5037069c5f142fcab063c342b95c58a998a81f093bdf1bd1bb00852a5a3a84d49e48790d5142bb languageName: node linkType: hard @@ -38780,15 +38772,6 @@ __metadata: languageName: node linkType: hard -"react-side-effect@npm:^2.1.0": - version: 2.1.2 - resolution: "react-side-effect@npm:2.1.2" - peerDependencies: - react: ^16.3.0 || ^17.0.0 || ^18.0.0 - checksum: c5eb1f42b464fb093bca59aaae0f1b2060373a2aaff95275b8781493628cdbbb6acdd6014e7883782c65c361f35a30f28cc515d68a1263ddb39cbbc47110be53 - languageName: node - linkType: hard - "react-spring@npm:9.7.3": version: 9.7.3 resolution: "react-spring@npm:9.7.3" @@ -44374,6 +44357,22 @@ __metadata: languageName: node linkType: hard +"uniswap@workspace:^, uniswap@workspace:packages/uniswap": + version: 0.0.0-use.local + resolution: "uniswap@workspace:packages/uniswap" + dependencies: + "@typechain/ethers-v5": 7.2.0 + "@uniswap/eslint-config": "workspace:^" + depcheck: 1.4.7 + eslint: 8.44.0 + ethers: 5.7.2 + jest: 29.6.4 + jest-presets: "workspace:^" + typechain: 5.2.0 + typescript: 5.3.3 + languageName: unknown + linkType: soft + "universal-user-agent@npm:^6.0.0": version: 6.0.0 resolution: "universal-user-agent@npm:6.0.0" @@ -45404,7 +45403,6 @@ __metadata: "@testing-library/jest-native": 5.4.2 "@testing-library/react-hooks": 7.0.2 "@testing-library/react-native": 11.5.0 - "@typechain/ethers-v5": 7.2.0 "@types/react": ^18.0.15 "@types/react-window": 1.8.2 "@types/ua-parser-js": 0.7.31 @@ -45459,11 +45457,11 @@ __metadata: redux-saga: 1.2.2 redux-saga-test-plan: 4.0.4 statsig-react-native: 4.11.0 - typechain: 5.2.0 typed-redux-saga: 1.5.0 typescript: 5.3.3 ua-parser-js: 1.0.37 ui: "workspace:^" + uniswap: "workspace:^" utilities: "workspace:^" uuid: 9.0.0 wcag-contrast: 3.0.0