ci(release): publish latest release

parent ae6fb88a
* @uniswap/web-admins
IPFS hash of the deployment:
- CIDv0: `QmPfhCQw4e63hhx4FqWnroACYRcyZHw6f5Nvo6DXHwkEGr`
- CIDv1: `bafybeiatxsersz6wkzf75lzpkhxoglubrcqv7uqy6nubdvvjyanidm4vam`
We are back with another round of updates. Here’s what’s new:
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
You can also access the Uniswap Interface from an IPFS gateway.
**BEWARE**: The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported.
**You should always use an IPFS gateway that enforces origin separation**, or our hosted deployment of the latest release at [app.uniswap.org](https://app.uniswap.org).
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://bafybeiatxsersz6wkzf75lzpkhxoglubrcqv7uqy6nubdvvjyanidm4vam.ipfs.dweb.link/
- https://bafybeiatxsersz6wkzf75lzpkhxoglubrcqv7uqy6nubdvvjyanidm4vam.ipfs.cf-ipfs.com/
- [ipfs://QmPfhCQw4e63hhx4FqWnroACYRcyZHw6f5Nvo6DXHwkEGr/](ipfs://QmPfhCQw4e63hhx4FqWnroACYRcyZHw6f5Nvo6DXHwkEGr/)
### 5.17.2 (2024-03-08)
### Bug Fixes
* **web:** [hotfix] fiatCurrency is undefined (#6811) (#6814) a78d4cc
Launched uni.eth usernames! This is a readable username that makes it easy to receive crypto and build your web3 profile. Claim your username in the app.
Other changes:
- Polish around recovery phrase settings and notifications
- Various bug fixes and performance improvements
web/5.17.2
\ No newline at end of file
mobile/1.22
\ No newline at end of file
......@@ -125,17 +125,17 @@ android {
dev {
isDefault(true)
applicationIdSuffix ".dev"
versionName "1.23"
versionName "1.22"
dimension "variant"
}
beta {
applicationIdSuffix ".beta"
versionName "1.23"
versionName "1.22"
dimension "variant"
}
prod {
dimension "variant"
versionName "1.23"
versionName "1.22"
}
}
......
......@@ -43,4 +43,11 @@ allprojects {
codegenDir = rootProject.file("../../../node_modules/react-native-codegen/")
}
}
repositories {
maven {
// expo-camera bundles a custom com.google.android:cameraview
url "$rootDir/../../../node_modules/expo-camera/android/maven"
}
}
}
......@@ -656,6 +656,8 @@ PODS:
- ExpoModulesCore
- ZXingObjC/OneD
- ZXingObjC/PDF417
- EXCamera (13.4.4):
- ExpoModulesCore
- EXFileSystem (15.3.0):
- ExpoModulesCore
- EXFont (11.1.1):
......@@ -1128,6 +1130,9 @@ PODS:
- react-native-appsflyer (6.10.3):
- AppsFlyerFramework (= 6.10.1)
- React
- react-native-compat (2.11.2):
- RCT-Folly (= 2021.07.22.00)
- React-Core
- react-native-context-menu-view (1.6.0):
- React
- react-native-get-random-values (1.8.0):
......@@ -1353,6 +1358,7 @@ DEPENDENCIES:
- EXApplication (from `../../../node_modules/expo-application/ios`)
- EXAV (from `../../../node_modules/expo-av/ios`)
- EXBarCodeScanner (from `../../../node_modules/expo-barcode-scanner/ios`)
- EXCamera (from `../../../node_modules/expo-camera/ios`)
- EXFileSystem (from `../../../node_modules/expo-file-system/ios`)
- EXFont (from `../../../node_modules/expo-font/ios`)
- EXImageLoader (from `../../../node_modules/expo-image-loader/ios`)
......@@ -1394,6 +1400,7 @@ DEPENDENCIES:
- React-jsinspector (from `../../../node_modules/react-native/ReactCommon/jsinspector`)
- React-logger (from `../../../node_modules/react-native/ReactCommon/logger`)
- react-native-appsflyer (from `../../../node_modules/react-native-appsflyer`)
- "react-native-compat (from `../../../node_modules/@walletconnect/react-native-compat`)"
- react-native-context-menu-view (from `../../../node_modules/react-native-context-menu-view`)
- react-native-get-random-values (from `../../../node_modules/react-native-get-random-values`)
- react-native-image-picker (from `../../../node_modules/react-native-image-picker`)
......@@ -1493,6 +1500,8 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/expo-av/ios"
EXBarCodeScanner:
:path: "../../../node_modules/expo-barcode-scanner/ios"
EXCamera:
:path: "../../../node_modules/expo-camera/ios"
EXFileSystem:
:path: "../../../node_modules/expo-file-system/ios"
EXFont:
......@@ -1565,6 +1574,8 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/react-native/ReactCommon/logger"
react-native-appsflyer:
:path: "../../../node_modules/react-native-appsflyer"
react-native-compat:
:path: "../../../node_modules/@walletconnect/react-native-compat"
react-native-context-menu-view:
:path: "../../../node_modules/react-native-context-menu-view"
react-native-get-random-values:
......@@ -1671,6 +1682,7 @@ SPEC CHECKSUMS:
EXApplication: d8f53a7eee90a870a75656280e8d4b85726ea903
EXAV: f393dfc0b28214d62855a31e06eb21d426d6e2da
EXBarCodeScanner: 296dd50f6c03928d1d71d37ea17473b304cfdb00
EXCamera: 6e6e79bf01a2b8190268d93297d8e79a843d5ede
EXFileSystem: 0b4a2c08c2dc92146849772145d61c1773144283
EXFont: 6ea3800df746be7233208d80fe379b8ed74f4272
EXImageLoader: 03063370bc06ea1825713d3f55fe0455f7c88d04
......@@ -1727,6 +1739,7 @@ SPEC CHECKSUMS:
React-jsinspector: 7e58fe86c7cc442fd11da0c9d8bef12a8d63f771
React-logger: a3f6ca0d018749852a2a6f07c154bfc6fcd4195a
react-native-appsflyer: 153e96b97ecc0c26b14fc79911675e51cfd35b36
react-native-compat: e31d4e4ba7db54f2649c4b34b9b594029b51b5e2
react-native-context-menu-view: d3b3e77985d5b05674a70f8e7eafe404dfa5bbcc
react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a
react-native-image-picker: 1569cfade34b3a011191ce262423e6ce2f8db5a1
......
......@@ -2450,7 +2450,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.23;
MARKETING_VERSION = 1.22;
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.23;
MARKETING_VERSION = 1.22;
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.23;
MARKETING_VERSION = 1.22;
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.23;
MARKETING_VERSION = 1.22;
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.23;
MARKETING_VERSION = 1.22;
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.23;
MARKETING_VERSION = 1.22;
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.23;
MARKETING_VERSION = 1.22;
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.23;
MARKETING_VERSION = 1.22;
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.23;
MARKETING_VERSION = 1.22;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -2833,7 +2833,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.23;
MARKETING_VERSION = 1.22;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3003,7 +3003,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.23;
MARKETING_VERSION = 1.22;
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.23;
MARKETING_VERSION = 1.22;
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.23;
MARKETING_VERSION = 1.22;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3214,7 +3214,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.23;
MARKETING_VERSION = 1.22;
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.23;
MARKETING_VERSION = 1.22;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3381,7 +3381,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.23;
MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension;
......
......@@ -5,8 +5,7 @@ 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 { initializeTranslation } from 'wallet/src/i18n/i18n'
import { mockLocalizationContext } from 'wallet/src/test/mocks/utils'
import { MockLocalizationContext } from 'wallet/src/test/utils'
// avoids polluting console in test runs, while keeping important log levels
global.console = {
......@@ -19,9 +18,6 @@ global.console = {
// error: jest.fn(),
}
// Uses real translations for tests
initializeTranslation()
// Mock Sentry crash reporting
jest.mock('@sentry/react-native', () => ({
init: () => jest.fn(),
......@@ -87,7 +83,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(),
......@@ -115,6 +111,22 @@ 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', () => {
......
......@@ -94,6 +94,7 @@
"expo-av": "13.4.1",
"expo-barcode-scanner": "12.7.0",
"expo-blur": "12.2.2",
"expo-camera": "13.4.4",
"expo-haptics": "12.0.1",
"expo-linear-gradient": "12.3.0",
"expo-linking": "4.0.1",
......@@ -143,7 +144,6 @@
"rive-react-native": "6.1.1",
"statsig-react-native": "4.11.0",
"typed-redux-saga": "1.5.0",
"uniswap": "workspace:^",
"utilities": "workspace:^",
"wallet": "workspace:^"
},
......
......@@ -72,8 +72,8 @@ function ErrorScreen({ error }: { error: Error }): JSX.Element {
<Flex centered grow gap="$spacing36">
<Image source={DEAD_LUNI} style={styles.errorImage} />
<Flex centered gap="$spacing8">
<Text variant="subheading1">{t('errors.crash.title')}</Text>
<Text variant="body2">{t('errors.crash.message')}</Text>
<Text variant="subheading1">{t('Uh oh!')}</Text>
<Text variant="body2">{t('Something crashed.')}</Text>
</Flex>
{error.message && __DEV__ && <Text variant="body2">{error.message}</Text>}
</Flex>
......@@ -82,7 +82,7 @@ function ErrorScreen({ error }: { error: Error }): JSX.Element {
onPress={(): void => {
RNRestart.Restart()
}}>
{t('errors.crash.restart')}
{t('Restart app')}
</Button>
</Flex>
</Flex>
......
......@@ -6,7 +6,6 @@ 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'
......@@ -17,7 +16,6 @@ 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()
......@@ -26,7 +24,6 @@ export function MobileWalletNavigationProvider({ children }: PropsWithChildren):
navigateToAccountActivityList={navigateToAccountActivityList}
navigateToAccountTokenList={navigateToAccountTokenList}
navigateToBuyOrReceiveWithEmptyWallet={navigateToBuyOrReceiveWithEmptyWallet}
navigateToNftDetails={navigateToNftDetails}
navigateToSwapFlow={navigateToSwapFlow}
navigateToTokenDetails={navigateToTokenDetails}>
{children}
......@@ -67,23 +64,6 @@ function useNavigateToTokenDetails(): (currencyId: string) => void {
)
}
function useNavigateToNftDetails(): (args: NavigateToNftItemArgs) => void {
const navigation = useAppStackNavigation()
return useCallback(
({ owner, address, tokenId, isSpam, fallbackData }: NavigateToNftItemArgs): void => {
navigation.navigate(Screens.NFTItem, {
owner,
address,
tokenId,
isSpam,
fallbackData,
})
},
[navigation]
)
}
function useNavigateToBuyOrReceiveWithEmptyWallet(): () => void {
const dispatch = useAppDispatch()
......
......@@ -97,25 +97,7 @@ 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 {
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',
}),
})
import { account, fiatOnRampTxDetailsFailed, txDetailsConfirmed } from 'wallet/src/test/fixtures'
// helps with object assignment
// eslint-disable-next-line @typescript-eslint/no-explicit-any
......
......@@ -6,11 +6,11 @@ import { MobileState } from 'src/app/reducer'
import { initialModalState } from 'src/features/modals/modalSlice'
import { render } from 'src/test/test-utils'
import { ModalName } from 'wallet/src/telemetry/constants'
import { ACCOUNT } from 'wallet/src/test/fixtures'
import { mockWalletPreloadedState, noOpFunction } from 'wallet/src/test/mocks'
import { mockWalletPreloadedState } from 'wallet/src/test/fixtures'
import { noOpFunction } from 'wallet/src/test/utils'
const preloadedState = {
...mockWalletPreloadedState(ACCOUNT),
...mockWalletPreloadedState,
modals: {
...initialModalState,
[ModalName.AccountSwitcher]: { isOpen: true },
......
......@@ -190,19 +190,17 @@ 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('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'),
? 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.'
),
[
{
text: t('account.cloud.error.unavailable.button.settings'),
onPress: openSettings,
style: 'default',
},
{ text: t('account.cloud.error.unavailable.button.cancel'), style: 'cancel' },
{ text: t('Go to settings'), onPress: openSettings, style: 'default' },
{ text: t('Not now'), style: 'cancel' },
]
)
return
......@@ -226,7 +224,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
borderBottomColor="$surface3"
borderBottomWidth={1}
p="$spacing16">
<Text variant="body1">{t('account.wallet.button.create')}</Text>
<Text variant="body1">{t('Create a new wallet')}</Text>
</Flex>
),
},
......@@ -235,7 +233,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
onPress: onPressAddViewOnlyWallet,
render: () => (
<Flex alignItems="center" p="$spacing16">
<Text variant="body1">{t('account.wallet.button.addViewOnly')}</Text>
<Text variant="body1">{t('Add a view-only wallet')}</Text>
</Flex>
),
},
......@@ -244,7 +242,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
onPress: onPressImportWallet,
render: () => (
<Flex alignItems="center" borderTopColor="$surface3" borderTopWidth={1} p="$spacing16">
<Text variant="body1">{t('account.wallet.button.import')}</Text>
<Text variant="body1">{t('Import a new wallet')}</Text>
</Flex>
),
},
......@@ -257,9 +255,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
render: () => (
<Flex alignItems="center" borderTopColor="$surface3" borderTopWidth={1} p="$spacing16">
<Text variant="body1">
{isAndroid
? t('account.cloud.button.restore.android')
: t('account.cloud.button.restore.ios')}
{isAndroid ? t('Restore from Google Drive') : t('Restore from iCloud')}
</Text>
</Flex>
),
......@@ -299,7 +295,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
testID={ElementName.WalletSettings}
theme="secondary"
onPress={onManageWallet}>
{t('account.wallet.button.manage')}
{t('Manage wallet')}
</Button>
</Flex>
</Flex>
......@@ -314,7 +310,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
<Icons.Plus color="$neutral2" size="$icon.12" strokeWidth={2} />
</Flex>
<Text color="$neutral2" variant="buttonLabel3">
{t('account.wallet.button.add')}
{t('Add wallet')}
</Text>
</Flex>
</TouchableArea>
......
......@@ -46,9 +46,11 @@ export function ViewOnlyExplainerModal(): JSX.Element {
<WalletImage height="100%" preserveAspectRatio="xMidYMid slice" width="100%" />
</Flex>
<Flex alignItems="center" gap="$spacing4">
<Text variant="subheading1">{t('account.wallet.viewOnly.title')}</Text>
<Text variant="subheading1">{t('This wallet is view-only')}</Text>
<Text color="$neutral2" textAlign="center" variant="body2">
{t('account.wallet.viewOnly.description')}
{t(
'To swap, buy, send, and receive tokens, you need to import this wallet’s recovery phrase.'
)}
</Text>
</Flex>
</Flex>
......@@ -59,7 +61,7 @@ export function ViewOnlyExplainerModal(): JSX.Element {
px={40}
theme="primary"
onPress={onPressImportWallet}>
{t('account.wallet.viewOnly.button')}
{t('Import wallet')}
</Button>
<Button
alignSelf="center"
......@@ -69,7 +71,7 @@ export function ViewOnlyExplainerModal(): JSX.Element {
px={40}
theme="secondary"
onPress={onClose}>
{t('common.button.later')}
{t('Maybe later')}
</Button>
</Flex>
</Flex>
......
......@@ -6,7 +6,7 @@ import { MobileState } from 'src/app/reducer'
import { initialModalState } from 'src/features/modals/modalSlice'
import { renderWithProviders } from 'src/test/render'
import { ModalName } from 'wallet/src/telemetry/constants'
import { mockWalletPreloadedState } from 'wallet/src/test/mocks'
import { mockWalletPreloadedState } from 'wallet/src/test/fixtures'
const preloadedState = {
...mockWalletPreloadedState,
......
......@@ -174,7 +174,7 @@ const SwapFAB = memo(function _SwapFAB({ activeScale = 0.96 }: SwapTabBarButtonP
color="$sporeWhite"
numberOfLines={1}
variant="buttonLabel2">
{t('common.button.swap')}
{t('Swap')}
</Text>
</AnimatedFlex>
</TapGestureHandler>
......@@ -256,7 +256,7 @@ function ExploreTabBarButton({ activeScale = 0.98 }: ExploreTabBarButtonProps):
pr="$spacing48"
style={{ lineHeight: fonts.body1.lineHeight }}
variant="body1">
{t('common.input.search')}
{t('Search')}
</Text>
</Flex>
</BlurView>
......
import { PersistState } from 'redux-persist'
import { apolloClient } from 'src/data/usePersistedApolloClient'
import { appRatingWatcherSaga } from 'src/features/appRating/saga'
import { cloudBackupsManagerSaga } from 'src/features/CloudBackup/saga'
......@@ -8,7 +9,7 @@ import { telemetrySaga } from 'src/features/telemetry/saga'
import { restoreMnemonicCompleteWatcher } from 'src/features/wallet/saga'
import { walletConnectSaga } from 'src/features/walletConnect/saga'
import { signWcRequestSaga } from 'src/features/walletConnect/signWcRequestSaga'
import { spawn } from 'typed-redux-saga'
import { delay, select, spawn } from 'typed-redux-saga'
import { appLanguageWatcherSaga } from 'wallet/src/features/language/saga'
import {
swapActions,
......@@ -44,6 +45,8 @@ import {
} from 'wallet/src/features/wallet/import/importAccountSaga'
import { getMonitoredSagaReducers, MonitoredSaga } from 'wallet/src/state/saga'
const REHYDRATION_STATUS_POLLING_INTERVAL = 50
// All regular sagas must be included here
const sagas = [
appLanguageWatcherSaga,
......@@ -96,6 +99,18 @@ export const monitoredSagas: Record<string, MonitoredSaga> = {
export const monitoredSagaReducers = getMonitoredSagaReducers(monitoredSagas)
export function* mobileSaga() {
// wait until redux-persist has finished rehydration
while (true) {
if (
yield* select(
(state: { _persist?: PersistState }): boolean | undefined => state._persist?.rehydrated
)
) {
break
}
yield* delay(REHYDRATION_STATUS_POLLING_INTERVAL)
}
for (const s of sagas) {
yield* spawn(s)
}
......
import { ImpactFeedbackStyle } from 'expo-haptics'
import { memo, useMemo } from 'react'
import { I18nManager } from 'react-native'
import { SharedValue } from 'react-native-reanimated'
import { SharedValue, useDerivedValue } from 'react-native-reanimated'
import { LineChart, LineChartProvider } from 'react-native-wagmi-charts'
import PriceExplorerAnimatedNumber from 'src/components/PriceExplorer/PriceExplorerAnimatedNumber'
import { PriceExplorerError } from 'src/components/PriceExplorer/PriceExplorerError'
......@@ -24,7 +24,7 @@ type PriceTextProps = {
loading: boolean
relativeChange?: SharedValue<number>
numberOfDigits: PriceNumberOfDigits
spotPrice?: number
spotPrice?: SharedValue<number>
}
function PriceTextSection({ loading, numberOfDigits, spotPrice }: PriceTextProps): JSX.Element {
......@@ -34,9 +34,6 @@ function PriceTextSection({ loading, numberOfDigits, spotPrice }: PriceTextProps
return (
<Flex mx={mx}>
{/* Specify maxWidth to allow text scaling. onLayout was sometimes called after more
than 5 seconds which is not acceptable so we have to provide the approximate width
of the PriceText component explicitly. */}
<PriceExplorerAnimatedNumber
currency={currency}
numberOfDigits={numberOfDigits}
......@@ -84,14 +81,15 @@ export const PriceExplorer = memo(function PriceExplorer({
return { lastPricePoint: lastPoint, convertedPriceHistory: priceHistory }
}, [data, conversionRate])
const convertedSpotValue = useDerivedValue(() => conversionRate * (data?.spot?.value?.value ?? 0))
const convertedSpot = useMemo((): TokenSpotData | undefined => {
return (
data?.spot && {
...data?.spot,
value: { value: conversionRate * (data?.spot?.value?.value ?? 0) },
value: convertedSpotValue,
}
)
}, [data, conversionRate])
}, [data, convertedSpotValue])
if (
!loading &&
......@@ -135,7 +133,7 @@ export const PriceExplorer = memo(function PriceExplorer({
loading={loading}
numberOfDigits={numberOfDigits}
relativeChange={convertedSpot?.relativeChange}
spotPrice={convertedSpot?.value.value}
spotPrice={convertedSpot?.value}
/>
{content}
<TimeRangeGroup setDuration={setDuration} />
......
......@@ -40,6 +40,19 @@ const getEmphasizedNumberColor = (
return emphasizedColor
}
const shouldUseSeparator = (
index: number,
commaIndex: number,
decimalPlaceIndex: number
): boolean => {
'worklet'
return (
(index - commaIndex) % 4 === 0 &&
index - commaIndex < 0 &&
index > commaIndex - decimalPlaceIndex
)
}
const NumbersMain = ({
color,
backgroundColor,
......@@ -163,11 +176,7 @@ const RollNumber = ({
// need it in case the current value is eg $999.00 but maximum value in chart is more than $1,000.00
// so it can hide the comma to avoid something like $,999.00
const animatedWrapperSeparatorStyle = useAnimatedStyle(() => {
const isSeparator =
(index - commaIndex) % 4 === 0 &&
index - commaIndex < 0 &&
index > commaIndex - decimalPlace.value
if (!isSeparator) {
if (!shouldUseSeparator(index, commaIndex, decimalPlace.value)) {
return {
width: withTiming(0),
}
......@@ -202,11 +211,7 @@ const RollNumber = ({
)
}
if (
(index - commaIndex) % 4 === 0 &&
index - commaIndex < 0 &&
index > commaIndex - decimalPlace.value
) {
if ((index - commaIndex) % 4 === 0 && index - commaIndex < 0) {
return (
<Animated.View style={animatedWrapperSeparatorStyle}>
<Animated.Text
......
......@@ -29,9 +29,9 @@ export function PriceExplorerError({
justifyContent="center"
overflow="hidden">
<BaseCard.ErrorState
description={t('token.priceExplorer.error.description')}
retryButtonLabel={showRetry ? t('common.button.retry') : undefined}
title={t('token.priceExplorer.error.title')}
description={t('Something went wrong.')}
retryButtonLabel={showRetry ? t('Retry') : undefined}
title={t('Couldn’t load price chart')}
onRetry={onRetry}
/>
</Flex>
......
......@@ -2,7 +2,7 @@ import React from 'react'
import * as charts from 'react-native-wagmi-charts'
import { DatetimeText, PriceText, RelativeChangeText } from 'src/components/PriceExplorer/Text'
import { render, within } from 'src/test/test-utils'
import { amounts } from 'wallet/src/test/fixtures'
import { Amounts } from 'wallet/src/test/gqlFixtures'
jest.mock('react-native-wagmi-charts')
const mockedUseLineChartPrice = charts.useLineChartPrice as jest.Mock
......@@ -12,7 +12,7 @@ const mockedUseLineChartDatetime = charts.useLineChartDatetime as jest.Mock
describe(PriceText, () => {
it('renders without error', () => {
mockedUseLineChartPrice.mockReturnValue({ value: '' })
mockedUseLineChart.mockReturnValue({ data: [{ timestamp: 0, value: amounts.md().value }] })
mockedUseLineChart.mockReturnValue({ data: [{ timestamp: 0, value: Amounts.md.value }] })
const tree = render(<PriceText loading={false} />)
......@@ -21,7 +21,7 @@ describe(PriceText, () => {
it('renders without error less than a dollar', () => {
mockedUseLineChartPrice.mockReturnValue({ value: '' })
mockedUseLineChart.mockReturnValue({ data: [{ timestamp: 0, value: amounts.xs().value }] })
mockedUseLineChart.mockReturnValue({ data: [{ timestamp: 0, value: Amounts.xs.value }] })
const tree = render(<PriceText loading={false} />)
......@@ -39,7 +39,7 @@ describe(PriceText, () => {
it('shows active price when scrubbing', async () => {
mockedUseLineChartPrice.mockReturnValue({
value: { value: amounts.sm().value.toString() },
value: { value: Amounts.sm.value.toString() },
})
const tree = render(<PriceText loading={false} />)
......@@ -48,7 +48,7 @@ describe(PriceText, () => {
const wholePart = await within(animatedText).findByTestId('wholePart')
const decimalPart = await within(animatedText).findByTestId('decimalPart')
expect(wholePart.props.text).toBe(`$${amounts.sm().value}`)
expect(wholePart.props.text).toBe(`$${Amounts.sm.value}`)
expect(decimalPart.props.text).toBe(`.00`)
})
})
......
......@@ -11,25 +11,9 @@ export const CURSOR_SIZE = CURSOR_INNER_SIZE + 6
export const LINE_WIDTH = 1
export const TIME_RANGES = [
[
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,
],
[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],
] as const
......@@ -111,8 +111,9 @@ describe(useLineChartPrice, () => {
})
it('returns currentSpot if it is provided', async () => {
const { result, rerender } = renderHookWithProviders(useLineChartPrice, {
initialProps: [1],
const spotPrice = makeMutable(1)
const { result } = renderHookWithProviders(useLineChartPrice, {
initialProps: [spotPrice],
})
expect(result.current).toEqual({
......@@ -121,9 +122,7 @@ describe(useLineChartPrice, () => {
shouldAnimate: expect.objectContaining({ value: true }),
})
await act(() => {
rerender([2])
})
spotPrice.value = 2
await waitFor(() => {
expect(result.current).toEqual({
......@@ -150,7 +149,7 @@ describe(useLineChartPrice, () => {
it('returns active cursor price even if currentSpot and data are provided', async () => {
mockCursorPrice('3')
const { result } = renderHookWithProviders(useLineChartPrice, {
initialProps: [4],
initialProps: [makeMutable(4)],
})
expect(result.current).toEqual({
......@@ -163,7 +162,7 @@ describe(useLineChartPrice, () => {
it('updates returned active cursor price when it changes', async () => {
mockCursorPrice('1')
const { result } = renderHookWithProviders(useLineChartPrice, {
initialProps: [4],
initialProps: [makeMutable(4)],
})
expect(result.current).toEqual(
......
......@@ -26,7 +26,9 @@ export type ValueAndFormattedWithAnimation = ValueAndFormatted & {
* Wrapper around react-native-wagmi-chart#useLineChartPrice
* @returns latest price when not scrubbing and active price when scrubbing
*/
export function useLineChartPrice(currentSpot?: number): ValueAndFormattedWithAnimation {
export function useLineChartPrice(
currentSpot?: SharedValue<number>
): ValueAndFormattedWithAnimation {
const { value: activeCursorPrice } = useRNWagmiChartLineChartPrice({
// do not round
precision: 18,
......@@ -55,7 +57,7 @@ export function useLineChartPrice(currentSpot?: number): ValueAndFormattedWithAn
shouldAnimate.value = true
// show spot price when chart not scrubbing, or if not available, show the last price in the chart
return currentSpot ?? data[data.length - 1]?.value ?? 0
return currentSpot?.value ?? data[data.length - 1]?.value ?? 0
})
const priceFormatted = useDerivedValue(() => {
const { symbol, code } = currencyInfo
......
......@@ -95,20 +95,22 @@ export function useTokenPriceHistory(
}, [priceHistory])
const numberOfDigits = useMemo(() => {
const max = maxBy(priceHistory, 'value')
const convertedMaxValue = convertFiatAmount(max?.value).amount
const maxPriceInHistory = maxBy(priceHistory, 'value')?.value
// If there is neither max price in history nor current price, return last number of digits
if (!maxPriceInHistory && price === undefined) {
return lastNumberOfDigits.current
}
const maxPrice = Math.max(maxPriceInHistory || 0, price || 0)
const convertedMaxValue = convertFiatAmount(maxPrice).amount
if (max) {
const newNumberOfDigits = {
left: String(convertedMaxValue).split('.')[0]?.length || 10,
right: Number(String(convertedMaxValue.toFixed(10)).split('.')[0]) > 0 ? 2 : 10,
}
lastNumberOfDigits.current = newNumberOfDigits
return newNumberOfDigits
}
return lastNumberOfDigits.current
}, [convertFiatAmount, priceHistory])
return newNumberOfDigits
}, [convertFiatAmount, priceHistory, price])
const retry = useCallback(async () => {
await refetch({ contract: currencyIdToContractInput(currencyId) })
......
import { BarCodeScanner, BarCodeScannerResult } from 'expo-barcode-scanner'
import { Camera, CameraType } from 'expo-camera'
import { PermissionStatus } from 'expo-modules-core'
import React, { memo, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
......@@ -50,7 +51,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
const colors = useSporeColors()
const dimensions = useDeviceDimensions()
const [permissionResponse, requestPermissionResponse] = BarCodeScanner.usePermissions()
const [permissionResponse, requestPermissionResponse] = Camera.useCameraPermissions()
const permissionStatus = permissionResponse?.status
const [isReadingImageFile, setIsReadingImageFile] = useState(false)
......@@ -94,7 +95,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
)[0]
if (!result) {
Alert.alert(t('qrScanner.error.none'))
Alert.alert(t('No QR code found'))
setIsReadingImageFile(false)
return
}
......@@ -109,12 +110,16 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
}
if (permissionStatus === PermissionStatus.DENIED) {
Alert.alert(t('qrScanner.error.camera.title'), t('qrScanner.error.camera.message'), [
{ text: t('common.navigation.systemSettings'), onPress: openSettings },
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('common.button.notNow'),
text: t('Not now'),
},
])
]
)
}
}, [permissionStatus, requestPermissionResponse, t])
......@@ -136,10 +141,12 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
overflow="hidden"
width={dimensions.fullWidth}>
{permissionStatus === PermissionStatus.GRANTED && !isReadingImageFile && (
<BarCodeScanner
barCodeTypes={[BarCodeScanner.Constants.BarCodeType.qr]}
<Camera
barCodeScannerSettings={{
barCodeTypes: [BarCodeScanner.Constants.BarCodeType.qr],
}}
style={StyleSheet.absoluteFillObject}
type={BarCodeScanner.Constants.Type.back}
type={CameraType.back}
onBarCodeScanned={shouldFreezeCamera ? undefined : handleBarCodeScanned}
/>
)}
......@@ -173,7 +180,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
width="100%"
onLayout={(event: LayoutChangeEvent): void => setInfoLayout(event.nativeEvent.layout)}>
<Text color="$neutral1" variant="heading3">
{t('qrScanner.title')}
{t('Scan a QR code')}
</Text>
</Flex>
{!shouldFreezeCamera ? (
......@@ -201,9 +208,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
</Flex>
<Flex style={{ marginTop: LOADER_SIZE + spacing.spacing24 }} />
<Text color="$neutral1" textAlign="center" variant="body1">
{isWalletConnectModal
? t('qrScanner.status.connecting')
: t('qrScanner.status.loading')}
{isWalletConnectModal ? t('Connecting...') : t('Loading...')}
</Text>
</Flex>
</Flex>
......@@ -266,7 +271,11 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
icon={<Icons.Global color="$neutral2" />}
theme="secondary"
onPress={props.onPressConnections}>
{t('qrScanner.button.connections', { count: props.numConnections })}
{props.numConnections === 1
? t('1 app connected')
: t('{{numConnections}} apps connected', {
numConnections: props.numConnections,
})}
</Button>
)}
</Flex>
......
......@@ -61,7 +61,7 @@ export function WalletQRCode({ address }: Props): JSX.Element | null {
/>
<Text color="$neutral2" lineHeight={20} textAlign="center" variant="body3">
{t('qrScanner.wallet.title')}
{t('You can send tokens on all of our supported networks to this address.')}
</Text>
<TouchableArea onPress={(): void => setShowModal(true)}>
<Flex row gap="$spacing4">
......@@ -78,8 +78,10 @@ export function WalletQRCode({ address }: Props): JSX.Element | null {
{showModal && (
<WarningModal
backgroundIconColor={colors.surface1.val}
caption={t('qrScanner.wallet.networks.description')}
closeText={t('common.button.close')}
caption={t(
'Uniswap Wallet supports tokens on Ethereum, Polygon, Arbitrum, Optimism, Base, and BNB Chain. Right now, we only support NFTs on Ethereum.'
)}
closeText={t('Close')}
icon={
<NetworkLogos
centered
......@@ -89,7 +91,7 @@ export function WalletQRCode({ address }: Props): JSX.Element | null {
/>
}
modalName={ModalName.QRCodeNetworkInfo}
title={t('qrScanner.wallet.networks.title')}
title={t('Supported Networks')}
onClose={(): void => setShowModal(false)}>
<LearnMoreLink url={uniswapUrls.helpArticleUrls.supportedNetworks} />
</WarningModal>
......
......@@ -44,14 +44,18 @@ export function RecipientScanModal({ onSelectRecipient, onClose }: Props): JSX.E
onSelectRecipient(supportedURI.value)
onClose()
} else {
Alert.alert(t('qrScanner.recipient.error.title'), t('qrScanner.recipient.error.message'), [
Alert.alert(
t('Invalid QR Code'),
t('Make sure that you’re scanning a valid Ethereum address QR code before trying again.'),
[
{
text: t('common.button.tryAgain'),
text: t('Try again'),
onPress: (): void => {
setShouldFreezeCamera(false)
},
},
])
]
)
}
}
......@@ -103,8 +107,8 @@ export function RecipientScanModal({ onSelectRecipient, onClose }: Props): JSX.E
)}
<Text color="$neutral1" variant="buttonLabel2">
{currentScreenState === ScannerModalState.ScanQr
? t('qrScanner.recipient.action.show')
: t('qrScanner.recipient.action.scan')}
? t('Show my QR code')
: t('Scan a QR code')}
</Text>
</Flex>
</TouchableArea>
......
......@@ -75,22 +75,22 @@ export function _RecipientSelect({
mt="$spacing16"
px="$spacing24">
<Flex row>
<Text variant="subheading1">{t('qrScanner.recipient.label.send')}</Text>
<Text variant="subheading1">{t('Send')}</Text>
</Flex>
<SearchBar
autoFocus
backgroundColor="$surface2"
endAdornment={<QRScannerIconButton onPress={onPressQRScanner} />}
placeholder={t('qrScanner.recipient.input.placeholder')}
placeholder={t('Search ENS or address')}
value={pattern ?? ''}
onBack={recipient ? onToggleShowRecipientSelector : undefined}
onChangeText={onChangePattern}
/>
{noResults ? (
<Flex centered gap="$spacing12" mt="$spacing24" px="$spacing24">
<Text variant="buttonLabel2">{t('qrScanner.recipient.results.empty')}</Text>
<Text variant="buttonLabel2">{t('No results found')}</Text>
<Text color="$neutral3" textAlign="center" variant="body1">
{t('qrScanner.recipient.results.error')}
{t('The address you typed either does not exist or is spelled incorrectly.')}
</Text>
</Flex>
) : (
......
......@@ -8,34 +8,20 @@ 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 { TransactionStatus } from 'wallet/src/features/transactions/types'
import { SendTokenTransactionInfo } 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,
sendTokenTransactionInfo,
signerMnemonicAccount,
transactionDetails,
sendTxDetailsConfirmed,
sendTxDetailsFailed,
sendTxDetailsPending,
} 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
*/
......@@ -72,8 +58,8 @@ const getPreloadedState = (props?: PreloadedStateProps): PreloadedState<MobileSt
}
}
const activeAccount = signerMnemonicAccount()
const inactiveAccount = signerMnemonicAccount()
const activeAccount = account
const inactiveAccount = account2
const validatedAddressRecipient: SearchableRecipient = {
address: SAMPLE_SEED_ADDRESS_1,
}
......@@ -89,15 +75,15 @@ const recentRecipientsSectionResult = {
title: 'Recent',
data: [
{
address: sendTxDetailsFailed.typeInfo.recipient,
address: (sendTxDetailsFailed.typeInfo as SendTokenTransactionInfo).recipient,
name: '',
},
{
address: sendTxDetailsConfirmed.typeInfo.recipient,
address: (sendTxDetailsConfirmed.typeInfo as SendTokenTransactionInfo).recipient,
name: '',
},
{
address: sendTxDetailsPending.typeInfo.recipient,
address: (sendTxDetailsPending.typeInfo as SendTokenTransactionInfo).recipient,
name: '',
},
],
......@@ -176,7 +162,9 @@ describe(useRecipients, () => {
expect(result.current.searchableRecipientOptions).toEqual(
expect.arrayContaining([
{
data: expect.objectContaining({ address: SAMPLE_SEED_ADDRESS_1 }),
data: {
address: SAMPLE_SEED_ADDRESS_1,
},
key: SAMPLE_SEED_ADDRESS_1,
},
])
......@@ -215,7 +203,7 @@ describe(useRecipients, () => {
title: 'Recent',
data: [
{
address: sendTxDetailsPending.typeInfo.recipient,
address: (sendTxDetailsPending.typeInfo as SendTokenTransactionInfo).recipient,
name: '',
},
],
......@@ -243,15 +231,15 @@ describe(useRecipients, () => {
// This method doesn't check the order of the elements
expect(section.data).toIncludeSameMembers([
{
address: sendTxDetailsPending.typeInfo.recipient,
address: (sendTxDetailsPending.typeInfo as SendTokenTransactionInfo).recipient,
name: '',
},
{
address: sendTxDetailsConfirmed.typeInfo.recipient,
address: (sendTxDetailsConfirmed.typeInfo as SendTokenTransactionInfo).recipient,
name: '',
},
{
address: sendTxDetailsFailed.typeInfo.recipient,
address: (sendTxDetailsFailed.typeInfo as SendTokenTransactionInfo).recipient,
name: '',
},
])
......
......@@ -29,7 +29,9 @@ export function RemoveLastMnemonicWalletFooter({
text={
<Flex>
<Text color="$neutral2" variant="body3">
{t('account.wallet.remove.check')}
{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.'
)}
</Text>
</Flex>
}
......@@ -44,7 +46,7 @@ export function RemoveLastMnemonicWalletFooter({
testID={ElementName.Confirm}
theme="detrimental"
onPress={onPress}>
{!inProgress ? t('account.wallet.button.remove') : undefined}
{!inProgress ? t('Remove wallet') : undefined}
</Button>
</Flex>
</>
......
......@@ -189,7 +189,7 @@ export function RemoveWalletModal(): JSX.Element | null {
<AnimatedFlex style={animatedCancelButtonSpanStyles} />
) : (
<Button fill disabled={inProgress} theme="outline" onPress={onClose}>
{t('common.button.cancel')}
{t('Cancel')}
</Button>
)}
<Button
......
......@@ -9,7 +9,7 @@ import WalletIcon from 'ui/src/assets/icons/wallet-filled.svg'
import { ThemeNames } from 'ui/src/theme'
import { Account, AccountType } from 'wallet/src/features/wallet/accounts/types'
import { useDisplayName } from 'wallet/src/features/wallet/hooks'
import { getCloudProviderName } from 'wallet/src/utils/platform'
import { isAndroid } from 'wallet/src/utils/platform'
export enum RemoveWalletStep {
Warning = 'warning',
......@@ -49,19 +49,21 @@ export const useModalContent = ({
if (isRemovingRecoveryPhrase && !isReplacing && currentStep === RemoveWalletStep.Warning) {
return {
title: (
<Trans t={t}>
<Text color="$neutral1" variant="body1">
<Trans i18nKey="account.seedPhrase.remove.initial.title">
You’re removing
You’re removing{' '}
<Text color="$statusCritical" variant="body1">
{{ walletName: displayName?.name }}
{{ wallet: displayName?.name }}
</Text>
</Trans>
</Text>
</Trans>
),
description: t(
'This will remove your wallet from this device along with your recovery phrase.'
),
description: t('account.seedPhrase.remove.initial.description'),
Icon: TrashIcon,
iconColorLabel: 'statusCritical',
actionButtonLabel: t('common.button.continue'),
actionButtonLabel: t('Continue'),
actionButtonTheme: 'detrimental',
}
}
......@@ -69,11 +71,13 @@ export const useModalContent = ({
// 1st speed bump when replacing recovery phrase
if (isRemovingRecoveryPhrase && isReplacing && currentStep === RemoveWalletStep.Warning) {
return {
title: t('account.wallet.button.import'),
description: t('account.seedPhrase.remove.import.description'),
title: t('Import a new wallet'),
description: t(
'You can only store one recovery phrase at a time. To continue importing a new one, you’ll need to remove your current recovery phrase and any associated wallets from this device.'
),
Icon: WalletIcon,
iconColorLabel: 'neutral2',
actionButtonLabel: t('common.button.continue'),
actionButtonLabel: t('Continue'),
actionButtonTheme: 'secondary',
}
}
......@@ -82,19 +86,25 @@ export const useModalContent = ({
if (isRemovingRecoveryPhrase && currentStep === RemoveWalletStep.Final) {
return {
title: (
<Trans t={t}>
<Text color="$neutral1" variant="body1">
<Trans i18nKey="account.seedPhrase.remove.final.title">
You’re removing your
You’re removing your{' '}
<Text color="$neutral1" variant="body1">
recovery phrase
</Text>
</Trans>
</Text>
</Trans>
),
description: (
<Trans i18nKey="account.seedPhrase.remove.final.description">
Make sure you’ve written down your recovery phrase or backed it up on
{{ cloudProviderName: getCloudProviderName() }}.
description: isAndroid ? (
<Trans t={t}>
Make sure you’ve written down your recovery phrase or backed it up on Google Drive.{' '}
<Text color="$statusCritical" maxFontSizeMultiplier={1.4} variant="body3">
You will not be able to access your funds otherwise.
</Text>
</Trans>
) : (
<Trans t={t}>
Make sure you’ve written down your recovery phrase or backed it up on iCloud.{' '}
<Text color="$statusCritical" maxFontSizeMultiplier={1.4} variant="body3">
You will not be able to access your funds otherwise.
</Text>
......@@ -109,32 +119,34 @@ export const useModalContent = ({
if (account?.type === AccountType.SignerMnemonic && currentStep === RemoveWalletStep.Final) {
const associatedAccountNames = concatListOfAccountNames(
associatedAccounts.filter((aa) => aa.address !== account?.address),
', '
t('and')
)
return {
title: (
<Trans t={t}>
<Text color="$neutral1" variant="body1">
<Trans i18nKey="account.seedPhrase.remove.initial.title">
You’re removing
You’re removing{' '}
<Text color="$statusCritical" variant="body1">
{{ walletName: displayName?.name }}
{{ wallet: displayName?.name }}
</Text>
</Trans>
</Text>
</Trans>
),
description: (
<Trans i18nKey="account.seedPhrase.remove.mnemonic.description">
It shares the same recovery phrase as
<Text color="$neutral2" variant="body3">
<Trans t={t}>
It shares the same recovery phrase as{' '}
<Text color="$neutral1" variant="body3">
{{ walletNames: associatedAccountNames }}
{{ wallets: associatedAccountNames }}
</Text>
. Your recovery phrase will remain stored until you delete all remaining wallets.
</Trans>
</Text>
),
Icon: TrashIcon,
iconColorLabel: 'statusCritical',
actionButtonLabel: t('common.button.remove'),
actionButtonLabel: t('Remove'),
actionButtonTheme: 'detrimental',
}
}
......@@ -143,19 +155,21 @@ export const useModalContent = ({
if (account?.type === AccountType.Readonly && currentStep === RemoveWalletStep.Final) {
return {
title: (
<Trans t={t}>
<Text color="$neutral1" variant="body1">
<Trans i18nKey="account.seedPhrase.remove.initial.title">
You’re removing
You’re removing{' '}
<Text color="$neutral2" variant="body1">
{{ walletName: displayName?.name }}
{{ wallet: displayName?.name }}
</Text>
</Trans>
</Text>
</Trans>
),
description: t(
'You can always add back view-only wallets by entering the wallet’s address.'
),
description: t('account.wallet.remove.viewOnly'),
Icon: TrashIcon,
iconColorLabel: 'neutral2',
actionButtonLabel: t('common.button.remove'),
actionButtonLabel: t('Remove'),
actionButtonTheme: 'secondary',
}
}
......
......@@ -51,17 +51,19 @@ export function RestoreWalletModal(): JSX.Element | null {
/>
</Flex>
<Text textAlign="center" variant="body1">
{t('account.wallet.button.restore')}
{t('Restore wallet')}
</Text>
<Text color="$neutral2" textAlign="center" variant="body2">
{t('account.wallet.restore.description')}
{t(
'Because you’re on a new device, you’ll need to restore your recovery phrase. This will allow you to swap and send tokens.'
)}
</Text>
<Flex centered row gap="$spacing12" pt="$spacing12">
<Button fill theme="tertiary" onPress={onDismiss}>
{t('common.button.dismiss')}
{t('Dismiss')}
</Button>
<Button fill testID={ElementName.RestoreWallet} theme="primary" onPress={onRestore}>
{t('common.button.restore')}
{t('Restore')}
</Button>
</Flex>
</Flex>
......
......@@ -7,7 +7,6 @@ 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
......@@ -21,19 +20,18 @@ export function BiometricAuthWarningModal({
onClose,
}: Props): JSX.Element {
const { t } = useTranslation()
const biometricsMethod = useBiometricName(isTouchIdDevice)
const authenticationTypeName = useBiometricName(isTouchIdDevice)
return (
<WarningModal
caption={
isAndroid
? t('settings.setting.biometrics.warning.message.android')
: t('settings.setting.biometrics.warning.message.ios', { biometricsMethod })
}
closeText={t('common.button.back')}
confirmText={t('common.button.skip')}
caption={t(
'If you don’t turn on {{authenticationTypeName}}, anyone who gains access to your device can open Uniswap Wallet and make transactions.',
{ authenticationTypeName }
)}
closeText={t('Back')}
confirmText={t('Skip')}
modalName={ModalName.FaceIDWarning}
severity={WarningSeverity.Low}
title={t('settings.setting.biometrics.warning.title')}
title={t('Are you sure?')}
onClose={onClose}
onConfirm={onConfirm}
/>
......
......@@ -164,7 +164,7 @@ export const TokenBalanceListInner = forwardRef<
const ListHeaderComponent = useMemo(() => {
return hasError ? (
<AnimatedFlex entering={FadeInDown} exiting={FadeOut} px="$spacing24" py="$spacing8">
<BaseCard.InlineErrorState title={t('home.tokens.error.fetch')} onRetry={refetch} />
<BaseCard.InlineErrorState title={t('Failed to fetch token balances')} onRetry={refetch} />
</AnimatedFlex>
) : null
}, [hasError, refetch, t])
......@@ -204,8 +204,8 @@ export const TokenBalanceListInner = forwardRef<
) : (
<Flex fill grow justifyContent="center" style={containerProps?.emptyContainerStyle}>
<BaseCard.ErrorState
retryButtonLabel={t('common.button.retry')}
title={t('home.tokens.error.load')}
retryButtonLabel="Retry"
title={t('Couldn’t load token balances')}
onRetry={(): void | undefined => refetch?.()}
/>
</Flex>
......
......@@ -68,7 +68,7 @@ export function TokenBalances({
{hasOtherChainBalances && otherChainBalances ? (
<Flex gap="$spacing8">
<Text color="$neutral2" variant="subheading2">
{t('token.balances.other')}
{t('Balances on other networks')}
</Text>
<Flex gap="$spacing12">
{otherChainBalances.map((balance) => {
......@@ -106,9 +106,7 @@ export function CurrentChainBalance({
<Flex row>
<Flex fill gap="$spacing8">
<Text color="$neutral2" variant="subheading2">
{isReadonly
? t('token.balances.viewOnly', { ownerAddress: displayName })
: t('token.balances.main')}
{isReadonly ? t('{{owner}}’s balance', { owner: displayName }) : t('Your balance')}
</Text>
<Flex fill gap="$spacing4">
<Text variant="heading3">
......
......@@ -58,13 +58,13 @@ export function TokenDetailsActionButtons({
px="$spacing16">
<CTAButton
element={ElementName.Buy}
title={t('common.button.buy')}
title={t('Buy')}
tokenColor={tokenColor}
onPress={onPressBuy}
/>
<CTAButton
element={ElementName.Sell}
title={t('common.button.sell')}
title={t('Sell')}
tokenColor={tokenColor}
onPress={onPressSell}
/>
......
......@@ -35,7 +35,7 @@ export function TokenDetailsLinks({
<View style={{ marginHorizontal: -14 }}>
<Flex gap="$spacing8">
<Text color="$neutral2" mx="$spacing16" variant="subheading2">
{t('token.links.title')}
{t('Links')}
</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<Flex row gap="$spacing8" px="$spacing16">
......@@ -51,7 +51,7 @@ export function TokenDetailsLinks({
Icon={GlobeIcon}
buttonType={LinkButtonType.Link}
element={ElementName.TokenLinkWebsite}
label={t('token.links.website')}
label={t('Website')}
value={homepageUrl}
/>
)}
......@@ -60,7 +60,7 @@ export function TokenDetailsLinks({
Icon={TwitterIcon}
buttonType={LinkButtonType.Link}
element={ElementName.TokenLinkTwitter}
label={t('token.links.twitter')}
label={t('Twitter')}
value={getTwitterLink(twitterName)}
/>
)}
......@@ -68,7 +68,7 @@ export function TokenDetailsLinks({
<LinkButton
buttonType={LinkButtonType.Copy}
element={ElementName.Copy}
label={t('token.links.contract')}
label={t('Contract')}
value={address}
/>
)}
......
......@@ -57,7 +57,7 @@ export function TokenDetailsMarketData({
return (
<Flex gap="$spacing8">
<StatsRow
label={t('token.stats.marketCap')}
label={t('Market Cap')}
statsIcon={
<Icons.ChartPie color={tokenColor ?? defaultTokenColor} size={iconSizes.icon16} />
}>
......@@ -66,7 +66,7 @@ export function TokenDetailsMarketData({
</Text>
</StatsRow>
<StatsRow
label={t('token.stats.fullyDilutedValuation')}
label={t('Fully Diluted Valuation')}
statsIcon={
<Icons.ChartPie color={tokenColor ?? defaultTokenColor} size={iconSizes.icon16} />
}>
......@@ -75,7 +75,7 @@ export function TokenDetailsMarketData({
</Text>
</StatsRow>
<StatsRow
label={t('token.stats.volume')}
label={t('24h Volume')}
statsIcon={
<Icons.ChartBar color={tokenColor ?? defaultTokenColor} size={iconSizes.icon16} />
}>
......@@ -84,7 +84,7 @@ export function TokenDetailsMarketData({
</Text>
</StatsRow>
<StatsRow
label={t('token.stats.priceHighYear')}
label={t('52W High')}
statsIcon={
<Icons.TrendUp color={tokenColor ?? defaultTokenColor} size={iconSizes.icon16} />
}>
......@@ -93,7 +93,7 @@ export function TokenDetailsMarketData({
</Text>
</StatsRow>
<StatsRow
label={t('token.stats.priceLowYear')}
label={t('52W Low')}
statsIcon={
<Icons.TrendDown color={tokenColor ?? defaultTokenColor} size={iconSizes.icon16} />
}>
......@@ -147,7 +147,7 @@ export function TokenDetailsStats({
<Flex gap="$spacing4">
{name && (
<Text color="$neutral2" variant="subheading2">
{t('token.stats.section.about', { token: name })}
{t('About {{ token }}', { token: name })}
</Text>
)}
<Flex gap="$spacing16">
......@@ -177,7 +177,7 @@ export function TokenDetailsStats({
</Text>
</Flex>
<Text color="$blue400" variant="buttonLabel4">
{t('token.stats.translation.original')}
{t('Show original')}
</Text>
</Flex>
) : (
......@@ -185,7 +185,7 @@ export function TokenDetailsStats({
<Flex row alignItems="center" gap="$spacing12">
<Icons.Language color="$neutral2" size="$icon.20" />
<Text color="$neutral2" variant="body3">
{t('token.stats.translation.translate', {
{t('Translate to {{ language }}', {
language: currentLanguageInfo.displayName,
})}
</Text>
......@@ -199,7 +199,7 @@ export function TokenDetailsStats({
)}
<Flex gap="$spacing4">
<Text color="$neutral2" variant="subheading2">
{t('token.stats.title')}
{t('Stats')}
</Text>
<TokenDetailsMarketData
fullyDilutedValuation={fullyDilutedValuation}
......
import { useCrossChainBalances, useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks'
import { Screens } from 'src/screens/Screens'
import { act, renderHook, waitFor } from 'src/test/test-utils'
import { USDBC_BASE, USDC_ARBITRUM } from 'wallet/src/constants/tokens'
import { Chain } from 'wallet/src/data/__generated__/types-and-hooks'
import { fromGraphQLChain } from 'wallet/src/features/chains/utils'
import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils'
import {
SAMPLE_CURRENCY_ID_1,
portfolio,
portfolioBalances,
tokenBalance,
usdcArbitrumToken,
usdcBaseToken,
} from 'wallet/src/test/fixtures'
import { mockWalletPreloadedState } from 'wallet/src/test/mocks'
import { SAMPLE_CURRENCY_ID_1, mockWalletPreloadedState } from 'wallet/src/test/fixtures'
import { Portfolio, Portfolio2, PortfolioBalancesById } from 'wallet/src/test/gqlFixtures'
const mockedNavigation = {
navigate: jest.fn(),
......@@ -32,7 +28,7 @@ describe(useCrossChainBalances, () => {
describe('currentChainBalance', () => {
it('returns null if there are no balances for the specified currency', async () => {
const { result } = renderHook(() => useCrossChainBalances(SAMPLE_CURRENCY_ID_1, null), {
preloadedState: mockWalletPreloadedState(),
preloadedState: mockWalletPreloadedState,
})
await act(() => undefined)
......@@ -45,25 +41,19 @@ describe(useCrossChainBalances, () => {
})
it('returns balance if there is at least one for the specified currency', async () => {
const Portfolio = portfolio()
const currentChainBalance = portfolioBalances({ portfolio: Portfolio })[0]!
const { result } = renderHook(
() => useCrossChainBalances(currentChainBalance.currencyInfo.currencyId, null),
{
preloadedState: mockWalletPreloadedState(),
const { result } = renderHook(() => useCrossChainBalances(SAMPLE_CURRENCY_ID_1, null), {
preloadedState: mockWalletPreloadedState,
resolvers: {
Query: {
portfolios: () => [Portfolio],
},
},
}
)
})
await waitFor(() => {
expect(result.current).toEqual(
expect.objectContaining({
currentChainBalance,
currentChainBalance: PortfolioBalancesById[SAMPLE_CURRENCY_ID_1],
})
)
})
......@@ -71,9 +61,15 @@ 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)
......@@ -86,35 +82,25 @@ describe(useCrossChainBalances, () => {
})
it('does not include current chain balance in other chain balances', async () => {
const tokenBalances = [
tokenBalance({ token: usdcBaseToken() }),
tokenBalance({ token: usdcArbitrumToken() }),
const bridgeInfo: { chain: Chain; address?: string }[] = [
{ chain: Chain.Base, address: USDBC_BASE.address.toLocaleLowerCase() },
{ chain: Chain.Arbitrum, address: USDC_ARBITRUM.address.toLocaleLowerCase() },
]
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(),
const { result } = renderHook(() => useCrossChainBalances(currencyId1, bridgeInfo), {
preloadedState: mockWalletPreloadedState,
resolvers: {
Query: {
portfolios: () => [Portfolio],
portfolios: () => [Portfolio2],
},
},
}
)
})
await waitFor(() => {
expect(result.current).toEqual(
expect.objectContaining({ currentChainBalance, otherChainBalances })
expect.objectContaining({
currentChainBalance: PortfolioBalancesById[currencyId1],
otherChainBalances: [PortfolioBalancesById[currencyId2]],
})
)
})
})
......
......@@ -71,8 +71,8 @@ function _TokenFiatOnRampList({
return (
<Flex centered grow>
<BaseCard.ErrorState
retryButtonLabel={t('common.button.retry')}
title={t('fiatOnRamp.error.load')}
retryButtonLabel="Retry"
title={t('Couldn’t load tokens to buy')}
onRetry={onRetry}
/>
</Flex>
......
......@@ -50,7 +50,7 @@ export function ConnectedDappsList({ backButton, sessions }: ConnectedDappsProps
</Flex>
<Flex alignItems="center" flexBasis="70%">
<Text color="$neutral1" numberOfLines={1} variant="body1">
{t('walletConnect.dapps.manage.title')}
{t('Manage connections')}
</Text>
</Flex>
<Flex alignItems="flex-end" flexBasis="15%">
......@@ -101,10 +101,10 @@ export function ConnectedDappsList({ backButton, sessions }: ConnectedDappsProps
paddingTop: fullHeight / 5,
}}>
<Text color="$neutral1" variant="subheading1">
{t('walletConnect.dapps.manage.empty.title')}
{t('No apps connected')}
</Text>
<Text color="$neutral2" textAlign="center" variant="body2">
{t('walletConnect.dapps.empty.description')}
{t('Connect to an app by scanning a code via WalletConnect')}
</Text>
</Flex>
)}
......
import { getSdkError } from '@walletconnect/utils'
import React from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { 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 { WalletConnectSession, removeSession } from 'src/features/walletConnect/walletConnectSlice'
import { removeSession, WalletConnectSession } 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,10 +66,8 @@ export function DappConnectedNetworkModal({
<Flex alignItems="center" gap="$spacing8">
<DappHeaderIcon dapp={dapp} />
<Text textAlign="center" variant="buttonLabel2">
<Trans i18nKey="walletConnect.dapps.connection">
<Text variant="body1">Connected to</Text>
{{ dappNameOrUrl: dapp.name || dapp.url }}
</Trans>
<Text variant="body1">{t('Connected to ')}</Text>
{dapp.name || dapp.url}
</Text>
<Text color="$accent1" numberOfLines={1} textAlign="center" variant="buttonLabel4">
{dapp.url}
......@@ -103,10 +101,10 @@ export function DappConnectedNetworkModal({
</Flex>
<Flex centered row gap="$spacing16">
<Button fill theme="secondary" onPress={onClose}>
{t('common.button.close')}
{t('Close')}
</Button>
<Button fill theme="detrimental" onPress={onDisconnect}>
{t('common.button.disconnect')}
{t('Disconnect')}
</Button>
</Flex>
</Flex>
......
......@@ -63,9 +63,7 @@ export function DappConnectionItem({
}
}
const menuActions = [
{ title: t('common.button.disconnect'), systemIcon: 'trash', destructive: true },
]
const menuActions = [{ title: t('Disconnect'), systemIcon: 'trash', destructive: true }]
const onPress = async (e: NativeSyntheticEvent<ContextMenuOnPressNativeEvent>): Promise<void> => {
if (e.nativeEvent.index === 0) {
......
import { Currency } from '@uniswap/sdk-core'
import React from 'react'
import { Trans } from 'react-i18next'
import { Trans, useTranslation } from 'react-i18next'
import { truncateDappName } from 'src/components/WalletConnect/ScanSheet/util'
import { WalletConnectRequest } from 'src/features/walletConnect/walletConnectSlice'
import { Text } from 'ui/src'
......@@ -16,6 +16,7 @@ export function HeaderText({
permitAmount?: number
permitCurrency?: Currency | null
}): JSX.Element {
const { t } = useTranslation()
const { dapp, type: method } = request
if (permitCurrency) {
......@@ -26,54 +27,39 @@ export function HeaderText({
})?.toExact()
return readablePermitAmount ? (
<Trans t={t}>
<Text textAlign="center" variant="heading3">
<Trans i18nKey="qrScanner.request.withAmount">
Allow {{ dappName: dapp.name }} to use up to
<Text fontWeight="bold"> {{ amount: readablePermitAmount }} </Text>
{{ currencySymbol: permitCurrency?.symbol }}?
</Trans>
Allow {dapp.name} to use up to
<Text fontWeight="bold"> {readablePermitAmount} </Text>
{permitCurrency?.symbol}?
</Text>
</Trans>
) : (
<Trans t={t}>
<Text textAlign="center" variant="heading3">
<Trans i18nKey="qrScanner.request.withoutAmount">
Allow <Text fontWeight="bold">{{ dappName: dapp.name }}</Text> to use your
{{ currencySymbol: permitCurrency?.symbol }}?
</Trans>
Allow <Text fontWeight="bold">{dapp.name}</Text> to use your {permitCurrency?.symbol}?
</Text>
</Trans>
)
}
const getReadableMethodName = (ethMethod: EthMethod, dappNameOrUrl: string): JSX.Element => {
const getReadableMethodName = (ethMethod: EthMethod): string => {
switch (ethMethod) {
case EthMethod.PersonalSign:
case EthMethod.EthSign:
case EthMethod.SignTypedData:
return (
<Trans i18nKey="qrScanner.request.method.signature">
Signature request from
<Text fontWeight="bold">{{ dappNameOrUrl }}</Text>
</Trans>
)
return t('Signature request from')
case EthMethod.EthSendTransaction:
return (
<Trans i18nKey="qrScanner.request.method.transaction">
Transaction request from
<Text fontWeight="bold">{{ dappNameOrUrl }}</Text>
</Trans>
)
return t('Transaction request from')
}
return (
<Trans i18nKey="qrScanner.request.method.default">
Request from
<Text fontWeight="bold">{{ dappNameOrUrl }}</Text>
</Trans>
)
return t('Request from')
}
return (
<Text textAlign="center" variant="heading3">
{getReadableMethodName(method, truncateDappName(dapp.name || dapp.url))}
<Text>{getReadableMethodName(method)}</Text>
<Text fontWeight="bold"> {truncateDappName(dapp.name || dapp.url)}</Text>
</Text>
)
}
import { BigNumber } from 'ethers'
import { Transaction, TransactionDescription } from 'no-yolo-signatures'
import React, { useEffect, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { 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,18 +162,15 @@ function TransactionDetails({
) : null}
{to ? (
<Flex row alignItems="center" gap="$spacing16">
<Trans i18nKey="walletConnect.request.details.recipient">
<Text color="$neutral2" variant="body2">
To:
{t('To')}:
</Text>
<AddressButton address={to} chainId={chainId} />
</Trans>
</Flex>
) : null}
<Flex row alignItems="center" gap="$spacing16">
<Trans i18nKey="walletConnect.request.details.function">
<Text color="$neutral2" variant="body2">
Function:
{t('Function')}:
</Text>
<Flex
backgroundColor={isLoading ? '$transparent' : '$surface3'}
......@@ -181,10 +178,9 @@ function TransactionDetails({
px="$spacing8"
py="$spacing4">
<Text color="$neutral1" loading={isLoading} variant="monospace">
{{ functionName: parsedData ? parsedData.name : t('common.text.unknown') }}
{parsedData ? parsedData.name : t('Unknown')}
</Text>
</Flex>
</Trans>
</Flex>
</Flex>
)
......@@ -220,7 +216,7 @@ function RequestDetailsContent({ request }: Props): JSX.Element {
<Text variant="body2">{message}</Text>
) : (
<Text color="$neutral2" variant="body2">
{t('qrScanner.request.message.unavailable')}
{t('No message found.')}
</Text>
)
}
......
import React from 'react'
import { Trans } from 'react-i18next'
import { useTranslation } from 'react-i18next'
import { Flex, Text } from 'ui/src'
import { iconSizes } from 'ui/src/theme'
import { NumberType } from 'utilities/src/format/types'
......@@ -18,6 +18,7 @@ export function SpendingDetails({
value: string
chainId: ChainId
}): JSX.Element {
const { t } = useTranslation()
const { convertFiatAmountFormatted, formatCurrencyAmount } = useLocalizationContext()
const nativeCurrencyInfo = useNativeCurrencyInfo(chainId)
......@@ -30,26 +31,21 @@ export function SpendingDetails({
: null
const usdValue = useUSDValue(chainId, value)
const tokenAmountWithSymbol =
formatCurrencyAmount({ value: nativeCurrencyAmount, type: NumberType.TokenTx }) +
' ' +
getSymbolDisplayText(nativeCurrencyInfo?.currency.symbol)
const fiatAmount = convertFiatAmountFormatted(usdValue, NumberType.FiatTokenPrice)
return (
<Flex row alignItems="center" gap="$spacing16">
<Trans i18nKey="walletConnect.request.details.sending">
<Text color="$neutral2" variant="body2">
Sending:
{t('Sending')}:
</Text>
<Flex row alignItems="center" gap="$spacing4">
<CurrencyLogo currencyInfo={nativeCurrencyInfo} size={iconSizes.icon16} />
<Text variant="subheading2">{{ tokenAmountWithSymbol }}</Text>
<Text variant="subheading2">
{formatCurrencyAmount({ value: nativeCurrencyAmount, type: NumberType.TokenTx })}{' '}
{getSymbolDisplayText(nativeCurrencyInfo?.currency.symbol)}
</Text>
<Text color="$neutral2" loading={!usdValue} variant="subheading2">
({{ fiatAmount }})
({convertFiatAmountFormatted(usdValue, NumberType.FiatTokenPrice)})
</Text>
</Flex>
</Trans>
</Flex>
)
}
......@@ -314,7 +314,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
) : (
<Flex row alignItems="center" justifyContent="space-between">
<Text color="$neutral1" variant="subheading2">
{t('walletConnect.request.label.network')}
{t('Network')}
</Text>
<NetworkPill
showIcon
......@@ -333,8 +333,8 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
<AccountDetails address={request.account} />
{!hasSufficientFunds && (
<Text color="$DEP_accentWarning" pt="$spacing8" variant="body2">
{t('walletConnect.request.error.insufficientFunds', {
currencySymbol: nativeCurrency?.symbol,
{t('You don’t have enough {{symbol}} to complete this transaction.', {
symbol: nativeCurrency?.symbol,
})}
</Text>
)}
......@@ -351,7 +351,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
/>
}
textColor="$DEP_accentWarning"
title={t('walletConnect.request.error.network')}
title={t('Internet or network connection error')}
/>
) : (
<WarningSection
......@@ -367,7 +367,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
testID={ElementName.Cancel}
theme="tertiary"
onPress={onReject}>
{t('common.button.cancel')}
{t('Cancel')}
</Button>
<Button
fill
......@@ -381,9 +381,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem
await onConfirm()
}
}}>
{isTransactionRequest(request)
? t('common.button.accept')
: t('walletConnect.request.button.sign')}
{isTransactionRequest(request) ? t('Accept') : t('Sign')}
</Button>
</Flex>
</Flex>
......@@ -421,9 +419,9 @@ function WarningSection({
width={iconSizes.icon16}
/>
<Text color="$neutral2" fontStyle="italic" variant="body3">
{isTransactionRequest(request)
? t('walletConnect.request.warning.general.transaction')
: t('walletConnect.request.warning.general.message')}
{t('Be careful: this {{ requestType }} may transfer assets', {
requestType: isTransactionRequest(request) ? 'transaction' : 'message',
})}
</Text>
</Flex>
)
......
......@@ -68,7 +68,7 @@ const SitePermissions = (): JSX.Element => {
allowFontScaling={false}
color="$neutral2"
variant="subheading2">
{t('walletConnect.permissions.title')}
{t('App permissions')}
</Text>
<Flex centered row gap="$spacing8">
<Icons.Check color="$statusSuccess" size={iconSizes.icon16} />
......@@ -78,7 +78,7 @@ const SitePermissions = (): JSX.Element => {
color="$neutral1"
flexGrow={1}
variant={normalInfoTextSize}>
{t('walletConnect.permissions.option.viewWalletAddress')}
{t('View your wallet address')}
</Text>
</Flex>
<Flex centered row gap="$spacing8">
......@@ -89,7 +89,7 @@ const SitePermissions = (): JSX.Element => {
color="$neutral1"
flexGrow={1}
variant={normalInfoTextSize}>
{t('walletConnect.permissions.option.viewTokenBalances')}
{t('View your token balances')}
</Text>
</Flex>
<Flex centered row gap="$spacing8">
......@@ -100,7 +100,7 @@ const SitePermissions = (): JSX.Element => {
color="$neutral1"
flexGrow={1}
variant={normalInfoTextSize}>
{t('walletConnect.permissions.option.transferAssets')}
{t('Transfer your assets without consent')}
</Text>
</Flex>
</Flex>
......@@ -124,7 +124,7 @@ const NetworksRow = ({ chains }: { chains: ChainId[] }): JSX.Element => {
allowFontScaling={false}
color="$neutral2"
variant="subheading2">
{t('walletConnect.permissions.networks')}
{t('Networks')}
</Text>
<NetworkLogos chains={chains} />
</Flex>
......@@ -258,7 +258,7 @@ export const PendingConnectionModal = ({ pendingSession, onClose }: Props): JSX.
px="$spacing24"
textAlign="center"
variant="heading3">
{t('walletConnect.pending.title', {
{t('{{ dappName }} wants to connect to your wallet', {
dappName: truncateDappName(dappName),
})}{' '}
</Text>
......@@ -287,13 +287,13 @@ export const PendingConnectionModal = ({ pendingSession, onClose }: Props): JSX.
testID="cancel-pending-connection"
theme="secondary"
onPress={(): Promise<void> => onPressSettleConnection(false)}>
{t('common.button.cancel')}
{t('Cancel')}
</Button>
<Button
fill
testID="connect-pending-connection"
onPress={(): Promise<void> => onPressSettleConnection(true)}>
{t('walletConnect.pending.button.connect')}
{t('Connect')}
</Button>
</Flex>
</AnimatedFlex>
......
......@@ -37,7 +37,7 @@ export const PendingConnectionSwitchAccountModal = ({
<ActionSheetModal
header={
<Flex centered gap="$spacing4" py="$spacing16">
<Text variant="buttonLabel2">{t('walletConnect.pending.switchAccount')}</Text>
<Text variant="buttonLabel2">{t('Switch Account')}</Text>
</Flex>
}
isVisible={true}
......
......@@ -63,7 +63,7 @@ export const PendingConnectionSwitchNetworkModal = ({
<ActionSheetModal
header={
<Flex centered gap="$spacing4" py="$spacing16">
<Text variant="buttonLabel2">{t('walletConnect.pending.switchNetwork')}</Text>
<Text variant="buttonLabel2">{t('Switch Network')}</Text>
</Flex>
}
isVisible={true}
......
......@@ -76,12 +76,14 @@ export function WalletConnectModal({
if (!supportedURI) {
setShouldFreezeCamera(true)
Alert.alert(
t('walletConnect.error.unsupported.title'),
t('Invalid QR Code'),
// TODO(EXT-495): Add Scantastic product name here when ready
t('walletConnect.error.unsupported.message'),
t(
'Make sure that you’re scanning a valid WalletConnect or Ethereum address QR code before trying again.'
),
[
{
text: t('common.button.tryAgain'),
text: t('Try again'),
onPress: (): void => {
setShouldFreezeCamera(false)
},
......@@ -101,11 +103,13 @@ export function WalletConnectModal({
if (supportedURI.type === URIType.WalletConnectURL) {
setShouldFreezeCamera(true)
Alert.alert(
t('walletConnect.error.unsupportedV1.title'),
t('walletConnect.error.unsupportedV1.message'),
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.'
),
[
{
text: t('common.button.ok'),
text: t('OK'),
onPress: (): void => {
setShouldFreezeCamera(false)
},
......@@ -122,11 +126,11 @@ export function WalletConnectModal({
} catch (error) {
logger.error(error, { tags: { file: 'WalletConnectModal', function: 'onScanCode' } })
Alert.alert(
t('walletConnect.error.general.title'),
t('walletConnect.error.general.message'),
t('WalletConnect Error'),
t('There was an issue with WalletConnect. Please try again'),
[
{
text: t('common.button.ok'),
text: t('OK'),
onPress: (): void => {
setShouldFreezeCamera(false)
},
......@@ -164,18 +168,14 @@ export function WalletConnectModal({
const isAllowed = isAllowedUwULinkRequest(parsedUwulinkRequest)
if (!isAllowed) {
Alert.alert(
t('walletConnect.error.uwu.title'),
t('walletConnect.error.uwu.unsupported'),
[
Alert.alert(t('UwU Link error'), t('This QR code is not supported.'), [
{
text: t('common.button.ok'),
text: t('OK'),
onPress: (): void => {
setShouldFreezeCamera(false)
},
},
]
)
])
return
}
......@@ -201,7 +201,7 @@ export function WalletConnectModal({
onClose()
} catch (_) {
setShouldFreezeCamera(false)
Alert.alert(t('walletConnect.error.uwu.title'), t('walletConnect.error.uwu.scan'))
Alert.alert(t('UwU Link error'), t('There was an issue scanning this QR code.'))
}
}
......@@ -312,8 +312,8 @@ export function WalletConnectModal({
)}
<Text color="$neutral1" variant="buttonLabel2">
{currentScreenState === ScannerModalState.ScanQr
? t('qrScanner.recipient.action.show')
: t('qrScanner.recipient.action.scan')}
? t('Show my QR code')
: t('Scan a QR code')}
</Text>
</Flex>
</TouchableArea>
......
......@@ -104,8 +104,10 @@ function RequestModal({ currRequest }: RequestModalProps): JSX.Element {
if (!isRequestFromSignerAccount) {
return (
<WarningModal
caption={t('walletConnect.request.warning.message')}
closeText={t('common.button.dismiss')}
caption={t(
'In order to sign messages or transactions, you’ll need to import the wallet’s recovery phrase.'
)}
closeText={t('Dismiss')}
icon={
<EyeIcon
color={colors.neutral2.get()}
......@@ -116,7 +118,7 @@ function RequestModal({ currRequest }: RequestModalProps): JSX.Element {
}
modalName={ModalName.WCViewOnlyWarning}
severity={WarningSeverity.None}
title={t('walletConnect.request.warning.title')}
title={t('This wallet is in view only mode')}
onCancel={onClose}
onClose={onClose}>
<Flex
......
......@@ -57,7 +57,7 @@ function PortfolioValue({
return (
<Text color="$neutral2" loading={isLoading} variant="subheading2">
{portfolioValue === undefined
? t('common.text.notAvailable')
? t('N/A')
: convertFiatAmountFormatted(portfolioValue, NumberType.PortfolioBalance)}
</Text>
)
......@@ -100,9 +100,9 @@ export function AccountCardItem({
const menuActions = useMemo(() => {
return [
{ 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 },
{ title: t('Copy wallet address'), systemIcon: 'doc.on.doc' },
{ title: t('Wallet settings'), systemIcon: 'gearshape' },
{ title: t('Remove wallet'), systemIcon: 'trash', destructive: true },
]
}, [t])
......
import React from 'react'
import { AccountHeader } from 'src/components/accounts/AccountHeader'
import { render } from 'src/test/test-utils'
import { ACCOUNT } from 'wallet/src/test/fixtures'
import { mockWalletPreloadedState } from 'wallet/src/test/mocks'
import { mockWalletPreloadedState } from 'wallet/src/test/fixtures'
describe(AccountHeader, () => {
it('renders without error', () => {
const tree = render(<AccountHeader />, { preloadedState: mockWalletPreloadedState(ACCOUNT) })
const tree = render(<AccountHeader />, { preloadedState: mockWalletPreloadedState })
expect(tree.toJSON()).toMatchSnapshot()
})
......
......@@ -4,23 +4,25 @@ 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 { ACCOUNT, ON_PRESS_EVENT_PAYLOAD, amounts } from 'wallet/src/test/fixtures'
import { mockLocalizedFormatter } from 'wallet/src/test/mocks'
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'
const resolvers: Resolvers = {
Portfolio: {
tokensTotalDenominatedValue: () => amounts.md(),
tokensTotalDenominatedValue: () => Amounts.md,
},
}
describe(AccountList, () => {
it('renders without error', async () => {
const tree = render(<AccountList accounts={[ACCOUNT]} onPress={jest.fn()} />, { resolvers })
const tree = render(<AccountList accounts={[account]} onPress={jest.fn()} />, { resolvers })
expect(
await screen.findByText(
mockLocalizedFormatter.formatNumberOrString({
value: amounts.md().value,
value: Portfolios[0].tokensTotalDenominatedValue?.value,
type: NumberType.PortfolioBalance,
currencyCode: 'usd',
})
......@@ -31,21 +33,21 @@ describe(AccountList, () => {
it('handles press on card items', async () => {
const onPressSpy = jest.fn()
render(<AccountList accounts={[ACCOUNT]} onPress={onPressSpy} />, {
render(<AccountList accounts={[account]} onPress={onPressSpy} />, {
resolvers,
})
// go to success state
expect(
await screen.findByText(
mockLocalizedFormatter.formatNumberOrString({
value: amounts.md().value,
value: Portfolios[0].tokensTotalDenominatedValue?.value,
type: NumberType.PortfolioBalance,
currencyCode: 'usd',
})
)
).toBeDefined()
fireEvent.press(screen.getByTestId(`account_item/${ACCOUNT.address}`), ON_PRESS_EVENT_PAYLOAD)
fireEvent.press(screen.getByTestId(`account_item/${account.address}`), ON_PRESS_EVENT_PAYLOAD)
expect(onPressSpy).toHaveBeenCalledTimes(1)
})
......
......@@ -31,7 +31,7 @@ const ViewOnlyHeader = (): JSX.Element => {
return (
<Flex fill px="$spacing24" py="$spacing8">
<Text color="$neutral2" variant="subheading2">
{t('account.wallet.header.viewOnly')}
{t('View only wallets')}
</Text>
</Flex>
)
......@@ -42,7 +42,7 @@ const SignerHeader = (): JSX.Element => {
return (
<Flex fill px="$spacing24" py="$spacing8">
<Text color="$neutral2" variant="subheading2">
{t('account.wallet.header.other')}
{t('Your other wallets')}
</Text>
</Flex>
)
......
......@@ -40,7 +40,7 @@ export function OfflineBanner(): JSX.Element | null {
width={iconSizes.icon24}
/>
}
text={t('home.banner.offline')}
text={t('You are in offline mode')}
translateY={BANNER_HEIGHT - EXTRA_MARGIN}
/>
) : null
......
import React from 'react'
import { BackButton } from 'src/components/buttons/BackButton'
import { fireEvent, render, screen } from 'src/test/test-utils'
import { ON_PRESS_EVENT_PAYLOAD } from 'wallet/src/test/fixtures'
import { ON_PRESS_EVENT_PAYLOAD } from 'wallet/src/test/eventFixtures'
const mockedGoBack = jest.fn()
jest.mock('@react-navigation/native', () => {
......
......@@ -42,7 +42,7 @@ export function CopyTextButton({ copyText }: Props): JSX.Element {
return (
<Button icon={isCopied ? copiedIcon : copyIcon} theme="tertiary" onPress={onPress}>
{isCopied ? t('common.button.copied') : t('common.button.copy')}
{isCopied ? t`Copied` : t`Copy`}
</Button>
)
}
import React, { ComponentProps, ReactNode, useCallback, useContext, useMemo } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { Trans } 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 { getCloudProviderName } from 'wallet/src/utils/platform'
import { isAndroid } from 'wallet/src/utils/platform'
function Page({
text,
......@@ -16,7 +16,6 @@ function Page({
text: ReactNode
params: OnboardingStackBaseParams
}): JSX.Element {
const { t } = useTranslation()
const { fullWidth } = useDeviceDimensions()
const { goToPrev, goToNext } = useContext(CarouselContext)
const navigation = useOnboardingStackNavigation()
......@@ -56,7 +55,7 @@ function Page({
px="$spacing24"
width={fullWidth}>
<Text color="$neutral2" variant="subheading2">
{t('onboarding.tooltip.recoveryPhrase.trigger')}
<Trans>What’s a recovery phrase?</Trans>
</Text>
<GestureDetector gesture={dismissGesture}>
<CloseButton color="$neutral2" onPress={(): void => undefined} />
......@@ -72,14 +71,13 @@ function Page({
)
}
const cloudProviderName = getCloudProviderName()
export const SeedPhraseEducationContent = (params: OnboardingStackBaseParams): JSX.Element[] => [
<Page
params={params}
text={
<CustomHeadingText>
<Trans i18nKey="account.seedPhrase.education.part1">
A recovery phrase (or seed phrase) is a
<Trans>
A recovery phrase (or seed phrase) is a{' '}
<CustomHeadingText color="$accent1">set of words</CustomHeadingText> required to access
your wallet, <CustomHeadingText color="$accent1">like a password.</CustomHeadingText>
</Trans>
......@@ -90,9 +88,9 @@ export const SeedPhraseEducationContent = (params: OnboardingStackBaseParams): J
params={params}
text={
<CustomHeadingText>
<Trans i18nKey="account.seedPhrase.education.part2">
<Trans>
You can <CustomHeadingText color="$accent1">enter</CustomHeadingText> your recovery phrase
on a new device
on a new device{' '}
<CustomHeadingText color="$accent1">to restore your wallet</CustomHeadingText> and its
contents.
</Trans>
......@@ -103,9 +101,9 @@ export const SeedPhraseEducationContent = (params: OnboardingStackBaseParams): J
params={params}
text={
<CustomHeadingText>
<Trans i18nKey="account.seedPhrase.education.part3">
But, if you
<CustomHeadingText color="$accent1">lose your recovery phrase</CustomHeadingText>, you’ll
<Trans>
But, if you{' '}
<CustomHeadingText color="$accent1">lose your recovery phrase</CustomHeadingText>, you’ll{' '}
<CustomHeadingText color="$accent1">lose access</CustomHeadingText> to your wallet.
</Trans>
</CustomHeadingText>
......@@ -115,13 +113,19 @@ export const SeedPhraseEducationContent = (params: OnboardingStackBaseParams): J
params={params}
text={
<CustomHeadingText>
<Trans i18nKey="account.seedPhrase.education.part4">
Instead of memorizing your recovery phrase, you can
<CustomHeadingText color="$accent1">
back it up to {{ cloudProviderName }}
</CustomHeadingText>
and protect it with a password.
{isAndroid ? (
<Trans>
Instead of memorizing your recovery phrase, you can{' '}
<CustomHeadingText color="$accent1">back it up to Google Drive</CustomHeadingText> and
protect it with a password.
</Trans>
) : (
<Trans>
Instead of memorizing your recovery phrase, you can{' '}
<CustomHeadingText color="$accent1">back it up to iCloud</CustomHeadingText> and protect
it with a password.
</Trans>
)}
</CustomHeadingText>
}
/>,
......@@ -129,8 +133,8 @@ export const SeedPhraseEducationContent = (params: OnboardingStackBaseParams): J
params={params}
text={
<CustomHeadingText>
<Trans i18nKey="account.seedPhrase.education.part5">
You can also manually back up your recovery phrase by
<Trans>
You can also manually back up your recovery phrase by{' '}
<CustomHeadingText color="$accent1">writing it down</CustomHeadingText> and storing it in
a safe place.
</Trans>
......@@ -141,8 +145,8 @@ export const SeedPhraseEducationContent = (params: OnboardingStackBaseParams): J
params={params}
text={
<CustomHeadingText>
<Trans i18nKey="account.seedPhrase.education.part6">
We recommend using
<Trans>
We recommend using{' '}
<CustomHeadingText color="$accent1">both types of backups</CustomHeadingText>, because if
you lose your recovery phrase, you won’t be able to restore your wallet.
</Trans>
......
......@@ -142,8 +142,8 @@ export function ExploreSections({ listRef }: ExploreSectionsProps): JSX.Element
return (
<Flex height="100%" pb="$spacing60">
<BaseCard.ErrorState
retryButtonLabel={t('common.button.retry')}
title={t('explore.tokens.error')}
retryButtonLabel={t('Retry')}
title={t('Couldn’t load tokens')}
onRetry={onRetry}
/>
</Flex>
......@@ -188,7 +188,7 @@ export function ExploreSections({ listRef }: ExploreSectionsProps): JSX.Element
mt="$spacing16"
pl="$spacing4">
<Text color="$neutral2" flexShrink={0} paddingEnd="$spacing8" variant="subheading2">
{t('explore.tokens.top.title')}
{t('Top tokens')}
</Text>
<Flex flexShrink={1}>
<SortButton orderBy={orderBy} />
......
......@@ -39,7 +39,7 @@ export function FavoriteHeaderRow({
) : (
<TouchableArea hitSlop={16} onPress={onPress}>
<Text color="$accent1" variant="buttonLabel3">
{t('common.button.done')}
{t('Done')}
</Text>
</TouchableArea>
)}
......
......@@ -72,9 +72,9 @@ export function FavoriteTokensGrid({
return (
<AnimatedFlex entering={FadeIn} style={animatedStyle}>
<FavoriteHeaderRow
editingTitle={t('explore.tokens.favorite.title.edit')}
editingTitle={t('Edit favorite tokens')}
isEditing={isEditing}
title={t('explore.tokens.favorite.title.default')}
title={t('Favorite tokens')}
onPress={(): void => setIsEditing(!isEditing)}
/>
{showLoading ? (
......
......@@ -52,8 +52,8 @@ function FavoriteWalletCard({
/// Options for long press context menu
const menuActions = useMemo(() => {
return [
{ title: t('explore.wallets.favorite.action.remove'), systemIcon: 'heart.fill' },
{ title: t('explore.wallets.favorite.action.edit'), systemIcon: 'square.and.pencil' },
{ title: t('Remove favorite'), systemIcon: 'heart.fill' },
{ title: t('Edit favorites'), systemIcon: 'square.and.pencil' },
]
}, [t])
......
......@@ -71,9 +71,9 @@ export function FavoriteWalletsGrid({
return (
<AnimatedFlex entering={FadeIn} style={animatedStyle}>
<FavoriteHeaderRow
editingTitle={t('explore.wallets.favorite.title.edit')}
editingTitle={t('Edit favorite wallets')}
isEditing={isEditing}
title={t('explore.wallets.favorite.title.default')}
title={t('Favorite wallets')}
onPress={(): void => setIsEditing(!isEditing)}
/>
{showLoading ? (
......
......@@ -74,12 +74,12 @@ export const TokenItem = memo(function _TokenItem({
const getMetadataSubtitle = (): string | undefined => {
switch (metadataDisplayType) {
case TokenMetadataDisplayType.MarketCap:
return t('explore.tokens.metadata.marketCap', { number: marketCapFormatted })
return t('{{num}} MCap', { num: marketCapFormatted })
case TokenMetadataDisplayType.Volume:
return t('explore.tokens.metadata.volume', { number: volume24hFormatted })
return t('{{num}} Vol', { num: volume24hFormatted })
case TokenMetadataDisplayType.TVL:
return t('explore.tokens.metadata.totalValueLocked', {
number: totalValueLockedFormatted,
return t('{{num}} TVL', {
num: totalValueLockedFormatted,
})
case TokenMetadataDisplayType.Symbol:
return symbol
......
......@@ -8,9 +8,9 @@ import { Resolvers } from 'wallet/src/data/__generated__/types-and-hooks'
import { FavoritesState } from 'wallet/src/features/favorites/slice'
import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types'
import { SectionName } from 'wallet/src/telemetry/constants'
import { SAMPLE_SEED_ADDRESS_1 } from 'wallet/src/test/fixtures/constants'
import { DaiAsset } from 'wallet/src/test/gqlFixtures'
const tokenId = SAMPLE_SEED_ADDRESS_1
const tokenId = DaiAsset.address?.toLowerCase() ?? ''
const currencyId = `1-${tokenId}`
const resolvers: Resolvers = {
......
......@@ -107,31 +107,29 @@ export function useExploreTokenContextMenu({
const menuActions = useMemo(
() => [
{
title: isFavorited
? t('explore.tokens.favorite.action.remove')
: t('explore.tokens.favorite.action.add'),
title: isFavorited ? t('Remove favorite') : t('Favorite token'),
systemIcon: isFavorited ? 'heart.fill' : 'heart',
onPress: onPressToggleFavorite,
},
...(onEditFavorites
? [
{
title: t('explore.tokens.favorite.action.edit'),
title: t('Edit favorites'),
systemIcon: 'square.and.pencil',
onPress: onEditFavorites,
},
]
: []),
{ title: t('common.button.swap'), systemIcon: 'arrow.2.squarepath', onPress: onPressSwap },
{ title: t('Swap'), systemIcon: 'arrow.2.squarepath', onPress: onPressSwap },
{
title: t('common.button.receive'),
title: t('Receive'),
systemIcon: 'qrcode',
onPress: onPressReceive,
},
...(!onEditFavorites
? [
{
title: t('common.button.share'),
title: t('Share'),
systemIcon: 'square.and.arrow.up',
onPress: onPressShare,
},
......
......@@ -73,13 +73,10 @@ export function SearchEmptySection(): JSX.Element {
gap="$spacing16"
justifyContent="space-between"
mb="$spacing4">
<SectionHeaderText
icon={<RecentIcon />}
title={t('explore.search.section.recent')}
/>
<SectionHeaderText icon={<RecentIcon />} title={t('Recent searches')} />
<TouchableArea onPress={onPressClearSearchHistory}>
<Text color="$accent1" variant="buttonLabel3">
{t('explore.search.action.clear')}
{t('Clear all')}
</Text>
</TouchableArea>
</Flex>
......@@ -92,19 +89,16 @@ export function SearchEmptySection(): JSX.Element {
</AnimatedFlex>
)}
<Flex gap="$spacing4">
<SectionHeaderText icon={<TrendIcon />} title={t('explore.search.section.popularTokens')} />
<SectionHeaderText icon={<TrendIcon />} title={t('Popular tokens')} />
<SearchPopularTokens />
</Flex>
<Flex gap="$spacing4">
<SectionHeaderText icon={<TrendIcon />} title={t('explore.search.section.popularNFT')} />
<SectionHeaderText icon={<TrendIcon />} title={t('Popular NFT collections')} />
<SearchPopularNFTCollections />
</Flex>
<FlatList
ListHeaderComponent={
<SectionHeaderText
icon={<TrendIcon />}
title={t('explore.search.section.suggestedWallets')}
/>
<SectionHeaderText icon={<TrendIcon />} title={t('Suggested wallets')} />
}
data={SUGGESTED_WALLETS}
keyExtractor={walletKey}
......
......@@ -2,12 +2,12 @@ import React from 'react'
import { SearchPopularTokens } from 'src/components/explore/search/SearchPopularTokens'
import { render, screen } from 'src/test/test-utils'
import { Resolvers } from 'wallet/src/data/__generated__/types-and-hooks'
import { ethToken, usdcToken, wethToken } from 'wallet/src/test/fixtures'
import { EthToken, TopTokens } from 'wallet/src/test/gqlFixtures'
const resolvers: Resolvers = {
Query: {
topTokens: () => [wethToken(), usdcToken()],
tokens: () => [ethToken({ address: null })],
topTokens: () => TopTokens,
tokens: () => [{ ...EthToken, address: null }],
},
}
......
......@@ -9,19 +9,19 @@ export const SearchResultsLoader = (): JSX.Element => {
return (
<Flex gap="$spacing16">
<Flex gap="$spacing12">
<SectionHeaderText title={t('explore.search.section.tokens')} />
<SectionHeaderText title={t('Tokens')} />
<AnimatedFlex entering={FadeIn} exiting={FadeOut} mx="$spacing8">
<Loader.Token repeat={2} />
</AnimatedFlex>
</Flex>
<Flex gap="$spacing12">
<SectionHeaderText title={t('explore.search.section.nft')} />
<SectionHeaderText title={t('NFT Collections')} />
<AnimatedFlex entering={FadeIn} exiting={FadeOut} mx="$spacing8">
<Loader.Token repeat={2} />
</AnimatedFlex>
</Flex>
<Flex gap="$spacing12">
<SectionHeaderText title={t('explore.search.section.wallets')} />
<SectionHeaderText title={t('Wallets')} />
<AnimatedFlex entering={FadeIn} exiting={FadeOut} mx="$spacing8">
<Loader.Token />
</AnimatedFlex>
......
......@@ -34,19 +34,19 @@ import { SearchResultOrHeader } from './types'
const WalletHeaderItem: SearchResultOrHeader = {
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('explore.search.section.wallets'),
title: i18n.t('Wallets'),
}
const TokenHeaderItem: SearchResultOrHeader = {
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('explore.search.section.tokens'),
title: i18n.t('Tokens'),
}
const NFTHeaderItem: SearchResultOrHeader = {
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('explore.search.section.nft'),
title: i18n.t('NFT Collections'),
}
const EtherscanHeaderItem: SearchResultOrHeader = {
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('explore.search.action.viewEtherscan', {
title: i18n.t('View on {{ blockExplorerName }}', {
blockExplorerName: CHAIN_INFO[ChainId.Mainnet].explorer.name,
}),
}
......@@ -170,8 +170,8 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }):
return (
<AnimatedFlex entering={FadeIn} exiting={FadeOut} pt="$spacing24">
<BaseCard.ErrorState
retryButtonLabel="common.button.retry"
title={t('explore.search.error')}
retryButtonLabel="Retry"
title={t('Couldn’t load search results')}
onRetry={onRetry}
/>
</AnimatedFlex>
......@@ -184,8 +184,8 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }):
ListEmptyComponent={
<AnimatedFlex entering={FadeIn} exiting={FadeOut} gap="$spacing8" mx="$spacing8">
<Text color="$neutral2" variant="body1">
<Trans i18nKey="explore.search.empty.full">
No results found for <Text color="$neutral1">"{{ searchQuery }}"</Text>
<Trans t={t}>
No results found for <Text color="$neutral1">"{searchQuery}"</Text>
</Trans>
</Text>
</AnimatedFlex>
......
......@@ -63,8 +63,8 @@ export function SearchENSAddressItem({
{showSecondLine ? (
<Text color="$neutral2" ellipsizeMode="tail" numberOfLines={1} variant="subheading2">
{showOwnedBy &&
t('explore.search.label.ownedBy', {
ownerAddress: primaryENSName || formattedAddress,
t('Owned by {{owner}}', {
owner: primaryENSName || formattedAddress,
})}
{showAddress && formattedAddress}
</Text>
......
......@@ -77,8 +77,8 @@ export function SearchWalletItemBase({
const menuActions = useMemo(() => {
return isFavorited
? [{ title: t('explore.wallets.favorite.action.remove'), systemIcon: 'heart.fill' }]
: [{ title: t('explore.wallets.favorite.action.add'), systemIcon: 'heart' }]
? [{ title: t('Remove favorite'), systemIcon: 'heart.fill' }]
: [{ title: t('Favorite wallet'), systemIcon: 'heart' }]
}, [isFavorited, t])
return (
......
......@@ -7,16 +7,7 @@ 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 {
amount,
ethToken,
nftCollection,
nftContract,
token,
tokenMarket,
tokenProject,
} from 'wallet/src/test/fixtures'
import { createArray } from 'wallet/src/test/utils'
import { SearchTokens, TopNFTCollections } from 'wallet/src/test/gqlFixtures'
type ExploreSearchResult = NonNullable<ExploreSearchQuery>
......@@ -26,31 +17,29 @@ describe(formatTokenSearchResults, () => {
})
it('filters out duplicate results', () => {
const searchToken = token()
const data = createArray(2, () => searchToken)
const data = [SearchTokens[0], SearchTokens[0]] as ExploreSearchResult['searchTokens']
const result = formatTokenSearchResults(data, '')
expect(result).toHaveLength(1)
expect(result?.[0]?.address).toEqual(data[0].address)
expect(result?.[0]?.address).toEqual(SearchTokens?.[0]?.address)
})
it('uses tokens with highest volume for tokens with the same project id', () => {
it('uses tokens with highest volume for duplicate results', () => {
const changedAddress = faker.finance.ethereumAddress()
const data = [
// Tokens with the same address and chain will have the same project id
ethToken({
market: tokenMarket({ volume: amount({ value: 10 }) }),
}),
ethToken({
SearchTokens[0],
{
...SearchTokens[0],
address: changedAddress,
market: tokenMarket({ volume: amount({ value: 100 }) }),
}),
ethToken({
market: tokenMarket({ volume: amount({ value: 20 }) }),
}),
]
market: {
volume: {
value: 100,
},
},
},
] as ExploreSearchResult['searchTokens']
const result = formatTokenSearchResults(data, '')
......@@ -60,10 +49,22 @@ describe(formatTokenSearchResults, () => {
expect(result?.[0]?.address).toEqual(changedAddress)
})
it('sorts results by best search query match', () => {
it('sorts results by search query match', () => {
const data: ExploreSearchResult['searchTokens'] = [
ethToken({ project: tokenProject({ name: 'UniswapStartingName' }) }),
ethToken({ project: tokenProject({ name: 'Uniswap' }) }),
{
project: {
name: 'UniswapStartingName',
id: '2',
},
chain: Chain.Ethereum,
},
{
project: {
name: 'Uniswap',
id: '1',
},
chain: Chain.Ethereum,
},
]
const result = formatTokenSearchResults(data, 'uniswap')
......@@ -74,65 +75,59 @@ describe(formatTokenSearchResults, () => {
})
it('properly formats token search result', () => {
const searchToken = token()
const data = [searchToken]
const data = [SearchTokens[0]] as ExploreSearchResult['searchTokens']
const result = formatTokenSearchResults(data, '')
expect(result).toHaveLength(1)
expect(result?.[0]?.type).toEqual(SearchResultType.Token)
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)
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 collection = nftCollection({
nftContracts: [nftContract({ chain: Chain.Ethereum })],
})
describe(gqlNFTToNFTCollectionSearchResult, () => {
const node = TopNFTCollections[0]
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)
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(collection)
const result = gqlNFTToNFTCollectionSearchResult(node)
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)
})
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)
})
})
describe(formatNFTCollectionSearchResults, () => {
describe(formatNFTCollectionSearchResults, () => {
it('returns undefined if there is no data', () => {
expect(formatNFTCollectionSearchResults(null)).toEqual(undefined)
})
it('filters out nfts that cannot be formatted', () => {
const topNFTCollections = createArray(2, nftCollection)
const nftSearchResult = {
edges: [
...topNFTCollections.map((nft) => ({ node: nft })),
{ node: nftCollection({ name: null }) },
...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)
})
expect(result?.[0]?.address).toEqual(TopNFTCollections?.[0]?.nftContracts?.[0]?.address)
expect(result?.[1]?.address).toEqual(TopNFTCollections?.[1]?.nftContracts?.[0]?.address)
})
})
import { SEARCH_RESULT_HEADER_KEY } from 'src/components/explore/search/constants'
import { SearchResultOrHeader } from 'src/components/explore/search/types'
import { Chain, ExploreSearchQuery } from 'wallet/src/data/__generated__/types-and-hooks'
import { fromGraphQLChain } from 'wallet/src/features/chains/utils'
import {
......@@ -8,6 +6,8 @@ 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(contract?.chain ?? Chain.Ethereum)
const chainId = fromGraphQLChain(node?.nftContracts?.[0]?.chain ?? Chain.Ethereum)
if (node.name && contract?.address && chainId) {
return {
type: SearchResultType.NFTCollection,
......
......@@ -25,7 +25,7 @@ export function FiatOnRampCtaButton({
}: FiatOnRampCtaButtonProps): JSX.Element {
const { t } = useTranslation()
const buttonAvailable = eligible || isLoading
const continueText = eligible ? continueButtonText : t('fiatOnRamp.error.unsupported')
const continueText = eligible ? continueButtonText : t('Not supported in region')
return (
<Trace
logPress
......
......@@ -90,15 +90,13 @@ export function FORQuoteItem({
<Flex alignItems="flex-end" gap="$spacing4">
{quoteAmount && (
<Text color="$neutral1" variant="body3">
{t('fiatOnRamp.quote.amount', {
tokenAmount: `${quoteAmount + getSymbolDisplayText(currency?.symbol)}`,
{t('Receive {{amount}}', {
amount: `${quoteAmount + getSymbolDisplayText(currency?.symbol)}`,
})}
</Text>
)}
<Text color="$neutral2" variant="body3">
{t('fiatOnRamp.quote.amountAfterFees', {
tokenAmount: quoteEquivalentInSourceCurrencyAmount,
})}
{t('{{amount}} after fees', { amount: quoteEquivalentInSourceCurrencyAmount })}
</Text>
</Flex>
{showCarret ? (
......
......@@ -65,20 +65,22 @@ export function ForceUpgradeModal(): JSX.Element {
<>
{isVisible && (
<WarningModal
confirmText={t('forceUpgrade.action.confirm')}
confirmText={t('Update app')}
hideHandlebar={upgradeStatus === UpgradeStatus.Required}
isDismissible={upgradeStatus !== UpgradeStatus.Required}
modalName={ModalName.ForceUpgradeModal}
severity={WarningSeverity.High}
title={t('forceUpgrade.title')}
title={t('Update the app to continue')}
onClose={onClose}
onConfirm={onPressConfirm}>
<Text color="$neutral2" textAlign="center" variant="body2">
{t('forceUpgrade.description')}
{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.'
)}
</Text>
{mnemonicId && (
<Text color="$accent1" variant="buttonLabel3" onPress={onPressViewRecovery}>
{t('forceUpgrade.action.seedPhrase')}
{t('View recovery phrase')}
</Text>
)}
</WarningModal>
......@@ -94,7 +96,7 @@ export function ForceUpgradeModal(): JSX.Element {
<TouchableArea onPress={onDismiss}>
<BackButtonView size={BACK_BUTTON_SIZE} />
</TouchableArea>
<Text variant="subheading1">{t('forceUpgrade.label.seedPhrase')}</Text>
<Text variant="subheading1">{t('Recovery phrase')}</Text>
<Flex width={BACK_BUTTON_SIZE} />
</Flex>
<SeedPhraseDisplay mnemonicId={mnemonicId} onDismiss={onDismiss} />
......
......@@ -76,8 +76,8 @@ export const FeedTab = memo(
const errorCard = (
<Flex grow style={containerProps?.emptyContainerStyle}>
<BaseCard.ErrorState
retryButtonLabel={t('common.button.retry')}
title={t('home.feed.error')}
retryButtonLabel={t('Retry')}
title={t('Couldn’t load activity')}
onRetry={onRetry}
/>
</Flex>
......@@ -86,9 +86,9 @@ export const FeedTab = memo(
const emptyListView = (
<Flex grow style={containerProps?.emptyContainerStyle}>
<BaseCard.EmptyState
description={t('home.feed.empty.description')}
description={t('When your favorited wallets makes transactions, they’ll appear here.')}
icon={<NoTransactions />}
title={t('home.feed.empty.title')}
title={t('No activity yet')}
onPress={onPressReceive}
/>
</Flex>
......
import { FlashList } from '@shopify/flash-list'
import React, { forwardRef, memo, useCallback, useMemo } from 'react'
import { RefreshControl } from 'react-native'
import { useAppDispatch } from 'src/app/hooks'
import { useAppStackNavigation } from 'src/app/navigation/types'
import { useAdaptiveFooter } from 'src/components/home/hooks'
import { TAB_BAR_HEIGHT, TabProps } from 'src/components/layout/TabHelpers'
import { NftView } from 'src/components/NFT/NftView'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { openModal } from 'src/features/modals/modalSlice'
import { removePendingSession } from 'src/features/walletConnect/walletConnectSlice'
import { Screens } from 'src/screens/Screens'
import { Flex, useDeviceInsets, useSporeColors } from 'ui/src'
import { NftsList } from 'wallet/src/components/nfts/NftsList'
import { GQLQueries } from 'wallet/src/data/queries'
import { NFTItem } from 'wallet/src/features/nfts/types'
import { ModalName } from 'wallet/src/telemetry/constants'
import { isAndroid } from 'wallet/src/utils/platform'
export const NFTS_TAB_DATA_DEPENDENCIES = [GQLQueries.NftsTab]
......@@ -34,7 +29,6 @@ export const NftsTab = memo(
ref
) {
const colors = useSporeColors()
const dispatch = useAppDispatch()
const insets = useDeviceInsets()
const navigation = useAppStackNavigation()
......@@ -42,14 +36,6 @@ export const NftsTab = memo(
containerProps?.contentContainerStyle
)
const onPressScan = (): void => {
// in case we received a pending session from a previous scan after closing modal
dispatch(removePendingSession())
dispatch(
openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr })
)
}
const renderNFTItem = useCallback(
(item: NFTItem) => {
const onPressNft = (): void => {
......@@ -95,7 +81,6 @@ export const NftsTab = memo(
renderNFTItem={renderNFTItem}
renderedInModal={renderedInModal}
onContentSizeChange={onContentSizeChange}
onPressEmptyState={onPressScan}
onRefresh={onRefresh}
onScroll={scrollHandler}
{...containerProps}
......
......@@ -71,9 +71,9 @@ export const TokensTab = memo(
// Show different empty state on external profile pages
return isExternalProfile ? (
<BaseCard.EmptyState
description={t('home.tokens.empty.description')}
description={t('When this wallet buys or receives tokens, they’ll appear here.')}
icon={<NoTokens />}
title={t('home.tokens.empty.title')}
title={t('No tokens yet')}
onPress={onPressAction}
/>
) : (
......
......@@ -37,8 +37,8 @@ export function WalletEmptyState(): JSX.Element {
const options: { [key in ActionOption]: ActionCardItem } = useMemo(
() => ({
[ActionOption.Buy]: {
title: t('home.tokens.empty.action.buy.title'),
blurb: t('home.tokens.empty.action.buy.description'),
title: t('Buy crypto'),
blurb: t('You’ll need ETH to get started. Buy with a card or bank.'),
elementName: ElementName.EmptyStateBuy,
icon: (
<IconContainer
......@@ -54,8 +54,8 @@ export function WalletEmptyState(): JSX.Element {
onPress: () => dispatch(openModal({ name: ModalName.FiatOnRamp })),
},
[ActionOption.Receive]: {
title: t('home.tokens.empty.action.receive.title'),
blurb: t('home.tokens.empty.action.receive.description'),
title: t('Receive funds'),
blurb: t('Transfer tokens from another wallet or crypto exchange.'),
elementName: ElementName.EmptyStateReceive,
icon: (
<IconContainer
......@@ -77,8 +77,8 @@ export function WalletEmptyState(): JSX.Element {
),
},
[ActionOption.Import]: {
title: t('home.tokens.empty.action.import.title'),
blurb: t('home.tokens.empty.action.import.description'),
title: t('Import wallet'),
blurb: t(`Enter this wallet’s recovery phrase to begin swapping and sending.`),
elementName: ElementName.EmptyStateImport,
icon: (
<IconContainer
......
......@@ -16,7 +16,7 @@ export function BackButtonView({ size, color, showButtonLabel }: Props): JSX.Ele
<Icons.RotatableChevron color={color ?? '$neutral2'} height={size} width={size} />
{showButtonLabel && (
<Text color="$neutral2" variant="subheading1">
{t('common.button.back')}
{t('Back')}
</Text>
)}
</Flex>
......
......@@ -78,22 +78,20 @@ export function SeedPhraseDisplay({
testID={ElementName.Next}
theme="secondary"
onPress={(): void => setShowSeedPhrase(!showSeedPhrase)}>
{showSeedPhrase
? t('setting.seedPhrase.action.hide')
: t('setting.seedPhrase.account.show')}
{showSeedPhrase ? t('Hide recovery phrase') : t('Show recovery phrase')}
</Button>
</Flex>
{showSeedPhraseViewWarningModal && (
<WarningModal
hideHandlebar
caption={t('setting.seedPhrase.warning.view.message')}
closeText={t('common.button.close')}
confirmText={t('common.button.view')}
caption={t('Anyone who knows your recovery phrase can access your wallet and funds.')}
closeText={t('Close')}
confirmText={t('View')}
isDismissible={false}
modalName={ModalName.ViewSeedPhraseWarning}
severity={WarningSeverity.High}
title={t('setting.seedPhrase.warning.view.title')}
title={t('View this in a private place')}
onCancel={(): void => {
setShowSeedPhraseViewWarningModal(false)
if (!showSeedPhrase) {
......@@ -105,10 +103,12 @@ export function SeedPhraseDisplay({
)}
{showScreenShotWarningModal && (
<WarningModal
caption={t('setting.seedPhrase.warning.screenshot.message')}
confirmText={t('common.button.close')}
caption={t(
'Anyone who gains access to your photos can access your wallet. We recommend that you write down your words instead.'
)}
confirmText={t('Close')}
modalName={ModalName.ScreenshotWarning}
title={t('setting.seedPhrase.warning.screenshot.title')}
title={t('Screenshots aren’t secure')}
onConfirm={(): void => setShowScreenShotWarningModal(false)}
/>
)}
......
......@@ -118,7 +118,7 @@ export function LongMarkdownText(props: LongMarkdownTextProps): JSX.Element {
testID="read-more-button"
variant="buttonLabel3"
onPress={toggleExpanded}>
{expanded ? t('common.longText.button.less') : t('common.longText.button.more')}
{expanded ? t('Read less') : t('Read more')}
</Text>
) : null}
</Flex>
......
......@@ -69,7 +69,7 @@ export function LongText(props: LongTextProps): JSX.Element {
testID="read-more-button"
variant="buttonLabel3"
onPress={(): void => setExpanded(!expanded)}>
{expanded ? t('common.longText.button.less') : t('common.longText.button.more')}
{expanded ? t('Read less') : t('Read more')}
</Text>
) : null}
</Flex>
......
......@@ -49,7 +49,7 @@ export function TooltipInfoButton({
<WarningModal
backgroundIconColor={backgroundIconColor}
caption={modalText}
closeText={closeText ?? t('common.button.close')}
closeText={closeText ?? t('Close')}
icon={modalIcon}
modalName={ModalName.TooltipContent}
title={modalTitle}
......
......@@ -127,7 +127,7 @@ export function ChangeUnitagModal({
dispatch(
pushNotification({
type: AppNotificationType.Success,
title: t('unitags.notification.username.title'),
title: t('Username changed'),
})
)
navigation.goBack()
......@@ -141,7 +141,7 @@ export function ChangeUnitagModal({
dispatch(
pushNotification({
type: AppNotificationType.Error,
errorMessage: t('unitags.notification.username.error'),
errorMessage: t('Could not change username. Try again later.'),
})
)
onClose()
......@@ -209,7 +209,7 @@ export function ChangeUnitagModal({
pt="$spacing12"
px="$spacing24">
<Text textAlign="center" variant="subheading1">
{t('unitags.editUsername.title')}
{t('Edit username')}
</Text>
<Flex
row
......@@ -249,7 +249,7 @@ export function ChangeUnitagModal({
py="$spacing12"
width="100%">
<Text color="$statusCritical" variant="body3">
{t('unitags.editUsername.warning.max')}
{t('You’ve reached the maximum number of 2 usernames changes.')}
</Text>
</Flex>
) : (
......@@ -260,7 +260,9 @@ export function ChangeUnitagModal({
py="$spacing12"
width="100%">
<Text color="$neutral2" variant="body3">
{t('unitags.editUsername.warning.default')}
{t(
'Once you change your username, you can never claim it again. You can only change it 2 times.'
)}
</Text>
</Flex>
)}
......@@ -283,7 +285,7 @@ export function ChangeUnitagModal({
<ActivityIndicator color={colors.sporeWhite.val} />
</Flex>
) : (
t('unitags.editUsername.button.confirm')
t('Save changes')
)}
</Button>
</Flex>
......@@ -314,17 +316,19 @@ function ChangeUnitagConfirmModal({
<Icons.AlertTriangle color="$statusCritical" size="$icon.24" />
</Flex>
<Text textAlign="center" variant="subheading1">
{t('unitags.editUsername.confirm.title')}
{t('Are you sure?')}
</Text>
<Text color="$neutral2" textAlign="center" variant="body2">
{t('unitags.editUsername.confirm.subtitle')}
{t(
'You’re about to change your username. Once you change it, you can never claim it again.'
)}
</Text>
<Flex centered row gap="$spacing12" pt="$spacing24">
<Button fill testID={ElementName.Remove} theme="secondary" onPress={onClose}>
{t('common.button.back')}
{t('Back')}
</Button>
<Button fill testID={ElementName.Remove} theme="detrimental" onPress={onChangeSubmit}>
{t('common.button.confirm')}
{t('Confirm')}
</Button>
</Flex>
</Flex>
......
......@@ -128,9 +128,9 @@ const ChoosePhotoOption = ({ type }: { type: PhotoAction }): JSX.Element => {
color={type === PhotoAction.RemovePhoto ? '$statusCritical' : '$neutral1'}
numberOfLines={1}
variant="buttonLabel2">
{type === PhotoAction.BrowseCameraRoll && t('unitags.choosePhoto.option.cameraRoll')}
{type === PhotoAction.BrowseNftsList && t('unitags.choosePhoto.option.nft')}
{type === PhotoAction.RemovePhoto && t('unitags.choosePhoto.option.remove')}
{type === PhotoAction.BrowseCameraRoll && t('Choose from camera roll')}
{type === PhotoAction.BrowseNftsList && t('Choose an NFT')}
{type === PhotoAction.RemovePhoto && t('Remove profile picture')}
</Text>
</Flex>
</Flex>
......
......@@ -39,7 +39,7 @@ export function DeleteUnitagModal({
dispatch(
pushNotification({
type: AppNotificationType.Error,
errorMessage: t('unitags.notification.delete.error'),
errorMessage: t('Could not delete username. Try again later.'),
})
)
onClose()
......@@ -66,7 +66,7 @@ export function DeleteUnitagModal({
dispatch(
pushNotification({
type: AppNotificationType.Success,
title: t('unitags.notification.delete.title'),
title: t('Username deleted'),
})
)
navigation.goBack()
......@@ -93,10 +93,12 @@ export function DeleteUnitagModal({
<Icons.AlertTriangle color="$statusCritical" size="$icon.24" />
</Flex>
<Text textAlign="center" variant="subheading1">
{t('unitags.delete.confirm.title')}
{t('Are you sure?')}
</Text>
<Text color="$neutral2" textAlign="center" variant="body2">
{t('unitags.delete.confirm.subtitle')}
{t(
'You’re about to delete your username and customizable profile details. You will not be able to reclaim it.'
)}
</Text>
<Flex centered row gap="$spacing12" pt="$spacing24">
<Button
......@@ -110,7 +112,7 @@ export function DeleteUnitagModal({
<ActivityIndicator color={colors.sporeWhite.val} />
</Flex>
) : (
t('common.button.delete')
t('Delete')
)}
</Button>
</Flex>
......
import React from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useTranslation } from 'react-i18next'
import { Keyboard, StyleProp, ViewStyle } from 'react-native'
import { useAppDispatch } from 'src/app/hooks'
import { navigate } from 'src/app/navigation/rootNavigation'
......@@ -110,24 +110,24 @@ export function UnitagBanner({
justifyContent="space-between"
onPress={onPressClaimNow}>
<Text color="$neutral2" variant="subheading2">
<Trans i18nKey="unitags.banner.title.compact">
<Text color="$accent1" variant="buttonLabel3">
Claim your {{ unitagDomain: UNITAG_SUFFIX_NO_LEADING_DOT }} username
{t('Claim your {{unitagSuffix}} username', {
unitagSuffix: UNITAG_SUFFIX_NO_LEADING_DOT,
})}
</Text>
and build out your customizable profile.
</Trans>
{t(' and build out your customizable profile.')}
</Text>
</Flex>
) : (
<Flex fill gap="$spacing16" justifyContent="space-between">
<Flex gap="$spacing4">
<Text variant="subheading2">
{t('unitags.banner.title.full', {
unitagDomain: UNITAG_SUFFIX_NO_LEADING_DOT,
{t('Claim your {{unitagSuffix}} username', {
unitagSuffix: UNITAG_SUFFIX_NO_LEADING_DOT,
})}
</Text>
<Text color="$neutral2" variant="body3">
{t('unitags.banner.subtitle')}
{t('Build a personalized web3 profile and easily share your address with friends.')}
</Text>
</Flex>
<Flex row gap="$spacing2">
......@@ -140,7 +140,7 @@ export function UnitagBanner({
testID={ElementName.Confirm}
onPress={onPressClaimNow}>
<Text color="white" variant="buttonLabel4">
{t('unitags.banner.button.claim')}
{t('Claim now')}
</Text>
</TouchableArea>
<TouchableArea
......@@ -151,7 +151,7 @@ export function UnitagBanner({
testID={ElementName.Cancel}
onPress={onPressMaybeLater}>
<Text color="$neutral2" variant="buttonLabel4">
{t('common.button.later')}
{t('Maybe later')}
</Text>
</TouchableArea>
</Flex>
......
......@@ -51,9 +51,11 @@ export function UnitagsIntroModal(): JSX.Element {
<BottomSheetModal name={ModalName.UnitagsIntro} onClose={onClose}>
<Flex gap="$spacing24" px="$spacing24" py="$spacing16">
<Flex alignItems="center" gap="$spacing12">
<Text variant="subheading1">{t('unitags.intro.title')}</Text>
<Text variant="subheading1">{t('Introducing usernames')}</Text>
<Text color="$neutral2" textAlign="center" variant="body2">
{t('unitags.intro.subtitle')}
{t(
'Say goodbye to 0x addresses. Usernames are readable names that make it easier to send and receive crypto.'
)}
</Text>
</Flex>
<Flex alignItems="center" maxHeight={105}>
......@@ -64,13 +66,13 @@ export function UnitagsIntroModal(): JSX.Element {
/>
</Flex>
<Flex gap="$spacing16" px="$spacing20">
<BodyItem Icon={Icons.UserSquare} title={t('unitags.intro.features.profile')} />
<BodyItem Icon={Icons.Ticket} title={t('unitags.intro.features.free')} />
<BodyItem Icon={Icons.Lightning} title={t('unitags.intro.features.ens')} />
<BodyItem Icon={Icons.UserSquare} title={t('Customizable profiles')} />
<BodyItem Icon={Icons.Ticket} title={t('Free to claim')} />
<BodyItem Icon={Icons.Lightning} title={t('Powered by ENS subdomains')} />
</Flex>
<Flex gap="$spacing8">
<Button size="medium" theme="primary" onPress={onPressClaimOneNow}>
{t('common.button.continue')}
{t('Continue')}
</Button>
</Flex>
<Flex $short={{ py: '$none', mx: '$spacing12' }} mx="$spacing24">
......
import React, { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Trans, 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('settings.setting.backup.password.error.weak')
errorText = t('Weak password')
} else if (error === PasswordErrors.PasswordsDoNotMatch) {
errorText = t('settings.setting.backup.password.error.mismatch')
errorText = t('Passwords do not match')
} else if (error) {
// use the upstream zxcvbn error message
errorText = error
......@@ -104,11 +104,7 @@ export function CloudBackupPasswordForm({
<Flex gap="$spacing8">
<PasswordInput
ref={passwordInputRef}
placeholder={
isConfirmation
? t('settings.setting.backup.password.placeholder.confirm')
: t('settings.setting.backup.password.placeholder.create')
}
placeholder={isConfirmation ? t('Confirm password') : t('Create password')}
returnKeyType="next"
value={password}
onChangeText={(newText: string): void => {
......@@ -124,42 +120,29 @@ export function CloudBackupPasswordForm({
<Flex centered row gap="$spacing12" px="$spacing16">
<Icons.DiamondExclamation color="$neutral2" size={iconSizes.icon20} />
<Text color="$neutral2" variant="body3">
{t('settings.setting.backup.password.disclaimer')}
{t(
'Uniswap Labs does not store your password and can’t recover it, so it’s crucial you remember it.'
)}
</Text>
</Flex>
)}
</Flex>
<Button disabled={isButtonDisabled} testID={ElementName.Next} onPress={onPressNext}>
{t('common.button.continue')}
{t('Continue')}
</Button>
</>
)
}
function PasswordStrengthText({ strength }: { strength: PasswordStrength }): JSX.Element {
const { t } = useTranslation()
const { color } = getPasswordStrengthTextAndColor(strength)
const { text, color } = getPasswordStrengthTextAndColor(strength)
const hasPassword = strength !== PasswordStrength.NONE
let strengthText: string = ''
switch (strength) {
case PasswordStrength.STRONG:
strengthText = t('settings.setting.backup.password.strong')
break
case PasswordStrength.MEDIUM:
strengthText = t('settings.setting.backup.password.medium')
break
case PasswordStrength.WEAK:
strengthText = t('settings.setting.backup.password.weak')
break
default:
break
}
return (
<Flex centered row opacity={hasPassword ? 1 : 0} pt="$spacing12" px="$spacing8">
<Text color={color} variant="body3">
{strengthText}
<Trans>This is a {text.toLowerCase()} password</Trans>
</Text>
</Flex>
)
......
......@@ -19,7 +19,7 @@ import {
} from 'wallet/src/features/wallet/accounts/editAccountSaga'
import { AccountType, BackupType } from 'wallet/src/features/wallet/accounts/types'
import { useAccount } from 'wallet/src/features/wallet/hooks'
import { getCloudProviderName } from 'wallet/src/utils/platform'
import { isAndroid } from 'wallet/src/utils/platform'
type Props = {
accountAddress: Address
......@@ -80,13 +80,17 @@ export function CloudBackupProcessingAnimation({
})
Alert.alert(
t('settings.setting.backup.error.title', { cloudProviderName: getCloudProviderName() }),
t('settings.setting.backup.error.message.full', {
cloudProviderName: getCloudProviderName(),
}),
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.'
),
[
{
text: t('common.button.ok'),
text: t('OK'),
style: 'default',
onPress: onErrorPress,
},
......@@ -114,9 +118,7 @@ export function CloudBackupProcessingAnimation({
<ActivityIndicator size="large" />
</Flex>
<Text variant="heading3">
{t('settings.setting.backup.status.inProgress', {
cloudProviderName: getCloudProviderName(),
})}
{isAndroid ? t('Backing up to Google Drive...') : t('Backing up to iCloud...')}
</Text>
</Flex>
) : (
......@@ -129,9 +131,7 @@ export function CloudBackupProcessingAnimation({
size={iconSize}
/>
<Text variant="heading3">
{t('settings.setting.backup.status.complete', {
cloudProviderName: getCloudProviderName(),
})}
{isAndroid ? t('Backed up to Google Drive') : t('Backed up to iCloud')}
</Text>
</Flex>
)
......
......@@ -7,15 +7,12 @@ import {
TransactionType,
} from 'wallet/src/features/transactions/types'
import { RootState } from 'wallet/src/state'
import { signerMnemonicAccount } from 'wallet/src/test/fixtures'
import { mockWalletPreloadedState } from 'wallet/src/test/mocks'
const account = signerMnemonicAccount()
import { account, mockWalletPreloadedState } from 'wallet/src/test/fixtures'
const MOCK_DATE_PROMPTED = Date.now()
const state = {
...mockWalletPreloadedState(),
...mockWalletPreloadedState,
wallet: {
appRatingProvidedMs: MOCK_DATE_PROMPTED,
},
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment